grepmax 0.13.5 → 0.14.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 +58 -0
- package/dist/commands/investigate.js +105 -0
- package/dist/commands/mcp.js +118 -4
- package/dist/commands/review.js +237 -0
- package/dist/index.js +4 -0
- package/dist/lib/daemon/daemon.js +113 -2
- package/dist/lib/daemon/ipc-handler.js +8 -0
- package/dist/lib/llm/diff.js +139 -0
- package/dist/lib/llm/investigate.js +212 -0
- package/dist/lib/llm/prompts.js +33 -0
- package/dist/lib/llm/report.js +141 -0
- package/dist/lib/llm/review.js +403 -0
- package/dist/lib/llm/tools.js +384 -0
- package/package.json +2 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/skills/grepmax/SKILL.md +9 -0
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/`:
|
|
@@ -0,0 +1,105 @@
|
|
|
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.investigateCmd = void 0;
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const commander_1 = require("commander");
|
|
48
|
+
const exit_1 = require("../lib/utils/exit");
|
|
49
|
+
const project_root_1 = require("../lib/utils/project-root");
|
|
50
|
+
exports.investigateCmd = new commander_1.Command("investigate")
|
|
51
|
+
.description("Ask a question about the codebase using local LLM + gmax tools")
|
|
52
|
+
.argument("<question>", "Natural language question about the codebase")
|
|
53
|
+
.option("--root <dir>", "Project root directory")
|
|
54
|
+
.option("--rounds <n>", "Max tool-call rounds (default 10)", "10")
|
|
55
|
+
.option("-v, --verbose", "Print tool calls and results to stderr", false)
|
|
56
|
+
.addHelpText("after", `
|
|
57
|
+
Examples:
|
|
58
|
+
gmax investigate "how does the search command work?"
|
|
59
|
+
gmax investigate "what would break if I changed VectorDB?" -v
|
|
60
|
+
gmax investigate "where is authentication handled?" --root ~/project
|
|
61
|
+
`)
|
|
62
|
+
.action((question, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
var _a;
|
|
64
|
+
try {
|
|
65
|
+
const root = opts.root ? path.resolve(opts.root) : process.cwd();
|
|
66
|
+
const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
|
|
67
|
+
const maxRounds = Math.min(Math.max(Number.parseInt(opts.rounds || "10", 10), 1), 20);
|
|
68
|
+
// Ensure LLM server is running
|
|
69
|
+
const { ensureDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
70
|
+
if (!(yield ensureDaemonRunning())) {
|
|
71
|
+
console.error("Failed to start daemon");
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
|
|
76
|
+
if (!llmResp.ok) {
|
|
77
|
+
console.error(`LLM server error: ${llmResp.error}`);
|
|
78
|
+
console.error("Run `gmax llm on` to enable the LLM server.");
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const { investigate } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/investigate")));
|
|
83
|
+
const result = yield investigate({
|
|
84
|
+
question,
|
|
85
|
+
projectRoot,
|
|
86
|
+
maxRounds,
|
|
87
|
+
verbose: opts.verbose,
|
|
88
|
+
});
|
|
89
|
+
console.log(result.answer);
|
|
90
|
+
if (opts.verbose) {
|
|
91
|
+
process.stderr.write("\n--- metrics ---\n");
|
|
92
|
+
process.stderr.write(`rounds: ${result.rounds}\n`);
|
|
93
|
+
process.stderr.write(`tool calls: ${result.toolCalls}\n`);
|
|
94
|
+
process.stderr.write(`wall time: ${(result.wallMs / 1000).toFixed(1)}s\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
+
console.error(`Investigate failed: ${msg}`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
yield (0, exit_1.gracefulExit)();
|
|
104
|
+
}
|
|
105
|
+
}));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -293,6 +293,40 @@ const TOOLS = [
|
|
|
293
293
|
required: ["topic"],
|
|
294
294
|
},
|
|
295
295
|
},
|
|
296
|
+
{
|
|
297
|
+
name: "investigate",
|
|
298
|
+
description: "Agentic codebase Q&A: a local LLM answers questions using search, trace, peek, impact, and related tools. Requires LLM to be enabled (gmax llm on).",
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: "object",
|
|
301
|
+
properties: {
|
|
302
|
+
question: { type: "string", description: "Natural language question about the codebase" },
|
|
303
|
+
max_rounds: { type: "number", description: "Max tool-call rounds (default 10)" },
|
|
304
|
+
},
|
|
305
|
+
required: ["question"],
|
|
306
|
+
},
|
|
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
|
+
},
|
|
296
330
|
];
|
|
297
331
|
// ---------------------------------------------------------------------------
|
|
298
332
|
// Helpers
|
|
@@ -1821,7 +1855,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1821
1855
|
return { tools: TOOLS };
|
|
1822
1856
|
}));
|
|
1823
1857
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, (request) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1824
|
-
var _a, _b, _c, _d, _e, _f;
|
|
1858
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1825
1859
|
const { name, arguments: args } = request.params;
|
|
1826
1860
|
const toolArgs = (args !== null && args !== void 0 ? args : {});
|
|
1827
1861
|
const startMs = Date.now();
|
|
@@ -1878,26 +1912,106 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1878
1912
|
case "build_context":
|
|
1879
1913
|
result = yield handleBuildContext(toolArgs);
|
|
1880
1914
|
break;
|
|
1915
|
+
case "investigate": {
|
|
1916
|
+
const question = String(toolArgs.question || "");
|
|
1917
|
+
if (!question) {
|
|
1918
|
+
result = err("Missing required parameter: question");
|
|
1919
|
+
break;
|
|
1920
|
+
}
|
|
1921
|
+
const maxRounds = Math.min(Math.max(Number(toolArgs.max_rounds) || 10, 1), 15);
|
|
1922
|
+
try {
|
|
1923
|
+
const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
1924
|
+
if (yield isDaemonRunning()) {
|
|
1925
|
+
const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
|
|
1926
|
+
if (!llmResp.ok) {
|
|
1927
|
+
result = err(`LLM server not available: ${llmResp.error}. Run \`gmax llm on && gmax llm start\`.`);
|
|
1928
|
+
break;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
else {
|
|
1932
|
+
result = err("LLM server not available. Run `gmax llm on && gmax llm start`.");
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
const { investigate } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/investigate")));
|
|
1936
|
+
const inv = yield investigate({ question, projectRoot, maxRounds });
|
|
1937
|
+
result = ok(inv.answer);
|
|
1938
|
+
}
|
|
1939
|
+
catch (e) {
|
|
1940
|
+
result = err(`Investigate failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1941
|
+
}
|
|
1942
|
+
break;
|
|
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
|
+
}
|
|
1881
1995
|
default:
|
|
1882
1996
|
return err(`Unknown tool: ${name}`);
|
|
1883
1997
|
}
|
|
1884
1998
|
// Best-effort query logging
|
|
1885
1999
|
try {
|
|
1886
2000
|
const { logQuery } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/query-log")));
|
|
1887
|
-
const text = (
|
|
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 : "";
|
|
1888
2002
|
const resultLines = text.split("\n").filter((l) => l.trim()).length;
|
|
1889
2003
|
logQuery({
|
|
1890
2004
|
ts: new Date().toISOString(),
|
|
1891
2005
|
source: "mcp",
|
|
1892
2006
|
tool: name,
|
|
1893
|
-
query: String((
|
|
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 : ""),
|
|
1894
2008
|
project: projectRoot,
|
|
1895
2009
|
results: resultLines,
|
|
1896
2010
|
ms: Date.now() - startMs,
|
|
1897
2011
|
error: result.isError ? text.slice(0, 200) : undefined,
|
|
1898
2012
|
});
|
|
1899
2013
|
}
|
|
1900
|
-
catch (
|
|
2014
|
+
catch (_h) { }
|
|
1901
2015
|
return result;
|
|
1902
2016
|
}));
|
|
1903
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
|
@@ -49,6 +49,7 @@ const extract_1 = require("./commands/extract");
|
|
|
49
49
|
const impact_1 = require("./commands/impact");
|
|
50
50
|
const droid_1 = require("./commands/droid");
|
|
51
51
|
const index_1 = require("./commands/index");
|
|
52
|
+
const investigate_1 = require("./commands/investigate");
|
|
52
53
|
const list_1 = require("./commands/list");
|
|
53
54
|
const llm_1 = require("./commands/llm");
|
|
54
55
|
const mcp_1 = require("./commands/mcp");
|
|
@@ -56,6 +57,7 @@ const peek_1 = require("./commands/peek");
|
|
|
56
57
|
const project_1 = require("./commands/project");
|
|
57
58
|
const recent_1 = require("./commands/recent");
|
|
58
59
|
const related_1 = require("./commands/related");
|
|
60
|
+
const review_1 = require("./commands/review");
|
|
59
61
|
const opencode_1 = require("./commands/opencode");
|
|
60
62
|
const plugin_1 = require("./commands/plugin");
|
|
61
63
|
const remove_1 = require("./commands/remove");
|
|
@@ -112,6 +114,8 @@ commander_1.program.addCommand(watch_1.watch);
|
|
|
112
114
|
commander_1.program.addCommand(mcp_1.mcp);
|
|
113
115
|
commander_1.program.addCommand(summarize_1.summarize);
|
|
114
116
|
commander_1.program.addCommand(llm_1.llm);
|
|
117
|
+
commander_1.program.addCommand(investigate_1.investigateCmd);
|
|
118
|
+
commander_1.program.addCommand(review_1.review);
|
|
115
119
|
// Setup & diagnostics
|
|
116
120
|
commander_1.program.addCommand(setup_1.setup);
|
|
117
121
|
commander_1.program.addCommand(config_1.config);
|