akm-opencode 0.4.2 → 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 +527 -73
- 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
|
`
|
|
@@ -92,6 +101,14 @@ type CliLogMeta = {
|
|
|
92
101
|
sessionID?: string
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
type SessionPromptBody = {
|
|
105
|
+
agent: string
|
|
106
|
+
parts: Array<{ type: "text"; text: string }>
|
|
107
|
+
system?: string
|
|
108
|
+
model?: { providerID: string; modelID: string }
|
|
109
|
+
tools?: Record<string, boolean>
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
function formatCliError(error: unknown): string {
|
|
96
113
|
if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") {
|
|
97
114
|
return "The 'akm' CLI was not found on PATH. Install it first from https://github.com/itlackey/akm."
|
|
@@ -480,7 +497,29 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
480
497
|
}
|
|
481
498
|
|
|
482
499
|
type CliError = { ok: false; error: string }
|
|
483
|
-
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
|
|
484
523
|
|
|
485
524
|
type ShowAgentResponse = {
|
|
486
525
|
type: "agent"
|
|
@@ -729,18 +768,111 @@ async function ensureTargetSessionID(input: {
|
|
|
729
768
|
context: { sessionID: string; directory: string }
|
|
730
769
|
title: string
|
|
731
770
|
client: PluginClient
|
|
771
|
+
logClient: LogCapableClient
|
|
772
|
+
toolName: string
|
|
732
773
|
}): Promise<{ ok: true; sessionID: string } | CliError> {
|
|
733
774
|
if (!input.useSubtask) return { ok: true, sessionID: input.context.sessionID }
|
|
734
775
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
776
|
+
try {
|
|
777
|
+
const created = await input.client.session.create({
|
|
778
|
+
body: { parentID: input.context.sessionID, title: input.title },
|
|
779
|
+
})
|
|
780
|
+
if (created.error || !created.data?.id) {
|
|
781
|
+
const reason = created.error ? JSON.stringify(created.error) : "missing child session id"
|
|
782
|
+
await writePluginLog(input.logClient, "error", "AKM dispatch child session failed", {
|
|
783
|
+
subsystem: "dispatch",
|
|
784
|
+
toolName: input.toolName,
|
|
785
|
+
sessionID: input.context.sessionID,
|
|
786
|
+
directory: input.context.directory,
|
|
787
|
+
title: input.title,
|
|
788
|
+
error: reason,
|
|
789
|
+
})
|
|
790
|
+
return { ok: false, error: `Failed to create child session: ${reason}` }
|
|
791
|
+
}
|
|
792
|
+
await writePluginLog(input.logClient, "info", "AKM dispatch child session created", {
|
|
793
|
+
subsystem: "dispatch",
|
|
794
|
+
toolName: input.toolName,
|
|
795
|
+
sessionID: input.context.sessionID,
|
|
796
|
+
directory: input.context.directory,
|
|
797
|
+
childSessionID: created.data.id,
|
|
798
|
+
title: input.title,
|
|
799
|
+
})
|
|
800
|
+
return { ok: true, sessionID: created.data.id }
|
|
801
|
+
} catch (error: unknown) {
|
|
802
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
803
|
+
await writePluginLog(input.logClient, "error", "AKM dispatch child session threw", {
|
|
804
|
+
subsystem: "dispatch",
|
|
805
|
+
toolName: input.toolName,
|
|
806
|
+
sessionID: input.context.sessionID,
|
|
807
|
+
directory: input.context.directory,
|
|
808
|
+
title: input.title,
|
|
809
|
+
error: reason,
|
|
810
|
+
})
|
|
741
811
|
return { ok: false, error: `Failed to create child session: ${reason}` }
|
|
742
812
|
}
|
|
743
|
-
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function promptTargetSession(input: {
|
|
816
|
+
client: PluginClient
|
|
817
|
+
logClient: LogCapableClient
|
|
818
|
+
toolName: string
|
|
819
|
+
context: { sessionID: string; directory: string }
|
|
820
|
+
targetSessionID: string
|
|
821
|
+
promptBody: SessionPromptBody
|
|
822
|
+
failureMessage: string
|
|
823
|
+
ref?: string
|
|
824
|
+
}): Promise<{ ok: true; data: { parts?: unknown } } | CliError> {
|
|
825
|
+
try {
|
|
826
|
+
const promptResponse = await input.client.session.prompt({
|
|
827
|
+
path: { id: input.targetSessionID },
|
|
828
|
+
body: input.promptBody,
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
if (promptResponse.error || !promptResponse.data) {
|
|
832
|
+
const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
|
|
833
|
+
await writePluginLog(input.logClient, "error", "AKM dispatch prompt failed", {
|
|
834
|
+
subsystem: "dispatch",
|
|
835
|
+
toolName: input.toolName,
|
|
836
|
+
sessionID: input.context.sessionID,
|
|
837
|
+
directory: input.context.directory,
|
|
838
|
+
targetSessionID: input.targetSessionID,
|
|
839
|
+
dispatchAgent: input.promptBody.agent,
|
|
840
|
+
ref: input.ref,
|
|
841
|
+
error: reason,
|
|
842
|
+
})
|
|
843
|
+
return {
|
|
844
|
+
ok: false,
|
|
845
|
+
error: `${input.failureMessage}: ${reason}`,
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
await writePluginLog(input.logClient, "info", "AKM dispatch prompt completed", {
|
|
850
|
+
subsystem: "dispatch",
|
|
851
|
+
toolName: input.toolName,
|
|
852
|
+
sessionID: input.context.sessionID,
|
|
853
|
+
directory: input.context.directory,
|
|
854
|
+
targetSessionID: input.targetSessionID,
|
|
855
|
+
dispatchAgent: input.promptBody.agent,
|
|
856
|
+
ref: input.ref,
|
|
857
|
+
})
|
|
858
|
+
return { ok: true, data: promptResponse.data }
|
|
859
|
+
} catch (error: unknown) {
|
|
860
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
861
|
+
await writePluginLog(input.logClient, "error", "AKM dispatch prompt threw", {
|
|
862
|
+
subsystem: "dispatch",
|
|
863
|
+
toolName: input.toolName,
|
|
864
|
+
sessionID: input.context.sessionID,
|
|
865
|
+
directory: input.context.directory,
|
|
866
|
+
targetSessionID: input.targetSessionID,
|
|
867
|
+
dispatchAgent: input.promptBody.agent,
|
|
868
|
+
ref: input.ref,
|
|
869
|
+
error: reason,
|
|
870
|
+
})
|
|
871
|
+
return {
|
|
872
|
+
ok: false,
|
|
873
|
+
error: `${input.failureMessage}: ${reason}`,
|
|
874
|
+
}
|
|
875
|
+
}
|
|
744
876
|
}
|
|
745
877
|
|
|
746
878
|
function splitArguments(raw: string): string[] {
|
|
@@ -767,7 +899,7 @@ function normalizeSearchSource(source: "local" | "stash" | "registry" | "both"):
|
|
|
767
899
|
|
|
768
900
|
function createSearchArgs(input: {
|
|
769
901
|
query: string
|
|
770
|
-
type?: AssetType | "any"
|
|
902
|
+
type?: AssetType | "any" | string
|
|
771
903
|
limit?: number
|
|
772
904
|
source?: "local" | "stash" | "registry" | "both"
|
|
773
905
|
defaultSource?: "local" | "stash" | "registry" | "both"
|
|
@@ -787,19 +919,11 @@ function createSearchArgs(input: {
|
|
|
787
919
|
type PluginClient = {
|
|
788
920
|
session: {
|
|
789
921
|
create: (input: {
|
|
790
|
-
query: { directory: string }
|
|
791
922
|
body: { parentID: string; title: string }
|
|
792
923
|
}) => Promise<{ data?: { id?: string }; error?: unknown }>
|
|
793
924
|
prompt: (input: {
|
|
794
|
-
query: { directory: string }
|
|
795
925
|
path: { id: string }
|
|
796
|
-
body:
|
|
797
|
-
agent: string
|
|
798
|
-
parts: Array<{ type: "text"; text: string }>
|
|
799
|
-
system?: string
|
|
800
|
-
model?: { providerID: string; modelID: string }
|
|
801
|
-
tools?: Record<string, boolean>
|
|
802
|
-
}
|
|
926
|
+
body: SessionPromptBody
|
|
803
927
|
}) => Promise<{ data?: { parts?: unknown }; error?: unknown }>
|
|
804
928
|
}
|
|
805
929
|
}
|
|
@@ -980,8 +1104,10 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
980
1104
|
? `opencode auto: ${input.tool} succeeded`
|
|
981
1105
|
: `opencode auto: ${input.tool} failed`
|
|
982
1106
|
for (const ref of allRefs) {
|
|
983
|
-
// Memories
|
|
984
|
-
|
|
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
|
|
985
1111
|
const ok = recordFeedbackSync(ref, feedback, note)
|
|
986
1112
|
if (!ok) break
|
|
987
1113
|
}
|
|
@@ -989,11 +1115,11 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
989
1115
|
},
|
|
990
1116
|
tool: {
|
|
991
1117
|
akm_search: tool({
|
|
992
|
-
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.",
|
|
993
1119
|
args: {
|
|
994
1120
|
query: tool.schema.string().describe("Case-insensitive substring search."),
|
|
995
1121
|
type: tool.schema
|
|
996
|
-
.enum(
|
|
1122
|
+
.enum(ASSET_TYPES as unknown as [string, ...string[]])
|
|
997
1123
|
.optional()
|
|
998
1124
|
.describe("Optional type filter. Defaults to 'any'."),
|
|
999
1125
|
limit: tool.schema.number().optional().describe("Maximum number of hits to return. Defaults to 20."),
|
|
@@ -1011,7 +1137,7 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1011
1137
|
args: {
|
|
1012
1138
|
query: tool.schema.string().describe("Search query for installable registry kits."),
|
|
1013
1139
|
type: tool.schema
|
|
1014
|
-
.enum(
|
|
1140
|
+
.enum(ASSET_TYPES as unknown as [string, ...string[]])
|
|
1015
1141
|
.optional()
|
|
1016
1142
|
.describe("Optional asset type filter. Defaults to 'any'."),
|
|
1017
1143
|
limit: tool.schema.number().optional().describe("Maximum number of registry hits to return. Defaults to 20."),
|
|
@@ -1077,12 +1203,29 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1077
1203
|
},
|
|
1078
1204
|
}),
|
|
1079
1205
|
akm_add: tool({
|
|
1080
|
-
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.",
|
|
1081
1207
|
args: {
|
|
1082
|
-
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)."),
|
|
1083
1217
|
},
|
|
1084
|
-
async execute({ package_ref }) {
|
|
1085
|
-
|
|
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" })
|
|
1086
1229
|
},
|
|
1087
1230
|
}),
|
|
1088
1231
|
akm_list: tool({
|
|
@@ -1203,6 +1346,8 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1203
1346
|
context: { sessionID: context.sessionID, directory: context.directory },
|
|
1204
1347
|
title: "akm:curator",
|
|
1205
1348
|
client: client as unknown as PluginClient,
|
|
1349
|
+
logClient,
|
|
1350
|
+
toolName: "akm_evolve",
|
|
1206
1351
|
})
|
|
1207
1352
|
if (!targetSession.ok) return JSON.stringify(targetSession)
|
|
1208
1353
|
|
|
@@ -1210,23 +1355,20 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1210
1355
|
? `Review recent AKM activity with an emphasis on: ${focus.trim()}. Produce the prioritized action list described in the system prompt.`
|
|
1211
1356
|
: "Review recent AKM activity and produce the prioritized action list described in the system prompt."
|
|
1212
1357
|
|
|
1213
|
-
const promptResponse = await
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1358
|
+
const promptResponse = await promptTargetSession({
|
|
1359
|
+
client: client as unknown as PluginClient,
|
|
1360
|
+
logClient,
|
|
1361
|
+
toolName: "akm_evolve",
|
|
1362
|
+
context: { sessionID: context.sessionID, directory: context.directory },
|
|
1363
|
+
targetSessionID: targetSession.sessionID,
|
|
1364
|
+
failureMessage: "Failed to dispatch curator",
|
|
1365
|
+
promptBody: {
|
|
1217
1366
|
agent: targetAgent,
|
|
1218
1367
|
system: CURATOR_AGENT_PROMPT,
|
|
1219
1368
|
parts: [{ type: "text", text: task }],
|
|
1220
1369
|
},
|
|
1221
1370
|
})
|
|
1222
|
-
|
|
1223
|
-
if (promptResponse.error || !promptResponse.data) {
|
|
1224
|
-
const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
|
|
1225
|
-
return JSON.stringify({
|
|
1226
|
-
ok: false,
|
|
1227
|
-
error: `Failed to dispatch curator: ${reason}`,
|
|
1228
|
-
})
|
|
1229
|
-
}
|
|
1371
|
+
if (!promptResponse.ok) return JSON.stringify(promptResponse)
|
|
1230
1372
|
|
|
1231
1373
|
return JSON.stringify({
|
|
1232
1374
|
ok: true,
|
|
@@ -1286,16 +1428,12 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1286
1428
|
context: { sessionID: context.sessionID, directory: context.directory },
|
|
1287
1429
|
title: `akm:${shown.name}`,
|
|
1288
1430
|
client: client as unknown as PluginClient,
|
|
1431
|
+
logClient,
|
|
1432
|
+
toolName: "akm_agent",
|
|
1289
1433
|
})
|
|
1290
1434
|
if (!targetSession.ok) return JSON.stringify(targetSession)
|
|
1291
1435
|
|
|
1292
|
-
const promptBody: {
|
|
1293
|
-
agent: string
|
|
1294
|
-
system: string
|
|
1295
|
-
parts: Array<{ type: "text"; text: string }>
|
|
1296
|
-
model?: { providerID: string; modelID: string }
|
|
1297
|
-
tools?: Record<string, boolean>
|
|
1298
|
-
} = {
|
|
1436
|
+
const promptBody: SessionPromptBody = {
|
|
1299
1437
|
agent: targetAgent,
|
|
1300
1438
|
system: shown.prompt,
|
|
1301
1439
|
parts: [{ type: "text", text: task_prompt }],
|
|
@@ -1303,19 +1441,17 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1303
1441
|
if (model) promptBody.model = model
|
|
1304
1442
|
if (tools) promptBody.tools = tools
|
|
1305
1443
|
|
|
1306
|
-
const promptResponse = await
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1444
|
+
const promptResponse = await promptTargetSession({
|
|
1445
|
+
client: client as unknown as PluginClient,
|
|
1446
|
+
logClient,
|
|
1447
|
+
toolName: "akm_agent",
|
|
1448
|
+
context: { sessionID: context.sessionID, directory: context.directory },
|
|
1449
|
+
targetSessionID: targetSession.sessionID,
|
|
1450
|
+
promptBody,
|
|
1451
|
+
failureMessage: `Failed to dispatch prompt for ${resolved.ref}`,
|
|
1452
|
+
ref: resolved.ref,
|
|
1310
1453
|
})
|
|
1311
|
-
|
|
1312
|
-
if (promptResponse.error || !promptResponse.data) {
|
|
1313
|
-
const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
|
|
1314
|
-
return JSON.stringify({
|
|
1315
|
-
ok: false,
|
|
1316
|
-
error: `Failed to dispatch prompt for ${resolved.ref}: ${reason}`,
|
|
1317
|
-
})
|
|
1318
|
-
}
|
|
1454
|
+
if (!promptResponse.ok) return JSON.stringify(promptResponse)
|
|
1319
1455
|
|
|
1320
1456
|
return JSON.stringify({
|
|
1321
1457
|
ok: true,
|
|
@@ -1370,25 +1506,25 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1370
1506
|
context: { sessionID: context.sessionID, directory: context.directory },
|
|
1371
1507
|
title: `akm:cmd:${shown.name}`,
|
|
1372
1508
|
client: client as unknown as PluginClient,
|
|
1509
|
+
logClient,
|
|
1510
|
+
toolName: "akm_cmd",
|
|
1373
1511
|
})
|
|
1374
1512
|
if (!targetSession.ok) return JSON.stringify(targetSession)
|
|
1375
1513
|
|
|
1376
|
-
const promptResponse = await
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1514
|
+
const promptResponse = await promptTargetSession({
|
|
1515
|
+
client: client as unknown as PluginClient,
|
|
1516
|
+
logClient,
|
|
1517
|
+
toolName: "akm_cmd",
|
|
1518
|
+
context: { sessionID: context.sessionID, directory: context.directory },
|
|
1519
|
+
targetSessionID: targetSession.sessionID,
|
|
1520
|
+
failureMessage: `Failed to execute command ${resolved.ref}`,
|
|
1521
|
+
ref: resolved.ref,
|
|
1522
|
+
promptBody: {
|
|
1380
1523
|
agent: targetAgent,
|
|
1381
1524
|
parts: [{ type: "text", text: rendered }],
|
|
1382
1525
|
},
|
|
1383
1526
|
})
|
|
1384
|
-
|
|
1385
|
-
if (promptResponse.error || !promptResponse.data) {
|
|
1386
|
-
const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
|
|
1387
|
-
return JSON.stringify({
|
|
1388
|
-
ok: false,
|
|
1389
|
-
error: `Failed to execute command ${resolved.ref}: ${reason}`,
|
|
1390
|
-
})
|
|
1391
|
-
}
|
|
1527
|
+
if (!promptResponse.ok) return JSON.stringify(promptResponse)
|
|
1392
1528
|
|
|
1393
1529
|
return JSON.stringify({
|
|
1394
1530
|
ok: true,
|
|
@@ -1481,6 +1617,324 @@ export const AkmPlugin: Plugin = async ({ client }) => {
|
|
|
1481
1617
|
return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_sources" })
|
|
1482
1618
|
},
|
|
1483
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
|
+
}),
|
|
1484
1938
|
akm_upgrade: tool({
|
|
1485
1939
|
description: "Check for or install akm CLI updates.",
|
|
1486
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",
|