grepmax 0.13.5 → 0.13.6

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.
@@ -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
+ }));
@@ -293,6 +293,18 @@ 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
+ },
296
308
  ];
297
309
  // ---------------------------------------------------------------------------
298
310
  // Helpers
@@ -1878,6 +1890,35 @@ exports.mcp = new commander_1.Command("mcp")
1878
1890
  case "build_context":
1879
1891
  result = yield handleBuildContext(toolArgs);
1880
1892
  break;
1893
+ case "investigate": {
1894
+ const question = String(toolArgs.question || "");
1895
+ if (!question) {
1896
+ result = err("Missing required parameter: question");
1897
+ break;
1898
+ }
1899
+ const maxRounds = Math.min(Math.max(Number(toolArgs.max_rounds) || 10, 1), 15);
1900
+ try {
1901
+ const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
1902
+ if (yield isDaemonRunning()) {
1903
+ const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
1904
+ if (!llmResp.ok) {
1905
+ result = err(`LLM server not available: ${llmResp.error}. Run \`gmax llm on && gmax llm start\`.`);
1906
+ break;
1907
+ }
1908
+ }
1909
+ else {
1910
+ result = err("LLM server not available. Run `gmax llm on && gmax llm start`.");
1911
+ break;
1912
+ }
1913
+ const { investigate } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/investigate")));
1914
+ const inv = yield investigate({ question, projectRoot, maxRounds });
1915
+ result = ok(inv.answer);
1916
+ }
1917
+ catch (e) {
1918
+ result = err(`Investigate failed: ${e instanceof Error ? e.message : String(e)}`);
1919
+ }
1920
+ break;
1921
+ }
1881
1922
  default:
1882
1923
  return err(`Unknown tool: ${name}`);
1883
1924
  }
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");
@@ -112,6 +113,7 @@ commander_1.program.addCommand(watch_1.watch);
112
113
  commander_1.program.addCommand(mcp_1.mcp);
113
114
  commander_1.program.addCommand(summarize_1.summarize);
114
115
  commander_1.program.addCommand(llm_1.llm);
116
+ commander_1.program.addCommand(investigate_1.investigateCmd);
115
117
  // Setup & diagnostics
116
118
  commander_1.program.addCommand(setup_1.setup);
117
119
  commander_1.program.addCommand(config_1.config);
