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