grepmax 0.13.6 → 0.14.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.
package/README.md CHANGED
@@ -135,6 +135,9 @@ Plugins auto-update when you run `npm install -g grepmax@latest` — no need to
135
135
  | `impact_analysis` | Dependents + affected tests for a symbol or file. |
136
136
  | `find_similar` | Vector similarity search. |
137
137
  | `build_context` | Token-budgeted topic summary. |
138
+ | `investigate` | Agentic codebase Q&A using local LLM + gmax tools. |
139
+ | `review_commit` | Review a git commit for bugs, security issues, and breaking changes. |
140
+ | `review_report` | Get accumulated code review findings for the current project. |
138
141
 
139
142
  ## Search Options
140
143
 
@@ -174,6 +177,61 @@ gmax status # See all projects + watcher status
174
177
 
175
178
  The daemon auto-starts when you run `gmax add`, `gmax index`, `gmax remove`, or `gmax summarize`. It shuts down after 30 minutes of inactivity.
176
179
 
180
+ ## Local LLM (optional)
181
+
182
+ gmax can use a local LLM (via llama-server) for agentic codebase investigation. This is entirely opt-in and disabled by default — gmax works fine without it.
183
+
184
+ ```bash
185
+ gmax llm on # Enable LLM features (persists to config)
186
+ gmax llm start # Start llama-server (auto-starts daemon too)
187
+ gmax llm status # Check server status
188
+ gmax llm stop # Stop llama-server
189
+ gmax llm off # Disable LLM + stop server
190
+ ```
191
+
192
+ ### Investigate
193
+
194
+ Ask questions about your codebase — the LLM autonomously uses gmax tools (search, trace, peek, impact, related) to gather evidence and synthesize an answer.
195
+
196
+ ```bash
197
+ gmax investigate "how does authentication work?"
198
+ gmax investigate "what would break if I changed VectorDB?" -v
199
+ gmax investigate "where are API routes defined?" --root ~/project
200
+ ```
201
+
202
+ ### Review
203
+
204
+ Automatic code review on git commits. Extracts the diff, gathers codebase context (callers, dependents, related files), and prompts the LLM for structured findings.
205
+
206
+ ```bash
207
+ gmax review # Review HEAD
208
+ gmax review --commit abc1234 # Review specific commit
209
+ gmax review --commit HEAD~3 -v # Verbose — shows context gathering + LLM progress
210
+ gmax review report # Show accumulated findings
211
+ gmax review report --json # Raw JSON output
212
+ gmax review clear # Clear report
213
+ ```
214
+
215
+ #### Post-commit hook
216
+
217
+ Install a git hook that automatically reviews every commit in the background via the daemon:
218
+
219
+ ```bash
220
+ gmax review install # Install in current repo
221
+ gmax review install ~/other-repo # Install in another repo
222
+ ```
223
+
224
+ The hook sends an IPC message to the daemon and returns instantly — it never blocks `git commit`. Findings accumulate in the report.
225
+
226
+ ### LLM Configuration
227
+
228
+ | Variable | Description | Default |
229
+ | --- | --- | --- |
230
+ | `GMAX_LLM_MODEL` | Path to GGUF model file | (none) |
231
+ | `GMAX_LLM_BINARY` | llama-server binary | `llama-server` |
232
+ | `GMAX_LLM_PORT` | Server port | `8079` |
233
+ | `GMAX_LLM_IDLE_TIMEOUT` | Minutes before auto-stop | `30` |
234
+
177
235
  ## Architecture
178
236
 
179
237
  All data lives in `~/.gmax/`:
@@ -305,6 +305,28 @@ const TOOLS = [
305
305
  required: ["question"],
306
306
  },
307
307
  },
