akm-cli 0.9.0-beta.26 → 0.9.0-beta.27

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.
@@ -43,6 +43,7 @@ const TYPE_HINTS = {
43
43
  script: "script assets are executable text files. Include a shebang and minimal usage comment.",
44
44
  env: "env assets are `.env` files holding a group of related CONFIGURATION for an app/service (KEY=VALUE pairs, `#` comments) — URLs, flags, and any credentials it needs. Values may or may not be sensitive; all are protected (key names discoverable, values stay on disk). Inject with `akm env run env:<name> -- <cmd>` (the safe path — values never reach stdout/your context); do NOT run `akm env export` and read its output, as that prints values. For a single sensitive value used on its own for authentication (token, key, cert) use a `secret` instead. Never echo values back to the user.",
45
45
  wiki: "wiki assets are markdown reference pages with `# Title` and structured headings.",
46
+ fact: "fact assets are durable stash-level facts (personal/team/project details, coding conventions, stash-meta). Frontmatter SHOULD include `description` and a `category` (personal|team|project|convention|meta); set `pinned: true` only for the small always-injected core. Keep each fact short, high-signal, and self-contained — it is durable context, not an episodic note.",
46
47
  };
47
48
  function hintForType(type) {
48
49
  return TYPE_HINTS[type] ?? `assets of type "${type}" — produce sensible markdown with optional frontmatter.`;
@@ -468,6 +468,44 @@ const sessionMdRenderer = {
468
468
  };
469
469
  },
470
470
  };
