@the-open-engine/zeroshot 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -474
- package/cli/event-copy.js +12 -0
- package/cli/index.js +311 -96
- package/cli/message-formatters-normal.js +14 -2
- package/cli/message-formatters-watch.js +9 -2
- package/cluster-hooks/block-ask-user-question.py +53 -0
- package/cluster-hooks/block-dangerous-git.py +145 -0
- package/lib/clusters-registry.js +48 -0
- package/lib/compose-utils.js +101 -0
- package/lib/detached-startup.js +9 -0
- package/lib/id-detector.js +8 -9
- package/lib/path-check.js +63 -0
- package/lib/run-mode.js +22 -0
- package/lib/start-cluster.js +39 -23
- package/package.json +3 -4
- package/scripts/check-path.js +19 -0
- package/scripts/validate-templates.js +11 -29
- package/src/agent/agent-hook-executor.js +1 -1
- package/src/agent/agent-task-executor.js +4 -3
- package/src/agent/pr-verification.js +27 -1
- package/src/agents/git-pusher-template.js +121 -4
- package/src/attach/socket-discovery.js +2 -3
- package/src/claude-credentials.js +75 -0
- package/src/claude-task-runner.js +1 -0
- package/src/isolation-manager.js +12 -9
- package/src/issue-providers/README.md +45 -15
- package/src/issue-providers/index.js +2 -0
- package/src/issue-providers/jira-provider.js +8 -1
- package/src/issue-providers/linear-provider.js +405 -0
- package/src/ledger.js +42 -3
- package/src/lib/gc.js +2 -3
- package/src/message-bus.js +10 -0
- package/src/orchestrator.js +264 -144
- package/src/preflight.js +12 -16
- package/src/providers/base-provider.js +7 -6
- package/src/status-footer.js +13 -1
- package/src/template-validation/index.js +3 -0
- package/src/template-validation/report-formatter.js +55 -0
- package/src/worktree-claude-config.js +2 -13
- package/task-lib/runner.js +1 -0
- package/task-lib/watcher.js +1 -0
package/cli/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const { URL } = require('url');
|
|
|
22
22
|
const chalk = require('chalk');
|
|
23
23
|
const Orchestrator = require('../src/orchestrator');
|
|
24
24
|
const { setupCompletion } = require('../lib/completion');
|
|
25
|
+
const { resolveRunMode, describeRunMode } = require('../lib/run-mode');
|
|
25
26
|
const { formatWatchMode } = require('./message-formatters-watch');
|
|
26
27
|
const {
|
|
27
28
|
formatAgentLifecycle,
|
|
@@ -48,10 +49,16 @@ const {
|
|
|
48
49
|
} = require('../lib/settings');
|
|
49
50
|
const { normalizeProviderName } = require('../lib/provider-names');
|
|
50
51
|
const { getProvider, parseProviderChunk } = require('../src/providers');
|
|
52
|
+
const { readClustersFileSync } = require('../lib/clusters-registry');
|
|
51
53
|
const { MOUNT_PRESETS, resolveEnvs } = require('../lib/docker-config');
|
|
52
54
|
const {
|
|
53
55
|
detectGitRepoRoot,
|
|
54
56
|
detectRunInput,
|
|
57
|
+
isStdinInput,
|
|
58
|
+
readStdinText,
|
|
59
|
+
encodeStdinEnv,
|
|
60
|
+
decodeStdinEnv,
|
|
61
|
+
buildTextInput,
|
|
55
62
|
loadClusterConfig,
|
|
56
63
|
resolveConfigPath,
|
|
57
64
|
resolveProviderOverride,
|
|
@@ -66,10 +73,13 @@ const { runCmdproof } = require('./commands/cmdproof');
|
|
|
66
73
|
const {
|
|
67
74
|
markDetachedSetupFailed,
|
|
68
75
|
registerDetachedSetupCluster,
|
|
76
|
+
removeDetachedSetupCluster,
|
|
69
77
|
} = require('../lib/detached-startup');
|
|
70
78
|
// Setup wizard removed - use: zeroshot settings set <key> <value>
|
|
71
79
|
const { checkForUpdates, printLegacyDistroNotice } = require('./lib/update-checker');
|
|
80
|
+
const { checkBinDirOnPath, printPathWarning } = require('../lib/path-check');
|
|
72
81
|
const { StatusFooter, AGENT_STATE, ACTIVE_STATES } = require('../src/status-footer');
|
|
82
|
+
const { EVENT_COPY, formatMergeStatus } = require('./event-copy');
|
|
73
83
|
|
|
74
84
|
// =============================================================================
|
|
75
85
|
// GLOBAL ERROR HANDLERS - Prevent silent process death
|
|
@@ -179,9 +189,10 @@ function normalizeRunOptions(options) {
|
|
|
179
189
|
if (options.docker) {
|
|
180
190
|
options.worktree = false;
|
|
181
191
|
}
|
|
192
|
+
options.autoMerge = Boolean(options.ship);
|
|
182
193
|
}
|
|
183
194
|
|
|
184
|
-
function runClusterPreflight({ input, options, providerOverride, settings, forceProvider }) {
|
|
195
|
+
async function runClusterPreflight({ input, options, providerOverride, settings, forceProvider }) {
|
|
185
196
|
// Detect which issue provider tool is needed
|
|
186
197
|
let issueProvider = null;
|
|
187
198
|
let targetHost = null;
|
|
@@ -205,7 +216,7 @@ function runClusterPreflight({ input, options, providerOverride, settings, force
|
|
|
205
216
|
}
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
requirePreflight({
|
|
219
|
+
await requirePreflight({
|
|
209
220
|
requireGh: issueProvider === 'github', // gh CLI required for GitHub
|
|
210
221
|
requireDocker: options.docker,
|
|
211
222
|
requireGit: options.worktree,
|
|
@@ -221,11 +232,8 @@ function shouldRunDetached(options) {
|
|
|
221
232
|
}
|
|
222
233
|
|
|
223
234
|
function printDetachedClusterStart(options, clusterId, logPath) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
} else {
|
|
227
|
-
console.log(`Started ${clusterId}`);
|
|
228
|
-
}
|
|
235
|
+
const runMode = resolveRunMode(options);
|
|
236
|
+
console.log(runMode ? `Started ${clusterId} (${runMode})` : `Started ${clusterId}`);
|
|
229
237
|
if (logPath) {
|
|
230
238
|
console.log(`Setup log: ${logPath}`);
|
|
231
239
|
}
|
|
@@ -256,7 +264,7 @@ function resolveMergeQueueEnv(value) {
|
|
|
256
264
|
return '';
|
|
257
265
|
}
|
|
258
266
|
|
|
259
|
-
function buildDaemonEnv(options, clusterId, targetCwd) {
|
|
267
|
+
function buildDaemonEnv(options, clusterId, targetCwd, stdinText) {
|
|
260
268
|
const mergeQueueEnv = resolveMergeQueueEnv(options.mergeQueue);
|
|
261
269
|
const runOptionsEnv = serializeRunOptions(options);
|
|
262
270
|
return {
|
|
@@ -275,10 +283,11 @@ function buildDaemonEnv(options, clusterId, targetCwd) {
|
|
|
275
283
|
ZEROSHOT_MERGE_QUEUE: mergeQueueEnv,
|
|
276
284
|
ZEROSHOT_CLOSE_ISSUE: options.closeIssue || '',
|
|
277
285
|
ZEROSHOT_CWD: targetCwd,
|
|
286
|
+
ZEROSHOT_STDIN_TASK: stdinText !== undefined ? encodeStdinEnv(stdinText) : '',
|
|
278
287
|
};
|
|
279
288
|
}
|
|
280
289
|
|
|
281
|
-
async function spawnDetachedCluster(options, clusterId) {
|
|
290
|
+
async function spawnDetachedCluster(options, clusterId, stdinText) {
|
|
282
291
|
const { spawn } = require('child_process');
|
|
283
292
|
const logFd = createDaemonLogFile(clusterId);
|
|
284
293
|
const targetCwd = detectGitRepoRoot();
|
|
@@ -287,7 +296,7 @@ async function spawnDetachedCluster(options, clusterId) {
|
|
|
287
296
|
detached: true,
|
|
288
297
|
stdio: ['ignore', logFd, logFd],
|
|
289
298
|
cwd: targetCwd,
|
|
290
|
-
env: buildDaemonEnv(options, clusterId, targetCwd),
|
|
299
|
+
env: buildDaemonEnv(options, clusterId, targetCwd, stdinText),
|
|
291
300
|
});
|
|
292
301
|
await registerDetachedSetupCluster({
|
|
293
302
|
clusterId,
|
|
@@ -321,13 +330,8 @@ function printForegroundStartInfo(options, clusterId, configName) {
|
|
|
321
330
|
if (process.env.ZEROSHOT_DAEMON) {
|
|
322
331
|
return;
|
|
323
332
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
} else if (options.worktree) {
|
|
327
|
-
console.log(`Starting ${clusterId} (worktree)`);
|
|
328
|
-
} else {
|
|
329
|
-
console.log(`Starting ${clusterId}`);
|
|
330
|
-
}
|
|
333
|
+
const runMode = resolveRunMode(options);
|
|
334
|
+
console.log(runMode ? `Starting ${clusterId} (${runMode})` : `Starting ${clusterId}`);
|
|
331
335
|
console.log(chalk.dim(`Config: ${configName}`));
|
|
332
336
|
console.log(chalk.dim('Ctrl+C to stop following (cluster keeps running)\n'));
|
|
333
337
|
}
|
|
@@ -389,13 +393,14 @@ function applyModelOverrideToConfig(config, modelOverride, providerOverride, set
|
|
|
389
393
|
console.log(chalk.dim(`Model override: ${modelOverride} (all agents)`));
|
|
390
394
|
}
|
|
391
395
|
|
|
392
|
-
function createStatusFooter(clusterId, messageBus) {
|
|
396
|
+
function createStatusFooter(clusterId, messageBus, runMode) {
|
|
393
397
|
const statusFooter = new StatusFooter({
|
|
394
398
|
refreshInterval: 1000,
|
|
395
399
|
enabled: process.stdout.isTTY,
|
|
396
400
|
});
|
|
397
401
|
statusFooter.setCluster(clusterId);
|
|
398
402
|
statusFooter.setClusterState('running');
|
|
403
|
+
statusFooter.setRunMode(runMode);
|
|
399
404
|
statusFooter.setMessageBus(messageBus);
|
|
400
405
|
activeStatusFooter = statusFooter;
|
|
401
406
|
return statusFooter;
|
|
@@ -571,11 +576,11 @@ function waitForClusterCompletion(orchestrator, clusterId, cleanup) {
|
|
|
571
576
|
});
|
|
572
577
|
}
|
|
573
578
|
|
|
574
|
-
async function streamClusterInForeground(cluster, orchestrator, clusterId) {
|
|
579
|
+
async function streamClusterInForeground(cluster, orchestrator, clusterId, options) {
|
|
575
580
|
const sendersWithOutput = new Set();
|
|
576
581
|
const processedMessageIds = new Set();
|
|
577
582
|
|
|
578
|
-
const statusFooter = createStatusFooter(clusterId, cluster.messageBus);
|
|
583
|
+
const statusFooter = createStatusFooter(clusterId, cluster.messageBus, resolveRunMode(options));
|
|
579
584
|
const handleLifecycleMessage = createLifecycleHandler(statusFooter);
|
|
580
585
|
const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic(
|
|
581
586
|
'AGENT_LIFECYCLE',
|
|
@@ -629,22 +634,28 @@ function setupDaemonCleanup(orchestrator, clusterId) {
|
|
|
629
634
|
}
|
|
630
635
|
|
|
631
636
|
function readClusterTokenTotals(orchestrator, clusterId) {
|
|
632
|
-
let totalTokens = 0;
|
|
633
|
-
let totalCostUsd = 0;
|
|
634
637
|
try {
|
|
635
638
|
const clusterObj = orchestrator.getCluster(clusterId);
|
|
636
|
-
if (clusterObj?.messageBus) {
|
|
637
|
-
|
|
638
|
-
if (tokensByRole?._total?.count > 0) {
|
|
639
|
-
const total = tokensByRole._total;
|
|
640
|
-
totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
|
|
641
|
-
totalCostUsd = total.totalCostUsd || 0;
|
|
642
|
-
}
|
|
639
|
+
if (!clusterObj?.messageBus) {
|
|
640
|
+
return { totalTokens: 0, totalCostUsd: 0 };
|
|
643
641
|
}
|
|
644
|
-
|
|
645
|
-
|
|
642
|
+
const { tokensByRole } = clusterObj.messageBus.readSnapshot(clusterId);
|
|
643
|
+
const total = tokensByRole?._total;
|
|
644
|
+
if (!total || total.count === 0) {
|
|
645
|
+
return { totalTokens: 0, totalCostUsd: 0 };
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
totalTokens: (total.inputTokens || 0) + (total.outputTokens || 0),
|
|
649
|
+
totalCostUsd: total.totalCostUsd || 0,
|
|
650
|
+
};
|
|
651
|
+
} catch (error) {
|
|
652
|
+
// A confirmed-empty ledger (0) and a failed read must stay distinguishable -
|
|
653
|
+
// fabricating 0 here is what produced phantom "msgs=0 $0" rows in `list --json`.
|
|
654
|
+
console.error(
|
|
655
|
+
`[cli] Failed to read token totals for cluster ${clusterId}: ${error.message || String(error)}`
|
|
656
|
+
);
|
|
657
|
+
return { totalTokens: null, totalCostUsd: null };
|
|
646
658
|
}
|
|
647
|
-
return { totalTokens, totalCostUsd };
|
|
648
659
|
}
|
|
649
660
|
|
|
650
661
|
function enrichClustersWithTokens(clusters, orchestrator) {
|
|
@@ -672,8 +683,9 @@ function formatClusterRow(cluster) {
|
|
|
672
683
|
const stateDisplay =
|
|
673
684
|
cluster.state === 'zombie' ? chalk.red(cluster.state.padEnd(12)) : cluster.state.padEnd(12);
|
|
674
685
|
const rowColor = cluster.state === 'zombie' ? chalk.red : (text) => text;
|
|
686
|
+
const modeDisplay = (cluster.runMode || '-').padEnd(10);
|
|
675
687
|
|
|
676
|
-
return `${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${cluster.agentCount
|
|
688
|
+
return `${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${modeDisplay} ${cluster.agentCount
|
|
677
689
|
.toString()
|
|
678
690
|
.padEnd(8)} ${tokenDisplay.padEnd(12)} ${costDisplay.padEnd(8)} ${created}`;
|
|
679
691
|
}
|
|
@@ -687,11 +699,11 @@ function printClusterTable(enrichedClusters) {
|
|
|
687
699
|
|
|
688
700
|
console.log(chalk.bold('\n=== Clusters ==='));
|
|
689
701
|
console.log(
|
|
690
|
-
`${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'
|
|
691
|
-
|
|
692
|
-
)} ${'Cost'.padEnd(8)} Created`
|
|
702
|
+
`${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'Mode'.padEnd(10)} ${'Agents'.padEnd(
|
|
703
|
+
8
|
|
704
|
+
)} ${'Tokens'.padEnd(12)} ${'Cost'.padEnd(8)} Created`
|
|
693
705
|
);
|
|
694
|
-
console.log('-'.repeat(
|
|
706
|
+
console.log('-'.repeat(110));
|
|
695
707
|
for (const cluster of enrichedClusters) {
|
|
696
708
|
console.log(formatClusterRow(cluster));
|
|
697
709
|
}
|
|
@@ -734,13 +746,16 @@ function reportMissingId(id, options) {
|
|
|
734
746
|
function getClusterTokensByRole(orchestrator, clusterId) {
|
|
735
747
|
try {
|
|
736
748
|
const cluster = orchestrator.getCluster(clusterId);
|
|
737
|
-
if (cluster?.messageBus) {
|
|
738
|
-
return
|
|
749
|
+
if (!cluster?.messageBus) {
|
|
750
|
+
return null;
|
|
739
751
|
}
|
|
740
|
-
|
|
741
|
-
|
|
752
|
+
return cluster.messageBus.readSnapshot(clusterId).tokensByRole;
|
|
753
|
+
} catch (error) {
|
|
754
|
+
console.error(
|
|
755
|
+
`[cli] Failed to read tokens by role for cluster ${clusterId}: ${error.message || String(error)}`
|
|
756
|
+
);
|
|
757
|
+
return null;
|
|
742
758
|
}
|
|
743
|
-
return null;
|
|
744
759
|
}
|
|
745
760
|
|
|
746
761
|
function printClusterStatusJson(status, tokensByRole) {
|
|
@@ -774,6 +789,9 @@ function printClusterStatusHeader(status, clusterId) {
|
|
|
774
789
|
} else {
|
|
775
790
|
console.log(`State: ${status.state}`);
|
|
776
791
|
}
|
|
792
|
+
if (status.runMode) {
|
|
793
|
+
console.log(`Mode: ${describeRunMode(status.runMode)}`);
|
|
794
|
+
}
|
|
777
795
|
if (status.pid) {
|
|
778
796
|
console.log(`PID: ${status.pid}`);
|
|
779
797
|
}
|
|
@@ -1216,9 +1234,26 @@ function followSetupLog(logPath) {
|
|
|
1216
1234
|
}
|
|
1217
1235
|
|
|
1218
1236
|
function printRecentMessages(messages, limit, isActive, options) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1237
|
+
if (options.watch) {
|
|
1238
|
+
const recentMessages = selectRecentPrintableMessages(messages, limit);
|
|
1239
|
+
for (const msg of recentMessages) {
|
|
1240
|
+
printMessage(msg, true, true, isActive);
|
|
1241
|
+
}
|
|
1242
|
+
if (recentMessages.length === 0 && messages.length > 0) {
|
|
1243
|
+
console.log(chalk.dim(`No printable history in ${messages.length} stored messages.`));
|
|
1244
|
+
}
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const output = renderRecentMessagesToTerminal(messages, limit);
|
|
1249
|
+
|
|
1250
|
+
if (output) {
|
|
1251
|
+
console.log(output);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (messages.length > 0) {
|
|
1256
|
+
console.log(chalk.dim(`No printable history in ${messages.length} stored messages.`));
|
|
1222
1257
|
}
|
|
1223
1258
|
}
|
|
1224
1259
|
|
|
@@ -1461,9 +1496,8 @@ async function printAttachableClusters(clusters, socketDiscovery) {
|
|
|
1461
1496
|
}
|
|
1462
1497
|
|
|
1463
1498
|
function readClusterFromDisk(id) {
|
|
1464
|
-
const clustersFile = path.join(os.homedir(), '.zeroshot', 'clusters.json');
|
|
1465
1499
|
try {
|
|
1466
|
-
const clusters =
|
|
1500
|
+
const clusters = readClustersFileSync(path.join(os.homedir(), '.zeroshot'));
|
|
1467
1501
|
return clusters[id] || null;
|
|
1468
1502
|
} catch {
|
|
1469
1503
|
return null;
|
|
@@ -1554,7 +1588,7 @@ async function resolveClusterSocketPath(id, options, socketDiscovery) {
|
|
|
1554
1588
|
const cluster = readClusterFromDisk(id);
|
|
1555
1589
|
ensureClusterRunning(cluster, id);
|
|
1556
1590
|
|
|
1557
|
-
const orchestrator = await Orchestrator.create({ quiet: true });
|
|
1591
|
+
const orchestrator = await Orchestrator.create({ quiet: true, readonly: true });
|
|
1558
1592
|
try {
|
|
1559
1593
|
const status = orchestrator.getStatus(id);
|
|
1560
1594
|
const activeAgents = getActiveAgents(status);
|
|
@@ -2028,6 +2062,31 @@ function getOrchestrator() {
|
|
|
2028
2062
|
return _orchestratorPromise;
|
|
2029
2063
|
}
|
|
2030
2064
|
|
|
2065
|
+
// Separate lazy-loaded read-only orchestrator for commands that only ever read
|
|
2066
|
+
// cluster state (list/status/logs). Read-only ledgers can never contend with a
|
|
2067
|
+
// live daemon's writer connection or mutate clusters.json as a side effect.
|
|
2068
|
+
/** @type {import('../src/orchestrator') | null} */
|
|
2069
|
+
let _readonlyOrchestrator = null;
|
|
2070
|
+
/** @type {Promise<import('../src/orchestrator')> | null} */
|
|
2071
|
+
let _readonlyOrchestratorPromise = null;
|
|
2072
|
+
/**
|
|
2073
|
+
* @returns {Promise<import('../src/orchestrator')>}
|
|
2074
|
+
*/
|
|
2075
|
+
function getReadonlyOrchestrator() {
|
|
2076
|
+
if (_readonlyOrchestrator) {
|
|
2077
|
+
return Promise.resolve(_readonlyOrchestrator);
|
|
2078
|
+
}
|
|
2079
|
+
if (!_readonlyOrchestratorPromise) {
|
|
2080
|
+
_readonlyOrchestratorPromise = Orchestrator.create({ quiet: true, readonly: true }).then(
|
|
2081
|
+
(orch) => {
|
|
2082
|
+
_readonlyOrchestrator = orch;
|
|
2083
|
+
return orch;
|
|
2084
|
+
}
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
return _readonlyOrchestratorPromise;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2031
2090
|
/**
|
|
2032
2091
|
* @typedef {Object} TaskLogMessage
|
|
2033
2092
|
* @property {string} topic
|
|
@@ -2323,7 +2382,9 @@ Shell completion:
|
|
|
2323
2382
|
// Run command - CLUSTER with auto-detection
|
|
2324
2383
|
program
|
|
2325
2384
|
.command('run <input>')
|
|
2326
|
-
.description(
|
|
2385
|
+
.description(
|
|
2386
|
+
'Start a multi-agent cluster (GitHub issue, markdown file, plain text, or "-" for stdin)'
|
|
2387
|
+
)
|
|
2327
2388
|
.option('--config <file>', 'Path to cluster config JSON (default: conductor-bootstrap)')
|
|
2328
2389
|
.option('--docker', 'Run cluster inside Docker container (full isolation)')
|
|
2329
2390
|
.option('--worktree', 'Use git worktree for isolation (lightweight, no Docker required)')
|
|
@@ -2337,7 +2398,7 @@ program
|
|
|
2337
2398
|
)
|
|
2338
2399
|
.option(
|
|
2339
2400
|
'--pr',
|
|
2340
|
-
'Create PR for human review (uses worktree isolation by default, use --docker for Docker)'
|
|
2401
|
+
'Create PR for human review (uses worktree isolation by default, use --docker for Docker). Never auto-merges itself; a repo-side branch-protection auto-merge rule or merge queue may still merge the PR independently of zeroshot.'
|
|
2341
2402
|
)
|
|
2342
2403
|
.option(
|
|
2343
2404
|
'--ship',
|
|
@@ -2364,6 +2425,7 @@ program
|
|
|
2364
2425
|
.option('-L, --gitlab', 'Force GitLab as issue source')
|
|
2365
2426
|
.option('-J, --jira', 'Force Jira as issue source')
|
|
2366
2427
|
.option('-D, --devops', 'Force Azure DevOps as issue source')
|
|
2428
|
+
.option('-N, --linear', 'Force Linear as issue source')
|
|
2367
2429
|
.option('-d, --detach', 'Run in background (default: attach to first agent)')
|
|
2368
2430
|
.option('--mount <spec...>', 'Add Docker mount (host:container[:ro]). Repeatable.')
|
|
2369
2431
|
.option('--no-mounts', 'Disable all Docker credential mounts')
|
|
@@ -2391,11 +2453,20 @@ Input formats:
|
|
|
2391
2453
|
Azure DevOps:
|
|
2392
2454
|
https://dev.azure.com/org/proj/_workitems/edit/123
|
|
2393
2455
|
|
|
2456
|
+
Linear:
|
|
2457
|
+
ENG-42 Issue key
|
|
2458
|
+
https://linear.app/<workspace>/issue/ENG-42/...
|
|
2459
|
+
|
|
2394
2460
|
File/Text:
|
|
2395
2461
|
feature.md Markdown file
|
|
2396
2462
|
"Implement feature X" Plain text task
|
|
2397
2463
|
|
|
2398
|
-
|
|
2464
|
+
Stdin:
|
|
2465
|
+
- Read task body from stdin (pipe/redirect)
|
|
2466
|
+
pbpaste | zeroshot run -
|
|
2467
|
+
zeroshot run - < issue.md
|
|
2468
|
+
|
|
2469
|
+
Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Linear)
|
|
2399
2470
|
`
|
|
2400
2471
|
)
|
|
2401
2472
|
.action(async (inputArg, options) => {
|
|
@@ -2409,14 +2480,41 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
|
|
|
2409
2480
|
else if (options.gitlab) forceProvider = 'gitlab';
|
|
2410
2481
|
else if (options.jira) forceProvider = 'jira';
|
|
2411
2482
|
else if (options.devops) forceProvider = 'azure-devops';
|
|
2483
|
+
else if (options.linear) forceProvider = 'linear';
|
|
2484
|
+
|
|
2485
|
+
// Stdin input ('-'): read task body from stdin to avoid shell-quoting breakage
|
|
2486
|
+
let stdinText;
|
|
2487
|
+
if (isStdinInput(inputArg)) {
|
|
2488
|
+
if (process.env.ZEROSHOT_DAEMON === '1') {
|
|
2489
|
+
const encoded = process.env.ZEROSHOT_STDIN_TASK;
|
|
2490
|
+
if (!encoded) {
|
|
2491
|
+
throw new Error('zeroshot run -: daemon mode missing forwarded stdin content');
|
|
2492
|
+
}
|
|
2493
|
+
stdinText = decodeStdinEnv(encoded);
|
|
2494
|
+
} else if (process.stdin.isTTY) {
|
|
2495
|
+
throw new Error(
|
|
2496
|
+
'zeroshot run -: no piped input detected. Pipe or redirect a task body, e.g.\n' +
|
|
2497
|
+
' pbpaste | zeroshot run -\n' +
|
|
2498
|
+
' zeroshot run - < issue.md'
|
|
2499
|
+
);
|
|
2500
|
+
} else {
|
|
2501
|
+
stdinText = await readStdinText();
|
|
2502
|
+
}
|
|
2503
|
+
if (!stdinText) {
|
|
2504
|
+
throw new Error('zeroshot run -: stdin was empty, no task content received');
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2412
2507
|
|
|
2413
2508
|
// Auto-detect input type
|
|
2414
|
-
const input = detectRunInput(inputArg);
|
|
2415
2509
|
const settings = loadSettings();
|
|
2510
|
+
const input =
|
|
2511
|
+
stdinText !== undefined
|
|
2512
|
+
? buildTextInput(stdinText)
|
|
2513
|
+
: detectRunInput(inputArg, settings, forceProvider);
|
|
2416
2514
|
const providerOverride = resolveProviderOverride(options);
|
|
2417
2515
|
|
|
2418
2516
|
// Preflight checks
|
|
2419
|
-
runClusterPreflight({ input, options, providerOverride, settings, forceProvider });
|
|
2517
|
+
await runClusterPreflight({ input, options, providerOverride, settings, forceProvider });
|
|
2420
2518
|
|
|
2421
2519
|
// Secondary preflight: token-free template simulation/validation
|
|
2422
2520
|
const simMode = String(options.sim || 'fast').toLowerCase();
|
|
@@ -2446,7 +2544,7 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
|
|
|
2446
2544
|
|
|
2447
2545
|
if (shouldRunDetached(options)) {
|
|
2448
2546
|
const clusterId = generateName('cluster');
|
|
2449
|
-
await spawnDetachedCluster(options, clusterId);
|
|
2547
|
+
await spawnDetachedCluster(options, clusterId, stdinText);
|
|
2450
2548
|
return;
|
|
2451
2549
|
}
|
|
2452
2550
|
|
|
@@ -2511,11 +2609,28 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps)
|
|
|
2511
2609
|
}
|
|
2512
2610
|
|
|
2513
2611
|
if (!process.env.ZEROSHOT_DAEMON) {
|
|
2514
|
-
await streamClusterInForeground(cluster, orchestrator, clusterId);
|
|
2612
|
+
await streamClusterInForeground(cluster, orchestrator, clusterId, options);
|
|
2515
2613
|
}
|
|
2516
2614
|
|
|
2517
2615
|
setupDaemonCleanup(orchestrator, clusterId);
|
|
2518
2616
|
} catch (error) {
|
|
2617
|
+
if (error.code === 'DUPLICATE_CLUSTER') {
|
|
2618
|
+
// Benign guard rejection, not a crash: nothing was allocated, so there is
|
|
2619
|
+
// nothing to roll back. No stack trace, no "Error:" framing.
|
|
2620
|
+
if (process.env.ZEROSHOT_DAEMON && process.env.ZEROSHOT_CLUSTER_ID) {
|
|
2621
|
+
try {
|
|
2622
|
+
await removeDetachedSetupCluster({
|
|
2623
|
+
clusterId: process.env.ZEROSHOT_CLUSTER_ID,
|
|
2624
|
+
storageDir: path.join(os.homedir(), '.zeroshot'),
|
|
2625
|
+
});
|
|
2626
|
+
} catch (removeError) {
|
|
2627
|
+
console.error('Failed to remove provisional setup cluster:', removeError.message);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
console.log(chalk.yellow(error.message));
|
|
2631
|
+
process.exit(1);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2519
2634
|
if (process.env.ZEROSHOT_DAEMON && process.env.ZEROSHOT_CLUSTER_ID) {
|
|
2520
2635
|
try {
|
|
2521
2636
|
await markDetachedSetupFailed({
|
|
@@ -2585,7 +2700,7 @@ taskCmd
|
|
|
2585
2700
|
const providerOverride = normalizeProviderName(
|
|
2586
2701
|
options.provider || process.env.ZEROSHOT_PROVIDER || settings.defaultProvider
|
|
2587
2702
|
);
|
|
2588
|
-
requirePreflight({
|
|
2703
|
+
await requirePreflight({
|
|
2589
2704
|
requireGh: false, // gh not needed for plain tasks
|
|
2590
2705
|
requireDocker: false, // Docker not needed for plain tasks
|
|
2591
2706
|
quiet: false,
|
|
@@ -2629,7 +2744,7 @@ program
|
|
|
2629
2744
|
.option('--json', 'Output as JSON')
|
|
2630
2745
|
.action(async (options) => {
|
|
2631
2746
|
try {
|
|
2632
|
-
const orchestrator = await
|
|
2747
|
+
const orchestrator = await getReadonlyOrchestrator();
|
|
2633
2748
|
const clusters = filterClustersByStatus(orchestrator.listClusters(), options.status);
|
|
2634
2749
|
const enrichedClusters = enrichClustersWithTokens(clusters, orchestrator);
|
|
2635
2750
|
|
|
@@ -2667,7 +2782,7 @@ program
|
|
|
2667
2782
|
}
|
|
2668
2783
|
|
|
2669
2784
|
if (type === 'cluster') {
|
|
2670
|
-
const orchestrator = await
|
|
2785
|
+
const orchestrator = await getReadonlyOrchestrator();
|
|
2671
2786
|
const status = orchestrator.getStatus(id);
|
|
2672
2787
|
const tokensByRole = getClusterTokensByRole(orchestrator, id);
|
|
2673
2788
|
if (options.json) {
|
|
@@ -2725,7 +2840,7 @@ program
|
|
|
2725
2840
|
}
|
|
2726
2841
|
|
|
2727
2842
|
const limit = parseLogLimit(options);
|
|
2728
|
-
const quietOrchestrator = await Orchestrator.create({ quiet: true });
|
|
2843
|
+
const quietOrchestrator = await Orchestrator.create({ quiet: true, readonly: true });
|
|
2729
2844
|
|
|
2730
2845
|
if (!id) {
|
|
2731
2846
|
showAllClusterLogs(quietOrchestrator, options, limit);
|
|
@@ -2949,9 +3064,9 @@ program
|
|
|
2949
3064
|
program
|
|
2950
3065
|
.command('export <cluster-id>')
|
|
2951
3066
|
.description('Export cluster conversation')
|
|
2952
|
-
.option('-f, --format <format>', 'Export format: json, markdown,
|
|
2953
|
-
.option('-o, --output <file>', 'Output file (auto-generated for
|
|
2954
|
-
.action(
|
|
3067
|
+
.option('-f, --format <format>', 'Export format: json, markdown, html', 'html')
|
|
3068
|
+
.option('-o, --output <file>', 'Output file (auto-generated for html)')
|
|
3069
|
+
.action((clusterId, options) => {
|
|
2955
3070
|
try {
|
|
2956
3071
|
// Get messages from DB
|
|
2957
3072
|
const Ledger = require('../src/ledger');
|
|
@@ -2993,10 +3108,12 @@ program
|
|
|
2993
3108
|
return;
|
|
2994
3109
|
}
|
|
2995
3110
|
|
|
2996
|
-
//
|
|
2997
|
-
|
|
3111
|
+
// HTML export - convert ANSI to a self-contained, print-ready HTML file.
|
|
3112
|
+
// Open it in any browser and use Print -> Save as PDF for a PDF copy.
|
|
3113
|
+
// `pdf` is accepted as a migration alias for the previous puppeteer-based export.
|
|
3114
|
+
const isLegacyPdfFormat = options.format === 'pdf';
|
|
3115
|
+
const outputFile = options.output || `${clusterId}.html`;
|
|
2998
3116
|
const AnsiToHtml = require('ansi-to-html');
|
|
2999
|
-
const { default: puppeteer } = await import('puppeteer');
|
|
3000
3117
|
|
|
3001
3118
|
const ansiConverter = new AnsiToHtml({
|
|
3002
3119
|
fg: '#d4d4d4',
|
|
@@ -3042,27 +3159,13 @@ program
|
|
|
3042
3159
|
</body>
|
|
3043
3160
|
</html>`;
|
|
3044
3161
|
|
|
3045
|
-
|
|
3046
|
-
try {
|
|
3047
|
-
const page = await browser.newPage();
|
|
3048
|
-
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
|
3049
|
-
const pdf = await page.pdf({
|
|
3050
|
-
format: 'A4',
|
|
3051
|
-
landscape: true,
|
|
3052
|
-
margin: {
|
|
3053
|
-
top: '10mm',
|
|
3054
|
-
right: '10mm',
|
|
3055
|
-
bottom: '10mm',
|
|
3056
|
-
left: '10mm',
|
|
3057
|
-
},
|
|
3058
|
-
printBackground: true,
|
|
3059
|
-
});
|
|
3060
|
-
|
|
3061
|
-
require('fs').writeFileSync(outputFile, pdf);
|
|
3062
|
-
} finally {
|
|
3063
|
-
await browser.close();
|
|
3064
|
-
}
|
|
3162
|
+
require('fs').writeFileSync(outputFile, fullHtml, 'utf8');
|
|
3065
3163
|
console.log(`Exported to ${outputFile}`);
|
|
3164
|
+
if (isLegacyPdfFormat) {
|
|
3165
|
+
console.log(
|
|
3166
|
+
'Note: PDF export now produces print-ready HTML. Open it in a browser and use Print -> Save as PDF.'
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3066
3169
|
} catch (error) {
|
|
3067
3170
|
console.error('Error exporting cluster:', error.message);
|
|
3068
3171
|
process.exit(1);
|
|
@@ -3096,7 +3199,7 @@ program
|
|
|
3096
3199
|
cluster.config?.defaultProvider ||
|
|
3097
3200
|
settings.defaultProvider;
|
|
3098
3201
|
|
|
3099
|
-
requirePreflight({
|
|
3202
|
+
await requirePreflight({
|
|
3100
3203
|
requireGh: false, // Resume doesn't fetch new issues
|
|
3101
3204
|
requireDocker: requiresDocker,
|
|
3102
3205
|
quiet: false,
|
|
@@ -3228,7 +3331,7 @@ program
|
|
|
3228
3331
|
// If task store is unavailable, fall back to default provider
|
|
3229
3332
|
}
|
|
3230
3333
|
|
|
3231
|
-
requirePreflight({
|
|
3334
|
+
await requirePreflight({
|
|
3232
3335
|
requireGh: false,
|
|
3233
3336
|
requireDocker: false,
|
|
3234
3337
|
quiet: false,
|
|
@@ -3334,6 +3437,23 @@ async function runGc(dryRun) {
|
|
|
3334
3437
|
}
|
|
3335
3438
|
}
|
|
3336
3439
|
|
|
3440
|
+
async function detectAndReportCorruptedClusters(dryRun) {
|
|
3441
|
+
if (dryRun) {
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
try {
|
|
3445
|
+
const orchestrator = await getOrchestrator();
|
|
3446
|
+
const corrupted = await orchestrator.detectCorruptedClusters();
|
|
3447
|
+
if (corrupted.length > 0) {
|
|
3448
|
+
console.log(chalk.yellow(`\nFound ${corrupted.length} corrupted cluster(s):`));
|
|
3449
|
+
corrupted.forEach((id) => console.log(chalk.yellow(` ${id}`)));
|
|
3450
|
+
console.log(chalk.dim(`Run 'zeroshot kill <id>' to remove them.`));
|
|
3451
|
+
}
|
|
3452
|
+
} catch (error) {
|
|
3453
|
+
console.error(`Error detecting corrupted clusters: ${error.message}`);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3337
3457
|
program
|
|
3338
3458
|
.command('gc')
|
|
3339
3459
|
.description('Clean up orphaned worktree directories and stale database files')
|
|
@@ -3341,6 +3461,7 @@ program
|
|
|
3341
3461
|
.action(async (options) => {
|
|
3342
3462
|
try {
|
|
3343
3463
|
printGcResult(await runGc(options.dryRun), options.dryRun);
|
|
3464
|
+
await detectAndReportCorruptedClusters(options.dryRun);
|
|
3344
3465
|
} catch (error) {
|
|
3345
3466
|
console.error('Error during garbage collection:', error.message);
|
|
3346
3467
|
process.exit(1);
|
|
@@ -3514,9 +3635,11 @@ function printNonDockerSettings(settings) {
|
|
|
3514
3635
|
if (dockerKeys.has(key)) {
|
|
3515
3636
|
continue;
|
|
3516
3637
|
}
|
|
3638
|
+
const displayValue =
|
|
3639
|
+
key === 'linearApiKey' && value ? `${String(value).slice(0, 7)}***` : value;
|
|
3517
3640
|
const isDefault = JSON.stringify(DEFAULT_SETTINGS[key]) === JSON.stringify(value);
|
|
3518
3641
|
const label = isDefault ? chalk.dim(key) : chalk.cyan(key);
|
|
3519
|
-
const val = isDefault ? chalk.dim(String(
|
|
3642
|
+
const val = isDefault ? chalk.dim(String(displayValue)) : chalk.white(String(displayValue));
|
|
3520
3643
|
console.log(` ${label.padEnd(30)} ${val}`);
|
|
3521
3644
|
}
|
|
3522
3645
|
}
|
|
@@ -4788,7 +4911,9 @@ function handleIssueOpenedRender({ msg, prefix, timestamp, lines }) {
|
|
|
4788
4911
|
}
|
|
4789
4912
|
|
|
4790
4913
|
function handleImplementationReadyRender({ prefix, timestamp, lines }) {
|
|
4791
|
-
lines.push(
|
|
4914
|
+
lines.push(
|
|
4915
|
+
`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow(`✅ ${EVENT_COPY.IMPLEMENTATION_READY.toUpperCase()}`)}`
|
|
4916
|
+
);
|
|
4792
4917
|
}
|
|
4793
4918
|
|
|
4794
4919
|
function normalizeIssueList(rawIssues) {
|
|
@@ -4857,12 +4982,17 @@ function handleValidationResultRender({ msg, prefix, timestamp, lines }) {
|
|
|
4857
4982
|
function handlePrCreatedRender({ msg, prefix, timestamp, lines }) {
|
|
4858
4983
|
lines.push('');
|
|
4859
4984
|
lines.push(chalk.bold.green('─'.repeat(80)));
|
|
4860
|
-
lines.push(
|
|
4985
|
+
lines.push(
|
|
4986
|
+
`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green(`🔗 ${EVENT_COPY.PR_CREATED.toUpperCase()}`)}`
|
|
4987
|
+
);
|
|
4861
4988
|
if (msg.content?.data?.pr_url) {
|
|
4862
4989
|
lines.push(`${prefix} ${chalk.cyan(msg.content.data.pr_url)}`);
|
|
4863
4990
|
}
|
|
4864
|
-
|
|
4865
|
-
|
|
4991
|
+
const mergeStatus = formatMergeStatus(msg.content?.data?.merged);
|
|
4992
|
+
if (mergeStatus) {
|
|
4993
|
+
lines.push(
|
|
4994
|
+
`${prefix} ${chalk.gray('Merge:')} ${mergeStatus === 'merged' ? chalk.bold.cyan(mergeStatus) : chalk.yellow(mergeStatus)}`
|
|
4995
|
+
);
|
|
4866
4996
|
}
|
|
4867
4997
|
lines.push(chalk.bold.green('─'.repeat(80)));
|
|
4868
4998
|
}
|
|
@@ -4945,7 +5075,15 @@ function handleAgentOutputRender({ msg, prefix, lines, buffers, toolCalls }) {
|
|
|
4945
5075
|
}
|
|
4946
5076
|
if (event.type === 'tool_result') {
|
|
4947
5077
|
appendAgentToolResultEvent(lines, msg.sender, prefix, toolCalls, event);
|
|
5078
|
+
continue;
|
|
4948
5079
|
}
|
|
5080
|
+
if (event.type === 'result') {
|
|
5081
|
+
appendAgentResultEvent(lines, prefix, event);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
if (events.length === 0) {
|
|
5086
|
+
appendRawAgentOutputLines(lines, prefix, content);
|
|
4949
5087
|
}
|
|
4950
5088
|
}
|
|
4951
5089
|
|
|
@@ -4960,6 +5098,63 @@ const RENDER_TOPIC_HANDLERS = {
|
|
|
4960
5098
|
AGENT_OUTPUT: handleAgentOutputRender,
|
|
4961
5099
|
};
|
|
4962
5100
|
|
|
5101
|
+
const HISTORY_NOISE_TOPICS = new Set(['STATE_SNAPSHOT', 'TOKEN_USAGE']);
|
|
5102
|
+
|
|
5103
|
+
function appendAgentResultEvent(lines, prefix, event) {
|
|
5104
|
+
if (!event.success) {
|
|
5105
|
+
lines.push(`${prefix} ${chalk.bold.red('✗ Error:')} ${event.error || 'Task failed'}`);
|
|
5106
|
+
return;
|
|
5107
|
+
}
|
|
5108
|
+
|
|
5109
|
+
const usage = [];
|
|
5110
|
+
if (Number.isFinite(event.inputTokens)) usage.push(`${event.inputTokens} in`);
|
|
5111
|
+
if (Number.isFinite(event.outputTokens)) usage.push(`${event.outputTokens} out`);
|
|
5112
|
+
|
|
5113
|
+
const suffix = usage.length > 0 ? ` (${usage.join(', ')})` : '';
|
|
5114
|
+
lines.push(`${prefix} ${chalk.green('✓')} completed${suffix}`);
|
|
5115
|
+
}
|
|
5116
|
+
|
|
5117
|
+
function appendRawAgentOutputLines(lines, prefix, content) {
|
|
5118
|
+
for (const line of String(content).split('\n')) {
|
|
5119
|
+
const trimmed = line.trim();
|
|
5120
|
+
if (shouldSkipAgentOutputLine(trimmed)) continue;
|
|
5121
|
+
lines.push(`${prefix} ${line}`);
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
function agentOutputHasPrintableHistory(msg) {
|
|
5126
|
+
const content = msg.content?.data?.line || msg.content?.data?.chunk || msg.content?.text;
|
|
5127
|
+
if (!content || !content.trim()) return false;
|
|
5128
|
+
|
|
5129
|
+
const provider = normalizeProviderName(
|
|
5130
|
+
msg.content?.data?.provider || msg.sender_provider || 'claude'
|
|
5131
|
+
);
|
|
5132
|
+
const events = parseProviderChunk(provider, content);
|
|
5133
|
+
if (events.length === 0) {
|
|
5134
|
+
return String(content)
|
|
5135
|
+
.split('\n')
|
|
5136
|
+
.some((line) => !shouldSkipAgentOutputLine(line.trim()));
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
return events.some((event) =>
|
|
5140
|
+
['text', 'thinking', 'thinking_start', 'tool_call', 'tool_result', 'result'].includes(
|
|
5141
|
+
event.type
|
|
5142
|
+
)
|
|
5143
|
+
);
|
|
5144
|
+
}
|
|
5145
|
+
|
|
5146
|
+
function isPrintableHistoryMessage(msg) {
|
|
5147
|
+
if (!msg?.topic || HISTORY_NOISE_TOPICS.has(msg.topic)) return false;
|
|
5148
|
+
if (msg.topic === 'AGENT_OUTPUT') return agentOutputHasPrintableHistory(msg);
|
|
5149
|
+
return true;
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
function selectRecentPrintableMessages(messages, limit) {
|
|
5153
|
+
const printableMessages = messages.filter(isPrintableHistoryMessage);
|
|
5154
|
+
if (!Number.isFinite(limit) || limit <= 0) return printableMessages;
|
|
5155
|
+
return printableMessages.slice(-limit);
|
|
5156
|
+
}
|
|
5157
|
+
|
|
4963
5158
|
/**
|
|
4964
5159
|
* Render messages to terminal-style output with ANSI colors (same as zeroshot logs)
|
|
4965
5160
|
*/
|
|
@@ -4987,6 +5182,13 @@ function renderMessagesToTerminal(clusterId, messages) {
|
|
|
4987
5182
|
return lines.join('\n');
|
|
4988
5183
|
}
|
|
4989
5184
|
|
|
5185
|
+
function renderRecentMessagesToTerminal(messages, limit) {
|
|
5186
|
+
const selectedMessages = selectRecentPrintableMessages(messages, limit);
|
|
5187
|
+
if (selectedMessages.length === 0) return '';
|
|
5188
|
+
|
|
5189
|
+
return renderMessagesToTerminal(null, selectedMessages);
|
|
5190
|
+
}
|
|
5191
|
+
|
|
4990
5192
|
// Get terminal width for word wrapping
|
|
4991
5193
|
function getTerminalWidth() {
|
|
4992
5194
|
return process.stdout.columns || 100;
|
|
@@ -5412,6 +5614,15 @@ async function main() {
|
|
|
5412
5614
|
|
|
5413
5615
|
printLegacyDistroNotice();
|
|
5414
5616
|
|
|
5617
|
+
try {
|
|
5618
|
+
const { onPath, binDir } = checkBinDirOnPath();
|
|
5619
|
+
if (!onPath && binDir) {
|
|
5620
|
+
printPathWarning(binDir);
|
|
5621
|
+
}
|
|
5622
|
+
} catch {
|
|
5623
|
+
// Never block CLI startup on a PATH check failure
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5415
5626
|
// Check for updates (non-blocking if offline)
|
|
5416
5627
|
if (!isTest) {
|
|
5417
5628
|
await checkForUpdates({ quiet: isQuiet });
|
|
@@ -5447,7 +5658,11 @@ async function main() {
|
|
|
5447
5658
|
}
|
|
5448
5659
|
|
|
5449
5660
|
// Run main
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5661
|
+
if (require.main === module) {
|
|
5662
|
+
main().catch((err) => {
|
|
5663
|
+
console.error('Fatal error:', err.message);
|
|
5664
|
+
process.exit(1);
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
module.exports = { renderRecentMessagesToTerminal, resolveRunMode };
|