@bahulam/code 2.6.12 → 2.6.14
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 +4 -4
- package/src/agents/scaffold.mjs +1 -0
- package/src/commands/agent.mjs +3 -2
- package/src/core/approval-log.mjs +22 -4
- package/src/core/approval.mjs +172 -24
- package/src/core/headless.mjs +14 -3
- package/src/core/local-agent.mjs +3 -2
- package/src/core/tool-executor.mjs +13 -3
- package/src/index.mjs +1 -1
- package/src/terminal/agents.mjs +194 -18
- package/src/terminal/repl-render.mjs +22 -4
- package/src/terminal/repl-state.mjs +1 -0
- package/src/terminal/repl.mjs +395 -21
- package/src/terminal/tool-display.mjs +135 -2
- package/src/ui/approval.mjs +200 -14
- package/src/ui/icons.mjs +11 -5
- package/src/ui/input-dock.mjs +90 -19
- package/src/ui/slash-commands.mjs +10 -0
- package/src/ui/tool-card.mjs +109 -21
- package/src/ui/tool-details.mjs +110 -11
- package/src/ui/transcript-block.mjs +2 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bahulam/code",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.14",
|
|
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.
|
|
53
|
-
"@bahulam/runtime-linux-x64": "2.6.
|
|
54
|
-
"@bahulam/runtime-win32-x64": "2.6.
|
|
52
|
+
"@bahulam/runtime-darwin-arm64": "2.6.14",
|
|
53
|
+
"@bahulam/runtime-linux-x64": "2.6.14",
|
|
54
|
+
"@bahulam/runtime-win32-x64": "2.6.14"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/agents/scaffold.mjs
CHANGED
package/src/commands/agent.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
|
@@ -2,6 +2,9 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
|
|
4
4
|
const REDACTED = 'REDACTED';
|
|
5
|
+
const MAX_ARG_LOG_CHARS = 4000;
|
|
6
|
+
const MAX_PROMPT_SUBJECT_CHARS = 4000;
|
|
7
|
+
const MAX_PROMPT_REASON_CHARS = 1200;
|
|
5
8
|
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
9
|
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
10
|
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 +51,10 @@ function sanitizeForLog(value, depth = 0) {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function safeArgs(args) {
|
|
51
|
-
if (args?.command) return redactSensitive(String(args.command)).slice(0,
|
|
52
|
-
if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0,
|
|
53
|
-
try { return JSON.stringify(sanitizeForLog(args || {})).slice(0,
|
|
54
|
-
catch { return redactSensitive(String(args || '')).slice(0,
|
|
54
|
+
if (args?.command) return redactSensitive(String(args.command)).slice(0, MAX_ARG_LOG_CHARS);
|
|
55
|
+
if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0, MAX_ARG_LOG_CHARS);
|
|
56
|
+
try { return JSON.stringify(sanitizeForLog(args || {})).slice(0, MAX_ARG_LOG_CHARS); }
|
|
57
|
+
catch { return redactSensitive(String(args || '')).slice(0, MAX_ARG_LOG_CHARS); }
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function safeText(value, max = 500) {
|
|
@@ -59,6 +62,19 @@ function safeText(value, max = 500) {
|
|
|
59
62
|
return redactSensitive(String(value)).slice(0, max);
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
function safePrompt(prompt) {
|
|
66
|
+
if (!prompt || typeof prompt !== 'object') return undefined;
|
|
67
|
+
const out = {
|
|
68
|
+
title: safeText(prompt.title, 200),
|
|
69
|
+
subject: safeText(prompt.subject, MAX_PROMPT_SUBJECT_CHARS),
|
|
70
|
+
reason: safeText(prompt.reason, MAX_PROMPT_REASON_CHARS),
|
|
71
|
+
};
|
|
72
|
+
for (const key of Object.keys(out)) {
|
|
73
|
+
if (out[key] === undefined) delete out[key];
|
|
74
|
+
}
|
|
75
|
+
return Object.keys(out).length ? out : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
export class ApprovalLog {
|
|
63
79
|
constructor({ cwd = process.cwd() } = {}) {
|
|
64
80
|
this.cwd = cwd;
|
|
@@ -68,6 +84,7 @@ export class ApprovalLog {
|
|
|
68
84
|
append(entry) {
|
|
69
85
|
try {
|
|
70
86
|
fs.mkdirSync(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
87
|
+
const prompt = safePrompt(entry.prompt);
|
|
71
88
|
const line = JSON.stringify({
|
|
72
89
|
ts: new Date().toISOString(),
|
|
73
90
|
tier: entry.tier,
|
|
@@ -77,6 +94,7 @@ export class ApprovalLog {
|
|
|
77
94
|
scope: entry.scope || 'once',
|
|
78
95
|
rule_id: entry.rule_id || null,
|
|
79
96
|
reason: safeText(entry.reason),
|
|
97
|
+
...(prompt ? { prompt } : {}),
|
|
80
98
|
});
|
|
81
99
|
fs.appendFileSync(this.filePath, line + '\n', { mode: 0o600 });
|
|
82
100
|
try { fs.chmodSync(this.filePath, 0o600); } catch {}
|
package/src/core/approval.mjs
CHANGED
|
@@ -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,10 +20,11 @@ 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 { clearInputPrompt, isInputDockMounted, moveToContent, renderDockOverlay } from '../ui/input-dock.mjs';
|
|
27
28
|
import { validateShellCommand } from './safety.mjs';
|
|
28
29
|
import { classifyCommand } from '../permissions/command-classifier.mjs';
|
|
29
30
|
import { ApprovalLog } from './approval-log.mjs';
|
|
@@ -72,12 +73,22 @@ function shellHardBlockReason(tool, args = {}) {
|
|
|
72
73
|
if (tool !== 'shell') return '';
|
|
73
74
|
const command = args.command || args.cmd || '';
|
|
74
75
|
const safety = validateShellCommand(command);
|
|
75
|
-
if (!safety.safe) return safety.reason || 'Blocked by shell safety policy';
|
|
76
|
+
if (!safety.safe) return withShellRetryHint(safety.reason || 'Blocked by shell safety policy');
|
|
76
77
|
const classification = classifyCommand(command);
|
|
77
|
-
if (classification.classification === 'blocked')
|
|
78
|
+
if (classification.classification === 'blocked') {
|
|
79
|
+
return withShellRetryHint(classification.reason || 'Blocked by shell safety policy');
|
|
80
|
+
}
|
|
78
81
|
return '';
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function withShellRetryHint(reason) {
|
|
85
|
+
const text = String(reason || '').trim();
|
|
86
|
+
if (/command substitution|backticks|\$\(\)/i.test(text)) {
|
|
87
|
+
return `${text}. Retry with separate simple shell commands instead of backticks or $().`;
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
// ── ANSI helpers ──
|
|
82
93
|
|
|
83
94
|
const RST = '\x1b[0m';
|
|
@@ -111,9 +122,10 @@ export class ApprovalManager {
|
|
|
111
122
|
|
|
112
123
|
setReadline(rl) { this._rl = rl; }
|
|
113
124
|
|
|
114
|
-
setExecutionHooks({ onPause, onResume } = {}) {
|
|
125
|
+
setExecutionHooks({ onPause, onResume, onApprovalPromptEnd } = {}) {
|
|
115
126
|
this._execPause = onPause || null;
|
|
116
127
|
this._execResume = onResume || null;
|
|
128
|
+
this._approvalPromptEnd = onApprovalPromptEnd || null;
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
revoke() {
|
|
@@ -143,7 +155,8 @@ export class ApprovalManager {
|
|
|
143
155
|
const reason = `Blocked by safety policy: ${hardBlock}`;
|
|
144
156
|
this.history.push({ tool: toolName, decision: 'safety-block', tier, time: Date.now(), reason });
|
|
145
157
|
this.approvalLog.append({ tool: toolName, args, tier, decision: 'safety_block', scope: 'none', reason });
|
|
146
|
-
|
|
158
|
+
if (isInputDockMounted()) moveToContent();
|
|
159
|
+
writeSafetyBlock(reason);
|
|
147
160
|
return { approved: false, tier, reason, blocked: true, code: 'safety_block' };
|
|
148
161
|
}
|
|
149
162
|
|
|
@@ -203,25 +216,48 @@ export class ApprovalManager {
|
|
|
203
216
|
async _prompt(toolName, args, context = {}) {
|
|
204
217
|
const tier = context.tier || classifyTier(toolName, args);
|
|
205
218
|
const why = context.reason || context.why || defaultWhy(tier, toolName, args);
|
|
206
|
-
const options = this._optionsFor(tier);
|
|
219
|
+
const options = this._optionsFor(tier, toolName, args);
|
|
207
220
|
|
|
208
221
|
let selected = 0; // arrow-driven cursor
|
|
209
222
|
let printedHeight = 0;
|
|
223
|
+
let showDetails = false;
|
|
224
|
+
|
|
225
|
+
const isInteractive = process.stdin.isTTY;
|
|
210
226
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
if (isInputDockMounted()) moveToContent();
|
|
227
|
+
// In rich TTY mode approval lives in the fixed input dock, replacing
|
|
228
|
+
// "+ add instruction" until the user decides. Fallback/plain mode
|
|
229
|
+
// still renders in the transcript.
|
|
230
|
+
const useDockPrompt = isInteractive && isInputDockMounted();
|
|
231
|
+
if (!useDockPrompt && isInputDockMounted()) moveToContent();
|
|
216
232
|
|
|
217
233
|
// For TTYs we redraw in place on every arrow key so the prompt feels
|
|
218
234
|
// live. For non-TTYs / pipes we just print once and read a line.
|
|
219
|
-
const isInteractive = process.stdin.isTTY;
|
|
220
235
|
if (!isInteractive) {
|
|
221
|
-
write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options }) + '\n');
|
|
236
|
+
write(renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options, showDetails }) + '\n');
|
|
222
237
|
}
|
|
223
238
|
|
|
224
239
|
const drawPrompt = () => {
|
|
240
|
+
if (useDockPrompt) {
|
|
241
|
+
const dock = renderApprovalDockPrompt({
|
|
242
|
+
tool: toolName,
|
|
243
|
+
args,
|
|
244
|
+
tier,
|
|
245
|
+
why,
|
|
246
|
+
selected,
|
|
247
|
+
options,
|
|
248
|
+
showDetails,
|
|
249
|
+
width: process.stderr.columns || process.stdout.columns || 96,
|
|
250
|
+
});
|
|
251
|
+
renderDockOverlay({
|
|
252
|
+
context: dock.context,
|
|
253
|
+
lines: dock.lines || [`${dock.prefix || ''}${dock.value || ''}`],
|
|
254
|
+
meta: dock.meta,
|
|
255
|
+
tips: dock.tips,
|
|
256
|
+
maxRows: dock.maxRows,
|
|
257
|
+
});
|
|
258
|
+
printedHeight = 0;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
225
261
|
// Move up over the previous render before re-printing. Use a
|
|
226
262
|
// bounded per-line clear instead of \x1b[J — a full clear-to-end
|
|
227
263
|
// would wipe the input dock's bottom rule and tips row that live
|
|
@@ -234,7 +270,7 @@ export class ApprovalManager {
|
|
|
234
270
|
}
|
|
235
271
|
write(`\x1b[${printedHeight}F`); // back to the anchor row
|
|
236
272
|
}
|
|
237
|
-
const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options });
|
|
273
|
+
const block = renderApprovalPrompt({ tool: toolName, args, tier, why, selected, options, showDetails });
|
|
238
274
|
write(block + '\n');
|
|
239
275
|
printedHeight = block.split('\n').length;
|
|
240
276
|
};
|
|
@@ -266,6 +302,11 @@ export class ApprovalManager {
|
|
|
266
302
|
// Letter shortcut: match against option.key
|
|
267
303
|
if (typeof k === 'string' && k.length === 1) {
|
|
268
304
|
const lower = k.toLowerCase();
|
|
305
|
+
if (lower === 'd' && toolName === 'shell') {
|
|
306
|
+
showDetails = !showDetails;
|
|
307
|
+
if (isInteractive) drawPrompt();
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
269
310
|
const idx = options.findIndex(o => o.key === lower);
|
|
270
311
|
if (idx >= 0) {
|
|
271
312
|
selected = idx;
|
|
@@ -278,11 +319,18 @@ export class ApprovalManager {
|
|
|
278
319
|
};
|
|
279
320
|
|
|
280
321
|
const value = await choose();
|
|
322
|
+
if (useDockPrompt) {
|
|
323
|
+
clearInputPrompt();
|
|
324
|
+
if (this._approvalPromptEnd) this._approvalPromptEnd();
|
|
325
|
+
moveToContent();
|
|
326
|
+
}
|
|
327
|
+
const prompt = promptForLog(toolName, args, tier, why);
|
|
281
328
|
|
|
282
329
|
switch (value) {
|
|
283
330
|
case 'approve':
|
|
284
331
|
this.history.push({ tool: toolName, decision: 'yes', tier, time: Date.now() });
|
|
285
|
-
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once' });
|
|
332
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once', prompt });
|
|
333
|
+
writeApprovalConfirmation(toolName, args, 'approved once');
|
|
286
334
|
return { approved: true, tier };
|
|
287
335
|
|
|
288
336
|
case 'allow-session': {
|
|
@@ -290,7 +338,7 @@ export class ApprovalManager {
|
|
|
290
338
|
const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'SESSION' });
|
|
291
339
|
write(` ${GREEN}✓${RST} ${DIM}trusted for this session: ${rule.pattern}${RST}\n\n`);
|
|
292
340
|
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 });
|
|
341
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id, prompt });
|
|
294
342
|
return { approved: true, tier, scope: 'SESSION', rule_id: rule.id };
|
|
295
343
|
}
|
|
296
344
|
|
|
@@ -299,7 +347,7 @@ export class ApprovalManager {
|
|
|
299
347
|
const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'PROJECT' });
|
|
300
348
|
write(` ${GREEN}✓${RST} ${DIM}trusted for this project: ${rule.pattern}${RST}\n\n`);
|
|
301
349
|
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 });
|
|
350
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id, prompt });
|
|
303
351
|
return { approved: true, tier, scope: 'PROJECT', rule_id: rule.id };
|
|
304
352
|
}
|
|
305
353
|
|
|
@@ -308,7 +356,7 @@ export class ApprovalManager {
|
|
|
308
356
|
const reason = 'User stopped the command';
|
|
309
357
|
write(` ${RED}✗${RST} ${DIM}stopped${RST}\n\n`);
|
|
310
358
|
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 });
|
|
359
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason, prompt });
|
|
312
360
|
this._rememberRejection({ tool: toolName, args, tier, decision: 'reject', reason, note: '' });
|
|
313
361
|
return { approved: false, tier, reason };
|
|
314
362
|
}
|
|
@@ -318,14 +366,15 @@ export class ApprovalManager {
|
|
|
318
366
|
this.approveAll = true;
|
|
319
367
|
write(` ${GREEN}✓✓${RST} ${DIM}allow-all activated${RST}\n\n`);
|
|
320
368
|
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' });
|
|
369
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session', prompt });
|
|
322
370
|
return { approved: true, tier };
|
|
323
371
|
|
|
324
372
|
case 'allow-type':
|
|
325
373
|
if (requiresExplicitApproval(tier)) return this._prompt(toolName, args, context);
|
|
326
374
|
this.approvedToolTypes.add(toolName);
|
|
327
375
|
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' });
|
|
376
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session', prompt });
|
|
377
|
+
writeApprovalConfirmation(toolName, args, 'always allow');
|
|
329
378
|
return { approved: true, tier };
|
|
330
379
|
|
|
331
380
|
case 'why':
|
|
@@ -340,7 +389,7 @@ export class ApprovalManager {
|
|
|
340
389
|
const reason = note ? `User asked to re-plan: ${note}` : 'User asked to re-plan';
|
|
341
390
|
write(` ${YELLOW}↩${RST} ${DIM}${note ? `re-plan — ${truncateNote(note)}` : 'reject with hint — rework the plan'}${RST}\n\n`);
|
|
342
391
|
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 });
|
|
392
|
+
this.approvalLog.append({ tool: toolName, args, tier, decision: 'replan', scope: 'once', reason, prompt });
|
|
344
393
|
this._rememberRejection({ tool: toolName, args, tier, decision: 'replan', reason, note });
|
|
345
394
|
return { approved: false, tier, reason };
|
|
346
395
|
}
|
|
@@ -439,8 +488,8 @@ export class ApprovalManager {
|
|
|
439
488
|
});
|
|
440
489
|
}
|
|
441
490
|
|
|
442
|
-
_optionsFor(tier) {
|
|
443
|
-
return approvalOptions(tier);
|
|
491
|
+
_optionsFor(tier, toolName, args) {
|
|
492
|
+
return approvalOptions(tier, { tool: toolName, args });
|
|
444
493
|
}
|
|
445
494
|
|
|
446
495
|
getSummary() {
|
|
@@ -474,3 +523,102 @@ function truncateNote(note) {
|
|
|
474
523
|
const text = String(note || '').trim();
|
|
475
524
|
return text.length <= 120 ? text : text.slice(0, 119) + '…';
|
|
476
525
|
}
|
|
526
|
+
|
|
527
|
+
function promptForLog(tool, args, tier, why) {
|
|
528
|
+
return {
|
|
529
|
+
title: `${approvalTitleForLog(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
|
|
530
|
+
subject: approvalSummary(tool, args),
|
|
531
|
+
reason: why || '',
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function approvalTitleForLog(tier) {
|
|
536
|
+
switch (tier) {
|
|
537
|
+
case TIERS.SENSITIVE_READ:
|
|
538
|
+
case TIERS.SHELL_DANGEROUS:
|
|
539
|
+
case TIERS.DESTRUCTIVE:
|
|
540
|
+
return 'EXPLICIT APPROVAL';
|
|
541
|
+
default:
|
|
542
|
+
return 'APPROVAL';
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function writeApprovalConfirmation(tool, args, label) {
|
|
547
|
+
if (tool === 'shell') {
|
|
548
|
+
write(` ${GREEN}✓${RST} ${DIM}${label} · shell ${shellApprovalSubject(args)}${RST}\n`);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const subject = approvalSummary(tool, args);
|
|
552
|
+
const suffix = subject ? ` · ${truncateNote(subject)}` : '';
|
|
553
|
+
write(` ${GREEN}✓${RST} ${DIM}${label}${suffix}${RST}\n\n`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function shellApprovalSubject(args = {}) {
|
|
557
|
+
const profile = shellCommandProfile(args.command || args.cmd || '');
|
|
558
|
+
const subject = profile.compact ? profile.summary : profile.command;
|
|
559
|
+
return `$ ${truncateNote(subject)}`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function writeSafetyBlock(reason) {
|
|
563
|
+
const cols = Math.max(40, Math.min(process.stderr.columns || process.stdout.columns || 80, 140));
|
|
564
|
+
const firstPrefix = ` ${RED}✗${RST} ${DIM}`;
|
|
565
|
+
const nextPrefix = ` ${DIM}`;
|
|
566
|
+
const firstWidth = Math.max(24, cols - 5);
|
|
567
|
+
const nextWidth = Math.max(24, cols - 5);
|
|
568
|
+
const lines = wrapSafetyText(reason, firstWidth, nextWidth);
|
|
569
|
+
|
|
570
|
+
write(`${firstPrefix}${lines[0] || ''}${RST}\n`);
|
|
571
|
+
for (const line of lines.slice(1)) {
|
|
572
|
+
write(`${nextPrefix}${line}${RST}\n`);
|
|
573
|
+
}
|
|
574
|
+
write('\n');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function wrapSafetyText(text, firstWidth, nextWidth) {
|
|
578
|
+
const words = String(text || '').replace(/\s+/g, ' ').trim().split(' ').filter(Boolean);
|
|
579
|
+
if (!words.length) return [''];
|
|
580
|
+
|
|
581
|
+
const lines = [];
|
|
582
|
+
let line = '';
|
|
583
|
+
let width = firstWidth;
|
|
584
|
+
|
|
585
|
+
for (const word of words) {
|
|
586
|
+
if (!line) {
|
|
587
|
+
if (word.length <= width) {
|
|
588
|
+
line = word;
|
|
589
|
+
} else {
|
|
590
|
+
const chunks = splitWord(word, width);
|
|
591
|
+
lines.push(...chunks.slice(0, -1));
|
|
592
|
+
line = chunks[chunks.length - 1] || '';
|
|
593
|
+
}
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (line.length + 1 + word.length <= width) {
|
|
598
|
+
line += ` ${word}`;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
lines.push(line);
|
|
603
|
+
width = nextWidth;
|
|
604
|
+
if (word.length <= width) {
|
|
605
|
+
line = word;
|
|
606
|
+
} else {
|
|
607
|
+
const chunks = splitWord(word, width);
|
|
608
|
+
lines.push(...chunks.slice(0, -1));
|
|
609
|
+
line = chunks[chunks.length - 1] || '';
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (line) lines.push(line);
|
|
614
|
+
return lines;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function splitWord(word, width) {
|
|
618
|
+
const size = Math.max(8, width);
|
|
619
|
+
const chunks = [];
|
|
620
|
+
for (let i = 0; i < word.length; i += size) {
|
|
621
|
+
chunks.push(word.slice(i, i + size));
|
|
622
|
+
}
|
|
623
|
+
return chunks;
|
|
624
|
+
}
|
package/src/core/headless.mjs
CHANGED
|
@@ -272,7 +272,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
272
272
|
tool_calls: data?.tool_calls || 0,
|
|
273
273
|
success: data?.success !== false,
|
|
274
274
|
});
|
|
275
|
-
emit({ type: 'sub_agent',
|
|
275
|
+
emit({ ...data, type: 'sub_agent', agent_type: data?.type || '' });
|
|
276
276
|
log(`SubAgent done: ${data?.type} (${data?.tool_calls} tools, ${data?.duration_s}s)`);
|
|
277
277
|
}
|
|
278
278
|
|
|
@@ -360,8 +360,19 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
360
360
|
const subAgentToolBreakdown = countBreakdown(toolCalls.filter(t => t.internal));
|
|
361
361
|
|
|
362
362
|
const subAgentReportedToolCount = subAgents.reduce((sum, sa) => sum + (sa.tool_calls || 0), 0);
|
|
363
|
-
const
|
|
364
|
-
|
|
363
|
+
const observedSubAgentToolCount = Math.max(
|
|
364
|
+
subAgentReportedToolCount,
|
|
365
|
+
subAgentForwardedToolCount,
|
|
366
|
+
);
|
|
367
|
+
const subAgentToolCount = (
|
|
368
|
+
backendSubAgentToolCount && backendSubAgentToolCount > 0
|
|
369
|
+
)
|
|
370
|
+
? backendSubAgentToolCount
|
|
371
|
+
: observedSubAgentToolCount;
|
|
372
|
+
const computedToolCount = primaryToolCount + subAgentToolCount;
|
|
373
|
+
const totalToolCount = backendToolCount != null
|
|
374
|
+
? Math.max(backendToolCount, computedToolCount)
|
|
375
|
+
: computedToolCount;
|
|
365
376
|
const primaryToolTotal = backendPrimaryToolCount ?? (
|
|
366
377
|
backendToolCount != null
|
|
367
378
|
? Math.max(0, totalToolCount - subAgentToolCount)
|
package/src/core/local-agent.mjs
CHANGED
|
@@ -21,11 +21,11 @@ const MAX_ITERATIONS = 50;
|
|
|
21
21
|
const TOOL_SCHEMAS = [
|
|
22
22
|
{
|
|
23
23
|
name: 'shell',
|
|
24
|
-
description: 'Run
|
|
24
|
+
description: 'Run one non-interactive shell command and return stdout/stderr. Do not use command substitution, backticks, or $(); run separate simple shell calls instead.',
|
|
25
25
|
input_schema: {
|
|
26
26
|
type: 'object',
|
|
27
27
|
properties: {
|
|
28
|
-
command: { type: 'string', description: 'The
|
|
28
|
+
command: { type: 'string', description: 'The command to execute. Avoid backticks and $(); split dependent checks into separate commands.' },
|
|
29
29
|
timeout: { type: 'number', description: 'Timeout in milliseconds (default: 120000)' },
|
|
30
30
|
},
|
|
31
31
|
required: ['command'],
|
|
@@ -478,6 +478,7 @@ export class LocalAgent {
|
|
|
478
478
|
'You are Bahulam Code, Bahulam\'s AI coding agent running in local mode.',
|
|
479
479
|
'You have access to tools for reading, writing, and executing code.',
|
|
480
480
|
'Use tools to accomplish the user\'s request. Be concise and direct.',
|
|
481
|
+
'For shell tools, never use command substitution, backticks, or $(). Run separate simple commands and carry values forward in text.',
|
|
481
482
|
];
|
|
482
483
|
if (context.cwd) parts.push(`Working directory: ${context.cwd}`);
|
|
483
484
|
if (context.gitBranch) parts.push(`Git branch: ${context.gitBranch}`);
|
|
@@ -74,6 +74,14 @@ export function createToolExecutor({
|
|
|
74
74
|
return path.resolve(cwd, value);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function blockedShellOutput(reason) {
|
|
78
|
+
const text = String(reason || 'Blocked by shell safety policy').trim();
|
|
79
|
+
const hint = /command substitution|backticks|\$\(\)/i.test(text)
|
|
80
|
+
? 'Retry with separate simple shell commands instead of backticks or $().'
|
|
81
|
+
: 'Work only inside a registered project root.';
|
|
82
|
+
return `BLOCKED: ${text}. ${hint}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
function longRunningObservationTimeoutMs() {
|
|
78
86
|
const configured = Number(process.env.KEPLER_LONG_RUNNING_TIMEOUT_MS);
|
|
79
87
|
return Number.isFinite(configured) && configured > 0 ? configured : 15_000;
|
|
@@ -578,7 +586,7 @@ export function createToolExecutor({
|
|
|
578
586
|
if (!shellCheck.safe) {
|
|
579
587
|
return {
|
|
580
588
|
success: false,
|
|
581
|
-
output:
|
|
589
|
+
output: blockedShellOutput(shellCheck.reason),
|
|
582
590
|
_tool: 'shell', _blocked: true,
|
|
583
591
|
};
|
|
584
592
|
}
|
|
@@ -588,7 +596,7 @@ export function createToolExecutor({
|
|
|
588
596
|
if (classification.classification === 'blocked') {
|
|
589
597
|
return {
|
|
590
598
|
success: false,
|
|
591
|
-
output:
|
|
599
|
+
output: blockedShellOutput(classification.reason),
|
|
592
600
|
_tool: 'shell', _blocked: true,
|
|
593
601
|
};
|
|
594
602
|
}
|
|
@@ -1513,7 +1521,9 @@ print('OK: replaced')
|
|
|
1513
1521
|
const created = listLocalAgents(process.cwd()).find(agent => agent.slug === result.slug);
|
|
1514
1522
|
const payload = {
|
|
1515
1523
|
...result,
|
|
1516
|
-
agent: created
|
|
1524
|
+
agent: created
|
|
1525
|
+
? { ...compactAgentMetadata(created), spec: created.spec }
|
|
1526
|
+
: null,
|
|
1517
1527
|
next_actions: [
|
|
1518
1528
|
`Edit ${result.filePath}`,
|
|
1519
1529
|
`Run /agents sync ${result.slug} when ready`,
|
package/src/index.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { printBanner, printProjectInfo, printHints, printAuthStatus, printStyled
|
|
|
35
35
|
import { ContextRetriever } from './context/retriever.mjs';
|
|
36
36
|
import { loadSettings } from './config/settings.mjs';
|
|
37
37
|
|
|
38
|
-
const VERSION = '2.6.
|
|
38
|
+
const VERSION = '2.6.14';
|
|
39
39
|
|
|
40
40
|
// ── Arg Parsing (consolidated from index.mjs + cli-args.mjs) ──
|
|
41
41
|
|