@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc263",
3
+ "version": "0.6.0-rc265",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -80,7 +80,7 @@
80
80
  "@anthropic-ai/claude-agent-sdk": "^0.1.46",
81
81
  "@modelcontextprotocol/sdk": "^1.0.0",
82
82
  "@nyariv/sandboxjs": "github:probelabs/SandboxJS",
83
- "@probelabs/maid": "^0.0.25",
83
+ "@probelabs/maid": "^0.0.26",
84
84
  "acorn": "^8.15.0",
85
85
  "acorn-walk": "^8.3.4",
86
86
  "adm-zip": "^0.5.16",
@@ -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
- child.kill('SIGTERM');
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
- child.kill('SIGKILL');
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
- child.kill('SIGTERM');
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
- child.kill('SIGTERM');
380
+ killProcessGroup('SIGTERM');
155
381
  }
156
382
  }
157
383
  });
@@ -87,6 +87,7 @@ If the solution is clear, you can jump to implementation right away. If not, ask
87
87
  - Avoid implementing special cases when a general approach works
88
88
  - Never expose secrets, API keys, or credentials in generated code. Never log sensitive information.
89
89
  - Do not surprise the user with unrequested changes. Do what was asked, including reasonable follow-up actions, but do not refactor surrounding code or add features that were not requested.
90
+ - When editing files, keep edits focused and minimal. For changes spanning more than a few lines, prefer line-targeted editing (start_line/end_line) over text replacement (old_string) — it constrains scope and prevents accidental removal of adjacent content. Never include unrelated sections in an edit operation.
90
91
  - After every significant change, verify the project still builds and passes linting. Do not wait until the end to discover breakage.
91
92
 
92
93
  # After Implementation
@@ -97,11 +98,33 @@ If the solution is clear, you can jump to implementation right away. If not, ask
97
98
 
98
99
  # GitHub Integration
99
100
  - Use the \`gh\` CLI for all GitHub operations: issues, pull requests, checks, releases.
100
- - To create a pull request: commit your changes, push the branch, then use \`gh pr create --title "..." --body "..."\`.
101
101
  - To view issues or PRs: \`gh issue view <number>\`, \`gh pr view <number>\`.
102
102
  - If given a GitHub URL, use \`gh\` to fetch the relevant information rather than guessing.
103
103
  - Always return the pull request URL to the user after creating one.
104
- - When checking GitHub Actions, only read logs of failed jobs — do not waste time on successful ones. Use \`gh run view <run-id> --log-failed\` to fetch only the relevant output.`,
104
+ - When checking GitHub Actions, only read logs of failed jobs — do not waste time on successful ones. Use \`gh run view <run-id> --log-failed\` to fetch only the relevant output.
105
+
106
+ # Pull Request Creation
107
+ - Commit your changes, push the branch, then use \`gh pr create --title "..." --body "..."\`.
108
+ - **PR title**: Keep it short (under 72 characters). Use imperative mood describing the change (e.g. "Add retry logic for API calls", "Fix race condition in cache invalidation"). Prefix with the type of change when useful: \`fix:\`, \`feat:\`, \`refactor:\`, \`docs:\`, \`test:\`, \`chore:\`.
109
+ - **PR body**: MUST follow this structure:
110
+
111
+ \`\`\`
112
+ ## Problem / Task
113
+ <What problem is being solved or what task was requested. If there is a linked issue, reference it with #number. Be specific about the root cause or motivation.>
114
+
115
+ ## Changes
116
+ <Concise list of what was actually changed. Describe each meaningful change — files modified, logic added/removed, and why. Do NOT just list filenames; explain what each change does.>
117
+
118
+ ## Testing
119
+ <What tests were added, modified, or run. Include:
120
+ - New test names and what they verify
121
+ - Whether existing tests still pass
122
+ - Manual verification steps if applicable
123
+ - Commands used to validate (e.g. \`make test\`, \`npm test\`)>
124
+ \`\`\`
125
+
126
+ - If the task originated from a GitHub issue, always reference it in the PR body (e.g. "Fixes #123" or "Closes #123") so the issue is automatically closed on merge. If it originated from an external ticket system (Jira, Linear, etc.), include the ticket ID and link in the Problem / Task section (e.g. "Resolves PROJ-456").
127
+ - Do not leave the PR body empty or vague. Every PR must clearly communicate what was done and why so reviewers can understand the change without reading every line of diff.`,
105
128
 
106
129
  'support': `You are ProbeChat Support, a specialized AI assistant focused on helping developers troubleshoot issues and solve problems. Your primary function is to help users diagnose errors, understand unexpected behaviors, and find solutions using the provided code analysis tools.
107
130
 
package/src/tools/edit.js CHANGED
@@ -448,6 +448,13 @@ Parameters:
448
448
  return `Error editing file: Multiple occurrences found - the old_string appears ${occurrences} times in ${file_path}. To fix: (1) Set replace_all=true to replace all occurrences, or (2) Include more surrounding lines in old_string to make the match unique (add the full line or adjacent lines for context).`;
449
449
  }
450
450
 
451
+ // Guard against over-scoped text edits (replacing large blocks with tiny replacements)
452
+ const oldLines = matchTarget.split('\n').length;
453
+ const newLines = new_string.split('\n').length;
454
+ if (oldLines >= 20 && newLines < oldLines * 0.5) {
455
+ return `Error editing file: Edit scope too large — replacing ${oldLines} lines with ${newLines} lines risks accidental content deletion. To fix: (1) Use line-targeted editing (start_line/end_line) instead to constrain scope, or (2) Split into smaller, focused edits that each target only the lines you intend to change.`;
456
+ }
457
+
451
458
  // Perform the replacement
452
459
  let newContent;
453
460
  if (replace_all) {