@covibes/zeroshot 5.3.0 → 5.4.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.
Files changed (82) hide show
  1. package/README.md +94 -14
  2. package/cli/commands/providers.js +8 -9
  3. package/cli/index.js +3032 -2409
  4. package/cli/message-formatters-normal.js +28 -6
  5. package/cluster-templates/base-templates/debug-workflow.json +1 -1
  6. package/cluster-templates/base-templates/full-workflow.json +72 -188
  7. package/cluster-templates/base-templates/worker-validator.json +59 -3
  8. package/cluster-templates/conductor-bootstrap.json +4 -4
  9. package/lib/docker-config.js +8 -0
  10. package/lib/git-remote-utils.js +165 -0
  11. package/lib/id-detector.js +10 -7
  12. package/lib/provider-defaults.js +62 -0
  13. package/lib/provider-names.js +2 -1
  14. package/lib/settings/claude-auth.js +78 -0
  15. package/lib/settings.js +161 -63
  16. package/package.json +7 -2
  17. package/scripts/setup-merge-queue.sh +170 -0
  18. package/src/agent/agent-config.js +135 -82
  19. package/src/agent/agent-context-builder.js +297 -188
  20. package/src/agent/agent-hook-executor.js +310 -113
  21. package/src/agent/agent-lifecycle.js +385 -325
  22. package/src/agent/agent-stuck-detector.js +7 -7
  23. package/src/agent/agent-task-executor.js +824 -565
  24. package/src/agent/output-extraction.js +41 -24
  25. package/src/agent/output-reformatter.js +1 -1
  26. package/src/agent/schema-utils.js +108 -73
  27. package/src/agent-wrapper.js +10 -1
  28. package/src/agents/git-pusher-template.js +285 -0
  29. package/src/claude-task-runner.js +85 -34
  30. package/src/config-validator.js +922 -657
  31. package/src/input-helpers.js +65 -0
  32. package/src/isolation-manager.js +289 -199
  33. package/src/issue-providers/README.md +305 -0
  34. package/src/issue-providers/azure-devops-provider.js +273 -0
  35. package/src/issue-providers/base-provider.js +232 -0
  36. package/src/issue-providers/github-provider.js +179 -0
  37. package/src/issue-providers/gitlab-provider.js +241 -0
  38. package/src/issue-providers/index.js +196 -0
  39. package/src/issue-providers/jira-provider.js +239 -0
  40. package/src/ledger.js +22 -5
  41. package/src/lib/safe-exec.js +88 -0
  42. package/src/orchestrator.js +1107 -811
  43. package/src/preflight.js +313 -159
  44. package/src/process-metrics.js +98 -56
  45. package/src/providers/anthropic/cli-builder.js +53 -25
  46. package/src/providers/anthropic/index.js +72 -2
  47. package/src/providers/anthropic/output-parser.js +32 -14
  48. package/src/providers/base-provider.js +70 -0
  49. package/src/providers/capabilities.js +9 -0
  50. package/src/providers/google/output-parser.js +47 -38
  51. package/src/providers/index.js +19 -3
  52. package/src/providers/openai/cli-builder.js +14 -3
  53. package/src/providers/openai/index.js +1 -0
  54. package/src/providers/openai/output-parser.js +44 -30
  55. package/src/providers/opencode/cli-builder.js +42 -0
  56. package/src/providers/opencode/index.js +103 -0
  57. package/src/providers/opencode/models.js +55 -0
  58. package/src/providers/opencode/output-parser.js +122 -0
  59. package/src/schemas/sub-cluster.js +68 -39
  60. package/src/status-footer.js +94 -48
  61. package/src/sub-cluster-wrapper.js +76 -35
  62. package/src/template-resolver.js +12 -9
  63. package/src/tui/data-poller.js +123 -99
  64. package/src/tui/formatters.js +4 -3
  65. package/src/tui/keybindings.js +259 -318
  66. package/src/tui/renderer.js +11 -21
  67. package/task-lib/attachable-watcher.js +118 -81
  68. package/task-lib/claude-recovery.js +61 -24
  69. package/task-lib/commands/episodes.js +105 -0
  70. package/task-lib/commands/list.js +2 -2
  71. package/task-lib/commands/logs.js +231 -189
  72. package/task-lib/commands/schedules.js +111 -61
  73. package/task-lib/config.js +0 -2
  74. package/task-lib/runner.js +84 -27
  75. package/task-lib/scheduler.js +1 -1
  76. package/task-lib/store.js +467 -168
  77. package/task-lib/tui/formatters.js +94 -45
  78. package/task-lib/tui/renderer.js +13 -3
  79. package/task-lib/tui.js +73 -32
  80. package/task-lib/watcher.js +128 -90
  81. package/src/agents/git-pusher-agent.json +0 -20
  82. package/src/github.js +0 -139
