@axplusb/kepler 1.0.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/context/retriever.mjs +42 -4
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/approval.mjs +154 -95
- package/src/core/backend-url.mjs +2 -2
- package/src/core/headless.mjs +5 -0
- package/src/core/pricing.mjs +23 -1
- package/src/core/risk-tier.mjs +239 -0
- package/src/core/tool-executor.mjs +78 -5
- package/src/onboarding/preflight.mjs +274 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/terminal/ansi.mjs +47 -27
- package/src/terminal/repl.mjs +407 -121
- package/src/ui/approval.mjs +167 -0
- package/src/ui/banner.mjs +133 -122
- package/src/ui/dock.mjs +88 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/mission-report.mjs +264 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/status-bar.mjs +275 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/tool-card.mjs +314 -0
- package/src/ui/tool-details.mjs +277 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orbit state machine — Mission Control (PRD-055 §5.2).
|
|
3
|
+
*
|
|
4
|
+
* The "orbit" is the current phase of the session. The status bar reads
|
|
5
|
+
* from this module; the REPL pushes events into it. It is intentionally a
|
|
6
|
+
* pure state machine — no I/O, no side effects, no globals. Each REPL
|
|
7
|
+
* creates one instance.
|
|
8
|
+
*
|
|
9
|
+
* const orbit = createOrbit();
|
|
10
|
+
* orbit.on('change', state => statusBar.render(state));
|
|
11
|
+
* orbit.onEvent({ type: 'tool_call', data: { tool: 'edit_file' } });
|
|
12
|
+
*
|
|
13
|
+
* States (PRD §5.2):
|
|
14
|
+
* IDLE — waiting for user input
|
|
15
|
+
* DISCOVERY — first message until first plan or edit
|
|
16
|
+
* PLANNING — preflight plan running OR plan() sub-agent active
|
|
17
|
+
* EXECUTION — write/edit/shell tools firing
|
|
18
|
+
* ALIGNMENT — tests / validators running
|
|
19
|
+
* AWAITING — approval required
|
|
20
|
+
* PAUSED — user pressed `p`
|
|
21
|
+
*
|
|
22
|
+
* Transitions are derived from existing backend SSE events. We never
|
|
23
|
+
* teach the backend about orbits; the CLI infers them from tool activity.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Tool families used for orbit inference. Mirrors src/ui/icons.mjs but
|
|
27
|
+
// scoped to the few orbits actually need.
|
|
28
|
+
const PLANNING_TOOLS = new Set(['plan']);
|
|
29
|
+
const EXECUTION_TOOLS = new Set(['edit_file', 'write_file', 'write_project', 'shell', 'delete_file']);
|
|
30
|
+
const ALIGNMENT_TOOLS = new Set(['run_tests', 'validate_build', 'lint_check', 'validate_file', 'validate_structure', 'git_diff', 'git_status']);
|
|
31
|
+
const RESEARCH_TOOLS = new Set(['search_code', 'search_files', 'grep', 'read_file', 'read_files', 'list_files', 'analyze_code', 'get_project_overview', 'explore']);
|
|
32
|
+
|
|
33
|
+
export const ORBITS = Object.freeze({
|
|
34
|
+
IDLE: 'IDLE',
|
|
35
|
+
DISCOVERY: 'DISCOVERY',
|
|
36
|
+
PLANNING: 'PLANNING',
|
|
37
|
+
EXECUTION: 'EXECUTION',
|
|
38
|
+
ALIGNMENT: 'ALIGNMENT',
|
|
39
|
+
AWAITING: 'AWAITING',
|
|
40
|
+
PAUSED: 'PAUSED',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @returns the snapshot consumed by the status bar.
|
|
45
|
+
*/
|
|
46
|
+
function snapshot(s) {
|
|
47
|
+
return {
|
|
48
|
+
orbit: s.orbit,
|
|
49
|
+
task: s.task || '',
|
|
50
|
+
turn: s.turn,
|
|
51
|
+
maxTurn: s.maxTurn,
|
|
52
|
+
cost: s.cost,
|
|
53
|
+
activeTool: s.activeTool || '',
|
|
54
|
+
subAgents: s.subAgents,
|
|
55
|
+
paused: s.paused,
|
|
56
|
+
awaitingTier: s.awaitingTier || null,
|
|
57
|
+
awaitingTool: s.awaitingTool || '',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createOrbit() {
|
|
62
|
+
const state = {
|
|
63
|
+
orbit: ORBITS.IDLE,
|
|
64
|
+
task: '',
|
|
65
|
+
turn: 0,
|
|
66
|
+
maxTurn: 0,
|
|
67
|
+
cost: 0,
|
|
68
|
+
activeTool: '',
|
|
69
|
+
subAgents: 0, // count of currently-active sub-agents
|
|
70
|
+
paused: false,
|
|
71
|
+
awaitingTool: '',
|
|
72
|
+
awaitingTier: null,
|
|
73
|
+
_hasEdited: false, // for DISCOVERY → EXECUTION transition
|
|
74
|
+
_resumeOrbit: null, // remembered orbit when paused
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const listeners = new Set();
|
|
78
|
+
|
|
79
|
+
function emit() {
|
|
80
|
+
const snap = snapshot(state);
|
|
81
|
+
for (const fn of listeners) {
|
|
82
|
+
try { fn(snap); } catch {}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setOrbit(next) {
|
|
87
|
+
if (state.paused && next !== ORBITS.PAUSED && next !== ORBITS.IDLE) {
|
|
88
|
+
// While paused, remember the orbit that would have applied but stay paused.
|
|
89
|
+
state._resumeOrbit = next;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (state.orbit === next) return;
|
|
93
|
+
state.orbit = next;
|
|
94
|
+
emit();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function inferOrbitFromTool(toolName) {
|
|
98
|
+
if (state.paused) return null;
|
|
99
|
+
if (state.subAgents > 0 && PLANNING_TOOLS.has(toolName)) return ORBITS.PLANNING;
|
|
100
|
+
if (PLANNING_TOOLS.has(toolName)) return ORBITS.PLANNING;
|
|
101
|
+
if (EXECUTION_TOOLS.has(toolName)) {
|
|
102
|
+
state._hasEdited = true;
|
|
103
|
+
return ORBITS.EXECUTION;
|
|
104
|
+
}
|
|
105
|
+
if (ALIGNMENT_TOOLS.has(toolName)) return ORBITS.ALIGNMENT;
|
|
106
|
+
if (RESEARCH_TOOLS.has(toolName)) {
|
|
107
|
+
// Stay in DISCOVERY until first edit; afterwards research stays in
|
|
108
|
+
// current orbit (EXECUTION) so the status doesn't flicker back.
|
|
109
|
+
return state._hasEdited ? null : ORBITS.DISCOVERY;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
state: () => snapshot(state),
|
|
116
|
+
|
|
117
|
+
on(event, fn) {
|
|
118
|
+
if (event !== 'change') return () => {};
|
|
119
|
+
listeners.add(fn);
|
|
120
|
+
return () => listeners.delete(fn);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// ── Inbound events from the REPL ──────────────────────────────────
|
|
124
|
+
|
|
125
|
+
onUserInput(text) {
|
|
126
|
+
// First user message of the session OR a new turn opens DISCOVERY.
|
|
127
|
+
state.turn++;
|
|
128
|
+
state.task = (text || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
129
|
+
state._hasEdited = false;
|
|
130
|
+
state.activeTool = '';
|
|
131
|
+
setOrbit(ORBITS.DISCOVERY);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
onMaxTurn(n) {
|
|
135
|
+
if (typeof n === 'number' && n > 0) {
|
|
136
|
+
state.maxTurn = n;
|
|
137
|
+
emit();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
onTask(text) {
|
|
142
|
+
if (!text) return;
|
|
143
|
+
state.task = String(text).replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
144
|
+
emit();
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
onCost(value) {
|
|
148
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
149
|
+
state.cost = value;
|
|
150
|
+
emit();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
onToolCall(toolName) {
|
|
155
|
+
state.activeTool = toolName || '';
|
|
156
|
+
const next = inferOrbitFromTool(toolName);
|
|
157
|
+
if (next) setOrbit(next);
|
|
158
|
+
else emit();
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
onToolResult() {
|
|
162
|
+
state.activeTool = '';
|
|
163
|
+
emit();
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
onSubAgentStart() {
|
|
167
|
+
state.subAgents = Math.max(0, state.subAgents) + 1;
|
|
168
|
+
setOrbit(ORBITS.PLANNING);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
onSubAgentEnd() {
|
|
172
|
+
state.subAgents = Math.max(0, state.subAgents - 1);
|
|
173
|
+
// Fall back to whatever the parent was doing — we don't track that
|
|
174
|
+
// precisely, so go to EXECUTION if an edit has happened, else
|
|
175
|
+
// DISCOVERY. The next tool_call event will refine.
|
|
176
|
+
if (state.subAgents === 0) {
|
|
177
|
+
setOrbit(state._hasEdited ? ORBITS.EXECUTION : ORBITS.DISCOVERY);
|
|
178
|
+
} else {
|
|
179
|
+
emit();
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
onApprovalRequired({ tool, tier } = {}) {
|
|
184
|
+
state.awaitingTool = tool || '';
|
|
185
|
+
state.awaitingTier = tier || null;
|
|
186
|
+
setOrbit(ORBITS.AWAITING);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
onApprovalResolved() {
|
|
190
|
+
state.awaitingTool = '';
|
|
191
|
+
state.awaitingTier = null;
|
|
192
|
+
// Drop back to the inferred orbit for the active tool, or EXECUTION
|
|
193
|
+
// if we have an active tool but can't classify, or IDLE.
|
|
194
|
+
const inferred = inferOrbitFromTool(state.activeTool) || (state._hasEdited ? ORBITS.EXECUTION : ORBITS.DISCOVERY);
|
|
195
|
+
setOrbit(inferred);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
onComplete({ cost } = {}) {
|
|
199
|
+
if (typeof cost === 'number') state.cost = cost;
|
|
200
|
+
state.activeTool = '';
|
|
201
|
+
setOrbit(ORBITS.IDLE);
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
onPause() {
|
|
205
|
+
if (state.paused) return;
|
|
206
|
+
state.paused = true;
|
|
207
|
+
state._resumeOrbit = state.orbit;
|
|
208
|
+
state.orbit = ORBITS.PAUSED;
|
|
209
|
+
emit();
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
onResume() {
|
|
213
|
+
if (!state.paused) return;
|
|
214
|
+
state.paused = false;
|
|
215
|
+
const resume = state._resumeOrbit || ORBITS.IDLE;
|
|
216
|
+
state._resumeOrbit = null;
|
|
217
|
+
state.orbit = resume;
|
|
218
|
+
emit();
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Generic event router so the REPL can feed raw SSE events without a
|
|
223
|
+
* giant switch in this module. Returns true if the event was handled.
|
|
224
|
+
*/
|
|
225
|
+
onEvent(event) {
|
|
226
|
+
if (!event || !event.type) return false;
|
|
227
|
+
const { type, data } = event;
|
|
228
|
+
switch (type) {
|
|
229
|
+
case 'tool_call':
|
|
230
|
+
case 'tool_request':
|
|
231
|
+
this.onToolCall(data?.tool || '');
|
|
232
|
+
return true;
|
|
233
|
+
case 'tool_result':
|
|
234
|
+
case 'tool_done':
|
|
235
|
+
this.onToolResult();
|
|
236
|
+
return true;
|
|
237
|
+
case 'sub_agent_start':
|
|
238
|
+
this.onSubAgentStart();
|
|
239
|
+
return true;
|
|
240
|
+
case 'sub_agent_complete':
|
|
241
|
+
this.onSubAgentEnd();
|
|
242
|
+
return true;
|
|
243
|
+
case 'approval_required':
|
|
244
|
+
this.onApprovalRequired({ tool: data?.tool, tier: data?.tier });
|
|
245
|
+
return true;
|
|
246
|
+
case 'approval_granted':
|
|
247
|
+
case 'approval_denied':
|
|
248
|
+
this.onApprovalResolved();
|
|
249
|
+
return true;
|
|
250
|
+
case 'complete': {
|
|
251
|
+
const usage = data?.usage || {};
|
|
252
|
+
this.onComplete({ cost: usage.total_cost_usd ?? usage.cost_usd });
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
case 'plan_created':
|
|
256
|
+
this.onTask(data?.title || data?.task || '');
|
|
257
|
+
return true;
|
|
258
|
+
default:
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verbosity modes — Mission Control (PRD-055 §12).
|
|
3
|
+
*
|
|
4
|
+
* quiet Folded summary only. Sub-agent inner tools hidden.
|
|
5
|
+
* default Folded summary. Sub-agent header shown, inner tools folded.
|
|
6
|
+
* verbose Folded summary. Sub-agent inner tools shown.
|
|
7
|
+
* surgical Expanded tool details + raw model reasoning.
|
|
8
|
+
*
|
|
9
|
+
* Persisted to `~/.kepler/config.json` under the `verbosity` key so the
|
|
10
|
+
* choice survives across sessions.
|
|
11
|
+
*
|
|
12
|
+
* import { getVerbosity, setVerbosity, showSubAgentTools, showReasoning } from './verbosity.mjs';
|
|
13
|
+
*
|
|
14
|
+
* No imports from the REPL — this module is pure state + filesystem.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
export const MODES = Object.freeze({
|
|
22
|
+
QUIET: 'quiet',
|
|
23
|
+
DEFAULT: 'default',
|
|
24
|
+
VERBOSE: 'verbose',
|
|
25
|
+
SURGICAL: 'surgical',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const VALID = new Set(Object.values(MODES));
|
|
29
|
+
|
|
30
|
+
const CONFIG_DIR = process.env.KEPLER_HOME || path.join(os.homedir(), '.kepler');
|
|
31
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
32
|
+
|
|
33
|
+
let _cached = null;
|
|
34
|
+
|
|
35
|
+
function readConfig() {
|
|
36
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); }
|
|
37
|
+
catch { return {}; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeConfig(obj) {
|
|
41
|
+
try {
|
|
42
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
43
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(obj, null, 2));
|
|
44
|
+
} catch { /* best effort */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read the current mode (falls back to default). */
|
|
48
|
+
export function getVerbosity() {
|
|
49
|
+
if (_cached) return _cached;
|
|
50
|
+
const v = readConfig().verbosity;
|
|
51
|
+
_cached = VALID.has(v) ? v : MODES.DEFAULT;
|
|
52
|
+
return _cached;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Update the persisted mode. Returns the new mode. */
|
|
56
|
+
export function setVerbosity(mode) {
|
|
57
|
+
if (!VALID.has(mode)) throw new Error(`Unknown verbosity mode: ${mode}`);
|
|
58
|
+
const cfg = readConfig();
|
|
59
|
+
cfg.verbosity = mode;
|
|
60
|
+
writeConfig(cfg);
|
|
61
|
+
_cached = mode;
|
|
62
|
+
return mode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Force-reload from disk (used by tests). */
|
|
66
|
+
export function _resetCache() { _cached = null; }
|
|
67
|
+
|
|
68
|
+
// ── Predicates — let other modules ask "should I render X?" ─────────────
|
|
69
|
+
|
|
70
|
+
/** Should sub-agent inner tool cards be printed? */
|
|
71
|
+
export function showSubAgentTools(mode = getVerbosity()) {
|
|
72
|
+
return mode === MODES.VERBOSE || mode === MODES.SURGICAL;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Should raw model reasoning be printed? */
|
|
76
|
+
export function showReasoning(mode = getVerbosity()) {
|
|
77
|
+
return mode === MODES.SURGICAL;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Should tool cards default to expanded instead of folded? */
|
|
81
|
+
export function defaultExpanded(mode = getVerbosity()) {
|
|
82
|
+
return mode === MODES.SURGICAL;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Should markdown be rendered? (only `surgical` shows raw, others render) */
|
|
86
|
+
export function renderMarkdown(mode = getVerbosity()) {
|
|
87
|
+
return mode !== MODES.SURGICAL;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Per-mode label for /help and status display. */
|
|
91
|
+
export function label(mode = getVerbosity()) {
|
|
92
|
+
switch (mode) {
|
|
93
|
+
case MODES.QUIET: return 'quiet (compact)';
|
|
94
|
+
case MODES.DEFAULT: return 'default';
|
|
95
|
+
case MODES.VERBOSE: return 'verbose (sub-agent tools visible)';
|
|
96
|
+
case MODES.SURGICAL: return 'surgical (everything shown)';
|
|
97
|
+
default: return String(mode || 'default');
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/terminal/ansi.mjs
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ANSI Terminal Renderer —
|
|
2
|
+
* ANSI Terminal Renderer — cursor control, box drawing, status bars.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Color helpers (the `c` object) now route through the semantic palette
|
|
5
|
+
* (`src/ui/palette.mjs`) so the entire CLI honors the Kepler brand and
|
|
6
|
+
* tier fallbacks (truecolor, ansi256, ansi16, none) without touching
|
|
7
|
+
* each call site. Hot-swap-friendly: external semantics like `c.red`,
|
|
8
|
+
* `c.bold`, `c.cyan` are preserved as the legacy contract; new code
|
|
9
|
+
* should prefer importing `paint` directly.
|
|
6
10
|
*/
|
|
7
11
|
|
|
12
|
+
import { paint } from '../ui/palette.mjs';
|
|
13
|
+
|
|
8
14
|
const ESC = '\x1b[';
|
|
9
15
|
const write = (s) => process.stderr.write(s);
|
|
10
16
|
|
|
@@ -27,27 +33,43 @@ export const cursor = {
|
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
// ── Colors ──
|
|
36
|
+
// Legacy color names re-mapped onto semantic palette tokens. The CLI's
|
|
37
|
+
// branding is centralized in palette.mjs; this object is preserved only
|
|
38
|
+
// so existing imports keep compiling. Internal Kepler color choices are
|
|
39
|
+
// documented next to each mapping for the next code review.
|
|
40
|
+
|
|
41
|
+
const identity = (s) => String(s ?? '');
|
|
30
42
|
|
|
31
43
|
export const c = {
|
|
32
|
-
reset:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
reset: identity, // palette already wraps with RESET
|
|
45
|
+
|
|
46
|
+
// Styles — work at every tier
|
|
47
|
+
bold: paint.bold,
|
|
48
|
+
dim: paint.dim,
|
|
49
|
+
italic: paint.italic,
|
|
50
|
+
underline: paint.underline,
|
|
51
|
+
|
|
52
|
+
// State semantics
|
|
53
|
+
red: paint.state.danger, // failure / hard error
|
|
54
|
+
green: paint.state.success, // pass / aligned
|
|
55
|
+
yellow: paint.state.warn, // soft warn / retry
|
|
56
|
+
|
|
57
|
+
// Brand semantics
|
|
58
|
+
blue: paint.brand.primary, // headers, primary brand
|
|
59
|
+
magenta: paint.brand.accent, // attention required
|
|
60
|
+
brand: paint.brand.primary, // primary brand surface
|
|
61
|
+
cyan: paint.brand.data, // code / file paths
|
|
62
|
+
cyanRegular: paint.brand.data,
|
|
63
|
+
cyanBold: (s) => paint.bold(paint.brand.data(s)),
|
|
64
|
+
|
|
65
|
+
// Text semantics
|
|
66
|
+
white: paint.text.primary, // primary text
|
|
67
|
+
gray: paint.text.dim, // hints, metadata, dim text
|
|
68
|
+
|
|
69
|
+
// Backgrounds — kept as raw ANSI; rarely used and have no palette analog
|
|
70
|
+
bgRed: (s) => `${ESC}41m${String(s ?? '')}${ESC}0m`,
|
|
71
|
+
bgGreen: (s) => `${ESC}42m${String(s ?? '')}${ESC}0m`,
|
|
72
|
+
bgCyan: (s) => `${ESC}46m${String(s ?? '')}${ESC}0m`,
|
|
51
73
|
};
|
|
52
74
|
|
|
53
75
|
// ── Box Drawing ──
|
|
@@ -399,7 +421,7 @@ export function formatElapsed(startMs) {
|
|
|
399
421
|
|
|
400
422
|
// ── Format Cost ──
|
|
401
423
|
|
|
402
|
-
import { calculateCost, formatCostValue } from '../core/pricing.mjs';
|
|
424
|
+
import { calculateCost, formatCostValue, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
403
425
|
|
|
404
426
|
/**
|
|
405
427
|
* Format cost from token counts.
|
|
@@ -407,15 +429,13 @@ import { calculateCost, formatCostValue } from '../core/pricing.mjs';
|
|
|
407
429
|
* or a single usage object with optional per-model breakdown.
|
|
408
430
|
*/
|
|
409
431
|
export function formatCost(inputOrUsage, outputTokens) {
|
|
410
|
-
// New API: pass a usage object directly
|
|
411
432
|
if (typeof inputOrUsage === 'object' && inputOrUsage !== null) {
|
|
412
433
|
const { total } = calculateCost(inputOrUsage);
|
|
413
|
-
return
|
|
434
|
+
return formatCredits(costToCredits(total));
|
|
414
435
|
}
|
|
415
|
-
// Legacy API: flat input/output token counts, default pricing
|
|
416
436
|
const { total } = calculateCost({
|
|
417
437
|
input_tokens: inputOrUsage || 0,
|
|
418
438
|
output_tokens: outputTokens || 0,
|
|
419
439
|
});
|
|
420
|
-
return
|
|
440
|
+
return formatCredits(costToCredits(total));
|
|
421
441
|
}
|