skillwiki 0.2.1-beta.16 → 0.2.1-beta.21
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/chunk-TPS5XD2J.js +19 -0
- package/dist/cli.js +3339 -368
- package/dist/git-M4WGJ5G3.js +9 -0
- package/package.json +3 -1
- package/skills/.claude-plugin/plugin.json +2 -2
- package/skills/package.json +1 -1
- package/skills/proj-decide/SKILL.md +1 -0
- package/skills/proj-distill/SKILL.md +1 -0
- package/skills/proj-init/SKILL.md +2 -1
- package/skills/proj-work/SKILL.md +1 -0
- package/skills/using-skillwiki/SKILL.md +7 -2
- package/skills/wiki-adapter-prd/SKILL.md +1 -0
- package/skills/wiki-add-task/SKILL.md +13 -16
- package/skills/wiki-archive/SKILL.md +1 -0
- package/skills/wiki-audit/SKILL.md +1 -0
- package/skills/wiki-canvas/SKILL.md +57 -0
- package/skills/wiki-crystallize/SKILL.md +1 -0
- package/skills/wiki-ingest/SKILL.md +1 -0
- package/skills/wiki-init/SKILL.md +1 -0
- package/skills/wiki-lint/SKILL.md +1 -0
- package/skills/wiki-query/SKILL.md +1 -0
- package/skills/wiki-reingest/SKILL.md +1 -0
- package/skills/wiki-sync/SKILL.md +91 -0
- package/templates/index.md +3 -0
- package/templates/project-README.md +3 -1
package/dist/cli.js
CHANGED
|
@@ -2,43 +2,20 @@
|
|
|
2
2
|
import {
|
|
3
3
|
semverGt
|
|
4
4
|
} from "./chunk-XM5IYZX7.js";
|
|
5
|
+
import {
|
|
6
|
+
git,
|
|
7
|
+
gitStrict
|
|
8
|
+
} from "./chunk-TPS5XD2J.js";
|
|
5
9
|
|
|
6
10
|
// src/cli.ts
|
|
7
|
-
import { readFileSync as
|
|
11
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
12
|
+
import { join as join37 } from "path";
|
|
8
13
|
import { Command } from "commander";
|
|
9
14
|
|
|
10
|
-
// src/utils/output.ts
|
|
11
|
-
function printJson(r) {
|
|
12
|
-
process.stdout.write(JSON.stringify(r) + "\n");
|
|
13
|
-
}
|
|
14
|
-
function printHuman(r) {
|
|
15
|
-
if (r.ok) {
|
|
16
|
-
if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
|
|
17
|
-
process.stdout.write(`${r.data.humanHint}
|
|
18
|
-
`);
|
|
19
|
-
} else {
|
|
20
|
-
process.stdout.write(`OK
|
|
21
|
-
${formatData(r.data)}
|
|
22
|
-
`);
|
|
23
|
-
}
|
|
24
|
-
} else {
|
|
25
|
-
process.stdout.write(`ERR ${r.error}
|
|
26
|
-
${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function formatData(d) {
|
|
30
|
-
if (d == null) return "";
|
|
31
|
-
if (typeof d === "string") return d;
|
|
32
|
-
return JSON.stringify(d, null, 2);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// src/commands/hash.ts
|
|
36
|
-
import { readFile } from "fs/promises";
|
|
37
|
-
import { createHash } from "crypto";
|
|
38
|
-
|
|
39
15
|
// ../shared/src/exit-codes.ts
|
|
40
16
|
var ExitCode = {
|
|
41
17
|
OK: 0,
|
|
18
|
+
INTERNAL_ERROR: 1,
|
|
42
19
|
FILE_NOT_FOUND: 2,
|
|
43
20
|
MISSING_CLOSING_DELIMITER: 3,
|
|
44
21
|
SCHEME_REJECTED: 4,
|
|
@@ -74,7 +51,15 @@ var ExitCode = {
|
|
|
74
51
|
MIGRATION_APPLIED: 34,
|
|
75
52
|
UNKNOWN_WIKI_PROFILE: 35,
|
|
76
53
|
DEDUP_APPLIED: 36,
|
|
77
|
-
PROJECT_NOT_FOUND: 37
|
|
54
|
+
PROJECT_NOT_FOUND: 37,
|
|
55
|
+
SYMLINK_FAILED: 38,
|
|
56
|
+
COMPOUND_PROMOTED: 39,
|
|
57
|
+
SKILL_VERSION_MISMATCH: 40,
|
|
58
|
+
INGEST_VALIDATION_FAILED: 41,
|
|
59
|
+
SYNC_PUSH_FAILED: 42,
|
|
60
|
+
SYNC_PULL_FAILED: 43,
|
|
61
|
+
BACKUP_SYNC_FAILED: 44,
|
|
62
|
+
BACKUP_RESTORE_CONFLICTS: 45
|
|
78
63
|
};
|
|
79
64
|
|
|
80
65
|
// ../shared/src/json-output.ts
|
|
@@ -98,7 +83,7 @@ var TypedKnowledgeSchema = z.object({
|
|
|
98
83
|
aliases: z.array(z.string()).optional(),
|
|
99
84
|
created: isoDate,
|
|
100
85
|
updated: isoDate,
|
|
101
|
-
type: z.enum(["entity", "concept", "comparison", "query"
|
|
86
|
+
type: z.enum(["entity", "concept", "comparison", "query"]),
|
|
102
87
|
tags: z.array(z.string()),
|
|
103
88
|
sources: z.array(z.string()).min(1),
|
|
104
89
|
confidence: z.enum(["high", "medium", "low"]).optional(),
|
|
@@ -118,7 +103,7 @@ var RawSourceSchema = z.object({
|
|
|
118
103
|
source_url: z.string().nullable(),
|
|
119
104
|
ingested: isoDate,
|
|
120
105
|
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
|
|
121
|
-
sha256: sha256Hex,
|
|
106
|
+
sha256: sha256Hex.optional(),
|
|
122
107
|
project: wikilink.optional(),
|
|
123
108
|
work_item: wikilink.optional(),
|
|
124
109
|
kind: z.enum(["postmortem", "session-log", "meeting-notes", "other", "idea", "bug", "task", "note"]).optional()
|
|
@@ -181,7 +166,9 @@ function detectSchema(fm) {
|
|
|
181
166
|
if (typeof fm.type === "string" && COMPOUND_TYPES.has(fm.type) && "project" in fm) return { schema: "compound" };
|
|
182
167
|
if (fm.type === "meta") return { schema: "meta" };
|
|
183
168
|
if ("type" in fm && "sources" in fm) return { schema: "typed-knowledge" };
|
|
184
|
-
if (
|
|
169
|
+
if ("ingested" in fm && ("source_url" in fm || "sha256" in fm)) return { schema: "raw" };
|
|
170
|
+
const RAW_KINDS = /* @__PURE__ */ new Set(["postmortem", "session-log", "meeting-notes", "other", "idea", "bug", "task", "note"]);
|
|
171
|
+
if ("ingested" in fm && typeof fm.kind === "string" && RAW_KINDS.has(fm.kind)) return { schema: "raw" };
|
|
185
172
|
if ("kind" in fm && "status" in fm) return { schema: "work-item" };
|
|
186
173
|
return { schema: null };
|
|
187
174
|
}
|
|
@@ -225,6 +212,56 @@ function isBlockedHost(host) {
|
|
|
225
212
|
return false;
|
|
226
213
|
}
|
|
227
214
|
|
|
215
|
+
// src/utils/output.ts
|
|
216
|
+
function printJson(r) {
|
|
217
|
+
process.stdout.write(JSON.stringify(r) + "\n");
|
|
218
|
+
}
|
|
219
|
+
function printHuman(r) {
|
|
220
|
+
if (r.ok) {
|
|
221
|
+
if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
|
|
222
|
+
process.stdout.write(`${r.data.humanHint}
|
|
223
|
+
`);
|
|
224
|
+
} else {
|
|
225
|
+
process.stdout.write(`OK
|
|
226
|
+
${formatData(r.data)}
|
|
227
|
+
`);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
process.stdout.write(`ERR ${r.error}
|
|
231
|
+
${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function formatData(d) {
|
|
235
|
+
if (d == null) return "";
|
|
236
|
+
if (typeof d === "string") return d;
|
|
237
|
+
return JSON.stringify(d, null, 2);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/utils/deprecation.ts
|
|
241
|
+
import { readFileSync } from "fs";
|
|
242
|
+
import { join } from "path";
|
|
243
|
+
function getDeprecatedWarnings(home) {
|
|
244
|
+
const manifestPath = join(home, ".claude", "skills", "wiki-manifest.json");
|
|
245
|
+
try {
|
|
246
|
+
const raw = readFileSync(manifestPath, "utf8");
|
|
247
|
+
const manifest = JSON.parse(raw);
|
|
248
|
+
if (!manifest.skills) return [];
|
|
249
|
+
const warnings = [];
|
|
250
|
+
for (const [dirName, meta] of Object.entries(manifest.skills)) {
|
|
251
|
+
if (meta.deprecated) {
|
|
252
|
+
warnings.push(`\u26A0 Skill "${meta.name || dirName}" is deprecated. See SKILL.md for migration notes.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return warnings;
|
|
256
|
+
} catch {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/commands/hash.ts
|
|
262
|
+
import { readFile } from "fs/promises";
|
|
263
|
+
import { createHash } from "crypto";
|
|
264
|
+
|
|
228
265
|
// src/parsers/frontmatter.ts
|
|
229
266
|
import yaml from "js-yaml";
|
|
230
267
|
var FM_OPEN = /^---\r?\n/;
|
|
@@ -313,7 +350,16 @@ function sanitizeUrl(u) {
|
|
|
313
350
|
}
|
|
314
351
|
|
|
315
352
|
// src/commands/validate.ts
|
|
316
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
353
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
354
|
+
import { join as join2, resolve, relative, sep } from "path";
|
|
355
|
+
var TYPE_TO_SECTION = {
|
|
356
|
+
entity: "Entities",
|
|
357
|
+
concept: "Concepts",
|
|
358
|
+
comparison: "Comparisons",
|
|
359
|
+
query: "Queries",
|
|
360
|
+
summary: "Summaries",
|
|
361
|
+
meta: "Meta"
|
|
362
|
+
};
|
|
317
363
|
var SCHEMAS = {
|
|
318
364
|
"typed-knowledge": TypedKnowledgeSchema,
|
|
319
365
|
"raw": RawSourceSchema,
|
|
@@ -337,36 +383,120 @@ async function runValidate(input) {
|
|
|
337
383
|
}
|
|
338
384
|
const det = detectSchema(fm.data);
|
|
339
385
|
if (!det.schema) {
|
|
340
|
-
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
|
|
386
|
+
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], index_updated: false, log_updated: false, humanHint: "schema not detected" }) };
|
|
341
387
|
}
|
|
342
388
|
const parsed = SCHEMAS[det.schema].safeParse(fm.data);
|
|
343
389
|
if (!parsed.success) {
|
|
344
390
|
const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
345
391
|
return {
|
|
346
392
|
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
347
|
-
result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
|
|
393
|
+
result: ok({ schema: det.schema, valid: false, errors, index_updated: false, log_updated: false, humanHint: `INVALID (${det.schema})
|
|
348
394
|
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
349
395
|
};
|
|
350
396
|
}
|
|
351
|
-
|
|
397
|
+
if (input.apply && !input.vault) {
|
|
398
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: "--vault is required when --apply is set" }) };
|
|
399
|
+
}
|
|
400
|
+
let indexUpdated = false;
|
|
401
|
+
let logUpdated = false;
|
|
402
|
+
let applyHint = "";
|
|
403
|
+
if (input.apply && input.vault) {
|
|
404
|
+
const absFile = resolve(input.file);
|
|
405
|
+
const absVault = resolve(input.vault);
|
|
406
|
+
const relPath = relative(absVault, absFile).split(sep).join("/");
|
|
407
|
+
if (relPath.startsWith("..")) {
|
|
408
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: `file ${input.file} is not inside vault ${input.vault}` }) };
|
|
409
|
+
}
|
|
410
|
+
const pageType = "type" in parsed.data && typeof parsed.data.type === "string" ? parsed.data.type : "";
|
|
411
|
+
const title = typeof parsed.data.title === "string" ? parsed.data.title : relPath.replace(/\.md$/, "");
|
|
412
|
+
if (det.schema === "typed-knowledge" || det.schema === "meta") {
|
|
413
|
+
indexUpdated = await addToIndex(input.vault, relPath, title, pageType);
|
|
414
|
+
}
|
|
415
|
+
logUpdated = await appendToLog(input.vault, relPath);
|
|
416
|
+
if (indexUpdated) applyHint += `
|
|
417
|
+
index: added [[${relPath.replace(/\.md$/, "")}]]`;
|
|
418
|
+
if (logUpdated) applyHint += "\n log: appended entry";
|
|
419
|
+
}
|
|
420
|
+
return { exitCode: ExitCode.OK, result: ok({
|
|
421
|
+
schema: det.schema,
|
|
422
|
+
valid: true,
|
|
423
|
+
errors: [],
|
|
424
|
+
index_updated: indexUpdated,
|
|
425
|
+
log_updated: logUpdated,
|
|
426
|
+
humanHint: `VALID (${det.schema})${applyHint}`
|
|
427
|
+
}) };
|
|
428
|
+
}
|
|
429
|
+
async function addToIndex(vault, relPath, title, pageType) {
|
|
430
|
+
const section = TYPE_TO_SECTION[pageType];
|
|
431
|
+
if (!section) return false;
|
|
432
|
+
const indexPath = join2(vault, "index.md");
|
|
433
|
+
let text;
|
|
434
|
+
try {
|
|
435
|
+
text = await readFile2(indexPath, "utf8");
|
|
436
|
+
} catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const ref = relPath.replace(/\.md$/, "");
|
|
440
|
+
if (text.includes(`[[${ref}]]`)) return false;
|
|
441
|
+
const entry = `- [[${ref}]] \u2014 ${title}`;
|
|
442
|
+
const lines = text.split("\n");
|
|
443
|
+
const sectionLine = `## ${section}`;
|
|
444
|
+
const sectionIdx = lines.findIndex((l) => l.trim() === sectionLine);
|
|
445
|
+
if (sectionIdx === -1) {
|
|
446
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
447
|
+
lines.push("", sectionLine, entry);
|
|
448
|
+
} else {
|
|
449
|
+
let endIdx = sectionIdx + 1;
|
|
450
|
+
while (endIdx < lines.length) {
|
|
451
|
+
if (lines[endIdx].startsWith("## ")) break;
|
|
452
|
+
endIdx++;
|
|
453
|
+
}
|
|
454
|
+
let insertAt = endIdx;
|
|
455
|
+
while (insertAt > sectionIdx + 1 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
456
|
+
lines.splice(insertAt, 0, entry);
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
await writeFile(indexPath, lines.join("\n"), "utf8");
|
|
460
|
+
} catch {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
async function appendToLog(vault, relPath) {
|
|
466
|
+
const logPath = join2(vault, "log.md");
|
|
467
|
+
let text;
|
|
468
|
+
try {
|
|
469
|
+
text = await readFile2(logPath, "utf8");
|
|
470
|
+
} catch {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
474
|
+
const entry = `
|
|
475
|
+
## [${today}] validate | added: ${relPath}`;
|
|
476
|
+
try {
|
|
477
|
+
await writeFile(logPath, text.trimEnd() + entry, "utf8");
|
|
478
|
+
} catch {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
return true;
|
|
352
482
|
}
|
|
353
483
|
|
|
354
484
|
// src/commands/graph.ts
|
|
355
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
485
|
+
import { writeFile as writeFile2, mkdir } from "fs/promises";
|
|
356
486
|
import { dirname } from "path";
|
|
357
487
|
|
|
358
488
|
// src/utils/vault.ts
|
|
359
489
|
import { readFile as readFile3, readdir, stat } from "fs/promises";
|
|
360
|
-
import { join, relative, sep } from "path";
|
|
490
|
+
import { join as join3, relative as relative2, sep as sep2 } from "path";
|
|
361
491
|
var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
362
492
|
async function scanVault(root) {
|
|
363
493
|
try {
|
|
364
|
-
await stat(
|
|
494
|
+
await stat(join3(root, "SCHEMA.md"));
|
|
365
495
|
} catch {
|
|
366
496
|
return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
|
|
367
497
|
}
|
|
368
498
|
const all = await walk(root);
|
|
369
|
-
const rels = all.map((p) => ({ absPath: p, relPath:
|
|
499
|
+
const rels = all.map((p) => ({ absPath: p, relPath: relative2(root, p).split(sep2).join("/") }));
|
|
370
500
|
return ok({
|
|
371
501
|
root,
|
|
372
502
|
typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
|
|
@@ -379,7 +509,7 @@ async function walk(dir) {
|
|
|
379
509
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
380
510
|
const out = [];
|
|
381
511
|
for (const e of entries) {
|
|
382
|
-
const p =
|
|
512
|
+
const p = join3(dir, e.name);
|
|
383
513
|
if (e.isDirectory()) out.push(...await walk(p));
|
|
384
514
|
else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
|
|
385
515
|
}
|
|
@@ -390,7 +520,7 @@ async function readPage(p) {
|
|
|
390
520
|
}
|
|
391
521
|
|
|
392
522
|
// src/parsers/wikilinks.ts
|
|
393
|
-
var FENCE =
|
|
523
|
+
var FENCE = /```[\s\S]*?```|`[^`\n]*`/g;
|
|
394
524
|
function extractBodyWikilinks(body) {
|
|
395
525
|
const stripped = body.replace(FENCE, "");
|
|
396
526
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -428,7 +558,7 @@ async function runGraphBuild(input) {
|
|
|
428
558
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
429
559
|
try {
|
|
430
560
|
await mkdir(dirname(input.out), { recursive: true });
|
|
431
|
-
await
|
|
561
|
+
await writeFile2(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
|
|
432
562
|
} catch (e) {
|
|
433
563
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
434
564
|
}
|
|
@@ -515,12 +645,21 @@ async function runOverlap(input) {
|
|
|
515
645
|
}
|
|
516
646
|
|
|
517
647
|
// src/utils/wiki-path.ts
|
|
518
|
-
import { join as
|
|
648
|
+
import { join as join4 } from "path";
|
|
519
649
|
|
|
520
650
|
// src/utils/dotenv.ts
|
|
521
|
-
import { readFile as readFile4, writeFile as
|
|
651
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
522
652
|
import { dirname as dirname2 } from "path";
|
|
523
|
-
var CONFIG_KEYS = [
|
|
653
|
+
var CONFIG_KEYS = [
|
|
654
|
+
"WIKI_PATH",
|
|
655
|
+
"WIKI_LANG",
|
|
656
|
+
"AUTO_COMMIT",
|
|
657
|
+
"BACKUP_ENDPOINT",
|
|
658
|
+
"BACKUP_BUCKET",
|
|
659
|
+
"BACKUP_REGION",
|
|
660
|
+
"BACKUP_ACCESS_KEY_ID",
|
|
661
|
+
"BACKUP_SECRET_ACCESS_KEY"
|
|
662
|
+
];
|
|
524
663
|
var _whitelist = new Set(CONFIG_KEYS);
|
|
525
664
|
var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
|
|
526
665
|
var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
|
|
@@ -559,7 +698,7 @@ async function parseDotenvFile(path) {
|
|
|
559
698
|
async function writeDotenv(filePath, entries, originalContent) {
|
|
560
699
|
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
561
700
|
await mkdir2(dirname2(filePath), { recursive: true });
|
|
562
|
-
await
|
|
701
|
+
await writeFile3(filePath, lines.join("\n") + "\n", "utf8");
|
|
563
702
|
}
|
|
564
703
|
function freshLines(entries) {
|
|
565
704
|
const out = [];
|
|
@@ -614,27 +753,27 @@ async function resolveInitTimePath(input) {
|
|
|
614
753
|
return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
|
|
615
754
|
}
|
|
616
755
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
617
|
-
const sw = await parseDotenvFile(
|
|
756
|
+
const sw = await parseDotenvFile(join4(input.home, ".skillwiki", ".env"));
|
|
618
757
|
if (sw.WIKI_PATH !== void 0) {
|
|
619
758
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
|
|
620
759
|
return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
|
|
621
760
|
}
|
|
622
761
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
623
|
-
const hermes = await parseDotenvFile(
|
|
762
|
+
const hermes = await parseDotenvFile(join4(input.home, ".hermes", ".env"));
|
|
624
763
|
if (hermes.WIKI_PATH !== void 0) {
|
|
625
764
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
|
|
626
765
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
627
766
|
}
|
|
628
767
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
|
|
629
768
|
if (input.cwd) {
|
|
630
|
-
const projCfg = await parseDotenvFile(
|
|
769
|
+
const projCfg = await parseDotenvFile(join4(input.cwd, ".skillwiki", ".env"));
|
|
631
770
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
632
771
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
633
772
|
return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
|
|
634
773
|
}
|
|
635
774
|
}
|
|
636
775
|
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
637
|
-
const fallback =
|
|
776
|
+
const fallback = join4(input.home, "wiki");
|
|
638
777
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
639
778
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
640
779
|
}
|
|
@@ -645,7 +784,7 @@ async function resolveRuntimePath(input) {
|
|
|
645
784
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
646
785
|
}
|
|
647
786
|
if (input.explain) chain.push({ source: "flag", matched: false });
|
|
648
|
-
const swGlobal = await parseDotenvFile(
|
|
787
|
+
const swGlobal = await parseDotenvFile(join4(input.home, ".skillwiki", ".env"));
|
|
649
788
|
const wikiName = input.wiki;
|
|
650
789
|
if (wikiName !== void 0 && wikiName.length > 0) {
|
|
651
790
|
if (wikiName.toLowerCase() === "default") {
|
|
@@ -689,7 +828,7 @@ async function resolveRuntimePath(input) {
|
|
|
689
828
|
}
|
|
690
829
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
691
830
|
if (input.cwd) {
|
|
692
|
-
const projCfg = await parseDotenvFile(
|
|
831
|
+
const projCfg = await parseDotenvFile(join4(input.cwd, ".skillwiki", ".env"));
|
|
693
832
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
694
833
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
695
834
|
return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
|
|
@@ -804,7 +943,7 @@ function simulateRemoval(adj, removed) {
|
|
|
804
943
|
|
|
805
944
|
// src/commands/audit.ts
|
|
806
945
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
807
|
-
import { dirname as dirname3, resolve, join as
|
|
946
|
+
import { dirname as dirname3, resolve as resolve2, join as join5 } from "path";
|
|
808
947
|
|
|
809
948
|
// src/parsers/citations.ts
|
|
810
949
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
@@ -911,12 +1050,12 @@ async function runAudit(input) {
|
|
|
911
1050
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
912
1051
|
const split = splitFrontmatter(text);
|
|
913
1052
|
const body = split.ok ? split.data.body : text;
|
|
914
|
-
const vault = await findVaultRoot(dirname3(
|
|
1053
|
+
const vault = await findVaultRoot(dirname3(resolve2(input.file)));
|
|
915
1054
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
916
1055
|
const markers = extractCitationMarkers(body);
|
|
917
1056
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
918
1057
|
try {
|
|
919
|
-
await stat2(
|
|
1058
|
+
await stat2(join5(vault, m.target));
|
|
920
1059
|
return { ...m, resolved: true };
|
|
921
1060
|
} catch {
|
|
922
1061
|
return { ...m, resolved: false };
|
|
@@ -927,7 +1066,7 @@ async function runAudit(input) {
|
|
|
927
1066
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
928
1067
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
929
1068
|
const broken = resolved.filter((m) => !m.resolved);
|
|
930
|
-
const footerMatch = body.match(/\n## Sources\n([\s\S]*)$/);
|
|
1069
|
+
const footerMatch = body.match(/\r?\n## Sources\r?\n([\s\S]*)$/);
|
|
931
1070
|
let footer_consistency;
|
|
932
1071
|
if (footerMatch) {
|
|
933
1072
|
const footerTargets = /* @__PURE__ */ new Set();
|
|
@@ -961,7 +1100,7 @@ async function findVaultRoot(start) {
|
|
|
961
1100
|
let cur = start;
|
|
962
1101
|
for (let i = 0; i < 20; i++) {
|
|
963
1102
|
try {
|
|
964
|
-
await stat2(
|
|
1103
|
+
await stat2(join5(cur, "SCHEMA.md"));
|
|
965
1104
|
return cur;
|
|
966
1105
|
} catch {
|
|
967
1106
|
}
|
|
@@ -971,13 +1110,53 @@ async function findVaultRoot(start) {
|
|
|
971
1110
|
}
|
|
972
1111
|
return null;
|
|
973
1112
|
}
|
|
1113
|
+
function stripWikilink(s) {
|
|
1114
|
+
return s.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
|
|
1115
|
+
}
|
|
1116
|
+
async function validateCompoundReferences(vault) {
|
|
1117
|
+
const scan = await scanVault(vault);
|
|
1118
|
+
if (!scan.ok) return scan;
|
|
1119
|
+
const slugToPage = /* @__PURE__ */ new Map();
|
|
1120
|
+
const pathToPage = /* @__PURE__ */ new Map();
|
|
1121
|
+
for (const p of scan.data.workItems) {
|
|
1122
|
+
slugToPage.set(p.relPath.replace(/\.md$/, "").split("/").pop().toLowerCase(), p);
|
|
1123
|
+
pathToPage.set(p.relPath, p);
|
|
1124
|
+
}
|
|
1125
|
+
const findings = [];
|
|
1126
|
+
for (const cp of scan.data.compound) {
|
|
1127
|
+
const text = await readPage(cp);
|
|
1128
|
+
const fm = extractFrontmatter(text);
|
|
1129
|
+
if (!fm.ok) continue;
|
|
1130
|
+
const projectRaw = fm.data.project;
|
|
1131
|
+
const workItems = fm.data.work_items;
|
|
1132
|
+
if (!projectRaw || !workItems?.length) continue;
|
|
1133
|
+
const projSlug = stripWikilink(String(projectRaw));
|
|
1134
|
+
for (const wi of workItems) {
|
|
1135
|
+
const target = stripWikilink(wi);
|
|
1136
|
+
const withExt = target.endsWith(".md") ? target : target + ".md";
|
|
1137
|
+
const resolved = pathToPage.get(withExt) ?? slugToPage.get(target.split("/").pop().replace(/\.md$/, "").toLowerCase());
|
|
1138
|
+
if (!resolved) {
|
|
1139
|
+
findings.push({ compound: cp.relPath, work_item: wi, kind: "missing", detail: `no work item found for [[${target}]]` });
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const wiFm = extractFrontmatter(await readPage(resolved));
|
|
1143
|
+
if (wiFm.ok && wiFm.data.project) {
|
|
1144
|
+
const wiProj = stripWikilink(String(wiFm.data.project));
|
|
1145
|
+
if (wiProj !== projSlug) {
|
|
1146
|
+
findings.push({ compound: cp.relPath, work_item: wi, kind: "cross_project", detail: `compound project [[${projSlug}]] != work_item project [[${wiProj}]]` });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return ok(findings);
|
|
1152
|
+
}
|
|
974
1153
|
|
|
975
1154
|
// src/commands/install.ts
|
|
976
|
-
import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
977
|
-
import { join as
|
|
1155
|
+
import { readdir as readdir2, stat as stat4, symlink, unlink, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
|
|
1156
|
+
import { join as join6, resolve as resolve3, dirname as dirname5 } from "path";
|
|
978
1157
|
|
|
979
1158
|
// src/utils/install-fs.ts
|
|
980
|
-
import { copyFile, mkdir as mkdir3, rename, writeFile as
|
|
1159
|
+
import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile4, stat as stat3 } from "fs/promises";
|
|
981
1160
|
import { dirname as dirname4 } from "path";
|
|
982
1161
|
async function atomicCopyWithBackup(src, dst) {
|
|
983
1162
|
await mkdir3(dirname4(dst), { recursive: true });
|
|
@@ -1000,10 +1179,36 @@ async function atomicCopyWithBackup(src, dst) {
|
|
|
1000
1179
|
async function writeManifest(path, m) {
|
|
1001
1180
|
await mkdir3(dirname4(path), { recursive: true });
|
|
1002
1181
|
const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
|
|
1003
|
-
await
|
|
1182
|
+
await writeFile4(path, JSON.stringify(enriched, null, 2));
|
|
1004
1183
|
}
|
|
1005
1184
|
|
|
1006
1185
|
// src/commands/install.ts
|
|
1186
|
+
function parseSkillMeta(content) {
|
|
1187
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1188
|
+
const meta = { name: "" };
|
|
1189
|
+
if (!fmMatch) return meta;
|
|
1190
|
+
const fm = fmMatch[1];
|
|
1191
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
1192
|
+
if (nameMatch) meta.name = nameMatch[1].trim();
|
|
1193
|
+
const versionMatch = fm.match(/^version:\s*(.+)$/m);
|
|
1194
|
+
if (versionMatch) meta.version = versionMatch[1].trim();
|
|
1195
|
+
const depMatch = fm.match(/^deprecated:\s*(.+)$/m);
|
|
1196
|
+
if (depMatch && /^(true|yes)$/i.test(depMatch[1].trim())) meta.deprecated = true;
|
|
1197
|
+
return meta;
|
|
1198
|
+
}
|
|
1199
|
+
async function createSymlink(src, dst) {
|
|
1200
|
+
await mkdir4(dirname5(dst), { recursive: true });
|
|
1201
|
+
try {
|
|
1202
|
+
await unlink(dst);
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
await symlink(resolve3(src), dst);
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
return err("SYMLINK_FAILED", { message: String(e) });
|
|
1209
|
+
}
|
|
1210
|
+
return ok({ linked: true });
|
|
1211
|
+
}
|
|
1007
1212
|
async function runInstall(input) {
|
|
1008
1213
|
let entries;
|
|
1009
1214
|
try {
|
|
@@ -1016,45 +1221,85 @@ async function runInstall(input) {
|
|
|
1016
1221
|
}
|
|
1017
1222
|
const installed = [];
|
|
1018
1223
|
const backed_up = [];
|
|
1224
|
+
const version_warnings = [];
|
|
1225
|
+
const skillMetas = {};
|
|
1019
1226
|
for (const name of entries) {
|
|
1020
|
-
const src =
|
|
1021
|
-
const dst =
|
|
1227
|
+
const src = join6(input.skillsRoot, name, "SKILL.md");
|
|
1228
|
+
const dst = join6(input.target, name, "SKILL.md");
|
|
1022
1229
|
try {
|
|
1023
1230
|
await stat4(src);
|
|
1024
1231
|
} catch {
|
|
1025
1232
|
return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { missing: src }) };
|
|
1026
1233
|
}
|
|
1234
|
+
try {
|
|
1235
|
+
const content = await readFile6(src, "utf8");
|
|
1236
|
+
const meta = parseSkillMeta(content);
|
|
1237
|
+
meta.name = meta.name || name;
|
|
1238
|
+
skillMetas[name] = meta;
|
|
1239
|
+
if (meta.deprecated) {
|
|
1240
|
+
version_warnings.push(`${name}: DEPRECATED \u2014 will be removed in a future release`);
|
|
1241
|
+
}
|
|
1242
|
+
if (!input.dryRun) {
|
|
1243
|
+
try {
|
|
1244
|
+
const existingContent = await readFile6(dst, "utf8");
|
|
1245
|
+
const existingMeta = parseSkillMeta(existingContent);
|
|
1246
|
+
if (existingMeta.version && meta.version && existingMeta.version !== meta.version) {
|
|
1247
|
+
version_warnings.push(`${name}: version changed ${existingMeta.version} \u2192 ${meta.version}`);
|
|
1248
|
+
}
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1027
1254
|
if (input.dryRun) {
|
|
1028
1255
|
installed.push(dst);
|
|
1029
1256
|
continue;
|
|
1030
1257
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1258
|
+
if (input.symlink) {
|
|
1259
|
+
const r = await createSymlink(src, dst);
|
|
1260
|
+
if (!r.ok) return { exitCode: ExitCode.SYMLINK_FAILED, result: r };
|
|
1261
|
+
installed.push(dst);
|
|
1262
|
+
} else {
|
|
1263
|
+
const r = await atomicCopyWithBackup(src, dst);
|
|
1264
|
+
if (!r.ok) return { exitCode: ExitCode.ATOMIC_COPY_FAILED, result: r };
|
|
1265
|
+
installed.push(dst);
|
|
1266
|
+
if (r.data.backupPath) backed_up.push(r.data.backupPath);
|
|
1267
|
+
}
|
|
1035
1268
|
}
|
|
1036
|
-
const binSrc =
|
|
1269
|
+
const binSrc = join6(input.skillsRoot, "bin", "skillwiki");
|
|
1037
1270
|
try {
|
|
1038
1271
|
await stat4(binSrc);
|
|
1039
|
-
const binDst =
|
|
1272
|
+
const binDst = join6(input.target, "bin", "skillwiki");
|
|
1040
1273
|
if (!input.dryRun) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1274
|
+
if (input.symlink) {
|
|
1275
|
+
const r = await createSymlink(binSrc, binDst);
|
|
1276
|
+
if (!r.ok) return { exitCode: ExitCode.SYMLINK_FAILED, result: r };
|
|
1277
|
+
installed.push(binDst);
|
|
1278
|
+
} else {
|
|
1279
|
+
const r = await atomicCopyWithBackup(binSrc, binDst);
|
|
1280
|
+
if (!r.ok) return { exitCode: ExitCode.ATOMIC_COPY_FAILED, result: r };
|
|
1281
|
+
installed.push(binDst);
|
|
1282
|
+
if (r.data.backupPath) backed_up.push(r.data.backupPath);
|
|
1283
|
+
}
|
|
1045
1284
|
} else {
|
|
1046
1285
|
installed.push(binDst);
|
|
1047
1286
|
}
|
|
1048
1287
|
} catch {
|
|
1049
1288
|
}
|
|
1050
|
-
const manifest_path =
|
|
1051
|
-
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
1289
|
+
const manifest_path = join6(input.target, "wiki-manifest.json");
|
|
1290
|
+
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up, symlink: input.symlink || void 0, skills: skillMetas });
|
|
1291
|
+
const mode = input.symlink ? "symlink (dev mode)" : "copy";
|
|
1052
1292
|
const hintLines = [
|
|
1053
|
-
`installed: ${installed.length}`,
|
|
1293
|
+
`installed: ${installed.length} (${mode})`,
|
|
1054
1294
|
input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
|
|
1055
1295
|
`manifest: ${manifest_path}`
|
|
1056
1296
|
];
|
|
1057
|
-
|
|
1297
|
+
if (version_warnings.length > 0) {
|
|
1298
|
+
hintLines.push(`version warnings: ${version_warnings.length}`);
|
|
1299
|
+
for (const w of version_warnings) hintLines.push(` ${w}`);
|
|
1300
|
+
}
|
|
1301
|
+
const exitCode = version_warnings.length > 0 ? ExitCode.SKILL_VERSION_MISMATCH : ExitCode.OK;
|
|
1302
|
+
return { exitCode, result: ok({ installed, backed_up, manifest_path, version_warnings, humanHint: hintLines.join("\n") }) };
|
|
1058
1303
|
}
|
|
1059
1304
|
|
|
1060
1305
|
// src/commands/path.ts
|
|
@@ -1083,7 +1328,7 @@ async function runPath(input) {
|
|
|
1083
1328
|
}
|
|
1084
1329
|
|
|
1085
1330
|
// src/utils/lang.ts
|
|
1086
|
-
import { join as
|
|
1331
|
+
import { join as join7 } from "path";
|
|
1087
1332
|
var ALIASES = {
|
|
1088
1333
|
english: "en",
|
|
1089
1334
|
en: "en",
|
|
@@ -1106,7 +1351,7 @@ async function resolveLang(input) {
|
|
|
1106
1351
|
if (input.envValue !== void 0 && input.envValue.length > 0) {
|
|
1107
1352
|
return { value: input.envValue, source: "env", canonical: normalizeLang(input.envValue) };
|
|
1108
1353
|
}
|
|
1109
|
-
const dotenv = await parseDotenvFile(
|
|
1354
|
+
const dotenv = await parseDotenvFile(join7(input.home, ".skillwiki", ".env"));
|
|
1110
1355
|
if (dotenv.WIKI_LANG !== void 0) {
|
|
1111
1356
|
return { value: dotenv.WIKI_LANG, source: "skillwiki-dotenv", canonical: normalizeLang(dotenv.WIKI_LANG) };
|
|
1112
1357
|
}
|
|
@@ -1114,7 +1359,7 @@ async function resolveLang(input) {
|
|
|
1114
1359
|
}
|
|
1115
1360
|
|
|
1116
1361
|
// src/commands/lang.ts
|
|
1117
|
-
import { join as
|
|
1362
|
+
import { join as join8 } from "path";
|
|
1118
1363
|
async function runLang(input) {
|
|
1119
1364
|
const resolved = await resolveLang({ flag: input.flag, envValue: input.envValue, home: input.home });
|
|
1120
1365
|
let chain;
|
|
@@ -1123,7 +1368,7 @@ async function runLang(input) {
|
|
|
1123
1368
|
{ source: "flag", matched: input.flag !== void 0 && input.flag.length > 0, value: input.flag },
|
|
1124
1369
|
{ source: "env", matched: input.envValue !== void 0 && input.envValue.length > 0, value: input.envValue }
|
|
1125
1370
|
];
|
|
1126
|
-
const sw = await parseDotenvFile(
|
|
1371
|
+
const sw = await parseDotenvFile(join8(input.home, ".skillwiki", ".env"));
|
|
1127
1372
|
chain.push({ source: "skillwiki-dotenv", matched: sw.WIKI_LANG !== void 0, value: sw.WIKI_LANG });
|
|
1128
1373
|
chain.push({ source: "default", matched: resolved.source === "default", value: "en" });
|
|
1129
1374
|
}
|
|
@@ -1140,8 +1385,8 @@ async function runLang(input) {
|
|
|
1140
1385
|
}
|
|
1141
1386
|
|
|
1142
1387
|
// src/commands/init.ts
|
|
1143
|
-
import { mkdir as
|
|
1144
|
-
import { join as
|
|
1388
|
+
import { mkdir as mkdir5, readFile as readFile7, readdir as readdir3, writeFile as writeFile5 } from "fs/promises";
|
|
1389
|
+
import { join as join10 } from "path";
|
|
1145
1390
|
|
|
1146
1391
|
// src/parsers/taxonomy.ts
|
|
1147
1392
|
import yaml2 from "js-yaml";
|
|
@@ -1168,6 +1413,48 @@ function extractTaxonomy(schemaText) {
|
|
|
1168
1413
|
return ok(tax);
|
|
1169
1414
|
}
|
|
1170
1415
|
|
|
1416
|
+
// src/utils/last-op.ts
|
|
1417
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
1418
|
+
import { join as join9 } from "path";
|
|
1419
|
+
var LAST_OP_DIR = ".skillwiki";
|
|
1420
|
+
var LAST_OP_FILE = "last-op.json";
|
|
1421
|
+
function lastOpPath(vault) {
|
|
1422
|
+
return join9(vault, LAST_OP_DIR, LAST_OP_FILE);
|
|
1423
|
+
}
|
|
1424
|
+
function readLastOp(vault) {
|
|
1425
|
+
const p = lastOpPath(vault);
|
|
1426
|
+
if (!existsSync(p)) return [];
|
|
1427
|
+
try {
|
|
1428
|
+
const raw = readFileSync2(p, "utf8");
|
|
1429
|
+
const parsed = JSON.parse(raw);
|
|
1430
|
+
if (!Array.isArray(parsed)) {
|
|
1431
|
+
unlinkSync(p);
|
|
1432
|
+
return [];
|
|
1433
|
+
}
|
|
1434
|
+
return parsed;
|
|
1435
|
+
} catch {
|
|
1436
|
+
try {
|
|
1437
|
+
unlinkSync(p);
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
return [];
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function appendLastOp(vault, entry) {
|
|
1444
|
+
const existing = readLastOp(vault);
|
|
1445
|
+
existing.push(entry);
|
|
1446
|
+
const dir = join9(vault, LAST_OP_DIR);
|
|
1447
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1448
|
+
writeFileSync(lastOpPath(vault), JSON.stringify(existing, null, 2), "utf8");
|
|
1449
|
+
}
|
|
1450
|
+
function clearLastOp(vault) {
|
|
1451
|
+
const p = lastOpPath(vault);
|
|
1452
|
+
try {
|
|
1453
|
+
unlinkSync(p);
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1171
1458
|
// src/commands/init.ts
|
|
1172
1459
|
var DEFAULT_TAXONOMY = [
|
|
1173
1460
|
"research",
|
|
@@ -1210,13 +1497,13 @@ async function discoverTagsFromPages(target, knownSlugs) {
|
|
|
1210
1497
|
for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
|
|
1211
1498
|
let entries;
|
|
1212
1499
|
try {
|
|
1213
|
-
entries = (await readdir3(
|
|
1500
|
+
entries = (await readdir3(join10(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
1214
1501
|
} catch {
|
|
1215
1502
|
continue;
|
|
1216
1503
|
}
|
|
1217
1504
|
for (const file of entries) {
|
|
1218
1505
|
try {
|
|
1219
|
-
const text = await
|
|
1506
|
+
const text = await readFile7(join10(target, dir, file), "utf8");
|
|
1220
1507
|
const fm = extractFrontmatter(text);
|
|
1221
1508
|
if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
|
|
1222
1509
|
for (const t of fm.data.tags) {
|
|
@@ -1235,7 +1522,7 @@ async function runInit(input) {
|
|
|
1235
1522
|
const canonicalLang = langRes.canonical;
|
|
1236
1523
|
let oldSchemaText;
|
|
1237
1524
|
try {
|
|
1238
|
-
oldSchemaText = await
|
|
1525
|
+
oldSchemaText = await readFile7(join10(target, "SCHEMA.md"), "utf8");
|
|
1239
1526
|
} catch {
|
|
1240
1527
|
}
|
|
1241
1528
|
if (oldSchemaText && !input.force) {
|
|
@@ -1244,10 +1531,10 @@ async function runInit(input) {
|
|
|
1244
1531
|
result: err("INIT_TARGET_NOT_EMPTY", { target })
|
|
1245
1532
|
};
|
|
1246
1533
|
}
|
|
1247
|
-
const envPath =
|
|
1534
|
+
const envPath = join10(input.home, ".skillwiki", ".env");
|
|
1248
1535
|
let existingEnvRaw = "";
|
|
1249
1536
|
try {
|
|
1250
|
-
existingEnvRaw = await
|
|
1537
|
+
existingEnvRaw = await readFile7(envPath, "utf8");
|
|
1251
1538
|
} catch {
|
|
1252
1539
|
}
|
|
1253
1540
|
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
@@ -1268,9 +1555,9 @@ async function runInit(input) {
|
|
|
1268
1555
|
}
|
|
1269
1556
|
const created = [];
|
|
1270
1557
|
try {
|
|
1271
|
-
await
|
|
1558
|
+
await mkdir5(target, { recursive: true });
|
|
1272
1559
|
for (const d of VAULT_DIRS) {
|
|
1273
|
-
await
|
|
1560
|
+
await mkdir5(join10(target, d), { recursive: true });
|
|
1274
1561
|
created.push(d + "/");
|
|
1275
1562
|
}
|
|
1276
1563
|
} catch (e) {
|
|
@@ -1299,9 +1586,9 @@ async function runInit(input) {
|
|
|
1299
1586
|
const discovered_tags = discovered.length;
|
|
1300
1587
|
const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
|
|
1301
1588
|
try {
|
|
1302
|
-
const schemaTpl = await
|
|
1589
|
+
const schemaTpl = await readFile7(join10(input.templates, "SCHEMA.md"), "utf8");
|
|
1303
1590
|
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
1304
|
-
await
|
|
1591
|
+
await writeFile5(join10(target, "SCHEMA.md"), schema, "utf8");
|
|
1305
1592
|
created.push("SCHEMA.md");
|
|
1306
1593
|
} catch (e) {
|
|
1307
1594
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
@@ -1309,7 +1596,7 @@ async function runInit(input) {
|
|
|
1309
1596
|
const preserved = [];
|
|
1310
1597
|
async function writeOrPreserve(fileName, render) {
|
|
1311
1598
|
try {
|
|
1312
|
-
const existing = await
|
|
1599
|
+
const existing = await readFile7(join10(target, fileName), "utf8");
|
|
1313
1600
|
if (existing.split("\n").length > 10) {
|
|
1314
1601
|
preserved.push(fileName);
|
|
1315
1602
|
return void 0;
|
|
@@ -1317,7 +1604,7 @@ async function runInit(input) {
|
|
|
1317
1604
|
} catch {
|
|
1318
1605
|
}
|
|
1319
1606
|
try {
|
|
1320
|
-
await
|
|
1607
|
+
await writeFile5(join10(target, fileName), await render(), "utf8");
|
|
1321
1608
|
created.push(fileName);
|
|
1322
1609
|
return void 0;
|
|
1323
1610
|
} catch (e) {
|
|
@@ -1325,7 +1612,7 @@ async function runInit(input) {
|
|
|
1325
1612
|
}
|
|
1326
1613
|
}
|
|
1327
1614
|
const err1 = await writeOrPreserve("index.md", async () => {
|
|
1328
|
-
const tpl = await
|
|
1615
|
+
const tpl = await readFile7(join10(input.templates, "index.md"), "utf8");
|
|
1329
1616
|
return tpl.replace("{{INIT_DATE}}", today);
|
|
1330
1617
|
});
|
|
1331
1618
|
if (err1) return err1;
|
|
@@ -1342,7 +1629,6 @@ async function runInit(input) {
|
|
|
1342
1629
|
"---",
|
|
1343
1630
|
"source_url:",
|
|
1344
1631
|
"ingested: {{date:YYYY-MM-DD}}",
|
|
1345
|
-
"sha256: # run: skillwiki hash <this-file>",
|
|
1346
1632
|
"kind: # idea | bug | task | note | other",
|
|
1347
1633
|
'project: # optional: "[[slug]]"',
|
|
1348
1634
|
"---",
|
|
@@ -1352,7 +1638,7 @@ async function runInit(input) {
|
|
|
1352
1638
|
});
|
|
1353
1639
|
if (errTemplate) return errTemplate;
|
|
1354
1640
|
const err22 = await writeOrPreserve("log.md", async () => {
|
|
1355
|
-
const tpl = await
|
|
1641
|
+
const tpl = await readFile7(join10(input.templates, "log.md"), "utf8");
|
|
1356
1642
|
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1357
1643
|
});
|
|
1358
1644
|
if (err22) return err22;
|
|
@@ -1384,6 +1670,14 @@ async function runInit(input) {
|
|
|
1384
1670
|
`discovered tags: ${discovered_tags}`,
|
|
1385
1671
|
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
1386
1672
|
].join("\n");
|
|
1673
|
+
if (created.length > 0) {
|
|
1674
|
+
appendLastOp(target, {
|
|
1675
|
+
operation: "init",
|
|
1676
|
+
summary: `initialized vault: ${domain}`,
|
|
1677
|
+
files: created,
|
|
1678
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1387
1681
|
return {
|
|
1388
1682
|
exitCode: ExitCode.OK,
|
|
1389
1683
|
result: ok({
|
|
@@ -1441,12 +1735,12 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
|
|
|
1441
1735
|
}
|
|
1442
1736
|
|
|
1443
1737
|
// src/commands/tag-audit.ts
|
|
1444
|
-
import { readFile as
|
|
1445
|
-
import { join as
|
|
1738
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1739
|
+
import { join as join11 } from "path";
|
|
1446
1740
|
async function runTagAudit(input) {
|
|
1447
1741
|
const scan = await scanVault(input.vault);
|
|
1448
1742
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1449
|
-
const schemaText = await
|
|
1743
|
+
const schemaText = await readFile8(join11(input.vault, "SCHEMA.md"), "utf8");
|
|
1450
1744
|
const tax = extractTaxonomy(schemaText);
|
|
1451
1745
|
if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
|
|
1452
1746
|
const allowed = new Set(tax.data);
|
|
@@ -1470,14 +1764,14 @@ async function runTagAudit(input) {
|
|
|
1470
1764
|
}
|
|
1471
1765
|
|
|
1472
1766
|
// src/commands/index-check.ts
|
|
1473
|
-
import { readFile as
|
|
1474
|
-
import { join as
|
|
1767
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1768
|
+
import { join as join12 } from "path";
|
|
1475
1769
|
async function runIndexCheck(input) {
|
|
1476
1770
|
const scan = await scanVault(input.vault);
|
|
1477
1771
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1478
1772
|
let indexText = "";
|
|
1479
1773
|
try {
|
|
1480
|
-
indexText = await
|
|
1774
|
+
indexText = await readFile9(join12(input.vault, "index.md"), "utf8");
|
|
1481
1775
|
} catch {
|
|
1482
1776
|
}
|
|
1483
1777
|
const indexSlugsLower = /* @__PURE__ */ new Map();
|
|
@@ -1486,12 +1780,18 @@ async function runIndexCheck(input) {
|
|
|
1486
1780
|
indexSlugsLower.set(tail.toLowerCase(), tail);
|
|
1487
1781
|
}
|
|
1488
1782
|
const fileSlugs = /* @__PURE__ */ new Map();
|
|
1783
|
+
const requiredSlugs = /* @__PURE__ */ new Map();
|
|
1489
1784
|
for (const p of scan.data.typedKnowledge) {
|
|
1490
1785
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1491
1786
|
fileSlugs.set(slug, p.relPath);
|
|
1787
|
+
requiredSlugs.set(slug, p.relPath);
|
|
1788
|
+
}
|
|
1789
|
+
for (const p of scan.data.compound) {
|
|
1790
|
+
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1791
|
+
fileSlugs.set(slug, p.relPath);
|
|
1492
1792
|
}
|
|
1493
1793
|
const missing_from_index = [];
|
|
1494
|
-
for (const [slug, relPath] of
|
|
1794
|
+
for (const [slug, relPath] of requiredSlugs.entries()) {
|
|
1495
1795
|
if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
|
|
1496
1796
|
}
|
|
1497
1797
|
const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
|
|
@@ -1510,44 +1810,176 @@ async function runIndexCheck(input) {
|
|
|
1510
1810
|
}
|
|
1511
1811
|
|
|
1512
1812
|
// src/commands/stale.ts
|
|
1513
|
-
import { readFile as
|
|
1514
|
-
import { join as
|
|
1515
|
-
function
|
|
1516
|
-
|
|
1517
|
-
const db = Date.parse(b);
|
|
1518
|
-
return Math.round((db - da) / 864e5);
|
|
1813
|
+
import { readdir as readdir4, rename as rename2, mkdir as mkdir6, readFile as readFile10 } from "fs/promises";
|
|
1814
|
+
import { join as join13 } from "path";
|
|
1815
|
+
function daysSince(isoDate2) {
|
|
1816
|
+
return Math.floor((Date.now() - Date.parse(isoDate2)) / 864e5);
|
|
1519
1817
|
}
|
|
1520
1818
|
async function runStale(input) {
|
|
1521
1819
|
const scan = await scanVault(input.vault);
|
|
1522
1820
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1821
|
+
const staleTranscripts = [];
|
|
1822
|
+
const incompleteWorkItems = [];
|
|
1823
|
+
const archived = [];
|
|
1824
|
+
const workDirs = /* @__PURE__ */ new Map();
|
|
1825
|
+
const projectsDir = join13(input.vault, "projects");
|
|
1826
|
+
let projectSlugs = [];
|
|
1827
|
+
try {
|
|
1828
|
+
projectSlugs = (await readdir4(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1829
|
+
} catch {
|
|
1830
|
+
}
|
|
1831
|
+
for (const slug of projectSlugs) {
|
|
1832
|
+
const workPath = join13(projectsDir, slug, "work");
|
|
1833
|
+
let entries;
|
|
1834
|
+
try {
|
|
1835
|
+
entries = await readdir4(workPath, { withFileTypes: true });
|
|
1836
|
+
} catch {
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
for (const e of entries) {
|
|
1840
|
+
if (!e.isDirectory()) continue;
|
|
1841
|
+
const relDir = `projects/${slug}/work/${e.name}`;
|
|
1842
|
+
const absDir = join13(workPath, e.name);
|
|
1843
|
+
let status = "";
|
|
1844
|
+
let files;
|
|
1533
1845
|
try {
|
|
1534
|
-
|
|
1846
|
+
files = await readdir4(absDir);
|
|
1535
1847
|
} catch {
|
|
1848
|
+
workDirs.set(relDir, "");
|
|
1536
1849
|
continue;
|
|
1537
1850
|
}
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1851
|
+
for (const f of files) {
|
|
1852
|
+
if (!f.endsWith(".md")) continue;
|
|
1853
|
+
try {
|
|
1854
|
+
const fm = extractFrontmatter(await readFile10(join13(absDir, f), "utf8"));
|
|
1855
|
+
if (fm.ok && typeof fm.data.status === "string") {
|
|
1856
|
+
status = fm.data.status;
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
} catch {
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
workDirs.set(relDir, status);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
const transcripts = scan.data.raw.filter((p) => p.relPath.startsWith("raw/transcripts/") && p.relPath.endsWith(".md"));
|
|
1866
|
+
for (const t of transcripts) {
|
|
1867
|
+
const datePrefix = t.relPath.split("/").pop().slice(0, 10);
|
|
1868
|
+
for (const [dir, status] of workDirs) {
|
|
1869
|
+
if (dir.split("/").pop().startsWith(datePrefix) && (status === "done" || status === "invalid")) {
|
|
1870
|
+
staleTranscripts.push({ path: t.relPath, reason: `work item ${dir} is ${status}` });
|
|
1871
|
+
break;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
const doneWorkItems = [];
|
|
1876
|
+
for (const [relDir, status] of workDirs) {
|
|
1877
|
+
const dirName = relDir.split("/").pop();
|
|
1878
|
+
const dateStr = dirName.slice(0, 10);
|
|
1879
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) continue;
|
|
1880
|
+
if (daysSince(dateStr) < input.days) continue;
|
|
1881
|
+
let files;
|
|
1882
|
+
try {
|
|
1883
|
+
files = await readdir4(join13(input.vault, relDir));
|
|
1884
|
+
} catch {
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
const hasSpec = files.includes("spec.md"), hasPlan = files.includes("plan.md"), hasWI = files.includes("work-item.md");
|
|
1888
|
+
if (status === "done") {
|
|
1889
|
+
doneWorkItems.push({ path: relDir, reason: "completed \u2014 should be archived" });
|
|
1890
|
+
} else if (status === "invalid") {
|
|
1891
|
+
doneWorkItems.push({ path: relDir, reason: "invalid \u2014 should be archived" });
|
|
1892
|
+
} else if (hasSpec && !hasPlan) {
|
|
1893
|
+
incompleteWorkItems.push({ path: relDir, reason: "has spec but no plan" });
|
|
1894
|
+
} else if (hasWI && !hasSpec && !hasPlan) {
|
|
1895
|
+
incompleteWorkItems.push({ path: relDir, reason: "only work-item.md, no spec or plan" });
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
const stale = [];
|
|
1899
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1900
|
+
try {
|
|
1901
|
+
const text = await readFile10(join13(input.vault, page.relPath), "utf8");
|
|
1902
|
+
const fm = extractFrontmatter(text);
|
|
1903
|
+
if (fm.ok && typeof fm.data.updated === "string") {
|
|
1904
|
+
const age = daysSince(fm.data.updated);
|
|
1905
|
+
if (age >= input.days) {
|
|
1906
|
+
stale.push({ page: page.relPath, reason: `updated ${age} days ago (threshold: ${input.days})` });
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
} catch {
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (input.archive) {
|
|
1913
|
+
const archiveDir = join13(input.vault, "_archive", (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
1914
|
+
await mkdir6(archiveDir, { recursive: true });
|
|
1915
|
+
const citedRawPaths = /* @__PURE__ */ new Set();
|
|
1916
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1917
|
+
const text = await readFile10(join13(input.vault, page.relPath), "utf8").catch(() => "");
|
|
1918
|
+
for (const line of text.split("\n")) {
|
|
1919
|
+
for (const m of line.matchAll(/\^\[(raw\/[^\]]+)\]/g)) {
|
|
1920
|
+
citedRawPaths.add(m[1]);
|
|
1921
|
+
}
|
|
1922
|
+
for (const m of line.matchAll(/raw\/[^\s,\]"]+\.md/g)) {
|
|
1923
|
+
citedRawPaths.add(m[0]);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
for (const t of staleTranscripts) {
|
|
1928
|
+
if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
|
|
1929
|
+
const dest = join13(archiveDir, t.path.split("/").pop());
|
|
1930
|
+
try {
|
|
1931
|
+
await rename2(join13(input.vault, t.path), dest);
|
|
1932
|
+
archived.push(t.path);
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1542
1935
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1936
|
+
for (const w of [...incompleteWorkItems, ...doneWorkItems]) {
|
|
1937
|
+
const parts = w.path.split("/");
|
|
1938
|
+
if (parts.length >= 4 && parts[0] === "projects") {
|
|
1939
|
+
const slug = parts[1];
|
|
1940
|
+
const itemName = parts[3];
|
|
1941
|
+
const histDir = join13(input.vault, "projects", slug, "history", "archived-work");
|
|
1942
|
+
await mkdir6(histDir, { recursive: true });
|
|
1943
|
+
const dest = join13(histDir, itemName);
|
|
1944
|
+
try {
|
|
1945
|
+
await rename2(join13(input.vault, w.path), dest);
|
|
1946
|
+
archived.push(w.path);
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
} else {
|
|
1950
|
+
const dest = join13(archiveDir, w.path.replace(/\//g, "_"));
|
|
1951
|
+
try {
|
|
1952
|
+
await rename2(join13(input.vault, w.path), dest);
|
|
1953
|
+
archived.push(w.path);
|
|
1954
|
+
} catch {
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1547
1957
|
}
|
|
1548
1958
|
}
|
|
1549
|
-
if (
|
|
1550
|
-
|
|
1959
|
+
if (input.archive && archived.length > 0) {
|
|
1960
|
+
appendLastOp(input.vault, {
|
|
1961
|
+
operation: "stale-archive",
|
|
1962
|
+
summary: `archived ${archived.length} stale items`,
|
|
1963
|
+
files: archived,
|
|
1964
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
const total = stale.length + staleTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
|
|
1968
|
+
const hintLines = [];
|
|
1969
|
+
if (stale.length > 0) hintLines.push(`stale_pages: ${stale.length}`, ...stale.map((p) => ` ${p.page}: ${p.reason}`));
|
|
1970
|
+
if (staleTranscripts.length > 0) hintLines.push(`stale_transcripts: ${staleTranscripts.length}`, ...staleTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
|
|
1971
|
+
if (incompleteWorkItems.length > 0) hintLines.push(`incomplete_work_items: ${incompleteWorkItems.length}`, ...incompleteWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
1972
|
+
if (doneWorkItems.length > 0) hintLines.push(`done_work_items: ${doneWorkItems.length}`, ...doneWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
|
|
1973
|
+
if (archived.length > 0) hintLines.push(`archived: ${archived.length}`, ...archived.map((a) => ` ${a}`));
|
|
1974
|
+
if (hintLines.length === 0) hintLines.push("no stale transcripts or incomplete work items");
|
|
1975
|
+
return { exitCode: total > 0 ? ExitCode.STALE_PAGE : ExitCode.OK, result: ok({
|
|
1976
|
+
stale: [...stale, ...staleTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...incompleteWorkItems.map((w) => ({ page: w.path, reason: w.reason })), ...doneWorkItems.map((w) => ({ page: w.path, reason: w.reason }))],
|
|
1977
|
+
stale_transcripts: staleTranscripts,
|
|
1978
|
+
incomplete_work_items: incompleteWorkItems,
|
|
1979
|
+
done_work_items: doneWorkItems,
|
|
1980
|
+
archived,
|
|
1981
|
+
humanHint: hintLines.join("\n")
|
|
1982
|
+
}) };
|
|
1551
1983
|
}
|
|
1552
1984
|
|
|
1553
1985
|
// src/commands/pagesize.ts
|
|
@@ -1567,19 +1999,19 @@ async function runPagesize(input) {
|
|
|
1567
1999
|
}
|
|
1568
2000
|
|
|
1569
2001
|
// src/commands/log-rotate.ts
|
|
1570
|
-
import { readFile as
|
|
1571
|
-
import { join as
|
|
2002
|
+
import { readFile as readFile11, rename as rename3, writeFile as writeFile6, stat as stat5 } from "fs/promises";
|
|
2003
|
+
import { join as join14 } from "path";
|
|
1572
2004
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
1573
2005
|
async function runLogRotate(input) {
|
|
1574
2006
|
try {
|
|
1575
|
-
await stat5(
|
|
2007
|
+
await stat5(join14(input.vault, "SCHEMA.md"));
|
|
1576
2008
|
} catch {
|
|
1577
2009
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
1578
2010
|
}
|
|
1579
|
-
const logPath =
|
|
2011
|
+
const logPath = join14(input.vault, "log.md");
|
|
1580
2012
|
let logText;
|
|
1581
2013
|
try {
|
|
1582
|
-
logText = await
|
|
2014
|
+
logText = await readFile11(logPath, "utf8");
|
|
1583
2015
|
} catch {
|
|
1584
2016
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
1585
2017
|
}
|
|
@@ -1596,9 +2028,9 @@ async function runLogRotate(input) {
|
|
|
1596
2028
|
}
|
|
1597
2029
|
const newestYear = matches[matches.length - 1][1];
|
|
1598
2030
|
const rotatedName = `log-${newestYear}.md`;
|
|
1599
|
-
const rotatedPath =
|
|
2031
|
+
const rotatedPath = join14(input.vault, rotatedName);
|
|
1600
2032
|
try {
|
|
1601
|
-
await
|
|
2033
|
+
await rename3(logPath, rotatedPath);
|
|
1602
2034
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1603
2035
|
const fresh = `# Vault Log
|
|
1604
2036
|
|
|
@@ -1608,15 +2040,23 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1608
2040
|
|
|
1609
2041
|
- Previous log moved to ${rotatedName}
|
|
1610
2042
|
`;
|
|
1611
|
-
await
|
|
2043
|
+
await writeFile6(logPath, fresh, "utf8");
|
|
1612
2044
|
} catch (e) {
|
|
1613
2045
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1614
2046
|
}
|
|
2047
|
+
appendLastOp(input.vault, {
|
|
2048
|
+
operation: "log-rotate",
|
|
2049
|
+
summary: `rotated ${entries} entries to ${rotatedName}`,
|
|
2050
|
+
files: ["log.md", rotatedName],
|
|
2051
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2052
|
+
});
|
|
1615
2053
|
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1616
2054
|
}
|
|
1617
2055
|
|
|
1618
2056
|
// src/commands/lint.ts
|
|
1619
|
-
import {
|
|
2057
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2058
|
+
import { readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
|
|
2059
|
+
import { join as join17 } from "path";
|
|
1620
2060
|
|
|
1621
2061
|
// src/commands/topic-map-check.ts
|
|
1622
2062
|
var DEFAULT_THRESHOLD = 200;
|
|
@@ -1638,13 +2078,13 @@ async function runTopicMapCheck(input) {
|
|
|
1638
2078
|
}
|
|
1639
2079
|
|
|
1640
2080
|
// src/commands/index-link-format.ts
|
|
1641
|
-
import { readFile as
|
|
1642
|
-
import { join as
|
|
2081
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
2082
|
+
import { join as join15 } from "path";
|
|
1643
2083
|
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
1644
2084
|
async function runIndexLinkFormat(input) {
|
|
1645
2085
|
let text = "";
|
|
1646
2086
|
try {
|
|
1647
|
-
text = await
|
|
2087
|
+
text = await readFile12(join15(input.vault, "index.md"), "utf8");
|
|
1648
2088
|
} catch {
|
|
1649
2089
|
}
|
|
1650
2090
|
const markdown_links = [];
|
|
@@ -1657,8 +2097,8 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
|
1657
2097
|
}
|
|
1658
2098
|
|
|
1659
2099
|
// src/commands/dedup.ts
|
|
1660
|
-
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
1661
|
-
import { join as
|
|
2100
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
2101
|
+
import { join as join16 } from "path";
|
|
1662
2102
|
async function runDedup(input) {
|
|
1663
2103
|
const scan = await scanVault(input.vault);
|
|
1664
2104
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1686,7 +2126,7 @@ async function runDedup(input) {
|
|
|
1686
2126
|
}
|
|
1687
2127
|
}
|
|
1688
2128
|
for (const page of scan.data.typedKnowledge) {
|
|
1689
|
-
const text =
|
|
2129
|
+
const text = readFileSync3(join16(input.vault, page.relPath), "utf-8");
|
|
1690
2130
|
let updated = text;
|
|
1691
2131
|
let changed = false;
|
|
1692
2132
|
for (const [oldPath, newPath] of replacements) {
|
|
@@ -1704,19 +2144,27 @@ async function runDedup(input) {
|
|
|
1704
2144
|
}
|
|
1705
2145
|
}
|
|
1706
2146
|
if (changed) {
|
|
1707
|
-
|
|
2147
|
+
writeFileSync2(join16(input.vault, page.relPath), updated);
|
|
1708
2148
|
rewired.push(page.relPath);
|
|
1709
2149
|
}
|
|
1710
2150
|
}
|
|
1711
2151
|
for (const [oldPath] of replacements) {
|
|
1712
|
-
const fullPath =
|
|
2152
|
+
const fullPath = join16(input.vault, oldPath);
|
|
1713
2153
|
try {
|
|
1714
|
-
|
|
2154
|
+
unlinkSync2(fullPath);
|
|
1715
2155
|
removed.push(oldPath);
|
|
1716
2156
|
} catch {
|
|
1717
2157
|
}
|
|
1718
2158
|
}
|
|
1719
2159
|
}
|
|
2160
|
+
if (input.apply && (rewired.length > 0 || removed.length > 0)) {
|
|
2161
|
+
appendLastOp(input.vault, {
|
|
2162
|
+
operation: "dedup",
|
|
2163
|
+
summary: `rewired ${rewired.length} pages, removed ${removed.length} duplicates`,
|
|
2164
|
+
files: [...rewired, ...removed],
|
|
2165
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
1720
2168
|
const exitCode = duplicates.length > 0 ? input.apply ? ExitCode.DEDUP_APPLIED : ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
|
|
1721
2169
|
const hintLines = [`scanned: ${totalFiles} raw files`];
|
|
1722
2170
|
if (duplicates.length > 0) {
|
|
@@ -1749,8 +2197,25 @@ function hasDuplicateFrontmatter(body) {
|
|
|
1749
2197
|
}
|
|
1750
2198
|
return false;
|
|
1751
2199
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
2200
|
+
function extractSourceEntries(rawFm) {
|
|
2201
|
+
const lines = rawFm.split(/\r?\n/);
|
|
2202
|
+
const sourcesLineIdx = lines.findIndex((l) => /^sources:/.test(l));
|
|
2203
|
+
if (sourcesLineIdx === -1) return [];
|
|
2204
|
+
const sourcesLine = lines[sourcesLineIdx].trim();
|
|
2205
|
+
const inlineMatch = sourcesLine.match(/^sources:\s*\[(.+)]\s*$/);
|
|
2206
|
+
if (inlineMatch) {
|
|
2207
|
+
return [...inlineMatch[1].matchAll(/"[^"]*"|'[^']*'|[^,\s]\S*/g)].map((m) => m[0].replace(/,\s*$/, ""));
|
|
2208
|
+
}
|
|
2209
|
+
const entries = [];
|
|
2210
|
+
for (let i = sourcesLineIdx + 1; i < lines.length; i++) {
|
|
2211
|
+
const line = lines[i];
|
|
2212
|
+
if (!/^\s+- /.test(line)) break;
|
|
2213
|
+
entries.push(line.replace(/^\s+- /, "").trim());
|
|
2214
|
+
}
|
|
2215
|
+
return entries;
|
|
2216
|
+
}
|
|
2217
|
+
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
|
|
2218
|
+
var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview"];
|
|
1754
2219
|
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation"];
|
|
1755
2220
|
async function runLint(input) {
|
|
1756
2221
|
const buckets = {};
|
|
@@ -1777,8 +2242,12 @@ async function runLint(input) {
|
|
|
1777
2242
|
if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
|
|
1778
2243
|
buckets.index_link_format = linkFmt.result.data.markdown_links;
|
|
1779
2244
|
}
|
|
1780
|
-
const
|
|
1781
|
-
if (
|
|
2245
|
+
const staleResult = await runStale({ vault: input.vault, days: input.days });
|
|
2246
|
+
if (staleResult.result.ok) {
|
|
2247
|
+
const st = staleResult.result.data;
|
|
2248
|
+
const staleList = [...st.stale_transcripts.map((t) => t.path), ...st.incomplete_work_items.map((w) => w.path), ...(st.done_work_items ?? []).map((w) => w.path)];
|
|
2249
|
+
if (staleList.length > 0) buckets.stale_page = staleList;
|
|
2250
|
+
}
|
|
1782
2251
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
1783
2252
|
if (pagesize.result.ok && pagesize.result.data.oversized.length > 0) buckets.page_too_large = pagesize.result.data.oversized;
|
|
1784
2253
|
const rotate = await runLogRotate({ vault: input.vault, threshold: input.logThreshold, apply: false });
|
|
@@ -1796,6 +2265,8 @@ async function runLint(input) {
|
|
|
1796
2265
|
}
|
|
1797
2266
|
const dedup = await runDedup({ vault: input.vault });
|
|
1798
2267
|
if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
|
|
2268
|
+
const compoundRefs = await validateCompoundReferences(input.vault);
|
|
2269
|
+
if (compoundRefs.ok && compoundRefs.data.length > 0) buckets.compound_refs = compoundRefs.data;
|
|
1799
2270
|
const scan = await scanVault(input.vault);
|
|
1800
2271
|
const allPages = scan.ok ? [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound] : [];
|
|
1801
2272
|
const slugs = scan.ok ? buildSlugMap(allPages) : /* @__PURE__ */ new Map();
|
|
@@ -1807,6 +2278,7 @@ async function runLint(input) {
|
|
|
1807
2278
|
const noOverview = [];
|
|
1808
2279
|
const fmWikilinkFlags = [];
|
|
1809
2280
|
const wikilinkCitationFlags = [];
|
|
2281
|
+
const brokenSourceFlags = [];
|
|
1810
2282
|
for (const page of scan.data.typedKnowledge) {
|
|
1811
2283
|
const text = await readPage(page);
|
|
1812
2284
|
const split = splitFrontmatter(text);
|
|
@@ -1817,6 +2289,15 @@ async function runLint(input) {
|
|
|
1817
2289
|
if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
|
|
1818
2290
|
if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
|
|
1819
2291
|
if (hasWikilinkCitations(body)) wikilinkCitationFlags.push(page.relPath);
|
|
2292
|
+
const sourcesEntries = extractSourceEntries(rawFm);
|
|
2293
|
+
for (const entry of sourcesEntries) {
|
|
2294
|
+
let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
2295
|
+
rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
2296
|
+
if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
|
|
2297
|
+
if (!existsSync2(join17(input.vault, rawPath)) && !existsSync2(join17(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync2(join17(input.vault, "_archive", rawPath)) && !existsSync2(join17(input.vault, "_archive", rawPath + ".md"))) {
|
|
2298
|
+
brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
1820
2301
|
const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
|
|
1821
2302
|
for (const link of fmLinks) {
|
|
1822
2303
|
const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
|
|
@@ -1846,19 +2327,75 @@ async function runLint(input) {
|
|
|
1846
2327
|
if (noOverview.length > 0) buckets.missing_overview = noOverview;
|
|
1847
2328
|
if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
|
|
1848
2329
|
if (wikilinkCitationFlags.length > 0) buckets.wikilink_citation = wikilinkCitationFlags;
|
|
1849
|
-
if (
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2330
|
+
if (brokenSourceFlags.length > 0) buckets.broken_sources = brokenSourceFlags;
|
|
2331
|
+
const workItemHealth = [];
|
|
2332
|
+
const workItemDirs = /* @__PURE__ */ new Map();
|
|
2333
|
+
for (const page of scan.data.workItems) {
|
|
2334
|
+
const dir = page.relPath.replace(/\/(spec|plan|log)\.md$/, "");
|
|
2335
|
+
const pages = workItemDirs.get(dir) ?? [];
|
|
2336
|
+
pages.push(page);
|
|
2337
|
+
workItemDirs.set(dir, pages);
|
|
2338
|
+
}
|
|
2339
|
+
for (const [dir, pages] of workItemDirs) {
|
|
2340
|
+
const hasSpec = pages.some((p) => p.relPath.endsWith("/spec.md"));
|
|
2341
|
+
const hasPlan = pages.some((p) => p.relPath.endsWith("/plan.md"));
|
|
2342
|
+
if (hasSpec && !hasPlan) {
|
|
2343
|
+
const lastSegment = dir.split("/").pop();
|
|
2344
|
+
const dateMatch = lastSegment.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
2345
|
+
if (dateMatch) {
|
|
2346
|
+
const dirDate = Date.parse(dateMatch[1]);
|
|
2347
|
+
if (!isNaN(dirDate) && Date.now() - dirDate > 24 * 60 * 60 * 1e3) {
|
|
2348
|
+
workItemHealth.push(`${dir}/spec.md: has spec but no plan after 24h`);
|
|
1860
2349
|
}
|
|
1861
|
-
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
for (const page of pages) {
|
|
2353
|
+
if (!page.relPath.endsWith("/spec.md")) continue;
|
|
2354
|
+
const text = await readPage(page);
|
|
2355
|
+
const fm = extractFrontmatter(text);
|
|
2356
|
+
if (fm.ok && fm.data.status === "in-progress" && !fm.data.started) {
|
|
2357
|
+
workItemHealth.push(`${page.relPath}: in-progress without started date`);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
if (workItemHealth.length > 0) buckets.work_item_health = workItemHealth;
|
|
2362
|
+
const orphanedProjectPages = [];
|
|
2363
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2364
|
+
const text = await readPage(page);
|
|
2365
|
+
const fm = extractFrontmatter(text);
|
|
2366
|
+
if (!fm.ok) continue;
|
|
2367
|
+
const pp = fm.data.provenance_projects;
|
|
2368
|
+
if (!Array.isArray(pp)) continue;
|
|
2369
|
+
for (const entry of pp) {
|
|
2370
|
+
const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
|
|
2371
|
+
if (!slugMatch) continue;
|
|
2372
|
+
const slug = slugMatch[1];
|
|
2373
|
+
const knowledgePath = join17(input.vault, "projects", slug, "knowledge.md");
|
|
2374
|
+
if (!existsSync2(knowledgePath)) continue;
|
|
2375
|
+
const pageRef = page.relPath.replace(/\.md$/, "");
|
|
2376
|
+
try {
|
|
2377
|
+
const knowledgeContent = await readFile13(knowledgePath, "utf8");
|
|
2378
|
+
if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
|
|
2379
|
+
orphanedProjectPages.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
|
|
2380
|
+
}
|
|
2381
|
+
} catch {
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (orphanedProjectPages.length > 0) buckets.orphaned_project_pages = orphanedProjectPages;
|
|
2386
|
+
if (input.fix && legacyPages.length > 0) {
|
|
2387
|
+
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
2388
|
+
const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
|
|
2389
|
+
for (const relPath of legacyPages) {
|
|
2390
|
+
try {
|
|
2391
|
+
const absPath = `${input.vault}/${relPath}`;
|
|
2392
|
+
const raw = await readFile13(absPath, "utf8");
|
|
2393
|
+
const split = splitFrontmatter(raw);
|
|
2394
|
+
if (!split.ok) {
|
|
2395
|
+
unresolved.push(relPath);
|
|
2396
|
+
continue;
|
|
2397
|
+
}
|
|
2398
|
+
const body = split.data.body;
|
|
1862
2399
|
const rawFm = split.data.rawFrontmatter;
|
|
1863
2400
|
const stripped = body.replace(FENCE_RE2, "");
|
|
1864
2401
|
const lines = stripped.split("\n");
|
|
@@ -1932,7 +2469,7 @@ async function runLint(input) {
|
|
|
1932
2469
|
${rawFm}
|
|
1933
2470
|
---
|
|
1934
2471
|
${newBody}`;
|
|
1935
|
-
await
|
|
2472
|
+
await writeFile7(absPath, newContent, "utf8");
|
|
1936
2473
|
fixed.push(relPath);
|
|
1937
2474
|
} catch {
|
|
1938
2475
|
unresolved.push(relPath);
|
|
@@ -1945,6 +2482,128 @@ ${newBody}`;
|
|
|
1945
2482
|
else delete buckets.legacy_citation_style;
|
|
1946
2483
|
}
|
|
1947
2484
|
}
|
|
2485
|
+
if (input.fix && noOverview.length > 0) {
|
|
2486
|
+
for (const relPath of noOverview) {
|
|
2487
|
+
try {
|
|
2488
|
+
const absPath = `${input.vault}/${relPath}`;
|
|
2489
|
+
const raw = await readFile13(absPath, "utf8");
|
|
2490
|
+
const split = splitFrontmatter(raw);
|
|
2491
|
+
if (!split.ok) {
|
|
2492
|
+
unresolved.push(relPath);
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
const body = split.data.body;
|
|
2496
|
+
const rawFm = split.data.rawFrontmatter;
|
|
2497
|
+
const fm = extractFrontmatter(raw);
|
|
2498
|
+
const title = fm.ok && typeof fm.data.title === "string" ? fm.data.title : "";
|
|
2499
|
+
const overviewSection = `## Overview
|
|
2500
|
+
|
|
2501
|
+
${title}`;
|
|
2502
|
+
const trimmedBody = body.replace(/^\n+/, "");
|
|
2503
|
+
const newContent = `---
|
|
2504
|
+
${rawFm}
|
|
2505
|
+
---
|
|
2506
|
+
|
|
2507
|
+
${overviewSection}
|
|
2508
|
+
|
|
2509
|
+
${trimmedBody}`;
|
|
2510
|
+
await writeFile7(absPath, newContent, "utf8");
|
|
2511
|
+
fixed.push(relPath);
|
|
2512
|
+
} catch {
|
|
2513
|
+
unresolved.push(relPath);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const fixedBeforeOverview = fixed.length;
|
|
2517
|
+
const fixedSet = new Set(fixed);
|
|
2518
|
+
const remaining = noOverview.filter((p) => !fixedSet.has(p));
|
|
2519
|
+
if (remaining.length > 0) buckets.missing_overview = remaining;
|
|
2520
|
+
else delete buckets.missing_overview;
|
|
2521
|
+
}
|
|
2522
|
+
if (input.fix && wikilinkCitationFlags.length > 0) {
|
|
2523
|
+
const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
|
|
2524
|
+
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
2525
|
+
const wikilinkFixed = [];
|
|
2526
|
+
for (const relPath of wikilinkCitationFlags) {
|
|
2527
|
+
try {
|
|
2528
|
+
const absPath = `${input.vault}/${relPath}`;
|
|
2529
|
+
const raw = await readFile13(absPath, "utf8");
|
|
2530
|
+
const split = splitFrontmatter(raw);
|
|
2531
|
+
if (!split.ok) {
|
|
2532
|
+
unresolved.push(relPath);
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
const body = split.data.body;
|
|
2536
|
+
const rawFm = split.data.rawFrontmatter;
|
|
2537
|
+
const stripped = body.replace(FENCE_RE2, "");
|
|
2538
|
+
const wikilinkMatches = [...stripped.matchAll(WIKILINK_RE)];
|
|
2539
|
+
if (wikilinkMatches.length === 0) {
|
|
2540
|
+
unresolved.push(relPath);
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
const wikilinkPaths = [...new Set(wikilinkMatches.map((m) => m[1]))];
|
|
2544
|
+
const bodyLines = body.split("\n");
|
|
2545
|
+
let inSrc = false;
|
|
2546
|
+
const newBodyLines = [];
|
|
2547
|
+
for (const line of bodyLines) {
|
|
2548
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2549
|
+
inSrc = true;
|
|
2550
|
+
newBodyLines.push(line);
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
if (inSrc) {
|
|
2554
|
+
newBodyLines.push(line);
|
|
2555
|
+
continue;
|
|
2556
|
+
}
|
|
2557
|
+
let cleaned = line.replace(/\[\[raw\/[^\]|]+(?:\|[^\]]*)?\]\]/g, "");
|
|
2558
|
+
cleaned = cleaned.replace(/\s+\./g, ".").replace(/\s{2,}/g, " ").replace(/\s+$/, "");
|
|
2559
|
+
if (cleaned.length > 0 || line.trim().length === 0) {
|
|
2560
|
+
newBodyLines.push(cleaned);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
let newBody = newBodyLines.join("\n");
|
|
2564
|
+
const citationMarkers = wikilinkPaths.map((p) => `^[raw/${p}]`);
|
|
2565
|
+
const sourceEntries = extractSourceEntries(rawFm);
|
|
2566
|
+
const fmMarkers = [];
|
|
2567
|
+
for (const entry of sourceEntries) {
|
|
2568
|
+
let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
2569
|
+
rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
2570
|
+
if (rawPath.startsWith("raw/")) {
|
|
2571
|
+
fmMarkers.push(`^[${rawPath}]`);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
const allMarkers = [.../* @__PURE__ */ new Set([...citationMarkers, ...fmMarkers])];
|
|
2575
|
+
const hasSourcesSection = /^## Sources\b/m.test(newBody);
|
|
2576
|
+
if (hasSourcesSection) {
|
|
2577
|
+
const existingSources = new Set(
|
|
2578
|
+
newBody.split("\n").filter((l) => /^- \^\[raw\//.test(l.trim())).map((l) => l.trim().replace(/^- /, ""))
|
|
2579
|
+
);
|
|
2580
|
+
const newMarkers = allMarkers.filter((m) => !existingSources.has(m));
|
|
2581
|
+
const sourceLines = newMarkers.map((m) => `- ${m}`);
|
|
2582
|
+
if (sourceLines.length > 0) {
|
|
2583
|
+
newBody = newBody.trimEnd() + "\n" + sourceLines.join("\n") + "\n";
|
|
2584
|
+
}
|
|
2585
|
+
} else {
|
|
2586
|
+
const sourceLines = allMarkers.map((m) => `- ${m}`);
|
|
2587
|
+
newBody = newBody.trimEnd() + "\n\n## Sources\n\n" + sourceLines.join("\n") + "\n";
|
|
2588
|
+
}
|
|
2589
|
+
const newContent = `---
|
|
2590
|
+
${rawFm}
|
|
2591
|
+
---
|
|
2592
|
+
${newBody}`;
|
|
2593
|
+
await writeFile7(absPath, newContent, "utf8");
|
|
2594
|
+
wikilinkFixed.push(relPath);
|
|
2595
|
+
} catch {
|
|
2596
|
+
unresolved.push(relPath);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
fixed.push(...wikilinkFixed);
|
|
2600
|
+
if (wikilinkFixed.length > 0) {
|
|
2601
|
+
const fixedSet = new Set(wikilinkFixed);
|
|
2602
|
+
const remaining = wikilinkCitationFlags.filter((p) => !fixedSet.has(p));
|
|
2603
|
+
if (remaining.length > 0) buckets.wikilink_citation = remaining;
|
|
2604
|
+
else delete buckets.wikilink_citation;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
1948
2607
|
}
|
|
1949
2608
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1950
2609
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1966,6 +2625,14 @@ ${newBody}`;
|
|
|
1966
2625
|
hintLines.push(` ${b.kind}: ${b.items.length}`);
|
|
1967
2626
|
}
|
|
1968
2627
|
if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
|
|
2628
|
+
if (input.fix && fixed.length > 0) {
|
|
2629
|
+
appendLastOp(input.vault, {
|
|
2630
|
+
operation: "lint-fix",
|
|
2631
|
+
summary: `fixed ${fixed.length} page(s)`,
|
|
2632
|
+
files: fixed,
|
|
2633
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
1969
2636
|
return {
|
|
1970
2637
|
exitCode,
|
|
1971
2638
|
result: ok({
|
|
@@ -1980,14 +2647,14 @@ ${newBody}`;
|
|
|
1980
2647
|
}
|
|
1981
2648
|
|
|
1982
2649
|
// src/commands/config.ts
|
|
1983
|
-
import { readFile as
|
|
1984
|
-
import { existsSync } from "fs";
|
|
1985
|
-
import { join as
|
|
2650
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2651
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2652
|
+
import { join as join18 } from "path";
|
|
1986
2653
|
function validateKey(key) {
|
|
1987
2654
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
1988
2655
|
}
|
|
1989
2656
|
function configPath(home) {
|
|
1990
|
-
return
|
|
2657
|
+
return join18(home, ".skillwiki", ".env");
|
|
1991
2658
|
}
|
|
1992
2659
|
async function runConfigGet(input) {
|
|
1993
2660
|
if (!validateKey(input.key)) {
|
|
@@ -2005,7 +2672,7 @@ async function runConfigSet(input) {
|
|
|
2005
2672
|
try {
|
|
2006
2673
|
let originalContent;
|
|
2007
2674
|
try {
|
|
2008
|
-
originalContent = await
|
|
2675
|
+
originalContent = await readFile14(filePath, "utf8");
|
|
2009
2676
|
} catch {
|
|
2010
2677
|
}
|
|
2011
2678
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -2037,17 +2704,17 @@ async function runConfigList(input) {
|
|
|
2037
2704
|
}
|
|
2038
2705
|
async function runConfigPath(input) {
|
|
2039
2706
|
const filePath = configPath(input.home);
|
|
2040
|
-
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists:
|
|
2707
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync3(filePath), humanHint: filePath }) };
|
|
2041
2708
|
}
|
|
2042
2709
|
|
|
2043
2710
|
// src/commands/doctor.ts
|
|
2044
|
-
import { existsSync as
|
|
2045
|
-
import { join as
|
|
2711
|
+
import { existsSync as existsSync5, lstatSync, readlinkSync, readdirSync, statSync } from "fs";
|
|
2712
|
+
import { join as join21, resolve as resolve4 } from "path";
|
|
2046
2713
|
import { execSync } from "child_process";
|
|
2047
2714
|
|
|
2048
2715
|
// src/utils/auto-update.ts
|
|
2049
|
-
import { readFileSync as
|
|
2050
|
-
import { join as
|
|
2716
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
2717
|
+
import { join as join19, dirname as dirname8 } from "path";
|
|
2051
2718
|
import { spawn } from "child_process";
|
|
2052
2719
|
|
|
2053
2720
|
// src/utils/update-consts.ts
|
|
@@ -2058,11 +2725,11 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
|
|
|
2058
2725
|
|
|
2059
2726
|
// src/utils/auto-update.ts
|
|
2060
2727
|
function cachePath(home) {
|
|
2061
|
-
return
|
|
2728
|
+
return join19(home, ".skillwiki", CACHE_FILENAME);
|
|
2062
2729
|
}
|
|
2063
2730
|
function readCacheRaw(home) {
|
|
2064
2731
|
try {
|
|
2065
|
-
const raw =
|
|
2732
|
+
const raw = readFileSync4(cachePath(home), "utf8");
|
|
2066
2733
|
return JSON.parse(raw);
|
|
2067
2734
|
} catch {
|
|
2068
2735
|
return null;
|
|
@@ -2077,8 +2744,8 @@ function readCache(home) {
|
|
|
2077
2744
|
}
|
|
2078
2745
|
function writeCache(home, cache) {
|
|
2079
2746
|
const p = cachePath(home);
|
|
2080
|
-
|
|
2081
|
-
|
|
2747
|
+
mkdirSync2(dirname8(p), { recursive: true });
|
|
2748
|
+
writeFileSync3(p, JSON.stringify(cache, null, 2));
|
|
2082
2749
|
}
|
|
2083
2750
|
function latestFromCache(home, currentVersion) {
|
|
2084
2751
|
const { cache } = readCache(home);
|
|
@@ -2096,7 +2763,7 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
2096
2763
|
const { isStale } = readCache(home);
|
|
2097
2764
|
if (!isStale) return;
|
|
2098
2765
|
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
2099
|
-
if (!
|
|
2766
|
+
if (!existsSync4(bgScript)) return;
|
|
2100
2767
|
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
2101
2768
|
detached: true,
|
|
2102
2769
|
stdio: "ignore"
|
|
@@ -2107,13 +2774,13 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
2107
2774
|
}
|
|
2108
2775
|
|
|
2109
2776
|
// src/utils/plugin-registry.ts
|
|
2110
|
-
import { readFileSync as
|
|
2111
|
-
import { join as
|
|
2112
|
-
var REGISTRY_PATH =
|
|
2777
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
2778
|
+
import { join as join20 } from "path";
|
|
2779
|
+
var REGISTRY_PATH = join20(".claude", "plugins", "installed_plugins.json");
|
|
2113
2780
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
2114
2781
|
function readInstalledPlugins(home) {
|
|
2115
2782
|
try {
|
|
2116
|
-
const raw =
|
|
2783
|
+
const raw = readFileSync5(join20(home, REGISTRY_PATH), "utf8");
|
|
2117
2784
|
return JSON.parse(raw);
|
|
2118
2785
|
} catch {
|
|
2119
2786
|
return null;
|
|
@@ -2138,23 +2805,79 @@ function checkNodeVersion() {
|
|
|
2138
2805
|
}
|
|
2139
2806
|
return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
|
|
2140
2807
|
}
|
|
2141
|
-
function
|
|
2808
|
+
function detectCliChannels(argv, home) {
|
|
2809
|
+
const channels = [];
|
|
2142
2810
|
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
2143
|
-
|
|
2811
|
+
const devPath = resolve4(argv[1]);
|
|
2812
|
+
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
2813
|
+
}
|
|
2814
|
+
try {
|
|
2815
|
+
const whichOut = execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
2816
|
+
if (whichOut) {
|
|
2817
|
+
const isDev = isDevSymlink(whichOut);
|
|
2818
|
+
if (!channels.some((c) => c.path === resolve4(whichOut))) {
|
|
2819
|
+
channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
} catch {
|
|
2144
2823
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2824
|
+
const plugin = findPlugin(home);
|
|
2825
|
+
if (plugin) {
|
|
2826
|
+
const pluginBin = join21(plugin.installPath, "bin", "skillwiki");
|
|
2827
|
+
if (existsSync5(pluginBin)) {
|
|
2828
|
+
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
const installBin = join21(home, ".claude", "skills", "bin", "skillwiki");
|
|
2832
|
+
if (existsSync5(installBin)) {
|
|
2833
|
+
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
2147
2834
|
}
|
|
2835
|
+
return channels;
|
|
2836
|
+
}
|
|
2837
|
+
function isDevSymlink(binPath) {
|
|
2148
2838
|
try {
|
|
2149
|
-
|
|
2150
|
-
|
|
2839
|
+
const st = lstatSync(binPath);
|
|
2840
|
+
if (st.isSymbolicLink()) {
|
|
2841
|
+
const target = resolve4(binPath, "..", readlinkSync(binPath));
|
|
2842
|
+
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
2843
|
+
}
|
|
2151
2844
|
} catch {
|
|
2152
|
-
return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
|
|
2153
2845
|
}
|
|
2846
|
+
return false;
|
|
2847
|
+
}
|
|
2848
|
+
function checkCliChannels(argv, home) {
|
|
2849
|
+
const channels = detectCliChannels(argv, home);
|
|
2850
|
+
if (channels.length === 0) {
|
|
2851
|
+
return check("warn", "cli_channels", "CLI channels", "skillwiki not found on any channel");
|
|
2852
|
+
}
|
|
2853
|
+
if (channels.length === 1) {
|
|
2854
|
+
const ch = channels[0];
|
|
2855
|
+
const label = ch.isDevLink ? `${ch.name} (dev source)` : ch.name;
|
|
2856
|
+
return check("pass", "cli_channels", "CLI channels", `Single channel: ${label}`);
|
|
2857
|
+
}
|
|
2858
|
+
const devChannels = channels.filter((c) => c.isDevLink);
|
|
2859
|
+
const prodChannels = channels.filter((c) => !c.isDevLink);
|
|
2860
|
+
if (devChannels.length > 0 && prodChannels.length > 0) {
|
|
2861
|
+
const devNames = devChannels.map((c) => `${c.name}(dev)`);
|
|
2862
|
+
const prodNames = prodChannels.map((c) => c.name);
|
|
2863
|
+
return check(
|
|
2864
|
+
"warn",
|
|
2865
|
+
"cli_channels",
|
|
2866
|
+
"CLI channels",
|
|
2867
|
+
`${channels.length} channels: ${[...devNames, ...prodNames].join(", ")} \u2014 dev and prod binaries overlap; dev repo should use project-local settings only`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
const names = channels.map((c) => c.name);
|
|
2871
|
+
return check(
|
|
2872
|
+
"warn",
|
|
2873
|
+
"cli_channels",
|
|
2874
|
+
"CLI channels",
|
|
2875
|
+
`${channels.length} channels: ${names.join(", ")} \u2014 remove unused install with: rm ~/.claude/skills/bin/skillwiki`
|
|
2876
|
+
);
|
|
2154
2877
|
}
|
|
2155
2878
|
async function checkConfigFile(home) {
|
|
2156
2879
|
const cfgPath = configPath(home);
|
|
2157
|
-
if (!
|
|
2880
|
+
if (!existsSync5(cfgPath)) {
|
|
2158
2881
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
2159
2882
|
}
|
|
2160
2883
|
try {
|
|
@@ -2169,7 +2892,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
2169
2892
|
if (resolvedPath === void 0) {
|
|
2170
2893
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
2171
2894
|
}
|
|
2172
|
-
if (
|
|
2895
|
+
if (existsSync5(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
2173
2896
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
2174
2897
|
}
|
|
2175
2898
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -2178,20 +2901,27 @@ function checkVaultStructure(resolvedPath) {
|
|
|
2178
2901
|
if (resolvedPath === void 0) {
|
|
2179
2902
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
2180
2903
|
}
|
|
2181
|
-
if (!
|
|
2904
|
+
if (!existsSync5(resolvedPath)) {
|
|
2182
2905
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
2183
2906
|
}
|
|
2184
2907
|
const missing = [];
|
|
2185
|
-
if (!
|
|
2908
|
+
if (!existsSync5(join21(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
2186
2909
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
2187
|
-
if (!
|
|
2910
|
+
if (!existsSync5(join21(resolvedPath, dir))) missing.push(dir + "/");
|
|
2188
2911
|
}
|
|
2189
2912
|
if (missing.length === 0) {
|
|
2190
2913
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
2191
2914
|
}
|
|
2192
2915
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
2193
2916
|
}
|
|
2194
|
-
function checkSkillsInstalled(home) {
|
|
2917
|
+
function checkSkillsInstalled(home, cwd) {
|
|
2918
|
+
const srcDir = cwd ? join21(cwd, "packages", "skills") : void 0;
|
|
2919
|
+
if (srcDir && existsSync5(srcDir)) {
|
|
2920
|
+
const found = findSkillMd(srcDir);
|
|
2921
|
+
if (found.length > 0) {
|
|
2922
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2195
2925
|
const plugin = findPlugin(home);
|
|
2196
2926
|
if (plugin) {
|
|
2197
2927
|
const found = findSkillMd(plugin.installPath);
|
|
@@ -2199,8 +2929,8 @@ function checkSkillsInstalled(home) {
|
|
|
2199
2929
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
2200
2930
|
}
|
|
2201
2931
|
}
|
|
2202
|
-
const skillsDir =
|
|
2203
|
-
if (
|
|
2932
|
+
const skillsDir = join21(home, ".claude", "skills");
|
|
2933
|
+
if (existsSync5(skillsDir)) {
|
|
2204
2934
|
const found = findSkillMd(skillsDir);
|
|
2205
2935
|
if (found.length > 0) {
|
|
2206
2936
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
@@ -2208,6 +2938,25 @@ function checkSkillsInstalled(home) {
|
|
|
2208
2938
|
}
|
|
2209
2939
|
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found");
|
|
2210
2940
|
}
|
|
2941
|
+
function checkDuplicateSkills(home) {
|
|
2942
|
+
const plugin = findPlugin(home);
|
|
2943
|
+
const skillsDir = join21(home, ".claude", "skills");
|
|
2944
|
+
if (!plugin || !existsSync5(skillsDir)) {
|
|
2945
|
+
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
2946
|
+
}
|
|
2947
|
+
const pluginSkills = findSkillNames(plugin.installPath);
|
|
2948
|
+
const cliSkills = findSkillNames(skillsDir);
|
|
2949
|
+
const duplicates = pluginSkills.filter((name) => cliSkills.includes(name));
|
|
2950
|
+
if (duplicates.length === 0) {
|
|
2951
|
+
return check("pass", "skills_duplicate", "Skills not duplicated", "No overlap between plugin and CLI install");
|
|
2952
|
+
}
|
|
2953
|
+
return check(
|
|
2954
|
+
"warn",
|
|
2955
|
+
"skills_duplicate",
|
|
2956
|
+
"Skills not duplicated",
|
|
2957
|
+
`${duplicates.length} skill(s) in both plugin and ~/.claude/skills/ \u2014 remove CLI copies: rm -r ~/.claude/skills/{${duplicates.slice(0, 3).join(",")}${duplicates.length > 3 ? ",\u2026" : ""}}`
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2211
2960
|
function checkNpmUpdate(home, currentVersion) {
|
|
2212
2961
|
const { hasUpdate, latest } = latestFromCache(home, currentVersion);
|
|
2213
2962
|
if (!latest) {
|
|
@@ -2257,12 +3006,112 @@ async function checkProfiles(home) {
|
|
|
2257
3006
|
}
|
|
2258
3007
|
async function checkProjectLocalOverride(cwd) {
|
|
2259
3008
|
const dir = cwd ?? process.cwd();
|
|
2260
|
-
const envPath =
|
|
2261
|
-
if (
|
|
3009
|
+
const envPath = join21(dir, ".skillwiki", ".env");
|
|
3010
|
+
if (existsSync5(envPath)) {
|
|
2262
3011
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
2263
3012
|
}
|
|
2264
3013
|
return check("pass", "project_local", "Project-local config", "None");
|
|
2265
3014
|
}
|
|
3015
|
+
function checkVaultGitRemote(resolvedPath) {
|
|
3016
|
+
if (resolvedPath === void 0) {
|
|
3017
|
+
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3018
|
+
}
|
|
3019
|
+
if (!existsSync5(join21(resolvedPath, ".git"))) {
|
|
3020
|
+
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
3021
|
+
}
|
|
3022
|
+
try {
|
|
3023
|
+
const remote = execSync("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3024
|
+
if (!remote) {
|
|
3025
|
+
return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
|
|
3026
|
+
}
|
|
3027
|
+
let branch = "(no commits yet)";
|
|
3028
|
+
try {
|
|
3029
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3030
|
+
} catch {
|
|
3031
|
+
}
|
|
3032
|
+
return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
|
|
3033
|
+
} catch {
|
|
3034
|
+
return check("warn", "vault_git_remote", "Vault git remote", "Could not read git remote info");
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
function checkObsidianTemplates(resolvedPath) {
|
|
3038
|
+
if (resolvedPath === void 0) {
|
|
3039
|
+
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3040
|
+
}
|
|
3041
|
+
const missing = [];
|
|
3042
|
+
if (!existsSync5(join21(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
3043
|
+
if (!existsSync5(join21(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
3044
|
+
if (!existsSync5(join21(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
3045
|
+
if (missing.length === 0) {
|
|
3046
|
+
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
3047
|
+
}
|
|
3048
|
+
return check("warn", "obsidian_templates", "Obsidian templates", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to create`);
|
|
3049
|
+
}
|
|
3050
|
+
function checkDotStoreClean(resolvedPath) {
|
|
3051
|
+
if (resolvedPath === void 0) {
|
|
3052
|
+
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3053
|
+
}
|
|
3054
|
+
const rawDir = join21(resolvedPath, "raw");
|
|
3055
|
+
if (!existsSync5(rawDir)) {
|
|
3056
|
+
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
3057
|
+
}
|
|
3058
|
+
const found = [];
|
|
3059
|
+
(function walk2(dir, rel) {
|
|
3060
|
+
let entries;
|
|
3061
|
+
try {
|
|
3062
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3063
|
+
} catch {
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
for (const entry of entries) {
|
|
3067
|
+
if (entry.name === ".DS_Store") {
|
|
3068
|
+
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
3069
|
+
} else if (entry.isDirectory()) {
|
|
3070
|
+
walk2(join21(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
})(rawDir, "");
|
|
3074
|
+
if (found.length === 0) {
|
|
3075
|
+
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "No .DS_Store files found");
|
|
3076
|
+
}
|
|
3077
|
+
return check("warn", "dsstore_clean", "No .DS_Store in raw/", `${found.length} .DS_Store file(s) found \u2014 remove with: find ${rawDir} -name .DS_Store -delete`);
|
|
3078
|
+
}
|
|
3079
|
+
function checkSyncLastPush(resolvedPath) {
|
|
3080
|
+
if (resolvedPath === void 0) {
|
|
3081
|
+
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3082
|
+
}
|
|
3083
|
+
if (!existsSync5(join21(resolvedPath, ".git"))) {
|
|
3084
|
+
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
3085
|
+
}
|
|
3086
|
+
let timestamp;
|
|
3087
|
+
try {
|
|
3088
|
+
const out = execSync("git log -1 --format=%ct origin/HEAD", {
|
|
3089
|
+
cwd: resolvedPath,
|
|
3090
|
+
encoding: "utf8",
|
|
3091
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3092
|
+
}).trim();
|
|
3093
|
+
timestamp = parseInt(out, 10);
|
|
3094
|
+
} catch {
|
|
3095
|
+
try {
|
|
3096
|
+
const out = execSync("git log -1 --format=%ct HEAD", {
|
|
3097
|
+
cwd: resolvedPath,
|
|
3098
|
+
encoding: "utf8",
|
|
3099
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3100
|
+
}).trim();
|
|
3101
|
+
timestamp = parseInt(out, 10);
|
|
3102
|
+
} catch {
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
if (timestamp === void 0 || isNaN(timestamp)) {
|
|
3106
|
+
return check("warn", "sync_last_push", "Vault sync recency", "No commits found \u2014 consider running `skillwiki sync status`");
|
|
3107
|
+
}
|
|
3108
|
+
const daysSince2 = Math.floor((Date.now() / 1e3 - timestamp) / 86400);
|
|
3109
|
+
const dateStr = new Date(timestamp * 1e3).toISOString().slice(0, 10);
|
|
3110
|
+
if (daysSince2 > 7) {
|
|
3111
|
+
return check("warn", "sync_last_push", "Vault sync recency", `Last push was ${daysSince2} days ago \u2014 consider running \`skillwiki sync status\``);
|
|
3112
|
+
}
|
|
3113
|
+
return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince2} day(s) ago)`);
|
|
3114
|
+
}
|
|
2266
3115
|
function findSkillMd(dir) {
|
|
2267
3116
|
const results = [];
|
|
2268
3117
|
let entries;
|
|
@@ -2273,9 +3122,24 @@ function findSkillMd(dir) {
|
|
|
2273
3122
|
}
|
|
2274
3123
|
for (const entry of entries) {
|
|
2275
3124
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
2276
|
-
results.push(
|
|
3125
|
+
results.push(join21(dir, entry.name));
|
|
2277
3126
|
} else if (entry.isDirectory()) {
|
|
2278
|
-
results.push(...findSkillMd(
|
|
3127
|
+
results.push(...findSkillMd(join21(dir, entry.name)));
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
return results;
|
|
3131
|
+
}
|
|
3132
|
+
function findSkillNames(dir) {
|
|
3133
|
+
const results = [];
|
|
3134
|
+
let entries;
|
|
3135
|
+
try {
|
|
3136
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3137
|
+
} catch {
|
|
3138
|
+
return results;
|
|
3139
|
+
}
|
|
3140
|
+
for (const entry of entries) {
|
|
3141
|
+
if (entry.isDirectory() && existsSync5(join21(dir, entry.name, "SKILL.md"))) {
|
|
3142
|
+
results.push(entry.name);
|
|
2279
3143
|
}
|
|
2280
3144
|
}
|
|
2281
3145
|
return results;
|
|
@@ -2283,7 +3147,7 @@ function findSkillMd(dir) {
|
|
|
2283
3147
|
async function runDoctor(input) {
|
|
2284
3148
|
const checks = [];
|
|
2285
3149
|
checks.push(checkNodeVersion());
|
|
2286
|
-
checks.push(
|
|
3150
|
+
checks.push(checkCliChannels(input.argv, input.home));
|
|
2287
3151
|
checks.push(await checkConfigFile(input.home));
|
|
2288
3152
|
checks.push(await checkProfiles(input.home));
|
|
2289
3153
|
checks.push(await checkProjectLocalOverride(input.cwd));
|
|
@@ -2296,7 +3160,12 @@ async function runDoctor(input) {
|
|
|
2296
3160
|
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
2297
3161
|
checks.push(checkWikiPathExists(resolvedPath));
|
|
2298
3162
|
checks.push(checkVaultStructure(resolvedPath));
|
|
2299
|
-
checks.push(
|
|
3163
|
+
checks.push(checkObsidianTemplates(resolvedPath));
|
|
3164
|
+
checks.push(checkVaultGitRemote(resolvedPath));
|
|
3165
|
+
checks.push(checkSyncLastPush(resolvedPath));
|
|
3166
|
+
checks.push(checkDotStoreClean(resolvedPath));
|
|
3167
|
+
checks.push(checkSkillsInstalled(input.home, input.cwd));
|
|
3168
|
+
checks.push(checkDuplicateSkills(input.home));
|
|
2300
3169
|
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
2301
3170
|
checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
|
|
2302
3171
|
const summary = {
|
|
@@ -2318,8 +3187,8 @@ async function runDoctor(input) {
|
|
|
2318
3187
|
}
|
|
2319
3188
|
|
|
2320
3189
|
// src/commands/archive.ts
|
|
2321
|
-
import { rename as
|
|
2322
|
-
import { join as
|
|
3190
|
+
import { rename as rename4, mkdir as mkdir7, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
|
|
3191
|
+
import { join as join22, dirname as dirname9 } from "path";
|
|
2323
3192
|
async function runArchive(input) {
|
|
2324
3193
|
const scan = await scanVault(input.vault);
|
|
2325
3194
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -2335,31 +3204,37 @@ async function runArchive(input) {
|
|
|
2335
3204
|
}
|
|
2336
3205
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
2337
3206
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
2338
|
-
const archivePath =
|
|
2339
|
-
await
|
|
3207
|
+
const archivePath = join22("_archive", relPath).replace(/\\/g, "/");
|
|
3208
|
+
await mkdir7(dirname9(join22(input.vault, archivePath)), { recursive: true });
|
|
2340
3209
|
let indexUpdated = false;
|
|
2341
3210
|
if (!isRaw) {
|
|
2342
|
-
const indexPath =
|
|
3211
|
+
const indexPath = join22(input.vault, "index.md");
|
|
2343
3212
|
try {
|
|
2344
|
-
const idx = await
|
|
3213
|
+
const idx = await readFile15(indexPath, "utf8");
|
|
2345
3214
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
2346
3215
|
const originalLines = idx.split("\n");
|
|
2347
3216
|
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
2348
3217
|
if (filtered.length !== originalLines.length) {
|
|
2349
|
-
await
|
|
3218
|
+
await writeFile8(indexPath, filtered.join("\n"), "utf8");
|
|
2350
3219
|
indexUpdated = true;
|
|
2351
3220
|
}
|
|
2352
3221
|
} catch (e) {
|
|
2353
3222
|
if (e?.code !== "ENOENT") throw e;
|
|
2354
3223
|
}
|
|
2355
3224
|
}
|
|
2356
|
-
await
|
|
3225
|
+
await rename4(join22(input.vault, relPath), join22(input.vault, archivePath));
|
|
3226
|
+
appendLastOp(input.vault, {
|
|
3227
|
+
operation: "archive",
|
|
3228
|
+
summary: `moved ${relPath} to ${archivePath}`,
|
|
3229
|
+
files: [relPath],
|
|
3230
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3231
|
+
});
|
|
2357
3232
|
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
2358
3233
|
}
|
|
2359
3234
|
|
|
2360
3235
|
// src/commands/drift.ts
|
|
2361
3236
|
import { createHash as createHash2 } from "crypto";
|
|
2362
|
-
import { writeFile as
|
|
3237
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
2363
3238
|
|
|
2364
3239
|
// src/utils/fetch.ts
|
|
2365
3240
|
async function controlledFetch(url, opts) {
|
|
@@ -2425,6 +3300,7 @@ async function runDrift(input) {
|
|
|
2425
3300
|
const sourceUrl = sourceUrlMatch[1].trim();
|
|
2426
3301
|
const storedHash = storedHashMatch[1];
|
|
2427
3302
|
if (!sourceUrl.startsWith("http://") && !sourceUrl.startsWith("https://")) continue;
|
|
3303
|
+
if (/^refreshable:\s*false\b/m.test(rawFrontmatter)) continue;
|
|
2428
3304
|
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
2429
3305
|
if (!resp.ok) {
|
|
2430
3306
|
results.push({
|
|
@@ -2445,7 +3321,7 @@ async function runDrift(input) {
|
|
|
2445
3321
|
${newFm}
|
|
2446
3322
|
---
|
|
2447
3323
|
${body}`;
|
|
2448
|
-
await
|
|
3324
|
+
await writeFile9(raw.absPath, newText, "utf8");
|
|
2449
3325
|
results.push({
|
|
2450
3326
|
raw_path: raw.relPath,
|
|
2451
3327
|
source_url: sourceUrl,
|
|
@@ -2473,6 +3349,14 @@ ${body}`;
|
|
|
2473
3349
|
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
2474
3350
|
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2475
3351
|
if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
|
|
3352
|
+
if (input.apply && updated.length > 0) {
|
|
3353
|
+
appendLastOp(input.vault, {
|
|
3354
|
+
operation: "drift-apply",
|
|
3355
|
+
summary: `updated ${updated.length} raw sources`,
|
|
3356
|
+
files: updated.map((u) => u.raw_path),
|
|
3357
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
2476
3360
|
return {
|
|
2477
3361
|
exitCode,
|
|
2478
3362
|
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, newFiles: newResults, unchanged, humanHint: hintLines.join("\n") })
|
|
@@ -2480,7 +3364,7 @@ ${body}`;
|
|
|
2480
3364
|
}
|
|
2481
3365
|
|
|
2482
3366
|
// src/commands/migrate-citations.ts
|
|
2483
|
-
import { writeFile as
|
|
3367
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
2484
3368
|
var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
|
|
2485
3369
|
function moveMarkersToParagraphEnd(body) {
|
|
2486
3370
|
const lines = body.split("\n");
|
|
@@ -2603,7 +3487,7 @@ ${migratedBody}${newFooter}`;
|
|
|
2603
3487
|
continue;
|
|
2604
3488
|
}
|
|
2605
3489
|
if (!input.dryRun) {
|
|
2606
|
-
await
|
|
3490
|
+
await writeFile10(page.absPath, newText, "utf8");
|
|
2607
3491
|
}
|
|
2608
3492
|
migrated.push(page.relPath);
|
|
2609
3493
|
}
|
|
@@ -2612,6 +3496,14 @@ ${migratedBody}${newFooter}`;
|
|
|
2612
3496
|
if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
|
|
2613
3497
|
if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
|
|
2614
3498
|
if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
|
|
3499
|
+
if (!input.dryRun && migrated.length > 0) {
|
|
3500
|
+
appendLastOp(input.vault, {
|
|
3501
|
+
operation: "migrate-citations",
|
|
3502
|
+
summary: `converted ${migrated.length} citation(s)`,
|
|
3503
|
+
files: migrated,
|
|
3504
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
2615
3507
|
return {
|
|
2616
3508
|
exitCode,
|
|
2617
3509
|
result: ok({
|
|
@@ -2625,7 +3517,7 @@ ${migratedBody}${newFooter}`;
|
|
|
2625
3517
|
}
|
|
2626
3518
|
|
|
2627
3519
|
// src/commands/frontmatter-fix.ts
|
|
2628
|
-
import { writeFile as
|
|
3520
|
+
import { writeFile as writeFile11 } from "fs/promises";
|
|
2629
3521
|
function isoToday() {
|
|
2630
3522
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2631
3523
|
}
|
|
@@ -2667,7 +3559,7 @@ ${newBody}`;
|
|
|
2667
3559
|
continue;
|
|
2668
3560
|
}
|
|
2669
3561
|
if (!input.dryRun) {
|
|
2670
|
-
await
|
|
3562
|
+
await writeFile11(page.absPath, newText, "utf8");
|
|
2671
3563
|
}
|
|
2672
3564
|
fixed.push(page.relPath);
|
|
2673
3565
|
}
|
|
@@ -2677,6 +3569,14 @@ ${newBody}`;
|
|
|
2677
3569
|
if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
|
|
2678
3570
|
if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
|
|
2679
3571
|
if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
|
|
3572
|
+
if (!input.dryRun && fixed.length > 0) {
|
|
3573
|
+
appendLastOp(input.vault, {
|
|
3574
|
+
operation: "frontmatter-fix",
|
|
3575
|
+
summary: `normalized frontmatter on ${fixed.length} page(s)`,
|
|
3576
|
+
files: fixed,
|
|
3577
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3578
|
+
});
|
|
3579
|
+
}
|
|
2680
3580
|
return {
|
|
2681
3581
|
exitCode,
|
|
2682
3582
|
result: ok({
|
|
@@ -2691,13 +3591,41 @@ ${newBody}`;
|
|
|
2691
3591
|
|
|
2692
3592
|
// src/commands/update.ts
|
|
2693
3593
|
import { execSync as execSync2 } from "child_process";
|
|
2694
|
-
import { readFileSync as
|
|
3594
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3595
|
+
import { join as join23 } from "path";
|
|
3596
|
+
function resolveGlobalSkillsRoot() {
|
|
3597
|
+
try {
|
|
3598
|
+
const globalRoot = execSync2("npm root -g", {
|
|
3599
|
+
encoding: "utf8",
|
|
3600
|
+
timeout: 5e3
|
|
3601
|
+
}).trim();
|
|
3602
|
+
return join23(globalRoot, "skillwiki", "skills");
|
|
3603
|
+
} catch {
|
|
3604
|
+
return null;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
async function refreshInstalledSkills(target) {
|
|
3608
|
+
const skillsRoot = resolveGlobalSkillsRoot();
|
|
3609
|
+
if (!skillsRoot) {
|
|
3610
|
+
return { warnings: ["could not locate global skillwiki installation for skill refresh"], refreshed: false };
|
|
3611
|
+
}
|
|
3612
|
+
try {
|
|
3613
|
+
const result = await runInstall({ skillsRoot, target, dryRun: false, symlink: false });
|
|
3614
|
+
if (result.result.ok) {
|
|
3615
|
+
return { warnings: result.result.data.version_warnings, refreshed: true };
|
|
3616
|
+
}
|
|
3617
|
+
return { warnings: [`skill refresh failed: ${result.result.error}`], refreshed: false };
|
|
3618
|
+
} catch (e) {
|
|
3619
|
+
return { warnings: [`skill refresh error: ${String(e)}`], refreshed: false };
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
2695
3622
|
async function runUpdate(input) {
|
|
2696
3623
|
const pkg2 = JSON.parse(
|
|
2697
|
-
|
|
3624
|
+
readFileSync6(new URL("../../package.json", import.meta.url), "utf8")
|
|
2698
3625
|
);
|
|
2699
3626
|
const currentVersion = pkg2.version;
|
|
2700
3627
|
const tag = input.distTag ?? "beta";
|
|
3628
|
+
const target = join23(input.home, ".claude", "skills");
|
|
2701
3629
|
let latest;
|
|
2702
3630
|
try {
|
|
2703
3631
|
latest = execSync2(`npm view skillwiki@${tag} version`, {
|
|
@@ -2723,6 +3651,8 @@ async function runUpdate(input) {
|
|
|
2723
3651
|
previousVersion: currentVersion,
|
|
2724
3652
|
newVersion: null,
|
|
2725
3653
|
wasAlreadyLatest: true,
|
|
3654
|
+
version_warnings: [],
|
|
3655
|
+
skills_refreshed: false,
|
|
2726
3656
|
humanHint: `Already on latest ${tag}: v${currentVersion}`
|
|
2727
3657
|
})
|
|
2728
3658
|
};
|
|
@@ -2739,78 +3669,254 @@ async function runUpdate(input) {
|
|
|
2739
3669
|
};
|
|
2740
3670
|
}
|
|
2741
3671
|
writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
|
|
3672
|
+
const installResult = await refreshInstalledSkills(target);
|
|
3673
|
+
const version_warnings = installResult.warnings;
|
|
3674
|
+
const skills_refreshed = installResult.refreshed;
|
|
3675
|
+
const hintLines = [
|
|
3676
|
+
`Updated skillwiki ${currentVersion} \u2192 ${latest}`,
|
|
3677
|
+
`skills refreshed: ${skills_refreshed}`
|
|
3678
|
+
];
|
|
3679
|
+
if (version_warnings.length > 0) {
|
|
3680
|
+
hintLines.push(`version warnings: ${version_warnings.length}`);
|
|
3681
|
+
for (const w of version_warnings) hintLines.push(` ${w}`);
|
|
3682
|
+
}
|
|
2742
3683
|
return {
|
|
2743
3684
|
exitCode: ExitCode.OK,
|
|
2744
3685
|
result: ok({
|
|
2745
3686
|
previousVersion: currentVersion,
|
|
2746
3687
|
newVersion: latest,
|
|
2747
3688
|
wasAlreadyLatest: false,
|
|
2748
|
-
|
|
3689
|
+
version_warnings,
|
|
3690
|
+
skills_refreshed,
|
|
3691
|
+
humanHint: hintLines.join("\n")
|
|
2749
3692
|
})
|
|
2750
3693
|
};
|
|
2751
3694
|
}
|
|
2752
3695
|
|
|
2753
|
-
// src/commands/
|
|
2754
|
-
import {
|
|
2755
|
-
import {
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
const
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
try {
|
|
2792
|
-
await readdir5(projectDir);
|
|
2793
|
-
} catch {
|
|
3696
|
+
// src/commands/self-update.ts
|
|
3697
|
+
import { execSync as execSync3 } from "child_process";
|
|
3698
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
3699
|
+
import { join as join24 } from "path";
|
|
3700
|
+
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
3701
|
+
async function runSelfUpdate(input) {
|
|
3702
|
+
const currentVersion = JSON.parse(
|
|
3703
|
+
readFileSync7(new URL("../../package.json", import.meta.url), "utf8")
|
|
3704
|
+
).version;
|
|
3705
|
+
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
3706
|
+
const localPkgPath = join24(sourceRoot, "packages", "cli", "package.json");
|
|
3707
|
+
const hasLocalSource = existsSync6(localPkgPath);
|
|
3708
|
+
if (input.check) {
|
|
3709
|
+
let availableVersion = null;
|
|
3710
|
+
let source;
|
|
3711
|
+
if (hasLocalSource) {
|
|
3712
|
+
source = "local";
|
|
3713
|
+
try {
|
|
3714
|
+
availableVersion = JSON.parse(readFileSync7(localPkgPath, "utf8")).version ?? null;
|
|
3715
|
+
} catch {
|
|
3716
|
+
availableVersion = null;
|
|
3717
|
+
}
|
|
3718
|
+
} else {
|
|
3719
|
+
source = "npm";
|
|
3720
|
+
try {
|
|
3721
|
+
availableVersion = execSync3("npm view skillwiki@beta version", {
|
|
3722
|
+
encoding: "utf8",
|
|
3723
|
+
timeout: 15e3
|
|
3724
|
+
}).trim();
|
|
3725
|
+
} catch (e) {
|
|
3726
|
+
return {
|
|
3727
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3728
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
const updateAvailable = availableVersion !== null && availableVersion !== currentVersion;
|
|
3733
|
+
const hint = updateAvailable ? `Update available: ${currentVersion} \u2192 ${availableVersion} (${source})` : `Already up to date: v${currentVersion} (${source})`;
|
|
2794
3734
|
return {
|
|
2795
|
-
exitCode: ExitCode.
|
|
3735
|
+
exitCode: ExitCode.OK,
|
|
3736
|
+
result: ok({
|
|
3737
|
+
source,
|
|
3738
|
+
currentVersion,
|
|
3739
|
+
availableVersion,
|
|
3740
|
+
updateAvailable,
|
|
3741
|
+
humanHint: hint
|
|
3742
|
+
})
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
if (hasLocalSource) {
|
|
3746
|
+
try {
|
|
3747
|
+
execSync3("npm run build -w packages/cli", {
|
|
3748
|
+
cwd: sourceRoot,
|
|
3749
|
+
stdio: "pipe",
|
|
3750
|
+
timeout: 6e4
|
|
3751
|
+
});
|
|
3752
|
+
} catch (e) {
|
|
3753
|
+
return {
|
|
3754
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3755
|
+
result: err("BUILD_FAILED", { message: `Build failed: ${String(e)}` })
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
try {
|
|
3759
|
+
execSync3("npm link ./packages/cli", {
|
|
3760
|
+
cwd: sourceRoot,
|
|
3761
|
+
stdio: "pipe",
|
|
3762
|
+
timeout: 3e4
|
|
3763
|
+
});
|
|
3764
|
+
} catch (e) {
|
|
3765
|
+
return {
|
|
3766
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3767
|
+
result: err("LINK_FAILED", { message: `npm link failed: ${String(e)}` })
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
const newVersion = (() => {
|
|
3771
|
+
try {
|
|
3772
|
+
return JSON.parse(readFileSync7(localPkgPath, "utf8")).version ?? "unknown";
|
|
3773
|
+
} catch {
|
|
3774
|
+
return "unknown";
|
|
3775
|
+
}
|
|
3776
|
+
})();
|
|
3777
|
+
return {
|
|
3778
|
+
exitCode: ExitCode.OK,
|
|
3779
|
+
result: ok({
|
|
3780
|
+
source: "local",
|
|
3781
|
+
currentVersion,
|
|
3782
|
+
availableVersion: newVersion,
|
|
3783
|
+
updateAvailable: newVersion !== currentVersion,
|
|
3784
|
+
newVersion,
|
|
3785
|
+
humanHint: `Built and linked from local source: v${newVersion}`
|
|
3786
|
+
})
|
|
3787
|
+
};
|
|
3788
|
+
}
|
|
3789
|
+
let latestVersion;
|
|
3790
|
+
try {
|
|
3791
|
+
latestVersion = execSync3("npm view skillwiki@beta version", {
|
|
3792
|
+
encoding: "utf8",
|
|
3793
|
+
timeout: 15e3
|
|
3794
|
+
}).trim();
|
|
3795
|
+
} catch (e) {
|
|
3796
|
+
return {
|
|
3797
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3798
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
if (latestVersion === currentVersion) {
|
|
3802
|
+
return {
|
|
3803
|
+
exitCode: ExitCode.OK,
|
|
3804
|
+
result: ok({
|
|
3805
|
+
source: "npm",
|
|
3806
|
+
currentVersion,
|
|
3807
|
+
availableVersion: latestVersion,
|
|
3808
|
+
updateAvailable: false,
|
|
3809
|
+
humanHint: `Already on latest beta: v${currentVersion}`
|
|
3810
|
+
})
|
|
3811
|
+
};
|
|
3812
|
+
}
|
|
3813
|
+
try {
|
|
3814
|
+
execSync3("npm install -g skillwiki@beta", {
|
|
3815
|
+
stdio: "pipe",
|
|
3816
|
+
timeout: 6e4
|
|
3817
|
+
});
|
|
3818
|
+
} catch (e) {
|
|
3819
|
+
return {
|
|
3820
|
+
exitCode: ExitCode.INTERNAL_ERROR,
|
|
3821
|
+
result: err("INSTALL_FAILED", { message: `npm install failed: ${String(e)}` })
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
return {
|
|
3825
|
+
exitCode: ExitCode.OK,
|
|
3826
|
+
result: ok({
|
|
3827
|
+
source: "npm",
|
|
3828
|
+
currentVersion,
|
|
3829
|
+
availableVersion: latestVersion,
|
|
3830
|
+
updateAvailable: true,
|
|
3831
|
+
newVersion: latestVersion,
|
|
3832
|
+
humanHint: `Updated skillwiki ${currentVersion} \u2192 ${latestVersion} via npm@beta`
|
|
3833
|
+
})
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
// src/commands/transcripts.ts
|
|
3838
|
+
import { readdir as readdir5, stat as stat6, readFile as readFile16 } from "fs/promises";
|
|
3839
|
+
import { join as join25 } from "path";
|
|
3840
|
+
async function runTranscripts(input) {
|
|
3841
|
+
const dir = join25(input.vault, "raw", "transcripts");
|
|
3842
|
+
let entries;
|
|
3843
|
+
try {
|
|
3844
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
3845
|
+
} catch {
|
|
3846
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
|
|
3847
|
+
}
|
|
3848
|
+
const transcripts = [];
|
|
3849
|
+
for (const entry of entries) {
|
|
3850
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
3851
|
+
const filePath = join25(dir, entry.name);
|
|
3852
|
+
const content = await readFile16(filePath, "utf8");
|
|
3853
|
+
const fm = extractFrontmatter(content);
|
|
3854
|
+
if (!fm.ok) continue;
|
|
3855
|
+
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
3856
|
+
if (input.since && ingested && ingested < input.since) continue;
|
|
3857
|
+
const s = await stat6(filePath);
|
|
3858
|
+
transcripts.push({
|
|
3859
|
+
file: `raw/transcripts/${entry.name}`,
|
|
3860
|
+
ingested,
|
|
3861
|
+
size: s.size
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
const hint = transcripts.length > 0 ? transcripts.map((t) => `${t.file} (ingested: ${t.ingested || "unknown"}, ${t.size}B)`).join("\n") : "no transcript files found";
|
|
3865
|
+
return { exitCode: ExitCode.OK, result: ok({ transcripts, humanHint: hint }) };
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// src/commands/project-index.ts
|
|
3869
|
+
import { readdir as readdir6, readFile as readFile17, writeFile as writeFile12, mkdir as mkdir8 } from "fs/promises";
|
|
3870
|
+
import { join as join26, dirname as dirname10 } from "path";
|
|
3871
|
+
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
3872
|
+
async function runProjectIndex(input) {
|
|
3873
|
+
const slug = input.slug;
|
|
3874
|
+
const projectDir = join26(input.vault, "projects", slug);
|
|
3875
|
+
try {
|
|
3876
|
+
await readdir6(projectDir);
|
|
3877
|
+
} catch {
|
|
3878
|
+
return {
|
|
3879
|
+
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
2796
3880
|
result: err("PROJECT_NOT_FOUND", { slug, path: projectDir })
|
|
2797
3881
|
};
|
|
2798
3882
|
}
|
|
2799
3883
|
const wikilinkPattern = `[[${slug}]]`;
|
|
2800
3884
|
const entries = [];
|
|
3885
|
+
const compoundDir = join26(input.vault, "projects", slug, "compound");
|
|
3886
|
+
try {
|
|
3887
|
+
const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
|
|
3888
|
+
for (const entry of compoundFiles) {
|
|
3889
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
3890
|
+
const filePath = join26(compoundDir, entry.name);
|
|
3891
|
+
let text;
|
|
3892
|
+
try {
|
|
3893
|
+
text = await readFile17(filePath, "utf8");
|
|
3894
|
+
} catch {
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3897
|
+
const fm = extractFrontmatter(text);
|
|
3898
|
+
if (!fm.ok) continue;
|
|
3899
|
+
entries.push({
|
|
3900
|
+
page: `projects/${slug}/compound/${entry.name}`,
|
|
3901
|
+
type: typeof fm.data.type === "string" ? fm.data.type : "compound",
|
|
3902
|
+
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
} catch {
|
|
3906
|
+
}
|
|
2801
3907
|
for (const dir of LAYER2_DIRS) {
|
|
2802
3908
|
let files;
|
|
2803
3909
|
try {
|
|
2804
|
-
files = await
|
|
3910
|
+
files = await readdir6(join26(input.vault, dir), { withFileTypes: true });
|
|
2805
3911
|
} catch {
|
|
2806
3912
|
continue;
|
|
2807
3913
|
}
|
|
2808
3914
|
for (const entry of files) {
|
|
2809
3915
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2810
|
-
const filePath =
|
|
3916
|
+
const filePath = join26(input.vault, dir, entry.name);
|
|
2811
3917
|
let text;
|
|
2812
3918
|
try {
|
|
2813
|
-
text = await
|
|
3919
|
+
text = await readFile17(filePath, "utf8");
|
|
2814
3920
|
} catch {
|
|
2815
3921
|
continue;
|
|
2816
3922
|
}
|
|
@@ -2820,117 +3926,1860 @@ async function runProjectIndex(input) {
|
|
|
2820
3926
|
if (!Array.isArray(pp) || !pp.some((p) => String(p) === wikilinkPattern)) continue;
|
|
2821
3927
|
entries.push({
|
|
2822
3928
|
page: `${dir}/${entry.name}`,
|
|
2823
|
-
type: fm.data.type
|
|
2824
|
-
|
|
2825
|
-
title: fm.data.title ?? entry.name.replace(/\.md$/, "")
|
|
3929
|
+
type: typeof fm.data.type === "string" ? fm.data.type : dir.slice(0, -1),
|
|
3930
|
+
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
2826
3931
|
});
|
|
2827
3932
|
}
|
|
2828
3933
|
}
|
|
2829
|
-
const typeOrder = { entity: 0, concept: 1, comparison: 2, query: 3, summary: 4, meta: 5 };
|
|
2830
|
-
entries.sort((a, b) => {
|
|
2831
|
-
const ta = typeOrder[a.type] ?? 99;
|
|
2832
|
-
const tb = typeOrder[b.type] ?? 99;
|
|
2833
|
-
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
2834
|
-
});
|
|
2835
|
-
const indexPath =
|
|
2836
|
-
let existing = false;
|
|
2837
|
-
let stale = false;
|
|
3934
|
+
const typeOrder = { entity: 0, concept: 1, comparison: 2, query: 3, summary: 4, meta: 5, pattern: 6, gotcha: 7, lesson: 8, antipattern: 9, compound: 10 };
|
|
3935
|
+
entries.sort((a, b) => {
|
|
3936
|
+
const ta = typeOrder[a.type] ?? 99;
|
|
3937
|
+
const tb = typeOrder[b.type] ?? 99;
|
|
3938
|
+
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
3939
|
+
});
|
|
3940
|
+
const indexPath = join26(projectDir, "knowledge.md");
|
|
3941
|
+
let existing = false;
|
|
3942
|
+
let stale = false;
|
|
3943
|
+
try {
|
|
3944
|
+
const existingText = await readFile17(indexPath, "utf8");
|
|
3945
|
+
existing = true;
|
|
3946
|
+
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
3947
|
+
const existingPages = new Set(existingEntries.map((l) => {
|
|
3948
|
+
const m = l.match(/\[\[([^\]]+)\]\]/);
|
|
3949
|
+
return m ? m[1] : "";
|
|
3950
|
+
}));
|
|
3951
|
+
const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
|
|
3952
|
+
stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
|
|
3953
|
+
} catch {
|
|
3954
|
+
}
|
|
3955
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3956
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3957
|
+
for (const e of entries) {
|
|
3958
|
+
const group = e.type;
|
|
3959
|
+
if (!grouped.has(group)) grouped.set(group, []);
|
|
3960
|
+
grouped.get(group).push(e);
|
|
3961
|
+
}
|
|
3962
|
+
let body = `# Knowledge Index: ${slug}
|
|
3963
|
+
|
|
3964
|
+
Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
3965
|
+
|
|
3966
|
+
`;
|
|
3967
|
+
for (const [type, items] of grouped) {
|
|
3968
|
+
body += `## ${type}
|
|
3969
|
+
|
|
3970
|
+
`;
|
|
3971
|
+
for (const item of items) {
|
|
3972
|
+
const pageRef = item.page.replace(/\.md$/, "");
|
|
3973
|
+
body += `- [[${pageRef}]] \u2014 ${item.title}
|
|
3974
|
+
`;
|
|
3975
|
+
}
|
|
3976
|
+
body += "\n";
|
|
3977
|
+
}
|
|
3978
|
+
if (entries.length === 0) {
|
|
3979
|
+
body += `No Layer 2 pages reference \`[[${slug}]]\` in provenance_projects.
|
|
3980
|
+
`;
|
|
3981
|
+
}
|
|
3982
|
+
if (input.apply) {
|
|
3983
|
+
try {
|
|
3984
|
+
await mkdir8(dirname10(indexPath), { recursive: true });
|
|
3985
|
+
await writeFile12(indexPath, body, "utf8");
|
|
3986
|
+
} catch (e) {
|
|
3987
|
+
return {
|
|
3988
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3989
|
+
result: err("WRITE_FAILED", { file: indexPath, message: String(e) })
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
const action = input.apply ? `written ${entries.length} entries to ${indexPath}` : `${entries.length} entries found (use --apply to write)`;
|
|
3994
|
+
const staleHint = stale ? " (STALE \u2014 existing index outdated)" : existing ? " (up to date)" : "";
|
|
3995
|
+
return {
|
|
3996
|
+
exitCode: ExitCode.OK,
|
|
3997
|
+
result: ok({
|
|
3998
|
+
slug,
|
|
3999
|
+
entries,
|
|
4000
|
+
existing,
|
|
4001
|
+
stale,
|
|
4002
|
+
index_path: `projects/${slug}/knowledge.md`,
|
|
4003
|
+
humanHint: `project: ${slug}
|
|
4004
|
+
entries: ${entries.length}${staleHint}
|
|
4005
|
+
${action}
|
|
4006
|
+
|
|
4007
|
+
${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
|
|
4008
|
+
})
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
// src/commands/compound.ts
|
|
4013
|
+
import { writeFile as writeFile13, mkdir as mkdir9, readdir as readdir7, unlink as unlink2 } from "fs/promises";
|
|
4014
|
+
import { join as join27 } from "path";
|
|
4015
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4016
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
4017
|
+
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
4018
|
+
var FIELD_RE = {
|
|
4019
|
+
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
4020
|
+
friction: /^-\s+\*?\*?Friction:?\*?\*?\s*(.+)$/m,
|
|
4021
|
+
generalize: /^-\s+\*?\*?Generalize\?:?\*?\*?\s*(.+)$/m
|
|
4022
|
+
};
|
|
4023
|
+
function slugify(name) {
|
|
4024
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+$/g, "");
|
|
4025
|
+
}
|
|
4026
|
+
function inferType(improve, friction) {
|
|
4027
|
+
if (/\bshould\b/i.test(improve)) return "pattern";
|
|
4028
|
+
if (/\bbug\b|\berror\b/i.test(friction)) return "gotcha";
|
|
4029
|
+
return "lesson";
|
|
4030
|
+
}
|
|
4031
|
+
function extractTags(generalize) {
|
|
4032
|
+
const tags = [];
|
|
4033
|
+
const parenRe = /\(([^)]+)\)/g;
|
|
4034
|
+
let match;
|
|
4035
|
+
while ((match = parenRe.exec(generalize)) !== null) {
|
|
4036
|
+
const words = match[1].trim().split(/\s+/);
|
|
4037
|
+
for (const w of words) {
|
|
4038
|
+
const cleaned = w.toLowerCase().replace(/[^a-z0-9-]/g, "").trim();
|
|
4039
|
+
if (cleaned.length > 0) tags.push(cleaned);
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
const appliesRe = /applies to any\s+(.+?)(?:\.|,|$)/i;
|
|
4043
|
+
const appliesMatch = generalize.match(appliesRe);
|
|
4044
|
+
if (appliesMatch) {
|
|
4045
|
+
const words = appliesMatch[1].trim().split(/\s+/);
|
|
4046
|
+
for (const w of words) {
|
|
4047
|
+
const cleaned = w.toLowerCase().replace(/[^a-z0-9-]/g, "").trim();
|
|
4048
|
+
if (cleaned.length > 0) tags.push(cleaned);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
if (tags.length === 0) {
|
|
4052
|
+
tags.push("dev-loop");
|
|
4053
|
+
}
|
|
4054
|
+
return [...new Set(tags)];
|
|
4055
|
+
}
|
|
4056
|
+
function parseRationale(generalize) {
|
|
4057
|
+
const yesMatch = generalize.match(/^yes[,:]\s*(.+)$/i);
|
|
4058
|
+
if (yesMatch) return yesMatch[1].trim();
|
|
4059
|
+
if (/^yes$/i.test(generalize.trim())) return "";
|
|
4060
|
+
return generalize.trim();
|
|
4061
|
+
}
|
|
4062
|
+
function parseRetroEntries(logText) {
|
|
4063
|
+
const entries = [];
|
|
4064
|
+
const lines = logText.split("\n");
|
|
4065
|
+
let currentDate = "";
|
|
4066
|
+
let currentCycleName = "";
|
|
4067
|
+
let currentBlock = [];
|
|
4068
|
+
let foundHeading = false;
|
|
4069
|
+
for (const line of lines) {
|
|
4070
|
+
const headingMatch = line.match(RETRO_HEADING_RE);
|
|
4071
|
+
if (headingMatch) {
|
|
4072
|
+
if (foundHeading && currentBlock.length > 0) {
|
|
4073
|
+
const entry = extractRetroFields(currentDate, currentCycleName, currentBlock);
|
|
4074
|
+
if (entry) entries.push(entry);
|
|
4075
|
+
}
|
|
4076
|
+
currentDate = headingMatch[1];
|
|
4077
|
+
currentCycleName = headingMatch[3];
|
|
4078
|
+
currentBlock = [];
|
|
4079
|
+
foundHeading = true;
|
|
4080
|
+
continue;
|
|
4081
|
+
}
|
|
4082
|
+
if (foundHeading && /^## /.test(line)) {
|
|
4083
|
+
const entry = extractRetroFields(currentDate, currentCycleName, currentBlock);
|
|
4084
|
+
if (entry) entries.push(entry);
|
|
4085
|
+
foundHeading = false;
|
|
4086
|
+
currentBlock = [];
|
|
4087
|
+
continue;
|
|
4088
|
+
}
|
|
4089
|
+
if (foundHeading) {
|
|
4090
|
+
currentBlock.push(line);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
if (foundHeading && currentBlock.length > 0) {
|
|
4094
|
+
const entry = extractRetroFields(currentDate, currentCycleName, currentBlock);
|
|
4095
|
+
if (entry) entries.push(entry);
|
|
4096
|
+
}
|
|
4097
|
+
return entries;
|
|
4098
|
+
}
|
|
4099
|
+
function extractRetroFields(date, cycleName, block) {
|
|
4100
|
+
const text = block.join("\n");
|
|
4101
|
+
const improveMatch = text.match(FIELD_RE.improve);
|
|
4102
|
+
const frictionMatch = text.match(FIELD_RE.friction);
|
|
4103
|
+
const generalizeMatch = text.match(FIELD_RE.generalize);
|
|
4104
|
+
if (!generalizeMatch) return null;
|
|
4105
|
+
return {
|
|
4106
|
+
date,
|
|
4107
|
+
cycleName,
|
|
4108
|
+
improve: improveMatch?.[1]?.trim() ?? "",
|
|
4109
|
+
friction: frictionMatch?.[1]?.trim() ?? "",
|
|
4110
|
+
generalize: generalizeMatch[1].trim()
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
4113
|
+
async function runCompound(input) {
|
|
4114
|
+
const logPath = join27(input.vault, "log.md");
|
|
4115
|
+
let logText;
|
|
4116
|
+
try {
|
|
4117
|
+
logText = await readFile18(logPath, "utf8");
|
|
4118
|
+
} catch {
|
|
4119
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
4120
|
+
}
|
|
4121
|
+
const entries = parseRetroEntries(logText);
|
|
4122
|
+
const promoted = [];
|
|
4123
|
+
const skipped = [];
|
|
4124
|
+
const compoundDir = join27(input.vault, "projects", input.project, "compound");
|
|
4125
|
+
for (const entry of entries) {
|
|
4126
|
+
const generalizeValue = entry.generalize.trim();
|
|
4127
|
+
if (!/^yes/i.test(generalizeValue)) {
|
|
4128
|
+
skipped.push(entry.date);
|
|
4129
|
+
continue;
|
|
4130
|
+
}
|
|
4131
|
+
const slug = slugify(entry.cycleName);
|
|
4132
|
+
const compoundPath = join27(compoundDir, `${slug}.md`);
|
|
4133
|
+
if (existsSync7(compoundPath)) {
|
|
4134
|
+
skipped.push(entry.date);
|
|
4135
|
+
continue;
|
|
4136
|
+
}
|
|
4137
|
+
const type = inferType(entry.improve, entry.friction);
|
|
4138
|
+
const rationale = parseRationale(generalizeValue);
|
|
4139
|
+
const tags = extractTags(generalizeValue);
|
|
4140
|
+
const tagsYaml = tags.map((t) => t).join(", ");
|
|
4141
|
+
const title = entry.cycleName;
|
|
4142
|
+
const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
|
|
4143
|
+
const frontmatter = [
|
|
4144
|
+
"---",
|
|
4145
|
+
`title: ${title}`,
|
|
4146
|
+
`created: ${entry.date}`,
|
|
4147
|
+
`updated: ${entry.date}`,
|
|
4148
|
+
`type: ${type}`,
|
|
4149
|
+
`tags: [${tagsYaml}]`,
|
|
4150
|
+
`confidence: medium`,
|
|
4151
|
+
`project: "[[${input.project}]]"`,
|
|
4152
|
+
`work_items: []`,
|
|
4153
|
+
"---"
|
|
4154
|
+
].join("\n");
|
|
4155
|
+
const body = [
|
|
4156
|
+
`## ${typeLabel}`,
|
|
4157
|
+
"",
|
|
4158
|
+
entry.improve,
|
|
4159
|
+
"",
|
|
4160
|
+
"## Evidence",
|
|
4161
|
+
"",
|
|
4162
|
+
entry.friction,
|
|
4163
|
+
"",
|
|
4164
|
+
"## Source",
|
|
4165
|
+
"",
|
|
4166
|
+
`Retro from ${entry.date} | ${entry.cycleName}. Generalize rationale: ${rationale}`,
|
|
4167
|
+
""
|
|
4168
|
+
].join("\n");
|
|
4169
|
+
const content = frontmatter + "\n" + body;
|
|
4170
|
+
if (!input.dryRun) {
|
|
4171
|
+
if (!existsSync7(compoundDir)) {
|
|
4172
|
+
await mkdir9(compoundDir, { recursive: true });
|
|
4173
|
+
}
|
|
4174
|
+
await writeFile13(compoundPath, content, "utf8");
|
|
4175
|
+
}
|
|
4176
|
+
promoted.push(`${slug}.md`);
|
|
4177
|
+
}
|
|
4178
|
+
const exitCode = promoted.length > 0 ? ExitCode.COMPOUND_PROMOTED : ExitCode.OK;
|
|
4179
|
+
const hintLines = [`scanned: ${entries.length}`];
|
|
4180
|
+
if (promoted.length > 0) hintLines.push(`promoted: ${promoted.length}`);
|
|
4181
|
+
if (skipped.length > 0) hintLines.push(`skipped (Generalize?: no): ${skipped.length}`);
|
|
4182
|
+
return {
|
|
4183
|
+
exitCode,
|
|
4184
|
+
result: ok({
|
|
4185
|
+
scanned: entries.length,
|
|
4186
|
+
promoted,
|
|
4187
|
+
skipped,
|
|
4188
|
+
humanHint: hintLines.join("\n")
|
|
4189
|
+
})
|
|
4190
|
+
};
|
|
4191
|
+
}
|
|
4192
|
+
async function runCompoundDelete(input) {
|
|
4193
|
+
const projectDir = join27(input.vault, "projects", input.project);
|
|
4194
|
+
if (!existsSync7(projectDir)) {
|
|
4195
|
+
return {
|
|
4196
|
+
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
4197
|
+
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
const entryName = input.entry.replace(/\.md$/, "");
|
|
4201
|
+
const compoundPath = join27(projectDir, "compound", `${entryName}.md`);
|
|
4202
|
+
if (!existsSync7(compoundPath)) {
|
|
4203
|
+
return {
|
|
4204
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
4205
|
+
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
4206
|
+
};
|
|
4207
|
+
}
|
|
4208
|
+
try {
|
|
4209
|
+
await unlink2(compoundPath);
|
|
4210
|
+
} catch (e) {
|
|
4211
|
+
return {
|
|
4212
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4213
|
+
result: err("WRITE_FAILED", { file: compoundPath, message: String(e) })
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
const indexResult = await runProjectIndex({ vault: input.vault, slug: input.project, apply: true });
|
|
4217
|
+
if (!indexResult.result.ok) {
|
|
4218
|
+
return {
|
|
4219
|
+
exitCode: indexResult.exitCode,
|
|
4220
|
+
result: err("INDEX_REGEN_FAILED", { detail: indexResult.result })
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
return {
|
|
4224
|
+
exitCode: ExitCode.OK,
|
|
4225
|
+
result: ok({
|
|
4226
|
+
deleted: compoundPath,
|
|
4227
|
+
project: input.project,
|
|
4228
|
+
humanHint: `deleted: ${entryName}.md
|
|
4229
|
+
project: ${input.project}
|
|
4230
|
+
knowledge.md regenerated`
|
|
4231
|
+
})
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
async function runCompoundList(input) {
|
|
4235
|
+
const compoundDir = join27(input.vault, "projects", input.project, "compound");
|
|
4236
|
+
if (!existsSync7(compoundDir)) {
|
|
4237
|
+
return {
|
|
4238
|
+
exitCode: ExitCode.OK,
|
|
4239
|
+
result: ok({
|
|
4240
|
+
project: input.project,
|
|
4241
|
+
entries: [],
|
|
4242
|
+
humanHint: `project: ${input.project}
|
|
4243
|
+
entries: 0
|
|
4244
|
+
no compound directory found`
|
|
4245
|
+
})
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
let dirents;
|
|
4249
|
+
try {
|
|
4250
|
+
dirents = await readdir7(compoundDir, { withFileTypes: true });
|
|
4251
|
+
} catch {
|
|
4252
|
+
return {
|
|
4253
|
+
exitCode: ExitCode.OK,
|
|
4254
|
+
result: ok({
|
|
4255
|
+
project: input.project,
|
|
4256
|
+
entries: [],
|
|
4257
|
+
humanHint: `project: ${input.project}
|
|
4258
|
+
entries: 0
|
|
4259
|
+
could not read compound directory`
|
|
4260
|
+
})
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
const entries = [];
|
|
4264
|
+
for (const dirent of dirents) {
|
|
4265
|
+
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
4266
|
+
const filePath = join27(compoundDir, dirent.name);
|
|
4267
|
+
let text;
|
|
4268
|
+
try {
|
|
4269
|
+
text = await readFile18(filePath, "utf8");
|
|
4270
|
+
} catch {
|
|
4271
|
+
continue;
|
|
4272
|
+
}
|
|
4273
|
+
const fm = extractFrontmatter(text);
|
|
4274
|
+
if (!fm.ok) continue;
|
|
4275
|
+
const tags = Array.isArray(fm.data.tags) ? fm.data.tags.map((t) => String(t)) : typeof fm.data.tags === "string" ? fm.data.tags.split(",").map((s) => s.trim()) : [];
|
|
4276
|
+
entries.push({
|
|
4277
|
+
file: dirent.name,
|
|
4278
|
+
title: typeof fm.data.title === "string" ? fm.data.title : dirent.name.replace(/\.md$/, ""),
|
|
4279
|
+
type: typeof fm.data.type === "string" ? fm.data.type : "lesson",
|
|
4280
|
+
created: typeof fm.data.created === "string" ? fm.data.created : "",
|
|
4281
|
+
tags
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
const hint = entries.length > 0 ? [`project: ${input.project}`, `entries: ${entries.length}`, "", ...entries.map((e) => ` ${e.file}: ${e.title} (${e.type}, created: ${e.created || "unknown"}, tags: ${e.tags.join(", ") || "none"})`)].join("\n") : `project: ${input.project}
|
|
4285
|
+
entries: 0
|
|
4286
|
+
no compound entries found`;
|
|
4287
|
+
return {
|
|
4288
|
+
exitCode: ExitCode.OK,
|
|
4289
|
+
result: ok({
|
|
4290
|
+
project: input.project,
|
|
4291
|
+
entries,
|
|
4292
|
+
humanHint: hint
|
|
4293
|
+
})
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
// src/commands/observe.ts
|
|
4298
|
+
import { mkdir as mkdir10, writeFile as writeFile14 } from "fs/promises";
|
|
4299
|
+
import { existsSync as existsSync8, statSync as statSync2 } from "fs";
|
|
4300
|
+
import { join as join28 } from "path";
|
|
4301
|
+
import { createHash as createHash3 } from "crypto";
|
|
4302
|
+
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
4303
|
+
function slugify2(text) {
|
|
4304
|
+
const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4305
|
+
return words || "untitled";
|
|
4306
|
+
}
|
|
4307
|
+
async function runObserve(input) {
|
|
4308
|
+
const kind = input.kind || "note";
|
|
4309
|
+
if (!ALLOWED_KINDS.has(kind)) {
|
|
4310
|
+
return {
|
|
4311
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4312
|
+
result: err("SCHEME_REJECTED", {
|
|
4313
|
+
message: `Invalid kind "${kind}". Allowed: ${[...ALLOWED_KINDS].join(", ")}`
|
|
4314
|
+
})
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
if (!input.text || input.text.trim().length === 0) {
|
|
4318
|
+
return {
|
|
4319
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4320
|
+
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
if (!existsSync8(input.vault) || !statSync2(input.vault).isDirectory()) {
|
|
4324
|
+
return {
|
|
4325
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4326
|
+
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
4327
|
+
};
|
|
4328
|
+
}
|
|
4329
|
+
const transcriptsDir = join28(input.vault, "raw", "transcripts");
|
|
4330
|
+
try {
|
|
4331
|
+
await mkdir10(transcriptsDir, { recursive: true });
|
|
4332
|
+
} catch {
|
|
4333
|
+
return {
|
|
4334
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4335
|
+
result: err("VAULT_PATH_INVALID", { path: transcriptsDir })
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4339
|
+
const slug = slugify2(input.text);
|
|
4340
|
+
const fileName = `${today}-observation-${slug}.md`;
|
|
4341
|
+
const filePath = join28(transcriptsDir, fileName);
|
|
4342
|
+
const body = `
|
|
4343
|
+
${input.text.trim()}
|
|
4344
|
+
`;
|
|
4345
|
+
const sha256 = createHash3("sha256").update(Buffer.from(body, "utf8")).digest("hex");
|
|
4346
|
+
const frontmatterLines = [
|
|
4347
|
+
"---",
|
|
4348
|
+
"source_url:",
|
|
4349
|
+
`ingested: ${today}`,
|
|
4350
|
+
`sha256: ${sha256}`,
|
|
4351
|
+
`kind: ${kind}`
|
|
4352
|
+
];
|
|
4353
|
+
if (input.project) {
|
|
4354
|
+
frontmatterLines.push(`project: "[[${input.project}]]"`);
|
|
4355
|
+
}
|
|
4356
|
+
frontmatterLines.push("---");
|
|
4357
|
+
const content = frontmatterLines.join("\n") + body;
|
|
4358
|
+
try {
|
|
4359
|
+
await writeFile14(filePath, content, "utf8");
|
|
4360
|
+
} catch (e) {
|
|
4361
|
+
return {
|
|
4362
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4363
|
+
result: err("WRITE_FAILED", { path: filePath, message: String(e) })
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
appendLastOp(input.vault, {
|
|
4367
|
+
operation: "observe",
|
|
4368
|
+
summary: `created observation: ${slug}`,
|
|
4369
|
+
files: [`raw/transcripts/${fileName}`],
|
|
4370
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4371
|
+
});
|
|
4372
|
+
const relPath = `raw/transcripts/${fileName}`;
|
|
4373
|
+
const humanHint = `created ${relPath} (${sha256.slice(0, 12)}...)`;
|
|
4374
|
+
return {
|
|
4375
|
+
exitCode: ExitCode.OK,
|
|
4376
|
+
result: ok({ path: relPath, sha256, humanHint })
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
// src/commands/ingest.ts
|
|
4381
|
+
import { readFile as readFile19, writeFile as writeFile15, mkdir as mkdir11 } from "fs/promises";
|
|
4382
|
+
import { join as join29 } from "path";
|
|
4383
|
+
import { createHash as createHash4 } from "crypto";
|
|
4384
|
+
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
|
|
4385
|
+
var TYPE_DIR = {
|
|
4386
|
+
entity: "entities",
|
|
4387
|
+
concept: "concepts",
|
|
4388
|
+
comparison: "comparisons",
|
|
4389
|
+
query: "queries"
|
|
4390
|
+
};
|
|
4391
|
+
var ALLOWED_PROVENANCE = /* @__PURE__ */ new Set(["research", "project"]);
|
|
4392
|
+
function slugify3(text) {
|
|
4393
|
+
return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "untitled";
|
|
4394
|
+
}
|
|
4395
|
+
function isUrl(source) {
|
|
4396
|
+
try {
|
|
4397
|
+
const u = new URL(source);
|
|
4398
|
+
return u.protocol === "https:" || u.protocol === "http:";
|
|
4399
|
+
} catch {
|
|
4400
|
+
return false;
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
function todayIso() {
|
|
4404
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4405
|
+
}
|
|
4406
|
+
function buildRawContent(sourceUrl, ingested, sha256, body) {
|
|
4407
|
+
const lines = [
|
|
4408
|
+
"---",
|
|
4409
|
+
sourceUrl !== null ? `source_url: "${sourceUrl}"` : "source_url:",
|
|
4410
|
+
`ingested: ${ingested}`,
|
|
4411
|
+
`sha256: ${sha256}`,
|
|
4412
|
+
`ingested_by: wiki-ingest`,
|
|
4413
|
+
"---",
|
|
4414
|
+
"",
|
|
4415
|
+
body
|
|
4416
|
+
];
|
|
4417
|
+
return lines.join("\n");
|
|
4418
|
+
}
|
|
4419
|
+
function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance) {
|
|
4420
|
+
const aliases = [];
|
|
4421
|
+
const sourcesYaml = ` - ${rawRelPath}`;
|
|
4422
|
+
const tagsYaml = tags.length > 0 ? tags.map((t) => ` - ${t}`).join("\n") : " []";
|
|
4423
|
+
const fm = {
|
|
4424
|
+
title,
|
|
4425
|
+
aliases,
|
|
4426
|
+
created: ingested,
|
|
4427
|
+
updated: ingested,
|
|
4428
|
+
type,
|
|
4429
|
+
tags,
|
|
4430
|
+
sources: [rawRelPath],
|
|
4431
|
+
confidence: "medium"
|
|
4432
|
+
};
|
|
4433
|
+
if (provenance) {
|
|
4434
|
+
fm.provenance = provenance;
|
|
4435
|
+
}
|
|
4436
|
+
const fmLines = ["---"];
|
|
4437
|
+
fmLines.push(`title: "${title}"`);
|
|
4438
|
+
if (aliases.length > 0) {
|
|
4439
|
+
fmLines.push("aliases:");
|
|
4440
|
+
for (const a of aliases) fmLines.push(` - ${a}`);
|
|
4441
|
+
} else {
|
|
4442
|
+
fmLines.push("aliases: []");
|
|
4443
|
+
}
|
|
4444
|
+
fmLines.push(`created: ${ingested}`);
|
|
4445
|
+
fmLines.push(`updated: ${ingested}`);
|
|
4446
|
+
fmLines.push(`type: ${type}`);
|
|
4447
|
+
fmLines.push("tags:");
|
|
4448
|
+
fmLines.push(tagsYaml);
|
|
4449
|
+
fmLines.push("sources:");
|
|
4450
|
+
fmLines.push(sourcesYaml);
|
|
4451
|
+
fmLines.push("confidence: medium");
|
|
4452
|
+
if (provenance) {
|
|
4453
|
+
fmLines.push(`provenance: ${provenance}`);
|
|
4454
|
+
}
|
|
4455
|
+
fmLines.push("---");
|
|
4456
|
+
fmLines.push("");
|
|
4457
|
+
const body = [
|
|
4458
|
+
`# ${title}`,
|
|
4459
|
+
"",
|
|
4460
|
+
"## Overview",
|
|
4461
|
+
"",
|
|
4462
|
+
"## See also",
|
|
4463
|
+
"",
|
|
4464
|
+
"## Sources",
|
|
4465
|
+
"",
|
|
4466
|
+
`^[${rawRelPath}]`,
|
|
4467
|
+
""
|
|
4468
|
+
].join("\n");
|
|
4469
|
+
return fmLines.join("\n") + body;
|
|
4470
|
+
}
|
|
4471
|
+
async function runIngest(input) {
|
|
4472
|
+
if (!input.source || input.source.trim().length === 0) {
|
|
4473
|
+
return {
|
|
4474
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4475
|
+
result: err("SCHEME_REJECTED", { message: "source is required" })
|
|
4476
|
+
};
|
|
4477
|
+
}
|
|
4478
|
+
if (!input.vault || input.vault.trim().length === 0) {
|
|
4479
|
+
return {
|
|
4480
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4481
|
+
result: err("VAULT_PATH_INVALID", { message: "vault path is required" })
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4484
|
+
if (!input.type || !ALLOWED_TYPES.has(input.type)) {
|
|
4485
|
+
return {
|
|
4486
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4487
|
+
result: err("SCHEME_REJECTED", {
|
|
4488
|
+
message: `Invalid type "${input.type}". Allowed: ${[...ALLOWED_TYPES].join(", ")}`
|
|
4489
|
+
})
|
|
4490
|
+
};
|
|
4491
|
+
}
|
|
4492
|
+
if (!input.title || input.title.trim().length === 0) {
|
|
4493
|
+
return {
|
|
4494
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4495
|
+
result: err("SCHEME_REJECTED", { message: "title is required" })
|
|
4496
|
+
};
|
|
4497
|
+
}
|
|
4498
|
+
if (input.provenance && !ALLOWED_PROVENANCE.has(input.provenance)) {
|
|
4499
|
+
return {
|
|
4500
|
+
exitCode: ExitCode.SCHEME_REJECTED,
|
|
4501
|
+
result: err("SCHEME_REJECTED", {
|
|
4502
|
+
message: `Invalid provenance "${input.provenance}". Allowed: ${[...ALLOWED_PROVENANCE].join(", ")}`
|
|
4503
|
+
})
|
|
4504
|
+
};
|
|
4505
|
+
}
|
|
4506
|
+
let sourceContent;
|
|
4507
|
+
let sourceUrl = null;
|
|
4508
|
+
if (isUrl(input.source)) {
|
|
4509
|
+
sourceUrl = input.source;
|
|
4510
|
+
const guardResult = runFetchGuardSync({ url: input.source });
|
|
4511
|
+
if (!guardResult.result.ok) {
|
|
4512
|
+
return {
|
|
4513
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
4514
|
+
result: err("INGEST_VALIDATION_FAILED", {
|
|
4515
|
+
message: "source URL blocked by fetch-guard",
|
|
4516
|
+
guardError: guardResult.result.error,
|
|
4517
|
+
guardDetail: guardResult.result.detail
|
|
4518
|
+
})
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
const fetchResult = await controlledFetch(input.source, {
|
|
4522
|
+
timeoutMs: 15e3,
|
|
4523
|
+
maxBytes: 1024 * 1024,
|
|
4524
|
+
// 1 MB
|
|
4525
|
+
maxRedirects: 5
|
|
4526
|
+
});
|
|
4527
|
+
if (!fetchResult.ok) {
|
|
4528
|
+
return {
|
|
4529
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
4530
|
+
result: err("INGEST_VALIDATION_FAILED", {
|
|
4531
|
+
message: "failed to fetch source URL",
|
|
4532
|
+
fetchError: fetchResult.error,
|
|
4533
|
+
fetchDetail: fetchResult.detail
|
|
4534
|
+
})
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
sourceContent = fetchResult.data.body;
|
|
4538
|
+
} else {
|
|
4539
|
+
try {
|
|
4540
|
+
sourceContent = await readFile19(input.source, "utf8");
|
|
4541
|
+
} catch {
|
|
4542
|
+
return {
|
|
4543
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
4544
|
+
result: err("FILE_NOT_FOUND", { path: input.source })
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
const sha256 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
|
|
4549
|
+
const today = todayIso();
|
|
4550
|
+
const slug = slugify3(input.title);
|
|
4551
|
+
const tags = input.tags && input.tags.length > 0 ? input.tags : [];
|
|
4552
|
+
const rawRelPath = `raw/articles/${slug}.md`;
|
|
4553
|
+
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
4554
|
+
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
4555
|
+
const rawAbsPath = join29(input.vault, rawRelPath);
|
|
4556
|
+
const typedAbsPath = join29(input.vault, typedRelPath);
|
|
4557
|
+
const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
|
|
4558
|
+
const typedContent = buildTypedContent(
|
|
4559
|
+
input.title,
|
|
4560
|
+
today,
|
|
4561
|
+
input.type,
|
|
4562
|
+
tags,
|
|
4563
|
+
rawRelPath,
|
|
4564
|
+
input.provenance
|
|
4565
|
+
);
|
|
4566
|
+
if (input.dryRun) {
|
|
4567
|
+
return {
|
|
4568
|
+
exitCode: ExitCode.OK,
|
|
4569
|
+
result: ok({
|
|
4570
|
+
raw_path: rawRelPath,
|
|
4571
|
+
typed_path: typedRelPath,
|
|
4572
|
+
sha256,
|
|
4573
|
+
dry_run: true,
|
|
4574
|
+
humanHint: [
|
|
4575
|
+
`DRY RUN \u2014 would create:`,
|
|
4576
|
+
` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
|
|
4577
|
+
` ${typedRelPath}`,
|
|
4578
|
+
` type: ${input.type}, tags: [${tags.join(", ")}]`,
|
|
4579
|
+
input.provenance ? ` provenance: ${input.provenance}` : ""
|
|
4580
|
+
].filter(Boolean).join("\n")
|
|
4581
|
+
})
|
|
4582
|
+
};
|
|
4583
|
+
}
|
|
4584
|
+
const typedFm = {
|
|
4585
|
+
title: input.title,
|
|
4586
|
+
aliases: [],
|
|
4587
|
+
created: today,
|
|
4588
|
+
updated: today,
|
|
4589
|
+
type: input.type,
|
|
4590
|
+
tags,
|
|
4591
|
+
sources: [rawRelPath],
|
|
4592
|
+
confidence: "medium",
|
|
4593
|
+
...input.provenance ? { provenance: input.provenance } : {}
|
|
4594
|
+
};
|
|
4595
|
+
const det = detectSchema(typedFm);
|
|
4596
|
+
if (!det.schema) {
|
|
4597
|
+
return {
|
|
4598
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
4599
|
+
result: err("INGEST_VALIDATION_FAILED", {
|
|
4600
|
+
message: "generated typed-knowledge page could not be detected as a valid schema"
|
|
4601
|
+
})
|
|
4602
|
+
};
|
|
4603
|
+
}
|
|
4604
|
+
const parsed = TypedKnowledgeSchema.safeParse(typedFm);
|
|
4605
|
+
if (!parsed.success) {
|
|
4606
|
+
const errors = parsed.error.issues.map((i) => ({
|
|
4607
|
+
path: i.path.join("."),
|
|
4608
|
+
message: i.message
|
|
4609
|
+
}));
|
|
4610
|
+
return {
|
|
4611
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
4612
|
+
result: err("INGEST_VALIDATION_FAILED", {
|
|
4613
|
+
message: "generated typed-knowledge page failed schema validation",
|
|
4614
|
+
errors
|
|
4615
|
+
})
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
try {
|
|
4619
|
+
await mkdir11(join29(input.vault, "raw", "articles"), { recursive: true });
|
|
4620
|
+
await writeFile15(rawAbsPath, rawContent, "utf8");
|
|
4621
|
+
} catch (e) {
|
|
4622
|
+
return {
|
|
4623
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4624
|
+
result: err("WRITE_FAILED", { path: rawAbsPath, message: String(e) })
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
try {
|
|
4628
|
+
await mkdir11(join29(input.vault, typedDir), { recursive: true });
|
|
4629
|
+
await writeFile15(typedAbsPath, typedContent, "utf8");
|
|
4630
|
+
} catch (e) {
|
|
4631
|
+
return {
|
|
4632
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4633
|
+
result: err("WRITE_FAILED", { path: typedAbsPath, message: String(e) })
|
|
4634
|
+
};
|
|
4635
|
+
}
|
|
4636
|
+
const humanHint = [
|
|
4637
|
+
`created:`,
|
|
4638
|
+
` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
|
|
4639
|
+
` ${typedRelPath}`
|
|
4640
|
+
].join("\n");
|
|
4641
|
+
appendLastOp(input.vault, {
|
|
4642
|
+
operation: "ingest",
|
|
4643
|
+
summary: `added ${slug}`,
|
|
4644
|
+
files: [rawRelPath, typedRelPath],
|
|
4645
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4646
|
+
});
|
|
4647
|
+
return {
|
|
4648
|
+
exitCode: ExitCode.OK,
|
|
4649
|
+
result: ok({
|
|
4650
|
+
raw_path: rawRelPath,
|
|
4651
|
+
typed_path: typedRelPath,
|
|
4652
|
+
sha256,
|
|
4653
|
+
dry_run: false,
|
|
4654
|
+
humanHint
|
|
4655
|
+
})
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
// src/commands/tag-sync.ts
|
|
4660
|
+
import { writeFile as writeFile16 } from "fs/promises";
|
|
4661
|
+
var ENUM_MIRRORS = {
|
|
4662
|
+
provenance: ["research", "project", "mixed"],
|
|
4663
|
+
confidence: ["high", "medium", "low"]
|
|
4664
|
+
};
|
|
4665
|
+
function toNestedTag(field, value) {
|
|
4666
|
+
return `${field}/${value}`;
|
|
4667
|
+
}
|
|
4668
|
+
function expectedNestedTags(fm) {
|
|
4669
|
+
const expected = /* @__PURE__ */ new Set();
|
|
4670
|
+
for (const [field, allowedValues] of Object.entries(ENUM_MIRRORS)) {
|
|
4671
|
+
const value = fm[field];
|
|
4672
|
+
if (typeof value === "string" && allowedValues.includes(value)) {
|
|
4673
|
+
expected.add(toNestedTag(field, value));
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
return expected;
|
|
4677
|
+
}
|
|
4678
|
+
function parseTagsFromYaml(rawFm) {
|
|
4679
|
+
const inlineMatch = rawFm.match(/^tags:\s*\[([^\]]*)\]/m);
|
|
4680
|
+
if (inlineMatch) {
|
|
4681
|
+
return inlineMatch[1].split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).filter((t) => t.length > 0);
|
|
4682
|
+
}
|
|
4683
|
+
const lines = rawFm.split("\n");
|
|
4684
|
+
const tagItems = [];
|
|
4685
|
+
let inTags = false;
|
|
4686
|
+
for (const line of lines) {
|
|
4687
|
+
if (/^tags:\s*$/.test(line)) {
|
|
4688
|
+
inTags = true;
|
|
4689
|
+
continue;
|
|
4690
|
+
}
|
|
4691
|
+
if (inTags) {
|
|
4692
|
+
if (/^\s+-\s+/.test(line) && !/^\s+-\s+\[\[/.test(line)) {
|
|
4693
|
+
const value = line.replace(/^\s+-\s+/, "").trim().replace(/^['"]|['"]$/g, "");
|
|
4694
|
+
if (value.length > 0) tagItems.push(value);
|
|
4695
|
+
} else {
|
|
4696
|
+
break;
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
return tagItems;
|
|
4701
|
+
}
|
|
4702
|
+
function rebuildTagsSection(rawFm, existingTags, toAdd) {
|
|
4703
|
+
const allTags = [...existingTags, ...toAdd];
|
|
4704
|
+
const tagsLine = `tags: [${allTags.join(", ")}]`;
|
|
4705
|
+
if (/^tags:\s*\[/m.test(rawFm)) {
|
|
4706
|
+
return rawFm.replace(/^tags:\s*\[[^\]]*\]/m, tagsLine);
|
|
4707
|
+
}
|
|
4708
|
+
const lines = rawFm.split("\n");
|
|
4709
|
+
const out = [];
|
|
4710
|
+
let inTags = false;
|
|
4711
|
+
let tagsReplaced = false;
|
|
4712
|
+
for (const line of lines) {
|
|
4713
|
+
if (/^tags:\s*$/.test(line)) {
|
|
4714
|
+
inTags = true;
|
|
4715
|
+
if (!tagsReplaced) {
|
|
4716
|
+
out.push(tagsLine);
|
|
4717
|
+
tagsReplaced = true;
|
|
4718
|
+
}
|
|
4719
|
+
continue;
|
|
4720
|
+
}
|
|
4721
|
+
if (inTags) {
|
|
4722
|
+
if (/^\s+-\s+/.test(line) && !/^\s+-\s+\[\[/.test(line)) {
|
|
4723
|
+
continue;
|
|
4724
|
+
} else {
|
|
4725
|
+
inTags = false;
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
out.push(line);
|
|
4729
|
+
}
|
|
4730
|
+
if (!tagsReplaced) {
|
|
4731
|
+
out.push(tagsLine);
|
|
4732
|
+
}
|
|
4733
|
+
return out.join("\n");
|
|
4734
|
+
}
|
|
4735
|
+
async function runTagSync(input) {
|
|
4736
|
+
const scan = await scanVault(input.vault);
|
|
4737
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
4738
|
+
const synced = [];
|
|
4739
|
+
let unchanged = 0;
|
|
4740
|
+
for (const page of scan.data.typedKnowledge) {
|
|
4741
|
+
const text = await readPage(page);
|
|
4742
|
+
const split = splitFrontmatter(text);
|
|
4743
|
+
if (!split.ok) {
|
|
4744
|
+
unchanged++;
|
|
4745
|
+
continue;
|
|
4746
|
+
}
|
|
4747
|
+
const { rawFrontmatter, body } = split.data;
|
|
4748
|
+
const fm = {};
|
|
4749
|
+
for (const [field, allowedValues] of Object.entries(ENUM_MIRRORS)) {
|
|
4750
|
+
for (const v of allowedValues) {
|
|
4751
|
+
if (rawFrontmatter.includes(`${field}: ${v}`)) {
|
|
4752
|
+
fm[field] = v;
|
|
4753
|
+
break;
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
const expected = expectedNestedTags(fm);
|
|
4758
|
+
if (expected.size === 0) {
|
|
4759
|
+
unchanged++;
|
|
4760
|
+
continue;
|
|
4761
|
+
}
|
|
4762
|
+
const existingTags = parseTagsFromYaml(rawFrontmatter);
|
|
4763
|
+
const existingSet = new Set(existingTags);
|
|
4764
|
+
const toAdd = [...expected].filter((t) => !existingSet.has(t));
|
|
4765
|
+
if (toAdd.length === 0) {
|
|
4766
|
+
unchanged++;
|
|
4767
|
+
continue;
|
|
4768
|
+
}
|
|
4769
|
+
const newFm = rebuildTagsSection(rawFrontmatter, existingTags, toAdd);
|
|
4770
|
+
const newText = `---
|
|
4771
|
+
${newFm}
|
|
4772
|
+
---
|
|
4773
|
+
${body}`;
|
|
4774
|
+
if (!input.dryRun) {
|
|
4775
|
+
await writeFile16(page.absPath, newText, "utf8");
|
|
4776
|
+
}
|
|
4777
|
+
synced.push(page.relPath);
|
|
4778
|
+
}
|
|
4779
|
+
if (!input.dryRun && synced.length > 0) {
|
|
4780
|
+
appendLastOp(input.vault, {
|
|
4781
|
+
operation: "tag-sync",
|
|
4782
|
+
summary: `synced tags on ${synced.length} pages`,
|
|
4783
|
+
files: synced,
|
|
4784
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4785
|
+
});
|
|
4786
|
+
}
|
|
4787
|
+
const exitCode = synced.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
4788
|
+
const hintLines = [`scanned: ${synced.length + unchanged}`];
|
|
4789
|
+
if (synced.length > 0) hintLines.push(`synced: ${synced.length}`);
|
|
4790
|
+
if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
|
|
4791
|
+
if (input.dryRun && synced.length > 0) hintLines.push("(dry run \u2014 no files written)");
|
|
4792
|
+
return {
|
|
4793
|
+
exitCode,
|
|
4794
|
+
result: ok({
|
|
4795
|
+
scanned: synced.length + unchanged,
|
|
4796
|
+
synced,
|
|
4797
|
+
unchanged,
|
|
4798
|
+
humanHint: hintLines.join("\n")
|
|
4799
|
+
})
|
|
4800
|
+
};
|
|
4801
|
+
}
|
|
4802
|
+
|
|
4803
|
+
// src/commands/sync.ts
|
|
4804
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4805
|
+
import { join as join30 } from "path";
|
|
4806
|
+
function runSyncStatus(input) {
|
|
4807
|
+
const vault = input.vault;
|
|
4808
|
+
if (!existsSync9(join30(vault, ".git"))) {
|
|
4809
|
+
return {
|
|
4810
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4811
|
+
result: ok({
|
|
4812
|
+
is_git_repo: false,
|
|
4813
|
+
dirty: 0,
|
|
4814
|
+
ahead: 0,
|
|
4815
|
+
behind: 0,
|
|
4816
|
+
last_commit: "never",
|
|
4817
|
+
status: "not_a_repo",
|
|
4818
|
+
humanHint: "not a git repository"
|
|
4819
|
+
})
|
|
4820
|
+
};
|
|
4821
|
+
}
|
|
4822
|
+
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
4823
|
+
const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
|
|
4824
|
+
const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
|
|
4825
|
+
let ahead = 0;
|
|
4826
|
+
let behind = 0;
|
|
4827
|
+
if (revOutput) {
|
|
4828
|
+
const parts = revOutput.split(/\s+/);
|
|
4829
|
+
behind = parseInt(parts[0], 10) || 0;
|
|
4830
|
+
ahead = parseInt(parts[1], 10) || 0;
|
|
4831
|
+
}
|
|
4832
|
+
const tsRaw = git(vault, ["log", "-1", "--format=%ct"]);
|
|
4833
|
+
let last_commit;
|
|
4834
|
+
if (tsRaw) {
|
|
4835
|
+
const ts = parseInt(tsRaw, 10);
|
|
4836
|
+
if (!isNaN(ts) && ts > 0) {
|
|
4837
|
+
last_commit = new Date(ts * 1e3).toISOString();
|
|
4838
|
+
} else {
|
|
4839
|
+
last_commit = "never";
|
|
4840
|
+
}
|
|
4841
|
+
} else {
|
|
4842
|
+
last_commit = "never";
|
|
4843
|
+
}
|
|
4844
|
+
let status;
|
|
4845
|
+
if (dirty > 0) {
|
|
4846
|
+
status = "dirty";
|
|
4847
|
+
} else if (ahead > 0) {
|
|
4848
|
+
status = "ahead";
|
|
4849
|
+
} else if (behind > 0) {
|
|
4850
|
+
status = "behind";
|
|
4851
|
+
} else {
|
|
4852
|
+
status = "clean";
|
|
4853
|
+
}
|
|
4854
|
+
const hintLines = [
|
|
4855
|
+
`status: ${status}`,
|
|
4856
|
+
`dirty: ${dirty}`,
|
|
4857
|
+
`ahead: ${ahead}`,
|
|
4858
|
+
`behind: ${behind}`,
|
|
4859
|
+
`last_commit: ${last_commit}`
|
|
4860
|
+
];
|
|
4861
|
+
const exitCode = status === "clean" ? ExitCode.OK : ExitCode.LINT_HAS_WARNINGS;
|
|
4862
|
+
return {
|
|
4863
|
+
exitCode,
|
|
4864
|
+
result: ok({
|
|
4865
|
+
is_git_repo: true,
|
|
4866
|
+
dirty,
|
|
4867
|
+
ahead,
|
|
4868
|
+
behind,
|
|
4869
|
+
last_commit,
|
|
4870
|
+
status,
|
|
4871
|
+
humanHint: hintLines.join("\n")
|
|
4872
|
+
})
|
|
4873
|
+
};
|
|
4874
|
+
}
|
|
4875
|
+
async function runSyncPush(input) {
|
|
4876
|
+
const vault = input.vault;
|
|
4877
|
+
if (!existsSync9(join30(vault, ".git"))) {
|
|
4878
|
+
return {
|
|
4879
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4880
|
+
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
4881
|
+
};
|
|
4882
|
+
}
|
|
4883
|
+
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
4884
|
+
const dirtyFiles = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0) : [];
|
|
4885
|
+
if (dirtyFiles.length === 0) {
|
|
4886
|
+
return {
|
|
4887
|
+
exitCode: ExitCode.OK,
|
|
4888
|
+
result: ok({
|
|
4889
|
+
files_committed: 0,
|
|
4890
|
+
commit_message: "",
|
|
4891
|
+
pushed: false,
|
|
4892
|
+
humanHint: "nothing to commit, working tree clean"
|
|
4893
|
+
})
|
|
4894
|
+
};
|
|
4895
|
+
}
|
|
4896
|
+
const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
|
|
4897
|
+
if (lintResult.result.ok && lintResult.result.data.summary.errors > 0) {
|
|
4898
|
+
return {
|
|
4899
|
+
exitCode: ExitCode.LINT_HAS_ERRORS,
|
|
4900
|
+
result: err("LINT_ERRORS_BLOCK_PUSH", {
|
|
4901
|
+
errors: lintResult.result.data.summary.errors,
|
|
4902
|
+
buckets: lintResult.result.data.by_severity.error
|
|
4903
|
+
})
|
|
4904
|
+
};
|
|
4905
|
+
}
|
|
4906
|
+
try {
|
|
4907
|
+
gitStrict(vault, ["add", "-A"]);
|
|
4908
|
+
try {
|
|
4909
|
+
gitStrict(vault, ["reset", "HEAD", "--", ".skillwiki/last-op.json"]);
|
|
4910
|
+
} catch {
|
|
4911
|
+
}
|
|
4912
|
+
} catch (e) {
|
|
4913
|
+
return {
|
|
4914
|
+
exitCode: ExitCode.SYNC_PUSH_FAILED,
|
|
4915
|
+
result: err("GIT_ADD_FAILED", { message: String(e) })
|
|
4916
|
+
};
|
|
4917
|
+
}
|
|
4918
|
+
const lastOps = readLastOp(vault);
|
|
4919
|
+
let commitMessage;
|
|
4920
|
+
if (lastOps.length > 0) {
|
|
4921
|
+
commitMessage = lastOps.map((op) => `${op.operation}: ${op.summary} (${op.files.length} files)`).join("; ");
|
|
4922
|
+
} else {
|
|
4923
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
4924
|
+
commitMessage = `sync: vault update ${timestamp}`;
|
|
4925
|
+
}
|
|
4926
|
+
try {
|
|
4927
|
+
gitStrict(vault, ["commit", "-m", commitMessage]);
|
|
4928
|
+
} catch (e) {
|
|
4929
|
+
return {
|
|
4930
|
+
exitCode: ExitCode.SYNC_PUSH_FAILED,
|
|
4931
|
+
result: err("GIT_COMMIT_FAILED", { message: String(e) })
|
|
4932
|
+
};
|
|
4933
|
+
}
|
|
4934
|
+
clearLastOp(vault);
|
|
4935
|
+
let pushed = false;
|
|
4936
|
+
try {
|
|
4937
|
+
gitStrict(vault, ["push", "origin", "HEAD"]);
|
|
4938
|
+
pushed = true;
|
|
4939
|
+
} catch (e) {
|
|
4940
|
+
return {
|
|
4941
|
+
exitCode: ExitCode.SYNC_PUSH_FAILED,
|
|
4942
|
+
result: ok({
|
|
4943
|
+
files_committed: dirtyFiles.length,
|
|
4944
|
+
commit_message: commitMessage,
|
|
4945
|
+
pushed: false,
|
|
4946
|
+
humanHint: `committed ${dirtyFiles.length} file(s) but push failed: ${String(e)}`
|
|
4947
|
+
})
|
|
4948
|
+
};
|
|
4949
|
+
}
|
|
4950
|
+
return {
|
|
4951
|
+
exitCode: ExitCode.OK,
|
|
4952
|
+
result: ok({
|
|
4953
|
+
files_committed: dirtyFiles.length,
|
|
4954
|
+
commit_message: commitMessage,
|
|
4955
|
+
pushed,
|
|
4956
|
+
humanHint: `committed and pushed ${dirtyFiles.length} file(s)`
|
|
4957
|
+
})
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4960
|
+
async function runSyncPull(input) {
|
|
4961
|
+
const vault = input.vault;
|
|
4962
|
+
if (!existsSync9(join30(vault, ".git"))) {
|
|
4963
|
+
return {
|
|
4964
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4965
|
+
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
4966
|
+
};
|
|
4967
|
+
}
|
|
4968
|
+
let fetched = false;
|
|
4969
|
+
try {
|
|
4970
|
+
gitStrict(vault, ["fetch", "origin"]);
|
|
4971
|
+
fetched = true;
|
|
4972
|
+
} catch (e) {
|
|
4973
|
+
return {
|
|
4974
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
4975
|
+
result: err("GIT_FETCH_FAILED", { message: String(e) })
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
let pulled = false;
|
|
4979
|
+
let conflicts = 0;
|
|
4980
|
+
let filesUpdated = 0;
|
|
4981
|
+
try {
|
|
4982
|
+
const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
|
|
4983
|
+
pulled = true;
|
|
4984
|
+
const fileMatch = pullOutput.match(/(\d+) file[s]? changed/);
|
|
4985
|
+
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
4986
|
+
} catch (e) {
|
|
4987
|
+
const errString = String(e);
|
|
4988
|
+
if (errString.includes("conflict")) {
|
|
4989
|
+
const porcelain = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
|
|
4990
|
+
conflicts = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
|
|
4991
|
+
return {
|
|
4992
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
4993
|
+
result: ok({
|
|
4994
|
+
fetched,
|
|
4995
|
+
pulled: false,
|
|
4996
|
+
files_updated: 0,
|
|
4997
|
+
conflicts,
|
|
4998
|
+
lint_errors: 0,
|
|
4999
|
+
lint_warnings: 0,
|
|
5000
|
+
humanHint: `pull failed with ${conflicts} conflict(s) \u2014 resolve manually`
|
|
5001
|
+
})
|
|
5002
|
+
};
|
|
5003
|
+
}
|
|
5004
|
+
return {
|
|
5005
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
5006
|
+
result: err("GIT_PULL_FAILED", { message: errString })
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
let lintErrors = 0;
|
|
5010
|
+
let lintWarnings = 0;
|
|
5011
|
+
const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
|
|
5012
|
+
if (lintResult.result.ok) {
|
|
5013
|
+
lintErrors = lintResult.result.data.summary.errors;
|
|
5014
|
+
lintWarnings = lintResult.result.data.summary.warnings;
|
|
5015
|
+
}
|
|
5016
|
+
const hintParts = [];
|
|
5017
|
+
if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
|
|
5018
|
+
else hintParts.push("already up to date");
|
|
5019
|
+
if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
|
|
5020
|
+
if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
|
|
5021
|
+
const exitCode = lintErrors > 0 ? ExitCode.LINT_HAS_ERRORS : lintWarnings > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
|
|
5022
|
+
return {
|
|
5023
|
+
exitCode,
|
|
5024
|
+
result: ok({
|
|
5025
|
+
fetched,
|
|
5026
|
+
pulled,
|
|
5027
|
+
files_updated: filesUpdated,
|
|
5028
|
+
conflicts,
|
|
5029
|
+
lint_errors: lintErrors,
|
|
5030
|
+
lint_warnings: lintWarnings,
|
|
5031
|
+
humanHint: hintParts.join(", ")
|
|
5032
|
+
})
|
|
5033
|
+
};
|
|
5034
|
+
}
|
|
5035
|
+
|
|
5036
|
+
// src/commands/backup.ts
|
|
5037
|
+
import { statSync as statSync3, readdirSync as readdirSync2, readFileSync as readFileSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5038
|
+
import { join as join31, relative as relative3, dirname as dirname11 } from "path";
|
|
5039
|
+
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
5040
|
+
|
|
5041
|
+
// src/utils/s3-client.ts
|
|
5042
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
5043
|
+
function createS3Client(config) {
|
|
5044
|
+
const clientConfig = {
|
|
5045
|
+
endpoint: config.endpoint,
|
|
5046
|
+
region: config.region,
|
|
5047
|
+
credentials: {
|
|
5048
|
+
accessKeyId: config.accessKeyId,
|
|
5049
|
+
secretAccessKey: config.secretAccessKey
|
|
5050
|
+
},
|
|
5051
|
+
forcePathStyle: true
|
|
5052
|
+
// Required for SeaweedFS / MinIO
|
|
5053
|
+
};
|
|
5054
|
+
return new S3Client(clientConfig);
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
// src/commands/backup.ts
|
|
5058
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_modules", ".skillwiki"]);
|
|
5059
|
+
function* walkMarkdown(dir, base) {
|
|
5060
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
5061
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
5062
|
+
const full = join31(dir, entry.name);
|
|
5063
|
+
if (entry.isDirectory()) {
|
|
5064
|
+
yield* walkMarkdown(full, base);
|
|
5065
|
+
} else if (entry.name.endsWith(".md")) {
|
|
5066
|
+
yield relative3(base, full).replace(/\\/g, "/");
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
async function runBackupSync(input) {
|
|
5071
|
+
if (!input.accessKeyId || !input.secretAccessKey) {
|
|
5072
|
+
return {
|
|
5073
|
+
exitCode: ExitCode.BACKUP_SYNC_FAILED,
|
|
5074
|
+
result: err("BACKUP_SYNC_FAILED", {
|
|
5075
|
+
message: "Backup credentials not configured. Run: skillwiki config set BACKUP_ACCESS_KEY_ID <key>"
|
|
5076
|
+
})
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
5079
|
+
const client = createS3Client(input);
|
|
5080
|
+
let uploaded = 0;
|
|
5081
|
+
let skipped = 0;
|
|
5082
|
+
let failed = 0;
|
|
5083
|
+
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
5084
|
+
for (const relPath of files) {
|
|
5085
|
+
const absPath = join31(input.vault, relPath);
|
|
5086
|
+
const localStat = statSync3(absPath);
|
|
5087
|
+
let needsUpload = true;
|
|
5088
|
+
try {
|
|
5089
|
+
const head = await client.send(new HeadObjectCommand({ Bucket: input.bucket, Key: relPath }));
|
|
5090
|
+
if (head.LastModified && head.LastModified >= localStat.mtime) {
|
|
5091
|
+
needsUpload = false;
|
|
5092
|
+
}
|
|
5093
|
+
} catch {
|
|
5094
|
+
}
|
|
5095
|
+
if (!needsUpload) {
|
|
5096
|
+
skipped++;
|
|
5097
|
+
continue;
|
|
5098
|
+
}
|
|
5099
|
+
if (input.dryRun) {
|
|
5100
|
+
uploaded++;
|
|
5101
|
+
continue;
|
|
5102
|
+
}
|
|
5103
|
+
try {
|
|
5104
|
+
const body = readFileSync8(absPath);
|
|
5105
|
+
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
5106
|
+
uploaded++;
|
|
5107
|
+
} catch {
|
|
5108
|
+
failed++;
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
let pruned = 0;
|
|
5112
|
+
if (input.prune && !input.dryRun) {
|
|
5113
|
+
try {
|
|
5114
|
+
const localSet = new Set(files);
|
|
5115
|
+
const list = await client.send(new ListObjectsV2Command({ Bucket: input.bucket }));
|
|
5116
|
+
const toDelete = (list.Contents ?? []).filter((obj) => obj.Key && !localSet.has(obj.Key)).map((obj) => ({ Key: obj.Key }));
|
|
5117
|
+
if (toDelete.length > 0) {
|
|
5118
|
+
await client.send(new DeleteObjectsCommand({ Bucket: input.bucket, Delete: { Objects: toDelete } }));
|
|
5119
|
+
pruned = toDelete.length;
|
|
5120
|
+
}
|
|
5121
|
+
} catch {
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
const hintParts = [];
|
|
5125
|
+
if (input.dryRun) hintParts.push("DRY RUN \u2014");
|
|
5126
|
+
hintParts.push(`scanned: ${files.length}, uploaded: ${uploaded}, skipped: ${skipped}`);
|
|
5127
|
+
if (failed > 0) hintParts.push(`failed: ${failed}`);
|
|
5128
|
+
if (pruned > 0) hintParts.push(`pruned: ${pruned}`);
|
|
5129
|
+
return {
|
|
5130
|
+
exitCode: failed > 0 ? ExitCode.BACKUP_SYNC_FAILED : ExitCode.OK,
|
|
5131
|
+
result: ok({
|
|
5132
|
+
scanned: files.length,
|
|
5133
|
+
uploaded,
|
|
5134
|
+
skipped,
|
|
5135
|
+
failed,
|
|
5136
|
+
pruned,
|
|
5137
|
+
dry_run: input.dryRun ?? false,
|
|
5138
|
+
humanHint: hintParts.join(", ")
|
|
5139
|
+
})
|
|
5140
|
+
};
|
|
5141
|
+
}
|
|
5142
|
+
async function runBackupRestore(input) {
|
|
5143
|
+
if (!input.accessKeyId || !input.secretAccessKey) {
|
|
5144
|
+
return {
|
|
5145
|
+
exitCode: ExitCode.BACKUP_SYNC_FAILED,
|
|
5146
|
+
result: err("BACKUP_SYNC_FAILED", {
|
|
5147
|
+
message: "Backup credentials not configured. Run: skillwiki config set BACKUP_ACCESS_KEY_ID <key>"
|
|
5148
|
+
})
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
const client = createS3Client(input);
|
|
5152
|
+
const target = input.target ?? input.vault;
|
|
5153
|
+
let downloaded = 0;
|
|
5154
|
+
let skipped = 0;
|
|
5155
|
+
let conflicts = 0;
|
|
5156
|
+
try {
|
|
5157
|
+
const list = await client.send(new ListObjectsV2Command({ Bucket: input.bucket }));
|
|
5158
|
+
const objects = list.Contents ?? [];
|
|
5159
|
+
for (const obj of objects) {
|
|
5160
|
+
if (!obj.Key) continue;
|
|
5161
|
+
const localPath = join31(target, obj.Key);
|
|
5162
|
+
try {
|
|
5163
|
+
const localStat = statSync3(localPath);
|
|
5164
|
+
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
5165
|
+
conflicts++;
|
|
5166
|
+
continue;
|
|
5167
|
+
}
|
|
5168
|
+
} catch {
|
|
5169
|
+
}
|
|
5170
|
+
try {
|
|
5171
|
+
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
5172
|
+
const body = await resp.Body?.transformToByteArray();
|
|
5173
|
+
if (body) {
|
|
5174
|
+
mkdirSync3(dirname11(localPath), { recursive: true });
|
|
5175
|
+
writeFileSync4(localPath, Buffer.from(body));
|
|
5176
|
+
downloaded++;
|
|
5177
|
+
}
|
|
5178
|
+
} catch {
|
|
5179
|
+
skipped++;
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
} catch (e) {
|
|
5183
|
+
return {
|
|
5184
|
+
exitCode: ExitCode.BACKUP_SYNC_FAILED,
|
|
5185
|
+
result: err("BACKUP_SYNC_FAILED", { message: `Failed to list bucket: ${String(e)}` })
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
const hintParts = [`downloaded: ${downloaded}`];
|
|
5189
|
+
if (skipped > 0) hintParts.push(`skipped: ${skipped}`);
|
|
5190
|
+
if (conflicts > 0) hintParts.push(`conflicts: ${conflicts} (local is newer)`);
|
|
5191
|
+
if (downloaded > 0) {
|
|
5192
|
+
appendLastOp(target, {
|
|
5193
|
+
operation: "backup-restore",
|
|
5194
|
+
summary: `restored ${downloaded} files from S3`,
|
|
5195
|
+
files: [],
|
|
5196
|
+
// Don't enumerate potentially hundreds of files
|
|
5197
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5198
|
+
});
|
|
5199
|
+
}
|
|
5200
|
+
return {
|
|
5201
|
+
exitCode: conflicts > 0 ? ExitCode.BACKUP_RESTORE_CONFLICTS : ExitCode.OK,
|
|
5202
|
+
result: ok({ downloaded, skipped, conflicts, humanHint: hintParts.join(", ") })
|
|
5203
|
+
};
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
// src/commands/status.ts
|
|
5207
|
+
import { existsSync as existsSync10, statSync as statSync4 } from "fs";
|
|
5208
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
5209
|
+
import { join as join32 } from "path";
|
|
5210
|
+
async function runStatus(input) {
|
|
5211
|
+
if (!existsSync10(input.vault)) {
|
|
5212
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
5213
|
+
}
|
|
5214
|
+
const scan = await scanVault(input.vault);
|
|
5215
|
+
if (!scan.ok) {
|
|
5216
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
5217
|
+
}
|
|
5218
|
+
const typedCounts = { entities: 0, concepts: 0, comparisons: 0, queries: 0, meta: 0 };
|
|
5219
|
+
for (const page of scan.data.typedKnowledge) {
|
|
5220
|
+
const segment = page.relPath.split("/")[0];
|
|
5221
|
+
if (segment in typedCounts) {
|
|
5222
|
+
typedCounts[segment]++;
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
let rawArticles = 0;
|
|
5226
|
+
let rawTranscripts = 0;
|
|
5227
|
+
for (const page of scan.data.raw) {
|
|
5228
|
+
const parts = page.relPath.split("/");
|
|
5229
|
+
if (parts[1] === "transcripts") rawTranscripts++;
|
|
5230
|
+
else rawArticles++;
|
|
5231
|
+
}
|
|
5232
|
+
const workItems = scan.data.workItems.length;
|
|
5233
|
+
const compound = scan.data.compound.length;
|
|
5234
|
+
let schemaVersion = "v1";
|
|
2838
5235
|
try {
|
|
2839
|
-
const
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
const existingPages = new Set(existingEntries.map((l) => {
|
|
2843
|
-
const m = l.match(/\[\[([^\]]+)\]\]/);
|
|
2844
|
-
return m ? m[1] : "";
|
|
2845
|
-
}));
|
|
2846
|
-
const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
|
|
2847
|
-
stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
|
|
5236
|
+
const schemaContent = await readFile20(join32(input.vault, "SCHEMA.md"), "utf8");
|
|
5237
|
+
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
5238
|
+
if (versionMatch) schemaVersion = versionMatch[1];
|
|
2848
5239
|
} catch {
|
|
2849
5240
|
}
|
|
2850
|
-
const
|
|
2851
|
-
const
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
5241
|
+
const langResult = await resolveLang({ flag: void 0, envValue: input.langEnvValue, home: input.home });
|
|
5242
|
+
const allPages = [
|
|
5243
|
+
...scan.data.typedKnowledge,
|
|
5244
|
+
...scan.data.raw,
|
|
5245
|
+
...scan.data.workItems,
|
|
5246
|
+
...scan.data.compound
|
|
5247
|
+
];
|
|
5248
|
+
let lastModified = "";
|
|
5249
|
+
let maxTime = 0;
|
|
5250
|
+
for (const page of allPages) {
|
|
5251
|
+
try {
|
|
5252
|
+
const st = statSync4(page.absPath);
|
|
5253
|
+
if (st.mtimeMs > maxTime) {
|
|
5254
|
+
maxTime = st.mtimeMs;
|
|
5255
|
+
lastModified = st.mtime.toISOString();
|
|
5256
|
+
}
|
|
5257
|
+
} catch {
|
|
5258
|
+
}
|
|
2856
5259
|
}
|
|
2857
|
-
|
|
5260
|
+
const pageCounts = {
|
|
5261
|
+
entities: typedCounts.entities,
|
|
5262
|
+
concepts: typedCounts.concepts,
|
|
5263
|
+
comparisons: typedCounts.comparisons,
|
|
5264
|
+
queries: typedCounts.queries,
|
|
5265
|
+
meta: typedCounts.meta,
|
|
5266
|
+
raw_articles: rawArticles,
|
|
5267
|
+
raw_transcripts: rawTranscripts,
|
|
5268
|
+
work_items: workItems,
|
|
5269
|
+
compound
|
|
5270
|
+
};
|
|
5271
|
+
const totalPages = Object.values(pageCounts).reduce((a, b) => a + b, 0);
|
|
5272
|
+
const rawTotal = rawArticles + rawTranscripts;
|
|
5273
|
+
const humanHint = [
|
|
5274
|
+
`vault: ${input.vault}`,
|
|
5275
|
+
`lang: ${langResult.value}`,
|
|
5276
|
+
`total: ${totalPages} pages`,
|
|
5277
|
+
` entities: ${pageCounts.entities} concepts: ${pageCounts.concepts} comparisons: ${pageCounts.comparisons} queries: ${pageCounts.queries} meta: ${pageCounts.meta}`,
|
|
5278
|
+
` raw: ${rawTotal} work_items: ${workItems} compound: ${compound}`,
|
|
5279
|
+
`last modified: ${lastModified.slice(0, 10)}`
|
|
5280
|
+
].join("\n");
|
|
5281
|
+
return {
|
|
5282
|
+
exitCode: ExitCode.OK,
|
|
5283
|
+
result: ok({
|
|
5284
|
+
vault_path: input.vault,
|
|
5285
|
+
schema_version: schemaVersion,
|
|
5286
|
+
lang: langResult.canonical,
|
|
5287
|
+
page_counts: pageCounts,
|
|
5288
|
+
total_pages: totalPages,
|
|
5289
|
+
last_modified: lastModified,
|
|
5290
|
+
humanHint
|
|
5291
|
+
})
|
|
5292
|
+
};
|
|
5293
|
+
}
|
|
2858
5294
|
|
|
2859
|
-
|
|
5295
|
+
// src/commands/seed.ts
|
|
5296
|
+
import { mkdir as mkdir12, writeFile as writeFile17, stat as stat7 } from "fs/promises";
|
|
5297
|
+
import { join as join33 } from "path";
|
|
5298
|
+
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5299
|
+
var EXAMPLE_PAGES = {
|
|
5300
|
+
"entities/example-project.md": `---
|
|
5301
|
+
title: Example Project
|
|
5302
|
+
aliases: [example-project]
|
|
5303
|
+
created: ${TODAY}
|
|
5304
|
+
updated: ${TODAY}
|
|
5305
|
+
type: entity
|
|
5306
|
+
tags: [research]
|
|
5307
|
+
sources: []
|
|
5308
|
+
confidence: medium
|
|
5309
|
+
provenance: research
|
|
5310
|
+
---
|
|
2860
5311
|
|
|
2861
|
-
|
|
2862
|
-
for (const [type, items] of grouped) {
|
|
2863
|
-
body += `## ${type}
|
|
5312
|
+
# Example Project
|
|
2864
5313
|
|
|
5314
|
+
## Overview
|
|
5315
|
+
|
|
5316
|
+
This is a seed entity page demonstrating the typed-knowledge format. Replace it with a real entity from your research.
|
|
5317
|
+
|
|
5318
|
+
## Key Facts
|
|
5319
|
+
|
|
5320
|
+
- This vault was seeded on ${TODAY}
|
|
5321
|
+
- Entity pages describe people, organizations, products, or projects
|
|
5322
|
+
- Each page should cite sources from the \`raw/\` directory
|
|
5323
|
+
`,
|
|
5324
|
+
"concepts/example-concept.md": `---
|
|
5325
|
+
title: Example Concept
|
|
5326
|
+
aliases: [example-concept]
|
|
5327
|
+
created: ${TODAY}
|
|
5328
|
+
updated: ${TODAY}
|
|
5329
|
+
type: concept
|
|
5330
|
+
tags: [concept]
|
|
5331
|
+
sources: []
|
|
5332
|
+
confidence: medium
|
|
5333
|
+
provenance: research
|
|
5334
|
+
---
|
|
5335
|
+
|
|
5336
|
+
# Example Concept
|
|
5337
|
+
|
|
5338
|
+
## Overview
|
|
5339
|
+
|
|
5340
|
+
This is a seed concept page. Concept pages capture topics, patterns, and ideas that span multiple sources.
|
|
5341
|
+
|
|
5342
|
+
## Related
|
|
5343
|
+
|
|
5344
|
+
- [[example-project]]
|
|
5345
|
+
|
|
5346
|
+
## Sources
|
|
5347
|
+
|
|
5348
|
+
(Add source citations here after ingesting raw material with \`wiki-ingest\`)
|
|
5349
|
+
`
|
|
5350
|
+
};
|
|
5351
|
+
var EXAMPLE_RAW = `---
|
|
5352
|
+
source_url: https://example.com
|
|
5353
|
+
ingested: ${TODAY}
|
|
5354
|
+
sha256: 0000000000000000000000000000000000000000000000000000000000000000
|
|
5355
|
+
---
|
|
5356
|
+
|
|
5357
|
+
# Example Source Article
|
|
5358
|
+
|
|
5359
|
+
This is a placeholder raw source. Replace it with real content ingested via \`skillwiki hash\` and the wiki-ingest skill.
|
|
5360
|
+
|
|
5361
|
+
Real sources are immutable after ingestion \u2014 never edit them.
|
|
2865
5362
|
`;
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
5363
|
+
async function runSeed(input) {
|
|
5364
|
+
try {
|
|
5365
|
+
await stat7(join33(input.vault, "SCHEMA.md"));
|
|
5366
|
+
} catch {
|
|
5367
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
5368
|
+
}
|
|
5369
|
+
const created = [];
|
|
5370
|
+
const skipped = [];
|
|
5371
|
+
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
5372
|
+
const absPath = join33(input.vault, relPath);
|
|
5373
|
+
try {
|
|
5374
|
+
await stat7(absPath);
|
|
5375
|
+
skipped.push(relPath);
|
|
5376
|
+
} catch {
|
|
5377
|
+
await mkdir12(join33(absPath, ".."), { recursive: true });
|
|
5378
|
+
await writeFile17(absPath, content, "utf8");
|
|
5379
|
+
created.push(relPath);
|
|
2870
5380
|
}
|
|
2871
|
-
body += "\n";
|
|
2872
5381
|
}
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
5382
|
+
const rawPath = join33(input.vault, "raw", "articles", "example-source.md");
|
|
5383
|
+
try {
|
|
5384
|
+
await stat7(rawPath);
|
|
5385
|
+
skipped.push("raw/articles/example-source.md");
|
|
5386
|
+
} catch {
|
|
5387
|
+
await mkdir12(join33(rawPath, ".."), { recursive: true });
|
|
5388
|
+
await writeFile17(rawPath, EXAMPLE_RAW, "utf8");
|
|
5389
|
+
created.push("raw/articles/example-source.md");
|
|
5390
|
+
}
|
|
5391
|
+
if (created.length > 0) {
|
|
5392
|
+
appendLastOp(input.vault, {
|
|
5393
|
+
operation: "seed",
|
|
5394
|
+
summary: `seeded ${created.length} example pages`,
|
|
5395
|
+
files: created,
|
|
5396
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5397
|
+
});
|
|
2876
5398
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
5399
|
+
const hintLines = [`seeded: ${created.length}`, `skipped (already exist): ${skipped.length}`];
|
|
5400
|
+
if (created.length > 0) {
|
|
5401
|
+
hintLines.push("next steps: ingest real sources with wiki-ingest, then cite them in concept/entity pages");
|
|
5402
|
+
}
|
|
5403
|
+
return {
|
|
5404
|
+
exitCode: ExitCode.OK,
|
|
5405
|
+
result: ok({ created, skipped, humanHint: hintLines.join("\n") })
|
|
5406
|
+
};
|
|
5407
|
+
}
|
|
5408
|
+
|
|
5409
|
+
// src/commands/canvas.ts
|
|
5410
|
+
import { readFile as readFile21, writeFile as writeFile18 } from "fs/promises";
|
|
5411
|
+
import { existsSync as existsSync11 } from "fs";
|
|
5412
|
+
import { join as join34 } from "path";
|
|
5413
|
+
var NODE_WIDTH = 240;
|
|
5414
|
+
var NODE_HEIGHT = 60;
|
|
5415
|
+
var COLUMN_SPACING = 400;
|
|
5416
|
+
var ROW_SPACING = 80;
|
|
5417
|
+
var TYPE_COLUMNS = {
|
|
5418
|
+
entities: 0,
|
|
5419
|
+
concepts: 1,
|
|
5420
|
+
comparisons: 2,
|
|
5421
|
+
queries: 3,
|
|
5422
|
+
meta: 3
|
|
5423
|
+
};
|
|
5424
|
+
var TYPE_COLORS = {
|
|
5425
|
+
entities: "1",
|
|
5426
|
+
// red
|
|
5427
|
+
concepts: "4",
|
|
5428
|
+
// green
|
|
5429
|
+
comparisons: "2",
|
|
5430
|
+
// orange
|
|
5431
|
+
queries: "5",
|
|
5432
|
+
// cyan
|
|
5433
|
+
meta: "6"
|
|
5434
|
+
// purple
|
|
5435
|
+
};
|
|
5436
|
+
var DEFAULT_COLOR = "3";
|
|
5437
|
+
var DEFAULT_COLUMN = 2;
|
|
5438
|
+
function inferNodeType(relPath) {
|
|
5439
|
+
const segment = relPath.split("/")[0] ?? "";
|
|
5440
|
+
return TYPE_COLUMNS[segment] !== void 0 ? segment : "";
|
|
5441
|
+
}
|
|
5442
|
+
function getColumnForType(nodeType) {
|
|
5443
|
+
return TYPE_COLUMNS[nodeType] ?? DEFAULT_COLUMN;
|
|
5444
|
+
}
|
|
5445
|
+
function getColorForType(nodeType) {
|
|
5446
|
+
return TYPE_COLORS[nodeType] ?? DEFAULT_COLOR;
|
|
5447
|
+
}
|
|
5448
|
+
function buildCanvasNodes(paths) {
|
|
5449
|
+
const columnY = {};
|
|
5450
|
+
const nodes = [];
|
|
5451
|
+
for (const relPath of paths) {
|
|
5452
|
+
const nodeType = inferNodeType(relPath);
|
|
5453
|
+
const col = getColumnForType(nodeType);
|
|
5454
|
+
const y = columnY[col] ?? 0;
|
|
5455
|
+
columnY[col] = y + ROW_SPACING;
|
|
5456
|
+
nodes.push({
|
|
5457
|
+
id: relPath,
|
|
5458
|
+
type: "file",
|
|
5459
|
+
file: relPath,
|
|
5460
|
+
x: col * COLUMN_SPACING,
|
|
5461
|
+
y,
|
|
5462
|
+
width: NODE_WIDTH,
|
|
5463
|
+
height: NODE_HEIGHT,
|
|
5464
|
+
color: getColorForType(nodeType)
|
|
5465
|
+
});
|
|
5466
|
+
}
|
|
5467
|
+
return nodes;
|
|
5468
|
+
}
|
|
5469
|
+
function buildCanvasEdges(adjacency) {
|
|
5470
|
+
const edges = [];
|
|
5471
|
+
let edgeIndex = 0;
|
|
5472
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5473
|
+
for (const [source, targets] of Object.entries(adjacency)) {
|
|
5474
|
+
for (const target of targets) {
|
|
5475
|
+
const key = `${source}->${target}`;
|
|
5476
|
+
if (seen.has(key)) continue;
|
|
5477
|
+
seen.add(key);
|
|
5478
|
+
edges.push({
|
|
5479
|
+
id: `edge-${edgeIndex++}`,
|
|
5480
|
+
fromNode: source,
|
|
5481
|
+
toNode: target,
|
|
5482
|
+
fromSide: "right",
|
|
5483
|
+
toSide: "left"
|
|
5484
|
+
});
|
|
2886
5485
|
}
|
|
2887
5486
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
5487
|
+
return edges;
|
|
5488
|
+
}
|
|
5489
|
+
async function runCanvasGenerate(input) {
|
|
5490
|
+
const graphPath = input.graphPath ?? join34(input.vault, ".skillwiki", "graph.json");
|
|
5491
|
+
if (!existsSync11(graphPath)) {
|
|
5492
|
+
return {
|
|
5493
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5494
|
+
result: err("FILE_NOT_FOUND", {
|
|
5495
|
+
path: graphPath,
|
|
5496
|
+
hint: "Run `skillwiki graph build` first to generate graph.json"
|
|
5497
|
+
})
|
|
5498
|
+
};
|
|
5499
|
+
}
|
|
5500
|
+
let raw;
|
|
5501
|
+
try {
|
|
5502
|
+
raw = await readFile21(graphPath, "utf8");
|
|
5503
|
+
} catch (e) {
|
|
5504
|
+
return {
|
|
5505
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5506
|
+
result: err("FILE_NOT_FOUND", { path: graphPath, message: String(e) })
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
let graph;
|
|
5510
|
+
try {
|
|
5511
|
+
graph = JSON.parse(raw);
|
|
5512
|
+
} catch {
|
|
5513
|
+
return {
|
|
5514
|
+
exitCode: ExitCode.SCHEMA_NOT_DETECTED,
|
|
5515
|
+
result: err("SCHEMA_NOT_DETECTED", { path: graphPath, reason: "Invalid JSON in graph.json" })
|
|
5516
|
+
};
|
|
5517
|
+
}
|
|
5518
|
+
if (!graph.adjacency || typeof graph.adjacency !== "object") {
|
|
5519
|
+
return {
|
|
5520
|
+
exitCode: ExitCode.SCHEMA_NOT_DETECTED,
|
|
5521
|
+
result: err("SCHEMA_NOT_DETECTED", { path: graphPath, reason: "graph.json missing adjacency field" })
|
|
5522
|
+
};
|
|
5523
|
+
}
|
|
5524
|
+
const paths = Object.keys(graph.adjacency);
|
|
5525
|
+
const nodes = buildCanvasNodes(paths);
|
|
5526
|
+
const edges = buildCanvasEdges(graph.adjacency);
|
|
5527
|
+
const canvas = { nodes, edges };
|
|
5528
|
+
const outPath = join34(input.vault, "vault-graph.canvas");
|
|
5529
|
+
try {
|
|
5530
|
+
await writeFile18(outPath, JSON.stringify(canvas, null, 2));
|
|
5531
|
+
} catch (e) {
|
|
5532
|
+
return {
|
|
5533
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
5534
|
+
result: err("WRITE_FAILED", { message: String(e), path: outPath })
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
2890
5537
|
return {
|
|
2891
5538
|
exitCode: ExitCode.OK,
|
|
2892
5539
|
result: ok({
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
humanHint: `project: ${slug}
|
|
2899
|
-
entries: ${entries.length}${staleHint}
|
|
2900
|
-
${action}
|
|
2901
|
-
|
|
2902
|
-
${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
|
|
5540
|
+
out_path: outPath,
|
|
5541
|
+
node_count: nodes.length,
|
|
5542
|
+
edge_count: edges.length,
|
|
5543
|
+
humanHint: `nodes: ${nodes.length}, edges: ${edges.length}
|
|
5544
|
+
written: ${outPath}`
|
|
2903
5545
|
})
|
|
2904
5546
|
};
|
|
2905
5547
|
}
|
|
2906
5548
|
|
|
5549
|
+
// src/commands/query.ts
|
|
5550
|
+
import { readFile as readFile22, stat as stat8 } from "fs/promises";
|
|
5551
|
+
import { join as join35 } from "path";
|
|
5552
|
+
var W_KEYWORD = 2;
|
|
5553
|
+
var W_SOURCE_OVERLAP = 4;
|
|
5554
|
+
var W_WIKILINK = 3;
|
|
5555
|
+
var W_ADAMIC_ADAR = 1.5;
|
|
5556
|
+
var W_TYPE_AFFINITY = 1;
|
|
5557
|
+
var NON_SEED_FACTOR = 0.4;
|
|
5558
|
+
var CONCEPT_INDICATORS = /* @__PURE__ */ new Set([
|
|
5559
|
+
"what",
|
|
5560
|
+
"how",
|
|
5561
|
+
"why",
|
|
5562
|
+
"concept",
|
|
5563
|
+
"idea",
|
|
5564
|
+
"pattern",
|
|
5565
|
+
"principle",
|
|
5566
|
+
"theory",
|
|
5567
|
+
"approach",
|
|
5568
|
+
"method",
|
|
5569
|
+
"framework",
|
|
5570
|
+
"model",
|
|
5571
|
+
"definition"
|
|
5572
|
+
]);
|
|
5573
|
+
async function runQuery(input) {
|
|
5574
|
+
const scan = await scanVault(input.vault);
|
|
5575
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
5576
|
+
const limit = input.limit ?? 10;
|
|
5577
|
+
const queryTerms = tokenize(input.text);
|
|
5578
|
+
if (queryTerms.length === 0) {
|
|
5579
|
+
return {
|
|
5580
|
+
exitCode: ExitCode.OK,
|
|
5581
|
+
result: ok({ results: [], humanHint: "no query terms" })
|
|
5582
|
+
};
|
|
5583
|
+
}
|
|
5584
|
+
const graph = await loadOrBuildGraph(input.vault);
|
|
5585
|
+
const pages = [];
|
|
5586
|
+
for (const p of scan.data.typedKnowledge) {
|
|
5587
|
+
const text = await readPage(p);
|
|
5588
|
+
const fm = extractFrontmatter(text);
|
|
5589
|
+
if (!fm.ok) continue;
|
|
5590
|
+
const title = String(fm.data.title ?? "");
|
|
5591
|
+
const type = String(fm.data.type ?? "");
|
|
5592
|
+
const tags = Array.isArray(fm.data.tags) ? fm.data.tags.map(String) : [];
|
|
5593
|
+
const sources = Array.isArray(fm.data.sources) ? fm.data.sources.map(String) : [];
|
|
5594
|
+
const split = splitFrontmatter(text);
|
|
5595
|
+
const body = split.ok ? split.data.body : text;
|
|
5596
|
+
const keywordScore = computeKeywordScore(queryTerms, title, tags, body);
|
|
5597
|
+
pages.push({ relPath: p.relPath, title, type, tags, sources, keywordScore });
|
|
5598
|
+
}
|
|
5599
|
+
const seedPaths = new Set(
|
|
5600
|
+
pages.filter((p) => p.keywordScore > 0).map((p) => p.relPath)
|
|
5601
|
+
);
|
|
5602
|
+
const results = pages.map((page) => {
|
|
5603
|
+
const sourceOverlap = scoreSourceOverlap(page, pages, seedPaths);
|
|
5604
|
+
const wikilink2 = scoreWikilink(page.relPath, seedPaths, graph);
|
|
5605
|
+
const aa = scoreAdamicAdar(page.relPath, seedPaths, graph);
|
|
5606
|
+
const typeAffinity = scoreTypeAffinity(page.type, queryTerms);
|
|
5607
|
+
const isSeed = page.keywordScore > 0;
|
|
5608
|
+
const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP + wikilink2 * W_WIKILINK + aa * W_ADAMIC_ADAR;
|
|
5609
|
+
const composite = isSeed ? page.keywordScore * W_KEYWORD + structuralBoost + typeAffinity * W_TYPE_AFFINITY : structuralBoost * NON_SEED_FACTOR + typeAffinity * W_TYPE_AFFINITY;
|
|
5610
|
+
return {
|
|
5611
|
+
path: page.relPath,
|
|
5612
|
+
score: Math.round(composite * 1e3) / 1e3,
|
|
5613
|
+
title: page.title,
|
|
5614
|
+
type: page.type
|
|
5615
|
+
};
|
|
5616
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
|
|
5617
|
+
const humanHint = results.length === 0 ? "no matching pages found" : results.map((r) => `${r.path} (score: ${r.score})`).join("\n");
|
|
5618
|
+
return { exitCode: ExitCode.OK, result: ok({ results, humanHint }) };
|
|
5619
|
+
}
|
|
5620
|
+
function scoreSourceOverlap(page, allPages, seedPaths) {
|
|
5621
|
+
if (page.sources.length === 0) return 0;
|
|
5622
|
+
let total = 0;
|
|
5623
|
+
for (const seed of allPages) {
|
|
5624
|
+
if (seed.relPath === page.relPath || !seedPaths.has(seed.relPath)) continue;
|
|
5625
|
+
const shared = page.sources.filter((s) => seed.sources.includes(s)).length;
|
|
5626
|
+
total += shared;
|
|
5627
|
+
}
|
|
5628
|
+
return total;
|
|
5629
|
+
}
|
|
5630
|
+
function scoreWikilink(candidatePath, seedPaths, graph) {
|
|
5631
|
+
if (!graph) return 0;
|
|
5632
|
+
let count = 0;
|
|
5633
|
+
for (const seedPath of seedPaths) {
|
|
5634
|
+
const neighbors = graph.adjacency[seedPath];
|
|
5635
|
+
if (neighbors && neighbors.includes(candidatePath)) count++;
|
|
5636
|
+
}
|
|
5637
|
+
return count;
|
|
5638
|
+
}
|
|
5639
|
+
function scoreAdamicAdar(candidatePath, seedPaths, graph) {
|
|
5640
|
+
if (!graph) return 0;
|
|
5641
|
+
let maxScore = 0;
|
|
5642
|
+
const aaForCandidate = graph.adamicAdar[candidatePath];
|
|
5643
|
+
if (!aaForCandidate) return 0;
|
|
5644
|
+
for (const seedPath of seedPaths) {
|
|
5645
|
+
const val = aaForCandidate[seedPath];
|
|
5646
|
+
if (val !== void 0 && val > maxScore) maxScore = val;
|
|
5647
|
+
}
|
|
5648
|
+
return maxScore;
|
|
5649
|
+
}
|
|
5650
|
+
function scoreTypeAffinity(pageType, queryTerms) {
|
|
5651
|
+
const hasConceptIntent = queryTerms.some((t) => CONCEPT_INDICATORS.has(t));
|
|
5652
|
+
if (hasConceptIntent && pageType === "concept") return 1;
|
|
5653
|
+
if (!hasConceptIntent && pageType === "entity") return 0.5;
|
|
5654
|
+
return 0;
|
|
5655
|
+
}
|
|
5656
|
+
function tokenize(text) {
|
|
5657
|
+
return text.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
5658
|
+
}
|
|
5659
|
+
function computeKeywordScore(terms, title, tags, body) {
|
|
5660
|
+
const lowerTitle = title.toLowerCase();
|
|
5661
|
+
const lowerTags = tags.map((t) => t.toLowerCase());
|
|
5662
|
+
const lowerBody = body.toLowerCase();
|
|
5663
|
+
let score = 0;
|
|
5664
|
+
for (const term of terms) {
|
|
5665
|
+
if (lowerTitle.includes(term)) score += 3;
|
|
5666
|
+
if (lowerTags.some((t) => t.includes(term))) score += 2;
|
|
5667
|
+
if (lowerBody.includes(term)) score += 1;
|
|
5668
|
+
}
|
|
5669
|
+
return score;
|
|
5670
|
+
}
|
|
5671
|
+
async function loadOrBuildGraph(vault) {
|
|
5672
|
+
const graphPath = join35(vault, ".skillwiki", "graph.json");
|
|
5673
|
+
let needsBuild = false;
|
|
5674
|
+
try {
|
|
5675
|
+
const fileStat = await stat8(graphPath);
|
|
5676
|
+
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
5677
|
+
if (ageHours > 24) needsBuild = true;
|
|
5678
|
+
} catch {
|
|
5679
|
+
needsBuild = true;
|
|
5680
|
+
}
|
|
5681
|
+
if (needsBuild) {
|
|
5682
|
+
const buildResult = await runGraphBuild({ vault, out: graphPath });
|
|
5683
|
+
if (buildResult.exitCode !== 0) return null;
|
|
5684
|
+
}
|
|
5685
|
+
try {
|
|
5686
|
+
const raw = await readFile22(graphPath, "utf8");
|
|
5687
|
+
return JSON.parse(raw);
|
|
5688
|
+
} catch {
|
|
5689
|
+
return null;
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
// src/utils/auto-commit.ts
|
|
5694
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5695
|
+
import { join as join36 } from "path";
|
|
5696
|
+
async function postCommit(vault, exitCode) {
|
|
5697
|
+
if (exitCode !== 0) return;
|
|
5698
|
+
const home = process.env.HOME ?? "";
|
|
5699
|
+
const dotenv = await parseDotenvFile(configPath(home));
|
|
5700
|
+
if (dotenv["AUTO_COMMIT"] === "false") return;
|
|
5701
|
+
if (!existsSync12(join36(vault, ".git"))) return;
|
|
5702
|
+
const lastOps = readLastOp(vault);
|
|
5703
|
+
if (lastOps.length === 0) return;
|
|
5704
|
+
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
5705
|
+
if (!porcelain || porcelain.trim().length === 0) return;
|
|
5706
|
+
const { gitStrict: gitStrict2 } = await import("./git-M4WGJ5G3.js");
|
|
5707
|
+
try {
|
|
5708
|
+
gitStrict2(vault, ["add", "-A"]);
|
|
5709
|
+
try {
|
|
5710
|
+
gitStrict2(vault, ["reset", "HEAD", "--", ".skillwiki/last-op.json"]);
|
|
5711
|
+
} catch {
|
|
5712
|
+
}
|
|
5713
|
+
} catch (e) {
|
|
5714
|
+
process.stderr.write(`auto-commit: git add failed: ${String(e)}
|
|
5715
|
+
`);
|
|
5716
|
+
return;
|
|
5717
|
+
}
|
|
5718
|
+
const commitMessage = lastOps.map((op) => `${op.operation}: ${op.summary} (${op.files.length} files)`).join("; ");
|
|
5719
|
+
try {
|
|
5720
|
+
gitStrict2(vault, ["commit", "-m", commitMessage]);
|
|
5721
|
+
} catch (e) {
|
|
5722
|
+
process.stderr.write(`auto-commit: git commit failed: ${String(e)}
|
|
5723
|
+
`);
|
|
5724
|
+
return;
|
|
5725
|
+
}
|
|
5726
|
+
clearLastOp(vault);
|
|
5727
|
+
}
|
|
5728
|
+
|
|
2907
5729
|
// src/cli.ts
|
|
2908
|
-
var pkg = JSON.parse(
|
|
5730
|
+
var pkg = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8"));
|
|
2909
5731
|
var program = new Command();
|
|
2910
5732
|
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
2911
5733
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
2912
|
-
function emit(r) {
|
|
5734
|
+
async function emit(r, vault) {
|
|
2913
5735
|
if (program.opts().human) printHuman(r.result);
|
|
2914
5736
|
else printJson(r.result);
|
|
5737
|
+
if (vault) await postCommit(vault, r.exitCode);
|
|
2915
5738
|
process.exit(r.exitCode);
|
|
2916
5739
|
}
|
|
2917
|
-
program.command("hash <file>").action(async (file) => emit(await runHash({ file })));
|
|
2918
|
-
program.command("fetch-guard <url>").action(async (url) => emit(await runFetchGuard({ url })));
|
|
2919
|
-
program.command("validate <file>").
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
5740
|
+
program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
|
|
5741
|
+
program.command("fetch-guard <url>").description("check if a URL passes fetch guard rules and sanitize secrets").action(async (url) => emit(await runFetchGuard({ url })));
|
|
5742
|
+
program.command("validate <file>").description("validate vault page frontmatter against its detected schema").option("--apply", "auto-update vault index.md and log.md after successful validation", false).option("--vault <dir>", "vault root directory (required with --apply)").option("--wiki <name>", "wiki profile name").action(async (file, opts) => {
|
|
5743
|
+
let vault;
|
|
5744
|
+
if (opts.apply) {
|
|
5745
|
+
const v = await resolveVaultArg(opts.vault, opts.wiki);
|
|
5746
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5747
|
+
else vault = v.vault;
|
|
5748
|
+
}
|
|
5749
|
+
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
5750
|
+
});
|
|
5751
|
+
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) => {
|
|
5752
|
+
const out = opts.out ?? join37(vault, ".skillwiki", "graph.json");
|
|
5753
|
+
emit(await runGraphBuild({ vault, out }), vault);
|
|
5754
|
+
});
|
|
5755
|
+
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
5756
|
+
canvasCmd.command("generate [vault]").description("generate .canvas from graph.json").option("--graph-path <path>", "explicit path to graph.json").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5757
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5758
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5759
|
+
else emit(await runCanvasGenerate({ vault: v.vault, graphPath: opts.graphPath }), v.vault);
|
|
5760
|
+
});
|
|
5761
|
+
program.command("overlap [vault]").description("detect typed-knowledge pages that share the same raw sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5762
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5763
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5764
|
+
else emit(await runOverlap({ vault: v.vault }), v.vault);
|
|
5765
|
+
});
|
|
5766
|
+
program.command("query <text> [vault]").description("score and rank vault pages by relevance to a query").option("--limit <n>", "max results to return", (s) => parseInt(s, 10), 10).option("--wiki <name>", "wiki profile name").action(async (text, vault, opts) => {
|
|
5767
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5768
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5769
|
+
else emit(await runQuery({ text, vault: v.vault, limit: opts.limit }), v.vault);
|
|
5770
|
+
});
|
|
5771
|
+
program.command("orphans [vault]").description("find pages not referenced by any other page").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runOrphans({
|
|
2923
5772
|
vault,
|
|
2924
5773
|
envValue: process.env.WIKI_PATH,
|
|
2925
5774
|
home: process.env.HOME ?? "",
|
|
2926
5775
|
wiki: opts.wiki
|
|
2927
5776
|
})));
|
|
2928
|
-
program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
|
|
2929
|
-
program.command("install").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").action(async (opts) => {
|
|
5777
|
+
program.command("audit <file>").description("audit citation markers and source provenance for a vault page").action(async (file) => emit(await runAudit({ file })));
|
|
5778
|
+
program.command("install").description("install skillwiki SKILL.md files into ~/.claude/skills/").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").option("--symlink", "create symlinks instead of copies (dev mode \u2014 edits to source are immediately visible)", false).action(async (opts) => {
|
|
2930
5779
|
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
2931
|
-
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
|
|
5780
|
+
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun, symlink: !!opts.symlink }));
|
|
2932
5781
|
});
|
|
2933
|
-
program.command("path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--wiki <name>", "wiki profile name").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
5782
|
+
program.command("path").description("show the resolved vault path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--wiki <name>", "wiki profile name").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
2934
5783
|
const initTime = !!opts.initTime;
|
|
2935
5784
|
const flag = initTime ? opts.target : opts.vault;
|
|
2936
5785
|
emit(await runPath({
|
|
@@ -2942,7 +5791,7 @@ program.command("path").option("--vault <dir>", "explicit vault override (runtim
|
|
|
2942
5791
|
explain: !!opts.explain
|
|
2943
5792
|
}));
|
|
2944
5793
|
});
|
|
2945
|
-
program.command("lang").option("--lang <code>", "explicit language override").option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
5794
|
+
program.command("lang").description("get or set the vault language").option("--lang <code>", "explicit language override").option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
2946
5795
|
emit(await runLang({
|
|
2947
5796
|
flag: opts.lang,
|
|
2948
5797
|
envValue: process.env.WIKI_LANG,
|
|
@@ -2950,7 +5799,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
2950
5799
|
explain: !!opts.explain
|
|
2951
5800
|
}));
|
|
2952
5801
|
});
|
|
2953
|
-
program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").option("--profile <name>", "write as named wiki profile instead of WIKI_PATH").action(async (opts) => {
|
|
5802
|
+
program.command("init").description("bootstrap a new vault with SCHEMA.md, index.md, log.md").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").option("--profile <name>", "write as named wiki profile instead of WIKI_PATH").action(async (opts) => {
|
|
2954
5803
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
2955
5804
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
2956
5805
|
emit(await runInit({
|
|
@@ -2981,37 +5830,47 @@ async function resolveVaultArg(arg, wiki) {
|
|
|
2981
5830
|
}
|
|
2982
5831
|
return { ok: true, vault: r.data.path };
|
|
2983
5832
|
}
|
|
2984
|
-
program.command("links [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5833
|
+
program.command("links [vault]").description("check wikilink integrity across the vault").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5834
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5835
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5836
|
+
else emit(await runLinks({ vault: v.vault }), v.vault);
|
|
5837
|
+
});
|
|
5838
|
+
program.command("tag-audit [vault]").description("audit tag taxonomy consistency").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5839
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5840
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5841
|
+
else emit(await runTagAudit({ vault: v.vault }), v.vault);
|
|
5842
|
+
});
|
|
5843
|
+
program.command("index-check [vault]").description("verify index.md entries match actual vault pages").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2985
5844
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2986
5845
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2987
|
-
else emit(await
|
|
5846
|
+
else emit(await runIndexCheck({ vault: v.vault }), v.vault);
|
|
2988
5847
|
});
|
|
2989
|
-
program.command("
|
|
5848
|
+
program.command("index-link-format [vault]").description("check index.md for markdown links that should be wikilinks").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2990
5849
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2991
5850
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2992
|
-
else emit(await
|
|
5851
|
+
else emit(await runIndexLinkFormat({ vault: v.vault }), v.vault);
|
|
2993
5852
|
});
|
|
2994
|
-
program.command("
|
|
5853
|
+
program.command("topic-map-check [vault]").description("check whether a topic map page is recommended based on page count").option("--threshold <n>", "page count threshold", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2995
5854
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2996
5855
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2997
|
-
else emit(await
|
|
5856
|
+
else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
|
|
2998
5857
|
});
|
|
2999
|
-
program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10),
|
|
5858
|
+
program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3000
5859
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3001
5860
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3002
|
-
else emit(await runStale({ vault: v.vault, days: opts.days }));
|
|
5861
|
+
else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive }), v.vault);
|
|
3003
5862
|
});
|
|
3004
|
-
program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5863
|
+
program.command("pagesize [vault]").description("report page sizes and flag oversized pages").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3005
5864
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3006
5865
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3007
|
-
else emit(await runPagesize({ vault: v.vault, lines: opts.lines }));
|
|
5866
|
+
else emit(await runPagesize({ vault: v.vault, lines: opts.lines }), v.vault);
|
|
3008
5867
|
});
|
|
3009
|
-
program.command("log-rotate [vault]").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5868
|
+
program.command("log-rotate [vault]").description("rotate or trim the vault log file").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3010
5869
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3011
5870
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3012
|
-
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
|
|
5871
|
+
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
|
|
3013
5872
|
});
|
|
3014
|
-
program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix legacy_citation_style violations").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5873
|
+
program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix legacy_citation_style violations").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3015
5874
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3016
5875
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3017
5876
|
else emit(await runLint({
|
|
@@ -3021,7 +5880,7 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
3021
5880
|
lines: opts.lines,
|
|
3022
5881
|
logThreshold: opts.logThreshold,
|
|
3023
5882
|
fix: opts.fix ?? false
|
|
3024
|
-
}));
|
|
5883
|
+
}), v.vault);
|
|
3025
5884
|
});
|
|
3026
5885
|
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
3027
5886
|
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
@@ -3035,47 +5894,159 @@ program.command("doctor").description("diagnose skillwiki setup issues").action(
|
|
|
3035
5894
|
currentVersion: pkg.version,
|
|
3036
5895
|
cwd: process.cwd()
|
|
3037
5896
|
})));
|
|
5897
|
+
program.command("status [vault]").description("output vault diagnostics").option("--human", "render terminal-readable output instead of JSON").option("--json", "output as JSON (default)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5898
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5899
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5900
|
+
else emit(await runStatus({
|
|
5901
|
+
vault: v.vault,
|
|
5902
|
+
home: process.env.HOME ?? "",
|
|
5903
|
+
langEnvValue: process.env.WIKI_LANG
|
|
5904
|
+
}), v.vault);
|
|
5905
|
+
});
|
|
3038
5906
|
program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
|
|
3039
5907
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3040
5908
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3041
|
-
else emit(await runArchive({ vault: v.vault, page }));
|
|
5909
|
+
else emit(await runArchive({ vault: v.vault, page }), v.vault);
|
|
3042
5910
|
});
|
|
3043
5911
|
program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3044
5912
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3045
5913
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3046
|
-
else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }));
|
|
5914
|
+
else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }), v.vault);
|
|
3047
5915
|
});
|
|
3048
5916
|
program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3049
5917
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3050
5918
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3051
|
-
else emit(await runDedup({ vault: v.vault, apply: opts.apply }));
|
|
5919
|
+
else emit(await runDedup({ vault: v.vault, apply: opts.apply }), v.vault);
|
|
3052
5920
|
});
|
|
3053
5921
|
program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3054
5922
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3055
5923
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3056
|
-
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
5924
|
+
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
|
|
3057
5925
|
});
|
|
3058
5926
|
program.command("frontmatter-fix [vault]").description("fix common frontmatter issues on typed-knowledge pages").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3059
5927
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3060
5928
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3061
|
-
else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
5929
|
+
else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
|
|
3062
5930
|
});
|
|
3063
5931
|
program.command("update").description("update skillwiki CLI to the latest version").option("--tag <tag>", "npm dist-tag", "beta").action(async (opts) => emit(await runUpdate({
|
|
3064
5932
|
home: process.env.HOME ?? "",
|
|
3065
5933
|
distTag: opts.tag
|
|
3066
5934
|
})));
|
|
5935
|
+
program.command("self-update").description("update skillwiki CLI from local source or npm@beta").option("--check", "check for updates without installing", false).action(async (opts) => emit(await runSelfUpdate({
|
|
5936
|
+
home: process.env.HOME ?? "",
|
|
5937
|
+
check: !!opts.check
|
|
5938
|
+
})));
|
|
3067
5939
|
program.command("transcripts [vault]").description("list transcript files in raw/transcripts/").option("--since <date>", "only files ingested on or after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
3068
5940
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3069
5941
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3070
|
-
else emit(await runTranscripts({ vault: v.vault, since: opts.since }));
|
|
5942
|
+
else emit(await runTranscripts({ vault: v.vault, since: opts.since }), v.vault);
|
|
3071
5943
|
});
|
|
3072
5944
|
program.command("project-index <slug> [vault]").description("generate a knowledge index for a project workspace").option("--apply", "write knowledge.md to the project directory", false).option("--wiki <name>", "wiki profile name").action(async (slug, vault, opts) => {
|
|
3073
5945
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
3074
5946
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
3075
|
-
else emit(await runProjectIndex({ vault: v.vault, slug, apply: !!opts.apply }));
|
|
5947
|
+
else emit(await runProjectIndex({ vault: v.vault, slug, apply: !!opts.apply }), v.vault);
|
|
5948
|
+
});
|
|
5949
|
+
var compoundCmd = program.command("compound").description("manage project compound entries");
|
|
5950
|
+
compoundCmd.command("promote [vault]").description("promote retros with Generalize?: yes to compound entries").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--dry-run", "preview promotions without writing files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5951
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5952
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5953
|
+
else emit(await runCompound({ vault: v.vault, project: opts.project, dryRun: !!opts.dryRun }), v.vault);
|
|
5954
|
+
});
|
|
5955
|
+
compoundCmd.command("list [vault]").description("list compound entries for a project").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5956
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5957
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5958
|
+
else emit(await runCompoundList({ vault: v.vault, project: opts.project }));
|
|
3076
5959
|
});
|
|
5960
|
+
compoundCmd.command("delete <entry> [vault]").description("delete a compound entry and regenerate knowledge index").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--wiki <name>", "wiki profile name").action(async (entry, vault, opts) => {
|
|
5961
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5962
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5963
|
+
else emit(await runCompoundDelete({ vault: v.vault, project: opts.project, entry }), v.vault);
|
|
5964
|
+
});
|
|
5965
|
+
program.command("tag-sync [vault]").description("mirror frontmatter enum values to nested Obsidian tags").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5966
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5967
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5968
|
+
else emit(await runTagSync({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
|
|
5969
|
+
});
|
|
5970
|
+
var syncCmd = program.command("sync").description("manage vault sync");
|
|
5971
|
+
syncCmd.command("status [vault]").description("check vault git sync status").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5972
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5973
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5974
|
+
else emit(runSyncStatus({ vault: v.vault }));
|
|
5975
|
+
});
|
|
5976
|
+
syncCmd.command("push [vault]").description("lint, commit, and push vault changes to remote").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5977
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5978
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5979
|
+
else emit(await runSyncPush({ vault: v.vault }));
|
|
5980
|
+
});
|
|
5981
|
+
syncCmd.command("pull [vault]").description("pull remote vault changes and lint").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5982
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5983
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5984
|
+
else emit(await runSyncPull({ vault: v.vault }), v.vault);
|
|
5985
|
+
});
|
|
5986
|
+
var backupCmd = program.command("backup").description("manage S3-compatible remote backup");
|
|
5987
|
+
backupCmd.command("sync [vault]").description("sync vault to S3-compatible remote backup").option("--dry-run", "list actions without executing").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--prune", "delete orphaned S3 objects not in vault", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5988
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5989
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5990
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
5991
|
+
const dotenv = await parseDotenvFile(configPath(home));
|
|
5992
|
+
emit(await runBackupSync({
|
|
5993
|
+
vault: v.vault,
|
|
5994
|
+
bucket: opts.bucket ?? dotenv["BACKUP_BUCKET"] ?? "",
|
|
5995
|
+
endpoint: opts.endpoint ?? dotenv["BACKUP_ENDPOINT"] ?? "",
|
|
5996
|
+
region: opts.region ?? dotenv["BACKUP_REGION"] ?? "us-east-1",
|
|
5997
|
+
accessKeyId: dotenv["BACKUP_ACCESS_KEY_ID"] ?? "",
|
|
5998
|
+
secretAccessKey: dotenv["BACKUP_SECRET_ACCESS_KEY"] ?? "",
|
|
5999
|
+
dryRun: opts.dryRun,
|
|
6000
|
+
prune: opts.prune
|
|
6001
|
+
}), v.vault);
|
|
6002
|
+
});
|
|
6003
|
+
backupCmd.command("restore [vault]").description("restore vault from S3-compatible remote backup").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--target <dir>", "restore target directory (defaults to vault)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6004
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6005
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6006
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
6007
|
+
const dotenv = await parseDotenvFile(configPath(home));
|
|
6008
|
+
emit(await runBackupRestore({
|
|
6009
|
+
vault: v.vault,
|
|
6010
|
+
bucket: opts.bucket ?? dotenv["BACKUP_BUCKET"] ?? "",
|
|
6011
|
+
endpoint: opts.endpoint ?? dotenv["BACKUP_ENDPOINT"] ?? "",
|
|
6012
|
+
region: opts.region ?? dotenv["BACKUP_REGION"] ?? "us-east-1",
|
|
6013
|
+
accessKeyId: dotenv["BACKUP_ACCESS_KEY_ID"] ?? "",
|
|
6014
|
+
secretAccessKey: dotenv["BACKUP_SECRET_ACCESS_KEY"] ?? "",
|
|
6015
|
+
target: opts.target
|
|
6016
|
+
}), v.vault);
|
|
6017
|
+
});
|
|
6018
|
+
program.command("seed [vault]").description("populate a vault with example content").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6019
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6020
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6021
|
+
else emit(await runSeed({ vault: v.vault }), v.vault);
|
|
6022
|
+
});
|
|
6023
|
+
program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "note").option("--project <slug>", "associated project slug").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6024
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6025
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6026
|
+
else emit(await runObserve({
|
|
6027
|
+
vault: v.vault,
|
|
6028
|
+
text: opts.text,
|
|
6029
|
+
kind: opts.kind,
|
|
6030
|
+
project: opts.project
|
|
6031
|
+
}), v.vault);
|
|
6032
|
+
});
|
|
6033
|
+
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) => {
|
|
6034
|
+
const tags = typeof opts.tags === "string" ? opts.tags.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : [];
|
|
6035
|
+
emit(await runIngest({
|
|
6036
|
+
source,
|
|
6037
|
+
vault: opts.vault,
|
|
6038
|
+
type: opts.type,
|
|
6039
|
+
title: opts.title,
|
|
6040
|
+
tags,
|
|
6041
|
+
provenance: opts.provenance,
|
|
6042
|
+
dryRun: !!opts.dryRun
|
|
6043
|
+
}), opts.vault);
|
|
6044
|
+
});
|
|
6045
|
+
for (const w of getDeprecatedWarnings(process.env.HOME ?? "")) {
|
|
6046
|
+
process.stderr.write(w + "\n");
|
|
6047
|
+
}
|
|
3077
6048
|
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
3078
6049
|
program.parseAsync(process.argv).catch((e) => {
|
|
3079
6050
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
3080
|
-
process.exit(
|
|
6051
|
+
process.exit(ExitCode.INTERNAL_ERROR);
|
|
3081
6052
|
});
|