308
+ {
309
+ name: "review_commit",
310
+ description: "Review a git commit for bugs, breaking changes, and security issues using local LLM + codebase context. Returns structured findings. Requires LLM to be enabled (gmax llm on).",
311
+ inputSchema: {
312
+ type: "object",
313
+ properties: {
314
+ commit: { type: "string", description: "Git ref to review (default: HEAD)" },
315
+ },
316
+ required: [],
317
+ },
318
+ },
319
+ {
320
+ name: "review_report",
321
+ description: "Get the accumulated code review report for the current project. Returns findings from all reviewed commits.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ json: { type: "boolean", description: "Return raw JSON instead of text (default: false)" },
326
+ },
327
+ required: [],
328
+ },
329
+ },
308
330
  ];
309
331
  // ---------------------------------------------------------------------------
310
332
  // Helpers
@@ -1833,7 +1855,7 @@ exports.mcp = new commander_1.Command("mcp")
1833
1855
  return { tools: TOOLS };
1834
1856
  }));
1835
1857
  server.setRequestHandler(types_js_1.CallToolRequestSchema, (request) => __awaiter(void 0, void 0, void 0, function* () {
1836
- var _a, _b, _c, _d, _e, _f;
1858
+ var _a, _b, _c, _d, _e, _f, _g;
1837
1859
  const { name, arguments: args } = request.params;
1838
1860
  const toolArgs = (args !== null && args !== void 0 ? args : {});
1839
1861
  const startMs = Date.now();
@@ -1919,26 +1941,77 @@ exports.mcp = new commander_1.Command("mcp")
1919
1941
  }
1920
1942
  break;
1921
1943
  }
1944
+ case "review_commit": {
1945
+ const commitRef = String(toolArgs.commit || "HEAD");
1946
+ try {
1947
+ const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
1948
+ if (yield isDaemonRunning()) {
1949
+ const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
1950
+ if (!llmResp.ok) {
1951
+ result = err(`LLM server not available: ${llmResp.error}. Run \`gmax llm on && gmax llm start\`.`);
1952
+ break;
1953
+ }
1954
+ }
1955
+ else {
1956
+ result = err("LLM server not available. Run `gmax llm on && gmax llm start`.");
1957
+ break;
1958
+ }
1959
+ const { reviewCommit } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/review")));
1960
+ const rev = yield reviewCommit({ commitRef, projectRoot });
1961
+ if (rev.clean) {
1962
+ result = ok(`Clean commit (${rev.commit}) — no issues found in ${rev.duration}s.`);
1963
+ }
1964
+ else {
1965
+ const { readReport } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
1966
+ const report = readReport(projectRoot);
1967
+ const entry = report === null || report === void 0 ? void 0 : report.reviews.find((r) => r.commit === rev.commit);
1968
+ result = ok(JSON.stringify({ commit: rev.commit, findings: (_a = entry === null || entry === void 0 ? void 0 : entry.findings) !== null && _a !== void 0 ? _a : [], duration: rev.duration }, null, 2));
1969
+ }
1970
+ }
1971
+ catch (e) {
1972
+ result = err(`Review failed: ${e instanceof Error ? e.message : String(e)}`);
1973
+ }
1974
+ break;
1975
+ }
1976
+ case "review_report": {
1977
+ try {
1978
+ const { readReport, formatReportText } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
1979
+ const report = readReport(projectRoot);
1980
+ if (!report || report.reviews.length === 0) {
1981
+ result = ok("No review findings yet.");
1982
+ }
1983
+ else if (toolArgs.json) {
1984
+ result = ok(JSON.stringify(report, null, 2));
1985
+ }
1986
+ else {
1987
+ result = ok(formatReportText(report));
1988
+ }
1989
+ }
1990
+ catch (e) {
1991
+ result = err(`Report failed: ${e instanceof Error ? e.message : String(e)}`);
1992
+ }
1993
+ break;
1994
+ }
1922
1995
  default:
1923
1996
  return err(`Unknown tool: ${name}`);
1924
1997
  }
1925
1998
  // Best-effort query logging
