commit 388933c0d2556a2182bfa6a250e32c1dc06fd558 Author: Thomas Marchand Date: Thu Jan 8 17:00:38 2026 +0000 Initial library template with built-in library management tools diff --git a/README.md b/README.md new file mode 100644 index 0000000..430e430 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# OpenAgent Library Template + +This is the default template for OpenAgent configuration libraries. Fork this repository to create your own library with custom skills, commands, tools, rules, and agents. + +## Structure + +``` +library/ +├── skill/ # Reusable skills (SKILL.md + reference files) +├── command/ # Slash commands (markdown with YAML frontmatter) +├── tool/ # Custom TypeScript tools (@opencode-ai/plugin) +├── rule/ # Reusable instruction sets +├── agent/ # Custom agent configurations +└── mcp.json # MCP server configurations +``` + +## Built-in Library Tools + +This template includes tools for managing library content programmatically: + +### `library-skills.ts` +- `list_skills` - List all skills in the library +- `get_skill` - Get full skill content by name +- `save_skill` - Create or update a skill +- `delete_skill` - Delete a skill + +### `library-commands.ts` +- `list_commands` - List all slash commands +- `get_command` - Get command content +- `save_command` - Create or update a command +- `delete_command` - Delete a command +- `list_tools` - List custom tools +- `get_tool` - Get tool source code +- `save_tool` - Create or update a tool +- `delete_tool` - Delete a tool +- `list_rules` - List rules +- `get_rule` - Get rule content +- `save_rule` - Create or update a rule +- `delete_rule` - Delete a rule + +### `library-git.ts` +- `status` - Get library git status +- `sync` - Pull latest changes from remote +- `commit` - Commit changes with a message +- `push` - Push to remote +- `get_mcps` - Get MCP server configurations +- `save_mcps` - Save MCP configurations + +## Usage + +1. Fork this repository +2. Configure your OpenAgent instance with `LIBRARY_REMOTE=git@github.com:your-username/your-library.git` +3. Add skills, commands, and tools via the dashboard or using the library tools + +## Creating Skills + +Skills are directories containing a `SKILL.md` file with YAML frontmatter: + +```markdown +--- +name: my-skill +description: Description of what this skill does +--- + +Instructions for the agent on how to use this skill... +``` + +## Creating Commands + +Commands are markdown files with YAML frontmatter: + +```markdown +--- +description: What this command does +model: claude-sonnet-4-20250514 +--- + +Prompt template for the command... +``` + +## Creating Tools + +Tools are TypeScript files using `@opencode-ai/plugin`: + +```typescript +import { tool } from "@opencode-ai/plugin" + +export const my_tool = tool({ + description: "What this tool does", + args: { + param: tool.schema.string().describe("Parameter description"), + }, + async execute(args) { + // Tool implementation + return "Result" + }, +}) +``` diff --git a/agent/.gitkeep b/agent/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/command/.gitkeep b/command/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/rule/.gitkeep b/rule/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skill/.gitkeep b/skill/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tool/library-commands.ts b/tool/library-commands.ts new file mode 100644 index 0000000..e316351 --- /dev/null +++ b/tool/library-commands.ts @@ -0,0 +1,209 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Commands +// ───────────────────────────────────────────────────────────────────────────── + +export const list_commands = tool({ + description: "List all commands in the library (slash commands like /commit, /test)", + args: {}, + async execute() { + const commands = await apiRequest("/command") + if (!commands || commands.length === 0) { + return "No commands found in the library." + } + return commands.map((c: { name: string; description?: string }) => + `- /${c.name}: ${c.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_command = tool({ + description: "Get the full content of a command by name", + args: { + name: tool.schema.string().describe("The command name (without the leading /)"), + }, + async execute(args) { + const command = await apiRequest(`/command/${encodeURIComponent(args.name)}`) + let result = `# Command: /${command.name}\n\n` + result += `**Path:** ${command.path}\n` + if (command.description) result += `**Description:** ${command.description}\n` + result += `\n## Content\n\n${command.content}` + return result + }, +}) + +export const save_command = tool({ + description: "Create or update a command. Provide the full markdown content including YAML frontmatter.", + args: { + name: tool.schema.string().describe("The command name (without the leading /)"), + content: tool.schema.string().describe("Full markdown content with YAML frontmatter (description, model, subtask, agent)"), + }, + async execute(args) { + await apiRequest(`/command/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Command '/${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_command = tool({ + description: "Delete a command from the library", + args: { + name: tool.schema.string().describe("The command name to delete (without the leading /)"), + }, + async execute(args) { + await apiRequest(`/command/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Command '/${args.name}' deleted. Remember to commit and push your changes.` + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Library Tools +// ───────────────────────────────────────────────────────────────────────────── + +export const list_tools = tool({ + description: "List all custom tools in the library (TypeScript tool definitions)", + args: {}, + async execute() { + const tools = await apiRequest("/tool") + if (!tools || tools.length === 0) { + return "No custom tools found in the library." + } + return tools.map((t: { name: string; description?: string }) => + `- ${t.name}: ${t.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_tool = tool({ + description: "Get the full TypeScript code of a custom tool by name", + args: { + name: tool.schema.string().describe("The tool name"), + }, + async execute(args) { + const t = await apiRequest(`/tool/${encodeURIComponent(args.name)}`) + let result = `# Tool: ${t.name}\n\n` + result += `**Path:** ${t.path}\n` + if (t.description) result += `**Description:** ${t.description}\n` + result += `\n## Code\n\n\`\`\`typescript\n${t.content}\n\`\`\`` + return result + }, +}) + +export const save_tool = tool({ + description: "Create or update a custom tool in the library. Provide TypeScript code using the @opencode-ai/plugin tool() helper.", + args: { + name: tool.schema.string().describe("The tool name"), + content: tool.schema.string().describe("Full TypeScript code for the tool"), + }, + async execute(args) { + await apiRequest(`/tool/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Tool '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_tool = tool({ + description: "Delete a custom tool from the library", + args: { + name: tool.schema.string().describe("The tool name to delete"), + }, + async execute(args) { + await apiRequest(`/tool/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Tool '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Rules +// ───────────────────────────────────────────────────────────────────────────── + +export const list_rules = tool({ + description: "List all rules in the library (reusable instruction sets for agents)", + args: {}, + async execute() { + const rules = await apiRequest("/rule") + if (!rules || rules.length === 0) { + return "No rules found in the library." + } + return rules.map((r: { name: string; description?: string }) => + `- ${r.name}: ${r.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_rule = tool({ + description: "Get the full content of a rule by name", + args: { + name: tool.schema.string().describe("The rule name"), + }, + async execute(args) { + const rule = await apiRequest(`/rule/${encodeURIComponent(args.name)}`) + let result = `# Rule: ${rule.name}\n\n` + result += `**Path:** ${rule.path}\n` + if (rule.description) result += `**Description:** ${rule.description}\n` + result += `\n## Content\n\n${rule.content}` + return result + }, +}) + +export const save_rule = tool({ + description: "Create or update a rule in the library. Provide markdown content with optional YAML frontmatter.", + args: { + name: tool.schema.string().describe("The rule name"), + content: tool.schema.string().describe("Full markdown content, optionally with YAML frontmatter (description)"), + }, + async execute(args) { + await apiRequest(`/rule/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Rule '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_rule = tool({ + description: "Delete a rule from the library", + args: { + name: tool.schema.string().describe("The rule name to delete"), + }, + async execute(args) { + await apiRequest(`/rule/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Rule '${args.name}' deleted. Remember to commit and push your changes.` + }, +}) diff --git a/tool/library-git.ts b/tool/library-git.ts new file mode 100644 index 0000000..af07221 --- /dev/null +++ b/tool/library-git.ts @@ -0,0 +1,140 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Git Operations +// ───────────────────────────────────────────────────────────────────────────── + +export const status = tool({ + description: "Get the git status of the library: current branch, commits ahead/behind, and modified files", + args: {}, + async execute() { + const status = await apiRequest("/status") + let result = `# Library Git Status\n\n` + result += `**Branch:** ${status.branch || "unknown"}\n` + result += `**Remote:** ${status.remote || "not configured"}\n` + + if (status.commits_ahead !== undefined) { + result += `**Commits ahead:** ${status.commits_ahead}\n` + } + if (status.commits_behind !== undefined) { + result += `**Commits behind:** ${status.commits_behind}\n` + } + + if (status.modified_files && status.modified_files.length > 0) { + result += `\n## Modified Files\n` + result += status.modified_files.map((f: string) => `- ${f}`).join("\n") + } else { + result += `\nNo uncommitted changes.` + } + + return result + }, +}) + +export const sync = tool({ + description: "Pull latest changes from the library remote (git pull)", + args: {}, + async execute() { + await apiRequest("/sync", { method: "POST" }) + return "Library synced successfully. Latest changes pulled from remote." + }, +}) + +export const commit = tool({ + description: "Commit all changes in the library with a message", + args: { + message: tool.schema.string().describe("Commit message describing what changed"), + }, + async execute(args) { + await apiRequest("/commit", { + method: "POST", + body: JSON.stringify({ message: args.message }), + }) + return `Changes committed with message: "${args.message}"\n\nUse library-git_push to push to remote.` + }, +}) + +export const push = tool({ + description: "Push committed changes to the library remote (git push)", + args: {}, + async execute() { + await apiRequest("/push", { method: "POST" }) + return "Changes pushed to remote successfully." + }, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// MCP Servers +// ───────────────────────────────────────────────────────────────────────────── + +export const get_mcps = tool({ + description: "Get all MCP server configurations from the library", + args: {}, + async execute() { + const mcps = await apiRequest("/mcps") + if (!mcps || Object.keys(mcps).length === 0) { + return "No MCP servers configured in the library." + } + + let result = "# MCP Servers\n\n" + for (const [name, config] of Object.entries(mcps)) { + const c = config as { type: string; command?: string[]; url?: string; enabled?: boolean } + result += `## ${name}\n` + result += `- Type: ${c.type}\n` + if (c.type === "local" && c.command) { + result += `- Command: \`${c.command.join(" ")}\`\n` + } + if (c.type === "remote" && c.url) { + result += `- URL: ${c.url}\n` + } + result += `- Enabled: ${c.enabled !== false}\n\n` + } + return result + }, +}) + +export const save_mcps = tool({ + description: "Save MCP server configurations to the library. Provide the full JSON object with all servers.", + args: { + servers: tool.schema.string().describe("JSON object with MCP server configurations. Each server has type (local/remote), command/url, env/headers, and enabled fields."), + }, + async execute(args) { + let parsed: Record + try { + parsed = JSON.parse(args.servers) + } catch (e) { + throw new Error(`Invalid JSON: ${e}`) + } + + await apiRequest("/mcps", { + method: "PUT", + body: JSON.stringify(parsed), + }) + return "MCP server configurations saved successfully. Remember to commit and push your changes." + }, +}) diff --git a/tool/library-skills.ts b/tool/library-skills.ts new file mode 100644 index 0000000..1276fac --- /dev/null +++ b/tool/library-skills.ts @@ -0,0 +1,102 @@ +import { tool } from "@opencode-ai/plugin" + +// The Open Agent API URL - the backend handles library configuration internally +const API_BASE = "http://127.0.0.1:3000" + +async function apiRequest(endpoint: string, options: RequestInit = {}) { + const url = `${API_BASE}/api/library${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + + if (!response.ok) { + const text = await response.text() + throw new Error(`API error ${response.status}: ${text}`) + } + + const contentType = response.headers.get("content-type") + if (contentType?.includes("application/json")) { + return response.json() + } + return response.text() +} + +// ───────────────────────────────────────────────────────────────────────────── +// Skills +// ───────────────────────────────────────────────────────────────────────────── + +export const list_skills = tool({ + description: "List all skills in the library with their names and descriptions", + args: {}, + async execute() { + const skills = await apiRequest("/skill") + if (!skills || skills.length === 0) { + return "No skills found in the library." + } + return skills.map((s: { name: string; description?: string }) => + `- ${s.name}: ${s.description || "(no description)"}` + ).join("\n") + }, +}) + +export const get_skill = tool({ + description: "Get the full content of a skill by name, including SKILL.md and any additional files", + args: { + name: tool.schema.string().describe("The skill name (e.g., 'git-release')"), + }, + async execute(args) { + const skill = await apiRequest(`/skill/${encodeURIComponent(args.name)}`) + let result = `# Skill: ${skill.name}\n\n` + result += `**Path:** ${skill.path}\n` + if (skill.description) { + result += `**Description:** ${skill.description}\n` + } + result += `\n## SKILL.md Content\n\n${skill.content}` + + if (skill.files && skill.files.length > 0) { + result += "\n\n## Additional Files\n" + for (const file of skill.files) { + result += `\n### ${file.path}\n\n${file.content}` + } + } + + if (skill.references && skill.references.length > 0) { + result += "\n\n## Reference Files\n" + result += skill.references.map((r: string) => `- ${r}`).join("\n") + } + + return result + }, +}) + +export const save_skill = tool({ + description: "Create or update a skill in the library. Provide the full SKILL.md content including YAML frontmatter.", + args: { + name: tool.schema.string().describe("The skill name (lowercase, hyphens allowed, 1-64 chars)"), + content: tool.schema.string().describe("Full SKILL.md content including YAML frontmatter with name and description"), + }, + async execute(args) { + await apiRequest(`/skill/${encodeURIComponent(args.name)}`, { + method: "PUT", + body: JSON.stringify({ content: args.content }), + }) + return `Skill '${args.name}' saved successfully. Remember to commit and push your changes.` + }, +}) + +export const delete_skill = tool({ + description: "Delete a skill from the library", + args: { + name: tool.schema.string().describe("The skill name to delete"), + }, + async execute(args) { + await apiRequest(`/skill/${encodeURIComponent(args.name)}`, { + method: "DELETE", + }) + return `Skill '${args.name}' deleted. Remember to commit and push your changes.` + }, +})