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.
- package/README.md +8 -4
- package/index.ts +146 -19
- 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
|
|
27
|
-
| `akm_remove` | Remove
|
|
28
|
-
| `akm_update` | Update one
|
|
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` |
|
|
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 = "
|
|
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", "
|
|
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
|
|
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(["
|
|
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. '
|
|
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(["
|
|
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
|
|
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
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
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, ["
|
|
1004
|
+
return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_sources" })
|
|
878
1005
|
},
|
|
879
1006
|
}),
|
|
880
1007
|
akm_upgrade: tool({
|