@covibes/zeroshot 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +20 -6
- package/cli/index.js +383 -145
- package/cli/lib/first-run.js +174 -0
- package/cli/lib/update-checker.js +234 -0
- package/lib/settings.js +25 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# [2.0.0](https://github.com/covibes/zeroshot/compare/v1.5.0...v2.0.0) (2025-12-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Code Refactoring
|
|
5
|
+
|
|
6
|
+
* **cli:** simplify flag hierarchy with cascading --ship → --pr → --isolation ([#18](https://github.com/covibes/zeroshot/issues/18)) ([5718ead](https://github.com/covibes/zeroshot/commit/5718ead37f1771a5dfa68dd9b4f55f73e1f6b9d7)), closes [#17](https://github.com/covibes/zeroshot/issues/17)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
* **cli:** `auto` command removed, use `run --ship` instead
|
|
12
|
+
|
|
13
|
+
- Remove `auto` command (use `run --ship` for full automation)
|
|
14
|
+
- Add `--ship` flag: isolation + PR + auto-merge
|
|
15
|
+
- `--pr` now auto-enables `--isolation`
|
|
16
|
+
- Rename `clear` → `purge` for clarity
|
|
17
|
+
- Update help text with cascading flag examples
|
|
18
|
+
- Add `agents` command stubs
|
|
19
|
+
- Add `--json` output support to list/status
|
|
20
|
+
|
|
1
21
|
# [1.5.0](https://github.com/covibes/zeroshot/compare/v1.4.0...v1.5.0) (2025-12-28)
|
|
2
22
|
|
|
3
23
|
|
package/README.md
CHANGED
|
@@ -67,19 +67,33 @@ gh auth login
|
|
|
67
67
|
## Commands
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
zeroshot 123
|
|
71
|
-
zeroshot "Add dark mode"
|
|
70
|
+
zeroshot run 123 # Run on GitHub issue
|
|
71
|
+
zeroshot run "Add dark mode" # Run from description
|
|
72
72
|
|
|
73
|
-
#
|
|
74
|
-
zeroshot
|
|
75
|
-
zeroshot
|
|
73
|
+
# Automation levels (cascading: --ship → --pr → --isolation)
|
|
74
|
+
zeroshot run 123 --isolation # Docker isolation, no PR
|
|
75
|
+
zeroshot run 123 --pr # Isolation + PR (human reviews)
|
|
76
|
+
zeroshot run 123 --ship # Isolation + PR + auto-merge (full automation)
|
|
77
|
+
|
|
78
|
+
# Background mode
|
|
79
|
+
zeroshot run 123 -d # Detached/daemon
|
|
80
|
+
zeroshot run 123 --ship -d # Full automation, background
|
|
76
81
|
|
|
77
82
|
# Control
|
|
78
|
-
zeroshot list # See all running
|
|
83
|
+
zeroshot list # See all running (--json for scripting)
|
|
84
|
+
zeroshot status <id> # Cluster status (--json for scripting)
|
|
79
85
|
zeroshot logs <id> -f # Follow output
|
|
80
86
|
zeroshot resume <id> # Continue after crash
|
|
81
87
|
zeroshot kill <id> # Stop
|
|
82
88
|
zeroshot watch # TUI dashboard
|
|
89
|
+
|
|
90
|
+
# Agent library
|
|
91
|
+
zeroshot agents list # View available agents
|
|
92
|
+
zeroshot agents show <name> # Agent details
|
|
93
|
+
|
|
94
|
+
# Maintenance
|
|
95
|
+
zeroshot clean # Remove old records
|
|
96
|
+
zeroshot purge # NUCLEAR: kill all + delete all
|
|
83
97
|
```
|
|
84
98
|
|
|
85
99
|
---
|
package/cli/index.js
CHANGED
|
@@ -47,6 +47,8 @@ const {
|
|
|
47
47
|
DEFAULT_SETTINGS,
|
|
48
48
|
} = require('../lib/settings');
|
|
49
49
|
const { requirePreflight } = require('../src/preflight');
|
|
50
|
+
const { checkFirstRun } = require('./lib/first-run');
|
|
51
|
+
const { checkForUpdates } = require('./lib/update-checker');
|
|
50
52
|
const { StatusFooter } = require('../src/status-footer');
|
|
51
53
|
|
|
52
54
|
const program = new Command();
|
|
@@ -375,11 +377,12 @@ program
|
|
|
375
377
|
.name('zeroshot')
|
|
376
378
|
.description('Multi-agent orchestration and task management for Claude')
|
|
377
379
|
.version(require('../package.json').version)
|
|
380
|
+
.option('-q, --quiet', 'Suppress prompts (first-run wizard, update checks)')
|
|
378
381
|
.addHelpText(
|
|
379
382
|
'after',
|
|
380
383
|
`
|
|
381
384
|
Examples:
|
|
382
|
-
${chalk.cyan('zeroshot
|
|
385
|
+
${chalk.cyan('zeroshot run 123 --ship')} Full automation: isolated + auto-merge PR
|
|
383
386
|
${chalk.cyan('zeroshot run 123')} Run cluster and attach to first agent
|
|
384
387
|
${chalk.cyan('zeroshot run 123 -d')} Run cluster in background (detached)
|
|
385
388
|
${chalk.cyan('zeroshot run "Implement feature X"')} Run cluster on plain text task
|
|
@@ -395,19 +398,20 @@ Examples:
|
|
|
395
398
|
${chalk.cyan('zeroshot status <id>')} Detailed status of task or cluster
|
|
396
399
|
${chalk.cyan('zeroshot finish <id>')} Convert cluster to completion task (creates and merges PR)
|
|
397
400
|
${chalk.cyan('zeroshot kill <id>')} Kill a running task or cluster
|
|
398
|
-
${chalk.cyan('zeroshot
|
|
399
|
-
${chalk.cyan('zeroshot
|
|
401
|
+
${chalk.cyan('zeroshot purge')} Kill all processes and delete all data (with confirmation)
|
|
402
|
+
${chalk.cyan('zeroshot purge -y')} Purge everything without confirmation
|
|
400
403
|
${chalk.cyan('zeroshot settings')} Show/manage zeroshot settings (default model, config, etc.)
|
|
401
404
|
${chalk.cyan('zeroshot settings set <key> <val>')} Set a setting (e.g., defaultModel haiku)
|
|
402
405
|
${chalk.cyan('zeroshot config list')} List available cluster configs
|
|
403
406
|
${chalk.cyan('zeroshot config show <name>')} Visualize a cluster config (agents, triggers, flow)
|
|
404
407
|
${chalk.cyan('zeroshot export <id>')} Export cluster conversation to file
|
|
405
408
|
|
|
406
|
-
|
|
407
|
-
${chalk.yellow('zeroshot
|
|
408
|
-
${chalk.yellow('zeroshot run')}
|
|
409
|
-
${chalk.yellow('zeroshot run
|
|
410
|
-
${chalk.yellow('zeroshot
|
|
409
|
+
Automation levels (cascading: --ship → --pr → --isolation):
|
|
410
|
+
${chalk.yellow('zeroshot run 123')} → Local run, no isolation
|
|
411
|
+
${chalk.yellow('zeroshot run 123 --isolation')} → Docker isolation, no PR
|
|
412
|
+
${chalk.yellow('zeroshot run 123 --pr')} → Isolation + PR (human reviews)
|
|
413
|
+
${chalk.yellow('zeroshot run 123 --ship')} → Isolation + PR + auto-merge (full automation)
|
|
414
|
+
${chalk.yellow('zeroshot task run')} → Single-agent background task (simpler, faster)
|
|
411
415
|
|
|
412
416
|
Shell completion:
|
|
413
417
|
${chalk.dim('zeroshot --completion >> ~/.bashrc && source ~/.bashrc')}
|
|
@@ -429,8 +433,8 @@ program
|
|
|
429
433
|
'--strict-schema',
|
|
430
434
|
'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
|
|
431
435
|
)
|
|
432
|
-
.option('--pr', 'Create PR
|
|
433
|
-
.option('--
|
|
436
|
+
.option('--pr', 'Create PR for human review (auto-enables --isolation)')
|
|
437
|
+
.option('--ship', 'Full automation: isolation + PR + auto-merge')
|
|
434
438
|
.option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
|
|
435
439
|
.option('-d, --detach', 'Run in background (default: attach to first agent)')
|
|
436
440
|
.addHelpText(
|
|
@@ -445,10 +449,15 @@ Input formats:
|
|
|
445
449
|
)
|
|
446
450
|
.action(async (inputArg, options) => {
|
|
447
451
|
try {
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
452
|
+
// Cascading flag implications: --ship → --pr → --isolation
|
|
453
|
+
// --ship = full automation (isolation + PR + auto-merge)
|
|
454
|
+
if (options.ship) {
|
|
451
455
|
options.pr = true;
|
|
456
|
+
options.isolation = true;
|
|
457
|
+
}
|
|
458
|
+
// --pr = PR for human review (auto-enables isolation)
|
|
459
|
+
if (options.pr) {
|
|
460
|
+
options.isolation = true;
|
|
452
461
|
}
|
|
453
462
|
|
|
454
463
|
// Auto-detect input type
|
|
@@ -476,18 +485,12 @@ Input formats:
|
|
|
476
485
|
// This gives users clear, actionable error messages upfront
|
|
477
486
|
const preflightOptions = {
|
|
478
487
|
requireGh: !!input.issue, // gh CLI required when fetching GitHub issues
|
|
479
|
-
requireDocker: options.isolation
|
|
488
|
+
requireDocker: options.isolation, // Docker required for isolation mode
|
|
480
489
|
quiet: process.env.CREW_DAEMON === '1', // Suppress success in daemon mode
|
|
481
490
|
};
|
|
482
491
|
requirePreflight(preflightOptions);
|
|
483
492
|
|
|
484
493
|
// === CLUSTER MODE ===
|
|
485
|
-
// Validate --pr requires --isolation
|
|
486
|
-
if (options.pr && !options.isolation) {
|
|
487
|
-
console.error(chalk.red('Error: --pr requires --isolation flag for safety'));
|
|
488
|
-
console.error(chalk.dim(' Usage: zeroshot run 123 --isolation --pr'));
|
|
489
|
-
process.exit(1);
|
|
490
|
-
}
|
|
491
494
|
|
|
492
495
|
const { generateName } = require('../src/name-generator');
|
|
493
496
|
|
|
@@ -811,68 +814,6 @@ Input formats:
|
|
|
811
814
|
}
|
|
812
815
|
});
|
|
813
816
|
|
|
814
|
-
// Auto command - full automation (isolation + PR)
|
|
815
|
-
program
|
|
816
|
-
.command('auto <input>')
|
|
817
|
-
.description('Full automation: isolated + auto-merge PR (shorthand for run --isolation --pr)')
|
|
818
|
-
.option('--config <file>', 'Path to cluster config JSON (default: conductor-bootstrap)')
|
|
819
|
-
.option('-m, --model <model>', 'Model for all agents: opus, sonnet, haiku (default: from config)')
|
|
820
|
-
.option(
|
|
821
|
-
'--isolation-image <image>',
|
|
822
|
-
'Docker image for isolation (default: zeroshot-cluster-base)'
|
|
823
|
-
)
|
|
824
|
-
.option(
|
|
825
|
-
'--strict-schema',
|
|
826
|
-
'Enforce JSON schema via CLI (no live streaming). Default: live streaming with local validation'
|
|
827
|
-
)
|
|
828
|
-
.option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
|
|
829
|
-
.option('-d, --detach', 'Run in background (default: attach to first agent)')
|
|
830
|
-
.addHelpText(
|
|
831
|
-
'after',
|
|
832
|
-
`
|
|
833
|
-
Input formats:
|
|
834
|
-
123 GitHub issue number (uses current repo)
|
|
835
|
-
org/repo#123 GitHub issue with explicit repo
|
|
836
|
-
https://github.com/.../issues/1 Full GitHub issue URL
|
|
837
|
-
"Implement feature X" Plain text task description
|
|
838
|
-
|
|
839
|
-
Examples:
|
|
840
|
-
${chalk.cyan('zeroshot auto 123')} Auto-resolve issue (isolated + PR)
|
|
841
|
-
${chalk.cyan('zeroshot auto 123 -d')} Same, but detached/background
|
|
842
|
-
`
|
|
843
|
-
)
|
|
844
|
-
.action((inputArg, options) => {
|
|
845
|
-
// Auto command is shorthand for: zeroshot run <input> --isolation --pr [options]
|
|
846
|
-
// Re-invoke CLI with the correct flags to avoid Commander.js internal API issues
|
|
847
|
-
const { spawn } = require('child_process');
|
|
848
|
-
|
|
849
|
-
const args = ['run', inputArg, '--isolation', '--pr'];
|
|
850
|
-
|
|
851
|
-
// Forward other options
|
|
852
|
-
if (options.config) args.push('--config', options.config);
|
|
853
|
-
if (options.model) args.push('--model', options.model);
|
|
854
|
-
if (options.isolationImage) args.push('--isolation-image', options.isolationImage);
|
|
855
|
-
if (options.strictSchema) args.push('--strict-schema');
|
|
856
|
-
if (options.workers) args.push('--workers', String(options.workers));
|
|
857
|
-
if (options.detach) args.push('--detach');
|
|
858
|
-
|
|
859
|
-
// Spawn zeroshot run with inherited stdio
|
|
860
|
-
const proc = spawn(process.execPath, [process.argv[1], ...args], {
|
|
861
|
-
stdio: 'inherit',
|
|
862
|
-
cwd: process.cwd(),
|
|
863
|
-
env: process.env,
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
proc.on('close', (code) => {
|
|
867
|
-
process.exit(code || 0);
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
proc.on('error', (err) => {
|
|
871
|
-
console.error(chalk.red(`Error: ${err.message}`));
|
|
872
|
-
process.exit(1);
|
|
873
|
-
});
|
|
874
|
-
});
|
|
875
|
-
|
|
876
817
|
// === TASK COMMANDS ===
|
|
877
818
|
// Task run - single-agent background task
|
|
878
819
|
const taskCmd = program.command('task').description('Single-agent task management');
|
|
@@ -956,48 +897,78 @@ program
|
|
|
956
897
|
.description('List all tasks and clusters')
|
|
957
898
|
.option('-s, --status <status>', 'Filter tasks by status (running, completed, failed)')
|
|
958
899
|
.option('-n, --limit <n>', 'Limit number of results', parseInt)
|
|
900
|
+
.option('--json', 'Output as JSON')
|
|
959
901
|
.action(async (options) => {
|
|
960
902
|
try {
|
|
961
903
|
// Get clusters
|
|
962
904
|
const clusters = getOrchestrator().listClusters();
|
|
905
|
+
const orchestrator = getOrchestrator();
|
|
906
|
+
|
|
907
|
+
// Enrich clusters with token data
|
|
908
|
+
const enrichedClusters = clusters.map((cluster) => {
|
|
909
|
+
let totalTokens = 0;
|
|
910
|
+
let totalCostUsd = 0;
|
|
911
|
+
try {
|
|
912
|
+
const clusterObj = orchestrator.getCluster(cluster.id);
|
|
913
|
+
if (clusterObj?.messageBus) {
|
|
914
|
+
const tokensByRole = clusterObj.messageBus.getTokensByRole(cluster.id);
|
|
915
|
+
if (tokensByRole?._total?.count > 0) {
|
|
916
|
+
const total = tokensByRole._total;
|
|
917
|
+
totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
|
|
918
|
+
totalCostUsd = total.totalCostUsd || 0;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
} catch {
|
|
922
|
+
/* Token tracking not available */
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
...cluster,
|
|
926
|
+
totalTokens,
|
|
927
|
+
totalCostUsd,
|
|
928
|
+
};
|
|
929
|
+
});
|
|
963
930
|
|
|
964
931
|
// Get tasks (dynamic import)
|
|
965
|
-
const { listTasks } = await import('../task-lib/commands/list.js');
|
|
932
|
+
const { listTasks, getTasksData } = await import('../task-lib/commands/list.js');
|
|
966
933
|
|
|
967
|
-
//
|
|
968
|
-
|
|
934
|
+
// JSON output mode
|
|
935
|
+
if (options.json) {
|
|
936
|
+
// Get tasks data if available
|
|
937
|
+
let tasks = [];
|
|
938
|
+
try {
|
|
939
|
+
if (typeof getTasksData === 'function') {
|
|
940
|
+
tasks = await getTasksData(options);
|
|
941
|
+
}
|
|
942
|
+
} catch {
|
|
943
|
+
/* Tasks not available */
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
console.log(
|
|
947
|
+
JSON.stringify(
|
|
948
|
+
{
|
|
949
|
+
clusters: enrichedClusters,
|
|
950
|
+
tasks,
|
|
951
|
+
},
|
|
952
|
+
null,
|
|
953
|
+
2
|
|
954
|
+
)
|
|
955
|
+
);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
969
958
|
|
|
959
|
+
// Human-readable output (default)
|
|
970
960
|
// Print clusters
|
|
971
|
-
if (
|
|
961
|
+
if (enrichedClusters.length > 0) {
|
|
972
962
|
console.log(chalk.bold('\n=== Clusters ==='));
|
|
973
963
|
console.log(
|
|
974
964
|
`${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'Agents'.padEnd(8)} ${'Tokens'.padEnd(12)} ${'Cost'.padEnd(8)} Created`
|
|
975
965
|
);
|
|
976
966
|
console.log('-'.repeat(100));
|
|
977
967
|
|
|
978
|
-
const
|
|
979
|
-
for (const cluster of clusters) {
|
|
968
|
+
for (const cluster of enrichedClusters) {
|
|
980
969
|
const created = new Date(cluster.createdAt).toLocaleString();
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
let tokenDisplay = '-';
|
|
984
|
-
let costDisplay = '-';
|
|
985
|
-
try {
|
|
986
|
-
const clusterObj = orchestrator.getCluster(cluster.id);
|
|
987
|
-
if (clusterObj?.messageBus) {
|
|
988
|
-
const tokensByRole = clusterObj.messageBus.getTokensByRole(cluster.id);
|
|
989
|
-
if (tokensByRole?._total?.count > 0) {
|
|
990
|
-
const total = tokensByRole._total;
|
|
991
|
-
const totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
|
|
992
|
-
tokenDisplay = totalTokens.toLocaleString();
|
|
993
|
-
if (total.totalCostUsd > 0) {
|
|
994
|
-
costDisplay = '$' + total.totalCostUsd.toFixed(3);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
} catch {
|
|
999
|
-
/* Token tracking not available */
|
|
1000
|
-
}
|
|
970
|
+
const tokenDisplay = cluster.totalTokens > 0 ? cluster.totalTokens.toLocaleString() : '-';
|
|
971
|
+
const costDisplay = cluster.totalCostUsd > 0 ? '$' + cluster.totalCostUsd.toFixed(3) : '-';
|
|
1001
972
|
|
|
1002
973
|
// Highlight zombie clusters in red
|
|
1003
974
|
const stateDisplay =
|
|
@@ -1029,14 +1000,19 @@ program
|
|
|
1029
1000
|
program
|
|
1030
1001
|
.command('status <id>')
|
|
1031
1002
|
.description('Get detailed status of a task or cluster')
|
|
1032
|
-
.
|
|
1003
|
+
.option('--json', 'Output as JSON')
|
|
1004
|
+
.action(async (id, options) => {
|
|
1033
1005
|
try {
|
|
1034
1006
|
const { detectIdType } = require('../lib/id-detector');
|
|
1035
1007
|
const type = detectIdType(id);
|
|
1036
1008
|
|
|
1037
1009
|
if (!type) {
|
|
1038
|
-
|
|
1039
|
-
|
|
1010
|
+
if (options.json) {
|
|
1011
|
+
console.log(JSON.stringify({ error: 'ID not found', id }, null, 2));
|
|
1012
|
+
} else {
|
|
1013
|
+
console.error(`ID not found: ${id}`);
|
|
1014
|
+
console.error('Not found in tasks or clusters');
|
|
1015
|
+
}
|
|
1040
1016
|
process.exit(1);
|
|
1041
1017
|
}
|
|
1042
1018
|
|
|
@@ -1044,6 +1020,35 @@ program
|
|
|
1044
1020
|
// Show cluster status
|
|
1045
1021
|
const status = getOrchestrator().getStatus(id);
|
|
1046
1022
|
|
|
1023
|
+
// Get token usage
|
|
1024
|
+
let tokensByRole = null;
|
|
1025
|
+
try {
|
|
1026
|
+
const cluster = getOrchestrator().getCluster(id);
|
|
1027
|
+
if (cluster?.messageBus) {
|
|
1028
|
+
tokensByRole = cluster.messageBus.getTokensByRole(id);
|
|
1029
|
+
}
|
|
1030
|
+
} catch {
|
|
1031
|
+
/* Token tracking not available */
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// JSON output mode
|
|
1035
|
+
if (options.json) {
|
|
1036
|
+
console.log(
|
|
1037
|
+
JSON.stringify(
|
|
1038
|
+
{
|
|
1039
|
+
type: 'cluster',
|
|
1040
|
+
...status,
|
|
1041
|
+
createdAtISO: new Date(status.createdAt).toISOString(),
|
|
1042
|
+
tokensByRole,
|
|
1043
|
+
},
|
|
1044
|
+
null,
|
|
1045
|
+
2
|
|
1046
|
+
)
|
|
1047
|
+
);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Human-readable output
|
|
1047
1052
|
console.log(`\nCluster: ${status.id}`);
|
|
1048
1053
|
if (status.isZombie) {
|
|
1049
1054
|
console.log(
|
|
@@ -1066,20 +1071,14 @@ program
|
|
|
1066
1071
|
console.log(`Messages: ${status.messageCount}`);
|
|
1067
1072
|
|
|
1068
1073
|
// Show token usage if available
|
|
1069
|
-
|
|
1070
|
-
const
|
|
1071
|
-
if (
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
console.log('');
|
|
1076
|
-
for (const line of tokenLines) {
|
|
1077
|
-
console.log(line);
|
|
1078
|
-
}
|
|
1074
|
+
if (tokensByRole) {
|
|
1075
|
+
const tokenLines = formatTokenUsage(tokensByRole);
|
|
1076
|
+
if (tokenLines) {
|
|
1077
|
+
console.log('');
|
|
1078
|
+
for (const line of tokenLines) {
|
|
1079
|
+
console.log(line);
|
|
1079
1080
|
}
|
|
1080
1081
|
}
|
|
1081
|
-
} catch {
|
|
1082
|
-
/* Token tracking not available */
|
|
1083
1082
|
}
|
|
1084
1083
|
|
|
1085
1084
|
console.log(`\nAgents:`);
|
|
@@ -1104,11 +1103,30 @@ program
|
|
|
1104
1103
|
console.log('');
|
|
1105
1104
|
} else {
|
|
1106
1105
|
// Show task status
|
|
1107
|
-
const { showStatus } = await import('../task-lib/commands/status.js');
|
|
1106
|
+
const { showStatus, getStatusData } = await import('../task-lib/commands/status.js');
|
|
1107
|
+
|
|
1108
|
+
if (options.json) {
|
|
1109
|
+
// Try to get JSON data if available
|
|
1110
|
+
let taskData = null;
|
|
1111
|
+
try {
|
|
1112
|
+
if (typeof getStatusData === 'function') {
|
|
1113
|
+
taskData = await getStatusData(id);
|
|
1114
|
+
}
|
|
1115
|
+
} catch {
|
|
1116
|
+
/* Not available */
|
|
1117
|
+
}
|
|
1118
|
+
console.log(JSON.stringify({ type: 'task', id, ...taskData }, null, 2));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1108
1122
|
await showStatus(id);
|
|
1109
1123
|
}
|
|
1110
1124
|
} catch (error) {
|
|
1111
|
-
|
|
1125
|
+
if (options.json) {
|
|
1126
|
+
console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
1127
|
+
} else {
|
|
1128
|
+
console.error('Error getting status:', error.message);
|
|
1129
|
+
}
|
|
1112
1130
|
process.exit(1);
|
|
1113
1131
|
}
|
|
1114
1132
|
});
|
|
@@ -2460,10 +2478,10 @@ program
|
|
|
2460
2478
|
}
|
|
2461
2479
|
});
|
|
2462
2480
|
|
|
2463
|
-
//
|
|
2481
|
+
// Purge all runs (clusters + tasks) - NUCLEAR option
|
|
2464
2482
|
program
|
|
2465
|
-
.command('
|
|
2466
|
-
.description('Kill all running processes and delete all data')
|
|
2483
|
+
.command('purge')
|
|
2484
|
+
.description('NUCLEAR: Kill all running processes and delete all data')
|
|
2467
2485
|
.option('-y, --yes', 'Skip confirmation')
|
|
2468
2486
|
.action(async (options) => {
|
|
2469
2487
|
try {
|
|
@@ -2604,7 +2622,7 @@ program
|
|
|
2604
2622
|
await cleanTasks({ all: true });
|
|
2605
2623
|
}
|
|
2606
2624
|
|
|
2607
|
-
console.log(chalk.bold.green('\nAll runs
|
|
2625
|
+
console.log(chalk.bold.green('\nAll runs purged.'));
|
|
2608
2626
|
} catch (error) {
|
|
2609
2627
|
console.error('Error clearing runs:', error.message);
|
|
2610
2628
|
process.exit(1);
|
|
@@ -3005,6 +3023,210 @@ configCmd
|
|
|
3005
3023
|
}
|
|
3006
3024
|
});
|
|
3007
3025
|
|
|
3026
|
+
// Agent library commands
|
|
3027
|
+
const agentsCmd = program.command('agents').description('View available agent definitions');
|
|
3028
|
+
|
|
3029
|
+
agentsCmd
|
|
3030
|
+
.command('list')
|
|
3031
|
+
.alias('ls')
|
|
3032
|
+
.description('List available agent definitions')
|
|
3033
|
+
.option('--verbose', 'Show full agent details')
|
|
3034
|
+
.option('--json', 'Output as JSON')
|
|
3035
|
+
.action((options) => {
|
|
3036
|
+
try {
|
|
3037
|
+
const agentsDir = path.join(PACKAGE_ROOT, 'src', 'agents');
|
|
3038
|
+
|
|
3039
|
+
// Check if agents directory exists
|
|
3040
|
+
if (!fs.existsSync(agentsDir)) {
|
|
3041
|
+
if (options.json) {
|
|
3042
|
+
console.log(JSON.stringify({ agents: [], error: null }, null, 2));
|
|
3043
|
+
} else {
|
|
3044
|
+
console.log(chalk.dim('No agents directory found.'));
|
|
3045
|
+
}
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.json'));
|
|
3050
|
+
|
|
3051
|
+
if (files.length === 0) {
|
|
3052
|
+
if (options.json) {
|
|
3053
|
+
console.log(JSON.stringify({ agents: [], error: null }, null, 2));
|
|
3054
|
+
} else {
|
|
3055
|
+
console.log(chalk.dim('No agent definitions found in src/agents/'));
|
|
3056
|
+
}
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// Parse all agent files
|
|
3061
|
+
const agents = [];
|
|
3062
|
+
for (const file of files) {
|
|
3063
|
+
try {
|
|
3064
|
+
const agentPath = path.join(agentsDir, file);
|
|
3065
|
+
const agent = JSON.parse(fs.readFileSync(agentPath, 'utf8'));
|
|
3066
|
+
agents.push({
|
|
3067
|
+
file: file.replace('.json', ''),
|
|
3068
|
+
id: agent.id || file.replace('.json', ''),
|
|
3069
|
+
role: agent.role || 'unspecified',
|
|
3070
|
+
model: agent.model || 'default',
|
|
3071
|
+
triggers: agent.triggers?.length || 0,
|
|
3072
|
+
prompt: agent.prompt || null,
|
|
3073
|
+
output: agent.output || null,
|
|
3074
|
+
});
|
|
3075
|
+
} catch (err) {
|
|
3076
|
+
// Skip invalid JSON files
|
|
3077
|
+
console.error(chalk.yellow(`Warning: Could not parse ${file}: ${err.message}`));
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// JSON output
|
|
3082
|
+
if (options.json) {
|
|
3083
|
+
console.log(JSON.stringify({ agents, error: null }, null, 2));
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
// Human-readable output
|
|
3088
|
+
console.log(chalk.bold('\nAvailable agent definitions:\n'));
|
|
3089
|
+
|
|
3090
|
+
for (const agent of agents) {
|
|
3091
|
+
console.log(
|
|
3092
|
+
` ${chalk.cyan(agent.id.padEnd(25))} ${chalk.dim('role:')} ${agent.role.padEnd(20)} ${chalk.dim('model:')} ${agent.model}`
|
|
3093
|
+
);
|
|
3094
|
+
|
|
3095
|
+
if (options.verbose) {
|
|
3096
|
+
console.log(chalk.dim(` Triggers: ${agent.triggers}`));
|
|
3097
|
+
if (agent.output) {
|
|
3098
|
+
console.log(chalk.dim(` Output topic: ${agent.output.topic || 'none'}`));
|
|
3099
|
+
}
|
|
3100
|
+
if (agent.prompt) {
|
|
3101
|
+
const promptPreview = agent.prompt.substring(0, 100).replace(/\n/g, ' ');
|
|
3102
|
+
console.log(chalk.dim(` Prompt: ${promptPreview}...`));
|
|
3103
|
+
}
|
|
3104
|
+
console.log('');
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
if (!options.verbose) {
|
|
3109
|
+
console.log('');
|
|
3110
|
+
console.log(chalk.dim(' Use --verbose for full details'));
|
|
3111
|
+
}
|
|
3112
|
+
console.log('');
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
if (options.json) {
|
|
3115
|
+
console.log(JSON.stringify({ agents: [], error: error.message }, null, 2));
|
|
3116
|
+
} else {
|
|
3117
|
+
console.error(chalk.red(`Error listing agents: ${error.message}`));
|
|
3118
|
+
}
|
|
3119
|
+
process.exit(1);
|
|
3120
|
+
}
|
|
3121
|
+
});
|
|
3122
|
+
|
|
3123
|
+
agentsCmd
|
|
3124
|
+
.command('show <name>')
|
|
3125
|
+
.description('Show detailed agent definition')
|
|
3126
|
+
.option('--json', 'Output as JSON')
|
|
3127
|
+
.action((name, options) => {
|
|
3128
|
+
try {
|
|
3129
|
+
const agentsDir = path.join(PACKAGE_ROOT, 'src', 'agents');
|
|
3130
|
+
|
|
3131
|
+
// Support both with and without .json extension
|
|
3132
|
+
const agentName = name.endsWith('.json') ? name : `${name}.json`;
|
|
3133
|
+
const agentPath = path.join(agentsDir, agentName);
|
|
3134
|
+
|
|
3135
|
+
if (!fs.existsSync(agentPath)) {
|
|
3136
|
+
// Try with -agent.json suffix
|
|
3137
|
+
const altPath = path.join(agentsDir, `${name}-agent.json`);
|
|
3138
|
+
if (fs.existsSync(altPath)) {
|
|
3139
|
+
const agent = JSON.parse(fs.readFileSync(altPath, 'utf8'));
|
|
3140
|
+
outputAgent(agent, options);
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
if (options.json) {
|
|
3145
|
+
console.log(JSON.stringify({ error: `Agent not found: ${name}` }, null, 2));
|
|
3146
|
+
} else {
|
|
3147
|
+
console.error(chalk.red(`Agent not found: ${name}`));
|
|
3148
|
+
console.log(chalk.dim('\nAvailable agents:'));
|
|
3149
|
+
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.json'));
|
|
3150
|
+
files.forEach((f) => console.log(chalk.dim(` - ${f.replace('.json', '')}`)));
|
|
3151
|
+
}
|
|
3152
|
+
process.exit(1);
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
const agent = JSON.parse(fs.readFileSync(agentPath, 'utf8'));
|
|
3156
|
+
outputAgent(agent, options);
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
if (options.json) {
|
|
3159
|
+
console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
3160
|
+
} else {
|
|
3161
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
3162
|
+
}
|
|
3163
|
+
process.exit(1);
|
|
3164
|
+
}
|
|
3165
|
+
});
|
|
3166
|
+
|
|
3167
|
+
function outputAgent(agent, options) {
|
|
3168
|
+
if (options.json) {
|
|
3169
|
+
console.log(JSON.stringify(agent, null, 2));
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// Human-readable output
|
|
3174
|
+
console.log('');
|
|
3175
|
+
console.log(chalk.bold.cyan('═'.repeat(80)));
|
|
3176
|
+
console.log(chalk.bold.cyan(` Agent: ${agent.id}`));
|
|
3177
|
+
console.log(chalk.bold.cyan('═'.repeat(80)));
|
|
3178
|
+
console.log('');
|
|
3179
|
+
|
|
3180
|
+
// Basic info
|
|
3181
|
+
console.log(chalk.bold('Configuration:'));
|
|
3182
|
+
console.log(` ${chalk.dim('ID:')} ${agent.id}`);
|
|
3183
|
+
console.log(` ${chalk.dim('Role:')} ${agent.role || 'unspecified'}`);
|
|
3184
|
+
console.log(` ${chalk.dim('Model:')} ${agent.model || 'default'}`);
|
|
3185
|
+
console.log('');
|
|
3186
|
+
|
|
3187
|
+
// Triggers
|
|
3188
|
+
if (agent.triggers && agent.triggers.length > 0) {
|
|
3189
|
+
console.log(chalk.bold('Triggers:'));
|
|
3190
|
+
for (const trigger of agent.triggers) {
|
|
3191
|
+
console.log(` ${chalk.yellow('•')} Topic: ${chalk.cyan(trigger.topic)}`);
|
|
3192
|
+
if (trigger.action) {
|
|
3193
|
+
console.log(` Action: ${trigger.action}`);
|
|
3194
|
+
}
|
|
3195
|
+
if (trigger.logic?.script) {
|
|
3196
|
+
const scriptPreview = trigger.logic.script.substring(0, 80).replace(/\n/g, ' ');
|
|
3197
|
+
console.log(chalk.dim(` Logic: ${scriptPreview}...`));
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
console.log('');
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// Output
|
|
3204
|
+
if (agent.output) {
|
|
3205
|
+
console.log(chalk.bold('Output:'));
|
|
3206
|
+
console.log(` ${chalk.dim('Topic:')} ${agent.output.topic || 'none'}`);
|
|
3207
|
+
if (agent.output.publishAfter) {
|
|
3208
|
+
console.log(` ${chalk.dim('Publish after:')} ${agent.output.publishAfter}`);
|
|
3209
|
+
}
|
|
3210
|
+
console.log('');
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// Prompt
|
|
3214
|
+
if (agent.prompt) {
|
|
3215
|
+
console.log(chalk.bold('Prompt:'));
|
|
3216
|
+
console.log(chalk.dim('─'.repeat(76)));
|
|
3217
|
+
// Show first 500 chars of prompt
|
|
3218
|
+
const promptLines = agent.prompt.substring(0, 500).split('\n');
|
|
3219
|
+
for (const line of promptLines) {
|
|
3220
|
+
console.log(` ${line}`);
|
|
3221
|
+
}
|
|
3222
|
+
if (agent.prompt.length > 500) {
|
|
3223
|
+
console.log(chalk.dim(` ... (${agent.prompt.length - 500} more characters)`));
|
|
3224
|
+
}
|
|
3225
|
+
console.log(chalk.dim('─'.repeat(76)));
|
|
3226
|
+
console.log('');
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3008
3230
|
// Helper function to keep the process alive for follow mode
|
|
3009
3231
|
function keepProcessAlive(cleanupFn) {
|
|
3010
3232
|
// Prevent Node.js from exiting by keeping the event loop active
|
|
@@ -4094,23 +4316,39 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
|
|
|
4094
4316
|
formatGenericMessage(msg, prefix, timestamp, safePrint);
|
|
4095
4317
|
}
|
|
4096
4318
|
|
|
4097
|
-
//
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
//
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4319
|
+
// Main async entry point
|
|
4320
|
+
async function main() {
|
|
4321
|
+
// First-run setup wizard (blocks on first use only)
|
|
4322
|
+
const isQuiet = process.argv.includes('-q') || process.argv.includes('--quiet');
|
|
4323
|
+
await checkFirstRun({ quiet: isQuiet });
|
|
4324
|
+
|
|
4325
|
+
// Check for updates (non-blocking if offline)
|
|
4326
|
+
await checkForUpdates({ quiet: isQuiet });
|
|
4327
|
+
|
|
4328
|
+
// Default command handling: if first arg doesn't match a known command, treat it as 'run'
|
|
4329
|
+
// This allows `zeroshot "task"` to work the same as `zeroshot run "task"`
|
|
4330
|
+
const args = process.argv.slice(2);
|
|
4331
|
+
if (args.length > 0) {
|
|
4332
|
+
const firstArg = args[0];
|
|
4333
|
+
|
|
4334
|
+
// Skip if it's a flag/option (starts with -)
|
|
4335
|
+
// Skip if it's --help or --version (these are handled by commander)
|
|
4336
|
+
if (!firstArg.startsWith('-')) {
|
|
4337
|
+
// Get all registered command names
|
|
4338
|
+
const commandNames = program.commands.map((cmd) => cmd.name());
|
|
4339
|
+
|
|
4340
|
+
// If first arg is not a known command, prepend 'run'
|
|
4341
|
+
if (!commandNames.includes(firstArg)) {
|
|
4342
|
+
process.argv.splice(2, 0, 'run');
|
|
4343
|
+
}
|
|
4112
4344
|
}
|
|
4113
4345
|
}
|
|
4346
|
+
|
|
4347
|
+
program.parse();
|
|
4114
4348
|
}
|
|
4115
4349
|
|
|
4116
|
-
|
|
4350
|
+
// Run main
|
|
4351
|
+
main().catch((err) => {
|
|
4352
|
+
console.error('Fatal error:', err.message);
|
|
4353
|
+
process.exit(1);
|
|
4354
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-Run Setup Wizard
|
|
3
|
+
*
|
|
4
|
+
* Interactive setup on first use:
|
|
5
|
+
* - Welcome banner
|
|
6
|
+
* - Default model selection (sonnet/opus/haiku)
|
|
7
|
+
* - Auto-update preference
|
|
8
|
+
* - Marks setup as complete
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const readline = require('readline');
|
|
12
|
+
const { loadSettings, saveSettings } = require('../../lib/settings');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Print welcome banner
|
|
16
|
+
*/
|
|
17
|
+
function printWelcome() {
|
|
18
|
+
console.log(`
|
|
19
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
20
|
+
║ ║
|
|
21
|
+
║ Welcome to Zeroshot! ║
|
|
22
|
+
║ Multi-agent orchestration for Claude ║
|
|
23
|
+
║ ║
|
|
24
|
+
║ Let's configure a few settings to get started. ║
|
|
25
|
+
║ ║
|
|
26
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create readline interface
|
|
32
|
+
* @returns {readline.Interface}
|
|
33
|
+
*/
|
|
34
|
+
function createReadline() {
|
|
35
|
+
return readline.createInterface({
|
|
36
|
+
input: process.stdin,
|
|
37
|
+
output: process.stdout,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Prompt for model selection
|
|
43
|
+
* @param {readline.Interface} rl
|
|
44
|
+
* @returns {Promise<string>}
|
|
45
|
+
*/
|
|
46
|
+
function promptModel(rl) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
console.log('Which Claude model should agents use by default?\n');
|
|
49
|
+
console.log(' 1) sonnet - Fast & capable (recommended)');
|
|
50
|
+
console.log(' 2) opus - Most capable, slower');
|
|
51
|
+
console.log(' 3) haiku - Fastest, for simple tasks\n');
|
|
52
|
+
|
|
53
|
+
rl.question('Enter 1, 2, or 3 [1]: ', (answer) => {
|
|
54
|
+
const choice = answer.trim() || '1';
|
|
55
|
+
switch (choice) {
|
|
56
|
+
case '2':
|
|
57
|
+
resolve('opus');
|
|
58
|
+
break;
|
|
59
|
+
case '3':
|
|
60
|
+
resolve('haiku');
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
resolve('sonnet');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prompt for auto-update preference
|
|
71
|
+
* @param {readline.Interface} rl
|
|
72
|
+
* @returns {Promise<boolean>}
|
|
73
|
+
*/
|
|
74
|
+
function promptAutoUpdate(rl) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
console.log('\nWould you like zeroshot to check for updates automatically?');
|
|
77
|
+
console.log('(Checks npm registry every 24 hours)\n');
|
|
78
|
+
|
|
79
|
+
rl.question('Enable auto-update checks? [Y/n]: ', (answer) => {
|
|
80
|
+
const normalized = answer.trim().toLowerCase();
|
|
81
|
+
// Default to yes if empty or starts with 'y'
|
|
82
|
+
resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Print completion message
|
|
89
|
+
* @param {object} settings - Saved settings
|
|
90
|
+
*/
|
|
91
|
+
function printComplete(settings) {
|
|
92
|
+
console.log(`
|
|
93
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
94
|
+
║ Setup complete! ║
|
|
95
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
96
|
+
|
|
97
|
+
Your settings:
|
|
98
|
+
• Default model: ${settings.defaultModel}
|
|
99
|
+
• Auto-updates: ${settings.autoCheckUpdates ? 'enabled' : 'disabled'}
|
|
100
|
+
|
|
101
|
+
Change anytime with: zeroshot settings set <key> <value>
|
|
102
|
+
|
|
103
|
+
Get started:
|
|
104
|
+
zeroshot run "Fix the bug in auth.js"
|
|
105
|
+
zeroshot run 123 (GitHub issue number)
|
|
106
|
+
zeroshot --help
|
|
107
|
+
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if first-run setup is needed
|
|
113
|
+
* @param {object} settings - Current settings
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
function detectFirstRun(settings) {
|
|
117
|
+
return !settings.firstRunComplete;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Main entry point - run first-time setup if needed
|
|
122
|
+
* @param {object} options
|
|
123
|
+
* @param {boolean} options.quiet - Skip interactive prompts
|
|
124
|
+
* @returns {Promise<boolean>} True if setup was run
|
|
125
|
+
*/
|
|
126
|
+
async function checkFirstRun(options = {}) {
|
|
127
|
+
const settings = loadSettings();
|
|
128
|
+
|
|
129
|
+
// Already completed setup
|
|
130
|
+
if (!detectFirstRun(settings)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Quiet mode - use defaults, mark complete
|
|
135
|
+
if (options.quiet) {
|
|
136
|
+
settings.firstRunComplete = true;
|
|
137
|
+
saveSettings(settings);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Interactive setup
|
|
142
|
+
printWelcome();
|
|
143
|
+
|
|
144
|
+
const rl = createReadline();
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Model selection
|
|
148
|
+
const model = await promptModel(rl);
|
|
149
|
+
settings.defaultModel = model;
|
|
150
|
+
|
|
151
|
+
// Auto-update preference
|
|
152
|
+
const autoUpdate = await promptAutoUpdate(rl);
|
|
153
|
+
settings.autoCheckUpdates = autoUpdate;
|
|
154
|
+
|
|
155
|
+
// Mark complete
|
|
156
|
+
settings.firstRunComplete = true;
|
|
157
|
+
saveSettings(settings);
|
|
158
|
+
|
|
159
|
+
// Print completion
|
|
160
|
+
printComplete(settings);
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
} finally {
|
|
164
|
+
rl.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
checkFirstRun,
|
|
170
|
+
// Exported for testing
|
|
171
|
+
detectFirstRun,
|
|
172
|
+
printWelcome,
|
|
173
|
+
printComplete,
|
|
174
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Checker - Checks npm registry for newer versions
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - 24-hour check interval (avoids registry spam)
|
|
6
|
+
* - 5-second timeout (non-blocking if offline)
|
|
7
|
+
* - Interactive prompt for manual update
|
|
8
|
+
* - Respects quiet mode (no prompts in CI/scripts)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
|
+
const readline = require('readline');
|
|
14
|
+
const { loadSettings, saveSettings } = require('../../lib/settings');
|
|
15
|
+
|
|
16
|
+
// 24 hours in milliseconds
|
|
17
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
// Timeout for npm registry fetch (5 seconds)
|
|
20
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
21
|
+
|
|
22
|
+
// npm registry URL
|
|
23
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@covibes/zeroshot/latest';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get current package version
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function getCurrentVersion() {
|
|
30
|
+
const pkg = require('../../package.json');
|
|
31
|
+
return pkg.version;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compare semver versions
|
|
36
|
+
* @param {string} current - Current version (e.g., "1.5.0")
|
|
37
|
+
* @param {string} latest - Latest version (e.g., "1.6.0")
|
|
38
|
+
* @returns {boolean} True if latest > current
|
|
39
|
+
*/
|
|
40
|
+
function isNewerVersion(current, latest) {
|
|
41
|
+
const currentParts = current.split('.').map(Number);
|
|
42
|
+
const latestParts = latest.split('.').map(Number);
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < 3; i++) {
|
|
45
|
+
const c = currentParts[i] || 0;
|
|
46
|
+
const l = latestParts[i] || 0;
|
|
47
|
+
if (l > c) return true;
|
|
48
|
+
if (l < c) return false;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetch latest version from npm registry
|
|
55
|
+
* @returns {Promise<string|null>} Latest version or null on failure
|
|
56
|
+
*/
|
|
57
|
+
function fetchLatestVersion() {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const req = https.get(REGISTRY_URL, { timeout: FETCH_TIMEOUT_MS }, (res) => {
|
|
60
|
+
if (res.statusCode !== 200) {
|
|
61
|
+
resolve(null);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let data = '';
|
|
66
|
+
res.on('data', (chunk) => {
|
|
67
|
+
data += chunk;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
res.on('end', () => {
|
|
71
|
+
try {
|
|
72
|
+
const json = JSON.parse(data);
|
|
73
|
+
resolve(json.version || null);
|
|
74
|
+
} catch {
|
|
75
|
+
resolve(null);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
req.on('error', () => {
|
|
81
|
+
resolve(null);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
req.on('timeout', () => {
|
|
85
|
+
req.destroy();
|
|
86
|
+
resolve(null);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Additional safety timeout
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
req.destroy();
|
|
92
|
+
resolve(null);
|
|
93
|
+
}, FETCH_TIMEOUT_MS + 1000);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Prompt user for update confirmation
|
|
99
|
+
* @param {string} currentVersion
|
|
100
|
+
* @param {string} latestVersion
|
|
101
|
+
* @returns {Promise<boolean>} True if user wants to update
|
|
102
|
+
*/
|
|
103
|
+
function promptForUpdate(currentVersion, latestVersion) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const rl = readline.createInterface({
|
|
106
|
+
input: process.stdin,
|
|
107
|
+
output: process.stdout,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(`\n📦 Update available: ${currentVersion} → ${latestVersion}`);
|
|
111
|
+
rl.question(' Install now? [y/N] ', (answer) => {
|
|
112
|
+
rl.close();
|
|
113
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Run npm install to update the package
|
|
120
|
+
* @returns {Promise<boolean>} True if update succeeded
|
|
121
|
+
*/
|
|
122
|
+
function runUpdate() {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
console.log('\n📥 Installing update...');
|
|
125
|
+
|
|
126
|
+
const proc = spawn('npm', ['install', '-g', '@covibes/zeroshot@latest'], {
|
|
127
|
+
stdio: 'inherit',
|
|
128
|
+
shell: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
proc.on('close', (code) => {
|
|
132
|
+
if (code === 0) {
|
|
133
|
+
console.log('✅ Update installed successfully!');
|
|
134
|
+
console.log(' Restart zeroshot to use the new version.\n');
|
|
135
|
+
resolve(true);
|
|
136
|
+
} else {
|
|
137
|
+
console.log('❌ Update failed. Try manually:');
|
|
138
|
+
console.log(' npm install -g @covibes/zeroshot@latest\n');
|
|
139
|
+
resolve(false);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
proc.on('error', () => {
|
|
144
|
+
console.log('❌ Update failed. Try manually:');
|
|
145
|
+
console.log(' npm install -g @covibes/zeroshot@latest\n');
|
|
146
|
+
resolve(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if update check should run
|
|
153
|
+
* @param {object} settings - Current settings
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
function shouldCheckForUpdates(settings) {
|
|
157
|
+
// Disabled by user
|
|
158
|
+
if (!settings.autoCheckUpdates) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Never checked before
|
|
163
|
+
if (!settings.lastUpdateCheckAt) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check if 24 hours have passed
|
|
168
|
+
const elapsed = Date.now() - settings.lastUpdateCheckAt;
|
|
169
|
+
return elapsed >= CHECK_INTERVAL_MS;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Main entry point - check for updates
|
|
174
|
+
* @param {object} options
|
|
175
|
+
* @param {boolean} options.quiet - Skip interactive prompts
|
|
176
|
+
* @returns {Promise<void>}
|
|
177
|
+
*/
|
|
178
|
+
async function checkForUpdates(options = {}) {
|
|
179
|
+
const settings = loadSettings();
|
|
180
|
+
|
|
181
|
+
// Skip check if not due
|
|
182
|
+
if (!shouldCheckForUpdates(settings)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const currentVersion = getCurrentVersion();
|
|
187
|
+
const latestVersion = await fetchLatestVersion();
|
|
188
|
+
|
|
189
|
+
// Update last check timestamp regardless of result
|
|
190
|
+
settings.lastUpdateCheckAt = Date.now();
|
|
191
|
+
saveSettings(settings);
|
|
192
|
+
|
|
193
|
+
// Network failure - silently skip
|
|
194
|
+
if (!latestVersion) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// No update available
|
|
199
|
+
if (!isNewerVersion(currentVersion, latestVersion)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Already notified about this version
|
|
204
|
+
if (settings.lastSeenVersion === latestVersion) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Update lastSeenVersion so we don't nag about the same version
|
|
209
|
+
settings.lastSeenVersion = latestVersion;
|
|
210
|
+
saveSettings(settings);
|
|
211
|
+
|
|
212
|
+
// Quiet mode - just inform, no prompt
|
|
213
|
+
if (options.quiet) {
|
|
214
|
+
console.log(`📦 Update available: ${currentVersion} → ${latestVersion}`);
|
|
215
|
+
console.log(' Run: npm install -g @covibes/zeroshot@latest\n');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Interactive mode - prompt for update
|
|
220
|
+
const wantsUpdate = await promptForUpdate(currentVersion, latestVersion);
|
|
221
|
+
if (wantsUpdate) {
|
|
222
|
+
await runUpdate();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
checkForUpdates,
|
|
228
|
+
// Exported for testing
|
|
229
|
+
getCurrentVersion,
|
|
230
|
+
isNewerVersion,
|
|
231
|
+
fetchLatestVersion,
|
|
232
|
+
shouldCheckForUpdates,
|
|
233
|
+
CHECK_INTERVAL_MS,
|
|
234
|
+
};
|
package/lib/settings.js
CHANGED
|
@@ -7,8 +7,14 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Get settings file path (dynamically reads env var for testing)
|
|
12
|
+
* Using a getter ensures tests can override the path at runtime
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function getSettingsFile() {
|
|
16
|
+
return process.env.ZEROSHOT_SETTINGS_FILE || path.join(os.homedir(), '.zeroshot', 'settings.json');
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
// Default settings
|
|
14
20
|
const DEFAULT_SETTINGS = {
|
|
@@ -17,17 +23,24 @@ const DEFAULT_SETTINGS = {
|
|
|
17
23
|
defaultIsolation: false,
|
|
18
24
|
strictSchema: true, // true = reliable json output (default), false = live streaming (may crash - see bold-meadow-11)
|
|
19
25
|
logLevel: 'normal',
|
|
26
|
+
// Auto-update settings
|
|
27
|
+
autoCheckUpdates: true, // Check npm registry for newer versions
|
|
28
|
+
lastUpdateCheckAt: null, // Unix timestamp of last check (null = never checked)
|
|
29
|
+
lastSeenVersion: null, // Don't re-prompt for same version
|
|
30
|
+
// First-run wizard
|
|
31
|
+
firstRunComplete: false, // Has user completed first-run setup?
|
|
20
32
|
};
|
|
21
33
|
|
|
22
34
|
/**
|
|
23
35
|
* Load settings from disk, merging with defaults
|
|
24
36
|
*/
|
|
25
37
|
function loadSettings() {
|
|
26
|
-
|
|
38
|
+
const settingsFile = getSettingsFile();
|
|
39
|
+
if (!fs.existsSync(settingsFile)) {
|
|
27
40
|
return { ...DEFAULT_SETTINGS };
|
|
28
41
|
}
|
|
29
42
|
try {
|
|
30
|
-
const data = fs.readFileSync(
|
|
43
|
+
const data = fs.readFileSync(settingsFile, 'utf8');
|
|
31
44
|
return { ...DEFAULT_SETTINGS, ...JSON.parse(data) };
|
|
32
45
|
} catch {
|
|
33
46
|
console.error('Warning: Could not load settings, using defaults');
|
|
@@ -39,11 +52,12 @@ function loadSettings() {
|
|
|
39
52
|
* Save settings to disk
|
|
40
53
|
*/
|
|
41
54
|
function saveSettings(settings) {
|
|
42
|
-
const
|
|
55
|
+
const settingsFile = getSettingsFile();
|
|
56
|
+
const dir = path.dirname(settingsFile);
|
|
43
57
|
if (!fs.existsSync(dir)) {
|
|
44
58
|
fs.mkdirSync(dir, { recursive: true });
|
|
45
59
|
}
|
|
46
|
-
fs.writeFileSync(
|
|
60
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), 'utf8');
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
/**
|
|
@@ -93,5 +107,9 @@ module.exports = {
|
|
|
93
107
|
validateSetting,
|
|
94
108
|
coerceValue,
|
|
95
109
|
DEFAULT_SETTINGS,
|
|
96
|
-
|
|
110
|
+
getSettingsFile,
|
|
111
|
+
// Backward compatibility: SETTINGS_FILE as getter (reads env var dynamically)
|
|
112
|
+
get SETTINGS_FILE() {
|
|
113
|
+
return getSettingsFile();
|
|
114
|
+
},
|
|
97
115
|
};
|