@bahulam/code 2.6.13 → 2.6.15

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": "@bahulam/code",
3
- "version": "2.6.13",
3
+ "version": "2.6.15",
4
4
  "description": "Bahulam Code \u2014 abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,8 +49,8 @@
49
49
  "web-tree-sitter": "^0.26.9"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@bahulam/runtime-darwin-arm64": "2.6.13",
53
- "@bahulam/runtime-linux-x64": "2.6.13",
54
- "@bahulam/runtime-win32-x64": "2.6.13"
52
+ "@bahulam/runtime-darwin-arm64": "2.6.15",
53
+ "@bahulam/runtime-linux-x64": "2.6.15",
54
+ "@bahulam/runtime-win32-x64": "2.6.15"
55
55
  }
56
56
  }
@@ -204,6 +204,7 @@ export async function syncAgentsToBackend({
204
204
  headers: {
205
205
  Authorization: `Bearer ${token}`,
206
206
  'Content-Type': 'application/json',
207
+ 'X-Product': 'bahulam',
207
208
  },
208
209
  body: JSON.stringify(payload),
209
210
  signal: AbortSignal.timeout(timeoutMs),
@@ -59,7 +59,7 @@ function getAuth() {
59
59
  const auth = new TarangAuth();
60
60
  const creds = auth.loadCredentials();
61
61
  if (!creds.token) {
62
- process.stderr.write(`${RED}✗ Not authenticated. Run `bahulam-code login` first.${RESET}\n`);
62
+ process.stderr.write(`${RED}✗ Not authenticated. Run bahulam-code login first.${RESET}\n`);
63
63
  process.exit(1);
64
64
  }
65
65
  return creds;
@@ -76,13 +76,14 @@ async function apiFetch(path, options = {}) {
76
76
  const headers = {
77
77
  'Authorization': `Bearer ${creds.token}`,
78
78
  'Content-Type': 'application/json',
79
+ 'X-Product': 'bahulam',
79
80
  ...options.headers,
80
81
  };
81
82
  const url = `${baseUrl}${path}`;
82
83
  const response = await fetch(url, { ...options, headers });
83
84
 
84
85
  if (response.status === 401) {
85
- process.stderr.write(`${RED}✗ Authentication failed. Run `bahulam-code login` to re-authenticate.${RESET}\n`);
86
+ process.stderr.write(`${RED}✗ Authentication failed. Run bahulam-code login to re-authenticate.${RESET}\n`);
86
87
  process.exit(1);
87
88
  }
88
89
 
@@ -1,7 +1,11 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { isSensitiveConfigPath } from './safety.mjs';
3
4
 
4
5
  const REDACTED = 'REDACTED';
6
+ const MAX_ARG_LOG_CHARS = 4000;
7
+ const MAX_PROMPT_SUBJECT_CHARS = 4000;
8
+ const MAX_PROMPT_REASON_CHARS = 1200;
5
9
  const SENSITIVE_KEY_RE = /^(?:authorization|api[-_]?key|apikey|key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key)$|(?:^|[-_])(?:api[-_]?key|apikey|token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key)(?:$|[-_])/i;
6
10
  const SENSITIVE_ASSIGNMENT_KEY = String.raw`[A-Z0-9_-]*(?:API[-_]?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|ACCESS[-_]?KEY|SECRET[-_]?ACCESS[-_]?KEY)[A-Z0-9_-]*`;
7
11
  const SENSITIVE_JSON_KEY = String.raw`(?:authorization|api[-_]?key|apikey|key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key|[A-Z0-9_-]*(?:API[-_]?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|ACCESS[-_]?KEY|SECRET[-_]?ACCESS[-_]?KEY)[A-Z0-9_-]*)`;
@@ -48,10 +52,32 @@ function sanitizeForLog(value, depth = 0) {
48
52
  }
49
53
 
50
54
  function safeArgs(args) {
51
- if (args?.command) return redactSensitive(String(args.command)).slice(0, 500);
52
- if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0, 500);
53
- try { return JSON.stringify(sanitizeForLog(args || {})).slice(0, 500); }
54
- catch { return redactSensitive(String(args || '')).slice(0, 500); }
55
+ if (args?.command) return redactSensitive(String(args.command)).slice(0, MAX_ARG_LOG_CHARS);
56
+ if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0, MAX_ARG_LOG_CHARS);
57
+ if (Array.isArray(args?.files)) {
58
+ try {
59
+ const { files, ...rest } = args;
60
+ return JSON.stringify({
61
+ ...sanitizeForLog(rest),
62
+ files: files.slice(0, 100).map(file => sanitizeProjectFileArg(file)),
63
+ }).slice(0, MAX_ARG_LOG_CHARS);
64
+ } catch {
65
+ return '[files omitted]';
66
+ }
67
+ }
68
+ try { return JSON.stringify(sanitizeForLog(args || {})).slice(0, MAX_ARG_LOG_CHARS); }
69
+ catch { return redactSensitive(String(args || '')).slice(0, MAX_ARG_LOG_CHARS); }
70
+ }
71
+
72
+ function sanitizeProjectFileArg(file) {
73
+ if (typeof file === 'string') return redactSensitive(file);
74
+ if (!file || typeof file !== 'object') return sanitizeForLog(file);
75
+ const out = sanitizeForLog(file);
76
+ const filePath = file.path || file.file_path || '';
77
+ if (isSensitiveConfigPath(filePath) && typeof out.content === 'string') {
78
+ out.content = '[redacted sensitive config]';
79
+ }
80
+ return out;
55
81
  }
56
82
 
57
83
  function safeText(value, max = 500) {
@@ -59,6 +85,19 @@ function safeText(value, max = 500) {
59
85
  return redactSensitive(String(value)).slice(0, max);
60
86
  }
61
87
 
88
+ function safePrompt(prompt) {
89
+ if (!prompt || typeof prompt !== 'object') return undefined;
90
+ const out = {
91
+ title: safeText(prompt.title, 200),
92
+ subject: safeText(prompt.subject, MAX_PROMPT_SUBJECT_CHARS),
93
+ reason: safeText(prompt.reason, MAX_PROMPT_REASON_CHARS),
94
+ };
95
+ for (const key of Object.keys(out)) {
96
+ if (out[key] === undefined) delete out[key];
97
+ }
98
+ return Object.keys(out).length ? out : undefined;
99
+ }
100
+
62
101
  export class ApprovalLog {
63
102
  constructor({ cwd = process.cwd() } = {}) {
64
103
  this.cwd = cwd;
@@ -68,6 +107,7 @@ export class ApprovalLog {
68
107
  append(entry) {
69
108
  try {
70
109
  fs.mkdirSync(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
110
+ const prompt = safePrompt(entry.prompt);
71
111
  const line = JSON.stringify({
72
112
  ts: new Date().toISOString(),
73
113
  tier: entry.tier,
@@ -77,6 +117,7 @@ export class ApprovalLog {
77
117
  scope: entry.scope || 'once',
78
118
  rule_id: entry.rule_id || null,
79
119
  reason: safeText(entry.reason),
120
+ ...(prompt ? { prompt } : {}),
80
121
  });
81
122
  fs.appendFileSync(this.filePath, line + '\n', { mode: 0o600 });
82
123
  try { fs.chmodSync(this.filePath, 0o600); } catch {}
@@ -10,7 +10,7 @@
10
10
  * Shell commands are risk-assessed (safe/medium/high).
11
11
  */
12
12
 
13
- import { shellCommandDisplay, toolDisplaySummary } from '../terminal/tool-display.mjs';
13
+ import { shellCommandDisplay, shellCommandProfile, toolDisplaySummary } from '../terminal/tool-display.mjs';
14
14
  import {
15
15
  classify as classifyTier,
16
16
  TIERS,
@@ -20,11 +20,12 @@ import {
20
20
  } from './risk-tier.mjs';
21
21
  import {
22
22
  renderApprovalPrompt,
23
+ renderApprovalDockPrompt,
23
24
  renderTrustedApproval,
24
25
  defaultOptions as approvalOptions,
25
26
  } from '../ui/approval.mjs';
26
- import { isInputDockMounted, moveToContent } from '../ui/input-dock.mjs';
27
- import { validateShellCommand } from './safety.mjs';
27
+ import { clearInputPrompt, isInputDockMounted, moveToContent, renderDockOverlay } from '../ui/input-dock.mjs';
28
+ import { isApprovalRequiredWritePath, isSensitiveConfigPath, validateShellCommand } from './safety.mjs';
28
29
  import { classifyCommand } from '../permissions/command-classifier.mjs';
29
30
  import { ApprovalLog } from './approval-log.mjs';
30
31
  import { TrustStore } from './trust.mjs';
@@ -45,9 +46,19 @@ const WRITE_TOOLS = new Set([
45
46
 
46
47
  function defaultWhy(tier, tool, args) {
47
48
  const subject = approvalSummary(tool, args);
49
+ const protectedTarget = protectedWriteTarget(tool, args);
50
+ if (protectedTarget) {
51
+ const target = protectedTarget.path;
52
+ if (protectedTarget.sensitive) {
53
+ return `Edits sensitive environment config (${target}). Confirm before writing; values stay redacted in summaries and diffs.`;
54
+ }
55
+ return `Edits protected project file (${subject || target}). Confirm before writing.`;
56
+ }
48
57
  switch (tier) {
49
58
  case TIERS.SENSITIVE_READ:
50
59
  return `Reads a sensitive path (${subject || 'secret-like file'}). Confirm before exposing its contents to the agent.`;
60
+ case TIERS.PROTECTED_EDIT:
61
+ return `Edits a protected project file. Confirm before writing.`;
51
62
  case TIERS.SHELL_DANGEROUS:
52
63
  return `Shell command matches a high-risk pattern (rm -rf, sudo, force push, etc.). Confirm before running.`;
53
64
  case TIERS.DESTRUCTIVE:
@@ -61,6 +72,26 @@ function defaultWhy(tier, tool, args) {
61
72
  }
62
73
  }
63
74
 
75
+ function protectedWriteTarget(tool, args = {}) {
76
+ const paths = writeTargetPaths(tool, args);
77
+ const path = paths.find(isApprovalRequiredWritePath);
78
+ if (!path) return null;
79
+ return { path, sensitive: isSensitiveConfigPath(path) };
80
+ }
81
+
82
+ function writeTargetPaths(tool, args = {}) {
83
+ const key = String(tool || '').toLowerCase();
84
+ if (key === 'write_file' || key === 'edit_file') {
85
+ return [args.file_path || args.path].filter(Boolean);
86
+ }
87
+ if (key === 'write_project') {
88
+ return (args.files || [])
89
+ .map(file => typeof file === 'string' ? file : file?.path || file?.file_path)
90
+ .filter(Boolean);
91
+ }
92
+ return [];
93
+ }
94
+
64
95
  function approvalSummary(tool, args = {}) {
65
96
  const summary = toolDisplaySummary(tool, args);
66
97
  if (tool !== 'shell') return summary;
@@ -72,12 +103,22 @@ function shellHardBlockReason(tool, args = {}) {
72
103
  if (tool !== 'shell') return '';
73
104
  const command = args.command || args.cmd || '';
74
105
  const safety = validateShellCommand(command);
75
- if (!safety.safe) return safety.reason || 'Blocked by shell safety policy';
106
+ if (!safety.safe) return withShellRetryHint(safety.reason || 'Blocked by shell safety policy');
76
107
  const classification = classifyCommand(command);
77
- if (classification.classification === 'blocked') return classification.reason || 'Blocked by shell safety policy';
108
+ if (classification.classification === 'blocked') {
109
+ return withShellRetryHint(classification.reason || 'Blocked by shell safety policy');
110
+ }
78
111
  return '';
79
112
  }
80
113
 
114
+ function withShellRetryHint(reason) {
115
+ const text = String(reason || '').trim();
116
+ if (/command substitution|backticks|\$\(\)/i.test(text)) {
117
+ return `${text}. Retry with separate simple shell commands instead of backticks or $().`;
118
+ }
119
+ return text;
120
+ }
121
+
81
122
  // ── ANSI helpers ──
82
123
 
83
124
  const RST = '\x1b[0m';
@@ -107,13 +148,39 @@ export class ApprovalManager {
107
148
  this.history = [];
108
149
  this.rejectionHints = [];
109
150
  this._rl = null;
151
+ this._approvalPromptActive = false;
152
+ this._approvalPromptWasRaw = false;
110
153
  }
111
154
 
112
155
  setReadline(rl) { this._rl = rl; }
113
156
 
114
- setExecutionHooks({ onPause, onResume } = {}) {
157
+ setExecutionHooks({ onPause, onResume, onApprovalPromptEnd } = {}) {
115
158
  this._execPause = onPause || null;
116
159
  this._execResume = onResume || null;
160
+ this._approvalPromptEnd = onApprovalPromptEnd || null;
161
+ }
162
+
163
+ _beginApprovalInput() {
164
+ if (this._approvalPromptActive || !process.stdin.isTTY) return false;
165
+ this._approvalPromptActive = true;
166
+ this._approvalPromptWasRaw = process.stdin.isRaw;
167
+ if (this._execPause) this._execPause();
168
+ if (this._rl) this._rl.pause();
169
+ if (typeof process.stdin.setRawMode === 'function') {
170
+ process.stdin.setRawMode(true);
171
+ }
172
+ process.stdin.resume();
173
+ return true;
174
+ }
175
+
176
+ _endApprovalInput() {
177
+ if (!this._approvalPromptActive) return;
178
+ if (typeof process.stdin.setRawMode === 'function') {
179
+ process.stdin.setRawMode(this._approvalPromptWasRaw || false);
180
+ }
181
+ if (this._rl) this._rl.resume();
182
+ if (this._execResume) this._execResume();
183
+ this._approvalPromptActive = false;
117
184
  }
118
185
 
119
186
  revoke() {
@@ -143,7 +210,8 @@ export class ApprovalManager {
143
210
  const reason = `Blocked by safety policy: ${hardBlock}`;
144
211
  this.history.push({ tool: toolName, decision: 'safety-block', tier, time: Date.now(), reason });
145
212
  this.approvalLog.append({ tool: toolName, args, tier, decision: 'safety_block', scope: 'none', reason });
146
- write(` ${RED}✗${RST} ${DIM}${reason}${RST}\n\n`);
213
+ if (isInputDockMounted()) moveToContent();
214
+ writeSafetyBlock(reason);
147
215
  return { approved: false, tier, reason, blocked: true, code: 'safety_block' };
148
216
  }
149
217
 
@@ -203,25 +271,48 @@ export class ApprovalManager {
203
271
  async _prompt(toolName, args, context = {}) {
204
272
  const tier = context.tier || classifyTier(toolName, args);
205
273
  const why = context.reason || context.why || defaultWhy(tier, toolName, args);
206
- const options = this._optionsFor(tier);
274
+ const options = this._optionsFor(tier, toolName, args);
207
275
 
208
276
  let selected = 0; // arrow-driven cursor
209
277
  let printedHeight = 0;
278
+ let showDetails = false;
279
+
280
+ const isInteractive = process.stdin.isTTY;
210
281
 
211
- // Approval must render ABOVE the fixed input dock (in the scrollable
212
- // content area) so it doesn't collide with the "+ add instruction"
213
- // prompt. Move cursor into content once; the in-place redraw below
214
- // stays within that region because printedHeight tracks the anchor.
215
- if (isInputDockMounted()) moveToContent();
282
+ // In rich TTY mode approval lives in the fixed input dock, replacing
283
+ // "+ add instruction" until the user decides. Fallback/plain mode
284
+ // still renders in the transcript.
285
+ const useDockPrompt = isInteractive && isInputDockMounted();
286
+ if (!useDockPrompt && isInputDockMounted()) moveToContent();
216
287
 
217
288
  // For TTYs we redraw in place on every arrow key so the prompt feels
218
289
  // live. For non-TTYs / pipes we just print once and read a line.
219
- const isInteractive = process.stdin.isTTY;
220
290
  if (!isInteractive) {
221
- write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options }) + '\n');
291
+ write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options, showDetails }) + '\n');
222
292
  }
223
293
 
224
294
  const drawPrompt = () => {
295
+ if (useDockPrompt) {
296
+ const dock = renderApprovalDockPrompt({
297
+ tool: toolName,
298
+ args,
299
+ tier,
300
+ why,
301
+ selected,
302
+ options,
303
+ showDetails,
304
+ width: process.stderr.columns || process.stdout.columns || 96,
305
+ });
306
+ renderDockOverlay({
307
+ context: dock.context,
308
+ lines: dock.lines || [`${dock.prefix || ''}${dock.value || ''}`],
309
+ meta: dock.meta,
310
+ tips: dock.tips,
311
+ maxRows: dock.maxRows,
312
+ });
313
+ printedHeight = 0;
314
+ return;
315
+ }
225
316
  // Move up over the previous render before re-printing. Use a
226
317
  // bounded per-line clear instead of \x1b[J — a full clear-to-end
227
318
  // would wipe the input dock's bottom rule and tips row that live
@@ -234,11 +325,14 @@ export class ApprovalManager {
234
325
  }
235
326
  write(`\x1b[${printedHeight}F`); // back to the anchor row
236
327
  }
237
- const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options });
328
+ const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options, showDetails });
238
329
  write(block + '\n');
239
330
  printedHeight = block.split('\n').length;
240
331
  };
241
332
 
333
+ const ownsApprovalInput = isInteractive ? this._beginApprovalInput() : false;
334
+
335
+ try {
242
336
  if (isInteractive) drawPrompt();
243
337
 
244
338
  // ── Input loop ─────────────────────────────────────────────────
@@ -266,6 +360,11 @@ export class ApprovalManager {
266
360
  // Letter shortcut: match against option.key
267
361
  if (typeof k === 'string' && k.length === 1) {
268
362
  const lower = k.toLowerCase();
363
+ if (lower === 'd' && toolName === 'shell') {
364
+ showDetails = !showDetails;
365
+ if (isInteractive) drawPrompt();
366
+ continue;
367
+ }
269
368
  const idx = options.findIndex(o => o.key === lower);
270
369
  if (idx >= 0) {
271
370
  selected = idx;
@@ -278,28 +377,35 @@ export class ApprovalManager {
278
377
  };
279
378
 
280
379
  const value = await choose();
380
+ if (useDockPrompt) {
381
+ clearInputPrompt();
382
+ if (this._approvalPromptEnd) this._approvalPromptEnd();
383
+ moveToContent();
384
+ }
385
+ const prompt = promptForLog(toolName, args, tier, why);
281
386
 
282
387
  switch (value) {
283
388
  case 'approve':
284
389
  this.history.push({ tool: toolName, decision: 'yes', tier, time: Date.now() });
285
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once' });
390
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once', prompt });
391
+ writeApprovalConfirmation(toolName, args, 'approved once');
286
392
  return { approved: true, tier };
287
393
 
288
394
  case 'allow-session': {
289
- if (!this.policy.hitl?.allowSessionTrust) return this._prompt(toolName, args, context);
395
+ if (!this.policy.hitl?.allowSessionTrust) return await this._prompt(toolName, args, context);
290
396
  const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'SESSION' });
291
397
  write(` ${GREEN}✓${RST} ${DIM}trusted for this session: ${rule.pattern}${RST}\n\n`);
292
398
  this.history.push({ tool: toolName, decision: 'session-trust', tier, time: Date.now(), rule_id: rule.id });
293
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id });
399
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id, prompt });
294
400
  return { approved: true, tier, scope: 'SESSION', rule_id: rule.id };
295
401
  }
296
402
 
297
403
  case 'allow-project': {
298
- if (!this.policy.hitl?.allowProjectTrust) return this._prompt(toolName, args, context);
404
+ if (!this.policy.hitl?.allowProjectTrust) return await this._prompt(toolName, args, context);
299
405
  const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'PROJECT' });
300
406
  write(` ${GREEN}✓${RST} ${DIM}trusted for this project: ${rule.pattern}${RST}\n\n`);
301
407
  this.history.push({ tool: toolName, decision: 'project-trust', tier, time: Date.now(), rule_id: rule.id });
302
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id });
408
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id, prompt });
303
409
  return { approved: true, tier, scope: 'PROJECT', rule_id: rule.id };
304
410
  }
305
411
 
@@ -308,30 +414,31 @@ export class ApprovalManager {
308
414
  const reason = 'User stopped the command';
309
415
  write(` ${RED}✗${RST} ${DIM}stopped${RST}\n\n`);
310
416
  this.history.push({ tool: toolName, decision: 'no', tier, time: Date.now(), reason });
311
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason });
417
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason, prompt });
312
418
  this._rememberRejection({ tool: toolName, args, tier, decision: 'reject', reason, note: '' });
