grepmax 0.23.0 → 0.25.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.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +25 -6
  3. package/dist/commands/add.js +38 -31
  4. package/dist/commands/config.js +16 -15
  5. package/dist/commands/context.js +38 -13
  6. package/dist/commands/doctor.js +76 -83
  7. package/dist/commands/extract.js +12 -1
  8. package/dist/commands/impact.js +33 -1
  9. package/dist/commands/index.js +23 -24
  10. package/dist/commands/list.js +23 -14
  11. package/dist/commands/mcp.js +494 -162
  12. package/dist/commands/peek.js +16 -5
  13. package/dist/commands/repair.js +54 -120
  14. package/dist/commands/search-output.js +30 -9
  15. package/dist/commands/search-run.js +75 -47
  16. package/dist/commands/search-skeletons.js +28 -18
  17. package/dist/commands/search.js +45 -49
  18. package/dist/commands/serve.js +415 -373
  19. package/dist/commands/setup.js +2 -2
  20. package/dist/commands/similar.js +5 -5
  21. package/dist/commands/skeleton.js +67 -41
  22. package/dist/commands/status.js +5 -2
  23. package/dist/commands/summarize.js +6 -0
  24. package/dist/commands/surprises.js +150 -0
  25. package/dist/commands/watch.js +102 -38
  26. package/dist/config.js +3 -0
  27. package/dist/eval-surprising-connections.js +191 -0
  28. package/dist/index.js +2 -0
  29. package/dist/lib/analysis/surprising-connections.js +600 -0
  30. package/dist/lib/daemon/daemon.js +1101 -379
  31. package/dist/lib/daemon/ipc-handler.js +122 -13
  32. package/dist/lib/daemon/mlx-server-manager.js +268 -125
  33. package/dist/lib/daemon/process-manager.js +7 -4
  34. package/dist/lib/daemon/search-handler.js +23 -9
  35. package/dist/lib/daemon/watcher-manager.js +353 -110
  36. package/dist/lib/graph/impact-rollup.js +202 -0
  37. package/dist/lib/graph/impact.js +15 -1
  38. package/dist/lib/help/agent-cheatsheet.js +1 -0
  39. package/dist/lib/index/batch-processor.js +231 -67
  40. package/dist/lib/index/embedding-generation.js +109 -0
  41. package/dist/lib/index/embedding-status.js +23 -0
  42. package/dist/lib/index/file-policy.js +273 -0
  43. package/dist/lib/index/ignore-patterns.js +4 -0
  44. package/dist/lib/index/index-config.js +18 -4
  45. package/dist/lib/index/syncer.js +256 -79
  46. package/dist/lib/index/walker.js +66 -86
  47. package/dist/lib/index/watcher-batch.js +9 -0
  48. package/dist/lib/index/watcher.js +129 -3
  49. package/dist/lib/llm/server.js +6 -0
  50. package/dist/lib/search/searcher.js +30 -19
  51. package/dist/lib/skeleton/skeletonizer.js +118 -0
  52. package/dist/lib/skeleton/summary-formatter.js +8 -1
  53. package/dist/lib/store/store-lease.js +473 -0
  54. package/dist/lib/store/vector-db.js +302 -57
  55. package/dist/lib/utils/blocked-roots.js +63 -0
  56. package/dist/lib/utils/cross-project.js +7 -3
  57. package/dist/lib/utils/daemon-client.js +24 -1
  58. package/dist/lib/utils/daemon-launcher.js +38 -13
  59. package/dist/lib/utils/doctor-status.js +76 -0
  60. package/dist/lib/utils/file-utils.js +74 -4
  61. package/dist/lib/utils/keyed-mutex.js +101 -0
  62. package/dist/lib/utils/logger.js +57 -1
  63. package/dist/lib/utils/mlx-hf-cache.js +114 -0
  64. package/dist/lib/utils/operation-coordinator.js +146 -0
  65. package/dist/lib/utils/path-containment.js +106 -0
  66. package/dist/lib/utils/process.js +44 -0
  67. package/dist/lib/utils/project-registry.js +351 -3
  68. package/dist/lib/utils/scope-filter.js +3 -9
  69. package/dist/lib/utils/stale-hint.js +12 -19
  70. package/dist/lib/utils/watcher-launcher.js +5 -1
  71. package/dist/lib/utils/watcher-store.js +2 -2
  72. package/dist/lib/workers/colbert-math.js +15 -12
  73. package/dist/lib/workers/embeddings/colbert.js +3 -2
  74. package/dist/lib/workers/embeddings/granite.js +4 -3
  75. package/dist/lib/workers/embeddings/mlx-client.js +59 -16
  76. package/dist/lib/workers/orchestrator.js +39 -8
  77. package/dist/lib/workers/pool.js +150 -83
  78. package/dist/lib/workers/process-child.js +11 -2
  79. package/dist/lib/workers/serialized-handler.js +10 -0
  80. package/dist/lib/workers/worker.js +13 -4
  81. package/mlx-embed-server/server.py +21 -3
  82. package/mlx-embed-server/summarizer.py +8 -0
  83. package/package.json +6 -3
  84. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  85. package/plugins/grepmax/hooks/start.js +3 -170
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.23.0",
13
+ "version": "0.25.0",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -73,6 +73,7 @@ gmax test handleAuth # Find tests via reverse call graph
73
73
  gmax impact handleAuth # Dependents + affected tests
