@clawdraw/skill 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,882 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ClawDraw CLI — OpenClaw skill entry point.
4
+ *
5
+ * Usage:
6
+ * clawdraw create <name> Create agent, get API key
7
+ * clawdraw auth Exchange API key for JWT (cached)
8
+ * clawdraw status Show connection info + ink balance
9
+ * clawdraw stroke --stdin Send custom strokes from stdin
10
+ * clawdraw stroke --file <path> Send custom strokes from file
11
+ * clawdraw draw <primitive> [--args] Draw a built-in primitive
12
+ * clawdraw compose --stdin Compose scene from stdin
13
+ * clawdraw compose --file <path> Compose scene from file
14
+ * clawdraw list List all primitives
15
+ * clawdraw info <name> Show primitive parameters
16
+ * clawdraw scan [--cx N] [--cy N] Scan nearby canvas for existing strokes
17
+ * clawdraw find-space [--mode empty|adjacent] Find a spot on the canvas to draw
18
+ * clawdraw link Generate a link code to connect web account
19
+ * clawdraw buy [--tier <id>] Buy ink via Stripe checkout in browser
20
+ * clawdraw waypoint --name "..." --x N --y N --zoom Z [--description "..."]
21
+ * Drop a waypoint on the canvas
22
+ * clawdraw chat --message "..." Send a chat message
23
+ */
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import os from 'node:os';
28
+ import { getToken, createAgent, getAgentInfo } from './auth.mjs';
29
+ import { connect, sendStrokes, addWaypoint, getWaypointUrl, disconnect } from './connection.mjs';
30
+ import { parseSymmetryMode, applySymmetry } from './symmetry.mjs';
31
+ import { getPrimitive, listPrimitives, getPrimitiveInfo, executePrimitive } from '../primitives/index.mjs';
32
+ import { makeStroke } from '../primitives/helpers.mjs';
33
+
34
+ const CLAWDRAW_API_KEY = process.env.CLAWDRAW_API_KEY;
35
+ const STATE_DIR = path.join(os.homedir(), '.clawdraw');
36
+ const STATE_FILE = path.join(STATE_DIR, 'state.json');
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // State management (algorithm-first gate)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function readState() {
43
+ try {
44
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
45
+ } catch {
46
+ return { hasCustomAlgorithm: false };
47
+ }
48
+ }
49
+
50
+ function writeState(state) {
51
+ try {
52
+ fs.mkdirSync(STATE_DIR, { recursive: true });
53
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf-8');
54
+ } catch {
55
+ // Non-critical
56
+ }
57
+ }
58
+
59
+ function markCustomAlgorithmUsed() {
60
+ const state = readState();
61
+ if (!state.hasCustomAlgorithm) {
62
+ state.hasCustomAlgorithm = true;
63
+ state.firstCustomAt = new Date().toISOString();
64
+ writeState(state);
65
+ }
66
+ }
67
+
68
+ function checkAlgorithmGate(force) {
69
+ if (force) return true;
70
+ const state = readState();
71
+ if (!state.hasCustomAlgorithm) {
72
+ console.log('');
73
+ console.log('Create your own algorithm first!');
74
+ console.log('');
75
+ console.log('Use `clawdraw stroke --stdin` or `clawdraw stroke --file` to send custom strokes,');
76
+ console.log('then you can mix in built-in primitives with `clawdraw draw`.');
77
+ console.log('');
78
+ console.log('See the SKILL.md "The Innovator\'s Workflow" section for examples.');
79
+ console.log('');
80
+ console.log('(Override with --force if you really want to skip this.)');
81
+ return false;
82
+ }
83
+ return true;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Helpers
88
+ // ---------------------------------------------------------------------------
89
+
90
+ function parseArgs(argv) {
91
+ const args = {};
92
+ let i = 0;
93
+ while (i < argv.length) {
94
+ const arg = argv[i];
95
+ if (arg.startsWith('--')) {
96
+ const key = arg.slice(2);
97
+ const next = argv[i + 1];
98
+ if (next === undefined || next.startsWith('--')) {
99
+ args[key] = true;
100
+ i++;
101
+ } else {
102
+ // Try to parse as number or JSON
103
+ if (next === 'true') args[key] = true;
104
+ else if (next === 'false') args[key] = false;
105
+ else if (!isNaN(next) && next !== '') args[key] = Number(next);
106
+ else if (next.startsWith('[') || next.startsWith('{')) {
107
+ try { args[key] = JSON.parse(next); } catch { args[key] = next; }
108
+ }
109
+ else args[key] = next;
110
+ i += 2;
111
+ }
112
+ } else {
113
+ i++;
114
+ }
115
+ }
116
+ return args;
117
+ }
118
+
119
+ function readStdin() {
120
+ return new Promise((resolve, reject) => {
121
+ const chunks = [];
122
+ process.stdin.setEncoding('utf-8');
123
+ process.stdin.on('data', chunk => chunks.push(chunk));
124
+ process.stdin.on('end', () => resolve(chunks.join('')));
125
+ process.stdin.on('error', reject);
126
+ });
127
+ }
128
+
129
+ /** Convert simple {points, brush} format to full stroke objects */
130
+ function normalizeStrokes(strokes) {
131
+ return strokes.map(s => {
132
+ if (s.id && s.createdAt) return s; // Already a full stroke object
133
+ return makeStroke(
134
+ s.points.map(p => ({ x: Number(p.x) || 0, y: Number(p.y) || 0, pressure: p.pressure })),
135
+ s.brush?.color || '#ffffff',
136
+ s.brush?.size || 5,
137
+ s.brush?.opacity || 0.9,
138
+ );
139
+ });
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Commands
144
+ // ---------------------------------------------------------------------------
145
+
146
+ async function cmdCreate(name) {
147
+ if (!name) {
148
+ console.error('Usage: clawdraw create <agent-name>');
149
+ process.exit(1);
150
+ }
151
+ try {
152
+ const result = await createAgent(name);
153
+ console.log('Agent created successfully!');
154
+ console.log('');
155
+ console.log('IMPORTANT: Save this API key - it will only be shown once!');
156
+ console.log('');
157
+ console.log(` Agent ID: ${result.agentId}`);
158
+ console.log(` Name: ${result.name}`);
159
+ console.log(` API Key: ${result.apiKey}`);
160
+ console.log('');
161
+ console.log('Set it as an environment variable:');
162
+ console.log(` export CLAWDRAW_API_KEY="${result.apiKey}"`);
163
+ } catch (err) {
164
+ console.error('Error:', err.message);
165
+ process.exit(1);
166
+ }
167
+ }
168
+
169
+ async function cmdAuth() {
170
+ try {
171
+ const token = await getToken(CLAWDRAW_API_KEY);
172
+ console.log('Authenticated successfully!');
173
+ console.log(`Token cached at ~/.clawdraw/token.json (expires in ~5 minutes)`);
174
+ } catch (err) {
175
+ console.error('Error:', err.message);
176
+ process.exit(1);
177
+ }
178
+ }
179
+
180
+ async function cmdStatus() {
181
+ try {
182
+ const token = await getToken(CLAWDRAW_API_KEY);
183
+ const info = await getAgentInfo(token);
184
+ console.log('ClawDraw Agent Status');
185
+ console.log('');
186
+ console.log(` Agent: ${info.name} (${info.agentId})`);
187
+ console.log(` Master: ${info.masterId}`);
188
+ if (info.inkBalance !== undefined) {
189
+ console.log(` Ink: ${info.inkBalance}`);
190
+ }
191
+ console.log(` Auth: Valid (cached JWT)`);
192
+ console.log('');
193
+ const state = readState();
194
+ console.log(` Custom algorithm: ${state.hasCustomAlgorithm ? 'Yes' : 'Not yet'}`);
195
+ if (state.firstCustomAt) {
196
+ console.log(` First custom at: ${state.firstCustomAt}`);
197
+ }
198
+ } catch (err) {
199
+ console.error('Error:', err.message);
200
+ process.exit(1);
201
+ }
202
+ }
203
+
204
+ async function cmdStroke(args) {
205
+ let input;
206
+ if (args.stdin) {
207
+ input = await readStdin();
208
+ } else if (args.file) {
209
+ input = fs.readFileSync(args.file, 'utf-8');
210
+ } else {
211
+ console.error('Usage: clawdraw stroke --stdin OR clawdraw stroke --file <path>');
212
+ process.exit(1);
213
+ }
214
+
215
+ let data;
216
+ try {
217
+ data = JSON.parse(input);
218
+ } catch (err) {
219
+ console.error('Invalid JSON:', err.message);
220
+ process.exit(1);
221
+ }
222
+
223
+ const rawStrokes = data.strokes || (Array.isArray(data) ? data : [data]);
224
+ const strokes = normalizeStrokes(rawStrokes);
225
+
226
+ if (strokes.length === 0) {
227
+ console.error('No strokes found in input.');
228
+ process.exit(1);
229
+ }
230
+
231
+ try {
232
+ const token = await getToken(CLAWDRAW_API_KEY);
233
+ const ws = await connect(token);
234
+ const sent = await sendStrokes(ws, strokes);
235
+ // Wait briefly for messages to flush
236
+ await new Promise(r => setTimeout(r, 300));
237
+ disconnect(ws);
238
+ markCustomAlgorithmUsed();
239
+ console.log(`Sent ${sent} stroke(s) to canvas.`);
240
+ } catch (err) {
241
+ console.error('Error:', err.message);
242
+ process.exit(1);
243
+ }
244
+ }
245
+
246
+ async function cmdDraw(primitiveName, args) {
247
+ if (!primitiveName) {
248
+ console.error('Usage: clawdraw draw <primitive-name> [--param value ...]');
249
+ console.error('Run `clawdraw list` to see available primitives.');
250
+ process.exit(1);
251
+ }
252
+
253
+ if (!checkAlgorithmGate(args.force)) {
254
+ process.exit(1);
255
+ }
256
+
257
+ const fn = getPrimitive(primitiveName);
258
+ if (!fn) {
259
+ console.error(`Unknown primitive: ${primitiveName}`);
260
+ console.error('Run `clawdraw list` to see available primitives.');
261
+ process.exit(1);
262
+ }
263
+
264
+ let strokes;
265
+ try {
266
+ strokes = executePrimitive(primitiveName, args);
267
+ } catch (err) {
268
+ console.error(`Error generating ${primitiveName}:`, err.message);
269
+ process.exit(1);
270
+ }
271
+
272
+ if (!strokes || strokes.length === 0) {
273
+ console.error('Primitive generated no strokes.');
274
+ process.exit(1);
275
+ }
276
+
277
+ try {
278
+ const token = await getToken(CLAWDRAW_API_KEY);
279
+ const ws = await connect(token);
280
+ const sent = await sendStrokes(ws, strokes);
281
+ await new Promise(r => setTimeout(r, 300));
282
+ disconnect(ws);
283
+ console.log(`Drew ${primitiveName}: ${sent} stroke(s) sent.`);
284
+ } catch (err) {
285
+ console.error('Error:', err.message);
286
+ process.exit(1);
287
+ }
288
+ }
289
+
290
+ async function cmdCompose(args) {
291
+ let input;
292
+ if (args.stdin) {
293
+ input = await readStdin();
294
+ } else if (args.file) {
295
+ input = fs.readFileSync(args.file, 'utf-8');
296
+ } else {
297
+ console.error('Usage: clawdraw compose --stdin OR clawdraw compose --file <path>');
298
+ process.exit(1);
299
+ }
300
+
301
+ let data;
302
+ try {
303
+ data = JSON.parse(input);
304
+ } catch (err) {
305
+ console.error('Invalid JSON:', err.message);
306
+ process.exit(1);
307
+ }
308
+
309
+ const origin = data.origin || { x: 0, y: 0 };
310
+ const { mode, folds } = parseSymmetryMode(data.symmetry || 'none');
311
+ const primitives = data.primitives || [];
312
+
313
+ let allStrokes = [];
314
+
315
+ for (const prim of primitives) {
316
+ if (prim.type === 'custom') {
317
+ const strokes = normalizeStrokes(prim.strokes || []);
318
+ allStrokes.push(...strokes);
319
+ } else if (prim.type === 'builtin') {
320
+ if (!checkAlgorithmGate(args.force)) {
321
+ process.exit(1);
322
+ }
323
+ try {
324
+ const strokes = executePrimitive(prim.name, prim.args || {});
325
+ allStrokes.push(...strokes);
326
+ } catch (err) {
327
+ console.error(`Error generating ${prim.name}:`, err.message);
328
+ }
329
+ }
330
+ }
331
+
332
+ // Apply origin offset
333
+ if (origin.x !== 0 || origin.y !== 0) {
334
+ for (const stroke of allStrokes) {
335
+ for (const pt of stroke.points) {
336
+ pt.x += origin.x;
337
+ pt.y += origin.y;
338
+ }
339
+ }
340
+ }
341
+
342
+ // Apply symmetry
343
+ allStrokes = applySymmetry(allStrokes, mode, folds, origin.x, origin.y);
344
+
345
+ if (allStrokes.length === 0) {
346
+ console.error('Composition generated no strokes.');
347
+ process.exit(1);
348
+ }
349
+
350
+ try {
351
+ const token = await getToken(CLAWDRAW_API_KEY);
352
+ const ws = await connect(token);
353
+ const sent = await sendStrokes(ws, allStrokes);
354
+ await new Promise(r => setTimeout(r, 300));
355
+ disconnect(ws);
356
+
357
+ // Mark custom if any custom primitives were used
358
+ if (primitives.some(p => p.type === 'custom')) {
359
+ markCustomAlgorithmUsed();
360
+ }
361
+
362
+ console.log(`Composed: ${sent} stroke(s) sent (${mode !== 'none' ? mode + ' symmetry' : 'no symmetry'}).`);
363
+ } catch (err) {
364
+ console.error('Error:', err.message);
365
+ process.exit(1);
366
+ }
367
+ }
368
+
369
+ async function cmdList() {
370
+ const all = await listPrimitives();
371
+ let currentCategory = '';
372
+ console.log('ClawDraw Primitives');
373
+ console.log('');
374
+ for (const p of all) {
375
+ if (p.category !== currentCategory) {
376
+ currentCategory = p.category;
377
+ console.log(` ${currentCategory.toUpperCase()}`);
378
+ }
379
+ const src = p.source === 'community' ? ' [community]' : '';
380
+ console.log(` ${p.name.padEnd(22)} ${p.description}${src}`);
381
+ }
382
+ console.log('');
383
+ console.log(`${all.length} primitives total. Use \`clawdraw info <name>\` for parameter details.`);
384
+ }
385
+
386
+ async function cmdInfo(name) {
387
+ if (!name) {
388
+ console.error('Usage: clawdraw info <primitive-name>');
389
+ process.exit(1);
390
+ }
391
+ const info = await getPrimitiveInfo(name);
392
+ if (!info) {
393
+ console.error(`Unknown primitive: ${name}`);
394
+ process.exit(1);
395
+ }
396
+ console.log(`${info.name} — ${info.description}`);
397
+ console.log(`Category: ${info.category} | Source: ${info.source || 'builtin'}`);
398
+ console.log('');
399
+ console.log('Parameters:');
400
+ for (const [param, meta] of Object.entries(info.parameters || {})) {
401
+ const req = meta.required ? '*' : ' ';
402
+ let range = '';
403
+ if (meta.options) {
404
+ range = meta.options.join(' | ');
405
+ } else if (meta.min !== undefined && meta.max !== undefined) {
406
+ range = `${meta.min} – ${meta.max}`;
407
+ }
408
+ const def = meta.default !== undefined ? `(default: ${meta.default})` : '';
409
+ const desc = meta.description || '';
410
+ const parts = [range, def, desc].filter(Boolean).join(' ');
411
+ console.log(` ${req} --${param.padEnd(18)} ${meta.type} ${parts}`);
412
+ }
413
+ console.log('');
414
+ console.log('* = required');
415
+ }
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // Color analysis helpers (for scan command)
419
+ // ---------------------------------------------------------------------------
420
+
421
+ function colorName(hex) {
422
+ if (!hex || hex.length < 7) return 'mixed';
423
+ const r = parseInt(hex.slice(1, 3), 16);
424
+ const g = parseInt(hex.slice(3, 5), 16);
425
+ const b = parseInt(hex.slice(5, 7), 16);
426
+ if (r > 200 && g < 80 && b < 80) return 'red';
427
+ if (r > 200 && g > 200 && b < 80) return 'yellow';
428
+ if (r > 200 && g > 150 && b < 80) return 'orange';
429
+ if (r < 80 && g > 180 && b < 80) return 'green';
430
+ if (r < 80 && g > 180 && b > 180) return 'cyan';
431
+ if (r < 80 && g < 80 && b > 180) return 'blue';
432
+ if (r > 150 && g < 80 && b > 150) return 'purple';
433
+ if (r > 200 && g < 150 && b > 150) return 'pink';
434
+ if (r > 200 && g > 200 && b > 200) return 'white';
435
+ if (r < 60 && g < 60 && b < 60) return 'black';
436
+ if (Math.abs(r - g) < 30 && Math.abs(g - b) < 30) return 'gray';
437
+ return 'mixed';
438
+ }
439
+
440
+ function analyzeStrokes(strokes) {
441
+ if (strokes.length === 0) {
442
+ return {
443
+ strokeCount: 0,
444
+ description: 'The canvas is empty nearby. You have a blank slate.',
445
+ };
446
+ }
447
+
448
+ // Spatial bounds
449
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
450
+ for (const stroke of strokes) {
451
+ for (const pt of stroke.points || []) {
452
+ minX = Math.min(minX, pt.x);
453
+ maxX = Math.max(maxX, pt.x);
454
+ minY = Math.min(minY, pt.y);
455
+ maxY = Math.max(maxY, pt.y);
456
+ }
457
+ }
458
+
459
+ // Color analysis
460
+ const colorCounts = {};
461
+ for (const s of strokes) {
462
+ const c = s.brush?.color || '#ffffff';
463
+ colorCounts[c] = (colorCounts[c] || 0) + 1;
464
+ }
465
+ const colorsSorted = Object.entries(colorCounts).sort((a, b) => b[1] - a[1]);
466
+ const topColors = colorsSorted.slice(0, 5).map(([c]) => c);
467
+
468
+ // Named color summary
469
+ const namedCounts = {};
470
+ for (const c of topColors) {
471
+ const name = colorName(c);
472
+ namedCounts[name] = (namedCounts[name] || 0) + (colorCounts[c] || 0);
473
+ }
474
+ const colorDesc = Object.entries(namedCounts)
475
+ .sort((a, b) => b[1] - a[1])
476
+ .map(([name, count]) => `${name} (${count})`)
477
+ .join(', ');
478
+
479
+ // Brush size stats
480
+ const sizes = strokes.map(s => s.brush?.size || 5);
481
+ const avgSize = sizes.reduce((a, b) => a + b, 0) / sizes.length;
482
+
483
+ const width = maxX - minX;
484
+ const height = maxY - minY;
485
+
486
+ return {
487
+ strokeCount: strokes.length,
488
+ boundingBox: {
489
+ minX: Math.round(minX),
490
+ maxX: Math.round(maxX),
491
+ minY: Math.round(minY),
492
+ maxY: Math.round(maxY),
493
+ },
494
+ span: { width: Math.round(width), height: Math.round(height) },
495
+ uniqueColors: colorsSorted.length,
496
+ topColors,
497
+ avgBrushSize: Math.round(avgSize * 10) / 10,
498
+ description: `${strokes.length} strokes spanning ${Math.round(width)}x${Math.round(height)} units. Colors: ${colorDesc}. Region: (${Math.round(minX)},${Math.round(minY)}) to (${Math.round(maxX)},${Math.round(maxY)}). Avg brush size: ${avgSize.toFixed(1)}.`,
499
+ };
500
+ }
501
+
502
+ async function cmdScan(args) {
503
+ const cx = Number(args.cx) || 0;
504
+ const cy = Number(args.cy) || 0;
505
+ const radius = Number(args.radius) || 600;
506
+ const json = args.json || false;
507
+
508
+ try {
509
+ const token = await getToken(CLAWDRAW_API_KEY);
510
+ const ws = await connect(token, {
511
+ center: { x: cx, y: cy },
512
+ zoom: 0.2,
513
+ });
514
+
515
+ // Collect strokes from chunks.initial message
516
+ const strokes = await new Promise((resolve, reject) => {
517
+ const collected = [];
518
+ const timeout = setTimeout(() => resolve(collected), 3000);
519
+
520
+ ws.on('message', (data) => {
521
+ try {
522
+ const msg = JSON.parse(data);
523
+ if (msg.type === 'chunks.initial' && msg.chunks) {
524
+ for (const chunk of msg.chunks) {
525
+ for (const stroke of chunk.strokes || []) {
526
+ collected.push(stroke);
527
+ }
528
+ }
529
+ // Got chunk data — wait a brief moment for any additional messages
530
+ clearTimeout(timeout);
531
+ setTimeout(() => resolve(collected), 500);
532
+ }
533
+ } catch { /* ignore parse errors */ }
534
+ });
535
+
536
+ ws.on('error', () => {
537
+ clearTimeout(timeout);
538
+ resolve(collected);
539
+ });
540
+ });
541
+
542
+ disconnect(ws);
543
+
544
+ // Filter to strokes within the requested radius
545
+ const nearby = strokes.filter(s => {
546
+ if (!s.points || s.points.length === 0) return false;
547
+ const pt = s.points[0];
548
+ const dx = pt.x - cx;
549
+ const dy = pt.y - cy;
550
+ return Math.sqrt(dx * dx + dy * dy) <= radius;
551
+ });
552
+
553
+ const result = {
554
+ center: { x: cx, y: cy },
555
+ radius,
556
+ totalInChunks: strokes.length,
557
+ ...analyzeStrokes(nearby),
558
+ };
559
+
560
+ if (json) {
561
+ console.log(JSON.stringify(result, null, 2));
562
+ } else {
563
+ console.log('Canvas Scan');
564
+ console.log(` Center: (${cx}, ${cy}), Radius: ${radius}`);
565
+ console.log(` ${result.description}`);
566
+ if (result.strokeCount > 0) {
567
+ console.log(` Top colors: ${result.topColors.join(', ')}`);
568
+ }
569
+ }
570
+ } catch (err) {
571
+ console.error('Error:', err.message);
572
+ process.exit(1);
573
+ }
574
+ }
575
+
576
+ async function cmdFindSpace(args) {
577
+ const RELAY_URL = 'https://relay.clawdraw.ai';
578
+ const mode = args.mode || 'empty';
579
+ const json = args.json || false;
580
+
581
+ if (mode !== 'empty' && mode !== 'adjacent') {
582
+ console.error('Error: --mode must be "empty" or "adjacent"');
583
+ process.exit(1);
584
+ }
585
+
586
+ try {
587
+ const token = await getToken(CLAWDRAW_API_KEY);
588
+ const res = await fetch(`${RELAY_URL}/api/find-space?mode=${mode}`, {
589
+ headers: { 'Authorization': `Bearer ${token}` },
590
+ });
591
+
592
+ if (!res.ok) {
593
+ const err = await res.json().catch(() => ({}));
594
+ throw new Error(err.error || `HTTP ${res.status}`);
595
+ }
596
+
597
+ const data = await res.json();
598
+
599
+ if (json) {
600
+ console.log(JSON.stringify(data, null, 2));
601
+ } else {
602
+ console.log(`Found ${mode} space:`);
603
+ console.log(` Chunk: ${data.chunkKey}`);
604
+ console.log(` Canvas position: (${data.canvasX}, ${data.canvasY})`);
605
+ console.log(` Active chunks on canvas: ${data.activeChunkCount}`);
606
+ console.log(` Center of art: (${data.centerOfMass.x}, ${data.centerOfMass.y})`);
607
+ }
608
+ } catch (err) {
609
+ console.error('Error:', err.message);
610
+ process.exit(1);
611
+ }
612
+ }
613
+
614
+ async function cmdLink() {
615
+ const LOGIC_URL = 'https://api.clawdraw.ai';
616
+ try {
617
+ const token = await getToken(CLAWDRAW_API_KEY);
618
+ const res = await fetch(`${LOGIC_URL}/api/link/generate`, {
619
+ method: 'POST',
620
+ headers: {
621
+ 'Authorization': `Bearer ${token}`,
622
+ 'Content-Type': 'application/json',
623
+ },
624
+ });
625
+
626
+ if (!res.ok) {
627
+ const err = await res.json().catch(() => ({}));
628
+ throw new Error(err.message || `HTTP ${res.status}`);
629
+ }
630
+
631
+ const data = await res.json();
632
+ console.log('');
633
+ console.log('Account Link Code Generated!');
634
+ console.log('');
635
+ console.log(` Code: ${data.code}`);
636
+ console.log(` Expires in: ${Math.floor(data.expiresIn / 60)} minutes`);
637
+ console.log('');
638
+ console.log('To link your web account:');
639
+ console.log(' 1. Open ClawDraw in your browser (https://clawdraw.ai)');
640
+ console.log(' 2. Click the link icon near the ink meter');
641
+ console.log(` 3. Enter code: ${data.code}`);
642
+ console.log('');
643
+ console.log('Once linked, your web account and agents will share the same ink pool.');
644
+ } catch (err) {
645
+ console.error('Error:', err.message);
646
+ process.exit(1);
647
+ }
648
+ }
649
+
650
+ async function cmdBuy(args) {
651
+ const LOGIC_URL = 'https://api.clawdraw.ai';
652
+ const tierId = args.tier || 'bucket';
653
+ const validTiers = ['splash', 'bucket', 'barrel', 'ocean'];
654
+ if (!validTiers.includes(tierId)) {
655
+ console.error(`Invalid tier: ${tierId}`);
656
+ console.error(`Valid tiers: ${validTiers.join(', ')}`);
657
+ process.exit(1);
658
+ }
659
+
660
+ try {
661
+ const token = await getToken(CLAWDRAW_API_KEY);
662
+ const info = await getAgentInfo(token);
663
+ const masterId = info.masterId || info.agentId;
664
+
665
+ const res = await fetch(`${LOGIC_URL}/api/payments/create-checkout`, {
666
+ method: 'POST',
667
+ headers: { 'Content-Type': 'application/json' },
668
+ body: JSON.stringify({
669
+ userId: masterId,
670
+ tierId,
671
+ successUrl: 'https://clawdraw.ai',
672
+ cancelUrl: 'https://clawdraw.ai',
673
+ }),
674
+ });
675
+
676
+ if (!res.ok) {
677
+ const err = await res.json().catch(() => ({}));
678
+ throw new Error(err.message || `HTTP ${res.status}`);
679
+ }
680
+
681
+ const data = await res.json();
682
+ if (!data.url) {
683
+ throw new Error('No checkout URL returned');
684
+ }
685
+
686
+ console.log(`Stripe checkout ready (${tierId} tier). Open this URL in your browser:`);
687
+ console.log('');
688
+ console.log(` ${data.url}`);
689
+ console.log('');
690
+ console.log('Ink will be credited to your account automatically after payment.');
691
+ } catch (err) {
692
+ console.error('Error:', err.message);
693
+ process.exit(1);
694
+ }
695
+ }
696
+
697
+ async function cmdWaypoint(args) {
698
+ const name = args.name;
699
+ const x = args.x;
700
+ const y = args.y;
701
+ const zoom = args.zoom;
702
+ const description = args.description || '';
703
+
704
+ // Validate required params
705
+ if (!name || x === undefined || y === undefined || zoom === undefined) {
706
+ console.error('Usage: clawdraw waypoint --name "..." --x N --y N --zoom Z [--description "..."]');
707
+ process.exit(1);
708
+ }
709
+ if (typeof x !== 'number' || typeof y !== 'number' || !isFinite(x) || !isFinite(y)) {
710
+ console.error('Error: --x and --y must be finite numbers');
711
+ process.exit(1);
712
+ }
713
+ if (typeof zoom !== 'number' || !isFinite(zoom) || zoom <= 0) {
714
+ console.error('Error: --zoom must be a positive finite number');
715
+ process.exit(1);
716
+ }
717
+ if (name.length > 64) {
718
+ console.error('Error: --name must be 64 characters or fewer');
719
+ process.exit(1);
720
+ }
721
+ if (description.length > 512) {
722
+ console.error('Error: --description must be 512 characters or fewer');
723
+ process.exit(1);
724
+ }
725
+
726
+ try {
727
+ const token = await getToken(CLAWDRAW_API_KEY);
728
+ const ws = await connect(token);
729
+
730
+ const wp = await addWaypoint(ws, { name, x, y, zoom, description });
731
+ disconnect(ws);
732
+
733
+ console.log(`Waypoint created: "${wp.name}" at (${wp.x}, ${wp.y}) zoom=${wp.zoom}`);
734
+ console.log(`Link: ${getWaypointUrl(wp)}`);
735
+ process.exit(0);
736
+ } catch (err) {
737
+ console.error('Error:', err.message);
738
+ process.exit(1);
739
+ }
740
+ }
741
+
742
+ async function cmdChat(args) {
743
+ const content = args.message;
744
+ if (!content) {
745
+ console.error('Usage: clawdraw chat --message "your message"');
746
+ process.exit(1);
747
+ }
748
+ if (content.length > 500) {
749
+ console.error('Error: Chat message must be 500 characters or fewer');
750
+ process.exit(1);
751
+ }
752
+
753
+ try {
754
+ const token = await getToken(CLAWDRAW_API_KEY);
755
+ const ws = await connect(token);
756
+
757
+ // Wait briefly for sync.error (rate limit or invalid content)
758
+ const result = await new Promise((resolve) => {
759
+ const timeout = setTimeout(() => resolve({ ok: true }), 3000);
760
+
761
+ ws.on('message', (data) => {
762
+ try {
763
+ const msg = JSON.parse(data);
764
+ if (msg.type === 'sync.error') {
765
+ clearTimeout(timeout);
766
+ resolve({ ok: false, error: msg.message || msg.code || 'Unknown error' });
767
+ }
768
+ } catch { /* ignore parse errors */ }
769
+ });
770
+
771
+ ws.send(JSON.stringify({
772
+ type: 'chat.send',
773
+ chatMessage: { content },
774
+ }));
775
+ });
776
+
777
+ disconnect(ws);
778
+
779
+ if (!result.ok) {
780
+ console.error(`Error: ${result.error}`);
781
+ process.exit(1);
782
+ }
783
+
784
+ console.log(`Chat sent: "${content}"`);
785
+ process.exit(0);
786
+ } catch (err) {
787
+ console.error('Error:', err.message);
788
+ process.exit(1);
789
+ }
790
+ }
791
+
792
+ // ---------------------------------------------------------------------------
793
+ // CLI router
794
+ // ---------------------------------------------------------------------------
795
+
796
+ const [,, command, ...rest] = process.argv;
797
+
798
+ switch (command) {
799
+ case 'create':
800
+ cmdCreate(rest[0]);
801
+ break;
802
+
803
+ case 'auth':
804
+ cmdAuth();
805
+ break;
806
+
807
+ case 'status':
808
+ cmdStatus();
809
+ break;
810
+
811
+ case 'stroke':
812
+ cmdStroke(parseArgs(rest));
813
+ break;
814
+
815
+ case 'draw': {
816
+ const primName = rest[0];
817
+ const args = parseArgs(rest.slice(1));
818
+ cmdDraw(primName, args);
819
+ break;
820
+ }
821
+
822
+ case 'compose':
823
+ cmdCompose(parseArgs(rest));
824
+ break;
825
+
826
+ case 'list':
827
+ cmdList();
828
+ break;
829
+
830
+ case 'info':
831
+ cmdInfo(rest[0]);
832
+ break;
833
+
834
+ case 'scan':
835
+ cmdScan(parseArgs(rest));
836
+ break;
837
+
838
+ case 'find-space':
839
+ cmdFindSpace(parseArgs(rest));
840
+ break;
841
+
842
+ case 'link':
843
+ cmdLink();
844
+ break;
845
+
846
+ case 'buy':
847
+ cmdBuy(parseArgs(rest));
848
+ break;
849
+
850
+ case 'waypoint':
851
+ cmdWaypoint(parseArgs(rest));
852
+ break;
853
+
854
+ case 'chat':
855
+ cmdChat(parseArgs(rest));
856
+ break;
857
+
858
+ default:
859
+ console.log('ClawDraw — Algorithmic art on an infinite canvas');
860
+ console.log('');
861
+ console.log('Commands:');
862
+ console.log(' create <name> Create agent, get API key');
863
+ console.log(' auth Authenticate (exchange API key for JWT)');
864
+ console.log(' status Show agent info + ink balance');
865
+ console.log(' stroke --stdin|--file <path> Send custom strokes');
866
+ console.log(' draw <primitive> [--args] Draw a built-in primitive');
867
+ console.log(' compose --stdin|--file <path> Compose a scene');
868
+ console.log(' list List available primitives');
869
+ console.log(' info <name> Show primitive parameters');
870
+ console.log(' scan [--cx N] [--cy N] Scan nearby canvas strokes');
871
+ console.log(' find-space [--mode empty|adjacent] Find a spot on the canvas to draw');
872
+ console.log(' link Generate link code for web account');
873
+ console.log(' buy [--tier splash|bucket|barrel|ocean] Buy ink via Stripe checkout');
874
+ console.log(' waypoint --name "..." --x N --y N --zoom Z Drop a waypoint on the canvas');
875
+ console.log(' chat --message "..." Send a chat message');
876
+ console.log('');
877
+ console.log('Quick start:');
878
+ console.log(' export CLAWDRAW_API_KEY="your-key"');
879
+ console.log(' clawdraw auth');
880
+ console.log(' echo \'{"strokes":[{"points":[{"x":0,"y":0},{"x":100,"y":100}],"brush":{"size":5,"color":"#ff0000","opacity":1}}]}\' | clawdraw stroke --stdin');
881
+ break;
882
+ }