skillwiki 0.10.22 → 0.10.23
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-6ZDKTNLA.js +294 -0
- package/dist/chunk-EQPU2BPM.js +468 -0
- package/dist/{chunk-54JVBNWK.js → chunk-QBZYEEBD.js} +12 -295
- package/dist/chunk-QNTBNNEL.js +616 -0
- package/dist/{chunk-7YDWC6D3.js → chunk-SYKSL3JQ.js} +852 -699
- package/dist/cli.js +613 -146
- package/dist/{index-projection-Q6WL76SO.js → index-projection-FRWTZB5Y.js} +2 -1
- package/dist/skillwiki-mcp.js +4 -2
- package/dist/sources-L2SQV63E.js +10 -0
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/README.md +13 -2
- package/skills/agents/wiki-add-task.md +2 -1
- package/skills/agents/wiki-archive.md +8 -9
- package/skills/agents/wiki-audit.md +2 -1
- package/skills/agents/wiki-lint.md +2 -1
- package/skills/agents/wiki-query.md +3 -1
- package/skills/agents/wiki-reingest.md +9 -8
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +19 -13
- package/skills/skills/wiki-add-task/SKILL.md +4 -3
- package/skills/skills/wiki-archive/SKILL.md +23 -18
- package/skills/skills/wiki-audit/SKILL.md +3 -1
- package/skills/skills/wiki-init/SKILL.md +12 -1
- package/skills/skills/wiki-lint/SKILL.md +2 -1
- package/skills/skills/wiki-query/SKILL.md +7 -0
- package/skills/skills/wiki-reingest/SKILL.md +12 -8
- package/skills/skills/wiki-remove/SKILL.md +22 -5
- package/skills/skills/wiki-sync/SKILL.md +2 -3
- package/skills/using-skillwiki/SKILL.md +19 -13
- package/skills/wiki-add-task/SKILL.md +4 -3
- package/skills/wiki-archive/SKILL.md +23 -18
- package/skills/wiki-audit/SKILL.md +3 -1
- package/skills/wiki-init/SKILL.md +12 -1
- package/skills/wiki-lint/SKILL.md +2 -1
- package/skills/wiki-query/SKILL.md +7 -0
- package/skills/wiki-reingest/SKILL.md +12 -8
- package/skills/wiki-remove/SKILL.md +22 -5
- package/skills/wiki-sync/SKILL.md +2 -3
- package/templates/SCHEMA.md +8 -5
- package/templates/web-clipper/llm-wiki-clippings.json +56 -0
- package/templates/web-clipper/readme.txt +16 -0
|
@@ -6,20 +6,10 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
atomicWriteText,
|
|
8
8
|
buildRootIndexUniverse,
|
|
9
|
-
extractFrontmatter,
|
|
10
|
-
mapWithConcurrency,
|
|
11
9
|
prepareTypedPage,
|
|
12
|
-
readPage,
|
|
13
|
-
readPageCached,
|
|
14
|
-
redactSensitiveContent,
|
|
15
10
|
renderRootIndex,
|
|
16
|
-
resolveReadOnlyVaultRoot,
|
|
17
|
-
scanSensitiveContent,
|
|
18
|
-
scanVault,
|
|
19
|
-
splitFrontmatter,
|
|
20
|
-
vaultIoConcurrency,
|
|
21
11
|
writeRootIndexProjection
|
|
22
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-QBZYEEBD.js";
|
|
23
13
|
import {
|
|
24
14
|
CONFIG_KEYS,
|
|
25
15
|
git,
|
|
@@ -35,6 +25,39 @@ import {
|
|
|
35
25
|
snapshotterAliasForLocalHost,
|
|
36
26
|
writeDotenv
|
|
37
27
|
} from "./chunk-PQG26AGJ.js";
|
|
28
|
+
import {
|
|
29
|
+
authorizeRawOperation,
|
|
30
|
+
buildSourceReferenceIndex,
|
|
31
|
+
buildSourceRelocationProjection,
|
|
32
|
+
classifyRawPath,
|
|
33
|
+
eventPathFor,
|
|
34
|
+
extractCitationMarkers,
|
|
35
|
+
hasOrphanedCitations,
|
|
36
|
+
hasWikilinkCitations,
|
|
37
|
+
isLegacyCitationStyle,
|
|
38
|
+
lifecycleDestination,
|
|
39
|
+
normalizeRawSourceTarget,
|
|
40
|
+
operationId,
|
|
41
|
+
rawSourceTargetExists,
|
|
42
|
+
rawSourceTargetExistsSync,
|
|
43
|
+
readSourceRelocations,
|
|
44
|
+
resolveAbsentTargetInsideVault,
|
|
45
|
+
resolveExistingRegularFileInsideVault,
|
|
46
|
+
stripFencedBlocks,
|
|
47
|
+
writeLogEvent
|
|
48
|
+
} from "./chunk-QNTBNNEL.js";
|
|
49
|
+
import {
|
|
50
|
+
extractFrontmatter,
|
|
51
|
+
mapWithConcurrency,
|
|
52
|
+
readPage,
|
|
53
|
+
readPageCached,
|
|
54
|
+
redactSensitiveContent,
|
|
55
|
+
resolveReadOnlyVaultRoot,
|
|
56
|
+
scanSensitiveContent,
|
|
57
|
+
scanVault,
|
|
58
|
+
splitFrontmatter,
|
|
59
|
+
vaultIoConcurrency
|
|
60
|
+
} from "./chunk-6ZDKTNLA.js";
|
|
38
61
|
import {
|
|
39
62
|
CompoundSchema,
|
|
40
63
|
ExitCode,
|
|
@@ -52,9 +75,9 @@ import {
|
|
|
52
75
|
} from "./chunk-C2DKFJFA.js";
|
|
53
76
|
|
|
54
77
|
// src/commands/log-append.ts
|
|
55
|
-
import { readFile
|
|
78
|
+
import { readFile, stat } from "fs/promises";
|
|
56
79
|
import { hostname } from "os";
|
|
57
|
-
import { join as
|
|
80
|
+
import { join as join3 } from "path";
|
|
58
81
|
|
|
59
82
|
// src/utils/last-op.ts
|
|
60
83
|
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
@@ -165,143 +188,6 @@ function releaseLogLock(handle) {
|
|
|
165
188
|
}
|
|
166
189
|
}
|
|
167
190
|
|
|
168
|
-
// src/utils/log-events.ts
|
|
169
|
-
import { mkdir, open, readFile, readdir } from "fs/promises";
|
|
170
|
-
import { join as join3 } from "path";
|
|
171
|
-
function isPlainObject(value) {
|
|
172
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
|
-
}
|
|
174
|
-
function canonicalize(value) {
|
|
175
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
176
|
-
if (!isPlainObject(value)) return value;
|
|
177
|
-
const out = {};
|
|
178
|
-
for (const key of Object.keys(value).sort()) {
|
|
179
|
-
out[key] = canonicalize(value[key]);
|
|
180
|
-
}
|
|
181
|
-
return out;
|
|
182
|
-
}
|
|
183
|
-
function eventPathFor(event) {
|
|
184
|
-
const day = event.occurred_at.slice(0, 10);
|
|
185
|
-
return `meta/log-events/${day}/${event.operation_id}.json`;
|
|
186
|
-
}
|
|
187
|
-
function validateLogEvent(event) {
|
|
188
|
-
if (event.schema !== "skillwiki-log-event/v1") {
|
|
189
|
-
return err("SCHEME_REJECTED", { message: "invalid event schema" });
|
|
190
|
-
}
|
|
191
|
-
if (!/^[0-9a-f]{64}$/.test(event.operation_id)) {
|
|
192
|
-
return err("SCHEME_REJECTED", { message: "operation_id must be 64 hex chars" });
|
|
193
|
-
}
|
|
194
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.occurred_at)) {
|
|
195
|
-
return err("SCHEME_REJECTED", { message: "occurred_at must be UTC ISO with milliseconds" });
|
|
196
|
-
}
|
|
197
|
-
for (const field of ["host_id", "actor", "kind", "target", "note"]) {
|
|
198
|
-
const v = event[field];
|
|
199
|
-
if (typeof v !== "string" || v.trim().length === 0 || v.length > 500) {
|
|
200
|
-
return err("SCHEME_REJECTED", { message: `invalid ${field}` });
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (!isPlainObject(event.metadata)) {
|
|
204
|
-
return err("SCHEME_REJECTED", { message: "metadata must be a plain object" });
|
|
205
|
-
}
|
|
206
|
-
const sensitive = scanSensitiveContent(JSON.stringify(event));
|
|
207
|
-
if (sensitive.length > 0) {
|
|
208
|
-
return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
|
|
209
|
-
}
|
|
210
|
-
return ok({
|
|
211
|
-
schema: "skillwiki-log-event/v1",
|
|
212
|
-
operation_id: event.operation_id,
|
|
213
|
-
occurred_at: event.occurred_at,
|
|
214
|
-
host_id: event.host_id,
|
|
215
|
-
actor: event.actor,
|
|
216
|
-
kind: event.kind,
|
|
217
|
-
target: event.target,
|
|
218
|
-
note: event.note,
|
|
219
|
-
metadata: canonicalize(event.metadata)
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
function canonicalEventJson(event) {
|
|
223
|
-
const ordered = {
|
|
224
|
-
schema: event.schema,
|
|
225
|
-
operation_id: event.operation_id,
|
|
226
|
-
occurred_at: event.occurred_at,
|
|
227
|
-
host_id: event.host_id,
|
|
228
|
-
actor: event.actor,
|
|
229
|
-
kind: event.kind,
|
|
230
|
-
target: event.target,
|
|
231
|
-
note: event.note,
|
|
232
|
-
metadata: event.metadata
|
|
233
|
-
};
|
|
234
|
-
return `${JSON.stringify(ordered, null, 2)}
|
|
235
|
-
`;
|
|
236
|
-
}
|
|
237
|
-
async function writeLogEvent(vault, event) {
|
|
238
|
-
const validated = validateLogEvent(event);
|
|
239
|
-
if (!validated.ok) return validated;
|
|
240
|
-
const rel = eventPathFor(validated.data);
|
|
241
|
-
const abs = join3(vault, rel);
|
|
242
|
-
await mkdir(join3(vault, "meta", "log-events", validated.data.occurred_at.slice(0, 10)), {
|
|
243
|
-
recursive: true
|
|
244
|
-
});
|
|
245
|
-
const body = canonicalEventJson(validated.data);
|
|
246
|
-
try {
|
|
247
|
-
const handle = await open(abs, "wx");
|
|
248
|
-
try {
|
|
249
|
-
await handle.writeFile(body, "utf8");
|
|
250
|
-
} finally {
|
|
251
|
-
await handle.close();
|
|
252
|
-
}
|
|
253
|
-
return ok({ path: rel, created: true });
|
|
254
|
-
} catch (error) {
|
|
255
|
-
if (error.code === "EEXIST") {
|
|
256
|
-
let existing;
|
|
257
|
-
try {
|
|
258
|
-
existing = await readFile(abs, "utf8");
|
|
259
|
-
} catch (readErr) {
|
|
260
|
-
return err("WRITE_FAILED", { path: rel, message: String(readErr) });
|
|
261
|
-
}
|
|
262
|
-
if (existing === body) return ok({ path: rel, created: false });
|
|
263
|
-
return err("EVENT_IDENTITY_COLLISION", { path: rel });
|
|
264
|
-
}
|
|
265
|
-
return err("WRITE_FAILED", { path: rel, message: String(error) });
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
async function readLogEvents(vault) {
|
|
269
|
-
const root = join3(vault, "meta", "log-events");
|
|
270
|
-
let days;
|
|
271
|
-
try {
|
|
272
|
-
days = (await readdir(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
273
|
-
} catch {
|
|
274
|
-
return ok([]);
|
|
275
|
-
}
|
|
276
|
-
const events = [];
|
|
277
|
-
for (const day of days) {
|
|
278
|
-
const files = (await readdir(join3(root, day))).filter((f) => f.endsWith(".json")).sort();
|
|
279
|
-
for (const file of files) {
|
|
280
|
-
const text = await readFile(join3(root, day, file), "utf8");
|
|
281
|
-
let parsed;
|
|
282
|
-
try {
|
|
283
|
-
parsed = JSON.parse(text);
|
|
284
|
-
} catch {
|
|
285
|
-
return err("SCHEME_REJECTED", { path: `meta/log-events/${day}/${file}`, message: "invalid JSON" });
|
|
286
|
-
}
|
|
287
|
-
const validated = validateLogEvent(parsed);
|
|
288
|
-
if (!validated.ok) return validated;
|
|
289
|
-
if (eventPathFor(validated.data) !== `meta/log-events/${day}/${file}`) {
|
|
290
|
-
return err("SCHEME_REJECTED", {
|
|
291
|
-
path: `meta/log-events/${day}/${file}`,
|
|
292
|
-
message: "path/identity mismatch"
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
events.push(validated.data);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
events.sort((a, b) => {
|
|
299
|
-
if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
|
|
300
|
-
return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
|
|
301
|
-
});
|
|
302
|
-
return ok(events);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
191
|
// src/commands/log-append.ts
|
|
306
192
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
307
193
|
function countLogEntries(logText) {
|
|
@@ -309,29 +195,29 @@ function countLogEntries(logText) {
|
|
|
309
195
|
for (const _ of logText.matchAll(ENTRY_RE)) count += 1;
|
|
310
196
|
return count;
|
|
311
197
|
}
|
|
312
|
-
function operationMarkers(
|
|
313
|
-
if (!/^[0-9a-f]{64}$/.test(
|
|
198
|
+
function operationMarkers(operationId2) {
|
|
199
|
+
if (!/^[0-9a-f]{64}$/.test(operationId2)) {
|
|
314
200
|
return err("USAGE", { message: "operationId must be a SHA-256 hex string" });
|
|
315
201
|
}
|
|
316
202
|
return ok([
|
|
317
|
-
`<!-- skillwiki-log-op:${
|
|
318
|
-
`<!-- skillwiki-page-publish:${
|
|
319
|
-
`<!-- skillwiki-project-page-publish:${
|
|
203
|
+
`<!-- skillwiki-log-op:${operationId2} -->`,
|
|
204
|
+
`<!-- skillwiki-page-publish:${operationId2} -->`,
|
|
205
|
+
`<!-- skillwiki-project-page-publish:${operationId2} -->`
|
|
320
206
|
]);
|
|
321
207
|
}
|
|
322
|
-
function preferredMarker(
|
|
208
|
+
function preferredMarker(operationId2, eventKind) {
|
|
323
209
|
if (!eventKind || eventKind === "page-publish") {
|
|
324
|
-
return `<!-- skillwiki-page-publish:${
|
|
210
|
+
return `<!-- skillwiki-page-publish:${operationId2} -->`;
|
|
325
211
|
}
|
|
326
212
|
if (eventKind === "project-page-publish") {
|
|
327
|
-
return `<!-- skillwiki-project-page-publish:${
|
|
213
|
+
return `<!-- skillwiki-project-page-publish:${operationId2} -->`;
|
|
328
214
|
}
|
|
329
|
-
return `<!-- skillwiki-log-op:${
|
|
215
|
+
return `<!-- skillwiki-log-op:${operationId2} -->`;
|
|
330
216
|
}
|
|
331
217
|
async function appendWhileLocked(logPath, content, markers, writeMarker) {
|
|
332
218
|
let logText;
|
|
333
219
|
try {
|
|
334
|
-
logText = await
|
|
220
|
+
logText = await readFile(logPath, "utf8");
|
|
335
221
|
} catch {
|
|
336
222
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
337
223
|
}
|
|
@@ -370,7 +256,7 @@ ${appendedContent}
|
|
|
370
256
|
}
|
|
371
257
|
async function runLogAppend(input) {
|
|
372
258
|
try {
|
|
373
|
-
await stat(
|
|
259
|
+
await stat(join3(input.vault, "SCHEMA.md"));
|
|
374
260
|
} catch {
|
|
375
261
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
376
262
|
}
|
|
@@ -443,7 +329,7 @@ async function runLogAppend(input) {
|
|
|
443
329
|
return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
|
|
444
330
|
}
|
|
445
331
|
const lockHandle = acquired.data;
|
|
446
|
-
const logPath =
|
|
332
|
+
const logPath = join3(input.vault, "log.md");
|
|
447
333
|
let outcome;
|
|
448
334
|
let released;
|
|
449
335
|
try {
|
|
@@ -494,7 +380,7 @@ async function runLogAppend(input) {
|
|
|
494
380
|
|
|
495
381
|
// src/utils/sync-lock.ts
|
|
496
382
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
497
|
-
import { join as
|
|
383
|
+
import { join as join4 } from "path";
|
|
498
384
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
499
385
|
function getEnvSessionId() {
|
|
500
386
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
@@ -517,7 +403,7 @@ function getCliSessionId(cwd) {
|
|
|
517
403
|
return `cli-${getCwdHash(cwd)}`;
|
|
518
404
|
}
|
|
519
405
|
function lockPath(vault) {
|
|
520
|
-
return
|
|
406
|
+
return join4(vault, ".skillwiki", "sync.lock");
|
|
521
407
|
}
|
|
522
408
|
function readLock(vault) {
|
|
523
409
|
const path = lockPath(vault);
|
|
@@ -536,7 +422,7 @@ function isStale(lock, now) {
|
|
|
536
422
|
}
|
|
537
423
|
function acquireLock(vault, opts = {}) {
|
|
538
424
|
const path = lockPath(vault);
|
|
539
|
-
const dir =
|
|
425
|
+
const dir = join4(vault, ".skillwiki");
|
|
540
426
|
if (!existsSync3(dir)) {
|
|
541
427
|
mkdirSync3(dir, { recursive: true });
|
|
542
428
|
}
|
|
@@ -621,7 +507,7 @@ function acquireOwnedSyncLock(vault, opts) {
|
|
|
621
507
|
};
|
|
622
508
|
const path = lockPath(vault);
|
|
623
509
|
try {
|
|
624
|
-
mkdirSync3(
|
|
510
|
+
mkdirSync3(join4(vault, ".skillwiki"), { recursive: true });
|
|
625
511
|
} catch (error) {
|
|
626
512
|
return err("WRITE_FAILED", { path, message: String(error) });
|
|
627
513
|
}
|
|
@@ -653,12 +539,12 @@ function releaseOwnedSyncLock(handle) {
|
|
|
653
539
|
|
|
654
540
|
// src/commands/validate.ts
|
|
655
541
|
import { createHash as createHash2 } from "crypto";
|
|
656
|
-
import { readFile as
|
|
542
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
657
543
|
import { resolve, relative, sep } from "path";
|
|
658
544
|
|
|
659
545
|
// src/utils/index-entry.ts
|
|
660
|
-
import { readFile as
|
|
661
|
-
import { join as
|
|
546
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
547
|
+
import { join as join5 } from "path";
|
|
662
548
|
var TYPE_SECTION = {
|
|
663
549
|
entity: "Entities",
|
|
664
550
|
concept: "Concepts",
|
|
@@ -702,10 +588,10 @@ function renderIndexUpsert(text, input) {
|
|
|
702
588
|
});
|
|
703
589
|
}
|
|
704
590
|
async function upsertIndexEntry(input) {
|
|
705
|
-
const path =
|
|
591
|
+
const path = join5(input.vault, "index.md");
|
|
706
592
|
let before = "";
|
|
707
593
|
try {
|
|
708
|
-
before = await
|
|
594
|
+
before = await readFile2(path, "utf8");
|
|
709
595
|
} catch {
|
|
710
596
|
before = "";
|
|
711
597
|
}
|
|
@@ -740,7 +626,7 @@ var SCHEMAS = {
|
|
|
740
626
|
async function runValidate(input) {
|
|
741
627
|
let text;
|
|
742
628
|
try {
|
|
743
|
-
text = await
|
|
629
|
+
text = await readFile3(input.file, "utf8");
|
|
744
630
|
} catch {
|
|
745
631
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
746
632
|
}
|
|
@@ -797,7 +683,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
797
683
|
if (relPath.startsWith("..")) {
|
|
798
684
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: `file ${input.file} is not inside vault ${input.vault}` }) };
|
|
799
685
|
}
|
|
800
|
-
const
|
|
686
|
+
const operationId2 = createHash2("sha256").update("skillwiki-validate-apply-v1\0").update(relPath).update("\0").update(text).digest("hex");
|
|
801
687
|
if (det.schema === "typed-knowledge" || det.schema === "meta") {
|
|
802
688
|
const prepared = prepareTypedPage(text, relPath);
|
|
803
689
|
if (!prepared.ok) {
|
|
@@ -852,7 +738,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
852
738
|
const logged = await runLogAppend({
|
|
853
739
|
vault: input.vault,
|
|
854
740
|
content: `## [${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}] validate | added: ${relPath}`,
|
|
855
|
-
operationId,
|
|
741
|
+
operationId: operationId2,
|
|
856
742
|
strictLock: true
|
|
857
743
|
});
|
|
858
744
|
if (!logged.result.ok) {
|
|
@@ -880,7 +766,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
880
766
|
}
|
|
881
767
|
|
|
882
768
|
// src/commands/graph.ts
|
|
883
|
-
import { writeFile, mkdir
|
|
769
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
884
770
|
import { dirname } from "path";
|
|
885
771
|
|
|
886
772
|
// src/parsers/wikilinks.ts
|
|
@@ -1035,7 +921,7 @@ async function runGraphBuild(input) {
|
|
|
1035
921
|
const adamicAdar = computeAdamicAdar(adjacency);
|
|
1036
922
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
1037
923
|
try {
|
|
1038
|
-
await
|
|
924
|
+
await mkdir(dirname(input.out), { recursive: true });
|
|
1039
925
|
await writeFile(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
|
|
1040
926
|
} catch (e) {
|
|
1041
927
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
@@ -1079,7 +965,7 @@ function computeAdamicAdar(adj) {
|
|
|
1079
965
|
}
|
|
1080
966
|
|
|
1081
967
|
// src/utils/wiki-path.ts
|
|
1082
|
-
import { join as
|
|
968
|
+
import { join as join6 } from "path";
|
|
1083
969
|
async function resolveInitTimePath(input) {
|
|
1084
970
|
const chain = [];
|
|
1085
971
|
if (input.flag !== void 0 && input.flag.length > 0) {
|
|
@@ -1092,27 +978,27 @@ async function resolveInitTimePath(input) {
|
|
|
1092
978
|
return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
|
|
1093
979
|
}
|
|
1094
980
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
1095
|
-
const sw = await parseDotenvFile(
|
|
981
|
+
const sw = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
|
|
1096
982
|
if (sw.WIKI_PATH !== void 0) {
|
|
1097
983
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
|
|
1098
984
|
return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
|
|
1099
985
|
}
|
|
1100
986
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
1101
|
-
const hermes = await parseDotenvFile(
|
|
987
|
+
const hermes = await parseDotenvFile(join6(input.home, ".hermes", ".env"));
|
|
1102
988
|
if (hermes.WIKI_PATH !== void 0) {
|
|
1103
989
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
|
|
1104
990
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
1105
991
|
}
|
|
1106
992
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
|
|
1107
993
|
if (input.cwd) {
|
|
1108
|
-
const projCfg = await parseDotenvFile(
|
|
994
|
+
const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
|
|
1109
995
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
1110
996
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
1111
997
|
return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
|
|
1112
998
|
}
|
|
1113
999
|
}
|
|
1114
1000
|
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
1115
|
-
const fallback =
|
|
1001
|
+
const fallback = join6(input.home, "wiki");
|
|
1116
1002
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
1117
1003
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
1118
1004
|
}
|
|
@@ -1123,7 +1009,7 @@ async function resolveRuntimePath(input) {
|
|
|
1123
1009
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
1124
1010
|
}
|
|
1125
1011
|
if (input.explain) chain.push({ source: "flag", matched: false });
|
|
1126
|
-
const swGlobal = await parseDotenvFile(
|
|
1012
|
+
const swGlobal = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
|
|
1127
1013
|
const wikiName = input.wiki;
|
|
1128
1014
|
if (wikiName !== void 0 && wikiName.length > 0) {
|
|
1129
1015
|
if (wikiName.toLowerCase() === "default") {
|
|
@@ -1167,7 +1053,7 @@ async function resolveRuntimePath(input) {
|
|
|
1167
1053
|
}
|
|
1168
1054
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
1169
1055
|
if (input.cwd) {
|
|
1170
|
-
const projCfg = await parseDotenvFile(
|
|
1056
|
+
const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
|
|
1171
1057
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
1172
1058
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
1173
1059
|
return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
|
|
@@ -1287,156 +1173,12 @@ function simulateRemoval(adj, removed) {
|
|
|
1287
1173
|
}
|
|
1288
1174
|
|
|
1289
1175
|
// src/commands/audit.ts
|
|
1290
|
-
import { readFile as
|
|
1291
|
-
import { dirname as dirname2, resolve as resolve2, join as
|
|
1292
|
-
|
|
1293
|
-
// src/parsers/citations.ts
|
|
1294
|
-
var FENCE2 = /```[\s\S]*?```/g;
|
|
1295
|
-
var INLINE_CODE = /``[^`\n]+``|`[^`\n]+`/g;
|
|
1296
|
-
var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
|
|
1297
|
-
var FRONTMATTER = /^---\n[\s\S]*?\n---\n?/;
|
|
1298
|
-
function stripFences(body) {
|
|
1299
|
-
return body.replace(FENCE2, "").replace(INLINE_CODE, "");
|
|
1300
|
-
}
|
|
1301
|
-
var TILDE_FENCE = /~~~[\s\S]*?~~~/g;
|
|
1302
|
-
function stripFencedBlocks(body) {
|
|
1303
|
-
return body.replace(FENCE2, "").replace(TILDE_FENCE, "");
|
|
1304
|
-
}
|
|
1305
|
-
function extractCitationMarkers(body) {
|
|
1306
|
-
const stripped = stripFences(body);
|
|
1307
|
-
const out = [];
|
|
1308
|
-
let m;
|
|
1309
|
-
while ((m = MARKER_RE.exec(stripped)) !== null) {
|
|
1310
|
-
if (m[1] === "raw/...") continue;
|
|
1311
|
-
out.push({ marker: m[0], target: m[1] });
|
|
1312
|
-
}
|
|
1313
|
-
return out;
|
|
1314
|
-
}
|
|
1315
|
-
function hasSourcesFooter(body) {
|
|
1316
|
-
return /^## Sources\s*$/m.test(stripFencedBlocks(body));
|
|
1317
|
-
}
|
|
1318
|
-
function isLegacyCitationStyle(body) {
|
|
1319
|
-
const markers = extractCitationMarkers(body);
|
|
1320
|
-
if (markers.length === 0) return false;
|
|
1321
|
-
if (!hasSourcesFooter(body)) return true;
|
|
1322
|
-
const lines = stripFences(body.replace(FRONTMATTER, "")).split("\n");
|
|
1323
|
-
let inSources = false;
|
|
1324
|
-
let lastNonBlankWasTable = false;
|
|
1325
|
-
for (const line of lines) {
|
|
1326
|
-
if (/^## Sources\b/.test(line.trim())) {
|
|
1327
|
-
inSources = true;
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
if (inSources) continue;
|
|
1331
|
-
const matches = [...line.matchAll(MARKER_RE)];
|
|
1332
|
-
if (matches.length === 0) {
|
|
1333
|
-
if (line.trim().length > 0) lastNonBlankWasTable = /^\|/.test(line.trim());
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
const markerOnly = line.replace(MARKER_RE, "").trim();
|
|
1337
|
-
if (markerOnly.length === 0 && !lastNonBlankWasTable) return true;
|
|
1338
|
-
lastNonBlankWasTable = false;
|
|
1339
|
-
const lastMatch = matches[matches.length - 1];
|
|
1340
|
-
const afterLast = line.slice(lastMatch.index + lastMatch[0].length).replace(MARKER_RE, "").trim();
|
|
1341
|
-
if (afterLast.length > 0) return true;
|
|
1342
|
-
const beforeFirst = line.slice(0, matches[0].index).trim();
|
|
1343
|
-
if (beforeFirst.length > 0 && !/[.!?]["'"]*\s*$/.test(beforeFirst)) return true;
|
|
1344
|
-
}
|
|
1345
|
-
return false;
|
|
1346
|
-
}
|
|
1347
|
-
function hasOrphanedCitations(body) {
|
|
1348
|
-
const noFm = body.replace(FRONTMATTER, "");
|
|
1349
|
-
const stripped = stripFences(noFm);
|
|
1350
|
-
const lines = stripped.split("\n");
|
|
1351
|
-
const rawLines = noFm.split("\n");
|
|
1352
|
-
let inSources = false;
|
|
1353
|
-
let sourcesEnded = false;
|
|
1354
|
-
let sourcesStartLine = -1;
|
|
1355
|
-
let lastNonBlankInSources = -1;
|
|
1356
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1357
|
-
const line = lines[i];
|
|
1358
|
-
const trimmed = line.trim();
|
|
1359
|
-
if (/^## Sources\b/.test(trimmed)) {
|
|
1360
|
-
inSources = true;
|
|
1361
|
-
sourcesStartLine = i;
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
if (!inSources || sourcesEnded) continue;
|
|
1365
|
-
if (trimmed.length === 0) {
|
|
1366
|
-
if (lastNonBlankInSources >= 0) {
|
|
1367
|
-
sourcesEnded = true;
|
|
1368
|
-
}
|
|
1369
|
-
continue;
|
|
1370
|
-
}
|
|
1371
|
-
const isListItem = /^\s*(?:[-*]|\d+\.)\s+/.test(line);
|
|
1372
|
-
const hasMarker = /\^\[raw\//.test(line);
|
|
1373
|
-
const hasBacktickRawPath = /`raw\/[^`]+`/.test(rawLines[i]);
|
|
1374
|
-
if (isListItem && hasMarker) {
|
|
1375
|
-
lastNonBlankInSources = i;
|
|
1376
|
-
} else if (isListItem && hasBacktickRawPath) {
|
|
1377
|
-
lastNonBlankInSources = i;
|
|
1378
|
-
} else if (hasMarker && !isListItem) {
|
|
1379
|
-
return true;
|
|
1380
|
-
} else {
|
|
1381
|
-
sourcesEnded = true;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
if (sourcesStartLine === -1) return false;
|
|
1385
|
-
if (sourcesEnded) {
|
|
1386
|
-
const scanStart = Math.max(lastNonBlankInSources + 1, sourcesStartLine + 1);
|
|
1387
|
-
for (let i = scanStart; i < lines.length; i++) {
|
|
1388
|
-
if (/\^\[raw\//.test(lines[i])) {
|
|
1389
|
-
return true;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
return false;
|
|
1394
|
-
}
|
|
1395
|
-
function hasWikilinkCitations(body) {
|
|
1396
|
-
const stripped = stripFences(body);
|
|
1397
|
-
return /\[\[raw\/[^\]]+\]\]/.test(stripped);
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// src/utils/raw-source.ts
|
|
1401
|
-
import { existsSync as existsSync4 } from "fs";
|
|
1402
|
-
import { stat as stat2 } from "fs/promises";
|
|
1403
|
-
import { join as join8 } from "path";
|
|
1404
|
-
function normalizeRawSourceTarget(entry) {
|
|
1405
|
-
let target = entry.trim().replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
1406
|
-
target = target.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
1407
|
-
if (!target.startsWith("raw/") && !target.startsWith("_archive/raw/")) return null;
|
|
1408
|
-
return target;
|
|
1409
|
-
}
|
|
1410
|
-
function rawSourceTargetCandidates(vault, target) {
|
|
1411
|
-
const normalized = normalizeRawSourceTarget(target);
|
|
1412
|
-
if (!normalized) return [];
|
|
1413
|
-
const candidates = [join8(vault, normalized)];
|
|
1414
|
-
if (!normalized.endsWith(".md")) candidates.push(join8(vault, `${normalized}.md`));
|
|
1415
|
-
if (normalized.startsWith("raw/")) {
|
|
1416
|
-
candidates.push(join8(vault, "_archive", normalized));
|
|
1417
|
-
if (!normalized.endsWith(".md")) candidates.push(join8(vault, "_archive", `${normalized}.md`));
|
|
1418
|
-
}
|
|
1419
|
-
return [...new Set(candidates)];
|
|
1420
|
-
}
|
|
1421
|
-
function rawSourceTargetExistsSync(vault, target) {
|
|
1422
|
-
return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync4(candidate));
|
|
1423
|
-
}
|
|
1424
|
-
async function rawSourceTargetExists(vault, target) {
|
|
1425
|
-
for (const candidate of rawSourceTargetCandidates(vault, target)) {
|
|
1426
|
-
try {
|
|
1427
|
-
await stat2(candidate);
|
|
1428
|
-
return true;
|
|
1429
|
-
} catch {
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
return false;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// src/commands/audit.ts
|
|
1176
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1177
|
+
import { dirname as dirname2, resolve as resolve2, join as join7 } from "path";
|
|
1436
1178
|
async function runAudit(input) {
|
|
1437
1179
|
let text;
|
|
1438
1180
|
try {
|
|
1439
|
-
text = await
|
|
1181
|
+
text = await readFile4(input.file, "utf8");
|
|
1440
1182
|
} catch {
|
|
1441
1183
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
1442
1184
|
}
|
|
@@ -1489,7 +1231,7 @@ async function findVaultRoot(start) {
|
|
|
1489
1231
|
let cur = start;
|
|
1490
1232
|
for (let i = 0; i < 20; i++) {
|
|
1491
1233
|
try {
|
|
1492
|
-
await
|
|
1234
|
+
await stat2(join7(cur, "SCHEMA.md"));
|
|
1493
1235
|
return cur;
|
|
1494
1236
|
} catch {
|
|
1495
1237
|
}
|
|
@@ -1581,8 +1323,8 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
|
|
|
1581
1323
|
}
|
|
1582
1324
|
|
|
1583
1325
|
// src/commands/tag-audit.ts
|
|
1584
|
-
import { readFile as
|
|
1585
|
-
import { join as
|
|
1326
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1327
|
+
import { join as join8 } from "path";
|
|
1586
1328
|
|
|
1587
1329
|
// src/parsers/taxonomy.ts
|
|
1588
1330
|
import yaml from "js-yaml";
|
|
@@ -1607,12 +1349,12 @@ function parseTaxonomyDocument(schemaText) {
|
|
|
1607
1349
|
const nextHeading = /^#{1,2}[ \t]+/m.exec(unboundedTail);
|
|
1608
1350
|
const sectionEnd = nextHeading?.index === void 0 ? schemaText.length : afterHeading + nextHeading.index;
|
|
1609
1351
|
const sectionText = schemaText.slice(afterHeading, sectionEnd);
|
|
1610
|
-
const
|
|
1611
|
-
if (!
|
|
1352
|
+
const open = /^```yaml[ \t]*\r?$/m.exec(sectionText);
|
|
1353
|
+
if (!open || open.index === void 0) {
|
|
1612
1354
|
return err("NO_TAXONOMY_BLOCK", { message: "Fenced YAML taxonomy block not found" });
|
|
1613
1355
|
}
|
|
1614
|
-
const openStart = afterHeading +
|
|
1615
|
-
const yamlStart = openStart +
|
|
1356
|
+
const openStart = afterHeading + open.index;
|
|
1357
|
+
const yamlStart = openStart + open[0].length + 1;
|
|
1616
1358
|
const afterOpen = schemaText.slice(yamlStart, sectionEnd);
|
|
1617
1359
|
const close = /^```[ \t]*\r?$/m.exec(afterOpen);
|
|
1618
1360
|
if (!close || close.index === void 0) {
|
|
@@ -1735,7 +1477,7 @@ async function runTagAudit(input) {
|
|
|
1735
1477
|
const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
|
|
1736
1478
|
if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
|
|
1737
1479
|
const scan = scanResult.data;
|
|
1738
|
-
const schemaText = await
|
|
1480
|
+
const schemaText = await readFile5(join8(input.vault, "SCHEMA.md"), "utf8");
|
|
1739
1481
|
const tax = extractTaxonomy(schemaText);
|
|
1740
1482
|
if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
|
|
1741
1483
|
const allowed = new Set(tax.data);
|
|
@@ -1767,8 +1509,8 @@ async function runTagAudit(input) {
|
|
|
1767
1509
|
}
|
|
1768
1510
|
|
|
1769
1511
|
// src/commands/index-check.ts
|
|
1770
|
-
import { readFile as
|
|
1771
|
-
import { join as
|
|
1512
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1513
|
+
import { join as join9 } from "path";
|
|
1772
1514
|
function normalizeIndexTarget(raw) {
|
|
1773
1515
|
return raw.replace(/\.md$/, "").replace(/^\.?\//, "");
|
|
1774
1516
|
}
|
|
@@ -1780,7 +1522,7 @@ async function runIndexCheck(input) {
|
|
|
1780
1522
|
}
|
|
1781
1523
|
let indexText = "";
|
|
1782
1524
|
try {
|
|
1783
|
-
indexText = await
|
|
1525
|
+
indexText = await readFile6(join9(input.vault, "index.md"), "utf8");
|
|
1784
1526
|
} catch {
|
|
1785
1527
|
}
|
|
1786
1528
|
const indexTargets = /* @__PURE__ */ new Set();
|
|
@@ -1834,20 +1576,20 @@ async function runIndexCheck(input) {
|
|
|
1834
1576
|
}
|
|
1835
1577
|
|
|
1836
1578
|
// src/commands/project-index.ts
|
|
1837
|
-
import { readdir
|
|
1838
|
-
import { join as
|
|
1579
|
+
import { readdir, readFile as readFile7, mkdir as mkdir2 } from "fs/promises";
|
|
1580
|
+
import { join as join10, dirname as dirname3, basename } from "path";
|
|
1839
1581
|
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
1840
1582
|
var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
|
|
1841
1583
|
async function scanMarkdownTree(rootAbs, rootRel) {
|
|
1842
1584
|
const found = [];
|
|
1843
1585
|
let entries;
|
|
1844
1586
|
try {
|
|
1845
|
-
entries = await
|
|
1587
|
+
entries = await readdir(rootAbs, { withFileTypes: true });
|
|
1846
1588
|
} catch {
|
|
1847
1589
|
return found;
|
|
1848
1590
|
}
|
|
1849
1591
|
for (const entry of entries) {
|
|
1850
|
-
const abs =
|
|
1592
|
+
const abs = join10(rootAbs, entry.name);
|
|
1851
1593
|
const rel = `${rootRel}/${entry.name}`;
|
|
1852
1594
|
if (entry.isDirectory()) {
|
|
1853
1595
|
found.push(...await scanMarkdownTree(abs, rel));
|
|
@@ -1876,23 +1618,23 @@ function projectLocalType(slug, page, data) {
|
|
|
1876
1618
|
return typeof data.type === "string" ? data.type : "project";
|
|
1877
1619
|
}
|
|
1878
1620
|
async function renderProjectIndex(vault, slug, opts = {}) {
|
|
1879
|
-
const projectDir =
|
|
1621
|
+
const projectDir = join10(vault, "projects", slug);
|
|
1880
1622
|
try {
|
|
1881
|
-
await
|
|
1623
|
+
await readdir(projectDir);
|
|
1882
1624
|
} catch {
|
|
1883
1625
|
return err("PROJECT_NOT_FOUND", { slug, path: projectDir });
|
|
1884
1626
|
}
|
|
1885
1627
|
const wikilinkPattern = `[[${slug}]]`;
|
|
1886
1628
|
const entries = [];
|
|
1887
|
-
const compoundDir =
|
|
1629
|
+
const compoundDir = join10(vault, "projects", slug, "compound");
|
|
1888
1630
|
try {
|
|
1889
|
-
const compoundFiles = await
|
|
1631
|
+
const compoundFiles = await readdir(compoundDir, { withFileTypes: true });
|
|
1890
1632
|
for (const entry of compoundFiles) {
|
|
1891
1633
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1892
|
-
const filePath =
|
|
1634
|
+
const filePath = join10(compoundDir, entry.name);
|
|
1893
1635
|
let text;
|
|
1894
1636
|
try {
|
|
1895
|
-
text = await
|
|
1637
|
+
text = await readFile7(filePath, "utf8");
|
|
1896
1638
|
} catch {
|
|
1897
1639
|
continue;
|
|
1898
1640
|
}
|
|
@@ -1909,16 +1651,16 @@ async function renderProjectIndex(vault, slug, opts = {}) {
|
|
|
1909
1651
|
for (const dir of LAYER2_DIRS) {
|
|
1910
1652
|
let files;
|
|
1911
1653
|
try {
|
|
1912
|
-
files = await
|
|
1654
|
+
files = await readdir(join10(vault, dir), { withFileTypes: true });
|
|
1913
1655
|
} catch {
|
|
1914
1656
|
continue;
|
|
1915
1657
|
}
|
|
1916
1658
|
for (const entry of files) {
|
|
1917
1659
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1918
|
-
const filePath =
|
|
1660
|
+
const filePath = join10(vault, dir, entry.name);
|
|
1919
1661
|
let text;
|
|
1920
1662
|
try {
|
|
1921
|
-
text = await
|
|
1663
|
+
text = await readFile7(filePath, "utf8");
|
|
1922
1664
|
} catch {
|
|
1923
1665
|
continue;
|
|
1924
1666
|
}
|
|
@@ -1934,14 +1676,14 @@ async function renderProjectIndex(vault, slug, opts = {}) {
|
|
|
1934
1676
|
}
|
|
1935
1677
|
}
|
|
1936
1678
|
for (const dir of PROJECT_LOCAL_DIRS) {
|
|
1937
|
-
const rootAbs =
|
|
1679
|
+
const rootAbs = join10(projectDir, dir);
|
|
1938
1680
|
const rootRel = `projects/${slug}/${dir}`;
|
|
1939
1681
|
const pages = await scanMarkdownTree(rootAbs, rootRel);
|
|
1940
1682
|
for (const page of pages) {
|
|
1941
|
-
const filePath =
|
|
1683
|
+
const filePath = join10(vault, page);
|
|
1942
1684
|
let text;
|
|
1943
1685
|
try {
|
|
1944
|
-
text = await
|
|
1686
|
+
text = await readFile7(filePath, "utf8");
|
|
1945
1687
|
} catch {
|
|
1946
1688
|
continue;
|
|
1947
1689
|
}
|
|
@@ -2014,7 +1756,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
|
2014
1756
|
}
|
|
2015
1757
|
async function runProjectIndex(input) {
|
|
2016
1758
|
const slug = input.slug;
|
|
2017
|
-
const projectDir =
|
|
1759
|
+
const projectDir = join10(input.vault, "projects", slug);
|
|
2018
1760
|
const rendered = await renderProjectIndex(input.vault, slug);
|
|
2019
1761
|
if (!rendered.ok) {
|
|
2020
1762
|
return {
|
|
@@ -2022,12 +1764,12 @@ async function runProjectIndex(input) {
|
|
|
2022
1764
|
result: rendered
|
|
2023
1765
|
};
|
|
2024
1766
|
}
|
|
2025
|
-
const indexPath =
|
|
1767
|
+
const indexPath = join10(projectDir, "knowledge.md");
|
|
2026
1768
|
const entries = rendered.data.entries;
|
|
2027
1769
|
let existing = false;
|
|
2028
1770
|
let stale = false;
|
|
2029
1771
|
try {
|
|
2030
|
-
const existingText = await
|
|
1772
|
+
const existingText = await readFile7(indexPath, "utf8");
|
|
2031
1773
|
existing = true;
|
|
2032
1774
|
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
2033
1775
|
const existingPages = new Set(existingEntries.map((l) => {
|
|
@@ -2040,7 +1782,7 @@ async function runProjectIndex(input) {
|
|
|
2040
1782
|
}
|
|
2041
1783
|
if (input.apply) {
|
|
2042
1784
|
try {
|
|
2043
|
-
await
|
|
1785
|
+
await mkdir2(dirname3(indexPath), { recursive: true });
|
|
2044
1786
|
} catch (e) {
|
|
2045
1787
|
return {
|
|
2046
1788
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -2072,8 +1814,8 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
|
|
|
2072
1814
|
}
|
|
2073
1815
|
|
|
2074
1816
|
// src/commands/stale.ts
|
|
2075
|
-
import { readdir as
|
|
2076
|
-
import { join as
|
|
1817
|
+
import { readdir as readdir2, rename, mkdir as mkdir4, readFile as readFile9 } from "fs/promises";
|
|
1818
|
+
import { join as join11 } from "path";
|
|
2077
1819
|
|
|
2078
1820
|
// src/parsers/expiry-annotations.ts
|
|
2079
1821
|
var HEADING_RE = /^#{1,6}\s+(.+)$/;
|
|
@@ -2109,6 +1851,144 @@ function parseExpiryAnnotations(content, pagePath) {
|
|
|
2109
1851
|
return annotations;
|
|
2110
1852
|
}
|
|
2111
1853
|
|
|
1854
|
+
// src/utils/raw-structural-transaction.ts
|
|
1855
|
+
import { createHash as createHash3 } from "crypto";
|
|
1856
|
+
import { copyFile, mkdir as mkdir3, readFile as readFile8, unlink } from "fs/promises";
|
|
1857
|
+
import { constants } from "fs";
|
|
1858
|
+
import { dirname as dirname4 } from "path";
|
|
1859
|
+
function sha256(bytes) {
|
|
1860
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
1861
|
+
}
|
|
1862
|
+
function token(plan) {
|
|
1863
|
+
const payload = Buffer.from(JSON.stringify(plan), "utf8").toString("base64url");
|
|
1864
|
+
return `swraw1.${payload}.${sha256(Buffer.from(payload, "utf8"))}`;
|
|
1865
|
+
}
|
|
1866
|
+
async function planRawStructuralMove(input) {
|
|
1867
|
+
const sourcePath = classifyRawPath(input.source);
|
|
1868
|
+
if (!sourcePath.ok) return sourcePath;
|
|
1869
|
+
const destinationPath = classifyRawPath(input.destination);
|
|
1870
|
+
if (!destinationPath.ok) return destinationPath;
|
|
1871
|
+
if (sourcePath.data.category === "assets" || destinationPath.data.category === "assets") {
|
|
1872
|
+
return err("RAW_ASSET_PATH_FROZEN", {
|
|
1873
|
+
source: input.source,
|
|
1874
|
+
destination: input.destination,
|
|
1875
|
+
message: "referenced asset paths are stable; routine raw lifecycle moves never move assets"
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
if (sourcePath.data.storage !== "active") {
|
|
1879
|
+
return err("RAW_SOURCE_NOT_ACTIVE", { path: input.source, storage: sourcePath.data.storage });
|
|
1880
|
+
}
|
|
1881
|
+
if (sourcePath.data.category !== destinationPath.data.category) {
|
|
1882
|
+
return err("RAW_MOVE_CATEGORY_MISMATCH", {
|
|
1883
|
+
source: input.source,
|
|
1884
|
+
destination: input.destination,
|
|
1885
|
+
source_category: sourcePath.data.category,
|
|
1886
|
+
destination_category: destinationPath.data.category
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
const expectedStorage = input.operation === "archive" ? "archived" : input.operation === "deduplicate" ? "duplicate" : "active";
|
|
1890
|
+
if (destinationPath.data.storage !== expectedStorage) {
|
|
1891
|
+
return err("RAW_MOVE_DESTINATION_INVALID", {
|
|
1892
|
+
operation: input.operation,
|
|
1893
|
+
destination: input.destination,
|
|
1894
|
+
expected_storage: expectedStorage,
|
|
1895
|
+
actual_storage: destinationPath.data.storage
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
const allowed = authorizeRawOperation({
|
|
1899
|
+
operationClass: "preserve-move",
|
|
1900
|
+
trigger: "attended-apply",
|
|
1901
|
+
source: input.source,
|
|
1902
|
+
destination: input.destination
|
|
1903
|
+
});
|
|
1904
|
+
if (!allowed.ok) return allowed;
|
|
1905
|
+
if (input.source === input.destination) return err("RAW_MOVE_NOOP", { path: input.source });
|
|
1906
|
+
const sourceResolved = await resolveExistingRegularFileInsideVault(input.vault, input.source);
|
|
1907
|
+
if (!sourceResolved.ok) return err("RAW_SOURCE_UNSAFE", { path: input.source, cause: sourceResolved });
|
|
1908
|
+
const destinationResolved = await resolveAbsentTargetInsideVault(input.vault, input.destination);
|
|
1909
|
+
if (!destinationResolved.ok) {
|
|
1910
|
+
return destinationResolved.error === "RAW_DESTINATION_EXISTS" ? destinationResolved : err("RAW_DESTINATION_UNSAFE", { path: input.destination, cause: destinationResolved });
|
|
1911
|
+
}
|
|
1912
|
+
const sourceBytes = await readFile8(sourceResolved.data);
|
|
1913
|
+
const sourceSha = sha256(sourceBytes);
|
|
1914
|
+
const operation_id = operationId("raw-structural", [input.operation, input.source, input.destination, sourceSha]);
|
|
1915
|
+
const base = {
|
|
1916
|
+
operation: input.operation,
|
|
1917
|
+
source: input.source,
|
|
1918
|
+
destination: input.destination,
|
|
1919
|
+
source_sha256: sourceSha,
|
|
1920
|
+
destination_state: "absent",
|
|
1921
|
+
operation_id
|
|
1922
|
+
};
|
|
1923
|
+
return ok({ ...base, approval_token: token(base) });
|
|
1924
|
+
}
|
|
1925
|
+
async function applyRawStructuralMove(input) {
|
|
1926
|
+
const planned = await planRawStructuralMove(input);
|
|
1927
|
+
if (!planned.ok) return planned;
|
|
1928
|
+
if (input.approve !== planned.data.approval_token) {
|
|
1929
|
+
return err("APPROVAL_INVALID", { message: "raw structural approval does not match live state" });
|
|
1930
|
+
}
|
|
1931
|
+
const sourceResolved = await resolveExistingRegularFileInsideVault(input.vault, input.source);
|
|
1932
|
+
if (!sourceResolved.ok) return err("RAW_SOURCE_UNSAFE", { path: input.source, cause: sourceResolved });
|
|
1933
|
+
const destinationResolved = await resolveAbsentTargetInsideVault(input.vault, input.destination);
|
|
1934
|
+
if (!destinationResolved.ok) {
|
|
1935
|
+
return destinationResolved.error === "RAW_DESTINATION_EXISTS" ? destinationResolved : err("RAW_DESTINATION_UNSAFE", { path: input.destination, cause: destinationResolved });
|
|
1936
|
+
}
|
|
1937
|
+
const sourceAbs = sourceResolved.data;
|
|
1938
|
+
const destinationAbs = destinationResolved.data;
|
|
1939
|
+
await mkdir3(dirname4(destinationAbs), { recursive: true });
|
|
1940
|
+
try {
|
|
1941
|
+
await copyFile(sourceAbs, destinationAbs, constants.COPYFILE_EXCL);
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
return err("RAW_MOVE_COPY_FAILED", { source: input.source, destination: input.destination, message: String(error) });
|
|
1944
|
+
}
|
|
1945
|
+
const destinationBytes = await readFile8(destinationAbs);
|
|
1946
|
+
const destinationSha = sha256(destinationBytes);
|
|
1947
|
+
if (destinationSha !== planned.data.source_sha256) {
|
|
1948
|
+
return err("RAW_MOVE_HASH_MISMATCH", { source: input.source, destination: input.destination, source_sha256: planned.data.source_sha256, destination_sha256: destinationSha });
|
|
1949
|
+
}
|
|
1950
|
+
const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1951
|
+
const event = await (input.writeEvent ?? writeLogEvent)(input.vault, {
|
|
1952
|
+
schema: "skillwiki-log-event/v1",
|
|
1953
|
+
operation_id: planned.data.operation_id,
|
|
1954
|
+
occurred_at: occurredAt,
|
|
1955
|
+
host_id: input.hostId ?? "local",
|
|
1956
|
+
actor: input.actor ?? "skillwiki-cli",
|
|
1957
|
+
kind: "source-relocation",
|
|
1958
|
+
target: input.destination,
|
|
1959
|
+
note: input.reason ?? `${input.operation} ${input.source} to ${input.destination}`,
|
|
1960
|
+
metadata: {
|
|
1961
|
+
operation: input.operation,
|
|
1962
|
+
previous_path: input.source,
|
|
1963
|
+
current_path: input.destination,
|
|
1964
|
+
source_sha256: planned.data.source_sha256,
|
|
1965
|
+
destination_sha256: destinationSha,
|
|
1966
|
+
command: input.command ?? "skillwiki",
|
|
1967
|
+
authority: "attended-apply",
|
|
1968
|
+
citation_changes: input.citationChanges ?? []
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
if (!event.ok) return event;
|
|
1972
|
+
try {
|
|
1973
|
+
await unlink(sourceAbs);
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
return err("RAW_MOVE_SOURCE_RETAINED", {
|
|
1976
|
+
source: input.source,
|
|
1977
|
+
destination: input.destination,
|
|
1978
|
+
event_path: event.data.path,
|
|
1979
|
+
message: String(error)
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
return ok({
|
|
1983
|
+
operation: planned.data.operation,
|
|
1984
|
+
source: planned.data.source,
|
|
1985
|
+
destination: planned.data.destination,
|
|
1986
|
+
source_sha256: planned.data.source_sha256,
|
|
1987
|
+
destination_sha256: destinationSha,
|
|
1988
|
+
event_path: event.data.path
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
|
|
2112
1992
|
// src/commands/stale.ts
|
|
2113
1993
|
function daysSince(isoDate) {
|
|
2114
1994
|
return Math.floor((Date.now() - Date.parse(isoDate)) / 864e5);
|
|
@@ -2122,10 +2002,10 @@ async function runStale(input) {
|
|
|
2122
2002
|
const archived = [];
|
|
2123
2003
|
const workDirs = /* @__PURE__ */ new Map();
|
|
2124
2004
|
const workDirsBySlug = /* @__PURE__ */ new Map();
|
|
2125
|
-
const projectsDir =
|
|
2005
|
+
const projectsDir = join11(input.vault, "projects");
|
|
2126
2006
|
let projectSlugs = [];
|
|
2127
2007
|
try {
|
|
2128
|
-
projectSlugs = (await
|
|
2008
|
+
projectSlugs = (await readdir2(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2129
2009
|
} catch {
|
|
2130
2010
|
}
|
|
2131
2011
|
if (input.project) {
|
|
@@ -2135,10 +2015,10 @@ async function runStale(input) {
|
|
|
2135
2015
|
projectSlugs = [input.project];
|
|
2136
2016
|
}
|
|
2137
2017
|
for (const slug of projectSlugs) {
|
|
2138
|
-
const workPath =
|
|
2018
|
+
const workPath = join11(projectsDir, slug, "work");
|
|
2139
2019
|
let entries;
|
|
2140
2020
|
try {
|
|
2141
|
-
entries = await
|
|
2021
|
+
entries = await readdir2(workPath, { withFileTypes: true });
|
|
2142
2022
|
} catch {
|
|
2143
2023
|
continue;
|
|
2144
2024
|
}
|
|
@@ -2146,11 +2026,11 @@ async function runStale(input) {
|
|
|
2146
2026
|
for (const e of entries) {
|
|
2147
2027
|
if (!e.isDirectory()) continue;
|
|
2148
2028
|
const relDir = `projects/${slug}/work/${e.name}`;
|
|
2149
|
-
const absDir =
|
|
2029
|
+
const absDir = join11(workPath, e.name);
|
|
2150
2030
|
let status = "";
|
|
2151
2031
|
let files;
|
|
2152
2032
|
try {
|
|
2153
|
-
files = await
|
|
2033
|
+
files = await readdir2(absDir);
|
|
2154
2034
|
} catch {
|
|
2155
2035
|
workDirs.set(relDir, "");
|
|
2156
2036
|
slugDirs.set(e.name, "");
|
|
@@ -2159,7 +2039,7 @@ async function runStale(input) {
|
|
|
2159
2039
|
for (const f of files) {
|
|
2160
2040
|
if (!f.endsWith(".md")) continue;
|
|
2161
2041
|
try {
|
|
2162
|
-
const fm = extractFrontmatter(await readFile9(
|
|
2042
|
+
const fm = extractFrontmatter(await readFile9(join11(absDir, f), "utf8"));
|
|
2163
2043
|
if (fm.ok && typeof fm.data.status === "string") {
|
|
2164
2044
|
status = fm.data.status;
|
|
2165
2045
|
break;
|
|
@@ -2255,7 +2135,7 @@ async function runStale(input) {
|
|
|
2255
2135
|
}
|
|
2256
2136
|
}
|
|
2257
2137
|
await mapWithConcurrency([...workDirs.keys()], vaultIoConcurrency(), async (relDir) => {
|
|
2258
|
-
const specPath =
|
|
2138
|
+
const specPath = join11(input.vault, relDir, "spec.md");
|
|
2259
2139
|
try {
|
|
2260
2140
|
const specContent = await readFile9(specPath, "utf8");
|
|
2261
2141
|
const specFm = extractFrontmatter(specContent);
|
|
@@ -2286,7 +2166,7 @@ async function runStale(input) {
|
|
|
2286
2166
|
if (daysSince(dateStr) < input.days) continue;
|
|
2287
2167
|
let files;
|
|
2288
2168
|
try {
|
|
2289
|
-
files = await
|
|
2169
|
+
files = await readdir2(join11(input.vault, relDir));
|
|
2290
2170
|
} catch {
|
|
2291
2171
|
continue;
|
|
2292
2172
|
}
|
|
@@ -2356,48 +2236,92 @@ async function runStale(input) {
|
|
|
2356
2236
|
staleSections.push(...staleSectionResults.flat());
|
|
2357
2237
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2358
2238
|
if (input.archive) {
|
|
2359
|
-
const archiveDir =
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2239
|
+
const archiveDir = join11(input.vault, "_archive", today);
|
|
2240
|
+
const relocations = await readSourceRelocations(input.vault);
|
|
2241
|
+
if (!relocations.ok) return { exitCode: ExitCode.WRITE_FAILED, result: relocations };
|
|
2242
|
+
const typedPaths = new Set(scan.typedKnowledge.map((page) => page.relPath));
|
|
2243
|
+
const references = await buildSourceReferenceIndex({
|
|
2244
|
+
typedPages: scan.typedKnowledge,
|
|
2245
|
+
otherPages: scan.allMarkdown.filter((page) => !typedPaths.has(page.relPath) && !page.relPath.startsWith("raw/")),
|
|
2246
|
+
availableRawPaths: scan.raw.map((page) => page.relPath),
|
|
2247
|
+
relocationProjection: buildSourceRelocationProjection(relocations.data)
|
|
2248
|
+
});
|
|
2249
|
+
const citedRawPaths = /* @__PURE__ */ new Set([
|
|
2250
|
+
...references.integratedBy.keys(),
|
|
2251
|
+
...references.referencedElsewhereBy.keys()
|
|
2252
|
+
]);
|
|
2253
|
+
const rawPlans = [];
|
|
2373
2254
|
for (const t of staleTranscripts) {
|
|
2374
2255
|
if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2256
|
+
const destination = lifecycleDestination(t.path, "archive");
|
|
2257
|
+
if (!destination.ok) return { exitCode: ExitCode.WRITE_FAILED, result: destination };
|
|
2258
|
+
const plan = await planRawStructuralMove({ vault: input.vault, operation: "archive", source: t.path, destination: destination.data });
|
|
2259
|
+
if (!plan.ok) return { exitCode: ExitCode.WRITE_FAILED, result: plan };
|
|
2260
|
+
rawPlans.push({
|
|
2261
|
+
from: t.path,
|
|
2262
|
+
to: destination.data,
|
|
2263
|
+
approval: plan.data.approval_token,
|
|
2264
|
+
sourceSha256: plan.data.source_sha256
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
const workPlans = [...incompleteWorkItems, ...doneWorkItems].map((w) => w.path).sort();
|
|
2268
|
+
const approvalToken = operationId("stale-archive-approval", [
|
|
2269
|
+
today,
|
|
2270
|
+
...rawPlans.flatMap((plan) => [plan.from, plan.to, plan.sourceSha256, plan.approval]),
|
|
2271
|
+
...workPlans
|
|
2272
|
+
]);
|
|
2273
|
+
if (!input.apply) {
|
|
2274
|
+
const total2 = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length + staleSections.length;
|
|
2275
|
+
return { exitCode: total2 > 0 ? ExitCode.STALE_PAGE : ExitCode.OK, result: ok({
|
|
2276
|
+
stale: [...stale, ...staleTranscripts.map((t) => ({ page: t.path, reason: t.reason })), ...unclaimedTranscripts.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 }))],
|
|
2277
|
+
stale_transcripts: staleTranscripts,
|
|
2278
|
+
unclaimed_transcripts: unclaimedTranscripts,
|
|
2279
|
+
incomplete_work_items: incompleteWorkItems,
|
|
2280
|
+
done_work_items: doneWorkItems,
|
|
2281
|
+
stale_sections: staleSections,
|
|
2282
|
+
archived: [],
|
|
2283
|
+
planned_archives: rawPlans.map((plan) => ({ from: plan.from, to: plan.to })),
|
|
2284
|
+
approval_token: approvalToken,
|
|
2285
|
+
humanHint: `DRY-RUN \u2014 ${rawPlans.length} raw transcript archive(s) and ${workPlans.length} work archive(s) require --apply --approve ${approvalToken}`
|
|
2286
|
+
}) };
|
|
2287
|
+
}
|
|
2288
|
+
if (input.approve !== approvalToken) {
|
|
2289
|
+
return { exitCode: ExitCode.USAGE, result: { ok: false, error: "APPROVAL_INVALID", detail: { message: "stale archive approval does not match live state", approval_token: approvalToken } } };
|
|
2290
|
+
}
|
|
2291
|
+
await mkdir4(archiveDir, { recursive: true });
|
|
2292
|
+
for (const plan of rawPlans) {
|
|
2293
|
+
const moved = await applyRawStructuralMove({
|
|
2294
|
+
vault: input.vault,
|
|
2295
|
+
operation: "archive",
|
|
2296
|
+
source: plan.from,
|
|
2297
|
+
destination: plan.to,
|
|
2298
|
+
approve: plan.approval,
|
|
2299
|
+
command: "skillwiki stale --archive --apply"
|
|
2300
|
+
});
|
|
2301
|
+
if (!moved.ok) return { exitCode: ExitCode.WRITE_FAILED, result: moved };
|
|
2302
|
+
archived.push(plan.from);
|
|
2381
2303
|
}
|
|
2382
2304
|
for (const w of [...incompleteWorkItems, ...doneWorkItems]) {
|
|
2383
2305
|
const parts = w.path.split("/");
|
|
2384
2306
|
if (parts.length >= 4 && parts[0] === "projects") {
|
|
2385
2307
|
const slug = parts[1];
|
|
2386
2308
|
const itemName = parts[3];
|
|
2387
|
-
const histDir =
|
|
2309
|
+
const histDir = join11(input.vault, "projects", slug, "history", "archived-work");
|
|
2388
2310
|
await mkdir4(histDir, { recursive: true });
|
|
2389
|
-
const dest =
|
|
2311
|
+
const dest = join11(histDir, itemName);
|
|
2390
2312
|
try {
|
|
2391
|
-
await rename(
|
|
2313
|
+
await rename(join11(input.vault, w.path), dest);
|
|
2392
2314
|
archived.push(w.path);
|
|
2393
|
-
} catch {
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: w.path, destination: dest, message: String(error) }) };
|
|
2394
2317
|
}
|
|
2395
2318
|
} else {
|
|
2396
|
-
const dest =
|
|
2319
|
+
const dest = join11(archiveDir, w.path.replace(/\//g, "_"));
|
|
2397
2320
|
try {
|
|
2398
|
-
await rename(
|
|
2321
|
+
await rename(join11(input.vault, w.path), dest);
|
|
2399
2322
|
archived.push(w.path);
|
|
2400
|
-
} catch {
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: w.path, destination: dest, message: String(error) }) };
|
|
2401
2325
|
}
|
|
2402
2326
|
}
|
|
2403
2327
|
}
|
|
@@ -2450,16 +2374,48 @@ async function runPagesize(input) {
|
|
|
2450
2374
|
}
|
|
2451
2375
|
|
|
2452
2376
|
// src/commands/log-rotate.ts
|
|
2453
|
-
import { readFile as readFile10, rename as rename2, writeFile as writeFile2, stat as
|
|
2454
|
-
import { join as
|
|
2377
|
+
import { access, readFile as readFile10, rename as rename2, writeFile as writeFile2, stat as stat3 } from "fs/promises";
|
|
2378
|
+
import { join as join12 } from "path";
|
|
2455
2379
|
var ENTRY_RE2 = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
2380
|
+
var FULL_DATE_RE = /^## \[(\d{4}-\d{2}-\d{2})\]/gm;
|
|
2381
|
+
async function pathExists(p) {
|
|
2382
|
+
try {
|
|
2383
|
+
await access(p);
|
|
2384
|
+
return true;
|
|
2385
|
+
} catch {
|
|
2386
|
+
return false;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
function archiveNameForExistingYearLog(existingText, fallbackYear) {
|
|
2390
|
+
const dates = [...existingText.matchAll(FULL_DATE_RE)].map((m) => m[1]);
|
|
2391
|
+
if (dates.length === 0) {
|
|
2392
|
+
return `log-archive-${fallbackYear}-unknown.md`;
|
|
2393
|
+
}
|
|
2394
|
+
const first = dates[0];
|
|
2395
|
+
const last = dates[dates.length - 1];
|
|
2396
|
+
const lastMd = last.slice(5);
|
|
2397
|
+
return `log-archive-${first}-to-${lastMd}.md`;
|
|
2398
|
+
}
|
|
2399
|
+
async function sidelineExistingYearLog(vault, rotatedPath, fallbackYear) {
|
|
2400
|
+
if (!await pathExists(rotatedPath)) return void 0;
|
|
2401
|
+
const existingText = await readFile10(rotatedPath, "utf8");
|
|
2402
|
+
let archiveName = archiveNameForExistingYearLog(existingText, fallbackYear);
|
|
2403
|
+
let archivePath = join12(vault, archiveName);
|
|
2404
|
+
if (await pathExists(archivePath)) {
|
|
2405
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2406
|
+
archiveName = archiveName.replace(/\.md$/, `-${stamp}.md`);
|
|
2407
|
+
archivePath = join12(vault, archiveName);
|
|
2408
|
+
}
|
|
2409
|
+
await rename2(rotatedPath, archivePath);
|
|
2410
|
+
return archiveName;
|
|
2411
|
+
}
|
|
2456
2412
|
async function runLogRotate(input) {
|
|
2457
2413
|
try {
|
|
2458
|
-
await
|
|
2414
|
+
await stat3(join12(input.vault, "SCHEMA.md"));
|
|
2459
2415
|
} catch {
|
|
2460
2416
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
2461
2417
|
}
|
|
2462
|
-
const logPath =
|
|
2418
|
+
const logPath = join12(input.vault, "log.md");
|
|
2463
2419
|
let logText;
|
|
2464
2420
|
try {
|
|
2465
2421
|
logText = await readFile10(logPath, "utf8");
|
|
@@ -2471,37 +2427,56 @@ async function runLogRotate(input) {
|
|
|
2471
2427
|
if (entries < input.threshold) {
|
|
2472
2428
|
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
|
|
2473
2429
|
}
|
|
2430
|
+
const newestYear = matches[matches.length - 1][1];
|
|
2431
|
+
const rotatedName = `log-${newestYear}.md`;
|
|
2432
|
+
const rotatedPath = join12(input.vault, rotatedName);
|
|
2433
|
+
const yearExists = await pathExists(rotatedPath);
|
|
2474
2434
|
if (!input.apply) {
|
|
2435
|
+
const hint = yearExists ? `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply); existing ${rotatedName} will be sidelined to log-archive-* first` : `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)`;
|
|
2475
2436
|
return {
|
|
2476
2437
|
exitCode: ExitCode.LOG_ROTATE_NEEDED,
|
|
2477
|
-
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint:
|
|
2438
|
+
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: hint })
|
|
2478
2439
|
};
|
|
2479
2440
|
}
|
|
2480
|
-
|
|
2481
|
-
const rotatedName = `log-${newestYear}.md`;
|
|
2482
|
-
const rotatedPath = join14(input.vault, rotatedName);
|
|
2441
|
+
let archivedExistingTo;
|
|
2483
2442
|
try {
|
|
2443
|
+
archivedExistingTo = await sidelineExistingYearLog(input.vault, rotatedPath, newestYear);
|
|
2484
2444
|
await rename2(logPath, rotatedPath);
|
|
2485
2445
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2446
|
+
const sidelineNote = archivedExistingTo ? `
|
|
2447
|
+
- Prior ${rotatedName} preserved as ${archivedExistingTo}
|
|
2448
|
+
` : "";
|
|
2486
2449
|
const fresh = `# Vault Log
|
|
2487
2450
|
|
|
2488
2451
|
Chronological action log. Newest entries last. Skill writes append entries; lint may rotate.
|
|
2489
2452
|
|
|
2490
2453
|
## [${today}] rotate | Log rotated from ${entries} entries
|
|
2491
2454
|
|
|
2492
|
-
- Previous log moved to ${rotatedName}
|
|
2493
|
-
`;
|
|
2455
|
+
- Previous log moved to ${rotatedName}${sidelineNote}`;
|
|
2494
2456
|
await writeFile2(logPath, fresh, "utf8");
|
|
2495
2457
|
} catch (e) {
|
|
2496
2458
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
2497
2459
|
}
|
|
2460
|
+
const files = ["log.md", rotatedName];
|
|
2461
|
+
if (archivedExistingTo) files.push(archivedExistingTo);
|
|
2462
|
+
const summary = archivedExistingTo ? `rotated ${entries} entries to ${rotatedName}; archived existing to ${archivedExistingTo}` : `rotated ${entries} entries to ${rotatedName}`;
|
|
2498
2463
|
appendLastOp(input.vault, {
|
|
2499
2464
|
operation: "log-rotate",
|
|
2500
|
-
summary
|
|
2501
|
-
files
|
|
2465
|
+
summary,
|
|
2466
|
+
files,
|
|
2502
2467
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2503
2468
|
});
|
|
2504
|
-
return {
|
|
2469
|
+
return {
|
|
2470
|
+
exitCode: ExitCode.OK,
|
|
2471
|
+
result: ok({
|
|
2472
|
+
entries,
|
|
2473
|
+
threshold: input.threshold,
|
|
2474
|
+
rotated: true,
|
|
2475
|
+
rotated_to: rotatedName,
|
|
2476
|
+
archived_existing_to: archivedExistingTo,
|
|
2477
|
+
humanHint: summary
|
|
2478
|
+
})
|
|
2479
|
+
};
|
|
2505
2480
|
}
|
|
2506
2481
|
|
|
2507
2482
|
// src/commands/topic-map-check.ts
|
|
@@ -2525,12 +2500,12 @@ async function runTopicMapCheck(input) {
|
|
|
2525
2500
|
|
|
2526
2501
|
// src/commands/index-link-format.ts
|
|
2527
2502
|
import { readFile as readFile11 } from "fs/promises";
|
|
2528
|
-
import { join as
|
|
2503
|
+
import { join as join13 } from "path";
|
|
2529
2504
|
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
2530
2505
|
async function runIndexLinkFormat(input) {
|
|
2531
2506
|
let text = "";
|
|
2532
2507
|
try {
|
|
2533
|
-
text = await readFile11(
|
|
2508
|
+
text = await readFile11(join13(input.vault, "index.md"), "utf8");
|
|
2534
2509
|
} catch {
|
|
2535
2510
|
}
|
|
2536
2511
|
const markdown_links = [];
|
|
@@ -2543,9 +2518,9 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
|
2543
2518
|
}
|
|
2544
2519
|
|
|
2545
2520
|
// src/commands/dedup.ts
|
|
2546
|
-
import { createHash as
|
|
2547
|
-
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4
|
|
2548
|
-
import { dirname as
|
|
2521
|
+
import { createHash as createHash5 } from "crypto";
|
|
2522
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2523
|
+
import { dirname as dirname5, join as join14, posix, resolve as resolve3 } from "path";
|
|
2549
2524
|
|
|
2550
2525
|
// src/utils/rclone.ts
|
|
2551
2526
|
import { execFile } from "child_process";
|
|
@@ -2594,6 +2569,140 @@ async function defaultRcloneRunner(args) {
|
|
|
2594
2569
|
}
|
|
2595
2570
|
}
|
|
2596
2571
|
|
|
2572
|
+
// src/utils/safe-write.ts
|
|
2573
|
+
import { readFile as readFile12, writeFile as writeFile3 } from "fs/promises";
|
|
2574
|
+
var DEFAULT_MIN_BODY_RATIO = 0.5;
|
|
2575
|
+
var DEFAULT_MIN_OLD_BODY_BYTES = 200;
|
|
2576
|
+
function bodyBytes(text) {
|
|
2577
|
+
const split = splitFrontmatter(text);
|
|
2578
|
+
if (!split.ok) return Buffer.byteLength(text, "utf8");
|
|
2579
|
+
return Buffer.byteLength(split.data.body, "utf8");
|
|
2580
|
+
}
|
|
2581
|
+
async function readIfExists(absPath) {
|
|
2582
|
+
try {
|
|
2583
|
+
return await readFile12(absPath, "utf8");
|
|
2584
|
+
} catch (e) {
|
|
2585
|
+
if (e.code === "ENOENT") return null;
|
|
2586
|
+
throw e;
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
async function safeWritePage(absPath, newContent, opts = {}) {
|
|
2590
|
+
const minRatio = opts.minBodyRatio === void 0 ? DEFAULT_MIN_BODY_RATIO : opts.minBodyRatio;
|
|
2591
|
+
const minOldBytes = opts.minOldBodyBytes ?? DEFAULT_MIN_OLD_BODY_BYTES;
|
|
2592
|
+
let oldContent;
|
|
2593
|
+
try {
|
|
2594
|
+
oldContent = await readIfExists(absPath);
|
|
2595
|
+
} catch (e) {
|
|
2596
|
+
return err("WRITE_FAILED", { path: absPath, phase: "read-existing", message: String(e) });
|
|
2597
|
+
}
|
|
2598
|
+
const isNew = oldContent === null;
|
|
2599
|
+
const oldBodyBytes = isNew ? 0 : bodyBytes(oldContent);
|
|
2600
|
+
const newBodyBytes = bodyBytes(newContent);
|
|
2601
|
+
const bodyRatio = oldBodyBytes > 0 ? newBodyBytes / oldBodyBytes : null;
|
|
2602
|
+
let guardSkippedSmall = false;
|
|
2603
|
+
if (!isNew && minRatio !== null && bodyRatio !== null && bodyRatio < minRatio) {
|
|
2604
|
+
if (oldBodyBytes < minOldBytes) {
|
|
2605
|
+
guardSkippedSmall = true;
|
|
2606
|
+
} else {
|
|
2607
|
+
return err("BODY_TRUNCATION_GUARD", {
|
|
2608
|
+
path: absPath,
|
|
2609
|
+
oldBodyBytes,
|
|
2610
|
+
newBodyBytes,
|
|
2611
|
+
bodyRatio,
|
|
2612
|
+
minBodyRatio: minRatio,
|
|
2613
|
+
hint: "Refusing to write \u2014 new body lost too much content. Likely a parse-modify-serialize bug or a write race. Verify the page source before retrying."
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
const written = await atomicWriteText(absPath, newContent);
|
|
2618
|
+
if (!written.ok) return written;
|
|
2619
|
+
return ok({
|
|
2620
|
+
changed: written.data.changed,
|
|
2621
|
+
isNew,
|
|
2622
|
+
oldBodyBytes,
|
|
2623
|
+
newBodyBytes,
|
|
2624
|
+
bodyRatio,
|
|
2625
|
+
guardSkippedSmall
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// src/utils/raw-reference-rewrite.ts
|
|
2630
|
+
function withoutMarkdownExtension(path) {
|
|
2631
|
+
return path.endsWith(".md") ? path.slice(0, -3) : path;
|
|
2632
|
+
}
|
|
2633
|
+
function replacementFor(value, oldPath, newPath) {
|
|
2634
|
+
const oldNoExtension = withoutMarkdownExtension(oldPath);
|
|
2635
|
+
const newNoExtension = withoutMarkdownExtension(newPath);
|
|
2636
|
+
if (value === oldPath) return newPath;
|
|
2637
|
+
if (value === oldNoExtension) return newNoExtension;
|
|
2638
|
+
if (value === `^[${oldPath}]`) return `^[${newPath}]`;
|
|
2639
|
+
if (value === `^[${oldNoExtension}]`) return `^[${newNoExtension}]`;
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
function renderSources(values) {
|
|
2643
|
+
if (values.length === 0) return "sources: []";
|
|
2644
|
+
return `sources:
|
|
2645
|
+
${values.map((value) => ` - ${value}`).join("\n")}`;
|
|
2646
|
+
}
|
|
2647
|
+
function rewriteRawSourceReferences(text, oldPath, newPath) {
|
|
2648
|
+
const split = splitFrontmatter(text);
|
|
2649
|
+
const oldNoExtension = withoutMarkdownExtension(oldPath);
|
|
2650
|
+
const newNoExtension = withoutMarkdownExtension(newPath);
|
|
2651
|
+
const body = split.ok ? split.data.body : text;
|
|
2652
|
+
let rewrittenBody = body;
|
|
2653
|
+
let bodyCitationCount = 0;
|
|
2654
|
+
for (const [oldMarker, newMarker] of [
|
|
2655
|
+
[`^[${oldPath}]`, `^[${newPath}]`],
|
|
2656
|
+
[`^[${oldNoExtension}]`, `^[${newNoExtension}]`]
|
|
2657
|
+
]) {
|
|
2658
|
+
const count = rewrittenBody.split(oldMarker).length - 1;
|
|
2659
|
+
if (count > 0) {
|
|
2660
|
+
rewrittenBody = rewrittenBody.replaceAll(oldMarker, newMarker);
|
|
2661
|
+
bodyCitationCount += count;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (!split.ok) {
|
|
2665
|
+
return { text: rewrittenBody, changed: rewrittenBody !== text, sourcesBefore: [], sourcesAfter: [], bodyCitationCount };
|
|
2666
|
+
}
|
|
2667
|
+
const frontmatter = extractFrontmatter(text);
|
|
2668
|
+
const sourcesBefore = frontmatter.ok && Array.isArray(frontmatter.data.sources) ? frontmatter.data.sources.filter((value) => typeof value === "string") : [];
|
|
2669
|
+
const sourcesAfter = sourcesBefore.map((value) => replacementFor(value, oldPath, newPath) ?? value);
|
|
2670
|
+
const sourcesChanged = sourcesBefore.some((value, index) => value !== sourcesAfter[index]);
|
|
2671
|
+
let rewrittenFrontmatter = split.data.rawFrontmatter;
|
|
2672
|
+
if (sourcesChanged) {
|
|
2673
|
+
rewrittenFrontmatter = rewrittenFrontmatter.replace(
|
|
2674
|
+
/^sources:\s*(?:\[[^\]]*\]|(?:\r?\n(?:\s*-\s.*))+)/m,
|
|
2675
|
+
renderSources(sourcesAfter)
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
if (!sourcesChanged && rewrittenBody === body) {
|
|
2679
|
+
return { text, changed: false, sourcesBefore, sourcesAfter, bodyCitationCount };
|
|
2680
|
+
}
|
|
2681
|
+
const rewritten = `---
|
|
2682
|
+
${rewrittenFrontmatter}
|
|
2683
|
+
---${rewrittenBody}`;
|
|
2684
|
+
return {
|
|
2685
|
+
text: rewritten,
|
|
2686
|
+
changed: rewritten !== text,
|
|
2687
|
+
sourcesBefore,
|
|
2688
|
+
sourcesAfter,
|
|
2689
|
+
bodyCitationCount
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// src/utils/maintained-page-state.ts
|
|
2694
|
+
import { createHash as createHash4 } from "crypto";
|
|
2695
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2696
|
+
async function snapshotMaintainedPageState(pages) {
|
|
2697
|
+
const entries = await Promise.all(
|
|
2698
|
+
pages.filter((page) => !page.relPath.startsWith("raw/")).map(async (page) => {
|
|
2699
|
+
const bytes = await readFile13(page.absPath);
|
|
2700
|
+
return `${page.relPath}:${createHash4("sha256").update(bytes).digest("hex")}`;
|
|
2701
|
+
})
|
|
2702
|
+
);
|
|
2703
|
+
return entries.sort();
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2597
2706
|
// src/commands/dedup.ts
|
|
2598
2707
|
async function runDedup(input) {
|
|
2599
2708
|
if (input.canonicalPolicy && input.canonicalPolicy !== "scan-order" && input.canonicalPolicy !== "stable-path") {
|
|
@@ -2614,7 +2723,11 @@ async function runDedup(input) {
|
|
|
2614
2723
|
}
|
|
2615
2724
|
const hashMap = /* @__PURE__ */ new Map();
|
|
2616
2725
|
let totalFiles = 0;
|
|
2617
|
-
const
|
|
2726
|
+
const activeRaw = scan.raw.filter((raw) => {
|
|
2727
|
+
const classified = classifyRawPath(raw.relPath);
|
|
2728
|
+
return classified.ok && classified.data.storage === "active" && classified.data.category !== "assets";
|
|
2729
|
+
});
|
|
2730
|
+
const rawEntries = await mapWithConcurrency(activeRaw, vaultIoConcurrency(), async (raw) => {
|
|
2618
2731
|
const fm = extractFrontmatter(await readPageCached(raw, input.pageTextCache));
|
|
2619
2732
|
if (!fm.ok) return null;
|
|
2620
2733
|
const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
@@ -2629,77 +2742,101 @@ async function runDedup(input) {
|
|
|
2629
2742
|
else hashMap.set(entry.sha, [entry.relPath]);
|
|
2630
2743
|
}
|
|
2631
2744
|
const canonicalPolicy = input.canonicalPolicy ?? "stable-path";
|
|
2632
|
-
const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([
|
|
2745
|
+
const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha2562, files]) => ({ sha256: sha2562, files: orderFiles(files, canonicalPolicy) }));
|
|
2633
2746
|
const rewired = [];
|
|
2634
2747
|
const removed = [];
|
|
2748
|
+
const relocated = [];
|
|
2635
2749
|
const unsafe = [];
|
|
2636
2750
|
const safeEntries = manifestFromFile?.ok ? manifestFromFile.data.entries : buildSafeEntries(input.vault, duplicates, unsafe);
|
|
2751
|
+
if (manifestFromFile?.ok) {
|
|
2752
|
+
const validatedManifest = validateManifestLocalState(input.vault, manifestFromFile.data);
|
|
2753
|
+
if (!validatedManifest.ok) return { exitCode: ExitCode.USAGE, result: validatedManifest };
|
|
2754
|
+
}
|
|
2637
2755
|
const manifest = safeEntries.length > 0 ? {
|
|
2638
2756
|
version: 1,
|
|
2639
2757
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2640
2758
|
vault: resolve3(input.vault),
|
|
2641
2759
|
entries: safeEntries
|
|
2642
2760
|
} : void 0;
|
|
2643
|
-
const
|
|
2644
|
-
if (!
|
|
2645
|
-
return {
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2761
|
+
const remoteDeleteCount = safeEntries.reduce((count, entry) => count + entry.duplicates.length, 0);
|
|
2762
|
+
if (input.remoteDelete && (!Number.isInteger(input.maxRemoteDeletes) || (input.maxRemoteDeletes ?? 0) < remoteDeleteCount)) {
|
|
2763
|
+
return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: `planned remote deletes ${remoteDeleteCount} exceed --max-remote-deletes ${input.maxRemoteDeletes ?? 0}` }) };
|
|
2764
|
+
}
|
|
2765
|
+
const structuralPlans = [];
|
|
2766
|
+
for (const entry of safeEntries) {
|
|
2767
|
+
for (const duplicate of entry.duplicates) {
|
|
2768
|
+
if (!existsSync4(join14(input.vault, duplicate))) continue;
|
|
2769
|
+
const destination = dedupDestination(input.vault, duplicate);
|
|
2770
|
+
const plan = await planRawStructuralMove({ vault: input.vault, operation: "deduplicate", source: duplicate, destination });
|
|
2771
|
+
if (!plan.ok) return { exitCode: ExitCode.WRITE_FAILED, result: plan };
|
|
2772
|
+
structuralPlans.push({ source: duplicate, destination, canonical: entry.canonical, approval: plan.data.approval_token, source_sha256: plan.data.source_sha256 });
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
const citationState = await snapshotMaintainedPageState(scan.allMarkdown);
|
|
2776
|
+
const approvalToken = structuralPlans.length > 0 ? operationId("raw-dedup-approval", [
|
|
2777
|
+
...structuralPlans.flatMap((plan) => [plan.source, plan.destination, plan.source_sha256]),
|
|
2778
|
+
...citationState
|
|
2779
|
+
]) : void 0;
|
|
2780
|
+
if (input.apply && structuralPlans.length > 0 && input.approve !== approvalToken) {
|
|
2781
|
+
return { exitCode: ExitCode.USAGE, result: err("APPROVAL_INVALID", { message: "dedup apply requires the live dry-run --approve token", approval_token: approvalToken }) };
|
|
2649
2782
|
}
|
|
2650
2783
|
if (input.manifestOut && manifest) {
|
|
2651
2784
|
try {
|
|
2652
|
-
mkdirSync4(
|
|
2785
|
+
mkdirSync4(dirname5(input.manifestOut), { recursive: true });
|
|
2653
2786
|
writeFileSync4(input.manifestOut, `${JSON.stringify(manifest, null, 2)}
|
|
2654
2787
|
`, "utf-8");
|
|
2655
2788
|
} catch (e) {
|
|
2656
2789
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: input.manifestOut, message: String(e) }) };
|
|
2657
2790
|
}
|
|
2658
2791
|
}
|
|
2659
|
-
if (input.apply &&
|
|
2792
|
+
if (input.apply && structuralPlans.length > 0) {
|
|
2660
2793
|
const replacements = /* @__PURE__ */ new Map();
|
|
2661
|
-
for (const
|
|
2662
|
-
|
|
2663
|
-
replacements.set(duplicate, entry.canonical);
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2794
|
+
for (const plan of structuralPlans) replacements.set(plan.source, plan.canonical);
|
|
2795
|
+
const pendingWrites = [];
|
|
2666
2796
|
for (const page of scan.allMarkdown.filter((p) => !p.relPath.startsWith("raw/"))) {
|
|
2667
|
-
const text = readFileSync4(
|
|
2797
|
+
const text = readFileSync4(join14(input.vault, page.relPath), "utf-8");
|
|
2668
2798
|
let updated = text;
|
|
2669
2799
|
let changed = false;
|
|
2670
2800
|
for (const [oldPath, newPath] of replacements) {
|
|
2671
|
-
const
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
updated = updated.replaceAll(oldMarker, newMarker);
|
|
2675
|
-
changed = true;
|
|
2676
|
-
}
|
|
2677
|
-
const oldFm = `- "^[${oldPath}]"`;
|
|
2678
|
-
const newFm = `- "^[${newPath}]"`;
|
|
2679
|
-
if (updated.includes(oldFm)) {
|
|
2680
|
-
updated = updated.replaceAll(oldFm, newFm);
|
|
2681
|
-
changed = true;
|
|
2682
|
-
}
|
|
2801
|
+
const rewritten = rewriteRawSourceReferences(updated, oldPath, newPath);
|
|
2802
|
+
updated = rewritten.text;
|
|
2803
|
+
changed = changed || rewritten.changed;
|
|
2683
2804
|
}
|
|
2684
2805
|
if (changed) {
|
|
2685
|
-
|
|
2806
|
+
pendingWrites.push({ page: page.relPath, text: updated });
|
|
2686
2807
|
rewired.push(page.relPath);
|
|
2687
2808
|
}
|
|
2688
2809
|
}
|
|
2689
|
-
for (const
|
|
2690
|
-
const
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2810
|
+
for (const plan of structuralPlans) {
|
|
2811
|
+
const moved = await applyRawStructuralMove({
|
|
2812
|
+
vault: input.vault,
|
|
2813
|
+
operation: "deduplicate",
|
|
2814
|
+
source: plan.source,
|
|
2815
|
+
destination: plan.destination,
|
|
2816
|
+
approve: plan.approval,
|
|
2817
|
+
command: "skillwiki dedup --apply",
|
|
2818
|
+
citationChanges: pendingWrites.map((write) => write.page)
|
|
2819
|
+
});
|
|
2820
|
+
if (!moved.ok) return { exitCode: ExitCode.WRITE_FAILED, result: moved };
|
|
2821
|
+
relocated.push({ from: plan.source, to: plan.destination });
|
|
2822
|
+
}
|
|
2823
|
+
for (const pending of pendingWrites) {
|
|
2824
|
+
const write = await safeWritePage(join14(input.vault, pending.page), pending.text);
|
|
2825
|
+
if (!write.ok) return { exitCode: ExitCode.WRITE_FAILED, result: write };
|
|
2696
2826
|
}
|
|
2697
2827
|
}
|
|
2698
|
-
|
|
2828
|
+
const remote = await planAndMaybePruneRemote({ ...input, remoteDelete: input.apply || input.manifestIn ? input.remoteDelete : false }, safeEntries);
|
|
2829
|
+
if (!remote.ok) {
|
|
2830
|
+
return {
|
|
2831
|
+
exitCode: remote.error === "SYNC_PUSH_FAILED" ? ExitCode.SYNC_PUSH_FAILED : ExitCode.USAGE,
|
|
2832
|
+
result: remote
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
if (input.apply && (rewired.length > 0 || relocated.length > 0)) {
|
|
2699
2836
|
appendLastOp(input.vault, {
|
|
2700
2837
|
operation: "dedup",
|
|
2701
|
-
summary: `rewired ${rewired.length} pages,
|
|
2702
|
-
files: [...rewired, ...
|
|
2838
|
+
summary: `rewired ${rewired.length} pages, preserved ${relocated.length} duplicates under raw/duplicates`,
|
|
2839
|
+
files: [...rewired, ...relocated.flatMap((move) => [move.from, move.to])],
|
|
2703
2840
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2704
2841
|
});
|
|
2705
2842
|
}
|
|
@@ -2710,7 +2847,7 @@ async function runDedup(input) {
|
|
|
2710
2847
|
for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
|
|
2711
2848
|
if (input.apply) {
|
|
2712
2849
|
hintLines.push(`rewired: ${rewired.length} pages`);
|
|
2713
|
-
hintLines.push(`
|
|
2850
|
+
hintLines.push(`preserved duplicates: ${relocated.length} raw files`);
|
|
2714
2851
|
}
|
|
2715
2852
|
if (unsafe.length > 0) hintLines.push(`unsafe: ${unsafe.length} groups skipped`);
|
|
2716
2853
|
if (remote.data.plannedDeletes.length > 0) {
|
|
@@ -2730,10 +2867,21 @@ async function runDedup(input) {
|
|
|
2730
2867
|
remote: remote.data,
|
|
2731
2868
|
rewired,
|
|
2732
2869
|
removed,
|
|
2870
|
+
relocated,
|
|
2871
|
+
...approvalToken ? { approval_token: approvalToken } : {},
|
|
2733
2872
|
humanHint: hintLines.join("\n")
|
|
2734
2873
|
})
|
|
2735
2874
|
};
|
|
2736
2875
|
}
|
|
2876
|
+
function dedupDestination(vault, source) {
|
|
2877
|
+
const base = lifecycleDestination(source, "deduplicate");
|
|
2878
|
+
if (!base.ok) throw new Error(`unsupported raw dedup path: ${source}`);
|
|
2879
|
+
if (!existsSync4(join14(vault, base.data))) return base.data;
|
|
2880
|
+
const contentHash = createHash5("sha256").update(readFileSync4(join14(vault, source))).digest("hex").slice(0, 8);
|
|
2881
|
+
const ext = posix.extname(base.data);
|
|
2882
|
+
const stem = ext ? base.data.slice(0, -ext.length) : base.data;
|
|
2883
|
+
return `${stem}--${contentHash}${ext}`;
|
|
2884
|
+
}
|
|
2737
2885
|
function orderFiles(files, policy) {
|
|
2738
2886
|
if (policy === "scan-order") return [...files];
|
|
2739
2887
|
return [...files].sort(compareStableRawPath);
|
|
@@ -2781,10 +2929,13 @@ function buildSafeEntries(vault, duplicates, unsafe) {
|
|
|
2781
2929
|
return entries;
|
|
2782
2930
|
}
|
|
2783
2931
|
function hashRawBody(vault, relPath) {
|
|
2784
|
-
const text = readFileSync4(
|
|
2932
|
+
const text = readFileSync4(join14(vault, relPath), "utf-8");
|
|
2933
|
+
return hashRawBodyText(text);
|
|
2934
|
+
}
|
|
2935
|
+
function hashRawBodyText(text) {
|
|
2785
2936
|
const split = splitFrontmatter(text);
|
|
2786
2937
|
const body = split.ok ? split.data.body : text;
|
|
2787
|
-
return
|
|
2938
|
+
return createHash5("sha256").update(body).digest("hex");
|
|
2788
2939
|
}
|
|
2789
2940
|
function readManifest(path) {
|
|
2790
2941
|
try {
|
|
@@ -2797,69 +2948,50 @@ function readManifest(path) {
|
|
|
2797
2948
|
return err("INVALID_FRONTMATTER", { path, message: String(e) });
|
|
2798
2949
|
}
|
|
2799
2950
|
}
|
|
2951
|
+
function validateManifestLocalState(vault, manifest) {
|
|
2952
|
+
if (resolve3(manifest.vault) !== resolve3(vault)) {
|
|
2953
|
+
return err("APPROVAL_INVALID", {
|
|
2954
|
+
message: "dedup manifest belongs to a different vault",
|
|
2955
|
+
manifest_vault: manifest.vault,
|
|
2956
|
+
vault: resolve3(vault)
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
for (const entry of manifest.entries) {
|
|
2960
|
+
if (!/^[a-f0-9]{64}$/i.test(entry.sha256) || !/^[a-f0-9]{64}$/i.test(entry.bodyHash)) {
|
|
2961
|
+
return err("APPROVAL_INVALID", { message: "dedup manifest contains an invalid hash", canonical: entry.canonical });
|
|
2962
|
+
}
|
|
2963
|
+
const paths = [entry.canonical, ...entry.duplicates];
|
|
2964
|
+
if (new Set(paths).size !== paths.length) {
|
|
2965
|
+
return err("APPROVAL_INVALID", { message: "dedup manifest contains repeated or self-duplicate paths", canonical: entry.canonical });
|
|
2966
|
+
}
|
|
2967
|
+
for (const path of paths) {
|
|
2968
|
+
const classified = typeof path === "string" ? classifyRawPath(path) : err("RAW_PATH_INVALID", { path });
|
|
2969
|
+
if (!classified.ok || classified.data.path !== path || classified.data.storage !== "active" || classified.data.category === "assets") {
|
|
2970
|
+
return err("APPROVAL_INVALID", { message: "dedup manifest contains an invalid active raw source path", path });
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
const canonicalExists = existsSync4(join14(vault, entry.canonical));
|
|
2974
|
+
const liveDuplicates = entry.duplicates.filter((path) => existsSync4(join14(vault, path)));
|
|
2975
|
+
if (!canonicalExists && liveDuplicates.length > 0) {
|
|
2976
|
+
return err("APPROVAL_INVALID", { message: "dedup manifest canonical is missing while a local duplicate exists", canonical: entry.canonical });
|
|
2977
|
+
}
|
|
2978
|
+
for (const path of canonicalExists ? [entry.canonical, ...liveDuplicates] : liveDuplicates) {
|
|
2979
|
+
const text = readFileSync4(join14(vault, path), "utf8");
|
|
2980
|
+
const fm = extractFrontmatter(text);
|
|
2981
|
+
const declaredSha = fm.ok && typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
2982
|
+
if (declaredSha !== entry.sha256 || hashRawBodyText(text) !== entry.bodyHash) {
|
|
2983
|
+
return err("APPROVAL_INVALID", { message: "dedup manifest does not match live local raw state", path });
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return ok(true);
|
|
2988
|
+
}
|
|
2800
2989
|
async function planAndMaybePruneRemote(input, entries) {
|
|
2801
2990
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
2802
2991
|
const plannedDeletes = remoteRoot ? entries.flatMap((entry) => entry.duplicates.map((path) => `${remoteRoot}/${path}`)) : [];
|
|
2803
2992
|
return planAndMaybePruneRemoteObjects(plannedDeletes, { ...input, defaultMaxDeletes: 50 });
|
|
2804
2993
|
}
|
|
2805
2994
|
|
|
2806
|
-
// src/utils/safe-write.ts
|
|
2807
|
-
import { readFile as readFile12, writeFile as writeFile3 } from "fs/promises";
|
|
2808
|
-
var DEFAULT_MIN_BODY_RATIO = 0.5;
|
|
2809
|
-
var DEFAULT_MIN_OLD_BODY_BYTES = 200;
|
|
2810
|
-
function bodyBytes(text) {
|
|
2811
|
-
const split = splitFrontmatter(text);
|
|
2812
|
-
if (!split.ok) return Buffer.byteLength(text, "utf8");
|
|
2813
|
-
return Buffer.byteLength(split.data.body, "utf8");
|
|
2814
|
-
}
|
|
2815
|
-
async function readIfExists(absPath) {
|
|
2816
|
-
try {
|
|
2817
|
-
return await readFile12(absPath, "utf8");
|
|
2818
|
-
} catch (e) {
|
|
2819
|
-
if (e.code === "ENOENT") return null;
|
|
2820
|
-
throw e;
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
async function safeWritePage(absPath, newContent, opts = {}) {
|
|
2824
|
-
const minRatio = opts.minBodyRatio === void 0 ? DEFAULT_MIN_BODY_RATIO : opts.minBodyRatio;
|
|
2825
|
-
const minOldBytes = opts.minOldBodyBytes ?? DEFAULT_MIN_OLD_BODY_BYTES;
|
|
2826
|
-
let oldContent;
|
|
2827
|
-
try {
|
|
2828
|
-
oldContent = await readIfExists(absPath);
|
|
2829
|
-
} catch (e) {
|
|
2830
|
-
return err("WRITE_FAILED", { path: absPath, phase: "read-existing", message: String(e) });
|
|
2831
|
-
}
|
|
2832
|
-
const isNew = oldContent === null;
|
|
2833
|
-
const oldBodyBytes = isNew ? 0 : bodyBytes(oldContent);
|
|
2834
|
-
const newBodyBytes = bodyBytes(newContent);
|
|
2835
|
-
const bodyRatio = oldBodyBytes > 0 ? newBodyBytes / oldBodyBytes : null;
|
|
2836
|
-
let guardSkippedSmall = false;
|
|
2837
|
-
if (!isNew && minRatio !== null && bodyRatio !== null && bodyRatio < minRatio) {
|
|
2838
|
-
if (oldBodyBytes < minOldBytes) {
|
|
2839
|
-
guardSkippedSmall = true;
|
|
2840
|
-
} else {
|
|
2841
|
-
return err("BODY_TRUNCATION_GUARD", {
|
|
2842
|
-
path: absPath,
|
|
2843
|
-
oldBodyBytes,
|
|
2844
|
-
newBodyBytes,
|
|
2845
|
-
bodyRatio,
|
|
2846
|
-
minBodyRatio: minRatio,
|
|
2847
|
-
hint: "Refusing to write \u2014 new body lost too much content. Likely a parse-modify-serialize bug or a write race. Verify the page source before retrying."
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
const written = await atomicWriteText(absPath, newContent);
|
|
2852
|
-
if (!written.ok) return written;
|
|
2853
|
-
return ok({
|
|
2854
|
-
changed: written.data.changed,
|
|
2855
|
-
isNew,
|
|
2856
|
-
oldBodyBytes,
|
|
2857
|
-
newBodyBytes,
|
|
2858
|
-
bodyRatio,
|
|
2859
|
-
guardSkippedSmall
|
|
2860
|
-
});
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
2995
|
// src/commands/frontmatter-fix.ts
|
|
2864
2996
|
function isoToday() {
|
|
2865
2997
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -2938,9 +3070,8 @@ ${newBody}`;
|
|
|
2938
3070
|
|
|
2939
3071
|
// src/commands/lint.ts
|
|
2940
3072
|
import { existsSync as existsSync6 } from "fs";
|
|
2941
|
-
import { readFile as
|
|
2942
|
-
import {
|
|
2943
|
-
import { join as join18, relative as relative2, sep as sep2 } from "path";
|
|
3073
|
+
import { readFile as readFile15, readdir as readdir3 } from "fs/promises";
|
|
3074
|
+
import { join as join16, relative as relative2, sep as sep2 } from "path";
|
|
2944
3075
|
|
|
2945
3076
|
// src/commands/sparse-community.ts
|
|
2946
3077
|
async function runSparseCommunity(input) {
|
|
@@ -2956,7 +3087,7 @@ async function runSparseCommunity(input) {
|
|
|
2956
3087
|
}
|
|
2957
3088
|
|
|
2958
3089
|
// src/commands/raw-body-dedup.ts
|
|
2959
|
-
import { createHash as
|
|
3090
|
+
import { createHash as createHash6 } from "crypto";
|
|
2960
3091
|
async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
2961
3092
|
const scanResult = scan ? ok(scan) : await scanVault(vault);
|
|
2962
3093
|
if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
|
|
@@ -2965,7 +3096,7 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
|
2965
3096
|
const text = await readPageCached(raw, pageTextCache);
|
|
2966
3097
|
const split = splitFrontmatter(text);
|
|
2967
3098
|
if (!split.ok) return null;
|
|
2968
|
-
const bodyHash =
|
|
3099
|
+
const bodyHash = createHash6("sha256").update(split.data.body).digest("hex");
|
|
2969
3100
|
const fm = extractFrontmatter(text);
|
|
2970
3101
|
let fmSha256 = null;
|
|
2971
3102
|
if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
|
|
@@ -2998,8 +3129,8 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
|
2998
3129
|
|
|
2999
3130
|
// src/commands/path-too-long.ts
|
|
3000
3131
|
import { existsSync as existsSync5 } from "fs";
|
|
3001
|
-
import { mkdir as mkdir5, readFile as
|
|
3002
|
-
import { dirname as
|
|
3132
|
+
import { mkdir as mkdir5, readFile as readFile14, rename as rename3, unlink as unlink2 } from "fs/promises";
|
|
3133
|
+
import { dirname as dirname6, join as join15, posix as posix2, resolve as resolve4 } from "path";
|
|
3003
3134
|
var MAX_PATH_LENGTH = 240;
|
|
3004
3135
|
var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
|
|
3005
3136
|
async function runPathTooLong(input) {
|
|
@@ -3025,6 +3156,10 @@ async function fixPathTooLong(input) {
|
|
|
3025
3156
|
const fixed = [];
|
|
3026
3157
|
const unresolved = [];
|
|
3027
3158
|
for (const violation of violations) {
|
|
3159
|
+
if (violation.relPath.startsWith("raw/")) {
|
|
3160
|
+
unresolved.push(violation.relPath);
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3028
3163
|
const target = await resolveFixTarget(input.vault, violation.relPath, violation.suggestedRelPath, maxFixLength);
|
|
3029
3164
|
if (!target || target.relPath === violation.relPath || target.relPath.length > maxFixLength) {
|
|
3030
3165
|
unresolved.push(violation.relPath);
|
|
@@ -3032,10 +3167,10 @@ async function fixPathTooLong(input) {
|
|
|
3032
3167
|
}
|
|
3033
3168
|
try {
|
|
3034
3169
|
if (target.mode === "dedupe") {
|
|
3035
|
-
await
|
|
3170
|
+
await unlink2(join15(input.vault, violation.relPath));
|
|
3036
3171
|
} else {
|
|
3037
|
-
await mkdir5(
|
|
3038
|
-
await rename3(
|
|
3172
|
+
await mkdir5(dirname6(join15(input.vault, target.relPath)), { recursive: true });
|
|
3173
|
+
await rename3(join15(input.vault, violation.relPath), join15(input.vault, target.relPath));
|
|
3039
3174
|
}
|
|
3040
3175
|
fixed.push({ from: violation.relPath, to: target.relPath });
|
|
3041
3176
|
} catch {
|
|
@@ -3049,7 +3184,7 @@ async function fixPathTooLong(input) {
|
|
|
3049
3184
|
for (const page of afterScan.data.allMarkdown) {
|
|
3050
3185
|
if (!shouldRewriteReferences(page.relPath)) continue;
|
|
3051
3186
|
try {
|
|
3052
|
-
const original = await
|
|
3187
|
+
const original = await readFile14(page.absPath, "utf8");
|
|
3053
3188
|
let updated = original;
|
|
3054
3189
|
for (const fix of fixed) {
|
|
3055
3190
|
updated = replacePathReferences(updated, fix.from, fix.to);
|
|
@@ -3112,9 +3247,9 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
|
|
|
3112
3247
|
async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
3113
3248
|
for (const candidate of candidateRelPaths(preferred, maxLength)) {
|
|
3114
3249
|
if (candidate === original || candidate.length > maxLength) continue;
|
|
3115
|
-
const candidatePath =
|
|
3250
|
+
const candidatePath = join15(vault, candidate);
|
|
3116
3251
|
if (!existsSync5(candidatePath)) return { relPath: candidate, mode: "rename" };
|
|
3117
|
-
if (await hasSameContent(
|
|
3252
|
+
if (await hasSameContent(join15(vault, original), candidatePath)) {
|
|
3118
3253
|
return { relPath: candidate, mode: "dedupe" };
|
|
3119
3254
|
}
|
|
3120
3255
|
}
|
|
@@ -3123,8 +3258,8 @@ async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
|
3123
3258
|
function candidateRelPaths(preferred, maxLength) {
|
|
3124
3259
|
const candidates = [preferred];
|
|
3125
3260
|
if (preferred.length > maxLength) return candidates;
|
|
3126
|
-
const dir =
|
|
3127
|
-
const filename =
|
|
3261
|
+
const dir = posix2.dirname(preferred) === "." ? "" : posix2.dirname(preferred);
|
|
3262
|
+
const filename = posix2.basename(preferred);
|
|
3128
3263
|
const ext = filename.endsWith(".md") ? ".md" : "";
|
|
3129
3264
|
const base = ext ? filename.slice(0, -3) : filename;
|
|
3130
3265
|
const dirPrefix = dir ? `${dir}/` : "";
|
|
@@ -3138,7 +3273,7 @@ function candidateRelPaths(preferred, maxLength) {
|
|
|
3138
3273
|
}
|
|
3139
3274
|
async function hasSameContent(a, b) {
|
|
3140
3275
|
try {
|
|
3141
|
-
const [left, right] = await Promise.all([
|
|
3276
|
+
const [left, right] = await Promise.all([readFile14(a), readFile14(b)]);
|
|
3142
3277
|
return left.equals(right);
|
|
3143
3278
|
} catch {
|
|
3144
3279
|
return false;
|
|
@@ -3151,8 +3286,8 @@ function shouldRewriteReferences(relPath) {
|
|
|
3151
3286
|
}
|
|
3152
3287
|
function replacePathReferences(content, oldRelPath, newRelPath) {
|
|
3153
3288
|
let updated = content.replaceAll(oldRelPath, newRelPath);
|
|
3154
|
-
const oldStem =
|
|
3155
|
-
const newStem =
|
|
3289
|
+
const oldStem = posix2.basename(oldRelPath).replace(/\.md$/, "");
|
|
3290
|
+
const newStem = posix2.basename(newRelPath).replace(/\.md$/, "");
|
|
3156
3291
|
if (oldStem !== newStem) {
|
|
3157
3292
|
const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3158
3293
|
const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
@@ -3181,7 +3316,8 @@ function buildCliSurface() {
|
|
|
3181
3316
|
program.command("graph");
|
|
3182
3317
|
program.command("canvas");
|
|
3183
3318
|
program.command("overlap").option("--wiki <name>");
|
|
3184
|
-
program.command("query").option("--limit <n>").option("--wiki <name>");
|
|
3319
|
+
program.command("query").option("--limit <n>").option("--include-pending").option("--wiki <name>");
|
|
3320
|
+
program.command("sources");
|
|
3185
3321
|
program.command("orphans").option("--wiki <name>");
|
|
3186
3322
|
program.command("audit");
|
|
3187
3323
|
program.command("install").option("--target <dir>").option("--dry-run").option("--skills-root <dir>").option("--symlink");
|
|
@@ -3193,7 +3329,7 @@ function buildCliSurface() {
|
|
|
3193
3329
|
program.command("index-check").option("--wiki <name>");
|
|
3194
3330
|
program.command("index-link-format").option("--wiki <name>");
|
|
3195
3331
|
program.command("topic-map-check").option("--threshold <n>").option("--wiki <name>");
|
|
3196
|
-
program.command("stale").option("--archive").option("--days <n>").option("--force-scan").option("--project <slug>").option("--wiki <name>");
|
|
3332
|
+
program.command("stale").option("--archive").option("--apply").option("--approve <token>").option("--days <n>").option("--force-scan").option("--project <slug>").option("--wiki <name>");
|
|
3197
3333
|
program.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
|
|
3198
3334
|
program.command("pagesize").option("--lines <n>").option("--wiki <name>");
|
|
3199
3335
|
program.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
|
|
@@ -3208,10 +3344,10 @@ function buildCliSurface() {
|
|
|
3208
3344
|
program.command("health").option("--wiki <name>").option("--sync <mode>").option("--no-fail").option("--out <path>").option("--examples <n>");
|
|
3209
3345
|
program.command("doctor").option("--check-snapshotter");
|
|
3210
3346
|
program.command("status").option("--wiki <name>");
|
|
3211
|
-
program.command("archive").option("--wiki <name>").option("--cascade").option("--apply").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>");
|
|
3347
|
+
program.command("archive").option("--wiki <name>").option("--cascade").option("--apply").option("--approve <token>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>");
|
|
3212
3348
|
program.command("remove").option("--wiki <name>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--reason <text>");
|
|
3213
3349
|
program.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
|
|
3214
|
-
program.command("dedup").option("--apply").option("--canonical-policy <policy>").option("--manifest-out <path>").option("--manifest-in <path>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--wiki <name>");
|
|
3350
|
+
program.command("dedup").option("--apply").option("--approve <token>").option("--canonical-policy <policy>").option("--manifest-out <path>").option("--manifest-in <path>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--wiki <name>");
|
|
3215
3351
|
program.command("migrate-citations").option("--dry-run").option("--wiki <name>");
|
|
3216
3352
|
program.command("frontmatter-fix").option("--dry-run").option("--wiki <name>");
|
|
3217
3353
|
program.command("update").option("--tag <tag>");
|
|
@@ -3236,6 +3372,10 @@ function buildCliSurface() {
|
|
|
3236
3372
|
program.command("mcp");
|
|
3237
3373
|
const graphCmd = program.commands.find((c) => c.name() === "graph");
|
|
3238
3374
|
graphCmd.command("build").option("--out <path>").option("--wiki <name>");
|
|
3375
|
+
const sourcesCmd = program.commands.find((c) => c.name() === "sources");
|
|
3376
|
+
sourcesCmd.command("pending").option("--since <date>").option("--older-than <days>").option("--match <text>").option("--ingested-by <channel>").option("--scope <scope>").option("--sort <order>").option("--limit <n>").option("--all").option("--include-integrated").option("--include-archived").option("--include-duplicates").option("--include-legacy-archived").option("--wiki <name>");
|
|
3377
|
+
sourcesCmd.command("disposition").requiredOption("--status <status>").requiredOption("--reason <text>").option("--review-after <date>").option("--duplicate-of <raw-path>").option("--write").option("--approve <token>").option("--wiki <name>");
|
|
3378
|
+
sourcesCmd.command("dispose").requiredOption("--reason <text>").option("--write").option("--approve <token>").option("--wiki <name>");
|
|
3239
3379
|
const canvasCmd = program.commands.find((c) => c.name() === "canvas");
|
|
3240
3380
|
canvasCmd.command("generate").option("--graph-path <path>").option("--wiki <name>");
|
|
3241
3381
|
const configCmd = program.commands.find((c) => c.name() === "config");
|
|
@@ -3576,17 +3716,6 @@ function readMirrorHintLines(vault) {
|
|
|
3576
3716
|
"if results look stale, refresh the read mirror or rerun with SKILLWIKI_DISABLE_VAULT_READ_MIRROR=1 for a live scan; live scans may be slower"
|
|
3577
3717
|
];
|
|
3578
3718
|
}
|
|
3579
|
-
function recomputeRawSha256IfPresent(content) {
|
|
3580
|
-
const split = splitFrontmatter(content);
|
|
3581
|
-
if (!split.ok) return content;
|
|
3582
|
-
if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
|
|
3583
|
-
const sha256 = createHash5("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
|
|
3584
|
-
const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
|
|
3585
|
-
return `---
|
|
3586
|
-
${rawFrontmatter}
|
|
3587
|
-
---
|
|
3588
|
-
${split.data.body}`;
|
|
3589
|
-
}
|
|
3590
3719
|
function appendLintFixLastOp(vault, fixed) {
|
|
3591
3720
|
if (fixed.length === 0) return;
|
|
3592
3721
|
appendLastOp(vault, {
|
|
@@ -3625,10 +3754,10 @@ function summarizeLintOutput(output, examplesLimit = 3) {
|
|
|
3625
3754
|
};
|
|
3626
3755
|
}
|
|
3627
3756
|
async function walkMarkdownFiles(absDir, vaultRoot) {
|
|
3628
|
-
const entries = await
|
|
3757
|
+
const entries = await readdir3(absDir, { withFileTypes: true });
|
|
3629
3758
|
const pages = [];
|
|
3630
3759
|
for (const entry of entries) {
|
|
3631
|
-
const absPath =
|
|
3760
|
+
const absPath = join16(absDir, entry.name);
|
|
3632
3761
|
if (entry.isDirectory()) {
|
|
3633
3762
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
3634
3763
|
pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
|
|
@@ -3639,12 +3768,12 @@ async function walkMarkdownFiles(absDir, vaultRoot) {
|
|
|
3639
3768
|
return pages;
|
|
3640
3769
|
}
|
|
3641
3770
|
async function collectCliRefsPages(vault) {
|
|
3642
|
-
if (!existsSync6(
|
|
3771
|
+
if (!existsSync6(join16(vault, "SCHEMA.md"))) {
|
|
3643
3772
|
return err("VAULT_PATH_INVALID", { root: vault, reason: "SCHEMA.md missing" });
|
|
3644
3773
|
}
|
|
3645
3774
|
const pages = [];
|
|
3646
3775
|
for (const dir of CLI_REFS_TYPED_DIRS) {
|
|
3647
|
-
const absDir =
|
|
3776
|
+
const absDir = join16(vault, dir);
|
|
3648
3777
|
if (!existsSync6(absDir)) continue;
|
|
3649
3778
|
pages.push(...await walkMarkdownFiles(absDir, vault));
|
|
3650
3779
|
}
|
|
@@ -3764,9 +3893,13 @@ async function applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSource
|
|
|
3764
3893
|
if (!input.fix || fileSourceUrlFrontmatterFlags.size === 0) return fileSourceUrlFlags;
|
|
3765
3894
|
const fileFixed = [];
|
|
3766
3895
|
for (const relPath of fileSourceUrlFrontmatterFlags) {
|
|
3896
|
+
if (relPath.startsWith("raw/")) {
|
|
3897
|
+
unresolved.push(relPath);
|
|
3898
|
+
continue;
|
|
3899
|
+
}
|
|
3767
3900
|
try {
|
|
3768
3901
|
const absPath = `${input.vault}/${relPath}`;
|
|
3769
|
-
const raw = await
|
|
3902
|
+
const raw = await readFile15(absPath, "utf8");
|
|
3770
3903
|
const parts = raw.split("---", 3);
|
|
3771
3904
|
if (parts.length < 3) {
|
|
3772
3905
|
unresolved.push(relPath);
|
|
@@ -4159,8 +4292,8 @@ async function runLint(input) {
|
|
|
4159
4292
|
const readKnowledgeContent = (slug) => {
|
|
4160
4293
|
const existing = knowledgeContentCache.get(slug);
|
|
4161
4294
|
if (existing) return existing;
|
|
4162
|
-
const knowledgePath =
|
|
4163
|
-
const pending = existsSync6(knowledgePath) ?
|
|
4295
|
+
const knowledgePath = join16(lintVault, "projects", slug, "knowledge.md");
|
|
4296
|
+
const pending = existsSync6(knowledgePath) ? readFile15(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
|
|
4164
4297
|
knowledgeContentCache.set(slug, pending);
|
|
4165
4298
|
return pending;
|
|
4166
4299
|
};
|
|
@@ -4237,8 +4370,11 @@ async function runLint(input) {
|
|
|
4237
4370
|
const raw = await readPage(page);
|
|
4238
4371
|
const redacted = redactSensitiveContent(raw, { file: page.relPath });
|
|
4239
4372
|
if (!redacted.changed) continue;
|
|
4240
|
-
|
|
4241
|
-
|
|
4373
|
+
if (page.relPath.startsWith("raw/")) {
|
|
4374
|
+
unresolved.push(page.relPath);
|
|
4375
|
+
continue;
|
|
4376
|
+
}
|
|
4377
|
+
const w = await safeWritePage(page.absPath, redacted.text, { minBodyRatio: null });
|
|
4242
4378
|
if (!w.ok) {
|
|
4243
4379
|
unresolved.push(page.relPath);
|
|
4244
4380
|
continue;
|
|
@@ -4266,7 +4402,7 @@ async function runLint(input) {
|
|
|
4266
4402
|
for (const relPath of legacyPages) {
|
|
4267
4403
|
try {
|
|
4268
4404
|
const absPath = `${input.vault}/${relPath}`;
|
|
4269
|
-
const raw = await
|
|
4405
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4270
4406
|
const split = splitFrontmatter(raw);
|
|
4271
4407
|
if (!split.ok) {
|
|
4272
4408
|
unresolved.push(relPath);
|
|
@@ -4365,7 +4501,7 @@ ${newBody}`;
|
|
|
4365
4501
|
for (const relPath of noOverview) {
|
|
4366
4502
|
try {
|
|
4367
4503
|
const absPath = `${input.vault}/${relPath}`;
|
|
4368
|
-
const raw = await
|
|
4504
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4369
4505
|
const split = splitFrontmatter(raw);
|
|
4370
4506
|
if (!split.ok) {
|
|
4371
4507
|
unresolved.push(relPath);
|
|
@@ -4406,7 +4542,7 @@ ${trimmedBody}`;
|
|
|
4406
4542
|
for (const relPath of missingTldrFlags) {
|
|
4407
4543
|
try {
|
|
4408
4544
|
const absPath = `${input.vault}/${relPath}`;
|
|
4409
|
-
const raw = await
|
|
4545
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4410
4546
|
const split = splitFrontmatter(raw);
|
|
4411
4547
|
if (!split.ok) {
|
|
4412
4548
|
unresolved.push(relPath);
|
|
@@ -4456,7 +4592,7 @@ ${lines.join("\n")}`;
|
|
|
4456
4592
|
for (const relPath of wikilinkCitationFlags) {
|
|
4457
4593
|
try {
|
|
4458
4594
|
const absPath = `${input.vault}/${relPath}`;
|
|
4459
|
-
const raw = await
|
|
4595
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4460
4596
|
const split = splitFrontmatter(raw);
|
|
4461
4597
|
if (!split.ok) {
|
|
4462
4598
|
unresolved.push(relPath);
|
|
@@ -4561,6 +4697,11 @@ ${newBody}`;
|
|
|
4561
4697
|
const invalidItems = buckets.frontmatter_yaml_invalid;
|
|
4562
4698
|
const remaining = [];
|
|
4563
4699
|
for (const item of invalidItems) {
|
|
4700
|
+
if (item.path.startsWith("raw/")) {
|
|
4701
|
+
unresolved.push(item.path);
|
|
4702
|
+
remaining.push(item);
|
|
4703
|
+
continue;
|
|
4704
|
+
}
|
|
4564
4705
|
const page = scan.allMarkdown.find((p) => p.relPath === item.path);
|
|
4565
4706
|
if (!page) {
|
|
4566
4707
|
unresolved.push(item.path);
|
|
@@ -4797,14 +4938,14 @@ async function runSyncLintDelta(input) {
|
|
|
4797
4938
|
}
|
|
4798
4939
|
|
|
4799
4940
|
// src/commands/config.ts
|
|
4800
|
-
import { readFile as
|
|
4941
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
4801
4942
|
import { existsSync as existsSync7 } from "fs";
|
|
4802
|
-
import { join as
|
|
4943
|
+
import { join as join17 } from "path";
|
|
4803
4944
|
function validateKey(key) {
|
|
4804
4945
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key) || isVaultSyncKey(key);
|
|
4805
4946
|
}
|
|
4806
4947
|
function configPath(home) {
|
|
4807
|
-
return
|
|
4948
|
+
return join17(home, ".skillwiki", ".env");
|
|
4808
4949
|
}
|
|
4809
4950
|
async function runConfigGet(input) {
|
|
4810
4951
|
if (!validateKey(input.key)) {
|
|
@@ -4828,7 +4969,7 @@ async function runConfigSet(input) {
|
|
|
4828
4969
|
try {
|
|
4829
4970
|
let originalContent;
|
|
4830
4971
|
try {
|
|
4831
|
-
originalContent = await
|
|
4972
|
+
originalContent = await readFile16(filePath, "utf8");
|
|
4832
4973
|
} catch {
|
|
4833
4974
|
}
|
|
4834
4975
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -4865,19 +5006,19 @@ async function runConfigPath(input) {
|
|
|
4865
5006
|
|
|
4866
5007
|
// src/commands/doctor.ts
|
|
4867
5008
|
import { existsSync as existsSync13, lstatSync, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
|
|
4868
|
-
import { join as
|
|
5009
|
+
import { join as join23, resolve as resolve5 } from "path";
|
|
4869
5010
|
import { execSync as execSync2 } from "child_process";
|
|
4870
5011
|
import { platform as platform2 } from "os";
|
|
4871
5012
|
|
|
4872
5013
|
// src/utils/plugin-registry.ts
|
|
4873
5014
|
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
4874
|
-
import { join as
|
|
4875
|
-
var REGISTRY_PATH =
|
|
4876
|
-
var CODEX_CONFIG_PATH =
|
|
5015
|
+
import { join as join18 } from "path";
|
|
5016
|
+
var REGISTRY_PATH = join18(".claude", "plugins", "installed_plugins.json");
|
|
5017
|
+
var CODEX_CONFIG_PATH = join18(".codex", "config.toml");
|
|
4877
5018
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
4878
5019
|
function readInstalledPlugins(home) {
|
|
4879
5020
|
try {
|
|
4880
|
-
const raw = readFileSync5(
|
|
5021
|
+
const raw = readFileSync5(join18(home, REGISTRY_PATH), "utf8");
|
|
4881
5022
|
return JSON.parse(raw);
|
|
4882
5023
|
} catch {
|
|
4883
5024
|
return null;
|
|
@@ -4913,7 +5054,7 @@ function findPluginInstallations(home, key = PLUGIN_KEY) {
|
|
|
4913
5054
|
function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
4914
5055
|
const config = readCodexPluginConfig(home, key, marketplace);
|
|
4915
5056
|
if (!config?.enabled) return null;
|
|
4916
|
-
const cacheRoot =
|
|
5057
|
+
const cacheRoot = join18(home, ".codex", "plugins", "cache", marketplace, pluginName);
|
|
4917
5058
|
if (!existsSync8(cacheRoot)) return null;
|
|
4918
5059
|
let versions;
|
|
4919
5060
|
try {
|
|
@@ -4929,7 +5070,7 @@ function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
|
4929
5070
|
key,
|
|
4930
5071
|
pluginName,
|
|
4931
5072
|
marketplace,
|
|
4932
|
-
installPath:
|
|
5073
|
+
installPath: join18(cacheRoot, version),
|
|
4933
5074
|
version,
|
|
4934
5075
|
sourceType: config.sourceType,
|
|
4935
5076
|
source: config.source
|
|
@@ -4946,7 +5087,7 @@ function parsePluginKey(key) {
|
|
|
4946
5087
|
function readCodexPluginConfig(home, key, marketplace) {
|
|
4947
5088
|
let raw;
|
|
4948
5089
|
try {
|
|
4949
|
-
raw = readFileSync5(
|
|
5090
|
+
raw = readFileSync5(join18(home, CODEX_CONFIG_PATH), "utf8");
|
|
4950
5091
|
} catch {
|
|
4951
5092
|
return null;
|
|
4952
5093
|
}
|
|
@@ -4989,7 +5130,7 @@ function parseTomlScalar(rawValue) {
|
|
|
4989
5130
|
|
|
4990
5131
|
// src/utils/conflict-markers.ts
|
|
4991
5132
|
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
4992
|
-
import { join as
|
|
5133
|
+
import { join as join19 } from "path";
|
|
4993
5134
|
function scanConflictMarkerBlocksInText(relPath, text) {
|
|
4994
5135
|
const findings = [];
|
|
4995
5136
|
const lines = text.split(/\r?\n/);
|
|
@@ -5040,7 +5181,7 @@ function walkMarkdownFiles2(root, dir, rel, out) {
|
|
|
5040
5181
|
for (const entry of entries) {
|
|
5041
5182
|
if (entry.isDirectory()) {
|
|
5042
5183
|
if (PRUNE_DIRS.has(entry.name)) continue;
|
|
5043
|
-
walkMarkdownFiles2(root,
|
|
5184
|
+
walkMarkdownFiles2(root, join19(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
|
|
5044
5185
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5045
5186
|
out.push(rel ? `${rel}/${entry.name}` : entry.name);
|
|
5046
5187
|
}
|
|
@@ -5054,7 +5195,7 @@ function scanVaultConflictMarkers(vaultRoot) {
|
|
|
5054
5195
|
for (const rel of relPaths) {
|
|
5055
5196
|
let text;
|
|
5056
5197
|
try {
|
|
5057
|
-
text = readFileSync6(
|
|
5198
|
+
text = readFileSync6(join19(vaultRoot, rel), "utf8");
|
|
5058
5199
|
} catch {
|
|
5059
5200
|
continue;
|
|
5060
5201
|
}
|
|
@@ -5065,7 +5206,7 @@ function scanVaultConflictMarkers(vaultRoot) {
|
|
|
5065
5206
|
|
|
5066
5207
|
// src/utils/remote-health.ts
|
|
5067
5208
|
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
5068
|
-
import { join as
|
|
5209
|
+
import { join as join20 } from "path";
|
|
5069
5210
|
import { execFileSync } from "child_process";
|
|
5070
5211
|
var REMOTE_PROBE_TIMEOUT_MS = 3e3;
|
|
5071
5212
|
var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
@@ -5076,7 +5217,7 @@ var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
|
5076
5217
|
}).trim();
|
|
5077
5218
|
function readWikiS3RemoteConfigured(home) {
|
|
5078
5219
|
try {
|
|
5079
|
-
const content = readFileSync7(
|
|
5220
|
+
const content = readFileSync7(join20(home, ".skillwiki", ".env"), "utf8");
|
|
5080
5221
|
for (const line of content.split(/\r?\n/)) {
|
|
5081
5222
|
const trimmed = line.trim();
|
|
5082
5223
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -5101,7 +5242,7 @@ function resolveWikiS3Remote(input) {
|
|
|
5101
5242
|
return readWikiS3RemoteConfigured(input.home);
|
|
5102
5243
|
}
|
|
5103
5244
|
function probeGithubReachability(vaultPath, exec = defaultExec) {
|
|
5104
|
-
if (!existsSync10(
|
|
5245
|
+
if (!existsSync10(join20(vaultPath, ".git"))) return "unknown";
|
|
5105
5246
|
try {
|
|
5106
5247
|
exec("git", ["remote", "get-url", "origin"], vaultPath);
|
|
5107
5248
|
} catch {
|
|
@@ -5167,10 +5308,10 @@ function probeRemoteHealth(input) {
|
|
|
5167
5308
|
|
|
5168
5309
|
// src/utils/satellite-run-health.ts
|
|
5169
5310
|
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
5170
|
-
import { join as
|
|
5311
|
+
import { join as join21 } from "path";
|
|
5171
5312
|
var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
|
|
5172
5313
|
function satelliteLatestRunPath(vault) {
|
|
5173
|
-
return
|
|
5314
|
+
return join21(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
|
|
5174
5315
|
}
|
|
5175
5316
|
function isFailedRunStatus(status) {
|
|
5176
5317
|
return status === "fail" || status === "failure";
|
|
@@ -5223,8 +5364,8 @@ function evaluateSatelliteRunHealth(vault, now) {
|
|
|
5223
5364
|
// src/utils/s3-mount-health.ts
|
|
5224
5365
|
import { execSync } from "child_process";
|
|
5225
5366
|
import { platform } from "os";
|
|
5226
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync as
|
|
5227
|
-
import { join as
|
|
5367
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile17 } from "fs";
|
|
5368
|
+
import { join as join22 } from "path";
|
|
5228
5369
|
var OS = platform();
|
|
5229
5370
|
function findRcloneMountPid() {
|
|
5230
5371
|
try {
|
|
@@ -5382,7 +5523,7 @@ function detectFuseMount(vaultPath) {
|
|
|
5382
5523
|
return null;
|
|
5383
5524
|
}
|
|
5384
5525
|
function writeTest(dir) {
|
|
5385
|
-
const testFile =
|
|
5526
|
+
const testFile = join22(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
5386
5527
|
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
5387
5528
|
const start = Date.now();
|
|
5388
5529
|
try {
|
|
@@ -5393,24 +5534,24 @@ function writeTest(dir) {
|
|
|
5393
5534
|
const writeMs = Date.now() - start;
|
|
5394
5535
|
const readStart = Date.now();
|
|
5395
5536
|
try {
|
|
5396
|
-
const back =
|
|
5537
|
+
const back = readFile17(testFile, "utf8");
|
|
5397
5538
|
const readMs = Date.now() - readStart;
|
|
5398
5539
|
if (back !== payload) {
|
|
5399
5540
|
try {
|
|
5400
|
-
|
|
5541
|
+
unlinkSync4(testFile);
|
|
5401
5542
|
} catch {
|
|
5402
5543
|
}
|
|
5403
5544
|
return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
|
|
5404
5545
|
}
|
|
5405
5546
|
} catch (e) {
|
|
5406
5547
|
try {
|
|
5407
|
-
|
|
5548
|
+
unlinkSync4(testFile);
|
|
5408
5549
|
} catch {
|
|
5409
5550
|
}
|
|
5410
5551
|
return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
|
|
5411
5552
|
}
|
|
5412
5553
|
try {
|
|
5413
|
-
|
|
5554
|
+
unlinkSync4(testFile);
|
|
5414
5555
|
} catch {
|
|
5415
5556
|
}
|
|
5416
5557
|
return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
|
|
@@ -5482,12 +5623,12 @@ function detectCliChannels(argv, home) {
|
|
|
5482
5623
|
}
|
|
5483
5624
|
const plugin = findPlugin(home);
|
|
5484
5625
|
if (plugin) {
|
|
5485
|
-
const pluginBin =
|
|
5626
|
+
const pluginBin = join23(plugin.installPath, "bin", "skillwiki");
|
|
5486
5627
|
if (existsSync13(pluginBin)) {
|
|
5487
5628
|
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
5488
5629
|
}
|
|
5489
5630
|
}
|
|
5490
|
-
const installBin =
|
|
5631
|
+
const installBin = join23(home, ".claude", "skills", "bin", "skillwiki");
|
|
5491
5632
|
if (existsSync13(installBin)) {
|
|
5492
5633
|
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
5493
5634
|
}
|
|
@@ -5577,9 +5718,9 @@ function checkVaultStructure(resolvedPath) {
|
|
|
5577
5718
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
5578
5719
|
}
|
|
5579
5720
|
const missing = [];
|
|
5580
|
-
if (!existsSync13(
|
|
5721
|
+
if (!existsSync13(join23(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
5581
5722
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
5582
|
-
if (!existsSync13(
|
|
5723
|
+
if (!existsSync13(join23(resolvedPath, dir))) missing.push(dir + "/");
|
|
5583
5724
|
}
|
|
5584
5725
|
if (missing.length === 0) {
|
|
5585
5726
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -5587,7 +5728,7 @@ function checkVaultStructure(resolvedPath) {
|
|
|
5587
5728
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
5588
5729
|
}
|
|
5589
5730
|
function checkSkillsInstalled(home, cwd) {
|
|
5590
|
-
const srcDir = cwd ?
|
|
5731
|
+
const srcDir = cwd ? join23(cwd, "packages", "skills") : void 0;
|
|
5591
5732
|
if (srcDir && existsSync13(srcDir)) {
|
|
5592
5733
|
const found = findInstalledSkillMd(srcDir);
|
|
5593
5734
|
if (found.length > 0) {
|
|
@@ -5601,7 +5742,7 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
5601
5742
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
5602
5743
|
}
|
|
5603
5744
|
}
|
|
5604
|
-
const skillsDir =
|
|
5745
|
+
const skillsDir = join23(home, ".claude", "skills");
|
|
5605
5746
|
if (existsSync13(skillsDir)) {
|
|
5606
5747
|
const found = findInstalledSkillMd(skillsDir);
|
|
5607
5748
|
if (found.length > 0) {
|
|
@@ -5612,10 +5753,10 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
5612
5753
|
}
|
|
5613
5754
|
function checkDuplicateSkills(home) {
|
|
5614
5755
|
const plugin = findPlugin(home);
|
|
5615
|
-
const skillsDir =
|
|
5756
|
+
const skillsDir = join23(home, ".claude", "skills");
|
|
5616
5757
|
const agentSkillDirs = [
|
|
5617
|
-
{ label: "~/.codex/skills/", path:
|
|
5618
|
-
{ label: "~/.agents/skills/", path:
|
|
5758
|
+
{ label: "~/.codex/skills/", path: join23(home, ".codex", "skills") },
|
|
5759
|
+
{ label: "~/.agents/skills/", path: join23(home, ".agents", "skills") }
|
|
5619
5760
|
];
|
|
5620
5761
|
if (!plugin) {
|
|
5621
5762
|
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
@@ -5718,7 +5859,7 @@ async function checkProfiles(home) {
|
|
|
5718
5859
|
}
|
|
5719
5860
|
async function checkProjectLocalOverride(cwd) {
|
|
5720
5861
|
const dir = cwd ?? process.cwd();
|
|
5721
|
-
const envPath =
|
|
5862
|
+
const envPath = join23(dir, ".skillwiki", ".env");
|
|
5722
5863
|
if (existsSync13(envPath)) {
|
|
5723
5864
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
5724
5865
|
}
|
|
@@ -5728,7 +5869,7 @@ function checkVaultGitRemote(resolvedPath) {
|
|
|
5728
5869
|
if (resolvedPath === void 0) {
|
|
5729
5870
|
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5730
5871
|
}
|
|
5731
|
-
if (!existsSync13(
|
|
5872
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5732
5873
|
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
5733
5874
|
}
|
|
5734
5875
|
try {
|
|
@@ -5751,9 +5892,9 @@ function checkObsidianTemplates(resolvedPath) {
|
|
|
5751
5892
|
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5752
5893
|
}
|
|
5753
5894
|
const missing = [];
|
|
5754
|
-
if (!existsSync13(
|
|
5755
|
-
if (!existsSync13(
|
|
5756
|
-
if (!existsSync13(
|
|
5895
|
+
if (!existsSync13(join23(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
5896
|
+
if (!existsSync13(join23(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
5897
|
+
if (!existsSync13(join23(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
5757
5898
|
if (missing.length === 0) {
|
|
5758
5899
|
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
5759
5900
|
}
|
|
@@ -5763,7 +5904,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
5763
5904
|
if (resolvedPath === void 0) {
|
|
5764
5905
|
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5765
5906
|
}
|
|
5766
|
-
const rawDir =
|
|
5907
|
+
const rawDir = join23(resolvedPath, "raw");
|
|
5767
5908
|
if (!existsSync13(rawDir)) {
|
|
5768
5909
|
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
5769
5910
|
}
|
|
@@ -5779,7 +5920,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
5779
5920
|
if (entry.name === ".DS_Store") {
|
|
5780
5921
|
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
5781
5922
|
} else if (entry.isDirectory()) {
|
|
5782
|
-
walk(
|
|
5923
|
+
walk(join23(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
5783
5924
|
}
|
|
5784
5925
|
}
|
|
5785
5926
|
})(rawDir, "");
|
|
@@ -5810,7 +5951,7 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
5810
5951
|
if (resolvedPath === void 0) {
|
|
5811
5952
|
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5812
5953
|
}
|
|
5813
|
-
if (!existsSync13(
|
|
5954
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5814
5955
|
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
5815
5956
|
}
|
|
5816
5957
|
let timestamp;
|
|
@@ -5859,7 +6000,7 @@ function checkVaultGitDirty(resolvedPath) {
|
|
|
5859
6000
|
if (resolvedPath === void 0) {
|
|
5860
6001
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
|
|
5861
6002
|
}
|
|
5862
|
-
if (!existsSync13(
|
|
6003
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5863
6004
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
|
|
5864
6005
|
}
|
|
5865
6006
|
try {
|
|
@@ -5928,7 +6069,7 @@ function remoteMainHash(resolvedPath) {
|
|
|
5928
6069
|
}
|
|
5929
6070
|
function checkStaleRemoteMain(resolvedPath) {
|
|
5930
6071
|
if (resolvedPath === void 0) return void 0;
|
|
5931
|
-
if (!existsSync13(
|
|
6072
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) return void 0;
|
|
5932
6073
|
const localOrigin = gitRefHash(resolvedPath, "origin/main");
|
|
5933
6074
|
if (!localOrigin) return void 0;
|
|
5934
6075
|
const remoteMain = remoteMainHash(resolvedPath);
|
|
@@ -5944,7 +6085,7 @@ function checkVaultLocalGit(resolvedPath) {
|
|
|
5944
6085
|
if (resolvedPath === void 0) {
|
|
5945
6086
|
return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5946
6087
|
}
|
|
5947
|
-
if (!existsSync13(
|
|
6088
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5948
6089
|
return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
|
|
5949
6090
|
}
|
|
5950
6091
|
try {
|
|
@@ -5963,7 +6104,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
|
|
|
5963
6104
|
if (resolvedPath === void 0) {
|
|
5964
6105
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
|
|
5965
6106
|
}
|
|
5966
|
-
if (!existsSync13(
|
|
6107
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5967
6108
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
|
|
5968
6109
|
}
|
|
5969
6110
|
const state = probeGithubReachability(resolvedPath, exec);
|
|
@@ -6007,7 +6148,7 @@ function checkVaultPromotionLag(resolvedPath) {
|
|
|
6007
6148
|
if (resolvedPath === void 0) {
|
|
6008
6149
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
|
|
6009
6150
|
}
|
|
6010
|
-
if (!existsSync13(
|
|
6151
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
6011
6152
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
|
|
6012
6153
|
}
|
|
6013
6154
|
try {
|
|
@@ -6034,7 +6175,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
|
|
|
6034
6175
|
if (resolvedPath === void 0) {
|
|
6035
6176
|
return check("pass", id, label, "No vault path \u2014 check skipped");
|
|
6036
6177
|
}
|
|
6037
|
-
if (!existsSync13(
|
|
6178
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
6038
6179
|
return check("pass", id, label, "No git repo \u2014 check skipped");
|
|
6039
6180
|
}
|
|
6040
6181
|
if (!hasOriginMain(resolvedPath)) {
|
|
@@ -6151,11 +6292,11 @@ async function checkFleetIdentity(input) {
|
|
|
6151
6292
|
}
|
|
6152
6293
|
function pullLogPaths(home) {
|
|
6153
6294
|
const paths = platform2() === "darwin" ? [
|
|
6154
|
-
|
|
6155
|
-
|
|
6295
|
+
join23(home, "Library", "Logs", "wiki-pull.log"),
|
|
6296
|
+
join23(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
|
|
6156
6297
|
] : [
|
|
6157
|
-
|
|
6158
|
-
|
|
6298
|
+
join23(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
|
|
6299
|
+
join23(home, "Library", "Logs", "wiki-pull.log")
|
|
6159
6300
|
];
|
|
6160
6301
|
return [...new Set(paths)];
|
|
6161
6302
|
}
|
|
@@ -6195,7 +6336,7 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
6195
6336
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
6196
6337
|
}
|
|
6197
6338
|
const mountPoint = fuse.mountPoint;
|
|
6198
|
-
const conceptsDir =
|
|
6339
|
+
const conceptsDir = join23(resolvedPath, "concepts");
|
|
6199
6340
|
if (!existsSync13(conceptsDir)) {
|
|
6200
6341
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
6201
6342
|
}
|
|
@@ -6378,7 +6519,7 @@ function checkWriteTest(resolvedPath) {
|
|
|
6378
6519
|
if (!fuse) {
|
|
6379
6520
|
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
6380
6521
|
}
|
|
6381
|
-
const conceptsDir =
|
|
6522
|
+
const conceptsDir = join23(resolvedPath, "concepts");
|
|
6382
6523
|
if (!existsSync13(conceptsDir)) {
|
|
6383
6524
|
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
6384
6525
|
}
|
|
@@ -6480,7 +6621,7 @@ function checkVaultSyncPullHelper(home, env) {
|
|
|
6480
6621
|
);
|
|
6481
6622
|
}
|
|
6482
6623
|
function checkVaultSyncReviewRequiredJournals(vaultPath) {
|
|
6483
|
-
if (!vaultPath || !existsSync13(
|
|
6624
|
+
if (!vaultPath || !existsSync13(join23(vaultPath, ".git"))) {
|
|
6484
6625
|
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
|
|
6485
6626
|
}
|
|
6486
6627
|
try {
|
|
@@ -6501,7 +6642,7 @@ function checkVaultSyncReviewRequiredJournals(vaultPath) {
|
|
|
6501
6642
|
}
|
|
6502
6643
|
function readVaultSyncConfig(home) {
|
|
6503
6644
|
try {
|
|
6504
|
-
const content = readFileSync10(
|
|
6645
|
+
const content = readFileSync10(join23(home, ".skillwiki", ".env"), "utf8");
|
|
6505
6646
|
let installed = false;
|
|
6506
6647
|
let role;
|
|
6507
6648
|
let serviceScope;
|
|
@@ -6630,7 +6771,7 @@ function snapshotterHealthChecks(scope, logDir, env) {
|
|
|
6630
6771
|
let completionOutcome = "unknown";
|
|
6631
6772
|
const logRecords = fixture ? fixture.log_records : (() => {
|
|
6632
6773
|
try {
|
|
6633
|
-
const content = readFileSync10(
|
|
6774
|
+
const content = readFileSync10(join23(logDir, "wiki-snapshot.log"), "utf8");
|
|
6634
6775
|
return content.split(/\r?\n/).filter(Boolean);
|
|
6635
6776
|
} catch {
|
|
6636
6777
|
return [];
|
|
@@ -6700,10 +6841,10 @@ function vaultSyncChecks(input) {
|
|
|
6700
6841
|
];
|
|
6701
6842
|
}
|
|
6702
6843
|
const isMac = os === "darwin";
|
|
6703
|
-
const logDir = input.logDir ?? (isMac ?
|
|
6704
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
6705
|
-
const filterPath = input.filterPath ??
|
|
6706
|
-
const packagedSnapshotPath =
|
|
6844
|
+
const logDir = input.logDir ?? (isMac ? join23(home, "Library", "Logs") : join23(home, ".local", "state", "vault-sync", "log"));
|
|
6845
|
+
const shareDir = input.shareDir ?? (isMac ? join23(home, "Library", "Application Support", "vault-sync", "bin") : join23(home, ".local", "share", "vault-sync", "bin"));
|
|
6846
|
+
const filterPath = input.filterPath ?? join23(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
6847
|
+
const packagedSnapshotPath = join23(shareDir, "wiki-snapshot.sh");
|
|
6707
6848
|
const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
6708
6849
|
const snapshotPath = input.snapshotScriptPath ?? (existsSync13(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
|
|
6709
6850
|
if (input.vaultSyncRole === "snapshotter") {
|
|
@@ -6759,7 +6900,7 @@ function vaultSyncChecks(input) {
|
|
|
6759
6900
|
}
|
|
6760
6901
|
return [c12, ...healthChecks, cFetch2, c42, c52];
|
|
6761
6902
|
}
|
|
6762
|
-
const pushScriptPath =
|
|
6903
|
+
const pushScriptPath = join23(shareDir, "wiki-push.sh");
|
|
6763
6904
|
const c1 = existsSync13(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
|
|
6764
6905
|
let c2;
|
|
6765
6906
|
try {
|
|
@@ -6811,7 +6952,7 @@ function vaultSyncChecks(input) {
|
|
|
6811
6952
|
"Scheduler check failed \u2014 run vault-sync-install"
|
|
6812
6953
|
);
|
|
6813
6954
|
}
|
|
6814
|
-
const logFile =
|
|
6955
|
+
const logFile = join23(logDir, "wiki-push.log");
|
|
6815
6956
|
let c3;
|
|
6816
6957
|
try {
|
|
6817
6958
|
const logContent = readFileSync10(logFile, "utf8");
|
|
@@ -6882,7 +7023,7 @@ function vaultSyncChecks(input) {
|
|
|
6882
7023
|
`Log directory not found at ${logDir}`
|
|
6883
7024
|
);
|
|
6884
7025
|
}
|
|
6885
|
-
const fetchLogFile =
|
|
7026
|
+
const fetchLogFile = join23(logDir, "wiki-fetch.log");
|
|
6886
7027
|
let cFetch;
|
|
6887
7028
|
try {
|
|
6888
7029
|
const logContent = readFileSync10(fetchLogFile, "utf8");
|
|
@@ -7026,15 +7167,15 @@ function findSkillMd(dir) {
|
|
|
7026
7167
|
}
|
|
7027
7168
|
for (const entry of entries) {
|
|
7028
7169
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
7029
|
-
results.push(
|
|
7170
|
+
results.push(join23(dir, entry.name));
|
|
7030
7171
|
} else if (entry.isDirectory()) {
|
|
7031
|
-
results.push(...findSkillMd(
|
|
7172
|
+
results.push(...findSkillMd(join23(dir, entry.name)));
|
|
7032
7173
|
}
|
|
7033
7174
|
}
|
|
7034
7175
|
return results;
|
|
7035
7176
|
}
|
|
7036
7177
|
function findInstalledSkillMd(dir) {
|
|
7037
|
-
const directSkills = findSkillNames(dir).map((name) =>
|
|
7178
|
+
const directSkills = findSkillNames(dir).map((name) => join23(dir, name, "SKILL.md"));
|
|
7038
7179
|
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
7039
7180
|
}
|
|
7040
7181
|
function findSkillNames(dir) {
|
|
@@ -7046,7 +7187,7 @@ function findSkillNames(dir) {
|
|
|
7046
7187
|
return results;
|
|
7047
7188
|
}
|
|
7048
7189
|
for (const entry of entries) {
|
|
7049
|
-
if (entry.isDirectory() && existsSync13(
|
|
7190
|
+
if (entry.isDirectory() && existsSync13(join23(dir, entry.name, "SKILL.md"))) {
|
|
7050
7191
|
results.push(entry.name);
|
|
7051
7192
|
}
|
|
7052
7193
|
}
|
|
@@ -7094,7 +7235,7 @@ async function vaultMetrics(resolvedPath) {
|
|
|
7094
7235
|
}
|
|
7095
7236
|
let logLines = 0;
|
|
7096
7237
|
try {
|
|
7097
|
-
logLines = readFileSync10(
|
|
7238
|
+
logLines = readFileSync10(join23(scanRoot, "log.md"), "utf8").split("\n").length;
|
|
7098
7239
|
} catch {
|
|
7099
7240
|
}
|
|
7100
7241
|
return [
|
|
@@ -7229,7 +7370,7 @@ function readCliPackageJson(baseUrl = import.meta.url) {
|
|
|
7229
7370
|
// src/utils/vault-write-gates.ts
|
|
7230
7371
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
7231
7372
|
import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
7232
|
-
import { join as
|
|
7373
|
+
import { join as join24, relative as relative3 } from "path";
|
|
7233
7374
|
var DEFAULT_DIRTY_VOLUME_THRESHOLD = 50;
|
|
7234
7375
|
var DEFAULT_CAPTURE_BUDGET = 20;
|
|
7235
7376
|
var DEFAULT_NO_DECISION_STREAK = 3;
|
|
@@ -7328,7 +7469,7 @@ function measureDirtyVolume(vault) {
|
|
|
7328
7469
|
if (!rel) continue;
|
|
7329
7470
|
if (code === "??") {
|
|
7330
7471
|
untracked += 1;
|
|
7331
|
-
const abs =
|
|
7472
|
+
const abs = join24(vault, rel);
|
|
7332
7473
|
if (existsSync14(abs) && statSync3(abs).isDirectory()) {
|
|
7333
7474
|
const files = listFilesRecursive(abs);
|
|
7334
7475
|
expanded += files.length;
|
|
@@ -7365,7 +7506,7 @@ function listFilesRecursive(dir) {
|
|
|
7365
7506
|
}
|
|
7366
7507
|
for (const name of entries) {
|
|
7367
7508
|
if (name === ".git") continue;
|
|
7368
|
-
const p =
|
|
7509
|
+
const p = join24(dir, name);
|
|
7369
7510
|
try {
|
|
7370
7511
|
const st = statSync3(p);
|
|
7371
7512
|
if (st.isDirectory()) out.push(...listFilesRecursive(p));
|
|
@@ -7496,7 +7637,7 @@ function listProjectDayCaptures(vault, project, day) {
|
|
|
7496
7637
|
return;
|
|
7497
7638
|
}
|
|
7498
7639
|
for (const name of entries) {
|
|
7499
|
-
const abs =
|
|
7640
|
+
const abs = join24(dir, name);
|
|
7500
7641
|
const rel = relBase ? `${relBase}/${name}` : name;
|
|
7501
7642
|
try {
|
|
7502
7643
|
const st = statSync3(abs);
|
|
@@ -7506,16 +7647,16 @@ function listProjectDayCaptures(vault, project, day) {
|
|
|
7506
7647
|
}
|
|
7507
7648
|
}
|
|
7508
7649
|
};
|
|
7509
|
-
walk(
|
|
7510
|
-
walk(
|
|
7511
|
-
walk(
|
|
7512
|
-
const workRoot =
|
|
7650
|
+
walk(join24(vault, "raw", "transcripts"), "raw/transcripts");
|
|
7651
|
+
walk(join24(vault, "projects", slug, "raw", "transcripts"), `projects/${slug}/raw/transcripts`);
|
|
7652
|
+
walk(join24(vault, "projects", slug, "requirements"), `projects/${slug}/requirements`);
|
|
7653
|
+
const workRoot = join24(vault, "projects", slug, "work");
|
|
7513
7654
|
if (existsSync14(workRoot)) {
|
|
7514
7655
|
try {
|
|
7515
7656
|
for (const name of readdirSync4(workRoot)) {
|
|
7516
7657
|
if (!name.startsWith(day)) continue;
|
|
7517
7658
|
if (!/investigate|pilot-q|research|cycle|dev-loop/.test(name)) continue;
|
|
7518
|
-
const abs =
|
|
7659
|
+
const abs = join24(workRoot, name);
|
|
7519
7660
|
if (statSync3(abs).isDirectory()) {
|
|
7520
7661
|
for (const f of listFilesRecursive(abs)) {
|
|
7521
7662
|
const rel = relative3(vault, f).replace(/\\/g, "/");
|
|
@@ -7621,8 +7762,8 @@ function runWritePreflight(input) {
|
|
|
7621
7762
|
// src/commands/observe.ts
|
|
7622
7763
|
import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
|
|
7623
7764
|
import { existsSync as existsSync15, statSync as statSync4 } from "fs";
|
|
7624
|
-
import { join as
|
|
7625
|
-
import { createHash as
|
|
7765
|
+
import { join as join25 } from "path";
|
|
7766
|
+
import { createHash as createHash7 } from "crypto";
|
|
7626
7767
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
7627
7768
|
function slugify(text) {
|
|
7628
7769
|
const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -7670,7 +7811,7 @@ async function runObserve(input) {
|
|
|
7670
7811
|
};
|
|
7671
7812
|
}
|
|
7672
7813
|
}
|
|
7673
|
-
const transcriptsDir =
|
|
7814
|
+
const transcriptsDir = join25(input.vault, "raw", "transcripts");
|
|
7674
7815
|
try {
|
|
7675
7816
|
await mkdir6(transcriptsDir, { recursive: true });
|
|
7676
7817
|
} catch {
|
|
@@ -7682,16 +7823,16 @@ async function runObserve(input) {
|
|
|
7682
7823
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7683
7824
|
const slug = slugify(input.text);
|
|
7684
7825
|
const fileName = `${today}-observation-${slug}.md`;
|
|
7685
|
-
const filePath =
|
|
7826
|
+
const filePath = join25(transcriptsDir, fileName);
|
|
7686
7827
|
const body = `
|
|
7687
7828
|
${input.text.trim()}
|
|
7688
7829
|
`;
|
|
7689
|
-
const
|
|
7830
|
+
const sha2562 = createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex");
|
|
7690
7831
|
const frontmatterLines = [
|
|
7691
7832
|
"---",
|
|
7692
7833
|
"source_url:",
|
|
7693
7834
|
`ingested: ${today}`,
|
|
7694
|
-
`sha256: ${
|
|
7835
|
+
`sha256: ${sha2562}`,
|
|
7695
7836
|
`kind: ${kind}`
|
|
7696
7837
|
];
|
|
7697
7838
|
if (input.project) {
|
|
@@ -7714,17 +7855,17 @@ ${input.text.trim()}
|
|
|
7714
7855
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7715
7856
|
});
|
|
7716
7857
|
const relPath = `raw/transcripts/${fileName}`;
|
|
7717
|
-
const humanHint = `created ${relPath} (${
|
|
7858
|
+
const humanHint = `created ${relPath} (${sha2562.slice(0, 12)}...)`;
|
|
7718
7859
|
return {
|
|
7719
7860
|
exitCode: ExitCode.OK,
|
|
7720
|
-
result: ok({ path: relPath, sha256, humanHint })
|
|
7861
|
+
result: ok({ path: relPath, sha256: sha2562, humanHint })
|
|
7721
7862
|
};
|
|
7722
7863
|
}
|
|
7723
7864
|
|
|
7724
7865
|
// src/commands/memory.ts
|
|
7725
|
-
import { createHash as
|
|
7726
|
-
import { mkdir as mkdir7, readFile as
|
|
7727
|
-
import { basename as basename2, extname, join as
|
|
7866
|
+
import { createHash as createHash8 } from "crypto";
|
|
7867
|
+
import { mkdir as mkdir7, readFile as readFile18, readdir as readdir4, stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
7868
|
+
import { basename as basename2, extname, join as join26, relative as relative4, sep as sep3 } from "path";
|
|
7728
7869
|
|
|
7729
7870
|
// src/utils/memory-authority.ts
|
|
7730
7871
|
var TIER_RANK = {
|
|
@@ -7843,8 +7984,8 @@ async function runMemoryIndex(input) {
|
|
|
7843
7984
|
}
|
|
7844
7985
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7845
7986
|
const relCachePath = memoryCacheRelPath(input.project);
|
|
7846
|
-
const absCachePath =
|
|
7847
|
-
await mkdir7(
|
|
7987
|
+
const absCachePath = join26(input.vault, relCachePath);
|
|
7988
|
+
await mkdir7(join26(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
|
|
7848
7989
|
await writeFile5(absCachePath, `${JSON.stringify({
|
|
7849
7990
|
generated_at: generatedAt,
|
|
7850
7991
|
project: input.project,
|
|
@@ -8036,7 +8177,7 @@ async function buildMemoryIndexState(pages, project) {
|
|
|
8036
8177
|
}
|
|
8037
8178
|
async function checkMemoryIndex(vault, project, current) {
|
|
8038
8179
|
const relCachePath = memoryCacheRelPath(project);
|
|
8039
|
-
const cacheText = await readIfExists2(
|
|
8180
|
+
const cacheText = await readIfExists2(join26(vault, relCachePath));
|
|
8040
8181
|
if (!cacheText) {
|
|
8041
8182
|
return {
|
|
8042
8183
|
ok: true,
|
|
@@ -8153,7 +8294,7 @@ async function readMemoryPage(page, project, warnings) {
|
|
|
8153
8294
|
title,
|
|
8154
8295
|
summary: summarize(body),
|
|
8155
8296
|
updated,
|
|
8156
|
-
hash:
|
|
8297
|
+
hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
|
|
8157
8298
|
topics,
|
|
8158
8299
|
project,
|
|
8159
8300
|
...stringField(fm.data.memory_kind) ? { memory_kind: stringField(fm.data.memory_kind) } : {},
|
|
@@ -8355,7 +8496,7 @@ function matchPastFailureTarget(target, candidateBody) {
|
|
|
8355
8496
|
}
|
|
8356
8497
|
if (!FAILURE_MARKER_RE.test(candidateBody)) return null;
|
|
8357
8498
|
const candidateTokens = new Set(tokenizeForMatch(candidate));
|
|
8358
|
-
const overlappingTokens = target.distinctiveTokens.filter((
|
|
8499
|
+
const overlappingTokens = target.distinctiveTokens.filter((token2) => candidateTokens.has(token2));
|
|
8359
8500
|
if (overlappingTokens.length >= 2) {
|
|
8360
8501
|
return { reason: `distinctive target terms overlapped prior failure memory (${overlappingTokens.slice(0, 4).join(", ")})` };
|
|
8361
8502
|
}
|
|
@@ -8371,7 +8512,7 @@ function extractCodeLikeNeedles(value) {
|
|
|
8371
8512
|
return (value.match(/[A-Za-z0-9_.:-]*[_.:-][A-Za-z0-9_.:-]+/g) ?? []).filter((needle) => /[A-Za-z]/.test(needle));
|
|
8372
8513
|
}
|
|
8373
8514
|
function extractDistinctiveTargetTokens(value) {
|
|
8374
|
-
return uniqueStrings(tokenizeForMatch(value).filter((
|
|
8515
|
+
return uniqueStrings(tokenizeForMatch(value).filter((token2) => token2.length >= 4).filter((token2) => !/^\d+$/.test(token2)).filter((token2) => !COMMON_FAILURE_MATCH_TOKENS.has(token2)));
|
|
8375
8516
|
}
|
|
8376
8517
|
function hasDistinctiveNeedle(value) {
|
|
8377
8518
|
return extractDistinctiveTargetTokens(value).length > 0;
|
|
@@ -8429,23 +8570,23 @@ function renderMemoryIndexStatusHint(status) {
|
|
|
8429
8570
|
}
|
|
8430
8571
|
async function readIfExists2(path) {
|
|
8431
8572
|
try {
|
|
8432
|
-
return await
|
|
8573
|
+
return await readFile18(path, "utf8");
|
|
8433
8574
|
} catch {
|
|
8434
8575
|
return "";
|
|
8435
8576
|
}
|
|
8436
8577
|
}
|
|
8437
8578
|
async function collectImportFiles(source) {
|
|
8438
|
-
const st = await
|
|
8579
|
+
const st = await stat4(source);
|
|
8439
8580
|
if (st.isFile()) return isImportCandidate(source) ? [source] : [];
|
|
8440
8581
|
const files = [];
|
|
8441
8582
|
await walkImportFiles(source, files);
|
|
8442
8583
|
return files.sort((a, b) => a.localeCompare(b));
|
|
8443
8584
|
}
|
|
8444
8585
|
async function walkImportFiles(dir, out) {
|
|
8445
|
-
const entries = await
|
|
8586
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
8446
8587
|
for (const entry of entries) {
|
|
8447
8588
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
8448
|
-
const path =
|
|
8589
|
+
const path = join26(dir, entry.name);
|
|
8449
8590
|
if (entry.isDirectory()) {
|
|
8450
8591
|
await walkImportFiles(path, out);
|
|
8451
8592
|
} else if (entry.isFile() && isImportCandidate(path)) {
|
|
@@ -8458,9 +8599,9 @@ function isImportCandidate(path) {
|
|
|
8458
8599
|
return ext === ".md" || ext === ".txt";
|
|
8459
8600
|
}
|
|
8460
8601
|
async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
8461
|
-
const st = await
|
|
8602
|
+
const st = await stat4(file);
|
|
8462
8603
|
const sourceKind = classifyImportSource(file);
|
|
8463
|
-
const hash =
|
|
8604
|
+
const hash = createHash8("sha256").update(await readFile18(file)).digest("hex");
|
|
8464
8605
|
const baseEntry = {
|
|
8465
8606
|
source_path: file,
|
|
8466
8607
|
source_kind: sourceKind,
|
|
@@ -8483,7 +8624,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
|
8483
8624
|
reason: "policy_source_not_imported"
|
|
8484
8625
|
};
|
|
8485
8626
|
}
|
|
8486
|
-
const text = await
|
|
8627
|
+
const text = await readFile18(file, "utf8");
|
|
8487
8628
|
const extracted = extractImportText(text, sourceKind);
|
|
8488
8629
|
if (!extracted) {
|
|
8489
8630
|
return {
|
|
@@ -8512,8 +8653,8 @@ async function writeImportCapture(vault, entry, today) {
|
|
|
8512
8653
|
const content = hiddenString(entry, "__content");
|
|
8513
8654
|
const project = hiddenString(entry, "__project");
|
|
8514
8655
|
const relPath = await availableImportPath(vault, entry.proposed_path);
|
|
8515
|
-
const absPath =
|
|
8516
|
-
await mkdir7(
|
|
8656
|
+
const absPath = join26(vault, relPath);
|
|
8657
|
+
await mkdir7(join26(vault, "raw", "transcripts"), { recursive: true });
|
|
8517
8658
|
await writeFile5(absPath, renderImportCapture(entry, content, project, today), "utf8");
|
|
8518
8659
|
const validation = await runValidate({ file: absPath });
|
|
8519
8660
|
return {
|
|
@@ -8529,7 +8670,7 @@ async function availableImportPath(vault, proposed) {
|
|
|
8529
8670
|
const stem = proposed.slice(0, -ext.length);
|
|
8530
8671
|
let candidate = proposed;
|
|
8531
8672
|
let i = 2;
|
|
8532
|
-
while (await readIfExists2(
|
|
8673
|
+
while (await readIfExists2(join26(vault, candidate))) {
|
|
8533
8674
|
candidate = `${stem}-${i}${ext}`;
|
|
8534
8675
|
i++;
|
|
8535
8676
|
}
|
|
@@ -8714,10 +8855,10 @@ function memoryCacheRelPath(project) {
|
|
|
8714
8855
|
}
|
|
8715
8856
|
async function readMemoryCache(vault, project) {
|
|
8716
8857
|
if (project) {
|
|
8717
|
-
const projectCache = await readIfExists2(
|
|
8858
|
+
const projectCache = await readIfExists2(join26(vault, memoryCacheRelPath(project)));
|
|
8718
8859
|
if (projectCache) return projectCache;
|
|
8719
8860
|
}
|
|
8720
|
-
return readIfExists2(
|
|
8861
|
+
return readIfExists2(join26(vault, ".skillwiki", "memory-topics.json"));
|
|
8721
8862
|
}
|
|
8722
8863
|
function dedupePages(pages) {
|
|
8723
8864
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8808,8 +8949,8 @@ function slugify2(value) {
|
|
|
8808
8949
|
}
|
|
8809
8950
|
|
|
8810
8951
|
// src/commands/query.ts
|
|
8811
|
-
import { readFile as
|
|
8812
|
-
import { join as
|
|
8952
|
+
import { readFile as readFile19, stat as stat5 } from "fs/promises";
|
|
8953
|
+
import { join as join27 } from "path";
|
|
8813
8954
|
var W_KEYWORD = 2;
|
|
8814
8955
|
var W_SOURCE_OVERLAP = 4;
|
|
8815
8956
|
var W_WIKILINK = 3;
|
|
@@ -8875,8 +9016,22 @@ async function runQuery(input) {
|
|
|
8875
9016
|
type: page.type
|
|
8876
9017
|
};
|
|
8877
9018
|
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
|
|
8878
|
-
|
|
8879
|
-
|
|
9019
|
+
let pendingSources;
|
|
9020
|
+
if (input.includePending) {
|
|
9021
|
+
const { runSourcesPending } = await import("./sources-L2SQV63E.js");
|
|
9022
|
+
const pending = await runSourcesPending({
|
|
9023
|
+
vault: input.vault,
|
|
9024
|
+
match: input.text,
|
|
9025
|
+
limit: input.limit ?? 10
|
|
9026
|
+
});
|
|
9027
|
+
pendingSources = pending.result.ok ? pending.result.data.items : [];
|
|
9028
|
+
}
|
|
9029
|
+
const humanHint = results.length === 0 ? pendingSources && pendingSources.length > 0 ? `no matching typed pages found
|
|
9030
|
+
${pendingSources.length} matching pending source(s)` : "no matching pages found" : results.map((r) => `${r.path} (score: ${r.score})`).join("\n");
|
|
9031
|
+
return {
|
|
9032
|
+
exitCode: ExitCode.OK,
|
|
9033
|
+
result: ok({ results, ...pendingSources ? { pending_sources: pendingSources } : {}, humanHint })
|
|
9034
|
+
};
|
|
8880
9035
|
}
|
|
8881
9036
|
function scoreSourceOverlap(page, allPages, seedPaths) {
|
|
8882
9037
|
if (page.sources.length === 0) return 0;
|
|
@@ -8930,10 +9085,10 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
8930
9085
|
return score;
|
|
8931
9086
|
}
|
|
8932
9087
|
async function loadOrBuildGraph(vault) {
|
|
8933
|
-
const graphPath =
|
|
9088
|
+
const graphPath = join27(vault, ".skillwiki", "graph.json");
|
|
8934
9089
|
let needsBuild = false;
|
|
8935
9090
|
try {
|
|
8936
|
-
const fileStat = await
|
|
9091
|
+
const fileStat = await stat5(graphPath);
|
|
8937
9092
|
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
8938
9093
|
if (ageHours > 24) needsBuild = true;
|
|
8939
9094
|
} catch {
|
|
@@ -8944,7 +9099,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
8944
9099
|
if (buildResult.exitCode !== 0) return null;
|
|
8945
9100
|
}
|
|
8946
9101
|
try {
|
|
8947
|
-
const raw = await
|
|
9102
|
+
const raw = await readFile19(graphPath, "utf8");
|
|
8948
9103
|
return JSON.parse(raw);
|
|
8949
9104
|
} catch {
|
|
8950
9105
|
return null;
|
|
@@ -8959,7 +9114,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
8959
9114
|
import { z } from "zod";
|
|
8960
9115
|
|
|
8961
9116
|
// src/mcp/vault-resolve.ts
|
|
8962
|
-
import { join as
|
|
9117
|
+
import { join as join28, resolve as resolve7 } from "path";
|
|
8963
9118
|
|
|
8964
9119
|
// src/mcp/allowlist.ts
|
|
8965
9120
|
import { resolve as resolve6, sep as sep4 } from "path";
|
|
@@ -9021,7 +9176,7 @@ async function resolveMcpVault(input) {
|
|
|
9021
9176
|
return ok({ vault: vaultPath, source });
|
|
9022
9177
|
}
|
|
9023
9178
|
function defaultGraphOut(vault) {
|
|
9024
|
-
return
|
|
9179
|
+
return join28(vault, ".skillwiki", "graph.json");
|
|
9025
9180
|
}
|
|
9026
9181
|
|
|
9027
9182
|
// src/mcp/result-format.ts
|
|
@@ -9038,7 +9193,7 @@ function formatToolResult(payload) {
|
|
|
9038
9193
|
// src/mcp/audit-log.ts
|
|
9039
9194
|
import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
9040
9195
|
import { homedir } from "os";
|
|
9041
|
-
import { join as
|
|
9196
|
+
import { join as join29 } from "path";
|
|
9042
9197
|
function auditEnabled() {
|
|
9043
9198
|
const v = process.env.SKILLWIKI_MCP_AUDIT;
|
|
9044
9199
|
if (v === "0" || v === "false") return false;
|
|
@@ -9050,7 +9205,7 @@ function auditSink() {
|
|
|
9050
9205
|
function auditFilePath() {
|
|
9051
9206
|
const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
|
|
9052
9207
|
if (custom && custom.length > 0) return custom;
|
|
9053
|
-
return
|
|
9208
|
+
return join29(homedir(), ".skillwiki", "mcp-audit.jsonl");
|
|
9054
9209
|
}
|
|
9055
9210
|
function auditMcpToolCall(entry) {
|
|
9056
9211
|
if (!auditEnabled()) return;
|
|
@@ -9060,7 +9215,7 @@ function auditMcpToolCall(entry) {
|
|
|
9060
9215
|
return;
|
|
9061
9216
|
}
|
|
9062
9217
|
const path = auditFilePath();
|
|
9063
|
-
mkdirSync5(
|
|
9218
|
+
mkdirSync5(join29(path, ".."), { recursive: true });
|
|
9064
9219
|
appendFileSync(path, line, "utf8");
|
|
9065
9220
|
}
|
|
9066
9221
|
async function runMcpToolHandler(tool, input, fn) {
|
|
@@ -9280,8 +9435,8 @@ function registerMcpMutatingTools(server) {
|
|
|
9280
9435
|
}
|
|
9281
9436
|
|
|
9282
9437
|
// src/mcp/resources.ts
|
|
9283
|
-
import { readFile as
|
|
9284
|
-
import { join as
|
|
9438
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
9439
|
+
import { join as join31 } from "path";
|
|
9285
9440
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9286
9441
|
|
|
9287
9442
|
// src/mcp/lint-bucket.ts
|
|
@@ -9409,8 +9564,8 @@ async function fetchQueryPreview(input) {
|
|
|
9409
9564
|
}
|
|
9410
9565
|
|
|
9411
9566
|
// src/mcp/graph-html.ts
|
|
9412
|
-
import { readFile as
|
|
9413
|
-
import { join as
|
|
9567
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
9568
|
+
import { join as join30 } from "path";
|
|
9414
9569
|
import { existsSync as existsSync16 } from "fs";
|
|
9415
9570
|
var TYPE_COLORS = {
|
|
9416
9571
|
entities: "#e74c3c",
|
|
@@ -9479,7 +9634,7 @@ ${nodeSvg}
|
|
|
9479
9634
|
return { html, node_count: nodes.length, edge_count: edges.length, truncated };
|
|
9480
9635
|
}
|
|
9481
9636
|
async function fetchGraphHtmlReport(input) {
|
|
9482
|
-
const graphPath = input.graphPath ??
|
|
9637
|
+
const graphPath = input.graphPath ?? join30(input.vault, ".skillwiki", "graph.json");
|
|
9483
9638
|
const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
|
|
9484
9639
|
if (!existsSync16(graphPath)) {
|
|
9485
9640
|
return {
|
|
@@ -9489,7 +9644,7 @@ async function fetchGraphHtmlReport(input) {
|
|
|
9489
9644
|
}
|
|
9490
9645
|
let raw;
|
|
9491
9646
|
try {
|
|
9492
|
-
raw = await
|
|
9647
|
+
raw = await readFile20(graphPath, "utf8");
|
|
9493
9648
|
} catch (e) {
|
|
9494
9649
|
return {
|
|
9495
9650
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -9553,7 +9708,7 @@ async function fetchStaleSummary(input) {
|
|
|
9553
9708
|
|
|
9554
9709
|
// src/mcp/resources.ts
|
|
9555
9710
|
async function readVaultFile(vault, rel) {
|
|
9556
|
-
return
|
|
9711
|
+
return readFile21(join31(vault, rel), "utf8");
|
|
9557
9712
|
}
|
|
9558
9713
|
async function tailLines(text, lines) {
|
|
9559
9714
|
const parts = text.split(/\r?\n/);
|
|
@@ -9639,9 +9794,9 @@ function registerMcpResources(server) {
|
|
|
9639
9794
|
if (!v.ok) {
|
|
9640
9795
|
return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
|
|
9641
9796
|
}
|
|
9642
|
-
const path =
|
|
9797
|
+
const path = join31(v.data.vault, ".skillwiki", "graph.json");
|
|
9643
9798
|
try {
|
|
9644
|
-
const raw = await
|
|
9799
|
+
const raw = await readFile21(path, "utf8");
|
|
9645
9800
|
const graph = JSON.parse(raw);
|
|
9646
9801
|
const adjacency = graph.adjacency ?? {};
|
|
9647
9802
|
const nodes = Object.keys(adjacency);
|
|
@@ -9954,11 +10109,6 @@ export {
|
|
|
9954
10109
|
readLastOp,
|
|
9955
10110
|
appendLastOp,
|
|
9956
10111
|
clearLastOp,
|
|
9957
|
-
eventPathFor,
|
|
9958
|
-
validateLogEvent,
|
|
9959
|
-
canonicalEventJson,
|
|
9960
|
-
writeLogEvent,
|
|
9961
|
-
readLogEvents,
|
|
9962
10112
|
runLogAppend,
|
|
9963
10113
|
renderIndexUpsert,
|
|
9964
10114
|
upsertIndexEntry,
|
|
@@ -9974,7 +10124,6 @@ export {
|
|
|
9974
10124
|
resolveInitTimePath,
|
|
9975
10125
|
resolveRuntimePath,
|
|
9976
10126
|
runOrphans,
|
|
9977
|
-
extractCitationMarkers,
|
|
9978
10127
|
runAudit,
|
|
9979
10128
|
findPlugin,
|
|
9980
10129
|
extractTaxonomy,
|
|
@@ -9986,6 +10135,8 @@ export {
|
|
|
9986
10135
|
runIndexCheck,
|
|
9987
10136
|
renderProjectIndex,
|
|
9988
10137
|
runProjectIndex,
|
|
10138
|
+
planRawStructuralMove,
|
|
10139
|
+
applyRawStructuralMove,
|
|
9989
10140
|
runStale,
|
|
9990
10141
|
runPagesize,
|
|
9991
10142
|
runLogRotate,
|
|
@@ -9996,8 +10147,10 @@ export {
|
|
|
9996
10147
|
buildRemoteObjectPath,
|
|
9997
10148
|
isValidRemoteDeleteCap,
|
|
9998
10149
|
planAndMaybePruneRemoteObjects,
|
|
9999
|
-
runDedup,
|
|
10000
10150
|
safeWritePage,
|
|
10151
|
+
rewriteRawSourceReferences,
|
|
10152
|
+
snapshotMaintainedPageState,
|
|
10153
|
+
runDedup,
|
|
10001
10154
|
runFrontmatterFix,
|
|
10002
10155
|
fixPathTooLong,
|
|
10003
10156
|
assessSourceIdentity,
|