akm-cli 0.7.5 → 0.8.0-rc.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.
Files changed (155) hide show
  1. package/.github/CHANGELOG.md +1 -1
  2. package/dist/cli/parse-args.js +86 -0
  3. package/dist/cli.js +1023 -521
  4. package/dist/commands/agent-dispatch.js +107 -0
  5. package/dist/commands/agent-support.js +62 -0
  6. package/dist/commands/config-cli.js +68 -84
  7. package/dist/commands/consolidate.js +812 -0
  8. package/dist/commands/distill-promotion-policy.js +658 -0
  9. package/dist/commands/distill.js +218 -43
  10. package/dist/commands/eval-cases.js +40 -0
  11. package/dist/commands/events.js +2 -23
  12. package/dist/commands/graph.js +222 -0
  13. package/dist/commands/health.js +376 -0
  14. package/dist/commands/help/help-accept.md +9 -0
  15. package/dist/commands/help/help-improve.md +53 -0
  16. package/dist/commands/help/help-proposals.md +15 -0
  17. package/dist/commands/help/help-propose.md +17 -0
  18. package/dist/commands/help/help-reject.md +8 -0
  19. package/dist/commands/history.js +3 -30
  20. package/dist/commands/improve.js +1161 -0
  21. package/dist/commands/info.js +2 -2
  22. package/dist/commands/init.js +2 -2
  23. package/dist/commands/install-audit.js +5 -1
  24. package/dist/commands/installed-stashes.js +118 -138
  25. package/dist/commands/knowledge.js +133 -0
  26. package/dist/commands/lint/agent-linter.js +46 -0
  27. package/dist/commands/lint/base-linter.js +291 -0
  28. package/dist/commands/lint/command-linter.js +46 -0
  29. package/dist/commands/lint/default-linter.js +13 -0
  30. package/dist/commands/lint/index.js +145 -0
  31. package/dist/commands/lint/knowledge-linter.js +13 -0
  32. package/dist/commands/lint/memory-linter.js +58 -0
  33. package/dist/commands/lint/registry.js +33 -0
  34. package/dist/commands/lint/skill-linter.js +42 -0
  35. package/dist/commands/lint/task-linter.js +47 -0
  36. package/dist/commands/lint/types.js +1 -0
  37. package/dist/commands/lint/vault-key-rules.js +67 -0
  38. package/dist/commands/lint/workflow-linter.js +53 -0
  39. package/dist/commands/lint.js +1 -0
  40. package/dist/commands/proposal.js +8 -7
  41. package/dist/commands/propose.js +71 -28
  42. package/dist/commands/reflect.js +135 -35
  43. package/dist/commands/registry-search.js +2 -2
  44. package/dist/commands/remember.js +54 -0
  45. package/dist/commands/schema-repair.js +130 -0
  46. package/dist/commands/search.js +21 -5
  47. package/dist/commands/show.js +125 -20
  48. package/dist/commands/source-add.js +10 -10
  49. package/dist/commands/source-manage.js +11 -19
  50. package/dist/commands/tasks.js +385 -0
  51. package/dist/commands/url-checker.js +39 -0
  52. package/dist/commands/vault.js +168 -77
  53. package/dist/core/action-contributors.js +25 -0
  54. package/dist/core/asset-ref.js +4 -0
  55. package/dist/core/asset-registry.js +4 -16
  56. package/dist/core/asset-spec.js +10 -0
  57. package/dist/core/common.js +100 -0
  58. package/dist/core/concurrent.js +22 -0
  59. package/dist/core/config.js +233 -133
  60. package/dist/core/events.js +73 -126
  61. package/dist/core/frontmatter.js +0 -6
  62. package/dist/core/markdown.js +17 -0
  63. package/dist/core/memory-improve.js +678 -0
  64. package/dist/core/parse.js +155 -0
  65. package/dist/core/paths.js +101 -3
  66. package/dist/core/proposal-validators.js +61 -0
  67. package/dist/core/proposals.js +49 -38
  68. package/dist/core/state-db.js +731 -0
  69. package/dist/core/time.js +51 -0
  70. package/dist/core/warn.js +59 -1
  71. package/dist/indexer/db-search.js +52 -238
  72. package/dist/indexer/db.js +403 -54
  73. package/dist/indexer/ensure-index.js +61 -0
  74. package/dist/indexer/graph-boost.js +247 -94
  75. package/dist/indexer/graph-db.js +201 -0
  76. package/dist/indexer/graph-dedup.js +99 -0
  77. package/dist/indexer/graph-extraction.js +409 -76
  78. package/dist/indexer/index-context.js +10 -0
  79. package/dist/indexer/indexer.js +456 -290
  80. package/dist/indexer/llm-cache.js +47 -0
  81. package/dist/indexer/matchers.js +124 -160
  82. package/dist/indexer/memory-inference.js +63 -29
  83. package/dist/indexer/metadata-contributors.js +26 -0
  84. package/dist/indexer/metadata.js +196 -197
  85. package/dist/indexer/path-resolver.js +89 -0
  86. package/dist/indexer/ranking-contributors.js +204 -0
  87. package/dist/indexer/ranking.js +74 -0
  88. package/dist/indexer/search-hit-enrichers.js +22 -0
  89. package/dist/indexer/search-source.js +24 -9
  90. package/dist/indexer/semantic-status.js +2 -16
  91. package/dist/indexer/walker.js +25 -0
  92. package/dist/integrations/agent/builders.js +109 -0
  93. package/dist/integrations/agent/config.js +203 -3
  94. package/dist/integrations/agent/index.js +5 -2
  95. package/dist/integrations/agent/model-aliases.js +63 -0
  96. package/dist/integrations/agent/profiles.js +67 -5
  97. package/dist/integrations/agent/prompts.js +77 -72
  98. package/dist/integrations/agent/sdk-runner.js +120 -0
  99. package/dist/integrations/agent/spawn.js +93 -22
  100. package/dist/integrations/lockfile.js +10 -18
  101. package/dist/integrations/session-logs/index.js +65 -0
  102. package/dist/integrations/session-logs/providers/claude-code.js +56 -0
  103. package/dist/integrations/session-logs/providers/opencode.js +52 -0
  104. package/dist/integrations/session-logs/types.js +1 -0
  105. package/dist/llm/call-ai.js +74 -0
  106. package/dist/llm/client.js +61 -122
  107. package/dist/llm/feature-gate.js +27 -16
  108. package/dist/llm/graph-extract.js +297 -62
  109. package/dist/llm/memory-infer.js +49 -71
  110. package/dist/llm/metadata-enhance.js +39 -22
  111. package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
  112. package/dist/output/cli-hints-full.md +277 -0
  113. package/dist/output/cli-hints-short.md +65 -0
  114. package/dist/output/cli-hints.js +2 -318
  115. package/dist/output/renderers.js +220 -256
  116. package/dist/output/shapes.js +101 -93
  117. package/dist/output/text.js +256 -17
  118. package/dist/registry/providers/skills-sh.js +61 -49
  119. package/dist/registry/providers/static-index.js +44 -48
  120. package/dist/registry/resolve.js +8 -16
  121. package/dist/setup/setup.js +510 -11
  122. package/dist/sources/provider-factory.js +2 -1
  123. package/dist/sources/providers/filesystem.js +16 -23
  124. package/dist/sources/providers/git.js +4 -5
  125. package/dist/sources/providers/website.js +15 -22
  126. package/dist/sources/website-ingest.js +4 -0
  127. package/dist/tasks/backends/cron.js +200 -0
  128. package/dist/tasks/backends/exec-utils.js +25 -0
  129. package/dist/tasks/backends/index.js +32 -0
  130. package/dist/tasks/backends/launchd-template.xml +19 -0
  131. package/dist/tasks/backends/launchd.js +184 -0
  132. package/dist/tasks/backends/schtasks-template.xml +29 -0
  133. package/dist/tasks/backends/schtasks.js +212 -0
  134. package/dist/tasks/parser.js +198 -0
  135. package/dist/tasks/resolveAkmBin.js +84 -0
  136. package/dist/tasks/runner.js +432 -0
  137. package/dist/tasks/schedule.js +208 -0
  138. package/dist/tasks/schema.js +13 -0
  139. package/dist/tasks/validator.js +59 -0
  140. package/dist/wiki/index-template.md +12 -0
  141. package/dist/wiki/ingest-workflow-template.md +54 -0
  142. package/dist/wiki/log-template.md +8 -0
  143. package/dist/wiki/schema-template.md +61 -0
  144. package/dist/wiki/wiki-templates.js +12 -0
  145. package/dist/wiki/wiki.js +10 -61
  146. package/dist/workflows/authoring.js +5 -25
  147. package/dist/workflows/renderer.js +8 -3
  148. package/dist/workflows/runs.js +59 -91
  149. package/dist/workflows/validator.js +1 -1
  150. package/dist/workflows/workflow-template.md +24 -0
  151. package/docs/README.md +5 -2
  152. package/docs/migration/release-notes/0.7.0.md +1 -1
  153. package/docs/migration/release-notes/0.8.0.md +43 -0
  154. package/package.json +3 -2
  155. package/dist/templates/wiki-templates.js +0 -100
