draw2agent 1.3.3 → 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 +8 -0
- package/dist/index.js +346 -49
- package/dist/index.js.map +1 -1
- package/overlay/dist/draw2agent-overlay.js +216 -216
- package/package.json +10 -2
- package/prompts/agent-ipad-instructions.txt +12 -0
- package/prompts/agent-scratch-instructions.txt +11 -0
package/README.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# draw2agent ✏️
|
|
2
2
|
|
|
3
|
+
[](https://registry.modelcontextprotocol.io/?q=draw2agent)
|
|
4
|
+
|
|
3
5
|
Draw on your website. Your AI agent sees it.
|
|
4
6
|
|
|
5
7
|
**draw2agent** is an MCP server that lets you draw annotations directly on top of your local dev page. When you submit, your IDE agent receives a screenshot, structured DOM data, and annotation context to make precise code edits.
|
|
6
8
|
|
|
9
|
+
👉 **Try it out at:** [draw2agent.vercel.app](https://draw2agent.vercel.app)
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
[](https://youtu.be/siv1ioOnOXk)
|
|
14
|
+
|
|
7
15
|
## Quick Start
|
|
8
16
|
|
|
9
17
|
### 1. Add to your IDE (one-time)
|
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,
|
|
782
|
-
if (!
|
|
781
|
+
function getElementAtPath(obj, path4) {
|
|
782
|
+
if (!path4)
|
|
783
783
|
return obj;
|
|
784
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
1397
|
-
for (const seg of
|
|
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
|
|
13375
|
-
if (
|
|
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 (
|
|
13380
|
-
const key =
|
|
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
|
|
14003
|
-
import
|
|
14004
|
-
import { fileURLToPath as
|
|
14005
|
-
var
|
|
14006
|
-
var INSTRUCTIONS_PATH =
|
|
14007
|
-
var ERROR_INSTRUCTIONS_PATH =
|
|
14008
|
-
var CLOSE_INSTRUCTIONS_PATH =
|
|
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
|
-
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
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
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
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
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
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
|
);
|