skillwiki 0.10.21 → 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-65Q5UGND.js → chunk-BASWDOQB.js} +2 -2
- package/dist/{chunk-Y6KRDGI2.js → chunk-C2DKFJFA.js} +6 -1
- package/dist/chunk-EQPU2BPM.js +468 -0
- package/dist/{chunk-GNS2ZV5P.js → chunk-PQG26AGJ.js} +1 -1
- package/dist/{chunk-I5JD3BQZ.js → chunk-QBZYEEBD.js} +13 -296
- package/dist/chunk-QNTBNNEL.js +616 -0
- package/dist/{chunk-UNPZDCWN.js → chunk-SYKSL3JQ.js} +860 -700
- package/dist/cli.js +2334 -273
- package/dist/{index-projection-HAXDEM2F.js → index-projection-FRWTZB5Y.js} +3 -2
- package/dist/{managed-write-preflight-CTXX2MHQ.js → managed-write-preflight-4LPVMP42.js} +3 -3
- package/dist/skillwiki-mcp.js +6 -4
- 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/proj-decide/SKILL.md +35 -5
- package/skills/skills/proj-decide/SKILL.md +35 -5
- 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 +39 -54
- 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 +39 -54
- 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,
|
|
@@ -34,7 +24,40 @@ import {
|
|
|
34
24
|
satelliteGateFromFleetLoad,
|
|
35
25
|
snapshotterAliasForLocalHost,
|
|
36
26
|
writeDotenv
|
|
37
|
-
} from "./chunk-
|
|
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,
|
|
@@ -49,12 +72,12 @@ import {
|
|
|
49
72
|
ok,
|
|
50
73
|
parseVaultSyncKeyValue,
|
|
51
74
|
systemdPropertyFor
|
|
52
|
-
} from "./chunk-
|
|
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,25 +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:${
|
|
203
|
+
`<!-- skillwiki-log-op:${operationId2} -->`,
|
|
204
|
+
`<!-- skillwiki-page-publish:${operationId2} -->`,
|
|
205
|
+
`<!-- skillwiki-project-page-publish:${operationId2} -->`
|
|
319
206
|
]);
|
|
320
207
|
}
|
|
321
|
-
function preferredMarker(
|
|
208
|
+
function preferredMarker(operationId2, eventKind) {
|
|
322
209
|
if (!eventKind || eventKind === "page-publish") {
|
|
323
|
-
return `<!-- skillwiki-page-publish:${
|
|
210
|
+
return `<!-- skillwiki-page-publish:${operationId2} -->`;
|
|
211
|
+
}
|
|
212
|
+
if (eventKind === "project-page-publish") {
|
|
213
|
+
return `<!-- skillwiki-project-page-publish:${operationId2} -->`;
|
|
324
214
|
}
|
|
325
|
-
return `<!-- skillwiki-log-op:${
|
|
215
|
+
return `<!-- skillwiki-log-op:${operationId2} -->`;
|
|
326
216
|
}
|
|
327
217
|
async function appendWhileLocked(logPath, content, markers, writeMarker) {
|
|
328
218
|
let logText;
|
|
329
219
|
try {
|
|
330
|
-
logText = await
|
|
220
|
+
logText = await readFile(logPath, "utf8");
|
|
331
221
|
} catch {
|
|
332
222
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
333
223
|
}
|
|
@@ -366,7 +256,7 @@ ${appendedContent}
|
|
|
366
256
|
}
|
|
367
257
|
async function runLogAppend(input) {
|
|
368
258
|
try {
|
|
369
|
-
await stat(
|
|
259
|
+
await stat(join3(input.vault, "SCHEMA.md"));
|
|
370
260
|
} catch {
|
|
371
261
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
372
262
|
}
|
|
@@ -439,7 +329,7 @@ async function runLogAppend(input) {
|
|
|
439
329
|
return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
|
|
440
330
|
}
|
|
441
331
|
const lockHandle = acquired.data;
|
|
442
|
-
const logPath =
|
|
332
|
+
const logPath = join3(input.vault, "log.md");
|
|
443
333
|
let outcome;
|
|
444
334
|
let released;
|
|
445
335
|
try {
|
|
@@ -490,7 +380,7 @@ async function runLogAppend(input) {
|
|
|
490
380
|
|
|
491
381
|
// src/utils/sync-lock.ts
|
|
492
382
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
493
|
-
import { join as
|
|
383
|
+
import { join as join4 } from "path";
|
|
494
384
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
495
385
|
function getEnvSessionId() {
|
|
496
386
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
@@ -513,7 +403,7 @@ function getCliSessionId(cwd) {
|
|
|
513
403
|
return `cli-${getCwdHash(cwd)}`;
|
|
514
404
|
}
|
|
515
405
|
function lockPath(vault) {
|
|
516
|
-
return
|
|
406
|
+
return join4(vault, ".skillwiki", "sync.lock");
|
|
517
407
|
}
|
|
518
408
|
function readLock(vault) {
|
|
519
409
|
const path = lockPath(vault);
|
|
@@ -532,7 +422,7 @@ function isStale(lock, now) {
|
|
|
532
422
|
}
|
|
533
423
|
function acquireLock(vault, opts = {}) {
|
|
534
424
|
const path = lockPath(vault);
|
|
535
|
-
const dir =
|
|
425
|
+
const dir = join4(vault, ".skillwiki");
|
|
536
426
|
if (!existsSync3(dir)) {
|
|
537
427
|
mkdirSync3(dir, { recursive: true });
|
|
538
428
|
}
|
|
@@ -617,7 +507,7 @@ function acquireOwnedSyncLock(vault, opts) {
|
|
|
617
507
|
};
|
|
618
508
|
const path = lockPath(vault);
|
|
619
509
|
try {
|
|
620
|
-
mkdirSync3(
|
|
510
|
+
mkdirSync3(join4(vault, ".skillwiki"), { recursive: true });
|
|
621
511
|
} catch (error) {
|
|
622
512
|
return err("WRITE_FAILED", { path, message: String(error) });
|
|
623
513
|
}
|
|
@@ -649,12 +539,12 @@ function releaseOwnedSyncLock(handle) {
|
|
|
649
539
|
|
|
650
540
|
// src/commands/validate.ts
|
|
651
541
|
import { createHash as createHash2 } from "crypto";
|
|
652
|
-
import { readFile as
|
|
542
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
653
543
|
import { resolve, relative, sep } from "path";
|
|
654
544
|
|
|
655
545
|
// src/utils/index-entry.ts
|
|
656
|
-
import { readFile as
|
|
657
|
-
import { join as
|
|
546
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
547
|
+
import { join as join5 } from "path";
|
|
658
548
|
var TYPE_SECTION = {
|
|
659
549
|
entity: "Entities",
|
|
660
550
|
concept: "Concepts",
|
|
@@ -698,10 +588,10 @@ function renderIndexUpsert(text, input) {
|
|
|
698
588
|
});
|
|
699
589
|
}
|
|
700
590
|
async function upsertIndexEntry(input) {
|
|
701
|
-
const path =
|
|
591
|
+
const path = join5(input.vault, "index.md");
|
|
702
592
|
let before = "";
|
|
703
593
|
try {
|
|
704
|
-
before = await
|
|
594
|
+
before = await readFile2(path, "utf8");
|
|
705
595
|
} catch {
|
|
706
596
|
before = "";
|
|
707
597
|
}
|
|
@@ -736,7 +626,7 @@ var SCHEMAS = {
|
|
|
736
626
|
async function runValidate(input) {
|
|
737
627
|
let text;
|
|
738
628
|
try {
|
|
739
|
-
text = await
|
|
629
|
+
text = await readFile3(input.file, "utf8");
|
|
740
630
|
} catch {
|
|
741
631
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
742
632
|
}
|
|
@@ -793,7 +683,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
793
683
|
if (relPath.startsWith("..")) {
|
|
794
684
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: `file ${input.file} is not inside vault ${input.vault}` }) };
|
|
795
685
|
}
|
|
796
|
-
const
|
|
686
|
+
const operationId2 = createHash2("sha256").update("skillwiki-validate-apply-v1\0").update(relPath).update("\0").update(text).digest("hex");
|
|
797
687
|
if (det.schema === "typed-knowledge" || det.schema === "meta") {
|
|
798
688
|
const prepared = prepareTypedPage(text, relPath);
|
|
799
689
|
if (!prepared.ok) {
|
|
@@ -848,7 +738,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
848
738
|
const logged = await runLogAppend({
|
|
849
739
|
vault: input.vault,
|
|
850
740
|
content: `## [${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}] validate | added: ${relPath}`,
|
|
851
|
-
operationId,
|
|
741
|
+
operationId: operationId2,
|
|
852
742
|
strictLock: true
|
|
853
743
|
});
|
|
854
744
|
if (!logged.result.ok) {
|
|
@@ -876,7 +766,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
876
766
|
}
|
|
877
767
|
|
|
878
768
|
// src/commands/graph.ts
|
|
879
|
-
import { writeFile, mkdir
|
|
769
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
880
770
|
import { dirname } from "path";
|
|
881
771
|
|
|
882
772
|
// src/parsers/wikilinks.ts
|
|
@@ -1031,7 +921,7 @@ async function runGraphBuild(input) {
|
|
|
1031
921
|
const adamicAdar = computeAdamicAdar(adjacency);
|
|
1032
922
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
1033
923
|
try {
|
|
1034
|
-
await
|
|
924
|
+
await mkdir(dirname(input.out), { recursive: true });
|
|
1035
925
|
await writeFile(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
|
|
1036
926
|
} catch (e) {
|
|
1037
927
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
@@ -1075,7 +965,7 @@ function computeAdamicAdar(adj) {
|
|
|
1075
965
|
}
|
|
1076
966
|
|
|
1077
967
|
// src/utils/wiki-path.ts
|
|
1078
|
-
import { join as
|
|
968
|
+
import { join as join6 } from "path";
|
|
1079
969
|
async function resolveInitTimePath(input) {
|
|
1080
970
|
const chain = [];
|
|
1081
971
|
if (input.flag !== void 0 && input.flag.length > 0) {
|
|
@@ -1088,27 +978,27 @@ async function resolveInitTimePath(input) {
|
|
|
1088
978
|
return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
|
|
1089
979
|
}
|
|
1090
980
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
1091
|
-
const sw = await parseDotenvFile(
|
|
981
|
+
const sw = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
|
|
1092
982
|
if (sw.WIKI_PATH !== void 0) {
|
|
1093
983
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
|
|
1094
984
|
return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
|
|
1095
985
|
}
|
|
1096
986
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
1097
|
-
const hermes = await parseDotenvFile(
|
|
987
|
+
const hermes = await parseDotenvFile(join6(input.home, ".hermes", ".env"));
|
|
1098
988
|
if (hermes.WIKI_PATH !== void 0) {
|
|
1099
989
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
|
|
1100
990
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
1101
991
|
}
|
|
1102
992
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
|
|
1103
993
|
if (input.cwd) {
|
|
1104
|
-
const projCfg = await parseDotenvFile(
|
|
994
|
+
const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
|
|
1105
995
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
1106
996
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
1107
997
|
return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
|
|
1108
998
|
}
|
|
1109
999
|
}
|
|
1110
1000
|
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
1111
|
-
const fallback =
|
|
1001
|
+
const fallback = join6(input.home, "wiki");
|
|
1112
1002
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
1113
1003
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
1114
1004
|
}
|
|
@@ -1119,7 +1009,7 @@ async function resolveRuntimePath(input) {
|
|
|
1119
1009
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
1120
1010
|
}
|
|
1121
1011
|
if (input.explain) chain.push({ source: "flag", matched: false });
|
|
1122
|
-
const swGlobal = await parseDotenvFile(
|
|
1012
|
+
const swGlobal = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
|
|
1123
1013
|
const wikiName = input.wiki;
|
|
1124
1014
|
if (wikiName !== void 0 && wikiName.length > 0) {
|
|
1125
1015
|
if (wikiName.toLowerCase() === "default") {
|
|
@@ -1163,7 +1053,7 @@ async function resolveRuntimePath(input) {
|
|
|
1163
1053
|
}
|
|
1164
1054
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
1165
1055
|
if (input.cwd) {
|
|
1166
|
-
const projCfg = await parseDotenvFile(
|
|
1056
|
+
const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
|
|
1167
1057
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
1168
1058
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
1169
1059
|
return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
|
|
@@ -1283,156 +1173,12 @@ function simulateRemoval(adj, removed) {
|
|
|
1283
1173
|
}
|
|
1284
1174
|
|
|
1285
1175
|
// src/commands/audit.ts
|
|
1286
|
-
import { readFile as
|
|
1287
|
-
import { dirname as dirname2, resolve as resolve2, join as
|
|
1288
|
-
|
|
1289
|
-
// src/parsers/citations.ts
|
|
1290
|
-
var FENCE2 = /```[\s\S]*?```/g;
|
|
1291
|
-
var INLINE_CODE = /``[^`\n]+``|`[^`\n]+`/g;
|
|
1292
|
-
var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
|
|
1293
|
-
var FRONTMATTER = /^---\n[\s\S]*?\n---\n?/;
|
|
1294
|
-
function stripFences(body) {
|
|
1295
|
-
return body.replace(FENCE2, "").replace(INLINE_CODE, "");
|
|
1296
|
-
}
|
|
1297
|
-
var TILDE_FENCE = /~~~[\s\S]*?~~~/g;
|
|
1298
|
-
function stripFencedBlocks(body) {
|
|
1299
|
-
return body.replace(FENCE2, "").replace(TILDE_FENCE, "");
|
|
1300
|
-
}
|
|
1301
|
-
function extractCitationMarkers(body) {
|
|
1302
|
-
const stripped = stripFences(body);
|
|
1303
|
-
const out = [];
|
|
1304
|
-
let m;
|
|
1305
|
-
while ((m = MARKER_RE.exec(stripped)) !== null) {
|
|
1306
|
-
if (m[1] === "raw/...") continue;
|
|
1307
|
-
out.push({ marker: m[0], target: m[1] });
|
|
1308
|
-
}
|
|
1309
|
-
return out;
|
|
1310
|
-
}
|
|
1311
|
-
function hasSourcesFooter(body) {
|
|
1312
|
-
return /^## Sources\s*$/m.test(stripFencedBlocks(body));
|
|
1313
|
-
}
|
|
1314
|
-
function isLegacyCitationStyle(body) {
|
|
1315
|
-
const markers = extractCitationMarkers(body);
|
|
1316
|
-
if (markers.length === 0) return false;
|
|
1317
|
-
if (!hasSourcesFooter(body)) return true;
|
|
1318
|
-
const lines = stripFences(body.replace(FRONTMATTER, "")).split("\n");
|
|
1319
|
-
let inSources = false;
|
|
1320
|
-
let lastNonBlankWasTable = false;
|
|
1321
|
-
for (const line of lines) {
|
|
1322
|
-
if (/^## Sources\b/.test(line.trim())) {
|
|
1323
|
-
inSources = true;
|
|
1324
|
-
continue;
|
|
1325
|
-
}
|
|
1326
|
-
if (inSources) continue;
|
|
1327
|
-
const matches = [...line.matchAll(MARKER_RE)];
|
|
1328
|
-
if (matches.length === 0) {
|
|
1329
|
-
if (line.trim().length > 0) lastNonBlankWasTable = /^\|/.test(line.trim());
|
|
1330
|
-
continue;
|
|
1331
|
-
}
|
|
1332
|
-
const markerOnly = line.replace(MARKER_RE, "").trim();
|
|
1333
|
-
if (markerOnly.length === 0 && !lastNonBlankWasTable) return true;
|
|
1334
|
-
lastNonBlankWasTable = false;
|
|
1335
|
-
const lastMatch = matches[matches.length - 1];
|
|
1336
|
-
const afterLast = line.slice(lastMatch.index + lastMatch[0].length).replace(MARKER_RE, "").trim();
|
|
1337
|
-
if (afterLast.length > 0) return true;
|
|
1338
|
-
const beforeFirst = line.slice(0, matches[0].index).trim();
|
|
1339
|
-
if (beforeFirst.length > 0 && !/[.!?]["'"]*\s*$/.test(beforeFirst)) return true;
|
|
1340
|
-
}
|
|
1341
|
-
return false;
|
|
1342
|
-
}
|
|
1343
|
-
function hasOrphanedCitations(body) {
|
|
1344
|
-
const noFm = body.replace(FRONTMATTER, "");
|
|
1345
|
-
const stripped = stripFences(noFm);
|
|
1346
|
-
const lines = stripped.split("\n");
|
|
1347
|
-
const rawLines = noFm.split("\n");
|
|
1348
|
-
let inSources = false;
|
|
1349
|
-
let sourcesEnded = false;
|
|
1350
|
-
let sourcesStartLine = -1;
|
|
1351
|
-
let lastNonBlankInSources = -1;
|
|
1352
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1353
|
-
const line = lines[i];
|
|
1354
|
-
const trimmed = line.trim();
|
|
1355
|
-
if (/^## Sources\b/.test(trimmed)) {
|
|
1356
|
-
inSources = true;
|
|
1357
|
-
sourcesStartLine = i;
|
|
1358
|
-
continue;
|
|
1359
|
-
}
|
|
1360
|
-
if (!inSources || sourcesEnded) continue;
|
|
1361
|
-
if (trimmed.length === 0) {
|
|
1362
|
-
if (lastNonBlankInSources >= 0) {
|
|
1363
|
-
sourcesEnded = true;
|
|
1364
|
-
}
|
|
1365
|
-
continue;
|
|
1366
|
-
}
|
|
1367
|
-
const isListItem = /^\s*(?:[-*]|\d+\.)\s+/.test(line);
|
|
1368
|
-
const hasMarker = /\^\[raw\//.test(line);
|
|
1369
|
-
const hasBacktickRawPath = /`raw\/[^`]+`/.test(rawLines[i]);
|
|
1370
|
-
if (isListItem && hasMarker) {
|
|
1371
|
-
lastNonBlankInSources = i;
|
|
1372
|
-
} else if (isListItem && hasBacktickRawPath) {
|
|
1373
|
-
lastNonBlankInSources = i;
|
|
1374
|
-
} else if (hasMarker && !isListItem) {
|
|
1375
|
-
return true;
|
|
1376
|
-
} else {
|
|
1377
|
-
sourcesEnded = true;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
if (sourcesStartLine === -1) return false;
|
|
1381
|
-
if (sourcesEnded) {
|
|
1382
|
-
const scanStart = Math.max(lastNonBlankInSources + 1, sourcesStartLine + 1);
|
|
1383
|
-
for (let i = scanStart; i < lines.length; i++) {
|
|
1384
|
-
if (/\^\[raw\//.test(lines[i])) {
|
|
1385
|
-
return true;
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
return false;
|
|
1390
|
-
}
|
|
1391
|
-
function hasWikilinkCitations(body) {
|
|
1392
|
-
const stripped = stripFences(body);
|
|
1393
|
-
return /\[\[raw\/[^\]]+\]\]/.test(stripped);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// src/utils/raw-source.ts
|
|
1397
|
-
import { existsSync as existsSync4 } from "fs";
|
|
1398
|
-
import { stat as stat2 } from "fs/promises";
|
|
1399
|
-
import { join as join8 } from "path";
|
|
1400
|
-
function normalizeRawSourceTarget(entry) {
|
|
1401
|
-
let target = entry.trim().replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
1402
|
-
target = target.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
1403
|
-
if (!target.startsWith("raw/") && !target.startsWith("_archive/raw/")) return null;
|
|
1404
|
-
return target;
|
|
1405
|
-
}
|
|
1406
|
-
function rawSourceTargetCandidates(vault, target) {
|
|
1407
|
-
const normalized = normalizeRawSourceTarget(target);
|
|
1408
|
-
if (!normalized) return [];
|
|
1409
|
-
const candidates = [join8(vault, normalized)];
|
|
1410
|
-
if (!normalized.endsWith(".md")) candidates.push(join8(vault, `${normalized}.md`));
|
|
1411
|
-
if (normalized.startsWith("raw/")) {
|
|
1412
|
-
candidates.push(join8(vault, "_archive", normalized));
|
|
1413
|
-
if (!normalized.endsWith(".md")) candidates.push(join8(vault, "_archive", `${normalized}.md`));
|
|
1414
|
-
}
|
|
1415
|
-
return [...new Set(candidates)];
|
|
1416
|
-
}
|
|
1417
|
-
function rawSourceTargetExistsSync(vault, target) {
|
|
1418
|
-
return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync4(candidate));
|
|
1419
|
-
}
|
|
1420
|
-
async function rawSourceTargetExists(vault, target) {
|
|
1421
|
-
for (const candidate of rawSourceTargetCandidates(vault, target)) {
|
|
1422
|
-
try {
|
|
1423
|
-
await stat2(candidate);
|
|
1424
|
-
return true;
|
|
1425
|
-
} catch {
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// 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";
|
|
1432
1178
|
async function runAudit(input) {
|
|
1433
1179
|
let text;
|
|
1434
1180
|
try {
|
|
1435
|
-
text = await
|
|
1181
|
+
text = await readFile4(input.file, "utf8");
|
|
1436
1182
|
} catch {
|
|
1437
1183
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
1438
1184
|
}
|
|
@@ -1485,7 +1231,7 @@ async function findVaultRoot(start) {
|
|
|
1485
1231
|
let cur = start;
|
|
1486
1232
|
for (let i = 0; i < 20; i++) {
|
|
1487
1233
|
try {
|
|
1488
|
-
await
|
|
1234
|
+
await stat2(join7(cur, "SCHEMA.md"));
|
|
1489
1235
|
return cur;
|
|
1490
1236
|
} catch {
|
|
1491
1237
|
}
|
|
@@ -1577,8 +1323,8 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
|
|
|
1577
1323
|
}
|
|
1578
1324
|
|
|
1579
1325
|
// src/commands/tag-audit.ts
|
|
1580
|
-
import { readFile as
|
|
1581
|
-
import { join as
|
|
1326
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1327
|
+
import { join as join8 } from "path";
|
|
1582
1328
|
|
|
1583
1329
|
// src/parsers/taxonomy.ts
|
|
1584
1330
|
import yaml from "js-yaml";
|
|
@@ -1603,12 +1349,12 @@ function parseTaxonomyDocument(schemaText) {
|
|
|
1603
1349
|
const nextHeading = /^#{1,2}[ \t]+/m.exec(unboundedTail);
|
|
1604
1350
|
const sectionEnd = nextHeading?.index === void 0 ? schemaText.length : afterHeading + nextHeading.index;
|
|
1605
1351
|
const sectionText = schemaText.slice(afterHeading, sectionEnd);
|
|
1606
|
-
const
|
|
1607
|
-
if (!
|
|
1352
|
+
const open = /^```yaml[ \t]*\r?$/m.exec(sectionText);
|
|
1353
|
+
if (!open || open.index === void 0) {
|
|
1608
1354
|
return err("NO_TAXONOMY_BLOCK", { message: "Fenced YAML taxonomy block not found" });
|
|
1609
1355
|
}
|
|
1610
|
-
const openStart = afterHeading +
|
|
1611
|
-
const yamlStart = openStart +
|
|
1356
|
+
const openStart = afterHeading + open.index;
|
|
1357
|
+
const yamlStart = openStart + open[0].length + 1;
|
|
1612
1358
|
const afterOpen = schemaText.slice(yamlStart, sectionEnd);
|
|
1613
1359
|
const close = /^```[ \t]*\r?$/m.exec(afterOpen);
|
|
1614
1360
|
if (!close || close.index === void 0) {
|
|
@@ -1731,7 +1477,7 @@ async function runTagAudit(input) {
|
|
|
1731
1477
|
const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
|
|
1732
1478
|
if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
|
|
1733
1479
|
const scan = scanResult.data;
|
|
1734
|
-
const schemaText = await
|
|
1480
|
+
const schemaText = await readFile5(join8(input.vault, "SCHEMA.md"), "utf8");
|
|
1735
1481
|
const tax = extractTaxonomy(schemaText);
|
|
1736
1482
|
if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
|
|
1737
1483
|
const allowed = new Set(tax.data);
|
|
@@ -1763,8 +1509,8 @@ async function runTagAudit(input) {
|
|
|
1763
1509
|
}
|
|
1764
1510
|
|
|
1765
1511
|
// src/commands/index-check.ts
|
|
1766
|
-
import { readFile as
|
|
1767
|
-
import { join as
|
|
1512
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1513
|
+
import { join as join9 } from "path";
|
|
1768
1514
|
function normalizeIndexTarget(raw) {
|
|
1769
1515
|
return raw.replace(/\.md$/, "").replace(/^\.?\//, "");
|
|
1770
1516
|
}
|
|
@@ -1776,7 +1522,7 @@ async function runIndexCheck(input) {
|
|
|
1776
1522
|
}
|
|
1777
1523
|
let indexText = "";
|
|
1778
1524
|
try {
|
|
1779
|
-
indexText = await
|
|
1525
|
+
indexText = await readFile6(join9(input.vault, "index.md"), "utf8");
|
|
1780
1526
|
} catch {
|
|
1781
1527
|
}
|
|
1782
1528
|
const indexTargets = /* @__PURE__ */ new Set();
|
|
@@ -1830,20 +1576,20 @@ async function runIndexCheck(input) {
|
|
|
1830
1576
|
}
|
|
1831
1577
|
|
|
1832
1578
|
// src/commands/project-index.ts
|
|
1833
|
-
import { readdir
|
|
1834
|
-
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";
|
|
1835
1581
|
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
1836
1582
|
var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
|
|
1837
1583
|
async function scanMarkdownTree(rootAbs, rootRel) {
|
|
1838
1584
|
const found = [];
|
|
1839
1585
|
let entries;
|
|
1840
1586
|
try {
|
|
1841
|
-
entries = await
|
|
1587
|
+
entries = await readdir(rootAbs, { withFileTypes: true });
|
|
1842
1588
|
} catch {
|
|
1843
1589
|
return found;
|
|
1844
1590
|
}
|
|
1845
1591
|
for (const entry of entries) {
|
|
1846
|
-
const abs =
|
|
1592
|
+
const abs = join10(rootAbs, entry.name);
|
|
1847
1593
|
const rel = `${rootRel}/${entry.name}`;
|
|
1848
1594
|
if (entry.isDirectory()) {
|
|
1849
1595
|
found.push(...await scanMarkdownTree(abs, rel));
|
|
@@ -1872,23 +1618,23 @@ function projectLocalType(slug, page, data) {
|
|
|
1872
1618
|
return typeof data.type === "string" ? data.type : "project";
|
|
1873
1619
|
}
|
|
1874
1620
|
async function renderProjectIndex(vault, slug, opts = {}) {
|
|
1875
|
-
const projectDir =
|
|
1621
|
+
const projectDir = join10(vault, "projects", slug);
|
|
1876
1622
|
try {
|
|
1877
|
-
await
|
|
1623
|
+
await readdir(projectDir);
|
|
1878
1624
|
} catch {
|
|
1879
1625
|
return err("PROJECT_NOT_FOUND", { slug, path: projectDir });
|
|
1880
1626
|
}
|
|
1881
1627
|
const wikilinkPattern = `[[${slug}]]`;
|
|
1882
1628
|
const entries = [];
|
|
1883
|
-
const compoundDir =
|
|
1629
|
+
const compoundDir = join10(vault, "projects", slug, "compound");
|
|
1884
1630
|
try {
|
|
1885
|
-
const compoundFiles = await
|
|
1631
|
+
const compoundFiles = await readdir(compoundDir, { withFileTypes: true });
|
|
1886
1632
|
for (const entry of compoundFiles) {
|
|
1887
1633
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1888
|
-
const filePath =
|
|
1634
|
+
const filePath = join10(compoundDir, entry.name);
|
|
1889
1635
|
let text;
|
|
1890
1636
|
try {
|
|
1891
|
-
text = await
|
|
1637
|
+
text = await readFile7(filePath, "utf8");
|
|
1892
1638
|
} catch {
|
|
1893
1639
|
continue;
|
|
1894
1640
|
}
|
|
@@ -1905,16 +1651,16 @@ async function renderProjectIndex(vault, slug, opts = {}) {
|
|
|
1905
1651
|
for (const dir of LAYER2_DIRS) {
|
|
1906
1652
|
let files;
|
|
1907
1653
|
try {
|
|
1908
|
-
files = await
|
|
1654
|
+
files = await readdir(join10(vault, dir), { withFileTypes: true });
|
|
1909
1655
|
} catch {
|
|
1910
1656
|
continue;
|
|
1911
1657
|
}
|
|
1912
1658
|
for (const entry of files) {
|
|
1913
1659
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1914
|
-
const filePath =
|
|
1660
|
+
const filePath = join10(vault, dir, entry.name);
|
|
1915
1661
|
let text;
|
|
1916
1662
|
try {
|
|
1917
|
-
text = await
|
|
1663
|
+
text = await readFile7(filePath, "utf8");
|
|
1918
1664
|
} catch {
|
|
1919
1665
|
continue;
|
|
1920
1666
|
}
|
|
@@ -1930,14 +1676,14 @@ async function renderProjectIndex(vault, slug, opts = {}) {
|
|
|
1930
1676
|
}
|
|
1931
1677
|
}
|
|
1932
1678
|
for (const dir of PROJECT_LOCAL_DIRS) {
|
|
1933
|
-
const rootAbs =
|
|
1679
|
+
const rootAbs = join10(projectDir, dir);
|
|
1934
1680
|
const rootRel = `projects/${slug}/${dir}`;
|
|
1935
1681
|
const pages = await scanMarkdownTree(rootAbs, rootRel);
|
|
1936
1682
|
for (const page of pages) {
|
|
1937
|
-
const filePath =
|
|
1683
|
+
const filePath = join10(vault, page);
|
|
1938
1684
|
let text;
|
|
1939
1685
|
try {
|
|
1940
|
-
text = await
|
|
1686
|
+
text = await readFile7(filePath, "utf8");
|
|
1941
1687
|
} catch {
|
|
1942
1688
|
continue;
|
|
1943
1689
|
}
|
|
@@ -2010,7 +1756,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
|
2010
1756
|
}
|
|
2011
1757
|
async function runProjectIndex(input) {
|
|
2012
1758
|
const slug = input.slug;
|
|
2013
|
-
const projectDir =
|
|
1759
|
+
const projectDir = join10(input.vault, "projects", slug);
|
|
2014
1760
|
const rendered = await renderProjectIndex(input.vault, slug);
|
|
2015
1761
|
if (!rendered.ok) {
|
|
2016
1762
|
return {
|
|
@@ -2018,12 +1764,12 @@ async function runProjectIndex(input) {
|
|
|
2018
1764
|
result: rendered
|
|
2019
1765
|
};
|
|
2020
1766
|
}
|
|
2021
|
-
const indexPath =
|
|
1767
|
+
const indexPath = join10(projectDir, "knowledge.md");
|
|
2022
1768
|
const entries = rendered.data.entries;
|
|
2023
1769
|
let existing = false;
|
|
2024
1770
|
let stale = false;
|
|
2025
1771
|
try {
|
|
2026
|
-
const existingText = await
|
|
1772
|
+
const existingText = await readFile7(indexPath, "utf8");
|
|
2027
1773
|
existing = true;
|
|
2028
1774
|
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
2029
1775
|
const existingPages = new Set(existingEntries.map((l) => {
|
|
@@ -2036,7 +1782,7 @@ async function runProjectIndex(input) {
|
|
|
2036
1782
|
}
|
|
2037
1783
|
if (input.apply) {
|
|
2038
1784
|
try {
|
|
2039
|
-
await
|
|
1785
|
+
await mkdir2(dirname3(indexPath), { recursive: true });
|
|
2040
1786
|
} catch (e) {
|
|
2041
1787
|
return {
|
|
2042
1788
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -2068,8 +1814,8 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
|
|
|
2068
1814
|
}
|
|
2069
1815
|
|
|
2070
1816
|
// src/commands/stale.ts
|
|
2071
|
-
import { readdir as
|
|
2072
|
-
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";
|
|
2073
1819
|
|
|
2074
1820
|
// src/parsers/expiry-annotations.ts
|
|
2075
1821
|
var HEADING_RE = /^#{1,6}\s+(.+)$/;
|
|
@@ -2105,6 +1851,144 @@ function parseExpiryAnnotations(content, pagePath) {
|
|
|
2105
1851
|
return annotations;
|
|
2106
1852
|
}
|
|
2107
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
|
+
|
|
2108
1992
|
// src/commands/stale.ts
|
|
2109
1993
|
function daysSince(isoDate) {
|
|
2110
1994
|
return Math.floor((Date.now() - Date.parse(isoDate)) / 864e5);
|
|
@@ -2118,10 +2002,10 @@ async function runStale(input) {
|
|
|
2118
2002
|
const archived = [];
|
|
2119
2003
|
const workDirs = /* @__PURE__ */ new Map();
|
|
2120
2004
|
const workDirsBySlug = /* @__PURE__ */ new Map();
|
|
2121
|
-
const projectsDir =
|
|
2005
|
+
const projectsDir = join11(input.vault, "projects");
|
|
2122
2006
|
let projectSlugs = [];
|
|
2123
2007
|
try {
|
|
2124
|
-
projectSlugs = (await
|
|
2008
|
+
projectSlugs = (await readdir2(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2125
2009
|
} catch {
|
|
2126
2010
|
}
|
|
2127
2011
|
if (input.project) {
|
|
@@ -2131,10 +2015,10 @@ async function runStale(input) {
|
|
|
2131
2015
|
projectSlugs = [input.project];
|
|
2132
2016
|
}
|
|
2133
2017
|
for (const slug of projectSlugs) {
|
|
2134
|
-
const workPath =
|
|
2018
|
+
const workPath = join11(projectsDir, slug, "work");
|
|
2135
2019
|
let entries;
|
|
2136
2020
|
try {
|
|
2137
|
-
entries = await
|
|
2021
|
+
entries = await readdir2(workPath, { withFileTypes: true });
|
|
2138
2022
|
} catch {
|
|
2139
2023
|
continue;
|
|
2140
2024
|
}
|
|
@@ -2142,11 +2026,11 @@ async function runStale(input) {
|
|
|
2142
2026
|
for (const e of entries) {
|
|
2143
2027
|
if (!e.isDirectory()) continue;
|
|
2144
2028
|
const relDir = `projects/${slug}/work/${e.name}`;
|
|
2145
|
-
const absDir =
|
|
2029
|
+
const absDir = join11(workPath, e.name);
|
|
2146
2030
|
let status = "";
|
|
2147
2031
|
let files;
|
|
2148
2032
|
try {
|
|
2149
|
-
files = await
|
|
2033
|
+
files = await readdir2(absDir);
|
|
2150
2034
|
} catch {
|
|
2151
2035
|
workDirs.set(relDir, "");
|
|
2152
2036
|
slugDirs.set(e.name, "");
|
|
@@ -2155,7 +2039,7 @@ async function runStale(input) {
|
|
|
2155
2039
|
for (const f of files) {
|
|
2156
2040
|
if (!f.endsWith(".md")) continue;
|
|
2157
2041
|
try {
|
|
2158
|
-
const fm = extractFrontmatter(await readFile9(
|
|
2042
|
+
const fm = extractFrontmatter(await readFile9(join11(absDir, f), "utf8"));
|
|
2159
2043
|
if (fm.ok && typeof fm.data.status === "string") {
|
|
2160
2044
|
status = fm.data.status;
|
|
2161
2045
|
break;
|
|
@@ -2251,7 +2135,7 @@ async function runStale(input) {
|
|
|
2251
2135
|
}
|
|
2252
2136
|
}
|
|
2253
2137
|
await mapWithConcurrency([...workDirs.keys()], vaultIoConcurrency(), async (relDir) => {
|
|
2254
|
-
const specPath =
|
|
2138
|
+
const specPath = join11(input.vault, relDir, "spec.md");
|
|
2255
2139
|
try {
|
|
2256
2140
|
const specContent = await readFile9(specPath, "utf8");
|
|
2257
2141
|
const specFm = extractFrontmatter(specContent);
|
|
@@ -2282,7 +2166,7 @@ async function runStale(input) {
|
|
|
2282
2166
|
if (daysSince(dateStr) < input.days) continue;
|
|
2283
2167
|
let files;
|
|
2284
2168
|
try {
|
|
2285
|
-
files = await
|
|
2169
|
+
files = await readdir2(join11(input.vault, relDir));
|
|
2286
2170
|
} catch {
|
|
2287
2171
|
continue;
|
|
2288
2172
|
}
|
|
@@ -2352,48 +2236,92 @@ async function runStale(input) {
|
|
|
2352
2236
|
staleSections.push(...staleSectionResults.flat());
|
|
2353
2237
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2354
2238
|
if (input.archive) {
|
|
2355
|
-
const archiveDir =
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
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 = [];
|
|
2369
2254
|
for (const t of staleTranscripts) {
|
|
2370
2255
|
if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
|
|
2371
|
-
const
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
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);
|
|
2377
2303
|
}
|
|
2378
2304
|
for (const w of [...incompleteWorkItems, ...doneWorkItems]) {
|
|
2379
2305
|
const parts = w.path.split("/");
|
|
2380
2306
|
if (parts.length >= 4 && parts[0] === "projects") {
|
|
2381
2307
|
const slug = parts[1];
|
|
2382
2308
|
const itemName = parts[3];
|
|
2383
|
-
const histDir =
|
|
2309
|
+
const histDir = join11(input.vault, "projects", slug, "history", "archived-work");
|
|
2384
2310
|
await mkdir4(histDir, { recursive: true });
|
|
2385
|
-
const dest =
|
|
2311
|
+
const dest = join11(histDir, itemName);
|
|
2386
2312
|
try {
|
|
2387
|
-
await rename(
|
|
2313
|
+
await rename(join11(input.vault, w.path), dest);
|
|
2388
2314
|
archived.push(w.path);
|
|
2389
|
-
} catch {
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: w.path, destination: dest, message: String(error) }) };
|
|
2390
2317
|
}
|
|
2391
2318
|
} else {
|
|
2392
|
-
const dest =
|
|
2319
|
+
const dest = join11(archiveDir, w.path.replace(/\//g, "_"));
|
|
2393
2320
|
try {
|
|
2394
|
-
await rename(
|
|
2321
|
+
await rename(join11(input.vault, w.path), dest);
|
|
2395
2322
|
archived.push(w.path);
|
|
2396
|
-
} catch {
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: w.path, destination: dest, message: String(error) }) };
|
|
2397
2325
|
}
|
|
2398
2326
|
}
|
|
2399
2327
|
}
|
|
@@ -2446,16 +2374,48 @@ async function runPagesize(input) {
|
|
|
2446
2374
|
}
|
|
2447
2375
|
|
|
2448
2376
|
// src/commands/log-rotate.ts
|
|
2449
|
-
import { readFile as readFile10, rename as rename2, writeFile as writeFile2, stat as
|
|
2450
|
-
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";
|
|
2451
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
|
+
}
|
|
2452
2412
|
async function runLogRotate(input) {
|
|
2453
2413
|
try {
|
|
2454
|
-
await
|
|
2414
|
+
await stat3(join12(input.vault, "SCHEMA.md"));
|
|
2455
2415
|
} catch {
|
|
2456
2416
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
2457
2417
|
}
|
|
2458
|
-
const logPath =
|
|
2418
|
+
const logPath = join12(input.vault, "log.md");
|
|
2459
2419
|
let logText;
|
|
2460
2420
|
try {
|
|
2461
2421
|
logText = await readFile10(logPath, "utf8");
|
|
@@ -2467,37 +2427,56 @@ async function runLogRotate(input) {
|
|
|
2467
2427
|
if (entries < input.threshold) {
|
|
2468
2428
|
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
|
|
2469
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);
|
|
2470
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)`;
|
|
2471
2436
|
return {
|
|
2472
2437
|
exitCode: ExitCode.LOG_ROTATE_NEEDED,
|
|
2473
|
-
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint:
|
|
2438
|
+
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: hint })
|
|
2474
2439
|
};
|
|
2475
2440
|
}
|
|
2476
|
-
|
|
2477
|
-
const rotatedName = `log-${newestYear}.md`;
|
|
2478
|
-
const rotatedPath = join14(input.vault, rotatedName);
|
|
2441
|
+
let archivedExistingTo;
|
|
2479
2442
|
try {
|
|
2443
|
+
archivedExistingTo = await sidelineExistingYearLog(input.vault, rotatedPath, newestYear);
|
|
2480
2444
|
await rename2(logPath, rotatedPath);
|
|
2481
2445
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2446
|
+
const sidelineNote = archivedExistingTo ? `
|
|
2447
|
+
- Prior ${rotatedName} preserved as ${archivedExistingTo}
|
|
2448
|
+
` : "";
|
|
2482
2449
|
const fresh = `# Vault Log
|
|
2483
2450
|
|
|
2484
2451
|
Chronological action log. Newest entries last. Skill writes append entries; lint may rotate.
|
|
2485
2452
|
|
|
2486
2453
|
## [${today}] rotate | Log rotated from ${entries} entries
|
|
2487
2454
|
|
|
2488
|
-
- Previous log moved to ${rotatedName}
|
|
2489
|
-
`;
|
|
2455
|
+
- Previous log moved to ${rotatedName}${sidelineNote}`;
|
|
2490
2456
|
await writeFile2(logPath, fresh, "utf8");
|
|
2491
2457
|
} catch (e) {
|
|
2492
2458
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
2493
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}`;
|
|
2494
2463
|
appendLastOp(input.vault, {
|
|
2495
2464
|
operation: "log-rotate",
|
|
2496
|
-
summary
|
|
2497
|
-
files
|
|
2465
|
+
summary,
|
|
2466
|
+
files,
|
|
2498
2467
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2499
2468
|
});
|
|
2500
|
-
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
|
+
};
|
|
2501
2480
|
}
|
|
2502
2481
|
|
|
2503
2482
|
// src/commands/topic-map-check.ts
|
|
@@ -2521,12 +2500,12 @@ async function runTopicMapCheck(input) {
|
|
|
2521
2500
|
|
|
2522
2501
|
// src/commands/index-link-format.ts
|
|
2523
2502
|
import { readFile as readFile11 } from "fs/promises";
|
|
2524
|
-
import { join as
|
|
2503
|
+
import { join as join13 } from "path";
|
|
2525
2504
|
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
2526
2505
|
async function runIndexLinkFormat(input) {
|
|
2527
2506
|
let text = "";
|
|
2528
2507
|
try {
|
|
2529
|
-
text = await readFile11(
|
|
2508
|
+
text = await readFile11(join13(input.vault, "index.md"), "utf8");
|
|
2530
2509
|
} catch {
|
|
2531
2510
|
}
|
|
2532
2511
|
const markdown_links = [];
|
|
@@ -2539,9 +2518,9 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
|
2539
2518
|
}
|
|
2540
2519
|
|
|
2541
2520
|
// src/commands/dedup.ts
|
|
2542
|
-
import { createHash as
|
|
2543
|
-
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4
|
|
2544
|
-
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";
|
|
2545
2524
|
|
|
2546
2525
|
// src/utils/rclone.ts
|
|
2547
2526
|
import { execFile } from "child_process";
|
|
@@ -2590,6 +2569,140 @@ async function defaultRcloneRunner(args) {
|
|
|
2590
2569
|
}
|
|
2591
2570
|
}
|
|
2592
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
|
+
|
|
2593
2706
|
// src/commands/dedup.ts
|
|
2594
2707
|
async function runDedup(input) {
|
|
2595
2708
|
if (input.canonicalPolicy && input.canonicalPolicy !== "scan-order" && input.canonicalPolicy !== "stable-path") {
|
|
@@ -2610,7 +2723,11 @@ async function runDedup(input) {
|
|
|
2610
2723
|
}
|
|
2611
2724
|
const hashMap = /* @__PURE__ */ new Map();
|
|
2612
2725
|
let totalFiles = 0;
|
|
2613
|
-
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) => {
|
|
2614
2731
|
const fm = extractFrontmatter(await readPageCached(raw, input.pageTextCache));
|
|
2615
2732
|
if (!fm.ok) return null;
|
|
2616
2733
|
const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
@@ -2625,77 +2742,101 @@ async function runDedup(input) {
|
|
|
2625
2742
|
else hashMap.set(entry.sha, [entry.relPath]);
|
|
2626
2743
|
}
|
|
2627
2744
|
const canonicalPolicy = input.canonicalPolicy ?? "stable-path";
|
|
2628
|
-
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) }));
|
|
2629
2746
|
const rewired = [];
|
|
2630
2747
|
const removed = [];
|
|
2748
|
+
const relocated = [];
|
|
2631
2749
|
const unsafe = [];
|
|
2632
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
|
+
}
|
|
2633
2755
|
const manifest = safeEntries.length > 0 ? {
|
|
2634
2756
|
version: 1,
|
|
2635
2757
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2636
2758
|
vault: resolve3(input.vault),
|
|
2637
2759
|
entries: safeEntries
|
|
2638
2760
|
} : void 0;
|
|
2639
|
-
const
|
|
2640
|
-
if (!
|
|
2641
|
-
return {
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
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 }) };
|
|
2645
2782
|
}
|
|
2646
2783
|
if (input.manifestOut && manifest) {
|
|
2647
2784
|
try {
|
|
2648
|
-
mkdirSync4(
|
|
2785
|
+
mkdirSync4(dirname5(input.manifestOut), { recursive: true });
|
|
2649
2786
|
writeFileSync4(input.manifestOut, `${JSON.stringify(manifest, null, 2)}
|
|
2650
2787
|
`, "utf-8");
|
|
2651
2788
|
} catch (e) {
|
|
2652
2789
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: input.manifestOut, message: String(e) }) };
|
|
2653
2790
|
}
|
|
2654
2791
|
}
|
|
2655
|
-
if (input.apply &&
|
|
2792
|
+
if (input.apply && structuralPlans.length > 0) {
|
|
2656
2793
|
const replacements = /* @__PURE__ */ new Map();
|
|
2657
|
-
for (const
|
|
2658
|
-
|
|
2659
|
-
replacements.set(duplicate, entry.canonical);
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2794
|
+
for (const plan of structuralPlans) replacements.set(plan.source, plan.canonical);
|
|
2795
|
+
const pendingWrites = [];
|
|
2662
2796
|
for (const page of scan.allMarkdown.filter((p) => !p.relPath.startsWith("raw/"))) {
|
|
2663
|
-
const text = readFileSync4(
|
|
2797
|
+
const text = readFileSync4(join14(input.vault, page.relPath), "utf-8");
|
|
2664
2798
|
let updated = text;
|
|
2665
2799
|
let changed = false;
|
|
2666
2800
|
for (const [oldPath, newPath] of replacements) {
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
updated = updated.replaceAll(oldMarker, newMarker);
|
|
2671
|
-
changed = true;
|
|
2672
|
-
}
|
|
2673
|
-
const oldFm = `- "^[${oldPath}]"`;
|
|
2674
|
-
const newFm = `- "^[${newPath}]"`;
|
|
2675
|
-
if (updated.includes(oldFm)) {
|
|
2676
|
-
updated = updated.replaceAll(oldFm, newFm);
|
|
2677
|
-
changed = true;
|
|
2678
|
-
}
|
|
2801
|
+
const rewritten = rewriteRawSourceReferences(updated, oldPath, newPath);
|
|
2802
|
+
updated = rewritten.text;
|
|
2803
|
+
changed = changed || rewritten.changed;
|
|
2679
2804
|
}
|
|
2680
2805
|
if (changed) {
|
|
2681
|
-
|
|
2806
|
+
pendingWrites.push({ page: page.relPath, text: updated });
|
|
2682
2807
|
rewired.push(page.relPath);
|
|
2683
2808
|
}
|
|
2684
2809
|
}
|
|
2685
|
-
for (const
|
|
2686
|
-
const
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
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 };
|
|
2692
2826
|
}
|
|
2693
2827
|
}
|
|
2694
|
-
|
|
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)) {
|
|
2695
2836
|
appendLastOp(input.vault, {
|
|
2696
2837
|
operation: "dedup",
|
|
2697
|
-
summary: `rewired ${rewired.length} pages,
|
|
2698
|
-
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])],
|
|
2699
2840
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2700
2841
|
});
|
|
2701
2842
|
}
|
|
@@ -2706,7 +2847,7 @@ async function runDedup(input) {
|
|
|
2706
2847
|
for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
|
|
2707
2848
|
if (input.apply) {
|
|
2708
2849
|
hintLines.push(`rewired: ${rewired.length} pages`);
|
|
2709
|
-
hintLines.push(`
|
|
2850
|
+
hintLines.push(`preserved duplicates: ${relocated.length} raw files`);
|
|
2710
2851
|
}
|
|
2711
2852
|
if (unsafe.length > 0) hintLines.push(`unsafe: ${unsafe.length} groups skipped`);
|
|
2712
2853
|
if (remote.data.plannedDeletes.length > 0) {
|
|
@@ -2726,10 +2867,21 @@ async function runDedup(input) {
|
|
|
2726
2867
|
remote: remote.data,
|
|
2727
2868
|
rewired,
|
|
2728
2869
|
removed,
|
|
2870
|
+
relocated,
|
|
2871
|
+
...approvalToken ? { approval_token: approvalToken } : {},
|
|
2729
2872
|
humanHint: hintLines.join("\n")
|
|
2730
2873
|
})
|
|
2731
2874
|
};
|
|
2732
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
|
+
}
|
|
2733
2885
|
function orderFiles(files, policy) {
|
|
2734
2886
|
if (policy === "scan-order") return [...files];
|
|
2735
2887
|
return [...files].sort(compareStableRawPath);
|
|
@@ -2777,10 +2929,13 @@ function buildSafeEntries(vault, duplicates, unsafe) {
|
|
|
2777
2929
|
return entries;
|
|
2778
2930
|
}
|
|
2779
2931
|
function hashRawBody(vault, relPath) {
|
|
2780
|
-
const text = readFileSync4(
|
|
2932
|
+
const text = readFileSync4(join14(vault, relPath), "utf-8");
|
|
2933
|
+
return hashRawBodyText(text);
|
|
2934
|
+
}
|
|
2935
|
+
function hashRawBodyText(text) {
|
|
2781
2936
|
const split = splitFrontmatter(text);
|
|
2782
2937
|
const body = split.ok ? split.data.body : text;
|
|
2783
|
-
return
|
|
2938
|
+
return createHash5("sha256").update(body).digest("hex");
|
|
2784
2939
|
}
|
|
2785
2940
|
function readManifest(path) {
|
|
2786
2941
|
try {
|
|
@@ -2793,69 +2948,50 @@ function readManifest(path) {
|
|
|
2793
2948
|
return err("INVALID_FRONTMATTER", { path, message: String(e) });
|
|
2794
2949
|
}
|
|
2795
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
|
+
}
|
|
2796
2989
|
async function planAndMaybePruneRemote(input, entries) {
|
|
2797
2990
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
2798
2991
|
const plannedDeletes = remoteRoot ? entries.flatMap((entry) => entry.duplicates.map((path) => `${remoteRoot}/${path}`)) : [];
|
|
2799
2992
|
return planAndMaybePruneRemoteObjects(plannedDeletes, { ...input, defaultMaxDeletes: 50 });
|
|
2800
2993
|
}
|
|
2801
2994
|
|
|
2802
|
-
// src/utils/safe-write.ts
|
|
2803
|
-
import { readFile as readFile12, writeFile as writeFile3 } from "fs/promises";
|
|
2804
|
-
var DEFAULT_MIN_BODY_RATIO = 0.5;
|
|
2805
|
-
var DEFAULT_MIN_OLD_BODY_BYTES = 200;
|
|
2806
|
-
function bodyBytes(text) {
|
|
2807
|
-
const split = splitFrontmatter(text);
|
|
2808
|
-
if (!split.ok) return Buffer.byteLength(text, "utf8");
|
|
2809
|
-
return Buffer.byteLength(split.data.body, "utf8");
|
|
2810
|
-
}
|
|
2811
|
-
async function readIfExists(absPath) {
|
|
2812
|
-
try {
|
|
2813
|
-
return await readFile12(absPath, "utf8");
|
|
2814
|
-
} catch (e) {
|
|
2815
|
-
if (e.code === "ENOENT") return null;
|
|
2816
|
-
throw e;
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
|
-
async function safeWritePage(absPath, newContent, opts = {}) {
|
|
2820
|
-
const minRatio = opts.minBodyRatio === void 0 ? DEFAULT_MIN_BODY_RATIO : opts.minBodyRatio;
|
|
2821
|
-
const minOldBytes = opts.minOldBodyBytes ?? DEFAULT_MIN_OLD_BODY_BYTES;
|
|
2822
|
-
let oldContent;
|
|
2823
|
-
try {
|
|
2824
|
-
oldContent = await readIfExists(absPath);
|
|
2825
|
-
} catch (e) {
|
|
2826
|
-
return err("WRITE_FAILED", { path: absPath, phase: "read-existing", message: String(e) });
|
|
2827
|
-
}
|
|
2828
|
-
const isNew = oldContent === null;
|
|
2829
|
-
const oldBodyBytes = isNew ? 0 : bodyBytes(oldContent);
|
|
2830
|
-
const newBodyBytes = bodyBytes(newContent);
|
|
2831
|
-
const bodyRatio = oldBodyBytes > 0 ? newBodyBytes / oldBodyBytes : null;
|
|
2832
|
-
let guardSkippedSmall = false;
|
|
2833
|
-
if (!isNew && minRatio !== null && bodyRatio !== null && bodyRatio < minRatio) {
|
|
2834
|
-
if (oldBodyBytes < minOldBytes) {
|
|
2835
|
-
guardSkippedSmall = true;
|
|
2836
|
-
} else {
|
|
2837
|
-
return err("BODY_TRUNCATION_GUARD", {
|
|
2838
|
-
path: absPath,
|
|
2839
|
-
oldBodyBytes,
|
|
2840
|
-
newBodyBytes,
|
|
2841
|
-
bodyRatio,
|
|
2842
|
-
minBodyRatio: minRatio,
|
|
2843
|
-
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."
|
|
2844
|
-
});
|
|
2845
|
-
}
|
|
2846
|
-
}
|
|
2847
|
-
const written = await atomicWriteText(absPath, newContent);
|
|
2848
|
-
if (!written.ok) return written;
|
|
2849
|
-
return ok({
|
|
2850
|
-
changed: written.data.changed,
|
|
2851
|
-
isNew,
|
|
2852
|
-
oldBodyBytes,
|
|
2853
|
-
newBodyBytes,
|
|
2854
|
-
bodyRatio,
|
|
2855
|
-
guardSkippedSmall
|
|
2856
|
-
});
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
2995
|
// src/commands/frontmatter-fix.ts
|
|
2860
2996
|
function isoToday() {
|
|
2861
2997
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -2934,9 +3070,8 @@ ${newBody}`;
|
|
|
2934
3070
|
|
|
2935
3071
|
// src/commands/lint.ts
|
|
2936
3072
|
import { existsSync as existsSync6 } from "fs";
|
|
2937
|
-
import { readFile as
|
|
2938
|
-
import {
|
|
2939
|
-
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";
|
|
2940
3075
|
|
|
2941
3076
|
// src/commands/sparse-community.ts
|
|
2942
3077
|
async function runSparseCommunity(input) {
|
|
@@ -2952,7 +3087,7 @@ async function runSparseCommunity(input) {
|
|
|
2952
3087
|
}
|
|
2953
3088
|
|
|
2954
3089
|
// src/commands/raw-body-dedup.ts
|
|
2955
|
-
import { createHash as
|
|
3090
|
+
import { createHash as createHash6 } from "crypto";
|
|
2956
3091
|
async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
2957
3092
|
const scanResult = scan ? ok(scan) : await scanVault(vault);
|
|
2958
3093
|
if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
|
|
@@ -2961,7 +3096,7 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
|
2961
3096
|
const text = await readPageCached(raw, pageTextCache);
|
|
2962
3097
|
const split = splitFrontmatter(text);
|
|
2963
3098
|
if (!split.ok) return null;
|
|
2964
|
-
const bodyHash =
|
|
3099
|
+
const bodyHash = createHash6("sha256").update(split.data.body).digest("hex");
|
|
2965
3100
|
const fm = extractFrontmatter(text);
|
|
2966
3101
|
let fmSha256 = null;
|
|
2967
3102
|
if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
|
|
@@ -2994,8 +3129,8 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
|
2994
3129
|
|
|
2995
3130
|
// src/commands/path-too-long.ts
|
|
2996
3131
|
import { existsSync as existsSync5 } from "fs";
|
|
2997
|
-
import { mkdir as mkdir5, readFile as
|
|
2998
|
-
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";
|
|
2999
3134
|
var MAX_PATH_LENGTH = 240;
|
|
3000
3135
|
var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
|
|
3001
3136
|
async function runPathTooLong(input) {
|
|
@@ -3021,6 +3156,10 @@ async function fixPathTooLong(input) {
|
|
|
3021
3156
|
const fixed = [];
|
|
3022
3157
|
const unresolved = [];
|
|
3023
3158
|
for (const violation of violations) {
|
|
3159
|
+
if (violation.relPath.startsWith("raw/")) {
|
|
3160
|
+
unresolved.push(violation.relPath);
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3024
3163
|
const target = await resolveFixTarget(input.vault, violation.relPath, violation.suggestedRelPath, maxFixLength);
|
|
3025
3164
|
if (!target || target.relPath === violation.relPath || target.relPath.length > maxFixLength) {
|
|
3026
3165
|
unresolved.push(violation.relPath);
|
|
@@ -3028,10 +3167,10 @@ async function fixPathTooLong(input) {
|
|
|
3028
3167
|
}
|
|
3029
3168
|
try {
|
|
3030
3169
|
if (target.mode === "dedupe") {
|
|
3031
|
-
await
|
|
3170
|
+
await unlink2(join15(input.vault, violation.relPath));
|
|
3032
3171
|
} else {
|
|
3033
|
-
await mkdir5(
|
|
3034
|
-
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));
|
|
3035
3174
|
}
|
|
3036
3175
|
fixed.push({ from: violation.relPath, to: target.relPath });
|
|
3037
3176
|
} catch {
|
|
@@ -3045,7 +3184,7 @@ async function fixPathTooLong(input) {
|
|
|
3045
3184
|
for (const page of afterScan.data.allMarkdown) {
|
|
3046
3185
|
if (!shouldRewriteReferences(page.relPath)) continue;
|
|
3047
3186
|
try {
|
|
3048
|
-
const original = await
|
|
3187
|
+
const original = await readFile14(page.absPath, "utf8");
|
|
3049
3188
|
let updated = original;
|
|
3050
3189
|
for (const fix of fixed) {
|
|
3051
3190
|
updated = replacePathReferences(updated, fix.from, fix.to);
|
|
@@ -3108,9 +3247,9 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
|
|
|
3108
3247
|
async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
3109
3248
|
for (const candidate of candidateRelPaths(preferred, maxLength)) {
|
|
3110
3249
|
if (candidate === original || candidate.length > maxLength) continue;
|
|
3111
|
-
const candidatePath =
|
|
3250
|
+
const candidatePath = join15(vault, candidate);
|
|
3112
3251
|
if (!existsSync5(candidatePath)) return { relPath: candidate, mode: "rename" };
|
|
3113
|
-
if (await hasSameContent(
|
|
3252
|
+
if (await hasSameContent(join15(vault, original), candidatePath)) {
|
|
3114
3253
|
return { relPath: candidate, mode: "dedupe" };
|
|
3115
3254
|
}
|
|
3116
3255
|
}
|
|
@@ -3119,8 +3258,8 @@ async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
|
3119
3258
|
function candidateRelPaths(preferred, maxLength) {
|
|
3120
3259
|
const candidates = [preferred];
|
|
3121
3260
|
if (preferred.length > maxLength) return candidates;
|
|
3122
|
-
const dir =
|
|
3123
|
-
const filename =
|
|
3261
|
+
const dir = posix2.dirname(preferred) === "." ? "" : posix2.dirname(preferred);
|
|
3262
|
+
const filename = posix2.basename(preferred);
|
|
3124
3263
|
const ext = filename.endsWith(".md") ? ".md" : "";
|
|
3125
3264
|
const base = ext ? filename.slice(0, -3) : filename;
|
|
3126
3265
|
const dirPrefix = dir ? `${dir}/` : "";
|
|
@@ -3134,7 +3273,7 @@ function candidateRelPaths(preferred, maxLength) {
|
|
|
3134
3273
|
}
|
|
3135
3274
|
async function hasSameContent(a, b) {
|
|
3136
3275
|
try {
|
|
3137
|
-
const [left, right] = await Promise.all([
|
|
3276
|
+
const [left, right] = await Promise.all([readFile14(a), readFile14(b)]);
|
|
3138
3277
|
return left.equals(right);
|
|
3139
3278
|
} catch {
|
|
3140
3279
|
return false;
|
|
@@ -3147,8 +3286,8 @@ function shouldRewriteReferences(relPath) {
|
|
|
3147
3286
|
}
|
|
3148
3287
|
function replacePathReferences(content, oldRelPath, newRelPath) {
|
|
3149
3288
|
let updated = content.replaceAll(oldRelPath, newRelPath);
|
|
3150
|
-
const oldStem =
|
|
3151
|
-
const newStem =
|
|
3289
|
+
const oldStem = posix2.basename(oldRelPath).replace(/\.md$/, "");
|
|
3290
|
+
const newStem = posix2.basename(newRelPath).replace(/\.md$/, "");
|
|
3152
3291
|
if (oldStem !== newStem) {
|
|
3153
3292
|
const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3154
3293
|
const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
@@ -3177,7 +3316,8 @@ function buildCliSurface() {
|
|
|
3177
3316
|
program.command("graph");
|
|
3178
3317
|
program.command("canvas");
|
|
3179
3318
|
program.command("overlap").option("--wiki <name>");
|
|
3180
|
-
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");
|
|
3181
3321
|
program.command("orphans").option("--wiki <name>");
|
|
3182
3322
|
program.command("audit");
|
|
3183
3323
|
program.command("install").option("--target <dir>").option("--dry-run").option("--skills-root <dir>").option("--symlink");
|
|
@@ -3189,7 +3329,7 @@ function buildCliSurface() {
|
|
|
3189
3329
|
program.command("index-check").option("--wiki <name>");
|
|
3190
3330
|
program.command("index-link-format").option("--wiki <name>");
|
|
3191
3331
|
program.command("topic-map-check").option("--threshold <n>").option("--wiki <name>");
|
|
3192
|
-
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>");
|
|
3193
3333
|
program.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
|
|
3194
3334
|
program.command("pagesize").option("--lines <n>").option("--wiki <name>");
|
|
3195
3335
|
program.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
|
|
@@ -3204,10 +3344,10 @@ function buildCliSurface() {
|
|
|
3204
3344
|
program.command("health").option("--wiki <name>").option("--sync <mode>").option("--no-fail").option("--out <path>").option("--examples <n>");
|
|
3205
3345
|
program.command("doctor").option("--check-snapshotter");
|
|
3206
3346
|
program.command("status").option("--wiki <name>");
|
|
3207
|
-
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>");
|
|
3208
3348
|
program.command("remove").option("--wiki <name>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--reason <text>");
|
|
3209
3349
|
program.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
|
|
3210
|
-
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>");
|
|
3211
3351
|
program.command("migrate-citations").option("--dry-run").option("--wiki <name>");
|
|
3212
3352
|
program.command("frontmatter-fix").option("--dry-run").option("--wiki <name>");
|
|
3213
3353
|
program.command("update").option("--tag <tag>");
|
|
@@ -3227,10 +3367,15 @@ function buildCliSurface() {
|
|
|
3227
3367
|
program.command("ingest").requiredOption("--vault <path>").requiredOption("--type <type>").requiredOption("--title <title>").option("--tags <csv>").option("--provenance <provenance>").option("--dry-run");
|
|
3228
3368
|
program.command("fleet");
|
|
3229
3369
|
program.command("page");
|
|
3370
|
+
program.command("project-page");
|
|
3230
3371
|
program.command("write-preflight").option("--command <name>").option("--dirty-threshold <n>").option("--skip-dirty").option("--prior-artifact-file <path>").option("--prior-artifact-text <text>").option("--consecutive-no-decision <n>").option("--no-decision-threshold <n>").option("--human-allow").option("--mission-kind <kind>").option("--skip-mission").option("--project <slug>").option("--capture-day <date>").option("--capture-budget <n>").option("--severity <level>").option("--skip-budget").option("--checks <list>").option("--wiki <name>");
|
|
3231
3372
|
program.command("mcp");
|
|
3232
3373
|
const graphCmd = program.commands.find((c) => c.name() === "graph");
|
|
3233
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>");
|
|
3234
3379
|
const canvasCmd = program.commands.find((c) => c.name() === "canvas");
|
|
3235
3380
|
canvasCmd.command("generate").option("--graph-path <path>").option("--wiki <name>");
|
|
3236
3381
|
const configCmd = program.commands.find((c) => c.name() === "config");
|
|
@@ -3245,7 +3390,9 @@ function buildCliSurface() {
|
|
|
3245
3390
|
const tagCmd = program.commands.find((c) => c.name() === "tag");
|
|
3246
3391
|
tagCmd.command("reconcile").requiredOption("--page <path>").option("--from <path>").option("--tags <csv>").option("--reason <text>").option("--write").option("--wiki <name>");
|
|
3247
3392
|
const pageCmd = program.commands.find((c) => c.name() === "page");
|
|
3248
|
-
pageCmd.command("publish").requiredOption("--target <path>").option("--log-note <text>").option("--write").option("--wiki <name>");
|
|
3393
|
+
pageCmd.command("publish").requiredOption("--target <path>").option("--log-note <text>").option("--write").option("--approve <token>").option("--wiki <name>");
|
|
3394
|
+
const projectPageCmd = program.commands.find((c) => c.name() === "project-page");
|
|
3395
|
+
projectPageCmd.command("publish").requiredOption("--project <slug>").requiredOption("--target <path>").option("--log-note <text>").option("--write").option("--approve <token>").option("--wiki <name>");
|
|
3249
3396
|
const logCmd = program.commands.find((c) => c.name() === "log");
|
|
3250
3397
|
logCmd.command("materialize").option("--write").option("--wiki <name>");
|
|
3251
3398
|
logCmd.command("migrate-legacy").option("--write").option("--converge-vault <dir>").option("--wiki <name>");
|
|
@@ -3569,17 +3716,6 @@ function readMirrorHintLines(vault) {
|
|
|
3569
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"
|
|
3570
3717
|
];
|
|
3571
3718
|
}
|
|
3572
|
-
function recomputeRawSha256IfPresent(content) {
|
|
3573
|
-
const split = splitFrontmatter(content);
|
|
3574
|
-
if (!split.ok) return content;
|
|
3575
|
-
if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
|
|
3576
|
-
const sha256 = createHash5("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
|
|
3577
|
-
const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
|
|
3578
|
-
return `---
|
|
3579
|
-
${rawFrontmatter}
|
|
3580
|
-
---
|
|
3581
|
-
${split.data.body}`;
|
|
3582
|
-
}
|
|
3583
3719
|
function appendLintFixLastOp(vault, fixed) {
|
|
3584
3720
|
if (fixed.length === 0) return;
|
|
3585
3721
|
appendLastOp(vault, {
|
|
@@ -3618,10 +3754,10 @@ function summarizeLintOutput(output, examplesLimit = 3) {
|
|
|
3618
3754
|
};
|
|
3619
3755
|
}
|
|
3620
3756
|
async function walkMarkdownFiles(absDir, vaultRoot) {
|
|
3621
|
-
const entries = await
|
|
3757
|
+
const entries = await readdir3(absDir, { withFileTypes: true });
|
|
3622
3758
|
const pages = [];
|
|
3623
3759
|
for (const entry of entries) {
|
|
3624
|
-
const absPath =
|
|
3760
|
+
const absPath = join16(absDir, entry.name);
|
|
3625
3761
|
if (entry.isDirectory()) {
|
|
3626
3762
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
3627
3763
|
pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
|
|
@@ -3632,12 +3768,12 @@ async function walkMarkdownFiles(absDir, vaultRoot) {
|
|
|
3632
3768
|
return pages;
|
|
3633
3769
|
}
|
|
3634
3770
|
async function collectCliRefsPages(vault) {
|
|
3635
|
-
if (!existsSync6(
|
|
3771
|
+
if (!existsSync6(join16(vault, "SCHEMA.md"))) {
|
|
3636
3772
|
return err("VAULT_PATH_INVALID", { root: vault, reason: "SCHEMA.md missing" });
|
|
3637
3773
|
}
|
|
3638
3774
|
const pages = [];
|
|
3639
3775
|
for (const dir of CLI_REFS_TYPED_DIRS) {
|
|
3640
|
-
const absDir =
|
|
3776
|
+
const absDir = join16(vault, dir);
|
|
3641
3777
|
if (!existsSync6(absDir)) continue;
|
|
3642
3778
|
pages.push(...await walkMarkdownFiles(absDir, vault));
|
|
3643
3779
|
}
|
|
@@ -3757,9 +3893,13 @@ async function applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSource
|
|
|
3757
3893
|
if (!input.fix || fileSourceUrlFrontmatterFlags.size === 0) return fileSourceUrlFlags;
|
|
3758
3894
|
const fileFixed = [];
|
|
3759
3895
|
for (const relPath of fileSourceUrlFrontmatterFlags) {
|
|
3896
|
+
if (relPath.startsWith("raw/")) {
|
|
3897
|
+
unresolved.push(relPath);
|
|
3898
|
+
continue;
|
|
3899
|
+
}
|
|
3760
3900
|
try {
|
|
3761
3901
|
const absPath = `${input.vault}/${relPath}`;
|
|
3762
|
-
const raw = await
|
|
3902
|
+
const raw = await readFile15(absPath, "utf8");
|
|
3763
3903
|
const parts = raw.split("---", 3);
|
|
3764
3904
|
if (parts.length < 3) {
|
|
3765
3905
|
unresolved.push(relPath);
|
|
@@ -4152,8 +4292,8 @@ async function runLint(input) {
|
|
|
4152
4292
|
const readKnowledgeContent = (slug) => {
|
|
4153
4293
|
const existing = knowledgeContentCache.get(slug);
|
|
4154
4294
|
if (existing) return existing;
|
|
4155
|
-
const knowledgePath =
|
|
4156
|
-
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);
|
|
4157
4297
|
knowledgeContentCache.set(slug, pending);
|
|
4158
4298
|
return pending;
|
|
4159
4299
|
};
|
|
@@ -4230,8 +4370,11 @@ async function runLint(input) {
|
|
|
4230
4370
|
const raw = await readPage(page);
|
|
4231
4371
|
const redacted = redactSensitiveContent(raw, { file: page.relPath });
|
|
4232
4372
|
if (!redacted.changed) continue;
|
|
4233
|
-
|
|
4234
|
-
|
|
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 });
|
|
4235
4378
|
if (!w.ok) {
|
|
4236
4379
|
unresolved.push(page.relPath);
|
|
4237
4380
|
continue;
|
|
@@ -4259,7 +4402,7 @@ async function runLint(input) {
|
|
|
4259
4402
|
for (const relPath of legacyPages) {
|
|
4260
4403
|
try {
|
|
4261
4404
|
const absPath = `${input.vault}/${relPath}`;
|
|
4262
|
-
const raw = await
|
|
4405
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4263
4406
|
const split = splitFrontmatter(raw);
|
|
4264
4407
|
if (!split.ok) {
|
|
4265
4408
|
unresolved.push(relPath);
|
|
@@ -4358,7 +4501,7 @@ ${newBody}`;
|
|
|
4358
4501
|
for (const relPath of noOverview) {
|
|
4359
4502
|
try {
|
|
4360
4503
|
const absPath = `${input.vault}/${relPath}`;
|
|
4361
|
-
const raw = await
|
|
4504
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4362
4505
|
const split = splitFrontmatter(raw);
|
|
4363
4506
|
if (!split.ok) {
|
|
4364
4507
|
unresolved.push(relPath);
|
|
@@ -4399,7 +4542,7 @@ ${trimmedBody}`;
|
|
|
4399
4542
|
for (const relPath of missingTldrFlags) {
|
|
4400
4543
|
try {
|
|
4401
4544
|
const absPath = `${input.vault}/${relPath}`;
|
|
4402
|
-
const raw = await
|
|
4545
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4403
4546
|
const split = splitFrontmatter(raw);
|
|
4404
4547
|
if (!split.ok) {
|
|
4405
4548
|
unresolved.push(relPath);
|
|
@@ -4449,7 +4592,7 @@ ${lines.join("\n")}`;
|
|
|
4449
4592
|
for (const relPath of wikilinkCitationFlags) {
|
|
4450
4593
|
try {
|
|
4451
4594
|
const absPath = `${input.vault}/${relPath}`;
|
|
4452
|
-
const raw = await
|
|
4595
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4453
4596
|
const split = splitFrontmatter(raw);
|
|
4454
4597
|
if (!split.ok) {
|
|
4455
4598
|
unresolved.push(relPath);
|
|
@@ -4554,6 +4697,11 @@ ${newBody}`;
|
|
|
4554
4697
|
const invalidItems = buckets.frontmatter_yaml_invalid;
|
|
4555
4698
|
const remaining = [];
|
|
4556
4699
|
for (const item of invalidItems) {
|
|
4700
|
+
if (item.path.startsWith("raw/")) {
|
|
4701
|
+
unresolved.push(item.path);
|
|
4702
|
+
remaining.push(item);
|
|
4703
|
+
continue;
|
|
4704
|
+
}
|
|
4557
4705
|
const page = scan.allMarkdown.find((p) => p.relPath === item.path);
|
|
4558
4706
|
if (!page) {
|
|
4559
4707
|
unresolved.push(item.path);
|
|
@@ -4790,14 +4938,14 @@ async function runSyncLintDelta(input) {
|
|
|
4790
4938
|
}
|
|
4791
4939
|
|
|
4792
4940
|
// src/commands/config.ts
|
|
4793
|
-
import { readFile as
|
|
4941
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
4794
4942
|
import { existsSync as existsSync7 } from "fs";
|
|
4795
|
-
import { join as
|
|
4943
|
+
import { join as join17 } from "path";
|
|
4796
4944
|
function validateKey(key) {
|
|
4797
4945
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key) || isVaultSyncKey(key);
|
|
4798
4946
|
}
|
|
4799
4947
|
function configPath(home) {
|
|
4800
|
-
return
|
|
4948
|
+
return join17(home, ".skillwiki", ".env");
|
|
4801
4949
|
}
|
|
4802
4950
|
async function runConfigGet(input) {
|
|
4803
4951
|
if (!validateKey(input.key)) {
|
|
@@ -4821,7 +4969,7 @@ async function runConfigSet(input) {
|
|
|
4821
4969
|
try {
|
|
4822
4970
|
let originalContent;
|
|
4823
4971
|
try {
|
|
4824
|
-
originalContent = await
|
|
4972
|
+
originalContent = await readFile16(filePath, "utf8");
|
|
4825
4973
|
} catch {
|
|
4826
4974
|
}
|
|
4827
4975
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -4858,19 +5006,19 @@ async function runConfigPath(input) {
|
|
|
4858
5006
|
|
|
4859
5007
|
// src/commands/doctor.ts
|
|
4860
5008
|
import { existsSync as existsSync13, lstatSync, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
|
|
4861
|
-
import { join as
|
|
5009
|
+
import { join as join23, resolve as resolve5 } from "path";
|
|
4862
5010
|
import { execSync as execSync2 } from "child_process";
|
|
4863
5011
|
import { platform as platform2 } from "os";
|
|
4864
5012
|
|
|
4865
5013
|
// src/utils/plugin-registry.ts
|
|
4866
5014
|
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
4867
|
-
import { join as
|
|
4868
|
-
var REGISTRY_PATH =
|
|
4869
|
-
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");
|
|
4870
5018
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
4871
5019
|
function readInstalledPlugins(home) {
|
|
4872
5020
|
try {
|
|
4873
|
-
const raw = readFileSync5(
|
|
5021
|
+
const raw = readFileSync5(join18(home, REGISTRY_PATH), "utf8");
|
|
4874
5022
|
return JSON.parse(raw);
|
|
4875
5023
|
} catch {
|
|
4876
5024
|
return null;
|
|
@@ -4906,7 +5054,7 @@ function findPluginInstallations(home, key = PLUGIN_KEY) {
|
|
|
4906
5054
|
function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
4907
5055
|
const config = readCodexPluginConfig(home, key, marketplace);
|
|
4908
5056
|
if (!config?.enabled) return null;
|
|
4909
|
-
const cacheRoot =
|
|
5057
|
+
const cacheRoot = join18(home, ".codex", "plugins", "cache", marketplace, pluginName);
|
|
4910
5058
|
if (!existsSync8(cacheRoot)) return null;
|
|
4911
5059
|
let versions;
|
|
4912
5060
|
try {
|
|
@@ -4922,7 +5070,7 @@ function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
|
4922
5070
|
key,
|
|
4923
5071
|
pluginName,
|
|
4924
5072
|
marketplace,
|
|
4925
|
-
installPath:
|
|
5073
|
+
installPath: join18(cacheRoot, version),
|
|
4926
5074
|
version,
|
|
4927
5075
|
sourceType: config.sourceType,
|
|
4928
5076
|
source: config.source
|
|
@@ -4939,7 +5087,7 @@ function parsePluginKey(key) {
|
|
|
4939
5087
|
function readCodexPluginConfig(home, key, marketplace) {
|
|
4940
5088
|
let raw;
|
|
4941
5089
|
try {
|
|
4942
|
-
raw = readFileSync5(
|
|
5090
|
+
raw = readFileSync5(join18(home, CODEX_CONFIG_PATH), "utf8");
|
|
4943
5091
|
} catch {
|
|
4944
5092
|
return null;
|
|
4945
5093
|
}
|
|
@@ -4982,7 +5130,7 @@ function parseTomlScalar(rawValue) {
|
|
|
4982
5130
|
|
|
4983
5131
|
// src/utils/conflict-markers.ts
|
|
4984
5132
|
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
4985
|
-
import { join as
|
|
5133
|
+
import { join as join19 } from "path";
|
|
4986
5134
|
function scanConflictMarkerBlocksInText(relPath, text) {
|
|
4987
5135
|
const findings = [];
|
|
4988
5136
|
const lines = text.split(/\r?\n/);
|
|
@@ -5033,7 +5181,7 @@ function walkMarkdownFiles2(root, dir, rel, out) {
|
|
|
5033
5181
|
for (const entry of entries) {
|
|
5034
5182
|
if (entry.isDirectory()) {
|
|
5035
5183
|
if (PRUNE_DIRS.has(entry.name)) continue;
|
|
5036
|
-
walkMarkdownFiles2(root,
|
|
5184
|
+
walkMarkdownFiles2(root, join19(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
|
|
5037
5185
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5038
5186
|
out.push(rel ? `${rel}/${entry.name}` : entry.name);
|
|
5039
5187
|
}
|
|
@@ -5047,7 +5195,7 @@ function scanVaultConflictMarkers(vaultRoot) {
|
|
|
5047
5195
|
for (const rel of relPaths) {
|
|
5048
5196
|
let text;
|
|
5049
5197
|
try {
|
|
5050
|
-
text = readFileSync6(
|
|
5198
|
+
text = readFileSync6(join19(vaultRoot, rel), "utf8");
|
|
5051
5199
|
} catch {
|
|
5052
5200
|
continue;
|
|
5053
5201
|
}
|
|
@@ -5058,7 +5206,7 @@ function scanVaultConflictMarkers(vaultRoot) {
|
|
|
5058
5206
|
|
|
5059
5207
|
// src/utils/remote-health.ts
|
|
5060
5208
|
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
5061
|
-
import { join as
|
|
5209
|
+
import { join as join20 } from "path";
|
|
5062
5210
|
import { execFileSync } from "child_process";
|
|
5063
5211
|
var REMOTE_PROBE_TIMEOUT_MS = 3e3;
|
|
5064
5212
|
var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
@@ -5069,7 +5217,7 @@ var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
|
5069
5217
|
}).trim();
|
|
5070
5218
|
function readWikiS3RemoteConfigured(home) {
|
|
5071
5219
|
try {
|
|
5072
|
-
const content = readFileSync7(
|
|
5220
|
+
const content = readFileSync7(join20(home, ".skillwiki", ".env"), "utf8");
|
|
5073
5221
|
for (const line of content.split(/\r?\n/)) {
|
|
5074
5222
|
const trimmed = line.trim();
|
|
5075
5223
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -5094,7 +5242,7 @@ function resolveWikiS3Remote(input) {
|
|
|
5094
5242
|
return readWikiS3RemoteConfigured(input.home);
|
|
5095
5243
|
}
|
|
5096
5244
|
function probeGithubReachability(vaultPath, exec = defaultExec) {
|
|
5097
|
-
if (!existsSync10(
|
|
5245
|
+
if (!existsSync10(join20(vaultPath, ".git"))) return "unknown";
|
|
5098
5246
|
try {
|
|
5099
5247
|
exec("git", ["remote", "get-url", "origin"], vaultPath);
|
|
5100
5248
|
} catch {
|
|
@@ -5160,10 +5308,10 @@ function probeRemoteHealth(input) {
|
|
|
5160
5308
|
|
|
5161
5309
|
// src/utils/satellite-run-health.ts
|
|
5162
5310
|
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
5163
|
-
import { join as
|
|
5311
|
+
import { join as join21 } from "path";
|
|
5164
5312
|
var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
|
|
5165
5313
|
function satelliteLatestRunPath(vault) {
|
|
5166
|
-
return
|
|
5314
|
+
return join21(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
|
|
5167
5315
|
}
|
|
5168
5316
|
function isFailedRunStatus(status) {
|
|
5169
5317
|
return status === "fail" || status === "failure";
|
|
@@ -5216,8 +5364,8 @@ function evaluateSatelliteRunHealth(vault, now) {
|
|
|
5216
5364
|
// src/utils/s3-mount-health.ts
|
|
5217
5365
|
import { execSync } from "child_process";
|
|
5218
5366
|
import { platform } from "os";
|
|
5219
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync as
|
|
5220
|
-
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";
|
|
5221
5369
|
var OS = platform();
|
|
5222
5370
|
function findRcloneMountPid() {
|
|
5223
5371
|
try {
|
|
@@ -5375,7 +5523,7 @@ function detectFuseMount(vaultPath) {
|
|
|
5375
5523
|
return null;
|
|
5376
5524
|
}
|
|
5377
5525
|
function writeTest(dir) {
|
|
5378
|
-
const testFile =
|
|
5526
|
+
const testFile = join22(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
5379
5527
|
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
5380
5528
|
const start = Date.now();
|
|
5381
5529
|
try {
|
|
@@ -5386,24 +5534,24 @@ function writeTest(dir) {
|
|
|
5386
5534
|
const writeMs = Date.now() - start;
|
|
5387
5535
|
const readStart = Date.now();
|
|
5388
5536
|
try {
|
|
5389
|
-
const back =
|
|
5537
|
+
const back = readFile17(testFile, "utf8");
|
|
5390
5538
|
const readMs = Date.now() - readStart;
|
|
5391
5539
|
if (back !== payload) {
|
|
5392
5540
|
try {
|
|
5393
|
-
|
|
5541
|
+
unlinkSync4(testFile);
|
|
5394
5542
|
} catch {
|
|
5395
5543
|
}
|
|
5396
5544
|
return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
|
|
5397
5545
|
}
|
|
5398
5546
|
} catch (e) {
|
|
5399
5547
|
try {
|
|
5400
|
-
|
|
5548
|
+
unlinkSync4(testFile);
|
|
5401
5549
|
} catch {
|
|
5402
5550
|
}
|
|
5403
5551
|
return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
|
|
5404
5552
|
}
|
|
5405
5553
|
try {
|
|
5406
|
-
|
|
5554
|
+
unlinkSync4(testFile);
|
|
5407
5555
|
} catch {
|
|
5408
5556
|
}
|
|
5409
5557
|
return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
|
|
@@ -5475,12 +5623,12 @@ function detectCliChannels(argv, home) {
|
|
|
5475
5623
|
}
|
|
5476
5624
|
const plugin = findPlugin(home);
|
|
5477
5625
|
if (plugin) {
|
|
5478
|
-
const pluginBin =
|
|
5626
|
+
const pluginBin = join23(plugin.installPath, "bin", "skillwiki");
|
|
5479
5627
|
if (existsSync13(pluginBin)) {
|
|
5480
5628
|
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
5481
5629
|
}
|
|
5482
5630
|
}
|
|
5483
|
-
const installBin =
|
|
5631
|
+
const installBin = join23(home, ".claude", "skills", "bin", "skillwiki");
|
|
5484
5632
|
if (existsSync13(installBin)) {
|
|
5485
5633
|
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
5486
5634
|
}
|
|
@@ -5570,9 +5718,9 @@ function checkVaultStructure(resolvedPath) {
|
|
|
5570
5718
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
5571
5719
|
}
|
|
5572
5720
|
const missing = [];
|
|
5573
|
-
if (!existsSync13(
|
|
5721
|
+
if (!existsSync13(join23(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
5574
5722
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
5575
|
-
if (!existsSync13(
|
|
5723
|
+
if (!existsSync13(join23(resolvedPath, dir))) missing.push(dir + "/");
|
|
5576
5724
|
}
|
|
5577
5725
|
if (missing.length === 0) {
|
|
5578
5726
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -5580,7 +5728,7 @@ function checkVaultStructure(resolvedPath) {
|
|
|
5580
5728
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
5581
5729
|
}
|
|
5582
5730
|
function checkSkillsInstalled(home, cwd) {
|
|
5583
|
-
const srcDir = cwd ?
|
|
5731
|
+
const srcDir = cwd ? join23(cwd, "packages", "skills") : void 0;
|
|
5584
5732
|
if (srcDir && existsSync13(srcDir)) {
|
|
5585
5733
|
const found = findInstalledSkillMd(srcDir);
|
|
5586
5734
|
if (found.length > 0) {
|
|
@@ -5594,7 +5742,7 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
5594
5742
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
5595
5743
|
}
|
|
5596
5744
|
}
|
|
5597
|
-
const skillsDir =
|
|
5745
|
+
const skillsDir = join23(home, ".claude", "skills");
|
|
5598
5746
|
if (existsSync13(skillsDir)) {
|
|
5599
5747
|
const found = findInstalledSkillMd(skillsDir);
|
|
5600
5748
|
if (found.length > 0) {
|
|
@@ -5605,10 +5753,10 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
5605
5753
|
}
|
|
5606
5754
|
function checkDuplicateSkills(home) {
|
|
5607
5755
|
const plugin = findPlugin(home);
|
|
5608
|
-
const skillsDir =
|
|
5756
|
+
const skillsDir = join23(home, ".claude", "skills");
|
|
5609
5757
|
const agentSkillDirs = [
|
|
5610
|
-
{ label: "~/.codex/skills/", path:
|
|
5611
|
-
{ label: "~/.agents/skills/", path:
|
|
5758
|
+
{ label: "~/.codex/skills/", path: join23(home, ".codex", "skills") },
|
|
5759
|
+
{ label: "~/.agents/skills/", path: join23(home, ".agents", "skills") }
|
|
5612
5760
|
];
|
|
5613
5761
|
if (!plugin) {
|
|
5614
5762
|
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
@@ -5711,7 +5859,7 @@ async function checkProfiles(home) {
|
|
|
5711
5859
|
}
|
|
5712
5860
|
async function checkProjectLocalOverride(cwd) {
|
|
5713
5861
|
const dir = cwd ?? process.cwd();
|
|
5714
|
-
const envPath =
|
|
5862
|
+
const envPath = join23(dir, ".skillwiki", ".env");
|
|
5715
5863
|
if (existsSync13(envPath)) {
|
|
5716
5864
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
5717
5865
|
}
|
|
@@ -5721,7 +5869,7 @@ function checkVaultGitRemote(resolvedPath) {
|
|
|
5721
5869
|
if (resolvedPath === void 0) {
|
|
5722
5870
|
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5723
5871
|
}
|
|
5724
|
-
if (!existsSync13(
|
|
5872
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5725
5873
|
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
5726
5874
|
}
|
|
5727
5875
|
try {
|
|
@@ -5744,9 +5892,9 @@ function checkObsidianTemplates(resolvedPath) {
|
|
|
5744
5892
|
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5745
5893
|
}
|
|
5746
5894
|
const missing = [];
|
|
5747
|
-
if (!existsSync13(
|
|
5748
|
-
if (!existsSync13(
|
|
5749
|
-
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");
|
|
5750
5898
|
if (missing.length === 0) {
|
|
5751
5899
|
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
5752
5900
|
}
|
|
@@ -5756,7 +5904,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
5756
5904
|
if (resolvedPath === void 0) {
|
|
5757
5905
|
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5758
5906
|
}
|
|
5759
|
-
const rawDir =
|
|
5907
|
+
const rawDir = join23(resolvedPath, "raw");
|
|
5760
5908
|
if (!existsSync13(rawDir)) {
|
|
5761
5909
|
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
5762
5910
|
}
|
|
@@ -5772,7 +5920,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
5772
5920
|
if (entry.name === ".DS_Store") {
|
|
5773
5921
|
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
5774
5922
|
} else if (entry.isDirectory()) {
|
|
5775
|
-
walk(
|
|
5923
|
+
walk(join23(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
5776
5924
|
}
|
|
5777
5925
|
}
|
|
5778
5926
|
})(rawDir, "");
|
|
@@ -5803,7 +5951,7 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
5803
5951
|
if (resolvedPath === void 0) {
|
|
5804
5952
|
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5805
5953
|
}
|
|
5806
|
-
if (!existsSync13(
|
|
5954
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5807
5955
|
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
5808
5956
|
}
|
|
5809
5957
|
let timestamp;
|
|
@@ -5852,7 +6000,7 @@ function checkVaultGitDirty(resolvedPath) {
|
|
|
5852
6000
|
if (resolvedPath === void 0) {
|
|
5853
6001
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
|
|
5854
6002
|
}
|
|
5855
|
-
if (!existsSync13(
|
|
6003
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5856
6004
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
|
|
5857
6005
|
}
|
|
5858
6006
|
try {
|
|
@@ -5921,7 +6069,7 @@ function remoteMainHash(resolvedPath) {
|
|
|
5921
6069
|
}
|
|
5922
6070
|
function checkStaleRemoteMain(resolvedPath) {
|
|
5923
6071
|
if (resolvedPath === void 0) return void 0;
|
|
5924
|
-
if (!existsSync13(
|
|
6072
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) return void 0;
|
|
5925
6073
|
const localOrigin = gitRefHash(resolvedPath, "origin/main");
|
|
5926
6074
|
if (!localOrigin) return void 0;
|
|
5927
6075
|
const remoteMain = remoteMainHash(resolvedPath);
|
|
@@ -5937,7 +6085,7 @@ function checkVaultLocalGit(resolvedPath) {
|
|
|
5937
6085
|
if (resolvedPath === void 0) {
|
|
5938
6086
|
return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5939
6087
|
}
|
|
5940
|
-
if (!existsSync13(
|
|
6088
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5941
6089
|
return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
|
|
5942
6090
|
}
|
|
5943
6091
|
try {
|
|
@@ -5956,7 +6104,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
|
|
|
5956
6104
|
if (resolvedPath === void 0) {
|
|
5957
6105
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
|
|
5958
6106
|
}
|
|
5959
|
-
if (!existsSync13(
|
|
6107
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
5960
6108
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
|
|
5961
6109
|
}
|
|
5962
6110
|
const state = probeGithubReachability(resolvedPath, exec);
|
|
@@ -6000,7 +6148,7 @@ function checkVaultPromotionLag(resolvedPath) {
|
|
|
6000
6148
|
if (resolvedPath === void 0) {
|
|
6001
6149
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
|
|
6002
6150
|
}
|
|
6003
|
-
if (!existsSync13(
|
|
6151
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
6004
6152
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
|
|
6005
6153
|
}
|
|
6006
6154
|
try {
|
|
@@ -6027,7 +6175,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
|
|
|
6027
6175
|
if (resolvedPath === void 0) {
|
|
6028
6176
|
return check("pass", id, label, "No vault path \u2014 check skipped");
|
|
6029
6177
|
}
|
|
6030
|
-
if (!existsSync13(
|
|
6178
|
+
if (!existsSync13(join23(resolvedPath, ".git"))) {
|
|
6031
6179
|
return check("pass", id, label, "No git repo \u2014 check skipped");
|
|
6032
6180
|
}
|
|
6033
6181
|
if (!hasOriginMain(resolvedPath)) {
|
|
@@ -6144,11 +6292,11 @@ async function checkFleetIdentity(input) {
|
|
|
6144
6292
|
}
|
|
6145
6293
|
function pullLogPaths(home) {
|
|
6146
6294
|
const paths = platform2() === "darwin" ? [
|
|
6147
|
-
|
|
6148
|
-
|
|
6295
|
+
join23(home, "Library", "Logs", "wiki-pull.log"),
|
|
6296
|
+
join23(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
|
|
6149
6297
|
] : [
|
|
6150
|
-
|
|
6151
|
-
|
|
6298
|
+
join23(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
|
|
6299
|
+
join23(home, "Library", "Logs", "wiki-pull.log")
|
|
6152
6300
|
];
|
|
6153
6301
|
return [...new Set(paths)];
|
|
6154
6302
|
}
|
|
@@ -6188,7 +6336,7 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
6188
6336
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
6189
6337
|
}
|
|
6190
6338
|
const mountPoint = fuse.mountPoint;
|
|
6191
|
-
const conceptsDir =
|
|
6339
|
+
const conceptsDir = join23(resolvedPath, "concepts");
|
|
6192
6340
|
if (!existsSync13(conceptsDir)) {
|
|
6193
6341
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
6194
6342
|
}
|
|
@@ -6371,7 +6519,7 @@ function checkWriteTest(resolvedPath) {
|
|
|
6371
6519
|
if (!fuse) {
|
|
6372
6520
|
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
6373
6521
|
}
|
|
6374
|
-
const conceptsDir =
|
|
6522
|
+
const conceptsDir = join23(resolvedPath, "concepts");
|
|
6375
6523
|
if (!existsSync13(conceptsDir)) {
|
|
6376
6524
|
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
6377
6525
|
}
|
|
@@ -6473,7 +6621,7 @@ function checkVaultSyncPullHelper(home, env) {
|
|
|
6473
6621
|
);
|
|
6474
6622
|
}
|
|
6475
6623
|
function checkVaultSyncReviewRequiredJournals(vaultPath) {
|
|
6476
|
-
if (!vaultPath || !existsSync13(
|
|
6624
|
+
if (!vaultPath || !existsSync13(join23(vaultPath, ".git"))) {
|
|
6477
6625
|
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
|
|
6478
6626
|
}
|
|
6479
6627
|
try {
|
|
@@ -6494,7 +6642,7 @@ function checkVaultSyncReviewRequiredJournals(vaultPath) {
|
|
|
6494
6642
|
}
|
|
6495
6643
|
function readVaultSyncConfig(home) {
|
|
6496
6644
|
try {
|
|
6497
|
-
const content = readFileSync10(
|
|
6645
|
+
const content = readFileSync10(join23(home, ".skillwiki", ".env"), "utf8");
|
|
6498
6646
|
let installed = false;
|
|
6499
6647
|
let role;
|
|
6500
6648
|
let serviceScope;
|
|
@@ -6623,7 +6771,7 @@ function snapshotterHealthChecks(scope, logDir, env) {
|
|
|
6623
6771
|
let completionOutcome = "unknown";
|
|
6624
6772
|
const logRecords = fixture ? fixture.log_records : (() => {
|
|
6625
6773
|
try {
|
|
6626
|
-
const content = readFileSync10(
|
|
6774
|
+
const content = readFileSync10(join23(logDir, "wiki-snapshot.log"), "utf8");
|
|
6627
6775
|
return content.split(/\r?\n/).filter(Boolean);
|
|
6628
6776
|
} catch {
|
|
6629
6777
|
return [];
|
|
@@ -6693,10 +6841,10 @@ function vaultSyncChecks(input) {
|
|
|
6693
6841
|
];
|
|
6694
6842
|
}
|
|
6695
6843
|
const isMac = os === "darwin";
|
|
6696
|
-
const logDir = input.logDir ?? (isMac ?
|
|
6697
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
6698
|
-
const filterPath = input.filterPath ??
|
|
6699
|
-
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");
|
|
6700
6848
|
const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
6701
6849
|
const snapshotPath = input.snapshotScriptPath ?? (existsSync13(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
|
|
6702
6850
|
if (input.vaultSyncRole === "snapshotter") {
|
|
@@ -6752,7 +6900,7 @@ function vaultSyncChecks(input) {
|
|
|
6752
6900
|
}
|
|
6753
6901
|
return [c12, ...healthChecks, cFetch2, c42, c52];
|
|
6754
6902
|
}
|
|
6755
|
-
const pushScriptPath =
|
|
6903
|
+
const pushScriptPath = join23(shareDir, "wiki-push.sh");
|
|
6756
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`);
|
|
6757
6905
|
let c2;
|
|
6758
6906
|
try {
|
|
@@ -6804,7 +6952,7 @@ function vaultSyncChecks(input) {
|
|
|
6804
6952
|
"Scheduler check failed \u2014 run vault-sync-install"
|
|
6805
6953
|
);
|
|
6806
6954
|
}
|
|
6807
|
-
const logFile =
|
|
6955
|
+
const logFile = join23(logDir, "wiki-push.log");
|
|
6808
6956
|
let c3;
|
|
6809
6957
|
try {
|
|
6810
6958
|
const logContent = readFileSync10(logFile, "utf8");
|
|
@@ -6875,7 +7023,7 @@ function vaultSyncChecks(input) {
|
|
|
6875
7023
|
`Log directory not found at ${logDir}`
|
|
6876
7024
|
);
|
|
6877
7025
|
}
|
|
6878
|
-
const fetchLogFile =
|
|
7026
|
+
const fetchLogFile = join23(logDir, "wiki-fetch.log");
|
|
6879
7027
|
let cFetch;
|
|
6880
7028
|
try {
|
|
6881
7029
|
const logContent = readFileSync10(fetchLogFile, "utf8");
|
|
@@ -7019,15 +7167,15 @@ function findSkillMd(dir) {
|
|
|
7019
7167
|
}
|
|
7020
7168
|
for (const entry of entries) {
|
|
7021
7169
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
7022
|
-
results.push(
|
|
7170
|
+
results.push(join23(dir, entry.name));
|
|
7023
7171
|
} else if (entry.isDirectory()) {
|
|
7024
|
-
results.push(...findSkillMd(
|
|
7172
|
+
results.push(...findSkillMd(join23(dir, entry.name)));
|
|
7025
7173
|
}
|
|
7026
7174
|
}
|
|
7027
7175
|
return results;
|
|
7028
7176
|
}
|
|
7029
7177
|
function findInstalledSkillMd(dir) {
|
|
7030
|
-
const directSkills = findSkillNames(dir).map((name) =>
|
|
7178
|
+
const directSkills = findSkillNames(dir).map((name) => join23(dir, name, "SKILL.md"));
|
|
7031
7179
|
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
7032
7180
|
}
|
|
7033
7181
|
function findSkillNames(dir) {
|
|
@@ -7039,7 +7187,7 @@ function findSkillNames(dir) {
|
|
|
7039
7187
|
return results;
|
|
7040
7188
|
}
|
|
7041
7189
|
for (const entry of entries) {
|
|
7042
|
-
if (entry.isDirectory() && existsSync13(
|
|
7190
|
+
if (entry.isDirectory() && existsSync13(join23(dir, entry.name, "SKILL.md"))) {
|
|
7043
7191
|
results.push(entry.name);
|
|
7044
7192
|
}
|
|
7045
7193
|
}
|
|
@@ -7087,7 +7235,7 @@ async function vaultMetrics(resolvedPath) {
|
|
|
7087
7235
|
}
|
|
7088
7236
|
let logLines = 0;
|
|
7089
7237
|
try {
|
|
7090
|
-
logLines = readFileSync10(
|
|
7238
|
+
logLines = readFileSync10(join23(scanRoot, "log.md"), "utf8").split("\n").length;
|
|
7091
7239
|
} catch {
|
|
7092
7240
|
}
|
|
7093
7241
|
return [
|
|
@@ -7222,7 +7370,7 @@ function readCliPackageJson(baseUrl = import.meta.url) {
|
|
|
7222
7370
|
// src/utils/vault-write-gates.ts
|
|
7223
7371
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
7224
7372
|
import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
7225
|
-
import { join as
|
|
7373
|
+
import { join as join24, relative as relative3 } from "path";
|
|
7226
7374
|
var DEFAULT_DIRTY_VOLUME_THRESHOLD = 50;
|
|
7227
7375
|
var DEFAULT_CAPTURE_BUDGET = 20;
|
|
7228
7376
|
var DEFAULT_NO_DECISION_STREAK = 3;
|
|
@@ -7321,7 +7469,7 @@ function measureDirtyVolume(vault) {
|
|
|
7321
7469
|
if (!rel) continue;
|
|
7322
7470
|
if (code === "??") {
|
|
7323
7471
|
untracked += 1;
|
|
7324
|
-
const abs =
|
|
7472
|
+
const abs = join24(vault, rel);
|
|
7325
7473
|
if (existsSync14(abs) && statSync3(abs).isDirectory()) {
|
|
7326
7474
|
const files = listFilesRecursive(abs);
|
|
7327
7475
|
expanded += files.length;
|
|
@@ -7358,7 +7506,7 @@ function listFilesRecursive(dir) {
|
|
|
7358
7506
|
}
|
|
7359
7507
|
for (const name of entries) {
|
|
7360
7508
|
if (name === ".git") continue;
|
|
7361
|
-
const p =
|
|
7509
|
+
const p = join24(dir, name);
|
|
7362
7510
|
try {
|
|
7363
7511
|
const st = statSync3(p);
|
|
7364
7512
|
if (st.isDirectory()) out.push(...listFilesRecursive(p));
|
|
@@ -7489,7 +7637,7 @@ function listProjectDayCaptures(vault, project, day) {
|
|
|
7489
7637
|
return;
|
|
7490
7638
|
}
|
|
7491
7639
|
for (const name of entries) {
|
|
7492
|
-
const abs =
|
|
7640
|
+
const abs = join24(dir, name);
|
|
7493
7641
|
const rel = relBase ? `${relBase}/${name}` : name;
|
|
7494
7642
|
try {
|
|
7495
7643
|
const st = statSync3(abs);
|
|
@@ -7499,16 +7647,16 @@ function listProjectDayCaptures(vault, project, day) {
|
|
|
7499
7647
|
}
|
|
7500
7648
|
}
|
|
7501
7649
|
};
|
|
7502
|
-
walk(
|
|
7503
|
-
walk(
|
|
7504
|
-
walk(
|
|
7505
|
-
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");
|
|
7506
7654
|
if (existsSync14(workRoot)) {
|
|
7507
7655
|
try {
|
|
7508
7656
|
for (const name of readdirSync4(workRoot)) {
|
|
7509
7657
|
if (!name.startsWith(day)) continue;
|
|
7510
7658
|
if (!/investigate|pilot-q|research|cycle|dev-loop/.test(name)) continue;
|
|
7511
|
-
const abs =
|
|
7659
|
+
const abs = join24(workRoot, name);
|
|
7512
7660
|
if (statSync3(abs).isDirectory()) {
|
|
7513
7661
|
for (const f of listFilesRecursive(abs)) {
|
|
7514
7662
|
const rel = relative3(vault, f).replace(/\\/g, "/");
|
|
@@ -7614,8 +7762,8 @@ function runWritePreflight(input) {
|
|
|
7614
7762
|
// src/commands/observe.ts
|
|
7615
7763
|
import { mkdir as mkdir6, writeFile as writeFile4 } from "fs/promises";
|
|
7616
7764
|
import { existsSync as existsSync15, statSync as statSync4 } from "fs";
|
|
7617
|
-
import { join as
|
|
7618
|
-
import { createHash as
|
|
7765
|
+
import { join as join25 } from "path";
|
|
7766
|
+
import { createHash as createHash7 } from "crypto";
|
|
7619
7767
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
7620
7768
|
function slugify(text) {
|
|
7621
7769
|
const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -7663,7 +7811,7 @@ async function runObserve(input) {
|
|
|
7663
7811
|
};
|
|
7664
7812
|
}
|
|
7665
7813
|
}
|
|
7666
|
-
const transcriptsDir =
|
|
7814
|
+
const transcriptsDir = join25(input.vault, "raw", "transcripts");
|
|
7667
7815
|
try {
|
|
7668
7816
|
await mkdir6(transcriptsDir, { recursive: true });
|
|
7669
7817
|
} catch {
|
|
@@ -7675,16 +7823,16 @@ async function runObserve(input) {
|
|
|
7675
7823
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7676
7824
|
const slug = slugify(input.text);
|
|
7677
7825
|
const fileName = `${today}-observation-${slug}.md`;
|
|
7678
|
-
const filePath =
|
|
7826
|
+
const filePath = join25(transcriptsDir, fileName);
|
|
7679
7827
|
const body = `
|
|
7680
7828
|
${input.text.trim()}
|
|
7681
7829
|
`;
|
|
7682
|
-
const
|
|
7830
|
+
const sha2562 = createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex");
|
|
7683
7831
|
const frontmatterLines = [
|
|
7684
7832
|
"---",
|
|
7685
7833
|
"source_url:",
|
|
7686
7834
|
`ingested: ${today}`,
|
|
7687
|
-
`sha256: ${
|
|
7835
|
+
`sha256: ${sha2562}`,
|
|
7688
7836
|
`kind: ${kind}`
|
|
7689
7837
|
];
|
|
7690
7838
|
if (input.project) {
|
|
@@ -7707,17 +7855,17 @@ ${input.text.trim()}
|
|
|
7707
7855
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7708
7856
|
});
|
|
7709
7857
|
const relPath = `raw/transcripts/${fileName}`;
|
|
7710
|
-
const humanHint = `created ${relPath} (${
|
|
7858
|
+
const humanHint = `created ${relPath} (${sha2562.slice(0, 12)}...)`;
|
|
7711
7859
|
return {
|
|
7712
7860
|
exitCode: ExitCode.OK,
|
|
7713
|
-
result: ok({ path: relPath, sha256, humanHint })
|
|
7861
|
+
result: ok({ path: relPath, sha256: sha2562, humanHint })
|
|
7714
7862
|
};
|
|
7715
7863
|
}
|
|
7716
7864
|
|
|
7717
7865
|
// src/commands/memory.ts
|
|
7718
|
-
import { createHash as
|
|
7719
|
-
import { mkdir as mkdir7, readFile as
|
|
7720
|
-
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";
|
|
7721
7869
|
|
|
7722
7870
|
// src/utils/memory-authority.ts
|
|
7723
7871
|
var TIER_RANK = {
|
|
@@ -7836,8 +7984,8 @@ async function runMemoryIndex(input) {
|
|
|
7836
7984
|
}
|
|
7837
7985
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7838
7986
|
const relCachePath = memoryCacheRelPath(input.project);
|
|
7839
|
-
const absCachePath =
|
|
7840
|
-
await mkdir7(
|
|
7987
|
+
const absCachePath = join26(input.vault, relCachePath);
|
|
7988
|
+
await mkdir7(join26(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
|
|
7841
7989
|
await writeFile5(absCachePath, `${JSON.stringify({
|
|
7842
7990
|
generated_at: generatedAt,
|
|
7843
7991
|
project: input.project,
|
|
@@ -8029,7 +8177,7 @@ async function buildMemoryIndexState(pages, project) {
|
|
|
8029
8177
|
}
|
|
8030
8178
|
async function checkMemoryIndex(vault, project, current) {
|
|
8031
8179
|
const relCachePath = memoryCacheRelPath(project);
|
|
8032
|
-
const cacheText = await readIfExists2(
|
|
8180
|
+
const cacheText = await readIfExists2(join26(vault, relCachePath));
|
|
8033
8181
|
if (!cacheText) {
|
|
8034
8182
|
return {
|
|
8035
8183
|
ok: true,
|
|
@@ -8146,7 +8294,7 @@ async function readMemoryPage(page, project, warnings) {
|
|
|
8146
8294
|
title,
|
|
8147
8295
|
summary: summarize(body),
|
|
8148
8296
|
updated,
|
|
8149
|
-
hash:
|
|
8297
|
+
hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
|
|
8150
8298
|
topics,
|
|
8151
8299
|
project,
|
|
8152
8300
|
...stringField(fm.data.memory_kind) ? { memory_kind: stringField(fm.data.memory_kind) } : {},
|
|
@@ -8348,7 +8496,7 @@ function matchPastFailureTarget(target, candidateBody) {
|
|
|
8348
8496
|
}
|
|
8349
8497
|
if (!FAILURE_MARKER_RE.test(candidateBody)) return null;
|
|
8350
8498
|
const candidateTokens = new Set(tokenizeForMatch(candidate));
|
|
8351
|
-
const overlappingTokens = target.distinctiveTokens.filter((
|
|
8499
|
+
const overlappingTokens = target.distinctiveTokens.filter((token2) => candidateTokens.has(token2));
|
|
8352
8500
|
if (overlappingTokens.length >= 2) {
|
|
8353
8501
|
return { reason: `distinctive target terms overlapped prior failure memory (${overlappingTokens.slice(0, 4).join(", ")})` };
|
|
8354
8502
|
}
|
|
@@ -8364,7 +8512,7 @@ function extractCodeLikeNeedles(value) {
|
|
|
8364
8512
|
return (value.match(/[A-Za-z0-9_.:-]*[_.:-][A-Za-z0-9_.:-]+/g) ?? []).filter((needle) => /[A-Za-z]/.test(needle));
|
|
8365
8513
|
}
|
|
8366
8514
|
function extractDistinctiveTargetTokens(value) {
|
|
8367
|
-
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)));
|
|
8368
8516
|
}
|
|
8369
8517
|
function hasDistinctiveNeedle(value) {
|
|
8370
8518
|
return extractDistinctiveTargetTokens(value).length > 0;
|
|
@@ -8422,23 +8570,23 @@ function renderMemoryIndexStatusHint(status) {
|
|
|
8422
8570
|
}
|
|
8423
8571
|
async function readIfExists2(path) {
|
|
8424
8572
|
try {
|
|
8425
|
-
return await
|
|
8573
|
+
return await readFile18(path, "utf8");
|
|
8426
8574
|
} catch {
|
|
8427
8575
|
return "";
|
|
8428
8576
|
}
|
|
8429
8577
|
}
|
|
8430
8578
|
async function collectImportFiles(source) {
|
|
8431
|
-
const st = await
|
|
8579
|
+
const st = await stat4(source);
|
|
8432
8580
|
if (st.isFile()) return isImportCandidate(source) ? [source] : [];
|
|
8433
8581
|
const files = [];
|
|
8434
8582
|
await walkImportFiles(source, files);
|
|
8435
8583
|
return files.sort((a, b) => a.localeCompare(b));
|
|
8436
8584
|
}
|
|
8437
8585
|
async function walkImportFiles(dir, out) {
|
|
8438
|
-
const entries = await
|
|
8586
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
8439
8587
|
for (const entry of entries) {
|
|
8440
8588
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
8441
|
-
const path =
|
|
8589
|
+
const path = join26(dir, entry.name);
|
|
8442
8590
|
if (entry.isDirectory()) {
|
|
8443
8591
|
await walkImportFiles(path, out);
|
|
8444
8592
|
} else if (entry.isFile() && isImportCandidate(path)) {
|
|
@@ -8451,9 +8599,9 @@ function isImportCandidate(path) {
|
|
|
8451
8599
|
return ext === ".md" || ext === ".txt";
|
|
8452
8600
|
}
|
|
8453
8601
|
async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
8454
|
-
const st = await
|
|
8602
|
+
const st = await stat4(file);
|
|
8455
8603
|
const sourceKind = classifyImportSource(file);
|
|
8456
|
-
const hash =
|
|
8604
|
+
const hash = createHash8("sha256").update(await readFile18(file)).digest("hex");
|
|
8457
8605
|
const baseEntry = {
|
|
8458
8606
|
source_path: file,
|
|
8459
8607
|
source_kind: sourceKind,
|
|
@@ -8476,7 +8624,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
|
8476
8624
|
reason: "policy_source_not_imported"
|
|
8477
8625
|
};
|
|
8478
8626
|
}
|
|
8479
|
-
const text = await
|
|
8627
|
+
const text = await readFile18(file, "utf8");
|
|
8480
8628
|
const extracted = extractImportText(text, sourceKind);
|
|
8481
8629
|
if (!extracted) {
|
|
8482
8630
|
return {
|
|
@@ -8505,8 +8653,8 @@ async function writeImportCapture(vault, entry, today) {
|
|
|
8505
8653
|
const content = hiddenString(entry, "__content");
|
|
8506
8654
|
const project = hiddenString(entry, "__project");
|
|
8507
8655
|
const relPath = await availableImportPath(vault, entry.proposed_path);
|
|
8508
|
-
const absPath =
|
|
8509
|
-
await mkdir7(
|
|
8656
|
+
const absPath = join26(vault, relPath);
|
|
8657
|
+
await mkdir7(join26(vault, "raw", "transcripts"), { recursive: true });
|
|
8510
8658
|
await writeFile5(absPath, renderImportCapture(entry, content, project, today), "utf8");
|
|
8511
8659
|
const validation = await runValidate({ file: absPath });
|
|
8512
8660
|
return {
|
|
@@ -8522,7 +8670,7 @@ async function availableImportPath(vault, proposed) {
|
|
|
8522
8670
|
const stem = proposed.slice(0, -ext.length);
|
|
8523
8671
|
let candidate = proposed;
|
|
8524
8672
|
let i = 2;
|
|
8525
|
-
while (await readIfExists2(
|
|
8673
|
+
while (await readIfExists2(join26(vault, candidate))) {
|
|
8526
8674
|
candidate = `${stem}-${i}${ext}`;
|
|
8527
8675
|
i++;
|
|
8528
8676
|
}
|
|
@@ -8707,10 +8855,10 @@ function memoryCacheRelPath(project) {
|
|
|
8707
8855
|
}
|
|
8708
8856
|
async function readMemoryCache(vault, project) {
|
|
8709
8857
|
if (project) {
|
|
8710
|
-
const projectCache = await readIfExists2(
|
|
8858
|
+
const projectCache = await readIfExists2(join26(vault, memoryCacheRelPath(project)));
|
|
8711
8859
|
if (projectCache) return projectCache;
|
|
8712
8860
|
}
|
|
8713
|
-
return readIfExists2(
|
|
8861
|
+
return readIfExists2(join26(vault, ".skillwiki", "memory-topics.json"));
|
|
8714
8862
|
}
|
|
8715
8863
|
function dedupePages(pages) {
|
|
8716
8864
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8801,8 +8949,8 @@ function slugify2(value) {
|
|
|
8801
8949
|
}
|
|
8802
8950
|
|
|
8803
8951
|
// src/commands/query.ts
|
|
8804
|
-
import { readFile as
|
|
8805
|
-
import { join as
|
|
8952
|
+
import { readFile as readFile19, stat as stat5 } from "fs/promises";
|
|
8953
|
+
import { join as join27 } from "path";
|
|
8806
8954
|
var W_KEYWORD = 2;
|
|
8807
8955
|
var W_SOURCE_OVERLAP = 4;
|
|
8808
8956
|
var W_WIKILINK = 3;
|
|
@@ -8868,8 +9016,22 @@ async function runQuery(input) {
|
|
|
8868
9016
|
type: page.type
|
|
8869
9017
|
};
|
|
8870
9018
|
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
|
|
8871
|
-
|
|
8872
|
-
|
|
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
|
+
};
|
|
8873
9035
|
}
|
|
8874
9036
|
function scoreSourceOverlap(page, allPages, seedPaths) {
|
|
8875
9037
|
if (page.sources.length === 0) return 0;
|
|
@@ -8923,10 +9085,10 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
8923
9085
|
return score;
|
|
8924
9086
|
}
|
|
8925
9087
|
async function loadOrBuildGraph(vault) {
|
|
8926
|
-
const graphPath =
|
|
9088
|
+
const graphPath = join27(vault, ".skillwiki", "graph.json");
|
|
8927
9089
|
let needsBuild = false;
|
|
8928
9090
|
try {
|
|
8929
|
-
const fileStat = await
|
|
9091
|
+
const fileStat = await stat5(graphPath);
|
|
8930
9092
|
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
8931
9093
|
if (ageHours > 24) needsBuild = true;
|
|
8932
9094
|
} catch {
|
|
@@ -8937,7 +9099,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
8937
9099
|
if (buildResult.exitCode !== 0) return null;
|
|
8938
9100
|
}
|
|
8939
9101
|
try {
|
|
8940
|
-
const raw = await
|
|
9102
|
+
const raw = await readFile19(graphPath, "utf8");
|
|
8941
9103
|
return JSON.parse(raw);
|
|
8942
9104
|
} catch {
|
|
8943
9105
|
return null;
|
|
@@ -8952,7 +9114,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
8952
9114
|
import { z } from "zod";
|
|
8953
9115
|
|
|
8954
9116
|
// src/mcp/vault-resolve.ts
|
|
8955
|
-
import { join as
|
|
9117
|
+
import { join as join28, resolve as resolve7 } from "path";
|
|
8956
9118
|
|
|
8957
9119
|
// src/mcp/allowlist.ts
|
|
8958
9120
|
import { resolve as resolve6, sep as sep4 } from "path";
|
|
@@ -9014,7 +9176,7 @@ async function resolveMcpVault(input) {
|
|
|
9014
9176
|
return ok({ vault: vaultPath, source });
|
|
9015
9177
|
}
|
|
9016
9178
|
function defaultGraphOut(vault) {
|
|
9017
|
-
return
|
|
9179
|
+
return join28(vault, ".skillwiki", "graph.json");
|
|
9018
9180
|
}
|
|
9019
9181
|
|
|
9020
9182
|
// src/mcp/result-format.ts
|
|
@@ -9031,7 +9193,7 @@ function formatToolResult(payload) {
|
|
|
9031
9193
|
// src/mcp/audit-log.ts
|
|
9032
9194
|
import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
9033
9195
|
import { homedir } from "os";
|
|
9034
|
-
import { join as
|
|
9196
|
+
import { join as join29 } from "path";
|
|
9035
9197
|
function auditEnabled() {
|
|
9036
9198
|
const v = process.env.SKILLWIKI_MCP_AUDIT;
|
|
9037
9199
|
if (v === "0" || v === "false") return false;
|
|
@@ -9043,7 +9205,7 @@ function auditSink() {
|
|
|
9043
9205
|
function auditFilePath() {
|
|
9044
9206
|
const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
|
|
9045
9207
|
if (custom && custom.length > 0) return custom;
|
|
9046
|
-
return
|
|
9208
|
+
return join29(homedir(), ".skillwiki", "mcp-audit.jsonl");
|
|
9047
9209
|
}
|
|
9048
9210
|
function auditMcpToolCall(entry) {
|
|
9049
9211
|
if (!auditEnabled()) return;
|
|
@@ -9053,7 +9215,7 @@ function auditMcpToolCall(entry) {
|
|
|
9053
9215
|
return;
|
|
9054
9216
|
}
|
|
9055
9217
|
const path = auditFilePath();
|
|
9056
|
-
mkdirSync5(
|
|
9218
|
+
mkdirSync5(join29(path, ".."), { recursive: true });
|
|
9057
9219
|
appendFileSync(path, line, "utf8");
|
|
9058
9220
|
}
|
|
9059
9221
|
async function runMcpToolHandler(tool, input, fn) {
|
|
@@ -9273,8 +9435,8 @@ function registerMcpMutatingTools(server) {
|
|
|
9273
9435
|
}
|
|
9274
9436
|
|
|
9275
9437
|
// src/mcp/resources.ts
|
|
9276
|
-
import { readFile as
|
|
9277
|
-
import { join as
|
|
9438
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
9439
|
+
import { join as join31 } from "path";
|
|
9278
9440
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9279
9441
|
|
|
9280
9442
|
// src/mcp/lint-bucket.ts
|
|
@@ -9402,8 +9564,8 @@ async function fetchQueryPreview(input) {
|
|
|
9402
9564
|
}
|
|
9403
9565
|
|
|
9404
9566
|
// src/mcp/graph-html.ts
|
|
9405
|
-
import { readFile as
|
|
9406
|
-
import { join as
|
|
9567
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
9568
|
+
import { join as join30 } from "path";
|
|
9407
9569
|
import { existsSync as existsSync16 } from "fs";
|
|
9408
9570
|
var TYPE_COLORS = {
|
|
9409
9571
|
entities: "#e74c3c",
|
|
@@ -9472,7 +9634,7 @@ ${nodeSvg}
|
|
|
9472
9634
|
return { html, node_count: nodes.length, edge_count: edges.length, truncated };
|
|
9473
9635
|
}
|
|
9474
9636
|
async function fetchGraphHtmlReport(input) {
|
|
9475
|
-
const graphPath = input.graphPath ??
|
|
9637
|
+
const graphPath = input.graphPath ?? join30(input.vault, ".skillwiki", "graph.json");
|
|
9476
9638
|
const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
|
|
9477
9639
|
if (!existsSync16(graphPath)) {
|
|
9478
9640
|
return {
|
|
@@ -9482,7 +9644,7 @@ async function fetchGraphHtmlReport(input) {
|
|
|
9482
9644
|
}
|
|
9483
9645
|
let raw;
|
|
9484
9646
|
try {
|
|
9485
|
-
raw = await
|
|
9647
|
+
raw = await readFile20(graphPath, "utf8");
|
|
9486
9648
|
} catch (e) {
|
|
9487
9649
|
return {
|
|
9488
9650
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -9546,7 +9708,7 @@ async function fetchStaleSummary(input) {
|
|
|
9546
9708
|
|
|
9547
9709
|
// src/mcp/resources.ts
|
|
9548
9710
|
async function readVaultFile(vault, rel) {
|
|
9549
|
-
return
|
|
9711
|
+
return readFile21(join31(vault, rel), "utf8");
|
|
9550
9712
|
}
|
|
9551
9713
|
async function tailLines(text, lines) {
|
|
9552
9714
|
const parts = text.split(/\r?\n/);
|
|
@@ -9632,9 +9794,9 @@ function registerMcpResources(server) {
|
|
|
9632
9794
|
if (!v.ok) {
|
|
9633
9795
|
return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
|
|
9634
9796
|
}
|
|
9635
|
-
const path =
|
|
9797
|
+
const path = join31(v.data.vault, ".skillwiki", "graph.json");
|
|
9636
9798
|
try {
|
|
9637
|
-
const raw = await
|
|
9799
|
+
const raw = await readFile21(path, "utf8");
|
|
9638
9800
|
const graph = JSON.parse(raw);
|
|
9639
9801
|
const adjacency = graph.adjacency ?? {};
|
|
9640
9802
|
const nodes = Object.keys(adjacency);
|
|
@@ -9947,11 +10109,6 @@ export {
|
|
|
9947
10109
|
readLastOp,
|
|
9948
10110
|
appendLastOp,
|
|
9949
10111
|
clearLastOp,
|
|
9950
|
-
eventPathFor,
|
|
9951
|
-
validateLogEvent,
|
|
9952
|
-
canonicalEventJson,
|
|
9953
|
-
writeLogEvent,
|
|
9954
|
-
readLogEvents,
|
|
9955
10112
|
runLogAppend,
|
|
9956
10113
|
renderIndexUpsert,
|
|
9957
10114
|
upsertIndexEntry,
|
|
@@ -9967,7 +10124,6 @@ export {
|
|
|
9967
10124
|
resolveInitTimePath,
|
|
9968
10125
|
resolveRuntimePath,
|
|
9969
10126
|
runOrphans,
|
|
9970
|
-
extractCitationMarkers,
|
|
9971
10127
|
runAudit,
|
|
9972
10128
|
findPlugin,
|
|
9973
10129
|
extractTaxonomy,
|
|
@@ -9979,6 +10135,8 @@ export {
|
|
|
9979
10135
|
runIndexCheck,
|
|
9980
10136
|
renderProjectIndex,
|
|
9981
10137
|
runProjectIndex,
|
|
10138
|
+
planRawStructuralMove,
|
|
10139
|
+
applyRawStructuralMove,
|
|
9982
10140
|
runStale,
|
|
9983
10141
|
runPagesize,
|
|
9984
10142
|
runLogRotate,
|
|
@@ -9989,8 +10147,10 @@ export {
|
|
|
9989
10147
|
buildRemoteObjectPath,
|
|
9990
10148
|
isValidRemoteDeleteCap,
|
|
9991
10149
|
planAndMaybePruneRemoteObjects,
|
|
9992
|
-
runDedup,
|
|
9993
10150
|
safeWritePage,
|
|
10151
|
+
rewriteRawSourceReferences,
|
|
10152
|
+
snapshotMaintainedPageState,
|
|
10153
|
+
runDedup,
|
|
9994
10154
|
runFrontmatterFix,
|
|
9995
10155
|
fixPathTooLong,
|
|
9996
10156
|
assessSourceIdentity,
|