471
+ // ── 9. fact-md ───────────────────────────────────────────────────────────────
472
+ /**
473
+ * Renderer for the `fact` asset type. A fact is durable stash-level semantic
474
+ * knowledge (personal/team/project details, coding conventions, stash-meta).
475
+ * It carries `category` (personal|team|project|convention|meta) and an
476
+ * optional `pinned` flag marking it as part of the always-injected core. The
477
+ * renderer surfaces a one-liner (category + pinned marker) so an agent can tell
478
+ * at a glance what kind of fact it is and whether it is core context.
479
+ */
480
+ const factMdRenderer = {
481
+ name: "fact-md",
482
+ buildShowResponse(ctx) {
483
+ const name = deriveName(ctx);
484
+ const parsed = parseFrontmatter(ctx.content());
485
+ const fm = parsed.data;
486
+ const category = asNonEmptyString(fm.category);
487
+ const description = asNonEmptyString(fm.description);
488
+ const pinned = fm.pinned === true;
489
+ const headerParts = [
490
+ category ? `category: ${category}` : undefined,
491
+ pinned ? "pinned (core context)" : undefined,
492
+ ].filter((p) => !!p);
493
+ const action = [
494
+ "Durable stash fact — apply it as background context.",
495
+ headerParts.length > 0 ? headerParts.join(" ") : undefined,
496
+ ]
497
+ .filter((p) => !!p)
498
+ .join("\n");
499
+ return {
500
+ type: "fact",
501
+ name,
502
+ path: ctx.absPath,
503
+ action,
504
+ description,
505
+ content: parsed.content,
506
+ };
507
+ },
508
+ };
471
509
  function applySessionMetadata(entry, ctx) {
472
510
  try {
473
511
  const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
@@ -501,6 +539,34 @@ function applyTocMetadata(entry, ctx) {
501
539
  // Non-fatal: skip TOC if file can't be read
502
540
  }
503
541
  }
542
+ /**
543
+ * Fact metadata: surface `category` and the `pinned` core marker as tags +
544
+ * search hints (no dedicated DB columns — same encoding pattern as session /
545
+ * task). `pinned` is mirrored to both a `pinned` tag and a `pinned` search
546
+ * hint so the ranking contributor can detect it and queries can target it.
547
+ */
548
+ function applyFactMetadata(entry, ctx) {
549
+ try {
550
+ const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
551
+ const tags = new Set([...(entry.tags ?? []), "fact"]);
552
+ const hints = new Set(entry.searchHints ?? []);
553
+ const category = asNonEmptyString(fm.category);
554
+ if (category) {
555
+ tags.add(category);
556
+ hints.add(`category:${category}`);
557
+ }
558
+ if (fm.pinned === true) {
559
+ tags.add("pinned");
560
+ hints.add("pinned");
561
+ }
562
+ entry.tags = Array.from(tags).filter(Boolean);
563
+ if (hints.size > 0)
564
+ entry.searchHints = Array.from(hints).filter(Boolean);
565
+ }
566
+ catch {
567
+ // Non-fatal: skip metadata extraction on parse error
568
+ }
569
+ }
504
570
  /**
505
571
  * Parse frontmatter, apply description (if not already set) and merge tags
506
572
  * into `entry`. Returns the raw frontmatter data object so callers can access
@@ -660,6 +726,11 @@ registerMetadataContributor({
660
726
  appliesTo: ({ rendererName }) => rendererName === "session-md",
661
727
  contribute: (entry, ctx) => applySessionMetadata(entry, ctx.renderContext),
662
728
  });
729
+ registerMetadataContributor({
730
+ name: "fact-md-metadata",
731
+ appliesTo: ({ rendererName }) => rendererName === "fact-md",
732
+ contribute: (entry, ctx) => applyFactMetadata(entry, ctx.renderContext),
733
+ });
663
734
  // ── Registration ─────────────────────────────────────────────────────────────
664
735
  /** All built-in renderers. */
665
736
  const builtinRenderers = [
@@ -676,6 +747,7 @@ const builtinRenderers = [
676
747
  secretFileRenderer,
677
748
  taskMdRenderer,
678
749
  sessionMdRenderer,
750
+ factMdRenderer,
679
751
  ];
680
752
  /**
681
753
  * Register all built-in renderers with the file-context registry.
@@ -687,4 +759,4 @@ export function registerBuiltinRenderers() {
687
759
  }
688
760
  }
689
761
  // ── Named exports for testing ────────────────────────────────────────────────
690
- export { agentMdRenderer, commandMdRenderer, envFileRenderer, INTERPRETER_MAP, knowledgeMdRenderer, lessonMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, secretFileRenderer, skillMdRenderer, wikiMdRenderer, workflowMdRenderer, };
762
+ export { agentMdRenderer, commandMdRenderer, envFileRenderer, factMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, lessonMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, secretFileRenderer, skillMdRenderer, wikiMdRenderer, workflowMdRenderer, };
@@ -8240,6 +8240,25 @@ function applyTocMetadata(entry, ctx) {
8240
8240
  entry.toc = toc.headings;
8241
8241
  } catch {}
8242
8242
  }
8243
+ function applyFactMetadata(entry, ctx) {
8244
+ try {
8245
+ const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
8246
+ const tags = new Set([...entry.tags ?? [], "fact"]);
8247
+ const hints = new Set(entry.searchHints ?? []);
8248
+ const category = asNonEmptyString(fm.category);
8249
+ if (category) {
8250
+ tags.add(category);
8251
+ hints.add(`category:${category}`);
8252
+ }
8253
+ if (fm.pinned === true) {
8254
+ tags.add("pinned");
8255
+ hints.add("pinned");
8256
+ }
8257
+ entry.tags = Array.from(tags).filter(Boolean);
8258
+ if (hints.size > 0)
8259
+ entry.searchHints = Array.from(hints).filter(Boolean);
8260
+ } catch {}
8261
+ }
8243
8262
  function applyFrontmatterDescriptionAndTags(entry, ctx) {
8244
8263
  const parsed = parseFrontmatter(ctx.content());
8245
8264
  const fm = parsed.data;
@@ -8386,6 +8405,11 @@ var init_renderers = __esm(() => {
8386
8405
  appliesTo: ({ rendererName }) => rendererName === "session-md",
8387
8406
  contribute: (entry, ctx) => applySessionMetadata(entry, ctx.renderContext)
8388
8407
  });
8408
+ registerMetadataContributor({
8409
+ name: "fact-md-metadata",
8410
+ appliesTo: ({ rendererName }) => rendererName === "fact-md",
8411
+ contribute: (entry, ctx) => applyFactMetadata(entry, ctx.renderContext)
8412
+ });
8389
8413
  });
8390
8414
 
8391
8415
  // src/core/asset/asset-registry.ts
@@ -8520,6 +8544,12 @@ var init_asset_spec = __esm(() => {
8520
8544
  ...markdownSpec,
8521
8545
  rendererName: "session-md",
8522
8546
  actionBuilder: (ref) => `akm show ${ref} -> read the session summary; follow the \`access\` frontmatter to open the raw log at \`log_path\``
8547
+ },
8548
+ fact: {
8549
+ stashDir: "facts",
8550
+ ...markdownSpec,
8551
+ rendererName: "fact-md",
8552
+ actionBuilder: (ref) => `akm show ${ref} -> read the stash fact and apply it as durable context`
8523
8553
  }
8524
8554
  };
8525
8555
  TYPE_DIRS = Object.fromEntries(Object.entries(ASSET_SPECS_INTERNAL).map(([type, spec]) => [type, spec.stashDir]));
@@ -15822,6 +15852,7 @@ var init_config_schema = __esm(() => {
15822
15852
  requirePlannedRefs: exports_external.boolean().optional(),
15823
15853
  dueDays: exports_external.number().int().min(0).optional(),
15824
15854
  maxPerRun: positiveInt.optional(),
15855
+ topN: positiveInt.optional(),
15825
15856
  minPendingCount: exports_external.number().int().min(0).optional(),
15826
15857
  minNewSessions: exports_external.number().int().min(0).optional(),
15827
15858
  maxSessionsPerRun: exports_external.number().int().min(0).optional(),
@@ -15920,6 +15951,7 @@ var init_config_schema = __esm(() => {
15920
15951
  processes: ImproveProfileProcessesSchema.optional(),
15921
15952
  autoAccept: nonNegativeNumber.optional(),
15922
15953
  limit: positiveInt.optional(),
15954
+ maxCycles: positiveInt.optional(),
15923
15955
  symmetricValence: exports_external.boolean().optional(),
15924
15956
  sync: exports_external.object({
15925
15957
  enabled: exports_external.boolean().optional(),
@@ -16046,7 +16078,8 @@ var init_config_schema = __esm(() => {
16046
16078
  "workflow",
16047
16079
  "lesson",
16048
16080
  "task",
16049
- "wiki"
16081
+ "wiki",
16082
+ "fact"
16050
16083
  ];
16051
16084
  INDEX_PASS_PROVIDER_KEYS = new Set([
16052
16085
  "endpoint",
@@ -16062,7 +16095,8 @@ var init_config_schema = __esm(() => {
16062
16095
  "llm",
16063
16096
  "graphExtractionBatchSize",
16064
16097
  "graphExtractionIncludeTypes",
16065
- "memoryInferenceBatchSize"
16098
+ "memoryInferenceBatchSize",
16099
+ "lazyGraphExtraction"
16066
16100
  ]);
16067
16101
  IndexPassConfigSchema = exports_external.preprocess((raw, ctx) => {
16068
16102
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
@@ -16080,7 +16114,7 @@ var init_config_schema = __esm(() => {
16080
16114
  if (!INDEX_PASS_KNOWN_KEYS.has(key)) {
16081
16115
  ctx.addIssue({
16082
16116
  code: exports_external.ZodIssueCode.custom,
16083
- message: `Unknown key \`${[...ctx.path ?? [], key].join(".")}\`. Per-pass entries support \`llm\` ` + "(boolean opt-out), `graphExtractionBatchSize`, `graphExtractionIncludeTypes`, and " + "`memoryInferenceBatchSize`."
16117
+ message: `Unknown key \`${[...ctx.path ?? [], key].join(".")}\`. Per-pass entries support \`llm\` ` + "(boolean opt-out), `graphExtractionBatchSize`, `graphExtractionIncludeTypes`, " + "`memoryInferenceBatchSize`, and `lazyGraphExtraction`."
16084
16118
  });
16085
16119
  return raw;
16086
16120
  }
@@ -16097,7 +16131,8 @@ var init_config_schema = __esm(() => {
16097
16131
  llm: exports_external.boolean().optional(),
16098
16132
  graphExtractionBatchSize: positiveInt.optional(),
16099
16133
  graphExtractionIncludeTypes: exports_external.array(exports_external.enum(GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED)).nonempty().optional(),
16100
- memoryInferenceBatchSize: positiveInt.optional()
16134
+ memoryInferenceBatchSize: positiveInt.optional(),
16135
+ lazyGraphExtraction: exports_external.boolean().optional()
16101
16136
  }).passthrough());
16102
16137
  MetadataEnhanceSchema = exports_external.object({ enabled: exports_external.boolean().optional() }).strict();
16103
16138
  StalenessDetectionSchema = exports_external.object({
@@ -16968,6 +17003,22 @@ function ensureSchema(db, embeddingDim, options) {
16968
17003
  FOREIGN KEY (stash_root, file_path, body_hash)
16969
17004
  REFERENCES graph_files(stash_root, file_path, body_hash) ON DELETE CASCADE
16970
17005
  );
17006
+
17007
+ -- #624-P3: lazy graph-extraction queue. Standalone table (NO FK to
17008
+ -- graph_files \u2014 a queued file by definition has no graph row yet).
17009
+ -- Idempotent on (stash_root, file_path); drained highest-priority-first.
17010
+ -- CREATE TABLE IF NOT EXISTS is the forward migration (no DB_VERSION bump).
17011
+ CREATE TABLE IF NOT EXISTS graph_extraction_queue (
17012
+ stash_root TEXT NOT NULL,
17013
+ file_path TEXT NOT NULL,
17014
+ body_hash TEXT NOT NULL,
17015
+ queued_at TEXT NOT NULL DEFAULT (datetime('now')),
17016
+ priority INTEGER NOT NULL DEFAULT 0,
17017
+ PRIMARY KEY (stash_root, file_path)
17018
+ );
17019
+
17020
+ CREATE INDEX IF NOT EXISTS idx_graph_extraction_queue_drain
17021
+ ON graph_extraction_queue(stash_root, priority DESC, queued_at);
16971
17022
  `);
16972
17023
  migrateGraphDataFromLegacy(db);
16973
17024
  db.exec(`
@@ -17035,6 +17086,7 @@ function handleVersionUpgrade(db) {
17035
17086
  db.exec("DROP TABLE IF EXISTS index_dir_state");
17036
17087
  db.exec("DROP TABLE IF EXISTS llm_enrichment_cache");
17037
17088
  db.exec("DROP INDEX IF EXISTS idx_llm_cache_updated");
17089
+ db.exec("DROP TABLE IF EXISTS graph_extraction_queue");
17038
17090
  db.exec("DROP TABLE IF EXISTS graph_file_relations");
17039
17091
  db.exec("DROP TABLE IF EXISTS graph_file_entities");
17040
17092
  db.exec("DROP TABLE IF EXISTS graph_files");
@@ -18058,6 +18110,8 @@ __export(exports_graph_db, {
18058
18110
  loadGraphFilesOnly: () => loadGraphFilesOnly,
18059
18111
  loadGraphEntitiesByPath: () => loadGraphEntitiesByPath,
18060
18112
  hasGraphData: () => hasGraphData,
18113
+ enqueueGraphExtraction: () => enqueueGraphExtraction,
18114
+ drainExtractionQueue: () => drainExtractionQueue,
18061
18115
  deleteStoredGraph: () => deleteStoredGraph
18062
18116
  });
18063
18117
  import fs13 from "fs";
@@ -18187,6 +18241,36 @@ function hasGraphData(db, stashRoot, filePath) {
18187
18241
  return false;
18188
18242
  }
18189
18243
  }
18244
+ function enqueueGraphExtraction(db, stashRoot, filePath, bodyHash, priority = 0) {
18245
+ try {
18246
+ db.prepare(`INSERT INTO graph_extraction_queue (stash_root, file_path, body_hash, priority)
18247
+ VALUES (?, ?, ?, ?)
18248
+ ON CONFLICT(stash_root, file_path) DO UPDATE SET
18249
+ body_hash = excluded.body_hash,
18250
+ priority = MAX(graph_extraction_queue.priority, excluded.priority),
18251
+ queued_at = datetime('now')`).run(stashRoot, filePath, bodyHash, priority);
18252
+ } catch (err) {
18253
+ rethrowIfTestIsolationError(err);
18254
+ }
18255
+ }
18256
+ function drainExtractionQueue(db, stashRoot, limit) {
18257
+ try {
18258
+ return db.transaction(() => {
18259
+ const rows = db.prepare(`SELECT file_path, body_hash, priority
18260
+ FROM graph_extraction_queue
18261
+ WHERE stash_root = ?
18262
+ ORDER BY priority DESC, queued_at ASC
18263
+ LIMIT ?`).all(stashRoot, limit);
18264
+ const del = db.prepare("DELETE FROM graph_extraction_queue WHERE stash_root = ? AND file_path = ?");
18265
+ for (const row of rows)
18266
+ del.run(stashRoot, row.file_path);
18267
+ return rows.map((row) => ({ filePath: row.file_path, bodyHash: row.body_hash, priority: row.priority }));
18268
+ })();
18269
+ } catch (err) {
18270
+ rethrowIfTestIsolationError(err);
18271
+ return [];
18272
+ }
18273
+ }
18190
18274
  function loadGraphMetaOnly(stashPath, db) {
18191
18275
  return loadStoredGraphMeta(stashPath, db);
18192
18276
  }
@@ -8126,6 +8126,25 @@ function applyTocMetadata(entry, ctx) {
8126
8126
  entry.toc = toc.headings;
8127
8127
  } catch {}
8128
8128
  }
8129
+ function applyFactMetadata(entry, ctx) {
8130
+ try {
8131
+ const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
8132
+ const tags = new Set([...entry.tags ?? [], "fact"]);
8133
+ const hints = new Set(entry.searchHints ?? []);
8134
+ const category = asNonEmptyString(fm.category);
8135
+ if (category) {
8136
+ tags.add(category);
8137
+ hints.add(`category:${category}`);
8138
+ }
8139
+ if (fm.pinned === true) {
8140
+ tags.add("pinned");
8141
+ hints.add("pinned");
8142
+ }
8143
+ entry.tags = Array.from(tags).filter(Boolean);
8144
+ if (hints.size > 0)
8145
+ entry.searchHints = Array.from(hints).filter(Boolean);
8146
+ } catch {}
8147
+ }
8129
8148
  function applyFrontmatterDescriptionAndTags(entry, ctx) {
8130
8149
  const parsed = parseFrontmatter(ctx.content());
8131
8150
  const fm = parsed.data;
@@ -8272,6 +8291,11 @@ var init_renderers = __esm(() => {
8272
8291
  appliesTo: ({ rendererName }) => rendererName === "session-md",
8273
8292
  contribute: (entry, ctx) => applySessionMetadata(entry, ctx.renderContext)
8274
8293
  });
8294
+ registerMetadataContributor({
8295
+ name: "fact-md-metadata",
8296
+ appliesTo: ({ rendererName }) => rendererName === "fact-md",
8297
+ contribute: (entry, ctx) => applyFactMetadata(entry, ctx.renderContext)
8298
+ });
8275
8299
  });
8276
8300
 
8277
8301
  // src/core/asset/asset-registry.ts
@@ -8406,6 +8430,12 @@ var init_asset_spec = __esm(() => {
8406
8430
  ...markdownSpec,
8407
8431
  rendererName: "session-md",
8408
8432
  actionBuilder: (ref) => `akm show ${ref} -> read the session summary; follow the \`access\` frontmatter to open the raw log at \`log_path\``
8433
+ },
8434
+ fact: {
8435
+ stashDir: "facts",
8436
+ ...markdownSpec,
8437
+ rendererName: "fact-md",
8438
+ actionBuilder: (ref) => `akm show ${ref} -> read the stash fact and apply it as durable context`
8409
8439
  }
8410
8440
  };
8411
8441
  TYPE_DIRS = Object.fromEntries(Object.entries(ASSET_SPECS_INTERNAL).map(([type, spec]) => [type, spec.stashDir]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.0-beta.26",
3
+ "version": "0.9.0-beta.27",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [
@@ -56,12 +56,13 @@
56
56
  "check:fast": "bun run lint && bunx tsc --noEmit && bun run test:unit",
57
57
  "check:changed": "bun test tests/output-baseline.test.ts tests/integration/e2e.test.ts tests/stash-search.test.ts && bun run lint && bunx tsc --noEmit",
58
58
  "sweep:tmp": "bun scripts/sweep-test-tmp.ts",
59
- "test": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
60
- "test:unit": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
61
- "test:integration": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows",
59
+ "test": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-1} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
60
+ "test:unit": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-1} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
61
+ "test:integration": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-1} --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows",
62
+ "test:unit:shard": "bun run sweep:tmp && bun test --parallel=1 --timeout=30000 ./tests --path-ignore-patterns=tests/integration --shard=${SHARD:?set SHARD=k/N}",
63
+ "test:integration:shard": "bun run sweep:tmp && bun test --parallel=1 --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows --shard=${SHARD:?set SHARD=k/N}",
62
64
  "test:node-smoke": "bun scripts/node-smoke.ts",
63
65
  "test:node-compat": "AKM_NODE_COMPAT_TESTS=1 bun test --timeout=120000 tests/integration/node-compat.test.ts",
64
- "test:sharded": "bun test ./tests --shard=1/4 & bun test ./tests --shard=2/4 & bun test ./tests --shard=3/4 & bun test ./tests --shard=4/4 & wait",
65
66
  "test:time": "bun scripts/test-timing-report.ts",
66
67
  "lint:isolation": "bun scripts/lint-tests-isolation.ts",
67
68
  "lint:devto-posts": "bun scripts/lint-devto-posts.ts",