@@ -1,47 +1,47 @@
1
1
  /**
2
- * Append-only events stream — `events.jsonl` (#204).
2
+ * Append-only events stream — backed by state.db (#204, Phase 3).
3
3
  *
4
4
  * Every mutating CLI verb funnels through `appendEvent` so external
5
5
  * observers (sync, replication, audit, dashboards) can react to stash
6
- * changes by tailing a single file. The file is plain newline-delimited
7
- * JSON; each line is a self-contained event envelope.
6
+ * changes. Events are stored in the `events` table in `state.db`
7
+ * (SQLite, WAL mode) instead of a flat `events.jsonl` file.
8
8
  *
9
- * The helper is the only thing in akm that writes to events.jsonl. It
10
- * accepts injectable `now()` and `path` so tests can pin time and use a
9
+ * The helper is the only thing in akm that writes to the events table. It
10
+ * accepts an injectable `dbPath` (via `EventsContext`) so tests can pin a
11
11
  * tmpdir without any global mutation.
12
12
  *
13
- * Format (each line):
13
+ * Format (each EventEnvelope):
14
14
  * { "schemaVersion": 1, "id": <number>, "ts": "<ISO>",
15
15
  * "eventType": "<verb>", "ref"?: "<asset-ref>", ... }
16
16
  *
17
- * - `id` is a monotonic integer per file. We use the file's pre-write
18
- * byte length as a durable cursor for `--since` (stable across processes
19
- * because every appender holds an O_APPEND write). Callers can also pass
20
- * a string ISO timestamp to `--since` and we filter by `ts >= since`.
17
+ * - `id` is a monotonic SQLite AUTOINCREMENT rowid. Callers can persist it
18
+ * as a durable cursor for `--since` resumption (replaces the old byte-offset
19
+ * cursor). The public API still surfaces this as `nextOffset` (an opaque
20
+ * number) for backward compatibility with callers that stored byte-offset
21
+ * cursors.
21
22
  * - `ts` is ISO-8601 (UTC, millisecond precision).
22
- *
23
- * The event `id` is derived at read time (line index) — the file itself
24
- * is the source of truth, so the writer never has to coordinate with a
25
- * counter. Tail consumers can persist a byte offset (durable cursor).
26
23
  */