@@ -13,7 +13,7 @@
13
13
  * - macOS: ps + lsof
14
14
  */
15
15
 
16
- const { execSync } = require('child_process');
16
+ const { execSync } = require('./lib/safe-exec'); // Enforces timeouts
17
17
  const fs = require('fs');
18
18
 
19
19
  const PLATFORM = process.platform;
@@ -56,41 +56,13 @@ function getChildPids(pid) {
56
56
  const children = [];
57
57
 
58
58
  try {
59
- if (PLATFORM === 'darwin') {
60
- // macOS: Use pgrep with -P flag
61
- const output = execSync(`pgrep -P ${escapeShell(String(pid))} 2>/dev/null`, {
62
- encoding: 'utf8',
63
- timeout: 2000,
64
- });
65
- const pids = output.trim().split('\n').filter(Boolean).map(Number);
66
- children.push(...pids);
67
-
68
- // Recursively get grandchildren
69
- for (const childPid of pids) {
70
- children.push(...getChildPids(childPid));
71
- }
72
- } else {
73
- // Linux: Read /proc/{pid}/task/{tid}/children
74
- const taskPath = `/proc/${pid}/task`;
75
- if (fs.existsSync(taskPath)) {
76
- const tids = fs.readdirSync(taskPath);
77
- for (const tid of tids) {
78
- const childrenPath = `/proc/${pid}/task/${tid}/children`;
79
- if (fs.existsSync(childrenPath)) {
80
- const childPids = fs.readFileSync(childrenPath, 'utf8')
81
- .trim()
82
- .split(/\s+/)
83
- .filter(Boolean)
84
- .map(Number);
85
- children.push(...childPids);
86
-
87
- // Recursively get grandchildren
88
- for (const childPid of childPids) {
89
- children.push(...getChildPids(childPid));
90
- }
91
- }
92
- }
93
- }
59
+ const childPids =
60
+ PLATFORM === 'darwin' ? collectDarwinChildPids(pid) : collectLinuxChildPids(pid);
61
+ children.push(...childPids);
62
+
63
+ // Recursively get grandchildren
64
+ for (const childPid of childPids) {
65
+ children.push(...getChildPids(childPid));
94
66
  }
95
67
  } catch {
96
68
  // Ignore errors (process may have exited)
@@ -99,6 +71,45 @@ function getChildPids(pid) {
99
71
  return [...new Set(children)]; // Dedupe
100
72
  }
101
73
 
74
+ function collectDarwinChildPids(pid) {
75
+ const output = execSync(`pgrep -P ${escapeShell(String(pid))} 2>/dev/null`, {
76
+ encoding: 'utf8',
77
+ timeout: 2000,
78
+ });
79
+
80
+ return output.trim().split('\n').filter(Boolean).map(Number);
81
+ }
82
+
83
+ function collectLinuxChildPids(pid) {
84
+ const taskPath = `/proc/${pid}/task`;
85
+ if (!fs.existsSync(taskPath)) {
86
+ return [];
87
+ }
88
+
89
+ const tids = fs.readdirSync(taskPath);
90
+ const childPids = [];
91
+
92
+ for (const tid of tids) {
93
+ const childrenPath = `/proc/${pid}/task/${tid}/children`;
94
+ childPids.push(...readChildPidFile(childrenPath));
95
+ }
96
+
97
+ return childPids;
98
+ }
99
+
100
+ function readChildPidFile(childrenPath) {
101
+ if (!fs.existsSync(childrenPath)) {
102
+ return [];
103
+ }
104
+
105
+ const raw = fs.readFileSync(childrenPath, 'utf8').trim();
106
+ if (!raw) {
107
+ return [];
108
+ }
109
+
110
+ return raw.split(/\s+/).filter(Boolean).map(Number);
111
+ }
112
+
102
113
  /**
103
114
  * Get metrics for a single process (Linux)
104
115
  * @param {number} pid - Process ID
@@ -187,17 +198,20 @@ function getNetworkStateLinux(pid) {
187
198
  hasActivity: false,
188
199
  sendQueueBytes: 0,
189
200
  recvQueueBytes: 0,
190
- bytesSent: 0, // Cumulative bytes sent across all sockets
191
- bytesReceived: 0, // Cumulative bytes received across all sockets
201
+ bytesSent: 0, // Cumulative bytes sent across all sockets
202
+ bytesReceived: 0, // Cumulative bytes received across all sockets
192
203
  };
193
204
 
194
205
  try {
195
206
  // Use ss -tip to get extended TCP info with bytes_sent/bytes_received
196
207
  // -t = TCP only, -i = show internal TCP info, -p = show process
197
- const output = execSync(`ss -tip 2>/dev/null | grep -A1 "pid=${escapeShell(String(pid))}," || true`, {
198
- encoding: 'utf8',
199
- timeout: 3000,
200
- });
208
+ const output = execSync(
209
+ `ss -tip 2>/dev/null | grep -A1 "pid=${escapeShell(String(pid))}," || true`,
210
+ {
211
+ encoding: 'utf8',
212
+ timeout: 3000,
213
+ }
214
+ );
201
215
 
202
216
  if (!output.trim()) {
203
217
  return result;
@@ -261,8 +275,8 @@ function getNetworkStateDarwin(pid) {
261
275
  hasActivity: false,
262
276
  sendQueueBytes: 0,
263
277
  recvQueueBytes: 0,
264
- bytesSent: 0, // Not available on macOS without root/dtrace
265
- bytesReceived: 0, // Not available on macOS without root/dtrace
278
+ bytesSent: 0, // Not available on macOS without root/dtrace
279
+ bytesReceived: 0, // Not available on macOS without root/dtrace
266
280
  };
267
281
 
268
282
  try {
@@ -281,7 +295,7 @@ function getNetworkStateDarwin(pid) {
281
295
  for (const line of lines) {
282
296
  const parts = line.split(/\s+/);
283
297
  // Look for ESTABLISHED connections
284
- if (parts.includes('ESTABLISHED') || parts.some(p => p.includes('->'))) {
298
+ if (parts.includes('ESTABLISHED') || parts.some((p) => p.includes('->'))) {
285
299
  result.established++;
286
300
  result.hasActivity = true; // lsof doesn't show queue sizes, assume activity
287
301
  }
@@ -334,14 +348,21 @@ async function getProcessMetricsLinuxAggregated(pid, samplePeriodMs) {
334
348
  memoryMB: 0,
335
349
  state: 'X',
336
350
  threads: 0,
337
- network: { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 },
351
+ network: {
352
+ established: 0,
353
+ hasActivity: false,
354
+ sendQueueBytes: 0,
355
+ recvQueueBytes: 0,
356
+ bytesSent: 0,
357
+ bytesReceived: 0,
358
+ },
338
359
  childCount: 0,
339
360
  timestamp: Date.now(),
340
361
  };
341
362
  }
342
363
 
343
364
  // Wait for sample period
344
- await new Promise(r => setTimeout(r, samplePeriodMs));
365
+ await new Promise((r) => setTimeout(r, samplePeriodMs));
345
366
 
346
367
  // Get second CPU sample
347
368
  const t1Metrics = {};
@@ -385,7 +406,14 @@ async function getProcessMetricsLinuxAggregated(pid, samplePeriodMs) {
385
406
  const cpuPercent = Math.min(100, rawCpuPercent / cpuCores);
386
407
 
387
408
  // Get network state for all processes
388
- let network = { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 };
409
+ let network = {
410
+ established: 0,
411
+ hasActivity: false,
412
+ sendQueueBytes: 0,
413
+ recvQueueBytes: 0,
414
+ bytesSent: 0,
415
+ bytesReceived: 0,
416
+ };
389
417
  for (const p of Object.keys(t1Metrics)) {
390
418
  const netState = getNetworkStateLinux(parseInt(p, 10));
391
419
  network.established += netState.established;
@@ -451,7 +479,14 @@ function getProcessMetricsDarwinAggregated(pid) {
451
479
  }
452
480
 
453
481
  // Get network state
454
- let network = { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 };
482
+ let network = {
483
+ established: 0,
484
+ hasActivity: false,
485
+ sendQueueBytes: 0,
486
+ recvQueueBytes: 0,
487
+ bytesSent: 0,
488
+ bytesReceived: 0,
489
+ };
455
490
  for (const p of allPids) {
456
491
  const netState = getNetworkStateDarwin(p);
457
492
  network.established += netState.established;
@@ -518,13 +553,20 @@ function formatMetrics(metrics) {
518
553
  */
519
554
  function getStateIcon(state) {
520
555
  switch (state) {
521
- case 'R': return '🟢'; // Running
522
- case 'S': return '🔵'; // Sleeping
523
- case 'D': return '🟡'; // Disk wait
524
- case 'Z': return '💀'; // Zombie
525
- case 'T': return '⏸️'; // Stopped
526
- case 'X': return ''; // Dead
527
- default: return '';
556
+ case 'R':
557
+ return '🟢'; // Running
558
+ case 'S':
559
+ return '🔵'; // Sleeping
560
+ case 'D':
561
+ return '🟡'; // Disk wait
562
+ case 'Z':
563
+ return '💀'; // Zombie
564
+ case 'T':
565
+ return '⏸️'; // Stopped
566
+ case 'X':
567
+ return '❌'; // Dead
568
+ default:
569
+ return '⚪';
528
570
  }
529
571
  }
530
572
 
@@ -1,43 +1,71 @@
1
1
  function buildCommand(context, options = {}, commandConfig = {}) {
2
- const { modelSpec, outputFormat, jsonSchema, autoApprove, cliFeatures = {} } = options;
2
+ const {
3
+ modelSpec,
4
+ outputFormat,
5
+ jsonSchema,
6
+ autoApprove,
7
+ cliFeatures = {},
8
+ authEnv = {},
9
+ } = options;
3
10
 
4
11
  const command = commandConfig.command || 'claude';
5
12
  const extraArgs = commandConfig.args || [];
6
13
  const args = [...extraArgs, '--print', '--input-format', 'text'];
7
14
 
8
- if (outputFormat && cliFeatures.supportsOutputFormat !== false) {
9
- args.push('--output-format', outputFormat);
10
- }
15
+ addOutputFormatArgs(args, outputFormat, cliFeatures);
16
+ addStreamJsonArgs(args, outputFormat, cliFeatures);
17
+ addJsonSchemaArgs(args, outputFormat, jsonSchema, cliFeatures);
18
+ addModelArgs(args, modelSpec, cliFeatures);
19
+ addAutoApproveArgs(args, autoApprove, cliFeatures);
11
20
 
12
- if (outputFormat === 'stream-json') {
13
- if (cliFeatures.supportsVerbose !== false) {
14
- args.push('--verbose');
15
- }
16
- if (cliFeatures.supportsIncludePartials !== false) {
17
- args.push('--include-partial-messages');
18
- }
19
- }
21
+ args.push(context);
20
22
 
21
- if (jsonSchema && outputFormat === 'json' && cliFeatures.supportsJsonSchema !== false) {
22
- const schemaString = typeof jsonSchema === 'string' ? jsonSchema : JSON.stringify(jsonSchema);
23
- args.push('--json-schema', schemaString);
23
+ return {
24
+ binary: command,
25
+ args,
26
+ env: authEnv,
27
+ };
28
+ }
29
+
30
+ function addOutputFormatArgs(args, outputFormat, cliFeatures) {
31
+ if (!outputFormat || cliFeatures.supportsOutputFormat === false) {
32
+ return;
24
33
  }
34
+ args.push('--output-format', outputFormat);
35
+ }
25
36
 
26
- if (modelSpec?.model && cliFeatures.supportsModel !== false) {
27
- args.push('--model', modelSpec.model);
37
+ function addStreamJsonArgs(args, outputFormat, cliFeatures) {
38
+ if (outputFormat !== 'stream-json') {
39
+ return;
40
+ }
41
+ if (cliFeatures.supportsVerbose !== false) {
42
+ args.push('--verbose');
28
43
  }
44
+ if (cliFeatures.supportsIncludePartials !== false) {
45
+ args.push('--include-partial-messages');
46
+ }
47
+ }
29
48
 
30
- if (autoApprove && cliFeatures.supportsAutoApprove !== false) {
31
- args.push('--dangerously-skip-permissions');
49
+ function addJsonSchemaArgs(args, outputFormat, jsonSchema, cliFeatures) {
50
+ if (!jsonSchema || outputFormat !== 'json' || cliFeatures.supportsJsonSchema === false) {
51
+ return;
32
52
  }
53
+ const schemaString = typeof jsonSchema === 'string' ? jsonSchema : JSON.stringify(jsonSchema);
54
+ args.push('--json-schema', schemaString);
55
+ }
33
56
 
34
- args.push(context);
57
+ function addModelArgs(args, modelSpec, cliFeatures) {
58
+ if (!modelSpec?.model || cliFeatures.supportsModel === false) {
59
+ return;
60
+ }
61
+ args.push('--model', modelSpec.model);
62
+ }
35
63
 
36
- return {
37
- binary: command,
38
- args,
39
- env: {},
40
- };
64
+ function addAutoApproveArgs(args, autoApprove, cliFeatures) {
65
+ if (!autoApprove || cliFeatures.supportsAutoApprove === false) {
66
+ return;
67
+ }
68
+ args.push('--dangerously-skip-permissions');
41
69
  }
42
70
 
43
71
  module.exports = {
@@ -1,6 +1,7 @@
1
1
  const BaseProvider = require('../base-provider');
2
- const { getClaudeCommand } = require('../../../lib/settings');
2
+ const { getClaudeCommand, loadSettings } = require('../../../lib/settings');
3
3
  const { commandExists, getCommandPath, getHelpOutput } = require('../../../lib/provider-detection');
4
+ const { resolveClaudeAuth } = require('../../../lib/settings/claude-auth');
4
5
  const { buildCommand } = require('./cli-builder');
5
6
  const { parseEvent } = require('./output-parser');
6
7
  const {
@@ -68,6 +69,16 @@ class AnthropicProvider extends BaseProvider {
68
69
  return ['~/.claude'];
69
70
  }
70
71
 
72
+ /**
73
+ * Resolve authentication environment variables for Claude CLI.
74
+ * Handles Bedrock, API key, and OAuth authentication.
75
+ * @returns {Object} Environment variables for authentication
76
+ */
77
+ resolveAuthEnv() {
78
+ const settings = loadSettings();
79
+ return resolveClaudeAuth(settings);
80
+ }
81
+
71
82
  buildCommand(context, options) {
72
83
  const { command, args } = getClaudeCommand();
73
84
  const cliFeatures = options.cliFeatures || {};
@@ -97,7 +108,10 @@ class AnthropicProvider extends BaseProvider {
97
108
  );
98
109
  }
99
110
 
100
- return buildCommand(context, { ...options, cliFeatures }, { command, args });
111
+ const authEnv = this.resolveAuthEnv();
112
+ const resolvedOptions = { ...options, cliFeatures, authEnv };
113
+
114
+ return buildCommand(context, resolvedOptions, { command, args });
101
115
  }
102
116
 
103
117
  parseEvent(line) {
@@ -129,6 +143,62 @@ class AnthropicProvider extends BaseProvider {
129
143
  warned.add(key);
130
144
  console.warn(`⚠️ ${message}`);
131
145
  }
146
+
147
+ /**
148
+ * Get default settings including Claude-specific auth fields
149
+ * @override
150
+ */
151
+ getDefaultSettings() {
152
+ return {
153
+ ...super.getDefaultSettings(),
154
+ // Authentication (optional persistent storage)
155
+ anthropicApiKey: null, // sk-ant-* key
156
+ bedrockApiKey: null, // AWS_BEARER_TOKEN_BEDROCK value
157
+ bedrockRegion: null, // AWS_REGION for Bedrock
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Validate Claude-specific settings including auth fields
163
+ * @override
164
+ */
165
+ validateSettings(settings) {
166
+ // First validate base provider settings (levels, etc.)
167
+ const baseError = super.validateSettings(settings);
168
+ if (baseError) return baseError;
169
+
170
+ // Claude-specific auth field validation
171
+ const {
172
+ isValidAnthropicKey,
173
+ ANTHROPIC_KEY_PREFIX,
174
+ } = require('../../../lib/settings/claude-auth');
175
+
176
+ // Validate string-or-null fields
177
+ for (const field of ['anthropicApiKey', 'bedrockApiKey', 'bedrockRegion']) {
178
+ if (
179
+ settings[field] !== undefined &&
180
+ settings[field] !== null &&
181
+ typeof settings[field] !== 'string'
182
+ ) {
183
+ return `providerSettings.claude.${field} must be a string or null`;
184
+ }
185
+ }
186
+
187
+ // Additional prefix validation for Anthropic API key
188
+ if (settings.anthropicApiKey && !isValidAnthropicKey(settings.anthropicApiKey)) {
189
+ return `providerSettings.claude.anthropicApiKey must start with ${ANTHROPIC_KEY_PREFIX}`;
190
+ }
191
+
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * Get Claude-specific setting field names
197
+ * @override
198
+ */
199
+ getSettingsFields() {
200
+ return [...super.getSettingsFields(), 'anthropicApiKey', 'bedrockApiKey', 'bedrockRegion'];
201
+ }
132
202
  }
133
203
 
134
204
  module.exports = AnthropicProvider;
@@ -2,6 +2,28 @@
2
2
  * Stream JSON Parser for Claude Code output
3
3
  */
4
4
 
5
+ /**
6
+ * Parse result event type
7
+ * @param {Object} event
8
+ * @returns {Object}
9
+ */
10
+ function parseResultEvent(event) {
11
+ const usage = event.usage || {};
12
+ return {
13
+ type: 'result',
14
+ success: event.subtype === 'success',
15
+ result: event.result,
16
+ error: event.is_error ? event.result : null,
17
+ cost: event.total_cost_usd,
18
+ duration: event.duration_ms,
19
+ inputTokens: usage.input_tokens || 0,
20
+ outputTokens: usage.output_tokens || 0,
21
+ cacheReadInputTokens: usage.cache_read_input_tokens || 0,
22
+ cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
23
+ modelUsage: event.modelUsage || null,
24
+ };
25
+ }
26
+
5
27
  /**
6
28
  * Parse a single JSON line and extract displayable content
7
29
  * @param {string} line
@@ -33,20 +55,7 @@ function parseEvent(line) {
33
55
  }
34
56
 
35
57
  if (event.type === 'result') {
36
- const usage = event.usage || {};
37
- return {
38
- type: 'result',
39
- success: event.subtype === 'success',
40
- result: event.result,
41
- error: event.is_error ? event.result : null,
42
- cost: event.total_cost_usd,
43
- duration: event.duration_ms,
44
- inputTokens: usage.input_tokens || 0,
45
- outputTokens: usage.output_tokens || 0,
46
- cacheReadInputTokens: usage.cache_read_input_tokens || 0,
47
- cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
48
- modelUsage: event.modelUsage || null,
49
- };
58
+ return parseResultEvent(event);
50
59
  }
51
60
 
52
61
  if (event.type === 'system') {
@@ -82,6 +91,15 @@ function parseAssistantMessage(message) {
82
91
  const results = [];
83
92
 
84
93
  for (const block of message.content) {
94
+ // Handle text content blocks (CRITICAL for Haiku/weaker models that return JSON in text)
95
+ // Issue #52: Haiku returns JSON in type:text blocks, not in result wrapper
96
+ if (block.type === 'text' && block.text) {
97
+ results.push({
98
+ type: 'text',
99
+ text: block.text,
100
+ });
101
+ }
102
+
85
103
  if (block.type === 'tool_use') {
86
104
  results.push({
87
105
  type: 'tool_call',
@@ -173,9 +173,79 @@ class BaseProvider {
173
173
  return modelId;
174
174
  }
175
175
 
176
+ /**
177
+ * Resolve a model name to its CLI-compatible identifier.
178
+ * Override in provider implementations that need model ID transformation
179
+ * (e.g., Anthropic Bedrock mapping).
180
+ * @param {string} model - Model name (e.g., 'opus', 'sonnet')
181
+ * @param {Object} _authEnv - Authentication environment variables
182
+ * @returns {string} CLI-compatible model identifier
183
+ */
184
+ resolveCliModel(model, _authEnv = {}) {
185
+ return model;
186
+ }
187
+
176
188
  getDefaultLevel() {
177
189
  throw new Error('Not implemented');
178
190
  }
191
+
192
+ /**
193
+ * Get default settings for this provider
194
+ * Override in provider implementations to provide provider-specific defaults
195
+ * @returns {Object} Default settings object
196
+ */
197
+ getDefaultSettings() {
198
+ return {
199
+ maxLevel: this.getDefaultMaxLevel?.() || 'level3',
200
+ minLevel: this.getDefaultMinLevel?.() || 'level1',
201
+ defaultLevel: this.getDefaultLevel() || 'level2',
202
+ levelOverrides: {},
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Validate provider-specific settings
208
+ * Override in provider implementations to add custom validation
209
+ * @param {Object} settings - Settings object to validate
210
+ * @returns {string|null} Error message if invalid, null if valid
211
+ */
212
+ validateSettings(settings) {
213
+ if (typeof settings !== 'object' || settings === null) {
214
+ return `providerSettings.${this.name} must be an object`;
215
+ }
216
+
217
+ // Validate level fields
218
+ const levelMapping = this.getLevelMapping();
219
+ const validLevels = Object.keys(levelMapping);
220
+
221
+ if (settings.maxLevel && !validLevels.includes(settings.maxLevel)) {
222
+ return `Invalid maxLevel for ${this.name}: ${settings.maxLevel}`;
223
+ }
224
+ if (settings.minLevel && !validLevels.includes(settings.minLevel)) {
225
+ return `Invalid minLevel for ${this.name}: ${settings.minLevel}`;
226
+ }
227
+ if (settings.defaultLevel && !validLevels.includes(settings.defaultLevel)) {
228
+ return `Invalid defaultLevel for ${this.name}: ${settings.defaultLevel}`;
229
+ }
230
+
231
+ if (
232
+ settings.levelOverrides &&
233
+ (typeof settings.levelOverrides !== 'object' || Array.isArray(settings.levelOverrides))
234
+ ) {
235
+ return `levelOverrides for ${this.name} must be an object`;
236
+ }
237
+
238
+ return null;
239
+ }
240
+
241
+ /**
242
+ * Get the list of setting field names specific to this provider
243
+ * Override in provider implementations to declare custom fields
244
+ * @returns {string[]} Array of field names
245
+ */
246
+ getSettingsFields() {
247
+ return ['maxLevel', 'minLevel', 'defaultLevel', 'levelOverrides'];
248
+ }
179
249
  }
180
250
 
181
251
  module.exports = BaseProvider;
@@ -28,6 +28,15 @@ const CAPABILITIES = {
28
28
  thinkingMode: true,
29
29
  reasoningEffort: false,
30
30
  },
31
+ opencode: {
32
+ dockerIsolation: true,
33
+ worktreeIsolation: true,
34
+ mcpServers: true,
35
+ jsonSchema: 'experimental',
36
+ streamJson: true,
37
+ thinkingMode: true,
38
+ reasoningEffort: true,
39
+ },
31
40
  };
32
41
 
33
42
  function checkCapability(provider, capability) {
@@ -9,6 +9,47 @@ function normalizeMessageContent(content) {
9
9
  return '';
10
10
  }
11
11
 
12
+ function parseMessageEvent(event) {
13
+ if (event.role === 'assistant') {
14
+ const text = normalizeMessageContent(event.content);
15
+ if (text) {
16
+ return { type: 'text', text };
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function parseToolUseEvent(event, state) {
23
+ const toolId = event.tool_call_id || event.tool_id || event.id || state.lastToolId;
24
+ const toolName = event.tool_name || event.name;
25
+ state.lastToolId = toolId;
26
+ return {
27
+ type: 'tool_call',
28
+ toolName,
29
+ toolId,
30
+ input: event.parameters || event.input || {},
31
+ };
32
+ }
33
+
34
+ function parseToolResultEvent(event, state) {
35
+ const toolId = event.tool_call_id || event.tool_id || event.id || state.lastToolId;
36
+ return {
37
+ type: 'tool_result',
38
+ toolId,
39
+ content: event.output ?? event.content ?? '',
40
+ isError: event.success === false,
41
+ };
42
+ }
43
+
44
+ function parseResultEvent(event) {
45
+ return {
46
+ type: 'result',
47
+ success: event.success !== false,
48
+ result: event.result || '',
49
+ error: event.success === false ? event.error || 'Result failed' : null,
50
+ };
51
+ }
52
+
12
53
  function parseEvent(line, state = {}, options = {}) {
13
54
  let event;
14
55
  try {
@@ -20,46 +61,14 @@ function parseEvent(line, state = {}, options = {}) {
20
61
  switch (event.type) {
21
62
  case 'init':
22
63
  return null;
23
-
24
64
  case 'message':
25
- if (event.role === 'assistant') {
26
- const text = normalizeMessageContent(event.content);
27
- if (text) {
28
- return { type: 'text', text };
29
- }
30
- }
31
- return null;
32
-
33
- case 'tool_use': {
34
- const toolId = event.tool_call_id || event.tool_id || event.id || state.lastToolId;
35
- const toolName = event.tool_name || event.name;
36
- state.lastToolId = toolId;
37
- return {
38
- type: 'tool_call',
39
- toolName,
40
- toolId,
41
- input: event.parameters || event.input || {},
42
- };
43
- }
44
-
45
- case 'tool_result': {
46
- const toolId = event.tool_call_id || event.tool_id || event.id || state.lastToolId;
47
- return {
48
- type: 'tool_result',
49
- toolId,
50
- content: event.output ?? event.content ?? '',
51
- isError: event.success === false,
52
- };
53
- }
54
-
65
+ return parseMessageEvent(event);
66
+ case 'tool_use':
67
+ return parseToolUseEvent(event, state);
68
+ case 'tool_result':
69
+ return parseToolResultEvent(event, state);
55
70
  case 'result':
56
- return {
57
- type: 'result',
58
- success: event.success !== false,
59
- result: event.result || '',
60
- error: event.success === false ? event.error || 'Result failed' : null,
61
- };
62
-
71
+ return parseResultEvent(event);
63
72
  default:
64
73
  if (options.onUnknown) {
65
74
  options.onUnknown(event.type, event);