@axplusb/kepler 2.0.7 → 2.3.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/KEPLER-README.md +157 -142
- package/README.md +37 -65
- package/package.json +2 -2
- package/src/config/cli-args.mjs +14 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/core/agent-loop.mjs +8 -2
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +164 -24
- package/src/core/cache-control.mjs +92 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/headless.mjs +99 -10
- package/src/core/jsonl-writer.mjs +50 -0
- package/src/core/local-agent.mjs +121 -14
- package/src/core/local-store.mjs +486 -5
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/rate-limit-display.mjs +97 -0
- package/src/core/resume-mode.mjs +154 -0
- package/src/core/risk-tier.mjs +88 -2
- package/src/core/safety.mjs +3 -0
- package/src/core/session-manager.mjs +53 -10
- package/src/core/stream-client.mjs +69 -10
- package/src/core/system-prompt.mjs +6 -1
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +72 -6
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +217 -0
- package/src/onboarding/preflight.mjs +27 -11
- package/src/permissions/command-classifier.mjs +78 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +9 -0
- package/src/terminal/repl.mjs +1822 -140
- package/src/tools/bash.mjs +57 -9
- package/src/tools/project-overview.mjs +83 -10
- package/src/ui/approval.mjs +154 -35
- package/src/ui/commands.mjs +5 -4
- package/src/ui/formatter.mjs +39 -5
- package/src/ui/mission-report.mjs +55 -22
- package/src/ui/slash-commands.mjs +57 -5
- package/src/ui/tool-card.mjs +51 -1
package/src/terminal/repl.mjs
CHANGED
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
|
|
18
18
|
import * as readline from 'node:readline';
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
20
21
|
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
21
22
|
import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
22
23
|
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
23
24
|
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
24
25
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
26
|
+
import { buildWorkScope } from '../core/work-scope.mjs';
|
|
25
27
|
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
28
|
+
import { HookRunner } from '../config/hook-runner.mjs';
|
|
26
29
|
import { runPreflight } from '../onboarding/preflight.mjs';
|
|
27
30
|
import { printBanner as printBrandedBanner } from '../ui/banner.mjs';
|
|
28
31
|
import { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '../ui/mission-report.mjs';
|
|
@@ -36,10 +39,17 @@ import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
|
36
39
|
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
37
40
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
38
41
|
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
42
|
+
import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
|
|
39
43
|
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
40
44
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
41
45
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
42
|
-
import {
|
|
46
|
+
import { loadEffectivePolicy, formatPolicySourceRows } from '../core/policy-resolver.mjs';
|
|
47
|
+
import { loadProjectContext } from '../core/project-context-loader.mjs';
|
|
48
|
+
import { buildContextEnvelope } from '../core/context-envelope.mjs';
|
|
49
|
+
import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
|
|
50
|
+
import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
|
|
51
|
+
import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
|
|
52
|
+
import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
|
|
43
53
|
import { createOrbit } from '../state/orbit.mjs';
|
|
44
54
|
import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
|
|
45
55
|
import { term } from '../ui/term.mjs';
|
|
@@ -89,6 +99,667 @@ function safeCwd() {
|
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
|
|
102
|
+
function messageCountLabel(count) {
|
|
103
|
+
return `${count} ${count === 1 ? 'message' : 'messages'}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sessionListTimestamp(s) {
|
|
107
|
+
const value = s.updatedAt || s.startedAt;
|
|
108
|
+
return value ? new Date(value).toLocaleString() : '?';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function oneLineInstruction(text, max = 72) {
|
|
112
|
+
const compact = String(text || '(no instruction)').replace(/\s+/g, ' ').trim();
|
|
113
|
+
return compact.length > max ? compact.slice(0, max - 3) + '...' : compact;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fitAnsiLine(text, maxColumns) {
|
|
117
|
+
const max = Math.max(1, Number(maxColumns) || 80);
|
|
118
|
+
const value = String(text || '');
|
|
119
|
+
if (stripAnsi(value).length <= max) return value;
|
|
120
|
+
|
|
121
|
+
let visible = 0;
|
|
122
|
+
let out = '';
|
|
123
|
+
for (let i = 0; i < value.length; i++) {
|
|
124
|
+
if (value[i] === '\x1b') {
|
|
125
|
+
const match = value.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
126
|
+
if (match) {
|
|
127
|
+
out += match[0];
|
|
128
|
+
i += match[0].length - 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (visible >= max - 1) break;
|
|
133
|
+
out += value[i];
|
|
134
|
+
visible++;
|
|
135
|
+
}
|
|
136
|
+
return `${out}${c.dim('…')}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeResumableSession(s) {
|
|
140
|
+
return {
|
|
141
|
+
sessionId: s.sessionId,
|
|
142
|
+
instruction: s.firstPrompt || s.instruction || '(no instruction)',
|
|
143
|
+
startedAt: s.startTime || s.startedAt || '',
|
|
144
|
+
updatedAt: s.endTime || s.updatedAt || (s.mtime ? new Date(s.mtime).toISOString() : ''),
|
|
145
|
+
project: s.project ? path.basename(s.project) : s.projectName || s.project || '',
|
|
146
|
+
projectPath: s.project || s.projectPath || '',
|
|
147
|
+
transcriptPath: s.filePath || s.transcriptPath || '',
|
|
148
|
+
messageCount: (s.userMessages || 0) + (s.assistantMessages || 0),
|
|
149
|
+
// PRD-068 §5.14.11 derived fields for the picker
|
|
150
|
+
endStatus: s.endStatus || 'unknown', // 'completed' | 'interrupted' | 'errored' | 'unknown'
|
|
151
|
+
contextTokens: s.contextTokens || 0, // projected transcript token count
|
|
152
|
+
resumeSummary: s.resumeSummary || null, // latest resume_summary checkpoint metadata
|
|
153
|
+
costUsd: typeof s.costUsd === 'number' ? s.costUsd : 0,
|
|
154
|
+
partial: !!s.partial, // true if the transcript file was partially malformed
|
|
155
|
+
source: 'transcript',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function listResumableSessions() {
|
|
160
|
+
// PRD-068 §5.14.6: JSONL is the single source of truth. The legacy
|
|
161
|
+
// per-project state-only entries never had a transcript, so they can't be
|
|
162
|
+
// replayed — silently dropping them removes a source of "picked a session
|
|
163
|
+
// and got a flat history" surprises.
|
|
164
|
+
const rich = (await getRecentSessions(Infinity)).map(normalizeResumableSession);
|
|
165
|
+
return rich.sort((a, b) => {
|
|
166
|
+
const at = Date.parse(a.updatedAt || a.startedAt || 0) || 0;
|
|
167
|
+
const bt = Date.parse(b.updatedAt || b.startedAt || 0) || 0;
|
|
168
|
+
return bt - at;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── PRD-068 §5.14 helpers ────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function endStatusMarker(status) {
|
|
175
|
+
switch (status) {
|
|
176
|
+
case 'completed': return c.green('✓');
|
|
177
|
+
case 'interrupted': return c.yellow('⚠');
|
|
178
|
+
case 'errored': return c.red('✗');
|
|
179
|
+
default: return c.dim('·');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatSessionCost(usd) {
|
|
184
|
+
const n = Number(usd);
|
|
185
|
+
if (!Number.isFinite(n) || n <= 0) return c.dim(' ');
|
|
186
|
+
if (n < 0.01) return c.dim('<$0.01');
|
|
187
|
+
return c.dim(`$${n.toFixed(2)}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function formatResumeCheckpointStatus(session) {
|
|
191
|
+
const marker = session?.resumeSummary;
|
|
192
|
+
if (!marker || !Number(marker.sourceMessageCount)) return '';
|
|
193
|
+
const full = Number(marker.fullMessageCount) || 0;
|
|
194
|
+
const covered = Number(marker.sourceMessageCount) || 0;
|
|
195
|
+
const pct = full > 0 ? ` ${Math.min(100, Math.round((covered / full) * 100))}%` : '';
|
|
196
|
+
return c.dim(` · summarized${pct}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function formatRelativeTime(iso) {
|
|
200
|
+
if (!iso) return '';
|
|
201
|
+
const t = Date.parse(iso);
|
|
202
|
+
if (!Number.isFinite(t)) return '';
|
|
203
|
+
const ago = Date.now() - t;
|
|
204
|
+
const m = Math.floor(ago / 60_000);
|
|
205
|
+
if (m < 1) return 'just now';
|
|
206
|
+
if (m < 60) return `${m}m ago`;
|
|
207
|
+
const h = Math.floor(m / 60);
|
|
208
|
+
if (h < 24) return `${h}h ago`;
|
|
209
|
+
const d = Math.floor(h / 24);
|
|
210
|
+
if (d < 30) return `${d}d ago`;
|
|
211
|
+
const mo = Math.floor(d / 30);
|
|
212
|
+
return `${mo}mo ago`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* PRD-068 §5.14.2 — enriched one-prompt picker.
|
|
217
|
+
* Returns the picked session, `null` on cancel, or `{ action: 'preview', session }`
|
|
218
|
+
* when the user hits P.
|
|
219
|
+
*/
|
|
220
|
+
async function pickResumableSession(resumable, ctx) {
|
|
221
|
+
const rl = ctx._rl || null;
|
|
222
|
+
if (rl) rl.pause();
|
|
223
|
+
|
|
224
|
+
return await new Promise((resolve) => {
|
|
225
|
+
if (!process.stdin.isTTY) { resolve(null); return; }
|
|
226
|
+
const wasRaw = process.stdin.isRaw;
|
|
227
|
+
const pageSize = Math.min(10, resumable.length);
|
|
228
|
+
const numWidth = String(resumable.length).length;
|
|
229
|
+
let selected = 0;
|
|
230
|
+
let offset = 0;
|
|
231
|
+
let renderedLines = 0;
|
|
232
|
+
|
|
233
|
+
const renderMenu = () => {
|
|
234
|
+
if (renderedLines > 0) {
|
|
235
|
+
process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
|
|
236
|
+
}
|
|
237
|
+
if (selected < offset) offset = selected;
|
|
238
|
+
if (selected >= offset + pageSize) offset = selected - pageSize + 1;
|
|
239
|
+
|
|
240
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
241
|
+
const lines = [];
|
|
242
|
+
lines.push(` ${c.bold('Resume a session')}`);
|
|
243
|
+
lines.push('');
|
|
244
|
+
const end = Math.min(offset + pageSize, resumable.length);
|
|
245
|
+
for (let i = offset; i < end; i++) {
|
|
246
|
+
const s = resumable[i];
|
|
247
|
+
const marker = i === selected ? c.brand('▸') : ' ';
|
|
248
|
+
const num = c.dim(`[${String(i + 1).padStart(numWidth, ' ')}]`);
|
|
249
|
+
const project = (s.project || '(unknown)').padEnd(18, ' ').slice(0, 18);
|
|
250
|
+
const ago = formatRelativeTime(s.updatedAt || s.startedAt).padEnd(9, ' ').slice(0, 9);
|
|
251
|
+
const status = endStatusMarker(s.endStatus);
|
|
252
|
+
const msgs = String(s.messageCount).padStart(3, ' ') + ' msgs';
|
|
253
|
+
const ctx = c.dim(`${formatCtxTokens(s.contextTokens).padStart(5, ' ')} ctx`) + formatResumeCheckpointStatus(s);
|
|
254
|
+
const cost = formatSessionCost(s.costUsd);
|
|
255
|
+
const partial = s.partial ? c.yellow(' ⚠partial') : '';
|
|
256
|
+
const instr = oneLineInstruction(s.instruction, 48);
|
|
257
|
+
lines.push(fitAnsiLine(
|
|
258
|
+
` ${marker} ${num} ${c.brand(project)} ${c.dim(ago)} ${status} ${c.dim(msgs)} ${ctx} ${cost}${partial} ${c.dim(instr)}`,
|
|
259
|
+
cols - 1
|
|
260
|
+
));
|
|
261
|
+
}
|
|
262
|
+
lines.push('');
|
|
263
|
+
lines.push(fitAnsiLine(
|
|
264
|
+
` ${c.dim(`↑↓ move · Enter resume · P preview · Esc cancel · ${selected + 1}/${resumable.length}`)}`,
|
|
265
|
+
cols - 1
|
|
266
|
+
));
|
|
267
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
268
|
+
renderedLines = lines.length;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const cleanup = (value) => {
|
|
272
|
+
process.stdin.removeListener('data', onData);
|
|
273
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
274
|
+
if (rl) rl.resume();
|
|
275
|
+
resolve(value);
|
|
276
|
+
};
|
|
277
|
+
const onData = (data) => {
|
|
278
|
+
const key = data.toString('utf8');
|
|
279
|
+
if (key === '' || key === '') { cleanup(null); return; }
|
|
280
|
+
if (key === '\r' || key === '\n') { cleanup({ action: 'resume', session: resumable[selected] }); return; }
|
|
281
|
+
if (key === 'p' || key === 'P') { cleanup({ action: 'preview', session: resumable[selected] }); return; }
|
|
282
|
+
if (key === '[A') { selected = Math.max(0, selected - 1); renderMenu(); return; }
|
|
283
|
+
if (key === '[B') { selected = Math.min(resumable.length - 1, selected + 1); renderMenu(); return; }
|
|
284
|
+
if (key === '[5~') { selected = Math.max(0, selected - pageSize); renderMenu(); return; }
|
|
285
|
+
if (key === '[6~') { selected = Math.min(resumable.length - 1, selected + pageSize); renderMenu(); return; }
|
|
286
|
+
if (key === '[H' || key === '[1~') { selected = 0; renderMenu(); return; }
|
|
287
|
+
if (key === '[F' || key === '[4~') { selected = resumable.length - 1; renderMenu(); return; }
|
|
288
|
+
if (/^[1-9]$/.test(key)) {
|
|
289
|
+
const index = Number(key) - 1;
|
|
290
|
+
if (index < resumable.length) cleanup({ action: 'resume', session: resumable[index] });
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
process.stdin.setRawMode(true);
|
|
295
|
+
process.stdin.resume();
|
|
296
|
+
process.stdin.on('data', onData);
|
|
297
|
+
renderMenu();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* PRD-068 §5.14.4 — tri-choice overlay shown only when projected ctx > highWatermark.
|
|
303
|
+
* Returns 'full' | 'summary' | 'tail-10' | 'tail-20' | null (cancel).
|
|
304
|
+
*/
|
|
305
|
+
async function chooseThresholdMode(ctx, decision) {
|
|
306
|
+
if (!process.stdin.isTTY) return decision.defaultChoice;
|
|
307
|
+
const rl = ctx._rl || null;
|
|
308
|
+
if (rl) rl.pause();
|
|
309
|
+
|
|
310
|
+
const canFull = decision.mode !== 'no-full-allowed';
|
|
311
|
+
const options = [
|
|
312
|
+
{ key: 'f', value: 'full', label: 'full transcript', enabled: canFull },
|
|
313
|
+
{ key: 's', value: 'summary', label: 'summary only', enabled: true },
|
|
314
|
+
{ key: '1', value: 'tail-10', label: 'summary + last 10 turns', enabled: true },
|
|
315
|
+
{ key: '2', value: 'tail-20', label: 'summary + last 20 turns', enabled: true },
|
|
316
|
+
];
|
|
317
|
+
let selected = options.findIndex(o => o.value === decision.defaultChoice && o.enabled);
|
|
318
|
+
if (selected < 0) selected = options.findIndex(o => o.enabled);
|
|
319
|
+
|
|
320
|
+
return await new Promise((resolve) => {
|
|
321
|
+
const wasRaw = process.stdin.isRaw;
|
|
322
|
+
let renderedLines = 0;
|
|
323
|
+
|
|
324
|
+
const render = () => {
|
|
325
|
+
if (renderedLines > 0) process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
|
|
326
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
327
|
+
const pct = Math.round(decision.usageRatio * 100);
|
|
328
|
+
const projected = formatCtxTokens(decision.projected);
|
|
329
|
+
const win = formatCtxTokens(decision.windowSize);
|
|
330
|
+
const lines = [];
|
|
331
|
+
lines.push(` This session would use ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
|
|
332
|
+
if (decision.resumeSummary?.sourceMessageCount) {
|
|
333
|
+
const covered = Number(decision.resumeSummary.sourceMessageCount) || 0;
|
|
334
|
+
const full = Number(decision.resumeSummary.fullMessageCount) || 0;
|
|
335
|
+
const suffix = full > 0 ? ` (${covered}/${full} resume messages)` : '';
|
|
336
|
+
lines.push(` ${c.green('✓')} ${c.dim(`summary checkpoint available${suffix}; summary/tail modes reuse it`)}`);
|
|
337
|
+
}
|
|
338
|
+
lines.push(canFull
|
|
339
|
+
? ` ${c.yellow('⚠')} ${c.dim('close to the highWatermark — consider a leaner mode:')}`
|
|
340
|
+
: ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
|
|
341
|
+
lines.push('');
|
|
342
|
+
for (let i = 0; i < options.length; i++) {
|
|
343
|
+
const o = options[i];
|
|
344
|
+
const disabled = !o.enabled;
|
|
345
|
+
const marker = i === selected && !disabled ? c.brand('▸') : ' ';
|
|
346
|
+
const keyTag = c.dim('[') + (disabled ? c.dim(o.key) : c.brand(o.key)) + c.dim(']');
|
|
347
|
+
const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected));
|
|
348
|
+
const label = disabled ? c.dim(o.label) : (i === selected ? c.brand(o.label) : o.label);
|
|
349
|
+
const projCol = c.dim(`${proj.padStart(5, ' ')} ctx`);
|
|
350
|
+
const suffix = disabled ? c.dim(' (over hardCap)') : '';
|
|
351
|
+
lines.push(fitAnsiLine(` ${marker} ${keyTag} ${label.padEnd(30, ' ')} ${projCol}${suffix}`, cols - 1));
|
|
352
|
+
}
|
|
353
|
+
lines.push('');
|
|
354
|
+
lines.push(fitAnsiLine(` ${c.dim('↑↓ move · Enter pick · f/s/1/2 shortcut · Esc cancel')}`, cols - 1));
|
|
355
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
356
|
+
renderedLines = lines.length;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const cleanup = (value) => {
|
|
360
|
+
process.stdin.removeListener('data', onData);
|
|
361
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
362
|
+
if (rl) rl.resume();
|
|
363
|
+
resolve(value);
|
|
364
|
+
};
|
|
365
|
+
const onData = (data) => {
|
|
366
|
+
const key = data.toString('utf8');
|
|
367
|
+
const low = key.toLowerCase();
|
|
368
|
+
if (key === '' || key === '') { cleanup(null); return; }
|
|
369
|
+
if (key === '\r' || key === '\n') { cleanup(options[selected]?.value || null); return; }
|
|
370
|
+
if (key === '[A') {
|
|
371
|
+
// step to previous enabled option
|
|
372
|
+
for (let i = selected - 1; i >= 0; i--) if (options[i].enabled) { selected = i; render(); return; }
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (key === '[B') {
|
|
376
|
+
for (let i = selected + 1; i < options.length; i++) if (options[i].enabled) { selected = i; render(); return; }
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
for (let i = 0; i < options.length; i++) {
|
|
380
|
+
if (options[i].key === low && options[i].enabled) { cleanup(options[i].value); return; }
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
process.stdin.setRawMode(true);
|
|
385
|
+
process.stdin.resume();
|
|
386
|
+
process.stdin.on('data', onData);
|
|
387
|
+
render();
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function resumeModeLabel(mode = 'full') {
|
|
392
|
+
if (mode === 'summary') return 'summary only';
|
|
393
|
+
const tailTurns = resumeTailTurnCount(mode);
|
|
394
|
+
if (tailTurns) return `summary + last ${tailTurns} turns`;
|
|
395
|
+
return mode || 'full';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* PRD-068 §5.14.5 — preview overlay for a session/mode. Read-only, `q` to return.
|
|
400
|
+
* If user hits Enter, resolve to the currently-previewed mode so caller can activate.
|
|
401
|
+
*/
|
|
402
|
+
async function previewResumeSession(session, ctx) {
|
|
403
|
+
if (!process.stdin.isTTY) return null;
|
|
404
|
+
const detail = await getSessionDetail(session.sessionId, { filePath: session.transcriptPath });
|
|
405
|
+
if (!detail) return null;
|
|
406
|
+
|
|
407
|
+
const rl = ctx._rl || null;
|
|
408
|
+
if (rl) rl.pause();
|
|
409
|
+
|
|
410
|
+
let mode = 'summary';
|
|
411
|
+
const rich = () => buildResumeHistory({ ...detail, recapTailTurns: 8 }, mode);
|
|
412
|
+
let history = rich();
|
|
413
|
+
|
|
414
|
+
return await new Promise((resolve) => {
|
|
415
|
+
const wasRaw = process.stdin.isRaw;
|
|
416
|
+
let renderedLines = 0;
|
|
417
|
+
let scrollOffset = 0;
|
|
418
|
+
|
|
419
|
+
const render = () => {
|
|
420
|
+
if (renderedLines > 0) process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
|
|
421
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
422
|
+
const rows = Math.max(10, Math.min((process.stderr.rows || 30) - 6, 20));
|
|
423
|
+
const contentLines = (history.summary || '').split('\n');
|
|
424
|
+
// For tail/full modes, also append serialized tail so preview reflects
|
|
425
|
+
// what the agent will actually receive.
|
|
426
|
+
if (mode !== 'summary') {
|
|
427
|
+
contentLines.push('', c.dim('── conversation tail ──'));
|
|
428
|
+
for (const msg of history.agentHistory.slice(1)) {
|
|
429
|
+
contentLines.push(`${msg.role === 'user' ? c.dim('You:') : c.brand('Kepler:')} ${String(msg.content).slice(0, 300)}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const totalLines = contentLines.length;
|
|
433
|
+
const maxOffset = Math.max(0, totalLines - rows);
|
|
434
|
+
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
|
|
435
|
+
|
|
436
|
+
const lines = [];
|
|
437
|
+
lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens)) + ' ctx')}`);
|
|
438
|
+
lines.push(` ${c.dim('─'.repeat(60))}`);
|
|
439
|
+
for (let i = scrollOffset; i < Math.min(scrollOffset + rows, totalLines); i++) {
|
|
440
|
+
lines.push(fitAnsiLine(` ${c.dim(contentLines[i] || '')}`, cols - 1));
|
|
441
|
+
}
|
|
442
|
+
lines.push('');
|
|
443
|
+
lines.push(fitAnsiLine(` ${c.dim(`↑↓/PgUp/PgDn scroll · f/s/1/2 switch mode · Enter resume this · q back · ${scrollOffset + 1}-${Math.min(scrollOffset + rows, totalLines)}/${totalLines}`)}`, cols - 1));
|
|
444
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
445
|
+
renderedLines = lines.length;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const cleanup = (value) => {
|
|
449
|
+
process.stdin.removeListener('data', onData);
|
|
450
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
451
|
+
if (rl) rl.resume();
|
|
452
|
+
resolve(value);
|
|
453
|
+
};
|
|
454
|
+
const onData = (data) => {
|
|
455
|
+
const key = data.toString('utf8');
|
|
456
|
+
const low = key.toLowerCase();
|
|
457
|
+
if (key === '' || key === '' || low === 'q') { cleanup({ action: 'back' }); return; }
|
|
458
|
+
if (key === '\r' || key === '\n') { cleanup({ action: 'resume', mode }); return; }
|
|
459
|
+
if (key === '[A') { scrollOffset = Math.max(0, scrollOffset - 1); render(); return; }
|
|
460
|
+
if (key === '[B') { scrollOffset += 1; render(); return; }
|
|
461
|
+
if (key === '[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
|
|
462
|
+
if (key === '[6~') { scrollOffset += 10; render(); return; }
|
|
463
|
+
if (low === 'f') { mode = 'full'; history = rich(); scrollOffset = 0; render(); return; }
|
|
464
|
+
if (low === 's') { mode = 'summary'; history = rich(); scrollOffset = 0; render(); return; }
|
|
465
|
+
if (low === '1') { mode = 'tail-10'; history = rich(); scrollOffset = 0; render(); return; }
|
|
466
|
+
if (low === '2') { mode = 'tail-20'; history = rich(); scrollOffset = 0; render(); return; }
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
process.stdin.setRawMode(true);
|
|
470
|
+
process.stdin.resume();
|
|
471
|
+
process.stdin.on('data', onData);
|
|
472
|
+
render();
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* PRD-068 §5.14.7 — explicit cwd confirmation when the picked session lives
|
|
478
|
+
* elsewhere. Returns 'switch' | 'stay' | 'cancel'.
|
|
479
|
+
*/
|
|
480
|
+
async function confirmCwdSwitch(ctx, savedPath, currentPath) {
|
|
481
|
+
if (!process.stdin.isTTY) return 'switch';
|
|
482
|
+
process.stderr.write(`\n ${c.dim('This session lives in another repo:')}\n`);
|
|
483
|
+
process.stderr.write(` ${c.dim('→')} ${c.brand(savedPath)} ${c.dim(`(current cwd: ${currentPath})`)}\n`);
|
|
484
|
+
process.stderr.write(` ${c.dim('[Enter]')} switch cwd and resume · ${c.dim('[s]')} stay here and resume anyway · ${c.dim('[n]')} cancel `);
|
|
485
|
+
const rl = ctx._rl || null;
|
|
486
|
+
if (rl) rl.pause();
|
|
487
|
+
return await new Promise((resolve) => {
|
|
488
|
+
const wasRaw = process.stdin.isRaw;
|
|
489
|
+
const cleanup = (value) => {
|
|
490
|
+
process.stdin.removeListener('data', onData);
|
|
491
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
492
|
+
if (rl) rl.resume();
|
|
493
|
+
process.stderr.write('\n');
|
|
494
|
+
resolve(value);
|
|
495
|
+
};
|
|
496
|
+
const onData = (data) => {
|
|
497
|
+
const key = data.toString('utf8').toLowerCase();
|
|
498
|
+
if (key === '' || key === '' || key === 'n') { cleanup('cancel'); return; }
|
|
499
|
+
if (key === '\r' || key === '\n') { cleanup('switch'); return; }
|
|
500
|
+
if (key === 's') { cleanup('stay'); return; }
|
|
501
|
+
};
|
|
502
|
+
process.stdin.setRawMode(true);
|
|
503
|
+
process.stdin.resume();
|
|
504
|
+
process.stdin.on('data', onData);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Legacy prompt (kept as a fallback for callers that force compact/full explicitly).
|
|
509
|
+
async function chooseResumeHistoryMode(ctx, { defaultMode = 'compact' } = {}) {
|
|
510
|
+
if (!process.stdin.isTTY) return defaultMode;
|
|
511
|
+
process.stderr.write(`\n ${c.dim('Load history for agent:')} ${c.brand('[c]')} ${c.dim('compact summary')} ${c.brand('[f]')} ${c.dim('full transcript')} ${c.dim('(Enter = compact, Esc = cancel):')} `);
|
|
512
|
+
const rl = ctx._rl || null;
|
|
513
|
+
if (rl) rl.pause();
|
|
514
|
+
return await new Promise((resolve) => {
|
|
515
|
+
const wasRaw = process.stdin.isRaw;
|
|
516
|
+
const cleanup = (value) => {
|
|
517
|
+
process.stdin.removeListener('data', onData);
|
|
518
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
519
|
+
if (rl) rl.resume();
|
|
520
|
+
process.stderr.write('\n');
|
|
521
|
+
resolve(value);
|
|
522
|
+
};
|
|
523
|
+
const onData = (data) => {
|
|
524
|
+
const key = data.toString('utf8').toLowerCase();
|
|
525
|
+
if (key === '\u0003' || key === '\u001b') { cleanup(null); return; }
|
|
526
|
+
if (key === '\r' || key === '\n' || key === 'c') { cleanup('compact'); return; }
|
|
527
|
+
if (key === 'f') { cleanup('full'); }
|
|
528
|
+
};
|
|
529
|
+
process.stdin.setRawMode(true);
|
|
530
|
+
process.stdin.resume();
|
|
531
|
+
process.stdin.on('data', onData);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function historyRoleLabel(role) {
|
|
536
|
+
return role === 'user'
|
|
537
|
+
? c.white('You')
|
|
538
|
+
: role === 'tool'
|
|
539
|
+
? c.dim('Tool')
|
|
540
|
+
: c.brand('Kepler');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function renderHistoryEntries(entries, { limit = 20, maxChars = 120, title = 'Conversation' } = {}) {
|
|
544
|
+
const shown = limit === Infinity ? entries : entries.slice(-limit);
|
|
545
|
+
process.stderr.write(`\n ${c.bold(title)} (${shown.length}${shown.length === entries.length ? '' : ` of ${entries.length}`} entries)\n`);
|
|
546
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
547
|
+
for (const msg of shown) {
|
|
548
|
+
const content = String(msg.content || '').replace(/\s+/g, ' ').trim();
|
|
549
|
+
process.stderr.write(` ${historyRoleLabel(msg.role)}: ${content.slice(0, maxChars)}${content.length > maxChars ? '...' : ''}\n`);
|
|
550
|
+
}
|
|
551
|
+
process.stderr.write('\n');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderResumePreview(resumed) {
|
|
555
|
+
const tailTurns = resumeTailTurnCount(resumed.historyMode);
|
|
556
|
+
if (resumed.historyMode === 'compact' || resumed.historyMode === 'summary') {
|
|
557
|
+
if (!resumed.summary) return;
|
|
558
|
+
process.stderr.write(`\n ${c.bold('Continuity Summary Sent To Agent')}\n`);
|
|
559
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
560
|
+
for (const line of resumed.summary.split('\n')) {
|
|
561
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
562
|
+
}
|
|
563
|
+
process.stderr.write('\n');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (tailTurns && resumed.summary) {
|
|
568
|
+
process.stderr.write(`\n ${c.bold(`Summary + Last ${tailTurns} Turns`)}\n`);
|
|
569
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
570
|
+
for (const line of resumed.summary.split('\n')) {
|
|
571
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
572
|
+
}
|
|
573
|
+
process.stderr.write('\n');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (resumed.replayEvents?.length) {
|
|
577
|
+
const replayStartOrder = replayStartOrderForMode(resumed.history || [], resumed.historyMode);
|
|
578
|
+
const replayEvents = filterResumeReplayEvents(resumed.replayEvents)
|
|
579
|
+
.filter(item => replayStartOrder == null || !Number.isFinite(Number(item.order)) || Number(item.order) >= replayStartOrder);
|
|
580
|
+
const userTurns = (resumed.history || [])
|
|
581
|
+
.filter(m => m.role === 'user')
|
|
582
|
+
.filter(m => replayStartOrder == null || !Number.isFinite(Number(m.order)) || Number(m.order) >= replayStartOrder);
|
|
583
|
+
const replayItems = mergeResumeReplayItems(userTurns, replayEvents);
|
|
584
|
+
const replayTitle = tailTurns ? `Last ${tailTurns} Turns Replay` : 'Replayed Live Session Events';
|
|
585
|
+
process.stderr.write(`\n ${c.bold(replayTitle)} (${userTurns.length} turns, ${replayEvents.length} events)\n`);
|
|
586
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
587
|
+
const sessionSnapshot = JSON.parse(JSON.stringify(session));
|
|
588
|
+
const savedOrbit = _orbit;
|
|
589
|
+
const savedSessionMgr = _sessionMgr;
|
|
590
|
+
_orbit = null;
|
|
591
|
+
_sessionMgr = null;
|
|
592
|
+
try {
|
|
593
|
+
startContentStream();
|
|
594
|
+
for (const item of replayItems) {
|
|
595
|
+
if (item.kind === 'user') {
|
|
596
|
+
flushContent();
|
|
597
|
+
stopSpinner();
|
|
598
|
+
const content = String(item.message.content || '').replace(/\s+/g, ' ').trim();
|
|
599
|
+
process.stderr.write(`\n ${historyRoleLabel('user')}: ${content}\n`);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
renderEvent(item.event.event);
|
|
603
|
+
}
|
|
604
|
+
flushContent();
|
|
605
|
+
stopSpinner();
|
|
606
|
+
} finally {
|
|
607
|
+
_orbit = savedOrbit;
|
|
608
|
+
_sessionMgr = savedSessionMgr;
|
|
609
|
+
for (const key of Object.keys(session)) delete session[key];
|
|
610
|
+
Object.assign(session, sessionSnapshot);
|
|
611
|
+
}
|
|
612
|
+
process.stderr.write('\n');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (resumed.history?.length) {
|
|
617
|
+
renderHistoryEntries(resumed.history, {
|
|
618
|
+
limit: Infinity,
|
|
619
|
+
maxChars: 220,
|
|
620
|
+
title: tailTurns ? `Last ${tailTurns} Turns` : 'Replayed Session History',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function summarizeResumeTranscript({
|
|
626
|
+
auth,
|
|
627
|
+
toolExecutor,
|
|
628
|
+
sessionId,
|
|
629
|
+
projectPath,
|
|
630
|
+
messages,
|
|
631
|
+
}) {
|
|
632
|
+
const creds = auth?.loadCredentials?.() || {};
|
|
633
|
+
if (!creds.backendUrl || !creds.token || !Array.isArray(messages) || messages.length === 0) {
|
|
634
|
+
return {
|
|
635
|
+
ok: false,
|
|
636
|
+
source: 'local',
|
|
637
|
+
reason: !creds.backendUrl || !creds.token ? 'missing backend credentials' : 'empty transcript',
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
const client = new TarangStreamClient({
|
|
642
|
+
baseUrl: creds.backendUrl,
|
|
643
|
+
token: creds.token,
|
|
644
|
+
toolExecutor,
|
|
645
|
+
});
|
|
646
|
+
const result = await client.summarizeSession(messages, {
|
|
647
|
+
sessionId,
|
|
648
|
+
projectPath,
|
|
649
|
+
maxTokens: 800,
|
|
650
|
+
timeoutMs: 15000,
|
|
651
|
+
});
|
|
652
|
+
return { ...result, ok: true };
|
|
653
|
+
} catch (err) {
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
source: 'local',
|
|
657
|
+
reason: err?.message || 'backend summary request failed',
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function resumeTailTurnCount(mode = '') {
|
|
663
|
+
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
664
|
+
if (!match) return null;
|
|
665
|
+
return Math.max(1, Number(match[1]) || 1);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function replayStartOrderForMode(history = [], mode = '') {
|
|
669
|
+
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
670
|
+
if (!match) return null;
|
|
671
|
+
const wanted = Math.max(1, Number(match[1]) || 1);
|
|
672
|
+
let seen = 0;
|
|
673
|
+
const userTurns = history.filter(m => m.role === 'user' && typeof m.content === 'string');
|
|
674
|
+
for (let i = userTurns.length - 1; i >= 0; i--) {
|
|
675
|
+
seen++;
|
|
676
|
+
if (seen >= wanted) {
|
|
677
|
+
const order = Number(userTurns[i].order);
|
|
678
|
+
return Number.isFinite(order) ? order : null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function filterResumeReplayEvents(events = []) {
|
|
685
|
+
return events.filter(item => {
|
|
686
|
+
const type = item?.event?.type;
|
|
687
|
+
return !['status', 'session_info', 'complete', 'resumed', 'paused'].includes(type);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function mergeResumeReplayItems(userTurns = [], replayEvents = []) {
|
|
692
|
+
const items = [];
|
|
693
|
+
let order = 0;
|
|
694
|
+
for (const message of userTurns) {
|
|
695
|
+
items.push({
|
|
696
|
+
kind: 'user',
|
|
697
|
+
message,
|
|
698
|
+
fileOrder: Number.isFinite(Number(message.order)) ? Number(message.order) : null,
|
|
699
|
+
order: order++,
|
|
700
|
+
time: Date.parse(message.timestamp || '') || 0,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
for (const event of replayEvents) {
|
|
704
|
+
items.push({
|
|
705
|
+
kind: 'event',
|
|
706
|
+
event,
|
|
707
|
+
fileOrder: Number.isFinite(Number(event.order)) ? Number(event.order) : null,
|
|
708
|
+
order: order++,
|
|
709
|
+
time: Date.parse(event.timestamp || '') || 0,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return items.sort((a, b) => {
|
|
713
|
+
if (a.fileOrder !== null && b.fileOrder !== null && a.fileOrder !== b.fileOrder) {
|
|
714
|
+
return a.fileOrder - b.fileOrder;
|
|
715
|
+
}
|
|
716
|
+
if (a.fileOrder !== null && b.fileOrder === null) return -1;
|
|
717
|
+
if (a.fileOrder === null && b.fileOrder !== null) return 1;
|
|
718
|
+
const at = a.time || Number.MAX_SAFE_INTEGER;
|
|
719
|
+
const bt = b.time || Number.MAX_SAFE_INTEGER;
|
|
720
|
+
return at - bt || a.order - b.order;
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function resumeProgressBar(percent, width = 12) {
|
|
725
|
+
const p = Math.max(0, Math.min(100, Math.round(percent)));
|
|
726
|
+
const filled = Math.round((p / 100) * width);
|
|
727
|
+
return `${c.brand('█'.repeat(filled))}${c.gray('░'.repeat(width - filled))} ${String(p).padStart(3)}%`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function startResumeProgress(mode = 'full') {
|
|
731
|
+
let percent = 8;
|
|
732
|
+
let label = `resuming as ${resumeModeLabel(mode)}`;
|
|
733
|
+
let active = true;
|
|
734
|
+
const started = Date.now();
|
|
735
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
736
|
+
let frame = 0;
|
|
737
|
+
|
|
738
|
+
const render = () => {
|
|
739
|
+
if (!active) return;
|
|
740
|
+
const glyph = frames[frame % frames.length];
|
|
741
|
+
frame++;
|
|
742
|
+
inPlace(` ${c.brand(glyph)} ${c.dim(label)} ${resumeProgressBar(percent)} ${c.dim(formatElapsed(started))}`);
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
render();
|
|
746
|
+
const timer = setInterval(render, 100);
|
|
747
|
+
return {
|
|
748
|
+
update(nextLabel, nextPercent) {
|
|
749
|
+
if (!active) return;
|
|
750
|
+
if (nextLabel) label = nextLabel;
|
|
751
|
+
if (Number.isFinite(nextPercent)) percent = Math.max(percent, Math.min(98, nextPercent));
|
|
752
|
+
render();
|
|
753
|
+
},
|
|
754
|
+
stop() {
|
|
755
|
+
if (!active) return;
|
|
756
|
+
active = false;
|
|
757
|
+
clearInterval(timer);
|
|
758
|
+
inPlace('');
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
92
763
|
// ── Session State ──
|
|
93
764
|
|
|
94
765
|
let _sessionMgr = null; // Set in startTerminalRepl, used by renderEvent
|
|
@@ -102,7 +773,8 @@ const session = {
|
|
|
102
773
|
toolCalls: 0,
|
|
103
774
|
totalToolCalls: 0, // across all turns
|
|
104
775
|
turns: 0,
|
|
105
|
-
history: [], //
|
|
776
|
+
history: [], // display transcript (can include reconstructed tool entries)
|
|
777
|
+
agentHistory: [], // backend continuity payload (compact or full)
|
|
106
778
|
inputHistory: [], // previous prompts (for Up/Down)
|
|
107
779
|
user: null, // { github_username, email, role }
|
|
108
780
|
model: null, // from backend user profile
|
|
@@ -111,6 +783,7 @@ const session = {
|
|
|
111
783
|
phases: [], // phase history: { name, time }
|
|
112
784
|
inSubAgent: false, // true while a sub-agent is running (for indented tool display)
|
|
113
785
|
filesChanged: [], // files modified this session
|
|
786
|
+
filesRead: [], // files read this turn
|
|
114
787
|
lastTurnDuration: 0,
|
|
115
788
|
toolCounts: {}, // per-tool histogram (mission report)
|
|
116
789
|
subAgentCounts: {}, // per-sub-agent histogram (mission report)
|
|
@@ -132,6 +805,8 @@ const session = {
|
|
|
132
805
|
creditsLimit: null, // per-period included credits limit
|
|
133
806
|
creditsCharged: 0, // session-cumulative server-reported charges
|
|
134
807
|
creditsLowWarned: false, // emit the low-balance hint only once per turn
|
|
808
|
+
rateLimit: null, // rolling message-window state from backend
|
|
809
|
+
msgsLowWarned: false, // emit the low-window hint only once per turn
|
|
135
810
|
};
|
|
136
811
|
|
|
137
812
|
// ── Commands ──
|
|
@@ -141,12 +816,15 @@ const COMMANDS = {
|
|
|
141
816
|
'/login': 'Sign in via browser',
|
|
142
817
|
'/whoami': 'Show logged-in user',
|
|
143
818
|
'/status': 'Session status & system info',
|
|
819
|
+
'/plan': 'Show plan/tasks',
|
|
820
|
+
'/tasks': 'Show or update project tasks',
|
|
144
821
|
'/stats': 'Progress bars & metrics',
|
|
145
822
|
'/clear': 'Clear conversation',
|
|
146
823
|
'/git': 'Git status',
|
|
147
824
|
'/diff': 'Git diff',
|
|
148
825
|
'/cost': 'Show session cost',
|
|
149
826
|
'/history': 'Show conversation',
|
|
827
|
+
'/settings': 'Show policy/settings',
|
|
150
828
|
'/last': 'Expand last tool output',
|
|
151
829
|
'/expand': 'Expand tool output by index (or "all")',
|
|
152
830
|
'/fold': 'Hide previously expanded tool output',
|
|
@@ -173,6 +851,240 @@ const COMMANDS = {
|
|
|
173
851
|
'/exit': 'Exit CLI',
|
|
174
852
|
};
|
|
175
853
|
|
|
854
|
+
const HELP_GROUPS = [
|
|
855
|
+
{
|
|
856
|
+
key: 'plan',
|
|
857
|
+
title: 'Plan',
|
|
858
|
+
summary: 'plan and project tasks',
|
|
859
|
+
commands: [
|
|
860
|
+
['/plan', 'Plan and task overview'],
|
|
861
|
+
['/plan status', 'Plan owner and task files'],
|
|
862
|
+
['/plan edit', 'Show editable task/plan paths'],
|
|
863
|
+
['/tasks', 'List project tasks'],
|
|
864
|
+
['/tasks add <text>', 'Add backlog task'],
|
|
865
|
+
['/tasks active|blocked|done <text>', 'Append to a task list'],
|
|
866
|
+
],
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
key: 'status',
|
|
870
|
+
title: 'Status',
|
|
871
|
+
summary: 'session, usage, budget',
|
|
872
|
+
commands: [
|
|
873
|
+
['/status', 'Session snapshot'],
|
|
874
|
+
['/status context', 'Loaded .kepler context'],
|
|
875
|
+
['/status metrics', 'Progress bars and runtime metrics'],
|
|
876
|
+
['/status cost', 'Credits and message window'],
|
|
877
|
+
['/status budget <amount|clear>', 'Set or clear session budget'],
|
|
878
|
+
],
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
key: 'history',
|
|
882
|
+
title: 'History',
|
|
883
|
+
summary: 'transcript, reports, undo',
|
|
884
|
+
commands: [
|
|
885
|
+
['/history', 'Recent transcript'],
|
|
886
|
+
['/history approvals', 'Approval log'],
|
|
887
|
+
['/history last', 'Expand last tool output'],
|
|
888
|
+
['/history expand [n|all]', 'Expand tool output'],
|
|
889
|
+
['/history checkpoint', 'List checkpoints'],
|
|
890
|
+
['/history undo', 'Restore latest checkpoint'],
|
|
891
|
+
['/history report', 'Save mission report'],
|
|
892
|
+
],
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
key: 'settings',
|
|
896
|
+
title: 'Settings',
|
|
897
|
+
summary: 'auth, policy, verbosity',
|
|
898
|
+
commands: [
|
|
899
|
+
['/settings policy', 'Effective project policy'],
|
|
900
|
+
['/settings login', 'Sign in'],
|
|
901
|
+
['/settings logout', 'Sign out'],
|
|
902
|
+
['/settings whoami', 'Current user'],
|
|
903
|
+
['/settings quiet|verbose|surgical', 'Verbosity'],
|
|
904
|
+
['/settings revoke', 'Revoke auto-approvals'],
|
|
905
|
+
],
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
key: 'worktree',
|
|
909
|
+
title: 'Worktree',
|
|
910
|
+
summary: 'git and files',
|
|
911
|
+
commands: [
|
|
912
|
+
['/git', 'Git status'],
|
|
913
|
+
['/diff', 'Git diff'],
|
|
914
|
+
['/map', 'Registered project tree'],
|
|
915
|
+
['/preflight', 'Onboarding diagnostic'],
|
|
916
|
+
['/safety', 'Safety guardrail status'],
|
|
917
|
+
],
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
key: 'agents',
|
|
921
|
+
title: 'Agents',
|
|
922
|
+
summary: 'specialist modes',
|
|
923
|
+
commands: [
|
|
924
|
+
['/agents', 'List built-in agents'],
|
|
925
|
+
['/explore <instruction>', 'Explore code'],
|
|
926
|
+
['/review <instruction>', 'Review code'],
|
|
927
|
+
['/architect <instruction>', 'Design an approach'],
|
|
928
|
+
],
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
key: 'session',
|
|
932
|
+
title: 'Session',
|
|
933
|
+
summary: 'resume and clear',
|
|
934
|
+
commands: [
|
|
935
|
+
['/sessions', 'List resumable sessions'],
|
|
936
|
+
['/resume [id]', 'Resume a session'],
|
|
937
|
+
['/compact', 'Compact conversation context'],
|
|
938
|
+
['/clear', 'Clear conversation'],
|
|
939
|
+
['/exit', 'Exit CLI'],
|
|
940
|
+
],
|
|
941
|
+
},
|
|
942
|
+
];
|
|
943
|
+
|
|
944
|
+
const HELP_GROUP_ALIASES = new Map(
|
|
945
|
+
HELP_GROUPS.flatMap(group => [[group.key, group], [group.title.toLowerCase(), group]])
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
const LEGACY_COMMAND_HINTS = {
|
|
949
|
+
'/stats': '/status metrics',
|
|
950
|
+
'/cost': '/status cost',
|
|
951
|
+
'/budget': '/status budget',
|
|
952
|
+
'/last': '/history last',
|
|
953
|
+
'/expand': '/history expand',
|
|
954
|
+
'/fold': '/history fold',
|
|
955
|
+
'/undo': '/history undo',
|
|
956
|
+
'/checkpoint': '/history checkpoint',
|
|
957
|
+
'/report': '/history report',
|
|
958
|
+
'/login': '/settings login',
|
|
959
|
+
'/logout': '/settings logout',
|
|
960
|
+
'/whoami': '/settings whoami',
|
|
961
|
+
'/quiet': '/settings quiet',
|
|
962
|
+
'/verbose': '/settings verbose',
|
|
963
|
+
'/surgical': '/settings surgical',
|
|
964
|
+
'/revoke': '/settings revoke',
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
const NAMESPACED_COMMANDS = {
|
|
968
|
+
'/status': {
|
|
969
|
+
metrics: '/stats',
|
|
970
|
+
stats: '/stats',
|
|
971
|
+
cost: '/cost',
|
|
972
|
+
credits: '/cost',
|
|
973
|
+
budget: '/budget',
|
|
974
|
+
},
|
|
975
|
+
'/history': {
|
|
976
|
+
last: '/last',
|
|
977
|
+
expand: '/expand',
|
|
978
|
+
fold: '/fold',
|
|
979
|
+
undo: '/undo',
|
|
980
|
+
checkpoint: '/checkpoint',
|
|
981
|
+
checkpoints: '/checkpoint',
|
|
982
|
+
report: '/report',
|
|
983
|
+
},
|
|
984
|
+
'/settings': {
|
|
985
|
+
login: '/login',
|
|
986
|
+
logout: '/logout',
|
|
987
|
+
whoami: '/whoami',
|
|
988
|
+
quiet: '/quiet',
|
|
989
|
+
verbose: '/verbose',
|
|
990
|
+
surgical: '/surgical',
|
|
991
|
+
revoke: '/revoke',
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
function normalizeCommandInput(input) {
|
|
996
|
+
const parts = input.trim().split(/\s+/).filter(Boolean);
|
|
997
|
+
const rawCmd = (parts[0] || '').toLowerCase();
|
|
998
|
+
const restParts = parts.slice(1);
|
|
999
|
+
const sub = (restParts[0] || '').toLowerCase();
|
|
1000
|
+
const namespaced = NAMESPACED_COMMANDS[rawCmd]?.[sub];
|
|
1001
|
+
if (namespaced) {
|
|
1002
|
+
return {
|
|
1003
|
+
cmd: namespaced,
|
|
1004
|
+
rest: restParts.slice(1).join(' '),
|
|
1005
|
+
rawCmd,
|
|
1006
|
+
aliasTarget: null,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
cmd: rawCmd,
|
|
1011
|
+
rest: restParts.join(' '),
|
|
1012
|
+
rawCmd,
|
|
1013
|
+
aliasTarget: LEGACY_COMMAND_HINTS[rawCmd] || null,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function renderHelp(topic = '') {
|
|
1018
|
+
const key = String(topic || '').trim().toLowerCase();
|
|
1019
|
+
if (!key) {
|
|
1020
|
+
process.stderr.write(`\n ${c.bold('Kepler Commands')}\n`);
|
|
1021
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
1022
|
+
const top = [
|
|
1023
|
+
['/help', 'Grouped command help'],
|
|
1024
|
+
['/status', 'Session snapshot'],
|
|
1025
|
+
['/plan', 'Task list and plan'],
|
|
1026
|
+
['/tasks', 'Project task files'],
|
|
1027
|
+
['/history', 'Transcript, approvals, undo'],
|
|
1028
|
+
['/settings', 'Policy, auth, verbosity'],
|
|
1029
|
+
['/why', 'Explain last reasoning'],
|
|
1030
|
+
];
|
|
1031
|
+
for (const [name, desc] of top) {
|
|
1032
|
+
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}\n`);
|
|
1033
|
+
}
|
|
1034
|
+
process.stderr.write(`\n ${c.bold('Categories')}\n`);
|
|
1035
|
+
for (const group of HELP_GROUPS) {
|
|
1036
|
+
process.stderr.write(` ${c.brand(('/help ' + group.key).padEnd(20))} ${c.dim(group.summary)}\n`);
|
|
1037
|
+
}
|
|
1038
|
+
process.stderr.write(`\n ${c.dim('Use /help all for legacy command aliases.')}\n`);
|
|
1039
|
+
renderKeyboardHelp();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (key === 'all' || key === 'commands') {
|
|
1044
|
+
process.stderr.write(`\n ${c.bold('All Commands')}\n`);
|
|
1045
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
1046
|
+
for (const [name, desc] of Object.entries(COMMANDS)) {
|
|
1047
|
+
const alias = LEGACY_COMMAND_HINTS[name] ? c.dim(` alias for ${LEGACY_COMMAND_HINTS[name]}`) : '';
|
|
1048
|
+
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}${alias}\n`);
|
|
1049
|
+
}
|
|
1050
|
+
process.stderr.write('\n');
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const group = HELP_GROUP_ALIASES.get(key);
|
|
1055
|
+
if (!group) {
|
|
1056
|
+
process.stderr.write(` ${c.gray(`Unknown help category: ${key}. Use /help.`)}\n`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
process.stderr.write(`\n ${c.bold(group.title)} ${c.dim(group.summary)}\n`);
|
|
1061
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
1062
|
+
for (const [name, desc] of group.commands) {
|
|
1063
|
+
process.stderr.write(` ${c.brand(name.padEnd(30))} ${desc}\n`);
|
|
1064
|
+
}
|
|
1065
|
+
process.stderr.write('\n');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function renderKeyboardHelp() {
|
|
1069
|
+
process.stderr.write(`\n ${c.bold('Keyboard')}\n`);
|
|
1070
|
+
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n`);
|
|
1071
|
+
process.stderr.write(` ${c.gray('d')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function commandCompletions(line) {
|
|
1075
|
+
if (line.startsWith('/help ')) {
|
|
1076
|
+
const topic = line.slice('/help '.length).toLowerCase();
|
|
1077
|
+
const categories = ['all', ...HELP_GROUPS.map(g => g.key)];
|
|
1078
|
+
const hits = categories.map(c => `/help ${c}`).filter(cmd => cmd.startsWith(`/help ${topic}`));
|
|
1079
|
+
return hits.length ? hits : categories.map(c => `/help ${c}`);
|
|
1080
|
+
}
|
|
1081
|
+
const top = ['/help', '/status', '/plan', '/tasks', '/history', '/settings', '/why'];
|
|
1082
|
+
const namespaced = HELP_GROUPS.flatMap(g => g.commands.map(([name]) => name.split(/\s+/)[0]));
|
|
1083
|
+
const all = [...new Set([...top, ...namespaced, ...Object.keys(COMMANDS), '/quit'])].sort();
|
|
1084
|
+
const hits = all.filter(cmd => cmd.startsWith(line));
|
|
1085
|
+
return hits.length ? hits : all;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
176
1088
|
// ── Banner ──
|
|
177
1089
|
|
|
178
1090
|
function printBanner(auth) {
|
|
@@ -210,19 +1122,26 @@ function printBanner(auth) {
|
|
|
210
1122
|
* Left side: last-turn summary (tools, time, cost)
|
|
211
1123
|
* Right side: session totals (ctx%, tokens)
|
|
212
1124
|
*/
|
|
1125
|
+
function computeCacheTotals() {
|
|
1126
|
+
let read = 0;
|
|
1127
|
+
let write = 0;
|
|
1128
|
+
for (const b of session.costBreakdown) {
|
|
1129
|
+
read += b.cache_read_tokens || 0;
|
|
1130
|
+
write += b.cache_creation_tokens || 0;
|
|
1131
|
+
}
|
|
1132
|
+
const denom = session.inputTokens + read;
|
|
1133
|
+
const hitRate = denom > 0 ? Math.round((read / denom) * 100) : 0;
|
|
1134
|
+
return { read, write, hitRate };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
213
1137
|
function buildContextStrip() {
|
|
214
1138
|
const totalTokens = session.inputTokens + session.outputTokens;
|
|
215
1139
|
const elapsed = formatElapsed(session.startTime);
|
|
1140
|
+
const cache = computeCacheTotals();
|
|
216
1141
|
|
|
217
|
-
// BYOK: user pays the provider directly, suppress credits entirely.
|
|
218
|
-
// Otherwise prefer the server-authoritative session counter, falling back
|
|
219
|
-
// to the local estimate when the backend hasn't pushed any number yet.
|
|
220
|
-
const usedCr = session.creditsCharged > 0
|
|
221
|
-
? session.creditsCharged
|
|
222
|
-
: costToCredits(session.totalCost);
|
|
223
1142
|
const right = [
|
|
224
1143
|
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
225
|
-
...(
|
|
1144
|
+
...(cache.read > 0 ? [c.dim(`cache ${cache.hitRate}%`)] : []),
|
|
226
1145
|
c.dim(elapsed),
|
|
227
1146
|
].join(c.dim(' · '));
|
|
228
1147
|
|
|
@@ -269,12 +1188,21 @@ function printTurnSummary(toolCount, durationS, turnCost) {
|
|
|
269
1188
|
const parts = [];
|
|
270
1189
|
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
271
1190
|
if (durationS) parts.push(`${Number(durationS).toFixed(1)}s`);
|
|
272
|
-
if (turnCost > 0 && !session.isByok) parts.push(formatCredits(costToCredits(turnCost)));
|
|
273
1191
|
if (parts.length > 0) {
|
|
274
1192
|
process.stderr.write(`\n ${c.green('✓')} ${c.dim(parts.join(' · '))}\n`);
|
|
275
1193
|
}
|
|
276
1194
|
}
|
|
277
1195
|
|
|
1196
|
+
function formatMessageChip(rateLimit) {
|
|
1197
|
+
const remaining = messagesRemaining(rateLimit);
|
|
1198
|
+
if (remaining === Infinity) return 'unlimited messages';
|
|
1199
|
+
const limit = Number(rateLimit?.msgs_per_window);
|
|
1200
|
+
if (typeof remaining === 'number' && Number.isFinite(limit)) {
|
|
1201
|
+
return `${remaining}/${limit} messages`;
|
|
1202
|
+
}
|
|
1203
|
+
return 'messages';
|
|
1204
|
+
}
|
|
1205
|
+
|
|
278
1206
|
function updateStatusBar() {
|
|
279
1207
|
// No-op: status is printed inline via printPromptBlock before each prompt
|
|
280
1208
|
}
|
|
@@ -355,6 +1283,7 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
355
1283
|
|
|
356
1284
|
const tool = data.tool || data._tool || '';
|
|
357
1285
|
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
1286
|
+
recordReadActivity(tool, data.args || {});
|
|
358
1287
|
|
|
359
1288
|
// Update the card buffer so /last and `d` can find it.
|
|
360
1289
|
if (callId) recordCard({ id: callId, tool, args: data.args, result: data, durationMs });
|
|
@@ -379,7 +1308,7 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
379
1308
|
// If the head for this call is still buffered (no interleaving content
|
|
380
1309
|
// landed), and the combined line fits the terminal width, emit ONE line
|
|
381
1310
|
// and skip the gutter entirely.
|
|
382
|
-
if (_pendingHead && _pendingHead.callId === callId && !hasLint) {
|
|
1311
|
+
if (_pendingHead && _pendingHead.callId === callId && !hasLint && !_pendingHead.head.includes('\n')) {
|
|
383
1312
|
const cols = process.stderr.columns || 120;
|
|
384
1313
|
const combined = `${_pendingHead.head} ${outcome}`;
|
|
385
1314
|
if (stripAnsi(combined).length <= cols) {
|
|
@@ -451,6 +1380,31 @@ function shortPath(p) {
|
|
|
451
1380
|
return parts.length > 2 ? parts.slice(-2).join('/') : p;
|
|
452
1381
|
}
|
|
453
1382
|
|
|
1383
|
+
function rememberReadFile(filePath) {
|
|
1384
|
+
const file = shortPath(String(filePath || '').trim());
|
|
1385
|
+
if (file && !session.filesRead.includes(file)) session.filesRead.push(file);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function recordReadActivity(tool, args = {}) {
|
|
1389
|
+
const normalized = String(tool || '').toLowerCase();
|
|
1390
|
+
if (normalized === 'read_file' || normalized === 'read') {
|
|
1391
|
+
rememberReadFile(args.file_path || args.path);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (normalized === 'read_files') {
|
|
1395
|
+
const files = args.file_paths || args.paths || args.files || [];
|
|
1396
|
+
for (const file of Array.isArray(files) ? files : []) {
|
|
1397
|
+
rememberReadFile(typeof file === 'string' ? file : file?.file_path || file?.path);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function thinkingKind(text) {
|
|
1403
|
+
return /\b(read|reading|inspect|scan|search|open|trace|look(?:ing)?\s+at)\b/i.test(text)
|
|
1404
|
+
? 'Reading'
|
|
1405
|
+
: 'Thinking';
|
|
1406
|
+
}
|
|
1407
|
+
|
|
454
1408
|
// ── Live Spinner ──
|
|
455
1409
|
// A real animated spinner that ticks on an interval, not just per-call.
|
|
456
1410
|
// Shows what's happening right now — thinking, tool executing, etc.
|
|
@@ -553,7 +1507,7 @@ function renderEvent(event) {
|
|
|
553
1507
|
if (text.length > 12 && text !== session._lastEmittedThinking) {
|
|
554
1508
|
flushPendingHead();
|
|
555
1509
|
stopSpinner();
|
|
556
|
-
process.stderr.write(` ${c.italic(c.dim(text.slice(0, 200)))}\n`);
|
|
1510
|
+
process.stderr.write(` ${c.dim(thinkingKind(text) + ' · ')}${c.italic(c.dim(text.slice(0, 200)))}\n`);
|
|
557
1511
|
session._lastEmittedThinking = text;
|
|
558
1512
|
}
|
|
559
1513
|
startSpinner(text.slice(0, 80));
|
|
@@ -612,8 +1566,17 @@ function renderEvent(event) {
|
|
|
612
1566
|
}
|
|
613
1567
|
|
|
614
1568
|
case 'approval_granted': {
|
|
615
|
-
//
|
|
616
|
-
//
|
|
1569
|
+
// Human approvals are rendered by approval.mjs. Auto-read grants are
|
|
1570
|
+
// otherwise invisible, so show one dim confirmation before the tool card.
|
|
1571
|
+
const scope = data?.grant_scope || data?.scope || '';
|
|
1572
|
+
if (scope === 'auto_read') {
|
|
1573
|
+
const toolName = data?.tool || data?.tool_name || '';
|
|
1574
|
+
const args = data?.args || data?.input || {};
|
|
1575
|
+
const summary = toolDisplaySummary(toolName, args);
|
|
1576
|
+
const label = toolDisplayLabel(toolName);
|
|
1577
|
+
const subject = summary ? `${label} ${summary}` : label;
|
|
1578
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`${subject} · auto-approved read`)}\n`);
|
|
1579
|
+
}
|
|
617
1580
|
break;
|
|
618
1581
|
}
|
|
619
1582
|
|
|
@@ -790,6 +1753,7 @@ function renderEvent(event) {
|
|
|
790
1753
|
if (typeof bal.included === 'number') session.creditsIncluded = bal.included;
|
|
791
1754
|
if (typeof bal.purchased === 'number') session.creditsPurchased = bal.purchased;
|
|
792
1755
|
}
|
|
1756
|
+
if (data?.rate_limit) session.rateLimit = data.rate_limit;
|
|
793
1757
|
break;
|
|
794
1758
|
}
|
|
795
1759
|
|
|
@@ -848,12 +1812,24 @@ function renderEvent(event) {
|
|
|
848
1812
|
}
|
|
849
1813
|
|
|
850
1814
|
session.lastTurnDuration = data?.duration_s || 0;
|
|
1815
|
+
if (data?.rate_limit) session.rateLimit = data.rate_limit;
|
|
851
1816
|
|
|
852
1817
|
// ── Server-authoritative credits ──
|
|
853
1818
|
// Backend sends usage.credits_charged (this turn) + balance (remaining)
|
|
854
1819
|
// in the complete event. CLI uses these instead of the local
|
|
855
1820
|
// costToCredits estimate so /status and /cost match the dashboard.
|
|
856
1821
|
if (!session.isByok) {
|
|
1822
|
+
const msgStatus = lowWindowStatus(session.rateLimit);
|
|
1823
|
+
if (!session.msgsLowWarned && msgStatus !== 'ok') {
|
|
1824
|
+
const windowLine = formatMessageWindow(session.rateLimit);
|
|
1825
|
+
if (msgStatus === 'exhausted') {
|
|
1826
|
+
process.stderr.write(`\n ${c.red('✗')} ${c.dim(`${windowLine}. Wait for the window to reset or upgrade at codekepler.ai/pricing.`)}\n`);
|
|
1827
|
+
} else {
|
|
1828
|
+
process.stderr.write(`\n ${c.yellow('⚠')} ${c.dim(`${windowLine}. Message window is running low.`)}\n`);
|
|
1829
|
+
}
|
|
1830
|
+
session.msgsLowWarned = true;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
857
1833
|
const charged = data?.usage?.credits_charged;
|
|
858
1834
|
if (typeof charged === 'number') session.creditsCharged += charged;
|
|
859
1835
|
const bal = data?.balance;
|
|
@@ -863,11 +1839,12 @@ function renderEvent(event) {
|
|
|
863
1839
|
if (typeof bal.purchased === 'number') session.creditsPurchased = bal.purchased;
|
|
864
1840
|
}
|
|
865
1841
|
// Warn once per turn when the remaining credits drop below 20% of the
|
|
866
|
-
// tier's included limit (or below 10 absolute for tiny tiers).
|
|
1842
|
+
// tier's included limit (or below 10 absolute for tiny tiers). Credits
|
|
1843
|
+
// stay out of the always-on prompt strip; this warning is the exception.
|
|
867
1844
|
if (!session.creditsLowWarned && typeof session.creditsTotal === 'number' && session.creditsLimit) {
|
|
868
1845
|
const threshold = Math.max(10, Math.floor(session.creditsLimit * 0.2));
|
|
869
1846
|
if (session.creditsTotal <= threshold && session.creditsTotal > 0) {
|
|
870
|
-
process.stderr.write(`\n ${c.yellow('⚠')} ${c.dim(`${session.creditsTotal} of ${session.creditsLimit} credits remaining on the ${session.subscriptionTier || 'free'} plan. Upgrade at codekepler.ai/pricing.`)}\n`);
|
|
1847
|
+
process.stderr.write(`\n ${c.yellow('⚠')} ${c.dim(`${session.creditsTotal} of ${session.creditsLimit} credits remaining on the ${session.subscriptionTier || 'free'} plan. Upgrade or top up at codekepler.ai/pricing.`)}\n`);
|
|
871
1848
|
session.creditsLowWarned = true;
|
|
872
1849
|
} else if (session.creditsTotal <= 0) {
|
|
873
1850
|
process.stderr.write(`\n ${c.red('✗')} ${c.dim(`Credit balance exhausted on the ${session.subscriptionTier || 'free'} plan. Purchase credits at codekepler.ai/pricing or switch to BYOK.`)}\n`);
|
|
@@ -892,10 +1869,10 @@ function renderEvent(event) {
|
|
|
892
1869
|
task: session.lastTask,
|
|
893
1870
|
success: successOverall,
|
|
894
1871
|
filesChanged: session.filesChanged,
|
|
1872
|
+
filesRead: session.filesRead,
|
|
895
1873
|
toolCounts: session.toolCounts,
|
|
896
|
-
subAgents: { ...session.subAgentCounts, savedUsd:
|
|
897
|
-
|
|
898
|
-
costUsd: session.isByok ? null : (turnCost || session.totalCost),
|
|
1874
|
+
subAgents: { ...session.subAgentCounts, savedUsd: 0 },
|
|
1875
|
+
costUsd: null,
|
|
899
1876
|
durationS: data?.duration_s,
|
|
900
1877
|
testsPass: data?.tests_passed != null
|
|
901
1878
|
? { passed: data.tests_passed, total: data.tests_total || data.tests_passed }
|
|
@@ -904,6 +1881,7 @@ function renderEvent(event) {
|
|
|
904
1881
|
nextActions: successOverall
|
|
905
1882
|
? ['/commit', '/pr', '/undo', '/report']
|
|
906
1883
|
: ['/why', '/undo', '/re-plan'],
|
|
1884
|
+
cwd: safeCwd(),
|
|
907
1885
|
});
|
|
908
1886
|
process.stderr.write(report + '\n');
|
|
909
1887
|
} else {
|
|
@@ -935,22 +1913,248 @@ function renderEvent(event) {
|
|
|
935
1913
|
|
|
936
1914
|
// ── Slash Commands ──
|
|
937
1915
|
|
|
1916
|
+
function taskListLabel(list) {
|
|
1917
|
+
return {
|
|
1918
|
+
active: 'Active',
|
|
1919
|
+
backlog: 'Backlog',
|
|
1920
|
+
blocked: 'Blocked',
|
|
1921
|
+
done: 'Done',
|
|
1922
|
+
}[list] || list;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function firstMeaningfulLines(content, limit = 6) {
|
|
1926
|
+
return String(content || '')
|
|
1927
|
+
.split(/\r?\n/)
|
|
1928
|
+
.map(line => line.trim())
|
|
1929
|
+
.filter(line => line && !line.startsWith('#'))
|
|
1930
|
+
.slice(0, limit);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function renderTaskBoard(board, { showDone = false } = {}) {
|
|
1934
|
+
const order = showDone
|
|
1935
|
+
? ['active', 'blocked', 'backlog', 'done']
|
|
1936
|
+
: ['active', 'blocked', 'backlog'];
|
|
1937
|
+
let any = false;
|
|
1938
|
+
for (const list of order) {
|
|
1939
|
+
const tasks = board.lists[list]?.tasks || [];
|
|
1940
|
+
if (!tasks.length) continue;
|
|
1941
|
+
any = true;
|
|
1942
|
+
process.stderr.write(`\n ${c.bold(taskListLabel(list))} ${c.dim(board.lists[list].fileName)}\n`);
|
|
1943
|
+
for (const task of tasks.slice(0, 12)) {
|
|
1944
|
+
const marker = task.checked ? c.green('[x]') : c.dim('[ ]');
|
|
1945
|
+
const section = task.section && task.section !== taskListLabel(list) ? c.dim(` · ${task.section}`) : '';
|
|
1946
|
+
process.stderr.write(` ${marker} ${task.text}${section}\n`);
|
|
1947
|
+
}
|
|
1948
|
+
if (tasks.length > 12) {
|
|
1949
|
+
process.stderr.write(` ${c.dim(`+${tasks.length - 12} more`)}\n`);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (!any) {
|
|
1953
|
+
process.stderr.write(` ${c.dim('No project tasks yet. Add one with /tasks add <text>.')}\n`);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function renderPlanOverview({ ctx, mode = 'overview' } = {}) {
|
|
1958
|
+
const cwd = safeCwd();
|
|
1959
|
+
ensureTaskFiles({ cwd });
|
|
1960
|
+
const board = loadTaskBoard({ cwd });
|
|
1961
|
+
const counts = taskCounts(board);
|
|
1962
|
+
const planLines = firstMeaningfulLines(board.plan.content, 8);
|
|
1963
|
+
const goalLines = firstMeaningfulLines(board.goal.content, 3);
|
|
1964
|
+
const effective = ctx.effectivePolicy || loadEffectivePolicy({ cwd });
|
|
1965
|
+
const owner = effective.policy?.planning?.owner || 'auto';
|
|
1966
|
+
|
|
1967
|
+
process.stderr.write(`\n ${c.bold('Plan')}\n`);
|
|
1968
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1969
|
+
process.stderr.write(` ${c.dim('Owner')} ${c.brand(owner)}\n`);
|
|
1970
|
+
process.stderr.write(` ${c.dim('Tasks')} ${counts.active} active, ${counts.blocked} blocked, ${counts.backlog} backlog, ${counts.done} done\n`);
|
|
1971
|
+
if (mode === 'status') {
|
|
1972
|
+
process.stderr.write(` ${c.dim('Plan file')} ${board.plan.exists ? board.plan.path : c.dim('(none)')}\n`);
|
|
1973
|
+
process.stderr.write(` ${c.dim('Tasks dir')} ${board.dir}\n`);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (goalLines.length) {
|
|
1977
|
+
process.stderr.write(`\n ${c.bold('Goal')}\n`);
|
|
1978
|
+
for (const line of goalLines) process.stderr.write(` ${line}\n`);
|
|
1979
|
+
}
|
|
1980
|
+
if (planLines.length) {
|
|
1981
|
+
process.stderr.write(`\n ${c.bold('Current Plan')}\n`);
|
|
1982
|
+
for (const line of planLines) process.stderr.write(` ${line}\n`);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
renderTaskBoard(board, { showDone: mode === 'status' });
|
|
1986
|
+
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function refreshTaskContext(ctx) {
|
|
1990
|
+
try {
|
|
1991
|
+
const previous = ctx.latestProjectContext || null;
|
|
1992
|
+
ctx.latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous });
|
|
1993
|
+
ctx.latestEnvelope = null;
|
|
1994
|
+
} catch { /* best effort */ }
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function handleTasksCommand(rest, ctx) {
|
|
1998
|
+
const raw = String(rest || '').trim();
|
|
1999
|
+
ensureTaskFiles({ cwd: safeCwd() });
|
|
2000
|
+
if (!raw || raw === 'list') {
|
|
2001
|
+
const board = loadTaskBoard({ cwd: safeCwd() });
|
|
2002
|
+
process.stderr.write(`\n ${c.bold('Tasks')}\n`);
|
|
2003
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
2004
|
+
renderTaskBoard(board, { showDone: true });
|
|
2005
|
+
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (raw === 'help') {
|
|
2010
|
+
renderHelp('plan');
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
const parts = raw.split(/\s+/);
|
|
2015
|
+
let verb = (parts.shift() || '').toLowerCase();
|
|
2016
|
+
|
|
2017
|
+
if (verb === 'move') {
|
|
2018
|
+
try {
|
|
2019
|
+
const [from, index, to, ...textParts] = parts;
|
|
2020
|
+
const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
|
|
2021
|
+
refreshTaskContext(ctx);
|
|
2022
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
|
|
2023
|
+
} catch (err) {
|
|
2024
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2025
|
+
process.stderr.write(` ${c.gray('Usage: /tasks move <active|backlog|blocked|done> <number> <active|backlog|blocked|done> [new text]')}\n`);
|
|
2026
|
+
}
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (verb === 'edit' || verb === 'rename') {
|
|
2031
|
+
try {
|
|
2032
|
+
const [list, index, ...textParts] = parts;
|
|
2033
|
+
const result = updateTask({ cwd: safeCwd(), list, index, text: textParts.join(' ') });
|
|
2034
|
+
refreshTaskContext(ctx);
|
|
2035
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`updated ${result.list} #${result.index}`)} ${result.text}\n`);
|
|
2036
|
+
} catch (err) {
|
|
2037
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2038
|
+
process.stderr.write(` ${c.gray('Usage: /tasks edit <active|backlog|blocked|done> <number> <new text>')}\n`);
|
|
2039
|
+
}
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
if (verb === 'remove' || verb === 'rm' || verb === 'delete') {
|
|
2044
|
+
try {
|
|
2045
|
+
const [list, index] = parts;
|
|
2046
|
+
const result = removeTask({ cwd: safeCwd(), list, index });
|
|
2047
|
+
refreshTaskContext(ctx);
|
|
2048
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`removed ${result.list} #${result.index}`)} ${result.task.text}\n`);
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2051
|
+
process.stderr.write(` ${c.gray('Usage: /tasks remove <active|backlog|blocked|done> <number>')}\n`);
|
|
2052
|
+
}
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
if (verb === 'finish' || verb === 'complete' || verb === 'block' || verb === 'unblock') {
|
|
2057
|
+
try {
|
|
2058
|
+
const [from, index, ...textParts] = parts;
|
|
2059
|
+
const to = verb === 'block' ? 'blocked' : verb === 'unblock' ? 'active' : 'done';
|
|
2060
|
+
const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
|
|
2061
|
+
refreshTaskContext(ctx);
|
|
2062
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2065
|
+
process.stderr.write(` ${c.gray(`Usage: /tasks ${verb} <active|backlog|blocked|done> <number> [new text]`)}\n`);
|
|
2066
|
+
}
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
let list = 'backlog';
|
|
2071
|
+
if (verb === 'add' || verb === 'new') {
|
|
2072
|
+
const maybeList = (parts[0] || '').toLowerCase();
|
|
2073
|
+
if (maybeList in TASK_FILES || ['todo', 'pending', 'current', 'doing', 'complete', 'completed'].includes(maybeList)) {
|
|
2074
|
+
list = parts.shift();
|
|
2075
|
+
}
|
|
2076
|
+
} else if (verb in TASK_FILES || ['todo', 'pending', 'current', 'doing', 'complete', 'completed'].includes(verb)) {
|
|
2077
|
+
list = verb;
|
|
2078
|
+
} else {
|
|
2079
|
+
parts.unshift(verb);
|
|
2080
|
+
verb = 'add';
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
try {
|
|
2084
|
+
const result = appendTask({ cwd: safeCwd(), list, text: parts.join(' ') });
|
|
2085
|
+
refreshTaskContext(ctx);
|
|
2086
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`added to ${result.list}`)} ${result.text}\n`);
|
|
2087
|
+
} catch (err) {
|
|
2088
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
938
2092
|
async function handleCommand(input, ctx) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
2093
|
+
const { cmd, rest, aliasTarget } = normalizeCommandInput(input);
|
|
2094
|
+
if (aliasTarget) {
|
|
2095
|
+
process.stderr.write(` ${c.dim(`Legacy alias: use ${aliasTarget}`)}\n`);
|
|
2096
|
+
}
|
|
942
2097
|
|
|
943
2098
|
switch (cmd) {
|
|
944
|
-
case '/help':
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
2099
|
+
case '/help': {
|
|
2100
|
+
renderHelp(rest);
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
case '/plan': {
|
|
2105
|
+
const mode = rest.trim().toLowerCase();
|
|
2106
|
+
if (mode === 'help') {
|
|
2107
|
+
renderHelp('plan');
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
if (mode === 'edit') {
|
|
2111
|
+
ensureTaskFiles({ cwd: safeCwd() });
|
|
2112
|
+
const board = loadTaskBoard({ cwd: safeCwd() });
|
|
2113
|
+
process.stderr.write(`\n ${c.bold('Editable Plan Files')}\n`);
|
|
2114
|
+
process.stderr.write(` ${c.dim('Plan')} ${board.plan.path}\n`);
|
|
2115
|
+
process.stderr.write(` ${c.dim('Active')} ${board.lists.active.path}\n`);
|
|
2116
|
+
process.stderr.write(` ${c.dim('Backlog')} ${board.lists.backlog.path}\n`);
|
|
2117
|
+
process.stderr.write(` ${c.dim('Blocked')} ${board.lists.blocked.path}\n`);
|
|
2118
|
+
process.stderr.write(` ${c.dim('Done')} ${board.lists.done.path}\n\n`);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
renderPlanOverview({ ctx, mode: mode === 'status' ? 'status' : 'overview' });
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
case '/tasks':
|
|
2126
|
+
handleTasksCommand(rest, ctx);
|
|
2127
|
+
return;
|
|
2128
|
+
|
|
2129
|
+
case '/history': {
|
|
2130
|
+
if (rest.trim() === 'fold') {
|
|
2131
|
+
process.stderr.write(` ${c.gray('Output is folded by default — there is nothing to hide. Use /history last or d to expand.')}\n`);
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
if (rest.trim() === 'help') {
|
|
2135
|
+
renderHelp('history');
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
if (rest.trim() === 'approvals') {
|
|
2139
|
+
const entries = ctx.approval?.approvalLog?.readRecent?.(20) || [];
|
|
2140
|
+
if (!entries.length) {
|
|
2141
|
+
process.stderr.write(` ${c.gray('No approval log entries yet.')}\n`);
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
process.stderr.write(`\n ${c.bold('Approval History')}\n`);
|
|
2145
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
2146
|
+
for (const e of entries) {
|
|
2147
|
+
const when = e.ts ? String(e.ts).slice(0, 19).replace('T', ' ') : '';
|
|
2148
|
+
const decision = e.decision?.includes('reject') || e.decision?.includes('deny') ? c.red(e.decision) : c.green(e.decision);
|
|
2149
|
+
process.stderr.write(` ${c.dim(when)} ${decision} ${c.brand(e.tool || '?')} ${c.dim(e.scope || 'once')} ${c.dim(e.args || '')}\n`);
|
|
2150
|
+
}
|
|
2151
|
+
process.stderr.write('\n');
|
|
2152
|
+
return;
|
|
949
2153
|
}
|
|
950
|
-
process.stderr.write(
|
|
951
|
-
|
|
952
|
-
process.stderr.write(` ${c.gray('d')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
|
|
2154
|
+
if (session.history.length === 0) { process.stderr.write(` ${c.gray('No conversation yet.')}\n`); return; }
|
|
2155
|
+
renderHistoryEntries(session.history, { limit: 20, maxChars: 120 });
|
|
953
2156
|
return;
|
|
2157
|
+
}
|
|
954
2158
|
|
|
955
2159
|
case '/login':
|
|
956
2160
|
process.stderr.write(`${c.brand('Starting login flow...')}\n`);
|
|
@@ -977,6 +2181,46 @@ async function handleCommand(input, ctx) {
|
|
|
977
2181
|
}
|
|
978
2182
|
|
|
979
2183
|
case '/status': {
|
|
2184
|
+
if (rest.trim() === 'help') {
|
|
2185
|
+
renderHelp('status');
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
if (rest.trim() === 'context') {
|
|
2189
|
+
const current = ctx.latestProjectContext || loadProjectContext({ cwd: safeCwd() });
|
|
2190
|
+
const envelope = ctx.latestEnvelope || buildContextEnvelope({
|
|
2191
|
+
cwd: safeCwd(),
|
|
2192
|
+
effectivePolicy: ctx.effectivePolicy,
|
|
2193
|
+
projectContext: current,
|
|
2194
|
+
projectResources: ctx.toolExecutor?.getProjectResources?.() || [],
|
|
2195
|
+
agentContext: ctx.toolExecutor?.getAgentContext?.() || {},
|
|
2196
|
+
});
|
|
2197
|
+
process.stderr.write(`\n ${c.bold('Context')}\n`);
|
|
2198
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
2199
|
+
const loaded = current.loaded || [];
|
|
2200
|
+
if (!loaded.length) {
|
|
2201
|
+
process.stderr.write(` ${c.dim('No .kepler context files loaded yet.')}\n`);
|
|
2202
|
+
} else {
|
|
2203
|
+
for (const file of loaded) {
|
|
2204
|
+
const changed = file.changed ? ` ${c.yellow('updated')}` : '';
|
|
2205
|
+
process.stderr.write(` ${c.brand(file.label.padEnd(18))} ${c.dim(file.hash)} ${c.dim(file.path)}${changed}\n`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
process.stderr.write(`\n ${c.bold('Command Context')}\n`);
|
|
2209
|
+
process.stderr.write(` ${c.dim('Active')} ${envelope.command_context.active_command || c.dim('(none)')}\n`);
|
|
2210
|
+
process.stderr.write(` ${c.dim('Source')} ${envelope.command_context.source}\n`);
|
|
2211
|
+
process.stderr.write(` ${c.dim('Timeout')} ${envelope.command_context.runtime_limits.command_timeout_seconds}s command, ${envelope.command_context.runtime_limits.tool_timeout_seconds}s tool\n`);
|
|
2212
|
+
process.stderr.write(` ${c.dim('Plan owner')} ${envelope.effective_options.plan_owner}\n`);
|
|
2213
|
+
process.stderr.write(` ${c.dim('HITL scope')} ${envelope.effective_options.hitl_default_scope} ${c.dim(`(reask ${envelope.effective_options.reask_after_minutes}m)`)}\n`);
|
|
2214
|
+
if (envelope.available_skills.length) {
|
|
2215
|
+
process.stderr.write(`\n ${c.bold('Skills')}\n`);
|
|
2216
|
+
for (const skill of envelope.available_skills.slice(0, 12)) {
|
|
2217
|
+
process.stderr.write(` ${c.brand(skill.name)} ${c.dim(skill.scope)} ${c.dim(skill.description || '')}\n`);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
process.stderr.write('\n');
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
980
2224
|
const creds = ctx.auth.loadCredentials();
|
|
981
2225
|
const env = process.env.TARANG_ENV || 'production';
|
|
982
2226
|
const os = await import('node:os');
|
|
@@ -1003,6 +2247,10 @@ async function handleCommand(input, ctx) {
|
|
|
1003
2247
|
if (session.subscriptionTier) {
|
|
1004
2248
|
process.stderr.write(` ${c.dim('Plan')} ${c.brand(session.subscriptionTier.toUpperCase())}\n`);
|
|
1005
2249
|
}
|
|
2250
|
+
const messageWindow = formatMessageWindow(session.rateLimit);
|
|
2251
|
+
if (messageWindow) {
|
|
2252
|
+
process.stderr.write(` ${c.dim('Messages')} ${messageWindow}\n`);
|
|
2253
|
+
}
|
|
1006
2254
|
if (typeof session.creditsTotal === 'number') {
|
|
1007
2255
|
const limit = session.creditsLimit ? ` ${c.dim('/ ' + formatCredits(session.creditsLimit))}` : '';
|
|
1008
2256
|
const used = session.creditsCharged ? ` ${c.dim(`(${formatCredits(session.creditsCharged)} used this session)`)}` : '';
|
|
@@ -1013,6 +2261,15 @@ async function handleCommand(input, ctx) {
|
|
|
1013
2261
|
}
|
|
1014
2262
|
process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
|
|
1015
2263
|
|
|
2264
|
+
// Cache — PRD-071 §1.2. Only surface when we have data; a fresh session
|
|
2265
|
+
// shows nothing rather than a misleading "0%".
|
|
2266
|
+
const cache = computeCacheTotals();
|
|
2267
|
+
if (cache.read > 0 || cache.write > 0) {
|
|
2268
|
+
const readLabel = formatTokens(cache.read);
|
|
2269
|
+
const writeLabel = formatTokens(cache.write);
|
|
2270
|
+
process.stderr.write(` ${c.dim('Cache')} ${cache.hitRate}% hit ${c.dim('·')} ${readLabel} read ${c.dim('·')} ${writeLabel} write\n`);
|
|
2271
|
+
}
|
|
2272
|
+
|
|
1016
2273
|
// Permissions
|
|
1017
2274
|
process.stderr.write(`\n ${c.bold('Permissions')}\n`);
|
|
1018
2275
|
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
@@ -1023,6 +2280,9 @@ async function handleCommand(input, ctx) {
|
|
|
1023
2280
|
if (approvalSummary.autoApprovedTypes.length > 0) {
|
|
1024
2281
|
process.stderr.write(` ${c.dim('Auto-types')} ${approvalSummary.autoApprovedTypes.join(', ')}\n`);
|
|
1025
2282
|
}
|
|
2283
|
+
if (approvalSummary.trust) {
|
|
2284
|
+
process.stderr.write(` ${c.dim('Trust')} ${approvalSummary.trust.sessionRules} session, ${approvalSummary.trust.projectRules} project rules\n`);
|
|
2285
|
+
}
|
|
1026
2286
|
process.stderr.write(` ${c.dim('Blocked')} ${session.blockedOps} by safety guardrails\n`);
|
|
1027
2287
|
|
|
1028
2288
|
// Orchestration
|
|
@@ -1062,6 +2322,34 @@ async function handleCommand(input, ctx) {
|
|
|
1062
2322
|
return;
|
|
1063
2323
|
}
|
|
1064
2324
|
|
|
2325
|
+
case '/settings': {
|
|
2326
|
+
const sub = rest.trim() || 'policy';
|
|
2327
|
+
if (sub === 'help') {
|
|
2328
|
+
renderHelp('settings');
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if (sub !== 'policy') {
|
|
2332
|
+
process.stderr.write(` ${c.gray('Usage: /settings policy or /help settings')}\n`);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
const effective = loadEffectivePolicy({ cwd: safeCwd() });
|
|
2336
|
+
ctx.effectivePolicy = effective;
|
|
2337
|
+
const rows = formatPolicySourceRows(effective);
|
|
2338
|
+
process.stderr.write(`\n ${c.bold('Effective Policy')}\n`);
|
|
2339
|
+
process.stderr.write(` ${c.dim('─'.repeat(86))}\n`);
|
|
2340
|
+
for (const row of rows.slice(0, 80)) {
|
|
2341
|
+
const value = typeof row.value === 'string' ? row.value : JSON.stringify(row.value);
|
|
2342
|
+
process.stderr.write(` ${c.brand(row.key.padEnd(38))} ${c.dim(row.source.padEnd(8))} ${String(value).slice(0, 34)}\n`);
|
|
2343
|
+
}
|
|
2344
|
+
if (rows.length > 80) process.stderr.write(` ${c.dim(`...and ${rows.length - 80} more`)}\n`);
|
|
2345
|
+
const projectLayer = effective.layers.find(l => l.name === 'project');
|
|
2346
|
+
if (projectLayer?.error) {
|
|
2347
|
+
process.stderr.write(`\n ${c.yellow('Project config error:')} ${projectLayer.error}\n`);
|
|
2348
|
+
}
|
|
2349
|
+
process.stderr.write('\n');
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
1065
2353
|
case '/stats': {
|
|
1066
2354
|
const os = await import('node:os');
|
|
1067
2355
|
const mem = process.memoryUsage();
|
|
@@ -1103,6 +2391,10 @@ async function handleCommand(input, ctx) {
|
|
|
1103
2391
|
const limit = session.creditsLimit ? ` / ${formatCredits(session.creditsLimit)}` : '';
|
|
1104
2392
|
process.stderr.write(` ${c.dim('Plan')} ${c.brand(session.subscriptionTier.toUpperCase())} ${c.dim('· remaining')} ${c.brand(remaining)}${c.dim(limit)}\n`);
|
|
1105
2393
|
}
|
|
2394
|
+
const messageWindow = formatMessageWindow(session.rateLimit);
|
|
2395
|
+
if (messageWindow) {
|
|
2396
|
+
process.stderr.write(` ${c.dim('Messages')} ${messageWindow}\n`);
|
|
2397
|
+
}
|
|
1106
2398
|
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
1107
2399
|
|
|
1108
2400
|
if (session.costBreakdown.length > 0) {
|
|
@@ -1135,21 +2427,10 @@ async function handleCommand(input, ctx) {
|
|
|
1135
2427
|
`${''.padStart(10)}` +
|
|
1136
2428
|
`${formatCredits(costToCredits(session.totalCost)).padStart(10)}\n`
|
|
1137
2429
|
);
|
|
1138
|
-
process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)}
|
|
2430
|
+
process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)}`)}\n\n`);
|
|
1139
2431
|
return;
|
|
1140
2432
|
}
|
|
1141
2433
|
|
|
1142
|
-
case '/history':
|
|
1143
|
-
if (session.history.length === 0) { process.stderr.write(` ${c.gray('No conversation yet.')}\n`); return; }
|
|
1144
|
-
process.stderr.write(`\n ${c.bold('Conversation')} (${session.history.length} messages)\n`);
|
|
1145
|
-
process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
|
|
1146
|
-
for (const msg of session.history.slice(-20)) {
|
|
1147
|
-
const role = msg.role === 'user' ? c.white('You') : c.brand('Kepler');
|
|
1148
|
-
process.stderr.write(` ${role}: ${msg.content.slice(0, 80)}${msg.content.length > 80 ? '...' : ''}\n`);
|
|
1149
|
-
}
|
|
1150
|
-
process.stderr.write('\n');
|
|
1151
|
-
return;
|
|
1152
|
-
|
|
1153
2434
|
case '/last':
|
|
1154
2435
|
expandLast();
|
|
1155
2436
|
return;
|
|
@@ -1207,7 +2488,7 @@ async function handleCommand(input, ctx) {
|
|
|
1207
2488
|
}
|
|
1208
2489
|
|
|
1209
2490
|
case '/report': {
|
|
1210
|
-
if (Object.keys(session.toolCounts).length === 0 && session.filesChanged.length === 0) {
|
|
2491
|
+
if (Object.keys(session.toolCounts).length === 0 && session.filesChanged.length === 0 && session.filesRead.length === 0) {
|
|
1211
2492
|
process.stderr.write(` ${c.gray('Nothing to report yet — run a task first.')}\n`);
|
|
1212
2493
|
return;
|
|
1213
2494
|
}
|
|
@@ -1215,11 +2496,13 @@ async function handleCommand(input, ctx) {
|
|
|
1215
2496
|
task: session.lastTask,
|
|
1216
2497
|
success: true,
|
|
1217
2498
|
filesChanged: session.filesChanged,
|
|
2499
|
+
filesRead: session.filesRead,
|
|
1218
2500
|
toolCounts: session.toolCounts,
|
|
1219
2501
|
subAgents: { ...session.subAgentCounts, savedUsd: session.isByok ? 0 : session.savedUsd },
|
|
1220
2502
|
costUsd: session.isByok ? null : session.totalCost,
|
|
1221
2503
|
durationS: (Date.now() - session.startTime) / 1000,
|
|
1222
2504
|
nextActions: ['/commit', '/pr', '/undo'],
|
|
2505
|
+
cwd: safeCwd(),
|
|
1223
2506
|
};
|
|
1224
2507
|
const out = saveReport(state, { cwd: safeCwd() });
|
|
1225
2508
|
process.stderr.write(` ${c.green('✓')} ${c.dim('Saved')} ${out}\n`);
|
|
@@ -1259,7 +2542,12 @@ async function handleCommand(input, ctx) {
|
|
|
1259
2542
|
|
|
1260
2543
|
case '/budget': {
|
|
1261
2544
|
const arg = rest.trim();
|
|
1262
|
-
if (!arg
|
|
2545
|
+
if (!arg) {
|
|
2546
|
+
const current = session.budgetUsd ? `$${session.budgetUsd.toFixed(2)}` : 'not set';
|
|
2547
|
+
process.stderr.write(` ${c.dim('Budget cap:')} ${c.brand(current)} ${c.dim('· set with /status budget <amount> or clear with /status budget clear')}\n`);
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
if (arg === 'clear' || arg === 'off') {
|
|
1263
2551
|
session.budgetUsd = null;
|
|
1264
2552
|
session.budgetExceeded = false;
|
|
1265
2553
|
process.stderr.write(` ${c.gray('Budget cap cleared.')}\n`);
|
|
@@ -1288,15 +2576,16 @@ async function handleCommand(input, ctx) {
|
|
|
1288
2576
|
}
|
|
1289
2577
|
|
|
1290
2578
|
case '/compact': {
|
|
1291
|
-
const before = session.history.length;
|
|
2579
|
+
const before = session.agentHistory.length || session.history.length;
|
|
1292
2580
|
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
1293
|
-
session.
|
|
1294
|
-
process.stderr.write(` ${c.gray(`Compacted: ${before} → ${session.
|
|
2581
|
+
session.agentHistory.splice(2, session.agentHistory.length - 6);
|
|
2582
|
+
process.stderr.write(` ${c.gray(`Compacted agent context: ${before} → ${session.agentHistory.length} messages`)}\n`);
|
|
1295
2583
|
return;
|
|
1296
2584
|
}
|
|
1297
2585
|
|
|
1298
2586
|
case '/clear':
|
|
1299
2587
|
session.history.length = 0;
|
|
2588
|
+
session.agentHistory.length = 0;
|
|
1300
2589
|
session.toolCalls = 0;
|
|
1301
2590
|
process.stderr.write(` ${c.gray('Conversation cleared.')}\n`);
|
|
1302
2591
|
return;
|
|
@@ -1348,7 +2637,7 @@ async function handleCommand(input, ctx) {
|
|
|
1348
2637
|
}
|
|
1349
2638
|
|
|
1350
2639
|
case '/sessions': {
|
|
1351
|
-
const resumable =
|
|
2640
|
+
const resumable = await listResumableSessions();
|
|
1352
2641
|
if (resumable.length === 0) {
|
|
1353
2642
|
process.stderr.write(` ${c.gray('No resumable sessions found.')}\n`);
|
|
1354
2643
|
return;
|
|
@@ -1356,89 +2645,168 @@ async function handleCommand(input, ctx) {
|
|
|
1356
2645
|
process.stderr.write(`\n ${c.bold('Resumable Sessions')}\n`);
|
|
1357
2646
|
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1358
2647
|
for (const s of resumable) {
|
|
1359
|
-
const date =
|
|
1360
|
-
const instr = s.instruction
|
|
1361
|
-
|
|
2648
|
+
const date = sessionListTimestamp(s);
|
|
2649
|
+
const instr = oneLineInstruction(s.instruction, 72);
|
|
2650
|
+
const project = s.project || path.basename(s.projectPath || '') || '(unknown)';
|
|
2651
|
+
process.stderr.write(` ${c.brand(s.sessionId)} ${c.brand(project)} ${c.dim(date)} ${messageCountLabel(s.messageCount)} ${c.dim(instr)}\n`);
|
|
1362
2652
|
}
|
|
1363
2653
|
process.stderr.write(`\n ${c.dim('Resume with:')} kepler --resume <sessionId>\n`);
|
|
1364
2654
|
return;
|
|
1365
2655
|
}
|
|
1366
2656
|
|
|
1367
2657
|
case '/resume': {
|
|
1368
|
-
|
|
1369
|
-
|
|
2658
|
+
// PRD-068 §5.14: one-prompt picker, context-length driven auto-decision,
|
|
2659
|
+
// direct-resume mode flags (--full, --tail10, --tail20, --summary).
|
|
2660
|
+
const parts = input.split(/\s+/).filter(Boolean);
|
|
2661
|
+
const forcedFlag = parts.find(p => /^(--full|--tail10|--tail20|--recap|--summary|-f|-1|-2|-r|-s)$/.test(p));
|
|
2662
|
+
const forcedMode = forcedFlag
|
|
2663
|
+
? ({ '--full': 'full', '-f': 'full',
|
|
2664
|
+
'--tail10': 'tail-10', '-1': 'tail-10',
|
|
2665
|
+
'--tail20': 'tail-20', '-2': 'tail-20',
|
|
2666
|
+
'--recap': 'tail-20', '-r': 'tail-20',
|
|
2667
|
+
'--summary': 'summary', '-s': 'summary' })[forcedFlag]
|
|
2668
|
+
: null;
|
|
2669
|
+
const targetId = parts.slice(1).find(p => !p.startsWith('-'));
|
|
2670
|
+
|
|
2671
|
+
const resumable = await listResumableSessions();
|
|
2672
|
+
if (resumable.length === 0) {
|
|
2673
|
+
process.stderr.write(` ${c.gray('No resumable sessions found.')}\n`);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
1370
2676
|
|
|
2677
|
+
// 1. Resolve which session to resume.
|
|
2678
|
+
let picked = null;
|
|
1371
2679
|
if (targetId) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
process.stderr.write(` ${c.yellow('!')} ${c.dim('No conversation found for session ' + targetId)}\n`);
|
|
2680
|
+
picked = resumable.find(s => s.sessionId === targetId || s.sessionId?.startsWith(targetId));
|
|
2681
|
+
if (!picked) {
|
|
2682
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`No session found matching id: ${targetId}`)}\n`);
|
|
1376
2683
|
return;
|
|
1377
2684
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
2685
|
+
} else {
|
|
2686
|
+
while (!picked) {
|
|
2687
|
+
const pickResult = await pickResumableSession(resumable, ctx);
|
|
2688
|
+
if (!pickResult) { process.stderr.write(`\n ${c.dim('Cancelled.')}\n`); return; }
|
|
2689
|
+
if (pickResult.action === 'resume') {
|
|
2690
|
+
if (!pickResult.session) {
|
|
2691
|
+
process.stderr.write(`\n ${c.yellow('!')} ${c.dim('Empty session — pick another.')}\n`);
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
picked = pickResult.session;
|
|
2695
|
+
break;
|
|
2696
|
+
}
|
|
2697
|
+
if (pickResult.action === 'preview') {
|
|
2698
|
+
// Yield to the event loop so any queued keystrokes from the picker
|
|
2699
|
+
// don't spill into the preview's stdin listener (PRD-068 §5.14 bugfix).
|
|
2700
|
+
await new Promise(r => setImmediate(r));
|
|
2701
|
+
const previewResult = await previewResumeSession(pickResult.session, ctx);
|
|
2702
|
+
if (previewResult && previewResult.action === 'resume') {
|
|
2703
|
+
picked = pickResult.session;
|
|
2704
|
+
// Preview already committed to a mode — skip the threshold overlay.
|
|
2705
|
+
picked._presetMode = previewResult.mode;
|
|
2706
|
+
break;
|
|
2707
|
+
}
|
|
2708
|
+
if (previewResult === null) {
|
|
2709
|
+
// getSessionDetail failed — file missing, unreadable, or huge.
|
|
2710
|
+
// Tell the user why before looping back to the picker.
|
|
2711
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('Could not load transcript for preview — pick another session or press Enter to resume without preview.')}\n`);
|
|
2712
|
+
}
|
|
2713
|
+
// Yield again so the loop-back render doesn't collide with the
|
|
2714
|
+
// just-closed preview's stdin cleanup.
|
|
2715
|
+
await new Promise(r => setImmediate(r));
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
1383
2718
|
}
|
|
1384
2719
|
|
|
1385
|
-
//
|
|
1386
|
-
|
|
1387
|
-
if (
|
|
1388
|
-
|
|
1389
|
-
|
|
2720
|
+
// 2. Decide the mode. Force-flag > preview-preset > auto-decide > overlay.
|
|
2721
|
+
let mode = forcedMode || picked._presetMode || null;
|
|
2722
|
+
if (!mode) {
|
|
2723
|
+
const currentModel = session?.model || session?.user?.default_reasoning_model || null;
|
|
2724
|
+
const decision = decideResumeMode({
|
|
2725
|
+
transcriptTokens: picked.contextTokens,
|
|
2726
|
+
model: currentModel,
|
|
2727
|
+
settings: ctx.effectivePolicy?.policy?.resume ? { resume: ctx.effectivePolicy.policy.resume } : {},
|
|
2728
|
+
});
|
|
2729
|
+
decision.resumeSummary = picked.resumeSummary || null;
|
|
2730
|
+
if (decision.mode === 'full') {
|
|
2731
|
+
mode = 'full';
|
|
2732
|
+
} else {
|
|
2733
|
+
// Yield before attaching the overlay listener — same race guard as
|
|
2734
|
+
// the preview branch above. Prevents a queued Enter from the picker
|
|
2735
|
+
// slipping into the overlay's stdin, which otherwise looked like a
|
|
2736
|
+
// duplicate picker render.
|
|
2737
|
+
await new Promise(r => setImmediate(r));
|
|
2738
|
+
const chosen = await chooseThresholdMode(ctx, decision);
|
|
2739
|
+
if (!chosen) { process.stderr.write(`\n ${c.dim('Cancelled.')}\n`); return; }
|
|
2740
|
+
mode = chosen;
|
|
2741
|
+
}
|
|
1390
2742
|
}
|
|
1391
2743
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
process.stderr.write(` ${c.dim(instr)}\n`);
|
|
1401
|
-
}
|
|
1402
|
-
process.stderr.write(`\n ${c.dim('Enter number (or Esc to cancel):')} `);
|
|
1403
|
-
|
|
1404
|
-
// Read single key for selection
|
|
1405
|
-
const rl = ctx._rl || null;
|
|
1406
|
-
if (rl) rl.pause();
|
|
1407
|
-
const choice = await new Promise((resolve) => {
|
|
1408
|
-
if (!process.stdin.isTTY) { resolve(null); return; }
|
|
1409
|
-
const wasRaw = process.stdin.isRaw;
|
|
1410
|
-
process.stdin.setRawMode(true);
|
|
1411
|
-
process.stdin.resume();
|
|
1412
|
-
process.stdin.once('data', (data) => {
|
|
1413
|
-
process.stdin.setRawMode(wasRaw || false);
|
|
1414
|
-
if (rl) rl.resume();
|
|
1415
|
-
const bytes = [...data];
|
|
1416
|
-
if (bytes[0] === 0x1b || bytes[0] === 0x03) { resolve(null); return; }
|
|
1417
|
-
const num = parseInt(data.toString(), 10);
|
|
1418
|
-
resolve(isNaN(num) ? null : num);
|
|
2744
|
+
// 3. Activate.
|
|
2745
|
+
const source = targetId ? 'direct' : 'picker';
|
|
2746
|
+
const progress = startResumeProgress(mode);
|
|
2747
|
+
let resumed;
|
|
2748
|
+
try {
|
|
2749
|
+
resumed = await ctx.activateResumedSession(picked.sessionId, source, mode, picked, {
|
|
2750
|
+
onProgress: progress.update,
|
|
2751
|
+
onProgressStop: progress.stop,
|
|
1419
2752
|
});
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
2753
|
+
} finally {
|
|
2754
|
+
progress.stop();
|
|
2755
|
+
}
|
|
2756
|
+
if (!resumed.ok) {
|
|
2757
|
+
if (resumed.reason === 'cwd-cancelled') {
|
|
2758
|
+
process.stderr.write(`\n ${c.dim('Cancelled.')}\n`);
|
|
2759
|
+
} else {
|
|
2760
|
+
process.stderr.write(`\n ${c.yellow('!')} ${c.dim(resumed.reason || 'No messages in that session.')}\n`);
|
|
2761
|
+
}
|
|
1424
2762
|
return;
|
|
1425
2763
|
}
|
|
1426
2764
|
|
|
1427
|
-
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
2765
|
+
// 4. Report — succinct honest single line, then optional hydration warning.
|
|
2766
|
+
const toolSummary = resumed.stats && resumed.stats.toolCalls
|
|
2767
|
+
? `${resumed.stats.toolCalls} tool calls`
|
|
2768
|
+
: `${resumed.messages} msgs`;
|
|
2769
|
+
const summaryLabel = mode !== 'full' && resumed.summarySource
|
|
2770
|
+
? ` · summary: ${resumed.summarySource}`
|
|
2771
|
+
: '';
|
|
2772
|
+
process.stderr.write(`\n ${c.green('↺')} ${c.dim('Resumed')} ${c.brand(picked.project || path.basename(safeCwd()))} ${c.dim(`· ${resumed.messages} msgs · ${toolSummary} · mode: ${resumeModeLabel(mode)}${summaryLabel}`)}\n`);
|
|
2773
|
+
if (resumed.summaryWarning) {
|
|
2774
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — using local summary (${resumed.summaryWarning})`)}\n`);
|
|
2775
|
+
}
|
|
2776
|
+
if (resumed.hydrationFailures?.length) {
|
|
2777
|
+
for (const failure of resumed.hydrationFailures) {
|
|
2778
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`could not re-read project root: ${failure}`)}\n`);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
if (resumed.stayedInCwd) {
|
|
2782
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`resumed transcript from ${resumed.savedProjectPath} — running against ${safeCwd()}`)}\n`);
|
|
1432
2783
|
}
|
|
1433
2784
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (
|
|
1439
|
-
|
|
2785
|
+
// 5. Show continuity context. Non-summary modes use the captured
|
|
2786
|
+
// kepler_event stream when available so the terminal replay matches
|
|
2787
|
+
// the original styled interaction; older sessions fall back to
|
|
2788
|
+
// reconstructed text.
|
|
2789
|
+
if (mode === 'summary' && resumed.summary) {
|
|
2790
|
+
// In summary mode the agent gets only the summary block. Show it so
|
|
2791
|
+
// the user knows what continuity context was included.
|
|
2792
|
+
process.stderr.write(`\n ${c.bold('Continuity Summary')}\n`);
|
|
2793
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
2794
|
+
for (const line of resumed.summary.split('\n')) {
|
|
2795
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
2796
|
+
}
|
|
2797
|
+
process.stderr.write('\n');
|
|
2798
|
+
} else if (resumed.replayEvents?.length) {
|
|
2799
|
+
renderResumePreview(resumed);
|
|
2800
|
+
} else if (resumed.history?.length) {
|
|
2801
|
+
// Full/tail modes feed real conversation to the agent — show
|
|
2802
|
+
// the tail so the user has visual context. Cap at 30 entries to avoid
|
|
2803
|
+
// flooding the terminal on long sessions.
|
|
2804
|
+
renderHistoryEntries(resumed.history, {
|
|
2805
|
+
limit: 30,
|
|
2806
|
+
maxChars: 200,
|
|
2807
|
+
title: mode?.startsWith('tail-') ? `Recent turns (${mode.replace('tail-', 'last ')})` : 'Conversation history (last 30 entries)',
|
|
2808
|
+
});
|
|
1440
2809
|
}
|
|
1441
|
-
process.stderr.write('\n');
|
|
1442
2810
|
return;
|
|
1443
2811
|
}
|
|
1444
2812
|
|
|
@@ -1512,22 +2880,239 @@ export async function startTerminalRepl() {
|
|
|
1512
2880
|
|
|
1513
2881
|
// Projects are registered and indexed on demand through get_project_overview.
|
|
1514
2882
|
// CheckpointManager records per-file snapshots before edits so /undo works.
|
|
1515
|
-
|
|
1516
|
-
|
|
2883
|
+
let checkpoints = new CheckpointManager(safeCwd());
|
|
2884
|
+
let effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
2885
|
+
let latestProjectContext = null;
|
|
2886
|
+
let latestEnvelope = null;
|
|
2887
|
+
let hookRunner = new HookRunner({ cwd: safeCwd() });
|
|
2888
|
+
let toolExecutor = createToolExecutor({ checkpoints, hookRunner });
|
|
1517
2889
|
const skipPerms = cliArgs.freeswim;
|
|
1518
|
-
|
|
2890
|
+
let approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
|
|
1519
2891
|
|
|
1520
2892
|
// Session manager — persists conversation messages to .kepler/conversations/
|
|
1521
|
-
|
|
2893
|
+
let sessionMgr = new SessionManager(safeCwd());
|
|
1522
2894
|
_sessionMgr = sessionMgr; // expose to renderEvent
|
|
1523
2895
|
|
|
1524
2896
|
// Local JSONL writer — writes cc-lens compatible session data to ~/.kepler/
|
|
1525
|
-
|
|
2897
|
+
let jsonlWriter = new JsonlWriter(safeCwd(), VERSION);
|
|
1526
2898
|
|
|
1527
2899
|
// Persistent stream client — session_id captured from backend on first turn
|
|
1528
2900
|
let streamClient = null;
|
|
1529
2901
|
|
|
1530
|
-
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints };
|
|
2902
|
+
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope };
|
|
2903
|
+
|
|
2904
|
+
/**
|
|
2905
|
+
* Activate a previously-recorded session for continuation.
|
|
2906
|
+
*
|
|
2907
|
+
* Contract (PRD-068 §5.14 and follow-up clarification):
|
|
2908
|
+
* 1. Keep the same sessionId. The resumed session IS the same session,
|
|
2909
|
+
* not a fork. Future turns are appended to the SAME .jsonl file that
|
|
2910
|
+
* was read here.
|
|
2911
|
+
* 2. Do not re-write the loaded transcript back to disk. The file already
|
|
2912
|
+
* contains every historical entry; the load path is read-only. Any
|
|
2913
|
+
* duplication would double-count tokens on the next resume.
|
|
2914
|
+
* 3. Fresh sessions (kepler started without /resume) get a fresh UUID
|
|
2915
|
+
* the first time jsonlWriter.writeUserTurn() runs — that path is
|
|
2916
|
+
* untouched by resume, so brand-new sessions never inherit an old id.
|
|
2917
|
+
* 4. In-memory history (session.history / session.agentHistory) mirrors
|
|
2918
|
+
* what the agent will see next turn; it is NOT written back to the
|
|
2919
|
+
* transcript at activation time.
|
|
2920
|
+
*/
|
|
2921
|
+
async function activateResumedSession(sessionId, source = 'resume', historyMode = 'full', resumeEntry = null, options = {}) {
|
|
2922
|
+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
2923
|
+
const onProgressStop = typeof options.onProgressStop === 'function' ? options.onProgressStop : () => {};
|
|
2924
|
+
// PRD-068 §5.14.6: JSONL is the only source. No legacy conversation fallback.
|
|
2925
|
+
onProgress('reading saved transcript', 14);
|
|
2926
|
+
const detail = await getSessionDetail(sessionId, { filePath: resumeEntry?.transcriptPath });
|
|
2927
|
+
if (!detail) {
|
|
2928
|
+
return { ok: false, reason: `No transcript found for session ${sessionId}` };
|
|
2929
|
+
}
|
|
2930
|
+
onProgress('building resume context', 28);
|
|
2931
|
+
const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, historyMode);
|
|
2932
|
+
const displayHistory = richHistory.displayHistory;
|
|
2933
|
+
if (!displayHistory.length) {
|
|
2934
|
+
return { ok: false, reason: `Session ${sessionId} has no readable messages` };
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// PRD-068 §5.14.7: explicit cwd confirmation if saved path differs.
|
|
2938
|
+
const savedProjectPath = detail?.meta?.project || '';
|
|
2939
|
+
let summarySource = 'local';
|
|
2940
|
+
let summaryWarning = '';
|
|
2941
|
+
if (historyMode !== 'full' && richHistory.sourceMessages?.length) {
|
|
2942
|
+
onProgress('summarizing transcript', 34);
|
|
2943
|
+
const backendSummary = await summarizeResumeTranscript({
|
|
2944
|
+
auth,
|
|
2945
|
+
toolExecutor,
|
|
2946
|
+
sessionId,
|
|
2947
|
+
projectPath: savedProjectPath || safeCwd(),
|
|
2948
|
+
messages: richHistory.sourceMessages,
|
|
2949
|
+
});
|
|
2950
|
+
if (backendSummary?.summary) {
|
|
2951
|
+
richHistory.summary = combineResumeSummaries(richHistory.priorSummary, backendSummary.summary);
|
|
2952
|
+
const summaryIndex = Number.isInteger(richHistory.summaryMessageIndex)
|
|
2953
|
+
? richHistory.summaryMessageIndex
|
|
2954
|
+
: 0;
|
|
2955
|
+
if (richHistory.agentHistory?.[summaryIndex]) {
|
|
2956
|
+
const tailTurns = resumeTailTurnCount(historyMode);
|
|
2957
|
+
const prefix = tailTurns
|
|
2958
|
+
? `Summary of earlier turns before the retained last ${tailTurns} conversation messages:\n`
|
|
2959
|
+
: 'Session continuity summary:\n';
|
|
2960
|
+
richHistory.agentHistory[summaryIndex] = {
|
|
2961
|
+
...richHistory.agentHistory[summaryIndex],
|
|
2962
|
+
content: `${prefix}${richHistory.summary}`,
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
summarySource = backendSummary.source || 'backend';
|
|
2966
|
+
} else {
|
|
2967
|
+
summarySource = 'local fallback';
|
|
2968
|
+
summaryWarning = backendSummary?.reason || 'backend summary unavailable';
|
|
2969
|
+
}
|
|
2970
|
+
} else if (historyMode !== 'full') {
|
|
2971
|
+
summarySource = 'not needed';
|
|
2972
|
+
summaryWarning = resumeTailTurnCount(historyMode)
|
|
2973
|
+
? 'retained tail covers the whole transcript'
|
|
2974
|
+
: 'empty transcript';
|
|
2975
|
+
}
|
|
2976
|
+
const agentHistory = richHistory.agentHistory;
|
|
2977
|
+
const originalCwd = safeCwd();
|
|
2978
|
+
let switchedProject = false;
|
|
2979
|
+
let projectMissing = false;
|
|
2980
|
+
let stayedInCwd = false;
|
|
2981
|
+
onProgress('checking project cwd', 40);
|
|
2982
|
+
if (savedProjectPath && savedProjectPath !== originalCwd) {
|
|
2983
|
+
if (fs.existsSync(savedProjectPath)) {
|
|
2984
|
+
onProgressStop();
|
|
2985
|
+
const choice = await confirmCwdSwitch(ctx, savedProjectPath, originalCwd);
|
|
2986
|
+
if (choice === 'cancel') return { ok: false, reason: 'cwd-cancelled' };
|
|
2987
|
+
if (choice === 'switch') {
|
|
2988
|
+
try {
|
|
2989
|
+
process.chdir(savedProjectPath);
|
|
2990
|
+
_cachedCwd = process.cwd();
|
|
2991
|
+
switchedProject = true;
|
|
2992
|
+
} catch {
|
|
2993
|
+
projectMissing = true;
|
|
2994
|
+
}
|
|
2995
|
+
} else if (choice === 'stay') {
|
|
2996
|
+
stayedInCwd = true;
|
|
2997
|
+
}
|
|
2998
|
+
} else {
|
|
2999
|
+
projectMissing = true;
|
|
3000
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`saved project path unavailable: ${savedProjectPath} — using current cwd`)}\n`);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
onProgress('rebuilding local session state', 55);
|
|
3005
|
+
checkpoints = new CheckpointManager(safeCwd());
|
|
3006
|
+
effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
3007
|
+
hookRunner = new HookRunner({ cwd: safeCwd(), sessionId });
|
|
3008
|
+
toolExecutor = createToolExecutor({ checkpoints, hookRunner });
|
|
3009
|
+
approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
|
|
3010
|
+
if (ctx._rl) approval.setReadline(ctx._rl);
|
|
3011
|
+
sessionMgr = new SessionManager(safeCwd());
|
|
3012
|
+
sessionMgr.activateSession(sessionId, {
|
|
3013
|
+
instruction: detail?.meta?.firstPrompt || '',
|
|
3014
|
+
started_at: detail?.meta?.startTime || new Date().toISOString(),
|
|
3015
|
+
}, displayHistory.filter(m => m.role === 'user' || m.role === 'assistant'));
|
|
3016
|
+
_sessionMgr = sessionMgr;
|
|
3017
|
+
|
|
3018
|
+
try { await jsonlWriter.close(); } catch {}
|
|
3019
|
+
jsonlWriter = new JsonlWriter(safeCwd(), VERSION);
|
|
3020
|
+
jsonlWriter.setSessionId(sessionId);
|
|
3021
|
+
if (
|
|
3022
|
+
historyMode !== 'full'
|
|
3023
|
+
&& richHistory.summary
|
|
3024
|
+
&& Number(richHistory.summaryCoveredMessageCount) > Number(richHistory.summaryCheckpointMessageCount || 0)
|
|
3025
|
+
) {
|
|
3026
|
+
jsonlWriter.writeKeplerEvent({
|
|
3027
|
+
type: 'resume_summary',
|
|
3028
|
+
data: {
|
|
3029
|
+
session_id: sessionId,
|
|
3030
|
+
mode: historyMode,
|
|
3031
|
+
mode_label: resumeModeLabel(historyMode),
|
|
3032
|
+
summary: richHistory.summary,
|
|
3033
|
+
summary_source: summarySource,
|
|
3034
|
+
summary_warning: summaryWarning || null,
|
|
3035
|
+
source_message_count: richHistory.summaryCoveredMessageCount,
|
|
3036
|
+
previous_source_message_count: richHistory.summaryCheckpointMessageCount || 0,
|
|
3037
|
+
full_message_count: richHistory.fullMessageCount || 0,
|
|
3038
|
+
},
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
jsonlWriter.writeKeplerEvent({
|
|
3042
|
+
type: 'resume_context',
|
|
3043
|
+
data: {
|
|
3044
|
+
session_id: sessionId,
|
|
3045
|
+
source,
|
|
3046
|
+
mode: historyMode,
|
|
3047
|
+
mode_label: resumeModeLabel(historyMode),
|
|
3048
|
+
messages: displayHistory.length,
|
|
3049
|
+
summary_source: summarySource,
|
|
3050
|
+
summary_injected: historyMode !== 'full' && Boolean(richHistory.agentHistory?.[richHistory.summaryMessageIndex ?? 0]?.content),
|
|
3051
|
+
summary_warning: summaryWarning || null,
|
|
3052
|
+
summary_source_message_count: richHistory.summaryCoveredMessageCount || 0,
|
|
3053
|
+
previous_summary_source_message_count: richHistory.summaryCheckpointMessageCount || 0,
|
|
3054
|
+
project_path: savedProjectPath || safeCwd(),
|
|
3055
|
+
},
|
|
3056
|
+
});
|
|
3057
|
+
|
|
3058
|
+
streamClient = null;
|
|
3059
|
+
latestProjectContext = loadProjectContext({ cwd: safeCwd() });
|
|
3060
|
+
latestEnvelope = null;
|
|
3061
|
+
|
|
3062
|
+
// PRD-068 §5.14.8: report hydration failures instead of swallowing them.
|
|
3063
|
+
const hydrationFailures = [];
|
|
3064
|
+
const resumeRoots = getTranscriptProjectRoots(detail);
|
|
3065
|
+
const rootsToRegister = [...new Set([safeCwd(), ...resumeRoots].filter(Boolean))];
|
|
3066
|
+
onProgress('hydrating project roots', 68);
|
|
3067
|
+
for (let i = 0; i < rootsToRegister.length; i++) {
|
|
3068
|
+
const root = rootsToRegister[i];
|
|
3069
|
+
onProgress(`hydrating project root ${i + 1}/${rootsToRegister.length}`, 68 + Math.round((i / Math.max(1, rootsToRegister.length)) * 18));
|
|
3070
|
+
try {
|
|
3071
|
+
await toolExecutor.execute('get_project_overview', { path: root });
|
|
3072
|
+
} catch {
|
|
3073
|
+
hydrationFailures.push(root);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
onProgress('preparing replay', 92);
|
|
3078
|
+
session.history = displayHistory;
|
|
3079
|
+
session.agentHistory = agentHistory;
|
|
3080
|
+
session.id = sessionId;
|
|
3081
|
+
session.turns = displayHistory.filter(m => m.role === 'user').length;
|
|
3082
|
+
session.lastTask = detail?.meta?.firstPrompt || session.history.find(m => m.role === 'user')?.content || '';
|
|
3083
|
+
|
|
3084
|
+
Object.assign(ctx, {
|
|
3085
|
+
toolExecutor,
|
|
3086
|
+
approval,
|
|
3087
|
+
jsonlWriter,
|
|
3088
|
+
sessionMgr,
|
|
3089
|
+
checkpoints,
|
|
3090
|
+
effectivePolicy,
|
|
3091
|
+
latestProjectContext,
|
|
3092
|
+
latestEnvelope,
|
|
3093
|
+
});
|
|
3094
|
+
|
|
3095
|
+
return {
|
|
3096
|
+
ok: true,
|
|
3097
|
+
messages: displayHistory.length,
|
|
3098
|
+
projectPath: savedProjectPath || safeCwd(),
|
|
3099
|
+
savedProjectPath,
|
|
3100
|
+
switchedProject,
|
|
3101
|
+
projectMissing,
|
|
3102
|
+
stayedInCwd,
|
|
3103
|
+
hydrationFailures,
|
|
3104
|
+
instruction: detail?.meta?.firstPrompt || '',
|
|
3105
|
+
historyMode,
|
|
3106
|
+
summary: richHistory.summary || '',
|
|
3107
|
+
summarySource,
|
|
3108
|
+
summaryWarning,
|
|
3109
|
+
history: displayHistory,
|
|
3110
|
+
replayEvents: detail.replayEvents || [],
|
|
3111
|
+
stats: richHistory.stats,
|
|
3112
|
+
source,
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
ctx.activateResumedSession = activateResumedSession;
|
|
1531
3116
|
|
|
1532
3117
|
// ── Print banner + preflight + init BEFORE mounting the status bar ──
|
|
1533
3118
|
// The status bar shrinks the scroll region; if it mounts first, the
|
|
@@ -1558,18 +3143,18 @@ export async function startTerminalRepl() {
|
|
|
1558
3143
|
: sessionMgr.getLastSession();
|
|
1559
3144
|
|
|
1560
3145
|
if (lastSession) {
|
|
1561
|
-
const
|
|
1562
|
-
if (
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
process.stderr.write(`
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
1569
|
-
}
|
|
3146
|
+
const resumed = await activateResumedSession(lastSession.sessionId, 'startup');
|
|
3147
|
+
if (resumed.ok) {
|
|
3148
|
+
process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed session: ${messageCountLabel(resumed.messages)}`)}`);
|
|
3149
|
+
process.stderr.write(` ${c.dim('· project')} ${c.brand(path.basename(safeCwd()))}`);
|
|
3150
|
+
process.stderr.write(` ${c.dim(`· agent ${resumed.historyMode}`)}`);
|
|
3151
|
+
if (resumed.switchedProject) process.stderr.write(` ${c.dim('(cwd restored)')}`);
|
|
3152
|
+
if (resumed.projectMissing) process.stderr.write(` ${c.yellow('(saved project path unavailable; using current cwd)')}`);
|
|
3153
|
+
if (resumed.instruction) process.stderr.write(` ${c.dim('—')} ${c.dim(resumed.instruction.slice(0, 50))}`);
|
|
1570
3154
|
process.stderr.write('\n');
|
|
3155
|
+
renderResumePreview(resumed);
|
|
1571
3156
|
} else {
|
|
1572
|
-
process.stderr.write(` ${c.yellow('!')} ${c.dim('No conversation found for session ' + lastSession.sessionId)}\n`);
|
|
3157
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(resumed.reason || 'No conversation found for session ' + lastSession.sessionId)}\n`);
|
|
1573
3158
|
}
|
|
1574
3159
|
} else {
|
|
1575
3160
|
process.stderr.write(` ${c.yellow('!')} ${c.dim('No previous session to resume')}\n`);
|
|
@@ -1615,8 +3200,7 @@ export async function startTerminalRepl() {
|
|
|
1615
3200
|
prompt: userPrompt(),
|
|
1616
3201
|
completer: (line) => {
|
|
1617
3202
|
if (line.startsWith('/')) {
|
|
1618
|
-
|
|
1619
|
-
return [hits.length ? hits : Object.keys(COMMANDS), line];
|
|
3203
|
+
return [commandCompletions(line), line];
|
|
1620
3204
|
}
|
|
1621
3205
|
return [[], line];
|
|
1622
3206
|
},
|
|
@@ -1637,7 +3221,44 @@ export async function startTerminalRepl() {
|
|
|
1637
3221
|
|
|
1638
3222
|
showPrompt();
|
|
1639
3223
|
|
|
3224
|
+
// Guard against concurrent line handlers.
|
|
3225
|
+
//
|
|
3226
|
+
// rl.on('line', async ...) does NOT wait for the previous async handler to
|
|
3227
|
+
// complete before firing again. So a stray '\n' (from readline processing
|
|
3228
|
+
// '\r\n' as two events after an interactive picker resumes it, or from a
|
|
3229
|
+
// paste) can spawn a second handleCommand run while the first is still
|
|
3230
|
+
// awaiting inside /resume, /login, etc. Symptom: the picker or overlay
|
|
3231
|
+
// appears to render twice.
|
|
3232
|
+
//
|
|
3233
|
+
// Rules:
|
|
3234
|
+
// - Only one command runs at a time.
|
|
3235
|
+
// - Empty lines that arrive while a command is in flight are dropped
|
|
3236
|
+
// (they are almost always stray '\n's from raw-mode key handling).
|
|
3237
|
+
// - Non-empty lines are queued and replayed after the current command
|
|
3238
|
+
// finishes, so the user can queue up work while a long-running command
|
|
3239
|
+
// is still executing.
|
|
3240
|
+
let _lineInFlight = false;
|
|
3241
|
+
const _queuedLines = [];
|
|
1640
3242
|
rl.on('line', async (line) => {
|
|
3243
|
+
if (_lineInFlight) {
|
|
3244
|
+
if (line && line.trim()) _queuedLines.push(line);
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
_lineInFlight = true;
|
|
3248
|
+
try {
|
|
3249
|
+
await _handleLine(line);
|
|
3250
|
+
} finally {
|
|
3251
|
+
_lineInFlight = false;
|
|
3252
|
+
if (_queuedLines.length) {
|
|
3253
|
+
const next = _queuedLines.shift();
|
|
3254
|
+
// Fire the next queued line asynchronously so we don't hold this
|
|
3255
|
+
// finally block open while the next command runs.
|
|
3256
|
+
setImmediate(() => rl.emit('line', next));
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
|
|
3261
|
+
async function _handleLine(line) {
|
|
1641
3262
|
const input = line.trim();
|
|
1642
3263
|
if (!input) { rl.prompt(); return; }
|
|
1643
3264
|
|
|
@@ -1660,16 +3281,20 @@ export async function startTerminalRepl() {
|
|
|
1660
3281
|
}
|
|
1661
3282
|
|
|
1662
3283
|
// Regular prompt
|
|
1663
|
-
|
|
3284
|
+
const userMessage = { role: 'user', content: input };
|
|
3285
|
+
session.history.push(userMessage);
|
|
3286
|
+
session.agentHistory.push(userMessage);
|
|
1664
3287
|
session.turns++;
|
|
1665
3288
|
session.toolCalls = 0;
|
|
1666
3289
|
session.lastTask = input;
|
|
1667
3290
|
// Reset per-turn counts so the mission report reflects this turn only.
|
|
1668
3291
|
session.toolCounts = {};
|
|
1669
3292
|
session.subAgentCounts = {};
|
|
3293
|
+
session.filesRead = [];
|
|
1670
3294
|
session.savedUsd = 0;
|
|
1671
3295
|
session._lastEmittedThinking = '';
|
|
1672
3296
|
session.creditsLowWarned = false;
|
|
3297
|
+
session.msgsLowWarned = false;
|
|
1673
3298
|
|
|
1674
3299
|
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
1675
3300
|
// task / turn counters in the status bar.
|
|
@@ -1679,11 +3304,14 @@ export async function startTerminalRepl() {
|
|
|
1679
3304
|
if (session.turns === 1) {
|
|
1680
3305
|
sessionMgr.start(input);
|
|
1681
3306
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
3307
|
+
let userTurnWritten = false;
|
|
3308
|
+
const writeCurrentUserTurn = () => {
|
|
3309
|
+
if (userTurnWritten) return;
|
|
3310
|
+
jsonlWriter.writeUserTurn(input);
|
|
3311
|
+
jsonlWriter.writeHistory(input);
|
|
3312
|
+
userTurnWritten = true;
|
|
3313
|
+
};
|
|
3314
|
+
if (session.id) writeCurrentUserTurn();
|
|
1687
3315
|
|
|
1688
3316
|
const creds = auth.loadCredentials();
|
|
1689
3317
|
if (!creds.token) {
|
|
@@ -1705,6 +3333,9 @@ export async function startTerminalRepl() {
|
|
|
1705
3333
|
});
|
|
1706
3334
|
}
|
|
1707
3335
|
const client = streamClient;
|
|
3336
|
+
if (session.id && !client.sessionId) {
|
|
3337
|
+
client.sessionId = session.id;
|
|
3338
|
+
}
|
|
1708
3339
|
|
|
1709
3340
|
let assistantContent = '';
|
|
1710
3341
|
|
|
@@ -1800,10 +3431,57 @@ export async function startTerminalRepl() {
|
|
|
1800
3431
|
|
|
1801
3432
|
const execContext = { cwd: safeCwd() };
|
|
1802
3433
|
if (skipPerms) execContext.freeswim = true;
|
|
1803
|
-
|
|
1804
|
-
|
|
3434
|
+
effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
3435
|
+
approval.policy = effectivePolicy.policy;
|
|
3436
|
+
if (approval.trustStore) approval.trustStore.policy = effectivePolicy.policy;
|
|
3437
|
+
hookRunner.reload();
|
|
3438
|
+
latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous: latestProjectContext });
|
|
3439
|
+
const projectResources = toolExecutor.getProjectResources();
|
|
3440
|
+
const promptHook = await hookRunner.run('UserPromptSubmit', {
|
|
3441
|
+
input: { prompt: input },
|
|
3442
|
+
turnId: String(session.turns),
|
|
3443
|
+
});
|
|
3444
|
+
const hookHints = (promptHook.results || [])
|
|
3445
|
+
.map(r => r.parsed?.feedback)
|
|
3446
|
+
.filter(Boolean)
|
|
3447
|
+
.map(text => ({ source: 'hook', kind: 'feedback', text, ttl_turns: 1, priority: 'medium' }));
|
|
3448
|
+
const rejectionHints = (approval.consumeRejectionHints?.() || [])
|
|
3449
|
+
.map(h => ({
|
|
3450
|
+
source: 'hitl',
|
|
3451
|
+
kind: 'approval_rejection',
|
|
3452
|
+
text: `${h.decision === 'replan' ? 'User requested a re-plan' : 'User rejected approval'} for ${h.tool}. ${h.note ? `Reason: ${h.note}` : h.reason}. Adjust the approach before retrying.`,
|
|
3453
|
+
ttl_turns: 1,
|
|
3454
|
+
priority: 'high',
|
|
3455
|
+
}));
|
|
3456
|
+
latestEnvelope = buildContextEnvelope({
|
|
3457
|
+
cwd: safeCwd(),
|
|
3458
|
+
effectivePolicy,
|
|
3459
|
+
projectContext: latestProjectContext,
|
|
3460
|
+
activeHints: [...hookHints, ...rejectionHints],
|
|
3461
|
+
projectResources,
|
|
3462
|
+
agentContext: toolExecutor.getAgentContext(),
|
|
3463
|
+
});
|
|
3464
|
+
ctx.effectivePolicy = effectivePolicy;
|
|
3465
|
+
ctx.latestProjectContext = latestProjectContext;
|
|
3466
|
+
ctx.latestEnvelope = latestEnvelope;
|
|
3467
|
+
Object.assign(execContext, latestEnvelope);
|
|
3468
|
+
if (skipPerms) execContext.freeswim = true;
|
|
3469
|
+
// PRD-071: seed work_scope from CLI so the backend has a byte-stable
|
|
3470
|
+
// scope block from turn 1. Uses projectResources already gathered by
|
|
3471
|
+
// the envelope above.
|
|
3472
|
+
execContext.work_scope = buildWorkScope({
|
|
3473
|
+
instruction: input,
|
|
3474
|
+
cwd: safeCwd(),
|
|
3475
|
+
projectResources,
|
|
3476
|
+
});
|
|
3477
|
+
for (const file of latestProjectContext.changed || []) {
|
|
3478
|
+
if (effectivePolicy.policy.context?.showReloadNotice) {
|
|
3479
|
+
process.stderr.write(` ${c.dim(`[Context] ${file.label} updated — re-read`)}\n`);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
1805
3482
|
|
|
1806
|
-
for await (const event of client.execute(input, execContext, session.
|
|
3483
|
+
for await (const event of client.execute(input, execContext, session.agentHistory)) {
|
|
3484
|
+
jsonlWriter.writeKeplerEvent(event);
|
|
1807
3485
|
if (event.type === 'plan_created' || event.type === 'goal_created') {
|
|
1808
3486
|
persistProjectArtifacts(
|
|
1809
3487
|
event.data,
|
|
@@ -1833,6 +3511,8 @@ export async function startTerminalRepl() {
|
|
|
1833
3511
|
// Local JSONL: capture session ID from backend
|
|
1834
3512
|
if (event.type === 'session_info' && event.data?.session_id) {
|
|
1835
3513
|
jsonlWriter.setSessionId(event.data.session_id);
|
|
3514
|
+
hookRunner.sessionId = event.data.session_id;
|
|
3515
|
+
writeCurrentUserTurn();
|
|
1836
3516
|
}
|
|
1837
3517
|
|
|
1838
3518
|
// Local JSONL: accumulate tool calls
|
|
@@ -1865,15 +3545,17 @@ export async function startTerminalRepl() {
|
|
|
1865
3545
|
}
|
|
1866
3546
|
|
|
1867
3547
|
if (assistantContent) {
|
|
1868
|
-
|
|
1869
|
-
|
|
3548
|
+
const assistantMessage = { role: 'assistant', content: assistantContent };
|
|
3549
|
+
session.history.push(assistantMessage);
|
|
3550
|
+
session.agentHistory.push(assistantMessage);
|
|
1870
3551
|
}
|
|
1871
3552
|
|
|
1872
3553
|
showPrompt();
|
|
1873
|
-
}
|
|
3554
|
+
}
|
|
1874
3555
|
|
|
1875
3556
|
rl.on('close', async () => {
|
|
1876
3557
|
stopSpinner();
|
|
3558
|
+
await hookRunner.run('Stop', { input: { session_id: session.id || '' } });
|
|
1877
3559
|
await jsonlWriter.close();
|
|
1878
3560
|
process.stderr.write(`\n ${c.dim('session ended')}\n\n`);
|
|
1879
3561
|
process.exit(0);
|