akm-opencode 0.0.21 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +10 -6
  2. package/index.ts +214 -58
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,15 +23,19 @@ Add to your OpenCode config (`opencode.json`):
23
23
  | `akm_agent` | Dispatch a stash `agent:*` into OpenCode using the stash prompt and metadata |
24
24
  | `akm_cmd` | Execute a stash `command:*` template in OpenCode via SDK session prompting |
25
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 |
26
+ | `akm_list` | List configured AKM sources |
27
+ | `akm_remove` | Remove a configured AKM source and reindex |
28
+ | `akm_update` | Update one managed source or all managed sources |
29
29
  | `akm_clone` | Clone an asset into the working stash or a custom destination for editing |
30
+ | `akm_remember` | Record a memory in the default stash |
31
+ | `akm_feedback` | Record positive or negative feedback for a stash asset |
30
32
  | `akm_config` | Get, set, unset, list, or inspect akm configuration (including `config path --all`) |
31
33
  | `akm_run` | Execute a stash script using its `run` field |
32
- | `akm_sources` | List all resolved stash search paths |
34
+ | `akm_sources` | Backward-compatible alias that lists configured AKM sources |
33
35
  | `akm_upgrade` | Check for or install akm CLI updates |
34
36
 
37
+ The plugin also uses the OpenCode `chat.message` and `tool.execute.after` hooks to record user/system feedback activity and memory usage in OpenCode app logs when relevant.
38
+
35
39
  ### Registry discovery
36
40
 
37
41
  Use either:
@@ -75,7 +79,7 @@ At least one of `ref` or `query` is required.
75
79
 
76
80
  ## Prerequisites
77
81
 
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).
82
+ When the plugin loads, it runs `bun install -g akm-cli@latest` so it always picks up the latest published npm package. It then prefers the Bun-installed binary and falls back to an existing `akm` on PATH when needed. It does not run the standalone shell installers automatically.
79
83
 
80
84
  ```sh
81
85
  # macOS / Linux
@@ -84,7 +88,7 @@ curl -fsSL https://raw.githubusercontent.com/itlackey/agentikit/main/install.sh
84
88
  irm https://raw.githubusercontent.com/itlackey/agentikit/main/install.ps1 -OutFile install.ps1; ./install.ps1
85
89
 
86
90
  # Or via Bun
87
- bun install -g akm-cli
91
+ bun install -g akm-cli@latest
88
92
  ```
89
93
 
90
94
  ## Stash model
package/index.ts CHANGED
@@ -3,7 +3,7 @@ import { execFileSync, execSync } from "node:child_process"
3
3
  import path from "node:path"
4
4
 
5
5
  let resolvedAkmCommand = "akm"
6
- let attemptedAutoInstall = false
6
+ const autoInstallPackageRef = "akm-cli@latest"
7
7
 
8
8
  type LogLevel = "debug" | "info" | "warn" | "error"
9
9
 
@@ -77,59 +77,84 @@ function getCommandStatus(command: string): "ok" | "missing" | "error" {
77
77
  }
78
78
  }
79
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." }
80
+ function getBunGlobalAkmCommand(): string | null {
81
+ try {
82
+ const globalBin = execFileSync("bun", ["pm", "bin", "-g"], {
83
+ encoding: "utf8",
84
+ timeout: 10_000,
85
+ }).trim()
86
+ if (!globalBin || !path.isAbsolute(globalBin)) return null
87
+ return process.platform === "win32"
88
+ ? path.join(globalBin, "akm.exe")
89
+ : path.join(globalBin, "akm")
90
+ } catch {
91
+ return null
86
92
  }
87
- attemptedAutoInstall = true
93
+ }
88
94
 
95
+ async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void> {
89
96
  try {
90
97
  execFileSync("bun", ["--version"], {
91
98
  encoding: "utf8",
92
99
  timeout: 10_000,
93
100
  })
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
- }
101
+ } catch (error: unknown) {
102
+ await writePluginLog(client, "warn", "AKM auto-install skipped", {
103
+ subsystem: "akm",
104
+ installer: "bun",
105
+ error: `Bun is not available: ${formatCliError(error)}`,
106
+ })
107
+ return
99
108
  }
100
109
 
