grepmax 0.4.1 → 0.5.1

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.
@@ -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,10 +49,12 @@ 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"));
55
56
  const config_1 = require("../../config");
57
+ const logger_1 = require("../utils/logger");
56
58
  const meta_cache_1 = require("../store/meta-cache");
57
59
  const vector_db_1 = require("../store/vector-db");
58
60
  const file_utils_1 = require("../utils/file-utils");
@@ -62,6 +64,69 @@ const project_root_1 = require("../utils/project-root");
62
64
  const pool_1 = require("../workers/pool");
63
65
  const index_config_1 = require("./index-config");
64
66
  const walker_1 = require("./walker");
67
+ function generateSummaries(db, pathPrefix, onProgress) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ let summarizeChunks;
70
+ try {
71
+ const mod = yield Promise.resolve().then(() => __importStar(require("../workers/summarize/llm-client")));
72
+ summarizeChunks = mod.summarizeChunks;
73
+ }
74
+ catch (_a) {
75
+ return 0;
76
+ }
77
+ // Quick availability check
78
+ const test = yield summarizeChunks([
79
+ { code: "test", language: "ts", file: "test" },
80
+ ]);
81
+ if (!test)
82
+ return 0;
83
+ const table = yield db.ensureTable();
84
+ const rows = yield table
85
+ .query()
86
+ .select(["id", "path", "content", "defined_symbols"])
87
+ .where(`path LIKE '${pathPrefix}%' AND (summary IS NULL OR summary = '')`)
88
+ .limit(50000)
89
+ .toArray();
90
+ if (rows.length === 0)
91
+ return 0;
92
+ let summarized = 0;
93
+ const BATCH_SIZE = 5;
94
+ for (let i = 0; i < rows.length; i += BATCH_SIZE) {
95
+ const batch = rows.slice(i, i + BATCH_SIZE);
96
+ const chunks = batch.map((r) => {
97
+ var _a;
98
+ const defs = Array.isArray(r.defined_symbols)
99
+ ? r.defined_symbols.filter((s) => typeof s === "string")
100
+ : typeof ((_a = r.defined_symbols) === null || _a === void 0 ? void 0 : _a.toArray) === "function"
101
+ ? r.defined_symbols.toArray()
102
+ : [];
103
+ return {
104
+ code: String(r.content || ""),
105
+ language: path.extname(String(r.path || "")).replace(/^\./, "") || "unknown",
106
+ file: String(r.path || ""),
107
+ symbols: defs,
108
+ };
109
+ });
110
+ const summaries = yield summarizeChunks(chunks);
111
+ if (!summaries)
112
+ break;
113
+ const ids = [];
114
+ const values = [];
115
+ for (let j = 0; j < batch.length; j++) {
116
+ if (summaries[j]) {
117
+ ids.push(String(batch[j].id));
118
+ values.push(summaries[j]);
119
+ }
120
+ }
121
+ if (ids.length > 0) {
122
+ yield db.updateRows(ids, "summary", values);
123
+ summarized += ids.length;
124
+ }
125
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress(summarized, rows.length);
126
+ }
127
+ return summarized;
128
+ });
129
+ }
65
130
  function flushBatch(db, meta, vectors, pendingMeta, pendingDeletes, dryRun) {
66
131
  return __awaiter(this, void 0, void 0, function* () {
67
132
  if (dryRun)
@@ -119,6 +184,8 @@ function initialSync(options) {
119
184
  : `${resolvedRoot}/`;
120
185
  // Propagate project root to worker processes
121
186
  process.env.GMAX_PROJECT_ROOT = paths.root;
187
+ const syncTimer = (0, logger_1.timer)("index", "Total");
188
+ (0, logger_1.log)("index", `Root: ${resolvedRoot}`);
122
189
  let lock = null;
123
190
  const vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
124
191
  const treatAsEmptyCache = reset && dryRun;
@@ -135,11 +202,15 @@ function initialSync(options) {
135
202
  if (!dryRun) {
136
203
  // Scope checks to this project's paths only
137
204
  const projectKeys = yield metaCache.getKeysWithPrefix(rootPrefix);
205
+ (0, logger_1.log)("index", `Cached files: ${projectKeys.size}`);
138
206
  const modelChanged = (0, index_config_1.checkModelMismatch)(paths.configPath);
139
207
  if (reset || modelChanged) {
140
208
  if (modelChanged) {
141
209
  const stored = (0, index_config_1.readIndexConfig)(paths.configPath);
142
- console.warn(`[syncer] Embedding model changed: ${stored === null || stored === void 0 ? void 0 : stored.embedModel} → ${config_1.MODEL_IDS.embed}. Forcing full re-index.`);
210
+ (0, logger_1.log)("index", `Reset: model changed (${stored === null || stored === void 0 ? void 0 : stored.embedModel} → ${config_1.MODEL_IDS.embed})`);
211
+ }
212
+ else {
213
+ (0, logger_1.log)("index", "Reset: --reset flag");
143
214
  }
144
215
  // Only delete this project's data from the centralized store
145
216
  yield vectorDb.deletePathsWithPrefix(rootPrefix);
@@ -166,6 +237,9 @@ function initialSync(options) {
166
237
  let processed = 0;
167
238
  let indexed = 0;
168
239
  let failedFiles = 0;
240
+ let cacheHits = 0;
241
+ let walkedFiles = 0;
242
+ const walkTimer = (0, logger_1.timer)("index", "Walk");
169
243
  let shouldSkipCleanup = false;
170
244
  let flushError;
171
245
  let flushPromise = null;
@@ -262,6 +336,7 @@ function initialSync(options) {
262
336
  }
263
337
  if (!(0, file_utils_1.isIndexableFile)(absPath))
264
338
  continue;
339
+ walkedFiles++;
265
340
  yield schedule(() => __awaiter(this, void 0, void 0, function* () {
266
341
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
267
342
  shouldSkipCleanup = true;
@@ -279,11 +354,14 @@ function initialSync(options) {
279
354
  if (cached &&
280
355
  cached.mtimeMs === stats.mtimeMs &&
281
356
  cached.size === stats.size) {
357
+ cacheHits++;
358
+ (0, logger_1.debug)("index", `SKIP ${relPath} (cached)`);
282
359
  processed += 1;
283
360
  seenPaths.add(absPath);
284
361
  markProgress(relPath);
285
362
  return;
286
363
  }
364
+ (0, logger_1.debug)("index", `EMBED ${relPath}`);
287
365
  const result = yield processFileWithRetry(absPath);
288
366
  const metaEntry = {
289
367
  hash: result.hash,
@@ -362,6 +440,9 @@ function initialSync(options) {
362
440
  finally { if (e_1) throw e_1.error; }
363
441
  }
364
442
  yield Promise.allSettled(activeTasks);
443
+ walkTimer();
444
+ (0, logger_1.log)("index", `Walk: ${walkedFiles} files`);
445
+ (0, logger_1.log)("index", `Embed: ${indexed} new, ${cacheHits} cached, ${failedFiles} failed`);
365
446
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
366
447
  shouldSkipCleanup = true;
367
448
  }
@@ -372,6 +453,7 @@ function initialSync(options) {
372
453
  : new Error(String(flushError));
373
454
  }
374
455
  if (!dryRun) {
456
+ const ftsTimer = (0, logger_1.timer)("index", "FTS");
375
457
  onProgress === null || onProgress === void 0 ? void 0 : onProgress({
376
458
  processed,
377
459
  indexed,
@@ -379,15 +461,38 @@ function initialSync(options) {
379
461
  filePath: "Creating FTS index...",
380
462
  });
381
463
  yield vectorDb.createFTSIndex();
464
+ ftsTimer();
382
465
  }
383
466
  // Stale cleanup: only remove paths scoped to this project's root
384
467
  const stale = Array.from(cachedPaths).filter((p) => !seenPaths.has(p));
385
468
  if (!dryRun && stale.length > 0 && !shouldSkipCleanup) {
469
+ (0, logger_1.log)("index", `Stale cleanup: ${stale.length} paths`);
386
470
  yield vectorDb.deletePaths(stale);
387
471
  stale.forEach((p) => {
388
472
  metaCache.delete(p);
389
473
  });
390
474
  }
475
+ // --- Summary post-processing (sequential, single process) ---
476
+ if (!dryRun && indexed > 0) {
477
+ const sumTimer = (0, logger_1.timer)("index", "Summarize");
478
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress({
479
+ processed,
480
+ indexed,
481
+ total,
482
+ filePath: "Generating summaries...",
483
+ });
484
+ const summarized = yield generateSummaries(vectorDb, rootPrefix, (count, chunkTotal) => {
485
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress({
486
+ processed: count,
487
+ indexed,
488
+ total: chunkTotal,
489
+ filePath: `Summarizing... (${count}/${chunkTotal})`,
490
+ });
491
+ });
492
+ sumTimer();
493
+ (0, logger_1.log)("index", `Summarize: ${summarized} chunks`);
494
+ }
495
+ syncTimer();
391
496
  // Write model config so future runs can detect model changes
392
497
  if (!dryRun) {
393
498
  (0, index_config_1.writeIndexConfig)(paths.configPath);
@@ -48,8 +48,10 @@ const fs = __importStar(require("node:fs"));
48
48
  const path = __importStar(require("node:path"));
49
49
  const chokidar_1 = require("chokidar");
50
50
  const file_utils_1 = require("../utils/file-utils");
51
+ const logger_1 = require("../utils/logger");
51
52
  const lock_1 = require("../utils/lock");
52
53
  const pool_1 = require("../workers/pool");
54
+ const llm_client_1 = require("../workers/summarize/llm-client");
53
55
  // Chokidar ignored — must exclude heavy directories to keep FD count low.
54
56
  // On macOS, chokidar uses FSEvents (single FD) but falls back to fs.watch()
55
57
  // (one FD per directory) if FSEvents isn't available or for some subdirs.
@@ -101,8 +103,10 @@ function startWatcher(opts) {
101
103
  processing = true;
102
104
  const batch = new Map(pending);
103
105
  pending.clear();
106
+ (0, logger_1.log)("watch", `Processing ${batch.size} changed files`);
104
107
  const start = Date.now();
105
108
  let reindexed = 0;
109
+ const changedIds = [];
106
110
  try {
107
111
  const lock = yield (0, lock_1.acquireWriterLockWithRetry)(dataDir, {
108
112
  maxRetries: 3,
@@ -115,10 +119,9 @@ function startWatcher(opts) {
115
119
  const metaUpdates = new Map();
116
120
  const metaDeletes = [];
117
121
  for (const [absPath, event] of batch) {
118
- const relPath = path.relative(projectRoot, absPath);
119
122
  if (event === "unlink") {
120
- deletes.push(relPath);
121
- metaDeletes.push(relPath);
123
+ deletes.push(absPath);
124
+ metaDeletes.push(absPath);
122
125
  reindexed++;
123
126
  continue;
124
127
  }
@@ -128,9 +131,9 @@ function startWatcher(opts) {
128
131
  if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size))
129
132
  continue;
130
133
  // Check if content actually changed via hash
131
- const cached = metaCache.get(relPath);
134
+ const cached = metaCache.get(absPath);
132
135
  const result = yield pool.processFile({
133
- path: relPath,
136
+ path: absPath,
134
137
  absolutePath: absPath,
135
138
  });
136
139
  const metaEntry = {
@@ -139,33 +142,36 @@ function startWatcher(opts) {
139
142
  size: result.size,
140
143
  };
141
144
  if (cached && cached.hash === result.hash) {
142
- // Content unchanged (mtime changed but hash same) — just update meta
143
- metaUpdates.set(relPath, metaEntry);
145
+ metaUpdates.set(absPath, metaEntry);
144
146
  continue;
145
147
  }
146
148
  if (result.shouldDelete) {
147
- deletes.push(relPath);
148
- metaUpdates.set(relPath, metaEntry);
149
+ deletes.push(absPath);
150
+ metaUpdates.set(absPath, metaEntry);
149
151
  reindexed++;
150
152
  continue;
151
153
  }
152
154
  // Delete old vectors, insert new
153
- deletes.push(relPath);
155
+ deletes.push(absPath);
154
156
  if (result.vectors.length > 0) {
155
157
  vectors.push(...result.vectors);
158
+ // Track IDs of new vectors for summarization
159
+ for (const v of result.vectors) {
160
+ changedIds.push(v.id);
161
+ }
156
162
  }
157
- metaUpdates.set(relPath, metaEntry);
163
+ metaUpdates.set(absPath, metaEntry);
158
164
  reindexed++;
159
165
  }
160
166
  catch (err) {
161
167
  const code = err === null || err === void 0 ? void 0 : err.code;
162
168
  if (code === "ENOENT") {
163
- deletes.push(relPath);
164
- metaDeletes.push(relPath);
169
+ deletes.push(absPath);
170
+ metaDeletes.push(absPath);
165
171
  reindexed++;
166
172
  }
167
173
  else {
168
- console.error(`[watch] Failed to process ${relPath}:`, err);
174
+ console.error(`[watch] Failed to process ${absPath}:`, err);
169
175
  }
170
176
  }
171
177
  }
@@ -187,6 +193,39 @@ function startWatcher(opts) {
187
193
  finally {
188
194
  yield lock.release();
189
195
  }
196
+ // Summarize new/changed chunks outside the lock (sequential, no GPU contention)
197
+ if (changedIds.length > 0) {
198
+ try {
199
+ const table = yield vectorDb.ensureTable();
200
+ for (const id of changedIds) {
201
+ const escaped = id.replace(/'/g, "''");
202
+ const rows = yield table
203
+ .query()
204
+ .select(["id", "path", "content"])
205
+ .where(`id = '${escaped}'`)
206
+ .limit(1)
207
+ .toArray();
208
+ if (rows.length === 0)
209
+ continue;
210
+ const r = rows[0];
211
+ const lang = path.extname(String(r.path || "")).replace(/^\./, "") ||
212
+ "unknown";
213
+ const summaries = yield (0, llm_client_1.summarizeChunks)([
214
+ {
215
+ code: String(r.content || ""),
216
+ language: lang,
217
+ file: String(r.path || ""),
218
+ },
219
+ ]);
220
+ if (summaries === null || summaries === void 0 ? void 0 : summaries[0]) {
221
+ yield vectorDb.updateRows([id], "summary", [summaries[0]]);
222
+ }
223
+ }
224
+ }
225
+ catch (_a) {
226
+ // Summarizer unavailable — skip silently
227
+ }
228
+ }
190
229
  if (reindexed > 0) {
191
230
  const duration = Date.now() - start;
192
231
  onReindex === null || onReindex === void 0 ? void 0 : onReindex(reindexed, duration);
@@ -47,6 +47,7 @@ const fs = __importStar(require("node:fs"));
47
47
  const lancedb = __importStar(require("@lancedb/lancedb"));
48
48
  const apache_arrow_1 = require("apache-arrow");
49
49
  const config_1 = require("../../config");
50
+ const logger_1 = require("../utils/logger");
50
51
  const cleanup_1 = require("../utils/cleanup");
51
52
  const TABLE_NAME = "chunks";
52
53
  class VectorDB {
@@ -151,6 +152,7 @@ class VectorDB {
151
152
  return table;
152
153
  }
153
154
  catch (_err) {
155
+ (0, logger_1.log)("db", `Creating table (${this.vectorDim}d)`);
154
156
  const schema = this.buildSchema();
155
157
  const table = yield db.createTable(TABLE_NAME, [this.seedRow()], {
156
158
  schema,
@@ -314,6 +316,21 @@ class VectorDB {
314
316
  }
315
317
  });
316
318
  }
319
+ updateRows(ids, field, values) {
320
+ return __awaiter(this, void 0, void 0, function* () {
321
+ var _a;
322
+ if (!ids.length)
323
+ return;
324
+ const table = yield this.ensureTable();
325
+ for (let i = 0; i < ids.length; i++) {
326
+ const escaped = ids[i].replace(/'/g, "''");
327
+ yield table.update({
328
+ where: `id = '${escaped}'`,
329
+ values: { [field]: (_a = values[i]) !== null && _a !== void 0 ? _a : "" },
330
+ });
331
+ }
332
+ });
333
+ }
317
334
  deletePathsWithPrefix(prefix) {
318
335
  return __awaiter(this, void 0, void 0, function* () {
319
336
  const table = yield this.ensureTable();
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.log = log;
4
+ exports.debug = debug;
5
+ exports.timer = timer;
6
+ const VERBOSE = process.env.GMAX_DEBUG === "1" || process.env.GMAX_VERBOSE === "1";
7
+ function log(tag, msg) {
8
+ process.stderr.write(`[${tag}] ${msg}\n`);
9
+ }
10
+ function debug(tag, msg) {
11
+ if (VERBOSE)
12
+ process.stderr.write(`[${tag}] ${msg}\n`);
13
+ }
14
+ function timer(tag, label) {
15
+ const start = Date.now();
16
+ return () => {
17
+ const ms = Date.now() - start;
18
+ const elapsed = ms > 60000
19
+ ? `${(ms / 60000).toFixed(1)}min`
20
+ : `${(ms / 1000).toFixed(1)}s`;
21
+ log(tag, `${label}: ${elapsed}`);
22
+ };
23
+ }
@@ -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 : {
@@ -51,6 +51,7 @@ exports.isWorkerPoolInitialized = isWorkerPoolInitialized;
51
51
  * to ensure the ONNX Runtime segfaults do not crash the main process.
52
52
  */
53
53
  const childProcess = __importStar(require("node:child_process"));
54
+ const logger_1 = require("../utils/logger");
54
55
  const fs = __importStar(require("node:fs"));
55
56
  const path = __importStar(require("node:path"));
56
57
  const config_1 = require("../../config");
@@ -149,6 +150,7 @@ class WorkerPool {
149
150
  task.reject(new Error(`Worker exited unexpectedly${code ? ` (code ${code})` : ""}${signal ? ` signal ${signal}` : ""}`));
150
151
  this.completeTask(task, null);
151
152
  }
153
+ (0, logger_1.log)("pool", `Worker PID:${worker.child.pid} exited (code:${code} signal:${signal})`);
152
154
  this.workers = this.workers.filter((w) => w !== worker);
153
155
  if (!this.destroyed) {
154
156
  this.spawnWorker();
@@ -157,6 +159,7 @@ class WorkerPool {
157
159
  }
158
160
  spawnWorker() {
159
161
  const worker = new ProcessWorker(this.modulePath, this.execArgv);
162
+ (0, logger_1.debug)("pool", `Spawned worker PID:${worker.child.pid}`);
160
163
  const onMessage = (msg) => {
161
164
  const task = this.tasks.get(msg.id);
162
165
  if (!task)
@@ -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,19 @@ 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
+ process.stderr.write("[summarizer] Request failed or server unavailable\n");
104
+ return null;
146
105
  }
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;
106
+ return data.summaries;
160
107
  });
161
108
  }
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.1",
3
+ "version": "0.5.1",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
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.