@covibes/zeroshot 5.3.0 → 5.4.1
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/LICENSE +1 -1
- package/README.md +8 -385
- package/cli/index.js +108 -4615
- package/package.json +16 -125
- package/CHANGELOG.md +0 -537
- package/cli/commands/providers.js +0 -150
- package/cli/lib/first-run.js +0 -211
- package/cli/lib/update-checker.js +0 -281
- package/cli/message-formatter-utils.js +0 -75
- package/cli/message-formatters-normal.js +0 -253
- package/cli/message-formatters-watch.js +0 -181
- package/cluster-templates/base-templates/debug-workflow.json +0 -375
- package/cluster-templates/base-templates/full-workflow.json +0 -697
- package/cluster-templates/base-templates/single-worker.json +0 -63
- package/cluster-templates/base-templates/worker-validator.json +0 -152
- package/cluster-templates/conductor-bootstrap.json +0 -124
- package/docker/zeroshot-cluster/Dockerfile +0 -152
- package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
- package/lib/completion.js +0 -174
- package/lib/docker-config.js +0 -212
- package/lib/id-detector.js +0 -53
- package/lib/provider-detection.js +0 -59
- package/lib/provider-names.js +0 -56
- package/lib/settings.js +0 -405
- package/lib/stream-json-parser.js +0 -10
- package/scripts/fix-node-pty-permissions.js +0 -75
- package/scripts/record-demo.sh +0 -279
- package/scripts/test-install.sh +0 -40
- package/scripts/validate-templates.js +0 -100
- package/src/agent/agent-config.js +0 -193
- package/src/agent/agent-context-builder.js +0 -335
- package/src/agent/agent-hook-executor.js +0 -402
- package/src/agent/agent-lifecycle.js +0 -683
- package/src/agent/agent-stuck-detector.js +0 -256
- package/src/agent/agent-task-executor.js +0 -1431
- package/src/agent/agent-trigger-evaluator.js +0 -67
- package/src/agent/output-extraction.js +0 -200
- package/src/agent/output-reformatter.js +0 -175
- package/src/agent/schema-utils.js +0 -111
- package/src/agent-wrapper.js +0 -570
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/attach/attach-client.js +0 -438
- package/src/attach/attach-server.js +0 -543
- package/src/attach/index.js +0 -35
- package/src/attach/protocol.js +0 -220
- package/src/attach/ring-buffer.js +0 -121
- package/src/attach/socket-discovery.js +0 -267
- package/src/claude-task-runner.js +0 -523
- package/src/config-router.js +0 -80
- package/src/config-validator.js +0 -1660
- package/src/copy-worker.js +0 -43
- package/src/github.js +0 -139
- package/src/isolation-manager.js +0 -1381
- package/src/ledger.js +0 -614
- package/src/logic-engine.js +0 -223
- package/src/message-bus-bridge.js +0 -139
- package/src/message-bus.js +0 -249
- package/src/name-generator.js +0 -232
- package/src/orchestrator.js +0 -2519
- package/src/preflight.js +0 -485
- package/src/process-metrics.js +0 -566
- package/src/providers/anthropic/cli-builder.js +0 -45
- package/src/providers/anthropic/index.js +0 -134
- package/src/providers/anthropic/models.js +0 -23
- package/src/providers/anthropic/output-parser.js +0 -159
- package/src/providers/base-provider.js +0 -181
- package/src/providers/capabilities.js +0 -51
- package/src/providers/google/cli-builder.js +0 -55
- package/src/providers/google/index.js +0 -116
- package/src/providers/google/models.js +0 -24
- package/src/providers/google/output-parser.js +0 -92
- package/src/providers/index.js +0 -75
- package/src/providers/openai/cli-builder.js +0 -122
- package/src/providers/openai/index.js +0 -135
- package/src/providers/openai/models.js +0 -21
- package/src/providers/openai/output-parser.js +0 -129
- package/src/schemas/sub-cluster.js +0 -156
- package/src/status-footer.js +0 -980
- package/src/sub-cluster-wrapper.js +0 -560
- package/src/task-runner.js +0 -30
- package/src/template-resolver.js +0 -369
- package/src/tui/CHANGES.txt +0 -133
- package/src/tui/LAYOUT.md +0 -261
- package/src/tui/README.txt +0 -192
- package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
- package/src/tui/data-poller.js +0 -325
- package/src/tui/demo.js +0 -208
- package/src/tui/formatters.js +0 -128
- package/src/tui/index.js +0 -193
- package/src/tui/keybindings.js +0 -383
- package/src/tui/layout.js +0 -334
- package/src/tui/renderer.js +0 -194
- package/task-lib/attachable-watcher.js +0 -272
- package/task-lib/claude-recovery.js +0 -119
- package/task-lib/commands/clean.js +0 -50
- package/task-lib/commands/get-log-path.js +0 -23
- package/task-lib/commands/kill.js +0 -32
- package/task-lib/commands/list.js +0 -105
- package/task-lib/commands/logs.js +0 -411
- package/task-lib/commands/resume.js +0 -42
- package/task-lib/commands/run.js +0 -57
- package/task-lib/commands/schedule.js +0 -105
- package/task-lib/commands/scheduler-cmd.js +0 -96
- package/task-lib/commands/schedules.js +0 -98
- package/task-lib/commands/status.js +0 -44
- package/task-lib/commands/unschedule.js +0 -16
- package/task-lib/completion.js +0 -9
- package/task-lib/config.js +0 -10
- package/task-lib/name-generator.js +0 -230
- package/task-lib/package.json +0 -3
- package/task-lib/runner.js +0 -144
- package/task-lib/scheduler.js +0 -252
- package/task-lib/store.js +0 -230
- package/task-lib/tui/formatters.js +0 -166
- package/task-lib/tui/index.js +0 -197
- package/task-lib/tui/layout.js +0 -111
- package/task-lib/tui/renderer.js +0 -119
- package/task-lib/tui.js +0 -384
- package/task-lib/watcher.js +0 -188
package/cli/index.js
CHANGED
|
@@ -1,4677 +1,170 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Commands:
|
|
7
|
-
* - run: Start a multi-agent cluster
|
|
8
|
-
* - list: List all clusters and tasks
|
|
9
|
-
* - status: Get cluster/task status
|
|
10
|
-
* - logs: View cluster/task logs
|
|
11
|
-
* - stop: Stop a cluster gracefully
|
|
12
|
-
* - kill: Force kill a task or cluster
|
|
13
|
-
* - kill-all: Kill all running tasks and clusters
|
|
14
|
-
* - export: Export cluster conversation
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const { Command } = require('commander');
|
|
18
|
-
const path = require('path');
|
|
19
|
-
const fs = require('fs');
|
|
20
|
-
const os = require('os');
|
|
21
|
-
const chalk = require('chalk');
|
|
22
|
-
const Orchestrator = require('../src/orchestrator');
|
|
23
|
-
const { setupCompletion } = require('../lib/completion');
|
|
24
|
-
const { formatWatchMode } = require('./message-formatters-watch');
|
|
25
|
-
const {
|
|
26
|
-
formatAgentLifecycle,
|
|
27
|
-
formatAgentError: formatAgentErrorNormal,
|
|
28
|
-
formatIssueOpened: formatIssueOpenedNormal,
|
|
29
|
-
formatImplementationReady: formatImplementationReadyNormal,
|
|
30
|
-
formatValidationResult: formatValidationResultNormal,
|
|
31
|
-
formatPrCreated,
|
|
32
|
-
formatClusterComplete,
|
|
33
|
-
formatClusterFailed,
|
|
34
|
-
formatGenericMessage,
|
|
35
|
-
} = require('./message-formatters-normal');
|
|
36
|
-
const {
|
|
37
|
-
getColorForSender,
|
|
38
|
-
buildMessagePrefix,
|
|
39
|
-
buildClusterPrefix,
|
|
40
|
-
} = require('./message-formatter-utils');
|
|
41
|
-
const {
|
|
42
|
-
loadSettings,
|
|
43
|
-
saveSettings,
|
|
44
|
-
validateSetting,
|
|
45
|
-
coerceValue,
|
|
46
|
-
DEFAULT_SETTINGS,
|
|
47
|
-
} = require('../lib/settings');
|
|
48
|
-
const { normalizeProviderName } = require('../lib/provider-names');
|
|
49
|
-
const { getProvider, parseProviderChunk } = require('../src/providers');
|
|
50
|
-
const { MOUNT_PRESETS, resolveEnvs } = require('../lib/docker-config');
|
|
51
|
-
const { requirePreflight } = require('../src/preflight');
|
|
52
|
-
const { providersCommand, setDefaultCommand, setupCommand } = require('./commands/providers');
|
|
53
|
-
// Setup wizard removed - use: zeroshot settings set <key> <value>
|
|
54
|
-
const { checkForUpdates } = require('./lib/update-checker');
|
|
55
|
-
const { StatusFooter, AGENT_STATE, ACTIVE_STATES } = require('../src/status-footer');
|
|
56
|
-
|
|
57
|
-
const program = new Command();
|
|
58
|
-
|
|
59
|
-
// =============================================================================
|
|
60
|
-
// GLOBAL ERROR HANDLERS - Prevent silent process death
|
|
61
|
-
// =============================================================================
|
|
62
|
-
// Track active cluster ID for cleanup on crash
|
|
63
|
-
/** @type {string | null} */
|
|
64
|
-
let activeClusterId = null;
|
|
65
|
-
/** @type {import('../src/orchestrator') | null} */
|
|
66
|
-
let orchestratorInstance = null;
|
|
67
|
-
|
|
68
|
-
// Track active status footer for safe output routing
|
|
69
|
-
// When set, all output routes through statusFooter.print() to prevent garbling
|
|
70
|
-
/** @type {import('../src/status-footer').StatusFooter | null} */
|
|
71
|
-
let activeStatusFooter = null;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Safe print - routes through statusFooter when active to prevent garbling
|
|
75
|
-
* @param {...any} args - Arguments to print (like console.log)
|
|
76
|
-
*/
|
|
77
|
-
function safePrint(...args) {
|
|
78
|
-
const text = args.map((arg) => (typeof arg === 'string' ? arg : String(arg))).join(' ');
|
|
79
|
-
|
|
80
|
-
if (activeStatusFooter) {
|
|
81
|
-
activeStatusFooter.print(text + '\n');
|
|
82
|
-
} else {
|
|
83
|
-
console.log(...args);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Safe write - routes through statusFooter when active
|
|
89
|
-
* @param {string} text - Text to write
|
|
90
|
-
*/
|
|
91
|
-
function safeWrite(text) {
|
|
92
|
-
if (activeStatusFooter) {
|
|
93
|
-
activeStatusFooter.print(text);
|
|
94
|
-
} else {
|
|
95
|
-
process.stdout.write(text);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Handle fatal errors: log, cleanup cluster state, exit
|
|
101
|
-
* @param {string} type - 'uncaughtException' or 'unhandledRejection'
|
|
102
|
-
* @param {Error|unknown} error - The error that caused the crash
|
|
103
|
-
*/
|
|
104
|
-
function handleFatalError(type, error) {
|
|
105
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
106
|
-
const errorStack = error instanceof Error ? error.stack : '';
|
|
107
|
-
|
|
108
|
-
console.error(chalk.red(`\n${'='.repeat(80)}`));
|
|
109
|
-
console.error(chalk.red.bold(`🔴 FATAL: ${type}`));
|
|
110
|
-
console.error(chalk.red(`${'='.repeat(80)}`));
|
|
111
|
-
console.error(chalk.red(`Error: ${errorMessage}`));
|
|
112
|
-
if (errorStack) {
|
|
113
|
-
console.error(chalk.dim(errorStack));
|
|
114
|
-
}
|
|
115
|
-
console.error(chalk.red(`${'='.repeat(80)}\n`));
|
|
116
|
-
|
|
117
|
-
// Try to update cluster state to 'failed' before exiting
|
|
118
|
-
if (activeClusterId && orchestratorInstance) {
|
|
119
|
-
try {
|
|
120
|
-
console.error(chalk.yellow(`Attempting to mark cluster ${activeClusterId} as failed...`));
|
|
121
|
-
const cluster = orchestratorInstance.clusters.get(activeClusterId);
|
|
122
|
-
if (cluster) {
|
|
123
|
-
cluster.state = 'failed';
|
|
124
|
-
cluster.pid = null;
|
|
125
|
-
cluster.failureInfo = {
|
|
126
|
-
type,
|
|
127
|
-
error: errorMessage,
|
|
128
|
-
timestamp: Date.now(),
|
|
129
|
-
};
|
|
130
|
-
orchestratorInstance._saveClusters();
|
|
131
|
-
console.error(chalk.yellow(`Cluster ${activeClusterId} marked as failed.`));
|
|
132
|
-
}
|
|
133
|
-
} catch (cleanupErr) {
|
|
134
|
-
const errMsg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
135
|
-
console.error(chalk.red(`Failed to update cluster state: ${errMsg}`));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
process.on('uncaughtException', (error) => {
|
|
143
|
-
handleFatalError('Uncaught Exception', error);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
process.on('unhandledRejection', (reason) => {
|
|
147
|
-
handleFatalError('Unhandled Promise Rejection', reason);
|
|
148
|
-
});
|
|
149
|
-
// =============================================================================
|
|
150
|
-
|
|
151
|
-
// Package root directory (for resolving default config paths)
|
|
152
|
-
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Detect git repository root from current directory
|
|
156
|
-
* Critical for CWD propagation - agents must work in the target repo, not where CLI was invoked
|
|
157
|
-
* @returns {string} Git repo root, or process.cwd() if not in a git repo
|
|
158
|
-
*/
|
|
159
|
-
function detectGitRepoRoot() {
|
|
160
|
-
const { execSync } = require('child_process');
|
|
161
|
-
try {
|
|
162
|
-
const root = execSync('git rev-parse --show-toplevel', {
|
|
163
|
-
encoding: 'utf8',
|
|
164
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
165
|
-
}).trim();
|
|
166
|
-
return root;
|
|
167
|
-
} catch {
|
|
168
|
-
// Not in a git repo - use current directory
|
|
169
|
-
return process.cwd();
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Parse CLI mount specs (host:container[:ro]) into mount config objects
|
|
175
|
-
* @param {string[]} specs - Array of mount specs from CLI
|
|
176
|
-
* @returns {Array<{host: string, container: string, readonly: boolean}>}
|
|
177
|
-
*/
|
|
178
|
-
function parseMountSpecs(specs) {
|
|
179
|
-
return specs.map((spec) => {
|
|
180
|
-
const parts = spec.split(':');
|
|
181
|
-
if (parts.length < 2) {
|
|
182
|
-
throw new Error(`Invalid mount spec: "${spec}". Format: host:container[:ro]`);
|
|
183
|
-
}
|
|
184
|
-
const host = parts[0];
|
|
185
|
-
const container = parts[1];
|
|
186
|
-
const readonly = parts[2] === 'ro';
|
|
187
|
-
return { host, container, readonly };
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Lazy-loaded orchestrator (quiet by default) - created on first use
|
|
192
|
-
/** @type {import('../src/orchestrator') | null} */
|
|
193
|
-
let _orchestrator = null;
|
|
194
|
-
/** @type {Promise<import('../src/orchestrator')> | null} */
|
|
195
|
-
let _orchestratorPromise = null;
|
|
196
|
-
/**
|
|
197
|
-
* @returns {Promise<import('../src/orchestrator')>}
|
|
198
|
-
*/
|
|
199
|
-
function getOrchestrator() {
|
|
200
|
-
if (_orchestrator) {
|
|
201
|
-
return Promise.resolve(_orchestrator);
|
|
202
|
-
}
|
|
203
|
-
// Use a promise to prevent multiple concurrent initializations
|
|
204
|
-
if (!_orchestratorPromise) {
|
|
205
|
-
_orchestratorPromise = Orchestrator.create({ quiet: true }).then((orch) => {
|
|
206
|
-
_orchestrator = orch;
|
|
207
|
-
return orch;
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
return _orchestratorPromise;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* @typedef {Object} TaskLogMessage
|
|
215
|
-
* @property {string} topic
|
|
216
|
-
* @property {string} sender
|
|
217
|
-
* @property {Object} content
|
|
218
|
-
* @property {number} timestamp
|
|
219
|
-
*/
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Read task logs from zeroshot task log files for agents in a cluster
|
|
223
|
-
* Returns messages in cluster message format (topic, sender, content, timestamp)
|
|
224
|
-
* @param {Object} cluster - Cluster object from orchestrator
|
|
225
|
-
* @returns {TaskLogMessage[]} Messages from task logs
|
|
226
|
-
*/
|
|
227
|
-
function readAgentTaskLogs(cluster) {
|
|
228
|
-
/** @type {TaskLogMessage[]} */
|
|
229
|
-
const messages = [];
|
|
230
|
-
const zeroshotLogsDir = path.join(os.homedir(), '.claude-zeroshot', 'logs');
|
|
231
|
-
|
|
232
|
-
if (!fs.existsSync(zeroshotLogsDir)) {
|
|
233
|
-
return messages;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Strategy 1: Find task IDs from AGENT_LIFECYCLE messages
|
|
237
|
-
const lifecycleMessages = cluster.messageBus.query({
|
|
238
|
-
cluster_id: cluster.id,
|
|
239
|
-
topic: 'AGENT_LIFECYCLE',
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
const taskIds = new Set(); // All task IDs we've found
|
|
243
|
-
for (const msg of lifecycleMessages) {
|
|
244
|
-
const taskId = msg.content?.data?.taskId;
|
|
245
|
-
if (taskId) {
|
|
246
|
-
taskIds.add(taskId);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Strategy 2: Find task IDs from current agent state
|
|
251
|
-
for (const agent of cluster.agents) {
|
|
252
|
-
const state = agent.getState();
|
|
253
|
-
if (state.currentTaskId) {
|
|
254
|
-
taskIds.add(state.currentTaskId);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Strategy 3: Scan for log files matching cluster start time (catch orphaned tasks)
|
|
259
|
-
// This handles the case where TASK_ID_ASSIGNED wasn't published to cluster DB
|
|
260
|
-
const clusterStartTime = cluster.createdAt;
|
|
261
|
-
const logFiles = fs.readdirSync(zeroshotLogsDir);
|
|
262
|
-
|
|
263
|
-
for (const logFile of logFiles) {
|
|
264
|
-
if (!logFile.endsWith('.log')) continue;
|
|
265
|
-
const taskId = logFile.replace(/\.log$/, '');
|
|
266
|
-
|
|
267
|
-
// Check file modification time - only include logs modified after cluster started
|
|
268
|
-
const logPath = path.join(zeroshotLogsDir, logFile);
|
|
269
|
-
try {
|
|
270
|
-
const stats = fs.statSync(logPath);
|
|
271
|
-
if (stats.mtimeMs >= clusterStartTime) {
|
|
272
|
-
taskIds.add(taskId);
|
|
273
|
-
}
|
|
274
|
-
} catch {
|
|
275
|
-
// Skip files we can't stat
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Read logs for all discovered tasks
|
|
280
|
-
for (const taskId of taskIds) {
|
|
281
|
-
const logPath = path.join(zeroshotLogsDir, `${taskId}.log`);
|
|
282
|
-
if (!fs.existsSync(logPath)) {
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
const content = fs.readFileSync(logPath, 'utf8');
|
|
288
|
-
const lines = content.split('\n').filter((line) => line.trim());
|
|
289
|
-
|
|
290
|
-
// Try to match task to agent (best effort, may not find a match for orphaned tasks)
|
|
291
|
-
let matchedAgent = null;
|
|
292
|
-
for (const agent of cluster.agents) {
|
|
293
|
-
const state = agent.getState();
|
|
294
|
-
if (state.currentTaskId === taskId) {
|
|
295
|
-
matchedAgent = agent;
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// If no agent match, try to infer from lifecycle messages
|
|
301
|
-
if (!matchedAgent) {
|
|
302
|
-
for (const msg of lifecycleMessages) {
|
|
303
|
-
if (msg.content?.data?.taskId === taskId) {
|
|
304
|
-
const agentId = msg.content?.data?.agent || msg.sender;
|
|
305
|
-
matchedAgent = cluster.agents.find((a) => a.id === agentId);
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Default to first agent if no match found (best effort for orphaned tasks)
|
|
312
|
-
const agent = matchedAgent || cluster.agents[0];
|
|
313
|
-
if (!agent) {
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const state = agent.getState();
|
|
318
|
-
|
|
319
|
-
for (const line of lines) {
|
|
320
|
-
// Lines are prefixed with [timestamp] - parse that first
|
|
321
|
-
const trimmed = line.trim();
|
|
322
|
-
if (!trimmed.startsWith('[')) {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
// Parse timestamp-prefixed line: [1733301234567]{json...} or [1733301234567][SYSTEM]...
|
|
328
|
-
let timestamp = Date.now();
|
|
329
|
-
let jsonContent = trimmed;
|
|
330
|
-
|
|
331
|
-
const timestampMatch = jsonContent.match(/^\[(\d{13})\](.*)$/);
|
|
332
|
-
if (timestampMatch) {
|
|
333
|
-
timestamp = parseInt(timestampMatch[1], 10);
|
|
334
|
-
jsonContent = timestampMatch[2];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Skip non-JSON (e.g., [SYSTEM] lines)
|
|
338
|
-
if (!jsonContent.startsWith('{')) {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Parse JSON
|
|
343
|
-
const parsed = JSON.parse(jsonContent);
|
|
344
|
-
|
|
345
|
-
// Skip system init messages
|
|
346
|
-
if (parsed.type === 'system' && parsed.subtype === 'init') {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Convert to cluster message format
|
|
351
|
-
messages.push({
|
|
352
|
-
id: `task-${taskId}-${timestamp}`,
|
|
353
|
-
timestamp,
|
|
354
|
-
topic: 'AGENT_OUTPUT',
|
|
355
|
-
sender: agent.id,
|
|
356
|
-
receiver: 'broadcast',
|
|
357
|
-
cluster_id: cluster.id,
|
|
358
|
-
content: {
|
|
359
|
-
text: jsonContent,
|
|
360
|
-
data: {
|
|
361
|
-
type: 'stdout',
|
|
362
|
-
line: jsonContent,
|
|
363
|
-
agent: agent.id,
|
|
364
|
-
role: agent.role,
|
|
365
|
-
iteration: state.iteration,
|
|
366
|
-
fromTaskLog: true, // Mark as coming from task log
|
|
367
|
-
},
|
|
368
|
-
},
|
|
369
|
-
});
|
|
370
|
-
} catch {
|
|
371
|
-
// Skip invalid JSON
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
} catch (err) {
|
|
375
|
-
// Log file read error - skip this task
|
|
376
|
-
console.warn(`Warning: Could not read log for ${taskId}: ${err.message}`);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return messages;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Setup shell completion
|
|
384
|
-
setupCompletion();
|
|
385
|
-
|
|
386
|
-
// Banner disabled
|
|
387
|
-
function showBanner() {
|
|
388
|
-
// Banner removed for cleaner output
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Show banner on startup (but not for completion, help, or daemon child)
|
|
392
|
-
const shouldShowBanner =
|
|
393
|
-
!process.env.ZEROSHOT_DAEMON &&
|
|
394
|
-
!process.argv.includes('--completion') &&
|
|
395
|
-
!process.argv.includes('-h') &&
|
|
396
|
-
!process.argv.includes('--help') &&
|
|
397
|
-
process.argv.length > 2;
|
|
398
|
-
if (shouldShowBanner) {
|
|
399
|
-
showBanner();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// NOTE: Agent color handling moved to message-formatter-utils.js
|
|
403
|
-
|
|
404
|
-
program
|
|
405
|
-
.name('zeroshot')
|
|
406
|
-
.description('Multi-agent orchestration and task management for Claude, Codex, and Gemini')
|
|
407
|
-
.version(require('../package.json').version)
|
|
408
|
-
.option('-q, --quiet', 'Suppress prompts (first-run wizard, update checks)')
|
|
409
|
-
.addHelpText(
|
|
410
|
-
'after',
|
|
411
|
-
`
|
|
412
|
-
Examples:
|
|
413
|
-
${chalk.cyan('zeroshot run 123 --ship')} Full automation: isolated + auto-merge PR
|
|
414
|
-
${chalk.cyan('zeroshot run 123')} Run cluster from GitHub issue
|
|
415
|
-
${chalk.cyan('zeroshot run feature.md')} Run cluster from markdown file
|
|
416
|
-
${chalk.cyan('zeroshot run "Implement feature X"')} Run cluster from plain text
|
|
417
|
-
${chalk.cyan('zeroshot run 123 -d')} Run in background (detached)
|
|
418
|
-
${chalk.cyan('zeroshot run 123 --docker')} Run in Docker container (safe for e2e tests)
|
|
419
|
-
${chalk.cyan('zeroshot task run "Fix the bug"')} Run single-agent background task
|
|
420
|
-
${chalk.cyan('zeroshot list')} List all tasks and clusters
|
|
421
|
-
${chalk.cyan('zeroshot task list')} List tasks only
|
|
422
|
-
${chalk.cyan('zeroshot task watch')} Interactive TUI - navigate tasks, view logs
|
|
423
|
-
${chalk.cyan('zeroshot attach <id>')} Attach to running task (Ctrl+B d to detach)
|
|
424
|
-
${chalk.cyan('zeroshot logs -f')} Stream logs in real-time (like tail -f)
|
|
425
|
-
${chalk.cyan('zeroshot logs -w')} Interactive watch mode (for tasks)
|
|
426
|
-
${chalk.cyan('zeroshot logs <id> -f')} Stream logs for specific cluster/task
|
|
427
|
-
${chalk.cyan('zeroshot status <id>')} Detailed status of task or cluster
|
|
428
|
-
${chalk.cyan('zeroshot finish <id>')} Convert cluster to completion task (creates and merges PR)
|
|
429
|
-
${chalk.cyan('zeroshot kill <id>')} Kill a running task or cluster
|
|
430
|
-
${chalk.cyan('zeroshot purge')} Kill all processes and delete all data (with confirmation)
|
|
431
|
-
${chalk.cyan('zeroshot purge -y')} Purge everything without confirmation
|
|
432
|
-
${chalk.cyan('zeroshot settings')} Show/manage zeroshot settings (maxModel, config, etc.)
|
|
433
|
-
${chalk.cyan('zeroshot settings set <key> <val>')} Set a setting (e.g., maxModel haiku)
|
|
434
|
-
${chalk.cyan('zeroshot providers')} Show provider status and defaults
|
|
435
|
-
${chalk.cyan('zeroshot config list')} List available cluster configs
|
|
436
|
-
${chalk.cyan('zeroshot config show <name>')} Visualize a cluster config (agents, triggers, flow)
|
|
437
|
-
${chalk.cyan('zeroshot export <id>')} Export cluster conversation to file
|
|
438
|
-
|
|
439
|
-
Automation levels (cascading: --ship → --pr → --worktree):
|
|
440
|
-
${chalk.yellow('zeroshot run 123')} → Local run, no isolation
|
|
441
|
-
${chalk.yellow('zeroshot run 123 --docker')} → Docker isolation, no PR
|
|
442
|
-
${chalk.yellow('zeroshot run 123 --worktree')} → Git worktree isolation, no PR
|
|
443
|
-
${chalk.yellow('zeroshot run 123 --pr')} → Worktree + PR (human reviews)
|
|
444
|
-
${chalk.yellow('zeroshot run 123 --ship')} → Worktree + PR + auto-merge (full automation)
|
|
445
|
-
${chalk.yellow('zeroshot task run')} → Single-agent background task (simpler, faster)
|
|
446
|
-
|
|
447
|
-
Shell completion:
|
|
448
|
-
${chalk.dim('zeroshot --completion >> ~/.bashrc && source ~/.bashrc')}
|
|
449
|
-
`
|
|
450
|
-
);
|
|
451
|
-
|
|
452
|
-
// Run command - CLUSTER with auto-detection
|
|
453
|
-
program
|
|
454
|
-
.command('run <input>')
|
|
455
|
-
.description('Start a multi-agent cluster (GitHub issue, markdown file, or plain text)')
|
|
456
|
-
.option('--config <file>', 'Path to cluster config JSON (default: conductor-bootstrap)')
|
|
457
|
-
.option('--docker', 'Run cluster inside Docker container (full isolation)')
|
|
458
|
-
.option('--worktree', 'Use git worktree for isolation (lightweight, no Docker required)')
|
|
459
|
-
.option(
|
|
460
|
-
'--docker-image <image>',
|
|
461
|
-
'Docker image for --docker mode (default: zeroshot-cluster-base)'
|
|
462
|
-
)
|
|
463
|
-
.option(
|
|
464
|
-
'--strict-schema',
|
|
465
|
-
'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
|
|
466
|
-
)
|
|
467
|
-
.option(
|
|
468
|
-
'--pr',
|
|
469
|
-
'Create PR for human review (uses worktree isolation by default, use --docker for Docker)'
|
|
470
|
-
)
|
|
471
|
-
.option(
|
|
472
|
-
'--ship',
|
|
473
|
-
'Full automation: worktree isolation + PR + auto-merge (use --docker for Docker)'
|
|
474
|
-
)
|
|
475
|
-
.option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
|
|
476
|
-
.option('--provider <provider>', 'Override all agents to use a provider (claude, codex, gemini)')
|
|
477
|
-
.option('--model <model>', 'Override all agent models (provider-specific model id)')
|
|
478
|
-
.option('-d, --detach', 'Run in background (default: attach to first agent)')
|
|
479
|
-
.option('--mount <spec...>', 'Add Docker mount (host:container[:ro]). Repeatable.')
|
|
480
|
-
.option('--no-mounts', 'Disable all Docker credential mounts')
|
|
481
|
-
.option(
|
|
482
|
-
'--container-home <path>',
|
|
483
|
-
'Container home directory for $HOME expansion (default: /root)'
|
|
484
|
-
)
|
|
485
|
-
.addHelpText(
|
|
486
|
-
'after',
|
|
487
|
-
`
|
|
488
|
-
Input formats:
|
|
489
|
-
123 GitHub issue number (uses current repo)
|
|
490
|
-
org/repo#123 GitHub issue with explicit repo
|
|
491
|
-
https://github.com/.../issues/1 Full GitHub issue URL
|
|
492
|
-
"Implement feature X" Plain text task description
|
|
493
|
-
`
|
|
494
|
-
)
|
|
495
|
-
.action(async (inputArg, options) => {
|
|
496
|
-
try {
|
|
497
|
-
// Cascading flag implications: --ship → --pr → worktree (unless --docker)
|
|
498
|
-
// --ship = full automation (worktree isolation + PR + auto-merge)
|
|
499
|
-
if (options.ship) {
|
|
500
|
-
options.pr = true;
|
|
501
|
-
// Use worktree by default, Docker only if explicitly requested
|
|
502
|
-
if (!options.docker) {
|
|
503
|
-
options.worktree = true;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
// --pr = PR for human review (worktree by default, Docker if requested)
|
|
507
|
-
if (options.pr && !options.docker && !options.worktree) {
|
|
508
|
-
options.worktree = true;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Mutual exclusivity: --docker explicitly disables worktree
|
|
512
|
-
if (options.docker) {
|
|
513
|
-
options.worktree = false;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Auto-detect input type
|
|
517
|
-
let input = {};
|
|
518
|
-
|
|
519
|
-
// Check if it's a GitHub issue URL
|
|
520
|
-
if (inputArg.match(/^https?:\/\/github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/)) {
|
|
521
|
-
input.issue = inputArg;
|
|
522
|
-
}
|
|
523
|
-
// Check if it's a GitHub issue number (just digits)
|
|
524
|
-
else if (/^\d+$/.test(inputArg)) {
|
|
525
|
-
input.issue = inputArg;
|
|
526
|
-
}
|
|
527
|
-
// Check if it's org/repo#123 format
|
|
528
|
-
else if (inputArg.match(/^[\w-]+\/[\w-]+#\d+$/)) {
|
|
529
|
-
input.issue = inputArg;
|
|
530
|
-
}
|
|
531
|
-
// Check if it's a markdown file (.md or .markdown)
|
|
532
|
-
else if (/\.(md|markdown)$/i.test(inputArg)) {
|
|
533
|
-
input.file = inputArg;
|
|
534
|
-
}
|
|
535
|
-
// Otherwise, treat as plain text
|
|
536
|
-
else {
|
|
537
|
-
input.text = inputArg;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// === PREFLIGHT CHECKS ===
|
|
541
|
-
// Validate all dependencies BEFORE starting anything
|
|
542
|
-
// This gives users clear, actionable error messages upfront
|
|
543
|
-
const settings = loadSettings();
|
|
544
|
-
const providerOverride = normalizeProviderName(
|
|
545
|
-
options.provider || process.env.ZEROSHOT_PROVIDER || settings.defaultProvider
|
|
546
|
-
);
|
|
547
|
-
const preflightOptions = {
|
|
548
|
-
requireGh: !!input.issue, // gh CLI required when fetching GitHub issues
|
|
549
|
-
requireDocker: options.docker, // Docker required for --docker mode
|
|
550
|
-
requireGit: options.worktree, // Git required for worktree isolation
|
|
551
|
-
quiet: process.env.ZEROSHOT_DAEMON === '1', // Suppress success in daemon mode
|
|
552
|
-
provider: providerOverride,
|
|
553
|
-
};
|
|
554
|
-
requirePreflight(preflightOptions);
|
|
555
|
-
|
|
556
|
-
// === CLUSTER MODE ===
|
|
557
|
-
|
|
558
|
-
const { generateName } = require('../src/name-generator');
|
|
559
|
-
|
|
560
|
-
// === DETACHED MODE (-d flag) ===
|
|
561
|
-
// Spawn daemon and exit immediately
|
|
562
|
-
if (options.detach && !process.env.ZEROSHOT_DAEMON) {
|
|
563
|
-
const { spawn } = require('child_process');
|
|
564
|
-
|
|
565
|
-
// Generate cluster ID in parent so we can display it
|
|
566
|
-
const clusterId = generateName('cluster');
|
|
567
|
-
|
|
568
|
-
// Output cluster ID and help
|
|
569
|
-
if (options.docker) {
|
|
570
|
-
console.log(`Started ${clusterId} (docker)`);
|
|
571
|
-
} else {
|
|
572
|
-
console.log(`Started ${clusterId}`);
|
|
573
|
-
}
|
|
574
|
-
console.log(`Monitor: zeroshot logs ${clusterId} -f`);
|
|
575
|
-
console.log(`Attach: zeroshot attach ${clusterId}`);
|
|
576
|
-
|
|
577
|
-
// Create log file for daemon output (captures startup errors)
|
|
578
|
-
const osModule = require('os');
|
|
579
|
-
const storageDir = path.join(osModule.homedir(), '.zeroshot');
|
|
580
|
-
if (!fs.existsSync(storageDir)) {
|
|
581
|
-
fs.mkdirSync(storageDir, { recursive: true });
|
|
582
|
-
}
|
|
583
|
-
const logPath = path.join(storageDir, `${clusterId}-daemon.log`);
|
|
584
|
-
const logFd = fs.openSync(logPath, 'w');
|
|
585
|
-
|
|
586
|
-
// Detect git repo root for CWD propagation
|
|
587
|
-
// CRITICAL: Agents must work in the target repo, not where CLI was invoked
|
|
588
|
-
const targetCwd = detectGitRepoRoot();
|
|
589
|
-
|
|
590
|
-
// Spawn ourselves as daemon (detached, logs to file)
|
|
591
|
-
const daemon = spawn(process.execPath, process.argv.slice(1), {
|
|
592
|
-
detached: true,
|
|
593
|
-
stdio: ['ignore', logFd, logFd], // stdout + stderr go to log file
|
|
594
|
-
cwd: targetCwd, // Daemon inherits correct working directory
|
|
595
|
-
env: {
|
|
596
|
-
...process.env,
|
|
597
|
-
ZEROSHOT_DAEMON: '1',
|
|
598
|
-
ZEROSHOT_CLUSTER_ID: clusterId,
|
|
599
|
-
ZEROSHOT_DOCKER: options.docker ? '1' : '',
|
|
600
|
-
ZEROSHOT_DOCKER_IMAGE: options.dockerImage || '',
|
|
601
|
-
ZEROSHOT_PR: options.pr ? '1' : '',
|
|
602
|
-
ZEROSHOT_WORKTREE: options.worktree ? '1' : '',
|
|
603
|
-
ZEROSHOT_WORKERS: options.workers?.toString() || '',
|
|
604
|
-
ZEROSHOT_MODEL: options.model || '',
|
|
605
|
-
ZEROSHOT_PROVIDER: options.provider || '',
|
|
606
|
-
ZEROSHOT_CWD: targetCwd, // Explicit CWD for orchestrator
|
|
607
|
-
},
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
daemon.unref();
|
|
611
|
-
fs.closeSync(logFd);
|
|
612
|
-
process.exit(0);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// === FOREGROUND MODE (default) or DAEMON CHILD ===
|
|
616
|
-
|
|
617
|
-
// Use cluster ID from env (daemon mode) or generate new one (foreground mode)
|
|
618
|
-
// IMPORTANT: Set env var so orchestrator picks it up
|
|
619
|
-
const clusterId = process.env.ZEROSHOT_CLUSTER_ID || generateName('cluster');
|
|
620
|
-
process.env.ZEROSHOT_CLUSTER_ID = clusterId;
|
|
621
|
-
|
|
622
|
-
// === LOAD CONFIG ===
|
|
623
|
-
// Priority: CLI --config > settings.defaultConfig
|
|
624
|
-
let config;
|
|
625
|
-
const configName = options.config || settings.defaultConfig;
|
|
626
|
-
|
|
627
|
-
// Resolve config path (check examples/ directory if not absolute/relative path)
|
|
628
|
-
let configPath;
|
|
629
|
-
if (
|
|
630
|
-
path.isAbsolute(configName) ||
|
|
631
|
-
configName.startsWith('./') ||
|
|
632
|
-
configName.startsWith('../')
|
|
633
|
-
) {
|
|
634
|
-
configPath = path.resolve(process.cwd(), configName);
|
|
635
|
-
} else if (configName.endsWith('.json')) {
|
|
636
|
-
// If it has .json extension, check examples/ directory
|
|
637
|
-
configPath = path.join(PACKAGE_ROOT, 'cluster-templates', configName);
|
|
638
|
-
} else {
|
|
639
|
-
// Otherwise assume it's a template name (add .json)
|
|
640
|
-
configPath = path.join(PACKAGE_ROOT, 'cluster-templates', `${configName}.json`);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Create orchestrator with clusterId override for foreground mode
|
|
644
|
-
const orchestrator = await getOrchestrator();
|
|
645
|
-
config = orchestrator.loadConfig(configPath);
|
|
646
|
-
|
|
647
|
-
if (!config.defaultProvider) {
|
|
648
|
-
config.defaultProvider = settings.defaultProvider || 'claude';
|
|
649
|
-
}
|
|
650
|
-
config.defaultProvider = normalizeProviderName(config.defaultProvider) || 'claude';
|
|
651
|
-
|
|
652
|
-
if (providerOverride) {
|
|
653
|
-
const provider = getProvider(providerOverride);
|
|
654
|
-
const providerSettings = settings.providerSettings?.[providerOverride] || {};
|
|
655
|
-
config.forceProvider = providerOverride;
|
|
656
|
-
config.defaultProvider = providerOverride;
|
|
657
|
-
config.forceLevel = providerSettings.defaultLevel || provider.getDefaultLevel();
|
|
658
|
-
config.defaultLevel = config.forceLevel;
|
|
659
|
-
console.log(chalk.dim(`Provider override: ${providerOverride} (all agents)`));
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Track for global error handler cleanup
|
|
663
|
-
activeClusterId = clusterId;
|
|
664
|
-
orchestratorInstance = orchestrator;
|
|
665
|
-
|
|
666
|
-
// In foreground mode, show startup info
|
|
667
|
-
if (!process.env.ZEROSHOT_DAEMON) {
|
|
668
|
-
if (options.docker) {
|
|
669
|
-
console.log(`Starting ${clusterId} (docker)`);
|
|
670
|
-
} else if (options.worktree) {
|
|
671
|
-
console.log(`Starting ${clusterId} (worktree)`);
|
|
672
|
-
} else {
|
|
673
|
-
console.log(`Starting ${clusterId}`);
|
|
674
|
-
}
|
|
675
|
-
console.log(chalk.dim(`Config: ${configName}`));
|
|
676
|
-
console.log(chalk.dim('Ctrl+C to stop following (cluster keeps running)\n'));
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Apply strictSchema setting to all agents (CLI > env > settings)
|
|
680
|
-
const strictSchema =
|
|
681
|
-
options.strictSchema || process.env.ZEROSHOT_STRICT_SCHEMA === '1' || settings.strictSchema;
|
|
682
|
-
if (strictSchema) {
|
|
683
|
-
for (const agent of config.agents) {
|
|
684
|
-
agent.strictSchema = true;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Apply model override to all agents (CLI > env)
|
|
689
|
-
const modelOverride = options.model || process.env.ZEROSHOT_MODEL;
|
|
690
|
-
if (modelOverride) {
|
|
691
|
-
const providerName = normalizeProviderName(
|
|
692
|
-
providerOverride || config.defaultProvider || settings.defaultProvider || 'claude'
|
|
693
|
-
);
|
|
694
|
-
const provider = getProvider(providerName);
|
|
695
|
-
const catalog = provider.getModelCatalog();
|
|
696
|
-
|
|
697
|
-
if (catalog && !catalog[modelOverride]) {
|
|
698
|
-
console.warn(
|
|
699
|
-
chalk.yellow(
|
|
700
|
-
`Warning: model override "${modelOverride}" is not in the ${providerName} catalog`
|
|
701
|
-
)
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (providerName === 'claude' && ['opus', 'sonnet', 'haiku'].includes(modelOverride)) {
|
|
706
|
-
const { validateModelAgainstMax } = require('../lib/settings');
|
|
707
|
-
try {
|
|
708
|
-
validateModelAgainstMax(modelOverride, settings.maxModel);
|
|
709
|
-
} catch (err) {
|
|
710
|
-
console.error(chalk.red(`Error: ${err.message}`));
|
|
711
|
-
process.exit(1);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Override all agent models
|
|
716
|
-
for (const agent of config.agents) {
|
|
717
|
-
agent.model = modelOverride;
|
|
718
|
-
if (agent.modelRules) {
|
|
719
|
-
delete agent.modelRules;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
console.log(chalk.dim(`Model override: ${modelOverride} (all agents)`));
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Build start options (CLI flags > env vars > settings)
|
|
726
|
-
// In foreground mode, use CLI options directly; in daemon mode, use env vars
|
|
727
|
-
// CRITICAL: cwd must be passed to orchestrator for agent CWD propagation
|
|
728
|
-
const targetCwd = process.env.ZEROSHOT_CWD || detectGitRepoRoot();
|
|
729
|
-
const startOptions = {
|
|
730
|
-
clusterId,
|
|
731
|
-
cwd: targetCwd, // Target working directory for agents
|
|
732
|
-
isolation: options.docker || process.env.ZEROSHOT_DOCKER === '1' || settings.defaultDocker,
|
|
733
|
-
isolationImage: options.dockerImage || process.env.ZEROSHOT_DOCKER_IMAGE || undefined,
|
|
734
|
-
worktree: options.worktree || process.env.ZEROSHOT_WORKTREE === '1',
|
|
735
|
-
autoPr: options.pr || process.env.ZEROSHOT_PR === '1',
|
|
736
|
-
autoMerge: process.env.ZEROSHOT_MERGE === '1',
|
|
737
|
-
autoPush: process.env.ZEROSHOT_PUSH === '1',
|
|
738
|
-
// Model override (for dynamically added agents)
|
|
739
|
-
modelOverride: modelOverride || undefined,
|
|
740
|
-
providerOverride: providerOverride || undefined,
|
|
741
|
-
// Docker mount options
|
|
742
|
-
noMounts: options.noMounts || false,
|
|
743
|
-
mounts: options.mount ? parseMountSpecs(options.mount) : undefined,
|
|
744
|
-
containerHome: options.containerHome || undefined,
|
|
745
|
-
};
|
|
746
|
-
|
|
747
|
-
// Start cluster
|
|
748
|
-
const cluster = await orchestrator.start(config, input, startOptions);
|
|
749
|
-
|
|
750
|
-
// === FOREGROUND MODE: Stream logs in real-time ===
|
|
751
|
-
// Subscribe to message bus directly (same process) for instant output
|
|
752
|
-
if (!process.env.ZEROSHOT_DAEMON) {
|
|
753
|
-
// Track senders that have output (for periodic flushing)
|
|
754
|
-
const sendersWithOutput = new Set();
|
|
755
|
-
// Track messages we've already processed (to avoid duplicates between history and subscription)
|
|
756
|
-
const processedMessageIds = new Set();
|
|
757
|
-
|
|
758
|
-
// === STATUS FOOTER: Live agent monitoring ===
|
|
759
|
-
// Shows CPU, memory, network metrics for all agents at bottom of terminal
|
|
760
|
-
const statusFooter = new StatusFooter({
|
|
761
|
-
refreshInterval: 1000,
|
|
762
|
-
enabled: process.stdout.isTTY,
|
|
763
|
-
});
|
|
764
|
-
statusFooter.setCluster(clusterId);
|
|
765
|
-
statusFooter.setClusterState('running');
|
|
766
|
-
statusFooter.setMessageBus(cluster.messageBus);
|
|
767
|
-
// Set module-level reference so safePrint/safeWrite route through footer
|
|
768
|
-
activeStatusFooter = statusFooter;
|
|
769
|
-
|
|
770
|
-
// Subscribe to AGENT_LIFECYCLE to track agent states and PIDs
|
|
771
|
-
const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic('AGENT_LIFECYCLE', (msg) => {
|
|
772
|
-
const data = msg.content?.data || {};
|
|
773
|
-
const event = data.event;
|
|
774
|
-
const agentId = data.agent || msg.sender;
|
|
775
|
-
|
|
776
|
-
// Update agent state based on lifecycle event
|
|
777
|
-
if (event === 'STARTED') {
|
|
778
|
-
statusFooter.updateAgent({
|
|
779
|
-
id: agentId,
|
|
780
|
-
state: AGENT_STATE.IDLE,
|
|
781
|
-
pid: null,
|
|
782
|
-
iteration: data.iteration || 0,
|
|
783
|
-
});
|
|
784
|
-
} else if (event === 'TASK_STARTED') {
|
|
785
|
-
statusFooter.updateAgent({
|
|
786
|
-
id: agentId,
|
|
787
|
-
state: AGENT_STATE.EXECUTING_TASK,
|
|
788
|
-
pid: statusFooter.agents.get(agentId)?.pid || null,
|
|
789
|
-
iteration: data.iteration || 0,
|
|
790
|
-
});
|
|
791
|
-
} else if (event === 'PROCESS_SPAWNED') {
|
|
792
|
-
// PROCESS_SPAWNED = proof of execution. If a process spawned, agent is executing.
|
|
793
|
-
const current = statusFooter.agents.get(agentId) || { iteration: 0 };
|
|
794
|
-
statusFooter.updateAgent({
|
|
795
|
-
id: agentId,
|
|
796
|
-
state: AGENT_STATE.EXECUTING_TASK,
|
|
797
|
-
pid: data.pid,
|
|
798
|
-
iteration: current.iteration,
|
|
799
|
-
});
|
|
800
|
-
} else if (event === 'TASK_COMPLETED' || event === 'TASK_FAILED') {
|
|
801
|
-
statusFooter.updateAgent({
|
|
802
|
-
id: agentId,
|
|
803
|
-
state: AGENT_STATE.IDLE,
|
|
804
|
-
pid: null,
|
|
805
|
-
iteration: data.iteration || 0,
|
|
806
|
-
});
|
|
807
|
-
} else if (event === 'STOPPED') {
|
|
808
|
-
statusFooter.removeAgent(agentId);
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
// Start the status footer
|
|
813
|
-
statusFooter.start();
|
|
814
|
-
|
|
815
|
-
// Message handler - processes messages, deduplicates by ID
|
|
816
|
-
const handleMessage = (msg) => {
|
|
817
|
-
if (msg.cluster_id !== clusterId) return;
|
|
818
|
-
if (processedMessageIds.has(msg.id)) return;
|
|
819
|
-
processedMessageIds.add(msg.id);
|
|
820
|
-
|
|
821
|
-
if (msg.topic === 'AGENT_OUTPUT' && msg.sender) {
|
|
822
|
-
sendersWithOutput.add(msg.sender);
|
|
823
|
-
}
|
|
824
|
-
printMessage(msg, false, false, true);
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
// Subscribe to NEW messages
|
|
828
|
-
const unsubscribe = cluster.messageBus.subscribe(handleMessage);
|
|
829
|
-
|
|
830
|
-
// CRITICAL: Replay historical messages that may have been published BEFORE we subscribed
|
|
831
|
-
// This fixes the race condition where fast-completing clusters miss output
|
|
832
|
-
const historicalMessages = cluster.messageBus.getAll(clusterId);
|
|
833
|
-
for (const msg of historicalMessages) {
|
|
834
|
-
handleMessage(msg);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Periodic flush of text buffers (streaming text may not have newlines)
|
|
838
|
-
const flushInterval = setInterval(() => {
|
|
839
|
-
for (const sender of sendersWithOutput) {
|
|
840
|
-
const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
|
|
841
|
-
flushLineBuffer(prefix, sender);
|
|
842
|
-
}
|
|
843
|
-
}, 250);
|
|
844
|
-
|
|
845
|
-
// Wait for cluster to complete
|
|
846
|
-
await new Promise((resolve) => {
|
|
847
|
-
const checkInterval = setInterval(() => {
|
|
848
|
-
try {
|
|
849
|
-
const status = orchestrator.getStatus(clusterId);
|
|
850
|
-
if (status.state !== 'running') {
|
|
851
|
-
clearInterval(checkInterval);
|
|
852
|
-
clearInterval(flushInterval);
|
|
853
|
-
lifecycleUnsubscribe();
|
|
854
|
-
// Final flush BEFORE stopping status footer
|
|
855
|
-
// (statusFooter.stop() sends ANSI codes that can clear terminal area)
|
|
856
|
-
for (const sender of sendersWithOutput) {
|
|
857
|
-
const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
|
|
858
|
-
flushLineBuffer(prefix, sender);
|
|
859
|
-
}
|
|
860
|
-
// Stop status footer AFTER output is done
|
|
861
|
-
statusFooter.stop();
|
|
862
|
-
activeStatusFooter = null;
|
|
863
|
-
unsubscribe();
|
|
864
|
-
resolve();
|
|
865
|
-
}
|
|
866
|
-
} catch {
|
|
867
|
-
// Cluster may have been removed
|
|
868
|
-
clearInterval(checkInterval);
|
|
869
|
-
clearInterval(flushInterval);
|
|
870
|
-
statusFooter.stop();
|
|
871
|
-
activeStatusFooter = null;
|
|
872
|
-
lifecycleUnsubscribe();
|
|
873
|
-
unsubscribe();
|
|
874
|
-
resolve();
|
|
875
|
-
}
|
|
876
|
-
}, 500);
|
|
877
|
-
|
|
878
|
-
// Handle Ctrl+C: Stop cluster since foreground mode has no daemon
|
|
879
|
-
// CRITICAL: In foreground mode, the cluster runs IN this process.
|
|
880
|
-
// If we exit without stopping, the cluster becomes a zombie (state=running but no process).
|
|
881
|
-
process.on('SIGINT', async () => {
|
|
882
|
-
// Stop status footer first to restore terminal
|
|
883
|
-
statusFooter.stop();
|
|
884
|
-
activeStatusFooter = null;
|
|
885
|
-
lifecycleUnsubscribe();
|
|
886
|
-
|
|
887
|
-
console.log(chalk.dim('\n\n--- Interrupted ---'));
|
|
888
|
-
clearInterval(checkInterval);
|
|
889
|
-
clearInterval(flushInterval);
|
|
890
|
-
unsubscribe();
|
|
891
|
-
|
|
892
|
-
// Stop the cluster properly so state is updated
|
|
893
|
-
try {
|
|
894
|
-
console.log(chalk.dim(`Stopping cluster ${clusterId}...`));
|
|
895
|
-
await orchestrator.stop(clusterId);
|
|
896
|
-
console.log(chalk.dim(`Cluster ${clusterId} stopped.`));
|
|
897
|
-
} catch (stopErr) {
|
|
898
|
-
console.error(chalk.red(`Failed to stop cluster: ${stopErr.message}`));
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
process.exit(0);
|
|
902
|
-
});
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
console.log(chalk.dim(`\nCluster ${clusterId} completed.`));
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Daemon mode: cluster runs in background, stay alive via orchestrator's setInterval
|
|
909
|
-
// Add cleanup handlers for daemon mode to ensure container cleanup on process exit
|
|
910
|
-
// CRITICAL: Without this, containers become orphaned when daemon process dies
|
|
911
|
-
if (process.env.ZEROSHOT_DAEMON) {
|
|
912
|
-
const cleanup = async (signal) => {
|
|
913
|
-
console.log(`\n[DAEMON] Received ${signal}, cleaning up cluster ${clusterId}...`);
|
|
914
|
-
try {
|
|
915
|
-
await orchestrator.stop(clusterId);
|
|
916
|
-
console.log(`[DAEMON] Cluster ${clusterId} stopped.`);
|
|
917
|
-
} catch (e) {
|
|
918
|
-
console.error(`[DAEMON] Cleanup error: ${e.message}`);
|
|
919
|
-
}
|
|
920
|
-
process.exit(0);
|
|
921
|
-
};
|
|
922
|
-
process.on('SIGTERM', () => cleanup('SIGTERM'));
|
|
923
|
-
process.on('SIGINT', () => cleanup('SIGINT'));
|
|
924
|
-
}
|
|
925
|
-
} catch (error) {
|
|
926
|
-
console.error('Error:', error.message);
|
|
927
|
-
process.exit(1);
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
// === TASK COMMANDS ===
|
|
932
|
-
// Task run - single-agent background task
|
|
933
|
-
const taskCmd = program.command('task').description('Single-agent task management');
|
|
934
|
-
|
|
935
|
-
taskCmd
|
|
936
|
-
.command('run <prompt>')
|
|
937
|
-
.description('Run a single-agent background task')
|
|
938
|
-
.option('-C, --cwd <path>', 'Working directory for task')
|
|
939
|
-
.option('--provider <provider>', 'Provider to use (claude, codex, gemini)')
|
|
940
|
-
.option('--model <model>', 'Model id override for the provider')
|
|
941
|
-
.option('--model-level <level>', 'Model level override (level1, level2, level3)')
|
|
942
|
-
.option('--reasoning-effort <effort>', 'Reasoning effort (low, medium, high, xhigh)')
|
|
943
|
-
.option('-r, --resume <sessionId>', 'Resume a specific Claude session (claude only)')
|
|
944
|
-
.option('-c, --continue', 'Continue the most recent Claude session (claude only)')
|
|
945
|
-
.option(
|
|
946
|
-
'-o, --output-format <format>',
|
|
947
|
-
'Output format: stream-json (default), text, json',
|
|
948
|
-
'stream-json'
|
|
949
|
-
)
|
|
950
|
-
.option('--json-schema <schema>', 'JSON schema for structured output')
|
|
951
|
-
.option('--silent-json-output', 'Log ONLY final structured output')
|
|
952
|
-
.action(async (prompt, options) => {
|
|
953
|
-
try {
|
|
954
|
-
// === PREFLIGHT CHECKS ===
|
|
955
|
-
// Provider CLI must be installed for task execution
|
|
956
|
-
const settings = loadSettings();
|
|
957
|
-
const providerOverride = normalizeProviderName(
|
|
958
|
-
options.provider || process.env.ZEROSHOT_PROVIDER || settings.defaultProvider
|
|
959
|
-
);
|
|
960
|
-
requirePreflight({
|
|
961
|
-
requireGh: false, // gh not needed for plain tasks
|
|
962
|
-
requireDocker: false, // Docker not needed for plain tasks
|
|
963
|
-
quiet: false,
|
|
964
|
-
provider: providerOverride,
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
// Dynamically import task command (ESM module)
|
|
968
|
-
const { runTask } = await import('../task-lib/commands/run.js');
|
|
969
|
-
await runTask(prompt, options);
|
|
970
|
-
} catch (error) {
|
|
971
|
-
console.error('Error:', error.message);
|
|
972
|
-
process.exit(1);
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
taskCmd
|
|
977
|
-
.command('list')
|
|
978
|
-
.alias('ls')
|
|
979
|
-
.description('List all tasks (use "zeroshot list" to see both tasks and clusters)')
|
|
980
|
-
.option('-s, --status <status>', 'Filter tasks by status (running, completed, failed)')
|
|
981
|
-
.option('-n, --limit <n>', 'Limit number of results', parseInt)
|
|
982
|
-
.option('-v, --verbose', 'Show detailed information (default: table view)')
|
|
983
|
-
.action(async (options) => {
|
|
984
|
-
try {
|
|
985
|
-
// Get tasks only (dynamic import)
|
|
986
|
-
const { listTasks } = await import('../task-lib/commands/list.js');
|
|
987
|
-
await listTasks(options);
|
|
988
|
-
} catch (error) {
|
|
989
|
-
console.error('Error listing tasks:', error.message);
|
|
990
|
-
process.exit(1);
|
|
991
|
-
}
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
taskCmd
|
|
995
|
-
.command('watch')
|
|
996
|
-
.description('Interactive TUI for tasks (navigate and view logs)')
|
|
997
|
-
.option('--refresh-rate <ms>', 'Refresh interval in milliseconds', '1000')
|
|
998
|
-
.action(async (options) => {
|
|
999
|
-
try {
|
|
1000
|
-
const TaskTUI = (await import('../task-lib/tui.js')).default;
|
|
1001
|
-
const tui = new TaskTUI({
|
|
1002
|
-
refreshRate: parseInt(options.refreshRate, 10),
|
|
1003
|
-
});
|
|
1004
|
-
await tui.start();
|
|
1005
|
-
} catch (error) {
|
|
1006
|
-
console.error('Error starting task TUI:', error.message);
|
|
1007
|
-
console.error(error.stack);
|
|
1008
|
-
process.exit(1);
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
// List command - unified (shows both tasks and clusters)
|
|
1013
|
-
program
|
|
1014
|
-
.command('list')
|
|
1015
|
-
.alias('ls')
|
|
1016
|
-
.description('List all tasks and clusters')
|
|
1017
|
-
.option('-s, --status <status>', 'Filter tasks by status (running, completed, failed)')
|
|
1018
|
-
.option('-n, --limit <n>', 'Limit number of results', parseInt)
|
|
1019
|
-
.option('--json', 'Output as JSON')
|
|
1020
|
-
.action(async (options) => {
|
|
1021
|
-
try {
|
|
1022
|
-
// Get clusters
|
|
1023
|
-
const clusters = (await getOrchestrator()).listClusters();
|
|
1024
|
-
const orchestrator = await getOrchestrator();
|
|
1025
|
-
|
|
1026
|
-
// Enrich clusters with token data
|
|
1027
|
-
const enrichedClusters = clusters.map((cluster) => {
|
|
1028
|
-
let totalTokens = 0;
|
|
1029
|
-
let totalCostUsd = 0;
|
|
1030
|
-
try {
|
|
1031
|
-
const clusterObj = orchestrator.getCluster(cluster.id);
|
|
1032
|
-
if (clusterObj?.messageBus) {
|
|
1033
|
-
const tokensByRole = clusterObj.messageBus.getTokensByRole(cluster.id);
|
|
1034
|
-
if (tokensByRole?._total?.count > 0) {
|
|
1035
|
-
const total = tokensByRole._total;
|
|
1036
|
-
totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
|
|
1037
|
-
totalCostUsd = total.totalCostUsd || 0;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
} catch {
|
|
1041
|
-
/* Token tracking not available */
|
|
1042
|
-
}
|
|
1043
|
-
return {
|
|
1044
|
-
...cluster,
|
|
1045
|
-
totalTokens,
|
|
1046
|
-
totalCostUsd,
|
|
1047
|
-
};
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
// Get tasks (dynamic import)
|
|
1051
|
-
const { listTasks, getTasksData } = await import('../task-lib/commands/list.js');
|
|
1052
|
-
|
|
1053
|
-
// JSON output mode
|
|
1054
|
-
if (options.json) {
|
|
1055
|
-
// Get tasks data if available
|
|
1056
|
-
let tasks = [];
|
|
1057
|
-
try {
|
|
1058
|
-
if (typeof getTasksData === 'function') {
|
|
1059
|
-
tasks = await getTasksData(options);
|
|
1060
|
-
}
|
|
1061
|
-
} catch {
|
|
1062
|
-
/* Tasks not available */
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
console.log(
|
|
1066
|
-
JSON.stringify(
|
|
1067
|
-
{
|
|
1068
|
-
clusters: enrichedClusters,
|
|
1069
|
-
tasks,
|
|
1070
|
-
},
|
|
1071
|
-
null,
|
|
1072
|
-
2
|
|
1073
|
-
)
|
|
1074
|
-
);
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// Human-readable output (default)
|
|
1079
|
-
// Print clusters
|
|
1080
|
-
if (enrichedClusters.length > 0) {
|
|
1081
|
-
console.log(chalk.bold('\n=== Clusters ==='));
|
|
1082
|
-
console.log(
|
|
1083
|
-
`${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'Agents'.padEnd(8)} ${'Tokens'.padEnd(12)} ${'Cost'.padEnd(8)} Created`
|
|
1084
|
-
);
|
|
1085
|
-
console.log('-'.repeat(100));
|
|
1086
|
-
|
|
1087
|
-
for (const cluster of enrichedClusters) {
|
|
1088
|
-
const created = new Date(cluster.createdAt).toLocaleString();
|
|
1089
|
-
const tokenDisplay = cluster.totalTokens > 0 ? cluster.totalTokens.toLocaleString() : '-';
|
|
1090
|
-
const costDisplay =
|
|
1091
|
-
cluster.totalCostUsd > 0 ? '$' + cluster.totalCostUsd.toFixed(3) : '-';
|
|
1092
|
-
|
|
1093
|
-
// Highlight zombie clusters in red
|
|
1094
|
-
const stateDisplay =
|
|
1095
|
-
cluster.state === 'zombie'
|
|
1096
|
-
? chalk.red(cluster.state.padEnd(12))
|
|
1097
|
-
: cluster.state.padEnd(12);
|
|
1098
|
-
|
|
1099
|
-
const rowColor = cluster.state === 'zombie' ? chalk.red : (s) => s;
|
|
1100
|
-
|
|
1101
|
-
console.log(
|
|
1102
|
-
`${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${cluster.agentCount.toString().padEnd(8)} ${tokenDisplay.padEnd(12)} ${costDisplay.padEnd(8)} ${created}`
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
} else {
|
|
1106
|
-
console.log(chalk.dim('\n=== Clusters ==='));
|
|
1107
|
-
console.log('No active clusters');
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// Print tasks
|
|
1111
|
-
console.log(chalk.bold('\n=== Tasks ==='));
|
|
1112
|
-
await listTasks(options);
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
console.error('Error listing:', error.message);
|
|
1115
|
-
process.exit(1);
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
// Status command - smart (works for both tasks and clusters)
|
|
1120
|
-
program
|
|
1121
|
-
.command('status <id>')
|
|
1122
|
-
.description('Get detailed status of a task or cluster')
|
|
1123
|
-
.option('--json', 'Output as JSON')
|
|
1124
|
-
.action(async (id, options) => {
|
|
1125
|
-
try {
|
|
1126
|
-
const { detectIdType } = require('../lib/id-detector');
|
|
1127
|
-
const type = detectIdType(id);
|
|
1128
|
-
|
|
1129
|
-
if (!type) {
|
|
1130
|
-
if (options.json) {
|
|
1131
|
-
console.log(JSON.stringify({ error: 'ID not found', id }, null, 2));
|
|
1132
|
-
} else {
|
|
1133
|
-
console.error(`ID not found: ${id}`);
|
|
1134
|
-
console.error('Not found in tasks or clusters');
|
|
1135
|
-
}
|
|
1136
|
-
process.exit(1);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
if (type === 'cluster') {
|
|
1140
|
-
// Show cluster status
|
|
1141
|
-
const status = (await getOrchestrator()).getStatus(id);
|
|
1142
|
-
|
|
1143
|
-
// Get token usage
|
|
1144
|
-
let tokensByRole = null;
|
|
1145
|
-
try {
|
|
1146
|
-
const cluster = (await getOrchestrator()).getCluster(id);
|
|
1147
|
-
if (cluster?.messageBus) {
|
|
1148
|
-
tokensByRole = cluster.messageBus.getTokensByRole(id);
|
|
1149
|
-
}
|
|
1150
|
-
} catch {
|
|
1151
|
-
/* Token tracking not available */
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// JSON output mode
|
|
1155
|
-
if (options.json) {
|
|
1156
|
-
console.log(
|
|
1157
|
-
JSON.stringify(
|
|
1158
|
-
{
|
|
1159
|
-
type: 'cluster',
|
|
1160
|
-
...status,
|
|
1161
|
-
createdAtISO: new Date(status.createdAt).toISOString(),
|
|
1162
|
-
tokensByRole,
|
|
1163
|
-
},
|
|
1164
|
-
null,
|
|
1165
|
-
2
|
|
1166
|
-
)
|
|
1167
|
-
);
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
// Human-readable output
|
|
1172
|
-
console.log(`\nCluster: ${status.id}`);
|
|
1173
|
-
if (status.isZombie) {
|
|
1174
|
-
console.log(
|
|
1175
|
-
chalk.red(
|
|
1176
|
-
`State: ${status.state} (process ${status.pid} died, cluster has no backing process)`
|
|
1177
|
-
)
|
|
1178
|
-
);
|
|
1179
|
-
console.log(
|
|
1180
|
-
chalk.yellow(
|
|
1181
|
-
` → Run 'zeroshot kill ${id}' to clean up, or 'zeroshot resume ${id}' to restart`
|
|
1182
|
-
)
|
|
1183
|
-
);
|
|
1184
|
-
} else {
|
|
1185
|
-
console.log(`State: ${status.state}`);
|
|
1186
|
-
}
|
|
1187
|
-
if (status.pid) {
|
|
1188
|
-
console.log(`PID: ${status.pid}`);
|
|
1189
|
-
}
|
|
1190
|
-
console.log(`Created: ${new Date(status.createdAt).toLocaleString()}`);
|
|
1191
|
-
console.log(`Messages: ${status.messageCount}`);
|
|
1192
|
-
|
|
1193
|
-
// Show token usage if available
|
|
1194
|
-
if (tokensByRole) {
|
|
1195
|
-
const tokenLines = formatTokenUsage(tokensByRole);
|
|
1196
|
-
if (tokenLines) {
|
|
1197
|
-
console.log('');
|
|
1198
|
-
for (const line of tokenLines) {
|
|
1199
|
-
console.log(line);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
console.log(`\nAgents:`);
|
|
1205
|
-
|
|
1206
|
-
for (const agent of status.agents) {
|
|
1207
|
-
// Check if subcluster
|
|
1208
|
-
if (agent.type === 'subcluster') {
|
|
1209
|
-
console.log(` - ${agent.id} (${agent.role}) [SubCluster]`);
|
|
1210
|
-
console.log(` State: ${agent.state}`);
|
|
1211
|
-
console.log(` Iteration: ${agent.iteration}`);
|
|
1212
|
-
console.log(` Child Cluster: ${agent.childClusterId || 'none'}`);
|
|
1213
|
-
console.log(` Child Running: ${agent.childRunning ? 'Yes' : 'No'}`);
|
|
1214
|
-
} else {
|
|
1215
|
-
const modelLabel = agent.model ? ` [${agent.model}]` : '';
|
|
1216
|
-
console.log(` - ${agent.id} (${agent.role})${modelLabel}`);
|
|
1217
|
-
console.log(` State: ${agent.state}`);
|
|
1218
|
-
console.log(` Iteration: ${agent.iteration}`);
|
|
1219
|
-
console.log(` Running task: ${agent.currentTask ? 'Yes' : 'No'}`);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
console.log('');
|
|
1224
|
-
} else {
|
|
1225
|
-
// Show task status
|
|
1226
|
-
const { showStatus, getStatusData } = await import('../task-lib/commands/status.js');
|
|
1227
|
-
|
|
1228
|
-
if (options.json) {
|
|
1229
|
-
// Try to get JSON data if available
|
|
1230
|
-
let taskData = null;
|
|
1231
|
-
try {
|
|
1232
|
-
if (typeof getStatusData === 'function') {
|
|
1233
|
-
taskData = await getStatusData(id);
|
|
1234
|
-
}
|
|
1235
|
-
} catch {
|
|
1236
|
-
/* Not available */
|
|
1237
|
-
}
|
|
1238
|
-
console.log(JSON.stringify({ type: 'task', id, ...taskData }, null, 2));
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
await showStatus(id);
|
|
1243
|
-
}
|
|
1244
|
-
} catch (error) {
|
|
1245
|
-
if (options.json) {
|
|
1246
|
-
console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
1247
|
-
} else {
|
|
1248
|
-
console.error('Error getting status:', error.message);
|
|
1249
|
-
}
|
|
1250
|
-
process.exit(1);
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
// Logs command - smart (works for both tasks and clusters)
|
|
1255
|
-
program
|
|
1256
|
-
.command('logs [id]')
|
|
1257
|
-
.description('View logs (omit ID for all clusters)')
|
|
1258
|
-
.option('-f, --follow', 'Follow logs in real-time (stream output like tail -f)')
|
|
1259
|
-
.option('-n, --limit <number>', 'Number of recent messages to show (default: 50)', '50')
|
|
1260
|
-
.option('--lines <number>', 'Number of lines to show (task mode)', parseInt)
|
|
1261
|
-
.option('-w, --watch', 'Watch mode: interactive TUI for tasks, high-level events for clusters')
|
|
1262
|
-
.action(async (id, options) => {
|
|
1263
|
-
try {
|
|
1264
|
-
// If ID provided, detect type
|
|
1265
|
-
if (id) {
|
|
1266
|
-
const { detectIdType } = require('../lib/id-detector');
|
|
1267
|
-
const type = detectIdType(id);
|
|
1268
|
-
|
|
1269
|
-
if (!type) {
|
|
1270
|
-
console.error(`ID not found: ${id}`);
|
|
1271
|
-
process.exit(1);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
if (type === 'task') {
|
|
1275
|
-
// Show task logs
|
|
1276
|
-
const { showLogs } = await import('../task-lib/commands/logs.js');
|
|
1277
|
-
await showLogs(id, options);
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
// Fall through to cluster logs below
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
// === CLUSTER LOGS ===
|
|
1284
|
-
const limit = parseInt(options.limit);
|
|
1285
|
-
const quietOrchestrator = await Orchestrator.create({ quiet: true });
|
|
1286
|
-
|
|
1287
|
-
// No ID: show/follow ALL clusters
|
|
1288
|
-
if (!id) {
|
|
1289
|
-
const allClusters = quietOrchestrator.listClusters();
|
|
1290
|
-
const activeClusters = allClusters.filter((c) => c.state === 'running');
|
|
1291
|
-
|
|
1292
|
-
if (allClusters.length === 0) {
|
|
1293
|
-
if (options.follow) {
|
|
1294
|
-
console.log('No clusters found. Waiting for new clusters...\n');
|
|
1295
|
-
console.log(chalk.dim('--- Waiting for clusters (Ctrl+C to stop) ---\n'));
|
|
1296
|
-
} else {
|
|
1297
|
-
console.log('No clusters found');
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// Track if multiple clusters
|
|
1303
|
-
const multiCluster = allClusters.length > 1;
|
|
1304
|
-
|
|
1305
|
-
// Follow mode: show header
|
|
1306
|
-
if (options.follow && allClusters.length > 0) {
|
|
1307
|
-
if (activeClusters.length === 0) {
|
|
1308
|
-
console.log(
|
|
1309
|
-
chalk.dim(
|
|
1310
|
-
`--- Showing history from ${allClusters.length} cluster(s), waiting for new activity (Ctrl+C to stop) ---\n`
|
|
1311
|
-
)
|
|
1312
|
-
);
|
|
1313
|
-
} else if (activeClusters.length === 1) {
|
|
1314
|
-
console.log(chalk.dim(`--- Following ${activeClusters[0].id} (Ctrl+C to stop) ---\n`));
|
|
1315
|
-
} else {
|
|
1316
|
-
console.log(
|
|
1317
|
-
chalk.dim(
|
|
1318
|
-
`--- Following ${activeClusters.length} active clusters (Ctrl+C to stop) ---`
|
|
1319
|
-
)
|
|
1320
|
-
);
|
|
1321
|
-
for (const c of activeClusters) {
|
|
1322
|
-
console.log(chalk.dim(` • ${c.id} [${c.state}]`));
|
|
1323
|
-
}
|
|
1324
|
-
console.log('');
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// Show recent messages from ALL clusters (history)
|
|
1329
|
-
// In follow mode, poll will handle new messages - this shows initial history
|
|
1330
|
-
for (const clusterInfo of allClusters) {
|
|
1331
|
-
const cluster = quietOrchestrator.getCluster(clusterInfo.id);
|
|
1332
|
-
if (cluster) {
|
|
1333
|
-
const messages = cluster.messageBus.getAll(clusterInfo.id);
|
|
1334
|
-
const recentMessages = messages.slice(-limit);
|
|
1335
|
-
const isActive = clusterInfo.state === 'running';
|
|
1336
|
-
for (const msg of recentMessages) {
|
|
1337
|
-
printMessage(msg, clusterInfo.id, options.watch, isActive);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
// Follow mode: poll SQLite for new messages (cross-process support)
|
|
1343
|
-
if (options.follow) {
|
|
1344
|
-
// Set terminal title based on task(s)
|
|
1345
|
-
const taskTitles = [];
|
|
1346
|
-
for (const clusterInfo of allClusters) {
|
|
1347
|
-
const cluster = quietOrchestrator.getCluster(clusterInfo.id);
|
|
1348
|
-
if (cluster) {
|
|
1349
|
-
const messages = cluster.messageBus.getAll(clusterInfo.id);
|
|
1350
|
-
const issueOpened = messages.find((m) => m.topic === 'ISSUE_OPENED');
|
|
1351
|
-
if (issueOpened) {
|
|
1352
|
-
taskTitles.push({
|
|
1353
|
-
id: clusterInfo.id,
|
|
1354
|
-
summary: formatTaskSummary(issueOpened, 30),
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
if (taskTitles.length === 1) {
|
|
1360
|
-
setTerminalTitle(`zeroshot [${taskTitles[0].id}]: ${taskTitles[0].summary}`);
|
|
1361
|
-
} else if (taskTitles.length > 1) {
|
|
1362
|
-
setTerminalTitle(`zeroshot: ${taskTitles.length} clusters`);
|
|
1363
|
-
} else {
|
|
1364
|
-
setTerminalTitle('zeroshot: waiting...');
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
// In watch mode, show the initial task for each cluster (after history)
|
|
1368
|
-
if (options.watch) {
|
|
1369
|
-
for (const clusterInfo of allClusters) {
|
|
1370
|
-
const cluster = quietOrchestrator.getCluster(clusterInfo.id);
|
|
1371
|
-
if (cluster) {
|
|
1372
|
-
const messages = cluster.messageBus.getAll(clusterInfo.id);
|
|
1373
|
-
const issueOpened = messages.find((m) => m.topic === 'ISSUE_OPENED');
|
|
1374
|
-
if (issueOpened) {
|
|
1375
|
-
const clusterLabel = multiCluster ? `[${clusterInfo.id}] ` : '';
|
|
1376
|
-
const taskSummary = formatTaskSummary(issueOpened);
|
|
1377
|
-
console.log(chalk.cyan(`${clusterLabel}Task: ${chalk.bold(taskSummary)}\n`));
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
const stopPollers = [];
|
|
1384
|
-
const messageBuffer = [];
|
|
1385
|
-
|
|
1386
|
-
// Track cluster states (for dim coloring of inactive clusters)
|
|
1387
|
-
const clusterStates = new Map(); // cluster_id -> state
|
|
1388
|
-
for (const c of allClusters) {
|
|
1389
|
-
clusterStates.set(c.id, c.state);
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// === STATUS FOOTER: Live agent monitoring (same as foreground mode) ===
|
|
1393
|
-
// Shows CPU, memory, network metrics for all agents at bottom of terminal
|
|
1394
|
-
let statusFooter = null;
|
|
1395
|
-
if ((options.follow || options.watch) && process.stdout.isTTY) {
|
|
1396
|
-
statusFooter = new StatusFooter({
|
|
1397
|
-
refreshInterval: 1000,
|
|
1398
|
-
enabled: true,
|
|
1399
|
-
});
|
|
1400
|
-
// Set first cluster as the active one (for display purposes)
|
|
1401
|
-
if (allClusters.length > 0) {
|
|
1402
|
-
statusFooter.setCluster(allClusters[0].id);
|
|
1403
|
-
statusFooter.setClusterState(clusterStates.get(allClusters[0].id) || 'running');
|
|
1404
|
-
}
|
|
1405
|
-
// Set module-level reference so safePrint/safeWrite route through footer
|
|
1406
|
-
activeStatusFooter = statusFooter;
|
|
1407
|
-
statusFooter.start();
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
// Buffered message handler - collects messages and sorts by timestamp
|
|
1411
|
-
const flushMessages = () => {
|
|
1412
|
-
if (messageBuffer.length === 0) return;
|
|
1413
|
-
// Sort by timestamp
|
|
1414
|
-
messageBuffer.sort((a, b) => a.timestamp - b.timestamp);
|
|
1415
|
-
|
|
1416
|
-
// Track senders with pending output
|
|
1417
|
-
const sendersWithOutput = new Set();
|
|
1418
|
-
for (const msg of messageBuffer) {
|
|
1419
|
-
if (msg.topic === 'AGENT_OUTPUT' && msg.sender) {
|
|
1420
|
-
sendersWithOutput.add(msg.sender);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// Update StatusFooter from polled AGENT_LIFECYCLE messages (cross-process)
|
|
1424
|
-
if (msg.topic === 'AGENT_LIFECYCLE' && statusFooter) {
|
|
1425
|
-
const data = msg.content?.data || {};
|
|
1426
|
-
const event = data.event;
|
|
1427
|
-
const agentId = data.agent || msg.sender;
|
|
1428
|
-
|
|
1429
|
-
if (event === 'STARTED') {
|
|
1430
|
-
statusFooter.updateAgent({
|
|
1431
|
-
id: agentId,
|
|
1432
|
-
state: AGENT_STATE.IDLE,
|
|
1433
|
-
pid: null,
|
|
1434
|
-
iteration: data.iteration || 0,
|
|
1435
|
-
});
|
|
1436
|
-
} else if (event === 'TASK_STARTED') {
|
|
1437
|
-
statusFooter.updateAgent({
|
|
1438
|
-
id: agentId,
|
|
1439
|
-
state: AGENT_STATE.EXECUTING_TASK,
|
|
1440
|
-
pid: statusFooter.agents.get(agentId)?.pid || null,
|
|
1441
|
-
iteration: data.iteration || 0,
|
|
1442
|
-
});
|
|
1443
|
-
} else if (event === 'PROCESS_SPAWNED') {
|
|
1444
|
-
// PROCESS_SPAWNED = proof of execution. If a process spawned, agent is executing.
|
|
1445
|
-
const current = statusFooter.agents.get(agentId) || { iteration: 0 };
|
|
1446
|
-
statusFooter.updateAgent({
|
|
1447
|
-
id: agentId,
|
|
1448
|
-
state: AGENT_STATE.EXECUTING_TASK,
|
|
1449
|
-
pid: data.pid,
|
|
1450
|
-
iteration: current.iteration,
|
|
1451
|
-
});
|
|
1452
|
-
} else if (event === 'TASK_COMPLETED' || event === 'TASK_FAILED') {
|
|
1453
|
-
statusFooter.updateAgent({
|
|
1454
|
-
id: agentId,
|
|
1455
|
-
state: AGENT_STATE.IDLE,
|
|
1456
|
-
pid: null,
|
|
1457
|
-
iteration: data.iteration || 0,
|
|
1458
|
-
});
|
|
1459
|
-
} else if (event === 'STOPPED') {
|
|
1460
|
-
statusFooter.removeAgent(agentId);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
const isActive = clusterStates.get(msg.cluster_id) === 'running';
|
|
1465
|
-
printMessage(msg, true, options.watch, isActive);
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// Save cluster ID before clearing buffer
|
|
1469
|
-
const firstClusterId = messageBuffer[0]?.cluster_id;
|
|
1470
|
-
messageBuffer.length = 0;
|
|
1471
|
-
|
|
1472
|
-
// Flush pending line buffers for all senders that had output
|
|
1473
|
-
// This ensures streaming text without newlines gets displayed
|
|
1474
|
-
for (const sender of sendersWithOutput) {
|
|
1475
|
-
const senderLabel = `${firstClusterId || ''}/${sender}`;
|
|
1476
|
-
const prefix = getColorForSender(sender)(`${senderLabel.padEnd(25)} |`);
|
|
1477
|
-
flushLineBuffer(prefix, sender);
|
|
1478
|
-
}
|
|
1479
|
-
};
|
|
1480
|
-
|
|
1481
|
-
// Flush buffer every 250ms
|
|
1482
|
-
const flushInterval = setInterval(flushMessages, 250);
|
|
1483
|
-
|
|
1484
|
-
for (const clusterInfo of allClusters) {
|
|
1485
|
-
const cluster = quietOrchestrator.getCluster(clusterInfo.id);
|
|
1486
|
-
if (cluster) {
|
|
1487
|
-
// Use polling for cross-process message detection
|
|
1488
|
-
const stopPoll = cluster.ledger.pollForMessages(
|
|
1489
|
-
clusterInfo.id,
|
|
1490
|
-
(msg) => {
|
|
1491
|
-
messageBuffer.push(msg);
|
|
1492
|
-
},
|
|
1493
|
-
300
|
|
1494
|
-
);
|
|
1495
|
-
stopPollers.push(stopPoll);
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
const stopWatching = quietOrchestrator.watchForNewClusters((newCluster) => {
|
|
1500
|
-
console.log(chalk.green(`\n✓ New cluster detected: ${newCluster.id}\n`));
|
|
1501
|
-
// Track new cluster as active
|
|
1502
|
-
clusterStates.set(newCluster.id, 'running');
|
|
1503
|
-
// Poll new cluster's ledger
|
|
1504
|
-
const stopPoll = newCluster.ledger.pollForMessages(
|
|
1505
|
-
newCluster.id,
|
|
1506
|
-
(msg) => {
|
|
1507
|
-
messageBuffer.push(msg);
|
|
1508
|
-
},
|
|
1509
|
-
300
|
|
1510
|
-
);
|
|
1511
|
-
stopPollers.push(stopPoll);
|
|
1512
|
-
});
|
|
1513
|
-
|
|
1514
|
-
keepProcessAlive(() => {
|
|
1515
|
-
clearInterval(flushInterval);
|
|
1516
|
-
flushMessages();
|
|
1517
|
-
stopPollers.forEach((stop) => stop());
|
|
1518
|
-
stopWatching();
|
|
1519
|
-
// Stop status footer and restore terminal
|
|
1520
|
-
if (statusFooter) {
|
|
1521
|
-
statusFooter.stop();
|
|
1522
|
-
activeStatusFooter = null;
|
|
1523
|
-
}
|
|
1524
|
-
// Restore terminal title
|
|
1525
|
-
restoreTerminalTitle();
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
// Specific cluster ID provided
|
|
1532
|
-
const cluster = quietOrchestrator.getCluster(id);
|
|
1533
|
-
if (!cluster) {
|
|
1534
|
-
console.error(`Cluster ${id} not found`);
|
|
1535
|
-
process.exit(1);
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
// Check if cluster is active
|
|
1539
|
-
const allClustersList = quietOrchestrator.listClusters();
|
|
1540
|
-
const clusterInfo = allClustersList.find((c) => c.id === id);
|
|
1541
|
-
const isActive = clusterInfo?.state === 'running';
|
|
1542
|
-
|
|
1543
|
-
// Get messages from cluster database
|
|
1544
|
-
const dbMessages = cluster.messageBus.getAll(id);
|
|
1545
|
-
|
|
1546
|
-
// Get messages from agent task logs
|
|
1547
|
-
const taskLogMessages = readAgentTaskLogs(cluster);
|
|
1548
|
-
|
|
1549
|
-
// Merge and sort by timestamp
|
|
1550
|
-
const allMessages = [...dbMessages, ...taskLogMessages].sort(
|
|
1551
|
-
(a, b) => a.timestamp - b.timestamp
|
|
1552
|
-
);
|
|
1553
|
-
const recentMessages = allMessages.slice(-limit);
|
|
1554
|
-
|
|
1555
|
-
// Print messages
|
|
1556
|
-
for (const msg of recentMessages) {
|
|
1557
|
-
printMessage(msg, true, options.watch, isActive);
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
// Follow mode for specific cluster (poll SQLite AND task logs)
|
|
1561
|
-
if (options.follow) {
|
|
1562
|
-
// Set terminal title based on task
|
|
1563
|
-
const issueOpened = dbMessages.find((m) => m.topic === 'ISSUE_OPENED');
|
|
1564
|
-
if (issueOpened) {
|
|
1565
|
-
setTerminalTitle(`zeroshot [${id}]: ${formatTaskSummary(issueOpened, 30)}`);
|
|
1566
|
-
} else {
|
|
1567
|
-
setTerminalTitle(`zeroshot [${id}]`);
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
console.log('\n--- Following logs (Ctrl+C to stop) ---\n');
|
|
1571
|
-
|
|
1572
|
-
// Poll cluster database for new messages
|
|
1573
|
-
const stopDbPoll = cluster.ledger.pollForMessages(
|
|
1574
|
-
id,
|
|
1575
|
-
(msg) => {
|
|
1576
|
-
printMessage(msg, true, options.watch, isActive);
|
|
1577
|
-
|
|
1578
|
-
// Flush pending line buffer for streaming text without newlines
|
|
1579
|
-
if (msg.topic === 'AGENT_OUTPUT' && msg.sender) {
|
|
1580
|
-
const senderLabel = `${msg.cluster_id || ''}/${msg.sender}`;
|
|
1581
|
-
const prefix = getColorForSender(msg.sender)(`${senderLabel.padEnd(25)} |`);
|
|
1582
|
-
flushLineBuffer(prefix, msg.sender);
|
|
1583
|
-
}
|
|
1584
|
-
},
|
|
1585
|
-
500
|
|
1586
|
-
);
|
|
1587
|
-
|
|
1588
|
-
// Poll agent task logs for new output
|
|
1589
|
-
const taskLogSizes = new Map(); // taskId -> last size
|
|
1590
|
-
const pollTaskLogs = () => {
|
|
1591
|
-
for (const agent of cluster.agents) {
|
|
1592
|
-
const state = agent.getState();
|
|
1593
|
-
const taskId = state.currentTaskId;
|
|
1594
|
-
if (!taskId) continue;
|
|
1595
|
-
|
|
1596
|
-
const logPath = path.join(os.homedir(), '.claude-zeroshot', 'logs', `${taskId}.log`);
|
|
1597
|
-
if (!fs.existsSync(logPath)) continue;
|
|
1598
|
-
|
|
1599
|
-
try {
|
|
1600
|
-
const stats = fs.statSync(logPath);
|
|
1601
|
-
const currentSize = stats.size;
|
|
1602
|
-
const lastSize = taskLogSizes.get(taskId) || 0;
|
|
1603
|
-
|
|
1604
|
-
if (currentSize > lastSize) {
|
|
1605
|
-
// Read new content
|
|
1606
|
-
const fd = fs.openSync(logPath, 'r');
|
|
1607
|
-
const buffer = Buffer.alloc(currentSize - lastSize);
|
|
1608
|
-
fs.readSync(fd, buffer, 0, buffer.length, lastSize);
|
|
1609
|
-
fs.closeSync(fd);
|
|
1610
|
-
|
|
1611
|
-
const newContent = buffer.toString('utf-8');
|
|
1612
|
-
const lines = newContent.split('\n').filter((line) => line.trim());
|
|
1613
|
-
|
|
1614
|
-
for (const line of lines) {
|
|
1615
|
-
if (!line.trim().startsWith('{')) continue;
|
|
1616
|
-
|
|
1617
|
-
try {
|
|
1618
|
-
// Parse timestamp-prefixed line
|
|
1619
|
-
let timestamp = Date.now();
|
|
1620
|
-
let jsonContent = line.trim();
|
|
1621
|
-
|
|
1622
|
-
const timestampMatch = jsonContent.match(/^\[(\d{13})\](.*)$/);
|
|
1623
|
-
if (timestampMatch) {
|
|
1624
|
-
timestamp = parseInt(timestampMatch[1], 10);
|
|
1625
|
-
jsonContent = timestampMatch[2];
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
if (!jsonContent.startsWith('{')) continue;
|
|
1629
|
-
|
|
1630
|
-
// Parse and validate JSON
|
|
1631
|
-
const parsed = JSON.parse(jsonContent);
|
|
1632
|
-
if (parsed.type === 'system' && parsed.subtype === 'init') continue;
|
|
1633
|
-
|
|
1634
|
-
// Create message and print immediately
|
|
1635
|
-
const msg = {
|
|
1636
|
-
id: `task-${taskId}-${timestamp}`,
|
|
1637
|
-
timestamp,
|
|
1638
|
-
topic: 'AGENT_OUTPUT',
|
|
1639
|
-
sender: agent.id,
|
|
1640
|
-
receiver: 'broadcast',
|
|
1641
|
-
cluster_id: cluster.id,
|
|
1642
|
-
content: {
|
|
1643
|
-
text: jsonContent,
|
|
1644
|
-
data: {
|
|
1645
|
-
type: 'stdout',
|
|
1646
|
-
line: jsonContent,
|
|
1647
|
-
agent: agent.id,
|
|
1648
|
-
role: agent.role,
|
|
1649
|
-
iteration: state.iteration,
|
|
1650
|
-
fromTaskLog: true,
|
|
1651
|
-
},
|
|
1652
|
-
},
|
|
1653
|
-
};
|
|
1654
|
-
|
|
1655
|
-
printMessage(msg, true, options.watch, isActive);
|
|
1656
|
-
|
|
1657
|
-
// Flush line buffer
|
|
1658
|
-
const senderLabel = `${cluster.id}/${agent.id}`;
|
|
1659
|
-
const prefix = getColorForSender(agent.id)(`${senderLabel.padEnd(25)} |`);
|
|
1660
|
-
flushLineBuffer(prefix, agent.id);
|
|
1661
|
-
} catch {
|
|
1662
|
-
// Skip invalid JSON
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
taskLogSizes.set(taskId, currentSize);
|
|
1667
|
-
}
|
|
1668
|
-
} catch {
|
|
1669
|
-
// File read error - skip
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
};
|
|
1673
|
-
|
|
1674
|
-
// Poll task logs every 300ms (same as agent-wrapper)
|
|
1675
|
-
const taskLogInterval = setInterval(pollTaskLogs, 300);
|
|
1676
|
-
|
|
1677
|
-
keepProcessAlive(() => {
|
|
1678
|
-
stopDbPoll();
|
|
1679
|
-
clearInterval(taskLogInterval);
|
|
1680
|
-
restoreTerminalTitle();
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
} catch (error) {
|
|
1684
|
-
console.error('Error viewing logs:', error.message);
|
|
1685
|
-
process.exit(1);
|
|
1686
|
-
}
|
|
1687
|
-
});
|
|
1688
|
-
|
|
1689
|
-
// Stop command (cluster-only)
|
|
1690
|
-
program
|
|
1691
|
-
.command('stop <cluster-id>')
|
|
1692
|
-
.description('Stop a cluster gracefully')
|
|
1693
|
-
.action(async (clusterId) => {
|
|
1694
|
-
try {
|
|
1695
|
-
console.log(`Stopping cluster ${clusterId}...`);
|
|
1696
|
-
await (await getOrchestrator()).stop(clusterId);
|
|
1697
|
-
console.log('Cluster stopped successfully');
|
|
1698
|
-
} catch (error) {
|
|
1699
|
-
console.error('Error stopping cluster:', error.message);
|
|
1700
|
-
process.exit(1);
|
|
1701
|
-
}
|
|
1702
|
-
});
|
|
1703
|
-
|
|
1704
|
-
// Kill command - smart (works for both tasks and clusters)
|
|
1705
|
-
program
|
|
1706
|
-
.command('kill <id>')
|
|
1707
|
-
.description('Kill a task or cluster')
|
|
1708
|
-
.action(async (id) => {
|
|
1709
|
-
try {
|
|
1710
|
-
const { detectIdType } = require('../lib/id-detector');
|
|
1711
|
-
const type = detectIdType(id);
|
|
1712
|
-
|
|
1713
|
-
if (!type) {
|
|
1714
|
-
console.error(`ID not found: ${id}`);
|
|
1715
|
-
process.exit(1);
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
if (type === 'cluster') {
|
|
1719
|
-
console.log(`Killing cluster ${id}...`);
|
|
1720
|
-
await (await getOrchestrator()).kill(id);
|
|
1721
|
-
console.log('Cluster killed successfully');
|
|
1722
|
-
} else {
|
|
1723
|
-
// Kill task
|
|
1724
|
-
const { killTaskCommand } = await import('../task-lib/commands/kill.js');
|
|
1725
|
-
await killTaskCommand(id);
|
|
1726
|
-
}
|
|
1727
|
-
} catch (error) {
|
|
1728
|
-
console.error('Error killing:', error.message);
|
|
1729
|
-
process.exit(1);
|
|
1730
|
-
}
|
|
1731
|
-
});
|
|
1732
|
-
|
|
1733
|
-
// Attach command - tmux-style attach to running task or cluster agent
|
|
1734
|
-
program
|
|
1735
|
-
.command('attach [id]')
|
|
1736
|
-
.description('Attach to a running task or cluster agent (Ctrl+C to detach, task keeps running)')
|
|
1737
|
-
.option('-a, --agent <name>', 'Attach to specific agent in cluster (required for clusters)')
|
|
1738
|
-
.addHelpText(
|
|
1739
|
-
'after',
|
|
1740
|
-
`
|
|
1741
|
-
Examples:
|
|
1742
|
-
${chalk.cyan('zeroshot attach')} List attachable tasks/clusters
|
|
1743
|
-
${chalk.cyan('zeroshot attach task-xxx')} Attach to task
|
|
1744
|
-
${chalk.cyan('zeroshot attach cluster-xxx --agent worker')} Attach to specific agent in cluster
|
|
1745
|
-
|
|
1746
|
-
Key bindings:
|
|
1747
|
-
${chalk.yellow('Ctrl+C')} Detach (task continues running)
|
|
1748
|
-
${chalk.yellow('Ctrl+B d')} Also detach (for tmux muscle memory)
|
|
1749
|
-
${chalk.yellow('Ctrl+B ?')} Show help
|
|
1750
|
-
${chalk.yellow('Ctrl+B c')} Interrupt agent (sends SIGINT) - USE WITH CAUTION
|
|
1751
|
-
`
|
|
1752
|
-
)
|
|
1753
|
-
.action(async (id, options) => {
|
|
1754
|
-
try {
|
|
1755
|
-
const { AttachClient, socketDiscovery } = require('../src/attach');
|
|
1756
|
-
|
|
1757
|
-
// If no ID provided, list attachable processes
|
|
1758
|
-
if (!id) {
|
|
1759
|
-
const tasks = await socketDiscovery.listAttachableTasks();
|
|
1760
|
-
const clusters = await socketDiscovery.listAttachableClusters();
|
|
1761
|
-
|
|
1762
|
-
if (tasks.length === 0 && clusters.length === 0) {
|
|
1763
|
-
console.log(chalk.dim('No attachable tasks or clusters found.'));
|
|
1764
|
-
console.log(chalk.dim('Start a task with: zeroshot task run "prompt"'));
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
console.log(chalk.bold('\nAttachable processes:\n'));
|
|
1769
|
-
|
|
1770
|
-
if (tasks.length > 0) {
|
|
1771
|
-
console.log(chalk.cyan('Tasks:'));
|
|
1772
|
-
for (const taskId of tasks) {
|
|
1773
|
-
console.log(` ${taskId}`);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
if (clusters.length > 0) {
|
|
1778
|
-
console.log(chalk.yellow('\nClusters:'));
|
|
1779
|
-
const OrchestratorModule = require('../src/orchestrator');
|
|
1780
|
-
for (const clusterId of clusters) {
|
|
1781
|
-
const agents = await socketDiscovery.listAttachableAgents(clusterId);
|
|
1782
|
-
console.log(` ${clusterId}`);
|
|
1783
|
-
// Get agent models and token usage from orchestrator (if available)
|
|
1784
|
-
let agentModels = {};
|
|
1785
|
-
let tokenUsageLines = null;
|
|
1786
|
-
try {
|
|
1787
|
-
const orchestrator = OrchestratorModule.getInstance();
|
|
1788
|
-
const status = orchestrator.getStatus(clusterId);
|
|
1789
|
-
for (const a of status.agents) {
|
|
1790
|
-
agentModels[a.id] = a.model;
|
|
1791
|
-
}
|
|
1792
|
-
// Get token usage from message bus
|
|
1793
|
-
const cluster = orchestrator.getCluster(clusterId);
|
|
1794
|
-
if (cluster?.messageBus) {
|
|
1795
|
-
const tokensByRole = cluster.messageBus.getTokensByRole(clusterId);
|
|
1796
|
-
tokenUsageLines = formatTokenUsage(tokensByRole);
|
|
1797
|
-
}
|
|
1798
|
-
} catch {
|
|
1799
|
-
/* orchestrator not running - models/tokens unavailable */
|
|
1800
|
-
}
|
|
1801
|
-
// Display token usage if available
|
|
1802
|
-
if (tokenUsageLines) {
|
|
1803
|
-
for (const line of tokenUsageLines) {
|
|
1804
|
-
console.log(` ${line}`);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
for (const agent of agents) {
|
|
1808
|
-
const modelLabel = agentModels[agent] ? chalk.dim(` [${agentModels[agent]}]`) : '';
|
|
1809
|
-
console.log(chalk.dim(` --agent ${agent}`) + modelLabel);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
console.log(chalk.dim('\nUsage: zeroshot attach <id> [--agent <name>]'));
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
// Determine socket path
|
|
1819
|
-
let socketPath;
|
|
1820
|
-
|
|
1821
|
-
if (id.startsWith('task-')) {
|
|
1822
|
-
socketPath = socketDiscovery.getTaskSocketPath(id);
|
|
1823
|
-
} else if (id.startsWith('cluster-') || socketDiscovery.isKnownCluster(id)) {
|
|
1824
|
-
// Clusters use the task system - each agent spawns a task with its own socket
|
|
1825
|
-
// Get cluster status to find which task each agent is running
|
|
1826
|
-
const clustersFile = path.join(os.homedir(), '.zeroshot', 'clusters.json');
|
|
1827
|
-
let cluster;
|
|
1828
|
-
try {
|
|
1829
|
-
const clusters = JSON.parse(fs.readFileSync(clustersFile, 'utf8'));
|
|
1830
|
-
cluster = clusters[id];
|
|
1831
|
-
} catch {
|
|
1832
|
-
cluster = null;
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
if (!cluster) {
|
|
1836
|
-
console.error(chalk.red(`Cluster ${id} not found`));
|
|
1837
|
-
process.exit(1);
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
if (cluster.state !== 'running') {
|
|
1841
|
-
console.error(chalk.red(`Cluster ${id} is not running (state: ${cluster.state})`));
|
|
1842
|
-
console.error(chalk.dim('Only running clusters have attachable agents.'));
|
|
1843
|
-
process.exit(1);
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
// Create orchestrator instance to query agent states
|
|
1847
|
-
// This loads the cluster from disk including its ledger and agents
|
|
1848
|
-
const orchestrator = await Orchestrator.create({ quiet: true });
|
|
1849
|
-
|
|
1850
|
-
try {
|
|
1851
|
-
const status = orchestrator.getStatus(id);
|
|
1852
|
-
// Agent is "active" if in any working state
|
|
1853
|
-
// Note: currentTaskId may be null briefly between TASK_STARTED and TASK_ID_ASSIGNED
|
|
1854
|
-
const activeAgents = status.agents.filter((a) => ACTIVE_STATES.has(a.state));
|
|
1855
|
-
|
|
1856
|
-
if (activeAgents.length === 0) {
|
|
1857
|
-
console.error(chalk.yellow(`No agents currently executing tasks in cluster ${id}`));
|
|
1858
|
-
console.log(chalk.dim('\nAgent states:'));
|
|
1859
|
-
for (const agent of status.agents) {
|
|
1860
|
-
const modelLabel = agent.model ? chalk.dim(` [${agent.model}]`) : '';
|
|
1861
|
-
console.log(
|
|
1862
|
-
chalk.dim(
|
|
1863
|
-
` ${agent.id}${modelLabel}: ${agent.state}${agent.currentTaskId ? ` (last task: ${agent.currentTaskId})` : ''}`
|
|
1864
|
-
)
|
|
1865
|
-
);
|
|
1866
|
-
}
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
if (!options.agent) {
|
|
1871
|
-
// Show list of agents and their task IDs
|
|
1872
|
-
console.log(chalk.yellow(`\nCluster ${id} - attachable agents:\n`));
|
|
1873
|
-
for (const agent of activeAgents) {
|
|
1874
|
-
const modelLabel = agent.model ? chalk.dim(` [${agent.model}]`) : '';
|
|
1875
|
-
if (agent.currentTaskId) {
|
|
1876
|
-
console.log(
|
|
1877
|
-
` ${chalk.cyan(agent.id)}${modelLabel} → task ${chalk.green(agent.currentTaskId)}`
|
|
1878
|
-
);
|
|
1879
|
-
console.log(chalk.dim(` zeroshot attach ${agent.currentTaskId}`));
|
|
1880
|
-
} else {
|
|
1881
|
-
// Task ID not yet assigned (Claude CLI still starting)
|
|
1882
|
-
console.log(
|
|
1883
|
-
` ${chalk.cyan(agent.id)}${modelLabel} → ${chalk.yellow('starting...')}`
|
|
1884
|
-
);
|
|
1885
|
-
console.log(chalk.dim(` (task ID not yet assigned, try again in a moment)`));
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
console.log(chalk.dim('\nAttach to an agent by running: zeroshot attach <taskId>'));
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
// Find the specified agent
|
|
1893
|
-
const agent = status.agents.find((a) => a.id === options.agent);
|
|
1894
|
-
if (!agent) {
|
|
1895
|
-
console.error(chalk.red(`Agent '${options.agent}' not found in cluster ${id}`));
|
|
1896
|
-
console.log(
|
|
1897
|
-
chalk.dim('Available agents: ' + status.agents.map((a) => a.id).join(', '))
|
|
1898
|
-
);
|
|
1899
|
-
process.exit(1);
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
if (!agent.currentTaskId) {
|
|
1903
|
-
if (ACTIVE_STATES.has(agent.state)) {
|
|
1904
|
-
// Agent is working but task ID not yet assigned
|
|
1905
|
-
console.error(
|
|
1906
|
-
chalk.yellow(
|
|
1907
|
-
`Agent '${options.agent}' is working (state: ${agent.state}, task ID not yet assigned)`
|
|
1908
|
-
)
|
|
1909
|
-
);
|
|
1910
|
-
console.log(chalk.dim('Try again in a moment...'));
|
|
1911
|
-
} else {
|
|
1912
|
-
console.error(
|
|
1913
|
-
chalk.yellow(`Agent '${options.agent}' is not currently running a task`)
|
|
1914
|
-
);
|
|
1915
|
-
console.log(chalk.dim(`State: ${agent.state}`));
|
|
1916
|
-
}
|
|
1917
|
-
return;
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
// Use the agent's task socket
|
|
1921
|
-
socketPath = socketDiscovery.getTaskSocketPath(agent.currentTaskId);
|
|
1922
|
-
console.log(
|
|
1923
|
-
chalk.dim(`Attaching to agent ${options.agent} via task ${agent.currentTaskId}...`)
|
|
1924
|
-
);
|
|
1925
|
-
} catch (err) {
|
|
1926
|
-
// Orchestrator not running or cluster not loaded - fall back to socket discovery
|
|
1927
|
-
console.error(chalk.yellow(`Could not get cluster status: ${err.message}`));
|
|
1928
|
-
console.log(
|
|
1929
|
-
chalk.dim('Try attaching directly to a task ID instead: zeroshot attach <taskId>')
|
|
1930
|
-
);
|
|
1931
|
-
|
|
1932
|
-
// Try to find any task sockets that might belong to this cluster
|
|
1933
|
-
const tasks = await socketDiscovery.listAttachableTasks();
|
|
1934
|
-
if (tasks.length > 0) {
|
|
1935
|
-
console.log(chalk.dim('\nAttachable tasks:'));
|
|
1936
|
-
for (const taskId of tasks) {
|
|
1937
|
-
console.log(chalk.dim(` zeroshot attach ${taskId}`));
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
return;
|
|
1941
|
-
}
|
|
1942
|
-
} else {
|
|
1943
|
-
// Try to auto-detect
|
|
1944
|
-
socketPath = socketDiscovery.getSocketPath(id, options.agent);
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
// Check if socket exists
|
|
1948
|
-
const socketAlive = await socketDiscovery.isSocketAlive(socketPath);
|
|
1949
|
-
if (!socketAlive) {
|
|
1950
|
-
console.error(chalk.red(`Cannot attach to ${id}`));
|
|
1951
|
-
|
|
1952
|
-
// Check if it's an old task without attach support
|
|
1953
|
-
const { detectIdType } = require('../lib/id-detector');
|
|
1954
|
-
const type = detectIdType(id);
|
|
1955
|
-
|
|
1956
|
-
if (type === 'task') {
|
|
1957
|
-
console.error(chalk.dim('Task may have been spawned before attach support was added.'));
|
|
1958
|
-
console.error(chalk.dim(`Try: zeroshot logs ${id} -f`));
|
|
1959
|
-
} else if (type === 'cluster') {
|
|
1960
|
-
console.error(chalk.dim('Cluster may not be running or agent may not exist.'));
|
|
1961
|
-
console.error(chalk.dim(`Check status: zeroshot status ${id}`));
|
|
1962
|
-
} else {
|
|
1963
|
-
console.error(chalk.dim('Process not found or not attachable.'));
|
|
1964
|
-
}
|
|
1965
|
-
process.exit(1);
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
// Connect
|
|
1969
|
-
console.log(
|
|
1970
|
-
chalk.dim(`Attaching to ${id}${options.agent ? ` (agent: ${options.agent})` : ''}...`)
|
|
1971
|
-
);
|
|
1972
|
-
console.log(chalk.dim('Press Ctrl+B ? for help, Ctrl+B d to detach\n'));
|
|
1973
|
-
|
|
1974
|
-
const client = new AttachClient({ socketPath });
|
|
1975
|
-
|
|
1976
|
-
client.on('state', (_state) => {
|
|
1977
|
-
// Could show status bar here in future
|
|
1978
|
-
});
|
|
1979
|
-
|
|
1980
|
-
client.on('exit', ({ code, signal }) => {
|
|
1981
|
-
console.log(chalk.dim(`\n\nProcess exited (code: ${code}, signal: ${signal})`));
|
|
1982
|
-
process.exit(code || 0);
|
|
1983
|
-
});
|
|
1984
|
-
|
|
1985
|
-
client.on('error', (err) => {
|
|
1986
|
-
console.error(chalk.red(`\nConnection error: ${err.message}`));
|
|
1987
|
-
process.exit(1);
|
|
1988
|
-
});
|
|
1989
|
-
|
|
1990
|
-
client.on('detach', () => {
|
|
1991
|
-
console.log(chalk.dim('\n\nDetached. Task continues running.'));
|
|
1992
|
-
console.log(
|
|
1993
|
-
chalk.dim(
|
|
1994
|
-
`Re-attach: zeroshot attach ${id}${options.agent ? ` --agent ${options.agent}` : ''}`
|
|
1995
|
-
)
|
|
1996
|
-
);
|
|
1997
|
-
process.exit(0);
|
|
1998
|
-
});
|
|
1999
|
-
|
|
2000
|
-
client.on('close', () => {
|
|
2001
|
-
console.log(chalk.dim('\n\nConnection closed.'));
|
|
2002
|
-
process.exit(0);
|
|
2003
|
-
});
|
|
2004
|
-
|
|
2005
|
-
await client.connect();
|
|
2006
|
-
} catch (error) {
|
|
2007
|
-
console.error(chalk.red(`Error attaching: ${error.message}`));
|
|
2008
|
-
process.exit(1);
|
|
2009
|
-
}
|
|
2010
|
-
});
|
|
2011
|
-
|
|
2012
|
-
// Kill-all command - kills all running tasks and clusters
|
|
2013
|
-
program
|
|
2014
|
-
.command('kill-all')
|
|
2015
|
-
.description('Kill all running tasks and clusters')
|
|
2016
|
-
.option('-y, --yes', 'Skip confirmation')
|
|
2017
|
-
.action(async (options) => {
|
|
2018
|
-
try {
|
|
2019
|
-
// Get counts first
|
|
2020
|
-
const orchestrator = await getOrchestrator();
|
|
2021
|
-
const clusters = orchestrator.listClusters();
|
|
2022
|
-
const runningClusters = clusters.filter(
|
|
2023
|
-
(c) => c.state === 'running' || c.state === 'initializing'
|
|
2024
|
-
);
|
|
2025
|
-
|
|
2026
|
-
const { loadTasks } = await import('../task-lib/store.js');
|
|
2027
|
-
const { isProcessRunning } = await import('../task-lib/runner.js');
|
|
2028
|
-
const tasks = loadTasks();
|
|
2029
|
-
const runningTasks = Object.values(tasks).filter(
|
|
2030
|
-
(t) => t.status === 'running' && isProcessRunning(t.pid)
|
|
2031
|
-
);
|
|
2032
|
-
|
|
2033
|
-
const totalCount = runningClusters.length + runningTasks.length;
|
|
2034
|
-
|
|
2035
|
-
if (totalCount === 0) {
|
|
2036
|
-
console.log(chalk.dim('No running tasks or clusters to kill.'));
|
|
2037
|
-
return;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
// Show what will be killed
|
|
2041
|
-
console.log(chalk.bold(`\nWill kill:`));
|
|
2042
|
-
if (runningClusters.length > 0) {
|
|
2043
|
-
console.log(chalk.cyan(` ${runningClusters.length} cluster(s)`));
|
|
2044
|
-
for (const c of runningClusters) {
|
|
2045
|
-
console.log(chalk.dim(` - ${c.id}`));
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
if (runningTasks.length > 0) {
|
|
2049
|
-
console.log(chalk.yellow(` ${runningTasks.length} task(s)`));
|
|
2050
|
-
for (const t of runningTasks) {
|
|
2051
|
-
console.log(chalk.dim(` - ${t.id}`));
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
// Confirm unless -y flag
|
|
2056
|
-
if (!options.yes) {
|
|
2057
|
-
const readline = require('readline');
|
|
2058
|
-
const rl = readline.createInterface({
|
|
2059
|
-
input: process.stdin,
|
|
2060
|
-
output: process.stdout,
|
|
2061
|
-
});
|
|
2062
|
-
|
|
2063
|
-
const answer = await new Promise((resolve) => {
|
|
2064
|
-
rl.question(chalk.bold('\nProceed? [y/N] '), resolve);
|
|
2065
|
-
});
|
|
2066
|
-
rl.close();
|
|
2067
|
-
|
|
2068
|
-
if (answer.toLowerCase() !== 'y') {
|
|
2069
|
-
console.log('Aborted.');
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
console.log('');
|
|
2075
|
-
|
|
2076
|
-
// Kill clusters
|
|
2077
|
-
if (runningClusters.length > 0) {
|
|
2078
|
-
const clusterResults = await orchestrator.killAll();
|
|
2079
|
-
for (const id of clusterResults.killed) {
|
|
2080
|
-
console.log(chalk.green(`✓ Killed cluster: ${id}`));
|
|
2081
|
-
}
|
|
2082
|
-
for (const err of clusterResults.errors) {
|
|
2083
|
-
console.log(chalk.red(`✗ Failed to kill cluster ${err.id}: ${err.error}`));
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
// Kill tasks
|
|
2088
|
-
if (runningTasks.length > 0) {
|
|
2089
|
-
const { killTask, isProcessRunning: checkPid } = await import('../task-lib/runner.js');
|
|
2090
|
-
const { updateTask } = await import('../task-lib/store.js');
|
|
2091
|
-
|
|
2092
|
-
for (const task of runningTasks) {
|
|
2093
|
-
if (!checkPid(task.pid)) {
|
|
2094
|
-
updateTask(task.id, {
|
|
2095
|
-
status: 'stale',
|
|
2096
|
-
error: 'Process died unexpectedly',
|
|
2097
|
-
});
|
|
2098
|
-
console.log(chalk.yellow(`○ Task ${task.id} was already dead, marked stale`));
|
|
2099
|
-
continue;
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
const killed = killTask(task.pid);
|
|
2103
|
-
if (killed) {
|
|
2104
|
-
updateTask(task.id, {
|
|
2105
|
-
status: 'killed',
|
|
2106
|
-
error: 'Killed by kill-all',
|
|
2107
|
-
});
|
|
2108
|
-
console.log(chalk.green(`✓ Killed task: ${task.id}`));
|
|
2109
|
-
} else {
|
|
2110
|
-
console.log(chalk.red(`✗ Failed to kill task: ${task.id}`));
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
console.log(chalk.bold.green(`\nDone.`));
|
|
2116
|
-
} catch (error) {
|
|
2117
|
-
console.error('Error:', error.message);
|
|
2118
|
-
process.exit(1);
|
|
2119
|
-
}
|
|
2120
|
-
});
|
|
2121
|
-
|
|
2122
|
-
// Export command (cluster-only)
|
|
2123
|
-
program
|
|
2124
|
-
.command('export <cluster-id>')
|
|
2125
|
-
.description('Export cluster conversation')
|
|
2126
|
-
.option('-f, --format <format>', 'Export format: json, markdown, pdf', 'pdf')
|
|
2127
|
-
.option('-o, --output <file>', 'Output file (auto-generated for pdf)')
|
|
2128
|
-
.action(async (clusterId, options) => {
|
|
2129
|
-
try {
|
|
2130
|
-
// Get messages from DB
|
|
2131
|
-
const Ledger = require('../src/ledger');
|
|
2132
|
-
const homeDir = require('os').homedir();
|
|
2133
|
-
const dbPath = path.join(homeDir, '.zeroshot', `${clusterId}.db`);
|
|
2134
|
-
|
|
2135
|
-
if (!require('fs').existsSync(dbPath)) {
|
|
2136
|
-
throw new Error(`Cluster ${clusterId} not found (no DB file)`);
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
const ledger = new Ledger(dbPath);
|
|
2140
|
-
const messages = ledger.getAll(clusterId);
|
|
2141
|
-
ledger.close();
|
|
2142
|
-
|
|
2143
|
-
// JSON export
|
|
2144
|
-
if (options.format === 'json') {
|
|
2145
|
-
const data = JSON.stringify({ cluster_id: clusterId, messages }, null, 2);
|
|
2146
|
-
if (options.output) {
|
|
2147
|
-
require('fs').writeFileSync(options.output, data, 'utf8');
|
|
2148
|
-
console.log(`Exported to ${options.output}`);
|
|
2149
|
-
} else {
|
|
2150
|
-
console.log(data);
|
|
2151
|
-
}
|
|
2152
|
-
return;
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// Terminal-style export (for markdown and pdf)
|
|
2156
|
-
const terminalOutput = renderMessagesToTerminal(clusterId, messages);
|
|
2157
|
-
|
|
2158
|
-
if (options.format === 'markdown') {
|
|
2159
|
-
// Strip ANSI codes for markdown
|
|
2160
|
-
const plainText = terminalOutput.replace(/\\x1b\[[0-9;]*m/g, '');
|
|
2161
|
-
if (options.output) {
|
|
2162
|
-
require('fs').writeFileSync(options.output, plainText, 'utf8');
|
|
2163
|
-
console.log(`Exported to ${options.output}`);
|
|
2164
|
-
} else {
|
|
2165
|
-
console.log(plainText);
|
|
2166
|
-
}
|
|
2167
|
-
return;
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
// PDF export - convert ANSI to HTML, then to PDF
|
|
2171
|
-
const outputFile = options.output || `${clusterId}.pdf`;
|
|
2172
|
-
const AnsiToHtml = require('ansi-to-html');
|
|
2173
|
-
const { mdToPdf } = await import('md-to-pdf');
|
|
2174
|
-
|
|
2175
|
-
const ansiConverter = new AnsiToHtml({
|
|
2176
|
-
fg: '#d4d4d4',
|
|
2177
|
-
bg: '#1e1e1e',
|
|
2178
|
-
colors: {
|
|
2179
|
-
0: '#1e1e1e',
|
|
2180
|
-
1: '#f44747',
|
|
2181
|
-
2: '#6a9955',
|
|
2182
|
-
3: '#dcdcaa',
|
|
2183
|
-
4: '#569cd6',
|
|
2184
|
-
5: '#c586c0',
|
|
2185
|
-
6: '#4ec9b0',
|
|
2186
|
-
7: '#d4d4d4',
|
|
2187
|
-
8: '#808080',
|
|
2188
|
-
9: '#f44747',
|
|
2189
|
-
10: '#6a9955',
|
|
2190
|
-
11: '#dcdcaa',
|
|
2191
|
-
12: '#569cd6',
|
|
2192
|
-
13: '#c586c0',
|
|
2193
|
-
14: '#4ec9b0',
|
|
2194
|
-
15: '#ffffff',
|
|
2195
|
-
},
|
|
2196
|
-
});
|
|
2197
|
-
|
|
2198
|
-
const htmlContent = ansiConverter.toHtml(terminalOutput);
|
|
2199
|
-
const fullHtml = `<pre style="margin:0;padding:0;white-space:pre-wrap;word-wrap:break-word;">${htmlContent}</pre>`;
|
|
2200
|
-
|
|
2201
|
-
const pdf = await mdToPdf(
|
|
2202
|
-
{ content: fullHtml },
|
|
2203
|
-
{
|
|
2204
|
-
pdf_options: {
|
|
2205
|
-
format: 'A4',
|
|
2206
|
-
margin: {
|
|
2207
|
-
top: '10mm',
|
|
2208
|
-
right: '10mm',
|
|
2209
|
-
bottom: '10mm',
|
|
2210
|
-
left: '10mm',
|
|
2211
|
-
},
|
|
2212
|
-
printBackground: true,
|
|
2213
|
-
},
|
|
2214
|
-
css: `
|
|
2215
|
-
@page { size: A4 landscape; }
|
|
2216
|
-
body {
|
|
2217
|
-
margin: 0; padding: 16px;
|
|
2218
|
-
background: #1e1e1e; color: #d4d4d4;
|
|
2219
|
-
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', monospace;
|
|
2220
|
-
font-size: 9pt; line-height: 1.4;
|
|
2221
|
-
}
|
|
2222
|
-
pre { margin: 0; font-family: inherit; }
|
|
2223
|
-
`,
|
|
2224
|
-
}
|
|
2225
|
-
);
|
|
2226
|
-
|
|
2227
|
-
require('fs').writeFileSync(outputFile, pdf.content);
|
|
2228
|
-
console.log(`Exported to ${outputFile}`);
|
|
2229
|
-
} catch (error) {
|
|
2230
|
-
console.error('Error exporting cluster:', error.message);
|
|
2231
|
-
process.exit(1);
|
|
2232
|
-
}
|
|
2233
|
-
});
|
|
2234
|
-
|
|
2235
|
-
// === TASK-SPECIFIC COMMANDS ===
|
|
2236
|
-
|
|
2237
|
-
// Resume task or cluster
|
|
2238
|
-
program
|
|
2239
|
-
.command('resume <id> [prompt]')
|
|
2240
|
-
.description('Resume a failed task or cluster')
|
|
2241
|
-
.option('-d, --detach', 'Resume in background (daemon mode)')
|
|
2242
|
-
.action(async (id, prompt, options) => {
|
|
2243
|
-
try {
|
|
2244
|
-
// Try cluster first, then task (both use same ID format: "adjective-noun-number")
|
|
2245
|
-
const OrchestratorModule = require('../src/orchestrator');
|
|
2246
|
-
const orchestrator = new OrchestratorModule();
|
|
2247
|
-
|
|
2248
|
-
// Check if cluster exists
|
|
2249
|
-
const cluster = orchestrator.getCluster(id);
|
|
2250
|
-
|
|
2251
|
-
const settings = loadSettings();
|
|
2252
|
-
|
|
2253
|
-
if (cluster) {
|
|
2254
|
-
// === PREFLIGHT CHECKS ===
|
|
2255
|
-
// Provider CLI must be installed; Docker needed if isolation was used
|
|
2256
|
-
const requiresDocker = cluster?.isolation?.enabled || false;
|
|
2257
|
-
const providerName =
|
|
2258
|
-
cluster.config?.forceProvider ||
|
|
2259
|
-
cluster.config?.defaultProvider ||
|
|
2260
|
-
settings.defaultProvider;
|
|
2261
|
-
|
|
2262
|
-
requirePreflight({
|
|
2263
|
-
requireGh: false, // Resume doesn't fetch new issues
|
|
2264
|
-
requireDocker: requiresDocker,
|
|
2265
|
-
quiet: false,
|
|
2266
|
-
provider: providerName,
|
|
2267
|
-
});
|
|
2268
|
-
|
|
2269
|
-
// Resume cluster
|
|
2270
|
-
console.log(chalk.cyan(`Resuming cluster ${id}...`));
|
|
2271
|
-
const result = await orchestrator.resume(id, prompt);
|
|
2272
|
-
|
|
2273
|
-
console.log(chalk.green(`✓ Cluster resumed`));
|
|
2274
|
-
if (result.resumeType === 'failure') {
|
|
2275
|
-
console.log(` Resume type: ${chalk.yellow('From failure')}`);
|
|
2276
|
-
console.log(` Resumed agent: ${result.resumedAgent}`);
|
|
2277
|
-
console.log(` Previous error: ${result.previousError}`);
|
|
2278
|
-
} else {
|
|
2279
|
-
console.log(` Resume type: ${chalk.cyan('Clean continuation')}`);
|
|
2280
|
-
if (result.resumedAgents && result.resumedAgents.length > 0) {
|
|
2281
|
-
console.log(` Resumed agents: ${result.resumedAgents.join(', ')}`);
|
|
2282
|
-
} else {
|
|
2283
|
-
console.log(` Published CLUSTER_RESUMED to trigger workflow`);
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
// === DAEMON MODE: Exit and let cluster run in background ===
|
|
2288
|
-
if (options.detach) {
|
|
2289
|
-
console.log('');
|
|
2290
|
-
console.log(chalk.dim(`Follow logs with: zeroshot logs ${id} -f`));
|
|
2291
|
-
return;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
// === FOREGROUND MODE: Stream logs in real-time (same as 'run' command) ===
|
|
2295
|
-
console.log('');
|
|
2296
|
-
console.log(chalk.dim('Streaming logs... (Ctrl+C to stop cluster)'));
|
|
2297
|
-
console.log('');
|
|
2298
|
-
|
|
2299
|
-
// Get the cluster's message bus for streaming
|
|
2300
|
-
const resumedCluster = orchestrator.getCluster(id);
|
|
2301
|
-
if (!resumedCluster || !resumedCluster.messageBus) {
|
|
2302
|
-
console.error(chalk.red('Failed to get message bus for resumed cluster'));
|
|
2303
|
-
process.exit(1);
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
// Track senders that have output (for periodic flushing)
|
|
2307
|
-
const sendersWithOutput = new Set();
|
|
2308
|
-
// Track messages we've already processed (to avoid duplicates between history and subscription)
|
|
2309
|
-
const processedMessageIds = new Set();
|
|
2310
|
-
|
|
2311
|
-
// Message handler - processes messages, deduplicates by ID
|
|
2312
|
-
const handleMessage = (msg) => {
|
|
2313
|
-
if (msg.cluster_id !== id) return;
|
|
2314
|
-
if (processedMessageIds.has(msg.id)) return;
|
|
2315
|
-
processedMessageIds.add(msg.id);
|
|
2316
|
-
|
|
2317
|
-
if (msg.topic === 'AGENT_OUTPUT' && msg.sender) {
|
|
2318
|
-
sendersWithOutput.add(msg.sender);
|
|
2319
|
-
}
|
|
2320
|
-
printMessage(msg, false, false, true);
|
|
2321
|
-
};
|
|
2322
|
-
|
|
2323
|
-
// Subscribe to NEW messages
|
|
2324
|
-
const unsubscribe = resumedCluster.messageBus.subscribe(handleMessage);
|
|
2325
|
-
|
|
2326
|
-
// Periodic flush of text buffers (streaming text may not have newlines)
|
|
2327
|
-
const flushInterval = setInterval(() => {
|
|
2328
|
-
for (const sender of sendersWithOutput) {
|
|
2329
|
-
const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
|
|
2330
|
-
flushLineBuffer(prefix, sender);
|
|
2331
|
-
}
|
|
2332
|
-
}, 250);
|
|
2333
|
-
|
|
2334
|
-
// Wait for cluster to complete
|
|
2335
|
-
await new Promise((resolve) => {
|
|
2336
|
-
const checkInterval = setInterval(() => {
|
|
2337
|
-
try {
|
|
2338
|
-
const status = orchestrator.getStatus(id);
|
|
2339
|
-
if (status.state !== 'running') {
|
|
2340
|
-
clearInterval(checkInterval);
|
|
2341
|
-
clearInterval(flushInterval);
|
|
2342
|
-
// Final flush
|
|
2343
|
-
for (const sender of sendersWithOutput) {
|
|
2344
|
-
const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
|
|
2345
|
-
flushLineBuffer(prefix, sender);
|
|
2346
|
-
}
|
|
2347
|
-
unsubscribe();
|
|
2348
|
-
resolve();
|
|
2349
|
-
}
|
|
2350
|
-
} catch {
|
|
2351
|
-
// Cluster may have been removed
|
|
2352
|
-
clearInterval(checkInterval);
|
|
2353
|
-
clearInterval(flushInterval);
|
|
2354
|
-
unsubscribe();
|
|
2355
|
-
resolve();
|
|
2356
|
-
}
|
|
2357
|
-
}, 500);
|
|
2358
|
-
|
|
2359
|
-
// Handle Ctrl+C: Stop cluster since foreground mode has no daemon
|
|
2360
|
-
// CRITICAL: In foreground mode, the cluster runs IN this process.
|
|
2361
|
-
// If we exit without stopping, the cluster becomes a zombie (state=running but no process).
|
|
2362
|
-
process.on('SIGINT', async () => {
|
|
2363
|
-
console.log(chalk.dim('\n\n--- Interrupted ---'));
|
|
2364
|
-
clearInterval(checkInterval);
|
|
2365
|
-
clearInterval(flushInterval);
|
|
2366
|
-
unsubscribe();
|
|
2367
|
-
|
|
2368
|
-
// Stop the cluster properly so state is updated
|
|
2369
|
-
try {
|
|
2370
|
-
console.log(chalk.dim(`Stopping cluster ${id}...`));
|
|
2371
|
-
await orchestrator.stop(id);
|
|
2372
|
-
console.log(chalk.dim(`Cluster ${id} stopped.`));
|
|
2373
|
-
} catch (stopErr) {
|
|
2374
|
-
console.error(chalk.red(`Failed to stop cluster: ${stopErr.message}`));
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
process.exit(0);
|
|
2378
|
-
});
|
|
2379
|
-
});
|
|
2380
|
-
|
|
2381
|
-
console.log(chalk.dim(`\nCluster ${id} completed.`));
|
|
2382
|
-
} else {
|
|
2383
|
-
let providerName = settings.defaultProvider;
|
|
2384
|
-
try {
|
|
2385
|
-
const { getTask } = await import('../task-lib/store.js');
|
|
2386
|
-
const task = getTask(id);
|
|
2387
|
-
if (task?.provider) {
|
|
2388
|
-
providerName = task.provider;
|
|
2389
|
-
}
|
|
2390
|
-
} catch {
|
|
2391
|
-
// If task store is unavailable, fall back to default provider
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
requirePreflight({
|
|
2395
|
-
requireGh: false,
|
|
2396
|
-
requireDocker: false,
|
|
2397
|
-
quiet: false,
|
|
2398
|
-
provider: providerName,
|
|
2399
|
-
});
|
|
2400
|
-
|
|
2401
|
-
// Try resuming as task
|
|
2402
|
-
const { resumeTask } = await import('../task-lib/commands/resume.js');
|
|
2403
|
-
await resumeTask(id, prompt);
|
|
2404
|
-
}
|
|
2405
|
-
} catch (error) {
|
|
2406
|
-
console.error(chalk.red('Error resuming:'), error.message);
|
|
2407
|
-
process.exit(1);
|
|
2408
|
-
}
|
|
2409
|
-
});
|
|
2410
|
-
|
|
2411
|
-
// Finish cluster - convert to single-agent completion task
|
|
2412
|
-
program
|
|
2413
|
-
.command('finish <id>')
|
|
2414
|
-
.description('Take existing cluster and create completion-focused task (creates PR and merges)')
|
|
2415
|
-
.option('-y, --yes', 'Skip confirmation if cluster is running')
|
|
2416
|
-
.action(async (id, options) => {
|
|
2417
|
-
try {
|
|
2418
|
-
const OrchestratorModule = require('../src/orchestrator');
|
|
2419
|
-
const orchestrator = new OrchestratorModule();
|
|
2420
|
-
|
|
2421
|
-
// Check if cluster exists
|
|
2422
|
-
const cluster = orchestrator.getCluster(id);
|
|
2423
|
-
|
|
2424
|
-
if (!cluster) {
|
|
2425
|
-
console.error(chalk.red(`Error: Cluster ${id} not found`));
|
|
2426
|
-
console.error(chalk.dim('Use "zeroshot list" to see available clusters'));
|
|
2427
|
-
process.exit(1);
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
// Stop cluster if it's running (with confirmation unless -y)
|
|
2431
|
-
if (cluster.state === 'running') {
|
|
2432
|
-
if (!options.y && !options.yes) {
|
|
2433
|
-
console.log(chalk.yellow(`Cluster ${id} is still running.`));
|
|
2434
|
-
console.log(chalk.dim('Stopping it before converting to completion task...'));
|
|
2435
|
-
console.log('');
|
|
2436
|
-
|
|
2437
|
-
// Simple confirmation prompt
|
|
2438
|
-
const readline = require('readline');
|
|
2439
|
-
const rl = readline.createInterface({
|
|
2440
|
-
input: process.stdin,
|
|
2441
|
-
output: process.stdout,
|
|
2442
|
-
});
|
|
2443
|
-
|
|
2444
|
-
const answer = await new Promise((resolve) => {
|
|
2445
|
-
rl.question(chalk.yellow('Continue? (y/N) '), resolve);
|
|
2446
|
-
});
|
|
2447
|
-
rl.close();
|
|
2448
|
-
|
|
2449
|
-
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
2450
|
-
console.log(chalk.red('Aborted'));
|
|
2451
|
-
process.exit(0);
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
console.log(chalk.cyan('Stopping cluster...'));
|
|
2456
|
-
await orchestrator.stop(id);
|
|
2457
|
-
console.log(chalk.green('✓ Cluster stopped'));
|
|
2458
|
-
console.log('');
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
console.log(chalk.cyan(`Converting cluster ${id} to completion task...`));
|
|
2462
|
-
console.log('');
|
|
2463
|
-
|
|
2464
|
-
// Extract cluster context from ledger
|
|
2465
|
-
const messages = cluster.messageBus.getAll(id);
|
|
2466
|
-
|
|
2467
|
-
// Find original task
|
|
2468
|
-
const issueOpened = messages.find((m) => m.topic === 'ISSUE_OPENED');
|
|
2469
|
-
const taskText = issueOpened?.content?.text || 'Unknown task';
|
|
2470
|
-
const issueNumber = issueOpened?.content?.data?.issue_number;
|
|
2471
|
-
const issueTitle = issueOpened?.content?.data?.title || 'Implementation';
|
|
2472
|
-
|
|
2473
|
-
// Find what's been done
|
|
2474
|
-
const agentOutputs = messages.filter((m) => m.topic === 'AGENT_OUTPUT');
|
|
2475
|
-
const validations = messages.filter((m) => m.topic === 'VALIDATION_RESULT');
|
|
2476
|
-
|
|
2477
|
-
// Build context summary
|
|
2478
|
-
let contextSummary = `# Original Task\n\n${taskText}\n\n`;
|
|
2479
|
-
|
|
2480
|
-
if (issueNumber) {
|
|
2481
|
-
contextSummary += `Issue: #${issueNumber} - ${issueTitle}\n\n`;
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
contextSummary += `# Progress So Far\n\n`;
|
|
2485
|
-
contextSummary += `- ${agentOutputs.length} agent outputs\n`;
|
|
2486
|
-
contextSummary += `- ${validations.length} validation results\n`;
|
|
2487
|
-
|
|
2488
|
-
const approvedValidations = validations.filter(
|
|
2489
|
-
(v) => v.content?.data?.approved === true || v.content?.data?.approved === 'true'
|
|
2490
|
-
);
|
|
2491
|
-
contextSummary += `- ${approvedValidations.length} approvals\n\n`;
|
|
2492
|
-
|
|
2493
|
-
// Add recent validation summaries
|
|
2494
|
-
if (validations.length > 0) {
|
|
2495
|
-
contextSummary += `## Recent Validations\n\n`;
|
|
2496
|
-
for (const v of validations.slice(-3)) {
|
|
2497
|
-
const approved =
|
|
2498
|
-
v.content?.data?.approved === true || v.content?.data?.approved === 'true';
|
|
2499
|
-
const icon = approved ? '✅' : '❌';
|
|
2500
|
-
contextSummary += `${icon} **${v.sender}**: ${v.content?.data?.summary || 'No summary'}\n`;
|
|
2501
|
-
}
|
|
2502
|
-
contextSummary += '\n';
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
// Build ultra-aggressive completion prompt (always merges)
|
|
2506
|
-
const mergeGoal = 'CREATE PR AND MERGE IT';
|
|
2507
|
-
const mergeStep = `
|
|
2508
|
-
8. MERGE THE PR - THIS IS MANDATORY:
|
|
2509
|
-
\`\`\`bash
|
|
2510
|
-
gh pr merge --merge --auto
|
|
2511
|
-
\`\`\`
|
|
2512
|
-
|
|
2513
|
-
IF MERGE FAILS DUE TO CONFLICTS:
|
|
2514
|
-
a) Pull and rebase: \`git fetch origin main && git rebase origin/main\`
|
|
2515
|
-
b) Resolve conflicts manually (read files, make decisions, edit)
|
|
2516
|
-
c) \`git add <resolved-files> && git rebase --continue\`
|
|
2517
|
-
d) Force push: \`git push --force-with-lease\`
|
|
2518
|
-
e) Retry merge: \`gh pr merge --merge\`
|
|
2519
|
-
|
|
2520
|
-
REPEAT UNTIL MERGED. DO NOT GIVE UP.`;
|
|
2521
|
-
|
|
2522
|
-
const completionPrompt = `# YOUR MISSION: ${mergeGoal}
|
|
2523
|
-
|
|
2524
|
-
${contextSummary}
|
|
2525
|
-
|
|
2526
|
-
# WHAT YOU MUST DO NOW
|
|
2527
|
-
|
|
2528
|
-
You are the FINISHER. Your ONLY job is to take this cluster's work and push it across the finish line.
|
|
2529
|
-
|
|
2530
|
-
**FAILURE IS NOT AN OPTION. YOU MUST COMPLETE THIS TASK.**
|
|
2531
|
-
|
|
2532
|
-
## Steps (MUST complete ALL):
|
|
2533
|
-
|
|
2534
|
-
1. REVIEW CURRENT STATE - Check git status, see what's changed:
|
|
2535
|
-
\`\`\`bash
|
|
2536
|
-
git status
|
|
2537
|
-
git diff
|
|
2538
|
-
\`\`\`
|
|
2539
|
-
|
|
2540
|
-
2. COMMIT ALL CHANGES - Stage and commit everything:
|
|
2541
|
-
\`\`\`bash
|
|
2542
|
-
git add .
|
|
2543
|
-
git commit -m "${issueTitle || 'feat: implement task'}"
|
|
2544
|
-
\`\`\`
|
|
2545
|
-
|
|
2546
|
-
3. CREATE BRANCH - Use issue number if available:
|
|
2547
|
-
\`\`\`bash
|
|
2548
|
-
${issueNumber ? `git checkout -b issue-${issueNumber}` : 'git checkout -b feature/implementation'}
|
|
2549
|
-
\`\`\`
|
|
2550
|
-
|
|
2551
|
-
4. PUSH TO REMOTE:
|
|
2552
|
-
\`\`\`bash
|
|
2553
|
-
git push -u origin HEAD
|
|
2554
|
-
\`\`\`
|
|
2555
|
-
|
|
2556
|
-
5. CREATE PULL REQUEST:
|
|
2557
|
-
\`\`\`bash
|
|
2558
|
-
gh pr create --title "${issueTitle || 'Implementation'}" --body "Closes #${issueNumber || 'N/A'}
|
|
2559
|
-
|
|
2560
|
-
## Summary
|
|
2561
|
-
${taskText.slice(0, 200)}...
|
|
2562
|
-
|
|
2563
|
-
## Changes
|
|
2564
|
-
- Implementation complete
|
|
2565
|
-
- All validations addressed
|
|
2566
|
-
|
|
2567
|
-
🤖 Generated with zeroshot finish"
|
|
2568
|
-
\`\`\`
|
|
2569
|
-
|
|
2570
|
-
6. GET PR URL:
|
|
2571
|
-
\`\`\`bash
|
|
2572
|
-
gh pr view --json url -q .url
|
|
2573
|
-
\`\`\`
|
|
2574
|
-
|
|
2575
|
-
7. OUTPUT THE PR URL - Print it clearly so user can see it
|
|
2576
|
-
${mergeStep}
|
|
2577
|
-
|
|
2578
|
-
## RULES
|
|
2579
|
-
|
|
2580
|
-
- NO EXCUSES: If something fails, FIX IT and retry
|
|
2581
|
-
- NO SHORTCUTS: Follow ALL steps above
|
|
2582
|
-
- NO PARTIAL WORK: Must reach PR creation and merge
|
|
2583
|
-
- IF TESTS FAIL: Fix them until they pass
|
|
2584
|
-
- IF CI FAILS: Wait for it, fix issues, retry
|
|
2585
|
-
- IF CONFLICTS: Resolve them intelligently
|
|
2586
|
-
|
|
2587
|
-
**DO NOT STOP UNTIL YOU HAVE A MERGED PR.**`;
|
|
2588
|
-
|
|
2589
|
-
// Show preview
|
|
2590
|
-
console.log(chalk.dim('='.repeat(80)));
|
|
2591
|
-
console.log(chalk.dim('Task prompt preview:'));
|
|
2592
|
-
console.log(chalk.dim('='.repeat(80)));
|
|
2593
|
-
console.log(completionPrompt.split('\n').slice(0, 20).join('\n'));
|
|
2594
|
-
console.log(chalk.dim('... (truncated) ...\n'));
|
|
2595
|
-
console.log(chalk.dim('='.repeat(80)));
|
|
2596
|
-
console.log('');
|
|
2597
|
-
|
|
2598
|
-
// Launch as task (preserve isolation if cluster was isolated)
|
|
2599
|
-
console.log(chalk.cyan('Launching completion task...'));
|
|
2600
|
-
const { runTask } = await import('../task-lib/commands/run.js');
|
|
2601
|
-
|
|
2602
|
-
const taskOptions = {
|
|
2603
|
-
cwd: process.cwd(),
|
|
2604
|
-
};
|
|
2605
|
-
|
|
2606
|
-
// If cluster was in isolation mode, pass container info to task
|
|
2607
|
-
if (cluster.isolation?.enabled && cluster.isolation?.containerId) {
|
|
2608
|
-
console.log(chalk.dim(`Using isolation container: ${cluster.isolation.containerId}`));
|
|
2609
|
-
taskOptions.isolation = {
|
|
2610
|
-
containerId: cluster.isolation.containerId,
|
|
2611
|
-
workDir: '/workspace', // Standard workspace mount point in isolation containers
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
await runTask(completionPrompt, taskOptions);
|
|
2616
|
-
|
|
2617
|
-
console.log('');
|
|
2618
|
-
console.log(chalk.green(`✓ Completion task started`));
|
|
2619
|
-
if (cluster.isolation?.enabled) {
|
|
2620
|
-
console.log(chalk.dim('Running in isolation container (same as cluster)'));
|
|
2621
|
-
}
|
|
2622
|
-
console.log(chalk.dim('Monitor with: zeroshot list'));
|
|
2623
|
-
} catch (error) {
|
|
2624
|
-
console.error(chalk.red('Error:'), error.message);
|
|
2625
|
-
process.exit(1);
|
|
2626
|
-
}
|
|
2627
|
-
});
|
|
2628
|
-
|
|
2629
|
-
// Clean tasks
|
|
2630
|
-
program
|
|
2631
|
-
.command('clean')
|
|
2632
|
-
.description('Remove old task records and logs')
|
|
2633
|
-
.option('-a, --all', 'Remove all tasks')
|
|
2634
|
-
.option('-c, --completed', 'Remove completed tasks')
|
|
2635
|
-
.option('-f, --failed', 'Remove failed/stale/killed tasks')
|
|
2636
|
-
.action(async (options) => {
|
|
2637
|
-
try {
|
|
2638
|
-
const { cleanTasks } = await import('../task-lib/commands/clean.js');
|
|
2639
|
-
await cleanTasks(options);
|
|
2640
|
-
} catch (error) {
|
|
2641
|
-
console.error('Error cleaning tasks:', error.message);
|
|
2642
|
-
process.exit(1);
|
|
2643
|
-
}
|
|
2644
|
-
});
|
|
2645
|
-
|
|
2646
|
-
// Purge all runs (clusters + tasks) - NUCLEAR option
|
|
2647
|
-
program
|
|
2648
|
-
.command('purge')
|
|
2649
|
-
.description('NUCLEAR: Kill all running processes and delete all data')
|
|
2650
|
-
.option('-y, --yes', 'Skip confirmation')
|
|
2651
|
-
.action(async (options) => {
|
|
2652
|
-
try {
|
|
2653
|
-
const orchestrator = await getOrchestrator();
|
|
2654
|
-
|
|
2655
|
-
// Get counts first
|
|
2656
|
-
const clusters = orchestrator.listClusters();
|
|
2657
|
-
const runningClusters = clusters.filter(
|
|
2658
|
-
(c) => c.state === 'running' || c.state === 'initializing'
|
|
2659
|
-
);
|
|
2660
|
-
|
|
2661
|
-
const { loadTasks } = await import('../task-lib/store.js');
|
|
2662
|
-
const { isProcessRunning } = await import('../task-lib/runner.js');
|
|
2663
|
-
const tasks = Object.values(loadTasks());
|
|
2664
|
-
const runningTasks = tasks.filter((t) => t.status === 'running' && isProcessRunning(t.pid));
|
|
2665
|
-
|
|
2666
|
-
// Check if there's anything to clear
|
|
2667
|
-
if (clusters.length === 0 && tasks.length === 0) {
|
|
2668
|
-
console.log(chalk.dim('No clusters or tasks to clear.'));
|
|
2669
|
-
return;
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
|
-
// Show what will be cleared
|
|
2673
|
-
console.log(chalk.bold('\nWill kill and delete:'));
|
|
2674
|
-
if (clusters.length > 0) {
|
|
2675
|
-
console.log(chalk.cyan(` ${clusters.length} cluster(s) with all history`));
|
|
2676
|
-
if (runningClusters.length > 0) {
|
|
2677
|
-
console.log(chalk.yellow(` ${runningClusters.length} running`));
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
if (tasks.length > 0) {
|
|
2681
|
-
console.log(chalk.yellow(` ${tasks.length} task(s) with all logs`));
|
|
2682
|
-
if (runningTasks.length > 0) {
|
|
2683
|
-
console.log(chalk.yellow(` ${runningTasks.length} running`));
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
console.log('');
|
|
2687
|
-
|
|
2688
|
-
// Confirm unless -y flag
|
|
2689
|
-
if (!options.yes) {
|
|
2690
|
-
const readline = require('readline');
|
|
2691
|
-
const rl = readline.createInterface({
|
|
2692
|
-
input: process.stdin,
|
|
2693
|
-
output: process.stdout,
|
|
2694
|
-
});
|
|
2695
|
-
|
|
2696
|
-
const answer = await new Promise((resolve) => {
|
|
2697
|
-
rl.question(
|
|
2698
|
-
chalk.bold.red(
|
|
2699
|
-
'This will kill all processes and permanently delete all data. Proceed? [y/N] '
|
|
2700
|
-
),
|
|
2701
|
-
resolve
|
|
2702
|
-
);
|
|
2703
|
-
});
|
|
2704
|
-
rl.close();
|
|
2705
|
-
|
|
2706
|
-
if (answer.toLowerCase() !== 'y') {
|
|
2707
|
-
console.log('Aborted.');
|
|
2708
|
-
return;
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
console.log('');
|
|
2713
|
-
|
|
2714
|
-
// Kill running clusters first
|
|
2715
|
-
if (runningClusters.length > 0) {
|
|
2716
|
-
console.log(chalk.bold('Killing running clusters...'));
|
|
2717
|
-
const clusterResults = await orchestrator.killAll();
|
|
2718
|
-
for (const id of clusterResults.killed) {
|
|
2719
|
-
console.log(chalk.green(`✓ Killed cluster: ${id}`));
|
|
2720
|
-
}
|
|
2721
|
-
for (const err of clusterResults.errors) {
|
|
2722
|
-
console.log(chalk.red(`✗ Failed to kill cluster ${err.id}: ${err.error}`));
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
// Kill running tasks
|
|
2727
|
-
if (runningTasks.length > 0) {
|
|
2728
|
-
console.log(chalk.bold('Killing running tasks...'));
|
|
2729
|
-
const { killTask } = await import('../task-lib/runner.js');
|
|
2730
|
-
const { updateTask } = await import('../task-lib/store.js');
|
|
2731
|
-
|
|
2732
|
-
for (const task of runningTasks) {
|
|
2733
|
-
if (!isProcessRunning(task.pid)) {
|
|
2734
|
-
updateTask(task.id, {
|
|
2735
|
-
status: 'stale',
|
|
2736
|
-
error: 'Process died unexpectedly',
|
|
2737
|
-
});
|
|
2738
|
-
console.log(chalk.yellow(`○ Task ${task.id} was already dead, marked stale`));
|
|
2739
|
-
continue;
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
const killed = killTask(task.pid);
|
|
2743
|
-
if (killed) {
|
|
2744
|
-
updateTask(task.id, { status: 'killed', error: 'Killed by clear' });
|
|
2745
|
-
console.log(chalk.green(`✓ Killed task: ${task.id}`));
|
|
2746
|
-
} else {
|
|
2747
|
-
console.log(chalk.red(`✗ Failed to kill task: ${task.id}`));
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
|
-
// Delete all cluster data
|
|
2753
|
-
if (clusters.length > 0) {
|
|
2754
|
-
console.log(chalk.bold('Deleting cluster data...'));
|
|
2755
|
-
const clustersFile = path.join(orchestrator.storageDir, 'clusters.json');
|
|
2756
|
-
const clustersDir = path.join(orchestrator.storageDir, 'clusters');
|
|
2757
|
-
|
|
2758
|
-
// Delete all cluster databases
|
|
2759
|
-
for (const cluster of clusters) {
|
|
2760
|
-
const dbPath = path.join(orchestrator.storageDir, `${cluster.id}.db`);
|
|
2761
|
-
if (fs.existsSync(dbPath)) {
|
|
2762
|
-
fs.unlinkSync(dbPath);
|
|
2763
|
-
console.log(chalk.green(`✓ Deleted cluster database: ${cluster.id}.db`));
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
// Delete clusters.json
|
|
2768
|
-
if (fs.existsSync(clustersFile)) {
|
|
2769
|
-
fs.unlinkSync(clustersFile);
|
|
2770
|
-
console.log(chalk.green(`✓ Deleted clusters.json`));
|
|
2771
|
-
}
|
|
2772
|
-
|
|
2773
|
-
// Delete clusters directory if exists
|
|
2774
|
-
if (fs.existsSync(clustersDir)) {
|
|
2775
|
-
fs.rmSync(clustersDir, { recursive: true, force: true });
|
|
2776
|
-
console.log(chalk.green(`✓ Deleted clusters/ directory`));
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
// Clear in-memory clusters
|
|
2780
|
-
orchestrator.clusters.clear();
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
// Delete all task data
|
|
2784
|
-
if (tasks.length > 0) {
|
|
2785
|
-
console.log(chalk.bold('Deleting task data...'));
|
|
2786
|
-
const { cleanTasks } = await import('../task-lib/commands/clean.js');
|
|
2787
|
-
await cleanTasks({ all: true });
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
console.log(chalk.bold.green('\nAll runs purged.'));
|
|
2791
|
-
} catch (error) {
|
|
2792
|
-
console.error('Error clearing runs:', error.message);
|
|
2793
|
-
process.exit(1);
|
|
2794
|
-
}
|
|
2795
|
-
});
|
|
2796
|
-
|
|
2797
|
-
// Schedule a task
|
|
2798
|
-
program
|
|
2799
|
-
.command('schedule <prompt>')
|
|
2800
|
-
.description('Create a recurring scheduled task')
|
|
2801
|
-
.option('-e, --every <interval>', 'Interval (e.g., "1h", "30m", "1d")')
|
|
2802
|
-
.option('--cron <expression>', 'Cron expression')
|
|
2803
|
-
.option('-C, --cwd <path>', 'Working directory')
|
|
2804
|
-
.action(async (prompt, options) => {
|
|
2805
|
-
try {
|
|
2806
|
-
const { createSchedule } = await import('../task-lib/commands/schedule.js');
|
|
2807
|
-
await createSchedule(prompt, options);
|
|
2808
|
-
} catch (error) {
|
|
2809
|
-
console.error('Error creating schedule:', error.message);
|
|
2810
|
-
process.exit(1);
|
|
2811
|
-
}
|
|
2812
|
-
});
|
|
2813
|
-
|
|
2814
|
-
// List schedules
|
|
2815
|
-
program
|
|
2816
|
-
.command('schedules')
|
|
2817
|
-
.description('List all scheduled tasks')
|
|
2818
|
-
.action(async () => {
|
|
2819
|
-
try {
|
|
2820
|
-
const { listSchedules } = await import('../task-lib/commands/schedules.js');
|
|
2821
|
-
await listSchedules();
|
|
2822
|
-
} catch (error) {
|
|
2823
|
-
console.error('Error listing schedules:', error.message);
|
|
2824
|
-
process.exit(1);
|
|
2825
|
-
}
|
|
2826
|
-
});
|
|
2827
|
-
|
|
2828
|
-
// Unschedule a task
|
|
2829
|
-
program
|
|
2830
|
-
.command('unschedule <scheduleId>')
|
|
2831
|
-
.description('Remove a scheduled task')
|
|
2832
|
-
.action(async (scheduleId) => {
|
|
2833
|
-
try {
|
|
2834
|
-
const { deleteSchedule } = await import('../task-lib/commands/unschedule.js');
|
|
2835
|
-
await deleteSchedule(scheduleId);
|
|
2836
|
-
} catch (error) {
|
|
2837
|
-
console.error('Error unscheduling:', error.message);
|
|
2838
|
-
process.exit(1);
|
|
2839
|
-
}
|
|
2840
|
-
});
|
|
2841
|
-
|
|
2842
|
-
// Scheduler daemon management
|
|
2843
|
-
program
|
|
2844
|
-
.command('scheduler <action>')
|
|
2845
|
-
.description('Manage scheduler daemon (start, stop, status, logs)')
|
|
2846
|
-
.action(async (action) => {
|
|
2847
|
-
try {
|
|
2848
|
-
const { schedulerCommand } = await import('../task-lib/commands/scheduler-cmd.js');
|
|
2849
|
-
await schedulerCommand(action);
|
|
2850
|
-
} catch (error) {
|
|
2851
|
-
console.error('Error managing scheduler:', error.message);
|
|
2852
|
-
process.exit(1);
|
|
2853
|
-
}
|
|
2854
|
-
});
|
|
2855
|
-
|
|
2856
|
-
// Get log path (machine-readable)
|
|
2857
|
-
program
|
|
2858
|
-
.command('get-log-path <taskId>')
|
|
2859
|
-
.description('Output log file path for a task (machine-readable)')
|
|
2860
|
-
.action(async (taskId) => {
|
|
2861
|
-
try {
|
|
2862
|
-
const { getLogPath } = await import('../task-lib/commands/get-log-path.js');
|
|
2863
|
-
await getLogPath(taskId);
|
|
2864
|
-
} catch (error) {
|
|
2865
|
-
console.error('Error getting log path:', error.message);
|
|
2866
|
-
process.exit(1);
|
|
2867
|
-
}
|
|
2868
|
-
});
|
|
2869
|
-
|
|
2870
|
-
// Watch command - interactive TUI dashboard
|
|
2871
|
-
program
|
|
2872
|
-
.command('watch')
|
|
2873
|
-
.description('Interactive TUI to monitor clusters')
|
|
2874
|
-
.option('--refresh-rate <ms>', 'Refresh interval in milliseconds', '1000')
|
|
2875
|
-
.action(async (options) => {
|
|
2876
|
-
try {
|
|
2877
|
-
const TUI = require('../src/tui');
|
|
2878
|
-
const tui = new TUI({
|
|
2879
|
-
orchestrator: await getOrchestrator(),
|
|
2880
|
-
refreshRate: parseInt(options.refreshRate, 10),
|
|
2881
|
-
});
|
|
2882
|
-
await tui.start();
|
|
2883
|
-
} catch (error) {
|
|
2884
|
-
console.error('Error starting TUI:', error.message);
|
|
2885
|
-
process.exit(1);
|
|
2886
|
-
}
|
|
2887
|
-
});
|
|
2888
|
-
|
|
2889
|
-
// Settings management
|
|
2890
|
-
const settingsCmd = program.command('settings').description('Manage zeroshot settings');
|
|
2891
|
-
|
|
2892
|
-
/**
|
|
2893
|
-
* Format settings list with grouped Docker configuration
|
|
2894
|
-
* Docker mounts shown as expanded table instead of raw JSON
|
|
2895
|
-
*/
|
|
2896
|
-
function formatSettingsList(settings, showUsage = false) {
|
|
2897
|
-
const DOCKER_KEYS = ['dockerMounts', 'dockerEnvPassthrough', 'dockerContainerHome'];
|
|
2898
|
-
|
|
2899
|
-
console.log(chalk.bold('\nSettings:\n'));
|
|
2900
|
-
|
|
2901
|
-
// Non-docker settings first
|
|
2902
|
-
for (const [key, value] of Object.entries(settings)) {
|
|
2903
|
-
if (DOCKER_KEYS.includes(key)) continue;
|
|
2904
|
-
|
|
2905
|
-
const isDefault = JSON.stringify(DEFAULT_SETTINGS[key]) === JSON.stringify(value);
|
|
2906
|
-
const label = isDefault ? chalk.dim(key) : chalk.cyan(key);
|
|
2907
|
-
const val = isDefault ? chalk.dim(String(value)) : chalk.white(String(value));
|
|
2908
|
-
console.log(` ${label.padEnd(30)} ${val}`);
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
|
-
// Docker configuration section (collapsible/grouped)
|
|
2912
|
-
console.log('');
|
|
2913
|
-
console.log(chalk.bold(' Docker Configuration:'));
|
|
2914
|
-
|
|
2915
|
-
const containerHome = settings.dockerContainerHome || '/root';
|
|
2916
|
-
const mounts = settings.dockerMounts || [];
|
|
2917
|
-
const envPassthrough = settings.dockerEnvPassthrough || [];
|
|
2918
|
-
|
|
2919
|
-
// Count presets vs custom mounts
|
|
2920
|
-
const presets = mounts.filter((m) => typeof m === 'string');
|
|
2921
|
-
const customMounts = mounts.filter((m) => typeof m === 'object');
|
|
2922
|
-
|
|
2923
|
-
// Mounts header with count
|
|
2924
|
-
const mountLabel =
|
|
2925
|
-
customMounts.length > 0
|
|
2926
|
-
? `Mounts (${presets.length} presets, ${customMounts.length} custom):`
|
|
2927
|
-
: `Mounts (${presets.length} presets):`;
|
|
2928
|
-
console.log(chalk.dim(` ${mountLabel}`));
|
|
2929
|
-
|
|
2930
|
-
// Display each mount as a formatted row
|
|
2931
|
-
for (const mount of mounts) {
|
|
2932
|
-
if (typeof mount === 'string') {
|
|
2933
|
-
const preset = MOUNT_PRESETS[mount];
|
|
2934
|
-
if (preset) {
|
|
2935
|
-
const container = preset.container.replace(/\$HOME/g, containerHome);
|
|
2936
|
-
const rwFlag = preset.readonly ? chalk.dim('ro') : chalk.green('rw');
|
|
2937
|
-
console.log(
|
|
2938
|
-
` ${chalk.cyan(mount.padEnd(10))} ${chalk.dim(preset.host.padEnd(20))} → ${container.padEnd(24)} (${rwFlag})`
|
|
2939
|
-
);
|
|
2940
|
-
} else {
|
|
2941
|
-
console.log(` ${chalk.red(mount.padEnd(10))} ${chalk.red('(unknown preset)')}`);
|
|
2942
|
-
}
|
|
2943
|
-
} else {
|
|
2944
|
-
// Custom mount object
|
|
2945
|
-
const container = mount.container.replace(/\$HOME/g, containerHome);
|
|
2946
|
-
const rwFlag = mount.readonly !== false ? chalk.dim('ro') : chalk.green('rw');
|
|
2947
|
-
console.log(
|
|
2948
|
-
` ${chalk.yellow('custom'.padEnd(10))} ${chalk.dim(mount.host.padEnd(20))} → ${container.padEnd(24)} (${rwFlag})`
|
|
2949
|
-
);
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
if (mounts.length === 0) {
|
|
2954
|
-
console.log(chalk.dim(' (none)'));
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
// Environment variables (resolved from presets + explicit)
|
|
2958
|
-
const resolvedEnvs = resolveEnvs(mounts, envPassthrough);
|
|
2959
|
-
if (resolvedEnvs.length > 0) {
|
|
2960
|
-
console.log(chalk.dim(` Environment (${resolvedEnvs.length} vars):`));
|
|
2961
|
-
// Group by source for clarity
|
|
2962
|
-
const fromPresets = resolvedEnvs.filter((e) => !envPassthrough.includes(e));
|
|
2963
|
-
const explicit = envPassthrough;
|
|
2964
|
-
|
|
2965
|
-
if (fromPresets.length > 0) {
|
|
2966
|
-
console.log(` ${chalk.dim('From presets:')} ${fromPresets.join(', ')}`);
|
|
2967
|
-
}
|
|
2968
|
-
if (explicit.length > 0) {
|
|
2969
|
-
console.log(` ${chalk.cyan('Explicit:')} ${explicit.join(', ')}`);
|
|
2970
|
-
}
|
|
2971
|
-
} else {
|
|
2972
|
-
console.log(chalk.dim(' Environment: (none)'));
|
|
2973
|
-
}
|
|
2974
|
-
|
|
2975
|
-
// Container home
|
|
2976
|
-
const homeIsDefault = containerHome === '/root';
|
|
2977
|
-
const homeLabel = homeIsDefault ? chalk.dim('Container home:') : chalk.cyan('Container home:');
|
|
2978
|
-
const homeVal = homeIsDefault ? chalk.dim(containerHome) : chalk.white(containerHome);
|
|
2979
|
-
console.log(` ${homeLabel} ${homeVal}`);
|
|
2980
|
-
|
|
2981
|
-
console.log('');
|
|
2982
|
-
|
|
2983
|
-
if (showUsage) {
|
|
2984
|
-
console.log(chalk.dim('Usage:'));
|
|
2985
|
-
console.log(chalk.dim(' zeroshot settings set <key> <value>'));
|
|
2986
|
-
console.log(chalk.dim(' zeroshot settings get <key>'));
|
|
2987
|
-
console.log(chalk.dim(' zeroshot settings reset'));
|
|
2988
|
-
console.log('');
|
|
2989
|
-
console.log(chalk.dim('Examples:'));
|
|
2990
|
-
console.log(chalk.dim(' zeroshot settings set maxModel opus'));
|
|
2991
|
-
console.log(chalk.dim(' zeroshot settings set dockerMounts \'["gh","git","ssh","aws"]\''));
|
|
2992
|
-
console.log(chalk.dim(' zeroshot settings set dockerEnvPassthrough \'["AWS_*","TF_VAR_*"]\''));
|
|
2993
|
-
console.log('');
|
|
2994
|
-
console.log(
|
|
2995
|
-
chalk.dim(
|
|
2996
|
-
'Available mount presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini'
|
|
2997
|
-
)
|
|
2998
|
-
);
|
|
2999
|
-
console.log('');
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
settingsCmd
|
|
3004
|
-
.command('list')
|
|
3005
|
-
.description('Show all settings')
|
|
3006
|
-
.action(() => {
|
|
3007
|
-
const settings = loadSettings();
|
|
3008
|
-
formatSettingsList(settings, false);
|
|
3009
|
-
});
|
|
3010
|
-
|
|
3011
|
-
settingsCmd
|
|
3012
|
-
.command('get <key>')
|
|
3013
|
-
.description('Get a setting value')
|
|
3014
|
-
.action((key) => {
|
|
3015
|
-
const settings = loadSettings();
|
|
3016
|
-
if (!(key in settings)) {
|
|
3017
|
-
console.error(chalk.red(`Unknown setting: ${key}`));
|
|
3018
|
-
console.log(chalk.dim('\nAvailable settings:'));
|
|
3019
|
-
Object.keys(DEFAULT_SETTINGS).forEach((k) => console.log(chalk.dim(` - ${k}`)));
|
|
3020
|
-
process.exit(1);
|
|
3021
|
-
}
|
|
3022
|
-
console.log(settings[key]);
|
|
3023
|
-
});
|
|
3024
|
-
|
|
3025
|
-
settingsCmd
|
|
3026
|
-
.command('set <key> <value>')
|
|
3027
|
-
.description('Set a setting value')
|
|
3028
|
-
.action((key, value) => {
|
|
3029
|
-
if (!(key in DEFAULT_SETTINGS)) {
|
|
3030
|
-
console.error(chalk.red(`Unknown setting: ${key}`));
|
|
3031
|
-
console.log(chalk.dim('\nAvailable settings:'));
|
|
3032
|
-
Object.keys(DEFAULT_SETTINGS).forEach((k) => console.log(chalk.dim(` - ${k}`)));
|
|
3033
|
-
process.exit(1);
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
const settings = loadSettings();
|
|
3037
|
-
|
|
3038
|
-
// Type coercion
|
|
3039
|
-
let parsedValue;
|
|
3040
|
-
try {
|
|
3041
|
-
parsedValue = coerceValue(key, value);
|
|
3042
|
-
} catch (error) {
|
|
3043
|
-
console.error(chalk.red(error.message));
|
|
3044
|
-
process.exit(1);
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
|
-
// Validation
|
|
3048
|
-
const validationError = validateSetting(key, parsedValue);
|
|
3049
|
-
if (validationError) {
|
|
3050
|
-
console.error(chalk.red(validationError));
|
|
3051
|
-
process.exit(1);
|
|
3052
|
-
}
|
|
3053
|
-
|
|
3054
|
-
settings[key] = parsedValue;
|
|
3055
|
-
saveSettings(settings);
|
|
3056
|
-
console.log(chalk.green(`✓ Set ${key} = ${parsedValue}`));
|
|
3057
|
-
});
|
|
3058
|
-
|
|
3059
|
-
settingsCmd
|
|
3060
|
-
.command('reset')
|
|
3061
|
-
.description('Reset all settings to defaults')
|
|
3062
|
-
.option('-y, --yes', 'Skip confirmation')
|
|
3063
|
-
.action((options) => {
|
|
3064
|
-
if (!options.yes) {
|
|
3065
|
-
const readline = require('readline');
|
|
3066
|
-
const rl = readline.createInterface({
|
|
3067
|
-
input: process.stdin,
|
|
3068
|
-
output: process.stdout,
|
|
3069
|
-
});
|
|
3070
|
-
|
|
3071
|
-
rl.question(chalk.yellow('Reset all settings to defaults? [y/N] '), (answer) => {
|
|
3072
|
-
rl.close();
|
|
3073
|
-
if (answer.toLowerCase() !== 'y') {
|
|
3074
|
-
console.log('Aborted.');
|
|
3075
|
-
return;
|
|
3076
|
-
}
|
|
3077
|
-
saveSettings(DEFAULT_SETTINGS);
|
|
3078
|
-
console.log(chalk.green('✓ Settings reset to defaults'));
|
|
3079
|
-
});
|
|
3080
|
-
} else {
|
|
3081
|
-
saveSettings(DEFAULT_SETTINGS);
|
|
3082
|
-
console.log(chalk.green('✓ Settings reset to defaults'));
|
|
3083
|
-
}
|
|
3084
|
-
});
|
|
3085
|
-
|
|
3086
|
-
// Add alias for settings list (just `zeroshot settings`)
|
|
3087
|
-
settingsCmd.action(() => {
|
|
3088
|
-
const settings = loadSettings();
|
|
3089
|
-
formatSettingsList(settings, true);
|
|
3090
|
-
});
|
|
3091
|
-
|
|
3092
|
-
// Providers management
|
|
3093
|
-
const providersCmd = program.command('providers').description('Manage AI providers');
|
|
3094
|
-
providersCmd.action(async () => {
|
|
3095
|
-
await providersCommand();
|
|
3096
|
-
});
|
|
3097
|
-
|
|
3098
|
-
providersCmd
|
|
3099
|
-
.command('set-default <provider>')
|
|
3100
|
-
.description('Set default provider (claude, codex, gemini)')
|
|
3101
|
-
.action(async (provider) => {
|
|
3102
|
-
await setDefaultCommand([provider]);
|
|
3103
|
-
});
|
|
3104
|
-
|
|
3105
|
-
providersCmd
|
|
3106
|
-
.command('setup <provider>')
|
|
3107
|
-
.description('Configure provider model levels and overrides')
|
|
3108
|
-
.action(async (provider) => {
|
|
3109
|
-
await setupCommand([provider]);
|
|
3110
|
-
});
|
|
3111
|
-
|
|
3112
|
-
// Update command
|
|
3113
|
-
program
|
|
3114
|
-
.command('update')
|
|
3115
|
-
.description('Update zeroshot to the latest version')
|
|
3116
|
-
.option('--check', 'Check for updates without installing')
|
|
3117
|
-
.action(async (options) => {
|
|
3118
|
-
const {
|
|
3119
|
-
getCurrentVersion,
|
|
3120
|
-
fetchLatestVersion,
|
|
3121
|
-
isNewerVersion,
|
|
3122
|
-
runUpdate,
|
|
3123
|
-
} = require('./lib/update-checker');
|
|
3124
|
-
|
|
3125
|
-
const currentVersion = getCurrentVersion();
|
|
3126
|
-
console.log(chalk.dim(`Current version: ${currentVersion}`));
|
|
3127
|
-
console.log(chalk.dim('Checking for updates...'));
|
|
3128
|
-
|
|
3129
|
-
const latestVersion = await fetchLatestVersion();
|
|
3130
|
-
|
|
3131
|
-
if (!latestVersion) {
|
|
3132
|
-
console.error(chalk.red('Failed to check for updates. Check your internet connection.'));
|
|
3133
|
-
process.exit(1);
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3136
|
-
console.log(chalk.dim(`Latest version: ${latestVersion}`));
|
|
3137
|
-
|
|
3138
|
-
if (!isNewerVersion(currentVersion, latestVersion)) {
|
|
3139
|
-
console.log(chalk.green('\n✓ You are already on the latest version!'));
|
|
3140
|
-
return;
|
|
3141
|
-
}
|
|
3142
|
-
|
|
3143
|
-
console.log(chalk.yellow(`\n📦 Update available: ${currentVersion} → ${latestVersion}`));
|
|
3144
|
-
|
|
3145
|
-
if (options.check) {
|
|
3146
|
-
console.log(chalk.dim('\nRun `zeroshot update` to install the update.'));
|
|
3147
|
-
return;
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
const success = await runUpdate();
|
|
3151
|
-
process.exit(success ? 0 : 1);
|
|
3152
|
-
});
|
|
3153
|
-
|
|
3154
|
-
// Config visualization commands
|
|
3155
|
-
const configCmd = program.command('config').description('Manage and visualize cluster configs');
|
|
3156
|
-
|
|
3157
|
-
configCmd
|
|
3158
|
-
.command('list')
|
|
3159
|
-
.description('List available cluster configs')
|
|
3160
|
-
.action(() => {
|
|
3161
|
-
try {
|
|
3162
|
-
const configsDir = path.join(PACKAGE_ROOT, 'cluster-templates');
|
|
3163
|
-
const files = fs.readdirSync(configsDir).filter((f) => f.endsWith('.json'));
|
|
3164
|
-
|
|
3165
|
-
if (files.length === 0) {
|
|
3166
|
-
console.log(chalk.dim('No configs found in examples/'));
|
|
3167
|
-
return;
|
|
3168
|
-
}
|
|
3169
|
-
|
|
3170
|
-
console.log(chalk.bold('\nAvailable configs:\n'));
|
|
3171
|
-
for (const file of files) {
|
|
3172
|
-
const configPath = path.join(configsDir, file);
|
|
3173
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
3174
|
-
const agentCount = config.agents?.length || 0;
|
|
3175
|
-
const name = file.replace('.json', '');
|
|
3176
|
-
|
|
3177
|
-
console.log(` ${chalk.cyan(name.padEnd(30))} ${chalk.dim(`${agentCount} agents`)}`);
|
|
3178
|
-
}
|
|
3179
|
-
console.log('');
|
|
3180
|
-
} catch (error) {
|
|
3181
|
-
console.error('Error listing configs:', error.message);
|
|
3182
|
-
process.exit(1);
|
|
3183
|
-
}
|
|
3184
|
-
});
|
|
3185
|
-
|
|
3186
|
-
configCmd
|
|
3187
|
-
.command('show <name>')
|
|
3188
|
-
.description('Visualize a cluster config')
|
|
3189
|
-
.action((name) => {
|
|
3190
|
-
try {
|
|
3191
|
-
// Support both with and without .json extension
|
|
3192
|
-
const configName = name.endsWith('.json') ? name : `${name}.json`;
|
|
3193
|
-
const configPath = path.join(PACKAGE_ROOT, 'cluster-templates', configName);
|
|
3194
|
-
|
|
3195
|
-
if (!fs.existsSync(configPath)) {
|
|
3196
|
-
console.error(chalk.red(`Config not found: ${configName}`));
|
|
3197
|
-
console.log(chalk.dim('\nAvailable configs:'));
|
|
3198
|
-
const files = fs
|
|
3199
|
-
.readdirSync(path.join(PACKAGE_ROOT, 'cluster-templates'))
|
|
3200
|
-
.filter((f) => f.endsWith('.json'));
|
|
3201
|
-
files.forEach((f) => console.log(chalk.dim(` - ${f.replace('.json', '')}`)));
|
|
3202
|
-
process.exit(1);
|
|
3203
|
-
}
|
|
3204
|
-
|
|
3205
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
3206
|
-
|
|
3207
|
-
// Header
|
|
3208
|
-
console.log('');
|
|
3209
|
-
console.log(chalk.bold.cyan('═'.repeat(80)));
|
|
3210
|
-
console.log(chalk.bold.cyan(` Config: ${name.replace('.json', '')}`));
|
|
3211
|
-
console.log(chalk.bold.cyan('═'.repeat(80)));
|
|
3212
|
-
console.log('');
|
|
3213
|
-
|
|
3214
|
-
// Agents section
|
|
3215
|
-
console.log(chalk.bold('Agents:\n'));
|
|
3216
|
-
|
|
3217
|
-
if (!config.agents || config.agents.length === 0) {
|
|
3218
|
-
console.log(chalk.dim(' No agents defined'));
|
|
3219
|
-
} else {
|
|
3220
|
-
for (const agent of config.agents) {
|
|
3221
|
-
const color = getColorForSender(agent.id);
|
|
3222
|
-
console.log(color.bold(` ${agent.id}`));
|
|
3223
|
-
console.log(chalk.dim(` Role: ${agent.role || 'none'}`));
|
|
3224
|
-
|
|
3225
|
-
if (agent.model) {
|
|
3226
|
-
console.log(chalk.dim(` Model: ${agent.model}`));
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
if (agent.triggers && agent.triggers.length > 0) {
|
|
3230
|
-
// Triggers are objects with topic field
|
|
3231
|
-
const triggerTopics = agent.triggers
|
|
3232
|
-
.map((t) => (typeof t === 'string' ? t : t.topic))
|
|
3233
|
-
.filter(Boolean);
|
|
3234
|
-
console.log(chalk.dim(` Triggers: ${triggerTopics.join(', ')}`));
|
|
3235
|
-
} else {
|
|
3236
|
-
console.log(chalk.dim(` Triggers: none (manual only)`));
|
|
3237
|
-
}
|
|
3238
|
-
|
|
3239
|
-
console.log('');
|
|
3240
|
-
}
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3243
|
-
// Message flow visualization
|
|
3244
|
-
if (config.agents && config.agents.length > 0) {
|
|
3245
|
-
console.log(chalk.bold('Message Flow:\n'));
|
|
3246
|
-
|
|
3247
|
-
// Build trigger map: topic -> [agents that listen]
|
|
3248
|
-
const triggerMap = new Map();
|
|
3249
|
-
for (const agent of config.agents) {
|
|
3250
|
-
if (agent.triggers) {
|
|
3251
|
-
for (const trigger of agent.triggers) {
|
|
3252
|
-
const topic = typeof trigger === 'string' ? trigger : trigger.topic;
|
|
3253
|
-
if (topic) {
|
|
3254
|
-
if (!triggerMap.has(topic)) {
|
|
3255
|
-
triggerMap.set(topic, []);
|
|
3256
|
-
}
|
|
3257
|
-
triggerMap.get(topic).push(agent.id);
|
|
3258
|
-
}
|
|
3259
|
-
}
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
if (triggerMap.size === 0) {
|
|
3264
|
-
console.log(chalk.dim(' No automatic triggers defined\n'));
|
|
3265
|
-
} else {
|
|
3266
|
-
for (const [topic, agents] of triggerMap) {
|
|
3267
|
-
console.log(
|
|
3268
|
-
` ${chalk.yellow(topic)} ${chalk.dim('→')} ${agents.map((a) => getColorForSender(a)(a)).join(', ')}`
|
|
3269
|
-
);
|
|
3270
|
-
}
|
|
3271
|
-
console.log('');
|
|
3272
|
-
}
|
|
3273
|
-
}
|
|
3274
|
-
|
|
3275
|
-
console.log(chalk.bold.cyan('═'.repeat(80)));
|
|
3276
|
-
console.log('');
|
|
3277
|
-
} catch (error) {
|
|
3278
|
-
console.error('Error showing config:', error.message);
|
|
3279
|
-
process.exit(1);
|
|
3280
|
-
}
|
|
3281
|
-
});
|
|
3282
|
-
|
|
3283
|
-
configCmd
|
|
3284
|
-
.command('validate <configPath>')
|
|
3285
|
-
.description('Validate a cluster config for structural issues')
|
|
3286
|
-
.option('--strict', 'Treat warnings as errors')
|
|
3287
|
-
.option('--json', 'Output as JSON')
|
|
3288
|
-
.action((configPath, options) => {
|
|
3289
|
-
try {
|
|
3290
|
-
const { validateConfig, formatValidationResult } = require('../src/config-validator');
|
|
3291
|
-
|
|
3292
|
-
// Resolve path (support relative paths and built-in names)
|
|
3293
|
-
let fullPath;
|
|
3294
|
-
if (fs.existsSync(configPath)) {
|
|
3295
|
-
fullPath = path.resolve(configPath);
|
|
3296
|
-
} else {
|
|
3297
|
-
// Try examples directory
|
|
3298
|
-
const configName = configPath.endsWith('.json') ? configPath : `${configPath}.json`;
|
|
3299
|
-
fullPath = path.join(PACKAGE_ROOT, 'cluster-templates', configName);
|
|
3300
|
-
if (!fs.existsSync(fullPath)) {
|
|
3301
|
-
console.error(chalk.red(`Config not found: ${configPath}`));
|
|
3302
|
-
console.log(chalk.dim('\nAvailable built-in configs:'));
|
|
3303
|
-
const files = fs
|
|
3304
|
-
.readdirSync(path.join(PACKAGE_ROOT, 'cluster-templates'))
|
|
3305
|
-
.filter((f) => f.endsWith('.json'));
|
|
3306
|
-
files.forEach((f) => console.log(chalk.dim(` - ${f.replace('.json', '')}`)));
|
|
3307
|
-
process.exit(1);
|
|
3308
|
-
}
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
// Load and validate
|
|
3312
|
-
const config = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
3313
|
-
const result = validateConfig(config);
|
|
3314
|
-
|
|
3315
|
-
// Apply strict mode
|
|
3316
|
-
if (options.strict && result.warnings.length > 0) {
|
|
3317
|
-
result.errors.push(...result.warnings.map((w) => `[strict] ${w}`));
|
|
3318
|
-
result.valid = false;
|
|
3319
|
-
}
|
|
3320
|
-
|
|
3321
|
-
// Output
|
|
3322
|
-
if (options.json) {
|
|
3323
|
-
console.log(JSON.stringify(result, null, 2));
|
|
3324
|
-
} else {
|
|
3325
|
-
console.log('');
|
|
3326
|
-
console.log(chalk.bold(`Validating: ${path.basename(fullPath)}`));
|
|
3327
|
-
console.log('');
|
|
3328
|
-
console.log(formatValidationResult(result));
|
|
3329
|
-
console.log('');
|
|
3330
|
-
}
|
|
3331
|
-
|
|
3332
|
-
// Exit code
|
|
3333
|
-
process.exit(result.valid ? 0 : 1);
|
|
3334
|
-
} catch (error) {
|
|
3335
|
-
if (error instanceof SyntaxError) {
|
|
3336
|
-
console.error(chalk.red(`Invalid JSON: ${error.message}`));
|
|
3337
|
-
} else {
|
|
3338
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
3339
|
-
}
|
|
3340
|
-
process.exit(1);
|
|
3341
|
-
}
|
|
3342
|
-
});
|
|
3343
|
-
|
|
3344
|
-
// Agent library commands
|
|
3345
|
-
const agentsCmd = program.command('agents').description('View available agent definitions');
|
|
3346
|
-
|
|
3347
|
-
agentsCmd
|
|
3348
|
-
.command('list')
|
|
3349
|
-
.alias('ls')
|
|
3350
|
-
.description('List available agent definitions')
|
|
3351
|
-
.option('--verbose', 'Show full agent details')
|
|
3352
|
-
.option('--json', 'Output as JSON')
|
|
3353
|
-
.action((options) => {
|
|
3354
|
-
try {
|
|
3355
|
-
const agentsDir = path.join(PACKAGE_ROOT, 'src', 'agents');
|
|
3356
|
-
|
|
3357
|
-
// Check if agents directory exists
|
|
3358
|
-
if (!fs.existsSync(agentsDir)) {
|
|
3359
|
-
if (options.json) {
|
|
3360
|
-
console.log(JSON.stringify({ agents: [], error: null }, null, 2));
|
|
3361
|
-
} else {
|
|
3362
|
-
console.log(chalk.dim('No agents directory found.'));
|
|
3363
|
-
}
|
|
3364
|
-
return;
|
|
3365
|
-
}
|
|
3366
|
-
|
|
3367
|
-
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.json'));
|
|
3368
|
-
|
|
3369
|
-
if (files.length === 0) {
|
|
3370
|
-
if (options.json) {
|
|
3371
|
-
console.log(JSON.stringify({ agents: [], error: null }, null, 2));
|
|
3372
|
-
} else {
|
|
3373
|
-
console.log(chalk.dim('No agent definitions found in src/agents/'));
|
|
3374
|
-
}
|
|
3375
|
-
return;
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
// Parse all agent files
|
|
3379
|
-
const agents = [];
|
|
3380
|
-
for (const file of files) {
|
|
3381
|
-
try {
|
|
3382
|
-
const agentPath = path.join(agentsDir, file);
|
|
3383
|
-
const agent = JSON.parse(fs.readFileSync(agentPath, 'utf8'));
|
|
3384
|
-
agents.push({
|
|
3385
|
-
file: file.replace('.json', ''),
|
|
3386
|
-
id: agent.id || file.replace('.json', ''),
|
|
3387
|
-
role: agent.role || 'unspecified',
|
|
3388
|
-
model: agent.model || 'default',
|
|
3389
|
-
triggers: agent.triggers?.length || 0,
|
|
3390
|
-
prompt: agent.prompt || null,
|
|
3391
|
-
output: agent.output || null,
|
|
3392
|
-
});
|
|
3393
|
-
} catch (err) {
|
|
3394
|
-
// Skip invalid JSON files
|
|
3395
|
-
console.error(chalk.yellow(`Warning: Could not parse ${file}: ${err.message}`));
|
|
3396
|
-
}
|
|
3397
|
-
}
|
|
3398
|
-
|
|
3399
|
-
// JSON output
|
|
3400
|
-
if (options.json) {
|
|
3401
|
-
console.log(JSON.stringify({ agents, error: null }, null, 2));
|
|
3402
|
-
return;
|
|
3403
|
-
}
|
|
3404
|
-
|
|
3405
|
-
// Human-readable output
|
|
3406
|
-
console.log(chalk.bold('\nAvailable agent definitions:\n'));
|
|
3407
|
-
|
|
3408
|
-
for (const agent of agents) {
|
|
3409
|
-
console.log(
|
|
3410
|
-
` ${chalk.cyan(agent.id.padEnd(25))} ${chalk.dim('role:')} ${agent.role.padEnd(20)} ${chalk.dim('model:')} ${agent.model}`
|
|
3411
|
-
);
|
|
3412
|
-
|
|
3413
|
-
if (options.verbose) {
|
|
3414
|
-
console.log(chalk.dim(` Triggers: ${agent.triggers}`));
|
|
3415
|
-
if (agent.output) {
|
|
3416
|
-
console.log(chalk.dim(` Output topic: ${agent.output.topic || 'none'}`));
|
|
3417
|
-
}
|
|
3418
|
-
if (agent.prompt) {
|
|
3419
|
-
const promptPreview = agent.prompt.substring(0, 100).replace(/\n/g, ' ');
|
|
3420
|
-
console.log(chalk.dim(` Prompt: ${promptPreview}...`));
|
|
3421
|
-
}
|
|
3422
|
-
console.log('');
|
|
3423
|
-
}
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
if (!options.verbose) {
|
|
3427
|
-
console.log('');
|
|
3428
|
-
console.log(chalk.dim(' Use --verbose for full details'));
|
|
3429
|
-
}
|
|
3430
|
-
console.log('');
|
|
3431
|
-
} catch (error) {
|
|
3432
|
-
if (options.json) {
|
|
3433
|
-
console.log(JSON.stringify({ agents: [], error: error.message }, null, 2));
|
|
3434
|
-
} else {
|
|
3435
|
-
console.error(chalk.red(`Error listing agents: ${error.message}`));
|
|
3436
|
-
}
|
|
3437
|
-
process.exit(1);
|
|
3438
|
-
}
|
|
3439
|
-
});
|
|
3440
|
-
|
|
3441
|
-
agentsCmd
|
|
3442
|
-
.command('show <name>')
|
|
3443
|
-
.description('Show detailed agent definition')
|
|
3444
|
-
.option('--json', 'Output as JSON')
|
|
3445
|
-
.action((name, options) => {
|
|
3446
|
-
try {
|
|
3447
|
-
const agentsDir = path.join(PACKAGE_ROOT, 'src', 'agents');
|
|
3448
|
-
|
|
3449
|
-
// Support both with and without .json extension
|
|
3450
|
-
const agentName = name.endsWith('.json') ? name : `${name}.json`;
|
|
3451
|
-
const agentPath = path.join(agentsDir, agentName);
|
|
3452
|
-
|
|
3453
|
-
if (!fs.existsSync(agentPath)) {
|
|
3454
|
-
// Try with -agent.json suffix
|
|
3455
|
-
const altPath = path.join(agentsDir, `${name}-agent.json`);
|
|
3456
|
-
if (fs.existsSync(altPath)) {
|
|
3457
|
-
const agent = JSON.parse(fs.readFileSync(altPath, 'utf8'));
|
|
3458
|
-
outputAgent(agent, options);
|
|
3459
|
-
return;
|
|
3460
|
-
}
|
|
3461
|
-
|
|
3462
|
-
if (options.json) {
|
|
3463
|
-
console.log(JSON.stringify({ error: `Agent not found: ${name}` }, null, 2));
|
|
3464
|
-
} else {
|
|
3465
|
-
console.error(chalk.red(`Agent not found: ${name}`));
|
|
3466
|
-
console.log(chalk.dim('\nAvailable agents:'));
|
|
3467
|
-
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.json'));
|
|
3468
|
-
files.forEach((f) => console.log(chalk.dim(` - ${f.replace('.json', '')}`)));
|
|
3469
|
-
}
|
|
3470
|
-
process.exit(1);
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
const agent = JSON.parse(fs.readFileSync(agentPath, 'utf8'));
|
|
3474
|
-
outputAgent(agent, options);
|
|
3475
|
-
} catch (error) {
|
|
3476
|
-
if (options.json) {
|
|
3477
|
-
console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
3478
|
-
} else {
|
|
3479
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
3480
|
-
}
|
|
3481
|
-
process.exit(1);
|
|
3482
|
-
}
|
|
3483
|
-
});
|
|
3484
|
-
|
|
3485
|
-
function outputAgent(agent, options) {
|
|
3486
|
-
if (options.json) {
|
|
3487
|
-
console.log(JSON.stringify(agent, null, 2));
|
|
3488
|
-
return;
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
// Human-readable output
|
|
3492
|
-
console.log('');
|
|
3493
|
-
console.log(chalk.bold.cyan('═'.repeat(80)));
|
|
3494
|
-
console.log(chalk.bold.cyan(` Agent: ${agent.id}`));
|
|
3495
|
-
console.log(chalk.bold.cyan('═'.repeat(80)));
|
|
3496
|
-
console.log('');
|
|
3497
|
-
|
|
3498
|
-
// Basic info
|
|
3499
|
-
console.log(chalk.bold('Configuration:'));
|
|
3500
|
-
console.log(` ${chalk.dim('ID:')} ${agent.id}`);
|
|
3501
|
-
console.log(` ${chalk.dim('Role:')} ${agent.role || 'unspecified'}`);
|
|
3502
|
-
console.log(` ${chalk.dim('Model:')} ${agent.model || 'default'}`);
|
|
3503
|
-
console.log('');
|
|
3504
|
-
|
|
3505
|
-
// Triggers
|
|
3506
|
-
if (agent.triggers && agent.triggers.length > 0) {
|
|
3507
|
-
console.log(chalk.bold('Triggers:'));
|
|
3508
|
-
for (const trigger of agent.triggers) {
|
|
3509
|
-
console.log(` ${chalk.yellow('•')} Topic: ${chalk.cyan(trigger.topic)}`);
|
|
3510
|
-
if (trigger.action) {
|
|
3511
|
-
console.log(` Action: ${trigger.action}`);
|
|
3512
|
-
}
|
|
3513
|
-
if (trigger.logic?.script) {
|
|
3514
|
-
const scriptPreview = trigger.logic.script.substring(0, 80).replace(/\n/g, ' ');
|
|
3515
|
-
console.log(chalk.dim(` Logic: ${scriptPreview}...`));
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
console.log('');
|
|
3519
|
-
}
|
|
3520
|
-
|
|
3521
|
-
// Output
|
|
3522
|
-
if (agent.output) {
|
|
3523
|
-
console.log(chalk.bold('Output:'));
|
|
3524
|
-
console.log(` ${chalk.dim('Topic:')} ${agent.output.topic || 'none'}`);
|
|
3525
|
-
if (agent.output.publishAfter) {
|
|
3526
|
-
console.log(` ${chalk.dim('Publish after:')} ${agent.output.publishAfter}`);
|
|
3527
|
-
}
|
|
3528
|
-
console.log('');
|
|
3529
|
-
}
|
|
3530
|
-
|
|
3531
|
-
// Prompt
|
|
3532
|
-
if (agent.prompt) {
|
|
3533
|
-
console.log(chalk.bold('Prompt:'));
|
|
3534
|
-
console.log(chalk.dim('─'.repeat(76)));
|
|
3535
|
-
// Show first 500 chars of prompt
|
|
3536
|
-
const promptLines = agent.prompt.substring(0, 500).split('\n');
|
|
3537
|
-
for (const line of promptLines) {
|
|
3538
|
-
console.log(` ${line}`);
|
|
3539
|
-
}
|
|
3540
|
-
if (agent.prompt.length > 500) {
|
|
3541
|
-
console.log(chalk.dim(` ... (${agent.prompt.length - 500} more characters)`));
|
|
3542
|
-
}
|
|
3543
|
-
console.log(chalk.dim('─'.repeat(76)));
|
|
3544
|
-
console.log('');
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3547
|
-
|
|
3548
|
-
// Helper function to keep the process alive for follow mode
|
|
3549
|
-
function keepProcessAlive(cleanupFn) {
|
|
3550
|
-
// Prevent Node.js from exiting by keeping the event loop active
|
|
3551
|
-
// Use setInterval with a long interval (1 hour) to minimize overhead
|
|
3552
|
-
const keepAliveInterval = setInterval(() => {}, 3600000);
|
|
3553
|
-
|
|
3554
|
-
// Handle graceful shutdown on Ctrl+C
|
|
3555
|
-
process.on('SIGINT', () => {
|
|
3556
|
-
clearInterval(keepAliveInterval);
|
|
3557
|
-
if (cleanupFn) cleanupFn();
|
|
3558
|
-
console.log('\n\nStopped following logs.');
|
|
3559
|
-
process.exit(0);
|
|
3560
|
-
});
|
|
3561
|
-
|
|
3562
|
-
// Also handle SIGTERM for graceful shutdown
|
|
3563
|
-
process.on('SIGTERM', () => {
|
|
3564
|
-
clearInterval(keepAliveInterval);
|
|
3565
|
-
if (cleanupFn) cleanupFn();
|
|
3566
|
-
process.exit(0);
|
|
3567
|
-
});
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
// Tool icons for different tool types
|
|
3571
|
-
function getToolIcon(toolName) {
|
|
3572
|
-
const icons = {
|
|
3573
|
-
Read: '📖',
|
|
3574
|
-
Write: '📝',
|
|
3575
|
-
Edit: '✏️',
|
|
3576
|
-
Bash: '💻',
|
|
3577
|
-
Glob: '🔍',
|
|
3578
|
-
Grep: '🔎',
|
|
3579
|
-
WebFetch: '🌐',
|
|
3580
|
-
WebSearch: '🔎',
|
|
3581
|
-
Task: '🤖',
|
|
3582
|
-
TodoWrite: '📋',
|
|
3583
|
-
AskUserQuestion: '❓',
|
|
3584
|
-
};
|
|
3585
|
-
return icons[toolName] || '🔧';
|
|
3586
|
-
}
|
|
3
|
+
const childProcess = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
3587
6
|
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
7
|
+
const LEGACY_PACKAGE_NAME = '@covibes/zeroshot';
|
|
8
|
+
const NEW_PACKAGE_NAME = '@the-open-engine/zeroshot';
|
|
9
|
+
const NEW_PACKAGE_SPEC = `${NEW_PACKAGE_NAME}@latest`;
|
|
3591
10
|
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
case 'Write':
|
|
3598
|
-
return input.file_path ? `→ ${input.file_path.split('/').slice(-2).join('/')}` : '';
|
|
3599
|
-
case 'Edit':
|
|
3600
|
-
return input.file_path ? input.file_path.split('/').slice(-2).join('/') : '';
|
|
3601
|
-
case 'Glob':
|
|
3602
|
-
return input.pattern || '';
|
|
3603
|
-
case 'Grep':
|
|
3604
|
-
return input.pattern ? `/${input.pattern}/` : '';
|
|
3605
|
-
case 'WebFetch':
|
|
3606
|
-
return input.url ? input.url.substring(0, 50) : '';
|
|
3607
|
-
case 'WebSearch':
|
|
3608
|
-
return input.query ? `"${input.query}"` : '';
|
|
3609
|
-
case 'Task':
|
|
3610
|
-
return input.description || '';
|
|
3611
|
-
case 'TodoWrite':
|
|
3612
|
-
if (input.todos && Array.isArray(input.todos)) {
|
|
3613
|
-
const statusCounts = {};
|
|
3614
|
-
input.todos.forEach((todo) => {
|
|
3615
|
-
statusCounts[todo.status] = (statusCounts[todo.status] || 0) + 1;
|
|
3616
|
-
});
|
|
3617
|
-
const parts = Object.entries(statusCounts).map(
|
|
3618
|
-
([status, count]) => `${count} ${status.replace('_', ' ')}`
|
|
3619
|
-
);
|
|
3620
|
-
return `${input.todos.length} todo${input.todos.length === 1 ? '' : 's'} (${parts.join(', ')})`;
|
|
3621
|
-
}
|
|
3622
|
-
return '';
|
|
3623
|
-
case 'AskUserQuestion':
|
|
3624
|
-
if (input.questions && Array.isArray(input.questions)) {
|
|
3625
|
-
const q = input.questions[0];
|
|
3626
|
-
const preview = q.question.substring(0, 50);
|
|
3627
|
-
return input.questions.length > 1
|
|
3628
|
-
? `${input.questions.length} questions: "${preview}..."`
|
|
3629
|
-
: `"${preview}${q.question.length > 50 ? '...' : ''}"`;
|
|
3630
|
-
}
|
|
3631
|
-
return '';
|
|
3632
|
-
default:
|
|
3633
|
-
// For unknown tools, show first key-value pair
|
|
3634
|
-
const keys = Object.keys(input);
|
|
3635
|
-
if (keys.length > 0) {
|
|
3636
|
-
const val = String(input[keys[0]]).substring(0, 40);
|
|
3637
|
-
return val.length < String(input[keys[0]]).length ? val + '...' : val;
|
|
3638
|
-
}
|
|
3639
|
-
return '';
|
|
3640
|
-
}
|
|
11
|
+
function printNotice() {
|
|
12
|
+
console.error(
|
|
13
|
+
`\n⚠️ ${LEGACY_PACKAGE_NAME} has moved to ${NEW_PACKAGE_NAME}. ` +
|
|
14
|
+
'Run `zeroshot update` to switch this installation.\n'
|
|
15
|
+
);
|
|
3641
16
|
}
|
|
3642
17
|
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
// For errors, show full message
|
|
3648
|
-
if (isError) {
|
|
3649
|
-
const firstLine = content.split('\n')[0].substring(0, 80);
|
|
3650
|
-
return chalk.red(firstLine);
|
|
3651
|
-
}
|
|
3652
|
-
|
|
3653
|
-
// For TodoWrite, show the actual todo items
|
|
3654
|
-
if (toolName === 'TodoWrite' && toolInput?.todos && Array.isArray(toolInput.todos)) {
|
|
3655
|
-
const todos = toolInput.todos;
|
|
3656
|
-
if (todos.length === 0) return chalk.dim('no todos');
|
|
3657
|
-
if (todos.length === 1) {
|
|
3658
|
-
const status =
|
|
3659
|
-
todos[0].status === 'completed' ? '✓' : todos[0].status === 'in_progress' ? '⧗' : '○';
|
|
3660
|
-
return chalk.dim(
|
|
3661
|
-
`${status} ${todos[0].content.substring(0, 50)}${todos[0].content.length > 50 ? '...' : ''}`
|
|
3662
|
-
);
|
|
3663
|
-
}
|
|
3664
|
-
// Multiple todos - show first one as preview
|
|
3665
|
-
const status =
|
|
3666
|
-
todos[0].status === 'completed' ? '✓' : todos[0].status === 'in_progress' ? '⧗' : '○';
|
|
3667
|
-
return chalk.dim(
|
|
3668
|
-
`${status} ${todos[0].content.substring(0, 40)}... (+${todos.length - 1} more)`
|
|
3669
|
-
);
|
|
18
|
+
function hasPathSuffix(parts, suffix) {
|
|
19
|
+
if (suffix.length > parts.length) {
|
|
20
|
+
return false;
|
|
3670
21
|
}
|
|
3671
22
|
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
if (lines.length === 0) return 'done';
|
|
3675
|
-
if (lines.length === 1) {
|
|
3676
|
-
const line = lines[0].substring(0, 60);
|
|
3677
|
-
return chalk.dim(line.length < lines[0].length ? line + '...' : line);
|
|
3678
|
-
}
|
|
3679
|
-
// Multiple lines - show count
|
|
3680
|
-
return chalk.dim(`${lines.length} lines`);
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
|
-
// Helper function to get deterministic color for an agent/sender based on name hash
|
|
3684
|
-
// Uses djb2 hash algorithm for good distribution across color palette
|
|
3685
|
-
// Track recently seen content to avoid duplicates
|
|
3686
|
-
const recentContentHashes = new Set();
|
|
3687
|
-
const MAX_RECENT_HASHES = 100;
|
|
3688
|
-
|
|
3689
|
-
// Track clusters that have already shown their NEW TASK header (suppress conductor re-publish)
|
|
3690
|
-
const shownNewTaskForCluster = new Set();
|
|
3691
|
-
|
|
3692
|
-
function hashContent(content) {
|
|
3693
|
-
// Simple hash for deduplication
|
|
3694
|
-
return content.substring(0, 200);
|
|
23
|
+
const start = parts.length - suffix.length;
|
|
24
|
+
return suffix.every((part, index) => parts[start + index] === part);
|
|
3695
25
|
}
|
|
3696
26
|
|
|
3697
|
-
function
|
|
3698
|
-
const
|
|
3699
|
-
|
|
3700
|
-
return true;
|
|
3701
|
-
}
|
|
3702
|
-
recentContentHashes.add(hash);
|
|
3703
|
-
// Prune old hashes
|
|
3704
|
-
if (recentContentHashes.size > MAX_RECENT_HASHES) {
|
|
3705
|
-
const arr = Array.from(recentContentHashes);
|
|
3706
|
-
recentContentHashes.clear();
|
|
3707
|
-
arr.slice(-50).forEach((h) => recentContentHashes.add(h));
|
|
3708
|
-
}
|
|
3709
|
-
return false;
|
|
27
|
+
function joinPathParts(parts) {
|
|
28
|
+
const joined = parts.join(path.sep);
|
|
29
|
+
return joined === '' ? path.parse(process.cwd()).root : joined;
|
|
3710
30
|
}
|
|
3711
31
|
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
const
|
|
3715
|
-
const issueNum = data.issue_number || data.number;
|
|
3716
|
-
const title = data.title;
|
|
3717
|
-
const url = data.url || data.html_url;
|
|
3718
|
-
|
|
3719
|
-
// Prefer: #N: Short title
|
|
3720
|
-
if (issueNum && title) {
|
|
3721
|
-
const truncatedTitle = title.length > maxLen ? title.slice(0, maxLen - 3) + '...' : title;
|
|
3722
|
-
return `#${issueNum}: ${truncatedTitle}`;
|
|
3723
|
-
}
|
|
3724
|
-
if (issueNum) return `#${issueNum}`;
|
|
3725
|
-
|
|
3726
|
-
// Extract from URL
|
|
3727
|
-
if (url) {
|
|
3728
|
-
const match = url.match(/issues\/(\d+)/);
|
|
3729
|
-
if (match) return `#${match[1]}`;
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
|
-
// Fallback: first meaningful line (for manual prompts)
|
|
3733
|
-
const text = issueOpened.content?.text || 'Task';
|
|
3734
|
-
const firstLine = text.split('\n').find((l) => l.trim() && !l.startsWith('#')) || 'Task';
|
|
3735
|
-
return firstLine.slice(0, maxLen) + (firstLine.length > maxLen ? '...' : '');
|
|
3736
|
-
}
|
|
32
|
+
function deriveInstallPrefixFromPackageRoot(packageRoot, packageName = LEGACY_PACKAGE_NAME) {
|
|
33
|
+
const parts = path.resolve(packageRoot).split(path.sep);
|
|
34
|
+
const packageParts = packageName.split('/');
|
|
3737
35
|
|
|
3738
|
-
|
|
3739
|
-
function formatTokenUsage(tokensByRole) {
|
|
3740
|
-
if (!tokensByRole || !tokensByRole._total || tokensByRole._total.count === 0) {
|
|
36
|
+
if (!hasPathSuffix(parts, packageParts)) {
|
|
3741
37
|
return null;
|
|
3742
38
|
}
|
|
3743
39
|
|
|
3744
|
-
const
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
// Format numbers with commas
|
|
3748
|
-
const fmt = (n) => n.toLocaleString();
|
|
3749
|
-
|
|
3750
|
-
// Total line
|
|
3751
|
-
const inputTokens = total.inputTokens || 0;
|
|
3752
|
-
const outputTokens = total.outputTokens || 0;
|
|
3753
|
-
const totalTokens = inputTokens + outputTokens;
|
|
3754
|
-
const cost = total.totalCostUsd || 0;
|
|
3755
|
-
|
|
3756
|
-
lines.push(
|
|
3757
|
-
chalk.dim('Tokens: ') +
|
|
3758
|
-
chalk.cyan(fmt(totalTokens)) +
|
|
3759
|
-
chalk.dim(' (') +
|
|
3760
|
-
chalk.green(fmt(inputTokens)) +
|
|
3761
|
-
chalk.dim(' in / ') +
|
|
3762
|
-
chalk.yellow(fmt(outputTokens)) +
|
|
3763
|
-
chalk.dim(' out)')
|
|
3764
|
-
);
|
|
3765
|
-
|
|
3766
|
-
// Cost line (if available)
|
|
3767
|
-
if (cost > 0) {
|
|
3768
|
-
lines.push(chalk.dim('Cost: ') + chalk.green('$' + cost.toFixed(4)));
|
|
40
|
+
const nodeModulesIndex = parts.length - packageParts.length - 1;
|
|
41
|
+
if (nodeModulesIndex < 0 || parts[nodeModulesIndex] !== 'node_modules') {
|
|
42
|
+
return null;
|
|
3769
43
|
}
|
|
3770
44
|
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
if (roles.length > 1) {
|
|
3774
|
-
const roleStats = roles
|
|
3775
|
-
.map((role) => {
|
|
3776
|
-
const r = tokensByRole[role];
|
|
3777
|
-
const roleTotal = (r.inputTokens || 0) + (r.outputTokens || 0);
|
|
3778
|
-
return `${role}: ${fmt(roleTotal)}`;
|
|
3779
|
-
})
|
|
3780
|
-
.join(chalk.dim(' | '));
|
|
3781
|
-
lines.push(chalk.dim('By role: ') + roleStats);
|
|
45
|
+
if (parts[nodeModulesIndex - 1] === 'lib') {
|
|
46
|
+
return joinPathParts(parts.slice(0, nodeModulesIndex - 1));
|
|
3782
47
|
}
|
|
3783
48
|
|
|
3784
|
-
return
|
|
3785
|
-
}
|
|
3786
|
-
|
|
3787
|
-
// Set terminal title (works in most terminals)
|
|
3788
|
-
function setTerminalTitle(title) {
|
|
3789
|
-
// ESC ] 0 ; <title> BEL
|
|
3790
|
-
process.stdout.write(`\\x1b]0;${title}\x07`);
|
|
49
|
+
return joinPathParts(parts.slice(0, nodeModulesIndex));
|
|
3791
50
|
}
|
|
3792
51
|
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
// Reset to default (empty title lets terminal use its default)
|
|
3796
|
-
process.stdout.write('\\x1b]0;\x07');
|
|
52
|
+
function getPackageRoot() {
|
|
53
|
+
return path.dirname(path.dirname(__filename));
|
|
3797
54
|
}
|
|
3798
55
|
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
// Headers: ## Header -> bold cyan
|
|
3804
|
-
if (/^#{1,3}\s/.test(formatted)) {
|
|
3805
|
-
formatted = formatted.replace(/^#{1,3}\s*/, '');
|
|
3806
|
-
return chalk.bold.cyan(formatted);
|
|
3807
|
-
}
|
|
3808
|
-
|
|
3809
|
-
// Blockquotes: > text -> dim italic with bar
|
|
3810
|
-
if (/^>\s/.test(formatted)) {
|
|
3811
|
-
formatted = formatted.replace(/^>\s*/, '');
|
|
3812
|
-
return chalk.dim('│ ') + chalk.italic(formatted);
|
|
3813
|
-
}
|
|
3814
|
-
|
|
3815
|
-
// Numbered lists: 1. item -> yellow number
|
|
3816
|
-
const numMatch = formatted.match(/^(\d+)\.\s+(.*)$/);
|
|
3817
|
-
if (numMatch) {
|
|
3818
|
-
return chalk.yellow(numMatch[1] + '.') + ' ' + formatInlineMarkdown(numMatch[2]);
|
|
3819
|
-
}
|
|
3820
|
-
|
|
3821
|
-
// Bullet lists: - item or * item -> dim bullet
|
|
3822
|
-
const bulletMatch = formatted.match(/^[-*]\s+(.*)$/);
|
|
3823
|
-
if (bulletMatch) {
|
|
3824
|
-
return chalk.dim('•') + ' ' + formatInlineMarkdown(bulletMatch[1]);
|
|
3825
|
-
}
|
|
56
|
+
function resolveNpmCommand(installPrefix) {
|
|
57
|
+
const npmName = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
58
|
+
const candidates = [];
|
|
3826
59
|
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
if (checkMatch) {
|
|
3830
|
-
const checked = checkMatch[1].toLowerCase() === 'x';
|
|
3831
|
-
const icon = checked ? chalk.green('✓') : chalk.dim('○');
|
|
3832
|
-
return icon + ' ' + formatInlineMarkdown(checkMatch[2]);
|
|
60
|
+
if (installPrefix) {
|
|
61
|
+
candidates.push(path.join(installPrefix, 'bin', npmName));
|
|
3833
62
|
}
|
|
3834
63
|
|
|
3835
|
-
|
|
3836
|
-
}
|
|
3837
|
-
|
|
3838
|
-
// Format inline markdown: **bold**, `code`
|
|
3839
|
-
function formatInlineMarkdown(text) {
|
|
3840
|
-
let result = text;
|
|
3841
|
-
|
|
3842
|
-
// Bold: **text** -> bold
|
|
3843
|
-
result = result.replace(/\*\*([^*]+)\*\*/g, (_, content) => chalk.bold(content));
|
|
3844
|
-
|
|
3845
|
-
// Inline code: `code` -> cyan dim
|
|
3846
|
-
result = result.replace(/`([^`]+)`/g, (_, content) => chalk.cyan.dim(content));
|
|
3847
|
-
|
|
3848
|
-
return result;
|
|
3849
|
-
}
|
|
3850
|
-
|
|
3851
|
-
// Line buffer per sender - tracks line state for prefix printing
|
|
3852
|
-
const lineBuffers = new Map();
|
|
3853
|
-
|
|
3854
|
-
// Track current tool call per sender - needed for matching tool results with calls
|
|
3855
|
-
const currentToolCall = new Map();
|
|
3856
|
-
|
|
3857
|
-
/**
|
|
3858
|
-
* Render messages to terminal-style output with ANSI colors (same as zeroshot logs)
|
|
3859
|
-
*/
|
|
3860
|
-
function renderMessagesToTerminal(clusterId, messages) {
|
|
3861
|
-
const lines = [];
|
|
3862
|
-
const buffers = new Map(); // Line buffers per sender
|
|
3863
|
-
const toolCalls = new Map(); // Track tool calls per sender
|
|
3864
|
-
|
|
3865
|
-
const getBuffer = (sender) => {
|
|
3866
|
-
if (!buffers.has(sender)) {
|
|
3867
|
-
buffers.set(sender, { text: '', needsPrefix: true });
|
|
3868
|
-
}
|
|
3869
|
-
return buffers.get(sender);
|
|
3870
|
-
};
|
|
3871
|
-
|
|
3872
|
-
const flushBuffer = (sender, prefix) => {
|
|
3873
|
-
const buf = buffers.get(sender);
|
|
3874
|
-
if (buf && buf.text.trim()) {
|
|
3875
|
-
const textLines = buf.text.split('\n');
|
|
3876
|
-
for (const line of textLines) {
|
|
3877
|
-
if (line.trim()) {
|
|
3878
|
-
lines.push(`${prefix} ${formatMarkdownLine(line)}`);
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
|
-
buf.text = '';
|
|
3882
|
-
buf.needsPrefix = true;
|
|
3883
|
-
}
|
|
3884
|
-
};
|
|
3885
|
-
|
|
3886
|
-
for (const msg of messages) {
|
|
3887
|
-
const timestamp = new Date(msg.timestamp).toLocaleTimeString('en-US', {
|
|
3888
|
-
hour12: false,
|
|
3889
|
-
});
|
|
3890
|
-
const color = getColorForSender(msg.sender);
|
|
3891
|
-
const prefix = color(`${msg.sender.padEnd(15)} |`);
|
|
3892
|
-
|
|
3893
|
-
// AGENT_LIFECYCLE
|
|
3894
|
-
if (msg.topic === 'AGENT_LIFECYCLE') {
|
|
3895
|
-
const data = msg.content?.data;
|
|
3896
|
-
const event = data?.event;
|
|
3897
|
-
let icon, eventText;
|
|
3898
|
-
switch (event) {
|
|
3899
|
-
case 'STARTED':
|
|
3900
|
-
icon = chalk.green('▶');
|
|
3901
|
-
const triggers = data.triggers?.join(', ') || 'none';
|
|
3902
|
-
eventText = `started (listening for: ${chalk.dim(triggers)})`;
|
|
3903
|
-
break;
|
|
3904
|
-
case 'TASK_STARTED':
|
|
3905
|
-
icon = chalk.yellow('⚡');
|
|
3906
|
-
eventText = `${chalk.cyan(data.triggeredBy)} → task #${data.iteration} (${chalk.dim(data.model)})`;
|
|
3907
|
-
break;
|
|
3908
|
-
case 'TASK_COMPLETED':
|
|
3909
|
-
icon = chalk.green('✓');
|
|
3910
|
-
eventText = `task #${data.iteration} completed`;
|
|
3911
|
-
break;
|
|
3912
|
-
default:
|
|
3913
|
-
icon = chalk.dim('•');
|
|
3914
|
-
eventText = event || 'unknown event';
|
|
3915
|
-
}
|
|
3916
|
-
lines.push(`${prefix} ${icon} ${eventText}`);
|
|
3917
|
-
continue;
|
|
3918
|
-
}
|
|
3919
|
-
|
|
3920
|
-
// ISSUE_OPENED
|
|
3921
|
-
if (msg.topic === 'ISSUE_OPENED') {
|
|
3922
|
-
lines.push('');
|
|
3923
|
-
lines.push(chalk.bold.blue('─'.repeat(80)));
|
|
3924
|
-
// Extract issue URL if present
|
|
3925
|
-
const issueData = msg.content?.data || {};
|
|
3926
|
-
const issueUrl = issueData.url || issueData.html_url;
|
|
3927
|
-
const issueTitle = issueData.title;
|
|
3928
|
-
const issueNum = issueData.issue_number || issueData.number;
|
|
3929
|
-
|
|
3930
|
-
if (issueUrl) {
|
|
3931
|
-
lines.push(
|
|
3932
|
-
`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋')} ${chalk.cyan(issueUrl)}`
|
|
3933
|
-
);
|
|
3934
|
-
if (issueTitle) {
|
|
3935
|
-
lines.push(`${prefix} ${chalk.white(issueTitle)}`);
|
|
3936
|
-
}
|
|
3937
|
-
} else if (issueNum) {
|
|
3938
|
-
lines.push(
|
|
3939
|
-
`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋')} Issue #${issueNum}`
|
|
3940
|
-
);
|
|
3941
|
-
if (issueTitle) {
|
|
3942
|
-
lines.push(`${prefix} ${chalk.white(issueTitle)}`);
|
|
3943
|
-
}
|
|
3944
|
-
} else {
|
|
3945
|
-
// Fallback: show first line of text only
|
|
3946
|
-
lines.push(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 TASK')}`);
|
|
3947
|
-
if (msg.content?.text) {
|
|
3948
|
-
const firstLine = msg.content.text
|
|
3949
|
-
.split('\n')
|
|
3950
|
-
.find((l) => l.trim() && l.trim() !== '# Manual Input');
|
|
3951
|
-
if (firstLine) {
|
|
3952
|
-
lines.push(`${prefix} ${chalk.white(firstLine.slice(0, 100))}`);
|
|
3953
|
-
}
|
|
3954
|
-
}
|
|
3955
|
-
}
|
|
3956
|
-
lines.push(chalk.bold.blue('─'.repeat(80)));
|
|
3957
|
-
continue;
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
// IMPLEMENTATION_READY
|
|
3961
|
-
if (msg.topic === 'IMPLEMENTATION_READY') {
|
|
3962
|
-
lines.push(
|
|
3963
|
-
`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow('✅ IMPLEMENTATION READY')}`
|
|
3964
|
-
);
|
|
3965
|
-
continue;
|
|
3966
|
-
}
|
|
3967
|
-
|
|
3968
|
-
// VALIDATION_RESULT
|
|
3969
|
-
if (msg.topic === 'VALIDATION_RESULT') {
|
|
3970
|
-
const data = msg.content?.data || {};
|
|
3971
|
-
const approved = data.approved === true || data.approved === 'true';
|
|
3972
|
-
const icon = approved ? chalk.green('✓ APPROVED') : chalk.red('✗ REJECTED');
|
|
3973
|
-
lines.push(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.magenta('VALIDATION_RESULT')}`);
|
|
3974
|
-
lines.push(`${prefix} ${icon} ${chalk.dim(data.summary || '')}`);
|
|
3975
|
-
if (!approved) {
|
|
3976
|
-
let issues = data.issues || data.errors;
|
|
3977
|
-
if (typeof issues === 'string') {
|
|
3978
|
-
try {
|
|
3979
|
-
issues = JSON.parse(issues);
|
|
3980
|
-
} catch {
|
|
3981
|
-
issues = [];
|
|
3982
|
-
}
|
|
3983
|
-
}
|
|
3984
|
-
if (Array.isArray(issues)) {
|
|
3985
|
-
for (const issue of issues) {
|
|
3986
|
-
lines.push(`${prefix} ${chalk.red('•')} ${issue}`);
|
|
3987
|
-
}
|
|
3988
|
-
}
|
|
3989
|
-
}
|
|
3990
|
-
continue;
|
|
3991
|
-
}
|
|
3992
|
-
|
|
3993
|
-
// PR_CREATED
|
|
3994
|
-
if (msg.topic === 'PR_CREATED') {
|
|
3995
|
-
lines.push('');
|
|
3996
|
-
lines.push(chalk.bold.green('─'.repeat(80)));
|
|
3997
|
-
lines.push(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('🔗 PR CREATED')}`);
|
|
3998
|
-
if (msg.content?.data?.pr_url) {
|
|
3999
|
-
lines.push(`${prefix} ${chalk.cyan(msg.content.data.pr_url)}`);
|
|
4000
|
-
}
|
|
4001
|
-
if (msg.content?.data?.merged) {
|
|
4002
|
-
lines.push(`${prefix} ${chalk.bold.cyan('✓ MERGED')}`);
|
|
4003
|
-
}
|
|
4004
|
-
lines.push(chalk.bold.green('─'.repeat(80)));
|
|
4005
|
-
continue;
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
// CLUSTER_COMPLETE
|
|
4009
|
-
if (msg.topic === 'CLUSTER_COMPLETE') {
|
|
4010
|
-
lines.push('');
|
|
4011
|
-
lines.push(chalk.bold.green('─'.repeat(80)));
|
|
4012
|
-
lines.push(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('✅ CLUSTER COMPLETE')}`);
|
|
4013
|
-
lines.push(chalk.bold.green('─'.repeat(80)));
|
|
4014
|
-
continue;
|
|
4015
|
-
}
|
|
4016
|
-
|
|
4017
|
-
// AGENT_ERROR
|
|
4018
|
-
if (msg.topic === 'AGENT_ERROR') {
|
|
4019
|
-
lines.push('');
|
|
4020
|
-
lines.push(chalk.bold.red('─'.repeat(80)));
|
|
4021
|
-
lines.push(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
|
|
4022
|
-
if (msg.content?.text) {
|
|
4023
|
-
lines.push(`${prefix} ${chalk.red(msg.content.text)}`);
|
|
4024
|
-
}
|
|
4025
|
-
lines.push(chalk.bold.red('─'.repeat(80)));
|
|
4026
|
-
continue;
|
|
4027
|
-
}
|
|
4028
|
-
|
|
4029
|
-
// AGENT_OUTPUT - parse streaming JSON
|
|
4030
|
-
if (msg.topic === 'AGENT_OUTPUT') {
|
|
4031
|
-
const content = msg.content?.data?.line || msg.content?.data?.chunk || msg.content?.text;
|
|
4032
|
-
if (!content || !content.trim()) continue;
|
|
4033
|
-
|
|
4034
|
-
const provider = normalizeProviderName(
|
|
4035
|
-
msg.content?.data?.provider || msg.sender_provider || 'claude'
|
|
4036
|
-
);
|
|
4037
|
-
const events = parseProviderChunk(provider, content);
|
|
4038
|
-
for (const event of events) {
|
|
4039
|
-
switch (event.type) {
|
|
4040
|
-
case 'text':
|
|
4041
|
-
const buf = getBuffer(msg.sender);
|
|
4042
|
-
buf.text += event.text;
|
|
4043
|
-
// Print complete lines
|
|
4044
|
-
while (buf.text.includes('\n')) {
|
|
4045
|
-
const idx = buf.text.indexOf('\n');
|
|
4046
|
-
const line = buf.text.slice(0, idx);
|
|
4047
|
-
buf.text = buf.text.slice(idx + 1);
|
|
4048
|
-
if (line.trim()) {
|
|
4049
|
-
lines.push(`${prefix} ${formatMarkdownLine(line)}`);
|
|
4050
|
-
}
|
|
4051
|
-
}
|
|
4052
|
-
break;
|
|
4053
|
-
case 'tool_call':
|
|
4054
|
-
flushBuffer(msg.sender, prefix);
|
|
4055
|
-
const icon = getToolIcon(event.toolName);
|
|
4056
|
-
const toolDesc = formatToolCall(event.toolName, event.input);
|
|
4057
|
-
lines.push(`${prefix} ${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
|
|
4058
|
-
toolCalls.set(msg.sender, {
|
|
4059
|
-
toolName: event.toolName,
|
|
4060
|
-
input: event.input,
|
|
4061
|
-
});
|
|
4062
|
-
break;
|
|
4063
|
-
case 'tool_result':
|
|
4064
|
-
const status = event.isError ? chalk.red('✗') : chalk.green('✓');
|
|
4065
|
-
const tc = toolCalls.get(msg.sender);
|
|
4066
|
-
const resultDesc = formatToolResult(
|
|
4067
|
-
event.content,
|
|
4068
|
-
event.isError,
|
|
4069
|
-
tc?.toolName,
|
|
4070
|
-
tc?.input
|
|
4071
|
-
);
|
|
4072
|
-
lines.push(`${prefix} ${status} ${resultDesc}`);
|
|
4073
|
-
toolCalls.delete(msg.sender);
|
|
4074
|
-
break;
|
|
4075
|
-
}
|
|
4076
|
-
}
|
|
4077
|
-
continue;
|
|
4078
|
-
}
|
|
4079
|
-
|
|
4080
|
-
// Other topics - show topic name
|
|
4081
|
-
if (msg.topic && !['AGENT_OUTPUT', 'AGENT_LIFECYCLE'].includes(msg.topic)) {
|
|
4082
|
-
lines.push(`${prefix} ${chalk.gray(timestamp)} ${chalk.yellow(msg.topic)}`);
|
|
4083
|
-
}
|
|
4084
|
-
}
|
|
64
|
+
candidates.push(path.join(path.dirname(process.execPath), npmName));
|
|
4085
65
|
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
const color = getColorForSender(sender);
|
|
4090
|
-
const prefix = color(`${sender.padEnd(15)} |`);
|
|
4091
|
-
for (const line of buf.text.split('\n')) {
|
|
4092
|
-
if (line.trim()) {
|
|
4093
|
-
lines.push(`${prefix} ${line}`);
|
|
4094
|
-
}
|
|
4095
|
-
}
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
if (fs.existsSync(candidate)) {
|
|
68
|
+
return candidate;
|
|
4096
69
|
}
|
|
4097
70
|
}
|
|
4098
71
|
|
|
4099
|
-
return
|
|
4100
|
-
}
|
|
4101
|
-
|
|
4102
|
-
// Get terminal width for word wrapping
|
|
4103
|
-
function getTerminalWidth() {
|
|
4104
|
-
return process.stdout.columns || 100;
|
|
72
|
+
return npmName;
|
|
4105
73
|
}
|
|
4106
74
|
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
const words = text.split(/(\s+)/); // Keep whitespace as separate tokens
|
|
4113
|
-
const lines = [];
|
|
4114
|
-
let currentLine = '';
|
|
4115
|
-
|
|
4116
|
-
for (const word of words) {
|
|
4117
|
-
// If adding this word exceeds width, start new line
|
|
4118
|
-
if (currentLine.length + word.length > maxWidth && currentLine.trim()) {
|
|
4119
|
-
lines.push(currentLine.trimEnd());
|
|
4120
|
-
currentLine = word.trimStart(); // Don't start new line with whitespace
|
|
4121
|
-
} else {
|
|
4122
|
-
currentLine += word;
|
|
4123
|
-
}
|
|
4124
|
-
}
|
|
4125
|
-
|
|
4126
|
-
if (currentLine.trim()) {
|
|
4127
|
-
lines.push(currentLine.trimEnd());
|
|
75
|
+
function getInstallPrefix(packageRoot = getPackageRoot()) {
|
|
76
|
+
const derivedPrefix = deriveInstallPrefixFromPackageRoot(packageRoot);
|
|
77
|
+
if (derivedPrefix) {
|
|
78
|
+
return derivedPrefix;
|
|
4128
79
|
}
|
|
4129
80
|
|
|
4130
|
-
return
|
|
4131
|
-
}
|
|
4132
|
-
|
|
4133
|
-
function getLineBuffer(sender) {
|
|
4134
|
-
if (!lineBuffers.has(sender)) {
|
|
4135
|
-
// needsPrefix: true when at start of new line (need to print prefix)
|
|
4136
|
-
// pendingNewline: text written but no newline yet (need newline before next prefix)
|
|
4137
|
-
// textBuffer: accumulate text until we have a complete line
|
|
4138
|
-
lineBuffers.set(sender, {
|
|
4139
|
-
needsPrefix: true,
|
|
4140
|
-
pendingNewline: false,
|
|
4141
|
-
thinkingNeedsPrefix: true,
|
|
4142
|
-
thinkingPendingNewline: false,
|
|
4143
|
-
textBuffer: '', // NEW: buffer for accumulating text
|
|
4144
|
-
});
|
|
4145
|
-
}
|
|
4146
|
-
return lineBuffers.get(sender);
|
|
81
|
+
return childProcess
|
|
82
|
+
.execFileSync(resolveNpmCommand(null), ['config', 'get', 'prefix'], { encoding: 'utf8' })
|
|
83
|
+
.trim();
|
|
4147
84
|
}
|
|
4148
85
|
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
function accumulateText(prefix, sender, text) {
|
|
4152
|
-
if (!text) return;
|
|
4153
|
-
const buf = getLineBuffer(sender);
|
|
4154
|
-
|
|
4155
|
-
// Add incoming text to buffer
|
|
4156
|
-
buf.textBuffer += text;
|
|
4157
|
-
|
|
4158
|
-
// Calculate widths for word wrapping
|
|
4159
|
-
const prefixLen = chalk.reset(prefix).replace(/\\x1b\[[0-9;]*m/g, '').length + 1;
|
|
4160
|
-
const termWidth = getTerminalWidth();
|
|
4161
|
-
const contentWidth = Math.max(40, termWidth - prefixLen - 2);
|
|
4162
|
-
const continuationPrefix = ' '.repeat(prefixLen);
|
|
4163
|
-
|
|
4164
|
-
// Process complete lines (ending with \n)
|
|
4165
|
-
while (buf.textBuffer.includes('\n')) {
|
|
4166
|
-
const newlineIdx = buf.textBuffer.indexOf('\n');
|
|
4167
|
-
const completeLine = buf.textBuffer.slice(0, newlineIdx);
|
|
4168
|
-
buf.textBuffer = buf.textBuffer.slice(newlineIdx + 1);
|
|
4169
|
-
|
|
4170
|
-
// Word wrap and print the complete line
|
|
4171
|
-
// CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
|
|
4172
|
-
const wrappedLines = wordWrap(completeLine, contentWidth);
|
|
4173
|
-
let outputBuffer = '';
|
|
4174
|
-
|
|
4175
|
-
for (let i = 0; i < wrappedLines.length; i++) {
|
|
4176
|
-
const wrappedLine = wrappedLines[i];
|
|
4177
|
-
|
|
4178
|
-
// Print prefix (real or continuation)
|
|
4179
|
-
if (buf.needsPrefix) {
|
|
4180
|
-
outputBuffer += `${prefix} `;
|
|
4181
|
-
buf.needsPrefix = false;
|
|
4182
|
-
} else if (i > 0) {
|
|
4183
|
-
outputBuffer += `${continuationPrefix}`;
|
|
4184
|
-
}
|
|
4185
|
-
|
|
4186
|
-
if (wrappedLine.trim()) {
|
|
4187
|
-
outputBuffer += formatInlineMarkdown(wrappedLine);
|
|
4188
|
-
}
|
|
4189
|
-
|
|
4190
|
-
// Newline after each wrapped segment
|
|
4191
|
-
if (i < wrappedLines.length - 1) {
|
|
4192
|
-
outputBuffer += '\n';
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
|
-
|
|
4196
|
-
// Complete the line
|
|
4197
|
-
outputBuffer += '\n';
|
|
4198
|
-
buf.needsPrefix = true;
|
|
4199
|
-
buf.pendingNewline = false;
|
|
4200
|
-
|
|
4201
|
-
// Single atomic write prevents interleaving
|
|
4202
|
-
if (outputBuffer) {
|
|
4203
|
-
safeWrite(outputBuffer);
|
|
4204
|
-
}
|
|
4205
|
-
}
|
|
4206
|
-
|
|
4207
|
-
// Mark that we have pending text (no newline yet)
|
|
4208
|
-
if (buf.textBuffer.length > 0) {
|
|
4209
|
-
buf.pendingNewline = true;
|
|
4210
|
-
}
|
|
86
|
+
function buildInstallArgs(installPrefix) {
|
|
87
|
+
return ['install', '-g', '--prefix', installPrefix, '--force', NEW_PACKAGE_SPEC];
|
|
4211
88
|
}
|
|
4212
89
|
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
const
|
|
4217
|
-
|
|
4218
|
-
// Calculate widths for word wrapping (same as accumulateText but with 💭 prefix)
|
|
4219
|
-
const prefixLen = chalk.reset(prefix).replace(/\\x1b\[[0-9;]*m/g, '').length + 4; // +4 for " 💭 "
|
|
4220
|
-
const termWidth = getTerminalWidth();
|
|
4221
|
-
const contentWidth = Math.max(40, termWidth - prefixLen - 2);
|
|
4222
|
-
const continuationPrefix = ' '.repeat(prefixLen);
|
|
90
|
+
function runUpdate(options = {}) {
|
|
91
|
+
const installPrefix = options.installPrefix || getInstallPrefix(options.packageRoot);
|
|
92
|
+
const npmCommand = options.npmCommand || resolveNpmCommand(installPrefix);
|
|
93
|
+
const spawn = options.spawn || childProcess.spawn;
|
|
4223
94
|
|
|
4224
|
-
|
|
4225
|
-
while (remaining.length > 0) {
|
|
4226
|
-
const newlineIdx = remaining.indexOf('\n');
|
|
4227
|
-
const rawLine = newlineIdx === -1 ? remaining : remaining.slice(0, newlineIdx);
|
|
95
|
+
console.error(`Installing ${NEW_PACKAGE_NAME} into ${installPrefix}...`);
|
|
4228
96
|
|
|
4229
|
-
|
|
4230
|
-
const
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
const wrappedLine = wrappedLines[i];
|
|
4235
|
-
|
|
4236
|
-
if (buf.thinkingNeedsPrefix) {
|
|
4237
|
-
outputBuffer += `${prefix} ${chalk.dim.italic('💭 ')}`;
|
|
4238
|
-
buf.thinkingNeedsPrefix = false;
|
|
4239
|
-
} else if (i > 0) {
|
|
4240
|
-
outputBuffer += `${continuationPrefix}`;
|
|
4241
|
-
}
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
const proc = spawn(npmCommand, buildInstallArgs(installPrefix), {
|
|
99
|
+
stdio: 'inherit',
|
|
100
|
+
shell: false,
|
|
101
|
+
});
|
|
4242
102
|
|
|
4243
|
-
|
|
4244
|
-
|
|
103
|
+
proc.on('close', (code) => {
|
|
104
|
+
if (code === 0) {
|
|
105
|
+
console.error(`\nDone. ${NEW_PACKAGE_NAME} now owns the global zeroshot command.\n`);
|
|
106
|
+
resolve(true);
|
|
107
|
+
return;
|
|
4245
108
|
}
|
|
4246
109
|
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
}
|
|
110
|
+
console.error(`\nUpdate failed. Run manually: npm install -g --force ${NEW_PACKAGE_SPEC}\n`);
|
|
111
|
+
resolve(false);
|
|
112
|
+
});
|
|
4251
113
|
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
}
|
|
4258
|
-
break;
|
|
4259
|
-
} else {
|
|
4260
|
-
outputBuffer += '\n';
|
|
4261
|
-
buf.thinkingNeedsPrefix = true;
|
|
4262
|
-
buf.thinkingPendingNewline = false;
|
|
4263
|
-
remaining = remaining.slice(newlineIdx + 1);
|
|
4264
|
-
// Single atomic write
|
|
4265
|
-
if (outputBuffer) {
|
|
4266
|
-
safeWrite(outputBuffer);
|
|
4267
|
-
}
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
114
|
+
proc.on('error', () => {
|
|
115
|
+
console.error(`\nUpdate failed. Run manually: npm install -g --force ${NEW_PACKAGE_SPEC}\n`);
|
|
116
|
+
resolve(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
4270
119
|
}
|
|
4271
120
|
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
const buf = lineBuffers.get(sender);
|
|
4275
|
-
if (!buf) return;
|
|
4276
|
-
|
|
4277
|
-
// CRITICAL: Batch all output into single safeWrite() to prevent interleaving with render()
|
|
4278
|
-
let outputBuffer = '';
|
|
4279
|
-
|
|
4280
|
-
// Flush any remaining text in textBuffer (text without trailing newline)
|
|
4281
|
-
if (buf.textBuffer && buf.textBuffer.length > 0) {
|
|
4282
|
-
// Calculate widths for word wrapping (same as accumulateText)
|
|
4283
|
-
const prefixLen = chalk.reset(prefix).replace(/\\x1b\[[0-9;]*m/g, '').length + 1;
|
|
4284
|
-
const termWidth = getTerminalWidth();
|
|
4285
|
-
const contentWidth = Math.max(40, termWidth - prefixLen - 2);
|
|
4286
|
-
const continuationPrefix = ' '.repeat(prefixLen);
|
|
4287
|
-
|
|
4288
|
-
const wrappedLines = wordWrap(buf.textBuffer, contentWidth);
|
|
4289
|
-
for (let i = 0; i < wrappedLines.length; i++) {
|
|
4290
|
-
const wrappedLine = wrappedLines[i];
|
|
4291
|
-
|
|
4292
|
-
if (buf.needsPrefix) {
|
|
4293
|
-
outputBuffer += `${prefix} `;
|
|
4294
|
-
buf.needsPrefix = false;
|
|
4295
|
-
} else if (i > 0) {
|
|
4296
|
-
outputBuffer += `${continuationPrefix}`;
|
|
4297
|
-
}
|
|
4298
|
-
|
|
4299
|
-
if (wrappedLine.trim()) {
|
|
4300
|
-
outputBuffer += formatInlineMarkdown(wrappedLine);
|
|
4301
|
-
}
|
|
4302
|
-
|
|
4303
|
-
if (i < wrappedLines.length - 1) {
|
|
4304
|
-
outputBuffer += '\n';
|
|
4305
|
-
}
|
|
4306
|
-
}
|
|
4307
|
-
|
|
4308
|
-
// Clear the buffer
|
|
4309
|
-
buf.textBuffer = '';
|
|
4310
|
-
buf.pendingNewline = true; // Mark that we need a newline before next prefix
|
|
4311
|
-
}
|
|
4312
|
-
|
|
4313
|
-
if (buf.pendingNewline) {
|
|
4314
|
-
outputBuffer += '\n';
|
|
4315
|
-
buf.needsPrefix = true;
|
|
4316
|
-
buf.pendingNewline = false;
|
|
4317
|
-
}
|
|
4318
|
-
if (buf.thinkingPendingNewline) {
|
|
4319
|
-
outputBuffer += '\n';
|
|
4320
|
-
buf.thinkingNeedsPrefix = true;
|
|
4321
|
-
buf.thinkingPendingNewline = false;
|
|
4322
|
-
}
|
|
4323
|
-
|
|
4324
|
-
// Single atomic write prevents interleaving
|
|
4325
|
-
if (outputBuffer) {
|
|
4326
|
-
safeWrite(outputBuffer);
|
|
4327
|
-
}
|
|
121
|
+
function resolveNewCli() {
|
|
122
|
+
return require.resolve(`${NEW_PACKAGE_NAME}/cli/index.js`);
|
|
4328
123
|
}
|
|
4329
124
|
|
|
4330
|
-
|
|
4331
|
-
const
|
|
4332
|
-
|
|
4333
|
-
/^--- Following log/,
|
|
4334
|
-
/--- Following logs/,
|
|
4335
|
-
/Ctrl\+C to stop/,
|
|
4336
|
-
/^=== (Claude|Codex|Gemini) Task:/,
|
|
4337
|
-
/^Started:/,
|
|
4338
|
-
/^Finished:/,
|
|
4339
|
-
/^Exit code:/,
|
|
4340
|
-
/^CWD:/,
|
|
4341
|
-
/^={50}$/,
|
|
4342
|
-
// Agent context metadata
|
|
4343
|
-
/^Prompt: You are agent/,
|
|
4344
|
-
/^Iteration:/,
|
|
4345
|
-
/^## Triggering Message/,
|
|
4346
|
-
/^## Messages from topic:/,
|
|
4347
|
-
/^## Instructions/,
|
|
4348
|
-
/^## Output Format/,
|
|
4349
|
-
/^Topic: [A-Z_]+$/,
|
|
4350
|
-
/^Sender:/,
|
|
4351
|
-
/^Data: \{/,
|
|
4352
|
-
/^"issue_number"/,
|
|
4353
|
-
/^"title"/,
|
|
4354
|
-
/^"commit"/,
|
|
4355
|
-
/^\[\d{4}-\d{2}-\d{2}T/, // ISO timestamps
|
|
4356
|
-
/^# Manual Input$/,
|
|
4357
|
-
// Task errors (internal)
|
|
4358
|
-
/^Task not found:/,
|
|
4359
|
-
// JSON fragments
|
|
4360
|
-
/^\s*\{$/,
|
|
4361
|
-
/^\s*\}$/,
|
|
4362
|
-
/^\s*"[a-z_]+":.*,?\s*$/,
|
|
4363
|
-
// Template variables (unresolved)
|
|
4364
|
-
/\{\{[a-z.]+\}\}/,
|
|
4365
|
-
];
|
|
4366
|
-
|
|
4367
|
-
// Helper function to print a message (docker-compose style with colors)
|
|
4368
|
-
function printMessage(msg, showClusterId = false, watchMode = false, isActive = true) {
|
|
4369
|
-
// Build prefix using utility function
|
|
4370
|
-
const prefix = buildMessagePrefix(msg, showClusterId, isActive);
|
|
125
|
+
function delegateToNewCli(args, options = {}) {
|
|
126
|
+
const spawn = options.spawn || childProcess.spawn;
|
|
127
|
+
const cliPath = options.cliPath || resolveNewCli();
|
|
4371
128
|
|
|
4372
|
-
const
|
|
4373
|
-
|
|
129
|
+
const proc = spawn(process.execPath, [cliPath, ...args], {
|
|
130
|
+
stdio: 'inherit',
|
|
131
|
+
shell: false,
|
|
4374
132
|
});
|
|
4375
133
|
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
formatWatchMode(msg, clusterPrefix);
|
|
4380
|
-
return;
|
|
4381
|
-
}
|
|
4382
|
-
|
|
4383
|
-
// Normal mode: delegate to appropriate formatter based on topic
|
|
4384
|
-
if (msg.topic === 'AGENT_LIFECYCLE') {
|
|
4385
|
-
formatAgentLifecycle(msg, prefix);
|
|
4386
|
-
return;
|
|
4387
|
-
}
|
|
4388
|
-
|
|
4389
|
-
if (msg.topic === 'AGENT_ERROR') {
|
|
4390
|
-
formatAgentErrorNormal(msg, prefix, timestamp);
|
|
4391
|
-
return;
|
|
4392
|
-
}
|
|
4393
|
-
|
|
4394
|
-
if (msg.topic === 'ISSUE_OPENED') {
|
|
4395
|
-
formatIssueOpenedNormal(msg, prefix, timestamp, shownNewTaskForCluster);
|
|
4396
|
-
return;
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
if (msg.topic === 'IMPLEMENTATION_READY') {
|
|
4400
|
-
formatImplementationReadyNormal(msg, prefix, timestamp);
|
|
4401
|
-
return;
|
|
4402
|
-
}
|
|
4403
|
-
|
|
4404
|
-
if (msg.topic === 'VALIDATION_RESULT') {
|
|
4405
|
-
formatValidationResultNormal(msg, prefix, timestamp);
|
|
4406
|
-
return;
|
|
4407
|
-
}
|
|
4408
|
-
|
|
4409
|
-
if (msg.topic === 'PR_CREATED') {
|
|
4410
|
-
formatPrCreated(msg, prefix, timestamp, safePrint);
|
|
4411
|
-
return;
|
|
4412
|
-
}
|
|
4413
|
-
|
|
4414
|
-
if (msg.topic === 'CLUSTER_COMPLETE') {
|
|
4415
|
-
formatClusterComplete(msg, prefix, timestamp, safePrint);
|
|
4416
|
-
return;
|
|
4417
|
-
}
|
|
4418
|
-
|
|
4419
|
-
if (msg.topic === 'CLUSTER_FAILED') {
|
|
4420
|
-
formatClusterFailed(msg, prefix, timestamp, safePrint);
|
|
4421
|
-
return;
|
|
4422
|
-
}
|
|
4423
|
-
|
|
4424
|
-
// AGENT_OUTPUT: handle separately (complex streaming logic - kept in main file due to dependencies)
|
|
4425
|
-
if (msg.topic === 'AGENT_OUTPUT') {
|
|
4426
|
-
// Support both old 'chunk' and new 'line' formats
|
|
4427
|
-
const content = msg.content?.data?.line || msg.content?.data?.chunk || msg.content?.text;
|
|
4428
|
-
if (!content || !content.trim()) return;
|
|
4429
|
-
|
|
4430
|
-
// Parse streaming JSON events using the parser
|
|
4431
|
-
const provider = normalizeProviderName(
|
|
4432
|
-
msg.content?.data?.provider || msg.sender_provider || 'claude'
|
|
4433
|
-
);
|
|
4434
|
-
const events = parseProviderChunk(provider, content);
|
|
4435
|
-
|
|
4436
|
-
for (const event of events) {
|
|
4437
|
-
switch (event.type) {
|
|
4438
|
-
case 'text':
|
|
4439
|
-
// Accumulate text, print complete lines
|
|
4440
|
-
accumulateText(prefix, msg.sender, event.text);
|
|
4441
|
-
break;
|
|
4442
|
-
|
|
4443
|
-
case 'thinking':
|
|
4444
|
-
case 'thinking_start':
|
|
4445
|
-
// Accumulate thinking, print complete lines
|
|
4446
|
-
if (event.text) {
|
|
4447
|
-
accumulateThinking(prefix, msg.sender, event.text);
|
|
4448
|
-
} else if (event.type === 'thinking_start') {
|
|
4449
|
-
safePrint(`${prefix} ${chalk.dim.italic('💭 thinking...')}`);
|
|
4450
|
-
}
|
|
4451
|
-
break;
|
|
4452
|
-
|
|
4453
|
-
case 'tool_start':
|
|
4454
|
-
// Flush pending text before tool - don't print, tool_call has details
|
|
4455
|
-
flushLineBuffer(prefix, msg.sender);
|
|
4456
|
-
break;
|
|
4457
|
-
|
|
4458
|
-
case 'tool_call':
|
|
4459
|
-
// Flush pending text before tool
|
|
4460
|
-
flushLineBuffer(prefix, msg.sender);
|
|
4461
|
-
const icon = getToolIcon(event.toolName);
|
|
4462
|
-
const toolDesc = formatToolCall(event.toolName, event.input);
|
|
4463
|
-
safePrint(`${prefix} ${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
|
|
4464
|
-
// Store tool call info for matching with result
|
|
4465
|
-
currentToolCall.set(msg.sender, {
|
|
4466
|
-
toolName: event.toolName,
|
|
4467
|
-
input: event.input,
|
|
4468
|
-
});
|
|
4469
|
-
break;
|
|
4470
|
-
|
|
4471
|
-
case 'tool_input':
|
|
4472
|
-
// Streaming tool input JSON - skip (shown in tool_call)
|
|
4473
|
-
break;
|
|
4474
|
-
|
|
4475
|
-
case 'tool_result':
|
|
4476
|
-
const status = event.isError ? chalk.red('✗') : chalk.green('✓');
|
|
4477
|
-
// Get stored tool call info for better formatting
|
|
4478
|
-
const toolCall = currentToolCall.get(msg.sender);
|
|
4479
|
-
const resultDesc = formatToolResult(
|
|
4480
|
-
event.content,
|
|
4481
|
-
event.isError,
|
|
4482
|
-
toolCall?.toolName,
|
|
4483
|
-
toolCall?.input
|
|
4484
|
-
);
|
|
4485
|
-
safePrint(`${prefix} ${status} ${resultDesc}`);
|
|
4486
|
-
// Clear stored tool call after result
|
|
4487
|
-
currentToolCall.delete(msg.sender);
|
|
4488
|
-
break;
|
|
4489
|
-
|
|
4490
|
-
case 'result':
|
|
4491
|
-
// Flush remaining buffer before result
|
|
4492
|
-
flushLineBuffer(prefix, msg.sender);
|
|
4493
|
-
// Final result - only show errors (success text already streamed)
|
|
4494
|
-
if (!event.success) {
|
|
4495
|
-
safePrint(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
|
|
4496
|
-
}
|
|
4497
|
-
break;
|
|
4498
|
-
|
|
4499
|
-
case 'block_end':
|
|
4500
|
-
// Block ended - skip
|
|
4501
|
-
break;
|
|
4502
|
-
|
|
4503
|
-
default:
|
|
4504
|
-
// Unknown event type - skip
|
|
4505
|
-
break;
|
|
4506
|
-
}
|
|
4507
|
-
}
|
|
4508
|
-
|
|
4509
|
-
// If no JSON events parsed, fall through to text filtering
|
|
4510
|
-
if (events.length === 0) {
|
|
4511
|
-
const lines = content.split('\n');
|
|
4512
|
-
for (const line of lines) {
|
|
4513
|
-
const trimmed = line.trim();
|
|
4514
|
-
if (!trimmed) continue;
|
|
4515
|
-
|
|
4516
|
-
// Check against filtered patterns
|
|
4517
|
-
let shouldSkip = false;
|
|
4518
|
-
for (const pattern of FILTERED_PATTERNS) {
|
|
4519
|
-
if (pattern.test(trimmed)) {
|
|
4520
|
-
shouldSkip = true;
|
|
4521
|
-
break;
|
|
4522
|
-
}
|
|
4523
|
-
}
|
|
4524
|
-
if (shouldSkip) continue;
|
|
4525
|
-
|
|
4526
|
-
// Skip JSON-like content
|
|
4527
|
-
if (
|
|
4528
|
-
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
4529
|
-
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
4530
|
-
)
|
|
4531
|
-
continue;
|
|
4532
|
-
|
|
4533
|
-
// Skip duplicate content
|
|
4534
|
-
if (isDuplicate(trimmed)) continue;
|
|
4535
|
-
|
|
4536
|
-
safePrint(`${prefix} ${line}`);
|
|
4537
|
-
}
|
|
4538
|
-
}
|
|
4539
|
-
return;
|
|
4540
|
-
}
|
|
4541
|
-
|
|
4542
|
-
// AGENT_ERROR: Show errors with visual prominence
|
|
4543
|
-
if (msg.topic === 'AGENT_ERROR') {
|
|
4544
|
-
safePrint(''); // Blank line before error
|
|
4545
|
-
safePrint(chalk.bold.red(`${'─'.repeat(60)}`));
|
|
4546
|
-
safePrint(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
|
|
4547
|
-
if (msg.content?.text) {
|
|
4548
|
-
safePrint(`${prefix} ${chalk.red(msg.content.text)}`);
|
|
4549
|
-
}
|
|
4550
|
-
if (msg.content?.data?.stack) {
|
|
4551
|
-
// Show first 5 lines of stack trace
|
|
4552
|
-
const stackLines = msg.content.data.stack.split('\n').slice(0, 5);
|
|
4553
|
-
for (const line of stackLines) {
|
|
4554
|
-
if (line.trim()) {
|
|
4555
|
-
safePrint(`${prefix} ${chalk.dim(line)}`);
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
}
|
|
4559
|
-
safePrint(chalk.bold.red(`${'─'.repeat(60)}`));
|
|
4560
|
-
return;
|
|
4561
|
-
}
|
|
4562
|
-
|
|
4563
|
-
// ISSUE_OPENED: Show as task header with visual separation
|
|
4564
|
-
// Skip duplicate - conductor re-publishes after spawning agents (same task, confusing UX)
|
|
4565
|
-
if (msg.topic === 'ISSUE_OPENED') {
|
|
4566
|
-
if (shownNewTaskForCluster.has(msg.cluster_id)) {
|
|
4567
|
-
return; // Already shown NEW TASK for this cluster
|
|
4568
|
-
}
|
|
4569
|
-
shownNewTaskForCluster.add(msg.cluster_id);
|
|
4570
|
-
|
|
4571
|
-
safePrint(''); // Blank line before new task
|
|
4572
|
-
safePrint(chalk.bold.blue(`${'─'.repeat(60)}`));
|
|
4573
|
-
safePrint(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 NEW TASK')}`);
|
|
4574
|
-
if (msg.content?.text) {
|
|
4575
|
-
// Show task description (first 3 lines max)
|
|
4576
|
-
const lines = msg.content.text.split('\n').slice(0, 3);
|
|
4577
|
-
for (const line of lines) {
|
|
4578
|
-
if (line.trim() && line.trim() !== '# Manual Input') {
|
|
4579
|
-
safePrint(`${prefix} ${chalk.white(line)}`);
|
|
4580
|
-
}
|
|
4581
|
-
}
|
|
4582
|
-
}
|
|
4583
|
-
safePrint(chalk.bold.blue(`${'─'.repeat(60)}`));
|
|
4584
|
-
return;
|
|
4585
|
-
}
|
|
4586
|
-
|
|
4587
|
-
// IMPLEMENTATION_READY: milestone marker
|
|
4588
|
-
if (msg.topic === 'IMPLEMENTATION_READY') {
|
|
4589
|
-
safePrint(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow('✅ IMPLEMENTATION READY')}`);
|
|
4590
|
-
if (msg.content?.data?.commit) {
|
|
4591
|
-
safePrint(
|
|
4592
|
-
`${prefix} ${chalk.gray('Commit:')} ${chalk.cyan(msg.content.data.commit.substring(0, 8))}`
|
|
4593
|
-
);
|
|
4594
|
-
}
|
|
4595
|
-
return;
|
|
4596
|
-
}
|
|
4597
|
-
|
|
4598
|
-
// VALIDATION_RESULT: show approval/rejection clearly
|
|
4599
|
-
if (msg.topic === 'VALIDATION_RESULT') {
|
|
4600
|
-
const data = msg.content?.data || {};
|
|
4601
|
-
const approved = data.approved === true || data.approved === 'true';
|
|
4602
|
-
const status = approved ? chalk.bold.green('✓ APPROVED') : chalk.bold.red('✗ REJECTED');
|
|
4603
|
-
|
|
4604
|
-
safePrint(`${prefix} ${chalk.gray(timestamp)} ${status}`);
|
|
4605
|
-
|
|
4606
|
-
// Show summary if present and not a template variable
|
|
4607
|
-
if (msg.content?.text && !msg.content.text.includes('{{')) {
|
|
4608
|
-
safePrint(`${prefix} ${msg.content.text.substring(0, 100)}`);
|
|
4609
|
-
}
|
|
134
|
+
proc.on('close', (code) => {
|
|
135
|
+
process.exit(code ?? 1);
|
|
136
|
+
});
|
|
4610
137
|
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
138
|
+
proc.on('error', () => {
|
|
139
|
+
console.error(`Could not start ${NEW_PACKAGE_NAME}. Run: zeroshot update`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
4615
143
|
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
safePrint(`${prefix} ${chalk.red('Errors:')}`);
|
|
4619
|
-
data.errors.forEach((err) => {
|
|
4620
|
-
if (err && typeof err === 'string') {
|
|
4621
|
-
safePrint(`${prefix} - ${err}`);
|
|
4622
|
-
}
|
|
4623
|
-
});
|
|
4624
|
-
}
|
|
144
|
+
async function main(args = process.argv.slice(2)) {
|
|
145
|
+
printNotice();
|
|
4625
146
|
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
if (issue && typeof issue === 'string') {
|
|
4630
|
-
safePrint(`${prefix} - ${issue}`);
|
|
4631
|
-
}
|
|
4632
|
-
});
|
|
4633
|
-
}
|
|
147
|
+
if (args[0] === 'update') {
|
|
148
|
+
const success = await runUpdate();
|
|
149
|
+
process.exit(success ? 0 : 1);
|
|
4634
150
|
return;
|
|
4635
151
|
}
|
|
4636
152
|
|
|
4637
|
-
|
|
4638
|
-
formatGenericMessage(msg, prefix, timestamp, safePrint);
|
|
153
|
+
delegateToNewCli(args);
|
|
4639
154
|
}
|
|
4640
155
|
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
const isQuiet =
|
|
4644
|
-
process.argv.includes('-q') ||
|
|
4645
|
-
process.argv.includes('--quiet') ||
|
|
4646
|
-
process.env.NODE_ENV === 'test';
|
|
4647
|
-
|
|
4648
|
-
// Check for updates (non-blocking if offline)
|
|
4649
|
-
await checkForUpdates({ quiet: isQuiet });
|
|
4650
|
-
|
|
4651
|
-
// Default command handling: if first arg doesn't match a known command, treat it as 'run'
|
|
4652
|
-
// This allows `zeroshot "task"` to work the same as `zeroshot run "task"`
|
|
4653
|
-
const args = process.argv.slice(2);
|
|
4654
|
-
if (args.length > 0) {
|
|
4655
|
-
const firstArg = args[0];
|
|
4656
|
-
|
|
4657
|
-
// Skip if it's a flag/option (starts with -)
|
|
4658
|
-
// Skip if it's --help or --version (these are handled by commander)
|
|
4659
|
-
if (!firstArg.startsWith('-')) {
|
|
4660
|
-
// Get all registered command names
|
|
4661
|
-
const commandNames = program.commands.map((cmd) => cmd.name());
|
|
4662
|
-
|
|
4663
|
-
// If first arg is not a known command, prepend 'run'
|
|
4664
|
-
if (!commandNames.includes(firstArg)) {
|
|
4665
|
-
process.argv.splice(2, 0, 'run');
|
|
4666
|
-
}
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4669
|
-
|
|
4670
|
-
program.parse();
|
|
156
|
+
if (require.main === module) {
|
|
157
|
+
main();
|
|
4671
158
|
}
|
|
4672
159
|
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
160
|
+
module.exports = {
|
|
161
|
+
LEGACY_PACKAGE_NAME,
|
|
162
|
+
NEW_PACKAGE_NAME,
|
|
163
|
+
NEW_PACKAGE_SPEC,
|
|
164
|
+
printNotice,
|
|
165
|
+
deriveInstallPrefixFromPackageRoot,
|
|
166
|
+
getInstallPrefix,
|
|
167
|
+
buildInstallArgs,
|
|
168
|
+
runUpdate,
|
|
169
|
+
delegateToNewCli,
|
|
170
|
+
};
|