1926
1999
  try {
1927
2000
  const { logQuery } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/query-log")));
1928
- const text = (_c = (_b = (_a = result.content) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : "";
2001
+ const text = (_d = (_c = (_b = result.content) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.text) !== null && _d !== void 0 ? _d : "";
1929
2002
  const resultLines = text.split("\n").filter((l) => l.trim()).length;
1930
2003
  logQuery({
1931
2004
  ts: new Date().toISOString(),
1932
2005
  source: "mcp",
1933
2006
  tool: name,
1934
- query: String((_f = (_e = (_d = toolArgs.query) !== null && _d !== void 0 ? _d : toolArgs.symbol) !== null && _e !== void 0 ? _e : toolArgs.target) !== null && _f !== void 0 ? _f : ""),
2007
+ query: String((_g = (_f = (_e = toolArgs.query) !== null && _e !== void 0 ? _e : toolArgs.symbol) !== null && _f !== void 0 ? _f : toolArgs.target) !== null && _g !== void 0 ? _g : ""),
1935
2008
  project: projectRoot,
1936
2009
  results: resultLines,
1937
2010
  ms: Date.now() - startMs,
1938
2011
  error: result.isError ? text.slice(0, 200) : undefined,
1939
2012
  });
1940
2013
  }
1941
- catch (_g) { }
2014
+ catch (_h) { }
1942
2015
  return result;
1943
2016
  }));
1944
2017
  yield server.connect(transport);