101
110
  try {
102
- execFileSync("bun", ["install", "-g", "akm-cli"], {
111
+ execFileSync("bun", ["install", "-g", autoInstallPackageRef], {
103
112
  encoding: "utf8",
104
113
  timeout: 120_000,
105
114
  stdio: "pipe",
106
115
  })
107
116
 
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") {
117
+ const bunGlobalAkm = getBunGlobalAkmCommand()
118
+ if (bunGlobalAkm && getCommandStatus(bunGlobalAkm) === "ok") {
119
+ resolvedAkmCommand = bunGlobalAkm
120
+ } else if (getCommandStatus("akm") === "ok") {
120
121
  resolvedAkmCommand = "akm"
121
- return resolvedAkmCommand
122
122
  }
123
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
- }
124
+ await writePluginLog(client, "info", "AKM CLI install check completed", {
125
+ subsystem: "akm",
126
+ installer: "bun",
127
+ package: autoInstallPackageRef,
128
+ command: resolvedAkmCommand,
129
+ })
128
130
  } catch (error: unknown) {
129
- return {
130
- ok: false,
131
- error: `Failed to auto-install 'akm-cli' via Bun: ${formatCliError(error)}`,
132
- }
131
+ await writePluginLog(client, "warn", "AKM auto-install failed", {
132
+ subsystem: "akm",
133
+ installer: "bun",
134
+ package: autoInstallPackageRef,
135
+ error: formatCliError(error),
136
+ })
137
+ }
138
+ }
139
+
140
+ function resolveAkmCommand(): string | CliError {
141
+ const currentStatus = getCommandStatus(resolvedAkmCommand)
142
+ if (currentStatus === "ok" || currentStatus === "error") return resolvedAkmCommand
143
+
144
+ const bunGlobalAkm = getBunGlobalAkmCommand()
145
+ if (bunGlobalAkm && getCommandStatus(bunGlobalAkm) === "ok") {
146
+ resolvedAkmCommand = bunGlobalAkm
147
+ return resolvedAkmCommand
148
+ }
149
+
150
+ if (getCommandStatus("akm") === "ok") {
151
+ resolvedAkmCommand = "akm"
152
+ return resolvedAkmCommand
153
+ }
154
+
155
+ return {
156
+ ok: false,
157
+ error: `The 'akm' CLI could not be resolved after attempting to install '${autoInstallPackageRef}' with Bun. Install akm from https://github.com/itlackey/agentikit.`,
133
158
  }
134
159
  }
135
160
 
@@ -187,7 +212,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
187
212
  }
188
213
 
189
214
  type CliError = { ok: false; error: string }
190
- type AssetType = "skill" | "command" | "agent" | "knowledge" | "script"
215
+ type AssetType = "agent" | "command" | "knowledge" | "memory" | "script" | "skill"
191
216
 
