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.
Files changed (3) hide show
  1. package/README.md +30 -5
  2. package/index.ts +382 -14
  3. 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** extension assets from stash directories and registries — 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.
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 for scripts, skills, commands, agents, and knowledge |
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 directories |
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
- └── knowledge/ # markdown files
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, or knowledge doc in the working stash and reindex with akm_index.
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 = "agent" | "command" | "knowledge" | "memory" | "script" | "skill"
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 do not accept feedback in the current CLI.
1077
- if (ref.startsWith("memory:")) continue
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 memories. Use source='registry' or akm_registry_search for installable community kits.",
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(["agent", "command", "knowledge", "memory", "script", "skill", "any"])
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(["agent", "command", "knowledge", "memory", "script", "skill", "any"])
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. Installed kits become searchable alongside local assets.",
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
- return runCli(client as unknown as LogCapableClient, ["add", package_ref], { toolName: "akm_add" })
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.4.3",
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",