@@ -0,0 +1,212 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.investigate = investigate;
49
+ const path = __importStar(require("node:path"));
50
+ const openai_1 = __importDefault(require("openai"));
51
+ const graph_builder_1 = require("../graph/graph-builder");
52
+ const searcher_1 = require("../search/searcher");
53
+ const vector_db_1 = require("../store/vector-db");
54
+ const project_root_1 = require("../utils/project-root");
55
+ const config_1 = require("./config");
56
+ const prompts_1 = require("./prompts");
57
+ const tools_1 = require("./tools");
58
+ function stripThinkTags(text) {
59
+ return text
60
+ .replace(/<think(?:ing)?>[\s\S]*?<\/think(?:ing)?>/g, "")
61
+ .trim();
62
+ }
63
+ function investigate(opts) {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ var _a, _b, _c;
66
+ const { question, projectRoot, verbose = false } = opts;
67
+ const maxRounds = (_a = opts.maxRounds) !== null && _a !== void 0 ? _a : prompts_1.MAX_ROUNDS;
68
+ const config = (0, config_1.getLlmConfig)();
69
+ const modelName = path.basename(config.model, path.extname(config.model));
70
+ const client = new openai_1.default({
71
+ baseURL: `http://${config.host}:${config.port}/v1`,
72
+ apiKey: "local",
73
+ });
74
+ // Health check
75
+ try {
76
+ yield client.models.list();
77
+ }
78
+ catch (_d) {
79
+ throw new Error("Cannot connect to LLM server. Run `gmax llm on && gmax llm start`.");
80
+ }
81
+ // Initialize gmax resources
82
+ const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
83
+ const vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
84
+ const searcher = new searcher_1.Searcher(vectorDb);
85
+ const graphBuilder = new graph_builder_1.GraphBuilder(vectorDb, projectRoot);
86
+ const ctx = { vectorDb, searcher, graphBuilder, projectRoot };
87
+ const messages = [
88
+ { role: "system", content: prompts_1.SYSTEM_PROMPT },
89
+ { role: "user", content: question },
90
+ ];
91
+ const wallStart = Date.now();
92
+ let searchCount = 0;
93
+ let totalToolCalls = 0;
94
+ let rounds = 0;
95
+ try {
96
+ for (let round = 0; round < maxRounds; round++) {
97
+ rounds = round + 1;
98
+ const roundStart = Date.now();
99
+ const response = yield client.chat.completions.create({
100
+ model: modelName,
101
+ messages,
102
+ tools: tools_1.TOOLS,
103
+ tool_choice: "auto",
104
+ temperature: 0,
105
+ });
106
+ if (!((_b = response.choices) === null || _b === void 0 ? void 0 : _b.length)) {
107
+ throw new Error("LLM returned empty response");
108
+ }
109
+ const message = response.choices[0].message;
110
+ const roundMs = Date.now() - roundStart;
111
+ // No tool calls — final answer
112
+ if (!message.tool_calls || message.tool_calls.length === 0) {
113
+ if (verbose) {
114
+ process.stderr.write(`[R${round}] Final answer (${roundMs}ms)\n`);
115
+ }
116
+ const answer = message.content
117
+ ? stripThinkTags(message.content)
118
+ : "(no response)";
119
+ return {
120
+ answer,
121
+ rounds,
122
+ toolCalls: totalToolCalls,
123
+ wallMs: Date.now() - wallStart,
124
+ };
125
+ }
126
+ if (verbose) {
127
+ process.stderr.write(`[R${round}] ${roundMs}ms — ${message.tool_calls.length} tool call(s)\n`);
128
+ if (message.content) {
129
+ const reasoning = stripThinkTags(message.content);
130
+ if (reasoning) {
131
+ process.stderr.write(` (reasoning) ${reasoning}\n`);
132
+ }
133
+ }
134
+ }
135
+ // Append assistant message (required before tool results)
136
+ messages.push(message);
137
+ // Execute each tool call
138
+ for (const tc of message.tool_calls) {
139
+ // Only handle function-type tool calls
140
+ if (tc.type !== "function")
141
+ continue;
142
+ const fn = tc.function;
143
+ totalToolCalls++;
144
+ let result;
145
+ let args;
146
+ try {
147
+ args = JSON.parse(fn.arguments);
148
+ }
149
+ catch (_e) {
150
+ args = {};
151
+ result = `(error: malformed arguments: ${fn.arguments})`;
152
+ messages.push({ role: "tool", tool_call_id: tc.id, content: result });
153
+ continue;
154
+ }
155
+ // Search count limit
156
+ if (fn.name === "search") {
157
+ searchCount++;
158
+ if (searchCount > prompts_1.MAX_SEARCHES) {
159
+ result = (0, prompts_1.searchLimitMessage)(prompts_1.MAX_SEARCHES);
160
+ if (verbose) {
161
+ process.stderr.write(` ${fn.name}() — BLOCKED (limit)\n`);
162
+ }
163
+ messages.push({ role: "tool", tool_call_id: tc.id, content: result });
164
+ continue;
165
+ }
166
+ }
167
+ if (verbose) {
168
+ const argsStr = JSON.stringify(args);
169
+ process.stderr.write(` ${fn.name}(${argsStr})\n`);
170
+ }
171
+ result = yield (0, tools_1.executeTool)(fn.name, args, ctx);
172
+ if (verbose) {
173
+ const preview = result.split("\n").slice(0, 5).join("\n");
174
+ const extra = result.split("\n").length - 5;
175
+ for (const line of preview.split("\n")) {
176
+ process.stderr.write(` ${line}\n`);
177
+ }
178
+ if (extra > 0) {
179
+ process.stderr.write(` ... (${extra} more lines)\n`);
180
+ }
181
+ }
182
+ messages.push({ role: "tool", tool_call_id: tc.id, content: result });
183
+ }
184
+ }
185
+ // Round cap — force final answer
186
+ if (verbose) {
187
+ process.stderr.write(`[R${maxRounds}] Round cap reached, forcing final answer\n`);
188
+ }
189
+ messages.push({ role: "user", content: prompts_1.FORCE_FINAL_MESSAGE });
190
+ const response = yield client.chat.completions.create({
191
+ model: modelName,
192
+ messages,
193
+ temperature: 0,
194
+ });
195
+ if (!((_c = response.choices) === null || _c === void 0 ? void 0 : _c.length)) {
196
+ throw new Error("LLM returned empty response");
197
+ }
198
+ const answer = response.choices[0].message.content
199
+ ? stripThinkTags(response.choices[0].message.content)
200
+ : "(no response)";
201
+ return {
202
+ answer,
203
+ rounds: rounds + 1,
204
+ toolCalls: totalToolCalls,
205
+ wallMs: Date.now() - wallStart,
206
+ };
207
+ }
208
+ finally {
209
+ yield vectorDb.close();
210
+ }
211
+ });
212
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FORCE_FINAL_MESSAGE = exports.SYSTEM_PROMPT = exports.MAX_SEARCHES = exports.MAX_ROUNDS = void 0;
4
+ exports.searchLimitMessage = searchLimitMessage;
5
+ exports.MAX_ROUNDS = 10;
6
+ exports.MAX_SEARCHES = 3;
7
+ exports.SYSTEM_PROMPT = `\
8
+ You are a code investigation agent. You answer questions about codebases by \
9
+ using tools to gather evidence, then synthesizing a clear answer.
10
+
11
+ Tools (use the right tool for the job):
12
+ - search: Find symbols and files by meaning. Use short, specific queries (e.g. "search handler" not "find the function that handles search requests"). Returns filepath:line symbol [ROLE] — snippet.
13
+ - peek: BEST FIRST TOOL after finding a symbol. Shows signature + callers + callees in one call. Use this to understand a symbol quickly.
14
+ - trace: Full call graph — all callers and callees with locations. Use when peek isn't detailed enough.
15
+ - impact: What depends on this symbol/file? Use when the question is about change effects or breakage.
16
+ - related: Files coupled by shared symbols. Use when the question is about file relationships.
17
+
18
+ Strategy:
19
+ 1. Search once or twice to find the right symbol names and file locations.
20
+ 2. IMMEDIATELY switch to peek/trace/impact once you have a symbol name. Do NOT keep searching for the same thing with different queries.
21
+ 3. Use impact when the question involves "what would break" or "what depends on".
22
+ 4. Answer as soon as you have enough evidence. You do not need to use all rounds.
23
+
24
+ Rules:
25
+ - Never call search more than 3 times total. After 2 searches you should have symbol names — use peek/trace/impact from there.
26
+ - If a tool returns "(not found)" or "(no results)", try a slightly different symbol name, not a longer description.
27
+ - Cite specific files, line numbers, and symbol names in your answer.
28
+ - If you cannot find the answer, say so. Do not hallucinate.
29
+ - Be concise — 2-5 paragraphs maximum.`;
30
+ exports.FORCE_FINAL_MESSAGE = "You have used all available tool rounds. Synthesize your final answer now from the evidence gathered.";
31
+ function searchLimitMessage(max) {
32
+ return `(search limit reached — you have used ${max} searches. Use peek, trace, or impact to investigate the symbols you already found.)`;
33
+ }
@@ -0,0 +1,384 @@
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.TOOLS = void 0;
46
+ exports.executeTool = executeTool;
47
+ const fs = __importStar(require("node:fs"));
48
+ const path = __importStar(require("node:path"));
49
+ const impact_1 = require("../graph/impact");
50
+ const arrow_1 = require("../utils/arrow");
51
+ const filter_builder_1 = require("../utils/filter-builder");
52
+ // ---------------------------------------------------------------------------
53
+ // Tool definitions (OpenAI function-calling format)
54
+ // ---------------------------------------------------------------------------
55
+ exports.TOOLS = [
56
+ {
57
+ type: "function",
58
+ function: {
59
+ name: "search",
60
+ description: "Semantic code search — finds code by meaning. Returns filepath:line symbol [ROLE] — snippet.",
61
+ parameters: {
62
+ type: "object",
63
+ properties: {
64
+ query: { type: "string", description: "Natural language search query" },
65
+ max_count: { type: "integer", description: "Max results (default 5)" },
66
+ lang: { type: "string", description: "Filter by extension (e.g. 'ts', 'py')" },
67
+ file: { type: "string", description: "Filter to files matching this name" },
68
+ },
69
+ required: ["query"],
70
+ },
71
+ },
72
+ },
73
+ {
74
+ type: "function",
75
+ function: {
76
+ name: "trace",
77
+ description: "Full call graph — all callers (<-) and callees (->) with locations.",
78
+ parameters: {
79
+ type: "object",
80
+ properties: {
81
+ symbol: { type: "string", description: "Symbol name to trace" },
82
+ depth: { type: "integer", description: "Caller depth 1-3 (default 1)" },
83
+ },
84
+ required: ["symbol"],
85
+ },
86
+ },
87
+ },
88
+ {
89
+ type: "function",
90
+ function: {
91
+ name: "peek",
92
+ description: "Compact symbol overview — signature, callers, and callees.",
93
+ parameters: {
94
+ type: "object",
95
+ properties: {
96
+ symbol: { type: "string", description: "Symbol name to peek at" },
97
+ depth: { type: "integer", description: "Caller depth 1-3 (default 1)" },
98
+ },
99
+ required: ["symbol"],
100
+ },
101
+ },
102
+ },
103
+ {
104
+ type: "function",
105
+ function: {
106
+ name: "impact",
107
+ description: "Change impact — files that depend on this symbol/file, and affected tests.",
108
+ parameters: {
109
+ type: "object",
110
+ properties: {
111
+ target: { type: "string", description: "Symbol name or file path" },
112
+ depth: { type: "integer", description: "Traversal depth 1-3 (default 1)" },
113
+ },
114
+ required: ["target"],
115
+ },
116
+ },
117
+ },
118
+ {
119
+ type: "function",
120
+ function: {
121
+ name: "related",
122
+ description: "Find files related by shared symbol references.",
123
+ parameters: {
124
+ type: "object",
125
+ properties: {
126
+ file: { type: "string", description: "File path relative to project root" },
127
+ },
128
+ required: ["file"],
129
+ },
130
+ },
131
+ },
132
+ ];
133
+ // ---------------------------------------------------------------------------
134
+ // Tool execution wrappers
135
+ // ---------------------------------------------------------------------------
136
+ function rel(absPath, root) {
137
+ return absPath.startsWith(root) ? absPath.slice(root.length + 1) : absPath;
138
+ }
139
+ function clampDepth(d) {
140
+ const n = Number(d) || 1;
141
+ return Math.min(Math.max(n, 1), 3);
142
+ }
143
+ function executeSearch(args, ctx) {
144
+ return __awaiter(this, void 0, void 0, function* () {
145
+ const query = String(args.query || "");
146
+ if (!query)
147
+ return "(error: missing query)";
148
+ const limit = Math.min(Number(args.max_count) || 5, 10);
149
+ const filters = {};
150
+ if (args.lang)
151
+ filters.language = String(args.lang);
152
+ if (args.file)
153
+ filters.file = String(args.file);
154
+ const pathPrefix = `${ctx.projectRoot}/`;
155
+ const resp = yield ctx.searcher.search(query, limit, { rerank: true }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
156
+ if (!resp.data || resp.data.length === 0)
157
+ return "(no results)";
158
+ const lines = resp.data.map((r) => {
159
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
160
+ const absPath = String((_c = (_b = (_a = r.metadata) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : r.path) !== null && _c !== void 0 ? _c : "");
161
+ const rp = rel(absPath, ctx.projectRoot);
162
+ const defs = (0, arrow_1.toArr)((_d = r.definedSymbols) !== null && _d !== void 0 ? _d : r.defined_symbols);
163
+ const sym = defs[0] || "(anonymous)";
164
+ const role = String((_e = r.role) !== null && _e !== void 0 ? _e : "IMPL").slice(0, 4).toUpperCase();
165
+ const startLine = (_j = (_h = (_f = r.startLine) !== null && _f !== void 0 ? _f : (_g = r.generated_metadata) === null || _g === void 0 ? void 0 : _g.start_line) !== null && _h !== void 0 ? _h : r.start_line) !== null && _j !== void 0 ? _j : 0;
166
+ const hint = String((_l = (_k = r.text) !== null && _k !== void 0 ? _k : r.content) !== null && _l !== void 0 ? _l : "").split("\n")[0].slice(0, 100);
167
+ return `${rp}:${startLine} ${sym} [${role}] — ${hint}`;
168
+ });
169
+ return lines.join("\n");
170
+ });
171
+ }
172
+ function executeTrace(args, ctx) {
173
+ return __awaiter(this, void 0, void 0, function* () {
174
+ const symbol = String(args.symbol || "");
175
+ if (!symbol)
176
+ return "(error: missing symbol)";
177
+ const depth = clampDepth(args.depth);
178
+ const graph = yield ctx.graphBuilder.buildGraphMultiHop(symbol, depth);
179
+ if (!graph.center)
180
+ return "(not found)";
181
+ const lines = [];
182
+ lines.push(`${graph.center.symbol}\t${rel(graph.center.file, ctx.projectRoot)}:${graph.center.line}\t${graph.center.role}`);
183
+ function walkCallers(tree, d) {
184
+ for (const t of tree) {
185
+ lines.push(`${" ".repeat(d)}<- ${t.node.symbol}\t${rel(t.node.file, ctx.projectRoot)}:${t.node.line}`);
186
+ walkCallers(t.callers, d + 1);
187
+ }
188
+ }
189
+ walkCallers(graph.callerTree, 0);
190
+ for (const c of graph.callees) {
191
+ if (c.file) {
192
+ lines.push(`-> ${c.symbol}\t${rel(c.file, ctx.projectRoot)}:${c.line}`);
193
+ }
194
+ else {
195
+ lines.push(`-> ${c.symbol}\t(not indexed)`);
196
+ }
197
+ }
198
+ return lines.join("\n");
199
+ });
200
+ }
201
+ function executePeek(args, ctx) {
202
+ return __awaiter(this, void 0, void 0, function* () {
203
+ var _a;
204
+ const symbol = String(args.symbol || "");
205
+ if (!symbol)
206
+ return "(error: missing symbol)";
207
+ const graph = yield ctx.graphBuilder.buildGraph(symbol);
208
+ if (!graph.center)
209
+ return "(not found)";
210
+ const center = graph.center;
211
+ const prefix = ctx.projectRoot.endsWith("/")
212
+ ? ctx.projectRoot
213
+ : `${ctx.projectRoot}/`;
214
+ // Get metadata
215
+ const table = yield ctx.vectorDb.ensureTable();
216
+ const metaRows = yield table
217
+ .query()
218
+ .select(["is_exported", "start_line", "end_line"])
219
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
220
+ .limit(1)
221
+ .toArray();
222
+ const exported = metaRows.length > 0 && Boolean(metaRows[0].is_exported);
223
+ const startLine = metaRows.length > 0 ? Number(metaRows[0].start_line || 0) : center.line;
224
+ const endLine = metaRows.length > 0 ? Number(metaRows[0].end_line || 0) : center.line;
225
+ // Extract signature
226
+ let sig = "(source not available)";
227
+ try {
228
+ const content = fs.readFileSync(center.file, "utf-8");
229
+ const fileLines = content.split("\n");
230
+ const chunk = fileLines.slice(startLine, endLine + 1);
231
+ const sigLines = [];
232
+ for (const line of chunk) {
233
+ sigLines.push(line);
234
+ if (line.includes("{") || line.includes("=>"))
235
+ break;
236
+ }
237
+ sig = ((_a = sigLines[0]) === null || _a === void 0 ? void 0 : _a.trim()) || sig;
238
+ }
239
+ catch (_b) { }
240
+ const lines = [];
241
+ lines.push(`${center.symbol}\t${rel(center.file, ctx.projectRoot)}:${center.line + 1}\t${center.role}\t${exported ? "exported" : ""}`);
242
+ lines.push(`sig: ${sig}`);
243
+ for (const c of graph.callers.slice(0, 5)) {
244
+ lines.push(`<- ${c.symbol}\t${c.file ? `${rel(c.file, ctx.projectRoot)}:${c.line + 1}` : "(not indexed)"}`);
245
+ }
246
+ if (graph.callers.length > 5) {
247
+ lines.push(`<- ... ${graph.callers.length - 5} more`);
248
+ }
249
+ for (const c of graph.callees.slice(0, 8)) {
250
+ lines.push(`-> ${c.symbol}\t${c.file ? `${rel(c.file, ctx.projectRoot)}:${c.line + 1}` : "(not indexed)"}`);
251
+ }
252
+ if (graph.callees.length > 8) {
253
+ lines.push(`-> ... ${graph.callees.length - 8} more`);
254
+ }
255
+ return lines.join("\n");
256
+ });
257
+ }
258
+ function executeImpact(args, ctx) {
259
+ return __awaiter(this, void 0, void 0, function* () {
260
+ const target = String(args.target || "");
261
+ if (!target)
262
+ return "(error: missing target)";
263
+ const depth = clampDepth(args.depth);
264
+ const { symbols, resolvedAsFile } = yield (0, impact_1.resolveTargetSymbols)(target, ctx.vectorDb, ctx.projectRoot);
265
+ if (symbols.length === 0)
266
+ return "(not found)";
267
+ const excludePaths = resolvedAsFile
268
+ ? new Set([path.resolve(ctx.projectRoot, target)])
269
+ : undefined;
270
+ const [deps, tests] = yield Promise.all([
271
+ (0, impact_1.findDependents)(symbols, ctx.vectorDb, ctx.projectRoot, excludePaths),
272
+ (0, impact_1.findTests)(symbols, ctx.vectorDb, ctx.projectRoot, depth),
273
+ ]);
274
+ if (deps.length === 0 && tests.length === 0)
275
+ return "(no impact detected)";
276
+ const lines = [];
277
+ for (const d of deps) {
278
+ lines.push(`dep: ${rel(d.file, ctx.projectRoot)}\t${d.sharedSymbols}`);
279
+ }
280
+ for (const t of tests) {
281
+ const hopLabel = t.hops === 0 ? "direct" : `${t.hops} hop${t.hops > 1 ? "s" : ""}`;
282
+ lines.push(`test: ${rel(t.file, ctx.projectRoot)}:${t.line}\t${t.symbol}\t${hopLabel}`);
283
+ }
284
+ return lines.join("\n");
285
+ });
286
+ }
287
+ function executeRelated(args, ctx) {
288
+ return __awaiter(this, void 0, void 0, function* () {
289
+ const filePath = String(args.file || "");
290
+ if (!filePath)
291
+ return "(error: missing file)";
292
+ const absPath = path.resolve(ctx.projectRoot, filePath);
293
+ const table = yield ctx.vectorDb.ensureTable();
294
+ // Get file's symbols
295
+ const fileChunks = yield table
296
+ .query()
297
+ .select(["defined_symbols", "referenced_symbols"])
298
+ .where(`path = '${(0, filter_builder_1.escapeSqlString)(absPath)}'`)
299
+ .toArray();
300
+ if (fileChunks.length === 0)
301
+ return "(file not indexed)";
302
+ const definedHere = new Set();
303
+ const referencedHere = new Set();
304
+ for (const chunk of fileChunks) {
305
+ for (const s of (0, arrow_1.toArr)(chunk.defined_symbols))
306
+ definedHere.add(s);
307
+ for (const s of (0, arrow_1.toArr)(chunk.referenced_symbols))
308
+ referencedHere.add(s);
309
+ }
310
+ // Dependencies: files that DEFINE symbols this file REFERENCES
311
+ const depCounts = new Map();
312
+ for (const sym of referencedHere) {
313
+ if (definedHere.has(sym))
314
+ continue;
315
+ const rows = yield table
316
+ .query()
317
+ .select(["path"])
318
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}')`)
319
+ .limit(3)
320
+ .toArray();
321
+ for (const row of rows) {
322
+ const p = String(row.path || "");
323
+ if (p === absPath)
324
+ continue;
325
+ depCounts.set(p, (depCounts.get(p) || 0) + 1);
326
+ }
327
+ }
328
+ // Dependents: files that REFERENCE symbols this file DEFINES
329
+ const revCounts = new Map();
330
+ for (const sym of definedHere) {
331
+ const rows = yield table
332
+ .query()
333
+ .select(["path"])
334
+ .where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}')`)
335
+ .limit(20)
336
+ .toArray();
337
+ for (const row of rows) {
338
+ const p = String(row.path || "");
339
+ if (p === absPath)
340
+ continue;
341
+ revCounts.set(p, (revCounts.get(p) || 0) + 1);
342
+ }
343
+ }
344
+ const topDeps = [...depCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
345
+ const topRevs = [...revCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
346
+ if (topDeps.length === 0 && topRevs.length === 0)
347
+ return "(none)";
348
+ const lines = [];
349
+ for (const [p, count] of topDeps) {
350
+ lines.push(`dep: ${rel(p, ctx.projectRoot)}\t${count}`);
351
+ }
352
+ for (const [p, count] of topRevs) {
353
+ lines.push(`rev: ${rel(p, ctx.projectRoot)}\t${count}`);
354
+ }
355
+ return lines.join("\n");
356
+ });
357
+ }
358
+ // ---------------------------------------------------------------------------
359
+ // Dispatcher
360
+ // ---------------------------------------------------------------------------
361
+ function executeTool(name, args, ctx) {
362
+ return __awaiter(this, void 0, void 0, function* () {
363
+ try {
364
+ switch (name) {
365
+ case "search":
366
+ return yield executeSearch(args, ctx);
367
+ case "trace":
368
+ return yield executeTrace(args, ctx);
369
+ case "peek":
370
+ return yield executePeek(args, ctx);
371
+ case "impact":
372
+ return yield executeImpact(args, ctx);
373
+ case "related":
374
+ return yield executeRelated(args, ctx);
375
+ default:
376
+ return `(error: unknown tool: ${name})`;
377
+ }
378
+ }
379
+ catch (err) {
380
+ const msg = err instanceof Error ? err.message : String(err);
381
+ return `(error: ${msg})`;
382
+ }
383
+ });
384
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.13.5",
3
+ "version": "0.13.6",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -46,6 +46,7 @@
46
46
  "ignore": "^7.0.5",
47
47
  "lmdb": "^3.5.2",
48
48
  "onnxruntime-node": "1.24.3",
49
+ "openai": "^6.33.0",
49
50
  "ora": "^9.3.0",
50
51
  "piscina": "^5.1.4",
51
52
  "proper-lockfile": "^4.1.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.13.5",
3
+ "version": "0.13.6",
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",