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
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ClaudeCodeAdapter } from './adapters/ClaudeCodeAdapter.js';
|
|
2
2
|
import { CodexAdapter } from './adapters/CodexAdapter.js';
|
|
3
|
+
import { GeminiAdapter } from './adapters/GeminiAdapter.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Factory/registry for agent adapters.
|
|
@@ -17,6 +18,7 @@ export class AgentGateway {
|
|
|
17
18
|
_registerDefaultAdapters() {
|
|
18
19
|
this.registerAdapter('claude-code', ClaudeCodeAdapter);
|
|
19
20
|
this.registerAdapter('codex', CodexAdapter);
|
|
21
|
+
this.registerAdapter('gemini', GeminiAdapter);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { BaseAgent } from '../BaseAgent.js';
|
|
2
|
+
import { executeGeminiCli } from './geminiCliRunner.js';
|
|
3
|
+
import { composeCliPrompt } from './cliUtils.js';
|
|
4
|
+
import { createGeminiSpawner } from '../../services/geminiSpawnHelper.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Module-level flag: once an ENOENT is observed for the Gemini CLI, remember
|
|
8
|
+
* it so subsequent calls can short-circuit.
|
|
9
|
+
*/
|
|
10
|
+
const geminiCliState = { unavailable: false };
|
|
11
|
+
|
|
12
|
+
function markGeminiCliUnavailable() {
|
|
13
|
+
geminiCliState.unavailable = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Test-only: reset the module-level ENOENT cache.
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
export function _resetGeminiCliUnavailableForTests() {
|
|
21
|
+
geminiCliState.unavailable = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adapter for Google Gemini CLI.
|
|
26
|
+
*
|
|
27
|
+
* Execution path:
|
|
28
|
+
* Spawns `gemini -p "prompt" --output-format stream-json -m <model>` and
|
|
29
|
+
* parses its newline-delimited JSON stdout. Uses {@link createGeminiEventMapper}
|
|
30
|
+
* to normalize events into the SDK-shaped envelope the rest of the app
|
|
31
|
+
* already understands for Claude Code.
|
|
32
|
+
*
|
|
33
|
+
* Capabilities:
|
|
34
|
+
* - streaming: true — stream-json provides real-time events
|
|
35
|
+
* - thinking: false — no separate thinking mode toggle
|
|
36
|
+
* - reasoningEffort: false — no effort level mapping yet
|
|
37
|
+
* - toolUse: true — Gemini CLI has built-in shell, file, and web tools
|
|
38
|
+
* - resume: false — no session resume support in headless mode
|
|
39
|
+
*/
|
|
40
|
+
export class GeminiAdapter extends BaseAgent {
|
|
41
|
+
static capabilities = Object.freeze({
|
|
42
|
+
streaming: true,
|
|
43
|
+
thinking: false,
|
|
44
|
+
reasoningEffort: false,
|
|
45
|
+
toolUse: true,
|
|
46
|
+
resume: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {Object} [opts]
|
|
51
|
+
* @param {Function} [opts.spawnGeminiProcess] - Optional DI for testing.
|
|
52
|
+
* Shape matches {@link createGeminiSpawner} output.
|
|
53
|
+
* @param {Object} [opts.rest] - Passed to {@link BaseAgent}.
|
|
54
|
+
*/
|
|
55
|
+
constructor({ spawnGeminiProcess, ...rest } = {}) {
|
|
56
|
+
super(rest);
|
|
57
|
+
this._spawnGemini = spawnGeminiProcess;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getCapabilities() {
|
|
61
|
+
return { ...GeminiAdapter.capabilities };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
supportsResume() {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Execute a Gemini query and yield SDK-shaped events.
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} queryParams
|
|
72
|
+
* @yields {Object} Normalized SDK events
|
|
73
|
+
*/
|
|
74
|
+
async *execute(queryParams, _meta) {
|
|
75
|
+
const options = queryParams.options || {};
|
|
76
|
+
yield* this._executeCli(queryParams, options);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_executeCli(queryParams, options) {
|
|
80
|
+
const child = this._spawnGeminiChild(queryParams, options);
|
|
81
|
+
return executeGeminiCli(child, queryParams, options, markGeminiCliUnavailable);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_spawnGeminiChild(queryParams, options) {
|
|
85
|
+
const spawnFn = this._spawnGemini ?? createGeminiSpawner();
|
|
86
|
+
const { cwd, env, abortController, model, systemPrompt, approvalMode } = options;
|
|
87
|
+
const composedPrompt = composeCliPrompt(systemPrompt, queryParams.prompt);
|
|
88
|
+
const args = ['-p', composedPrompt, '--output-format', 'stream-json', '--skip-trust', '-m', model];
|
|
89
|
+
if (approvalMode) {
|
|
90
|
+
args.push(`--approval-mode=${approvalMode}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
return spawnFn({ command: 'gemini', args, cwd, env, signal: abortController?.signal });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err?.code === 'ENOENT') {
|
|
97
|
+
geminiCliState.unavailable = true;
|
|
98
|
+
const notFound = new Error('Gemini CLI not found');
|
|
99
|
+
notFound.code = 'GEMINI_CLI_NOT_FOUND';
|
|
100
|
+
throw notFound;
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose a CLI prompt by prepending the system prompt (if provided) to the
|
|
3
|
+
* user prompt. Used by both Codex and Gemini CLI runners.
|
|
4
|
+
*
|
|
5
|
+
* @param {string|null} systemPrompt
|
|
6
|
+
* @param {string|null} prompt
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function composeCliPrompt(systemPrompt, prompt) {
|
|
10
|
+
const user = prompt ?? '';
|
|
11
|
+
if (typeof systemPrompt === 'string' && systemPrompt.length > 0) {
|
|
12
|
+
return `SYSTEM PROMPT:\n${systemPrompt}\n\nUSER:\n${user}`;
|
|
13
|
+
}
|
|
14
|
+
return user;
|
|
15
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import readline from 'readline';
|
|
2
2
|
import { createCodexEventMapper } from './codexEventMapper.js';
|
|
3
|
+
import { composeCliPrompt } from './cliUtils.js';
|
|
3
4
|
|
|
4
5
|
export async function *executeCodexCli(child, queryParams, options, markCliUnavailable) {
|
|
5
6
|
const state = new CliState(options.model, markCliUnavailable);
|
|
@@ -76,14 +77,6 @@ function writePromptToStdin(child, prompt) {
|
|
|
76
77
|
} catch { /* ignore */ }
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
function composeCliPrompt(systemPrompt, prompt) {
|
|
80
|
-
const user = prompt ?? '';
|
|
81
|
-
if (typeof systemPrompt === 'string' && systemPrompt.length > 0) {
|
|
82
|
-
return `SYSTEM PROMPT:\n${systemPrompt}\n\nUSER:\n${user}`;
|
|
83
|
-
}
|
|
84
|
-
return user;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
80
|
function attachStdoutReader(child, state) {
|
|
88
81
|
const rl = readline.createInterface({ input: child.stdout });
|
|
89
82
|
state.assign({ rl });
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { createGeminiEventMapper } from './geminiEventMapper.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Execute the Gemini CLI child process and yield SDK-shaped events.
|
|
6
|
+
*
|
|
7
|
+
* Unlike Codex, the prompt is NOT passed via stdin — it's passed as the `-p`
|
|
8
|
+
* CLI argument by GeminiAdapter._spawnGeminiChild(). This runner only reads
|
|
9
|
+
* stdout/stderr and handles process lifecycle.
|
|
10
|
+
*
|
|
11
|
+
* @param {import('child_process').ChildProcess} child
|
|
12
|
+
* @param {Object} queryParams
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {Function} markCliUnavailable
|
|
15
|
+
* @yields {Object} SDK-shaped events
|
|
16
|
+
*/
|
|
17
|
+
export async function *executeGeminiCli(child, queryParams, options, markCliUnavailable) {
|
|
18
|
+
const state = new CliState(options.model, markCliUnavailable);
|
|
19
|
+
|
|
20
|
+
attachAbortHandling(child, options.abortController, state);
|
|
21
|
+
attachStdoutReader(child, state);
|
|
22
|
+
attachStderrReader(child, state);
|
|
23
|
+
attachProcessLifecycleHandlers(child, state);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
yield* drainCliEvents(state);
|
|
27
|
+
} finally {
|
|
28
|
+
cleanupCli(options.abortController, state);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class CliState {
|
|
33
|
+
constructor(model, markCliUnavailable) {
|
|
34
|
+
this.pending = [];
|
|
35
|
+
this.error = null;
|
|
36
|
+
this.ended = false;
|
|
37
|
+
this.resolveNext = null;
|
|
38
|
+
this.rejectAll = null;
|
|
39
|
+
this.mapper = createGeminiEventMapper({ model });
|
|
40
|
+
this.rl = null;
|
|
41
|
+
this.killTimer = null;
|
|
42
|
+
this.onAbort = null;
|
|
43
|
+
this.stderrBuffer = '';
|
|
44
|
+
this.markCliUnavailable = markCliUnavailable;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
assign(patch) {
|
|
48
|
+
Object.assign(this, patch);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pushEvents(events) {
|
|
52
|
+
for (const event of events) this.pending.push(event);
|
|
53
|
+
this.tickWaiter();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
tickWaiter() {
|
|
57
|
+
if (!this.resolveNext) return;
|
|
58
|
+
const resolve = this.resolveNext;
|
|
59
|
+
this.resolveNext = null;
|
|
60
|
+
resolve();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
failWith(error) {
|
|
64
|
+
this.error = error;
|
|
65
|
+
if (this.rejectAll) this.rejectAll(error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
markEnded() {
|
|
69
|
+
this.ended = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function attachAbortHandling(child, abortController, state) {
|
|
74
|
+
const onAbort = () => {
|
|
75
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
try { child.kill('SIGKILL'); } catch { /* ignore */ }
|
|
78
|
+
}, 2000);
|
|
79
|
+
state.assign({ killTimer: timer });
|
|
80
|
+
};
|
|
81
|
+
state.assign({ onAbort });
|
|
82
|
+
abortController?.signal?.addEventListener('abort', onAbort);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function attachStdoutReader(child, state) {
|
|
86
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
87
|
+
state.assign({ rl });
|
|
88
|
+
rl.on('line', (line) => handleCliStdoutLine(line, state));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleCliStdoutLine(line, state) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed) return;
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(trimmed);
|
|
97
|
+
} catch {
|
|
98
|
+
return; // Ignore malformed JSON lines
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const mapped = state.mapper.map(parsed);
|
|
102
|
+
if (mapped.length > 0) state.pushEvents(mapped);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
state.failWith(error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function attachStderrReader(child, state) {
|
|
109
|
+
if (!child.stderr) return;
|
|
110
|
+
child.stderr.on('data', (chunk) => {
|
|
111
|
+
if (state.error || state.ended) return;
|
|
112
|
+
state.assign({ stderrBuffer: state.stderrBuffer + chunk.toString() });
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function attachProcessLifecycleHandlers(child, state) {
|
|
117
|
+
child.on('error', (error) => {
|
|
118
|
+
if (error?.code === 'ENOENT') {
|
|
119
|
+
state.markCliUnavailable?.();
|
|
120
|
+
state.failWith(makeCliNotFoundError());
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
state.failWith(error);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
child.on('exit', (code) => handleChildExit(code, state));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function makeCliNotFoundError() {
|
|
130
|
+
const error = new Error('Gemini CLI not found');
|
|
131
|
+
error.code = 'GEMINI_CLI_NOT_FOUND';
|
|
132
|
+
return error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleChildExit(code, state) {
|
|
136
|
+
state.markEnded();
|
|
137
|
+
if (code !== 0 && !state.error) {
|
|
138
|
+
state.failWith(makeCliExitError(code, state.stderrBuffer));
|
|
139
|
+
} else if (!state.error) {
|
|
140
|
+
finalizeMappedEvents(state);
|
|
141
|
+
}
|
|
142
|
+
state.tickWaiter();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function makeCliExitError(code, stderrBuffer) {
|
|
146
|
+
const stderrTrimmed = stderrBuffer.trim();
|
|
147
|
+
const error = stderrTrimmed.length > 0
|
|
148
|
+
? new Error(stderrTrimmed)
|
|
149
|
+
: new Error(`Gemini CLI exited with code ${code}`);
|
|
150
|
+
error.code = 'GEMINI_CLI_EXIT';
|
|
151
|
+
error.exitCode = code;
|
|
152
|
+
return error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function finalizeMappedEvents(state) {
|
|
156
|
+
try {
|
|
157
|
+
const events = state.mapper.finalize();
|
|
158
|
+
if (events.length > 0) state.pushEvents(events);
|
|
159
|
+
} catch { /* ignore */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function *drainCliEvents(state) {
|
|
163
|
+
while (true) {
|
|
164
|
+
if (state.error) throw state.error;
|
|
165
|
+
if (state.pending.length > 0) {
|
|
166
|
+
yield state.pending.shift();
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (state.ended) break;
|
|
170
|
+
await new Promise((resolve, reject) => {
|
|
171
|
+
state.assign({ resolveNext: resolve, rejectAll: reject });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (state.error) throw state.error;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cleanupCli(abortController, state) {
|
|
178
|
+
if (state.onAbort) {
|
|
179
|
+
abortController?.signal?.removeEventListener('abort', state.onAbort);
|
|
180
|
+
}
|
|
181
|
+
if (state.killTimer) clearTimeout(state.killTimer);
|
|
182
|
+
try { state.rl?.close(); } catch { /* ignore */ }
|
|
183
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI event mapper.
|
|
3
|
+
*
|
|
4
|
+
* Translates Gemini CLI `--output-format stream-json` JSONL events into the
|
|
5
|
+
* normalized SDK-shaped events that Circus Chief already understands:
|
|
6
|
+
*
|
|
7
|
+
* - system(init)
|
|
8
|
+
* - stream_event(content_block_delta)
|
|
9
|
+
* - assistant
|
|
10
|
+
* - tool_result
|
|
11
|
+
* - result(success, usage)
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @param {string} [options.model] - Model name to surface in the system(init) event.
|
|
15
|
+
* @returns {{
|
|
16
|
+
* map: (geminiEvent: Object) => Array<Object>,
|
|
17
|
+
* reset: () => void,
|
|
18
|
+
* finalize: () => Array<Object>
|
|
19
|
+
* }}
|
|
20
|
+
*/
|
|
21
|
+
export function createGeminiEventMapper({ model } = {}) {
|
|
22
|
+
const state = new GeminiMapperState();
|
|
23
|
+
const warnedUnknownTypes = new Set();
|
|
24
|
+
|
|
25
|
+
function map(geminiEvent) {
|
|
26
|
+
if (!geminiEvent || typeof geminiEvent !== 'object') return [];
|
|
27
|
+
|
|
28
|
+
const type = geminiEvent.type;
|
|
29
|
+
|
|
30
|
+
if (type === 'init') {
|
|
31
|
+
return handleInit(geminiEvent, model);
|
|
32
|
+
}
|
|
33
|
+
if (type === 'message') {
|
|
34
|
+
return handleMessage(geminiEvent, state);
|
|
35
|
+
}
|
|
36
|
+
if (type === 'tool_use') {
|
|
37
|
+
return [...state.flushAccumulatedText(), ...handleToolUse(geminiEvent, state)];
|
|
38
|
+
}
|
|
39
|
+
if (type === 'tool_result') {
|
|
40
|
+
return [...state.flushAccumulatedText(), ...handleToolResult(geminiEvent, state)];
|
|
41
|
+
}
|
|
42
|
+
if (type === 'result') {
|
|
43
|
+
return [...state.flushAccumulatedText(), ...state.onResult(geminiEvent)];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Unknown event type — warn once
|
|
47
|
+
if (type && !warnedUnknownTypes.has(type)) {
|
|
48
|
+
warnedUnknownTypes.add(type);
|
|
49
|
+
console.warn(`[geminiEventMapper] Unknown Gemini event type: "${type}"`);
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
map,
|
|
56
|
+
reset: () => state.reset(),
|
|
57
|
+
finalize: () => state.finalize(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- State class -----------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
class GeminiMapperState {
|
|
64
|
+
constructor() {
|
|
65
|
+
this.reset();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
reset() {
|
|
69
|
+
this.lastUsage = null;
|
|
70
|
+
this.terminated = false;
|
|
71
|
+
this.pendingToolUse = new Map();
|
|
72
|
+
this.accumulatedText = '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Flush any accumulated delta text as a synthetic `assistant` event.
|
|
77
|
+
* Returns an array (possibly empty) of events to prepend.
|
|
78
|
+
*/
|
|
79
|
+
flushAccumulatedText() {
|
|
80
|
+
if (!this.accumulatedText) return [];
|
|
81
|
+
const text = this.accumulatedText;
|
|
82
|
+
this.accumulatedText = '';
|
|
83
|
+
return [{
|
|
84
|
+
type: 'assistant',
|
|
85
|
+
message: { content: [{ type: 'text', text }] },
|
|
86
|
+
}];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
appendDeltaText(text) {
|
|
90
|
+
this.accumulatedText += text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
clearAccumulatedText() {
|
|
94
|
+
this.accumulatedText = '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
finalize() {
|
|
98
|
+
if (this.terminated) return [];
|
|
99
|
+
this.terminated = true;
|
|
100
|
+
return [...this.flushAccumulatedText(), this.buildResultEvent()];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onResult(evt) {
|
|
104
|
+
if (evt.stats) {
|
|
105
|
+
this.lastUsage = {
|
|
106
|
+
input_tokens: evt.stats.input_tokens || 0,
|
|
107
|
+
output_tokens: evt.stats.output_tokens || 0,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
this.terminated = true;
|
|
111
|
+
return [this.buildResultEvent()];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
buildResultEvent() {
|
|
115
|
+
const usage = this.lastUsage || { input_tokens: 0, output_tokens: 0 };
|
|
116
|
+
return {
|
|
117
|
+
type: 'result',
|
|
118
|
+
subtype: 'success',
|
|
119
|
+
usage: {
|
|
120
|
+
input_tokens: usage.input_tokens || 0,
|
|
121
|
+
output_tokens: usage.output_tokens || 0,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Event handlers --------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function handleInit(evt, constructorModel) {
|
|
130
|
+
return [{
|
|
131
|
+
type: 'system',
|
|
132
|
+
subtype: 'init',
|
|
133
|
+
session_id: evt.session_id || `gemini-${Date.now()}`,
|
|
134
|
+
model: evt.model || constructorModel || undefined,
|
|
135
|
+
}];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleMessage(evt, state) {
|
|
139
|
+
// Ignore user message echoes
|
|
140
|
+
if (evt.role === 'user') return [];
|
|
141
|
+
|
|
142
|
+
if (evt.role === 'assistant') {
|
|
143
|
+
const text = evt.content || '';
|
|
144
|
+
|
|
145
|
+
// Delta (streaming partial) — accumulate for later persistence AND emit for live streaming
|
|
146
|
+
if (evt.delta) {
|
|
147
|
+
state.appendDeltaText(text);
|
|
148
|
+
return [{
|
|
149
|
+
type: 'stream_event',
|
|
150
|
+
event: {
|
|
151
|
+
type: 'content_block_delta',
|
|
152
|
+
delta: { type: 'text_delta', text },
|
|
153
|
+
},
|
|
154
|
+
}];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Full message (non-delta) replaces any prior streamed delta accumulation.
|
|
158
|
+
// Some Gemini streams include both live deltas and a final full message; only
|
|
159
|
+
// the full message should be persisted in that mixed case.
|
|
160
|
+
state.clearAccumulatedText();
|
|
161
|
+
return [{
|
|
162
|
+
type: 'assistant',
|
|
163
|
+
message: { content: [{ type: 'text', text }] },
|
|
164
|
+
}];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleToolUse(evt, state) {
|
|
171
|
+
const toolName = evt.tool_name || 'unknown';
|
|
172
|
+
const toolId = evt.tool_id || `tool-${Date.now()}`;
|
|
173
|
+
const parameters = evt.parameters || {};
|
|
174
|
+
|
|
175
|
+
// Track for matching tool_result back
|
|
176
|
+
state.pendingToolUse.set(toolId, toolName);
|
|
177
|
+
|
|
178
|
+
return [{
|
|
179
|
+
type: 'tool_result',
|
|
180
|
+
tool_name: toolName,
|
|
181
|
+
content: JSON.stringify(parameters),
|
|
182
|
+
}];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function handleToolResult(evt, state) {
|
|
186
|
+
const toolId = evt.tool_id || '';
|
|
187
|
+
const toolName = state.pendingToolUse.get(toolId) || 'unknown';
|
|
188
|
+
const output = evt.output || '';
|
|
189
|
+
|
|
190
|
+
return [{
|
|
191
|
+
type: 'tool_result',
|
|
192
|
+
tool_name: toolName,
|
|
193
|
+
content: output,
|
|
194
|
+
}];
|
|
195
|
+
}
|
|
@@ -5,10 +5,11 @@ import { normalizeCommitAttributionOverride } from '../../../shared/src/contract
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Valid values for `providers.kind`. Maps 1:1 to an agent adapter:
|
|
8
|
-
* - 'anthropic'
|
|
8
|
+
* - 'anthropic' ��� 'claude-code'
|
|
9
9
|
* - 'openai' → 'codex'
|
|
10
|
+
* - 'google' → 'gemini'
|
|
10
11
|
*/
|
|
11
|
-
export const PROVIDER_KINDS = Object.freeze(['anthropic', 'openai']);
|
|
12
|
+
export const PROVIDER_KINDS = Object.freeze(['anthropic', 'openai', 'google']);
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Mapping from provider kind to the agent adapter that should drive sessions
|
|
@@ -17,6 +18,7 @@ export const PROVIDER_KINDS = Object.freeze(['anthropic', 'openai']);
|
|
|
17
18
|
export const AGENT_TYPE_BY_KIND = Object.freeze({
|
|
18
19
|
anthropic: 'claude-code',
|
|
19
20
|
openai: 'codex',
|
|
21
|
+
google: 'gemini',
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
const BUILT_IN_MUTABLE_FIELDS = Object.freeze(['commitAttributionOverride']);
|
|
@@ -255,6 +255,15 @@ export const allMigrations = validateMigrations([
|
|
|
255
255
|
// --- Update built-in Opus model to 4.7 ---
|
|
256
256
|
pr.get('providers-update-built-in-opus-4-7'),
|
|
257
257
|
|
|
258
|
+
// --- Widen providers kind CHECK constraint to include 'google' ---
|
|
259
|
+
pr.get('providers-widen-kind-check-google'),
|
|
260
|
+
|
|
261
|
+
// --- Seed built-in Google provider + Gemini models ---
|
|
262
|
+
pr.get('providers-seed-built-in-google'),
|
|
263
|
+
|
|
264
|
+
// --- Update expired Gemini Flash Lite preview model to stable GA ---
|
|
265
|
+
pr.get('providers-update-gemini-flash-lite-model'),
|
|
266
|
+
|
|
258
267
|
// --- Project session defaults: add 'current' git mode ---
|
|
259
268
|
p.get('project_session_defaults-git_mode-add-current'),
|
|
260
269
|
|