@dotdotgod/claude-code 0.3.2 → 0.3.4

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/README.md CHANGED
@@ -34,6 +34,8 @@ Then use the bundled `/dd:*` commands in your repository:
34
34
  /dd:impact
35
35
  ```
36
36
 
37
+ After an update, restart Claude Code and confirm the active plugin points to the new installed cache entry. Source changes do not rewrite an existing `~/.claude/plugins/cache/.../<hash>/`; reinstall the plugin through the supported flow if Claude continues using an older cache hash.
38
+
37
39
  For local development from a source checkout, load the plugin directory directly instead of installing:
38
40
 
39
41
  ```bash
@@ -45,7 +47,7 @@ claude --plugin-dir /path/to/dotdotgod/packages/claude-code
45
47
  | Command or skill | Use it for | Result |
46
48
  | --- | --- | --- |
47
49
  | `/dd:init` | Start a repository with dotdotgod conventions. | Creates or normalizes shared agent files, docs indexes, local-memory areas, and the complete default project config. |
48
- | `/dd:load` | Load project memory without changing maintained project files. | Renders the shared Markdown tree and uses `dotdotgod query` when focus text is provided; query may refresh ignored caches. |
50
+ | `/dd:load` | Load project memory without changing maintained project files. | Renders the shared Markdown tree and uses `dotdotgod query` when focus text is provided; unavailable optional semantic routing falls back to the map with bounded evidence. |
49
51
  | `/dd:plan` | Plan before implementation. | Writes or updates durable task intent in `docs/plan/<task-slug>/README.md`. |
50
52
  | `/dd:impact` | Review changed files before verification or handoff. | Uses `dotdotgod graph impact` to identify likely related docs, tests, commands, and source files. |
51
53
  | `document-clarify` | Improve docs wording without changing behavior contracts. | Clarifies README/spec/test/arch/plan/archive docs using memory-area roles. |
package/hooks/hooks.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "hooks": {
3
3
  "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.mjs\" sessionstart" }] }],
4
- "PreToolUse": [{ "matcher": "Bash|Shell|Edit|Write|mcp__", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.mjs\" pretooluse" }] }],
5
- "PostToolUse": [{ "matcher": "Edit|Write|mcp__", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.mjs\" posttooluse" }] }]
4
+ "PreToolUse": [{ "matcher": "Bash|Shell|Edit|Write|mcp__.*", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.mjs\" pretooluse" }] }],
5
+ "PostToolUse": [{ "matcher": "Edit|Write|mcp__.*", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.mjs\" posttooluse" }] }]
6
6
  }
7
7
  }
package/hooks/runtime.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // packages/context/src/hooks.mjs
5
5
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
6
  import { createHash } from "node:crypto";
7
- import { dirname, join, resolve } from "node:path";
7
+ import { dirname, join, parse, resolve } from "node:path";
8
8
  function inputText() {
9
9
  return readFileSync(0, "utf8");
10
10
  }
@@ -12,24 +12,45 @@ function parseInput() {
12
12
  const text = inputText().trim();
13
13
  return text ? JSON.parse(text) : {};
14
14
  }
15
- function projectRoot(input) {
16
- return resolve(input.cwd || input.project_dir || process.env.CLAUDE_PROJECT_DIR || process.cwd());
17
- }
18
15
  function sessionId(input) {
19
16
  return String(input.session_id || input.sessionId || "default").replace(/[^a-zA-Z0-9._-]/g, "_");
20
17
  }
