@cerulin/chell 0.2.5

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.
Files changed (53) hide show
  1. package/README.md +75 -0
  2. package/bin/chell-mcp.mjs +33 -0
  3. package/bin/chell.mjs +37 -0
  4. package/dist/codex/chellMcpStdioBridge.cjs +80 -0
  5. package/dist/codex/chellMcpStdioBridge.d.cts +2 -0
  6. package/dist/codex/chellMcpStdioBridge.d.mts +2 -0
  7. package/dist/codex/chellMcpStdioBridge.mjs +78 -0
  8. package/dist/index-B443j7JQ.mjs +6714 -0
  9. package/dist/index-qS668VWY.cjs +6730 -0
  10. package/dist/index.cjs +42 -0
  11. package/dist/index.d.cts +1 -0
  12. package/dist/index.d.mts +1 -0
  13. package/dist/index.mjs +39 -0
  14. package/dist/lib.cjs +32 -0
  15. package/dist/lib.d.cts +891 -0
  16. package/dist/lib.d.mts +891 -0
  17. package/dist/lib.mjs +22 -0
  18. package/dist/runCodex-DHtm7TWT.cjs +2020 -0
  19. package/dist/runCodex-DLbjgnc4.mjs +2017 -0
  20. package/dist/runGemini-C03RUmvr.mjs +788 -0
  21. package/dist/runGemini-fdb5jxAA.cjs +791 -0
  22. package/dist/types-DBjv5m4J.cjs +2499 -0
  23. package/dist/types-fM_iFuNp.mjs +2452 -0
  24. package/package.json +131 -0
  25. package/scripts/claude_local_launcher.cjs +98 -0
  26. package/scripts/claude_remote_launcher.cjs +13 -0
  27. package/scripts/codex_local_launcher.cjs +155 -0
  28. package/scripts/codex_preload.cjs +56 -0
  29. package/scripts/codex_remote_launcher.cjs +129 -0
  30. package/scripts/obfuscate-dist.mjs +73 -0
  31. package/scripts/pack-chell.cjs +32 -0
  32. package/scripts/publish-scoped.ps1 +58 -0
  33. package/scripts/ripgrep_launcher.cjs +33 -0
  34. package/scripts/unpack-tools.cjs +163 -0
  35. package/tools/archives/difftastic-LICENSE +21 -0
  36. package/tools/archives/difftastic-arm64-darwin.tar.gz +0 -0
  37. package/tools/archives/difftastic-arm64-linux.tar.gz +0 -0
  38. package/tools/archives/difftastic-x64-darwin.tar.gz +0 -0
  39. package/tools/archives/difftastic-x64-linux.tar.gz +0 -0
  40. package/tools/archives/difftastic-x64-win32.tar.gz +0 -0
  41. package/tools/archives/ripgrep-LICENSE +3 -0
  42. package/tools/archives/ripgrep-arm64-darwin.tar.gz +0 -0
  43. package/tools/archives/ripgrep-arm64-linux.tar.gz +0 -0
  44. package/tools/archives/ripgrep-x64-darwin.tar.gz +0 -0
  45. package/tools/archives/ripgrep-x64-linux.tar.gz +0 -0
  46. package/tools/archives/ripgrep-x64-win32.tar.gz +0 -0
  47. package/tools/licenses/difftastic-LICENSE +21 -0
  48. package/tools/licenses/ripgrep-LICENSE +3 -0
  49. package/tools/unpacked/difft +0 -0
  50. package/tools/unpacked/difft.exe +0 -0
  51. package/tools/unpacked/rg +0 -0
  52. package/tools/unpacked/rg.exe +0 -0
  53. package/tools/unpacked/ripgrep.node +0 -0
