akm-opencode 0.7.0 → 0.7.2
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 +2 -1
- package/index.ts +343 -159
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,10 +14,11 @@ Add to your OpenCode config (`opencode.json`):
|
|
|
14
14
|
|
|
15
15
|
## Tools
|
|
16
16
|
|
|
17
|
-
The plugin exposes **
|
|
17
|
+
The plugin exposes **20 high-value tools**. Long-tail verbs (`add`, `save`, `import`, `clone`, `update`, `remove`, `list`-sources, `registry-search`, `index`-reindex, `config`, `upgrade`, ad-hoc `run`, raw `agent`) are reachable via `akm_help` plus the raw `akm` CLI through the `bash` tool.
|
|
18
18
|
|
|
19
19
|
| Tool | Description |
|
|
20
20
|
|------|-------------|
|
|
21
|
+
| `akm_info` | Show `akm info` output together with the installed `akm-opencode` plugin version and install location |
|
|
21
22
|
| `akm_search` | Search the local stash, the registry, or both. Type filter accepts `skill`, `command`, `agent`, `knowledge`, `lesson`, `memory`, `script`, `workflow`, `vault`, `wiki`, `any`; proposed hits can be included explicitly |
|
|
22
23
|
| `akm_show` | Show a stash asset by its ref |
|
|
23
24
|
| `akm_agent` | Dispatch a stash `agent:*` into OpenCode using the stash prompt and metadata |
|
package/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ const AKM_RETROSPECTIVE_FEEDBACK_RE = createRetrospectiveFeedbackRegex()
|
|
|
25
25
|
const AKM_RETROSPECTIVE_NEGATIVE_RE = createRetrospectiveNegativeRegex()
|
|
26
26
|
const AKM_EXPLICIT_CORRECTION_RE = createExplicitCorrectionRegex()
|
|
27
27
|
const PLUGIN_VERSION = readPackageVersion()
|
|
28
|
+
const PLUGIN_INSTALL_LOCATION = moduleDir
|
|
28
29
|
|
|
29
30
|
// Per-session state that drives the compound-engineering loop.
|
|
30
31
|
// These maps are keyed by OpenCode sessionID.
|
|
@@ -52,6 +53,7 @@ const sessionSuccessfulAssetTouchCount = new Map<string, number>()
|
|
|
52
53
|
const pendingProposalSummaryCache = new Map<string, { count: number; expiresAt: number; unsupported?: boolean }>()
|
|
53
54
|
const retrospectiveState = new Map<string, { recentRefs: string[]; lastNegativeSignalAt?: number }>()
|
|
54
55
|
let cachedAkmStashDir: string | undefined
|
|
56
|
+
let agentSetupPromise: Promise<boolean> | null = null
|
|
55
57
|
|
|
56
58
|
// Asset-ref grammar matching the stash skill: [origin//]type:name.
|
|
57
59
|
// We validate normalized tokens individually instead of running a global regex
|
|
@@ -217,6 +219,16 @@ function formatCliError(error: unknown): string {
|
|
|
217
219
|
return error instanceof Error ? error.message : String(error)
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
function needsAgentSetup(message: string): boolean {
|
|
223
|
+
return /(agent commands are disabled|agent not configured|no [`'"]?agent[`'"]? block in config\.json)/i.test(message)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function addAgentSetupGuidance(message: string): string {
|
|
227
|
+
if (!needsAgentSetup(message)) return message
|
|
228
|
+
if (/\bakm_setup\b|\bakm setup\b/i.test(message)) return message
|
|
229
|
+
return `${message}. Run akm_setup to detect an installed agent CLI and persist agent.default. If none is installed yet, install one of: opencode, claude, codex, gemini, aider.`
|
|
230
|
+
}
|
|
231
|
+
|
|
220
232
|
function toLogString(value: unknown): string | undefined {
|
|
221
233
|
if (typeof value === "string") return value
|
|
222
234
|
if (value instanceof Buffer) return value.toString("utf8")
|
|
@@ -245,6 +257,79 @@ async function writePluginLog(client: LogCapableClient, level: LogLevel, message
|
|
|
245
257
|
}
|
|
246
258
|
}
|
|
247
259
|
|
|
260
|
+
async function runAgentSetup(client: LogCapableClient, meta: { directory?: string; sessionID?: string; trigger: string; force?: boolean }): Promise<boolean> {
|
|
261
|
+
const command = resolveAkmCommand()
|
|
262
|
+
if (typeof command !== "string") {
|
|
263
|
+
await writePluginLog(client, "warn", "AKM agent setup skipped", {
|
|
264
|
+
subsystem: "akm",
|
|
265
|
+
trigger: meta.trigger,
|
|
266
|
+
sessionID: meta.sessionID,
|
|
267
|
+
directory: meta.directory,
|
|
268
|
+
error: command.error,
|
|
269
|
+
})
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const args = ["setup"]
|
|
274
|
+
if (meta.force) args.push("--force")
|
|
275
|
+
args.push("--format", "json")
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const stdout = execFileSync(command, args, {
|
|
279
|
+
encoding: "utf8",
|
|
280
|
+
timeout: 30_000,
|
|
281
|
+
})
|
|
282
|
+
const parsed = safeJsonParse<{ ok?: boolean; error?: string }>(stdout)
|
|
283
|
+
if (parsed?.ok === false) {
|
|
284
|
+
await writePluginLog(client, "warn", "AKM agent setup reported an error", {
|
|
285
|
+
subsystem: "akm",
|
|
286
|
+
trigger: meta.trigger,
|
|
287
|
+
sessionID: meta.sessionID,
|
|
288
|
+
directory: meta.directory,
|
|
289
|
+
command,
|
|
290
|
+
args,
|
|
291
|
+
stdout,
|
|
292
|
+
error: parsed.error ?? "Unknown akm setup error",
|
|
293
|
+
})
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
await writePluginLog(client, "info", "AKM agent setup completed", {
|
|
297
|
+
subsystem: "akm",
|
|
298
|
+
trigger: meta.trigger,
|
|
299
|
+
sessionID: meta.sessionID,
|
|
300
|
+
directory: meta.directory,
|
|
301
|
+
command,
|
|
302
|
+
args,
|
|
303
|
+
stdout,
|
|
304
|
+
})
|
|
305
|
+
return true
|
|
306
|
+
} catch (error: unknown) {
|
|
307
|
+
const message = addAgentSetupGuidance(formatCliError(error))
|
|
308
|
+
await writePluginLog(client, "warn", "AKM agent setup failed", {
|
|
309
|
+
subsystem: "akm",
|
|
310
|
+
trigger: meta.trigger,
|
|
311
|
+
sessionID: meta.sessionID,
|
|
312
|
+
directory: meta.directory,
|
|
313
|
+
command,
|
|
314
|
+
args,
|
|
315
|
+
error: message,
|
|
316
|
+
stdout: toLogString((error as { stdout?: unknown }).stdout) ?? "",
|
|
317
|
+
stderr: toLogString((error as { stderr?: unknown }).stderr) ?? message,
|
|
318
|
+
})
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function ensureAgentSetup(client: LogCapableClient, meta: { directory?: string; sessionID?: string; trigger: string; force?: boolean }): Promise<boolean> {
|
|
324
|
+
if (agentSetupPromise) return agentSetupPromise
|
|
325
|
+
const promise = runAgentSetup(client, meta)
|
|
326
|
+
const trackedPromise = promise.finally(() => {
|
|
327
|
+
if (agentSetupPromise === trackedPromise) agentSetupPromise = null
|
|
328
|
+
})
|
|
329
|
+
agentSetupPromise = trackedPromise
|
|
330
|
+
return trackedPromise
|
|
331
|
+
}
|
|
332
|
+
|
|
248
333
|
function nowIso(): string {
|
|
249
334
|
return new Date().toISOString()
|
|
250
335
|
}
|
|
@@ -677,6 +762,21 @@ function queueFeedback(
|
|
|
677
762
|
stdio: "ignore",
|
|
678
763
|
},
|
|
679
764
|
)
|
|
765
|
+
child.on("exit", (code, signal) => {
|
|
766
|
+
if ((typeof code === "number" && code !== 0) || signal) {
|
|
767
|
+
void writePluginLog(client, "warn", "AKM auto-feedback failed", {
|
|
768
|
+
subsystem: "feedback",
|
|
769
|
+
toolName: meta.toolName,
|
|
770
|
+
sessionID: meta.sessionID,
|
|
771
|
+
directory: meta.directory,
|
|
772
|
+
ref,
|
|
773
|
+
sentiment,
|
|
774
|
+
error: signal
|
|
775
|
+
? `akm feedback exited via signal ${signal}`
|
|
776
|
+
: `akm feedback exited with code ${code}`,
|
|
777
|
+
})
|
|
778
|
+
}
|
|
779
|
+
})
|
|
680
780
|
child.on("error", (error) => {
|
|
681
781
|
void writePluginLog(client, "warn", "AKM auto-feedback failed", {
|
|
682
782
|
subsystem: "feedback",
|
|
@@ -1313,11 +1413,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1313
1413
|
const fullArgs = args.includes("--format") ? [...args] : [...args, "--format", "json"]
|
|
1314
1414
|
const proposalId = args[0] === "proposal" && typeof args[2] === "string" ? args[2] : null
|
|
1315
1415
|
|
|
1316
|
-
|
|
1317
|
-
const stdout = execFileSync(command, fullArgs, {
|
|
1318
|
-
encoding: "utf8",
|
|
1319
|
-
timeout: 60_000,
|
|
1320
|
-
})
|
|
1416
|
+
const recordSuccess = async (stdout: string): Promise<string> => {
|
|
1321
1417
|
await writePluginLog(client, "info", "AKM command completed", {
|
|
1322
1418
|
subsystem: "akm",
|
|
1323
1419
|
toolName: meta.toolName,
|
|
@@ -1383,8 +1479,45 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1383
1479
|
})
|
|
1384
1480
|
}
|
|
1385
1481
|
return stdout
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
try {
|
|
1485
|
+
const stdout = execFileSync(command, fullArgs, {
|
|
1486
|
+
encoding: "utf8",
|
|
1487
|
+
timeout: 60_000,
|
|
1488
|
+
})
|
|
1489
|
+
return recordSuccess(stdout)
|
|
1386
1490
|
} catch (error: unknown) {
|
|
1387
|
-
|
|
1491
|
+
let message = formatCliError(error)
|
|
1492
|
+
if (["akm_reflect", "akm_propose"].includes(meta.toolName) && needsAgentSetup(message)) {
|
|
1493
|
+
const setupOk = await ensureAgentSetup(client, {
|
|
1494
|
+
directory: meta.directory,
|
|
1495
|
+
sessionID: meta.sessionID,
|
|
1496
|
+
trigger: `${meta.toolName}:retry`,
|
|
1497
|
+
force: true,
|
|
1498
|
+
})
|
|
1499
|
+
if (setupOk) {
|
|
1500
|
+
try {
|
|
1501
|
+
const stdout = execFileSync(command, fullArgs, {
|
|
1502
|
+
encoding: "utf8",
|
|
1503
|
+
timeout: 60_000,
|
|
1504
|
+
})
|
|
1505
|
+
await writePluginLog(client, "info", "AKM command recovered after setup", {
|
|
1506
|
+
subsystem: "akm",
|
|
1507
|
+
toolName: meta.toolName,
|
|
1508
|
+
sessionID: meta.sessionID,
|
|
1509
|
+
directory: meta.directory,
|
|
1510
|
+
command,
|
|
1511
|
+
args: fullArgs,
|
|
1512
|
+
})
|
|
1513
|
+
return recordSuccess(stdout)
|
|
1514
|
+
} catch (retryError: unknown) {
|
|
1515
|
+
error = retryError
|
|
1516
|
+
message = formatCliError(retryError)
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
message = addAgentSetupGuidance(message)
|
|
1388
1521
|
await writePluginLog(client, "error", "AKM command failed", {
|
|
1389
1522
|
subsystem: "akm",
|
|
1390
1523
|
toolName: meta.toolName,
|
|
@@ -1536,6 +1669,17 @@ function parseCliJson<T>(raw: string): T | CliError {
|
|
|
1536
1669
|
}
|
|
1537
1670
|
}
|
|
1538
1671
|
|
|
1672
|
+
function formatAkmInfoResponse(raw: string): string {
|
|
1673
|
+
const parsed = parseCliJson<Record<string, unknown>>(raw)
|
|
1674
|
+
if (isCliError(parsed)) return JSON.stringify(parsed)
|
|
1675
|
+
return JSON.stringify({
|
|
1676
|
+
ok: true,
|
|
1677
|
+
pluginVersion: PLUGIN_VERSION,
|
|
1678
|
+
pluginInstallLocation: PLUGIN_INSTALL_LOCATION,
|
|
1679
|
+
akmInfo: parsed,
|
|
1680
|
+
})
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1539
1683
|
function blockedToolResponse(args: Record<string, unknown>): string | null {
|
|
1540
1684
|
return typeof args.__akmBlocked === "string"
|
|
1541
1685
|
? JSON.stringify({ ok: false, error: args.__akmBlocked })
|
|
@@ -1968,6 +2112,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1968
2112
|
if (!sid) return
|
|
1969
2113
|
if (!sessionContextEpoch.has(sid)) sessionContextEpoch.set(sid, 0)
|
|
1970
2114
|
if (type === "session.created") {
|
|
2115
|
+
await ensureAgentSetup(logClient, {
|
|
2116
|
+
directory,
|
|
2117
|
+
sessionID: sid,
|
|
2118
|
+
trigger: "session.created",
|
|
2119
|
+
})
|
|
1971
2120
|
warmIndexInBackground()
|
|
1972
2121
|
if (AKM_AUTO_CURATE && !sessionCurated.has(sid)) {
|
|
1973
2122
|
const curated = runCurateForSession(sid)
|
|
@@ -2043,24 +2192,6 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2043
2192
|
// Best-effort only.
|
|
2044
2193
|
}
|
|
2045
2194
|
},
|
|
2046
|
-
"experimental.session.compacting": async (input, output) => {
|
|
2047
|
-
try {
|
|
2048
|
-
const sid = input.sessionID
|
|
2049
|
-
if (!sid) return
|
|
2050
|
-
if (!Array.isArray(output.context)) return
|
|
2051
|
-
markContextEpochDirty(sid)
|
|
2052
|
-
const blocks = [
|
|
2053
|
-
sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
|
|
2054
|
-
sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
|
|
2055
|
-
sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
|
|
2056
|
-
(await getPendingProposalCount(logClient, sid)).count > 0 && !(await getPendingProposalCount(logClient, sid)).unsupported ? formatPendingProposalContext((await getPendingProposalCount(logClient, sid)).count) : "",
|
|
2057
|
-
sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
|
|
2058
|
-
]
|
|
2059
|
-
output.context.push(...applyContextBudget(blocks))
|
|
2060
|
-
} catch {
|
|
2061
|
-
// Never break compaction because of plugin context.
|
|
2062
|
-
}
|
|
2063
|
-
},
|
|
2064
2195
|
// experimental.chat.system.transform is how OpenCode exposes the
|
|
2065
2196
|
// additionalContext channel. We append the cached hints (once per session)
|
|
2066
2197
|
// and the curated assets (once per turn) so the next LLM call sees them.
|
|
@@ -2176,142 +2307,172 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2176
2307
|
}
|
|
2177
2308
|
},
|
|
2178
2309
|
"chat.message": async (input, output) => {
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2310
|
+
try {
|
|
2311
|
+
const text = extractText(output.parts).trim()
|
|
2312
|
+
if (!text) return
|
|
2313
|
+
await writePluginLog(logClient, "info", "AKM user feedback recorded", {
|
|
2314
|
+
subsystem: "feedback",
|
|
2315
|
+
actor: "user",
|
|
2316
|
+
sessionID: input.sessionID,
|
|
2317
|
+
messageID: input.messageID,
|
|
2318
|
+
agent: input.agent,
|
|
2319
|
+
text: truncateLogText(text),
|
|
2320
|
+
})
|
|
2189
2321
|
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2322
|
+
// Compound-engineering loop: on every user message, curate the stash and
|
|
2323
|
+
// stash the result so experimental.chat.system.transform can inject it.
|
|
2324
|
+
if (AKM_AUTO_CURATE && input.sessionID) {
|
|
2325
|
+
const curated = runCurateForPrompt(text, input.sessionID)
|
|
2326
|
+
if (curated) {
|
|
2327
|
+
sessionCurated.set(input.sessionID, curated)
|
|
2328
|
+
bumpCuratedVersion(input.sessionID)
|
|
2329
|
+
}
|
|
2197
2330
|
}
|
|
2198
|
-
}
|
|
2199
2331
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2332
|
+
// Track explicit memory intents so capture-memory has something durable
|
|
2333
|
+
// to flush when the session ends.
|
|
2334
|
+
if (/\b(remember|memory|memories)\b/i.test(text)) {
|
|
2335
|
+
addBufferEntry(input.sessionID, {
|
|
2336
|
+
kind: "memory-intent",
|
|
2337
|
+
note: truncateLogText(text, 500),
|
|
2338
|
+
})
|
|
2339
|
+
}
|
|
2208
2340
|
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2341
|
+
if (input.sessionID && AKM_AUTO_FEEDBACK && AKM_RETROSPECTIVE_FEEDBACK_RE.test(text)) {
|
|
2342
|
+
const recentRefs = (sessionBuffer.get(input.sessionID) ?? [])
|
|
2343
|
+
.filter((entry) => entry.kind === "tool-ref" && !!entry.ref)
|
|
2344
|
+
.map((entry) => entry.ref!)
|
|
2345
|
+
.filter((ref, index, refs) => !ref.startsWith("memory:") && !ref.startsWith("vault:") && refs.indexOf(ref) === index)
|
|
2346
|
+
.slice(-3)
|
|
2347
|
+
const dedupe = new Set<string>()
|
|
2348
|
+
for (const ref of recentRefs) {
|
|
2349
|
+
queueFeedback(logClient, ref, "positive", "opencode retrospective: user confirmed it worked", {
|
|
2350
|
+
toolName: "chat.message",
|
|
2351
|
+
sessionID: input.sessionID,
|
|
2352
|
+
}, dedupe)
|
|
2353
|
+
}
|
|
2221
2354
|
}
|
|
2355
|
+
await recordRetrospectiveFeedback(logClient, input.sessionID, text)
|
|
2356
|
+
} catch (error: unknown) {
|
|
2357
|
+
await writePluginLog(logClient, "error", "AKM chat.message hook failed", {
|
|
2358
|
+
subsystem: "hook",
|
|
2359
|
+
hook: "chat.message",
|
|
2360
|
+
sessionID: input?.sessionID,
|
|
2361
|
+
messageID: input?.messageID,
|
|
2362
|
+
agent: input?.agent,
|
|
2363
|
+
error: formatCliError(error),
|
|
2364
|
+
})
|
|
2222
2365
|
}
|
|
2223
|
-
await recordRetrospectiveFeedback(logClient, input.sessionID, text)
|
|
2224
2366
|
},
|
|
2225
2367
|
"tool.execute.after": async (input, output) => {
|
|
2226
|
-
|
|
2368
|
+
try {
|
|
2369
|
+
if (!input.tool.startsWith("akm_")) return
|
|
2227
2370
|
|
|
2228
|
-
|
|
2229
|
-
|
|
2371
|
+
const parsed = parseToolOutput(output.output)
|
|
2372
|
+
if (!parsed) return
|
|
2230
2373
|
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2374
|
+
const feedback = classifyToolFeedback(parsed)
|
|
2375
|
+
if (feedback) {
|
|
2376
|
+
await writePluginLog(logClient, feedback === "negative" ? "warn" : "info", "AKM system feedback recorded", {
|
|
2377
|
+
subsystem: "feedback",
|
|
2378
|
+
actor: "system",
|
|
2379
|
+
feedback,
|
|
2380
|
+
toolName: input.tool,
|
|
2381
|
+
sessionID: input.sessionID,
|
|
2382
|
+
callID: input.callID,
|
|
2383
|
+
title: output.title,
|
|
2384
|
+
error: typeof (parsed as { error?: unknown }).error === "string" ? (parsed as { error?: string }).error : undefined,
|
|
2385
|
+
})
|
|
2386
|
+
}
|
|
2244
2387
|
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2388
|
+
const memoryRefs = extractMemoryRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
2389
|
+
if (memoryRefs.length > 0) {
|
|
2390
|
+
await writePluginLog(logClient, "info", "AKM memory usage recorded", {
|
|
2391
|
+
subsystem: "memory",
|
|
2392
|
+
toolName: input.tool,
|
|
2393
|
+
sessionID: input.sessionID,
|
|
2394
|
+
callID: input.callID,
|
|
2395
|
+
refs: memoryRefs,
|
|
2396
|
+
})
|
|
2397
|
+
}
|
|
2255
2398
|
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2399
|
+
// Auto-feedback + session buffering: record every asset ref the tool
|
|
2400
|
+
// touched so the stash ranking improves over time and so Stop/Compact
|
|
2401
|
+
// has material to flush into a session summary memory.
|
|
2259
2402
|
const refResult = extractToolRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
2260
2403
|
noteRecentRefs(input.sessionID, refResult.refs)
|
|
2261
2404
|
if (refResult.refs.length > 0 && input.sessionID) {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
})
|
|
2269
|
-
}
|
|
2270
|
-
if (feedback === "positive") {
|
|
2271
|
-
sessionSuccessfulAssetTouchCount.set(
|
|
2272
|
-
input.sessionID,
|
|
2273
|
-
(sessionSuccessfulAssetTouchCount.get(input.sessionID) ?? 0) + 1,
|
|
2274
|
-
)
|
|
2275
|
-
const checkpointRef = maybeCheckpointSessionMemory(input.sessionID)
|
|
2276
|
-
if (checkpointRef) {
|
|
2277
|
-
await writePluginLog(logClient, "info", "AKM checkpoint memory captured", {
|
|
2278
|
-
subsystem: "memory",
|
|
2279
|
-
actor: "system",
|
|
2280
|
-
sessionID: input.sessionID,
|
|
2281
|
-
reason: "checkpoint",
|
|
2282
|
-
ref: checkpointRef,
|
|
2405
|
+
for (const ref of refResult.refs) {
|
|
2406
|
+
addBufferEntry(input.sessionID, {
|
|
2407
|
+
kind: "tool-ref",
|
|
2408
|
+
toolName: input.tool,
|
|
2409
|
+
ref,
|
|
2410
|
+
status: feedback ?? "unknown",
|
|
2283
2411
|
})
|
|
2284
2412
|
}
|
|
2413
|
+
if (feedback === "positive") {
|
|
2414
|
+
sessionSuccessfulAssetTouchCount.set(
|
|
2415
|
+
input.sessionID,
|
|
2416
|
+
(sessionSuccessfulAssetTouchCount.get(input.sessionID) ?? 0) + 1,
|
|
2417
|
+
)
|
|
2418
|
+
const checkpointRef = maybeCheckpointSessionMemory(input.sessionID)
|
|
2419
|
+
if (checkpointRef) {
|
|
2420
|
+
await writePluginLog(logClient, "info", "AKM checkpoint memory captured", {
|
|
2421
|
+
subsystem: "memory",
|
|
2422
|
+
actor: "system",
|
|
2423
|
+
sessionID: input.sessionID,
|
|
2424
|
+
reason: "checkpoint",
|
|
2425
|
+
ref: checkpointRef,
|
|
2426
|
+
})
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2285
2429
|
}
|
|
2286
|
-
}
|
|
2287
2430
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2431
|
+
if (
|
|
2432
|
+
AKM_AUTO_FEEDBACK
|
|
2433
|
+
&& feedback
|
|
2434
|
+
&& input.tool !== "akm_feedback"
|
|
2435
|
+
&& refResult.refs.length > 0
|
|
2436
|
+
) {
|
|
2437
|
+
const dedupe = new Set<string>()
|
|
2438
|
+
const feedbackRefs = feedback === "positive"
|
|
2439
|
+
? refResult.refs
|
|
2440
|
+
: refResult.refs.filter((ref) => !refResult.positiveOnlyRefs.includes(ref))
|
|
2441
|
+
const note = feedback === "positive"
|
|
2442
|
+
? `opencode auto: ${input.tool} succeeded`
|
|
2443
|
+
: `opencode auto: ${input.tool} failed`
|
|
2444
|
+
for (const ref of feedbackRefs) {
|
|
2445
|
+
// Memories and vault refs are not first-class feedback targets —
|
|
2446
|
+
// memories do not accept feedback, and vault values never surface in
|
|
2447
|
+
// JSON so automatic usage signals would be misleading.
|
|
2448
|
+
if (ref.startsWith("memory:") || ref.startsWith("vault:")) continue
|
|
2449
|
+
const ok = queueFeedback(logClient, ref, feedback, note, {
|
|
2450
|
+
toolName: input.tool,
|
|
2451
|
+
sessionID: input.sessionID,
|
|
2452
|
+
}, dedupe)
|
|
2453
|
+
if (!ok) break
|
|
2454
|
+
}
|
|
2311
2455
|
}
|
|
2456
|
+
} catch (error: unknown) {
|
|
2457
|
+
await writePluginLog(logClient, "error", "AKM tool.execute.after hook failed", {
|
|
2458
|
+
subsystem: "hook",
|
|
2459
|
+
hook: "tool.execute.after",
|
|
2460
|
+
toolName: input?.tool,
|
|
2461
|
+
sessionID: input?.sessionID,
|
|
2462
|
+
callID: input?.callID,
|
|
2463
|
+
error: formatCliError(error),
|
|
2464
|
+
})
|
|
2312
2465
|
}
|
|
2313
2466
|
},
|
|
2314
2467
|
tool: {
|
|
2468
|
+
akm_info: tool({
|
|
2469
|
+
description: "Show `akm info` output plus the installed akm-opencode plugin version and install location.",
|
|
2470
|
+
args: {},
|
|
2471
|
+
async execute() {
|
|
2472
|
+
const raw = await runCli(client as unknown as LogCapableClient, ["info"], { toolName: "akm_info" })
|
|
2473
|
+
return formatAkmInfoResponse(raw)
|
|
2474
|
+
},
|
|
2475
|
+
}),
|
|
2315
2476
|
akm_search: tool({
|
|
2316
2477
|
description: "Search your stash or the akm registry for scripts, skills, commands, agents, knowledge, memories, workflows, vaults, and wikis. Use source='registry' for installable community kits.",
|
|
2317
2478
|
args: {
|
|
@@ -2480,16 +2641,27 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2480
2641
|
description: "Read compact text summaries of the parent session's messages so a dispatched AKM subagent can inherit upstream context.",
|
|
2481
2642
|
args: {},
|
|
2482
2643
|
async execute(_input, context) {
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2644
|
+
try {
|
|
2645
|
+
const parent = await getParentSessionID(sdkClient, context.sessionID, context.directory)
|
|
2646
|
+
if (!parent.ok) return JSON.stringify(parent)
|
|
2647
|
+
const messages = await sdkClient.session.messages({
|
|
2648
|
+
path: { id: parent.parentID },
|
|
2649
|
+
query: { directory: context.directory },
|
|
2650
|
+
})
|
|
2651
|
+
if (messages.error || !messages.data) {
|
|
2652
|
+
return JSON.stringify({ ok: false, error: "Failed to read parent session messages." })
|
|
2653
|
+
}
|
|
2654
|
+
return JSON.stringify(summarizeSessionMessages(parent.parentID, messages.data))
|
|
2655
|
+
} catch (error: unknown) {
|
|
2656
|
+
await writePluginLog(logClient, "error", "AKM parent session read failed", {
|
|
2657
|
+
subsystem: "session",
|
|
2658
|
+
toolName: "akm_parent_messages",
|
|
2659
|
+
sessionID: context.sessionID,
|
|
2660
|
+
directory: context.directory,
|
|
2661
|
+
error: formatCliError(error),
|
|
2662
|
+
})
|
|
2490
2663
|
return JSON.stringify({ ok: false, error: "Failed to read parent session messages." })
|
|
2491
2664
|
}
|
|
2492
|
-
return JSON.stringify(summarizeSessionMessages(parent.parentID, messages.data))
|
|
2493
2665
|
},
|
|
2494
2666
|
}),
|
|
2495
2667
|
akm_session_messages: tool({
|
|
@@ -2498,23 +2670,35 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2498
2670
|
session_id: tool.schema.string().describe("OpenCode session ID to inspect."),
|
|
2499
2671
|
},
|
|
2500
2672
|
async execute({ session_id }, context) {
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2673
|
+
try {
|
|
2674
|
+
const parent = await getParentSessionID(sdkClient, context.sessionID, context.directory)
|
|
2675
|
+
const allowedSessionIDs = new Set<string>([context.sessionID])
|
|
2676
|
+
if (parent.ok) allowedSessionIDs.add(parent.parentID)
|
|
2677
|
+
if (context.agent !== "akm-curator" && !allowedSessionIDs.has(session_id)) {
|
|
2678
|
+
return JSON.stringify({
|
|
2679
|
+
ok: false,
|
|
2680
|
+
error: "akm_session_messages only allows arbitrary session IDs for the akm-curator agent. Use akm_parent_messages for parent context.",
|
|
2681
|
+
})
|
|
2682
|
+
}
|
|
2683
|
+
const messages = await sdkClient.session.messages({
|
|
2684
|
+
path: { id: session_id },
|
|
2685
|
+
query: { directory: context.directory },
|
|
2686
|
+
})
|
|
2687
|
+
if (messages.error || !messages.data) {
|
|
2688
|
+
return JSON.stringify({ ok: false, error: `Failed to read messages for session '${session_id}'.` })
|
|
2689
|
+
}
|
|
2690
|
+
return JSON.stringify(summarizeSessionMessages(session_id, messages.data))
|
|
2691
|
+
} catch (error: unknown) {
|
|
2692
|
+
await writePluginLog(logClient, "error", "AKM session messages read failed", {
|
|
2693
|
+
subsystem: "session",
|
|
2694
|
+
toolName: "akm_session_messages",
|
|
2695
|
+
sessionID: context.sessionID,
|
|
2696
|
+
directory: context.directory,
|
|
2697
|
+
targetSessionID: session_id,
|
|
2698
|
+
error: formatCliError(error),
|
|
2508
2699
|
})
|
|
2509
|
-
}
|
|
2510
|
-
const messages = await sdkClient.session.messages({
|
|
2511
|
-
path: { id: session_id },
|
|
2512
|
-
query: { directory: context.directory },
|
|
2513
|
-
})
|
|
2514
|
-
if (messages.error || !messages.data) {
|
|
2515
2700
|
return JSON.stringify({ ok: false, error: `Failed to read messages for session '${session_id}'.` })
|
|
2516
2701
|
}
|
|
2517
|
-
return JSON.stringify(summarizeSessionMessages(session_id, messages.data))
|
|
2518
2702
|
},
|
|
2519
2703
|
}),
|
|
2520
2704
|
akm_agent: tool({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for AKM v0.7.0 - search, show, and manage extension assets via the akm CLI, including vaults, wikis, workflows, the proposal queue, lesson assets, and reflect/propose/distill commands, with agentic hooks that auto-load relevant stash assets, record feedback, and harvest session memories so the stash improves every session.",
|
|
6
6
|
"keywords": [
|