akm-cli 0.9.0-beta.44 → 0.9.0-beta.46

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 (80) hide show
  1. package/dist/cli/shared.js +28 -0
  2. package/dist/cli.js +1 -2
  3. package/dist/commands/env/env-cli.js +16 -24
  4. package/dist/commands/env/secret-cli.js +12 -20
  5. package/dist/commands/graph/graph-cli.js +5 -13
  6. package/dist/commands/graph/graph.js +3 -3
  7. package/dist/commands/health.js +5 -0
  8. package/dist/commands/improve/consolidate/chunking.js +141 -0
  9. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  10. package/dist/commands/improve/consolidate/merge.js +145 -0
  11. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  12. package/dist/commands/improve/consolidate/types.js +4 -0
  13. package/dist/commands/improve/consolidate.js +20 -571
  14. package/dist/commands/improve/distill.js +5 -9
  15. package/dist/commands/improve/eligibility.js +434 -0
  16. package/dist/commands/improve/extract-cli.js +9 -1
  17. package/dist/commands/improve/extract.js +5 -19
  18. package/dist/commands/improve/improve-auto-accept.js +4 -8
  19. package/dist/commands/improve/improve-cli.js +35 -60
  20. package/dist/commands/improve/improve-result-file.js +5 -23
  21. package/dist/commands/improve/improve-session.js +58 -0
  22. package/dist/commands/improve/improve.js +107 -3606
  23. package/dist/commands/improve/locks.js +154 -0
  24. package/dist/commands/improve/loop-stages.js +1079 -0
  25. package/dist/commands/improve/preparation.js +1963 -0
  26. package/dist/commands/improve/recombine.js +6 -12
  27. package/dist/commands/improve/reflect.js +29 -34
  28. package/dist/commands/proposal/drain.js +25 -48
  29. package/dist/commands/proposal/proposal-cli.js +21 -31
  30. package/dist/commands/proposal/validators/proposals.js +3 -7
  31. package/dist/commands/read/curate.js +70 -14
  32. package/dist/commands/read/knowledge.js +2 -2
  33. package/dist/commands/sources/self-update.js +2 -2
  34. package/dist/commands/sources/stash-cli.js +9 -37
  35. package/dist/commands/tasks/tasks-cli.js +19 -27
  36. package/dist/commands/wiki-cli.js +21 -35
  37. package/dist/core/config/config.js +18 -2
  38. package/dist/core/events.js +3 -7
  39. package/dist/core/logs-db.js +6 -63
  40. package/dist/core/parse.js +36 -16
  41. package/dist/core/state/migrations.js +714 -0
  42. package/dist/core/state-db.js +28 -779
  43. package/dist/indexer/db/db.js +82 -216
  44. package/dist/indexer/graph/graph-extraction.js +2 -0
  45. package/dist/indexer/indexer.js +11 -112
  46. package/dist/indexer/passes/dir-staleness.js +114 -0
  47. package/dist/indexer/search/search-source.js +10 -24
  48. package/dist/integrations/agent/runner-dispatch.js +59 -0
  49. package/dist/llm/client.js +22 -11
  50. package/dist/llm/graph-extract.js +58 -42
  51. package/dist/llm/memory-infer.js +34 -22
  52. package/dist/llm/metadata-enhance.js +35 -30
  53. package/dist/llm/structured-call.js +49 -0
  54. package/dist/output/shapes/passthrough.js +0 -1
  55. package/dist/registry/providers/skills-sh.js +21 -147
  56. package/dist/registry/providers/static-index.js +15 -157
  57. package/dist/registry/resolve.js +22 -9
  58. package/dist/scripts/migrate-storage.js +892 -1186
  59. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +214 -179
  60. package/dist/setup/setup.js +26 -5
  61. package/dist/sources/providers/filesystem.js +0 -1
  62. package/dist/sources/providers/git-install.js +206 -0
  63. package/dist/sources/providers/git-provider.js +234 -0
  64. package/dist/sources/providers/git-stash.js +248 -0
  65. package/dist/sources/providers/git.js +10 -671
  66. package/dist/sources/providers/npm.js +2 -6
  67. package/dist/sources/providers/sync-from-ref.js +9 -1
  68. package/dist/sources/providers/website.js +2 -3
  69. package/dist/sources/website-ingest.js +51 -9
  70. package/dist/sources/wiki-fetchers/registry.js +53 -0
  71. package/dist/sources/wiki-fetchers/youtube.js +181 -0
  72. package/dist/storage/database.js +45 -10
  73. package/dist/storage/managed-db.js +82 -0
  74. package/dist/storage/repositories/registry-cache.js +92 -0
  75. package/dist/tasks/runner.js +5 -13
  76. package/dist/workflows/runtime/runs.js +1 -117
  77. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  78. package/package.json +5 -5
  79. package/dist/commands/db-cli.js +0 -23
  80. package/dist/indexer/db/db-backup.js +0 -376
