draw2agent 1.3.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # draw2agent ✏️
2
2
 
3
- [![mcp-registry](https://img.shields.io/badge/mcp--registry-draw2agent%401.3.3-blue)](https://mcpregistry.com/packages/draw2agent)
3
+ [![mcp-registry](https://img.shields.io/badge/mcp--registry-io.github.zero--abd%2Fdraw2agent%401.3.4-blue)](https://registry.modelcontextprotocol.io/?q=draw2agent)
4
4
 
5
5
  Draw on your website. Your AI agent sees it.
6
6
 
@@ -11,7 +11,6 @@ Draw on your website. Your AI agent sees it.
11
11
  ## Demo
12
12
 
13
13
  [![draw2agent demo video](https://img.youtube.com/vi/siv1ioOnOXk/maxresdefault.jpg)](https://youtu.be/siv1ioOnOXk)
14
- *(Note: Video hosted on YouTube. Demo files have been ignored from the repository to save space.)*
15
14
 
16
15
  ## Quick Start
17
16
 
package/dist/index.js CHANGED
@@ -778,10 +778,10 @@ function mergeDefs(...defs) {
778
778
  function cloneDef(schema) {
779
779
  return mergeDefs(schema._zod.def);
780
780
  }
781
- function getElementAtPath(obj, path3) {
782
- if (!path3)
781
+ function getElementAtPath(obj, path4) {
782
+ if (!path4)
783
783
  return obj;
784
- return path3.reduce((acc, key) => acc?.[key], obj);
784
+ return path4.reduce((acc, key) => acc?.[key], obj);
785
785
  }
786
786
  function promiseAllObject(promisesObj) {
787
787
  const keys = Object.keys(promisesObj);
@@ -1164,11 +1164,11 @@ function aborted(x, startIndex = 0) {
1164
1164
  }
1165
1165
  return false;
1166
1166
  }
1167
- function prefixIssues(path3, issues) {
1167
+ function prefixIssues(path4, issues) {
1168
1168
  return issues.map((iss) => {
1169
1169
  var _a2;
1170
1170
  (_a2 = iss).path ?? (_a2.path = []);
1171
- iss.path.unshift(path3);
1171
+ iss.path.unshift(path4);
1172
1172
  return iss;
1173
1173
  });
1174
1174
  }
@@ -1351,7 +1351,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1351
1351
  }
1352
1352
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1353
1353
  const result = { errors: [] };
1354
- const processError = (error49, path3 = []) => {
1354
+ const processError = (error49, path4 = []) => {
1355
1355
  var _a2, _b;
1356
1356
  for (const issue2 of error49.issues) {
1357
1357
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1361,7 +1361,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1361
1361
  } else if (issue2.code === "invalid_element") {
1362
1362
  processError({ issues: issue2.issues }, issue2.path);
1363
1363
  } else {
1364
- const fullpath = [...path3, ...issue2.path];
1364
+ const fullpath = [...path4, ...issue2.path];
1365
1365
  if (fullpath.length === 0) {
1366
1366
  result.errors.push(mapper(issue2));
1367
1367
  continue;
@@ -1393,8 +1393,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1393
1393
  }
1394
1394
  function toDotPath(_path) {
1395
1395
  const segs = [];
1396
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1397
- for (const seg of path3) {
1396
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1397
+ for (const seg of path4) {
1398
1398
  if (typeof seg === "number")
1399
1399
  segs.push(`[${seg}]`);
1400
1400
  else if (typeof seg === "symbol")
@@ -13371,13 +13371,13 @@ function resolveRef(ref, ctx) {
13371
13371
  if (!ref.startsWith("#")) {
13372
13372
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13373
13373
  }
13374
- const path3 = ref.slice(1).split("/").filter(Boolean);
13375
- if (path3.length === 0) {
13374
+ const path4 = ref.slice(1).split("/").filter(Boolean);
13375
+ if (path4.length === 0) {
13376
13376
  return ctx.rootSchema;
13377
13377
  }
13378
13378
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13379
- if (path3[0] === defsKey) {
13380
- const key = path3[1];
13379
+ if (path4[0] === defsKey) {
13380
+ const key = path4[1];
13381
13381
  if (!key || !ctx.defs[key]) {
13382
13382
  throw new Error(`Reference not found: ${ref}`);
13383
13383
  }
@@ -13992,6 +13992,203 @@ function stopHttpServer() {
13992
13992
  }
13993
13993
  }
13994
13994
 
13995
+ // src/scratch-server.ts
13996
+ import http2 from "http";
13997
+ import fs2 from "fs";
13998
+ import path2 from "path";
13999
+ import { fileURLToPath as fileURLToPath2 } from "url";
14000
+ var __dirname2 = path2.dirname(fileURLToPath2(import.meta.url));
14001
+ var OVERLAY_DIR2 = path2.resolve(__dirname2, "..", "overlay", "dist");
14002
+ var D2A_PREFIX2 = "/__d2a__";
14003
+ var MIME_TYPES2 = {
14004
+ ".js": "application/javascript",
14005
+ ".css": "text/css",
14006
+ ".html": "text/html",
14007
+ ".json": "application/json",
14008
+ ".png": "image/png",
14009
+ ".svg": "image/svg+xml",
14010
+ ".woff2": "font/woff2",
14011
+ ".woff": "font/woff",
14012
+ ".ttf": "font/ttf"
14013
+ };
14014
+ var scratchServer = null;
14015
+ function getScratchHTML() {
14016
+ return `<!DOCTYPE html>
14017
+ <html lang="en">
14018
+ <head>
14019
+ <meta charset="UTF-8">
14020
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
14021
+ <title>draw2agent \u2014 Scratch Whiteboard</title>
14022
+ <link rel="stylesheet" href="${D2A_PREFIX2}/draw2agent-overlay.css">
14023
+ <style>
14024
+ * { margin: 0; padding: 0; box-sizing: border-box; }
14025
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #1e1e2e; }
14026
+ /* In scratch mode, make the Excalidraw canvas fill the entire page with a visible background */
14027
+ #draw2agent-root {
14028
+ position: fixed !important;
14029
+ inset: 0 !important;
14030
+ z-index: 1 !important;
14031
+ }
14032
+ #draw2agent-root .d2a-canvas-container {
14033
+ pointer-events: all !important;
14034
+ position: fixed !important;
14035
+ inset: 0 !important;
14036
+ z-index: 1 !important;
14037
+ }
14038
+ /* Show a background for the Excalidraw canvas in scratch mode */
14039
+ #draw2agent-root .excalidraw {
14040
+ --ui-font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
14041
+ }
14042
+ /* Override transparent background for scratch mode */
14043
+ #draw2agent-root .excalidraw .excalidraw__canvas {
14044
+ background: #1e1e2e !important;
14045
+ }
14046
+ </style>
14047
+ </head>
14048
+ <body data-d2a-mode="scratch">
14049
+ <script src="${D2A_PREFIX2}/draw2agent-overlay.js"></script>
14050
+ </body>
14051
+ </html>`;
14052
+ }
14053
+ function startScratchServer(port) {
14054
+ return new Promise((resolve, reject) => {
14055
+ if (scratchServer) {
14056
+ resolve(`http://localhost:${port}`);
14057
+ return;
14058
+ }
14059
+ scratchServer = http2.createServer((req, res) => {
14060
+ const url2 = req.url || "/";
14061
+ if (req.method === "OPTIONS") {
14062
+ res.writeHead(204, {
14063
+ "Access-Control-Allow-Origin": "*",
14064
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
14065
+ "Access-Control-Allow-Headers": "Content-Type"
14066
+ });
14067
+ res.end();
14068
+ return;
14069
+ }
14070
+ if (url2 === `${D2A_PREFIX2}/capture` && req.method === "POST") {
14071
+ let body = "";
14072
+ req.on("data", (chunk) => body += chunk);
14073
+ req.on("end", () => {
14074
+ try {
14075
+ const payload = JSON.parse(body);
14076
+ payload.timestamp = (/* @__PURE__ */ new Date()).toISOString();
14077
+ payload.targetUrl = "scratch://whiteboard";
14078
+ setState(payload);
14079
+ res.writeHead(200, {
14080
+ "Content-Type": "application/json",
14081
+ "Access-Control-Allow-Origin": "*"
14082
+ });
14083
+ res.end(JSON.stringify({ success: true }));
14084
+ console.error("[draw2agent] \u2705 Scratch state captured successfully");
14085
+ } catch (err) {
14086
+ const msg = err instanceof Error ? err.message : String(err);
14087
+ console.error("[draw2agent] \u274C Scratch capture error:", msg);
14088
+ rejectState(`Failed to parse capture payload: ${msg}`);
14089
+ res.writeHead(400, { "Content-Type": "application/json" });
14090
+ res.end(JSON.stringify({ error: "Invalid JSON payload" }));
14091
+ }
14092
+ });
14093
+ return;
14094
+ }
14095
+ if (url2 === `${D2A_PREFIX2}/close` && req.method === "POST") {
14096
+ rejectState("User closed the draw2agent session.");
14097
+ res.writeHead(200, {
14098
+ "Content-Type": "application/json",
14099
+ "Access-Control-Allow-Origin": "*"
14100
+ });
14101
+ res.end(JSON.stringify({ success: true }));
14102
+ console.error("[draw2agent] \u{1F6D1} Scratch session closed by user");
14103
+ return;
14104
+ }
14105
+ if (url2.startsWith(D2A_PREFIX2 + "/")) {
14106
+ const filePath = path2.join(OVERLAY_DIR2, url2.slice(D2A_PREFIX2.length));
14107
+ const ext = path2.extname(filePath);
14108
+ const mime = MIME_TYPES2[ext] || "application/octet-stream";
14109
+ fs2.readFile(filePath, (err, data) => {
14110
+ if (err) {
14111
+ res.writeHead(404, { "Content-Type": "text/plain" });
14112
+ res.end("Not found");
14113
+ return;
14114
+ }
14115
+ res.writeHead(200, {
14116
+ "Content-Type": mime,
14117
+ "Access-Control-Allow-Origin": "*",
14118
+ "Cache-Control": "no-cache"
14119
+ });
14120
+ res.end(data);
14121
+ });
14122
+ return;
14123
+ }
14124
+ if (url2 === "/" || url2 === "/index.html") {
14125
+ const html = getScratchHTML();
14126
+ res.writeHead(200, {
14127
+ "Content-Type": "text/html",
14128
+ "Cache-Control": "no-cache"
14129
+ });
14130
+ res.end(html);
14131
+ return;
14132
+ }
14133
+ res.writeHead(404, { "Content-Type": "text/plain" });
14134
+ res.end("Not found");
14135
+ });
14136
+ scratchServer.listen(port, () => {
14137
+ const url2 = `http://localhost:${port}`;
14138
+ console.error(`[draw2agent] \u{1F3A8} Scratch whiteboard at ${url2}`);
14139
+ resolve(url2);
14140
+ });
14141
+ scratchServer.on("error", (err) => {
14142
+ reject(err);
14143
+ });
14144
+ });
14145
+ }
14146
+ function stopScratchServer() {
14147
+ if (scratchServer) {
14148
+ scratchServer.close();
14149
+ scratchServer = null;
14150
+ }
14151
+ }
14152
+
14153
+ // src/tunnel.ts
14154
+ import localtunnel from "localtunnel";
14155
+ var activeTunnel = null;
14156
+ async function startTunnel(localPort) {
14157
+ await stopTunnel();
14158
+ const tunnel = await localtunnel({ port: localPort });
14159
+ tunnel.on("close", () => {
14160
+ console.error("[draw2agent] \u{1F50C} Tunnel closed");
14161
+ activeTunnel = null;
14162
+ });
14163
+ tunnel.on("error", (err) => {
14164
+ console.error("[draw2agent] Tunnel error:", err.message);
14165
+ });
14166
+ activeTunnel = tunnel;
14167
+ console.error(`[draw2agent] \u{1F310} Tunnel opened: ${tunnel.url}`);
14168
+ return tunnel.url;
14169
+ }
14170
+ async function stopTunnel() {
14171
+ if (activeTunnel) {
14172
+ activeTunnel.close();
14173
+ activeTunnel = null;
14174
+ }
14175
+ }
14176
+
14177
+ // src/utils/qrcode.ts
14178
+ import QRCode from "qrcode";
14179
+ async function generateQR(url2) {
14180
+ const [dataUrl, ascii] = await Promise.all([
14181
+ QRCode.toDataURL(url2, {
14182
+ type: "image/png",
14183
+ width: 400,
14184
+ margin: 2,
14185
+ color: { dark: "#000000", light: "#ffffff" }
14186
+ }),
14187
+ QRCode.toString(url2, { type: "terminal", small: true })
14188
+ ]);
14189
+ return { dataUrl, ascii };
14190
+ }
14191
+
13995
14192
  // src/utils/browser.ts
13996
14193
  import open from "open";
13997
14194
  async function openBrowser(url2) {
@@ -13999,14 +14196,50 @@ async function openBrowser(url2) {
13999
14196
  }
14000
14197
 
14001
14198
  // src/mcp-server.ts
14002
- import fs2 from "fs";
14003
- import path2 from "path";
14004
- import { fileURLToPath as fileURLToPath2 } from "url";
14005
- var __dirname2 = path2.dirname(fileURLToPath2(import.meta.url));
14006
- var INSTRUCTIONS_PATH = path2.resolve(__dirname2, "..", "prompts", "agent-instructions.txt");
14007
- var ERROR_INSTRUCTIONS_PATH = path2.resolve(__dirname2, "..", "prompts", "agent-error-instructions.txt");
14008
- var CLOSE_INSTRUCTIONS_PATH = path2.resolve(__dirname2, "..", "prompts", "agent-close-instructions.txt");
14199
+ import fs3 from "fs";
14200
+ import path3 from "path";
14201
+ import { fileURLToPath as fileURLToPath3 } from "url";
14202
+ var __dirname3 = path3.dirname(fileURLToPath3(import.meta.url));
14203
+ var INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-instructions.txt");
14204
+ var ERROR_INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-error-instructions.txt");
14205
+ var CLOSE_INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-close-instructions.txt");
14206
+ var IPAD_INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-ipad-instructions.txt");
14207
+ var SCRATCH_INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-scratch-instructions.txt");
14009
14208
  var DEFAULT_PORT = 9742;
14209
+ var DEFAULT_SCRATCH_PORT = 9743;
14210
+ function readPromptFile(filePath, fallback) {
14211
+ try {
14212
+ if (fs3.existsSync(filePath)) {
14213
+ return fs3.readFileSync(filePath, "utf-8");
14214
+ }
14215
+ } catch (e) {
14216
+ console.error(`[draw2agent] Failed to read ${path3.basename(filePath)}, using default.`, e);
14217
+ }
14218
+ return fallback;
14219
+ }
14220
+ function handleToolError(err, toolName) {
14221
+ const message = err instanceof Error ? err.message : String(err);
14222
+ let customInstructions = `\u274C Failed to capture canvas: ${message}`;
14223
+ let isErrorResult = true;
14224
+ if (message.includes("User closed the draw2agent session")) {
14225
+ customInstructions = readPromptFile(
14226
+ CLOSE_INSTRUCTIONS_PATH,
14227
+ "The user closed the draw2agent session. Please summarize the changes you made."
14228
+ );
14229
+ customInstructions = customInstructions.replace(/launch_canvas/g, toolName);
14230
+ isErrorResult = false;
14231
+ stopHttpServer();
14232
+ stopScratchServer();
14233
+ stopTunnel();
14234
+ clearProxyInfo();
14235
+ } else {
14236
+ customInstructions = readPromptFile(ERROR_INSTRUCTIONS_PATH, customInstructions).replace("{{ERROR_MESSAGE}}", message);
14237
+ }
14238
+ return {
14239
+ content: [{ type: "text", text: customInstructions }],
14240
+ isError: isErrorResult
14241
+ };
14242
+ }
14010
14243
  function createMcpServer() {
14011
14244
  const server2 = new McpServer({
14012
14245
  name: "draw2agent",
@@ -14046,15 +14279,10 @@ function createMcpServer() {
14046
14279
  }
14047
14280
  clearState();
14048
14281
  const state = await waitForState();
14049
- let instructionsUrl = "";
14050
- let customInstructions = "Analyze the attached screenshot with user annotations and implement the requested UI changes in the codebase.";
14051
- try {
14052
- if (fs2.existsSync(INSTRUCTIONS_PATH)) {
14053
- customInstructions = fs2.readFileSync(INSTRUCTIONS_PATH, "utf-8");
14054
- }
14055
- } catch (e) {
14056
- console.error("[draw2agent] Failed to read agent-instructions.txt, using default.", e);
14057
- }
14282
+ const customInstructions = readPromptFile(
14283
+ INSTRUCTIONS_PATH,
14284
+ "Analyze the attached screenshot with user annotations and implement the requested UI changes in the codebase."
14285
+ );
14058
14286
  return {
14059
14287
  content: [
14060
14288
  {
@@ -14069,36 +14297,105 @@ function createMcpServer() {
14069
14297
  ]
14070
14298
  };
14071
14299
  } catch (err) {
14072
- const message = err instanceof Error ? err.message : String(err);
14073
- let customInstructions = `\u274C Failed to capture canvas: ${message}`;
14074
- let isErrorResult = true;
14300
+ return handleToolError(err, "launch_canvas");
14301
+ }
14302
+ }
14303
+ );
14304
+ server2.tool(
14305
+ "launch_ipad_canvas",
14306
+ "Launch a remote drawing canvas accessible from an iPad or mobile device. Creates a tunnel to expose the local dev page over the internet and returns a QR code that the user can scan from their iPad. The user draws annotations on their device, and this tool blocks until they submit, returning the visual context. Ideal for touch-based annotation workflows.",
14307
+ {
14308
+ targetUrl: external_exports.string().describe("The URL of the local dev server to overlay (e.g. http://localhost:3000)"),
14309
+ port: external_exports.number().optional().describe("Port for the draw2agent proxy server (default: 9742)")
14310
+ },
14311
+ async ({ targetUrl, port }) => {
14312
+ const proxyPort = port ?? DEFAULT_PORT;
14313
+ try {
14075
14314
  try {
14076
- if (message.includes("User closed the draw2agent session")) {
14077
- if (fs2.existsSync(CLOSE_INSTRUCTIONS_PATH)) {
14078
- customInstructions = fs2.readFileSync(CLOSE_INSTRUCTIONS_PATH, "utf-8");
14079
- } else {
14080
- customInstructions = "The user closed the draw2agent session. Please summarize the changes you made.";
14081
- }
14082
- isErrorResult = false;
14083
- stopHttpServer();
14084
- clearProxyInfo();
14085
- } else {
14086
- if (fs2.existsSync(ERROR_INSTRUCTIONS_PATH)) {
14087
- customInstructions = fs2.readFileSync(ERROR_INSTRUCTIONS_PATH, "utf-8").replace("{{ERROR_MESSAGE}}", message);
14088
- }
14315
+ const checkUrl = targetUrl.replace("://localhost", "://127.0.0.1");
14316
+ await fetch(checkUrl);
14317
+ } catch (err) {
14318
+ if (err.cause?.code === "ECONNREFUSED") {
14319
+ return {
14320
+ content: [
14321
+ {
14322
+ type: "text",
14323
+ text: `\u274C Connection refused to ${targetUrl}. There is no dev server running on that port. Please ask the user to confirm the correct local server URL.`
14324
+ }
14325
+ ],
14326
+ isError: true
14327
+ };
14089
14328
  }
14090
- } catch (e) {
14091
- console.error("[draw2agent] Failed to read fallback instructions.", e);
14092
14329
  }
14330
+ const proxyInfo2 = getProxyInfo();
14331
+ if (!proxyInfo2.running) {
14332
+ const proxyUrl = await startHttpServer(targetUrl, proxyPort);
14333
+ setProxyInfo(proxyUrl);
14334
+ }
14335
+ const tunnelUrl = await startTunnel(proxyPort);
14336
+ const qr = await generateQR(tunnelUrl);
14337
+ console.error(`
14338
+ [draw2agent] \u{1F4F1} iPad Canvas Ready!`);
14339
+ console.error(`[draw2agent] \u{1F517} Scan this QR code or open: ${tunnelUrl}`);
14340
+ console.error(qr.ascii);
14341
+ clearState();
14342
+ const state = await waitForState();
14343
+ await stopTunnel();
14344
+ const customInstructions = readPromptFile(
14345
+ IPAD_INSTRUCTIONS_PATH,
14346
+ "Analyze the attached screenshot with user annotations and implement the requested UI changes in the codebase."
14347
+ );
14348
+ return {
14349
+ content: [
14350
+ {
14351
+ type: "text",
14352
+ text: customInstructions
14353
+ },
14354
+ {
14355
+ type: "image",
14356
+ data: state.annotatedScreenshot.replace(/^data:image\/\w+;base64,/, ""),
14357
+ mimeType: "image/png"
14358
+ }
14359
+ ]
14360
+ };
14361
+ } catch (err) {
14362
+ await stopTunnel();
14363
+ return handleToolError(err, "launch_ipad_canvas");
14364
+ }
14365
+ }
14366
+ );
14367
+ server2.tool(
14368
+ "launch_scratch",
14369
+ "Open a standalone whiteboard for freehand drawing and sketching. No target URL needed \u2014 the user gets a blank Excalidraw canvas to sketch UI mockups, wireframes, or diagrams. The agent receives the drawing as visual context. This tool blocks until the user submits their sketch.",
14370
+ {
14371
+ port: external_exports.number().optional().describe("Port for the scratch whiteboard server (default: 9743)")
14372
+ },
14373
+ async ({ port }) => {
14374
+ const scratchPort = port ?? DEFAULT_SCRATCH_PORT;
14375
+ try {
14376
+ const scratchUrl = await startScratchServer(scratchPort);
14377
+ await openBrowser(scratchUrl);
14378
+ clearState();
14379
+ const state = await waitForState();
14380
+ const customInstructions = readPromptFile(
14381
+ SCRATCH_INSTRUCTIONS_PATH,
14382
+ "The user has drawn a freehand sketch. Analyze it and implement the design."
14383
+ );
14093
14384
  return {
14094
14385
  content: [
14095
14386
  {
14096
14387
  type: "text",
14097
14388
  text: customInstructions
14389
+ },
14390
+ {
14391
+ type: "image",
14392
+ data: state.annotatedScreenshot.replace(/^data:image\/\w+;base64,/, ""),
14393
+ mimeType: "image/png"
14098
14394
  }
14099
- ],
14100
- isError: isErrorResult
14395
+ ]
14101
14396
  };
14397
+ } catch (err) {
14398
+ return handleToolError(err, "launch_scratch");
14102
14399
  }
14103
14400
  }
14104
14401
  );