@@ -0,0 +1,791 @@
1
+ 'use strict';
2
+
3
+ var ink = require('ink');
4
+ var React = require('react');
5
+ var types = require('./types-DBjv5m4J.cjs');
6
+ var child_process = require('child_process');
7
+ var fs = require('fs');
8
+ var path = require('path');
9
+ var os = require('os');
10
+ var crypto = require('crypto');
11
+ var node_crypto = require('node:crypto');
12
+ var index = require('./index-qS668VWY.cjs');
13
+ var os$1 = require('node:os');
14
+ var node_path = require('node:path');
15
+ require('axios');
16
+ require('chalk');
17
+ require('node:fs');
18
+ require('node:fs/promises');
19
+ require('zod');
20
+ require('tweetnacl');
21
+ require('node:events');
22
+ require('socket.io-client');
23
+ require('util');
24
+ require('fs/promises');
25
+ require('url');
26
+ require('node-pty');
27
+ require('expo-server-sdk');
28
+ require('node:child_process');
29
+ require('node:readline');
30
+ require('node:url');
31
+ require('ps-list');
32
+ require('cross-spawn');
33
+ require('qrcode-terminal');
34
+ require('open');
35
+ require('fastify');
36
+ require('fastify-type-provider-zod');
37
+ require('@modelcontextprotocol/sdk/server/mcp.js');
38
+ require('node:http');
39
+ require('@modelcontextprotocol/sdk/server/streamableHttp.js');
40
+ require('http');
41
+ require('readline');
42
+
43
+ class GeminiClient {
44
+ originalSettings = null;
45
+ settingsPath;
46
+ handler = null;
47
+ isRunning = false;
48
+ sessionId = null;
49
+ config = null;
50
+ checkpointDir = null;
51
+ constructor() {
52
+ const geminiHome = process.env.GEMINI_HOME || path.join(os.homedir(), ".gemini");
53
+ this.settingsPath = path.join(geminiHome, "settings.json");
54
+ }
55
+ /**
56
+ * Set event handler for Gemini events
57
+ */
58
+ setHandler(handler) {
59
+ this.handler = handler;
60
+ }
61
+ /**
62
+ * Inject chell MCP server into Gemini settings
63
+ * Backs up existing settings and merges our MCP server config
64
+ */
65
+ injectMcpServerConfig(chellServerUrl, bridgeCommand) {
66
+ if (fs.existsSync(this.settingsPath)) {
67
+ this.originalSettings = fs.readFileSync(this.settingsPath, "utf-8");
68
+ types.logger.debug("[GeminiClient] Backed up existing settings");
69
+ }
70
+ let existingSettings = {};
71
+ if (this.originalSettings) {
72
+ try {
73
+ existingSettings = JSON.parse(this.originalSettings);
74
+ } catch (error) {
75
+ types.logger.debug("[GeminiClient] Failed to parse existing settings, using empty config");
76
+ }
77
+ }
78
+ const updatedSettings = {
79
+ ...existingSettings,
80
+ mcpServers: {
81
+ ...existingSettings.mcpServers,
82
+ chell: {
83
+ command: "node",
84
+ args: [bridgeCommand, "--url", chellServerUrl],
85
+ trust: true
86
+ }
87
+ }
88
+ };
89
+ fs.mkdirSync(path.join(this.settingsPath, ".."), { recursive: true });
90
+ fs.writeFileSync(this.settingsPath, JSON.stringify(updatedSettings, null, 2));
91
+ types.logger.debug("[GeminiClient] Injected MCP server config into:", this.settingsPath);
92
+ }
93
+ /**
94
+ * Start Gemini session (stores config, injects MCP, creates checkpoint dir)
95
+ */
96
+ async startSession(config, chellServerUrl, bridgeCommand, options) {
97
+ if (this.isRunning) {
98
+ throw new Error("Session already running");
99
+ }
100
+ this.sessionId = crypto.randomUUID();
101
+ this.config = config;
102
+ this.isRunning = true;
103
+ const geminiHome = process.env.GEMINI_HOME || path.join(os.homedir(), ".gemini");
104
+ this.checkpointDir = path.join(geminiHome, "chell-checkpoints", this.sessionId);
105
+ fs.mkdirSync(this.checkpointDir, { recursive: true });
106
+ this.injectMcpServerConfig(chellServerUrl, bridgeCommand);
107
+ types.logger.debug("[GeminiClient] Session initialized with checkpoint dir:", this.checkpointDir);
108
+ }
109
+ /**
110
+ * Send user message to Gemini (spawns one-shot process)
111
+ */
112
+ async sendMessage(message) {
113
+ if (!this.isRunning || !this.config) {
114
+ throw new Error("No active session");
115
+ }
116
+ types.logger.debug("[GeminiClient] Running one-shot Gemini:", message);
117
+ const args = [
118
+ "--output-format",
119
+ "json",
120
+ "--checkpointing",
121
+ // Enable checkpointing for conversation history
122
+ "--prompt",
123
+ message
124
+ ];
125
+ if (this.config.model) {
126
+ args.push("-m", this.config.model);
127
+ }
128
+ if (this.config.yolo) {
129
+ args.push("--yolo");
130
+ }
131
+ if (this.config.debug) {
132
+ args.push("--debug");
133
+ }
134
+ if (this.config.includeDirectories && this.config.includeDirectories.length > 0) {
135
+ args.push("--include-directories", this.config.includeDirectories.join(","));
136
+ }
137
+ return new Promise((resolve, reject) => {
138
+ const proc = child_process.spawn("gemini", args, {
139
+ cwd: this.checkpointDir || process.cwd(),
140
+ // Run in checkpoint dir to use same checkpoint files
141
+ env: {
142
+ ...process.env,
143
+ DISABLE_PROGRESS_BAR: "1",
144
+ NO_COLOR: "1",
145
+ FORCE_COLOR: "0"
146
+ }
147
+ });
148
+ let stdout = "";
149
+ let stderr = "";
150
+ proc.stdout?.on("data", (data) => {
151
+ stdout += data.toString();
152
+ });
153
+ proc.stderr?.on("data", (data) => {
154
+ stderr += data.toString();
155
+ types.logger.debug("[GeminiClient] stderr:", data.toString());
156
+ });
157
+ proc.on("exit", (code) => {
158
+ if (code !== 0) {
159
+ types.logger.debug("[GeminiClient] Process exited with code:", code);
160
+ this.handler?.({
161
+ type: "error",
162
+ message: `Gemini failed: ${stderr || "Unknown error"}`
163
+ });
164
+ reject(new Error(`Gemini exited with code ${code}`));
165
+ return;
166
+ }
167
+ try {
168
+ const response = JSON.parse(stdout);
169
+ types.logger.debug("[GeminiClient] Response:", response);
170
+ const text = response.response || response.text || JSON.stringify(response);
171
+ this.handler?.({
172
+ type: "message",
173
+ role: "assistant",
174
+ content: text
175
+ });
176
+ resolve();
177
+ } catch (error) {
178
+ types.logger.debug("[GeminiClient] Failed to parse JSON:", stdout);
179
+ if (stdout.trim()) {
180
+ this.handler?.({
181
+ type: "message",
182
+ role: "assistant",
183
+ content: stdout.trim()
184
+ });
185
+ }
186
+ resolve();
187
+ }
188
+ });
189
+ proc.on("error", (error) => {
190
+ types.logger.debug("[GeminiClient] Process error:", error);
191
+ reject(error);
192
+ });
193
+ });
194
+ }
195
+ /**
196
+ * Abort current task (no-op for one-shot mode)
197
+ */
198
+ abort() {
199
+ types.logger.debug("[GeminiClient] Abort called (no-op in one-shot mode)");
200
+ }
201
+ /**
202
+ * Check if session is active
203
+ */
204
+ hasActiveSession() {
205
+ return this.isRunning;
206
+ }
207
+ /**
208
+ * Get current session ID
209
+ */
210
+ getSessionId() {
211
+ return this.sessionId;
212
+ }
213
+ /**
214
+ * Kill session and cleanup
215
+ */
216
+ async disconnect() {
217
+ types.logger.debug("[GeminiClient] Disconnecting");
218
+ if (this.originalSettings !== null) {
219
+ try {
220
+ fs.writeFileSync(this.settingsPath, this.originalSettings);
221
+ types.logger.debug("[GeminiClient] Restored original settings");
222
+ } catch (error) {
223
+ types.logger.debug("[GeminiClient] Failed to restore settings:", error);
224
+ }
225
+ this.originalSettings = null;
226
+ } else {
227
+ try {
228
+ if (fs.existsSync(this.settingsPath)) {
229
+ fs.unlinkSync(this.settingsPath);
230
+ types.logger.debug("[GeminiClient] Removed injected settings");
231
+ }
232
+ } catch (error) {
233
+ types.logger.debug("[GeminiClient] Failed to remove settings:", error);
234
+ }
235
+ }
236
+ if (this.checkpointDir) {
237
+ try {
238
+ types.logger.debug("[GeminiClient] Keeping checkpoint dir for potential resume:", this.checkpointDir);
239
+ } catch (error) {
240
+ types.logger.debug("[GeminiClient] Error with checkpoint cleanup:", error);
241
+ }
242
+ this.checkpointDir = null;
243
+ }
244
+ this.isRunning = false;
245
+ this.sessionId = null;
246
+ this.config = null;
247
+ types.logger.debug("[GeminiClient] Disconnected");
248
+ }
249
+ }
250
+
251
+ class GeminiPermissionHandler {
252
+ session;
253
+ pendingRequests = /* @__PURE__ */ new Map();
254
+ constructor(session) {
255
+ this.session = session;
256
+ this.setupRpcHandler();
257
+ }
258
+ /**
259
+ * Handle tool permission request
260
+ */
261
+ async handleToolCall(callId, toolName, input) {
262
+ return new Promise((resolve, reject) => {
263
+ this.pendingRequests.set(callId, {
264
+ resolve,
265
+ reject,
266
+ toolName,
267
+ input
268
+ });
269
+ this.session.updateAgentState((currentState) => ({
270
+ ...currentState,
271
+ requests: {
272
+ ...currentState.requests,
273
+ [callId]: {
274
+ tool: toolName,
275
+ arguments: input,
276
+ createdAt: Date.now()
277
+ }
278
+ }
279
+ }));
280
+ types.logger.debug(`[Gemini] Permission request sent for tool: ${toolName} (${callId})`);
281
+ });
282
+ }
283
+ /**
284
+ * Setup RPC handler for permission responses
285
+ */
286
+ setupRpcHandler() {
287
+ this.session.rpcHandlerManager.registerHandler(
288
+ "permission",
289
+ async (response) => {
290
+ const pending = this.pendingRequests.get(response.id);
291
+ if (!pending) {
292
+ types.logger.debug("[Gemini] Permission request not found or already resolved");
293
+ return;
294
+ }
295
+ this.pendingRequests.delete(response.id);
296
+ const result = response.approved ? { decision: "approved" } : { decision: "denied" };
297
+ pending.resolve(result);
298
+ this.session.updateAgentState((currentState) => {
299
+ const request = currentState.requests?.[response.id];
300
+ if (!request) return currentState;
301
+ const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
302
+ return {
303
+ ...currentState,
304
+ requests: remainingRequests,
305
+ completedRequests: {
306
+ ...currentState.completedRequests,
307
+ [response.id]: {
308
+ ...request,
309
+ completedAt: Date.now(),
310
+ status: response.approved ? "approved" : "denied",
311
+ decision: result.decision
312
+ }
313
+ }
314
+ };
315
+ });
316
+ types.logger.debug(`[Gemini] Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
317
+ }
318
+ );
319
+ }
320
+ /**
321
+ * Reset handler state (on abort or task completion)
322
+ */
323
+ reset() {
324
+ for (const [id, pending] of this.pendingRequests.entries()) {
325
+ pending.reject(new Error("Permission handler reset"));
326
+ }
327
+ this.pendingRequests.clear();
328
+ this.session.updateAgentState((currentState) => {
329
+ const pendingRequests = currentState.requests || {};
330
+ const completedRequests = { ...currentState.completedRequests };
331
+ for (const [id, request] of Object.entries(pendingRequests)) {
332
+ completedRequests[id] = {
333
+ ...request,
334
+ completedAt: Date.now(),
335
+ status: "canceled",
336
+ reason: "Session reset"
337
+ };
338
+ }
339
+ return {
340
+ ...currentState,
341
+ requests: {},
342
+ completedRequests
343
+ };
344
+ });
345
+ types.logger.debug("[Gemini] Permission handler reset");
346
+ }
347
+ }
348
+
349
+ const GeminiDisplay = ({ messageBuffer, logPath, onExit }) => {
350
+ const [messages, setMessages] = React.useState([]);
351
+ const [confirmationMode, setConfirmationMode] = React.useState(false);
352
+ const [actionInProgress, setActionInProgress] = React.useState(false);
353
+ const confirmationTimeoutRef = React.useRef(null);
354
+ const { stdout } = ink.useStdout();
355
+ const terminalWidth = stdout.columns || 80;
356
+ const terminalHeight = stdout.rows || 24;
357
+ React.useEffect(() => {
358
+ setMessages(messageBuffer.getMessages());
359
+ const unsubscribe = messageBuffer.onUpdate((newMessages) => {
360
+ setMessages(newMessages);
361
+ });
362
+ return () => {
363
+ unsubscribe();
364
+ if (confirmationTimeoutRef.current) {
365
+ clearTimeout(confirmationTimeoutRef.current);
366
+ }
367
+ };
368
+ }, [messageBuffer]);
369
+ const resetConfirmation = React.useCallback(() => {
370
+ setConfirmationMode(false);
371
+ if (confirmationTimeoutRef.current) {
372
+ clearTimeout(confirmationTimeoutRef.current);
373
+ confirmationTimeoutRef.current = null;
374
+ }
375
+ }, []);
376
+ const setConfirmationWithTimeout = React.useCallback(() => {
377
+ setConfirmationMode(true);
378
+ if (confirmationTimeoutRef.current) {
379
+ clearTimeout(confirmationTimeoutRef.current);
380
+ }
381
+ confirmationTimeoutRef.current = setTimeout(() => {
382
+ resetConfirmation();
383
+ }, 15e3);
384
+ }, [resetConfirmation]);
385
+ ink.useInput(React.useCallback(async (input, key) => {
386
+ if (actionInProgress) return;
387
+ if (key.ctrl && input === "c") {
388
+ if (confirmationMode) {
389
+ resetConfirmation();
390
+ setActionInProgress(true);
391
+ await new Promise((resolve) => setTimeout(resolve, 100));
392
+ onExit?.();
393
+ } else {
394
+ setConfirmationWithTimeout();
395
+ }
396
+ return;
397
+ }
398
+ if (confirmationMode) {
399
+ resetConfirmation();
400
+ }
401
+ }, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
402
+ const getMessageColor = (type) => {
403
+ switch (type) {
404
+ case "user":
405
+ return "magenta";
406
+ case "assistant":
407
+ return "cyan";
408
+ case "system":
409
+ return "blue";
410
+ case "tool":
411
+ return "yellow";
412
+ case "result":
413
+ return "green";
414
+ case "status":
415
+ return "gray";
416
+ default:
417
+ return "white";
418
+ }
419
+ };
420
+ const formatMessage = (msg) => {
421
+ const lines = msg.content.split("\n");
422
+ const maxLineLength = terminalWidth - 10;
423
+ return lines.map((line) => {
424
+ if (line.length <= maxLineLength) return line;
425
+ const chunks = [];
426
+ for (let i = 0; i < line.length; i += maxLineLength) {
427
+ chunks.push(line.slice(i, i + maxLineLength));
428
+ }
429
+ return chunks.join("\n");
430
+ }).join("\n");
431
+ };
432
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
433
+ ink.Box,
434
+ {
435
+ flexDirection: "column",
436
+ width: terminalWidth,
437
+ height: terminalHeight - 4,
438
+ borderStyle: "round",
439
+ borderColor: "gray",
440
+ paddingX: 1,
441
+ overflow: "hidden"
442
+ },
443
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "\u{1F916} Gemini Agent Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
444
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
445
+ // Show only the last messages that fit in the available space
446
+ messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
447
+ ))
448
+ ), /* @__PURE__ */ React.createElement(
449
+ ink.Box,
450
+ {
451
+ width: terminalWidth,
452
+ borderStyle: "round",
453
+ borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "green",
454
+ paddingX: 2,
455
+ justifyContent: "center",
456
+ alignItems: "center",
457
+ flexDirection: "column"
458
+ },
459
+ /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "green", bold: true }, "\u{1F916} Gemini Agent Running \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
460
+ ));
461
+ };
462
+
463
+ function emitReadyIfIdle({ pending, queueSize, shouldExit, sendReady, notify }) {
464
+ if (shouldExit) {
465
+ return false;
466
+ }
467
+ if (queueSize() > 0) {
468
+ return false;
469
+ }
470
+ sendReady();
471
+ notify?.();
472
+ return true;
473
+ }
474
+ async function runGemini(opts) {
475
+ const sessionTag = node_crypto.randomUUID();
476
+ const api = new types.ApiClient(opts.token, opts.secret);
477
+ types.logger.debug(`[gemini] Starting Gemini session`);
478
+ const settings = await types.readSettings();
479
+ let machineId = settings?.machineId;
480
+ if (!machineId) {
481
+ console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue.`);
482
+ process.exit(1);
483
+ }
484
+ types.logger.debug(`Using machineId: ${machineId}`);
485
+ await api.getOrCreateMachine({
486
+ machineId,
487
+ metadata: index.initialMachineMetadata
488
+ });
489
+ let state = {
490
+ controlledByUser: false
491
+ };
492
+ let metadata = {
493
+ path: process.cwd(),
494
+ host: os$1.hostname(),
495
+ version: types.packageJson.version,
496
+ os: os$1.platform(),
497
+ machineId,
498
+ homeDir: os$1.homedir(),
499
+ happyHomeDir: types.configuration.happyHomeDir,
500
+ happyLibDir: types.projectPath(),
501
+ happyToolsDir: node_path.join(types.projectPath(), "tools", "unpacked"),
502
+ startedFromDaemon: false,
503
+ hostPid: process.pid,
504
+ startedBy: "terminal",
505
+ lifecycleState: "running",
506
+ lifecycleStateSince: Date.now(),
507
+ flavor: "gemini"
508
+ };
509
+ const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
510
+ const session = api.sessionSyncClient(response);
511
+ const messageQueue = new index.MessageQueue2((mode) => index.hashObject({
512
+ permissionMode: mode.permissionMode,
513
+ model: mode.model
514
+ }));
515
+ let currentPermissionMode = void 0;
516
+ let currentModel = void 0;
517
+ session.onUserMessage((message) => {
518
+ let messagePermissionMode = currentPermissionMode;
519
+ if (message.meta?.permissionMode) {
520
+ const validModes = ["default", "read-only", "safe-yolo", "yolo"];
521
+ if (validModes.includes(message.meta.permissionMode)) {
522
+ messagePermissionMode = message.meta.permissionMode;
523
+ currentPermissionMode = messagePermissionMode;
524
+ types.logger.debug(`[Gemini] Permission mode updated: ${currentPermissionMode}`);
525
+ }
526
+ }
527
+ let messageModel = currentModel;
528
+ if (message.meta?.hasOwnProperty("model")) {
529
+ messageModel = message.meta.model || void 0;
530
+ currentModel = messageModel;
531
+ types.logger.debug(`[Gemini] Model updated: ${messageModel || "default"}`);
532
+ }
533
+ const enhancedMode = {
534
+ permissionMode: messagePermissionMode || "default",
535
+ model: messageModel
536
+ };
537
+ messageQueue.push(message.content.text, enhancedMode);
538
+ });
539
+ let thinking = false;
540
+ session.keepAlive(thinking, "remote");
541
+ const keepAliveInterval = setInterval(() => {
542
+ session.keepAlive(thinking, "remote");
543
+ }, 2e3);
544
+ const sendReady = () => {
545
+ session.sendSessionEvent({ type: "ready" });
546
+ };
547
+ let abortController = new AbortController();
548
+ let shouldExit = false;
549
+ async function handleAbort() {
550
+ types.logger.debug("[Gemini] Abort requested");
551
+ try {
552
+ abortController.abort();
553
+ messageQueue.reset();
554
+ permissionHandler.reset();
555
+ if (client.hasActiveSession()) {
556
+ client.abort();
557
+ }
558
+ types.logger.debug("[Gemini] Abort completed");
559
+ } catch (error) {
560
+ types.logger.debug("[Gemini] Error during abort:", error);
561
+ } finally {
562
+ abortController = new AbortController();
563
+ }
564
+ }
565
+ const handleKillSession = async () => {
566
+ types.logger.debug("[Gemini] Kill session requested");
567
+ await handleAbort();
568
+ try {
569
+ if (session) {
570
+ session.updateMetadata((currentMetadata) => ({
571
+ ...currentMetadata,
572
+ lifecycleState: "archived",
573
+ lifecycleStateSince: Date.now(),
574
+ archivedBy: "cli",
575
+ archiveReason: "User terminated"
576
+ }));
577
+ session.sendSessionDeath();
578
+ await session.flush();
579
+ await session.close();
580
+ }
581
+ chellServer.stop();
582
+ types.logger.debug("[Gemini] Session termination complete");
583
+ process.exit(0);
584
+ } catch (error) {
585
+ types.logger.debug("[Gemini] Error during termination:", error);
586
+ process.exit(1);
587
+ }
588
+ };
589
+ session.rpcHandlerManager.registerHandler("abort", handleAbort);
590
+ index.registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
591
+ const messageBuffer = new index.MessageBuffer();
592
+ const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
593
+ let inkInstance = null;
594
+ if (hasTTY) {
595
+ console.clear();
596
+ inkInstance = ink.render(React.createElement(GeminiDisplay, {
597
+ messageBuffer,
598
+ logPath: process.env.DEBUG ? types.logger.getLogPath() : void 0,
599
+ onExit: async () => {
600
+ types.logger.debug("[gemini]: Exiting via Ctrl-C");
601
+ shouldExit = true;
602
+ await handleAbort();
603
+ }
604
+ }), {
605
+ exitOnCtrlC: false,
606
+ patchConsole: false
607
+ });
608
+ }
609
+ if (hasTTY) {
610
+ process.stdin.resume();
611
+ if (process.stdin.isTTY) {
612
+ process.stdin.setRawMode(true);
613
+ }
614
+ process.stdin.setEncoding("utf8");
615
+ let inputBuffer = "";
616
+ process.stdin.on("data", (data) => {
617
+ for (let i = 0; i < data.length; i++) {
618
+ const char = data[i];
619
+ const code = data.charCodeAt(i);
620
+ if (code === 3) continue;
621
+ if (code === 127 || code === 8) {
622
+ if (inputBuffer.length > 0) {
623
+ inputBuffer = inputBuffer.slice(0, -1);
624
+ }
625
+ continue;
626
+ }
627
+ if (char === "\r" || char === "\n") {
628
+ const trimmed = inputBuffer.trim();
629
+ if (trimmed) {
630
+ const enhancedMode = {
631
+ permissionMode: currentPermissionMode || "default",
632
+ model: currentModel
633
+ };
634
+ messageQueue.push(trimmed, enhancedMode);
635
+ messageBuffer.addMessage(trimmed, "user");
636
+ types.logger.debug(`[Gemini] Local input: ${trimmed}`);
637
+ try {
638
+ session.sendClaudeSessionMessage({
639
+ type: "user",
640
+ uuid: node_crypto.randomUUID(),
641
+ message: {
642
+ content: [{ type: "text", text: trimmed }]
643
+ }
644
+ });
645
+ } catch (e) {
646
+ types.logger.debug("[Gemini] Failed to mirror input:", e);
647
+ }
648
+ }
649
+ inputBuffer = "";
650
+ continue;
651
+ }
652
+ if (code >= 32 && code < 127) {
653
+ inputBuffer += char;
654
+ }
655
+ }
656
+ });
657
+ }
658
+ const client = new GeminiClient();
659
+ const permissionHandler = new GeminiPermissionHandler(session);
660
+ const chellServer = await index.startChellServer(session);
661
+ const bridgeCommand = node_path.join(types.projectPath(), "bin", "chell-mcp.mjs");
662
+ client.setHandler((event) => {
663
+ types.logger.debug(`[Gemini] Event: ${event.type}`);
664
+ types.updateActivityTimestamp();
665
+ if (event.type === "message") {
666
+ messageBuffer.addMessage(event.content, event.role);
667
+ } else if (event.type === "tool_use") {
668
+ messageBuffer.addMessage(`Using tool: ${event.toolName}`, "tool");
669
+ } else if (event.type === "tool_result") {
670
+ const output = event.output ? JSON.stringify(event.output).substring(0, 200) : "Done";
671
+ messageBuffer.addMessage(`Result: ${output}`, "result");
672
+ } else if (event.type === "error") {
673
+ messageBuffer.addMessage(`Error: ${event.message}`, "status");
674
+ } else if (event.type === "task_started") {
675
+ messageBuffer.addMessage("Task started", "status");
676
+ if (!thinking) {
677
+ thinking = true;
678
+ session.keepAlive(thinking, "remote");
679
+ }
680
+ } else if (event.type === "task_complete") {
681
+ messageBuffer.addMessage("Task completed", "status");
682
+ if (thinking) {
683
+ thinking = false;
684
+ session.keepAlive(thinking, "remote");
685
+ }
686
+ sendReady();
687
+ }
688
+ if (event.type === "message") {
689
+ session.sendCodexMessage({
690
+ type: "message",
691
+ message: event.content,
692
+ id: node_crypto.randomUUID()
693
+ });
694
+ try {
695
+ session.sendClaudeSessionMessage({
696
+ type: "assistant",
697
+ uuid: node_crypto.randomUUID(),
698
+ message: {
699
+ content: [{ type: "text", text: event.content }]
700
+ }
701
+ });
702
+ } catch (e) {
703
+ types.logger.debug("[Gemini] Failed to mirror message:", e);
704
+ }
705
+ }
706
+ });
707
+ try {
708
+ let sessionStarted = false;
709
+ while (!shouldExit) {
710
+ const waitSignal = abortController.signal;
711
+ const batch = await messageQueue.waitForMessagesAndGetAsString(waitSignal);
712
+ if (!batch) {
713
+ if (waitSignal.aborted && !shouldExit) {
714
+ types.logger.debug("[gemini]: Wait aborted while idle");
715
+ continue;
716
+ }
717
+ break;
718
+ }
719
+ messageBuffer.addMessage(batch.message, "user");
720
+ try {
721
+ if (!sessionStarted) {
722
+ const config = {
723
+ model: batch.mode.model || "gemini-2.5-flash",
724
+ yolo: batch.mode.permissionMode === "yolo",
725
+ debug: !!process.env.DEBUG
726
+ };
727
+ await client.startSession(
728
+ config,
729
+ chellServer.url,
730
+ bridgeCommand,
731
+ { signal: abortController.signal }
732
+ );
733
+ sessionStarted = true;
734
+ }
735
+ await client.sendMessage(batch.message);
736
+ } catch (error) {
737
+ types.logger.warn("Error in gemini session:", error);
738
+ const isAbortError = error instanceof Error && error.name === "AbortError";
739
+ if (isAbortError) {
740
+ messageBuffer.addMessage("Aborted by user", "status");
741
+ session.sendSessionEvent({ type: "message", message: "Aborted by user" });
742
+ } else {
743
+ messageBuffer.addMessage("Process exited unexpectedly", "status");
744
+ session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
745
+ }
746
+ } finally {
747
+ permissionHandler.reset();
748
+ thinking = false;
749
+ session.keepAlive(thinking, "remote");
750
+ emitReadyIfIdle({
751
+ pending: null,
752
+ queueSize: () => messageQueue.size(),
753
+ shouldExit,
754
+ sendReady
755
+ });
756
+ }
757
+ }
758
+ } finally {
759
+ types.logger.debug("[gemini]: Final cleanup");
760
+ try {
761
+ session.sendSessionDeath();
762
+ await session.flush();
763
+ await session.close();
764
+ } catch (e) {
765
+ types.logger.debug("[gemini]: Error closing session", e);
766
+ }
767
+ await client.disconnect();
768
+ chellServer.stop();
769
+ if (process.stdin.isTTY) {
770
+ try {
771
+ process.stdin.setRawMode(false);
772
+ } catch {
773
+ }
774
+ }
775
+ if (hasTTY) {
776
+ try {
777
+ process.stdin.pause();
778
+ } catch {
779
+ }
780
+ }
781
+ clearInterval(keepAliveInterval);
782
+ if (inkInstance) {
783
+ inkInstance.unmount();
784
+ }
785
+ messageBuffer.clear();
786
+ types.logger.debug("[gemini]: Final cleanup completed");
787
+ }
788
+ }
789
+
790
+ exports.emitReadyIfIdle = emitReadyIfIdle;
791
+ exports.runGemini = runGemini;