313
419
  return { approved: false, tier, reason };
314
420
  }
315
421
 
316
422
  case 'allow-all':
317
- if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
423
+ if (requiresExplicitApproval(tier)) return await this._prompt(toolName, args, context);
318
424
  this.approveAll = true;
319
425
  write(` ${GREEN}✓✓${RST} ${DIM}allow-all activated${RST}\n\n`);
320
426
  this.history.push({ tool: toolName, decision: 'approve-all', tier, time: Date.now() });
321
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session' });
427
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session', prompt });
322
428
  return { approved: true, tier };
323
429
 
324
430
  case 'allow-type':
325
- if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
431
+ if (requiresExplicitApproval(tier)) return await this._prompt(toolName, args, context);
326
432
  this.approvedToolTypes.add(toolName);
327
433
  this.history.push({ tool: toolName, decision: 'type-approve', tier, time: Date.now() });
328
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session' });
434
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session', prompt });
435
+ writeApprovalConfirmation(toolName, args, 'always allow');
329
436
  return { approved: true, tier };
330
437
 
331
438
  case 'why':
332
439
  write(`\n ${DIM}${(context.reason || why).slice(0, 400)}${RST}\n\n`);
333
440
  printedHeight = 0;
334
- return this._prompt(toolName, args, context);
441
+ return await this._prompt(toolName, args, context);
335
442
 
