@xiaohhhh1/canvas-agent 0.2.2 → 0.3.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 (42) hide show
  1. package/README.md +9 -1
  2. package/agent-instructions.md +21 -0
  3. package/dist/agent/claude.d.ts +3 -0
  4. package/dist/agent/claude.js +46 -0
  5. package/dist/agent/codex-client.d.ts +73 -0
  6. package/dist/agent/codex-client.js +438 -0
  7. package/dist/agent/codex-history.d.ts +25 -0
  8. package/dist/agent/codex-history.js +405 -0
  9. package/dist/agent/codex-protocol.d.ts +208 -0
  10. package/dist/{agents.d.ts → agent/codex.d.ts} +34 -31
  11. package/dist/agent/codex.js +210 -0
  12. package/dist/agent/types.d.ts +14 -0
  13. package/dist/agent/types.js +1 -0
  14. package/dist/canvas/operations.d.ts +13 -0
  15. package/dist/canvas/operations.js +161 -0
  16. package/dist/{schemas.d.ts → canvas/schemas.d.ts} +21 -20
  17. package/dist/{schemas.js → canvas/schemas.js} +1 -0
  18. package/dist/{canvas-session.d.ts → canvas/session.d.ts} +21 -1
  19. package/dist/canvas/session.js +256 -0
  20. package/dist/{tools.d.ts → canvas/tools.d.ts} +13 -8
  21. package/dist/{tools.js → canvas/tools.js} +5 -0
  22. package/dist/{types.d.ts → canvas/types.d.ts} +1 -10
  23. package/dist/canvas/types.js +1 -0
  24. package/dist/config.d.ts +5 -1
  25. package/dist/config.js +22 -4
  26. package/dist/index.js +2 -2
  27. package/dist/server/http.d.ts +2 -0
  28. package/dist/{http-server.js → server/http.js} +100 -16
  29. package/dist/server/mcp.d.ts +2 -0
  30. package/dist/{mcp-server.js → server/mcp.js} +5 -2
  31. package/dist/utils/date.d.ts +2 -0
  32. package/dist/utils/date.js +7 -0
  33. package/dist/utils/logger.d.ts +17 -0
  34. package/dist/utils/logger.js +83 -0
  35. package/dist/utils/value.d.ts +5 -0
  36. package/dist/utils/value.js +8 -0
  37. package/package.json +7 -4
  38. package/dist/agents.js +0 -557
  39. package/dist/canvas-session.js +0 -391
  40. package/dist/http-server.d.ts +0 -1
  41. package/dist/mcp-server.d.ts +0 -1
  42. /package/dist/{types.js → agent/codex-protocol.js} +0 -0
@@ -1,19 +1,27 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import path from "node:path";
1
4
  import express from "express";
