akm-opencode 0.2.0 → 0.4.1
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 +47 -8
- package/index.ts +626 -22
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# akm-opencode
|
|
2
2
|
|
|
3
|
-
OpenCode plugin for the [
|
|
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.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -23,14 +23,53 @@ Add to your OpenCode config (`opencode.json`):
|
|
|
23
23
|
| `akm_agent` | Dispatch a stash `agent:*` into OpenCode using the stash prompt and metadata |
|
|
24
24
|
| `akm_cmd` | Execute a stash `command:*` template in OpenCode via SDK session prompting |
|
|
25
25
|
| `akm_add` | Install kits from npm, GitHub, git URLs, or local directories |
|
|
26
|
-
| `akm_list` | List
|
|
27
|
-
| `akm_remove` | Remove
|
|
28
|
-
| `akm_update` | Update one
|
|
26
|
+
| `akm_list` | List configured AKM sources |
|
|
27
|
+
| `akm_remove` | Remove a configured AKM source and reindex |
|
|
28
|
+
| `akm_update` | Update one managed source or all managed sources |
|
|
29
29
|
| `akm_clone` | Clone an asset into the working stash or a custom destination for editing |
|
|
30
|
+
| `akm_remember` | Record a memory in the default stash |
|
|
31
|
+
| `akm_feedback` | Record positive or negative feedback for a stash asset |
|
|
30
32
|
| `akm_config` | Get, set, unset, list, or inspect akm configuration (including `config path --all`) |
|
|
31
33
|
| `akm_run` | Execute a stash script using its `run` field |
|
|
32
|
-
| `akm_sources` |
|
|
34
|
+
| `akm_sources` | Backward-compatible alias that lists configured AKM sources |
|
|
33
35
|
| `akm_upgrade` | Check for or install akm CLI updates |
|
|
36
|
+
| `akm_curate` | Curate the stash for a task or topic and return ranked matches the agent can use |
|
|
37
|
+
| `akm_evolve` | Dispatch the AKM curator agent to review recent session activity and propose stash improvements |
|
|
38
|
+
|
|
39
|
+
## Compound-engineering hooks
|
|
40
|
+
|
|
41
|
+
The plugin subscribes to OpenCode lifecycle events so AKM participates in the
|
|
42
|
+
session loop instead of waiting to be called. Every hook is non-blocking and
|
|
43
|
+
fails silently when `akm` is not on PATH — the TUI is never affected.
|
|
44
|
+
|
|
45
|
+
| Event | What happens |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| **`session.created`** (event hook) | Warms the stash index in the background and caches `akm hints` for the next system transform so the agent knows the CLI surface area at turn 0. |
|
|
48
|
+
| **`chat.message`** | Runs `akm curate "<prompt>"` on each user message (prompts shorter than `AKM_CURATE_MIN_CHARS` are skipped). The top matches are stored for injection. Memory intents (prompts mentioning "remember" / "memory") are tracked in the session buffer. |
|
|
49
|
+
| **`experimental.chat.system.transform`** | Appends the cached hints (once per session) and the curated context (once per turn) to the model's system prompt so the agent sees relevant stash assets before answering. |
|
|
50
|
+
| **`tool.execute.after`** (`akm_*` tools) | Logs asset usage, accumulates refs into the session buffer, and records `akm feedback <ref> --positive` / `--negative` automatically based on whether the tool succeeded or failed. Never recurses into `akm_feedback` and skips `memory:` refs. |
|
|
51
|
+
| **`stop`** / **`session.idle`** / **`session.compacted`** / **`session.deleted`** | Flushes the per-session buffer into a `memory:opencode-session-YYYYMMDD-<sid>` memory so every meaningful session contributes durable context for future searches. Requires at least two observations before persisting. |
|
|
52
|
+
|
|
53
|
+
### Environment overrides
|
|
54
|
+
|
|
55
|
+
| Variable | Default | Purpose |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `AKM_AUTO_CURATE` | `1` | Set to `0` to disable automatic `akm curate` on user messages. |
|
|
58
|
+
| `AKM_AUTO_FEEDBACK` | `1` | Set to `0` to disable automatic `akm feedback` on tool success/failure. |
|
|
59
|
+
| `AKM_AUTO_HINTS` | `1` | Set to `0` to skip injecting `akm hints` at session start. |
|
|
60
|
+
| `AKM_AUTO_MEMORY` | `1` | Set to `0` to disable automatic session-summary memories. |
|
|
61
|
+
| `AKM_CURATE_LIMIT` | `5` | Max curated results injected into context per prompt. |
|
|
62
|
+
| `AKM_CURATE_MIN_CHARS` | `16` | Minimum prompt length before curation runs. |
|
|
63
|
+
| `AKM_CURATE_TIMEOUT` | `8` | Wall-clock seconds for `akm` invocations inside hooks. |
|
|
64
|
+
|
|
65
|
+
### Curator agent
|
|
66
|
+
|
|
67
|
+
`akm_evolve` dispatches a child OpenCode session running a built-in curator
|
|
68
|
+
prompt that reviews recent AKM activity (OpenCode app logs, session-summary
|
|
69
|
+
memories, live stash) and produces a prioritized action list: hot assets to
|
|
70
|
+
promote, cold ones to investigate, coverage gaps to draft, duplicates to
|
|
71
|
+
consolidate. The curator never applies destructive changes without explicit
|
|
72
|
+
user approval.
|
|
34
73
|
|
|
35
74
|
### Registry discovery
|
|
36
75
|
|
|
@@ -79,9 +118,9 @@ When the plugin loads, it runs `bun install -g akm-cli@latest` so it always pick
|
|
|
79
118
|
|
|
80
119
|
```sh
|
|
81
120
|
# macOS / Linux
|
|
82
|
-
curl -fsSL https://raw.githubusercontent.com/itlackey/
|
|
121
|
+
curl -fsSL https://raw.githubusercontent.com/itlackey/akm/main/install.sh | bash
|
|
83
122
|
# PowerShell (Windows)
|
|
84
|
-
irm https://raw.githubusercontent.com/itlackey/
|
|
123
|
+
irm https://raw.githubusercontent.com/itlackey/akm/main/install.ps1 -OutFile install.ps1; ./install.ps1
|
|
85
124
|
|
|
86
125
|
# Or via Bun
|
|
87
126
|
bun install -g akm-cli@latest
|
|
@@ -110,6 +149,6 @@ Assets are resolved from three source types: **working** (local stash), **search
|
|
|
110
149
|
|
|
111
150
|
## Docs
|
|
112
151
|
|
|
113
|
-
- [
|
|
152
|
+
- [AKM CLI](https://github.com/itlackey/akm)
|
|
114
153
|
- [OpenCode Plugins](https://opencode.ai/docs/plugins/)
|
|
115
154
|
- [OpenCode Custom Tools](https://opencode.ai/docs/custom-tools/)
|
package/index.ts
CHANGED
|
@@ -5,6 +5,71 @@ import path from "node:path"
|
|
|
5
5
|
let resolvedAkmCommand = "akm"
|
|
6
6
|
const autoInstallPackageRef = "akm-cli@latest"
|
|
7
7
|
|
|
8
|
+
const AKM_AUTO_FEEDBACK = (process.env.AKM_AUTO_FEEDBACK ?? "1") !== "0"
|
|
9
|
+
const AKM_AUTO_MEMORY = (process.env.AKM_AUTO_MEMORY ?? "1") !== "0"
|
|
10
|
+
const AKM_AUTO_CURATE = (process.env.AKM_AUTO_CURATE ?? "1") !== "0"
|
|
11
|
+
const AKM_AUTO_HINTS = (process.env.AKM_AUTO_HINTS ?? "1") !== "0"
|
|
12
|
+
const AKM_CURATE_LIMIT = Math.max(1, Number(process.env.AKM_CURATE_LIMIT ?? "5") || 5)
|
|
13
|
+
const AKM_CURATE_MIN_CHARS = Math.max(1, Number(process.env.AKM_CURATE_MIN_CHARS ?? "16") || 16)
|
|
14
|
+
const AKM_CURATE_TIMEOUT_MS = Math.max(1_000, (Number(process.env.AKM_CURATE_TIMEOUT ?? "8") || 8) * 1_000)
|
|
15
|
+
|
|
16
|
+
// Per-session state that drives the compound-engineering loop.
|
|
17
|
+
// These maps are keyed by OpenCode sessionID.
|
|
18
|
+
const sessionHints = new Map<string, string>()
|
|
19
|
+
const sessionCurated = new Map<string, string>()
|
|
20
|
+
type SessionBufferEntry = {
|
|
21
|
+
timestamp: string
|
|
22
|
+
kind: "memory-intent" | "tool-ref"
|
|
23
|
+
toolName?: string
|
|
24
|
+
ref?: string
|
|
25
|
+
status?: "positive" | "negative" | "unknown"
|
|
26
|
+
note?: string
|
|
27
|
+
}
|
|
28
|
+
const sessionBuffer = new Map<string, SessionBufferEntry[]>()
|
|
29
|
+
const sessionMemoryCaptured = new Set<string>()
|
|
30
|
+
|
|
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
|
|
33
|
+
|
|
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
|
+
|
|
36
|
+
Inputs you should inspect:
|
|
37
|
+
1. OpenCode app logs that include the "akm-opencode" service (feedback, memory, tool invocations).
|
|
38
|
+
2. Session-summary memories named memory:opencode-session-*.
|
|
39
|
+
3. The live stash: call akm_list, akm_search "" --limit 50, and akm_show <ref>.
|
|
40
|
+
|
|
41
|
+
Signals to act on:
|
|
42
|
+
- Hot refs: assets repeatedly appearing in positive tool outcomes. Call akm_feedback <ref> positive --note "curator: consistently useful" to reinforce.
|
|
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.
|
|
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.
|
|
47
|
+
|
|
48
|
+
Rules of engagement:
|
|
49
|
+
- Never apply destructive changes without explicit user approval.
|
|
50
|
+
- Report findings as a prioritized action list of concrete akm_* tool calls the user can run.
|
|
51
|
+
- Prefer small, reversible edits: promote via positive feedback, draft a candidate skill, or clone and tweak.
|
|
52
|
+
- When drafting new assets, write them into the working stash directory (akm_config get stashDir) under skills/, commands/, agents/, knowledge/, or scripts/. Call akm_index when finished.
|
|
53
|
+
- When finished, persist your own summary with akm_remember (name: curator-run-<timestamp>) so the next curator run can build on yours.
|
|
54
|
+
|
|
55
|
+
Output shape: end every run with a markdown report that has these sections:
|
|
56
|
+
|
|
57
|
+
## Hot assets (promote)
|
|
58
|
+
- <ref> — why it helped — command to run
|
|
59
|
+
|
|
60
|
+
## Cold assets (investigate)
|
|
61
|
+
- <ref> — failure signal — proposed fix
|
|
62
|
+
|
|
63
|
+
## Coverage gaps
|
|
64
|
+
- <theme> — proposed asset (type, name, one-line description)
|
|
65
|
+
|
|
66
|
+
## Duplicates / drift
|
|
67
|
+
- <ref a> vs <ref b> — consolidation proposal
|
|
68
|
+
|
|
69
|
+
## Housekeeping
|
|
70
|
+
- stale memories, reindex needs, config tweaks
|
|
71
|
+
`
|
|
72
|
+
|
|
8
73
|
type LogLevel = "debug" | "info" | "warn" | "error"
|
|
9
74
|
|
|
10
75
|
type LogCapableClient = {
|
|
@@ -29,7 +94,7 @@ type CliLogMeta = {
|
|
|
29
94
|
|
|
30
95
|
function formatCliError(error: unknown): string {
|
|
31
96
|
if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") {
|
|
32
|
-
return "The 'akm' CLI was not found on PATH. Install it first from https://github.com/itlackey/
|
|
97
|
+
return "The 'akm' CLI was not found on PATH. Install it first from https://github.com/itlackey/akm."
|
|
33
98
|
}
|
|
34
99
|
return error instanceof Error ? error.message : String(error)
|
|
35
100
|
}
|
|
@@ -62,6 +127,209 @@ async function writePluginLog(client: LogCapableClient, level: LogLevel, message
|
|
|
62
127
|
}
|
|
63
128
|
}
|
|
64
129
|
|
|
130
|
+
function nowIso(): string {
|
|
131
|
+
return new Date().toISOString()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function addBufferEntry(sessionID: string | undefined, entry: Omit<SessionBufferEntry, "timestamp">) {
|
|
135
|
+
if (!sessionID) return
|
|
136
|
+
const buf = sessionBuffer.get(sessionID) ?? []
|
|
137
|
+
buf.push({ timestamp: nowIso(), ...entry })
|
|
138
|
+
sessionBuffer.set(sessionID, buf)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Synchronous CLI invocation used by the lifecycle hooks — the plugin host does
|
|
142
|
+
// not await these in a hot path, but we still cap execution time so a slow
|
|
143
|
+
// stash never wedges the session loop.
|
|
144
|
+
function runCliSyncRaw(args: string[], timeoutMs: number): { ok: true; stdout: string } | { ok: false; error: string } {
|
|
145
|
+
const command = resolveAkmCommand()
|
|
146
|
+
if (typeof command !== "string") return { ok: false, error: command.error }
|
|
147
|
+
try {
|
|
148
|
+
const stdout = execFileSync(command, args, {
|
|
149
|
+
encoding: "utf8",
|
|
150
|
+
timeout: timeoutMs,
|
|
151
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
152
|
+
})
|
|
153
|
+
return { ok: true, stdout }
|
|
154
|
+
} catch (error: unknown) {
|
|
155
|
+
return { ok: false, error: formatCliError(error) }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runCurateForPrompt(text: string): string | null {
|
|
160
|
+
if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
|
|
161
|
+
const result = runCliSyncRaw(
|
|
162
|
+
[
|
|
163
|
+
"--for-agent",
|
|
164
|
+
"--format",
|
|
165
|
+
"text",
|
|
166
|
+
"--detail",
|
|
167
|
+
"summary",
|
|
168
|
+
"-q",
|
|
169
|
+
"curate",
|
|
170
|
+
text,
|
|
171
|
+
"--limit",
|
|
172
|
+
String(AKM_CURATE_LIMIT),
|
|
173
|
+
],
|
|
174
|
+
AKM_CURATE_TIMEOUT_MS,
|
|
175
|
+
)
|
|
176
|
+
if (!result.ok) return null
|
|
177
|
+
const body = result.stdout.trim()
|
|
178
|
+
return body || null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function runHintsForSession(): string | null {
|
|
182
|
+
const result = runCliSyncRaw(["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS)
|
|
183
|
+
if (!result.ok) return null
|
|
184
|
+
const body = result.stdout.trim()
|
|
185
|
+
return body || null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function warmIndexInBackground(): void {
|
|
189
|
+
const command = resolveAkmCommand()
|
|
190
|
+
if (typeof command !== "string") return
|
|
191
|
+
try {
|
|
192
|
+
// Fire and forget — execSync with a timeout would block, so spawn via the
|
|
193
|
+
// shell and detach. Errors here are never surfaced to the session.
|
|
194
|
+
execSync(`${JSON.stringify(command)} index >/dev/null 2>&1 &`, { timeout: 2_000 })
|
|
195
|
+
} catch {
|
|
196
|
+
// Intentionally ignore — warming is best-effort.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function recordFeedbackSync(ref: string, sentiment: "positive" | "negative", note: string): boolean {
|
|
201
|
+
const result = runCliSyncRaw(
|
|
202
|
+
[
|
|
203
|
+
"--format",
|
|
204
|
+
"json",
|
|
205
|
+
"-q",
|
|
206
|
+
"feedback",
|
|
207
|
+
ref,
|
|
208
|
+
sentiment === "positive" ? "--positive" : "--negative",
|
|
209
|
+
"--note",
|
|
210
|
+
note,
|
|
211
|
+
],
|
|
212
|
+
AKM_CURATE_TIMEOUT_MS,
|
|
213
|
+
)
|
|
214
|
+
return result.ok
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function captureSessionMemory(sessionID: string, reason: string): string | null {
|
|
218
|
+
if (!AKM_AUTO_MEMORY) return null
|
|
219
|
+
if (!sessionID) return null
|
|
220
|
+
if (sessionMemoryCaptured.has(sessionID)) return null
|
|
221
|
+
const entries = sessionBuffer.get(sessionID) ?? []
|
|
222
|
+
// Require at least two observations before persisting — single events are noise.
|
|
223
|
+
if (entries.length < 2) {
|
|
224
|
+
sessionBuffer.delete(sessionID)
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const lines: string[] = []
|
|
229
|
+
lines.push(`# Session summary (${nowIso()})`)
|
|
230
|
+
lines.push(`Reason: ${reason}`)
|
|
231
|
+
lines.push(`Session: ${sessionID}`)
|
|
232
|
+
lines.push("")
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
if (entry.kind === "memory-intent") {
|
|
235
|
+
lines.push(`## ${entry.timestamp} — user memory intent`)
|
|
236
|
+
if (entry.note) lines.push(entry.note)
|
|
237
|
+
lines.push("")
|
|
238
|
+
} else {
|
|
239
|
+
lines.push(`## ${entry.timestamp} — ${entry.toolName ?? "tool"} ${entry.status ?? "unknown"}`)
|
|
240
|
+
if (entry.ref) lines.push(`- ref: ${entry.ref}`)
|
|
241
|
+
if (entry.note) lines.push(`- note: ${entry.note}`)
|
|
242
|
+
lines.push("")
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const body = lines.join("\n")
|
|
246
|
+
|
|
247
|
+
const dateTag = new Date().toISOString().replace(/[-:]/g, "").slice(0, 8)
|
|
248
|
+
const shortSid = sessionID.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 8) || "session"
|
|
249
|
+
const name = `opencode-session-${dateTag}-${shortSid}`
|
|
250
|
+
|
|
251
|
+
const command = resolveAkmCommand()
|
|
252
|
+
if (typeof command !== "string") {
|
|
253
|
+
sessionMemoryCaptured.add(sessionID)
|
|
254
|
+
sessionBuffer.delete(sessionID)
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
execFileSync(command, ["--format", "json", "-q", "remember", "--name", name, "--force"], {
|
|
259
|
+
encoding: "utf8",
|
|
260
|
+
timeout: AKM_CURATE_TIMEOUT_MS * 2,
|
|
261
|
+
input: body,
|
|
262
|
+
})
|
|
263
|
+
sessionMemoryCaptured.add(sessionID)
|
|
264
|
+
sessionBuffer.delete(sessionID)
|
|
265
|
+
return `memory:${name}`
|
|
266
|
+
} catch {
|
|
267
|
+
sessionMemoryCaptured.add(sessionID)
|
|
268
|
+
sessionBuffer.delete(sessionID)
|
|
269
|
+
return null
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function extractToolRefs(toolName: string, args: Record<string, unknown>, output: unknown): string[] {
|
|
274
|
+
const refs = new Set<string>()
|
|
275
|
+
const addMatches = (value: unknown) => {
|
|
276
|
+
if (typeof value !== "string") return
|
|
277
|
+
const matches = value.match(AKM_REF_PATTERN)
|
|
278
|
+
if (matches) for (const ref of matches) refs.add(ref)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const key of ["ref", "package_ref"]) {
|
|
282
|
+
addMatches((args as Record<string, unknown>)[key])
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (output && typeof output === "object") {
|
|
286
|
+
const o = output as Record<string, unknown>
|
|
287
|
+
addMatches(o.ref)
|
|
288
|
+
if (Array.isArray(o.hits)) {
|
|
289
|
+
for (const hit of o.hits) {
|
|
290
|
+
if (hit && typeof hit === "object") addMatches((hit as Record<string, unknown>).ref)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(o.assetHits)) {
|
|
294
|
+
for (const hit of o.assetHits) {
|
|
295
|
+
if (hit && typeof hit === "object") addMatches((hit as Record<string, unknown>).ref)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (toolName === "akm_remember" && typeof o.ref === "string") addMatches(o.ref)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return [...refs]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const AKM_HINTS_PREFIX = [
|
|
305
|
+
"# AKM is available in this session",
|
|
306
|
+
"",
|
|
307
|
+
"You have an AKM stash on this machine. Before writing anything from scratch, call `akm_search` or `akm_curate` to see if the stash already covers it. Record `akm_feedback <ref> positive|negative` whenever an asset materially helps or misses, and use `akm_remember` to persist durable learnings so future sessions inherit them.",
|
|
308
|
+
].join("\n")
|
|
309
|
+
|
|
310
|
+
const AKM_CURATED_HEADER = "# AKM stash — assets relevant to this prompt"
|
|
311
|
+
const AKM_CURATED_TAIL = "\n\nTip: call `akm_show <ref>` to fetch full content, and record `akm_feedback <ref> positive|negative` once you know whether the asset helped."
|
|
312
|
+
|
|
313
|
+
function extractSessionIdFromEvent(payload: unknown): string | undefined {
|
|
314
|
+
if (!payload || typeof payload !== "object") return undefined
|
|
315
|
+
const p = payload as Record<string, unknown>
|
|
316
|
+
const candidates = [
|
|
317
|
+
p.sessionID,
|
|
318
|
+
p.session_id,
|
|
319
|
+
p.session,
|
|
320
|
+
(p.session as Record<string, unknown> | undefined)?.id,
|
|
321
|
+
(p.properties as Record<string, unknown> | undefined)?.sessionID,
|
|
322
|
+
(p.properties as Record<string, unknown> | undefined)?.session_id,
|
|
323
|
+
(p.properties as Record<string, unknown> | undefined)?.id,
|
|
324
|
+
(p.info as Record<string, unknown> | undefined)?.id,
|
|
325
|
+
(p.info as Record<string, unknown> | undefined)?.sessionID,
|
|
326
|
+
]
|
|
327
|
+
for (const value of candidates) {
|
|
328
|
+
if (typeof value === "string" && value) return value
|
|
329
|
+
}
|
|
330
|
+
return undefined
|
|
331
|
+
}
|
|
332
|
+
|
|
65
333
|
function getCommandStatus(command: string): "ok" | "missing" | "error" {
|
|
66
334
|
try {
|
|
67
335
|
execFileSync(command, ["--version"], {
|
|
@@ -154,7 +422,7 @@ function resolveAkmCommand(): string | CliError {
|
|
|
154
422
|
|
|
155
423
|
return {
|
|
156
424
|
ok: false,
|
|
157
|
-
error: `The 'akm' CLI could not be resolved after attempting to install '${autoInstallPackageRef}' with Bun. Install akm from https://github.com/itlackey/
|
|
425
|
+
error: `The 'akm' CLI could not be resolved after attempting to install '${autoInstallPackageRef}' with Bun. Install akm from https://github.com/itlackey/akm.`,
|
|
158
426
|
}
|
|
159
427
|
}
|
|
160
428
|
|
|
@@ -212,7 +480,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
212
480
|
}
|
|
213
481
|
|
|
214
482
|
type CliError = { ok: false; error: string }
|
|
215
|
-
type AssetType = "
|
|
483
|
+
type AssetType = "agent" | "command" | "knowledge" | "memory" | "script" | "skill"
|
|
216
484
|
|
|
217
485
|
type ShowAgentResponse = {
|
|
218
486
|
type: "agent"
|
|
@@ -276,7 +544,7 @@ type SearchHit = {
|
|
|
276
544
|
|
|
277
545
|
type SearchResponse = {
|
|
278
546
|
hits?: SearchHit[]
|
|
279
|
-
source?: "local" | "registry" | "both"
|
|
547
|
+
source?: "local" | "stash" | "registry" | "both"
|
|
280
548
|
stashDir?: string
|
|
281
549
|
timing?: { totalMs?: number; rankMs?: number; embedMs?: number }
|
|
282
550
|
warnings?: string[]
|
|
@@ -376,6 +644,59 @@ function extractText(parts: unknown): string {
|
|
|
376
644
|
return segments.join("\n\n")
|
|
377
645
|
}
|
|
378
646
|
|
|
647
|
+
function parseToolOutput(raw: string): unknown {
|
|
648
|
+
try {
|
|
649
|
+
return JSON.parse(raw)
|
|
650
|
+
} catch {
|
|
651
|
+
return undefined
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function extractMemoryRefs(toolName: string, args: Record<string, unknown>, value: unknown): string[] {
|
|
656
|
+
const refs = new Set<string>()
|
|
657
|
+
const parsed = value && typeof value === "object" ? value as {
|
|
658
|
+
type?: unknown
|
|
659
|
+
ref?: unknown
|
|
660
|
+
name?: unknown
|
|
661
|
+
hits?: unknown
|
|
662
|
+
} : undefined
|
|
663
|
+
|
|
664
|
+
if (toolName === "akm_remember" && typeof parsed?.ref === "string" && parsed.ref) {
|
|
665
|
+
refs.add(parsed.ref)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (parsed?.type === "memory") {
|
|
669
|
+
if (typeof parsed.ref === "string" && parsed.ref) refs.add(parsed.ref)
|
|
670
|
+
if (typeof args.ref === "string" && args.ref) refs.add(args.ref)
|
|
671
|
+
if (refs.size === 0 && typeof parsed.name === "string" && parsed.name) refs.add(`memory:${parsed.name}`)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (Array.isArray(parsed?.hits)) {
|
|
675
|
+
for (const hit of parsed.hits) {
|
|
676
|
+
if (!hit || typeof hit !== "object") continue
|
|
677
|
+
if ((hit as { type?: unknown }).type !== "memory") continue
|
|
678
|
+
const ref = (hit as { ref?: unknown }).ref
|
|
679
|
+
if (typeof ref === "string" && ref) refs.add(ref)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return [...refs]
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function classifyToolFeedback(value: unknown): "positive" | "negative" | undefined {
|
|
687
|
+
if (!value || typeof value !== "object") return undefined
|
|
688
|
+
if (isCliError(value)) return "negative"
|
|
689
|
+
if ("ok" in value && (value as { ok?: unknown }).ok === false) return "negative"
|
|
690
|
+
if ("error" in value && typeof (value as { error?: unknown }).error === "string") return "negative"
|
|
691
|
+
if ("ok" in value && (value as { ok?: unknown }).ok === true) return "positive"
|
|
692
|
+
if ("type" in value || "hits" in value || "assetHits" in value || "sources" in value) return "positive"
|
|
693
|
+
return undefined
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function truncateLogText(value: string, limit = 1_000): string {
|
|
697
|
+
return value.length > limit ? `${value.slice(0, limit)}…` : value
|
|
698
|
+
}
|
|
699
|
+
|
|
379
700
|
async function resolveRefInput(
|
|
380
701
|
client: LogCapableClient,
|
|
381
702
|
input: { ref?: string; query?: string },
|
|
@@ -391,7 +712,7 @@ async function resolveRefInput(
|
|
|
391
712
|
return { ok: false, error: "Provide either 'ref' or 'query'." }
|
|
392
713
|
}
|
|
393
714
|
|
|
394
|
-
const raw = await runCli(client, ["search", query, "--type", type, "--limit", "1", "--detail", "normal", "--source", "
|
|
715
|
+
const raw = await runCli(client, ["search", query, "--type", type, "--limit", "1", "--detail", "normal", "--source", "stash"], meta)
|
|
395
716
|
const parsed = parseCliJson<SearchResponse>(raw)
|
|
396
717
|
if (isCliError(parsed)) return parsed
|
|
397
718
|
|
|
@@ -440,20 +761,24 @@ function renderCommandTemplate(template: string, rawArguments: string): string {
|
|
|
440
761
|
.replace(/\$(\d+)/g, (_m, index: string) => args[Number(index) - 1] ?? "")
|
|
441
762
|
}
|
|
442
763
|
|
|
764
|
+
function normalizeSearchSource(source: "local" | "stash" | "registry" | "both"): "stash" | "registry" | "both" {
|
|
765
|
+
return source === "local" ? "stash" : source
|
|
766
|
+
}
|
|
767
|
+
|
|
443
768
|
function createSearchArgs(input: {
|
|
444
769
|
query: string
|
|
445
770
|
type?: AssetType | "any"
|
|
446
771
|
limit?: number
|
|
447
|
-
source?: "local" | "registry" | "both"
|
|
448
|
-
defaultSource?: "local" | "registry" | "both"
|
|
772
|
+
source?: "local" | "stash" | "registry" | "both"
|
|
773
|
+
defaultSource?: "local" | "stash" | "registry" | "both"
|
|
449
774
|
}): string[] {
|
|
450
775
|
const args = ["search", input.query]
|
|
451
776
|
if (input.type) args.push("--type", input.type)
|
|
452
777
|
if (input.limit) args.push("--limit", String(input.limit))
|
|
453
778
|
if (input.source) {
|
|
454
|
-
args.push("--source", input.source)
|
|
779
|
+
args.push("--source", normalizeSearchSource(input.source))
|
|
455
780
|
} else if (input.defaultSource) {
|
|
456
|
-
args.push("--source", input.defaultSource)
|
|
781
|
+
args.push("--source", normalizeSearchSource(input.defaultSource))
|
|
457
782
|
}
|
|
458
783
|
args.push("--detail", "normal")
|
|
459
784
|
return args
|
|
@@ -479,24 +804,203 @@ type PluginClient = {
|
|
|
479
804
|
}
|
|
480
805
|
}
|
|
481
806
|
|
|
482
|
-
export const
|
|
807
|
+
export const AkmPlugin: Plugin = async ({ client }) => {
|
|
483
808
|
await ensureLatestAkmInstalled(client as unknown as LogCapableClient)
|
|
484
809
|
|
|
810
|
+
const logClient = client as unknown as LogCapableClient
|
|
811
|
+
|
|
485
812
|
return {
|
|
813
|
+
// Events cover the lifecycle boundaries that Claude Code exposes as
|
|
814
|
+
// SessionStart / Stop / PreCompact. We use them to warm the stash, capture
|
|
815
|
+
// hints for the next system transform, and flush per-session memories.
|
|
816
|
+
event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
817
|
+
try {
|
|
818
|
+
const type = event?.type
|
|
819
|
+
if (!type) return
|
|
820
|
+
const sid = extractSessionIdFromEvent(event) ?? extractSessionIdFromEvent((event as { properties?: unknown }).properties)
|
|
821
|
+
if (type === "session.created" || type === "session.updated") {
|
|
822
|
+
if (!sid) return
|
|
823
|
+
if (!AKM_AUTO_HINTS) return
|
|
824
|
+
if (sessionHints.has(sid)) return
|
|
825
|
+
warmIndexInBackground()
|
|
826
|
+
const hints = runHintsForSession()
|
|
827
|
+
if (hints) sessionHints.set(sid, hints)
|
|
828
|
+
} else if (type === "session.compacted" || type === "session.idle" || type === "session.deleted") {
|
|
829
|
+
if (!sid) return
|
|
830
|
+
const captured = captureSessionMemory(sid, type)
|
|
831
|
+
if (captured) {
|
|
832
|
+
await writePluginLog(logClient, "info", "AKM session memory captured", {
|
|
833
|
+
subsystem: "memory",
|
|
834
|
+
actor: "system",
|
|
835
|
+
sessionID: sid,
|
|
836
|
+
reason: type,
|
|
837
|
+
ref: captured,
|
|
838
|
+
})
|
|
839
|
+
}
|
|
840
|
+
// Drop per-session state so a re-created session does not inherit
|
|
841
|
+
// stale hints/curation.
|
|
842
|
+
if (type === "session.deleted") {
|
|
843
|
+
sessionHints.delete(sid)
|
|
844
|
+
sessionCurated.delete(sid)
|
|
845
|
+
sessionMemoryCaptured.delete(sid)
|
|
846
|
+
sessionBuffer.delete(sid)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
} catch {
|
|
850
|
+
// Lifecycle hooks must never throw into the TUI.
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
// Stop is the closest analogue to Claude's Stop/SubagentStop — the user or
|
|
854
|
+
// agent halted the active run. Flush the session buffer so learnings are
|
|
855
|
+
// preserved even if the session.idle event does not fire.
|
|
856
|
+
stop: async (input: unknown) => {
|
|
857
|
+
try {
|
|
858
|
+
const sid = extractSessionIdFromEvent(input)
|
|
859
|
+
if (!sid) return
|
|
860
|
+
const captured = captureSessionMemory(sid, "stop")
|
|
861
|
+
if (captured) {
|
|
862
|
+
await writePluginLog(logClient, "info", "AKM session memory captured", {
|
|
863
|
+
subsystem: "memory",
|
|
864
|
+
actor: "system",
|
|
865
|
+
sessionID: sid,
|
|
866
|
+
reason: "stop",
|
|
867
|
+
ref: captured,
|
|
868
|
+
})
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
// Best-effort only.
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
// experimental.chat.system.transform is how OpenCode exposes the
|
|
875
|
+
// additionalContext channel. We append the cached hints (once per session)
|
|
876
|
+
// and the curated assets (once per turn) so the next LLM call sees them.
|
|
877
|
+
"experimental.chat.system.transform": async (
|
|
878
|
+
input: { sessionID?: string; session_id?: string } | undefined,
|
|
879
|
+
output: { system?: string[] } | undefined,
|
|
880
|
+
) => {
|
|
881
|
+
try {
|
|
882
|
+
if (!output || !Array.isArray(output.system)) return
|
|
883
|
+
const sid = extractSessionIdFromEvent(input) ?? ""
|
|
884
|
+
const hints = sid ? sessionHints.get(sid) : undefined
|
|
885
|
+
if (hints) {
|
|
886
|
+
output.system.push(`${AKM_HINTS_PREFIX}\n\n${hints}`)
|
|
887
|
+
// Only inject hints on the first transform of the session.
|
|
888
|
+
sessionHints.delete(sid)
|
|
889
|
+
}
|
|
890
|
+
const curated = sid ? sessionCurated.get(sid) : undefined
|
|
891
|
+
if (curated) {
|
|
892
|
+
output.system.push(`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`)
|
|
893
|
+
sessionCurated.delete(sid)
|
|
894
|
+
}
|
|
895
|
+
} catch {
|
|
896
|
+
// Never break the turn because of a transform failure.
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
"chat.message": async (input, output) => {
|
|
900
|
+
const text = extractText(output.parts).trim()
|
|
901
|
+
if (!text) return
|
|
902
|
+
await writePluginLog(logClient, "info", "AKM user feedback recorded", {
|
|
903
|
+
subsystem: "feedback",
|
|
904
|
+
actor: "user",
|
|
905
|
+
sessionID: input.sessionID,
|
|
906
|
+
messageID: input.messageID,
|
|
907
|
+
agent: input.agent,
|
|
908
|
+
text: truncateLogText(text),
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
// Compound-engineering loop: on every user message, curate the stash and
|
|
912
|
+
// stash the result so experimental.chat.system.transform can inject it.
|
|
913
|
+
if (AKM_AUTO_CURATE && input.sessionID) {
|
|
914
|
+
const curated = runCurateForPrompt(text)
|
|
915
|
+
if (curated) sessionCurated.set(input.sessionID, curated)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Track explicit memory intents so capture-memory has something durable
|
|
919
|
+
// to flush when the session ends.
|
|
920
|
+
if (/\b(remember|memory|memories)\b/i.test(text)) {
|
|
921
|
+
addBufferEntry(input.sessionID, {
|
|
922
|
+
kind: "memory-intent",
|
|
923
|
+
note: truncateLogText(text, 500),
|
|
924
|
+
})
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
"tool.execute.after": async (input, output) => {
|
|
928
|
+
if (!input.tool.startsWith("akm_")) return
|
|
929
|
+
|
|
930
|
+
const parsed = parseToolOutput(output.output)
|
|
931
|
+
if (!parsed) return
|
|
932
|
+
|
|
933
|
+
const feedback = classifyToolFeedback(parsed)
|
|
934
|
+
if (feedback) {
|
|
935
|
+
await writePluginLog(logClient, feedback === "negative" ? "warn" : "info", "AKM system feedback recorded", {
|
|
936
|
+
subsystem: "feedback",
|
|
937
|
+
actor: "system",
|
|
938
|
+
feedback,
|
|
939
|
+
toolName: input.tool,
|
|
940
|
+
sessionID: input.sessionID,
|
|
941
|
+
callID: input.callID,
|
|
942
|
+
title: output.title,
|
|
943
|
+
error: typeof (parsed as { error?: unknown }).error === "string" ? (parsed as { error?: string }).error : undefined,
|
|
944
|
+
})
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const memoryRefs = extractMemoryRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
948
|
+
if (memoryRefs.length > 0) {
|
|
949
|
+
await writePluginLog(logClient, "info", "AKM memory usage recorded", {
|
|
950
|
+
subsystem: "memory",
|
|
951
|
+
toolName: input.tool,
|
|
952
|
+
sessionID: input.sessionID,
|
|
953
|
+
callID: input.callID,
|
|
954
|
+
refs: memoryRefs,
|
|
955
|
+
})
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Auto-feedback + session buffering: record every asset ref the tool
|
|
959
|
+
// touched so the stash ranking improves over time and so Stop/Compact
|
|
960
|
+
// has material to flush into a session summary memory.
|
|
961
|
+
const allRefs = extractToolRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
962
|
+
if (allRefs.length > 0 && input.sessionID) {
|
|
963
|
+
for (const ref of allRefs) {
|
|
964
|
+
addBufferEntry(input.sessionID, {
|
|
965
|
+
kind: "tool-ref",
|
|
966
|
+
toolName: input.tool,
|
|
967
|
+
ref,
|
|
968
|
+
status: feedback ?? "unknown",
|
|
969
|
+
})
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (
|
|
974
|
+
AKM_AUTO_FEEDBACK
|
|
975
|
+
&& feedback
|
|
976
|
+
&& input.tool !== "akm_feedback"
|
|
977
|
+
&& allRefs.length > 0
|
|
978
|
+
) {
|
|
979
|
+
const note = feedback === "positive"
|
|
980
|
+
? `opencode auto: ${input.tool} succeeded`
|
|
981
|
+
: `opencode auto: ${input.tool} failed`
|
|
982
|
+
for (const ref of allRefs) {
|
|
983
|
+
// Memories do not accept feedback in the current CLI.
|
|
984
|
+
if (ref.startsWith("memory:")) continue
|
|
985
|
+
const ok = recordFeedbackSync(ref, feedback, note)
|
|
986
|
+
if (!ok) break
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
},
|
|
486
990
|
tool: {
|
|
487
991
|
akm_search: tool({
|
|
488
|
-
description: "Search your
|
|
992
|
+
description: "Search your stash or the akm registry for scripts, skills, commands, agents, knowledge, and memories. Use source='registry' or akm_registry_search for installable community kits.",
|
|
489
993
|
args: {
|
|
490
994
|
query: tool.schema.string().describe("Case-insensitive substring search."),
|
|
491
995
|
type: tool.schema
|
|
492
|
-
.enum(["
|
|
996
|
+
.enum(["agent", "command", "knowledge", "memory", "script", "skill", "any"])
|
|
493
997
|
.optional()
|
|
494
998
|
.describe("Optional type filter. Defaults to 'any'."),
|
|
495
999
|
limit: tool.schema.number().optional().describe("Maximum number of hits to return. Defaults to 20."),
|
|
496
1000
|
source: tool.schema
|
|
497
|
-
.enum(["local", "registry", "both"])
|
|
1001
|
+
.enum(["local", "stash", "registry", "both"])
|
|
498
1002
|
.optional()
|
|
499
|
-
.describe("Search source. '
|
|
1003
|
+
.describe("Search source. 'stash' searches local stash directories, 'registry' searches registries, and 'both' searches all sources. 'local' remains a backward-compatible alias for 'stash'."),
|
|
500
1004
|
},
|
|
501
1005
|
async execute({ query, type, limit, source }) {
|
|
502
1006
|
return runCli(client as unknown as LogCapableClient, createSearchArgs({ query, type, limit, source }), { toolName: "akm_search" })
|
|
@@ -507,7 +1011,7 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
|
|
|
507
1011
|
args: {
|
|
508
1012
|
query: tool.schema.string().describe("Search query for installable registry kits."),
|
|
509
1013
|
type: tool.schema
|
|
510
|
-
.enum(["
|
|
1014
|
+
.enum(["agent", "command", "knowledge", "memory", "script", "skill", "any"])
|
|
511
1015
|
.optional()
|
|
512
1016
|
.describe("Optional asset type filter. Defaults to 'any'."),
|
|
513
1017
|
limit: tool.schema.number().optional().describe("Maximum number of registry hits to return. Defaults to 20."),
|
|
@@ -582,25 +1086,25 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
|
|
|
582
1086
|
},
|
|
583
1087
|
}),
|
|
584
1088
|
akm_list: tool({
|
|
585
|
-
description: "List all kits
|
|
1089
|
+
description: "List all configured AKM sources, including local directories, managed kits, and remote providers.",
|
|
586
1090
|
args: {},
|
|
587
1091
|
async execute() {
|
|
588
1092
|
return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_list" })
|
|
589
1093
|
},
|
|
590
1094
|
}),
|
|
591
1095
|
akm_remove: tool({
|
|
592
|
-
description: "Remove
|
|
1096
|
+
description: "Remove a configured AKM source by id, ref, path, URL, or name and reindex the stash.",
|
|
593
1097
|
args: {
|
|
594
|
-
package_ref: tool.schema.string().describe("
|
|
1098
|
+
package_ref: tool.schema.string().describe("Source id, ref, path, URL, or name, such as npm:@scope/kit, owner/repo, or ~/.claude/skills."),
|
|
595
1099
|
},
|
|
596
1100
|
async execute({ package_ref }) {
|
|
597
1101
|
return runCli(client as unknown as LogCapableClient, ["remove", package_ref], { toolName: "akm_remove" })
|
|
598
1102
|
},
|
|
599
1103
|
}),
|
|
600
1104
|
akm_update: tool({
|
|
601
|
-
description: "Update one
|
|
1105
|
+
description: "Update one managed AKM source or all managed sources to the latest available version.",
|
|
602
1106
|
args: {
|
|
603
|
-
package_ref: tool.schema.string().optional().describe("
|
|
1107
|
+
package_ref: tool.schema.string().optional().describe("Managed source id or ref to update."),
|
|
604
1108
|
all: tool.schema.boolean().optional().describe("Update all installed kits."),
|
|
605
1109
|
force: tool.schema.boolean().optional().describe("Force a fresh download even if the version is unchanged."),
|
|
606
1110
|
},
|
|
@@ -634,6 +1138,106 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
|
|
|
634
1138
|
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_clone" })
|
|
635
1139
|
},
|
|
636
1140
|
}),
|
|
1141
|
+
akm_remember: tool({
|
|
1142
|
+
description: "Record a memory in the default AKM stash so it can be searched and shown later.",
|
|
1143
|
+
args: {
|
|
1144
|
+
content: tool.schema.string().describe("Memory content to store."),
|
|
1145
|
+
name: tool.schema.string().optional().describe("Optional memory name."),
|
|
1146
|
+
force: tool.schema.boolean().optional().describe("Overwrite an existing memory with the same name."),
|
|
1147
|
+
},
|
|
1148
|
+
async execute({ content, name, force }) {
|
|
1149
|
+
const args = ["remember", content]
|
|
1150
|
+
if (name) args.push("--name", name)
|
|
1151
|
+
if (force) args.push("--force")
|
|
1152
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_remember" })
|
|
1153
|
+
},
|
|
1154
|
+
}),
|
|
1155
|
+
akm_feedback: tool({
|
|
1156
|
+
description: "Record positive or negative feedback for a stash asset so AKM can improve future ranking.",
|
|
1157
|
+
args: {
|
|
1158
|
+
ref: tool.schema.string().describe("Asset ref to record feedback for."),
|
|
1159
|
+
sentiment: tool.schema.enum(["positive", "negative"]).describe("Whether the feedback is positive or negative."),
|
|
1160
|
+
note: tool.schema.string().optional().describe("Optional note to attach to the feedback."),
|
|
1161
|
+
},
|
|
1162
|
+
async execute({ ref, sentiment, note }) {
|
|
1163
|
+
const args = ["feedback", ref, sentiment === "positive" ? "--positive" : "--negative"]
|
|
1164
|
+
if (note) args.push("--note", note)
|
|
1165
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_feedback" })
|
|
1166
|
+
},
|
|
1167
|
+
}),
|
|
1168
|
+
akm_curate: tool({
|
|
1169
|
+
description: "Curate stash assets for a task or topic. Returns the top matches as a ranked list so the agent can inspect and use them.",
|
|
1170
|
+
args: {
|
|
1171
|
+
query: tool.schema.string().describe("Task, topic, or natural-language description of what you want to do."),
|
|
1172
|
+
limit: tool.schema.number().optional().describe("Maximum number of curated matches to return. Defaults to 6."),
|
|
1173
|
+
detail: tool.schema.enum(["summary", "normal", "full"]).optional().describe("Detail level for each match. Defaults to 'summary'."),
|
|
1174
|
+
},
|
|
1175
|
+
async execute({ query, limit, detail }) {
|
|
1176
|
+
const args = [
|
|
1177
|
+
"--for-agent",
|
|
1178
|
+
"--format",
|
|
1179
|
+
"text",
|
|
1180
|
+
"--detail",
|
|
1181
|
+
detail ?? "summary",
|
|
1182
|
+
"-q",
|
|
1183
|
+
"curate",
|
|
1184
|
+
query,
|
|
1185
|
+
"--limit",
|
|
1186
|
+
String(limit ?? 6),
|
|
1187
|
+
]
|
|
1188
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_curate" })
|
|
1189
|
+
},
|
|
1190
|
+
}),
|
|
1191
|
+
akm_evolve: tool({
|
|
1192
|
+
description: "Dispatch the AKM curator agent to review recent session activity and propose stash improvements (promote hot assets, flag cold ones, draft missing coverage).",
|
|
1193
|
+
args: {
|
|
1194
|
+
focus: tool.schema.string().optional().describe("Optional focus area or theme to weight the review toward."),
|
|
1195
|
+
dispatch_agent: tool.schema.string().optional().describe("OpenCode agent to run the curator with. Defaults to 'general'."),
|
|
1196
|
+
as_subtask: tool.schema.boolean().optional().describe("Run in a child session with parent context. Defaults to true."),
|
|
1197
|
+
},
|
|
1198
|
+
async execute({ focus, dispatch_agent, as_subtask }, context) {
|
|
1199
|
+
const useSubtask = as_subtask ?? true
|
|
1200
|
+
const targetAgent = dispatch_agent ?? "general"
|
|
1201
|
+
const targetSession = await ensureTargetSessionID({
|
|
1202
|
+
useSubtask,
|
|
1203
|
+
context: { sessionID: context.sessionID, directory: context.directory },
|
|
1204
|
+
title: "akm:curator",
|
|
1205
|
+
client: client as unknown as PluginClient,
|
|
1206
|
+
})
|
|
1207
|
+
if (!targetSession.ok) return JSON.stringify(targetSession)
|
|
1208
|
+
|
|
1209
|
+
const task = focus && focus.trim()
|
|
1210
|
+
? `Review recent AKM activity with an emphasis on: ${focus.trim()}. Produce the prioritized action list described in the system prompt.`
|
|
1211
|
+
: "Review recent AKM activity and produce the prioritized action list described in the system prompt."
|
|
1212
|
+
|
|
1213
|
+
const promptResponse = await client.session.prompt({
|
|
1214
|
+
query: { directory: context.directory },
|
|
1215
|
+
path: { id: targetSession.sessionID },
|
|
1216
|
+
body: {
|
|
1217
|
+
agent: targetAgent,
|
|
1218
|
+
system: CURATOR_AGENT_PROMPT,
|
|
1219
|
+
parts: [{ type: "text", text: task }],
|
|
1220
|
+
},
|
|
1221
|
+
})
|
|
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
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return JSON.stringify({
|
|
1232
|
+
ok: true,
|
|
1233
|
+
dispatchAgent: targetAgent,
|
|
1234
|
+
usedSubtask: useSubtask,
|
|
1235
|
+
sessionID: targetSession.sessionID,
|
|
1236
|
+
focus: focus ?? null,
|
|
1237
|
+
text: extractText(promptResponse.data.parts),
|
|
1238
|
+
})
|
|
1239
|
+
},
|
|
1240
|
+
}),
|
|
637
1241
|
akm_agent: tool({
|
|
638
1242
|
description: "Dispatch a stash agent by ref into a child OpenCode session, applying the agent prompt and metadata from akm_show.",
|
|
639
1243
|
args: {
|
|
@@ -871,10 +1475,10 @@ export const AgentikitPlugin: Plugin = async ({ client }) => {
|
|
|
871
1475
|
},
|
|
872
1476
|
}),
|
|
873
1477
|
akm_sources: tool({
|
|
874
|
-
description: "List all
|
|
1478
|
+
description: "List all configured AKM sources. Kept as a backward-compatible alias for the older sources command.",
|
|
875
1479
|
args: {},
|
|
876
1480
|
async execute() {
|
|
877
|
-
return runCli(client as unknown as LogCapableClient, ["
|
|
1481
|
+
return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_sources" })
|
|
878
1482
|
},
|
|
879
1483
|
}),
|
|
880
1484
|
akm_upgrade: tool({
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenCode plugin for
|
|
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.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"opencode",
|
|
8
8
|
"opencode-ai",
|
|
9
9
|
"opencode-plugin",
|
|
10
10
|
"opencode-extensions",
|
|
11
|
-
"
|
|
11
|
+
"akm",
|
|
12
12
|
"ai-agent",
|
|
13
13
|
"developer-tools",
|
|
14
14
|
"plugin"
|