|
|
@@ -4,131 +4,215 @@ import { UI } from "../ui"
|
|
|
import { Global } from "../../global"
|
|
|
import { Agent } from "../../agent/agent"
|
|
|
import path from "path"
|
|
|
+import fs from "fs/promises"
|
|
|
import matter from "gray-matter"
|
|
|
import { Instance } from "../../project/instance"
|
|
|
import { EOL } from "os"
|
|
|
+import type { Argv } from "yargs"
|
|
|
+
|
|
|
+type AgentMode = "all" | "primary" | "subagent"
|
|
|
+
|
|
|
+const AVAILABLE_TOOLS = [
|
|
|
+ "bash",
|
|
|
+ "read",
|
|
|
+ "write",
|
|
|
+ "edit",
|
|
|
+ "list",
|
|
|
+ "glob",
|
|
|
+ "grep",
|
|
|
+ "webfetch",
|
|
|
+ "task",
|
|
|
+ "todowrite",
|
|
|
+ "todoread",
|
|
|
+]
|
|
|
|
|
|
const AgentCreateCommand = cmd({
|
|
|
command: "create",
|
|
|
describe: "create a new agent",
|
|
|
- async handler() {
|
|
|
+ builder: (yargs: Argv) =>
|
|
|
+ yargs
|
|
|
+ .option("path", {
|
|
|
+ type: "string",
|
|
|
+ describe: "directory path to generate the agent file",
|
|
|
+ })
|
|
|
+ .option("description", {
|
|
|
+ type: "string",
|
|
|
+ describe: "what the agent should do",
|
|
|
+ })
|
|
|
+ .option("mode", {
|
|
|
+ type: "string",
|
|
|
+ describe: "agent mode",
|
|
|
+ choices: ["all", "primary", "subagent"] as const,
|
|
|
+ })
|
|
|
+ .option("tools", {
|
|
|
+ type: "string",
|
|
|
+ describe: `comma-separated list of tools to enable (default: all). Available: "${AVAILABLE_TOOLS.join(", ")}"`,
|
|
|
+ }),
|
|
|
+ async handler(args) {
|
|
|
await Instance.provide({
|
|
|
directory: process.cwd(),
|
|
|
async fn() {
|
|
|
- UI.empty()
|
|
|
- prompts.intro("Create agent")
|
|
|
+ const cliPath = args.path
|
|
|
+ const cliDescription = args.description
|
|
|
+ const cliMode = args.mode as AgentMode | undefined
|
|
|
+ const cliTools = args.tools
|
|
|
+
|
|
|
+ const isFullyNonInteractive = cliPath && cliDescription && cliMode && cliTools !== undefined
|
|
|
+
|
|
|
+ if (!isFullyNonInteractive) {
|
|
|
+ UI.empty()
|
|
|
+ prompts.intro("Create agent")
|
|
|
+ }
|
|
|
+
|
|
|
const project = Instance.project
|
|
|
|
|
|
- let scope: "global" | "project" = "global"
|
|
|
- if (project.vcs === "git") {
|
|
|
- const scopeResult = await prompts.select({
|
|
|
- message: "Location",
|
|
|
- options: [
|
|
|
- {
|
|
|
- label: "Current project",
|
|
|
- value: "project" as const,
|
|
|
- hint: Instance.worktree,
|
|
|
- },
|
|
|
- {
|
|
|
- label: "Global",
|
|
|
- value: "global" as const,
|
|
|
- hint: Global.Path.config,
|
|
|
- },
|
|
|
- ],
|
|
|
- })
|
|
|
- if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
|
|
- scope = scopeResult
|
|
|
+ // Determine scope/path
|
|
|
+ let targetPath: string
|
|
|
+ if (cliPath) {
|
|
|
+ targetPath = path.join(cliPath, "agent")
|
|
|
+ } else {
|
|
|
+ let scope: "global" | "project" = "global"
|
|
|
+ if (project.vcs === "git") {
|
|
|
+ const scopeResult = await prompts.select({
|
|
|
+ message: "Location",
|
|
|
+ options: [
|
|
|
+ {
|
|
|
+ label: "Current project",
|
|
|
+ value: "project" as const,
|
|
|
+ hint: Instance.worktree,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "Global",
|
|
|
+ value: "global" as const,
|
|
|
+ hint: Global.Path.config,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ })
|
|
|
+ if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
|
|
+ scope = scopeResult
|
|
|
+ }
|
|
|
+ targetPath = path.join(
|
|
|
+ scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
|
|
|
+ "agent",
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- const query = await prompts.text({
|
|
|
- message: "Description",
|
|
|
- placeholder: "What should this agent do?",
|
|
|
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
|
- })
|
|
|
- if (prompts.isCancel(query)) throw new UI.CancelledError()
|
|
|
+ // Get description
|
|
|
+ let description: string
|
|
|
+ if (cliDescription) {
|
|
|
+ description = cliDescription
|
|
|
+ } else {
|
|
|
+ const query = await prompts.text({
|
|
|
+ message: "Description",
|
|
|
+ placeholder: "What should this agent do?",
|
|
|
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
|
+ })
|
|
|
+ if (prompts.isCancel(query)) throw new UI.CancelledError()
|
|
|
+ description = query
|
|
|
+ }
|
|
|
|
|
|
+ // Generate agent
|
|
|
const spinner = prompts.spinner()
|
|
|
-
|
|
|
spinner.start("Generating agent configuration...")
|
|
|
- const generated = await Agent.generate({ description: query }).catch((error) => {
|
|
|
+ const generated = await Agent.generate({ description }).catch((error) => {
|
|
|
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
|
|
+ if (isFullyNonInteractive) process.exit(1)
|
|
|
throw new UI.CancelledError()
|
|
|
})
|
|
|
spinner.stop(`Agent ${generated.identifier} generated`)
|
|
|
|
|
|
- const availableTools = [
|
|
|
- "bash",
|
|
|
- "read",
|
|
|
- "write",
|
|
|
- "edit",
|
|
|
- "list",
|
|
|
- "glob",
|
|
|
- "grep",
|
|
|
- "webfetch",
|
|
|
- "task",
|
|
|
- "todowrite",
|
|
|
- "todoread",
|
|
|
- ]
|
|
|
-
|
|
|
- const selectedTools = await prompts.multiselect({
|
|
|
- message: "Select tools to enable",
|
|
|
- options: availableTools.map((tool) => ({
|
|
|
- label: tool,
|
|
|
- value: tool,
|
|
|
- })),
|
|
|
- initialValues: availableTools,
|
|
|
- })
|
|
|
- if (prompts.isCancel(selectedTools)) throw new UI.CancelledError()
|
|
|
-
|
|
|
- const modeResult = await prompts.select({
|
|
|
- message: "Agent mode",
|
|
|
- options: [
|
|
|
- {
|
|
|
- label: "All",
|
|
|
- value: "all" as const,
|
|
|
- hint: "Can function in both primary and subagent roles",
|
|
|
- },
|
|
|
- {
|
|
|
- label: "Primary",
|
|
|
- value: "primary" as const,
|
|
|
- hint: "Acts as a primary/main agent",
|
|
|
- },
|
|
|
- {
|
|
|
- label: "Subagent",
|
|
|
- value: "subagent" as const,
|
|
|
- hint: "Can be used as a subagent by other agents",
|
|
|
- },
|
|
|
- ],
|
|
|
- initialValue: "all",
|
|
|
- })
|
|
|
- if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
|
|
|
+ // Select tools
|
|
|
+ let selectedTools: string[]
|
|
|
+ if (cliTools !== undefined) {
|
|
|
+ selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS
|
|
|
+ } else {
|
|
|
+ const result = await prompts.multiselect({
|
|
|
+ message: "Select tools to enable",
|
|
|
+ options: AVAILABLE_TOOLS.map((tool) => ({
|
|
|
+ label: tool,
|
|
|
+ value: tool,
|
|
|
+ })),
|
|
|
+ initialValues: AVAILABLE_TOOLS,
|
|
|
+ })
|
|
|
+ if (prompts.isCancel(result)) throw new UI.CancelledError()
|
|
|
+ selectedTools = result
|
|
|
+ }
|
|
|
|
|
|
+ // Get mode
|
|
|
+ let mode: AgentMode
|
|
|
+ if (cliMode) {
|
|
|
+ mode = cliMode
|
|
|
+ } else {
|
|
|
+ const modeResult = await prompts.select({
|
|
|
+ message: "Agent mode",
|
|
|
+ options: [
|
|
|
+ {
|
|
|
+ label: "All",
|
|
|
+ value: "all" as const,
|
|
|
+ hint: "Can function in both primary and subagent roles",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "Primary",
|
|
|
+ value: "primary" as const,
|
|
|
+ hint: "Acts as a primary/main agent",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "Subagent",
|
|
|
+ value: "subagent" as const,
|
|
|
+ hint: "Can be used as a subagent by other agents",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ initialValue: "all" as const,
|
|
|
+ })
|
|
|
+ if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
|
|
|
+ mode = modeResult
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build tools config
|
|
|
const tools: Record<string, boolean> = {}
|
|
|
- for (const tool of availableTools) {
|
|
|
+ for (const tool of AVAILABLE_TOOLS) {
|
|
|
if (!selectedTools.includes(tool)) {
|
|
|
tools[tool] = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const frontmatter: any = {
|
|
|
+ // Build frontmatter
|
|
|
+ const frontmatter: {
|
|
|
+ description: string
|
|
|
+ mode: AgentMode
|
|
|
+ tools?: Record<string, boolean>
|
|
|
+ } = {
|
|
|
description: generated.whenToUse,
|
|
|
- mode: modeResult,
|
|
|
+ mode,
|
|
|
}
|
|
|
if (Object.keys(tools).length > 0) {
|
|
|
frontmatter.tools = tools
|
|
|
}
|
|
|
|
|
|
+ // Write file
|
|
|
const content = matter.stringify(generated.systemPrompt, frontmatter)
|
|
|
- const filePath = path.join(
|
|
|
- scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
|
|
|
- `agent`,
|
|
|
- `${generated.identifier}.md`,
|
|
|
- )
|
|
|
+ const filePath = path.join(targetPath, `${generated.identifier}.md`)
|
|
|
+
|
|
|
+ await fs.mkdir(targetPath, { recursive: true })
|
|
|
+
|
|
|
+ const file = Bun.file(filePath)
|
|
|
+ if (await file.exists()) {
|
|
|
+ if (isFullyNonInteractive) {
|
|
|
+ console.error(`Error: Agent file already exists: ${filePath}`)
|
|
|
+ process.exit(1)
|
|
|
+ }
|
|
|
+ prompts.log.error(`Agent file already exists: ${filePath}`)
|
|
|
+ throw new UI.CancelledError()
|
|
|
+ }
|
|
|
|
|
|
await Bun.write(filePath, content)
|
|
|
|
|
|
- prompts.log.success(`Agent created: ${filePath}`)
|
|
|
- prompts.outro("Done")
|
|
|
+ if (isFullyNonInteractive) {
|
|
|
+ console.log(filePath)
|
|
|
+ } else {
|
|
|
+ prompts.log.success(`Agent created: ${filePath}`)
|
|
|
+ prompts.outro("Done")
|
|
|
+ }
|
|
|
},
|
|
|
})
|
|
|
},
|