@openai-lite/codex-feishu 0.1.0
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 +164 -0
- package/bin/codex-feishu.js +8 -0
- package/docs/ARCHITECTURE.md +118 -0
- package/docs/FEISHU_SETUP.zh-CN.md +46 -0
- package/docs/README.zh-CN.md +130 -0
- package/docs/assets/feishu-preview-image-reply.png +0 -0
- package/docs/assets/feishu-preview-image-reply1.jpg +0 -0
- package/docs/assets/feishu-preview-session.png +0 -0
- package/package.json +23 -0
- package/src/cli.js +158 -0
- package/src/commands/daemon.js +6037 -0
- package/src/commands/doctor.js +126 -0
- package/src/commands/inbound.js +27 -0
- package/src/commands/init.js +224 -0
- package/src/commands/mcp.js +295 -0
- package/src/commands/qrcode.js +163 -0
- package/src/lib/app_server_client.js +831 -0
- package/src/lib/daemon_control.js +190 -0
- package/src/lib/feishu_bridge.js +461 -0
- package/src/lib/fs_utils.js +41 -0
- package/src/lib/paths.js +59 -0
- package/src/lib/state_store.js +146 -0
- package/src/lib/uds_rpc.js +195 -0
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
import { EventEmitter, once } from "node:events";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
|
|
5
|
+
function normalizeJsonRpcPayload(payload) {
|
|
6
|
+
if (!payload || typeof payload !== "object") {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
if (payload.jsonrpc === undefined) {
|
|
10
|
+
return { jsonrpc: "2.0", ...payload };
|
|
11
|
+
}
|
|
12
|
+
return payload;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toTextInput(text) {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
type: "text",
|
|
19
|
+
text,
|
|
20
|
+
text_elements: [],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toProtoItems(input) {
|
|
26
|
+
if (!Array.isArray(input)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const item of input) {
|
|
31
|
+
if (!item || typeof item !== "object") {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (item.type === "text" && typeof item.text === "string" && item.text.length > 0) {
|
|
35
|
+
out.push({
|
|
36
|
+
type: "text",
|
|
37
|
+
text: item.text,
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (item.type === "localImage" && typeof item.path === "string" && item.path.length > 0) {
|
|
42
|
+
out.push({
|
|
43
|
+
type: "local_image",
|
|
44
|
+
path: item.path,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function withTimeout(promise, timeoutMs, onTimeout) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
try {
|
|
55
|
+
onTimeout?.();
|
|
56
|
+
} catch {
|
|
57
|
+
// noop
|
|
58
|
+
}
|
|
59
|
+
reject(new Error(`operation timeout after ${timeoutMs}ms`));
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
promise.then(
|
|
62
|
+
(value) => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve(value);
|
|
65
|
+
},
|
|
66
|
+
(err) => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
reject(err);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function probeSubcommand(codexBin, args, timeoutMs = 2_500) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
let settled = false;
|
|
77
|
+
let stdout = "";
|
|
78
|
+
let stderr = "";
|
|
79
|
+
let timer = null;
|
|
80
|
+
const child = spawn(codexBin, args, {
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
82
|
+
env: process.env,
|
|
83
|
+
});
|
|
84
|
+
child.stdout.on("data", (chunk) => {
|
|
85
|
+
stdout += chunk.toString("utf8");
|
|
86
|
+
});
|
|
87
|
+
child.stderr.on("data", (chunk) => {
|
|
88
|
+
stderr += chunk.toString("utf8");
|
|
89
|
+
});
|
|
90
|
+
const done = (result) => {
|
|
91
|
+
if (settled) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
settled = true;
|
|
95
|
+
if (timer) {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
}
|
|
98
|
+
resolve(result);
|
|
99
|
+
};
|
|
100
|
+
timer = setTimeout(() => {
|
|
101
|
+
try {
|
|
102
|
+
child.kill("SIGKILL");
|
|
103
|
+
} catch {
|
|
104
|
+
// noop
|
|
105
|
+
}
|
|
106
|
+
done({
|
|
107
|
+
ok: false,
|
|
108
|
+
code: null,
|
|
109
|
+
stdout,
|
|
110
|
+
stderr,
|
|
111
|
+
timedOut: true,
|
|
112
|
+
});
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
child.on("error", (err) => {
|
|
115
|
+
done({
|
|
116
|
+
ok: false,
|
|
117
|
+
code: null,
|
|
118
|
+
stdout,
|
|
119
|
+
stderr: `${stderr}\n${err?.message ?? String(err)}`,
|
|
120
|
+
timedOut: false,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
child.on("close", (code) => {
|
|
124
|
+
done({
|
|
125
|
+
ok: code === 0,
|
|
126
|
+
code,
|
|
127
|
+
stdout,
|
|
128
|
+
stderr,
|
|
129
|
+
timedOut: false,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class AppServerClient extends EventEmitter {
|
|
136
|
+
constructor(options = {}) {
|
|
137
|
+
super();
|
|
138
|
+
this.codexBin = options.codexBin || process.env.CODEX_BIN || "codex";
|
|
139
|
+
this.transport = options.transport || process.env.CODEX_FEISHU_TRANSPORT || "auto";
|
|
140
|
+
this.protoCwd = options.protoCwd || process.env.CODEX_FEISHU_CWD || process.cwd();
|
|
141
|
+
this.protoModel = options.protoModel || process.env.CODEX_FEISHU_MODEL || "gpt-5.3-codex";
|
|
142
|
+
this.protoApprovalPolicy = process.env.CODEX_FEISHU_APPROVAL_POLICY || "on-failure";
|
|
143
|
+
this.protoSandboxMode = process.env.CODEX_FEISHU_SANDBOX_MODE || "workspace-write";
|
|
144
|
+
this.protoSummary = process.env.CODEX_FEISHU_SUMMARY || "auto";
|
|
145
|
+
|
|
146
|
+
this.process = null;
|
|
147
|
+
this.stdoutRl = null;
|
|
148
|
+
this.stderrRl = null;
|
|
149
|
+
this.starting = null;
|
|
150
|
+
this.nextId = 1;
|
|
151
|
+
this.pending = new Map();
|
|
152
|
+
this.initialized = false;
|
|
153
|
+
this.lastStartError = null;
|
|
154
|
+
this.protocolMode = null;
|
|
155
|
+
this.probedMode = null;
|
|
156
|
+
this.startupStderr = [];
|
|
157
|
+
this.sessionId = null;
|
|
158
|
+
this.activeTurns = new Map();
|
|
159
|
+
this.protoReady = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
status() {
|
|
163
|
+
return {
|
|
164
|
+
running: Boolean(this.process && !this.process.killed),
|
|
165
|
+
pid: this.process?.pid ?? null,
|
|
166
|
+
initialized: this.initialized,
|
|
167
|
+
pending_requests: this.pending.size,
|
|
168
|
+
last_start_error: this.lastStartError,
|
|
169
|
+
transport: this.transport,
|
|
170
|
+
mode: this.protocolMode,
|
|
171
|
+
session_id: this.sessionId,
|
|
172
|
+
active_turns: this.activeTurns.size,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async ensureStarted() {
|
|
177
|
+
if (this.process && !this.process.killed && this.initialized) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!this.starting) {
|
|
181
|
+
this.starting = this.start();
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await this.starting;
|
|
185
|
+
} finally {
|
|
186
|
+
this.starting = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async resolveTransportMode() {
|
|
191
|
+
if (this.transport === "app-server" || this.transport === "proto") {
|
|
192
|
+
return this.transport;
|
|
193
|
+
}
|
|
194
|
+
if (this.probedMode) {
|
|
195
|
+
return this.probedMode;
|
|
196
|
+
}
|
|
197
|
+
// Prefer app-server when available so behavior stays aligned with interactive Codex.
|
|
198
|
+
// Fall back to proto only when app-server is not supported by the local codex binary.
|
|
199
|
+
const appServerProbe = await probeSubcommand(this.codexBin, ["app-server", "--help"]);
|
|
200
|
+
const appServerText = `${appServerProbe.stdout}\n${appServerProbe.stderr}`;
|
|
201
|
+
const looksLikeAppServerHelp =
|
|
202
|
+
/transport endpoint url/i.test(appServerText) ||
|
|
203
|
+
/analytics are disabled by default for app-server/i.test(appServerText) ||
|
|
204
|
+
(/--listen/i.test(appServerText) && /stdio:\/\//i.test(appServerText));
|
|
205
|
+
if (looksLikeAppServerHelp) {
|
|
206
|
+
this.probedMode = "app-server";
|
|
207
|
+
return this.probedMode;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const protoProbe = await probeSubcommand(this.codexBin, ["proto", "--help"]);
|
|
211
|
+
const protoText = `${protoProbe.stdout}\n${protoProbe.stderr}`;
|
|
212
|
+
if (protoProbe.ok && /protocol stream/i.test(protoText)) {
|
|
213
|
+
this.probedMode = "proto";
|
|
214
|
+
return this.probedMode;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Conservative final fallback for very old/odd builds.
|
|
218
|
+
this.probedMode = "app-server";
|
|
219
|
+
return this.probedMode;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async start() {
|
|
223
|
+
this.lastStartError = null;
|
|
224
|
+
this.initialized = false;
|
|
225
|
+
this.sessionId = null;
|
|
226
|
+
this.activeTurns.clear();
|
|
227
|
+
this.startupStderr = [];
|
|
228
|
+
|
|
229
|
+
const mode = await this.resolveTransportMode();
|
|
230
|
+
this.protocolMode = mode;
|
|
231
|
+
if (mode === "proto") {
|
|
232
|
+
await this.startProto();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
await this.startLegacyAppServer();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async startLegacyAppServer() {
|
|
239
|
+
const child = spawn(this.codexBin, ["app-server", "--listen", "stdio://"], {
|
|
240
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
241
|
+
env: process.env,
|
|
242
|
+
cwd: this.protoCwd,
|
|
243
|
+
});
|
|
244
|
+
this.process = child;
|
|
245
|
+
|
|
246
|
+
child.on("error", (err) => {
|
|
247
|
+
if (this.process !== child) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.lastStartError = err?.message ?? String(err);
|
|
251
|
+
this.emit("error", err);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
child.on("close", (code, signal) => {
|
|
255
|
+
if (this.process !== child) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this.process = null;
|
|
259
|
+
this.initialized = false;
|
|
260
|
+
const stderrHint = this.startupStderr.slice(-6).join(" | ").trim();
|
|
261
|
+
for (const [id, entry] of this.pending.entries()) {
|
|
262
|
+
clearTimeout(entry.timeout);
|
|
263
|
+
const extra = stderrHint ? `; stderr: ${stderrHint}` : "";
|
|
264
|
+
entry.reject(new Error(`app-server exited while waiting for response (id=${id})${extra}`));
|
|
265
|
+
}
|
|
266
|
+
this.pending.clear();
|
|
267
|
+
this.emit("exit", { code, signal });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
this.stdoutRl = readline.createInterface({
|
|
271
|
+
input: child.stdout,
|
|
272
|
+
crlfDelay: Infinity,
|
|
273
|
+
});
|
|
274
|
+
this.stderrRl = readline.createInterface({
|
|
275
|
+
input: child.stderr,
|
|
276
|
+
crlfDelay: Infinity,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
this.stdoutRl.on("line", (line) => this.handleLine(line));
|
|
280
|
+
this.stderrRl.on("line", (line) => {
|
|
281
|
+
this.startupStderr.push(line);
|
|
282
|
+
if (this.startupStderr.length > 40) {
|
|
283
|
+
this.startupStderr = this.startupStderr.slice(-40);
|
|
284
|
+
}
|
|
285
|
+
this.emit("stderr", line);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await this.request(
|
|
290
|
+
"initialize",
|
|
291
|
+
{
|
|
292
|
+
clientInfo: {
|
|
293
|
+
name: "codex_feishu",
|
|
294
|
+
title: "Codex Feishu Bridge",
|
|
295
|
+
version: "0.1.0",
|
|
296
|
+
},
|
|
297
|
+
capabilities: {
|
|
298
|
+
experimentalApi: true,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
15_000,
|
|
302
|
+
);
|
|
303
|
+
this.notify("initialized");
|
|
304
|
+
this.initialized = true;
|
|
305
|
+
} catch (err) {
|
|
306
|
+
this.lastStartError = err?.message ?? String(err);
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async startProto() {
|
|
312
|
+
const child = spawn(this.codexBin, ["proto"], {
|
|
313
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
314
|
+
env: process.env,
|
|
315
|
+
cwd: this.protoCwd,
|
|
316
|
+
});
|
|
317
|
+
this.process = child;
|
|
318
|
+
this.protoReady = {};
|
|
319
|
+
|
|
320
|
+
child.on("error", (err) => {
|
|
321
|
+
if (this.process !== child) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.lastStartError = err?.message ?? String(err);
|
|
325
|
+
this.emit("error", err);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
child.on("close", (code, signal) => {
|
|
329
|
+
if (this.process !== child) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.process = null;
|
|
333
|
+
this.initialized = false;
|
|
334
|
+
for (const [id, entry] of this.pending.entries()) {
|
|
335
|
+
clearTimeout(entry.timeout);
|
|
336
|
+
entry.reject(new Error(`codex proto exited while waiting for response (id=${id})`));
|
|
337
|
+
}
|
|
338
|
+
this.pending.clear();
|
|
339
|
+
for (const [turnId, turn] of this.activeTurns.entries()) {
|
|
340
|
+
this.emit("notification", {
|
|
341
|
+
method: "turn/completed",
|
|
342
|
+
params: {
|
|
343
|
+
threadId: turn.threadId,
|
|
344
|
+
turnId,
|
|
345
|
+
turn: {
|
|
346
|
+
id: turnId,
|
|
347
|
+
status: "failed",
|
|
348
|
+
},
|
|
349
|
+
error: `codex proto exited (code=${code ?? "unknown"}, signal=${signal ?? "none"})`,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
this.activeTurns.clear();
|
|
354
|
+
if (this.protoReady?.reject) {
|
|
355
|
+
this.protoReady.reject(new Error(`codex proto exited before session configured (code=${code ?? "unknown"})`));
|
|
356
|
+
}
|
|
357
|
+
this.protoReady = null;
|
|
358
|
+
this.emit("exit", { code, signal });
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
this.stdoutRl = readline.createInterface({
|
|
362
|
+
input: child.stdout,
|
|
363
|
+
crlfDelay: Infinity,
|
|
364
|
+
});
|
|
365
|
+
this.stderrRl = readline.createInterface({
|
|
366
|
+
input: child.stderr,
|
|
367
|
+
crlfDelay: Infinity,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
this.stdoutRl.on("line", (line) => this.handleLine(line));
|
|
371
|
+
this.stderrRl.on("line", (line) => {
|
|
372
|
+
this.startupStderr.push(line);
|
|
373
|
+
if (this.startupStderr.length > 40) {
|
|
374
|
+
this.startupStderr = this.startupStderr.slice(-40);
|
|
375
|
+
}
|
|
376
|
+
this.emit("stderr", line);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await withTimeout(
|
|
380
|
+
new Promise((resolve, reject) => {
|
|
381
|
+
this.protoReady.resolve = resolve;
|
|
382
|
+
this.protoReady.reject = reject;
|
|
383
|
+
}),
|
|
384
|
+
20_000,
|
|
385
|
+
() => {
|
|
386
|
+
try {
|
|
387
|
+
child.kill("SIGKILL");
|
|
388
|
+
} catch {
|
|
389
|
+
// noop
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
this.initialized = true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async forceStopProcess() {
|
|
397
|
+
if (!this.process || this.process.killed) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
this.process.kill("SIGKILL");
|
|
402
|
+
} catch {
|
|
403
|
+
// noop
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async stop() {
|
|
408
|
+
if (!this.process || this.process.killed) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const current = this.process;
|
|
412
|
+
if (this.protocolMode === "proto") {
|
|
413
|
+
try {
|
|
414
|
+
this.writeProto({
|
|
415
|
+
id: `shutdown-${Date.now()}`,
|
|
416
|
+
op: { type: "shutdown" },
|
|
417
|
+
});
|
|
418
|
+
} catch {
|
|
419
|
+
// noop
|
|
420
|
+
}
|
|
421
|
+
await withTimeout(
|
|
422
|
+
once(current, "close"),
|
|
423
|
+
1_500,
|
|
424
|
+
() => {
|
|
425
|
+
try {
|
|
426
|
+
current.kill("SIGKILL");
|
|
427
|
+
} catch {
|
|
428
|
+
// noop
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
).catch(() => undefined);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
current.kill("SIGTERM");
|
|
436
|
+
} catch {
|
|
437
|
+
// noop
|
|
438
|
+
}
|
|
439
|
+
await withTimeout(
|
|
440
|
+
once(current, "close"),
|
|
441
|
+
1_500,
|
|
442
|
+
() => {
|
|
443
|
+
try {
|
|
444
|
+
current.kill("SIGKILL");
|
|
445
|
+
} catch {
|
|
446
|
+
// noop
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
).catch(() => undefined);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
writePayload(payload) {
|
|
453
|
+
if (!this.process || this.process.killed || !this.process.stdin) {
|
|
454
|
+
throw new Error("app-server is not running");
|
|
455
|
+
}
|
|
456
|
+
this.process.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
writeProto(submission) {
|
|
460
|
+
if (!this.process || this.process.killed || !this.process.stdin) {
|
|
461
|
+
throw new Error("codex proto is not running");
|
|
462
|
+
}
|
|
463
|
+
this.process.stdin.write(`${JSON.stringify(submission)}\n`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
notify(method, params = undefined) {
|
|
467
|
+
const payload = normalizeJsonRpcPayload({
|
|
468
|
+
method,
|
|
469
|
+
params,
|
|
470
|
+
});
|
|
471
|
+
this.writePayload(payload);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
request(method, params, timeoutMs = 120_000) {
|
|
475
|
+
if (this.protocolMode === "proto") {
|
|
476
|
+
return Promise.reject(new Error(`request(${method}) is unsupported in proto mode`));
|
|
477
|
+
}
|
|
478
|
+
const id = `app-${this.nextId++}`;
|
|
479
|
+
const payload = normalizeJsonRpcPayload({
|
|
480
|
+
id,
|
|
481
|
+
method,
|
|
482
|
+
params,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
const timeout = setTimeout(() => {
|
|
487
|
+
this.pending.delete(id);
|
|
488
|
+
reject(new Error(`app-server request timeout after ${timeoutMs}ms`));
|
|
489
|
+
}, timeoutMs);
|
|
490
|
+
const entry = { resolve, reject, timeout };
|
|
491
|
+
this.pending.set(id, entry);
|
|
492
|
+
try {
|
|
493
|
+
this.writePayload(payload);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
clearTimeout(entry.timeout);
|
|
496
|
+
this.pending.delete(id);
|
|
497
|
+
reject(err);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
respond(id, result) {
|
|
503
|
+
this.writePayload(
|
|
504
|
+
normalizeJsonRpcPayload({
|
|
505
|
+
id,
|
|
506
|
+
result,
|
|
507
|
+
}),
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
respondError(id, code, message, data) {
|
|
512
|
+
this.writePayload(
|
|
513
|
+
normalizeJsonRpcPayload({
|
|
514
|
+
id,
|
|
515
|
+
error: {
|
|
516
|
+
code,
|
|
517
|
+
message,
|
|
518
|
+
data,
|
|
519
|
+
},
|
|
520
|
+
}),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async startThread(params = {}) {
|
|
525
|
+
await this.ensureStarted();
|
|
526
|
+
if (this.protocolMode === "proto") {
|
|
527
|
+
await this.stop();
|
|
528
|
+
await this.ensureStarted();
|
|
529
|
+
return {
|
|
530
|
+
threadId: this.sessionId,
|
|
531
|
+
thread: {
|
|
532
|
+
id: this.sessionId,
|
|
533
|
+
title: params?.title ?? null,
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return this.request("thread/start", {
|
|
538
|
+
experimentalRawEvents: false,
|
|
539
|
+
persistExtendedHistory: true,
|
|
540
|
+
...params,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async resumeThread(threadId, params = {}) {
|
|
545
|
+
await this.ensureStarted();
|
|
546
|
+
if (this.protocolMode === "proto") {
|
|
547
|
+
if (threadId && threadId === this.sessionId) {
|
|
548
|
+
return {
|
|
549
|
+
threadId: this.sessionId,
|
|
550
|
+
thread: {
|
|
551
|
+
id: this.sessionId,
|
|
552
|
+
resumed: true,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return this.startThread(params);
|
|
557
|
+
}
|
|
558
|
+
return this.request("thread/resume", {
|
|
559
|
+
threadId,
|
|
560
|
+
...params,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async startTurn(threadId, text, params = {}) {
|
|
565
|
+
const requestedCwd =
|
|
566
|
+
typeof params?.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : null;
|
|
567
|
+
if (requestedCwd && (!this.process || this.process.killed)) {
|
|
568
|
+
this.protoCwd = requestedCwd;
|
|
569
|
+
}
|
|
570
|
+
await this.ensureStarted();
|
|
571
|
+
const input = Array.isArray(text) ? text : toTextInput(text);
|
|
572
|
+
|
|
573
|
+
if (this.protocolMode === "proto") {
|
|
574
|
+
let targetThreadId = threadId || this.sessionId;
|
|
575
|
+
if (!targetThreadId || targetThreadId !== this.sessionId) {
|
|
576
|
+
const created = await this.startThread({});
|
|
577
|
+
targetThreadId = created.threadId;
|
|
578
|
+
}
|
|
579
|
+
const turnCwd = requestedCwd || this.protoCwd;
|
|
580
|
+
if (requestedCwd) {
|
|
581
|
+
this.protoCwd = requestedCwd;
|
|
582
|
+
}
|
|
583
|
+
const turnId = `turn-${this.nextId++}`;
|
|
584
|
+
const items = toProtoItems(input);
|
|
585
|
+
this.activeTurns.set(turnId, {
|
|
586
|
+
threadId: targetThreadId,
|
|
587
|
+
});
|
|
588
|
+
this.writeProto({
|
|
589
|
+
id: turnId,
|
|
590
|
+
op: {
|
|
591
|
+
type: "user_turn",
|
|
592
|
+
cwd: turnCwd,
|
|
593
|
+
approval_policy: this.protoApprovalPolicy,
|
|
594
|
+
sandbox_policy: { mode: this.protoSandboxMode },
|
|
595
|
+
model: this.protoModel,
|
|
596
|
+
summary: this.protoSummary,
|
|
597
|
+
items,
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
return {
|
|
601
|
+
thread: {
|
|
602
|
+
id: targetThreadId,
|
|
603
|
+
},
|
|
604
|
+
turn: {
|
|
605
|
+
id: turnId,
|
|
606
|
+
status: "in_progress",
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (requestedCwd && requestedCwd !== this.protoCwd) {
|
|
612
|
+
this.protoCwd = requestedCwd;
|
|
613
|
+
await this.stop();
|
|
614
|
+
await this.ensureStarted();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return this.request("turn/start", {
|
|
618
|
+
threadId,
|
|
619
|
+
input,
|
|
620
|
+
...params,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async steerTurn(threadId, expectedTurnId, text) {
|
|
625
|
+
await this.ensureStarted();
|
|
626
|
+
if (this.protocolMode === "proto") {
|
|
627
|
+
return this.startTurn(threadId, text, {});
|
|
628
|
+
}
|
|
629
|
+
return this.request("turn/steer", {
|
|
630
|
+
threadId,
|
|
631
|
+
expectedTurnId,
|
|
632
|
+
input: toTextInput(text),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async stopTurn(threadId, turnId, options = {}) {
|
|
637
|
+
const timeoutMs =
|
|
638
|
+
Number.isFinite(options?.timeoutMs) && options.timeoutMs > 0
|
|
639
|
+
? Number(options.timeoutMs)
|
|
640
|
+
: 10_000;
|
|
641
|
+
await this.ensureStarted();
|
|
642
|
+
|
|
643
|
+
if (this.protocolMode === "proto") {
|
|
644
|
+
const hadActive = this.activeTurns.size > 0;
|
|
645
|
+
if (!hadActive && !turnId) {
|
|
646
|
+
return {
|
|
647
|
+
ok: false,
|
|
648
|
+
stopped: false,
|
|
649
|
+
mode: "proto",
|
|
650
|
+
reason: "no_active_turn",
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
await this.stop();
|
|
654
|
+
await this.ensureStarted();
|
|
655
|
+
return {
|
|
656
|
+
ok: true,
|
|
657
|
+
stopped: true,
|
|
658
|
+
mode: "restart",
|
|
659
|
+
reason: "proto_no_cancel_rpc",
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const payload = {};
|
|
664
|
+
if (threadId) {
|
|
665
|
+
payload.threadId = threadId;
|
|
666
|
+
}
|
|
667
|
+
if (turnId) {
|
|
668
|
+
payload.turnId = turnId;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
await this.request("turn/cancel", payload, timeoutMs);
|
|
673
|
+
return {
|
|
674
|
+
ok: true,
|
|
675
|
+
stopped: true,
|
|
676
|
+
mode: "rpc",
|
|
677
|
+
};
|
|
678
|
+
} catch (err) {
|
|
679
|
+
await this.stop();
|
|
680
|
+
await this.ensureStarted();
|
|
681
|
+
return {
|
|
682
|
+
ok: true,
|
|
683
|
+
stopped: true,
|
|
684
|
+
mode: "restart",
|
|
685
|
+
reason: err?.message ?? String(err),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
handleProtoPayload(payload) {
|
|
691
|
+
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
692
|
+
const msg = payload?.msg && typeof payload.msg === "object" ? payload.msg : null;
|
|
693
|
+
if (!msg || !msg.type) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (msg.type === "session_configured") {
|
|
698
|
+
this.sessionId = msg.session_id ?? this.sessionId;
|
|
699
|
+
this.protoModel = msg.model ?? this.protoModel;
|
|
700
|
+
this.initialized = true;
|
|
701
|
+
if (this.protoReady?.resolve) {
|
|
702
|
+
this.protoReady.resolve(msg);
|
|
703
|
+
}
|
|
704
|
+
this.protoReady = null;
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!id) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (this.pending.has(id)) {
|
|
713
|
+
const pending = this.pending.get(id);
|
|
714
|
+
clearTimeout(pending.timeout);
|
|
715
|
+
this.pending.delete(id);
|
|
716
|
+
pending.resolve(msg);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const active = this.activeTurns.get(id);
|
|
721
|
+
if (!active) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const threadId = active.threadId ?? this.sessionId;
|
|
725
|
+
const turnId = id;
|
|
726
|
+
|
|
727
|
+
if (msg.type === "agent_message_delta" && typeof msg.delta === "string" && msg.delta.length > 0) {
|
|
728
|
+
this.emit("notification", {
|
|
729
|
+
method: "item/agentMessage/delta",
|
|
730
|
+
params: {
|
|
731
|
+
threadId,
|
|
732
|
+
turnId,
|
|
733
|
+
delta: msg.delta,
|
|
734
|
+
model: this.protoModel,
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (msg.type === "task_complete") {
|
|
741
|
+
this.activeTurns.delete(id);
|
|
742
|
+
this.emit("notification", {
|
|
743
|
+
method: "turn/completed",
|
|
744
|
+
params: {
|
|
745
|
+
threadId,
|
|
746
|
+
turnId,
|
|
747
|
+
model: this.protoModel,
|
|
748
|
+
turn: {
|
|
749
|
+
id: turnId,
|
|
750
|
+
status: "completed",
|
|
751
|
+
},
|
|
752
|
+
last_agent_message: msg.last_agent_message ?? null,
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (msg.type === "error" || msg.type === "task_error") {
|
|
759
|
+
this.activeTurns.delete(id);
|
|
760
|
+
this.emit("notification", {
|
|
761
|
+
method: "turn/completed",
|
|
762
|
+
params: {
|
|
763
|
+
threadId,
|
|
764
|
+
turnId,
|
|
765
|
+
model: this.protoModel,
|
|
766
|
+
turn: {
|
|
767
|
+
id: turnId,
|
|
768
|
+
status: "failed",
|
|
769
|
+
},
|
|
770
|
+
error: msg.message ?? "task failed",
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
handleLegacyPayload(msg) {
|
|
778
|
+
if (!msg) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if ("id" in msg && !("method" in msg)) {
|
|
782
|
+
const pending = this.pending.get(msg.id);
|
|
783
|
+
if (!pending) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
clearTimeout(pending.timeout);
|
|
787
|
+
this.pending.delete(msg.id);
|
|
788
|
+
if ("error" in msg) {
|
|
789
|
+
const err = new Error(msg.error?.message || "app-server rpc error");
|
|
790
|
+
err.code = msg.error?.code;
|
|
791
|
+
err.data = msg.error?.data;
|
|
792
|
+
pending.reject(err);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
pending.resolve(msg.result);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if ("method" in msg && "id" in msg) {
|
|
800
|
+
this.emit("request", msg);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if ("method" in msg) {
|
|
805
|
+
this.emit("notification", msg);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
handleLine(line) {
|
|
810
|
+
const trimmed = line.trim();
|
|
811
|
+
if (!trimmed) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
let payload;
|
|
816
|
+
try {
|
|
817
|
+
payload = JSON.parse(trimmed);
|
|
818
|
+
} catch {
|
|
819
|
+
this.emit("parse_error", line);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (payload && typeof payload === "object" && "msg" in payload && "id" in payload && !("jsonrpc" in payload)) {
|
|
824
|
+
this.handleProtoPayload(payload);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const msg = normalizeJsonRpcPayload(payload);
|
|
829
|
+
this.handleLegacyPayload(msg);
|
|
830
|
+
}
|
|
831
|
+
}
|