@axplusb/kepler 2.0.7 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +164 -24
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/headless.mjs +14 -1
- package/src/core/jsonl-writer.mjs +50 -0
- package/src/core/local-store.mjs +486 -5
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/rate-limit-display.mjs +97 -0
- package/src/core/resume-mode.mjs +154 -0
- package/src/core/risk-tier.mjs +88 -2
- package/src/core/safety.mjs +3 -0
- package/src/core/session-manager.mjs +53 -10
- package/src/core/stream-client.mjs +69 -10
- package/src/core/system-prompt.mjs +6 -1
- package/src/core/tasks.mjs +130 -0
- package/src/core/tool-executor.mjs +72 -6
- package/src/core/trust.mjs +158 -0
- package/src/onboarding/preflight.mjs +27 -11
- package/src/permissions/command-classifier.mjs +78 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +7 -0
- package/src/terminal/repl.mjs +1735 -140
- package/src/tools/bash.mjs +57 -9
- package/src/tools/project-overview.mjs +83 -10
- package/src/ui/approval.mjs +154 -35
- package/src/ui/formatter.mjs +39 -5
- package/src/ui/mission-report.mjs +55 -22
- package/src/ui/slash-commands.mjs +57 -5
- package/src/ui/tool-card.mjs +51 -1
package/src/tools/bash.mjs
CHANGED
|
@@ -17,6 +17,7 @@ function stripAnsi(str) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const MAX_OUTPUT_BYTES = 1024 * 1024; // 1MB
|
|
20
|
+
const TIMEOUT_TAIL_BYTES = 64 * 1024;
|
|
20
21
|
|
|
21
22
|
export const BashTool = {
|
|
22
23
|
name: 'Bash',
|
|
@@ -47,6 +48,8 @@ export const BashTool = {
|
|
|
47
48
|
return new Promise((resolve) => {
|
|
48
49
|
let stdout = '';
|
|
49
50
|
let stderr = '';
|
|
51
|
+
let stdoutTail = '';
|
|
52
|
+
let stderrTail = '';
|
|
50
53
|
let killed = false;
|
|
51
54
|
let exitCode = null;
|
|
52
55
|
|
|
@@ -54,27 +57,28 @@ export const BashTool = {
|
|
|
54
57
|
cwd: input.cwd,
|
|
55
58
|
env: { ...process.env },
|
|
56
59
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
60
|
+
detached: process.platform !== 'win32',
|
|
57
61
|
timeout: 0, // we handle timeout ourselves
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
proc.stdout.on('data', (chunk) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const text = chunk.toString();
|
|
66
|
+
stdout = appendHead(stdout, text, MAX_OUTPUT_BYTES);
|
|
67
|
+
stdoutTail = appendTail(stdoutTail, text, TIMEOUT_TAIL_BYTES);
|
|
64
68
|
});
|
|
65
69
|
|
|
66
70
|
proc.stderr.on('data', (chunk) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
const text = chunk.toString();
|
|
72
|
+
stderr = appendHead(stderr, text, MAX_OUTPUT_BYTES);
|
|
73
|
+
stderrTail = appendTail(stderrTail, text, TIMEOUT_TAIL_BYTES);
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
// Timeout: SIGTERM first, then SIGKILL after 5s
|
|
73
77
|
const timer = setTimeout(() => {
|
|
74
78
|
killed = true;
|
|
75
|
-
proc
|
|
79
|
+
killProcess(proc, 'SIGTERM');
|
|
76
80
|
setTimeout(() => {
|
|
77
|
-
|
|
81
|
+
killProcess(proc, 'SIGKILL');
|
|
78
82
|
}, 5000);
|
|
79
83
|
}, timeout);
|
|
80
84
|
|
|
@@ -95,7 +99,8 @@ export const BashTool = {
|
|
|
95
99
|
stderr = stripAnsi(stderr);
|
|
96
100
|
|
|
97
101
|
if (killed) {
|
|
98
|
-
|
|
102
|
+
const tail = formatTimeoutTail(stdoutTail, stderrTail);
|
|
103
|
+
resolve(`Error: Command timed out after ${timeout}ms\n${tail}`.trim());
|
|
99
104
|
return;
|
|
100
105
|
}
|
|
101
106
|
|
|
@@ -118,6 +123,49 @@ export const BashTool = {
|
|
|
118
123
|
},
|
|
119
124
|
};
|
|
120
125
|
|
|
126
|
+
function appendHead(current, chunk, maxBytes) {
|
|
127
|
+
if (current.length >= maxBytes) return current;
|
|
128
|
+
const next = current + chunk;
|
|
129
|
+
return next.length > maxBytes ? next.slice(0, maxBytes) : next;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function appendTail(current, chunk, maxBytes) {
|
|
133
|
+
const next = current + chunk;
|
|
134
|
+
return next.length > maxBytes ? next.slice(next.length - maxBytes) : next;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatTimeoutTail(stdoutTail, stderrTail) {
|
|
138
|
+
const out = stripAnsi(stdoutTail || '').trim();
|
|
139
|
+
const err = stripAnsi(stderrTail || '').trim();
|
|
140
|
+
const lines = [];
|
|
141
|
+
if (out) {
|
|
142
|
+
lines.push('[stdout tail]');
|
|
143
|
+
lines.push(tailLines(out, 80));
|
|
144
|
+
}
|
|
145
|
+
if (err) {
|
|
146
|
+
if (lines.length) lines.push('');
|
|
147
|
+
lines.push('[stderr tail]');
|
|
148
|
+
lines.push(tailLines(err, 80));
|
|
149
|
+
}
|
|
150
|
+
return lines.length ? lines.join('\n') : '(no output captured before timeout)';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function tailLines(text, maxLines) {
|
|
154
|
+
const lines = String(text || '').split('\n');
|
|
155
|
+
return lines.slice(-maxLines).join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function killProcess(proc, signal) {
|
|
159
|
+
if (!proc?.pid) return;
|
|
160
|
+
try {
|
|
161
|
+
if (process.platform !== 'win32') {
|
|
162
|
+
process.kill(-proc.pid, signal);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} catch { /* fall through to direct process kill */ }
|
|
166
|
+
try { proc.kill(signal); } catch { /* already exited */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
121
169
|
// Background jobs store
|
|
122
170
|
const backgroundJobs = new Map();
|
|
123
171
|
let bgJobId = 0;
|
|
@@ -64,6 +64,19 @@ function isWithin(root, candidate) {
|
|
|
64
64
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function uniqueValues(values) {
|
|
68
|
+
return [...new Set(values.filter(Boolean))];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canonicalRoot(rootPath) {
|
|
72
|
+
const resolved = path.resolve(normalizePathInput(rootPath));
|
|
73
|
+
try {
|
|
74
|
+
return fs.realpathSync(resolved);
|
|
75
|
+
} catch {
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
function canonicalizeCandidate(candidate) {
|
|
68
81
|
if (fs.existsSync(candidate)) return fs.realpathSync(candidate);
|
|
69
82
|
|
|
@@ -233,6 +246,9 @@ function formatResource(resource) {
|
|
|
233
246
|
if (resource.project_context) {
|
|
234
247
|
lines.push('', '--- Project Context ---', resource.project_context);
|
|
235
248
|
}
|
|
249
|
+
if (resource.style) {
|
|
250
|
+
lines.push('', '--- Project Style ---', resource.style);
|
|
251
|
+
}
|
|
236
252
|
if (resource.goal) {
|
|
237
253
|
lines.push('', '--- Current Goal ---', resource.goal);
|
|
238
254
|
}
|
|
@@ -261,27 +277,53 @@ function _scanSkills(keplerDir) {
|
|
|
261
277
|
const skillsDir = path.join(keplerDir, 'skills');
|
|
262
278
|
if (!fs.existsSync(skillsDir)) return [];
|
|
263
279
|
try {
|
|
264
|
-
return fs.readdirSync(skillsDir)
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
|
|
280
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
281
|
+
.map(entry => {
|
|
282
|
+
const file = entry.isDirectory()
|
|
283
|
+
? path.join(skillsDir, entry.name, 'SKILL.md')
|
|
284
|
+
: path.join(skillsDir, entry.name);
|
|
285
|
+
if (!fs.existsSync(file) || !file.endsWith('.md')) return null;
|
|
286
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
268
287
|
const descMatch = content.match(/^#\s+.*\n+(.+)/);
|
|
269
288
|
return {
|
|
270
|
-
name:
|
|
271
|
-
description:
|
|
289
|
+
name: entry.isDirectory() ? entry.name : entry.name.replace('.md', ''),
|
|
290
|
+
description: content.match(/^description:\s*(.+)$/mi)?.[1]?.trim()
|
|
291
|
+
|| (descMatch ? descMatch[1].slice(0, 100) : entry.name.replace('.md', '')),
|
|
272
292
|
};
|
|
273
|
-
})
|
|
293
|
+
})
|
|
294
|
+
.filter(Boolean);
|
|
274
295
|
} catch { return []; }
|
|
275
296
|
}
|
|
276
297
|
|
|
298
|
+
function defaultScratchRoots() {
|
|
299
|
+
return uniqueValues([
|
|
300
|
+
'/tmp',
|
|
301
|
+
'/private/tmp',
|
|
302
|
+
os.tmpdir(),
|
|
303
|
+
process.env.TMPDIR,
|
|
304
|
+
...(process.env.KEPLER_SCRATCH_ROOTS || '')
|
|
305
|
+
.split(path.delimiter)
|
|
306
|
+
.map(s => s.trim())
|
|
307
|
+
.filter(Boolean),
|
|
308
|
+
]).map(canonicalRoot);
|
|
309
|
+
}
|
|
310
|
+
|
|
277
311
|
export class ProjectRegistry {
|
|
278
312
|
constructor() {
|
|
279
313
|
this.projects = new Map();
|
|
314
|
+
this.scratchRoots = new Set(defaultScratchRoots());
|
|
280
315
|
this._globalIdentity = null;
|
|
281
316
|
this._globalPreferences = null;
|
|
282
317
|
this._globalSkills = null;
|
|
283
318
|
}
|
|
284
319
|
|
|
320
|
+
addScratchRoot(rawPath) {
|
|
321
|
+
if (!rawPath) return null;
|
|
322
|
+
const root = canonicalRoot(rawPath);
|
|
323
|
+
this.scratchRoots.add(root);
|
|
324
|
+
return root;
|
|
325
|
+
}
|
|
326
|
+
|
|
285
327
|
/**
|
|
286
328
|
* Load global context from ~/.kepler/ (once per session).
|
|
287
329
|
*/
|
|
@@ -382,10 +424,13 @@ export class ProjectRegistry {
|
|
|
382
424
|
fs.writeFileSync(resourcePath, JSON.stringify(resource));
|
|
383
425
|
}
|
|
384
426
|
|
|
385
|
-
// Read project-level context files (.kepler/project.md, goal.md, skills/)
|
|
427
|
+
// Read project-level context files (.kepler/KEPLER.md, project.md, goal.md, plan.md, style.md, skills/)
|
|
386
428
|
const keplerDir = path.join(root, '.kepler');
|
|
387
429
|
resource.environment = detectEnvironment();
|
|
388
|
-
resource.project_context = _readIfExists(keplerDir, '
|
|
430
|
+
resource.project_context = _readIfExists(keplerDir, 'KEPLER.md', 10000) ||
|
|
431
|
+
_readIfExists(root, 'KEPLER.md', 10000) ||
|
|
432
|
+
_readIfExists(keplerDir, 'project.md', 8000);
|
|
433
|
+
resource.style = _readIfExists(keplerDir, 'style.md', 4000);
|
|
389
434
|
resource.goal = _readIfExists(keplerDir, 'goal.md', 2000);
|
|
390
435
|
resource.plan = _readIfExists(keplerDir, 'plan.md', 6000);
|
|
391
436
|
resource.skills_index = _scanSkills(keplerDir);
|
|
@@ -410,6 +455,23 @@ export class ProjectRegistry {
|
|
|
410
455
|
return this.projects.get(projectIdValue) || null;
|
|
411
456
|
}
|
|
412
457
|
|
|
458
|
+
projectScratchRoots() {
|
|
459
|
+
return this.resources().map(resource => path.join(resource.root, '.kepler', 'tmp'));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
allowedScratchRoots() {
|
|
463
|
+
return uniqueValues([
|
|
464
|
+
...this.scratchRoots,
|
|
465
|
+
...this.projectScratchRoots(),
|
|
466
|
+
]).map(canonicalRoot);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
isAllowedScratchPath(filePath) {
|
|
470
|
+
const normalized = normalizePathInput(filePath);
|
|
471
|
+
const candidate = canonicalizeCandidate(path.resolve(normalized));
|
|
472
|
+
return this.allowedScratchRoots().some(root => isWithin(root, candidate));
|
|
473
|
+
}
|
|
474
|
+
|
|
413
475
|
resolvePath(rawPath, projectIdValue, { allowMissing = false } = {}) {
|
|
414
476
|
let root = null;
|
|
415
477
|
if (projectIdValue) {
|
|
@@ -453,8 +515,12 @@ export class ProjectRegistry {
|
|
|
453
515
|
const findContaining = (cand) => [...this.projects.values()].find(({ resource }) =>
|
|
454
516
|
isWithin(resource.root, cand)
|
|
455
517
|
);
|
|
518
|
+
const findScratchRoot = (cand) => this.allowedScratchRoots().find(scratchRoot =>
|
|
519
|
+
isWithin(scratchRoot, cand)
|
|
520
|
+
);
|
|
456
521
|
|
|
457
522
|
let containingProject = findContaining(candidate);
|
|
523
|
+
let containingScratchRoot = containingProject ? null : findScratchRoot(candidate);
|
|
458
524
|
|
|
459
525
|
// Two reasons to try the unescaped variant:
|
|
460
526
|
// (1) candidate is outside every project root (literal "Tarang\ Orca"
|
|
@@ -473,12 +539,19 @@ export class ProjectRegistry {
|
|
|
473
539
|
if (altProject && (allowMissing || fs.existsSync(altCandidate))) {
|
|
474
540
|
candidate = altCandidate;
|
|
475
541
|
containingProject = altProject;
|
|
542
|
+
containingScratchRoot = null;
|
|
543
|
+
} else {
|
|
544
|
+
const altScratchRoot = findScratchRoot(altCandidate);
|
|
545
|
+
if (altScratchRoot && (allowMissing || fs.existsSync(altCandidate))) {
|
|
546
|
+
candidate = altCandidate;
|
|
547
|
+
containingScratchRoot = altScratchRoot;
|
|
548
|
+
}
|
|
476
549
|
}
|
|
477
550
|
} catch { /* fall through to the original error */ }
|
|
478
551
|
}
|
|
479
552
|
}
|
|
480
553
|
|
|
481
|
-
if (!containingProject) {
|
|
554
|
+
if (!containingProject && !containingScratchRoot) {
|
|
482
555
|
throw new Error(`Path is outside registered project roots: ${rawPath}`);
|
|
483
556
|
}
|
|
484
557
|
if (!allowMissing && !fs.existsSync(candidate)) {
|
package/src/ui/approval.mjs
CHANGED
|
@@ -34,20 +34,19 @@ const PAD_X = 3;
|
|
|
34
34
|
* hint — secondary description shown to the right of the label
|
|
35
35
|
*/
|
|
36
36
|
export function defaultOptions(tier) {
|
|
37
|
+
const shared = [
|
|
38
|
+
{ key: 'y', label: 'approve once', value: 'approve', hint: 'run this call' },
|
|
39
|
+
{ key: 'r', label: 're-plan with note...', value: 'replan', hint: 'steer the agent' },
|
|
40
|
+
{ key: 'n', label: 'stop', value: 'reject', hint: 'do not run' },
|
|
41
|
+
{ key: '?', label: 'why', value: 'why', hint: 'show reasoning' },
|
|
42
|
+
];
|
|
37
43
|
if (requiresExplicitApproval(tier)) {
|
|
38
|
-
return
|
|
39
|
-
{ key: 'y', label: 'approve', value: 'approve', hint: 'run this once' },
|
|
40
|
-
{ key: 'e', label: 'edit', value: 'edit', hint: 'tweak the args before running' },
|
|
41
|
-
{ key: 'r', label: 're-plan', value: 'replan', hint: 'send back to the agent' },
|
|
42
|
-
{ key: 'n', label: 'reject', value: 'reject', hint: 'do not run' },
|
|
43
|
-
{ key: '?', label: 'why', value: 'why', hint: 'show reasoning' },
|
|
44
|
-
];
|
|
44
|
+
return shared;
|
|
45
45
|
}
|
|
46
46
|
return [
|
|
47
|
-
|
|
47
|
+
shared[0],
|
|
48
48
|
{ key: 't', label: 'always allow', value: 'allow-type', hint: 'auto-approve future calls to this tool' },
|
|
49
|
-
|
|
50
|
-
{ key: '?', label: 'why', value: 'why', hint: 'show reasoning' },
|
|
49
|
+
...shared.slice(1),
|
|
51
50
|
];
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -88,22 +87,30 @@ export function renderApprovalPrompt({
|
|
|
88
87
|
const explicit = requiresExplicitApproval(tier);
|
|
89
88
|
const accent = explicit ? paint.brand.accent : paint.brand.data;
|
|
90
89
|
const opts = options || defaultOptions(tier);
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const toolLine = `${icon(tool)} ${paint.text.primary(toolDisplayLabel(tool))} ${paint.text.muted(summary ? `"${truncate(summary, cols - 20)}"` : '')}`;
|
|
90
|
+
const available = cols - 2 - PAD_X * 2;
|
|
91
|
+
const title = `${riskIcon(tier)} ${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`;
|
|
94
92
|
|
|
95
93
|
const lines = [
|
|
96
94
|
bar(cols, accent),
|
|
97
|
-
fill('
|
|
95
|
+
fill(' ' + ' '.repeat(PAD_X - 1) + paint.bold(accent(title)), cols, accent),
|
|
98
96
|
fill('', cols, accent),
|
|
99
|
-
|
|
97
|
+
...subjectRows(tool, args, cols, accent),
|
|
100
98
|
fill('', cols, accent),
|
|
101
|
-
fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim('Tier: ') + accent(tierLabel(tier)), cols, accent),
|
|
102
99
|
];
|
|
103
100
|
if (why) {
|
|
104
|
-
|
|
101
|
+
const whyLines = wrapText(why, Math.max(20, available - 6));
|
|
102
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim('Why ') + paint.text.primary(whyLines[0]), cols, accent));
|
|
103
|
+
for (const line of whyLines.slice(1, 4)) {
|
|
104
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim(' ') + paint.text.primary(line), cols, accent));
|
|
105
|
+
}
|
|
106
|
+
if (whyLines.length > 4) {
|
|
107
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim(' ...'), cols, accent));
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim('Why ') + paint.text.muted('No additional reason provided'), cols, accent));
|
|
105
111
|
}
|
|
106
112
|
lines.push(fill('', cols, accent));
|
|
113
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim('Decision:'), cols, accent));
|
|
107
114
|
|
|
108
115
|
for (let i = 0; i < opts.length; i++) {
|
|
109
116
|
const o = opts[i];
|
|
@@ -111,7 +118,7 @@ export function renderApprovalPrompt({
|
|
|
111
118
|
const cursor = isSel ? accent('▸ ') : paint.text.dim(' ');
|
|
112
119
|
const keyTag = paint.text.dim('[') + (isSel ? accent(o.key) : paint.brand.data(o.key)) + paint.text.dim('] ');
|
|
113
120
|
const label = isSel ? paint.bold(accent(o.label)) : paint.text.primary(o.label);
|
|
114
|
-
const hint = o.hint ? '
|
|
121
|
+
const hint = o.hint ? ' ' + paint.text.muted(o.hint) : '';
|
|
115
122
|
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + cursor + keyTag + label + hint, cols, accent));
|
|
116
123
|
}
|
|
117
124
|
|
|
@@ -119,12 +126,20 @@ export function renderApprovalPrompt({
|
|
|
119
126
|
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) +
|
|
120
127
|
paint.text.dim('↑↓ ') + paint.brand.data('move') +
|
|
121
128
|
paint.text.dim(' · Enter ') + paint.brand.data('pick') +
|
|
122
|
-
paint.text.dim(' ·
|
|
129
|
+
paint.text.dim(' · letter shortcut · Esc stop'), cols, accent));
|
|
123
130
|
lines.push(bar(cols, accent));
|
|
124
131
|
|
|
125
132
|
return '\n' + lines.join('\n');
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
export function renderTrustedApproval({ tool, args = {}, scope = 'session', ruleId = '', delaySeconds = 0 } = {}) {
|
|
136
|
+
const summary = toolDisplaySummary(tool, args, {});
|
|
137
|
+
const subject = `${tool || 'tool'}${summary ? ` "${truncate(summary, 80)}"` : ''}`;
|
|
138
|
+
const rule = ruleId ? ` · rule ${ruleId}` : '';
|
|
139
|
+
const delay = delaySeconds > 0 ? ` · auto-approved after ${delaySeconds}s` : '';
|
|
140
|
+
return ` ${paint.state.success('✓')} ${paint.text.primary(subject)} ${paint.text.dim(`· pre-approved (${String(scope || 'session').toLowerCase()}${rule})${delay}`)}\n`;
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
function bar(width, painter) {
|
|
129
144
|
// Top / bottom rule: a magenta vertical-stack line spanning the full width.
|
|
130
145
|
return painter('▔'.repeat(width));
|
|
@@ -142,26 +157,130 @@ function truncate(text, n) {
|
|
|
142
157
|
return s.slice(0, Math.max(0, n - 1)) + '…';
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
// ──
|
|
160
|
+
// ── Compatibility wrapper ──────────────────────────────────────────────
|
|
146
161
|
|
|
147
162
|
/**
|
|
148
|
-
* Render the
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* ? Run `npm install lodash`? Tier: SHELL-MEDIUM [Enter=yes n=no ?=why]
|
|
163
|
+
* Render the unified approval prompt. Kept as a named export for older call
|
|
164
|
+
* sites/tests that imported the previous inline renderer.
|
|
152
165
|
*/
|
|
153
166
|
export function renderInlinePrompt({ tool, args = {}, tier, why = '' } = {}) {
|
|
154
|
-
|
|
155
|
-
const label = toolDisplayLabel(tool);
|
|
156
|
-
const lines = [];
|
|
157
|
-
const head = `${paint.brand.data('?')} ${paint.text.primary(label)} ${paint.text.muted(summary ? `"${truncate(summary, 80)}"` : '')}`;
|
|
158
|
-
lines.push(' ' + head);
|
|
159
|
-
lines.push(' ' + paint.text.dim('Tier ') + paint.brand.data(tierLabel(tier)) +
|
|
160
|
-
(why ? ' ' + paint.text.dim('Why ') + paint.text.muted(truncate(why, 60)) : ''));
|
|
161
|
-
lines.push(' ' + paint.text.dim('[') + paint.brand.data('Enter') + paint.text.dim('=yes ') +
|
|
162
|
-
paint.brand.data('n') + paint.text.dim('=no ') +
|
|
163
|
-
paint.brand.data('?') + paint.text.dim('=why]'));
|
|
164
|
-
return '\n' + lines.join('\n');
|
|
167
|
+
return renderApprovalPrompt({ tool, args, tier, why });
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
export { TIERS };
|
|
171
|
+
|
|
172
|
+
function riskIcon(tier) {
|
|
173
|
+
switch (tier) {
|
|
174
|
+
case TIERS.SENSITIVE_READ:
|
|
175
|
+
return '🔐';
|
|
176
|
+
case TIERS.SHELL_DANGEROUS:
|
|
177
|
+
case TIERS.DESTRUCTIVE:
|
|
178
|
+
return '🔴';
|
|
179
|
+
case TIERS.SHELL_MEDIUM:
|
|
180
|
+
return '⚠';
|
|
181
|
+
case TIERS.NETWORK:
|
|
182
|
+
return '🌐';
|
|
183
|
+
case TIERS.LOCAL_EDIT:
|
|
184
|
+
return '✎';
|
|
185
|
+
default:
|
|
186
|
+
return '◈';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function tierTitle(tier) {
|
|
191
|
+
switch (tier) {
|
|
192
|
+
case TIERS.SENSITIVE_READ: return 'SENSITIVE';
|
|
193
|
+
case TIERS.SHELL_DANGEROUS: return 'DANGEROUS';
|
|
194
|
+
case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
|
|
195
|
+
case TIERS.SHELL_MEDIUM: return 'MEDIUM';
|
|
196
|
+
case TIERS.NETWORK: return 'NETWORK';
|
|
197
|
+
case TIERS.LOCAL_EDIT: return 'EDIT';
|
|
198
|
+
case TIERS.SHELL_SAFE: return 'SAFE';
|
|
199
|
+
case TIERS.READ: return 'READ';
|
|
200
|
+
default: return tierLabel(tier);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function approvalTitle(tier) {
|
|
205
|
+
switch (tier) {
|
|
206
|
+
case TIERS.SENSITIVE_READ:
|
|
207
|
+
case TIERS.SHELL_DANGEROUS:
|
|
208
|
+
case TIERS.DESTRUCTIVE:
|
|
209
|
+
return tierTitle(tier);
|
|
210
|
+
default:
|
|
211
|
+
return 'APPROVAL';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function subjectRows(tool, args, cols, accent) {
|
|
216
|
+
const available = cols - 2 - PAD_X * 2;
|
|
217
|
+
const prefix = ' ' + ' '.repeat(PAD_X - 1);
|
|
218
|
+
const rows = [];
|
|
219
|
+
const summary = toolDisplaySummary(tool, args, {});
|
|
220
|
+
const label = `${icon(tool)} ${paint.text.primary(toolDisplayLabel(tool))}`;
|
|
221
|
+
rows.push(fill(prefix + label, cols, accent));
|
|
222
|
+
|
|
223
|
+
for (const line of subjectDetails(tool, args, summary, available)) {
|
|
224
|
+
rows.push(fill(prefix + paint.text.primary(line), cols, accent));
|
|
225
|
+
}
|
|
226
|
+
return rows;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function subjectDetails(tool, args = {}, summary = '', available = 72) {
|
|
230
|
+
if (tool === 'shell') {
|
|
231
|
+
const command = args.command || args.cmd || summary || '';
|
|
232
|
+
const lines = wrapText(command, available);
|
|
233
|
+
return lines.length ? lines : ['(empty command)'];
|
|
234
|
+
}
|
|
235
|
+
if (tool === 'write_file') {
|
|
236
|
+
const file = args.file_path || args.path || summary || '';
|
|
237
|
+
const lineCount = typeof args.content === 'string' ? args.content.split('\n').length : null;
|
|
238
|
+
return [`${file}${lineCount ? ` · ${lineCount} lines` : ''}`];
|
|
239
|
+
}
|
|
240
|
+
if (tool === 'edit_file') {
|
|
241
|
+
const file = args.file_path || args.path || '';
|
|
242
|
+
const search = String(args.search || args.old_string || '').trim();
|
|
243
|
+
const replacement = String(args.replace || args.new_string || '').trim();
|
|
244
|
+
const details = [`${file || summary}`];
|
|
245
|
+
if (search) details.push(`match: ${truncate(search, available - 7)}`);
|
|
246
|
+
if (replacement) details.push(`replace: ${truncate(replacement, available - 9)}`);
|
|
247
|
+
return details.slice(0, 4);
|
|
248
|
+
}
|
|
249
|
+
if (tool === 'delete_file') {
|
|
250
|
+
return [args.file_path || args.path || summary || ''];
|
|
251
|
+
}
|
|
252
|
+
if (tool === 'WebFetch' || tool === 'fetch_url') {
|
|
253
|
+
const url = args.url || summary || '';
|
|
254
|
+
return [`Target ${hostFromUrl(url) || url}`];
|
|
255
|
+
}
|
|
256
|
+
return wrapText(summary || JSON.stringify(args || {}), available).slice(0, 4);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function hostFromUrl(url) {
|
|
260
|
+
try {
|
|
261
|
+
return new URL(String(url)).host;
|
|
262
|
+
} catch {
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function wrapText(text, width) {
|
|
268
|
+
const words = String(text || '').split(/\s+/).filter(Boolean);
|
|
269
|
+
if (!words.length) return [''];
|
|
270
|
+
const lines = [];
|
|
271
|
+
let line = '';
|
|
272
|
+
for (const word of words) {
|
|
273
|
+
if (!line) {
|
|
274
|
+
line = word;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if ((line + ' ' + word).length <= width) {
|
|
278
|
+
line += ' ' + word;
|
|
279
|
+
} else {
|
|
280
|
+
lines.push(line);
|
|
281
|
+
line = word;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (line) lines.push(line);
|
|
285
|
+
return lines;
|
|
286
|
+
}
|
package/src/ui/formatter.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
12
|
+
import { formatMessageWindow } from '../core/rate-limit-display.mjs';
|
|
12
13
|
|
|
13
14
|
const RESET = '\x1b[0m';
|
|
14
15
|
const BOLD = '\x1b[1m';
|
|
@@ -65,6 +66,9 @@ export class EventFormatter {
|
|
|
65
66
|
this._toolDone(data);
|
|
66
67
|
return true;
|
|
67
68
|
case 'complete':
|
|
69
|
+
if (data?.rate_limit) {
|
|
70
|
+
this.sessionInfo = { ...(this.sessionInfo || {}), rate_limit: data.rate_limit };
|
|
71
|
+
}
|
|
68
72
|
this._complete(data);
|
|
69
73
|
return true;
|
|
70
74
|
case 'error':
|
|
@@ -121,12 +125,11 @@ export class EventFormatter {
|
|
|
121
125
|
|
|
122
126
|
_status(data) {
|
|
123
127
|
const msg = data?.message || '';
|
|
124
|
-
// Skip noisy statuses
|
|
128
|
+
// Skip noisy per-turn statuses. Backend emits "Creating agent..." and
|
|
129
|
+
// "Task type: ..." on every SSE turn (v3_sse.py:566), not just the first —
|
|
130
|
+
// repeating them clutters the transcript.
|
|
125
131
|
if (!msg || msg === 'Agent started') return;
|
|
126
|
-
if (msg.startsWith('Creating agent') || msg.startsWith('Task type:'))
|
|
127
|
-
process.stderr.write(`${DIM} ${msg}${RESET}\n`);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
132
|
+
if (msg.startsWith('Creating agent') || msg.startsWith('Task type:')) return;
|
|
130
133
|
process.stderr.write(` ${this._spinner()} ${CYAN}${msg}${RESET}\n`);
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -183,6 +186,14 @@ export class EventFormatter {
|
|
|
183
186
|
|
|
184
187
|
const label = toolDisplayLabel(tool);
|
|
185
188
|
const summary = toolDisplaySummary(tool, args);
|
|
189
|
+
if (tool === 'shell' && summary) {
|
|
190
|
+
process.stderr.write(` ${this._spinner()} [${this.toolCount}] ${CYAN}${label}${RESET}\n`);
|
|
191
|
+
for (const line of wrapShellSummary(summary, 100)) {
|
|
192
|
+
process.stderr.write(` ${DIM} ${line}${RESET}\n`);
|
|
193
|
+
}
|
|
194
|
+
this.toolCalls.push({ name: tool, callId, startTime: Date.now() });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
186
197
|
const detail = summary ? `${DIM}${summary}${RESET}` : '';
|
|
187
198
|
process.stderr.write(` ${this._spinner()} [${this.toolCount}] ${CYAN}${label}${RESET}${detail ? ` ${detail}` : ''}\n`);
|
|
188
199
|
|
|
@@ -334,6 +345,11 @@ export class EventFormatter {
|
|
|
334
345
|
process.stderr.write(`\n ${GREEN}✓ Done${stats}${RESET}\n`);
|
|
335
346
|
}
|
|
336
347
|
|
|
348
|
+
const rateLimitLine = formatMessageWindow(data?.rate_limit || this.sessionInfo?.rate_limit);
|
|
349
|
+
if (rateLimitLine) {
|
|
350
|
+
process.stderr.write(` ${DIM}Messages: ${rateLimitLine}${RESET}\n`);
|
|
351
|
+
}
|
|
352
|
+
|
|
337
353
|
// Token usage if available
|
|
338
354
|
const usage = data?.usage;
|
|
339
355
|
if (usage && (usage.input_tokens || usage.total_tokens)) {
|
|
@@ -343,3 +359,21 @@ export class EventFormatter {
|
|
|
343
359
|
}
|
|
344
360
|
}
|
|
345
361
|
}
|
|
362
|
+
|
|
363
|
+
function wrapShellSummary(command, width = 100) {
|
|
364
|
+
const max = Math.max(32, Number(width) || 100);
|
|
365
|
+
const text = String(command || '');
|
|
366
|
+
const lines = [];
|
|
367
|
+
let line = '';
|
|
368
|
+
for (const token of text.match(/\S+\s*/g) || [text]) {
|
|
369
|
+
const next = line + token;
|
|
370
|
+
if (line && next.trimEnd().length > max) {
|
|
371
|
+
lines.push(line.trimEnd());
|
|
372
|
+
line = token;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
line = next;
|
|
376
|
+
}
|
|
377
|
+
if (line.trimEnd()) lines.push(line.trimEnd());
|
|
378
|
+
return lines.length ? lines : ['(empty command)'];
|
|
379
|
+
}
|