336
443
  case 'edit':
337
444
  case 'replan':
@@ -340,13 +447,16 @@ export class ApprovalManager {
340
447
  const reason = note ? `User asked to re-plan: ${note}` : 'User asked to re-plan';
341
448
  write(` ${YELLOW}↩${RST} ${DIM}${note ? `re-plan — ${truncateNote(note)}` : 'reject with hint — rework the plan'}${RST}\n\n`);
342
449
  this.history.push({ tool: toolName, decision: 'replan', tier, time: Date.now(), reason });
343
- this.approvalLog.append({ tool: toolName, args, tier, decision: 'replan', scope: 'once', reason });
450
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'replan', scope: 'once', reason, prompt });
344
451
  this._rememberRejection({ tool: toolName, args, tier, decision: 'replan', reason, note });
345
452
  return { approved: false, tier, reason };
346
453
  }
347
454
 
348
455
  default:
349
- return this._prompt(toolName, args, context);
456
+ return await this._prompt(toolName, args, context);
457
+ }
458
+ } finally {
459
+ if (ownsApprovalInput) this._endApprovalInput();
350
460
  }
351
461
  }
352
462
 
@@ -357,16 +467,25 @@ export class ApprovalManager {
357
467
  return;
358
468
  }
359
469
 
360
- if (this._execPause) this._execPause();
361
- if (this._rl) this._rl.pause();
470
+ const managesInput = !this._approvalPromptActive;
471
+ if (managesInput) {
472
+ if (this._execPause) this._execPause();
473
+ if (this._rl) this._rl.pause();
474
+ }
362
475
 
