akm-opencode 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/index.ts +139 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,14 +41,14 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
|
|
|
41
41
|
|
|
42
42
|
| Event | What happens |
|
|
43
43
|
| --- | --- |
|
|
44
|
-
| **`session.created`** (event hook) | Warms the stash index in the background
|
|
45
|
-
| **`chat.message`** | Runs `akm curate "<prompt>"
|
|
44
|
+
| **`session.created`** (event hook) | 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 see relevant stash context before the first user message. |
|
|
45
|
+
| **`chat.message`** | Runs `akm curate "<prompt>" --run <sessionID>` 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. |
|
|
46
46
|
| **`experimental.chat.system.transform`** | Appends cached hints, active workflow state, 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. |
|
|
47
47
|
| **`tool.execute.before`** (`akm_*` tools) | Blocks destructive or sensitive operations until `confirm:true` is provided. |
|
|
48
48
|
| **`tool.execute.after`** (`akm_*` tools) | Logs asset usage, accumulates refs into the session buffer, records `akm feedback <ref> --positive` / `--negative` asynchronously with per-call dedupe, checkpoints memories every `AKM_MEMORY_CHECKPOINT_EVERY` successful asset-touching tool calls, and scans child-agent free text for additional refs. |
|
|
49
49
|
| **`experimental.session.compacting`** | Pushes hints, curated context, active workflows, and the last curator report into the compaction prompt so they survive transcript shrinking. |
|
|
50
50
|
| **`shell.env`** | Exposes `AKM_STASH_DIR`, `AKM_PROJECT`, and `AKM_PLUGIN_VERSION` to shell tools so plain `akm` calls inherit the right context. |
|
|
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. |
|
|
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. When `AKM_INDEX_ON_SESSION_END=1`, the hook follows a successful flush with `akm index` so upstream inference/graph passes run immediately. |
|
|
52
52
|
|
|
53
53
|
### Environment overrides
|
|
54
54
|
|
|
@@ -58,9 +58,11 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
|
|
|
58
58
|
| `AKM_AUTO_FEEDBACK` | `1` | Set to `0` to disable automatic `akm feedback` on tool success/failure. |
|
|
59
59
|
| `AKM_AUTO_HINTS` | `1` | Set to `0` to skip injecting `akm hints` at session start. |
|
|
60
60
|
| `AKM_AUTO_MEMORY` | `1` | Set to `0` to disable automatic session-summary memories. |
|
|
61
|
+
| `AKM_INDEX_ON_SESSION_END` | `0` | Set to `1` to run `akm index` after a session-end memory is captured. |
|
|
61
62
|
| `AKM_CURATE_LIMIT` | `5` | Max curated results injected into context per prompt. |
|
|
62
63
|
| `AKM_CURATE_MIN_CHARS` | `16` | Minimum prompt length before curation runs. |
|
|
63
64
|
| `AKM_CURATE_TIMEOUT` | `8` | Wall-clock seconds for `akm` invocations inside hooks. |
|
|
65
|
+
| `AKM_CONTEXT_BUDGET_CHARS` | `4000` | Max total characters injected into system/compaction context for a single turn. |
|
|
64
66
|
| `AKM_CURATOR_CONTEXT_MAX_CHARS` | `4000` | Max cached curator-report characters re-injected into system/compaction context; the full report is still persisted as memory. |
|
|
65
67
|
| `AKM_MEMORY_CHECKPOINT_EVERY` | `8` | Number of successful asset-touching tool calls between mid-session checkpoint memories. |
|
|
66
68
|
| `AKM_RETROSPECTIVE_FEEDBACK_PATTERN` | `\b(thanks|perfect|worked)\b` | Case-insensitive regex used for lightweight positive retrospective feedback on the most recent refs. |
|
package/index.ts
CHANGED
|
@@ -207,6 +207,10 @@ function nowIso(): string {
|
|
|
207
207
|
return new Date().toISOString()
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
function shouldIndexOnSessionEnd(): boolean {
|
|
211
|
+
return (process.env.AKM_INDEX_ON_SESSION_END ?? "0") === "1"
|
|
212
|
+
}
|
|
213
|
+
|
|
210
214
|
function buildDateTag(options?: { includeTime?: boolean }): string {
|
|
211
215
|
const compactIso = new Date().toISOString().replace(/[-:]/g, "")
|
|
212
216
|
return compactIso.slice(0, options?.includeTime ? CHECKPOINT_DATE_TAG_LENGTH : SESSION_DATE_TAG_LENGTH)
|
|
@@ -257,28 +261,57 @@ function runCliSyncRaw(args: string[], timeoutMs: number): { ok: true; stdout: s
|
|
|
257
261
|
}
|
|
258
262
|
}
|
|
259
263
|
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"text",
|
|
267
|
-
"--detail",
|
|
268
|
-
"summary",
|
|
269
|
-
"-q",
|
|
270
|
-
"curate",
|
|
271
|
-
text,
|
|
272
|
-
"--limit",
|
|
273
|
-
String(AKM_CURATE_LIMIT),
|
|
274
|
-
],
|
|
275
|
-
AKM_CURATE_TIMEOUT_MS,
|
|
276
|
-
)
|
|
264
|
+
function appendRunScopeArgs(args: string[], sessionID: string | undefined): string[] {
|
|
265
|
+
return sessionID ? [...args, "--run", sessionID] : args
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function runCurate(args: string[]): string | null {
|
|
269
|
+
const result = runCliSyncRaw(args, AKM_CURATE_TIMEOUT_MS)
|
|
277
270
|
if (!result.ok) return null
|
|
278
271
|
const body = result.stdout.trim()
|
|
279
272
|
return body || null
|
|
280
273
|
}
|
|
281
274
|
|
|
275
|
+
function runCurateForPrompt(text: string, sessionID?: string): string | null {
|
|
276
|
+
if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
|
|
277
|
+
return runCurate(
|
|
278
|
+
appendRunScopeArgs(
|
|
279
|
+
[
|
|
280
|
+
"--for-agent",
|
|
281
|
+
"--format",
|
|
282
|
+
"text",
|
|
283
|
+
"--detail",
|
|
284
|
+
"summary",
|
|
285
|
+
"-q",
|
|
286
|
+
"curate",
|
|
287
|
+
text,
|
|
288
|
+
"--limit",
|
|
289
|
+
String(AKM_CURATE_LIMIT),
|
|
290
|
+
],
|
|
291
|
+
sessionID,
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function runCurateForSession(sessionID: string): string | null {
|
|
297
|
+
return runCurate(
|
|
298
|
+
appendRunScopeArgs(
|
|
299
|
+
[
|
|
300
|
+
"--for-agent",
|
|
301
|
+
"--format",
|
|
302
|
+
"text",
|
|
303
|
+
"--detail",
|
|
304
|
+
"summary",
|
|
305
|
+
"-q",
|
|
306
|
+
"curate",
|
|
307
|
+
"--limit",
|
|
308
|
+
String(AKM_CURATE_LIMIT),
|
|
309
|
+
],
|
|
310
|
+
sessionID,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
282
315
|
function runHintsForSession(): string | null {
|
|
283
316
|
const result = runCliSyncRaw(["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS)
|
|
284
317
|
if (!result.ok) return null
|
|
@@ -545,6 +578,25 @@ function maybeCheckpointSessionMemory(sessionID: string): string | null {
|
|
|
545
578
|
return captured
|
|
546
579
|
}
|
|
547
580
|
|
|
581
|
+
async function maybeIndexSessionMemory(
|
|
582
|
+
client: LogCapableClient,
|
|
583
|
+
sessionID: string,
|
|
584
|
+
reason: string,
|
|
585
|
+
ref: string,
|
|
586
|
+
): Promise<void> {
|
|
587
|
+
if (!shouldIndexOnSessionEnd()) return
|
|
588
|
+
const result = runCliSyncRaw(["index"], AKM_CURATE_TIMEOUT_MS)
|
|
589
|
+
if (result.ok) return
|
|
590
|
+
await writePluginLog(client, "warn", "AKM session indexing failed", {
|
|
591
|
+
subsystem: "memory",
|
|
592
|
+
actor: "system",
|
|
593
|
+
sessionID,
|
|
594
|
+
reason,
|
|
595
|
+
ref,
|
|
596
|
+
error: result.error,
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
|
|
548
600
|
const AKM_REF_EDGE_PUNCTUATION = new Set([".", ",", ";", ":", "!", "?", "(", ")", "[", "]", "{", "}", "'", "\"", "`"])
|
|
549
601
|
|
|
550
602
|
function normalizeExtractedRef(ref: string): string {
|
|
@@ -616,6 +668,41 @@ const AKM_HINTS_PREFIX = [
|
|
|
616
668
|
|
|
617
669
|
const AKM_CURATED_HEADER = "# AKM stash — assets relevant to this prompt"
|
|
618
670
|
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."
|
|
671
|
+
const AKM_CONTEXT_TRUNCATED_MARKER = "\n\n[truncated for context]"
|
|
672
|
+
|
|
673
|
+
function getContextBudgetChars(): number {
|
|
674
|
+
const parsed = Number(process.env.AKM_CONTEXT_BUDGET_CHARS)
|
|
675
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 4000
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function truncateContextBlock(block: string, maxChars: number): string {
|
|
679
|
+
if (block.length <= maxChars) return block
|
|
680
|
+
if (maxChars <= AKM_CONTEXT_TRUNCATED_MARKER.length) return block.slice(0, maxChars)
|
|
681
|
+
return `${block.slice(0, maxChars - AKM_CONTEXT_TRUNCATED_MARKER.length)}${AKM_CONTEXT_TRUNCATED_MARKER}`
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function applyContextBudget(blocks: string[]): string[] {
|
|
685
|
+
const budget = getContextBudgetChars()
|
|
686
|
+
const injected: string[] = []
|
|
687
|
+
let remaining = budget
|
|
688
|
+
for (const block of blocks) {
|
|
689
|
+
if (!block) continue
|
|
690
|
+
// The host effectively concatenates injected blocks into one prompt body;
|
|
691
|
+
// we budget for a single newline separator between adjacent blocks.
|
|
692
|
+
const separatorCost = injected.length > 0 ? 1 : 0
|
|
693
|
+
if (remaining <= separatorCost) break
|
|
694
|
+
const allowed = remaining - separatorCost
|
|
695
|
+
if (block.length <= allowed) {
|
|
696
|
+
injected.push(block)
|
|
697
|
+
remaining -= separatorCost + block.length
|
|
698
|
+
continue
|
|
699
|
+
}
|
|
700
|
+
const truncated = truncateContextBlock(block, allowed)
|
|
701
|
+
if (truncated) injected.push(truncated)
|
|
702
|
+
break
|
|
703
|
+
}
|
|
704
|
+
return injected
|
|
705
|
+
}
|
|
619
706
|
|
|
620
707
|
// Curated quick-reference for the long-tail of `akm` CLI verbs that no longer
|
|
621
708
|
// have a dedicated tool wrapper. Surfaced through akm_help so agents can
|
|
@@ -1552,7 +1639,16 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1552
1639
|
if (type === "session.created" || type === "session.updated") {
|
|
1553
1640
|
if (!sid) return
|
|
1554
1641
|
if (!sessionContextEpoch.has(sid)) sessionContextEpoch.set(sid, 0)
|
|
1555
|
-
if (type === "session.created")
|
|
1642
|
+
if (type === "session.created") {
|
|
1643
|
+
warmIndexInBackground()
|
|
1644
|
+
if (AKM_AUTO_CURATE && !sessionCurated.has(sid)) {
|
|
1645
|
+
const curated = runCurateForSession(sid)
|
|
1646
|
+
if (curated) {
|
|
1647
|
+
bumpCuratedVersion(sid)
|
|
1648
|
+
sessionCurated.set(sid, curated)
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1556
1652
|
if (AKM_AUTO_HINTS && !sessionHints.has(sid)) {
|
|
1557
1653
|
const hints = runHintsForSession()
|
|
1558
1654
|
if (hints) sessionHints.set(sid, hints)
|
|
@@ -1571,6 +1667,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1571
1667
|
reason: type,
|
|
1572
1668
|
ref: captured,
|
|
1573
1669
|
})
|
|
1670
|
+
await maybeIndexSessionMemory(logClient, sid, type, captured)
|
|
1574
1671
|
}
|
|
1575
1672
|
// Drop per-session state so a re-created session does not inherit
|
|
1576
1673
|
// stale hints/curation.
|
|
@@ -1608,6 +1705,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1608
1705
|
reason: "stop",
|
|
1609
1706
|
ref: captured,
|
|
1610
1707
|
})
|
|
1708
|
+
await maybeIndexSessionMemory(logClient, sid, "stop", captured)
|
|
1611
1709
|
}
|
|
1612
1710
|
} catch {
|
|
1613
1711
|
// Best-effort only.
|
|
@@ -1619,14 +1717,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1619
1717
|
if (!sid) return
|
|
1620
1718
|
if (!Array.isArray(output.context)) return
|
|
1621
1719
|
markContextEpochDirty(sid)
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
if (curatorReport) output.context.push(formatCuratorReportContext(curatorReport))
|
|
1720
|
+
const blocks = [
|
|
1721
|
+
sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
|
|
1722
|
+
sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
|
|
1723
|
+
sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
|
|
1724
|
+
sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
|
|
1725
|
+
]
|
|
1726
|
+
output.context.push(...applyContextBudget(blocks))
|
|
1630
1727
|
} catch {
|
|
1631
1728
|
// Never break compaction because of plugin context.
|
|
1632
1729
|
}
|
|
@@ -1644,19 +1741,25 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1644
1741
|
const epoch = sessionContextEpoch.get(sid) ?? 0
|
|
1645
1742
|
const injectedEpoch = sessionContextInjectedEpoch.get(sid)
|
|
1646
1743
|
if (sid && injectedEpoch !== epoch) {
|
|
1647
|
-
const
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1744
|
+
const blocks = [
|
|
1745
|
+
sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
|
|
1746
|
+
sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
|
|
1747
|
+
sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
|
|
1748
|
+
sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
|
|
1749
|
+
]
|
|
1750
|
+
output.system.push(...applyContextBudget(blocks))
|
|
1653
1751
|
sessionContextInjectedEpoch.set(sid, epoch)
|
|
1752
|
+
if (sessionCurated.has(sid)) {
|
|
1753
|
+
// Startup curation is already included in the epoch-scoped block,
|
|
1754
|
+
// so mark that version as consumed to avoid a duplicate inject below.
|
|
1755
|
+
sessionCuratedInjectedVersion.set(sid, sessionCuratedVersion.get(sid) ?? 0)
|
|
1756
|
+
}
|
|
1654
1757
|
}
|
|
1655
1758
|
const curated = sid ? sessionCurated.get(sid) : undefined
|
|
1656
1759
|
const curatedVersion = sessionCuratedVersion.get(sid) ?? 0
|
|
1657
1760
|
if (curated) {
|
|
1658
1761
|
if (sessionCuratedInjectedVersion.get(sid) !== curatedVersion) {
|
|
1659
|
-
output.system.push(`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`)
|
|
1762
|
+
output.system.push(...applyContextBudget([`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`]))
|
|
1660
1763
|
sessionCuratedInjectedVersion.set(sid, curatedVersion)
|
|
1661
1764
|
}
|
|
1662
1765
|
}
|
|
@@ -1706,7 +1809,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1706
1809
|
// Compound-engineering loop: on every user message, curate the stash and
|
|
1707
1810
|
// stash the result so experimental.chat.system.transform can inject it.
|
|
1708
1811
|
if (AKM_AUTO_CURATE && input.sessionID) {
|
|
1709
|
-
const curated = runCurateForPrompt(text)
|
|
1812
|
+
const curated = runCurateForPrompt(text, input.sessionID)
|
|
1710
1813
|
if (curated) {
|
|
1711
1814
|
sessionCurated.set(input.sessionID, curated)
|
|
1712
1815
|
bumpCuratedVersion(input.sessionID)
|
|
@@ -1907,18 +2010,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1907
2010
|
detail: tool.schema.enum(["summary", "normal", "full"]).optional().describe("Detail level for each match. Defaults to 'summary'."),
|
|
1908
2011
|
},
|
|
1909
2012
|
async execute({ query, limit, detail }) {
|
|
1910
|
-
const args = [
|
|
1911
|
-
|
|
1912
|
-
"--format",
|
|
1913
|
-
"text",
|
|
1914
|
-
"--detail",
|
|
1915
|
-
detail ?? "summary",
|
|
1916
|
-
"-q",
|
|
1917
|
-
"curate",
|
|
1918
|
-
query,
|
|
1919
|
-
"--limit",
|
|
1920
|
-
String(limit ?? 6),
|
|
1921
|
-
]
|
|
2013
|
+
const args = ["curate", query, "--limit", String(limit ?? 6)]
|
|
2014
|
+
if (detail) args.push("--detail", detail)
|
|
1922
2015
|
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_curate" })
|
|
1923
2016
|
},
|
|
1924
2017
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for AKM - search, show, and manage extension assets via the akm CLI, including v0.5.0 vaults, wikis, and workflows, with agentic hooks that auto-load relevant stash assets, record feedback, and harvest session memories so the stash improves every session.",
|
|
6
6
|
"keywords": [
|