74
74
  gmax similar handleAuth # Find similar code patterns
75
75
  gmax audit --top 10 # God nodes, hub files, file cycles, dead candidates
76
+ gmax surprises --experimental --agent # Experimental: similar but graph-disconnected file pairs
76
77
  gmax dead handleAuth # Unused-symbol check via call graph (DEAD / PUBLIC EXPORT / LIVE)
77
78
  gmax context "auth system" --budget 4000 # Token-budgeted topic summary
78
79
  gmax context src/lib/auth.ts --budget 4000 # Deterministic file/path context
@@ -134,6 +135,7 @@ Plugins auto-update when you run `npm install -g grepmax@latest` — no need to
134
135
  | `peek_symbol` | Compact overview: signature + callers + callees. |
135
136
  | `dead` | Unused-symbol check via call graph. Returns `DEAD`, `PUBLIC EXPORT`, or `LIVE` with caller count. |
136
137
  | `audit` | Graph summary: god nodes, hub files, symbol-derived file dependency cycles, and dead-code candidates. |
138
+ | `surprising_connections` | Experimental orientation signal: embedding-similar file pairs with no direct indexed static file edge. Requires `experimental:true`. |
137
139
  | `get_neighbors` | Graph primitive: symbols reachable from a node along call edges within N hops. |
138
140
  | `find_paths` | Graph primitive: shortest call-graph path between two symbols. |
139
141
  | `subgraph_for_files` | Graph primitive: local dependency subgraph for a set of files. |
@@ -177,6 +179,18 @@ gmax "query" [options]
177
179
  | `--seed-symbol <name>` | Bias results toward an identifier you're working with (repeatable). | — |
178
180
  | `--skeleton` | Show file skeletons for top matches. | `false` |
179
181
  | `--context-for-llm` | Full function bodies + imports per result. | `false` |
182
+
183
+ ## Experimental Orientation
184
+
185
+ `gmax surprises --experimental` finds file pairs that are semantically similar but not already connected by the indexed static graph. Use it for architecture orientation, duplicate-logic sweeps, and cross-package drift checks, not as proof that two files are unrelated.
186
+
187
+ ```bash
188
+ gmax surprises --experimental --agent
189
+ gmax surprises --experimental --in packages/app/src --dir-depth 4 --agent
190
+ gmax surprises --experimental --exclude generated --top 10
191
+ ```
192
+
193
+ The CLI and MCP output include score, max similarity, pair count, representative symbols, directory buckets, top similarities, applied penalties, and `gmax skeleton` follow-up hints. On large monorepos, prefer `--in`/`--exclude`; the default scan is capped at 50,000 rows and user-provided `--max-rows` is capped at 100,000. If a narrow `--in` scope returns no findings, increase `--dir-depth` so subdirectories are compared inside the scope.
180
194
  | `--budget <tokens>` | Cap output tokens (for `--context-for-llm`). | `8000` |
