skillwiki 0.10.48 → 0.10.50
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-UHZBOOMK.js → chunk-74OSLCXE.js} +1 -1
- package/dist/{chunk-FXASPB3C.js → chunk-EUTNN4RN.js} +1949 -1296
- package/dist/{chunk-PLUHIOCQ.js → chunk-GAHMWLWU.js} +174 -175
- package/dist/chunk-HBQTTYXZ.js +1045 -0
- package/dist/{chunk-EVQASYII.js → chunk-IJ7DD7QZ.js} +59 -0
- package/dist/{chunk-S4NZQOXI.js → chunk-MKZKMSYM.js} +2402 -1414
- package/dist/{chunk-GAU4D25O.js → chunk-O3HCB7R2.js} +2 -2
- package/dist/cli.js +2391 -1789
- package/dist/{index-projection-BJNZUPH6.js → index-projection-EXBX32RR.js} +3 -3
- package/dist/{managed-write-preflight-H7KVK2KI.js → managed-write-preflight-SJDRNJSU.js} +4 -4
- package/dist/skillwiki-mcp.js +7 -6
- package/dist/sources-BFYEFTS4.js +9 -0
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +2 -2
- package/skills/.codex-plugin/plugin.json +3 -3
- package/skills/README.md +1 -1
- package/skills/agents/wiki-ingest.md +2 -0
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +13 -8
- package/skills/skills/wiki-freshness-repair/SKILL.md +94 -0
- package/skills/skills/wiki-ingest/SKILL.md +8 -0
- package/skills/skills/wiki-query/SKILL.md +1 -1
- package/skills/using-skillwiki/SKILL.md +13 -8
- package/skills/wiki-freshness-repair/SKILL.md +94 -0
- package/skills/wiki-ingest/SKILL.md +8 -0
- package/skills/wiki-query/SKILL.md +1 -1
- package/dist/chunk-QNSD72FH.js +0 -466
- package/dist/sources-CMV5DKS5.js +0 -9
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
renderRootIndex,
|
|
8
8
|
writeRootIndexProjection
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-O3HCB7R2.js";
|
|
10
10
|
import {
|
|
11
11
|
CONFIG_KEYS,
|
|
12
12
|
acquireOwnedSyncLock,
|
|
@@ -35,15 +35,28 @@ import {
|
|
|
35
35
|
snapshotterAliasForLocalHost,
|
|
36
36
|
toUndirectedWeighted,
|
|
37
37
|
writeDotenv
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-EUTNN4RN.js";
|
|
39
39
|
import {
|
|
40
40
|
atomicWriteText,
|
|
41
41
|
prepareTypedPage
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-74OSLCXE.js";
|
|
43
|
+
import {
|
|
44
|
+
applySourceCompileClaim,
|
|
45
|
+
applySourceCompilePublished,
|
|
46
|
+
applySourceCompileRelease,
|
|
47
|
+
applySourceReview,
|
|
48
|
+
listCompileStatus,
|
|
49
|
+
listSourceReviews,
|
|
50
|
+
planSourceCompileClaim,
|
|
51
|
+
planSourceCompilePublished,
|
|
52
|
+
planSourceCompileRelease,
|
|
53
|
+
planSourceReview,
|
|
54
|
+
runSourcesPending
|
|
55
|
+
} from "./chunk-HBQTTYXZ.js";
|
|
43
56
|
import {
|
|
44
57
|
eventPathFor,
|
|
45
58
|
writeLogEvent
|
|
46
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-GAHMWLWU.js";
|
|
47
60
|
import {
|
|
48
61
|
CompoundSchema,
|
|
49
62
|
ExitCode,
|
|
@@ -64,7 +77,7 @@ import {
|
|
|
64
77
|
scanVault,
|
|
65
78
|
splitFrontmatter,
|
|
66
79
|
systemdPropertyFor
|
|
67
|
-
} from "./chunk-
|
|
80
|
+
} from "./chunk-IJ7DD7QZ.js";
|
|
68
81
|
|
|
69
82
|
// src/commands/log-append.ts
|
|
70
83
|
import { readFile, stat } from "fs/promises";
|
|
@@ -1056,11 +1069,13 @@ async function runConfigPath(input) {
|
|
|
1056
1069
|
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync2(filePath), humanHint: filePath }) };
|
|
1057
1070
|
}
|
|
1058
1071
|
|
|
1059
|
-
// src/
|
|
1060
|
-
import { existsSync as
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
import {
|
|
1072
|
+
// src/doctor/runner.ts
|
|
1073
|
+
import { existsSync as existsSync17 } from "fs";
|
|
1074
|
+
|
|
1075
|
+
// src/doctor/probes/environment.ts
|
|
1076
|
+
import { existsSync as existsSync4, lstatSync, readlinkSync } from "fs";
|
|
1077
|
+
import { resolve as resolve2, join as join7 } from "path";
|
|
1078
|
+
import { execSync } from "child_process";
|
|
1064
1079
|
|
|
1065
1080
|
// src/utils/plugin-registry.ts
|
|
1066
1081
|
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
@@ -1180,792 +1195,396 @@ function parseTomlScalar(rawValue) {
|
|
|
1180
1195
|
return value;
|
|
1181
1196
|
}
|
|
1182
1197
|
|
|
1183
|
-
// src/
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const line = lines[i];
|
|
1194
|
-
if (line.startsWith("```") || line.startsWith("~~~")) {
|
|
1195
|
-
inFence = !inFence;
|
|
1196
|
-
continue;
|
|
1197
|
-
}
|
|
1198
|
-
if (inFence) continue;
|
|
1199
|
-
if (line.startsWith("<<<<<<< ")) {
|
|
1200
|
-
openLine = i + 1;
|
|
1201
|
-
sawSeparator = false;
|
|
1202
|
-
continue;
|
|
1203
|
-
}
|
|
1204
|
-
if (line === "=======" && openLine > 0) {
|
|
1205
|
-
sawSeparator = true;
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
if (line.startsWith(">>>>>>> ")) {
|
|
1209
|
-
if (openLine > 0 && sawSeparator) {
|
|
1210
|
-
findings.push({ path: relPath, line: openLine });
|
|
1211
|
-
}
|
|
1212
|
-
openLine = 0;
|
|
1213
|
-
sawSeparator = false;
|
|
1214
|
-
}
|
|
1198
|
+
// src/doctor/probes/helpers.ts
|
|
1199
|
+
function check(status, id, label, detail) {
|
|
1200
|
+
return { id, label, status, detail };
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/doctor/probes/environment.ts
|
|
1204
|
+
function checkNodeVersion() {
|
|
1205
|
+
const major = parseInt(process.version.slice(1).split(".")[0], 10);
|
|
1206
|
+
if (major >= 20) {
|
|
1207
|
+
return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
|
|
1215
1208
|
}
|
|
1216
|
-
return
|
|
1209
|
+
return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
|
|
1217
1210
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
".
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
".playwright-cli"
|
|
1225
|
-
]);
|
|
1226
|
-
function walkMarkdownFiles(root, dir, rel, out) {
|
|
1227
|
-
let entries;
|
|
1211
|
+
function detectCliChannels(argv, home) {
|
|
1212
|
+
const channels = [];
|
|
1213
|
+
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
1214
|
+
const devPath = resolve2(argv[1]);
|
|
1215
|
+
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
1216
|
+
}
|
|
1228
1217
|
try {
|
|
1229
|
-
|
|
1218
|
+
const whichOut = execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
1219
|
+
if (whichOut) {
|
|
1220
|
+
const isDev = isDevSymlink(whichOut);
|
|
1221
|
+
if (!channels.some((c) => c.path === resolve2(whichOut))) {
|
|
1222
|
+
channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1230
1225
|
} catch {
|
|
1231
|
-
return;
|
|
1232
1226
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
out.push(rel ? `${rel}/${entry.name}` : entry.name);
|
|
1227
|
+
const plugin = findPlugin(home);
|
|
1228
|
+
if (plugin) {
|
|
1229
|
+
const pluginBin = join7(plugin.installPath, "bin", "skillwiki");
|
|
1230
|
+
if (existsSync4(pluginBin)) {
|
|
1231
|
+
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
1239
1232
|
}
|
|
1240
1233
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
const relPaths = [];
|
|
1245
|
-
walkMarkdownFiles(vaultRoot, vaultRoot, "", relPaths);
|
|
1246
|
-
const all = [];
|
|
1247
|
-
for (const rel of relPaths) {
|
|
1248
|
-
let text;
|
|
1249
|
-
try {
|
|
1250
|
-
text = readFileSync3(join7(vaultRoot, rel), "utf8");
|
|
1251
|
-
} catch {
|
|
1252
|
-
continue;
|
|
1253
|
-
}
|
|
1254
|
-
all.push(...scanConflictMarkerBlocksInText(rel, text));
|
|
1234
|
+
const installBin = join7(home, ".claude", "skills", "bin", "skillwiki");
|
|
1235
|
+
if (existsSync4(installBin)) {
|
|
1236
|
+
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
1255
1237
|
}
|
|
1256
|
-
return
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// src/utils/satellite-run-health.ts
|
|
1260
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1261
|
-
import { join as join8 } from "path";
|
|
1262
|
-
var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
|
|
1263
|
-
function satelliteLatestRunPath(vault) {
|
|
1264
|
-
return join8(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
|
|
1265
|
-
}
|
|
1266
|
-
function isFailedRunStatus(status) {
|
|
1267
|
-
return status === "fail" || status === "failure";
|
|
1238
|
+
return channels;
|
|
1268
1239
|
}
|
|
1269
|
-
function
|
|
1240
|
+
function isDevSymlink(binPath) {
|
|
1270
1241
|
try {
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
return { status, finishedAt, failureClass };
|
|
1242
|
+
const st = lstatSync(binPath);
|
|
1243
|
+
if (st.isSymbolicLink()) {
|
|
1244
|
+
const target = resolve2(binPath, "..", readlinkSync(binPath));
|
|
1245
|
+
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
1246
|
+
}
|
|
1277
1247
|
} catch {
|
|
1278
|
-
return null;
|
|
1279
1248
|
}
|
|
1249
|
+
return false;
|
|
1280
1250
|
}
|
|
1281
|
-
function
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const latestPath = satelliteLatestRunPath(vault);
|
|
1286
|
-
if (!existsSync5(latestPath)) return null;
|
|
1287
|
-
try {
|
|
1288
|
-
return parseLatestRunFile(readFileSync4(latestPath, "utf8"));
|
|
1289
|
-
} catch {
|
|
1290
|
-
return null;
|
|
1251
|
+
function checkCliChannels(argv, home) {
|
|
1252
|
+
const channels = detectCliChannels(argv, home);
|
|
1253
|
+
if (channels.length === 0) {
|
|
1254
|
+
return check("warn", "cli_channels", "CLI channels", "skillwiki not found on any channel");
|
|
1291
1255
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
return { failed: false, stale: false };
|
|
1256
|
+
if (channels.length === 1) {
|
|
1257
|
+
const ch = channels[0];
|
|
1258
|
+
const label = ch.isDevLink ? `${ch.name} (dev source)` : ch.name;
|
|
1259
|
+
return check("pass", "cli_channels", "CLI channels", `Single channel: ${label}`);
|
|
1297
1260
|
}
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
if (
|
|
1301
|
-
const
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
1261
|
+
const devChannels = channels.filter((c) => c.isDevLink);
|
|
1262
|
+
const prodChannels = channels.filter((c) => !c.isDevLink);
|
|
1263
|
+
if (devChannels.length > 0 && prodChannels.length > 0) {
|
|
1264
|
+
const hasInstall2 = prodChannels.some((c) => c.name === "install");
|
|
1265
|
+
if (!hasInstall2) {
|
|
1266
|
+
const devNames2 = devChannels.map((c) => `${c.name}(dev)`);
|
|
1267
|
+
const prodNames2 = prodChannels.map((c) => c.name);
|
|
1268
|
+
return check("pass", "cli_channels", "CLI channels", `${channels.length} channels: ${[...devNames2, ...prodNames2].join(", ")} \u2014 dev source with installed production channels`);
|
|
1304
1269
|
}
|
|
1270
|
+
const devNames = devChannels.map((c) => `${c.name}(dev)`);
|
|
1271
|
+
const prodNames = prodChannels.map((c) => c.name);
|
|
1272
|
+
return check(
|
|
1273
|
+
"warn",
|
|
1274
|
+
"cli_channels",
|
|
1275
|
+
"CLI channels",
|
|
1276
|
+
`${channels.length} channels: ${[...devNames, ...prodNames].join(", ")} \u2014 dev and prod binaries overlap; dev repo should use project-local settings only`
|
|
1277
|
+
);
|
|
1305
1278
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1279
|
+
const names = channels.map((c) => c.name);
|
|
1280
|
+
const hasInstall = channels.some((c) => c.name === "install");
|
|
1281
|
+
if (hasInstall) {
|
|
1282
|
+
return check(
|
|
1283
|
+
"warn",
|
|
1284
|
+
"cli_channels",
|
|
1285
|
+
"CLI channels",
|
|
1286
|
+
`${channels.length} channels: ${names.join(", ")} \u2014 remove unused install with: rm ~/.claude/skills/bin/skillwiki`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
return check("pass", "cli_channels", "CLI channels", `${channels.length} channels: ${names.join(", ")}`);
|
|
1312
1290
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
import { join as join9 } from "path";
|
|
1319
|
-
var OS = platform();
|
|
1320
|
-
function findRcloneMountPid() {
|
|
1291
|
+
async function checkConfigFile(home) {
|
|
1292
|
+
const cfgPath = configPath(home);
|
|
1293
|
+
if (!existsSync4(cfgPath)) {
|
|
1294
|
+
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1295
|
+
}
|
|
1321
1296
|
try {
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
const pids = out.split("\n").filter(Boolean);
|
|
1328
|
-
if (pids.length === 0) return null;
|
|
1329
|
-
return parseInt(pids[0], 10);
|
|
1330
|
-
} catch {
|
|
1331
|
-
try {
|
|
1332
|
-
const out = execSync("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
|
|
1333
|
-
for (const line of out.split("\n")) {
|
|
1334
|
-
if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
|
|
1335
|
-
const parts = line.trim().split(/\s+/);
|
|
1336
|
-
if (parts.length >= 2) return parseInt(parts[1], 10);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
} catch {
|
|
1340
|
-
}
|
|
1341
|
-
return null;
|
|
1297
|
+
const map = await parseDotenvFile(cfgPath);
|
|
1298
|
+
const keys = Object.keys(map);
|
|
1299
|
+
return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
|
|
1300
|
+
} catch (e) {
|
|
1301
|
+
return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
|
|
1342
1302
|
}
|
|
1343
1303
|
}
|
|
1344
|
-
function
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
const eq = arg.indexOf("=");
|
|
1352
|
-
flags.set(arg.slice(0, eq), arg.slice(eq + 1));
|
|
1353
|
-
} else if (arg.startsWith("--")) {
|
|
1354
|
-
const next = args[i + 1];
|
|
1355
|
-
if (next && !next.startsWith("-")) {
|
|
1356
|
-
flags.set(arg, next);
|
|
1357
|
-
i++;
|
|
1358
|
-
} else {
|
|
1359
|
-
flags.set(arg, "");
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1304
|
+
async function checkProfiles(home) {
|
|
1305
|
+
const map = await parseDotenvFile(configPath(home));
|
|
1306
|
+
const profiles = [];
|
|
1307
|
+
for (const key of Object.keys(map)) {
|
|
1308
|
+
if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
|
|
1309
|
+
const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
|
|
1310
|
+
profiles.push(name);
|
|
1362
1311
|
}
|
|
1363
|
-
} catch {
|
|
1364
1312
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
function getRcloneVersion() {
|
|
1368
|
-
try {
|
|
1369
|
-
const out = execSync("rclone version", {
|
|
1370
|
-
encoding: "utf8",
|
|
1371
|
-
timeout: 3e3,
|
|
1372
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1373
|
-
});
|
|
1374
|
-
const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
|
|
1375
|
-
if (!match) return null;
|
|
1376
|
-
return {
|
|
1377
|
-
major: parseInt(match[1], 10),
|
|
1378
|
-
minor: parseInt(match[2], 10),
|
|
1379
|
-
patch: parseInt(match[3], 10),
|
|
1380
|
-
raw: out.split("\n")[0].trim()
|
|
1381
|
-
};
|
|
1382
|
-
} catch {
|
|
1383
|
-
return null;
|
|
1313
|
+
if (profiles.length === 0) {
|
|
1314
|
+
return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
|
|
1384
1315
|
}
|
|
1316
|
+
const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
|
|
1317
|
+
return check(
|
|
1318
|
+
"pass",
|
|
1319
|
+
"wiki_profiles",
|
|
1320
|
+
"Wiki profiles",
|
|
1321
|
+
`${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
|
|
1322
|
+
);
|
|
1385
1323
|
}
|
|
1386
|
-
function
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
continue;
|
|
1392
|
-
}
|
|
1393
|
-
if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
|
|
1394
|
-
return arg;
|
|
1395
|
-
}
|
|
1324
|
+
async function checkProjectLocalOverride(cwd) {
|
|
1325
|
+
const dir = cwd ?? process.cwd();
|
|
1326
|
+
const envPath = join7(dir, ".skillwiki", ".env");
|
|
1327
|
+
if (existsSync4(envPath)) {
|
|
1328
|
+
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
1396
1329
|
}
|
|
1397
|
-
return
|
|
1330
|
+
return check("pass", "project_local", "Project-local config", "None");
|
|
1398
1331
|
}
|
|
1399
|
-
function
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
const raw = readFileSync5(`/proc/${pid}/cmdline`);
|
|
1403
|
-
return new TextDecoder().decode(raw).split("\0").filter(Boolean);
|
|
1404
|
-
} else {
|
|
1405
|
-
const out = execSync(`ps -o args= -p ${pid}`, {
|
|
1406
|
-
encoding: "utf8",
|
|
1407
|
-
timeout: 2e3,
|
|
1408
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1409
|
-
}).trim();
|
|
1410
|
-
return out.split(/\s+/);
|
|
1411
|
-
}
|
|
1412
|
-
} catch {
|
|
1413
|
-
return [];
|
|
1332
|
+
function checkWikiPathSet(ctx) {
|
|
1333
|
+
if (ctx.resolvedPath) {
|
|
1334
|
+
return check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${ctx.wikiPathSource ?? "unknown"}: ${ctx.resolvedPath}`);
|
|
1414
1335
|
}
|
|
1336
|
+
return check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault.");
|
|
1415
1337
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
}
|
|
1428
|
-
const dc = data.diskCache || {};
|
|
1429
|
-
return {
|
|
1430
|
-
erroredFiles: dc.erroredFiles ?? 0,
|
|
1431
|
-
uploadsInProgress: dc.uploadsInProgress ?? 0,
|
|
1432
|
-
uploadsQueued: dc.uploadsQueued ?? 0,
|
|
1433
|
-
outOfSpace: dc.outOfSpace ?? false,
|
|
1434
|
-
bytesUsed: dc.bytesUsed ?? 0,
|
|
1435
|
-
files: dc.files ?? 0,
|
|
1436
|
-
totalSize: data.totalSize || "unknown"
|
|
1437
|
-
};
|
|
1438
|
-
} catch {
|
|
1439
|
-
return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
|
|
1338
|
+
var environmentProbe = {
|
|
1339
|
+
id: "environment",
|
|
1340
|
+
async run(ctx) {
|
|
1341
|
+
return [
|
|
1342
|
+
checkNodeVersion(),
|
|
1343
|
+
checkCliChannels(ctx.input.argv, ctx.input.home),
|
|
1344
|
+
await checkConfigFile(ctx.input.home),
|
|
1345
|
+
await checkProfiles(ctx.input.home),
|
|
1346
|
+
await checkProjectLocalOverride(ctx.input.cwd),
|
|
1347
|
+
checkWikiPathSet(ctx)
|
|
1348
|
+
];
|
|
1440
1349
|
}
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
if (parts.length < 3) continue;
|
|
1450
|
-
const point = parts[1];
|
|
1451
|
-
const fs = parts[2];
|
|
1452
|
-
if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
|
|
1453
|
-
best = { point, fs };
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
if (best && best.fs.includes("fuse")) return { mountPoint: best.point, fsType: best.fs };
|
|
1457
|
-
} else if (OS === "darwin") {
|
|
1458
|
-
const out = execSync("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1459
|
-
let best = null;
|
|
1460
|
-
for (const line of out.split("\n")) {
|
|
1461
|
-
const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
|
|
1462
|
-
if (!match) continue;
|
|
1463
|
-
const point = match[2];
|
|
1464
|
-
const opts = match[3];
|
|
1465
|
-
if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
|
|
1466
|
-
best = { point, fsType: `fuse.${match[1].split(":")[0] || "unknown"}` };
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
if (best) return { mountPoint: best.point, fsType: best.fsType };
|
|
1470
|
-
}
|
|
1471
|
-
} catch {
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// src/doctor/probes/vault-structure.ts
|
|
1353
|
+
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
1354
|
+
import { join as join8 } from "path";
|
|
1355
|
+
function checkWikiPathExists(resolvedPath) {
|
|
1356
|
+
if (resolvedPath === void 0) {
|
|
1357
|
+
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1472
1358
|
}
|
|
1473
|
-
|
|
1359
|
+
if (existsSync5(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
|
|
1360
|
+
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1361
|
+
}
|
|
1362
|
+
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
1474
1363
|
}
|
|
1475
|
-
function
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
const start = Date.now();
|
|
1479
|
-
try {
|
|
1480
|
-
writeFileSync2(testFile, payload, "utf8");
|
|
1481
|
-
} catch (e) {
|
|
1482
|
-
return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
|
|
1364
|
+
function checkVaultStructure(resolvedPath) {
|
|
1365
|
+
if (resolvedPath === void 0) {
|
|
1366
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1483
1367
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
try {
|
|
1487
|
-
const back = readFile6(testFile, "utf8");
|
|
1488
|
-
const readMs = Date.now() - readStart;
|
|
1489
|
-
if (back !== payload) {
|
|
1490
|
-
try {
|
|
1491
|
-
unlinkSync2(testFile);
|
|
1492
|
-
} catch {
|
|
1493
|
-
}
|
|
1494
|
-
return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
|
|
1495
|
-
}
|
|
1496
|
-
} catch (e) {
|
|
1497
|
-
try {
|
|
1498
|
-
unlinkSync2(testFile);
|
|
1499
|
-
} catch {
|
|
1500
|
-
}
|
|
1501
|
-
return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
|
|
1368
|
+
if (!existsSync5(resolvedPath)) {
|
|
1369
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1502
1370
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1371
|
+
const missing = [];
|
|
1372
|
+
if (!existsSync5(join8(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1373
|
+
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1374
|
+
if (!existsSync5(join8(resolvedPath, dir))) missing.push(dir + "/");
|
|
1506
1375
|
}
|
|
1507
|
-
|
|
1376
|
+
if (missing.length === 0) {
|
|
1377
|
+
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1378
|
+
}
|
|
1379
|
+
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
1508
1380
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
m: 60,
|
|
1513
|
-
h: 3600,
|
|
1514
|
-
d: 86400,
|
|
1515
|
-
w: 604800
|
|
1516
|
-
};
|
|
1517
|
-
function parseDurationSeconds(raw) {
|
|
1518
|
-
const input = raw.trim().toLowerCase();
|
|
1519
|
-
if (!input) return null;
|
|
1520
|
-
if (/^\d+(?:\.\d+)?$/.test(input)) {
|
|
1521
|
-
const num = parseFloat(input);
|
|
1522
|
-
return Number.isFinite(num) ? num : null;
|
|
1381
|
+
function checkObsidianTemplates(resolvedPath) {
|
|
1382
|
+
if (resolvedPath === void 0) {
|
|
1383
|
+
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1523
1384
|
}
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
const unit = match[2];
|
|
1531
|
-
if (!Number.isFinite(value)) return null;
|
|
1532
|
-
const factor = DURATION_UNIT_SECONDS[unit];
|
|
1533
|
-
if (factor === void 0) return null;
|
|
1534
|
-
total += value * factor;
|
|
1535
|
-
consumed += full.length;
|
|
1385
|
+
const missing = [];
|
|
1386
|
+
if (!existsSync5(join8(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
1387
|
+
if (!existsSync5(join8(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
1388
|
+
if (!existsSync5(join8(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
1389
|
+
if (missing.length === 0) {
|
|
1390
|
+
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
1536
1391
|
}
|
|
1537
|
-
|
|
1538
|
-
return total;
|
|
1392
|
+
return check("warn", "obsidian_templates", "Obsidian templates", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to create`);
|
|
1539
1393
|
}
|
|
1540
|
-
var
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1394
|
+
var vaultStructureProbe = {
|
|
1395
|
+
id: "vault_structure",
|
|
1396
|
+
run(ctx) {
|
|
1397
|
+
return [
|
|
1398
|
+
checkWikiPathExists(ctx.resolvedPath),
|
|
1399
|
+
checkVaultStructure(ctx.resolvedPath),
|
|
1400
|
+
checkObsidianTemplates(ctx.resolvedPath)
|
|
1401
|
+
];
|
|
1402
|
+
}
|
|
1544
1403
|
};
|
|
1545
|
-
var MIN_RCLONE_VERSION = { major: 1, minor: 65, patch: 0 };
|
|
1546
1404
|
|
|
1547
|
-
// src/
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
if (
|
|
1554
|
-
return check("
|
|
1405
|
+
// src/doctor/probes/git-fleet.ts
|
|
1406
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
1407
|
+
import { join as join9 } from "path";
|
|
1408
|
+
import { execSync as execSync2 } from "child_process";
|
|
1409
|
+
import { platform } from "os";
|
|
1410
|
+
function checkVaultGitRemote(resolvedPath) {
|
|
1411
|
+
if (resolvedPath === void 0) {
|
|
1412
|
+
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1555
1413
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
function detectCliChannels(argv, home) {
|
|
1559
|
-
const channels = [];
|
|
1560
|
-
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
1561
|
-
const devPath = resolve2(argv[1]);
|
|
1562
|
-
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
1414
|
+
if (!existsSync6(join9(resolvedPath, ".git"))) {
|
|
1415
|
+
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
1563
1416
|
}
|
|
1564
1417
|
try {
|
|
1565
|
-
const
|
|
1566
|
-
if (
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1418
|
+
const remote = execSync2("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1419
|
+
if (!remote) {
|
|
1420
|
+
return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
|
|
1421
|
+
}
|
|
1422
|
+
let branch = "(no commits yet)";
|
|
1423
|
+
try {
|
|
1424
|
+
branch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1425
|
+
} catch {
|
|
1571
1426
|
}
|
|
1427
|
+
return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
|
|
1572
1428
|
} catch {
|
|
1429
|
+
return check("warn", "vault_git_remote", "Vault git remote", "Could not read git remote info");
|
|
1573
1430
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
1579
|
-
}
|
|
1431
|
+
}
|
|
1432
|
+
async function checkFleetIdentity(input) {
|
|
1433
|
+
if (!input.vaultPath) {
|
|
1434
|
+
return check("pass", "fleet_identity", "Fleet identity", "No vault path \u2014 check skipped");
|
|
1580
1435
|
}
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
1436
|
+
const load = input.fleetLoad !== void 0 ? input.fleetLoad : await loadFleetManifestAndHost({
|
|
1437
|
+
vault: input.vaultPath,
|
|
1438
|
+
env: { ...process.env, WIKI_PATH: input.envValue ?? input.vaultPath },
|
|
1439
|
+
home: input.home,
|
|
1440
|
+
cwd: input.cwd ?? process.cwd(),
|
|
1441
|
+
osHostname: process.env.HOSTNAME,
|
|
1442
|
+
user: process.env.USER
|
|
1443
|
+
});
|
|
1444
|
+
if (!load) {
|
|
1445
|
+
return check("pass", "fleet_identity", "Fleet identity", "Fleet manifest unavailable \u2014 check skipped");
|
|
1584
1446
|
}
|
|
1585
|
-
|
|
1586
|
-
}
|
|
1587
|
-
function isDevSymlink(binPath) {
|
|
1588
|
-
try {
|
|
1589
|
-
const st = lstatSync(binPath);
|
|
1590
|
-
if (st.isSymbolicLink()) {
|
|
1591
|
-
const target = resolve2(binPath, "..", readlinkSync(binPath));
|
|
1592
|
-
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
1593
|
-
}
|
|
1594
|
-
} catch {
|
|
1447
|
+
if (load.identityStatus === "known") {
|
|
1448
|
+
return check("pass", "fleet_identity", "Fleet identity", `Resolved ${load.hostId ?? "unknown"} via ${load.source ?? "unknown"}`);
|
|
1595
1449
|
}
|
|
1596
|
-
|
|
1450
|
+
const detail = load.warnings.length > 0 ? load.warnings.join("; ") : "Fleet identity is unresolved";
|
|
1451
|
+
return check("warn", "fleet_identity", "Fleet identity", detail);
|
|
1597
1452
|
}
|
|
1598
|
-
function
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
return check("warn", "cli_channels", "CLI channels", "skillwiki not found on any channel");
|
|
1453
|
+
function checkSyncLastPush(resolvedPath) {
|
|
1454
|
+
if (resolvedPath === void 0) {
|
|
1455
|
+
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1602
1456
|
}
|
|
1603
|
-
if (
|
|
1604
|
-
|
|
1605
|
-
const label = ch.isDevLink ? `${ch.name} (dev source)` : ch.name;
|
|
1606
|
-
return check("pass", "cli_channels", "CLI channels", `Single channel: ${label}`);
|
|
1457
|
+
if (!existsSync6(join9(resolvedPath, ".git"))) {
|
|
1458
|
+
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
1607
1459
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1460
|
+
let timestamp;
|
|
1461
|
+
try {
|
|
1462
|
+
const out = execSync2("git log -1 --format=%ct origin/HEAD", {
|
|
1463
|
+
cwd: resolvedPath,
|
|
1464
|
+
encoding: "utf8",
|
|
1465
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1466
|
+
}).trim();
|
|
1467
|
+
timestamp = parseInt(out, 10);
|
|
1468
|
+
} catch {
|
|
1469
|
+
try {
|
|
1470
|
+
const out = execSync2("git log -1 --format=%ct HEAD", {
|
|
1471
|
+
cwd: resolvedPath,
|
|
1472
|
+
encoding: "utf8",
|
|
1473
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1474
|
+
}).trim();
|
|
1475
|
+
timestamp = parseInt(out, 10);
|
|
1476
|
+
} catch {
|
|
1616
1477
|
}
|
|
1617
|
-
const devNames = devChannels.map((c) => `${c.name}(dev)`);
|
|
1618
|
-
const prodNames = prodChannels.map((c) => c.name);
|
|
1619
|
-
return check(
|
|
1620
|
-
"warn",
|
|
1621
|
-
"cli_channels",
|
|
1622
|
-
"CLI channels",
|
|
1623
|
-
`${channels.length} channels: ${[...devNames, ...prodNames].join(", ")} \u2014 dev and prod binaries overlap; dev repo should use project-local settings only`
|
|
1624
|
-
);
|
|
1625
|
-
}
|
|
1626
|
-
const names = channels.map((c) => c.name);
|
|
1627
|
-
const hasInstall = channels.some((c) => c.name === "install");
|
|
1628
|
-
if (hasInstall) {
|
|
1629
|
-
return check(
|
|
1630
|
-
"warn",
|
|
1631
|
-
"cli_channels",
|
|
1632
|
-
"CLI channels",
|
|
1633
|
-
`${channels.length} channels: ${names.join(", ")} \u2014 remove unused install with: rm ~/.claude/skills/bin/skillwiki`
|
|
1634
|
-
);
|
|
1635
1478
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
function isDevSourceRun(argv) {
|
|
1639
|
-
return argv.length >= 2 && argv[1].endsWith("cli.js");
|
|
1640
|
-
}
|
|
1641
|
-
async function checkConfigFile(home) {
|
|
1642
|
-
const cfgPath = configPath(home);
|
|
1643
|
-
if (!existsSync7(cfgPath)) {
|
|
1644
|
-
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1479
|
+
if (timestamp === void 0 || isNaN(timestamp)) {
|
|
1480
|
+
return check("warn", "sync_last_push", "Vault sync recency", "No commits found \u2014 consider running `skillwiki sync status`");
|
|
1645
1481
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
return check("
|
|
1650
|
-
} catch (e) {
|
|
1651
|
-
return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
|
|
1482
|
+
const daysSince = Math.floor((Date.now() / 1e3 - timestamp) / 86400);
|
|
1483
|
+
const dateStr = new Date(timestamp * 1e3).toISOString().slice(0, 10);
|
|
1484
|
+
if (daysSince > 7) {
|
|
1485
|
+
return check("warn", "sync_last_push", "Vault sync recency", `Last push was ${daysSince} days ago \u2014 consider running \`skillwiki sync status\``);
|
|
1652
1486
|
}
|
|
1487
|
+
return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince} day(s) ago)`);
|
|
1653
1488
|
}
|
|
1654
|
-
function
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1489
|
+
function hasOriginMain(resolvedPath) {
|
|
1490
|
+
try {
|
|
1491
|
+
execSync2("git rev-parse --verify --quiet origin/main", {
|
|
1492
|
+
cwd: resolvedPath,
|
|
1493
|
+
encoding: "utf8",
|
|
1494
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1495
|
+
timeout: 2e3
|
|
1496
|
+
});
|
|
1497
|
+
return true;
|
|
1498
|
+
} catch {
|
|
1499
|
+
return false;
|
|
1660
1500
|
}
|
|
1661
|
-
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
1662
1501
|
}
|
|
1663
|
-
function
|
|
1502
|
+
function checkVaultGitDirty(resolvedPath) {
|
|
1664
1503
|
if (resolvedPath === void 0) {
|
|
1665
|
-
return check("
|
|
1666
|
-
}
|
|
1667
|
-
if (!existsSync7(resolvedPath)) {
|
|
1668
|
-
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1669
|
-
}
|
|
1670
|
-
const missing = [];
|
|
1671
|
-
if (!existsSync7(join10(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1672
|
-
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1673
|
-
if (!existsSync7(join10(resolvedPath, dir))) missing.push(dir + "/");
|
|
1674
|
-
}
|
|
1675
|
-
if (missing.length === 0) {
|
|
1676
|
-
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1677
|
-
}
|
|
1678
|
-
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
1679
|
-
}
|
|
1680
|
-
function checkSkillsInstalled(home, cwd) {
|
|
1681
|
-
const srcDir = cwd ? join10(cwd, "packages", "skills") : void 0;
|
|
1682
|
-
if (srcDir && existsSync7(srcDir)) {
|
|
1683
|
-
const found = findInstalledSkillMd(srcDir);
|
|
1684
|
-
if (found.length > 0) {
|
|
1685
|
-
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
1686
|
-
}
|
|
1504
|
+
return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
|
|
1687
1505
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
const found = findInstalledSkillMd(plugin.installPath);
|
|
1691
|
-
if (found.length > 0) {
|
|
1692
|
-
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
1693
|
-
}
|
|
1506
|
+
if (!existsSync6(join9(resolvedPath, ".git"))) {
|
|
1507
|
+
return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
|
|
1694
1508
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1509
|
+
try {
|
|
1510
|
+
const lines = execSync2("git status --porcelain", {
|
|
1511
|
+
cwd: resolvedPath,
|
|
1512
|
+
encoding: "utf8",
|
|
1513
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1514
|
+
timeout: 5e3
|
|
1515
|
+
}).trim().split("\n").filter(Boolean);
|
|
1516
|
+
if (lines.length > 0) {
|
|
1517
|
+
return check("warn", "vault_git_dirty", "Vault git dirty state", `${lines.length} dirty file(s) in vault worktree`);
|
|
1700
1518
|
}
|
|
1519
|
+
return check("pass", "vault_git_dirty", "Vault git dirty state", "Clean worktree");
|
|
1520
|
+
} catch {
|
|
1521
|
+
return check("warn", "vault_git_dirty", "Vault git dirty state", "Could not read git status");
|
|
1701
1522
|
}
|
|
1702
|
-
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found");
|
|
1703
1523
|
}
|
|
1704
|
-
function
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
return
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
const cliSkills = findSkillNames(skillsDir);
|
|
1716
|
-
const cliDuplicates = cliSkills.filter((name) => pluginSkills.includes(name));
|
|
1717
|
-
const agentDuplicates = [];
|
|
1718
|
-
for (const { label, path } of agentSkillDirs) {
|
|
1719
|
-
const overlap = findSkillNames(path).filter((name) => pluginSkills.includes(name));
|
|
1720
|
-
if (overlap.length > 0) {
|
|
1721
|
-
agentDuplicates.push({ dir: label, names: overlap });
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
if (cliDuplicates.length === 0 && agentDuplicates.length === 0) {
|
|
1725
|
-
return check("pass", "skills_duplicate", "Skills not duplicated", "No overlap between plugin and other channels");
|
|
1726
|
-
}
|
|
1727
|
-
const parts = [];
|
|
1728
|
-
if (cliDuplicates.length > 0) {
|
|
1729
|
-
parts.push(`${cliDuplicates.length} skill(s) in both plugin and ~/.claude/skills/ \u2014 remove CLI copies: rm -r ~/.claude/skills/{${cliDuplicates.slice(0, 3).join(",")}${cliDuplicates.length > 3 ? ",\u2026" : ""}}`);
|
|
1730
|
-
}
|
|
1731
|
-
for (const { dir, names } of agentDuplicates) {
|
|
1732
|
-
parts.push(`${names.length} stale skill(s) in ${dir} \u2014 plugin provides: ${names.slice(0, 3).join(", ")}${names.length > 3 ? ", \u2026" : ""}`);
|
|
1524
|
+
function gitRefHash(resolvedPath, ref) {
|
|
1525
|
+
try {
|
|
1526
|
+
const out = execSync2(`git rev-parse --verify ${ref}`, {
|
|
1527
|
+
cwd: resolvedPath,
|
|
1528
|
+
encoding: "utf8",
|
|
1529
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1530
|
+
timeout: 2e3
|
|
1531
|
+
}).trim();
|
|
1532
|
+
return out || void 0;
|
|
1533
|
+
} catch {
|
|
1534
|
+
return void 0;
|
|
1733
1535
|
}
|
|
1734
|
-
const status = cliDuplicates.length > 0 ? "warn" : "info";
|
|
1735
|
-
return check(status, "skills_duplicate", "Skills not duplicated", parts.join("; "));
|
|
1736
1536
|
}
|
|
1737
|
-
function
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1537
|
+
function remoteMainHash(resolvedPath) {
|
|
1538
|
+
try {
|
|
1539
|
+
const out = execSync2("git ls-remote origin refs/heads/main", {
|
|
1540
|
+
cwd: resolvedPath,
|
|
1541
|
+
encoding: "utf8",
|
|
1542
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1543
|
+
timeout: 3e3
|
|
1544
|
+
}).trim();
|
|
1545
|
+
const hash = out.split(/\s+/)[0];
|
|
1546
|
+
return /^[0-9a-f]{40}$/i.test(hash) ? hash : void 0;
|
|
1547
|
+
} catch {
|
|
1548
|
+
return void 0;
|
|
1744
1549
|
}
|
|
1745
|
-
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: v${latest})`);
|
|
1746
1550
|
}
|
|
1747
|
-
function
|
|
1748
|
-
|
|
1749
|
-
if (
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
const
|
|
1753
|
-
if (
|
|
1754
|
-
if (plugins.length === 1 && plugins[0].channel === "claude") {
|
|
1755
|
-
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
1756
|
-
}
|
|
1757
|
-
if (plugins.length === 1) {
|
|
1758
|
-
return check("pass", "plugin_version_drift", "Plugin/CLI version", `${plugins[0].label} plugin and CLI both at v${currentVersion}`);
|
|
1759
|
-
}
|
|
1760
|
-
const labels = plugins.map((plugin) => `${plugin.label} plugin`).join(", ");
|
|
1761
|
-
return check("pass", "plugin_version_drift", "Plugin/CLI version", `${labels}, and CLI all at v${currentVersion}`);
|
|
1762
|
-
}
|
|
1763
|
-
if (devSourceRun && drifted.every((plugin) => semverGt(currentVersion, plugin.version))) {
|
|
1764
|
-
const details2 = drifted.map((plugin) => `${plugin.label} plugin v${plugin.version}`).join(", ");
|
|
1765
|
-
return check("info", "plugin_version_drift", "Plugin/CLI version", `Dev source v${currentVersion} is ahead of installed ${details2}`);
|
|
1766
|
-
}
|
|
1767
|
-
const details = drifted.map((plugin) => {
|
|
1768
|
-
const updateCmd = pluginUpdateCommand(plugin, currentVersion);
|
|
1769
|
-
return `${plugin.label} plugin v${plugin.version} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``;
|
|
1770
|
-
});
|
|
1551
|
+
function checkStaleRemoteMain(resolvedPath) {
|
|
1552
|
+
if (resolvedPath === void 0) return void 0;
|
|
1553
|
+
if (!existsSync6(join9(resolvedPath, ".git"))) return void 0;
|
|
1554
|
+
const localOrigin = gitRefHash(resolvedPath, "origin/main");
|
|
1555
|
+
if (!localOrigin) return void 0;
|
|
1556
|
+
const remoteMain = remoteMainHash(resolvedPath);
|
|
1557
|
+
if (!remoteMain || remoteMain === localOrigin) return void 0;
|
|
1771
1558
|
return check(
|
|
1772
1559
|
"warn",
|
|
1773
|
-
"
|
|
1774
|
-
"
|
|
1775
|
-
|
|
1560
|
+
"vault_git_behind",
|
|
1561
|
+
"Vault commits behind",
|
|
1562
|
+
`Remote main differs from local origin/main (${remoteMain.slice(0, 8)} != ${localOrigin.slice(0, 8)}) \u2014 run git fetch before trusting behind count`
|
|
1776
1563
|
);
|
|
1777
1564
|
}
|
|
1778
|
-
function
|
|
1779
|
-
if (
|
|
1780
|
-
return "
|
|
1565
|
+
function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix, zeroDetail) {
|
|
1566
|
+
if (resolvedPath === void 0) {
|
|
1567
|
+
return check("pass", id, label, "No vault path \u2014 check skipped");
|
|
1781
1568
|
}
|
|
1782
|
-
if (
|
|
1783
|
-
return "
|
|
1569
|
+
if (!existsSync6(join9(resolvedPath, ".git"))) {
|
|
1570
|
+
return check("pass", id, label, "No git repo \u2014 check skipped");
|
|
1784
1571
|
}
|
|
1785
|
-
if (
|
|
1786
|
-
return "
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
return check("
|
|
1801
|
-
}
|
|
1802
|
-
const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
|
|
1803
|
-
return check(
|
|
1804
|
-
"pass",
|
|
1805
|
-
"wiki_profiles",
|
|
1806
|
-
"Wiki profiles",
|
|
1807
|
-
`${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
|
|
1808
|
-
);
|
|
1809
|
-
}
|
|
1810
|
-
async function checkProjectLocalOverride(cwd) {
|
|
1811
|
-
const dir = cwd ?? process.cwd();
|
|
1812
|
-
const envPath = join10(dir, ".skillwiki", ".env");
|
|
1813
|
-
if (existsSync7(envPath)) {
|
|
1814
|
-
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
1815
|
-
}
|
|
1816
|
-
return check("pass", "project_local", "Project-local config", "None");
|
|
1817
|
-
}
|
|
1818
|
-
function checkVaultGitRemote(resolvedPath) {
|
|
1819
|
-
if (resolvedPath === void 0) {
|
|
1820
|
-
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1821
|
-
}
|
|
1822
|
-
if (!existsSync7(join10(resolvedPath, ".git"))) {
|
|
1823
|
-
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
1824
|
-
}
|
|
1825
|
-
try {
|
|
1826
|
-
const remote = execSync2("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1827
|
-
if (!remote) {
|
|
1828
|
-
return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
|
|
1829
|
-
}
|
|
1830
|
-
let branch = "(no commits yet)";
|
|
1831
|
-
try {
|
|
1832
|
-
branch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1833
|
-
} catch {
|
|
1834
|
-
}
|
|
1835
|
-
return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
|
|
1836
|
-
} catch {
|
|
1837
|
-
return check("warn", "vault_git_remote", "Vault git remote", "Could not read git remote info");
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
function checkObsidianTemplates(resolvedPath) {
|
|
1841
|
-
if (resolvedPath === void 0) {
|
|
1842
|
-
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1843
|
-
}
|
|
1844
|
-
const missing = [];
|
|
1845
|
-
if (!existsSync7(join10(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
1846
|
-
if (!existsSync7(join10(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
1847
|
-
if (!existsSync7(join10(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
1848
|
-
if (missing.length === 0) {
|
|
1849
|
-
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
1850
|
-
}
|
|
1851
|
-
return check("warn", "obsidian_templates", "Obsidian templates", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to create`);
|
|
1852
|
-
}
|
|
1853
|
-
function checkDotStoreClean(resolvedPath) {
|
|
1854
|
-
if (resolvedPath === void 0) {
|
|
1855
|
-
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1856
|
-
}
|
|
1857
|
-
const rawDir = join10(resolvedPath, "raw");
|
|
1858
|
-
if (!existsSync7(rawDir)) {
|
|
1859
|
-
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
1860
|
-
}
|
|
1861
|
-
const found = [];
|
|
1862
|
-
(function walk(dir, rel) {
|
|
1863
|
-
let entries;
|
|
1864
|
-
try {
|
|
1865
|
-
entries = readdirSync3(dir, { withFileTypes: true });
|
|
1866
|
-
} catch {
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
for (const entry of entries) {
|
|
1870
|
-
if (entry.name === ".DS_Store") {
|
|
1871
|
-
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
1872
|
-
} else if (entry.isDirectory()) {
|
|
1873
|
-
walk(join10(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
})(rawDir, "");
|
|
1877
|
-
if (found.length === 0) {
|
|
1878
|
-
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "No .DS_Store files found");
|
|
1879
|
-
}
|
|
1880
|
-
return check("info", "dsstore_clean", "No .DS_Store in raw/", `${found.length} .DS_Store file(s) found \u2014 remove with: find ${rawDir} -name .DS_Store -delete`);
|
|
1881
|
-
}
|
|
1882
|
-
function checkVaultConflictMarkers(resolvedPath) {
|
|
1883
|
-
if (resolvedPath === void 0) {
|
|
1884
|
-
return check("pass", "vault_conflict_markers", "Vault conflict markers", "No vault path \u2014 check skipped");
|
|
1885
|
-
}
|
|
1886
|
-
const findings = scanVaultConflictMarkers(resolvedPath);
|
|
1887
|
-
if (findings.length === 0) {
|
|
1888
|
-
return check("pass", "vault_conflict_markers", "Vault conflict markers", "No complete conflict-marker blocks");
|
|
1889
|
-
}
|
|
1890
|
-
const first = findings[0];
|
|
1891
|
-
const n = findings.length;
|
|
1892
|
-
const fileWord = n === 1 ? "file" : "files";
|
|
1893
|
-
return check(
|
|
1894
|
-
"error",
|
|
1895
|
-
"vault_conflict_markers",
|
|
1896
|
-
"Vault conflict markers",
|
|
1897
|
-
`${n} ${fileWord}, first: ${first.path}:${first.line}`
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
1900
|
-
function checkSyncLastPush(resolvedPath) {
|
|
1901
|
-
if (resolvedPath === void 0) {
|
|
1902
|
-
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1903
|
-
}
|
|
1904
|
-
if (!existsSync7(join10(resolvedPath, ".git"))) {
|
|
1905
|
-
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
1906
|
-
}
|
|
1907
|
-
let timestamp;
|
|
1908
|
-
try {
|
|
1909
|
-
const out = execSync2("git log -1 --format=%ct origin/HEAD", {
|
|
1910
|
-
cwd: resolvedPath,
|
|
1911
|
-
encoding: "utf8",
|
|
1912
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1913
|
-
}).trim();
|
|
1914
|
-
timestamp = parseInt(out, 10);
|
|
1915
|
-
} catch {
|
|
1916
|
-
try {
|
|
1917
|
-
const out = execSync2("git log -1 --format=%ct HEAD", {
|
|
1918
|
-
cwd: resolvedPath,
|
|
1919
|
-
encoding: "utf8",
|
|
1920
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1921
|
-
}).trim();
|
|
1922
|
-
timestamp = parseInt(out, 10);
|
|
1923
|
-
} catch {
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
if (timestamp === void 0 || isNaN(timestamp)) {
|
|
1927
|
-
return check("warn", "sync_last_push", "Vault sync recency", "No commits found \u2014 consider running `skillwiki sync status`");
|
|
1928
|
-
}
|
|
1929
|
-
const daysSince = Math.floor((Date.now() / 1e3 - timestamp) / 86400);
|
|
1930
|
-
const dateStr = new Date(timestamp * 1e3).toISOString().slice(0, 10);
|
|
1931
|
-
if (daysSince > 7) {
|
|
1932
|
-
return check("warn", "sync_last_push", "Vault sync recency", `Last push was ${daysSince} days ago \u2014 consider running \`skillwiki sync status\``);
|
|
1933
|
-
}
|
|
1934
|
-
return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince} day(s) ago)`);
|
|
1935
|
-
}
|
|
1936
|
-
function hasOriginMain(resolvedPath) {
|
|
1937
|
-
try {
|
|
1938
|
-
execSync2("git rev-parse --verify --quiet origin/main", {
|
|
1939
|
-
cwd: resolvedPath,
|
|
1940
|
-
encoding: "utf8",
|
|
1941
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1942
|
-
timeout: 2e3
|
|
1943
|
-
});
|
|
1944
|
-
return true;
|
|
1945
|
-
} catch {
|
|
1946
|
-
return false;
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
function checkVaultGitDirty(resolvedPath) {
|
|
1950
|
-
if (resolvedPath === void 0) {
|
|
1951
|
-
return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
|
|
1952
|
-
}
|
|
1953
|
-
if (!existsSync7(join10(resolvedPath, ".git"))) {
|
|
1954
|
-
return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
|
|
1955
|
-
}
|
|
1956
|
-
try {
|
|
1957
|
-
const lines = execSync2("git status --porcelain", {
|
|
1958
|
-
cwd: resolvedPath,
|
|
1959
|
-
encoding: "utf8",
|
|
1960
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1961
|
-
timeout: 5e3
|
|
1962
|
-
}).trim().split("\n").filter(Boolean);
|
|
1963
|
-
if (lines.length > 0) {
|
|
1964
|
-
return check("warn", "vault_git_dirty", "Vault git dirty state", `${lines.length} dirty file(s) in vault worktree`);
|
|
1965
|
-
}
|
|
1966
|
-
return check("pass", "vault_git_dirty", "Vault git dirty state", "Clean worktree");
|
|
1967
|
-
} catch {
|
|
1968
|
-
return check("warn", "vault_git_dirty", "Vault git dirty state", "Could not read git status");
|
|
1572
|
+
if (!hasOriginMain(resolvedPath)) {
|
|
1573
|
+
return check("pass", id, label, "origin/main unavailable \u2014 check skipped");
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
const count = parseInt(execSync2(`git rev-list --count ${range}`, {
|
|
1577
|
+
cwd: resolvedPath,
|
|
1578
|
+
encoding: "utf8",
|
|
1579
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1580
|
+
timeout: 5e3
|
|
1581
|
+
}).trim(), 10);
|
|
1582
|
+
if (count > 0) {
|
|
1583
|
+
return check("warn", id, label, `${count} commit(s) ${nonZeroSuffix}`);
|
|
1584
|
+
}
|
|
1585
|
+
return check("pass", id, label, zeroDetail);
|
|
1586
|
+
} catch {
|
|
1587
|
+
return check("warn", id, label, "Could not compare HEAD with origin/main");
|
|
1969
1588
|
}
|
|
1970
1589
|
}
|
|
1971
1590
|
function checkVaultGitAhead(resolvedPath) {
|
|
@@ -1990,52 +1609,48 @@ function checkVaultGitBehind(resolvedPath) {
|
|
|
1990
1609
|
"0 commits behind origin/main"
|
|
1991
1610
|
);
|
|
1992
1611
|
}
|
|
1993
|
-
function
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
} catch {
|
|
2003
|
-
return void 0;
|
|
2004
|
-
}
|
|
1612
|
+
function pullLogPaths(home) {
|
|
1613
|
+
const paths = platform() === "darwin" ? [
|
|
1614
|
+
join9(home, "Library", "Logs", "wiki-pull.log"),
|
|
1615
|
+
join9(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
|
|
1616
|
+
] : [
|
|
1617
|
+
join9(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
|
|
1618
|
+
join9(home, "Library", "Logs", "wiki-pull.log")
|
|
1619
|
+
];
|
|
1620
|
+
return [...new Set(paths)];
|
|
2005
1621
|
}
|
|
2006
|
-
function
|
|
1622
|
+
function isRecentLogLine(line, nowMs) {
|
|
1623
|
+
const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
|
|
1624
|
+
if (!match) return true;
|
|
1625
|
+
const ts = Date.parse(match[1]);
|
|
1626
|
+
if (!Number.isFinite(ts)) return true;
|
|
1627
|
+
return nowMs - ts <= 24 * 60 * 60 * 1e3;
|
|
1628
|
+
}
|
|
1629
|
+
function checkVaultGitPullFailures(home) {
|
|
1630
|
+
const path = pullLogPaths(home).find((p) => existsSync6(p));
|
|
1631
|
+
if (!path) {
|
|
1632
|
+
return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
|
|
1633
|
+
}
|
|
2007
1634
|
try {
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
1635
|
+
const lines = readFileSync3(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
1636
|
+
const now = Date.now();
|
|
1637
|
+
const failures = lines.filter(
|
|
1638
|
+
(line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
|
|
1639
|
+
);
|
|
1640
|
+
if (failures.length > 0) {
|
|
1641
|
+
const sample = failures.slice(-2).map((line) => line.slice(0, 100)).join(" | ");
|
|
1642
|
+
return check("warn", "vault_git_pull_failures", "Vault pull failures", `${failures.length} recent pull failure(s): ${sample}`);
|
|
1643
|
+
}
|
|
1644
|
+
return check("pass", "vault_git_pull_failures", "Vault pull failures", "No recent pull failures logged");
|
|
2016
1645
|
} catch {
|
|
2017
|
-
return
|
|
1646
|
+
return check("warn", "vault_git_pull_failures", "Vault pull failures", `Could not read ${path}`);
|
|
2018
1647
|
}
|
|
2019
1648
|
}
|
|
2020
|
-
function checkStaleRemoteMain(resolvedPath) {
|
|
2021
|
-
if (resolvedPath === void 0) return void 0;
|
|
2022
|
-
if (!existsSync7(join10(resolvedPath, ".git"))) return void 0;
|
|
2023
|
-
const localOrigin = gitRefHash(resolvedPath, "origin/main");
|
|
2024
|
-
if (!localOrigin) return void 0;
|
|
2025
|
-
const remoteMain = remoteMainHash(resolvedPath);
|
|
2026
|
-
if (!remoteMain || remoteMain === localOrigin) return void 0;
|
|
2027
|
-
return check(
|
|
2028
|
-
"warn",
|
|
2029
|
-
"vault_git_behind",
|
|
2030
|
-
"Vault commits behind",
|
|
2031
|
-
`Remote main differs from local origin/main (${remoteMain.slice(0, 8)} != ${localOrigin.slice(0, 8)}) \u2014 run git fetch before trusting behind count`
|
|
2032
|
-
);
|
|
2033
|
-
}
|
|
2034
1649
|
function checkVaultLocalGit(resolvedPath) {
|
|
2035
1650
|
if (resolvedPath === void 0) {
|
|
2036
1651
|
return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
2037
1652
|
}
|
|
2038
|
-
if (!
|
|
1653
|
+
if (!existsSync6(join9(resolvedPath, ".git"))) {
|
|
2039
1654
|
return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
|
|
2040
1655
|
}
|
|
2041
1656
|
try {
|
|
@@ -2054,7 +1669,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
|
|
|
2054
1669
|
if (resolvedPath === void 0) {
|
|
2055
1670
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
|
|
2056
1671
|
}
|
|
2057
|
-
if (!
|
|
1672
|
+
if (!existsSync6(join9(resolvedPath, ".git"))) {
|
|
2058
1673
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
|
|
2059
1674
|
}
|
|
2060
1675
|
const state = probeGithubReachability(resolvedPath, exec);
|
|
@@ -2098,7 +1713,7 @@ function checkVaultPromotionLag(resolvedPath) {
|
|
|
2098
1713
|
if (resolvedPath === void 0) {
|
|
2099
1714
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
|
|
2100
1715
|
}
|
|
2101
|
-
if (!
|
|
1716
|
+
if (!existsSync6(join9(resolvedPath, ".git"))) {
|
|
2102
1717
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
|
|
2103
1718
|
}
|
|
2104
1719
|
try {
|
|
@@ -2121,162 +1736,409 @@ function checkVaultPromotionLag(resolvedPath) {
|
|
|
2121
1736
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "Could not read origin/main \u2014 check skipped");
|
|
2122
1737
|
}
|
|
2123
1738
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
1739
|
+
var gitFleetProbe = {
|
|
1740
|
+
id: "git_fleet",
|
|
1741
|
+
async run(ctx) {
|
|
1742
|
+
return [
|
|
1743
|
+
checkVaultGitRemote(ctx.gitCheckPath),
|
|
1744
|
+
await checkFleetIdentity({
|
|
1745
|
+
vaultPath: ctx.resolvedPath,
|
|
1746
|
+
home: ctx.input.home,
|
|
1747
|
+
cwd: ctx.input.cwd,
|
|
1748
|
+
envValue: ctx.input.envValue,
|
|
1749
|
+
fleetLoad: ctx.fleetLoad
|
|
1750
|
+
}),
|
|
1751
|
+
checkSyncLastPush(ctx.gitCheckPath),
|
|
1752
|
+
checkVaultGitDirty(ctx.gitCheckPath),
|
|
1753
|
+
checkVaultGitAhead(ctx.gitCheckPath),
|
|
1754
|
+
checkVaultGitBehind(ctx.gitCheckPath),
|
|
1755
|
+
checkVaultGitPullFailures(ctx.input.home),
|
|
1756
|
+
checkVaultLocalGit(ctx.gitCheckPath),
|
|
1757
|
+
checkVaultGithubRemote(ctx.gitCheckPath, ctx.input.execProbe),
|
|
1758
|
+
checkVaultS3Remote(ctx.input.home, ctx.input.execProbe, ctx.input.env ?? process.env),
|
|
1759
|
+
checkVaultSnapshotterReachable(ctx.fleetLoad, ctx.input.checkSnapshotter, ctx.input.execProbe),
|
|
1760
|
+
checkVaultPromotionLag(ctx.gitCheckPath)
|
|
1761
|
+
];
|
|
2147
1762
|
}
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// src/doctor/probes/hygiene.ts
|
|
1766
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
|
|
1767
|
+
import { join as join11 } from "path";
|
|
1768
|
+
|
|
1769
|
+
// src/utils/conflict-markers.ts
|
|
1770
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
1771
|
+
import { join as join10 } from "path";
|
|
1772
|
+
function scanConflictMarkerBlocksInText(relPath, text) {
|
|
1773
|
+
const findings = [];
|
|
1774
|
+
const lines = text.split(/\r?\n/);
|
|
1775
|
+
let inFence = false;
|
|
1776
|
+
let openLine = 0;
|
|
1777
|
+
let sawSeparator = false;
|
|
1778
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1779
|
+
const line = lines[i];
|
|
1780
|
+
if (line.startsWith("```") || line.startsWith("~~~")) {
|
|
1781
|
+
inFence = !inFence;
|
|
1782
|
+
continue;
|
|
2166
1783
|
}
|
|
2167
|
-
if (
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
`Last run finished_at is older than 26h (${health.finishedAt})`
|
|
2173
|
-
);
|
|
1784
|
+
if (inFence) continue;
|
|
1785
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1786
|
+
openLine = i + 1;
|
|
1787
|
+
sawSeparator = false;
|
|
1788
|
+
continue;
|
|
2174
1789
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
1790
|
+
if (line === "=======" && openLine > 0) {
|
|
1791
|
+
sawSeparator = true;
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
if (line.startsWith(">>>>>>> ")) {
|
|
1795
|
+
if (openLine > 0 && sawSeparator) {
|
|
1796
|
+
findings.push({ path: relPath, line: openLine });
|
|
1797
|
+
}
|
|
1798
|
+
openLine = 0;
|
|
1799
|
+
sawSeparator = false;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return findings;
|
|
1803
|
+
}
|
|
1804
|
+
var PRUNE_DIRS = /* @__PURE__ */ new Set([
|
|
1805
|
+
".git",
|
|
1806
|
+
".obsidian",
|
|
1807
|
+
".skillwiki",
|
|
1808
|
+
".claude",
|
|
1809
|
+
".antigravitycli",
|
|
1810
|
+
".playwright-cli"
|
|
1811
|
+
]);
|
|
1812
|
+
function walkMarkdownFiles(root, dir, rel, out) {
|
|
1813
|
+
let entries;
|
|
1814
|
+
try {
|
|
1815
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
2181
1816
|
} catch {
|
|
2182
|
-
return
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
for (const entry of entries) {
|
|
1820
|
+
if (entry.isDirectory()) {
|
|
1821
|
+
if (PRUNE_DIRS.has(entry.name)) continue;
|
|
1822
|
+
walkMarkdownFiles(root, join10(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
|
|
1823
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1824
|
+
out.push(rel ? `${rel}/${entry.name}` : entry.name);
|
|
1825
|
+
}
|
|
2183
1826
|
}
|
|
2184
1827
|
}
|
|
2185
|
-
function
|
|
2186
|
-
return
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
return void 0;
|
|
2197
|
-
}
|
|
1828
|
+
function scanVaultConflictMarkers(vaultRoot) {
|
|
1829
|
+
if (!existsSync7(vaultRoot)) return [];
|
|
1830
|
+
const relPaths = [];
|
|
1831
|
+
walkMarkdownFiles(vaultRoot, vaultRoot, "", relPaths);
|
|
1832
|
+
const all = [];
|
|
1833
|
+
for (const rel of relPaths) {
|
|
1834
|
+
let text;
|
|
1835
|
+
try {
|
|
1836
|
+
text = readFileSync4(join10(vaultRoot, rel), "utf8");
|
|
1837
|
+
} catch {
|
|
1838
|
+
continue;
|
|
2198
1839
|
}
|
|
2199
|
-
|
|
1840
|
+
all.push(...scanConflictMarkerBlocksInText(rel, text));
|
|
1841
|
+
}
|
|
1842
|
+
return all;
|
|
2200
1843
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
1844
|
+
|
|
1845
|
+
// src/doctor/probes/hygiene.ts
|
|
1846
|
+
function checkDotStoreClean(resolvedPath) {
|
|
1847
|
+
if (resolvedPath === void 0) {
|
|
1848
|
+
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
2204
1849
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
1850
|
+
const rawDir = join11(resolvedPath, "raw");
|
|
1851
|
+
if (!existsSync8(rawDir)) {
|
|
1852
|
+
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
2207
1853
|
}
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
1854
|
+
const found = [];
|
|
1855
|
+
(function walk(dir, rel) {
|
|
1856
|
+
let entries;
|
|
1857
|
+
try {
|
|
1858
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
1859
|
+
} catch {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
for (const entry of entries) {
|
|
1863
|
+
if (entry.name === ".DS_Store") {
|
|
1864
|
+
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
1865
|
+
} else if (entry.isDirectory()) {
|
|
1866
|
+
walk(join11(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
})(rawDir, "");
|
|
1870
|
+
if (found.length === 0) {
|
|
1871
|
+
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "No .DS_Store files found");
|
|
2211
1872
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
1873
|
+
return check("info", "dsstore_clean", "No .DS_Store in raw/", `${found.length} .DS_Store file(s) found \u2014 remove with: find ${rawDir} -name .DS_Store -delete`);
|
|
1874
|
+
}
|
|
1875
|
+
function checkVaultConflictMarkers(resolvedPath) {
|
|
1876
|
+
if (resolvedPath === void 0) {
|
|
1877
|
+
return check("pass", "vault_conflict_markers", "Vault conflict markers", "No vault path \u2014 check skipped");
|
|
1878
|
+
}
|
|
1879
|
+
const findings = scanVaultConflictMarkers(resolvedPath);
|
|
1880
|
+
if (findings.length === 0) {
|
|
1881
|
+
return check("pass", "vault_conflict_markers", "Vault conflict markers", "No complete conflict-marker blocks");
|
|
2214
1882
|
}
|
|
1883
|
+
const first = findings[0];
|
|
1884
|
+
const n = findings.length;
|
|
1885
|
+
const fileWord = n === 1 ? "file" : "files";
|
|
2215
1886
|
return check(
|
|
2216
1887
|
"error",
|
|
2217
|
-
"
|
|
2218
|
-
"
|
|
2219
|
-
|
|
1888
|
+
"vault_conflict_markers",
|
|
1889
|
+
"Vault conflict markers",
|
|
1890
|
+
`${n} ${fileWord}, first: ${first.path}:${first.line}`
|
|
2220
1891
|
);
|
|
2221
1892
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
1893
|
+
var hygieneProbe = {
|
|
1894
|
+
id: "hygiene",
|
|
1895
|
+
run(ctx) {
|
|
1896
|
+
return [
|
|
1897
|
+
checkDotStoreClean(ctx.readOnlyScanRoot),
|
|
1898
|
+
checkVaultConflictMarkers(ctx.readOnlyScanRoot)
|
|
1899
|
+
];
|
|
2225
1900
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
// src/doctor/probes/s3-mount-health.ts
|
|
1904
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1905
|
+
import { join as join13 } from "path";
|
|
1906
|
+
import { execSync as execSync4 } from "child_process";
|
|
1907
|
+
|
|
1908
|
+
// src/utils/s3-mount-health.ts
|
|
1909
|
+
import { execSync as execSync3 } from "child_process";
|
|
1910
|
+
import { platform as platform2 } from "os";
|
|
1911
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, readFileSync as readFile6 } from "fs";
|
|
1912
|
+
import { join as join12 } from "path";
|
|
1913
|
+
var OS = platform2();
|
|
1914
|
+
function findRcloneMountPid() {
|
|
1915
|
+
try {
|
|
1916
|
+
const out = execSync3("pgrep -f 'rclone.*mount'", {
|
|
1917
|
+
encoding: "utf8",
|
|
1918
|
+
timeout: 2e3,
|
|
1919
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1920
|
+
}).trim();
|
|
1921
|
+
const pids = out.split("\n").filter(Boolean);
|
|
1922
|
+
if (pids.length === 0) return null;
|
|
1923
|
+
return parseInt(pids[0], 10);
|
|
1924
|
+
} catch {
|
|
1925
|
+
try {
|
|
1926
|
+
const out = execSync3("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
|
|
1927
|
+
for (const line of out.split("\n")) {
|
|
1928
|
+
if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
|
|
1929
|
+
const parts = line.trim().split(/\s+/);
|
|
1930
|
+
if (parts.length >= 2) return parseInt(parts[1], 10);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1935
|
+
return null;
|
|
2236
1936
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
1937
|
+
}
|
|
1938
|
+
function parseRcloneFlags(pid) {
|
|
1939
|
+
const flags = /* @__PURE__ */ new Map();
|
|
1940
|
+
try {
|
|
1941
|
+
const args = getRcloneArgs(pid);
|
|
1942
|
+
for (let i = 0; i < args.length; i++) {
|
|
1943
|
+
const arg = args[i];
|
|
1944
|
+
if (arg.startsWith("--") && arg.includes("=")) {
|
|
1945
|
+
const eq = arg.indexOf("=");
|
|
1946
|
+
flags.set(arg.slice(0, eq), arg.slice(eq + 1));
|
|
1947
|
+
} else if (arg.startsWith("--")) {
|
|
1948
|
+
const next = args[i + 1];
|
|
1949
|
+
if (next && !next.startsWith("-")) {
|
|
1950
|
+
flags.set(arg, next);
|
|
1951
|
+
i++;
|
|
1952
|
+
} else {
|
|
1953
|
+
flags.set(arg, "");
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
} catch {
|
|
2239
1958
|
}
|
|
2240
|
-
|
|
2241
|
-
return check("warn", "fleet_identity", "Fleet identity", detail);
|
|
1959
|
+
return flags;
|
|
2242
1960
|
}
|
|
2243
|
-
function
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
1961
|
+
function getRcloneVersion() {
|
|
1962
|
+
try {
|
|
1963
|
+
const out = execSync3("rclone version", {
|
|
1964
|
+
encoding: "utf8",
|
|
1965
|
+
timeout: 3e3,
|
|
1966
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1967
|
+
});
|
|
1968
|
+
const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
|
|
1969
|
+
if (!match) return null;
|
|
1970
|
+
return {
|
|
1971
|
+
major: parseInt(match[1], 10),
|
|
1972
|
+
minor: parseInt(match[2], 10),
|
|
1973
|
+
patch: parseInt(match[3], 10),
|
|
1974
|
+
raw: out.split("\n")[0].trim()
|
|
1975
|
+
};
|
|
1976
|
+
} catch {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
2252
1979
|
}
|
|
2253
|
-
function
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
1980
|
+
function extractRcloneFs(args) {
|
|
1981
|
+
let foundMount = false;
|
|
1982
|
+
for (const arg of args) {
|
|
1983
|
+
if (arg === "mount") {
|
|
1984
|
+
foundMount = true;
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
|
|
1988
|
+
return arg;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
function getRcloneArgs(pid) {
|
|
1994
|
+
try {
|
|
1995
|
+
if (OS === "linux") {
|
|
1996
|
+
const raw = readFileSync5(`/proc/${pid}/cmdline`);
|
|
1997
|
+
return new TextDecoder().decode(raw).split("\0").filter(Boolean);
|
|
1998
|
+
} else {
|
|
1999
|
+
const out = execSync3(`ps -o args= -p ${pid}`, {
|
|
2000
|
+
encoding: "utf8",
|
|
2001
|
+
timeout: 2e3,
|
|
2002
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2003
|
+
}).trim();
|
|
2004
|
+
return out.split(/\s+/);
|
|
2005
|
+
}
|
|
2006
|
+
} catch {
|
|
2007
|
+
return [];
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
function queryRcloneRC(rcAddr, fs) {
|
|
2011
|
+
try {
|
|
2012
|
+
const payload = JSON.stringify({ fs });
|
|
2013
|
+
const out = execSync3(
|
|
2014
|
+
`curl -s --max-time 3 -X POST "http://${rcAddr}/vfs/stats" -H "Content-Type: application/json" -d '${payload}' 2>/dev/null`,
|
|
2015
|
+
{ encoding: "utf8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
2016
|
+
);
|
|
2017
|
+
if (!out.trim()) return null;
|
|
2018
|
+
const data = JSON.parse(out);
|
|
2019
|
+
if (data.status && data.status >= 400) {
|
|
2020
|
+
return { error: data.error || `RC error (status ${data.status})`, erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
|
|
2021
|
+
}
|
|
2022
|
+
const dc = data.diskCache || {};
|
|
2023
|
+
return {
|
|
2024
|
+
erroredFiles: dc.erroredFiles ?? 0,
|
|
2025
|
+
uploadsInProgress: dc.uploadsInProgress ?? 0,
|
|
2026
|
+
uploadsQueued: dc.uploadsQueued ?? 0,
|
|
2027
|
+
outOfSpace: dc.outOfSpace ?? false,
|
|
2028
|
+
bytesUsed: dc.bytesUsed ?? 0,
|
|
2029
|
+
files: dc.files ?? 0,
|
|
2030
|
+
totalSize: data.totalSize || "unknown"
|
|
2031
|
+
};
|
|
2032
|
+
} catch {
|
|
2033
|
+
return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
function detectFuseMount(vaultPath) {
|
|
2037
|
+
try {
|
|
2038
|
+
if (OS === "linux") {
|
|
2039
|
+
const mounts = readFileSync5("/proc/mounts", "utf8");
|
|
2040
|
+
let best = null;
|
|
2041
|
+
for (const line of mounts.split("\n")) {
|
|
2042
|
+
const parts = line.split(" ");
|
|
2043
|
+
if (parts.length < 3) continue;
|
|
2044
|
+
const point = parts[1];
|
|
2045
|
+
const fs = parts[2];
|
|
2046
|
+
if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
|
|
2047
|
+
best = { point, fs };
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
if (best && best.fs.includes("fuse")) return { mountPoint: best.point, fsType: best.fs };
|
|
2051
|
+
} else if (OS === "darwin") {
|
|
2052
|
+
const out = execSync3("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
2053
|
+
let best = null;
|
|
2054
|
+
for (const line of out.split("\n")) {
|
|
2055
|
+
const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
|
|
2056
|
+
if (!match) continue;
|
|
2057
|
+
const point = match[2];
|
|
2058
|
+
const opts = match[3];
|
|
2059
|
+
if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
|
|
2060
|
+
best = { point, fsType: `fuse.${match[1].split(":")[0] || "unknown"}` };
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
if (best) return { mountPoint: best.point, fsType: best.fsType };
|
|
2064
|
+
}
|
|
2065
|
+
} catch {
|
|
2066
|
+
}
|
|
2067
|
+
return null;
|
|
2259
2068
|
}
|
|
2260
|
-
function
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2263
|
-
|
|
2069
|
+
function writeTest(dir) {
|
|
2070
|
+
const testFile = join12(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
2071
|
+
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
2072
|
+
const start = Date.now();
|
|
2073
|
+
try {
|
|
2074
|
+
writeFileSync2(testFile, payload, "utf8");
|
|
2075
|
+
} catch (e) {
|
|
2076
|
+
return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
|
|
2264
2077
|
}
|
|
2078
|
+
const writeMs = Date.now() - start;
|
|
2079
|
+
const readStart = Date.now();
|
|
2265
2080
|
try {
|
|
2266
|
-
const
|
|
2267
|
-
const
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
return
|
|
2081
|
+
const back = readFile6(testFile, "utf8");
|
|
2082
|
+
const readMs = Date.now() - readStart;
|
|
2083
|
+
if (back !== payload) {
|
|
2084
|
+
try {
|
|
2085
|
+
unlinkSync2(testFile);
|
|
2086
|
+
} catch {
|
|
2087
|
+
}
|
|
2088
|
+
return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
|
|
2274
2089
|
}
|
|
2275
|
-
|
|
2090
|
+
} catch (e) {
|
|
2091
|
+
try {
|
|
2092
|
+
unlinkSync2(testFile);
|
|
2093
|
+
} catch {
|
|
2094
|
+
}
|
|
2095
|
+
return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
|
|
2096
|
+
}
|
|
2097
|
+
try {
|
|
2098
|
+
unlinkSync2(testFile);
|
|
2276
2099
|
} catch {
|
|
2277
|
-
return check("warn", "vault_git_pull_failures", "Vault pull failures", `Could not read ${path}`);
|
|
2278
2100
|
}
|
|
2101
|
+
return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
|
|
2102
|
+
}
|
|
2103
|
+
var DURATION_UNIT_SECONDS = {
|
|
2104
|
+
ms: 1 / 1e3,
|
|
2105
|
+
s: 1,
|
|
2106
|
+
m: 60,
|
|
2107
|
+
h: 3600,
|
|
2108
|
+
d: 86400,
|
|
2109
|
+
w: 604800
|
|
2110
|
+
};
|
|
2111
|
+
function parseDurationSeconds(raw) {
|
|
2112
|
+
const input = raw.trim().toLowerCase();
|
|
2113
|
+
if (!input) return null;
|
|
2114
|
+
if (/^\d+(?:\.\d+)?$/.test(input)) {
|
|
2115
|
+
const num = parseFloat(input);
|
|
2116
|
+
return Number.isFinite(num) ? num : null;
|
|
2117
|
+
}
|
|
2118
|
+
const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g;
|
|
2119
|
+
let total = 0;
|
|
2120
|
+
let consumed = 0;
|
|
2121
|
+
for (const match of input.matchAll(re)) {
|
|
2122
|
+
const full = match[0];
|
|
2123
|
+
const value = parseFloat(match[1]);
|
|
2124
|
+
const unit = match[2];
|
|
2125
|
+
if (!Number.isFinite(value)) return null;
|
|
2126
|
+
const factor = DURATION_UNIT_SECONDS[unit];
|
|
2127
|
+
if (factor === void 0) return null;
|
|
2128
|
+
total += value * factor;
|
|
2129
|
+
consumed += full.length;
|
|
2130
|
+
}
|
|
2131
|
+
if (consumed !== input.length) return null;
|
|
2132
|
+
return total;
|
|
2279
2133
|
}
|
|
2134
|
+
var FLAG_THRESHOLDS = {
|
|
2135
|
+
"--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
|
|
2136
|
+
"--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
|
|
2137
|
+
"--vfs-cache-max-age": { min: 24, unit: "h", label: "VFS cache max age" }
|
|
2138
|
+
};
|
|
2139
|
+
var MIN_RCLONE_VERSION = { major: 1, minor: 65, patch: 0 };
|
|
2140
|
+
|
|
2141
|
+
// src/doctor/probes/s3-mount-health.ts
|
|
2280
2142
|
function checkS3MountPerf(resolvedPath) {
|
|
2281
2143
|
if (resolvedPath === void 0) {
|
|
2282
2144
|
return check("pass", "s3_mount_perf", "S3 mount performance", "No vault path \u2014 check skipped");
|
|
@@ -2286,14 +2148,14 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
2286
2148
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
2287
2149
|
}
|
|
2288
2150
|
const mountPoint = fuse.mountPoint;
|
|
2289
|
-
const conceptsDir =
|
|
2290
|
-
if (!
|
|
2151
|
+
const conceptsDir = join13(resolvedPath, "concepts");
|
|
2152
|
+
if (!existsSync10(conceptsDir)) {
|
|
2291
2153
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
2292
2154
|
}
|
|
2293
2155
|
const start = Date.now();
|
|
2294
2156
|
let timedOut = false;
|
|
2295
2157
|
try {
|
|
2296
|
-
|
|
2158
|
+
execSync4(`rg -l "." "${conceptsDir}"`, {
|
|
2297
2159
|
timeout: 5e3,
|
|
2298
2160
|
encoding: "utf8",
|
|
2299
2161
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2469,8 +2331,8 @@ function checkWriteTest(resolvedPath) {
|
|
|
2469
2331
|
if (!fuse) {
|
|
2470
2332
|
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
2471
2333
|
}
|
|
2472
|
-
const conceptsDir =
|
|
2473
|
-
if (!
|
|
2334
|
+
const conceptsDir = join13(resolvedPath, "concepts");
|
|
2335
|
+
if (!existsSync10(conceptsDir)) {
|
|
2474
2336
|
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
2475
2337
|
}
|
|
2476
2338
|
const result = writeTest(conceptsDir);
|
|
@@ -2554,45 +2416,266 @@ function checkVfsCacheHealth(resolvedPath) {
|
|
|
2554
2416
|
`${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
|
|
2555
2417
|
);
|
|
2556
2418
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2419
|
+
var s3MountHealthProbe = {
|
|
2420
|
+
id: "s3_mount_health",
|
|
2421
|
+
run(ctx) {
|
|
2422
|
+
return [
|
|
2423
|
+
checkS3MountPerf(ctx.resolvedPath),
|
|
2424
|
+
checkS3MountFreshness(ctx.resolvedPath),
|
|
2425
|
+
checkRcloneFlagAudit(ctx.resolvedPath),
|
|
2426
|
+
checkRcloneVersion(ctx.resolvedPath, ctx.vsConfig.installed),
|
|
2427
|
+
checkWriteTest(ctx.resolvedPath),
|
|
2428
|
+
checkVfsCacheHealth(ctx.resolvedPath)
|
|
2429
|
+
];
|
|
2565
2430
|
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
// src/doctor/probes/skills-plugins.ts
|
|
2434
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
|
|
2435
|
+
import { join as join14 } from "path";
|
|
2436
|
+
function findSkillMd(dir) {
|
|
2437
|
+
const results = [];
|
|
2438
|
+
let entries;
|
|
2439
|
+
try {
|
|
2440
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2441
|
+
} catch {
|
|
2442
|
+
return results;
|
|
2443
|
+
}
|
|
2444
|
+
for (const entry of entries) {
|
|
2445
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
2446
|
+
results.push(join14(dir, entry.name));
|
|
2447
|
+
} else if (entry.isDirectory()) {
|
|
2448
|
+
results.push(...findSkillMd(join14(dir, entry.name)));
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return results;
|
|
2572
2452
|
}
|
|
2573
|
-
function
|
|
2574
|
-
|
|
2575
|
-
|
|
2453
|
+
function findSkillNames(dir) {
|
|
2454
|
+
const results = [];
|
|
2455
|
+
let entries;
|
|
2456
|
+
try {
|
|
2457
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2458
|
+
} catch {
|
|
2459
|
+
return results;
|
|
2460
|
+
}
|
|
2461
|
+
for (const entry of entries) {
|
|
2462
|
+
if (entry.isDirectory() && existsSync11(join14(dir, entry.name, "SKILL.md"))) {
|
|
2463
|
+
results.push(entry.name);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return results;
|
|
2467
|
+
}
|
|
2468
|
+
function findInstalledSkillMd(dir) {
|
|
2469
|
+
const directSkills = findSkillNames(dir).map((name) => join14(dir, name, "SKILL.md"));
|
|
2470
|
+
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
2471
|
+
}
|
|
2472
|
+
function checkSkillsInstalled(home, cwd) {
|
|
2473
|
+
const srcDir = cwd ? join14(cwd, "packages", "skills") : void 0;
|
|
2474
|
+
if (srcDir && existsSync11(srcDir)) {
|
|
2475
|
+
const found = findInstalledSkillMd(srcDir);
|
|
2476
|
+
if (found.length > 0) {
|
|
2477
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const plugin = findPlugin(home);
|
|
2481
|
+
if (plugin) {
|
|
2482
|
+
const found = findInstalledSkillMd(plugin.installPath);
|
|
2483
|
+
if (found.length > 0) {
|
|
2484
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
const skillsDir = join14(home, ".claude", "skills");
|
|
2488
|
+
if (existsSync11(skillsDir)) {
|
|
2489
|
+
const found = findInstalledSkillMd(skillsDir);
|
|
2490
|
+
if (found.length > 0) {
|
|
2491
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found");
|
|
2495
|
+
}
|
|
2496
|
+
function checkDuplicateSkills(home) {
|
|
2497
|
+
const plugin = findPlugin(home);
|
|
2498
|
+
const skillsDir = join14(home, ".claude", "skills");
|
|
2499
|
+
const agentSkillDirs = [
|
|
2500
|
+
{ label: "~/.codex/skills/", path: join14(home, ".codex", "skills") },
|
|
2501
|
+
{ label: "~/.agents/skills/", path: join14(home, ".agents", "skills") }
|
|
2502
|
+
];
|
|
2503
|
+
if (!plugin) {
|
|
2504
|
+
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
2505
|
+
}
|
|
2506
|
+
const pluginSkills = findSkillNames(plugin.installPath);
|
|
2507
|
+
const cliSkills = findSkillNames(skillsDir);
|
|
2508
|
+
const cliDuplicates = cliSkills.filter((name) => pluginSkills.includes(name));
|
|
2509
|
+
const agentDuplicates = [];
|
|
2510
|
+
for (const { label, path } of agentSkillDirs) {
|
|
2511
|
+
const overlap = findSkillNames(path).filter((name) => pluginSkills.includes(name));
|
|
2512
|
+
if (overlap.length > 0) {
|
|
2513
|
+
agentDuplicates.push({ dir: label, names: overlap });
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
if (cliDuplicates.length === 0 && agentDuplicates.length === 0) {
|
|
2517
|
+
return check("pass", "skills_duplicate", "Skills not duplicated", "No overlap between plugin and other channels");
|
|
2518
|
+
}
|
|
2519
|
+
const parts = [];
|
|
2520
|
+
if (cliDuplicates.length > 0) {
|
|
2521
|
+
parts.push(`${cliDuplicates.length} skill(s) in both plugin and ~/.claude/skills/ \u2014 remove CLI copies: rm -r ~/.claude/skills/{${cliDuplicates.slice(0, 3).join(",")}${cliDuplicates.length > 3 ? ",\u2026" : ""}}`);
|
|
2522
|
+
}
|
|
2523
|
+
for (const { dir, names } of agentDuplicates) {
|
|
2524
|
+
parts.push(`${names.length} stale skill(s) in ${dir} \u2014 plugin provides: ${names.slice(0, 3).join(", ")}${names.length > 3 ? ", \u2026" : ""}`);
|
|
2525
|
+
}
|
|
2526
|
+
const status = cliDuplicates.length > 0 ? "warn" : "info";
|
|
2527
|
+
return check(status, "skills_duplicate", "Skills not duplicated", parts.join("; "));
|
|
2528
|
+
}
|
|
2529
|
+
var GROK_ACTIVATION_REFERENCE = "Read @~/.grok/skillwiki.md for SkillWiki activation context.";
|
|
2530
|
+
var STALE_GROK_ACTIVATION_REFERENCE = "Read @skillwiki.md";
|
|
2531
|
+
var ACTIVATION_FIX_HINT = "run `npm run install:activation` from the llm-wiki repo";
|
|
2532
|
+
function findGrokActivationTemplate(home, cwd) {
|
|
2533
|
+
if (cwd) {
|
|
2534
|
+
const src = join14(cwd, "packages", "skills", "using-skillwiki", "activation.md");
|
|
2535
|
+
if (existsSync11(src)) return src;
|
|
2576
2536
|
}
|
|
2537
|
+
const pluginsRoot = join14(home, ".grok", "installed-plugins");
|
|
2538
|
+
if (!existsSync11(pluginsRoot)) return void 0;
|
|
2539
|
+
let entries;
|
|
2577
2540
|
try {
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2541
|
+
entries = readdirSync4(pluginsRoot, { withFileTypes: true });
|
|
2542
|
+
} catch {
|
|
2543
|
+
return void 0;
|
|
2544
|
+
}
|
|
2545
|
+
for (const entry of entries) {
|
|
2546
|
+
if (!entry.isDirectory()) continue;
|
|
2547
|
+
const root = join14(pluginsRoot, entry.name);
|
|
2548
|
+
for (const rel of ["using-skillwiki/activation.md", "skills/using-skillwiki/activation.md"]) {
|
|
2549
|
+
const candidate = join14(root, rel);
|
|
2550
|
+
if (existsSync11(candidate)) return candidate;
|
|
2581
2551
|
}
|
|
2582
|
-
|
|
2552
|
+
}
|
|
2553
|
+
return void 0;
|
|
2554
|
+
}
|
|
2555
|
+
function checkGrokActivation(home, cwd) {
|
|
2556
|
+
const grokDir = join14(home, ".grok");
|
|
2557
|
+
if (!existsSync11(grokDir)) {
|
|
2558
|
+
return check("pass", "activation_grok", "Grok activation", "Not a Grok host");
|
|
2559
|
+
}
|
|
2560
|
+
const activationPath = join14(grokDir, "skillwiki.md");
|
|
2561
|
+
const agentsPath = join14(grokDir, "AGENTS.md");
|
|
2562
|
+
const hasActivation = existsSync11(activationPath);
|
|
2563
|
+
const issues = [];
|
|
2564
|
+
if (!hasActivation) {
|
|
2565
|
+
issues.push("~/.grok/skillwiki.md missing");
|
|
2566
|
+
}
|
|
2567
|
+
if (!existsSync11(agentsPath)) {
|
|
2568
|
+
issues.push("~/.grok/AGENTS.md missing");
|
|
2569
|
+
} else {
|
|
2570
|
+
const agents = readFileSync6(agentsPath, "utf8");
|
|
2571
|
+
const hasBegin = agents.includes("<!-- skillwiki:begin -->");
|
|
2572
|
+
const hasExpected = agents.includes(GROK_ACTIVATION_REFERENCE);
|
|
2573
|
+
const hasStale = agents.includes(STALE_GROK_ACTIVATION_REFERENCE);
|
|
2574
|
+
if (!hasBegin) {
|
|
2575
|
+
issues.push("AGENTS.md marker missing");
|
|
2576
|
+
} else if (hasStale && !hasExpected) {
|
|
2577
|
+
issues.push("AGENTS.md marker is stale (@skillwiki.md)");
|
|
2578
|
+
} else if (!hasExpected) {
|
|
2579
|
+
issues.push("AGENTS.md marker is stale");
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
if (hasActivation) {
|
|
2583
|
+
const template = findGrokActivationTemplate(home, cwd);
|
|
2584
|
+
if (template) {
|
|
2585
|
+
try {
|
|
2586
|
+
const installed = readFileSync6(activationPath, "utf8");
|
|
2587
|
+
const expected = readFileSync6(template, "utf8");
|
|
2588
|
+
if (installed !== expected) {
|
|
2589
|
+
issues.push("~/.grok/skillwiki.md differs from template");
|
|
2590
|
+
}
|
|
2591
|
+
} catch {
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
if (issues.length > 0) {
|
|
2583
2596
|
return check(
|
|
2584
2597
|
"warn",
|
|
2585
|
-
"
|
|
2586
|
-
"
|
|
2587
|
-
`${
|
|
2598
|
+
"activation_grok",
|
|
2599
|
+
"Grok activation",
|
|
2600
|
+
`${issues.join("; ")} \u2014 ${ACTIVATION_FIX_HINT}`
|
|
2588
2601
|
);
|
|
2589
|
-
} catch {
|
|
2590
|
-
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
|
|
2591
2602
|
}
|
|
2603
|
+
return check("pass", "activation_grok", "Grok activation", "Marker and compact file are current");
|
|
2604
|
+
}
|
|
2605
|
+
function checkNpmUpdate(home, currentVersion) {
|
|
2606
|
+
const { hasUpdate, latest, distTag } = latestFromCache(home, currentVersion);
|
|
2607
|
+
if (!latest) {
|
|
2608
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: no cache yet)`);
|
|
2609
|
+
}
|
|
2610
|
+
if (hasUpdate) {
|
|
2611
|
+
return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 ${distTag} update available: v${latest}. Run \`skillwiki update --tag ${distTag}\`.`);
|
|
2612
|
+
}
|
|
2613
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (${distTag}: v${latest})`);
|
|
2614
|
+
}
|
|
2615
|
+
function pluginUpdateCommand(plugin, currentVersion) {
|
|
2616
|
+
if (semverGt(plugin.version, currentVersion)) {
|
|
2617
|
+
return "npm install -g skillwiki@latest";
|
|
2618
|
+
}
|
|
2619
|
+
if (plugin.channel === "claude") {
|
|
2620
|
+
return "claude plugin update skillwiki@llm-wiki";
|
|
2621
|
+
}
|
|
2622
|
+
if (plugin.sourceType === "git") {
|
|
2623
|
+
return "codex plugin marketplace upgrade llm-wiki && codex plugin remove skillwiki@llm-wiki && codex plugin add skillwiki@llm-wiki";
|
|
2624
|
+
}
|
|
2625
|
+
return "codex plugin remove skillwiki@llm-wiki && codex plugin add skillwiki@llm-wiki";
|
|
2626
|
+
}
|
|
2627
|
+
function checkPluginVersionDrift(home, currentVersion, devSourceRun) {
|
|
2628
|
+
const plugins = findPluginInstallations(home);
|
|
2629
|
+
if (plugins.length === 0) {
|
|
2630
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin not installed \u2014 CLI only");
|
|
2631
|
+
}
|
|
2632
|
+
const drifted = plugins.filter((plugin) => plugin.version !== currentVersion);
|
|
2633
|
+
if (drifted.length === 0) {
|
|
2634
|
+
if (plugins.length === 1 && plugins[0].channel === "claude") {
|
|
2635
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
2636
|
+
}
|
|
2637
|
+
if (plugins.length === 1) {
|
|
2638
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `${plugins[0].label} plugin and CLI both at v${currentVersion}`);
|
|
2639
|
+
}
|
|
2640
|
+
const labels = plugins.map((plugin) => `${plugin.label} plugin`).join(", ");
|
|
2641
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `${labels}, and CLI all at v${currentVersion}`);
|
|
2642
|
+
}
|
|
2643
|
+
if (devSourceRun && drifted.every((plugin) => semverGt(currentVersion, plugin.version))) {
|
|
2644
|
+
const details2 = drifted.map((plugin) => `${plugin.label} plugin v${plugin.version}`).join(", ");
|
|
2645
|
+
return check("info", "plugin_version_drift", "Plugin/CLI version", `Dev source v${currentVersion} is ahead of installed ${details2}`);
|
|
2646
|
+
}
|
|
2647
|
+
const details = drifted.map((plugin) => {
|
|
2648
|
+
const updateCmd = pluginUpdateCommand(plugin, currentVersion);
|
|
2649
|
+
return `${plugin.label} plugin v${plugin.version} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``;
|
|
2650
|
+
});
|
|
2651
|
+
return check(
|
|
2652
|
+
"warn",
|
|
2653
|
+
"plugin_version_drift",
|
|
2654
|
+
"Plugin/CLI version",
|
|
2655
|
+
details.join("; ")
|
|
2656
|
+
);
|
|
2592
2657
|
}
|
|
2658
|
+
var skillsPluginsProbe = {
|
|
2659
|
+
id: "skills_plugins",
|
|
2660
|
+
run(ctx) {
|
|
2661
|
+
return [
|
|
2662
|
+
checkSkillsInstalled(ctx.input.home, ctx.input.cwd),
|
|
2663
|
+
checkDuplicateSkills(ctx.input.home),
|
|
2664
|
+
checkGrokActivation(ctx.input.home, ctx.input.cwd),
|
|
2665
|
+
checkNpmUpdate(ctx.input.home, ctx.input.currentVersion),
|
|
2666
|
+
checkPluginVersionDrift(ctx.input.home, ctx.input.currentVersion, ctx.devSourceRun)
|
|
2667
|
+
];
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
|
|
2671
|
+
// src/doctor/probes/vault-sync.ts
|
|
2672
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
2673
|
+
import { join as join15 } from "path";
|
|
2674
|
+
import { execSync as execSync5 } from "child_process";
|
|
2675
|
+
import { platform as platform3 } from "os";
|
|
2593
2676
|
function readVaultSyncConfig(home) {
|
|
2594
2677
|
try {
|
|
2595
|
-
const content =
|
|
2678
|
+
const content = readFileSync7(join15(home, ".skillwiki", ".env"), "utf8");
|
|
2596
2679
|
let installed = false;
|
|
2597
2680
|
let role;
|
|
2598
2681
|
let serviceScope;
|
|
@@ -2615,12 +2698,6 @@ function readVaultSyncConfig(home) {
|
|
|
2615
2698
|
return { installed: false };
|
|
2616
2699
|
}
|
|
2617
2700
|
}
|
|
2618
|
-
function resolveSnapshotGitWorktree(home) {
|
|
2619
|
-
const configured = resolveConfiguredSnapshotWorktree(home);
|
|
2620
|
-
if (configured) return configured;
|
|
2621
|
-
const defaultPath = "/root/wiki-git";
|
|
2622
|
-
return existsSync7(defaultPath) ? defaultPath : void 0;
|
|
2623
|
-
}
|
|
2624
2701
|
function normalizeSystemdValue(raw) {
|
|
2625
2702
|
if (raw == null) return void 0;
|
|
2626
2703
|
const v = raw.trim();
|
|
@@ -2632,9 +2709,9 @@ function hasCompletedRunEvidence(...timestamps) {
|
|
|
2632
2709
|
}
|
|
2633
2710
|
function loadSnapshotFixture(env) {
|
|
2634
2711
|
const path = env.VS_SNAPSHOT_HEALTH_FIXTURE;
|
|
2635
|
-
if (!path || !
|
|
2712
|
+
if (!path || !existsSync12(path)) return null;
|
|
2636
2713
|
try {
|
|
2637
|
-
return JSON.parse(
|
|
2714
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2638
2715
|
} catch {
|
|
2639
2716
|
return null;
|
|
2640
2717
|
}
|
|
@@ -2642,7 +2719,7 @@ function loadSnapshotFixture(env) {
|
|
|
2642
2719
|
function systemctlShowProperty(scope, unit, prop) {
|
|
2643
2720
|
try {
|
|
2644
2721
|
const cmd = scope === "system" ? `systemctl show ${unit} --property=${prop} --value` : `systemctl --user show ${unit} --property=${prop} --value`;
|
|
2645
|
-
const out =
|
|
2722
|
+
const out = execSync5(cmd, {
|
|
2646
2723
|
encoding: "utf8",
|
|
2647
2724
|
timeout: 2e3,
|
|
2648
2725
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2721,7 +2798,7 @@ function snapshotterHealthChecks(scope, logDir, env) {
|
|
|
2721
2798
|
let completionOutcome = "unknown";
|
|
2722
2799
|
const logRecords = fixture ? fixture.log_records : (() => {
|
|
2723
2800
|
try {
|
|
2724
|
-
const content =
|
|
2801
|
+
const content = readFileSync7(join15(logDir, "wiki-snapshot.log"), "utf8");
|
|
2725
2802
|
return content.split(/\r?\n/).filter(Boolean);
|
|
2726
2803
|
} catch {
|
|
2727
2804
|
return [];
|
|
@@ -2775,7 +2852,7 @@ function snapshotterHealthChecks(scope, logDir, env) {
|
|
|
2775
2852
|
return [jobs, serviceResult, freshness, consecutiveFailures];
|
|
2776
2853
|
}
|
|
2777
2854
|
function vaultSyncChecks(input) {
|
|
2778
|
-
const os = input.os ??
|
|
2855
|
+
const os = input.os ?? platform3();
|
|
2779
2856
|
const home = input.home;
|
|
2780
2857
|
if (!input.vaultSyncInstalled) {
|
|
2781
2858
|
const skip = (id, label) => check("pass", id, label, "vault-sync not installed \u2014 check skipped");
|
|
@@ -2791,14 +2868,14 @@ function vaultSyncChecks(input) {
|
|
|
2791
2868
|
];
|
|
2792
2869
|
}
|
|
2793
2870
|
const isMac = os === "darwin";
|
|
2794
|
-
const logDir = input.logDir ?? (isMac ?
|
|
2795
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
2796
|
-
const filterPath = input.filterPath ??
|
|
2797
|
-
const packagedSnapshotPath =
|
|
2871
|
+
const logDir = input.logDir ?? (isMac ? join15(home, "Library", "Logs") : join15(home, ".local", "state", "vault-sync", "log"));
|
|
2872
|
+
const shareDir = input.shareDir ?? (isMac ? join15(home, "Library", "Application Support", "vault-sync", "bin") : join15(home, ".local", "share", "vault-sync", "bin"));
|
|
2873
|
+
const filterPath = input.filterPath ?? join15(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
2874
|
+
const packagedSnapshotPath = join15(shareDir, "wiki-snapshot.sh");
|
|
2798
2875
|
const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
2799
|
-
const snapshotPath = input.snapshotScriptPath ?? (
|
|
2876
|
+
const snapshotPath = input.snapshotScriptPath ?? (existsSync12(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
|
|
2800
2877
|
if (input.vaultSyncRole === "snapshotter") {
|
|
2801
|
-
const c12 =
|
|
2878
|
+
const c12 = existsSync12(snapshotPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found snapshot script: ${snapshotPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Snapshot script not found at ${snapshotPath}`);
|
|
2802
2879
|
const serviceScope = input.vaultSyncServiceScope ?? "user";
|
|
2803
2880
|
const healthChecks = snapshotterHealthChecks(serviceScope, logDir, input.env ?? process.env);
|
|
2804
2881
|
const cFetch2 = check(
|
|
@@ -2815,7 +2892,7 @@ function vaultSyncChecks(input) {
|
|
|
2815
2892
|
);
|
|
2816
2893
|
let c52;
|
|
2817
2894
|
try {
|
|
2818
|
-
if (!
|
|
2895
|
+
if (!existsSync12(snapshotPath)) {
|
|
2819
2896
|
c52 = check(
|
|
2820
2897
|
"error",
|
|
2821
2898
|
"vault_sync_snapshot_guard",
|
|
@@ -2823,7 +2900,7 @@ function vaultSyncChecks(input) {
|
|
|
2823
2900
|
`Snapshot script not found at ${snapshotPath}`
|
|
2824
2901
|
);
|
|
2825
2902
|
} else {
|
|
2826
|
-
const content =
|
|
2903
|
+
const content = readFileSync7(snapshotPath, "utf8");
|
|
2827
2904
|
if (!content.includes("--max-delete")) {
|
|
2828
2905
|
c52 = check(
|
|
2829
2906
|
"error",
|
|
@@ -2832,7 +2909,263 @@ function vaultSyncChecks(input) {
|
|
|
2832
2909
|
`${snapshotPath} is missing --max-delete guard \u2014 dangerous without it`
|
|
2833
2910
|
);
|
|
2834
2911
|
} else {
|
|
2835
|
-
c52 = check(
|
|
2912
|
+
c52 = check(
|
|
2913
|
+
"pass",
|
|
2914
|
+
"vault_sync_snapshot_guard",
|
|
2915
|
+
"Snapshot script guard",
|
|
2916
|
+
`--max-delete present in ${snapshotPath}`
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
} catch {
|
|
2921
|
+
c52 = check(
|
|
2922
|
+
"error",
|
|
2923
|
+
"vault_sync_snapshot_guard",
|
|
2924
|
+
"Snapshot script guard",
|
|
2925
|
+
`Cannot read ${snapshotPath}`
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
return [c12, ...healthChecks, cFetch2, c42, c52];
|
|
2929
|
+
}
|
|
2930
|
+
const pushScriptPath = join15(shareDir, "wiki-push.sh");
|
|
2931
|
+
const c1 = existsSync12(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`);
|
|
2932
|
+
let c2;
|
|
2933
|
+
try {
|
|
2934
|
+
if (isMac) {
|
|
2935
|
+
const uidStr = execSync5("id -u", {
|
|
2936
|
+
encoding: "utf8",
|
|
2937
|
+
timeout: 2e3,
|
|
2938
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2939
|
+
}).trim();
|
|
2940
|
+
const uid = parseInt(uidStr, 10);
|
|
2941
|
+
execSync5(`launchctl print gui/${uid}/com.karlchow.wiki-push`, {
|
|
2942
|
+
encoding: "utf8",
|
|
2943
|
+
timeout: 2e3,
|
|
2944
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
2945
|
+
});
|
|
2946
|
+
c2 = check(
|
|
2947
|
+
"pass",
|
|
2948
|
+
"vault_sync_jobs_enabled",
|
|
2949
|
+
"Vault sync jobs enabled",
|
|
2950
|
+
"launchd: com.karlchow.wiki-push loaded"
|
|
2951
|
+
);
|
|
2952
|
+
} else {
|
|
2953
|
+
const out = execSync5("systemctl --user is-enabled wiki-push.timer", {
|
|
2954
|
+
encoding: "utf8",
|
|
2955
|
+
timeout: 2e3,
|
|
2956
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2957
|
+
}).trim();
|
|
2958
|
+
if (out === "enabled") {
|
|
2959
|
+
c2 = check(
|
|
2960
|
+
"pass",
|
|
2961
|
+
"vault_sync_jobs_enabled",
|
|
2962
|
+
"Vault sync jobs enabled",
|
|
2963
|
+
"systemd: wiki-push.timer enabled"
|
|
2964
|
+
);
|
|
2965
|
+
} else {
|
|
2966
|
+
c2 = check(
|
|
2967
|
+
"error",
|
|
2968
|
+
"vault_sync_jobs_enabled",
|
|
2969
|
+
"Vault sync jobs enabled",
|
|
2970
|
+
`systemd: wiki-push.timer is ${out} \u2014 run vault-sync-install`
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
} catch {
|
|
2975
|
+
c2 = check(
|
|
2976
|
+
"error",
|
|
2977
|
+
"vault_sync_jobs_enabled",
|
|
2978
|
+
"Vault sync jobs enabled",
|
|
2979
|
+
"Scheduler check failed \u2014 run vault-sync-install"
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
const logFile = join15(logDir, "wiki-push.log");
|
|
2983
|
+
let c3;
|
|
2984
|
+
try {
|
|
2985
|
+
const logContent = readFileSync7(logFile, "utf8");
|
|
2986
|
+
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
2987
|
+
if (lines.length === 0) {
|
|
2988
|
+
c3 = check(
|
|
2989
|
+
"warn",
|
|
2990
|
+
"vault_sync_last_push_age",
|
|
2991
|
+
"Vault sync last push recency",
|
|
2992
|
+
"Log file is empty"
|
|
2993
|
+
);
|
|
2994
|
+
} else {
|
|
2995
|
+
const lastLine = [...lines].reverse().find((line) => /FAIL|OK push/.test(line)) ?? lines[lines.length - 1];
|
|
2996
|
+
if (/FAIL/.test(lastLine)) {
|
|
2997
|
+
c3 = check(
|
|
2998
|
+
"error",
|
|
2999
|
+
"vault_sync_last_push_age",
|
|
3000
|
+
"Vault sync last push recency",
|
|
3001
|
+
`Last push failed: ${lastLine}`
|
|
3002
|
+
);
|
|
3003
|
+
} else if (/OK push/.test(lastLine)) {
|
|
3004
|
+
const tsMatch = lastLine.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
|
|
3005
|
+
if (tsMatch) {
|
|
3006
|
+
const lastPush = new Date(tsMatch[1]).getTime();
|
|
3007
|
+
const ageSec = (Date.now() - lastPush) / 1e3;
|
|
3008
|
+
if (ageSec <= 180) {
|
|
3009
|
+
c3 = check(
|
|
3010
|
+
"pass",
|
|
3011
|
+
"vault_sync_last_push_age",
|
|
3012
|
+
"Vault sync last push recency",
|
|
3013
|
+
`Last push ${ageSec.toFixed(0)}s ago`
|
|
3014
|
+
);
|
|
3015
|
+
} else {
|
|
3016
|
+
c3 = check(
|
|
3017
|
+
"warn",
|
|
3018
|
+
"vault_sync_last_push_age",
|
|
3019
|
+
"Vault sync last push recency",
|
|
3020
|
+
`Last push ${Math.round(ageSec)}s ago (>3 min)`
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
} else {
|
|
3024
|
+
c3 = check(
|
|
3025
|
+
"warn",
|
|
3026
|
+
"vault_sync_last_push_age",
|
|
3027
|
+
"Vault sync last push recency",
|
|
3028
|
+
`Unparseable push line: ${lastLine.slice(0, 80)}`
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
} else {
|
|
3032
|
+
c3 = check(
|
|
3033
|
+
"warn",
|
|
3034
|
+
"vault_sync_last_push_age",
|
|
3035
|
+
"Vault sync last push recency",
|
|
3036
|
+
`Last log entry: ${lastLine.slice(0, 80)}`
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
} catch {
|
|
3041
|
+
c3 = existsSync12(logDir) ? check(
|
|
3042
|
+
"warn",
|
|
3043
|
+
"vault_sync_last_push_age",
|
|
3044
|
+
"Vault sync last push recency",
|
|
3045
|
+
`Log file not found at ${logFile}`
|
|
3046
|
+
) : check(
|
|
3047
|
+
"error",
|
|
3048
|
+
"vault_sync_last_push_age",
|
|
3049
|
+
"Vault sync last push recency",
|
|
3050
|
+
`Log directory not found at ${logDir}`
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
const fetchLogFile = join15(logDir, "wiki-fetch.log");
|
|
3054
|
+
let cFetch;
|
|
3055
|
+
try {
|
|
3056
|
+
const logContent = readFileSync7(fetchLogFile, "utf8");
|
|
3057
|
+
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
3058
|
+
if (lines.length === 0) {
|
|
3059
|
+
cFetch = check(
|
|
3060
|
+
"warn",
|
|
3061
|
+
"vault_sync_last_fetch_status",
|
|
3062
|
+
"Vault sync last fetch status",
|
|
3063
|
+
"Fetch log file is empty"
|
|
3064
|
+
);
|
|
3065
|
+
} else {
|
|
3066
|
+
const lastLine = lines[lines.length - 1];
|
|
3067
|
+
if (/fetch failed/i.test(lastLine)) {
|
|
3068
|
+
cFetch = check(
|
|
3069
|
+
"error",
|
|
3070
|
+
"vault_sync_last_fetch_status",
|
|
3071
|
+
"Vault sync last fetch status",
|
|
3072
|
+
`Last fetch failed: ${lastLine.slice(0, 100)}`
|
|
3073
|
+
);
|
|
3074
|
+
} else if (/OK/.test(lastLine)) {
|
|
3075
|
+
cFetch = check(
|
|
3076
|
+
"pass",
|
|
3077
|
+
"vault_sync_last_fetch_status",
|
|
3078
|
+
"Vault sync last fetch status",
|
|
3079
|
+
lastLine.slice(0, 100)
|
|
3080
|
+
);
|
|
3081
|
+
} else {
|
|
3082
|
+
cFetch = check(
|
|
3083
|
+
"warn",
|
|
3084
|
+
"vault_sync_last_fetch_status",
|
|
3085
|
+
"Vault sync last fetch status",
|
|
3086
|
+
`Last fetch log entry: ${lastLine.slice(0, 80)}`
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
} catch {
|
|
3091
|
+
cFetch = check(
|
|
3092
|
+
"warn",
|
|
3093
|
+
"vault_sync_last_fetch_status",
|
|
3094
|
+
"Vault sync last fetch status",
|
|
3095
|
+
`Fetch log not found at ${fetchLogFile}`
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
let c4;
|
|
3099
|
+
try {
|
|
3100
|
+
if (!existsSync12(filterPath)) {
|
|
3101
|
+
c4 = check(
|
|
3102
|
+
"error",
|
|
3103
|
+
"vault_sync_filter_present",
|
|
3104
|
+
"Vault sync filter file present",
|
|
3105
|
+
`Filter file not found at ${filterPath}`
|
|
3106
|
+
);
|
|
3107
|
+
} else {
|
|
3108
|
+
const content = readFileSync7(filterPath, "utf8");
|
|
3109
|
+
const requiredExcludes = [
|
|
3110
|
+
"remotely-save/data.json",
|
|
3111
|
+
".skillwiki/sync.lock",
|
|
3112
|
+
".skillwiki/memory/",
|
|
3113
|
+
".skillwiki/memory-topics.json",
|
|
3114
|
+
".claude/settings.local.json"
|
|
3115
|
+
];
|
|
3116
|
+
const missing = requiredExcludes.filter((ex) => !content.includes(ex));
|
|
3117
|
+
if (missing.length > 0) {
|
|
3118
|
+
c4 = check(
|
|
3119
|
+
"warn",
|
|
3120
|
+
"vault_sync_filter_present",
|
|
3121
|
+
"Vault sync filter file present",
|
|
3122
|
+
`Missing required excludes: ${missing.join(", ")}`
|
|
3123
|
+
);
|
|
3124
|
+
} else {
|
|
3125
|
+
c4 = check(
|
|
3126
|
+
"pass",
|
|
3127
|
+
"vault_sync_filter_present",
|
|
3128
|
+
"Vault sync filter file present",
|
|
3129
|
+
`Found with required excludes at ${filterPath}`
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
} catch {
|
|
3134
|
+
c4 = check(
|
|
3135
|
+
"error",
|
|
3136
|
+
"vault_sync_filter_present",
|
|
3137
|
+
"Vault sync filter file present",
|
|
3138
|
+
`Cannot read filter file at ${filterPath}`
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
let c5;
|
|
3142
|
+
if (input.vaultSyncRole !== "snapshotter") {
|
|
3143
|
+
c5 = check(
|
|
3144
|
+
"pass",
|
|
3145
|
+
"vault_sync_snapshot_guard",
|
|
3146
|
+
"Snapshot script guard",
|
|
3147
|
+
"Not a snapshotter host \u2014 check skipped"
|
|
3148
|
+
);
|
|
3149
|
+
} else {
|
|
3150
|
+
try {
|
|
3151
|
+
if (!existsSync12(snapshotPath)) {
|
|
3152
|
+
c5 = check(
|
|
3153
|
+
"error",
|
|
3154
|
+
"vault_sync_snapshot_guard",
|
|
3155
|
+
"Snapshot script guard",
|
|
3156
|
+
`Snapshot script not found at ${snapshotPath}`
|
|
3157
|
+
);
|
|
3158
|
+
} else {
|
|
3159
|
+
const content = readFileSync7(snapshotPath, "utf8");
|
|
3160
|
+
if (!content.includes("--max-delete")) {
|
|
3161
|
+
c5 = check(
|
|
3162
|
+
"error",
|
|
3163
|
+
"vault_sync_snapshot_guard",
|
|
3164
|
+
"Snapshot script guard",
|
|
3165
|
+
`${snapshotPath} is missing --max-delete guard \u2014 dangerous without it`
|
|
3166
|
+
);
|
|
3167
|
+
} else {
|
|
3168
|
+
c5 = check(
|
|
2836
3169
|
"pass",
|
|
2837
3170
|
"vault_sync_snapshot_guard",
|
|
2838
3171
|
"Snapshot script guard",
|
|
@@ -2841,464 +3174,643 @@ function vaultSyncChecks(input) {
|
|
|
2841
3174
|
}
|
|
2842
3175
|
}
|
|
2843
3176
|
} catch {
|
|
2844
|
-
|
|
3177
|
+
c5 = check(
|
|
2845
3178
|
"error",
|
|
2846
3179
|
"vault_sync_snapshot_guard",
|
|
2847
3180
|
"Snapshot script guard",
|
|
2848
3181
|
`Cannot read ${snapshotPath}`
|
|
2849
3182
|
);
|
|
2850
3183
|
}
|
|
2851
|
-
return [c12, ...healthChecks, cFetch2, c42, c52];
|
|
2852
3184
|
}
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3185
|
+
return [c1, c2, c3, cFetch, c4, c5];
|
|
3186
|
+
}
|
|
3187
|
+
function checkVaultSyncPullHelper(home, env) {
|
|
3188
|
+
const path = resolveVaultSyncPullHelper({
|
|
3189
|
+
vault: "",
|
|
3190
|
+
home,
|
|
3191
|
+
env
|
|
3192
|
+
});
|
|
3193
|
+
if (path) {
|
|
3194
|
+
return check("pass", "vault_sync_pull_helper", "Vault-sync pull helper", `Resolved: ${path}`);
|
|
3195
|
+
}
|
|
3196
|
+
return check(
|
|
3197
|
+
"error",
|
|
3198
|
+
"vault_sync_pull_helper",
|
|
3199
|
+
"Vault-sync pull helper",
|
|
3200
|
+
"Not found \u2014 install skillwiki@0.10.1+, redeploy vault-sync, or set SKILLWIKI_VAULT_SYNC_PULL_HELPER"
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
function checkVaultSyncReviewRequiredJournals(vaultPath) {
|
|
3204
|
+
if (!vaultPath || !existsSync12(join15(vaultPath, ".git"))) {
|
|
3205
|
+
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
|
|
3206
|
+
}
|
|
2856
3207
|
try {
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3208
|
+
const ops = listReviewRequiredOps(vaultPath);
|
|
3209
|
+
if (ops.length === 0) {
|
|
3210
|
+
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "None");
|
|
3211
|
+
}
|
|
3212
|
+
const sample = ops[0]?.opId ?? "?";
|
|
3213
|
+
return check(
|
|
3214
|
+
"warn",
|
|
3215
|
+
"vault_sync_review_required_journals",
|
|
3216
|
+
"Review-required journals",
|
|
3217
|
+
`${ops.length} handoff(s); oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
|
|
3218
|
+
);
|
|
3219
|
+
} catch {
|
|
3220
|
+
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
var vaultSyncProbe = {
|
|
3224
|
+
id: "vault_sync",
|
|
3225
|
+
run(ctx) {
|
|
3226
|
+
const checks = [];
|
|
3227
|
+
checks.push(...vaultSyncChecks({
|
|
3228
|
+
home: ctx.input.home,
|
|
3229
|
+
vaultSyncInstalled: ctx.vsConfig.installed,
|
|
3230
|
+
vaultSyncRole: ctx.vsConfig.role,
|
|
3231
|
+
vaultSyncServiceScope: ctx.vsConfig.serviceScope,
|
|
3232
|
+
snapshotScriptPath: ctx.vsConfig.snapshotScript,
|
|
3233
|
+
env: ctx.input.env ?? process.env
|
|
3234
|
+
}));
|
|
3235
|
+
checks.push(checkVaultSyncPullHelper(ctx.input.home, ctx.input.env ?? process.env));
|
|
3236
|
+
checks.push(checkVaultSyncReviewRequiredJournals(ctx.resolvedPath));
|
|
3237
|
+
return checks;
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
// src/doctor/probes/satellite.ts
|
|
3242
|
+
import { existsSync as existsSync14 } from "fs";
|
|
3243
|
+
import { execSync as execSync6 } from "child_process";
|
|
3244
|
+
import { platform as platform4 } from "os";
|
|
3245
|
+
|
|
3246
|
+
// src/utils/satellite-run-health.ts
|
|
3247
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
|
|
3248
|
+
import { join as join16 } from "path";
|
|
3249
|
+
var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
|
|
3250
|
+
function satelliteLatestRunPath(vault) {
|
|
3251
|
+
return join16(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
|
|
3252
|
+
}
|
|
3253
|
+
function isFailedRunStatus(status) {
|
|
3254
|
+
return status === "fail" || status === "failure";
|
|
3255
|
+
}
|
|
3256
|
+
function parseLatestRunFile(text) {
|
|
3257
|
+
try {
|
|
3258
|
+
const parsed = JSON.parse(text);
|
|
3259
|
+
const status = typeof parsed.status === "string" ? parsed.status : "";
|
|
3260
|
+
if (!status) return null;
|
|
3261
|
+
const finishedAt = typeof parsed.finished_at === "string" && parsed.finished_at.length > 0 ? parsed.finished_at : void 0;
|
|
3262
|
+
const failureClass = parsed.failure_class != null && String(parsed.failure_class).length > 0 ? String(parsed.failure_class) : void 0;
|
|
3263
|
+
return { status, finishedAt, failureClass };
|
|
3264
|
+
} catch {
|
|
3265
|
+
return null;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
function readSatelliteLatestRunFromText(text) {
|
|
3269
|
+
return parseLatestRunFile(text);
|
|
3270
|
+
}
|
|
3271
|
+
function readSatelliteLatestRun(vault) {
|
|
3272
|
+
const latestPath = satelliteLatestRunPath(vault);
|
|
3273
|
+
if (!existsSync13(latestPath)) return null;
|
|
3274
|
+
try {
|
|
3275
|
+
return parseLatestRunFile(readFileSync8(latestPath, "utf8"));
|
|
3276
|
+
} catch {
|
|
3277
|
+
return null;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
function evaluateSatelliteRunHealth(vault, now) {
|
|
3281
|
+
const run = readSatelliteLatestRun(vault);
|
|
3282
|
+
if (!run) {
|
|
3283
|
+
return { failed: false, stale: false };
|
|
3284
|
+
}
|
|
3285
|
+
const failed = isFailedRunStatus(run.status);
|
|
3286
|
+
let stale = false;
|
|
3287
|
+
if (!failed && run.finishedAt) {
|
|
3288
|
+
const ts = Date.parse(run.finishedAt);
|
|
3289
|
+
if (Number.isFinite(ts) && now.getTime() - ts > SATELLITE_STALE_MS) {
|
|
3290
|
+
stale = true;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
return {
|
|
3294
|
+
failed,
|
|
3295
|
+
stale,
|
|
3296
|
+
failureClass: run.failureClass,
|
|
3297
|
+
finishedAt: run.finishedAt
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
// src/doctor/probes/satellite.ts
|
|
3302
|
+
function checkSatelliteLastRun(vaultPath, satelliteExpected) {
|
|
3303
|
+
if (!satelliteExpected) {
|
|
3304
|
+
return check("pass", "satellite_job_last_run", "Satellite job last run", "Satellite job not expected on this host");
|
|
3305
|
+
}
|
|
3306
|
+
if (vaultPath === void 0) {
|
|
3307
|
+
return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
|
|
3308
|
+
}
|
|
3309
|
+
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
3310
|
+
if (!existsSync14(latestPath)) {
|
|
3311
|
+
return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
|
|
3312
|
+
}
|
|
3313
|
+
try {
|
|
3314
|
+
const health = evaluateSatelliteRunHealth(vaultPath, /* @__PURE__ */ new Date());
|
|
3315
|
+
if (health.failed) {
|
|
3316
|
+
const fc = health.failureClass;
|
|
3317
|
+
const detail = fc ? `Last satellite run failed (failure_class: ${fc})` : "Last satellite run failed";
|
|
3318
|
+
return check("error", "satellite_job_last_run", "Satellite job last run", detail);
|
|
3319
|
+
}
|
|
3320
|
+
if (health.stale && health.finishedAt) {
|
|
3321
|
+
return check(
|
|
3322
|
+
"warn",
|
|
3323
|
+
"satellite_job_last_run",
|
|
3324
|
+
"Satellite job last run",
|
|
3325
|
+
`Last run finished_at is older than 26h (${health.finishedAt})`
|
|
2874
3326
|
);
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
3327
|
+
}
|
|
3328
|
+
return check(
|
|
3329
|
+
"pass",
|
|
3330
|
+
"satellite_job_last_run",
|
|
3331
|
+
"Satellite job last run",
|
|
3332
|
+
health.finishedAt ? `Last run ok (finished_at ${health.finishedAt})` : "Last run ok"
|
|
3333
|
+
);
|
|
3334
|
+
} catch {
|
|
3335
|
+
return check("warn", "satellite_job_last_run", "Satellite job last run", `Could not read ${latestPath}`);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
function defaultSatelliteTimerDeps() {
|
|
3339
|
+
return {
|
|
3340
|
+
platform: () => platform4(),
|
|
3341
|
+
systemctlIsActive: (unit) => {
|
|
3342
|
+
try {
|
|
3343
|
+
return execSync6(`systemctl is-active ${unit}`, {
|
|
3344
|
+
encoding: "utf8",
|
|
3345
|
+
timeout: 2e3,
|
|
3346
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3347
|
+
}).trim();
|
|
3348
|
+
} catch {
|
|
3349
|
+
return void 0;
|
|
2895
3350
|
}
|
|
2896
3351
|
}
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
function checkSatelliteTimer(satelliteExpected, deps = defaultSatelliteTimerDeps()) {
|
|
3355
|
+
if (!satelliteExpected) {
|
|
3356
|
+
return check("pass", "satellite_job_timer", "Satellite job timer", "Satellite job not expected on this host");
|
|
3357
|
+
}
|
|
3358
|
+
if (deps.platform() !== "linux") {
|
|
3359
|
+
return check("pass", "satellite_job_timer", "Satellite job timer", "Timer check skipped \u2014 Linux only");
|
|
3360
|
+
}
|
|
3361
|
+
const out = deps.systemctlIsActive("agent-memory-trends.timer");
|
|
3362
|
+
if (out === void 0) {
|
|
3363
|
+
return check("pass", "satellite_job_timer", "Satellite job timer", "systemctl unavailable");
|
|
3364
|
+
}
|
|
3365
|
+
if (out === "active") {
|
|
3366
|
+
return check("pass", "satellite_job_timer", "Satellite job timer", "systemd: agent-memory-trends.timer active");
|
|
3367
|
+
}
|
|
3368
|
+
return check(
|
|
3369
|
+
"error",
|
|
3370
|
+
"satellite_job_timer",
|
|
3371
|
+
"Satellite job timer",
|
|
3372
|
+
`systemd: agent-memory-trends.timer is ${out || "not active"}`
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
var satelliteProbe = {
|
|
3376
|
+
id: "satellite",
|
|
3377
|
+
run(ctx) {
|
|
3378
|
+
return [
|
|
3379
|
+
checkSatelliteLastRun(ctx.resolvedPath, ctx.satelliteGate.satelliteExpected),
|
|
3380
|
+
checkSatelliteTimer(ctx.satelliteGate.satelliteExpected)
|
|
3381
|
+
];
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
|
|
3385
|
+
// src/doctor/probes/metrics.ts
|
|
3386
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3387
|
+
import { join as join17 } from "path";
|
|
3388
|
+
var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
3389
|
+
function doctorReadOnlyScanRoot(resolvedPath) {
|
|
3390
|
+
return resolveReadOnlyVaultRoot(resolvedPath).root;
|
|
3391
|
+
}
|
|
3392
|
+
async function vaultMetrics(resolvedPath) {
|
|
3393
|
+
const ids = [
|
|
3394
|
+
["vault_metric_pages", "Vault pages by type"],
|
|
3395
|
+
["vault_metric_orphans", "Vault orphan rate"],
|
|
3396
|
+
["vault_metric_bridges", "Vault bridge count"],
|
|
3397
|
+
["vault_metric_cohesion", "Mean community cohesion"],
|
|
3398
|
+
["vault_metric_log_size", "Vault log size"]
|
|
3399
|
+
];
|
|
3400
|
+
const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
|
|
3401
|
+
if (!resolvedPath) return noVault();
|
|
3402
|
+
const scanRoot = doctorReadOnlyScanRoot(resolvedPath);
|
|
3403
|
+
const scan = await scanVault(scanRoot);
|
|
3404
|
+
if (!scan.ok) return noVault();
|
|
3405
|
+
const tk = scan.data.typedKnowledge;
|
|
3406
|
+
const typedCount = tk.length;
|
|
3407
|
+
const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
|
|
3408
|
+
const adj = await buildWikilinkAdjacency(tk, void 0, scan.data.allMarkdown);
|
|
3409
|
+
const g = toUndirectedWeighted(adj);
|
|
3410
|
+
const nodes = [...g.keys()];
|
|
3411
|
+
const total = nodes.length;
|
|
3412
|
+
const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
|
|
3413
|
+
const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
|
|
3414
|
+
const comm = louvain(g);
|
|
3415
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3416
|
+
for (const [node, c] of comm) {
|
|
3417
|
+
const arr = groups.get(c);
|
|
3418
|
+
if (arr) arr.push(node);
|
|
3419
|
+
else groups.set(c, [node]);
|
|
3420
|
+
}
|
|
3421
|
+
const cohesions = [...groups.values()].filter((m) => m.length >= 2).map((m) => communityCohesion(m, g));
|
|
3422
|
+
const meanCohesion = cohesions.length > 0 ? Math.round(cohesions.reduce((a, b) => a + b, 0) / cohesions.length * 1e3) / 1e3 : 0;
|
|
3423
|
+
let bridges = 0;
|
|
3424
|
+
for (const n of nodes) {
|
|
3425
|
+
const nbrComms = /* @__PURE__ */ new Set();
|
|
3426
|
+
for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
|
|
3427
|
+
if (nbrComms.size >= 3) bridges++;
|
|
3428
|
+
}
|
|
3429
|
+
let logLines = 0;
|
|
3430
|
+
try {
|
|
3431
|
+
logLines = readFileSync9(join17(scanRoot, "log.md"), "utf8").split("\n").length;
|
|
2897
3432
|
} catch {
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
3433
|
+
}
|
|
3434
|
+
return [
|
|
3435
|
+
check("info", "vault_metric_pages", "Vault pages by type", `${total} graph node(s) (${typedCount} typed; ${perType})`),
|
|
3436
|
+
check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
|
|
3437
|
+
check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
|
|
3438
|
+
check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
|
|
3439
|
+
check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
|
|
3440
|
+
];
|
|
3441
|
+
}
|
|
3442
|
+
var metricsProbe = {
|
|
3443
|
+
id: "metrics",
|
|
3444
|
+
async run(ctx) {
|
|
3445
|
+
return await vaultMetrics(ctx.resolvedPath);
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
|
|
3449
|
+
// src/doctor/probes/fuse-staleness.ts
|
|
3450
|
+
import { platform as platform5 } from "os";
|
|
3451
|
+
var MAX_DIR_CACHE_TIME_SECONDS2 = 15 * 60;
|
|
3452
|
+
function defaultFuseStalenessDeps() {
|
|
3453
|
+
return {
|
|
3454
|
+
platform: () => platform5(),
|
|
3455
|
+
detectFuseMount: (vp) => detectFuseMount(vp),
|
|
3456
|
+
findRcloneMountPid: () => findRcloneMountPid(),
|
|
3457
|
+
parseRcloneFlags: (pid) => parseRcloneFlags(pid),
|
|
3458
|
+
getRcloneArgs: (pid) => getRcloneArgs(pid),
|
|
3459
|
+
queryRcloneRC: (rcAddr, fs) => queryRcloneRC(rcAddr, fs)
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
function formatDurationForHumans2(seconds) {
|
|
3463
|
+
if (!Number.isFinite(seconds)) return `${seconds}s`;
|
|
3464
|
+
if (seconds >= 3600) return `${(seconds / 3600).toFixed(1)}h`;
|
|
3465
|
+
if (seconds >= 60) return `${(seconds / 60).toFixed(1)}m`;
|
|
3466
|
+
if (seconds >= 1) return `${seconds.toFixed(1)}s`;
|
|
3467
|
+
return `${Math.round(seconds * 1e3)}ms`;
|
|
3468
|
+
}
|
|
3469
|
+
function checkFuseStaleness(resolvedPath, deps = {}) {
|
|
3470
|
+
const resolvedDeps = { ...defaultFuseStalenessDeps(), ...deps };
|
|
3471
|
+
const os = resolvedDeps.platform();
|
|
3472
|
+
if (os !== "linux") {
|
|
3473
|
+
return check(
|
|
3474
|
+
"pass",
|
|
3475
|
+
"fuse_staleness",
|
|
3476
|
+
"FUSE visibility freshness",
|
|
3477
|
+
`Non-Linux host (${os}) \u2014 check skipped`
|
|
2903
3478
|
);
|
|
2904
3479
|
}
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
);
|
|
2926
|
-
} else if (/OK push/.test(lastLine)) {
|
|
2927
|
-
const tsMatch = lastLine.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
|
|
2928
|
-
if (tsMatch) {
|
|
2929
|
-
const lastPush = new Date(tsMatch[1]).getTime();
|
|
2930
|
-
const ageSec = (Date.now() - lastPush) / 1e3;
|
|
2931
|
-
if (ageSec <= 180) {
|
|
2932
|
-
c3 = check(
|
|
2933
|
-
"pass",
|
|
2934
|
-
"vault_sync_last_push_age",
|
|
2935
|
-
"Vault sync last push recency",
|
|
2936
|
-
`Last push ${ageSec.toFixed(0)}s ago`
|
|
2937
|
-
);
|
|
2938
|
-
} else {
|
|
2939
|
-
c3 = check(
|
|
2940
|
-
"warn",
|
|
2941
|
-
"vault_sync_last_push_age",
|
|
2942
|
-
"Vault sync last push recency",
|
|
2943
|
-
`Last push ${Math.round(ageSec)}s ago (>3 min)`
|
|
2944
|
-
);
|
|
2945
|
-
}
|
|
2946
|
-
} else {
|
|
2947
|
-
c3 = check(
|
|
2948
|
-
"warn",
|
|
2949
|
-
"vault_sync_last_push_age",
|
|
2950
|
-
"Vault sync last push recency",
|
|
2951
|
-
`Unparseable push line: ${lastLine.slice(0, 80)}`
|
|
2952
|
-
);
|
|
2953
|
-
}
|
|
2954
|
-
} else {
|
|
2955
|
-
c3 = check(
|
|
2956
|
-
"warn",
|
|
2957
|
-
"vault_sync_last_push_age",
|
|
2958
|
-
"Vault sync last push recency",
|
|
2959
|
-
`Last log entry: ${lastLine.slice(0, 80)}`
|
|
2960
|
-
);
|
|
2961
|
-
}
|
|
2962
|
-
}
|
|
2963
|
-
} catch {
|
|
2964
|
-
c3 = existsSync7(logDir) ? check(
|
|
3480
|
+
if (!resolvedPath) {
|
|
3481
|
+
return check(
|
|
3482
|
+
"pass",
|
|
3483
|
+
"fuse_staleness",
|
|
3484
|
+
"FUSE visibility freshness",
|
|
3485
|
+
"No vault path \u2014 check skipped"
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
3488
|
+
const fuse = resolvedDeps.detectFuseMount(resolvedPath);
|
|
3489
|
+
if (!fuse) {
|
|
3490
|
+
return check(
|
|
3491
|
+
"pass",
|
|
3492
|
+
"fuse_staleness",
|
|
3493
|
+
"FUSE visibility freshness",
|
|
3494
|
+
"local disk (non-FUSE) \u2014 check skipped"
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
const pid = resolvedDeps.findRcloneMountPid();
|
|
3498
|
+
if (pid === null) {
|
|
3499
|
+
return check(
|
|
2965
3500
|
"warn",
|
|
2966
|
-
"
|
|
2967
|
-
"
|
|
2968
|
-
`
|
|
2969
|
-
) : check(
|
|
2970
|
-
"error",
|
|
2971
|
-
"vault_sync_last_push_age",
|
|
2972
|
-
"Vault sync last push recency",
|
|
2973
|
-
`Log directory not found at ${logDir}`
|
|
3501
|
+
"fuse_staleness",
|
|
3502
|
+
"FUSE visibility freshness",
|
|
3503
|
+
`S3 FUSE mount (${fuse.mountPoint}) but no rclone process found \u2014 cannot audit dir-cache freshness`
|
|
2974
3504
|
);
|
|
2975
3505
|
}
|
|
2976
|
-
const
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3506
|
+
const flags = resolvedDeps.parseRcloneFlags(pid);
|
|
3507
|
+
const rawDirCache = flags.get("--dir-cache-time");
|
|
3508
|
+
let dirCachePassed = true;
|
|
3509
|
+
let dirCacheDetail = "";
|
|
3510
|
+
if (!rawDirCache) {
|
|
3511
|
+
dirCacheDetail = "PID " + pid + ": --dir-cache-time not set (rclone default 5m, within <=15m SLA)";
|
|
3512
|
+
} else {
|
|
3513
|
+
const seconds = parseDurationSeconds(rawDirCache);
|
|
3514
|
+
if (seconds === null) {
|
|
3515
|
+
return check(
|
|
2983
3516
|
"warn",
|
|
2984
|
-
"
|
|
2985
|
-
"
|
|
2986
|
-
|
|
3517
|
+
"fuse_staleness",
|
|
3518
|
+
"FUSE visibility freshness",
|
|
3519
|
+
`PID ${pid}: could not parse --dir-cache-time=${rawDirCache}`
|
|
2987
3520
|
);
|
|
3521
|
+
}
|
|
3522
|
+
if (seconds > MAX_DIR_CACHE_TIME_SECONDS2) {
|
|
3523
|
+
dirCachePassed = false;
|
|
3524
|
+
dirCacheDetail = `PID ${pid}: --dir-cache-time=${rawDirCache} (${formatDurationForHumans2(seconds)}) exceeds 15m SLA \u2014 external changes may remain invisible`;
|
|
2988
3525
|
} else {
|
|
2989
|
-
|
|
2990
|
-
if (/fetch failed/i.test(lastLine)) {
|
|
2991
|
-
cFetch = check(
|
|
2992
|
-
"error",
|
|
2993
|
-
"vault_sync_last_fetch_status",
|
|
2994
|
-
"Vault sync last fetch status",
|
|
2995
|
-
`Last fetch failed: ${lastLine.slice(0, 100)}`
|
|
2996
|
-
);
|
|
2997
|
-
} else if (/OK/.test(lastLine)) {
|
|
2998
|
-
cFetch = check(
|
|
2999
|
-
"pass",
|
|
3000
|
-
"vault_sync_last_fetch_status",
|
|
3001
|
-
"Vault sync last fetch status",
|
|
3002
|
-
lastLine.slice(0, 100)
|
|
3003
|
-
);
|
|
3004
|
-
} else {
|
|
3005
|
-
cFetch = check(
|
|
3006
|
-
"warn",
|
|
3007
|
-
"vault_sync_last_fetch_status",
|
|
3008
|
-
"Vault sync last fetch status",
|
|
3009
|
-
`Last fetch log entry: ${lastLine.slice(0, 80)}`
|
|
3010
|
-
);
|
|
3011
|
-
}
|
|
3526
|
+
dirCacheDetail = `PID ${pid}: --dir-cache-time=${rawDirCache} (${formatDurationForHumans2(seconds)}), within <=15m SLA`;
|
|
3012
3527
|
}
|
|
3013
|
-
} catch {
|
|
3014
|
-
cFetch = check(
|
|
3015
|
-
"warn",
|
|
3016
|
-
"vault_sync_last_fetch_status",
|
|
3017
|
-
"Vault sync last fetch status",
|
|
3018
|
-
`Fetch log not found at ${fetchLogFile}`
|
|
3019
|
-
);
|
|
3020
3528
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
if (missing.length > 0) {
|
|
3041
|
-
c4 = check(
|
|
3529
|
+
if (!dirCachePassed) {
|
|
3530
|
+
return check("warn", "fuse_staleness", "FUSE visibility freshness", dirCacheDetail);
|
|
3531
|
+
}
|
|
3532
|
+
if (flags.has("--rc")) {
|
|
3533
|
+
const rcAddr = flags.get("--rc-addr") || "127.0.0.1:5572";
|
|
3534
|
+
const args = resolvedDeps.getRcloneArgs(pid);
|
|
3535
|
+
const fs = extractRcloneFs(args) || "unknown:";
|
|
3536
|
+
const stats = resolvedDeps.queryRcloneRC(rcAddr, fs);
|
|
3537
|
+
if (stats) {
|
|
3538
|
+
if (stats.error) {
|
|
3539
|
+
return check("warn", "fuse_staleness", "FUSE visibility freshness", `${dirCacheDetail}; RC query error: ${stats.error}`);
|
|
3540
|
+
}
|
|
3541
|
+
const issues = [];
|
|
3542
|
+
if (stats.uploadsInProgress > 0) issues.push(`${stats.uploadsInProgress} upload(s) in progress`);
|
|
3543
|
+
if (stats.uploadsQueued > 10) issues.push(`${stats.uploadsQueued} upload(s) queued (backlog)`);
|
|
3544
|
+
if (stats.erroredFiles > 0) issues.push(`${stats.erroredFiles} errored file(s)`);
|
|
3545
|
+
if (stats.outOfSpace) issues.push("cache disk full");
|
|
3546
|
+
if (issues.length > 0) {
|
|
3547
|
+
return check(
|
|
3042
3548
|
"warn",
|
|
3043
|
-
"
|
|
3044
|
-
"
|
|
3045
|
-
|
|
3046
|
-
);
|
|
3047
|
-
} else {
|
|
3048
|
-
c4 = check(
|
|
3049
|
-
"pass",
|
|
3050
|
-
"vault_sync_filter_present",
|
|
3051
|
-
"Vault sync filter file present",
|
|
3052
|
-
`Found with required excludes at ${filterPath}`
|
|
3549
|
+
"fuse_staleness",
|
|
3550
|
+
"FUSE visibility freshness",
|
|
3551
|
+
`${dirCacheDetail}; VFS cache degradation: ${issues.join(", ")}`
|
|
3053
3552
|
);
|
|
3054
3553
|
}
|
|
3055
3554
|
}
|
|
3555
|
+
}
|
|
3556
|
+
return check("pass", "fuse_staleness", "FUSE visibility freshness", dirCacheDetail);
|
|
3557
|
+
}
|
|
3558
|
+
var fuseStalenessProbe = {
|
|
3559
|
+
id: "fuse_staleness",
|
|
3560
|
+
run(ctx) {
|
|
3561
|
+
return [checkFuseStaleness(ctx.resolvedPath)];
|
|
3562
|
+
}
|
|
3563
|
+
};
|
|
3564
|
+
|
|
3565
|
+
// src/doctor/probes/activation-marker.ts
|
|
3566
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
|
|
3567
|
+
import { join as join18 } from "path";
|
|
3568
|
+
var GROK_ACTIVATION_REFERENCE2 = "Read @~/.grok/skillwiki.md for SkillWiki activation context.";
|
|
3569
|
+
var STALE_GROK_ACTIVATION_REFERENCE2 = "Read @skillwiki.md";
|
|
3570
|
+
var ACTIVATION_FIX_HINT2 = "run `npm run install:activation` from the llm-wiki repo";
|
|
3571
|
+
function findGrokActivationTemplate2(home, cwd) {
|
|
3572
|
+
if (cwd) {
|
|
3573
|
+
const src = join18(cwd, "packages", "skills", "using-skillwiki", "activation.md");
|
|
3574
|
+
if (existsSync15(src)) return src;
|
|
3575
|
+
const directSrc = join18(cwd, "skills", "using-skillwiki", "activation.md");
|
|
3576
|
+
if (existsSync15(directSrc)) return directSrc;
|
|
3577
|
+
}
|
|
3578
|
+
const pluginsRoot = join18(home, ".grok", "installed-plugins");
|
|
3579
|
+
if (!existsSync15(pluginsRoot)) return void 0;
|
|
3580
|
+
let entries;
|
|
3581
|
+
try {
|
|
3582
|
+
entries = readdirSync5(pluginsRoot, { withFileTypes: true });
|
|
3056
3583
|
} catch {
|
|
3057
|
-
|
|
3058
|
-
"error",
|
|
3059
|
-
"vault_sync_filter_present",
|
|
3060
|
-
"Vault sync filter file present",
|
|
3061
|
-
`Cannot read filter file at ${filterPath}`
|
|
3062
|
-
);
|
|
3584
|
+
return void 0;
|
|
3063
3585
|
}
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3586
|
+
for (const entry of entries) {
|
|
3587
|
+
if (!entry.isDirectory()) continue;
|
|
3588
|
+
const root = join18(pluginsRoot, entry.name);
|
|
3589
|
+
for (const rel of ["using-skillwiki/activation.md", "skills/using-skillwiki/activation.md"]) {
|
|
3590
|
+
const candidate = join18(root, rel);
|
|
3591
|
+
if (existsSync15(candidate)) return candidate;
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
return void 0;
|
|
3595
|
+
}
|
|
3596
|
+
function checkActivationMarker(home, cwd) {
|
|
3597
|
+
const grokDir = join18(home, ".grok");
|
|
3598
|
+
if (!existsSync15(grokDir)) {
|
|
3599
|
+
return check("pass", "activation_marker", "Activation marker", "Not a Grok host \u2014 check skipped");
|
|
3600
|
+
}
|
|
3601
|
+
const activationPath = join18(grokDir, "skillwiki.md");
|
|
3602
|
+
const agentsPath = join18(grokDir, "AGENTS.md");
|
|
3603
|
+
const hasActivation = existsSync15(activationPath);
|
|
3604
|
+
const issues = [];
|
|
3605
|
+
if (!hasActivation) {
|
|
3606
|
+
issues.push("~/.grok/skillwiki.md missing");
|
|
3607
|
+
}
|
|
3608
|
+
if (!existsSync15(agentsPath)) {
|
|
3609
|
+
issues.push("~/.grok/AGENTS.md missing");
|
|
3072
3610
|
} else {
|
|
3073
3611
|
try {
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
c5 = check(
|
|
3085
|
-
"error",
|
|
3086
|
-
"vault_sync_snapshot_guard",
|
|
3087
|
-
"Snapshot script guard",
|
|
3088
|
-
`${snapshotPath} is missing --max-delete guard \u2014 dangerous without it`
|
|
3089
|
-
);
|
|
3090
|
-
} else {
|
|
3091
|
-
c5 = check(
|
|
3092
|
-
"pass",
|
|
3093
|
-
"vault_sync_snapshot_guard",
|
|
3094
|
-
"Snapshot script guard",
|
|
3095
|
-
`--max-delete present in ${snapshotPath}`
|
|
3096
|
-
);
|
|
3097
|
-
}
|
|
3612
|
+
const agents = readFileSync10(agentsPath, "utf8");
|
|
3613
|
+
const hasBegin = agents.includes("<!-- skillwiki:begin -->");
|
|
3614
|
+
const hasExpected = agents.includes(GROK_ACTIVATION_REFERENCE2);
|
|
3615
|
+
const hasStale = agents.includes(STALE_GROK_ACTIVATION_REFERENCE2);
|
|
3616
|
+
if (!hasBegin) {
|
|
3617
|
+
issues.push("AGENTS.md marker missing");
|
|
3618
|
+
} else if (hasStale && !hasExpected) {
|
|
3619
|
+
issues.push("AGENTS.md marker is stale (@skillwiki.md)");
|
|
3620
|
+
} else if (!hasExpected) {
|
|
3621
|
+
issues.push("AGENTS.md marker is stale");
|
|
3098
3622
|
}
|
|
3099
3623
|
} catch {
|
|
3100
|
-
|
|
3101
|
-
"error",
|
|
3102
|
-
"vault_sync_snapshot_guard",
|
|
3103
|
-
"Snapshot script guard",
|
|
3104
|
-
`Cannot read ${snapshotPath}`
|
|
3105
|
-
);
|
|
3624
|
+
issues.push("could not read ~/.grok/AGENTS.md");
|
|
3106
3625
|
}
|
|
3107
3626
|
}
|
|
3108
|
-
|
|
3627
|
+
if (hasActivation) {
|
|
3628
|
+
const template = findGrokActivationTemplate2(home, cwd);
|
|
3629
|
+
if (template) {
|
|
3630
|
+
try {
|
|
3631
|
+
const installed = readFileSync10(activationPath, "utf8");
|
|
3632
|
+
const expected = readFileSync10(template, "utf8");
|
|
3633
|
+
if (installed !== expected) {
|
|
3634
|
+
issues.push("~/.grok/skillwiki.md differs from template");
|
|
3635
|
+
}
|
|
3636
|
+
} catch {
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
if (issues.length > 0) {
|
|
3641
|
+
return check(
|
|
3642
|
+
"warn",
|
|
3643
|
+
"activation_marker",
|
|
3644
|
+
"Activation marker",
|
|
3645
|
+
`${issues.join("; ")} \u2014 ${ACTIVATION_FIX_HINT2}`
|
|
3646
|
+
);
|
|
3647
|
+
}
|
|
3648
|
+
return check("pass", "activation_marker", "Activation marker", "Marker and compact file match home-path contract");
|
|
3109
3649
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3650
|
+
var activationMarkerProbe = {
|
|
3651
|
+
id: "activation_marker",
|
|
3652
|
+
run(ctx) {
|
|
3653
|
+
return [checkActivationMarker(ctx.input.home, ctx.input.cwd)];
|
|
3654
|
+
}
|
|
3655
|
+
};
|
|
3656
|
+
|
|
3657
|
+
// src/doctor/probes/ds-store-noise.ts
|
|
3658
|
+
import { existsSync as existsSync16, readdirSync as readdirSync6 } from "fs";
|
|
3659
|
+
import { join as join19 } from "path";
|
|
3660
|
+
var VAULT_TRACKED_DIRS = ["raw", "entities", "concepts", "comparisons", "queries", "meta", "_archive", "_Templates"];
|
|
3661
|
+
function checkDsStoreNoise(resolvedPath) {
|
|
3662
|
+
if (resolvedPath === void 0) {
|
|
3663
|
+
return check("pass", "ds_store_noise", "No .DS_Store noise", "No vault path \u2014 check skipped");
|
|
3664
|
+
}
|
|
3665
|
+
if (!existsSync16(resolvedPath)) {
|
|
3666
|
+
return check("pass", "ds_store_noise", "No .DS_Store noise", "Vault directory does not exist \u2014 check skipped");
|
|
3667
|
+
}
|
|
3668
|
+
const found = [];
|
|
3669
|
+
function walk(dir, rel) {
|
|
3670
|
+
let entries;
|
|
3671
|
+
try {
|
|
3672
|
+
entries = readdirSync6(dir, { withFileTypes: true });
|
|
3673
|
+
} catch {
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
for (const entry of entries) {
|
|
3677
|
+
if (entry.name === ".DS_Store") {
|
|
3678
|
+
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
3679
|
+
} else if (entry.isDirectory()) {
|
|
3680
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
3681
|
+
walk(join19(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3117
3684
|
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3685
|
+
let scannedAny = false;
|
|
3686
|
+
for (const sub of VAULT_TRACKED_DIRS) {
|
|
3687
|
+
const subDir = join19(resolvedPath, sub);
|
|
3688
|
+
if (existsSync16(subDir)) {
|
|
3689
|
+
scannedAny = true;
|
|
3690
|
+
walk(subDir, sub);
|
|
3123
3691
|
}
|
|
3124
3692
|
}
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
function findInstalledSkillMd(dir) {
|
|
3128
|
-
const directSkills = findSkillNames(dir).map((name) => join10(dir, name, "SKILL.md"));
|
|
3129
|
-
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
3130
|
-
}
|
|
3131
|
-
function findSkillNames(dir) {
|
|
3132
|
-
const results = [];
|
|
3133
|
-
let entries;
|
|
3134
|
-
try {
|
|
3135
|
-
entries = readdirSync3(dir, { withFileTypes: true });
|
|
3136
|
-
} catch {
|
|
3137
|
-
return results;
|
|
3693
|
+
if (!scannedAny) {
|
|
3694
|
+
walk(resolvedPath, "");
|
|
3138
3695
|
}
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
results.push(entry.name);
|
|
3142
|
-
}
|
|
3696
|
+
if (found.length === 0) {
|
|
3697
|
+
return check("pass", "ds_store_noise", "No .DS_Store noise", "No .DS_Store files found in vault");
|
|
3143
3698
|
}
|
|
3144
|
-
|
|
3699
|
+
const examples = found.slice(0, 3).join(", ");
|
|
3700
|
+
const more = found.length > 3 ? ", \u2026" : "";
|
|
3701
|
+
return check(
|
|
3702
|
+
"warn",
|
|
3703
|
+
"ds_store_noise",
|
|
3704
|
+
"No .DS_Store noise",
|
|
3705
|
+
`${found.length} .DS_Store file(s) found (${examples}${more}) \u2014 remove with: find ${resolvedPath} -name .DS_Store -delete`
|
|
3706
|
+
);
|
|
3145
3707
|
}
|
|
3146
|
-
var
|
|
3147
|
-
|
|
3148
|
-
|
|
3708
|
+
var dsStoreNoiseProbe = {
|
|
3709
|
+
id: "ds_store_noise",
|
|
3710
|
+
run(ctx) {
|
|
3711
|
+
return [checkDsStoreNoise(ctx.readOnlyScanRoot ?? ctx.resolvedPath)];
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
|
|
3715
|
+
// src/doctor/probes/index.ts
|
|
3716
|
+
var DOCTOR_PROBES = [
|
|
3717
|
+
environmentProbe,
|
|
3718
|
+
vaultStructureProbe,
|
|
3719
|
+
gitFleetProbe,
|
|
3720
|
+
hygieneProbe,
|
|
3721
|
+
s3MountHealthProbe,
|
|
3722
|
+
skillsPluginsProbe,
|
|
3723
|
+
vaultSyncProbe,
|
|
3724
|
+
satelliteProbe,
|
|
3725
|
+
metricsProbe,
|
|
3726
|
+
fuseStalenessProbe,
|
|
3727
|
+
activationMarkerProbe,
|
|
3728
|
+
dsStoreNoiseProbe
|
|
3729
|
+
];
|
|
3730
|
+
|
|
3731
|
+
// src/doctor/runner.ts
|
|
3732
|
+
function isDevSourceRun(argv) {
|
|
3733
|
+
return argv.length >= 2 && argv[1].endsWith("cli.js");
|
|
3149
3734
|
}
|
|
3150
|
-
|
|
3151
|
-
const
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
const scanRoot = doctorReadOnlyScanRoot(resolvedPath);
|
|
3161
|
-
const scan = await scanVault(scanRoot);
|
|
3162
|
-
if (!scan.ok) return noVault();
|
|
3163
|
-
const tk = scan.data.typedKnowledge;
|
|
3164
|
-
const typedCount = tk.length;
|
|
3165
|
-
const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
|
|
3166
|
-
const adj = await buildWikilinkAdjacency(tk, void 0, scan.data.allMarkdown);
|
|
3167
|
-
const g = toUndirectedWeighted(adj);
|
|
3168
|
-
const nodes = [...g.keys()];
|
|
3169
|
-
const total = nodes.length;
|
|
3170
|
-
const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
|
|
3171
|
-
const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
|
|
3172
|
-
const comm = louvain(g);
|
|
3173
|
-
const groups = /* @__PURE__ */ new Map();
|
|
3174
|
-
for (const [node, c] of comm) {
|
|
3175
|
-
const arr = groups.get(c);
|
|
3176
|
-
if (arr) arr.push(node);
|
|
3177
|
-
else groups.set(c, [node]);
|
|
3735
|
+
function resolveSnapshotGitWorktree(home) {
|
|
3736
|
+
const configured = resolveConfiguredSnapshotWorktree(home);
|
|
3737
|
+
if (configured) return configured;
|
|
3738
|
+
const defaultPath = "/root/wiki-git";
|
|
3739
|
+
return existsSync17(defaultPath) ? defaultPath : void 0;
|
|
3740
|
+
}
|
|
3741
|
+
var DoctorRunner = class {
|
|
3742
|
+
probes;
|
|
3743
|
+
constructor(probes = DOCTOR_PROBES) {
|
|
3744
|
+
this.probes = probes;
|
|
3178
3745
|
}
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
let bridges = 0;
|
|
3182
|
-
for (const n of nodes) {
|
|
3183
|
-
const nbrComms = /* @__PURE__ */ new Set();
|
|
3184
|
-
for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
|
|
3185
|
-
if (nbrComms.size >= 3) bridges++;
|
|
3746
|
+
getRegisteredProbes() {
|
|
3747
|
+
return this.probes;
|
|
3186
3748
|
}
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3749
|
+
async run(input) {
|
|
3750
|
+
const devSourceRun = isDevSourceRun(input.argv);
|
|
3751
|
+
const vsConfig = readVaultSyncConfig(input.home);
|
|
3752
|
+
const resolved = await resolveRuntimePath({
|
|
3753
|
+
flag: void 0,
|
|
3754
|
+
envValue: input.envValue,
|
|
3755
|
+
home: input.home,
|
|
3756
|
+
cwd: input.cwd
|
|
3757
|
+
});
|
|
3758
|
+
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
3759
|
+
const gitCheckPath = vsConfig.role === "snapshotter" ? resolveSnapshotGitWorktree(input.home) ?? resolvedPath : resolvedPath;
|
|
3760
|
+
const fleetLoad = resolvedPath ? await loadFleetManifestAndHost({
|
|
3761
|
+
vault: resolvedPath,
|
|
3762
|
+
env: { ...process.env, WIKI_PATH: input.envValue ?? resolvedPath },
|
|
3763
|
+
home: input.home,
|
|
3764
|
+
cwd: input.cwd,
|
|
3765
|
+
osHostname: process.env.HOSTNAME,
|
|
3766
|
+
user: process.env.USER
|
|
3767
|
+
}) : null;
|
|
3768
|
+
const satelliteGate = satelliteGateFromFleetLoad(fleetLoad);
|
|
3769
|
+
const readOnlyScanRoot = resolvedPath ? doctorReadOnlyScanRoot(resolvedPath) : void 0;
|
|
3770
|
+
const ctx = {
|
|
3771
|
+
input,
|
|
3772
|
+
devSourceRun,
|
|
3773
|
+
vsConfig,
|
|
3774
|
+
resolvedPath,
|
|
3775
|
+
wikiPathSource: resolved.ok ? resolved.data.source : void 0,
|
|
3776
|
+
gitCheckPath,
|
|
3777
|
+
fleetLoad,
|
|
3778
|
+
readOnlyScanRoot,
|
|
3779
|
+
satelliteGate
|
|
3780
|
+
};
|
|
3781
|
+
const checks = [];
|
|
3782
|
+
for (const probe of this.probes) {
|
|
3783
|
+
const probeChecks = await probe.run(ctx);
|
|
3784
|
+
checks.push(...probeChecks);
|
|
3785
|
+
}
|
|
3786
|
+
const summary = {
|
|
3787
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
3788
|
+
info: checks.filter((c) => c.status === "info").length,
|
|
3789
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
3790
|
+
error: checks.filter((c) => c.status === "error").length
|
|
3791
|
+
};
|
|
3792
|
+
const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
|
|
3793
|
+
const statusIcon = { pass: "\u2713", info: "i", warn: "\u26A0", error: "\u2717" };
|
|
3794
|
+
const lines = checks.map((c) => {
|
|
3795
|
+
const icon = statusIcon[c.status];
|
|
3796
|
+
const padded = c.label.padEnd(24);
|
|
3797
|
+
return ` ${icon} ${padded} ${c.detail}`;
|
|
3798
|
+
});
|
|
3799
|
+
lines.push("");
|
|
3800
|
+
const summaryParts = [`${summary.pass} pass`];
|
|
3801
|
+
if (summary.info > 0) summaryParts.push(`${summary.info} info`);
|
|
3802
|
+
summaryParts.push(`${summary.warn} warn`, `${summary.error} error`);
|
|
3803
|
+
lines.push(summaryParts.join(" \xB7 "));
|
|
3804
|
+
const humanHint = lines.join("\n");
|
|
3805
|
+
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
3191
3806
|
}
|
|
3192
|
-
|
|
3193
|
-
check("info", "vault_metric_pages", "Vault pages by type", `${total} graph node(s) (${typedCount} typed; ${perType})`),
|
|
3194
|
-
check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
|
|
3195
|
-
check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
|
|
3196
|
-
check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
|
|
3197
|
-
check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
|
|
3198
|
-
];
|
|
3199
|
-
}
|
|
3807
|
+
};
|
|
3200
3808
|
async function runDoctor(input) {
|
|
3201
|
-
|
|
3202
|
-
const devSourceRun = isDevSourceRun(input.argv);
|
|
3203
|
-
const vsConfig = readVaultSyncConfig(input.home);
|
|
3204
|
-
checks.push(checkNodeVersion());
|
|
3205
|
-
checks.push(checkCliChannels(input.argv, input.home));
|
|
3206
|
-
checks.push(await checkConfigFile(input.home));
|
|
3207
|
-
checks.push(await checkProfiles(input.home));
|
|
3208
|
-
checks.push(await checkProjectLocalOverride(input.cwd));
|
|
3209
|
-
const resolved = await resolveRuntimePath({
|
|
3210
|
-
flag: void 0,
|
|
3211
|
-
envValue: input.envValue,
|
|
3212
|
-
home: input.home,
|
|
3213
|
-
cwd: input.cwd
|
|
3214
|
-
});
|
|
3215
|
-
if (resolved.ok) {
|
|
3216
|
-
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
3217
|
-
} else {
|
|
3218
|
-
checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
|
|
3219
|
-
}
|
|
3220
|
-
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
3221
|
-
const gitCheckPath = vsConfig.role === "snapshotter" ? resolveSnapshotGitWorktree(input.home) ?? resolvedPath : resolvedPath;
|
|
3222
|
-
checks.push(checkWikiPathExists(resolvedPath));
|
|
3223
|
-
checks.push(checkVaultStructure(resolvedPath));
|
|
3224
|
-
checks.push(checkObsidianTemplates(resolvedPath));
|
|
3225
|
-
checks.push(checkVaultGitRemote(gitCheckPath));
|
|
3226
|
-
const fleetLoad = resolvedPath ? await loadFleetManifestAndHost({
|
|
3227
|
-
vault: resolvedPath,
|
|
3228
|
-
env: { ...process.env, WIKI_PATH: input.envValue ?? resolvedPath },
|
|
3229
|
-
home: input.home,
|
|
3230
|
-
cwd: input.cwd,
|
|
3231
|
-
osHostname: process.env.HOSTNAME,
|
|
3232
|
-
user: process.env.USER
|
|
3233
|
-
}) : null;
|
|
3234
|
-
checks.push(await checkFleetIdentity({
|
|
3235
|
-
vaultPath: resolvedPath,
|
|
3236
|
-
home: input.home,
|
|
3237
|
-
cwd: input.cwd,
|
|
3238
|
-
envValue: input.envValue,
|
|
3239
|
-
fleetLoad
|
|
3240
|
-
}));
|
|
3241
|
-
checks.push(checkSyncLastPush(gitCheckPath));
|
|
3242
|
-
checks.push(checkVaultGitDirty(gitCheckPath));
|
|
3243
|
-
checks.push(checkVaultGitAhead(gitCheckPath));
|
|
3244
|
-
checks.push(checkVaultGitBehind(gitCheckPath));
|
|
3245
|
-
checks.push(checkVaultGitPullFailures(input.home));
|
|
3246
|
-
checks.push(checkVaultLocalGit(gitCheckPath));
|
|
3247
|
-
checks.push(checkVaultGithubRemote(gitCheckPath, input.execProbe));
|
|
3248
|
-
checks.push(checkVaultS3Remote(input.home, input.execProbe, input.env ?? process.env));
|
|
3249
|
-
checks.push(checkVaultSnapshotterReachable(fleetLoad, input.checkSnapshotter, input.execProbe));
|
|
3250
|
-
checks.push(checkVaultPromotionLag(gitCheckPath));
|
|
3251
|
-
const readOnlyScanRoot = resolvedPath ? doctorReadOnlyScanRoot(resolvedPath) : void 0;
|
|
3252
|
-
checks.push(checkDotStoreClean(readOnlyScanRoot));
|
|
3253
|
-
checks.push(checkVaultConflictMarkers(readOnlyScanRoot));
|
|
3254
|
-
checks.push(checkS3MountPerf(resolvedPath));
|
|
3255
|
-
checks.push(checkS3MountFreshness(resolvedPath));
|
|
3256
|
-
checks.push(checkRcloneFlagAudit(resolvedPath));
|
|
3257
|
-
checks.push(checkRcloneVersion(resolvedPath, vsConfig.installed));
|
|
3258
|
-
checks.push(checkWriteTest(resolvedPath));
|
|
3259
|
-
checks.push(checkVfsCacheHealth(resolvedPath));
|
|
3260
|
-
checks.push(checkSkillsInstalled(input.home, input.cwd));
|
|
3261
|
-
checks.push(checkDuplicateSkills(input.home));
|
|
3262
|
-
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
3263
|
-
checks.push(checkPluginVersionDrift(input.home, input.currentVersion, devSourceRun));
|
|
3264
|
-
checks.push(...vaultSyncChecks({
|
|
3265
|
-
home: input.home,
|
|
3266
|
-
vaultSyncInstalled: vsConfig.installed,
|
|
3267
|
-
vaultSyncRole: vsConfig.role,
|
|
3268
|
-
vaultSyncServiceScope: vsConfig.serviceScope,
|
|
3269
|
-
snapshotScriptPath: vsConfig.snapshotScript,
|
|
3270
|
-
env: input.env ?? process.env
|
|
3271
|
-
}));
|
|
3272
|
-
checks.push(checkVaultSyncPullHelper(input.home, input.env ?? process.env));
|
|
3273
|
-
checks.push(checkVaultSyncReviewRequiredJournals(resolvedPath));
|
|
3274
|
-
const satelliteGate = satelliteGateFromFleetLoad(fleetLoad);
|
|
3275
|
-
checks.push(checkSatelliteLastRun(resolvedPath, satelliteGate.satelliteExpected));
|
|
3276
|
-
checks.push(checkSatelliteTimer(satelliteGate.satelliteExpected));
|
|
3277
|
-
checks.push(...await vaultMetrics(resolvedPath));
|
|
3278
|
-
const summary = {
|
|
3279
|
-
pass: checks.filter((c) => c.status === "pass").length,
|
|
3280
|
-
info: checks.filter((c) => c.status === "info").length,
|
|
3281
|
-
warn: checks.filter((c) => c.status === "warn").length,
|
|
3282
|
-
error: checks.filter((c) => c.status === "error").length
|
|
3283
|
-
};
|
|
3284
|
-
const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
|
|
3285
|
-
const statusIcon = { pass: "\u2713", info: "i", warn: "\u26A0", error: "\u2717" };
|
|
3286
|
-
const lines = checks.map((c) => {
|
|
3287
|
-
const icon = statusIcon[c.status];
|
|
3288
|
-
const padded = c.label.padEnd(24);
|
|
3289
|
-
return ` ${icon} ${padded} ${c.detail}`;
|
|
3290
|
-
});
|
|
3291
|
-
lines.push("");
|
|
3292
|
-
const summaryParts = [`${summary.pass} pass`];
|
|
3293
|
-
if (summary.info > 0) summaryParts.push(`${summary.info} info`);
|
|
3294
|
-
summaryParts.push(`${summary.warn} warn`, `${summary.error} error`);
|
|
3295
|
-
lines.push(summaryParts.join(" \xB7 "));
|
|
3296
|
-
const humanHint = lines.join("\n");
|
|
3297
|
-
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
3809
|
+
return new DoctorRunner().run(input);
|
|
3298
3810
|
}
|
|
3299
3811
|
|
|
3300
3812
|
// src/utils/package-info.ts
|
|
3301
|
-
import { readFileSync as
|
|
3813
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
3302
3814
|
function packageJsonCandidateUrls(baseUrl = import.meta.url) {
|
|
3303
3815
|
return [
|
|
3304
3816
|
new URL("../package.json", baseUrl),
|
|
@@ -3308,7 +3820,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
|
|
|
3308
3820
|
function readCliPackageJson(baseUrl = import.meta.url) {
|
|
3309
3821
|
for (const url of packageJsonCandidateUrls(baseUrl)) {
|
|
3310
3822
|
try {
|
|
3311
|
-
const pkg = JSON.parse(
|
|
3823
|
+
const pkg = JSON.parse(readFileSync11(url, "utf8"));
|
|
3312
3824
|
if (typeof pkg.version === "string") {
|
|
3313
3825
|
return { ...pkg, version: pkg.version };
|
|
3314
3826
|
}
|
|
@@ -3320,8 +3832,8 @@ function readCliPackageJson(baseUrl = import.meta.url) {
|
|
|
3320
3832
|
|
|
3321
3833
|
// src/utils/vault-write-gates.ts
|
|
3322
3834
|
import { execFileSync } from "child_process";
|
|
3323
|
-
import { existsSync as
|
|
3324
|
-
import { join as
|
|
3835
|
+
import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
3836
|
+
import { join as join20, relative as relative2 } from "path";
|
|
3325
3837
|
var DEFAULT_DIRTY_VOLUME_THRESHOLD = 50;
|
|
3326
3838
|
var DEFAULT_CAPTURE_BUDGET = 20;
|
|
3327
3839
|
var DEFAULT_NO_DECISION_STREAK = 3;
|
|
@@ -3383,7 +3895,7 @@ function measureDirtyVolume(vault) {
|
|
|
3383
3895
|
is_git_repo: false,
|
|
3384
3896
|
...extra
|
|
3385
3897
|
});
|
|
3386
|
-
if (!
|
|
3898
|
+
if (!existsSync18(vault) || !statSync3(vault).isDirectory()) {
|
|
3387
3899
|
return empty();
|
|
3388
3900
|
}
|
|
3389
3901
|
const gitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
|
|
@@ -3421,8 +3933,8 @@ function measureDirtyVolume(vault) {
|
|
|
3421
3933
|
if (!rel) continue;
|
|
3422
3934
|
if (code === "??") {
|
|
3423
3935
|
untracked += 1;
|
|
3424
|
-
const abs =
|
|
3425
|
-
if (
|
|
3936
|
+
const abs = join20(vault, rel);
|
|
3937
|
+
if (existsSync18(abs) && statSync3(abs).isDirectory()) {
|
|
3426
3938
|
const files = listFilesRecursive(abs);
|
|
3427
3939
|
expanded += files.length;
|
|
3428
3940
|
addBucket(rel, files.length);
|
|
@@ -3452,13 +3964,13 @@ function listFilesRecursive(dir) {
|
|
|
3452
3964
|
const out = [];
|
|
3453
3965
|
let entries;
|
|
3454
3966
|
try {
|
|
3455
|
-
entries =
|
|
3967
|
+
entries = readdirSync7(dir);
|
|
3456
3968
|
} catch {
|
|
3457
3969
|
return out;
|
|
3458
3970
|
}
|
|
3459
3971
|
for (const name of entries) {
|
|
3460
3972
|
if (name === ".git") continue;
|
|
3461
|
-
const p =
|
|
3973
|
+
const p = join20(dir, name);
|
|
3462
3974
|
try {
|
|
3463
3975
|
const st = statSync3(p);
|
|
3464
3976
|
if (st.isDirectory()) out.push(...listFilesRecursive(p));
|
|
@@ -3553,7 +4065,7 @@ var CAPTURE_HYGIENE_CONTRACT = `After each productive cycle (or when daily captu
|
|
|
3553
4065
|
function listProjectDayCaptures(vault, project, day) {
|
|
3554
4066
|
const found = [];
|
|
3555
4067
|
const slug = project.replace(/^\[\[/, "").replace(/\]\]$/, "").trim();
|
|
3556
|
-
if (!slug || !
|
|
4068
|
+
if (!slug || !existsSync18(vault)) return found;
|
|
3557
4069
|
const consider = (abs, rel) => {
|
|
3558
4070
|
if (!rel.endsWith(".md")) return;
|
|
3559
4071
|
const base = rel.split(/[/\\]/).pop() ?? "";
|
|
@@ -3570,7 +4082,7 @@ function listProjectDayCaptures(vault, project, day) {
|
|
|
3570
4082
|
}
|
|
3571
4083
|
if (norm.startsWith("raw/transcripts/")) {
|
|
3572
4084
|
try {
|
|
3573
|
-
const body =
|
|
4085
|
+
const body = readFileSync12(abs, "utf8");
|
|
3574
4086
|
if (body.includes(`project: ${slug}`) || body.includes(`project: "[[${slug}]]"`) || body.includes(`project: [[${slug}]]`)) {
|
|
3575
4087
|
found.push(norm);
|
|
3576
4088
|
} else if (base.includes(slug)) {
|
|
@@ -3581,15 +4093,15 @@ function listProjectDayCaptures(vault, project, day) {
|
|
|
3581
4093
|
}
|
|
3582
4094
|
};
|
|
3583
4095
|
const walk = (dir, relBase) => {
|
|
3584
|
-
if (!
|
|
4096
|
+
if (!existsSync18(dir)) return;
|
|
3585
4097
|
let entries;
|
|
3586
4098
|
try {
|
|
3587
|
-
entries =
|
|
4099
|
+
entries = readdirSync7(dir);
|
|
3588
4100
|
} catch {
|
|
3589
4101
|
return;
|
|
3590
4102
|
}
|
|
3591
4103
|
for (const name of entries) {
|
|
3592
|
-
const abs =
|
|
4104
|
+
const abs = join20(dir, name);
|
|
3593
4105
|
const rel = relBase ? `${relBase}/${name}` : name;
|
|
3594
4106
|
try {
|
|
3595
4107
|
const st = statSync3(abs);
|
|
@@ -3599,16 +4111,16 @@ function listProjectDayCaptures(vault, project, day) {
|
|
|
3599
4111
|
}
|
|
3600
4112
|
}
|
|
3601
4113
|
};
|
|
3602
|
-
walk(
|
|
3603
|
-
walk(
|
|
3604
|
-
walk(
|
|
3605
|
-
const workRoot =
|
|
3606
|
-
if (
|
|
4114
|
+
walk(join20(vault, "raw", "transcripts"), "raw/transcripts");
|
|
4115
|
+
walk(join20(vault, "projects", slug, "raw", "transcripts"), `projects/${slug}/raw/transcripts`);
|
|
4116
|
+
walk(join20(vault, "projects", slug, "requirements"), `projects/${slug}/requirements`);
|
|
4117
|
+
const workRoot = join20(vault, "projects", slug, "work");
|
|
4118
|
+
if (existsSync18(workRoot)) {
|
|
3607
4119
|
try {
|
|
3608
|
-
for (const name of
|
|
4120
|
+
for (const name of readdirSync7(workRoot)) {
|
|
3609
4121
|
if (!name.startsWith(day)) continue;
|
|
3610
4122
|
if (!/investigate|pilot-q|research|cycle|dev-loop/.test(name)) continue;
|
|
3611
|
-
const abs =
|
|
4123
|
+
const abs = join20(workRoot, name);
|
|
3612
4124
|
if (statSync3(abs).isDirectory()) {
|
|
3613
4125
|
for (const f of listFilesRecursive(abs)) {
|
|
3614
4126
|
const rel = relative2(vault, f).replace(/\\/g, "/");
|
|
@@ -3653,7 +4165,7 @@ function evaluateCaptureBudget(input) {
|
|
|
3653
4165
|
return { allowed: true, reason: "under_budget", report };
|
|
3654
4166
|
}
|
|
3655
4167
|
function runWritePreflight(input) {
|
|
3656
|
-
if (!input.vault || !
|
|
4168
|
+
if (!input.vault || !existsSync18(input.vault)) {
|
|
3657
4169
|
return err(GateError.VAULT_PATH_INVALID, { path: input.vault });
|
|
3658
4170
|
}
|
|
3659
4171
|
const want = new Set(input.checks ?? ["all"]);
|
|
@@ -3713,8 +4225,8 @@ function runWritePreflight(input) {
|
|
|
3713
4225
|
|
|
3714
4226
|
// src/commands/observe.ts
|
|
3715
4227
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
3716
|
-
import { existsSync as
|
|
3717
|
-
import { join as
|
|
4228
|
+
import { existsSync as existsSync19, statSync as statSync4 } from "fs";
|
|
4229
|
+
import { join as join21 } from "path";
|
|
3718
4230
|
import { createHash as createHash2 } from "crypto";
|
|
3719
4231
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
3720
4232
|
function slugify(text) {
|
|
@@ -3737,7 +4249,7 @@ async function runObserve(input) {
|
|
|
3737
4249
|
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
3738
4250
|
};
|
|
3739
4251
|
}
|
|
3740
|
-
if (!
|
|
4252
|
+
if (!existsSync19(input.vault) || !statSync4(input.vault).isDirectory()) {
|
|
3741
4253
|
return {
|
|
3742
4254
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
3743
4255
|
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
@@ -3763,7 +4275,7 @@ async function runObserve(input) {
|
|
|
3763
4275
|
};
|
|
3764
4276
|
}
|
|
3765
4277
|
}
|
|
3766
|
-
const transcriptsDir =
|
|
4278
|
+
const transcriptsDir = join21(input.vault, "raw", "transcripts");
|
|
3767
4279
|
try {
|
|
3768
4280
|
await mkdir3(transcriptsDir, { recursive: true });
|
|
3769
4281
|
} catch {
|
|
@@ -3775,7 +4287,7 @@ async function runObserve(input) {
|
|
|
3775
4287
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3776
4288
|
const slug = slugify(input.text);
|
|
3777
4289
|
const fileName = `${today}-observation-${slug}.md`;
|
|
3778
|
-
const filePath =
|
|
4290
|
+
const filePath = join21(transcriptsDir, fileName);
|
|
3779
4291
|
const body = `
|
|
3780
4292
|
${input.text.trim()}
|
|
3781
4293
|
`;
|
|
@@ -3817,7 +4329,7 @@ ${input.text.trim()}
|
|
|
3817
4329
|
// src/commands/memory.ts
|
|
3818
4330
|
import { createHash as createHash3 } from "crypto";
|
|
3819
4331
|
import { mkdir as mkdir4, readFile as readFile7, readdir as readdir2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
3820
|
-
import { basename as basename2, extname, join as
|
|
4332
|
+
import { basename as basename2, extname, join as join22, relative as relative3, sep as sep2 } from "path";
|
|
3821
4333
|
|
|
3822
4334
|
// src/utils/memory-authority.ts
|
|
3823
4335
|
var TIER_RANK = {
|
|
@@ -3936,8 +4448,8 @@ async function runMemoryIndex(input) {
|
|
|
3936
4448
|
}
|
|
3937
4449
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
3938
4450
|
const relCachePath = memoryCacheRelPath(input.project);
|
|
3939
|
-
const absCachePath =
|
|
3940
|
-
await mkdir4(
|
|
4451
|
+
const absCachePath = join22(input.vault, relCachePath);
|
|
4452
|
+
await mkdir4(join22(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
|
|
3941
4453
|
await writeFile3(absCachePath, `${JSON.stringify({
|
|
3942
4454
|
generated_at: generatedAt,
|
|
3943
4455
|
project: input.project,
|
|
@@ -4129,7 +4641,7 @@ async function buildMemoryIndexState(pages, project) {
|
|
|
4129
4641
|
}
|
|
4130
4642
|
async function checkMemoryIndex(vault, project, current) {
|
|
4131
4643
|
const relCachePath = memoryCacheRelPath(project);
|
|
4132
|
-
const cacheText = await readIfExists(
|
|
4644
|
+
const cacheText = await readIfExists(join22(vault, relCachePath));
|
|
4133
4645
|
if (!cacheText) {
|
|
4134
4646
|
return {
|
|
4135
4647
|
ok: true,
|
|
@@ -4538,7 +5050,7 @@ async function walkImportFiles(dir, out) {
|
|
|
4538
5050
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
4539
5051
|
for (const entry of entries) {
|
|
4540
5052
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
4541
|
-
const path =
|
|
5053
|
+
const path = join22(dir, entry.name);
|
|
4542
5054
|
if (entry.isDirectory()) {
|
|
4543
5055
|
await walkImportFiles(path, out);
|
|
4544
5056
|
} else if (entry.isFile() && isImportCandidate(path)) {
|
|
@@ -4605,8 +5117,8 @@ async function writeImportCapture(vault, entry, today) {
|
|
|
4605
5117
|
const content = hiddenString(entry, "__content");
|
|
4606
5118
|
const project = hiddenString(entry, "__project");
|
|
4607
5119
|
const relPath = await availableImportPath(vault, entry.proposed_path);
|
|
4608
|
-
const absPath =
|
|
4609
|
-
await mkdir4(
|
|
5120
|
+
const absPath = join22(vault, relPath);
|
|
5121
|
+
await mkdir4(join22(vault, "raw", "transcripts"), { recursive: true });
|
|
4610
5122
|
await writeFile3(absPath, renderImportCapture(entry, content, project, today), "utf8");
|
|
4611
5123
|
const validation = await runValidate({ file: absPath });
|
|
4612
5124
|
return {
|
|
@@ -4622,7 +5134,7 @@ async function availableImportPath(vault, proposed) {
|
|
|
4622
5134
|
const stem = proposed.slice(0, -ext.length);
|
|
4623
5135
|
let candidate = proposed;
|
|
4624
5136
|
let i = 2;
|
|
4625
|
-
while (await readIfExists(
|
|
5137
|
+
while (await readIfExists(join22(vault, candidate))) {
|
|
4626
5138
|
candidate = `${stem}-${i}${ext}`;
|
|
4627
5139
|
i++;
|
|
4628
5140
|
}
|
|
@@ -4807,10 +5319,10 @@ function memoryCacheRelPath(project) {
|
|
|
4807
5319
|
}
|
|
4808
5320
|
async function readMemoryCache(vault, project) {
|
|
4809
5321
|
if (project) {
|
|
4810
|
-
const projectCache = await readIfExists(
|
|
5322
|
+
const projectCache = await readIfExists(join22(vault, memoryCacheRelPath(project)));
|
|
4811
5323
|
if (projectCache) return projectCache;
|
|
4812
5324
|
}
|
|
4813
|
-
return readIfExists(
|
|
5325
|
+
return readIfExists(join22(vault, ".skillwiki", "memory-topics.json"));
|
|
4814
5326
|
}
|
|
4815
5327
|
function dedupePages(pages) {
|
|
4816
5328
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4901,8 +5413,310 @@ function slugify2(value) {
|
|
|
4901
5413
|
}
|
|
4902
5414
|
|
|
4903
5415
|
// src/commands/query.ts
|
|
4904
|
-
import { readFile as
|
|
4905
|
-
import { join as
|
|
5416
|
+
import { readFile as readFile9, stat as stat4 } from "fs/promises";
|
|
5417
|
+
import { join as join24 } from "path";
|
|
5418
|
+
|
|
5419
|
+
// src/utils/rrf.ts
|
|
5420
|
+
var RRF_K = 60;
|
|
5421
|
+
function fuseRankings(lists, k = RRF_K) {
|
|
5422
|
+
const scores = /* @__PURE__ */ new Map();
|
|
5423
|
+
for (const list of lists) {
|
|
5424
|
+
list.forEach((id, index) => {
|
|
5425
|
+
scores.set(id, (scores.get(id) ?? 0) + 1 / (k + index + 1));
|
|
5426
|
+
});
|
|
5427
|
+
}
|
|
5428
|
+
return [...scores.entries()].map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5431
|
+
// src/utils/vector-index.ts
|
|
5432
|
+
import { mkdir as mkdir5, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
5433
|
+
import { join as join23 } from "path";
|
|
5434
|
+
import { existsSync as existsSync20 } from "fs";
|
|
5435
|
+
var VECTOR_INDEX_REL = ".skillwiki/vectors/index.json";
|
|
5436
|
+
var VECTOR_INDEX_SCHEMA = "skillwiki-tfidf-index/v1";
|
|
5437
|
+
function tokenize(text) {
|
|
5438
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
|
|
5439
|
+
}
|
|
5440
|
+
function extractPageTokens(text) {
|
|
5441
|
+
const fm = extractFrontmatter(text);
|
|
5442
|
+
const split = splitFrontmatter(text);
|
|
5443
|
+
const title = fm.ok ? String(fm.data.title ?? "") : "";
|
|
5444
|
+
const body = split.ok ? split.data.body : text;
|
|
5445
|
+
return tokenize(`${title} ${body}`);
|
|
5446
|
+
}
|
|
5447
|
+
function vectorIndexPath(vault) {
|
|
5448
|
+
return join23(vault, ...VECTOR_INDEX_REL.split("/"));
|
|
5449
|
+
}
|
|
5450
|
+
function termFreq(tokens) {
|
|
5451
|
+
const tf = /* @__PURE__ */ new Map();
|
|
5452
|
+
for (const token of tokens) tf.set(token, (tf.get(token) ?? 0) + 1);
|
|
5453
|
+
return tf;
|
|
5454
|
+
}
|
|
5455
|
+
function toTfIdf(tf, df, docs) {
|
|
5456
|
+
const out = {};
|
|
5457
|
+
for (const [term, count] of tf) {
|
|
5458
|
+
const appears = df[term] ?? 0;
|
|
5459
|
+
if (appears === 0) continue;
|
|
5460
|
+
out[term] = count / Math.max(1, tf.size) * Math.log((docs + 1) / appears);
|
|
5461
|
+
}
|
|
5462
|
+
return out;
|
|
5463
|
+
}
|
|
5464
|
+
function cosine(a, b) {
|
|
5465
|
+
let dot = 0;
|
|
5466
|
+
let na = 0;
|
|
5467
|
+
let nb = 0;
|
|
5468
|
+
for (const value of Object.values(a)) na += value * value;
|
|
5469
|
+
for (const value of Object.values(b)) nb += value * value;
|
|
5470
|
+
if (na === 0 || nb === 0) return 0;
|
|
5471
|
+
const shorter = Object.keys(a).length < Object.keys(b).length ? a : b;
|
|
5472
|
+
const longer = shorter === a ? b : a;
|
|
5473
|
+
for (const [term, value] of Object.entries(shorter)) {
|
|
5474
|
+
const other = longer[term];
|
|
5475
|
+
if (other) dot += value * other;
|
|
5476
|
+
}
|
|
5477
|
+
return dot / Math.sqrt(na * nb);
|
|
5478
|
+
}
|
|
5479
|
+
async function buildVectorIndex(vault, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5480
|
+
const scan = await scanVault(vault);
|
|
5481
|
+
if (!scan.ok) return scan;
|
|
5482
|
+
const tokenized = [];
|
|
5483
|
+
for (const page of scan.data.typedKnowledge) {
|
|
5484
|
+
const text = await readPage(page);
|
|
5485
|
+
tokenized.push({ path: page.relPath, tokens: extractPageTokens(text) });
|
|
5486
|
+
}
|
|
5487
|
+
const df = {};
|
|
5488
|
+
for (const doc of tokenized) {
|
|
5489
|
+
for (const term of new Set(doc.tokens)) df[term] = (df[term] ?? 0) + 1;
|
|
5490
|
+
}
|
|
5491
|
+
const docs = {};
|
|
5492
|
+
for (const doc of tokenized) {
|
|
5493
|
+
docs[doc.path] = toTfIdf(termFreq(doc.tokens), df, tokenized.length);
|
|
5494
|
+
}
|
|
5495
|
+
const index = {
|
|
5496
|
+
schema: VECTOR_INDEX_SCHEMA,
|
|
5497
|
+
built_at: now,
|
|
5498
|
+
page_count: tokenized.length,
|
|
5499
|
+
df,
|
|
5500
|
+
docs
|
|
5501
|
+
};
|
|
5502
|
+
const dest = vectorIndexPath(vault);
|
|
5503
|
+
await mkdir5(join23(vault, ".skillwiki", "vectors"), { recursive: true });
|
|
5504
|
+
await atomicWriteText(dest, `${JSON.stringify(index)}
|
|
5505
|
+
`);
|
|
5506
|
+
return ok(index);
|
|
5507
|
+
}
|
|
5508
|
+
async function loadVectorIndex(vault) {
|
|
5509
|
+
try {
|
|
5510
|
+
const raw = await readFile8(vectorIndexPath(vault), "utf8");
|
|
5511
|
+
const parsed = JSON.parse(raw);
|
|
5512
|
+
if (parsed.schema !== VECTOR_INDEX_SCHEMA || typeof parsed.docs !== "object" || parsed.docs === null) {
|
|
5513
|
+
return err("HYBRID_INDEX_INVALID", { path: VECTOR_INDEX_REL });
|
|
5514
|
+
}
|
|
5515
|
+
return ok(parsed);
|
|
5516
|
+
} catch {
|
|
5517
|
+
return err("HYBRID_INDEX_MISSING", { path: VECTOR_INDEX_REL, message: "run skillwiki vectors rebuild" });
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
async function vectorIndexStatus(vault) {
|
|
5521
|
+
const path = vectorIndexPath(vault);
|
|
5522
|
+
try {
|
|
5523
|
+
const fileStat = await stat3(path);
|
|
5524
|
+
const loaded = await loadVectorIndex(vault);
|
|
5525
|
+
if (!loaded.ok) return ok({ path: VECTOR_INDEX_REL, present: false });
|
|
5526
|
+
return ok({
|
|
5527
|
+
path: VECTOR_INDEX_REL,
|
|
5528
|
+
present: true,
|
|
5529
|
+
page_count: loaded.data.page_count,
|
|
5530
|
+
built_at: loaded.data.built_at,
|
|
5531
|
+
age_hours: (Date.now() - fileStat.mtimeMs) / 36e5
|
|
5532
|
+
});
|
|
5533
|
+
} catch {
|
|
5534
|
+
return ok({ path: VECTOR_INDEX_REL, present: false });
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
function rankVectorIndex(index, query) {
|
|
5538
|
+
const q = toTfIdf(termFreq(tokenize(query)), index.df, Math.max(1, index.page_count));
|
|
5539
|
+
return Object.entries(index.docs).map(([path, vector]) => ({ path, score: cosine(q, vector) })).filter((row) => row.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).map((row) => row.path);
|
|
5540
|
+
}
|
|
5541
|
+
async function reindexPageInVectorIndex(vault, pageRelPath, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5542
|
+
const normalizedPage = pageRelPath.split(/\\|\//).join("/");
|
|
5543
|
+
if (!normalizedPage.endsWith(".md")) {
|
|
5544
|
+
return err("USAGE", { message: `Page must be a .md file: ${pageRelPath}` });
|
|
5545
|
+
}
|
|
5546
|
+
const absPath = join23(vault, ...normalizedPage.split("/"));
|
|
5547
|
+
let text;
|
|
5548
|
+
try {
|
|
5549
|
+
text = await readFile8(absPath, "utf8");
|
|
5550
|
+
} catch {
|
|
5551
|
+
return err("FILE_NOT_FOUND", { path: normalizedPage });
|
|
5552
|
+
}
|
|
5553
|
+
const loaded = await loadVectorIndex(vault);
|
|
5554
|
+
if (!loaded.ok) return loaded;
|
|
5555
|
+
const index = loaded.data;
|
|
5556
|
+
const tokens = extractPageTokens(text);
|
|
5557
|
+
const newTf = termFreq(tokens);
|
|
5558
|
+
const newTerms = new Set(newTf.keys());
|
|
5559
|
+
const oldDoc = index.docs[normalizedPage];
|
|
5560
|
+
const oldTerms = oldDoc ? new Set(Object.keys(oldDoc)) : /* @__PURE__ */ new Set();
|
|
5561
|
+
const isNewPage = !oldDoc;
|
|
5562
|
+
const newPageCount = isNewPage ? index.page_count + 1 : index.page_count;
|
|
5563
|
+
const termsAdded = [];
|
|
5564
|
+
const termsRemoved = [];
|
|
5565
|
+
for (const t of newTerms) {
|
|
5566
|
+
if (!oldTerms.has(t)) termsAdded.push(t);
|
|
5567
|
+
}
|
|
5568
|
+
for (const t of oldTerms) {
|
|
5569
|
+
if (!newTerms.has(t)) termsRemoved.push(t);
|
|
5570
|
+
}
|
|
5571
|
+
const dfChangedTerms = /* @__PURE__ */ new Set();
|
|
5572
|
+
for (const t of termsAdded) {
|
|
5573
|
+
index.df[t] = (index.df[t] ?? 0) + 1;
|
|
5574
|
+
dfChangedTerms.add(t);
|
|
5575
|
+
}
|
|
5576
|
+
for (const t of termsRemoved) {
|
|
5577
|
+
const nextDf = (index.df[t] ?? 1) - 1;
|
|
5578
|
+
if (nextDf <= 0) {
|
|
5579
|
+
delete index.df[t];
|
|
5580
|
+
} else {
|
|
5581
|
+
index.df[t] = nextDf;
|
|
5582
|
+
}
|
|
5583
|
+
dfChangedTerms.add(t);
|
|
5584
|
+
}
|
|
5585
|
+
index.page_count = newPageCount;
|
|
5586
|
+
index.built_at = now;
|
|
5587
|
+
index.docs[normalizedPage] = toTfIdf(newTf, index.df, index.page_count);
|
|
5588
|
+
if (isNewPage) {
|
|
5589
|
+
const oldDocsTotal = newPageCount - 1;
|
|
5590
|
+
for (const [docPath, docVector] of Object.entries(index.docs)) {
|
|
5591
|
+
if (docPath === normalizedPage) continue;
|
|
5592
|
+
const updatedVector = {};
|
|
5593
|
+
for (const [term, oldScore] of Object.entries(docVector)) {
|
|
5594
|
+
const dfVal = index.df[term];
|
|
5595
|
+
if (!dfVal || dfVal <= 0) continue;
|
|
5596
|
+
const oldDf = termsAdded.includes(term) ? index.df[term] - 1 : index.df[term];
|
|
5597
|
+
const oldIdf = Math.log((oldDocsTotal + 1) / oldDf);
|
|
5598
|
+
const newIdf = Math.log((newPageCount + 1) / dfVal);
|
|
5599
|
+
updatedVector[term] = oldIdf !== 0 ? oldScore * (newIdf / oldIdf) : 0;
|
|
5600
|
+
}
|
|
5601
|
+
index.docs[docPath] = updatedVector;
|
|
5602
|
+
}
|
|
5603
|
+
} else if (dfChangedTerms.size > 0) {
|
|
5604
|
+
for (const [docPath, docVector] of Object.entries(index.docs)) {
|
|
5605
|
+
if (docPath === normalizedPage) continue;
|
|
5606
|
+
let touched = false;
|
|
5607
|
+
for (const t of Object.keys(docVector)) {
|
|
5608
|
+
if (dfChangedTerms.has(t)) {
|
|
5609
|
+
touched = true;
|
|
5610
|
+
break;
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
if (!touched) continue;
|
|
5614
|
+
const updatedVector = {};
|
|
5615
|
+
for (const [term, oldScore] of Object.entries(docVector)) {
|
|
5616
|
+
const dfVal = index.df[term];
|
|
5617
|
+
if (!dfVal || dfVal <= 0) continue;
|
|
5618
|
+
if (dfChangedTerms.has(term)) {
|
|
5619
|
+
const oldDf = termsAdded.includes(term) ? dfVal - 1 : termsRemoved.includes(term) ? dfVal + 1 : dfVal;
|
|
5620
|
+
const oldIdf = Math.log((newPageCount + 1) / oldDf);
|
|
5621
|
+
const newIdf = Math.log((newPageCount + 1) / dfVal);
|
|
5622
|
+
updatedVector[term] = oldIdf !== 0 ? oldScore * (newIdf / oldIdf) : 0;
|
|
5623
|
+
} else {
|
|
5624
|
+
updatedVector[term] = oldScore;
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
index.docs[docPath] = updatedVector;
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
const dest = vectorIndexPath(vault);
|
|
5631
|
+
await mkdir5(join23(vault, ".skillwiki", "vectors"), { recursive: true });
|
|
5632
|
+
await atomicWriteText(dest, `${JSON.stringify(index)}
|
|
5633
|
+
`);
|
|
5634
|
+
return ok({
|
|
5635
|
+
path: VECTOR_INDEX_REL,
|
|
5636
|
+
page: normalizedPage,
|
|
5637
|
+
terms_added: termsAdded.length,
|
|
5638
|
+
terms_removed: termsRemoved.length,
|
|
5639
|
+
page_count: index.page_count
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
async function pruneVectorIndex(vault, opts) {
|
|
5643
|
+
const loaded = await loadVectorIndex(vault);
|
|
5644
|
+
if (!loaded.ok) return loaded;
|
|
5645
|
+
const index = loaded.data;
|
|
5646
|
+
const orphans = [];
|
|
5647
|
+
for (const docKey of Object.keys(index.docs)) {
|
|
5648
|
+
const normalizedKey = docKey.split(/\\|\//).join("/");
|
|
5649
|
+
const absPath = join23(vault, ...normalizedKey.split("/"));
|
|
5650
|
+
if (!existsSync20(absPath)) {
|
|
5651
|
+
orphans.push(normalizedKey);
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
if (orphans.length === 0) {
|
|
5655
|
+
return ok({
|
|
5656
|
+
path: VECTOR_INDEX_REL,
|
|
5657
|
+
orphans: [],
|
|
5658
|
+
removed: 0,
|
|
5659
|
+
page_count: index.page_count,
|
|
5660
|
+
terms_pruned: 0
|
|
5661
|
+
});
|
|
5662
|
+
}
|
|
5663
|
+
const oldPageCount = index.page_count;
|
|
5664
|
+
const newPageCount = Math.max(0, oldPageCount - orphans.length);
|
|
5665
|
+
let termsPrunedCount = 0;
|
|
5666
|
+
for (const orphanKey of orphans) {
|
|
5667
|
+
const orphanDoc = index.docs[orphanKey] ?? {};
|
|
5668
|
+
for (const term of Object.keys(orphanDoc)) {
|
|
5669
|
+
const nextDf = (index.df[term] ?? 1) - 1;
|
|
5670
|
+
if (nextDf <= 0) {
|
|
5671
|
+
delete index.df[term];
|
|
5672
|
+
termsPrunedCount++;
|
|
5673
|
+
} else {
|
|
5674
|
+
index.df[term] = nextDf;
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
delete index.docs[orphanKey];
|
|
5678
|
+
}
|
|
5679
|
+
index.page_count = newPageCount;
|
|
5680
|
+
const now = opts?.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
5681
|
+
index.built_at = now;
|
|
5682
|
+
const scan = await scanVault(vault);
|
|
5683
|
+
if (!scan.ok) return scan;
|
|
5684
|
+
const remainingPagesByRel = /* @__PURE__ */ new Map();
|
|
5685
|
+
for (const p of scan.data.typedKnowledge) {
|
|
5686
|
+
remainingPagesByRel.set(p.relPath, p);
|
|
5687
|
+
}
|
|
5688
|
+
for (const docKey of Object.keys(index.docs)) {
|
|
5689
|
+
const page = remainingPagesByRel.get(docKey);
|
|
5690
|
+
if (page) {
|
|
5691
|
+
const text = await readPage(page);
|
|
5692
|
+
const tokens = extractPageTokens(text);
|
|
5693
|
+
index.docs[docKey] = toTfIdf(termFreq(tokens), index.df, newPageCount);
|
|
5694
|
+
} else {
|
|
5695
|
+
try {
|
|
5696
|
+
const text = await readFile8(join23(vault, ...docKey.split("/")), "utf8");
|
|
5697
|
+
const tokens = extractPageTokens(text);
|
|
5698
|
+
index.docs[docKey] = toTfIdf(termFreq(tokens), index.df, newPageCount);
|
|
5699
|
+
} catch {
|
|
5700
|
+
delete index.docs[docKey];
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
if (!opts?.dryRun) {
|
|
5705
|
+
const dest = vectorIndexPath(vault);
|
|
5706
|
+
await mkdir5(join23(vault, ".skillwiki", "vectors"), { recursive: true });
|
|
5707
|
+
await atomicWriteText(dest, `${JSON.stringify(index)}
|
|
5708
|
+
`);
|
|
5709
|
+
}
|
|
5710
|
+
return ok({
|
|
5711
|
+
path: VECTOR_INDEX_REL,
|
|
5712
|
+
orphans,
|
|
5713
|
+
removed: orphans.length,
|
|
5714
|
+
page_count: newPageCount,
|
|
5715
|
+
terms_pruned: termsPrunedCount
|
|
5716
|
+
});
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5719
|
+
// src/commands/query.ts
|
|
4906
5720
|
var W_KEYWORD = 2;
|
|
4907
5721
|
var W_SOURCE_OVERLAP = 4;
|
|
4908
5722
|
var W_WIKILINK = 3;
|
|
@@ -4930,7 +5744,7 @@ async function runQuery(input) {
|
|
|
4930
5744
|
const scan = await scanVault(input.vault);
|
|
4931
5745
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
4932
5746
|
const limit = input.limit ?? 10;
|
|
4933
|
-
const queryTerms =
|
|
5747
|
+
const queryTerms = tokenize2(input.text);
|
|
4934
5748
|
if (queryTerms.length === 0) {
|
|
4935
5749
|
return {
|
|
4936
5750
|
exitCode: ExitCode.OK,
|
|
@@ -4975,7 +5789,7 @@ async function runQuery(input) {
|
|
|
4975
5789
|
}
|
|
4976
5790
|
const suppressRepetitiveHistoricalCycles = historicalCyclePageCount >= 3 && hasDirectOperationalSeed;
|
|
4977
5791
|
const structuralSeedPaths = suppressRepetitiveHistoricalCycles ? operationalSeedPaths : seedPaths;
|
|
4978
|
-
const
|
|
5792
|
+
const structural = pages.map((page) => {
|
|
4979
5793
|
const sourceOverlap = scoreSourceOverlap(page, pages, structuralSeedPaths);
|
|
4980
5794
|
const wikilink = scoreWikilink(page.relPath, structuralSeedPaths, graph);
|
|
4981
5795
|
const aa = scoreAdamicAdar(page.relPath, structuralSeedPaths, graph);
|
|
@@ -4990,11 +5804,31 @@ async function runQuery(input) {
|
|
|
4990
5804
|
title: page.title,
|
|
4991
5805
|
type: page.type
|
|
4992
5806
|
};
|
|
4993
|
-
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
5807
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
5808
|
+
let results = structural.slice(0, limit);
|
|
5809
|
+
let hybridMeta;
|
|
5810
|
+
if (input.hybrid) {
|
|
5811
|
+
const index = await loadVectorIndex(input.vault);
|
|
5812
|
+
if (!index.ok) return { exitCode: ExitCode.USAGE, result: index };
|
|
5813
|
+
const byPath = new Map(pages.map((page) => [page.relPath, page]));
|
|
5814
|
+
const fused = fuseRankings([structural.map((row) => row.path), rankVectorIndex(index.data, input.text)]);
|
|
5815
|
+
results = fused.slice(0, limit).map((row) => {
|
|
5816
|
+
const existing = structural.find((item) => item.path === row.id);
|
|
5817
|
+
if (existing) return { ...existing, score: Math.round(row.score * 1e3) / 1e3 };
|
|
5818
|
+
const page = byPath.get(row.id);
|
|
5819
|
+
return {
|
|
5820
|
+
path: row.id,
|
|
5821
|
+
score: Math.round(row.score * 1e3) / 1e3,
|
|
5822
|
+
title: page?.title ?? "",
|
|
5823
|
+
type: page?.type ?? ""
|
|
5824
|
+
};
|
|
5825
|
+
});
|
|
5826
|
+
hybridMeta = { used: true, rrf_k: RRF_K };
|
|
5827
|
+
}
|
|
4994
5828
|
let pendingSources;
|
|
4995
5829
|
if (input.includePending) {
|
|
4996
|
-
const { runSourcesPending } = await import("./sources-
|
|
4997
|
-
const pending = await
|
|
5830
|
+
const { runSourcesPending: runSourcesPending2 } = await import("./sources-BFYEFTS4.js");
|
|
5831
|
+
const pending = await runSourcesPending2({
|
|
4998
5832
|
vault: input.vault,
|
|
4999
5833
|
match: input.text,
|
|
5000
5834
|
limit: input.limit ?? 10
|
|
@@ -5013,6 +5847,7 @@ ${pendingSources.length} matching pending source(s)` : "no matching pages found"
|
|
|
5013
5847
|
results,
|
|
5014
5848
|
...pendingSources ? { pending_sources: pendingSources } : {},
|
|
5015
5849
|
...rankingGuardrails ? { ranking_guardrails: rankingGuardrails } : {},
|
|
5850
|
+
...hybridMeta ? { hybrid: hybridMeta } : {},
|
|
5016
5851
|
humanHint
|
|
5017
5852
|
})
|
|
5018
5853
|
};
|
|
@@ -5056,7 +5891,7 @@ function scoreTypeAffinity(pageType, queryTerms) {
|
|
|
5056
5891
|
function isHistoricalCyclePage(relPath, title) {
|
|
5057
5892
|
return HISTORICAL_CYCLE_RE.test(`${relPath} ${title}`);
|
|
5058
5893
|
}
|
|
5059
|
-
function
|
|
5894
|
+
function tokenize2(text) {
|
|
5060
5895
|
return text.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
5061
5896
|
}
|
|
5062
5897
|
function computeKeywordScore(terms, title, tags, body) {
|
|
@@ -5072,10 +5907,10 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
5072
5907
|
return score;
|
|
5073
5908
|
}
|
|
5074
5909
|
async function loadOrBuildGraph(vault) {
|
|
5075
|
-
const graphPath =
|
|
5910
|
+
const graphPath = join24(vault, ".skillwiki", "graph.json");
|
|
5076
5911
|
let needsBuild = false;
|
|
5077
5912
|
try {
|
|
5078
|
-
const fileStat = await
|
|
5913
|
+
const fileStat = await stat4(graphPath);
|
|
5079
5914
|
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
5080
5915
|
if (ageHours > 24) needsBuild = true;
|
|
5081
5916
|
} catch {
|
|
@@ -5086,13 +5921,73 @@ async function loadOrBuildGraph(vault) {
|
|
|
5086
5921
|
if (buildResult.exitCode !== 0) return null;
|
|
5087
5922
|
}
|
|
5088
5923
|
try {
|
|
5089
|
-
const raw = await
|
|
5924
|
+
const raw = await readFile9(graphPath, "utf8");
|
|
5090
5925
|
return JSON.parse(raw);
|
|
5091
5926
|
} catch {
|
|
5092
5927
|
return null;
|
|
5093
5928
|
}
|
|
5094
5929
|
}
|
|
5095
5930
|
|
|
5931
|
+
// src/commands/source-compile.ts
|
|
5932
|
+
function writeGuard(approve) {
|
|
5933
|
+
if (!approve) return { ok: false, error: "APPROVAL_INVALID", detail: { message: "--write requires --approve" } };
|
|
5934
|
+
return null;
|
|
5935
|
+
}
|
|
5936
|
+
async function runSourceCompileClaim(input) {
|
|
5937
|
+
if (!input.write) {
|
|
5938
|
+
const plan = await planSourceCompileClaim(input);
|
|
5939
|
+
return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
|
|
5940
|
+
}
|
|
5941
|
+
const missing = writeGuard(input.approve);
|
|
5942
|
+
if (missing) return { exitCode: ExitCode.USAGE, result: missing };
|
|
5943
|
+
const applied = await applySourceCompileClaim({ ...input, approve: input.approve });
|
|
5944
|
+
if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
|
|
5945
|
+
return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `claimed ${input.rawPath}` }) };
|
|
5946
|
+
}
|
|
5947
|
+
async function runSourceCompileRelease(input) {
|
|
5948
|
+
if (!input.write) {
|
|
5949
|
+
const plan = await planSourceCompileRelease(input);
|
|
5950
|
+
return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
|
|
5951
|
+
}
|
|
5952
|
+
const missing = writeGuard(input.approve);
|
|
5953
|
+
if (missing) return { exitCode: ExitCode.USAGE, result: missing };
|
|
5954
|
+
const applied = await applySourceCompileRelease({ ...input, approve: input.approve });
|
|
5955
|
+
if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
|
|
5956
|
+
return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `released ${input.rawPath}` }) };
|
|
5957
|
+
}
|
|
5958
|
+
async function runSourceCompilePublished(input) {
|
|
5959
|
+
if (!input.write) {
|
|
5960
|
+
const plan = await planSourceCompilePublished(input);
|
|
5961
|
+
return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
|
|
5962
|
+
}
|
|
5963
|
+
const missing = writeGuard(input.approve);
|
|
5964
|
+
if (missing) return { exitCode: ExitCode.USAGE, result: missing };
|
|
5965
|
+
const applied = await applySourceCompilePublished({ ...input, approve: input.approve });
|
|
5966
|
+
if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
|
|
5967
|
+
return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `published compile turn for ${input.rawPath}` }) };
|
|
5968
|
+
}
|
|
5969
|
+
async function runSourceReview(input) {
|
|
5970
|
+
if (!input.write) {
|
|
5971
|
+
const plan = await planSourceReview(input);
|
|
5972
|
+
return { exitCode: plan.ok ? ExitCode.OK : ExitCode.USAGE, result: plan };
|
|
5973
|
+
}
|
|
5974
|
+
const missing = writeGuard(input.approve);
|
|
5975
|
+
if (missing) return { exitCode: ExitCode.USAGE, result: missing };
|
|
5976
|
+
const applied = await applySourceReview({ ...input, approve: input.approve });
|
|
5977
|
+
if (!applied.ok) return { exitCode: ExitCode.USAGE, result: applied };
|
|
5978
|
+
return { exitCode: ExitCode.OK, result: ok({ ...applied.data, humanHint: `${input.status} review for ${input.rawPath}` }) };
|
|
5979
|
+
}
|
|
5980
|
+
async function runSourceCompileStatus(input) {
|
|
5981
|
+
const listed = await listCompileStatus(input);
|
|
5982
|
+
if (!listed.ok) return { exitCode: ExitCode.USAGE, result: listed };
|
|
5983
|
+
return { exitCode: ExitCode.OK, result: ok({ ...listed.data, humanHint: listed.data.items.length ? `${listed.data.items.length} compile turns` : "no active compile turns" }) };
|
|
5984
|
+
}
|
|
5985
|
+
async function runSourceReviews(input) {
|
|
5986
|
+
const listed = await listSourceReviews(input);
|
|
5987
|
+
if (!listed.ok) return { exitCode: ExitCode.USAGE, result: listed };
|
|
5988
|
+
return { exitCode: ExitCode.OK, result: ok({ ...listed.data, humanHint: listed.data.items.length ? `${listed.data.items.length} open reviews` : "no open reviews" }) };
|
|
5989
|
+
}
|
|
5990
|
+
|
|
5096
5991
|
// src/mcp/server.ts
|
|
5097
5992
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5098
5993
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -5101,7 +5996,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5101
5996
|
import { z } from "zod";
|
|
5102
5997
|
|
|
5103
5998
|
// src/mcp/vault-resolve.ts
|
|
5104
|
-
import { join as
|
|
5999
|
+
import { join as join25, resolve as resolve4 } from "path";
|
|
5105
6000
|
|
|
5106
6001
|
// src/mcp/allowlist.ts
|
|
5107
6002
|
import { resolve as resolve3, sep as sep3 } from "path";
|
|
@@ -5163,7 +6058,7 @@ async function resolveMcpVault(input) {
|
|
|
5163
6058
|
return ok({ vault: vaultPath, source });
|
|
5164
6059
|
}
|
|
5165
6060
|
function defaultGraphOut(vault) {
|
|
5166
|
-
return
|
|
6061
|
+
return join25(vault, ".skillwiki", "graph.json");
|
|
5167
6062
|
}
|
|
5168
6063
|
|
|
5169
6064
|
// src/mcp/result-format.ts
|
|
@@ -5180,7 +6075,7 @@ function formatToolResult(payload) {
|
|
|
5180
6075
|
// src/mcp/audit-log.ts
|
|
5181
6076
|
import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
5182
6077
|
import { homedir } from "os";
|
|
5183
|
-
import { join as
|
|
6078
|
+
import { join as join26 } from "path";
|
|
5184
6079
|
function auditEnabled() {
|
|
5185
6080
|
const v = process.env.SKILLWIKI_MCP_AUDIT;
|
|
5186
6081
|
if (v === "0" || v === "false") return false;
|
|
@@ -5192,7 +6087,7 @@ function auditSink() {
|
|
|
5192
6087
|
function auditFilePath() {
|
|
5193
6088
|
const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
|
|
5194
6089
|
if (custom && custom.length > 0) return custom;
|
|
5195
|
-
return
|
|
6090
|
+
return join26(homedir(), ".skillwiki", "mcp-audit.jsonl");
|
|
5196
6091
|
}
|
|
5197
6092
|
function auditMcpToolCall(entry) {
|
|
5198
6093
|
if (!auditEnabled()) return;
|
|
@@ -5202,7 +6097,7 @@ function auditMcpToolCall(entry) {
|
|
|
5202
6097
|
return;
|
|
5203
6098
|
}
|
|
5204
6099
|
const path = auditFilePath();
|
|
5205
|
-
mkdirSync2(
|
|
6100
|
+
mkdirSync2(join26(path, ".."), { recursive: true });
|
|
5206
6101
|
appendFileSync(path, line, "utf8");
|
|
5207
6102
|
}
|
|
5208
6103
|
async function runMcpToolHandler(tool, input, fn) {
|
|
@@ -5371,6 +6266,59 @@ function registerMcpTools(server) {
|
|
|
5371
6266
|
return formatToolResult(r);
|
|
5372
6267
|
})
|
|
5373
6268
|
);
|
|
6269
|
+
server.registerTool(
|
|
6270
|
+
"skillwiki.sources_pending",
|
|
6271
|
+
{
|
|
6272
|
+
description: "List captured raw articles/papers awaiting typed integration (read-only).",
|
|
6273
|
+
inputSchema: z.object({
|
|
6274
|
+
...vaultFields,
|
|
6275
|
+
match: z.string().optional().describe("Literal title, URL, or path match"),
|
|
6276
|
+
scope: z.enum(["articles", "papers", "all"]).optional(),
|
|
6277
|
+
limit: z.number().int().positive().optional().describe("Max items (default 50)"),
|
|
6278
|
+
includeIntegrated: z.boolean().optional()
|
|
6279
|
+
})
|
|
6280
|
+
},
|
|
6281
|
+
async ({ vault, wiki, match, scope, limit, includeIntegrated }) => runMcpToolHandler("skillwiki.sources_pending", { vault, wiki }, async () => {
|
|
6282
|
+
const v = await resolveMcpVault({ vault, wiki });
|
|
6283
|
+
if (!v.ok) return formatToolResult({ exitCode: 25, result: v });
|
|
6284
|
+
const r = await runSourcesPending({
|
|
6285
|
+
vault: v.data.vault,
|
|
6286
|
+
match,
|
|
6287
|
+
scope,
|
|
6288
|
+
limit,
|
|
6289
|
+
includeIntegrated
|
|
6290
|
+
});
|
|
6291
|
+
return formatToolResult(r);
|
|
6292
|
+
})
|
|
6293
|
+
);
|
|
6294
|
+
server.registerTool(
|
|
6295
|
+
"skillwiki.compile_status",
|
|
6296
|
+
{
|
|
6297
|
+
description: "List compiling and review-open pending compile-turns (read-only).",
|
|
6298
|
+
inputSchema: z.object({
|
|
6299
|
+
...vaultFields
|
|
6300
|
+
})
|
|
6301
|
+
},
|
|
6302
|
+
async ({ vault, wiki }) => runMcpToolHandler("skillwiki.compile_status", { vault, wiki }, async () => {
|
|
6303
|
+
const v = await resolveMcpVault({ vault, wiki });
|
|
6304
|
+
if (!v.ok) return formatToolResult({ exitCode: 25, result: v });
|
|
6305
|
+
return formatToolResult(await runSourceCompileStatus({ vault: v.data.vault }));
|
|
6306
|
+
})
|
|
6307
|
+
);
|
|
6308
|
+
server.registerTool(
|
|
6309
|
+
"skillwiki.reviews",
|
|
6310
|
+
{
|
|
6311
|
+
description: "List open or needs-fix post-compile reviews (read-only).",
|
|
6312
|
+
inputSchema: z.object({
|
|
6313
|
+
...vaultFields
|
|
6314
|
+
})
|
|
6315
|
+
},
|
|
6316
|
+
async ({ vault, wiki }) => runMcpToolHandler("skillwiki.reviews", { vault, wiki }, async () => {
|
|
6317
|
+
const v = await resolveMcpVault({ vault, wiki });
|
|
6318
|
+
if (!v.ok) return formatToolResult({ exitCode: 25, result: v });
|
|
6319
|
+
return formatToolResult(await runSourceReviews({ vault: v.data.vault }));
|
|
6320
|
+
})
|
|
6321
|
+
);
|
|
5374
6322
|
}
|
|
5375
6323
|
|
|
5376
6324
|
// src/mcp/mutating-tools.ts
|
|
@@ -5422,8 +6370,8 @@ function registerMcpMutatingTools(server) {
|
|
|
5422
6370
|
}
|
|
5423
6371
|
|
|
5424
6372
|
// src/mcp/resources.ts
|
|
5425
|
-
import { readFile as
|
|
5426
|
-
import { join as
|
|
6373
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
6374
|
+
import { join as join28 } from "path";
|
|
5427
6375
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5428
6376
|
|
|
5429
6377
|
// src/mcp/lint-bucket.ts
|
|
@@ -5454,7 +6402,8 @@ var WARNING_KINDS = /* @__PURE__ */ new Set([
|
|
|
5454
6402
|
"work_item_health",
|
|
5455
6403
|
"orphaned_project_pages",
|
|
5456
6404
|
"missing_overview",
|
|
5457
|
-
"missing_diagram"
|
|
6405
|
+
"missing_diagram",
|
|
6406
|
+
"cycle_traps"
|
|
5458
6407
|
]);
|
|
5459
6408
|
function bucketSeverity(kind) {
|
|
5460
6409
|
if (ERROR_KINDS.has(kind)) return "error";
|
|
@@ -5551,9 +6500,9 @@ async function fetchQueryPreview(input) {
|
|
|
5551
6500
|
}
|
|
5552
6501
|
|
|
5553
6502
|
// src/mcp/graph-html.ts
|
|
5554
|
-
import { readFile as
|
|
5555
|
-
import { join as
|
|
5556
|
-
import { existsSync as
|
|
6503
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
6504
|
+
import { join as join27 } from "path";
|
|
6505
|
+
import { existsSync as existsSync21 } from "fs";
|
|
5557
6506
|
var TYPE_COLORS = {
|
|
5558
6507
|
entities: "#e74c3c",
|
|
5559
6508
|
concepts: "#27ae60",
|
|
@@ -5621,9 +6570,9 @@ ${nodeSvg}
|
|
|
5621
6570
|
return { html, node_count: nodes.length, edge_count: edges.length, truncated };
|
|
5622
6571
|
}
|
|
5623
6572
|
async function fetchGraphHtmlReport(input) {
|
|
5624
|
-
const graphPath = input.graphPath ??
|
|
6573
|
+
const graphPath = input.graphPath ?? join27(input.vault, ".skillwiki", "graph.json");
|
|
5625
6574
|
const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
|
|
5626
|
-
if (!
|
|
6575
|
+
if (!existsSync21(graphPath)) {
|
|
5627
6576
|
return {
|
|
5628
6577
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5629
6578
|
result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
|
|
@@ -5631,7 +6580,7 @@ async function fetchGraphHtmlReport(input) {
|
|
|
5631
6580
|
}
|
|
5632
6581
|
let raw;
|
|
5633
6582
|
try {
|
|
5634
|
-
raw = await
|
|
6583
|
+
raw = await readFile10(graphPath, "utf8");
|
|
5635
6584
|
} catch (e) {
|
|
5636
6585
|
return {
|
|
5637
6586
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -5695,7 +6644,7 @@ async function fetchStaleSummary(input) {
|
|
|
5695
6644
|
|
|
5696
6645
|
// src/mcp/resources.ts
|
|
5697
6646
|
async function readVaultFile(vault, rel) {
|
|
5698
|
-
return
|
|
6647
|
+
return readFile11(join28(vault, rel), "utf8");
|
|
5699
6648
|
}
|
|
5700
6649
|
async function tailLines(text, lines) {
|
|
5701
6650
|
const parts = text.split(/\r?\n/);
|
|
@@ -5781,9 +6730,9 @@ function registerMcpResources(server) {
|
|
|
5781
6730
|
if (!v.ok) {
|
|
5782
6731
|
return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
|
|
5783
6732
|
}
|
|
5784
|
-
const path =
|
|
6733
|
+
const path = join28(v.data.vault, ".skillwiki", "graph.json");
|
|
5785
6734
|
try {
|
|
5786
|
-
const raw = await
|
|
6735
|
+
const raw = await readFile11(path, "utf8");
|
|
5787
6736
|
const graph = JSON.parse(raw);
|
|
5788
6737
|
const adjacency = graph.adjacency ?? {};
|
|
5789
6738
|
const nodes = Object.keys(adjacency);
|
|
@@ -6071,6 +7020,35 @@ function registerMcpPrompts(server) {
|
|
|
6071
7020
|
]
|
|
6072
7021
|
})
|
|
6073
7022
|
);
|
|
7023
|
+
server.registerPrompt(
|
|
7024
|
+
"skillwiki-pending-review",
|
|
7025
|
+
{
|
|
7026
|
+
description: "Inspect pending sources and compile reviews without mutating the vault",
|
|
7027
|
+
argsSchema: {
|
|
7028
|
+
match: z3.string().optional().describe("Optional title/URL/path filter")
|
|
7029
|
+
}
|
|
7030
|
+
},
|
|
7031
|
+
({ match }) => ({
|
|
7032
|
+
messages: [
|
|
7033
|
+
{
|
|
7034
|
+
role: "user",
|
|
7035
|
+
content: {
|
|
7036
|
+
type: "text",
|
|
7037
|
+
text: [
|
|
7038
|
+
"List pending raw sources and open compile reviews.",
|
|
7039
|
+
match ? `Filter: ${match}` : "No extra filter.",
|
|
7040
|
+
"",
|
|
7041
|
+
"1. Call skillwiki.sources_pending (read-only).",
|
|
7042
|
+
"2. Call skillwiki.compile_status and skillwiki.reviews.",
|
|
7043
|
+
"3. Summarize what an attended session should compile next.",
|
|
7044
|
+
"",
|
|
7045
|
+
"Do not call mutating tools. Compile claim/publish stays on the interactive CLI."
|
|
7046
|
+
].join("\n")
|
|
7047
|
+
}
|
|
7048
|
+
}
|
|
7049
|
+
]
|
|
7050
|
+
})
|
|
7051
|
+
);
|
|
6074
7052
|
}
|
|
6075
7053
|
|
|
6076
7054
|
// src/mcp/server.ts
|
|
@@ -6108,13 +7086,13 @@ export {
|
|
|
6108
7086
|
runConfigSet,
|
|
6109
7087
|
runConfigList,
|
|
6110
7088
|
runConfigPath,
|
|
7089
|
+
detectFuseMount,
|
|
7090
|
+
snapshotterHealthChecks,
|
|
6111
7091
|
SATELLITE_STALE_MS,
|
|
6112
7092
|
satelliteLatestRunPath,
|
|
6113
7093
|
isFailedRunStatus,
|
|
6114
7094
|
readSatelliteLatestRunFromText,
|
|
6115
7095
|
evaluateSatelliteRunHealth,
|
|
6116
|
-
detectFuseMount,
|
|
6117
|
-
snapshotterHealthChecks,
|
|
6118
7096
|
runDoctor,
|
|
6119
7097
|
readCliPackageJson,
|
|
6120
7098
|
DEFAULT_DIRTY_VOLUME_THRESHOLD,
|
|
@@ -6131,6 +7109,16 @@ export {
|
|
|
6131
7109
|
runMemoryRecall,
|
|
6132
7110
|
runMemoryReview,
|
|
6133
7111
|
runMemoryImport,
|
|
7112
|
+
buildVectorIndex,
|
|
7113
|
+
vectorIndexStatus,
|
|
7114
|
+
reindexPageInVectorIndex,
|
|
7115
|
+
pruneVectorIndex,
|
|
6134
7116
|
runQuery,
|
|
7117
|
+
runSourceCompileClaim,
|
|
7118
|
+
runSourceCompileRelease,
|
|
7119
|
+
runSourceCompilePublished,
|
|
7120
|
+
runSourceReview,
|
|
7121
|
+
runSourceCompileStatus,
|
|
7122
|
+
runSourceReviews,
|
|
6135
7123
|
runSkillwikiMcpStdio
|
|
6136
7124
|
};
|