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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/index.ts +343 -159
  3. 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 **19 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.
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
- try {
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
- const message = formatCliError(error)
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
- const text = extractText(output.parts).trim()
2180
- if (!text) return
2181
- await writePluginLog(logClient, "info", "AKM user feedback recorded", {
2182
- subsystem: "feedback",
2183
- actor: "user",
2184
- sessionID: input.sessionID,
2185
- messageID: input.messageID,
2186
- agent: input.agent,
2187
- text: truncateLogText(text),
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
- // Compound-engineering loop: on every user message, curate the stash and
2191
- // stash the result so experimental.chat.system.transform can inject it.
2192
- if (AKM_AUTO_CURATE && input.sessionID) {
2193
- const curated = runCurateForPrompt(text, input.sessionID)
2194
- if (curated) {
2195
- sessionCurated.set(input.sessionID, curated)
2196
- bumpCuratedVersion(input.sessionID)
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
- // Track explicit memory intents so capture-memory has something durable
2201
- // to flush when the session ends.
2202
- if (/\b(remember|memory|memories)\b/i.test(text)) {
2203
- addBufferEntry(input.sessionID, {
2204
- kind: "memory-intent",
2205
- note: truncateLogText(text, 500),
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
- if (input.sessionID && AKM_AUTO_FEEDBACK && AKM_RETROSPECTIVE_FEEDBACK_RE.test(text)) {
2210
- const recentRefs = (sessionBuffer.get(input.sessionID) ?? [])
2211
- .filter((entry) => entry.kind === "tool-ref" && !!entry.ref)
2212
- .map((entry) => entry.ref!)
2213
- .filter((ref, index, refs) => !ref.startsWith("memory:") && !ref.startsWith("vault:") && refs.indexOf(ref) === index)
2214
- .slice(-3)
2215
- const dedupe = new Set<string>()
2216
- for (const ref of recentRefs) {
2217
- queueFeedback(logClient, ref, "positive", "opencode retrospective: user confirmed it worked", {
2218
- toolName: "chat.message",
2219
- sessionID: input.sessionID,
2220
- }, dedupe)
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
- if (!input.tool.startsWith("akm_")) return
2368
+ try {
2369
+ if (!input.tool.startsWith("akm_")) return
2227
2370
 
2228
- const parsed = parseToolOutput(output.output)
2229
- if (!parsed) return
2371
+ const parsed = parseToolOutput(output.output)
2372
+ if (!parsed) return
2230
2373
 
2231
- const feedback = classifyToolFeedback(parsed)
2232
- if (feedback) {
2233
- await writePluginLog(logClient, feedback === "negative" ? "warn" : "info", "AKM system feedback recorded", {
2234
- subsystem: "feedback",
2235
- actor: "system",
2236
- feedback,
2237
- toolName: input.tool,
2238
- sessionID: input.sessionID,
2239
- callID: input.callID,
2240
- title: output.title,
2241
- error: typeof (parsed as { error?: unknown }).error === "string" ? (parsed as { error?: string }).error : undefined,
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
- const memoryRefs = extractMemoryRefs(input.tool, input.args as Record<string, unknown>, parsed)
2246
- if (memoryRefs.length > 0) {
2247
- await writePluginLog(logClient, "info", "AKM memory usage recorded", {
2248
- subsystem: "memory",
2249
- toolName: input.tool,
2250
- sessionID: input.sessionID,
2251
- callID: input.callID,
2252
- refs: memoryRefs,
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
- // Auto-feedback + session buffering: record every asset ref the tool
2257
- // touched so the stash ranking improves over time and so Stop/Compact
2258
- // has material to flush into a session summary memory.
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
- for (const ref of refResult.refs) {
2263
- addBufferEntry(input.sessionID, {
2264
- kind: "tool-ref",
2265
- toolName: input.tool,
2266
- ref,
2267
- status: feedback ?? "unknown",
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
- if (
2289
- AKM_AUTO_FEEDBACK
2290
- && feedback
2291
- && input.tool !== "akm_feedback"
2292
- && refResult.refs.length > 0
2293
- ) {
2294
- const dedupe = new Set<string>()
2295
- const feedbackRefs = feedback === "positive"
2296
- ? refResult.refs
2297
- : refResult.refs.filter((ref) => !refResult.positiveOnlyRefs.includes(ref))
2298
- const note = feedback === "positive"
2299
- ? `opencode auto: ${input.tool} succeeded`
2300
- : `opencode auto: ${input.tool} failed`
2301
- for (const ref of feedbackRefs) {
2302
- // Memories and vault refs are not first-class feedback targets —
2303
- // memories do not accept feedback, and vault values never surface in
2304
- // JSON so automatic usage signals would be misleading.
2305
- if (ref.startsWith("memory:") || ref.startsWith("vault:")) continue
2306
- const ok = queueFeedback(logClient, ref, feedback, note, {
2307
- toolName: input.tool,
2308
- sessionID: input.sessionID,
2309
- }, dedupe)
2310
- if (!ok) break
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
- const parent = await getParentSessionID(sdkClient, context.sessionID, context.directory)
2484
- if (!parent.ok) return JSON.stringify(parent)
2485
- const messages = await sdkClient.session.messages({
2486
- path: { id: parent.parentID },
2487
- query: { directory: context.directory },
2488
- })
2489
- if (messages.error || !messages.data) {
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
- const parent = await getParentSessionID(sdkClient, context.sessionID, context.directory)
2502
- const allowedSessionIDs = new Set<string>([context.sessionID])
2503
- if (parent.ok) allowedSessionIDs.add(parent.parentID)
2504
- if (context.agent !== "akm-curator" && !allowedSessionIDs.has(session_id)) {
2505
- return JSON.stringify({
2506
- ok: false,
2507
- error: "akm_session_messages only allows arbitrary session IDs for the akm-curator agent. Use akm_parent_messages for parent context.",
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.0",
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": [