akm-opencode 0.0.0 → 0.0.19

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