tokburn 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/cli.js +7 -2
  2. package/init-ui.mjs +466 -0
  3. package/init.js +67 -1
  4. package/package.json +7 -2
package/cli.js CHANGED
@@ -120,8 +120,13 @@ program
120
120
  .command('init')
121
121
  .description('Interactive setup wizard for Claude Code')
122
122
  .action(async () => {
123
- const { runInit } = require('./init');
124
- await runInit();
123
+ try {
124
+ await import('./init-ui.mjs');
125
+ } catch (err) {
126
+ // Fallback to readline wizard if Ink fails (e.g. non-TTY, missing deps)
127
+ const { runInit } = require('./init');
128
+ await runInit();
129
+ }
125
130
  });
126
131
 
127
132
  program
package/init-ui.mjs ADDED
@@ -0,0 +1,466 @@
1
+ /**
2
+ * tokburn -- init-ui.mjs
3
+ * Ink-based setup wizard for `tokburn init`.
4
+ * ESM module using Ink 6.x, @inkjs/ui v2, React 19.
5
+ */
6
+
7
+ import React, { useState, useEffect, useCallback } from 'react';
8
+ import { render, Box, Text, Newline, useApp } from 'ink';
9
+ import { Select, Spinner, ProgressBar } from '@inkjs/ui';
10
+ import chalk from 'chalk';
11
+ import { createRequire } from 'node:module';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const { detectEnvironment, configurePlan, configureProxy, configureShell, configureStatusLine, PLANS } = require('./init.js');
15
+ const { getConfig } = require('./config.js');
16
+ const { PRESETS, MODULE_LIST } = require('./statusline.js');
17
+
18
+ const pkg = require('./package.json');
19
+
20
+ // ── ASCII Logo ──────────────────────────────────────────────────────────────
21
+
22
+ const LOGO = ` _ _
23
+ | |_ ___ | | __ ___ _ _ _ __ _ __
24
+ | __/ _ \\| |/ /| _ | | | | '__| '_ \\
25
+ | || (_) | < | _ | |_| | | | | | |
26
+ \\__\\___/|_|\\_\\|___|\\__,_|_| |_| |_|`;
27
+
28
+ // ── Helpers ─────────────────────────────────────────────────────────────────
29
+
30
+ const PLAN_LABELS = {
31
+ pro: 'Pro (~500K/5hr)',
32
+ max: 'Max (~2M/5hr)',
33
+ api: 'API only',
34
+ };
35
+
36
+ function dots(label, value, width = 40) {
37
+ const used = label.length + value.length + 2;
38
+ const count = Math.max(2, width - used);
39
+ return chalk.dim('.'.repeat(count));
40
+ }
41
+
42
+ function completedLine(label, value) {
43
+ return ` ${chalk.green('\u2713')} ${label} ${dots(label, value)} ${chalk.bold(value)}`;
44
+ }
45
+
46
+ function previewForPreset(presetKey) {
47
+ if (presetKey === 'skip') return '(no status line)';
48
+ const modules = PRESETS[presetKey];
49
+ if (!modules) return '';
50
+ const lineOne = [];
51
+ const extra = [];
52
+ for (const key of modules) {
53
+ const info = MODULE_LIST.find(m => m.key === key);
54
+ if (!info) continue;
55
+ if (key === 'current_limit' || key === 'weekly_limit') {
56
+ extra.push(info.example);
57
+ } else {
58
+ lineOne.push(info.example);
59
+ }
60
+ }
61
+ let out = '';
62
+ if (lineOne.length > 0) out += lineOne.join(' | ');
63
+ for (const e of extra) out += '\n' + e;
64
+ return out;
65
+ }
66
+
67
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
68
+
69
+ // ── Phase constants ─────────────────────────────────────────────────────────
70
+
71
+ const PHASE_WELCOME = 'welcome';
72
+ const PHASE_PLAN = 'plan';
73
+ const PHASE_PROXY = 'proxy';
74
+ const PHASE_SHELL = 'shell';
75
+ const PHASE_STATUSLINE = 'statusline';
76
+ const PHASE_PROCESSING = 'processing';
77
+ const PHASE_DONE = 'done';
78
+
79
+ // ── Main App ────────────────────────────────────────────────────────────────
80
+
81
+ function App() {
82
+ const { exit } = useApp();
83
+ const [phase, setPhase] = useState(PHASE_WELCOME);
84
+ const [env, setEnv] = useState(null);
85
+
86
+ // User choices
87
+ const [plan, setPlan] = useState(null);
88
+ const [wantProxy, setWantProxy] = useState(null);
89
+ const [wantShell, setWantShell] = useState(null);
90
+ const [statusPreset, setStatusPreset] = useState(null);
91
+
92
+ // Processing state
93
+ const [taskIndex, setTaskIndex] = useState(-1);
94
+ const [taskResults, setTaskResults] = useState({});
95
+ const [progress, setProgress] = useState(0);
96
+
97
+ // Detect environment on mount
98
+ useEffect(() => {
99
+ try {
100
+ const detected = detectEnvironment();
101
+ setEnv(detected);
102
+ } catch (e) {
103
+ setEnv({ home: '', shell: 'unknown', rcFile: '', rcPath: '', claudeDir: '', claudeSettings: '', hasClaudeCode: false });
104
+ }
105
+ }, []);
106
+
107
+ // Auto-advance from welcome after a short pause
108
+ useEffect(() => {
109
+ if (phase === PHASE_WELCOME && env) {
110
+ const t = setTimeout(() => setPhase(PHASE_PLAN), 100);
111
+ return () => clearTimeout(t);
112
+ }
113
+ }, [phase, env]);
114
+
115
+ // Run processing tasks
116
+ useEffect(() => {
117
+ if (phase !== PHASE_PROCESSING) return;
118
+ let cancelled = false;
119
+
120
+ async function runTasks() {
121
+ const tasks = buildTaskList();
122
+ const results = {};
123
+
124
+ for (let i = 0; i < tasks.length; i++) {
125
+ if (cancelled) return;
126
+ setTaskIndex(i);
127
+ setProgress(Math.round(((i) / tasks.length) * 100));
128
+ await sleep(200);
129
+
130
+ try {
131
+ const result = await tasks[i].run();
132
+ results[tasks[i].key] = { success: true, ...(result || {}) };
133
+ } catch (e) {
134
+ results[tasks[i].key] = { success: false, error: e.message };
135
+ }
136
+ }
137
+
138
+ if (cancelled) return;
139
+ setTaskResults(results);
140
+ setProgress(100);
141
+ setTaskIndex(tasks.length);
142
+ await sleep(300);
143
+ setPhase(PHASE_DONE);
144
+ }
145
+
146
+ runTasks();
147
+ return () => { cancelled = true; };
148
+ }, [phase]);
149
+
150
+ // Exit after done phase renders
151
+ useEffect(() => {
152
+ if (phase === PHASE_DONE) {
153
+ const t = setTimeout(() => exit(), 500);
154
+ return () => clearTimeout(t);
155
+ }
156
+ }, [phase, exit]);
157
+
158
+ // ── Task builders ───────────────────────────────────────────────────────
159
+
160
+ function buildTaskList() {
161
+ const tasks = [];
162
+
163
+ tasks.push({
164
+ key: 'plan',
165
+ label: 'Plan configured',
166
+ run: () => {
167
+ configurePlan(plan);
168
+ return {};
169
+ },
170
+ });
171
+
172
+ if (wantProxy) {
173
+ tasks.push({
174
+ key: 'proxy',
175
+ label: 'Proxy started',
176
+ run: () => {
177
+ const result = configureProxy();
178
+ return { pid: result.pid, message: result.message };
179
+ },
180
+ });
181
+ }
182
+
183
+ if (wantShell && env && env.hasClaudeCode) {
184
+ tasks.push({
185
+ key: 'shell',
186
+ label: 'Shell configured',
187
+ run: () => {
188
+ const config = getConfig();
189
+ const result = configureShell(env.rcPath, config.port);
190
+ return { added: result.added, reason: result.reason };
191
+ },
192
+ });
193
+ }
194
+
195
+ if (statusPreset && statusPreset !== 'skip' && env && env.hasClaudeCode) {
196
+ tasks.push({
197
+ key: 'statusline',
198
+ label: 'Status line configured',
199
+ run: () => {
200
+ const modules = PRESETS[statusPreset];
201
+ configureStatusLine(modules);
202
+ return { count: modules.length };
203
+ },
204
+ });
205
+ }
206
+
207
+ return tasks;
208
+ }
209
+
210
+ // ── Handlers ────────────────────────────────────────────────────────────
211
+
212
+ const handlePlanSelect = useCallback((value) => {
213
+ setPlan(value);
214
+ setPhase(PHASE_PROXY);
215
+ }, []);
216
+
217
+ const handleProxySelect = useCallback((value) => {
218
+ setWantProxy(value === 'yes');
219
+ if (env && env.hasClaudeCode) {
220
+ setPhase(PHASE_SHELL);
221
+ } else {
222
+ // Skip shell and statusline if no Claude Code
223
+ setWantShell(false);
224
+ setStatusPreset('skip');
225
+ setPhase(PHASE_PROCESSING);
226
+ }
227
+ }, [env]);
228
+
229
+ const handleShellSelect = useCallback((value) => {
230
+ setWantShell(value === 'yes');
231
+ if (env && env.hasClaudeCode) {
232
+ setPhase(PHASE_STATUSLINE);
233
+ } else {
234
+ setStatusPreset('skip');
235
+ setPhase(PHASE_PROCESSING);
236
+ }
237
+ }, [env]);
238
+
239
+ const handleStatusLineSelect = useCallback((value) => {
240
+ setStatusPreset(value);
241
+ setPhase(PHASE_PROCESSING);
242
+ }, []);
243
+
244
+ // ── Render ──────────────────────────────────────────────────────────────
245
+
246
+ if (!env) {
247
+ return React.createElement(Box, { paddingX: 2 },
248
+ React.createElement(Spinner, { label: 'Detecting environment...' })
249
+ );
250
+ }
251
+
252
+ return React.createElement(Box, { flexDirection: 'column', paddingX: 1 },
253
+ // Logo + header (always shown)
254
+ React.createElement(Box, { flexDirection: 'column' },
255
+ React.createElement(Text, { color: 'yellow' }, LOGO),
256
+ React.createElement(Text, { dimColor: true }, ` token tracking for Claude Code v${pkg.version}`),
257
+ React.createElement(Newline, null),
258
+ React.createElement(Text, { dimColor: true }, ' ' + '\u2500'.repeat(44)),
259
+ React.createElement(Newline, null),
260
+ React.createElement(Text, null,
261
+ ' Detected: ',
262
+ React.createElement(Text, { bold: true }, env.shell),
263
+ env.hasClaudeCode
264
+ ? React.createElement(Text, null, ' + ', React.createElement(Text, { color: 'cyan' }, 'Claude Code'))
265
+ : React.createElement(Text, { dimColor: true }, ' (no Claude Code)')
266
+ ),
267
+ React.createElement(Text, null,
268
+ ' Works with: ',
269
+ React.createElement(Text, { color: 'cyan' }, 'Claude Code'),
270
+ React.createElement(Text, { dimColor: true }, ' | Codex, Cursor -- coming soon')
271
+ ),
272
+ React.createElement(Newline, null)
273
+ ),
274
+
275
+ // Completed steps
276
+ plan ? React.createElement(Text, null, completedLine('Plan', PLAN_LABELS[plan])) : null,
277
+ wantProxy !== null ? React.createElement(Text, null, completedLine('Proxy', wantProxy ? 'start now' : 'skipped')) : null,
278
+ wantShell !== null ? React.createElement(Text, null, completedLine('Shell', wantShell ? `add to ~/${env.rcFile}` : 'manual')) : null,
279
+ statusPreset !== null && phase !== PHASE_STATUSLINE ? React.createElement(Text, null, completedLine('Status line', statusPreset === 'skip' ? 'skipped' : `${statusPreset[0].toUpperCase() + statusPreset.slice(1)} (${(PRESETS[statusPreset] || []).length})`)) : null,
280
+
281
+ // Active phase
282
+ phase === PHASE_PLAN ? React.createElement(PlanStep, { onSelect: handlePlanSelect }) : null,
283
+ phase === PHASE_PROXY ? React.createElement(ProxyStep, { onSelect: handleProxySelect }) : null,
284
+ phase === PHASE_SHELL ? React.createElement(ShellStep, { onSelect: handleShellSelect, rcFile: env.rcFile }) : null,
285
+ phase === PHASE_STATUSLINE ? React.createElement(StatusLineStep, { onSelect: handleStatusLineSelect }) : null,
286
+ phase === PHASE_PROCESSING ? React.createElement(ProcessingPhase, {
287
+ tasks: buildTaskList(),
288
+ taskIndex,
289
+ taskResults,
290
+ progress,
291
+ }) : null,
292
+ phase === PHASE_DONE ? React.createElement(DoneSummary, {
293
+ plan,
294
+ wantProxy,
295
+ wantShell,
296
+ statusPreset,
297
+ env,
298
+ taskResults,
299
+ }) : null
300
+ );
301
+ }
302
+
303
+ // ── Step Components ─────────────────────────────────────────────────────────
304
+
305
+ function PlanStep({ onSelect }) {
306
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 0 },
307
+ React.createElement(Text, { bold: true }, ' [1/4] Which Claude plan are you on?'),
308
+ React.createElement(Newline, null),
309
+ React.createElement(Box, { paddingLeft: 4 },
310
+ React.createElement(Select, {
311
+ options: [
312
+ { label: 'Pro ~500K tokens / 5hr window', value: 'pro' },
313
+ { label: 'Max ~2M tokens / 5hr window', value: 'max' },
314
+ { label: 'API only (no plan limits)', value: 'api' },
315
+ ],
316
+ onChange: onSelect,
317
+ })
318
+ )
319
+ );
320
+ }
321
+
322
+ function ProxyStep({ onSelect }) {
323
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 0 },
324
+ React.createElement(Text, { bold: true }, ' [2/4] Start the proxy daemon?'),
325
+ React.createElement(Text, { dimColor: true }, ' Enables per-request tracking for detailed breakdowns.'),
326
+ React.createElement(Newline, null),
327
+ React.createElement(Box, { paddingLeft: 4 },
328
+ React.createElement(Select, {
329
+ options: [
330
+ { label: 'Yes, start now', value: 'yes' },
331
+ { label: 'No, skip', value: 'no' },
332
+ ],
333
+ onChange: onSelect,
334
+ })
335
+ )
336
+ );
337
+ }
338
+
339
+ function ShellStep({ onSelect, rcFile }) {
340
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 0 },
341
+ React.createElement(Text, { bold: true }, ` [3/4] Add ANTHROPIC_BASE_URL to ~/${rcFile}?`),
342
+ React.createElement(Text, { dimColor: true }, ' Required for the proxy to intercept API calls.'),
343
+ React.createElement(Newline, null),
344
+ React.createElement(Box, { paddingLeft: 4 },
345
+ React.createElement(Select, {
346
+ options: [
347
+ { label: 'Yes, add it', value: 'yes' },
348
+ { label: 'No, I\'ll do it manually', value: 'no' },
349
+ ],
350
+ onChange: onSelect,
351
+ })
352
+ )
353
+ );
354
+ }
355
+
356
+ function StatusLineStep({ onSelect }) {
357
+ const [selected, setSelected] = useState(null);
358
+
359
+ const handleChange = useCallback((value) => {
360
+ setSelected(value);
361
+ onSelect(value);
362
+ }, [onSelect]);
363
+
364
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 0 },
365
+ React.createElement(Text, { bold: true }, ' [4/4] Configure Claude Code status line?'),
366
+ React.createElement(Newline, null),
367
+ React.createElement(Box, { paddingLeft: 4 },
368
+ React.createElement(Select, {
369
+ options: [
370
+ { label: 'Recommended model | ctx% | repo | limits | cost', value: 'recommended' },
371
+ { label: 'Minimal model | current rate limit', value: 'minimal' },
372
+ { label: 'Full everything including burn rate', value: 'full' },
373
+ { label: 'Skip', value: 'skip' },
374
+ ],
375
+ onChange: handleChange,
376
+ })
377
+ ),
378
+ // Preview box shown after selection or for the default
379
+ !selected ? React.createElement(Box, {
380
+ borderStyle: 'round',
381
+ borderColor: 'gray',
382
+ paddingX: 1,
383
+ marginTop: 1,
384
+ marginLeft: 4,
385
+ },
386
+ React.createElement(Box, { flexDirection: 'column' },
387
+ React.createElement(Text, { dimColor: true }, 'Preview (Recommended):'),
388
+ ...previewForPreset('recommended').split('\n').map((line, i) =>
389
+ React.createElement(Text, { key: `p-${i}` }, line)
390
+ )
391
+ )
392
+ ) : null
393
+ );
394
+ }
395
+
396
+ // ── Processing Phase ────────────────────────────────────────────────────────
397
+
398
+ function ProcessingPhase({ tasks, taskIndex, taskResults, progress }) {
399
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
400
+ React.createElement(Text, { bold: true }, ' Setting up tokburn...'),
401
+ React.createElement(Newline, null),
402
+ React.createElement(Box, { paddingLeft: 2 },
403
+ React.createElement(ProgressBar, { value: progress })
404
+ ),
405
+ React.createElement(Newline, null),
406
+ ...tasks.map((task, i) => {
407
+ if (i < taskIndex) {
408
+ // Done
409
+ return React.createElement(Text, { key: task.key },
410
+ ' ', React.createElement(Text, { color: 'green' }, '\u2713'),
411
+ ' ', task.label,
412
+ taskResults[task.key] && taskResults[task.key].pid ? ` (PID ${taskResults[task.key].pid})` : ''
413
+ );
414
+ } else if (i === taskIndex) {
415
+ // Active
416
+ return React.createElement(Box, { key: task.key },
417
+ React.createElement(Text, null, ' '),
418
+ React.createElement(Spinner, { label: task.label })
419
+ );
420
+ } else {
421
+ // Pending
422
+ return React.createElement(Text, { key: task.key, dimColor: true },
423
+ ' \u25CB ', task.label
424
+ );
425
+ }
426
+ })
427
+ );
428
+ }
429
+
430
+ // ── Done Summary ────────────────────────────────────────────────────────────
431
+
432
+ function DoneSummary({ plan, wantProxy, wantShell, statusPreset, env, taskResults }) {
433
+ const proxyResult = taskResults.proxy || {};
434
+ const shellResult = taskResults.shell || {};
435
+
436
+ const proxyDesc = !wantProxy ? 'skipped'
437
+ : proxyResult.pid ? `started :${proxyResult.pid}`
438
+ : proxyResult.message || 'started';
439
+
440
+ const shellDesc = !wantShell ? 'skipped'
441
+ : shellResult.added ? `added to ~/${env.rcFile}`
442
+ : shellResult.reason === 'already exists' ? 'already in rc'
443
+ : 'configured';
444
+
445
+ const statusDesc = !statusPreset || statusPreset === 'skip' ? 'skipped'
446
+ : `${statusPreset[0].toUpperCase() + statusPreset.slice(1)} (${(PRESETS[statusPreset] || []).length})`;
447
+
448
+ return React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
449
+ React.createElement(Text, { dimColor: true }, ` ${'─'.repeat(2)} Setup complete ${'─'.repeat(25)}`),
450
+ React.createElement(Newline, null),
451
+ React.createElement(Text, null, completedLine('Plan', PLAN_LABELS[plan])),
452
+ React.createElement(Text, null, completedLine('Proxy', proxyDesc)),
453
+ React.createElement(Text, null, completedLine('Shell', shellDesc)),
454
+ React.createElement(Text, null, completedLine('Status line', statusDesc)),
455
+ React.createElement(Newline, null),
456
+ React.createElement(Text, { bold: true }, ' Try these:'),
457
+ React.createElement(Text, null, ' ', React.createElement(Text, { color: 'cyan' }, 'tokburn status'), ' check everything'),
458
+ React.createElement(Text, null, ' ', React.createElement(Text, { color: 'cyan' }, 'tokburn today'), ' see today\'s usage'),
459
+ React.createElement(Text, null, ' ', React.createElement(Text, { color: 'cyan' }, 'tokburn live'), ' real-time dashboard'),
460
+ React.createElement(Newline, null)
461
+ );
462
+ }
463
+
464
+ // ── Entry point ─────────────────────────────────────────────────────────────
465
+
466
+ render(React.createElement(App));
package/init.js CHANGED
@@ -257,4 +257,70 @@ async function runInit() {
257
257
  rl.close();
258
258
  }