@@ -0,0 +1,237 @@
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.review = void 0;
46
+ const node_child_process_1 = require("node:child_process");
47
+ const fs = __importStar(require("node:fs"));
48
+ const path = __importStar(require("node:path"));
49
+ const commander_1 = require("commander");
50
+ const exit_1 = require("../lib/utils/exit");
51
+ const project_root_1 = require("../lib/utils/project-root");
52
+ exports.review = new commander_1.Command("review")
53
+ .description("Review code changes using local LLM + codebase context")
54
+ .option("--commit <ref>", "Commit to review", "HEAD")
55
+ .option("--root <dir>", "Project root directory")
56
+ .option("--background", "Run review asynchronously via daemon", false)
57
+ .option("-v, --verbose", "Print progress to stderr", false)
58
+ .addHelpText("after", `
59
+ Examples:
60
+ gmax review Review HEAD
61
+ gmax review --commit abc1234 Review specific commit
62
+ gmax review --background Run async via daemon
63
+
64
+ Subcommands:
65
+ gmax review report [--json] Show accumulated findings
66
+ gmax review clear Clear report
67
+ gmax review install [DIR] Install post-commit hook
68
+ `)
69
+ .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
70
+ var _a;
71
+ try {
72
+ const root = opts.root ? path.resolve(opts.root) : process.cwd();
73
+ const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
74
+ const commitRef = opts.commit;
75
+ if (opts.background) {
76
+ // Fire-and-forget via daemon
77
+ const { ensureDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
78
+ if (!(yield ensureDaemonRunning())) {
79
+ console.error("Failed to start daemon");
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ const resp = yield sendDaemonCommand({ cmd: "review", root: projectRoot, commitRef }, { timeoutMs: 5000 });
84
+ if (!resp.ok) {
85
+ console.error(`Review failed: ${resp.error}`);
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ console.log(`Review queued for ${commitRef}`);
90
+ return;
91
+ }
92
+ // Foreground: ensure LLM server is running
93
+ const { ensureDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
94
+ if (!(yield ensureDaemonRunning())) {
95
+ console.error("Failed to start daemon");
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
100
+ if (!llmResp.ok) {
101
+ console.error(`LLM server error: ${llmResp.error}`);
102
+ console.error("Run `gmax llm on` to enable the LLM server.");
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ const { reviewCommit } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/review")));
107
+ const result = yield reviewCommit({
108
+ commitRef,
109
+ projectRoot,
110
+ verbose: opts.verbose,
111
+ });
112
+ if (result.clean) {
113
+ console.log(`${result.commit} — clean (${result.duration}s)`);
114
+ }
115
+ else {
116
+ console.log(`${result.commit} — ${result.findingCount} finding(s) (${result.duration}s)`);
117
+ console.log("Run `gmax review report` to see details.");
118
+ }
119
+ }
120
+ catch (err) {
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ console.error(`Review failed: ${msg}`);
123
+ process.exitCode = 1;
124
+ }
125
+ finally {
126
+ yield (0, exit_1.gracefulExit)();
127
+ }
128
+ }));
129
+ // --- Subcommands ---
130
+ exports.review
131
+ .command("report")
132
+ .description("Show accumulated review findings")
133
+ .option("--json", "Output raw JSON", false)
134
+ .option("--root <dir>", "Project root directory")
135
+ .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
136
+ var _a;
137
+ try {
138
+ const root = opts.root ? path.resolve(opts.root) : process.cwd();
139
+ const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
140
+ const { readReport, formatReportText } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
141
+ const report = readReport(projectRoot);
142
+ if (!report || report.reviews.length === 0) {
143
+ console.log("No review findings yet.");
144
+ return;
145
+ }
146
+ if (opts.json) {
147
+ console.log(JSON.stringify(report, null, 2));
148
+ }
149
+ else {
150
+ console.log(formatReportText(report));
151
+ }
152
+ }
153
+ catch (err) {
154
+ const msg = err instanceof Error ? err.message : String(err);
155
+ console.error(`Report failed: ${msg}`);
156
+ process.exitCode = 1;
157
+ }
158
+ finally {
159
+ yield (0, exit_1.gracefulExit)();
160
+ }
161
+ }));
162
+ exports.review
163
+ .command("clear")
164
+ .description("Clear the review report")
165
+ .option("--root <dir>", "Project root directory")
166
+ .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
167
+ var _a;
168
+ try {
169
+ const root = opts.root ? path.resolve(opts.root) : process.cwd();
170
+ const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
171
+ const { clearReport } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
172
+ clearReport(projectRoot);
173
+ console.log("Report cleared.");
174
+ }
175
+ finally {
176
+ yield (0, exit_1.gracefulExit)();
177
+ }
178
+ }));
179
+ exports.review
180
+ .command("install [dir]")
181
+ .description("Install post-commit hook for automatic review")
182
+ .action((dir) => __awaiter(void 0, void 0, void 0, function* () {
183
+ try {
184
+ let targetDir;
185
+ if (dir) {
186
+ targetDir = path.resolve(dir);
187
+ }
188
+ else {
189
+ try {
190
+ targetDir = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
191
+ encoding: "utf-8",
192
+ }).trim();
193
+ }
194
+ catch (_a) {
195
+ console.error("Not in a git repo and no directory specified.");
196
+ process.exitCode = 1;
197
+ return;
198
+ }
199
+ }
200
+ const hooksDir = path.join(targetDir, ".git", "hooks");
201
+ if (!fs.existsSync(hooksDir)) {
202
+ console.error(`Not a git repo: ${targetDir}`);
203
+ process.exitCode = 1;
204
+ return;
205
+ }
206
+ const hookFile = path.join(hooksDir, "post-commit");
207
+ // Backup existing hook if it doesn't mention gmax
208
+ if (fs.existsSync(hookFile)) {
209
+ const existing = fs.readFileSync(hookFile, "utf-8");
210
+ if (!existing.includes("gmax review")) {
211
+ fs.copyFileSync(hookFile, `${hookFile}.gmax-backup`);
212
+ console.log("Backed up existing post-commit hook.");
213
+ }
214
+ }
215
+ // Resolve gmax binary path
216
+ let gmaxBin = "gmax";
217
+ try {
218
+ gmaxBin = (0, node_child_process_1.execFileSync)("which", ["gmax"], { encoding: "utf-8" }).trim();
219
+ }
220
+ catch (_b) { }
221
+ const hookContent = `#!/usr/bin/env bash
222
+ # gmax review — async code review on commit
223
+ # Always exits 0 to never block git
224
+ "${gmaxBin}" review --commit HEAD --background --root "${targetDir}" || true
225
+ `;
226
+ fs.writeFileSync(hookFile, hookContent, { mode: 0o755 });
227
+ console.log(`Installed post-commit hook in ${targetDir}`);
228
+ }
229
+ catch (err) {
230
+ const msg = err instanceof Error ? err.message : String(err);
231
+ console.error(`Install failed: ${msg}`);
232
+ process.exitCode = 1;
233
+ }
234
+ finally {
235
+ yield (0, exit_1.gracefulExit)();
236
+ }
237
+ }));
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ const peek_1 = require("./commands/peek");
57
57
  const project_1 = require("./commands/project");