363
476
  const wasRaw = process.stdin.isRaw;
364
- process.stdin.setRawMode(true);
477
+ if (managesInput && typeof process.stdin.setRawMode === 'function') {
478
+ process.stdin.setRawMode(true);
479
+ }
365
480
  process.stdin.resume();
366
481
  process.stdin.once('data', (data) => {
367
- process.stdin.setRawMode(wasRaw || false);
368
- if (this._rl) this._rl.resume();
369
- if (this._execResume) this._execResume();
482
+ if (managesInput) {
483
+ if (typeof process.stdin.setRawMode === 'function') {
484
+ process.stdin.setRawMode(wasRaw || false);
485
+ }
486
+ if (this._rl) this._rl.resume();
487
+ if (this._execResume) this._execResume();
488
+ }
370
489
 
371
490
  const bytes = [...data];
372
491
  const str = data.toString();
@@ -395,8 +514,11 @@ export class ApprovalManager {
395
514
  return;
396
515
  }
397
516
 
398
- if (this._execPause) this._execPause();
399
- if (this._rl) this._rl.pause();
517
+ const managesInput = !this._approvalPromptActive;
518
+ if (managesInput) {
519
+ if (this._execPause) this._execPause();
520
+ if (this._rl) this._rl.pause();
521
+ }
400
522
 
401
523
  const wasRaw = process.stdin.isRaw;
402
524
  if (typeof process.stdin.setRawMode === 'function') {
@@ -411,8 +533,10 @@ export class ApprovalManager {
411
533
  if (typeof process.stdin.setRawMode === 'function') {
412
534
  process.stdin.setRawMode(wasRaw || false);
413
535
  }
414
- if (this._rl) this._rl.resume();
415
- if (this._execResume) this._execResume();
536
+ if (managesInput) {
537
+ if (this._rl) this._rl.resume();
538
+ if (this._execResume) this._execResume();
539
+ }
416
540
  };
417
541
  const finish = () => {
418
542
  cleanup();
@@ -439,8 +563,8 @@ export class ApprovalManager {
439
563
  });
440
564
  }
441
565
 
442
- _optionsFor(tier) {
443
- return approvalOptions(tier);
566
+ _optionsFor(tier, toolName, args) {
567
+ return approvalOptions(tier, { tool: toolName, args });
444
568
  }
445
569
 
446
570
  getSummary() {
@@ -474,3 +598,103 @@ function truncateNote(note) {
474
598
  const text = String(note || '').trim();
475
599
  return text.length <= 120 ? text : text.slice(0, 119) + '…';
476
600
  }
601
+
602
+ function promptForLog(tool, args, tier, why) {
603
+ return {
604
+ title: `${approvalTitleForLog(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
605
+ subject: approvalSummary(tool, args),
606
+ reason: why || '',
607
+ };
608
+ }
609
+
610
+ function approvalTitleForLog(tier) {
611
+ switch (tier) {
612
+ case TIERS.SENSITIVE_READ:
613
+ case TIERS.PROTECTED_EDIT:
614
+ case TIERS.SHELL_DANGEROUS:
615
+ case TIERS.DESTRUCTIVE:
616
+ return 'EXPLICIT APPROVAL';
617
+ default:
618
+ return 'APPROVAL';
619
+ }
620
+ }
621
+
622
+ function writeApprovalConfirmation(tool, args, label) {
623
+ if (tool === 'shell') {
624
+ write(` ${GREEN}✓${RST} ${DIM}${label} · shell ${shellApprovalSubject(args)}${RST}\n`);
625
+ return;
626
+ }
627
+ const subject = approvalSummary(tool, args);
628
+ const suffix = subject ? ` · ${truncateNote(subject)}` : '';
629
+ write(` ${GREEN}✓${RST} ${DIM}${label}${suffix}${RST}\n\n`);
630
+ }
631
+
632
+ function shellApprovalSubject(args = {}) {
633
+ const profile = shellCommandProfile(args.command || args.cmd || '');
634
+ const subject = profile.compact ? profile.summary : profile.command;
635
+ return `$ ${truncateNote(subject)}`;
636
+ }
637
+
638
+ function writeSafetyBlock(reason) {
639
+ const cols = Math.max(40, Math.min(process.stderr.columns || process.stdout.columns || 80, 140));
640
+ const firstPrefix = ` ${RED}✗${RST} ${DIM}`;
641
+ const nextPrefix = ` ${DIM}`;
642
+ const firstWidth = Math.max(24, cols - 5);
643
+ const nextWidth = Math.max(24, cols - 5);
644
+ const lines = wrapSafetyText(reason, firstWidth, nextWidth);
645
+
646
+ write(`${firstPrefix}${lines[0] || ''}${RST}\n`);
647
+ for (const line of lines.slice(1)) {
648
+ write(`${nextPrefix}${line}${RST}\n`);
649
+ }
650
+ write('\n');
651
+ }
652
+
653
+ function wrapSafetyText(text, firstWidth, nextWidth) {
654
+ const words = String(text || '').replace(/\s+/g, ' ').trim().split(' ').filter(Boolean);
655
+ if (!words.length) return [''];
656
+
657
+ const lines = [];
658
+ let line = '';
659
+ let width = firstWidth;
660
+
661
+ for (const word of words) {
662
+ if (!line) {
663
+ if (word.length <= width) {
664
+ line = word;
665
+ } else {
666
+ const chunks = splitWord(word, width);
667
+ lines.push(...chunks.slice(0, -1));
668
+ line = chunks[chunks.length - 1] || '';
669
+ }
670
+ continue;
671
+ }
672
+
673
+ if (line.length + 1 + word.length <= width) {
674
+ line += ` ${word}`;
675
+ continue;
676
+ }
677
+
678
+ lines.push(line);
679
+ width = nextWidth;
680
+ if (word.length <= width) {
681
+ line = word;
682
+ } else {
683
+ const chunks = splitWord(word, width);
684
+ lines.push(...chunks.slice(0, -1));
685
+ line = chunks[chunks.length - 1] || '';
686
+ }
687
+ }
688
+
689
+ if (line) lines.push(line);
690
+ return lines;
691
+ }
692
+
693
+ function splitWord(word, width) {
694
+ const size = Math.max(8, width);
695
+ const chunks = [];
696
+ for (let i = 0; i < word.length; i += size) {
697
+ chunks.push(word.slice(i, i + size));
698
+ }
699
+ return chunks;
700
+ }
@@ -8,7 +8,7 @@ export function buildFileDiff({
8
8
  after = '',
9
9
  cwd = process.cwd(),
10
10
  context = 3,
11
- maxDiffLines = 400,
11
+ maxDiffLines = Infinity,
12
12
  } = {}) {
13
13
  const oldText = before == null ? '' : String(before);
14
14
  const newText = after == null ? '' : String(after);