@covibes/zeroshot 5.2.1 → 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.
- package/CHANGELOG.md +174 -189
- package/README.md +226 -195
- package/cli/commands/providers.js +149 -0
- package/cli/index.js +3145 -2366
- package/cli/lib/first-run.js +40 -3
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +24 -78
- package/cluster-templates/base-templates/full-workflow.json +99 -316
- package/cluster-templates/base-templates/single-worker.json +23 -15
- package/cluster-templates/base-templates/worker-validator.json +105 -36
- package/cluster-templates/conductor-bootstrap.json +9 -7
- package/lib/docker-config.js +14 -1
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-detection.js +59 -0
- package/lib/provider-names.js +57 -0
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +298 -15
- package/lib/stream-json-parser.js +4 -238
- package/package.json +27 -6
- package/scripts/setup-merge-queue.sh +170 -0
- package/scripts/validate-templates.js +100 -0
- package/src/agent/agent-config.js +140 -63
- package/src/agent/agent-context-builder.js +336 -165
- package/src/agent/agent-hook-executor.js +337 -67
- package/src/agent/agent-lifecycle.js +386 -287
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +944 -683
- package/src/agent/output-extraction.js +217 -0
- package/src/agent/output-reformatter.js +175 -0
- package/src/agent/schema-utils.js +146 -0
- package/src/agent-wrapper.js +112 -31
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +145 -44
- package/src/config-router.js +13 -13
- package/src/config-validator.js +1049 -563
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +499 -320
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +50 -11
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1348 -757
- package/src/preflight.js +306 -149
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +73 -0
- package/src/providers/anthropic/index.js +204 -0
- package/src/providers/anthropic/models.js +23 -0
- package/src/providers/anthropic/output-parser.js +177 -0
- package/src/providers/base-provider.js +251 -0
- package/src/providers/capabilities.js +60 -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 +101 -0
- package/src/providers/index.js +91 -0
- package/src/providers/openai/cli-builder.js +133 -0
- package/src/providers/openai/index.js +136 -0
- package/src/providers/openai/models.js +21 -0
- package/src/providers/openai/output-parser.js +143 -0
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +92 -36
- package/src/task-runner.js +8 -6
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/layout.js +20 -3
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +150 -111
- package/task-lib/claude-recovery.js +156 -0
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +3 -3
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/resume.js +3 -2
- package/task-lib/commands/run.js +12 -3
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +128 -50
- package/task-lib/scheduler.js +3 -3
- package/task-lib/store.js +464 -152
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +157 -100
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -103
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OpenAI structured outputs have strict schema requirements:
|
|
7
|
+
* 1. additionalProperties: false on ALL object schemas
|
|
8
|
+
* 2. 'required' must include ALL keys in 'properties'
|
|
9
|
+
* This function recursively enforces both constraints.
|
|
10
|
+
* @param {Object} schema - JSON Schema object
|
|
11
|
+
* @returns {Object} - Modified schema compliant with OpenAI structured output requirements
|
|
12
|
+
*/
|
|
13
|
+
function enforceStrictSchema(schema) {
|
|
14
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
15
|
+
|
|
16
|
+
const result = { ...schema };
|
|
17
|
+
|
|
18
|
+
// Add additionalProperties: false to object types
|
|
19
|
+
// OpenAI also requires 'required' to include ALL property keys
|
|
20
|
+
if (result.type === 'object') {
|
|
21
|
+
result.additionalProperties = false;
|
|
22
|
+
if (result.properties) {
|
|
23
|
+
result.required = Object.keys(result.properties);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Recurse into properties
|
|
28
|
+
if (result.properties) {
|
|
29
|
+
result.properties = {};
|
|
30
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
31
|
+
result.properties[key] = enforceStrictSchema(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Recurse into items (arrays)
|
|
36
|
+
if (result.items) {
|
|
37
|
+
result.items = enforceStrictSchema(schema.items);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Recurse into anyOf/oneOf/allOf
|
|
41
|
+
for (const key of ['anyOf', 'oneOf', 'allOf']) {
|
|
42
|
+
if (Array.isArray(result[key])) {
|
|
43
|
+
result[key] = result[key].map(enforceStrictSchema);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Recurse into additionalProperties if it's a schema
|
|
48
|
+
if (result.additionalProperties && typeof result.additionalProperties === 'object') {
|
|
49
|
+
result.additionalProperties = enforceStrictSchema(result.additionalProperties);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildCommand(context, options = {}) {
|
|
56
|
+
const { modelSpec, outputFormat, jsonSchema, cwd, autoApprove, cliFeatures = {} } = options;
|
|
57
|
+
|
|
58
|
+
const args = ['exec'];
|
|
59
|
+
const cleanup = []; // Files to cleanup after command completes
|
|
60
|
+
|
|
61
|
+
if ((outputFormat === 'stream-json' || outputFormat === 'json') && cliFeatures.supportsJson) {
|
|
62
|
+
args.push('--json');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (modelSpec?.model) {
|
|
66
|
+
args.push('-m', modelSpec.model);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (modelSpec?.reasoningEffort && cliFeatures.supportsConfigOverride) {
|
|
70
|
+
args.push('--config', `model_reasoning_effort="${modelSpec.reasoningEffort}"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (cwd && cliFeatures.supportsCwd) {
|
|
74
|
+
args.push('-C', cwd);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (autoApprove && cliFeatures.supportsAutoApprove) {
|
|
78
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Always skip git repo check - zeroshot runs non-interactively and may run in any directory
|
|
82
|
+
if (cliFeatures.supportsSkipGitRepoCheck !== false) {
|
|
83
|
+
args.push('--skip-git-repo-check');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Augment context with schema if CLI doesn't support native --output-schema
|
|
87
|
+
let finalContext = context;
|
|
88
|
+
if (jsonSchema && cliFeatures.supportsOutputSchema) {
|
|
89
|
+
// CRITICAL: Codex --output-schema takes a FILE PATH, not a JSON string
|
|
90
|
+
// Write schema to temp file and pass the path
|
|
91
|
+
// Codex requires additionalProperties: false on all object schemas
|
|
92
|
+
const parsedSchema = typeof jsonSchema === 'string' ? JSON.parse(jsonSchema) : jsonSchema;
|
|
93
|
+
const strictSchema = enforceStrictSchema(parsedSchema);
|
|
94
|
+
const schemaStr = JSON.stringify(strictSchema, null, 2);
|
|
95
|
+
const schemaFile = path.join(
|
|
96
|
+
os.tmpdir(),
|
|
97
|
+
`zeroshot-schema-${Date.now()}-${Math.random().toString(36).slice(2)}.json`
|
|
98
|
+
);
|
|
99
|
+
fs.writeFileSync(schemaFile, schemaStr);
|
|
100
|
+
cleanup.push(schemaFile);
|
|
101
|
+
args.push('--output-schema', schemaFile);
|
|
102
|
+
} else if (jsonSchema) {
|
|
103
|
+
// CRITICAL: Inject schema into prompt when CLI doesn't support --output-schema
|
|
104
|
+
// Without this, model outputs free-form text instead of JSON
|
|
105
|
+
const schemaStr =
|
|
106
|
+
typeof jsonSchema === 'string' ? jsonSchema : JSON.stringify(jsonSchema, null, 2);
|
|
107
|
+
finalContext =
|
|
108
|
+
context +
|
|
109
|
+
`\n\n## OUTPUT FORMAT (CRITICAL - REQUIRED)
|
|
110
|
+
|
|
111
|
+
You MUST respond with a JSON object that exactly matches this schema. NO markdown, NO explanation, NO code blocks. ONLY the raw JSON object.
|
|
112
|
+
|
|
113
|
+
Schema:
|
|
114
|
+
\`\`\`json
|
|
115
|
+
${schemaStr}
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
Your response must be ONLY valid JSON. Start with { and end with }. Nothing else.`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
args.push(finalContext);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
binary: 'codex',
|
|
125
|
+
args,
|
|
126
|
+
env: {},
|
|
127
|
+
cleanup, // Temp files to delete after command completes
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
buildCommand,
|
|
133
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const BaseProvider = require('../base-provider');
|
|
2
|
+
const { commandExists, getCommandPath, getHelpOutput } = require('../../../lib/provider-detection');
|
|
3
|
+
const { buildCommand } = require('./cli-builder');
|
|
4
|
+
const { parseEvent } = require('./output-parser');
|
|
5
|
+
const {
|
|
6
|
+
MODEL_CATALOG,
|
|
7
|
+
LEVEL_MAPPING,
|
|
8
|
+
DEFAULT_LEVEL,
|
|
9
|
+
DEFAULT_MAX_LEVEL,
|
|
10
|
+
DEFAULT_MIN_LEVEL,
|
|
11
|
+
} = require('./models');
|
|
12
|
+
|
|
13
|
+
const warned = new Set();
|
|
14
|
+
|
|
15
|
+
class OpenAIProvider extends BaseProvider {
|
|
16
|
+
constructor() {
|
|
17
|
+
super({ name: 'codex', displayName: 'Codex', cliCommand: 'codex' });
|
|
18
|
+
this._cliFeatures = null;
|
|
19
|
+
this._unknownEventCounts = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// SDK not implemented - uses CLI only
|
|
23
|
+
// See BaseProvider for SDK extension point documentation
|
|
24
|
+
|
|
25
|
+
isAvailable() {
|
|
26
|
+
return commandExists(this.cliCommand);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getCliPath() {
|
|
30
|
+
return getCommandPath(this.cliCommand) || this.cliCommand;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getInstallInstructions() {
|
|
34
|
+
return 'npm install -g @openai/codex';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getAuthInstructions() {
|
|
38
|
+
return 'codex login';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getCliFeatures() {
|
|
42
|
+
if (this._cliFeatures) return this._cliFeatures;
|
|
43
|
+
// CRITICAL: Check 'codex exec --help' not 'codex --help'
|
|
44
|
+
// The --output-schema flag is on the exec subcommand, not the main command
|
|
45
|
+
const help = getHelpOutput(this.cliCommand, ['exec']);
|
|
46
|
+
const unknown = !help;
|
|
47
|
+
|
|
48
|
+
const features = {
|
|
49
|
+
supportsJson: unknown ? true : /--json\b/.test(help),
|
|
50
|
+
supportsOutputSchema: unknown ? true : /--output-schema\b/.test(help),
|
|
51
|
+
supportsAutoApprove: unknown
|
|
52
|
+
? true
|
|
53
|
+
: /--dangerously-bypass-approvals-and-sandbox\b/.test(help),
|
|
54
|
+
supportsCwd: unknown ? true : /\s-C\b/.test(help) || /--cwd\b/.test(help),
|
|
55
|
+
supportsConfigOverride: unknown ? true : /--config\b/.test(help),
|
|
56
|
+
supportsModel: unknown ? true : /\s-m\b/.test(help) || /--model\b/.test(help),
|
|
57
|
+
supportsSkipGitRepoCheck: unknown ? true : /--skip-git-repo-check\b/.test(help),
|
|
58
|
+
unknown,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
this._cliFeatures = features;
|
|
62
|
+
return features;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getCredentialPaths() {
|
|
66
|
+
return ['~/.config/codex', '~/.codex'];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
buildCommand(context, options) {
|
|
70
|
+
const cliFeatures = options.cliFeatures || {};
|
|
71
|
+
|
|
72
|
+
if (options.autoApprove && cliFeatures.supportsAutoApprove === false) {
|
|
73
|
+
this._warnOnce(
|
|
74
|
+
'codex-auto-approve',
|
|
75
|
+
'Codex CLI does not support auto-approve; continuing without bypass flag.'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.jsonSchema && cliFeatures.supportsOutputSchema === false) {
|
|
80
|
+
this._warnOnce(
|
|
81
|
+
'codex-jsonschema',
|
|
82
|
+
'Codex CLI does not support --output-schema; skipping schema flag.'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.modelSpec?.reasoningEffort && cliFeatures.supportsConfigOverride === false) {
|
|
87
|
+
this._warnOnce(
|
|
88
|
+
'codex-reasoning',
|
|
89
|
+
'Codex CLI does not support --config overrides; skipping reasoningEffort.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return buildCommand(context, { ...options, cliFeatures });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
parseEvent(line) {
|
|
97
|
+
return parseEvent(line, {
|
|
98
|
+
onUnknown: (type) => this._logUnknown(type),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getModelCatalog() {
|
|
103
|
+
return MODEL_CATALOG;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getLevelMapping() {
|
|
107
|
+
return LEVEL_MAPPING;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getDefaultLevel() {
|
|
111
|
+
return DEFAULT_LEVEL;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getDefaultMaxLevel() {
|
|
115
|
+
return DEFAULT_MAX_LEVEL;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getDefaultMinLevel() {
|
|
119
|
+
return DEFAULT_MIN_LEVEL;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_warnOnce(key, message) {
|
|
123
|
+
if (warned.has(key)) return;
|
|
124
|
+
warned.add(key);
|
|
125
|
+
console.warn(`⚠️ ${message}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_logUnknown(type) {
|
|
129
|
+
const current = this._unknownEventCounts.get(type) || 0;
|
|
130
|
+
if (current >= 5) return;
|
|
131
|
+
this._unknownEventCounts.set(type, current + 1);
|
|
132
|
+
console.debug(`[codex] Unknown event type: ${type}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = OpenAIProvider;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Codex CLI - use null to let CLI pick its default model
|
|
2
|
+
// Levels vary by reasoning effort only
|
|
3
|
+
const MODEL_CATALOG = {};
|
|
4
|
+
|
|
5
|
+
const LEVEL_MAPPING = {
|
|
6
|
+
level1: { rank: 1, model: null, reasoningEffort: 'low' },
|
|
7
|
+
level2: { rank: 2, model: null, reasoningEffort: 'medium' },
|
|
8
|
+
level3: { rank: 3, model: null, reasoningEffort: 'high' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DEFAULT_LEVEL = 'level2';
|
|
12
|
+
const DEFAULT_MAX_LEVEL = 'level3';
|
|
13
|
+
const DEFAULT_MIN_LEVEL = 'level1';
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
MODEL_CATALOG,
|
|
17
|
+
LEVEL_MAPPING,
|
|
18
|
+
DEFAULT_LEVEL,
|
|
19
|
+
DEFAULT_MAX_LEVEL,
|
|
20
|
+
DEFAULT_MIN_LEVEL,
|
|
21
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
function safeJsonParse(value, fallback) {
|
|
2
|
+
try {
|
|
3
|
+
return JSON.parse(value);
|
|
4
|
+
} catch {
|
|
5
|
+
return fallback;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseAssistantMessage(item) {
|
|
10
|
+
const events = [];
|
|
11
|
+
const content = Array.isArray(item.content)
|
|
12
|
+
? item.content
|
|
13
|
+
: [{ type: 'text', text: item.content }];
|
|
14
|
+
const text = content
|
|
15
|
+
.filter((c) => c.type === 'text')
|
|
16
|
+
.map((c) => c.text)
|
|
17
|
+
.join('');
|
|
18
|
+
const thinking = content
|
|
19
|
+
.filter((c) => c.type === 'thinking' || c.type === 'reasoning')
|
|
20
|
+
.map((c) => c.text)
|
|
21
|
+
.join('');
|
|
22
|
+
if (text) events.push({ type: 'text', text });
|
|
23
|
+
if (thinking) events.push({ type: 'thinking', text: thinking });
|
|
24
|
+
return events;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseFunctionCall(item) {
|
|
28
|
+
const toolId = item.call_id || item.id || item.tool_call_id || item.tool_id;
|
|
29
|
+
const args =
|
|
30
|
+
typeof item.arguments === 'string' ? safeJsonParse(item.arguments, {}) : item.arguments || {};
|
|
31
|
+
return {
|
|
32
|
+
type: 'tool_call',
|
|
33
|
+
toolName: item.name,
|
|
34
|
+
toolId,
|
|
35
|
+
input: args,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseFunctionCallOutput(item) {
|
|
40
|
+
const toolId = item.call_id || item.id || item.tool_call_id || item.tool_id;
|
|
41
|
+
const content = item.output ?? item.result ?? item.content ?? '';
|
|
42
|
+
return {
|
|
43
|
+
type: 'tool_result',
|
|
44
|
+
toolId,
|
|
45
|
+
content,
|
|
46
|
+
isError: !!item.error,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseItem(item) {
|
|
51
|
+
const events = [];
|
|
52
|
+
|
|
53
|
+
// Handle assistant messages (Claude-style: type=message, role=assistant)
|
|
54
|
+
if (item.type === 'message' && item.role === 'assistant') {
|
|
55
|
+
events.push(...parseAssistantMessage(item));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle agent messages (Codex-style: type=agent_message, text=string)
|
|
59
|
+
if (item.type === 'agent_message' && item.text) {
|
|
60
|
+
events.push({ type: 'text', text: item.text });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (item.type === 'function_call') {
|
|
64
|
+
events.push(parseFunctionCall(item));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (item.type === 'function_call_output') {
|
|
68
|
+
events.push(parseFunctionCallOutput(item));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (events.length === 1) return events[0];
|
|
72
|
+
if (events.length > 1) return events;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseEvent(line, options = {}) {
|
|
77
|
+
let event;
|
|
78
|
+
try {
|
|
79
|
+
event = JSON.parse(line);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (event.type) {
|
|
85
|
+
case 'thread.started':
|
|
86
|
+
case 'turn.started':
|
|
87
|
+
return null;
|
|
88
|
+
|
|
89
|
+
case 'item.created':
|
|
90
|
+
case 'item.completed':
|
|
91
|
+
return parseItem(event.item);
|
|
92
|
+
|
|
93
|
+
case 'turn.completed': {
|
|
94
|
+
const usage = event.usage || event.response?.usage || {};
|
|
95
|
+
return {
|
|
96
|
+
type: 'result',
|
|
97
|
+
success: true,
|
|
98
|
+
inputTokens: usage.input_tokens || 0,
|
|
99
|
+
outputTokens: usage.output_tokens || 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case 'turn.failed':
|
|
104
|
+
return {
|
|
105
|
+
type: 'result',
|
|
106
|
+
success: false,
|
|
107
|
+
error: event.error?.message || event.error || 'Turn failed',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
// Only log warnings for actual unknown string types, not malformed events
|
|
112
|
+
// (undefined/object types are just noise from non-standard CLI output)
|
|
113
|
+
if (options.onUnknown && typeof event.type === 'string') {
|
|
114
|
+
options.onUnknown(event.type, event);
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseChunk(chunk, options = {}) {
|
|
121
|
+
const events = [];
|
|
122
|
+
const lines = chunk.split('\n');
|
|
123
|
+
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
if (!line.trim()) continue;
|
|
126
|
+
const event = parseEvent(line, options);
|
|
127
|
+
if (!event) continue;
|
|
128
|
+
if (Array.isArray(event)) {
|
|
129
|
+
events.push(...event);
|
|
130
|
+
} else {
|
|
131
|
+
events.push(event);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return events;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
parseEvent,
|
|
140
|
+
parseChunk,
|
|
141
|
+
parseItem,
|
|
142
|
+
safeJsonParse,
|
|
143
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function buildCommand(context, options = {}) {
|
|
2
|
+
const { modelSpec, outputFormat, jsonSchema, cwd, cliFeatures = {} } = options;
|
|
3
|
+
|
|
4
|
+
let finalContext = context;
|
|
5
|
+
if (jsonSchema) {
|
|
6
|
+
const schemaStr =
|
|
7
|
+
typeof jsonSchema === 'string' ? jsonSchema : JSON.stringify(jsonSchema, null, 2);
|
|
8
|
+
finalContext =
|
|
9
|
+
context +
|
|
10
|
+
`\n\n## OUTPUT FORMAT (CRITICAL - REQUIRED)\n\nYou MUST respond with a JSON object that exactly matches this schema. NO markdown, NO explanation, NO code blocks. ONLY the raw JSON object.\n\nSchema:\n\`\`\`json\n${schemaStr}\n\`\`\`\n\nYour response must be ONLY valid JSON. Start with { and end with }. Nothing else.`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const args = ['run'];
|
|
14
|
+
|
|
15
|
+
if ((outputFormat === 'stream-json' || outputFormat === 'json') && cliFeatures.supportsJson) {
|
|
16
|
+
args.push('--format', 'json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (modelSpec?.model) {
|
|
20
|
+
args.push('--model', modelSpec.model);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (modelSpec?.reasoningEffort && cliFeatures.supportsVariant) {
|
|
24
|
+
args.push('--variant', modelSpec.reasoningEffort);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (cwd && cliFeatures.supportsCwd) {
|
|
28
|
+
args.push('--cwd', cwd);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
args.push(finalContext);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
binary: 'opencode',
|
|
35
|
+
args,
|
|
36
|
+
env: {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
buildCommand,
|
|
42
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const BaseProvider = require('../base-provider');
|
|
2
|
+
const { commandExists, getCommandPath, getHelpOutput } = require('../../../lib/provider-detection');
|
|
3
|
+
const { buildCommand } = require('./cli-builder');
|
|
4
|
+
const { parseEvent } = require('./output-parser');
|
|
5
|
+
const {
|
|
6
|
+
MODEL_CATALOG,
|
|
7
|
+
LEVEL_MAPPING,
|
|
8
|
+
DEFAULT_LEVEL,
|
|
9
|
+
DEFAULT_MAX_LEVEL,
|
|
10
|
+
DEFAULT_MIN_LEVEL,
|
|
11
|
+
} = require('./models');
|
|
12
|
+
|
|
13
|
+
const warned = new Set();
|
|
14
|
+
|
|
15
|
+
class OpencodeProvider extends BaseProvider {
|
|
16
|
+
constructor() {
|
|
17
|
+
super({ name: 'opencode', displayName: 'Opencode', cliCommand: 'opencode' });
|
|
18
|
+
this._cliFeatures = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
isAvailable() {
|
|
22
|
+
return commandExists(this.cliCommand);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getCliPath() {
|
|
26
|
+
return getCommandPath(this.cliCommand) || this.cliCommand;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getInstallInstructions() {
|
|
30
|
+
return 'See https://opencode.ai for installation instructions.';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getAuthInstructions() {
|
|
34
|
+
return 'opencode auth login';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getCliFeatures() {
|
|
38
|
+
if (this._cliFeatures) return this._cliFeatures;
|
|
39
|
+
const help = getHelpOutput(this.cliCommand, ['run']);
|
|
40
|
+
const unknown = !help;
|
|
41
|
+
|
|
42
|
+
const features = {
|
|
43
|
+
supportsJson: unknown ? true : /--format\b/.test(help),
|
|
44
|
+
supportsModel: unknown ? true : /--model\b/.test(help),
|
|
45
|
+
supportsVariant: unknown ? true : /--variant\b/.test(help),
|
|
46
|
+
supportsCwd: unknown ? false : /--cwd\b/.test(help),
|
|
47
|
+
supportsAutoApprove: false,
|
|
48
|
+
unknown,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this._cliFeatures = features;
|
|
52
|
+
return features;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getCredentialPaths() {
|
|
56
|
+
return ['~/.local/share/opencode'];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
buildCommand(context, options) {
|
|
60
|
+
const cliFeatures = options.cliFeatures || {};
|
|
61
|
+
|
|
62
|
+
if (options.modelSpec?.reasoningEffort && cliFeatures.supportsVariant === false) {
|
|
63
|
+
this._warnOnce(
|
|
64
|
+
'opencode-variant',
|
|
65
|
+
'Opencode CLI does not support --variant; skipping reasoningEffort.'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return buildCommand(context, { ...options, cliFeatures });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
parseEvent(line) {
|
|
73
|
+
return parseEvent(line);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getModelCatalog() {
|
|
77
|
+
return MODEL_CATALOG;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getLevelMapping() {
|
|
81
|
+
return LEVEL_MAPPING;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getDefaultLevel() {
|
|
85
|
+
return DEFAULT_LEVEL;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getDefaultMaxLevel() {
|
|
89
|
+
return DEFAULT_MAX_LEVEL;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getDefaultMinLevel() {
|
|
93
|
+
return DEFAULT_MIN_LEVEL;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_warnOnce(key, message) {
|
|
97
|
+
if (warned.has(key)) return;
|
|
98
|
+
warned.add(key);
|
|
99
|
+
console.warn(`⚠️ ${message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = OpencodeProvider;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const MODEL_CATALOG = {
|
|
2
|
+
'opencode/big-pickle': { rank: 1 },
|
|
3
|
+
'opencode/glm-4.7-free': { rank: 1 },
|
|
4
|
+
'opencode/gpt-5-nano': { rank: 1 },
|
|
5
|
+
'opencode/grok-code': { rank: 1 },
|
|
6
|
+
'opencode/minimax-m2.1-free': { rank: 1 },
|
|
7
|
+
'google/gemini-1.5-flash': { rank: 1 },
|
|
8
|
+
'google/gemini-1.5-flash-8b': { rank: 1 },
|
|
9
|
+
'google/gemini-1.5-pro': { rank: 1 },
|
|
10
|
+
'google/gemini-2.0-flash': { rank: 1 },
|
|
11
|
+
'google/gemini-2.0-flash-lite': { rank: 1 },
|
|
12
|
+
'google/gemini-2.5-flash': { rank: 1 },
|
|
13
|
+
'google/gemini-2.5-flash-image': { rank: 1 },
|
|
14
|
+
'google/gemini-2.5-flash-image-preview': { rank: 1 },
|
|
15
|
+
'google/gemini-2.5-flash-lite': { rank: 1 },
|
|
16
|
+
'google/gemini-2.5-flash-lite-preview-06-17': { rank: 1 },
|
|
17
|
+
'google/gemini-2.5-flash-lite-preview-09-2025': { rank: 1 },
|
|
18
|
+
'google/gemini-2.5-flash-preview-04-17': { rank: 1 },
|
|
19
|
+
'google/gemini-2.5-flash-preview-05-20': { rank: 1 },
|
|
20
|
+
'google/gemini-2.5-flash-preview-09-2025': { rank: 1 },
|
|
21
|
+
'google/gemini-2.5-flash-preview-tts': { rank: 1 },
|
|
22
|
+
'google/gemini-2.5-pro': { rank: 1 },
|
|
23
|
+
'google/gemini-2.5-pro-preview-05-06': { rank: 1 },
|
|
24
|
+
'google/gemini-2.5-pro-preview-06-05': { rank: 1 },
|
|
25
|
+
'google/gemini-2.5-pro-preview-tts': { rank: 1 },
|
|
26
|
+
'google/gemini-3-flash-preview': { rank: 1 },
|
|
27
|
+
'google/gemini-3-pro-preview': { rank: 1 },
|
|
28
|
+
'google/gemini-embedding-001': { rank: 1 },
|
|
29
|
+
'google/gemini-flash-latest': { rank: 1 },
|
|
30
|
+
'google/gemini-flash-lite-latest': { rank: 1 },
|
|
31
|
+
'google/gemini-live-2.5-flash': { rank: 1 },
|
|
32
|
+
'google/gemini-live-2.5-flash-preview-native-audio': { rank: 1 },
|
|
33
|
+
'openai/gpt-5.1-codex-max': { rank: 1 },
|
|
34
|
+
'openai/gpt-5.1-codex-mini': { rank: 1 },
|
|
35
|
+
'openai/gpt-5.2': { rank: 1 },
|
|
36
|
+
'openai/gpt-5.2-codex': { rank: 1 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const LEVEL_MAPPING = {
|
|
40
|
+
level1: { rank: 1, model: null, reasoningEffort: 'low' },
|
|
41
|
+
level2: { rank: 2, model: null, reasoningEffort: 'medium' },
|
|
42
|
+
level3: { rank: 3, model: null, reasoningEffort: 'high' },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DEFAULT_LEVEL = 'level2';
|
|
46
|
+
const DEFAULT_MAX_LEVEL = 'level3';
|
|
47
|
+
const DEFAULT_MIN_LEVEL = 'level1';
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
MODEL_CATALOG,
|
|
51
|
+
LEVEL_MAPPING,
|
|
52
|
+
DEFAULT_LEVEL,
|
|
53
|
+
DEFAULT_MAX_LEVEL,
|
|
54
|
+
DEFAULT_MIN_LEVEL,
|
|
55
|
+
};
|