agent.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import { cmd } from "./cmd"
  2. import * as prompts from "@clack/prompts"
  3. import { UI } from "../ui"
  4. import { Global } from "../../global"
  5. import { Agent } from "../../agent/agent"
  6. import { Provider } from "../../provider/provider"
  7. import path from "path"
  8. import fs from "fs/promises"
  9. import matter from "gray-matter"
  10. import { Instance } from "../../project/instance"
  11. import { EOL } from "os"
  12. import type { Argv } from "yargs"
  13. type AgentMode = "all" | "primary" | "subagent"
  14. const AVAILABLE_TOOLS = [
  15. "bash",
  16. "read",
  17. "write",
  18. "edit",
  19. "list",
  20. "glob",
  21. "grep",
  22. "webfetch",
  23. "task",
  24. "todowrite",
  25. "todoread",
  26. ]
  27. const AgentCreateCommand = cmd({
  28. command: "create",
  29. describe: "create a new agent",
  30. builder: (yargs: Argv) =>
  31. yargs
  32. .option("path", {
  33. type: "string",
  34. describe: "directory path to generate the agent file",
  35. })
  36. .option("description", {
  37. type: "string",
  38. describe: "what the agent should do",
  39. })
  40. .option("mode", {
  41. type: "string",
  42. describe: "agent mode",
  43. choices: ["all", "primary", "subagent"] as const,
  44. })
  45. .option("tools", {
  46. type: "string",
  47. describe: `comma-separated list of tools to enable (default: all). Available: "${AVAILABLE_TOOLS.join(", ")}"`,
  48. })
  49. .option("model", {
  50. type: "string",
  51. alias: ["m"],
  52. describe: "model to use in the format of provider/model",
  53. }),
  54. async handler(args) {
  55. await Instance.provide({
  56. directory: process.cwd(),
  57. async fn() {
  58. const cliPath = args.path
  59. const cliDescription = args.description
  60. const cliMode = args.mode as AgentMode | undefined
  61. const cliTools = args.tools
  62. const isFullyNonInteractive = cliPath && cliDescription && cliMode && cliTools !== undefined
  63. if (!isFullyNonInteractive) {
  64. UI.empty()
  65. prompts.intro("Create agent")
  66. }
  67. const project = Instance.project
  68. // Determine scope/path
  69. let targetPath: string
  70. if (cliPath) {
  71. targetPath = path.join(cliPath, "agent")
  72. } else {
  73. let scope: "global" | "project" = "global"
  74. if (project.vcs === "git") {
  75. const scopeResult = await prompts.select({
  76. message: "Location",
  77. options: [
  78. {
  79. label: "Current project",
  80. value: "project" as const,
  81. hint: Instance.worktree,
  82. },
  83. {
  84. label: "Global",
  85. value: "global" as const,
  86. hint: Global.Path.config,
  87. },
  88. ],
  89. })
  90. if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
  91. scope = scopeResult
  92. }
  93. targetPath = path.join(
  94. scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
  95. "agent",
  96. )
  97. }
  98. // Get description
  99. let description: string
  100. if (cliDescription) {
  101. description = cliDescription
  102. } else {
  103. const query = await prompts.text({
  104. message: "Description",
  105. placeholder: "What should this agent do?",
  106. validate: (x) => (x && x.length > 0 ? undefined : "Required"),
  107. })
  108. if (prompts.isCancel(query)) throw new UI.CancelledError()
  109. description = query
  110. }
  111. // Generate agent
  112. const spinner = prompts.spinner()
  113. spinner.start("Generating agent configuration...")
  114. const model = args.model ? Provider.parseModel(args.model) : undefined
  115. const generated = await Agent.generate({ description, model }).catch((error) => {
  116. spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
  117. if (isFullyNonInteractive) process.exit(1)
  118. throw new UI.CancelledError()
  119. })
  120. spinner.stop(`Agent ${generated.identifier} generated`)
  121. // Select tools
  122. let selectedTools: string[]
  123. if (cliTools !== undefined) {
  124. selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS
  125. } else {
  126. const result = await prompts.multiselect({
  127. message: "Select tools to enable",
  128. options: AVAILABLE_TOOLS.map((tool) => ({
  129. label: tool,
  130. value: tool,
  131. })),
  132. initialValues: AVAILABLE_TOOLS,
  133. })
  134. if (prompts.isCancel(result)) throw new UI.CancelledError()
  135. selectedTools = result
  136. }
  137. // Get mode
  138. let mode: AgentMode
  139. if (cliMode) {
  140. mode = cliMode
  141. } else {
  142. const modeResult = await prompts.select({
  143. message: "Agent mode",
  144. options: [
  145. {
  146. label: "All",
  147. value: "all" as const,
  148. hint: "Can function in both primary and subagent roles",
  149. },
  150. {
  151. label: "Primary",
  152. value: "primary" as const,
  153. hint: "Acts as a primary/main agent",
  154. },
  155. {
  156. label: "Subagent",
  157. value: "subagent" as const,
  158. hint: "Can be used as a subagent by other agents",
  159. },
  160. ],
  161. initialValue: "all" as const,
  162. })
  163. if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
  164. mode = modeResult
  165. }
  166. // Build tools config
  167. const tools: Record<string, boolean> = {}
  168. for (const tool of AVAILABLE_TOOLS) {
  169. if (!selectedTools.includes(tool)) {
  170. tools[tool] = false
  171. }
  172. }
  173. // Build frontmatter
  174. const frontmatter: {
  175. description: string
  176. mode: AgentMode
  177. tools?: Record<string, boolean>
  178. } = {
  179. description: generated.whenToUse,
  180. mode,
  181. }
  182. if (Object.keys(tools).length > 0) {
  183. frontmatter.tools = tools
  184. }
  185. // Write file
  186. const content = matter.stringify(generated.systemPrompt, frontmatter)
  187. const filePath = path.join(targetPath, `${generated.identifier}.md`)
  188. await fs.mkdir(targetPath, { recursive: true })
  189. const file = Bun.file(filePath)
  190. if (await file.exists()) {
  191. if (isFullyNonInteractive) {
  192. console.error(`Error: Agent file already exists: ${filePath}`)
  193. process.exit(1)
  194. }
  195. prompts.log.error(`Agent file already exists: ${filePath}`)
  196. throw new UI.CancelledError()
  197. }
  198. await Bun.write(filePath, content)
  199. if (isFullyNonInteractive) {
  200. console.log(filePath)
  201. } else {
  202. prompts.log.success(`Agent created: ${filePath}`)
  203. prompts.outro("Done")
  204. }
  205. },
  206. })
  207. },
  208. })
  209. const AgentListCommand = cmd({
  210. command: "list",
  211. describe: "list all available agents",
  212. async handler() {
  213. await Instance.provide({
  214. directory: process.cwd(),
  215. async fn() {
  216. const agents = await Agent.list()
  217. const sortedAgents = agents.sort((a, b) => {
  218. if (a.native !== b.native) {
  219. return a.native ? -1 : 1
  220. }
  221. return a.name.localeCompare(b.name)
  222. })
  223. for (const agent of sortedAgents) {
  224. process.stdout.write(`${agent.name} (${agent.mode})${EOL}`)
  225. }
  226. },
  227. })
  228. },
  229. })
  230. export const AgentCommand = cmd({
  231. command: "agent",
  232. describe: "manage agents",
  233. builder: (yargs) => yargs.command(AgentCreateCommand).command(AgentListCommand).demandCommand(),
  234. async handler() {},
  235. })