@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/SKILL.md +245 -0
- package/community/README.md +69 -0
- package/community/_template.mjs +39 -0
- package/community/helpers.mjs +1 -0
- package/package.json +44 -0
- package/primitives/basic-shapes.mjs +176 -0
- package/primitives/community-palettes.json +1 -0
- package/primitives/decorative.mjs +373 -0
- package/primitives/fills.mjs +217 -0
- package/primitives/flow-abstract.mjs +276 -0
- package/primitives/helpers.mjs +291 -0
- package/primitives/index.mjs +154 -0
- package/primitives/organic.mjs +514 -0
- package/primitives/utility.mjs +342 -0
- package/references/ALGORITHM_GUIDE.md +211 -0
- package/references/COMMUNITY.md +72 -0
- package/references/EXAMPLES.md +165 -0
- package/references/PALETTES.md +46 -0
- package/references/PRIMITIVES.md +301 -0
- package/references/PRO_TIPS.md +114 -0
- package/references/SECURITY.md +58 -0
- package/references/STROKE_FORMAT.md +78 -0
- package/references/SYMMETRY.md +59 -0
- package/references/WEBSOCKET.md +83 -0
- package/scripts/auth.mjs +145 -0
- package/scripts/clawdraw.mjs +882 -0
- package/scripts/connection.mjs +330 -0
- package/scripts/symmetry.mjs +217 -0
|
@@ -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
|
+
}
|