@axplusb/kepler 0.0.1 → 1.0.1
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/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +101 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,1484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kepler REPL — Full Claude-like terminal UX.
|
|
3
|
+
*
|
|
4
|
+
* Pure ANSI. No React. No Ink. No flickering.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Persistent status bar (model, cost, context, elapsed)
|
|
8
|
+
* - Streaming content with live partial updates
|
|
9
|
+
* - Tool execution display (transparent, collapsible)
|
|
10
|
+
* - File diff display with +/- highlighting
|
|
11
|
+
* - Phase/worker progress indicators
|
|
12
|
+
* - Built-in agents (explore, review, architect)
|
|
13
|
+
* - Permission prompts (Y/n/a/t)
|
|
14
|
+
* - Input history & Tab autocomplete
|
|
15
|
+
* - Safety guardrails on all tool execution
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as readline from 'node:readline';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
21
|
+
import { calculateCost, formatCostValue, formatTokens } from '../core/pricing.mjs';
|
|
22
|
+
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
23
|
+
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
24
|
+
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
25
|
+
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
26
|
+
import { ApprovalManager } from '../core/approval.mjs';
|
|
27
|
+
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
28
|
+
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
29
|
+
import { ContextRetriever } from '../context/retriever.mjs';
|
|
30
|
+
import { buildProjectSkeleton } from '../context/skeleton.mjs';
|
|
31
|
+
import { SessionManager } from '../core/session-manager.mjs';
|
|
32
|
+
import { parseArgs } from '../config/cli-args.mjs';
|
|
33
|
+
import { formatShellCommand, toolDisplayLabel } from './tool-display.mjs';
|
|
34
|
+
|
|
35
|
+
import { createRequire } from 'node:module';
|
|
36
|
+
const __require = createRequire(import.meta.url);
|
|
37
|
+
const VERSION = __require('../../package.json').version;
|
|
38
|
+
|
|
39
|
+
// ── Safe CWD ──
|
|
40
|
+
// If the working directory gets deleted (by a rogue tool call),
|
|
41
|
+
// process.cwd() throws ENOENT. Detect and recover.
|
|
42
|
+
|
|
43
|
+
let _cachedCwd = null;
|
|
44
|
+
|
|
45
|
+
function safeCwd() {
|
|
46
|
+
try {
|
|
47
|
+
_cachedCwd = process.cwd();
|
|
48
|
+
return _cachedCwd;
|
|
49
|
+
} catch {
|
|
50
|
+
// CWD deleted — try to recover
|
|
51
|
+
const fallback = _cachedCwd || process.env.HOME || '/tmp';
|
|
52
|
+
try {
|
|
53
|
+
process.chdir(fallback);
|
|
54
|
+
process.stderr.write(` ${c.yellow('Working directory was deleted. Recovered to: ' + fallback)}\n`);
|
|
55
|
+
_cachedCwd = fallback;
|
|
56
|
+
return fallback;
|
|
57
|
+
} catch {
|
|
58
|
+
return process.env.HOME || '/tmp';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Session State ──
|
|
64
|
+
|
|
65
|
+
let _sessionMgr = null; // Set in startTerminalRepl, used by renderEvent
|
|
66
|
+
|
|
67
|
+
const session = {
|
|
68
|
+
id: null, // set by backend on first turn via session_info event
|
|
69
|
+
startTime: Date.now(),
|
|
70
|
+
inputTokens: 0,
|
|
71
|
+
outputTokens: 0,
|
|
72
|
+
toolCalls: 0,
|
|
73
|
+
totalToolCalls: 0, // across all turns
|
|
74
|
+
turns: 0,
|
|
75
|
+
history: [], // conversation messages
|
|
76
|
+
inputHistory: [], // previous prompts (for Up/Down)
|
|
77
|
+
user: null, // { github_username, email, role }
|
|
78
|
+
model: null, // from backend user profile
|
|
79
|
+
blockedOps: 0, // safety guardrail blocks
|
|
80
|
+
delegations: [], // agent delegation events: { from, to, time }
|
|
81
|
+
phases: [], // phase history: { name, time }
|
|
82
|
+
inSubAgent: false, // true while a sub-agent is running (for indented tool display)
|
|
83
|
+
filesChanged: [], // files modified this session
|
|
84
|
+
lastTurnDuration: 0,
|
|
85
|
+
costBreakdown: [], // per-model usage: [{ model, role, input_tokens, output_tokens, cost }]
|
|
86
|
+
totalCost: 0, // accumulated session cost (USD)
|
|
87
|
+
costAccurate: false, // true if backend provides per-model breakdown
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ── Commands ──
|
|
91
|
+
|
|
92
|
+
const COMMANDS = {
|
|
93
|
+
'/help': 'Show commands',
|
|
94
|
+
'/login': 'Sign in via browser',
|
|
95
|
+
'/whoami': 'Show logged-in user',
|
|
96
|
+
'/status': 'Session status & system info',
|
|
97
|
+
'/stats': 'Progress bars & metrics',
|
|
98
|
+
'/clear': 'Clear conversation',
|
|
99
|
+
'/git': 'Git status',
|
|
100
|
+
'/diff': 'Git diff',
|
|
101
|
+
'/cost': 'Show session cost',
|
|
102
|
+
'/history': 'Show conversation',
|
|
103
|
+
'/compact': 'Compact conversation context',
|
|
104
|
+
'/agents': 'List available agents',
|
|
105
|
+
'/explore': 'Code explorer agent',
|
|
106
|
+
'/review': 'Code review agent',
|
|
107
|
+
'/architect':'Feature architect agent',
|
|
108
|
+
'/safety': 'Show safety guardrail status',
|
|
109
|
+
'/revoke': 'Revoke auto-approvals',
|
|
110
|
+
'/resume': 'Resume a previous session',
|
|
111
|
+
'/sessions': 'List resumable sessions',
|
|
112
|
+
'/logout': 'Sign out and clear credentials',
|
|
113
|
+
'/exit': 'Exit CLI',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ── Banner ──
|
|
117
|
+
|
|
118
|
+
function printBanner(auth) {
|
|
119
|
+
const creds = auth.loadCredentials();
|
|
120
|
+
const env = process.env.TARANG_ENV || 'production';
|
|
121
|
+
const authStatus = creds.token ? c.green('authenticated') : c.red('/login to start');
|
|
122
|
+
|
|
123
|
+
const art = [
|
|
124
|
+
' ██████╗ ██████╗ ██████╗ █████╗',
|
|
125
|
+
' ██╔═══██╗██╔══██╗██╔════╝██╔══██╗',
|
|
126
|
+
' ██║ ██║██████╔╝██║ ███████║',
|
|
127
|
+
' ██║ ██║██╔══██╗██║ ██╔══██║',
|
|
128
|
+
' ╚██████╔╝██║ ██║╚██████╗██║ ██║',
|
|
129
|
+
' ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝',
|
|
130
|
+
];
|
|
131
|
+
process.stderr.write('\n');
|
|
132
|
+
for (const line of art) {
|
|
133
|
+
process.stderr.write(c.brand(line) + '\n');
|
|
134
|
+
}
|
|
135
|
+
process.stderr.write('\n');
|
|
136
|
+
process.stderr.write(` ${c.gray('v' + VERSION)} ${c.dim(env)} ${authStatus}\n`);
|
|
137
|
+
process.stderr.write('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Prompt Chrome ──
|
|
141
|
+
//
|
|
142
|
+
// Design: let the content breathe. The prompt area is a thin contextual
|
|
143
|
+
// strip — only shows what changed since last turn. No heavy borders.
|
|
144
|
+
//
|
|
145
|
+
// Layout after a response:
|
|
146
|
+
//
|
|
147
|
+
// <assistant content>
|
|
148
|
+
//
|
|
149
|
+
// ✓ 3 tools · 1.2s · $0.02 ctx 21% · 42k tok
|
|
150
|
+
// ╶─────────────────────────────────────────────────────────────────╴
|
|
151
|
+
// kepler ›
|
|
152
|
+
//
|
|
153
|
+
// Layout on first prompt (no stats yet):
|
|
154
|
+
//
|
|
155
|
+
// ╶─────────────────────────────────────────────────────────────────╴
|
|
156
|
+
// kepler ›
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build the contextual status strip — compact, one line.
|
|
160
|
+
* Left side: last-turn summary (tools, time, cost)
|
|
161
|
+
* Right side: session totals (ctx%, tokens)
|
|
162
|
+
*/
|
|
163
|
+
function buildContextStrip() {
|
|
164
|
+
const totalTokens = session.inputTokens + session.outputTokens;
|
|
165
|
+
const cost = formatCostValue(session.totalCost);
|
|
166
|
+
const elapsed = formatElapsed(session.startTime);
|
|
167
|
+
|
|
168
|
+
// Right side — always shown
|
|
169
|
+
const right = [
|
|
170
|
+
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
171
|
+
c.dim(cost),
|
|
172
|
+
c.dim(elapsed),
|
|
173
|
+
].join(c.dim(' · '));
|
|
174
|
+
|
|
175
|
+
return right;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Print the prompt separator + prompt label.
|
|
180
|
+
* Minimal horizontal rule with contextual info.
|
|
181
|
+
*/
|
|
182
|
+
function printPromptBlock() {
|
|
183
|
+
const w = process.stdout.columns || 80;
|
|
184
|
+
const strip = buildContextStrip();
|
|
185
|
+
const stripPlain = stripAnsi(strip);
|
|
186
|
+
|
|
187
|
+
// Rule with context strip right-aligned
|
|
188
|
+
const ruleLen = Math.max(0, w - stripPlain.length - 4);
|
|
189
|
+
process.stderr.write(
|
|
190
|
+
c.dim('╶') + c.dim('─'.repeat(ruleLen)) + ' ' + strip + ' ' + c.dim('╴') + '\n'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Print a turn summary after a response completes.
|
|
196
|
+
* Shows only when there's something meaningful to report.
|
|
197
|
+
*/
|
|
198
|
+
function printTurnSummary(toolCount, durationS, turnCost) {
|
|
199
|
+
const parts = [];
|
|
200
|
+
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
201
|
+
if (durationS) parts.push(`${Number(durationS).toFixed(1)}s`);
|
|
202
|
+
if (turnCost > 0) parts.push(formatCostValue(turnCost));
|
|
203
|
+
if (parts.length > 0) {
|
|
204
|
+
process.stderr.write(`\n ${c.green('✓')} ${c.dim(parts.join(' · '))}\n`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function updateStatusBar() {
|
|
209
|
+
// No-op: status is printed inline via printPromptBlock before each prompt
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Tool Display Renderer ──
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Render a tool call in a transparent, informational way.
|
|
216
|
+
* Shows tool name + key args on one line, no box borders for reads.
|
|
217
|
+
*/
|
|
218
|
+
function renderToolCall(data) {
|
|
219
|
+
const tool = data?.tool || 'unknown';
|
|
220
|
+
const label = toolDisplayLabel(tool);
|
|
221
|
+
const args = data?.args || {};
|
|
222
|
+
const indent = session.inSubAgent ? ' ' : ' ';
|
|
223
|
+
|
|
224
|
+
// Build summary string (what the tool will do)
|
|
225
|
+
let summary;
|
|
226
|
+
switch (tool) {
|
|
227
|
+
case 'read_file': {
|
|
228
|
+
const fp = shortPath(args.file_path || args.path || '');
|
|
229
|
+
const range = args.start_line && args.end_line
|
|
230
|
+
? ` lines ${args.start_line}-${args.end_line}`
|
|
231
|
+
: args.start_line ? ` from line ${args.start_line}` : '';
|
|
232
|
+
summary = `${fp}${range}`;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'write_file': {
|
|
236
|
+
const fp = shortPath(args.file_path || args.path || '');
|
|
237
|
+
const lines = args.content ? `, ${args.content.split('\n').length} lines` : '';
|
|
238
|
+
summary = `${fp}${lines}`;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'edit_file': {
|
|
242
|
+
const fp = shortPath(args.file_path || args.path || '');
|
|
243
|
+
const search = args.search ? `, "${(args.search || '').slice(0, 30)}..."` : '';
|
|
244
|
+
summary = `${fp}${search}`;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case 'shell':
|
|
248
|
+
summary = args.command || '';
|
|
249
|
+
break;
|
|
250
|
+
case 'search_code':
|
|
251
|
+
summary = `"${args.query || args.pattern || ''}"`;
|
|
252
|
+
break;
|
|
253
|
+
case 'list_files':
|
|
254
|
+
summary = `${args.pattern || '*'}${args.path ? ` in ${shortPath(args.path)}` : ''}`;
|
|
255
|
+
break;
|
|
256
|
+
case 'delete_file':
|
|
257
|
+
summary = shortPath(args.file_path || args.path || '');
|
|
258
|
+
break;
|
|
259
|
+
case 'read_files':
|
|
260
|
+
summary = (args.file_paths || args.paths || []).map(shortPath).join(', ');
|
|
261
|
+
break;
|
|
262
|
+
case 'write_project': {
|
|
263
|
+
const files = (args.files || []).map(f => shortPath(f.path || f.file_path || ''));
|
|
264
|
+
summary = files.length > 0 ? files.join(', ') : '';
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
default:
|
|
268
|
+
summary = Object.values(args || {}).filter(v => typeof v === 'string').join(', ').slice(0, 60);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Render: ⏺ Human-readable action(summary)
|
|
272
|
+
// Use terminal width minus label and padding, minimum 60
|
|
273
|
+
const cols = process.stderr.columns || 120;
|
|
274
|
+
const maxSummary = Math.max(60, cols - label.length - 10);
|
|
275
|
+
let displaySummary = summary || '';
|
|
276
|
+
if (displaySummary.length > maxSummary) {
|
|
277
|
+
displaySummary = '...' + displaySummary.slice(-(maxSummary - 3));
|
|
278
|
+
}
|
|
279
|
+
const summaryStr = displaySummary
|
|
280
|
+
? c.gray('(') + (tool === 'shell'
|
|
281
|
+
? formatShellCommand(displaySummary, c)
|
|
282
|
+
: c.white(displaySummary)) + c.gray(')')
|
|
283
|
+
: '';
|
|
284
|
+
process.stderr.write(`\n${indent}${c.brand('⏺')} ${c.bold(label)}${summaryStr}\n`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Render a tool result (success/failure, output snippet).
|
|
289
|
+
*/
|
|
290
|
+
const _renderedToolResults = new Set();
|
|
291
|
+
|
|
292
|
+
function formatToolDuration(data) {
|
|
293
|
+
const ms = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
294
|
+
if (ms == null) return '';
|
|
295
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function firstOutputLine(data) {
|
|
299
|
+
const output = data?.output_preview || data?.output || data?.message || '';
|
|
300
|
+
return String(output).split('\n').map(line => line.trim()).find(Boolean) || '';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function fileTypeLabel(filePath) {
|
|
304
|
+
const ext = path.extname(filePath || '').toLowerCase();
|
|
305
|
+
if (ext === '.md' || ext === '.mdx') return 'Markdown';
|
|
306
|
+
if (ext === '.json' || ext === '.jsonl') return 'JSON';
|
|
307
|
+
if (ext === '.yaml' || ext === '.yml') return 'YAML';
|
|
308
|
+
if (ext === '.toml') return 'TOML';
|
|
309
|
+
if (ext === '.csv' || ext === '.tsv') return 'tabular data';
|
|
310
|
+
if (['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.swift'].includes(ext)) {
|
|
311
|
+
return 'source';
|
|
312
|
+
}
|
|
313
|
+
return ext ? `${ext.slice(1).toUpperCase()} file` : 'file';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function renderToolResult(data, eventType = 'tool_result') {
|
|
317
|
+
if (!data) return;
|
|
318
|
+
const indent = session.inSubAgent ? ' ' : ' ';
|
|
319
|
+
const gutter = `${indent}${c.dim('⎿')} `;
|
|
320
|
+
const callId = data.call_id || data._callId;
|
|
321
|
+
if (eventType === 'tool_done' && callId && _renderedToolResults.has(callId)) return;
|
|
322
|
+
if (callId) _renderedToolResults.add(callId);
|
|
323
|
+
const duration = formatToolDuration(data);
|
|
324
|
+
const suffix = duration ? c.dim(` · ${duration}`) : '';
|
|
325
|
+
|
|
326
|
+
if (data._blocked) {
|
|
327
|
+
session.blockedOps++;
|
|
328
|
+
process.stderr.write(`${gutter}${c.red(firstOutputLine(data) || 'Blocked by safety guardrails')}${suffix}\n`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (data.success === false) {
|
|
333
|
+
const msg = (data.error || firstOutputLine(data) || 'Failed').slice(0, 140);
|
|
334
|
+
process.stderr.write(`${gutter}${c.red(msg)}${suffix}\n`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const tool = data.tool || data._tool || '';
|
|
339
|
+
let summary = 'Completed';
|
|
340
|
+
if (tool === 'read_file') {
|
|
341
|
+
const lines = data._total_lines || String(data.output || '').split('\n').length;
|
|
342
|
+
const filePath = data.args?.file_path || data.args?.path || '';
|
|
343
|
+
summary = `Read ${fileTypeLabel(filePath)} · ${lines} line${lines === 1 ? '' : 's'}`;
|
|
344
|
+
} else if (tool === 'read_files') {
|
|
345
|
+
summary = 'Files read';
|
|
346
|
+
} else if (tool === 'search_code' || tool === 'list_files') {
|
|
347
|
+
const lines = String(data.output || '').split('\n').filter(line => line.trim()).length;
|
|
348
|
+
summary = lines > 0 ? `${lines} result${lines === 1 ? '' : 's'}` : 'No results';
|
|
349
|
+
} else if (tool === 'write_file' || tool === 'edit_file' || tool === 'write_project') {
|
|
350
|
+
summary = 'Updated';
|
|
351
|
+
} else if (tool === 'delete_file') {
|
|
352
|
+
summary = 'Deleted';
|
|
353
|
+
} else if (data.server_side) {
|
|
354
|
+
summary = firstOutputLine(data).slice(0, 100) || 'Completed server-side';
|
|
355
|
+
} else if (tool === 'shell') {
|
|
356
|
+
summary = firstOutputLine(data).slice(0, 100) || 'Command completed';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const renderedSummary = tool === 'shell' ? c.green(summary) : c.white(summary);
|
|
360
|
+
process.stderr.write(`${gutter}${renderedSummary}${suffix}\n`);
|
|
361
|
+
|
|
362
|
+
// For writes, show lint warnings
|
|
363
|
+
if (tool === 'write_file' || tool === 'edit_file') {
|
|
364
|
+
const lint = data.lint;
|
|
365
|
+
if (lint) {
|
|
366
|
+
process.stderr.write(`${gutter}${c.yellow('⚠ ' + lint.split('\n')[0].slice(0, 80))}\n`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Shorten a file path for display: /Users/sree/Sites/project/src/foo.mjs → src/foo.mjs
|
|
373
|
+
*/
|
|
374
|
+
function shortPath(p) {
|
|
375
|
+
if (!p) return '';
|
|
376
|
+
const cwd = safeCwd();
|
|
377
|
+
if (p.startsWith(cwd)) return p.slice(cwd.length + 1);
|
|
378
|
+
// Show last 2 segments
|
|
379
|
+
const parts = p.split('/');
|
|
380
|
+
return parts.length > 2 ? parts.slice(-2).join('/') : p;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Live Spinner ──
|
|
384
|
+
// A real animated spinner that ticks on an interval, not just per-call.
|
|
385
|
+
// Shows what's happening right now — thinking, tool executing, etc.
|
|
386
|
+
|
|
387
|
+
const SPIN_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
388
|
+
let _spinInterval = null;
|
|
389
|
+
let _spinFrame = 0;
|
|
390
|
+
let _spinText = '';
|
|
391
|
+
|
|
392
|
+
function startSpinner(text) {
|
|
393
|
+
_spinText = text;
|
|
394
|
+
_spinFrame = 0;
|
|
395
|
+
if (_spinInterval) return; // already running
|
|
396
|
+
_spinInterval = setInterval(() => {
|
|
397
|
+
if (!_spinText) return;
|
|
398
|
+
const frame = SPIN_FRAMES[_spinFrame % SPIN_FRAMES.length];
|
|
399
|
+
_spinFrame++;
|
|
400
|
+
inPlace(` ${c.brand(frame)} ${c.dim(_spinText)}`);
|
|
401
|
+
}, 80);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function updateSpinner(text) {
|
|
405
|
+
_spinText = text;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function stopSpinner() {
|
|
409
|
+
if (_spinInterval) { clearInterval(_spinInterval); _spinInterval = null; }
|
|
410
|
+
_spinText = '';
|
|
411
|
+
inPlace('');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── Content Streaming Display ──
|
|
415
|
+
|
|
416
|
+
let _streamBuffer = '';
|
|
417
|
+
let _streamedPartialText = '';
|
|
418
|
+
let _streamTimer = null;
|
|
419
|
+
let _renderedContentThisTurn = false;
|
|
420
|
+
|
|
421
|
+
function startContentStream() {
|
|
422
|
+
_streamBuffer = '';
|
|
423
|
+
_streamedPartialText = '';
|
|
424
|
+
_renderedToolResults.clear();
|
|
425
|
+
_renderedContentThisTurn = false;
|
|
426
|
+
stopSpinner();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function appendContent(text) {
|
|
430
|
+
if (!text) return;
|
|
431
|
+
_streamBuffer += text;
|
|
432
|
+
_streamedPartialText += text;
|
|
433
|
+
|
|
434
|
+
// Debounce rendering to avoid flicker on rapid partial updates
|
|
435
|
+
if (_streamTimer) clearTimeout(_streamTimer);
|
|
436
|
+
_streamTimer = setTimeout(() => flushContent(), 50);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function flushContent() {
|
|
440
|
+
if (_streamTimer) { clearTimeout(_streamTimer); _streamTimer = null; }
|
|
441
|
+
if (!_streamBuffer) return;
|
|
442
|
+
|
|
443
|
+
stopSpinner();
|
|
444
|
+
const rendered = renderMarkdown(_streamBuffer);
|
|
445
|
+
for (const line of rendered.split('\n')) {
|
|
446
|
+
process.stdout.write(` ${line}\n`);
|
|
447
|
+
}
|
|
448
|
+
_streamBuffer = '';
|
|
449
|
+
_renderedContentThisTurn = true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Event Renderer ──
|
|
453
|
+
|
|
454
|
+
function renderEvent(event) {
|
|
455
|
+
const { type, data } = event;
|
|
456
|
+
|
|
457
|
+
switch (type) {
|
|
458
|
+
case 'status': {
|
|
459
|
+
const msg = data?.message || '';
|
|
460
|
+
if (!msg || msg === 'Agent started') return;
|
|
461
|
+
startSpinner(msg);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
case 'thinking': {
|
|
466
|
+
const text = data?.message || data?.text || '';
|
|
467
|
+
if (text && !text.startsWith('Processing')) {
|
|
468
|
+
startSpinner(text.slice(0, 80));
|
|
469
|
+
}
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
case 'content': {
|
|
474
|
+
let text = data?.text || '';
|
|
475
|
+
if (text) {
|
|
476
|
+
flushContent();
|
|
477
|
+
stopSpinner();
|
|
478
|
+
if (_streamedPartialText && text.startsWith(_streamedPartialText)) {
|
|
479
|
+
text = text.slice(_streamedPartialText.length);
|
|
480
|
+
} else if (_streamedPartialText.includes(text)) {
|
|
481
|
+
text = '';
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (text) {
|
|
485
|
+
const rendered = renderMarkdown(text);
|
|
486
|
+
for (const line of rendered.split('\n')) {
|
|
487
|
+
process.stdout.write(` ${line}\n`);
|
|
488
|
+
}
|
|
489
|
+
_renderedContentThisTurn = true;
|
|
490
|
+
}
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
case 'content_partial': {
|
|
495
|
+
const text = data?.text || '';
|
|
496
|
+
if (text) {
|
|
497
|
+
stopSpinner();
|
|
498
|
+
appendContent(text);
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
case 'tool_call':
|
|
504
|
+
case 'tool_request': {
|
|
505
|
+
session.toolCalls++;
|
|
506
|
+
session.totalToolCalls++;
|
|
507
|
+
stopSpinner();
|
|
508
|
+
flushContent();
|
|
509
|
+
renderToolCall(data);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── HITL: Framework-level approval events ──
|
|
514
|
+
|
|
515
|
+
case 'approval_required': {
|
|
516
|
+
stopSpinner();
|
|
517
|
+
flushContent();
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
case 'approval_granted': {
|
|
522
|
+
// approval.mjs _prompt already rendered the result line for human approvals.
|
|
523
|
+
// Nothing extra needed here — avoid duplicate output.
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
case 'approval_denied': {
|
|
528
|
+
const reason = data?.reason || 'User denied';
|
|
529
|
+
const toolName = data?.tool || '';
|
|
530
|
+
const indent = session.inSubAgent ? ' ' : ' ';
|
|
531
|
+
process.stderr.write(`${indent}${c.red('✗')} ${c.dim(`Denied ${toolName}: ${reason}`)}\n`);
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
case 'tool_result':
|
|
536
|
+
case 'tool_done': {
|
|
537
|
+
stopSpinner();
|
|
538
|
+
renderToolResult(data, type);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
case 'plan': {
|
|
543
|
+
stopSpinner();
|
|
544
|
+
flushContent();
|
|
545
|
+
const milestones = data?.milestones || data?.steps || [];
|
|
546
|
+
const title = data?.title || 'Plan';
|
|
547
|
+
process.stderr.write(`\n ${c.brand('▸')} ${c.bold(title)}\n`);
|
|
548
|
+
for (const [index, milestone] of milestones.entries()) {
|
|
549
|
+
const label = typeof milestone === 'string'
|
|
550
|
+
? milestone
|
|
551
|
+
: milestone.name || milestone.title || milestone.description || `Step ${index + 1}`;
|
|
552
|
+
const status = typeof milestone === 'object' ? milestone.status : '';
|
|
553
|
+
const marker = status === 'complete' || status === 'completed' ? c.green('✓') : c.dim(`${index + 1}.`);
|
|
554
|
+
process.stderr.write(` ${marker} ${label}\n`);
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
case 'change': {
|
|
560
|
+
stopSpinner();
|
|
561
|
+
const changeType = data?.type || 'modify';
|
|
562
|
+
const filePath = shortPath(data?.path || '');
|
|
563
|
+
const icon = changeType === 'create' ? c.green('+') :
|
|
564
|
+
changeType === 'delete' ? c.red('-') : c.yellow('~');
|
|
565
|
+
process.stderr.write(` ${icon} ${c.dim(filePath)}\n`);
|
|
566
|
+
// Track changed files
|
|
567
|
+
if (filePath && !session.filesChanged.includes(filePath)) {
|
|
568
|
+
session.filesChanged.push(filePath);
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
case 'phase_start':
|
|
574
|
+
case 'phase_update': {
|
|
575
|
+
const phase = data?.phase || data?.stage_name || '';
|
|
576
|
+
if (phase) {
|
|
577
|
+
stopSpinner();
|
|
578
|
+
session.phases.push({ name: phase, time: Date.now() });
|
|
579
|
+
process.stderr.write(`\n ${c.brand('▸')} ${c.bold(phase)}\n`);
|
|
580
|
+
}
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
case 'phase_summary': {
|
|
585
|
+
const summary = data?.summary || '';
|
|
586
|
+
if (summary) {
|
|
587
|
+
process.stderr.write(` ${c.dim(summary.slice(0, 120))}\n`);
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
case 'worker_start':
|
|
593
|
+
case 'worker_update': {
|
|
594
|
+
const worker = data?.worker || data?.name || '';
|
|
595
|
+
const status = data?.status || data?.message || 'working';
|
|
596
|
+
if (worker) startSpinner(`${worker}: ${status}`);
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
case 'worker_done': {
|
|
601
|
+
stopSpinner();
|
|
602
|
+
const worker = data?.worker || data?.name || '';
|
|
603
|
+
if (worker) process.stderr.write(` ${c.green('✓')} ${c.dim(worker)}\n`);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
case 'delegation': {
|
|
608
|
+
stopSpinner();
|
|
609
|
+
const from = data?.from || '';
|
|
610
|
+
const to = data?.to || '';
|
|
611
|
+
session.delegations.push({ from, to, time: Date.now() });
|
|
612
|
+
process.stderr.write(`\n ${c.brand('↳')} ${c.dim(from)} ${c.brand('→')} ${c.bold(to)}`);
|
|
613
|
+
if (data?.instruction) {
|
|
614
|
+
process.stderr.write(` ${c.dim(data.instruction.slice(0, 50))}`);
|
|
615
|
+
}
|
|
616
|
+
process.stderr.write('\n');
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ── Sub-Agent Activity ──
|
|
621
|
+
|
|
622
|
+
case 'sub_agent_start': {
|
|
623
|
+
stopSpinner();
|
|
624
|
+
session.inSubAgent = true;
|
|
625
|
+
const agentType = data?.type || 'sub-agent';
|
|
626
|
+
const model = data?.model || '';
|
|
627
|
+
const query = data?.query || '';
|
|
628
|
+
const icon = agentType === 'explore' ? '🔭' : agentType === 'plan' ? '📐' : '🤖';
|
|
629
|
+
process.stderr.write(`\n ${icon} ${c.bold(c.brand(`${agentType} agent`))} ${c.dim('started')}\n`);
|
|
630
|
+
if (model) process.stderr.write(` ${c.gray('model:')} ${c.dim(model)}\n`);
|
|
631
|
+
if (query) process.stderr.write(` ${c.gray('query:')} ${c.dim(query)}\n`);
|
|
632
|
+
startSpinner(`${agentType}: working...`);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
case 'sub_agent_tool': {
|
|
637
|
+
// No separate display — the regular tool_call event shows full detail
|
|
638
|
+
// indented under the sub-agent block. Just update the spinner text.
|
|
639
|
+
const agentType = data?.type || 'sub-agent';
|
|
640
|
+
const tool = data?.tool || '';
|
|
641
|
+
if (tool) updateSpinner(`${agentType} → ${tool}`);
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
case 'sub_agent_complete': {
|
|
646
|
+
stopSpinner();
|
|
647
|
+
session.inSubAgent = false;
|
|
648
|
+
const agentType = data?.type || 'sub-agent';
|
|
649
|
+
const model = data?.model || '';
|
|
650
|
+
const resultLen = data?.result_length || 0;
|
|
651
|
+
const usage = data?.usage || {};
|
|
652
|
+
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
653
|
+
const parts = [];
|
|
654
|
+
if (data?.tool_calls > 0) parts.push(`${data.tool_calls} tools`);
|
|
655
|
+
if (data?.iterations > 0) parts.push(`${data.iterations} iterations`);
|
|
656
|
+
if (resultLen > 0) parts.push(`${resultLen} chars`);
|
|
657
|
+
if (tokens > 0) parts.push(`${formatTokens(tokens)} tok`);
|
|
658
|
+
if (data?.duration_s != null) parts.push(`${Number(data.duration_s).toFixed(1)}s`);
|
|
659
|
+
const icon = agentType === 'explore' ? '🔭' : agentType === 'plan' ? '📐' : '🤖';
|
|
660
|
+
const marker = data?.success === false ? c.red('✗') : c.green('✓');
|
|
661
|
+
const label = data?.success === false ? `${agentType} agent failed` : `${agentType} agent complete`;
|
|
662
|
+
process.stderr.write(` ${icon} ${marker} ${c.dim(label)}${parts.length ? ' ' + c.dim(parts.join(' · ')) : ''}\n`);
|
|
663
|
+
if (data?.error) process.stderr.write(` ${c.red(String(data.error).slice(0, 140))}\n`);
|
|
664
|
+
process.stderr.write('\n');
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
case 'session_info': {
|
|
669
|
+
if (data?.session_id) {
|
|
670
|
+
session.id = data.session_id;
|
|
671
|
+
// Track in session manager so conversations save to the right file
|
|
672
|
+
if (_sessionMgr) _sessionMgr.setSessionInfo({ session_id: data.session_id });
|
|
673
|
+
}
|
|
674
|
+
if (data?.model) session.model = data.model;
|
|
675
|
+
if (data?.user) session.user = { ...session.user, ...data.user };
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
case 'error':
|
|
680
|
+
stopSpinner();
|
|
681
|
+
flushContent();
|
|
682
|
+
process.stderr.write(`\n ${c.red('✗')} ${data?.message || 'Unknown error'}\n`);
|
|
683
|
+
if ((data?.message || '').includes('Authentication')) {
|
|
684
|
+
process.stderr.write(` ${c.dim('Run /login to re-authenticate')}\n`);
|
|
685
|
+
}
|
|
686
|
+
break;
|
|
687
|
+
|
|
688
|
+
case 'complete': {
|
|
689
|
+
stopSpinner();
|
|
690
|
+
flushContent();
|
|
691
|
+
|
|
692
|
+
const summary = data?.summary || '';
|
|
693
|
+
if (summary && !_renderedContentThisTurn) {
|
|
694
|
+
const rendered = renderMarkdown(summary);
|
|
695
|
+
for (const line of rendered.split('\n')) {
|
|
696
|
+
process.stdout.write(` ${line}\n`);
|
|
697
|
+
}
|
|
698
|
+
_renderedContentThisTurn = true;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Update session token counts
|
|
702
|
+
const usage = data?.usage;
|
|
703
|
+
let turnCost = 0;
|
|
704
|
+
if (usage) {
|
|
705
|
+
const inp = usage.total_input_tokens || usage.input_tokens || 0;
|
|
706
|
+
const out = usage.total_output_tokens || usage.output_tokens || 0;
|
|
707
|
+
session.inputTokens += inp;
|
|
708
|
+
session.outputTokens += out;
|
|
709
|
+
|
|
710
|
+
// Model-aware cost calculation
|
|
711
|
+
const costResult = calculateCost(usage);
|
|
712
|
+
turnCost = costResult.total;
|
|
713
|
+
session.totalCost += costResult.total;
|
|
714
|
+
session.costAccurate = costResult.accurate;
|
|
715
|
+
|
|
716
|
+
// Accumulate per-model breakdown
|
|
717
|
+
for (const entry of costResult.breakdown) {
|
|
718
|
+
const existing = session.costBreakdown.find(b => b.model === entry.model);
|
|
719
|
+
if (existing) {
|
|
720
|
+
existing.input_tokens += entry.input_tokens;
|
|
721
|
+
existing.output_tokens += entry.output_tokens;
|
|
722
|
+
existing.cache_read_tokens += entry.cache_read_tokens || 0;
|
|
723
|
+
existing.cache_creation_tokens += entry.cache_creation_tokens || 0;
|
|
724
|
+
existing.cost += entry.cost;
|
|
725
|
+
} else {
|
|
726
|
+
session.costBreakdown.push({ ...entry });
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
session.lastTurnDuration = data?.duration_s || 0;
|
|
732
|
+
|
|
733
|
+
// Compact turn summary
|
|
734
|
+
const tools = data?.tool_calls || session.toolCalls || 0;
|
|
735
|
+
printTurnSummary(tools, data?.duration_s, turnCost);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
case 'cancelled':
|
|
740
|
+
stopSpinner();
|
|
741
|
+
flushContent();
|
|
742
|
+
process.stderr.write(`\n ${c.yellow('⏹')} Cancelled${data?.reason ? ': ' + c.dim(data.reason) : ''}\n`);
|
|
743
|
+
break;
|
|
744
|
+
|
|
745
|
+
case 'paused':
|
|
746
|
+
stopSpinner();
|
|
747
|
+
process.stderr.write(` ${c.yellow('⏸')} Paused${data?.reason ? ' ' + c.dim(data.reason) : ''}\n`);
|
|
748
|
+
break;
|
|
749
|
+
|
|
750
|
+
case 'resumed':
|
|
751
|
+
process.stderr.write(` ${c.green('▶')} Resumed\n`);
|
|
752
|
+
break;
|
|
753
|
+
|
|
754
|
+
default:
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ── Slash Commands ──
|
|
760
|
+
|
|
761
|
+
async function handleCommand(input, ctx) {
|
|
762
|
+
const parts = input.split(/\s+/);
|
|
763
|
+
const cmd = parts[0].toLowerCase();
|
|
764
|
+
const rest = parts.slice(1).join(' ');
|
|
765
|
+
|
|
766
|
+
switch (cmd) {
|
|
767
|
+
case '/help':
|
|
768
|
+
process.stderr.write(`\n ${c.bold('Kepler Commands')}\n`);
|
|
769
|
+
process.stderr.write(` ${c.gray('─'.repeat(44))}\n`);
|
|
770
|
+
for (const [name, desc] of Object.entries(COMMANDS)) {
|
|
771
|
+
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}\n`);
|
|
772
|
+
}
|
|
773
|
+
process.stderr.write(`\n ${c.bold('Keyboard')}\n`);
|
|
774
|
+
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n\n`);
|
|
775
|
+
return;
|
|
776
|
+
|
|
777
|
+
case '/login':
|
|
778
|
+
process.stderr.write(`${c.brand('Starting login flow...')}\n`);
|
|
779
|
+
try {
|
|
780
|
+
await ctx.auth.login();
|
|
781
|
+
process.stderr.write(`${c.green('✓ Login successful!')}\n`);
|
|
782
|
+
await fetchUser(ctx);
|
|
783
|
+
} catch (err) {
|
|
784
|
+
process.stderr.write(`${c.red('✗ Login failed: ' + err.message)}\n`);
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
|
|
788
|
+
case '/whoami': {
|
|
789
|
+
if (!session.user) await fetchUser(ctx);
|
|
790
|
+
if (session.user) {
|
|
791
|
+
process.stderr.write(`\n ${c.green('✓')} ${session.user.github_username}\n`);
|
|
792
|
+
process.stderr.write(` ${c.gray('Email:')} ${session.user.email || 'n/a'}\n`);
|
|
793
|
+
process.stderr.write(` ${c.gray('User ID:')} ${session.user.id}\n`);
|
|
794
|
+
process.stderr.write(` ${c.gray('Role:')} ${session.user.role || 'user'}\n\n`);
|
|
795
|
+
} else {
|
|
796
|
+
process.stderr.write(` ${c.red('Not logged in. Run /login.')}\n`);
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
case '/status': {
|
|
802
|
+
const creds = ctx.auth.loadCredentials();
|
|
803
|
+
const env = process.env.TARANG_ENV || 'production';
|
|
804
|
+
const os = await import('node:os');
|
|
805
|
+
const mem = process.memoryUsage();
|
|
806
|
+
const approvalSummary = ctx.approval.getSummary();
|
|
807
|
+
|
|
808
|
+
process.stderr.write(`\n ${c.bold('Session')}\n`);
|
|
809
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
810
|
+
process.stderr.write(` ${c.dim('ID')} ${session.id || c.dim('(not assigned yet)')}\n`);
|
|
811
|
+
process.stderr.write(` ${c.dim('User')} ${session.user?.github_username || '—'}\n`);
|
|
812
|
+
process.stderr.write(` ${c.dim('Model')} ${session.model || 'backend default'}\n`);
|
|
813
|
+
if (env === 'local') {
|
|
814
|
+
process.stderr.write(` ${c.dim('Backend')} ${creds.backendUrl}\n`);
|
|
815
|
+
}
|
|
816
|
+
process.stderr.write(` ${c.dim('Env')} ${env}\n`);
|
|
817
|
+
process.stderr.write(` ${c.dim('Turns')} ${session.turns}\n`);
|
|
818
|
+
process.stderr.write(` ${c.dim('Tools')} ${session.totalToolCalls} total, ${session.toolCalls} last turn\n`);
|
|
819
|
+
process.stderr.write(` ${c.dim('Duration')} ${formatElapsed(session.startTime)}\n`);
|
|
820
|
+
process.stderr.write(` ${c.dim('Cost')} ${formatCostValue(session.totalCost)}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
821
|
+
process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
|
|
822
|
+
|
|
823
|
+
// Permissions
|
|
824
|
+
process.stderr.write(`\n ${c.bold('Permissions')}\n`);
|
|
825
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
826
|
+
process.stderr.write(` ${c.dim('Approved')} ${approvalSummary.approved} ${c.dim('Denied')} ${approvalSummary.denied}\n`);
|
|
827
|
+
if (approvalSummary.autoApproveAll) {
|
|
828
|
+
process.stderr.write(` ${c.dim('Mode')} ${c.yellow('approve-all active')}\n`);
|
|
829
|
+
}
|
|
830
|
+
if (approvalSummary.autoApprovedTypes.length > 0) {
|
|
831
|
+
process.stderr.write(` ${c.dim('Auto-types')} ${approvalSummary.autoApprovedTypes.join(', ')}\n`);
|
|
832
|
+
}
|
|
833
|
+
process.stderr.write(` ${c.dim('Blocked')} ${session.blockedOps} by safety guardrails\n`);
|
|
834
|
+
|
|
835
|
+
// Orchestration
|
|
836
|
+
if (session.delegations.length > 0 || session.phases.length > 0) {
|
|
837
|
+
process.stderr.write(`\n ${c.bold('Orchestration')}\n`);
|
|
838
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
839
|
+
if (session.delegations.length > 0) {
|
|
840
|
+
process.stderr.write(` ${c.dim('Delegations')} ${session.delegations.length}\n`);
|
|
841
|
+
for (const d of session.delegations.slice(-5)) {
|
|
842
|
+
process.stderr.write(` ${c.dim(d.from)} ${c.brand('→')} ${d.to}\n`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (session.phases.length > 0) {
|
|
846
|
+
process.stderr.write(` ${c.dim('Phases')} ${session.phases.map(p => p.name).join(' → ')}\n`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Files changed
|
|
851
|
+
if (session.filesChanged.length > 0) {
|
|
852
|
+
process.stderr.write(`\n ${c.bold('Files Changed')} ${c.dim(`(${session.filesChanged.length})`)}\n`);
|
|
853
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
854
|
+
for (const f of session.filesChanged.slice(-10)) {
|
|
855
|
+
process.stderr.write(` ${c.dim('~')} ${f}\n`);
|
|
856
|
+
}
|
|
857
|
+
if (session.filesChanged.length > 10) {
|
|
858
|
+
process.stderr.write(` ${c.dim(` ...and ${session.filesChanged.length - 10} more`)}\n`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// System
|
|
863
|
+
process.stderr.write(`\n ${c.bold('System')}\n`);
|
|
864
|
+
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
865
|
+
process.stderr.write(` ${c.dim('Node')} ${process.version}\n`);
|
|
866
|
+
process.stderr.write(` ${c.dim('Platform')} ${process.platform} ${os.arch()}\n`);
|
|
867
|
+
process.stderr.write(` ${c.dim('Heap')} ${(mem.heapUsed / 1024 / 1024).toFixed(0)} MB\n`);
|
|
868
|
+
process.stderr.write(` ${c.dim('Memory')} ${((os.totalmem() - os.freemem()) / 1024 / 1024 / 1024).toFixed(1)}G / ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(1)}G\n\n`);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
case '/stats': {
|
|
873
|
+
const os = await import('node:os');
|
|
874
|
+
const mem = process.memoryUsage();
|
|
875
|
+
const totalMem = os.totalmem();
|
|
876
|
+
const usedMem = totalMem - os.freemem();
|
|
877
|
+
const totalTokens = session.inputTokens + session.outputTokens;
|
|
878
|
+
const ctxPct = Math.min(100, (totalTokens / 200000) * 100);
|
|
879
|
+
|
|
880
|
+
process.stderr.write(`\n ${c.bold('Metrics')}\n`);
|
|
881
|
+
process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
|
|
882
|
+
process.stderr.write(` ${progressBar(ctxPct, 15, 'Context')} ${(totalTokens / 1000).toFixed(1)}k tok\n`);
|
|
883
|
+
process.stderr.write(` ${progressBar(Math.round((usedMem / totalMem) * 100), 15, 'Memory')} ${(usedMem / 1024 / 1024 / 1024).toFixed(1)}G\n`);
|
|
884
|
+
process.stderr.write(` ${progressBar(Math.round((mem.heapUsed / mem.heapTotal) * 100), 15, 'Heap')} ${(mem.heapUsed / 1024 / 1024).toFixed(0)}M\n`);
|
|
885
|
+
process.stderr.write(` ${c.gray('Turns:')} ${session.turns}\n`);
|
|
886
|
+
process.stderr.write(` ${c.gray('Tools:')} ${session.toolCalls}\n`);
|
|
887
|
+
process.stderr.write(` ${c.gray('Blocked:')} ${session.blockedOps}\n`);
|
|
888
|
+
process.stderr.write(` ${c.gray('Cost:')} ${formatCostValue(session.totalCost)}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
889
|
+
process.stderr.write(` ${c.gray('Elapsed:')} ${formatElapsed(session.startTime)}\n\n`);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
case '/cost': {
|
|
894
|
+
process.stderr.write(`\n ${c.bold('Session Cost')}`);
|
|
895
|
+
if (!session.costAccurate) {
|
|
896
|
+
process.stderr.write(` ${c.yellow('(estimated — backend not sending model breakdown)')}`);
|
|
897
|
+
}
|
|
898
|
+
process.stderr.write('\n');
|
|
899
|
+
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
900
|
+
|
|
901
|
+
if (session.costBreakdown.length > 0) {
|
|
902
|
+
// Header
|
|
903
|
+
process.stderr.write(` ${c.dim('Model'.padEnd(36))}${c.dim('Input'.padStart(10))}${c.dim('Output'.padStart(10))}${c.dim('Cache'.padStart(10))}${c.dim('Cost'.padStart(10))}\n`);
|
|
904
|
+
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
905
|
+
|
|
906
|
+
for (const b of session.costBreakdown) {
|
|
907
|
+
const modelLabel = b.model === 'unknown' ? c.yellow('unknown model') : b.model;
|
|
908
|
+
const roleTag = b.role && b.role !== 'unknown' ? ` ${c.dim(`(${b.role})`)}` : '';
|
|
909
|
+
const cacheTokens = (b.cache_read_tokens || 0) + (b.cache_creation_tokens || 0);
|
|
910
|
+
const costStr = b.free ? c.green('free') : formatCostValue(b.cost);
|
|
911
|
+
|
|
912
|
+
process.stderr.write(
|
|
913
|
+
` ${(modelLabel + roleTag).padEnd(36)}` +
|
|
914
|
+
`${formatTokens(b.input_tokens).padStart(10)}` +
|
|
915
|
+
`${formatTokens(b.output_tokens).padStart(10)}` +
|
|
916
|
+
`${(cacheTokens > 0 ? formatTokens(cacheTokens) : '—').padStart(10)}` +
|
|
917
|
+
`${costStr.padStart(10)}\n`
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
process.stderr.write(
|
|
925
|
+
` ${c.bold('Total'.padEnd(36))}` +
|
|
926
|
+
`${formatTokens(session.inputTokens).padStart(10)}` +
|
|
927
|
+
`${formatTokens(session.outputTokens).padStart(10)}` +
|
|
928
|
+
`${''.padStart(10)}` +
|
|
929
|
+
`${formatCostValue(session.totalCost).padStart(10)}\n`
|
|
930
|
+
);
|
|
931
|
+
process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)}`)}\n\n`);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
case '/history':
|
|
936
|
+
if (session.history.length === 0) { process.stderr.write(` ${c.gray('No conversation yet.')}\n`); return; }
|
|
937
|
+
process.stderr.write(`\n ${c.bold('Conversation')} (${session.history.length} messages)\n`);
|
|
938
|
+
process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
|
|
939
|
+
for (const msg of session.history.slice(-20)) {
|
|
940
|
+
const role = msg.role === 'user' ? c.white('You') : c.brand('Kepler');
|
|
941
|
+
process.stderr.write(` ${role}: ${msg.content.slice(0, 80)}${msg.content.length > 80 ? '...' : ''}\n`);
|
|
942
|
+
}
|
|
943
|
+
process.stderr.write('\n');
|
|
944
|
+
return;
|
|
945
|
+
|
|
946
|
+
case '/compact': {
|
|
947
|
+
const before = session.history.length;
|
|
948
|
+
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
949
|
+
session.history.splice(2, session.history.length - 6);
|
|
950
|
+
process.stderr.write(` ${c.gray(`Compacted: ${before} → ${session.history.length} messages`)}\n`);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
case '/clear':
|
|
955
|
+
session.history.length = 0;
|
|
956
|
+
session.toolCalls = 0;
|
|
957
|
+
process.stderr.write(` ${c.gray('Conversation cleared.')}\n`);
|
|
958
|
+
return;
|
|
959
|
+
|
|
960
|
+
case '/git': {
|
|
961
|
+
const { execSync } = await import('node:child_process');
|
|
962
|
+
try { process.stdout.write(execSync('git status --short --branch', { encoding: 'utf-8' }) + '\n'); }
|
|
963
|
+
catch (e) { process.stderr.write(` ${c.red(e.message)}\n`); }
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
case '/diff': {
|
|
968
|
+
const { execSync } = await import('node:child_process');
|
|
969
|
+
try {
|
|
970
|
+
const diff = execSync('git diff --no-ext-diff --unified=3', {
|
|
971
|
+
encoding: 'utf-8',
|
|
972
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
973
|
+
});
|
|
974
|
+
process.stdout.write(diff ? renderDiff(diff) + '\n' : c.dim('(no changes)') + '\n');
|
|
975
|
+
}
|
|
976
|
+
catch (e) { process.stderr.write(` ${c.red(e.message)}\n`); }
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
case '/safety': {
|
|
981
|
+
const { getSafetyRules } = await import('../core/safety.mjs');
|
|
982
|
+
const rules = getSafetyRules();
|
|
983
|
+
const summary = ctx.approval.getSummary();
|
|
984
|
+
process.stderr.write(`\n ${c.bold('Safety Guardrails')} ${c.green('ACTIVE')}\n`);
|
|
985
|
+
process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
|
|
986
|
+
process.stderr.write(` ${c.gray('Approval mode:')} ${ctx.approval.getModeLabel()}\n`);
|
|
987
|
+
process.stderr.write(` ${c.gray('Approved:')} ${summary.approved} ${c.gray('Denied:')} ${summary.denied}\n`);
|
|
988
|
+
process.stderr.write(` ${c.gray('Protected files:')} ${rules.protectedNames.join(', ')}\n`);
|
|
989
|
+
process.stderr.write(` ${c.gray('Source dirs:')} ${rules.sourceDirs.join(', ')}\n`);
|
|
990
|
+
process.stderr.write(` ${c.gray('Blocked patterns:')} ${rules.blockedPatterns}\n`);
|
|
991
|
+
process.stderr.write(` ${c.gray('High-risk patterns:')} ${rules.highRiskPatterns}\n`);
|
|
992
|
+
process.stderr.write(` ${c.gray('Ops blocked:')} ${session.blockedOps}\n\n`);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
case '/revoke': {
|
|
997
|
+
const wasActive = ctx.approval.revoke();
|
|
998
|
+
if (wasActive) {
|
|
999
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Auto-approvals revoked. All tool calls will prompt again.')}\n`);
|
|
1000
|
+
} else {
|
|
1001
|
+
process.stderr.write(` ${c.gray('No auto-approvals were active.')}\n`);
|
|
1002
|
+
}
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
case '/sessions': {
|
|
1007
|
+
const resumable = ctx.sessionMgr.listResumable(10);
|
|
1008
|
+
if (resumable.length === 0) {
|
|
1009
|
+
process.stderr.write(` ${c.gray('No resumable sessions found.')}\n`);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
process.stderr.write(`\n ${c.bold('Resumable Sessions')}\n`);
|
|
1013
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1014
|
+
for (const s of resumable) {
|
|
1015
|
+
const date = s.startedAt ? new Date(s.startedAt).toLocaleDateString() : '?';
|
|
1016
|
+
const instr = s.instruction ? s.instruction.slice(0, 40) : '(no instruction)';
|
|
1017
|
+
process.stderr.write(` ${c.brand(s.sessionId)} ${c.dim(date)} ${s.messageCount} msgs ${c.dim(instr)}\n`);
|
|
1018
|
+
}
|
|
1019
|
+
process.stderr.write(`\n ${c.dim('Resume with:')} kepler --resume <sessionId>\n`);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
case '/resume': {
|
|
1024
|
+
const parts = input.split(/\s+/);
|
|
1025
|
+
const targetId = parts[1]; // /resume <sessionId>
|
|
1026
|
+
|
|
1027
|
+
if (targetId) {
|
|
1028
|
+
// Direct resume by ID
|
|
1029
|
+
const messages = ctx.sessionMgr.loadMessages(targetId);
|
|
1030
|
+
if (messages.length === 0) {
|
|
1031
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('No conversation found for session ' + targetId)}\n`);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
session.history = messages;
|
|
1035
|
+
session.id = targetId;
|
|
1036
|
+
session.turns = Math.floor(messages.length / 2);
|
|
1037
|
+
process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed: ${messages.length} messages`)}\n`);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// No ID given — list sessions and let user pick by number
|
|
1042
|
+
const resumable = ctx.sessionMgr.listResumable(10);
|
|
1043
|
+
if (resumable.length === 0) {
|
|
1044
|
+
process.stderr.write(` ${c.gray('No resumable sessions found.')}\n`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
process.stderr.write(`\n ${c.bold('Pick a session to resume:')}\n\n`);
|
|
1049
|
+
for (let i = 0; i < resumable.length; i++) {
|
|
1050
|
+
const s = resumable[i];
|
|
1051
|
+
const date = s.startedAt ? new Date(s.startedAt).toLocaleString() : '?';
|
|
1052
|
+
const instr = (s.instruction || '(no instruction)').slice(0, 45);
|
|
1053
|
+
const proj = s.project ? c.brand(s.project) + ' ' : '';
|
|
1054
|
+
const num = `[${i + 1}]`;
|
|
1055
|
+
process.stderr.write(` ${c.brand(num)} ${proj}${c.dim(date)} ${s.messageCount} msgs\n`);
|
|
1056
|
+
process.stderr.write(` ${c.dim(instr)}\n`);
|
|
1057
|
+
}
|
|
1058
|
+
process.stderr.write(`\n ${c.dim('Enter number (or Esc to cancel):')} `);
|
|
1059
|
+
|
|
1060
|
+
// Read single key for selection
|
|
1061
|
+
const rl = ctx._rl || null;
|
|
1062
|
+
if (rl) rl.pause();
|
|
1063
|
+
const choice = await new Promise((resolve) => {
|
|
1064
|
+
if (!process.stdin.isTTY) { resolve(null); return; }
|
|
1065
|
+
const wasRaw = process.stdin.isRaw;
|
|
1066
|
+
process.stdin.setRawMode(true);
|
|
1067
|
+
process.stdin.resume();
|
|
1068
|
+
process.stdin.once('data', (data) => {
|
|
1069
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
1070
|
+
if (rl) rl.resume();
|
|
1071
|
+
const bytes = [...data];
|
|
1072
|
+
if (bytes[0] === 0x1b || bytes[0] === 0x03) { resolve(null); return; }
|
|
1073
|
+
const num = parseInt(data.toString(), 10);
|
|
1074
|
+
resolve(isNaN(num) ? null : num);
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
if (!choice || choice < 1 || choice > resumable.length) {
|
|
1079
|
+
process.stderr.write(`\n ${c.dim('Cancelled.')}\n`);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const picked = resumable[choice - 1];
|
|
1084
|
+
const messages = ctx.sessionMgr.loadMessages(picked.sessionId);
|
|
1085
|
+
if (messages.length === 0) {
|
|
1086
|
+
process.stderr.write(`\n ${c.yellow('!')} ${c.dim('No messages in that session.')}\n`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
session.history = messages;
|
|
1091
|
+
session.id = picked.sessionId;
|
|
1092
|
+
session.turns = Math.floor(messages.length / 2);
|
|
1093
|
+
process.stderr.write(`\n ${c.green('↺')} ${c.dim(`Resumed: ${messages.length} messages`)}`);
|
|
1094
|
+
if (picked.instruction) {
|
|
1095
|
+
process.stderr.write(` ${c.dim('—')} ${c.dim(picked.instruction.slice(0, 50))}`);
|
|
1096
|
+
}
|
|
1097
|
+
process.stderr.write('\n');
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
case '/agents':
|
|
1102
|
+
process.stderr.write(`\n ${c.bold('Built-in Agents')}\n`);
|
|
1103
|
+
process.stderr.write(` ${c.gray('─'.repeat(44))}\n`);
|
|
1104
|
+
for (const agent of BUILTIN_AGENTS) {
|
|
1105
|
+
process.stderr.write(` ${c.brand(('/' + agent.command).padEnd(14))} ${agent.description}\n`);
|
|
1106
|
+
process.stderr.write(` ${' '.repeat(14)} ${c.gray(agent.detail)}\n`);
|
|
1107
|
+
}
|
|
1108
|
+
process.stderr.write(`\n ${c.gray('Usage: /<agent> <instruction>')}\n`);
|
|
1109
|
+
process.stderr.write(` ${c.gray('Example: /explore how does the auth flow work?')}\n\n`);
|
|
1110
|
+
return;
|
|
1111
|
+
|
|
1112
|
+
case '/explore':
|
|
1113
|
+
case '/review':
|
|
1114
|
+
case '/architect': {
|
|
1115
|
+
if (!rest) {
|
|
1116
|
+
process.stderr.write(` ${c.yellow('Usage:')} ${cmd} <instruction>\n`);
|
|
1117
|
+
process.stderr.write(` ${c.gray(`Example: ${cmd} ${cmd === '/explore' ? 'how does authentication work?' : cmd === '/review' ? 'check src/core/ for bugs' : 'design a caching layer'}`)}\n`);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
return await runAgent(cmd.slice(1), rest, ctx, session, renderEvent);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
case '/logout': {
|
|
1124
|
+
const success = ctx.auth.logout();
|
|
1125
|
+
if (success) {
|
|
1126
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Signed out. Credentials cleared from ~/.kepler/config.json')}\n`);
|
|
1127
|
+
process.stderr.write(` ${c.dim('Run /login to sign in again.')}\n`);
|
|
1128
|
+
} else {
|
|
1129
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('No credentials to clear.')}\n`);
|
|
1130
|
+
}
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
case '/exit':
|
|
1135
|
+
case '/quit':
|
|
1136
|
+
process.stderr.write(`\n ${c.brand('Goodbye!')}\n\n`);
|
|
1137
|
+
process.exit(0);
|
|
1138
|
+
|
|
1139
|
+
default:
|
|
1140
|
+
process.stderr.write(` ${c.gray(`Unknown: ${cmd}. Type /help.`)}\n`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ── Fetch User Profile ──
|
|
1145
|
+
|
|
1146
|
+
async function fetchUser(ctx) {
|
|
1147
|
+
const creds = ctx.auth.loadCredentials();
|
|
1148
|
+
if (!creds.token) return;
|
|
1149
|
+
try {
|
|
1150
|
+
const resp = await fetch(`${creds.backendUrl}/api/user/me`, {
|
|
1151
|
+
headers: { 'Authorization': `Bearer ${creds.token}` },
|
|
1152
|
+
});
|
|
1153
|
+
if (resp.ok) {
|
|
1154
|
+
session.user = await resp.json();
|
|
1155
|
+
session.model = session.user.default_reasoning_model || session.user.default_orchestrator_model || null;
|
|
1156
|
+
}
|
|
1157
|
+
} catch {}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// ── Main REPL ──
|
|
1161
|
+
// Cache CWD at startup so safeCwd() has a fallback if the dir gets deleted
|
|
1162
|
+
|
|
1163
|
+
export async function startTerminalRepl() {
|
|
1164
|
+
_cachedCwd = process.cwd(); // Cache startup CWD for recovery
|
|
1165
|
+
|
|
1166
|
+
const cliArgs = parseArgs(process.argv.slice(2));
|
|
1167
|
+
const auth = new TarangAuth();
|
|
1168
|
+
|
|
1169
|
+
// BM25 retriever — indexes project files for search_code tool
|
|
1170
|
+
const retriever = new ContextRetriever(safeCwd());
|
|
1171
|
+
const toolExecutor = createToolExecutor({ retriever });
|
|
1172
|
+
const skipPerms = cliArgs.freeswim;
|
|
1173
|
+
const approval = new ApprovalManager({ autoApprove: skipPerms });
|
|
1174
|
+
|
|
1175
|
+
// Session manager — persists conversation messages to .kepler/conversations/
|
|
1176
|
+
const sessionMgr = new SessionManager(safeCwd());
|
|
1177
|
+
_sessionMgr = sessionMgr; // expose to renderEvent
|
|
1178
|
+
|
|
1179
|
+
// Local JSONL writer — writes cc-lens compatible session data to ~/.kepler/
|
|
1180
|
+
const jsonlWriter = new JsonlWriter(safeCwd(), VERSION);
|
|
1181
|
+
|
|
1182
|
+
// Persistent stream client — session_id captured from backend on first turn
|
|
1183
|
+
let streamClient = null;
|
|
1184
|
+
|
|
1185
|
+
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr };
|
|
1186
|
+
|
|
1187
|
+
printBanner(auth);
|
|
1188
|
+
|
|
1189
|
+
// ── Initialization with progress ──
|
|
1190
|
+
// BM25 indexing is CPU-bound and blocks the event loop, so setInterval
|
|
1191
|
+
// spinners won't tick during it. Instead, show a static "Initializing..."
|
|
1192
|
+
// message, then yield to the event loop between phases so the spinner runs.
|
|
1193
|
+
let projectSkeleton = '';
|
|
1194
|
+
|
|
1195
|
+
// Phase 1: Show immediate feedback
|
|
1196
|
+
process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
|
|
1197
|
+
|
|
1198
|
+
// Fetch user in parallel (network I/O, won't block event loop)
|
|
1199
|
+
const userPromise = fetchUser(ctx);
|
|
1200
|
+
|
|
1201
|
+
// Phase 2: BM25 index — CPU-bound, blocks event loop.
|
|
1202
|
+
// Wrap in a microtask break so the initial message renders first.
|
|
1203
|
+
const indexResult = await new Promise((resolve) => {
|
|
1204
|
+
// Let the event loop flush stderr before blocking
|
|
1205
|
+
setImmediate(async () => {
|
|
1206
|
+
try {
|
|
1207
|
+
process.stderr.write(`\r ${c.brand('⠹')} ${c.dim('Indexing project files...')}${' '.repeat(20)}\r`);
|
|
1208
|
+
const result = await retriever.buildIndex();
|
|
1209
|
+
resolve(result);
|
|
1210
|
+
} catch {
|
|
1211
|
+
resolve({ fileCount: 0, chunkCount: 0 });
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// Phase 3: Build skeleton (fast, synchronous)
|
|
1217
|
+
process.stderr.write(`\r ${c.brand('⠼')} ${c.dim('Building project skeleton...')}${' '.repeat(20)}\r`);
|
|
1218
|
+
await new Promise(r => setImmediate(r)); // yield so message renders
|
|
1219
|
+
projectSkeleton = buildProjectSkeleton(safeCwd());
|
|
1220
|
+
|
|
1221
|
+
// Wait for user fetch
|
|
1222
|
+
await userPromise;
|
|
1223
|
+
|
|
1224
|
+
// Clear the spinner line
|
|
1225
|
+
process.stderr.write(`\r${' '.repeat(60)}\r`);
|
|
1226
|
+
|
|
1227
|
+
// Show init summary
|
|
1228
|
+
if (indexResult.fileCount > 0) {
|
|
1229
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Indexed ${indexResult.fileCount} files (${indexResult.chunkCount} chunks)`)}\n`);
|
|
1230
|
+
}
|
|
1231
|
+
if (projectSkeleton) {
|
|
1232
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Project skeleton ready')}\n`);
|
|
1233
|
+
}
|
|
1234
|
+
if (session.user) {
|
|
1235
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
|
|
1236
|
+
}
|
|
1237
|
+
// ── Resume previous session ──
|
|
1238
|
+
if (cliArgs.resume) {
|
|
1239
|
+
const lastSession = cliArgs.resumeSessionId
|
|
1240
|
+
? { sessionId: cliArgs.resumeSessionId }
|
|
1241
|
+
: sessionMgr.getLastSession();
|
|
1242
|
+
|
|
1243
|
+
if (lastSession) {
|
|
1244
|
+
const messages = sessionMgr.loadMessages(lastSession.sessionId);
|
|
1245
|
+
if (messages.length > 0) {
|
|
1246
|
+
session.history = messages;
|
|
1247
|
+
session.id = lastSession.sessionId;
|
|
1248
|
+
session.turns = Math.floor(messages.length / 2);
|
|
1249
|
+
process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed session: ${messages.length} messages`)}`);
|
|
1250
|
+
if (lastSession.instruction) {
|
|
1251
|
+
process.stderr.write(` ${c.dim('—')} ${c.dim(lastSession.instruction.slice(0, 50))}`);
|
|
1252
|
+
}
|
|
1253
|
+
process.stderr.write('\n');
|
|
1254
|
+
} else {
|
|
1255
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('No conversation found for session ' + lastSession.sessionId)}\n`);
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('No previous session to resume')}\n`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
process.stderr.write(`\n ${c.dim('Press')} ${c.brand('Enter')} ${c.dim('to start, or type a prompt below.')}\n`);
|
|
1263
|
+
|
|
1264
|
+
const PROMPT = `${c.brand('kepler')} ${c.dim('›')} `;
|
|
1265
|
+
|
|
1266
|
+
const rl = readline.createInterface({
|
|
1267
|
+
input: process.stdin,
|
|
1268
|
+
output: process.stderr,
|
|
1269
|
+
prompt: PROMPT,
|
|
1270
|
+
completer: (line) => {
|
|
1271
|
+
if (line.startsWith('/')) {
|
|
1272
|
+
const hits = Object.keys(COMMANDS).filter(cmd => cmd.startsWith(line));
|
|
1273
|
+
return [hits.length ? hits : Object.keys(COMMANDS), line];
|
|
1274
|
+
}
|
|
1275
|
+
return [[], line];
|
|
1276
|
+
},
|
|
1277
|
+
historySize: 100,
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// Give approval manager access to readline for pause/resume
|
|
1281
|
+
approval.setReadline(rl);
|
|
1282
|
+
ctx._rl = rl; // expose to /resume command for readline pause
|
|
1283
|
+
|
|
1284
|
+
// Helper: show prompt with separator + vertical breathing room
|
|
1285
|
+
function showPrompt() {
|
|
1286
|
+
printPromptBlock();
|
|
1287
|
+
process.stderr.write('\n'); // half-inch vertical gap above input line
|
|
1288
|
+
rl.prompt();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
showPrompt();
|
|
1292
|
+
|
|
1293
|
+
rl.on('line', async (line) => {
|
|
1294
|
+
const input = line.trim();
|
|
1295
|
+
if (!input) { rl.prompt(); return; }
|
|
1296
|
+
|
|
1297
|
+
// Save to input history
|
|
1298
|
+
session.inputHistory.push(input);
|
|
1299
|
+
|
|
1300
|
+
// Slash commands
|
|
1301
|
+
if (input.startsWith('/')) {
|
|
1302
|
+
await handleCommand(input, ctx);
|
|
1303
|
+
showPrompt();
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Regular prompt
|
|
1308
|
+
session.history.push({ role: 'user', content: input });
|
|
1309
|
+
session.turns++;
|
|
1310
|
+
session.toolCalls = 0;
|
|
1311
|
+
|
|
1312
|
+
// Start session tracking on first turn
|
|
1313
|
+
if (session.turns === 1) {
|
|
1314
|
+
sessionMgr.start(input);
|
|
1315
|
+
}
|
|
1316
|
+
sessionMgr.saveMessage('user', input);
|
|
1317
|
+
|
|
1318
|
+
// Local JSONL: write user turn + history
|
|
1319
|
+
jsonlWriter.writeUserTurn(input);
|
|
1320
|
+
jsonlWriter.writeHistory(input);
|
|
1321
|
+
|
|
1322
|
+
const creds = auth.loadCredentials();
|
|
1323
|
+
if (!creds.token) {
|
|
1324
|
+
process.stderr.write(` ${c.red('Not logged in. Run /login first.')}\n`);
|
|
1325
|
+
showPrompt();
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Kepler response label
|
|
1330
|
+
process.stderr.write(`\n${c.bold(c.brand('kepler'))}\n`);
|
|
1331
|
+
|
|
1332
|
+
// Create or reuse stream client — sessionId persists across turns
|
|
1333
|
+
if (!streamClient || streamClient.baseUrl !== creds.backendUrl || streamClient.token !== creds.token) {
|
|
1334
|
+
streamClient = new TarangStreamClient({
|
|
1335
|
+
baseUrl: creds.backendUrl,
|
|
1336
|
+
token: creds.token,
|
|
1337
|
+
toolExecutor,
|
|
1338
|
+
approvalManager: approval,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
const client = streamClient;
|
|
1342
|
+
|
|
1343
|
+
let assistantContent = '';
|
|
1344
|
+
|
|
1345
|
+
// ── Execution keypress listener (Esc = cancel, Space = pause/resume) ──
|
|
1346
|
+
let executionPaused = false;
|
|
1347
|
+
let keypressCleanup = null;
|
|
1348
|
+
let execListenerActive = false;
|
|
1349
|
+
|
|
1350
|
+
if (process.stdin.isTTY) {
|
|
1351
|
+
rl.pause();
|
|
1352
|
+
const wasRaw = process.stdin.isRaw;
|
|
1353
|
+
process.stdin.setRawMode(true);
|
|
1354
|
+
process.stdin.resume();
|
|
1355
|
+
execListenerActive = true;
|
|
1356
|
+
|
|
1357
|
+
const onData = (data) => {
|
|
1358
|
+
if (!execListenerActive) return; // paused for approval menu
|
|
1359
|
+
const bytes = [...data];
|
|
1360
|
+
|
|
1361
|
+
// Esc key (single byte 0x1b, not part of arrow sequence)
|
|
1362
|
+
if (bytes.length === 1 && bytes[0] === 0x1b) {
|
|
1363
|
+
stopSpinner();
|
|
1364
|
+
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('Cancelling...')}\n`);
|
|
1365
|
+
client.cancel();
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Space — toggle pause/resume
|
|
1370
|
+
if (bytes.length === 1 && bytes[0] === 0x20) {
|
|
1371
|
+
if (executionPaused) {
|
|
1372
|
+
executionPaused = false;
|
|
1373
|
+
process.stderr.write(` ${c.green('▶')} ${c.dim('Resumed')}\n`);
|
|
1374
|
+
client.resume();
|
|
1375
|
+
} else {
|
|
1376
|
+
executionPaused = true;
|
|
1377
|
+
stopSpinner();
|
|
1378
|
+
process.stderr.write(` ${c.yellow('⏸')} ${c.dim('Paused — press Space to resume, Esc to cancel')}\n`);
|
|
1379
|
+
client.pause();
|
|
1380
|
+
}
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Ctrl+C during execution
|
|
1385
|
+
if (bytes[0] === 0x03) {
|
|
1386
|
+
stopSpinner();
|
|
1387
|
+
client.cancel();
|
|
1388
|
+
process.exit(0);
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
process.stdin.on('data', onData);
|
|
1393
|
+
|
|
1394
|
+
// Let approval manager pause/resume this listener
|
|
1395
|
+
approval.setExecutionHooks({
|
|
1396
|
+
onPause: () => { execListenerActive = false; },
|
|
1397
|
+
onResume: () => { execListenerActive = true; },
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
keypressCleanup = () => {
|
|
1401
|
+
process.stdin.removeListener('data', onData);
|
|
1402
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
1403
|
+
execListenerActive = false;
|
|
1404
|
+
approval.setExecutionHooks({}); // clear hooks
|
|
1405
|
+
rl.resume();
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
try {
|
|
1410
|
+
startContentStream();
|
|
1411
|
+
|
|
1412
|
+
const execContext = { cwd: safeCwd() };
|
|
1413
|
+
if (skipPerms) execContext.freeswim = true;
|
|
1414
|
+
if (projectSkeleton) execContext.project_skeleton = projectSkeleton;
|
|
1415
|
+
|
|
1416
|
+
for await (const event of client.execute(input, execContext, session.history)) {
|
|
1417
|
+
renderEvent(event);
|
|
1418
|
+
|
|
1419
|
+
if (event.type === 'content_partial') {
|
|
1420
|
+
const text = event.data?.text || '';
|
|
1421
|
+
assistantContent += text;
|
|
1422
|
+
jsonlWriter.accumulateContent(text);
|
|
1423
|
+
} else if (event.type === 'content') {
|
|
1424
|
+
const text = event.data?.text || '';
|
|
1425
|
+
const newText = assistantContent && text.startsWith(assistantContent)
|
|
1426
|
+
? text.slice(assistantContent.length)
|
|
1427
|
+
: text === assistantContent ? '' : text;
|
|
1428
|
+
if (text) {
|
|
1429
|
+
assistantContent = assistantContent && !text.startsWith(assistantContent)
|
|
1430
|
+
? assistantContent + text
|
|
1431
|
+
: text;
|
|
1432
|
+
}
|
|
1433
|
+
if (newText) jsonlWriter.accumulateContent(newText);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Local JSONL: capture session ID from backend
|
|
1437
|
+
if (event.type === 'session_info' && event.data?.session_id) {
|
|
1438
|
+
jsonlWriter.setSessionId(event.data.session_id);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Local JSONL: accumulate tool calls
|
|
1442
|
+
if (event.type === 'tool_call' || event.type === 'tool_request') {
|
|
1443
|
+
const d = event.data || {};
|
|
1444
|
+
jsonlWriter.accumulateToolCall(d.call_id || d.request_id, d.tool, d.args);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Local JSONL: record tool results
|
|
1448
|
+
if (event.type === 'tool_done' || event.type === 'tool_result') {
|
|
1449
|
+
const d = event.data || {};
|
|
1450
|
+
jsonlWriter.recordToolResult(d.call_id || d._callId, d.output, d.success === false);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Local JSONL: flush assistant turn on complete
|
|
1454
|
+
if (event.type === 'complete') {
|
|
1455
|
+
jsonlWriter.setTurnUsage(event.data?.usage, session.model);
|
|
1456
|
+
jsonlWriter.flushAssistantTurn();
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
flushContent();
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
inPlace('');
|
|
1463
|
+
flushContent();
|
|
1464
|
+
process.stderr.write(` ${c.red('Error: ' + err.message)}\n`);
|
|
1465
|
+
} finally {
|
|
1466
|
+
// Clean up execution keypress listener
|
|
1467
|
+
if (keypressCleanup) keypressCleanup();
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (assistantContent) {
|
|
1471
|
+
session.history.push({ role: 'assistant', content: assistantContent });
|
|
1472
|
+
sessionMgr.saveMessage('assistant', assistantContent);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
showPrompt();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
rl.on('close', async () => {
|
|
1479
|
+
stopSpinner();
|
|
1480
|
+
await jsonlWriter.close();
|
|
1481
|
+
process.stderr.write(`\n ${c.dim('session ended')}\n\n`);
|
|
1482
|
+
process.exit(0);
|
|
1483
|
+
});
|
|
1484
|
+
}
|