Browse Source

feat: add CLI arguments to agent create command for scripting (#5157)

Timor 10 months ago
parent
commit
b2057791aa
2 changed files with 171 additions and 87 deletions
  1. 1 1
      packages/opencode/src/agent/agent.ts
  2. 170 86
      packages/opencode/src/cli/cmd/agent.ts

+ 1 - 1
packages/opencode/src/agent/agent.ts

@@ -252,7 +252,7 @@ export namespace Agent {
         },
       },
       temperature: 0.3,
-      prompt: [
+      messages: [
         ...system.map(
           (item): ModelMessage => ({
             role: "system",

+ 170 - 86
packages/opencode/src/cli/cmd/agent.ts

@@ -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")
+        }
       },
     })
   },