agentikit-opencode 0.0.10 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +34 -0
  2. package/index.ts +327 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,6 +19,40 @@ Add to your OpenCode config (`opencode.json`):
19
19
  | `agentikit_search` | Search the stash for tools, skills, commands, agents, and knowledge |
20
20
  | `agentikit_show` | Show a stash asset by its ref |
21
21
  | `agentikit_index` | Build or rebuild the search index |
22
+ | `agentikit_dispatch_agent` | Dispatch a stash `agent:*` into OpenCode using the stash prompt and metadata |
23
+ | `agentikit_exec_cmd` | Execute a stash `command:*` template in OpenCode via SDK session prompting |
24
+
25
+ ## Agent Dispatch
26
+
27
+ Use `agentikit_dispatch_agent` after retrieving an agent ref from `agentikit_search`.
28
+
29
+ Inputs:
30
+ - `ref` (optional): stash ref like `agent:coach.md`
31
+ - `query` (optional): resolve best matching stash agent when `ref` is omitted
32
+ - `task_prompt` (required): user task to run
33
+ - `dispatch_agent` (optional): OpenCode agent name (defaults to `general`)
34
+ - `as_subtask` (optional): create child session (defaults to `true`)
35
+
36
+ At least one of `ref` or `query` is required.
37
+
38
+ Behavior:
39
+ - Loads the stash agent via `akm show`
40
+ - Uses stash `prompt` verbatim as OpenCode `system`
41
+ - Applies stash `modelHint` when in `provider/model` format
42
+ - Applies stash `toolPolicy` when it maps to boolean tool flags
43
+
44
+ ## Command Execution
45
+
46
+ Use `agentikit_exec_cmd` to execute stash command templates through the OpenCode SDK.
47
+
48
+ Inputs:
49
+ - `ref` (optional): stash ref like `command:review.md`
50
+ - `query` (optional): resolve best matching stash command when `ref` is omitted
51
+ - `arguments` (optional): raw command arguments for `$ARGUMENTS`, `$1`, `$2`, etc.
52
+ - `dispatch_agent` (optional): OpenCode agent name (defaults to current agent)
53
+ - `as_subtask` (optional): create child session (defaults to `false`)
54
+
55
+ At least one of `ref` or `query` is required.
22
56
 
23
57
  ## Prerequisites
24
58
 
package/index.ts CHANGED
@@ -13,10 +13,179 @@ function runCli(args: string[]): string {
13
13
  }
14
14
  }
15
15
 
