claude-simple 0.2.8 → 0.2.10
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 +13 -5
- package/dist/app.0.2.9.min.js +1084 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.9.min.css +1598 -0
- package/package.json +11 -3
- package/public/chat.js +14 -11
- package/public/files.js +5 -5
- package/public/index.html +4 -5
- package/public/lib.js +1 -1
- package/public/router.js +36 -8
- package/public/settings.js +54 -2
- package/public/shell/boot.js +7 -7
- package/public/shell/layout.js +2 -3
- package/public/shell/session-list.js +22 -16
- package/public/shell/templates.js +26 -24
- package/public/shell/util.js +3 -1
- package/public/shell.js +54 -13
- package/public/state.js +50 -4
- package/public/sw.js +6 -2
- package/public/ui.min.css +2 -0
- package/public/vendor/highlight-dark.min.css +10 -0
- package/public/vendor/highlight.min.js +1262 -0
- package/public/vendor/jszip.min.js +13 -0
- package/public/vendor/mammoth.browser.min.js +21 -0
- package/public/vendor/marked.min.js +6 -0
- package/public/vendor/mini-react.min.js +4 -0
- package/public/vendor/pdf.min.js +22 -0
- package/public/vendor/pdf.worker.min.js +22 -0
- package/public/vendor/xlsx.full.min.js +22 -0
- package/src/agents/cursor.js +189 -0
- package/src/agents/gemini.js +182 -0
- package/src/agents/grok.js +300 -37
- package/src/agents/meta-agent.js +9 -5
- package/src/agents/models.js +114 -3
- package/src/agents/sessions.js +258 -14
- package/src/app.js +131 -9
- package/src/mcp.js +275 -0
- package/dist/app.0.2.8.min.js +0 -1080
- package/dist/style.0.2.8.min.css +0 -1598
package/src/agents/grok.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
import readline from 'readline';
|
|
3
5
|
import crypto from 'crypto';
|
|
4
6
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// individual browser tab, serialize follow-up prompts.
|
|
7
|
+
// Persistent ACP (JSON-RPC over stdio). Advertise terminal+fs so Grok routes
|
|
8
|
+
// shell/file tools through client RPCs instead of its own broken terminal path.
|
|
8
9
|
const clients = new Map();
|
|
9
10
|
const queues = new Map();
|
|
10
11
|
const queueAliases = new Map();
|
|
@@ -13,6 +14,147 @@ function profileKey(options) {
|
|
|
13
14
|
return `${options.model || ''}\u001f${options.effort || ''}`;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function envFromParams(envList) {
|
|
18
|
+
const env = { ...process.env };
|
|
19
|
+
if (!Array.isArray(envList)) return env;
|
|
20
|
+
for (const item of envList) {
|
|
21
|
+
if (!item || typeof item.name !== 'string') continue;
|
|
22
|
+
env[item.name] = item.value == null ? '' : String(item.value);
|
|
23
|
+
}
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class TerminalHost {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.terminals = new Map();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
create(params = {}) {
|
|
33
|
+
const terminalId = `term_${crypto.randomUUID()}`;
|
|
34
|
+
const command = String(params.command || '');
|
|
35
|
+
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
36
|
+
const cwd = params.cwd || process.cwd();
|
|
37
|
+
const limit = Number.isFinite(params.outputByteLimit) && params.outputByteLimit > 0
|
|
38
|
+
? params.outputByteLimit
|
|
39
|
+
: 1024 * 1024;
|
|
40
|
+
const env = envFromParams(params.env);
|
|
41
|
+
|
|
42
|
+
let child;
|
|
43
|
+
try {
|
|
44
|
+
child = spawn(command, args, {
|
|
45
|
+
cwd,
|
|
46
|
+
env,
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
shell: args.length === 0,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const err = new Error(error.message || 'Failed to start terminal');
|
|
52
|
+
err.code = -32000;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const entry = {
|
|
57
|
+
id: terminalId,
|
|
58
|
+
child,
|
|
59
|
+
output: '',
|
|
60
|
+
truncated: false,
|
|
61
|
+
limit,
|
|
62
|
+
exitCode: null,
|
|
63
|
+
signal: null,
|
|
64
|
+
exited: false,
|
|
65
|
+
waiters: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const append = chunk => {
|
|
69
|
+
entry.output += chunk;
|
|
70
|
+
if (Buffer.byteLength(entry.output, 'utf8') > entry.limit) {
|
|
71
|
+
// Truncate from the start at a UTF-8-safe boundary.
|
|
72
|
+
let buf = Buffer.from(entry.output, 'utf8');
|
|
73
|
+
buf = buf.subarray(buf.length - entry.limit);
|
|
74
|
+
while (buf.length && (buf[0] & 0xc0) === 0x80) buf = buf.subarray(1);
|
|
75
|
+
entry.output = buf.toString('utf8');
|
|
76
|
+
entry.truncated = true;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
child.stdout?.setEncoding('utf8');
|
|
81
|
+
child.stderr?.setEncoding('utf8');
|
|
82
|
+
child.stdout?.on('data', append);
|
|
83
|
+
child.stderr?.on('data', append);
|
|
84
|
+
child.on('error', err => {
|
|
85
|
+
append(`\n[terminal error] ${err.message}\n`);
|
|
86
|
+
if (!entry.exited) {
|
|
87
|
+
entry.exited = true;
|
|
88
|
+
entry.exitCode = 1;
|
|
89
|
+
entry.signal = null;
|
|
90
|
+
for (const resolve of entry.waiters) resolve({ exitCode: 1, signal: null });
|
|
91
|
+
entry.waiters = [];
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
child.on('close', (code, signal) => {
|
|
95
|
+
entry.exited = true;
|
|
96
|
+
entry.exitCode = code;
|
|
97
|
+
entry.signal = signal || null;
|
|
98
|
+
for (const resolve of entry.waiters) resolve({ exitCode: code, signal: signal || null });
|
|
99
|
+
entry.waiters = [];
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.terminals.set(terminalId, entry);
|
|
103
|
+
return { terminalId };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get(terminalId) {
|
|
107
|
+
const entry = this.terminals.get(terminalId);
|
|
108
|
+
if (!entry) {
|
|
109
|
+
const err = new Error(`Unknown terminal: ${terminalId}`);
|
|
110
|
+
err.code = -32000;
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
output(terminalId) {
|
|
117
|
+
const entry = this.get(terminalId);
|
|
118
|
+
const result = {
|
|
119
|
+
output: entry.output,
|
|
120
|
+
truncated: entry.truncated,
|
|
121
|
+
};
|
|
122
|
+
if (entry.exited) {
|
|
123
|
+
result.exitStatus = { exitCode: entry.exitCode, signal: entry.signal };
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
waitForExit(terminalId) {
|
|
129
|
+
const entry = this.get(terminalId);
|
|
130
|
+
if (entry.exited) return Promise.resolve({ exitCode: entry.exitCode, signal: entry.signal });
|
|
131
|
+
return new Promise(resolve => entry.waiters.push(resolve));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
kill(terminalId) {
|
|
135
|
+
const entry = this.get(terminalId);
|
|
136
|
+
if (!entry.exited && entry.child?.pid) {
|
|
137
|
+
try { entry.child.kill('SIGTERM'); } catch { /* already gone */ }
|
|
138
|
+
}
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
release(terminalId) {
|
|
143
|
+
const entry = this.terminals.get(terminalId);
|
|
144
|
+
if (!entry) return {};
|
|
145
|
+
if (!entry.exited && entry.child?.pid) {
|
|
146
|
+
try { entry.child.kill('SIGTERM'); } catch { /* already gone */ }
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
if (!entry.exited && entry.child?.pid) {
|
|
149
|
+
try { entry.child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
150
|
+
}
|
|
151
|
+
}, 800).unref?.();
|
|
152
|
+
}
|
|
153
|
+
this.terminals.delete(terminalId);
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
16
158
|
class GrokACPClient {
|
|
17
159
|
constructor({ cliPath, model, effort }) {
|
|
18
160
|
const args = ['agent', '--always-approve'];
|
|
@@ -22,13 +164,17 @@ class GrokACPClient {
|
|
|
22
164
|
this.child = spawn(cliPath || 'grok', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
23
165
|
this.pending = new Map();
|
|
24
166
|
this.listeners = new Map();
|
|
25
|
-
|
|
167
|
+
// High ids avoid colliding with agent-originated request ids (often 0..N).
|
|
168
|
+
this.nextID = 100000;
|
|
169
|
+
this.terminals = new TerminalHost();
|
|
26
170
|
this.ready = this.initialize();
|
|
27
171
|
this.child.stderr.on('data', d => console.warn(`[grok acp] ${String(d).trim()}`));
|
|
28
172
|
this.child.on('close', () => {
|
|
29
173
|
for (const { reject } of this.pending.values()) reject(new Error('Grok ACP process stopped'));
|
|
30
174
|
this.pending.clear();
|
|
31
|
-
for (const fn of this.listeners.values())
|
|
175
|
+
for (const fn of this.listeners.values()) {
|
|
176
|
+
fn({ type: 'error', message: 'Grok ACP process stopped', agent: 'grok' });
|
|
177
|
+
}
|
|
32
178
|
this.listeners.clear();
|
|
33
179
|
});
|
|
34
180
|
const rl = readline.createInterface({ input: this.child.stdout, crlfDelay: Infinity });
|
|
@@ -38,19 +184,82 @@ class GrokACPClient {
|
|
|
38
184
|
async initialize() {
|
|
39
185
|
const init = await this.request('initialize', {
|
|
40
186
|
protocolVersion: 1,
|
|
41
|
-
clientCapabilities: {
|
|
42
|
-
|
|
187
|
+
clientCapabilities: {
|
|
188
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
189
|
+
terminal: true,
|
|
190
|
+
},
|
|
191
|
+
clientInfo: { name: 'claude-simple-ui', version: '0.2.9' },
|
|
43
192
|
});
|
|
44
193
|
const methods = new Set((init.authMethods || []).map(x => x?.id));
|
|
45
194
|
const methodId = process.env.XAI_API_KEY && methods.has('xai.api_key')
|
|
46
195
|
? 'xai.api_key'
|
|
47
196
|
: (methods.has('cached_token') ? 'cached_token' : null);
|
|
48
197
|
if (methodId) await this.request('authenticate', { methodId, _meta: { headless: true } });
|
|
198
|
+
return init;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async handleClientRequest(msg) {
|
|
202
|
+
const method = msg.method;
|
|
203
|
+
const params = msg.params || {};
|
|
204
|
+
try {
|
|
205
|
+
let result = {};
|
|
206
|
+
if (method === 'fs/read_text_file') {
|
|
207
|
+
const filePath = String(params.path || '');
|
|
208
|
+
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
209
|
+
const limit = Number.isFinite(params.limit) ? params.limit : null;
|
|
210
|
+
const line = Number.isFinite(params.line) ? params.line : null;
|
|
211
|
+
if (line != null || limit != null) {
|
|
212
|
+
const lines = content.split('\n');
|
|
213
|
+
const start = Math.max(0, (line || 1) - 1);
|
|
214
|
+
const end = limit != null ? start + limit : lines.length;
|
|
215
|
+
result = { content: lines.slice(start, end).join('\n') };
|
|
216
|
+
} else {
|
|
217
|
+
result = { content };
|
|
218
|
+
}
|
|
219
|
+
} else if (method === 'fs/write_text_file') {
|
|
220
|
+
const filePath = String(params.path || '');
|
|
221
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
222
|
+
await fs.promises.writeFile(filePath, params.content ?? '', 'utf8');
|
|
223
|
+
result = {};
|
|
224
|
+
} else if (method === 'terminal/create') {
|
|
225
|
+
result = this.terminals.create(params);
|
|
226
|
+
} else if (method === 'terminal/output') {
|
|
227
|
+
result = this.terminals.output(params.terminalId);
|
|
228
|
+
} else if (method === 'terminal/wait_for_exit') {
|
|
229
|
+
result = await this.terminals.waitForExit(params.terminalId);
|
|
230
|
+
} else if (method === 'terminal/kill') {
|
|
231
|
+
result = this.terminals.kill(params.terminalId);
|
|
232
|
+
} else if (method === 'terminal/release') {
|
|
233
|
+
result = this.terminals.release(params.terminalId);
|
|
234
|
+
} else {
|
|
235
|
+
this.write({
|
|
236
|
+
jsonrpc: '2.0',
|
|
237
|
+
id: msg.id,
|
|
238
|
+
error: { code: -32601, message: `Unsupported client method: ${method}` },
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.write({ jsonrpc: '2.0', id: msg.id, result });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.write({
|
|
245
|
+
jsonrpc: '2.0',
|
|
246
|
+
id: msg.id,
|
|
247
|
+
error: { code: error.code || -32000, message: error.message || 'Client request failed' },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
49
250
|
}
|
|
50
251
|
|
|
51
252
|
receive(line) {
|
|
52
253
|
let msg;
|
|
53
254
|
try { msg = JSON.parse(line); } catch { return; }
|
|
255
|
+
|
|
256
|
+
// Agent→client requests always carry a method. Check before pending so
|
|
257
|
+
// overlapping numeric ids cannot steal a client RPC as our response.
|
|
258
|
+
if (msg.id != null && msg.method) {
|
|
259
|
+
void this.handleClientRequest(msg);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
54
263
|
if (msg.id != null && this.pending.has(msg.id)) {
|
|
55
264
|
const pending = this.pending.get(msg.id);
|
|
56
265
|
this.pending.delete(msg.id);
|
|
@@ -58,13 +267,7 @@ class GrokACPClient {
|
|
|
58
267
|
else pending.resolve(msg.result || {});
|
|
59
268
|
return;
|
|
60
269
|
}
|
|
61
|
-
|
|
62
|
-
// Return a structured error for optional extension calls instead of leaving
|
|
63
|
-
// an agent request hanging forever.
|
|
64
|
-
if (msg.id != null && msg.method) {
|
|
65
|
-
this.write({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Unsupported client method: ${msg.method}` } });
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
270
|
+
|
|
68
271
|
if (msg.method === 'session/update') {
|
|
69
272
|
const sid = msg.params?.sessionId;
|
|
70
273
|
const fn = sid && this.listeners.get(sid);
|
|
@@ -109,37 +312,68 @@ class GrokACPClient {
|
|
|
109
312
|
function clientFor(options, cliPath) {
|
|
110
313
|
const key = profileKey(options);
|
|
111
314
|
let client = clients.get(key);
|
|
112
|
-
if (!client || client.child.exitCode != null) {
|
|
315
|
+
if (!client || client.child.exitCode != null || client.child.signalCode != null) {
|
|
113
316
|
client = new GrokACPClient({ cliPath, model: options.model, effort: options.effort });
|
|
114
317
|
clients.set(key, client);
|
|
115
318
|
}
|
|
116
319
|
return client;
|
|
117
320
|
}
|
|
118
321
|
|
|
119
|
-
function emitUpdate(update, send,
|
|
322
|
+
function emitUpdate(update, send, state) {
|
|
120
323
|
const kind = update?.sessionUpdate;
|
|
121
324
|
if (kind === 'agent_message_chunk' && update.content?.text) {
|
|
122
|
-
if (
|
|
325
|
+
if (state.thoughtOpen) {
|
|
326
|
+
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
327
|
+
state.thoughtOpen = false;
|
|
328
|
+
}
|
|
329
|
+
if (!state.textOpen) {
|
|
330
|
+
send({ type: 'assistant_stream_start', agent: 'grok', ts: Date.now() });
|
|
331
|
+
state.textOpen = true;
|
|
332
|
+
}
|
|
123
333
|
send({ type: 'assistant_delta', data: update.content.text, agent: 'grok' });
|
|
124
334
|
} else if (kind === 'agent_thought_chunk' && update.content?.text) {
|
|
125
|
-
if (!state.thoughtOpen) {
|
|
335
|
+
if (!state.thoughtOpen) {
|
|
336
|
+
send({ type: 'thought_stream_start', agent: 'grok' });
|
|
337
|
+
state.thoughtOpen = true;
|
|
338
|
+
}
|
|
126
339
|
send({ type: 'thought_delta', data: update.content.text, agent: 'grok' });
|
|
127
340
|
} else if (kind === 'tool_call' || kind === 'tool_call_update') {
|
|
128
|
-
if (state.textOpen) {
|
|
129
|
-
|
|
341
|
+
if (state.textOpen) {
|
|
342
|
+
send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
343
|
+
state.textOpen = false;
|
|
344
|
+
}
|
|
345
|
+
if (state.thoughtOpen) {
|
|
346
|
+
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
347
|
+
state.thoughtOpen = false;
|
|
348
|
+
}
|
|
130
349
|
if (kind === 'tool_call') {
|
|
131
|
-
send({
|
|
132
|
-
type: '
|
|
133
|
-
|
|
350
|
+
send({
|
|
351
|
+
type: 'assistant',
|
|
352
|
+
agent: 'grok',
|
|
353
|
+
message: {
|
|
354
|
+
content: [{
|
|
355
|
+
type: 'tool_use',
|
|
356
|
+
name: update.title || update.kind || 'tool',
|
|
357
|
+
input: update.rawInput || update.input || {},
|
|
358
|
+
}],
|
|
359
|
+
},
|
|
360
|
+
});
|
|
134
361
|
}
|
|
135
362
|
}
|
|
136
363
|
}
|
|
137
364
|
|
|
138
365
|
async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
139
366
|
const { command, options, send } = item;
|
|
140
|
-
const
|
|
367
|
+
const effort = options.effort || 'low';
|
|
368
|
+
const client = clientFor({ ...options, effort }, cliPath);
|
|
141
369
|
const localKey = options.sessionId || crypto.randomUUID();
|
|
142
|
-
const entry = {
|
|
370
|
+
const entry = {
|
|
371
|
+
kind: 'grok-acp',
|
|
372
|
+
startTime: Date.now(),
|
|
373
|
+
busy: true,
|
|
374
|
+
key: localKey,
|
|
375
|
+
sessionId: options.sessionId || null,
|
|
376
|
+
};
|
|
143
377
|
activeSessions.set(localKey, entry);
|
|
144
378
|
const state = { textOpen: false, thoughtOpen: false };
|
|
145
379
|
let sessionId = options.sessionId;
|
|
@@ -148,9 +382,14 @@ async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
|
148
382
|
queueAliases.set(sessionId, queueKey);
|
|
149
383
|
entry.sessionId = sessionId;
|
|
150
384
|
activeSessions.set(sessionId, entry);
|
|
151
|
-
entry.abort = () => {
|
|
385
|
+
entry.abort = () => {
|
|
386
|
+
try {
|
|
387
|
+
client.write({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } });
|
|
388
|
+
} catch { /* process may already be gone */ }
|
|
389
|
+
};
|
|
152
390
|
if (!options.sessionId) send({ type: 'session-created', sessionId, agent: 'grok' });
|
|
153
|
-
client.listeners.set(sessionId, update => emitUpdate(update, send,
|
|
391
|
+
client.listeners.set(sessionId, update => emitUpdate(update, send, state));
|
|
392
|
+
send({ type: 'status', message: 'Grok ACP running…', agent: 'grok', sessionId });
|
|
154
393
|
const result = await client.request('session/prompt', {
|
|
155
394
|
sessionId,
|
|
156
395
|
prompt: [{ type: 'text', text: command }],
|
|
@@ -158,10 +397,24 @@ async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
|
158
397
|
if (state.textOpen) send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
159
398
|
if (state.thoughtOpen) send({ type: 'thought_stream_end', agent: 'grok' });
|
|
160
399
|
entry.busy = false;
|
|
161
|
-
send({
|
|
162
|
-
|
|
400
|
+
send({
|
|
401
|
+
type: 'result',
|
|
402
|
+
is_error: result.stopReason === 'error',
|
|
403
|
+
usage: result._meta?.usage || null,
|
|
404
|
+
sessionId,
|
|
405
|
+
agent: 'grok',
|
|
406
|
+
});
|
|
407
|
+
send({
|
|
408
|
+
type: 'complete',
|
|
409
|
+
exitCode: result.stopReason === 'cancelled' ? 130 : 0,
|
|
410
|
+
aborted: result.stopReason === 'cancelled',
|
|
411
|
+
sessionId,
|
|
412
|
+
agent: 'grok',
|
|
413
|
+
});
|
|
163
414
|
} catch (e) {
|
|
164
415
|
entry.busy = false;
|
|
416
|
+
if (state.textOpen) send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
417
|
+
if (state.thoughtOpen) send({ type: 'thought_stream_end', agent: 'grok' });
|
|
165
418
|
send({ type: 'error', message: e.message, sessionId, agent: 'grok' });
|
|
166
419
|
send({ type: 'complete', exitCode: 1, sessionId, agent: 'grok' });
|
|
167
420
|
} finally {
|
|
@@ -171,19 +424,26 @@ async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
|
171
424
|
}
|
|
172
425
|
}
|
|
173
426
|
|
|
174
|
-
/** Queue all Grok follow-ups at the edge.
|
|
427
|
+
/** Queue all Grok follow-ups at the edge. Promise resolves after this item runs. */
|
|
175
428
|
export function runGrok(command, options, send, { cliPath, activeSessions }) {
|
|
176
|
-
const key = (options.sessionId && queueAliases.get(options.sessionId))
|
|
429
|
+
const key = (options.sessionId && queueAliases.get(options.sessionId))
|
|
430
|
+
|| options.sessionId
|
|
431
|
+
|| `new:${options.cwd || ''}`;
|
|
177
432
|
const queue = queues.get(key) || { items: [], running: false };
|
|
178
433
|
queues.set(key, queue);
|
|
179
434
|
return new Promise(resolve => {
|
|
180
435
|
queue.items.push({ command, options, send, resolve });
|
|
181
436
|
if (queue.running) {
|
|
182
|
-
send({
|
|
437
|
+
send({
|
|
438
|
+
type: 'status',
|
|
439
|
+
message: `Grok follow-up queued (${queue.items.length - 1} ahead)`,
|
|
440
|
+
sessionId: options.sessionId,
|
|
441
|
+
agent: 'grok',
|
|
442
|
+
});
|
|
183
443
|
return;
|
|
184
444
|
}
|
|
185
445
|
queue.running = true;
|
|
186
|
-
|
|
446
|
+
void (async () => {
|
|
187
447
|
while (queue.items.length) {
|
|
188
448
|
const item = queue.items.shift();
|
|
189
449
|
await runOne(item, { cliPath, activeSessions, queueKey: key });
|
|
@@ -192,16 +452,19 @@ export function runGrok(command, options, send, { cliPath, activeSessions }) {
|
|
|
192
452
|
queue.running = false;
|
|
193
453
|
queues.delete(key);
|
|
194
454
|
for (const [sid, alias] of queueAliases) if (alias === key) queueAliases.delete(sid);
|
|
195
|
-
};
|
|
196
|
-
void drain();
|
|
455
|
+
})();
|
|
197
456
|
});
|
|
198
457
|
}
|
|
199
458
|
|
|
200
459
|
export async function interruptGrok(sessionId) {
|
|
201
460
|
for (const client of clients.values()) {
|
|
202
461
|
if (client.listeners.has(sessionId)) {
|
|
203
|
-
|
|
204
|
-
|
|
462
|
+
try {
|
|
463
|
+
client.write({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } });
|
|
464
|
+
return true;
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
205
468
|
}
|
|
206
469
|
}
|
|
207
470
|
return false;
|
package/src/agents/meta-agent.js
CHANGED
|
@@ -38,7 +38,7 @@ export const AI_DEFAULTS = {
|
|
|
38
38
|
auto_report_days: 1,
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
export const DEFAULT_SYSTEM = `你是 Claude Simple UI 的内置工作助手(Meta Agent)。你能访问本机上的 Claude / Codex / Grok 会话、项目笔记、Git 与文件,并生成「我做了啥」的工作报告。
|
|
41
|
+
export const DEFAULT_SYSTEM = `你是 Claude Simple UI 的内置工作助手(Meta Agent)。你能访问本机上的 Claude / Codex / Grok / Gemini 会话、项目笔记、Git 与文件,并生成「我做了啥」的工作报告。
|
|
42
42
|
|
|
43
43
|
规则:
|
|
44
44
|
1. 需要会话 id / 项目路径时,先 list_projects 或 search_activity / list_sessions,再深入 get_session_brief / get_session_summary。超长会话先读 get_session_brief;只有需要逐事件定位问题时才读 get_session_jsonl。
|
|
@@ -73,12 +73,12 @@ const TOOLS = [
|
|
|
73
73
|
type: 'function',
|
|
74
74
|
function: {
|
|
75
75
|
name: 'list_sessions',
|
|
76
|
-
description: '列出最近 agent 会话(Claude/Codex/Grok)元数据',
|
|
76
|
+
description: '列出最近 agent 会话(Claude/Codex/Grok/Gemini)元数据',
|
|
77
77
|
parameters: {
|
|
78
78
|
type: 'object',
|
|
79
79
|
properties: {
|
|
80
80
|
days: { type: 'integer', description: '只看最近 N 天,0=全部,默认 14' },
|
|
81
|
-
agent: { type: 'string', enum: ['claude', 'codex', 'grok'], description: '可选过滤' },
|
|
81
|
+
agent: { type: 'string', enum: ['claude', 'codex', 'grok', 'gemini', 'cursor'], description: '可选过滤' },
|
|
82
82
|
cwd: { type: 'string', description: '按工作目录过滤' },
|
|
83
83
|
limit: { type: 'integer', description: '最多返回条数,默认 40' },
|
|
84
84
|
},
|
|
@@ -153,13 +153,13 @@ const TOOLS = [
|
|
|
153
153
|
type: 'function',
|
|
154
154
|
function: {
|
|
155
155
|
name: 'get_session_jsonl',
|
|
156
|
-
description: '读取 Claude/Codex/Grok 指定会话的原始 JSONL 行,用于逐事件、工具调用或失败原因分析;内容可能敏感,只在当前回答中使用',
|
|
156
|
+
description: '读取 Claude/Codex/Grok/Gemini 指定会话的原始 JSONL 行,用于逐事件、工具调用或失败原因分析;内容可能敏感,只在当前回答中使用',
|
|
157
157
|
parameters: {
|
|
158
158
|
type: 'object',
|
|
159
159
|
required: ['sessionId'],
|
|
160
160
|
properties: {
|
|
161
161
|
sessionId: { type: 'string' },
|
|
162
|
-
agent: { type: 'string', enum: ['claude', 'codex', 'grok'], description: '建议明确指定来源 agent' },
|
|
162
|
+
agent: { type: 'string', enum: ['claude', 'codex', 'grok', 'gemini', 'cursor'], description: '建议明确指定来源 agent' },
|
|
163
163
|
offset: { type: 'integer', description: '从第几行开始,默认 0' },
|
|
164
164
|
maxLines: { type: 'integer', description: '每次最多 2000 行,默认 500' },
|
|
165
165
|
},
|
|
@@ -500,6 +500,8 @@ export function createMetaAgent(deps) {
|
|
|
500
500
|
claudeConfigDirs,
|
|
501
501
|
codexHome,
|
|
502
502
|
grokHome,
|
|
503
|
+
geminiHome,
|
|
504
|
+
cursorHome,
|
|
503
505
|
} = deps;
|
|
504
506
|
|
|
505
507
|
const configFile = path.join(rootDir, '.ai_config.json');
|
|
@@ -1508,6 +1510,8 @@ export function createMetaAgent(deps) {
|
|
|
1508
1510
|
claudeConfigDirs: claudeConfigDirs(),
|
|
1509
1511
|
codexHome: codexHome(),
|
|
1510
1512
|
grokHome: grokHome(),
|
|
1513
|
+
geminiHome: geminiHome(),
|
|
1514
|
+
cursorHome: cursorHome(),
|
|
1511
1515
|
};
|
|
1512
1516
|
}
|
|
1513
1517
|
|
package/src/agents/models.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
|
+
import { execFile } from 'child_process';
|
|
8
9
|
|
|
9
10
|
/** Static fallbacks when local cache/config is missing. */
|
|
10
11
|
export const FALLBACK_MODELS = {
|
|
@@ -37,6 +38,31 @@ export const FALLBACK_MODELS = {
|
|
|
37
38
|
{ value: 'grok-3-mini', label: 'grok-3-mini' },
|
|
38
39
|
],
|
|
39
40
|
},
|
|
41
|
+
gemini: {
|
|
42
|
+
model: 'gemini-3.7-flash-high',
|
|
43
|
+
models: [
|
|
44
|
+
{ value: 'gemini-3.7-flash-high', label: 'Gemini 3.7 Flash (High)' },
|
|
45
|
+
{ value: 'gemini-3.7-flash-medium', label: 'Gemini 3.7 Flash (Medium)' },
|
|
46
|
+
{ value: 'gemini-3.7-flash-low', label: 'Gemini 3.7 Flash (Low)' },
|
|
47
|
+
{ value: 'gemini-3.6-flash-high', label: 'Gemini 3.6 Flash (High)' },
|
|
48
|
+
{ value: 'gemini-3.6-flash-medium', label: 'Gemini 3.6 Flash (Medium)' },
|
|
49
|
+
{ value: 'gemini-3.6-flash-low', label: 'Gemini 3.6 Flash (Low)' },
|
|
50
|
+
{ value: 'gemini-3.5-flash-high', label: 'Gemini 3.5 Flash (High)' },
|
|
51
|
+
{ value: 'gemini-3.5-flash-medium', label: 'Gemini 3.5 Flash (Medium)' },
|
|
52
|
+
{ value: 'gemini-3.5-flash-low', label: 'Gemini 3.5 Flash (Low)' },
|
|
53
|
+
{ value: 'gemini-3.1-pro-high', label: 'Gemini 3.1 Pro (High)' },
|
|
54
|
+
{ value: 'gemini-3.1-pro-low', label: 'Gemini 3.1 Pro (Low)' },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
cursor: {
|
|
58
|
+
model: 'auto',
|
|
59
|
+
models: [
|
|
60
|
+
{ value: 'auto', label: 'Auto' },
|
|
61
|
+
{ value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
|
|
62
|
+
{ value: 'composer-2.5', label: 'Composer 2.5' },
|
|
63
|
+
{ value: 'claude-sonnet-5-thinking-high', label: 'Claude Sonnet 5' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
40
66
|
};
|
|
41
67
|
|
|
42
68
|
const DEFAULT_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
@@ -304,7 +330,8 @@ async function discoverGrok(grokHome) {
|
|
|
304
330
|
|
|
305
331
|
const active = models.find(m => m.value === defaultModel);
|
|
306
332
|
const efforts = active?.efforts || DEFAULT_EFFORTS;
|
|
307
|
-
|
|
333
|
+
// This UI defaults Grok reasoning effort to low.
|
|
334
|
+
const effortDefault = 'low';
|
|
308
335
|
|
|
309
336
|
return {
|
|
310
337
|
model: defaultModel,
|
|
@@ -317,11 +344,89 @@ async function discoverGrok(grokHome) {
|
|
|
317
344
|
};
|
|
318
345
|
}
|
|
319
346
|
|
|
347
|
+
let geminiModelsCache = null;
|
|
348
|
+
let geminiModelsCachedAt = 0;
|
|
349
|
+
|
|
350
|
+
async function listAgyModels(cliPath) {
|
|
351
|
+
if (geminiModelsCache && Date.now() - geminiModelsCachedAt < 60_000) return geminiModelsCache;
|
|
352
|
+
// agy renders its model picker through a TTY. `script` supplies a tiny
|
|
353
|
+
// pseudo-terminal while keeping this request non-interactive.
|
|
354
|
+
const command = cliPath || 'agy';
|
|
355
|
+
const scriptArgs = process.platform === 'darwin'
|
|
356
|
+
? ['-q', '/dev/null', command, 'models']
|
|
357
|
+
: ['-q', '-c', `${command} models`, '/dev/null'];
|
|
358
|
+
const output = await new Promise(resolve => {
|
|
359
|
+
execFile('script', scriptArgs, { timeout: 20_000, maxBuffer: 512 * 1024 }, (error, stdout) => {
|
|
360
|
+
resolve(error ? '' : String(stdout || ''));
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
const models = output.split(/\r?\n/)
|
|
364
|
+
.map(line => line.replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, '').trim())
|
|
365
|
+
.filter(line => /^gemini-/i.test(line))
|
|
366
|
+
.map(line => {
|
|
367
|
+
const [value, ...label] = line.split(/\t+/);
|
|
368
|
+
return { value: value.trim(), label: label.join(' ').trim() || shortLabel(value.trim()) };
|
|
369
|
+
});
|
|
370
|
+
geminiModelsCache = uniqModels(models);
|
|
371
|
+
geminiModelsCachedAt = Date.now();
|
|
372
|
+
return geminiModelsCache;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function discoverGemini(geminiHome, cliPath) {
|
|
376
|
+
const home = geminiHome || path.join(os.homedir(), '.gemini');
|
|
377
|
+
const settings = await readJson(path.join(home, 'antigravity-cli', 'settings.json'))
|
|
378
|
+
|| await readJson(path.join(home, 'settings.json'));
|
|
379
|
+
let models = await listAgyModels(cliPath);
|
|
380
|
+
let source = models.length ? 'agy' : 'fallback';
|
|
381
|
+
if (!models.length) models = FALLBACK_MODELS.gemini.models.map(m => ({ ...m }));
|
|
382
|
+
const configured = typeof settings?.model === 'string' ? settings.model.trim() : null;
|
|
383
|
+
const selected = configured && models.find(m => m.value === configured || m.label === configured);
|
|
384
|
+
const defaultModel = selected?.value || models[0]?.value || FALLBACK_MODELS.gemini.model;
|
|
385
|
+
if (configured) source += '+settings';
|
|
386
|
+
return {
|
|
387
|
+
model: defaultModel,
|
|
388
|
+
models: uniqModels(ensureDefaultInList(models, defaultModel)),
|
|
389
|
+
efforts: ['low', 'medium', 'high'],
|
|
390
|
+
effortDefault: defaultModel.match(/-(low|medium|high)$/i)?.[1] || null,
|
|
391
|
+
source,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let cursorModelsCache = null;
|
|
396
|
+
let cursorModelsCachedAt = 0;
|
|
397
|
+
|
|
398
|
+
async function listCursorModels(cliPath) {
|
|
399
|
+
if (cursorModelsCache && Date.now() - cursorModelsCachedAt < 60_000) return cursorModelsCache;
|
|
400
|
+
const output = await new Promise(resolve => {
|
|
401
|
+
execFile(cliPath || 'cursor-agent', ['--list-models'], { timeout: 20_000, maxBuffer: 1024 * 1024 }, (error, stdout) => {
|
|
402
|
+
resolve(error ? '' : String(stdout || ''));
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
const models = output.split(/\r?\n/)
|
|
406
|
+
.map(line => line.trim())
|
|
407
|
+
.map(line => {
|
|
408
|
+
const m = /^([^\s]+)\s+-\s+(.+)$/.exec(line);
|
|
409
|
+
return m ? { value: m[1], label: m[2] } : null;
|
|
410
|
+
})
|
|
411
|
+
.filter(Boolean);
|
|
412
|
+
cursorModelsCache = uniqModels(models);
|
|
413
|
+
cursorModelsCachedAt = Date.now();
|
|
414
|
+
return cursorModelsCache;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function discoverCursor(cursorHome, cliPath) {
|
|
418
|
+
let models = await listCursorModels(cliPath);
|
|
419
|
+
const source = models.length ? 'cursor-agent' : 'fallback';
|
|
420
|
+
if (!models.length) models = FALLBACK_MODELS.cursor.models.map(m => ({ ...m }));
|
|
421
|
+
const defaultModel = models.find(m => m.value === 'auto')?.value || models[0]?.value || FALLBACK_MODELS.cursor.model;
|
|
422
|
+
return { model: defaultModel, models: uniqModels(ensureDefaultInList(models, defaultModel)), efforts: [], source };
|
|
423
|
+
}
|
|
424
|
+
|
|
320
425
|
/**
|
|
321
|
-
* @param {{ claudeConfigDirs?: string[], codexHome?: string, grokHome?: string, agents?: string[] }} opts
|
|
426
|
+
* @param {{ claudeConfigDirs?: string[], codexHome?: string, grokHome?: string, geminiHome?: string, geminiCliPath?: string, cursorHome?: string, cursorAgentPath?: string, agents?: string[] }} opts
|
|
322
427
|
*/
|
|
323
428
|
export async function discoverAllAgentModels(opts = {}) {
|
|
324
|
-
const agents = opts.agents?.length ? opts.agents : ['claude', 'codex', 'grok'];
|
|
429
|
+
const agents = opts.agents?.length ? opts.agents : ['claude', 'codex', 'grok', 'gemini', 'cursor'];
|
|
325
430
|
const want = new Set(agents);
|
|
326
431
|
const defaults = {};
|
|
327
432
|
|
|
@@ -334,6 +439,12 @@ export async function discoverAllAgentModels(opts = {}) {
|
|
|
334
439
|
if (want.has('grok')) {
|
|
335
440
|
defaults.grok = await discoverGrok(opts.grokHome);
|
|
336
441
|
}
|
|
442
|
+
if (want.has('gemini')) {
|
|
443
|
+
defaults.gemini = await discoverGemini(opts.geminiHome, opts.geminiCliPath);
|
|
444
|
+
}
|
|
445
|
+
if (want.has('cursor')) {
|
|
446
|
+
defaults.cursor = await discoverCursor(opts.cursorHome, opts.cursorAgentPath);
|
|
447
|
+
}
|
|
337
448
|
|
|
338
449
|
return defaults;
|
|
339
450
|
}
|