181
195
  | `--explain` | Show scoring breakdown per result. | `false` |
182
196
  | `--scores` | Show relevance scores. | `false` |
@@ -318,16 +332,21 @@ worker RAM, and dense-vector storage while *lowering* recall on code search.
318
332
  Unless you have a measured reason to switch (e.g. a recall complaint on a very
319
333
  large repo — benchmark it first), stay on 384d.
320
334
 
321
- To change tiers:
335
+ The model tier is part of the persisted embedding generation. You can change the desired
336
+ configuration, but existing indexes remain on their built generation until you explicitly run the
337
+ guarded whole-corpus rebuild:
322
338
 
323
339
  ```bash
324
- gmax config --model-tier standard # a dim change needs a full re-embed
325
- gmax repair --rebuild # drops the shared table, re-embeds every project at the new dim
340
+ gmax config --model-tier standard # changes desired configuration only
341
+ gmax config # shows configured vs built identity
342
+ gmax status # reports current / legacy / stale / unbuilt per project
343
+ gmax repair --rebuild # guarded whole-corpus rebuild through the daemon
326
344
  ```
327
345
 
328
- A vector-dimension change can't be applied by a per-project `gmax index --reset`
329
- the shared `chunks` table is fixed-width, so `gmax repair --rebuild` is the
330
- sanctioned path.
346
+ A model change cannot be applied by a per-project `gmax index --reset`, even when vector widths
347
+ match: one query embedding cannot safely search rows from another embedding space. Stale-generation
348
+ search and sync fail before mutation, preserving the existing index. Run `gmax repair --rebuild` to
349
+ replace the whole corpus, or restore the prior model tier as the non-destructive alternative.
331
350
 
332
351
  ### Ignoring Files
333
352
 
@@ -54,30 +54,12 @@ const sync_helpers_1 = require("../lib/index/sync-helpers");
54
54
  const syncer_1 = require("../lib/index/syncer");
55
55
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
56
56
  const vector_db_1 = require("../lib/store/vector-db");
57
+ const blocked_roots_1 = require("../lib/utils/blocked-roots");
57
58
  const exit_1 = require("../lib/utils/exit");
58
59
  const project_marker_1 = require("../lib/utils/project-marker");
59
60
  const project_registry_1 = require("../lib/utils/project-registry");
60
61
  const project_root_1 = require("../lib/utils/project-root");
61
62
  const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
