@tigerowo/canvas-agent 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.
Files changed (4) hide show
  1. package/README.md +64 -0
  2. package/codex.mjs +91 -0
  3. package/index.mjs +310 -0
  4. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Canvas Agent
2
+
3
+ 网页对话通过本地服务运行 Codex,插件通过同一个 MCP 操作画布。工具定义、节点操作和生成流程继续使用网页现有实现。
4
+
5
+ ## 启动
6
+
7
+ 安装 Node.js 22+ 后,在终端执行,npx 会从 npm 获取服务及依赖:
8
+
9
+ ```sh
10
+ npx -y @tigerowo/canvas-agent@latest
11
+ ```
12
+
13
+ 首次启动自动生成 Token,保存在当前用户的 `~/.infinite-canvas/codex-agent.json`;下次启动复用。终端显示 `Local URL` 和 `Connect token`,默认地址为 `http://127.0.0.1:3210`。网页与服务在同一台电脑运行,不需要克隆仓库或手工创建 `.env`;正确 Token 连接后自动记录网站来源。
14
+
15
+ Codex 尚未登录时执行 `npx -y @openai/codex@0.153.4 login` 并完成登录,已有登录态会直接复用。
16
+
17
+ ## 插件自动连接
18
+
19
+ 在 Codex 所在电脑执行:
20
+
21
+ ```sh
22
+ codex plugin marketplace add https://github.com/tigerowo/infinite-canvas.git
23
+ codex plugin add infinite-canvas@infinite-canvas-local
24
+ ```
25
+
26
+ 安装后新建 Codex 对话,说“帮我打开并连接到 Infinite Canvas”。插件优先使用当前对话提供的画布地址或原画布标签;无法确定站点时再询问地址。
27
+
28
+ 插件市场定义在仓库根目录的 `.agents/plugins/marketplace.json`,指向仓库内的 `canvas-agent` 插件目录。市场名 `infinite-canvas-local` 仅为安装标识;插件由 GitHub 获取,MCP 和连接 Skill 通过 npx 使用 npm 上的服务,无需进入插件缓存安装依赖。
29
+
30
+ 插件会启动服务、自动带入连接信息,并通过 MCP 确认目标画布。已有画布优先复用原浏览器标签;标签不可控时使用 `npx -y @tigerowo/canvas-agent@latest open "<画布URL>" <浏览器> [浏览器参数...]`。浏览器支持 `chrome`、`edge`、`firefox`、`brave` 和 `default`,保持原浏览器及配置;画布地址沿用实际的 `/canvas/[id]` 路由。浏览器的本地网络权限提示由用户允许。
31
+
32
+ 移除插件执行 `codex plugin remove infinite-canvas`。
33
+
34
+ ## 直接注册 MCP
35
+
36
+ 与插件安装二选一:
37
+
38
+ ```sh
39
+ codex mcp add infinite-canvas -- npx -y @tigerowo/canvas-agent@latest mcp
40
+ ```
41
+
42
+ 直接注册 MCP 不包含自动打开 Skill,需要先启动服务并连接画布。移除时执行 `codex mcp remove infinite-canvas`。
43
+
44
+ ## 发布
45
+
46
+ `canvas-agent/package.json` 使用独立版本号。npm 包名为 `@tigerowo/canvas-agent`,命令入口为 `index.mjs`;运行文件直接发布,无需编译。使用对 `@tigerowo` 有发布权限的 npm 账号,在项目根目录执行:
47
+
48
+ ```sh
49
+ cd canvas-agent
50
+ npm login
51
+ npm publish --access public
52
+ ```
53
+
54
+ 后续发布前更新包版本。当前仓库未配置自动发布 npm;发布 npm 包并将插件配置推送到 GitHub 后,对外安装命令才可使用。
55
+
56
+ ## 本机配置与范围
57
+
58
+ `npx -y @tigerowo/canvas-agent@latest config` 输出 `{url, token, origins}`,首次尚未启动时会提示先启动服务。HTTP `GET /config` 只返回 `{url, hasToken}`;Token 和包含它的连接链接不要分享。
59
+
60
+ `CANVAS_AGENT_TOKEN`、`CANVAS_AGENT_PORT`、`CANVAS_AGENT_ORIGINS` 可选环境变量覆盖 Token、端口和预置来源;更改后重启服务。通常无需设置。
61
+
62
+ 外部 MCP 默认只操作唯一已连接的画布;多个画布连接时,通过 MCP 环境变量 `CANVAS_AGENT_CLIENT_ID` 明确目标。断开连接会结束对应 Codex 进程,已提交的生成任务仍由现有画布流程处理。
63
+
64
+ 默认权限为 `workspace-write` 与 `on-request`,工作目录为实际 Agent 包目录。服务不写 Codex 全局配置文件;其他全局 MCP 是否合并到当前会话尚未联调确认。
package/codex.mjs ADDED
@@ -0,0 +1,91 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const binary = join(dirname(require.resolve("@openai/codex/package.json")), "bin/codex.js");
8
+
9
+ export class CodexClient {
10
+ pending = new Map();
11
+ approvals = new Set();
12
+ nextId = 0;
13
+ stopped = false;
14
+
15
+ constructor(emit) {
16
+ this.emit = emit;
17
+ this.child = spawn(process.execPath, [binary, "app-server"], {
18
+ stdio: ["pipe", "pipe", "pipe"],
19
+ windowsHide: true,
20
+ });
21
+ createInterface({ input: this.child.stdout }).on("line", (line) => {
22
+ try {
23
+ const message = JSON.parse(line);
24
+ if (message.method) {
25
+ if (message.id !== undefined) this.approvals.add(message.id);
26
+ if (message.method === "serverRequest/resolved") this.approvals.delete(message.params.requestId);
27
+ this.emit("rpc", message);
28
+ } else {
29
+ const pending = this.pending.get(message.id);
30
+ if (!pending) return;
31
+ clearTimeout(pending.timer);
32
+ this.pending.delete(message.id);
33
+ if (message.error) pending.reject(new Error(message.error.message));
34
+ else pending.resolve(message.result);
35
+ }
36
+ } catch (error) {
37
+ this.close(new Error("Codex 输出解析失败:" + error.message));
38
+ }
39
+ });
40
+ this.child.stderr.resume();
41
+ this.child.stdin.on("error", (error) => this.close(error));
42
+ this.child.on("error", (error) => this.close(error));
43
+ this.child.on("exit", (code) => this.close(new Error("Codex 已退出:" + code)));
44
+ this.ready = this.request("initialize", {
45
+ clientInfo: { name: "infinite-canvas", title: "Infinite Canvas", version: "0.1.0" },
46
+ capabilities: { experimentalApi: true, requestAttestation: false },
47
+ }).then(() => this.write({ method: "initialized" }));
48
+ }
49
+
50
+ write(message) {
51
+ if (this.stopped) throw new Error("Codex 已断开");
52
+ this.child.stdin.write(JSON.stringify(message) + "\n");
53
+ }
54
+
55
+ request(method, params) {
56
+ return new Promise((resolve, reject) => {
57
+ const id = ++this.nextId;
58
+ const timer = setTimeout(() => {
59
+ this.close(new Error("Codex 请求超时:" + method));
60
+ }, 90000);
61
+ this.pending.set(id, { resolve, reject, timer });
62
+ try { this.write({ id, method, params }); }
63
+ catch (error) {
64
+ clearTimeout(timer);
65
+ this.pending.delete(id);
66
+ reject(error);
67
+ }
68
+ });
69
+ }
70
+
71
+ reply(id, result, error) {
72
+ if (!this.approvals.delete(id)) return;
73
+ this.write(error ? { id, error } : { id, result });
74
+ }
75
+
76
+ close(error = new Error("画布连接已关闭")) {
77
+ if (this.stopped) return;
78
+ this.stopped = true;
79
+ for (const pending of this.pending.values()) {
80
+ clearTimeout(pending.timer);
81
+ pending.reject(error);
82
+ }
83
+ this.pending.clear();
84
+ this.approvals.clear();
85
+ if (process.platform === "win32" && this.child.pid && this.child.exitCode === null && this.child.signalCode === null) {
86
+ spawn("taskkill", ["/PID", String(this.child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" })
87
+ .on("error", () => this.child.kill());
88
+ } else this.child.kill();
89
+ this.emit("rpc", { method: "bridge/disconnected", params: { message: error.message } });
90
+ }
91
+ }
package/index.mjs ADDED
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env node
2
+ import express from "express";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { fileURLToPath } from "node:url";
7
+ import { randomBytes, randomUUID } from "node:crypto";
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
11
+ import { CodexClient } from "./codex.mjs";
12
+
13
+ const entry = fileURLToPath(import.meta.url);
14
+ const configPath = resolve(homedir(), ".infinite-canvas", "codex-agent.json");
15
+ const readConfig = () => existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : null;
16
+ const savedConfig = readConfig();
17
+ const serviceId = savedConfig?.serviceId || randomUUID();
18
+ const mode = process.argv[2];
19
+ if (!savedConfig && ["config", "open"].includes(mode)) throw new Error("请先启动本地 Canvas Agent 服务");
20
+ const token = process.env.CANVAS_AGENT_TOKEN || savedConfig?.token || (mode === "mcp" ? "" : randomBytes(32).toString("hex"));
21
+ if (mode !== "mcp" && (typeof token !== "string" || token.length < 32)) throw new Error("本地连接 Token 至少需要 32 位字符");
22
+ const port = Number(process.env.CANVAS_AGENT_PORT || savedConfig?.port || 3210);
23
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("本地服务端口无效");
24
+ const endpoint = "http://127.0.0.1:" + port;
25
+ const workspace = dirname(entry);
26
+ const origins = new Set(process.env.CANVAS_AGENT_ORIGINS?.split(",").map((value) => value.trim()).filter(Boolean) || savedConfig?.origins || []);
27
+ const sessions = new Map();
28
+ const methods = new Set(["model/list", "thread/start", "thread/resume", "thread/archive", "turn/start", "turn/interrupt", "bridge/stop"]);
29
+
30
+ async function openCanvas() {
31
+ const target = URL.canParse(process.argv[3] || "") ? new URL(process.argv[3]) : null;
32
+ if (!target || !["http:", "https:"].includes(target.protocol) || target.username || target.password || !/^\/canvas\/[^/]+\/?$/.test(target.pathname)) throw new Error("请提供实际画布的 HTTP(S) 地址");
33
+ const browser = process.argv[4];
34
+ if (!["chrome", "edge", "firefox", "brave", "default"].includes(browser)) throw new Error("请指定原画布浏览器:chrome、edge、firefox、brave 或 default");
35
+ const params = new URLSearchParams(target.hash.slice(1));
36
+ params.set("agentUrl", endpoint);
37
+ params.set("agentToken", token);
38
+ target.hash = params.toString();
39
+ const { default: open, apps } = await import("open");
40
+ try {
41
+ await open(target.href, { app: { name: browser === "default" ? apps.browser : apps[browser], arguments: process.argv.slice(5) } });
42
+ } catch (error) {
43
+ throw new Error("打开浏览器失败,请检查原浏览器、配置和启动权限" + (error.code ? ":" + error.code : ""));
44
+ }
45
+ console.log("已请求浏览器打开原画布,等待 MCP 确认连接");
46
+ }
47
+
48
+ function saveConfig() {
49
+ mkdirSync(dirname(configPath), { recursive: true });
50
+ writeFileSync(configPath, JSON.stringify({ url: endpoint, port, token, serviceId, origins: [...origins] }, null, 2) + "\n", { mode: 0o600 });
51
+ }
52
+
53
+ function findSession(clientId) {
54
+ if (!clientId) {
55
+ const connected = [...sessions.values()].filter((session) => session.events && session.canvasId);
56
+ if (connected.length !== 1) throw new Error("请只连接一个画布,或为 MCP 设置 CANVAS_AGENT_CLIENT_ID");
57
+ return connected[0];
58
+ }
59
+ const session = sessions.get(clientId);
60
+ if (!session) throw new Error("画布尚未连接");
61
+ return session;
62
+ }
63
+
64
+ function emit(session, event, data) {
65
+ session.events?.write("event: " + event + "\ndata: " + JSON.stringify(data) + "\n\n");
66
+ }
67
+
68
+ function closeSession(session) {
69
+ if (sessions.get(session.clientId) === session) sessions.delete(session.clientId);
70
+ for (const pending of session.toolsPending.values()) {
71
+ clearTimeout(pending.timer);
72
+ pending.reject(new Error("画布已断开,工具未完成"));
73
+ }
74
+ session.toolsPending.clear();
75
+ const events = session.events;
76
+ session.events = undefined;
77
+ session.codex?.close();
78
+ events?.end();
79
+ }
80
+
81
+ function callTool(session, name, args, source, generation, res) {
82
+ if (!session.events || !session.tools.some((tool) => tool.name === name)) throw new Error("画布未连接或工具不存在");
83
+ if (source === "codex" && (generation !== session.codexGeneration || !session.codex || session.codex.stopped)) throw new Error("Codex 请求已停止");
84
+ if (res.destroyed) throw new Error("画布工具请求已取消");
85
+ const requestId = randomUUID();
86
+ const cancel = (message) => {
87
+ const pending = session.toolsPending.get(requestId);
88
+ if (!pending) return;
89
+ clearTimeout(pending.timer);
90
+ session.toolsPending.delete(requestId);
91
+ emit(session, "tool-cancel", { requestId });
92
+ pending.reject(new Error(message));
93
+ };
94
+ const onClose = () => { if (!res.writableEnded) cancel("画布工具请求已取消"); };
95
+ res.once("close", onClose);
96
+ return new Promise((resolve, reject) => {
97
+ const timer = setTimeout(() => cancel("画布工具执行超时"), 90000);
98
+ session.toolsPending.set(requestId, { resolve, reject, timer, source, cancel });
99
+ emit(session, "tool", { requestId, name, arguments: args, source, generation });
100
+ }).finally(() => res.off("close", onClose));
101
+ }
102
+
103
+ async function runRpc(session, method, params = {}, generation) {
104
+ if (!methods.has(method)) throw new Error("不支持的 Codex 方法");
105
+ if (!session.events || !session.canvasId) throw new Error("请先连接画布并注册工具");
106
+ if (!Number.isSafeInteger(generation) || generation < session.codexGeneration) throw new Error("Codex 请求已停止");
107
+ if (generation > session.codexGeneration) {
108
+ session.codexGeneration = generation;
109
+ session.codex?.close();
110
+ }
111
+ if (method === "bridge/stop") return { ok: true };
112
+ if (!session.codex || session.codex.stopped) session.codex = new CodexClient((event, data) => {
113
+ if (data.method === "bridge/disconnected") {
114
+ if (generation === session.codexGeneration) session.codexGeneration += 1;
115
+ for (const pending of session.toolsPending.values()) if (pending.source === "codex") pending.cancel("Codex 请求已停止");
116
+ }
117
+ emit(session, event, { ...data, generation });
118
+ });
119
+ const codex = session.codex;
120
+ await codex.ready;
121
+ if (method === "thread/start" || method === "thread/resume") {
122
+ params = {
123
+ ...params,
124
+ cwd: workspace,
125
+ approvalPolicy: "on-request",
126
+ sandbox: "workspace-write",
127
+ config: { mcp_servers: { "infinite-canvas": {
128
+ command: process.execPath,
129
+ args: [entry, "mcp"],
130
+ default_tools_approval_mode: "approve",
131
+ env: { CANVAS_AGENT_TOKEN: token, CANVAS_AGENT_PORT: String(port), CANVAS_AGENT_CLIENT_ID: session.clientId, CANVAS_AGENT_SOURCE: "codex", CANVAS_AGENT_CODEX_GENERATION: String(generation) },
132
+ startup_timeout_sec: 20,
133
+ tool_timeout_sec: 90,
134
+ } } },
135
+ };
136
+ }
137
+ if (method === "turn/start") {
138
+ params = { ...params, approvalPolicy: "on-request", sandboxPolicy: { type: "workspaceWrite", networkAccess: false } };
139
+ }
140
+ return codex.request(method, params);
141
+ }
142
+
143
+ async function startMcp() {
144
+ const clientId = process.env.CANVAS_AGENT_CLIENT_ID || "";
145
+ const source = process.env.CANVAS_AGENT_SOURCE === "codex" ? "codex" : "external";
146
+ const generation = Number(process.env.CANVAS_AGENT_CODEX_GENERATION || 0);
147
+ async function request(path, body, signal) {
148
+ const config = readConfig();
149
+ const requestToken = process.env.CANVAS_AGENT_TOKEN || config?.token;
150
+ if (!requestToken) throw new Error("请先启动本地 Canvas Agent 服务");
151
+ const url = "http://127.0.0.1:" + (process.env.CANVAS_AGENT_PORT || config?.port || 3210);
152
+ const response = await fetch(url + path, {
153
+ method: body ? "POST" : "GET",
154
+ headers: { "Content-Type": "application/json", "x-canvas-agent-token": requestToken },
155
+ body: body ? JSON.stringify({ ...body, clientId, source, generation }) : undefined,
156
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(95000)]) : AbortSignal.timeout(95000),
157
+ });
158
+ const value = await response.json();
159
+ if (!response.ok) throw new Error(value.error || "画布连接失败");
160
+ return value;
161
+ }
162
+ const server = new Server({ name: "infinite-canvas", version: "0.1.0" }, { capabilities: { tools: { listChanged: true } } });
163
+ const toolsPath = "/tools?clientId=" + encodeURIComponent(clientId);
164
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
165
+ try {
166
+ const tools = await request(toolsPath);
167
+ return { tools: source === "codex" ? tools : tools.filter(({ name }) => name !== "set_agent_state" && name !== "read_skill_file") };
168
+ } catch { return { tools: [] }; }
169
+ });
170
+ server.setRequestHandler(CallToolRequestSchema, async ({ params }, { signal }) => {
171
+ try {
172
+ const result = await request("/tools/call", { name: params.name, arguments: params.arguments || {} }, signal);
173
+ return { isError: result?.ok === false, content: [{ type: "text", text: JSON.stringify(result) }] };
174
+ } catch (error) {
175
+ return { isError: true, content: [{ type: "text", text: error.message }] };
176
+ }
177
+ });
178
+ let retry;
179
+ let closed = false;
180
+ async function announceTools() {
181
+ try {
182
+ const tools = await request(toolsPath);
183
+ if (!tools.length) throw new Error("等待画布连接");
184
+ if (!closed) await server.notification({ method: "notifications/tools/list_changed" });
185
+ } catch {
186
+ if (!closed) retry = setTimeout(announceTools, 2000);
187
+ }
188
+ }
189
+ server.oninitialized = () => { void announceTools(); };
190
+ server.onclose = () => { closed = true; clearTimeout(retry); };
191
+ await server.connect(new StdioServerTransport());
192
+ }
193
+
194
+ function startHttp() {
195
+ const app = express();
196
+ app.use((req, res, next) => {
197
+ if (!["127.0.0.1:" + port, "localhost:" + port].includes(req.headers.host)) return res.status(403).json({ error: "无效的本地地址" });
198
+ const origin = req.headers.origin;
199
+ if (origin) {
200
+ res.setHeader("Access-Control-Allow-Origin", origin);
201
+ res.setHeader("Vary", "Origin");
202
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, x-canvas-agent-token");
203
+ res.setHeader("Access-Control-Expose-Headers", "x-canvas-agent-generation");
204
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
205
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
206
+ }
207
+ if (req.method === "OPTIONS") return res.sendStatus(204);
208
+ if (req.method === "GET" && req.path === "/config") return res.json({ url: endpoint, hasToken: Boolean(token) });
209
+ const credential = req.path === "/events" ? req.query.token : req.headers["x-canvas-agent-token"];
210
+ if (credential !== token) return res.status(401).json({ error: "Token 不正确" });
211
+ if (origin && !origins.has(origin)) {
212
+ origins.add(origin);
213
+ saveConfig();
214
+ }
215
+ next();
216
+ });
217
+ app.use(express.json({ limit: "20mb" }));
218
+ app.post("/connect", (req, res) => {
219
+ const { clientId, canvasId, tools } = req.body;
220
+ if (typeof clientId !== "string" || !clientId || typeof canvasId !== "string" || !canvasId || !Array.isArray(tools)) {
221
+ return res.status(400).json({ error: "连接参数不完整" });
222
+ }
223
+ const session = findSession(clientId);
224
+ if ((session.canvasId && session.canvasId !== canvasId) || session.origin !== req.headers.origin) {
225
+ return res.status(409).json({ error: "连接标识已被其他画布使用" });
226
+ }
227
+ session.canvasId = canvasId;
228
+ session.tools = tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
229
+ res.json({ serviceId });
230
+ });
231
+ app.get("/events", (req, res) => {
232
+ const clientId = req.query.clientId;
233
+ if (typeof clientId !== "string" || !clientId) return res.status(400).json({ error: "缺少连接标识" });
234
+ let session = sessions.get(clientId);
235
+ if (!session) {
236
+ session = { clientId, origin: req.headers.origin, tools: [], toolsPending: new Map(), codexGeneration: 0 };
237
+ sessions.set(clientId, session);
238
+ }
239
+ if (session.origin !== req.headers.origin) return res.status(403).json({ error: "连接来源不匹配" });
240
+ const previous = session.events;
241
+ session.events = res;
242
+ previous?.end();
243
+ res.setHeader("Content-Type", "text/event-stream");
244
+ res.setHeader("Cache-Control", "no-cache");
245
+ res.setHeader("Connection", "keep-alive");
246
+ res.flushHeaders();
247
+ res.write(": connected\n\n");
248
+ const heartbeat = setInterval(() => res.write(": ping\n\n"), 15000);
249
+ res.on("close", () => {
250
+ clearInterval(heartbeat);
251
+ if (session.events === res) closeSession(session);
252
+ });
253
+ });
254
+ app.post("/rpc", async (req, res) => {
255
+ const { clientId, method, params, generation } = req.body;
256
+ const session = findSession(clientId);
257
+ res.json(await runRpc(session, method, params, generation).finally(() => res.setHeader("x-canvas-agent-generation", String(session.codexGeneration))));
258
+ });
259
+ app.post("/reply", (req, res) => {
260
+ const { clientId, id, result, error, generation } = req.body;
261
+ const session = findSession(clientId);
262
+ if (generation === session.codexGeneration) session.codex?.reply(id, result, error);
263
+ res.json({ ok: true });
264
+ });
265
+ app.post("/result", (req, res) => {
266
+ const { clientId, requestId, result, error } = req.body;
267
+ const session = findSession(clientId);
268
+ const pending = session.toolsPending.get(requestId);
269
+ if (!pending) return res.status(409).json({ error: "工具请求已结束" });
270
+ clearTimeout(pending.timer);
271
+ session.toolsPending.delete(requestId);
272
+ if (error) pending.reject(new Error(error));
273
+ else pending.resolve(result);
274
+ res.json({ ok: true });
275
+ });
276
+ app.get("/tools", (req, res) => {
277
+ const session = findSession(req.query.clientId);
278
+ if (!session.canvasId) throw new Error("请先在网页连接画布");
279
+ res.json(session.tools);
280
+ });
281
+ app.post("/tools/call", async (req, res) => {
282
+ const { clientId, name, arguments: args, source, generation } = req.body;
283
+ res.json(await callTool(findSession(clientId), name, args, source === "codex" ? "codex" : "external", generation, res));
284
+ });
285
+ app.use((error, req, res, next) => {
286
+ if (res.destroyed) return;
287
+ if (res.headersSent) return next(error);
288
+ res.status(400).json({ error: error.message });
289
+ });
290
+ const server = app.listen(port, "127.0.0.1", () => {
291
+ saveConfig();
292
+ console.log("Local URL: " + endpoint);
293
+ console.log("Connect token: " + token);
294
+ });
295
+ server.on("error", (error) => {
296
+ console.error("Canvas Agent 启动失败:" + error.message);
297
+ process.exitCode = 1;
298
+ });
299
+ function shutdown() {
300
+ for (const session of sessions.values()) closeSession(session);
301
+ server.close();
302
+ }
303
+ process.on("SIGINT", shutdown);
304
+ process.on("SIGTERM", shutdown);
305
+ }
306
+
307
+ if (mode === "config") console.log(JSON.stringify({ url: endpoint, token, origins: [...origins] }));
308
+ else if (mode === "open") await openCanvas();
309
+ else if (mode === "mcp") await startMcp();
310
+ else startHttp();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@tigerowo/canvas-agent",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "canvas-agent": "index.mjs"
7
+ },
8
+ "files": ["index.mjs", "codex.mjs"],
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/tigerowo/infinite-canvas.git",
15
+ "directory": "canvas-agent"
16
+ },
17
+ "scripts": {
18
+ "start": "node index.mjs",
19
+ "codex": "codex"
20
+ },
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.12.1",
26
+ "@openai/codex": "0.153.4",
27
+ "express": "^5.1.0",
28
+ "open": "^11.0.1"
29
+ }
30
+ }