circuschief 1.0.0 → 1.1.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/package.json +1 -1
- package/packages/server/src/agents/AgentGateway.js +2 -0
- package/packages/server/src/agents/adapters/GeminiAdapter.js +105 -0
- package/packages/server/src/agents/adapters/cliUtils.js +15 -0
- package/packages/server/src/agents/adapters/codexCliRunner.js +1 -8
- package/packages/server/src/agents/adapters/geminiCliRunner.js +183 -0
- package/packages/server/src/agents/adapters/geminiEventMapper.js +195 -0
- package/packages/server/src/db/ProviderRepository.js +4 -2
- package/packages/server/src/db/migrations/index.js +9 -0
- package/packages/server/src/db/migrations/providerMigrations.js +88 -3
- package/packages/server/src/db/seedBaselineData.js +23 -1
- package/packages/server/src/schema.sql +1 -1
- package/packages/server/src/services/e2eSpawnCapture.js +47 -6
- package/packages/server/src/services/geminiSpawnHelper.js +47 -0
- package/packages/server/src/services/gitDiff.js +22 -47
- package/packages/server/src/services/gitService.js +6 -2
- package/packages/server/src/services/providerTestService.js +59 -1
- package/packages/server/src/services/queryParamBuilder.js +33 -1
- package/packages/server/src/services/sessionExecution.js +4 -0
- package/packages/server/src/services/sessionPrompts.js +22 -0
- package/packages/server/src/services/sessionProvider.js +41 -1
- package/packages/shared/src/constants.js +1 -1
- package/packages/shared/src/contracts/providers.js +1 -1
- package/packages/shared/src/types.js +7 -0
- package/packages/web/dist/assets/{ActiveSessionsView-Cxh8mHmB.js → ActiveSessionsView-EdNxmPmZ.js} +1 -1
- package/packages/web/dist/assets/{AgentLogsView-xdfI2bR6.js → AgentLogsView-C2wX0JPP.js} +1 -1
- package/packages/web/dist/assets/{ArchiveConfirmModal-DXZYdzHR.js → ArchiveConfirmModal-DJERn5XO.js} +1 -1
- package/packages/web/dist/assets/{CommandButtonDetailView-D8xfqLAp.js → CommandButtonDetailView-CBPI8-US.js} +1 -1
- package/packages/web/dist/assets/EffortLevelSelector-PaBpUveC.js +1 -0
- package/packages/web/dist/assets/{GeneralSettingsView-sPXkLlLy.js → GeneralSettingsView-Dw-x83R0.js} +1 -1
- package/packages/web/dist/assets/{InputWithButton-B-o0DgMH.js → InputWithButton-CHHcpF4I.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-Dxn1li4l.js → InterpolationHelp-CLNPz8s8.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-DYi1igfT.js +2 -0
- package/packages/web/dist/assets/ModelSelector-Cko_yTO5.js +1 -0
- package/packages/web/dist/assets/{ModelSelector-BNYKujL-.css → ModelSelector-Dtwe5xLH.css} +1 -1
- package/packages/web/dist/assets/{NewSessionView-BR_COfgW.js → NewSessionView-DwUfBg70.js} +1 -1
- package/packages/web/dist/assets/{ProjectEditView-WImU7sNd.js → ProjectEditView-CSbsea3U.js} +1 -1
- package/packages/web/dist/assets/{ProjectListView-CYmmAcBD.js → ProjectListView-CEc_LWZL.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-DEhqw3Jv.js → ProjectNewView-D4U0uRlp.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-2KCOiY6Q.css +1 -0
- package/packages/web/dist/assets/ProvidersView-CD1j8BOv.js +1 -0
- package/packages/web/dist/assets/{QuickResponsesPanel-BqmnTd-D.js → QuickResponsesPanel-Dp39f12o.js} +1 -1
- package/packages/web/dist/assets/ResizableTextarea-BWywIqOv.js +1 -0
- package/packages/web/dist/assets/ResizableTextarea-DERSH3Wz.css +1 -0
- package/packages/web/dist/assets/{SessionCard-Bw77-KwD.js → SessionCard-B6d5ijDW.js} +1 -1
- package/packages/web/dist/assets/{SessionDetailView-B59TEkr-.js → SessionDetailView-DWbXdx7A.js} +19 -19
- package/packages/web/dist/assets/{SessionDetailView-CKVBnR4T.css → SessionDetailView-ULeIkWS0.css} +1 -1
- package/packages/web/dist/assets/{SessionFormOptions-hqijxc0S.js → SessionFormOptions-Dz9ik4Fo.js} +1 -1
- package/packages/web/dist/assets/{SessionListView-DYXHM9I-.js → SessionListView-C129buBe.js} +1 -1
- package/packages/web/dist/assets/{SessionLogStream-5NfVr9pF.js → SessionLogStream-BvXUNNBZ.js} +1 -1
- package/packages/web/dist/assets/{SettingsView-DI8ncOAV.js → SettingsView-DW1NvpX_.js} +1 -1
- package/packages/web/dist/assets/SlashCommandWizard-DleYBxrE.js +1 -0
- package/packages/web/dist/assets/{SummarySettingsView-C2Qs35mm.js → SummarySettingsView-CLUfcWvf.js} +1 -1
- package/packages/web/dist/assets/{TemplateDetailView-zVkIvgtu.js → TemplateDetailView-Cukb205e.js} +1 -1
- package/packages/web/dist/assets/{commandButtons-CoU3G4zK.js → commandButtons-DejH0rVN.js} +1 -1
- package/packages/web/dist/assets/{index-CLRsVASf.js → index-BD7Y3rBE.js} +1 -1
- package/packages/web/dist/assets/{index-uySCcnA_.css → index-Bd20AzX1.css} +1 -1
- package/packages/web/dist/assets/{index-CslU0psO.js → index-BgJiarKe.js} +1 -1
- package/packages/web/dist/assets/{index-9yF1uCCA.js → index-Bk32fSSG.js} +1 -1
- package/packages/web/dist/assets/{index-CAGdsDh7.js → index-BkA6pF2Z.js} +1 -1
- package/packages/web/dist/assets/{index-DsjWqc6R.js → index-Cltr-Ldt.js} +1 -1
- package/packages/web/dist/assets/{index-DI4NxaWD.js → index-Co-46Tp3.js} +1 -1
- package/packages/web/dist/assets/{index-DUa7adFh.js → index-Cpykk857.js} +1 -1
- package/packages/web/dist/assets/{index-C7Ww2auW.js → index-CtABl0D1.js} +1 -1
- package/packages/web/dist/assets/{index-BKstCaYU.js → index-Cuqk5m9S.js} +1 -1
- package/packages/web/dist/assets/{index-C2QFVD7d.js → index-CvXApbVC.js} +15 -15
- package/packages/web/dist/assets/{index-BhbH7eOk.js → index-D2gN-xEH.js} +1 -1
- package/packages/web/dist/assets/{index-Bo7PdwM5.js → index-Dd3WpmyQ.js} +1 -1
- package/packages/web/dist/assets/{index-c99Bo3JV.js → index-Dk6--9rj.js} +1 -1
- package/packages/web/dist/assets/{index-BjuRttEY.js → index-MZf7MlPX.js} +3 -3
- package/packages/web/dist/assets/{index-CP-SxOlV.js → index-NShCcwfj.js} +1 -1
- package/packages/web/dist/assets/{index-rkQx2tso.js → index-hA3VEuSq.js} +1 -1
- package/packages/web/dist/assets/{index-DOzONENy.js → index-p0mp3nca.js} +1 -1
- package/packages/web/dist/assets/{index-mT1JpxDc.js → index-qntNa5r_.js} +1 -1
- package/packages/web/dist/assets/{index-DZBpETI5.js → index-qq9ceNSK.js} +1 -1
- package/packages/web/dist/assets/{projectDefaults-B8esIcYq.js → projectDefaults-D9xkp2XR.js} +1 -1
- package/packages/web/dist/assets/{projects-C-8PSxKi.js → projects-BvLADGKx.js} +1 -1
- package/packages/web/dist/assets/{providers-oXifvvqN.js → providers-DZ-fOa4G.js} +1 -1
- package/packages/web/dist/assets/{sessions-Nq5VafSf.js → sessions-DETEyjPI.js} +1 -1
- package/packages/web/dist/assets/{settings-DtpuiyT6.js → settings-TWfbahn5.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/EffortLevelSelector-D2Hdzc_8.js +0 -1
- package/packages/web/dist/assets/MarkdownEditor-D4Kbb-9l.js +0 -2
- package/packages/web/dist/assets/ModelSelector-72C7MUH4.js +0 -1
- package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +0 -1
- package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +0 -1
- package/packages/web/dist/assets/ResizableTextarea-BQNw5e0C.css +0 -1
- package/packages/web/dist/assets/ResizableTextarea-DpWdIAP6.js +0 -1
- package/packages/web/dist/assets/SlashCommandWizard-BQ_rMzn-.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OPENAI_MODELS } from '../../../../shared/src/index.js';
|
|
1
|
+
import { OPENAI_MODELS, GEMINI_MODELS } from '../../../../shared/src/index.js';
|
|
2
2
|
import { addColumnIfMissing, tableExists } from './migrationUtils.js';
|
|
3
3
|
|
|
4
4
|
function seedBuiltInAnthropicProvider(db) {
|
|
@@ -54,6 +54,27 @@ function seedBuiltInOpenAIProvider(db) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function seedBuiltInGoogleProvider(db) {
|
|
58
|
+
const providerId = 'google-default';
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
|
|
61
|
+
db.prepare(
|
|
62
|
+
`INSERT OR IGNORE INTO providers (
|
|
63
|
+
id, name, base_url, auth_token, kind, is_built_in, created_at, updated_at
|
|
64
|
+
)
|
|
65
|
+
VALUES (?, ?, NULL, NULL, 'google', 1, ?, ?)`
|
|
66
|
+
).run(providerId, 'Google (Official)', now, now);
|
|
67
|
+
|
|
68
|
+
const insertModel = db.prepare(
|
|
69
|
+
`INSERT OR IGNORE INTO provider_models (id, provider_id, model_id, display_name, description, tier, created_at)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?, 'custom', ?)`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
for (const model of GEMINI_MODELS) {
|
|
74
|
+
insertModel.run(model.seedId, providerId, model.id, model.name, model.description, now);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
function seedBuiltInProviders(db) {
|
|
58
79
|
seedBuiltInAnthropicProvider(db);
|
|
59
80
|
seedBuiltInOpenAIProvider(db);
|
|
@@ -98,7 +119,7 @@ export const providerMigrations = [
|
|
|
98
119
|
additional_env_vars TEXT,
|
|
99
120
|
commit_attribution_override TEXT,
|
|
100
121
|
is_built_in INTEGER NOT NULL DEFAULT 0,
|
|
101
|
-
kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai')),
|
|
122
|
+
kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai','google')),
|
|
102
123
|
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
103
124
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
104
125
|
);
|
|
@@ -123,7 +144,7 @@ export const providerMigrations = [
|
|
|
123
144
|
db,
|
|
124
145
|
'providers',
|
|
125
146
|
'kind',
|
|
126
|
-
"TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai'))"
|
|
147
|
+
"TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai','google'))"
|
|
127
148
|
);
|
|
128
149
|
},
|
|
129
150
|
},
|
|
@@ -162,4 +183,68 @@ export const providerMigrations = [
|
|
|
162
183
|
).run('anthropic-opus-4-7', providerId, 'claude-opus-4-7', 'Opus 4.7', 'Most capable (default)', 'opus', Date.now());
|
|
163
184
|
},
|
|
164
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: 'providers-widen-kind-check-google',
|
|
188
|
+
up(db) {
|
|
189
|
+
// SQLite CHECK constraints are baked into the table definition and can't
|
|
190
|
+
// be altered in-place. Recreate the table with the wider CHECK.
|
|
191
|
+
//
|
|
192
|
+
// IMPORTANT: Disable foreign key enforcement during the table swap.
|
|
193
|
+
// provider_models has ON DELETE CASCADE referencing providers. SQLite
|
|
194
|
+
// fires that cascade when DROP TABLE deletes parent rows, which would
|
|
195
|
+
// wipe all provider_models data. Disabling FK enforcement prevents the
|
|
196
|
+
// cascade. We re-enable it immediately after the rename.
|
|
197
|
+
db.pragma('foreign_keys = OFF');
|
|
198
|
+
try {
|
|
199
|
+
db.exec(`
|
|
200
|
+
CREATE TABLE IF NOT EXISTS providers_new (
|
|
201
|
+
id TEXT PRIMARY KEY,
|
|
202
|
+
name TEXT NOT NULL,
|
|
203
|
+
base_url TEXT,
|
|
204
|
+
auth_token TEXT,
|
|
205
|
+
api_timeout_ms INTEGER,
|
|
206
|
+
additional_env_vars TEXT,
|
|
207
|
+
commit_attribution_override TEXT,
|
|
208
|
+
is_built_in INTEGER NOT NULL DEFAULT 0,
|
|
209
|
+
kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai','google')),
|
|
210
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
211
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
INSERT OR IGNORE INTO providers_new SELECT * FROM providers;
|
|
215
|
+
|
|
216
|
+
DROP TABLE providers;
|
|
217
|
+
|
|
218
|
+
ALTER TABLE providers_new RENAME TO providers;
|
|
219
|
+
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_provider_models_provider ON provider_models(provider_id);
|
|
221
|
+
`);
|
|
222
|
+
} finally {
|
|
223
|
+
db.pragma('foreign_keys = ON');
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'providers-seed-built-in-google',
|
|
229
|
+
up(db) { seedBuiltInGoogleProvider(db); },
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'providers-update-gemini-flash-lite-model',
|
|
233
|
+
up(db) {
|
|
234
|
+
// The preview model 'gemini-2.5-flash-lite-preview-06-17' has been
|
|
235
|
+
// removed by Google. Update to the stable GA model ID.
|
|
236
|
+
db.prepare(
|
|
237
|
+
`UPDATE provider_models
|
|
238
|
+
SET model_id = ?, description = ?
|
|
239
|
+
WHERE id = ? AND provider_id = ?`
|
|
240
|
+
).run('gemini-2.5-flash-lite', 'Lightweight & cost-efficient', 'google-gemini-2-5-flash-lite', 'google-default');
|
|
241
|
+
|
|
242
|
+
// Also update any sessions that were using the old model ID
|
|
243
|
+
db.prepare(
|
|
244
|
+
`UPDATE sessions
|
|
245
|
+
SET model = ?
|
|
246
|
+
WHERE model = ?`
|
|
247
|
+
).run('gemini-2.5-flash-lite', 'gemini-2.5-flash-lite-preview-06-17');
|
|
248
|
+
},
|
|
249
|
+
},
|
|
165
250
|
];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { OPENAI_MODELS } from '../../../shared/src/index.js';
|
|
2
|
+
import { OPENAI_MODELS, GEMINI_MODELS } from '../../../shared/src/index.js';
|
|
3
3
|
|
|
4
4
|
export const BUILT_IN_ANTHROPIC_PROVIDER = {
|
|
5
5
|
id: 'anthropic-default',
|
|
@@ -13,6 +13,12 @@ export const BUILT_IN_OPENAI_PROVIDER = {
|
|
|
13
13
|
kind: 'openai',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export const BUILT_IN_GOOGLE_PROVIDER = {
|
|
17
|
+
id: 'google-default',
|
|
18
|
+
name: 'Google (Official)',
|
|
19
|
+
kind: 'google',
|
|
20
|
+
};
|
|
21
|
+
|
|
16
22
|
export const BUILT_IN_ANTHROPIC_MODELS = [
|
|
17
23
|
{ id: 'anthropic-haiku', providerId: BUILT_IN_ANTHROPIC_PROVIDER.id, modelId: 'claude-haiku-4-5-20251001', displayName: 'Haiku 4.5', description: 'Fast & lightweight', tier: 'haiku' },
|
|
18
24
|
{ id: 'anthropic-sonnet', providerId: BUILT_IN_ANTHROPIC_PROVIDER.id, modelId: 'claude-sonnet-4-6', displayName: 'Sonnet 4.6', description: 'Balanced', tier: 'sonnet' },
|
|
@@ -29,6 +35,15 @@ export const BUILT_IN_OPENAI_MODELS = OPENAI_MODELS.map((model) => ({
|
|
|
29
35
|
tier: 'custom',
|
|
30
36
|
}));
|
|
31
37
|
|
|
38
|
+
export const BUILT_IN_GOOGLE_MODELS = GEMINI_MODELS.map((model) => ({
|
|
39
|
+
id: model.seedId,
|
|
40
|
+
providerId: BUILT_IN_GOOGLE_PROVIDER.id,
|
|
41
|
+
modelId: model.id,
|
|
42
|
+
displayName: model.name,
|
|
43
|
+
description: model.description,
|
|
44
|
+
tier: 'custom',
|
|
45
|
+
}));
|
|
46
|
+
|
|
32
47
|
export const DEFAULT_QUICK_RESPONSES = [
|
|
33
48
|
{ label: 'Put a plan on the canvas', content: 'Put a plan on the canvas to get this done', autoSubmit: false, sortOrder: 0 },
|
|
34
49
|
{ label: 'Yes', content: 'Yes', autoSubmit: true, sortOrder: 1 },
|
|
@@ -87,6 +102,13 @@ function seedBuiltInProviders(db) {
|
|
|
87
102
|
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
88
103
|
);
|
|
89
104
|
|
|
105
|
+
// Note: Google provider and models are NOT seeded here because seedBaselineData
|
|
106
|
+
// runs before migrations. On existing databases the providers table still has
|
|
107
|
+
// CHECK(kind IN ('anthropic','openai')), so an INSERT with kind='google' would
|
|
108
|
+
// be silently ignored by INSERT OR IGNORE, and the subsequent model inserts
|
|
109
|
+
// would fail with a FOREIGN KEY constraint. The 'providers-seed-built-in-google'
|
|
110
|
+
// migration handles seeding Google for both fresh and existing databases after
|
|
111
|
+
// the 'providers-widen-kind-check-google' migration has widened the CHECK constraint.
|
|
90
112
|
for (const model of [...BUILT_IN_ANTHROPIC_MODELS, ...BUILT_IN_OPENAI_MODELS]) {
|
|
91
113
|
insertModel.run(model.id, model.providerId, model.modelId, model.displayName, model.description, model.tier, now);
|
|
92
114
|
}
|
|
@@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS providers (
|
|
|
43
43
|
additional_env_vars TEXT,
|
|
44
44
|
commit_attribution_override TEXT,
|
|
45
45
|
is_built_in INTEGER NOT NULL DEFAULT 0,
|
|
46
|
-
kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai')),
|
|
46
|
+
kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai','google')),
|
|
47
47
|
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
48
48
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
49
49
|
);
|
|
@@ -49,9 +49,12 @@ export function createCapturedSpawnProcess(agentType) {
|
|
|
49
49
|
});
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
if (agentType === 'claude-code') {
|
|
52
|
+
if (agentType === 'claude-code' || agentType === 'gemini') {
|
|
53
|
+
// Claude Code and Gemini don't pass prompts via stdin (they use CLI args).
|
|
54
|
+
// Complete after a short delay to simulate process execution.
|
|
53
55
|
setTimeout(complete, 10);
|
|
54
56
|
} else {
|
|
57
|
+
// Codex passes the prompt via stdin; complete when stdin is closed.
|
|
55
58
|
stdin.once('finish', complete);
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -59,12 +62,15 @@ export function createCapturedSpawnProcess(agentType) {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
function summarizeSpawnEnv(env = {}) {
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const summary = {};
|
|
66
|
+
if (Object.prototype.hasOwnProperty.call(env, 'CIRCUSCHIEF_COMMIT_ATTRIBUTION')) {
|
|
67
|
+
summary.CIRCUSCHIEF_COMMIT_ATTRIBUTION = env.CIRCUSCHIEF_COMMIT_ATTRIBUTION;
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(env, 'GEMINI_CLI_TRUST_WORKSPACE')) {
|
|
70
|
+
summary.GEMINI_CLI_TRUST_WORKSPACE = env.GEMINI_CLI_TRUST_WORKSPACE;
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(summary).length === 0) return {};
|
|
73
|
+
return summary;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
function summarizeSpawnOptions(agentType, spawnOptions) {
|
|
@@ -78,6 +84,15 @@ function summarizeSpawnOptions(agentType, spawnOptions) {
|
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
if (agentType === 'gemini') {
|
|
88
|
+
return {
|
|
89
|
+
model: valueAfter(args, '-m'),
|
|
90
|
+
outputFormat: valueAfter(args, '--output-format'),
|
|
91
|
+
approvalMode: optionValue(args, '--approval-mode'),
|
|
92
|
+
prompt: valueAfter(args, '-p'),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
return {
|
|
82
97
|
model: valueAfter(args, '-m'),
|
|
83
98
|
sandbox: valueAfter(args, '--sandbox'),
|
|
@@ -91,6 +106,14 @@ function valueAfter(args, flag) {
|
|
|
91
106
|
return args[index + 1] ?? null;
|
|
92
107
|
}
|
|
93
108
|
|
|
109
|
+
function optionValue(args, flag) {
|
|
110
|
+
const separateValue = valueAfter(args, flag);
|
|
111
|
+
if (separateValue !== null) return separateValue;
|
|
112
|
+
const prefix = `${flag}=`;
|
|
113
|
+
const arg = args.find((value) => value.startsWith(prefix));
|
|
114
|
+
return arg ? arg.slice(prefix.length) : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
function valuesAfter(args, flag) {
|
|
95
118
|
const values = [];
|
|
96
119
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -116,6 +139,24 @@ function writeCapturedAgentEvents(agentType, stdout) {
|
|
|
116
139
|
return;
|
|
117
140
|
}
|
|
118
141
|
|
|
142
|
+
if (agentType === 'gemini') {
|
|
143
|
+
// Gemini CLI stream-json format: init → message → result
|
|
144
|
+
writeJsonLine(stdout, {
|
|
145
|
+
type: 'init',
|
|
146
|
+
session_id: `e2e-gemini-${Date.now()}`,
|
|
147
|
+
});
|
|
148
|
+
writeJsonLine(stdout, {
|
|
149
|
+
type: 'message',
|
|
150
|
+
role: 'assistant',
|
|
151
|
+
content: 'E2E spawn capture response.',
|
|
152
|
+
});
|
|
153
|
+
writeJsonLine(stdout, {
|
|
154
|
+
type: 'result',
|
|
155
|
+
stats: { input_tokens: 0, output_tokens: 0 },
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
119
160
|
writeJsonLine(stdout, {
|
|
120
161
|
type: 'system',
|
|
121
162
|
subtype: 'init',
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createRobustEnv } from './nodeSpawnHelper.js';
|
|
3
|
+
import {
|
|
4
|
+
captureSpawnAttempt,
|
|
5
|
+
createCapturedSpawnProcess,
|
|
6
|
+
isE2ESpawnCaptureEnabled,
|
|
7
|
+
} from './e2eSpawnCapture.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a custom spawn function for the Gemini CLI.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors {@link createCodexSpawner} but for the `gemini` command.
|
|
13
|
+
*
|
|
14
|
+
* As with other CLI helpers:
|
|
15
|
+
* - The command 'node' is replaced with {@link process.execPath} so child
|
|
16
|
+
* processes use the same Node binary.
|
|
17
|
+
* - `createRobustEnv` guarantees the Node bin directory is on PATH.
|
|
18
|
+
*
|
|
19
|
+
* @returns {Function} Spawn function of shape (options) => childProcess
|
|
20
|
+
*/
|
|
21
|
+
export function createGeminiSpawner() {
|
|
22
|
+
return (options) => {
|
|
23
|
+
const { command, args, cwd, env, signal } = options;
|
|
24
|
+
// Replace 'node' with the absolute path to the current Node executable
|
|
25
|
+
const actualCommand = command === 'node' ? process.execPath : command;
|
|
26
|
+
|
|
27
|
+
// Ensure PATH includes the directory containing Node
|
|
28
|
+
const robustEnv = createRobustEnv(env);
|
|
29
|
+
|
|
30
|
+
// Trust the workspace automatically in headless/automated mode.
|
|
31
|
+
// Without this, Gemini CLI refuses to run in untrusted directories.
|
|
32
|
+
robustEnv.GEMINI_CLI_TRUST_WORKSPACE = 'true';
|
|
33
|
+
|
|
34
|
+
if (isE2ESpawnCaptureEnabled()) {
|
|
35
|
+
captureSpawnAttempt('gemini', { ...options, env: robustEnv });
|
|
36
|
+
return createCapturedSpawnProcess('gemini');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return spawn(actualCommand, args, {
|
|
40
|
+
cwd,
|
|
41
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
|
+
signal,
|
|
43
|
+
env: robustEnv,
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -6,11 +6,7 @@ import { git } from './gitService.js';
|
|
|
6
6
|
* @returns {Promise<string>}
|
|
7
7
|
*/
|
|
8
8
|
export async function getDiff(directory) {
|
|
9
|
-
|
|
10
|
-
return await git(directory, 'diff');
|
|
11
|
-
} catch {
|
|
12
|
-
return '';
|
|
13
|
-
}
|
|
9
|
+
return git(directory, 'diff');
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
/**
|
|
@@ -19,11 +15,7 @@ export async function getDiff(directory) {
|
|
|
19
15
|
* @returns {Promise<string>}
|
|
20
16
|
*/
|
|
21
17
|
export async function getStagedDiff(directory) {
|
|
22
|
-
|
|
23
|
-
return await git(directory, 'diff --cached');
|
|
24
|
-
} catch {
|
|
25
|
-
return '';
|
|
26
|
-
}
|
|
18
|
+
return git(directory, 'diff --cached');
|
|
27
19
|
}
|
|
28
20
|
|
|
29
21
|
/**
|
|
@@ -36,7 +28,8 @@ export async function getUntrackedFiles(directory) {
|
|
|
36
28
|
const output = await git(directory, 'ls-files --others --exclude-standard');
|
|
37
29
|
if (!output) return [];
|
|
38
30
|
return output.split('\n').filter((line) => line.trim());
|
|
39
|
-
} catch {
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn(`Failed to get untracked files for ${directory}:`, error.message);
|
|
40
33
|
return [];
|
|
41
34
|
}
|
|
42
35
|
}
|
|
@@ -48,11 +41,7 @@ export async function getUntrackedFiles(directory) {
|
|
|
48
41
|
* @returns {Promise<string>}
|
|
49
42
|
*/
|
|
50
43
|
export async function getDiffAgainstBranch(directory, branch) {
|
|
51
|
-
|
|
52
|
-
return await git(directory, `diff ${branch}`);
|
|
53
|
-
} catch {
|
|
54
|
-
return '';
|
|
55
|
-
}
|
|
44
|
+
return git(directory, `diff ${branch}`);
|
|
56
45
|
}
|
|
57
46
|
|
|
58
47
|
/**
|
|
@@ -62,11 +51,7 @@ export async function getDiffAgainstBranch(directory, branch) {
|
|
|
62
51
|
* @returns {Promise<string>}
|
|
63
52
|
*/
|
|
64
53
|
export async function getStagedDiffAgainstBranch(directory, branch) {
|
|
65
|
-
|
|
66
|
-
return await git(directory, `diff --cached ${branch}`);
|
|
67
|
-
} catch {
|
|
68
|
-
return '';
|
|
69
|
-
}
|
|
54
|
+
return git(directory, `diff --cached ${branch}`);
|
|
70
55
|
}
|
|
71
56
|
|
|
72
57
|
/**
|
|
@@ -78,11 +63,16 @@ export async function getStagedDiffAgainstBranch(directory, branch) {
|
|
|
78
63
|
* @returns {Promise<string>}
|
|
79
64
|
*/
|
|
80
65
|
export async function getDiffBetweenRefs(directory, fromRef, toRef) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
66
|
+
return git(directory, `diff ${fromRef} ${toRef}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function addGitPathOutput(files, output) {
|
|
70
|
+
if (!output) return;
|
|
71
|
+
|
|
72
|
+
output.split('\n').forEach((file) => {
|
|
73
|
+
const trimmed = file.trim();
|
|
74
|
+
if (trimmed) files.add(trimmed);
|
|
75
|
+
});
|
|
86
76
|
}
|
|
87
77
|
|
|
88
78
|
/**
|
|
@@ -94,35 +84,20 @@ export async function getDiffBetweenRefs(directory, fromRef, toRef) {
|
|
|
94
84
|
*/
|
|
95
85
|
export async function getModifiedFilesCount(directory, branch) {
|
|
96
86
|
try {
|
|
97
|
-
|
|
98
|
-
// This includes: committed changes vs branch + staged
|
|
99
|
-
const committedAndStaged = await git(
|
|
87
|
+
const committed = await git(
|
|
100
88
|
directory,
|
|
101
89
|
`diff --name-only ${branch}...HEAD`
|
|
102
90
|
);
|
|
103
91
|
|
|
104
|
-
|
|
92
|
+
const staged = await git(directory, 'diff --cached --name-only');
|
|
105
93
|
const unstaged = await git(directory, 'diff --name-only');
|
|
106
|
-
|
|
107
|
-
// Get untracked files
|
|
108
94
|
const untracked = await getUntrackedFiles(directory);
|
|
109
95
|
|
|
110
|
-
// Combine all files into a Set to get unique count
|
|
111
96
|
const allFiles = new Set();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (unstaged) {
|
|
120
|
-
unstaged.split('\n').forEach(f => {
|
|
121
|
-
if (f.trim()) allFiles.add(f.trim());
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
untracked.forEach(f => allFiles.add(f));
|
|
97
|
+
addGitPathOutput(allFiles, committed);
|
|
98
|
+
addGitPathOutput(allFiles, staged);
|
|
99
|
+
addGitPathOutput(allFiles, unstaged);
|
|
100
|
+
untracked.forEach((file) => allFiles.add(file));
|
|
126
101
|
|
|
127
102
|
return allFiles.size;
|
|
128
103
|
} catch (error) {
|
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
} from './gitWorktree.js';
|
|
31
31
|
|
|
32
32
|
const execAsync = promisify(exec);
|
|
33
|
+
export const DEFAULT_GIT_MAX_BUFFER = 100 * 1024 * 1024;
|
|
33
34
|
|
|
34
35
|
// Cache for default branch detection: directory -> { branch, timestamp }
|
|
35
36
|
const defaultBranchCache = new Map();
|
|
@@ -70,10 +71,14 @@ function evictOldestCacheEntries() {
|
|
|
70
71
|
* @param {Object} [opts]
|
|
71
72
|
* @param {Object} [opts.env]
|
|
72
73
|
* @param {number} [opts.timeout]
|
|
74
|
+
* @param {number} [opts.maxBuffer]
|
|
73
75
|
* @returns {Promise<string>}
|
|
74
76
|
*/
|
|
75
77
|
export async function git(directory, command, opts = {}) {
|
|
76
|
-
const execOpts = {
|
|
78
|
+
const execOpts = {
|
|
79
|
+
cwd: directory,
|
|
80
|
+
maxBuffer: opts.maxBuffer ?? DEFAULT_GIT_MAX_BUFFER,
|
|
81
|
+
};
|
|
77
82
|
if (opts.env) execOpts.env = opts.env;
|
|
78
83
|
if (opts.timeout) execOpts.timeout = opts.timeout;
|
|
79
84
|
const { stdout } = await execAsync(`git ${command}`, execOpts);
|
|
@@ -290,4 +295,3 @@ export async function pinAuthorInWorktree(worktreePath, projectDir, { env } = {}
|
|
|
290
295
|
|
|
291
296
|
return true;
|
|
292
297
|
}
|
|
293
|
-
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
|
+
import { createGeminiSpawner } from './geminiSpawnHelper.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Test a provider configuration by making a minimal API call.
|
|
@@ -22,11 +23,14 @@ import OpenAI from 'openai';
|
|
|
22
23
|
* @param {number} [config.apiTimeoutMs] - API timeout in milliseconds
|
|
23
24
|
* @returns {Promise<{success: boolean, message: string, details?: Object}>}
|
|
24
25
|
*/
|
|
25
|
-
export async function testProviderConnection(config) {
|
|
26
|
+
export async function testProviderConnection(config, deps = {}) {
|
|
26
27
|
const { kind = 'anthropic' } = config || {};
|
|
27
28
|
if (kind === 'openai') {
|
|
28
29
|
return testOpenAIConnection(config);
|
|
29
30
|
}
|
|
31
|
+
if (kind === 'google') {
|
|
32
|
+
return testGoogleConnection(config, deps);
|
|
33
|
+
}
|
|
30
34
|
return testAnthropicConnection(config);
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -127,6 +131,60 @@ async function testOpenAIChatEndpoint(client, config) {
|
|
|
127
131
|
});
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Google/Gemini connection test. Spawns `gemini -p "Hi" --output-format json`
|
|
136
|
+
* and checks for a clean exit. No SDK dependency needed.
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
async function testGoogleConnection(config, deps = {}) {
|
|
140
|
+
try {
|
|
141
|
+
const env = {};
|
|
142
|
+
if (config.authToken) env.GEMINI_API_KEY = config.authToken;
|
|
143
|
+
const timeoutMs = config.apiTimeoutMs || 30000;
|
|
144
|
+
const spawnGeminiProcess = deps.spawnGeminiProcess || createGeminiSpawner();
|
|
145
|
+
const child = spawnGeminiProcess({
|
|
146
|
+
command: 'gemini',
|
|
147
|
+
args: ['-p', 'Hi', '--output-format', 'json', '--skip-trust', '--approval-mode=auto_edit', '-m', 'gemini-2.5-flash'],
|
|
148
|
+
cwd: config.workingDirectory,
|
|
149
|
+
env,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return await new Promise((resolve) => {
|
|
153
|
+
let stderr = '';
|
|
154
|
+
let killed = false;
|
|
155
|
+
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
killed = true;
|
|
158
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
159
|
+
resolve(failureResponse(new Error(`Gemini CLI timed out after ${timeoutMs}ms`)));
|
|
160
|
+
}, timeoutMs);
|
|
161
|
+
|
|
162
|
+
child.stdout?.on('data', () => { /* drain */ });
|
|
163
|
+
child.stderr?.on('data', (d) => { stderr += d; });
|
|
164
|
+
child.on('error', (error) => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
if (killed) return;
|
|
167
|
+
if (error.code === 'ENOENT') {
|
|
168
|
+
resolve(failureResponse(new Error('Gemini CLI not found. Install via: npm install -g @google/gemini-cli')));
|
|
169
|
+
} else {
|
|
170
|
+
resolve(failureResponse(error));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
child.on('exit', (code) => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
if (killed) return;
|
|
176
|
+
if (code === 0) {
|
|
177
|
+
resolve(connectionSuccess({ model: 'gemini-2.5-flash' }));
|
|
178
|
+
} else {
|
|
179
|
+
resolve(failureResponse(new Error(stderr.trim() || `Gemini CLI exited with code ${code}`)));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return failureResponse(error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
130
188
|
function connectionSuccess(details) {
|
|
131
189
|
return {
|
|
132
190
|
success: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createClaudeCodeSpawner } from './nodeSpawnHelper.js';
|
|
2
2
|
import {
|
|
3
3
|
buildSystemPromptConfig,
|
|
4
|
+
getGeminiApprovalModeForSession,
|
|
4
5
|
getPermissionModeForSession,
|
|
5
6
|
getSandboxModeForSession,
|
|
6
7
|
} from './sessionPrompts.js';
|
|
@@ -64,6 +65,34 @@ function buildCodexQueryParams({
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Build query parameters for the Gemini adapter.
|
|
70
|
+
*
|
|
71
|
+
* Gemini CLI is prompt-driven via `-p` flag. It does not need Claude-specific
|
|
72
|
+
* options (permissionMode, settingSources, resume) or Codex-specific options
|
|
73
|
+
* (sandboxMode, effortLevel).
|
|
74
|
+
*
|
|
75
|
+
* @returns {Object}
|
|
76
|
+
*/
|
|
77
|
+
function buildGeminiQueryParams({
|
|
78
|
+
prompt, workingDirectory, controller, session, sessionId, systemPrompt, model, sessionEnv,
|
|
79
|
+
}) {
|
|
80
|
+
const isVCR = Boolean(process.env.VCR_MODE);
|
|
81
|
+
const effectiveModel = isVCR ? 'gemini-2.5-flash' : model;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
prompt,
|
|
85
|
+
options: {
|
|
86
|
+
cwd: workingDirectory,
|
|
87
|
+
abortController: controller,
|
|
88
|
+
env: sessionEnv,
|
|
89
|
+
model: effectiveModel,
|
|
90
|
+
approvalMode: getGeminiApprovalModeForSession(session?.mode),
|
|
91
|
+
systemPrompt: buildSystemPromptConfig(sessionId, session.projectId, systemPrompt, session.mode),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
67
96
|
/**
|
|
68
97
|
* Build query parameters for executing a session via the configured agent.
|
|
69
98
|
* Shared by runSession, continueSession, and continueSessionWithExistingMessage.
|
|
@@ -78,7 +107,7 @@ function buildCodexQueryParams({
|
|
|
78
107
|
* @param {string|null} options.model - Model to use
|
|
79
108
|
* @param {Object} options.sessionEnv - Environment variables for the session
|
|
80
109
|
* @param {string|null} [options.resumeSessionId] - Session ID to resume (null for new session)
|
|
81
|
-
* @param {string} [options.agentType] - 'claude-code' (default) | 'codex'
|
|
110
|
+
* @param {string} [options.agentType] - 'claude-code' (default) | 'codex' | 'gemini'
|
|
82
111
|
* @returns {Object} Query parameters for agent.execute()
|
|
83
112
|
*/
|
|
84
113
|
export function buildQueryParams(options) {
|
|
@@ -86,5 +115,8 @@ export function buildQueryParams(options) {
|
|
|
86
115
|
if (agentType === 'codex') {
|
|
87
116
|
return buildCodexQueryParams(options);
|
|
88
117
|
}
|
|
118
|
+
if (agentType === 'gemini') {
|
|
119
|
+
return buildGeminiQueryParams(options);
|
|
120
|
+
}
|
|
89
121
|
return buildClaudeCodeQueryParams(options);
|
|
90
122
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sessions, messages, attachments, conversations } from '../database.js';
|
|
2
2
|
import { createCodexSpawner } from './codexSpawnHelper.js';
|
|
3
|
+
import { createGeminiSpawner } from './geminiSpawnHelper.js';
|
|
3
4
|
import { resolveProviderFromModel, resolveProviderMetadataFromModel, buildSessionEnv } from './sessionProvider.js';
|
|
4
5
|
import { agentGateway } from '../agents/AgentGateway.js';
|
|
5
6
|
import { LoggingAgentWrapper } from '../agents/LoggingAgentWrapper.js';
|
|
@@ -37,6 +38,9 @@ function buildAgentConfig(agentType) {
|
|
|
37
38
|
if (agentType === 'codex') {
|
|
38
39
|
return { spawnCodexProcess: createCodexSpawner() };
|
|
39
40
|
}
|
|
41
|
+
if (agentType === 'gemini') {
|
|
42
|
+
return { spawnGeminiProcess: createGeminiSpawner() };
|
|
43
|
+
}
|
|
40
44
|
return {};
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -134,6 +134,28 @@ export function getSandboxModeForSession(mode) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Map session mode to Gemini CLI --approval-mode.
|
|
139
|
+
*
|
|
140
|
+
* plan -> plan
|
|
141
|
+
* standard -> auto_edit
|
|
142
|
+
* yolo -> yolo
|
|
143
|
+
*
|
|
144
|
+
* @param {string} mode - Session mode ('plan', 'standard', 'yolo')
|
|
145
|
+
* @returns {string} Gemini CLI approval mode
|
|
146
|
+
*/
|
|
147
|
+
export function getGeminiApprovalModeForSession(mode) {
|
|
148
|
+
switch (mode) {
|
|
149
|
+
case 'plan':
|
|
150
|
+
return 'plan';
|
|
151
|
+
case 'yolo':
|
|
152
|
+
return 'yolo';
|
|
153
|
+
case 'standard':
|
|
154
|
+
default:
|
|
155
|
+
return 'auto_edit';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
137
159
|
/** Plan mode system prompt instructions */
|
|
138
160
|
export const PLAN_MODE_PROMPT = `## Plan Mode Active
|
|
139
161
|
|