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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/praana.js +17 -0
- package/bin/pran.js +17 -0
- package/dist/app-banner.d.ts +11 -0
- package/dist/app-banner.js +161 -0
- package/dist/app-controller.d.ts +44 -0
- package/dist/app-controller.js +143 -0
- package/dist/app-identity.d.ts +18 -0
- package/dist/app-identity.js +52 -0
- package/dist/auto-compact.d.ts +16 -0
- package/dist/auto-compact.js +101 -0
- package/dist/cli-args.d.ts +14 -0
- package/dist/cli-args.js +69 -0
- package/dist/compile-classic.d.ts +21 -0
- package/dist/compile-classic.js +106 -0
- package/dist/compiler.d.ts +75 -0
- package/dist/compiler.js +406 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +433 -0
- package/dist/context-engine/activity-log.d.ts +9 -0
- package/dist/context-engine/activity-log.js +109 -0
- package/dist/context-engine/artifact-store.d.ts +32 -0
- package/dist/context-engine/artifact-store.js +272 -0
- package/dist/context-engine/bm25.d.ts +3 -0
- package/dist/context-engine/bm25.js +32 -0
- package/dist/context-engine/checkpoint.d.ts +34 -0
- package/dist/context-engine/checkpoint.js +430 -0
- package/dist/context-engine/classify.d.ts +3 -0
- package/dist/context-engine/classify.js +60 -0
- package/dist/context-engine/db.d.ts +73 -0
- package/dist/context-engine/db.js +505 -0
- package/dist/context-engine/distiller.d.ts +30 -0
- package/dist/context-engine/distiller.js +67 -0
- package/dist/context-engine/engine-compiler.d.ts +23 -0
- package/dist/context-engine/engine-compiler.js +297 -0
- package/dist/context-engine/error-tracker.d.ts +21 -0
- package/dist/context-engine/error-tracker.js +74 -0
- package/dist/context-engine/event-lineage.d.ts +26 -0
- package/dist/context-engine/event-lineage.js +120 -0
- package/dist/context-engine/extraction.d.ts +26 -0
- package/dist/context-engine/extraction.js +83 -0
- package/dist/context-engine/index.d.ts +82 -0
- package/dist/context-engine/index.js +238 -0
- package/dist/context-engine/scoring.d.ts +13 -0
- package/dist/context-engine/scoring.js +47 -0
- package/dist/context-engine/state-snapshot.d.ts +8 -0
- package/dist/context-engine/state-snapshot.js +50 -0
- package/dist/context-engine/summarize.d.ts +6 -0
- package/dist/context-engine/summarize.js +32 -0
- package/dist/context-engine/telemetry.d.ts +25 -0
- package/dist/context-engine/telemetry.js +64 -0
- package/dist/context-engine/turn-digest.d.ts +50 -0
- package/dist/context-engine/turn-digest.js +250 -0
- package/dist/context-engine/turn-ledger.d.ts +18 -0
- package/dist/context-engine/turn-ledger.js +184 -0
- package/dist/context-engine/turn-recorder.d.ts +24 -0
- package/dist/context-engine/turn-recorder.js +88 -0
- package/dist/context-engine/types.d.ts +201 -0
- package/dist/context-engine/types.js +4 -0
- package/dist/context-pressure.d.ts +19 -0
- package/dist/context-pressure.js +36 -0
- package/dist/distillers/generic.d.ts +14 -0
- package/dist/distillers/generic.js +93 -0
- package/dist/distillers/git-diff.d.ts +8 -0
- package/dist/distillers/git-diff.js +119 -0
- package/dist/distillers/index.d.ts +2 -0
- package/dist/distillers/index.js +16 -0
- package/dist/distillers/npm-test.d.ts +8 -0
- package/dist/distillers/npm-test.js +50 -0
- package/dist/distillers/rg-results.d.ts +8 -0
- package/dist/distillers/rg-results.js +28 -0
- package/dist/distillers/tsc-errors.d.ts +8 -0
- package/dist/distillers/tsc-errors.js +52 -0
- package/dist/event-log.d.ts +56 -0
- package/dist/event-log.js +214 -0
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +155 -0
- package/dist/logger.d.ts +94 -0
- package/dist/logger.js +287 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +54 -0
- package/dist/memory/confidence.d.ts +7 -0
- package/dist/memory/confidence.js +37 -0
- package/dist/memory/consolidation.d.ts +26 -0
- package/dist/memory/consolidation.js +166 -0
- package/dist/memory/db.d.ts +40 -0
- package/dist/memory/db.js +283 -0
- package/dist/memory/dedup.d.ts +6 -0
- package/dist/memory/dedup.js +50 -0
- package/dist/memory/embedder-factory.d.ts +3 -0
- package/dist/memory/embedder-factory.js +81 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.js +67 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/ollama-summarizer.d.ts +19 -0
- package/dist/memory/ollama-summarizer.js +72 -0
- package/dist/memory/openai-summarizer.d.ts +21 -0
- package/dist/memory/openai-summarizer.js +51 -0
- package/dist/memory/store.d.ts +61 -0
- package/dist/memory/store.js +502 -0
- package/dist/memory/summarizer-factory.d.ts +3 -0
- package/dist/memory/summarizer-factory.js +69 -0
- package/dist/memory/summarizer.d.ts +4 -0
- package/dist/memory/summarizer.js +112 -0
- package/dist/memory/types.d.ts +87 -0
- package/dist/memory/types.js +17 -0
- package/dist/model-context.d.ts +15 -0
- package/dist/model-context.js +212 -0
- package/dist/project-detector.d.ts +37 -0
- package/dist/project-detector.js +604 -0
- package/dist/render.d.ts +15 -0
- package/dist/render.js +46 -0
- package/dist/session.d.ts +118 -0
- package/dist/session.js +809 -0
- package/dist/skills/index.d.ts +69 -0
- package/dist/skills/index.js +885 -0
- package/dist/skills/types.d.ts +93 -0
- package/dist/skills/types.js +8 -0
- package/dist/slash-commands.d.ts +14 -0
- package/dist/slash-commands.js +301 -0
- package/dist/state-graph.d.ts +38 -0
- package/dist/state-graph.js +255 -0
- package/dist/status-bar.d.ts +54 -0
- package/dist/status-bar.js +184 -0
- package/dist/thinking-display.d.ts +21 -0
- package/dist/thinking-display.js +37 -0
- package/dist/tool-summary.d.ts +4 -0
- package/dist/tool-summary.js +67 -0
- package/dist/tools/index.d.ts +925 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/knowledge.d.ts +140 -0
- package/dist/tools/knowledge.js +260 -0
- package/dist/tools/memory.d.ts +39 -0
- package/dist/tools/memory.js +300 -0
- package/dist/tools/search-code.d.ts +134 -0
- package/dist/tools/search-code.js +390 -0
- package/dist/tools/system.d.ts +16 -0
- package/dist/tools/system.js +499 -0
- package/dist/tools/tool-def.d.ts +6 -0
- package/dist/tools/tool-def.js +3 -0
- package/dist/turn-control.d.ts +51 -0
- package/dist/turn-control.js +210 -0
- package/dist/turn.d.ts +20 -0
- package/dist/turn.js +624 -0
- package/dist/types.d.ts +233 -0
- package/dist/types.js +4 -0
- package/dist/ui/readline-ui.d.ts +2 -0
- package/dist/ui/readline-ui.js +176 -0
- package/dist/ui/tui/app.d.ts +13 -0
- package/dist/ui/tui/app.js +270 -0
- package/dist/ui/tui/busy-indicator.d.ts +2 -0
- package/dist/ui/tui/busy-indicator.js +13 -0
- package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
- package/dist/ui/tui/components/gutter-rule.js +9 -0
- package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
- package/dist/ui/tui/components/inline-tool-row.js +8 -0
- package/dist/ui/tui/components/prompt-input.d.ts +20 -0
- package/dist/ui/tui/components/prompt-input.js +120 -0
- package/dist/ui/tui/components/system-line.d.ts +5 -0
- package/dist/ui/tui/components/system-line.js +6 -0
- package/dist/ui/tui/components/thinking-block.d.ts +11 -0
- package/dist/ui/tui/components/thinking-block.js +31 -0
- package/dist/ui/tui/components/toast-line.d.ts +4 -0
- package/dist/ui/tui/components/toast-line.js +8 -0
- package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
- package/dist/ui/tui/components/tool-result-line.js +6 -0
- package/dist/ui/tui/components/turn-footer.d.ts +5 -0
- package/dist/ui/tui/components/turn-footer.js +7 -0
- package/dist/ui/tui/components/user-block.d.ts +6 -0
- package/dist/ui/tui/components/user-block.js +6 -0
- package/dist/ui/tui/logo-banner.d.ts +5 -0
- package/dist/ui/tui/logo-banner.js +8 -0
- package/dist/ui/tui/markdown-render.d.ts +16 -0
- package/dist/ui/tui/markdown-render.js +218 -0
- package/dist/ui/tui/palette.d.ts +12 -0
- package/dist/ui/tui/palette.js +13 -0
- package/dist/ui/tui/reasoning-summary.d.ts +12 -0
- package/dist/ui/tui/reasoning-summary.js +27 -0
- package/dist/ui/tui/reducer.d.ts +92 -0
- package/dist/ui/tui/reducer.js +260 -0
- package/dist/ui/tui/run.d.ts +3 -0
- package/dist/ui/tui/run.js +40 -0
- package/dist/ui/tui/sink.d.ts +4 -0
- package/dist/ui/tui/sink.js +89 -0
- package/dist/ui/tui/status-bar-view.d.ts +5 -0
- package/dist/ui/tui/status-bar-view.js +44 -0
- package/dist/ui/tui/terminal-height.d.ts +12 -0
- package/dist/ui/tui/terminal-height.js +20 -0
- package/dist/ui/tui/terminal-width.d.ts +2 -0
- package/dist/ui/tui/terminal-width.js +5 -0
- package/dist/ui/tui/tool-display.d.ts +23 -0
- package/dist/ui/tui/tool-display.js +217 -0
- package/dist/ui/tui/transcript-line.d.ts +12 -0
- package/dist/ui/tui/transcript-line.js +43 -0
- package/dist/ui/tui/transcript-replay.d.ts +12 -0
- package/dist/ui/tui/transcript-replay.js +117 -0
- package/dist/ui-events.d.ts +39 -0
- package/dist/ui-events.js +33 -0
- package/dist/ui.d.ts +77 -0
- package/dist/ui.js +179 -0
- package/package.json +73 -0
- 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
|
+
};
|