akm-opencode 0.7.2 → 0.7.3
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 +4 -4
- package/index.ts +244 -141
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ The plugin exposes **20 high-value tools**. Long-tail verbs (`add`, `save`, `imp
|
|
|
36
36
|
| `akm_reflect` | Generate a reflection proposal via the configured agent CLI; output lands in the proposal queue only |
|
|
37
37
|
| `akm_propose` | Generate a new-asset proposal via the configured agent CLI; the result is `quality:"proposed"` until accepted |
|
|
38
38
|
| `akm_distill` | Distill an AKM ref (typically `memory:*` or `knowledge:*`) into a proposed `lesson` (gated by `llm.features.feedback_distillation`) |
|
|
39
|
-
| `
|
|
39
|
+
| `akm_init` | Initialize AKM's working stash directory and persist `stashDir` in config. This is the agent-safe initialization path; interactive `akm setup` is human-facing |
|
|
40
40
|
| `akm_help` | Discover the right `akm` CLI invocation for non-first-class verbs. Returns a curated quick-reference table plus live `akm <subcommand> --help` output |
|
|
41
41
|
|
|
42
42
|
## Compound-engineering hooks
|
|
@@ -47,8 +47,8 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
|
|
|
47
47
|
|
|
48
48
|
| Event | What happens |
|
|
49
49
|
| --- | --- |
|
|
50
|
-
| **`session.created`** (event hook) |
|
|
51
|
-
| **`chat.message`** |
|
|
50
|
+
| **`session.created`** (event hook) | Sets `agent.default` to `opencode` when missing, warms the stash index in the background, caches `akm hints` plus active workflow status, and runs a scoped `akm curate --run <sessionID>` so fresh sessions start with relevant stash context. |
|
|
51
|
+
| **`chat.message`** | Records user feedback/memory intent and appends a short reminder to use `akm_search` / `akm_curate` when more stash context is needed. It does not auto-run AKM CLI lookups on every message. |
|
|
52
52
|
| **`experimental.chat.system.transform`** | Appends cached hints, active workflow state, pending proposal summaries, the last curator report, and the current prompt's curated context to the model's system prompt. Hints and workflow state are re-injected after transcript compaction. |
|
|
53
53
|
| **`tool.execute.before`** (`akm_*` tools) | Blocks destructive or sensitive operations until `confirm:true` is provided. |
|
|
54
54
|
| **`permission.ask`** / **`command.execute.before`** | Detects risky raw `akm` CLI commands executed through shell/commands and denies them until the user explicitly approves the exact operation. |
|
|
@@ -61,7 +61,7 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
|
|
|
61
61
|
|
|
62
62
|
| Variable | Default | Purpose |
|
|
63
63
|
| --- | --- | --- |
|
|
64
|
-
| `AKM_AUTO_CURATE` | `1` | Set to `0` to disable automatic `akm curate` on user messages. |
|
|
64
|
+
| `AKM_AUTO_CURATE` | `1` | Set to `0` to disable automatic `akm curate` on user messages. Session start no longer auto-curates. |
|
|
65
65
|
| `AKM_AUTO_FEEDBACK` | `1` | Set to `0` to disable automatic `akm feedback` on tool success/failure. |
|
|
66
66
|
| `AKM_AUTO_HINTS` | `1` | Set to `0` to skip injecting `akm hints` at session start. |
|
|
67
67
|
| `AKM_AUTO_MEMORY` | `1` | Set to `0` to disable automatic session-summary memories. |
|
package/index.ts
CHANGED
|
@@ -71,7 +71,7 @@ const AKM_WORKFLOW_INSTRUCTION = [
|
|
|
71
71
|
"3. Record `akm_feedback` after the result is known.",
|
|
72
72
|
"4. Use the dedicated v0.7.0 tools for the proposal flow: `akm_proposal` (list/show/diff/accept/reject), `akm_reflect`, `akm_propose`, and `akm_distill`. Fall back to `akm_help` for any verb without a dedicated tool.",
|
|
73
73
|
"5. Treat `lesson:*` as first-class durable learning assets — they are produced by `akm_distill <ref>` as proposals and accepted via `akm_proposal action=accept`.",
|
|
74
|
-
"6.
|
|
74
|
+
"6. Use `akm_init` when you need to create the working stash or persist `stashDir`. Do not use `akm setup` from an agent; it is interactive and human-facing.",
|
|
75
75
|
`7. ${PROPOSED_QUALITY_WARNING}`,
|
|
76
76
|
"8. Never accept or reject proposals, push saves, remove sources, or access vault values without explicit user approval.",
|
|
77
77
|
].join("\n")
|
|
@@ -225,42 +225,17 @@ function needsAgentSetup(message: string): boolean {
|
|
|
225
225
|
|
|
226
226
|
function addAgentSetupGuidance(message: string): string {
|
|
227
227
|
if (!needsAgentSetup(message)) return message
|
|
228
|
-
if (/\
|
|
229
|
-
return `${message}.
|
|
228
|
+
if (/\bakm init\b|\bakm_init\b|\bakm setup\b/i.test(message)) return message
|
|
229
|
+
return `${message}. Agents should not run akm setup because it is interactive. The plugin should set agent.default to the current platform when it is missing; if interactive configuration is still needed, ask the user to run akm setup manually. Use akm_init for agent-safe stash initialization.`
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function getExecStatus(error: unknown): number | null {
|
|
239
|
-
if (!error || typeof error !== "object" || !("status" in error)) return null
|
|
240
|
-
const status = (error as { status?: unknown }).status
|
|
241
|
-
return typeof status === "number" ? status : null
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function writePluginLog(client: LogCapableClient, level: LogLevel, message: string, extra: Record<string, unknown>) {
|
|
245
|
-
try {
|
|
246
|
-
await client.app.log({
|
|
247
|
-
query: typeof extra.directory === "string" ? { directory: extra.directory } : undefined,
|
|
248
|
-
body: {
|
|
249
|
-
service: "akm-opencode",
|
|
250
|
-
level,
|
|
251
|
-
message,
|
|
252
|
-
extra,
|
|
253
|
-
},
|
|
254
|
-
})
|
|
255
|
-
} catch {
|
|
256
|
-
// Avoid breaking the TUI if logging itself fails.
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function runAgentSetup(client: LogCapableClient, meta: { directory?: string; sessionID?: string; trigger: string; force?: boolean }): Promise<boolean> {
|
|
232
|
+
async function ensurePlatformAgentDefault(
|
|
233
|
+
client: LogCapableClient,
|
|
234
|
+
meta: { directory?: string; sessionID?: string; trigger: string; platform: string },
|
|
235
|
+
): Promise<boolean> {
|
|
261
236
|
const command = resolveAkmCommand()
|
|
262
237
|
if (typeof command !== "string") {
|
|
263
|
-
await writePluginLog(client, "warn", "AKM agent
|
|
238
|
+
await writePluginLog(client, "warn", "AKM agent default check skipped", {
|
|
264
239
|
subsystem: "akm",
|
|
265
240
|
trigger: meta.trigger,
|
|
266
241
|
sessionID: meta.sessionID,
|
|
@@ -270,59 +245,56 @@ async function runAgentSetup(client: LogCapableClient, meta: { directory?: strin
|
|
|
270
245
|
return false
|
|
271
246
|
}
|
|
272
247
|
|
|
273
|
-
const args = ["setup"]
|
|
274
|
-
if (meta.force) args.push("--force")
|
|
275
|
-
args.push("--format", "json")
|
|
276
|
-
|
|
277
248
|
try {
|
|
278
|
-
const
|
|
249
|
+
const currentRaw = execFileSync(command, ["config", "get", "agent.default", "--format", "json"], {
|
|
279
250
|
encoding: "utf8",
|
|
280
251
|
timeout: 30_000,
|
|
281
252
|
})
|
|
282
|
-
const parsed = safeJsonParse<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
253
|
+
const parsed = safeJsonParse<unknown>(currentRaw)
|
|
254
|
+
const current = typeof parsed === "string"
|
|
255
|
+
? parsed.trim()
|
|
256
|
+
: parsed && typeof parsed === "object"
|
|
257
|
+
? typeof (parsed as Record<string, unknown>).value === "string"
|
|
258
|
+
? ((parsed as Record<string, unknown>).value as string).trim()
|
|
259
|
+
: typeof (parsed as Record<string, unknown>)["agent.default"] === "string"
|
|
260
|
+
? ((parsed as Record<string, unknown>)["agent.default"] as string).trim()
|
|
261
|
+
: ""
|
|
262
|
+
: ""
|
|
263
|
+
if (current) return true
|
|
264
|
+
|
|
265
|
+
execFileSync(command, ["config", "set", "agent.default", meta.platform, "--format", "json"], {
|
|
266
|
+
encoding: "utf8",
|
|
267
|
+
timeout: 30_000,
|
|
268
|
+
})
|
|
269
|
+
await writePluginLog(client, "info", "AKM agent default initialized", {
|
|
297
270
|
subsystem: "akm",
|
|
298
271
|
trigger: meta.trigger,
|
|
299
272
|
sessionID: meta.sessionID,
|
|
300
273
|
directory: meta.directory,
|
|
301
|
-
|
|
302
|
-
args,
|
|
303
|
-
stdout,
|
|
274
|
+
agentDefault: meta.platform,
|
|
304
275
|
})
|
|
305
276
|
return true
|
|
306
277
|
} catch (error: unknown) {
|
|
307
|
-
|
|
308
|
-
await writePluginLog(client, "warn", "AKM agent setup failed", {
|
|
278
|
+
await writePluginLog(client, "warn", "AKM agent default initialization failed", {
|
|
309
279
|
subsystem: "akm",
|
|
310
280
|
trigger: meta.trigger,
|
|
311
281
|
sessionID: meta.sessionID,
|
|
312
282
|
directory: meta.directory,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
error: message,
|
|
283
|
+
platform: meta.platform,
|
|
284
|
+
error: addAgentSetupGuidance(formatCliError(error)),
|
|
316
285
|
stdout: toLogString((error as { stdout?: unknown }).stdout) ?? "",
|
|
317
|
-
stderr: toLogString((error as { stderr?: unknown }).stderr) ??
|
|
286
|
+
stderr: toLogString((error as { stderr?: unknown }).stderr) ?? "",
|
|
318
287
|
})
|
|
319
288
|
return false
|
|
320
289
|
}
|
|
321
290
|
}
|
|
322
291
|
|
|
323
|
-
async function ensureAgentSetup(
|
|
292
|
+
async function ensureAgentSetup(
|
|
293
|
+
client: LogCapableClient,
|
|
294
|
+
meta: { directory?: string; sessionID?: string; trigger: string; platform: string },
|
|
295
|
+
): Promise<boolean> {
|
|
324
296
|
if (agentSetupPromise) return agentSetupPromise
|
|
325
|
-
const promise =
|
|
297
|
+
const promise = ensurePlatformAgentDefault(client, meta)
|
|
326
298
|
const trackedPromise = promise.finally(() => {
|
|
327
299
|
if (agentSetupPromise === trackedPromise) agentSetupPromise = null
|
|
328
300
|
})
|
|
@@ -330,6 +302,52 @@ async function ensureAgentSetup(client: LogCapableClient, meta: { directory?: st
|
|
|
330
302
|
return trackedPromise
|
|
331
303
|
}
|
|
332
304
|
|
|
305
|
+
function isNotIndexedFeedbackError(message: string): boolean {
|
|
306
|
+
return /not in the current index|run ["'`]?akm index["'`]? and try again/i.test(message)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function toLogString(value: unknown): string | undefined {
|
|
310
|
+
if (typeof value === "string") return value
|
|
311
|
+
if (value instanceof Buffer) return value.toString("utf8")
|
|
312
|
+
return undefined
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function getExecStatus(error: unknown): number | null {
|
|
316
|
+
if (!error || typeof error !== "object" || !("status" in error)) return null
|
|
317
|
+
const status = (error as { status?: unknown }).status
|
|
318
|
+
return typeof status === "number" ? status : null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function writePluginLog(client: LogCapableClient, level: LogLevel, message: string, extra: Record<string, unknown>) {
|
|
322
|
+
try {
|
|
323
|
+
await client.app.log({
|
|
324
|
+
query: typeof extra.directory === "string" ? { directory: extra.directory } : undefined,
|
|
325
|
+
body: {
|
|
326
|
+
service: "akm-opencode",
|
|
327
|
+
level,
|
|
328
|
+
message,
|
|
329
|
+
extra,
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
} catch {
|
|
333
|
+
// Avoid breaking the TUI if logging itself fails.
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function logHookFailure(
|
|
338
|
+
client: LogCapableClient,
|
|
339
|
+
hook: string,
|
|
340
|
+
error: unknown,
|
|
341
|
+
extra?: Record<string, unknown>,
|
|
342
|
+
) {
|
|
343
|
+
await writePluginLog(client, "error", `AKM ${hook} hook failed`, {
|
|
344
|
+
subsystem: "hook",
|
|
345
|
+
hook,
|
|
346
|
+
error: formatCliError(error),
|
|
347
|
+
...extra,
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
333
351
|
function nowIso(): string {
|
|
334
352
|
return new Date().toISOString()
|
|
335
353
|
}
|
|
@@ -431,9 +449,20 @@ function runCurate(args: string[]): string | null {
|
|
|
431
449
|
return body || null
|
|
432
450
|
}
|
|
433
451
|
|
|
434
|
-
function
|
|
452
|
+
async function runCurateLogged(
|
|
453
|
+
client: LogCapableClient,
|
|
454
|
+
args: string[],
|
|
455
|
+
meta: CliLogMeta & { operation: string },
|
|
456
|
+
): Promise<string | null> {
|
|
457
|
+
return runCliSyncBestEffort(client, args, AKM_CURATE_TIMEOUT_MS, {
|
|
458
|
+
...meta,
|
|
459
|
+
subsystem: "curation",
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function runCurateForPrompt(client: LogCapableClient, text: string, sessionID?: string): Promise<string | null> {
|
|
435
464
|
if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
|
|
436
|
-
return
|
|
465
|
+
return runCurateLogged(client,
|
|
437
466
|
appendRunScopeArgs(
|
|
438
467
|
[
|
|
439
468
|
"--detail",
|
|
@@ -448,11 +477,12 @@ function runCurateForPrompt(text: string, sessionID?: string): string | null {
|
|
|
448
477
|
],
|
|
449
478
|
sessionID,
|
|
450
479
|
),
|
|
480
|
+
{ toolName: "chat.message", sessionID, operation: "prompt-curate" },
|
|
451
481
|
)
|
|
452
482
|
}
|
|
453
483
|
|
|
454
|
-
function runCurateForSession(sessionID: string): string | null {
|
|
455
|
-
return
|
|
484
|
+
async function runCurateForSession(client: LogCapableClient, sessionID: string): Promise<string | null> {
|
|
485
|
+
return runCurateLogged(client,
|
|
456
486
|
appendRunScopeArgs(
|
|
457
487
|
[
|
|
458
488
|
"--detail",
|
|
@@ -466,14 +496,17 @@ function runCurateForSession(sessionID: string): string | null {
|
|
|
466
496
|
],
|
|
467
497
|
sessionID,
|
|
468
498
|
),
|
|
499
|
+
{ toolName: "session.start", sessionID, operation: "session-curate" },
|
|
469
500
|
)
|
|
470
501
|
}
|
|
471
502
|
|
|
472
|
-
function runHintsForSession(): string | null {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
503
|
+
async function runHintsForSession(client: LogCapableClient, sessionID?: string): Promise<string | null> {
|
|
504
|
+
return runCliSyncBestEffort(client, ["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS, {
|
|
505
|
+
toolName: "session.start",
|
|
506
|
+
sessionID,
|
|
507
|
+
subsystem: "hints",
|
|
508
|
+
operation: "session-hints",
|
|
509
|
+
})
|
|
477
510
|
}
|
|
478
511
|
|
|
479
512
|
function summarizeWorkflowList(value: unknown): string | null {
|
|
@@ -507,10 +540,15 @@ function summarizeWorkflowList(value: unknown): string | null {
|
|
|
507
540
|
return null
|
|
508
541
|
}
|
|
509
542
|
|
|
510
|
-
function runWorkflowSummaryForSession(): string | null {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
543
|
+
async function runWorkflowSummaryForSession(client: LogCapableClient, sessionID?: string): Promise<string | null> {
|
|
544
|
+
const raw = await runCliSyncBestEffort(client, ["--format", "json", "-q", "workflow", "list", "--active"], AKM_CURATE_TIMEOUT_MS, {
|
|
545
|
+
toolName: "session.start",
|
|
546
|
+
sessionID,
|
|
547
|
+
subsystem: "workflow",
|
|
548
|
+
operation: "active-workflow-summary",
|
|
549
|
+
})
|
|
550
|
+
if (!raw) return null
|
|
551
|
+
const parsed = parseMaybeJson(raw)
|
|
514
552
|
const summary = summarizeWorkflowList(
|
|
515
553
|
Array.isArray(parsed)
|
|
516
554
|
? parsed
|
|
@@ -547,14 +585,20 @@ function summarizeCuratorReportForContext(report: string): string {
|
|
|
547
585
|
return `${report.slice(0, AKM_CURATOR_CONTEXT_MAX_CHARS).trimEnd()}\n\n[truncated for context]`
|
|
548
586
|
}
|
|
549
587
|
|
|
550
|
-
function getAkmStashDir(): string | undefined {
|
|
588
|
+
async function getAkmStashDir(client?: LogCapableClient): Promise<string | undefined> {
|
|
551
589
|
if (cachedAkmStashDir !== undefined) return cachedAkmStashDir || undefined
|
|
552
|
-
const
|
|
553
|
-
|
|
590
|
+
const raw = client
|
|
591
|
+
? await runCliSyncBestEffort(client, ["--format", "json", "-q", "config", "get", "stashDir"], AKM_CURATE_TIMEOUT_MS, {
|
|
592
|
+
toolName: "shell.env",
|
|
593
|
+
subsystem: "config",
|
|
594
|
+
operation: "get-stash-dir",
|
|
595
|
+
})
|
|
596
|
+
: runCurate(["--format", "json", "-q", "config", "get", "stashDir"])
|
|
597
|
+
if (!raw) {
|
|
554
598
|
cachedAkmStashDir = ""
|
|
555
599
|
return undefined
|
|
556
600
|
}
|
|
557
|
-
const parsed = parseMaybeJson(
|
|
601
|
+
const parsed = parseMaybeJson(raw)
|
|
558
602
|
if (typeof parsed === "string" && parsed.trim()) {
|
|
559
603
|
cachedAkmStashDir = parsed.trim()
|
|
560
604
|
return cachedAkmStashDir
|
|
@@ -568,7 +612,6 @@ function getAkmStashDir(): string | undefined {
|
|
|
568
612
|
}
|
|
569
613
|
}
|
|
570
614
|
}
|
|
571
|
-
const raw = result.stdout.trim()
|
|
572
615
|
cachedAkmStashDir = raw || ""
|
|
573
616
|
return cachedAkmStashDir || undefined
|
|
574
617
|
}
|
|
@@ -602,6 +645,29 @@ function emitWorkflowTelemetry(client: LogCapableClient, level: LogLevel, eventT
|
|
|
602
645
|
})
|
|
603
646
|
}
|
|
604
647
|
|
|
648
|
+
async function runCliSyncBestEffort(
|
|
649
|
+
client: LogCapableClient,
|
|
650
|
+
args: string[],
|
|
651
|
+
timeoutMs: number,
|
|
652
|
+
meta: CliLogMeta & { subsystem: string; operation: string },
|
|
653
|
+
): Promise<string | null> {
|
|
654
|
+
const result = runCliSyncRaw(args, timeoutMs)
|
|
655
|
+
if (!result.ok) {
|
|
656
|
+
await writePluginLog(client, "warn", "AKM synchronous helper failed", {
|
|
657
|
+
subsystem: meta.subsystem,
|
|
658
|
+
operation: meta.operation,
|
|
659
|
+
toolName: meta.toolName,
|
|
660
|
+
sessionID: meta.sessionID,
|
|
661
|
+
directory: meta.directory,
|
|
662
|
+
args,
|
|
663
|
+
error: result.error,
|
|
664
|
+
})
|
|
665
|
+
return null
|
|
666
|
+
}
|
|
667
|
+
const body = result.stdout.trim()
|
|
668
|
+
return body || null
|
|
669
|
+
}
|
|
670
|
+
|
|
605
671
|
function noteRecentRefs(sessionID: string | undefined, refs: string[]) {
|
|
606
672
|
if (!sessionID || refs.length === 0) return
|
|
607
673
|
const state = retrospectiveState.get(sessionID) ?? { recentRefs: [] }
|
|
@@ -659,7 +725,7 @@ async function recordRetrospectiveFeedback(client: LogCapableClient, sessionID:
|
|
|
659
725
|
sessionID,
|
|
660
726
|
})
|
|
661
727
|
const parsed = safeJsonParse<{ ok?: boolean }>(raw)
|
|
662
|
-
if (parsed?.ok
|
|
728
|
+
if (parsed?.ok === true) {
|
|
663
729
|
await emitWorkflowTelemetry(client, "info", "akm.feedback.recorded", {
|
|
664
730
|
sessionID,
|
|
665
731
|
toolName: "akm_feedback",
|
|
@@ -1490,32 +1556,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1490
1556
|
} catch (error: unknown) {
|
|
1491
1557
|
let message = formatCliError(error)
|
|
1492
1558
|
if (["akm_reflect", "akm_propose"].includes(meta.toolName) && needsAgentSetup(message)) {
|
|
1493
|
-
|
|
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
|
-
}
|
|
1559
|
+
message = addAgentSetupGuidance(message)
|
|
1519
1560
|
}
|
|
1520
1561
|
message = addAgentSetupGuidance(message)
|
|
1521
1562
|
await writePluginLog(client, "error", "AKM command failed", {
|
|
@@ -2116,10 +2157,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2116
2157
|
directory,
|
|
2117
2158
|
sessionID: sid,
|
|
2118
2159
|
trigger: "session.created",
|
|
2160
|
+
platform: "opencode",
|
|
2119
2161
|
})
|
|
2120
2162
|
warmIndexInBackground()
|
|
2121
2163
|
if (AKM_AUTO_CURATE && !sessionCurated.has(sid)) {
|
|
2122
|
-
const curated = runCurateForSession(sid)
|
|
2164
|
+
const curated = await runCurateForSession(logClient, sid)
|
|
2123
2165
|
if (curated) {
|
|
2124
2166
|
bumpCuratedVersion(sid)
|
|
2125
2167
|
sessionCurated.set(sid, curated)
|
|
@@ -2127,11 +2169,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2127
2169
|
}
|
|
2128
2170
|
}
|
|
2129
2171
|
if (AKM_AUTO_HINTS && !sessionHints.has(sid)) {
|
|
2130
|
-
const hints = runHintsForSession()
|
|
2172
|
+
const hints = await runHintsForSession(logClient, sid)
|
|
2131
2173
|
if (hints) sessionHints.set(sid, hints)
|
|
2132
2174
|
}
|
|
2133
2175
|
if (!sessionWorkflow.has(sid)) {
|
|
2134
|
-
sessionWorkflow.set(sid, runWorkflowSummaryForSession() ?? "")
|
|
2176
|
+
sessionWorkflow.set(sid, await runWorkflowSummaryForSession(logClient, sid) ?? "")
|
|
2135
2177
|
}
|
|
2136
2178
|
const proposalSummary = await getPendingProposalCount(logClient, sid)
|
|
2137
2179
|
if (!proposalSummary.unsupported && proposalSummary.count > 0) {
|
|
@@ -2166,8 +2208,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2166
2208
|
sessionBuffer.delete(sid)
|
|
2167
2209
|
}
|
|
2168
2210
|
}
|
|
2169
|
-
} catch {
|
|
2170
|
-
|
|
2211
|
+
} catch (error: unknown) {
|
|
2212
|
+
await logHookFailure(logClient, "event", error)
|
|
2171
2213
|
}
|
|
2172
2214
|
},
|
|
2173
2215
|
// Stop is the closest analogue to Claude's Stop/SubagentStop — the user or
|
|
@@ -2188,8 +2230,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2188
2230
|
})
|
|
2189
2231
|
await maybeIndexSessionMemory(logClient, sid, "stop", captured)
|
|
2190
2232
|
}
|
|
2191
|
-
} catch {
|
|
2192
|
-
|
|
2233
|
+
} catch (error: unknown) {
|
|
2234
|
+
await logHookFailure(logClient, "stop", error)
|
|
2193
2235
|
}
|
|
2194
2236
|
},
|
|
2195
2237
|
// experimental.chat.system.transform is how OpenCode exposes the
|
|
@@ -2228,8 +2270,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2228
2270
|
sessionCuratedInjectedVersion.set(sid, curatedVersion)
|
|
2229
2271
|
}
|
|
2230
2272
|
}
|
|
2231
|
-
} catch {
|
|
2232
|
-
|
|
2273
|
+
} catch (error: unknown) {
|
|
2274
|
+
await logHookFailure(logClient, "experimental.chat.system.transform", error)
|
|
2233
2275
|
}
|
|
2234
2276
|
},
|
|
2235
2277
|
"tool.execute.before": async (input, output) => {
|
|
@@ -2245,8 +2287,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2245
2287
|
return
|
|
2246
2288
|
}
|
|
2247
2289
|
output.args = args
|
|
2248
|
-
} catch {
|
|
2249
|
-
|
|
2290
|
+
} catch (error: unknown) {
|
|
2291
|
+
await logHookFailure(logClient, "tool.execute.before", error, {
|
|
2292
|
+
toolName: input?.tool,
|
|
2293
|
+
sessionID: input?.sessionID,
|
|
2294
|
+
})
|
|
2250
2295
|
}
|
|
2251
2296
|
},
|
|
2252
2297
|
"permission.ask": async (input, output) => {
|
|
@@ -2274,8 +2319,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2274
2319
|
command,
|
|
2275
2320
|
category: assessment.category,
|
|
2276
2321
|
})
|
|
2277
|
-
} catch {
|
|
2278
|
-
|
|
2322
|
+
} catch (error: unknown) {
|
|
2323
|
+
await logHookFailure(logClient, "permission.ask", error, {
|
|
2324
|
+
sessionID: input?.sessionID,
|
|
2325
|
+
command: typeof input?.metadata?.command === "string" ? input.metadata.command : undefined,
|
|
2326
|
+
})
|
|
2279
2327
|
}
|
|
2280
2328
|
},
|
|
2281
2329
|
"command.execute.before": async (input, output) => {
|
|
@@ -2292,18 +2340,21 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2292
2340
|
command,
|
|
2293
2341
|
category: assessment.category,
|
|
2294
2342
|
})
|
|
2295
|
-
} catch {
|
|
2296
|
-
|
|
2343
|
+
} catch (error: unknown) {
|
|
2344
|
+
await logHookFailure(logClient, "command.execute.before", error, {
|
|
2345
|
+
sessionID: input?.sessionID,
|
|
2346
|
+
command: input?.command,
|
|
2347
|
+
})
|
|
2297
2348
|
}
|
|
2298
2349
|
},
|
|
2299
2350
|
"shell.env": async (_input, output) => {
|
|
2300
2351
|
try {
|
|
2301
2352
|
output.env.AKM_PROJECT = worktree
|
|
2302
2353
|
output.env.AKM_PLUGIN_VERSION = PLUGIN_VERSION
|
|
2303
|
-
const stashDir = getAkmStashDir()
|
|
2354
|
+
const stashDir = await getAkmStashDir(logClient)
|
|
2304
2355
|
if (stashDir) output.env.AKM_STASH_DIR = stashDir
|
|
2305
|
-
} catch {
|
|
2306
|
-
|
|
2356
|
+
} catch (error: unknown) {
|
|
2357
|
+
await logHookFailure(logClient, "shell.env", error)
|
|
2307
2358
|
}
|
|
2308
2359
|
},
|
|
2309
2360
|
"chat.message": async (input, output) => {
|
|
@@ -2319,12 +2370,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2319
2370
|
text: truncateLogText(text),
|
|
2320
2371
|
})
|
|
2321
2372
|
|
|
2322
|
-
//
|
|
2323
|
-
//
|
|
2373
|
+
// Session-start curation already seeded context. On later user messages,
|
|
2374
|
+
// inject only a small reminder instead of firing more automatic AKM CLI calls.
|
|
2324
2375
|
if (AKM_AUTO_CURATE && input.sessionID) {
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2376
|
+
const hint = "Need more AKM context? Use `akm_search` or `akm_curate` before writing from scratch."
|
|
2377
|
+
const current = sessionCurated.get(input.sessionID) ?? ""
|
|
2378
|
+
if (!current.includes(hint)) {
|
|
2379
|
+
sessionCurated.set(input.sessionID, current ? `${current}\n\n${hint}` : hint)
|
|
2328
2380
|
bumpCuratedVersion(input.sessionID)
|
|
2329
2381
|
}
|
|
2330
2382
|
}
|
|
@@ -2552,6 +2604,32 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2552
2604
|
if (note) args.push("--note", note)
|
|
2553
2605
|
args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
|
|
2554
2606
|
const raw = await runCli(client as unknown as LogCapableClient, args, { toolName: "akm_feedback", sessionID: context.sessionID, directory: context.directory })
|
|
2607
|
+
const parsed = safeJsonParse<{ ok?: boolean; error?: string }>(raw)
|
|
2608
|
+
if (parsed?.ok === false) {
|
|
2609
|
+
const error = parsed.error ?? "Unknown akm feedback error"
|
|
2610
|
+
if (isNotIndexedFeedbackError(error)) {
|
|
2611
|
+
await writePluginLog(logClient, "warn", "AKM feedback skipped", {
|
|
2612
|
+
subsystem: "feedback",
|
|
2613
|
+
toolName: "akm_feedback",
|
|
2614
|
+
sessionID: context.sessionID,
|
|
2615
|
+
directory: context.directory,
|
|
2616
|
+
ref,
|
|
2617
|
+
sentiment,
|
|
2618
|
+
reason: "ref_not_indexed",
|
|
2619
|
+
error,
|
|
2620
|
+
})
|
|
2621
|
+
await emitWorkflowTelemetry(logClient, "warn", "akm.feedback.skipped", {
|
|
2622
|
+
sessionID: context.sessionID,
|
|
2623
|
+
toolName: "akm_feedback",
|
|
2624
|
+
assetRef: ref,
|
|
2625
|
+
outcome: "skipped",
|
|
2626
|
+
reason: "ref not indexed",
|
|
2627
|
+
directory: context.directory,
|
|
2628
|
+
})
|
|
2629
|
+
return JSON.stringify({ ok: true, skipped: true, reason: "ref_not_indexed", ref, sentiment })
|
|
2630
|
+
}
|
|
2631
|
+
return raw
|
|
2632
|
+
}
|
|
2555
2633
|
await emitWorkflowTelemetry(logClient, "info", "akm.feedback.recorded", {
|
|
2556
2634
|
sessionID: context.sessionID,
|
|
2557
2635
|
toolName: "akm_feedback",
|
|
@@ -2916,6 +2994,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2916
2994
|
})
|
|
2917
2995
|
return JSON.stringify({ ok: true, ref, shell: stdout.trim() })
|
|
2918
2996
|
} catch (error: unknown) {
|
|
2997
|
+
await writePluginLog(logClient, "error", "AKM vault load failed", {
|
|
2998
|
+
subsystem: "vault",
|
|
2999
|
+
toolName: "akm_vault",
|
|
3000
|
+
action: "load",
|
|
3001
|
+
ref,
|
|
3002
|
+
error: formatCliError(error),
|
|
3003
|
+
})
|
|
2919
3004
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
2920
3005
|
}
|
|
2921
3006
|
}
|
|
@@ -3023,6 +3108,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3023
3108
|
})
|
|
3024
3109
|
return stdout
|
|
3025
3110
|
} catch (error: unknown) {
|
|
3111
|
+
await writePluginLog(logClient, "error", "AKM wiki stash failed", {
|
|
3112
|
+
subsystem: "wiki",
|
|
3113
|
+
toolName: "akm_wiki",
|
|
3114
|
+
action: "stash",
|
|
3115
|
+
name,
|
|
3116
|
+
source,
|
|
3117
|
+
as_slug,
|
|
3118
|
+
error: formatCliError(error),
|
|
3119
|
+
})
|
|
3026
3120
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
3027
3121
|
}
|
|
3028
3122
|
}
|
|
@@ -3130,6 +3224,12 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3130
3224
|
})
|
|
3131
3225
|
return JSON.stringify({ ok: true, template: stdout })
|
|
3132
3226
|
} catch (error: unknown) {
|
|
3227
|
+
await writePluginLog(logClient, "error", "AKM workflow template failed", {
|
|
3228
|
+
subsystem: "workflow",
|
|
3229
|
+
toolName: "akm_workflow",
|
|
3230
|
+
action: "template",
|
|
3231
|
+
error: formatCliError(error),
|
|
3232
|
+
})
|
|
3133
3233
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
3134
3234
|
}
|
|
3135
3235
|
}
|
|
@@ -3212,15 +3312,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3212
3312
|
return runCli(client as unknown as LogCapableClient, ["distill", ref], { toolName: "akm_distill" })
|
|
3213
3313
|
},
|
|
3214
3314
|
}),
|
|
3215
|
-
|
|
3216
|
-
description: "
|
|
3217
|
-
args: {
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
async execute({ force }) {
|
|
3221
|
-
const args = ["setup"]
|
|
3222
|
-
if (force) args.push("--force")
|
|
3223
|
-
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_setup" })
|
|
3315
|
+
akm_init: tool({
|
|
3316
|
+
description: "Initialize AKM's working stash directory and persist `stashDir` in config. This is the agent-safe initialization path; do not use interactive `akm setup` from tools.",
|
|
3317
|
+
args: {},
|
|
3318
|
+
async execute() {
|
|
3319
|
+
return runCli(client as unknown as LogCapableClient, ["init"], { toolName: "akm_init" })
|
|
3224
3320
|
},
|
|
3225
3321
|
}),
|
|
3226
3322
|
akm_help: tool({
|
|
@@ -3242,6 +3338,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3242
3338
|
timeout: 30_000,
|
|
3243
3339
|
}).toString().trim()
|
|
3244
3340
|
} catch (error: unknown) {
|
|
3341
|
+
await writePluginLog(logClient, "error", "AKM help command failed", {
|
|
3342
|
+
subsystem: "help",
|
|
3343
|
+
toolName: "akm_help",
|
|
3344
|
+
command: cliCommand,
|
|
3345
|
+
args: helpArgs,
|
|
3346
|
+
error: formatCliError(error),
|
|
3347
|
+
})
|
|
3245
3348
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
3246
3349
|
}
|
|
3247
3350
|
return JSON.stringify({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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": [
|