grepmax 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,8 @@ Natural-language search that works like `grep`. Fast, local, and built for codin
24
24
  - **Role Detection:** Distinguishes `ORCHESTRATION` (high-level logic) from `DEFINITION` (types/classes).
25
25
  - **Local & Private:** 100% local embeddings via ONNX (CPU) or MLX (Apple Silicon GPU).
26
26
  - **Centralized Index:** One database at `~/.gmax/` — index once, search from anywhere.
27
- - **Agent-Ready:** Native output with symbols, roles, and call graphs.
27
+ - **LLM Summaries:** Optional Qwen3-Coder generates one-line descriptions per code chunk at index time.
28
+ - **Agent-Ready:** Pointer mode returns metadata (symbol, role, calls, summary) — no code snippets, ~80% fewer tokens.
28
29
 
29
30
  ## Quick Start
30
31
 
@@ -99,8 +100,8 @@ In our public benchmarks, `grepmax` can save about 20% of your LLM tokens and de
99
100
 
100
101
  | Tool | Description |
101
102
  | --- | --- |
102
- | `semantic_search` | Natural language code search. Use `root` to search a parent or sibling directory. |
103
- | `search_all` | Search ALL indexed code across every directory. |
103
+ | `semantic_search` | Code search by meaning. Returns pointers (symbol, file:line, role, calls, summary) by default. Use `root` for cross-directory search, `detail: "code"` for snippets. |
104
+ | `search_all` | Search ALL indexed code across every directory. Same pointer format. |
104
105
  | `code_skeleton` | Collapsed file structure (~4x fewer tokens than reading the full file) |
105
106
  | `trace_calls` | Call graph — who calls a symbol and what it calls (unscoped, crosses project boundaries) |
106
107
  | `list_symbols` | List indexed functions, classes, and types with definition locations |
@@ -228,6 +229,19 @@ On Macs with Apple Silicon, gmax defaults to MLX for GPU-accelerated embeddings.
228
229
 
229
230
  To force CPU mode: `GMAX_EMBED_MODE=cpu gmax index`
230
231
 
232
+ ### LLM Summaries
233
+
234
+ gmax can generate one-line natural language descriptions for every code chunk using a local LLM (Qwen3-Coder-30B-A3B via MLX). Summaries are pre-computed at index time and stored in LanceDB — zero latency at search time.
235
+
236
+ The summarizer server runs on port `8101` and auto-starts alongside the embed server. If unavailable, indexing proceeds without summaries.
237
+
238
+ Example search output with summaries:
239
+ ```
240
+ handleAuth [exported ORCH C:8] src/auth/handler.ts:45-90
241
+ Validates JWT from Authorization header, checks RBAC permissions, returns 401 on failure
242
+ parent:AuthController calls:validateToken,checkRole,respond
243
+ ```
244
+
231
245
  ## Configuration
232
246
 
233
247
  ### Ignoring Files
@@ -48,16 +48,17 @@ const os = __importStar(require("node:os"));
48
48
  const path = __importStar(require("node:path"));
49
49
  const commander_1 = require("commander");
50
50
  const config_1 = require("../config");
51
+ const index_config_1 = require("../lib/index/index-config");
51
52
  const exit_1 = require("../lib/utils/exit");
52
53
  const project_root_1 = require("../lib/utils/project-root");
53
54
  exports.doctor = new commander_1.Command("doctor")
54
55
  .description("Check gmax health and paths")
