akm-opencode 0.2.0 → 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 +8 -4
  2. package/index.ts +146 -19
  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:
package/index.ts CHANGED
@@ -212,7 +212,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
212
212
  }
213
213
 
214
214
  type CliError = { ok: false; error: string }
215
- type AssetType = "skill" | "command" | "agent" | "knowledge" | "script"
215
+ type AssetType = "agent" | "command" | "knowledge" | "memory" | "script" | "skill"
216
216
 
217
217
  type ShowAgentResponse = {
218
218
  type: "agent"
@@ -276,7 +276,7 @@ type SearchHit = {
276
276
 
277
277
  type SearchResponse = {
278
278
  hits?: SearchHit[]
279
- source?: "local" | "registry" | "both"
279
+ source?: "local" | "stash" | "registry" | "both"
280
280
  stashDir?: string
281
281
  timing?: { totalMs?: number; rankMs?: number; embedMs?: number }
282
282
  warnings?: string[]
@@ -376,6 +376,59 @@ function extractText(parts: unknown): string {
376
376
  return segments.join("\n\n")
377
377
  }
378
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
+
379
432
  async function resolveRefInput(
380
433
  client: LogCapableClient,
381
434
  input: { ref?: string; query?: string },
@@ -391,7 +444,7 @@ async function resolveRefInput(
391
444
  return { ok: false, error: "Provide either 'ref' or 'query'." }
392
445
  }
393
446
 
394
- 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)
395
448
  const parsed = parseCliJson<SearchResponse>(raw)
396
449
  if (isCliError(parsed)) return parsed
397
450
 
@@ -440,20 +493,24 @@ function renderCommandTemplate(template: string, rawArguments: string): string {
440
493
  .replace(/\$(\d+)/g, (_m, index: string) => args[Number(index) - 1] ?? "")
441
494
  }
442
495
 
496
+ function normalizeSearchSource(source: "local" | "stash" | "registry" | "both"): "stash" | "registry" | "both" {
497
+ return source === "local" ? "stash" : source
498
+ }
499
+
443
500
  function createSearchArgs(input: {
444
501
  query: string
445
502
  type?: AssetType | "any"
446
503
  limit?: number
447
- source?: "local" | "registry" | "both"
448
- defaultSource?: "local" | "registry" | "both"
504
+ source?: "local" | "stash" | "registry" | "both"
505
+ defaultSource?: "local" | "stash" | "registry" | "both"
449
506
  }): string[] {
450
507
  const args = ["search", input.query]
451
508
  if (input.type) args.push("--type", input.type)
452
509
  if (input.limit) args.push("--limit", String(input.limit))
453
510
  if (input.source) {
454
- args.push("--source", input.source)
511
+ args.push("--source", normalizeSearchSource(input.source))
455
512
  } else if (input.defaultSource) {
456
- args.push("--source", input.defaultSource)
513
+ args.push("--source", normalizeSearchSource(input.defaultSource))
457
514
  }
458
515
  args.push("--detail", "normal")
459
516
  return args
@@ -483,20 +540,63 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
483
540
  await ensureLatestAkmInstalled(client as unknown as LogCapableClient)
484
541
 
485
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
+ },
486
586
  tool: {
487
587
  akm_search: tool({
488
- 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.",
489
589
  args: {
490
590
  query: tool.schema.string().describe("Case-insensitive substring search."),
491
591
  type: tool.schema
492
- .enum(["skill", "command", "agent", "knowledge", "script", "any"])
592
+ .enum(["agent", "command", "knowledge", "memory", "script", "skill", "any"])
493
593
  .optional()
494
594
  .describe("Optional type filter. Defaults to 'any'."),
495
595
  limit: tool.schema.number().optional().describe("Maximum number of hits to return. Defaults to 20."),
496
596
  source: tool.schema
497
- .enum(["local", "registry", "both"])
597
+ .enum(["local", "stash", "registry", "both"])
498
598
  .optional()
499
- .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'."),
500
600
  },
501
601
  async execute({ query, type, limit, source }) {
502
602
  return runCli(client as unknown as LogCapableClient, createSearchArgs({ query, type, limit, source }), { toolName: "akm_search" })
@@ -507,7 +607,7 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
507
607
  args: {
508
608
  query: tool.schema.string().describe("Search query for installable registry kits."),
509
609
  type: tool.schema
510
- .enum(["skill", "command", "agent", "knowledge", "script", "any"])
610
+ .enum(["agent", "command", "knowledge", "memory", "script", "skill", "any"])
511
611
  .optional()
512
612
  .describe("Optional asset type filter. Defaults to 'any'."),
513
613
  limit: tool.schema.number().optional().describe("Maximum number of registry hits to return. Defaults to 20."),
@@ -582,25 +682,25 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
582
682
  },
583
683
  }),
584
684
  akm_list: tool({
585
- description: "List all kits installed from the registry.",
685
+ description: "List all configured AKM sources, including local directories, managed kits, and remote providers.",
586
686
  args: {},
587
687
  async execute() {
588
688
  return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_list" })
589
689
  },
590
690
  }),
591
691
  akm_remove: tool({
592
- 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.",
593
693
  args: {
594
- 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."),
595
695
  },
596
696
  async execute({ package_ref }) {
597
697
  return runCli(client as unknown as LogCapableClient, ["remove", package_ref], { toolName: "akm_remove" })
598
698
  },
599
699
  }),
600
700
  akm_update: tool({
601
- 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.",
602
702
  args: {
603
- 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."),
604
704
  all: tool.schema.boolean().optional().describe("Update all installed kits."),
605
705
  force: tool.schema.boolean().optional().describe("Force a fresh download even if the version is unchanged."),
606
706
  },
@@ -634,6 +734,33 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
634
734
  return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_clone" })
635
735
  },
636
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
+ }),
637
764
  akm_agent: tool({
638
765
  description: "Dispatch a stash agent by ref into a child OpenCode session, applying the agent prompt and metadata from akm_show.",
639
766
  args: {
@@ -871,10 +998,10 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
871
998
  },
872
999
  }),
873
1000
  akm_sources: tool({
874
- 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.",
875
1002
  args: {},
876
1003
  async execute() {
877
- return runCli(client as unknown as LogCapableClient, ["sources"], { toolName: "akm_sources" })
1004
+ return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_sources" })
878
1005
  },
879
1006
  }),
880
1007
  akm_upgrade: tool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.2.0",
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": [