@@ -11,9 +11,9 @@
11
11
  * `process.exit(1)` after the wrapper when findings exist.
12
12
  */
13
13
  import { defineCommand } from "citty";
14
- import { getStringArg, hasSubcommand, parsePositiveIntFlag } from "../cli/parse-args.js";
15
- import { defineJsonCommand, output, runWithJsonErrors } from "../cli/shared.js";
16
- import { isHttpUrl, resolveStashDir } from "../core/common.js";
14
+ import { getStringArg, parsePositiveIntFlag } from "../cli/parse-args.js";
15
+ import { defineGroupCommand, defineJsonCommand, output, runWithJsonErrors } from "../cli/shared.js";
16
+ import { resolveStashDir } from "../core/common.js";
17
17
  import { loadConfig, resolveConfiguredSources } from "../core/config/config.js";
18
18
  import { ConfigError, UsageError } from "../core/errors.js";
19
19
  import { getHyphenatedArg, getHyphenatedBoolean } from "../output/context.js";
@@ -168,13 +168,6 @@ const wikiStashCommand = defineJsonCommand({
168
168
  },
169
169
  async run({ args }) {
170
170
  const { stashRaw } = await import("../wiki/wiki.js");
171
- const { content, preferredName } = await (async () => {
172
- if (!isHttpUrl(args.source))
173
- return readKnowledgeInput(args.source);
174
- const { fetchWebsiteMarkdownSnapshot } = await import("../sources/website-ingest.js");
175
- const snapshot = await fetchWebsiteMarkdownSnapshot(args.source);
176
- return { content: snapshot.content, preferredName: args.as ?? snapshot.preferredName };
177
- })();
178
171
  let stashDir;
179
172
  if (args.target) {
180
173
  // Resolve the named source to its filesystem path.
@@ -193,6 +186,7 @@ const wikiStashCommand = defineJsonCommand({
193
186
  else {
194
187
  stashDir = resolveStashDir();
195
188
  }
189
+ const { content, preferredName } = await readKnowledgeInput(args.source, { stashDir });
196
190
  const result = stashRaw({
197
191
  stashDir,
198
192
  wikiName: args.name,
@@ -274,34 +268,26 @@ const wikiIngestCommand = defineJsonCommand({
274
268
  });
275
269
  },
276
270
  });
277
- // Single source of truth: the routing set is derived from the subCommands keys
278
- // (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
279
- const wikiSubCommands = {
280
- create: wikiCreateCommand,
281
- register: wikiRegisterCommand,
282
- list: wikiListCommand,
283
- show: wikiShowCommand,
284
- remove: wikiRemoveCommand,
285
- pages: wikiPagesCommand,
286
- search: wikiSearchCommand,
287
- stash: wikiStashCommand,
288
- lint: wikiLintCommand,
289
- ingest: wikiIngestCommand,
290
- };
291
- const WIKI_SUBCOMMAND_SET = new Set(Object.keys(wikiSubCommands));
292
- export const wikiCommand = defineCommand({
271
+ export const wikiCommand = defineGroupCommand({
293
272
  meta: {
294
273
  name: "wiki",
295
274
  description: "Manage multiple markdown wikis (Karpathy-style). akm surfaces (lifecycle, raw/, lint, index); the agent writes pages.",
296
275
  },
297
- subCommands: wikiSubCommands,
298
- run({ args }) {
299
- return runWithJsonErrors(async () => {
300
- if (hasSubcommand(args, WIKI_SUBCOMMAND_SET))
301
- return;
302
- // Default action: list wikis
303
- const { listWikis } = await import("../wiki/wiki.js");
304
- output("wiki-list", { wikis: listWikis(resolveStashDir()) });
305
- });
276
+ subCommands: {
277
+ create: wikiCreateCommand,
278
+ register: wikiRegisterCommand,
279
+ list: wikiListCommand,
280
+ show: wikiShowCommand,
281
+ remove: wikiRemoveCommand,
282
+ pages: wikiPagesCommand,
283
+ search: wikiSearchCommand,
284
+ stash: wikiStashCommand,
285
+ lint: wikiLintCommand,
286
+ ingest: wikiIngestCommand,
287
+ },
288
+ async defaultRun() {
289
+ // Default action: list wikis
290
+ const { listWikis } = await import("../wiki/wiki.js");
291
+ output("wiki-list", { wikis: listWikis(resolveStashDir()) });
306
292
  },
307
293
  });
@@ -221,7 +221,18 @@ function maybeAutoMigrateConfigFile(configPath, text) {
221
221
  catch {
222
222
  return text; // Malformed JSON — let parseAndValidate surface the error.
223
223
  }
224
- if (compareConfigVersion(obj.configVersion, CURRENT_CONFIG_VERSION) === 1) {
224
+ // Downgrade protection. Skip migration when the on-disk config is NEWER than
225
+ // this binary (=== 1), OR when it carries a configVersion we cannot order
226
+ // against ours (compareConfigVersion returns undefined for an unparseable
227
+ // value — e.g. one written by a newer/foreign akm). Migrating such a config
228
+ // could strip fields a newer binary added, a cross-version data-loss path.
229
+ //
230
+ // A MISSING configVersion (absent → undefined) is a legacy pre-versioning
231
+ // config that MUST still migrate, so the unparseable-skip is gated on the
232
+ // field being PRESENT (`onDiskVersion !== undefined`).
233
+ const onDiskVersion = obj.configVersion;
234
+ const versionOrder = compareConfigVersion(onDiskVersion, CURRENT_CONFIG_VERSION);
235
+ if (versionOrder === 1 || (onDiskVersion !== undefined && versionOrder === undefined)) {
225
236
  return text;
226
237
  }
227
238
  const { changed, result } = migrateConfigShape(obj);
@@ -450,7 +461,12 @@ function applyRuntimeEnvApiKeys(config) {
450
461
  }
451
462
  // LLM profile keys: AKM_LLM_API_KEY for the default profile, then
452
463
  // AKM_PROFILE_<UPPER>_API_KEY for any profile (per-profile wins).
453
- const defaultProfile = next.defaults?.llm;
464
+ // Resolve the default profile the SAME way the rest of the config layer does
465
+ // (resolveDefaultLlmProfileName), so the implicit `profiles.llm.default`
466
+ // fallback is honored. Keying off the raw `defaults.llm` field alone silently
467
+ // dropped AKM_LLM_API_KEY for configs that rely on the implicit default —
468
+ // the same no-op-run class as the 2026-05-23 incident.
469
+ const defaultProfile = resolveDefaultLlmProfileName(next);
454
470
  if (next.profiles?.llm) {
455
471
  const updated = { ...next.profiles.llm };
456
472
  let changed = false;
@@ -27,7 +27,7 @@
27
27
  import path from "node:path";
28
28
  import { rethrowIfTestIsolationError } from "./errors.js";
29
29
  import { getDataDir } from "./paths.js";
30
- import { insertEvent, openStateDatabase, readStateEvents } from "./state-db.js";
30
+ import { insertEvent, openStateDatabase, readStateEvents, withStateDb } from "./state-db.js";
31
31
  import { error } from "./warn.js";
32
32
  /**
33
33
  * Legacy events.jsonl path — used only by the migration script
@@ -82,18 +82,14 @@ export function appendEvent(input, ctx) {
82
82
  // Default path: open, insert, close.
83
83
  const dbPath = resolveDbPath(ctx);
84
84
  try {
85
- const db = openStateDatabase(dbPath);
86
- try {
85
+ withStateDb((db) => {
87
86
  insertEvent(db, {
88
87
  eventType: input.eventType,
89
88
  ts,
90
89
  ref: input.ref,
91
90
  metadata: input.metadata,
92
91
  });
93
- }
94
- finally {
95
- db.close();
96
- }
92
+ }, { path: dbPath });
97
93
  }
98
94
  catch (err) {
99
95
  // Never mask the bun-test isolation guard as a silent "events failed".
@@ -16,8 +16,10 @@
16
16
  * Log lines are high-volume, append-only, and freely purgeable; state.db rows
17
17
  * (events, proposals, task_history) are durable records. Separating them keeps
18
18
  * state.db small and lets log retention be aggressive without touching durable
19
- * state. Cross-db queries (e.g. "failed task_history row its log lines") use
20
- * SQLite ATTACH see {@link attachStateDatabase}.
19
+ * state. Callers that need to correlate a task_history row with its log lines do
20
+ * an application-side join on the {@link buildTaskRunId} key (e.g. `health` via
21
+ * {@link getLoggedRunIds}) — no SQLite ATTACH, so the split survives a future
22
+ * provider change.
21
23
  *
22
24
  * ## run_id
23
25
  *
@@ -41,7 +43,6 @@ import { openDatabase } from "../storage/database.js";
41
43
  import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
42
44
  import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
43
45
  import { getDataDir } from "./paths.js";
44
- import { getStateDbPath } from "./state-db.js";
45
46
  // ── Path helper ──────────────────────────────────────────────────────────────
46
47
  /**
47
48
  * Default path: `<dataDir>/logs.db` — alongside state.db so cooperating
@@ -146,8 +147,8 @@ export function runMigrations(db) {
146
147
  * Encode a task run's identity — the unique `(task_id, started_at)` pair from
147
148
  * state.db `task_history` — as a single run_id string.
148
149
  *
149
- * The format MUST stay in sync with the SQL expression
150
- * `task_id || '@' || started_at` used by {@link queryFailedRunLogLines}.
150
+ * The format MUST stay in sync with the application-side join key that callers
151
+ * build from a `task_history` row's `task_id` and `started_at`.
151
152
  */
152
153
  export function buildTaskRunId(taskId, startedAtIso) {
153
154
  return `${taskId}@${startedAtIso}`;
@@ -230,64 +231,6 @@ export function getLoggedRunIds(db, runIds) {
230
231
  }
231
232
  return out;
232
233
  }
233
- // ── Cross-db: ATTACH state.db ────────────────────────────────────────────────
234
- /**
235
- * ATTACH state.db to an open logs.db handle under the schema name `state`,
236
- * enabling cross-db joins like task_history × task_logs.
237
- *
238
- * The state.db file must already exist (callers always open state.db first in
239
- * practice); attaching a non-existent path would silently create an empty,
240
- * unmigrated database file, so this throws instead.
241
- */
242
- export function attachStateDatabase(db, stateDbPath) {
243
- const resolved = stateDbPath ?? getStateDbPath();
244
- if (!fs.existsSync(resolved)) {
245
- throw new Error(`Cannot ATTACH state.db: file does not exist at ${resolved}`);
246
- }
247
- // prepare().run() rather than db.run(): both drivers support parameterised
248
- // ATTACH through a prepared statement, and no other call site uses db.run().
249
- db.prepare("ATTACH DATABASE ? AS state").run(resolved);
250
- }
251
- /**
252
- * Convenience: open logs.db with state.db attached as `state`. The returned
253
- * handle supports cross-db queries such as {@link queryFailedRunLogLines}.
254
- * Close it like any other handle (DETACH is implicit on close).
255
- */
256
- export function openLogsDatabaseWithState(logsDbPath, stateDbPath) {
257
- const db = openLogsDatabase(logsDbPath);
258
- try {
259
- attachStateDatabase(db, stateDbPath);
260
- }
261
- catch (err) {
262
- db.close();
263
- throw err;
264
- }
265
- return db;
266
- }
267
- /**
268
- * Cross-db join: every log line belonging to a FAILED task_history run whose
269
- * `started_at` is `>= since` (all failed runs when omitted). Requires a handle
270
- * opened via {@link openLogsDatabaseWithState}.
271
- *
272
- * The join key is the run_id encoding documented on {@link buildTaskRunId}:
273
- * `task_logs.run_id = task_history.task_id || '@' || task_history.started_at`.
274
- */
275
- export function queryFailedRunLogLines(db, options = {}) {
276
- const conditions = ["th.status = 'failed'"];
277
- const params = [];
278
- if (options.since) {
279
- conditions.push("th.started_at >= ?");
280
- params.push(options.since);
281
- }
282
- const limit = options.limit !== undefined && options.limit >= 0 ? ` LIMIT ${Math.floor(options.limit)}` : "";
283
- return db
284
- .prepare(`SELECT th.task_id, l.run_id, th.started_at, th.status, l.ts, l.stream, l.level, l.line
285
- FROM state.task_history th
286
- JOIN task_logs l ON l.run_id = th.task_id || '@' || th.started_at
287
- WHERE ${conditions.join(" AND ")}
288
- ORDER BY th.started_at DESC, l.id ASC${limit}`)
289
- .all(...params);
290
- }
291
234
  // ── Retention ────────────────────────────────────────────────────────────────
292
235
  /**
293
236
  * Delete task_logs rows older than `retentionDays` (default: 90). Mirrors
@@ -98,16 +98,24 @@ export function parseJsonResponse(raw) {
98
98
  * balanced `{ }` or `[ ]` structure in the text and attempts to parse that
99
99
  * substring. Returns `undefined` if no valid JSON structure is found.
100
100
  *
101
- * Non-array results are preferred: if a `{…}` object is found first, it is
102
- * returned immediately. Arrays (`[…]`) are captured as a fallback and
103
- * returned only when no object was found.
101
+ * Shape preference is controlled by {@link ParseEmbeddedJsonOptions.expect}:
102
+ * - `"any"` (default): non-array results are preferred a `{…}` object found
103
+ * first is returned immediately; arrays (`[…]`) are a fallback.
104
+ * - `"array"`: only top-level arrays are returned. The direct parse is
105
+ * accepted only if it is an array, and the scanner returns the first
106
+ * balanced `[…]` while skipping `{…}` openers entirely.
104
107
  */
105
- export function parseEmbeddedJsonResponse(raw) {
108
+ export function parseEmbeddedJsonResponse(raw, options) {
109
+ const expectArray = options?.expect === "array";
106
110
  const direct = parseJsonResponse(raw);
107
- if (direct !== undefined)
111
+ if (direct !== undefined && (!expectArray || Array.isArray(direct)))
108
112
  return direct;
109
113
  const text = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
110
114
  let arrayFallback;
115
+ // Scan only *top-level* balanced structures: once a `{…}`/`[…]` is matched we
116
+ // jump `start` past its closing bracket rather than re-scanning its interior.
117
+ // This keeps array mode from salvaging an array *nested inside* a leading
118
+ // object (e.g. the `entities` array of a bare `{entities,relations}` object).
111
119
  for (let start = 0; start < text.length; start++) {
112
120
  const opener = text[start];
113
121
  if (opener !== "{" && opener !== "[")
@@ -116,6 +124,7 @@ export function parseEmbeddedJsonResponse(raw) {
116
124
  let depth = 0;
117
125
  let inString = false;
118
126
  let escaped = false;
127
+ let end = -1;
119
128
  for (let i = start; i < text.length; i++) {
120
129
  const ch = text[i];
121
130
  if (inString) {
@@ -139,20 +148,31 @@ export function parseEmbeddedJsonResponse(raw) {
139
148
  if (ch === closer) {
140
149
  depth -= 1;
141
150
  if (depth === 0) {
142
- try {
143
- const parsed = JSON.parse(text.slice(start, i + 1));
144
- if (!Array.isArray(parsed)) {
145
- return parsed;
146
- }
147
- arrayFallback ??= parsed;
148
- break;
149
- }
150
- catch {
151
- break;
152
- }
151
+ end = i;
152
+ break;
153
153
  }
154
154
  }
155
155
  }
156
+ if (end === -1)
157
+ continue; // never balanced — let the next opener try
158
+ try {
159
+ const parsed = JSON.parse(text.slice(start, end + 1));
160
+ if (Array.isArray(parsed)) {
161
+ // First valid array wins in array mode; in "any" mode it is the
162
+ // fallback returned only if no object is found.
163
+ if (expectArray)
164
+ return parsed;
165
+ arrayFallback ??= parsed;
166
+ }
167
+ else if (!expectArray) {
168
+ return parsed;
169
+ }
170
+ // Skip past this balanced structure so we don't descend into it.
171
+ start = end;
172
+ }
173
+ catch {
174
+ // Malformed candidate — advance one char and try the next opener.
175
+ }
156
176
  }
157
177
  return arrayFallback;
158
178
  }