27
- import fs from "node:fs";
28
24
  import path from "node:path";
29
- import { getCacheDir } from "./paths";
25
+ import { getDataDir } from "./paths";
26
+ import { insertEvent, openStateDatabase, readStateEvents } from "./state-db";
27
+ import { error } from "./warn";
30
28
  /**
31
- * Default events.jsonl location: `<cacheDir>/events.jsonl`.
32
- *
33
- * Env-isolation caveat: `getCacheDir()` reads `XDG_CACHE_HOME` at the time of
34
- * each call. Two cooperating processes (e.g. one writing events, one tailing)
35
- * MUST inherit the same `XDG_CACHE_HOME` or they will read/write different
36
- * `events.jsonl` files. This is the same env-isolation behaviour as the rest
37
- * of akm — config, indexes, and caches all key off XDG paths — so set
38
- * `XDG_CACHE_HOME` consistently across processes that share the events bus.
29
+ * Legacy events.jsonl path — used only by the migration script
30
+ * (`scripts/migrate-storage.ts`) to import existing event history into
31
+ * state.db. No events are written here by akm v0.9+.
39
32
  */
40
33
  export function getEventsPath() {
41
- return path.join(getCacheDir(), "events.jsonl");
34
+ return path.join(getDataDir(), "events.jsonl");
42
35
  }
