create-claude-workspace 1.1.142 → 1.1.144
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/dist/scripts/autonomous.mjs +2 -1
- package/dist/scripts/lib/tui.mjs +258 -233
- package/dist/scripts/lib/tui.spec.js +1 -1
- package/package.json +2 -5
- package/dist/scripts/lib/tui.js +0 -304
|
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
|
|
|
7
7
|
import { DEFAULTS } from './lib/types.mjs';
|
|
8
8
|
import { emptyCheckpoint, readCheckpoint, writeCheckpoint } from './lib/state.mjs';
|
|
9
9
|
import { pollForNewWork } from './lib/idle-poll.mjs';
|
|
10
|
-
import { TUI } from './lib/tui.
|
|
10
|
+
import { TUI } from './lib/tui.mjs';
|
|
11
11
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
12
12
|
import { config as dotenvConfig } from '@dotenvx/dotenvx';
|
|
13
13
|
// Disable SDK built-in agents (general-purpose, Explore, Plan, statusline-setup)
|
|
@@ -301,6 +301,7 @@ async function main() {
|
|
|
301
301
|
dotenvConfig({ path: resolve(opts.projectDir, '.env'), override: false, quiet: true });
|
|
302
302
|
const logPath = resolve(opts.projectDir, opts.logFile);
|
|
303
303
|
const tui = new TUI(logPath, opts.interactive);
|
|
304
|
+
tui.setTopAgent('orchestrator');
|
|
304
305
|
tui.banner();
|
|
305
306
|
tui.info(`Project: ${opts.projectDir}`);
|
|
306
307
|
tui.info(`Max iterations: ${opts.maxIterations} │ Max turns: ${opts.maxTurns}`);
|
package/dist/scripts/lib/tui.mjs
CHANGED
|
@@ -1,185 +1,231 @@
|
|
|
1
1
|
// ─── Terminal UI for autonomous loop ───
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Log lines: normal stdout (scrollable, persistent history)
|
|
3
|
+
// Status bar: overwrites last line using \r (carriage return)
|
|
4
|
+
// No ink render() — all rendering is raw ANSI to preserve scroll history.
|
|
4
5
|
import { appendFileSync } from 'node:fs';
|
|
5
|
-
// ───
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
fg: {
|
|
9
|
-
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
10
|
-
blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m',
|
|
11
|
-
gray: '\x1b[90m', brightRed: '\x1b[91m', brightGreen: '\x1b[92m',
|
|
12
|
-
brightYellow: '\x1b[93m', brightBlue: '\x1b[94m', brightMagenta: '\x1b[95m',
|
|
13
|
-
brightCyan: '\x1b[96m',
|
|
14
|
-
},
|
|
15
|
-
bg: { gray: '\x1b[48;5;236m' },
|
|
16
|
-
};
|
|
17
|
-
// ─── Shared utilities ───
|
|
18
|
-
const AGENT_PALETTE = [a.fg.brightCyan, a.fg.brightMagenta, a.fg.brightGreen, a.fg.brightYellow, a.fg.brightBlue, a.fg.brightRed];
|
|
19
|
-
const agentColors = new Map();
|
|
6
|
+
// ─── Agent colors ───
|
|
7
|
+
const PALETTE = ['cyan', 'magenta', 'green', 'yellow', 'blue', 'red'];
|
|
8
|
+
const agentColorMap = new Map();
|
|
20
9
|
let nextColor = 0;
|
|
21
10
|
function agentColor(name) {
|
|
22
|
-
if (!
|
|
23
|
-
|
|
24
|
-
return
|
|
11
|
+
if (!agentColorMap.has(name))
|
|
12
|
+
agentColorMap.set(name, PALETTE[nextColor++ % PALETTE.length]);
|
|
13
|
+
return agentColorMap.get(name);
|
|
25
14
|
}
|
|
15
|
+
const ANSI_COLORS = {
|
|
16
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
|
|
17
|
+
magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',
|
|
18
|
+
};
|
|
19
|
+
const RESET = '\x1b[0m';
|
|
20
|
+
const BOLD = '\x1b[1m';
|
|
21
|
+
const DIM = '\x1b[2m';
|
|
22
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
23
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
24
|
+
const CLEAR_LINE = '\r\x1b[2K';
|
|
25
|
+
// ─── Tool icons ───
|
|
26
26
|
const ICONS = {
|
|
27
27
|
Bash: '⚡', Read: '📖', Write: '✏️ ', Edit: '🔧', Glob: '🔍', Grep: '🔎',
|
|
28
28
|
Agent: '🤖', TodoRead: '📋', TodoWrite: '📝', WebSearch: '🌐', WebFetch: '🌐', AskUserQuestion: '❓',
|
|
29
29
|
};
|
|
30
|
+
// ─── Helpers ───
|
|
30
31
|
function ts() { return new Date().toLocaleTimeString('en-GB', { hour12: false }); }
|
|
31
|
-
function strip(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
32
32
|
function trunc(s, n) { const c = s.replace(/\n/g, ' ').trim(); return c.length > n ? c.slice(0, n) + '…' : c; }
|
|
33
33
|
function fmtTok(n) { return n < 1e3 ? `${n}` : n < 1e6 ? `${(n / 1e3).toFixed(1)}K` : `${(n / 1e6).toFixed(2)}M`; }
|
|
34
34
|
function fmtDur(ms) { return ms < 1e3 ? `${ms}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`; }
|
|
35
|
-
|
|
36
|
-
// ─── TUI ───
|
|
35
|
+
// ─── TUI class ───
|
|
37
36
|
export class TUI {
|
|
38
37
|
logFile;
|
|
39
38
|
interactive;
|
|
40
39
|
onInput = null;
|
|
41
40
|
onHotkey = null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
dirty = false;
|
|
48
|
-
// Stats
|
|
49
|
-
loopStart = Date.now();
|
|
50
|
-
iteration_ = 0;
|
|
51
|
-
maxIter = 0;
|
|
52
|
-
taskName_ = '';
|
|
53
|
-
tasksDone_ = 0;
|
|
54
|
-
tasksTotal_ = 0;
|
|
55
|
-
tools = 0;
|
|
56
|
-
tokens_ = { input: 0, output: 0 };
|
|
57
|
-
iterStart_ = 0;
|
|
58
|
-
agents = [];
|
|
59
|
-
inputBuf = '';
|
|
41
|
+
state;
|
|
42
|
+
statusBarVisible = false;
|
|
43
|
+
statusTimer = null;
|
|
44
|
+
stdinHandler = null;
|
|
45
|
+
topAgent = null;
|
|
60
46
|
constructor(logFile, interactive = false) {
|
|
61
47
|
this.logFile = logFile;
|
|
62
48
|
this.interactive = interactive && process.stdin.isTTY === true;
|
|
49
|
+
this.state = {
|
|
50
|
+
iteration: 0, maxIter: 0, loopStart: Date.now(), iterStart: 0,
|
|
51
|
+
tools: 0, tokensIn: 0, tokensOut: 0, agents: [], taskName: '',
|
|
52
|
+
tasksDone: 0, tasksTotal: 0, paused: false, inputBuf: '',
|
|
53
|
+
};
|
|
63
54
|
if (this.interactive) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
55
|
+
// Hide cursor to prevent flickering
|
|
56
|
+
process.stdout.write(HIDE_CURSOR);
|
|
57
|
+
// Set up raw stdin for keyboard input
|
|
58
|
+
if (process.stdin.setRawMode) {
|
|
59
|
+
process.stdin.setRawMode(true);
|
|
60
|
+
process.stdin.resume();
|
|
61
|
+
process.stdin.setEncoding('utf8');
|
|
62
|
+
this.stdinHandler = (data) => {
|
|
63
|
+
const str = String(data);
|
|
64
|
+
for (let i = 0; i < str.length; i++) {
|
|
65
|
+
const ch = str[i];
|
|
66
|
+
const code = ch.charCodeAt(0);
|
|
67
|
+
// Ctrl+C (0x03)
|
|
68
|
+
if (code === 0x03) {
|
|
69
|
+
this.onHotkey?.('quit');
|
|
70
|
+
this.destroy();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Ctrl+Z (0x1A)
|
|
75
|
+
if (code === 0x1A) {
|
|
76
|
+
this.state.paused = !this.state.paused;
|
|
77
|
+
this.onHotkey?.(this.state.paused ? 'pause' : 'resume');
|
|
78
|
+
this.renderStatusBar();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Ctrl+S (0x13)
|
|
82
|
+
if (code === 0x13) {
|
|
83
|
+
this.onHotkey?.('stop');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Enter (0x0D or 0x0A)
|
|
87
|
+
if (code === 0x0D || code === 0x0A) {
|
|
88
|
+
if (this.state.inputBuf.trim()) {
|
|
89
|
+
this.onInput?.(this.state.inputBuf.trim());
|
|
90
|
+
}
|
|
91
|
+
this.state.inputBuf = '';
|
|
92
|
+
this.renderStatusBar();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Backspace (0x08 or 0x7F)
|
|
96
|
+
if (code === 0x08 || code === 0x7F) {
|
|
97
|
+
this.state.inputBuf = this.state.inputBuf.slice(0, -1);
|
|
98
|
+
this.renderStatusBar();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Escape sequences (arrows, etc.) — skip
|
|
102
|
+
if (code === 0x1B) {
|
|
103
|
+
// consume the rest of the escape sequence
|
|
104
|
+
while (i + 1 < str.length && str[i + 1] >= '\x20' && str[i + 1] <= '\x7E')
|
|
105
|
+
i++;
|
|
106
|
+
if (i + 1 < str.length && str[i + 1] === '[')
|
|
107
|
+
i++; // CSI
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Printable characters
|
|
111
|
+
if (code >= 0x20) {
|
|
112
|
+
this.state.inputBuf += ch;
|
|
113
|
+
this.renderStatusBar();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
process.stdin.on('data', this.stdinHandler);
|
|
84
118
|
}
|
|
85
|
-
|
|
119
|
+
// Timer to refresh status bar every second (for elapsed time)
|
|
120
|
+
this.statusTimer = setInterval(() => this.renderStatusBar(), 1000);
|
|
86
121
|
}
|
|
87
|
-
frame += newStatus;
|
|
88
|
-
out.write(frame);
|
|
89
|
-
this.lastStatusText = newStatus;
|
|
90
|
-
this.dirty = false;
|
|
91
122
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
`${a.fg.white}${elapsed}`,
|
|
99
|
-
`Iter ${this.iteration_}/${this.maxIter} ${bar(pct, 8)}`,
|
|
100
|
-
`${iterTime}`,
|
|
101
|
-
`${a.fg.cyan}${this.tools}${a.reset} tools`,
|
|
102
|
-
`${a.fg.yellow}${tok}${a.reset} tok`,
|
|
103
|
-
];
|
|
104
|
-
if (this.agents.length > 0) {
|
|
105
|
-
const cur = this.agents[this.agents.length - 1];
|
|
106
|
-
parts.push(`${agentColor(cur)}${cur}${a.reset}`);
|
|
107
|
-
}
|
|
108
|
-
if (this.taskName_)
|
|
109
|
-
parts.push(`${a.fg.cyan}${trunc(this.taskName_, 20)}${a.reset}`);
|
|
110
|
-
if (this.paused_)
|
|
111
|
-
parts.push(`${a.fg.yellow}⏸ PAUSED${a.reset}`);
|
|
112
|
-
const info = parts.join(`${a.fg.gray} │ ${a.reset}`);
|
|
113
|
-
const input = this.inputBuf ? ` ${a.fg.gray}›${a.reset} ${this.inputBuf}` : '';
|
|
114
|
-
const cols = process.stdout.columns || 120;
|
|
115
|
-
const line = ` ${info}${input} `;
|
|
116
|
-
const pad = Math.max(0, cols - strip(line).length);
|
|
117
|
-
return `\r${a.bg.gray}${line}${' '.repeat(pad)}${a.reset}`;
|
|
118
|
-
}
|
|
119
|
-
// ─── Input ───
|
|
120
|
-
setupInput() {
|
|
121
|
-
if (!process.stdin.isTTY)
|
|
122
|
-
return;
|
|
123
|
-
process.stdin.setRawMode(true);
|
|
124
|
-
process.stdin.resume();
|
|
125
|
-
process.stdin.on('data', (data) => {
|
|
126
|
-
const key = data.toString();
|
|
127
|
-
if (key === '\x03') {
|
|
128
|
-
this.onHotkey?.('quit');
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (key === '\x1a') {
|
|
132
|
-
this.paused_ = !this.paused_;
|
|
133
|
-
this.dirty = true;
|
|
134
|
-
this.onHotkey?.(this.paused_ ? 'pause' : 'resume');
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (key === '\x13') {
|
|
138
|
-
this.onHotkey?.('stop');
|
|
139
|
-
return;
|
|
123
|
+
destroy() {
|
|
124
|
+
if (this.interactive) {
|
|
125
|
+
// Clear status bar
|
|
126
|
+
if (this.statusBarVisible) {
|
|
127
|
+
process.stdout.write(CLEAR_LINE);
|
|
128
|
+
this.statusBarVisible = false;
|
|
140
129
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.
|
|
146
|
-
|
|
130
|
+
// Show cursor
|
|
131
|
+
process.stdout.write(SHOW_CURSOR);
|
|
132
|
+
// Stop timer
|
|
133
|
+
if (this.statusTimer) {
|
|
134
|
+
clearInterval(this.statusTimer);
|
|
135
|
+
this.statusTimer = null;
|
|
147
136
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.
|
|
151
|
-
|
|
137
|
+
// Restore stdin
|
|
138
|
+
if (this.stdinHandler) {
|
|
139
|
+
process.stdin.removeListener('data', this.stdinHandler);
|
|
140
|
+
this.stdinHandler = null;
|
|
152
141
|
}
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
142
|
+
if (process.stdin.setRawMode) {
|
|
143
|
+
try {
|
|
144
|
+
process.stdin.setRawMode(false);
|
|
145
|
+
process.stdin.pause();
|
|
146
|
+
}
|
|
147
|
+
catch { /* may already be closed */ }
|
|
156
148
|
}
|
|
157
|
-
}
|
|
149
|
+
}
|
|
158
150
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
151
|
+
setInputHandler(h) { this.onInput = h; }
|
|
152
|
+
setHotkeyHandler(h) { this.onHotkey = h; }
|
|
153
|
+
isPaused() { return this.state.paused; }
|
|
154
|
+
/** Set the top-level agent name so all log lines show it from the start. */
|
|
155
|
+
setTopAgent(name) {
|
|
156
|
+
this.topAgent = name;
|
|
157
|
+
if (this.state.agents.length === 0) {
|
|
158
|
+
this.state.agents.push(name);
|
|
163
159
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
}
|
|
161
|
+
// ─── Status bar (overwrites current line, no \n) ───
|
|
162
|
+
buildStatusBar() {
|
|
163
|
+
const s = this.state;
|
|
164
|
+
const elapsed = fmtDur(Date.now() - s.loopStart);
|
|
165
|
+
const iterTime = s.iterStart ? fmtDur(Date.now() - s.iterStart) : '—';
|
|
166
|
+
const pct = s.maxIter > 0 ? Math.round((s.iteration / s.maxIter) * 100) : 0;
|
|
167
|
+
const filled = Math.round((pct / 100) * 8);
|
|
168
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(8 - filled);
|
|
169
|
+
const tok = fmtTok(s.tokensIn + s.tokensOut);
|
|
170
|
+
const cur = s.agents.length > 0 ? s.agents[s.agents.length - 1] : '';
|
|
171
|
+
let line = ` ${elapsed} | Iter ${s.iteration}/${s.maxIter} ${bar} | ${iterTime} | ${s.tools} tools | ${tok} tok`;
|
|
172
|
+
if (cur)
|
|
173
|
+
line += ` | ${cur}`;
|
|
174
|
+
if (s.taskName)
|
|
175
|
+
line += ` | ${trunc(s.taskName, 20)}`;
|
|
176
|
+
if (s.paused)
|
|
177
|
+
line += ' | PAUSED';
|
|
178
|
+
if (s.inputBuf)
|
|
179
|
+
line += ` > ${s.inputBuf}`;
|
|
180
|
+
// Truncate to terminal width
|
|
181
|
+
const cols = process.stdout.columns || 120;
|
|
182
|
+
if (line.length > cols)
|
|
183
|
+
line = line.slice(0, cols - 1) + '…';
|
|
184
|
+
return line;
|
|
185
|
+
}
|
|
186
|
+
buildColoredStatusBar() {
|
|
187
|
+
const s = this.state;
|
|
188
|
+
const elapsed = fmtDur(Date.now() - s.loopStart);
|
|
189
|
+
const iterTime = s.iterStart ? fmtDur(Date.now() - s.iterStart) : '—';
|
|
190
|
+
const pct = s.maxIter > 0 ? Math.round((s.iteration / s.maxIter) * 100) : 0;
|
|
191
|
+
const filled = Math.round((pct / 100) * 8);
|
|
192
|
+
const bar = `${ANSI_COLORS.green}${'\u2588'.repeat(filled)}${ANSI_COLORS.gray}${'\u2591'.repeat(8 - filled)}${RESET}`;
|
|
193
|
+
const tok = fmtTok(s.tokensIn + s.tokensOut);
|
|
194
|
+
const cur = s.agents.length > 0 ? s.agents[s.agents.length - 1] : '';
|
|
195
|
+
let line = `\x1b[7m ${elapsed} | Iter ${s.iteration}/${s.maxIter} \x1b[27m ${bar} \x1b[7m ${iterTime} | ${ANSI_COLORS.cyan}${s.tools}${RESET}\x1b[7m tools | ${ANSI_COLORS.yellow}${tok}${RESET}\x1b[7m tok`;
|
|
196
|
+
if (cur) {
|
|
197
|
+
const col = ANSI_COLORS[agentColor(cur)] || '';
|
|
198
|
+
line += ` | ${col}${BOLD}${cur}${RESET}\x1b[7m`;
|
|
169
199
|
}
|
|
200
|
+
if (s.taskName)
|
|
201
|
+
line += ` | ${ANSI_COLORS.cyan}${trunc(s.taskName, 20)}${RESET}\x1b[7m`;
|
|
202
|
+
if (s.paused)
|
|
203
|
+
line += ` | ${ANSI_COLORS.yellow}⏸ PAUSED${RESET}\x1b[7m`;
|
|
204
|
+
if (s.inputBuf)
|
|
205
|
+
line += ` › ${s.inputBuf}`;
|
|
206
|
+
line += ` ${RESET}`;
|
|
207
|
+
return line;
|
|
170
208
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
renderStatusBar() {
|
|
210
|
+
if (!this.interactive)
|
|
211
|
+
return;
|
|
212
|
+
const line = this.buildColoredStatusBar();
|
|
213
|
+
process.stdout.write(CLEAR_LINE + line);
|
|
214
|
+
this.statusBarVisible = true;
|
|
215
|
+
}
|
|
216
|
+
// ─── Log output (stdout — preserves scroll history) ───
|
|
217
|
+
log(formatted, raw) {
|
|
218
|
+
if (this.interactive && this.statusBarVisible) {
|
|
219
|
+
// Clear the status bar line, write the log line, then re-render status bar
|
|
220
|
+
process.stdout.write(CLEAR_LINE + formatted + '\n');
|
|
221
|
+
this.renderStatusBar();
|
|
178
222
|
}
|
|
179
223
|
else {
|
|
180
|
-
|
|
224
|
+
process.stdout.write(formatted + '\n');
|
|
225
|
+
if (this.interactive)
|
|
226
|
+
this.renderStatusBar();
|
|
181
227
|
}
|
|
182
|
-
const plain = raw ||
|
|
228
|
+
const plain = raw || formatted.replace(/\x1b\[[0-9;]*m/g, '');
|
|
183
229
|
if (this.logFile) {
|
|
184
230
|
try {
|
|
185
231
|
appendFileSync(this.logFile, `[${new Date().toISOString()}] ${plain}\n`);
|
|
@@ -196,64 +242,52 @@ export class TUI {
|
|
|
196
242
|
}
|
|
197
243
|
}
|
|
198
244
|
// ─── Agent prefix ───
|
|
199
|
-
|
|
200
|
-
if (this.agents.length === 0)
|
|
245
|
+
prefix() {
|
|
246
|
+
if (this.state.agents.length === 0)
|
|
201
247
|
return '';
|
|
202
|
-
const cur = this.agents[this.agents.length - 1];
|
|
203
|
-
const col = agentColor(cur);
|
|
204
|
-
const
|
|
205
|
-
return `${col}${
|
|
206
|
-
}
|
|
207
|
-
indent() {
|
|
208
|
-
const prefix = this.agentPrefix();
|
|
209
|
-
if (this.agents.length <= 1)
|
|
210
|
-
return ` ${prefix}`;
|
|
211
|
-
const pipes = this.agents.slice(0, -1).map(n => `${agentColor(n)}│${a.reset}`).join('');
|
|
212
|
-
return ` ${pipes} ${prefix}`;
|
|
248
|
+
const cur = this.state.agents[this.state.agents.length - 1];
|
|
249
|
+
const col = ANSI_COLORS[agentColor(cur)] || '';
|
|
250
|
+
const name = cur.length > 14 ? cur.slice(0, 14) : cur;
|
|
251
|
+
return `${col}${name.padEnd(15)}${RESET}`;
|
|
213
252
|
}
|
|
214
253
|
// ─── Public API ───
|
|
215
254
|
banner() {
|
|
216
|
-
this.
|
|
217
|
-
this.
|
|
218
|
-
this.
|
|
219
|
-
this.
|
|
255
|
+
this.log('');
|
|
256
|
+
this.log(` ${ANSI_COLORS.cyan}${BOLD}╔══════════════════════════════════════════════╗${RESET}`);
|
|
257
|
+
this.log(` ${ANSI_COLORS.cyan}${BOLD}║ ${ANSI_COLORS.white}Claude Starter Kit — Autonomous Loop${ANSI_COLORS.cyan} ║${RESET}`);
|
|
258
|
+
this.log(` ${ANSI_COLORS.cyan}${BOLD}╚══════════════════════════════════════════════╝${RESET}`);
|
|
220
259
|
if (this.interactive) {
|
|
221
|
-
this.
|
|
260
|
+
this.log(` ${ANSI_COLORS.gray} Ctrl+Z pause │ Ctrl+S stop │ Ctrl+C quit │ Type to send input${RESET}`);
|
|
222
261
|
}
|
|
223
|
-
this.
|
|
262
|
+
this.log('');
|
|
224
263
|
}
|
|
225
|
-
info(msg) { this.
|
|
226
|
-
warn(msg) { this.
|
|
227
|
-
error(msg) { this.
|
|
228
|
-
success(msg) { this.
|
|
264
|
+
info(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.blue}ℹ${RESET} ${msg}`); }
|
|
265
|
+
warn(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.yellow}⚠ ${msg}${RESET}`); }
|
|
266
|
+
error(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.red}✗ ${msg}${RESET}`); }
|
|
267
|
+
success(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.green}✓ ${msg}${RESET}`); }
|
|
229
268
|
setIteration(i, max) {
|
|
230
|
-
this.
|
|
231
|
-
this.maxIter = max;
|
|
232
|
-
this.
|
|
233
|
-
this.tools = 0;
|
|
234
|
-
this.
|
|
235
|
-
this.
|
|
269
|
+
this.state.iteration = i;
|
|
270
|
+
this.state.maxIter = max;
|
|
271
|
+
this.state.iterStart = Date.now();
|
|
272
|
+
this.state.tools = 0;
|
|
273
|
+
this.state.tokensIn = 0;
|
|
274
|
+
this.state.tokensOut = 0;
|
|
275
|
+
this.state.agents = this.topAgent ? [this.topAgent] : [];
|
|
236
276
|
const pct = Math.round((i / max) * 100);
|
|
237
|
-
const elapsed = fmtDur(Date.now() - this.loopStart);
|
|
238
|
-
this.
|
|
239
|
-
this.
|
|
240
|
-
|
|
241
|
-
const tPct = this.tasksTotal_ > 0 ? Math.round((this.tasksDone_ / this.tasksTotal_) * 100) : 0;
|
|
242
|
-
this.push(` ${a.fg.cyan}📋 ${this.taskName_}${a.reset} ${a.fg.gray}(${this.tasksDone_}/${this.tasksTotal_} tasks ${bar(tPct, 10)} ${tPct}%)${a.reset}`);
|
|
243
|
-
}
|
|
244
|
-
this.push('');
|
|
277
|
+
const elapsed = fmtDur(Date.now() - this.state.loopStart);
|
|
278
|
+
this.log('');
|
|
279
|
+
this.log(` ${BOLD}━━━ Iteration ${i}/${max} ${pct}% │ ${elapsed} elapsed ━━━${RESET}`);
|
|
280
|
+
this.log('');
|
|
245
281
|
}
|
|
246
282
|
setTask(name, done, total) {
|
|
247
|
-
this.
|
|
248
|
-
this.
|
|
249
|
-
this.
|
|
283
|
+
this.state.taskName = name;
|
|
284
|
+
this.state.tasksDone = done;
|
|
285
|
+
this.state.tasksTotal = total;
|
|
250
286
|
}
|
|
251
287
|
iterationEnd() {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.push('');
|
|
256
|
-
this.push(` ${a.fg.gray}━━━━ ${iterElapsed} (iter) │ ${totalElapsed} (total) │ ${this.tools} tools │ ${tok} tokens ━━━━${a.reset}`);
|
|
288
|
+
const s = this.state;
|
|
289
|
+
this.log('');
|
|
290
|
+
this.log(` ${ANSI_COLORS.gray}━━━━ ${fmtDur(Date.now() - s.iterStart)} (iter) │ ${fmtDur(Date.now() - s.loopStart)} (total) │ ${s.tools} tools │ ${fmtTok(s.tokensIn + s.tokensOut)} tokens ━━━━${RESET}`);
|
|
257
291
|
}
|
|
258
292
|
// ─── SDK message handler ───
|
|
259
293
|
handleMessage(message) {
|
|
@@ -277,54 +311,54 @@ export class TUI {
|
|
|
277
311
|
if (!Array.isArray(content))
|
|
278
312
|
return;
|
|
279
313
|
if (msg.message?.usage) {
|
|
280
|
-
this.
|
|
281
|
-
this.
|
|
314
|
+
this.state.tokensIn += msg.message.usage.input_tokens || 0;
|
|
315
|
+
this.state.tokensOut += msg.message.usage.output_tokens || 0;
|
|
282
316
|
}
|
|
283
317
|
for (const block of content) {
|
|
284
318
|
if (block.type === 'text' && block.text?.trim()) {
|
|
285
|
-
this.
|
|
319
|
+
this.log(` ${this.prefix()}${trunc(block.text, 300)}`, `TEXT: ${trunc(block.text, 300)}`);
|
|
286
320
|
}
|
|
287
321
|
if (block.type === 'tool_use')
|
|
288
322
|
this.onToolUse(block);
|
|
289
323
|
}
|
|
290
324
|
}
|
|
291
325
|
onToolUse(block) {
|
|
292
|
-
this.tools++;
|
|
326
|
+
this.state.tools++;
|
|
293
327
|
const name = block.name || '?';
|
|
294
328
|
const icon = ICONS[name] || '⚙️';
|
|
295
329
|
const input = block.input || {};
|
|
296
|
-
const pre = this.
|
|
297
|
-
const time = `${
|
|
330
|
+
const pre = this.prefix();
|
|
331
|
+
const time = `${ANSI_COLORS.gray}${ts()}${RESET}`;
|
|
298
332
|
if (name === 'Agent') {
|
|
299
333
|
const type = input.subagent_type || input.type || 'agent';
|
|
300
|
-
const model = input.model
|
|
334
|
+
const model = input.model || '';
|
|
301
335
|
const desc = trunc(input.description || input.prompt || '', 50);
|
|
302
|
-
const col = agentColor(type);
|
|
303
|
-
this.agents.push(type);
|
|
304
|
-
this.
|
|
336
|
+
const col = ANSI_COLORS[agentColor(type)] || '';
|
|
337
|
+
this.state.agents.push(type);
|
|
338
|
+
this.log(` ${pre}${time} ${icon} ${col}${BOLD}${type}${RESET}${model ? ` ${ANSI_COLORS.gray}(${model})${RESET}` : ''} ${ANSI_COLORS.gray}${desc}${RESET}`, `AGENT: ${type} ${model} — ${desc}`);
|
|
305
339
|
return;
|
|
306
340
|
}
|
|
307
341
|
let detail;
|
|
308
342
|
switch (name) {
|
|
309
343
|
case 'Bash':
|
|
310
|
-
detail = `${
|
|
344
|
+
detail = `${ANSI_COLORS.yellow}${trunc(input.command || '', 70)}${RESET}`;
|
|
311
345
|
break;
|
|
312
346
|
case 'Read':
|
|
313
|
-
detail = `${
|
|
347
|
+
detail = `${ANSI_COLORS.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${RESET}`;
|
|
314
348
|
break;
|
|
315
349
|
case 'Write':
|
|
316
350
|
case 'Edit':
|
|
317
|
-
detail = `${
|
|
351
|
+
detail = `${ANSI_COLORS.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${RESET}`;
|
|
318
352
|
break;
|
|
319
353
|
case 'Glob':
|
|
320
|
-
detail = `${
|
|
354
|
+
detail = `${ANSI_COLORS.cyan}${input.pattern || ''}${RESET}`;
|
|
321
355
|
break;
|
|
322
356
|
case 'Grep':
|
|
323
|
-
detail = `${
|
|
357
|
+
detail = `${ANSI_COLORS.cyan}/${input.pattern || ''}/${RESET}${input.path ? ` ${ANSI_COLORS.gray}in ${input.path}${RESET}` : ''}`;
|
|
324
358
|
break;
|
|
325
|
-
default: detail = `${
|
|
359
|
+
default: detail = `${ANSI_COLORS.gray}${trunc(JSON.stringify(input), 60)}${RESET}`;
|
|
326
360
|
}
|
|
327
|
-
this.
|
|
361
|
+
this.log(` ${pre}${time} ${icon} ${BOLD}${name}${RESET} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
|
|
328
362
|
}
|
|
329
363
|
onToolResult(msg) {
|
|
330
364
|
const content = msg.message?.content;
|
|
@@ -336,55 +370,46 @@ export class TUI {
|
|
|
336
370
|
const output = String(block.content || '').trim();
|
|
337
371
|
if (!output)
|
|
338
372
|
continue;
|
|
339
|
-
const pre = this.
|
|
373
|
+
const pre = this.prefix();
|
|
340
374
|
if (block.is_error) {
|
|
341
|
-
this.
|
|
375
|
+
this.log(` ${pre} ${ANSI_COLORS.red}✗ ${trunc(output, 100)}${RESET}`, `ERROR: ${trunc(output, 100)}`);
|
|
342
376
|
}
|
|
343
377
|
else if (output.length < 150) {
|
|
344
|
-
this.
|
|
378
|
+
this.log(` ${pre} ${ANSI_COLORS.green}✓${RESET} ${DIM}${trunc(output, 100)}${RESET}`, `OK: ${trunc(output, 100)}`);
|
|
345
379
|
}
|
|
346
380
|
else {
|
|
347
381
|
const n = output.split('\n').length;
|
|
348
|
-
this.
|
|
382
|
+
this.log(` ${pre} ${ANSI_COLORS.green}✓${RESET} ${DIM}${n} lines${RESET}`, `OK: (${n} lines)`);
|
|
349
383
|
}
|
|
350
384
|
}
|
|
351
385
|
}
|
|
352
386
|
onSystem(msg) {
|
|
353
387
|
if (msg.subtype === 'init') {
|
|
354
|
-
|
|
355
|
-
const ver = msg.claude_code_version || '';
|
|
356
|
-
const agentList = msg.agents?.join(', ') || 'none';
|
|
357
|
-
this.push(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.cyan}⚙${a.reset} Claude Code ${ver} │ Model: ${a.bold}${model}${a.reset} │ Agents: ${a.fg.cyan}${agentList}${a.reset}`);
|
|
358
|
-
// Set top-level agent name from permissionMode or first agent
|
|
359
|
-
if (this.agents.length === 0 && msg.agents?.length) {
|
|
360
|
-
const topAgent = msg.agents[0];
|
|
361
|
-
this.agents.push(topAgent);
|
|
362
|
-
this.push(` ${a.fg.gray}${ts()}${a.reset} 🤖 ${agentColor(topAgent)}${a.bold}${topAgent}${a.reset} ${a.fg.gray}(${model})${a.reset}`, `AGENT_START: ${topAgent} (${model})`);
|
|
363
|
-
}
|
|
388
|
+
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.cyan}⚙${RESET} Claude Code ${msg.claude_code_version || ''} │ Model: ${BOLD}${msg.model || '?'}${RESET} │ Agents: ${ANSI_COLORS.cyan}${msg.agents?.join(', ') || 'none'}${RESET}`);
|
|
364
389
|
return;
|
|
365
390
|
}
|
|
366
391
|
if (msg.subtype === 'task_started') {
|
|
367
392
|
const desc = msg.description || '';
|
|
368
|
-
const col = agentColor(desc);
|
|
369
|
-
this.agents.push(desc);
|
|
370
|
-
this.
|
|
393
|
+
const col = ANSI_COLORS[agentColor(desc)] || '';
|
|
394
|
+
this.state.agents.push(desc);
|
|
395
|
+
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} 🤖 ${col}${BOLD}${desc}${RESET}`, `AGENT_START: ${desc}`);
|
|
371
396
|
return;
|
|
372
397
|
}
|
|
373
398
|
if (msg.subtype === 'task_notification') {
|
|
374
399
|
const status = msg.status || '';
|
|
375
|
-
const name = this.agents.length > 0 ? this.agents.pop() : 'agent';
|
|
376
|
-
const col = agentColor(name);
|
|
377
|
-
const icon = status === 'completed' ? `${
|
|
378
|
-
const summary = msg.summary ? ` ${
|
|
379
|
-
this.
|
|
400
|
+
const name = this.state.agents.length > 0 ? this.state.agents.pop() : 'agent';
|
|
401
|
+
const col = ANSI_COLORS[agentColor(name)] || '';
|
|
402
|
+
const icon = status === 'completed' ? `${ANSI_COLORS.green}✓` : `${ANSI_COLORS.red}✗`;
|
|
403
|
+
const summary = msg.summary ? ` ${ANSI_COLORS.gray}${trunc(msg.summary, 60)}${RESET}` : '';
|
|
404
|
+
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${icon}${RESET} ${col}${name}${RESET} ${status}${summary}`, `AGENT_END: ${name} ${status}`);
|
|
380
405
|
return;
|
|
381
406
|
}
|
|
382
407
|
if (msg.subtype === 'task_progress' && msg.description) {
|
|
383
|
-
this.
|
|
408
|
+
this.log(` ${this.prefix()} ${DIM}${trunc(msg.description, 70)}${RESET}`, `PROGRESS: ${msg.description}`);
|
|
384
409
|
return;
|
|
385
410
|
}
|
|
386
411
|
if (msg.subtype === 'api_retry') {
|
|
387
|
-
this.
|
|
412
|
+
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.yellow}↻ API retry ${msg.attempt}/${msg.max_retries} (${msg.error || ''})${RESET}`);
|
|
388
413
|
return;
|
|
389
414
|
}
|
|
390
415
|
}
|
|
@@ -392,6 +417,6 @@ export class TUI {
|
|
|
392
417
|
if (msg.session_id)
|
|
393
418
|
this.fileOnly(`SESSION: ${msg.session_id}`);
|
|
394
419
|
}
|
|
395
|
-
resetAgentStack() { this.agents = []; }
|
|
420
|
+
resetAgentStack() { this.state.agents = this.topAgent ? [this.topAgent] : []; }
|
|
396
421
|
getSessionId(msg) { return msg.session_id ?? null; }
|
|
397
422
|
}
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
2
2
|
import { mkdirSync, rmSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve, dirname } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { TUI } from './tui.
|
|
5
|
+
import { TUI } from './tui.mjs';
|
|
6
6
|
// Helper to create partial SDK messages for testing
|
|
7
7
|
function msg(partial) {
|
|
8
8
|
return { parent_tool_use_id: null, uuid: 'test', session_id: 'test', ...partial };
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-claude-workspace",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.144",
|
|
4
4
|
"author": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://gitlab.com/LadaBr/claude-starter-kit.git"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@types/react": "^19.2.14",
|
|
11
10
|
"typescript": "^5.8.0",
|
|
12
11
|
"vitest": "^4.0.18"
|
|
13
12
|
},
|
|
@@ -43,8 +42,6 @@
|
|
|
43
42
|
"type": "module",
|
|
44
43
|
"dependencies": {
|
|
45
44
|
"@anthropic-ai/claude-agent-sdk": "^0.2.81",
|
|
46
|
-
"@dotenvx/dotenvx": "^1.57.2"
|
|
47
|
-
"ink": "^6.8.0",
|
|
48
|
-
"react": "^19.2.4"
|
|
45
|
+
"@dotenvx/dotenvx": "^1.57.2"
|
|
49
46
|
}
|
|
50
47
|
}
|
package/dist/scripts/lib/tui.js
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// ─── Terminal UI for autonomous loop ───
|
|
3
|
-
// Log lines: normal stdout (scrollable, persistent history)
|
|
4
|
-
// Status bar: ink renders ONLY the last line (overwrites in place)
|
|
5
|
-
import { useState, useEffect } from 'react';
|
|
6
|
-
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
7
|
-
import { appendFileSync } from 'node:fs';
|
|
8
|
-
// ─── Agent colors ───
|
|
9
|
-
const PALETTE = ['cyan', 'magenta', 'green', 'yellow', 'blue', 'red'];
|
|
10
|
-
const agentColorMap = new Map();
|
|
11
|
-
let nextColor = 0;
|
|
12
|
-
function agentColor(name) {
|
|
13
|
-
if (!agentColorMap.has(name))
|
|
14
|
-
agentColorMap.set(name, PALETTE[nextColor++ % PALETTE.length]);
|
|
15
|
-
return agentColorMap.get(name);
|
|
16
|
-
}
|
|
17
|
-
const ANSI_COLORS = {
|
|
18
|
-
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
|
|
19
|
-
magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',
|
|
20
|
-
};
|
|
21
|
-
const RESET = '\x1b[0m';
|
|
22
|
-
const BOLD = '\x1b[1m';
|
|
23
|
-
const DIM = '\x1b[2m';
|
|
24
|
-
// ─── Tool icons ───
|
|
25
|
-
const ICONS = {
|
|
26
|
-
Bash: '⚡', Read: '📖', Write: '✏️ ', Edit: '🔧', Glob: '🔍', Grep: '🔎',
|
|
27
|
-
Agent: '🤖', TodoRead: '📋', TodoWrite: '📝', WebSearch: '🌐', WebFetch: '🌐', AskUserQuestion: '❓',
|
|
28
|
-
};
|
|
29
|
-
// ─── Helpers ───
|
|
30
|
-
function ts() { return new Date().toLocaleTimeString('en-GB', { hour12: false }); }
|
|
31
|
-
function trunc(s, n) { const c = s.replace(/\n/g, ' ').trim(); return c.length > n ? c.slice(0, n) + '…' : c; }
|
|
32
|
-
function fmtTok(n) { return n < 1e3 ? `${n}` : n < 1e6 ? `${(n / 1e3).toFixed(1)}K` : `${(n / 1e6).toFixed(2)}M`; }
|
|
33
|
-
function fmtDur(ms) { return ms < 1e3 ? `${ms}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`; }
|
|
34
|
-
// ─── Ink Status Bar (only component — renders as last line) ───
|
|
35
|
-
function StatusBar({ state }) {
|
|
36
|
-
const [, setTick] = useState(0);
|
|
37
|
-
useEffect(() => { const t = setInterval(() => setTick(n => n + 1), 1000); return () => clearInterval(t); }, []);
|
|
38
|
-
const elapsed = fmtDur(Date.now() - state.loopStart);
|
|
39
|
-
const iterTime = state.iterStart ? fmtDur(Date.now() - state.iterStart) : '—';
|
|
40
|
-
const pct = state.maxIter > 0 ? Math.round((state.iteration / state.maxIter) * 100) : 0;
|
|
41
|
-
const filled = Math.round((pct / 100) * 8);
|
|
42
|
-
const tok = fmtTok(state.tokensIn + state.tokensOut);
|
|
43
|
-
const cur = state.agents.length > 0 ? state.agents[state.agents.length - 1] : '';
|
|
44
|
-
return (_jsx(Box, { children: _jsxs(Text, { backgroundColor: "gray", color: "white", children: [' ', elapsed, " \u2502 Iter ", state.iteration, "/", state.maxIter, ' ', _jsx(Text, { color: "green", children: '█'.repeat(filled) }), _jsx(Text, { color: "gray", children: '░'.repeat(8 - filled) }), ' ', "\u2502 ", iterTime, " \u2502", ' ', _jsx(Text, { color: "cyan", children: state.tools }), " tools \u2502", ' ', _jsx(Text, { color: "yellow", children: tok }), " tok", cur ? _jsxs(Text, { children: [" \u2502 ", _jsx(Text, { color: agentColor(cur), bold: true, children: cur })] }) : null, state.taskName ? _jsxs(Text, { children: [" \u2502 ", _jsx(Text, { color: "cyan", children: trunc(state.taskName, 20) })] }) : null, state.paused ? _jsxs(Text, { children: [" \u2502 ", _jsx(Text, { color: "yellow", children: "\u23F8 PAUSED" })] }) : null, state.inputBuf ? _jsxs(Text, { children: [" \u203A ", state.inputBuf] }) : null, ' '] }) }));
|
|
45
|
-
}
|
|
46
|
-
function InkApp({ state, onInput, onHotkey }) {
|
|
47
|
-
const { exit } = useApp();
|
|
48
|
-
useInput((input, key) => {
|
|
49
|
-
if (key.ctrl && input === 'c') {
|
|
50
|
-
onHotkey?.('quit');
|
|
51
|
-
exit();
|
|
52
|
-
}
|
|
53
|
-
else if (key.ctrl && input === 'z') {
|
|
54
|
-
state.paused = !state.paused;
|
|
55
|
-
onHotkey?.(state.paused ? 'pause' : 'resume');
|
|
56
|
-
}
|
|
57
|
-
else if (key.ctrl && input === 's') {
|
|
58
|
-
onHotkey?.('stop');
|
|
59
|
-
}
|
|
60
|
-
else if (key.return) {
|
|
61
|
-
if (state.inputBuf.trim())
|
|
62
|
-
onInput?.(state.inputBuf.trim());
|
|
63
|
-
state.inputBuf = '';
|
|
64
|
-
}
|
|
65
|
-
else if (key.backspace || key.delete) {
|
|
66
|
-
state.inputBuf = state.inputBuf.slice(0, -1);
|
|
67
|
-
}
|
|
68
|
-
else if (input && !key.ctrl && !key.meta) {
|
|
69
|
-
state.inputBuf += input;
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
return _jsx(StatusBar, { state: state });
|
|
73
|
-
}
|
|
74
|
-
// ─── TUI class ───
|
|
75
|
-
export class TUI {
|
|
76
|
-
logFile;
|
|
77
|
-
interactive;
|
|
78
|
-
onInput = null;
|
|
79
|
-
onHotkey = null;
|
|
80
|
-
inkInstance = null;
|
|
81
|
-
state;
|
|
82
|
-
constructor(logFile, interactive = false) {
|
|
83
|
-
this.logFile = logFile;
|
|
84
|
-
this.interactive = interactive && process.stdin.isTTY === true;
|
|
85
|
-
this.state = {
|
|
86
|
-
iteration: 0, maxIter: 0, loopStart: Date.now(), iterStart: 0,
|
|
87
|
-
tools: 0, tokensIn: 0, tokensOut: 0, agents: [], taskName: '',
|
|
88
|
-
tasksDone: 0, tasksTotal: 0, paused: false, inputBuf: '',
|
|
89
|
-
};
|
|
90
|
-
if (this.interactive) {
|
|
91
|
-
this.inkInstance = render(_jsx(InkApp, { state: this.state, onInput: (s) => this.onInput?.(s), onHotkey: (a) => this.onHotkey?.(a) }));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
destroy() { this.inkInstance?.unmount(); }
|
|
95
|
-
setInputHandler(h) { this.onInput = h; }
|
|
96
|
-
setHotkeyHandler(h) { this.onHotkey = h; }
|
|
97
|
-
isPaused() { return this.state.paused; }
|
|
98
|
-
// ─── Log output (stdout, NOT ink — preserves scroll history) ───
|
|
99
|
-
log(formatted, raw) {
|
|
100
|
-
// Pause ink rendering, write log line to stdout, resume ink
|
|
101
|
-
if (this.interactive && this.inkInstance) {
|
|
102
|
-
this.inkInstance.clear?.();
|
|
103
|
-
process.stdout.write(formatted + '\n');
|
|
104
|
-
// Re-render ink status bar below the new log line
|
|
105
|
-
this.inkInstance.rerender(_jsx(InkApp, { state: this.state, onInput: (s) => this.onInput?.(s), onHotkey: (a) => this.onHotkey?.(a) }));
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
process.stdout.write(formatted + '\n');
|
|
109
|
-
}
|
|
110
|
-
const plain = raw || formatted.replace(/\x1b\[[0-9;]*m/g, '');
|
|
111
|
-
if (this.logFile) {
|
|
112
|
-
try {
|
|
113
|
-
appendFileSync(this.logFile, `[${new Date().toISOString()}] ${plain}\n`);
|
|
114
|
-
}
|
|
115
|
-
catch { /* */ }
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
fileOnly(msg) {
|
|
119
|
-
if (this.logFile) {
|
|
120
|
-
try {
|
|
121
|
-
appendFileSync(this.logFile, `[${new Date().toISOString()}] ${msg}\n`);
|
|
122
|
-
}
|
|
123
|
-
catch { /* */ }
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// ─── Agent prefix ───
|
|
127
|
-
prefix() {
|
|
128
|
-
if (this.state.agents.length === 0)
|
|
129
|
-
return '';
|
|
130
|
-
const cur = this.state.agents[this.state.agents.length - 1];
|
|
131
|
-
const col = ANSI_COLORS[agentColor(cur)] || '';
|
|
132
|
-
const name = cur.length > 14 ? cur.slice(0, 14) : cur;
|
|
133
|
-
return `${col}${name.padEnd(15)}${RESET}`;
|
|
134
|
-
}
|
|
135
|
-
// ─── Public API ───
|
|
136
|
-
banner() {
|
|
137
|
-
this.log('');
|
|
138
|
-
this.log(` ${ANSI_COLORS.cyan}${BOLD}╔══════════════════════════════════════════════╗${RESET}`);
|
|
139
|
-
this.log(` ${ANSI_COLORS.cyan}${BOLD}║ ${ANSI_COLORS.white}Claude Starter Kit — Autonomous Loop${ANSI_COLORS.cyan} ║${RESET}`);
|
|
140
|
-
this.log(` ${ANSI_COLORS.cyan}${BOLD}╚══════════════════════════════════════════════╝${RESET}`);
|
|
141
|
-
if (this.interactive) {
|
|
142
|
-
this.log(` ${ANSI_COLORS.gray} Ctrl+Z pause │ Ctrl+S stop │ Ctrl+C quit │ Type to send input${RESET}`);
|
|
143
|
-
}
|
|
144
|
-
this.log('');
|
|
145
|
-
}
|
|
146
|
-
info(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.blue}ℹ${RESET} ${msg}`); }
|
|
147
|
-
warn(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.yellow}⚠ ${msg}${RESET}`); }
|
|
148
|
-
error(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.red}✗ ${msg}${RESET}`); }
|
|
149
|
-
success(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.green}✓ ${msg}${RESET}`); }
|
|
150
|
-
setIteration(i, max) {
|
|
151
|
-
this.state.iteration = i;
|
|
152
|
-
this.state.maxIter = max;
|
|
153
|
-
this.state.iterStart = Date.now();
|
|
154
|
-
this.state.tools = 0;
|
|
155
|
-
this.state.tokensIn = 0;
|
|
156
|
-
this.state.tokensOut = 0;
|
|
157
|
-
this.state.agents = [];
|
|
158
|
-
const pct = Math.round((i / max) * 100);
|
|
159
|
-
const elapsed = fmtDur(Date.now() - this.state.loopStart);
|
|
160
|
-
this.log('');
|
|
161
|
-
this.log(` ${BOLD}━━━ Iteration ${i}/${max} ${pct}% │ ${elapsed} elapsed ━━━${RESET}`);
|
|
162
|
-
this.log('');
|
|
163
|
-
}
|
|
164
|
-
setTask(name, done, total) {
|
|
165
|
-
this.state.taskName = name;
|
|
166
|
-
this.state.tasksDone = done;
|
|
167
|
-
this.state.tasksTotal = total;
|
|
168
|
-
}
|
|
169
|
-
iterationEnd() {
|
|
170
|
-
const s = this.state;
|
|
171
|
-
this.log('');
|
|
172
|
-
this.log(` ${ANSI_COLORS.gray}━━━━ ${fmtDur(Date.now() - s.iterStart)} (iter) │ ${fmtDur(Date.now() - s.loopStart)} (total) │ ${s.tools} tools │ ${fmtTok(s.tokensIn + s.tokensOut)} tokens ━━━━${RESET}`);
|
|
173
|
-
}
|
|
174
|
-
// ─── SDK message handler ───
|
|
175
|
-
handleMessage(message) {
|
|
176
|
-
switch (message.type) {
|
|
177
|
-
case 'assistant':
|
|
178
|
-
this.onAssistant(message);
|
|
179
|
-
break;
|
|
180
|
-
case 'user':
|
|
181
|
-
this.onToolResult(message);
|
|
182
|
-
break;
|
|
183
|
-
case 'system':
|
|
184
|
-
this.onSystem(message);
|
|
185
|
-
break;
|
|
186
|
-
case 'result':
|
|
187
|
-
this.onResult(message);
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
onAssistant(msg) {
|
|
192
|
-
const content = msg.message?.content;
|
|
193
|
-
if (!Array.isArray(content))
|
|
194
|
-
return;
|
|
195
|
-
if (msg.message?.usage) {
|
|
196
|
-
this.state.tokensIn += msg.message.usage.input_tokens || 0;
|
|
197
|
-
this.state.tokensOut += msg.message.usage.output_tokens || 0;
|
|
198
|
-
}
|
|
199
|
-
for (const block of content) {
|
|
200
|
-
if (block.type === 'text' && block.text?.trim()) {
|
|
201
|
-
this.log(` ${this.prefix()}${trunc(block.text, 300)}`, `TEXT: ${trunc(block.text, 300)}`);
|
|
202
|
-
}
|
|
203
|
-
if (block.type === 'tool_use')
|
|
204
|
-
this.onToolUse(block);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
onToolUse(block) {
|
|
208
|
-
this.state.tools++;
|
|
209
|
-
const name = block.name || '?';
|
|
210
|
-
const icon = ICONS[name] || '⚙️';
|
|
211
|
-
const input = block.input || {};
|
|
212
|
-
const pre = this.prefix();
|
|
213
|
-
const time = `${ANSI_COLORS.gray}${ts()}${RESET}`;
|
|
214
|
-
if (name === 'Agent') {
|
|
215
|
-
const type = input.subagent_type || input.type || 'agent';
|
|
216
|
-
const model = input.model || '';
|
|
217
|
-
const desc = trunc(input.description || input.prompt || '', 50);
|
|
218
|
-
const col = ANSI_COLORS[agentColor(type)] || '';
|
|
219
|
-
this.state.agents.push(type);
|
|
220
|
-
this.log(` ${pre}${time} ${icon} ${col}${BOLD}${type}${RESET}${model ? ` ${ANSI_COLORS.gray}(${model})${RESET}` : ''} ${ANSI_COLORS.gray}${desc}${RESET}`, `AGENT: ${type} ${model} — ${desc}`);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
let detail;
|
|
224
|
-
switch (name) {
|
|
225
|
-
case 'Bash':
|
|
226
|
-
detail = `${ANSI_COLORS.yellow}${trunc(input.command || '', 70)}${RESET}`;
|
|
227
|
-
break;
|
|
228
|
-
case 'Read':
|
|
229
|
-
detail = `${ANSI_COLORS.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${RESET}`;
|
|
230
|
-
break;
|
|
231
|
-
case 'Write':
|
|
232
|
-
case 'Edit':
|
|
233
|
-
detail = `${ANSI_COLORS.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${RESET}`;
|
|
234
|
-
break;
|
|
235
|
-
case 'Glob':
|
|
236
|
-
detail = `${ANSI_COLORS.cyan}${input.pattern || ''}${RESET}`;
|
|
237
|
-
break;
|
|
238
|
-
case 'Grep':
|
|
239
|
-
detail = `${ANSI_COLORS.cyan}/${input.pattern || ''}/${RESET}${input.path ? ` ${ANSI_COLORS.gray}in ${input.path}${RESET}` : ''}`;
|
|
240
|
-
break;
|
|
241
|
-
default: detail = `${ANSI_COLORS.gray}${trunc(JSON.stringify(input), 60)}${RESET}`;
|
|
242
|
-
}
|
|
243
|
-
this.log(` ${pre}${time} ${icon} ${BOLD}${name}${RESET} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
|
|
244
|
-
}
|
|
245
|
-
onToolResult(msg) {
|
|
246
|
-
const content = msg.message?.content;
|
|
247
|
-
if (!Array.isArray(content))
|
|
248
|
-
return;
|
|
249
|
-
for (const block of content) {
|
|
250
|
-
if (block.type !== 'tool_result')
|
|
251
|
-
continue;
|
|
252
|
-
const output = String(block.content || '').trim();
|
|
253
|
-
if (!output)
|
|
254
|
-
continue;
|
|
255
|
-
const pre = this.prefix();
|
|
256
|
-
if (block.is_error) {
|
|
257
|
-
this.log(` ${pre} ${ANSI_COLORS.red}✗ ${trunc(output, 100)}${RESET}`, `ERROR: ${trunc(output, 100)}`);
|
|
258
|
-
}
|
|
259
|
-
else if (output.length < 150) {
|
|
260
|
-
this.log(` ${pre} ${ANSI_COLORS.green}✓${RESET} ${DIM}${trunc(output, 100)}${RESET}`, `OK: ${trunc(output, 100)}`);
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
const n = output.split('\n').length;
|
|
264
|
-
this.log(` ${pre} ${ANSI_COLORS.green}✓${RESET} ${DIM}${n} lines${RESET}`, `OK: (${n} lines)`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
onSystem(msg) {
|
|
269
|
-
if (msg.subtype === 'init') {
|
|
270
|
-
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.cyan}⚙${RESET} Claude Code ${msg.claude_code_version || ''} │ Model: ${BOLD}${msg.model || '?'}${RESET} │ Agents: ${ANSI_COLORS.cyan}${msg.agents?.join(', ') || 'none'}${RESET}`);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (msg.subtype === 'task_started') {
|
|
274
|
-
const desc = msg.description || '';
|
|
275
|
-
const col = ANSI_COLORS[agentColor(desc)] || '';
|
|
276
|
-
this.state.agents.push(desc);
|
|
277
|
-
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} 🤖 ${col}${BOLD}${desc}${RESET}`, `AGENT_START: ${desc}`);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (msg.subtype === 'task_notification') {
|
|
281
|
-
const status = msg.status || '';
|
|
282
|
-
const name = this.state.agents.length > 0 ? this.state.agents.pop() : 'agent';
|
|
283
|
-
const col = ANSI_COLORS[agentColor(name)] || '';
|
|
284
|
-
const icon = status === 'completed' ? `${ANSI_COLORS.green}✓` : `${ANSI_COLORS.red}✗`;
|
|
285
|
-
const summary = msg.summary ? ` ${ANSI_COLORS.gray}${trunc(msg.summary, 60)}${RESET}` : '';
|
|
286
|
-
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${icon}${RESET} ${col}${name}${RESET} ${status}${summary}`, `AGENT_END: ${name} ${status}`);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (msg.subtype === 'task_progress' && msg.description) {
|
|
290
|
-
this.log(` ${this.prefix()} ${DIM}${trunc(msg.description, 70)}${RESET}`, `PROGRESS: ${msg.description}`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
if (msg.subtype === 'api_retry') {
|
|
294
|
-
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.yellow}↻ API retry ${msg.attempt}/${msg.max_retries} (${msg.error || ''})${RESET}`);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
onResult(msg) {
|
|
299
|
-
if (msg.session_id)
|
|
300
|
-
this.fileOnly(`SESSION: ${msg.session_id}`);
|
|
301
|
-
}
|
|
302
|
-
resetAgentStack() { this.state.agents = []; }
|
|
303
|
-
getSessionId(msg) { return msg.session_id ?? null; }
|
|
304
|
-
}
|