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,499 @@
|
|
|
1
|
+
import { defineTool } from "./tool-def.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, realpathSync, rmSync, } from "node:fs";
|
|
5
|
+
import { dirname, resolve, isAbsolute, extname, normalize } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import * as toml from "toml";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { writeUiStderr } from "../ui.js";
|
|
11
|
+
/**
|
|
12
|
+
* Validate content for known structured formats.
|
|
13
|
+
* Returns a warning string if the content looks malformed, or null if fine.
|
|
14
|
+
*/
|
|
15
|
+
function validateStructuredContent(filePath, content) {
|
|
16
|
+
const ext = extname(filePath).toLowerCase();
|
|
17
|
+
if (ext === ".json") {
|
|
18
|
+
try {
|
|
19
|
+
JSON.parse(content);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return `Warning: content does not parse as valid JSON (${e.message}). File written anyway.`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (ext === ".toml") {
|
|
26
|
+
try {
|
|
27
|
+
toml.parse(content);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return `Warning: content does not parse as valid TOML (${e.message}). File written anyway.`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
export function createSystemTools(ctx) {
|
|
36
|
+
const { cwd, getAbortSignal, sandbox, editConfirm } = ctx;
|
|
37
|
+
const resolvePath = (p) => {
|
|
38
|
+
if (isAbsolute(p))
|
|
39
|
+
return p;
|
|
40
|
+
return resolve(cwd, p);
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
shell: defineTool({
|
|
44
|
+
description: "Execute a shell command in the working directory. Returns stdout, stderr, and exit code.",
|
|
45
|
+
parameters: z.object({
|
|
46
|
+
command: z.string().describe("Shell command to execute"),
|
|
47
|
+
timeout: z
|
|
48
|
+
.number()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Timeout in milliseconds (default 30000)"),
|
|
51
|
+
}),
|
|
52
|
+
execute: async ({ command, timeout }) => {
|
|
53
|
+
const signal = getAbortSignal?.();
|
|
54
|
+
if (signal?.aborted) {
|
|
55
|
+
return { ok: false, stdout: "", stderr: "Interrupted", exitCode: 130 };
|
|
56
|
+
}
|
|
57
|
+
// Sandbox validation
|
|
58
|
+
if (sandbox?.enabled) {
|
|
59
|
+
const dangerousPatterns = [
|
|
60
|
+
/\bsudo\b/,
|
|
61
|
+
/\brm\b.*-r.*\//,
|
|
62
|
+
/\brm\b.*-f.*\//,
|
|
63
|
+
/\bmkfs\b/,
|
|
64
|
+
/\bdd\b.*if=/,
|
|
65
|
+
/\bdd\b.*of=/,
|
|
66
|
+
/\bshutdown\b/,
|
|
67
|
+
/\breboot\b/,
|
|
68
|
+
/\bhalt\b/,
|
|
69
|
+
/\bpoweroff\b/,
|
|
70
|
+
/\bfdisk\b/,
|
|
71
|
+
/\bparted\b/,
|
|
72
|
+
/\bwipefs\b/,
|
|
73
|
+
/\bcryptsetup\b/,
|
|
74
|
+
/\bchmod\b.*-R.*777.*\//,
|
|
75
|
+
/\bchown\b.*-R.*\//,
|
|
76
|
+
/\>\s*\/dev\/sd[a-z]/,
|
|
77
|
+
/\:\(\)\{\s*:\|\&\s*\};/,
|
|
78
|
+
];
|
|
79
|
+
for (const pattern of dangerousPatterns) {
|
|
80
|
+
if (pattern.test(command)) {
|
|
81
|
+
return { ok: false, stdout: "", stderr: "Blocked by sandbox: dangerous command detected", exitCode: 1 };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (sandbox.allowed_paths.length > 0) {
|
|
85
|
+
const pathPattern = /(?:["']([^"']+)["']|(\/[\w./-]+|~\/[\w./-]+))/g;
|
|
86
|
+
let match;
|
|
87
|
+
while ((match = pathPattern.exec(command)) !== null) {
|
|
88
|
+
const rawPath = match[1] ?? match[2];
|
|
89
|
+
if (!rawPath)
|
|
90
|
+
continue;
|
|
91
|
+
const expanded = rawPath.replace(/^~/, homedir());
|
|
92
|
+
const normalized = normalize(expanded);
|
|
93
|
+
let resolved;
|
|
94
|
+
try {
|
|
95
|
+
resolved = existsSync(normalized) ? realpathSync(normalized) : normalized;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
resolved = normalized;
|
|
99
|
+
}
|
|
100
|
+
const isAllowed = sandbox.allowed_paths.some(ap => {
|
|
101
|
+
const apExpanded = ap.replace(/^~/, homedir());
|
|
102
|
+
const apNormalized = normalize(apExpanded);
|
|
103
|
+
let apResolved;
|
|
104
|
+
try {
|
|
105
|
+
apResolved = existsSync(apNormalized) ? realpathSync(apNormalized) : apNormalized;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
apResolved = apNormalized;
|
|
109
|
+
}
|
|
110
|
+
return resolved === apResolved || resolved.startsWith(apResolved + "/");
|
|
111
|
+
});
|
|
112
|
+
if (!isAllowed) {
|
|
113
|
+
return { ok: false, stdout: "", stderr: `Blocked by sandbox: path not in allowed list: ${rawPath}`, exitCode: 1 };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const ms = timeout ?? 30000;
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const child = spawn(command, [], {
|
|
121
|
+
cwd,
|
|
122
|
+
shell: "/bin/bash",
|
|
123
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
124
|
+
});
|
|
125
|
+
let stdout = "";
|
|
126
|
+
let stderr = "";
|
|
127
|
+
let settled = false;
|
|
128
|
+
const maxBuf = 10 * 1024 * 1024; // 10MB
|
|
129
|
+
const finish = (result) => {
|
|
130
|
+
if (settled)
|
|
131
|
+
return;
|
|
132
|
+
settled = true;
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
signal?.removeEventListener("abort", onAbort);
|
|
135
|
+
resolve(result);
|
|
136
|
+
};
|
|
137
|
+
const onAbort = () => {
|
|
138
|
+
child.kill("SIGTERM");
|
|
139
|
+
setTimeout(() => child.kill("SIGKILL"), 3000);
|
|
140
|
+
};
|
|
141
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
child.kill("SIGTERM");
|
|
144
|
+
setTimeout(() => child.kill("SIGKILL"), 3000);
|
|
145
|
+
}, ms);
|
|
146
|
+
child.stdout?.on("data", (chunk) => {
|
|
147
|
+
// Stream raw output to terminal so long-running commands show progress.
|
|
148
|
+
// ANSI escape sequences from child processes pass through unmodified —
|
|
149
|
+
// this matches standard terminal multiplexer behavior (script(1), tee).
|
|
150
|
+
process.stdout.write(chunk);
|
|
151
|
+
if (stdout.length < maxBuf)
|
|
152
|
+
stdout += chunk.toString();
|
|
153
|
+
});
|
|
154
|
+
child.stderr?.on("data", (chunk) => {
|
|
155
|
+
process.stderr.write(chunk);
|
|
156
|
+
if (stderr.length < maxBuf)
|
|
157
|
+
stderr += chunk.toString();
|
|
158
|
+
});
|
|
159
|
+
child.on("close", (code) => {
|
|
160
|
+
if (signal?.aborted) {
|
|
161
|
+
finish({
|
|
162
|
+
ok: false,
|
|
163
|
+
stdout: stdout.slice(0, maxBuf),
|
|
164
|
+
stderr: stderr.slice(0, maxBuf) || "Interrupted",
|
|
165
|
+
exitCode: 130,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
finish({
|
|
170
|
+
ok: code === 0,
|
|
171
|
+
stdout: stdout.slice(0, maxBuf),
|
|
172
|
+
stderr: stderr.slice(0, maxBuf),
|
|
173
|
+
exitCode: code ?? 1,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
child.on("error", (err) => {
|
|
177
|
+
finish({
|
|
178
|
+
ok: false,
|
|
179
|
+
stdout,
|
|
180
|
+
stderr: err.message,
|
|
181
|
+
exitCode: 1,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
read_file: defineTool({
|
|
188
|
+
description: "Read contents of a file. Supports optional offset and limit for partial reads.",
|
|
189
|
+
parameters: z.object({
|
|
190
|
+
path: z.string().describe("File path (relative to working dir or absolute)"),
|
|
191
|
+
offset: z
|
|
192
|
+
.number()
|
|
193
|
+
.optional()
|
|
194
|
+
.describe("Line number to start reading from (1-indexed)"),
|
|
195
|
+
limit: z
|
|
196
|
+
.number()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe("Maximum lines to read"),
|
|
199
|
+
}),
|
|
200
|
+
execute: async ({ path, offset, limit }) => {
|
|
201
|
+
const absPath = resolvePath(path);
|
|
202
|
+
try {
|
|
203
|
+
if (!existsSync(absPath)) {
|
|
204
|
+
return { ok: false, error: `File not found: ${path}` };
|
|
205
|
+
}
|
|
206
|
+
const content = readFileSync(absPath, "utf-8");
|
|
207
|
+
let lines = content.split("\n");
|
|
208
|
+
if (offset !== undefined) {
|
|
209
|
+
lines = lines.slice(offset - 1);
|
|
210
|
+
}
|
|
211
|
+
if (limit !== undefined) {
|
|
212
|
+
lines = lines.slice(0, limit);
|
|
213
|
+
}
|
|
214
|
+
return { ok: true, content: lines.join("\n") };
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
return { ok: false, error: err?.message ?? "Failed to read file" };
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
read_and_summarize: defineTool({
|
|
222
|
+
description: "Read a file and return a structured summary: key exports, imports/dependencies, and basic metrics. Use instead of read_file when you need an overview of a file.",
|
|
223
|
+
parameters: z.object({
|
|
224
|
+
path: z.string().describe("File path (relative to working dir or absolute)"),
|
|
225
|
+
}),
|
|
226
|
+
execute: async ({ path }) => {
|
|
227
|
+
const absPath = resolvePath(path);
|
|
228
|
+
try {
|
|
229
|
+
if (!existsSync(absPath)) {
|
|
230
|
+
return { ok: false, error: `File not found: ${path}` };
|
|
231
|
+
}
|
|
232
|
+
const content = readFileSync(absPath, "utf-8");
|
|
233
|
+
const lines = content.split("\n");
|
|
234
|
+
// Extract exports (declarations + named exports)
|
|
235
|
+
const exports = [];
|
|
236
|
+
const exportDeclPattern = /export\s+(?:default\s+)?(?:function|class|const|let|var|interface|type)\s+(\w+)/g;
|
|
237
|
+
let match;
|
|
238
|
+
while ((match = exportDeclPattern.exec(content)) !== null) {
|
|
239
|
+
exports.push(match[1]);
|
|
240
|
+
}
|
|
241
|
+
// Named exports: export { foo, bar } or export { foo } from './mod'
|
|
242
|
+
const exportNamedPattern = /export\s*\{([^}]+)\}/g;
|
|
243
|
+
while ((match = exportNamedPattern.exec(content)) !== null) {
|
|
244
|
+
const names = match[1].split(",").map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
|
|
245
|
+
exports.push(...names);
|
|
246
|
+
}
|
|
247
|
+
// Extract imports (import statements + require calls)
|
|
248
|
+
const imports = [];
|
|
249
|
+
const importFromPattern = /import\s+.*?\s+from\s+["']([^"']+)["']/g;
|
|
250
|
+
while ((match = importFromPattern.exec(content)) !== null) {
|
|
251
|
+
imports.push(match[1]);
|
|
252
|
+
}
|
|
253
|
+
// Side-effect imports: import "foo"
|
|
254
|
+
const sideEffectPattern = /^import\s+["']([^"']+)["']/gm;
|
|
255
|
+
while ((match = sideEffectPattern.exec(content)) !== null) {
|
|
256
|
+
imports.push(match[1]);
|
|
257
|
+
}
|
|
258
|
+
// require("foo")
|
|
259
|
+
const requirePattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
260
|
+
while ((match = requirePattern.exec(content)) !== null) {
|
|
261
|
+
imports.push(match[1]);
|
|
262
|
+
}
|
|
263
|
+
// Extract function declarations (named functions + arrow functions)
|
|
264
|
+
const functions = [];
|
|
265
|
+
const funcPattern = /(?:async\s+)?function\s+(\w+)/g;
|
|
266
|
+
while ((match = funcPattern.exec(content)) !== null) {
|
|
267
|
+
functions.push(match[1]);
|
|
268
|
+
}
|
|
269
|
+
// Arrow functions: const name = async () =>
|
|
270
|
+
const arrowFuncPattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(/g;
|
|
271
|
+
while ((match = arrowFuncPattern.exec(content)) !== null) {
|
|
272
|
+
functions.push(match[1]);
|
|
273
|
+
}
|
|
274
|
+
// Function expressions: const name = async function
|
|
275
|
+
const funcExprPattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function/g;
|
|
276
|
+
while ((match = funcExprPattern.exec(content)) !== null) {
|
|
277
|
+
functions.push(match[1]);
|
|
278
|
+
}
|
|
279
|
+
// Check for concerns (large files, many TODOs, many exports)
|
|
280
|
+
const concerns = [];
|
|
281
|
+
if (lines.length > 500)
|
|
282
|
+
concerns.push(`Large file: ${lines.length} lines`);
|
|
283
|
+
const todoCount = (content.match(/TODO|FIXME|HACK/gi) ?? []).length;
|
|
284
|
+
if (todoCount > 3)
|
|
285
|
+
concerns.push(`Many TODOs/FIXMEs: ${todoCount}`);
|
|
286
|
+
if (exports.length > 15)
|
|
287
|
+
concerns.push(`Many exports: ${exports.length}`);
|
|
288
|
+
return {
|
|
289
|
+
ok: true,
|
|
290
|
+
path,
|
|
291
|
+
lines: lines.length,
|
|
292
|
+
exports,
|
|
293
|
+
functions,
|
|
294
|
+
imports,
|
|
295
|
+
concerns,
|
|
296
|
+
contentPreview: lines.slice(0, 20).join("\n"),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
return { ok: false, error: err?.message ?? "Failed to summarize file" };
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
write_file: defineTool({
|
|
305
|
+
description: "Create or overwrite a file. Creates parent directories if needed.",
|
|
306
|
+
parameters: z.object({
|
|
307
|
+
path: z.string().describe("File path (relative to working dir or absolute)"),
|
|
308
|
+
content: z.string().describe("Content to write"),
|
|
309
|
+
}),
|
|
310
|
+
execute: async ({ path, content }) => {
|
|
311
|
+
const absPath = resolvePath(path);
|
|
312
|
+
try {
|
|
313
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
314
|
+
writeFileSync(absPath, content);
|
|
315
|
+
const warning = validateStructuredContent(absPath, content);
|
|
316
|
+
return warning ? { ok: true, warning } : { ok: true };
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
return { ok: false, error: err?.message ?? "Failed to write file" };
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
edit_file: defineTool({
|
|
324
|
+
description: "Replace a specific text block in a file. Finds the exact oldText and replaces with newText. Fails if oldText is not unique in the file.",
|
|
325
|
+
parameters: z.object({
|
|
326
|
+
path: z.string().describe("File path (relative to working dir or absolute)"),
|
|
327
|
+
oldText: z.string().describe("Exact text to find and replace"),
|
|
328
|
+
newText: z.string().describe("Replacement text"),
|
|
329
|
+
}),
|
|
330
|
+
execute: async ({ path, oldText, newText }) => {
|
|
331
|
+
const absPath = resolvePath(path);
|
|
332
|
+
try {
|
|
333
|
+
if (!existsSync(absPath)) {
|
|
334
|
+
return { ok: false, error: `File not found: ${path}` };
|
|
335
|
+
}
|
|
336
|
+
const content = readFileSync(absPath, "utf-8");
|
|
337
|
+
// Exact match via indexOf — no regex, handles all special chars
|
|
338
|
+
const idx = content.indexOf(oldText);
|
|
339
|
+
if (idx === -1) {
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
error: "oldText not found in file. Make sure the text matches exactly.",
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (content.indexOf(oldText, idx + 1) !== -1) {
|
|
346
|
+
const count = content.split(oldText).length - 1;
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
error: `oldText found ${count} times in file. Must be unique. Provide more context to make it unique.`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const newContent = content.slice(0, idx) + newText + content.slice(idx + oldText.length);
|
|
353
|
+
// Show diff and prompt for confirmation if editConfirm is enabled
|
|
354
|
+
if (editConfirm) {
|
|
355
|
+
const matchLine = content.slice(0, idx).split("\n").length;
|
|
356
|
+
const oldLines = oldText.split("\n");
|
|
357
|
+
const newLines = newText.split("\n");
|
|
358
|
+
writeUiStderr(chalk.dim(`\n--- ${path}:${matchLine} (before)`));
|
|
359
|
+
for (const line of oldLines)
|
|
360
|
+
writeUiStderr(chalk.red(`- ${line}`));
|
|
361
|
+
writeUiStderr(chalk.dim(`+++ ${path}:${matchLine} (after)`));
|
|
362
|
+
for (const line of newLines)
|
|
363
|
+
writeUiStderr(chalk.green(`+ ${line}`));
|
|
364
|
+
const answer = await new Promise((resolve) => {
|
|
365
|
+
const rl = createInterface({
|
|
366
|
+
input: process.stdin,
|
|
367
|
+
output: process.stderr,
|
|
368
|
+
});
|
|
369
|
+
rl.question("Apply edit? [y/N] ", (ans) => {
|
|
370
|
+
rl.close();
|
|
371
|
+
resolve(ans.trim().toLowerCase());
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
if (answer !== "y" && answer !== "yes") {
|
|
375
|
+
return { ok: false, error: "Edit cancelled by user" };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
writeFileSync(absPath, newContent);
|
|
379
|
+
return { ok: true };
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
return { ok: false, error: err?.message ?? "Failed to edit file" };
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
}),
|
|
386
|
+
batch_write: defineTool({
|
|
387
|
+
description: "Write multiple files atomically. All files are written or none. Creates parent directories as needed. Use for creating multi-file components in one call.",
|
|
388
|
+
parameters: z.object({
|
|
389
|
+
files: z.array(z.object({
|
|
390
|
+
path: z.string().describe("File path"),
|
|
391
|
+
content: z.string().describe("File content"),
|
|
392
|
+
})).describe("Array of files to write"),
|
|
393
|
+
}),
|
|
394
|
+
execute: async ({ files }) => {
|
|
395
|
+
// Validate all paths first (all must resolve)
|
|
396
|
+
const resolved = [];
|
|
397
|
+
for (const f of files) {
|
|
398
|
+
const absPath = resolvePath(f.path);
|
|
399
|
+
resolved.push({ absPath, content: f.content, relPath: f.path });
|
|
400
|
+
}
|
|
401
|
+
// Write all files, tracking what was written for rollback
|
|
402
|
+
const written = [];
|
|
403
|
+
const originals = new Map();
|
|
404
|
+
try {
|
|
405
|
+
for (const { absPath, content, relPath } of resolved) {
|
|
406
|
+
// Save original if file exists (for rollback)
|
|
407
|
+
if (existsSync(absPath)) {
|
|
408
|
+
originals.set(absPath, readFileSync(absPath, "utf-8"));
|
|
409
|
+
}
|
|
410
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
411
|
+
writeFileSync(absPath, content);
|
|
412
|
+
written.push(relPath);
|
|
413
|
+
}
|
|
414
|
+
return { ok: true, files: written };
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
// Rollback: restore originals, delete newly created files
|
|
418
|
+
for (const { absPath } of resolved) {
|
|
419
|
+
if (originals.has(absPath)) {
|
|
420
|
+
try {
|
|
421
|
+
writeFileSync(absPath, originals.get(absPath));
|
|
422
|
+
}
|
|
423
|
+
catch { /* best-effort */ }
|
|
424
|
+
}
|
|
425
|
+
else if (written.some((r) => resolvePath(r) === absPath)) {
|
|
426
|
+
try {
|
|
427
|
+
rmSync(absPath);
|
|
428
|
+
}
|
|
429
|
+
catch { /* best-effort */ }
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return { ok: false, error: err?.message ?? "Batch write failed", written };
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
}),
|
|
436
|
+
batch_edit: defineTool({
|
|
437
|
+
description: "Edit multiple files atomically. All edits are applied or none. Edits to the same file are applied sequentially (each edit sees the result of the previous one). Edits across different files are independent. Use for multi-file refactors in one call.",
|
|
438
|
+
parameters: z.object({
|
|
439
|
+
edits: z.array(z.object({
|
|
440
|
+
path: z.string().describe("File path"),
|
|
441
|
+
oldText: z.string().describe("Exact text to find"),
|
|
442
|
+
newText: z.string().describe("Replacement text"),
|
|
443
|
+
})).describe("Array of edits to apply"),
|
|
444
|
+
}),
|
|
445
|
+
execute: async ({ edits }) => {
|
|
446
|
+
if (edits.length === 0) {
|
|
447
|
+
return { ok: true, files: [] };
|
|
448
|
+
}
|
|
449
|
+
// Resolve paths and verify files exist
|
|
450
|
+
const resolvedEdits = [];
|
|
451
|
+
for (const e of edits) {
|
|
452
|
+
const absPath = resolvePath(e.path);
|
|
453
|
+
if (!existsSync(absPath)) {
|
|
454
|
+
return { ok: false, error: `File not found: ${e.path}` };
|
|
455
|
+
}
|
|
456
|
+
resolvedEdits.push({ absPath, oldText: e.oldText, newText: e.newText, relPath: e.path });
|
|
457
|
+
}
|
|
458
|
+
// Snapshot original contents for rollback
|
|
459
|
+
const snapshots = new Map();
|
|
460
|
+
const workingContents = new Map();
|
|
461
|
+
for (const { absPath } of resolvedEdits) {
|
|
462
|
+
if (!snapshots.has(absPath)) {
|
|
463
|
+
const original = readFileSync(absPath, "utf-8");
|
|
464
|
+
snapshots.set(absPath, original);
|
|
465
|
+
workingContents.set(absPath, original);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const edited = [];
|
|
469
|
+
try {
|
|
470
|
+
for (const { absPath, oldText, newText, relPath } of resolvedEdits) {
|
|
471
|
+
const content = workingContents.get(absPath);
|
|
472
|
+
const idx = content.indexOf(oldText);
|
|
473
|
+
if (idx === -1) {
|
|
474
|
+
throw new Error(`oldText not found in ${relPath}`);
|
|
475
|
+
}
|
|
476
|
+
if (content.indexOf(oldText, idx + 1) !== -1) {
|
|
477
|
+
throw new Error(`oldText not unique in ${relPath}`);
|
|
478
|
+
}
|
|
479
|
+
workingContents.set(absPath, content.slice(0, idx) + newText + content.slice(idx + oldText.length));
|
|
480
|
+
edited.push(relPath);
|
|
481
|
+
}
|
|
482
|
+
for (const [absPath, content] of workingContents) {
|
|
483
|
+
writeFileSync(absPath, content);
|
|
484
|
+
}
|
|
485
|
+
return { ok: true, files: edited };
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
for (const [absPath, original] of snapshots) {
|
|
489
|
+
try {
|
|
490
|
+
writeFileSync(absPath, original);
|
|
491
|
+
}
|
|
492
|
+
catch { /* best-effort */ }
|
|
493
|
+
}
|
|
494
|
+
return { ok: false, error: err?.message ?? "Batch edit failed", edited };
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
}),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
/** Max gap between Esc presses to count as a double-tap interrupt. */
|
|
3
|
+
export declare const ESC_INTERRUPT_WINDOW_MS = 600;
|
|
4
|
+
/** Delay before treating a lone ESC byte as a keypress (not an arrow/function prefix). */
|
|
5
|
+
export declare const ESC_SEQUENCE_DEFER_MS = 30;
|
|
6
|
+
export declare function nextEscapeInterruptState(lastEscAt: number, now: number, windowMs?: number): {
|
|
7
|
+
lastEscAt: number;
|
|
8
|
+
triggered: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** Skip an ANSI/SS3 escape sequence starting at `start` (on the ESC byte). */
|
|
11
|
+
export declare function skipEscapeSequence(chunk: Buffer, start: number): number;
|
|
12
|
+
export declare function chunkContainsCtrlC(chunk: Buffer): boolean;
|
|
13
|
+
export declare function consumeEscapeBytes(chunk: Buffer, lastEscAt: number, now: number, windowMs?: number): {
|
|
14
|
+
lastEscAt: number;
|
|
15
|
+
triggered: boolean;
|
|
16
|
+
deferred: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** Listens for Esc Esc while a turn is running. */
|
|
19
|
+
export declare class EscInterruptListener {
|
|
20
|
+
private lastEscAt;
|
|
21
|
+
private dataHandler;
|
|
22
|
+
private active;
|
|
23
|
+
private priorRawMode;
|
|
24
|
+
private rl;
|
|
25
|
+
private savedWrite;
|
|
26
|
+
private deferTimer;
|
|
27
|
+
private onInterrupt;
|
|
28
|
+
start(onInterrupt: () => void, rl?: readline.Interface): void;
|
|
29
|
+
private handleData;
|
|
30
|
+
private consumeAnsiContinuation;
|
|
31
|
+
private clearDeferTimer;
|
|
32
|
+
stop(): void;
|
|
33
|
+
}
|
|
34
|
+
/** Tracks and aborts the currently running turn (LLM stream / tool execution). */
|
|
35
|
+
export declare class TurnController {
|
|
36
|
+
private controller;
|
|
37
|
+
private active;
|
|
38
|
+
/** Start a new turn; aborts any previous in-flight turn. */
|
|
39
|
+
begin(): AbortSignal;
|
|
40
|
+
/** Whether a turn lifecycle is open (between begin() and end()). */
|
|
41
|
+
isActive(): boolean;
|
|
42
|
+
/** Abort the in-flight turn. No-op if already aborted or ended. */
|
|
43
|
+
abort(): void;
|
|
44
|
+
get signal(): AbortSignal | undefined;
|
|
45
|
+
get inProgress(): boolean;
|
|
46
|
+
end(): void;
|
|
47
|
+
}
|
|
48
|
+
export declare class TurnAbortedError extends Error {
|
|
49
|
+
readonly partialResponse: string;
|
|
50
|
+
constructor(partialResponse?: string);
|
|
51
|
+
}
|