claude-simple 0.2.7 → 0.2.9
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 +0 -0
- package/dist/app.0.2.9.min.js +1080 -0
- package/dist/index.html +2 -2
- package/dist/{style.0.2.7.min.css → style.0.2.9.min.css} +1 -1
- package/gateway.js +0 -0
- package/package.json +1 -1
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +0 -0
- package/public/api.js +2 -0
- package/public/app.js +0 -0
- package/public/base.css +0 -0
- package/public/cache.js +0 -0
- package/public/chat.js +42 -8
- package/public/files.js +0 -0
- package/public/git.js +0 -0
- package/public/icons/icon-180.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/index.html +0 -0
- package/public/lib.js +0 -0
- package/public/manifest.webmanifest +0 -0
- package/public/mobile.js +0 -0
- package/public/notifications.js +215 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +0 -0
- package/public/router.js +0 -0
- package/public/settings.js +0 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +46 -26
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +2 -3
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +22 -18
- package/public/shell/session-nav.js +18 -1
- package/public/shell/templates.js +18 -0
- package/public/shell/util.js +0 -0
- package/public/shell.js +24 -6
- package/public/state.js +64 -7
- package/public/style.css +0 -0
- package/public/sw.js +1 -1
- package/public/terminal.js +0 -0
- package/public/wake-lock.js +0 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agents/claude.js +0 -0
- package/src/agents/codex.js +0 -0
- package/src/agents/grok.js +415 -271
- package/src/agents/meta-agent.js +0 -0
- package/src/agents/models.js +2 -1
- package/src/agents/sessions.js +0 -0
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +12 -2
- package/src/auto-update.js +0 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/protocol.js +0 -0
- package/dist/app.0.2.7.min.js +0 -1039
package/src/agents/grok.js
CHANGED
|
@@ -1,327 +1,471 @@
|
|
|
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
|
-
import { createDeltaBatcher } from './stream-batch.js';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* Resume:
|
|
12
|
-
* grok --resume <sessionId> -p <prompt> --output-format streaming-json ...
|
|
13
|
-
*
|
|
14
|
-
* Stream events (line-delimited JSON):
|
|
15
|
-
* { type: "thought", data: "..." }
|
|
16
|
-
* { type: "text", data: "..." }
|
|
17
|
-
* { type: "tool_*", ... } // when CLI emits them
|
|
18
|
-
* { type: "end", sessionId, usage, ... }
|
|
19
|
-
*
|
|
20
|
-
* We forward deltas immediately so the UI updates live instead of dumping at end.
|
|
21
|
-
*/
|
|
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.
|
|
9
|
+
const clients = new Map();
|
|
10
|
+
const queues = new Map();
|
|
11
|
+
const queueAliases = new Map();
|
|
22
12
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
if (permissionMode == null) return 'bypassPermissions';
|
|
26
|
-
if (permissionMode === 'default') return null;
|
|
27
|
-
return permissionMode;
|
|
13
|
+
function profileKey(options) {
|
|
14
|
+
return `${options.model || ''}\u001f${options.effort || ''}`;
|
|
28
15
|
}
|
|
29
16
|
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// -p = headless single-turn (required for non-TTY streaming-json)
|
|
38
|
-
args.push('-p', command || '');
|
|
39
|
-
args.push('--output-format', 'streaming-json');
|
|
40
|
-
|
|
41
|
-
if (options.cwd) args.push('--cwd', options.cwd);
|
|
42
|
-
if (options.model) args.push('--model', options.model);
|
|
43
|
-
if (options.effort) args.push('--reasoning-effort', options.effort);
|
|
44
|
-
|
|
45
|
-
const perm = mapPermission(options.permissionMode, options.allowDangerouslySkipPermissions);
|
|
46
|
-
if (perm) args.push('--permission-mode', perm);
|
|
47
|
-
if (options.allowDangerouslySkipPermissions || options.permissionMode === 'bypassPermissions') {
|
|
48
|
-
args.push('--always-approve');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (options.newSessionId && !options.sessionId) {
|
|
52
|
-
args.push('--session-id', options.newSessionId);
|
|
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);
|
|
53
23
|
}
|
|
54
|
-
|
|
55
|
-
return args;
|
|
24
|
+
return env;
|
|
56
25
|
}
|
|
57
26
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (typeof v === 'string') {
|
|
62
|
-
try { return JSON.parse(v); } catch { return { input: v }; }
|
|
27
|
+
class TerminalHost {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.terminals = new Map();
|
|
63
30
|
}
|
|
64
|
-
return { value: v };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function runGrok(command, options, send, { cliPath, activeSessions }) {
|
|
68
|
-
return new Promise((resolve) => {
|
|
69
|
-
const sessionKey = options.sessionId || crypto.randomUUID();
|
|
70
|
-
const bin = cliPath || 'grok';
|
|
71
|
-
const args = buildArgs(command, options);
|
|
72
31
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
77
41
|
|
|
78
42
|
let child;
|
|
79
43
|
try {
|
|
80
|
-
child = spawn(
|
|
81
|
-
cwd
|
|
44
|
+
child = spawn(command, args, {
|
|
45
|
+
cwd,
|
|
82
46
|
env,
|
|
83
47
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
shell: args.length === 0,
|
|
84
49
|
});
|
|
85
|
-
} catch (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const err = new Error(error.message || 'Failed to start terminal');
|
|
52
|
+
err.code = -32000;
|
|
53
|
+
throw err;
|
|
89
54
|
}
|
|
90
55
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
let thoughtOpen = false;
|
|
103
|
-
let textChars = 0;
|
|
104
|
-
let thoughtChars = 0;
|
|
105
|
-
|
|
106
|
-
const textBatch = createDeltaBatcher(send, 'assistant_delta', { agent: 'grok' }, { maxWait: 48, maxSize: 480 });
|
|
107
|
-
const thoughtBatch = createDeltaBatcher(send, 'thought_delta', { agent: 'grok' }, { maxWait: 140, maxSize: 600 });
|
|
108
|
-
|
|
109
|
-
child.stderr.on('data', d => { stderr += d.toString(); });
|
|
110
|
-
|
|
111
|
-
// Use 'readable' + line split for lower latency than waiting for large chunks
|
|
112
|
-
const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
113
|
-
|
|
114
|
-
function ensureStream() {
|
|
115
|
-
if (!streamOpen) {
|
|
116
|
-
send({ type: 'assistant_stream_start', agent: 'grok', ts: Date.now() });
|
|
117
|
-
streamOpen = true;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
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
|
+
};
|
|
120
67
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
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;
|
|
126
77
|
}
|
|
127
|
-
}
|
|
78
|
+
};
|
|
128
79
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 = [];
|
|
134
92
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
});
|
|
139
101
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (thoughtOpen) {
|
|
144
|
-
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
145
|
-
thoughtOpen = false;
|
|
146
|
-
}
|
|
147
|
-
endStream();
|
|
102
|
+
this.terminals.set(terminalId, entry);
|
|
103
|
+
return { terminalId };
|
|
104
|
+
}
|
|
148
105
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
115
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 };
|
|
161
124
|
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
162
127
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
send({ type: 'status', message: line.slice(0, 200), agent: 'grok' });
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
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
|
+
}
|
|
171
133
|
|
|
172
|
-
|
|
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
|
+
}
|
|
173
141
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 */ }
|
|
180
150
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
151
|
+
}, 800).unref?.();
|
|
152
|
+
}
|
|
153
|
+
this.terminals.delete(terminalId);
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
188
157
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
158
|
+
class GrokACPClient {
|
|
159
|
+
constructor({ cliPath, model, effort }) {
|
|
160
|
+
const args = ['agent', '--always-approve'];
|
|
161
|
+
if (model) args.push('--model', model);
|
|
162
|
+
if (effort) args.push('--reasoning-effort', effort);
|
|
163
|
+
args.push('stdio');
|
|
164
|
+
this.child = spawn(cliPath || 'grok', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
165
|
+
this.pending = new Map();
|
|
166
|
+
this.listeners = new Map();
|
|
167
|
+
// High ids avoid colliding with agent-originated request ids (often 0..N).
|
|
168
|
+
this.nextID = 100000;
|
|
169
|
+
this.terminals = new TerminalHost();
|
|
170
|
+
this.ready = this.initialize();
|
|
171
|
+
this.child.stderr.on('data', d => console.warn(`[grok acp] ${String(d).trim()}`));
|
|
172
|
+
this.child.on('close', () => {
|
|
173
|
+
for (const { reject } of this.pending.values()) reject(new Error('Grok ACP process stopped'));
|
|
174
|
+
this.pending.clear();
|
|
175
|
+
for (const fn of this.listeners.values()) {
|
|
176
|
+
fn({ type: 'error', message: 'Grok ACP process stopped', agent: 'grok' });
|
|
197
177
|
}
|
|
178
|
+
this.listeners.clear();
|
|
179
|
+
});
|
|
180
|
+
const rl = readline.createInterface({ input: this.child.stdout, crlfDelay: Infinity });
|
|
181
|
+
rl.on('line', line => this.receive(line));
|
|
182
|
+
}
|
|
198
183
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
184
|
+
async initialize() {
|
|
185
|
+
const init = await this.request('initialize', {
|
|
186
|
+
protocolVersion: 1,
|
|
187
|
+
clientCapabilities: {
|
|
188
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
189
|
+
terminal: true,
|
|
190
|
+
},
|
|
191
|
+
clientInfo: { name: 'claude-simple-ui', version: '0.2.9' },
|
|
192
|
+
});
|
|
193
|
+
const methods = new Set((init.authMethods || []).map(x => x?.id));
|
|
194
|
+
const methodId = process.env.XAI_API_KEY && methods.has('xai.api_key')
|
|
195
|
+
? 'xai.api_key'
|
|
196
|
+
: (methods.has('cached_token') ? 'cached_token' : null);
|
|
197
|
+
if (methodId) await this.request('authenticate', { methodId, _meta: { headless: true } });
|
|
198
|
+
return init;
|
|
199
|
+
}
|
|
212
200
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 };
|
|
220
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
|
+
});
|
|
221
240
|
return;
|
|
222
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
|
+
}
|
|
250
|
+
}
|
|
223
251
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
flushThought();
|
|
228
|
-
if (thoughtOpen) {
|
|
229
|
-
send({ type: 'thought_stream_end', agent: 'grok' });
|
|
230
|
-
thoughtOpen = false;
|
|
231
|
-
}
|
|
232
|
-
endStream();
|
|
252
|
+
receive(line) {
|
|
253
|
+
let msg;
|
|
254
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
233
255
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
}
|
|
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
|
+
}
|
|
242
262
|
|
|
243
|
-
|
|
244
|
-
|
|
263
|
+
if (msg.id != null && this.pending.has(msg.id)) {
|
|
264
|
+
const pending = this.pending.get(msg.id);
|
|
265
|
+
this.pending.delete(msg.id);
|
|
266
|
+
if (msg.error) pending.reject(new Error(msg.error.message || 'Grok ACP request failed'));
|
|
267
|
+
else pending.resolve(msg.result || {});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
245
270
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
if (msg.method === 'session/update') {
|
|
272
|
+
const sid = msg.params?.sessionId;
|
|
273
|
+
const fn = sid && this.listeners.get(sid);
|
|
274
|
+
if (fn) fn(msg.params?.update || {});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
250
277
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
usage: {
|
|
256
|
-
inputTokens: u.input_tokens || u.inputTokens || 0,
|
|
257
|
-
outputTokens: u.output_tokens || u.outputTokens || 0,
|
|
258
|
-
cacheReadInputTokens: u.cache_read_input_tokens || u.cachedReadTokens || 0,
|
|
259
|
-
totalTokens: u.total_tokens || u.totalTokens
|
|
260
|
-
|| ((u.input_tokens || u.inputTokens || 0) + (u.output_tokens || u.outputTokens || 0)),
|
|
261
|
-
reasoningTokens: u.reasoning_tokens || u.reasoningTokens || 0,
|
|
262
|
-
costUSD,
|
|
263
|
-
},
|
|
264
|
-
sessionId: sid,
|
|
265
|
-
agent: 'grok',
|
|
266
|
-
stats: { textChars, thoughtChars },
|
|
267
|
-
});
|
|
268
|
-
// Early complete so client can send next turn without waiting for process exit.
|
|
269
|
-
send({ type: 'complete', exitCode: 0, sessionId: sid, agent: 'grok' });
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
278
|
+
write(data) {
|
|
279
|
+
if (!this.child.stdin?.writable) throw new Error('Grok ACP stdin is unavailable');
|
|
280
|
+
this.child.stdin.write(JSON.stringify(data) + '\n');
|
|
281
|
+
}
|
|
272
282
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
283
|
+
request(method, params) {
|
|
284
|
+
const id = this.nextID++;
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
this.pending.set(id, { resolve, reject });
|
|
287
|
+
try { this.write({ jsonrpc: '2.0', id, method, params }); }
|
|
288
|
+
catch (e) { this.pending.delete(id); reject(e); }
|
|
277
289
|
});
|
|
290
|
+
}
|
|
278
291
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (sessionId) activeSessions.delete(sessionId);
|
|
287
|
-
|
|
288
|
-
// Skip duplicate complete if turn already finished via streaming-json "end".
|
|
289
|
-
if (gotEnd && !aborted) {
|
|
290
|
-
resolve();
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (aborted) {
|
|
295
|
-
send({ type: 'complete', exitCode: 130, aborted: true, sessionId: sessionId || sessionKey, agent: 'grok' });
|
|
296
|
-
} else if (code !== 0 && code != null && !gotEnd) {
|
|
297
|
-
const errMsg = stderr.trim().split('\n').filter(l => !/stdin is not a terminal/i.test(l)).join('\n')
|
|
298
|
-
|| `grok exited with code ${code}`;
|
|
299
|
-
send({ type: 'error', message: errMsg, agent: 'grok' });
|
|
300
|
-
send({ type: 'complete', exitCode: code, sessionId: sessionId || sessionKey, agent: 'grok' });
|
|
301
|
-
} else {
|
|
302
|
-
if (sessionId && !options.sessionId && !gotEnd) {
|
|
303
|
-
send({ type: 'session-created', sessionId, agent: 'grok' });
|
|
304
|
-
}
|
|
305
|
-
send({ type: 'complete', exitCode: code ?? 0, sessionId: sessionId || sessionKey, agent: 'grok' });
|
|
292
|
+
async ensureSession(options) {
|
|
293
|
+
await this.ready;
|
|
294
|
+
if (options.sessionId) {
|
|
295
|
+
try {
|
|
296
|
+
await this.request('session/load', { sessionId: options.sessionId, cwd: options.cwd, mcpServers: [] });
|
|
297
|
+
} catch {
|
|
298
|
+
await this.request('session/resume', { sessionId: options.sessionId, cwd: options.cwd, mcpServers: [] });
|
|
306
299
|
}
|
|
307
|
-
|
|
300
|
+
return options.sessionId;
|
|
301
|
+
}
|
|
302
|
+
const result = await this.request('session/new', {
|
|
303
|
+
cwd: options.cwd || process.cwd(),
|
|
304
|
+
mcpServers: [],
|
|
305
|
+
_meta: { yoloMode: true },
|
|
308
306
|
});
|
|
307
|
+
if (!result.sessionId) throw new Error('Grok ACP did not return a session id');
|
|
308
|
+
return result.sessionId;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
309
311
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
312
|
+
function clientFor(options, cliPath) {
|
|
313
|
+
const key = profileKey(options);
|
|
314
|
+
let client = clients.get(key);
|
|
315
|
+
if (!client || client.child.exitCode != null || client.child.signalCode != null) {
|
|
316
|
+
client = new GrokACPClient({ cliPath, model: options.model, effort: options.effort });
|
|
317
|
+
clients.set(key, client);
|
|
318
|
+
}
|
|
319
|
+
return client;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function emitUpdate(update, send, state) {
|
|
323
|
+
const kind = update?.sessionUpdate;
|
|
324
|
+
if (kind === 'agent_message_chunk' && update.content?.text) {
|
|
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
|
+
}
|
|
333
|
+
send({ type: 'assistant_delta', data: update.content.text, agent: 'grok' });
|
|
334
|
+
} else if (kind === 'agent_thought_chunk' && update.content?.text) {
|
|
335
|
+
if (!state.thoughtOpen) {
|
|
336
|
+
send({ type: 'thought_stream_start', agent: 'grok' });
|
|
337
|
+
state.thoughtOpen = true;
|
|
338
|
+
}
|
|
339
|
+
send({ type: 'thought_delta', data: update.content.text, agent: 'grok' });
|
|
340
|
+
} else if (kind === 'tool_call' || kind === 'tool_call_update') {
|
|
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
|
+
}
|
|
349
|
+
if (kind === 'tool_call') {
|
|
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
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
317
364
|
|
|
365
|
+
async function runOne(item, { cliPath, activeSessions, queueKey }) {
|
|
366
|
+
const { command, options, send } = item;
|
|
367
|
+
const effort = options.effort || 'low';
|
|
368
|
+
const client = clientFor({ ...options, effort }, cliPath);
|
|
369
|
+
const localKey = options.sessionId || crypto.randomUUID();
|
|
370
|
+
const entry = {
|
|
371
|
+
kind: 'grok-acp',
|
|
372
|
+
startTime: Date.now(),
|
|
373
|
+
busy: true,
|
|
374
|
+
key: localKey,
|
|
375
|
+
sessionId: options.sessionId || null,
|
|
376
|
+
};
|
|
377
|
+
activeSessions.set(localKey, entry);
|
|
378
|
+
const state = { textOpen: false, thoughtOpen: false };
|
|
379
|
+
let sessionId = options.sessionId;
|
|
380
|
+
try {
|
|
381
|
+
sessionId = await client.ensureSession(options);
|
|
382
|
+
queueAliases.set(sessionId, queueKey);
|
|
383
|
+
entry.sessionId = sessionId;
|
|
384
|
+
activeSessions.set(sessionId, entry);
|
|
318
385
|
entry.abort = () => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
386
|
+
try {
|
|
387
|
+
client.write({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } });
|
|
388
|
+
} catch { /* process may already be gone */ }
|
|
322
389
|
};
|
|
390
|
+
if (!options.sessionId) send({ type: 'session-created', sessionId, agent: 'grok' });
|
|
391
|
+
client.listeners.set(sessionId, update => emitUpdate(update, send, state));
|
|
392
|
+
send({ type: 'status', message: 'Grok ACP running…', agent: 'grok', sessionId });
|
|
393
|
+
const result = await client.request('session/prompt', {
|
|
394
|
+
sessionId,
|
|
395
|
+
prompt: [{ type: 'text', text: command }],
|
|
396
|
+
});
|
|
397
|
+
if (state.textOpen) send({ type: 'assistant_stream_end', agent: 'grok' });
|
|
398
|
+
if (state.thoughtOpen) send({ type: 'thought_stream_end', agent: 'grok' });
|
|
399
|
+
entry.busy = false;
|
|
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
|
+
});
|
|
414
|
+
} catch (e) {
|
|
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' });
|
|
418
|
+
send({ type: 'error', message: e.message, sessionId, agent: 'grok' });
|
|
419
|
+
send({ type: 'complete', exitCode: 1, sessionId, agent: 'grok' });
|
|
420
|
+
} finally {
|
|
421
|
+
if (sessionId) client.listeners.delete(sessionId);
|
|
422
|
+
activeSessions.delete(localKey);
|
|
423
|
+
if (sessionId) activeSessions.delete(sessionId);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
323
426
|
|
|
324
|
-
|
|
325
|
-
|
|
427
|
+
/** Queue all Grok follow-ups at the edge. Promise resolves after this item runs. */
|
|
428
|
+
export function runGrok(command, options, send, { cliPath, activeSessions }) {
|
|
429
|
+
const key = (options.sessionId && queueAliases.get(options.sessionId))
|
|
430
|
+
|| options.sessionId
|
|
431
|
+
|| `new:${options.cwd || ''}`;
|
|
432
|
+
const queue = queues.get(key) || { items: [], running: false };
|
|
433
|
+
queues.set(key, queue);
|
|
434
|
+
return new Promise(resolve => {
|
|
435
|
+
queue.items.push({ command, options, send, resolve });
|
|
436
|
+
if (queue.running) {
|
|
437
|
+
send({
|
|
438
|
+
type: 'status',
|
|
439
|
+
message: `Grok follow-up queued (${queue.items.length - 1} ahead)`,
|
|
440
|
+
sessionId: options.sessionId,
|
|
441
|
+
agent: 'grok',
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
queue.running = true;
|
|
446
|
+
void (async () => {
|
|
447
|
+
while (queue.items.length) {
|
|
448
|
+
const item = queue.items.shift();
|
|
449
|
+
await runOne(item, { cliPath, activeSessions, queueKey: key });
|
|
450
|
+
item.resolve();
|
|
451
|
+
}
|
|
452
|
+
queue.running = false;
|
|
453
|
+
queues.delete(key);
|
|
454
|
+
for (const [sid, alias] of queueAliases) if (alias === key) queueAliases.delete(sid);
|
|
455
|
+
})();
|
|
326
456
|
});
|
|
327
457
|
}
|
|
458
|
+
|
|
459
|
+
export async function interruptGrok(sessionId) {
|
|
460
|
+
for (const client of clients.values()) {
|
|
461
|
+
if (client.listeners.has(sessionId)) {
|
|
462
|
+
try {
|
|
463
|
+
client.write({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } });
|
|
464
|
+
return true;
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
}
|