grepmax 0.21.3 → 0.22.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.
@@ -10,14 +10,12 @@
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.21.3",
13
+ "version": "0.22.0",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
17
17
  },
18
- "skills": [
19
- "./skills/grepmax"
20
- ]
18
+ "skills": ["./skills/grepmax"]
21
19
  }
22
20
  ]
23
21
  }
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.dead = void 0;
46
46
  const commander_1 = require("commander");
47
+ const languages_1 = require("../lib/core/languages");
47
48
  const graph_builder_1 = require("../lib/graph/graph-builder");
48
49
  const vector_db_1 = require("../lib/store/vector-db");
49
50
  const agent_errors_1 = require("../lib/utils/agent-errors");
@@ -147,7 +148,7 @@ exports.dead = new commander_1.Command("dead")
147
148
  const defLine = Number(defRow.start_line || 0);
148
149
  const isExported = Boolean(defRow.is_exported);
149
150
  const builder = new graph_builder_1.GraphBuilder(vectorDb, scope.pathPrefix, scope.excludePrefixes);
150
- const callers = yield builder.getCallers(symbol);
151
+ const callers = yield builder.getCallers(symbol, (0, languages_1.languageFamilyForPath)(defPath));
151
152
  const status = callers.length === 0 ? (isExported ? "PUBLIC_EXPORT" : "DEAD") : "LIVE";
152
153
  const topCallers = callers
153
154
  .slice(0, TOP_CALLERS)
@@ -77,7 +77,7 @@ exports.impact = new commander_1.Command("impact")
77
77
  (0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
78
78
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
79
79
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
80
- const { symbols, resolvedAsFile } = yield (0, impact_1.resolveTargetSymbols)(target, vectorDb, projectRoot);
80
+ const { symbols, resolvedAsFile, symbolFamilies } = yield (0, impact_1.resolveTargetSymbols)(target, vectorDb, projectRoot);
81
81
  if (symbols.length === 0) {
82
82
  console.log(resolvedAsFile
83
83
  ? `No symbols found in file: ${target}`
@@ -109,9 +109,9 @@ exports.impact = new commander_1.Command("impact")
109
109
  // traversal entirely so the affected-tests section is omitted (not just
110
110
  // empty) below.
111
111
  const [dependents, tests] = yield Promise.all([
112
- (0, impact_1.findDependents)(symbols, vectorDb, queryRoot, excludePaths, undefined, scope.excludePrefixes),
112
+ (0, impact_1.findDependents)(symbols, vectorDb, queryRoot, excludePaths, undefined, scope.excludePrefixes, symbolFamilies),
113
113
  includeTests
114
- ? (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes)
114
+ ? (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes, symbolFamilies)
115
115
  : Promise.resolve([]),
116
116
  ]);
117
117
  // Separate test files from non-test dependents
@@ -65,6 +65,7 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
65
65
  const commander_1 = require("commander");
66
66
  const zod_1 = require("zod");
67
67
  const config_1 = require("../config");
68
+ const languages_1 = require("../lib/core/languages");
68
69
  const graph_builder_1 = require("../lib/graph/graph-builder");
69
70
  const index_config_1 = require("../lib/index/index-config");
70
71
  const syncer_1 = require("../lib/index/syncer");
@@ -906,6 +907,7 @@ exports.mcp = new commander_1.Command("mcp")
906
907
  symbol: t.node.symbol,
907
908
  file: t.node.file,
908
909
  line: t.node.line,
910
+ edgeKind: t.node.edgeKind,
909
911
  });
910
912
  walkCallers(t.callers);
911
913
  }
@@ -933,7 +935,12 @@ exports.mcp = new commander_1.Command("mcp")
933
935
  const loc = c.file
934
936
  ? `${rel(c.file)}:${c.line + 1}`
935
937
  : "(not indexed)";
936
- parts.push(` <- ${c.symbol} ${loc}`);
938
+ // `(member)`/`(type)` flag inferred edges (receiver-unverified call
939
+ // or a type-position ref) so they don't read as confirmed callers.
940
+ const tag = c.edgeKind === "member" || c.edgeKind === "type"
941
+ ? ` (${c.edgeKind})`
942
+ : "";
943
+ parts.push(` <- ${c.symbol} ${loc}${tag}`);
937
944
  }
