akm-opencode 0.0.0 → 0.0.18

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 (4) hide show
  1. package/README.md +111 -5
  2. package/index.ts +762 -0
  3. package/package.json +29 -9
  4. package/index.js +0 -4
package/README.md CHANGED
@@ -1,9 +1,115 @@
1
1
  # akm-opencode
2
2
 
3
- > This is a placeholder package. The real thing is coming soon.
3
+ OpenCode plugin for the [Agentikit](https://github.com/itlackey/agentikit) CLI. Registers tools that let your AI agent **search**, **show**, and **manage** extension assets from stash directories and registries.
4
4
 
5
- **akm** — the Agent-i-Kit — is a package manager for AI agent capabilities:
6
- tools, skills, commands, agents, and knowledge documents.
5
+ ## Installation
7
6
 
8
- - GitHub: https://github.com/itlackey/agentikit
9
- - License: MPL-2.0
7
+ Add to your OpenCode config (`opencode.json`):
8
+
9
+ ```json
10
+ {
11
+ "plugin": ["akm-opencode"]
12
+ }
13
+ ```
14
+
15
+ ## Tools
16
+
17
+ | Tool | Description |
18
+ |------|-------------|
19
+ | `akm_search` | Search the local stash, the registry, or both for scripts, skills, commands, agents, and knowledge |
20
+ | `akm_registry_search` | Search configured registries for installable kits and optional asset-level hits |
21
+ | `akm_show` | Show a stash asset by its ref |
22
+ | `akm_index` | Build or rebuild the search index |
23
+ | `akm_agent` | Dispatch a stash `agent:*` into OpenCode using the stash prompt and metadata |
24
+ | `akm_cmd` | Execute a stash `command:*` template in OpenCode via SDK session prompting |
25
+ | `akm_add` | Install kits from npm, GitHub, git URLs, or local directories |
26
+ | `akm_list` | List installed registry kits |
27
+ | `akm_remove` | Remove an installed registry kit and reindex |
28
+ | `akm_update` | Update one installed kit or all installed kits |
29
+ | `akm_clone` | Clone an asset into the working stash or a custom destination for editing |
30
+ | `akm_config` | Get, set, unset, list, or inspect akm configuration (including `config path --all`) |
31
+ | `akm_run` | Execute a stash script using its `run` field |
32
+ | `akm_sources` | List all resolved stash search paths |
33
+ | `akm_upgrade` | Check for or install akm CLI updates |
34
+
35
+ ### Registry discovery
36
+
37
+ Use either:
38
+
39
+ - `akm_search` with `source: "registry"` or `source: "both"`
40
+ - `akm_registry_search` when you only want installable community kits
41
+
42
+ Registry hits include `id` and `action` fields. Use `assets: true` when you also want asset-level matches from registry v2 indexes.
43
+
44
+ ## Agent Dispatch
45
+
46
+ Use `akm_agent` after retrieving an agent ref from `akm_search`.
47
+
48
+ Inputs:
49
+ - `ref` (optional): stash ref like `agent:coach.md`
50
+ - `query` (optional): resolve best matching stash agent when `ref` is omitted
51
+ - `task_prompt` (required): user task to run
52
+ - `dispatch_agent` (optional): OpenCode agent name (defaults to `general`)
53
+ - `as_subtask` (optional): create child session (defaults to `true`)
54
+
55
+ At least one of `ref` or `query` is required.
56
+
57
+ Behavior:
58
+ - Loads the stash agent via `akm show`
59
+ - Uses stash `prompt` verbatim as OpenCode `system`
60
+ - Applies stash `modelHint` when in `provider/model` format
61
+ - Applies stash `toolPolicy` when it maps to boolean tool flags
62
+
63
+ ## Command Execution
64
+
65
+ Use `akm_cmd` to execute stash command templates through the OpenCode SDK.
66
+
67
+ Inputs:
68
+ - `ref` (optional): stash ref like `command:review.md`
69
+ - `query` (optional): resolve best matching stash command when `ref` is omitted
70
+ - `arguments` (optional): raw command arguments for `$ARGUMENTS`, `$1`, `$2`, etc.
71
+ - `dispatch_agent` (optional): OpenCode agent name (defaults to current agent)
72
+ - `as_subtask` (optional): create child session (defaults to `false`)
73
+
74
+ At least one of `ref` or `query` is required.
75
+
76
+ ## Prerequisites
77
+
78
+ The plugin prefers an existing `akm` on PATH. If `akm` is missing and `bun` is available, it will attempt `bun install -g akm-cli` automatically. It does not run the standalone shell installers automatically. If Bun is not available, install `akm` from the [agentikit repo](https://github.com/itlackey/agentikit).
79
+
80
+ ```sh
81
+ # macOS / Linux
82
+ curl -fsSL https://raw.githubusercontent.com/itlackey/agentikit/main/install.sh | bash
83
+ # PowerShell (Windows)
84
+ irm https://raw.githubusercontent.com/itlackey/agentikit/main/install.ps1 -OutFile install.ps1; ./install.ps1
85
+
86
+ # Or via Bun
87
+ bun install -g akm-cli
88
+ ```
89
+
90
+ ## Stash model
91
+
92
+ The stash directory is resolved automatically via a three-tier fallback: `AKM_STASH_DIR` env var (optional override) → `stashDir` in `config.json` → platform default. Set it persistently with:
93
+
94
+ ```sh
95
+ akm config set stashDir /abs/path/to/your-stash
96
+ ```
97
+
98
+ Expected layout:
99
+
100
+ ```
101
+ stash/
102
+ ├── scripts/ # executable scripts (.sh, .ts, .js, .ps1, .cmd, .bat, .py, .rb, .go, .pl, .php, .lua, .r, .swift, .kt)
103
+ ├── skills/ # skill directories containing SKILL.md
104
+ ├── commands/ # markdown files
105
+ ├── agents/ # markdown files
106
+ └── knowledge/ # markdown files
107
+ ```
108
+
109
+ Assets are resolved from three source types: **working** (local stash), **search paths** (additional dirs via `searchPaths` config), and **installed** (registry kits via `akm add`).
110
+
111
+ ## Docs
112
+
113
+ - [Agentikit CLI](https://github.com/itlackey/agentikit)
114
+ - [OpenCode Plugins](https://opencode.ai/docs/plugins/)
115
+ - [OpenCode Custom Tools](https://opencode.ai/docs/custom-tools/)
package/index.ts ADDED
@@ -0,0 +1,762 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin"
2
+ import { execFileSync, execSync } from "node:child_process"
3
+ import path from "node:path"
4
+
5
+ let resolvedAkmCommand = "akm"
6
+ let attemptedAutoInstall = false
7
+
8
+ function formatCliError(error: unknown): string {
9
+ if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") {
10
+ return "The 'akm' CLI was not found on PATH. Install it first from https://github.com/itlackey/agentikit."
11
+ }
12
+ return error instanceof Error ? error.message : String(error)
13
+ }
14
+
15
+ function getCommandStatus(command: string): "ok" | "missing" | "error" {
16
+ try {
17
+ execFileSync(command, ["--version"], {
18
+ encoding: "utf8",
19
+ timeout: 10_000,
20
+ })
21
+ return "ok"
22
+ } catch (error: unknown) {
23
+ if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") {
24
+ return "missing"
25
+ }
26
+ return "error"
27
+ }
28
+ }
29
+
30
+ function resolveAkmCommand(): string | CliError {
31
+ const currentStatus = getCommandStatus(resolvedAkmCommand)
32
+ if (currentStatus === "ok" || currentStatus === "error") return resolvedAkmCommand
33
+
34
+ if (attemptedAutoInstall) {
35
+ return { ok: false, error: "The 'akm' CLI was not found on PATH and automatic installation was unsuccessful." }
36
+ }
37
+ attemptedAutoInstall = true
38
+
39
+ try {
40
+ execFileSync("bun", ["--version"], {
41
+ encoding: "utf8",
42
+ timeout: 10_000,
43
+ })
44
+ } catch {
45
+ return {
46
+ ok: false,
47
+ error: "The 'akm' CLI was not found on PATH, and Bun is not available for automatic installation. Install akm from https://github.com/itlackey/agentikit.",
48
+ }
49
+ }
50
+
51
+ try {
52
+ execFileSync("bun", ["install", "-g", "akm-cli"], {
53
+ encoding: "utf8",
54
+ timeout: 120_000,
55
+ stdio: "pipe",
56
+ })
57
+
58
+ const globalBin = execFileSync("bun", ["pm", "bin", "-g"], {
59
+ encoding: "utf8",
60
+ timeout: 10_000,
61
+ }).trim()
62
+
63
+ const candidate = path.join(globalBin, process.platform === "win32" ? "akm.exe" : "akm")
64
+ if (getCommandStatus(candidate) === "ok") {
65
+ resolvedAkmCommand = candidate
66
+ return resolvedAkmCommand
67
+ }
68
+
69
+ if (getCommandStatus("akm") === "ok") {
70
+ resolvedAkmCommand = "akm"
71
+ return resolvedAkmCommand
72
+ }
73
+
74
+ return {
75
+ ok: false,
76
+ error: "Installed 'akm-cli' via Bun, but the 'akm' executable could not be resolved. Check your Bun global bin directory and PATH.",
77
+ }
78
+ } catch (error: unknown) {
79
+ return {
80
+ ok: false,
81
+ error: `Failed to auto-install 'akm-cli' via Bun: ${formatCliError(error)}`,
82
+ }
83
+ }
84
+ }
85
+
86
+ function runCli(args: string[]): string {
87
+ const command = resolveAkmCommand()
88
+ if (typeof command !== "string") return JSON.stringify(command)
89
+
90
+ try {
91
+ return execFileSync(command, [...args, "--format", "json"], {
92
+ encoding: "utf8",
93
+ timeout: 60_000,
94
+ })
95
+ } catch (error: unknown) {
96
+ const message = formatCliError(error)
97
+ return JSON.stringify({ ok: false, error: message })
98
+ }
99
+ }
100
+
101
+ type CliError = { ok: false; error: string }
102
+ type AssetType = "skill" | "command" | "agent" | "knowledge" | "script"
103
+
104
+ type ShowAgentResponse = {
105
+ type: "agent"
106
+ name: string
107
+ path: string
108
+ description?: string
109
+ prompt?: string
110
+ toolPolicy?: unknown
111
+ modelHint?: unknown
112
+ editable?: boolean
113
+ origin?: string | null
114
+ action?: string
115
+ editHint?: string
116
+ }
117
+
118
+ type ShowCommandResponse = {
119
+ type: "command"
120
+ name: string
121
+ path: string
122
+ description?: string
123
+ template?: string
124
+ editable?: boolean
125
+ agent?: string
126
+ origin?: string | null
127
+ action?: string
128
+ parameters?: string[]
129
+ editHint?: string
130
+ }
131
+
132
+ type ShowToolResponse = {
133
+ type: "tool" | "script"
134
+ name: string
135
+ path?: string
136
+ description?: string
137
+ run?: string
138
+ setup?: string
139
+ cwd?: string
140
+ editable?: boolean
141
+ origin?: string | null
142
+ action?: string
143
+ editHint?: string
144
+ }
145
+
146
+ type SearchHit = {
147
+ type: AssetType | "registry" | "registry-asset"
148
+ ref?: string
149
+ id?: string
150
+ editable?: boolean
151
+ name?: string
152
+ description?: string
153
+ score?: number
154
+ whyMatched?: string[]
155
+ run?: string
156
+ origin?: string | null
157
+ size?: string
158
+ action?: string
159
+ editHint?: string
160
+ curated?: boolean
161
+ }
162
+
163
+ type SearchResponse = {
164
+ hits?: SearchHit[]
165
+ source?: "local" | "registry" | "both"
166
+ stashDir?: string
167
+ timing?: { totalMs?: number; rankMs?: number; embedMs?: number }
168
+ warnings?: string[]
169
+ tip?: string
170
+ }
171
+
172
+ function isShowToolResponse(value: unknown): value is ShowToolResponse {
173
+ return !!value
174
+ && typeof value === "object"
175
+ && ((value as { type?: unknown }).type === "tool" || (value as { type?: unknown }).type === "script")
176
+ }
177
+
178
+ function isShowAgentResponse(value: unknown): value is ShowAgentResponse {
179
+ return !!value
180
+ && typeof value === "object"
181
+ && (value as { type?: unknown }).type === "agent"
182
+ }
183
+
184
+ function isShowCommandResponse(value: unknown): value is ShowCommandResponse {
185
+ return !!value
186
+ && typeof value === "object"
187
+ && (value as { type?: unknown }).type === "command"
188
+ }
189
+
190
+ function parseCliJson<T>(raw: string): T | CliError {
191
+ try {
192
+ return JSON.parse(raw) as T
193
+ } catch {
194
+ return {
195
+ ok: false,
196
+ error: "akm CLI returned non-JSON output",
197
+ }
198
+ }
199
+ }
200
+
201
+ function isCliError(value: unknown): value is CliError {
202
+ return !!value
203
+ && typeof value === "object"
204
+ && "ok" in value
205
+ && (value as { ok?: unknown }).ok === false
206
+ && "error" in value
207
+ }
208
+
209
+ function parseModelHint(modelHint: unknown): { providerID: string; modelID: string } | undefined {
210
+ if (typeof modelHint !== "string") return undefined
211
+ const [providerID, ...modelParts] = modelHint.split("/")
212
+ const modelID = modelParts.join("/")
213
+ if (!providerID || !modelID) return undefined
214
+ return { providerID, modelID }
215
+ }
216
+
217
+ function parseToolPolicy(toolPolicy: unknown): Record<string, boolean> | undefined {
218
+ const result: Record<string, boolean> = {}
219
+
220
+ const assign = (key: string, value: unknown) => {
221
+ const normalizedKey = key.trim().toLowerCase()
222
+ if (!normalizedKey) return
223
+ if (typeof value === "boolean") {
224
+ result[normalizedKey] = value
225
+ return
226
+ }
227
+ if (typeof value === "string") {
228
+ if (value === "allow") result[normalizedKey] = true
229
+ if (value === "deny") result[normalizedKey] = false
230
+ }
231
+ }
232
+
233
+ if (typeof toolPolicy === "string") {
234
+ assign(toolPolicy, true)
235
+ return Object.keys(result).length > 0 ? result : undefined
236
+ }
237
+
238
+ if (Array.isArray(toolPolicy)) {
239
+ for (const item of toolPolicy) {
240
+ if (typeof item === "string") assign(item, true)
241
+ }
242
+ return Object.keys(result).length > 0 ? result : undefined
243
+ }
244
+
245
+ if (!toolPolicy || typeof toolPolicy !== "object") return undefined
246
+
247
+ for (const [key, value] of Object.entries(toolPolicy as Record<string, unknown>)) {
248
+ assign(key, value)
249
+ }
250
+ return Object.keys(result).length > 0 ? result : undefined
251
+ }
252
+
253
+ function extractText(parts: unknown): string {
254
+ if (!Array.isArray(parts)) return ""
255
+ const segments: string[] = []
256
+ for (const part of parts as Array<Record<string, unknown>>) {
257
+ if (part?.type === "text" && typeof part.text === "string") {
258
+ const text = part.text.trim()
259
+ if (text) segments.push(text)
260
+ }
261
+ }
262
+ return segments.join("\n\n")
263
+ }
264
+
265
+ function resolveRefInput(input: { ref?: string; query?: string }, type: AssetType): { ok: true; ref: string } | CliError {
266
+ if (input.ref && input.ref.trim()) {
267
+ return { ok: true, ref: input.ref.trim() }
268
+ }
269
+
270
+ const query = input.query?.trim()
271
+ if (!query) {
272
+ return { ok: false, error: "Provide either 'ref' or 'query'." }
273
+ }
274
+
275
+ const raw = runCli(["search", query, "--type", type, "--limit", "1", "--detail", "normal", "--source", "local"])
276
+ const parsed = parseCliJson<SearchResponse>(raw)
277
+ if (isCliError(parsed)) return parsed
278
+
279
+ const ref = parsed.hits?.[0]?.ref
280
+ if (!ref) {
281
+ return { ok: false, error: `No ${type} match found for query '${query}'.` }
282
+ }
283
+
284
+ return { ok: true, ref }
285
+ }
286
+
287
+ async function ensureTargetSessionID(input: {
288
+ useSubtask: boolean
289
+ context: { sessionID: string; directory: string }
290
+ title: string
291
+ client: PluginClient
292
+ }): Promise<{ ok: true; sessionID: string } | CliError> {
293
+ if (!input.useSubtask) return { ok: true, sessionID: input.context.sessionID }
294
+
295
+ const created = await input.client.session.create({
296
+ query: { directory: input.context.directory },
297
+ body: { parentID: input.context.sessionID, title: input.title },
298
+ })
299
+ if (created.error || !created.data?.id) {
300
+ const reason = created.error ? JSON.stringify(created.error) : "missing child session id"
301
+ return { ok: false, error: `Failed to create child session: ${reason}` }
302
+ }
303
+ return { ok: true, sessionID: created.data.id }
304
+ }
305
+
306
+ function splitArguments(raw: string): string[] {
307
+ if (!raw.trim()) return []
308
+ const args: string[] = []
309
+ const re = /"([^"]*)"|'([^']*)'|`([^`]*)`|(\S+)/g
310
+ let match: RegExpExecArray | null
311
+ while ((match = re.exec(raw)) !== null) {
312
+ args.push(match[1] ?? match[2] ?? match[3] ?? match[4] ?? "")
313
+ }
314
+ return args
315
+ }
316
+
317
+ function renderCommandTemplate(template: string, rawArguments: string): string {
318
+ const args = splitArguments(rawArguments)
319
+ return template
320
+ .replace(/\$ARGUMENTS/g, rawArguments)
321
+ .replace(/\$(\d+)/g, (_m, index: string) => args[Number(index) - 1] ?? "")
322
+ }
323
+
324
+ function createSearchArgs(input: {
325
+ query: string
326
+ type?: AssetType | "any"
327
+ limit?: number
328
+ source?: "local" | "registry" | "both"
329
+ defaultSource?: "local" | "registry" | "both"
330
+ }): string[] {
331
+ const args = ["search", input.query]
332
+ if (input.type) args.push("--type", input.type)
333
+ if (input.limit) args.push("--limit", String(input.limit))
334
+ if (input.source) {
335
+ args.push("--source", input.source)
336
+ } else if (input.defaultSource) {
337
+ args.push("--source", input.defaultSource)
338
+ }
339
+ args.push("--detail", "normal")
340
+ return args
341
+ }
342
+
343
+ type PluginClient = {
344
+ session: {
345
+ create: (input: {
346
+ query: { directory: string }
347
+ body: { parentID: string; title: string }
348
+ }) => Promise<{ data?: { id?: string }; error?: unknown }>
349
+ prompt: (input: {
350
+ query: { directory: string }
351
+ path: { id: string }
352
+ body: {
353
+ agent: string
354
+ parts: Array<{ type: "text"; text: string }>
355
+ system?: string
356
+ model?: { providerID: string; modelID: string }
357
+ tools?: Record<string, boolean>
358
+ }
359
+ }) => Promise<{ data?: { parts?: unknown }; error?: unknown }>
360
+ }
361
+ }
362
+
363
+ export const AgentikitPlugin: Plugin = async ({ client }) => ({
364
+ tool: {
365
+ akm_search: tool({
366
+ description: "Search your local stash or the akm registry for scripts, skills, commands, agents, and knowledge. Use source='registry' or akm_registry_search for installable community kits.",
367
+ args: {
368
+ query: tool.schema.string().describe("Case-insensitive substring search."),
369
+ type: tool.schema
370
+ .enum(["skill", "command", "agent", "knowledge", "script", "any"])
371
+ .optional()
372
+ .describe("Optional type filter. Defaults to 'any'."),
373
+ limit: tool.schema.number().optional().describe("Maximum number of hits to return. Defaults to 20."),
374
+ source: tool.schema
375
+ .enum(["local", "registry", "both"])
376
+ .optional()
377
+ .describe("Search source. 'local' searches stash dirs, 'registry' searches npm/GitHub, 'both' searches all. Defaults to 'local'."),
378
+ },
379
+ async execute({ query, type, limit, source }) {
380
+ return runCli(createSearchArgs({ query, type, limit, source }))
381
+ },
382
+ }),
383
+ akm_registry_search: tool({
384
+ description: "Search configured akm registries only. Use this when you want installable kits without mixing in local stash results.",
385
+ args: {
386
+ query: tool.schema.string().describe("Search query for installable registry kits."),
387
+ type: tool.schema
388
+ .enum(["skill", "command", "agent", "knowledge", "script", "any"])
389
+ .optional()
390
+ .describe("Optional asset type filter. Defaults to 'any'."),
391
+ limit: tool.schema.number().optional().describe("Maximum number of registry hits to return. Defaults to 20."),
392
+ assets: tool.schema.boolean().optional().describe("Include asset-level results from registry index v2 payloads."),
393
+ },
394
+ async execute({ query, type, limit, assets }) {
395
+ const args = ["registry", "search", query]
396
+ if (limit) args.push("--limit", String(limit))
397
+ const assetTypeFilter = type && type !== "any" ? type : undefined
398
+ if (assets || assetTypeFilter) args.push("--assets")
399
+
400
+ const raw = runCli(args)
401
+ if (!assetTypeFilter) return raw
402
+
403
+ const parsed = parseCliJson<{
404
+ hits?: SearchHit[]
405
+ assetHits?: Array<SearchHit & { assetType?: AssetType }>
406
+ warnings?: string[]
407
+ query?: string
408
+ }>(raw)
409
+ if (isCliError(parsed)) return JSON.stringify(parsed)
410
+
411
+ return JSON.stringify({
412
+ ...parsed,
413
+ hits: [],
414
+ assetHits: (parsed.assetHits ?? []).filter((hit) => hit.assetType === assetTypeFilter),
415
+ })
416
+ },
417
+ }),
418
+ akm_show: tool({
419
+ description: "Show a stash asset by ref. For knowledge assets, use view_mode to retrieve specific content (toc, section, lines, frontmatter).",
420
+ args: {
421
+ ref: tool.schema.string().describe("Asset reference returned by akm_search."),
422
+ view_mode: tool.schema
423
+ .enum(["full", "toc", "frontmatter", "section", "lines"])
424
+ .optional()
425
+ .describe("View mode for knowledge assets. Defaults to 'full'. Ignored for other types."),
426
+ heading: tool.schema.string().optional()
427
+ .describe("Section heading to extract (required when view_mode is 'section')."),
428
+ start_line: tool.schema.number().optional()
429
+ .describe("Start line number, 1-based (for view_mode 'lines')."),
430
+ end_line: tool.schema.number().optional()
431
+ .describe("End line number, 1-based inclusive (for view_mode 'lines')."),
432
+ },
433
+ async execute({ ref, view_mode, heading, start_line, end_line }) {
434
+ const args = ["show", ref]
435
+ if (view_mode) {
436
+ args.push(view_mode)
437
+ if (view_mode === "section" && heading) args.push(heading)
438
+ if (view_mode === "lines") {
439
+ if (start_line != null) args.push(String(start_line))
440
+ if (end_line != null) args.push(String(end_line))
441
+ }
442
+ }
443
+ return runCli(args)
444
+ },
445
+ }),
446
+ akm_index: tool({
447
+ description: "Build or rebuild the akm stash index. Scans stash directories, generates missing .stash.json metadata, and builds a semantic search index.",
448
+ args: {},
449
+ async execute() {
450
+ return runCli(["index"])
451
+ },
452
+ }),
453
+ akm_add: tool({
454
+ description: "Install a kit from npm, GitHub, another git host, or a local directory. Installed kits become searchable alongside local assets.",
455
+ args: {
456
+ package_ref: tool.schema.string().describe("Package reference such as npm:@scope/kit, github:<owner>/<repo>, git+https://host/repo, or ./local/kit."),
457
+ },
458
+ async execute({ package_ref }) {
459
+ return runCli(["add", package_ref])
460
+ },
461
+ }),
462
+ akm_list: tool({
463
+ description: "List all kits installed from the registry.",
464
+ args: {},
465
+ async execute() {
466
+ return runCli(["list"])
467
+ },
468
+ }),
469
+ akm_remove: tool({
470
+ description: "Remove an installed registry kit by id or ref and reindex the stash.",
471
+ args: {
472
+ package_ref: tool.schema.string().describe("Installed kit id or ref, such as npm:@scope/kit or owner/repo."),
473
+ },
474
+ async execute({ package_ref }) {
475
+ return runCli(["remove", package_ref])
476
+ },
477
+ }),
478
+ akm_update: tool({
479
+ description: "Update one installed kit or all installed kits to the latest available version.",
480
+ args: {
481
+ package_ref: tool.schema.string().optional().describe("Installed kit id or ref to update."),
482
+ all: tool.schema.boolean().optional().describe("Update all installed kits."),
483
+ force: tool.schema.boolean().optional().describe("Force a fresh download even if the version is unchanged."),
484
+ },
485
+ async execute({ package_ref, all, force }) {
486
+ const args = ["update"]
487
+ const packageRef = package_ref?.trim()
488
+ if (all) {
489
+ args.push("--all")
490
+ } else if (packageRef) {
491
+ args.push(packageRef)
492
+ } else {
493
+ return JSON.stringify({ ok: false, error: "Provide 'package_ref' or set 'all' to true." })
494
+ }
495
+ if (force) args.push("--force")
496
+ return runCli(args)
497
+ },
498
+ }),
499
+ akm_clone: tool({
500
+ description: "Clone an asset from any source into the working stash or a custom destination for editing.",
501
+ args: {
502
+ ref: tool.schema.string().describe("Asset ref to clone, including optional origin such as npm:@scope/pkg//script:deploy.sh."),
503
+ name: tool.schema.string().optional().describe("Optional new asset name."),
504
+ dest: tool.schema.string().optional().describe("Optional destination directory. The type subdirectory is appended automatically by akm."),
505
+ force: tool.schema.boolean().optional().describe("Overwrite the destination if it already exists."),
506
+ },
507
+ async execute({ ref, name, dest, force }) {
508
+ const args = ["clone", ref]
509
+ if (name) args.push("--name", name)
510
+ if (dest) args.push("--dest", dest)
511
+ if (force) args.push("--force")
512
+ return runCli(args)
513
+ },
514
+ }),
515
+ akm_agent: tool({
516
+ description: "Dispatch a stash agent by ref into a child OpenCode session, applying the agent prompt and metadata from akm_show.",
517
+ args: {
518
+ ref: tool.schema.string().optional().describe("Agent ref from akm_search (e.g. agent:my-agent.md)."),
519
+ query: tool.schema.string().optional().describe("If ref is omitted, resolve best matching stash agent for this query."),
520
+ task_prompt: tool.schema.string().describe("Task prompt sent to the dispatched OpenCode agent."),
521
+ dispatch_agent: tool.schema.string().optional().describe("OpenCode agent to run the task with. Defaults to 'general'."),
522
+ as_subtask: tool.schema.boolean().optional().describe("Run in child session with parent context. Defaults to true."),
523
+ },
524
+ async execute({ ref, query, task_prompt, dispatch_agent, as_subtask }, context) {
525
+ const resolved = resolveRefInput({ ref, query }, "agent")
526
+ if (!resolved.ok) return JSON.stringify(resolved)
527
+
528
+ const shownRaw = runCli(["show", resolved.ref])
529
+ const shown = parseCliJson<ShowAgentResponse | { type: string }>(shownRaw)
530
+ if (isCliError(shown)) {
531
+ return JSON.stringify(shown)
532
+ }
533
+
534
+ if (!isShowAgentResponse(shown)) {
535
+ return JSON.stringify({
536
+ ok: false,
537
+ error: `Ref ${ref} is not an agent payload from akm_show.`,
538
+ })
539
+ }
540
+
541
+ if (!shown.prompt || !shown.prompt.trim()) {
542
+ return JSON.stringify({
543
+ ok: false,
544
+ error: `Agent ${shown.name} is missing prompt content.`,
545
+ })
546
+ }
547
+
548
+ const useSubtask = as_subtask ?? true
549
+ const targetAgent = dispatch_agent ?? "general"
550
+ const model = parseModelHint(shown.modelHint)
551
+ const tools = parseToolPolicy(shown.toolPolicy)
552
+
553
+ const targetSession = await ensureTargetSessionID({
554
+ useSubtask,
555
+ context: { sessionID: context.sessionID, directory: context.directory },
556
+ title: `akm:${shown.name}`,
557
+ client: client as unknown as PluginClient,
558
+ })
559
+ if (!targetSession.ok) return JSON.stringify(targetSession)
560
+
561
+ const promptBody: {
562
+ agent: string
563
+ system: string
564
+ parts: Array<{ type: "text"; text: string }>
565
+ model?: { providerID: string; modelID: string }
566
+ tools?: Record<string, boolean>
567
+ } = {
568
+ agent: targetAgent,
569
+ system: shown.prompt,
570
+ parts: [{ type: "text", text: task_prompt }],
571
+ }
572
+ if (model) promptBody.model = model
573
+ if (tools) promptBody.tools = tools
574
+
575
+ const promptResponse = await client.session.prompt({
576
+ query: { directory: context.directory },
577
+ path: { id: targetSession.sessionID },
578
+ body: promptBody,
579
+ })
580
+
581
+ if (promptResponse.error || !promptResponse.data) {
582
+ const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
583
+ return JSON.stringify({
584
+ ok: false,
585
+ error: `Failed to dispatch prompt for ${resolved.ref}: ${reason}`,
586
+ })
587
+ }
588
+
589
+ return JSON.stringify({
590
+ ok: true,
591
+ ref: resolved.ref,
592
+ stashAgent: shown.name,
593
+ dispatchAgent: targetAgent,
594
+ usedSubtask: useSubtask,
595
+ sessionID: targetSession.sessionID,
596
+ model,
597
+ tools,
598
+ text: extractText(promptResponse.data.parts),
599
+ })
600
+ },
601
+ }),
602
+ akm_cmd: tool({
603
+ description: "Execute a stash command template through the OpenCode SDK in the current or child session.",
604
+ args: {
605
+ ref: tool.schema.string().optional().describe("Command ref from akm_search (e.g. command:review.md)."),
606
+ query: tool.schema.string().optional().describe("If ref is omitted, resolve best matching stash command for this query."),
607
+ arguments: tool.schema.string().optional().describe("Command arguments used for $ARGUMENTS and positional placeholders ($1, $2, ...)."),
608
+ dispatch_agent: tool.schema.string().optional().describe("OpenCode agent to run the rendered command. Defaults to current agent."),
609
+ as_subtask: tool.schema.boolean().optional().describe("Run in child session with parent context. Defaults to false."),
610
+ },
611
+ async execute({ ref, query, arguments: commandArguments, dispatch_agent, as_subtask }, context) {
612
+ const resolved = resolveRefInput({ ref, query }, "command")
613
+ if (!resolved.ok) return JSON.stringify(resolved)
614
+
615
+ const shownRaw = runCli(["show", resolved.ref])
616
+ const shown = parseCliJson<ShowCommandResponse | { type: string }>(shownRaw)
617
+ if (isCliError(shown)) return JSON.stringify(shown)
618
+ if (!isShowCommandResponse(shown)) {
619
+ return JSON.stringify({ ok: false, error: `Ref ${resolved.ref} is not a command payload from akm_show.` })
620
+ }
621
+
622
+ const template = shown.template?.trim()
623
+ if (!template) {
624
+ return JSON.stringify({ ok: false, error: `Command ${shown.name} is missing template content.` })
625
+ }
626
+
627
+ const argsText = commandArguments ?? ""
628
+ const rendered = renderCommandTemplate(template, argsText)
629
+ const useSubtask = as_subtask ?? false
630
+ const targetAgent = dispatch_agent ?? context.agent
631
+
632
+ const targetSession = await ensureTargetSessionID({
633
+ useSubtask,
634
+ context: { sessionID: context.sessionID, directory: context.directory },
635
+ title: `akm:cmd:${shown.name}`,
636
+ client: client as unknown as PluginClient,
637
+ })
638
+ if (!targetSession.ok) return JSON.stringify(targetSession)
639
+
640
+ const promptResponse = await client.session.prompt({
641
+ query: { directory: context.directory },
642
+ path: { id: targetSession.sessionID },
643
+ body: {
644
+ agent: targetAgent,
645
+ parts: [{ type: "text", text: rendered }],
646
+ },
647
+ })
648
+
649
+ if (promptResponse.error || !promptResponse.data) {
650
+ const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
651
+ return JSON.stringify({
652
+ ok: false,
653
+ error: `Failed to execute command ${resolved.ref}: ${reason}`,
654
+ })
655
+ }
656
+
657
+ return JSON.stringify({
658
+ ok: true,
659
+ ref: resolved.ref,
660
+ stashCommand: shown.name,
661
+ dispatchAgent: targetAgent,
662
+ usedSubtask: useSubtask,
663
+ sessionID: targetSession.sessionID,
664
+ arguments: argsText,
665
+ renderedTemplate: rendered,
666
+ text: extractText(promptResponse.data.parts),
667
+ })
668
+ },
669
+ }),
670
+ akm_config: tool({
671
+ description: "View or update akm configuration settings.",
672
+ args: {
673
+ action: tool.schema.enum(["get", "set", "list", "unset", "path"]).describe("Config action: get, set, list, unset, or path."),
674
+ key: tool.schema.string().optional().describe("Config key (required for get/set)."),
675
+ value: tool.schema.string().optional().describe("Config value (required for set)."),
676
+ all: tool.schema.boolean().optional().describe("When action is 'path', include config, stash, cache, and index paths."),
677
+ },
678
+ async execute({ action, key, value, all }) {
679
+ const args = ["config", action]
680
+ if (key) args.push(key)
681
+ if (value) args.push(value)
682
+ if (action === "path" && all) args.push("--all")
683
+ return runCli(args)
684
+ },
685
+ }),
686
+ akm_run: tool({
687
+ description: "Execute a stash script by ref. Resolves via search, fetches metadata via show, and runs the run command.",
688
+ args: {
689
+ ref: tool.schema.string().optional().describe("Script ref from akm_search (e.g. script:deploy.sh)."),
690
+ query: tool.schema.string().optional().describe("If ref is omitted, resolve best matching stash script for this query."),
691
+ args: tool.schema.string().optional().describe("Arguments to append to the run command."),
692
+ },
693
+ async execute({ ref, query, args: runArgs }) {
694
+ const resolved = resolveRefInput({ ref, query }, "script")
695
+ if (!resolved.ok) return JSON.stringify(resolved)
696
+
697
+ const shownRaw = runCli(["show", resolved.ref])
698
+ const shown = parseCliJson<ShowToolResponse | { type: string }>(shownRaw)
699
+ if (isCliError(shown)) return JSON.stringify(shown)
700
+
701
+ if (!isShowToolResponse(shown)) {
702
+ return JSON.stringify({
703
+ ok: false,
704
+ error: `Ref ${resolved.ref} is not a script payload from akm_show.`,
705
+ })
706
+ }
707
+
708
+ if (!shown.run || !shown.run.trim()) {
709
+ return JSON.stringify({
710
+ ok: false,
711
+ error: `Script ${shown.name} is missing run command.`,
712
+ })
713
+ }
714
+
715
+ let cmd = shown.run
716
+ if (runArgs && runArgs.trim()) {
717
+ cmd = `${cmd} ${runArgs.trim()}`
718
+ }
719
+
720
+ try {
721
+ const output = execSync(cmd, {
722
+ encoding: "utf8",
723
+ timeout: 120_000,
724
+ })
725
+ return JSON.stringify({
726
+ ok: true,
727
+ ref: resolved.ref,
728
+ script: shown.name,
729
+ run: cmd,
730
+ output,
731
+ })
732
+ } catch (error: unknown) {
733
+ const message = error instanceof Error ? error.message : String(error)
734
+ return JSON.stringify({
735
+ ok: false,
736
+ error: `Failed to execute run command for ${shown.name}: ${message}`,
737
+ })
738
+ }
739
+ },
740
+ }),
741
+ akm_sources: tool({
742
+ description: "List all resolved stash search paths and their status.",
743
+ args: {},
744
+ async execute() {
745
+ return runCli(["sources"])
746
+ },
747
+ }),
748
+ akm_upgrade: tool({
749
+ description: "Check for or install akm CLI updates.",
750
+ args: {
751
+ check: tool.schema.boolean().optional().describe("Only check for updates without installing."),
752
+ force: tool.schema.boolean().optional().describe("Force upgrade even if already on latest version."),
753
+ },
754
+ async execute({ check, force }) {
755
+ const args = ["upgrade"]
756
+ if (check) args.push("--check")
757
+ if (force) args.push("--force")
758
+ return runCli(args)
759
+ },
760
+ }),
761
+ },
762
+ })
package/package.json CHANGED
@@ -1,17 +1,37 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.0.0",
4
- "description": "Placeholder — akm (the Agent-i-Kit) is coming soon. https://github.com/itlackey/agentikit",
5
-
6
- "keywords": ["akm", "agent-i-kit", "ai-agent", "cli", "package-manager"],
7
- "license": "MPL-2.0",
8
- "author": "itlackey",
3
+ "version": "0.0.18",
4
+ "type": "module",
5
+ "description": "OpenCode plugin for Agentikit - search and show extension assets via the akm CLI.",
6
+ "keywords": [
7
+ "opencode",
8
+ "opencode-ai",
9
+ "opencode-plugin",
10
+ "opencode-extensions",
11
+ "agentikit",
12
+ "ai-agent",
13
+ "developer-tools",
14
+ "plugin"
15
+ ],
16
+ "homepage": "https://github.com/itlackey/akm-plugins#readme",
9
17
  "repository": {
10
18
  "type": "git",
11
- "url": "git+https://github.com/itlackey/agentikit.git"
19
+ "url": "git+https://github.com/itlackey/akm-plugins.git",
20
+ "directory": "opencode"
12
21
  },
13
- "homepage": "https://github.com/itlackey/agentikit#readme",
14
22
  "bugs": {
15
- "url": "https://github.com/itlackey/agentikit/issues"
23
+ "url": "https://github.com/itlackey/akm-plugins/issues"
24
+ },
25
+ "license": "MPL-2.0",
26
+ "files": [
27
+ "index.ts",
28
+ "README.md"
29
+ ],
30
+ "main": "./index.ts",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@opencode-ai/plugin": "^1.2.20"
16
36
  }
17
37
  }
package/index.js DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- console.log("akm (the Agent-i-Kit) is coming soon.");
3
- console.log("See: https://github.com/itlackey/agentikit");
4
- process.exit(0);