@probelabs/probe 0.6.0-rc263 → 0.6.0-rc265
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/bin/binaries/probe-v0.6.0-rc265-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc265-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc265-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc265-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc265-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +1 -0
- package/build/agent/bashExecutor.js +233 -7
- package/build/agent/index.js +244 -67
- package/build/agent/shared/prompts.js +25 -2
- package/build/tools/edit.js +7 -0
- package/cjs/agent/ProbeAgent.cjs +410 -80
- package/cjs/index.cjs +410 -80
- package/package.json +2 -2
- package/src/agent/ProbeAgent.js +1 -0
- package/src/agent/bashExecutor.js +233 -7
- package/src/agent/shared/prompts.js +25 -2
- package/src/tools/edit.js +7 -0
- package/bin/binaries/probe-v0.6.0-rc263-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc263-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc263-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc263-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc263-x86_64-unknown-linux-musl.tar.gz +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2771,6 +2771,7 @@ Follow these instructions carefully:
|
|
|
2771
2771
|
* For rewriting entire functions/classes/methods, use the symbol parameter instead (no exact text matching needed).
|
|
2772
2772
|
* For editing specific lines from search/extract output, use start_line (and optionally end_line) with the line numbers shown in the output.${this.hashLines ? ' Line references include content hashes (e.g. "42:ab") for integrity verification.' : ''}
|
|
2773
2773
|
* For editing inside large functions: first use extract with the symbol target (e.g. "file.js#myFunction") to see the function with line numbers${this.hashLines ? ' and hashes' : ''}, then use start_line/end_line to surgically edit specific lines within it.
|
|
2774
|
+
* IMPORTANT: Keep old_string as small as possible — include only the lines you need to change plus minimal context for uniqueness. For replacing large blocks (10+ lines), prefer line-targeted editing with start_line/end_line to constrain scope.
|
|
2774
2775
|
- Use 'create' for new files or complete file rewrites.
|
|
2775
2776
|
- If an edit fails, read the error message — it tells you exactly how to fix the call and retry.
|
|
2776
2777
|
- The system tracks which files you've seen via search/extract. If you try to edit a file you haven't read, or one that changed since you last read it, the edit will fail with instructions to re-read first. Always use extract before editing to ensure you have current file content.` : ''}
|
|
@@ -8,6 +8,191 @@ import { resolve, join } from 'path';
|
|
|
8
8
|
import { existsSync } from 'fs';
|
|
9
9
|
import { parseCommandForExecution, isComplexCommand } from './bashCommandUtils.js';
|
|
10
10
|
|
|
11
|
+
// ─── Interactive Command Detection ─────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Split a command string by shell operators (&&, ||, |, ;) while respecting quotes.
|
|
15
|
+
* Used for interactive command detection in complex pipelines.
|
|
16
|
+
* @param {string} command - Command string to split
|
|
17
|
+
* @returns {string[]} Array of individual command strings
|
|
18
|
+
*/
|
|
19
|
+
function splitCommandComponents(command) {
|
|
20
|
+
const parts = [];
|
|
21
|
+
let current = '';
|
|
22
|
+
let inQuote = false;
|
|
23
|
+
let quoteChar = '';
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < command.length; i++) {
|
|
26
|
+
const c = command[i];
|
|
27
|
+
const next = command[i + 1] || '';
|
|
28
|
+
|
|
29
|
+
// Handle escape sequences
|
|
30
|
+
if (c === '\\' && !inQuote) {
|
|
31
|
+
current += c + next;
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (inQuote && quoteChar === '"' && c === '\\' && next) {
|
|
36
|
+
current += c + next;
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Track quotes
|
|
42
|
+
if (!inQuote && (c === '"' || c === "'")) {
|
|
43
|
+
inQuote = true;
|
|
44
|
+
quoteChar = c;
|
|
45
|
+
current += c;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (inQuote && c === quoteChar) {
|
|
49
|
+
inQuote = false;
|
|
50
|
+
current += c;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Split on operators outside quotes
|
|
55
|
+
if (!inQuote) {
|
|
56
|
+
if ((c === '&' && next === '&') || (c === '|' && next === '|')) {
|
|
57
|
+
if (current.trim()) parts.push(current.trim());
|
|
58
|
+
current = '';
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (c === '|' || c === ';') {
|
|
63
|
+
if (current.trim()) parts.push(current.trim());
|
|
64
|
+
current = '';
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
current += c;
|
|
70
|
+
}
|
|
71
|
+
if (current.trim()) parts.push(current.trim());
|
|
72
|
+
return parts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check a single (non-compound) command for interactive behavior.
|
|
77
|
+
* Strips leading env-var assignments (e.g. GIT_EDITOR=true) before checking.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} command - Single command string
|
|
80
|
+
* @returns {string|null} Error message with suggestion, or null if not interactive
|
|
81
|
+
*/
|
|
82
|
+
function checkSingleCommandInteractive(command) {
|
|
83
|
+
let effective = command.trim();
|
|
84
|
+
|
|
85
|
+
// Strip leading VAR=VALUE prefixes (e.g. "GIT_EDITOR=true git rebase --continue")
|
|
86
|
+
while (/^\w+=\S*\s/.test(effective)) {
|
|
87
|
+
effective = effective.replace(/^\w+=\S*\s+/, '');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const parts = effective.split(/\s+/);
|
|
91
|
+
const base = parts[0];
|
|
92
|
+
const args = parts.slice(1);
|
|
93
|
+
|
|
94
|
+
// ── Interactive editors ──
|
|
95
|
+
if (['vi', 'vim', 'nvim', 'nano', 'emacs', 'pico', 'joe', 'mcedit'].includes(base)) {
|
|
96
|
+
return `'${base}' is an interactive editor and cannot run without a terminal. Use non-interactive file manipulation commands instead.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Interactive pagers ──
|
|
100
|
+
if (['less', 'more'].includes(base)) {
|
|
101
|
+
return `'${base}' is an interactive pager. Use 'cat', 'head', or 'tail' instead.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Git commands that open an editor ──
|
|
105
|
+
if (base === 'git') {
|
|
106
|
+
const sub = args[0];
|
|
107
|
+
|
|
108
|
+
// git commit without -m / --message / -C / -c / --fixup / --squash / --no-edit
|
|
109
|
+
if (sub === 'commit') {
|
|
110
|
+
const hasNonInteractiveFlag = args.some(a =>
|
|
111
|
+
a === '-m' || a.startsWith('--message') ||
|
|
112
|
+
a === '-C' || a === '-c' ||
|
|
113
|
+
a.startsWith('--fixup') || a.startsWith('--squash') ||
|
|
114
|
+
a === '--allow-empty-message' || a === '--no-edit'
|
|
115
|
+
);
|
|
116
|
+
if (!hasNonInteractiveFlag) {
|
|
117
|
+
return "Interactive command: 'git commit' opens an editor for the commit message. Use 'git commit -m \"your message\"' instead.";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// git rebase --continue / --skip (opens editor for commit message)
|
|
122
|
+
if (sub === 'rebase' && (args.includes('--continue') || args.includes('--skip'))) {
|
|
123
|
+
return "Interactive command: 'git rebase --continue' opens an editor. Set environment variable GIT_EDITOR=true to accept default messages, e.g. pass env: {GIT_EDITOR: 'true'} or prepend GIT_EDITOR=true to the command.";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// git rebase -i / --interactive
|
|
127
|
+
if (sub === 'rebase' && (args.includes('-i') || args.includes('--interactive'))) {
|
|
128
|
+
return "Interactive command: 'git rebase -i' requires an interactive editor. Interactive rebase cannot run without a terminal.";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// git merge without --no-edit / --no-commit / --ff-only
|
|
132
|
+
if (sub === 'merge' && !args.includes('--no-edit') && !args.includes('--no-commit') && !args.includes('--ff-only')) {
|
|
133
|
+
return "Interactive command: 'git merge' may open an editor for the merge commit message. Add '--no-edit' to accept the default message.";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// git cherry-pick without --no-edit
|
|
137
|
+
if (sub === 'cherry-pick' && !args.includes('--no-edit')) {
|
|
138
|
+
return "Interactive command: 'git cherry-pick' may open an editor. Add '--no-edit' to accept the default message.";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// git revert without --no-edit
|
|
142
|
+
if (sub === 'revert' && !args.includes('--no-edit')) {
|
|
143
|
+
return "Interactive command: 'git revert' opens an editor. Add '--no-edit' to accept the default message.";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// git tag -a without -m
|
|
147
|
+
if (sub === 'tag' && args.includes('-a') && !args.some(a => a === '-m' || a.startsWith('--message'))) {
|
|
148
|
+
return "Interactive command: 'git tag -a' opens an editor for the tag message. Use 'git tag -a <name> -m \"message\"' instead.";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// git add -i / --interactive / -p / --patch
|
|
152
|
+
if (sub === 'add' && (args.includes('-i') || args.includes('--interactive') || args.includes('-p') || args.includes('--patch'))) {
|
|
153
|
+
return "Interactive command: 'git add -i/-p' requires interactive input. Use 'git add <files>' to stage specific files instead.";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Interactive REPLs (no arguments = interactive mode) ──
|
|
158
|
+
if (['python', 'python3', 'node', 'irb', 'ghci', 'lua', 'R', 'ruby'].includes(base) && args.length === 0) {
|
|
159
|
+
return `Interactive command: '${base}' without arguments starts an interactive REPL. Provide a script file or use '-c'/'--eval' for inline code.`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Database clients without query flag ──
|
|
163
|
+
if (base === 'mysql' && !args.some(a => a === '-e' || a.startsWith('--execute'))) {
|
|
164
|
+
return "Interactive command: 'mysql' without -e flag starts an interactive session. Use 'mysql -e \"SQL QUERY\"' instead.";
|
|
165
|
+
}
|
|
166
|
+
if (base === 'psql' && !args.some(a => a === '-c' || a.startsWith('--command') || a === '-f' || a.startsWith('--file'))) {
|
|
167
|
+
return "Interactive command: 'psql' without -c flag starts an interactive session. Use 'psql -c \"SQL QUERY\"' instead.";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Interactive TUI tools ──
|
|
171
|
+
if (['top', 'htop', 'btop', 'nmon'].includes(base)) {
|
|
172
|
+
return `Interactive command: '${base}' is an interactive TUI tool. Use 'ps aux' or 'top -b -n 1' for non-interactive process listing.`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if a command (simple or complex) would require interactive TTY input.
|
|
180
|
+
* For complex commands (with &&, ||, |, ;), checks each component individually.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} command - Full command string
|
|
183
|
+
* @returns {string|null} Error message with suggestion for non-interactive alternative, or null if OK
|
|
184
|
+
*/
|
|
185
|
+
export function checkInteractiveCommand(command) {
|
|
186
|
+
if (!command || typeof command !== 'string') return null;
|
|
187
|
+
|
|
188
|
+
const components = splitCommandComponents(command.trim());
|
|
189
|
+
for (const component of components) {
|
|
190
|
+
const result = checkSingleCommandInteractive(component);
|
|
191
|
+
if (result) return result;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
11
196
|
/**
|
|
12
197
|
* Execute a bash command with security controls
|
|
13
198
|
* @param {string} command - Command to execute
|
|
@@ -50,6 +235,26 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
50
235
|
|
|
51
236
|
const startTime = Date.now();
|
|
52
237
|
|
|
238
|
+
// Check for interactive commands that would hang without a TTY
|
|
239
|
+
const interactiveError = checkInteractiveCommand(command);
|
|
240
|
+
if (interactiveError) {
|
|
241
|
+
if (debug) {
|
|
242
|
+
console.log(`[BashExecutor] Blocked interactive command: "${command}"`);
|
|
243
|
+
console.log(`[BashExecutor] Reason: ${interactiveError}`);
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
error: interactiveError,
|
|
248
|
+
stdout: '',
|
|
249
|
+
stderr: interactiveError,
|
|
250
|
+
exitCode: 1,
|
|
251
|
+
command,
|
|
252
|
+
workingDirectory: cwd,
|
|
253
|
+
duration: 0,
|
|
254
|
+
interactive: true
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
53
258
|
if (debug) {
|
|
54
259
|
console.log(`[BashExecutor] Executing command: "${command}"`);
|
|
55
260
|
console.log(`[BashExecutor] Working directory: "${cwd}"`);
|
|
@@ -57,11 +262,16 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
57
262
|
}
|
|
58
263
|
|
|
59
264
|
return new Promise((resolve, reject) => {
|
|
60
|
-
// Create environment
|
|
265
|
+
// Create environment with non-interactive safety defaults.
|
|
266
|
+
// These prevent commands from opening editors or TTY prompts
|
|
267
|
+
// when stdin is not available (which would cause hangs).
|
|
61
268
|
const processEnv = {
|
|
62
269
|
...process.env,
|
|
63
270
|
...env
|
|
64
271
|
};
|
|
272
|
+
// Only set defaults if not already provided by user config
|
|
273
|
+
if (!processEnv.GIT_EDITOR) processEnv.GIT_EDITOR = 'true';
|
|
274
|
+
if (!processEnv.GIT_TERMINAL_PROMPT) processEnv.GIT_TERMINAL_PROMPT = '0';
|
|
65
275
|
|
|
66
276
|
// Check if this is a complex command (contains pipes, operators, etc.)
|
|
67
277
|
const isComplex = isComplexCommand(command);
|
|
@@ -97,12 +307,17 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
97
307
|
useShell = false;
|
|
98
308
|
}
|
|
99
309
|
|
|
100
|
-
// Spawn the process
|
|
310
|
+
// Spawn the process in a new session (detached: true → setsid on Linux).
|
|
311
|
+
// This detaches the child from the parent's controlling terminal, making
|
|
312
|
+
// /dev/tty unavailable. Any program that tries to open an interactive
|
|
313
|
+
// editor or TTY prompt (e.g. vim from git rebase) will get ENXIO and
|
|
314
|
+
// fail immediately instead of hanging forever.
|
|
101
315
|
const child = spawn(cmd, cmdArgs, {
|
|
102
316
|
cwd,
|
|
103
317
|
env: processEnv,
|
|
104
318
|
stdio: ['ignore', 'pipe', 'pipe'], // stdin ignored, capture stdout/stderr
|
|
105
319
|
shell: useShell, // false for security
|
|
320
|
+
detached: true, // new session — no controlling terminal
|
|
106
321
|
windowsHide: true
|
|
107
322
|
});
|
|
108
323
|
|
|
@@ -111,17 +326,28 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
111
326
|
let killed = false;
|
|
112
327
|
let timeoutHandle;
|
|
113
328
|
|
|
329
|
+
// Helper: kill the entire process group (negative PID) so that
|
|
330
|
+
// sub-processes spawned by the command (e.g. an editor) are also killed.
|
|
331
|
+
// Falls back to killing just the child if process.kill fails.
|
|
332
|
+
const killProcessGroup = (signal) => {
|
|
333
|
+
try {
|
|
334
|
+
if (child.pid) process.kill(-child.pid, signal);
|
|
335
|
+
} catch {
|
|
336
|
+
try { child.kill(signal); } catch { /* already dead */ }
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
114
340
|
// Set timeout
|
|
115
341
|
if (timeout > 0) {
|
|
116
342
|
timeoutHandle = setTimeout(() => {
|
|
117
343
|
if (!killed) {
|
|
118
344
|
killed = true;
|
|
119
|
-
|
|
120
|
-
|
|
345
|
+
killProcessGroup('SIGTERM');
|
|
346
|
+
|
|
121
347
|
// Force kill after 5 seconds if still running
|
|
122
348
|
setTimeout(() => {
|
|
123
349
|
if (child.exitCode === null) {
|
|
124
|
-
|
|
350
|
+
killProcessGroup('SIGKILL');
|
|
125
351
|
}
|
|
126
352
|
}, 5000);
|
|
127
353
|
}
|
|
@@ -137,7 +363,7 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
137
363
|
// Buffer overflow
|
|
138
364
|
if (!killed) {
|
|
139
365
|
killed = true;
|
|
140
|
-
|
|
366
|
+
killProcessGroup('SIGTERM');
|
|
141
367
|
}
|
|
142
368
|
}
|
|
143
369
|
});
|
|
@@ -151,7 +377,7 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
151
377
|
// Buffer overflow
|
|
152
378
|
if (!killed) {
|
|
153
379
|
killed = true;
|
|
154
|
-
|
|
380
|
+
killProcessGroup('SIGTERM');
|
|
155
381
|
}
|
|
156
382
|
}
|
|
157
383
|
});
|