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 +681 -61
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
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
|
|
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
|
|
2364
|
-
if (!LOOP_CYCLE_PATTERN.test(
|
|
2365
|
-
const m =
|
|
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: [
|
|
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/
|
|
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
|
|
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
|
|
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 =
|
|
9435
|
+
const sha256 = createHash9("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
|
|
8855
9436
|
const today = todayIso();
|
|
8856
|
-
const slug =
|
|
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 =
|
|
8862
|
-
const typedAbsPath =
|
|
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
|
|
8945
|
-
await
|
|
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
|
|
8954
|
-
await
|
|
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
|
|
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
|
|
9138
|
-
import { createHash as
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
9852
|
-
import { join as
|
|
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
|
|
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
|
|
9940
|
-
import { join as
|
|
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
|
|
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 =
|
|
10597
|
+
const absPath = join45(input.vault, relPath);
|
|
10017
10598
|
try {
|
|
10018
|
-
await
|
|
10599
|
+
await stat10(absPath);
|
|
10019
10600
|
skipped.push(relPath);
|
|
10020
10601
|
} catch {
|
|
10021
|
-
await
|
|
10022
|
-
await
|
|
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 =
|
|
10607
|
+
const rawPath = join45(input.vault, "raw", "articles", "example-source.md");
|
|
10027
10608
|
try {
|
|
10028
|
-
await
|
|
10609
|
+
await stat10(rawPath);
|
|
10029
10610
|
skipped.push("raw/articles/example-source.md");
|
|
10030
10611
|
} catch {
|
|
10031
|
-
await
|
|
10032
|
-
await
|
|
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
|
|
10635
|
+
import { readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
|
|
10055
10636
|
import { existsSync as existsSync19 } from "fs";
|
|
10056
|
-
import { join as
|
|
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 ??
|
|
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
|
|
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 =
|
|
10753
|
+
const outPath = join46(input.vault, "vault-graph.canvas");
|
|
10173
10754
|
try {
|
|
10174
|
-
await
|
|
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
|
|
10195
|
-
import { join as
|
|
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 =
|
|
10897
|
+
const graphPath = join47(vault, ".skillwiki", "graph.json");
|
|
10317
10898
|
let needsBuild = false;
|
|
10318
10899
|
try {
|
|
10319
|
-
const fileStat = await
|
|
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
|
|
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
|
|
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(
|
|
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 ??
|
|
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.
|
|
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": {
|