@drawsy/companion 0.0.0-stage → 0.1.29

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,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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.29",
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
+ }