praana 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/praana.js +17 -0
  4. package/bin/pran.js +17 -0
  5. package/dist/app-banner.d.ts +11 -0
  6. package/dist/app-banner.js +161 -0
  7. package/dist/app-controller.d.ts +44 -0
  8. package/dist/app-controller.js +143 -0
  9. package/dist/app-identity.d.ts +18 -0
  10. package/dist/app-identity.js +52 -0
  11. package/dist/auto-compact.d.ts +16 -0
  12. package/dist/auto-compact.js +101 -0
  13. package/dist/cli-args.d.ts +14 -0
  14. package/dist/cli-args.js +69 -0
  15. package/dist/compile-classic.d.ts +21 -0
  16. package/dist/compile-classic.js +106 -0
  17. package/dist/compiler.d.ts +75 -0
  18. package/dist/compiler.js +406 -0
  19. package/dist/config.d.ts +3 -0
  20. package/dist/config.js +433 -0
  21. package/dist/context-engine/activity-log.d.ts +9 -0
  22. package/dist/context-engine/activity-log.js +109 -0
  23. package/dist/context-engine/artifact-store.d.ts +32 -0
  24. package/dist/context-engine/artifact-store.js +272 -0
  25. package/dist/context-engine/bm25.d.ts +3 -0
  26. package/dist/context-engine/bm25.js +32 -0
  27. package/dist/context-engine/checkpoint.d.ts +34 -0
  28. package/dist/context-engine/checkpoint.js +430 -0
  29. package/dist/context-engine/classify.d.ts +3 -0
  30. package/dist/context-engine/classify.js +60 -0
  31. package/dist/context-engine/db.d.ts +73 -0
  32. package/dist/context-engine/db.js +505 -0
  33. package/dist/context-engine/distiller.d.ts +30 -0
  34. package/dist/context-engine/distiller.js +67 -0
  35. package/dist/context-engine/engine-compiler.d.ts +23 -0
  36. package/dist/context-engine/engine-compiler.js +297 -0
  37. package/dist/context-engine/error-tracker.d.ts +21 -0
  38. package/dist/context-engine/error-tracker.js +74 -0
  39. package/dist/context-engine/event-lineage.d.ts +26 -0
  40. package/dist/context-engine/event-lineage.js +120 -0
  41. package/dist/context-engine/extraction.d.ts +26 -0
  42. package/dist/context-engine/extraction.js +83 -0
  43. package/dist/context-engine/index.d.ts +82 -0
  44. package/dist/context-engine/index.js +238 -0
  45. package/dist/context-engine/scoring.d.ts +13 -0
  46. package/dist/context-engine/scoring.js +47 -0
  47. package/dist/context-engine/state-snapshot.d.ts +8 -0
  48. package/dist/context-engine/state-snapshot.js +50 -0
  49. package/dist/context-engine/summarize.d.ts +6 -0
  50. package/dist/context-engine/summarize.js +32 -0
  51. package/dist/context-engine/telemetry.d.ts +25 -0
  52. package/dist/context-engine/telemetry.js +64 -0
  53. package/dist/context-engine/turn-digest.d.ts +50 -0
  54. package/dist/context-engine/turn-digest.js +250 -0
  55. package/dist/context-engine/turn-ledger.d.ts +18 -0
  56. package/dist/context-engine/turn-ledger.js +184 -0
  57. package/dist/context-engine/turn-recorder.d.ts +24 -0
  58. package/dist/context-engine/turn-recorder.js +88 -0
  59. package/dist/context-engine/types.d.ts +201 -0
  60. package/dist/context-engine/types.js +4 -0
  61. package/dist/context-pressure.d.ts +19 -0
  62. package/dist/context-pressure.js +36 -0
  63. package/dist/distillers/generic.d.ts +14 -0
  64. package/dist/distillers/generic.js +93 -0
  65. package/dist/distillers/git-diff.d.ts +8 -0
  66. package/dist/distillers/git-diff.js +119 -0
  67. package/dist/distillers/index.d.ts +2 -0
  68. package/dist/distillers/index.js +16 -0
  69. package/dist/distillers/npm-test.d.ts +8 -0
  70. package/dist/distillers/npm-test.js +50 -0
  71. package/dist/distillers/rg-results.d.ts +8 -0
  72. package/dist/distillers/rg-results.js +28 -0
  73. package/dist/distillers/tsc-errors.d.ts +8 -0
  74. package/dist/distillers/tsc-errors.js +52 -0
  75. package/dist/event-log.d.ts +56 -0
  76. package/dist/event-log.js +214 -0
  77. package/dist/llm.d.ts +29 -0
  78. package/dist/llm.js +155 -0
  79. package/dist/logger.d.ts +94 -0
  80. package/dist/logger.js +287 -0
  81. package/dist/main.d.ts +1 -0
  82. package/dist/main.js +54 -0
  83. package/dist/memory/confidence.d.ts +7 -0
  84. package/dist/memory/confidence.js +37 -0
  85. package/dist/memory/consolidation.d.ts +26 -0
  86. package/dist/memory/consolidation.js +166 -0
  87. package/dist/memory/db.d.ts +40 -0
  88. package/dist/memory/db.js +283 -0
  89. package/dist/memory/dedup.d.ts +6 -0
  90. package/dist/memory/dedup.js +50 -0
  91. package/dist/memory/embedder-factory.d.ts +3 -0
  92. package/dist/memory/embedder-factory.js +81 -0
  93. package/dist/memory/embeddings.d.ts +15 -0
  94. package/dist/memory/embeddings.js +67 -0
  95. package/dist/memory/index.d.ts +9 -0
  96. package/dist/memory/index.js +11 -0
  97. package/dist/memory/ollama-summarizer.d.ts +19 -0
  98. package/dist/memory/ollama-summarizer.js +72 -0
  99. package/dist/memory/openai-summarizer.d.ts +21 -0
  100. package/dist/memory/openai-summarizer.js +51 -0
  101. package/dist/memory/store.d.ts +61 -0
  102. package/dist/memory/store.js +502 -0
  103. package/dist/memory/summarizer-factory.d.ts +3 -0
  104. package/dist/memory/summarizer-factory.js +69 -0
  105. package/dist/memory/summarizer.d.ts +4 -0
  106. package/dist/memory/summarizer.js +112 -0
  107. package/dist/memory/types.d.ts +87 -0
  108. package/dist/memory/types.js +17 -0
  109. package/dist/model-context.d.ts +15 -0
  110. package/dist/model-context.js +212 -0
  111. package/dist/project-detector.d.ts +37 -0
  112. package/dist/project-detector.js +604 -0
  113. package/dist/render.d.ts +15 -0
  114. package/dist/render.js +46 -0
  115. package/dist/session.d.ts +118 -0
  116. package/dist/session.js +809 -0
  117. package/dist/skills/index.d.ts +69 -0
  118. package/dist/skills/index.js +885 -0
  119. package/dist/skills/types.d.ts +93 -0
  120. package/dist/skills/types.js +8 -0
  121. package/dist/slash-commands.d.ts +14 -0
  122. package/dist/slash-commands.js +301 -0
  123. package/dist/state-graph.d.ts +38 -0
  124. package/dist/state-graph.js +255 -0
  125. package/dist/status-bar.d.ts +54 -0
  126. package/dist/status-bar.js +184 -0
  127. package/dist/thinking-display.d.ts +21 -0
  128. package/dist/thinking-display.js +37 -0
  129. package/dist/tool-summary.d.ts +4 -0
  130. package/dist/tool-summary.js +67 -0
  131. package/dist/tools/index.d.ts +925 -0
  132. package/dist/tools/index.js +86 -0
  133. package/dist/tools/knowledge.d.ts +140 -0
  134. package/dist/tools/knowledge.js +260 -0
  135. package/dist/tools/memory.d.ts +39 -0
  136. package/dist/tools/memory.js +300 -0
  137. package/dist/tools/search-code.d.ts +134 -0
  138. package/dist/tools/search-code.js +390 -0
  139. package/dist/tools/system.d.ts +16 -0
  140. package/dist/tools/system.js +499 -0
  141. package/dist/tools/tool-def.d.ts +6 -0
  142. package/dist/tools/tool-def.js +3 -0
  143. package/dist/turn-control.d.ts +51 -0
  144. package/dist/turn-control.js +210 -0
  145. package/dist/turn.d.ts +20 -0
  146. package/dist/turn.js +624 -0
  147. package/dist/types.d.ts +233 -0
  148. package/dist/types.js +4 -0
  149. package/dist/ui/readline-ui.d.ts +2 -0
  150. package/dist/ui/readline-ui.js +176 -0
  151. package/dist/ui/tui/app.d.ts +13 -0
  152. package/dist/ui/tui/app.js +270 -0
  153. package/dist/ui/tui/busy-indicator.d.ts +2 -0
  154. package/dist/ui/tui/busy-indicator.js +13 -0
  155. package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
  156. package/dist/ui/tui/components/gutter-rule.js +9 -0
  157. package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
  158. package/dist/ui/tui/components/inline-tool-row.js +8 -0
  159. package/dist/ui/tui/components/prompt-input.d.ts +20 -0
  160. package/dist/ui/tui/components/prompt-input.js +120 -0
  161. package/dist/ui/tui/components/system-line.d.ts +5 -0
  162. package/dist/ui/tui/components/system-line.js +6 -0
  163. package/dist/ui/tui/components/thinking-block.d.ts +11 -0
  164. package/dist/ui/tui/components/thinking-block.js +31 -0
  165. package/dist/ui/tui/components/toast-line.d.ts +4 -0
  166. package/dist/ui/tui/components/toast-line.js +8 -0
  167. package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
  168. package/dist/ui/tui/components/tool-result-line.js +6 -0
  169. package/dist/ui/tui/components/turn-footer.d.ts +5 -0
  170. package/dist/ui/tui/components/turn-footer.js +7 -0
  171. package/dist/ui/tui/components/user-block.d.ts +6 -0
  172. package/dist/ui/tui/components/user-block.js +6 -0
  173. package/dist/ui/tui/logo-banner.d.ts +5 -0
  174. package/dist/ui/tui/logo-banner.js +8 -0
  175. package/dist/ui/tui/markdown-render.d.ts +16 -0
  176. package/dist/ui/tui/markdown-render.js +218 -0
  177. package/dist/ui/tui/palette.d.ts +12 -0
  178. package/dist/ui/tui/palette.js +13 -0
  179. package/dist/ui/tui/reasoning-summary.d.ts +12 -0
  180. package/dist/ui/tui/reasoning-summary.js +27 -0
  181. package/dist/ui/tui/reducer.d.ts +92 -0
  182. package/dist/ui/tui/reducer.js +260 -0
  183. package/dist/ui/tui/run.d.ts +3 -0
  184. package/dist/ui/tui/run.js +40 -0
  185. package/dist/ui/tui/sink.d.ts +4 -0
  186. package/dist/ui/tui/sink.js +89 -0
  187. package/dist/ui/tui/status-bar-view.d.ts +5 -0
  188. package/dist/ui/tui/status-bar-view.js +44 -0
  189. package/dist/ui/tui/terminal-height.d.ts +12 -0
  190. package/dist/ui/tui/terminal-height.js +20 -0
  191. package/dist/ui/tui/terminal-width.d.ts +2 -0
  192. package/dist/ui/tui/terminal-width.js +5 -0
  193. package/dist/ui/tui/tool-display.d.ts +23 -0
  194. package/dist/ui/tui/tool-display.js +217 -0
  195. package/dist/ui/tui/transcript-line.d.ts +12 -0
  196. package/dist/ui/tui/transcript-line.js +43 -0
  197. package/dist/ui/tui/transcript-replay.d.ts +12 -0
  198. package/dist/ui/tui/transcript-replay.js +117 -0
  199. package/dist/ui-events.d.ts +39 -0
  200. package/dist/ui-events.js +33 -0
  201. package/dist/ui.d.ts +77 -0
  202. package/dist/ui.js +179 -0
  203. package/package.json +73 -0
  204. package/praana.config.example.toml +231 -0