16
- export const AgentikitPlugin: Plugin = async ({ directory }) => ({
16
+ type CliError = { ok: false; error: string }
17
+ type AssetType = "tool" | "skill" | "command" | "agent" | "knowledge"
18
+
19
+ type ShowAgentResponse = {
20
+ type: "agent"
21
+ name: string
22
+ path: string
23
+ description?: string
24
+ prompt?: string
25
+ toolPolicy?: unknown
26
+ modelHint?: unknown
27
+ }
28
+
29
+ type ShowCommandResponse = {
30
+ type: "command"
31
+ name: string
32
+ path: string
33
+ description?: string
34
+ template?: string
35
+ }
36
+
37
+ type SearchHit = {
38
+ type: AssetType | "registry"
39
+ openRef?: string
40
+ }
41
+
42
+ type SearchResponse = {
43
+ hits?: SearchHit[]
44
+ }
45
+
46
+ function isShowAgentResponse(value: unknown): value is ShowAgentResponse {
47
+ return !!value
48
+ && typeof value === "object"
49
+ && (value as { type?: unknown }).type === "agent"
50
+ }
51
+
52
+ function isShowCommandResponse(value: unknown): value is ShowCommandResponse {
53
+ return !!value
54
+ && typeof value === "object"
55
+ && (value as { type?: unknown }).type === "command"
56
+ }
57
+
58
+ function parseCliJson<T>(raw: string): T | CliError {
59
+ try {
60
+ return JSON.parse(raw) as T
61
+ } catch {
62
+ return {
63
+ ok: false,
64
+ error: "Agentikit CLI returned non-JSON output",
65
+ }
66
+ }
67
+ }
68
+
69
+ function isCliError(value: unknown): value is CliError {
70
+ return !!value
71
+ && typeof value === "object"
72
+ && "ok" in value
73
+ && (value as { ok?: unknown }).ok === false
74
+ && "error" in value
75
+ }
76
+
77
+ function parseModelHint(modelHint: unknown): { providerID: string; modelID: string } | undefined {
78
+ if (typeof modelHint !== "string") return undefined
79
+ const [providerID, ...modelParts] = modelHint.split("/")
80
+ const modelID = modelParts.join("/")
81
+ if (!providerID || !modelID) return undefined
82
+ return { providerID, modelID }
83
+ }
84
+
85
+ function parseToolPolicy(toolPolicy: unknown): Record<string, boolean> | undefined {
86
+ if (!toolPolicy || typeof toolPolicy !== "object" || Array.isArray(toolPolicy)) return undefined
87
+ const result: Record<string, boolean> = {}
88
+ for (const [key, value] of Object.entries(toolPolicy as Record<string, unknown>)) {
89
+ if (typeof value === "boolean") result[key] = value
90
+ }
91
+ return Object.keys(result).length > 0 ? result : undefined
92
+ }
93
+
94
+ function extractText(parts: unknown): string {
95
+ if (!Array.isArray(parts)) return ""
96
+ const segments: string[] = []
97
+ for (const part of parts as Array<Record<string, unknown>>) {
98
+ if (part?.type === "text" && typeof part.text === "string") {
99
+ const text = part.text.trim()
100
+ if (text) segments.push(text)
101
+ }
102
+ }
103
+ return segments.join("\n\n")
104
+ }
105
+
106
+ function resolveRefInput(input: { ref?: string; query?: string }, type: AssetType): { ok: true; ref: string } | CliError {
107
+ if (input.ref && input.ref.trim()) {
108
+ return { ok: true, ref: input.ref.trim() }
109
+ }
110
+
111
+ const query = input.query?.trim()
112
+ if (!query) {
113
+ return { ok: false, error: "Provide either 'ref' or 'query'." }
114
+ }
115
+
116
+ const raw = runCli(["search", query, "--type", type, "--limit", "1", "--usage", "none", "--source", "local"])
117
+ const parsed = parseCliJson<SearchResponse>(raw)
118
+ if (isCliError(parsed)) return parsed
119
+
120
+ const openRef = parsed.hits?.[0]?.openRef
121
+ if (!openRef) {
122
+ return { ok: false, error: `No ${type} match found for query '${query}'.` }
123
+ }
124
+
125
+ return { ok: true, ref: openRef }
126
+ }
127
+
128
+ async function ensureTargetSessionID(input: {
129
+ useSubtask: boolean
130
+ context: { sessionID: string; directory: string }
131
+ title: string
132
+ client: PluginClient
133
+ }): Promise<{ ok: true; sessionID: string } | CliError> {
134
+ if (!input.useSubtask) return { ok: true, sessionID: input.context.sessionID }
135
+
136
+ const created = await input.client.session.create({
137
+ query: { directory: input.context.directory },
138
+ body: { parentID: input.context.sessionID, title: input.title },
139
+ })
140
+ if (created.error || !created.data?.id) {
141
+ const reason = created.error ? JSON.stringify(created.error) : "missing child session id"
142
+ return { ok: false, error: `Failed to create child session: ${reason}` }
143
+ }
144
+ return { ok: true, sessionID: created.data.id }
145
+ }
146
+
147
+ function splitArguments(raw: string): string[] {
148
+ if (!raw.trim()) return []
149
+ const args: string[] = []
150
+ const re = /"([^"]*)"|'([^']*)'|`([^`]*)`|(\S+)/g
151
+ let match: RegExpExecArray | null
152
+ while ((match = re.exec(raw)) !== null) {
153
+ args.push(match[1] ?? match[2] ?? match[3] ?? match[4] ?? "")
154
+ }
155
+ return args
156
+ }
157
+
158
+ function renderCommandTemplate(template: string, rawArguments: string): string {
159
+ const args = splitArguments(rawArguments)
160
+ return template
161
+ .replace(/\$ARGUMENTS/g, rawArguments)
162
+ .replace(/\$(\d+)/g, (_m, index: string) => args[Number(index) - 1] ?? "")
163
+ }
164
+
165
+ type PluginClient = {
166
+ session: {
167
+ create: (input: {
168
+ query: { directory: string }
169
+ body: { parentID: string; title: string }
170
+ }) => Promise<{ data?: { id?: string }; error?: unknown }>
171
+ prompt: (input: {
172
+ query: { directory: string }
173
+ path: { id: string }
174
+ body: {
175
+ agent: string
176
+ parts: Array<{ type: "text"; text: string }>
177
+ system?: string
178
+ model?: { providerID: string; modelID: string }
179
+ tools?: Record<string, boolean>
180
+ }
181
+ }) => Promise<{ data?: { parts?: unknown }; error?: unknown }>
182
+ }
183
+ }
184
+
185
+ export const AgentikitPlugin: Plugin = async ({ client }) => ({
17
186
  tool: {
18
187
  agentikit_search: tool({
19
- description: "Search the Agentikit stash for tools, skills, commands, agents, and knowledge.",
188
+ description: "Search your stash of tools, skills, commands, agents, and knowledge. Use this tool anytime you need to find resources for a task.",
20
189
  args: {
21
190
  query: tool.schema.string().describe("Case-insensitive substring search."),
22
191
  type: tool.schema
@@ -57,11 +226,166 @@ export const AgentikitPlugin: Plugin = async ({ directory }) => ({
57
226
  },
58
227
  }),
59
228
  agentikit_index: tool({
60
- description: "Build or rebuild the Agentikit search index. Scans stash directories, generates missing .stash.json metadata, and builds a semantic search index.",
229
+ description: "Build or rebuild the Agentikit stash index. Scans stash directories, generates missing .stash.json metadata, and builds a semantic search index.",
61
230
  args: {},
62
231
  async execute() {
63
232
  return runCli(["index"])
64
233
  },
65
234
  }),
235
+ agentikit_dispatch_agent: tool({
236
+ description: "Dispatch a stash agent by ref into a child OpenCode session, applying the agent prompt and metadata from agentikit_show.",
237
+ args: {
238
+ ref: tool.schema.string().optional().describe("Agent ref from agentikit_search (e.g. agent:my-agent.md)."),
239
+ query: tool.schema.string().optional().describe("If ref is omitted, resolve best matching stash agent for this query."),
240
+ task_prompt: tool.schema.string().describe("Task prompt sent to the dispatched OpenCode agent."),
241
+ dispatch_agent: tool.schema.string().optional().describe("OpenCode agent to run the task with. Defaults to 'general'."),
242
+ as_subtask: tool.schema.boolean().optional().describe("Run in child session with parent context. Defaults to true."),
243
+ },
244
+ async execute({ ref, query, task_prompt, dispatch_agent, as_subtask }, context) {
245
+ const resolved = resolveRefInput({ ref, query }, "agent")
246
+ if (!resolved.ok) return JSON.stringify(resolved)
247
+
248
+ const shownRaw = runCli(["show", resolved.ref])
249
+ const shown = parseCliJson<ShowAgentResponse | { type: string }>(shownRaw)
250
+ if (isCliError(shown)) {
251
+ return JSON.stringify(shown)
252
+ }
253
+
254
+ if (!isShowAgentResponse(shown)) {
255
+ return JSON.stringify({
256
+ ok: false,
257
+ error: `Ref ${ref} is not an agent payload from agentikit_show.`,
258
+ })
259
+ }
260
+
261
+ if (!shown.prompt || !shown.prompt.trim()) {
262
+ return JSON.stringify({
263
+ ok: false,
264
+ error: `Agent ${shown.name} is missing prompt content.`,
265
+ })
266
+ }
267
+
268
+ const useSubtask = as_subtask ?? true
269
+ const targetAgent = dispatch_agent ?? "general"
270
+ const model = parseModelHint(shown.modelHint)
271
+ const tools = parseToolPolicy(shown.toolPolicy)
272
+
273
+ const targetSession = await ensureTargetSessionID({
274
+ useSubtask,
275
+ context: { sessionID: context.sessionID, directory: context.directory },
276
+ title: `agentikit:${shown.name}`,
277
+ client: client as unknown as PluginClient,
278
+ })
279
+ if (!targetSession.ok) return JSON.stringify(targetSession)
280
+
281
+ const promptBody: {
282
+ agent: string
283
+ system: string
284
+ parts: Array<{ type: "text"; text: string }>
285
+ model?: { providerID: string; modelID: string }
286
+ tools?: Record<string, boolean>
287
+ } = {
288
+ agent: targetAgent,
289
+ system: shown.prompt,
290
+ parts: [{ type: "text", text: task_prompt }],
291
+ }
292
+ if (model) promptBody.model = model
293
+ if (tools) promptBody.tools = tools
294
+
295
+ const promptResponse = await client.session.prompt({
296
+ query: { directory: context.directory },
297
+ path: { id: targetSession.sessionID },
298
+ body: promptBody,
299
+ })
300
+
301
+ if (promptResponse.error || !promptResponse.data) {
302
+ const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
303
+ return JSON.stringify({
304
+ ok: false,
305
+ error: `Failed to dispatch prompt for ${resolved.ref}: ${reason}`,
306
+ })
307
+ }
308
+
309
+ return JSON.stringify({
310
+ ok: true,
311
+ ref: resolved.ref,
312
+ stashAgent: shown.name,
313
+ dispatchAgent: targetAgent,
314
+ usedSubtask: useSubtask,
315
+ sessionID: targetSession.sessionID,
316
+ model,
317
+ tools,
318
+ text: extractText(promptResponse.data.parts),
319
+ })
320
+ },
321
+ }),
322
+ agentikit_exec_cmd: tool({
323
+ description: "Execute a stash command template through the OpenCode SDK in the current or child session.",
324
+ args: {
325
+ ref: tool.schema.string().optional().describe("Command ref from agentikit_search (e.g. command:review.md)."),
326
+ query: tool.schema.string().optional().describe("If ref is omitted, resolve best matching stash command for this query."),
327
+ arguments: tool.schema.string().optional().describe("Command arguments used for $ARGUMENTS and positional placeholders ($1, $2, ...)."),
328
+ dispatch_agent: tool.schema.string().optional().describe("OpenCode agent to run the rendered command. Defaults to current agent."),
329
+ as_subtask: tool.schema.boolean().optional().describe("Run in child session with parent context. Defaults to false."),
330
+ },
331
+ async execute({ ref, query, arguments: commandArguments, dispatch_agent, as_subtask }, context) {
332
+ const resolved = resolveRefInput({ ref, query }, "command")
333
+ if (!resolved.ok) return JSON.stringify(resolved)
334
+
335
+ const shownRaw = runCli(["show", resolved.ref])
336
+ const shown = parseCliJson<ShowCommandResponse | { type: string }>(shownRaw)
337
+ if (isCliError(shown)) return JSON.stringify(shown)
338
+ if (!isShowCommandResponse(shown)) {
339
+ return JSON.stringify({ ok: false, error: `Ref ${resolved.ref} is not a command payload from agentikit_show.` })
340
+ }
341
+
342
+ const template = shown.template?.trim()
343
+ if (!template) {
344
+ return JSON.stringify({ ok: false, error: `Command ${shown.name} is missing template content.` })
345
+ }
346
+
347
+ const argsText = commandArguments ?? ""
348
+ const rendered = renderCommandTemplate(template, argsText)
349
+ const useSubtask = as_subtask ?? false
350
+ const targetAgent = dispatch_agent ?? context.agent
351
+
352
+ const targetSession = await ensureTargetSessionID({
353
+ useSubtask,
354
+ context: { sessionID: context.sessionID, directory: context.directory },
355
+ title: `agentikit:cmd:${shown.name}`,
356
+ client: client as unknown as PluginClient,
357
+ })
358
+ if (!targetSession.ok) return JSON.stringify(targetSession)
359
+
360
+ const promptResponse = await client.session.prompt({
361
+ query: { directory: context.directory },
362
+ path: { id: targetSession.sessionID },
363
+ body: {
364
+ agent: targetAgent,
365
+ parts: [{ type: "text", text: rendered }],
366
+ },
367
+ })
368
+
369
+ if (promptResponse.error || !promptResponse.data) {
370
+ const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
371
+ return JSON.stringify({
372
+ ok: false,
373
+ error: `Failed to execute command ${resolved.ref}: ${reason}`,
374
+ })
375
+ }
376
+
377
+ return JSON.stringify({
378
+ ok: true,
379
+ ref: resolved.ref,
380
+ stashCommand: shown.name,
381
+ dispatchAgent: targetAgent,
382
+ usedSubtask: useSubtask,
383
+ sessionID: targetSession.sessionID,
384
+ arguments: argsText,
385
+ renderedTemplate: rendered,
386
+ text: extractText(promptResponse.data.parts),
387
+ })
388
+ },
389
+ }),
66
390
  },
67
391
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentikit-opencode",
3
- "version": "0.0.10",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agentikit - search and show extension assets via the akm CLI.",
6
6
  "keywords": [