golem-cc 2.1.2 → 3.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/.claude/commands/golem/build.md +18 -0
- package/.claude/commands/golem/config.md +39 -0
- package/.claude/commands/golem/continue.md +73 -0
- package/.claude/commands/golem/doctor.md +46 -0
- package/.claude/commands/golem/document.md +138 -0
- package/.claude/commands/golem/help.md +58 -0
- package/.claude/commands/golem/pause.md +130 -0
- package/.claude/commands/golem/plan.md +111 -0
- package/.claude/commands/golem/review.md +166 -0
- package/.claude/commands/golem/security.md +186 -0
- package/.claude/commands/golem/simplify.md +76 -0
- package/.claude/commands/golem/spec.md +105 -0
- package/.claude/commands/golem/status.md +33 -0
- package/.golem/agents/code-simplifier.md +54 -0
- package/.golem/agents/review-architecture.md +59 -0
- package/.golem/agents/review-logic.md +50 -0
- package/.golem/agents/review-security.md +50 -0
- package/.golem/agents/review-style.md +48 -0
- package/.golem/agents/review-tests.md +48 -0
- package/.golem/agents/spec-builder.md +60 -0
- package/.golem/bin/golem.mjs +270 -0
- package/.golem/lib/build.mjs +557 -0
- package/.golem/lib/claude.mjs +95 -0
- package/.golem/lib/config.mjs +421 -0
- package/.golem/lib/display.mjs +191 -0
- package/.golem/lib/doctor.mjs +197 -0
- package/.golem/lib/document.mjs +792 -0
- package/.golem/lib/gates.mjs +78 -0
- package/.golem/lib/init.mjs +166 -0
- package/.golem/lib/output.mjs +40 -0
- package/.golem/lib/ratelimit.mjs +86 -0
- package/.golem/lib/security.mjs +603 -0
- package/.golem/lib/simplify.mjs +101 -0
- package/.golem/lib/tui.mjs +368 -0
- package/.golem/lib/usage.mjs +119 -0
- package/.golem/lib/worktree.mjs +509 -0
- package/.golem/prompts/build.md +23 -0
- package/.golem/prompts/document-inline.md +66 -0
- package/.golem/prompts/document-markdown.md +80 -0
- package/.golem/prompts/simplify.md +35 -0
- package/README.md +141 -142
- package/bin/golem-shim.mjs +36 -0
- package/bin/install.mjs +193 -0
- package/package.json +27 -32
- package/.env.example +0 -17
- package/bin/golem +0 -1040
- package/commands/golem/build.md +0 -235
- package/commands/golem/config.md +0 -55
- package/commands/golem/doctor.md +0 -137
- package/commands/golem/help.md +0 -212
- package/commands/golem/plan.md +0 -214
- package/commands/golem/review.md +0 -376
- package/commands/golem/security.md +0 -204
- package/commands/golem/simplify.md +0 -94
- package/commands/golem/spec.md +0 -226
- package/commands/golem/status.md +0 -60
- package/dist/api/freshworks.d.ts +0 -61
- package/dist/api/freshworks.d.ts.map +0 -1
- package/dist/api/freshworks.js +0 -119
- package/dist/api/freshworks.js.map +0 -1
- package/dist/api/gitea.d.ts +0 -96
- package/dist/api/gitea.d.ts.map +0 -1
- package/dist/api/gitea.js +0 -154
- package/dist/api/gitea.js.map +0 -1
- package/dist/cli/index.d.ts +0 -9
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -352
- package/dist/cli/index.js.map +0 -1
- package/dist/sync/ticket-sync.d.ts +0 -53
- package/dist/sync/ticket-sync.d.ts.map +0 -1
- package/dist/sync/ticket-sync.js +0 -226
- package/dist/sync/ticket-sync.js.map +0 -1
- package/dist/types.d.ts +0 -125
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/worktree/manager.d.ts +0 -54
- package/dist/worktree/manager.d.ts.map +0 -1
- package/dist/worktree/manager.js +0 -190
- package/dist/worktree/manager.js.map +0 -1
- package/golem/agents/code-simplifier.md +0 -81
- package/golem/agents/spec-builder.md +0 -90
- package/golem/prompts/PROMPT_build.md +0 -71
- package/golem/prompts/PROMPT_plan.md +0 -102
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { sparkline, formatTokens } from './usage.mjs';
|
|
3
|
+
import { formatResetTime } from './ratelimit.mjs';
|
|
4
|
+
|
|
5
|
+
const ESC = '\x1b';
|
|
6
|
+
const write = (s) => process.stdout.write(s);
|
|
7
|
+
|
|
8
|
+
// ANSI helpers
|
|
9
|
+
const altScreenOn = () => write(`${ESC}[?1049h`);
|
|
10
|
+
const altScreenOff = () => write(`${ESC}[?1049l`);
|
|
11
|
+
const mouseOn = () => write(`${ESC}[?1000h${ESC}[?1006h`);
|
|
12
|
+
const mouseOff = () => write(`${ESC}[?1000l${ESC}[?1006l`);
|
|
13
|
+
const cursorHide = () => write(`${ESC}[?25l`);
|
|
14
|
+
const cursorShow = () => write(`${ESC}[?25h`);
|
|
15
|
+
const moveTo = (row, col = 1) => write(`${ESC}[${row};${col}H`);
|
|
16
|
+
const clearLine = () => write(`${ESC}[K`);
|
|
17
|
+
const setScrollRegion = (top, bottom) => write(`${ESC}[${top};${bottom}r`);
|
|
18
|
+
const resetScrollRegion = () => write(`${ESC}[r`);
|
|
19
|
+
const saveCursor = () => write(`${ESC}7`);
|
|
20
|
+
const restoreCursor = () => write(`${ESC}8`);
|
|
21
|
+
|
|
22
|
+
const HEADER_LINES = 8;
|
|
23
|
+
|
|
24
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
25
|
+
|
|
26
|
+
function wordWrap(str, maxWidth, indent = 0) {
|
|
27
|
+
if (maxWidth <= 0) return [str];
|
|
28
|
+
const visible = stripAnsi(str);
|
|
29
|
+
if (visible.length <= maxWidth) return [str];
|
|
30
|
+
|
|
31
|
+
const lines = [];
|
|
32
|
+
let visPos = 0;
|
|
33
|
+
let strPos = 0;
|
|
34
|
+
let lineStart = 0;
|
|
35
|
+
let lineVisStart = 0;
|
|
36
|
+
let lastSpaceVis = -1;
|
|
37
|
+
let lastSpaceStr = -1;
|
|
38
|
+
const prefix = ' '.repeat(indent);
|
|
39
|
+
|
|
40
|
+
while (strPos < str.length) {
|
|
41
|
+
// Skip ANSI sequences
|
|
42
|
+
const ansiMatch = str.slice(strPos).match(/^\x1b\[[0-9;]*m/);
|
|
43
|
+
if (ansiMatch) {
|
|
44
|
+
strPos += ansiMatch[0].length;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (str[strPos] === ' ') {
|
|
49
|
+
lastSpaceVis = visPos;
|
|
50
|
+
lastSpaceStr = strPos;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
visPos++;
|
|
54
|
+
strPos++;
|
|
55
|
+
|
|
56
|
+
const lineWidth = lines.length === 0 ? maxWidth : maxWidth - indent;
|
|
57
|
+
if (visPos - lineVisStart >= lineWidth) {
|
|
58
|
+
if (lastSpaceStr > lineStart) {
|
|
59
|
+
// Break at last space
|
|
60
|
+
lines.push(str.slice(lineStart, lastSpaceStr));
|
|
61
|
+
lineStart = lastSpaceStr + 1;
|
|
62
|
+
lineVisStart = lastSpaceVis + 1;
|
|
63
|
+
} else {
|
|
64
|
+
// No space found, hard break
|
|
65
|
+
lines.push(str.slice(lineStart, strPos));
|
|
66
|
+
lineStart = strPos;
|
|
67
|
+
lineVisStart = visPos;
|
|
68
|
+
}
|
|
69
|
+
lastSpaceVis = -1;
|
|
70
|
+
lastSpaceStr = -1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (lineStart < str.length) {
|
|
75
|
+
lines.push(str.slice(lineStart));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return lines.map((l, i) => i === 0 ? l : prefix + l);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createTui() {
|
|
82
|
+
let rows = process.stdout.rows || 24;
|
|
83
|
+
let cols = process.stdout.columns || 80;
|
|
84
|
+
let timer = null;
|
|
85
|
+
let startTime = Date.now();
|
|
86
|
+
let cumulativeMs = 0;
|
|
87
|
+
let destroyed = false;
|
|
88
|
+
|
|
89
|
+
// State for header
|
|
90
|
+
let state = {
|
|
91
|
+
taskId: '',
|
|
92
|
+
taskTitle: '',
|
|
93
|
+
stage: '',
|
|
94
|
+
done: 0,
|
|
95
|
+
total: 0,
|
|
96
|
+
attempt: 0,
|
|
97
|
+
maxAttempts: 0,
|
|
98
|
+
claudeCalls: 0,
|
|
99
|
+
inputTokens: 0,
|
|
100
|
+
outputTokens: 0,
|
|
101
|
+
todaySparkline: '',
|
|
102
|
+
todayTotal: '0',
|
|
103
|
+
weekSparkline: '',
|
|
104
|
+
weekTotal: '0',
|
|
105
|
+
opus5hPct: null,
|
|
106
|
+
opus5hResetsAt: null,
|
|
107
|
+
opus7dPct: null,
|
|
108
|
+
opus7dResetsAt: null,
|
|
109
|
+
sonnet5hPct: null,
|
|
110
|
+
sonnet5hResetsAt: null,
|
|
111
|
+
sonnet7dPct: null,
|
|
112
|
+
sonnet7dResetsAt: null,
|
|
113
|
+
taskModel: '',
|
|
114
|
+
extraUsed: null,
|
|
115
|
+
extraLimit: null,
|
|
116
|
+
extraPct: null,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function formatElapsed() {
|
|
120
|
+
const secs = Math.floor((Date.now() - startTime) / 1000);
|
|
121
|
+
const m = Math.floor(secs / 60);
|
|
122
|
+
const s = secs % 60;
|
|
123
|
+
return `${m}m ${String(s).padStart(2, '0')}s`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function progressBar(done, total, width) {
|
|
127
|
+
if (total === 0) return '░'.repeat(width);
|
|
128
|
+
const filled = Math.round((done / total) * width);
|
|
129
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pctStr(pct) {
|
|
133
|
+
return String(Math.round(pct)).padStart(3) + '%';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function colorPct(pct) {
|
|
137
|
+
const colorFn = pct >= 80 ? chalk.red : pct >= 50 ? chalk.yellow : chalk.green;
|
|
138
|
+
return colorFn(pctStr(pct));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function modelLine(name, pct5h, reset5h, pct7d, reset7d, isCurrent) {
|
|
142
|
+
const label = isCurrent ? chalk.white.bold(name.padEnd(6)) : chalk.dim(name.padEnd(6));
|
|
143
|
+
const marker = isCurrent ? chalk.white(' ←') : '';
|
|
144
|
+
const r5 = reset5h ? ' ' + formatResetTime(reset5h) : '';
|
|
145
|
+
const r7 = reset7d ? ' ' + formatResetTime(reset7d) : '';
|
|
146
|
+
const sep = chalk.dim(' │ ');
|
|
147
|
+
return ` ${label}${sep}${chalk.dim('5h')} ${colorPct(pct5h)}${chalk.dim(r5)}${sep}${chalk.dim('7d')} ${colorPct(pct7d)}${chalk.dim(r7)}${marker}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function drawHeader() {
|
|
151
|
+
if (destroyed) return;
|
|
152
|
+
saveCursor();
|
|
153
|
+
|
|
154
|
+
const pct = state.total > 0 ? Math.round((state.done / state.total) * 100) : 0;
|
|
155
|
+
const barWidth = Math.min(20, cols - 40);
|
|
156
|
+
const bar = progressBar(state.done, state.total, barWidth);
|
|
157
|
+
|
|
158
|
+
// Line 1: Title + progress bar
|
|
159
|
+
moveTo(1);
|
|
160
|
+
clearLine();
|
|
161
|
+
write(` ${chalk.bold.cyan('GOLEM BUILD')} ${chalk.green(bar)} ${state.done}/${state.total} (${pct}%)`);
|
|
162
|
+
|
|
163
|
+
// Line 2: Stage + claude call count
|
|
164
|
+
moveTo(2);
|
|
165
|
+
clearLine();
|
|
166
|
+
const stagePart = state.stage ? ` ${chalk.dim(state.stage)}` : '';
|
|
167
|
+
const callsPart = state.claudeCalls > 0 ? ` ${chalk.dim('calls:')} ${chalk.white(String(state.claudeCalls))}` : '';
|
|
168
|
+
write(`${stagePart}${callsPart}`);
|
|
169
|
+
|
|
170
|
+
// Line 3: Task info + attempt counter + timer
|
|
171
|
+
moveTo(3);
|
|
172
|
+
clearLine();
|
|
173
|
+
const taskLabel = state.taskId ? `Task ${state.taskId}: ${state.taskTitle}` : '';
|
|
174
|
+
const elapsed = formatElapsed();
|
|
175
|
+
const showAttempt = state.maxAttempts > 1 || state.attempt > 1;
|
|
176
|
+
const attemptStr = showAttempt ? `attempt ${state.attempt}/${state.maxAttempts}` : '';
|
|
177
|
+
const reservedWidth = elapsed.length + 6 + (showAttempt ? attemptStr.length + 4 : 0);
|
|
178
|
+
const taskStr = taskLabel.slice(0, Math.max(10, cols - reservedWidth - 4));
|
|
179
|
+
const attemptColored = showAttempt
|
|
180
|
+
? (state.attempt >= 3 ? chalk.red : state.attempt === 2 ? chalk.yellow : chalk.green)(attemptStr)
|
|
181
|
+
: '';
|
|
182
|
+
const rightSide = (showAttempt ? attemptColored + ' ' : '') + chalk.dim('⏱') + ' ' + chalk.yellow(elapsed);
|
|
183
|
+
const rightLen = (showAttempt ? attemptStr.length + 2 : 0) + 2 + elapsed.length;
|
|
184
|
+
const gap = Math.max(1, cols - taskStr.length - rightLen - 2);
|
|
185
|
+
write(` ${chalk.white(taskStr)}${' '.repeat(gap)}${rightSide}`)
|
|
186
|
+
|
|
187
|
+
// Lines 4-5: Per-model rate limits or sparklines fallback
|
|
188
|
+
moveTo(4);
|
|
189
|
+
clearLine();
|
|
190
|
+
moveTo(5);
|
|
191
|
+
clearLine();
|
|
192
|
+
if (state.opus5hPct !== null) {
|
|
193
|
+
const cur = (state.taskModel || '').toLowerCase();
|
|
194
|
+
moveTo(4);
|
|
195
|
+
write(modelLine('opus', state.opus5hPct, state.opus5hResetsAt, state.opus7dPct, state.opus7dResetsAt, cur.includes('opus')));
|
|
196
|
+
moveTo(5);
|
|
197
|
+
write(modelLine('sonnet', state.sonnet5hPct, state.sonnet5hResetsAt, state.sonnet7dPct, state.sonnet7dResetsAt, cur.includes('sonnet')));
|
|
198
|
+
moveTo(6);
|
|
199
|
+
clearLine();
|
|
200
|
+
if (state.extraPct !== null) {
|
|
201
|
+
const used = `$${state.extraUsed.toFixed(2)}`;
|
|
202
|
+
const limit = `$${state.extraLimit}`;
|
|
203
|
+
const colorFn = state.extraPct >= 80 ? chalk.red : state.extraPct >= 50 ? chalk.yellow : chalk.green;
|
|
204
|
+
write(` ${chalk.dim('extra')} ${colorFn(used)}${chalk.dim('/')}${chalk.dim(limit)} ${colorFn(pctStr(state.extraPct))}`);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
moveTo(4);
|
|
208
|
+
const todayPart = `Today ${chalk.cyan(state.todaySparkline)} ${chalk.white(state.todayTotal)} tokens`;
|
|
209
|
+
const weekPart = `Week ${chalk.cyan(state.weekSparkline)} ${chalk.white(state.weekTotal)}`;
|
|
210
|
+
write(` ${todayPart} ${chalk.dim('│')} ${weekPart}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Line 7: Separator
|
|
214
|
+
moveTo(7);
|
|
215
|
+
clearLine();
|
|
216
|
+
write(chalk.dim('─'.repeat(cols)));
|
|
217
|
+
|
|
218
|
+
restoreCursor();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function setupScrollRegion() {
|
|
222
|
+
rows = process.stdout.rows || 24;
|
|
223
|
+
cols = process.stdout.columns || 80;
|
|
224
|
+
setScrollRegion(HEADER_LINES, rows);
|
|
225
|
+
moveTo(rows); // cursor at bottom of scroll region
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const tui = {
|
|
229
|
+
init(taskData, usageData) {
|
|
230
|
+
startTime = Date.now();
|
|
231
|
+
|
|
232
|
+
state.taskId = taskData.taskId || '';
|
|
233
|
+
state.taskTitle = taskData.taskTitle || '';
|
|
234
|
+
state.stage = taskData.stage || '';
|
|
235
|
+
state.done = taskData.done || 0;
|
|
236
|
+
state.total = taskData.total || 0;
|
|
237
|
+
|
|
238
|
+
if (usageData) {
|
|
239
|
+
state.todaySparkline = sparkline(usageData.today);
|
|
240
|
+
state.todayTotal = formatTokens(usageData.todayTotal);
|
|
241
|
+
state.weekSparkline = sparkline(usageData.week);
|
|
242
|
+
state.weekTotal = formatTokens(usageData.weekTotal);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
altScreenOn();
|
|
246
|
+
cursorHide();
|
|
247
|
+
mouseOn();
|
|
248
|
+
|
|
249
|
+
if (process.stdin.isTTY && !process.stdin.isRaw) {
|
|
250
|
+
process.stdin.setRawMode(true);
|
|
251
|
+
process.stdin.resume();
|
|
252
|
+
process.stdin.on('data', (data) => {
|
|
253
|
+
if (data[0] === 3) process.emit('SIGINT');
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
setupScrollRegion();
|
|
258
|
+
drawHeader();
|
|
259
|
+
|
|
260
|
+
timer = setInterval(() => drawHeader(), 1000);
|
|
261
|
+
|
|
262
|
+
process.stdout.on('resize', () => {
|
|
263
|
+
setupScrollRegion();
|
|
264
|
+
drawHeader();
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
resetTimer() {
|
|
269
|
+
cumulativeMs += Date.now() - startTime;
|
|
270
|
+
startTime = Date.now();
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
getCumulativeElapsed() {
|
|
274
|
+
return cumulativeMs + (Date.now() - startTime);
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
updateTask(taskData) {
|
|
278
|
+
if (taskData.taskId !== undefined) state.taskId = taskData.taskId;
|
|
279
|
+
if (taskData.taskTitle !== undefined) state.taskTitle = taskData.taskTitle;
|
|
280
|
+
if (taskData.stage !== undefined) state.stage = taskData.stage;
|
|
281
|
+
if (taskData.done !== undefined) state.done = taskData.done;
|
|
282
|
+
if (taskData.total !== undefined) state.total = taskData.total;
|
|
283
|
+
if (taskData.attempt !== undefined) state.attempt = taskData.attempt;
|
|
284
|
+
if (taskData.maxAttempts !== undefined) state.maxAttempts = taskData.maxAttempts;
|
|
285
|
+
if (taskData.claudeCalls !== undefined) state.claudeCalls = taskData.claudeCalls;
|
|
286
|
+
drawHeader();
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
updateTokens(input, output) {
|
|
290
|
+
state.inputTokens += input;
|
|
291
|
+
state.outputTokens += output;
|
|
292
|
+
drawHeader();
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
updateRateLimit(usage) {
|
|
296
|
+
// 5h is shared across models
|
|
297
|
+
state.opus5hPct = usage.fiveHour.utilization;
|
|
298
|
+
state.opus5hResetsAt = usage.fiveHour.resetsAt;
|
|
299
|
+
state.sonnet5hPct = usage.fiveHour.utilization;
|
|
300
|
+
state.sonnet5hResetsAt = usage.fiveHour.resetsAt;
|
|
301
|
+
|
|
302
|
+
// 7d: opus uses its own bucket, or falls back to overall seven_day
|
|
303
|
+
const opusBucket = usage.opus || usage.sevenDay;
|
|
304
|
+
state.opus7dPct = opusBucket.utilization;
|
|
305
|
+
state.opus7dResetsAt = opusBucket.resetsAt;
|
|
306
|
+
|
|
307
|
+
// 7d: sonnet has its own bucket, or falls back to overall
|
|
308
|
+
const sonnetBucket = usage.sonnet || usage.sevenDay;
|
|
309
|
+
state.sonnet7dPct = sonnetBucket.utilization;
|
|
310
|
+
state.sonnet7dResetsAt = sonnetBucket.resetsAt;
|
|
311
|
+
|
|
312
|
+
// Extra usage (overage billing)
|
|
313
|
+
if (usage.extraUsage && usage.extraUsage.enabled) {
|
|
314
|
+
state.extraUsed = usage.extraUsage.used;
|
|
315
|
+
state.extraLimit = usage.extraUsage.limit;
|
|
316
|
+
state.extraPct = usage.extraUsage.utilization;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
drawHeader();
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
updateUsage(usageData) {
|
|
323
|
+
state.todaySparkline = sparkline(usageData.today);
|
|
324
|
+
state.todayTotal = formatTokens(usageData.todayTotal);
|
|
325
|
+
state.weekSparkline = sparkline(usageData.week);
|
|
326
|
+
state.weekTotal = formatTokens(usageData.weekTotal);
|
|
327
|
+
drawHeader();
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
setTaskModel(raw) {
|
|
331
|
+
// Normalize full model IDs to short names
|
|
332
|
+
const m = (raw || '').toLowerCase();
|
|
333
|
+
if (m.includes('opus')) state.taskModel = 'opus';
|
|
334
|
+
else if (m.includes('sonnet')) state.taskModel = 'sonnet';
|
|
335
|
+
else if (m.includes('haiku')) state.taskModel = 'haiku';
|
|
336
|
+
else state.taskModel = raw || '';
|
|
337
|
+
drawHeader();
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
appendLog(line) {
|
|
341
|
+
if (destroyed) return;
|
|
342
|
+
const maxWidth = cols - 2; // 2 char right padding
|
|
343
|
+
const wrapped = wordWrap(line, maxWidth, 8);
|
|
344
|
+
for (const wl of wrapped) {
|
|
345
|
+
moveTo(rows);
|
|
346
|
+
write('\n' + wl);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
destroy() {
|
|
351
|
+
if (destroyed) return;
|
|
352
|
+
destroyed = true;
|
|
353
|
+
if (timer) clearInterval(timer);
|
|
354
|
+
process.stdout.removeAllListeners('resize');
|
|
355
|
+
mouseOff();
|
|
356
|
+
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
357
|
+
process.stdin.setRawMode(false);
|
|
358
|
+
process.stdin.removeAllListeners('data');
|
|
359
|
+
process.stdin.pause();
|
|
360
|
+
}
|
|
361
|
+
resetScrollRegion();
|
|
362
|
+
altScreenOff();
|
|
363
|
+
cursorShow();
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return tui;
|
|
368
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
6
|
+
const SPARK_CHARS = '▁▂▃▄▅▆▇█';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a sparkline string from an array of numeric values.
|
|
10
|
+
*/
|
|
11
|
+
export function sparkline(values) {
|
|
12
|
+
if (!values.length) return '';
|
|
13
|
+
const max = Math.max(...values);
|
|
14
|
+
if (max === 0) return SPARK_CHARS[0].repeat(values.length);
|
|
15
|
+
return values.map(v => SPARK_CHARS[Math.min(Math.floor((v / max) * 7), 7)]).join('');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format token count for display: 1.2k, 142k, 1.3M
|
|
20
|
+
*/
|
|
21
|
+
export function formatTokens(n) {
|
|
22
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
23
|
+
if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
|
|
24
|
+
return String(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Scan all JSONL session files, sum input+output tokens.
|
|
29
|
+
* Returns { today: number[], todayTotal: number, week: number[], weekTotal: number }
|
|
30
|
+
* today = 24-element array (tokens per hour)
|
|
31
|
+
* week = 7-element array (tokens per day, index 0 = 6 days ago, 6 = today)
|
|
32
|
+
*/
|
|
33
|
+
export async function getUsageData() {
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
36
|
+
const weekStart = new Date(todayStart);
|
|
37
|
+
weekStart.setDate(weekStart.getDate() - 6);
|
|
38
|
+
|
|
39
|
+
const hourly = new Array(24).fill(0);
|
|
40
|
+
const daily = new Array(7).fill(0);
|
|
41
|
+
let todayTotal = 0;
|
|
42
|
+
let weekTotal = 0;
|
|
43
|
+
|
|
44
|
+
let projectDirs;
|
|
45
|
+
try {
|
|
46
|
+
projectDirs = await readdir(PROJECTS_DIR);
|
|
47
|
+
} catch {
|
|
48
|
+
return { today: hourly, todayTotal: 0, week: daily, weekTotal: 0 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const dir of projectDirs) {
|
|
52
|
+
const projectPath = join(PROJECTS_DIR, dir);
|
|
53
|
+
let files;
|
|
54
|
+
try {
|
|
55
|
+
files = await readdir(projectPath);
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
62
|
+
const filePath = join(projectPath, file);
|
|
63
|
+
|
|
64
|
+
let fileStat;
|
|
65
|
+
try {
|
|
66
|
+
fileStat = await stat(filePath);
|
|
67
|
+
} catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Skip files not modified in the last 7 days
|
|
72
|
+
if (fileStat.mtimeMs < weekStart.getTime()) continue;
|
|
73
|
+
|
|
74
|
+
let content;
|
|
75
|
+
try {
|
|
76
|
+
content = await readFile(filePath, 'utf-8');
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const line of content.split('\n')) {
|
|
82
|
+
if (!line) continue;
|
|
83
|
+
let obj;
|
|
84
|
+
try {
|
|
85
|
+
obj = JSON.parse(line);
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const msg = obj.message;
|
|
91
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
92
|
+
const usage = msg.usage;
|
|
93
|
+
if (!usage) continue;
|
|
94
|
+
|
|
95
|
+
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
96
|
+
if (tokens === 0) continue;
|
|
97
|
+
|
|
98
|
+
const ts = obj.timestamp ? new Date(obj.timestamp) : null;
|
|
99
|
+
if (!ts || ts < weekStart) continue;
|
|
100
|
+
|
|
101
|
+
// Weekly bucket
|
|
102
|
+
const dayIndex = Math.floor((ts - weekStart) / 86_400_000);
|
|
103
|
+
if (dayIndex >= 0 && dayIndex < 7) {
|
|
104
|
+
daily[dayIndex] += tokens;
|
|
105
|
+
weekTotal += tokens;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Hourly bucket (today only)
|
|
109
|
+
if (ts >= todayStart) {
|
|
110
|
+
const hour = ts.getHours();
|
|
111
|
+
hourly[hour] += tokens;
|
|
112
|
+
todayTotal += tokens;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { today: hourly, todayTotal, week: daily, weekTotal };
|
|
119
|
+
}
|