@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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseProvider - Abstract provider interface
|
|
3
|
+
*
|
|
4
|
+
* All providers support two execution paths:
|
|
5
|
+
* 1. SDK (fast) - Direct API calls using provider's SDK (requires API key)
|
|
6
|
+
* 2. CLI (slower) - Spawns provider's CLI tool (uses OAuth/login auth)
|
|
7
|
+
*
|
|
8
|
+
* Use callSimple() for simple prompts - it tries SDK first, falls back to CLI.
|
|
9
|
+
*/
|
|
10
|
+
class BaseProvider {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.name = options.name || 'base';
|
|
13
|
+
this.displayName = options.displayName || 'Base';
|
|
14
|
+
this.cliCommand = options.cliCommand || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// SDK SUPPORT (Future Extension Point)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
//
|
|
21
|
+
// SDK support is NOT IMPLEMENTED. These methods exist for future extension.
|
|
22
|
+
// Currently, all providers use CLI (claude, codex, gemini) for execution.
|
|
23
|
+
//
|
|
24
|
+
// To add SDK support for a provider:
|
|
25
|
+
// 1. Override getSDKEnvVar() to return the API key env var
|
|
26
|
+
// 2. Override callSDK() to implement the actual API call
|
|
27
|
+
// 3. The callSimple() method will then work automatically
|
|
28
|
+
//
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the environment variable name for the API key
|
|
33
|
+
* @returns {string} Environment variable name (e.g., 'ANTHROPIC_API_KEY')
|
|
34
|
+
*/
|
|
35
|
+
getSDKEnvVar() {
|
|
36
|
+
throw new Error(`${this.name}: SDK not implemented. Use CLI instead.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if SDK is configured (API key is set)
|
|
41
|
+
* @returns {boolean} True if API key is available
|
|
42
|
+
*/
|
|
43
|
+
isSDKConfigured() {
|
|
44
|
+
try {
|
|
45
|
+
const envVar = this.getSDKEnvVar();
|
|
46
|
+
return !!process.env[envVar];
|
|
47
|
+
} catch {
|
|
48
|
+
// getSDKEnvVar() throws if not implemented
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Make a simple API call via SDK (fast path)
|
|
55
|
+
* NOT IMPLEMENTED - exists for future extension.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} _prompt - The prompt to send
|
|
58
|
+
* @param {Object} _options - Call options
|
|
59
|
+
* @returns {Promise<{success: boolean, text: string, usage?: Object, error?: string}>}
|
|
60
|
+
*/
|
|
61
|
+
callSDK(_prompt, _options) {
|
|
62
|
+
return Promise.reject(new Error(`${this.name}: SDK not implemented. Use CLI instead.`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Make a simple API call via SDK
|
|
67
|
+
* NOT IMPLEMENTED - exists for future extension.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} _prompt - The prompt to send
|
|
70
|
+
* @param {Object} _options - Call options
|
|
71
|
+
* @returns {Promise<{success: boolean, text: string, usage?: Object, error?: string}>}
|
|
72
|
+
*/
|
|
73
|
+
callSimple(_prompt, _options = {}) {
|
|
74
|
+
return Promise.reject(new Error(`${this.name}: SDK not implemented. Use CLI instead.`));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
isAvailable() {
|
|
78
|
+
throw new Error('Not implemented');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getCliPath() {
|
|
82
|
+
throw new Error('Not implemented');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getInstallInstructions() {
|
|
86
|
+
throw new Error('Not implemented');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getAuthInstructions() {
|
|
90
|
+
throw new Error('Not implemented');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getCliFeatures() {
|
|
94
|
+
throw new Error('Not implemented');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getCredentialPaths() {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
buildCommand(_context, _options) {
|
|
102
|
+
throw new Error('Not implemented');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
parseEvent(_line) {
|
|
106
|
+
throw new Error('Not implemented');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getModelCatalog() {
|
|
110
|
+
throw new Error('Not implemented');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getLevelMapping() {
|
|
114
|
+
throw new Error('Not implemented');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
resolveModelSpec(level, overrides = {}) {
|
|
118
|
+
const mapping = this.getLevelMapping();
|
|
119
|
+
const base = mapping[level] || mapping[this.getDefaultLevel()];
|
|
120
|
+
if (!base) {
|
|
121
|
+
throw new Error(`Unknown level "${level}" for provider "${this.name}"`);
|
|
122
|
+
}
|
|
123
|
+
const override = overrides[level] || {};
|
|
124
|
+
return {
|
|
125
|
+
level,
|
|
126
|
+
model: override.model || base.model,
|
|
127
|
+
reasoningEffort: override.reasoningEffort || base.reasoningEffort,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
validateLevel(level, minLevel, maxLevel) {
|
|
132
|
+
const mapping = this.getLevelMapping();
|
|
133
|
+
const rank = (key) => mapping[key]?.rank;
|
|
134
|
+
|
|
135
|
+
if (!mapping[level]) {
|
|
136
|
+
throw new Error(`Invalid level "${level}" for provider "${this.name}"`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (minLevel && !mapping[minLevel]) {
|
|
140
|
+
throw new Error(`Invalid minLevel "${minLevel}" for provider "${this.name}"`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (maxLevel && !mapping[maxLevel]) {
|
|
144
|
+
throw new Error(`Invalid maxLevel "${maxLevel}" for provider "${this.name}"`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (minLevel && maxLevel && rank(minLevel) > rank(maxLevel)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`minLevel "${minLevel}" exceeds maxLevel "${maxLevel}" for provider "${this.name}"`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (maxLevel && rank(level) > rank(maxLevel)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Level "${level}" exceeds maxLevel "${maxLevel}" for provider "${this.name}"`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (minLevel && rank(level) < rank(minLevel)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Level "${level}" is below minLevel "${minLevel}" for provider "${this.name}"`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return level;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
validateModelId(modelId) {
|
|
169
|
+
const catalog = this.getModelCatalog();
|
|
170
|
+
if (modelId && !catalog[modelId]) {
|
|
171
|
+
throw new Error(`Invalid model "${modelId}" for provider "${this.name}"`);
|
|
172
|
+
}
|
|
173
|
+
return modelId;
|
|
174
|
+
}
|
|
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
|
+
|
|
188
|
+
getDefaultLevel() {
|
|
189
|
+
throw new Error('Not implemented');
|
|
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
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = BaseProvider;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { normalizeProviderName } = require('../../lib/provider-names');
|
|
2
|
+
|
|
3
|
+
const CAPABILITIES = {
|
|
4
|
+
claude: {
|
|
5
|
+
dockerIsolation: true,
|
|
6
|
+
worktreeIsolation: true,
|
|
7
|
+
mcpServers: true,
|
|
8
|
+
jsonSchema: true,
|
|
9
|
+
streamJson: true,
|
|
10
|
+
thinkingMode: true,
|
|
11
|
+
reasoningEffort: false,
|
|
12
|
+
},
|
|
13
|
+
codex: {
|
|
14
|
+
dockerIsolation: true,
|
|
15
|
+
worktreeIsolation: true,
|
|
16
|
+
mcpServers: true,
|
|
17
|
+
jsonSchema: true,
|
|
18
|
+
streamJson: true,
|
|
19
|
+
thinkingMode: true,
|
|
20
|
+
reasoningEffort: true,
|
|
21
|
+
},
|
|
22
|
+
gemini: {
|
|
23
|
+
dockerIsolation: true,
|
|
24
|
+
worktreeIsolation: true,
|
|
25
|
+
mcpServers: true,
|
|
26
|
+
jsonSchema: 'experimental',
|
|
27
|
+
streamJson: true,
|
|
28
|
+
thinkingMode: true,
|
|
29
|
+
reasoningEffort: false,
|
|
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
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function checkCapability(provider, capability) {
|
|
43
|
+
const caps = CAPABILITIES[normalizeProviderName(provider)];
|
|
44
|
+
if (!caps) return false;
|
|
45
|
+
return caps[capability] === true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function warnIfExperimental(provider, capability) {
|
|
49
|
+
const normalized = normalizeProviderName(provider);
|
|
50
|
+
const caps = CAPABILITIES[normalized];
|
|
51
|
+
if (caps?.[capability] === 'experimental') {
|
|
52
|
+
console.warn(`⚠️ ${capability} is experimental for ${normalized} and may not work reliably`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
CAPABILITIES,
|
|
58
|
+
checkCapability,
|
|
59
|
+
warnIfExperimental,
|
|
60
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function buildCommand(context, options = {}) {
|
|
2
|
+
const { modelSpec, outputFormat, jsonSchema, cwd, autoApprove, cliFeatures = {} } = options;
|
|
3
|
+
|
|
4
|
+
// Augment context with schema if provided (Gemini CLI doesn't support native schema enforcement)
|
|
5
|
+
let finalContext = context;
|
|
6
|
+
if (jsonSchema) {
|
|
7
|
+
// CRITICAL: Inject schema into prompt since Gemini CLI has no --output-schema flag
|
|
8
|
+
// Without this, model outputs free-form text instead of JSON
|
|
9
|
+
const schemaStr =
|
|
10
|
+
typeof jsonSchema === 'string' ? jsonSchema : JSON.stringify(jsonSchema, null, 2);
|
|
11
|
+
finalContext =
|
|
12
|
+
context +
|
|
13
|
+
`\n\n## OUTPUT FORMAT (CRITICAL - REQUIRED)
|
|
14
|
+
|
|
15
|
+
You MUST respond with a JSON object that exactly matches this schema. NO markdown, NO explanation, NO code blocks. ONLY the raw JSON object.
|
|
16
|
+
|
|
17
|
+
Schema:
|
|
18
|
+
\`\`\`json
|
|
19
|
+
${schemaStr}
|
|
20
|
+
\`\`\`
|
|
21
|
+
|
|
22
|
+
Your response must be ONLY valid JSON. Start with { and end with }. Nothing else.`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const args = ['-p', finalContext];
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
(outputFormat === 'stream-json' || outputFormat === 'json') &&
|
|
29
|
+
cliFeatures.supportsStreamJson
|
|
30
|
+
) {
|
|
31
|
+
args.push('--output-format', 'stream-json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (modelSpec?.model) {
|
|
35
|
+
args.push('-m', modelSpec.model);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (cwd && cliFeatures.supportsCwd) {
|
|
39
|
+
args.push('--cwd', cwd);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (autoApprove && cliFeatures.supportsAutoApprove) {
|
|
43
|
+
args.push('--yolo');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
binary: 'gemini',
|
|
48
|
+
args,
|
|
49
|
+
env: {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
buildCommand,
|
|
55
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
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 GoogleProvider extends BaseProvider {
|
|
16
|
+
constructor() {
|
|
17
|
+
super({ name: 'gemini', displayName: 'Gemini', cliCommand: 'gemini' });
|
|
18
|
+
this._cliFeatures = null;
|
|
19
|
+
this._unknownEventCounts = new Map();
|
|
20
|
+
this._parserState = { lastToolId: null };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// SDK not implemented - uses CLI only
|
|
24
|
+
// See BaseProvider for SDK extension point documentation
|
|
25
|
+
|
|
26
|
+
isAvailable() {
|
|
27
|
+
return commandExists(this.cliCommand);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getCliPath() {
|
|
31
|
+
return getCommandPath(this.cliCommand) || this.cliCommand;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getInstallInstructions() {
|
|
35
|
+
return 'npm install -g @google/gemini-cli';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getAuthInstructions() {
|
|
39
|
+
return 'gemini auth login';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getCliFeatures() {
|
|
43
|
+
if (this._cliFeatures) return this._cliFeatures;
|
|
44
|
+
const help = getHelpOutput(this.cliCommand);
|
|
45
|
+
const unknown = !help;
|
|
46
|
+
|
|
47
|
+
const features = {
|
|
48
|
+
supportsStreamJson: unknown ? true : /--output-format\b/.test(help),
|
|
49
|
+
supportsAutoApprove: unknown ? true : /--yolo\b/.test(help),
|
|
50
|
+
supportsCwd: unknown ? true : /--cwd\b/.test(help),
|
|
51
|
+
supportsModel: unknown ? true : /\s-m\b/.test(help) || /--model\b/.test(help),
|
|
52
|
+
unknown,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this._cliFeatures = features;
|
|
56
|
+
return features;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getCredentialPaths() {
|
|
60
|
+
return ['~/.config/gcloud', '~/.config/gemini', '~/.gemini'];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
buildCommand(context, options) {
|
|
64
|
+
const cliFeatures = options.cliFeatures || {};
|
|
65
|
+
|
|
66
|
+
if (options.autoApprove && cliFeatures.supportsAutoApprove === false) {
|
|
67
|
+
this._warnOnce(
|
|
68
|
+
'gemini-auto-approve',
|
|
69
|
+
'Gemini CLI does not support --yolo; continuing without auto-approve.'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return buildCommand(context, { ...options, cliFeatures });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
parseEvent(line) {
|
|
77
|
+
return parseEvent(line, this._parserState, {
|
|
78
|
+
onUnknown: (type) => this._logUnknown(type),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getModelCatalog() {
|
|
83
|
+
return MODEL_CATALOG;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getLevelMapping() {
|
|
87
|
+
return LEVEL_MAPPING;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getDefaultLevel() {
|
|
91
|
+
return DEFAULT_LEVEL;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getDefaultMaxLevel() {
|
|
95
|
+
return DEFAULT_MAX_LEVEL;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getDefaultMinLevel() {
|
|
99
|
+
return DEFAULT_MIN_LEVEL;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_warnOnce(key, message) {
|
|
103
|
+
if (warned.has(key)) return;
|
|
104
|
+
warned.add(key);
|
|
105
|
+
console.warn(`⚠️ ${message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_logUnknown(type) {
|
|
109
|
+
const current = this._unknownEventCounts.get(type) || 0;
|
|
110
|
+
if (current >= 5) return;
|
|
111
|
+
this._unknownEventCounts.set(type, current + 1);
|
|
112
|
+
console.debug(`[gemini] Unknown event type: ${type}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = GoogleProvider;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Gemini CLI model names
|
|
2
|
+
// Model is optional - Gemini defaults to best available
|
|
3
|
+
const MODEL_CATALOG = {
|
|
4
|
+
'gemini-2.5-pro': { rank: 3 },
|
|
5
|
+
'gemini-2.0-flash': { rank: 1 },
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const LEVEL_MAPPING = {
|
|
9
|
+
level1: { rank: 1, model: null },
|
|
10
|
+
level2: { rank: 2, model: null },
|
|
11
|
+
level3: { rank: 3, model: null },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DEFAULT_LEVEL = 'level2';
|
|
15
|
+
const DEFAULT_MAX_LEVEL = 'level3';
|
|
16
|
+
const DEFAULT_MIN_LEVEL = 'level1';
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
MODEL_CATALOG,
|
|
20
|
+
LEVEL_MAPPING,
|
|
21
|
+
DEFAULT_LEVEL,
|
|
22
|
+
DEFAULT_MAX_LEVEL,
|
|
23
|
+
DEFAULT_MIN_LEVEL,
|
|
24
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
function normalizeMessageContent(content) {
|
|
2
|
+
if (typeof content === 'string') return content;
|
|
3
|
+
if (Array.isArray(content)) {
|
|
4
|
+
return content.map((item) => (typeof item === 'string' ? item : item?.text || '')).join('');
|
|
5
|
+
}
|
|
6
|
+
if (content && typeof content === 'object') {
|
|
7
|
+
if (typeof content.text === 'string') return content.text;
|
|
8
|
+
}
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
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
|
+
|
|
53
|
+
function parseEvent(line, state = {}, options = {}) {
|
|
54
|
+
let event;
|
|
55
|
+
try {
|
|
56
|
+
event = JSON.parse(line);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
switch (event.type) {
|
|
62
|
+
case 'init':
|
|
63
|
+
return null;
|
|
64
|
+
case 'message':
|
|
65
|
+
return parseMessageEvent(event);
|
|
66
|
+
case 'tool_use':
|
|
67
|
+
return parseToolUseEvent(event, state);
|
|
68
|
+
case 'tool_result':
|
|
69
|
+
return parseToolResultEvent(event, state);
|
|
70
|
+
case 'result':
|
|
71
|
+
return parseResultEvent(event);
|
|
72
|
+
default:
|
|
73
|
+
if (options.onUnknown) {
|
|
74
|
+
options.onUnknown(event.type, event);
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseChunk(chunk, state = {}, options = {}) {
|
|
81
|
+
const events = [];
|
|
82
|
+
const lines = chunk.split('\n');
|
|
83
|
+
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
if (!line.trim()) continue;
|
|
86
|
+
const event = parseEvent(line, state, options);
|
|
87
|
+
if (!event) continue;
|
|
88
|
+
if (Array.isArray(event)) {
|
|
89
|
+
events.push(...event);
|
|
90
|
+
} else {
|
|
91
|
+
events.push(event);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return events;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
parseEvent,
|
|
100
|
+
parseChunk,
|
|
101
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const AnthropicProvider = require('./anthropic');
|
|
2
|
+
const OpenAIProvider = require('./openai');
|
|
3
|
+
const GoogleProvider = require('./google');
|
|
4
|
+
const OpencodeProvider = require('./opencode');
|
|
5
|
+
const { normalizeProviderName } = require('../../lib/provider-names');
|
|
6
|
+
|
|
7
|
+
const PROVIDERS = {
|
|
8
|
+
claude: AnthropicProvider,
|
|
9
|
+
codex: OpenAIProvider,
|
|
10
|
+
gemini: GoogleProvider,
|
|
11
|
+
opencode: OpencodeProvider,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function getProvider(name) {
|
|
15
|
+
const normalized = normalizeProviderName(name || '');
|
|
16
|
+
const Provider = PROVIDERS[normalized];
|
|
17
|
+
if (!Provider) {
|
|
18
|
+
throw new Error(`Unknown provider: ${name}. Valid: ${Object.keys(PROVIDERS).join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
return new Provider();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function detectProviders() {
|
|
24
|
+
const results = {};
|
|
25
|
+
for (const [name, Provider] of Object.entries(PROVIDERS)) {
|
|
26
|
+
const provider = new Provider();
|
|
27
|
+
results[name] = {
|
|
28
|
+
available: await provider.isAvailable(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function listProviders() {
|
|
35
|
+
return Object.keys(PROVIDERS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stripTimestampPrefix(line) {
|
|
39
|
+
if (!line || typeof line !== 'string') return '';
|
|
40
|
+
let trimmed = line.trim().replace(/\r$/, '');
|
|
41
|
+
if (!trimmed) return '';
|
|
42
|
+
|
|
43
|
+
const tsMatch = trimmed.match(/^\[(\d{13})\](.*)$/);
|
|
44
|
+
if (tsMatch) trimmed = (tsMatch[2] || '').trimStart();
|
|
45
|
+
|
|
46
|
+
// In cluster logs, lines are often prefixed like:
|
|
47
|
+
// "validator | {json...}"
|
|
48
|
+
// Strip the "<agent> | " prefix so provider parsers can JSON.parse the event.
|
|
49
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
50
|
+
const pipeMatch = trimmed.match(/^[^|]{1,40}\|\s*(.*)$/);
|
|
51
|
+
if (pipeMatch) {
|
|
52
|
+
const afterPipe = (pipeMatch[1] || '').trimStart();
|
|
53
|
+
if (afterPipe.startsWith('{') || afterPipe.startsWith('[')) return afterPipe;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return trimmed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseChunkWithProvider(provider, chunk) {
|
|
61
|
+
if (!chunk) return [];
|
|
62
|
+
const events = [];
|
|
63
|
+
const lines = chunk.split('\n');
|
|
64
|
+
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const content = stripTimestampPrefix(line);
|
|
67
|
+
if (!content) continue;
|
|
68
|
+
const event = provider.parseEvent(content);
|
|
69
|
+
if (!event) continue;
|
|
70
|
+
if (Array.isArray(event)) {
|
|
71
|
+
events.push(...event);
|
|
72
|
+
} else {
|
|
73
|
+
events.push(event);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return events;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseProviderChunk(providerName, chunk) {
|
|
81
|
+
const provider = getProvider(providerName || 'claude');
|
|
82
|
+
return parseChunkWithProvider(provider, chunk);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
getProvider,
|
|
87
|
+
detectProviders,
|
|
88
|
+
listProviders,
|
|
89
|
+
parseProviderChunk,
|
|
90
|
+
parseChunkWithProvider,
|
|
91
|
+
};
|