skillwiki 0.9.6 → 0.9.8

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>");
@@ -8329,6 +8335,7 @@ async function runSessionBrief(input) {
8329
8335
  const workItems = await loadWorkItems(scan.data.workItems);
8330
8336
  const digests = await loadTrendDigests(scan.data.typedKnowledge);
8331
8337
  const healthWarnings = await loadHealthWarnings(input.vault);
8338
+ const memoryTopics = project ? await loadMemoryTopics(input.vault, project) : [];
8332
8339
  const latestLogs = newest(transcripts.filter((t) => t.kind === "session-log"), 3);
8333
8340
  const unclaimedCaptures = newest(transcripts.filter((t) => {
8334
8341
  if (t.kind !== "task" && t.kind !== "bug") return false;
@@ -8348,6 +8355,7 @@ async function runSessionBrief(input) {
8348
8355
  activeWork,
8349
8356
  latestDigest,
8350
8357
  projectLogs,
8358
+ memoryTopics,
8351
8359
  healthWarnings
8352
8360
  }), MAX_WORDS);
8353
8361
  let filesWritten = [];
@@ -8359,7 +8367,8 @@ async function runSessionBrief(input) {
8359
8367
  brief,
8360
8368
  generatedAt,
8361
8369
  today,
8362
- wordCount: countWords(brief)
8370
+ wordCount: countWords(brief),
8371
+ memoryTopics
8363
8372
  });
8364
8373
  filesWritten = writeResult.filesWritten;
8365
8374
  indexUpdated = writeResult.indexUpdated;
@@ -8376,6 +8385,7 @@ async function runSessionBrief(input) {
8376
8385
  index_updated: indexUpdated,
8377
8386
  log_updated: logUpdated,
8378
8387
  generated_at: generatedAt,
8388
+ memory_topics: memoryTopics,
8379
8389
  humanHint
8380
8390
  })
8381
8391
  };
@@ -8488,6 +8498,7 @@ function renderBrief(input) {
8488
8498
  appendSection(lines, "Unclaimed Captures", input.unclaimedCaptures, "No unclaimed task or bug captures found.");
8489
8499
  appendSection(lines, "Recent Session Logs", input.project ? input.projectLogs : input.latestLogs, "No recent session logs found.");
8490
8500
  appendSection(lines, "Latest Agent Memory Trends", input.latestDigest, "No agent memory trends digest found.");
8501
+ appendMemoryTopicsSection(lines, input.project, input.memoryTopics);
8491
8502
  appendTextSection(lines, "Health Warnings", input.healthWarnings, "No high-level health warnings found.");
8492
8503
  lines.push(
8493
8504
  "## Suggested Commands",
@@ -8499,6 +8510,15 @@ function renderBrief(input) {
8499
8510
  );
8500
8511
  return lines.join("\n").trimEnd() + "\n";
8501
8512
  }
8513
+ function appendMemoryTopicsSection(lines, project, topics) {
8514
+ if (!project || topics.length === 0) return;
8515
+ lines.push("## Memory Topics", "");
8516
+ for (const topic of topics.slice(0, 5)) {
8517
+ const sourceCount = topic.paths.length === 1 ? "1 source" : `${topic.paths.length} sources`;
8518
+ lines.push(`- ${topic.updated} ${topic.name} (${sourceCount}) \u2014 ${topic.summary}; recall: \`skillwiki memory recall --project ${project} --topic ${topic.name}\``);
8519
+ }
8520
+ lines.push("");
8521
+ }
8502
8522
  function appendSection(lines, title, items, empty) {
8503
8523
  lines.push(`## ${title}`, "");
8504
8524
  if (items.length === 0) {
@@ -8537,6 +8557,23 @@ async function loadHealthWarnings(vault) {
8537
8557
  return [];
8538
8558
  }
8539
8559
  }
8560
+ async function loadMemoryTopics(vault, project) {
8561
+ const text = await readIfExists2(join38(vault, ".skillwiki", "memory", project, "topics.json"));
8562
+ if (!text) return [];
8563
+ try {
8564
+ const parsed = JSON.parse(text);
8565
+ if (!Array.isArray(parsed.topics)) return [];
8566
+ return parsed.topics.filter((topic) => typeof topic === "object" && topic !== null && !Array.isArray(topic)).map((topic) => ({
8567
+ name: stringField(topic.name),
8568
+ summary: stringField(topic.summary),
8569
+ project: stringField(topic.project) || void 0,
8570
+ updated: stringField(topic.updated),
8571
+ paths: Array.isArray(topic.paths) ? topic.paths.filter((path) => typeof path === "string") : []
8572
+ })).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);
8573
+ } catch {
8574
+ return [];
8575
+ }
8576
+ }
8540
8577
  async function writeBriefArtifacts(vault, input) {
8541
8578
  const metaPath = join38(vault, "meta", "latest-session-brief.md");
8542
8579
  const cacheMdPath = join38(vault, ".skillwiki", "session-brief.md");
@@ -8553,7 +8590,8 @@ async function writeBriefArtifacts(vault, input) {
8553
8590
  project: input.project,
8554
8591
  brief: input.brief,
8555
8592
  word_count: input.wordCount,
8556
- generated_at: input.generatedAt
8593
+ generated_at: input.generatedAt,
8594
+ memory_topics: input.memoryTopics
8557
8595
  }, null, 2)}