938
945
  if (callerList.length > maxCallers) {
939
946
  parts.push(` ... and ${callerList.length - maxCallers} more`);
@@ -993,7 +1000,7 @@ exports.mcp = new commander_1.Command("mcp")
993
1000
  const defLine = Number(defRow.start_line || 0);
994
1001
  const isExported = Boolean(defRow.is_exported);
995
1002
  const builder = new graph_builder_1.GraphBuilder(db, root);
996
- const callers = yield builder.getCallers(symbol);
1003
+ const callers = yield builder.getCallers(symbol, (0, languages_1.languageFamilyForPath)(defPath));
997
1004
  const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
998
1005
  const defLoc = `${rel(defPath)}:${defLine + 1}`;
999
1006
  if (callers.length === 0) {
@@ -1714,10 +1721,10 @@ exports.mcp = new commander_1.Command("mcp")
1714
1721
  try {
1715
1722
  const { resolveTargetSymbols, findTests } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
1716
1723
  const db = getVectorDb();
1717
- const { symbols } = yield resolveTargetSymbols(target, db, projectRoot);
1724
+ const { symbols, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
1718
1725
  if (symbols.length === 0)
1719
1726
  return ok(`No symbols found for: ${target}`);
1720
- const tests = yield findTests(symbols, db, projectRoot, depth);
1727
+ const tests = yield findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies);
1721
1728
  if (tests.length === 0)
1722
1729
  return ok(`No tests found for ${target}.`);
1723
1730
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
@@ -1742,7 +1749,7 @@ exports.mcp = new commander_1.Command("mcp")
1742
1749
  try {
1743
1750
  const { resolveTargetSymbols, findTests, findDependents, isTestPath } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
1744
1751
  const db = getVectorDb();
1745
- const { symbols, resolvedAsFile } = yield resolveTargetSymbols(target, db, projectRoot);
1752
+ const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
1746
1753
  if (symbols.length === 0)
1747
1754
  return ok(`No symbols found for: ${target}`);
1748
1755
  const targetPath = resolvedAsFile
@@ -1750,8 +1757,8 @@ exports.mcp = new commander_1.Command("mcp")
1750
1757
  : undefined;
1751
1758
  const excludePaths = targetPath ? new Set([targetPath]) : undefined;
1752
1759
  const [dependents, tests] = yield Promise.all([
1753
- findDependents(symbols, db, projectRoot, excludePaths),
1754
- findTests(symbols, db, projectRoot, depth),
1760
+ findDependents(symbols, db, projectRoot, excludePaths, undefined, undefined, symbolFamilies),
1761
+ findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies),
1755
1762
  ]);
1756
1763
  const nonTestDeps = dependents.filter((d) => !isTestPath(d.file));
1757
1764
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
@@ -212,7 +212,6 @@ exports.peek = new commander_1.Command("peek")
212
212
  const endLine = metaRows.length > 0
213
213
  ? Number(metaRows[0].end_line || 0)
214
214
  : center.line;
215
- // Get multi-hop callers if depth > 1
216
215
  let callerList;
217
216
  if (depth > 1) {
218
217
  const multiHop = yield graphBuilder.buildGraphMultiHop(symbol, depth);
@@ -224,6 +223,7 @@ exports.peek = new commander_1.Command("peek")
224
223
  symbol: t.node.symbol,
225
224
  file: t.node.file,
226
225
  line: t.node.line,
226
+ edgeKind: t.node.edgeKind,
227
227
  });
228
228
  walkCallers(t.callers);
229
229
  }
@@ -236,8 +236,16 @@ exports.peek = new commander_1.Command("peek")
236
236
  symbol: c.symbol,
237
237
  file: c.file,
238
238
  line: c.line,
239
+ edgeKind: c.edgeKind,
239
240
  }));
240
241
  }