21
- function statePath(input) {
22
- return join(projectRoot(input), ".dotdotgod", "context", "runtime", `${sessionId(input)}.json`);
18
+ function runtimeStatePath(root, id) {
19
+ return join(root, ".dotdotgod", "context", "runtime", `${id}.json`);
20
+ }
21
+ function declaredProjectRoot(input) {
22
+ const value = input.project_dir || process.env.CLAUDE_PROJECT_DIR;
23
+ return typeof value === "string" && value.trim() ? resolve(value) : null;
24
+ }
25
+ function findExistingStateRoot(cwd, id) {
26
+ let current = resolve(cwd);
27
+ const filesystemRoot = parse(current).root;
28
+ while (true) {
29
+ if (existsSync(runtimeStatePath(current, id))) return current;
30
+ if (current === filesystemRoot) return null;
31
+ current = dirname(current);
32
+ }
23
33
  }
24
- function readState(input) {
34
+ function resolveHookContext(input, event) {
35
+ const id = sessionId(input);
36
+ const declared = declaredProjectRoot(input);
37
+ if (declared) return { root: declared, path: runtimeStatePath(declared, id) };
38
+ if (typeof input.cwd !== "string" || !input.cwd.trim()) return null;
39
+ const existing = findExistingStateRoot(input.cwd, id);
40
+ if (existing) return { root: existing, path: runtimeStatePath(existing, id) };
41
+ if (event !== "sessionstart") return null;
42
+ const root = resolve(input.cwd);
43
+ return { root, path: runtimeStatePath(root, id) };
44
+ }
45
+ function readState(path) {
25
46
  try {
26
- return JSON.parse(readFileSync(statePath(input), "utf8"));
47
+ return JSON.parse(readFileSync(path, "utf8"));
27
48
  } catch {
28
49
  return { loadRequired: false, pending: {} };
29
50
  }
30
51
  }
31
- function writeState(input, state) {
32
- const path = statePath(input);
52
+ function writeState(path, state) {
53
+ if (!path) return;
33
54
  mkdirSync(dirname(path), { recursive: true });
34
55
  const temp = `${path}.${process.pid}.tmp`;
35
56
  writeFileSync(temp, `${JSON.stringify(state, null, 2)}
@@ -66,19 +87,21 @@ function isImpactGate(name, input) {
66
87
  if (!/^(Bash|Shell|local_shell|shell|shell_command|exec_command)$/i.test(name)) return false;
67
88
  return /(^|\s)(test|build|lint|verify|commit|push|publish|deploy)(\s|:|$)|\b(git\s+(commit|push)|npm\s+(test|publish)|pnpm\s+[^\n]*(test|build|lint|verify|publish))\b/i.test(commandFrom(input));
68
89
  }
69
- function changedPath(input) {
90
+ function changedPath(input, root) {
70
91
  const value = toolInput(input);
71
92
  const path = value.file_path || value.path;
72
- return typeof path === "string" ? resolve(projectRoot(input), path) : null;
93
+ return root && typeof path === "string" ? resolve(root, path) : null;
73
94
  }
74
95
  function runHook(event) {
75
96
  const input = parseInput();
76
97
  const name = toolName(input);
77
- const state = readState(input);
98
+ const context = resolveHookContext(input, event);
99
+ const state = readState(context?.path);
78
100
  state.pending ||= {};
79
101
  if (event === "sessionstart") {
102
+ if (!context) return {};
80
103
  state.loadRequired = true;
81
- writeState(input, state);
104
+ writeState(context.path, state);
82
105
  return { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: "Dotdotgod project memory has not been loaded for this session. Before substantive shell or write work, call dotdotgod_project_load once with a concise task-specific focus." } };
83
106
  }
84
107
  if (event === "pretooluse") {
@@ -89,18 +112,19 @@ function runHook(event) {
89
112
  return {};
90
113
  }
91
114
  if (event === "posttooluse") {
115
+ if (!context) return {};
92
116
  if (mcpTool(name, "dotdotgod_project_load")) state.loadRequired = false;
93
117
  else if (mcpTool(name, "dotdotgod_project_impact")) {
94
118
  const checked = toolInput(input).paths;
95
119
  if (Array.isArray(checked)) for (const path of checked) {
96
- const absolute = resolve(projectRoot(input), path);
120
+ const absolute = resolve(context.root, path);
97
121
  if (state.pending[absolute] === fileHash(absolute)) delete state.pending[absolute];
98
122
  }
99
123
  } else if (/^(Edit|Write|apply_patch)$/i.test(name)) {
100
- const path = changedPath(input);
124
+ const path = changedPath(input, context.root);
101
125
  if (path) state.pending[path] = fileHash(path);
102
126
  }
103
- writeState(input, state);
127
+ writeState(context.path, state);
104
128
  return {};
105
129
  }
106
130
  return {};
package/mcp/cli.mjs CHANGED
@@ -1309,6 +1309,20 @@ Create dotdotgod.config.json from a bundled or user template. Without --template
1309
1309
  dotdotgod query <root> <query> [--limit n] [--json]
1310
1310
 
1311
1311
  Search shared project documentation with the resolved local or remote embedding provider.`;
1312
+ case "embedding":
1313
+ return `Usage:
1314
+ dotdotgod embedding status [<root>] [--json]
1315
+ dotdotgod embedding install [<root>] --confirm [--json]`;
1316
+ case "embedding status":
1317
+ return `Usage:
1318
+ dotdotgod embedding status [<root>] [--json]
1319
+
1320
+ Inspect the optional persistent local embedding runtime without network access.`;
1321
+ case "embedding install":
1322
+ return `Usage:
1323
+ dotdotgod embedding install [<root>] --confirm [--json]
1324
+
1325
+ After user approval, install the fixed optional runtime persistently. Uses network access and dependency install scripts; the model may download on first query.`;
1312
1326
  case "map":
1313
1327
  return `Usage:
1314
1328
  dotdotgod map <root> [--depth <positive-integer>] [--json]
@@ -1351,6 +1365,7 @@ Commands:
1351
1365
  init Initialize project memory files.
1352
1366
  config Inspect or initialize project configuration.
1353
1367
  query Search project documentation.
1368
+ embedding Inspect or install the optional local embedding runtime.
1354
1369
  map Render the project documentation map.
1355
1370
  resolve Resolve a project reference.
1356
1371
  expand Expand references in a prompt.
@@ -1403,6 +1418,7 @@ function helpCommandFromArgs(args) {
1403
1418
  if (nonHelp[0] === "graph" && nonHelp[1]) return `graph ${nonHelp[1]}`;
1404
1419
  if (nonHelp[0] === "traceability" && nonHelp[1]) return `traceability ${nonHelp[1]}`;
1405
1420
  if (nonHelp[0] === "config" && nonHelp[1] === "init") return "config init";
1421
+ if (nonHelp[0] === "embedding" && nonHelp[1]) return `embedding ${nonHelp[1]}`;
1406
1422
  return nonHelp[0] ?? "root";
1407
1423
  }
1408
1424
  function parseCommon(argv) {
@@ -2709,6 +2725,74 @@ function collectDocumentationChunks(root, exclude, documentationRoot = "docs") {
2709
2725
  });
2710
2726
  }
2711
2727
 
2728
+ // packages/cli/src/query/embedding-runtime.mjs
2729
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync11, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
2730
+ import { spawnSync as spawnSync2 } from "node:child_process";
2731
+ import { createRequire } from "node:module";
2732
+ import { homedir as homedir3 } from "node:os";
2733
+ import { join as join11 } from "node:path";
2734
+ import { pathToFileURL } from "node:url";
2735
+ var EMBEDDING_RUNTIME_PACKAGE = "@huggingface/transformers";
2736
+ var EMBEDDING_RUNTIME_VERSION = "4.2.0";
2737
+ var EmbeddingRuntimeMissingError = class extends Error {
2738
+ constructor() {
2739
+ super("Optional local embedding runtime is not installed.");
2740
+ this.name = "EmbeddingRuntimeMissingError";
2741
+ this.code = "EMBEDDING_RUNTIME_MISSING";
2742
+ }
2743
+ };
2744
+ function embeddingRuntimeRoot(options = {}) {
2745
+ return join11(options.home ?? homedir3(), ".dotdotgod", "runtime", "embedding");
2746
+ }
2747
+ function resolvePersistentTransformers(options = {}) {
2748
+ const root = embeddingRuntimeRoot(options);
2749
+ try {
2750
+ const require2 = createRequire(join11(root, "package.json"));
2751
+ return require2.resolve(EMBEDDING_RUNTIME_PACKAGE);
2752
+ } catch {
2753
+ throw new EmbeddingRuntimeMissingError();
2754
+ }
2755
+ }
2756
+ function embeddingRuntimeStatus(options = {}) {
2757
+ const root = embeddingRuntimeRoot(options);
2758
+ try {
2759
+ const require2 = createRequire(join11(root, "package.json"));
2760
+ const packagePath = require2.resolve(`${EMBEDDING_RUNTIME_PACKAGE}/package.json`);
2761
+ const version = JSON.parse(readFileSync11(packagePath, "utf8")).version;
2762
+ return { ok: true, installed: true, installAvailable: true, package: EMBEDDING_RUNTIME_PACKAGE, requiredVersion: EMBEDDING_RUNTIME_VERSION, packageVersion: version, location: root };
2763
+ } catch {
2764
+ return { ok: true, installed: false, installAvailable: true, package: EMBEDDING_RUNTIME_PACKAGE, requiredVersion: EMBEDDING_RUNTIME_VERSION, packageVersion: null, location: root };
2765
+ }
2766
+ }
2767
+ function installEmbeddingRuntime(options = {}) {
2768
+ if (options.confirm !== true) throw new Error("Embedding runtime installation requires explicit confirmation.");
2769
+ const root = embeddingRuntimeRoot(options);
2770
+ const lock = join11(root, ".installing");
2771
+ if (existsSync12(lock)) throw new Error("Embedding runtime installation is already in progress.");
2772
+ mkdirSync3(root, { recursive: true });
2773
+ writeFileSync4(lock, `${process.pid}
2774
+ `, { flag: "wx" });
2775
+ try {
2776
+ const spawn = options.spawnImpl ?? spawnSync2;
2777
+ const result = spawn(options.npmCommand ?? "npm", ["install", "--prefix", root, "--save-exact", "--omit=dev", "--no-audit", "--no-fund", `${EMBEDDING_RUNTIME_PACKAGE}@${EMBEDDING_RUNTIME_VERSION}`], { encoding: "utf8", timeout: options.timeoutMs ?? 3e5, shell: false, stdio: ["ignore", "pipe", "pipe"] });
2778
+ if (result.error || result.status !== 0) throw new Error(result.error?.code === "ENOENT" ? "npm is unavailable." : "Embedding runtime installation failed.");
2779
+ const status = embeddingRuntimeStatus({ home: options.home });
2780
+ if (!status.installed) throw new Error("Embedding runtime installation could not be verified.");
2781
+ return { ...status, command: "embedding install", modelDownloadPending: true };
2782
+ } finally {
2783
+ rmSync(lock, { force: true });
2784
+ }
2785
+ }
2786
+ async function importTransformers(options = {}) {
2787
+ try {
2788
+ return await import(EMBEDDING_RUNTIME_PACKAGE);
2789
+ } catch (error) {
2790
+ if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error;
2791
+ const path = resolvePersistentTransformers(options);
2792
+ return import(pathToFileURL(path).href);
2793
+ }
2794
+ }
2795
+
2712
2796
  // packages/cli/src/query/embedder.mjs
2713
2797
  var localPipelines = /* @__PURE__ */ new Map();
2714
2798
  function normalize(row) {
@@ -2719,7 +2803,7 @@ function normalize(row) {
2719
2803
  }
2720
2804
  async function localEmbedder(profile) {
2721
2805
  if (!localPipelines.has(profile.model)) {
2722
- localPipelines.set(profile.model, import("@huggingface/transformers").then(async ({ env, pipeline }) => {
2806
+ localPipelines.set(profile.model, importTransformers().then(async ({ env, pipeline }) => {
2723
2807
  env.allowLocalModels = true;
2724
2808
  return pipeline("feature-extraction", profile.model);
2725
2809
  }));
@@ -2787,8 +2871,8 @@ async function resolveEmbedder(root, options = {}) {
2787
2871
 
2788
2872
  // packages/cli/src/query/store.mjs
2789
2873
  import { createHash as createHash4 } from "node:crypto";
2790
- import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync11, renameSync, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
2791
- import { join as join11 } from "node:path";
2874
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync12, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
2875
+ import { join as join12 } from "node:path";
2792
2876
  var VECTOR_SCHEMA_VERSION = 2;
2793
2877
  var VECTOR_DIMENSIONS = 384;
2794
2878
  function profileFingerprint(identity) {
@@ -2808,18 +2892,18 @@ function isValidNormalizedVectorValues(values, dimensions = VECTOR_DIMENSIONS) {
2808
2892
  return true;
2809
2893
  }
2810
2894
  function vectorCachePaths(root) {
2811
- const directory = join11(root, ".dotdotgod", "vectors");
2812
- return { directory, manifest: join11(directory, "manifest.json"), chunks: join11(directory, "chunks.jsonl"), embeddings: join11(directory, "embeddings.f32") };
2895
+ const directory = join12(root, ".dotdotgod", "vectors");
2896
+ return { directory, manifest: join12(directory, "manifest.json"), chunks: join12(directory, "chunks.jsonl"), embeddings: join12(directory, "embeddings.f32") };
2813
2897
  }
2814
2898
  function readVectorCache(root, identity = null) {
2815
2899
  const paths = vectorCachePaths(root);
2816
- if (!existsSync12(paths.manifest) || !existsSync12(paths.chunks) || !existsSync12(paths.embeddings)) return null;
2900
+ if (!existsSync13(paths.manifest) || !existsSync13(paths.chunks) || !existsSync13(paths.embeddings)) return null;
2817
2901
  try {
2818
- const manifest = JSON.parse(readFileSync11(paths.manifest, "utf8"));
2902
+ const manifest = JSON.parse(readFileSync12(paths.manifest, "utf8"));
2819
2903
  if (manifest.schemaVersion !== VECTOR_SCHEMA_VERSION || !Number.isInteger(manifest.dimensions) || manifest.dimensions < 1) return null;
2820
2904
  if (identity && manifest.profileFingerprint !== profileFingerprint(identity)) return null;
2821
- const chunks = readFileSync11(paths.chunks, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
2822
- const buffer = readFileSync11(paths.embeddings);
2905
+ const chunks = readFileSync12(paths.chunks, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
2906
+ const buffer = readFileSync12(paths.embeddings);
2823
2907
  if (buffer.byteLength !== chunks.length * manifest.dimensions * 4) return null;
2824
2908
  const vectors = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
2825
2909
  if (!isValidNormalizedVectorValues(vectors, manifest.dimensions)) return null;
@@ -2830,14 +2914,14 @@ function readVectorCache(root, identity = null) {
2830
2914
  }
2831
2915
  function atomicWrite(path, data) {
2832
2916
  const temporary = `${path}.tmp-${process.pid}`;
2833
- writeFileSync4(temporary, data);
2917
+ writeFileSync5(temporary, data);
2834
2918
  renameSync(temporary, path);
2835
2919
  }
2836
2920
  function writeVectorCache(root, chunks, vectors, metadata = {}) {
2837
2921
  const paths = vectorCachePaths(root);
2838
2922
  const dimensions = metadata.dimensions;
2839
2923
  if (!Number.isInteger(dimensions) || dimensions < 1) throw new Error("Vector dimensions must be a positive integer.");
2840
- mkdirSync3(paths.directory, { recursive: true });
2924
+ mkdirSync4(paths.directory, { recursive: true });
2841
2925
  if (vectors.length !== chunks.length * dimensions) throw new Error("Vector count does not match chunk metadata.");
2842
2926
  if (!isValidNormalizedVectorValues(vectors, dimensions)) throw new Error("Vectors must contain finite normalized values.");
2843
2927
  const manifest = { schemaVersion: VECTOR_SCHEMA_VERSION, chunks: chunks.length, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...metadata };
@@ -2976,12 +3060,44 @@ async function runQuery(argv) {
2976
3060
  }
2977
3061
  }
2978
3062
 
3063
+ // packages/cli/src/commands/embedding.mjs
3064
+ import { resolve as resolve10 } from "node:path";
3065
+ function parse(argv) {
3066
+ const [action2 = "status", ...rest] = argv;
3067
+ let root = ".";
3068
+ let json = false;
3069
+ let confirm = false;
3070
+ for (const arg of rest) {
3071
+ if (arg === "--json") json = true;
3072
+ else if (arg === "--confirm") confirm = true;
3073
+ else if (!arg.startsWith("-") && root === ".") root = arg;
3074
+ else usage(`Unknown embedding option: ${arg}`, `embedding ${action2}`);
3075
+ }
3076
+ return { action: action2, root: resolve10(root), json, confirm };
3077
+ }
3078
+ function runEmbedding(argv, options = {}) {
3079
+ const parsed = parse(argv);
3080
+ if (!["status", "install"].includes(parsed.action)) usage(`Unknown embedding command: ${parsed.action}`, "embedding");
3081
+ const resolved = resolveEmbeddingProfile(parsed.root, options);
3082
+ if (resolved.profile.provider !== "local") {
3083
+ const result2 = { ok: true, command: `embedding ${parsed.action}`, provider: resolved.profile.provider, required: false, installed: false, installAvailable: false };
3084
+ console.log(parsed.json ? JSON.stringify(result2, null, 2) : "Local embedding runtime is not required for the configured remote provider.");
3085
+ return result2;
3086
+ }
3087
+ if (parsed.action === "install" && !parsed.confirm) usage("embedding install requires --confirm after user approval.", "embedding install");
3088
+ const runtimeOptions = { home: options.home };
3089
+ const result = parsed.action === "status" ? embeddingRuntimeStatus(runtimeOptions) : installEmbeddingRuntime({ ...runtimeOptions, confirm: true, spawnImpl: options.spawnImpl, npmCommand: options.npmCommand });
3090
+ const payload = { ...result, command: `embedding ${parsed.action}`, provider: "local", required: true, ...parsed.action === "install" ? { disclosure: "Installation uses network access and dependency install scripts; the configured model may download on first query." } : {} };
3091
+ console.log(parsed.json ? JSON.stringify(payload, null, 2) : payload.installed ? `Embedding runtime installed (${payload.packageVersion}).` : "Embedding runtime is not installed.");
3092
+ return payload;
3093
+ }
3094
+
2979
3095
  // packages/cli/src/commands/map.mjs
2980
- import { resolve as resolve11 } from "node:path";
3096
+ import { resolve as resolve12 } from "node:path";
2981
3097
 
2982
3098
  // packages/cli/src/memory/documentation-map.mjs
2983
- import { existsSync as existsSync13, readdirSync as readdirSync4, statSync as statSync6 } from "node:fs";
2984
- import { join as join12, resolve as resolve10 } from "node:path";
3099
+ import { existsSync as existsSync14, readdirSync as readdirSync4, statSync as statSync6 } from "node:fs";
3100
+ import { join as join13, resolve as resolve11 } from "node:path";
2985
3101
  var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", ".git", ".dotdotgod"]);
2986
3102
  function node() {
2987
3103
  return { directories: /* @__PURE__ */ new Map(), files: [] };
@@ -3044,7 +3160,7 @@ function discoverMarkdown(root, documentationRoot, exclude) {
3044
3160
  const walk = (relativeDirectory) => {
3045
3161
  let entries;
3046
3162
  try {
3047
- entries = readdirSync4(join12(root, relativeDirectory), { withFileTypes: true, encoding: "utf8" });
3163
+ entries = readdirSync4(join13(root, relativeDirectory), { withFileTypes: true, encoding: "utf8" });
3048
3164
  } catch {
3049
3165
  return;
3050
3166
  }
@@ -3061,8 +3177,8 @@ function discoverMarkdown(root, documentationRoot, exclude) {
3061
3177
  return paths.sort();
3062
3178
  }
3063
3179
  function buildDocumentationMap(projectRoot = ".", { depth = 5 } = {}) {
3064
- const root = resolve10(projectRoot);
3065
- if (!existsSync13(root)) return { ok: false, error: { code: "ROOT_NOT_FOUND", message: `Project root not found: ${root}` } };
3180
+ const root = resolve11(projectRoot);
3181
+ if (!existsSync14(root)) return { ok: false, error: { code: "ROOT_NOT_FOUND", message: `Project root not found: ${root}` } };
3066
3182
  try {
3067
3183
  if (!statSync6(root).isDirectory()) return { ok: false, error: { code: "ROOT_NOT_FOUND", message: `Project root not found: ${root}` } };
3068
3184
  } catch {
@@ -3110,7 +3226,7 @@ function parseMapOptions(argv) {
3110
3226
  } else if (!arg.startsWith("-")) fail(options.json, "UNEXPECTED_ARGUMENT", `Unexpected argument: ${arg}`);
3111
3227
  else fail(options.json, "UNKNOWN_OPTION", `Unknown option: ${arg}`);
3112
3228
  }
3113
- options.root = resolve11(options.root);
3229
+ options.root = resolve12(options.root);
3114
3230
  return options;
3115
3231
  }
3116
3232
  function runMap(argv) {
@@ -3122,7 +3238,7 @@ function runMap(argv) {
3122
3238
  }
3123
3239
 
3124
3240
  // packages/cli/src/reference/resolve.mjs
3125
- import { resolve as resolve13 } from "node:path";
3241
+ import { resolve as resolve14 } from "node:path";
3126
3242
 
3127
3243
  // node_modules/.pnpm/leiden-ts@0.1.0/node_modules/leiden-ts/dist/index.js
3128
3244
  var GraphValidationError = class extends Error {
@@ -4493,15 +4609,15 @@ function compareImpactItems(seeds) {
4493
4609
  }
4494
4610
 
4495
4611
  // packages/cli/src/impact/vector-profile.mjs
4496
- import { lstatSync, readFileSync as readFileSync12, realpathSync } from "node:fs";
4497
- import { extname as extname5, relative as relative4, resolve as resolve12, sep } from "node:path";
4612
+ import { lstatSync, readFileSync as readFileSync13, realpathSync } from "node:fs";
4613
+ import { extname as extname5, relative as relative4, resolve as resolve13, sep } from "node:path";
4498
4614
  var MAX_VECTOR_PROFILE_FILE_BYTES = 64 * 1024;
4499
4615
  var MAX_VECTOR_PROFILE_CHARS = 4e3;
4500
4616
  var MAX_VECTOR_PROFILE_METADATA_ITEMS = 20;
4501
4617
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".cc", ".cpp", ".css", ".go", ".h", ".hpp", ".html", ".java", ".js", ".json", ".jsx", ".md", ".mjs", ".py", ".rb", ".rs", ".sh", ".ts", ".tsx", ".txt", ".yaml", ".yml"]);
4502
4618
  var GENERATED_SEGMENTS = /* @__PURE__ */ new Set([".dotdotgod", "build", "coverage", "dist", "node_modules"]);
4503
4619
  function insideRoot(root, absolute) {
4504
- const normalizedRoot = resolve12(root);
4620
+ const normalizedRoot = resolve13(root);
4505
4621
  return absolute === normalizedRoot || absolute.startsWith(`${normalizedRoot}${sep}`);
4506
4622
  }
4507
4623
  function normalizeChangedPath(path) {
@@ -4512,8 +4628,8 @@ function normalizeChangedPath(path) {
4512
4628
  function canonicalizeChangedPath(root, path) {
4513
4629
  const normalized = normalizeChangedPath(path);
4514
4630
  if (!normalized) return null;
4515
- const rootAbsolute = resolve12(root);
4516
- const absolute = resolve12(rootAbsolute, normalized);
4631
+ const rootAbsolute = resolve13(root);
4632
+ const absolute = resolve13(rootAbsolute, normalized);
4517
4633
  if (!insideRoot(rootAbsolute, absolute)) return normalized;
4518
4634
  try {
4519
4635
  const canonicalRoot = realpathSync(rootAbsolute);
@@ -4544,7 +4660,7 @@ function graphMetadata(graph, seedId) {
4544
4660
  function safeTextPrefix(root, path) {
4545
4661
  if (unsafeProfilePath(path) || !TEXT_EXTENSIONS.has(extname5(path).toLowerCase())) return "";
4546
4662
  try {
4547
- const absolute = resolve12(root, path);
4663
+ const absolute = resolve13(root, path);
4548
4664
  if (!insideRoot(root, absolute)) return "";
4549
4665
  const canonicalAbsolute = realpathSync(absolute);
4550
4666
  if (!insideRoot(realpathSync(root), canonicalAbsolute)) return "";
@@ -4552,7 +4668,7 @@ function safeTextPrefix(root, path) {
4552
4668
  if (canonicalPath !== path || unsafeProfilePath(canonicalPath) || !TEXT_EXTENSIONS.has(extname5(canonicalPath).toLowerCase())) return "";
4553
4669
  const stats = lstatSync(canonicalAbsolute);
4554
4670
  if (!stats.isFile() || stats.isSymbolicLink() || stats.size > MAX_VECTOR_PROFILE_FILE_BYTES) return "";
4555
- const buffer = readFileSync12(canonicalAbsolute);
4671
+ const buffer = readFileSync13(canonicalAbsolute);
4556
4672
  if (buffer.includes(0)) return "";
4557
4673
  const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
4558
4674
  return text.slice(0, MAX_VECTOR_PROFILE_CHARS);
@@ -4945,7 +5061,7 @@ function parseReferenceOptions(argv, command) {
4945
5061
  else filtered.push(arg);
4946
5062
  }
4947
5063
  const operands = filtered.filter((arg) => !arg.startsWith("-"));
4948
- return { ...options, root: resolve13(operands[0] ?? "."), json: filtered.includes("--json"), rootArgv: operands.slice(1) };
5064
+ return { ...options, root: resolve14(operands[0] ?? "."), json: filtered.includes("--json"), rootArgv: operands.slice(1) };
4949
5065
  }
4950
5066
  function formatReferenceOutput(payload) {
4951
5067
  const refreshNote = payload.metadata.cacheRefreshed ? ", refreshed" : "";
@@ -4994,15 +5110,15 @@ function runExpand(argv) {
4994
5110
  }
4995
5111
 
4996
5112
  // packages/cli/src/commands/traceability.mjs
4997
- import { existsSync as existsSync14, readFileSync as readFileSync13, readdirSync as readdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
4998
- import { join as join13, resolve as resolve14 } from "node:path";
5113
+ import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
5114
+ import { join as join14, resolve as resolve15 } from "node:path";
4999
5115
  function collectDocsMarkdownFiles(root) {
5000
- const docs = join13(root, "docs");
5116
+ const docs = join14(root, "docs");
5001
5117
  const files = [];
5002
5118
  const walk = (dir) => {
5003
- if (!existsSync14(dir)) return;
5119
+ if (!existsSync15(dir)) return;
5004
5120
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5005
- const path = join13(dir, entry.name);
5121
+ const path = join14(dir, entry.name);
5006
5122
  if (entry.isDirectory()) walk(path);
5007
5123
  else if (entry.isFile() && entry.name.endsWith(".md")) files.push(path);
5008
5124
  }
@@ -5027,7 +5143,7 @@ function parseTraceabilityOptions(argv) {
5027
5143
  }
5028
5144
  if (options.check && options.write) usage("Choose only one traceability links mode: --check or --write.", "traceability links");
5029
5145
  options.check = options.check || !options.write;
5030
- options.root = resolve14(options.root);
5146
+ options.root = resolve15(options.root);
5031
5147
  return options;
5032
5148
  }
5033
5149
  function runTraceability(argv) {
@@ -5038,7 +5154,7 @@ function runTraceability(argv) {
5038
5154
  const errors = [...config.errors ?? []];
5039
5155
  for (const file of files) {
5040
5156
  if (options.write && errors.length > 0) break;
5041
- const content = readFileSync13(file, "utf8");
5157
+ const content = readFileSync14(file, "utf8");
5042
5158
  for (const error of validateTraceabilityLinksRegion(content, options.root, file)) errors.push(error);
5043
5159
  const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
5044
5160
  if (blocks.length === 0) continue;
@@ -5047,7 +5163,7 @@ function runTraceability(argv) {
5047
5163
  for (const error of result.errors ?? []) errors.push(error);
5048
5164
  continue;
5049
5165
  }
5050
- if (options.write && result.changed) writeFileSync5(file, result.content);
5166
+ if (options.write && result.changed) writeFileSync6(file, result.content);
5051
5167
  if (result.changed) results.push({ file: rel(options.root, file), changed: true });
5052
5168
  }
5053
5169
  const ok = errors.length === 0 && (!options.check || results.length === 0);
@@ -5253,6 +5369,7 @@ async function runCli(argv = process.argv.slice(2)) {
5253
5369
  else if (command === "config") runConfig(args);
5254
5370
  else if (command === "status") runStatus(args);
5255
5371
  else if (command === "query") await runQuery(args);
5372
+ else if (command === "embedding") runEmbedding(args);
5256
5373
  else if (command === "map") runMap(args);
5257
5374
  else if (command === "resolve") runResolve(args);
5258
5375
  else if (command === "expand") runExpand(args);
package/mcp/server.mjs CHANGED
@@ -23179,10 +23179,37 @@ async function cliJson(args, cwd) {
23179
23179
  }
23180
23180
  async function projectLoad(input) {
23181
23181
  const root2 = resolve5(input.root || process.cwd());
23182
- const tree = markdownTree(root2, input.maxDepth ?? (input.focus ? 3 : 5));
23182
+ const focus = input.focus?.trim() ?? "";
23183
+ const tree = markdownTree(root2, input.maxDepth ?? (focus ? 3 : 5));
23183
23184
  let query = null;
23184
- if (input.focus?.trim()) query = await cliJson(["query", root2, input.focus.trim(), "--limit", String(Math.min(input.limit ?? 30, 30)), "--json"], root2);
23185
- return { ok: true, root: root2, focus: input.focus?.trim() ?? "", documentationTree: tree, query };
23185
+ let queryUnavailable;
23186
+ if (focus) {
23187
+ try {
23188
+ query = await cliJson(["query", root2, focus, "--limit", String(Math.min(input.limit ?? 30, 30)), "--json"], root2);
23189
+ } catch (error2) {
23190
+ const missingRuntime = String(error2?.message ?? error2).includes("Optional local embedding runtime is not installed");
23191
+ queryUnavailable = missingRuntime ? {
23192
+ code: "EMBEDDING_RUNTIME_MISSING",
23193
+ message: "Local semantic search requires an optional embedding runtime; continue with the documentation map or ask the user before installation.",
23194
+ recovery: { kind: "embedding-runtime-install", requiresConfirmation: true, statusTool: "dotdotgod_embedding_status", installTool: "dotdotgod_embedding_install", cliCommand: "dotdotgod embedding install --confirm" }
23195
+ } : {
23196
+ code: "QUERY_UNAVAILABLE",
23197
+ message: "Semantic project query is unavailable; continue with the documentation map and targeted reads."
23198
+ };
23199
+ }
23200
+ }
23201
+ return { ok: true, root: root2, focus, documentationTree: tree, query, ...queryUnavailable ? { queryUnavailable } : {} };
23202
+ }
23203
+ async function projectEmbeddingStatus(input = {}) {
23204
+ const root2 = resolve5(input.root || process.cwd());
23205
+ const result = await cliJson(["embedding", "status", root2, "--json"], root2);
23206
+ return { ...result, ...result.location ? { location: "~/.dotdotgod/runtime/embedding" } : {} };
23207
+ }
23208
+ async function projectEmbeddingInstall(input = {}) {
23209
+ if (input.confirm !== true) throw new Error("confirm: true is required after explicit user approval.");
23210
+ const root2 = resolve5(input.root || process.cwd());
23211
+ const result = await cliJson(["embedding", "install", root2, "--confirm", "--json"], root2);
23212
+ return { ...result, ...result.location ? { location: "~/.dotdotgod/runtime/embedding" } : {} };
23186
23213
  }
23187
23214
  async function projectImpact(input) {
23188
23215
  const root2 = resolve5(input.root || process.cwd());
@@ -23216,10 +23243,13 @@ var IngestionJobRunner = class {
23216
23243
  this.renderer = renderer;
23217
23244
  this.running = false;
23218
23245
  this.scheduled = false;
23246
+ this.closed = false;
23247
+ this.pumpPromise = void 0;
23219
23248
  this.controllers = /* @__PURE__ */ new Map();
23220
23249
  this.schedule();
23221
23250
  }
23222
23251
  enqueue(kind, input) {
23252
+ if (this.closed) throw new Error("Ingestion job runner is closed.");
23223
23253
  if (!["index", "fetch"].includes(kind)) throw new Error("Unsupported ingestion job kind.");
23224
23254
  const job = this.store.createJob({ kind, input, sessionId: this.sessionId ?? null });
23225
23255
  this.schedule();
@@ -23233,18 +23263,31 @@ var IngestionJobRunner = class {
23233
23263
  return this.store.cancelJob(id);
23234
23264
  }
23235
23265
  schedule() {
23236
- if (this.scheduled || this.running) return;
23266
+ if (this.closed || this.scheduled || this.running) return;
23237
23267
  this.scheduled = true;
23238
23268
  queueMicrotask(() => {
23239
23269
  this.scheduled = false;
23240
- void this.pump();
23270
+ if (this.closed) return;
23271
+ const pending = this.pump();
23272
+ this.pumpPromise = pending;
23273
+ const clearPending = () => {
23274
+ if (this.pumpPromise === pending) this.pumpPromise = void 0;
23275
+ };
23276
+ void pending.then(clearPending, clearPending);
23241
23277
  });
23242
23278
  }
23279
+ async close() {
23280
+ this.closed = true;
23281
+ for (const controller of this.controllers.values()) controller.abort();
23282
+ if (this.scheduled) await Promise.resolve();
23283
+ await this.pumpPromise;
23284
+ }
23243
23285
  async pump() {
23244
- if (this.running) return;
23286
+ if (this.closed || this.running) return;
23245
23287
  this.running = true;
23246
23288
  try {
23247
23289
  for (; ; ) {
23290
+ if (this.closed) break;
23248
23291
  const job = this.store.claimNextJob();
23249
23292
  if (!job) break;
23250
23293
  const controller = new AbortController();
@@ -23259,7 +23302,7 @@ var IngestionJobRunner = class {
23259
23302
  } finally {
23260
23303
  this.controllers.delete(job.id);
23261
23304
  }
23262
- await new Promise((resolve6) => setImmediate(resolve6));
23305
+ if (!this.closed) await new Promise((resolve6) => setImmediate(resolve6));
23263
23306
  }
23264
23307
  } finally {
23265
23308
  this.running = false;
@@ -23427,10 +23470,11 @@ register("session_resume", "Use an explicit opaque session ID for subsequent con
23427
23470
  register("ingestion_job_start", "Queue one durable bounded background index or strict-fetch ingestion job.", phase3Shape("ingestion_job_start"), (input) => ({ ok: true, job: getJobs().enqueue(input.kind, input.input) }));
23428
23471
  register("ingestion_job_status", "Return bounded status for one background ingestion job.", phase3Shape("ingestion_job_status"), (input) => ({ ok: true, job: getJobs().status(input.id) }), { readOnlyHint: true });
23429
23472
  register("ingestion_job_cancel", "Cancel one queued or running background ingestion job.", phase3Shape("ingestion_job_cancel"), (input) => ({ ok: true, ...getJobs().cancel(input.id) }), { destructiveHint: true });
23430
- register("context_heal", "Explicitly back up and migrate only a recognized recoverable context database.", phase3Shape("context_heal"), () => {
23473
+ register("context_heal", "Explicitly back up and migrate only a recognized recoverable context database.", phase3Shape("context_heal"), async () => {
23474
+ await jobRunner?.close();
23475
+ jobRunner = void 0;
23431
23476
  contextStore?.close();
23432
23477
  contextStore = void 0;
23433
- jobRunner = void 0;
23434
23478
  return healContextDatabase(root);
23435
23479
  }, { destructiveHint: true });
23436
23480
  register("stats", "Report local context store counts and location.", {}, () => ({ ok: true, sessionId, ...getStore().stats() }), { readOnlyHint: true });
@@ -23447,6 +23491,8 @@ register("dotdotgod_project_load", "Load a bounded documentation map and optiona
23447
23491
  limit: external_exports.number().int().min(1).max(30).optional(),
23448
23492
  maxDepth: external_exports.number().int().min(1).max(5).optional()
23449
23493
  }, (input) => projectLoad(projectInput(input)), { readOnlyHint: true });
23494
+ register("dotdotgod_embedding_status", "Inspect the optional persistent embedding runtime without installing or contacting a registry.", { root: external_exports.string().optional() }, (input) => projectEmbeddingStatus(projectInput(input)), { readOnlyHint: true });
23495
+ register("dotdotgod_embedding_install", "After explicit user approval, install the fixed optional embedding runtime persistently. Uses network and dependency install scripts.", { root: external_exports.string().optional(), confirm: external_exports.literal(true) }, (input) => projectEmbeddingInstall(projectInput(input)), { destructiveHint: true, openWorldHint: true });
23450
23496
  register("dotdotgod_project_impact", "Run bounded graph impact for up to 20 changed paths.", {
23451
23497
  root: external_exports.string().optional(),
23452
23498
  paths: external_exports.array(external_exports.string()).min(1).max(20)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/claude-code",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Claude Code adapter for dotdotgod project memory workflows.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
@@ -23,12 +23,9 @@
23
23
  "engines": {
24
24
  "node": ">=22.5.0"
25
25
  },
26
- "dependencies": {
27
- "@huggingface/transformers": "^4.2.0"
28
- },
29
26
  "devDependencies": {
30
27
  "esbuild": "0.27.3",
31
- "@dotdotgod/context": "0.3.2"
28
+ "@dotdotgod/context": "0.3.4"
32
29
  },
33
30
  "files": [
34
31
  ".claude-plugin",