55
56
  .action(() => __awaiter(void 0, void 0, void 0, function* () {
57
+ var _a;
56
58
  console.log("🏥 gmax Doctor\n");
57
59
  const root = config_1.PATHS.globalRoot;
58
60
  const models = config_1.PATHS.models;
59
61
  const grammars = config_1.PATHS.grammars;
60
- const modelIds = [config_1.MODEL_IDS.embed, config_1.MODEL_IDS.colbert];
61
62
  const checkDir = (name, p) => {
62
63
  const exists = fs.existsSync(p);
63
64
  const symbol = exists ? "✅" : "❌";
@@ -66,18 +67,20 @@ exports.doctor = new commander_1.Command("doctor")
66
67
  checkDir("Root", root);
67
68
  checkDir("Models", models);
68
69
  checkDir("Grammars", grammars);
69
- const modelStatuses = modelIds.map((id) => {
70
+ const globalConfig = (0, index_config_1.readGlobalConfig)();
71
+ const tier = (_a = config_1.MODEL_TIERS[globalConfig.modelTier]) !== null && _a !== void 0 ? _a : config_1.MODEL_TIERS.small;
72
+ const embedModel = globalConfig.embedMode === "gpu" ? tier.mlxModel : tier.onnxModel;
73
+ console.log(`\nEmbed mode: ${globalConfig.embedMode} | Model tier: ${globalConfig.modelTier} (${tier.vectorDim}d)`);
74
+ console.log(`Embed model: ${embedModel}`);
75
+ console.log(`ColBERT model: ${config_1.MODEL_IDS.colbert}`);
76
+ const modelStatuses = [embedModel, config_1.MODEL_IDS.colbert].map((id) => {
70
77
  const modelPath = path.join(models, ...id.split("/"));
71
78
  return { id, path: modelPath, exists: fs.existsSync(modelPath) };
72
79
  });
73
- modelStatuses.forEach(({ id, path: p, exists }) => {
74
- const symbol = exists ? "✅" : "";
75
- console.log(`${symbol} Model: ${id} (${p})`);
80
+ modelStatuses.forEach(({ id, exists }) => {
81
+ const symbol = exists ? "✅" : "⚠️ ";
82
+ console.log(`${symbol} ${id}: ${exists ? "downloaded" : "will download on first use"}`);
76
83
  });
77
- const missingModels = modelStatuses.filter(({ exists }) => !exists);
78
- if (missingModels.length > 0) {
79
- console.log("❌ Some models are missing; gmax will try bundled copies first, then download.");
80
- }
81
84
  console.log(`\nLocal Project: ${process.cwd()}`);
82
85
  const projectRoot = (0, project_root_1.findProjectRoot)(process.cwd());
83
86
  if (projectRoot) {
@@ -87,6 +90,40 @@ exports.doctor = new commander_1.Command("doctor")
87
90
  else {
88
91
  console.log(`ℹ️ No index found in current directory (run 'gmax index' to create one)`);
89
92
  }
93
+ // Check MLX embed server
94
+ const embedUp = yield fetch("http://127.0.0.1:8100/health")
95
+ .then((r) => r.ok)
96
+ .catch(() => false);
97
+ console.log(`${embedUp ? "✅" : "⚠️ "} MLX Embed: ${embedUp ? "running (port 8100)" : "not running"}`);
98
+ // Check summarizer server
99
+ const summarizerUp = yield fetch("http://127.0.0.1:8101/health")
100
+ .then((r) => r.ok)
101
+ .catch(() => false);
102
+ console.log(`${summarizerUp ? "✅" : "⚠️ "} Summarizer: ${summarizerUp ? "running (port 8101)" : "not running"}`);
103
+ // Check summary coverage
104
+ try {
105
+ const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
106
+ const db = new VectorDB(config_1.PATHS.lancedbDir);
107
+ const table = yield db.ensureTable();
108
+ const totalChunks = yield table.countRows();
109
+ if (totalChunks > 0) {
110
+ const withSummary = (yield table
111
+ .query()
112
+ .where("length(summary) > 5")
113
+ .select(["id"])
114
+ .toArray()).length;
115
+ const pct = Math.round((withSummary / totalChunks) * 100);
116
+ const symbol = pct >= 90 ? "✅" : pct > 0 ? "⚠️ " : "❌";
117
+ console.log(`${symbol} Summary coverage: ${withSummary}/${totalChunks} (${pct}%)`);
118
+ }
119
+ else {
120
+ console.log("ℹ️ No indexed chunks yet");
121
+ }
122
+ yield db.close();
123
+ }
124
+ catch (_b) {
125
+ console.log("⚠️ Could not check summary coverage");
126
+ }
90
127
  console.log(`\nSystem: ${os.platform()} ${os.arch()} | Node: ${process.version}`);
91
128
  console.log("\nIf you see ✅ everywhere, you are ready to search!");
92
129
  yield (0, exit_1.gracefulExit)();
@@ -585,19 +585,34 @@ exports.mcp = new commander_1.Command("mcp")
585
585
  }
586
586
  function handleIndexStatus() {
587
587
  return __awaiter(this, void 0, void 0, function* () {
588
- var _a, _b, _c;
588
+ var _a, _b, _c, _d;
589
589
  try {
590
590
  const config = (0, index_config_1.readIndexConfig)(config_1.PATHS.configPath);
591
591
  const projects = (0, project_registry_1.listProjects)();
592
592
  const db = getVectorDb();
593
593
  const stats = yield db.getStats();
594
594
  const fileCount = yield db.getDistinctFileCount();
595
+ // Watcher status
596
+ const watcher = (0, watcher_registry_1.getWatcherCoveringPath)(projectRoot);
597
+ let watcherLine = "Watcher: not running";
598
+ if (watcher) {
599
+ const status = (_a = watcher.status) !== null && _a !== void 0 ? _a : "unknown";
600
+ const root = path.basename(watcher.projectRoot);
601
+ const reindex = watcher.lastReindex
602
+ ? `last reindex: ${Math.round((Date.now() - watcher.lastReindex) / 60000)}m ago`
603
+ : "";
604
+ watcherLine = `Watcher: ${status} (${root}/)${reindex ? ` ${reindex}` : ""}`;
605
+ if (status === "syncing") {
606
+ watcherLine += " — search results may be incomplete";
607
+ }
608
+ }
595
609
  const lines = [
596
610
  `Index: ~/.gmax/lancedb (${stats.chunks} chunks, ${fileCount} files)`,
597
- `Model: ${(_a = config === null || config === void 0 ? void 0 : config.embedModel) !== null && _a !== void 0 ? _a : "unknown"} (${(_b = config === null || config === void 0 ? void 0 : config.vectorDim) !== null && _b !== void 0 ? _b : "?"}d, ${(_c = config === null || config === void 0 ? void 0 : config.embedMode) !== null && _c !== void 0 ? _c : "unknown"})`,
611
+ `Model: ${(_b = config === null || config === void 0 ? void 0 : config.embedModel) !== null && _b !== void 0 ? _b : "unknown"} (${(_c = config === null || config === void 0 ? void 0 : config.vectorDim) !== null && _c !== void 0 ? _c : "?"}d, ${(_d = config === null || config === void 0 ? void 0 : config.embedMode) !== null && _d !== void 0 ? _d : "unknown"})`,
598
612
  (config === null || config === void 0 ? void 0 : config.indexedAt)
599
613
  ? `Last indexed: ${config.indexedAt}`
600
614
  : "",
615
+ watcherLine,
601
616
  "",
602
617
  "Indexed directories:",
603
618
  ...projects.map((p) => { var _a; return ` ${p.name}\t${p.root}\t${(_a = p.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"}`; }),
@@ -122,6 +122,7 @@ function toCompactHits(data) {
122
122
  ? chunk.defined_symbols.toArray().slice(0, 3)
123
123
  : [],
124
124
  preview: getPreviewText(chunk),
125
+ summary: typeof chunk.summary === "string" ? chunk.summary : undefined,
125
126
  };
126
127
  });
127
128
  }
@@ -174,12 +175,12 @@ function padL(s, w) {
174
175
  return " ".repeat(n) + s;
175
176
  }
176
177
  function formatCompactTSV(hits, projectRoot, query) {
177
- var _a;
178
+ var _a, _b;
178
179
  if (!hits.length)
179
180
  return "No matches found.";
180
181
  const lines = [];
181
182
  lines.push(`gmax hits\tquery=${query}\tcount=${hits.length}`);
182
- lines.push("path\tlines\tscore\trole\tconf\tdefined");
183
+ lines.push("path\tlines\tscore\trole\tconf\tdefined\tsummary");
183
184
  for (const hit of hits) {
184
185
  const relPath = path.isAbsolute(hit.path)
185
186
  ? path.relative(projectRoot, hit.path)
@@ -188,7 +189,8 @@ function formatCompactTSV(hits, projectRoot, query) {
188
189
  const role = compactRole(hit.role);
189
190
  const conf = compactConf(hit.confidence);
190
191
  const defs = ((_a = hit.defined) !== null && _a !== void 0 ? _a : []).join(",");
191
- lines.push([relPath, hit.range, score, role, conf, defs].join("\t"));
192
+ const summary = (_b = hit.summary) !== null && _b !== void 0 ? _b : "";
193
+ lines.push([relPath, hit.range, score, role, conf, defs, summary].join("\t"));
192
194
  }
193
195
  return lines.join("\n");
194
196
  }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.summarize = void 0;
46
+ const path = __importStar(require("node:path"));
47
+ const commander_1 = require("commander");
48
+ const sync_helpers_1 = require("../lib/index/sync-helpers");
49
+ const syncer_1 = require("../lib/index/syncer");
50
+ const vector_db_1 = require("../lib/store/vector-db");
51
+ const exit_1 = require("../lib/utils/exit");
52
+ const project_root_1 = require("../lib/utils/project-root");
53
+ exports.summarize = new commander_1.Command("summarize")
54
+ .description("Generate LLM summaries for indexed chunks without re-indexing")
55
+ .option("-p, --path <dir>", "Only summarize chunks under this directory")
56
+ .action((options) => __awaiter(void 0, void 0, void 0, function* () {
57
+ const paths = (0, project_root_1.ensureProjectPaths)(process.cwd());
58
+ const vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
59
+ const rootPrefix = options.path
60
+ ? `${path.resolve(options.path)}/`
61
+ : "";
62
+ const { spinner } = (0, sync_helpers_1.createIndexingSpinner)("", "Summarizing...");
63
+ try {
64
+ const count = yield (0, syncer_1.generateSummaries)(vectorDb, rootPrefix, (done, total) => {
65
+ spinner.text = `Summarizing... (${done}/${total})`;
66
+ });
67
+ if (count > 0) {
68
+ spinner.succeed(`Summarized ${count} chunks`);
69
+ }
70
+ else {
71
+ spinner.succeed("All chunks already have summaries (or summarizer unavailable)");
72
+ }
73
+ }
74
+ catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ spinner.fail(`Summarization failed: ${msg}`);
77
+ process.exitCode = 1;
78
+ }
79
+ finally {
80
+ yield vectorDb.close();
81
+ yield (0, exit_1.gracefulExit)();
82
+ }
83
+ }));
@@ -99,21 +99,31 @@ exports.watch = new commander_1.Command("watch")
99
99
  // Propagate project root to worker processes
100
100
  process.env.GMAX_PROJECT_ROOT = paths.root;
101
101
  console.log(`[watch:${projectName}] Starting...`);
102
- // Initial sync if no index
102
+ // Register early so MCP can see status
103
+ (0, watcher_registry_1.registerWatcher)({
104
+ pid: process.pid,
105
+ projectRoot,
106
+ startTime: Date.now(),
107
+ status: "syncing",
108
+ });
109
+ // Initial sync if this directory isn't indexed yet
103
110
  const vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
104
- if (!(yield vectorDb.hasAnyRows())) {
105
- console.log(`[watch:${projectName}] No index found, running initial sync...`);
111
+ const table = yield vectorDb.ensureTable();
112
+ const prefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
113
+ const indexed = yield table
114
+ .query()
115
+ .select(["id"])
116
+ .where(`path LIKE '${prefix}%'`)
117
+ .limit(1)
118
+ .toArray();
119
+ if (indexed.length === 0) {
120
+ console.log(`[watch:${projectName}] No index found for ${projectRoot}, running initial sync...`);
106
121
  yield (0, syncer_1.initialSync)({ projectRoot });
107
122
  console.log(`[watch:${projectName}] Initial sync complete.`);
108
123
  }
124
+ (0, watcher_registry_1.updateWatcherStatus)(process.pid, "watching");
109
125
  // Open resources for watcher
110
126
  const metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
111
- // Register
112
- (0, watcher_registry_1.registerWatcher)({
113
- pid: process.pid,
114
- projectRoot,
115
- startTime: Date.now(),
116
- });
117
127
  // Start watching
118
128
  const watcher = (0, watcher_1.startWatcher)({
119
129
  projectRoot,
@@ -123,6 +133,7 @@ exports.watch = new commander_1.Command("watch")
123
133
  onReindex: (files, ms) => {
124
134
  console.log(`[watch:${projectName}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
125
135
  lastActivity = Date.now();
136
+ (0, watcher_registry_1.updateWatcherStatus)(process.pid, "watching", Date.now());
126
137
  },
127
138
  });
128
139
  console.log(`[watch:${projectName}] File watcher active`);
package/dist/index.js CHANGED
@@ -49,6 +49,7 @@ const search_1 = require("./commands/search");
49
49
  const serve_1 = require("./commands/serve");
50
50
  const setup_1 = require("./commands/setup");
51
51
  const skeleton_1 = require("./commands/skeleton");
52
+ const summarize_1 = require("./commands/summarize");
52
53
  const symbols_1 = require("./commands/symbols");
53
54
  const trace_1 = require("./commands/trace");
54
55
  const watch_1 = require("./commands/watch");
@@ -82,5 +83,6 @@ commander_1.program.addCommand(droid_1.installDroid);
82
83
  commander_1.program.addCommand(droid_1.uninstallDroid);
83
84
  commander_1.program.addCommand(opencode_1.installOpencode);
84
85
  commander_1.program.addCommand(opencode_1.uninstallOpencode);
86
+ commander_1.program.addCommand(summarize_1.summarize);
85
87
  commander_1.program.addCommand(doctor_1.doctor);
86
88
  commander_1.program.parse();
@@ -51,16 +51,21 @@ const path = __importStar(require("node:path"));
51
51
  const config_1 = require("../../config");
52
52
  const GLOBAL_CONFIG_PATH = path.join(config_1.PATHS.globalRoot, "config.json");
53
53
  function readGlobalConfig() {
54
+ const defaultEmbedMode = process.arch === "arm64" && process.platform === "darwin" ? "gpu" : "cpu";
54
55
  try {
55
56
  const raw = fs.readFileSync(GLOBAL_CONFIG_PATH, "utf-8");
56
- return JSON.parse(raw);
57
+ const parsed = JSON.parse(raw);
58
+ // Ensure embedMode has a default even if missing from stored config
59
+ if (!parsed.embedMode)
60
+ parsed.embedMode = defaultEmbedMode;
61
+ return parsed;
57
62
  }
58
63
  catch (_a) {
59
64
  const tier = config_1.MODEL_TIERS[config_1.DEFAULT_MODEL_TIER];
60
65
  return {
61
66
  modelTier: config_1.DEFAULT_MODEL_TIER,
62
67
  vectorDim: tier.vectorDim,
63
- embedMode: process.arch === "arm64" && process.platform === "darwin" ? "gpu" : "cpu",
68
+ embedMode: defaultEmbedMode,
64
69
  };
65
70
  }
66
71
  }
@@ -49,6 +49,7 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
49
49
  function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
50
50
  };
51
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.generateSummaries = generateSummaries;
52
53
  exports.initialSync = initialSync;
53
54
  const fs = __importStar(require("node:fs"));
54
55
  const path = __importStar(require("node:path"));
@@ -62,6 +63,69 @@ const project_root_1 = require("../utils/project-root");
62
63
  const pool_1 = require("../workers/pool");
63
64
  const index_config_1 = require("./index-config");
64
65
  const walker_1 = require("./walker");
66
+ function generateSummaries(db, pathPrefix, onProgress) {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ let summarizeChunks;
69
+ try {
70
+ const mod = yield Promise.resolve().then(() => __importStar(require("../workers/summarize/llm-client")));
71
+ summarizeChunks = mod.summarizeChunks;
72
+ }
73
+ catch (_a) {
74
+ return 0;
75
+ }
76
+ // Quick availability check
77
+ const test = yield summarizeChunks([
78
+ { code: "test", language: "ts", file: "test" },
79
+ ]);
80
+ if (!test)
81
+ return 0;
82
+ const table = yield db.ensureTable();
83
+ const rows = yield table
84
+ .query()
85
+ .select(["id", "path", "content", "defined_symbols"])
86
+ .where(`path LIKE '${pathPrefix}%' AND (summary IS NULL OR summary = '')`)
87
+ .limit(50000)
88
+ .toArray();
89
+ if (rows.length === 0)
90
+ return 0;
91
+ let summarized = 0;
92
+ const BATCH_SIZE = 5;
93
+ for (let i = 0; i < rows.length; i += BATCH_SIZE) {
94
+ const batch = rows.slice(i, i + BATCH_SIZE);
95
+ const chunks = batch.map((r) => {
96
+ var _a;
97
+ const defs = Array.isArray(r.defined_symbols)
98
+ ? r.defined_symbols.filter((s) => typeof s === "string")
99
+ : typeof ((_a = r.defined_symbols) === null || _a === void 0 ? void 0 : _a.toArray) === "function"
100
+ ? r.defined_symbols.toArray()
101
+ : [];
102
+ return {
103
+ code: String(r.content || ""),
104
+ language: path.extname(String(r.path || "")).replace(/^\./, "") || "unknown",
105
+ file: String(r.path || ""),
106
+ symbols: defs,
107
+ };
108
+ });
109
+ const summaries = yield summarizeChunks(chunks);
110
+ if (!summaries)
111
+ break;
112
+ const ids = [];
113
+ const values = [];
114
+ for (let j = 0; j < batch.length; j++) {
115
+ if (summaries[j]) {
116
+ ids.push(String(batch[j].id));
117
+ values.push(summaries[j]);
118
+ }
119
+ }
120
+ if (ids.length > 0) {
121
+ yield db.updateRows(ids, "summary", values);
122
+ summarized += ids.length;
123
+ }
124
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress(summarized, rows.length);
125
+ }
126
+ return summarized;
127
+ });
128
+ }
65
129
  function flushBatch(db, meta, vectors, pendingMeta, pendingDeletes, dryRun) {
66
130
  return __awaiter(this, void 0, void 0, function* () {
67
131
  if (dryRun)
@@ -388,6 +452,31 @@ function initialSync(options) {
388
452
  metaCache.delete(p);
389
453
  });
390
454
  }
455
+ // --- Summary post-processing (sequential, single process) ---
456
+ if (!dryRun && indexed > 0) {
457
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress({
458
+ processed,
459
+ indexed,
460
+ total,
461
+ filePath: "Generating summaries...",
462
+ });
463
+ const summarized = yield generateSummaries(vectorDb, rootPrefix, (count, chunkTotal) => {
464
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress({
465
+ processed: count,
466
+ indexed,
467
+ total: chunkTotal,
468
+ filePath: `Summarizing... (${count}/${chunkTotal})`,
469
+ });
470
+ });
471
+ if (summarized > 0) {
472
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress({
473
+ processed,
474
+ indexed,
475
+ total,
476
+ filePath: `Summarized ${summarized} chunks`,
477
+ });
478
+ }
479
+ }
391
480
  // Write model config so future runs can detect model changes
392
481
  if (!dryRun) {
393
482
  (0, index_config_1.writeIndexConfig)(paths.configPath);
@@ -50,6 +50,7 @@ const chokidar_1 = require("chokidar");
50
50
  const file_utils_1 = require("../utils/file-utils");
51
51
  const lock_1 = require("../utils/lock");
52
52
  const pool_1 = require("../workers/pool");
53
+ const llm_client_1 = require("../workers/summarize/llm-client");
53
54
  // Chokidar ignored — must exclude heavy directories to keep FD count low.
54
55
  // On macOS, chokidar uses FSEvents (single FD) but falls back to fs.watch()
55
56
  // (one FD per directory) if FSEvents isn't available or for some subdirs.
@@ -103,6 +104,7 @@ function startWatcher(opts) {
103
104
  pending.clear();
104
105
  const start = Date.now();
105
106
  let reindexed = 0;
107
+ const changedIds = [];
106
108
  try {
107
109
  const lock = yield (0, lock_1.acquireWriterLockWithRetry)(dataDir, {
108
110
  maxRetries: 3,
@@ -115,10 +117,9 @@ function startWatcher(opts) {
115
117
  const metaUpdates = new Map();
116
118
  const metaDeletes = [];
117
119
  for (const [absPath, event] of batch) {
118
- const relPath = path.relative(projectRoot, absPath);
119
120
  if (event === "unlink") {
120
- deletes.push(relPath);
121
- metaDeletes.push(relPath);
121
+ deletes.push(absPath);
122
+ metaDeletes.push(absPath);
122
123
  reindexed++;
123
124
  continue;
124
125
  }
@@ -128,9 +129,9 @@ function startWatcher(opts) {
128
129
  if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size))
129
130
  continue;
130
131
  // Check if content actually changed via hash
131
- const cached = metaCache.get(relPath);
132
+ const cached = metaCache.get(absPath);
132
133
  const result = yield pool.processFile({
133
- path: relPath,
134
+ path: absPath,
134
135
  absolutePath: absPath,
135
136
  });
136
137
  const metaEntry = {
@@ -139,33 +140,36 @@ function startWatcher(opts) {
139
140
  size: result.size,
140
141
  };
141
142
  if (cached && cached.hash === result.hash) {
142
- // Content unchanged (mtime changed but hash same) — just update meta
143
- metaUpdates.set(relPath, metaEntry);
143
+ metaUpdates.set(absPath, metaEntry);
144
144
  continue;
145
145
  }
146
146
  if (result.shouldDelete) {
147
- deletes.push(relPath);
148
- metaUpdates.set(relPath, metaEntry);
147
+ deletes.push(absPath);
148
+ metaUpdates.set(absPath, metaEntry);
149
149
  reindexed++;
150
150
  continue;
151
151
  }
152
152
  // Delete old vectors, insert new
153
- deletes.push(relPath);
153
+ deletes.push(absPath);
154
154
  if (result.vectors.length > 0) {
155
155
  vectors.push(...result.vectors);
156
+ // Track IDs of new vectors for summarization
157
+ for (const v of result.vectors) {
158
+ changedIds.push(v.id);
159
+ }
156
160
  }
157
- metaUpdates.set(relPath, metaEntry);
161
+ metaUpdates.set(absPath, metaEntry);
158
162
  reindexed++;
159
163
  }
160
164
  catch (err) {
161
165
  const code = err === null || err === void 0 ? void 0 : err.code;
162
166
  if (code === "ENOENT") {
163
- deletes.push(relPath);
164
- metaDeletes.push(relPath);
167
+ deletes.push(absPath);
168
+ metaDeletes.push(absPath);
165
169
  reindexed++;
166
170
  }
167
171
  else {
168
- console.error(`[watch] Failed to process ${relPath}:`, err);
172
+ console.error(`[watch] Failed to process ${absPath}:`, err);
169
173
  }
170
174
  }
171
175
  }
@@ -187,6 +191,39 @@ function startWatcher(opts) {
187
191
  finally {
188
192
  yield lock.release();
189
193
  }
194
+ // Summarize new/changed chunks outside the lock (sequential, no GPU contention)
195
+ if (changedIds.length > 0) {
196
+ try {
197
+ const table = yield vectorDb.ensureTable();
198
+ for (const id of changedIds) {
199
+ const escaped = id.replace(/'/g, "''");
200
+ const rows = yield table
201
+ .query()
202
+ .select(["id", "path", "content"])
203
+ .where(`id = '${escaped}'`)
204
+ .limit(1)
205
+ .toArray();
206
+ if (rows.length === 0)
207
+ continue;
208
+ const r = rows[0];
209
+ const lang = path.extname(String(r.path || "")).replace(/^\./, "") ||
210
+ "unknown";
211
+ const summaries = yield (0, llm_client_1.summarizeChunks)([
212
+ {
213
+ code: String(r.content || ""),
214
+ language: lang,
215
+ file: String(r.path || ""),
216
+ },
217
+ ]);
218
+ if (summaries === null || summaries === void 0 ? void 0 : summaries[0]) {
219
+ yield vectorDb.updateRows([id], "summary", [summaries[0]]);
220
+ }
221
+ }
222
+ }
223
+ catch (_a) {
224
+ // Summarizer unavailable — skip silently
225
+ }
226
+ }
190
227
  if (reindexed > 0) {
191
228
  const duration = Date.now() - start;
192
229
  onReindex === null || onReindex === void 0 ? void 0 : onReindex(reindexed, duration);
@@ -314,6 +314,21 @@ class VectorDB {
314
314
  }
315
315
  });
316
316
  }
317
+ updateRows(ids, field, values) {
318
+ return __awaiter(this, void 0, void 0, function* () {
319
+ var _a;
320
+ if (!ids.length)
321
+ return;
322
+ const table = yield this.ensureTable();
323
+ for (let i = 0; i < ids.length; i++) {
324
+ const escaped = ids[i].replace(/'/g, "''");
325
+ yield table.update({
326
+ where: `id = '${escaped}'`,
327
+ values: { [field]: (_a = values[i]) !== null && _a !== void 0 ? _a : "" },
328
+ });
329
+ }
330
+ });
331
+ }
317
332
  deletePathsWithPrefix(prefix) {
318
333
  return __awaiter(this, void 0, void 0, function* () {
319
334
  const table = yield this.ensureTable();
@@ -41,6 +41,7 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.isProcessRunning = isProcessRunning;
43
43
  exports.registerWatcher = registerWatcher;
44
+ exports.updateWatcherStatus = updateWatcherStatus;
44
45
  exports.unregisterWatcher = unregisterWatcher;
45
46
  exports.getWatcherForProject = getWatcherForProject;
46
47
  exports.getWatcherCoveringPath = getWatcherCoveringPath;
@@ -76,6 +77,16 @@ function registerWatcher(info) {
76
77
  entries.push(info);
77
78
  saveRegistry(entries);
78
79
  }
80
+ function updateWatcherStatus(pid, status, lastReindex) {
81
+ const entries = loadRegistry();
82
+ const match = entries.find((e) => e.pid === pid);
83
+ if (match) {
84
+ match.status = status;
85
+ if (lastReindex)
86
+ match.lastReindex = lastReindex;
87
+ saveRegistry(entries);
88
+ }
89
+ }
79
90
  function unregisterWatcher(pid) {
80
91
  const entries = loadRegistry().filter((e) => e.pid !== pid);
81
92
  saveRegistry(entries);
@@ -49,7 +49,6 @@ const transformers_1 = require("@huggingface/transformers");
49
49
  const ort = __importStar(require("onnxruntime-node"));
50
50
  const uuid_1 = require("uuid");
51
51
  const config_1 = require("../../config");
52
- const llm_client_1 = require("./summarize/llm-client");
53
52
  const chunker_1 = require("../index/chunker");
54
53
  const skeleton_1 = require("../skeleton");
55
54
  const file_utils_1 = require("../utils/file-utils");
@@ -214,23 +213,7 @@ class WorkerOrchestrator {
214
213
  if (!chunks.length)
215
214
  return { vectors: [], hash, mtimeMs, size };
216
215
  const preparedChunks = this.toPreparedChunks(input.path, hash, chunks, skeletonResult.success ? skeletonResult.skeleton : undefined);
217
- // Run embedding and summarization in parallel
218
- const lang = path.extname(input.path).replace(/^\./, "") || "unknown";
219
- const [hybrids, summaries] = yield Promise.all([
220
- this.computeHybrid(preparedChunks.map((chunk) => chunk.content), onProgress),
221
- (0, llm_client_1.summarizeChunks)(preparedChunks.map((c) => ({
222
- code: c.content,
223
- language: lang,
224
- file: c.path,
225
- }))),
226
- ]);
227
- // Attach summaries if available
228
- if (summaries) {
229
- for (let i = 0; i < preparedChunks.length; i++) {
230
- if (summaries[i])
231
- preparedChunks[i].summary = summaries[i];
232
- }
233
- }
216
+ const hybrids = yield this.computeHybrid(preparedChunks.map((chunk) => chunk.content), onProgress);
234
217
  const vectors = preparedChunks.map((chunk, idx) => {
235
218
  var _a;
236
219
  const hybrid = (_a = hybrids[idx]) !== null && _a !== void 0 ? _a : {
@@ -3,6 +3,9 @@
3
3
  * LLM summarizer HTTP client.
4
4
  * Talks to the MLX summarizer server to generate code summaries.
5
5
  * Returns null if server isn't running — caller skips summaries gracefully.
6
+ *
7
+ * Called from the main syncer process (not worker processes) to avoid
8
+ * GPU contention from multiple concurrent workers.
6
9
  */
7
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
11
  if (k2 === undefined) k2 = k;
@@ -48,14 +51,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
48
51
  };
49
52
  Object.defineProperty(exports, "__esModule", { value: true });
50
53
  exports.summarizeChunks = summarizeChunks;
51
- exports.resetSummarizerCache = resetSummarizerCache;
52
54
  const http = __importStar(require("node:http"));
53
55
  const SUMMARY_PORT = parseInt(process.env.GMAX_SUMMARY_PORT || "8101", 10);
54
56
  const SUMMARY_HOST = "127.0.0.1";
55
- const SUMMARY_TIMEOUT_MS = 120000; // 2 min — batches of chunks take time
56
- let summarizerAvailable = null;
57
- let lastCheck = 0;
58
- const CHECK_INTERVAL_MS = 5000; // short cache — retry quickly if server just started
57
+ const SUMMARY_TIMEOUT_MS = 120000;
59
58
  function postJSON(path, body) {
60
59
  return new Promise((resolve) => {
61
60
  const payload = JSON.stringify(body);
@@ -91,75 +90,18 @@ function postJSON(path, body) {
91
90
  req.end();
92
91
  });
93
92
  }
94
- function isSummarizerUp() {
95
- return __awaiter(this, void 0, void 0, function* () {
96
- const now = Date.now();
97
- if (summarizerAvailable !== null && now - lastCheck < CHECK_INTERVAL_MS) {
98
- return summarizerAvailable;
99
- }
100
- const result = yield new Promise((resolve) => {
101
- const req = http.get({
102
- hostname: SUMMARY_HOST,
103
- port: SUMMARY_PORT,
104
- path: "/health",
105
- timeout: 5000,
106
- }, (res) => {
107
- res.resume();
108
- resolve(res.statusCode === 200);
109
- });
110
- req.on("error", () => resolve(false));
111
- req.on("timeout", () => {
112
- req.destroy();
113
- resolve(false);
114
- });
115
- });
116
- summarizerAvailable = result;
117
- lastCheck = now;
118
- return result;
119
- });
120
- }
121
93
  /**
122
94
  * Generate summaries for code chunks via the local LLM server.
123
- * Sends one chunk at a time. Skips health check — just tries the request.
124
- * If the server is busy, the TCP connection queues until it's ready.
125
95
  * Returns string[] on success, null if server unavailable.
126
96
  */
127
97
  function summarizeChunks(chunks) {
128
98
  return __awaiter(this, void 0, void 0, function* () {
129
- var _a;
130
99
  if (chunks.length === 0)
131
100
  return [];
132
- // Quick check only if we've never connected
133
- if (summarizerAvailable === null) {
134
- summarizerAvailable = yield isSummarizerUp();
135
- if (!summarizerAvailable)
136
- return null;
137
- }
138
- if (summarizerAvailable === false) {
139
- // Recheck periodically
140
- const now = Date.now();
141
- if (now - lastCheck < CHECK_INTERVAL_MS)
142
- return null;
143
- summarizerAvailable = yield isSummarizerUp();
144
- if (!summarizerAvailable)
145
- return null;
101
+ const { ok, data } = yield postJSON("/summarize", { chunks });
102
+ if (!ok || !(data === null || data === void 0 ? void 0 : data.summaries)) {
103
+ return null;
146
104
  }
147
- const summaries = [];
148
- for (const chunk of chunks) {
149
- const { ok, data } = yield postJSON("/summarize", {
150
- chunks: [chunk],
151
- });
152
- if (!ok || !((_a = data === null || data === void 0 ? void 0 : data.summaries) === null || _a === void 0 ? void 0 : _a[0])) {
153
- summaries.push("");
154
- }
155
- else {
156
- summaries.push(data.summaries[0]);
157
- }
158
- }
159
- return summaries;
105
+ return data.summaries;
160
106
  });
161
107
  }
162
- function resetSummarizerCache() {
163
- summarizerAvailable = null;
164
- lastCheck = 0;
165
- }
@@ -50,8 +50,12 @@ SYSTEM_PROMPT = """You are a code summarizer. Given a code chunk, produce exactl
50
50
  Be specific about business logic, services, and side effects. Do not describe syntax.
51
51
  Do not use phrases like "This function" or "This code". Start with a verb."""
52
52
 
53
- def build_prompt(code: str, language: str, file: str) -> str:
54
- return f"Language: {language}\nFile: {file}\n\n```\n{code}\n```"
53
+ def build_prompt(code: str, language: str, file: str, symbols: list[str] | None = None) -> str:
54
+ parts = [f"Language: {language}", f"File: {file}"]
55
+ if symbols:
56
+ parts.append(f"Defines: {', '.join(symbols)}")
57
+ parts.append(f"\n```\n{code}\n```")
58
+ return "\n".join(parts)
55
59
 
56
60
 
57
61
  def is_port_in_use(port: int) -> bool:
@@ -59,11 +63,11 @@ def is_port_in_use(port: int) -> bool:
59
63
  return s.connect_ex(("127.0.0.1", port)) == 0
60
64
 
61
65
 
62
- def summarize_chunk(code: str, language: str, file: str) -> str:
66
+ def summarize_chunk(code: str, language: str, file: str, symbols: list[str] | None = None) -> str:
63
67
  """Generate a one-line summary for a code chunk."""
64
68
  messages = [
65
69
  {"role": "system", "content": SYSTEM_PROMPT},
66
- {"role": "user", "content": build_prompt(code, language, file)},
70
+ {"role": "user", "content": build_prompt(code, language, file, symbols)},
67
71
  ]
68
72
  prompt = tokenizer.apply_chat_template(
69
73
  messages, tokenize=False, add_generation_prompt=True
@@ -106,6 +110,7 @@ class ChunkInput(BaseModel):
106
110
  code: str
107
111
  language: str = "unknown"
108
112
  file: str = ""
113
+ symbols: list[str] = []
109
114
 
110
115
 
111
116
  class SummarizeRequest(BaseModel):
@@ -125,7 +130,7 @@ async def summarize(request: SummarizeRequest) -> SummarizeResponse:
125
130
  async with _mlx_lock:
126
131
  for chunk in request.chunks:
127
132
  try:
128
- summary = summarize_chunk(chunk.code, chunk.language, chunk.file)
133
+ summary = summarize_chunk(chunk.code, chunk.language, chunk.file, chunk.symbols or None)
129
134
  summaries.append(summary)
130
135
  except Exception as e:
131
136
  summaries.append(f"(summary failed: {e})")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -29,7 +29,7 @@
29
29
  "NOTICE"
30
30
  ],
31
31
  "license": "Apache-2.0",
32
- "description": "Local grep-like search tool for your codebase.",
32
+ "description": "Semantic code search for coding agents. Local embeddings, LLM summaries, call graph tracing.",
33
33
  "dependencies": {
34
34
  "@clack/prompts": "^1.1.0",
35
35
  "@huggingface/transformers": "^3.8.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",
@@ -6,63 +6,86 @@ allowed-tools: "mcp__grepmax__semantic_search, mcp__grepmax__search_all, mcp__gr
6
6
 
7
7
  ## What gmax does
8
8
 
9
- Finds code by meaning. When you'd ask a colleague "where do we handle auth?", use gmax.
9
+ Semantic code search finds code by meaning, not just strings.
10
10
 
11
- - grep/ripgrep: exact string match, fast
12
- - gmax: concept match, finds code you couldn't grep for
11
+ - grep/ripgrep: exact string match
12
+ - gmax: concept match ("where do we handle auth?", "how does booking flow work?")
13
13
 
14
14
  ## MCP tools
15
15
 
16
16
  ### semantic_search
17
- Search code by meaning. Returns **pointers** by default — symbol, file:line, role, calls. No code snippets unless requested.
18
- - `query` (required): Natural language. Be specific — more words = better results.
19
- - `limit` (optional): Max results (default 3, max 50)
20
- - `root` (optional): Directory to search. Defaults to project root. Use to search a parent directory (e.g. `root: "../"` to search the monorepo).
21
- - `path` (optional): Restrict to path prefix (e.g. "src/auth/")
22
- - `detail` (optional): `"pointer"` (default) or `"code"` (adds 4-line numbered snippets)
23
- - `min_score` (optional): Filter by minimum relevance score (0-1)
24
- - `max_per_file` (optional): Cap results per file for diversity
17
+ Search code by meaning. Two output modes:
25
18
 
26
- **Output format (pointer mode):**
19
+ **Pointer mode (default)** — returns metadata + LLM-generated summary per result:
27
20
  ```
28
21
  handleAuth [exported ORCH C:8] src/auth/handler.ts:45-90
22
+ Validates JWT from Authorization header, checks RBAC permissions, returns 401 on failure
29
23
  parent:AuthController calls:validateToken,checkRole,respond
30
24
  ```
31
25
 
32
- **When to use `detail: "code"`:** Only when you need to see the actual code before deciding to Read — e.g. comparing implementations, checking syntax. For navigation ("where is X?"), pointer mode is sufficient.
26
+ **Code mode (`detail: "code"`)** includes 4-line numbered code snippets:
27
+ ```
28
+ handleAuth [exported ORCH C:8] src/auth/handler.ts:45-90
29
+ Validates JWT from Authorization header, checks RBAC permissions, returns 401 on failure
30
+ parent:AuthController calls:validateToken,checkRole,respond
31
+ 45│ const token = req.headers.get("Authorization");
32
+ 46│ const claims = await validateToken(token);
33
+ 47│ if (!claims) return unauthorized();
34
+ 48│ const allowed = await checkRole(claims.role, req.path);
35
+ ```
36
+
37
+ Parameters:
38
+ - `query` (required): Natural language. More words = better results.
39
+ - `limit` (optional): Max results (default 3, max 50)
40
+ - `root` (optional): Directory to search. Use `root: "../"` to search a parent directory.
41
+ - `path` (optional): Restrict to path prefix (e.g. "src/auth/")
42
+ - `detail` (optional): `"pointer"` (default) or `"code"`
43
+ - `min_score` (optional): Filter by minimum relevance score (0-1)
44
+ - `max_per_file` (optional): Cap results per file for diversity
45
+
46
+ **When to use which mode:**
47
+ - `pointer` — navigation, finding locations, understanding architecture
48
+ - `code` — comparing implementations, finding duplicates, checking syntax
33
49
 
34
50
  ### search_all
35
- Search ALL indexed code across every directory. Same output format as semantic_search. Use when code could be anywhere — e.g. tracing a function across projects.
51
+ Search ALL indexed code across every directory. Same modes as semantic_search.
36
52
 
37
53
  ### code_skeleton
38
- Show file structure — signatures with bodies collapsed (~4x fewer tokens).
54
+ File structure — signatures with bodies collapsed (~4x fewer tokens).
39
55
  - `target` (required): File path relative to project root
40
56
 
41
57
  ### trace_calls
42
- Trace call graph — who calls a symbol and what it calls. Unscoped — follows calls across all indexed directories.
43
- - `symbol` (required): Function/method/class name (e.g. "handleAuth")
58
+ Call graph — who calls a symbol and what it calls. Unscoped — follows calls across all indexed directories.
59
+ - `symbol` (required): Function/method/class name
44
60
 
45
61
  ### list_symbols
46
62
  List indexed symbols with definition locations.
47
- - `pattern` (optional): Filter by name (case-insensitive substring)
48
- - `limit` (optional): Max results (default 20, max 100)
63
+ - `pattern` (optional): Filter by name
64
+ - `limit` (optional): Max results (default 20)
49
65
  - `path` (optional): Only symbols under this path prefix
50
66
 
51
67
  ### index_status
52
- Check centralized index health — chunk count, files, indexed directories, model info.
68
+ Check centralized index health — chunks, files, indexed directories, model info.
53
69
 
54
70
  ## Workflow
55
71
 
56
- 1. **Locate** — `semantic_search` with pointer mode to find relevant code
72
+ 1. **Search** — `semantic_search` to find relevant code (pointers by default)
57
73
  2. **Read** — `Read file:line` for the specific ranges you need
58
- 3. **Trace** — `trace_calls` to understand how functions connect
59
- 4. **Skeleton** — `code_skeleton` before reading large files
74
+ 3. **Compare** — `semantic_search` with `detail: "code"` when comparing implementations
75
+ 4. **Trace** — `trace_calls` to understand call flow across files
76
+ 5. **Skeleton** — `code_skeleton` before reading large files
77
+
78
+ ## If results seem stale
60
79
 
61
- Don't read entire files. Use the line ranges from search results.
80
+ 1. Check `index_status` if watcher shows "syncing", results may be incomplete. Wait for it.
81
+ 2. To force a re-index: `Bash(gmax index)` (indexes current directory)
82
+ 3. To add summaries without re-indexing: `Bash(gmax summarize)`
83
+ 4. Do NOT use `gmax reindex` — it doesn't exist.
62
84
 
63
85
  ## Tips
64
86
 
65
87
  - More words = better results. "auth" is vague. "where does the server validate JWT tokens" is specific.
66
- - ORCH results contain the logic — prioritize these over DEF/IMPL.
88
+ - ORCH results contain the logic — prioritize over DEF/IMPL.
89
+ - Summaries tell you what the code does without reading it. Use them to decide what to Read.
67
90
  - Use `root` to search parent directories (monorepo, workspace).
68
91
  - Use `search_all` sparingly — it searches everything indexed.