242
+ // Confidence per caller symbol: getCallers sorts free calls first, so the
243
+ // first edgeKind seen for a symbol is its highest-confidence one.
244
+ const edgeKindBySym = new Map();
245
+ for (const c of callerList) {
246
+ if (!edgeKindBySym.has(c.symbol))
247
+ edgeKindBySym.set(c.symbol, c.edgeKind);
248
+ }
241
249
  // Re-anchor chunk-level caller rows to actual call sites and dedupe —
242
250
  // getCallers() returns one row per chunk, which multiplies callers for
243
251
  // classes split across many chunks (verified: 3 real call sites → 66).
@@ -247,6 +255,7 @@ exports.peek = new commander_1.Command("peek")
247
255
  symbol: c.symbol,
248
256
  file: c.file,
249
257
  line: (_a = c.snippetLine) !== null && _a !== void 0 ? _a : c.line,
258
+ edgeKind: edgeKindBySym.get(c.symbol),
250
259
  });
251
260
  });
252
261
  // Builtins listed as "(not indexed)" callees (trunc, now, filter, …)
@@ -266,6 +275,10 @@ exports.peek = new commander_1.Command("peek")
266
275
  file: c.file,
267
276
  line: c.line,
268
277
  }));
278
+ // Inferred-caller marker: `member` = receiver-unverified `x.T()`, `type` =
279
+ // a type-position reference (not a call). Free calls render clean — the
280
+ // trustworthy default — so only guesses carry a tag.
281
+ const kindTag = (k) => k === "member" || k === "type" ? ` (${k})` : "";
269
282
  if (opts.agent) {
270
283
  // Compact TSV output
271
284
  const exportedStr = exported ? "exported" : "";
@@ -290,7 +303,7 @@ exports.peek = new commander_1.Command("peek")
290
303
  console.log(`sig: ${sigOnly}`);
291
304
  // Callers
292
305
  for (const c of resolvedCallers.slice(0, MAX_CALLERS)) {
293
- console.log(`<- ${c.symbol}\t${c.file ? `${rel(c.file)}:${c.line + 1}` : "(not indexed)"}`);
306
+ console.log(`<- ${c.symbol}\t${c.file ? `${rel(c.file)}:${c.line + 1}` : "(not indexed)"}${kindTag(c.edgeKind)}`);
294
307
  }
295
308
  if (resolvedCallers.length > MAX_CALLERS) {
296
309
  console.log(`<- ... ${resolvedCallers.length - MAX_CALLERS} more`);
@@ -338,10 +351,10 @@ exports.peek = new commander_1.Command("peek")
338
351
  console.log(style.bold(`callers (${resolvedCallers.length}):`));
339
352
  for (const c of shown) {
340
353
  if (c.file) {
341
- console.log(` ${style.blue("\u2190")} ${style.green(c.symbol.padEnd(25))} ${style.dim(`${rel(c.file)}:${c.line + 1}`)}`);
354
+ console.log(` ${style.blue("\u2190")} ${style.green(c.symbol.padEnd(25))} ${style.dim(`${rel(c.file)}:${c.line + 1}`)}${style.dim(kindTag(c.edgeKind))}`);
342
355
  }
343
356
  else {
344
- console.log(` ${style.blue("\u2190")} ${c.symbol.padEnd(25)} ${style.dim("(not indexed)")}`);
357
+ console.log(` ${style.blue("\u2190")} ${c.symbol.padEnd(25)} ${style.dim("(not indexed)")}${style.dim(kindTag(c.edgeKind))}`);
345
358
  }
346
359
  }
347
360
  if (resolvedCallers.length > MAX_CALLERS) {
@@ -174,33 +174,40 @@ function installAll() {
174
174
  });
175
175
  }
176
176
  // --- Subcommands ---
177
- const addCmd = new commander_1.Command("add")
178
- .description("Install or update gmax plugins")
179
- .argument("[client]", "Client to install (claude, opencode, codex, droid, all)")
180
- .action((clientArg) => __awaiter(void 0, void 0, void 0, function* () {
181
- const clients = getClients();
182
- const onlyId = clientArg && clientArg !== "all" ? clientArg : undefined;
183
- if (onlyId) {
184
- const client = clients.find((c) => c.id === onlyId);
185
- if (!client) {
186
- console.error(`Unknown client: ${onlyId}`);
187
- console.error(`Available: ${clients.map((c) => c.id).join(", ")}`);
188
- yield (0, exit_1.gracefulExit)(1);
189
- return;
190
- }
191
- if (!client.detect()) {
192
- console.error(`${client.name} not found on this system`);
193
- yield (0, exit_1.gracefulExit)(1);
177
+ function installAction(clientArg) {
178
+ return __awaiter(this, void 0, void 0, function* () {
179
+ const clients = getClients();
180
+ const onlyId = clientArg && clientArg !== "all" ? clientArg : undefined;
181
+ if (onlyId) {
182
+ const client = clients.find((c) => c.id === onlyId);
183
+ if (!client) {
184
+ console.error(`Unknown client: ${onlyId}`);
185
+ console.error(`Available: ${clients.map((c) => c.id).join(", ")}`);
186
+ yield (0, exit_1.gracefulExit)(1);
187
+ return;
188
+ }
189
+ if (!client.detect()) {
190
+ console.error(`${client.name} not found on this system`);
191
+ yield (0, exit_1.gracefulExit)(1);
192
+ return;
193
+ }
194
+ yield client.install();
195
+ yield (0, exit_1.gracefulExit)();
194
196
  return;
195
197
  }
196
- yield client.install();
198
+ // Install all detected
199
+ yield installAll();
197
200
  yield (0, exit_1.gracefulExit)();
198
- return;
199
- }
200
- // Install all detected
201
- yield installAll();
202
- yield (0, exit_1.gracefulExit)();
203
- }));
201
+ });
202
+ }
203
+ const addCmd = new commander_1.Command("add")
204
+ .description("Install or update gmax plugins")
205
+ .argument("[client]", "Client to install (claude, opencode, codex, droid, all)")
206
+ .action(installAction);
207
+ const updateCmd = new commander_1.Command("update")
208
+ .description("Update gmax plugins")
209
+ .argument("[client]", "Client to update (claude, opencode, codex, droid, all)")
210
+ .action(installAction);
204
211
  const removeCmd = new commander_1.Command("remove")
205
212
  .description("Remove gmax plugins")
206
213
  .argument("[client]", "Client to remove (claude, opencode, codex, droid, all)")
@@ -261,6 +268,8 @@ function statusAction() {
261
268
  console.log("\nCommands:");
262
269
  console.log(" gmax plugin add Install all detected clients");
263
270
  console.log(" gmax plugin add <client> Install a specific client");
271
+ console.log(" gmax plugin update Update all detected clients");
272
+ console.log(" gmax plugin update <client> Update a specific client");
264
273
  console.log(" gmax plugin remove Remove all installed plugins");
265
274
  console.log(" gmax plugin remove <client> Remove a specific plugin");
266
275
  yield (0, exit_1.gracefulExit)();
@@ -270,4 +279,5 @@ exports.plugin = new commander_1.Command("plugin")
270
279
  .description("Manage gmax plugins for AI coding clients")
271
280
  .action(statusAction)
272
281
  .addCommand(addCmd)
282
+ .addCommand(updateCmd)
273
283
  .addCommand(removeCmd);
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.runSearch = runSearch;
46
46
  const path = __importStar(require("node:path"));
47
+ const config_1 = require("../config");
47
48
  const grammar_loader_1 = require("../lib/index/grammar-loader");
48
49
  const sync_helpers_1 = require("../lib/index/sync-helpers");
49
50
  const syncer_1 = require("../lib/index/syncer");
@@ -188,6 +189,7 @@ function runSearch(params) {
188
189
  lastIndexed: new Date().toISOString(),
189
190
  chunkCount: result.indexed,
190
191
  status: "indexed",
192
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
191
193
  });
192
194
  const failedSuffix = result.failedFiles > 0 ? ` • ${result.failedFiles} failed` : "";
193
195
  spinner.succeed(`${options.sync ? "Indexing" : "Initial indexing"} complete (${result.processed}/${result.total}) • indexed ${result.indexed}${failedSuffix}`);
@@ -144,7 +144,17 @@ Examples:
144
144
  // Check for running server. The per-project HTTP server can't answer
145
145
  // cross-project queries, so cross-project mode skips it and uses the
146
146
  // daemon-mediated / in-process path (both query the shared table).
147
- const execPathForServer = exec_path ? path.resolve(exec_path) : root;
147
+ let resolvedOptionRoot;
148
+ if (options.root && !crossProject.active) {
149
+ resolvedOptionRoot = (0, project_registry_1.resolveRootOrExit)(options.root);
150
+ if (resolvedOptionRoot === null)
151
+ return;
152
+ }
153
+ const execPathForServer = resolvedOptionRoot
154
+ ? resolvedOptionRoot
155
+ : exec_path
156
+ ? path.resolve(exec_path)
157
+ : root;
148
158
  const projectRootForServer = (_a = (0, project_root_1.findProjectRoot)(execPathForServer)) !== null && _a !== void 0 ? _a : execPathForServer;
149
159
  const server = crossProject.active
150
160
  ? null
@@ -167,21 +177,21 @@ Examples:
167
177
  try {
168
178
  yield (0, setup_helpers_1.ensureSetup)();
169
179
  const searchRoot = exec_path ? path.resolve(exec_path) : root;
170
- const projectRoot = (_b = (0, project_root_1.findProjectRoot)(searchRoot)) !== null && _b !== void 0 ? _b : searchRoot;
171
- const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
172
- // Propagate project root to worker processes
173
- process.env.GMAX_PROJECT_ROOT = projectRoot;
174
- // Check if project is registered
175
- let checkRoot;
180
+ const cwdProjectRoot = (_b = (0, project_root_1.findProjectRoot)(searchRoot)) !== null && _b !== void 0 ? _b : searchRoot;
181
+ let checkRoot = cwdProjectRoot;
176
182
  if (options.root) {
177
- const resolved = (0, project_registry_1.resolveRootOrExit)(options.root);
183
+ const resolved = resolvedOptionRoot !== undefined
184
+ ? resolvedOptionRoot
185
+ : (0, project_registry_1.resolveRootOrExit)(options.root);
178
186
  if (resolved === null)
179
187
  return;
180
188
  checkRoot = (_c = (0, project_root_1.findProjectRoot)(resolved)) !== null && _c !== void 0 ? _c : resolved;
181
189
  }
182
- else {
183
- checkRoot = projectRoot;
184
- }
190
+ const projectRoot = crossProject.active ? cwdProjectRoot : checkRoot;
191
+ const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
192
+ // Propagate project root to worker processes
193
+ process.env.GMAX_PROJECT_ROOT = projectRoot;
194
+ // Check if project is registered
185
195
  const project = (0, project_registry_1.getProject)(checkRoot);
186
196
  if (!project) {
187
197
  console.error(`This project hasn't been added to gmax yet.\n\nRun: gmax add ${checkRoot}\n`);
@@ -72,7 +72,7 @@ exports.testFind = new commander_1.Command("test")
72
72
  (0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
73
73
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
74
74
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
75
- const { symbols, resolvedAsFile } = yield (0, impact_1.resolveTargetSymbols)(target, vectorDb, projectRoot);
75
+ const { symbols, resolvedAsFile, symbolFamilies } = yield (0, impact_1.resolveTargetSymbols)(target, vectorDb, projectRoot);
76
76
  if (symbols.length === 0) {
77
77
  console.log(resolvedAsFile
78
78
  ? `No symbols found in file: ${target}`
@@ -89,7 +89,7 @@ exports.testFind = new commander_1.Command("test")
89
89
  const queryRoot = opts.in && opts.in.length > 0
90
90
  ? scope.pathPrefix.replace(/\/$/, "")
91
91
  : projectRoot;
92
- const tests = yield (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes);
92
+ const tests = yield (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes, symbolFamilies);
93
93
  if (tests.length === 0) {
94
94
  console.log(`No tests found for ${target}.`);
95
95
  return;
@@ -56,6 +56,10 @@ const stale_hint_1 = require("../lib/utils/stale-hint");
56
56
  const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
57
57
  const dim = (s) => (useColors ? `\x1b[2m${s}\x1b[22m` : s);
58
58
  const bold = (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s);
59
+ // Inferred-edge marker for caller rows: `member` = receiver-unverified `x.T()`,
60
+ // `type` = a type-position reference (not a call). Free calls stay unmarked —
61
+ // the trustworthy default — so only guesses carry a tag.
62
+ const kindTag = (k) => k === "member" || k === "type" ? ` (${k})` : "";
59
63
  function formatTraceAgent(graph, projectRoot, raw = false) {
60
64
  if (!graph.center)
61
65
  return "(not found)";
@@ -71,7 +75,7 @@ function formatTraceAgent(graph, projectRoot, raw = false) {
71
75
  function walkCallers(tree, depth) {
72
76
  if (raw) {
73
77
  for (const t of tree) {
74
- lines.push(`${" ".repeat(depth)}<- ${t.node.symbol}\t${rel(t.node.file)}:${t.node.line}`);
78
+ lines.push(`${" ".repeat(depth)}<- ${t.node.symbol}\t${rel(t.node.file)}:${t.node.line}${kindTag(t.node.edgeKind)}`);
75
79
  walkCallers(t.callers, depth + 1);
76
80
  }
77
81
  return;
@@ -92,6 +96,9 @@ function formatTraceAgent(graph, projectRoot, raw = false) {
92
96
  file: t.node.file,
93
97
  lineSet: new Set(),
94
98
  sub: [],
99
+ // Callers arrive free-first (getCallers sorts), so the first edgeKind
100
+ // seen for this group is its highest-confidence one.
101
+ edgeKind: t.node.edgeKind,
95
102
  };
96
103
  groups.set(key, g);
97
104
  order.push(key);
@@ -103,7 +110,7 @@ function formatTraceAgent(graph, projectRoot, raw = false) {
103
110
  const g = groups.get(key);
104
111
  const locs = [...g.lineSet].sort((a, b) => a - b).join(",");
105
112
  const loc = g.file ? `${rel(g.file)}:${locs}` : "(not indexed)";
106
- lines.push(`${" ".repeat(depth)}<- ${g.symbol}\t${loc}`);
113
+ lines.push(`${" ".repeat(depth)}<- ${g.symbol}\t${loc}${kindTag(g.edgeKind)}`);
107
114
  walkCallers(g.sub, depth + 1);
108
115
  }
109
116
  }
@@ -139,6 +146,7 @@ function buildInboundTree(callerTree, targetSymbol, fileCache, withSnippets, lim
139
146
  line: t.node.line,
140
147
  snippet: (_b = snippet === null || snippet === void 0 ? void 0 : snippet.snippet) !== null && _b !== void 0 ? _b : null,
141
148
  snippetLine: (_c = snippet === null || snippet === void 0 ? void 0 : snippet.snippetLine) !== null && _c !== void 0 ? _c : null,
149
+ edgeKind: t.node.edgeKind,
142
150
  callers: buildInboundTree(t.callers, t.node.symbol, fileCache, withSnippets, limit),
143
151
  });
144
152
  if (out.length >= limit)
@@ -158,8 +166,8 @@ function formatInboundAgent(center, tree, projectRoot, withSnippets) {
158
166
  ? `${rel(n.file)}:${((_a = n.snippetLine) !== null && _a !== void 0 ? _a : n.line) + 1}`
159
167
  : "(not indexed)";
160
168
  const cols = withSnippets
161
- ? `${loc}\t${n.symbol}\t${(_b = n.snippet) !== null && _b !== void 0 ? _b : ""}`
162
- : `${loc}\t${n.symbol}`;
169
+ ? `${loc}\t${n.symbol}${kindTag(n.edgeKind)}\t${(_b = n.snippet) !== null && _b !== void 0 ? _b : ""}`
170
+ : `${loc}\t${n.symbol}${kindTag(n.edgeKind)}`;
163
171
  lines.push(`${prefix}${cols}`);
164
172
  walk(n.callers, depth + 1);
165
173
  }
@@ -194,7 +202,7 @@ function formatInboundHuman(center, tree, projectRoot, withSnippets) {
194
202
  const loc = n.file
195
203
  ? `${rel(n.file)}:${((_a = n.snippetLine) !== null && _a !== void 0 ? _a : n.line) + 1}`
196
204
  : "(not indexed)";
197
- lines.push(`${indent}${n.symbol} ${dim(loc)}`);
205
+ lines.push(`${indent}${n.symbol} ${dim(loc)}${dim(kindTag(n.edgeKind))}`);
198
206
  if (withSnippets && n.snippet) {
199
207
  lines.push(`${indent} ${dim(n.snippet)}`);
200
208
  }
@@ -115,6 +115,7 @@ function handleCommand(daemon, cmd, conn) {
115
115
  case "ping":
116
116
  return {
117
117
  ok: true,
118
+ ready: daemon.isReady(),
118
119
  pid: process.pid,
119
120
  uptime: daemon.uptime(),
120
121
  version: DAEMON_VERSION,
@@ -147,22 +147,66 @@ class MlxServerManager {
147
147
  const env = Object.assign({}, process.env);
148
148
  if (mlxModel)
149
149
  env.MLX_EMBED_MODEL = mlxModel;
150
- this.mlxChild = (0, node_child_process_1.spawn)("uv", ["run", "python", "server.py"], {
151
- cwd: serverDir,
152
- detached: true,
153
- stdio: ["ignore", logFd, logFd],
154
- env,
150
+ const closeLogFd = () => {
151
+ try {
152
+ fs.closeSync(logFd);
153
+ }
154
+ catch (_a) { }
155
+ };
156
+ try {
157
+ this.mlxChild = (0, node_child_process_1.spawn)("uv", ["run", "python", "server.py"], {
158
+ cwd: serverDir,
159
+ detached: true,
160
+ stdio: ["ignore", logFd, logFd],
161
+ env,
162
+ });
163
+ }
164
+ catch (err) {
165
+ closeLogFd();
166
+ console.error(`[daemon] MLX embed server failed to spawn: ${err instanceof Error ? err.message : String(err)} — falling back to CPU embeddings`);
167
+ this.mlxChild = null;
168
+ return;
169
+ }
170
+ const child = this.mlxChild;
171
+ let startupSettled = false;
172
+ let resolveStartupError;
173
+ const startupError = new Promise((resolve) => {
174
+ resolveStartupError = resolve;
155
175
  });
156
- this.mlxChild.unref();
157
- console.log(`[daemon] Starting MLX embed server (PID: ${this.mlxChild.pid})`);
176
+ const onChildError = (err) => {
177
+ if (!startupSettled) {
178
+ resolveStartupError(err);
179
+ return;
180
+ }
181
+ console.error(`[daemon] MLX embed server process error: ${err.message}`);
182
+ if (this.mlxChild === child)
183
+ this.mlxChild = null;
184
+ };
185
+ child.on("error", onChildError);
186
+ child.unref();
187
+ console.log(`[daemon] Starting MLX embed server (PID: ${child.pid})`);
158
188
  // Poll for readiness (up to 30s)
159
189
  for (let i = 0; i < 30; i++) {
160
- yield new Promise((r) => setTimeout(r, 1000));
190
+ const spawnError = yield Promise.race([
191
+ startupError,
192
+ new Promise((r) => setTimeout(() => r(null), 1000)),
193
+ ]);
194
+ if (spawnError) {
195
+ startupSettled = true;
196
+ child.off("error", onChildError);
197
+ closeLogFd();
198
+ console.error(`[daemon] MLX embed server failed to spawn: ${spawnError.message} — falling back to CPU embeddings`);
199
+ if (this.mlxChild === child)
200
+ this.mlxChild = null;
201
+ return;
202
+ }
161
203
  if (yield this.isMlxServerUp()) {
204
+ startupSettled = true;
162
205
  console.log("[daemon] MLX embed server ready");
163
206
  return;
164
207
  }
165
208
  }
209
+ startupSettled = true;
166
210
  console.error("[daemon] MLX embed server failed to start within 30s — falling back to CPU embeddings");
167
211
  this.mlxChild = null;
168
212
  });
@@ -84,7 +84,7 @@ class ProcessManager {
84
84
  return __awaiter(this, void 0, void 0, function* () {
85
85
  // 1. Check for other daemon processes
86
86
  const daemonPids = this.findProcessesByTitle("gmax-daemon").filter((pid) => pid !== process.pid);
87
- const workerPids = this.findProcessesByTitle("gmax-worker");
87
+ let workerPids = this.findProcessesByTitle("gmax-worker");
88
88
  if (daemonPids.length === 0 && workerPids.length === 0) {
89
89
  (0, logger_1.log)("daemon", "No stale processes found");
90
90
  return;
@@ -92,15 +92,21 @@ class ProcessManager {
92
92
  // A peer that's gracefully shutting down has already dropped its
93
93
  // socket/PID/lock (so the liveness probes below read "stale") yet is still
94
94
  // mid-cleanup — destroying workers, closing LanceDB. Killing it now corrupts
95
- // that teardown. Defer to its draining marker: don't kill it, and don't
96
- // sweep its workers (it's reaping them itself); just take over the free lock.
97
- let anyDraining = false;
95
+ // that teardown. Defer to its draining marker until the process exits; only
96
+ // after the marker grace elapses do we treat it like any other stale daemon.
98
97
  for (const pid of daemonPids) {
99
98
  (0, logger_1.log)("daemon", `found daemon PID:${pid}, checking liveness...`);
100
99
  if ((0, daemon_client_1.isDaemonDraining)(pid)) {
101
- anyDraining = true;
102
- (0, logger_1.log)("daemon", `daemon PID:${pid} is gracefully draining — leaving it to exit, taking over`);
103
- continue;
100
+ (0, logger_1.log)("daemon", `daemon PID:${pid} is gracefully draining — waiting for it to exit`);
101
+ const exited = yield (0, daemon_client_1.waitForDaemonDrain)(pid);
102
+ if (exited) {
103
+ (0, logger_1.log)("daemon", `daemon PID:${pid} finished draining`);
104
+ // The peer may have reaped its workers while we waited. Rescan before
105
+ // sweeping so we don't act on a stale process snapshot.
106
+ workerPids = this.findProcessesByTitle("gmax-worker");
107
+ continue;
108
+ }
109
+ (0, logger_1.log)("daemon", `daemon PID:${pid} still exists after drain grace — checking stale probes`);
104
110
  }
105
111
  // A busy daemon (mid-index, compaction, big LMDB write) can block the
106
112
  // event loop long enough to miss a ping. Two independent liveness
@@ -120,16 +126,9 @@ class ProcessManager {
120
126
  }
121
127
  // 2. Kill orphaned workers from previous daemon instances.
122
128
  // Safe because this runs before the new daemon's worker pool is initialized.
123
- // Skipped while a peer is draining — those workers belong to it and it's
124
- // tearing them down; sweeping them here would race its own cleanup.
125
- if (anyDraining) {
126
- (0, logger_1.log)("daemon", "skipping orphan-worker sweep — a peer is draining its own workers");
127
- }
128
- else {
129
- for (const pid of workerPids) {
130
- (0, logger_1.log)("daemon", `killing orphaned worker PID:${pid}`);
131
- yield (0, process_1.killProcess)(pid);
132
- }
129
+ for (const pid of workerPids) {
130
+ (0, logger_1.log)("daemon", `killing orphaned worker PID:${pid}`);
131
+ yield (0, process_1.killProcess)(pid);
133
132
  }
134
133
  (0, logger_1.log)("daemon", `Cleaned up ${daemonPids.length} stale daemon(s), ${workerPids.length} orphaned worker(s)`);
135
134
  });