259
259
 
260
- module.exports = { runInit };
260
+ // ── Exported config functions (used by Ink UI) ──────────────────────────────
261
+
262
+ function detectEnvironment() {
263
+ const home = process.env.HOME || process.env.USERPROFILE;
264
+ const shell = path.basename(process.env.SHELL || 'bash');
265
+ const rcFile = shell === 'zsh' ? '.zshrc' : shell === 'fish' ? '.config/fish/config.fish' : '.bashrc';
266
+ const rcPath = path.join(home, rcFile);
267
+ const claudeDir = path.join(home, '.claude');
268
+ const claudeSettings = path.join(claudeDir, 'settings.json');
269
+ const hasClaudeCode = fs.existsSync(claudeDir);
270
+
271
+ return { home, shell, rcFile, rcPath, claudeDir, claudeSettings, hasClaudeCode };
272
+ }
273
+
274
+ function configurePlan(plan) {
275
+ setConfig({ plan, limits: PLANS });
276
+ }
277
+
278
+ function configureProxy() {
279
+ if (isRunning()) {
280
+ return { success: true, message: 'already running', pid: null };
281
+ }
282
+ return startDaemon();
283
+ }
284
+
285
+ function configureShell(rcPath, port) {
286
+ const envLine = `export ANTHROPIC_BASE_URL=http://127.0.0.1:${port}`;
287
+ const existing = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, 'utf8') : '';
288
+ if (existing.includes('ANTHROPIC_BASE_URL')) {
289
+ return { added: false, reason: 'already exists' };
290
+ }
291
+ fs.appendFileSync(rcPath, '\n# tokburn proxy\n' + envLine + '\n');
292
+ return { added: true };
293
+ }
294
+
295
+ function configureStatusLine(selectedModules) {
296
+ const home = process.env.HOME || process.env.USERPROFILE;
297
+ const claudeDir = path.join(home, '.claude');
298
+ const claudeSettings = path.join(claudeDir, 'settings.json');
299
+
300
+ setConfig({ statusline_modules: selectedModules });
301
+
302
+ const srcScript = path.join(__dirname, 'statusline.js');
303
+ const destScript = path.join(claudeDir, 'tokburn-statusline.js');
304
+
305
+ if (fs.existsSync(srcScript)) {
306
+ fs.copyFileSync(srcScript, destScript);
307
+ fs.chmodSync(destScript, '755');
308
+ }
309
+
310
+ let settings = {};
311
+ if (fs.existsSync(claudeSettings)) {
312
+ try { settings = JSON.parse(fs.readFileSync(claudeSettings, 'utf8')); } catch (_) {}
313
+ }
314
+
315
+ settings.statusLine = { type: 'command', command: destScript };
316
+
317
+ if (!fs.existsSync(claudeDir)) {
318
+ fs.mkdirSync(claudeDir, { recursive: true });
319
+ }
320
+ fs.writeFileSync(claudeSettings, JSON.stringify(settings, null, 2) + '\n');
321
+ }
322
+
323
+ module.exports = {
324
+ runInit, detectEnvironment, configurePlan, configureProxy,
325
+ configureShell, configureStatusLine, PLANS
326
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokburn",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "See exactly how fast you're burning tokens and money across Claude Code sessions",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "costs.js",
16
16
  "config.js",
17
17
  "init.js",
18
+ "init-ui.mjs",
18
19
  "card.js",
19
20
  "statusline.js",
20
21
  "statusline.sh",
@@ -55,6 +56,10 @@
55
56
  "node": ">=18.0.0"
56
57
  },
57
58
  "dependencies": {
58
- "commander": "^12.0.0"
59
+ "@inkjs/ui": "^2.0.0",
60
+ "chalk": "^5.6.2",
61
+ "commander": "^12.0.0",
62
+ "ink": "^6.8.0",
63
+ "react": "^19.2.4"
59
64
  }
60
65
  }