8558
8596
  `, "utf8");
8559
8597
  const indexUpdated = await ensureIndexEntry(vault);
@@ -8589,7 +8627,7 @@ function renderCommittedBrief(input) {
8589
8627
  `created: ${input.today}`,
8590
8628
  `updated: ${input.today}`,
8591
8629
  "type: meta",
8592
- "tags: [generated, session-brief]",
8630
+ "tags: [meta, session-brief]",
8593
8631
  "confidence: high",
8594
8632
  "generated_by: skillwiki session-brief",
8595
8633
  `generated_at: ${input.generatedAt}`,
@@ -8672,10 +8710,553 @@ function dateFromPath(path) {
8672
8710
  return path.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
8673
8711
  }
8674
8712
 
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";
8713
+ // src/commands/memory.ts
8678
8714
  import { createHash as createHash8 } from "crypto";
8715
+ import { mkdir as mkdir14, readFile as readFile26, readdir as readdir8, stat as stat9, writeFile as writeFile15 } from "fs/promises";
8716
+ import { basename as basename2, extname, join as join39, relative as relative4, sep as sep4 } from "path";
8717
+ async function runMemoryTopics(input) {
8718
+ const scan = await scanVault(input.vault);
8719
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8720
+ const cacheText = await readMemoryCache(input.vault, input.project);
8721
+ if (!cacheText) {
8722
+ return {
8723
+ exitCode: ExitCode.OK,
8724
+ result: ok({
8725
+ cache_present: false,
8726
+ topics: [],
8727
+ files_written: [],
8728
+ humanHint: "no memory topics cache found"
8729
+ })
8730
+ };
8731
+ }
8732
+ let parsed;
8733
+ try {
8734
+ parsed = JSON.parse(cacheText);
8735
+ } catch (e) {
8736
+ return {
8737
+ exitCode: ExitCode.WRITE_FAILED,
8738
+ result: err("WRITE_FAILED", {
8739
+ path: ".skillwiki/memory-topics.json",
8740
+ message: `invalid memory topics cache: ${String(e)}`
8741
+ })
8742
+ };
8743
+ }
8744
+ const limit = normalizeLimit(input.limit);
8745
+ const topics = normalizeTopics(parsed.topics).filter((topic) => !input.project || topic.project === input.project).sort(compareTopics).slice(0, limit);
8746
+ return {
8747
+ exitCode: ExitCode.OK,
8748
+ result: ok({
8749
+ generated_at: stringField2(parsed.generated_at) || void 0,
8750
+ cache_present: true,
8751
+ topics,
8752
+ files_written: [],
8753
+ humanHint: renderHumanHint(topics)
8754
+ })
8755
+ };
8756
+ }
8757
+ async function runMemoryIndex(input) {
8758
+ const scan = await scanVault(input.vault);
8759
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8760
+ const warnings = [];
8761
+ const sourcePages = dedupePages(scan.data.allMarkdown);
8762
+ const sources = [];
8763
+ for (const page of sourcePages) {
8764
+ const source = await readMemorySource(page, input.project, warnings);
8765
+ if (source) sources.push(source);
8766
+ }
8767
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
8768
+ const topics = buildTopics(input.project, sources);
8769
+ const relCachePath = memoryCacheRelPath(input.project);
8770
+ const absCachePath = join39(input.vault, relCachePath);
8771
+ await mkdir14(join39(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
8772
+ await writeFile15(absCachePath, `${JSON.stringify({
8773
+ generated_at: generatedAt,
8774
+ project: input.project,
8775
+ topics,
8776
+ sources,
8777
+ warnings
8778
+ }, null, 2)}
8779
+ `, "utf8");
8780
+ return {
8781
+ exitCode: ExitCode.OK,
8782
+ result: ok({
8783
+ project: input.project,
8784
+ generated_at: generatedAt,
8785
+ topic_count: topics.length,
8786
+ source_count: sources.length,
8787
+ topics,
8788
+ files_written: [relCachePath],
8789
+ warnings,
8790
+ humanHint: `indexed ${sources.length} memory sources into ${topics.length} topics for ${input.project}`
8791
+ })
8792
+ };
8793
+ }
8794
+ async function runMemoryRecall(input) {
8795
+ const scan = await scanVault(input.vault);
8796
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8797
+ const cacheText = await readMemoryCache(input.vault, input.project);
8798
+ if (!cacheText) {
8799
+ return {
8800
+ exitCode: ExitCode.OK,
8801
+ result: ok({
8802
+ project: input.project,
8803
+ topic: input.topic,
8804
+ sources: [],
8805
+ humanHint: `no memory topics cache found for project ${input.project}`
8806
+ })
8807
+ };
8808
+ }
8809
+ let parsed;
8810
+ try {
8811
+ parsed = JSON.parse(cacheText);
8812
+ } catch (e) {
8813
+ return {
8814
+ exitCode: ExitCode.WRITE_FAILED,
8815
+ result: err("WRITE_FAILED", {
8816
+ path: memoryCacheRelPath(input.project),
8817
+ message: `invalid memory topics cache: ${String(e)}`
8818
+ })
8819
+ };
8820
+ }
8821
+ const limit = normalizeLimit(input.limit);
8822
+ const sources = normalizeSources(parsed.sources).filter((source) => source.topics.includes(input.topic)).filter((source) => source.memory_privacy !== "secret-blocked").sort(compareSources).slice(0, limit);
8823
+ return {
8824
+ exitCode: ExitCode.OK,
8825
+ result: ok({
8826
+ project: input.project,
8827
+ topic: input.topic,
8828
+ sources,
8829
+ humanHint: renderRecallHint(input.topic, sources)
8830
+ })
8831
+ };
8832
+ }
8833
+ async function runMemoryImport(input) {
8834
+ const scan = await scanVault(input.vault);
8835
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
8836
+ const maxBytes = input.maxBytes ?? 2e5;
8837
+ let files;
8838
+ try {
8839
+ files = await collectImportFiles(input.from);
8840
+ } catch (e) {
8841
+ return {
8842
+ exitCode: ExitCode.WRITE_FAILED,
8843
+ result: err("WRITE_FAILED", { path: input.from, message: String(e) })
8844
+ };
8845
+ }
8846
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
8847
+ const today = generatedAt.slice(0, 10);
8848
+ const entries = [];
8849
+ const filesWritten = [];
8850
+ for (const file of files) {
8851
+ let entry;
8852
+ try {
8853
+ entry = await buildImportEntry(file, input.from, input.project, today, maxBytes);
8854
+ if (input.apply && entry.status === "ready" && entry.proposed_path) {
8855
+ const written2 = await writeImportCapture(input.vault, entry, today);
8856
+ entry.status = "written";
8857
+ entry.written_path = written2.relPath;
8858
+ entry.validation = written2.validation;
8859
+ filesWritten.push(written2.relPath);
8860
+ }
8861
+ } catch (e) {
8862
+ return {
8863
+ exitCode: ExitCode.WRITE_FAILED,
8864
+ result: err("WRITE_FAILED", { path: file, message: String(e) })
8865
+ };
8866
+ }
8867
+ entries.push(entry);
8868
+ }
8869
+ const manifest = {
8870
+ project: input.project,
8871
+ generated_at: generatedAt,
8872
+ source_root: input.from,
8873
+ entries
8874
+ };
8875
+ const ready = entries.filter((entry) => entry.status === "ready").length;
8876
+ const written = entries.filter((entry) => entry.status === "written").length;
8877
+ const rejected = entries.filter((entry) => entry.status === "rejected").length;
8878
+ return {
8879
+ exitCode: ExitCode.OK,
8880
+ result: ok({
8881
+ applied: !!input.apply,
8882
+ manifest,
8883
+ files_written: filesWritten,
8884
+ humanHint: input.apply ? `memory import applied: ${written} written, ${rejected} rejected` : `memory import dry-run: ${ready} ready, ${rejected} rejected`
8885
+ })
8886
+ };
8887
+ }
8888
+ async function readIfExists3(path) {
8889
+ try {
8890
+ return await readFile26(path, "utf8");
8891
+ } catch {
8892
+ return "";
8893
+ }
8894
+ }
8895
+ async function collectImportFiles(source) {
8896
+ const st = await stat9(source);
8897
+ if (st.isFile()) return isImportCandidate(source) ? [source] : [];
8898
+ const files = [];
8899
+ await walkImportFiles(source, files);
8900
+ return files.sort((a, b) => a.localeCompare(b));
8901
+ }
8902
+ async function walkImportFiles(dir, out) {
8903
+ const entries = await readdir8(dir, { withFileTypes: true });
8904
+ for (const entry of entries) {
8905
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
8906
+ const path = join39(dir, entry.name);
8907
+ if (entry.isDirectory()) {
8908
+ await walkImportFiles(path, out);
8909
+ } else if (entry.isFile() && isImportCandidate(path)) {
8910
+ out.push(path);
8911
+ }
8912
+ }
8913
+ }
8914
+ function isImportCandidate(path) {
8915
+ const ext = extname(path).toLowerCase();
8916
+ return ext === ".md" || ext === ".txt";
8917
+ }
8918
+ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
8919
+ const st = await stat9(file);
8920
+ const sourceKind = classifyImportSource(file);
8921
+ const hash = createHash8("sha256").update(await readFile26(file)).digest("hex");
8922
+ const baseEntry = {
8923
+ source_path: file,
8924
+ source_kind: sourceKind,
8925
+ sha256: hash,
8926
+ memory_kind: defaultImportKind(sourceKind),
8927
+ memory_privacy: "local",
8928
+ redaction_count: 0
8929
+ };
8930
+ if (st.size > maxBytes) {
8931
+ return {
8932
+ ...baseEntry,
8933
+ status: "rejected",
8934
+ reason: "oversized_source"
8935
+ };
8936
+ }
8937
+ if (sourceKind === "codex-rule" || sourceKind === "agents-policy") {
8938
+ return {
8939
+ ...baseEntry,
8940
+ status: "rejected",
8941
+ reason: "policy_source_not_imported"
8942
+ };
8943
+ }
8944
+ const text = await readFile26(file, "utf8");
8945
+ const extracted = extractImportText(text, sourceKind);
8946
+ if (!extracted) {
8947
+ return {
8948
+ ...baseEntry,
8949
+ status: "rejected",
8950
+ reason: "policy_source_not_imported"
8951
+ };
8952
+ }
8953
+ const redacted = redactSensitiveContent(extracted, { file });
8954
+ const privacy = redacted.findings.length > 0 ? "sensitive" : "local";
8955
+ const sourceSlug = slugify3(basename2(file, extname(file)));
8956
+ const relSource = relative4(sourceRoot, file).split(sep4).join("/");
8957
+ const entry = {
8958
+ ...baseEntry,
8959
+ status: "ready",
8960
+ memory_privacy: privacy,
8961
+ redaction_count: redacted.findings.length,
8962
+ proposed_path: `raw/transcripts/${today}-memory-import-${sourceSlug}.md`,
8963
+ source_path: relSource || file
8964
+ };
8965
+ Object.defineProperty(entry, "__content", { value: redacted.text, enumerable: false });
8966
+ Object.defineProperty(entry, "__project", { value: project, enumerable: false });
8967
+ return entry;
8968
+ }
8969
+ async function writeImportCapture(vault, entry, today) {
8970
+ const content = hiddenString(entry, "__content");
8971
+ const project = hiddenString(entry, "__project");
8972
+ const relPath = await availableImportPath(vault, entry.proposed_path);
8973
+ const absPath = join39(vault, relPath);
8974
+ await mkdir14(join39(vault, "raw", "transcripts"), { recursive: true });
8975
+ await writeFile15(absPath, renderImportCapture(entry, content, project, today), "utf8");
8976
+ const validation = await runValidate({ file: absPath });
8977
+ return {
8978
+ relPath,
8979
+ validation: {
8980
+ valid: validation.exitCode === ExitCode.OK,
8981
+ exit_code: validation.exitCode
8982
+ }
8983
+ };
8984
+ }
8985
+ async function availableImportPath(vault, proposed) {
8986
+ const ext = extname(proposed);
8987
+ const stem = proposed.slice(0, -ext.length);
8988
+ let candidate = proposed;
8989
+ let i = 2;
8990
+ while (await readIfExists3(join39(vault, candidate))) {
8991
+ candidate = `${stem}-${i}${ext}`;
8992
+ i++;
8993
+ }
8994
+ return candidate;
8995
+ }
8996
+ function renderImportCapture(entry, content, project, today) {
8997
+ return [
8998
+ "---",
8999
+ "source_url:",
9000
+ `ingested: ${today}`,
9001
+ "kind: note",
9002
+ `project: "[[${project}]]"`,
9003
+ `memory_kind: ${entry.memory_kind}`,
9004
+ "memory_topics: [imported-memory]",
9005
+ "memory_scope: project",
9006
+ "memory_policy: historical",
9007
+ `memory_privacy: ${entry.memory_privacy}`,
9008
+ "memory_status: active",
9009
+ `source_agent: skillwiki memory import`,
9010
+ `source_hash: ${entry.sha256}`,
9011
+ `source_paths: ["${entry.source_path.replaceAll('"', '\\"')}"]`,
9012
+ "---",
9013
+ "",
9014
+ `# Imported Memory: ${basename2(entry.source_path, extname(entry.source_path))}`,
9015
+ "",
9016
+ `Source kind: ${entry.source_kind}`,
9017
+ "",
9018
+ "Promotion guidance: review this raw capture before promoting it to compound or typed knowledge.",
9019
+ "",
9020
+ content.trimEnd(),
9021
+ ""
9022
+ ].join("\n");
9023
+ }
9024
+ function hiddenString(entry, key) {
9025
+ return entry[key] ?? "";
9026
+ }
9027
+ function classifyImportSource(file) {
9028
+ const rel = file.split(sep4).join("/");
9029
+ const name = basename2(file);
9030
+ if (rel.includes("/.codex/memories/")) return "codex-memory";
9031
+ if (rel.includes("/.codex/rules/")) return "codex-rule";
9032
+ if (rel.includes("/.claude/") && name === "napkin.md") return "napkin";
9033
+ if (rel.includes("/.memsearch/memory/")) return "memsearch";
9034
+ if (name === "AGENTS.md") return "agents-policy";
9035
+ if (name === "CLAUDE.md") return "claude-auto-memory";
9036
+ return "generic";
9037
+ }
9038
+ function defaultImportKind(sourceKind) {
9039
+ switch (sourceKind) {
9040
+ case "codex-memory":
9041
+ case "claude-auto-memory":
9042
+ return "preference";
9043
+ case "napkin":
9044
+ return "correction";
9045
+ case "memsearch":
9046
+ return "handoff";
9047
+ default:
9048
+ return "handoff";
9049
+ }
9050
+ }
9051
+ function extractImportText(text, sourceKind) {
9052
+ if (sourceKind !== "claude-auto-memory") return text;
9053
+ const match = text.match(/<!--\s*AUTO-MEMORY:START\s*-->([\s\S]*?)<!--\s*AUTO-MEMORY:END\s*-->/i);
9054
+ return match?.[1]?.trim() ?? "";
9055
+ }
9056
+ function normalizeTopics(value) {
9057
+ if (!Array.isArray(value)) return [];
9058
+ const topics = [];
9059
+ for (const item of value) {
9060
+ if (!isRecord(item)) continue;
9061
+ const name = stringField2(item.name);
9062
+ const summary = stringField2(item.summary);
9063
+ const updated = stringField2(item.updated);
9064
+ const paths = stringArray(item.paths);
9065
+ if (!name || !summary || !updated || paths.length === 0) continue;
9066
+ const project = stringField2(item.project);
9067
+ topics.push({
9068
+ name,
9069
+ summary,
9070
+ ...project ? { project } : {},
9071
+ updated,
9072
+ paths
9073
+ });
9074
+ }
9075
+ return topics;
9076
+ }
9077
+ function normalizeSources(value) {
9078
+ if (!Array.isArray(value)) return [];
9079
+ const sources = [];
9080
+ for (const item of value) {
9081
+ if (!isRecord(item)) continue;
9082
+ const path = stringField2(item.path);
9083
+ const title = stringField2(item.title);
9084
+ const summary = stringField2(item.summary);
9085
+ const updated = stringField2(item.updated);
9086
+ const hash = stringField2(item.hash);
9087
+ const topics = stringArray(item.topics);
9088
+ if (!path || !title || !summary || !updated || !hash || topics.length === 0) continue;
9089
+ const project = stringField2(item.project);
9090
+ sources.push({
9091
+ path,
9092
+ title,
9093
+ summary,
9094
+ updated,
9095
+ hash,
9096
+ topics,
9097
+ ...project ? { project } : {},
9098
+ ...stringField2(item.memory_kind) ? { memory_kind: stringField2(item.memory_kind) } : {},
9099
+ ...stringField2(item.memory_scope) ? { memory_scope: stringField2(item.memory_scope) } : {},
9100
+ ...stringField2(item.memory_policy) ? { memory_policy: stringField2(item.memory_policy) } : {},
9101
+ memory_privacy: stringField2(item.memory_privacy) || "local",
9102
+ memory_status: stringField2(item.memory_status) || "active"
9103
+ });
9104
+ }
9105
+ return sources;
9106
+ }
9107
+ function normalizeLimit(value) {
9108
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return 10;
9109
+ return Math.floor(value);
9110
+ }
9111
+ function compareTopics(a, b) {
9112
+ return b.updated.localeCompare(a.updated) || a.name.localeCompare(b.name);
9113
+ }
9114
+ function compareSources(a, b) {
9115
+ return b.updated.localeCompare(a.updated) || a.path.localeCompare(b.path);
9116
+ }
9117
+ function renderHumanHint(topics) {
9118
+ if (topics.length === 0) return "no memory topics found";
9119
+ return topics.map((topic) => {
9120
+ const project = topic.project ? ` [${topic.project}]` : "";
9121
+ return `${topic.updated} ${topic.name}${project} \u2014 ${topic.summary} (${topic.paths.join(", ")})`;
9122
+ }).join("\n");
9123
+ }
9124
+ function renderRecallHint(topic, sources) {
9125
+ if (sources.length === 0) return `no memory sources found for topic ${topic}`;
9126
+ return sources.map((source) => `${source.updated} ${source.title} (${source.path}) \u2014 ${source.summary}`).join("\n");
9127
+ }
9128
+ function memoryCacheRelPath(project) {
9129
+ return `.skillwiki/memory/${project}/topics.json`;
9130
+ }
9131
+ async function readMemoryCache(vault, project) {
9132
+ if (project) {
9133
+ const projectCache = await readIfExists3(join39(vault, memoryCacheRelPath(project)));
9134
+ if (projectCache) return projectCache;
9135
+ }
9136
+ return readIfExists3(join39(vault, ".skillwiki", "memory-topics.json"));
9137
+ }
9138
+ function dedupePages(pages) {
9139
+ const seen = /* @__PURE__ */ new Set();
9140
+ const out = [];
9141
+ for (const page of pages) {
9142
+ if (seen.has(page.relPath)) continue;
9143
+ seen.add(page.relPath);
9144
+ out.push(page);
9145
+ }
9146
+ return out;
9147
+ }
9148
+ async function readMemorySource(page, project, warnings) {
9149
+ const text = await readPage(page);
9150
+ const fm = extractFrontmatter(text);
9151
+ if (!fm.ok) return null;
9152
+ const topics = normalizeMemoryTopics(fm.data.memory_topics, page.relPath, warnings);
9153
+ if (topics.length === 0) return null;
9154
+ const projects = memoryProjects(page.relPath, fm.data);
9155
+ const scope = stringField2(fm.data.memory_scope);
9156
+ if (!projects.includes(project) && scope !== "global" && scope !== "cross-agent") return null;
9157
+ const privacy = stringField2(fm.data.memory_privacy) || "local";
9158
+ if (privacy === "secret-blocked") {
9159
+ warnings.push(`${page.relPath}: skipped secret-blocked memory`);
9160
+ return null;
9161
+ }
9162
+ const status = stringField2(fm.data.memory_status) || "active";
9163
+ if (status === "archived" || status === "rejected") return null;
9164
+ const split = splitFrontmatter(text);
9165
+ const body = split.ok ? split.data.body : text;
9166
+ const updated = stringField2(fm.data.last_seen) || stringField2(fm.data.updated) || stringField2(fm.data.ingested) || dateFromPath2(page.relPath);
9167
+ const title = stringField2(fm.data.title) || page.relPath.split("/").pop()?.replace(/\.md$/, "") || page.relPath;
9168
+ return {
9169
+ path: page.relPath,
9170
+ title,
9171
+ summary: summarize2(body),
9172
+ updated,
9173
+ hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
9174
+ topics,
9175
+ project,
9176
+ ...stringField2(fm.data.memory_kind) ? { memory_kind: stringField2(fm.data.memory_kind) } : {},
9177
+ ...scope ? { memory_scope: scope } : {},
9178
+ ...stringField2(fm.data.memory_policy) ? { memory_policy: stringField2(fm.data.memory_policy) } : {},
9179
+ memory_privacy: privacy,
9180
+ memory_status: status
9181
+ };
9182
+ }
9183
+ function normalizeMemoryTopics(value, path, warnings) {
9184
+ const rawTopics = typeof value === "string" ? [value] : stringArray(value);
9185
+ const topics = [];
9186
+ for (const topic of rawTopics) {
9187
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(topic)) {
9188
+ warnings.push(`${path}: invalid memory topic slug ${topic}`);
9189
+ continue;
9190
+ }
9191
+ if (!topics.includes(topic)) topics.push(topic);
9192
+ }
9193
+ return topics;
9194
+ }
9195
+ function memoryProjects(path, fm) {
9196
+ const projects = /* @__PURE__ */ new Set();
9197
+ const project = wikilinkSlug2(fm.project);
9198
+ if (project) projects.add(project);
9199
+ if (Array.isArray(fm.provenance_projects)) {
9200
+ for (const value of fm.provenance_projects) {
9201
+ const slug = wikilinkSlug2(value);
9202
+ if (slug) projects.add(slug);
9203
+ }
9204
+ }
9205
+ const pathProject = path.match(/^projects\/([^/]+)\//)?.[1];
9206
+ if (pathProject) projects.add(pathProject);
9207
+ return [...projects];
9208
+ }
9209
+ function buildTopics(project, sources) {
9210
+ const byTopic = /* @__PURE__ */ new Map();
9211
+ for (const source of sources) {
9212
+ for (const topic of source.topics) {
9213
+ const list = byTopic.get(topic) ?? [];
9214
+ list.push(source);
9215
+ byTopic.set(topic, list);
9216
+ }
9217
+ }
9218
+ return [...byTopic.entries()].map(([name, topicSources]) => {
9219
+ const sorted = [...topicSources].sort(compareSources);
9220
+ return {
9221
+ name,
9222
+ project,
9223
+ summary: sorted[0]?.summary ?? "",
9224
+ updated: sorted[0]?.updated ?? "",
9225
+ paths: sorted.map((source) => source.path)
9226
+ };
9227
+ }).sort((a, b) => a.name.localeCompare(b.name));
9228
+ }
9229
+ function isRecord(value) {
9230
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9231
+ }
9232
+ function stringField2(value) {
9233
+ return typeof value === "string" ? value : "";
9234
+ }
9235
+ function stringArray(value) {
9236
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
9237
+ }
9238
+ function wikilinkSlug2(value) {
9239
+ if (typeof value !== "string") return void 0;
9240
+ const match = value.match(/^\[\[([^\]]+)\]\]$/);
9241
+ return match?.[1] ?? value;
9242
+ }
9243
+ function summarize2(body) {
9244
+ const lines = body.split(/\r?\n/).map((line) => line.replace(/^#+\s*/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("```"));
9245
+ const summary = lines.join(" ").replace(/\s+/g, " ").trim();
9246
+ return summary.length > 180 ? `${summary.slice(0, 177).trimEnd()}...` : summary;
9247
+ }
9248
+ function dateFromPath2(path) {
9249
+ return path.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
9250
+ }
9251
+ function slugify3(value) {
9252
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
9253
+ return slug || "memory";
9254
+ }
9255
+
9256
+ // src/commands/ingest.ts
9257
+ import { readFile as readFile27, writeFile as writeFile16, mkdir as mkdir15 } from "fs/promises";
9258
+ import { join as join40 } from "path";
9259
+ import { createHash as createHash9 } from "crypto";
8679
9260
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
8680
9261
  var TYPE_DIR = {
8681
9262
  entity: "entities",
@@ -8684,7 +9265,7 @@ var TYPE_DIR = {
8684
9265
  query: "queries"
8685
9266
  };
8686
9267
  var ALLOWED_PROVENANCE = /* @__PURE__ */ new Set(["research", "project"]);
8687
- function slugify3(text) {
9268
+ function slugify4(text) {
8688
9269
  return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "untitled";
8689
9270
  }
8690
9271
  function isUrl(source) {
@@ -8833,7 +9414,7 @@ async function runIngest(input) {
8833
9414
  sourceContent = fetchResult.data.body;
8834
9415
  } else {
8835
9416
  try {
8836
- sourceContent = await readFile26(input.source, "utf8");
9417
+ sourceContent = await readFile27(input.source, "utf8");
8837
9418
  } catch {
8838
9419
  return {
8839
9420
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -8851,15 +9432,15 @@ async function runIngest(input) {
8851
9432
  })
8852
9433
  };
8853
9434
  }
8854
- const sha256 = createHash8("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
9435
+ const sha256 = createHash9("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
8855
9436
  const today = todayIso();
8856
- const slug = slugify3(input.title);
9437
+ const slug = slugify4(input.title);
8857
9438
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
8858
9439
  const rawRelPath = `raw/articles/${slug}.md`;
8859
9440
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
8860
9441
  const typedRelPath = `${typedDir}/${slug}.md`;
8861
- const rawAbsPath = join39(input.vault, rawRelPath);
8862
- const typedAbsPath = join39(input.vault, typedRelPath);
9442
+ const rawAbsPath = join40(input.vault, rawRelPath);
9443
+ const typedAbsPath = join40(input.vault, typedRelPath);
8863
9444
  const identity = assessSourceIdentity({
8864
9445
  rawPath: rawRelPath,
8865
9446
  sourceUrl: sourceUrl ?? void 0,
@@ -8941,8 +9522,8 @@ async function runIngest(input) {
8941
9522
  };
8942
9523
  }
8943
9524
  try {
8944
- await mkdir14(join39(input.vault, "raw", "articles"), { recursive: true });
8945
- await writeFile15(rawAbsPath, rawContent, "utf8");
9525
+ await mkdir15(join40(input.vault, "raw", "articles"), { recursive: true });
9526
+ await writeFile16(rawAbsPath, rawContent, "utf8");
8946
9527
  } catch (e) {
8947
9528
  return {
8948
9529
  exitCode: ExitCode.WRITE_FAILED,
@@ -8950,8 +9531,8 @@ async function runIngest(input) {
8950
9531
  };
8951
9532
  }
8952
9533
  try {
8953
- await mkdir14(join39(input.vault, typedDir), { recursive: true });
8954
- await writeFile15(typedAbsPath, typedContent, "utf8");
9534
+ await mkdir15(join40(input.vault, typedDir), { recursive: true });
9535
+ await writeFile16(typedAbsPath, typedContent, "utf8");
8955
9536
  } catch (e) {
8956
9537
  return {
8957
9538
  exitCode: ExitCode.WRITE_FAILED,
@@ -9130,19 +9711,19 @@ ${body}`;
9130
9711
 
9131
9712
  // src/commands/sync.ts
9132
9713
  import { existsSync as existsSync17 } from "fs";
9133
- import { join as join41 } from "path";
9714
+ import { join as join42 } from "path";
9134
9715
 
9135
9716
  // src/utils/sync-lock.ts
9136
9717
  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";
9718
+ import { join as join41 } from "path";
9719
+ import { createHash as createHash10 } from "crypto";
9139
9720
  function getSessionId() {
9140
9721
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
9141
9722
  if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
9142
9723
  return process.pid.toString();
9143
9724
  }
9144
9725
  function lockPath(vault) {
9145
- return join40(vault, ".skillwiki", "sync.lock");
9726
+ return join41(vault, ".skillwiki", "sync.lock");
9146
9727
  }
9147
9728
  function readLock(vault) {
9148
9729
  const path = lockPath(vault);
@@ -9161,7 +9742,7 @@ function isStale(lock, now) {
9161
9742
  }
9162
9743
  function acquireLock(vault, opts = {}) {
9163
9744
  const path = lockPath(vault);
9164
- const dir = join40(vault, ".skillwiki");
9745
+ const dir = join41(vault, ".skillwiki");
9165
9746
  if (!existsSync16(dir)) {
9166
9747
  mkdirSync6(dir, { recursive: true });
9167
9748
  }
@@ -9236,7 +9817,7 @@ function releaseLock(vault, opts = {}) {
9236
9817
  function runSyncStatus(input) {
9237
9818
  const vault = input.vault;
9238
9819
  const includeStashes = input.includeStashes ?? false;
9239
- if (!existsSync17(join41(vault, ".git"))) {
9820
+ if (!existsSync17(join42(vault, ".git"))) {
9240
9821
  return {
9241
9822
  exitCode: ExitCode.VAULT_PATH_INVALID,
9242
9823
  result: ok({
@@ -9314,7 +9895,7 @@ function runSyncStatus(input) {
9314
9895
  }
9315
9896
  async function runSyncPush(input) {
9316
9897
  const vault = input.vault;
9317
- if (!existsSync17(join41(vault, ".git"))) {
9898
+ if (!existsSync17(join42(vault, ".git"))) {
9318
9899
  return {
9319
9900
  exitCode: ExitCode.VAULT_PATH_INVALID,
9320
9901
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -9437,7 +10018,7 @@ function enableGitLongPathsOnWindows(vault) {
9437
10018
  }
9438
10019
  async function runSyncPull(input) {
9439
10020
  const vault = input.vault;
9440
- if (!existsSync17(join41(vault, ".git"))) {
10021
+ if (!existsSync17(join42(vault, ".git"))) {
9441
10022
  return {
9442
10023
  exitCode: ExitCode.VAULT_PATH_INVALID,
9443
10024
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -9678,7 +10259,7 @@ function runSyncUnlock(input) {
9678
10259
 
9679
10260
  // src/commands/backup.ts
9680
10261
  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";
10262
+ import { join as join43, relative as relative5, dirname as dirname14 } from "path";
9682
10263
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
9683
10264
 
9684
10265
  // src/utils/s3-client.ts
@@ -9702,11 +10283,11 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node
9702
10283
  function* walkMarkdown(dir, base) {
9703
10284
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
9704
10285
  if (SKIP_DIRS2.has(entry.name)) continue;
9705
- const full = join42(dir, entry.name);
10286
+ const full = join43(dir, entry.name);
9706
10287
  if (entry.isDirectory()) {
9707
10288
  yield* walkMarkdown(full, base);
9708
10289
  } else if (entry.name.endsWith(".md")) {
9709
- yield relative4(base, full).replace(/\\/g, "/");
10290
+ yield relative5(base, full).replace(/\\/g, "/");
9710
10291
  }
9711
10292
  }
9712
10293
  }
@@ -9725,7 +10306,7 @@ async function runBackupSync(input) {
9725
10306
  let failed = 0;
9726
10307
  const files = [...walkMarkdown(input.vault, input.vault)];
9727
10308
  for (const relPath of files) {
9728
- const absPath = join42(input.vault, relPath);
10309
+ const absPath = join43(input.vault, relPath);
9729
10310
  const localStat = statSync5(absPath);
9730
10311
  let needsUpload = true;
9731
10312
  try {
@@ -9801,7 +10382,7 @@ async function runBackupRestore(input) {
9801
10382
  const objects = list.Contents ?? [];
9802
10383
  for (const obj of objects) {
9803
10384
  if (!obj.Key) continue;
9804
- const localPath = join42(target, obj.Key);
10385
+ const localPath = join43(target, obj.Key);
9805
10386
  try {
9806
10387
  const localStat = statSync5(localPath);
9807
10388
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -9848,8 +10429,8 @@ async function runBackupRestore(input) {
9848
10429
 
9849
10430
  // src/commands/status.ts
9850
10431
  import { existsSync as existsSync18, statSync as statSync6 } from "fs";
9851
- import { readFile as readFile27 } from "fs/promises";
9852
- import { join as join43 } from "path";
10432
+ import { readFile as readFile28 } from "fs/promises";
10433
+ import { join as join44 } from "path";
9853
10434
  async function runStatus(input) {
9854
10435
  if (!existsSync18(input.vault)) {
9855
10436
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
@@ -9876,7 +10457,7 @@ async function runStatus(input) {
9876
10457
  const compound = scan.data.compound.length;
9877
10458
  let schemaVersion = "v1";
9878
10459
  try {
9879
- const schemaContent = await readFile27(join43(input.vault, "SCHEMA.md"), "utf8");
10460
+ const schemaContent = await readFile28(join44(input.vault, "SCHEMA.md"), "utf8");
9880
10461
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
9881
10462
  if (versionMatch) schemaVersion = versionMatch[1];
9882
10463
  } catch {
@@ -9936,8 +10517,8 @@ async function runStatus(input) {
9936
10517
  }
9937
10518
 
9938
10519
  // 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";
10520
+ import { mkdir as mkdir16, writeFile as writeFile17, stat as stat10 } from "fs/promises";
10521
+ import { join as join45 } from "path";
9941
10522
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9942
10523
  var EXAMPLE_PAGES = {
9943
10524
  "entities/example-project.md": `---
@@ -10006,30 +10587,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
10006
10587
  `;
10007
10588
  async function runSeed(input) {
10008
10589
  try {
10009
- await stat9(join44(input.vault, "SCHEMA.md"));
10590
+ await stat10(join45(input.vault, "SCHEMA.md"));
10010
10591
  } catch {
10011
10592
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
10012
10593
  }
10013
10594
  const created = [];
10014
10595
  const skipped = [];
10015
10596
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
10016
- const absPath = join44(input.vault, relPath);
10597
+ const absPath = join45(input.vault, relPath);
10017
10598
  try {
10018
- await stat9(absPath);
10599
+ await stat10(absPath);
10019
10600
  skipped.push(relPath);
10020
10601
  } catch {
10021
- await mkdir15(join44(absPath, ".."), { recursive: true });
10022
- await writeFile16(absPath, content, "utf8");
10602
+ await mkdir16(join45(absPath, ".."), { recursive: true });
10603
+ await writeFile17(absPath, content, "utf8");
10023
10604
  created.push(relPath);
10024
10605
  }
10025
10606
  }
10026
- const rawPath = join44(input.vault, "raw", "articles", "example-source.md");
10607
+ const rawPath = join45(input.vault, "raw", "articles", "example-source.md");
10027
10608
  try {
10028
- await stat9(rawPath);
10609
+ await stat10(rawPath);
10029
10610
  skipped.push("raw/articles/example-source.md");
10030
10611
  } catch {
10031
- await mkdir15(join44(rawPath, ".."), { recursive: true });
10032
- await writeFile16(rawPath, EXAMPLE_RAW, "utf8");
10612
+ await mkdir16(join45(rawPath, ".."), { recursive: true });
10613
+ await writeFile17(rawPath, EXAMPLE_RAW, "utf8");
10033
10614
  created.push("raw/articles/example-source.md");
10034
10615
  }
10035
10616
  if (created.length > 0) {
@@ -10051,9 +10632,9 @@ async function runSeed(input) {
10051
10632
  }
10052
10633
 
10053
10634
  // src/commands/canvas.ts
10054
- import { readFile as readFile28, writeFile as writeFile17 } from "fs/promises";
10635
+ import { readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
10055
10636
  import { existsSync as existsSync19 } from "fs";
10056
- import { join as join45 } from "path";
10637
+ import { join as join46 } from "path";
10057
10638
  var NODE_WIDTH = 240;
10058
10639
  var NODE_HEIGHT = 60;
10059
10640
  var COLUMN_SPACING = 400;
@@ -10131,7 +10712,7 @@ function buildCanvasEdges(adjacency) {
10131
10712
  return edges;
10132
10713
  }
10133
10714
  async function runCanvasGenerate(input) {
10134
- const graphPath = input.graphPath ?? join45(input.vault, ".skillwiki", "graph.json");
10715
+ const graphPath = input.graphPath ?? join46(input.vault, ".skillwiki", "graph.json");
10135
10716
  if (!existsSync19(graphPath)) {
10136
10717
  return {
10137
10718
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -10143,7 +10724,7 @@ async function runCanvasGenerate(input) {
10143
10724
  }
10144
10725
  let raw;
10145
10726
  try {
10146
- raw = await readFile28(graphPath, "utf8");
10727
+ raw = await readFile29(graphPath, "utf8");
10147
10728
  } catch (e) {
10148
10729
  return {
10149
10730
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -10169,9 +10750,9 @@ async function runCanvasGenerate(input) {
10169
10750
  const nodes = buildCanvasNodes(paths);
10170
10751
  const edges = buildCanvasEdges(graph.adjacency);
10171
10752
  const canvas = { nodes, edges };
10172
- const outPath = join45(input.vault, "vault-graph.canvas");
10753
+ const outPath = join46(input.vault, "vault-graph.canvas");
10173
10754
  try {
10174
- await writeFile17(outPath, JSON.stringify(canvas, null, 2));
10755
+ await writeFile18(outPath, JSON.stringify(canvas, null, 2));
10175
10756
  } catch (e) {
10176
10757
  return {
10177
10758
  exitCode: ExitCode.WRITE_FAILED,
@@ -10191,8 +10772,8 @@ written: ${outPath}`
10191
10772
  }
10192
10773
 
10193
10774
  // src/commands/query.ts
10194
- import { readFile as readFile29, stat as stat10 } from "fs/promises";
10195
- import { join as join46 } from "path";
10775
+ import { readFile as readFile30, stat as stat11 } from "fs/promises";
10776
+ import { join as join47 } from "path";
10196
10777
  var W_KEYWORD = 2;
10197
10778
  var W_SOURCE_OVERLAP = 4;
10198
10779
  var W_WIKILINK = 3;
@@ -10313,10 +10894,10 @@ function computeKeywordScore(terms, title, tags, body) {
10313
10894
  return score;
10314
10895
  }
10315
10896
  async function loadOrBuildGraph(vault) {
10316
- const graphPath = join46(vault, ".skillwiki", "graph.json");
10897
+ const graphPath = join47(vault, ".skillwiki", "graph.json");
10317
10898
  let needsBuild = false;
10318
10899
  try {
10319
- const fileStat = await stat10(graphPath);
10900
+ const fileStat = await stat11(graphPath);
10320
10901
  const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
10321
10902
  if (ageHours > 24) needsBuild = true;
10322
10903
  } catch {
@@ -10327,7 +10908,7 @@ async function loadOrBuildGraph(vault) {
10327
10908
  if (buildResult.exitCode !== 0) return null;
10328
10909
  }
10329
10910
  try {
10330
- const raw = await readFile29(graphPath, "utf8");
10911
+ const raw = await readFile30(graphPath, "utf8");
10331
10912
  return JSON.parse(raw);
10332
10913
  } catch {
10333
10914
  return null;
@@ -10336,14 +10917,14 @@ async function loadOrBuildGraph(vault) {
10336
10917
 
10337
10918
  // src/utils/auto-commit.ts
10338
10919
  import { existsSync as existsSync20 } from "fs";
10339
- import { join as join47 } from "path";
10920
+ import { join as join48 } from "path";
10340
10921
  async function postCommit(vault, exitCode) {
10341
10922
  if (exitCode !== 0) return;
10342
10923
  const home = process.env.HOME ?? "";
10343
10924
  const dotenv = await parseDotenvFile(configPath(home));
10344
10925
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
10345
10926
  if (autoCommit === "false") return;
10346
- if (!existsSync20(join47(vault, ".git"))) return;
10927
+ if (!existsSync20(join48(vault, ".git"))) return;
10347
10928
  const lastOps = readLastOp(vault);
10348
10929
  if (lastOps.length === 0) return;
10349
10930
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -10394,7 +10975,7 @@ program.command("validate <file>").description("validate vault page frontmatter
10394
10975
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
10395
10976
  });
10396
10977
  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");
10978
+ const out = opts.out ?? join49(vault, ".skillwiki", "graph.json");
10398
10979
  emit(await runGraphBuild({ vault, out }), vault);
10399
10980
  });
10400
10981
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -10763,6 +11344,45 @@ program.command("session-brief [vault]").description("render or refresh the boun
10763
11344
  env: { SKILLWIKI_PROJECT: process.env.SKILLWIKI_PROJECT }
10764
11345
  }), v.vault, { postCommit: !!opts.write });
10765
11346
  });
11347
+ var memoryCmd = program.command("memory").description("inspect derived agent memory caches");
11348
+ 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) => {
11349
+ const v = await resolveVaultArg(vault, opts.wiki);
11350
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11351
+ else emit(await runMemoryTopics({
11352
+ vault: v.vault,
11353
+ project: opts.project,
11354
+ limit: opts.limit
11355
+ }), v.vault, { postCommit: false });
11356
+ });
11357
+ 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) => {
11358
+ const v = await resolveVaultArg(vault, opts.wiki);
11359
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11360
+ else emit(await runMemoryIndex({
11361
+ vault: v.vault,
11362
+ project: opts.project
11363
+ }), v.vault);
11364
+ });
11365
+ 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) => {
11366
+ const v = await resolveVaultArg(vault, opts.wiki);
11367
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11368
+ else emit(await runMemoryRecall({
11369
+ vault: v.vault,
11370
+ project: opts.project,
11371
+ topic: opts.topic,
11372
+ limit: opts.limit
11373
+ }), v.vault, { postCommit: false });
11374
+ });
11375
+ 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) => {
11376
+ const v = await resolveVaultArg(vault, opts.wiki);
11377
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
11378
+ else emit(await runMemoryImport({
11379
+ vault: v.vault,
11380
+ from: opts.from,
11381
+ project: opts.project,
11382
+ apply: !!opts.apply && !opts.dryRun,
11383
+ maxBytes: opts.maxBytes
11384
+ }), v.vault, { postCommit: !!opts.apply && !opts.dryRun });
11385
+ });
10766
11386
  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
11387
  const tags = typeof opts.tags === "string" ? opts.tags.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : [];
10768
11388
  emit(await runIngest({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
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.6",
3
+ "version": "0.9.8",
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.6",
3
+ "version": "0.9.8",
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.6",
3
+ "version": "0.9.8",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",