@theglitchking/semantic-pages 0.8.0 → 0.10.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/commands/healthcheck.md +6 -0
- package/commands/normalize-config.md +6 -0
- package/dist/cli/index.js +152 -106
- package/dist/cli/index.js.map +1 -1
- package/hooks/reconcile.js +157 -0
- package/hooks/session-start.js +12 -281
- package/package.json +3 -2
- package/scripts/link-skills.js +17 -197
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Official marketplace for semantic-pages - Semantic search + knowledge graph MCP server with auto-wiring for .claude/.vault and hit-em-with-the-docs companion",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.10.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "semantic-pages",
|
|
14
14
|
"description": "Semantic search + knowledge graph MCP server for markdown vaults. Auto-wires a read/write vault at .claude/.vault, and a read-only docs index at .documentation when hit-em-with-the-docs is also installed.",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.10.0",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TheGlitchKing"
|
|
18
18
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semantic-pages",
|
|
3
3
|
"description": "Semantic search + knowledge graph MCP server for markdown vaults. Auto-wires a read/write vault at .claude/.vault, and a read-only docs index at .documentation when hit-em-with-the-docs is also installed.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TheGlitchKing",
|
|
7
7
|
"email": "theglitchking@users.noreply.github.com"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Verify the local semantic-pages install starts cleanly; self-heals npx-cache ERR_MODULE_NOT_FOUND
|
|
3
|
+
allowed-tools: Bash(npx:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run `npx --no @theglitchking/semantic-pages healthcheck` and relay the output. If it reports a fragile `.mcp.json` form, suggest running `/semantic-pages:normalize-config`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rewrite fragile npx-@latest entries in .mcp.json to the stable node-against-node_modules form
|
|
3
|
+
allowed-tools: Bash(npx:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run `npx --no @theglitchking/semantic-pages normalize-config` and report the result to the user. Mention that `.mcp.json.bak` was created as a safety backup, and that the MCPs need to be toggled in `/mcp` to pick up the change.
|
package/dist/cli/index.js
CHANGED
|
@@ -2,85 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { program } from "commander";
|
|
5
|
-
import { resolve, dirname,
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync,
|
|
5
|
+
import { resolve, join, dirname, relative } from "path";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
-
import {
|
|
9
|
+
import { registerUpdateCommands } from "@theglitchking/claude-plugin-runtime";
|
|
10
10
|
var require_ = createRequire(import.meta.url);
|
|
11
11
|
var { version } = require_("../../package.json");
|
|
12
12
|
var PKG_NAME = "@theglitchking/semantic-pages";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return fallback;
|
|
13
|
+
function runRelink(cwd) {
|
|
14
|
+
const linker = join(cwd, "node_modules", "@theglitchking", "semantic-pages", "scripts", "link-skills.js");
|
|
15
|
+
const script = existsSync(linker) ? linker : resolve(process.cwd(), "scripts", "link-skills.js");
|
|
16
|
+
if (!existsSync(script)) {
|
|
17
|
+
console.error("link-skills.js not found \u2014 is the package installed?");
|
|
18
|
+
return;
|
|
20
19
|
}
|
|
20
|
+
spawnSync(process.execPath, [script], {
|
|
21
|
+
cwd,
|
|
22
|
+
env: { ...process.env, INIT_CWD: cwd },
|
|
23
|
+
stdio: "inherit"
|
|
24
|
+
});
|
|
21
25
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
function configPath(cwd = process.cwd()) {
|
|
27
|
-
return join(cwd, ".claude", "semantic-pages.json");
|
|
28
|
-
}
|
|
29
|
-
function currentPolicy(cwd = process.cwd()) {
|
|
30
|
-
const env = process.env.SEMANTIC_PAGES_UPDATE_POLICY;
|
|
31
|
-
if (env && VALID_POLICIES.includes(env)) return env;
|
|
32
|
-
const cfg = readJsonSafe(configPath(cwd));
|
|
33
|
-
const p = cfg?.updatePolicy;
|
|
34
|
-
if (p && VALID_POLICIES.includes(p)) return p;
|
|
35
|
-
return "nudge";
|
|
26
|
+
function findLocalBin(cwd) {
|
|
27
|
+
const p = join(cwd, "node_modules", "@theglitchking", "semantic-pages", "bin", "semantic-pages");
|
|
28
|
+
return existsSync(p) ? p : null;
|
|
36
29
|
}
|
|
37
|
-
function
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return
|
|
30
|
+
function localBinArg(cwd) {
|
|
31
|
+
const abs = findLocalBin(cwd);
|
|
32
|
+
if (!abs) return null;
|
|
33
|
+
const rel = relative(cwd, abs);
|
|
34
|
+
return rel.startsWith("..") ? abs : `./${rel}`;
|
|
42
35
|
}
|
|
43
|
-
|
|
44
|
-
const ctrl = new AbortController();
|
|
45
|
-
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
36
|
+
function readJsonSafe(path) {
|
|
46
37
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
headers: { accept: "application/json" }
|
|
50
|
-
});
|
|
51
|
-
if (!res.ok) return null;
|
|
52
|
-
const json = await res.json();
|
|
53
|
-
return json.version ?? null;
|
|
38
|
+
const raw = readFileSync(path, "utf8");
|
|
39
|
+
return raw.trim() ? JSON.parse(raw) : null;
|
|
54
40
|
} catch {
|
|
55
41
|
return null;
|
|
56
|
-
} finally {
|
|
57
|
-
clearTimeout(timer);
|
|
58
42
|
}
|
|
59
43
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
44
|
+
function isNpxForm(entry) {
|
|
45
|
+
if (!entry || typeof entry !== "object") return false;
|
|
46
|
+
const cmd = entry.command;
|
|
47
|
+
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
48
|
+
return cmd === "npx" && args.some((a) => typeof a === "string" && a.includes(PKG_NAME));
|
|
62
49
|
}
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
cwd,
|
|
73
|
-
env: { ...process.env, INIT_CWD: cwd },
|
|
74
|
-
stdio: "inherit"
|
|
75
|
-
});
|
|
76
|
-
return r2.status ?? 1;
|
|
77
|
-
}
|
|
78
|
-
const r = spawnSync(process.execPath, [linker], {
|
|
79
|
-
cwd,
|
|
80
|
-
env: { ...process.env, INIT_CWD: cwd },
|
|
81
|
-
stdio: "inherit"
|
|
82
|
-
});
|
|
83
|
-
return r.status ?? 1;
|
|
50
|
+
function extractNotesPath(entry) {
|
|
51
|
+
const args = Array.isArray(entry?.args) ? entry.args : [];
|
|
52
|
+
const i = args.indexOf("--notes");
|
|
53
|
+
if (i === -1 || i + 1 >= args.length) return null;
|
|
54
|
+
return String(args[i + 1]);
|
|
55
|
+
}
|
|
56
|
+
function extractExtraFlags(entry) {
|
|
57
|
+
const args = Array.isArray(entry?.args) ? entry.args : [];
|
|
58
|
+
return args.filter((a) => typeof a === "string" && a.startsWith("--") && a !== "--notes");
|
|
84
59
|
}
|
|
85
60
|
var TOOL_HELP = {
|
|
86
61
|
// Search
|
|
@@ -330,57 +305,128 @@ program.command("serve", { isDefault: true }).description("Start the MCP server
|
|
|
330
305
|
readOnly: opts.readOnly
|
|
331
306
|
});
|
|
332
307
|
});
|
|
333
|
-
program
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (r.status !== 0) {
|
|
339
|
-
console.error("npm update failed.");
|
|
340
|
-
process.exit(r.status ?? 1);
|
|
341
|
-
}
|
|
342
|
-
const after = installedVersion(cwd);
|
|
343
|
-
console.log(`Now: ${after ?? "(not installed)"}`);
|
|
344
|
-
if (after && before && after !== before) runRelink(cwd);
|
|
345
|
-
process.exit(0);
|
|
308
|
+
registerUpdateCommands(program, {
|
|
309
|
+
packageName: PKG_NAME,
|
|
310
|
+
pluginName: "semantic-pages",
|
|
311
|
+
configFile: "semantic-pages.json",
|
|
312
|
+
onAfterUpdate: (cwd) => runRelink(cwd)
|
|
346
313
|
});
|
|
347
|
-
program.command("
|
|
314
|
+
program.command("normalize-config").description(
|
|
315
|
+
"Rewrite fragile `npx @latest` entries in .mcp.json to the stable node-against-node_modules form (with backup and validation)"
|
|
316
|
+
).option("--dry-run", "print the proposed changes but don't write").action((opts) => {
|
|
348
317
|
const cwd = process.cwd();
|
|
349
|
-
|
|
350
|
-
|
|
318
|
+
const mcpPath = join(cwd, ".mcp.json");
|
|
319
|
+
if (!existsSync(mcpPath)) {
|
|
320
|
+
console.log("no .mcp.json in current directory \u2014 nothing to do");
|
|
351
321
|
process.exit(0);
|
|
352
322
|
}
|
|
353
|
-
|
|
354
|
-
|
|
323
|
+
const data = readJsonSafe(mcpPath);
|
|
324
|
+
if (!data || typeof data !== "object" || !data.mcpServers || typeof data.mcpServers !== "object") {
|
|
325
|
+
console.error(`could not parse ${mcpPath}`);
|
|
355
326
|
process.exit(1);
|
|
356
327
|
}
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
328
|
+
const bin = localBinArg(cwd);
|
|
329
|
+
if (!bin) {
|
|
330
|
+
console.error(
|
|
331
|
+
`no local install found at ./node_modules/@theglitchking/semantic-pages/bin/semantic-pages.
|
|
332
|
+
run 'npm install --save @theglitchking/semantic-pages' first, then re-run this command.`
|
|
333
|
+
);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
const rewritten = [];
|
|
337
|
+
for (const [key, entry] of Object.entries(data.mcpServers)) {
|
|
338
|
+
if (!isNpxForm(entry)) continue;
|
|
339
|
+
const notes = extractNotesPath(entry) ?? "./.claude/.vault";
|
|
340
|
+
const extra = extractExtraFlags(entry);
|
|
341
|
+
data.mcpServers[key] = {
|
|
342
|
+
type: "stdio",
|
|
343
|
+
command: "node",
|
|
344
|
+
args: [bin, "--notes", notes, ...extra]
|
|
345
|
+
};
|
|
346
|
+
rewritten.push(key);
|
|
347
|
+
}
|
|
348
|
+
if (rewritten.length === 0) {
|
|
349
|
+
console.log("no npx-form entries found \u2014 .mcp.json is already in the stable form.");
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
352
|
+
console.log(`Rewriting ${rewritten.length} entr${rewritten.length === 1 ? "y" : "ies"}: ${rewritten.join(", ")}`);
|
|
353
|
+
if (opts.dryRun) {
|
|
354
|
+
console.log("--- proposed .mcp.json:");
|
|
355
|
+
console.log(JSON.stringify(data, null, 2));
|
|
356
|
+
console.log("--- (dry-run; no changes written)");
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
const bakPath = join(dirname(mcpPath), ".mcp.json.bak");
|
|
360
|
+
try {
|
|
361
|
+
writeFileSync(bakPath, readFileSync(mcpPath, "utf8"));
|
|
362
|
+
console.log(`backup written: ${bakPath}`);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.error(`could not write backup: ${err.message}`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
writeFileSync(mcpPath, JSON.stringify(data, null, 2) + "\n");
|
|
368
|
+
const verify = spawnSync("node", [bin, "--version"], { cwd, stdio: "pipe", timeout: 15e3 });
|
|
369
|
+
if (verify.status !== 0) {
|
|
370
|
+
console.error(`verification failed (exit ${verify.status}):
|
|
371
|
+
${verify.stderr?.toString() || ""}`);
|
|
372
|
+
console.error(`rolling back from ${bakPath}`);
|
|
373
|
+
writeFileSync(mcpPath, readFileSync(bakPath, "utf8"));
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
console.log(`\u2713 .mcp.json normalized and verified. Toggle the MCPs in /mcp to reconnect.`);
|
|
362
377
|
process.exit(0);
|
|
363
378
|
});
|
|
364
|
-
program.command("
|
|
379
|
+
program.command("healthcheck").description(
|
|
380
|
+
"Verify the local install starts cleanly; self-heal common npx-cache corruption (ERR_MODULE_NOT_FOUND)"
|
|
381
|
+
).action(() => {
|
|
365
382
|
const cwd = process.cwd();
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
const bin = findLocalBin(cwd);
|
|
384
|
+
if (!bin) {
|
|
385
|
+
console.error(`no local install at ./node_modules/@theglitchking/semantic-pages/bin/semantic-pages`);
|
|
386
|
+
console.error(`run 'npm install --save @theglitchking/semantic-pages' to install.`);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
const mcpPath = join(cwd, ".mcp.json");
|
|
390
|
+
if (existsSync(mcpPath)) {
|
|
391
|
+
const data = readJsonSafe(mcpPath);
|
|
392
|
+
const entries = data?.mcpServers && typeof data.mcpServers === "object" ? Object.entries(data.mcpServers) : [];
|
|
393
|
+
const fragile = entries.filter(([, e]) => isNpxForm(e)).map(([k]) => k);
|
|
394
|
+
if (fragile.length > 0) {
|
|
395
|
+
console.warn(
|
|
396
|
+
`\u26A0\uFE0F .mcp.json uses the fragile npx-@latest form for: ${fragile.join(", ")}`
|
|
397
|
+
);
|
|
398
|
+
console.warn(` Rewrite to the stable form:`);
|
|
399
|
+
console.warn(` npx --no @theglitchking/semantic-pages normalize-config`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const r = spawnSync("node", [bin, "--version"], { cwd, stdio: "pipe", timeout: 15e3 });
|
|
403
|
+
if (r.status === 0) {
|
|
404
|
+
console.log(`\u2713 local install starts cleanly (v${r.stdout?.toString().trim()})`);
|
|
405
|
+
process.exit(0);
|
|
406
|
+
}
|
|
407
|
+
const stderr = r.stderr?.toString() ?? "";
|
|
408
|
+
if (stderr.includes("ERR_MODULE_NOT_FOUND")) {
|
|
409
|
+
const match = stderr.match(/([\/~][^'"\s]*\/_npx\/[^\/'"\s]+)/);
|
|
410
|
+
if (match) {
|
|
411
|
+
const bad = match[1];
|
|
412
|
+
console.warn(`detected broken npx cache at ${bad} \u2014 clearing and retrying...`);
|
|
413
|
+
try {
|
|
414
|
+
rmSync(bad, { recursive: true, force: true });
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
const r2 = spawnSync("node", [bin, "--version"], { cwd, stdio: "pipe", timeout: 15e3 });
|
|
418
|
+
if (r2.status === 0) {
|
|
419
|
+
console.log(`\u2713 cleared npx cache and verified (v${r2.stdout?.toString().trim()})`);
|
|
420
|
+
process.exit(0);
|
|
421
|
+
}
|
|
422
|
+
console.error(`retry still failed:
|
|
423
|
+
${r2.stderr?.toString() || ""}`);
|
|
424
|
+
} else {
|
|
425
|
+
console.error(`ERR_MODULE_NOT_FOUND but could not locate offending cache path in the error.`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
console.error(stderr || `local install failed (exit ${r.status})`);
|
|
429
|
+
process.exit(1);
|
|
384
430
|
});
|
|
385
431
|
program.parse();
|
|
386
432
|
//# sourceMappingURL=index.js.map
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { program } from \"commander\";\nimport { resolve, dirname, join } from \"node:path\";\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { spawnSync } from \"node:child_process\";\nimport { fileURLToPath } from \"node:url\";\n\nconst require_ = createRequire(import.meta.url);\nconst { version } = require_(\"../../package.json\") as { version: string };\n\nconst PKG_NAME = \"@theglitchking/semantic-pages\";\nconst VALID_POLICIES = [\"auto\", \"nudge\", \"off\"] as const;\ntype Policy = (typeof VALID_POLICIES)[number];\n\nfunction readJsonSafe<T = unknown>(p: string, fallback: T | null = null): T | null {\n try {\n const raw = readFileSync(p, \"utf8\");\n return raw.trim() ? (JSON.parse(raw) as T) : fallback;\n } catch {\n return fallback;\n }\n}\n\nfunction writeJsonFile(p: string, value: unknown) {\n mkdirSync(dirname(p), { recursive: true });\n writeFileSync(p, JSON.stringify(value, null, 2) + \"\\n\");\n}\n\nfunction configPath(cwd = process.cwd()) {\n return join(cwd, \".claude\", \"semantic-pages.json\");\n}\n\nfunction currentPolicy(cwd = process.cwd()): Policy {\n const env = process.env.SEMANTIC_PAGES_UPDATE_POLICY as Policy | undefined;\n if (env && (VALID_POLICIES as readonly string[]).includes(env)) return env;\n const cfg = readJsonSafe<{ updatePolicy?: Policy }>(configPath(cwd));\n const p = cfg?.updatePolicy;\n if (p && (VALID_POLICIES as readonly string[]).includes(p)) return p;\n return \"nudge\";\n}\n\nfunction installedVersion(cwd = process.cwd()): string | null {\n const localPkg = join(cwd, \"node_modules\", \"@theglitchking\", \"semantic-pages\", \"package.json\");\n const pkg = readJsonSafe<{ version?: string }>(localPkg);\n if (pkg?.version) return pkg.version;\n // fallback: our own package.json (when running via plugin dir)\n return version;\n}\n\nasync function fetchLatestVersion(timeoutMs = 5000): Promise<string | null> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeoutMs);\n try {\n const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, {\n signal: ctrl.signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) return null;\n const json = (await res.json()) as { version?: string };\n return json.version ?? null;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction runNpmUpdate(cwd: string) {\n return spawnSync(\"npm\", [\"update\", PKG_NAME], { cwd, stdio: \"inherit\" });\n}\n\nfunction runRelink(cwd: string) {\n const linker = join(cwd, \"node_modules\", \"@theglitchking\", \"semantic-pages\", \"scripts\", \"link-skills.js\");\n if (!existsSync(linker)) {\n // Running against our own repo — point at the source linker.\n const localLinker = resolve(fileURLToPath(import.meta.url), \"..\", \"..\", \"..\", \"scripts\", \"link-skills.js\");\n if (!existsSync(localLinker)) {\n console.error(\"link-skills.js not found — is the package installed?\");\n return 1;\n }\n const r = spawnSync(process.execPath, [localLinker], {\n cwd,\n env: { ...process.env, INIT_CWD: cwd },\n stdio: \"inherit\",\n });\n return r.status ?? 1;\n }\n const r = spawnSync(process.execPath, [linker], {\n cwd,\n env: { ...process.env, INIT_CWD: cwd },\n stdio: \"inherit\",\n });\n return r.status ?? 1;\n}\n\nconst TOOL_HELP: Record<string, { description: string; args: string; examples: string[] }> = {\n // Search\n search_semantic: {\n description: \"Vector similarity search — find notes by meaning, not just keywords\",\n args: '{ \"query\": \"string\", \"limit?\": 10 }',\n examples: [\n '{ \"query\": \"microservices architecture\", \"limit\": 5 }',\n '{ \"query\": \"how to deploy to production\" }',\n ],\n },\n search_text: {\n description: \"Full-text keyword or regex search with optional filters\",\n args: '{ \"pattern\": \"string\", \"regex?\": false, \"caseSensitive?\": false, \"pathGlob?\": \"string\", \"tagFilter?\": [\"string\"], \"limit?\": 20 }',\n examples: [\n '{ \"pattern\": \"RabbitMQ\" }',\n '{ \"pattern\": \"OAuth\\\\\\\\d\", \"regex\": true }',\n '{ \"pattern\": \"deploy\", \"pathGlob\": \"devops/**\", \"tagFilter\": [\"kubernetes\"] }',\n ],\n },\n search_graph: {\n description: \"Graph traversal — find notes connected to a concept via wikilinks and tags\",\n args: '{ \"concept\": \"string\", \"maxDepth?\": 2 }',\n examples: [\n '{ \"concept\": \"microservices\" }',\n '{ \"concept\": \"auth\", \"maxDepth\": 3 }',\n ],\n },\n search_hybrid: {\n description: \"Combined semantic + graph search — vector results re-ranked by graph proximity\",\n args: '{ \"query\": \"string\", \"limit?\": 10 }',\n examples: [\n '{ \"query\": \"event driven architecture\", \"limit\": 5 }',\n ],\n },\n\n // Read\n read_note: {\n description: \"Read the full content of a specific note by path\",\n args: '{ \"path\": \"string\" }',\n examples: [\n '{ \"path\": \"project-overview.md\" }',\n '{ \"path\": \"notes/meeting-2024-01-15.md\" }',\n ],\n },\n read_multiple_notes: {\n description: \"Batch read multiple notes in one call\",\n args: '{ \"paths\": [\"string\"] }',\n examples: [\n '{ \"paths\": [\"overview.md\", \"architecture.md\", \"deployment.md\"] }',\n ],\n },\n list_notes: {\n description: \"List all indexed notes with metadata (title, tags, link count)\",\n args: \"{}\",\n examples: [\"{}\"],\n },\n\n // Write\n create_note: {\n description: \"Create a new markdown note with optional YAML frontmatter\",\n args: '{ \"path\": \"string\", \"content\": \"string\", \"frontmatter?\": {} }',\n examples: [\n '{ \"path\": \"new-guide.md\", \"content\": \"# Guide\\\\n\\\\nContent here.\" }',\n '{ \"path\": \"tagged.md\", \"content\": \"Content.\", \"frontmatter\": { \"title\": \"Tagged Note\", \"tags\": [\"test\"] } }',\n ],\n },\n update_note: {\n description: \"Edit note content — overwrite, append, prepend, or patch by heading\",\n args: '{ \"path\": \"string\", \"content\": \"string\", \"mode\": \"overwrite|append|prepend|patch-by-heading\", \"heading?\": \"string\" }',\n examples: [\n '{ \"path\": \"guide.md\", \"content\": \"New content.\", \"mode\": \"overwrite\" }',\n '{ \"path\": \"guide.md\", \"content\": \"\\\\n## Appendix\\\\nExtra info.\", \"mode\": \"append\" }',\n '{ \"path\": \"guide.md\", \"content\": \"Updated architecture section.\", \"mode\": \"patch-by-heading\", \"heading\": \"Architecture\" }',\n ],\n },\n delete_note: {\n description: \"Delete a note permanently (requires confirm=true)\",\n args: '{ \"path\": \"string\", \"confirm\": true }',\n examples: [\n '{ \"path\": \"old-note.md\", \"confirm\": true }',\n '{ \"path\": \"old-note.md\", \"confirm\": false } // returns warning, does not delete',\n ],\n },\n move_note: {\n description: \"Move or rename a note — automatically updates wikilinks across the vault\",\n args: '{ \"from\": \"string\", \"to\": \"string\" }',\n examples: [\n '{ \"from\": \"user-service.md\", \"to\": \"auth-service.md\" }',\n '{ \"from\": \"old/note.md\", \"to\": \"new/location/note.md\" }',\n ],\n },\n\n // Metadata\n get_frontmatter: {\n description: \"Read parsed YAML frontmatter from a note as JSON\",\n args: '{ \"path\": \"string\" }',\n examples: ['{ \"path\": \"project-overview.md\" }'],\n },\n update_frontmatter: {\n description: \"Set or delete YAML frontmatter keys — pass null to delete a key\",\n args: '{ \"path\": \"string\", \"fields\": {} }',\n examples: [\n '{ \"path\": \"note.md\", \"fields\": { \"status\": \"active\", \"priority\": 1 } }',\n '{ \"path\": \"note.md\", \"fields\": { \"deprecated_field\": null } } // deletes the key',\n ],\n },\n manage_tags: {\n description: \"Add, remove, or list tags on a note (frontmatter and inline)\",\n args: '{ \"path\": \"string\", \"action\": \"add|remove|list\", \"tags?\": [\"string\"] }',\n examples: [\n '{ \"path\": \"note.md\", \"action\": \"list\" }',\n '{ \"path\": \"note.md\", \"action\": \"add\", \"tags\": [\"important\", \"reviewed\"] }',\n '{ \"path\": \"note.md\", \"action\": \"remove\", \"tags\": [\"draft\"] }',\n ],\n },\n rename_tag: {\n description: \"Rename a tag across all notes in the vault (frontmatter + inline)\",\n args: '{ \"oldTag\": \"string\", \"newTag\": \"string\" }',\n examples: ['{ \"oldTag\": \"architecture\", \"newTag\": \"arch\" }'],\n },\n\n // Graph\n backlinks: {\n description: \"Find all notes that link TO a given note via [[wikilinks]]\",\n args: '{ \"path\": \"string\" }',\n examples: ['{ \"path\": \"microservices.md\" }'],\n },\n forwardlinks: {\n description: \"Find all notes linked FROM a given note\",\n args: '{ \"path\": \"string\" }',\n examples: ['{ \"path\": \"project-overview.md\" }'],\n },\n graph_path: {\n description: \"Find the shortest path between two notes in the knowledge graph\",\n args: '{ \"from\": \"string\", \"to\": \"string\" }',\n examples: ['{ \"from\": \"project-overview.md\", \"to\": \"user-service.md\" }'],\n },\n graph_statistics: {\n description: \"Knowledge graph stats — most connected nodes, orphans, density\",\n args: \"{}\",\n examples: [\"{}\"],\n },\n\n // System\n get_stats: {\n description: \"Vault and index statistics — note count, chunks, embeddings, graph density\",\n args: \"{}\",\n examples: [\"{}\"],\n },\n reindex: {\n description: \"Force a full reindex of the vault\",\n args: \"{}\",\n examples: [\"{}\"],\n },\n};\n\nconst TOOL_CATEGORIES: Record<string, string[]> = {\n Search: [\"search_semantic\", \"search_text\", \"search_graph\", \"search_hybrid\"],\n Read: [\"read_note\", \"read_multiple_notes\", \"list_notes\"],\n Write: [\"create_note\", \"update_note\", \"delete_note\", \"move_note\"],\n Metadata: [\"get_frontmatter\", \"update_frontmatter\", \"manage_tags\", \"rename_tag\"],\n Graph: [\"backlinks\", \"forwardlinks\", \"graph_path\", \"graph_statistics\"],\n System: [\"get_stats\", \"reindex\"],\n};\n\nfunction printToolList() {\n console.log(\"\\nSemantic Pages — 21 MCP Tools\\n\");\n console.log(\"Usage: These tools are available via MCP when the server is running.\");\n console.log(\" Run `semantic-pages tools <name>` for details on a specific tool.\\n\");\n\n for (const [category, tools] of Object.entries(TOOL_CATEGORIES)) {\n console.log(` ${category}:`);\n for (const name of tools) {\n const tool = TOOL_HELP[name];\n console.log(` ${name.padEnd(24)} ${tool.description}`);\n }\n console.log();\n }\n\n console.log(\"Run `semantic-pages tools <tool-name>` for arguments and examples.\");\n}\n\nfunction printToolDetail(name: string) {\n const tool = TOOL_HELP[name];\n if (!tool) {\n console.error(`Unknown tool: ${name}`);\n console.error(`Run \\`semantic-pages tools\\` to see all available tools.`);\n process.exit(1);\n }\n\n console.log(`\\n ${name}`);\n console.log(` ${\"─\".repeat(name.length)}`);\n console.log(` ${tool.description}\\n`);\n console.log(` Arguments:`);\n console.log(` ${tool.args}\\n`);\n console.log(` Examples:`);\n for (const ex of tool.examples) {\n console.log(` ${ex}`);\n }\n console.log();\n}\n\nprogram\n .name(\"semantic-pages\")\n .description(\n \"Semantic search + knowledge graph MCP server for markdown files\\n\\n\" +\n \" Start MCP server: semantic-pages --notes ./vault\\n\" +\n \" Show vault stats: semantic-pages --notes ./vault --stats\\n\" +\n \" Force reindex: semantic-pages --notes ./vault --reindex\\n\" +\n \" List MCP tools: semantic-pages tools\\n\" +\n \" Tool details: semantic-pages tools search_semantic\"\n )\n .version(version);\n\nprogram\n .command(\"tools [name]\")\n .description(\"List all MCP tools, or show details for a specific tool\")\n .action((name?: string) => {\n if (name) {\n printToolDetail(name);\n } else {\n printToolList();\n }\n process.exit(0);\n });\n\nprogram\n .command(\"serve\", { isDefault: true })\n .description(\"Start the MCP server (default command)\")\n .requiredOption(\"--notes <path>\", \"Path to markdown notes directory\")\n .option(\"--reindex\", \"Force full reindex and exit\")\n .option(\"--stats\", \"Show vault statistics and exit\")\n .option(\"--wait-for-ready\", \"Block startup until index is fully built before serving (default: index in background; tools return 'Indexing in progress' until ready)\")\n .option(\"--read-only\", \"Suppress write tools (create_note, update_note, delete_note, move_note, update_frontmatter, manage_tags, rename_tag) — use for shared docs vaults owned by another tool\")\n .option(\"--model <name>\", \"Embedding model to use (default: all-MiniLM-L6-v2, fast; use nomic-ai/nomic-embed-text-v1.5 for higher quality)\")\n .option(\"--workers <n>\", \"Number of worker threads for parallel embedding\", parseInt)\n .option(\"--batch-size <n>\", \"Texts per ONNX forward pass (default: 8)\", parseInt)\n .option(\"--no-quantized\", \"Use full-precision model instead of quantized (slower, slightly higher quality)\")\n .option(\"--no-watch\", \"Disable file watcher\")\n .action(async (opts) => {\n const notesPath = resolve(opts.notes);\n\n if (!existsSync(notesPath)) {\n console.error(`Error: notes directory not found: ${notesPath}`);\n process.exit(1);\n }\n\n if (opts.stats) {\n const { Indexer } = await import(\"../core/indexer.js\");\n const indexer = new Indexer(notesPath);\n const docs = await indexer.indexAll();\n console.log(`Notes: ${docs.length}`);\n console.log(`Chunks: ${docs.reduce((n: number, d: any) => n + d.chunks.length, 0)}`);\n console.log(`Wikilinks: ${docs.reduce((n: number, d: any) => n + d.wikilinks.length, 0)}`);\n console.log(`Tags: ${new Set(docs.flatMap((d: any) => d.tags)).size} unique`);\n process.exit(0);\n }\n\n if (opts.reindex) {\n const { createServer } = await import(\"../mcp/server.js\");\n await createServer(notesPath, {\n watch: false,\n waitForReady: true,\n model: opts.model,\n workers: opts.workers,\n batchSize: opts.batchSize,\n quantized: opts.quantized,\n onProgress: (embedded, total) => {\n process.stderr.write(`\\rEmbedding ${embedded}/${total} chunks...`);\n },\n });\n process.stderr.write(\"\\n\");\n console.log(\"Reindex complete.\");\n process.exit(0);\n }\n\n // Default: start MCP server on stdio\n const { startServer } = await import(\"../mcp/server.js\");\n await startServer(notesPath, {\n watch: opts.watch,\n waitForReady: opts.waitForReady,\n model: opts.model,\n workers: opts.workers,\n batchSize: opts.batchSize,\n quantized: opts.quantized,\n readOnly: opts.readOnly,\n });\n });\n\nprogram\n .command(\"update\")\n .description(\"Update semantic-pages to the latest version (project-local install)\")\n .action(async () => {\n const cwd = process.cwd();\n const before = installedVersion(cwd);\n console.log(`Current: ${before ?? \"(not installed)\"}`);\n const r = runNpmUpdate(cwd);\n if (r.status !== 0) {\n console.error(\"npm update failed.\");\n process.exit(r.status ?? 1);\n }\n const after = installedVersion(cwd);\n console.log(`Now: ${after ?? \"(not installed)\"}`);\n if (after && before && after !== before) runRelink(cwd);\n process.exit(0);\n });\n\nprogram\n .command(\"policy [mode]\")\n .description(`Show or set the update policy (${VALID_POLICIES.join(\" | \")})`)\n .action((mode?: string) => {\n const cwd = process.cwd();\n if (!mode) {\n console.log(`updatePolicy = ${currentPolicy(cwd)} (${configPath(cwd)})`);\n process.exit(0);\n }\n if (!(VALID_POLICIES as readonly string[]).includes(mode)) {\n console.error(`Invalid policy: ${mode}. Valid: ${VALID_POLICIES.join(\", \")}`);\n process.exit(1);\n }\n const p = configPath(cwd);\n const cfg = readJsonSafe<Record<string, unknown>>(p) ?? {};\n cfg.updatePolicy = mode;\n writeJsonFile(p, cfg);\n console.log(`updatePolicy = ${mode} (${p})`);\n process.exit(0);\n });\n\nprogram\n .command(\"status\")\n .description(\"Show installed version, latest available, current policy, and hook registration state\")\n .action(async () => {\n const cwd = process.cwd();\n const current = installedVersion(cwd);\n const latest = await fetchLatestVersion();\n const policy = currentPolicy(cwd);\n const settings = readJsonSafe<{ hooks?: { SessionStart?: Array<{ hooks?: Array<{ command?: string }> }> } }>(\n join(cwd, \".claude\", \"settings.json\"),\n );\n const hookRegistered = !!settings?.hooks?.SessionStart?.some((g) =>\n (g?.hooks || []).some((h) => typeof h?.command === \"string\" && h.command.includes(\"semantic-pages\")),\n );\n console.log(`semantic-pages status`);\n console.log(` installed: ${current ?? \"(not installed)\"}`);\n console.log(` latest: ${latest ?? \"(unknown)\"}`);\n console.log(` policy: ${policy}`);\n console.log(` hook: ${hookRegistered ? \"registered in .claude/settings.json\" : \"not in .claude/settings.json\"}`);\n process.exit(0);\n });\n\nprogram\n .command(\"relink\")\n .description(\"Re-run the skill linker (symlinks bundled skills into .claude/skills/)\")\n .action(() => {\n process.exit(runRelink(process.cwd()));\n });\n\nprogram.parse();\n"],"mappings":"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","names":["r"]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { program } from \"commander\";\nimport { resolve, join, dirname, relative } from \"node:path\";\nimport { existsSync, readFileSync, writeFileSync, rmSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { spawnSync } from \"node:child_process\";\nimport { registerUpdateCommands } from \"@theglitchking/claude-plugin-runtime\";\n\nconst require_ = createRequire(import.meta.url);\nconst { version } = require_(\"../../package.json\") as { version: string };\n\nconst PKG_NAME = \"@theglitchking/semantic-pages\";\n\nfunction runRelink(cwd: string) {\n const linker = join(cwd, \"node_modules\", \"@theglitchking\", \"semantic-pages\", \"scripts\", \"link-skills.js\");\n const script = existsSync(linker) ? linker : resolve(process.cwd(), \"scripts\", \"link-skills.js\");\n if (!existsSync(script)) {\n console.error(\"link-skills.js not found — is the package installed?\");\n return;\n }\n spawnSync(process.execPath, [script], {\n cwd,\n env: { ...process.env, INIT_CWD: cwd },\n stdio: \"inherit\",\n });\n}\n\nfunction findLocalBin(cwd: string): string | null {\n const p = join(cwd, \"node_modules\", \"@theglitchking\", \"semantic-pages\", \"bin\", \"semantic-pages\");\n return existsSync(p) ? p : null;\n}\n\nfunction localBinArg(cwd: string): string | null {\n const abs = findLocalBin(cwd);\n if (!abs) return null;\n const rel = relative(cwd, abs);\n return rel.startsWith(\"..\") ? abs : `./${rel}`;\n}\n\nfunction readJsonSafe(path: string): any {\n try {\n const raw = readFileSync(path, \"utf8\");\n return raw.trim() ? JSON.parse(raw) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Returns the fragile npx-form pattern we used to write in pre-0.10.0\n * SessionStart hooks, so normalize-config can detect and rewrite it.\n */\nfunction isNpxForm(entry: any): boolean {\n if (!entry || typeof entry !== \"object\") return false;\n const cmd = entry.command;\n const args = Array.isArray(entry.args) ? entry.args : [];\n return cmd === \"npx\" && args.some((a: unknown) => typeof a === \"string\" && a.includes(PKG_NAME));\n}\n\nfunction isLocalForm(entry: any): boolean {\n if (!entry || typeof entry !== \"object\") return false;\n const cmd = entry.command;\n const args = Array.isArray(entry.args) ? entry.args : [];\n return (\n cmd === \"node\" &&\n args.some((a: unknown) => typeof a === \"string\" && a.includes(\"node_modules/@theglitchking/semantic-pages\"))\n );\n}\n\nfunction extractNotesPath(entry: any): string | null {\n const args = Array.isArray(entry?.args) ? entry.args : [];\n const i = args.indexOf(\"--notes\");\n if (i === -1 || i + 1 >= args.length) return null;\n return String(args[i + 1]);\n}\n\nfunction extractExtraFlags(entry: any): string[] {\n const args = Array.isArray(entry?.args) ? entry.args : [];\n return args.filter((a: unknown): a is string => typeof a === \"string\" && a.startsWith(\"--\") && a !== \"--notes\");\n}\n\nconst TOOL_HELP: Record<string, { description: string; args: string; examples: string[] }> = {\n // Search\n search_semantic: {\n description: \"Vector similarity search — find notes by meaning, not just keywords\",\n args: '{ \"query\": \"string\", \"limit?\": 10 }',\n examples: [\n '{ \"query\": \"microservices architecture\", \"limit\": 5 }',\n '{ \"query\": \"how to deploy to production\" }',\n ],\n },\n search_text: {\n description: \"Full-text keyword or regex search with optional filters\",\n args: '{ \"pattern\": \"string\", \"regex?\": false, \"caseSensitive?\": false, \"pathGlob?\": \"string\", \"tagFilter?\": [\"string\"], \"limit?\": 20 }',\n examples: [\n '{ \"pattern\": \"RabbitMQ\" }',\n '{ \"pattern\": \"OAuth\\\\\\\\d\", \"regex\": true }',\n '{ \"pattern\": \"deploy\", \"pathGlob\": \"devops/**\", \"tagFilter\": [\"kubernetes\"] }',\n ],\n },\n search_graph: {\n description: \"Graph traversal — find notes connected to a concept via wikilinks and tags\",\n args: '{ \"concept\": \"string\", \"maxDepth?\": 2 }',\n examples: [\n '{ \"concept\": \"microservices\" }',\n '{ \"concept\": \"auth\", \"maxDepth\": 3 }',\n ],\n },\n search_hybrid: {\n description: \"Combined semantic + graph search — vector results re-ranked by graph proximity\",\n args: '{ \"query\": \"string\", \"limit?\": 10 }',\n examples: [\n '{ \"query\": \"event driven architecture\", \"limit\": 5 }',\n ],\n },\n\n // Read\n read_note: {\n description: \"Read the full content of a specific note by path\",\n args: '{ \"path\": \"string\" }',\n examples: [\n '{ \"path\": \"project-overview.md\" }',\n '{ \"path\": \"notes/meeting-2024-01-15.md\" }',\n ],\n },\n read_multiple_notes: {\n description: \"Batch read multiple notes in one call\",\n args: '{ \"paths\": [\"string\"] }',\n examples: [\n '{ \"paths\": [\"overview.md\", \"architecture.md\", \"deployment.md\"] }',\n ],\n },\n list_notes: {\n description: \"List all indexed notes with metadata (title, tags, link count)\",\n args: \"{}\",\n examples: [\"{}\"],\n },\n\n // Write\n create_note: {\n description: \"Create a new markdown note with optional YAML frontmatter\",\n args: '{ \"path\": \"string\", \"content\": \"string\", \"frontmatter?\": {} }',\n examples: [\n '{ \"path\": \"new-guide.md\", \"content\": \"# Guide\\\\n\\\\nContent here.\" }',\n '{ \"path\": \"tagged.md\", \"content\": \"Content.\", \"frontmatter\": { \"title\": \"Tagged Note\", \"tags\": [\"test\"] } }',\n ],\n },\n update_note: {\n description: \"Edit note content — overwrite, append, prepend, or patch by heading\",\n args: '{ \"path\": \"string\", \"content\": \"string\", \"mode\": \"overwrite|append|prepend|patch-by-heading\", \"heading?\": \"string\" }',\n examples: [\n '{ \"path\": \"guide.md\", \"content\": \"New content.\", \"mode\": \"overwrite\" }',\n '{ \"path\": \"guide.md\", \"content\": \"\\\\n## Appendix\\\\nExtra info.\", \"mode\": \"append\" }',\n '{ \"path\": \"guide.md\", \"content\": \"Updated architecture section.\", \"mode\": \"patch-by-heading\", \"heading\": \"Architecture\" }',\n ],\n },\n delete_note: {\n description: \"Delete a note permanently (requires confirm=true)\",\n args: '{ \"path\": \"string\", \"confirm\": true }',\n examples: [\n '{ \"path\": \"old-note.md\", \"confirm\": true }',\n '{ \"path\": \"old-note.md\", \"confirm\": false } // returns warning, does not delete',\n ],\n },\n move_note: {\n description: \"Move or rename a note — automatically updates wikilinks across the vault\",\n args: '{ \"from\": \"string\", \"to\": \"string\" }',\n examples: [\n '{ \"from\": \"user-service.md\", \"to\": \"auth-service.md\" }',\n '{ \"from\": \"old/note.md\", \"to\": \"new/location/note.md\" }',\n ],\n },\n\n // Metadata\n get_frontmatter: {\n description: \"Read parsed YAML frontmatter from a note as JSON\",\n args: '{ \"path\": \"string\" }',\n examples: ['{ \"path\": \"project-overview.md\" }'],\n },\n update_frontmatter: {\n description: \"Set or delete YAML frontmatter keys — pass null to delete a key\",\n args: '{ \"path\": \"string\", \"fields\": {} }',\n examples: [\n '{ \"path\": \"note.md\", \"fields\": { \"status\": \"active\", \"priority\": 1 } }',\n '{ \"path\": \"note.md\", \"fields\": { \"deprecated_field\": null } } // deletes the key',\n ],\n },\n manage_tags: {\n description: \"Add, remove, or list tags on a note (frontmatter and inline)\",\n args: '{ \"path\": \"string\", \"action\": \"add|remove|list\", \"tags?\": [\"string\"] }',\n examples: [\n '{ \"path\": \"note.md\", \"action\": \"list\" }',\n '{ \"path\": \"note.md\", \"action\": \"add\", \"tags\": [\"important\", \"reviewed\"] }',\n '{ \"path\": \"note.md\", \"action\": \"remove\", \"tags\": [\"draft\"] }',\n ],\n },\n rename_tag: {\n description: \"Rename a tag across all notes in the vault (frontmatter + inline)\",\n args: '{ \"oldTag\": \"string\", \"newTag\": \"string\" }',\n examples: ['{ \"oldTag\": \"architecture\", \"newTag\": \"arch\" }'],\n },\n\n // Graph\n backlinks: {\n description: \"Find all notes that link TO a given note via [[wikilinks]]\",\n args: '{ \"path\": \"string\" }',\n examples: ['{ \"path\": \"microservices.md\" }'],\n },\n forwardlinks: {\n description: \"Find all notes linked FROM a given note\",\n args: '{ \"path\": \"string\" }',\n examples: ['{ \"path\": \"project-overview.md\" }'],\n },\n graph_path: {\n description: \"Find the shortest path between two notes in the knowledge graph\",\n args: '{ \"from\": \"string\", \"to\": \"string\" }',\n examples: ['{ \"from\": \"project-overview.md\", \"to\": \"user-service.md\" }'],\n },\n graph_statistics: {\n description: \"Knowledge graph stats — most connected nodes, orphans, density\",\n args: \"{}\",\n examples: [\"{}\"],\n },\n\n // System\n get_stats: {\n description: \"Vault and index statistics — note count, chunks, embeddings, graph density\",\n args: \"{}\",\n examples: [\"{}\"],\n },\n reindex: {\n description: \"Force a full reindex of the vault\",\n args: \"{}\",\n examples: [\"{}\"],\n },\n};\n\nconst TOOL_CATEGORIES: Record<string, string[]> = {\n Search: [\"search_semantic\", \"search_text\", \"search_graph\", \"search_hybrid\"],\n Read: [\"read_note\", \"read_multiple_notes\", \"list_notes\"],\n Write: [\"create_note\", \"update_note\", \"delete_note\", \"move_note\"],\n Metadata: [\"get_frontmatter\", \"update_frontmatter\", \"manage_tags\", \"rename_tag\"],\n Graph: [\"backlinks\", \"forwardlinks\", \"graph_path\", \"graph_statistics\"],\n System: [\"get_stats\", \"reindex\"],\n};\n\nfunction printToolList() {\n console.log(\"\\nSemantic Pages — 21 MCP Tools\\n\");\n console.log(\"Usage: These tools are available via MCP when the server is running.\");\n console.log(\" Run `semantic-pages tools <name>` for details on a specific tool.\\n\");\n\n for (const [category, tools] of Object.entries(TOOL_CATEGORIES)) {\n console.log(` ${category}:`);\n for (const name of tools) {\n const tool = TOOL_HELP[name];\n console.log(` ${name.padEnd(24)} ${tool.description}`);\n }\n console.log();\n }\n\n console.log(\"Run `semantic-pages tools <tool-name>` for arguments and examples.\");\n}\n\nfunction printToolDetail(name: string) {\n const tool = TOOL_HELP[name];\n if (!tool) {\n console.error(`Unknown tool: ${name}`);\n console.error(`Run \\`semantic-pages tools\\` to see all available tools.`);\n process.exit(1);\n }\n\n console.log(`\\n ${name}`);\n console.log(` ${\"─\".repeat(name.length)}`);\n console.log(` ${tool.description}\\n`);\n console.log(` Arguments:`);\n console.log(` ${tool.args}\\n`);\n console.log(` Examples:`);\n for (const ex of tool.examples) {\n console.log(` ${ex}`);\n }\n console.log();\n}\n\nprogram\n .name(\"semantic-pages\")\n .description(\n \"Semantic search + knowledge graph MCP server for markdown files\\n\\n\" +\n \" Start MCP server: semantic-pages --notes ./vault\\n\" +\n \" Show vault stats: semantic-pages --notes ./vault --stats\\n\" +\n \" Force reindex: semantic-pages --notes ./vault --reindex\\n\" +\n \" List MCP tools: semantic-pages tools\\n\" +\n \" Tool details: semantic-pages tools search_semantic\"\n )\n .version(version);\n\nprogram\n .command(\"tools [name]\")\n .description(\"List all MCP tools, or show details for a specific tool\")\n .action((name?: string) => {\n if (name) {\n printToolDetail(name);\n } else {\n printToolList();\n }\n process.exit(0);\n });\n\nprogram\n .command(\"serve\", { isDefault: true })\n .description(\"Start the MCP server (default command)\")\n .requiredOption(\"--notes <path>\", \"Path to markdown notes directory\")\n .option(\"--reindex\", \"Force full reindex and exit\")\n .option(\"--stats\", \"Show vault statistics and exit\")\n .option(\"--wait-for-ready\", \"Block startup until index is fully built before serving (default: index in background; tools return 'Indexing in progress' until ready)\")\n .option(\"--read-only\", \"Suppress write tools (create_note, update_note, delete_note, move_note, update_frontmatter, manage_tags, rename_tag) — use for shared docs vaults owned by another tool\")\n .option(\"--model <name>\", \"Embedding model to use (default: all-MiniLM-L6-v2, fast; use nomic-ai/nomic-embed-text-v1.5 for higher quality)\")\n .option(\"--workers <n>\", \"Number of worker threads for parallel embedding\", parseInt)\n .option(\"--batch-size <n>\", \"Texts per ONNX forward pass (default: 8)\", parseInt)\n .option(\"--no-quantized\", \"Use full-precision model instead of quantized (slower, slightly higher quality)\")\n .option(\"--no-watch\", \"Disable file watcher\")\n .action(async (opts) => {\n const notesPath = resolve(opts.notes);\n\n if (!existsSync(notesPath)) {\n console.error(`Error: notes directory not found: ${notesPath}`);\n process.exit(1);\n }\n\n if (opts.stats) {\n const { Indexer } = await import(\"../core/indexer.js\");\n const indexer = new Indexer(notesPath);\n const docs = await indexer.indexAll();\n console.log(`Notes: ${docs.length}`);\n console.log(`Chunks: ${docs.reduce((n: number, d: any) => n + d.chunks.length, 0)}`);\n console.log(`Wikilinks: ${docs.reduce((n: number, d: any) => n + d.wikilinks.length, 0)}`);\n console.log(`Tags: ${new Set(docs.flatMap((d: any) => d.tags)).size} unique`);\n process.exit(0);\n }\n\n if (opts.reindex) {\n const { createServer } = await import(\"../mcp/server.js\");\n await createServer(notesPath, {\n watch: false,\n waitForReady: true,\n model: opts.model,\n workers: opts.workers,\n batchSize: opts.batchSize,\n quantized: opts.quantized,\n onProgress: (embedded, total) => {\n process.stderr.write(`\\rEmbedding ${embedded}/${total} chunks...`);\n },\n });\n process.stderr.write(\"\\n\");\n console.log(\"Reindex complete.\");\n process.exit(0);\n }\n\n // Default: start MCP server on stdio\n const { startServer } = await import(\"../mcp/server.js\");\n await startServer(notesPath, {\n watch: opts.watch,\n waitForReady: opts.waitForReady,\n model: opts.model,\n workers: opts.workers,\n batchSize: opts.batchSize,\n quantized: opts.quantized,\n readOnly: opts.readOnly,\n });\n });\n\nregisterUpdateCommands(program, {\n packageName: PKG_NAME,\n pluginName: \"semantic-pages\",\n configFile: \"semantic-pages.json\",\n onAfterUpdate: (cwd) => runRelink(cwd),\n});\n\nprogram\n .command(\"normalize-config\")\n .description(\n \"Rewrite fragile `npx @latest` entries in .mcp.json to the stable node-against-node_modules form (with backup and validation)\",\n )\n .option(\"--dry-run\", \"print the proposed changes but don't write\")\n .action((opts: { dryRun?: boolean }) => {\n const cwd = process.cwd();\n const mcpPath = join(cwd, \".mcp.json\");\n if (!existsSync(mcpPath)) {\n console.log(\"no .mcp.json in current directory — nothing to do\");\n process.exit(0);\n }\n const data = readJsonSafe(mcpPath);\n if (!data || typeof data !== \"object\" || !data.mcpServers || typeof data.mcpServers !== \"object\") {\n console.error(`could not parse ${mcpPath}`);\n process.exit(1);\n }\n const bin = localBinArg(cwd);\n if (!bin) {\n console.error(\n `no local install found at ./node_modules/@theglitchking/semantic-pages/bin/semantic-pages.\\n` +\n `run 'npm install --save @theglitchking/semantic-pages' first, then re-run this command.`,\n );\n process.exit(1);\n }\n const rewritten: string[] = [];\n for (const [key, entry] of Object.entries<any>(data.mcpServers)) {\n if (!isNpxForm(entry)) continue;\n const notes = extractNotesPath(entry) ?? \"./.claude/.vault\";\n const extra = extractExtraFlags(entry);\n data.mcpServers[key] = {\n type: \"stdio\",\n command: \"node\",\n args: [bin, \"--notes\", notes, ...extra],\n };\n rewritten.push(key);\n }\n if (rewritten.length === 0) {\n console.log(\"no npx-form entries found — .mcp.json is already in the stable form.\");\n process.exit(0);\n }\n console.log(`Rewriting ${rewritten.length} entr${rewritten.length === 1 ? \"y\" : \"ies\"}: ${rewritten.join(\", \")}`);\n if (opts.dryRun) {\n console.log(\"--- proposed .mcp.json:\");\n console.log(JSON.stringify(data, null, 2));\n console.log(\"--- (dry-run; no changes written)\");\n process.exit(0);\n }\n // Back up\n const bakPath = join(dirname(mcpPath), \".mcp.json.bak\");\n try {\n writeFileSync(bakPath, readFileSync(mcpPath, \"utf8\"));\n console.log(`backup written: ${bakPath}`);\n } catch (err: any) {\n console.error(`could not write backup: ${err.message}`);\n process.exit(1);\n }\n writeFileSync(mcpPath, JSON.stringify(data, null, 2) + \"\\n\");\n\n // Verify the local bin starts cleanly. We just run --version — fast and\n // enough to catch ERR_MODULE_NOT_FOUND type failures.\n const verify = spawnSync(\"node\", [bin, \"--version\"], { cwd, stdio: \"pipe\", timeout: 15_000 });\n if (verify.status !== 0) {\n console.error(`verification failed (exit ${verify.status}):\\n${verify.stderr?.toString() || \"\"}`);\n console.error(`rolling back from ${bakPath}`);\n writeFileSync(mcpPath, readFileSync(bakPath, \"utf8\"));\n process.exit(1);\n }\n console.log(`✓ .mcp.json normalized and verified. Toggle the MCPs in /mcp to reconnect.`);\n process.exit(0);\n });\n\nprogram\n .command(\"healthcheck\")\n .description(\n \"Verify the local install starts cleanly; self-heal common npx-cache corruption (ERR_MODULE_NOT_FOUND)\",\n )\n .action(() => {\n const cwd = process.cwd();\n const bin = findLocalBin(cwd);\n if (!bin) {\n console.error(`no local install at ./node_modules/@theglitchking/semantic-pages/bin/semantic-pages`);\n console.error(`run 'npm install --save @theglitchking/semantic-pages' to install.`);\n process.exit(1);\n }\n\n // 1. Warn if .mcp.json uses the fragile form.\n const mcpPath = join(cwd, \".mcp.json\");\n if (existsSync(mcpPath)) {\n const data = readJsonSafe(mcpPath);\n const entries = data?.mcpServers && typeof data.mcpServers === \"object\" ? Object.entries<any>(data.mcpServers) : [];\n const fragile = entries.filter(([, e]) => isNpxForm(e)).map(([k]) => k);\n if (fragile.length > 0) {\n console.warn(\n `⚠️ .mcp.json uses the fragile npx-@latest form for: ${fragile.join(\", \")}`,\n );\n console.warn(` Rewrite to the stable form:`);\n console.warn(` npx --no @theglitchking/semantic-pages normalize-config`);\n }\n }\n\n // 2. Smoke-test the local bin.\n const r = spawnSync(\"node\", [bin, \"--version\"], { cwd, stdio: \"pipe\", timeout: 15_000 });\n if (r.status === 0) {\n console.log(`✓ local install starts cleanly (v${r.stdout?.toString().trim()})`);\n process.exit(0);\n }\n\n const stderr = r.stderr?.toString() ?? \"\";\n\n // 3. Self-heal ERR_MODULE_NOT_FOUND in npx cache (rare but the classic\n // failure mode that triggered 0.10.0). Extract the offending npx cache\n // dir from the error message, rm -rf it, retry once.\n if (stderr.includes(\"ERR_MODULE_NOT_FOUND\")) {\n const match = stderr.match(/([\\/~][^'\"\\s]*\\/_npx\\/[^\\/'\"\\s]+)/);\n if (match) {\n const bad = match[1];\n console.warn(`detected broken npx cache at ${bad} — clearing and retrying...`);\n try { rmSync(bad, { recursive: true, force: true }); } catch {}\n const r2 = spawnSync(\"node\", [bin, \"--version\"], { cwd, stdio: \"pipe\", timeout: 15_000 });\n if (r2.status === 0) {\n console.log(`✓ cleared npx cache and verified (v${r2.stdout?.toString().trim()})`);\n process.exit(0);\n }\n console.error(`retry still failed:\\n${r2.stderr?.toString() || \"\"}`);\n } else {\n console.error(`ERR_MODULE_NOT_FOUND but could not locate offending cache path in the error.`);\n }\n }\n\n console.error(stderr || `local install failed (exit ${r.status})`);\n process.exit(1);\n });\n\nprogram.parse();\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Plugin-specific SessionStart reconciliation for semantic-pages.
|
|
2
|
+
//
|
|
3
|
+
// Responsibilities (all conservative — the user's config always wins):
|
|
4
|
+
// 1. Ensure <project>/.claude/.vault exists.
|
|
5
|
+
// 2. If .mcp.json is missing a "semantic-vault" entry AND the package is
|
|
6
|
+
// installed locally, add one in the node-against-node_modules form.
|
|
7
|
+
// Existing entries (any shape) are preserved — if you want to rewrite
|
|
8
|
+
// a stale entry, run `npx --no @theglitchking/semantic-pages
|
|
9
|
+
// normalize-config`.
|
|
10
|
+
// 3. If hit-em-with-the-docs is enabled AND ./.documentation exists,
|
|
11
|
+
// conditionally add a read-only "semantic-pages" entry pointed at
|
|
12
|
+
// ./.documentation (same preserve-existing rule). Remove it ONLY if
|
|
13
|
+
// the existing entry matches a shape we recognize as ours.
|
|
14
|
+
//
|
|
15
|
+
// Any write is preceded by a backup to .mcp.json.bak. Never writes the
|
|
16
|
+
// `npx @latest` form — that shape is fragile (npx cache corruption causes
|
|
17
|
+
// ERR_MODULE_NOT_FOUND) and was the root cause of the 0.10.0 bugfix.
|
|
18
|
+
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { dirname, join, relative } from "node:path";
|
|
22
|
+
|
|
23
|
+
const PKG = "@theglitchking/semantic-pages";
|
|
24
|
+
const VAULT_KEY = "semantic-vault";
|
|
25
|
+
const DOCS_KEY = "semantic-pages";
|
|
26
|
+
|
|
27
|
+
function readJson(path, fallback = null) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = readFileSync(path, "utf8");
|
|
30
|
+
return raw.trim() ? JSON.parse(raw) : fallback;
|
|
31
|
+
} catch {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hewtdEnabled() {
|
|
37
|
+
const settings = readJson(join(homedir(), ".claude", "settings.json"));
|
|
38
|
+
const enabled = settings?.enabledPlugins || {};
|
|
39
|
+
return Object.keys(enabled).some(
|
|
40
|
+
(k) => k.startsWith("hit-em-with-the-docs@") && enabled[k] === true,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Absolute path to the locally-installed bin script, or null. */
|
|
45
|
+
export function findLocalBin(projectRoot) {
|
|
46
|
+
const p = join(projectRoot, "node_modules", "@theglitchking", "semantic-pages", "bin", "semantic-pages");
|
|
47
|
+
return existsSync(p) ? p : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Returns the arg form that goes into .mcp.json for this project. */
|
|
51
|
+
function binArg(projectRoot) {
|
|
52
|
+
const abs = findLocalBin(projectRoot);
|
|
53
|
+
if (!abs) return null;
|
|
54
|
+
const rel = relative(projectRoot, abs);
|
|
55
|
+
// Always use ./-prefixed relative form so it's machine-portable within the repo.
|
|
56
|
+
return rel.startsWith("..") ? abs : `./${rel}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildEntry(binArgStr, notesPath, extraArgs = []) {
|
|
60
|
+
return {
|
|
61
|
+
type: "stdio",
|
|
62
|
+
command: "node",
|
|
63
|
+
args: [binArgStr, "--notes", notesPath, ...extraArgs],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Detect an entry as "one of ours" so we can safely remove it when the
|
|
69
|
+
* condition that created it no longer holds. Matches both shapes we've ever
|
|
70
|
+
* written: the legacy npx-form and the new node-against-node_modules form.
|
|
71
|
+
*/
|
|
72
|
+
function isOurEntry(entry, notesPath) {
|
|
73
|
+
if (!entry || typeof entry !== "object") return false;
|
|
74
|
+
const cmd = entry.command;
|
|
75
|
+
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
76
|
+
const argStr = args.map((a) => String(a)).join(" ");
|
|
77
|
+
|
|
78
|
+
// Legacy npx form we shipped in 0.8.0 and prior SessionStart hooks.
|
|
79
|
+
if (cmd === "npx" && argStr.includes(PKG) && argStr.includes(notesPath)) return true;
|
|
80
|
+
// Current node-against-node_modules form (0.10.0+).
|
|
81
|
+
if (
|
|
82
|
+
cmd === "node" &&
|
|
83
|
+
args.some((a) => typeof a === "string" && a.includes("node_modules/@theglitchking/semantic-pages")) &&
|
|
84
|
+
argStr.includes(notesPath)
|
|
85
|
+
) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function backup(mcpPath) {
|
|
92
|
+
try {
|
|
93
|
+
if (existsSync(mcpPath)) {
|
|
94
|
+
writeFileSync(join(dirname(mcpPath), ".mcp.json.bak"), readFileSync(mcpPath, "utf8"));
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function reconcile(projectRoot) {
|
|
100
|
+
const vaultDir = join(projectRoot, ".claude", ".vault");
|
|
101
|
+
const docsDir = join(projectRoot, ".documentation");
|
|
102
|
+
const mcpPath = join(projectRoot, ".mcp.json");
|
|
103
|
+
|
|
104
|
+
try { mkdirSync(vaultDir, { recursive: true }); } catch {}
|
|
105
|
+
|
|
106
|
+
const docsWired = hewtdEnabled() && existsSync(docsDir);
|
|
107
|
+
|
|
108
|
+
let data = { mcpServers: {} };
|
|
109
|
+
if (existsSync(mcpPath)) {
|
|
110
|
+
const parsed = readJson(mcpPath, null);
|
|
111
|
+
if (parsed === null) {
|
|
112
|
+
let raw = "";
|
|
113
|
+
try { raw = readFileSync(mcpPath, "utf8"); } catch {}
|
|
114
|
+
if (raw.trim()) {
|
|
115
|
+
process.stderr.write(`semantic-pages hook: could not parse ${mcpPath}; leaving untouched\n`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
} else if (parsed && typeof parsed === "object") {
|
|
119
|
+
data = parsed;
|
|
120
|
+
if (!data.mcpServers || typeof data.mcpServers !== "object") {
|
|
121
|
+
data.mcpServers = {};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const before = JSON.stringify(data);
|
|
127
|
+
|
|
128
|
+
const bin = binArg(projectRoot);
|
|
129
|
+
|
|
130
|
+
// VAULT entry: create if missing AND we have a local install. Never
|
|
131
|
+
// overwrite an existing entry (even if it looks "wrong" — the user
|
|
132
|
+
// controls their .mcp.json).
|
|
133
|
+
if (!data.mcpServers[VAULT_KEY]) {
|
|
134
|
+
if (bin) {
|
|
135
|
+
data.mcpServers[VAULT_KEY] = buildEntry(bin, "./.claude/.vault");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// DOCS entry: same preserve-existing rule. Conditional on hewtd +
|
|
140
|
+
// .documentation. When removing, only remove shapes we recognize as ours.
|
|
141
|
+
if (docsWired) {
|
|
142
|
+
if (!data.mcpServers[DOCS_KEY]) {
|
|
143
|
+
if (bin) {
|
|
144
|
+
data.mcpServers[DOCS_KEY] = buildEntry(bin, "./.documentation", ["--read-only"]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else if (data.mcpServers[DOCS_KEY] && isOurEntry(data.mcpServers[DOCS_KEY], ".documentation")) {
|
|
148
|
+
delete data.mcpServers[DOCS_KEY];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const after = JSON.stringify(data);
|
|
152
|
+
if (after === before) return;
|
|
153
|
+
|
|
154
|
+
backup(mcpPath);
|
|
155
|
+
mkdirSync(dirname(mcpPath), { recursive: true });
|
|
156
|
+
writeFileSync(mcpPath, JSON.stringify(data, null, 2) + "\n");
|
|
157
|
+
}
|
package/hooks/session-start.js
CHANGED
|
@@ -1,283 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// semantic-pages SessionStart hook
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Never hard-fails. Emits a valid SessionStart hookSpecificOutput on stdout and
|
|
15
|
-
// exits 0. Has a ~3s budget when up-to-date (network fetch timeout). Caches the
|
|
16
|
-
// latest version for 6h. Skipped entirely in CI. Self-skips its update section
|
|
17
|
-
// when it detects a project-registered counterpart to avoid double-firing when
|
|
18
|
-
// the user has BOTH the Claude Code plugin AND the npm dep installed.
|
|
19
|
-
|
|
20
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
-
import { homedir } from "node:os";
|
|
22
|
-
import { dirname, join } from "node:path";
|
|
23
|
-
import { spawnSync } from "node:child_process";
|
|
24
|
-
|
|
25
|
-
const PKG = "@theglitchking/semantic-pages";
|
|
26
|
-
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
27
|
-
const NETWORK_TIMEOUT_MS = 3000;
|
|
28
|
-
const AUTO_UPDATE_TIMEOUT_MS = 60_000;
|
|
29
|
-
|
|
30
|
-
const VALID_POLICIES = new Set(["auto", "nudge", "off"]);
|
|
31
|
-
|
|
32
|
-
function emit(additionalContext) {
|
|
33
|
-
const out = { hookSpecificOutput: { hookEventName: "SessionStart" } };
|
|
34
|
-
if (additionalContext) out.hookSpecificOutput.additionalContext = additionalContext;
|
|
35
|
-
process.stdout.write(JSON.stringify(out) + "\n");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function logWarn(msg) {
|
|
39
|
-
process.stderr.write(`semantic-pages hook: ${msg}\n`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function readJson(path, fallback = null) {
|
|
43
|
-
try {
|
|
44
|
-
const raw = readFileSync(path, "utf8");
|
|
45
|
-
return raw.trim() ? JSON.parse(raw) : fallback;
|
|
46
|
-
} catch {
|
|
47
|
-
return fallback;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isCi() {
|
|
52
|
-
return ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI"].some(
|
|
53
|
-
(k) => !!process.env[k],
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function hewtdEnabled() {
|
|
58
|
-
const settings = readJson(join(homedir(), ".claude", "settings.json"));
|
|
59
|
-
const enabled = settings?.enabledPlugins || {};
|
|
60
|
-
return Object.keys(enabled).some(
|
|
61
|
-
(k) => k.startsWith("hit-em-with-the-docs@") && enabled[k] === true,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function hasProjectRegisteredHook(projectRoot) {
|
|
66
|
-
const settings = readJson(join(projectRoot, ".claude", "settings.json"));
|
|
67
|
-
const groups = settings?.hooks?.SessionStart;
|
|
68
|
-
if (!Array.isArray(groups)) return false;
|
|
69
|
-
for (const group of groups) {
|
|
70
|
-
for (const h of group?.hooks || []) {
|
|
71
|
-
const cmd = h?.command;
|
|
72
|
-
if (typeof cmd === "string" && cmd.includes("semantic-pages")) return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function reconcileMcp(projectRoot) {
|
|
79
|
-
const vaultDir = join(projectRoot, ".claude", ".vault");
|
|
80
|
-
const docsDir = join(projectRoot, ".documentation");
|
|
81
|
-
const mcpPath = join(projectRoot, ".mcp.json");
|
|
82
|
-
|
|
83
|
-
try { mkdirSync(vaultDir, { recursive: true }); } catch {}
|
|
84
|
-
|
|
85
|
-
const docsWired = hewtdEnabled() && existsSync(docsDir);
|
|
86
|
-
|
|
87
|
-
let data = { mcpServers: {} };
|
|
88
|
-
if (existsSync(mcpPath)) {
|
|
89
|
-
const parsed = readJson(mcpPath, null);
|
|
90
|
-
if (parsed === null) {
|
|
91
|
-
// File exists but didn't parse — leave it alone, don't clobber user data.
|
|
92
|
-
let raw = "";
|
|
93
|
-
try { raw = readFileSync(mcpPath, "utf8"); } catch {}
|
|
94
|
-
if (raw.trim()) {
|
|
95
|
-
logWarn(`could not parse ${mcpPath}; leaving untouched`);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
} else if (parsed && typeof parsed === "object") {
|
|
99
|
-
data = parsed;
|
|
100
|
-
if (!data.mcpServers || typeof data.mcpServers !== "object") {
|
|
101
|
-
data.mcpServers = {};
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const before = JSON.stringify(data);
|
|
107
|
-
|
|
108
|
-
data.mcpServers["semantic-vault"] = {
|
|
109
|
-
type: "stdio",
|
|
110
|
-
command: "npx",
|
|
111
|
-
args: ["-y", `${PKG}@latest`, "--notes", "./.claude/.vault"],
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (docsWired) {
|
|
115
|
-
data.mcpServers["semantic-pages"] = {
|
|
116
|
-
type: "stdio",
|
|
117
|
-
command: "npx",
|
|
118
|
-
args: ["-y", `${PKG}@latest`, "--notes", "./.documentation", "--read-only"],
|
|
119
|
-
};
|
|
120
|
-
} else if (data.mcpServers["semantic-pages"]) {
|
|
121
|
-
// Only remove if it looks like ours (points at .documentation). Preserves
|
|
122
|
-
// any user-defined semantic-pages entry pointing elsewhere.
|
|
123
|
-
const existing = data.mcpServers["semantic-pages"];
|
|
124
|
-
const args = Array.isArray(existing.args) ? existing.args : [];
|
|
125
|
-
const looksLikeOurs = args.some(
|
|
126
|
-
(a) => typeof a === "string" && a.includes(".documentation"),
|
|
127
|
-
);
|
|
128
|
-
if (looksLikeOurs) delete data.mcpServers["semantic-pages"];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const after = JSON.stringify(data);
|
|
132
|
-
if (after === before) return;
|
|
133
|
-
|
|
134
|
-
mkdirSync(dirname(mcpPath), { recursive: true });
|
|
135
|
-
writeFileSync(mcpPath, JSON.stringify(data, null, 2) + "\n");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function loadPolicy(projectRoot) {
|
|
139
|
-
const env = process.env.SEMANTIC_PAGES_UPDATE_POLICY;
|
|
140
|
-
if (env && VALID_POLICIES.has(env)) return env;
|
|
141
|
-
const cfg = readJson(join(projectRoot, ".claude", "semantic-pages.json"));
|
|
142
|
-
if (cfg?.updatePolicy && VALID_POLICIES.has(cfg.updatePolicy)) return cfg.updatePolicy;
|
|
143
|
-
return "nudge";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function getInstalledVersion(projectRoot) {
|
|
147
|
-
const candidates = [
|
|
148
|
-
join(projectRoot, "node_modules", "@theglitchking", "semantic-pages", "package.json"),
|
|
149
|
-
];
|
|
150
|
-
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
151
|
-
candidates.push(join(process.env.CLAUDE_PLUGIN_ROOT, "package.json"));
|
|
152
|
-
}
|
|
153
|
-
for (const p of candidates) {
|
|
154
|
-
const pkg = readJson(p);
|
|
155
|
-
if (pkg?.version) return pkg.version;
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function compareVersions(a, b) {
|
|
161
|
-
const pa = String(a).split(/[.-]/).map((p) => (/^\d+$/.test(p) ? +p : p));
|
|
162
|
-
const pb = String(b).split(/[.-]/).map((p) => (/^\d+$/.test(p) ? +p : p));
|
|
163
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
164
|
-
const x = pa[i] ?? 0;
|
|
165
|
-
const y = pb[i] ?? 0;
|
|
166
|
-
if (x < y) return -1;
|
|
167
|
-
if (x > y) return 1;
|
|
168
|
-
}
|
|
169
|
-
return 0;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function fetchLatestVersion(cacheFile) {
|
|
173
|
-
const cached = readJson(cacheFile);
|
|
174
|
-
if (
|
|
175
|
-
cached?.latest &&
|
|
176
|
-
typeof cached.checkedAt === "number" &&
|
|
177
|
-
Date.now() - cached.checkedAt < CACHE_TTL_MS
|
|
178
|
-
) {
|
|
179
|
-
return cached.latest;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const ctrl = new AbortController();
|
|
183
|
-
const timer = setTimeout(() => ctrl.abort(), NETWORK_TIMEOUT_MS);
|
|
184
|
-
try {
|
|
185
|
-
const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
|
|
186
|
-
signal: ctrl.signal,
|
|
187
|
-
headers: { accept: "application/json" },
|
|
188
|
-
});
|
|
189
|
-
if (!res.ok) return null;
|
|
190
|
-
const json = await res.json();
|
|
191
|
-
const latest = json?.version;
|
|
192
|
-
if (latest) {
|
|
193
|
-
try {
|
|
194
|
-
mkdirSync(dirname(cacheFile), { recursive: true });
|
|
195
|
-
writeFileSync(cacheFile, JSON.stringify({ latest, checkedAt: Date.now() }));
|
|
196
|
-
} catch {}
|
|
197
|
-
}
|
|
198
|
-
return latest || null;
|
|
199
|
-
} catch {
|
|
200
|
-
return null;
|
|
201
|
-
} finally {
|
|
202
|
-
clearTimeout(timer);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function runAutoUpdate(projectRoot) {
|
|
207
|
-
const r = spawnSync("npm", ["update", PKG], {
|
|
208
|
-
cwd: projectRoot,
|
|
209
|
-
timeout: AUTO_UPDATE_TIMEOUT_MS,
|
|
210
|
-
stdio: "ignore",
|
|
211
|
-
});
|
|
212
|
-
if (r.status !== 0) return false;
|
|
213
|
-
// Re-link skills so any new skills ship to .claude/skills/.
|
|
214
|
-
const linker = join(
|
|
215
|
-
projectRoot,
|
|
216
|
-
"node_modules",
|
|
217
|
-
"@theglitchking",
|
|
218
|
-
"semantic-pages",
|
|
219
|
-
"scripts",
|
|
220
|
-
"link-skills.js",
|
|
221
|
-
);
|
|
222
|
-
if (existsSync(linker)) {
|
|
223
|
-
spawnSync(process.execPath, [linker], {
|
|
224
|
-
cwd: projectRoot,
|
|
225
|
-
env: { ...process.env, INIT_CWD: projectRoot },
|
|
226
|
-
stdio: "ignore",
|
|
227
|
-
timeout: 10_000,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function runUpdateCheck(projectRoot) {
|
|
234
|
-
const policy = loadPolicy(projectRoot);
|
|
235
|
-
if (policy === "off") return "";
|
|
236
|
-
const current = getInstalledVersion(projectRoot);
|
|
237
|
-
if (!current) return "";
|
|
238
|
-
const cacheFile = join(projectRoot, ".claude", ".semantic-pages-update-cache.json");
|
|
239
|
-
const latest = await fetchLatestVersion(cacheFile);
|
|
240
|
-
if (!latest || compareVersions(current, latest) >= 0) return "";
|
|
241
|
-
|
|
242
|
-
if (policy === "nudge") {
|
|
243
|
-
return `💡 semantic-pages v${current} → v${latest} available.\n Run /plugin semantic-pages → 'Update now' to upgrade.`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// auto
|
|
247
|
-
const ok = runAutoUpdate(projectRoot);
|
|
248
|
-
if (ok) return `⬆️ semantic-pages v${current} → v${latest}`;
|
|
249
|
-
return `💡 semantic-pages v${current} → v${latest} available (auto-update failed — run /plugin semantic-pages → 'Update now').`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async function main() {
|
|
253
|
-
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
254
|
-
try {
|
|
255
|
-
process.chdir(projectRoot);
|
|
256
|
-
} catch {
|
|
257
|
-
return emit();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
try { reconcileMcp(projectRoot); } catch (err) { logWarn(`reconcile failed: ${err.message}`); }
|
|
261
|
-
|
|
262
|
-
// Dedup: if we're the plugin-registered instance (CLAUDE_PLUGIN_ROOT set) and
|
|
263
|
-
// the project has its own semantic-pages hook in .claude/settings.json, the
|
|
264
|
-
// project-registered hook will handle the update check. Defer to it.
|
|
265
|
-
if (process.env.CLAUDE_PLUGIN_ROOT && hasProjectRegisteredHook(projectRoot)) {
|
|
266
|
-
return emit();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (isCi()) return emit();
|
|
270
|
-
|
|
271
|
-
let msg = "";
|
|
272
|
-
try {
|
|
273
|
-
msg = await runUpdateCheck(projectRoot);
|
|
274
|
-
} catch (err) {
|
|
275
|
-
logWarn(`update check failed: ${err.message}`);
|
|
276
|
-
}
|
|
277
|
-
emit(msg || undefined);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
main().catch((err) => {
|
|
281
|
-
logWarn(`fatal: ${err?.message || err}`);
|
|
282
|
-
emit();
|
|
2
|
+
// semantic-pages SessionStart hook. Delegates lifecycle + update-check to
|
|
3
|
+
// @theglitchking/claude-plugin-runtime; plugin-specific .mcp.json wiring
|
|
4
|
+
// lives in ./reconcile.js.
|
|
5
|
+
|
|
6
|
+
import { runSessionStart } from "@theglitchking/claude-plugin-runtime";
|
|
7
|
+
import { reconcile } from "./reconcile.js";
|
|
8
|
+
|
|
9
|
+
await runSessionStart({
|
|
10
|
+
packageName: "@theglitchking/semantic-pages",
|
|
11
|
+
pluginName: "semantic-pages",
|
|
12
|
+
configFile: "semantic-pages.json",
|
|
13
|
+
reconcile,
|
|
283
14
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theglitchking/semantic-pages",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Semantic search + knowledge graph MCP server for markdown vaults. Claude Code plugin with auto-wiring for .claude/.vault and read-only .documentation companion when hit-em-with-the-docs is installed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/core/index.js",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@huggingface/transformers": "^4.0.1",
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
61
|
-
"
|
|
61
|
+
"@theglitchking/claude-plugin-runtime": "^0.1.0",
|
|
62
62
|
"chokidar": "^5.0.0",
|
|
63
63
|
"commander": "^14.0.3",
|
|
64
64
|
"glob": "^13.0.6",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"gray-matter": "^4.0.3",
|
|
70
70
|
"hnswlib-node": "^3.0.0",
|
|
71
71
|
"minimatch": "^10.2.5",
|
|
72
|
+
"onnxruntime-web": "^1.24.3",
|
|
72
73
|
"remark-parse": "^11.0.0",
|
|
73
74
|
"remark-wiki-link": "^2.0.1",
|
|
74
75
|
"unified": "^11.0.5"
|
package/scripts/link-skills.js
CHANGED
|
@@ -1,205 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Postinstall
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// ({ "updatePolicy": "nudge" }) if one doesn't already exist.
|
|
7
|
-
// 3. Register the SessionStart hook in <consumer>/.claude/settings.json —
|
|
8
|
-
// but only if settings.json already exists, the Claude Code plugin
|
|
9
|
-
// version isn't already handling it, and no semantic-pages hook is
|
|
10
|
-
// already registered.
|
|
11
|
-
//
|
|
12
|
-
// Env overrides:
|
|
13
|
-
// SEMANTIC_PAGES_SKIP_LINK=1 — skip skill linking
|
|
14
|
-
// SEMANTIC_PAGES_SKIP_HOOK_REGISTER=1 — skip settings.json hook registration
|
|
15
|
-
//
|
|
16
|
-
// Never hard-fails — any error downgrades to a warning and exit 0 so the
|
|
17
|
-
// package still installs.
|
|
2
|
+
// Postinstall — delegates to @theglitchking/claude-plugin-runtime.
|
|
3
|
+
// The runtime handles: skill symlinking, default policy config write, and
|
|
4
|
+
// settings.json hook registration with plugin/npm dedup. See the runtime's
|
|
5
|
+
// docs/PLUGIN_AUTHORING_SCAFFOLD.md for the full contract.
|
|
18
6
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
lstatSync,
|
|
22
|
-
mkdirSync,
|
|
23
|
-
readFileSync,
|
|
24
|
-
readdirSync,
|
|
25
|
-
readlinkSync,
|
|
26
|
-
rmSync,
|
|
27
|
-
symlinkSync,
|
|
28
|
-
writeFileSync,
|
|
29
|
-
} from "node:fs";
|
|
30
|
-
import { homedir } from "node:os";
|
|
31
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { runPostinstall } from "@theglitchking/claude-plugin-runtime";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
32
9
|
import { fileURLToPath } from "node:url";
|
|
33
10
|
|
|
34
|
-
const
|
|
35
|
-
const info = (msg) => console.log(`[semantic-pages] ${msg}`);
|
|
36
|
-
|
|
37
|
-
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
38
|
-
const CONSUMER_ROOT = process.env.INIT_CWD || process.cwd();
|
|
39
|
-
const HOOK_COMMAND = `node ./node_modules/@theglitchking/semantic-pages/hooks/session-start.js`;
|
|
40
|
-
const HOOK_MARKER = "semantic-pages"; // substring we scan for to detect our hooks
|
|
41
|
-
|
|
42
|
-
function readJson(path, fallback = null) {
|
|
43
|
-
try {
|
|
44
|
-
const raw = readFileSync(path, "utf8");
|
|
45
|
-
return raw.trim() ? JSON.parse(raw) : fallback;
|
|
46
|
-
} catch {
|
|
47
|
-
return fallback;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function writeJson(path, value) {
|
|
52
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
-
writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function lstatSafe(p) {
|
|
57
|
-
try { return lstatSync(p); } catch { return null; }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function isDeveloperInPlace() {
|
|
61
|
-
return resolve(CONSUMER_ROOT) === resolve(PACKAGE_ROOT);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function linkSkills() {
|
|
65
|
-
if (process.env.SEMANTIC_PAGES_SKIP_LINK === "1") {
|
|
66
|
-
info("SEMANTIC_PAGES_SKIP_LINK=1 — skipping skill linking.");
|
|
67
|
-
return 0;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const skillsSrcDir = join(PACKAGE_ROOT, "skills");
|
|
71
|
-
if (!existsSync(skillsSrcDir)) return 0;
|
|
72
|
-
|
|
73
|
-
const skillDirs = readdirSync(skillsSrcDir, { withFileTypes: true })
|
|
74
|
-
.filter((d) => d.isDirectory() && !d.name.endsWith("-workspace"))
|
|
75
|
-
.map((d) => d.name);
|
|
76
|
-
if (skillDirs.length === 0) return 0;
|
|
77
|
-
|
|
78
|
-
const destDir = join(CONSUMER_ROOT, ".claude", "skills");
|
|
79
|
-
try { mkdirSync(destDir, { recursive: true }); } catch (err) {
|
|
80
|
-
warn(`could not create ${destDir}: ${err.message}`);
|
|
81
|
-
return 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let linked = 0;
|
|
85
|
-
for (const name of skillDirs) {
|
|
86
|
-
const src = join(skillsSrcDir, name);
|
|
87
|
-
const dest = join(destDir, name);
|
|
88
|
-
const rel = relative(dirname(dest), src);
|
|
89
|
-
try {
|
|
90
|
-
const st = lstatSafe(dest);
|
|
91
|
-
if (st) {
|
|
92
|
-
if (st.isSymbolicLink()) {
|
|
93
|
-
if (readlinkSync(dest) === rel) { linked++; continue; }
|
|
94
|
-
rmSync(dest, { force: true });
|
|
95
|
-
} else {
|
|
96
|
-
warn(`skipping ${dest} — a non-symlink already exists there.`);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
symlinkSync(rel, dest, "dir");
|
|
101
|
-
linked++;
|
|
102
|
-
} catch (err) {
|
|
103
|
-
warn(`could not link skill "${name}": ${err.message}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return linked;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function writeDefaultConfig() {
|
|
110
|
-
const cfgPath = join(CONSUMER_ROOT, ".claude", "semantic-pages.json");
|
|
111
|
-
if (existsSync(cfgPath)) {
|
|
112
|
-
const cur = readJson(cfgPath);
|
|
113
|
-
return cur?.updatePolicy || "nudge";
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
writeJson(cfgPath, { updatePolicy: "nudge" });
|
|
117
|
-
} catch (err) {
|
|
118
|
-
warn(`could not write ${cfgPath}: ${err.message}`);
|
|
119
|
-
}
|
|
120
|
-
return "nudge";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function pluginAlreadyHandling() {
|
|
124
|
-
// If the Claude Code plugin is enabled globally, its own hooks.json handles
|
|
125
|
-
// SessionStart — no need to also register in project settings.json.
|
|
126
|
-
const settings = readJson(join(homedir(), ".claude", "settings.json"));
|
|
127
|
-
const enabled = settings?.enabledPlugins || {};
|
|
128
|
-
return Object.keys(enabled).some(
|
|
129
|
-
(k) => k.startsWith("semantic-pages@") && enabled[k] === true,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function settingsHasOurHook(settings) {
|
|
134
|
-
const groups = settings?.hooks?.SessionStart;
|
|
135
|
-
if (!Array.isArray(groups)) return false;
|
|
136
|
-
return groups.some((g) =>
|
|
137
|
-
(g?.hooks || []).some(
|
|
138
|
-
(h) => typeof h?.command === "string" && h.command.includes(HOOK_MARKER),
|
|
139
|
-
),
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function registerHookIfApplicable() {
|
|
144
|
-
if (process.env.SEMANTIC_PAGES_SKIP_HOOK_REGISTER === "1") {
|
|
145
|
-
return { status: "skipped", reason: "env" };
|
|
146
|
-
}
|
|
147
|
-
if (pluginAlreadyHandling()) {
|
|
148
|
-
return { status: "skipped", reason: "plugin" };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const settingsPath = join(CONSUMER_ROOT, ".claude", "settings.json");
|
|
152
|
-
if (!existsSync(settingsPath)) {
|
|
153
|
-
// Only register when the user has already opted into project settings.
|
|
154
|
-
return { status: "skipped", reason: "no-settings" };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const settings = readJson(settingsPath);
|
|
158
|
-
if (!settings || typeof settings !== "object") {
|
|
159
|
-
warn(`could not parse ${settingsPath}; not registering hook.`);
|
|
160
|
-
return { status: "skipped", reason: "unparseable" };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (settingsHasOurHook(settings)) {
|
|
164
|
-
return { status: "already" };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
settings.hooks ||= {};
|
|
168
|
-
settings.hooks.SessionStart ||= [];
|
|
169
|
-
settings.hooks.SessionStart.push({
|
|
170
|
-
hooks: [{ type: "command", command: HOOK_COMMAND }],
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
writeJson(settingsPath, settings);
|
|
175
|
-
return { status: "registered" };
|
|
176
|
-
} catch (err) {
|
|
177
|
-
warn(`could not write ${settingsPath}: ${err.message}`);
|
|
178
|
-
return { status: "skipped", reason: "write-failed" };
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function main() {
|
|
183
|
-
if (isDeveloperInPlace()) return;
|
|
184
|
-
|
|
185
|
-
const linkedCount = linkSkills();
|
|
186
|
-
const policy = writeDefaultConfig();
|
|
187
|
-
const hookResult = registerHookIfApplicable();
|
|
188
|
-
|
|
189
|
-
const parts = [];
|
|
190
|
-
if (linkedCount > 0) parts.push(`${linkedCount} skill(s) linked`);
|
|
191
|
-
parts.push(`update mode = ${policy}`);
|
|
192
|
-
if (hookResult.status === "registered") parts.push("hook registered");
|
|
193
|
-
else if (hookResult.status === "already") parts.push("hook already registered");
|
|
194
|
-
else if (hookResult.status === "skipped" && hookResult.reason === "plugin") {
|
|
195
|
-
parts.push("hook deferred to plugin");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
info(`${parts.join(", ")} (change via /plugin semantic-pages)`);
|
|
199
|
-
}
|
|
11
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
200
12
|
|
|
201
13
|
try {
|
|
202
|
-
|
|
14
|
+
runPostinstall({
|
|
15
|
+
packageName: "@theglitchking/semantic-pages",
|
|
16
|
+
pluginName: "semantic-pages",
|
|
17
|
+
configFile: "semantic-pages.json",
|
|
18
|
+
skillsDir: "skills",
|
|
19
|
+
packageRoot,
|
|
20
|
+
hookCommand:
|
|
21
|
+
"node ./node_modules/@theglitchking/semantic-pages/hooks/session-start.js",
|
|
22
|
+
});
|
|
203
23
|
} catch (err) {
|
|
204
|
-
warn(`postinstall failed: ${err?.message || err}`);
|
|
24
|
+
console.warn(`[semantic-pages] postinstall failed: ${err?.message || err}`);
|
|
205
25
|
}
|