create-claude-workspace 1.1.136 → 1.1.138

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.
@@ -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.mjs';
10
+ import { TUI } from './lib/tui.js';
11
11
  import { query } from '@anthropic-ai/claude-agent-sdk';
12
12
  import { config as dotenvConfig } from '@dotenvx/dotenvx';
13
13
  // ─── Args ───
@@ -420,16 +420,21 @@ async function main() {
420
420
  claudeMd = readFileSync(claudeMdPath, 'utf-8');
421
421
  else if (existsSync(dotClaudeMdPath))
422
422
  claudeMd = readFileSync(dotClaudeMdPath, 'utf-8');
423
+ // Build system prompt: orchestrator instructions + project CLAUDE.md
424
+ // Do NOT use preset: 'claude_code' — it adds built-in agents (general-purpose, Explore, Plan)
425
+ const orchestratorPrompt = agents['orchestrator']?.prompt || '';
426
+ const systemParts = [orchestratorPrompt, claudeMd].filter(Boolean);
427
+ const systemPrompt = systemParts.join('\n\n---\n\n');
428
+ // Only include our agents, not the orchestrator itself (it's the top-level)
429
+ const subAgents = { ...agents };
430
+ delete subAgents['orchestrator'];
423
431
  const queryOptions = {
424
- agents,
425
- agent: 'orchestrator',
432
+ agents: subAgents,
433
+ systemPrompt: systemPrompt || undefined,
426
434
  model: 'claude-opus-4-6',
427
435
  maxTurns: opts.maxTurns,
428
436
  cwd: opts.projectDir,
429
437
  };
430
- if (claudeMd) {
431
- queryOptions.systemPrompt = { type: 'preset', preset: 'claude_code', append: claudeMd };
432
- }
433
438
  if (opts.skipPermissions) {
434
439
  queryOptions.permissionMode = 'bypassPermissions';
435
440
  queryOptions.allowDangerouslySkipPermissions = true;
@@ -0,0 +1,348 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // ─── Terminal UI for autonomous loop (ink-based) ───
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { render, Box, Text, useInput, useApp } from 'ink';
5
+ import { appendFileSync } from 'node:fs';
6
+ // ─── Agent colors ───
7
+ const AGENT_PALETTE = ['cyan', 'magenta', 'green', 'yellow', 'blue', 'red'];
8
+ const agentColorMap = new Map();
9
+ let nextColor = 0;
10
+ function agentColor(name) {
11
+ if (!agentColorMap.has(name))
12
+ agentColorMap.set(name, AGENT_PALETTE[nextColor++ % AGENT_PALETTE.length]);
13
+ return agentColorMap.get(name);
14
+ }
15
+ // ─── Tool icons ───
16
+ const ICONS = {
17
+ Bash: '⚡', Read: '📖', Write: '✏️ ', Edit: '🔧', Glob: '🔍', Grep: '🔎',
18
+ Agent: '🤖', TodoRead: '📋', TodoWrite: '📝', WebSearch: '🌐', WebFetch: '🌐', AskUserQuestion: '❓',
19
+ };
20
+ // ─── Helpers ───
21
+ function ts() { return new Date().toLocaleTimeString('en-GB', { hour12: false }); }
22
+ function trunc(s, n) { const c = s.replace(/\n/g, ' ').trim(); return c.length > n ? c.slice(0, n) + '…' : c; }
23
+ function fmtTok(n) { return n < 1e3 ? `${n}` : n < 1e6 ? `${(n / 1e3).toFixed(1)}K` : `${(n / 1e6).toFixed(2)}M`; }
24
+ function fmtDur(ms) { return ms < 1e3 ? `${ms}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`; }
25
+ // ─── Components ───
26
+ function ProgressBar({ pct, width }) {
27
+ const filled = Math.round((pct / 100) * width);
28
+ return (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: '█'.repeat(filled) }), _jsx(Text, { color: "gray", children: '░'.repeat(width - filled) })] }));
29
+ }
30
+ function StatusBar({ state }) {
31
+ const elapsed = fmtDur(Date.now() - state.loopStart);
32
+ const iterTime = state.iterStart ? fmtDur(Date.now() - state.iterStart) : '—';
33
+ const pct = state.maxIter > 0 ? Math.round((state.iteration / state.maxIter) * 100) : 0;
34
+ const tok = fmtTok(state.tokensIn + state.tokensOut);
35
+ const curAgent = state.agents.length > 0 ? state.agents[state.agents.length - 1] : '';
36
+ return (_jsx(Box, { children: _jsxs(Text, { backgroundColor: "gray", children: [' ', _jsx(Text, { color: "white", bold: true, children: elapsed }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsxs(Text, { children: ["Iter ", state.iteration, "/", state.maxIter, " "] }), _jsx(ProgressBar, { pct: pct, width: 8 }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { children: iterTime }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: "cyan", children: state.tools }), _jsx(Text, { children: " tools" }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: "yellow", children: tok }), _jsx(Text, { children: " tok" }), curAgent && _jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: agentColor(curAgent), bold: true, children: curAgent })] }), state.taskName && _jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: "cyan", children: trunc(state.taskName, 20) })] }), state.paused && _jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { color: "yellow", children: "\u23F8 PAUSED" })] }), state.inputBuf && _jsx(_Fragment, { children: _jsxs(Text, { children: [" \u203A ", state.inputBuf] }) }), ' '] }) }));
37
+ }
38
+ function LogView({ lines }) {
39
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((line) => (_jsxs(Box, { children: [line.agent && (_jsx(Text, { color: agentColor(line.agent), children: line.agent.slice(0, 14).padEnd(15) })), _jsx(Text, { color: line.color, dimColor: line.dim, bold: line.bold, children: line.text })] }, line.id))) }));
40
+ }
41
+ function App({ stateRef, onInput, onHotkey }) {
42
+ const [, setTick] = useState(0);
43
+ const { exit } = useApp();
44
+ // Rerender every 100ms to update timer
45
+ useEffect(() => {
46
+ const timer = setInterval(() => setTick(t => t + 1), 100);
47
+ return () => clearInterval(timer);
48
+ }, []);
49
+ useInput(useCallback((input, key) => {
50
+ if (key.ctrl && input === 'c') {
51
+ onHotkey?.('quit');
52
+ exit();
53
+ return;
54
+ }
55
+ if (key.ctrl && input === 'z') {
56
+ stateRef.current.paused = !stateRef.current.paused;
57
+ onHotkey?.(stateRef.current.paused ? 'pause' : 'resume');
58
+ return;
59
+ }
60
+ if (key.ctrl && input === 's') {
61
+ onHotkey?.('stop');
62
+ return;
63
+ }
64
+ if (key.return) {
65
+ if (stateRef.current.inputBuf.trim())
66
+ onInput?.(stateRef.current.inputBuf.trim());
67
+ stateRef.current.inputBuf = '';
68
+ return;
69
+ }
70
+ if (key.backspace || key.delete) {
71
+ stateRef.current.inputBuf = stateRef.current.inputBuf.slice(0, -1);
72
+ return;
73
+ }
74
+ if (input && !key.ctrl && !key.meta) {
75
+ stateRef.current.inputBuf += input;
76
+ }
77
+ }, [stateRef, onInput, onHotkey, exit]));
78
+ const state = stateRef.current;
79
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(LogView, { lines: state.lines }), _jsx(StatusBar, { state: state })] }));
80
+ }
81
+ // ─── TUI class (same public API as before) ───
82
+ export class TUI {
83
+ logFile;
84
+ interactive;
85
+ onInput = null;
86
+ onHotkey = null;
87
+ stateRef;
88
+ inkInstance = null;
89
+ lineId = 0;
90
+ constructor(logFile, interactive = false) {
91
+ this.logFile = logFile;
92
+ this.interactive = interactive && process.stdin.isTTY === true;
93
+ this.stateRef = {
94
+ current: {
95
+ lines: [],
96
+ iteration: 0,
97
+ maxIter: 0,
98
+ loopStart: Date.now(),
99
+ iterStart: 0,
100
+ tools: 0,
101
+ tokensIn: 0,
102
+ tokensOut: 0,
103
+ agents: [],
104
+ taskName: '',
105
+ tasksDone: 0,
106
+ tasksTotal: 0,
107
+ paused: false,
108
+ inputBuf: '',
109
+ tick: 0,
110
+ },
111
+ };
112
+ if (this.interactive) {
113
+ this.inkInstance = render(_jsx(App, { stateRef: this.stateRef, onInput: (input) => this.onInput?.(input), onHotkey: (action) => this.onHotkey?.(action) }));
114
+ }
115
+ }
116
+ destroy() {
117
+ this.inkInstance?.unmount();
118
+ }
119
+ setInputHandler(handler) { this.onInput = handler; }
120
+ setHotkeyHandler(handler) { this.onHotkey = handler; }
121
+ isPaused() { return this.stateRef.current.paused; }
122
+ // ─── Log output ───
123
+ push(text, raw, opts) {
124
+ const agent = this.stateRef.current.agents.length > 0
125
+ ? this.stateRef.current.agents[this.stateRef.current.agents.length - 1]
126
+ : undefined;
127
+ const line = {
128
+ id: this.lineId++,
129
+ text,
130
+ raw: raw || text,
131
+ agent,
132
+ ...opts,
133
+ };
134
+ if (this.interactive) {
135
+ this.stateRef.current.lines.push(line);
136
+ // Keep last 500 lines
137
+ if (this.stateRef.current.lines.length > 500) {
138
+ this.stateRef.current.lines = this.stateRef.current.lines.slice(-500);
139
+ }
140
+ }
141
+ else {
142
+ // Non-interactive: print with ANSI colors
143
+ const prefix = agent ? `\x1b[${colorCode(agentColor(agent))}m${agent.slice(0, 14).padEnd(15)}\x1b[0m` : '';
144
+ const colored = opts?.color ? `\x1b[${colorCode(opts.color)}m${text}\x1b[0m` : text;
145
+ console.log(`${prefix}${colored}`);
146
+ }
147
+ if (this.logFile) {
148
+ try {
149
+ appendFileSync(this.logFile, `[${new Date().toISOString()}] ${line.raw}\n`);
150
+ }
151
+ catch { /* */ }
152
+ }
153
+ }
154
+ fileOnly(msg) {
155
+ if (this.logFile) {
156
+ try {
157
+ appendFileSync(this.logFile, `[${new Date().toISOString()}] ${msg}\n`);
158
+ }
159
+ catch { /* */ }
160
+ }
161
+ }
162
+ // ─── Public methods ───
163
+ banner() {
164
+ this.push('');
165
+ this.push(' ╔══════════════════════════════════════════════╗', undefined, { color: 'cyan', bold: true });
166
+ this.push(' ║ Claude Starter Kit — Autonomous Loop ║', undefined, { color: 'cyan', bold: true });
167
+ this.push(' ╚══════════════════════════════════════════════╝', undefined, { color: 'cyan', bold: true });
168
+ if (this.interactive) {
169
+ this.push(' Ctrl+Z pause │ Ctrl+S stop │ Ctrl+C quit │ Type to send input', undefined, { color: 'gray' });
170
+ }
171
+ this.push('');
172
+ }
173
+ info(msg) { this.push(` ${ts()} ℹ ${msg}`, `INFO: ${msg}`); }
174
+ warn(msg) { this.push(` ${ts()} ⚠ ${msg}`, `WARN: ${msg}`, { color: 'yellow' }); }
175
+ error(msg) { this.push(` ${ts()} ✗ ${msg}`, `ERROR: ${msg}`, { color: 'red' }); }
176
+ success(msg) { this.push(` ${ts()} ✓ ${msg}`, `OK: ${msg}`, { color: 'green' }); }
177
+ setIteration(i, max) {
178
+ const s = this.stateRef.current;
179
+ s.iteration = i;
180
+ s.maxIter = max;
181
+ s.iterStart = Date.now();
182
+ s.tools = 0;
183
+ s.tokensIn = 0;
184
+ s.tokensOut = 0;
185
+ s.agents = [];
186
+ const pct = Math.round((i / max) * 100);
187
+ const elapsed = fmtDur(Date.now() - s.loopStart);
188
+ this.push('');
189
+ this.push(` ━━━ Iteration ${i}/${max} ${pct}% │ ${elapsed} elapsed ━━━`, `--- Iteration ${i}/${max} ---`, { bold: true });
190
+ if (s.taskName) {
191
+ const tPct = s.tasksTotal > 0 ? Math.round((s.tasksDone / s.tasksTotal) * 100) : 0;
192
+ this.push(` 📋 ${s.taskName} (${s.tasksDone}/${s.tasksTotal} tasks ${tPct}%)`, undefined, { color: 'cyan' });
193
+ }
194
+ this.push('');
195
+ }
196
+ setTask(name, done, total) {
197
+ const s = this.stateRef.current;
198
+ s.taskName = name;
199
+ s.tasksDone = done;
200
+ s.tasksTotal = total;
201
+ }
202
+ iterationEnd() {
203
+ const s = this.stateRef.current;
204
+ const iterElapsed = fmtDur(Date.now() - s.iterStart);
205
+ const totalElapsed = fmtDur(Date.now() - s.loopStart);
206
+ const tok = fmtTok(s.tokensIn + s.tokensOut);
207
+ this.push('');
208
+ this.push(` ━━━━ ${iterElapsed} (iter) │ ${totalElapsed} (total) │ ${s.tools} tools │ ${tok} tokens ━━━━`, undefined, { color: 'gray' });
209
+ }
210
+ // ─── SDK message handler ───
211
+ handleMessage(message) {
212
+ switch (message.type) {
213
+ case 'assistant':
214
+ this.onAssistant(message);
215
+ break;
216
+ case 'user':
217
+ this.onToolResult(message);
218
+ break;
219
+ case 'system':
220
+ this.onSystem(message);
221
+ break;
222
+ case 'result':
223
+ this.onResult(message);
224
+ break;
225
+ }
226
+ }
227
+ onAssistant(msg) {
228
+ const content = msg.message?.content;
229
+ if (!Array.isArray(content))
230
+ return;
231
+ const s = this.stateRef.current;
232
+ if (msg.message?.usage) {
233
+ s.tokensIn += msg.message.usage.input_tokens || 0;
234
+ s.tokensOut += msg.message.usage.output_tokens || 0;
235
+ }
236
+ for (const block of content) {
237
+ if (block.type === 'text' && block.text?.trim()) {
238
+ this.push(` ${trunc(block.text, 300)}`, `TEXT: ${trunc(block.text, 300)}`);
239
+ }
240
+ if (block.type === 'tool_use')
241
+ this.onToolUse(block);
242
+ }
243
+ }
244
+ onToolUse(block) {
245
+ this.stateRef.current.tools++;
246
+ const name = block.name || '?';
247
+ const icon = ICONS[name] || '⚙️';
248
+ const input = block.input || {};
249
+ const time = ts();
250
+ if (name === 'Agent') {
251
+ const type = input.subagent_type || input.type || 'agent';
252
+ const model = input.model || '';
253
+ const desc = trunc(input.description || input.prompt || '', 50);
254
+ this.stateRef.current.agents.push(type);
255
+ this.push(` ${time} ${icon} ${type} ${model ? `(${model})` : ''} ${desc}`, `AGENT: ${type} ${model} — ${desc}`, { bold: true });
256
+ return;
257
+ }
258
+ let detail;
259
+ switch (name) {
260
+ case 'Bash':
261
+ detail = trunc(input.command || '', 70);
262
+ break;
263
+ case 'Read':
264
+ detail = (input.file_path || '').replace(/^\/project\//, '');
265
+ break;
266
+ case 'Write':
267
+ case 'Edit':
268
+ detail = (input.file_path || '').replace(/^\/project\//, '');
269
+ break;
270
+ case 'Glob':
271
+ detail = input.pattern || '';
272
+ break;
273
+ case 'Grep':
274
+ detail = `/${input.pattern || ''}/${input.path ? ` in ${input.path}` : ''}`;
275
+ break;
276
+ default: detail = trunc(JSON.stringify(input), 60);
277
+ }
278
+ this.push(` ${time} ${icon} ${name} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
279
+ }
280
+ onToolResult(msg) {
281
+ const content = msg.message?.content;
282
+ if (!Array.isArray(content))
283
+ return;
284
+ for (const block of content) {
285
+ if (block.type !== 'tool_result')
286
+ continue;
287
+ const output = String(block.content || '').trim();
288
+ if (!output)
289
+ continue;
290
+ if (block.is_error) {
291
+ this.push(` ✗ ${trunc(output, 100)}`, `ERROR: ${trunc(output, 100)}`, { color: 'red' });
292
+ }
293
+ else if (output.length < 150) {
294
+ this.push(` ✓ ${trunc(output, 100)}`, `OK: ${trunc(output, 100)}`, { color: 'green', dim: true });
295
+ }
296
+ else {
297
+ const n = output.split('\n').length;
298
+ this.push(` ✓ ${n} lines`, `OK: (${n} lines)`, { color: 'green', dim: true });
299
+ }
300
+ }
301
+ }
302
+ onSystem(msg) {
303
+ if (msg.subtype === 'init') {
304
+ this.push(` ${ts()} ⚙ Claude Code ${msg.claude_code_version || ''} │ Model: ${msg.model || '?'} │ Agents: ${msg.agents?.join(', ') || 'none'}`, undefined, { color: 'cyan' });
305
+ if (this.stateRef.current.agents.length === 0 && msg.agents?.length) {
306
+ this.stateRef.current.agents.push(msg.agents[0]);
307
+ this.push(` ${ts()} 🤖 ${msg.agents[0]} (${msg.model || '?'})`, `AGENT_START: ${msg.agents[0]}`, { bold: true });
308
+ }
309
+ return;
310
+ }
311
+ if (msg.subtype === 'task_started') {
312
+ const desc = msg.description || '';
313
+ this.stateRef.current.agents.push(desc);
314
+ this.push(` ${ts()} 🤖 ${desc}`, `AGENT_START: ${desc}`, { bold: true });
315
+ return;
316
+ }
317
+ if (msg.subtype === 'task_notification') {
318
+ const status = msg.status || '';
319
+ const name = this.stateRef.current.agents.length > 0 ? this.stateRef.current.agents.pop() : 'agent';
320
+ const summary = msg.summary ? ` ${trunc(msg.summary, 60)}` : '';
321
+ const color = status === 'completed' ? 'green' : 'red';
322
+ this.push(` ${ts()} ${status === 'completed' ? '✓' : '✗'} ${name} ${status}${summary}`, `AGENT_END: ${name} ${status}`, { color });
323
+ return;
324
+ }
325
+ if (msg.subtype === 'task_progress' && msg.description) {
326
+ this.push(` ${trunc(msg.description, 70)}`, `PROGRESS: ${msg.description}`, { dim: true });
327
+ return;
328
+ }
329
+ if (msg.subtype === 'api_retry') {
330
+ this.push(` ${ts()} ↻ API retry ${msg.attempt}/${msg.max_retries} (${msg.error || ''})`, undefined, { color: 'yellow' });
331
+ return;
332
+ }
333
+ }
334
+ onResult(msg) {
335
+ if (msg.session_id)
336
+ this.fileOnly(`SESSION: ${msg.session_id}`);
337
+ }
338
+ resetAgentStack() { this.stateRef.current.agents = []; }
339
+ getSessionId(msg) { return msg.session_id ?? null; }
340
+ }
341
+ // ─── Color code helper for non-interactive fallback ───
342
+ function colorCode(color) {
343
+ const map = {
344
+ red: '31', green: '32', yellow: '33', blue: '34', magenta: '35',
345
+ cyan: '36', white: '37', gray: '90',
346
+ };
347
+ return map[color] || '37';
348
+ }
@@ -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.mjs';
5
+ import { TUI } from './tui.js';
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 };
@@ -170,8 +170,9 @@ describe('TUI — message handling', () => {
170
170
  message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/project/b.ts' } }] },
171
171
  }));
172
172
  const out = consoleOutput();
173
- // Should have pipe characters for nesting
174
- expect(out).toContain('');
173
+ // Should show agent names as prefix
174
+ expect(out).toContain('orchestrator');
175
+ expect(out).toContain('reviewer');
175
176
  });
176
177
  it('handles short tool results', () => {
177
178
  const tui = new TUI(LOG_FILE, false);
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.136",
3
+ "version": "1.1.138",
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",
10
11
  "typescript": "^5.8.0",
11
12
  "vitest": "^4.0.18"
12
13
  },
@@ -42,6 +43,8 @@
42
43
  "type": "module",
43
44
  "dependencies": {
44
45
  "@anthropic-ai/claude-agent-sdk": "^0.2.81",
45
- "@dotenvx/dotenvx": "^1.57.2"
46
+ "@dotenvx/dotenvx": "^1.57.2",
47
+ "ink": "^6.8.0",
48
+ "react": "^19.2.4"
46
49
  }
47
50
  }