58
58
  const recent_1 = require("./commands/recent");
59
59
  const related_1 = require("./commands/related");
60
+ const review_1 = require("./commands/review");
60
61
  const opencode_1 = require("./commands/opencode");
61
62
  const plugin_1 = require("./commands/plugin");
62
63
  const remove_1 = require("./commands/remove");
@@ -114,6 +115,7 @@ commander_1.program.addCommand(mcp_1.mcp);
114
115
  commander_1.program.addCommand(summarize_1.summarize);
115
116
  commander_1.program.addCommand(llm_1.llm);
116
117
  commander_1.program.addCommand(investigate_1.investigateCmd);
118
+ commander_1.program.addCommand(review_1.review);
117
119
  // Setup & diagnostics
118
120
  commander_1.program.addCommand(setup_1.setup);
119
121
  commander_1.program.addCommand(config_1.config);
@@ -41,6 +41,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
41
41
  step((generator = generator.apply(thisArg, _arguments || [])).next());
42
42
  });
43
43
  };
44
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
45
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
46
+ var m = o[Symbol.asyncIterator], i;
47
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
48
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
49
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
50
+ };
44
51
  var __importDefault = (this && this.__importDefault) || function (mod) {
45
52
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
53
  };
@@ -131,8 +138,9 @@ class Daemon {
131
138
  // 7. Register daemon (only after resources are open)
132
139
  (0, watcher_store_1.registerDaemon)(process.pid);
133
140
  // 7. Subscribe to all registered projects (skip missing directories)
134
- const projects = (0, project_registry_1.listProjects)().filter((p) => p.status === "indexed");
135
- for (const p of projects) {
141
+ const allProjects = (0, project_registry_1.listProjects)();
142
+ const indexed = allProjects.filter((p) => p.status === "indexed");
143
+ for (const p of indexed) {
136
144
  if (!fs.existsSync(p.root)) {
137
145
  console.log(`[daemon] Skipping ${path.basename(p.root)} — directory not found`);
138
146
  continue;
@@ -144,6 +152,13 @@ class Daemon {
144
152
  console.error(`[daemon] Failed to watch ${path.basename(p.root)}:`, err);
145
153
  }
146
154
  }
155
+ // 7b. Index pending projects in the background
156
+ const pending = allProjects.filter((p) => p.status === "pending" && fs.existsSync(p.root));
157
+ for (const p of pending) {
158
+ this.indexPendingProject(p.root).catch((err) => {
159
+ console.error(`[daemon] Failed to index pending ${path.basename(p.root)}:`, err);
160
+ });
161
+ }
147
162
  // 8. Heartbeat
148
163
  this.heartbeatInterval = setInterval(() => {
149
164
  (0, watcher_store_1.heartbeat)(process.pid);
@@ -265,10 +280,88 @@ class Daemon {
265
280
  status: "watching",
266
281
  lastHeartbeat: Date.now(),
267
282
  });
283
+ // Catchup scan — find files changed while daemon was offline
284
+ this.catchupScan(root, processor).catch((err) => {
285
+ console.error(`[daemon:${path.basename(root)}] Catchup scan failed:`, err);
286
+ });
268
287
  this.pendingOps.delete(root);
269
288
  console.log(`[daemon] Watching ${root}`);
270
289
  });
271
290
  }
291
+ catchupScan(root, processor) {
292
+ return __awaiter(this, void 0, void 0, function* () {
293
+ var _a, e_1, _b, _c;
294
+ const { walk } = yield Promise.resolve().then(() => __importStar(require("../index/walker")));
295
+ const { INDEXABLE_EXTENSIONS } = yield Promise.resolve().then(() => __importStar(require("../../config")));
296
+ const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
297
+ let queued = 0;
298
+ try {
299
+ for (var _d = true, _e = __asyncValues(walk(root, {
300
+ additionalPatterns: ["**/.git/**", "**/.gmax/**", "**/.osgrep/**"],
301
+ })), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
302
+ _c = _f.value;
303
+ _d = false;
304
+ const relPath = _c;
305
+ const absPath = path.join(root, relPath);
306
+ const ext = path.extname(absPath).toLowerCase();
307
+ const bn = path.basename(absPath).toLowerCase();
308
+ if (!INDEXABLE_EXTENSIONS.has(ext) && !INDEXABLE_EXTENSIONS.has(bn))
309
+ continue;
310
+ try {
311
+ const stats = yield fs.promises.stat(absPath);
312
+ const cached = this.metaCache.get(absPath);
313
+ if (!isFileCached(cached, stats)) {
314
+ processor.handleFileEvent("change", absPath);
315
+ queued++;
316
+ }
317
+ }
318
+ catch (_g) { }
319
+ }
320
+ }
321
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
322
+ finally {
323
+ try {
324
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
325
+ }
326
+ finally { if (e_1) throw e_1.error; }
327
+ }
328
+ if (queued > 0) {
329
+ console.log(`[daemon:${path.basename(root)}] Catchup: ${queued} file(s) changed while offline`);
330
+ }
331
+ });
332
+ }
333
+ indexPendingProject(root) {
334
+ return __awaiter(this, void 0, void 0, function* () {
335
+ yield this.withProjectLock(root, () => __awaiter(this, void 0, void 0, function* () {
336
+ var _a;
337
+ if (!this.vectorDb || !this.metaCache)
338
+ return;
339
+ console.log(`[daemon] Indexing pending project: ${path.basename(root)}`);
340
+ this.vectorDb.pauseMaintenanceLoop();
341
+ try {
342
+ const result = yield (0, syncer_1.initialSync)({
343
+ projectRoot: root,
344
+ vectorDb: this.vectorDb,
345
+ metaCache: this.metaCache,
346
+ onProgress: () => { this.resetActivity(); },
347
+ });
348
+ const proj = (0, project_registry_1.getProject)(root);
349
+ if (proj) {
350
+ (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { lastIndexed: new Date().toISOString(), chunkCount: result.indexed, status: "indexed" }));
351
+ }
352
+ yield this.watchProject(root);
353
+ console.log(`[daemon] Indexed ${path.basename(root)} (${result.total} files, ${result.indexed} chunks)`);
354
+ }
355
+ catch (err) {
356
+ const msg = err instanceof Error ? err.message : String(err);
357
+ console.error(`[daemon] indexPendingProject failed for ${path.basename(root)}: ${msg}`);
358
+ }
359
+ finally {
360
+ (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
361
+ }
362
+ }));
363
+ });
364
+ }
272
365
  unwatchProject(root) {
273
366
  return __awaiter(this, void 0, void 0, function* () {
274
367
  const processor = this.processors.get(root);
@@ -539,6 +632,24 @@ class Daemon {
539
632
  var _a;
540
633
  (_a = this.llmServer) === null || _a === void 0 ? void 0 : _a.touchIdle();
541
634
  }
635
+ reviewCommit(root, commitRef) {
636
+ return __awaiter(this, void 0, void 0, function* () {
637
+ this.resetActivity();
638
+ try {
639
+ if (!this.llmServer) {
640
+ console.log("[review] daemon not initialized, skipping");
641
+ return;
642
+ }
643
+ yield this.llmServer.ensure();
644
+ const { reviewCommit } = yield Promise.resolve().then(() => __importStar(require("../llm/review")));
645
+ const result = yield reviewCommit({ commitRef, projectRoot: root });
646
+ console.log(`[review] ${result.commit} — ${result.findingCount} finding(s) in ${result.duration}s`);
647
+ }
648
+ catch (err) {
649
+ console.error(`[review] failed: ${err instanceof Error ? err.message : String(err)}`);
650
+ }
651
+ });
652
+ }
542
653
  shutdown() {
543
654
  return __awaiter(this, void 0, void 0, function* () {
544
655
  var _a, _b, _c, _d;
@@ -102,6 +102,14 @@ function handleCommand(daemon, cmd, conn) {
102
102
  return null;
103
103
  }
104
104
  // --- LLM server management ---
105
+ case "review": {
106
+ const root = String(cmd.root || "");
107
+ const commitRef = String(cmd.commitRef || "HEAD");
108
+ if (!root)
109
+ return { ok: false, error: "missing root" };
110
+ setImmediate(() => daemon.reviewCommit(root, commitRef));
111
+ return { ok: true };
112
+ }
105
113
  case "llm-start":
106
114
  return yield daemon.llmStart();
107
115
  case "llm-stop":
@@ -53,6 +53,7 @@ const pool_1 = require("../workers/pool");
53
53
  const watcher_batch_1 = require("./watcher-batch");
54
54
  const DEBOUNCE_MS = 2000;
55
55
  const MAX_RETRIES = 5;
56
+ const MAX_BATCH_SIZE = 50;
56
57
  class ProjectBatchProcessor {
57
58
  constructor(opts) {
58
59
  this.pending = new Map();
@@ -112,12 +113,22 @@ class ProjectBatchProcessor {
112
113
  (0, logger_1.log)(this.wtag, `Batch timed out after ${this.batchTimeoutMs}ms, aborting`);
113
114
  batchAc.abort();
114
115
  }, this.batchTimeoutMs);
115
- const batch = new Map(this.pending);
116
- this.pending.clear();
116
+ const batch = new Map();
117
+ let taken = 0;
118
+ for (const [absPath, event] of this.pending) {
119
+ batch.set(absPath, event);
120
+ taken++;
121
+ if (taken >= MAX_BATCH_SIZE)
122
+ break;
123
+ }
124
+ for (const key of batch.keys()) {
125
+ this.pending.delete(key);
126
+ }
117
127
  const filenames = [...batch.keys()].map((p) => path.basename(p));
118
128
  (0, logger_1.log)(this.wtag, `Processing ${batch.size} changed files: ${filenames.join(", ")}`);
119
129
  const start = Date.now();
120
130
  let reindexed = 0;
131
+ let processed = 0;
121
132
  try {
122
133
  // No lock needed — daemon is the single writer to LanceDB/MetaCache
123
134
  const pool = (0, pool_1.getWorkerPool)();
@@ -130,6 +141,10 @@ class ProjectBatchProcessor {
130
141
  if (batchAc.signal.aborted)
131
142
  break;
132
143
  attempted.add(absPath);
144
+ processed++;
145
+ if (batch.size > 10 && (processed % 10 === 0 || processed === batch.size)) {
146
+ (0, logger_1.log)(this.wtag, `Progress: ${processed}/${batch.size} (${reindexed} reindexed)`);
147
+ }
133
148
  if (event === "unlink") {
134
149
  deletes.push(absPath);
135
150
  metaDeletes.push(absPath);
@@ -219,7 +234,8 @@ class ProjectBatchProcessor {
219
234
  if (reindexed > 0) {
220
235
  (_a = this.onReindex) === null || _a === void 0 ? void 0 : _a.call(this, reindexed, duration);
221
236
  }
222
- (0, logger_1.log)(this.wtag, `Batch complete: ${batch.size} files, ${reindexed} reindexed (${(duration / 1000).toFixed(1)}s)`);
237
+ const remaining = this.pending.size;
238
+ (0, logger_1.log)(this.wtag, `Batch complete: ${batch.size} files, ${reindexed} reindexed (${(duration / 1000).toFixed(1)}s)${remaining > 0 ? ` — ${remaining} remaining` : ""}`);
223
239
  for (const absPath of batch.keys()) {
224
240
  this.retryCount.delete(absPath);
225
241
  }