@drawsy/companion 0.0.0-stage → 0.1.28
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/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/drawsy/bridge-main.js +15 -0
- package/dist/drawsy/bridge.js +1794 -0
- package/dist/drawsy/cli-autostart.js +201 -0
- package/dist/drawsy/cli.js +303 -0
- package/dist/drawsy/codex-app-server.js +2199 -0
- package/dist/drawsy/codex-binary.js +26 -0
- package/dist/drawsy/connector-backend.js +57 -0
- package/dist/drawsy/conversation-history.js +35 -0
- package/dist/drawsy/engine-status.js +20 -0
- package/dist/drawsy/environment.js +17 -0
- package/dist/drawsy/executable-resolver.js +95 -0
- package/dist/drawsy/folder-picker.js +66 -0
- package/dist/drawsy/image-asset.js +173 -0
- package/dist/drawsy/local-conversation-store.js +241 -0
- package/dist/drawsy/mcp-launcher.js +15 -0
- package/dist/drawsy/mcp.js +31588 -0
- package/dist/drawsy/opencode-app-server.js +1251 -0
- package/dist/drawsy/opencode-binary.js +26 -0
- package/dist/drawsy/protocol.js +519 -0
- package/package.json +40 -4
- package/skills/drawsy-browser-use/SKILL.md +128 -0
- package/skills/drawsy-browser-use/references/drawsy-tool-catalog.md +90 -0
- package/skills/drawsy-fast-diagrams/SKILL.md +15 -0
- package/skills/drawsy-teaching-diagrams/SKILL.md +117 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { detectExecutable, resolveExecutable } from "./executable-resolver.js";
|
|
2
|
+
const parseVersion = (value) => {
|
|
3
|
+
const match = value.match(/(?:opencode\s+)?(\d+)\.(\d+)\.(\d+)/i);
|
|
4
|
+
return match
|
|
5
|
+
? [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
6
|
+
: null;
|
|
7
|
+
};
|
|
8
|
+
export const resolveOpenCodeBinary = () => resolveExecutable({
|
|
9
|
+
configured: process.env.DRAWSY_OPENCODE_BIN,
|
|
10
|
+
names: process.platform === "win32"
|
|
11
|
+
? ["opencode.cmd", "opencode.exe", "opencode"]
|
|
12
|
+
: ["opencode"],
|
|
13
|
+
parseVersion
|
|
14
|
+
}) || null;
|
|
15
|
+
export const detectOpenCodeBinary = () => {
|
|
16
|
+
const resolved = detectExecutable({
|
|
17
|
+
configured: process.env.DRAWSY_OPENCODE_BIN,
|
|
18
|
+
names: process.platform === "win32"
|
|
19
|
+
? ["opencode.cmd", "opencode.exe", "opencode"]
|
|
20
|
+
: ["opencode"],
|
|
21
|
+
parseVersion
|
|
22
|
+
});
|
|
23
|
+
return resolved
|
|
24
|
+
? { path: resolved.path, version: resolved.version.join(".") }
|
|
25
|
+
: null;
|
|
26
|
+
};
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
export const MAX_BODY_BYTES = 12 * 1024 * 1024;
|
|
2
|
+
export const MAX_CANVAS_ASSET_BYTES = 8 * 1024 * 1024;
|
|
3
|
+
export const MAX_DIAGRAM_SOURCE_BYTES = 256 * 1024;
|
|
4
|
+
export const CANVAS_REQUEST_TIMEOUT_MS = 60_000;
|
|
5
|
+
export const parseGenerationMode = (value) => {
|
|
6
|
+
if (value === undefined || value === "draw")
|
|
7
|
+
return "draw";
|
|
8
|
+
if (value === "fast")
|
|
9
|
+
return "fast";
|
|
10
|
+
throw new Error("generationMode must be 'fast' or 'draw'.");
|
|
11
|
+
};
|
|
12
|
+
export const surfaceSupportsLivePreview = (surfaceKind) => surfaceKind === "canvas" || surfaceKind === "presentation";
|
|
13
|
+
export const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
export const parseDrawsyTabId = (value) => {
|
|
15
|
+
if (value === undefined || value === null) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value !== "string" ||
|
|
19
|
+
value.length < 16 ||
|
|
20
|
+
value.length > 128 ||
|
|
21
|
+
!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
22
|
+
throw new Error("drawsyTabId is invalid.");
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
export const parseDrawsyTabUrl = (value) => {
|
|
27
|
+
if (value === undefined || value === null) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value !== "string" || value.length > 2_048) {
|
|
31
|
+
throw new Error("drawsyTabUrl is invalid.");
|
|
32
|
+
}
|
|
33
|
+
let url;
|
|
34
|
+
try {
|
|
35
|
+
url = new URL(value);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error("drawsyTabUrl is invalid.");
|
|
39
|
+
}
|
|
40
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
41
|
+
throw new Error("drawsyTabUrl is invalid.");
|
|
42
|
+
}
|
|
43
|
+
return url.href;
|
|
44
|
+
};
|
|
45
|
+
const renderedElementType = (element) => {
|
|
46
|
+
const type = typeof element.type === "string" ? element.type : "unknown";
|
|
47
|
+
const rounded = isRecord(element.roundness);
|
|
48
|
+
if (type === "ellipse") {
|
|
49
|
+
if (element.dimensionality === "3d") {
|
|
50
|
+
return "cylinder";
|
|
51
|
+
}
|
|
52
|
+
const width = typeof element.width === "number" ? Math.abs(element.width) : 0;
|
|
53
|
+
const height = typeof element.height === "number" ? Math.abs(element.height) : 0;
|
|
54
|
+
return width > 0 &&
|
|
55
|
+
height > 0 &&
|
|
56
|
+
Math.abs(width - height) / Math.max(width, height) <= 0.05
|
|
57
|
+
? "circle"
|
|
58
|
+
: "ellipse";
|
|
59
|
+
}
|
|
60
|
+
if (type === "diamond") {
|
|
61
|
+
return rounded ? "rounded diamond" : "diamond";
|
|
62
|
+
}
|
|
63
|
+
if (type === "rectangle") {
|
|
64
|
+
return rounded ? "rounded rectangle" : "rectangle";
|
|
65
|
+
}
|
|
66
|
+
return type;
|
|
67
|
+
};
|
|
68
|
+
export const addCanvasRenderSemantics = (value) => {
|
|
69
|
+
if (!isRecord(value) || !Array.isArray(value.elements)) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
const renderContext = isRecord(value.renderContext)
|
|
73
|
+
? value.renderContext
|
|
74
|
+
: {};
|
|
75
|
+
const visibleElements = value.elements.filter((element) => isRecord(element) &&
|
|
76
|
+
element.isDeleted !== true &&
|
|
77
|
+
typeof element.id === "string");
|
|
78
|
+
const appState = isRecord(value.appState) ? value.appState : {};
|
|
79
|
+
const selection = isRecord(value.selection) && Array.isArray(value.selection.elementIds)
|
|
80
|
+
? value.selection.elementIds.filter((id) => typeof id === "string")
|
|
81
|
+
: isRecord(appState.selectedElementIds)
|
|
82
|
+
? Object.entries(appState.selectedElementIds)
|
|
83
|
+
.filter(([, selected]) => selected === true)
|
|
84
|
+
.map(([id]) => id)
|
|
85
|
+
: [];
|
|
86
|
+
const countColors = (key) => {
|
|
87
|
+
const counts = new Map();
|
|
88
|
+
for (const element of visibleElements) {
|
|
89
|
+
const color = element[key];
|
|
90
|
+
if (typeof color === "string") {
|
|
91
|
+
counts.set(color, (counts.get(color) || 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return [...counts.entries()]
|
|
95
|
+
.sort((a, b) => b[1] - a[1])
|
|
96
|
+
.map(([color, count]) => ({ color, count }));
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
...value,
|
|
100
|
+
renderSemantics: {
|
|
101
|
+
canvas: {
|
|
102
|
+
theme: renderContext.theme === "dark" || renderContext.theme === "light"
|
|
103
|
+
? renderContext.theme
|
|
104
|
+
: "unknown",
|
|
105
|
+
backgroundColor: typeof renderContext.renderedCanvasBackgroundColor === "string"
|
|
106
|
+
? renderContext.renderedCanvasBackgroundColor
|
|
107
|
+
: typeof renderContext.canvasBackgroundColor === "string"
|
|
108
|
+
? renderContext.canvasBackgroundColor
|
|
109
|
+
: "unknown",
|
|
110
|
+
themePreset: typeof renderContext.themePreset === "string"
|
|
111
|
+
? renderContext.themePreset
|
|
112
|
+
: "unknown",
|
|
113
|
+
},
|
|
114
|
+
selection,
|
|
115
|
+
styles: Array.isArray(value.styleSummary)
|
|
116
|
+
? { rendered: value.styleSummary }
|
|
117
|
+
: {
|
|
118
|
+
storedStrokeColors: countColors("strokeColor"),
|
|
119
|
+
storedBackgroundColors: countColors("backgroundColor")
|
|
120
|
+
},
|
|
121
|
+
relationships: Array.isArray(value.relationships) ? value.relationships : visibleElements.flatMap((element) => element.type === "arrow"
|
|
122
|
+
? [{
|
|
123
|
+
arrowId: element.id,
|
|
124
|
+
sourceId: isRecord(element.startBinding)
|
|
125
|
+
? element.startBinding.elementId ?? null
|
|
126
|
+
: null,
|
|
127
|
+
targetId: isRecord(element.endBinding)
|
|
128
|
+
? element.endBinding.elementId ?? null
|
|
129
|
+
: null,
|
|
130
|
+
}]
|
|
131
|
+
: []),
|
|
132
|
+
elements: visibleElements.map((element) => ({
|
|
133
|
+
id: element.id,
|
|
134
|
+
renderedType: renderedElementType(element),
|
|
135
|
+
})),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
const CONNECTOR_CAPABILITIES = new Set([
|
|
140
|
+
"mail",
|
|
141
|
+
"calendar",
|
|
142
|
+
"drive",
|
|
143
|
+
"notion",
|
|
144
|
+
"slack",
|
|
145
|
+
"github",
|
|
146
|
+
"read-ai",
|
|
147
|
+
"fireflies",
|
|
148
|
+
"aws",
|
|
149
|
+
]);
|
|
150
|
+
export const isConnectorCapability = (value) => typeof value === "string" &&
|
|
151
|
+
CONNECTOR_CAPABILITIES.has(value);
|
|
152
|
+
const boundedConnectorString = (value, label, maxLength) => {
|
|
153
|
+
if (typeof value !== "string" ||
|
|
154
|
+
!value.trim() ||
|
|
155
|
+
value.length > maxLength ||
|
|
156
|
+
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
157
|
+
throw new Error(`${label} is invalid.`);
|
|
158
|
+
}
|
|
159
|
+
return value.trim();
|
|
160
|
+
};
|
|
161
|
+
export const parseAgentConnectorTurn = (value) => {
|
|
162
|
+
if (value === undefined)
|
|
163
|
+
return null;
|
|
164
|
+
if (!isRecord(value)) {
|
|
165
|
+
throw new Error("connectors must be an object.");
|
|
166
|
+
}
|
|
167
|
+
const allowedKeys = new Set(["turnId", "sources", "grants"]);
|
|
168
|
+
if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
|
|
169
|
+
throw new Error("connectors contains an unknown field.");
|
|
170
|
+
}
|
|
171
|
+
const turnId = boundedConnectorString(value.turnId, "turnId", 256);
|
|
172
|
+
if (!Array.isArray(value.sources) ||
|
|
173
|
+
value.sources.length < 1 ||
|
|
174
|
+
value.sources.length > 12) {
|
|
175
|
+
throw new Error("connectors.sources must contain 1 to 12 sources.");
|
|
176
|
+
}
|
|
177
|
+
const sources = value.sources.map((source) => {
|
|
178
|
+
if (!isRecord(source) ||
|
|
179
|
+
Object.keys(source).some((key) => !["connectionId", "capability", "label", "accountLabel"].includes(key)) ||
|
|
180
|
+
!isConnectorCapability(source.capability)) {
|
|
181
|
+
throw new Error("A connected source is invalid.");
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
connectionId: boundedConnectorString(source.connectionId, "source connectionId", 256),
|
|
185
|
+
capability: source.capability,
|
|
186
|
+
label: boundedConnectorString(source.label, "source label", 64),
|
|
187
|
+
accountLabel: boundedConnectorString(source.accountLabel, "source accountLabel", 256),
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
if (new Set(sources.map((source) => `${source.connectionId}:${source.capability}`)).size !== sources.length) {
|
|
191
|
+
throw new Error("Connected sources must be unique.");
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(value.grants) ||
|
|
194
|
+
value.grants.length < 1 ||
|
|
195
|
+
value.grants.length > 6) {
|
|
196
|
+
throw new Error("connectors.grants must contain 1 to 6 grants.");
|
|
197
|
+
}
|
|
198
|
+
const grants = value.grants.map((grant) => {
|
|
199
|
+
if (!isRecord(grant) ||
|
|
200
|
+
Object.keys(grant).some((key) => !["connectionId", "grant", "expiresAt"].includes(key)) ||
|
|
201
|
+
typeof grant.expiresAt !== "number" ||
|
|
202
|
+
!Number.isInteger(grant.expiresAt) ||
|
|
203
|
+
grant.expiresAt <= Date.now() ||
|
|
204
|
+
grant.expiresAt > Date.now() + 15 * 60 * 1000) {
|
|
205
|
+
throw new Error("A connector grant is invalid or expired.");
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
connectionId: boundedConnectorString(grant.connectionId, "grant connectionId", 256),
|
|
209
|
+
grant: boundedConnectorString(grant.grant, "connector grant", 8_192),
|
|
210
|
+
expiresAt: grant.expiresAt,
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
if (new Set(grants.map((grant) => grant.connectionId)).size !== grants.length) {
|
|
214
|
+
throw new Error("Connector grants must be unique per connection.");
|
|
215
|
+
}
|
|
216
|
+
const grantedConnections = new Set(grants.map((grant) => grant.connectionId));
|
|
217
|
+
if (sources.some((source) => !grantedConnections.has(source.connectionId))) {
|
|
218
|
+
throw new Error("Every connected source requires a matching grant.");
|
|
219
|
+
}
|
|
220
|
+
return { turnId, sources, grants };
|
|
221
|
+
};
|
|
222
|
+
const AI_RESOURCES = new Set(["kanban", "jira"]);
|
|
223
|
+
export const parseAgentResourceTurn = (value) => {
|
|
224
|
+
if (value === undefined)
|
|
225
|
+
return null;
|
|
226
|
+
if (!isRecord(value) ||
|
|
227
|
+
Object.keys(value).some((key) => !["turnId", "resources", "grant", "expiresAt"].includes(key)) ||
|
|
228
|
+
!Array.isArray(value.resources) ||
|
|
229
|
+
value.resources.length < 1 ||
|
|
230
|
+
value.resources.length > AI_RESOURCES.size ||
|
|
231
|
+
value.resources.some((resource) => typeof resource !== "string" ||
|
|
232
|
+
!AI_RESOURCES.has(resource)) ||
|
|
233
|
+
new Set(value.resources).size !== value.resources.length ||
|
|
234
|
+
typeof value.expiresAt !== "number" ||
|
|
235
|
+
!Number.isInteger(value.expiresAt) ||
|
|
236
|
+
value.expiresAt <= Date.now() ||
|
|
237
|
+
value.expiresAt > Date.now() + 15 * 60 * 1000) {
|
|
238
|
+
throw new Error("A Drawsy resource grant is invalid or expired.");
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
turnId: boundedConnectorString(value.turnId, "resource turnId", 256),
|
|
242
|
+
resources: value.resources,
|
|
243
|
+
grant: boundedConnectorString(value.grant, "resource grant", 8_192),
|
|
244
|
+
expiresAt: value.expiresAt,
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
export const parseCanvasImageRequest = (value) => {
|
|
248
|
+
if (!isRecord(value)) {
|
|
249
|
+
throw new Error("Canvas image request must be an object.");
|
|
250
|
+
}
|
|
251
|
+
const finiteWithin = (candidate, min, max) => typeof candidate === "number" &&
|
|
252
|
+
Number.isFinite(candidate) &&
|
|
253
|
+
candidate >= min &&
|
|
254
|
+
candidate <= max;
|
|
255
|
+
if (typeof value.sourcePath !== "string" || !value.sourcePath.trim()) {
|
|
256
|
+
throw new Error("sourcePath must be a non-empty string.");
|
|
257
|
+
}
|
|
258
|
+
if (!finiteWithin(value.x, -1_000_000, 1_000_000) ||
|
|
259
|
+
!finiteWithin(value.y, -1_000_000, 1_000_000) ||
|
|
260
|
+
!finiteWithin(value.maxWidth, Number.EPSILON, 100_000) ||
|
|
261
|
+
(value.maxHeight !== undefined &&
|
|
262
|
+
!finiteWithin(value.maxHeight, Number.EPSILON, 100_000)) ||
|
|
263
|
+
(value.elementId !== undefined &&
|
|
264
|
+
(typeof value.elementId !== "string" ||
|
|
265
|
+
!value.elementId.trim() ||
|
|
266
|
+
value.elementId.length > 128)) ||
|
|
267
|
+
(value.frameId !== undefined &&
|
|
268
|
+
value.frameId !== null &&
|
|
269
|
+
(typeof value.frameId !== "string" ||
|
|
270
|
+
!value.frameId.trim() ||
|
|
271
|
+
value.frameId.length > 128))) {
|
|
272
|
+
throw new Error("Canvas image placement is invalid.");
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
sourcePath: value.sourcePath.trim(),
|
|
276
|
+
x: value.x,
|
|
277
|
+
y: value.y,
|
|
278
|
+
maxWidth: value.maxWidth,
|
|
279
|
+
...(value.maxHeight === undefined
|
|
280
|
+
? {}
|
|
281
|
+
: { maxHeight: value.maxHeight }),
|
|
282
|
+
...(value.elementId === undefined
|
|
283
|
+
? {}
|
|
284
|
+
: { elementId: value.elementId.trim() }),
|
|
285
|
+
...(value.frameId === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: {
|
|
288
|
+
frameId: value.frameId === null ? null : value.frameId.trim(),
|
|
289
|
+
}),
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
const finiteWithin = (candidate, min, max) => typeof candidate === "number" &&
|
|
293
|
+
Number.isFinite(candidate) &&
|
|
294
|
+
candidate >= min &&
|
|
295
|
+
candidate <= max;
|
|
296
|
+
export const parseLivePreviewRequest = (value) => {
|
|
297
|
+
if (!isRecord(value) ||
|
|
298
|
+
Object.keys(value).some((key) => !["previewId", "url", "title", "x", "y", "width", "height"].includes(key)) ||
|
|
299
|
+
typeof value.url !== "string" ||
|
|
300
|
+
!value.url.trim() ||
|
|
301
|
+
value.url.length > 2_048) {
|
|
302
|
+
throw new Error("Live preview request is invalid.");
|
|
303
|
+
}
|
|
304
|
+
let url;
|
|
305
|
+
try {
|
|
306
|
+
url = new URL(value.url.trim());
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
throw new Error("Live preview URL is invalid.");
|
|
310
|
+
}
|
|
311
|
+
if (url.hostname === "0.0.0.0")
|
|
312
|
+
url.hostname = "127.0.0.1";
|
|
313
|
+
if (url.hostname === "[::]")
|
|
314
|
+
url.hostname = "[::1]";
|
|
315
|
+
if (!["http:", "https:"].includes(url.protocol) ||
|
|
316
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ||
|
|
317
|
+
url.username ||
|
|
318
|
+
url.password) {
|
|
319
|
+
throw new Error("Live previews must use a local loopback URL.");
|
|
320
|
+
}
|
|
321
|
+
url.hash = "";
|
|
322
|
+
const optionalString = (candidate, maxLength) => candidate === undefined ||
|
|
323
|
+
(typeof candidate === "string" &&
|
|
324
|
+
!!candidate.trim() &&
|
|
325
|
+
candidate.length <= maxLength &&
|
|
326
|
+
!/[\u0000-\u001f\u007f]/.test(candidate));
|
|
327
|
+
if (!optionalString(value.previewId, 128) ||
|
|
328
|
+
!optionalString(value.title, 120) ||
|
|
329
|
+
(value.x !== undefined && !finiteWithin(value.x, -1_000_000, 1_000_000)) ||
|
|
330
|
+
(value.y !== undefined && !finiteWithin(value.y, -1_000_000, 1_000_000)) ||
|
|
331
|
+
(value.width !== undefined && !finiteWithin(value.width, 360, 4_000)) ||
|
|
332
|
+
(value.height !== undefined && !finiteWithin(value.height, 260, 4_000))) {
|
|
333
|
+
throw new Error("Live preview placement is invalid.");
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
url: url.toString(),
|
|
337
|
+
...(value.previewId === undefined
|
|
338
|
+
? {}
|
|
339
|
+
: { previewId: value.previewId.trim() }),
|
|
340
|
+
...(value.title === undefined
|
|
341
|
+
? {}
|
|
342
|
+
: { title: value.title.trim() }),
|
|
343
|
+
...(value.x === undefined ? {} : { x: value.x }),
|
|
344
|
+
...(value.y === undefined ? {} : { y: value.y }),
|
|
345
|
+
...(value.width === undefined ? {} : { width: value.width }),
|
|
346
|
+
...(value.height === undefined ? {} : { height: value.height }),
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
const parseContextBounds = (value) => {
|
|
350
|
+
if (!isRecord(value) ||
|
|
351
|
+
!finiteWithin(value.x, -1_000_000, 1_000_000) ||
|
|
352
|
+
!finiteWithin(value.y, -1_000_000, 1_000_000) ||
|
|
353
|
+
!finiteWithin(value.width, Number.EPSILON, 2_000_000) ||
|
|
354
|
+
!finiteWithin(value.height, Number.EPSILON, 2_000_000)) {
|
|
355
|
+
throw new Error("Canvas context bounds are invalid.");
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
x: value.x,
|
|
359
|
+
y: value.y,
|
|
360
|
+
width: value.width,
|
|
361
|
+
height: value.height,
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
const parseElementIds = (value) => {
|
|
365
|
+
if (!Array.isArray(value) ||
|
|
366
|
+
value.length > 250 ||
|
|
367
|
+
value.some((id) => typeof id !== "string" || !id.trim() || id.length > 128)) {
|
|
368
|
+
throw new Error("Canvas context elementIds are invalid.");
|
|
369
|
+
}
|
|
370
|
+
return [...new Set(value.map((id) => id.trim()))];
|
|
371
|
+
};
|
|
372
|
+
const canvasElementId = (value) => typeof value === "string" && value.trim().length > 0 && value.length <= 128;
|
|
373
|
+
export const parseCanvasConnectorRequest = (value) => {
|
|
374
|
+
if (!isRecord(value) ||
|
|
375
|
+
Object.keys(value).some((key) => !["sourceId", "targetId", "route", "waypoints", "arrowId"].includes(key)) ||
|
|
376
|
+
!canvasElementId(value.sourceId) ||
|
|
377
|
+
!canvasElementId(value.targetId) ||
|
|
378
|
+
value.sourceId === value.targetId ||
|
|
379
|
+
(value.arrowId !== undefined && !canvasElementId(value.arrowId)) ||
|
|
380
|
+
(value.route !== undefined &&
|
|
381
|
+
!["straight", "rounded", "elbow", "auto"].includes(value.route)) ||
|
|
382
|
+
(value.waypoints !== undefined &&
|
|
383
|
+
(!Array.isArray(value.waypoints) ||
|
|
384
|
+
value.waypoints.length > 16 ||
|
|
385
|
+
value.waypoints.some((point) => !Array.isArray(point) ||
|
|
386
|
+
point.length !== 2 ||
|
|
387
|
+
!point.every((coordinate) => finiteWithin(coordinate, -1_000_000, 1_000_000)))))) {
|
|
388
|
+
throw new Error("Canvas connector request is invalid.");
|
|
389
|
+
}
|
|
390
|
+
return value;
|
|
391
|
+
};
|
|
392
|
+
export const parseCanvasLabelRequest = (value) => {
|
|
393
|
+
const style = isRecord(value) ? value.style : undefined;
|
|
394
|
+
if (!isRecord(value) ||
|
|
395
|
+
Object.keys(value).some((key) => !["containerId", "text", "style"].includes(key)) ||
|
|
396
|
+
!canvasElementId(value.containerId) ||
|
|
397
|
+
typeof value.text !== "string" ||
|
|
398
|
+
!value.text.trim() ||
|
|
399
|
+
value.text.length > 20_000 ||
|
|
400
|
+
(style !== undefined &&
|
|
401
|
+
(!isRecord(style) ||
|
|
402
|
+
Object.keys(style).some((key) => !["fontSize", "fontFamily", "strokeColor", "textAlign", "verticalAlign"].includes(key)) ||
|
|
403
|
+
(style.fontSize !== undefined && !finiteWithin(style.fontSize, 1, 256)) ||
|
|
404
|
+
(style.fontFamily !== undefined && !finiteWithin(style.fontFamily, 1, 100)) ||
|
|
405
|
+
(style.strokeColor !== undefined &&
|
|
406
|
+
(typeof style.strokeColor !== "string" || style.strokeColor.length > 128)) ||
|
|
407
|
+
(style.textAlign !== undefined &&
|
|
408
|
+
!["left", "center", "right"].includes(style.textAlign)) ||
|
|
409
|
+
(style.verticalAlign !== undefined &&
|
|
410
|
+
!["top", "middle", "bottom"].includes(style.verticalAlign))))) {
|
|
411
|
+
throw new Error("Canvas label request is invalid.");
|
|
412
|
+
}
|
|
413
|
+
return value;
|
|
414
|
+
};
|
|
415
|
+
export const parseCanvasDiagramCodeRequest = (value) => {
|
|
416
|
+
if (!isRecord(value) ||
|
|
417
|
+
Object.keys(value).some((key) => !["format", "source", "operationId", "replaceOperationId"].includes(key)) ||
|
|
418
|
+
value.format !== "mermaid" ||
|
|
419
|
+
typeof value.source !== "string" ||
|
|
420
|
+
!value.source.trim() ||
|
|
421
|
+
Buffer.byteLength(value.source, "utf8") > MAX_DIAGRAM_SOURCE_BYTES ||
|
|
422
|
+
typeof value.operationId !== "string" ||
|
|
423
|
+
!/^[A-Za-z0-9_-]{1,128}$/.test(value.operationId) ||
|
|
424
|
+
(value.replaceOperationId !== undefined &&
|
|
425
|
+
(typeof value.replaceOperationId !== "string" ||
|
|
426
|
+
!/^[A-Za-z0-9_-]{1,128}$/.test(value.replaceOperationId) ||
|
|
427
|
+
value.replaceOperationId === value.operationId))) {
|
|
428
|
+
throw new Error("Diagram code request is invalid or exceeds 256 KiB.");
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
format: "mermaid",
|
|
432
|
+
source: value.source,
|
|
433
|
+
operationId: value.operationId,
|
|
434
|
+
...(typeof value.replaceOperationId === "string"
|
|
435
|
+
? { replaceOperationId: value.replaceOperationId }
|
|
436
|
+
: {})
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
export const parseCanvasContextRequest = (value) => {
|
|
440
|
+
if (!isRecord(value)) {
|
|
441
|
+
throw new Error("Canvas context request must be an object.");
|
|
442
|
+
}
|
|
443
|
+
const hasElementIds = value.elementIds !== undefined;
|
|
444
|
+
const hasBounds = value.bounds !== undefined;
|
|
445
|
+
if (hasElementIds === hasBounds) {
|
|
446
|
+
throw new Error("Choose either elementIds or bounds for canvas context.");
|
|
447
|
+
}
|
|
448
|
+
const elementIds = hasElementIds ? parseElementIds(value.elementIds) : null;
|
|
449
|
+
if (elementIds && !elementIds.length) {
|
|
450
|
+
throw new Error("Canvas context requires at least one element.");
|
|
451
|
+
}
|
|
452
|
+
const maxDimension = value.maxDimension ?? 2048;
|
|
453
|
+
if (!finiteWithin(maxDimension, 256, 4096)) {
|
|
454
|
+
throw new Error("Canvas context maxDimension must be 256 to 4096.");
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
...(elementIds ? { elementIds } : {}),
|
|
458
|
+
...(hasBounds ? { bounds: parseContextBounds(value.bounds) } : {}),
|
|
459
|
+
includeSourceImages: value.includeSourceImages !== false,
|
|
460
|
+
maxDimension: maxDimension,
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
export const parseCanvasContextReference = (value) => {
|
|
464
|
+
if (!isRecord(value) ||
|
|
465
|
+
typeof value.id !== "string" ||
|
|
466
|
+
!/^[a-f0-9-]{36}$/i.test(value.id)) {
|
|
467
|
+
throw new Error("Canvas context reference is invalid.");
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
id: value.id,
|
|
471
|
+
elementIds: parseElementIds(value.elementIds),
|
|
472
|
+
bounds: parseContextBounds(value.bounds),
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
export const parseCanvasOperations = (value) => {
|
|
476
|
+
if (!isRecord(value)) {
|
|
477
|
+
throw new Error("Canvas operations must be an object.");
|
|
478
|
+
}
|
|
479
|
+
const upsertElements = value.upsertElements ?? [];
|
|
480
|
+
const deleteElementIds = value.deleteElementIds ?? [];
|
|
481
|
+
const files = value.files ?? [];
|
|
482
|
+
if (!Array.isArray(upsertElements)) {
|
|
483
|
+
throw new Error("upsertElements must be an array.");
|
|
484
|
+
}
|
|
485
|
+
if (!Array.isArray(deleteElementIds) ||
|
|
486
|
+
deleteElementIds.some((id) => typeof id !== "string" || !id.trim())) {
|
|
487
|
+
throw new Error("deleteElementIds must contain non-empty strings.");
|
|
488
|
+
}
|
|
489
|
+
if (!Array.isArray(files) || files.length > 8) {
|
|
490
|
+
throw new Error("files must be an array of at most 8 canvas assets.");
|
|
491
|
+
}
|
|
492
|
+
const parsedFiles = files.map((file) => {
|
|
493
|
+
if (!isRecord(file) ||
|
|
494
|
+
typeof file.id !== "string" ||
|
|
495
|
+
!/^[a-zA-Z0-9_-]{1,128}$/.test(file.id) ||
|
|
496
|
+
(file.mimeType !== "image/png" &&
|
|
497
|
+
file.mimeType !== "image/jpeg" &&
|
|
498
|
+
file.mimeType !== "image/gif" &&
|
|
499
|
+
file.mimeType !== "image/webp") ||
|
|
500
|
+
typeof file.dataURL !== "string" ||
|
|
501
|
+
!file.dataURL.startsWith(`data:${file.mimeType};base64,`) ||
|
|
502
|
+
file.dataURL.length > Math.ceil((MAX_CANVAS_ASSET_BYTES * 4) / 3) + 64 ||
|
|
503
|
+
typeof file.created !== "number" ||
|
|
504
|
+
!Number.isFinite(file.created) ||
|
|
505
|
+
file.created <= 0) {
|
|
506
|
+
throw new Error("files contains an invalid canvas image asset.");
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
id: file.id,
|
|
510
|
+
mimeType: file.mimeType,
|
|
511
|
+
dataURL: file.dataURL,
|
|
512
|
+
created: file.created,
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
if (new Set(parsedFiles.map((file) => file.id)).size !== parsedFiles.length) {
|
|
516
|
+
throw new Error("files must contain unique ids.");
|
|
517
|
+
}
|
|
518
|
+
return { upsertElements, deleteElementIds, files: parsedFiles };
|
|
519
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawsy/companion",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.28",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Drawsy's local Companion server and background command-line controls",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Drawsy",
|
|
10
|
+
"email": "a@drawsyai.com"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/DrawsyAI/drawsy-ai-companion.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://drawsyai.com",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/DrawsyAI/drawsy-ai-companion/issues"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org/"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"drawsy-companion": "dist/drawsy/cli.js",
|
|
29
|
+
"drawsy-ai-mcp": "dist/drawsy/mcp.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist/drawsy",
|
|
33
|
+
"skills",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "1.25.2",
|
|
39
|
+
"cross-spawn": "7.0.6",
|
|
40
|
+
"zod": "^4.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|