@axplusb/kepler 1.0.10 → 2.0.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 +5 -2
- package/src/context/retriever.mjs +42 -4
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/approval.mjs +154 -95
- package/src/core/backend-url.mjs +2 -2
- package/src/core/headless.mjs +5 -0
- package/src/core/risk-tier.mjs +239 -0
- package/src/core/tool-executor.mjs +49 -3
- package/src/onboarding/preflight.mjs +274 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/terminal/ansi.mjs +44 -22
- package/src/terminal/repl.mjs +395 -108
- package/src/ui/approval.mjs +167 -0
- package/src/ui/banner.mjs +133 -122
- package/src/ui/dock.mjs +88 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/mission-report.mjs +264 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/status-bar.mjs +275 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/tool-card.mjs +314 -0
- package/src/ui/tool-details.mjs +277 -0
package/src/terminal/repl.mjs
CHANGED
|
@@ -16,13 +16,21 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import * as readline from 'node:readline';
|
|
19
|
-
import * as path from 'node:path';
|
|
20
19
|
import * as fs from 'node:fs';
|
|
21
20
|
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
22
21
|
import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
23
22
|
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
24
23
|
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
25
24
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
25
|
+
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
26
|
+
import { runPreflight } from '../onboarding/preflight.mjs';
|
|
27
|
+
import { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '../ui/mission-report.mjs';
|
|
28
|
+
import {
|
|
29
|
+
getVerbosity,
|
|
30
|
+
setVerbosity,
|
|
31
|
+
label as verbosityLabel,
|
|
32
|
+
MODES as V_MODES,
|
|
33
|
+
} from '../state/verbosity.mjs';
|
|
26
34
|
import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
27
35
|
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
28
36
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
@@ -30,7 +38,27 @@ import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
|
30
38
|
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
31
39
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
32
40
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
33
|
-
import {
|
|
41
|
+
import { toolDisplayLabel } from './tool-display.mjs';
|
|
42
|
+
import { createOrbit } from '../state/orbit.mjs';
|
|
43
|
+
import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
|
|
44
|
+
import { term } from '../ui/term.mjs';
|
|
45
|
+
import {
|
|
46
|
+
formatCardHead,
|
|
47
|
+
summarizeResult,
|
|
48
|
+
recordCard,
|
|
49
|
+
lastCard,
|
|
50
|
+
getCard,
|
|
51
|
+
allCards,
|
|
52
|
+
} from '../ui/tool-card.mjs';
|
|
53
|
+
import { detailFor } from '../ui/tool-details.mjs';
|
|
54
|
+
import { paint } from '../ui/palette.mjs';
|
|
55
|
+
import {
|
|
56
|
+
renderSubAgentOpen,
|
|
57
|
+
renderSubAgentClose,
|
|
58
|
+
subAgentIndent,
|
|
59
|
+
inSubAgent as inSubAgentBlock,
|
|
60
|
+
resetSubAgents,
|
|
61
|
+
} from '../ui/sub-agent.mjs';
|
|
34
62
|
|
|
35
63
|
import { createRequire } from 'node:module';
|
|
36
64
|
const __require = createRequire(import.meta.url);
|
|
@@ -63,6 +91,7 @@ function safeCwd() {
|
|
|
63
91
|
// ── Session State ──
|
|
64
92
|
|
|
65
93
|
let _sessionMgr = null; // Set in startTerminalRepl, used by renderEvent
|
|
94
|
+
let _orbit = null; // Mission Control orbit state machine; set in startTerminalRepl
|
|
66
95
|
|
|
67
96
|
const session = {
|
|
68
97
|
id: null, // set by backend on first turn via session_info event
|
|
@@ -82,6 +111,13 @@ const session = {
|
|
|
82
111
|
inSubAgent: false, // true while a sub-agent is running (for indented tool display)
|
|
83
112
|
filesChanged: [], // files modified this session
|
|
84
113
|
lastTurnDuration: 0,
|
|
114
|
+
toolCounts: {}, // per-tool histogram (mission report)
|
|
115
|
+
subAgentCounts: {}, // per-sub-agent histogram (mission report)
|
|
116
|
+
savedUsd: 0, // total sub-agent cost (for "saved by routing")
|
|
117
|
+
lastTask: '', // most recent user prompt (mission report title)
|
|
118
|
+
lastReasoning: '', // captured from agent for /why
|
|
119
|
+
budgetUsd: null, // /budget cap, null = unlimited
|
|
120
|
+
budgetExceeded: false,
|
|
85
121
|
costBreakdown: [], // per-model usage: [{ model, role, input_tokens, output_tokens, cost }]
|
|
86
122
|
totalCost: 0, // accumulated session cost (USD)
|
|
87
123
|
costAccurate: false, // true if backend provides per-model breakdown
|
|
@@ -100,6 +136,19 @@ const COMMANDS = {
|
|
|
100
136
|
'/diff': 'Git diff',
|
|
101
137
|
'/cost': 'Show session cost',
|
|
102
138
|
'/history': 'Show conversation',
|
|
139
|
+
'/last': 'Expand last tool output',
|
|
140
|
+
'/expand': 'Expand tool output by index (or "all")',
|
|
141
|
+
'/fold': 'Hide previously expanded tool output',
|
|
142
|
+
'/checkpoint':'List recent file checkpoints',
|
|
143
|
+
'/undo': 'Restore the last file checkpoint',
|
|
144
|
+
'/preflight':'Re-run the onboarding diagnostic',
|
|
145
|
+
'/report': 'Save the mission report as markdown',
|
|
146
|
+
'/why': 'Print the agent reasoning for the last decision',
|
|
147
|
+
'/map': 'Show the registered project tree',
|
|
148
|
+
'/budget': 'Set / clear a hard session cost cap',
|
|
149
|
+
'/quiet': 'Verbosity: hide sub-agent inner tools',
|
|
150
|
+
'/verbose': 'Verbosity: show sub-agent inner tools',
|
|
151
|
+
'/surgical': 'Verbosity: show everything (reasoning, expanded tools)',
|
|
103
152
|
'/compact': 'Compact conversation context',
|
|
104
153
|
'/agents': 'List available agents',
|
|
105
154
|
'/explore': 'Code explorer agent',
|
|
@@ -195,6 +244,22 @@ function printPromptBlock() {
|
|
|
195
244
|
* Print a turn summary after a response completes.
|
|
196
245
|
* Shows only when there's something meaningful to report.
|
|
197
246
|
*/
|
|
247
|
+
/**
|
|
248
|
+
* Pull blocker bullet points from the completion payload — used by the
|
|
249
|
+
* failure variant of the mission report.
|
|
250
|
+
*/
|
|
251
|
+
function extractBlockers(data) {
|
|
252
|
+
const out = [];
|
|
253
|
+
if (data?.error) out.push(String(data.error).slice(0, 160));
|
|
254
|
+
if (Array.isArray(data?.failed_tests)) {
|
|
255
|
+
for (const t of data.failed_tests.slice(0, 6)) {
|
|
256
|
+
if (typeof t === 'string') out.push(t);
|
|
257
|
+
else if (t?.name) out.push(`${t.name}${t.message ? ': ' + t.message : ''}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
198
263
|
function printTurnSummary(toolCount, durationS, turnCost) {
|
|
199
264
|
const parts = [];
|
|
200
265
|
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
@@ -212,31 +277,25 @@ function updateStatusBar() {
|
|
|
212
277
|
// ── Tool Display Renderer ──
|
|
213
278
|
|
|
214
279
|
/**
|
|
215
|
-
* Render a tool call
|
|
216
|
-
*
|
|
280
|
+
* Render a tool call as the head of a Mission Control card — icon + label +
|
|
281
|
+
* args. The result arrives later via `renderToolResult` and is appended as a
|
|
282
|
+
* gutter line. Sub-agent calls are indented per session.inSubAgent.
|
|
217
283
|
*/
|
|
218
284
|
function renderToolCall(data) {
|
|
219
285
|
const tool = data?.tool || 'unknown';
|
|
220
|
-
const label = toolDisplayLabel(tool);
|
|
221
286
|
const args = data?.args || {};
|
|
222
|
-
const indent =
|
|
287
|
+
const indent = subAgentIndent();
|
|
288
|
+
const callId = data?.call_id || data?._callId || `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
223
289
|
|
|
224
|
-
const
|
|
290
|
+
const head = formatCardHead(tool, args, {
|
|
291
|
+
cwd: safeCwd(),
|
|
292
|
+
columns: process.stderr.columns || 120,
|
|
293
|
+
indent,
|
|
294
|
+
});
|
|
225
295
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const maxSummary = Math.max(60, cols - label.length - 10);
|
|
230
|
-
let displaySummary = summary || '';
|
|
231
|
-
if (displaySummary.length > maxSummary) {
|
|
232
|
-
displaySummary = '...' + displaySummary.slice(-(maxSummary - 3));
|
|
233
|
-
}
|
|
234
|
-
const summaryStr = displaySummary
|
|
235
|
-
? c.gray('(') + (tool === 'shell'
|
|
236
|
-
? formatShellCommand(displaySummary, c)
|
|
237
|
-
: c.white(displaySummary)) + c.gray(')')
|
|
238
|
-
: '';
|
|
239
|
-
process.stderr.write(`\n${indent}${c.brand('⏺')} ${c.bold(label)}${summaryStr}\n`);
|
|
296
|
+
recordCard({ id: callId, tool, args, head, startedAt: Date.now() });
|
|
297
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
298
|
+
process.stderr.write(`\n${head}\n`);
|
|
240
299
|
}
|
|
241
300
|
|
|
242
301
|
/**
|
|
@@ -250,77 +309,71 @@ function formatToolDuration(data) {
|
|
|
250
309
|
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
251
310
|
}
|
|
252
311
|
|
|
253
|
-
function firstOutputLine(data) {
|
|
254
|
-
const output = data?.output_preview || data?.output || data?.message || '';
|
|
255
|
-
return String(output).split('\n').map(line => line.trim()).find(Boolean) || '';
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function fileTypeLabel(filePath) {
|
|
259
|
-
const ext = path.extname(filePath || '').toLowerCase();
|
|
260
|
-
if (ext === '.md' || ext === '.mdx') return 'Markdown';
|
|
261
|
-
if (ext === '.json' || ext === '.jsonl') return 'JSON';
|
|
262
|
-
if (ext === '.yaml' || ext === '.yml') return 'YAML';
|
|
263
|
-
if (ext === '.toml') return 'TOML';
|
|
264
|
-
if (ext === '.csv' || ext === '.tsv') return 'tabular data';
|
|
265
|
-
if (['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.swift'].includes(ext)) {
|
|
266
|
-
return 'source';
|
|
267
|
-
}
|
|
268
|
-
return ext ? `${ext.slice(1).toUpperCase()} file` : 'file';
|
|
269
|
-
}
|
|
270
|
-
|
|
271
312
|
function renderToolResult(data, eventType = 'tool_result') {
|
|
272
313
|
if (!data) return;
|
|
273
|
-
const indent =
|
|
274
|
-
const gutter = `${indent}${
|
|
314
|
+
const indent = subAgentIndent();
|
|
315
|
+
const gutter = `${indent}${paint.text.dim('⎿')} `;
|
|
275
316
|
const callId = data.call_id || data._callId;
|
|
276
317
|
if (eventType === 'tool_done' && callId && _renderedToolResults.has(callId)) return;
|
|
277
318
|
if (callId) _renderedToolResults.add(callId);
|
|
319
|
+
|
|
320
|
+
const tool = data.tool || data._tool || '';
|
|
321
|
+
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
322
|
+
|
|
323
|
+
// Update the card buffer so /last and `d` can find it.
|
|
324
|
+
if (callId) recordCard({ id: callId, tool, args: data.args, result: data, durationMs });
|
|
325
|
+
|
|
326
|
+
if (data._blocked) session.blockedOps++;
|
|
327
|
+
|
|
328
|
+
const { text, tone: t } = summarizeResult(tool, data);
|
|
329
|
+
const arrow = paint.text.dim('→');
|
|
330
|
+
const painter = t === 'success' ? paint.state.success
|
|
331
|
+
: t === 'warn' ? paint.state.warn
|
|
332
|
+
: t === 'danger' ? paint.state.danger
|
|
333
|
+
: paint.text.dim;
|
|
278
334
|
const duration = formatToolDuration(data);
|
|
279
|
-
const
|
|
335
|
+
const tail = duration ? paint.text.dim(` · ${duration}`) : '';
|
|
336
|
+
process.stderr.write(`${gutter}${arrow} ${painter(text || 'done')}${tail}\n`);
|
|
280
337
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
process.stderr.write(`${gutter}${
|
|
284
|
-
return;
|
|
338
|
+
// Lint warnings stay visible alongside writes.
|
|
339
|
+
if ((tool === 'write_file' || tool === 'edit_file') && data.lint) {
|
|
340
|
+
process.stderr.write(`${gutter}${paint.state.warn('⚠ ' + String(data.lint).split('\n')[0].slice(0, 80))}\n`);
|
|
285
341
|
}
|
|
342
|
+
}
|
|
286
343
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
344
|
+
// ── Expand handler — `d`, `/last`, `/expand` ───────────────────────────
|
|
345
|
+
//
|
|
346
|
+
// All three call into the same renderer so output is consistent across
|
|
347
|
+
// keypress and slash-command paths. `expandLast` and `expandIndex` write
|
|
348
|
+
// directly to stderr.
|
|
349
|
+
|
|
350
|
+
function expandLast() {
|
|
351
|
+
const card = lastCard();
|
|
352
|
+
if (!card) {
|
|
353
|
+
process.stderr.write(` ${paint.text.dim('(no tool to expand yet)')}\n`);
|
|
290
354
|
return;
|
|
291
355
|
}
|
|
356
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
357
|
+
}
|
|
292
358
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
} else if (tool === 'read_files') {
|
|
300
|
-
summary = 'Files read';
|
|
301
|
-
} else if (tool === 'search_code' || tool === 'list_files') {
|
|
302
|
-
const lines = String(data.output || '').split('\n').filter(line => line.trim()).length;
|
|
303
|
-
summary = lines > 0 ? `${lines} result${lines === 1 ? '' : 's'}` : 'No results';
|
|
304
|
-
} else if (tool === 'write_file' || tool === 'edit_file' || tool === 'write_project') {
|
|
305
|
-
summary = 'Updated';
|
|
306
|
-
} else if (tool === 'delete_file') {
|
|
307
|
-
summary = 'Deleted';
|
|
308
|
-
} else if (data.server_side) {
|
|
309
|
-
summary = firstOutputLine(data).slice(0, 100) || 'Completed server-side';
|
|
310
|
-
} else if (tool === 'shell') {
|
|
311
|
-
summary = firstOutputLine(data).slice(0, 100) || 'Command completed';
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const renderedSummary = tool === 'shell' ? c.green(summary) : c.white(summary);
|
|
315
|
-
process.stderr.write(`${gutter}${renderedSummary}${suffix}\n`);
|
|
316
|
-
|
|
317
|
-
// For writes, show lint warnings
|
|
318
|
-
if (tool === 'write_file' || tool === 'edit_file') {
|
|
319
|
-
const lint = data.lint;
|
|
320
|
-
if (lint) {
|
|
321
|
-
process.stderr.write(`${gutter}${c.yellow('⚠ ' + lint.split('\n')[0].slice(0, 80))}\n`);
|
|
359
|
+
function expandIndex(idxOrAll) {
|
|
360
|
+
if (idxOrAll === 'all') {
|
|
361
|
+
const cards = allCards();
|
|
362
|
+
if (!cards.length) {
|
|
363
|
+
process.stderr.write(` ${paint.text.dim('(no tools to expand yet)')}\n`);
|
|
364
|
+
return;
|
|
322
365
|
}
|
|
366
|
+
process.stderr.write('\n');
|
|
367
|
+
for (const c of cards) process.stderr.write(detailFor(c) + '\n');
|
|
368
|
+
process.stderr.write('\n');
|
|
369
|
+
return;
|
|
323
370
|
}
|
|
371
|
+
const card = getCard(idxOrAll);
|
|
372
|
+
if (!card) {
|
|
373
|
+
process.stderr.write(` ${paint.text.dim('(no card at index ' + idxOrAll + ')')}\n`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
324
377
|
}
|
|
325
378
|
|
|
326
379
|
/**
|
|
@@ -409,6 +462,11 @@ function flushContent() {
|
|
|
409
462
|
function renderEvent(event) {
|
|
410
463
|
const { type, data } = event;
|
|
411
464
|
|
|
465
|
+
// Push every event into the orbit state machine before rendering so the
|
|
466
|
+
// bottom status bar reflects what is happening this very moment. The orbit
|
|
467
|
+
// module is a no-op when status-bar is not mounted (non-TTY, --headless).
|
|
468
|
+
if (_orbit) _orbit.onEvent(event);
|
|
469
|
+
|
|
412
470
|
switch (type) {
|
|
413
471
|
case 'status': {
|
|
414
472
|
const msg = data?.message || '';
|
|
@@ -421,6 +479,8 @@ function renderEvent(event) {
|
|
|
421
479
|
const text = data?.message || data?.text || '';
|
|
422
480
|
if (text && !text.startsWith('Processing')) {
|
|
423
481
|
startSpinner(text.slice(0, 80));
|
|
482
|
+
// Capture reasoning so /why can replay it.
|
|
483
|
+
session.lastReasoning = text;
|
|
424
484
|
}
|
|
425
485
|
break;
|
|
426
486
|
}
|
|
@@ -482,7 +542,7 @@ function renderEvent(event) {
|
|
|
482
542
|
case 'approval_denied': {
|
|
483
543
|
const reason = data?.reason || 'User denied';
|
|
484
544
|
const toolName = data?.tool || '';
|
|
485
|
-
const indent =
|
|
545
|
+
const indent = subAgentIndent();
|
|
486
546
|
process.stderr.write(`${indent}${c.red('✗')} ${c.dim(`Denied ${toolName}: ${reason}`)}\n`);
|
|
487
547
|
break;
|
|
488
548
|
}
|
|
@@ -576,21 +636,19 @@ function renderEvent(event) {
|
|
|
576
636
|
|
|
577
637
|
case 'sub_agent_start': {
|
|
578
638
|
stopSpinner();
|
|
579
|
-
session.inSubAgent = true;
|
|
580
639
|
const agentType = data?.type || 'sub-agent';
|
|
581
640
|
const model = data?.model || '';
|
|
582
641
|
const query = data?.query || '';
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (query) process.stderr.write(` ${c.gray('query:')} ${c.dim(query)}\n`);
|
|
642
|
+
process.stderr.write(renderSubAgentOpen({ type: agentType, model, query }) + '\n');
|
|
643
|
+
session.inSubAgent = inSubAgentBlock(); // kept for legacy readers
|
|
644
|
+
session.subAgentCounts[agentType] = (session.subAgentCounts[agentType] || 0) + 1;
|
|
587
645
|
startSpinner(`${agentType}: working...`);
|
|
588
646
|
break;
|
|
589
647
|
}
|
|
590
648
|
|
|
591
649
|
case 'sub_agent_tool': {
|
|
592
|
-
//
|
|
593
|
-
//
|
|
650
|
+
// The regular tool_call event renders the card, indented by the
|
|
651
|
+
// sub-agent stack depth. Just update the spinner text here.
|
|
594
652
|
const agentType = data?.type || 'sub-agent';
|
|
595
653
|
const tool = data?.tool || '';
|
|
596
654
|
if (tool) updateSpinner(`${agentType} → ${tool}`);
|
|
@@ -599,24 +657,25 @@ function renderEvent(event) {
|
|
|
599
657
|
|
|
600
658
|
case 'sub_agent_complete': {
|
|
601
659
|
stopSpinner();
|
|
602
|
-
session.inSubAgent = false;
|
|
603
660
|
const agentType = data?.type || 'sub-agent';
|
|
604
|
-
const model = data?.model || '';
|
|
605
|
-
const resultLen = data?.result_length || 0;
|
|
606
661
|
const usage = data?.usage || {};
|
|
607
662
|
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
608
|
-
const
|
|
609
|
-
if (
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
663
|
+
const costUsd = usage.cost_usd ?? usage.total_cost_usd ?? data?.cost_usd ?? null;
|
|
664
|
+
if (typeof costUsd === 'number') session.savedUsd += costUsd;
|
|
665
|
+
const summary = data?.result_summary
|
|
666
|
+
|| (data?.result_length > 0 ? `${agentType} returned ${data.result_length} chars` : '');
|
|
667
|
+
process.stderr.write(renderSubAgentClose({
|
|
668
|
+
type: agentType,
|
|
669
|
+
success: data?.success !== false,
|
|
670
|
+
summary,
|
|
671
|
+
costUsd,
|
|
672
|
+
tokens,
|
|
673
|
+
durationS: data?.duration_s,
|
|
674
|
+
toolCalls: data?.tool_calls,
|
|
675
|
+
iterations: data?.iterations,
|
|
676
|
+
error: data?.error,
|
|
677
|
+
}) + '\n\n');
|
|
678
|
+
session.inSubAgent = inSubAgentBlock();
|
|
620
679
|
break;
|
|
621
680
|
}
|
|
622
681
|
|
|
@@ -653,6 +712,8 @@ function renderEvent(event) {
|
|
|
653
712
|
case 'complete': {
|
|
654
713
|
stopSpinner();
|
|
655
714
|
flushContent();
|
|
715
|
+
resetSubAgents();
|
|
716
|
+
session.inSubAgent = false;
|
|
656
717
|
|
|
657
718
|
const summary = data?.summary || '';
|
|
658
719
|
if (summary && !_renderedContentThisTurn) {
|
|
@@ -695,9 +756,38 @@ function renderEvent(event) {
|
|
|
695
756
|
|
|
696
757
|
session.lastTurnDuration = data?.duration_s || 0;
|
|
697
758
|
|
|
759
|
+
// Sync cumulative session cost into the orbit (status bar shows it).
|
|
760
|
+
if (_orbit) _orbit.onCost(session.totalCost);
|
|
761
|
+
|
|
698
762
|
// Compact turn summary
|
|
699
763
|
const tools = data?.tool_calls || session.toolCalls || 0;
|
|
700
|
-
|
|
764
|
+
|
|
765
|
+
// Mission report — replaces the trailing "Done" when the turn did real
|
|
766
|
+
// work (touched files or invoked tools). Plain chat turns keep the
|
|
767
|
+
// tight printTurnSummary so the report does not feel ceremonial.
|
|
768
|
+
const didRealWork = tools > 0 || session.filesChanged.length > 0;
|
|
769
|
+
if (didRealWork) {
|
|
770
|
+
const successOverall = data?.success !== false;
|
|
771
|
+
const report = renderMissionReport({
|
|
772
|
+
task: session.lastTask,
|
|
773
|
+
success: successOverall,
|
|
774
|
+
filesChanged: session.filesChanged,
|
|
775
|
+
toolCounts: session.toolCounts,
|
|
776
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.savedUsd },
|
|
777
|
+
costUsd: turnCost || session.totalCost,
|
|
778
|
+
durationS: data?.duration_s,
|
|
779
|
+
testsPass: data?.tests_passed != null
|
|
780
|
+
? { passed: data.tests_passed, total: data.tests_total || data.tests_passed }
|
|
781
|
+
: null,
|
|
782
|
+
blockers: !successOverall ? (data?.blockers || extractBlockers(data)) : null,
|
|
783
|
+
nextActions: successOverall
|
|
784
|
+
? ['/commit', '/pr', '/undo', '/report']
|
|
785
|
+
: ['/why', '/undo', '/re-plan'],
|
|
786
|
+
});
|
|
787
|
+
process.stderr.write(report + '\n');
|
|
788
|
+
} else {
|
|
789
|
+
printTurnSummary(tools, data?.duration_s, turnCost);
|
|
790
|
+
}
|
|
701
791
|
break;
|
|
702
792
|
}
|
|
703
793
|
|
|
@@ -736,7 +826,8 @@ async function handleCommand(input, ctx) {
|
|
|
736
826
|
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}\n`);
|
|
737
827
|
}
|
|
738
828
|
process.stderr.write(`\n ${c.bold('Keyboard')}\n`);
|
|
739
|
-
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n
|
|
829
|
+
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n`);
|
|
830
|
+
process.stderr.write(` ${c.gray('d')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
|
|
740
831
|
return;
|
|
741
832
|
|
|
742
833
|
case '/login':
|
|
@@ -908,6 +999,143 @@ async function handleCommand(input, ctx) {
|
|
|
908
999
|
process.stderr.write('\n');
|
|
909
1000
|
return;
|
|
910
1001
|
|
|
1002
|
+
case '/last':
|
|
1003
|
+
expandLast();
|
|
1004
|
+
return;
|
|
1005
|
+
|
|
1006
|
+
case '/expand': {
|
|
1007
|
+
const arg = rest.trim();
|
|
1008
|
+
if (!arg) { expandLast(); return; }
|
|
1009
|
+
if (arg === 'all') { expandIndex('all'); return; }
|
|
1010
|
+
const n = Number(arg);
|
|
1011
|
+
if (!Number.isFinite(n)) {
|
|
1012
|
+
process.stderr.write(` ${c.gray('Usage: /expand [n|all] — n is the 1-based index from the start of the session')}\n`);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
// Users pass 1-based; getCard accepts negative (-1 = last) or positive index.
|
|
1016
|
+
expandIndex(n > 0 ? n - 1 : n);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
case '/fold':
|
|
1021
|
+
process.stderr.write(` ${c.gray('Output is folded by default — there is nothing to hide. Use /last or d to expand.')}\n`);
|
|
1022
|
+
return;
|
|
1023
|
+
|
|
1024
|
+
case '/undo': {
|
|
1025
|
+
const result = ctx.checkpoints?.undo();
|
|
1026
|
+
if (!result) {
|
|
1027
|
+
process.stderr.write(` ${c.gray('No checkpoints to undo.')}\n`);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (result.restored) {
|
|
1031
|
+
process.stderr.write(` ${c.green('↩')} ${c.dim('Restored')} ${result.filePath}\n`);
|
|
1032
|
+
} else {
|
|
1033
|
+
process.stderr.write(` ${c.red('✗')} ${c.dim('Undo failed: ' + (result.error || 'unknown error'))}\n`);
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
case '/checkpoint': {
|
|
1039
|
+
const list = ctx.checkpoints?.list(10) || [];
|
|
1040
|
+
if (!list.length) {
|
|
1041
|
+
process.stderr.write(` ${c.gray('No checkpoints recorded yet — they are taken automatically before each edit.')}\n`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
process.stderr.write(`\n ${c.bold('Recent checkpoints')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1045
|
+
for (const ckpt of list) {
|
|
1046
|
+
const when = String(ckpt.timestamp).slice(11, 19);
|
|
1047
|
+
process.stderr.write(` ${c.gray(when)} ${c.white(ckpt.file)} ${c.gray(formatTokens(ckpt.size) + ' bytes')}\n`);
|
|
1048
|
+
}
|
|
1049
|
+
process.stderr.write(`\n ${c.gray('/undo restores the most recent one')}\n\n`);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
case '/preflight': {
|
|
1054
|
+
await runPreflight({ auth: ctx.auth, cwd: safeCwd(), version: VERSION });
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
case '/report': {
|
|
1059
|
+
if (Object.keys(session.toolCounts).length === 0 && session.filesChanged.length === 0) {
|
|
1060
|
+
process.stderr.write(` ${c.gray('Nothing to report yet — run a task first.')}\n`);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const state = {
|
|
1064
|
+
task: session.lastTask,
|
|
1065
|
+
success: true,
|
|
1066
|
+
filesChanged: session.filesChanged,
|
|
1067
|
+
toolCounts: session.toolCounts,
|
|
1068
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.savedUsd },
|
|
1069
|
+
costUsd: session.totalCost,
|
|
1070
|
+
durationS: (Date.now() - session.startTime) / 1000,
|
|
1071
|
+
nextActions: ['/commit', '/pr', '/undo'],
|
|
1072
|
+
};
|
|
1073
|
+
const out = saveReport(state, { cwd: safeCwd() });
|
|
1074
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Saved')} ${out}\n`);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
case '/why': {
|
|
1079
|
+
if (!session.lastReasoning) {
|
|
1080
|
+
process.stderr.write(` ${c.gray('No reasoning captured yet for this session.')}\n`);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
process.stderr.write(`\n ${c.bold('Last reasoning')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1084
|
+
for (const line of String(session.lastReasoning).split('\n')) {
|
|
1085
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
1086
|
+
}
|
|
1087
|
+
process.stderr.write('\n');
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
case '/map': {
|
|
1092
|
+
try {
|
|
1093
|
+
const resources = ctx.toolExecutor?.getProjectResources?.() || [];
|
|
1094
|
+
if (!resources.length) {
|
|
1095
|
+
process.stderr.write(` ${c.gray('No project resources registered yet. Use get_project_overview to register one.')}\n`);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
process.stderr.write(`\n ${c.bold('Registered projects')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1099
|
+
for (const r of resources) {
|
|
1100
|
+
process.stderr.write(` ${c.brand('•')} ${c.white(r.id || r.name || '?')} ${c.dim(r.root || r.path || '')}\n`);
|
|
1101
|
+
}
|
|
1102
|
+
process.stderr.write('\n');
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
process.stderr.write(` ${c.red('/map failed: ' + err.message)}\n`);
|
|
1105
|
+
}
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
case '/budget': {
|
|
1110
|
+
const arg = rest.trim();
|
|
1111
|
+
if (!arg || arg === 'clear' || arg === 'off') {
|
|
1112
|
+
session.budgetUsd = null;
|
|
1113
|
+
session.budgetExceeded = false;
|
|
1114
|
+
process.stderr.write(` ${c.gray('Budget cap cleared.')}\n`);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const n = Number(arg.replace(/^\$/, ''));
|
|
1118
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1119
|
+
process.stderr.write(` ${c.gray('Usage: /budget <amount in USD> or /budget clear')}\n`);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
session.budgetUsd = n;
|
|
1123
|
+
session.budgetExceeded = false;
|
|
1124
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Budget set: ')} $${n.toFixed(2)}\n`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
case '/quiet':
|
|
1129
|
+
case '/verbose':
|
|
1130
|
+
case '/surgical': {
|
|
1131
|
+
const mode = cmd === '/quiet' ? V_MODES.QUIET
|
|
1132
|
+
: cmd === '/verbose' ? V_MODES.VERBOSE
|
|
1133
|
+
: V_MODES.SURGICAL;
|
|
1134
|
+
setVerbosity(mode);
|
|
1135
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Verbosity: ')} ${c.brand(verbosityLabel(mode))}\n`);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
911
1139
|
case '/compact': {
|
|
912
1140
|
const before = session.history.length;
|
|
913
1141
|
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
@@ -1132,7 +1360,9 @@ export async function startTerminalRepl() {
|
|
|
1132
1360
|
const auth = new TarangAuth();
|
|
1133
1361
|
|
|
1134
1362
|
// Projects are registered and indexed on demand through get_project_overview.
|
|
1135
|
-
|
|
1363
|
+
// CheckpointManager records per-file snapshots before edits so /undo works.
|
|
1364
|
+
const checkpoints = new CheckpointManager(safeCwd());
|
|
1365
|
+
const toolExecutor = createToolExecutor({ checkpoints });
|
|
1136
1366
|
const skipPerms = cliArgs.freeswim;
|
|
1137
1367
|
const approval = new ApprovalManager({ autoApprove: skipPerms });
|
|
1138
1368
|
|
|
@@ -1146,10 +1376,30 @@ export async function startTerminalRepl() {
|
|
|
1146
1376
|
// Persistent stream client — session_id captured from backend on first turn
|
|
1147
1377
|
let streamClient = null;
|
|
1148
1378
|
|
|
1149
|
-
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr };
|
|
1379
|
+
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints };
|
|
1380
|
+
|
|
1381
|
+
// ── Mission Control orbit + status bar ──
|
|
1382
|
+
// Opt-out via KEPLER_STATUS_BAR=0 (debugging) or KEPLER_PLAIN=1 (PRD-055).
|
|
1383
|
+
// status-bar.mjs already no-ops when stdout is not a TTY, but the explicit
|
|
1384
|
+
// env opt-out is useful for debugging escape-sequence noise.
|
|
1385
|
+
const statusBarEnabled = process.env.KEPLER_STATUS_BAR !== '0' && term().isTTY && !term().plain;
|
|
1386
|
+
if (statusBarEnabled) {
|
|
1387
|
+
_orbit = createOrbit();
|
|
1388
|
+
attachOrbit(_orbit);
|
|
1389
|
+
// Always unmount before exit so the terminal scroll region is restored.
|
|
1390
|
+
process.on('beforeExit', unmountStatusBar);
|
|
1391
|
+
process.on('exit', unmountStatusBar);
|
|
1392
|
+
}
|
|
1150
1393
|
|
|
1151
1394
|
printBanner(auth);
|
|
1152
1395
|
|
|
1396
|
+
// Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
|
|
1397
|
+
// KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
|
|
1398
|
+
if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.freeswim) {
|
|
1399
|
+
try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
|
|
1400
|
+
catch { /* preflight is best-effort */ }
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1153
1403
|
// ── Initialization ──
|
|
1154
1404
|
process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
|
|
1155
1405
|
await fetchUser(ctx);
|
|
@@ -1230,10 +1480,27 @@ export async function startTerminalRepl() {
|
|
|
1230
1480
|
return;
|
|
1231
1481
|
}
|
|
1232
1482
|
|
|
1483
|
+
// Budget cap (PRD-055 §10). Stop before the next paid call when exceeded.
|
|
1484
|
+
if (session.budgetUsd && session.totalCost >= session.budgetUsd) {
|
|
1485
|
+
session.budgetExceeded = true;
|
|
1486
|
+
process.stderr.write(` ${c.yellow('⏹')} ${c.dim(`Budget reached ($${session.totalCost.toFixed(2)} of $${session.budgetUsd.toFixed(2)}). Use /budget clear to continue.`)}\n`);
|
|
1487
|
+
showPrompt();
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1233
1491
|
// Regular prompt
|
|
1234
1492
|
session.history.push({ role: 'user', content: input });
|
|
1235
1493
|
session.turns++;
|
|
1236
1494
|
session.toolCalls = 0;
|
|
1495
|
+
session.lastTask = input;
|
|
1496
|
+
// Reset per-turn counts so the mission report reflects this turn only.
|
|
1497
|
+
session.toolCounts = {};
|
|
1498
|
+
session.subAgentCounts = {};
|
|
1499
|
+
session.savedUsd = 0;
|
|
1500
|
+
|
|
1501
|
+
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
1502
|
+
// task / turn counters in the status bar.
|
|
1503
|
+
if (_orbit) _orbit.onUserInput(input);
|
|
1237
1504
|
|
|
1238
1505
|
// Start session tracking on first turn
|
|
1239
1506
|
if (session.turns === 1) {
|
|
@@ -1272,6 +1539,7 @@ export async function startTerminalRepl() {
|
|
|
1272
1539
|
let executionPaused = false;
|
|
1273
1540
|
let keypressCleanup = null;
|
|
1274
1541
|
let execListenerActive = false;
|
|
1542
|
+
let lastCtrlCAt = 0; // PRD-055 §8.4: first Ctrl+C cancels, second exits
|
|
1275
1543
|
|
|
1276
1544
|
if (process.stdin.isTTY) {
|
|
1277
1545
|
rl.pause();
|
|
@@ -1298,20 +1566,39 @@ export async function startTerminalRepl() {
|
|
|
1298
1566
|
executionPaused = false;
|
|
1299
1567
|
process.stderr.write(` ${c.green('▶')} ${c.dim('Resumed')}\n`);
|
|
1300
1568
|
client.resume();
|
|
1569
|
+
if (_orbit) _orbit.onResume();
|
|
1301
1570
|
} else {
|
|
1302
1571
|
executionPaused = true;
|
|
1303
1572
|
stopSpinner();
|
|
1304
1573
|
process.stderr.write(` ${c.yellow('⏸')} ${c.dim('Paused — press Space to resume, Esc to cancel')}\n`);
|
|
1305
1574
|
client.pause();
|
|
1575
|
+
if (_orbit) _orbit.onPause();
|
|
1306
1576
|
}
|
|
1307
1577
|
return;
|
|
1308
1578
|
}
|
|
1309
1579
|
|
|
1310
|
-
// Ctrl+C during execution
|
|
1580
|
+
// Ctrl+C during execution — PRD-055 §8.4 two-step semantics:
|
|
1581
|
+
// first press → cancel current backend run, stay in REPL
|
|
1582
|
+
// second press within 2s → exit the CLI
|
|
1311
1583
|
if (bytes[0] === 0x03) {
|
|
1312
1584
|
stopSpinner();
|
|
1313
|
-
|
|
1314
|
-
|
|
1585
|
+
const now = Date.now();
|
|
1586
|
+
if (lastCtrlCAt && (now - lastCtrlCAt) < 2000) {
|
|
1587
|
+
process.stderr.write(`\n ${c.dim('exiting…')}\n`);
|
|
1588
|
+
try { client.cancel(); } catch {}
|
|
1589
|
+
process.exit(0);
|
|
1590
|
+
}
|
|
1591
|
+
lastCtrlCAt = now;
|
|
1592
|
+
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('Cancelled. Press Ctrl+C again within 2s to exit.')}\n`);
|
|
1593
|
+
try { client.cancel(); } catch {}
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// `d` — expand last tool card (Mission Control §6.2)
|
|
1598
|
+
if (bytes.length === 1 && (bytes[0] === 0x64 || bytes[0] === 0x44)) {
|
|
1599
|
+
stopSpinner();
|
|
1600
|
+
expandLast();
|
|
1601
|
+
return;
|
|
1315
1602
|
}
|
|
1316
1603
|
};
|
|
1317
1604
|
|