62
- function getBlockedRoots() {
63
- const home = os.homedir();
64
- return new Set([
65
- home,
66
- path.dirname(home),
67
- "/",
68
- "/tmp",
69
- "/private",
70
- "/private/tmp",
71
- "/private/var",
72
- "/var",
73
- "/usr",
74
- "/opt",
75
- "/etc",
76
- "/System",
77
- "/Library",
78
- "/Applications",
79
- ].map((p) => path.resolve(p)));
80
- }
81
63
  function logBlockedAttempt(reason, attempted, extra) {
82
64
  var _a, _b;
83
65
  try {
@@ -101,17 +83,16 @@ Examples:
101
83
  gmax add . --no-index Register only, index later with gmax index
102
84
  `)
103
85
  .action((dir, opts) => __awaiter(void 0, void 0, void 0, function* () {
104
- var _a, _b, _c, _d, _e;
86
+ var _a, _b, _c, _d, _e, _f;
105
87
  let vectorDb = null;
106
88
  try {
107
89
  const targetDir = dir ? path.resolve(dir) : process.cwd();
108
90
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(targetDir)) !== null && _a !== void 0 ? _a : targetDir;
109
91
  const projectName = path.basename(projectRoot);
110
- const blocked = getBlockedRoots();
111
- if (blocked.has(path.resolve(projectRoot))) {
92
+ if ((0, blocked_roots_1.isBlockedProjectRoot)(projectRoot)) {
112
93
  logBlockedAttempt("blocked_root", projectRoot);
113
94
  console.error(`Refusing to add ${projectRoot}: this path is blocked from indexing.\n` +
114
- `(Blocked: home, /, /Users, /tmp, /private, /var, /usr, /opt, /etc, /System, /Library, /Applications.)\n` +
95
+ `(Blocked: ${blocked_roots_1.BLOCKED_ROOTS_DESCRIPTION}.)\n` +
115
96
  `Pick a specific project subdirectory instead.\n` +
116
97
  `Diagnostic logged to ~/.gmax/logs/blocked-add.log (cwd=${process.cwd()} ppid=${process.ppid}).`);
117
98
  process.exitCode = 1;
@@ -221,14 +202,18 @@ Examples:
221
202
  if (!done.ok) {
222
203
  throw new Error((_c = done.error) !== null && _c !== void 0 ? _c : "daemon add failed");
223
204
  }
224
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, pendingEntry), { lastIndexed: new Date().toISOString(), chunkCount: (_d = done.indexed) !== null && _d !== void 0 ? _d : 0, status: "indexed", chunkerVersion: config_1.CONFIG.CHUNKER_VERSION }));
225
- const failedFiles = (_e = done.failedFiles) !== null && _e !== void 0 ? _e : 0;
205
+ if (done.degraded === true) {
206
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, pendingEntry), { status: "pending" }));
207
+ spinner.warn(`Added ${projectName}, but indexing is incomplete (${(_d = done.scanErrors) !== null && _d !== void 0 ? _d : 0} scan errors, ${(_e = done.failedFiles) !== null && _e !== void 0 ? _e : 0} failed)`);
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+ const failedFiles = (_f = done.failedFiles) !== null && _f !== void 0 ? _f : 0;
226
212
  const failedSuffix = failedFiles > 0 ? ` · ${failedFiles} failed` : "";
227
213
  spinner.succeed(`Added ${projectName} (${done.total} files, ${done.indexed} chunks${failedSuffix})`);
228
214
  // Watcher already started by daemon's addProject
229
215
  }
230
216
  catch (e) {
231
- (0, project_registry_1.registerProject)(pendingEntry);
232
217
  spinner.fail(`Failed to index ${projectName}`);
233
218
  throw e;
234
219
  }
@@ -242,12 +227,34 @@ Examples:
242
227
  projectRoot,
243
228
  onProgress,
244
229
  });
245
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, pendingEntry), { lastIndexed: new Date().toISOString(), chunkCount: result.indexed, status: "indexed", chunkerVersion: config_1.CONFIG.CHUNKER_VERSION }));
246
- const failedSuffix = result.failedFiles > 0 ? ` · ${result.failedFiles} failed` : "";
247
- spinner.succeed(`Added ${projectName} (${result.total} files, ${result.indexed} chunks${failedSuffix})`);
230
+ if (result.degraded) {
231
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, pendingEntry), { status: "pending" }));
232
+ spinner.warn(`Added ${projectName}, but indexing is incomplete (${result.scanErrors} scan errors, ${result.failedFiles} failed)`);
233
+ process.exitCode = 1;
234
+ }
235
+ else {
236
+ const prefix = projectRoot.endsWith("/")
237
+ ? projectRoot
238
+ : `${projectRoot}/`;
239
+ const chunkCount = yield vectorDb.countRowsForPath(prefix);
240
+ (0, project_registry_1.stampProjectFullSync)({
241
+ root: projectRoot,
242
+ name: projectName,
243
+ generation: result.generation,
244
+ embedMode: result.embedMode,
245
+ chunkCount,
246
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
247
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
248
+ expectedRebuildId: result.registryExpectation.rebuildId,
249
+ });
250
+ const failedSuffix = result.failedFiles > 0 ? ` · ${result.failedFiles} failed` : "";
251
+ spinner.succeed(`Added ${projectName} (${result.total} files, ${result.indexed} chunks${failedSuffix})`);
252
+ }
248
253
  }
249
254
  catch (e) {
250
- (0, project_registry_1.registerProject)(pendingEntry);
255
+ if (!(e instanceof project_registry_1.ProjectRegistryConflictError)) {
256
+ (0, project_registry_1.registerProject)(pendingEntry);
257
+ }
251
258
  spinner.fail(`Failed to index ${projectName}`);
252
259
  throw e;
253
260
  }
@@ -271,7 +278,7 @@ Examples:
271
278
  try {
272
279
  yield vectorDb.close();
273
280
  }
274
- catch (_f) { }
281
+ catch (_g) { }
275
282
  }
276
283
  yield (0, exit_1.gracefulExit)();
277
284
  }
@@ -12,8 +12,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.config = void 0;
13
13
  const commander_1 = require("commander");
14
14
  const config_1 = require("../config");
15
+ const embedding_status_1 = require("../lib/index/embedding-status");
15
16
  const index_config_1 = require("../lib/index/index-config");
16
17
  const exit_1 = require("../lib/utils/exit");
18
+ const project_registry_1 = require("../lib/utils/project-registry");
17
19
  const project_root_1 = require("../lib/utils/project-root");
18
20
  exports.config = new commander_1.Command("config")
19
21
  .description("View or update gmax configuration")
@@ -28,24 +30,29 @@ Examples:
28
30
  gmax config --query-log on Enable query logging
29
31
  `)
30
32
  .action((_opts, cmd) => __awaiter(void 0, void 0, void 0, function* () {
31
- var _a, _b, _c, _d;
33
+ var _a, _b, _c, _d, _e;
32
34
  const options = cmd.optsWithGlobals();
33
35
  const globalConfig = (0, index_config_1.readGlobalConfig)();
34
36
  const paths = (0, project_root_1.ensureProjectPaths)(process.cwd());
35
- const indexConfig = (0, index_config_1.readIndexConfig)(paths.configPath);
37
+ const project = (0, project_registry_1.getProject)((_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd());
36
38
  const hasUpdates = options.embedMode !== undefined ||
37
39
  options.modelTier !== undefined ||
38
40
  options.queryLog !== undefined;
39
41
  if (!hasUpdates) {
40
42
  // Show current config
41
- const tier = (_a = config_1.MODEL_TIERS[globalConfig.modelTier]) !== null && _a !== void 0 ? _a : config_1.MODEL_TIERS.small;
43
+ const tier = (_b = config_1.MODEL_TIERS[globalConfig.modelTier]) !== null && _b !== void 0 ? _b : config_1.MODEL_TIERS.small;
42
44
  console.log("gmax configuration (~/.gmax/config.json)\n");
43
45
  console.log(` Model tier: ${globalConfig.modelTier} (${tier.vectorDim}d, ${tier.params})`);
44
46
  console.log(` Embed mode: ${globalConfig.embedMode}`);
45
47
  console.log(` Embed model: ${globalConfig.embedMode === "gpu" ? tier.mlxModel : tier.onnxModel}`);
48
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
49
+ console.log(` Configured: ${identity.configured.tier} ${identity.configured.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.configured.fingerprint)}]`);
50
+ if (identity.built) {
51
+ console.log(` Built: ${identity.built.tier} ${identity.built.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.built.fingerprint)}] (${identity.state})`);
52
+ }
46
53
  console.log(` Query log: ${globalConfig.queryLog ? "on" : "off"}`);
47
- if (indexConfig === null || indexConfig === void 0 ? void 0 : indexConfig.indexedAt) {
48
- console.log(` Last indexed: ${indexConfig.indexedAt}`);
54
+ if (project === null || project === void 0 ? void 0 : project.lastIndexed) {
55
+ console.log(` Last indexed: ${project.lastIndexed}`);
49
56
  }
50
57
  console.log(`\nTo change: gmax config --embed-mode <cpu|gpu> --model-tier <small|standard> --query-log <on|off>`);
51
58
  yield (0, exit_1.gracefulExit)();
@@ -80,11 +87,10 @@ Examples:
80
87
  // Reload config after queryLog write
81
88
  Object.assign(globalConfig, (0, index_config_1.readGlobalConfig)());
82
89
  }
83
- const newTier = (_b = options.modelTier) !== null && _b !== void 0 ? _b : globalConfig.modelTier;
84
- const newMode = (_c = options.embedMode) !== null && _c !== void 0 ? _c : globalConfig.embedMode;
85
- const tier = (_d = config_1.MODEL_TIERS[newTier]) !== null && _d !== void 0 ? _d : config_1.MODEL_TIERS.small;
90
+ const newTier = (_c = options.modelTier) !== null && _c !== void 0 ? _c : globalConfig.modelTier;
91
+ const newMode = (_d = options.embedMode) !== null && _d !== void 0 ? _d : globalConfig.embedMode;
92
+ const tier = (_e = config_1.MODEL_TIERS[newTier]) !== null && _e !== void 0 ? _e : config_1.MODEL_TIERS.small;
86
93
  const tierChanged = newTier !== globalConfig.modelTier;
87
- const dimChanged = tier.vectorDim !== globalConfig.vectorDim;
88
94
  (0, index_config_1.writeGlobalConfig)({
89
95
  modelTier: newTier,
90
96
  vectorDim: tier.vectorDim,
@@ -99,12 +105,7 @@ Examples:
99
105
  });
100
106
  console.log(`Updated: embed-mode=${newMode}, model-tier=${newTier} (${tier.vectorDim}d)`);
101
107
  if (tierChanged) {
102
- // A dimension change can't be fixed by a per-project `gmax index --reset`:
103
- // the shared `chunks` table is fixed-width, so it must be dropped and
104
- // rebuilt. A same-dim model swap (future tiers) can use a per-project reset.
105
- console.log(dimChanged
106
- ? `⚠️ Model tier changed (${globalConfig.vectorDim}d → ${tier.vectorDim}d). The shared vector table is fixed-width — run \`${config_1.REBUILD_COMMAND}\` to drop it and re-embed every project at the new dim.`
107
- : "⚠️ Model tier changed — run `gmax index --reset` per project to re-embed with the new model.");
108
+ console.log(`Model tier changed (${globalConfig.vectorDim}d ${tier.vectorDim}d). Existing indexes are preserved and search is blocked until you run 'gmax repair --rebuild'.`);
108
109
  }
109
110
  yield (0, exit_1.gracefulExit)();
110
111
  }));
@@ -53,7 +53,9 @@ const vector_db_1 = require("../lib/store/vector-db");
53
53
  const arrow_1 = require("../lib/utils/arrow");
54
54
  const budget_pack_1 = require("../lib/utils/budget-pack");
55
55
  const exit_1 = require("../lib/utils/exit");
56
+ const file_utils_1 = require("../lib/utils/file-utils");
56
57
  const filter_builder_1 = require("../lib/utils/filter-builder");
58
+ const path_containment_1 = require("../lib/utils/path-containment");
57
59
  const project_registry_1 = require("../lib/utils/project-registry");
58
60
  const project_root_1 = require("../lib/utils/project-root");
59
61
  function estimateTokens(text) {
@@ -106,8 +108,11 @@ function resolveExistingPath(target, root, projectRoot) {
106
108
  path.resolve(projectRoot, target),
107
109
  ];
108
110
  for (const candidate of candidates) {
109
- if (fs.existsSync(candidate))
110
- return candidate;
111
+ if (!fs.existsSync(candidate))
112
+ continue;
113
+ return (0, path_containment_1.resolveContainedPath)(projectRoot, candidate, {
114
+ verifyExistingTarget: true,
115
+ });
111
116
  }
112
117
  return null;
113
118
  }
@@ -137,7 +142,7 @@ function renderPathContext(target, absPath, projectRoot, budget) {
137
142
  }
138
143
  return state;
139
144
  }
140
- const content = fs.readFileSync(absPath, "utf-8");
145
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, absPath);
141
146
  const skeletonizer = new skeleton_1.Skeletonizer();
142
147
  yield skeletonizer.init();
143
148
  if (skeletonizer.isSupported(absPath).supported) {
@@ -196,8 +201,9 @@ exports.context = new commander_1.Command("context")
196
201
  }
197
202
  const searcher = new searcher_1.Searcher(vectorDb);
198
203
  // Phase 1: Semantic search
199
- const response = yield searcher.search(topic, maxResults, { rerank: true }, {}, projectRoot);
200
- if (response.data.length === 0) {
204
+ const response = yield searcher.search(topic, maxResults, { rerank: true }, {}, `${projectRoot}/`);
205
+ const scopedData = response.data.filter((result) => (0, path_containment_1.isPathWithin)(projectRoot, chunkPath(result)));
206
+ if (scopedData.length === 0) {
201
207
  console.log(`No results found for "${topic}".`);
202
208
  return;
203
209
  }
@@ -208,8 +214,8 @@ exports.context = new commander_1.Command("context")
208
214
  sections.push(header);
209
215
  tokensUsed += estimateTokens(header);
210
216
  // Phase 2: Entry points (ORCHESTRATION role results)
211
- const orchestrators = response.data.filter((r) => r.role === "ORCHESTRATION");
212
- const entryPoints = orchestrators.length > 0 ? orchestrators : response.data.slice(0, 3);
217
+ const orchestrators = scopedData.filter((r) => r.role === "ORCHESTRATION");
218
+ const entryPoints = orchestrators.length > 0 ? orchestrators : scopedData.slice(0, 3);
213
219
  const epSection = ["\n## Entry Points"];
214
220
  for (const r of entryPoints.slice(0, 5)) {
215
221
  const p = chunkPath(r);
@@ -230,13 +236,21 @@ exports.context = new commander_1.Command("context")
230
236
  const topChunks = entryPoints.slice(0, 3);
231
237
  const bodyBlobs = topChunks.map((r) => {
232
238
  var _a, _b;
233
- const absP = chunkPath(r);
239
+ let absP;
240
+ try {
241
+ absP = (0, path_containment_1.resolveContainedPath)(projectRoot, chunkPath(r), {
242
+ verifyExistingTarget: true,
243
+ });
244
+ }
245
+ catch (_c) {
246
+ return null;
247
+ }
234
248
  const startLine = chunkStartLine(r);
235
249
  const endLine = chunkEndLine(r);
236
250
  const sym = (_b = (_a = (0, arrow_1.toArr)(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
237
251
  const parentSym = String(r.parent_symbol || "");
238
252
  try {
239
- const content = fs.readFileSync(absP, "utf-8");
253
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, absP);
240
254
  const allLines = content.split("\n");
241
255
  const body = allLines
242
256
  .slice(startLine, Math.min(endLine + 1, allLines.length))
@@ -255,7 +269,7 @@ exports.context = new commander_1.Command("context")
255
269
  }
256
270
  return `\n--- ${relPath(projectRoot, absP)}:${startLine + 1} ${label} ---\n${enclosing}${body}`;
257
271
  }
258
- catch (_c) {
272
+ catch (_d) {
259
273
  return null; // File not readable — drop
260
274
  }
261
275
  });
@@ -279,7 +293,18 @@ exports.context = new commander_1.Command("context")
279
293
  }
280
294
  // Phase 4: File skeletons for unique files
281
295
  const uniqueFiles = [
282
- ...new Set(response.data.map((r) => chunkPath(r)).filter(Boolean)),
296
+ ...new Set(scopedData
297
+ .map((r) => {
298
+ try {
299
+ return (0, path_containment_1.resolveContainedPath)(projectRoot, chunkPath(r), {
300
+ verifyExistingTarget: true,
301
+ });
302
+ }
303
+ catch (_a) {
304
+ return "";
305
+ }
306
+ })
307
+ .filter(Boolean)),
283
308
  ].slice(0, 5);
284
309
  const skelSection = ["\n## File Structure"];
285
310
  const skeletonizer = new skeleton_1.Skeletonizer();
@@ -288,7 +313,7 @@ exports.context = new commander_1.Command("context")
288
313
  if (!skeletonizer.isSupported(absP).supported)
289
314
  continue;
290
315
  try {
291
- const content = fs.readFileSync(absP, "utf-8");
316
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, absP);
292
317
  const result = yield skeletonizer.skeletonizeFile(absP, content);
293
318
  if (!result.success)
294
319
  continue;
@@ -311,7 +336,7 @@ exports.context = new commander_1.Command("context")
311
336
  // Phase 5: Related files summary
312
337
  const table = yield vectorDb.ensureTable();
313
338
  const allSymbols = new Set();
314
- for (const r of response.data) {
339
+ for (const r of scopedData) {
315
340
  for (const s of (0, arrow_1.toArr)(r.defined_symbols))
316
341
  allSymbols.add(s);
317
342
  }