akm-opencode 0.5.1 → 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 +6 -4
- package/index.ts +275 -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. |
|
|
@@ -112,7 +114,7 @@ At least one of `ref` or `query` is required.
|
|
|
112
114
|
|
|
113
115
|
## Prerequisites
|
|
114
116
|
|
|
115
|
-
When the plugin loads, it runs `bun install -g akm-cli@latest`
|
|
117
|
+
When the plugin loads, it checks the installed `akm` version first and only runs `bun install -g akm-cli@latest` when `akm` is missing or older than the latest stable npm release. Newer pre-releases and local builds are left in place. It then prefers the Bun-installed binary and falls back to an existing `akm` on PATH when needed. It does not run the standalone shell installers automatically.
|
|
116
118
|
|
|
117
119
|
```sh
|
|
118
120
|
# macOS / Linux
|
package/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url"
|
|
|
7
7
|
let resolvedAkmCommand = "akm"
|
|
8
8
|
const autoInstallPackageRef = "akm-cli@latest"
|
|
9
9
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const SEMVER_PATTERN = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/
|
|
10
11
|
|
|
11
12
|
const AKM_AUTO_FEEDBACK = (process.env.AKM_AUTO_FEEDBACK ?? "1") !== "0"
|
|
12
13
|
const AKM_AUTO_MEMORY = (process.env.AKM_AUTO_MEMORY ?? "1") !== "0"
|
|
@@ -32,6 +33,7 @@ const sessionContextEpoch = new Map<string, number>()
|
|
|
32
33
|
const sessionContextInjectedEpoch = new Map<string, number>()
|
|
33
34
|
const sessionCuratedVersion = new Map<string, number>()
|
|
34
35
|
const sessionCuratedInjectedVersion = new Map<string, number>()
|
|
36
|
+
type ParsedSemver = { core: [number, number, number]; prerelease: Array<string | number> | null }
|
|
35
37
|
type SessionBufferEntry = {
|
|
36
38
|
timestamp: string
|
|
37
39
|
kind: "memory-intent" | "tool-ref"
|
|
@@ -205,6 +207,10 @@ function nowIso(): string {
|
|
|
205
207
|
return new Date().toISOString()
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
function shouldIndexOnSessionEnd(): boolean {
|
|
211
|
+
return (process.env.AKM_INDEX_ON_SESSION_END ?? "0") === "1"
|
|
212
|
+
}
|
|
213
|
+
|
|
208
214
|
function buildDateTag(options?: { includeTime?: boolean }): string {
|
|
209
215
|
const compactIso = new Date().toISOString().replace(/[-:]/g, "")
|
|
210
216
|
return compactIso.slice(0, options?.includeTime ? CHECKPOINT_DATE_TAG_LENGTH : SESSION_DATE_TAG_LENGTH)
|
|
@@ -255,28 +261,57 @@ function runCliSyncRaw(args: string[], timeoutMs: number): { ok: true; stdout: s
|
|
|
255
261
|
}
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"text",
|
|
265
|
-
"--detail",
|
|
266
|
-
"summary",
|
|
267
|
-
"-q",
|
|
268
|
-
"curate",
|
|
269
|
-
text,
|
|
270
|
-
"--limit",
|
|
271
|
-
String(AKM_CURATE_LIMIT),
|
|
272
|
-
],
|
|
273
|
-
AKM_CURATE_TIMEOUT_MS,
|
|
274
|
-
)
|
|
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)
|
|
275
270
|
if (!result.ok) return null
|
|
276
271
|
const body = result.stdout.trim()
|
|
277
272
|
return body || null
|
|
278
273
|
}
|
|
279
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
|
+
|
|
280
315
|
function runHintsForSession(): string | null {
|
|
281
316
|
const result = runCliSyncRaw(["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS)
|
|
282
317
|
if (!result.ok) return null
|
|
@@ -543,6 +578,25 @@ function maybeCheckpointSessionMemory(sessionID: string): string | null {
|
|
|
543
578
|
return captured
|
|
544
579
|
}
|
|
545
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
|
+
|
|
546
600
|
const AKM_REF_EDGE_PUNCTUATION = new Set([".", ",", ";", ":", "!", "?", "(", ")", "[", "]", "{", "}", "'", "\"", "`"])
|
|
547
601
|
|
|
548
602
|
function normalizeExtractedRef(ref: string): string {
|
|
@@ -614,6 +668,41 @@ const AKM_HINTS_PREFIX = [
|
|
|
614
668
|
|
|
615
669
|
const AKM_CURATED_HEADER = "# AKM stash — assets relevant to this prompt"
|
|
616
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
|
+
}
|
|
617
706
|
|
|
618
707
|
// Curated quick-reference for the long-tail of `akm` CLI verbs that no longer
|
|
619
708
|
// have a dedicated tool wrapper. Surfaced through akm_help so agents can
|
|
@@ -742,6 +831,72 @@ function getCommandStatus(command: string): "ok" | "missing" | "error" {
|
|
|
742
831
|
}
|
|
743
832
|
}
|
|
744
833
|
|
|
834
|
+
function extractFirstSemverMatch(value: string): string | null {
|
|
835
|
+
return value.match(SEMVER_PATTERN)?.[0] ?? null
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function getCommandVersion(command: string): string | null {
|
|
839
|
+
try {
|
|
840
|
+
const version = execFileSync(command, ["--version"], {
|
|
841
|
+
encoding: "utf8",
|
|
842
|
+
timeout: 10_000,
|
|
843
|
+
})
|
|
844
|
+
return extractFirstSemverMatch(version)
|
|
845
|
+
} catch {
|
|
846
|
+
return null
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function parseSemver(version: string): ParsedSemver | null {
|
|
851
|
+
const normalized = extractFirstSemverMatch(version)
|
|
852
|
+
if (!normalized) return null
|
|
853
|
+
|
|
854
|
+
const [withoutBuildMetadata] = normalized.split("+", 1)
|
|
855
|
+
const [release, prereleaseText] = withoutBuildMetadata.split("-", 2)
|
|
856
|
+
const parts = release.split(".").map((part) => Number(part))
|
|
857
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) {
|
|
858
|
+
return null
|
|
859
|
+
}
|
|
860
|
+
const core = [parts[0], parts[1], parts[2]] as [number, number, number]
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
core,
|
|
864
|
+
prerelease: prereleaseText
|
|
865
|
+
? prereleaseText.split(".").map((part) => (/^\d+$/.test(part) ? Number(part) : part))
|
|
866
|
+
: null,
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function compareSemver(left: string, right: string): number {
|
|
871
|
+
const leftParsed = parseSemver(left)
|
|
872
|
+
const rightParsed = parseSemver(right)
|
|
873
|
+
if (!leftParsed || !rightParsed) return left.localeCompare(right)
|
|
874
|
+
|
|
875
|
+
for (let index = 0; index < leftParsed.core.length; index += 1) {
|
|
876
|
+
const delta = leftParsed.core[index] - rightParsed.core[index]
|
|
877
|
+
if (delta !== 0) return delta
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (!leftParsed.prerelease && !rightParsed.prerelease) return 0
|
|
881
|
+
if (!leftParsed.prerelease) return 1
|
|
882
|
+
if (!rightParsed.prerelease) return -1
|
|
883
|
+
|
|
884
|
+
const length = Math.max(leftParsed.prerelease.length, rightParsed.prerelease.length)
|
|
885
|
+
for (let index = 0; index < length; index += 1) {
|
|
886
|
+
const leftPart = leftParsed.prerelease[index]
|
|
887
|
+
const rightPart = rightParsed.prerelease[index]
|
|
888
|
+
if (leftPart === undefined) return -1
|
|
889
|
+
if (rightPart === undefined) return 1
|
|
890
|
+
if (leftPart === rightPart) continue
|
|
891
|
+
if (typeof leftPart === "number" && typeof rightPart === "number") return leftPart - rightPart
|
|
892
|
+
if (typeof leftPart === "number") return -1
|
|
893
|
+
if (typeof rightPart === "number") return 1
|
|
894
|
+
return leftPart.localeCompare(rightPart)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return 0
|
|
898
|
+
}
|
|
899
|
+
|
|
745
900
|
function getBunGlobalAkmCommand(): string | null {
|
|
746
901
|
try {
|
|
747
902
|
const globalBin = execFileSync("bun", ["pm", "bin", "-g"], {
|
|
@@ -757,6 +912,39 @@ function getBunGlobalAkmCommand(): string | null {
|
|
|
757
912
|
}
|
|
758
913
|
}
|
|
759
914
|
|
|
915
|
+
function getInstalledAkmDetails(): { command: string; version: string } | null {
|
|
916
|
+
const candidates = [resolvedAkmCommand, getBunGlobalAkmCommand(), "akm"]
|
|
917
|
+
const seen = new Set<string>()
|
|
918
|
+
for (const candidate of candidates) {
|
|
919
|
+
if (!candidate || seen.has(candidate)) continue
|
|
920
|
+
seen.add(candidate)
|
|
921
|
+
const version = getCommandVersion(candidate)
|
|
922
|
+
if (version) return { command: candidate, version }
|
|
923
|
+
}
|
|
924
|
+
return null
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function getLatestNpmPackageVersion(packageName: string): Promise<string | null> {
|
|
928
|
+
if (typeof fetch !== "function") return null
|
|
929
|
+
|
|
930
|
+
const controller = new AbortController()
|
|
931
|
+
const timeout = setTimeout(() => controller.abort(), 10_000)
|
|
932
|
+
try {
|
|
933
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
934
|
+
headers: { accept: "application/json" },
|
|
935
|
+
signal: controller.signal,
|
|
936
|
+
})
|
|
937
|
+
if (!response.ok) return null
|
|
938
|
+
const body = await response.json()
|
|
939
|
+
if (!body || typeof body !== "object") return null
|
|
940
|
+
return typeof body.version === "string" ? extractFirstSemverMatch(body.version) : null
|
|
941
|
+
} catch {
|
|
942
|
+
return null
|
|
943
|
+
} finally {
|
|
944
|
+
clearTimeout(timeout)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
760
948
|
async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void> {
|
|
761
949
|
try {
|
|
762
950
|
execFileSync("bun", ["--version"], {
|
|
@@ -772,6 +960,39 @@ async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void>
|
|
|
772
960
|
return
|
|
773
961
|
}
|
|
774
962
|
|
|
963
|
+
const installedAkm = getInstalledAkmDetails()
|
|
964
|
+
const latestStable = await getLatestNpmPackageVersion("akm-cli")
|
|
965
|
+
|
|
966
|
+
if (installedAkm) {
|
|
967
|
+
if (!latestStable) {
|
|
968
|
+
resolvedAkmCommand = installedAkm.command
|
|
969
|
+
await writePluginLog(client, "info", "AKM auto-install skipped", {
|
|
970
|
+
subsystem: "akm",
|
|
971
|
+
installer: "bun",
|
|
972
|
+
package: autoInstallPackageRef,
|
|
973
|
+
command: resolvedAkmCommand,
|
|
974
|
+
installedVersion: installedAkm.version,
|
|
975
|
+
latestStable,
|
|
976
|
+
reason: "latest_version_unavailable",
|
|
977
|
+
})
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (compareSemver(installedAkm.version, latestStable) >= 0) {
|
|
982
|
+
resolvedAkmCommand = installedAkm.command
|
|
983
|
+
await writePluginLog(client, "info", "AKM auto-install skipped", {
|
|
984
|
+
subsystem: "akm",
|
|
985
|
+
installer: "bun",
|
|
986
|
+
package: autoInstallPackageRef,
|
|
987
|
+
command: resolvedAkmCommand,
|
|
988
|
+
installedVersion: installedAkm.version,
|
|
989
|
+
latestStable,
|
|
990
|
+
reason: "installed_version_not_older",
|
|
991
|
+
})
|
|
992
|
+
return
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
775
996
|
try {
|
|
776
997
|
execFileSync("bun", ["install", "-g", autoInstallPackageRef], {
|
|
777
998
|
encoding: "utf8",
|
|
@@ -791,6 +1012,8 @@ async function ensureLatestAkmInstalled(client: LogCapableClient): Promise<void>
|
|
|
791
1012
|
installer: "bun",
|
|
792
1013
|
package: autoInstallPackageRef,
|
|
793
1014
|
command: resolvedAkmCommand,
|
|
1015
|
+
installedVersion: installedAkm?.version ?? null,
|
|
1016
|
+
latestStable,
|
|
794
1017
|
})
|
|
795
1018
|
} catch (error: unknown) {
|
|
796
1019
|
await writePluginLog(client, "warn", "AKM auto-install failed", {
|
|
@@ -1416,7 +1639,16 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1416
1639
|
if (type === "session.created" || type === "session.updated") {
|
|
1417
1640
|
if (!sid) return
|
|
1418
1641
|
if (!sessionContextEpoch.has(sid)) sessionContextEpoch.set(sid, 0)
|
|
1419
|
-
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
|
+
}
|
|
1420
1652
|
if (AKM_AUTO_HINTS && !sessionHints.has(sid)) {
|
|
1421
1653
|
const hints = runHintsForSession()
|
|
1422
1654
|
if (hints) sessionHints.set(sid, hints)
|
|
@@ -1435,6 +1667,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1435
1667
|
reason: type,
|
|
1436
1668
|
ref: captured,
|
|
1437
1669
|
})
|
|
1670
|
+
await maybeIndexSessionMemory(logClient, sid, type, captured)
|
|
1438
1671
|
}
|
|
1439
1672
|
// Drop per-session state so a re-created session does not inherit
|
|
1440
1673
|
// stale hints/curation.
|
|
@@ -1472,6 +1705,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1472
1705
|
reason: "stop",
|
|
1473
1706
|
ref: captured,
|
|
1474
1707
|
})
|
|
1708
|
+
await maybeIndexSessionMemory(logClient, sid, "stop", captured)
|
|
1475
1709
|
}
|
|
1476
1710
|
} catch {
|
|
1477
1711
|
// Best-effort only.
|
|
@@ -1483,14 +1717,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1483
1717
|
if (!sid) return
|
|
1484
1718
|
if (!Array.isArray(output.context)) return
|
|
1485
1719
|
markContextEpochDirty(sid)
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
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))
|
|
1494
1727
|
} catch {
|
|
1495
1728
|
// Never break compaction because of plugin context.
|
|
1496
1729
|
}
|
|
@@ -1508,19 +1741,25 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1508
1741
|
const epoch = sessionContextEpoch.get(sid) ?? 0
|
|
1509
1742
|
const injectedEpoch = sessionContextInjectedEpoch.get(sid)
|
|
1510
1743
|
if (sid && injectedEpoch !== epoch) {
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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))
|
|
1517
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
|
+
}
|
|
1518
1757
|
}
|
|
1519
1758
|
const curated = sid ? sessionCurated.get(sid) : undefined
|
|
1520
1759
|
const curatedVersion = sessionCuratedVersion.get(sid) ?? 0
|
|
1521
1760
|
if (curated) {
|
|
1522
1761
|
if (sessionCuratedInjectedVersion.get(sid) !== curatedVersion) {
|
|
1523
|
-
output.system.push(`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`)
|
|
1762
|
+
output.system.push(...applyContextBudget([`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`]))
|
|
1524
1763
|
sessionCuratedInjectedVersion.set(sid, curatedVersion)
|
|
1525
1764
|
}
|
|
1526
1765
|
}
|
|
@@ -1570,7 +1809,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1570
1809
|
// Compound-engineering loop: on every user message, curate the stash and
|
|
1571
1810
|
// stash the result so experimental.chat.system.transform can inject it.
|
|
1572
1811
|
if (AKM_AUTO_CURATE && input.sessionID) {
|
|
1573
|
-
const curated = runCurateForPrompt(text)
|
|
1812
|
+
const curated = runCurateForPrompt(text, input.sessionID)
|
|
1574
1813
|
if (curated) {
|
|
1575
1814
|
sessionCurated.set(input.sessionID, curated)
|
|
1576
1815
|
bumpCuratedVersion(input.sessionID)
|
|
@@ -1771,18 +2010,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1771
2010
|
detail: tool.schema.enum(["summary", "normal", "full"]).optional().describe("Detail level for each match. Defaults to 'summary'."),
|
|
1772
2011
|
},
|
|
1773
2012
|
async execute({ query, limit, detail }) {
|
|
1774
|
-
const args = [
|
|
1775
|
-
|
|
1776
|
-
"--format",
|
|
1777
|
-
"text",
|
|
1778
|
-
"--detail",
|
|
1779
|
-
detail ?? "summary",
|
|
1780
|
-
"-q",
|
|
1781
|
-
"curate",
|
|
1782
|
-
query,
|
|
1783
|
-
"--limit",
|
|
1784
|
-
String(limit ?? 6),
|
|
1785
|
-
]
|
|
2013
|
+
const args = ["curate", query, "--limit", String(limit ?? 6)]
|
|
2014
|
+
if (detail) args.push("--detail", detail)
|
|
1786
2015
|
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_curate" })
|
|
1787
2016
|
},
|
|
1788
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": [
|