@covibes/zeroshot 5.2.1 → 5.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/CHANGELOG.md +174 -189
- package/README.md +199 -248
- package/cli/commands/providers.js +150 -0
- package/cli/index.js +214 -58
- package/cli/lib/first-run.js +40 -3
- package/cluster-templates/base-templates/debug-workflow.json +24 -78
- package/cluster-templates/base-templates/full-workflow.json +44 -145
- package/cluster-templates/base-templates/single-worker.json +23 -15
- package/cluster-templates/base-templates/worker-validator.json +47 -34
- package/cluster-templates/conductor-bootstrap.json +7 -5
- package/lib/docker-config.js +6 -1
- package/lib/provider-detection.js +59 -0
- package/lib/provider-names.js +56 -0
- package/lib/settings.js +191 -6
- package/lib/stream-json-parser.js +4 -238
- package/package.json +21 -5
- package/scripts/validate-templates.js +100 -0
- package/src/agent/agent-config.js +37 -13
- package/src/agent/agent-context-builder.js +64 -2
- package/src/agent/agent-hook-executor.js +82 -9
- package/src/agent/agent-lifecycle.js +53 -14
- package/src/agent/agent-task-executor.js +196 -194
- package/src/agent/output-extraction.js +200 -0
- package/src/agent/output-reformatter.js +175 -0
- package/src/agent/schema-utils.js +111 -0
- package/src/agent-wrapper.js +102 -30
- package/src/agents/git-pusher-agent.json +1 -1
- package/src/claude-task-runner.js +80 -30
- package/src/config-router.js +13 -13
- package/src/config-validator.js +231 -10
- package/src/github.js +36 -0
- package/src/isolation-manager.js +243 -154
- package/src/ledger.js +28 -6
- package/src/orchestrator.js +391 -96
- package/src/preflight.js +85 -82
- package/src/providers/anthropic/cli-builder.js +45 -0
- package/src/providers/anthropic/index.js +134 -0
- package/src/providers/anthropic/models.js +23 -0
- package/src/providers/anthropic/output-parser.js +159 -0
- package/src/providers/base-provider.js +181 -0
- package/src/providers/capabilities.js +51 -0
- package/src/providers/google/cli-builder.js +55 -0
- package/src/providers/google/index.js +116 -0
- package/src/providers/google/models.js +24 -0
- package/src/providers/google/output-parser.js +92 -0
- package/src/providers/index.js +75 -0
- package/src/providers/openai/cli-builder.js +122 -0
- package/src/providers/openai/index.js +135 -0
- package/src/providers/openai/models.js +21 -0
- package/src/providers/openai/output-parser.js +129 -0
- package/src/sub-cluster-wrapper.js +18 -3
- package/src/task-runner.js +8 -6
- package/src/tui/layout.js +20 -3
- package/task-lib/attachable-watcher.js +80 -78
- package/task-lib/claude-recovery.js +119 -0
- package/task-lib/commands/list.js +1 -1
- package/task-lib/commands/resume.js +3 -2
- package/task-lib/commands/run.js +12 -3
- package/task-lib/runner.js +59 -38
- package/task-lib/scheduler.js +2 -2
- package/task-lib/store.js +43 -30
- package/task-lib/watcher.js +81 -62
package/lib/settings.js
CHANGED
|
@@ -7,6 +7,11 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const { validateMountConfig, validateEnvPassthrough } = require('./docker-config');
|
|
10
|
+
const {
|
|
11
|
+
VALID_PROVIDERS,
|
|
12
|
+
normalizeProviderName,
|
|
13
|
+
normalizeProviderSettings,
|
|
14
|
+
} = require('./provider-names');
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Get settings file path (dynamically reads env var for testing)
|
|
@@ -14,7 +19,9 @@ const { validateMountConfig, validateEnvPassthrough } = require('./docker-config
|
|
|
14
19
|
* @returns {string}
|
|
15
20
|
*/
|
|
16
21
|
function getSettingsFile() {
|
|
17
|
-
return
|
|
22
|
+
return (
|
|
23
|
+
process.env.ZEROSHOT_SETTINGS_FILE || path.join(os.homedir(), '.zeroshot', 'settings.json')
|
|
24
|
+
);
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
/**
|
|
@@ -28,15 +35,17 @@ const MODEL_HIERARCHY = {
|
|
|
28
35
|
};
|
|
29
36
|
|
|
30
37
|
const VALID_MODELS = Object.keys(MODEL_HIERARCHY);
|
|
38
|
+
const LEVEL_RANKS = { level1: 1, level2: 2, level3: 3 };
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
|
-
* Validate a requested model against the maxModel ceiling
|
|
41
|
+
* Validate a requested model against the maxModel ceiling and minModel floor
|
|
34
42
|
* @param {string} requestedModel - Model the agent wants to use
|
|
35
43
|
* @param {string} maxModel - Maximum allowed model (cost ceiling)
|
|
44
|
+
* @param {string|null} minModel - Minimum required model (cost floor)
|
|
36
45
|
* @returns {string} The validated model
|
|
37
|
-
* @throws {Error} If requested model exceeds ceiling
|
|
46
|
+
* @throws {Error} If requested model exceeds ceiling or falls below floor
|
|
38
47
|
*/
|
|
39
|
-
function validateModelAgainstMax(requestedModel, maxModel) {
|
|
48
|
+
function validateModelAgainstMax(requestedModel, maxModel, minModel = null) {
|
|
40
49
|
if (!requestedModel) return maxModel; // Default to ceiling if unspecified
|
|
41
50
|
|
|
42
51
|
if (!VALID_MODELS.includes(requestedModel)) {
|
|
@@ -52,12 +61,50 @@ function validateModelAgainstMax(requestedModel, maxModel) {
|
|
|
52
61
|
`Either lower agent's model or raise maxModel.`
|
|
53
62
|
);
|
|
54
63
|
}
|
|
64
|
+
|
|
65
|
+
if (minModel) {
|
|
66
|
+
if (!VALID_MODELS.includes(minModel)) {
|
|
67
|
+
throw new Error(`Invalid minModel "${minModel}". Valid: ${VALID_MODELS.join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
if (MODEL_HIERARCHY[minModel] > MODEL_HIERARCHY[maxModel]) {
|
|
70
|
+
throw new Error(`minModel "${minModel}" cannot be higher than maxModel "${maxModel}".`);
|
|
71
|
+
}
|
|
72
|
+
if (MODEL_HIERARCHY[requestedModel] < MODEL_HIERARCHY[minModel]) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Agent requests "${requestedModel}" but minModel is "${minModel}". ` +
|
|
75
|
+
`Either raise agent's model or lower minModel.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
return requestedModel;
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
// Default settings
|
|
59
84
|
const DEFAULT_SETTINGS = {
|
|
60
85
|
maxModel: 'opus', // Cost ceiling - agents cannot use models above this
|
|
86
|
+
minModel: null, // Cost floor - agents cannot use models below this (null = no minimum)
|
|
87
|
+
defaultProvider: 'claude',
|
|
88
|
+
providerSettings: {
|
|
89
|
+
claude: {
|
|
90
|
+
maxLevel: 'level3',
|
|
91
|
+
minLevel: 'level1',
|
|
92
|
+
defaultLevel: 'level2',
|
|
93
|
+
levelOverrides: {},
|
|
94
|
+
},
|
|
95
|
+
codex: {
|
|
96
|
+
maxLevel: 'level3',
|
|
97
|
+
minLevel: 'level1',
|
|
98
|
+
defaultLevel: 'level2',
|
|
99
|
+
levelOverrides: {},
|
|
100
|
+
},
|
|
101
|
+
gemini: {
|
|
102
|
+
maxLevel: 'level3',
|
|
103
|
+
minLevel: 'level1',
|
|
104
|
+
defaultLevel: 'level2',
|
|
105
|
+
levelOverrides: {},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
61
108
|
defaultConfig: 'conductor-bootstrap',
|
|
62
109
|
defaultDocker: false,
|
|
63
110
|
strictSchema: true, // true = reliable json output (default), false = live streaming (may crash - see bold-meadow-11)
|
|
@@ -70,7 +117,7 @@ const DEFAULT_SETTINGS = {
|
|
|
70
117
|
// Example: 'ccr code' for claude-code-router integration
|
|
71
118
|
claudeCommand: 'claude',
|
|
72
119
|
// Docker isolation mounts - preset names or {host, container, readonly?} objects
|
|
73
|
-
// Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud
|
|
120
|
+
// Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini
|
|
74
121
|
dockerMounts: ['gh', 'git', 'ssh'],
|
|
75
122
|
// Extra env vars to pass to Docker container (in addition to preset-implied ones)
|
|
76
123
|
// Supports: VAR (if set), VAR_* (pattern), VAR=value (forced), VAR= (empty)
|
|
@@ -80,6 +127,75 @@ const DEFAULT_SETTINGS = {
|
|
|
80
127
|
dockerContainerHome: '/home/node',
|
|
81
128
|
};
|
|
82
129
|
|
|
130
|
+
function mapLegacyModelToLevel(model) {
|
|
131
|
+
switch (model) {
|
|
132
|
+
case 'haiku':
|
|
133
|
+
return 'level1';
|
|
134
|
+
case 'sonnet':
|
|
135
|
+
return 'level2';
|
|
136
|
+
case 'opus':
|
|
137
|
+
return 'level3';
|
|
138
|
+
default:
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function mergeProviderSettings(current, overrides) {
|
|
144
|
+
const merged = { ...current };
|
|
145
|
+
for (const provider of VALID_PROVIDERS) {
|
|
146
|
+
merged[provider] = {
|
|
147
|
+
...current[provider],
|
|
148
|
+
...(overrides?.[provider] || {}),
|
|
149
|
+
};
|
|
150
|
+
if (!merged[provider].levelOverrides) {
|
|
151
|
+
merged[provider].levelOverrides = {};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function applyLegacyModelBounds(settings) {
|
|
158
|
+
if (!settings.providerSettings) return settings;
|
|
159
|
+
const claude = settings.providerSettings.claude || {};
|
|
160
|
+
const legacyMaxLevel = mapLegacyModelToLevel(settings.maxModel);
|
|
161
|
+
const legacyMinLevel = mapLegacyModelToLevel(settings.minModel);
|
|
162
|
+
|
|
163
|
+
if (legacyMaxLevel) {
|
|
164
|
+
claude.maxLevel = legacyMaxLevel;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (legacyMinLevel) {
|
|
168
|
+
claude.minLevel = legacyMinLevel;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const minRank = LEVEL_RANKS[claude.minLevel] || LEVEL_RANKS.level1;
|
|
172
|
+
const maxRank = LEVEL_RANKS[claude.maxLevel] || LEVEL_RANKS.level3;
|
|
173
|
+
const defaultRank = LEVEL_RANKS[claude.defaultLevel] || LEVEL_RANKS.level2;
|
|
174
|
+
|
|
175
|
+
if (minRank > maxRank) {
|
|
176
|
+
claude.minLevel = 'level1';
|
|
177
|
+
claude.maxLevel = 'level3';
|
|
178
|
+
} else if (defaultRank < minRank) {
|
|
179
|
+
claude.defaultLevel = claude.minLevel;
|
|
180
|
+
} else if (defaultRank > maxRank) {
|
|
181
|
+
claude.defaultLevel = claude.maxLevel;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
settings.providerSettings.claude = claude;
|
|
185
|
+
return settings;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeLoadedSettings(parsed) {
|
|
189
|
+
const normalized = { ...parsed };
|
|
190
|
+
if (parsed.defaultProvider) {
|
|
191
|
+
normalized.defaultProvider = normalizeProviderName(parsed.defaultProvider);
|
|
192
|
+
}
|
|
193
|
+
if (parsed.providerSettings) {
|
|
194
|
+
normalized.providerSettings = normalizeProviderSettings(parsed.providerSettings);
|
|
195
|
+
}
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
|
|
83
199
|
/**
|
|
84
200
|
* Load settings from disk, merging with defaults
|
|
85
201
|
*/
|
|
@@ -90,7 +206,15 @@ function loadSettings() {
|
|
|
90
206
|
}
|
|
91
207
|
try {
|
|
92
208
|
const data = fs.readFileSync(settingsFile, 'utf8');
|
|
93
|
-
|
|
209
|
+
const parsed = normalizeLoadedSettings(JSON.parse(data));
|
|
210
|
+
const merged = { ...DEFAULT_SETTINGS, ...parsed };
|
|
211
|
+
merged.defaultProvider =
|
|
212
|
+
normalizeProviderName(merged.defaultProvider) || DEFAULT_SETTINGS.defaultProvider;
|
|
213
|
+
merged.providerSettings = mergeProviderSettings(
|
|
214
|
+
DEFAULT_SETTINGS.providerSettings,
|
|
215
|
+
parsed.providerSettings
|
|
216
|
+
);
|
|
217
|
+
return applyLegacyModelBounds(merged);
|
|
94
218
|
} catch {
|
|
95
219
|
console.error('Warning: Could not load settings, using defaults');
|
|
96
220
|
return { ...DEFAULT_SETTINGS };
|
|
@@ -122,6 +246,10 @@ function validateSetting(key, value) {
|
|
|
122
246
|
return `Invalid model: ${value}. Valid models: ${VALID_MODELS.join(', ')}`;
|
|
123
247
|
}
|
|
124
248
|
|
|
249
|
+
if (key === 'minModel' && value !== null && !VALID_MODELS.includes(value)) {
|
|
250
|
+
return `Invalid model: ${value}. Valid models: ${VALID_MODELS.join(', ')}, null`;
|
|
251
|
+
}
|
|
252
|
+
|
|
125
253
|
if (key === 'logLevel' && !['quiet', 'normal', 'verbose'].includes(value)) {
|
|
126
254
|
return `Invalid log level: ${value}. Valid levels: quiet, normal, verbose`;
|
|
127
255
|
}
|
|
@@ -135,6 +263,43 @@ function validateSetting(key, value) {
|
|
|
135
263
|
}
|
|
136
264
|
}
|
|
137
265
|
|
|
266
|
+
if (key === 'defaultProvider') {
|
|
267
|
+
const normalized = normalizeProviderName(value);
|
|
268
|
+
if (!VALID_PROVIDERS.includes(normalized)) {
|
|
269
|
+
return `Invalid provider: ${value}. Valid providers: ${VALID_PROVIDERS.join(', ')}`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (key === 'providerSettings') {
|
|
274
|
+
const normalizedSettings = normalizeProviderSettings(value);
|
|
275
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
276
|
+
return 'providerSettings must be an object';
|
|
277
|
+
}
|
|
278
|
+
for (const [provider, settings] of Object.entries(normalizedSettings || {})) {
|
|
279
|
+
if (!VALID_PROVIDERS.includes(provider)) {
|
|
280
|
+
return `Unknown provider in providerSettings: ${provider}`;
|
|
281
|
+
}
|
|
282
|
+
if (typeof settings !== 'object' || settings === null) {
|
|
283
|
+
return `providerSettings.${provider} must be an object`;
|
|
284
|
+
}
|
|
285
|
+
if (settings.maxLevel && !LEVEL_RANKS[settings.maxLevel]) {
|
|
286
|
+
return `Invalid maxLevel for ${provider}: ${settings.maxLevel}`;
|
|
287
|
+
}
|
|
288
|
+
if (settings.minLevel && !LEVEL_RANKS[settings.minLevel]) {
|
|
289
|
+
return `Invalid minLevel for ${provider}: ${settings.minLevel}`;
|
|
290
|
+
}
|
|
291
|
+
if (settings.defaultLevel && !LEVEL_RANKS[settings.defaultLevel]) {
|
|
292
|
+
return `Invalid defaultLevel for ${provider}: ${settings.defaultLevel}`;
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
settings.levelOverrides &&
|
|
296
|
+
(typeof settings.levelOverrides !== 'object' || Array.isArray(settings.levelOverrides))
|
|
297
|
+
) {
|
|
298
|
+
return `levelOverrides for ${provider} must be an object`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
138
303
|
if (key === 'dockerMounts') {
|
|
139
304
|
return validateMountConfig(value);
|
|
140
305
|
}
|
|
@@ -152,6 +317,11 @@ function validateSetting(key, value) {
|
|
|
152
317
|
function coerceValue(key, value) {
|
|
153
318
|
const defaultValue = DEFAULT_SETTINGS[key];
|
|
154
319
|
|
|
320
|
+
// Handle null values for minModel
|
|
321
|
+
if (key === 'minModel' && (value === 'null' || value === null)) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
155
325
|
if (typeof defaultValue === 'boolean') {
|
|
156
326
|
return value === 'true' || value === '1' || value === 'yes' || value === true;
|
|
157
327
|
}
|
|
@@ -183,6 +353,21 @@ function coerceValue(key, value) {
|
|
|
183
353
|
return value;
|
|
184
354
|
}
|
|
185
355
|
|
|
356
|
+
if (key === 'providerSettings') {
|
|
357
|
+
if (typeof value === 'string') {
|
|
358
|
+
try {
|
|
359
|
+
return normalizeProviderSettings(JSON.parse(value));
|
|
360
|
+
} catch {
|
|
361
|
+
throw new Error(`Invalid JSON for providerSettings: ${value}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return normalizeProviderSettings(value);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (key === 'defaultProvider') {
|
|
368
|
+
return normalizeProviderName(value);
|
|
369
|
+
}
|
|
370
|
+
|
|
186
371
|
return value;
|
|
187
372
|
}
|
|
188
373
|
|
|
@@ -1,244 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Parses NDJSON (newline-delimited JSON) streaming output from Claude Code.
|
|
5
|
-
* Extracts: text output, tool calls, tool results, thinking, errors.
|
|
6
|
-
*
|
|
7
|
-
* Event types from Claude Code stream-json format:
|
|
8
|
-
* - system: Session initialization
|
|
9
|
-
* - stream_event: Real-time streaming (content_block_start, content_block_delta, etc.)
|
|
10
|
-
* - assistant: Complete assistant message with content array
|
|
11
|
-
* - user: Tool results
|
|
12
|
-
* - result: Final task result
|
|
2
|
+
* Compatibility wrapper for Claude stream-json parsing.
|
|
3
|
+
* Prefer provider-specific parsers in src/providers.
|
|
13
4
|
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse a single JSON line and extract displayable content
|
|
17
|
-
* @param {string} line - Single line of NDJSON
|
|
18
|
-
* @returns {Object|null} Parsed event with type and content, or null if not displayable
|
|
19
|
-
*/
|
|
20
|
-
function parseStreamLine(line) {
|
|
21
|
-
const trimmed = line.trim();
|
|
22
|
-
if (!trimmed || !trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let event;
|
|
27
|
-
try {
|
|
28
|
-
event = JSON.parse(trimmed);
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// stream_event - real-time streaming updates
|
|
34
|
-
if (event.type === 'stream_event' && event.event) {
|
|
35
|
-
return parseStreamEvent(event.event);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// assistant - complete message with content blocks
|
|
39
|
-
if (event.type === 'assistant' && event.message?.content) {
|
|
40
|
-
return parseAssistantMessage(event.message);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// user - tool result
|
|
44
|
-
if (event.type === 'user' && event.message?.content) {
|
|
45
|
-
return parseUserMessage(event.message);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// result - final task result (includes token usage and cost)
|
|
49
|
-
if (event.type === 'result') {
|
|
50
|
-
const usage = event.usage || {};
|
|
51
|
-
return {
|
|
52
|
-
type: 'result',
|
|
53
|
-
success: event.subtype === 'success',
|
|
54
|
-
result: event.result,
|
|
55
|
-
error: event.is_error ? event.result : null,
|
|
56
|
-
cost: event.total_cost_usd,
|
|
57
|
-
duration: event.duration_ms,
|
|
58
|
-
// Token usage from Claude API
|
|
59
|
-
inputTokens: usage.input_tokens || 0,
|
|
60
|
-
outputTokens: usage.output_tokens || 0,
|
|
61
|
-
cacheReadInputTokens: usage.cache_read_input_tokens || 0,
|
|
62
|
-
cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
|
|
63
|
-
// Per-model breakdown (for multi-model tasks)
|
|
64
|
-
modelUsage: event.modelUsage || null,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// system - session init (skip, not user-facing)
|
|
69
|
-
if (event.type === 'system') {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Parse stream_event inner event
|
|
78
|
-
*/
|
|
79
|
-
function parseStreamEvent(inner) {
|
|
80
|
-
// content_block_start - tool use or text block starting
|
|
81
|
-
if (inner.type === 'content_block_start' && inner.content_block) {
|
|
82
|
-
const block = inner.content_block;
|
|
83
|
-
|
|
84
|
-
if (block.type === 'tool_use') {
|
|
85
|
-
return {
|
|
86
|
-
type: 'tool_start',
|
|
87
|
-
toolName: block.name,
|
|
88
|
-
toolId: block.id,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (block.type === 'thinking') {
|
|
93
|
-
return {
|
|
94
|
-
type: 'thinking_start',
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// text block start - usually empty, skip
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// content_block_delta - incremental content
|
|
103
|
-
if (inner.type === 'content_block_delta' && inner.delta) {
|
|
104
|
-
const delta = inner.delta;
|
|
105
|
-
|
|
106
|
-
if (delta.type === 'text_delta' && delta.text) {
|
|
107
|
-
return {
|
|
108
|
-
type: 'text',
|
|
109
|
-
text: delta.text,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (delta.type === 'thinking_delta' && delta.thinking) {
|
|
114
|
-
return {
|
|
115
|
-
type: 'thinking',
|
|
116
|
-
text: delta.thinking,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (delta.type === 'input_json_delta' && delta.partial_json) {
|
|
121
|
-
return {
|
|
122
|
-
type: 'tool_input',
|
|
123
|
-
json: delta.partial_json,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// content_block_stop - block ended
|
|
129
|
-
if (inner.type === 'content_block_stop') {
|
|
130
|
-
return {
|
|
131
|
-
type: 'block_end',
|
|
132
|
-
index: inner.index,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// message_start - skip text extraction here since it will come via text_delta events
|
|
137
|
-
// (extracting here causes duplicate text output)
|
|
138
|
-
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Parse assistant message content blocks
|
|
144
|
-
* NOTE: Skip text blocks here since they were already streamed via text_delta events.
|
|
145
|
-
* Extracting text here would cause duplicate output.
|
|
146
|
-
*/
|
|
147
|
-
function parseAssistantMessage(message) {
|
|
148
|
-
const results = [];
|
|
149
|
-
|
|
150
|
-
for (const block of message.content) {
|
|
151
|
-
// Skip text blocks - already streamed via text_delta events
|
|
152
|
-
// Extracting here causes duplicate output
|
|
153
|
-
|
|
154
|
-
if (block.type === 'tool_use') {
|
|
155
|
-
results.push({
|
|
156
|
-
type: 'tool_call',
|
|
157
|
-
toolName: block.name,
|
|
158
|
-
toolId: block.id,
|
|
159
|
-
input: block.input,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (block.type === 'thinking' && block.thinking) {
|
|
164
|
-
// Skip thinking blocks too - already streamed via thinking_delta
|
|
165
|
-
// But keep for non-streaming contexts (direct API responses)
|
|
166
|
-
// Only emit if we haven't seen streaming deltas (detected by having results already)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (results.length === 1) {
|
|
171
|
-
return results[0];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (results.length > 1) {
|
|
175
|
-
return {
|
|
176
|
-
type: 'multi',
|
|
177
|
-
events: results,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Parse user message (tool results)
|
|
186
|
-
*/
|
|
187
|
-
function parseUserMessage(message) {
|
|
188
|
-
const results = [];
|
|
189
|
-
|
|
190
|
-
for (const block of message.content) {
|
|
191
|
-
if (block.type === 'tool_result') {
|
|
192
|
-
results.push({
|
|
193
|
-
type: 'tool_result',
|
|
194
|
-
toolId: block.tool_use_id,
|
|
195
|
-
content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
|
|
196
|
-
isError: block.is_error || false,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (results.length === 1) {
|
|
202
|
-
return results[0];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (results.length > 1) {
|
|
206
|
-
return {
|
|
207
|
-
type: 'multi',
|
|
208
|
-
events: results,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Parse multiple lines of NDJSON
|
|
217
|
-
* @param {string} chunk - Chunk of text potentially containing multiple JSON lines
|
|
218
|
-
* @returns {Array} Array of parsed events
|
|
219
|
-
*/
|
|
220
|
-
function parseChunk(chunk) {
|
|
221
|
-
const events = [];
|
|
222
|
-
const lines = chunk.split('\n');
|
|
223
|
-
|
|
224
|
-
for (const line of lines) {
|
|
225
|
-
const event = parseStreamLine(line);
|
|
226
|
-
if (event) {
|
|
227
|
-
if (event.type === 'multi') {
|
|
228
|
-
events.push(...event.events);
|
|
229
|
-
} else {
|
|
230
|
-
events.push(event);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return events;
|
|
236
|
-
}
|
|
5
|
+
const { parseEvent, parseChunk } = require('../src/providers/anthropic/output-parser');
|
|
237
6
|
|
|
238
7
|
module.exports = {
|
|
239
|
-
|
|
8
|
+
parseEvent,
|
|
240
9
|
parseChunk,
|
|
241
|
-
parseStreamEvent,
|
|
242
|
-
parseAssistantMessage,
|
|
243
|
-
parseUserMessage,
|
|
244
10
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@covibes/zeroshot",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "Multi-agent orchestration engine for Claude
|
|
3
|
+
"version": "5.3.0",
|
|
4
|
+
"description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
|
|
5
5
|
"main": "src/orchestrator.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zeroshot": "./cli/index.js"
|
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
"test": "tests"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "mocha 'tests/**/*.test.js'",
|
|
15
|
-
"test:
|
|
16
|
-
"test:
|
|
14
|
+
"test": "mocha 'tests/unit/**/*.test.js' 'tests/*.test.js' --parallel",
|
|
15
|
+
"test:unit": "mocha 'tests/unit/**/*.test.js' 'tests/*.test.js' --parallel",
|
|
16
|
+
"test:slow": "mocha 'tests/integration/**/*.test.js' --timeout 180000",
|
|
17
|
+
"test:all": "npm run test && npm run test:slow",
|
|
18
|
+
"test:coverage": "c8 npm run test:unit",
|
|
19
|
+
"test:coverage:report": "c8 --reporter=html npm run test:unit && echo 'Coverage report generated at coverage/index.html'",
|
|
17
20
|
"postinstall": "node scripts/fix-node-pty-permissions.js",
|
|
18
21
|
"start": "node cli/index.js",
|
|
19
22
|
"typecheck": "tsc --noEmit",
|
|
20
23
|
"lint": "eslint .",
|
|
21
24
|
"lint:fix": "eslint . --fix",
|
|
25
|
+
"validate:templates": "node scripts/validate-templates.js",
|
|
22
26
|
"format": "prettier --write .",
|
|
23
27
|
"format:check": "prettier --check .",
|
|
24
28
|
"deadcode": "ts-prune --skip node_modules",
|
|
@@ -55,6 +59,8 @@
|
|
|
55
59
|
"multi-agent",
|
|
56
60
|
"orchestration",
|
|
57
61
|
"claude",
|
|
62
|
+
"codex",
|
|
63
|
+
"gemini",
|
|
58
64
|
"automation"
|
|
59
65
|
],
|
|
60
66
|
"author": "Covibes",
|
|
@@ -115,7 +121,9 @@
|
|
|
115
121
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
116
122
|
"husky": "^9.1.7",
|
|
117
123
|
"jscpd": "^3.5.10",
|
|
124
|
+
"lint-staged": "^16.2.7",
|
|
118
125
|
"mocha": "^11.7.5",
|
|
126
|
+
"prettier": "^3.7.4",
|
|
119
127
|
"semantic-release": "^25.0.2",
|
|
120
128
|
"sinon": "^21.0.0",
|
|
121
129
|
"ts-prune": "^0.10.3",
|
|
@@ -124,5 +132,13 @@
|
|
|
124
132
|
},
|
|
125
133
|
"overrides": {
|
|
126
134
|
"xml2js": "^0.5.0"
|
|
135
|
+
},
|
|
136
|
+
"lint-staged": {
|
|
137
|
+
"*.{js,mjs,cjs}": [
|
|
138
|
+
"eslint --fix"
|
|
139
|
+
],
|
|
140
|
+
"*.{js,mjs,cjs,json,md}": [
|
|
141
|
+
"prettier --write"
|
|
142
|
+
]
|
|
127
143
|
}
|
|
128
144
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate all cluster templates for config errors
|
|
4
|
+
* Run in CI to prevent broken templates from being merged
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/validate-templates.js
|
|
7
|
+
* Exit codes: 0 = all valid, 1 = validation errors found
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { validateConfig } = require('../src/config-validator');
|
|
13
|
+
|
|
14
|
+
const TEMPLATES_DIR = path.join(__dirname, '../cluster-templates');
|
|
15
|
+
|
|
16
|
+
function findJsonFiles(dir) {
|
|
17
|
+
const files = [];
|
|
18
|
+
if (!fs.existsSync(dir)) return files;
|
|
19
|
+
|
|
20
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const fullPath = path.join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
files.push(...findJsonFiles(fullPath));
|
|
24
|
+
} else if (entry.name.endsWith('.json')) {
|
|
25
|
+
files.push(fullPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateTemplate(filePath) {
|
|
32
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
36
|
+
const config = JSON.parse(content);
|
|
37
|
+
|
|
38
|
+
// Skip non-cluster configs (like package.json)
|
|
39
|
+
if (!config.agents && !config.name) {
|
|
40
|
+
return { valid: true, skipped: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = validateConfig(config);
|
|
44
|
+
|
|
45
|
+
if (!result.valid) {
|
|
46
|
+
console.error(`\n❌ ${relativePath}`);
|
|
47
|
+
for (const error of result.errors) {
|
|
48
|
+
console.error(` ERROR: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
} else if (result.warnings.length > 0) {
|
|
51
|
+
console.warn(`\n⚠️ ${relativePath}`);
|
|
52
|
+
for (const warning of result.warnings) {
|
|
53
|
+
console.warn(` WARN: ${warning}`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
console.log(`✓ ${relativePath}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`\n❌ ${relativePath}`);
|
|
62
|
+
console.error(` PARSE ERROR: ${err.message}`);
|
|
63
|
+
return { valid: false, errors: [err.message], warnings: [] };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function main() {
|
|
68
|
+
console.log('Validating cluster templates...\n');
|
|
69
|
+
|
|
70
|
+
const templateFiles = [...findJsonFiles(TEMPLATES_DIR)];
|
|
71
|
+
|
|
72
|
+
let hasErrors = false;
|
|
73
|
+
let validated = 0;
|
|
74
|
+
let skipped = 0;
|
|
75
|
+
|
|
76
|
+
for (const file of templateFiles) {
|
|
77
|
+
const result = validateTemplate(file);
|
|
78
|
+
if (result.skipped) {
|
|
79
|
+
skipped++;
|
|
80
|
+
} else {
|
|
81
|
+
validated++;
|
|
82
|
+
if (!result.valid) {
|
|
83
|
+
hasErrors = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
89
|
+
console.log(`Validated: ${validated} templates, Skipped: ${skipped} files`);
|
|
90
|
+
|
|
91
|
+
if (hasErrors) {
|
|
92
|
+
console.error('\n❌ VALIDATION FAILED - Fix errors above before merging\n');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
} else {
|
|
95
|
+
console.log('\n✓ All templates valid\n');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main();
|