@@ -0,0 +1,390 @@
1
+ import { defineTool } from "./tool-def.js";
2
+ import { z } from "zod";
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync, realpathSync } from "node:fs";
5
+ import { resolve as resolvePath, isAbsolute, normalize } from "node:path";
6
+ import { homedir } from "node:os";
7
+ const searchCodeSchema = z.object({
8
+ pattern: z
9
+ .string()
10
+ .min(1)
11
+ .describe("Regex pattern to search for (ripgrep regex syntax)"),
12
+ path: z
13
+ .string()
14
+ .optional()
15
+ .describe("Directory or file to search (default: working directory)"),
16
+ glob: z
17
+ .union([z.string(), z.array(z.string())])
18
+ .optional()
19
+ .describe("Include glob filter(s), e.g. '*.ts' or ['*.ts', '*.tsx']"),
20
+ glob_exclude: z
21
+ .union([z.string(), z.array(z.string())])
22
+ .optional()
23
+ .describe("Exclude glob filter(s)"),
24
+ case_insensitive: z
25
+ .boolean()
26
+ .optional()
27
+ .describe("Case-insensitive search (-i)"),
28
+ context: z
29
+ .number()
30
+ .int()
31
+ .min(0)
32
+ .max(50)
33
+ .optional()
34
+ .describe("Lines of context before and after each match (-C). Default 0."),
35
+ max_results: z
36
+ .number()
37
+ .int()
38
+ .min(1)
39
+ .optional()
40
+ .describe("Stop after this many matches are found"),
41
+ file_type: z
42
+ .string()
43
+ .optional()
44
+ .describe("ripgrep file type filter (e.g. 'ts', 'rust', 'py')"),
45
+ include_hidden: z
46
+ .boolean()
47
+ .optional()
48
+ .describe("Search hidden files and directories (--hidden)"),
49
+ no_ignore: z
50
+ .boolean()
51
+ .optional()
52
+ .describe("Don't respect .gitignore/.ignore (--no-ignore)"),
53
+ multiline: z
54
+ .boolean()
55
+ .optional()
56
+ .describe("Allow patterns to match across multiple lines (-U)"),
57
+ timeout: z
58
+ .number()
59
+ .int()
60
+ .min(1)
61
+ .optional()
62
+ .describe("Timeout in milliseconds (default 30000)"),
63
+ });
64
+ /**
65
+ * Build the ripgrep argv for the given arguments.
66
+ *
67
+ * Pattern is passed as `--` then `pattern` so it can't be misinterpreted as
68
+ * a flag even if it starts with `-`.
69
+ */
70
+ export function buildRipgrepArgs(args, searchPath) {
71
+ const argv = [
72
+ "--json",
73
+ "--no-heading",
74
+ "--no-messages",
75
+ // --no-config: ignore the user's ~/.ripgreprc / RIPGREP_CONFIG_PATH so the
76
+ // tool's behavior is deterministic across machines. Custom ripgrep configs
77
+ // (e.g. --type-add) are NOT honored by this tool.
78
+ "--no-config",
79
+ ];
80
+ if (args.case_insensitive)
81
+ argv.push("-i");
82
+ if (args.multiline)
83
+ argv.push("-U");
84
+ if (args.include_hidden)
85
+ argv.push("--hidden");
86
+ if (args.no_ignore)
87
+ argv.push("--no-ignore");
88
+ if (args.file_type)
89
+ argv.push("--type", args.file_type);
90
+ const ctx = args.context ?? 0;
91
+ if (ctx > 0)
92
+ argv.push("-C", String(ctx));
93
+ for (const g of args.glob ? (Array.isArray(args.glob) ? args.glob : [args.glob]) : []) {
94
+ argv.push("--glob", g);
95
+ }
96
+ for (const g of args.glob_exclude
97
+ ? Array.isArray(args.glob_exclude)
98
+ ? args.glob_exclude
99
+ : [args.glob_exclude]
100
+ : []) {
101
+ argv.push("--glob", "!" + g);
102
+ }
103
+ argv.push("--", args.pattern, searchPath);
104
+ return argv;
105
+ }
106
+ /** Return null if the path is allowed by the sandbox, else a human-readable error. */
107
+ function sandboxBlockReason(path, sandbox) {
108
+ if (!sandbox?.enabled || sandbox.allowed_paths.length === 0)
109
+ return null;
110
+ const resolve = (p) => {
111
+ const expanded = p.replace(/^~/, homedir());
112
+ const normalized = normalize(expanded);
113
+ if (!existsSync(normalized))
114
+ return normalized;
115
+ try {
116
+ return realpathSync(normalized);
117
+ }
118
+ catch {
119
+ return normalized;
120
+ }
121
+ };
122
+ const resolved = resolve(path);
123
+ const allowed = sandbox.allowed_paths.some((ap) => {
124
+ const apResolved = resolve(ap);
125
+ return resolved === apResolved || resolved.startsWith(apResolved + "/");
126
+ });
127
+ return allowed
128
+ ? null
129
+ : `Blocked by sandbox: path not in allowed list: ${path}`;
130
+ }
131
+ export function createParseState() {
132
+ return {
133
+ matches: [],
134
+ totalMatches: 0,
135
+ truncated: false,
136
+ _currentFile: null,
137
+ _currentFileLineMap: new Map(),
138
+ _lastMatchInFile: null,
139
+ };
140
+ }
141
+ function processEvent(state, ev, context, maxResults, onTruncate) {
142
+ if (ev.type === "begin") {
143
+ state._currentFile = ev.data.path?.text ?? null;
144
+ state._currentFileLineMap = new Map();
145
+ state._lastMatchInFile = null;
146
+ return;
147
+ }
148
+ if (ev.type === "end") {
149
+ if (state._lastMatchInFile && context > 0 && state._currentFile) {
150
+ const after = [];
151
+ for (let i = 1; i <= context; i++) {
152
+ const t = state._currentFileLineMap.get(state._lastMatchInFile.line + i);
153
+ if (t !== undefined)
154
+ after.push(t);
155
+ }
156
+ state._lastMatchInFile.context_after = after;
157
+ }
158
+ return;
159
+ }
160
+ if (ev.type === "context") {
161
+ const ln = ev.data.line_number;
162
+ const text = ev.data.lines?.text ?? "";
163
+ if (ln !== undefined) {
164
+ const trimmed = text.endsWith("\n") ? text.slice(0, -1) : text;
165
+ state._currentFileLineMap.set(ln, trimmed);
166
+ if (state._lastMatchInFile &&
167
+ context > 0 &&
168
+ ln > state._lastMatchInFile.line &&
169
+ ln <= state._lastMatchInFile.line + context) {
170
+ state._lastMatchInFile.context_after.push(trimmed);
171
+ }
172
+ }
173
+ return;
174
+ }
175
+ if (ev.type === "match") {
176
+ if (state.truncated)
177
+ return;
178
+ if (maxResults !== undefined && state.totalMatches >= maxResults) {
179
+ state.truncated = true;
180
+ onTruncate();
181
+ return;
182
+ }
183
+ const file = ev.data.path?.text ?? "";
184
+ const line = ev.data.line_number ?? 0;
185
+ const sub = ev.data.submatches?.[0];
186
+ const column = (sub?.start ?? 0) + 1;
187
+ const rawText = ev.data.lines?.text ?? "";
188
+ const text = rawText.endsWith("\n") ? rawText.slice(0, -1) : rawText;
189
+ const before = [];
190
+ if (context > 0) {
191
+ for (let i = context; i >= 1; i--) {
192
+ const t = state._currentFileLineMap.get(line - i);
193
+ if (t !== undefined)
194
+ before.push(t);
195
+ }
196
+ }
197
+ const m = {
198
+ file,
199
+ line,
200
+ column,
201
+ text,
202
+ context_before: before,
203
+ context_after: [],
204
+ };
205
+ state.matches.push(m);
206
+ state._lastMatchInFile = m;
207
+ state.totalMatches++;
208
+ return;
209
+ }
210
+ }
211
+ /** Feed raw rg --json lines into the parse state. Triggers `onTruncate` on cap. */
212
+ export function feedParseState(state, rawLines, context, maxResults, onTruncate) {
213
+ for (const raw of rawLines) {
214
+ if (state.truncated)
215
+ break;
216
+ if (!raw)
217
+ continue;
218
+ let ev;
219
+ try {
220
+ ev = JSON.parse(raw);
221
+ }
222
+ catch {
223
+ // Malformed line — rg should not produce these, skip defensively.
224
+ continue;
225
+ }
226
+ processEvent(state, ev, context, maxResults, onTruncate);
227
+ }
228
+ }
229
+ /** Convenience wrapper: feed a complete log at once. Used by tests. */
230
+ export function parseRipgrepEvents(rawLines, context, maxResults, onTruncate) {
231
+ const state = createParseState();
232
+ feedParseState(state, rawLines, context, maxResults, onTruncate);
233
+ const filesWithMatches = new Set(state.matches.map((m) => m.file)).size;
234
+ return {
235
+ matches: state.matches,
236
+ totalMatches: state.totalMatches,
237
+ filesWithMatches,
238
+ truncated: state.truncated,
239
+ };
240
+ }
241
+ /** Spawn ripgrep, return structured result. */
242
+ export async function runRipgrep(args, rgBin, cwd, sandbox, getAbortSignal) {
243
+ const started = Date.now();
244
+ const searchPath = args.path
245
+ ? isAbsolute(args.path)
246
+ ? args.path
247
+ : resolvePath(cwd, args.path)
248
+ : cwd;
249
+ const blockReason = sandboxBlockReason(searchPath, sandbox);
250
+ if (blockReason)
251
+ return { ok: false, error: blockReason };
252
+ const argv = buildRipgrepArgs(args, searchPath);
253
+ const ctx = args.context ?? 0;
254
+ const maxResults = args.max_results;
255
+ const timeoutMs = args.timeout ?? 30_000;
256
+ const signal = getAbortSignal?.();
257
+ if (signal?.aborted)
258
+ return { ok: false, error: "Interrupted" };
259
+ return new Promise((resolve) => {
260
+ const child = spawn(rgBin, argv, {
261
+ cwd,
262
+ stdio: ["ignore", "pipe", "pipe"],
263
+ // Node 22 resolves a bare "rg" against PATH on POSIX.
264
+ });
265
+ let stdoutBuf = "";
266
+ let resolved = false;
267
+ const stderrChunks = [];
268
+ const state = createParseState();
269
+ const kill = (sig) => {
270
+ if (!child.killed)
271
+ child.kill(sig);
272
+ };
273
+ /** Stop the child and drop the stdout pipe so no more bytes enter the buffer. */
274
+ const truncateNow = () => {
275
+ child.stdout?.destroy();
276
+ // SIGKILL is the right signal here: we no longer care about rg's
277
+ // cleanup, only that it stops writing to the pipe.
278
+ kill("SIGKILL");
279
+ };
280
+ const finish = (result) => {
281
+ if (resolved)
282
+ return;
283
+ resolved = true;
284
+ clearTimeout(timer);
285
+ signal?.removeEventListener("abort", onAbort);
286
+ kill("SIGTERM");
287
+ setTimeout(() => kill("SIGKILL"), 500).unref();
288
+ resolve(result);
289
+ };
290
+ const onAbort = () => finish({ ok: false, error: "Interrupted" });
291
+ signal?.addEventListener("abort", onAbort, { once: true });
292
+ const timer = setTimeout(() => {
293
+ finish({ ok: false, error: `search_code timed out after ${timeoutMs}ms` });
294
+ }, timeoutMs);
295
+ child.stdout?.on("data", (chunk) => {
296
+ if (state.truncated)
297
+ return; // backpressure: stop buffering post-cap bytes
298
+ stdoutBuf += chunk.toString("utf-8");
299
+ let nl;
300
+ const newLines = [];
301
+ while ((nl = stdoutBuf.indexOf("\n")) !== -1) {
302
+ newLines.push(stdoutBuf.slice(0, nl));
303
+ stdoutBuf = stdoutBuf.slice(nl + 1);
304
+ }
305
+ if (newLines.length > 0) {
306
+ feedParseState(state, newLines, ctx, maxResults, truncateNow);
307
+ }
308
+ });
309
+ child.stderr?.on("data", (chunk) => {
310
+ stderrChunks.push(chunk);
311
+ if (stderrChunks.length > 64)
312
+ stderrChunks.shift();
313
+ });
314
+ child.on("error", (err) => {
315
+ if (err.code === "ENOENT") {
316
+ finish({
317
+ ok: false,
318
+ error: "ripgrep ('rg') not found in PATH. Install ripgrep (https://github.com/BurntSushi/ripgrep) or set search_code.rg_path in praana.config.toml to point at the binary.",
319
+ });
320
+ return;
321
+ }
322
+ finish({ ok: false, error: `Failed to run ripgrep: ${err.message}` });
323
+ });
324
+ child.on("close", (code) => {
325
+ if (resolved) {
326
+ // finish() already ran (abort / timeout / error). Drop any tail bytes.
327
+ stdoutBuf = "";
328
+ return;
329
+ }
330
+ if (stdoutBuf.length > 0) {
331
+ feedParseState(state, [stdoutBuf], ctx, maxResults, truncateNow);
332
+ stdoutBuf = "";
333
+ }
334
+ const stderrTail = Buffer.concat(stderrChunks).toString("utf-8").trim();
335
+ // rg exit codes: 0 = matches, 1 = no matches, 2 = error.
336
+ if (code === 2) {
337
+ finish({
338
+ ok: false,
339
+ error: stderrTail
340
+ ? `ripgrep error: ${stderrTail}`
341
+ : "ripgrep exited with code 2 (regex parse error or other failure)",
342
+ });
343
+ return;
344
+ }
345
+ if (code === 0 || code === 1 || state.matches.length > 0) {
346
+ const filesWithMatches = new Set(state.matches.map((m) => m.file)).size;
347
+ finish({
348
+ ok: true,
349
+ pattern: args.pattern,
350
+ path: searchPath,
351
+ matches: state.matches,
352
+ stats: {
353
+ totalMatches: state.totalMatches,
354
+ filesWithMatches,
355
+ truncated: state.truncated,
356
+ dropped: state.truncated ? 1 : 0, // exact count unknown; >= 1
357
+ },
358
+ duration_ms: Date.now() - started,
359
+ });
360
+ return;
361
+ }
362
+ finish({
363
+ ok: false,
364
+ error: stderrTail
365
+ ? `ripgrep failed: ${stderrTail}`
366
+ : `ripgrep exited with code ${code}`,
367
+ });
368
+ });
369
+ });
370
+ }
371
+ export function createSearchCodeTool(ctx) {
372
+ return {
373
+ search_code: defineTool({
374
+ description: "Fast structured code search powered by ripgrep. Returns file:line:column matches with optional context lines. Use instead of `shell grep` for codebase exploration.",
375
+ parameters: searchCodeSchema,
376
+ execute: async (raw) => {
377
+ const parsed = searchCodeSchema.safeParse(raw);
378
+ if (!parsed.success) {
379
+ return {
380
+ ok: false,
381
+ error: `Invalid arguments: ${parsed.error.issues
382
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
383
+ .join("; ")}`,
384
+ };
385
+ }
386
+ return runRipgrep(parsed.data, ctx.rgPath ?? "rg", ctx.cwd, ctx.sandbox, ctx.getAbortSignal);
387
+ },
388
+ }),
389
+ };
390
+ }
@@ -0,0 +1,16 @@
1
+ import type { SandboxConfig } from "../types.js";
2
+ export interface SystemToolContext {
3
+ cwd: string;
4
+ getAbortSignal?: () => AbortSignal | undefined;
5
+ sandbox?: SandboxConfig;
6
+ editConfirm?: boolean;
7
+ }
8
+ export declare function createSystemTools(ctx: SystemToolContext): {
9
+ shell: import("./tool-def.js").ToolDefinition;
10
+ read_file: import("./tool-def.js").ToolDefinition;
11
+ read_and_summarize: import("./tool-def.js").ToolDefinition;
12
+ write_file: import("./tool-def.js").ToolDefinition;
13
+ edit_file: import("./tool-def.js").ToolDefinition;
14
+ batch_write: import("./tool-def.js").ToolDefinition;
15
+ batch_edit: import("./tool-def.js").ToolDefinition;
16
+ };