akm-opencode 0.4.3 → 0.5.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 +30 -5
- package/index.ts +382 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# akm-opencode
|
|
2
2
|
|
|
3
|
-
OpenCode plugin for the [AKM](https://github.com/itlackey/akm) CLI. Registers tools that let your AI agent **search**, **show**, and **manage**
|
|
3
|
+
OpenCode plugin for the [AKM](https://github.com/itlackey/akm) CLI (v0.5.0+). Registers tools that let your AI agent **search**, **show**, and **manage** stash assets — skills, commands, agents, knowledge, memories, scripts, workflows, vaults, and wikis — plus **agentic hooks** that auto-load relevant assets into each turn, record feedback when assets are used, and harvest session memories so the stash improves with every session.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -16,25 +16,30 @@ Add to your OpenCode config (`opencode.json`):
|
|
|
16
16
|
|
|
17
17
|
| Tool | Description |
|
|
18
18
|
|------|-------------|
|
|
19
|
-
| `akm_search` | Search the local stash, the registry, or both
|
|
19
|
+
| `akm_search` | Search the local stash, the registry, or both. Type filter accepts `skill`, `command`, `agent`, `knowledge`, `memory`, `script`, `workflow`, `vault`, `wiki`, `any` |
|
|
20
20
|
| `akm_registry_search` | Search configured registries for installable kits and optional asset-level hits |
|
|
21
21
|
| `akm_show` | Show a stash asset by its ref |
|
|
22
22
|
| `akm_index` | Build or rebuild the search index |
|
|
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
|
-
| `akm_add` | Install kits from npm, GitHub, git URLs, or local
|
|
25
|
+
| `akm_add` | Install kits or register external sources from npm, GitHub, git URLs, URLs, or local dirs (use `type: "wiki"` to register a wiki; `writable`, `trust`, `max_pages`, `max_depth`, `provider`, `options` also supported) |
|
|
26
26
|
| `akm_list` | List configured AKM sources |
|
|
27
27
|
| `akm_remove` | Remove a configured AKM source and reindex |
|
|
28
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
30
|
| `akm_remember` | Record a memory in the default stash |
|
|
31
|
-
| `akm_feedback` | Record positive or negative feedback for a stash asset |
|
|
31
|
+
| `akm_feedback` | Record positive or negative feedback for a stash asset (skipped automatically for `memory:` and `vault:` refs) |
|
|
32
32
|
| `akm_config` | Get, set, unset, list, or inspect akm configuration (including `config path --all`) |
|
|
33
33
|
| `akm_run` | Execute a stash script using its `run` field |
|
|
34
34
|
| `akm_sources` | Backward-compatible alias that lists configured AKM sources |
|
|
35
35
|
| `akm_upgrade` | Check for or install akm CLI updates |
|
|
36
36
|
| `akm_curate` | Curate the stash for a task or topic and return ranked matches the agent can use |
|
|
37
37
|
| `akm_evolve` | Dispatch the AKM curator agent to review recent session activity and propose stash improvements |
|
|
38
|
+
| `akm_save` | Commit (and push, when writable) pending changes in a git-backed stash |
|
|
39
|
+
| `akm_import` | Import a file (or stdin content) into the stash as a typed asset |
|
|
40
|
+
| `akm_vault` | Manage vaults (`list`, `show`, `create`, `set`, `unset`, `shell_snippet`). **Values never surface** — `show`/`list` return key names only. `shell_snippet` returns opaque `eval` text |
|
|
41
|
+
| `akm_wiki` | Manage wikis (`create`, `register`, `list`, `show`, `pages`, `search`, `stash`, `lint`, `ingest`, `remove`) |
|
|
42
|
+
| `akm_workflow` | Drive workflow runs (`start`, `next`, `complete`, `status`, `list`, `create`, `template`, `resume`) |
|
|
38
43
|
|
|
39
44
|
## Compound-engineering hooks
|
|
40
45
|
|
|
@@ -142,9 +147,29 @@ stash/
|
|
|
142
147
|
├── skills/ # skill directories containing SKILL.md
|
|
143
148
|
├── commands/ # markdown files
|
|
144
149
|
├── agents/ # markdown files
|
|
145
|
-
|
|
150
|
+
├── knowledge/ # markdown files
|
|
151
|
+
├── memories/ # markdown memory files (akm remember)
|
|
152
|
+
├── workflows/ # multi-step procedures (workflow:<name>)
|
|
153
|
+
├── vaults/ # .env secret stores (vault:<name>) — values never surface through structured output
|
|
154
|
+
└── wikis/ # per-wiki directories <name>/{schema,index,log}.md + raw/ + pages
|
|
146
155
|
```
|
|
147
156
|
|
|
157
|
+
## Vaults
|
|
158
|
+
|
|
159
|
+
`akm_vault` is the one tool in this plugin with a hard contract on output. The
|
|
160
|
+
AKM CLI itself guarantees vault values never appear in JSON, the search index,
|
|
161
|
+
`.stash.json`, or any structured output channel. This plugin mirrors that:
|
|
162
|
+
|
|
163
|
+
- `action: "list"` / `"show"` return key names and comments only.
|
|
164
|
+
- `action: "set"` / `"unset"` never echo the value.
|
|
165
|
+
- `action: "shell_snippet"` wraps `akm vault load` and returns the raw shell
|
|
166
|
+
text as-is. Treat it as opaque and hand it straight to a shell via
|
|
167
|
+
`eval "$(…)"` — do not log it, do not pass it through another tool, and do
|
|
168
|
+
not let the agent inspect it.
|
|
169
|
+
|
|
170
|
+
Automatic feedback recording (`tool.execute.after`) skips `vault:*` refs so
|
|
171
|
+
that usage signals can't leak which vault was touched.
|
|
172
|
+
|
|
148
173
|
Assets are resolved from three source types: **working** (local stash), **search paths** (additional dirs via `searchPaths` config), and **installed** (registry kits via `akm add`).
|
|
149
174
|
|
|
150
175
|
## Docs
|
package/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ const sessionBuffer = new Map<string, SessionBufferEntry[]>()
|
|
|
29
29
|
const sessionMemoryCaptured = new Set<string>()
|
|
30
30
|
|
|
31
31
|
// Asset-ref grammar matching the stash skill: [origin//]type:name
|
|
32
|
-
const AKM_REF_PATTERN = /(?:[A-Za-z0-9@._+/-]+\/\/)?(?:skill|command|agent|knowledge|memory|script):[A-Za-z0-9._/\-]+/g
|
|
32
|
+
const AKM_REF_PATTERN = /(?:[A-Za-z0-9@._+/-]+\/\/)?(?:skill|command|agent|knowledge|memory|script|workflow|vault|wiki):[A-Za-z0-9._/\-]+/g
|
|
33
33
|
|
|
34
34
|
const CURATOR_AGENT_PROMPT = `You are the AKM curator — a compound-engineering agent that keeps the user's AKM stash improving every time the main agent finishes a task.
|
|
35
35
|
|
|
@@ -41,9 +41,12 @@ Inputs you should inspect:
|
|
|
41
41
|
Signals to act on:
|
|
42
42
|
- Hot refs: assets repeatedly appearing in positive tool outcomes. Call akm_feedback <ref> positive --note "curator: consistently useful" to reinforce.
|
|
43
43
|
- Cold refs: assets tied to failures or user complaints. Record akm_feedback <ref> negative --note "<excerpt>" and open the asset for review.
|
|
44
|
-
- Missing coverage: recurring user prompts with no matching asset. Draft a new skill, command,
|
|
44
|
+
- Missing coverage: recurring user prompts with no matching asset. Draft a new skill, command, knowledge doc, wiki page, or workflow in the working stash and reindex with akm_index.
|
|
45
45
|
- Duplicates / drift: near-identical descriptions or overlapping responsibilities. Propose a consolidation.
|
|
46
|
-
- Stale memories: session summaries that never get recalled. Propose akm_remove memory:<name> once distilled into a durable knowledge doc.
|
|
46
|
+
- Stale memories: session summaries that never get recalled. Propose akm_remove memory:<name> once distilled into a durable knowledge doc or wiki page.
|
|
47
|
+
- Wiki hygiene: for each wiki returned by akm_wiki list, run akm_wiki lint <name> and report orphans, broken xrefs, uncited raws, and stale indexes as fix candidates.
|
|
48
|
+
- Stuck workflows: run akm_workflow list --active and surface any runs in blocked or failed state with their step ids. Propose whether to resume or escalate.
|
|
49
|
+
- Never touch vaults: do not call akm_vault show or shell_snippet unless the user explicitly asks. Vault values must never appear in reports.
|
|
47
50
|
|
|
48
51
|
Rules of engagement:
|
|
49
52
|
- Never apply destructive changes without explicit user approval.
|
|
@@ -66,6 +69,12 @@ Output shape: end every run with a markdown report that has these sections:
|
|
|
66
69
|
## Duplicates / drift
|
|
67
70
|
- <ref a> vs <ref b> — consolidation proposal
|
|
68
71
|
|
|
72
|
+
## Wiki health
|
|
73
|
+
- <wiki> — lint findings (orphan, broken-xref, uncited-raw, stale-index) with suggested fix
|
|
74
|
+
|
|
75
|
+
## Workflow health
|
|
76
|
+
- <workflow|runId> — blocked/failed state — resume or escalate
|
|
77
|
+
|
|
69
78
|
## Housekeeping
|
|
70
79
|
- stale memories, reindex needs, config tweaks
|
|
71
80
|
`
|
|
@@ -488,7 +497,29 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
488
497
|
}
|
|
489
498
|
|
|
490
499
|
type CliError = { ok: false; error: string }
|
|
491
|
-
type AssetType =
|
|
500
|
+
type AssetType =
|
|
501
|
+
| "agent"
|
|
502
|
+
| "command"
|
|
503
|
+
| "knowledge"
|
|
504
|
+
| "memory"
|
|
505
|
+
| "script"
|
|
506
|
+
| "skill"
|
|
507
|
+
| "workflow"
|
|
508
|
+
| "vault"
|
|
509
|
+
| "wiki"
|
|
510
|
+
|
|
511
|
+
const ASSET_TYPES = [
|
|
512
|
+
"agent",
|
|
513
|
+
"command",
|
|
514
|
+
"knowledge",
|
|
515
|
+
"memory",
|
|
516
|
+
"script",
|
|
517
|
+
"skill",
|
|
518
|
+
"workflow",
|
|
519
|
+
"vault",
|
|
520
|
+
"wiki",
|
|
521
|
+
"any",
|
|
522
|
+
] as const
|
|
492
523
|
|
|
493
524
|
type ShowAgentResponse = {
|
|
494
525
|
type: "agent"
|
|
@@ -868,7 +899,7 @@ function normalizeSearchSource(source: "local" | "stash" | "registry" | "both"):
|
|
|
868
899
|
|
|
869
900
|
function createSearchArgs(input: {
|
|
870
901
|
query: string
|
|
871
|
-
type?: AssetType | "any"
|
|
902
|
+
type?: AssetType | "any" | string
|
|
872
903
|
limit?: number
|
|
873
904
|
source?: "local" | "stash" | "registry" | "both"
|
|
874
905
|
defaultSource?: "local" | "stash" | "registry" | "both"
|
|
@@ -1073,8 +1104,10 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1073
1104
|
? `opencode auto: ${input.tool} succeeded`
|
|
1074
1105
|
: `opencode auto: ${input.tool} failed`
|
|
1075
1106
|
for (const ref of allRefs) {
|
|
1076
|
-
// Memories
|
|
1077
|
-
|
|
1107
|
+
// Memories and vault refs are not first-class feedback targets —
|
|
1108
|
+
// memories do not accept feedback, and vault values never surface in
|
|
1109
|
+
// JSON so automatic usage signals would be misleading.
|
|
1110
|
+
if (ref.startsWith("memory:") || ref.startsWith("vault:")) continue
|
|
1078
1111
|
const ok = recordFeedbackSync(ref, feedback, note)
|
|
1079
1112
|
if (!ok) break
|
|
1080
1113
|
}
|
|
@@ -1082,11 +1115,11 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1082
1115
|
},
|
|
1083
1116
|
tool: {
|
|
1084
1117
|
akm_search: tool({
|
|
1085
|
-
description: "Search your stash or the akm registry for scripts, skills, commands, agents, knowledge, and
|
|
1118
|
+
description: "Search your stash or the akm registry for scripts, skills, commands, agents, knowledge, memories, workflows, vaults, and wikis. Use source='registry' or akm_registry_search for installable community kits.",
|
|
1086
1119
|
args: {
|
|
1087
1120
|
query: tool.schema.string().describe("Case-insensitive substring search."),
|
|
1088
1121
|
type: tool.schema
|
|
1089
|
-
.enum(
|
|
1122
|
+
.enum(ASSET_TYPES as unknown as [string, ...string[]])
|
|
1090
1123
|
.optional()
|
|
1091
1124
|
.describe("Optional type filter. Defaults to 'any'."),
|
|
1092
1125
|
limit: tool.schema.number().optional().describe("Maximum number of hits to return. Defaults to 20."),
|
|
@@ -1104,7 +1137,7 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1104
1137
|
args: {
|
|
1105
1138
|
query: tool.schema.string().describe("Search query for installable registry kits."),
|
|
1106
1139
|
type: tool.schema
|
|
1107
|
-
.enum(
|
|
1140
|
+
.enum(ASSET_TYPES as unknown as [string, ...string[]])
|
|
1108
1141
|
.optional()
|
|
1109
1142
|
.describe("Optional asset type filter. Defaults to 'any'."),
|
|
1110
1143
|
limit: tool.schema.number().optional().describe("Maximum number of registry hits to return. Defaults to 20."),
|
|
@@ -1170,12 +1203,29 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1170
1203
|
},
|
|
1171
1204
|
}),
|
|
1172
1205
|
akm_add: tool({
|
|
1173
|
-
description: "Install a kit from npm, GitHub, another git host, or a local directory.
|
|
1206
|
+
description: "Install a kit or register an external source from npm, GitHub, another git host, a URL, or a local directory. Use type='wiki' to register a wiki source instead of a stash kit.",
|
|
1174
1207
|
args: {
|
|
1175
|
-
package_ref: tool.schema.string().describe("Package reference such as npm:@scope/kit, github:<owner>/<repo>, git+https://host/repo, or ./local/kit."),
|
|
1208
|
+
package_ref: tool.schema.string().describe("Package reference such as npm:@scope/kit, github:<owner>/<repo>, git+https://host/repo, https://url, or ./local/kit."),
|
|
1209
|
+
type: tool.schema.enum(["wiki"]).optional().describe("Route the add through a typed registrar. 'wiki' registers an external wiki source."),
|
|
1210
|
+
name: tool.schema.string().optional().describe("Optional name to register the source under."),
|
|
1211
|
+
writable: tool.schema.boolean().optional().describe("Mark a git-backed source as push-writable (used by akm save)."),
|
|
1212
|
+
trust: tool.schema.boolean().optional().describe("Bypass install-audit blocking for this registration only."),
|
|
1213
|
+
provider: tool.schema.string().optional().describe("Provider hint (required for raw URL refs, e.g. 'github', 'website')."),
|
|
1214
|
+
options: tool.schema.string().optional().describe("JSON string of provider-specific options."),
|
|
1215
|
+
max_pages: tool.schema.number().optional().describe("Cap for website crawlers (default 50)."),
|
|
1216
|
+
max_depth: tool.schema.number().optional().describe("Depth cap for website crawlers (default 3)."),
|
|
1176
1217
|
},
|
|
1177
|
-
async execute({ package_ref }) {
|
|
1178
|
-
|
|
1218
|
+
async execute({ package_ref, type, name, writable, trust, provider, options, max_pages, max_depth }) {
|
|
1219
|
+
const args = ["add", package_ref]
|
|
1220
|
+
if (type) args.push("--type", type)
|
|
1221
|
+
if (name) args.push("--name", name)
|
|
1222
|
+
if (writable) args.push("--writable")
|
|
1223
|
+
if (trust) args.push("--trust")
|
|
1224
|
+
if (provider) args.push("--provider", provider)
|
|
1225
|
+
if (options) args.push("--options", options)
|
|
1226
|
+
if (max_pages != null) args.push("--max-pages", String(max_pages))
|
|
1227
|
+
if (max_depth != null) args.push("--max-depth", String(max_depth))
|
|
1228
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_add" })
|
|
1179
1229
|
},
|
|
1180
1230
|
}),
|
|
1181
1231
|
akm_list: tool({
|
|
@@ -1567,6 +1617,324 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1567
1617
|
return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_sources" })
|
|
1568
1618
|
},
|
|
1569
1619
|
}),
|
|
1620
|
+
akm_save: tool({
|
|
1621
|
+
description: "Commit (and push, when writable) pending changes in a git-backed stash. No-op for non-git stashes.",
|
|
1622
|
+
args: {
|
|
1623
|
+
name: tool.schema.string().optional().describe("Optional stash source name. Defaults to the primary stash."),
|
|
1624
|
+
message: tool.schema.string().optional().describe("Commit message. Defaults to an auto-generated summary."),
|
|
1625
|
+
},
|
|
1626
|
+
async execute({ name, message }) {
|
|
1627
|
+
const args = ["save"]
|
|
1628
|
+
if (name) args.push(name)
|
|
1629
|
+
if (message) args.push("-m", message)
|
|
1630
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_save" })
|
|
1631
|
+
},
|
|
1632
|
+
}),
|
|
1633
|
+
akm_import: tool({
|
|
1634
|
+
description: "Import a file (or stdin) into the stash as a typed asset. Pass '-' as source to read from a string via content.",
|
|
1635
|
+
args: {
|
|
1636
|
+
source: tool.schema.string().describe("Path to the source file, or '-' to read from stdin (use 'content' to provide it)."),
|
|
1637
|
+
name: tool.schema.string().optional().describe("Optional asset name override."),
|
|
1638
|
+
force: tool.schema.boolean().optional().describe("Overwrite an existing asset with the same name."),
|
|
1639
|
+
content: tool.schema.string().optional().describe("Raw content to feed on stdin when source is '-'."),
|
|
1640
|
+
},
|
|
1641
|
+
async execute({ source, name, force, content }) {
|
|
1642
|
+
const args = ["import", source]
|
|
1643
|
+
if (name) args.push("--name", name)
|
|
1644
|
+
if (force) args.push("--force")
|
|
1645
|
+
const command = resolveAkmCommand()
|
|
1646
|
+
if (typeof command !== "string") return JSON.stringify(command)
|
|
1647
|
+
if (source === "-" && content) {
|
|
1648
|
+
try {
|
|
1649
|
+
const stdout = execFileSync(command, [...args, "--format", "json"], {
|
|
1650
|
+
encoding: "utf8",
|
|
1651
|
+
timeout: 60_000,
|
|
1652
|
+
input: content,
|
|
1653
|
+
})
|
|
1654
|
+
return stdout
|
|
1655
|
+
} catch (error: unknown) {
|
|
1656
|
+
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_import" })
|
|
1660
|
+
},
|
|
1661
|
+
}),
|
|
1662
|
+
akm_vault: tool({
|
|
1663
|
+
description: "Manage encrypted-at-rest vaults of KEY=VALUE pairs. Values never surface in any output channel — 'show'/'list' return key names only, 'set'/'unset' never echo the value. Use 'shell_snippet' to get a shell-eval snippet that loads values into the process.",
|
|
1664
|
+
args: {
|
|
1665
|
+
action: tool.schema.enum(["list", "show", "create", "set", "unset", "shell_snippet"]).describe("Vault subcommand. 'shell_snippet' wraps 'vault load' — treat its output as opaque shell text meant for eval."),
|
|
1666
|
+
ref: tool.schema.string().optional().describe("Vault ref such as vault:prod or vault:team/prod. Required for show/set/unset/shell_snippet; optional for list."),
|
|
1667
|
+
name: tool.schema.string().optional().describe("Vault name when action is 'create' (e.g. 'prod' → vaults/prod.env)."),
|
|
1668
|
+
key: tool.schema.string().optional().describe("Variable name for set/unset. May include '=' to pass KEY=VALUE in one field when value is omitted."),
|
|
1669
|
+
value: tool.schema.string().optional().describe("Value to store. Never echoed back."),
|
|
1670
|
+
comment: tool.schema.string().optional().describe("Optional inline '# comment' written above the key for 'set'."),
|
|
1671
|
+
},
|
|
1672
|
+
async execute({ action, ref, name, key, value, comment }) {
|
|
1673
|
+
const logMeta = { toolName: "akm_vault" }
|
|
1674
|
+
switch (action) {
|
|
1675
|
+
case "list": {
|
|
1676
|
+
const args = ["vault", "list"]
|
|
1677
|
+
if (ref) args.push(ref)
|
|
1678
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1679
|
+
}
|
|
1680
|
+
case "show": {
|
|
1681
|
+
if (!ref) return JSON.stringify({ ok: false, error: "'ref' is required for action='show'." })
|
|
1682
|
+
return runCli(client as unknown as LogCapableClient, ["vault", "show", ref], logMeta)
|
|
1683
|
+
}
|
|
1684
|
+
case "create": {
|
|
1685
|
+
if (!name) return JSON.stringify({ ok: false, error: "'name' is required for action='create'." })
|
|
1686
|
+
return runCli(client as unknown as LogCapableClient, ["vault", "create", name], logMeta)
|
|
1687
|
+
}
|
|
1688
|
+
case "set": {
|
|
1689
|
+
if (!ref) return JSON.stringify({ ok: false, error: "'ref' is required for action='set'." })
|
|
1690
|
+
if (!key) return JSON.stringify({ ok: false, error: "'key' is required for action='set'." })
|
|
1691
|
+
const args = ["vault", "set", ref, key]
|
|
1692
|
+
if (value != null) args.push(value)
|
|
1693
|
+
if (comment) args.push("--comment", comment)
|
|
1694
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1695
|
+
}
|
|
1696
|
+
case "unset": {
|
|
1697
|
+
if (!ref) return JSON.stringify({ ok: false, error: "'ref' is required for action='unset'." })
|
|
1698
|
+
if (!key) return JSON.stringify({ ok: false, error: "'key' is required for action='unset'." })
|
|
1699
|
+
return runCli(client as unknown as LogCapableClient, ["vault", "unset", ref, key], logMeta)
|
|
1700
|
+
}
|
|
1701
|
+
case "shell_snippet": {
|
|
1702
|
+
if (!ref) return JSON.stringify({ ok: false, error: "'ref' is required for action='shell_snippet'." })
|
|
1703
|
+
// `vault load` emits raw shell — not JSON. Return the snippet verbatim
|
|
1704
|
+
// so the caller can hand it to a shell via eval. Never parse values.
|
|
1705
|
+
const command = resolveAkmCommand()
|
|
1706
|
+
if (typeof command !== "string") return JSON.stringify(command)
|
|
1707
|
+
try {
|
|
1708
|
+
const stdout = execFileSync(command, ["vault", "load", ref], {
|
|
1709
|
+
encoding: "utf8",
|
|
1710
|
+
timeout: 30_000,
|
|
1711
|
+
})
|
|
1712
|
+
return JSON.stringify({ ok: true, ref, shell: stdout.trim() })
|
|
1713
|
+
} catch (error: unknown) {
|
|
1714
|
+
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
},
|
|
1719
|
+
}),
|
|
1720
|
+
akm_wiki: tool({
|
|
1721
|
+
description: "Manage AKM wikis — multi-wiki knowledge bases under <stashDir>/wikis/<name>/. Supports scaffolding, registering external sources, listing pages, scoped search, stashing raw sources, lint, and ingest workflow.",
|
|
1722
|
+
args: {
|
|
1723
|
+
action: tool.schema.enum([
|
|
1724
|
+
"create",
|
|
1725
|
+
"register",
|
|
1726
|
+
"list",
|
|
1727
|
+
"show",
|
|
1728
|
+
"remove",
|
|
1729
|
+
"pages",
|
|
1730
|
+
"search",
|
|
1731
|
+
"stash",
|
|
1732
|
+
"lint",
|
|
1733
|
+
"ingest",
|
|
1734
|
+
]).describe("Wiki subcommand."),
|
|
1735
|
+
name: tool.schema.string().optional().describe("Wiki name (required for every action except 'list')."),
|
|
1736
|
+
source_ref: tool.schema.string().optional().describe("Source ref to register (required for action='register'). Accepts directory paths, git URLs, github owner/repo, or https:// website roots."),
|
|
1737
|
+
writable: tool.schema.boolean().optional().describe("When registering a git-backed source, mark it as push-writable (used by akm_save)."),
|
|
1738
|
+
trust: tool.schema.boolean().optional().describe("Bypass install-audit blocking for this registration only."),
|
|
1739
|
+
max_pages: tool.schema.number().optional().describe("Crawler page cap when registering a website (default 50)."),
|
|
1740
|
+
max_depth: tool.schema.number().optional().describe("Crawler depth cap when registering a website (default 3)."),
|
|
1741
|
+
query: tool.schema.string().optional().describe("Query string for action='search'."),
|
|
1742
|
+
limit: tool.schema.number().optional().describe("Result cap for action='search'."),
|
|
1743
|
+
source: tool.schema.string().optional().describe("Source path (or '-' for stdin) for action='stash'."),
|
|
1744
|
+
as_slug: tool.schema.string().optional().describe("Explicit slug for action='stash' (defaults to derived from source)."),
|
|
1745
|
+
content: tool.schema.string().optional().describe("Raw content to feed stdin when stashing with source='-'."),
|
|
1746
|
+
force: tool.schema.boolean().optional().describe("Required for action='remove'."),
|
|
1747
|
+
with_sources: tool.schema.boolean().optional().describe("When removing, also delete the raw/ sources (default false)."),
|
|
1748
|
+
},
|
|
1749
|
+
async execute({ action, name, source_ref, writable, trust, max_pages, max_depth, query, limit, source, as_slug, content, force, with_sources }) {
|
|
1750
|
+
const logMeta = { toolName: "akm_wiki" }
|
|
1751
|
+
const requireName = () => {
|
|
1752
|
+
if (!name) return JSON.stringify({ ok: false, error: `'name' is required for action='${action}'.` })
|
|
1753
|
+
return null
|
|
1754
|
+
}
|
|
1755
|
+
switch (action) {
|
|
1756
|
+
case "list":
|
|
1757
|
+
return runCli(client as unknown as LogCapableClient, ["wiki", "list"], logMeta)
|
|
1758
|
+
case "create": {
|
|
1759
|
+
const err = requireName(); if (err) return err
|
|
1760
|
+
return runCli(client as unknown as LogCapableClient, ["wiki", "create", name!], logMeta)
|
|
1761
|
+
}
|
|
1762
|
+
case "show": {
|
|
1763
|
+
const err = requireName(); if (err) return err
|
|
1764
|
+
return runCli(client as unknown as LogCapableClient, ["wiki", "show", name!], logMeta)
|
|
1765
|
+
}
|
|
1766
|
+
case "pages": {
|
|
1767
|
+
const err = requireName(); if (err) return err
|
|
1768
|
+
return runCli(client as unknown as LogCapableClient, ["wiki", "pages", name!], logMeta)
|
|
1769
|
+
}
|
|
1770
|
+
case "ingest": {
|
|
1771
|
+
const err = requireName(); if (err) return err
|
|
1772
|
+
return runCli(client as unknown as LogCapableClient, ["wiki", "ingest", name!], logMeta)
|
|
1773
|
+
}
|
|
1774
|
+
case "lint": {
|
|
1775
|
+
const err = requireName(); if (err) return err
|
|
1776
|
+
// `wiki lint` exits 1 when findings exist, which runCli surfaces as
|
|
1777
|
+
// an error envelope. That is still useful output — the JSON body is
|
|
1778
|
+
// the lint report. Pass through either way.
|
|
1779
|
+
return runCli(client as unknown as LogCapableClient, ["wiki", "lint", name!], logMeta)
|
|
1780
|
+
}
|
|
1781
|
+
case "register": {
|
|
1782
|
+
const err = requireName(); if (err) return err
|
|
1783
|
+
if (!source_ref) return JSON.stringify({ ok: false, error: "'source_ref' is required for action='register'." })
|
|
1784
|
+
const args = ["wiki", "register", name!, source_ref]
|
|
1785
|
+
if (writable) args.push("--writable")
|
|
1786
|
+
if (trust) args.push("--trust")
|
|
1787
|
+
if (max_pages != null) args.push("--max-pages", String(max_pages))
|
|
1788
|
+
if (max_depth != null) args.push("--max-depth", String(max_depth))
|
|
1789
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1790
|
+
}
|
|
1791
|
+
case "remove": {
|
|
1792
|
+
const err = requireName(); if (err) return err
|
|
1793
|
+
if (!force) return JSON.stringify({ ok: false, error: "'force' must be true to remove a wiki." })
|
|
1794
|
+
const args = ["wiki", "remove", name!, "--force"]
|
|
1795
|
+
if (with_sources) args.push("--with-sources")
|
|
1796
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1797
|
+
}
|
|
1798
|
+
case "search": {
|
|
1799
|
+
const err = requireName(); if (err) return err
|
|
1800
|
+
if (!query) return JSON.stringify({ ok: false, error: "'query' is required for action='search'." })
|
|
1801
|
+
const args = ["wiki", "search", name!, query]
|
|
1802
|
+
if (limit != null) args.push("--limit", String(limit))
|
|
1803
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1804
|
+
}
|
|
1805
|
+
case "stash": {
|
|
1806
|
+
const err = requireName(); if (err) return err
|
|
1807
|
+
if (!source) return JSON.stringify({ ok: false, error: "'source' is required for action='stash'." })
|
|
1808
|
+
const args = ["wiki", "stash", name!, source]
|
|
1809
|
+
if (as_slug) args.push("--as", as_slug)
|
|
1810
|
+
if (source === "-" && content) {
|
|
1811
|
+
const command = resolveAkmCommand()
|
|
1812
|
+
if (typeof command !== "string") return JSON.stringify(command)
|
|
1813
|
+
try {
|
|
1814
|
+
const stdout = execFileSync(command, [...args, "--format", "json"], {
|
|
1815
|
+
encoding: "utf8",
|
|
1816
|
+
timeout: 60_000,
|
|
1817
|
+
input: content,
|
|
1818
|
+
})
|
|
1819
|
+
return stdout
|
|
1820
|
+
} catch (error: unknown) {
|
|
1821
|
+
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
}),
|
|
1829
|
+
akm_workflow: tool({
|
|
1830
|
+
description: "Manage AKM workflow runs — stateful multi-step procedures defined as workflow:<name> assets. Use start/next/complete/resume to drive a run, status/list to inspect, create/template to author.",
|
|
1831
|
+
args: {
|
|
1832
|
+
action: tool.schema.enum([
|
|
1833
|
+
"start",
|
|
1834
|
+
"next",
|
|
1835
|
+
"complete",
|
|
1836
|
+
"status",
|
|
1837
|
+
"list",
|
|
1838
|
+
"create",
|
|
1839
|
+
"template",
|
|
1840
|
+
"resume",
|
|
1841
|
+
]).describe("Workflow subcommand."),
|
|
1842
|
+
ref: tool.schema.string().optional().describe("Workflow ref (e.g. workflow:release). Required for start; accepted by next/status as a target."),
|
|
1843
|
+
target: tool.schema.string().optional().describe("Run id or workflow ref for next/status. When a workflow ref is passed to 'next', a new run is auto-started."),
|
|
1844
|
+
run_id: tool.schema.string().optional().describe("Workflow run id. Required for complete and resume."),
|
|
1845
|
+
params: tool.schema.string().optional().describe("JSON object string of parameters for start/next."),
|
|
1846
|
+
step: tool.schema.string().optional().describe("Step id to transition (required for action='complete')."),
|
|
1847
|
+
state: tool.schema.enum(["completed", "blocked", "failed", "skipped"]).optional().describe("Step state for 'complete'. Defaults to 'completed'."),
|
|
1848
|
+
notes: tool.schema.string().optional().describe("Freeform notes attached to the step transition."),
|
|
1849
|
+
evidence: tool.schema.string().optional().describe("JSON object string of evidence attached to the step transition."),
|
|
1850
|
+
name: tool.schema.string().optional().describe("Workflow name for action='create'."),
|
|
1851
|
+
from: tool.schema.string().optional().describe("Path to a markdown template for action='create'."),
|
|
1852
|
+
force: tool.schema.boolean().optional().describe("Overwrite an existing workflow on create (requires --from or --reset)."),
|
|
1853
|
+
reset: tool.schema.boolean().optional().describe("Reset to the built-in template for action='create'."),
|
|
1854
|
+
filter_ref: tool.schema.string().optional().describe("Restrict action='list' to runs of this workflow ref."),
|
|
1855
|
+
active_only: tool.schema.boolean().optional().describe("Restrict action='list' to active (non-terminal) runs."),
|
|
1856
|
+
},
|
|
1857
|
+
async execute({
|
|
1858
|
+
action,
|
|
1859
|
+
ref,
|
|
1860
|
+
target,
|
|
1861
|
+
run_id,
|
|
1862
|
+
params,
|
|
1863
|
+
step,
|
|
1864
|
+
state,
|
|
1865
|
+
notes,
|
|
1866
|
+
evidence,
|
|
1867
|
+
name,
|
|
1868
|
+
from,
|
|
1869
|
+
force,
|
|
1870
|
+
reset,
|
|
1871
|
+
filter_ref,
|
|
1872
|
+
active_only,
|
|
1873
|
+
}) {
|
|
1874
|
+
const logMeta = { toolName: "akm_workflow" }
|
|
1875
|
+
switch (action) {
|
|
1876
|
+
case "start": {
|
|
1877
|
+
if (!ref) return JSON.stringify({ ok: false, error: "'ref' is required for action='start'." })
|
|
1878
|
+
const args = ["workflow", "start", ref]
|
|
1879
|
+
if (params) args.push("--params", params)
|
|
1880
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1881
|
+
}
|
|
1882
|
+
case "next": {
|
|
1883
|
+
const picked = target ?? run_id ?? ref
|
|
1884
|
+
if (!picked) return JSON.stringify({ ok: false, error: "'target', 'run_id', or 'ref' is required for action='next'." })
|
|
1885
|
+
const args = ["workflow", "next", picked]
|
|
1886
|
+
if (params) args.push("--params", params)
|
|
1887
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1888
|
+
}
|
|
1889
|
+
case "complete": {
|
|
1890
|
+
if (!run_id) return JSON.stringify({ ok: false, error: "'run_id' is required for action='complete'." })
|
|
1891
|
+
if (!step) return JSON.stringify({ ok: false, error: "'step' is required for action='complete'." })
|
|
1892
|
+
const args = ["workflow", "complete", run_id, "--step", step]
|
|
1893
|
+
if (state) args.push("--state", state)
|
|
1894
|
+
if (notes) args.push("--notes", notes)
|
|
1895
|
+
if (evidence) args.push("--evidence", evidence)
|
|
1896
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1897
|
+
}
|
|
1898
|
+
case "status": {
|
|
1899
|
+
const picked = target ?? run_id ?? ref
|
|
1900
|
+
if (!picked) return JSON.stringify({ ok: false, error: "'target', 'run_id', or 'ref' is required for action='status'." })
|
|
1901
|
+
return runCli(client as unknown as LogCapableClient, ["workflow", "status", picked], logMeta)
|
|
1902
|
+
}
|
|
1903
|
+
case "list": {
|
|
1904
|
+
const args = ["workflow", "list"]
|
|
1905
|
+
if (filter_ref) args.push("--ref", filter_ref)
|
|
1906
|
+
if (active_only) args.push("--active")
|
|
1907
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1908
|
+
}
|
|
1909
|
+
case "create": {
|
|
1910
|
+
if (!name) return JSON.stringify({ ok: false, error: "'name' is required for action='create'." })
|
|
1911
|
+
const args = ["workflow", "create", name]
|
|
1912
|
+
if (from) args.push("--from", from)
|
|
1913
|
+
if (force) args.push("--force")
|
|
1914
|
+
if (reset) args.push("--reset")
|
|
1915
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
1916
|
+
}
|
|
1917
|
+
case "template": {
|
|
1918
|
+
// The workflow template is emitted as raw markdown, not JSON.
|
|
1919
|
+
const command = resolveAkmCommand()
|
|
1920
|
+
if (typeof command !== "string") return JSON.stringify(command)
|
|
1921
|
+
try {
|
|
1922
|
+
const stdout = execFileSync(command, ["workflow", "template"], {
|
|
1923
|
+
encoding: "utf8",
|
|
1924
|
+
timeout: 30_000,
|
|
1925
|
+
})
|
|
1926
|
+
return JSON.stringify({ ok: true, template: stdout })
|
|
1927
|
+
} catch (error: unknown) {
|
|
1928
|
+
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
case "resume": {
|
|
1932
|
+
if (!run_id) return JSON.stringify({ ok: false, error: "'run_id' is required for action='resume'." })
|
|
1933
|
+
return runCli(client as unknown as LogCapableClient, ["workflow", "resume", run_id], logMeta)
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
},
|
|
1937
|
+
}),
|
|
1570
1938
|
akm_upgrade: tool({
|
|
1571
1939
|
description: "Check for or install akm CLI updates.",
|
|
1572
1940
|
args: {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenCode plugin for AKM - search, show, and manage extension assets via the akm CLI, with agentic hooks that auto-load relevant stash assets, record feedback, and harvest session memories so the stash improves every session.",
|
|
5
|
+
"description": "OpenCode plugin for AKM - search, show, and manage extension assets via the akm CLI, including v0.5.0 vaults, wikis, and workflows, with agentic hooks that auto-load relevant stash assets, record feedback, and harvest session memories so the stash improves every session.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"opencode",
|
|
8
8
|
"opencode-ai",
|