192
217
  type ShowAgentResponse = {
193
218
  type: "agent"
@@ -251,7 +276,7 @@ type SearchHit = {
251
276
 
252
277
  type SearchResponse = {
253
278
  hits?: SearchHit[]
254
- source?: "local" | "registry" | "both"
279
+ source?: "local" | "stash" | "registry" | "both"
255
280
  stashDir?: string
256
281
  timing?: { totalMs?: number; rankMs?: number; embedMs?: number }
257
282
  warnings?: string[]
@@ -351,6 +376,59 @@ function extractText(parts: unknown): string {
351
376
  return segments.join("\n\n")
352
377
  }
353
378
 
379
+ function parseToolOutput(raw: string): unknown {
380
+ try {
381
+ return JSON.parse(raw)
382
+ } catch {
383
+ return undefined
384
+ }
385
+ }
386
+
387
+ function extractMemoryRefs(toolName: string, args: Record<string, unknown>, value: unknown): string[] {
388
+ const refs = new Set<string>()
389
+ const parsed = value && typeof value === "object" ? value as {
390
+ type?: unknown
391
+ ref?: unknown
392
+ name?: unknown
393
+ hits?: unknown
394
+ } : undefined
395
+
396
+ if (toolName === "akm_remember" && typeof parsed?.ref === "string" && parsed.ref) {
397
+ refs.add(parsed.ref)
398
+ }
399
+
400
+ if (parsed?.type === "memory") {
401
+ if (typeof parsed.ref === "string" && parsed.ref) refs.add(parsed.ref)
402
+ if (typeof args.ref === "string" && args.ref) refs.add(args.ref)
403
+ if (refs.size === 0 && typeof parsed.name === "string" && parsed.name) refs.add(`memory:${parsed.name}`)
404
+ }
405
+
406
+ if (Array.isArray(parsed?.hits)) {
407
+ for (const hit of parsed.hits) {
408
+ if (!hit || typeof hit !== "object") continue
409
+ if ((hit as { type?: unknown }).type !== "memory") continue
410
+ const ref = (hit as { ref?: unknown }).ref
411
+ if (typeof ref === "string" && ref) refs.add(ref)
412
+ }
413
+ }
414
+
415
+ return [...refs]
416
+ }
417
+
418
+ function classifyToolFeedback(value: unknown): "positive" | "negative" | undefined {
419
+ if (!value || typeof value !== "object") return undefined
420
+ if (isCliError(value)) return "negative"
421
+ if ("ok" in value && (value as { ok?: unknown }).ok === false) return "negative"
422
+ if ("error" in value && typeof (value as { error?: unknown }).error === "string") return "negative"
423
+ if ("ok" in value && (value as { ok?: unknown }).ok === true) return "positive"
424
+ if ("type" in value || "hits" in value || "assetHits" in value || "sources" in value) return "positive"
425
+ return undefined
426
+ }
427
+
428
+ function truncateLogText(value: string, limit = 1_000): string {
429
+ return value.length > limit ? `${value.slice(0, limit)}…` : value
430
+ }
431
+
354
432
  async function resolveRefInput(
355
433
  client: LogCapableClient,
356
434
  input: { ref?: string; query?: string },
@@ -366,7 +444,7 @@ async function resolveRefInput(
366
444
  return { ok: false, error: "Provide either 'ref' or 'query'." }
367
445
  }
368
446
 
369
- const raw = await runCli(client, ["search", query, "--type", type, "--limit", "1", "--detail", "normal", "--source", "local"], meta)
447
+ const raw = await runCli(client, ["search", query, "--type", type, "--limit", "1", "--detail", "normal", "--source", "stash"], meta)
370
448
  const parsed = parseCliJson<SearchResponse>(raw)
371
449
  if (isCliError(parsed)) return parsed
372
450
 
@@ -415,20 +493,24 @@ function renderCommandTemplate(template: string, rawArguments: string): string {
415
493
  .replace(/\$(\d+)/g, (_m, index: string) => args[Number(index) - 1] ?? "")
416
494
  }
417
495
 
496
+ function normalizeSearchSource(source: "local" | "stash" | "registry" | "both"): "stash" | "registry" | "both" {
497
+ return source === "local" ? "stash" : source
498
+ }
499
+
418
500
  function createSearchArgs(input: {
419
501
  query: string
420
502
  type?: AssetType | "any"
421
503
  limit?: number
422
- source?: "local" | "registry" | "both"
423
- defaultSource?: "local" | "registry" | "both"
504
+ source?: "local" | "stash" | "registry" | "both"
505
+ defaultSource?: "local" | "stash" | "registry" | "both"
424
506
  }): string[] {
425
507
  const args = ["search", input.query]
426
508
  if (input.type) args.push("--type", input.type)
427
509
  if (input.limit) args.push("--limit", String(input.limit))
428
510
  if (input.source) {
429
- args.push("--source", input.source)
511
+ args.push("--source", normalizeSearchSource(input.source))
430
512
  } else if (input.defaultSource) {
431
- args.push("--source", input.defaultSource)
513
+ args.push("--source", normalizeSearchSource(input.defaultSource))
432
514
  }
433
515
  args.push("--detail", "normal")
434
516
  return args
@@ -454,21 +536,67 @@ type PluginClient = {
454
536
  }
455
537
  }
456
538
 
457
- export const AgentikitPlugin: Plugin = async ({ client }) => ({
458
- tool: {
539
+ export const AgentikitPlugin: Plugin = async ({ client }) => {
540
+ await ensureLatestAkmInstalled(client as unknown as LogCapableClient)
541
+
542
+ return {
543
+ "chat.message": async (input, output) => {
544
+ const text = extractText(output.parts).trim()
545
+ if (!text) return
546
+ await writePluginLog(client as unknown as LogCapableClient, "info", "AKM user feedback recorded", {
547
+ subsystem: "feedback",
548
+ actor: "user",
549
+ sessionID: input.sessionID,
550
+ messageID: input.messageID,
551
+ agent: input.agent,
552
+ text: truncateLogText(text),
553
+ })
554
+ },
555
+ "tool.execute.after": async (input, output) => {
556
+ if (!input.tool.startsWith("akm_")) return
557
+
558
+ const parsed = parseToolOutput(output.output)
559
+ if (!parsed) return
560
+
561
+ const feedback = classifyToolFeedback(parsed)
562
+ if (feedback) {
563
+ await writePluginLog(client as unknown as LogCapableClient, feedback === "negative" ? "warn" : "info", "AKM system feedback recorded", {
564
+ subsystem: "feedback",
565
+ actor: "system",
566
+ feedback,
567
+ toolName: input.tool,
568
+ sessionID: input.sessionID,
569
+ callID: input.callID,
570
+ title: output.title,
571
+ error: typeof (parsed as { error?: unknown }).error === "string" ? (parsed as { error?: string }).error : undefined,
572
+ })
573
+ }
574
+
575
+ const memoryRefs = extractMemoryRefs(input.tool, input.args as Record<string, unknown>, parsed)
576
+ if (memoryRefs.length > 0) {
577
+ await writePluginLog(client as unknown as LogCapableClient, "info", "AKM memory usage recorded", {
578
+ subsystem: "memory",
579
+ toolName: input.tool,
580
+ sessionID: input.sessionID,
581
+ callID: input.callID,
582
+ refs: memoryRefs,
583
+ })
584
+ }
585
+ },
586
+ tool: {
459
587
  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.",
588
+ description: "Search your stash or the akm registry for scripts, skills, commands, agents, knowledge, and memories. Use source='registry' or akm_registry_search for installable community kits.",
461
589
  args: {
462
590
  query: tool.schema.string().describe("Case-insensitive substring search."),
463
591
  type: tool.schema
464
- .enum(["skill", "command", "agent", "knowledge", "script", "any"])
592
+ .enum(["agent", "command", "knowledge", "memory", "script", "skill", "any"])
465
593
  .optional()
466
594
  .describe("Optional type filter. Defaults to 'any'."),
467
595
  limit: tool.schema.number().optional().describe("Maximum number of hits to return. Defaults to 20."),
468
596
  source: tool.schema
469
- .enum(["local", "registry", "both"])
597
+ .enum(["local", "stash", "registry", "both"])
470
598
  .optional()
471
- .describe("Search source. 'local' searches stash dirs, 'registry' searches npm/GitHub, 'both' searches all. Defaults to 'local'."),
599
+ .describe("Search source. 'stash' searches local stash directories, 'registry' searches registries, and 'both' searches all sources. 'local' remains a backward-compatible alias for 'stash'."),
472
600
  },
473
601
  async execute({ query, type, limit, source }) {
474
602
  return runCli(client as unknown as LogCapableClient, createSearchArgs({ query, type, limit, source }), { toolName: "akm_search" })
@@ -479,7 +607,7 @@ export const AgentikitPlugin: Plugin = async ({ client }) => ({
479
607
  args: {
480
608
  query: tool.schema.string().describe("Search query for installable registry kits."),
481
609
  type: tool.schema
482
- .enum(["skill", "command", "agent", "knowledge", "script", "any"])
610
+ .enum(["agent", "command", "knowledge", "memory", "script", "skill", "any"])
483
611
  .optional()
484
612
  .describe("Optional asset type filter. Defaults to 'any'."),
485
613
  limit: tool.schema.number().optional().describe("Maximum number of registry hits to return. Defaults to 20."),
@@ -554,25 +682,25 @@ export const AgentikitPlugin: Plugin = async ({ client }) => ({
554
682
  },
555
683
  }),
556
684
  akm_list: tool({
557
- description: "List all kits installed from the registry.",
685
+ description: "List all configured AKM sources, including local directories, managed kits, and remote providers.",
558
686
  args: {},
559
687
  async execute() {
560
688
  return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_list" })
561
689
  },
562
690
  }),
563
691
  akm_remove: tool({
564
- description: "Remove an installed registry kit by id or ref and reindex the stash.",
692
+ description: "Remove a configured AKM source by id, ref, path, URL, or name and reindex the stash.",
565
693
  args: {
566
- package_ref: tool.schema.string().describe("Installed kit id or ref, such as npm:@scope/kit or owner/repo."),
694
+ package_ref: tool.schema.string().describe("Source id, ref, path, URL, or name, such as npm:@scope/kit, owner/repo, or ~/.claude/skills."),
567
695
  },
568
696
  async execute({ package_ref }) {
569
697
  return runCli(client as unknown as LogCapableClient, ["remove", package_ref], { toolName: "akm_remove" })
570
698
  },
571
699
  }),
572
700
  akm_update: tool({
573
- description: "Update one installed kit or all installed kits to the latest available version.",
701
+ description: "Update one managed AKM source or all managed sources to the latest available version.",
574
702
  args: {
575
- package_ref: tool.schema.string().optional().describe("Installed kit id or ref to update."),
703
+ package_ref: tool.schema.string().optional().describe("Managed source id or ref to update."),
576
704
  all: tool.schema.boolean().optional().describe("Update all installed kits."),
577
705
  force: tool.schema.boolean().optional().describe("Force a fresh download even if the version is unchanged."),
578
706
  },
@@ -606,6 +734,33 @@ export const AgentikitPlugin: Plugin = async ({ client }) => ({
606
734
  return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_clone" })
607
735
  },
608
736
  }),
737
+ akm_remember: tool({
738
+ description: "Record a memory in the default AKM stash so it can be searched and shown later.",
739
+ args: {
740
+ content: tool.schema.string().describe("Memory content to store."),
741
+ name: tool.schema.string().optional().describe("Optional memory name."),
742
+ force: tool.schema.boolean().optional().describe("Overwrite an existing memory with the same name."),
743
+ },
744
+ async execute({ content, name, force }) {
745
+ const args = ["remember", content]
746
+ if (name) args.push("--name", name)
747
+ if (force) args.push("--force")
748
+ return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_remember" })
749
+ },
750
+ }),
751
+ akm_feedback: tool({
752
+ description: "Record positive or negative feedback for a stash asset so AKM can improve future ranking.",
753
+ args: {
754
+ ref: tool.schema.string().describe("Asset ref to record feedback for."),
755
+ sentiment: tool.schema.enum(["positive", "negative"]).describe("Whether the feedback is positive or negative."),
756
+ note: tool.schema.string().optional().describe("Optional note to attach to the feedback."),
757
+ },
758
+ async execute({ ref, sentiment, note }) {
759
+ const args = ["feedback", ref, sentiment === "positive" ? "--positive" : "--negative"]
760
+ if (note) args.push("--note", note)
761
+ return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_feedback" })
762
+ },
763
+ }),
609
764
  akm_agent: tool({
610
765
  description: "Dispatch a stash agent by ref into a child OpenCode session, applying the agent prompt and metadata from akm_show.",
611
766
  args: {
@@ -843,10 +998,10 @@ export const AgentikitPlugin: Plugin = async ({ client }) => ({
843
998
  },
844
999
  }),
845
1000
  akm_sources: tool({
846
- description: "List all resolved stash search paths and their status.",
1001
+ description: "List all configured AKM sources. Kept as a backward-compatible alias for the older sources command.",
847
1002
  args: {},
848
1003
  async execute() {
849
- return runCli(client as unknown as LogCapableClient, ["sources"], { toolName: "akm_sources" })
1004
+ return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_sources" })
850
1005
  },
851
1006
  }),
852
1007
  akm_upgrade: tool({
@@ -862,5 +1017,6 @@ export const AgentikitPlugin: Plugin = async ({ client }) => ({
862
1017
  return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_upgrade" })
863
1018
  },
864
1019
  }),
865
- },
866
- })
1020
+ },
1021
+ }
1022
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.0.21",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agentikit - search and show extension assets via the akm CLI.",
6
6
  "keywords": [