@xiaohhhh1/canvas-agent 0.2.2

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/dist/agents.js ADDED
@@ -0,0 +1,557 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { createRequire } from "node:module";
6
+ import { fileURLToPath } from "node:url";
7
+ import { AGENT_PROMPT, VERSION } from "./config.js";
8
+ let codexQueue = Promise.resolve();
9
+ let codexApp = null;
10
+ let codexAppStart = null;
11
+ let codexThreadId = "";
12
+ const canvasAgentMcp = canvasAgentMcpCommand();
13
+ const require = createRequire(import.meta.url);
14
+ export function withAgentPrompt(prompt) {
15
+ const commerceGuidelines = "画布内所有商品图片、视频参考图和中间资产默认使用用户自有 OSS;禁止把任何密钥写入画布、提示词、日志或前端。用户请求带货视频但未指定流程时,先确认流程 A 或 B;流程 A 使用结构化脚本和已审核的同 SKU 官方商品图,流程 B 使用最多 5 张同 SKU 官方图生成审核后的商品板。两个流程都只提交稳定 HTTPS 的 OSS 素材 URL。";
16
+ return prompt.trim() ? `${AGENT_PROMPT}\n\n${commerceGuidelines}\n\n用户请求:${prompt}` : "";
17
+ }
18
+ export async function runCodexTurn(prompt, emit, attachments = [], options = {}) {
19
+ if (!prompt.trim())
20
+ return;
21
+ codexQueue = codexQueue.catch(() => undefined).then(() => runCodexTurnNow(prompt, emit, attachments, options));
22
+ await codexQueue;
23
+ }
24
+ export function interruptCodexTurn(threadId) {
25
+ if (!codexApp || (threadId && threadId !== codexThreadId))
26
+ return false;
27
+ return codexApp.interruptCurrentTurn();
28
+ }
29
+ async function runCodexTurnNow(prompt, emit, attachments, options) {
30
+ let files = [];
31
+ try {
32
+ options.onStart?.();
33
+ files = await writeAttachmentFiles(attachments);
34
+ const app = await getCodexApp(options.appEmit || emit);
35
+ let threadId = await ensureCodexThread(app, options, emit);
36
+ options.onThread?.(threadId);
37
+ try {
38
+ await app.startTurn(threadId, prompt, files, options.onTurn);
39
+ }
40
+ catch (error) {
41
+ if (!isRecoverableThreadError(error))
42
+ throw error;
43
+ emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` });
44
+ codexThreadId = "";
45
+ threadId = await ensureCodexThread(app, { cwd: options.cwd }, emit);
46
+ options.onThread?.(threadId);
47
+ await app.startTurn(threadId, prompt, files, options.onTurn);
48
+ }
49
+ }
50
+ catch (error) {
51
+ emit("agent_error", { message: errorMessage(error) });
52
+ }
53
+ finally {
54
+ options.onFinish?.();
55
+ await Promise.all(files.map((file) => fs.unlink(file).catch(() => undefined)));
56
+ }
57
+ }
58
+ export async function startCodexThread(emit, cwd) {
59
+ const app = await getCodexApp(emit);
60
+ const thread = await app.startThread(cwd);
61
+ codexThreadId = String(field(thread, "id") || "");
62
+ return thread;
63
+ }
64
+ export async function resumeCodexThread(emit, threadId, cwd) {
65
+ const app = await getCodexApp(emit);
66
+ await loadCodexThread(emit, threadId, cwd, false);
67
+ const thread = await app.resumeThread(threadId, cwd);
68
+ assertThreadWorkspace(thread, cwd);
69
+ codexThreadId = String(field(thread, "id") || threadId);
70
+ return { thread, messages: threadMessages(thread) };
71
+ }
72
+ export async function listCodexThreads(emit, options) {
73
+ const app = await getCodexApp(emit);
74
+ const result = await app.listThreads({
75
+ limit: options.limit || 40,
76
+ sortKey: "updated_at",
77
+ sortDirection: "desc",
78
+ sourceKinds: ["cli", "vscode", "appServer", "exec"],
79
+ cwd: options.cwd,
80
+ ...(options.searchTerm ? { searchTerm: options.searchTerm } : {}),
81
+ });
82
+ const data = Array.isArray(field(result, "data")) ? field(result, "data").map(summarizeCodexThread).filter((thread) => threadInWorkspace(thread, options.cwd)) : [];
83
+ return { data, nextCursor: field(result, "nextCursor") || null, backwardsCursor: field(result, "backwardsCursor") || null };
84
+ }
85
+ export async function readCodexThread(emit, threadId, cwd) {
86
+ const thread = await loadCodexThread(emit, threadId, cwd, true);
87
+ return { thread: summarizeCodexThread(thread), messages: threadMessages(thread) };
88
+ }
89
+ export async function verifyCodexThreadWorkspace(emit, threadId, cwd) {
90
+ await loadCodexThread(emit, threadId, cwd, false);
91
+ }
92
+ export async function archiveCodexThread(emit, threadId, cwd) {
93
+ const app = await getCodexApp(emit);
94
+ await loadCodexThread(emit, threadId, cwd, false);
95
+ await app.archiveThread(threadId);
96
+ }
97
+ export function runClaudeTurn(prompt, emit) {
98
+ if (!prompt.trim())
99
+ return;
100
+ const child = spawnAgent("claude", ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--allowedTools", "mcp__infinite-canvas__*", prompt], ["ignore", "pipe", "pipe"], emit);
101
+ if (!child)
102
+ return;
103
+ pipeJsonLines(child, emit, "claude");
104
+ }
105
+ async function ensureCodexThread(app, options, emit) {
106
+ if (options.threadId) {
107
+ if (options.threadId === codexThreadId)
108
+ return codexThreadId;
109
+ try {
110
+ const result = await app.readThread(options.threadId, false);
111
+ assertThreadWorkspace(field(result, "thread") || {}, options.cwd);
112
+ const thread = await app.resumeThread(options.threadId, options.cwd);
113
+ assertThreadWorkspace(thread, options.cwd);
114
+ codexThreadId = String(field(thread, "id") || options.threadId);
115
+ return codexThreadId;
116
+ }
117
+ catch (error) {
118
+ if (!isRecoverableThreadError(error))
119
+ throw error;
120
+ emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` });
121
+ }
122
+ }
123
+ if (!codexThreadId) {
124
+ const thread = await app.startThread(options.cwd);
125
+ codexThreadId = String(field(thread, "id") || "");
126
+ }
127
+ return codexThreadId;
128
+ }
129
+ export function isRecoverableThreadError(error) {
130
+ return /thread not loaded|no rollout found/i.test(errorMessage(error));
131
+ }
132
+ class CodexAppClient {
133
+ child;
134
+ emit;
135
+ nextId = 1;
136
+ buffer = "";
137
+ textByItem = new Map();
138
+ deltaCount = 0;
139
+ lastUsage = null;
140
+ pending = new Map();
141
+ activeTurns = new Map();
142
+ completedTurns = new Map();
143
+ constructor(child, emit) {
144
+ this.child = child;
145
+ this.emit = emit;
146
+ }
147
+ static async start(emit) {
148
+ const child = spawn(process.execPath, [codexBin(), "app-server", "--stdio"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
149
+ const client = new CodexAppClient(child, emit);
150
+ child.stdout?.on("data", (chunk) => client.read(chunk.toString()));
151
+ child.stderr?.on("data", (chunk) => emit("agent_log", { text: chunk.toString() }));
152
+ child.on("error", (error) => emit("agent_error", { message: error.message }));
153
+ child.on("exit", (code) => {
154
+ client.failAll(`Codex app-server exited: ${code ?? 0}`);
155
+ codexApp = null;
156
+ codexThreadId = "";
157
+ emit("agent_log", { text: `Codex app-server exited: ${code ?? 0}` });
158
+ });
159
+ await client.request("initialize", { clientInfo: { name: "canvas-agent", title: "Infinite Canvas Agent", version: VERSION }, capabilities: { experimentalApi: true, requestAttestation: false } });
160
+ client.notify("initialized");
161
+ return client;
162
+ }
163
+ async startThread(cwd) {
164
+ const result = await this.request("thread/start", { approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), ...(cwd ? { cwd } : {}), threadSource: "user" });
165
+ const thread = field(result, "thread");
166
+ const id = String(field(thread, "id") || "");
167
+ if (!id)
168
+ throw new Error("Codex app-server 没有返回 thread id");
169
+ return thread || {};
170
+ }
171
+ async resumeThread(threadId, cwd) {
172
+ const result = await this.request("thread/resume", { threadId, approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), ...(cwd ? { cwd } : {}) });
173
+ const thread = field(result, "thread");
174
+ const id = String(field(thread, "id") || "");
175
+ if (!id)
176
+ throw new Error("Codex app-server 没有返回 thread id");
177
+ return thread || {};
178
+ }
179
+ listThreads(params) {
180
+ return this.request("thread/list", params);
181
+ }
182
+ readThread(threadId, includeTurns = true) {
183
+ return this.request("thread/read", { threadId, includeTurns });
184
+ }
185
+ archiveThread(threadId) {
186
+ return this.request("thread/archive", { threadId });
187
+ }
188
+ async startTurn(threadId, prompt, images, onTurn) {
189
+ const result = await this.request("turn/start", { threadId, input: codexInput(prompt, images), approvalPolicy: "never" });
190
+ const turnId = String(field(field(result, "turn"), "id") || "");
191
+ if (!turnId)
192
+ throw new Error("Codex app-server 没有返回 turn id");
193
+ onTurn?.(turnId);
194
+ const completed = this.completedTurns.get(turnId);
195
+ if (this.completedTurns.has(turnId)) {
196
+ this.completedTurns.delete(turnId);
197
+ if (completed)
198
+ throw completed;
199
+ return;
200
+ }
201
+ await new Promise((resolve, reject) => this.activeTurns.set(turnId, { resolve, reject }));
202
+ }
203
+ interruptCurrentTurn() {
204
+ if (this.activeTurns.size === 0)
205
+ return false;
206
+ try {
207
+ this.child.kill("SIGINT");
208
+ return true;
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
214
+ request(method, params) {
215
+ const id = this.nextId++;
216
+ this.write({ id, method, params });
217
+ return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
218
+ }
219
+ notify(method, params) {
220
+ this.write(params === undefined ? { method } : { method, params });
221
+ }
222
+ write(value) {
223
+ this.child.stdin?.write(`${JSON.stringify(value)}\n`);
224
+ }
225
+ read(chunk) {
226
+ this.buffer += chunk;
227
+ const lines = this.buffer.split(/\r?\n/);
228
+ this.buffer = lines.pop() || "";
229
+ lines.filter(Boolean).forEach((line) => {
230
+ try {
231
+ this.handle(JSON.parse(line));
232
+ }
233
+ catch {
234
+ this.emit("agent_log", { text: line });
235
+ }
236
+ });
237
+ }
238
+ handle(message) {
239
+ const id = Number(message.id);
240
+ if (message.error && this.pending.has(id))
241
+ return this.reject(id, String(field(message.error, "message") || "Codex request failed"));
242
+ if (this.pending.has(id))
243
+ return this.resolve(id, message.result);
244
+ if (typeof message.method === "string" && "id" in message)
245
+ return this.answerServerRequest(message);
246
+ if (typeof message.method === "string")
247
+ this.handleNotification(message.method, (message.params || {}));
248
+ }
249
+ handleNotification(method, params) {
250
+ if (method === "item/agentMessage/delta")
251
+ return this.emitDelta(params);
252
+ if (method === "thread/tokenUsage/updated")
253
+ this.lastUsage = normalizeUsage(params);
254
+ const event = normalizeCodexNotification(method, params);
255
+ if (!event)
256
+ return;
257
+ if (event.type === "turn.completed")
258
+ event.usage = this.lastUsage;
259
+ this.emit("agent_event", { agent: "codex", ...event });
260
+ if (event.type === "turn.completed") {
261
+ const turnId = String(field(params, "turnId") || field(field(params, "turn"), "id") || "");
262
+ const pending = this.activeTurns.get(turnId);
263
+ const error = field(field(params, "turn"), "error");
264
+ if (pending) {
265
+ this.activeTurns.delete(turnId);
266
+ error ? pending.reject(new Error(String(field(error, "message") || "Codex turn failed"))) : pending.resolve(event);
267
+ }
268
+ else if (turnId) {
269
+ this.completedTurns.set(turnId, error ? new Error(String(field(error, "message") || "Codex turn failed")) : null);
270
+ }
271
+ this.emit("agent_event", { agent: "codex", type: "stream.summary", delta_count: this.deltaCount, ...codexEventScope(params) });
272
+ this.deltaCount = 0;
273
+ this.emit("agent_done", { agent: "codex", usage: event.usage, ...codexEventScope(params) });
274
+ }
275
+ }
276
+ emitDelta(params) {
277
+ const id = String(field(params, "itemId") || "");
278
+ const text = `${this.textByItem.get(id) || ""}${String(field(params, "delta") || "")}`;
279
+ this.deltaCount += 1;
280
+ this.textByItem.set(id, text);
281
+ this.emit("agent_event", { agent: "codex", type: "item.updated", item: { id, type: "agent_message", text }, ...codexEventScope(params) });
282
+ }
283
+ answerServerRequest(message) {
284
+ const method = String(message.method);
285
+ const result = method === "mcpServer/elicitation/request" ? { action: "accept", content: {}, _meta: null } : { decision: "decline" };
286
+ this.write({ id: message.id, result });
287
+ this.emit("agent_event", { agent: "codex", type: "server.request", method, params: message.params, result });
288
+ }
289
+ resolve(id, result) {
290
+ const pending = this.pending.get(id);
291
+ if (pending)
292
+ (this.pending.delete(id), pending.resolve(result));
293
+ }
294
+ reject(id, message) {
295
+ const pending = this.pending.get(id);
296
+ if (pending)
297
+ (this.pending.delete(id), pending.reject(new Error(message)));
298
+ }
299
+ failAll(message) {
300
+ [...this.pending.values(), ...this.activeTurns.values()].forEach((item) => item.reject(new Error(message)));
301
+ this.pending.clear();
302
+ this.activeTurns.clear();
303
+ }
304
+ }
305
+ function canvasAgentMcpCommand() {
306
+ const current = process.argv.find((arg) => /index\.(t|j)s$/.test(arg)) || "";
307
+ const entry = path.resolve(current || fileURLToPath(new URL("./index.js", import.meta.url)));
308
+ const tsx = path.join(path.dirname(entry), "..", "node_modules", "tsx", "dist", "cli.mjs");
309
+ return entry.endsWith(".ts") ? { command: process.execPath, args: [tsx, entry, "mcp"] } : { command: process.execPath, args: [entry, "mcp"] };
310
+ }
311
+ function codexConfig() {
312
+ return {
313
+ model: process.env.CANVAS_AGENT_CODEX_MODEL || "gpt-5.4",
314
+ mcp_servers: { "infinite-canvas": { command: canvasAgentMcp.command, args: canvasAgentMcp.args, default_tools_approval_mode: "approve", startup_timeout_sec: 20, tool_timeout_sec: 90 } },
315
+ };
316
+ }
317
+ function codexInput(prompt, images) {
318
+ return [{ type: "text", text: prompt, text_elements: [] }, ...images.map((file) => ({ type: "localImage", path: file }))];
319
+ }
320
+ function normalizeCodexNotification(method, params) {
321
+ const scope = codexEventScope(params);
322
+ if (method === "thread/started")
323
+ return { type: "thread.started", ...scope };
324
+ if (method === "turn/started")
325
+ return { type: "turn.started", ...scope };
326
+ if (method === "turn/completed")
327
+ return { type: "turn.completed", usage: null, ...scope };
328
+ if (method === "item/started")
329
+ return { type: "item.started", item: normalizeItem(field(params, "item")), ...scope };
330
+ if (method === "item/completed")
331
+ return { type: "item.completed", item: normalizeItem(field(params, "item")), ...scope };
332
+ if (method === "error")
333
+ return { type: "error", message: field(params, "message"), ...scope };
334
+ return null;
335
+ }
336
+ function codexEventScope(params) {
337
+ const threadId = String(field(params, "threadId") || field(field(params, "thread"), "id") || "");
338
+ const turnId = String(field(params, "turnId") || field(field(params, "turn"), "id") || "");
339
+ return { ...(threadId ? { thread_id: threadId } : {}), ...(turnId ? { turn_id: turnId } : {}) };
340
+ }
341
+ async function loadCodexThread(emit, threadId, cwd, includeTurns) {
342
+ const app = await getCodexApp(emit);
343
+ const result = await app.readThread(threadId, includeTurns);
344
+ const thread = field(result, "thread") || {};
345
+ assertThreadWorkspace(thread, cwd);
346
+ return thread;
347
+ }
348
+ async function getCodexApp(emit) {
349
+ if (codexApp)
350
+ return codexApp;
351
+ codexAppStart ||= CodexAppClient.start(emit);
352
+ try {
353
+ codexApp = await codexAppStart;
354
+ return codexApp;
355
+ }
356
+ finally {
357
+ codexAppStart = null;
358
+ }
359
+ }
360
+ function assertThreadWorkspace(thread, cwd) {
361
+ if (!cwd || threadInWorkspace(thread, cwd))
362
+ return;
363
+ throw new Error("该 Codex 会话不属于当前画布工作空间");
364
+ }
365
+ function threadInWorkspace(thread, cwd) {
366
+ const threadCwd = String(field(thread, "cwd") || "");
367
+ return Boolean(threadCwd && path.resolve(threadCwd) === path.resolve(cwd));
368
+ }
369
+ function normalizeItem(item) {
370
+ const value = item && typeof item === "object" ? { ...item } : {};
371
+ if (value.type === "agentMessage")
372
+ value.type = "agent_message";
373
+ if (value.type === "mcpToolCall")
374
+ value.type = "mcp_tool_call";
375
+ if (value.type === "agent_message" && typeof value.id === "string")
376
+ value.text = String(value.text || "");
377
+ if ("arguments" in value)
378
+ value.arguments = parseMaybeJson(value.arguments);
379
+ return value;
380
+ }
381
+ function normalizeUsage(params) {
382
+ const total = field(field(params, "tokenUsage"), "total");
383
+ return {
384
+ input_tokens: field(total, "inputTokens"),
385
+ cached_input_tokens: field(total, "cachedInputTokens"),
386
+ output_tokens: field(total, "outputTokens"),
387
+ reasoning_output_tokens: field(total, "reasoningOutputTokens"),
388
+ };
389
+ }
390
+ function parseMaybeJson(value) {
391
+ if (typeof value !== "string")
392
+ return value;
393
+ try {
394
+ return JSON.parse(value);
395
+ }
396
+ catch {
397
+ return value;
398
+ }
399
+ }
400
+ function field(value, key) {
401
+ return value && typeof value === "object" ? value[key] : undefined;
402
+ }
403
+ export function summarizeCodexThread(thread) {
404
+ return {
405
+ id: String(field(thread, "id") || ""),
406
+ sessionId: String(field(thread, "sessionId") || ""),
407
+ preview: displayUserText(String(field(thread, "preview") || "")),
408
+ name: stringOrNull(field(thread, "name")),
409
+ cwd: String(field(thread, "cwd") || ""),
410
+ status: String(field(thread, "status") || ""),
411
+ source: field(thread, "source"),
412
+ threadSource: field(thread, "threadSource"),
413
+ createdAt: Number(field(thread, "createdAt") || 0),
414
+ updatedAt: Number(field(thread, "updatedAt") || 0),
415
+ };
416
+ }
417
+ function threadMessages(thread) {
418
+ const turns = arrayValue(field(thread, "turns"));
419
+ const messages = [];
420
+ turns.forEach((turn, turnIndex) => {
421
+ arrayValue(field(turn, "items")).forEach((item, itemIndex) => {
422
+ const type = String(field(item, "type") || "");
423
+ const id = String(field(item, "id") || `${turnIndex}-${itemIndex}`);
424
+ if (type === "userMessage") {
425
+ const text = displayUserText(userInputText(field(item, "content")));
426
+ if (text)
427
+ messages.push({ id, role: "user", text });
428
+ }
429
+ if (type === "agentMessage") {
430
+ const text = String(field(item, "text") || "").trim();
431
+ if (text)
432
+ messages.push({ id, role: "assistant", title: "Codex", text, streamId: id });
433
+ }
434
+ if (type === "mcpToolCall") {
435
+ const tool = String(field(item, "tool") || "工具调用");
436
+ const error = field(field(item, "error"), "message");
437
+ messages.push({ id, role: error ? "error" : "tool", title: toolName(tool), text: error ? String(error) : `${toolName(tool)} ${String(field(item, "status") || "完成")}`, detail: item });
438
+ }
439
+ if (type === "commandExecution") {
440
+ const command = String(field(item, "command") || "").trim();
441
+ if (command)
442
+ messages.push({ id, role: "tool", title: "命令", text: command, detail: { cwd: field(item, "cwd"), status: field(item, "status"), exitCode: field(item, "exitCode") } });
443
+ }
444
+ if (type === "fileChange")
445
+ messages.push({ id, role: "tool", title: "文件变更", text: "Codex 修改了文件", detail: item });
446
+ });
447
+ });
448
+ return messages.filter((item) => item.text).slice(-120);
449
+ }
450
+ function userInputText(content) {
451
+ return arrayValue(content)
452
+ .map((item) => {
453
+ const type = String(field(item, "type") || "");
454
+ if (type === "text")
455
+ return String(field(item, "text") || "");
456
+ if (type === "image" || type === "localImage")
457
+ return "图片附件";
458
+ if (type === "mention")
459
+ return `@${String(field(item, "name") || "文件")}`;
460
+ return "";
461
+ })
462
+ .filter(Boolean)
463
+ .join("\n");
464
+ }
465
+ function displayUserText(text) {
466
+ const value = text.trim();
467
+ const marker = "用户请求:";
468
+ const index = value.lastIndexOf(marker);
469
+ return (index >= 0 ? value.slice(index + marker.length) : value).trim();
470
+ }
471
+ function arrayValue(value) {
472
+ return Array.isArray(value) ? value : [];
473
+ }
474
+ function stringOrNull(value) {
475
+ return typeof value === "string" && value.trim() ? value : null;
476
+ }
477
+ function toolName(name) {
478
+ if (name === "canvas_apply_ops")
479
+ return "画布操作";
480
+ if (name === "canvas_get_state")
481
+ return "读取画布";
482
+ if (name === "canvas_get_selection")
483
+ return "读取选区";
484
+ if (name === "canvas_export_snapshot")
485
+ return "导出快照";
486
+ if (name === "canvas_create_attachment_nodes")
487
+ return "添加附件图片";
488
+ if (name === "canvas_create_text_node")
489
+ return "创建文本";
490
+ if (name === "canvas_create_image_prompt_flow")
491
+ return "创建生图流程";
492
+ if (name === "canvas_create_generation_flow")
493
+ return "创建生成流程";
494
+ if (name === "canvas_generate_text")
495
+ return "生成文本";
496
+ if (name === "canvas_generate_image")
497
+ return "生成图片";
498
+ if (name === "canvas_generate_video")
499
+ return "生成视频";
500
+ if (name === "canvas_generate_audio")
501
+ return "生成音频";
502
+ if (name === "canvas_run_generation")
503
+ return "触发生成";
504
+ return name;
505
+ }
506
+ async function writeAttachmentFiles(attachments) {
507
+ return await Promise.all(attachments.filter((item) => item.dataUrl?.startsWith("data:image/")).map(writeAttachmentFile));
508
+ }
509
+ async function writeAttachmentFile(item) {
510
+ const [, meta = "", data = ""] = item.dataUrl?.match(/^data:([^;]+);base64,(.+)$/) || [];
511
+ if (!data)
512
+ throw new Error(`图片附件无效:${item.name || "未命名图片"}`);
513
+ const file = path.join(os.tmpdir(), `infinite-canvas-${Date.now()}-${Math.random().toString(16).slice(2)}.${imageExt(meta || item.type)}`);
514
+ await fs.writeFile(file, Buffer.from(data, "base64"));
515
+ return file;
516
+ }
517
+ function imageExt(type = "") {
518
+ if (type.includes("png"))
519
+ return "png";
520
+ if (type.includes("webp"))
521
+ return "webp";
522
+ return "jpg";
523
+ }
524
+ function codexBin() {
525
+ return path.join(path.dirname(require.resolve("@openai/codex/package.json")), "bin", "codex.js");
526
+ }
527
+ function pipeJsonLines(child, emit, agent) {
528
+ let out = "";
529
+ child.stdout?.on("data", (chunk) => {
530
+ out += chunk.toString();
531
+ const lines = out.split(/\r?\n/);
532
+ out = lines.pop() || "";
533
+ lines.filter(Boolean).forEach((line) => {
534
+ try {
535
+ emit("agent_event", { agent, ...JSON.parse(line) });
536
+ }
537
+ catch {
538
+ emit("agent_event", { agent, type: "raw", text: line });
539
+ }
540
+ });
541
+ });
542
+ child.stderr?.on("data", (chunk) => emit("agent_log", { text: chunk.toString() }));
543
+ child.on("error", (error) => emit("agent_error", { message: error.message }));
544
+ child.on("close", (code) => emit("agent_done", { agent, code }));
545
+ }
546
+ function spawnAgent(name, args, stdio, emit) {
547
+ try {
548
+ return spawn(name, args, { stdio, shell: process.platform === "win32", windowsHide: true });
549
+ }
550
+ catch (error) {
551
+ emit("agent_error", { message: errorMessage(error) });
552
+ return null;
553
+ }
554
+ }
555
+ function errorMessage(error) {
556
+ return error instanceof Error ? error.message : String(error);
557
+ }
@@ -0,0 +1,64 @@
1
+ import type { ServerResponse } from "node:http";
2
+ import type { AgentAttachment } from "./types.js";
3
+ type TurnAttachment = {
4
+ clientId: string;
5
+ id: string;
6
+ name: string;
7
+ type: string;
8
+ size: number;
9
+ width: number;
10
+ height: number;
11
+ dataUrl: string;
12
+ };
13
+ export type CodexState = {
14
+ busy: boolean;
15
+ threadId: string;
16
+ turnId: string;
17
+ };
18
+ export declare class CanvasSession {
19
+ private clients;
20
+ private clientFocusOrder;
21
+ private pending;
22
+ private canvasStates;
23
+ private turnAttachments;
24
+ private activeClientId;
25
+ private boundClientId;
26
+ private focusSequence;
27
+ private codexState;
28
+ private get canvasState();
29
+ private get targetClientId();
30
+ health(): {
31
+ ok: boolean;
32
+ hasCanvas: boolean;
33
+ clients: number;
34
+ codexBusy: boolean;
35
+ };
36
+ get codexBusy(): boolean;
37
+ setCodexState(patch: Partial<CodexState>): void;
38
+ openEvents(url: URL, res: ServerResponse): void;
39
+ updateState(body: unknown, clientId?: string): void;
40
+ activateClient(clientId: string): void;
41
+ bindClient(clientId: string): void;
42
+ releaseClient(clientId: string): void;
43
+ setTurnAttachments(clientId: string, attachments: AgentAttachment[]): {
44
+ id: string;
45
+ name: string;
46
+ type: string;
47
+ size: number;
48
+ width: number;
49
+ height: number;
50
+ }[];
51
+ clearTurnAttachments(clientId?: string): void;
52
+ getTurnAttachment(clientId: string, attachmentId: string): TurnAttachment;
53
+ resolveResult(clientId: string, body: {
54
+ requestId?: string;
55
+ error?: string;
56
+ result?: unknown;
57
+ }): boolean;
58
+ emitAll(type: string, payload: unknown): void;
59
+ emitThread(type: string, threadId: string, payload?: Record<string, unknown>): void;
60
+ callTool(name: unknown, rawInput: unknown): Promise<unknown>;
61
+ private createAttachmentNodes;
62
+ private requestCanvasTool;
63
+ }
64
+ export {};