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.
- package/README.md +34 -0
- package/index.ts +327 -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
|
-
|
|
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
|
|
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
|
|
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
|
})
|