@pixelbyte-software/pixcode 1.51.2 → 1.51.4
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/CODE_OF_CONDUCT.md +41 -41
- package/CONTRIBUTING.md +155 -155
- package/LICENSE +718 -718
- package/README.de.md +169 -169
- package/README.ja.md +167 -167
- package/README.ko.md +167 -167
- package/README.md +419 -419
- package/README.ru.md +169 -169
- package/README.tr.md +298 -298
- package/README.zh-CN.md +167 -167
- package/SECURITY.md +46 -46
- package/dist/api-automation.html +110 -110
- package/dist/api-docs.html +548 -548
- package/dist/assets/index-B9N-gfOQ.css +32 -0
- package/dist/assets/{index-EN9ngyxf.js → index-HfGHXhD6.js} +175 -175
- package/dist/clear-cache.html +85 -85
- package/dist/convert-icons.md +52 -52
- package/dist/docs.html +308 -308
- package/dist/favicon.svg +8 -8
- package/dist/features.html +133 -133
- package/dist/generate-icons.js +48 -48
- package/dist/humans.txt +15 -15
- package/dist/icons/codex-white.svg +3 -3
- package/dist/icons/codex.svg +3 -3
- package/dist/icons/cursor-white.svg +11 -11
- package/dist/icons/icon-128x128.svg +9 -9
- package/dist/icons/icon-144x144.svg +9 -9
- package/dist/icons/icon-152x152.svg +9 -9
- package/dist/icons/icon-192x192.svg +9 -9
- package/dist/icons/icon-384x384.svg +9 -9
- package/dist/icons/icon-512x512.svg +9 -9
- package/dist/icons/icon-72x72.svg +9 -9
- package/dist/icons/icon-96x96.svg +9 -9
- package/dist/icons/icon-template.svg +9 -9
- package/dist/icons/qwen-logo.svg +14 -14
- package/dist/index.html +59 -59
- package/dist/landing.html +268 -268
- package/dist/llms-full.txt +119 -119
- package/dist/llms.txt +53 -53
- package/dist/logo.svg +12 -12
- package/dist/manifest.json +60 -60
- package/dist/openapi.yaml +1696 -1696
- package/dist/orchestration.html +125 -125
- package/dist/robots.txt +4 -4
- package/dist/site.css +692 -692
- package/dist/sitemap.xml +51 -51
- package/dist/sw.js +132 -132
- package/dist-server/server/cli.js +96 -96
- package/dist-server/server/daemon/manager.js +33 -33
- package/dist-server/server/daemon-manager.js +64 -64
- package/dist-server/server/database/db.js +14 -2
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +191 -31
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +16 -5
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js +84 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js +43 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js.map +1 -0
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +55 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
- package/dist-server/server/modules/orchestration/index.js +1 -0
- package/dist-server/server/modules/orchestration/index.js.map +1 -1
- package/dist-server/server/routes/auth.js +12 -5
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/commands.js +25 -25
- package/dist-server/server/routes/git.js +29 -17
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/live-view.js +46 -46
- package/dist-server/server/routes/platformization.js +7 -6
- package/dist-server/server/routes/platformization.js.map +1 -1
- package/dist-server/server/services/hermes-gateway.js +310 -0
- package/dist-server/server/services/hermes-gateway.js.map +1 -1
- package/dist-server/server/services/platformization.js +58 -2
- package/dist-server/server/services/platformization.js.map +1 -1
- package/dist-server/server/services/public-api-manifest.js +59 -51
- package/dist-server/server/services/public-api-manifest.js.map +1 -1
- package/package.json +222 -222
- package/scripts/fix-node-pty.js +67 -67
- package/scripts/github/create-v1.38-issues.mjs +351 -351
- package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
- package/scripts/hermes/configure-pixcode-mcp.mjs +165 -163
- package/scripts/hermes/pixcode-mcp-server.mjs +1009 -958
- package/scripts/smoke/changes-panel-layout.mjs +48 -48
- package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
- package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
- package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
- package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
- package/scripts/smoke/chat-session-state.mjs +19 -19
- package/scripts/smoke/code-editor-theme.mjs +55 -55
- package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
- package/scripts/smoke/command-center-agent-writes.mjs +79 -79
- package/scripts/smoke/command-center-non-git.mjs +46 -46
- package/scripts/smoke/context-packet.mjs +43 -43
- package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
- package/scripts/smoke/daemon-entrypoint.mjs +20 -20
- package/scripts/smoke/default-landing-routing.mjs +33 -33
- package/scripts/smoke/desktop-native-notifications.mjs +30 -30
- package/scripts/smoke/desktop-tray-icon.mjs +33 -33
- package/scripts/smoke/discord-release-workflow.mjs +24 -24
- package/scripts/smoke/git-install-update.mjs +255 -255
- package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
- package/scripts/smoke/hermes-api-install.mjs +56 -56
- package/scripts/smoke/hermes-gateway-persistence.mjs +104 -104
- package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +426 -367
- package/scripts/smoke/hermes-rest-chat-api.mjs +162 -162
- package/scripts/smoke/hermes-rest-chat-live.mjs +45 -45
- package/scripts/smoke/hermes-rest-codex-launch.mjs +209 -209
- package/scripts/smoke/hermes-rest-gateway.mjs +79 -70
- package/scripts/smoke/hermes-rest-live.mjs +42 -42
- package/scripts/smoke/hermes-roundtrip.mjs +167 -167
- package/scripts/smoke/hermes-settings-commands.mjs +349 -346
- package/scripts/smoke/hermes-smoke-launcher-guard.mjs +34 -34
- package/scripts/smoke/live-view-diagnostics.mjs +53 -53
- package/scripts/smoke/live-view-environment.mjs +92 -92
- package/scripts/smoke/live-view-integration.mjs +450 -450
- package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
- package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
- package/scripts/smoke/model-registry.mjs +36 -36
- package/scripts/smoke/multi-project-ui.mjs +45 -45
- package/scripts/smoke/multi-worker-slots.mjs +42 -42
- package/scripts/smoke/notification-center.mjs +87 -87
- package/scripts/smoke/notification-inapp-preference.mjs +23 -23
- package/scripts/smoke/notification-taxonomy.mjs +58 -58
- package/scripts/smoke/orchestration-api.mjs +172 -172
- package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
- package/scripts/smoke/orchestration-live-run.mjs +176 -176
- package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
- package/scripts/smoke/orchestration-model-sync.mjs +30 -30
- package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
- package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
- package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
- package/scripts/smoke/permission-policy.mjs +50 -50
- package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -167
- package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
- package/scripts/smoke/provider-rest-api.mjs +124 -124
- package/scripts/smoke/provider-selection-status.mjs +52 -52
- package/scripts/smoke/run-state-refresh.mjs +52 -52
- package/scripts/smoke/runtime-manager.mjs +99 -99
- package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
- package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
- package/scripts/smoke/static-root-routing.mjs +21 -21
- package/scripts/smoke/strict-handoff-compact.mjs +60 -60
- package/scripts/smoke/taskmaster-config.mjs +24 -24
- package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
- package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
- package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
- package/scripts/smoke/telegram-control.mjs +242 -242
- package/scripts/smoke/tunnel-persistence.mjs +56 -56
- package/scripts/smoke/update-issue-progress.mjs +69 -69
- package/scripts/smoke/update-ux.mjs +55 -55
- package/scripts/smoke/v138-completion.mjs +132 -132
- package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
- package/scripts/smoke/v138-diagnostics.mjs +63 -63
- package/scripts/smoke/v138-issue-planner.mjs +33 -33
- package/scripts/smoke/v143-remote-control.mjs +76 -76
- package/scripts/smoke/v144-production-loop.mjs +47 -47
- package/scripts/smoke/v145-platformization.mjs +46 -46
- package/scripts/smoke/v146-control-room-ui.mjs +150 -150
- package/scripts/smoke/version-modal-autoshow.mjs +29 -29
- package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
- package/scripts/smoke/vscode-workbench-polish.mjs +461 -436
- package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
- package/scripts/smoke/workflow-templates.mjs +43 -43
- package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
- package/scripts/update-git-install.mjs +293 -293
- package/server/claude-sdk.js +920 -920
- package/server/cli.js +1039 -1039
- package/server/constants/config.js +4 -4
- package/server/cursor-cli.js +344 -344
- package/server/daemon/manager.js +563 -563
- package/server/daemon-manager.js +964 -964
- package/server/database/db.js +908 -895
- package/server/database/json-store.js +197 -197
- package/server/gemini-cli.js +550 -550
- package/server/gemini-response-handler.js +79 -79
- package/server/index.js +201 -30
- package/server/load-env.js +35 -35
- package/server/middleware/auth.js +171 -156
- package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
- package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
- package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
- package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
- package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
- package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/adapters/json-event.adapter.test.ts +60 -0
- package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +101 -0
- package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/agent-card.ts +55 -55
- package/server/modules/orchestration/a2a/routes.ts +590 -590
- package/server/modules/orchestration/a2a/task-store.ts +178 -178
- package/server/modules/orchestration/a2a/types.ts +126 -126
- package/server/modules/orchestration/a2a/validator.ts +113 -113
- package/server/modules/orchestration/hermes/hermes.routes.ts +642 -583
- package/server/modules/orchestration/index.ts +101 -100
- package/server/modules/orchestration/preview/port-watcher.ts +112 -112
- package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
- package/server/modules/orchestration/preview/types.ts +19 -19
- package/server/modules/orchestration/security/permission-policy.ts +401 -401
- package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
- package/server/modules/orchestration/tasks/orchestration-task.routes.ts +64 -64
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +209 -209
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
- package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
- package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
- package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
- package/server/modules/orchestration/workflows/context-packet.ts +186 -186
- package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
- package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
- package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
- package/server/modules/orchestration/workflows/workflow-runner.ts +2070 -2070
- package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
- package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
- package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
- package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
- package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
- package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
- package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
- package/server/modules/orchestration/workspace/path-safety.ts +55 -55
- package/server/modules/orchestration/workspace/types.ts +52 -52
- package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
- package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
- package/server/modules/providers/index.ts +2 -2
- package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
- package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
- package/server/modules/providers/list/claude/claude.provider.ts +15 -15
- package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
- package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
- package/server/modules/providers/list/codex/codex.provider.ts +15 -15
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
- package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
- package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
- package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
- package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
- package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
- package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
- package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
- package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
- package/server/modules/providers/provider.registry.ts +40 -40
- package/server/modules/providers/provider.routes.ts +944 -944
- package/server/modules/providers/services/mcp.service.ts +86 -86
- package/server/modules/providers/services/provider-auth.service.ts +26 -26
- package/server/modules/providers/services/sessions.service.ts +45 -45
- package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
- package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
- package/server/modules/providers/shared/provider-configs.ts +142 -142
- package/server/modules/providers/tests/mcp.test.ts +293 -293
- package/server/openai-codex.js +462 -462
- package/server/opencode-cli.js +491 -491
- package/server/opencode-response-handler.js +111 -111
- package/server/projects.js +3008 -3008
- package/server/qwen-code-cli.js +410 -410
- package/server/qwen-response-handler.js +73 -73
- package/server/routes/agent.js +1435 -1435
- package/server/routes/auth.js +154 -146
- package/server/routes/codex.js +20 -20
- package/server/routes/commands.js +570 -570
- package/server/routes/cursor.js +61 -61
- package/server/routes/diagnostics.js +41 -41
- package/server/routes/gemini.js +25 -25
- package/server/routes/git.js +1650 -1635
- package/server/routes/live-view.js +411 -411
- package/server/routes/mcp-utils.js +13 -13
- package/server/routes/messages.js +62 -62
- package/server/routes/network.js +125 -125
- package/server/routes/platformization.js +198 -197
- package/server/routes/plugins.js +320 -320
- package/server/routes/production-agent-loop.js +90 -90
- package/server/routes/projects.js +917 -917
- package/server/routes/public-api.js +34 -34
- package/server/routes/qwen.js +27 -27
- package/server/routes/remote.js +55 -55
- package/server/routes/settings.js +321 -321
- package/server/routes/telegram.js +140 -140
- package/server/routes/user.js +125 -125
- package/server/routes/webhooks.js +63 -63
- package/server/services/control-room.js +102 -102
- package/server/services/diagnostics.js +165 -165
- package/server/services/external-access.js +375 -375
- package/server/services/hermes-gateway.js +1562 -1247
- package/server/services/hermes-install-jobs.js +729 -729
- package/server/services/install-jobs.js +715 -715
- package/server/services/live-view.js +956 -956
- package/server/services/managed-runtimes.js +493 -493
- package/server/services/model-registry.js +144 -144
- package/server/services/notification-orchestrator.js +365 -365
- package/server/services/notification-taxonomy.js +204 -204
- package/server/services/platformization.js +844 -779
- package/server/services/production-agent-loop.js +248 -248
- package/server/services/provider-cli-versions.js +149 -149
- package/server/services/provider-credentials.js +189 -189
- package/server/services/provider-models.js +396 -396
- package/server/services/public-api-manifest.js +190 -182
- package/server/services/remote-connection.js +127 -127
- package/server/services/runtime-manager.js +323 -323
- package/server/services/startup-update.js +234 -234
- package/server/services/telegram/bot.js +331 -331
- package/server/services/telegram/control-center.js +979 -979
- package/server/services/telegram/telegram-http-client.js +151 -151
- package/server/services/telegram/translations.js +340 -340
- package/server/services/vapid-keys.js +36 -36
- package/server/services/webhooks.js +216 -216
- package/server/sessionManager.js +225 -225
- package/server/shared/interfaces.ts +54 -54
- package/server/shared/types.ts +172 -172
- package/server/shared/utils.ts +193 -193
- package/server/tsconfig.json +36 -36
- package/server/utils/colors.js +21 -21
- package/server/utils/commandParser.js +305 -305
- package/server/utils/frontmatter.js +18 -18
- package/server/utils/gitConfig.js +34 -34
- package/server/utils/plugin-loader.js +457 -457
- package/server/utils/plugin-process-manager.js +185 -185
- package/server/utils/port-access.js +209 -209
- package/server/utils/runtime-paths.js +37 -37
- package/server/utils/url-detection.js +71 -71
- package/server/vite-daemon.js +79 -79
- package/shared/modelConstants.js +161 -161
- package/shared/networkHosts.js +22 -22
- package/dist/assets/index-DMz0zv6T.css +0 -32
package/server/gemini-cli.js
CHANGED
|
@@ -1,550 +1,550 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { promises as fs } from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
import crossSpawn from 'cross-spawn';
|
|
7
|
-
|
|
8
|
-
import sessionManager from './sessionManager.js';
|
|
9
|
-
import GeminiResponseHandler from './gemini-response-handler.js';
|
|
10
|
-
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
11
|
-
import { buildSpawnEnv } from './services/provider-credentials.js';
|
|
12
|
-
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
13
|
-
import { createNormalizedMessage } from './shared/utils.js';
|
|
14
|
-
|
|
15
|
-
// Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
|
|
16
|
-
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
17
|
-
let activeGeminiProcesses = new Map(); // Track active processes by session ID
|
|
18
|
-
const DEFAULT_CLI_IDLE_TIMEOUT_MS = 600000;
|
|
19
|
-
|
|
20
|
-
function readCliIdleTimeoutMs() {
|
|
21
|
-
const configured = Number.parseInt(process.env.PIXCODE_CLI_IDLE_TIMEOUT_MS || '', 10);
|
|
22
|
-
return Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_CLI_IDLE_TIMEOUT_MS;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Auto-create `~/.gemini/settings.json` when the user has signed in via OAuth
|
|
27
|
-
* (so `oauth_creds.json` exists) but never opened the Gemini TUI to write the
|
|
28
|
-
* `selectedAuthType` field. Without this, `gemini --prompt … --output-format
|
|
29
|
-
* stream-json --yolo` exits 41 with "Please set an Auth method in your
|
|
30
|
-
* settings.json" — even though credentials are perfectly valid. We respect a
|
|
31
|
-
* pre-existing `settings.json` and never overwrite a chosen auth type.
|
|
32
|
-
*
|
|
33
|
-
* Triggered every spawn (cheap: one fs.access + maybe one tiny write).
|
|
34
|
-
*/
|
|
35
|
-
async function ensureGeminiSettingsJson() {
|
|
36
|
-
const home = os.homedir();
|
|
37
|
-
const dir = path.join(home, '.gemini');
|
|
38
|
-
const settingsPath = path.join(dir, 'settings.json');
|
|
39
|
-
const oauthPath = path.join(dir, 'oauth_creds.json');
|
|
40
|
-
const apiKey = process.env.GEMINI_API_KEY;
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
await fs.access(settingsPath);
|
|
44
|
-
return; // user-managed, leave it.
|
|
45
|
-
} catch { /* missing — we may create it */ }
|
|
46
|
-
|
|
47
|
-
let selectedAuthType = null;
|
|
48
|
-
if (apiKey && apiKey.trim()) {
|
|
49
|
-
selectedAuthType = 'gemini-api-key';
|
|
50
|
-
} else {
|
|
51
|
-
try {
|
|
52
|
-
await fs.access(oauthPath);
|
|
53
|
-
selectedAuthType = 'oauth-personal';
|
|
54
|
-
} catch { /* no oauth either */ }
|
|
55
|
-
}
|
|
56
|
-
if (!selectedAuthType) return;
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
await fs.mkdir(dir, { recursive: true });
|
|
60
|
-
await fs.writeFile(settingsPath, JSON.stringify({ selectedAuthType }, null, 2), { mode: 0o600 });
|
|
61
|
-
console.log(`[gemini] auto-bootstrapped ~/.gemini/settings.json with selectedAuthType="${selectedAuthType}"`);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.warn('[gemini] failed to bootstrap settings.json:', error?.message || error);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function spawnGemini(command, options = {}, ws) {
|
|
68
|
-
const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
|
|
69
|
-
let capturedSessionId = sessionId; // Track session ID throughout the process
|
|
70
|
-
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
|
71
|
-
let assistantBlocks = []; // Accumulate the full response blocks including tools
|
|
72
|
-
|
|
73
|
-
// Use tools settings passed from frontend, or defaults
|
|
74
|
-
const settings = toolsSettings || {
|
|
75
|
-
allowedTools: [],
|
|
76
|
-
disallowedTools: [],
|
|
77
|
-
skipPermissions: false
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Build Gemini CLI command - start with print/resume flags first
|
|
81
|
-
const args = [];
|
|
82
|
-
|
|
83
|
-
// Add prompt flag with command if we have a command
|
|
84
|
-
if (command && command.trim()) {
|
|
85
|
-
args.push('--prompt', command);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// If we have a sessionId, we want to resume
|
|
89
|
-
if (sessionId) {
|
|
90
|
-
const session = sessionManager.getSession(sessionId);
|
|
91
|
-
if (session && session.cliSessionId) {
|
|
92
|
-
args.push('--resume', session.cliSessionId);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Use cwd (actual project directory) instead of projectPath (Gemini's metadata directory)
|
|
97
|
-
// Clean the path by removing any non-printable characters
|
|
98
|
-
const cleanPath = (cwd || projectPath || process.cwd()).replace(/[^\x20-\x7E]/g, '').trim();
|
|
99
|
-
const workingDir = cleanPath;
|
|
100
|
-
|
|
101
|
-
// Handle images by saving them to temporary files and passing paths to Gemini
|
|
102
|
-
const tempImagePaths = [];
|
|
103
|
-
let tempDir = null;
|
|
104
|
-
if (images && images.length > 0) {
|
|
105
|
-
try {
|
|
106
|
-
// Create temp directory in the project directory so Gemini can access it
|
|
107
|
-
tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
|
|
108
|
-
await fs.mkdir(tempDir, { recursive: true });
|
|
109
|
-
|
|
110
|
-
// Save each image to a temp file
|
|
111
|
-
for (const [index, image] of images.entries()) {
|
|
112
|
-
// Extract base64 data and mime type
|
|
113
|
-
const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
|
|
114
|
-
if (!matches) {
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const [, mimeType, base64Data] = matches;
|
|
119
|
-
const extension = mimeType.split('/')[1] || 'png';
|
|
120
|
-
const filename = `image_${index}.${extension}`;
|
|
121
|
-
const filepath = path.join(tempDir, filename);
|
|
122
|
-
|
|
123
|
-
// Write base64 data to file
|
|
124
|
-
await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
|
|
125
|
-
tempImagePaths.push(filepath);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Include the full image paths in the prompt for Gemini to reference
|
|
129
|
-
// Gemini CLI can read images from file paths in the prompt
|
|
130
|
-
if (tempImagePaths.length > 0 && command && command.trim()) {
|
|
131
|
-
const imageNote = `\n\n[Images given: ${tempImagePaths.length} images are located at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
|
|
132
|
-
const modifiedCommand = command + imageNote;
|
|
133
|
-
|
|
134
|
-
// Update the command in args
|
|
135
|
-
const promptIndex = args.indexOf('--prompt');
|
|
136
|
-
if (promptIndex !== -1 && args[promptIndex + 1] === command) {
|
|
137
|
-
args[promptIndex + 1] = modifiedCommand;
|
|
138
|
-
} else if (promptIndex !== -1) {
|
|
139
|
-
// If we're using context, update the full prompt
|
|
140
|
-
args[promptIndex + 1] = args[promptIndex + 1] + imageNote;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} catch (error) {
|
|
144
|
-
console.error('Error processing images for Gemini:', error);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Add basic flags for Gemini
|
|
149
|
-
if (options.debug) {
|
|
150
|
-
args.push('--debug');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Add MCP config flag only if MCP servers are configured
|
|
154
|
-
try {
|
|
155
|
-
const geminiConfigPath = path.join(os.homedir(), '.gemini.json');
|
|
156
|
-
let hasMcpServers = false;
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
await fs.access(geminiConfigPath);
|
|
160
|
-
const geminiConfigRaw = await fs.readFile(geminiConfigPath, 'utf8');
|
|
161
|
-
const geminiConfig = JSON.parse(geminiConfigRaw);
|
|
162
|
-
|
|
163
|
-
// Check global MCP servers
|
|
164
|
-
if (geminiConfig.mcpServers && Object.keys(geminiConfig.mcpServers).length > 0) {
|
|
165
|
-
hasMcpServers = true;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Check project-specific MCP servers
|
|
169
|
-
if (!hasMcpServers && geminiConfig.geminiProjects) {
|
|
170
|
-
const currentProjectPath = process.cwd();
|
|
171
|
-
const projectConfig = geminiConfig.geminiProjects[currentProjectPath];
|
|
172
|
-
if (projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) {
|
|
173
|
-
hasMcpServers = true;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
} catch (e) {
|
|
177
|
-
// Ignore if file doesn't exist or isn't parsable
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (hasMcpServers) {
|
|
181
|
-
args.push('--mcp-config', geminiConfigPath);
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
// Ignore outer errors
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Add model for all sessions (both new and resumed)
|
|
188
|
-
let modelToUse = options.model || 'gemini-2.5-flash';
|
|
189
|
-
args.push('--model', modelToUse);
|
|
190
|
-
args.push('--output-format', 'stream-json');
|
|
191
|
-
|
|
192
|
-
// Handle approval modes and allowed tools
|
|
193
|
-
if (
|
|
194
|
-
settings.skipPermissions ||
|
|
195
|
-
options.skipPermissions ||
|
|
196
|
-
permissionMode === 'yolo' ||
|
|
197
|
-
permissionMode === 'bypassPermissions' ||
|
|
198
|
-
permissionMode === 'acceptEdits'
|
|
199
|
-
) {
|
|
200
|
-
args.push('--yolo');
|
|
201
|
-
} else if (permissionMode === 'auto_edit') {
|
|
202
|
-
args.push('--approval-mode', 'auto_edit');
|
|
203
|
-
} else if (permissionMode === 'plan') {
|
|
204
|
-
args.push('--approval-mode', 'plan');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (settings.allowedTools && settings.allowedTools.length > 0) {
|
|
208
|
-
args.push('--allowed-tools', settings.allowedTools.join(','));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Try to find gemini in PATH first, then fall back to environment variable
|
|
212
|
-
const geminiPath = process.env.GEMINI_PATH || 'gemini';
|
|
213
|
-
console.log('Spawning Gemini CLI:', geminiPath, args.join(' '));
|
|
214
|
-
console.log('Working directory:', workingDir);
|
|
215
|
-
|
|
216
|
-
let spawnCmd = geminiPath;
|
|
217
|
-
let spawnArgs = args;
|
|
218
|
-
|
|
219
|
-
// On non-Windows platforms, wrap the execution in a shell to avoid ENOEXEC
|
|
220
|
-
// which happens when the target is a script lacking a shebang.
|
|
221
|
-
if (os.platform() !== 'win32') {
|
|
222
|
-
spawnCmd = 'sh';
|
|
223
|
-
// Use exec to replace the shell process, ensuring signals hit gemini directly
|
|
224
|
-
spawnArgs = ['-c', 'exec "$0" "$@"', geminiPath, ...args];
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Pixcode UI-saved API key (stored in ~/.pixcode/provider-credentials.json)
|
|
228
|
-
// overlays on top of process.env so Gemini picks it up without the user
|
|
229
|
-
// exporting GEMINI_API_KEY in their shell.
|
|
230
|
-
const spawnEnv = await buildSpawnEnv('gemini');
|
|
231
|
-
|
|
232
|
-
// OAuth-only users never opened the TUI → no settings.json → spawn dies
|
|
233
|
-
// with exit 41. Bootstrap it once.
|
|
234
|
-
await ensureGeminiSettingsJson();
|
|
235
|
-
|
|
236
|
-
// Headless OAuth handshake. Without this Gemini exits 41 with "Please
|
|
237
|
-
// set an Auth method..." even when ~/.gemini/oauth_creds.json is fully
|
|
238
|
-
// valid — the CLI gates oauth-personal mode behind GOOGLE_GENAI_USE_GCA
|
|
239
|
-
// when running non-interactively. Only set when the user hasn't supplied
|
|
240
|
-
// an API key (which has its own auth path).
|
|
241
|
-
if (!spawnEnv.GEMINI_API_KEY) {
|
|
242
|
-
spawnEnv.GOOGLE_GENAI_USE_GCA = 'true';
|
|
243
|
-
}
|
|
244
|
-
// `--yolo` skips approval prompts but Gemini still refuses to operate
|
|
245
|
-
// on directories it doesn't recognise as trusted. There's no
|
|
246
|
-
// interactive prompt available in our pty-less spawn, so we set the
|
|
247
|
-
// documented headless escape hatch.
|
|
248
|
-
spawnEnv.GEMINI_CLI_TRUST_WORKSPACE = 'true';
|
|
249
|
-
|
|
250
|
-
return new Promise((resolve, reject) => {
|
|
251
|
-
const geminiProcess = spawnFunction(spawnCmd, spawnArgs, {
|
|
252
|
-
cwd: workingDir,
|
|
253
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
254
|
-
env: spawnEnv,
|
|
255
|
-
});
|
|
256
|
-
let terminalNotificationSent = false;
|
|
257
|
-
let terminalFailureReason = null;
|
|
258
|
-
|
|
259
|
-
const notifyTerminalState = ({ code = null, error = null } = {}) => {
|
|
260
|
-
if (terminalNotificationSent) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
terminalNotificationSent = true;
|
|
265
|
-
|
|
266
|
-
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
267
|
-
if (code === 0 && !error) {
|
|
268
|
-
notifyRunStopped({
|
|
269
|
-
userId: ws?.userId || null,
|
|
270
|
-
provider: 'gemini',
|
|
271
|
-
sessionId: finalSessionId,
|
|
272
|
-
sessionName: sessionSummary,
|
|
273
|
-
stopReason: 'completed'
|
|
274
|
-
});
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
notifyRunFailed({
|
|
279
|
-
userId: ws?.userId || null,
|
|
280
|
-
provider: 'gemini',
|
|
281
|
-
sessionId: finalSessionId,
|
|
282
|
-
sessionName: sessionSummary,
|
|
283
|
-
error: error || terminalFailureReason || `Gemini CLI exited with code ${code}`
|
|
284
|
-
});
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// Attach temp file info to process for cleanup later
|
|
288
|
-
geminiProcess.tempImagePaths = tempImagePaths;
|
|
289
|
-
geminiProcess.tempDir = tempDir;
|
|
290
|
-
|
|
291
|
-
// Store process reference for potential abort
|
|
292
|
-
const processKey = capturedSessionId || sessionId || Date.now().toString();
|
|
293
|
-
activeGeminiProcesses.set(processKey, geminiProcess);
|
|
294
|
-
|
|
295
|
-
// Store sessionId on the process object for debugging
|
|
296
|
-
geminiProcess.sessionId = processKey;
|
|
297
|
-
|
|
298
|
-
// Close stdin to signal we're done sending input
|
|
299
|
-
geminiProcess.stdin.end();
|
|
300
|
-
|
|
301
|
-
// Add timeout handler
|
|
302
|
-
const timeoutMs = readCliIdleTimeoutMs();
|
|
303
|
-
let timeout;
|
|
304
|
-
|
|
305
|
-
const startTimeout = () => {
|
|
306
|
-
if (timeout) clearTimeout(timeout);
|
|
307
|
-
if (timeoutMs === 0) return;
|
|
308
|
-
timeout = setTimeout(() => {
|
|
309
|
-
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey);
|
|
310
|
-
terminalFailureReason = `Gemini CLI timeout - no response received for ${timeoutMs / 1000} seconds`;
|
|
311
|
-
ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
|
|
312
|
-
try {
|
|
313
|
-
geminiProcess.kill('SIGTERM');
|
|
314
|
-
} catch (e) { }
|
|
315
|
-
}, timeoutMs);
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
startTimeout();
|
|
319
|
-
|
|
320
|
-
// Save user message to session when starting
|
|
321
|
-
if (command && capturedSessionId) {
|
|
322
|
-
sessionManager.addMessage(capturedSessionId, 'user', command);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Create response handler for NDJSON buffering
|
|
326
|
-
let responseHandler;
|
|
327
|
-
if (ws) {
|
|
328
|
-
responseHandler = new GeminiResponseHandler(ws, {
|
|
329
|
-
onContentFragment: (content) => {
|
|
330
|
-
if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') {
|
|
331
|
-
assistantBlocks[assistantBlocks.length - 1].text += content;
|
|
332
|
-
} else {
|
|
333
|
-
assistantBlocks.push({ type: 'text', text: content });
|
|
334
|
-
}
|
|
335
|
-
},
|
|
336
|
-
onToolUse: (event) => {
|
|
337
|
-
assistantBlocks.push({
|
|
338
|
-
type: 'tool_use',
|
|
339
|
-
id: event.tool_id,
|
|
340
|
-
name: event.tool_name,
|
|
341
|
-
input: event.parameters
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
onToolResult: (event) => {
|
|
345
|
-
if (capturedSessionId) {
|
|
346
|
-
if (assistantBlocks.length > 0) {
|
|
347
|
-
sessionManager.addMessage(capturedSessionId, 'assistant', [...assistantBlocks]);
|
|
348
|
-
assistantBlocks = [];
|
|
349
|
-
}
|
|
350
|
-
sessionManager.addMessage(capturedSessionId, 'user', [{
|
|
351
|
-
type: 'tool_result',
|
|
352
|
-
tool_use_id: event.tool_id,
|
|
353
|
-
content: event.output === undefined ? null : event.output,
|
|
354
|
-
is_error: event.status === 'error'
|
|
355
|
-
}]);
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
onInit: (event) => {
|
|
359
|
-
if (capturedSessionId) {
|
|
360
|
-
const sess = sessionManager.getSession(capturedSessionId);
|
|
361
|
-
if (sess && !sess.cliSessionId) {
|
|
362
|
-
sess.cliSessionId = event.session_id;
|
|
363
|
-
sessionManager.saveSession(capturedSessionId);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Handle stdout
|
|
371
|
-
geminiProcess.stdout.on('data', (data) => {
|
|
372
|
-
const rawOutput = data.toString();
|
|
373
|
-
startTimeout(); // Re-arm the timeout
|
|
374
|
-
|
|
375
|
-
// For new sessions, create a session ID FIRST
|
|
376
|
-
if (!sessionId && !sessionCreatedSent && !capturedSessionId) {
|
|
377
|
-
capturedSessionId = `gemini_${Date.now()}`;
|
|
378
|
-
sessionCreatedSent = true;
|
|
379
|
-
|
|
380
|
-
// Create session in session manager
|
|
381
|
-
sessionManager.createSession(capturedSessionId, cwd || process.cwd());
|
|
382
|
-
|
|
383
|
-
// Save the user message now that we have a session ID
|
|
384
|
-
if (command) {
|
|
385
|
-
sessionManager.addMessage(capturedSessionId, 'user', command);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Update process key with captured session ID
|
|
389
|
-
if (processKey !== capturedSessionId) {
|
|
390
|
-
activeGeminiProcesses.delete(processKey);
|
|
391
|
-
activeGeminiProcesses.set(capturedSessionId, geminiProcess);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
ws.setSessionId && typeof ws.setSessionId === 'function' && ws.setSessionId(capturedSessionId);
|
|
395
|
-
|
|
396
|
-
ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' }));
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (responseHandler) {
|
|
400
|
-
responseHandler.processData(rawOutput);
|
|
401
|
-
} else if (rawOutput) {
|
|
402
|
-
// Fallback to direct sending for raw CLI mode without WS
|
|
403
|
-
if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') {
|
|
404
|
-
assistantBlocks[assistantBlocks.length - 1].text += rawOutput;
|
|
405
|
-
} else {
|
|
406
|
-
assistantBlocks.push({ type: 'text', text: rawOutput });
|
|
407
|
-
}
|
|
408
|
-
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
|
|
409
|
-
ws.send(createNormalizedMessage({ kind: 'stream_delta', content: rawOutput, sessionId: socketSessionId, provider: 'gemini' }));
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// Handle stderr
|
|
414
|
-
geminiProcess.stderr.on('data', (data) => {
|
|
415
|
-
const errorMsg = data.toString();
|
|
416
|
-
startTimeout();
|
|
417
|
-
|
|
418
|
-
// Filter out deprecation warnings and "Loaded cached credentials" message
|
|
419
|
-
if (errorMsg.includes('[DEP0040]') ||
|
|
420
|
-
errorMsg.includes('DeprecationWarning') ||
|
|
421
|
-
errorMsg.includes('--trace-deprecation') ||
|
|
422
|
-
errorMsg.includes('Loaded cached credentials')) {
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
|
|
427
|
-
ws.send(createNormalizedMessage({ kind: 'error', content: errorMsg, sessionId: socketSessionId, provider: 'gemini' }));
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// Handle process completion
|
|
431
|
-
geminiProcess.on('close', async (code) => {
|
|
432
|
-
clearTimeout(timeout);
|
|
433
|
-
|
|
434
|
-
// Flush any remaining buffered content
|
|
435
|
-
if (responseHandler) {
|
|
436
|
-
responseHandler.forceFlush();
|
|
437
|
-
responseHandler.destroy();
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Clean up process reference
|
|
441
|
-
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
442
|
-
activeGeminiProcesses.delete(finalSessionId);
|
|
443
|
-
|
|
444
|
-
// Save assistant response to session if we have one
|
|
445
|
-
if (finalSessionId && assistantBlocks.length > 0) {
|
|
446
|
-
sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'gemini' }));
|
|
450
|
-
|
|
451
|
-
// Clean up temporary image files if any
|
|
452
|
-
if (geminiProcess.tempImagePaths && geminiProcess.tempImagePaths.length > 0) {
|
|
453
|
-
for (const imagePath of geminiProcess.tempImagePaths) {
|
|
454
|
-
await fs.unlink(imagePath).catch(err => { });
|
|
455
|
-
}
|
|
456
|
-
if (geminiProcess.tempDir) {
|
|
457
|
-
await fs.rm(geminiProcess.tempDir, { recursive: true, force: true }).catch(err => { });
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (code === 0) {
|
|
462
|
-
notifyTerminalState({ code });
|
|
463
|
-
resolve();
|
|
464
|
-
} else {
|
|
465
|
-
// code 127 = shell "command not found" — check installation
|
|
466
|
-
if (code === 127) {
|
|
467
|
-
const installed = await providerAuthService.isProviderInstalled('gemini');
|
|
468
|
-
if (!installed) {
|
|
469
|
-
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
|
470
|
-
ws.send(createNormalizedMessage({ kind: 'error', content: 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli', sessionId: socketSessionId, provider: 'gemini' }));
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
notifyTerminalState({
|
|
475
|
-
code,
|
|
476
|
-
error: code === null ? 'Gemini CLI process was terminated or timed out' : null
|
|
477
|
-
});
|
|
478
|
-
reject(new Error(code === null ? 'Gemini CLI process was terminated or timed out' : `Gemini CLI exited with code ${code}`));
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// Handle process errors
|
|
483
|
-
geminiProcess.on('error', async (error) => {
|
|
484
|
-
// Clean up process reference on error
|
|
485
|
-
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
486
|
-
activeGeminiProcesses.delete(finalSessionId);
|
|
487
|
-
|
|
488
|
-
// Check if Gemini CLI is installed for a clearer error message
|
|
489
|
-
const installed = await providerAuthService.isProviderInstalled('gemini');
|
|
490
|
-
const errorContent = !installed
|
|
491
|
-
? 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli'
|
|
492
|
-
: error.message;
|
|
493
|
-
|
|
494
|
-
const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
|
495
|
-
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'gemini' }));
|
|
496
|
-
notifyTerminalState({ error });
|
|
497
|
-
|
|
498
|
-
reject(error);
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function abortGeminiSession(sessionId) {
|
|
505
|
-
let geminiProc = activeGeminiProcesses.get(sessionId);
|
|
506
|
-
let processKey = sessionId;
|
|
507
|
-
|
|
508
|
-
if (!geminiProc) {
|
|
509
|
-
for (const [key, proc] of activeGeminiProcesses.entries()) {
|
|
510
|
-
if (proc.sessionId === sessionId) {
|
|
511
|
-
geminiProc = proc;
|
|
512
|
-
processKey = key;
|
|
513
|
-
break;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (geminiProc) {
|
|
519
|
-
try {
|
|
520
|
-
geminiProc.kill('SIGTERM');
|
|
521
|
-
setTimeout(() => {
|
|
522
|
-
if (activeGeminiProcesses.has(processKey)) {
|
|
523
|
-
try {
|
|
524
|
-
geminiProc.kill('SIGKILL');
|
|
525
|
-
} catch (e) { }
|
|
526
|
-
}
|
|
527
|
-
}, 2000); // Wait 2 seconds before force kill
|
|
528
|
-
|
|
529
|
-
return true;
|
|
530
|
-
} catch (error) {
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function isGeminiSessionActive(sessionId) {
|
|
538
|
-
return activeGeminiProcesses.has(sessionId);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function getActiveGeminiSessions() {
|
|
542
|
-
return Array.from(activeGeminiProcesses.keys());
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
export {
|
|
546
|
-
spawnGemini,
|
|
547
|
-
abortGeminiSession,
|
|
548
|
-
isGeminiSessionActive,
|
|
549
|
-
getActiveGeminiSessions
|
|
550
|
-
};
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import crossSpawn from 'cross-spawn';
|
|
7
|
+
|
|
8
|
+
import sessionManager from './sessionManager.js';
|
|
9
|
+
import GeminiResponseHandler from './gemini-response-handler.js';
|
|
10
|
+
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
11
|
+
import { buildSpawnEnv } from './services/provider-credentials.js';
|
|
12
|
+
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
13
|
+
import { createNormalizedMessage } from './shared/utils.js';
|
|
14
|
+
|
|
15
|
+
// Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
|
|
16
|
+
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
17
|
+
let activeGeminiProcesses = new Map(); // Track active processes by session ID
|
|
18
|
+
const DEFAULT_CLI_IDLE_TIMEOUT_MS = 600000;
|
|
19
|
+
|
|
20
|
+
function readCliIdleTimeoutMs() {
|
|
21
|
+
const configured = Number.parseInt(process.env.PIXCODE_CLI_IDLE_TIMEOUT_MS || '', 10);
|
|
22
|
+
return Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_CLI_IDLE_TIMEOUT_MS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Auto-create `~/.gemini/settings.json` when the user has signed in via OAuth
|
|
27
|
+
* (so `oauth_creds.json` exists) but never opened the Gemini TUI to write the
|
|
28
|
+
* `selectedAuthType` field. Without this, `gemini --prompt … --output-format
|
|
29
|
+
* stream-json --yolo` exits 41 with "Please set an Auth method in your
|
|
30
|
+
* settings.json" — even though credentials are perfectly valid. We respect a
|
|
31
|
+
* pre-existing `settings.json` and never overwrite a chosen auth type.
|
|
32
|
+
*
|
|
33
|
+
* Triggered every spawn (cheap: one fs.access + maybe one tiny write).
|
|
34
|
+
*/
|
|
35
|
+
async function ensureGeminiSettingsJson() {
|
|
36
|
+
const home = os.homedir();
|
|
37
|
+
const dir = path.join(home, '.gemini');
|
|
38
|
+
const settingsPath = path.join(dir, 'settings.json');
|
|
39
|
+
const oauthPath = path.join(dir, 'oauth_creds.json');
|
|
40
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(settingsPath);
|
|
44
|
+
return; // user-managed, leave it.
|
|
45
|
+
} catch { /* missing — we may create it */ }
|
|
46
|
+
|
|
47
|
+
let selectedAuthType = null;
|
|
48
|
+
if (apiKey && apiKey.trim()) {
|
|
49
|
+
selectedAuthType = 'gemini-api-key';
|
|
50
|
+
} else {
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(oauthPath);
|
|
53
|
+
selectedAuthType = 'oauth-personal';
|
|
54
|
+
} catch { /* no oauth either */ }
|
|
55
|
+
}
|
|
56
|
+
if (!selectedAuthType) return;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await fs.mkdir(dir, { recursive: true });
|
|
60
|
+
await fs.writeFile(settingsPath, JSON.stringify({ selectedAuthType }, null, 2), { mode: 0o600 });
|
|
61
|
+
console.log(`[gemini] auto-bootstrapped ~/.gemini/settings.json with selectedAuthType="${selectedAuthType}"`);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.warn('[gemini] failed to bootstrap settings.json:', error?.message || error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function spawnGemini(command, options = {}, ws) {
|
|
68
|
+
const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
|
|
69
|
+
let capturedSessionId = sessionId; // Track session ID throughout the process
|
|
70
|
+
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
|
71
|
+
let assistantBlocks = []; // Accumulate the full response blocks including tools
|
|
72
|
+
|
|
73
|
+
// Use tools settings passed from frontend, or defaults
|
|
74
|
+
const settings = toolsSettings || {
|
|
75
|
+
allowedTools: [],
|
|
76
|
+
disallowedTools: [],
|
|
77
|
+
skipPermissions: false
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Build Gemini CLI command - start with print/resume flags first
|
|
81
|
+
const args = [];
|
|
82
|
+
|
|
83
|
+
// Add prompt flag with command if we have a command
|
|
84
|
+
if (command && command.trim()) {
|
|
85
|
+
args.push('--prompt', command);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If we have a sessionId, we want to resume
|
|
89
|
+
if (sessionId) {
|
|
90
|
+
const session = sessionManager.getSession(sessionId);
|
|
91
|
+
if (session && session.cliSessionId) {
|
|
92
|
+
args.push('--resume', session.cliSessionId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Use cwd (actual project directory) instead of projectPath (Gemini's metadata directory)
|
|
97
|
+
// Clean the path by removing any non-printable characters
|
|
98
|
+
const cleanPath = (cwd || projectPath || process.cwd()).replace(/[^\x20-\x7E]/g, '').trim();
|
|
99
|
+
const workingDir = cleanPath;
|
|
100
|
+
|
|
101
|
+
// Handle images by saving them to temporary files and passing paths to Gemini
|
|
102
|
+
const tempImagePaths = [];
|
|
103
|
+
let tempDir = null;
|
|
104
|
+
if (images && images.length > 0) {
|
|
105
|
+
try {
|
|
106
|
+
// Create temp directory in the project directory so Gemini can access it
|
|
107
|
+
tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
|
|
108
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
// Save each image to a temp file
|
|
111
|
+
for (const [index, image] of images.entries()) {
|
|
112
|
+
// Extract base64 data and mime type
|
|
113
|
+
const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
|
|
114
|
+
if (!matches) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const [, mimeType, base64Data] = matches;
|
|
119
|
+
const extension = mimeType.split('/')[1] || 'png';
|
|
120
|
+
const filename = `image_${index}.${extension}`;
|
|
121
|
+
const filepath = path.join(tempDir, filename);
|
|
122
|
+
|
|
123
|
+
// Write base64 data to file
|
|
124
|
+
await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
|
|
125
|
+
tempImagePaths.push(filepath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Include the full image paths in the prompt for Gemini to reference
|
|
129
|
+
// Gemini CLI can read images from file paths in the prompt
|
|
130
|
+
if (tempImagePaths.length > 0 && command && command.trim()) {
|
|
131
|
+
const imageNote = `\n\n[Images given: ${tempImagePaths.length} images are located at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
|
|
132
|
+
const modifiedCommand = command + imageNote;
|
|
133
|
+
|
|
134
|
+
// Update the command in args
|
|
135
|
+
const promptIndex = args.indexOf('--prompt');
|
|
136
|
+
if (promptIndex !== -1 && args[promptIndex + 1] === command) {
|
|
137
|
+
args[promptIndex + 1] = modifiedCommand;
|
|
138
|
+
} else if (promptIndex !== -1) {
|
|
139
|
+
// If we're using context, update the full prompt
|
|
140
|
+
args[promptIndex + 1] = args[promptIndex + 1] + imageNote;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Error processing images for Gemini:', error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add basic flags for Gemini
|
|
149
|
+
if (options.debug) {
|
|
150
|
+
args.push('--debug');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add MCP config flag only if MCP servers are configured
|
|
154
|
+
try {
|
|
155
|
+
const geminiConfigPath = path.join(os.homedir(), '.gemini.json');
|
|
156
|
+
let hasMcpServers = false;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await fs.access(geminiConfigPath);
|
|
160
|
+
const geminiConfigRaw = await fs.readFile(geminiConfigPath, 'utf8');
|
|
161
|
+
const geminiConfig = JSON.parse(geminiConfigRaw);
|
|
162
|
+
|
|
163
|
+
// Check global MCP servers
|
|
164
|
+
if (geminiConfig.mcpServers && Object.keys(geminiConfig.mcpServers).length > 0) {
|
|
165
|
+
hasMcpServers = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check project-specific MCP servers
|
|
169
|
+
if (!hasMcpServers && geminiConfig.geminiProjects) {
|
|
170
|
+
const currentProjectPath = process.cwd();
|
|
171
|
+
const projectConfig = geminiConfig.geminiProjects[currentProjectPath];
|
|
172
|
+
if (projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) {
|
|
173
|
+
hasMcpServers = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// Ignore if file doesn't exist or isn't parsable
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (hasMcpServers) {
|
|
181
|
+
args.push('--mcp-config', geminiConfigPath);
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// Ignore outer errors
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Add model for all sessions (both new and resumed)
|
|
188
|
+
let modelToUse = options.model || 'gemini-2.5-flash';
|
|
189
|
+
args.push('--model', modelToUse);
|
|
190
|
+
args.push('--output-format', 'stream-json');
|
|
191
|
+
|
|
192
|
+
// Handle approval modes and allowed tools
|
|
193
|
+
if (
|
|
194
|
+
settings.skipPermissions ||
|
|
195
|
+
options.skipPermissions ||
|
|
196
|
+
permissionMode === 'yolo' ||
|
|
197
|
+
permissionMode === 'bypassPermissions' ||
|
|
198
|
+
permissionMode === 'acceptEdits'
|
|
199
|
+
) {
|
|
200
|
+
args.push('--yolo');
|
|
201
|
+
} else if (permissionMode === 'auto_edit') {
|
|
202
|
+
args.push('--approval-mode', 'auto_edit');
|
|
203
|
+
} else if (permissionMode === 'plan') {
|
|
204
|
+
args.push('--approval-mode', 'plan');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (settings.allowedTools && settings.allowedTools.length > 0) {
|
|
208
|
+
args.push('--allowed-tools', settings.allowedTools.join(','));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Try to find gemini in PATH first, then fall back to environment variable
|
|
212
|
+
const geminiPath = process.env.GEMINI_PATH || 'gemini';
|
|
213
|
+
console.log('Spawning Gemini CLI:', geminiPath, args.join(' '));
|
|
214
|
+
console.log('Working directory:', workingDir);
|
|
215
|
+
|
|
216
|
+
let spawnCmd = geminiPath;
|
|
217
|
+
let spawnArgs = args;
|
|
218
|
+
|
|
219
|
+
// On non-Windows platforms, wrap the execution in a shell to avoid ENOEXEC
|
|
220
|
+
// which happens when the target is a script lacking a shebang.
|
|
221
|
+
if (os.platform() !== 'win32') {
|
|
222
|
+
spawnCmd = 'sh';
|
|
223
|
+
// Use exec to replace the shell process, ensuring signals hit gemini directly
|
|
224
|
+
spawnArgs = ['-c', 'exec "$0" "$@"', geminiPath, ...args];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Pixcode UI-saved API key (stored in ~/.pixcode/provider-credentials.json)
|
|
228
|
+
// overlays on top of process.env so Gemini picks it up without the user
|
|
229
|
+
// exporting GEMINI_API_KEY in their shell.
|
|
230
|
+
const spawnEnv = await buildSpawnEnv('gemini');
|
|
231
|
+
|
|
232
|
+
// OAuth-only users never opened the TUI → no settings.json → spawn dies
|
|
233
|
+
// with exit 41. Bootstrap it once.
|
|
234
|
+
await ensureGeminiSettingsJson();
|
|
235
|
+
|
|
236
|
+
// Headless OAuth handshake. Without this Gemini exits 41 with "Please
|
|
237
|
+
// set an Auth method..." even when ~/.gemini/oauth_creds.json is fully
|
|
238
|
+
// valid — the CLI gates oauth-personal mode behind GOOGLE_GENAI_USE_GCA
|
|
239
|
+
// when running non-interactively. Only set when the user hasn't supplied
|
|
240
|
+
// an API key (which has its own auth path).
|
|
241
|
+
if (!spawnEnv.GEMINI_API_KEY) {
|
|
242
|
+
spawnEnv.GOOGLE_GENAI_USE_GCA = 'true';
|
|
243
|
+
}
|
|
244
|
+
// `--yolo` skips approval prompts but Gemini still refuses to operate
|
|
245
|
+
// on directories it doesn't recognise as trusted. There's no
|
|
246
|
+
// interactive prompt available in our pty-less spawn, so we set the
|
|
247
|
+
// documented headless escape hatch.
|
|
248
|
+
spawnEnv.GEMINI_CLI_TRUST_WORKSPACE = 'true';
|
|
249
|
+
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
const geminiProcess = spawnFunction(spawnCmd, spawnArgs, {
|
|
252
|
+
cwd: workingDir,
|
|
253
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
254
|
+
env: spawnEnv,
|
|
255
|
+
});
|
|
256
|
+
let terminalNotificationSent = false;
|
|
257
|
+
let terminalFailureReason = null;
|
|
258
|
+
|
|
259
|
+
const notifyTerminalState = ({ code = null, error = null } = {}) => {
|
|
260
|
+
if (terminalNotificationSent) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
terminalNotificationSent = true;
|
|
265
|
+
|
|
266
|
+
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
267
|
+
if (code === 0 && !error) {
|
|
268
|
+
notifyRunStopped({
|
|
269
|
+
userId: ws?.userId || null,
|
|
270
|
+
provider: 'gemini',
|
|
271
|
+
sessionId: finalSessionId,
|
|
272
|
+
sessionName: sessionSummary,
|
|
273
|
+
stopReason: 'completed'
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
notifyRunFailed({
|
|
279
|
+
userId: ws?.userId || null,
|
|
280
|
+
provider: 'gemini',
|
|
281
|
+
sessionId: finalSessionId,
|
|
282
|
+
sessionName: sessionSummary,
|
|
283
|
+
error: error || terminalFailureReason || `Gemini CLI exited with code ${code}`
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Attach temp file info to process for cleanup later
|
|
288
|
+
geminiProcess.tempImagePaths = tempImagePaths;
|
|
289
|
+
geminiProcess.tempDir = tempDir;
|
|
290
|
+
|
|
291
|
+
// Store process reference for potential abort
|
|
292
|
+
const processKey = capturedSessionId || sessionId || Date.now().toString();
|
|
293
|
+
activeGeminiProcesses.set(processKey, geminiProcess);
|
|
294
|
+
|
|
295
|
+
// Store sessionId on the process object for debugging
|
|
296
|
+
geminiProcess.sessionId = processKey;
|
|
297
|
+
|
|
298
|
+
// Close stdin to signal we're done sending input
|
|
299
|
+
geminiProcess.stdin.end();
|
|
300
|
+
|
|
301
|
+
// Add timeout handler
|
|
302
|
+
const timeoutMs = readCliIdleTimeoutMs();
|
|
303
|
+
let timeout;
|
|
304
|
+
|
|
305
|
+
const startTimeout = () => {
|
|
306
|
+
if (timeout) clearTimeout(timeout);
|
|
307
|
+
if (timeoutMs === 0) return;
|
|
308
|
+
timeout = setTimeout(() => {
|
|
309
|
+
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey);
|
|
310
|
+
terminalFailureReason = `Gemini CLI timeout - no response received for ${timeoutMs / 1000} seconds`;
|
|
311
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
|
|
312
|
+
try {
|
|
313
|
+
geminiProcess.kill('SIGTERM');
|
|
314
|
+
} catch (e) { }
|
|
315
|
+
}, timeoutMs);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
startTimeout();
|
|
319
|
+
|
|
320
|
+
// Save user message to session when starting
|
|
321
|
+
if (command && capturedSessionId) {
|
|
322
|
+
sessionManager.addMessage(capturedSessionId, 'user', command);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Create response handler for NDJSON buffering
|
|
326
|
+
let responseHandler;
|
|
327
|
+
if (ws) {
|
|
328
|
+
responseHandler = new GeminiResponseHandler(ws, {
|
|
329
|
+
onContentFragment: (content) => {
|
|
330
|
+
if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') {
|
|
331
|
+
assistantBlocks[assistantBlocks.length - 1].text += content;
|
|
332
|
+
} else {
|
|
333
|
+
assistantBlocks.push({ type: 'text', text: content });
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
onToolUse: (event) => {
|
|
337
|
+
assistantBlocks.push({
|
|
338
|
+
type: 'tool_use',
|
|
339
|
+
id: event.tool_id,
|
|
340
|
+
name: event.tool_name,
|
|
341
|
+
input: event.parameters
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
onToolResult: (event) => {
|
|
345
|
+
if (capturedSessionId) {
|
|
346
|
+
if (assistantBlocks.length > 0) {
|
|
347
|
+
sessionManager.addMessage(capturedSessionId, 'assistant', [...assistantBlocks]);
|
|
348
|
+
assistantBlocks = [];
|
|
349
|
+
}
|
|
350
|
+
sessionManager.addMessage(capturedSessionId, 'user', [{
|
|
351
|
+
type: 'tool_result',
|
|
352
|
+
tool_use_id: event.tool_id,
|
|
353
|
+
content: event.output === undefined ? null : event.output,
|
|
354
|
+
is_error: event.status === 'error'
|
|
355
|
+
}]);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
onInit: (event) => {
|
|
359
|
+
if (capturedSessionId) {
|
|
360
|
+
const sess = sessionManager.getSession(capturedSessionId);
|
|
361
|
+
if (sess && !sess.cliSessionId) {
|
|
362
|
+
sess.cliSessionId = event.session_id;
|
|
363
|
+
sessionManager.saveSession(capturedSessionId);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Handle stdout
|
|
371
|
+
geminiProcess.stdout.on('data', (data) => {
|
|
372
|
+
const rawOutput = data.toString();
|
|
373
|
+
startTimeout(); // Re-arm the timeout
|
|
374
|
+
|
|
375
|
+
// For new sessions, create a session ID FIRST
|
|
376
|
+
if (!sessionId && !sessionCreatedSent && !capturedSessionId) {
|
|
377
|
+
capturedSessionId = `gemini_${Date.now()}`;
|
|
378
|
+
sessionCreatedSent = true;
|
|
379
|
+
|
|
380
|
+
// Create session in session manager
|
|
381
|
+
sessionManager.createSession(capturedSessionId, cwd || process.cwd());
|
|
382
|
+
|
|
383
|
+
// Save the user message now that we have a session ID
|
|
384
|
+
if (command) {
|
|
385
|
+
sessionManager.addMessage(capturedSessionId, 'user', command);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Update process key with captured session ID
|
|
389
|
+
if (processKey !== capturedSessionId) {
|
|
390
|
+
activeGeminiProcesses.delete(processKey);
|
|
391
|
+
activeGeminiProcesses.set(capturedSessionId, geminiProcess);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
ws.setSessionId && typeof ws.setSessionId === 'function' && ws.setSessionId(capturedSessionId);
|
|
395
|
+
|
|
396
|
+
ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' }));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (responseHandler) {
|
|
400
|
+
responseHandler.processData(rawOutput);
|
|
401
|
+
} else if (rawOutput) {
|
|
402
|
+
// Fallback to direct sending for raw CLI mode without WS
|
|
403
|
+
if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') {
|
|
404
|
+
assistantBlocks[assistantBlocks.length - 1].text += rawOutput;
|
|
405
|
+
} else {
|
|
406
|
+
assistantBlocks.push({ type: 'text', text: rawOutput });
|
|
407
|
+
}
|
|
408
|
+
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
|
|
409
|
+
ws.send(createNormalizedMessage({ kind: 'stream_delta', content: rawOutput, sessionId: socketSessionId, provider: 'gemini' }));
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Handle stderr
|
|
414
|
+
geminiProcess.stderr.on('data', (data) => {
|
|
415
|
+
const errorMsg = data.toString();
|
|
416
|
+
startTimeout();
|
|
417
|
+
|
|
418
|
+
// Filter out deprecation warnings and "Loaded cached credentials" message
|
|
419
|
+
if (errorMsg.includes('[DEP0040]') ||
|
|
420
|
+
errorMsg.includes('DeprecationWarning') ||
|
|
421
|
+
errorMsg.includes('--trace-deprecation') ||
|
|
422
|
+
errorMsg.includes('Loaded cached credentials')) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
|
|
427
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: errorMsg, sessionId: socketSessionId, provider: 'gemini' }));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Handle process completion
|
|
431
|
+
geminiProcess.on('close', async (code) => {
|
|
432
|
+
clearTimeout(timeout);
|
|
433
|
+
|
|
434
|
+
// Flush any remaining buffered content
|
|
435
|
+
if (responseHandler) {
|
|
436
|
+
responseHandler.forceFlush();
|
|
437
|
+
responseHandler.destroy();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Clean up process reference
|
|
441
|
+
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
442
|
+
activeGeminiProcesses.delete(finalSessionId);
|
|
443
|
+
|
|
444
|
+
// Save assistant response to session if we have one
|
|
445
|
+
if (finalSessionId && assistantBlocks.length > 0) {
|
|
446
|
+
sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'gemini' }));
|
|
450
|
+
|
|
451
|
+
// Clean up temporary image files if any
|
|
452
|
+
if (geminiProcess.tempImagePaths && geminiProcess.tempImagePaths.length > 0) {
|
|
453
|
+
for (const imagePath of geminiProcess.tempImagePaths) {
|
|
454
|
+
await fs.unlink(imagePath).catch(err => { });
|
|
455
|
+
}
|
|
456
|
+
if (geminiProcess.tempDir) {
|
|
457
|
+
await fs.rm(geminiProcess.tempDir, { recursive: true, force: true }).catch(err => { });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (code === 0) {
|
|
462
|
+
notifyTerminalState({ code });
|
|
463
|
+
resolve();
|
|
464
|
+
} else {
|
|
465
|
+
// code 127 = shell "command not found" — check installation
|
|
466
|
+
if (code === 127) {
|
|
467
|
+
const installed = await providerAuthService.isProviderInstalled('gemini');
|
|
468
|
+
if (!installed) {
|
|
469
|
+
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
|
470
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli', sessionId: socketSessionId, provider: 'gemini' }));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
notifyTerminalState({
|
|
475
|
+
code,
|
|
476
|
+
error: code === null ? 'Gemini CLI process was terminated or timed out' : null
|
|
477
|
+
});
|
|
478
|
+
reject(new Error(code === null ? 'Gemini CLI process was terminated or timed out' : `Gemini CLI exited with code ${code}`));
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Handle process errors
|
|
483
|
+
geminiProcess.on('error', async (error) => {
|
|
484
|
+
// Clean up process reference on error
|
|
485
|
+
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
486
|
+
activeGeminiProcesses.delete(finalSessionId);
|
|
487
|
+
|
|
488
|
+
// Check if Gemini CLI is installed for a clearer error message
|
|
489
|
+
const installed = await providerAuthService.isProviderInstalled('gemini');
|
|
490
|
+
const errorContent = !installed
|
|
491
|
+
? 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli'
|
|
492
|
+
: error.message;
|
|
493
|
+
|
|
494
|
+
const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
|
495
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'gemini' }));
|
|
496
|
+
notifyTerminalState({ error });
|
|
497
|
+
|
|
498
|
+
reject(error);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function abortGeminiSession(sessionId) {
|
|
505
|
+
let geminiProc = activeGeminiProcesses.get(sessionId);
|
|
506
|
+
let processKey = sessionId;
|
|
507
|
+
|
|
508
|
+
if (!geminiProc) {
|
|
509
|
+
for (const [key, proc] of activeGeminiProcesses.entries()) {
|
|
510
|
+
if (proc.sessionId === sessionId) {
|
|
511
|
+
geminiProc = proc;
|
|
512
|
+
processKey = key;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (geminiProc) {
|
|
519
|
+
try {
|
|
520
|
+
geminiProc.kill('SIGTERM');
|
|
521
|
+
setTimeout(() => {
|
|
522
|
+
if (activeGeminiProcesses.has(processKey)) {
|
|
523
|
+
try {
|
|
524
|
+
geminiProc.kill('SIGKILL');
|
|
525
|
+
} catch (e) { }
|
|
526
|
+
}
|
|
527
|
+
}, 2000); // Wait 2 seconds before force kill
|
|
528
|
+
|
|
529
|
+
return true;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function isGeminiSessionActive(sessionId) {
|
|
538
|
+
return activeGeminiProcesses.has(sessionId);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getActiveGeminiSessions() {
|
|
542
|
+
return Array.from(activeGeminiProcesses.keys());
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export {
|
|
546
|
+
spawnGemini,
|
|
547
|
+
abortGeminiSession,
|
|
548
|
+
isGeminiSessionActive,
|
|
549
|
+
getActiveGeminiSessions
|
|
550
|
+
};
|