skillwiki 0.9.7 → 0.9.9

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/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-TPS5XD2J.js";
15
15
 
16
16
  // src/cli.ts
17
- import { join as join48 } from "path";
17
+ import { join as join49 } from "path";
18
18
  import { Command as Command2 } from "commander";
19
19
 
20
20
  // ../shared/src/exit-codes.ts
@@ -2360,9 +2360,9 @@ async function runStale(input) {
2360
2360
  if (input.project && !project.includes(input.project)) continue;
2361
2361
  let inferred = false;
2362
2362
  if (input.forceScan && !kind) {
2363
- const basename2 = t.relPath.split("/").pop();
2364
- if (!LOOP_CYCLE_PATTERN.test(basename2)) {
2365
- const m = basename2.match(KIND_FROM_FILENAME);
2363
+ const basename3 = t.relPath.split("/").pop();
2364
+ if (!LOOP_CYCLE_PATTERN.test(basename3)) {
2365
+ const m = basename3.match(KIND_FROM_FILENAME);
2366
2366
  if (m) {
2367
2367
  kind = m[1];
2368
2368
  inferred = true;
@@ -3499,6 +3499,7 @@ function buildCliSurface() {
3499
3499
  program2.command("seed").option("--wiki <name>");
3500
3500
  program2.command("observe").requiredOption("--text <text>").option("--kind <kind>").option("--project <slug>").option("--wiki <name>");
3501
3501
  program2.command("session-brief").option("--project <slug>").option("--write").option("--wiki <name>");
3502
+ program2.command("memory");
3502
3503
  program2.command("ingest").requiredOption("--vault <path>").requiredOption("--type <type>").requiredOption("--title <title>").option("--tags <csv>").option("--provenance <provenance>").option("--dry-run");
3503
3504
  program2.command("fleet");
3504
3505
  const graphCmd = program2.commands.find((c) => c.name() === "graph");
@@ -3524,6 +3525,11 @@ function buildCliSurface() {
3524
3525
  const backupCmd2 = program2.commands.find((c) => c.name() === "backup");
3525
3526
  backupCmd2.command("sync").option("--dry-run").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--prune").option("--wiki <name>");
3526
3527
  backupCmd2.command("restore").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--target <dir>").option("--wiki <name>");
3528
+ const memoryCmd2 = program2.commands.find((c) => c.name() === "memory");
3529
+ memoryCmd2.command("topics").option("--project <slug>").option("--limit <n>").option("--wiki <name>");
3530
+ memoryCmd2.command("index").requiredOption("--project <slug>").option("--wiki <name>");
3531
+ memoryCmd2.command("recall").requiredOption("--project <slug>").requiredOption("--topic <slug>").option("--limit <n>").option("--wiki <name>");
3532
+ memoryCmd2.command("import").requiredOption("--from <path>").requiredOption("--project <slug>").option("--dry-run").option("--apply").option("--max-bytes <n>").option("--wiki <name>");
3527
3533
  const fleetCmd2 = program2.commands.find((c) => c.name() === "fleet");
3528
3534
  fleetCmd2.command("validate");
3529
3535
  fleetCmd2.command("context").option("--file <path>").option("--host-id <id>");
@@ -6381,6 +6387,8 @@ function vaultSyncChecks(input) {
6381
6387
  const requiredExcludes = [
6382
6388
  "remotely-save/data.json",
6383
6389
  ".skillwiki/sync.lock",
6390
+ ".skillwiki/memory/",
6391
+ ".skillwiki/memory-topics.json",
6384
6392
  ".claude/settings.local.json"
6385
6393
  ];
6386
6394
  const missing = requiredExcludes.filter((ex) => !content.includes(ex));
@@ -6762,7 +6770,13 @@ function runVaultSyncHealth(home, syncMode) {
6762
6770
  checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
6763
6771
  } else {
6764
6772
  const content = readFileSync8(filterPath, "utf8");
6765
- const missing = ["remotely-save/data.json", ".skillwiki/sync.lock", ".claude/settings.local.json"].filter((item) => !content.includes(item));
6773
+ const missing = [
6774
+ "remotely-save/data.json",
6775
+ ".skillwiki/sync.lock",
6776
+ ".skillwiki/memory/",
6777
+ ".skillwiki/memory-topics.json",
6778
+ ".claude/settings.local.json"
6779
+ ].filter((item) => !content.includes(item));
6766
6780
  checks.push(missing.length > 0 ? { id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "warn", detail: `Missing excludes: ${missing.join(", ")}` } : { id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "pass", detail: "Required excludes present" });
6767
6781
  }
6768
6782
  checks.push({ id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "pass", detail: "Not a snapshotter host \u2014 check skipped" });
@@ -8329,6 +8343,7 @@ async function runSessionBrief(input) {
8329
8343
  const workItems = await loadWorkItems(scan.data.workItems);
8330
8344
  const digests = await loadTrendDigests(scan.data.typedKnowledge);
8331
8345
  const healthWarnings = await loadHealthWarnings(input.vault);
8346
+ const memoryTopics = project ? await loadMemoryTopics(input.vault, project) : [];
8332
8347
  const latestLogs = newest(transcripts.filter((t) => t.kind === "session-log"), 3);
8333
8348
  const unclaimedCaptures = newest(transcripts.filter((t) => {
8334
8349
  if (t.kind !== "task" && t.kind !== "bug") return false;
@@ -8348,6 +8363,7 @@ async function runSessionBrief(input) {
8348
8363
  activeWork,
8349
8364
  latestDigest,
8350
8365
  projectLogs,
8366
+ memoryTopics,
8351
8367
  healthWarnings
8352
8368
  }), MAX_WORDS);
8353
8369
  let filesWritten = [];
@@ -8359,7 +8375,8 @@ async function runSessionBrief(input) {
8359
8375
  brief,
8360
8376
  generatedAt,
8361
8377
  today,
8362
- wordCount: countWords(brief)
8378
+ wordCount: countWords(brief),
8379
+ memoryTopics
8363
8380
  });
8364
8381
  filesWritten = writeResult.filesWritten;
8365
8382
  indexUpdated = writeResult.indexUpdated;
@@ -8376,6 +8393,7 @@ async function runSessionBrief(input) {
8376
8393
  index_updated: indexUpdated,
8377
8394
  log_updated: logUpdated,
8378
8395
  generated_at: generatedAt,
8396
+ memory_topics: memoryTopics,
8379
8397
  humanHint
8380
8398
  })
8381
8399
  };
@@ -8488,6 +8506,7 @@ function renderBrief(input) {
8488
8506
  appendSection(lines, "Unclaimed Captures", input.unclaimedCaptures, "No unclaimed task or bug captures found.");
8489
8507
  appendSection(lines, "Recent Session Logs", input.project ? input.projectLogs : input.latestLogs, "No recent session logs found.");
8490
8508
  appendSection(lines, "Latest Agent Memory Trends", input.latestDigest, "No agent memory trends digest found.");
8509
+ appendMemoryTopicsSection(lines, input.project, input.memoryTopics);
8491
8510
  appendTextSection(lines, "Health Warnings", input.healthWarnings, "No high-level health warnings found.");
8492
8511
  lines.push(
8493
8512
  "## Suggested Commands",
@@ -8499,6 +8518,15 @@ function renderBrief(input) {
8499
8518
  );
8500
8519
  return lines.join("\n").trimEnd() + "\n";
8501
8520
  }
8521
+ function appendMemoryTopicsSection(lines, project, topics) {
8522
+ if (!project || topics.length === 0) return;
8523
+ lines.push("## Memory Topics", "");
8524
+ for (const topic of topics.slice(0, 5)) {
8525
+ const sourceCount = topic.paths.length === 1 ? "1 source" : `${topic.paths.length} sources`;
8526
+ lines.push(`- ${topic.updated} ${topic.name} (${sourceCount}) \u2014 ${topic.summary}; recall: \`skillwiki memory recall --project ${project} --topic ${topic.name}\``);
8527
+ }
8528
+ lines.push("");
8529
+ }
8502
8530
  function appendSection(lines, title, items, empty) {
8503
8531
  lines.push(`## ${title}`, "");
8504
8532
  if (items.length === 0) {
@@ -8537,6 +8565,23 @@ async function loadHealthWarnings(vault) {
8537
8565
  return [];
8538
8566
  }
8539
8567
  }
8568
+ async function loadMemoryTopics(vault, project) {
8569
+ const text = await readIfExists2(join38(vault, ".skillwiki", "memory", project, "topics.json"));
8570
+ if (!text) return [];
8571
+ try {
8572
+ const parsed = JSON.parse(text);
8573
+ if (!Array.isArray(parsed.topics)) return [];
8574
+ return parsed.topics.filter((topic) => typeof topic === "object" && topic !== null && !Array.isArray(topic)).map((topic) => ({
8575
+ name: stringField(topic.name),
8576
+ summary: stringField(topic.summary),
8577
+ project: stringField(topic.project) || void 0,
8578
+ updated: stringField(topic.updated),
8579
+ paths: Array.isArray(topic.paths) ? topic.paths.filter((path) => typeof path === "string") : []
8580
+ })).filter((topic) => topic.name && topic.summary && topic.updated && topic.paths.length > 0).sort((a, b) => b.updated.localeCompare(a.updated) || a.name.localeCompare(b.name)).slice(0, 5);
8581
+ } catch {
8582
+ return [];
8583
+ }
8584
+ }
8540
8585
  async function writeBriefArtifacts(vault, input) {
8541
8586
  const metaPath = join38(vault, "meta", "latest-session-brief.md");
8542
8587
  const cacheMdPath = join38(vault, ".skillwiki", "session-brief.md");
@@ -8553,7 +8598,8 @@ async function writeBriefArtifacts(vault, input) {
8553
8598
  project: input.project,
8554
8599
  brief: input.brief,
8555
8600
  word_count: input.wordCount,
8556
- generated_at: input.generatedAt
8601
+ generated_at: input.generatedAt,
8602
+ memory_topics: input.memoryTopics
8557
8603
  }, null, 2)}
8558
8604
  `, "utf8");
8559
8605
  const indexUpdated = await ensureIndexEntry(vault);
@@ -8589,7 +8635,7 @@ function renderCommittedBrief(input) {
8589
8635
  `created: ${input.today}`,
8590
8636
  `updated: ${input.today}`,
8591
8637
  "type: meta",
8592
- "tags: [generated, session-brief]",
8638
+ "tags: [meta, session-brief]",
8593
8639
  "confidence: high",
8594
8640
  "generated_by: skillwiki session-brief",
8595
8641
  `generated_at: ${input.generatedAt}`,
@@ -8672,10 +8718,553 @@ function dateFromPath(path) {
8672
8718
  return path.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
8673
8719
  }
8674
8720
 
8675
- // src/commands/ingest.ts
8676
- import { readFile as readFile26, writeFile as writeFile15, mkdir as mkdir14 } from "fs/promises";
8677
- import { join as join39 } from "path";
8721
+ // src/commands/memory.ts
8678
8722
  import { createHash as createHash8 } from "crypto";
8723
+ import { mkdir as mkdir14, readFile as readFile26, readdir as readdir8, stat as stat9, writeFile as writeFile15 } from "fs/promises";
8724
+ import { basename as basename2, extname, join as join39, relative as relative4, sep as sep4 } from "path";
8725
+ async function runMemoryTopics(input) {
8726
+ const scan = await scanVault(input.vault);
8727
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8728
+ const cacheText = await readMemoryCache(input.vault, input.project);
8729
+ if (!cacheText) {
8730
+ return {
8731
+ exitCode: ExitCode.OK,
8732
+ result: ok({
8733
+ cache_present: false,
8734
+ topics: [],
8735
+ files_written: [],
8736
+ humanHint: "no memory topics cache found"
8737
+ })
8738
+ };
8739
+ }
8740
+ let parsed;
8741
+ try {
8742
+ parsed = JSON.parse(cacheText);
8743
+ } catch (e) {
8744
+ return {
8745
+ exitCode: ExitCode.WRITE_FAILED,
8746
+ result: err("WRITE_FAILED", {
8747
+ path: ".skillwiki/memory-topics.json",
8748
+ message: `invalid memory topics cache: ${String(e)}`
8749
+ })
8750
+ };
8751
+ }
8752
+ const limit = normalizeLimit(input.limit);
8753
+ const topics = normalizeTopics(parsed.topics).filter((topic) => !input.project || topic.project === input.project).sort(compareTopics).slice(0, limit);
8754
+ return {
8755
+ exitCode: ExitCode.OK,
8756
+ result: ok({
8757
+ generated_at: stringField2(parsed.generated_at) || void 0,
8758
+ cache_present: true,
8759
+ topics,
8760
+ files_written: [],
8761
+ humanHint: renderHumanHint(topics)
8762
+ })
8763
+ };
8764
+ }
8765
+ async function runMemoryIndex(input) {
8766
+ const scan = await scanVault(input.vault);
8767
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8768
+ const warnings = [];
8769
+ const sourcePages = dedupePages(scan.data.allMarkdown);
8770
+ const sources = [];
8771
+ for (const page of sourcePages) {
8772
+ const source = await readMemorySource(page, input.project, warnings);
8773
+ if (source) sources.push(source);
8774
+ }
8775
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
8776
+ const topics = buildTopics(input.project, sources);
8777
+ const relCachePath = memoryCacheRelPath(input.project);
8778
+ const absCachePath = join39(input.vault, relCachePath);
8779
+ await mkdir14(join39(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
8780
+ await writeFile15(absCachePath, `${JSON.stringify({
8781
+ generated_at: generatedAt,
8782
+ project: input.project,
8783
+ topics,
8784
+ sources,
8785
+ warnings
8786
+ }, null, 2)}
8787
+ `, "utf8");
8788
+ return {
8789
+ exitCode: ExitCode.OK,
8790
+ result: ok({
8791
+ project: input.project,
8792
+ generated_at: generatedAt,
8793
+ topic_count: topics.length,
8794
+ source_count: sources.length,
8795
+ topics,
8796
+ files_written: [relCachePath],
8797
+ warnings,
8798
+ humanHint: `indexed ${sources.length} memory sources into ${topics.length} topics for ${input.project}`
8799
+ })
8800
+ };
8801
+ }
8802
+ async function runMemoryRecall(input) {
8803
+ const scan = await scanVault(input.vault);
8804
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8805
+ const cacheText = await readMemoryCache(input.vault, input.project);
8806
+ if (!cacheText) {
8807
+ return {
8808
+ exitCode: ExitCode.OK,
8809
+ result: ok({
8810
+ project: input.project,
8811
+ topic: input.topic,
8812
+ sources: [],
8813
+ humanHint: `no memory topics cache found for project ${input.project}`
8814
+ })
8815
+ };
8816
+ }
8817
+ let parsed;
8818
+ try {
8819
+ parsed = JSON.parse(cacheText);
8820
+ } catch (e) {
8821
+ return {
8822
+ exitCode: ExitCode.WRITE_FAILED,
8823
+ result: err("WRITE_FAILED", {
8824
+ path: memoryCacheRelPath(input.project),
8825
+ message: `invalid memory topics cache: ${String(e)}`
8826
+ })
8827
+ };
8828
+ }
8829
+ const limit = normalizeLimit(input.limit);
8830
+ const sources = normalizeSources(parsed.sources).filter((source) => source.topics.includes(input.topic)).filter((source) => source.memory_privacy !== "secret-blocked").sort(compareSources).slice(0, limit);
8831
+ return {
8832
+ exitCode: ExitCode.OK,
8833
+ result: ok({
8834
+ project: input.project,
8835
+ topic: input.topic,
8836
+ sources,
8837
+ humanHint: renderRecallHint(input.topic, sources)
8838
+ })
8839
+ };
8840
+ }
8841
+ async function runMemoryImport(input) {
8842
+ const scan = await scanVault(input.vault);
8843
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8844
+ const maxBytes = input.maxBytes ?? 2e5;
8845
+ let files;
8846
+ try {
8847
+ files = await collectImportFiles(input.from);
8848
+ } catch (e) {
8849
+ return {
8850
+ exitCode: ExitCode.WRITE_FAILED,
8851
+ result: err("WRITE_FAILED", { path: input.from, message: String(e) })
8852
+ };
8853
+ }
8854
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
8855
+ const today = generatedAt.slice(0, 10);
8856
+ const entries = [];
8857
+ const filesWritten = [];
8858
+ for (const file of files) {
8859
+ let entry;
8860
+ try {
8861
+ entry = await buildImportEntry(file, input.from, input.project, today, maxBytes);
8862
+ if (input.apply && entry.status === "ready" && entry.proposed_path) {
8863
+ const written2 = await writeImportCapture(input.vault, entry, today);
8864
+ entry.status = "written";
8865
+ entry.written_path = written2.relPath;
8866
+ entry.validation = written2.validation;
8867
+ filesWritten.push(written2.relPath);
8868
+ }
8869
+ } catch (e) {
8870
+ return {
8871
+ exitCode: ExitCode.WRITE_FAILED,
8872
+ result: err("WRITE_FAILED", { path: file, message: String(e) })
8873
+ };
8874
+ }
8875
+ entries.push(entry);
8876
+ }
8877
+ const manifest = {
8878
+ project: input.project,
8879
+ generated_at: generatedAt,
8880
+ source_root: input.from,
8881
+ entries
8882
+ };
8883
+ const ready = entries.filter((entry) => entry.status === "ready").length;
8884
+ const written = entries.filter((entry) => entry.status === "written").length;
8885
+ const rejected = entries.filter((entry) => entry.status === "rejected").length;
8886
+ return {
8887
+ exitCode: ExitCode.OK,
8888
+ result: ok({
8889
+ applied: !!input.apply,
8890
+ manifest,
8891
+ files_written: filesWritten,
8892
+ humanHint: input.apply ? `memory import applied: ${written} written, ${rejected} rejected` : `memory import dry-run: ${ready} ready, ${rejected} rejected`
8893
+ })
8894
+ };
8895
+ }
8896
+ async function readIfExists3(path) {
8897
+ try {
8898
+ return await readFile26(path, "utf8");
8899
+ } catch {
8900
+ return "";
8901
+ }
8902
+ }
8903
+ async function collectImportFiles(source) {
8904
+ const st = await stat9(source);
8905
+ if (st.isFile()) return isImportCandidate(source) ? [source] : [];
8906
+ const files = [];
8907
+ await walkImportFiles(source, files);
8908
+ return files.sort((a, b) => a.localeCompare(b));
8909
+ }
8910
+ async function walkImportFiles(dir, out) {
8911
+ const entries = await readdir8(dir, { withFileTypes: true });
8912
+ for (const entry of entries) {
8913
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
8914
+ const path = join39(dir, entry.name);
8915
+ if (entry.isDirectory()) {
8916
+ await walkImportFiles(path, out);
8917
+ } else if (entry.isFile() && isImportCandidate(path)) {
8918
+ out.push(path);
8919
+ }
8920
+ }
8921
+ }
8922
+ function isImportCandidate(path) {
8923
+ const ext = extname(path).toLowerCase();
8924
+ return ext === ".md" || ext === ".txt";
8925
+ }
8926
+ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
8927
+ const st = await stat9(file);
8928
+ const sourceKind = classifyImportSource(file);
8929
+ const hash = createHash8("sha256").update(await readFile26(file)).digest("hex");
8930
+ const baseEntry = {
8931
+ source_path: file,
8932
+ source_kind: sourceKind,
8933
+ sha256: hash,
8934
+ memory_kind: defaultImportKind(sourceKind),
8935
+ memory_privacy: "local",
8936
+ redaction_count: 0
8937
+ };
8938
+ if (st.size > maxBytes) {
8939
+ return {
8940
+ ...baseEntry,
8941
+ status: "rejected",
8942
+ reason: "oversized_source"
8943
+ };
8944
+ }
8945
+ if (sourceKind === "codex-rule" || sourceKind === "agents-policy") {
8946
+ return {
8947
+ ...baseEntry,
8948
+ status: "rejected",
8949
+ reason: "policy_source_not_imported"
8950
+ };
8951
+ }
8952
+ const text = await readFile26(file, "utf8");
8953
+ const extracted = extractImportText(text, sourceKind);
8954
+ if (!extracted) {
8955
+ return {
8956
+ ...baseEntry,
8957
+ status: "rejected",
8958
+ reason: "policy_source_not_imported"
8959
+ };
8960
+ }
8961
+ const redacted = redactSensitiveContent(extracted, { file });
8962
+ const privacy = redacted.findings.length > 0 ? "sensitive" : "local";
8963
+ const sourceSlug = slugify3(basename2(file, extname(file)));
8964
+ const relSource = relative4(sourceRoot, file).split(sep4).join("/");
8965
+ const entry = {
8966
+ ...baseEntry,
8967
+ status: "ready",
8968
+ memory_privacy: privacy,
8969
+ redaction_count: redacted.findings.length,
8970
+ proposed_path: `raw/transcripts/${today}-memory-import-${sourceSlug}.md`,
8971
+ source_path: relSource || file
8972
+ };
8973
+ Object.defineProperty(entry, "__content", { value: redacted.text, enumerable: false });
8974
+ Object.defineProperty(entry, "__project", { value: project, enumerable: false });
8975
+ return entry;
8976
+ }
8977
+ async function writeImportCapture(vault, entry, today) {
8978
+ const content = hiddenString(entry, "__content");
8979
+ const project = hiddenString(entry, "__project");
8980
+ const relPath = await availableImportPath(vault, entry.proposed_path);
8981
+ const absPath = join39(vault, relPath);
8982
+ await mkdir14(join39(vault, "raw", "transcripts"), { recursive: true });
8983
+ await writeFile15(absPath, renderImportCapture(entry, content, project, today), "utf8");
8984
+ const validation = await runValidate({ file: absPath });
8985
+ return {
8986
+ relPath,
8987
+ validation: {
8988
+ valid: validation.exitCode === ExitCode.OK,
8989
+ exit_code: validation.exitCode
8990
+ }
8991
+ };
8992
+ }
8993
+ async function availableImportPath(vault, proposed) {
8994
+ const ext = extname(proposed);
8995
+ const stem = proposed.slice(0, -ext.length);
8996
+ let candidate = proposed;
8997
+ let i = 2;
8998
+ while (await readIfExists3(join39(vault, candidate))) {
8999
+ candidate = `${stem}-${i}${ext}`;
9000
+ i++;
9001
+ }
9002
+ return candidate;
9003
+ }
9004
+ function renderImportCapture(entry, content, project, today) {
9005
+ return [
9006
+ "---",
9007
+ "source_url:",
9008
+ `ingested: ${today}`,
9009
+ "kind: note",
9010
+ `project: "[[${project}]]"`,
9011
+ `memory_kind: ${entry.memory_kind}`,
9012
+ "memory_topics: [imported-memory]",
9013
+ "memory_scope: project",
9014
+ "memory_policy: historical",
9015
+ `memory_privacy: ${entry.memory_privacy}`,
9016
+ "memory_status: active",
9017
+ `source_agent: skillwiki memory import`,
9018
+ `source_hash: ${entry.sha256}`,
9019
+ `source_paths: ["${entry.source_path.replaceAll('"', '\\"')}"]`,
9020
+ "---",
9021
+ "",
9022
+ `# Imported Memory: ${basename2(entry.source_path, extname(entry.source_path))}`,
9023
+ "",
9024
+ `Source kind: ${entry.source_kind}`,
9025
+ "",
9026
+ "Promotion guidance: review this raw capture before promoting it to compound or typed knowledge.",
9027
+ "",
9028
+ content.trimEnd(),
9029
+ ""
9030
+ ].join("\n");
9031
+ }
9032
+ function hiddenString(entry, key) {
9033
+ return entry[key] ?? "";
9034
+ }
9035
+ function classifyImportSource(file) {
9036
+ const rel = file.split(sep4).join("/");
9037
+ const name = basename2(file);
9038
+ if (rel.includes("/.codex/memories/")) return "codex-memory";
9039
+ if (rel.includes("/.codex/rules/")) return "codex-rule";
9040
+ if (rel.includes("/.claude/") && name === "napkin.md") return "napkin";
9041
+ if (rel.includes("/.memsearch/memory/")) return "memsearch";
9042
+ if (name === "AGENTS.md") return "agents-policy";
9043
+ if (name === "CLAUDE.md") return "claude-auto-memory";
9044
+ return "generic";
9045
+ }
9046
+ function defaultImportKind(sourceKind) {
9047
+ switch (sourceKind) {
9048
+ case "codex-memory":
9049
+ case "claude-auto-memory":
9050
+ return "preference";
9051
+ case "napkin":
9052
+ return "correction";
9053
+ case "memsearch":
9054
+ return "handoff";
9055
+ default:
9056
+ return "handoff";
9057
+ }
9058
+ }
9059
+ function extractImportText(text, sourceKind) {
9060
+ if (sourceKind !== "claude-auto-memory") return text;
9061
+ const match = text.match(/<!--\s*AUTO-MEMORY:START\s*-->([\s\S]*?)<!--\s*AUTO-MEMORY:END\s*-->/i);
9062
+ return match?.[1]?.trim() ?? "";
9063
+ }
9064
+ function normalizeTopics(value) {
9065
+ if (!Array.isArray(value)) return [];
9066
+ const topics = [];
9067
+ for (const item of value) {
9068
+ if (!isRecord(item)) continue;
9069
+ const name = stringField2(item.name);
9070
+ const summary = stringField2(item.summary);
9071
+ const updated = stringField2(item.updated);
9072
+ const paths = stringArray(item.paths);
9073
+ if (!name || !summary || !updated || paths.length === 0) continue;
9074
+ const project = stringField2(item.project);
9075
+ topics.push({
9076
+ name,
9077
+ summary,
9078
+ ...project ? { project } : {},
9079
+ updated,
9080
+ paths
9081
+ });
9082
+ }
9083
+ return topics;
9084
+ }
9085
+ function normalizeSources(value) {
9086
+ if (!Array.isArray(value)) return [];
9087
+ const sources = [];
9088
+ for (const item of value) {
9089
+ if (!isRecord(item)) continue;
9090
+ const path = stringField2(item.path);
9091
+ const title = stringField2(item.title);
9092
+ const summary = stringField2(item.summary);
9093
+ const updated = stringField2(item.updated);
9094
+ const hash = stringField2(item.hash);
9095
+ const topics = stringArray(item.topics);
9096
+ if (!path || !title || !summary || !updated || !hash || topics.length === 0) continue;
9097
+ const project = stringField2(item.project);
9098
+ sources.push({
9099
+ path,
9100
+ title,
9101
+ summary,
9102
+ updated,
9103
+ hash,
9104
+ topics,
9105
+ ...project ? { project } : {},
9106
+ ...stringField2(item.memory_kind) ? { memory_kind: stringField2(item.memory_kind) } : {},
9107
+ ...stringField2(item.memory_scope) ? { memory_scope: stringField2(item.memory_scope) } : {},
9108
+ ...stringField2(item.memory_policy) ? { memory_policy: stringField2(item.memory_policy) } : {},
9109
+ memory_privacy: stringField2(item.memory_privacy) || "local",
9110
+ memory_status: stringField2(item.memory_status) || "active"
9111
+ });
9112
+ }
9113
+ return sources;
9114
+ }
9115
+ function normalizeLimit(value) {
9116
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return 10;
9117
+ return Math.floor(value);
9118
+ }
9119
+ function compareTopics(a, b) {
9120
+ return b.updated.localeCompare(a.updated) || a.name.localeCompare(b.name);
9121
+ }
9122
+ function compareSources(a, b) {
9123
+ return b.updated.localeCompare(a.updated) || a.path.localeCompare(b.path);
9124
+ }
9125
+ function renderHumanHint(topics) {
9126
+ if (topics.length === 0) return "no memory topics found";
9127
+ return topics.map((topic) => {
9128
+ const project = topic.project ? ` [${topic.project}]` : "";
9129
+ return `${topic.updated} ${topic.name}${project} \u2014 ${topic.summary} (${topic.paths.join(", ")})`;
9130
+ }).join("\n");
9131
+ }
9132
+ function renderRecallHint(topic, sources) {
9133
+ if (sources.length === 0) return `no memory sources found for topic ${topic}`;
9134
+ return sources.map((source) => `${source.updated} ${source.title} (${source.path}) \u2014 ${source.summary}`).join("\n");
9135
+ }
9136
+ function memoryCacheRelPath(project) {
9137
+ return `.skillwiki/memory/${project}/topics.json`;
9138
+ }
9139
+ async function readMemoryCache(vault, project) {
9140
+ if (project) {
9141
+ const projectCache = await readIfExists3(join39(vault, memoryCacheRelPath(project)));
9142
+ if (projectCache) return projectCache;
9143
+ }
9144
+ return readIfExists3(join39(vault, ".skillwiki", "memory-topics.json"));
9145
+ }
9146
+ function dedupePages(pages) {
9147
+ const seen = /* @__PURE__ */ new Set();
9148
+ const out = [];
9149
+ for (const page of pages) {
9150
+ if (seen.has(page.relPath)) continue;
9151
+ seen.add(page.relPath);
9152
+ out.push(page);
9153
+ }
9154
+ return out;
9155
+ }
9156
+ async function readMemorySource(page, project, warnings) {
9157
+ const text = await readPage(page);
9158
+ const fm = extractFrontmatter(text);
9159
+ if (!fm.ok) return null;
9160
+ const topics = normalizeMemoryTopics(fm.data.memory_topics, page.relPath, warnings);
9161
+ if (topics.length === 0) return null;
9162
+ const projects = memoryProjects(page.relPath, fm.data);
9163
+ const scope = stringField2(fm.data.memory_scope);
9164
+ if (!projects.includes(project) && scope !== "global" && scope !== "cross-agent") return null;
9165
+ const privacy = stringField2(fm.data.memory_privacy) || "local";
9166
+ if (privacy === "secret-blocked") {
9167
+ warnings.push(`${page.relPath}: skipped secret-blocked memory`);
9168
+ return null;
9169
+ }
9170
+ const status = stringField2(fm.data.memory_status) || "active";
9171
+ if (status === "archived" || status === "rejected") return null;
9172
+ const split = splitFrontmatter(text);
9173
+ const body = split.ok ? split.data.body : text;
9174
+ const updated = stringField2(fm.data.last_seen) || stringField2(fm.data.updated) || stringField2(fm.data.ingested) || dateFromPath2(page.relPath);
9175
+ const title = stringField2(fm.data.title) || page.relPath.split("/").pop()?.replace(/\.md$/, "") || page.relPath;
9176
+ return {
9177
+ path: page.relPath,
9178
+ title,
9179
+ summary: summarize2(body),
9180
+ updated,
9181
+ hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
9182
+ topics,
9183
+ project,
9184
+ ...stringField2(fm.data.memory_kind) ? { memory_kind: stringField2(fm.data.memory_kind) } : {},
9185
+ ...scope ? { memory_scope: scope } : {},
9186
+ ...stringField2(fm.data.memory_policy) ? { memory_policy: stringField2(fm.data.memory_policy) } : {},
9187
+ memory_privacy: privacy,
9188
+ memory_status: status
9189
+ };
9190
+ }
9191
+ function normalizeMemoryTopics(value, path, warnings) {
9192
+ const rawTopics = typeof value === "string" ? [value] : stringArray(value);
9193
+ const topics = [];
9194
+ for (const topic of rawTopics) {
9195
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(topic)) {
9196
+ warnings.push(`${path}: invalid memory topic slug ${topic}`);
9197
+ continue;
9198
+ }
9199
+ if (!topics.includes(topic)) topics.push(topic);
9200
+ }
9201
+ return topics;
9202
+ }
9203
+ function memoryProjects(path, fm) {
9204
+ const projects = /* @__PURE__ */ new Set();
9205
+ const project = wikilinkSlug2(fm.project);
9206
+ if (project) projects.add(project);
9207
+ if (Array.isArray(fm.provenance_projects)) {
9208
+ for (const value of fm.provenance_projects) {
9209
+ const slug = wikilinkSlug2(value);
9210
+ if (slug) projects.add(slug);
9211
+ }
9212
+ }
9213
+ const pathProject = path.match(/^projects\/([^/]+)\//)?.[1];
9214
+ if (pathProject) projects.add(pathProject);
9215
+ return [...projects];
9216
+ }
9217
+ function buildTopics(project, sources) {
9218
+ const byTopic = /* @__PURE__ */ new Map();
9219
+ for (const source of sources) {
9220
+ for (const topic of source.topics) {
9221
+ const list = byTopic.get(topic) ?? [];
9222
+ list.push(source);
9223
+ byTopic.set(topic, list);
9224
+ }
9225
+ }
9226
+ return [...byTopic.entries()].map(([name, topicSources]) => {
9227
+ const sorted = [...topicSources].sort(compareSources);
9228
+ return {
9229
+ name,
9230
+ project,
9231
+ summary: sorted[0]?.summary ?? "",
9232
+ updated: sorted[0]?.updated ?? "",
9233
+ paths: sorted.map((source) => source.path)
9234
+ };
9235
+ }).sort((a, b) => a.name.localeCompare(b.name));
9236
+ }
9237
+ function isRecord(value) {
9238
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9239
+ }
9240
+ function stringField2(value) {
9241
+ return typeof value === "string" ? value : "";
9242
+ }
9243
+ function stringArray(value) {
9244
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
9245
+ }
9246
+ function wikilinkSlug2(value) {
9247
+ if (typeof value !== "string") return void 0;
9248
+ const match = value.match(/^\[\[([^\]]+)\]\]$/);
9249
+ return match?.[1] ?? value;
9250
+ }
9251
+ function summarize2(body) {
9252
+ const lines = body.split(/\r?\n/).map((line) => line.replace(/^#+\s*/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("```"));
9253
+ const summary = lines.join(" ").replace(/\s+/g, " ").trim();
9254
+ return summary.length > 180 ? `${summary.slice(0, 177).trimEnd()}...` : summary;
9255
+ }
9256
+ function dateFromPath2(path) {
9257
+ return path.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
9258
+ }
9259
+ function slugify3(value) {
9260
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
9261
+ return slug || "memory";
9262
+ }
9263
+
9264
+ // src/commands/ingest.ts
9265
+ import { readFile as readFile27, writeFile as writeFile16, mkdir as mkdir15 } from "fs/promises";
9266
+ import { join as join40 } from "path";
9267
+ import { createHash as createHash9 } from "crypto";
8679
9268
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
8680
9269
  var TYPE_DIR = {
8681
9270
  entity: "entities",
@@ -8684,7 +9273,7 @@ var TYPE_DIR = {
8684
9273
  query: "queries"
8685
9274
  };
8686
9275
  var ALLOWED_PROVENANCE = /* @__PURE__ */ new Set(["research", "project"]);
8687
- function slugify3(text) {
9276
+ function slugify4(text) {
8688
9277
  return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "untitled";
8689
9278
  }
8690
9279
  function isUrl(source) {
@@ -8833,7 +9422,7 @@ async function runIngest(input) {
8833
9422
  sourceContent = fetchResult.data.body;
8834
9423
  } else {
8835
9424
  try {
8836
- sourceContent = await readFile26(input.source, "utf8");
9425
+ sourceContent = await readFile27(input.source, "utf8");
8837
9426
  } catch {
8838
9427
  return {
8839
9428
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -8851,15 +9440,15 @@ async function runIngest(input) {
8851
9440
  })
8852
9441
  };
8853
9442
  }
8854
- const sha256 = createHash8("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
9443
+ const sha256 = createHash9("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
8855
9444
  const today = todayIso();
8856
- const slug = slugify3(input.title);
9445
+ const slug = slugify4(input.title);
8857
9446
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
8858
9447
  const rawRelPath = `raw/articles/${slug}.md`;
8859
9448
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
8860
9449
  const typedRelPath = `${typedDir}/${slug}.md`;
8861
- const rawAbsPath = join39(input.vault, rawRelPath);
8862
- const typedAbsPath = join39(input.vault, typedRelPath);
9450
+ const rawAbsPath = join40(input.vault, rawRelPath);
9451
+ const typedAbsPath = join40(input.vault, typedRelPath);
8863
9452
  const identity = assessSourceIdentity({
8864
9453
  rawPath: rawRelPath,
8865
9454
  sourceUrl: sourceUrl ?? void 0,
@@ -8941,8 +9530,8 @@ async function runIngest(input) {
8941
9530
  };
8942
9531
  }
8943
9532
  try {
8944
- await mkdir14(join39(input.vault, "raw", "articles"), { recursive: true });
8945
- await writeFile15(rawAbsPath, rawContent, "utf8");
9533
+ await mkdir15(join40(input.vault, "raw", "articles"), { recursive: true });
9534
+ await writeFile16(rawAbsPath, rawContent, "utf8");
8946
9535
  } catch (e) {
8947
9536
  return {
8948
9537
  exitCode: ExitCode.WRITE_FAILED,
@@ -8950,8 +9539,8 @@ async function runIngest(input) {
8950
9539
  };
8951
9540
  }
8952
9541
  try {
8953
- await mkdir14(join39(input.vault, typedDir), { recursive: true });
8954
- await writeFile15(typedAbsPath, typedContent, "utf8");
9542
+ await mkdir15(join40(input.vault, typedDir), { recursive: true });
9543
+ await writeFile16(typedAbsPath, typedContent, "utf8");
8955
9544
  } catch (e) {
8956
9545
  return {
8957
9546
  exitCode: ExitCode.WRITE_FAILED,
@@ -9130,19 +9719,19 @@ ${body}`;
9130
9719
 
9131
9720
  // src/commands/sync.ts
9132
9721
  import { existsSync as existsSync17 } from "fs";
9133
- import { join as join41 } from "path";
9722
+ import { join as join42 } from "path";
9134
9723
 
9135
9724
  // src/utils/sync-lock.ts
9136
9725
  import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
9137
- import { join as join40 } from "path";
9138
- import { createHash as createHash9 } from "crypto";
9726
+ import { join as join41 } from "path";
9727
+ import { createHash as createHash10 } from "crypto";
9139
9728
  function getSessionId() {
9140
9729
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
9141
9730
  if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
9142
9731
  return process.pid.toString();
9143
9732
  }
9144
9733
  function lockPath(vault) {
9145
- return join40(vault, ".skillwiki", "sync.lock");
9734
+ return join41(vault, ".skillwiki", "sync.lock");
9146
9735
  }
9147
9736
  function readLock(vault) {
9148
9737
  const path = lockPath(vault);
@@ -9161,7 +9750,7 @@ function isStale(lock, now) {
9161
9750
  }
9162
9751
  function acquireLock(vault, opts = {}) {
9163
9752
  const path = lockPath(vault);
9164
- const dir = join40(vault, ".skillwiki");
9753
+ const dir = join41(vault, ".skillwiki");
9165
9754
  if (!existsSync16(dir)) {
9166
9755
  mkdirSync6(dir, { recursive: true });
9167
9756
  }
@@ -9232,11 +9821,22 @@ function releaseLock(vault, opts = {}) {
9232
9821
  }
9233
9822
  }
9234
9823
 
9824
+ // src/utils/vault-git-pathspec.ts
9825
+ var VAULT_GENERATED_COMMIT_PATHS = [
9826
+ ".skillwiki/last-op.json",
9827
+ ".skillwiki/memory",
9828
+ ".skillwiki/memory-topics.json"
9829
+ ];
9830
+ var VAULT_GENERATED_COMMIT_EXCLUDES = [
9831
+ ...VAULT_GENERATED_COMMIT_PATHS.map((path) => `:!${path}`)
9832
+ ];
9833
+ var VAULT_COMMIT_PATHSPEC = [".", ...VAULT_GENERATED_COMMIT_EXCLUDES];
9834
+
9235
9835
  // src/commands/sync.ts
9236
9836
  function runSyncStatus(input) {
9237
9837
  const vault = input.vault;
9238
9838
  const includeStashes = input.includeStashes ?? false;
9239
- if (!existsSync17(join41(vault, ".git"))) {
9839
+ if (!existsSync17(join42(vault, ".git"))) {
9240
9840
  return {
9241
9841
  exitCode: ExitCode.VAULT_PATH_INVALID,
9242
9842
  result: ok({
@@ -9314,7 +9914,7 @@ function runSyncStatus(input) {
9314
9914
  }
9315
9915
  async function runSyncPush(input) {
9316
9916
  const vault = input.vault;
9317
- if (!existsSync17(join41(vault, ".git"))) {
9917
+ if (!existsSync17(join42(vault, ".git"))) {
9318
9918
  return {
9319
9919
  exitCode: ExitCode.VAULT_PATH_INVALID,
9320
9920
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -9357,10 +9957,12 @@ async function runSyncPush(input) {
9357
9957
  };
9358
9958
  }
9359
9959
  try {
9360
- gitStrict(vault, ["add", "-A"]);
9361
- try {
9362
- gitStrict(vault, ["reset", "HEAD", "--", ".skillwiki/last-op.json"]);
9363
- } catch (_e) {
9960
+ gitStrict(vault, ["add", "-A", "--", ...VAULT_COMMIT_PATHSPEC]);
9961
+ for (const generatedPath of VAULT_GENERATED_COMMIT_PATHS) {
9962
+ try {
9963
+ gitStrict(vault, ["reset", "HEAD", "--", generatedPath]);
9964
+ } catch (_e) {
9965
+ }
9364
9966
  }
9365
9967
  } catch (e) {
9366
9968
  return {
@@ -9437,7 +10039,7 @@ function enableGitLongPathsOnWindows(vault) {
9437
10039
  }
9438
10040
  async function runSyncPull(input) {
9439
10041
  const vault = input.vault;
9440
- if (!existsSync17(join41(vault, ".git"))) {
10042
+ if (!existsSync17(join42(vault, ".git"))) {
9441
10043
  return {
9442
10044
  exitCode: ExitCode.VAULT_PATH_INVALID,
9443
10045
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -9678,7 +10280,7 @@ function runSyncUnlock(input) {
9678
10280
 
9679
10281
  // src/commands/backup.ts
9680
10282
  import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
9681
- import { join as join42, relative as relative4, dirname as dirname14 } from "path";
10283
+ import { join as join43, relative as relative5, dirname as dirname14 } from "path";
9682
10284
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
9683
10285
 
9684
10286
  // src/utils/s3-client.ts
@@ -9702,11 +10304,11 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node
9702
10304
  function* walkMarkdown(dir, base) {
9703
10305
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
9704
10306
  if (SKIP_DIRS2.has(entry.name)) continue;
9705
- const full = join42(dir, entry.name);
10307
+ const full = join43(dir, entry.name);
9706
10308
  if (entry.isDirectory()) {
9707
10309
  yield* walkMarkdown(full, base);
9708
10310
  } else if (entry.name.endsWith(".md")) {
9709
- yield relative4(base, full).replace(/\\/g, "/");
10311
+ yield relative5(base, full).replace(/\\/g, "/");
9710
10312
  }
9711
10313
  }
9712
10314
  }
@@ -9725,7 +10327,7 @@ async function runBackupSync(input) {
9725
10327
  let failed = 0;
9726
10328
  const files = [...walkMarkdown(input.vault, input.vault)];
9727
10329
  for (const relPath of files) {
9728
- const absPath = join42(input.vault, relPath);
10330
+ const absPath = join43(input.vault, relPath);
9729
10331
  const localStat = statSync5(absPath);
9730
10332
  let needsUpload = true;
9731
10333
  try {
@@ -9801,7 +10403,7 @@ async function runBackupRestore(input) {
9801
10403
  const objects = list.Contents ?? [];
9802
10404
  for (const obj of objects) {
9803
10405
  if (!obj.Key) continue;
9804
- const localPath = join42(target, obj.Key);
10406
+ const localPath = join43(target, obj.Key);
9805
10407
  try {
9806
10408
  const localStat = statSync5(localPath);
9807
10409
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -9848,8 +10450,8 @@ async function runBackupRestore(input) {
9848
10450
 
9849
10451
  // src/commands/status.ts
9850
10452
  import { existsSync as existsSync18, statSync as statSync6 } from "fs";
9851
- import { readFile as readFile27 } from "fs/promises";
9852
- import { join as join43 } from "path";
10453
+ import { readFile as readFile28 } from "fs/promises";
10454
+ import { join as join44 } from "path";
9853
10455
  async function runStatus(input) {
9854
10456
  if (!existsSync18(input.vault)) {
9855
10457
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
@@ -9876,7 +10478,7 @@ async function runStatus(input) {
9876
10478
  const compound = scan.data.compound.length;
9877
10479
  let schemaVersion = "v1";
9878
10480
  try {
9879
- const schemaContent = await readFile27(join43(input.vault, "SCHEMA.md"), "utf8");
10481
+ const schemaContent = await readFile28(join44(input.vault, "SCHEMA.md"), "utf8");
9880
10482
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
9881
10483
  if (versionMatch) schemaVersion = versionMatch[1];
9882
10484
  } catch {
@@ -9936,8 +10538,8 @@ async function runStatus(input) {
9936
10538
  }
9937
10539
 
9938
10540
  // src/commands/seed.ts
9939
- import { mkdir as mkdir15, writeFile as writeFile16, stat as stat9 } from "fs/promises";
9940
- import { join as join44 } from "path";
10541
+ import { mkdir as mkdir16, writeFile as writeFile17, stat as stat10 } from "fs/promises";
10542
+ import { join as join45 } from "path";
9941
10543
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9942
10544
  var EXAMPLE_PAGES = {
9943
10545
  "entities/example-project.md": `---
@@ -10006,30 +10608,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
10006
10608
  `;
10007
10609
  async function runSeed(input) {
10008
10610
  try {
10009
- await stat9(join44(input.vault, "SCHEMA.md"));
10611
+ await stat10(join45(input.vault, "SCHEMA.md"));
10010
10612
  } catch {
10011
10613
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
10012
10614
  }
10013
10615
  const created = [];
10014
10616
  const skipped = [];
10015
10617
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
10016
- const absPath = join44(input.vault, relPath);
10618
+ const absPath = join45(input.vault, relPath);
10017
10619
  try {
10018
- await stat9(absPath);
10620
+ await stat10(absPath);
10019
10621
  skipped.push(relPath);
10020
10622
  } catch {
10021
- await mkdir15(join44(absPath, ".."), { recursive: true });
10022
- await writeFile16(absPath, content, "utf8");
10623
+ await mkdir16(join45(absPath, ".."), { recursive: true });
10624
+ await writeFile17(absPath, content, "utf8");
10023
10625
  created.push(relPath);
10024
10626
  }
10025
10627
  }
10026
- const rawPath = join44(input.vault, "raw", "articles", "example-source.md");
10628
+ const rawPath = join45(input.vault, "raw", "articles", "example-source.md");
10027
10629
  try {
10028
- await stat9(rawPath);
10630
+ await stat10(rawPath);
10029
10631
  skipped.push("raw/articles/example-source.md");
10030
10632
  } catch {
10031
- await mkdir15(join44(rawPath, ".."), { recursive: true });
10032
- await writeFile16(rawPath, EXAMPLE_RAW, "utf8");
10633
+ await mkdir16(join45(rawPath, ".."), { recursive: true });
10634
+ await writeFile17(rawPath, EXAMPLE_RAW, "utf8");
10033
10635
  created.push("raw/articles/example-source.md");
10034
10636
  }
10035
10637
  if (created.length > 0) {
@@ -10051,9 +10653,9 @@ async function runSeed(input) {
10051
10653
  }
10052
10654
 
10053
10655
  // src/commands/canvas.ts
10054
- import { readFile as readFile28, writeFile as writeFile17 } from "fs/promises";
10656
+ import { readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
10055
10657
  import { existsSync as existsSync19 } from "fs";
10056
- import { join as join45 } from "path";
10658
+ import { join as join46 } from "path";
10057
10659
  var NODE_WIDTH = 240;
10058
10660
  var NODE_HEIGHT = 60;
10059
10661
  var COLUMN_SPACING = 400;
@@ -10131,7 +10733,7 @@ function buildCanvasEdges(adjacency) {
10131
10733
  return edges;
10132
10734
  }
10133
10735
  async function runCanvasGenerate(input) {
10134
- const graphPath = input.graphPath ?? join45(input.vault, ".skillwiki", "graph.json");
10736
+ const graphPath = input.graphPath ?? join46(input.vault, ".skillwiki", "graph.json");
10135
10737
  if (!existsSync19(graphPath)) {
10136
10738
  return {
10137
10739
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -10143,7 +10745,7 @@ async function runCanvasGenerate(input) {
10143
10745
  }
10144
10746
  let raw;
10145
10747
  try {
10146
- raw = await readFile28(graphPath, "utf8");
10748
+ raw = await readFile29(graphPath, "utf8");
10147
10749
  } catch (e) {
10148
10750
  return {
10149
10751
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -10169,9 +10771,9 @@ async function runCanvasGenerate(input) {
10169
10771
  const nodes = buildCanvasNodes(paths);
10170
10772
  const edges = buildCanvasEdges(graph.adjacency);
10171
10773
  const canvas = { nodes, edges };
10172
- const outPath = join45(input.vault, "vault-graph.canvas");
10774
+ const outPath = join46(input.vault, "vault-graph.canvas");
10173
10775
  try {
10174
- await writeFile17(outPath, JSON.stringify(canvas, null, 2));
10776
+ await writeFile18(outPath, JSON.stringify(canvas, null, 2));
10175
10777
  } catch (e) {
10176
10778
  return {
10177
10779
  exitCode: ExitCode.WRITE_FAILED,
@@ -10191,8 +10793,8 @@ written: ${outPath}`
10191
10793
  }
10192
10794
 
10193
10795
  // src/commands/query.ts
10194
- import { readFile as readFile29, stat as stat10 } from "fs/promises";
10195
- import { join as join46 } from "path";
10796
+ import { readFile as readFile30, stat as stat11 } from "fs/promises";
10797
+ import { join as join47 } from "path";
10196
10798
  var W_KEYWORD = 2;
10197
10799
  var W_SOURCE_OVERLAP = 4;
10198
10800
  var W_WIKILINK = 3;
@@ -10313,10 +10915,10 @@ function computeKeywordScore(terms, title, tags, body) {
10313
10915
  return score;
10314
10916
  }
10315
10917
  async function loadOrBuildGraph(vault) {
10316
- const graphPath = join46(vault, ".skillwiki", "graph.json");
10918
+ const graphPath = join47(vault, ".skillwiki", "graph.json");
10317
10919
  let needsBuild = false;
10318
10920
  try {
10319
- const fileStat = await stat10(graphPath);
10921
+ const fileStat = await stat11(graphPath);
10320
10922
  const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
10321
10923
  if (ageHours > 24) needsBuild = true;
10322
10924
  } catch {
@@ -10327,7 +10929,7 @@ async function loadOrBuildGraph(vault) {
10327
10929
  if (buildResult.exitCode !== 0) return null;
10328
10930
  }
10329
10931
  try {
10330
- const raw = await readFile29(graphPath, "utf8");
10932
+ const raw = await readFile30(graphPath, "utf8");
10331
10933
  return JSON.parse(raw);
10332
10934
  } catch {
10333
10935
  return null;
@@ -10336,24 +10938,26 @@ async function loadOrBuildGraph(vault) {
10336
10938
 
10337
10939
  // src/utils/auto-commit.ts
10338
10940
  import { existsSync as existsSync20 } from "fs";
10339
- import { join as join47 } from "path";
10941
+ import { join as join48 } from "path";
10340
10942
  async function postCommit(vault, exitCode) {
10341
10943
  if (exitCode !== 0) return;
10342
10944
  const home = process.env.HOME ?? "";
10343
10945
  const dotenv = await parseDotenvFile(configPath(home));
10344
10946
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
10345
10947
  if (autoCommit === "false") return;
10346
- if (!existsSync20(join47(vault, ".git"))) return;
10948
+ if (!existsSync20(join48(vault, ".git"))) return;
10347
10949
  const lastOps = readLastOp(vault);
10348
10950
  if (lastOps.length === 0) return;
10349
- const porcelain = git(vault, ["status", "--porcelain"]);
10951
+ const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
10350
10952
  if (!porcelain || porcelain.trim().length === 0) return;
10351
10953
  const { gitStrict: gitStrict2 } = await import("./git-M4WGJ5G3.js");
10352
10954
  try {
10353
- gitStrict2(vault, ["add", "-A"]);
10354
- try {
10355
- gitStrict2(vault, ["reset", "HEAD", "--", ".skillwiki/last-op.json"]);
10356
- } catch (_e) {
10955
+ gitStrict2(vault, ["add", "-A", "--", ...VAULT_COMMIT_PATHSPEC]);
10956
+ for (const generatedPath of VAULT_GENERATED_COMMIT_PATHS) {
10957
+ try {
10958
+ gitStrict2(vault, ["reset", "HEAD", "--", generatedPath]);
10959
+ } catch (_e) {
10960
+ }
10357
10961
  }
10358
10962
  } catch (e) {
10359
10963
  process.stderr.write(`auto-commit: git add failed: ${String(e)}
@@ -10394,7 +10998,7 @@ program.command("validate <file>").description("validate vault page frontmatter
10394
10998
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
10395
10999
  });
10396
11000
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
10397
- const out = opts.out ?? join48(vault, ".skillwiki", "graph.json");
11001
+ const out = opts.out ?? join49(vault, ".skillwiki", "graph.json");
10398
11002
  emit(await runGraphBuild({ vault, out }), vault);
10399
11003
  });
10400
11004
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -10763,6 +11367,45 @@ program.command("session-brief [vault]").description("render or refresh the boun
10763
11367
  env: { SKILLWIKI_PROJECT: process.env.SKILLWIKI_PROJECT }
10764
11368
  }), v.vault, { postCommit: !!opts.write });
10765
11369
  });
11370
+ var memoryCmd = program.command("memory").description("inspect derived agent memory caches");
11371
+ memoryCmd.command("topics [vault]").description("list topic-oriented memory from the optional derived cache").option("--project <slug>", "filter topics by project slug").option("--limit <n>", "maximum topics to return", (s) => parseInt(s, 10), 10).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
11372
+ const v = await resolveVaultArg(vault, opts.wiki);
11373
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11374
+ else emit(await runMemoryTopics({
11375
+ vault: v.vault,
11376
+ project: opts.project,
11377
+ limit: opts.limit
11378
+ }), v.vault, { postCommit: false });
11379
+ });
11380
+ memoryCmd.command("index [vault]").description("build the derived project memory topic cache").requiredOption("--project <slug>", "project slug").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
11381
+ const v = await resolveVaultArg(vault, opts.wiki);
11382
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11383
+ else emit(await runMemoryIndex({
11384
+ vault: v.vault,
11385
+ project: opts.project
11386
+ }), v.vault);
11387
+ });
11388
+ memoryCmd.command("recall [vault]").description("recall bounded source summaries for a memory topic").requiredOption("--project <slug>", "project slug").requiredOption("--topic <slug>", "memory topic slug").option("--limit <n>", "maximum sources to return", (s) => parseInt(s, 10), 10).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
11389
+ const v = await resolveVaultArg(vault, opts.wiki);
11390
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11391
+ else emit(await runMemoryRecall({
11392
+ vault: v.vault,
11393
+ project: opts.project,
11394
+ topic: opts.topic,
11395
+ limit: opts.limit
11396
+ }), v.vault, { postCommit: false });
11397
+ });
11398
+ memoryCmd.command("import [vault]").description("preview or apply local memory imports into raw captures").requiredOption("--from <path>", "source file or directory to scan").requiredOption("--project <slug>", "project slug").option("--dry-run", "preview only; do not write captures", false).option("--apply", "write validated raw captures for ready entries", false).option("--max-bytes <n>", "reject source files above this size", (s) => parseInt(s, 10), 2e5).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
11399
+ const v = await resolveVaultArg(vault, opts.wiki);
11400
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11401
+ else emit(await runMemoryImport({
11402
+ vault: v.vault,
11403
+ from: opts.from,
11404
+ project: opts.project,
11405
+ apply: !!opts.apply && !opts.dryRun,
11406
+ maxBytes: opts.maxBytes
11407
+ }), v.vault, { postCommit: !!opts.apply && !opts.dryRun });
11408
+ });
10766
11409
  program.command("ingest <source>").description("ingest a source URL or local file into the vault").requiredOption("--vault <path>", "vault root directory").requiredOption("--type <type>", "typed-knowledge type (entity|concept|comparison|query)").requiredOption("--title <title>", "page title").option("--tags <csv>", "comma-separated tags").option("--provenance <provenance>", "provenance (research|project)").option("--dry-run", "preview without writing files", false).action(async (source, opts) => {
10767
11410
  const tags = typeof opts.tags === "string" ? opts.tags.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : [];
10768
11411
  emit(await runIngest({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 18 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",