qcanvas-local-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.
@@ -0,0 +1,275 @@
1
+ import http from "node:http";
2
+ import { CONFIG_FILE, ensureCanvasWorkspace, loadConfig, rememberOrigin, saveConfig, setCanvasActiveThread, VERSION, } from "./config.js";
3
+ import { CanvasSession } from "./canvas-session.js";
4
+ import { archiveCodexThread, listCodexThreads, LOCAL_AGENTS, readCodexThread, resumeCodexThread, runClaudeTurn, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace, withAgentPrompt, } from "./agents.js";
5
+ const MAX_BODY_BYTES = 30 * 1024 * 1024;
6
+ function readOrigin(req) {
7
+ const value = req.headers.origin;
8
+ return typeof value === "string" && value.trim() ? value.trim() : null;
9
+ }
10
+ function sendCors(res, origin) {
11
+ if (origin) {
12
+ res.setHeader("access-control-allow-origin", origin);
13
+ res.setHeader("vary", "origin");
14
+ }
15
+ res.setHeader("access-control-allow-methods", "GET,POST,OPTIONS");
16
+ res.setHeader("access-control-allow-headers", "content-type,x-qcanvas-agent-token,x-canvas-agent-token");
17
+ }
18
+ function sendJson(res, status, body) {
19
+ res.writeHead(status, { "content-type": "application/json" });
20
+ res.end(JSON.stringify(body));
21
+ }
22
+ function sendError(res, status, error) {
23
+ sendJson(res, status, { ok: false, error });
24
+ }
25
+ function readToken(req, url) {
26
+ const qToken = url.searchParams.get("token");
27
+ if (qToken && qToken.trim())
28
+ return qToken.trim();
29
+ const qcanvasHeader = req.headers["x-qcanvas-agent-token"];
30
+ if (typeof qcanvasHeader === "string" && qcanvasHeader.trim())
31
+ return qcanvasHeader.trim();
32
+ const legacyHeader = req.headers["x-canvas-agent-token"];
33
+ if (typeof legacyHeader === "string" && legacyHeader.trim())
34
+ return legacyHeader.trim();
35
+ return null;
36
+ }
37
+ function assertAuthorized(req, url, config) {
38
+ const token = readToken(req, url);
39
+ if (!token || token !== config.token)
40
+ throw new Error("invalid QCanvas local agent token");
41
+ }
42
+ function assertOrigin(config, origin) {
43
+ if (!origin || config.origins.length === 0 || config.origins.includes(origin))
44
+ return;
45
+ throw new Error(`origin is not allowed for this local agent: ${origin}`);
46
+ }
47
+ async function readBody(req) {
48
+ const chunks = [];
49
+ let total = 0;
50
+ for await (const chunk of req) {
51
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
52
+ total += buffer.byteLength;
53
+ if (total > MAX_BODY_BYTES)
54
+ throw new Error("request body is too large");
55
+ chunks.push(buffer);
56
+ }
57
+ const text = Buffer.concat(chunks).toString("utf8").trim();
58
+ if (!text)
59
+ return {};
60
+ return JSON.parse(text);
61
+ }
62
+ function readClientId(ctx, body) {
63
+ const fromQuery = ctx.url.searchParams.get("clientId");
64
+ if (fromQuery && fromQuery.trim())
65
+ return fromQuery.trim();
66
+ if (body && typeof body === "object" && !Array.isArray(body)) {
67
+ const fromBody = body.clientId;
68
+ if (typeof fromBody === "string" && fromBody.trim())
69
+ return fromBody.trim();
70
+ }
71
+ return null;
72
+ }
73
+ async function routeRequest(ctx, session, configRef) {
74
+ const { req, res, url, origin } = ctx;
75
+ sendCors(res, origin);
76
+ if (req.method === "OPTIONS") {
77
+ res.writeHead(204);
78
+ res.end();
79
+ return;
80
+ }
81
+ if (req.method === "GET" && url.pathname === "/health") {
82
+ sendJson(res, 200, {
83
+ ok: true,
84
+ version: VERSION,
85
+ configFile: CONFIG_FILE,
86
+ ...session.health(),
87
+ });
88
+ return;
89
+ }
90
+ if (req.method === "GET" && url.pathname === "/config") {
91
+ sendJson(res, 200, {
92
+ ok: true,
93
+ url: configRef.current.url,
94
+ hasToken: true,
95
+ configFile: CONFIG_FILE,
96
+ });
97
+ return;
98
+ }
99
+ assertAuthorized(req, url, configRef.current);
100
+ assertOrigin(configRef.current, origin);
101
+ configRef.current = rememberOrigin(configRef.current, origin);
102
+ if (req.method === "GET" && url.pathname === "/events") {
103
+ session.openEvents(url.searchParams.get("clientId"), res);
104
+ return;
105
+ }
106
+ if (req.method === "POST" && url.pathname === "/canvas/state") {
107
+ const body = await readBody(req);
108
+ const clientId = readClientId(ctx, body);
109
+ if (!clientId)
110
+ throw new Error("canvas state missing clientId");
111
+ const snapshot = session.updateState(body, clientId);
112
+ sendJson(res, 200, { ok: true, updatedAt: snapshot.updatedAt });
113
+ return;
114
+ }
115
+ if (req.method === "POST" && url.pathname === "/canvas/result") {
116
+ const body = await readBody(req);
117
+ session.resolveResult(body && typeof body === "object" && !Array.isArray(body) ? body : {});
118
+ sendJson(res, 200, { ok: true });
119
+ return;
120
+ }
121
+ if (req.method === "POST" && url.pathname === "/api/tools") {
122
+ const body = await readBody(req);
123
+ const record = body && typeof body === "object" && !Array.isArray(body) ? body : {};
124
+ const result = await session.callTool(record.name, record.input, readString(record.agent));
125
+ const envelope = { ok: true, result };
126
+ sendJson(res, 200, envelope);
127
+ return;
128
+ }
129
+ if (url.pathname.startsWith("/agent/")) {
130
+ await routeAgentRequest(ctx, session, configRef);
131
+ return;
132
+ }
133
+ sendError(res, 404, `unknown local agent route: ${req.method || "GET"} ${url.pathname}`);
134
+ }
135
+ function readRecordBody(body) {
136
+ return body && typeof body === "object" && !Array.isArray(body) ? body : {};
137
+ }
138
+ function readAttachments(value) {
139
+ return Array.isArray(value) ? value : [];
140
+ }
141
+ function readString(value) {
142
+ return typeof value === "string" ? value.trim() : "";
143
+ }
144
+ function isEmptySavedCodexThreadError(error) {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ return message.includes("no rollout found for thread id");
147
+ }
148
+ async function routeAgentRequest(ctx, session, configRef) {
149
+ const { req, res, url } = ctx;
150
+ const method = req.method || "GET";
151
+ const emit = (type, payload) => session.emitAll(type, payload);
152
+ const config = configRef.current;
153
+ if (method === "GET" && url.pathname === "/agent/list") {
154
+ sendJson(res, 200, { ok: true, agents: LOCAL_AGENTS });
155
+ return;
156
+ }
157
+ if (method === "GET" && url.pathname === "/agent/codex/workspace") {
158
+ const workspace = ensureCanvasWorkspace(config, url.searchParams.get("canvasId") || "");
159
+ sendJson(res, 200, { ok: true, workspace });
160
+ return;
161
+ }
162
+ if (method === "GET" && url.pathname === "/agent/codex/threads") {
163
+ const workspace = ensureCanvasWorkspace(config, url.searchParams.get("canvasId") || "");
164
+ const result = await listCodexThreads(emit, {
165
+ cwd: workspace.workspacePath,
166
+ searchTerm: url.searchParams.get("searchTerm") || "",
167
+ });
168
+ sendJson(res, 200, { ok: true, workspace, ...result });
169
+ return;
170
+ }
171
+ if (method === "POST" && url.pathname === "/agent/codex/threads/new") {
172
+ const body = readRecordBody(await readBody(req));
173
+ const workspace = ensureCanvasWorkspace(config, String(body.canvasId || ""));
174
+ const thread = await startCodexThread(emit, workspace.workspacePath);
175
+ const activeThreadId = String(thread.id || "");
176
+ const updated = setCanvasActiveThread(config, workspace.canvasId, "codex", activeThreadId);
177
+ sendJson(res, 200, { ok: true, workspace: updated, thread: summarizeCodexThread(thread), messages: [] });
178
+ return;
179
+ }
180
+ const threadMatch = url.pathname.match(/^\/agent\/codex\/threads\/([^/]+)(\/resume|\/delete)?$/);
181
+ if (threadMatch) {
182
+ const threadId = decodeURIComponent(threadMatch[1] || "");
183
+ const action = threadMatch[2] || "";
184
+ if (method === "GET" && !action) {
185
+ const workspace = ensureCanvasWorkspace(config, url.searchParams.get("canvasId") || "");
186
+ sendJson(res, 200, { ok: true, workspace, ...(await readCodexThread(emit, threadId, workspace.workspacePath)) });
187
+ return;
188
+ }
189
+ if (method === "POST" && action === "/resume") {
190
+ const body = readRecordBody(await readBody(req));
191
+ const workspace = ensureCanvasWorkspace(config, String(body.canvasId || ""));
192
+ const result = await resumeCodexThread(emit, threadId, workspace.workspacePath);
193
+ const updated = setCanvasActiveThread(config, workspace.canvasId, "codex", threadId);
194
+ sendJson(res, 200, { ok: true, workspace: updated, ...result });
195
+ return;
196
+ }
197
+ if (method === "POST" && action === "/delete") {
198
+ const body = readRecordBody(await readBody(req));
199
+ const workspace = ensureCanvasWorkspace(config, String(body.canvasId || ""));
200
+ await archiveCodexThread(emit, threadId, workspace.workspacePath);
201
+ if (workspace.activeThreadIds.codex === threadId)
202
+ setCanvasActiveThread(config, workspace.canvasId, "codex", "");
203
+ sendJson(res, 200, { ok: true });
204
+ return;
205
+ }
206
+ }
207
+ if (method === "POST" && url.pathname === "/agent/codex/turn") {
208
+ const body = readRecordBody(await readBody(req));
209
+ const attachments = readAttachments(body.attachments);
210
+ const workspace = ensureCanvasWorkspace(config, String(body.canvasId || ""));
211
+ const requestedThreadId = readString(body.threadId);
212
+ let threadId = requestedThreadId || workspace.activeThreadIds.codex || "";
213
+ if (!threadId) {
214
+ const thread = await startCodexThread(emit, workspace.workspacePath);
215
+ threadId = String(thread.id || "");
216
+ setCanvasActiveThread(config, workspace.canvasId, "codex", threadId);
217
+ }
218
+ else {
219
+ try {
220
+ await verifyCodexThreadWorkspace(emit, threadId, workspace.workspacePath);
221
+ }
222
+ catch (error) {
223
+ if (requestedThreadId || !isEmptySavedCodexThreadError(error))
224
+ throw error;
225
+ emit("agent_log", { agent: "codex", text: `Saved Codex thread ${threadId} has no rollout; starting a new local thread.` });
226
+ const thread = await startCodexThread(emit, workspace.workspacePath);
227
+ threadId = String(thread.id || "");
228
+ }
229
+ setCanvasActiveThread(config, workspace.canvasId, "codex", threadId);
230
+ }
231
+ void runCodexTurn(withAgentPrompt(String(body.prompt || "")), emit, attachments, { threadId, cwd: workspace.workspacePath });
232
+ sendJson(res, 200, { ok: true, threadId });
233
+ return;
234
+ }
235
+ if (method === "POST" && url.pathname === "/agent/claude/turn") {
236
+ const body = readRecordBody(await readBody(req));
237
+ const attachments = readAttachments(body.attachments);
238
+ const workspace = ensureCanvasWorkspace(config, String(body.canvasId || ""));
239
+ const requestedThreadId = readString(body.threadId);
240
+ const threadId = requestedThreadId || workspace.activeThreadIds.claude || "";
241
+ void runClaudeTurn(withAgentPrompt(String(body.prompt || "")), emit, attachments, {
242
+ ...(threadId ? { threadId } : {}),
243
+ cwd: workspace.workspacePath,
244
+ command: config.claudeCommand,
245
+ allowFreshFallback: !requestedThreadId,
246
+ onThread: (sessionId) => {
247
+ setCanvasActiveThread(config, workspace.canvasId, "claude", sessionId);
248
+ },
249
+ });
250
+ sendJson(res, 200, { ok: true, threadId: threadId || null });
251
+ return;
252
+ }
253
+ sendError(res, 404, `unknown local agent route: ${method} ${url.pathname}`);
254
+ }
255
+ export async function startHttpServer() {
256
+ const configRef = { current: loadConfig(true) };
257
+ saveConfig(configRef.current);
258
+ const session = new CanvasSession();
259
+ const server = http.createServer((req, res) => {
260
+ const origin = readOrigin(req);
261
+ const url = new URL(req.url || "/", configRef.current.url);
262
+ void routeRequest({ req, res, url, origin }, session, configRef).catch((error) => {
263
+ sendCors(res, origin);
264
+ const message = error instanceof Error ? error.message : "QCanvas local agent request failed";
265
+ sendError(res, 500, message);
266
+ });
267
+ });
268
+ await new Promise((resolve) => {
269
+ server.listen(configRef.current.port, configRef.current.host, resolve);
270
+ });
271
+ console.log(`QCanvas Local Agent ${VERSION}`);
272
+ console.log(`Local URL: ${configRef.current.url}`);
273
+ console.log(`Connect token: ${configRef.current.token}`);
274
+ console.log(`Config: ${CONFIG_FILE}`);
275
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { startHttpServer } from "./http-server.js";
3
+ import { startMcpServer } from "./mcp-server.js";
4
+ const mode = process.argv[2] || "http";
5
+ if (mode === "mcp") {
6
+ await startMcpServer();
7
+ }
8
+ else {
9
+ await startHttpServer();
10
+ }
@@ -0,0 +1 @@
1
+ export declare function startMcpServer(): Promise<void>;
@@ -0,0 +1,49 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { loadConfig, VERSION } from "./config.js";
4
+ import { localAgentToolDescriptions, localAgentToolInputSchemas, localAgentToolNames, } from "./schemas.js";
5
+ const INSTRUCTIONS = [
6
+ "Use QCanvas local tools only for the connected live browser canvas.",
7
+ "Read tapcanvas_flow_get before writing.",
8
+ "Prefer tapcanvas_create_generation_flow over tapcanvas_flow_patch when creating a single generation node with reference wiring; it auto-computes placement, handles, and reference asset inputs. It only creates the node and never submits generation by itself.",
9
+ "All tapcanvas_flow_patch writes (including those issued by tapcanvas_create_generation_flow) are confirmed by the browser before they apply.",
10
+ "If no live browser tab is connected, report the error directly.",
11
+ ].join("\n");
12
+ export async function startMcpServer() {
13
+ const config = loadConfig(true);
14
+ // 由启动方(codex/claude 驱动的 mcp 配置)注入,标记工具调用来自哪个智能体。
15
+ const agentId = (process.env.QCANVAS_AGENT_ID || "").trim();
16
+ const server = new McpServer({ name: "qcanvas-local-agent", version: VERSION }, { instructions: INSTRUCTIONS });
17
+ for (const toolName of localAgentToolNames) {
18
+ registerTool(server, config.url, config.token, toolName, agentId);
19
+ }
20
+ await server.connect(new StdioServerTransport());
21
+ }
22
+ function registerTool(server, url, token, name, agentId) {
23
+ const schema = localAgentToolInputSchemas[name];
24
+ server.registerTool(name, {
25
+ description: localAgentToolDescriptions[name],
26
+ inputSchema: schema.shape,
27
+ }, async (input) => {
28
+ const parsed = schema.parse(input);
29
+ const result = await postTool(url, token, name, parsed, agentId);
30
+ return {
31
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
32
+ };
33
+ });
34
+ }
35
+ async function postTool(url, token, name, input, agentId) {
36
+ const response = await fetch(`${url}/api/tools`, {
37
+ method: "POST",
38
+ headers: {
39
+ "content-type": "application/json",
40
+ "x-qcanvas-agent-token": token,
41
+ },
42
+ body: JSON.stringify({ name, input, ...(agentId ? { agent: agentId } : {}) }),
43
+ });
44
+ const body = await response.json();
45
+ if (!response.ok || !body.ok) {
46
+ throw new Error(body.error || `QCanvas local tool failed: ${response.status}`);
47
+ }
48
+ return body.result;
49
+ }
@@ -0,0 +1,257 @@
1
+ import { z } from "zod";
2
+ export declare const localAgentToolNames: readonly ["tapcanvas_flow_get", "tapcanvas_create_generation_flow", "tapcanvas_flow_patch"];
3
+ export type LocalAgentToolName = (typeof localAgentToolNames)[number];
4
+ export declare const localAgentToolInputSchemas: {
5
+ tapcanvas_flow_get: z.ZodObject<{}, "passthrough", z.ZodTypeAny, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">, z.objectInputType<{}, z.ZodTypeAny, "passthrough">>;
6
+ tapcanvas_create_generation_flow: z.ZodObject<{
7
+ kind: z.ZodString;
8
+ prompt: z.ZodString;
9
+ label: z.ZodOptional<z.ZodString>;
10
+ position: z.ZodOptional<z.ZodObject<{
11
+ x: z.ZodNumber;
12
+ y: z.ZodNumber;
13
+ }, "strip", z.ZodTypeAny, {
14
+ x: number;
15
+ y: number;
16
+ }, {
17
+ x: number;
18
+ y: number;
19
+ }>>;
20
+ referenceNodeIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
21
+ referenceImageUrls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
22
+ model: z.ZodOptional<z.ZodString>;
23
+ aspect: z.ZodOptional<z.ZodString>;
24
+ groupId: z.ZodOptional<z.ZodString>;
25
+ nodeId: z.ZodOptional<z.ZodString>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ kind: string;
28
+ prompt: string;
29
+ position?: {
30
+ x: number;
31
+ y: number;
32
+ } | undefined;
33
+ label?: string | undefined;
34
+ referenceNodeIds?: string[] | undefined;
35
+ referenceImageUrls?: string[] | undefined;
36
+ model?: string | undefined;
37
+ aspect?: string | undefined;
38
+ groupId?: string | undefined;
39
+ nodeId?: string | undefined;
40
+ }, {
41
+ kind: string;
42
+ prompt: string;
43
+ position?: {
44
+ x: number;
45
+ y: number;
46
+ } | undefined;
47
+ label?: string | undefined;
48
+ referenceNodeIds?: string[] | undefined;
49
+ referenceImageUrls?: string[] | undefined;
50
+ model?: string | undefined;
51
+ aspect?: string | undefined;
52
+ groupId?: string | undefined;
53
+ nodeId?: string | undefined;
54
+ }>;
55
+ tapcanvas_flow_patch: z.ZodObject<{
56
+ allowOverwrite: z.ZodOptional<z.ZodBoolean>;
57
+ deleteNodeIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
58
+ deleteEdgeIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
59
+ createNodes: z.ZodOptional<z.ZodArray<z.ZodObject<{
60
+ id: z.ZodString;
61
+ type: z.ZodEnum<["taskNode", "groupNode"]>;
62
+ position: z.ZodObject<{
63
+ x: z.ZodNumber;
64
+ y: z.ZodNumber;
65
+ }, "strip", z.ZodTypeAny, {
66
+ x: number;
67
+ y: number;
68
+ }, {
69
+ x: number;
70
+ y: number;
71
+ }>;
72
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
73
+ parentId: z.ZodOptional<z.ZodString>;
74
+ selected: z.ZodOptional<z.ZodBoolean>;
75
+ draggable: z.ZodOptional<z.ZodBoolean>;
76
+ selectable: z.ZodOptional<z.ZodBoolean>;
77
+ focusable: z.ZodOptional<z.ZodBoolean>;
78
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
79
+ id: z.ZodString;
80
+ type: z.ZodEnum<["taskNode", "groupNode"]>;
81
+ position: z.ZodObject<{
82
+ x: z.ZodNumber;
83
+ y: z.ZodNumber;
84
+ }, "strip", z.ZodTypeAny, {
85
+ x: number;
86
+ y: number;
87
+ }, {
88
+ x: number;
89
+ y: number;
90
+ }>;
91
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
92
+ parentId: z.ZodOptional<z.ZodString>;
93
+ selected: z.ZodOptional<z.ZodBoolean>;
94
+ draggable: z.ZodOptional<z.ZodBoolean>;
95
+ selectable: z.ZodOptional<z.ZodBoolean>;
96
+ focusable: z.ZodOptional<z.ZodBoolean>;
97
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
98
+ id: z.ZodString;
99
+ type: z.ZodEnum<["taskNode", "groupNode"]>;
100
+ position: z.ZodObject<{
101
+ x: z.ZodNumber;
102
+ y: z.ZodNumber;
103
+ }, "strip", z.ZodTypeAny, {
104
+ x: number;
105
+ y: number;
106
+ }, {
107
+ x: number;
108
+ y: number;
109
+ }>;
110
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
111
+ parentId: z.ZodOptional<z.ZodString>;
112
+ selected: z.ZodOptional<z.ZodBoolean>;
113
+ draggable: z.ZodOptional<z.ZodBoolean>;
114
+ selectable: z.ZodOptional<z.ZodBoolean>;
115
+ focusable: z.ZodOptional<z.ZodBoolean>;
116
+ }, z.ZodTypeAny, "passthrough">>, "many">>;
117
+ createEdges: z.ZodOptional<z.ZodArray<z.ZodObject<{
118
+ id: z.ZodString;
119
+ source: z.ZodString;
120
+ target: z.ZodString;
121
+ sourceHandle: z.ZodOptional<z.ZodString>;
122
+ targetHandle: z.ZodOptional<z.ZodString>;
123
+ type: z.ZodOptional<z.ZodString>;
124
+ label: z.ZodOptional<z.ZodString>;
125
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
126
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
127
+ id: z.ZodString;
128
+ source: z.ZodString;
129
+ target: z.ZodString;
130
+ sourceHandle: z.ZodOptional<z.ZodString>;
131
+ targetHandle: z.ZodOptional<z.ZodString>;
132
+ type: z.ZodOptional<z.ZodString>;
133
+ label: z.ZodOptional<z.ZodString>;
134
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
135
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
136
+ id: z.ZodString;
137
+ source: z.ZodString;
138
+ target: z.ZodString;
139
+ sourceHandle: z.ZodOptional<z.ZodString>;
140
+ targetHandle: z.ZodOptional<z.ZodString>;
141
+ type: z.ZodOptional<z.ZodString>;
142
+ label: z.ZodOptional<z.ZodString>;
143
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
144
+ }, z.ZodTypeAny, "passthrough">>, "many">>;
145
+ patchNodeData: z.ZodOptional<z.ZodArray<z.ZodObject<{
146
+ id: z.ZodString;
147
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
148
+ }, "strip", z.ZodTypeAny, {
149
+ id: string;
150
+ data: Record<string, unknown>;
151
+ }, {
152
+ id: string;
153
+ data: Record<string, unknown>;
154
+ }>, "many">>;
155
+ appendNodeArrays: z.ZodOptional<z.ZodArray<z.ZodObject<{
156
+ id: z.ZodString;
157
+ key: z.ZodString;
158
+ items: z.ZodArray<z.ZodUnknown, "many">;
159
+ }, "strip", z.ZodTypeAny, {
160
+ key: string;
161
+ id: string;
162
+ items: unknown[];
163
+ }, {
164
+ key: string;
165
+ id: string;
166
+ items: unknown[];
167
+ }>, "many">>;
168
+ }, "strip", z.ZodTypeAny, {
169
+ allowOverwrite?: boolean | undefined;
170
+ deleteNodeIds?: string[] | undefined;
171
+ deleteEdgeIds?: string[] | undefined;
172
+ createNodes?: z.objectOutputType<{
173
+ id: z.ZodString;
174
+ type: z.ZodEnum<["taskNode", "groupNode"]>;
175
+ position: z.ZodObject<{
176
+ x: z.ZodNumber;
177
+ y: z.ZodNumber;
178
+ }, "strip", z.ZodTypeAny, {
179
+ x: number;
180
+ y: number;
181
+ }, {
182
+ x: number;
183
+ y: number;
184
+ }>;
185
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
186
+ parentId: z.ZodOptional<z.ZodString>;
187
+ selected: z.ZodOptional<z.ZodBoolean>;
188
+ draggable: z.ZodOptional<z.ZodBoolean>;
189
+ selectable: z.ZodOptional<z.ZodBoolean>;
190
+ focusable: z.ZodOptional<z.ZodBoolean>;
191
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
192
+ createEdges?: z.objectOutputType<{
193
+ id: z.ZodString;
194
+ source: z.ZodString;
195
+ target: z.ZodString;
196
+ sourceHandle: z.ZodOptional<z.ZodString>;
197
+ targetHandle: z.ZodOptional<z.ZodString>;
198
+ type: z.ZodOptional<z.ZodString>;
199
+ label: z.ZodOptional<z.ZodString>;
200
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
201
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
202
+ patchNodeData?: {
203
+ id: string;
204
+ data: Record<string, unknown>;
205
+ }[] | undefined;
206
+ appendNodeArrays?: {
207
+ key: string;
208
+ id: string;
209
+ items: unknown[];
210
+ }[] | undefined;
211
+ }, {
212
+ allowOverwrite?: boolean | undefined;
213
+ deleteNodeIds?: string[] | undefined;
214
+ deleteEdgeIds?: string[] | undefined;
215
+ createNodes?: z.objectInputType<{
216
+ id: z.ZodString;
217
+ type: z.ZodEnum<["taskNode", "groupNode"]>;
218
+ position: z.ZodObject<{
219
+ x: z.ZodNumber;
220
+ y: z.ZodNumber;
221
+ }, "strip", z.ZodTypeAny, {
222
+ x: number;
223
+ y: number;
224
+ }, {
225
+ x: number;
226
+ y: number;
227
+ }>;
228
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
229
+ parentId: z.ZodOptional<z.ZodString>;
230
+ selected: z.ZodOptional<z.ZodBoolean>;
231
+ draggable: z.ZodOptional<z.ZodBoolean>;
232
+ selectable: z.ZodOptional<z.ZodBoolean>;
233
+ focusable: z.ZodOptional<z.ZodBoolean>;
234
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
235
+ createEdges?: z.objectInputType<{
236
+ id: z.ZodString;
237
+ source: z.ZodString;
238
+ target: z.ZodString;
239
+ sourceHandle: z.ZodOptional<z.ZodString>;
240
+ targetHandle: z.ZodOptional<z.ZodString>;
241
+ type: z.ZodOptional<z.ZodString>;
242
+ label: z.ZodOptional<z.ZodString>;
243
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
244
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
245
+ patchNodeData?: {
246
+ id: string;
247
+ data: Record<string, unknown>;
248
+ }[] | undefined;
249
+ appendNodeArrays?: {
250
+ key: string;
251
+ id: string;
252
+ items: unknown[];
253
+ }[] | undefined;
254
+ }>;
255
+ };
256
+ export declare const localAgentToolDescriptions: Record<LocalAgentToolName, string>;
257
+ export declare function isLocalAgentToolName(value: unknown): value is LocalAgentToolName;
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ const recordSchema = z.record(z.unknown());
3
+ const finitePositionSchema = z.object({
4
+ x: z.number().finite(),
5
+ y: z.number().finite(),
6
+ });
7
+ const createNodeSchema = z
8
+ .object({
9
+ id: z.string().min(1),
10
+ type: z.enum(["taskNode", "groupNode"]),
11
+ position: finitePositionSchema,
12
+ data: recordSchema,
13
+ parentId: z.string().min(1).optional(),
14
+ selected: z.boolean().optional(),
15
+ draggable: z.boolean().optional(),
16
+ selectable: z.boolean().optional(),
17
+ focusable: z.boolean().optional(),
18
+ })
19
+ .passthrough();
20
+ const createEdgeSchema = z
21
+ .object({
22
+ id: z.string().min(1),
23
+ source: z.string().min(1),
24
+ target: z.string().min(1),
25
+ sourceHandle: z.string().min(1).optional(),
26
+ targetHandle: z.string().min(1).optional(),
27
+ type: z.string().min(1).optional(),
28
+ label: z.string().optional(),
29
+ data: recordSchema.optional(),
30
+ })
31
+ .passthrough();
32
+ const patchNodeDataSchema = z.object({
33
+ id: z.string().min(1),
34
+ data: recordSchema,
35
+ });
36
+ const appendNodeArraySchema = z.object({
37
+ id: z.string().min(1),
38
+ key: z.string().min(1),
39
+ items: z.array(z.unknown()).min(1),
40
+ });
41
+ const createGenerationFlowSchema = z.object({
42
+ kind: z.string().min(1),
43
+ prompt: z.string().min(1),
44
+ label: z.string().min(1).optional(),
45
+ position: finitePositionSchema.optional(),
46
+ referenceNodeIds: z.array(z.string().min(1)).optional(),
47
+ referenceImageUrls: z.array(z.string().min(1)).optional(),
48
+ model: z.string().min(1).optional(),
49
+ aspect: z.string().min(1).optional(),
50
+ groupId: z.string().min(1).optional(),
51
+ nodeId: z.string().min(1).optional(),
52
+ });
53
+ export const localAgentToolNames = [
54
+ "tapcanvas_flow_get",
55
+ "tapcanvas_create_generation_flow",
56
+ "tapcanvas_flow_patch",
57
+ ];
58
+ export const localAgentToolInputSchemas = {
59
+ tapcanvas_flow_get: z.object({}).passthrough(),
60
+ tapcanvas_create_generation_flow: createGenerationFlowSchema,
61
+ tapcanvas_flow_patch: z.object({
62
+ allowOverwrite: z.boolean().optional(),
63
+ deleteNodeIds: z.array(z.string().min(1)).optional(),
64
+ deleteEdgeIds: z.array(z.string().min(1)).optional(),
65
+ createNodes: z.array(createNodeSchema).optional(),
66
+ createEdges: z.array(createEdgeSchema).optional(),
67
+ patchNodeData: z.array(patchNodeDataSchema).optional(),
68
+ appendNodeArrays: z.array(appendNodeArraySchema).optional(),
69
+ }),
70
+ };
71
+ export const localAgentToolDescriptions = {
72
+ tapcanvas_flow_get: "Read the live QCanvas graph from the connected browser tab. Use this before any patch.",
73
+ tapcanvas_create_generation_flow: "Create one runnable taskNode for a generation intent, auto-placed next to existing nodes and auto-wired to referenceNodeIds via real edges + assetInputs/referenceImages carrying those nodes' current output image. This only creates and wires the node; it does NOT submit generation. Call tapcanvas_flow_patch's browser-side runner (or ask the user to run the node) to actually generate.",
74
+ tapcanvas_flow_patch: "Request a browser-confirmed QCanvas graph patch. The browser must approve the write before it is applied.",
75
+ };
76
+ export function isLocalAgentToolName(value) {
77
+ return typeof value === "string" && localAgentToolNames.includes(value);
78
+ }