43
- function resolvePath(ctx) {
44
- return ctx?.filePath ?? getEventsPath();
36
+ /**
37
+ * Resolve the state.db path from context:
38
+ * 1. `ctx.dbPath` — explicit override (test seam)
39
+ * 2. default — `<dataDir>/state.db`
40
+ */
41
+ function resolveDbPath(ctx) {
42
+ if (ctx?.dbPath)
43
+ return ctx.dbPath;
44
+ return path.join(getDataDir(), "state.db");
45
45
  }
46
46
  function resolveNow(ctx) {
47
47
  return ctx?.now ?? Date.now;
@@ -50,129 +50,76 @@ function resolveNow(ctx) {
50
50
  * Append a single event. Best-effort: a write failure is logged once to
51
51
  * stderr but never propagates — observability must not break mutation.
52
52
  *
53
- * The id field is intentionally omitted on write (the line index is the
54
- * id; the reader assigns it). Keeping it off the wire avoids a coordination
55
- * step between concurrent appenders.
53
+ * Events are written exclusively to the `events` table in `state.db`.
56
54
  */
57
55
  export function appendEvent(input, ctx) {
58
- const filePath = resolvePath(ctx);
56
+ const dbPath = resolveDbPath(ctx);
59
57
  const now = resolveNow(ctx);
60
58
  const ts = new Date(now()).toISOString();
61
- const envelope = {
62
- schemaVersion: 1,
63
- ts,
64
- eventType: input.eventType,
65
- ...(input.ref !== undefined ? { ref: input.ref } : {}),
66
- ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
67
- };
68
- const line = `${JSON.stringify(envelope)}\n`;
69
59
  try {
70
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
71
- // O_APPEND guarantees atomic appends ≤ PIPE_BUF (4 KiB on Linux); our
72
- // events are well under that ceiling, so concurrent processes can write
73
- // safely without locking. `appendFileSync` opens with `'a'` which sets
74
- // O_APPEND.
75
- fs.appendFileSync(filePath, line, { encoding: "utf8" });
60
+ const db = openStateDatabase(dbPath);
61
+ try {
62
+ insertEvent(db, {
63
+ eventType: input.eventType,
64
+ ts,
65
+ ref: input.ref,
66
+ metadata: input.metadata,
67
+ });
68
+ }
69
+ finally {
70
+ db.close();
71
+ }
76
72
  }
77
73
  catch (err) {
78
74
  // Best-effort: events stream failures must not break the mutating verb.
79
75
  // Surface once to stderr so operators can diagnose.
80
- const message = err instanceof Error ? err.message : String(err);
81
- process.stderr.write(`akm: events.jsonl append failed (${message})\n`);
76
+ error(`akm: appendEvent failed: ${String(err)}`);
82
77
  }
83
78
  }
84
79
  /**
85
80
  * Read all events matching the filter. Returns a `nextOffset` that callers
86
- * can persist between processes for monotonic resumption — `sinceOffset`
87
- * is the durable cursor referenced in the acceptance criteria.
81
+ * can persist between processes for monotonic resumption.
88
82
  */
89
83
  export function readEvents(options = {}, ctx) {
90
- const filePath = resolvePath(ctx);
91
- if (!fs.existsSync(filePath)) {
92
- return { events: [], nextOffset: 0 };
84
+ const dbPath = resolveDbPath(ctx);
85
+ let db;
86
+ try {
87
+ db = openStateDatabase(dbPath);
93
88
  }
94
- const stat = fs.statSync(filePath);
95
- const startOffset = options.sinceOffset && options.sinceOffset > 0 ? options.sinceOffset : 0;
96
- if (startOffset >= stat.size) {
97
- return { events: [], nextOffset: stat.size };
89
+ catch {
90
+ // DB does not exist yet or cannot be opened return empty result.
91
+ return { events: [], nextOffset: 0 };
98
92
  }
99
- const fd = fs.openSync(filePath, "r");
100
93
  try {
101
- const length = stat.size - startOffset;
102
- const buf = Buffer.alloc(length);
103
- fs.readSync(fd, buf, 0, length, startOffset);
104
- const text = buf.toString("utf8");
105
- const events = parseEventLines(text, options, startOffset);
106
- return { events, nextOffset: stat.size };
94
+ const { events: rawEvents, nextId } = readStateEvents(db, {
95
+ sinceId: options.sinceOffset,
96
+ since: options.since,
97
+ type: options.type,
98
+ ref: options.ref,
99
+ });
100
+ // Apply tag filters in application code (same as the old JSONL implementation).
101
+ const events = rawEvents.filter((envelope) => {
102
+ const tags = envelope.metadata?.tags ?? [];
103
+ if (options.excludeTags?.some((t) => tags.includes(t)))
104
+ return false;
105
+ if (options.includeTags && !options.includeTags.every((t) => tags.includes(t)))
106
+ return false;
107
+ return true;
108
+ });
109
+ return { events, nextOffset: nextId };
107
110
  }
108
111
  finally {
109
- fs.closeSync(fd);
112
+ db.close();
110
113
  }
111
114
  }
112
- function parseEventLines(text, options, startOffset) {
113
- // Each line that ends with \n is a complete event. A trailing partial
114
- // line (no terminating \n) is ignored — the next read will pick it up
115
- // once it is fully written.
116
- const out = [];
117
- let lineStart = 0;
118
- // The envelope id is the 1-based line index across the whole file. We
119
- // approximate that here as the line index from the start of the read
120
- // window plus a synthetic offset — for callers using `--since`, the
121
- // absolute id is less useful than the byte cursor anyway. To keep ids
122
- // monotonic across reads we use absolute byte position as a stable
123
- // surrogate identifier.
124
- for (let i = 0; i < text.length; i += 1) {
125
- if (text.charCodeAt(i) !== 10 /* \n */)
126
- continue;
127
- const line = text.slice(lineStart, i);
128
- const absStart = startOffset + lineStart;
129
- lineStart = i + 1;
130
- if (!line.trim())
131
- continue;
132
- let parsed;
133
- try {
134
- parsed = JSON.parse(line);
135
- }
136
- catch {
137
- // Skip malformed lines — better than crashing the read pipeline.
138
- continue;
139
- }
140
- const envelope = {
141
- schemaVersion: 1,
142
- id: absStart,
143
- ts: typeof parsed.ts === "string" ? parsed.ts : "",
144
- eventType: typeof parsed.eventType === "string" ? parsed.eventType : "unknown",
145
- ...(typeof parsed.ref === "string" ? { ref: parsed.ref } : {}),
146
- ...(parsed.metadata !== undefined ? { metadata: parsed.metadata } : {}),
147
- };
148
- if (!matchesFilter(envelope, options))
149
- continue;
150
- out.push(envelope);
151
- }
152
- return out;
153
- }
154
- function matchesFilter(envelope, options) {
155
- if (options.type && envelope.eventType !== options.type)
156
- return false;
157
- if (options.ref && envelope.ref !== options.ref)
158
- return false;
159
- if (options.since && envelope.ts && envelope.ts < options.since)
160
- return false;
161
- const tags = envelope.metadata?.tags ?? [];
162
- if (options.excludeTags?.some((t) => tags.includes(t)))
163
- return false;
164
- if (options.includeTags && !options.includeTags.every((t) => tags.includes(t)))
165
- return false;
166
- return true;
167
- }
168
115
  /**
169
- * Follow events.jsonl. Polls at `intervalMs` (default 75ms) and emits
170
- * every new event to `onEvent`. Resolves when `signal` aborts, when
116
+ * Follow the events table in state.db. Polls at `intervalMs` (default 75ms)
117
+ * and emits every new event to `onEvent`. Resolves when `signal` aborts, when
171
118
  * `maxEvents` events have been observed, or when `maxDurationMs` elapses.
172
119
  *
173
- * The polling cursor is byte-offset based, so concurrent writers cannot
174
- * cause skips: between two reads we always pick up everything appended
175
- * since the last `nextOffset`.
120
+ * The polling cursor is a monotonic SQLite rowid so concurrent writers cannot
121
+ * cause skips: between two reads we always pick up everything inserted since
122
+ * the last `nextOffset`.
176
123
  */
177
124
  export async function tailEvents(options = {}, ctx) {
178
125
  const intervalMs = options.intervalMs ?? 75;
@@ -223,7 +170,7 @@ export async function tailEvents(options = {}, ctx) {
223
170
  cursor = result.nextOffset;
224
171
  for (const event of result.events) {
225
172
  // Apply --since filter inside the polling loop too — the cursor is
226
- // byte-offset so it can hand us events the user filtered out.
173
+ // rowid-based so it can hand us events the user filtered out.
227
174
  if (options.since && event.ts && event.ts < options.since)
228
175
  continue;
229
176
  collected.push(event);
@@ -150,9 +150,3 @@ export function parseYamlScalar(value) {
150
150
  }
151
151
  return value;
152
152
  }
153
- /**
154
- * Coerce an unknown value to a trimmed string, or return undefined if empty/non-string.
155
- */
156
- export function toStringOrUndefined(value) {
157
- return typeof value === "string" && value.trim() ? value : undefined;
158
- }
@@ -75,3 +75,20 @@ export function formatToc(toc) {
75
75
  parts.push(`\n${toc.totalLines} lines total`);
76
76
  return parts.join("\n");
77
77
  }
78
+ // ── Fence stripping ──────────────────────────────────────────────────────────
79
+ /**
80
+ * Best-effort fence stripping. Strips `<think>` reasoning blocks emitted by
81
+ * local LLMs (e.g. Qwen3) before the content, which otherwise breaks YAML
82
+ * frontmatter detection. Only strips outer triple-fence pairs — leaves inner
83
+ * code blocks intact.
84
+ */
85
+ export function stripMarkdownFences(raw) {
86
+ const stripped = raw
87
+ .trim()
88
+ .replace(/<think>[\s\S]*?<\/think>/gi, "")
89
+ .trim();
90
+ const fence = stripped.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/i);
91
+ if (fence)
92
+ return fence[1].trim();
93
+ return stripped;
94
+ }