2
- import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace } from "./config.js";
3
- import { CanvasSession } from "./canvas-session.js";
4
- import { startRelayBridge } from "./relay-bridge.js";
5
- import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resumeCodexThread, runClaudeTurn, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace, withAgentPrompt } from "./agents.js";
5
+ import { runClaudeTurn } from "../agent/claude.js";
6
+ import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resolveCodexApproval, resumeCodexThread, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace } from "../agent/codex.js";
7
+ import { CanvasSession } from "../canvas/session.js";
8
+ import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace } from "../config.js";
9
+ import { startRelayBridge } from "../relay-bridge.js";
10
+ import { logger } from "../utils/logger.js";
11
+ /** 启动仅监听本机的 Canvas Agent HTTP 服务。 */
6
12
  export function startHttpServer() {
7
13
  const config = loadConfig(true);
8
14
  const port = Number(process.env.PORT) || Number(new URL(config.url).port) || DEFAULT_PORT;
9
15
  config.url = `http://127.0.0.1:${port}`;
10
16
  saveConfig(config);
11
17
  const session = new CanvasSession();
18
+ /** 将 Agent 事件广播到所属线程或全部网页。 */
12
19
  const emit = (type, payload) => {
13
20
  const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { value: payload };
14
21
  const threadId = String(data.threadId || data.thread_id || ensureSiteWorkspace(config).activeThreadId || "");
15
22
  threadId ? session.emitThread(type, threadId, data) : session.emitAll(type, data);
16
23
  };
24
+ /** 保存并广播当前站点工作空间的活跃线程。 */
17
25
  const setActiveThread = (activeThreadId, payload = {}) => {
18
26
  const workspace = updateSiteWorkspace(config, { activeThreadId: activeThreadId || undefined });
19
27
  session.emitThread("workspace_changed", activeThreadId, { ...payload, activeThreadId });
@@ -22,6 +30,18 @@ export function startHttpServer() {
22
30
  const app = express();
23
31
  app.disable("x-powered-by");
24
32
  app.use(express.json({ limit: "30mb" }));
33
+ app.use((req, res, next) => {
34
+ if (!logger.enabled)
35
+ return next();
36
+ const startedAt = Date.now();
37
+ const url = requestUrl(req, config);
38
+ res.on("finish", () => {
39
+ if (req.method === "OPTIONS" || (res.statusCode < 400 && ["/health", "/canvas/state", "/canvas/activate"].includes(url.pathname)))
40
+ return;
41
+ logger.debug(`HTTP ${req.method} ${url.pathname}`, { status: res.statusCode, durationMs: Date.now() - startedAt });
42
+ });
43
+ next();
44
+ });
25
45
  app.use((req, res, next) => {
26
46
  const url = requestUrl(req, config);
27
47
  if (!setCors(req, res, url, config))
@@ -58,6 +78,24 @@ export function startHttpServer() {
58
78
  res.setHeader("Cache-Control", "no-store");
59
79
  res.type(attachment.type).send(Buffer.from(data, "base64"));
60
80
  }));
81
+ app.post("/agent/local-file/reveal", route(async (req, res) => {
82
+ const filePath = String(req.body?.path || "");
83
+ if (!path.isAbsolute(filePath))
84
+ return res.status(400).json({ ok: false, error: "文件路径必须是绝对路径" });
85
+ const file = await stat(filePath);
86
+ await revealLocalFile(filePath, file.isDirectory());
87
+ res.json({ ok: true });
88
+ }));
89
+ app.post("/agent/local-image", route(async (req, res) => {
90
+ const filePath = String(req.body?.path || "");
91
+ if (!path.isAbsolute(filePath) || !/\.(?:avif|gif|jpe?g|png|webp)$/i.test(filePath))
92
+ return res.status(400).json({ ok: false, error: "图片路径无效" });
93
+ const file = await stat(filePath);
94
+ if (!file.isFile())
95
+ return res.status(400).json({ ok: false, error: "图片文件无效" });
96
+ res.setHeader("Cache-Control", "no-store");
97
+ res.type(path.extname(filePath)).send(await readFile(filePath));
98
+ }));
61
99
  app.post("/api/tools", route(async (req, res) => res.json({ ok: true, result: await session.callTool(req.body?.name, req.body?.input || {}) })));
62
100
  app.get("/agent/codex/workspace", (_req, res) => {
63
101
  const workspace = ensureSiteWorkspace(config);
@@ -68,15 +106,20 @@ export function startHttpServer() {
68
106
  const result = await listCodexThreads(emit, { cwd: workspace.workspacePath, searchTerm: String(req.query.searchTerm || "") });
69
107
  res.json({ ok: true, workspace, ...result });
70
108
  }));
71
- app.post("/agent/codex/threads/new", route(async (_req, res) => {
109
+ app.post("/agent/codex/threads/new", route(async (req, res) => {
72
110
  if (session.codexBusy)
73
111
  return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
74
112
  const workspace = ensureSiteWorkspace(config);
75
- const thread = await startCodexThread(emit, workspace.workspacePath);
113
+ const thread = await startCodexThread(emit, workspace.workspacePath, permissionMode(req.body?.permissionMode));
76
114
  const activeThreadId = String(thread.id || "");
77
115
  const nextWorkspace = setActiveThread(activeThreadId, { emptyThread: true });
78
116
  res.json({ ok: true, workspace: nextWorkspace, thread: summarizeCodexThread(thread), messages: [] });
79
117
  }));
118
+ app.post("/agent/codex/threads/reset", (req, res) => {
119
+ if (session.codexBusy)
120
+ return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
121
+ res.json({ ok: true, workspace: setActiveThread("", { emptyThread: true, draftThread: true }) });
122
+ });
80
123
  app.get("/agent/codex/threads/:threadId", route(async (req, res) => {
81
124
  const workspace = ensureSiteWorkspace(config);
82
125
  const threadId = routeParam(req.params.threadId);
@@ -94,7 +137,7 @@ export function startHttpServer() {
94
137
  return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" });
95
138
  const workspace = ensureSiteWorkspace(config);
96
139
  const threadId = routeParam(req.params.threadId);
97
- const result = await resumeCodexThread(emit, threadId, workspace.workspacePath);
140
+ const result = await resumeCodexThread(emit, threadId, workspace.workspacePath, permissionMode(req.body?.permissionMode));
98
141
  const nextWorkspace = setActiveThread(threadId);
99
142
  res.json({ ok: true, workspace: nextWorkspace, ...result });
100
143
  }));
@@ -116,12 +159,13 @@ export function startHttpServer() {
116
159
  if (!prompt.trim())
117
160
  return res.status(400).json({ ok: false, error: "请输入任务内容" });
118
161
  const clientId = String(req.body?.clientId || "");
162
+ logger.info("Codex turn accepted", { threadId: req.body?.threadId, promptLength: prompt.length, attachmentCount: attachments.length });
119
163
  session.setCodexState({ busy: true, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" });
120
164
  try {
121
165
  let threadId = String(req.body?.threadId || workspace.activeThreadId || "");
122
166
  let turnId = "";
123
167
  if (!threadId) {
124
- const thread = await startCodexThread(emit, workspace.workspacePath);
168
+ const thread = await startCodexThread(emit, workspace.workspacePath, permissionMode(req.body?.permissionMode));
125
169
  threadId = String(thread.id || "");
126
170
  setActiveThread(threadId, { emptyThread: true });
127
171
  }
@@ -135,13 +179,15 @@ export function startHttpServer() {
135
179
  message: { id: String(req.body?.messageId || Date.now()), role: "user", text: String(req.body?.messageText || prompt || `发送了 ${attachments.length} 张图片`) },
136
180
  };
137
181
  let chatThreadId = "";
182
+ /** 将当前 turn 事件固定广播到实际线程。 */
138
183
  const turnEmit = (type, payload) => {
139
184
  const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { value: payload };
140
- session.emitThread(type, threadId, data);
185
+ session.emitThread(type, threadId, { ...data, ...(turnId ? { turn_id: turnId } : {}) });
141
186
  };
142
- void runCodexTurn(withAgentPrompt(withAttachmentContext(prompt, attachmentRefs)), turnEmit, attachments, {
187
+ void runCodexTurn(withAttachmentContext(prompt, attachmentRefs), turnEmit, attachments, {
143
188
  threadId,
144
189
  cwd: workspace.workspacePath,
190
+ permissionMode: permissionMode(req.body?.permissionMode),
145
191
  appEmit: emit,
146
192
  onStart: clientId ? () => session.bindClient(clientId) : undefined,
147
193
  onThread: (actualThreadId) => {
@@ -157,9 +203,11 @@ export function startHttpServer() {
157
203
  },
158
204
  onTurn: (actualTurnId) => {
159
205
  turnId = actualTurnId;
206
+ logger.info("Codex turn started", { threadId, turnId });
160
207
  session.setCodexState({ busy: true, threadId, turnId });
161
208
  },
162
209
  onFinish: () => {
210
+ logger.info("Codex turn finished", { threadId, turnId });
163
211
  session.clearTurnAttachments(clientId);
164
212
  if (clientId)
165
213
  session.releaseClient(clientId);
@@ -173,16 +221,23 @@ export function startHttpServer() {
173
221
  throw error;
174
222
  }
175
223
  }));
176
- app.post("/agent/codex/interrupt", (req, res) => {
177
- const ok = interruptCodexTurn(String(req.body?.threadId || ""));
178
- res.json({ ok });
179
- });
224
+ app.post("/agent/codex/approval", route(async (req, res) => {
225
+ const decision = String(req.body?.decision || "");
226
+ if (!["accept", "acceptForSession", "decline", "cancel"].includes(decision))
227
+ return res.status(400).json({ ok: false, error: "无效的审批决定" });
228
+ const ok = await resolveCodexApproval(String(req.body?.requestId || ""), decision);
229
+ res.status(ok ? 200 : 409).json({ ok, ...(ok ? {} : { error: "审批请求已失效" }) });
230
+ }));
231
+ app.post("/agent/codex/interrupt", route(async (req, res) => res.json({ ok: await interruptCodexTurn(String(req.body?.threadId || "")) })));
180
232
  app.post("/agent/claude/turn", (req, res) => {
181
- runClaudeTurn(withAgentPrompt(String(req.body?.prompt || "")), emit);
233
+ runClaudeTurn(String(req.body?.prompt || ""), emit);
182
234
  res.json({ ok: true });
183
235
  });
184
236
  app.use((_req, res) => res.status(404).json({ ok: false, error: "not found" }));
185
- app.use((error, _req, res, _next) => res.status(500).json({ ok: false, error: error.message }));
237
+ app.use((error, req, res, _next) => {
238
+ logger.error("HTTP request failed", { method: req.method, path: req.path, error });
239
+ res.status(500).json({ ok: false, error: error.message });
240
+ });
186
241
  app.listen(port, "127.0.0.1", () => {
187
242
  console.log("Infinite Canvas Agent");
188
243
  console.log(`Local URL: ${config.url}`);
@@ -191,17 +246,44 @@ export function startHttpServer() {
191
246
  console.log("Optional MCP add: codex mcp add infinite-canvas -- npx -y @xiaohhhh1/canvas-agent mcp");
192
247
  console.log("Remove manually added MCP: codex mcp remove infinite-canvas");
193
248
  startRelayBridge(config);
249
+ if (logger.enabled)
250
+ console.log(`Debug log: ${logger.filePath}`);
251
+ logger.info("Canvas Agent started", { url: config.url, workspace: ensureSiteWorkspace(config).workspacePath, debugLog: logger.filePath });
194
252
  });
195
253
  }
254
+ /** 将异步 Express 路由异常交给统一错误处理中间件。 */
196
255
  function route(handler) {
197
256
  return (req, res, next) => void handler(req, res).catch(next);
198
257
  }
258
+ /** 从 Express 路由参数中读取单个字符串。 */
199
259
  function routeParam(value) {
200
260
  return Array.isArray(value) ? value[0] || "" : value;
201
261
  }
262
+ function permissionMode(value) {
263
+ return value === "automatic" || value === "full" ? value : "request";
264
+ }
265
+ /** 使用当前操作系统的文件管理器定位本地文件。 */
266
+ function revealLocalFile(filePath, isDirectory) {
267
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
268
+ const args = process.platform === "darwin"
269
+ ? ["-R", filePath]
270
+ : process.platform === "win32"
271
+ ? [isDirectory ? filePath : `/select,${filePath}`]
272
+ : [isDirectory ? filePath : path.dirname(filePath)];
273
+ return new Promise((resolve, reject) => {
274
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
275
+ child.once("spawn", () => {
276
+ child.unref();
277
+ resolve();
278
+ });
279
+ child.once("error", reject);
280
+ });
281
+ }
282
+ /** 结合服务配置解析当前请求 URL。 */
202
283
  function requestUrl(req, config) {
203
284
  return new URL(req.originalUrl || req.url || "/", config.url);
204
285
  }
286
+ /** 设置跨域响应头并记录通过 token 授权的来源。 */
205
287
  function setCors(req, res, url, config) {
206
288
  const origin = req.headers.origin;
207
289
  res.setHeader("Access-Control-Allow-Origin", origin || "*");
@@ -218,10 +300,12 @@ function setCors(req, res, url, config) {
218
300
  res.setHeader("Vary", "Origin");
219
301
  return config.origins.includes(origin);
220
302
  }
303
+ /** 校验请求查询参数或请求头中的连接 token。 */
221
304
  function validToken(req, url, token) {
222
305
  const header = req.headers["x-canvas-agent-token"];
223
306
  return url.searchParams.get("token") === token || header === token || (Array.isArray(header) && header.includes(token));
224
307
  }
308
+ /** 向 Agent 提示词追加本轮图片附件引用说明。 */
225
309
  function withAttachmentContext(prompt, attachments) {
226
310
  if (!attachments.length)
227
311
  return prompt;
@@ -0,0 +1,2 @@
1
+ /** 启动通过标准输入输出通信的 MCP 服务。 */
2
+ export declare function startMcpServer(): Promise<void>;
@@ -1,13 +1,15 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { AGENT_PROMPT, loadConfig, VERSION } from "./config.js";
4
- import { toolDescriptions, toolInputSchemas, toolNames } from "./schemas.js";
3
+ import { toolDescriptions, toolInputSchemas, toolNames } from "../canvas/schemas.js";
4
+ import { AGENT_PROMPT, loadConfig, VERSION } from "../config.js";
5
+ /** 启动通过标准输入输出通信的 MCP 服务。 */
5
6
  export async function startMcpServer() {
6
7
  const config = loadConfig(true);
7
8
  const server = new McpServer({ name: "canvas-agent", version: VERSION }, { instructions: AGENT_PROMPT });
8
9
  toolNames.forEach((name) => registerCanvasTool(server, config, name));
9
10
  await server.connect(new StdioServerTransport());
10
11
  }
12
+ /** 向 MCP Server 注册单个 Canvas Agent 工具。 */
11
13
  function registerCanvasTool(server, config, name) {
12
14
  const schema = toolInputSchemas[name];
13
15
  server.registerTool(name, { description: toolDescriptions[name], inputSchema: schema.shape }, async (input) => {
@@ -15,6 +17,7 @@ function registerCanvasTool(server, config, name) {
15
17
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
16
18
  });
17
19
  }
20
+ /** 将 MCP 工具调用转发到本地 Canvas Agent HTTP 服务。 */
18
21
  async function postCanvasAgentTool(config, name, input) {
19
22
  const res = await fetch(`${config.url}/api/tools`, { method: "POST", headers: { "content-type": "application/json", "x-canvas-agent-token": config.token }, body: JSON.stringify({ name, input }) });
20
23
  const body = (await res.json());
@@ -0,0 +1,2 @@
1
+ /** 将日期格式化为适合文件名使用的本地日期字符串。 */
2
+ export declare function formatDateForFilename(date?: Date): string;
@@ -0,0 +1,7 @@
1
+ /** 将日期格式化为适合文件名使用的本地日期字符串。 */
2
+ export function formatDateForFilename(date = new Date()) {
3
+ const year = date.getFullYear();
4
+ const month = String(date.getMonth() + 1).padStart(2, "0");
5
+ const day = String(date.getDate()).padStart(2, "0");
6
+ return `${year}-${month}-${day}`;
7
+ }
@@ -0,0 +1,17 @@
1
+ /** 管理 Canvas Agent 的终端与文件 Debug 日志。 */
2
+ export declare class Logger {
3
+ readonly enabled: boolean;
4
+ readonly filePath: string;
5
+ private readonly logger;
6
+ /** 根据命令行 Debug 参数初始化日志输出。 */
7
+ constructor();
8
+ /** 输出 Debug 级别日志。 */
9
+ debug(message: string, details?: unknown): void;
10
+ /** 输出 Info 级别日志。 */
11
+ info(message: string, details?: unknown): void;
12
+ /** 输出 Warn 级别日志。 */
13
+ warn(message: string, details?: unknown): void;
14
+ /** 输出 Error 级别日志。 */
15
+ error(message: string, details?: unknown): void;
16
+ }
17
+ export declare const logger: Logger;
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { inspect } from "node:util";
5
+ import winston, { format, transports } from "winston";
6
+ import { formatDateForFilename } from "./date.js";
7
+ /** 管理 Canvas Agent 的终端与文件 Debug 日志。 */
8
+ export class Logger {
9
+ enabled = process.argv.includes("--debug");
10
+ filePath = this.enabled ? path.join(os.homedir(), ".infinite-canvas", "logs", `canvas-agent-${formatDateForFilename()}.log`) : "";
11
+ logger;
12
+ /** 根据命令行 Debug 参数初始化日志输出。 */
13
+ constructor() {
14
+ if (!this.enabled) {
15
+ this.logger = null;
16
+ return;
17
+ }
18
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
19
+ const line = format.printf(({ level, message, timestamp, details }) => `${timestamp} ${level.toUpperCase()} ${message}${formatDetails(details)}`);
20
+ this.logger = winston.createLogger({
21
+ level: "debug",
22
+ transports: [
23
+ new transports.Console({ format: format.combine(format.timestamp({ format: "HH:mm:ss" }), line) }),
24
+ new transports.File({ filename: this.filePath, format: format.combine(format.timestamp({ format: "HH:mm:ss" }), line) }),
25
+ ],
26
+ });
27
+ }
28
+ /** 输出 Debug 级别日志。 */
29
+ debug(message, details) {
30
+ if (details === undefined)
31
+ this.logger?.debug(message);
32
+ else
33
+ this.logger?.debug(message, { details: sanitize(details) });
34
+ }
35
+ /** 输出 Info 级别日志。 */
36
+ info(message, details) {
37
+ if (details === undefined)
38
+ this.logger?.info(message);
39
+ else
40
+ this.logger?.info(message, { details: sanitize(details) });
41
+ }
42
+ /** 输出 Warn 级别日志。 */
43
+ warn(message, details) {
44
+ if (details === undefined)
45
+ this.logger?.warn(message);
46
+ else
47
+ this.logger?.warn(message, { details: sanitize(details) });
48
+ }
49
+ /** 输出 Error 级别日志。 */
50
+ error(message, details) {
51
+ if (details === undefined)
52
+ this.logger?.error(message);
53
+ else
54
+ this.logger?.error(message, { details: sanitize(details) });
55
+ }
56
+ }
57
+ /** 将日志详情格式化为紧凑的单行文本。 */
58
+ function formatDetails(details) {
59
+ if (details === undefined)
60
+ return "";
61
+ if (!details || typeof details !== "object" || Array.isArray(details))
62
+ return ` ${inspect(details, { depth: null, breakLength: Infinity })}`;
63
+ const text = Object.entries(details).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${inspect(value, { depth: null, breakLength: Infinity })}`).join(" ");
64
+ return text ? ` ${text}` : "";
65
+ }
66
+ /** 清理日志内容中的敏感数据和不可序列化引用。 */
67
+ function sanitize(value, key = "", seen = new WeakSet()) {
68
+ if (/token|authorization|api.?key|dataurl/i.test(key))
69
+ return "[REDACTED]";
70
+ if (typeof value === "string" && value.startsWith("data:"))
71
+ return `[DATA URL ${value.length} chars]`;
72
+ if (value instanceof Error)
73
+ return { name: value.name, message: value.message, stack: value.stack };
74
+ if (!value || typeof value !== "object")
75
+ return value;
76
+ if (seen.has(value))
77
+ return "[CIRCULAR]";
78
+ seen.add(value);
79
+ if (Array.isArray(value))
80
+ return value.map((item) => sanitize(item, key, seen));
81
+ return Object.fromEntries(Object.entries(value).map(([field, item]) => [field, sanitize(item, field, seen)]));
82
+ }
83
+ export const logger = new Logger();
@@ -0,0 +1,5 @@
1
+ export type JsonRecord = Record<string, unknown>;
2
+ /** 安全读取未知对象中的指定字段。 */
3
+ export declare function field(value: unknown, key: string): unknown;
4
+ /** 将未知异常转换为可展示的错误信息。 */
5
+ export declare function errorMessage(error: unknown): string;
@@ -0,0 +1,8 @@
1
+ /** 安全读取未知对象中的指定字段。 */
2
+ export function field(value, key) {
3
+ return value && typeof value === "object" ? value[key] : undefined;
4
+ }
5
+ /** 将未知异常转换为可展示的错误信息。 */
6
+ export function errorMessage(error) {
7
+ return error instanceof Error ? error.message : String(error);
8
+ }
package/package.json CHANGED
@@ -1,28 +1,31 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "bin": {
8
- "canvas-agent": "./dist/index.js"
8
+ "canvas-agent": "dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "agent-instructions.md",
12
13
  "README.md"
13
14
  ],
14
15
  "scripts": {
15
16
  "dev": "tsx src/index.ts",
16
- "test": "tsx --test src/canvas-session.test.ts",
17
+ "debug": "tsx src/index.ts --debug",
18
+ "test": "tsx --test src/canvas/session.test.ts",
17
19
  "build": "tsc -p tsconfig.json",
18
20
  "start": "node dist/index.js",
19
21
  "prepack": "npm run build"
20
22
  },
21
23
  "dependencies": {
22
24
  "@modelcontextprotocol/sdk": "^1.12.1",
23
- "@openai/codex": "^0.144.2",
25
+ "@openai/codex": "0.145.0",
24
26
  "express": "^5.1.0",
25
27
  "ws": "^8.18.3",
28
+ "winston": "^3.19.0",
26
29
  "zod": "^3.25.0"
27
30
  },
28
31
  "devDependencies": {