claude-simple 0.2.9 → 0.2.11
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 +143 -139
- package/dist/style.0.2.9.min.css +1 -1
- package/package.json +11 -3
- package/public/chat.js +5 -5
- package/public/files.js +5 -5
- package/public/index.html +6 -7
- 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 +5 -12
- package/public/shell/templates.js +34 -28
- package/public/shell/util.js +3 -1
- package/public/shell.js +54 -13
- package/public/state.js +36 -5
- package/public/style.css +13 -0
- 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/meta-agent.js +9 -5
- package/src/agents/models.js +112 -2
- package/src/agents/sessions.js +303 -14
- package/src/app.js +119 -7
- package/src/mcp.js +275 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
|
|
7
|
+
// Cursor Agent owns its native transcript format. Keep a small UI journal so
|
|
8
|
+
// sessions started here remain searchable/readable without relying on that
|
|
9
|
+
// private format.
|
|
10
|
+
function journalPath(cursorHome, sessionId) {
|
|
11
|
+
return path.join(cursorHome, 'claude-simple-ui', 'sessions', `${sessionId}.jsonl`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function journal(cursorHome, sessionId, value) {
|
|
15
|
+
if (!sessionId) return;
|
|
16
|
+
const file = journalPath(cursorHome, sessionId);
|
|
17
|
+
fs.mkdir(path.dirname(file), { recursive: true })
|
|
18
|
+
.then(() => fs.appendFile(file, `${JSON.stringify(value)}\n`, 'utf8'))
|
|
19
|
+
.catch(() => {});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildArgs(command, options) {
|
|
23
|
+
const args = ['--print', '--output-format', 'stream-json', '--stream-partial-output'];
|
|
24
|
+
if (options.sessionId) args.push('--resume', options.sessionId);
|
|
25
|
+
if (options.model) args.push('--model', options.model);
|
|
26
|
+
if (options.cwd) args.push('--workspace', options.cwd);
|
|
27
|
+
if (options.permissionMode === 'plan') args.push('--mode', 'plan');
|
|
28
|
+
else if (options.permissionMode === 'ask') args.push('--mode', 'ask');
|
|
29
|
+
else if (options.allowDangerouslySkipPermissions || ['bypassPermissions', 'acceptEdits', 'auto'].includes(options.permissionMode)) args.push('--force');
|
|
30
|
+
// The UI has already selected the project directory; do not trigger Cursor's
|
|
31
|
+
// interactive trust prompt in a non-interactive child process.
|
|
32
|
+
args.push('--trust', command || '');
|
|
33
|
+
return args;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function textBlocks(message) {
|
|
37
|
+
const content = message?.content;
|
|
38
|
+
if (typeof content === 'string') return content;
|
|
39
|
+
if (!Array.isArray(content)) return '';
|
|
40
|
+
return content.filter(b => b?.type === 'text' && b.text).map(b => b.text).join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toolBlocks(message) {
|
|
44
|
+
return Array.isArray(message?.content)
|
|
45
|
+
? message.content.filter(b => b?.type === 'tool_use' || b?.type === 'tool-call')
|
|
46
|
+
: [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Run one Cursor Agent turn in headless stream-json mode. */
|
|
50
|
+
export function runCursor(command, options, send, { cliPath, cursorHome, activeSessions }) {
|
|
51
|
+
return new Promise(resolve => {
|
|
52
|
+
const sessionKey = options.sessionId || crypto.randomUUID();
|
|
53
|
+
const home = cursorHome || path.join(process.env.HOME || '', '.cursor');
|
|
54
|
+
const args = buildArgs(command, options);
|
|
55
|
+
let child;
|
|
56
|
+
try {
|
|
57
|
+
child = spawn(cliPath || 'cursor-agent', args, {
|
|
58
|
+
cwd: options.cwd || process.cwd(), env: { ...process.env }, stdio: ['ignore', 'pipe', 'pipe'],
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
send({ type: 'error', message: `Failed to start cursor-agent: ${error.message}`, agent: 'cursor' });
|
|
62
|
+
resolve();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const entry = { kind: 'cursor', child, startTime: Date.now(), busy: true, key: sessionKey, sessionId: options.sessionId || null };
|
|
67
|
+
activeSessions.set(sessionKey, entry);
|
|
68
|
+
let sessionId = options.sessionId || null;
|
|
69
|
+
let stderr = '';
|
|
70
|
+
let textOpen = false;
|
|
71
|
+
let resultSeen = false;
|
|
72
|
+
let aborted = false;
|
|
73
|
+
let finalText = '';
|
|
74
|
+
let thoughtOpen = false;
|
|
75
|
+
const emittedTexts = new Set();
|
|
76
|
+
|
|
77
|
+
const closeText = () => {
|
|
78
|
+
if (!textOpen) return;
|
|
79
|
+
textOpen = false;
|
|
80
|
+
send({ type: 'assistant_stream_end', agent: 'cursor' });
|
|
81
|
+
};
|
|
82
|
+
const attachSession = id => {
|
|
83
|
+
if (!id || sessionId === id) return;
|
|
84
|
+
sessionId = id;
|
|
85
|
+
entry.sessionId = id;
|
|
86
|
+
activeSessions.set(id, entry);
|
|
87
|
+
if (!options.sessionId) send({ type: 'session-created', sessionId: id, agent: 'cursor' });
|
|
88
|
+
journal(home, id, { type: 'session_meta', sessionId: id, cwd: options.cwd || null, model: options.model || null, ts: Date.now() });
|
|
89
|
+
journal(home, id, { type: 'user', content: command || '', ts: Date.now() });
|
|
90
|
+
};
|
|
91
|
+
if (options.sessionId) {
|
|
92
|
+
journal(home, options.sessionId, { type: 'session_meta', sessionId: options.sessionId, cwd: options.cwd || null, model: options.model || null, ts: Date.now() });
|
|
93
|
+
journal(home, options.sessionId, { type: 'user', content: command || '', ts: Date.now() });
|
|
94
|
+
}
|
|
95
|
+
const emitDelta = text => {
|
|
96
|
+
if (!text) return;
|
|
97
|
+
if (!textOpen) {
|
|
98
|
+
textOpen = true;
|
|
99
|
+
send({ type: 'assistant_stream_start', agent: 'cursor', ts: Date.now() });
|
|
100
|
+
}
|
|
101
|
+
send({ type: 'assistant_delta', data: text, agent: 'cursor' });
|
|
102
|
+
journal(home, sessionId, { type: 'assistant_delta', data: text, ts: Date.now() });
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
child.stderr.on('data', d => { stderr += String(d); });
|
|
106
|
+
const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
107
|
+
rl.on('line', line => {
|
|
108
|
+
if (!line.trim()) return;
|
|
109
|
+
let raw;
|
|
110
|
+
try { raw = JSON.parse(line); } catch { return; }
|
|
111
|
+
attachSession(raw.session_id || raw.sessionId);
|
|
112
|
+
if (raw.type === 'thinking' && raw.subtype === 'delta') {
|
|
113
|
+
if (!thoughtOpen) {
|
|
114
|
+
thoughtOpen = true;
|
|
115
|
+
send({ type: 'thought_stream_start', agent: 'cursor', ts: Date.now() });
|
|
116
|
+
}
|
|
117
|
+
send({ type: 'thought_delta', data: raw.text || '', agent: 'cursor' });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (raw.type === 'thinking' && raw.subtype === 'completed' && thoughtOpen) {
|
|
121
|
+
thoughtOpen = false;
|
|
122
|
+
send({ type: 'thought_stream_end', agent: 'cursor' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (raw.type === 'assistant') {
|
|
126
|
+
const text = textBlocks(raw.message);
|
|
127
|
+
for (const tool of toolBlocks(raw.message)) {
|
|
128
|
+
closeText();
|
|
129
|
+
const input = tool.input ?? tool.args ?? {};
|
|
130
|
+
send({ type: 'assistant', message: { content: [{ type: 'tool_use', name: tool.name || tool.toolName || 'tool', input }] }, agent: 'cursor' });
|
|
131
|
+
journal(home, sessionId, { type: 'tool', name: tool.name || tool.toolName || 'tool', input, ts: Date.now() });
|
|
132
|
+
}
|
|
133
|
+
// Cursor emits a final assistant message twice in stream-json mode.
|
|
134
|
+
// Deltas are authoritative; only emit a complete message if no delta
|
|
135
|
+
// was received for this exact response.
|
|
136
|
+
if (text && !emittedTexts.has(text)) {
|
|
137
|
+
emittedTexts.add(text);
|
|
138
|
+
if (!textOpen) emitDelta(text);
|
|
139
|
+
finalText = text;
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (raw.type === 'result') {
|
|
144
|
+
closeText();
|
|
145
|
+
resultSeen = true;
|
|
146
|
+
if (raw.is_error || raw.subtype === 'error') {
|
|
147
|
+
send({ type: 'error', message: raw.result || raw.error || 'Cursor Agent turn failed', agent: 'cursor' });
|
|
148
|
+
} else {
|
|
149
|
+
const u = raw.usage || {};
|
|
150
|
+
const usage = {
|
|
151
|
+
input_tokens: u.inputTokens ?? u.input_tokens ?? 0,
|
|
152
|
+
output_tokens: u.outputTokens ?? u.output_tokens ?? 0,
|
|
153
|
+
cache_read_input_tokens: u.cacheReadTokens ?? u.cache_read_tokens ?? 0,
|
|
154
|
+
cache_write_input_tokens: u.cacheWriteTokens ?? u.cache_write_tokens ?? 0,
|
|
155
|
+
};
|
|
156
|
+
if (raw.result && !finalText) emitDelta(String(raw.result));
|
|
157
|
+
journal(home, sessionId, { type: 'result', usage, ts: Date.now() });
|
|
158
|
+
send({ type: 'result', is_error: false, usage, sessionId: sessionId || sessionKey, agent: 'cursor' });
|
|
159
|
+
}
|
|
160
|
+
} else if (raw.type === 'error') {
|
|
161
|
+
send({ type: 'error', message: raw.error?.message || raw.message || 'Cursor Agent error', agent: 'cursor' });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
child.on('close', code => {
|
|
166
|
+
rl.close(); closeText();
|
|
167
|
+
if (thoughtOpen) send({ type: 'thought_stream_end', agent: 'cursor' });
|
|
168
|
+
activeSessions.delete(sessionKey);
|
|
169
|
+
if (sessionId) activeSessions.delete(sessionId);
|
|
170
|
+
if (aborted) send({ type: 'complete', exitCode: 130, aborted: true, sessionId: sessionId || sessionKey, agent: 'cursor' });
|
|
171
|
+
else {
|
|
172
|
+
if (code && !resultSeen) send({ type: 'error', message: stderr.trim() || `cursor-agent exited with code ${code}`, agent: 'cursor' });
|
|
173
|
+
send({ type: 'complete', exitCode: code || 0, sessionId: sessionId || sessionKey, agent: 'cursor' });
|
|
174
|
+
}
|
|
175
|
+
resolve();
|
|
176
|
+
});
|
|
177
|
+
child.on('error', error => {
|
|
178
|
+
activeSessions.delete(sessionKey);
|
|
179
|
+
if (sessionId) activeSessions.delete(sessionId);
|
|
180
|
+
send({ type: 'error', message: error.message, agent: 'cursor' });
|
|
181
|
+
resolve();
|
|
182
|
+
});
|
|
183
|
+
entry.abort = () => {
|
|
184
|
+
aborted = true;
|
|
185
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
186
|
+
setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref?.();
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
|
|
7
|
+
// agy (the Antigravity/Gemini CLI) exposes its print mode as NDJSON. Keeping a
|
|
8
|
+
// small, UI-owned journal alongside its native SQLite conversation store gives
|
|
9
|
+
// the web UI reliable history/search support without depending on the CLI's
|
|
10
|
+
// private protobuf schema.
|
|
11
|
+
function journalPath(geminiHome, sessionId) {
|
|
12
|
+
return path.join(geminiHome, 'claude-simple-ui', 'sessions', `${sessionId}.jsonl`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function journal(geminiHome, sessionId, value) {
|
|
16
|
+
if (!sessionId) return;
|
|
17
|
+
const file = journalPath(geminiHome, sessionId);
|
|
18
|
+
fs.mkdir(path.dirname(file), { recursive: true })
|
|
19
|
+
.then(() => fs.appendFile(file, `${JSON.stringify(value)}\n`, 'utf8'))
|
|
20
|
+
.catch(() => {});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isAutoApprove(options) {
|
|
24
|
+
return options.allowDangerouslySkipPermissions
|
|
25
|
+
|| ['bypassPermissions', 'acceptEdits', 'auto'].includes(options.permissionMode);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildArgs(command, options) {
|
|
29
|
+
const args = ['--output-format', 'stream-json'];
|
|
30
|
+
if (options.sessionId) args.push('--conversation', options.sessionId);
|
|
31
|
+
if (options.model) args.push('--model', options.model);
|
|
32
|
+
if (options.effort && ['low', 'medium', 'high'].includes(options.effort)) {
|
|
33
|
+
args.push('--effort', options.effort);
|
|
34
|
+
}
|
|
35
|
+
// In print mode agy otherwise starts command tools in its scratch directory.
|
|
36
|
+
// Adding the requested project preserves normal agent access to the workspace.
|
|
37
|
+
if (options.cwd) args.push('--add-dir', options.cwd);
|
|
38
|
+
if (isAutoApprove(options)) args.push('--dangerously-skip-permissions');
|
|
39
|
+
else if (options.permissionMode === 'plan') args.push('--mode', 'plan');
|
|
40
|
+
else args.push('--mode', 'accept-edits');
|
|
41
|
+
args.push(`--prompt=${command || ''}`);
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toolInput(update) {
|
|
46
|
+
const info = update.tool_info || update.toolInfo || {};
|
|
47
|
+
return info.parameters || info.input || info.args || {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Run a Gemini/Antigravity turn through `agy` print mode. */
|
|
51
|
+
export function runGemini(command, options, send, { cliPath, geminiHome, activeSessions }) {
|
|
52
|
+
return new Promise(resolve => {
|
|
53
|
+
const sessionKey = options.sessionId || crypto.randomUUID();
|
|
54
|
+
const home = geminiHome || path.join(process.env.HOME || '', '.gemini');
|
|
55
|
+
const args = buildArgs(command, options);
|
|
56
|
+
// A resumed native conversation may predate the UI journal. Start recording
|
|
57
|
+
// from this turn so its subsequent history/search/usage is still complete.
|
|
58
|
+
if (options.sessionId) {
|
|
59
|
+
journal(home, options.sessionId, {
|
|
60
|
+
type: 'session_meta', sessionId: options.sessionId, cwd: options.cwd || null,
|
|
61
|
+
model: options.model || null, ts: Date.now(),
|
|
62
|
+
});
|
|
63
|
+
journal(home, options.sessionId, { type: 'user', content: command || '', ts: Date.now() });
|
|
64
|
+
}
|
|
65
|
+
let child;
|
|
66
|
+
try {
|
|
67
|
+
child = spawn(cliPath || 'agy', args, {
|
|
68
|
+
cwd: options.cwd || process.cwd(),
|
|
69
|
+
env: { ...process.env },
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
send({ type: 'error', message: `Failed to start agy: ${error.message}`, agent: 'gemini' });
|
|
74
|
+
resolve();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const entry = {
|
|
79
|
+
kind: 'gemini', child, startTime: Date.now(), busy: true,
|
|
80
|
+
key: sessionKey, sessionId: options.sessionId || null,
|
|
81
|
+
};
|
|
82
|
+
activeSessions.set(sessionKey, entry);
|
|
83
|
+
let sessionId = options.sessionId || null;
|
|
84
|
+
let stderr = '';
|
|
85
|
+
let aborted = false;
|
|
86
|
+
let textOpen = false;
|
|
87
|
+
let resultSeen = false;
|
|
88
|
+
|
|
89
|
+
const closeText = () => {
|
|
90
|
+
if (!textOpen) return;
|
|
91
|
+
textOpen = false;
|
|
92
|
+
send({ type: 'assistant_stream_end', agent: 'gemini' });
|
|
93
|
+
};
|
|
94
|
+
const attachSession = id => {
|
|
95
|
+
if (!id || sessionId === id) return;
|
|
96
|
+
sessionId = id;
|
|
97
|
+
entry.sessionId = id;
|
|
98
|
+
activeSessions.set(id, entry);
|
|
99
|
+
if (!options.sessionId) send({ type: 'session-created', sessionId: id, agent: 'gemini' });
|
|
100
|
+
journal(home, id, {
|
|
101
|
+
type: 'session_meta', sessionId: id, cwd: options.cwd || null,
|
|
102
|
+
model: options.model || null, ts: Date.now(),
|
|
103
|
+
});
|
|
104
|
+
journal(home, id, { type: 'user', content: command || '', ts: Date.now() });
|
|
105
|
+
};
|
|
106
|
+
const usage = raw => ({
|
|
107
|
+
input_tokens: raw?.input_tokens ?? 0,
|
|
108
|
+
output_tokens: raw?.output_tokens ?? 0,
|
|
109
|
+
reasoning_tokens: raw?.thinking_tokens ?? 0,
|
|
110
|
+
cache_read_input_tokens: raw?.cache_read_tokens ?? 0,
|
|
111
|
+
total_tokens: raw?.total_tokens ?? 0,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
child.stderr.on('data', d => { stderr += String(d); });
|
|
115
|
+
const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
116
|
+
rl.on('line', line => {
|
|
117
|
+
if (!line.trim()) return;
|
|
118
|
+
let raw;
|
|
119
|
+
try { raw = JSON.parse(line); } catch { return; }
|
|
120
|
+
const id = raw.conversation_id || raw.result?.conversation_id || raw.step_update?.conversation_id;
|
|
121
|
+
attachSession(id);
|
|
122
|
+
|
|
123
|
+
if (raw.event === 'step_update') {
|
|
124
|
+
const update = raw.step_update || {};
|
|
125
|
+
if (update.step_type === 'agent_response' && update.text_delta) {
|
|
126
|
+
if (!textOpen) {
|
|
127
|
+
textOpen = true;
|
|
128
|
+
send({ type: 'assistant_stream_start', agent: 'gemini', ts: Date.now() });
|
|
129
|
+
}
|
|
130
|
+
send({ type: 'assistant_delta', data: update.text_delta, agent: 'gemini' });
|
|
131
|
+
journal(home, sessionId, { type: 'assistant_delta', data: update.text_delta, ts: Date.now() });
|
|
132
|
+
}
|
|
133
|
+
if (update.step_type === 'tool' && update.state === 'ACTIVE') {
|
|
134
|
+
closeText();
|
|
135
|
+
const name = update.tool_name || update.toolName || update.tool_info?.name || 'tool';
|
|
136
|
+
const input = toolInput(update);
|
|
137
|
+
send({ type: 'assistant', message: { content: [{ type: 'tool_use', name, input }] }, agent: 'gemini' });
|
|
138
|
+
journal(home, sessionId, { type: 'tool', name, input, ts: Date.now() });
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (raw.event === 'result') {
|
|
143
|
+
closeText();
|
|
144
|
+
resultSeen = true;
|
|
145
|
+
const result = raw.result || {};
|
|
146
|
+
const u = usage(result.usage);
|
|
147
|
+
journal(home, sessionId, { type: 'result', usage: u, ts: Date.now() });
|
|
148
|
+
if (String(result.status || '').toUpperCase() !== 'SUCCESS') {
|
|
149
|
+
send({ type: 'error', message: result.error || result.response || `Gemini turn ${result.status || 'failed'}`, agent: 'gemini' });
|
|
150
|
+
} else {
|
|
151
|
+
send({ type: 'result', is_error: false, usage: u, sessionId: sessionId || sessionKey, agent: 'gemini' });
|
|
152
|
+
}
|
|
153
|
+
} else if (raw.event === 'error') {
|
|
154
|
+
send({ type: 'error', message: raw.error?.message || raw.message || 'Gemini CLI error', agent: 'gemini' });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
child.on('close', code => {
|
|
159
|
+
rl.close();
|
|
160
|
+
closeText();
|
|
161
|
+
activeSessions.delete(sessionKey);
|
|
162
|
+
if (sessionId) activeSessions.delete(sessionId);
|
|
163
|
+
if (aborted) send({ type: 'complete', exitCode: 130, aborted: true, sessionId: sessionId || sessionKey, agent: 'gemini' });
|
|
164
|
+
else {
|
|
165
|
+
if (code && !resultSeen) send({ type: 'error', message: stderr.trim() || `agy exited with code ${code}`, agent: 'gemini' });
|
|
166
|
+
send({ type: 'complete', exitCode: code || 0, sessionId: sessionId || sessionKey, agent: 'gemini' });
|
|
167
|
+
}
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
child.on('error', error => {
|
|
171
|
+
activeSessions.delete(sessionKey);
|
|
172
|
+
if (sessionId) activeSessions.delete(sessionId);
|
|
173
|
+
send({ type: 'error', message: error.message, agent: 'gemini' });
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
entry.abort = () => {
|
|
177
|
+
aborted = true;
|
|
178
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
179
|
+
setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref?.();
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
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'];
|
|
@@ -318,11 +344,89 @@ async function discoverGrok(grokHome) {
|
|
|
318
344
|
};
|
|
319
345
|
}
|
|
320
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
|
+
|
|
321
425
|
/**
|
|
322
|
-
* @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
|
|
323
427
|
*/
|
|
324
428
|
export async function discoverAllAgentModels(opts = {}) {
|
|
325
|
-
const agents = opts.agents?.length ? opts.agents : ['claude', 'codex', 'grok'];
|
|
429
|
+
const agents = opts.agents?.length ? opts.agents : ['claude', 'codex', 'grok', 'gemini', 'cursor'];
|
|
326
430
|
const want = new Set(agents);
|
|
327
431
|
const defaults = {};
|
|
328
432
|
|
|
@@ -335,6 +439,12 @@ export async function discoverAllAgentModels(opts = {}) {
|
|
|
335
439
|
if (want.has('grok')) {
|
|
336
440
|
defaults.grok = await discoverGrok(opts.grokHome);
|
|
337
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
|
+
}
|
|
338
448
|
|
|
339
449
|
return defaults;
|
|
340
450
|
}
|