@theglitchking/semantic-pages 0.6.6 → 0.8.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/README.md +107 -0
- package/commands/policy.md +16 -0
- package/commands/relink.md +6 -0
- package/commands/status.md +6 -0
- package/commands/update.md +6 -0
- package/dist/cli/index.js +131 -3
- package/dist/cli/index.js.map +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/session-start.js +283 -0
- package/package.json +5 -2
- package/scripts/link-skills.js +205 -0
- package/hooks/session-start.sh +0 -147
|
@@ -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.8.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.8.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.8.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TheGlitchKing",
|
|
7
7
|
"email": "theglitchking@users.noreply.github.com"
|
package/README.md
CHANGED
|
@@ -479,6 +479,113 @@ Each flow probes its MCP server independently and degrades gracefully when one i
|
|
|
479
479
|
|
|
480
480
|
When you install `semantic-pages` via the Claude Code plugin marketplace or via npm, Claude Code picks up this skill automatically — no extra wiring required.
|
|
481
481
|
|
|
482
|
+
### How auto-linking works *(0.7.0+)*
|
|
483
|
+
|
|
484
|
+
When installed as an npm dependency, a `postinstall` script symlinks every directory under the package's `skills/` into your project's `.claude/skills/` (creating that directory if missing). Claude Code only scans `<project>/.claude/skills/` and `~/.claude/skills/` — it does not look inside `node_modules/` — so this link is what makes bundled skills discoverable.
|
|
485
|
+
|
|
486
|
+
- The link is relative, pointing into `node_modules/@theglitchking/semantic-pages/skills/<name>`, and is refreshed on every install/update.
|
|
487
|
+
- If a regular file or directory already exists at the destination (i.e. someone copied a skill manually), the script leaves it alone and prints a warning.
|
|
488
|
+
- The script is a no-op when you're developing the plugin itself (`INIT_CWD` equals the package root), so the plugin never self-links into its own repo.
|
|
489
|
+
- The linker never hard-fails an install — permission errors and odd filesystems downgrade to a warning.
|
|
490
|
+
|
|
491
|
+
Add the linked skills to `.gitignore` so they don't get committed in consuming projects:
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
/.claude/skills/semantic-first/
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Opt-out**: set `SEMANTIC_PAGES_SKIP_LINK=1` to skip linking during install — useful if you want to manage `.claude/skills/` manually.
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
SEMANTIC_PAGES_SKIP_LINK=1 npm install @theglitchking/semantic-pages
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Update Policy & Session-Start Hook *(0.8.0+)*
|
|
506
|
+
|
|
507
|
+
When you install `semantic-pages` as an npm dependency, the postinstall step also:
|
|
508
|
+
|
|
509
|
+
1. Writes `<project>/.claude/semantic-pages.json` with `{ "updatePolicy": "nudge" }` if one doesn't exist.
|
|
510
|
+
2. Registers a `SessionStart` hook in `<project>/.claude/settings.json` — but **only** if `settings.json` exists and neither the Claude Code plugin nor an existing semantic-pages hook is already handling it.
|
|
511
|
+
|
|
512
|
+
### What the hook does
|
|
513
|
+
|
|
514
|
+
At the start of every Claude Code session, the hook:
|
|
515
|
+
|
|
516
|
+
- Reconciles `.mcp.json` (the same `semantic-vault` + conditional `.documentation` wiring you'd get from the plugin).
|
|
517
|
+
- Checks npm for a newer version of `@theglitchking/semantic-pages` (~3s budget, results cached for 6h, skipped in CI).
|
|
518
|
+
- Acts on the result according to your policy.
|
|
519
|
+
|
|
520
|
+
### Policies
|
|
521
|
+
|
|
522
|
+
| Policy | Behavior |
|
|
523
|
+
|--------|----------|
|
|
524
|
+
| `nudge` *(default)* | Print a one-liner when a newer version is available. No changes. |
|
|
525
|
+
| `auto` | Run `npm update @theglitchking/semantic-pages`, re-link skills, print `⬆️ vX → vY`. |
|
|
526
|
+
| `off` | Silent — no update check. |
|
|
527
|
+
|
|
528
|
+
### Setting the policy
|
|
529
|
+
|
|
530
|
+
Preferred:
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
/semantic-pages:policy auto
|
|
534
|
+
/semantic-pages:policy nudge
|
|
535
|
+
/semantic-pages:policy off
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Or from the terminal:
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
npx --no @theglitchking/semantic-pages policy auto
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Resolution order:
|
|
545
|
+
|
|
546
|
+
1. `SEMANTIC_PAGES_UPDATE_POLICY` env var (one-shot override).
|
|
547
|
+
2. `<project>/.claude/semantic-pages.json` → `updatePolicy`.
|
|
548
|
+
3. Default: `nudge`.
|
|
549
|
+
|
|
550
|
+
### Plugin-command menu
|
|
551
|
+
|
|
552
|
+
With `semantic-pages` installed (plugin or npm), you get these slash commands:
|
|
553
|
+
|
|
554
|
+
| Command | Does |
|
|
555
|
+
|---------|------|
|
|
556
|
+
| `/semantic-pages:update` | Runs `npm update`, re-links skills, reports before/after. |
|
|
557
|
+
| `/semantic-pages:policy [auto\|nudge\|off]` | Get or set the update policy. |
|
|
558
|
+
| `/semantic-pages:status` | Installed version, latest on npm, policy, hook state. |
|
|
559
|
+
| `/semantic-pages:relink` | Re-symlink bundled skills into `.claude/skills/`. |
|
|
560
|
+
|
|
561
|
+
Each has a CLI equivalent: `npx --no @theglitchking/semantic-pages <subcommand>`.
|
|
562
|
+
|
|
563
|
+
### Dedup between plugin and npm installs
|
|
564
|
+
|
|
565
|
+
If you install via **both** the Claude Code plugin and the npm dep, the postinstall detects the plugin in `~/.claude/settings.json` → `enabledPlugins` and skips the settings.json hook registration. One hook fires per session, not two. If you later uninstall the plugin, the next `npm install` re-registers the project-level hook.
|
|
566
|
+
|
|
567
|
+
### Opt-out
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
# Skip settings.json hook registration:
|
|
571
|
+
SEMANTIC_PAGES_SKIP_HOOK_REGISTER=1 npm install @theglitchking/semantic-pages
|
|
572
|
+
|
|
573
|
+
# Skip skill symlinking:
|
|
574
|
+
SEMANTIC_PAGES_SKIP_LINK=1 npm install @theglitchking/semantic-pages
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Uninstalling cleanly
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
# 1. Remove the hook entry from .claude/settings.json (look for any SessionStart
|
|
581
|
+
# entry whose command references "semantic-pages").
|
|
582
|
+
# 2. Remove the policy file:
|
|
583
|
+
rm .claude/semantic-pages.json
|
|
584
|
+
|
|
585
|
+
# 3. Remove the package:
|
|
586
|
+
npm uninstall @theglitchking/semantic-pages
|
|
587
|
+
```
|
|
588
|
+
|
|
482
589
|
---
|
|
483
590
|
|
|
484
591
|
## Common Workflows
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Get or set the semantic-pages update policy (auto | nudge | off)
|
|
3
|
+
allowed-tools: Bash(npx:*)
|
|
4
|
+
argument-hint: "[auto|nudge|off]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Arguments: $ARGUMENTS
|
|
8
|
+
|
|
9
|
+
- If `$ARGUMENTS` is empty, run `npx --no @theglitchking/semantic-pages policy` and report the current policy and config path.
|
|
10
|
+
- If `$ARGUMENTS` is one of `auto`, `nudge`, `off`, run `npx --no @theglitchking/semantic-pages policy $ARGUMENTS` and confirm the new setting to the user.
|
|
11
|
+
- If `$ARGUMENTS` is anything else, tell the user the valid values are `auto`, `nudge`, `off`.
|
|
12
|
+
|
|
13
|
+
Policies:
|
|
14
|
+
- `auto` — auto-run `npm update @theglitchking/semantic-pages` at session start when a newer version is available.
|
|
15
|
+
- `nudge` — print a one-liner when a newer version is available (default).
|
|
16
|
+
- `off` — do not check for updates.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Update semantic-pages to the latest version (runs npm update + re-links skills)
|
|
3
|
+
allowed-tools: Bash(npx:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run `npx --no @theglitchking/semantic-pages update` and report the before/after versions to the user. If the project doesn't have a local install, fall back to `npx -y @theglitchking/semantic-pages update` and note that in your summary.
|
package/dist/cli/index.js
CHANGED
|
@@ -2,10 +2,86 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { program } from "commander";
|
|
5
|
-
import { resolve } from "path";
|
|
6
|
-
import { existsSync } from "fs";
|
|
5
|
+
import { resolve, dirname, join } from "path";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
|
-
|
|
8
|
+
import { spawnSync } from "child_process";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var require_ = createRequire(import.meta.url);
|
|
11
|
+
var { version } = require_("../../package.json");
|
|
12
|
+
var PKG_NAME = "@theglitchking/semantic-pages";
|
|
13
|
+
var VALID_POLICIES = ["auto", "nudge", "off"];
|
|
14
|
+
function readJsonSafe(p, fallback = null) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = readFileSync(p, "utf8");
|
|
17
|
+
return raw.trim() ? JSON.parse(raw) : fallback;
|
|
18
|
+
} catch {
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writeJsonFile(p, value) {
|
|
23
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
24
|
+
writeFileSync(p, JSON.stringify(value, null, 2) + "\n");
|
|
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";
|
|
36
|
+
}
|
|
37
|
+
function installedVersion(cwd = process.cwd()) {
|
|
38
|
+
const localPkg = join(cwd, "node_modules", "@theglitchking", "semantic-pages", "package.json");
|
|
39
|
+
const pkg = readJsonSafe(localPkg);
|
|
40
|
+
if (pkg?.version) return pkg.version;
|
|
41
|
+
return version;
|
|
42
|
+
}
|
|
43
|
+
async function fetchLatestVersion(timeoutMs = 5e3) {
|
|
44
|
+
const ctrl = new AbortController();
|
|
45
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, {
|
|
48
|
+
signal: ctrl.signal,
|
|
49
|
+
headers: { accept: "application/json" }
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) return null;
|
|
52
|
+
const json = await res.json();
|
|
53
|
+
return json.version ?? null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function runNpmUpdate(cwd) {
|
|
61
|
+
return spawnSync("npm", ["update", PKG_NAME], { cwd, stdio: "inherit" });
|
|
62
|
+
}
|
|
63
|
+
function runRelink(cwd) {
|
|
64
|
+
const linker = join(cwd, "node_modules", "@theglitchking", "semantic-pages", "scripts", "link-skills.js");
|
|
65
|
+
if (!existsSync(linker)) {
|
|
66
|
+
const localLinker = resolve(fileURLToPath(import.meta.url), "..", "..", "..", "scripts", "link-skills.js");
|
|
67
|
+
if (!existsSync(localLinker)) {
|
|
68
|
+
console.error("link-skills.js not found \u2014 is the package installed?");
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const r2 = spawnSync(process.execPath, [localLinker], {
|
|
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;
|
|
84
|
+
}
|
|
9
85
|
var TOOL_HELP = {
|
|
10
86
|
// Search
|
|
11
87
|
search_semantic: {
|
|
@@ -254,5 +330,57 @@ program.command("serve", { isDefault: true }).description("Start the MCP server
|
|
|
254
330
|
readOnly: opts.readOnly
|
|
255
331
|
});
|
|
256
332
|
});
|
|
333
|
+
program.command("update").description("Update semantic-pages to the latest version (project-local install)").action(async () => {
|
|
334
|
+
const cwd = process.cwd();
|
|
335
|
+
const before = installedVersion(cwd);
|
|
336
|
+
console.log(`Current: ${before ?? "(not installed)"}`);
|
|
337
|
+
const r = runNpmUpdate(cwd);
|
|
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);
|
|
346
|
+
});
|
|
347
|
+
program.command("policy [mode]").description(`Show or set the update policy (${VALID_POLICIES.join(" | ")})`).action((mode) => {
|
|
348
|
+
const cwd = process.cwd();
|
|
349
|
+
if (!mode) {
|
|
350
|
+
console.log(`updatePolicy = ${currentPolicy(cwd)} (${configPath(cwd)})`);
|
|
351
|
+
process.exit(0);
|
|
352
|
+
}
|
|
353
|
+
if (!VALID_POLICIES.includes(mode)) {
|
|
354
|
+
console.error(`Invalid policy: ${mode}. Valid: ${VALID_POLICIES.join(", ")}`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
const p = configPath(cwd);
|
|
358
|
+
const cfg = readJsonSafe(p) ?? {};
|
|
359
|
+
cfg.updatePolicy = mode;
|
|
360
|
+
writeJsonFile(p, cfg);
|
|
361
|
+
console.log(`updatePolicy = ${mode} (${p})`);
|
|
362
|
+
process.exit(0);
|
|
363
|
+
});
|
|
364
|
+
program.command("status").description("Show installed version, latest available, current policy, and hook registration state").action(async () => {
|
|
365
|
+
const cwd = process.cwd();
|
|
366
|
+
const current = installedVersion(cwd);
|
|
367
|
+
const latest = await fetchLatestVersion();
|
|
368
|
+
const policy = currentPolicy(cwd);
|
|
369
|
+
const settings = readJsonSafe(
|
|
370
|
+
join(cwd, ".claude", "settings.json")
|
|
371
|
+
);
|
|
372
|
+
const hookRegistered = !!settings?.hooks?.SessionStart?.some(
|
|
373
|
+
(g) => (g?.hooks || []).some((h) => typeof h?.command === "string" && h.command.includes("semantic-pages"))
|
|
374
|
+
);
|
|
375
|
+
console.log(`semantic-pages status`);
|
|
376
|
+
console.log(` installed: ${current ?? "(not installed)"}`);
|
|
377
|
+
console.log(` latest: ${latest ?? "(unknown)"}`);
|
|
378
|
+
console.log(` policy: ${policy}`);
|
|
379
|
+
console.log(` hook: ${hookRegistered ? "registered in .claude/settings.json" : "not in .claude/settings.json"}`);
|
|
380
|
+
process.exit(0);
|
|
381
|
+
});
|
|
382
|
+
program.command("relink").description("Re-run the skill linker (symlinks bundled skills into .claude/skills/)").action(() => {
|
|
383
|
+
process.exit(runRelink(process.cwd()));
|
|
384
|
+
});
|
|
257
385
|
program.parse();
|
|
258
386
|
//# 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 } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\n\nconst { version } = createRequire(import.meta.url)(\"../../package.json\") as { version: string };\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.parse();\n"],"mappings":"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","names":[]}
|
|
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"]}
|
package/hooks/hooks.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "semantic-pages SessionStart hook —
|
|
2
|
+
"description": "semantic-pages SessionStart hook — reconciles .mcp.json wiring and nudges/auto-updates per policy.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "
|
|
9
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.js\""
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// semantic-pages SessionStart hook (Node, cross-platform).
|
|
3
|
+
// Does two jobs:
|
|
4
|
+
// 1. Reconcile <project>/.mcp.json (port of the original session-start.sh):
|
|
5
|
+
// - ensure "semantic-vault" -> ./.claude/.vault (read/write)
|
|
6
|
+
// - if hit-em-with-the-docs is enabled AND ./.documentation exists:
|
|
7
|
+
// add "semantic-pages" -> ./.documentation (read-only)
|
|
8
|
+
// else: remove our own docs entry if present (never touch user entries)
|
|
9
|
+
// 2. Check for package updates and act per policy:
|
|
10
|
+
// off -> silent
|
|
11
|
+
// nudge -> print a one-liner (default)
|
|
12
|
+
// auto -> run `npm update` and re-link skills
|
|
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();
|
|
283
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theglitchking/semantic-pages",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
".claude-plugin",
|
|
19
19
|
"hooks",
|
|
20
|
+
"commands",
|
|
21
|
+
"scripts/link-skills.js",
|
|
20
22
|
"skills/semantic-first",
|
|
21
23
|
"README.md",
|
|
22
24
|
"LICENSE"
|
|
@@ -27,7 +29,8 @@
|
|
|
27
29
|
"test": "vitest run",
|
|
28
30
|
"test:watch": "vitest",
|
|
29
31
|
"lint": "tsc --noEmit",
|
|
30
|
-
"prepublishOnly": "npm run build"
|
|
32
|
+
"prepublishOnly": "npm run build",
|
|
33
|
+
"postinstall": "node scripts/link-skills.js"
|
|
31
34
|
},
|
|
32
35
|
"keywords": [
|
|
33
36
|
"mcp",
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Postinstall hook. Three jobs (all idempotent, all fail-open):
|
|
3
|
+
// 1. Symlink bundled skills into <consumer>/.claude/skills/ so Claude Code
|
|
4
|
+
// can discover them (it doesn't scan node_modules/).
|
|
5
|
+
// 2. Write a default update-policy config at <consumer>/.claude/semantic-pages.json
|
|
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.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
existsSync,
|
|
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";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
|
|
34
|
+
const warn = (msg) => console.warn(`[semantic-pages] ${msg}`);
|
|
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
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
main();
|
|
203
|
+
} catch (err) {
|
|
204
|
+
warn(`postinstall failed: ${err?.message || err}`);
|
|
205
|
+
}
|
package/hooks/session-start.sh
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# semantic-pages SessionStart hook
|
|
4
|
-
#
|
|
5
|
-
# Reconciles the project's .mcp.json so that:
|
|
6
|
-
# 1. A "semantic-vault" entry always points at ./.claude/.vault (read/write).
|
|
7
|
-
# 2. A "semantic-pages" entry points at ./.documentation (read-only) ONLY IF
|
|
8
|
-
# both conditions hold:
|
|
9
|
-
# - hit-em-with-the-docs is installed+enabled as a Claude Code plugin
|
|
10
|
-
# - ./.documentation/ exists in this project
|
|
11
|
-
# Otherwise any stale "semantic-pages" entry is removed.
|
|
12
|
-
#
|
|
13
|
-
# Idempotent: only writes .mcp.json when the computed JSON differs from disk.
|
|
14
|
-
# Fails open: any error logs to stderr and exits 0 so Claude Code isn't blocked.
|
|
15
|
-
#
|
|
16
|
-
# Runs on every SessionStart event. Keeps wiring in sync with plugin state.
|
|
17
|
-
|
|
18
|
-
set -u
|
|
19
|
-
|
|
20
|
-
# Fail-open: if anything goes sideways, swallow and return the empty SessionStart
|
|
21
|
-
# response so the session keeps going.
|
|
22
|
-
trap 'emit_empty_response; exit 0' ERR
|
|
23
|
-
|
|
24
|
-
emit_empty_response() {
|
|
25
|
-
# SessionStart hooks need to return JSON; empty additionalContext is fine.
|
|
26
|
-
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart"}}\n'
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
log() {
|
|
30
|
-
printf 'semantic-pages hook: %s\n' "$*" >&2
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
# Project root is cwd when Claude Code invokes the hook.
|
|
34
|
-
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
35
|
-
cd "$PROJECT_ROOT" || { emit_empty_response; exit 0; }
|
|
36
|
-
|
|
37
|
-
VAULT_DIR="$PROJECT_ROOT/.claude/.vault"
|
|
38
|
-
DOCS_DIR="$PROJECT_ROOT/.documentation"
|
|
39
|
-
MCP_JSON="$PROJECT_ROOT/.mcp.json"
|
|
40
|
-
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
|
41
|
-
|
|
42
|
-
# 1. Ensure .claude/.vault exists (idempotent).
|
|
43
|
-
mkdir -p "$VAULT_DIR" 2>/dev/null || true
|
|
44
|
-
|
|
45
|
-
# 2. Detect hit-em-with-the-docs: must be listed in enabledPlugins in
|
|
46
|
-
# ~/.claude/settings.json (covers any marketplace source).
|
|
47
|
-
HEWTD_ENABLED=0
|
|
48
|
-
if [ -f "$CLAUDE_SETTINGS" ]; then
|
|
49
|
-
if node -e '
|
|
50
|
-
const fs = require("fs");
|
|
51
|
-
try {
|
|
52
|
-
const s = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
53
|
-
const enabled = s.enabledPlugins || {};
|
|
54
|
-
const hit = Object.keys(enabled).some(
|
|
55
|
-
(k) => k.startsWith("hit-em-with-the-docs@") && enabled[k] === true
|
|
56
|
-
);
|
|
57
|
-
process.exit(hit ? 0 : 1);
|
|
58
|
-
} catch { process.exit(1); }
|
|
59
|
-
' "$CLAUDE_SETTINGS" 2>/dev/null; then
|
|
60
|
-
HEWTD_ENABLED=1
|
|
61
|
-
fi
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
# 3. Docs MCP entry is conditional on BOTH hewtd enabled AND .documentation present.
|
|
65
|
-
DOCS_WIRED=0
|
|
66
|
-
if [ "$HEWTD_ENABLED" = "1" ] && [ -d "$DOCS_DIR" ]; then
|
|
67
|
-
DOCS_WIRED=1
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
# 4. Reconcile .mcp.json using node (cross-platform JSON edit, idempotent write).
|
|
71
|
-
node - "$MCP_JSON" "$DOCS_WIRED" <<'NODE' || { log "reconcile failed"; emit_empty_response; exit 0; }
|
|
72
|
-
const fs = require("fs");
|
|
73
|
-
const path = require("path");
|
|
74
|
-
const [, , mcpPath, docsWiredArg] = process.argv;
|
|
75
|
-
const docsWired = docsWiredArg === "1";
|
|
76
|
-
|
|
77
|
-
let data = { mcpServers: {} };
|
|
78
|
-
if (fs.existsSync(mcpPath)) {
|
|
79
|
-
try {
|
|
80
|
-
const raw = fs.readFileSync(mcpPath, "utf8");
|
|
81
|
-
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
82
|
-
if (parsed && typeof parsed === "object") data = parsed;
|
|
83
|
-
if (!data.mcpServers || typeof data.mcpServers !== "object") data.mcpServers = {};
|
|
84
|
-
} catch (err) {
|
|
85
|
-
// Corrupt .mcp.json — leave it alone and emit a note to stderr
|
|
86
|
-
process.stderr.write(
|
|
87
|
-
`semantic-pages hook: could not parse ${mcpPath} (${err.message}); leaving untouched\n`
|
|
88
|
-
);
|
|
89
|
-
process.exit(0);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Canonical entries
|
|
94
|
-
const vaultEntry = {
|
|
95
|
-
type: "stdio",
|
|
96
|
-
command: "npx",
|
|
97
|
-
args: [
|
|
98
|
-
"-y",
|
|
99
|
-
"@theglitchking/semantic-pages@latest",
|
|
100
|
-
"--notes",
|
|
101
|
-
"./.claude/.vault",
|
|
102
|
-
],
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const docsEntry = {
|
|
106
|
-
type: "stdio",
|
|
107
|
-
command: "npx",
|
|
108
|
-
args: [
|
|
109
|
-
"-y",
|
|
110
|
-
"@theglitchking/semantic-pages@latest",
|
|
111
|
-
"--notes",
|
|
112
|
-
"./.documentation",
|
|
113
|
-
"--read-only",
|
|
114
|
-
],
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const before = JSON.stringify(data);
|
|
118
|
-
|
|
119
|
-
// Always ensure semantic-vault
|
|
120
|
-
data.mcpServers["semantic-vault"] = vaultEntry;
|
|
121
|
-
|
|
122
|
-
// semantic-pages (docs) is conditional
|
|
123
|
-
if (docsWired) {
|
|
124
|
-
data.mcpServers["semantic-pages"] = docsEntry;
|
|
125
|
-
} else if (data.mcpServers["semantic-pages"]) {
|
|
126
|
-
// Only remove if it looks like ours (points at .documentation). Don't clobber
|
|
127
|
-
// a user-custom entry under the same name.
|
|
128
|
-
const existing = data.mcpServers["semantic-pages"];
|
|
129
|
-
const args = Array.isArray(existing.args) ? existing.args : [];
|
|
130
|
-
const looksLikeOurs =
|
|
131
|
-
args.some((a) => typeof a === "string" && a.includes(".documentation"));
|
|
132
|
-
if (looksLikeOurs) delete data.mcpServers["semantic-pages"];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const after = JSON.stringify(data, null, 2) + "\n";
|
|
136
|
-
|
|
137
|
-
// Only write if content actually changed — prevents needless git churn.
|
|
138
|
-
if (JSON.stringify(JSON.parse(after)) === before) {
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
fs.mkdirSync(path.dirname(mcpPath), { recursive: true });
|
|
143
|
-
fs.writeFileSync(mcpPath, after);
|
|
144
|
-
NODE
|
|
145
|
-
|
|
146
|
-
emit_empty_response
|
|
147
|
-
exit 0
|