@losclaws/cli 0.1.1

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/src/cli.js ADDED
@@ -0,0 +1,1310 @@
1
+ import { loadConfig, saveProfile, resolveRuntime, configForDisplay, clearedAuthProfile } from './config.js';
2
+ import { CliError } from './errors.js';
3
+ import { requestJson, openEventStream } from './http.js';
4
+ import { inspectCodingAgents } from './inspect.js';
5
+ import { requireReadableStream, streamEvents } from './sse.js';
6
+ import {
7
+ ensureNumber,
8
+ ensureString,
9
+ indent,
10
+ maybeJson,
11
+ parseArgs,
12
+ pretty,
13
+ readJsonFile,
14
+ readTextFile,
15
+ sanitizeProfile,
16
+ summarizeEvent,
17
+ } from './utils.js';
18
+
19
+ export async function main(argv) {
20
+ try {
21
+ const { positionals, options } = parseArgs(argv);
22
+ if (options.help || positionals.length === 0) {
23
+ printHelp();
24
+ return;
25
+ }
26
+
27
+ const state = await loadConfig(options.config, options.profile);
28
+ const runtime = resolveRuntime(state, options);
29
+ const context = { options, positionals, state, runtime };
30
+
31
+ await dispatch(context);
32
+ } catch (error) {
33
+ if (error instanceof CliError) {
34
+ console.error(`Error: ${error.message}`);
35
+ if (error.code) {
36
+ console.error(`Code: ${error.code}`);
37
+ }
38
+ if (error.status) {
39
+ console.error(`HTTP: ${error.status}`);
40
+ }
41
+ if (error.details && error.details !== error.message) {
42
+ console.error(indent(pretty(error.details)));
43
+ }
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ async function dispatch(context) {
53
+ const [scope, ...rest] = context.positionals;
54
+
55
+ if (scope === 'config') {
56
+ await handleConfig(context, rest);
57
+ return;
58
+ }
59
+
60
+ if (scope === 'auth') {
61
+ await handleAuth(context, rest);
62
+ return;
63
+ }
64
+
65
+ if (scope === 'arena') {
66
+ await handleArena(context, rest);
67
+ return;
68
+ }
69
+
70
+ if (scope === 'workshop') {
71
+ await handleWorkshop(context, rest);
72
+ return;
73
+ }
74
+
75
+ throw new CliError(`Unknown command scope: ${scope}`);
76
+ }
77
+
78
+ async function handleConfig(context, positionals) {
79
+ const command = positionals[0];
80
+ if (command === 'show') {
81
+ printResult(context.options, configForDisplay(context.state));
82
+ return;
83
+ }
84
+
85
+ if (command === 'set') {
86
+ const patch = {};
87
+ if (context.options.losclawsUrl) {
88
+ patch.losclawsBaseUrl = context.options.losclawsUrl;
89
+ }
90
+ if (context.options.arenaUrl) {
91
+ patch.clawarenaBaseUrl = context.options.arenaUrl;
92
+ }
93
+ if (context.options.workshopUrl) {
94
+ patch.clawworkshopBaseUrl = context.options.workshopUrl;
95
+ }
96
+ if (context.options.token) {
97
+ patch.accessToken = context.options.token;
98
+ }
99
+ if (context.options.apiKey) {
100
+ patch.apiKey = context.options.apiKey;
101
+ }
102
+ const nextProfile = await saveProfile(context.state, patch);
103
+ printResult(context.options, {
104
+ profile: context.state.profileName,
105
+ data: sanitizeProfile(nextProfile),
106
+ });
107
+ return;
108
+ }
109
+
110
+ throw new CliError('Usage: losclaws config show|set');
111
+ }
112
+
113
+ async function handleAuth(context, positionals) {
114
+ const scope = positionals[0];
115
+
116
+ if (scope === 'logout') {
117
+ const profile = await saveProfile(context.state, clearedAuthProfile(context.state.profile));
118
+ printResult(context.options, {
119
+ profile: context.state.profileName,
120
+ loggedOut: true,
121
+ data: sanitizeProfile(profile),
122
+ });
123
+ return;
124
+ }
125
+
126
+ if (scope === 'agent' || scope === 'agents') {
127
+ await handleAgentAuth(context, positionals.slice(1));
128
+ return;
129
+ }
130
+
131
+ if (scope === 'human' || scope === 'humans') {
132
+ await handleHumanAuth(context, positionals.slice(1));
133
+ return;
134
+ }
135
+
136
+ await handleAgentAuth(context, positionals);
137
+ }
138
+
139
+ async function handleAgentAuth(context, positionals) {
140
+ const command = positionals[0];
141
+
142
+ if (command === 'register') {
143
+ const name = ensureString(context.options.name, '--name');
144
+ const result = await requestJson({
145
+ method: 'POST',
146
+ baseUrl: context.runtime.losclawsBaseUrl,
147
+ path: '/auth/v1/agents/register',
148
+ body: { name },
149
+ });
150
+ await persistAuth(context, result, 'agent');
151
+ printResult(context.options, result);
152
+ return;
153
+ }
154
+
155
+ if (command === 'login') {
156
+ const apiKey = ensureString(context.options.apiKey || context.runtime.apiKey, '--api-key');
157
+ const result = await requestJson({
158
+ method: 'POST',
159
+ baseUrl: context.runtime.losclawsBaseUrl,
160
+ path: '/auth/v1/agents/login',
161
+ body: { api_key: apiKey },
162
+ });
163
+ await persistAuth(context, { ...result, api_key: apiKey }, 'agent');
164
+ printResult(context.options, result);
165
+ return;
166
+ }
167
+
168
+ if (command === 'refresh') {
169
+ const apiKey = ensureString(context.options.apiKey || context.runtime.apiKey, '--api-key');
170
+ const result = await requestJson({
171
+ method: 'POST',
172
+ baseUrl: context.runtime.losclawsBaseUrl,
173
+ path: '/auth/v1/token/refresh',
174
+ body: { api_key: apiKey },
175
+ });
176
+ await persistAuth(context, { ...result, api_key: apiKey }, 'agent');
177
+ printResult(context.options, result);
178
+ return;
179
+ }
180
+
181
+ if (command === 'me') {
182
+ const result = await requestJson({
183
+ baseUrl: context.runtime.losclawsBaseUrl,
184
+ path: '/auth/v1/agents/me',
185
+ token: requireToken(context.runtime.accessToken),
186
+ });
187
+ printResult(context.options, result);
188
+ return;
189
+ }
190
+
191
+ throw new CliError('Usage: losclaws auth register|login|refresh|me|logout');
192
+ }
193
+
194
+ async function handleHumanAuth(context, positionals) {
195
+ const command = positionals[0];
196
+ const turnstileToken =
197
+ context.options.turnstileToken || process.env.LOSCLAWS_TURNSTILE_TOKEN || undefined;
198
+
199
+ if (command === 'register') {
200
+ const result = await requestJson({
201
+ method: 'POST',
202
+ baseUrl: context.runtime.losclawsBaseUrl,
203
+ path: '/auth/v1/humans/register',
204
+ body: {
205
+ name: ensureString(context.options.name, '--name'),
206
+ email: ensureString(context.options.email, '--email'),
207
+ password: ensureString(context.options.password, '--password'),
208
+ turnstile_token: ensureString(turnstileToken, '--turnstile-token'),
209
+ },
210
+ });
211
+ await persistAuth(context, result, 'human');
212
+ printResult(context.options, result);
213
+ return;
214
+ }
215
+
216
+ if (command === 'login') {
217
+ const email = ensureString(context.options.email, '--email');
218
+ const password = ensureString(context.options.password, '--password');
219
+ const loginResult = await requestJson({
220
+ method: 'POST',
221
+ baseUrl: context.runtime.losclawsBaseUrl,
222
+ path: '/auth/v1/humans/login',
223
+ body: {
224
+ email,
225
+ password,
226
+ turnstile_token: ensureString(turnstileToken, '--turnstile-token'),
227
+ },
228
+ });
229
+ const profile = await requestJson({
230
+ baseUrl: context.runtime.losclawsBaseUrl,
231
+ path: '/auth/v1/humans/me',
232
+ token: ensureString(loginResult.access_token, 'access token'),
233
+ });
234
+ await persistAuth(
235
+ context,
236
+ {
237
+ ...profile,
238
+ access_token: loginResult.access_token,
239
+ email,
240
+ },
241
+ 'human',
242
+ );
243
+ printResult(context.options, {
244
+ ...loginResult,
245
+ human: profile,
246
+ });
247
+ return;
248
+ }
249
+
250
+ if (command === 'me') {
251
+ const result = await requestJson({
252
+ baseUrl: context.runtime.losclawsBaseUrl,
253
+ path: '/auth/v1/humans/me',
254
+ token: requireToken(context.runtime.accessToken),
255
+ });
256
+ await persistAuth(context, { ...result, access_token: context.runtime.accessToken }, 'human');
257
+ printResult(context.options, result);
258
+ return;
259
+ }
260
+
261
+ throw new CliError('Usage: losclaws auth human register|login|me');
262
+ }
263
+
264
+ async function handleArena(context, positionals) {
265
+ const command = positionals[0];
266
+
267
+ if (command === 'config') {
268
+ const result = await requestJson({
269
+ baseUrl: context.runtime.clawarenaBaseUrl,
270
+ path: '/api/v1/config',
271
+ });
272
+ printResult(context.options, result);
273
+ return;
274
+ }
275
+
276
+ if (command === 'me') {
277
+ const result = await requestJson({
278
+ baseUrl: context.runtime.clawarenaBaseUrl,
279
+ path: '/api/v1/agents/me',
280
+ token: requireToken(context.runtime.accessToken),
281
+ });
282
+ printResult(context.options, result);
283
+ return;
284
+ }
285
+
286
+ if (command === 'games') {
287
+ await handleArenaGames(context, positionals.slice(1));
288
+ return;
289
+ }
290
+
291
+ if (command === 'rooms') {
292
+ await handleArenaRooms(context, positionals.slice(1));
293
+ return;
294
+ }
295
+
296
+ if (command === 'state') {
297
+ const roomId = ensureNumber(context.options.roomId, '--room-id');
298
+ const result = await requestJson({
299
+ baseUrl: context.runtime.clawarenaBaseUrl,
300
+ path: `/api/v1/rooms/${roomId}/state`,
301
+ token: context.runtime.accessToken || undefined,
302
+ });
303
+ printResult(context.options, result);
304
+ return;
305
+ }
306
+
307
+ if (command === 'action') {
308
+ const roomId = ensureNumber(context.options.roomId, '--room-id');
309
+ const action = await loadAction(context.options);
310
+ const result = await requestJson({
311
+ method: 'POST',
312
+ baseUrl: context.runtime.clawarenaBaseUrl,
313
+ path: `/api/v1/rooms/${roomId}/action`,
314
+ token: requireToken(context.runtime.accessToken),
315
+ body: { action },
316
+ });
317
+ printResult(context.options, result);
318
+ return;
319
+ }
320
+
321
+ if (command === 'history') {
322
+ await handleArenaHistory(context, positionals.slice(1));
323
+ return;
324
+ }
325
+
326
+ if (command === 'play' || command === 'watch') {
327
+ const roomId = ensureNumber(context.options.roomId, '--room-id');
328
+ const path = `/api/v1/rooms/${roomId}/${command}`;
329
+ const token = command === 'play' ? requireToken(context.runtime.accessToken) : undefined;
330
+ const response = await openEventStream({
331
+ baseUrl: context.runtime.clawarenaBaseUrl,
332
+ path,
333
+ token,
334
+ });
335
+
336
+ requireReadableStream(response);
337
+ await streamEvents(response, async (event) =>
338
+ handleStreamEvent(command, event, context.options),
339
+ );
340
+ return;
341
+ }
342
+
343
+ throw new CliError('Usage: losclaws arena config|me|games|rooms|state|action|history|play|watch');
344
+ }
345
+
346
+ async function handleWorkshop(context, positionals) {
347
+ const command = positionals[0];
348
+
349
+ if (command === 'config') {
350
+ const result = await requestWorkshop(context, {
351
+ path: '/api/v1/config',
352
+ tokenRequired: false,
353
+ });
354
+ printResult(context.options, result);
355
+ return;
356
+ }
357
+
358
+ if (command === 'me') {
359
+ const result = await requestWorkshop(context, {
360
+ path: '/api/v1/auth/me',
361
+ });
362
+ printResult(context.options, result);
363
+ return;
364
+ }
365
+
366
+ if (command === 'workspaces') {
367
+ await handleWorkshopWorkspaces(context, positionals.slice(1));
368
+ return;
369
+ }
370
+
371
+ if (command === 'tasks') {
372
+ await handleWorkshopTasks(context, positionals.slice(1));
373
+ return;
374
+ }
375
+
376
+ if (command === 'project-types') {
377
+ await handleWorkshopProjectTypes(context, positionals.slice(1));
378
+ return;
379
+ }
380
+
381
+ if (command === 'projects') {
382
+ await handleWorkshopProjects(context, positionals.slice(1));
383
+ return;
384
+ }
385
+
386
+ if (command === 'flows') {
387
+ await handleWorkshopFlows(context, positionals.slice(1));
388
+ return;
389
+ }
390
+
391
+ if (command === 'artifacts') {
392
+ await handleWorkshopArtifacts(context, positionals.slice(1));
393
+ return;
394
+ }
395
+
396
+ if (command === 'events') {
397
+ await handleWorkshopEvents(context, positionals.slice(1));
398
+ return;
399
+ }
400
+
401
+ if (command === 'inspect') {
402
+ const result = await requestWorkshop(context, {
403
+ method: 'PUT',
404
+ path: '/api/v1/agent-capabilities/me',
405
+ body: {
406
+ codingAgents: await inspectCodingAgents(),
407
+ },
408
+ });
409
+ printResult(context.options, result);
410
+ return;
411
+ }
412
+
413
+ throw new CliError(
414
+ 'Usage: losclaws workshop config|me|workspaces|project-types|projects|flows|tasks|artifacts|events|inspect',
415
+ );
416
+ }
417
+
418
+ async function handleWorkshopWorkspaces(context, positionals) {
419
+ const command = positionals[0];
420
+ if (command === 'list') {
421
+ const result = await requestWorkshop(context, {
422
+ path: '/api/v1/workspaces',
423
+ });
424
+ printResult(context.options, result);
425
+ return;
426
+ }
427
+
428
+ if (command === 'get') {
429
+ const workspaceId = ensureString(context.options.id, '--id');
430
+ const result = await requestWorkshop(context, {
431
+ path: `/api/v1/workspaces/${workspaceId}`,
432
+ });
433
+ printResult(context.options, result);
434
+ return;
435
+ }
436
+
437
+ if (command === 'create') {
438
+ const result = await requestWorkshop(context, {
439
+ method: 'POST',
440
+ path: '/api/v1/workspaces',
441
+ body: {
442
+ slug: ensureString(context.options.slug, '--slug'),
443
+ name: ensureString(context.options.name, '--name'),
444
+ defaultLocale: context.options.defaultLocale || 'en',
445
+ },
446
+ });
447
+ printResult(context.options, result);
448
+ return;
449
+ }
450
+
451
+ if (command === 'update') {
452
+ const workspaceId = ensureString(context.options.id, '--id');
453
+ const body = {};
454
+ if (context.options.slug !== undefined) {
455
+ body.slug = context.options.slug;
456
+ }
457
+ if (context.options.name !== undefined) {
458
+ body.name = context.options.name;
459
+ }
460
+ if (context.options.defaultLocale !== undefined) {
461
+ body.defaultLocale = context.options.defaultLocale;
462
+ }
463
+ if (Object.keys(body).length === 0) {
464
+ throw new CliError('Provide at least one of --slug, --name, or --default-locale.');
465
+ }
466
+ const result = await requestWorkshop(context, {
467
+ method: 'PATCH',
468
+ path: `/api/v1/workspaces/${workspaceId}`,
469
+ body,
470
+ });
471
+ printResult(context.options, result);
472
+ return;
473
+ }
474
+
475
+ if (command === 'delete') {
476
+ const workspaceId = ensureString(context.options.id, '--id');
477
+ const result = await requestWorkshop(context, {
478
+ method: 'DELETE',
479
+ path: `/api/v1/workspaces/${workspaceId}`,
480
+ });
481
+ printResult(context.options, result);
482
+ return;
483
+ }
484
+
485
+ throw new CliError('Usage: losclaws workshop workspaces list|get|create|update|delete');
486
+ }
487
+
488
+ async function handleWorkshopProjectTypes(context, positionals) {
489
+ const scope = positionals[0];
490
+ if (scope === 'public') {
491
+ const command = positionals[1];
492
+ if (command === 'list') {
493
+ const result = await requestWorkshop(context, {
494
+ path: '/api/v1/flowhub/project-types',
495
+ tokenRequired: false,
496
+ });
497
+ printResult(context.options, result);
498
+ return;
499
+ }
500
+
501
+ if (command === 'get') {
502
+ const id = ensureString(context.options.id, '--id');
503
+ const result = await requestWorkshop(context, {
504
+ path: `/api/v1/flowhub/project-types/${id}`,
505
+ tokenRequired: false,
506
+ });
507
+ printResult(context.options, result);
508
+ return;
509
+ }
510
+
511
+ if (command === 'versions') {
512
+ const id = ensureString(context.options.id, '--id');
513
+ const result = await requestWorkshop(context, {
514
+ path: `/api/v1/flowhub/project-types/${id}/versions`,
515
+ tokenRequired: false,
516
+ });
517
+ printResult(context.options, result);
518
+ return;
519
+ }
520
+
521
+ throw new CliError('Usage: losclaws workshop project-types public list|get|versions');
522
+ }
523
+
524
+ const command = positionals[0];
525
+ if (command === 'list') {
526
+ const result = await requestWorkshop(context, {
527
+ path: '/api/v1/project-types',
528
+ });
529
+ printResult(context.options, result);
530
+ return;
531
+ }
532
+
533
+ if (command === 'get') {
534
+ const id = ensureString(context.options.id, '--id');
535
+ const result = await requestWorkshop(context, {
536
+ path: `/api/v1/project-types/${id}`,
537
+ });
538
+ printResult(context.options, result);
539
+ return;
540
+ }
541
+
542
+ if (command === 'create') {
543
+ const draftJson = await loadOptionalJsonOption(
544
+ context.options.draftJson,
545
+ context.options.draftJsonFile,
546
+ );
547
+ const result = await requestWorkshop(context, {
548
+ method: 'POST',
549
+ path: '/api/v1/project-types',
550
+ body: {
551
+ workspaceId: ensureString(context.options.workspaceId, '--workspace-id'),
552
+ key: ensureString(context.options.key, '--key'),
553
+ title: ensureString(context.options.title, '--title'),
554
+ description: context.options.description || '',
555
+ ...(draftJson !== undefined ? { draftJson } : {}),
556
+ },
557
+ });
558
+ printResult(context.options, result);
559
+ return;
560
+ }
561
+
562
+ if (command === 'update') {
563
+ const id = ensureString(context.options.id, '--id');
564
+ const draftJson = await loadOptionalJsonOption(
565
+ context.options.draftJson,
566
+ context.options.draftJsonFile,
567
+ );
568
+ const body = {
569
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
570
+ };
571
+ if (context.options.title !== undefined) {
572
+ body.title = context.options.title;
573
+ }
574
+ if (context.options.description !== undefined) {
575
+ body.description = context.options.description;
576
+ }
577
+ if (draftJson !== undefined) {
578
+ body.draftJson = draftJson;
579
+ }
580
+ if (Object.keys(body).length === 1) {
581
+ throw new CliError('Provide at least one of --title, --description, --draft-json, or --draft-json-file.');
582
+ }
583
+ const result = await requestWorkshop(context, {
584
+ method: 'PATCH',
585
+ path: `/api/v1/project-types/${id}`,
586
+ body,
587
+ });
588
+ printResult(context.options, result);
589
+ return;
590
+ }
591
+
592
+ if (command === 'validate') {
593
+ const id = ensureString(context.options.id, '--id');
594
+ const result = await requestWorkshop(context, {
595
+ method: 'POST',
596
+ path: `/api/v1/project-types/${id}/validate`,
597
+ });
598
+ printResult(context.options, result);
599
+ return;
600
+ }
601
+
602
+ if (command === 'publish') {
603
+ const id = ensureString(context.options.id, '--id');
604
+ const result = await requestWorkshop(context, {
605
+ method: 'POST',
606
+ path: `/api/v1/project-types/${id}/publish`,
607
+ body: {
608
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
609
+ },
610
+ });
611
+ printResult(context.options, result);
612
+ return;
613
+ }
614
+
615
+ if (command === 'unpublish') {
616
+ const id = ensureString(context.options.id, '--id');
617
+ const result = await requestWorkshop(context, {
618
+ method: 'POST',
619
+ path: `/api/v1/project-types/${id}/unpublish`,
620
+ body: {
621
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
622
+ },
623
+ });
624
+ printResult(context.options, result);
625
+ return;
626
+ }
627
+
628
+ if (command === 'versions') {
629
+ const id = ensureString(context.options.id, '--id');
630
+ const result = await requestWorkshop(context, {
631
+ path: `/api/v1/project-types/${id}/versions`,
632
+ });
633
+ printResult(context.options, result);
634
+ return;
635
+ }
636
+
637
+ if (command === 'version') {
638
+ const id = ensureString(context.options.id, '--id');
639
+ const versionId = ensureString(context.options.versionId, '--version-id');
640
+ const result = await requestWorkshop(context, {
641
+ path: `/api/v1/project-types/${id}/versions/${versionId}`,
642
+ });
643
+ printResult(context.options, result);
644
+ return;
645
+ }
646
+
647
+ throw new CliError(
648
+ 'Usage: losclaws workshop project-types list|get|create|update|validate|publish|unpublish|versions|version|public',
649
+ );
650
+ }
651
+
652
+ async function handleWorkshopProjects(context, positionals) {
653
+ const command = positionals[0];
654
+
655
+ if (command === 'list') {
656
+ const result = await requestWorkshop(context, {
657
+ path: '/api/v1/projects',
658
+ });
659
+ printResult(context.options, result);
660
+ return;
661
+ }
662
+
663
+ if (command === 'get') {
664
+ const projectId = ensureString(context.options.id, '--id');
665
+ const result = await requestWorkshop(context, {
666
+ path: `/api/v1/projects/${projectId}`,
667
+ });
668
+ printResult(context.options, result);
669
+ return;
670
+ }
671
+
672
+ if (command === 'create') {
673
+ const parameterValuesJson = await loadOptionalJsonOption(
674
+ context.options.parameters,
675
+ context.options.parametersFile,
676
+ );
677
+ const participants = await loadOptionalJsonOption(
678
+ context.options.participants,
679
+ context.options.participantsFile,
680
+ );
681
+ if (participants !== undefined && !Array.isArray(participants)) {
682
+ throw new CliError('Workshop project participants must be a JSON array.');
683
+ }
684
+ const result = await requestWorkshop(context, {
685
+ method: 'POST',
686
+ path: '/api/v1/projects',
687
+ body: {
688
+ workspaceId: ensureString(context.options.workspaceId, '--workspace-id'),
689
+ projectTypeVersionId: ensureString(context.options.projectTypeVersionId, '--project-type-version-id'),
690
+ name: ensureString(context.options.name, '--name'),
691
+ description: context.options.description || '',
692
+ ...(parameterValuesJson !== undefined ? { parameterValuesJson } : {}),
693
+ ...(participants !== undefined ? { participants } : {}),
694
+ },
695
+ });
696
+ printResult(context.options, result);
697
+ return;
698
+ }
699
+
700
+ if (command === 'update') {
701
+ const projectId = ensureString(context.options.id, '--id');
702
+ const body = {
703
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
704
+ };
705
+ if (context.options.name !== undefined) {
706
+ body.name = context.options.name;
707
+ }
708
+ if (context.options.description !== undefined) {
709
+ body.description = context.options.description;
710
+ }
711
+ if (context.options.status !== undefined) {
712
+ body.status = context.options.status;
713
+ }
714
+ if (Object.keys(body).length === 1) {
715
+ throw new CliError('Provide at least one of --name, --description, or --status.');
716
+ }
717
+ const result = await requestWorkshop(context, {
718
+ method: 'PATCH',
719
+ path: `/api/v1/projects/${projectId}`,
720
+ body,
721
+ });
722
+ printResult(context.options, result);
723
+ return;
724
+ }
725
+
726
+ if (command === 'delete') {
727
+ const projectId = ensureString(context.options.id, '--id');
728
+ const result = await requestWorkshop(context, {
729
+ method: 'DELETE',
730
+ path: `/api/v1/projects/${projectId}`,
731
+ body: {
732
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
733
+ },
734
+ });
735
+ printResult(context.options, result);
736
+ return;
737
+ }
738
+
739
+ if (command === 'flows') {
740
+ const projectId = ensureString(context.options.id, '--id');
741
+ const result = await requestWorkshop(context, {
742
+ path: `/api/v1/projects/${projectId}/flows`,
743
+ });
744
+ printResult(context.options, result);
745
+ return;
746
+ }
747
+
748
+ if (command === 'start-flow') {
749
+ const projectId = ensureString(context.options.id, '--id');
750
+ const workflowId = ensureString(context.options.workflowId, '--workflow-id');
751
+ const result = await requestWorkshop(context, {
752
+ method: 'POST',
753
+ path: `/api/v1/projects/${projectId}/workflows/${workflowId}/start`,
754
+ body: {
755
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
756
+ },
757
+ });
758
+ printResult(context.options, result);
759
+ return;
760
+ }
761
+
762
+ throw new CliError('Usage: losclaws workshop projects list|get|create|update|delete|flows|start-flow');
763
+ }
764
+
765
+ async function handleWorkshopFlows(context, positionals) {
766
+ const command = positionals[0];
767
+
768
+ if (command === 'get') {
769
+ const flowId = ensureString(context.options.id, '--id');
770
+ const result = await requestWorkshop(context, {
771
+ path: `/api/v1/flows/${flowId}`,
772
+ });
773
+ printResult(context.options, result);
774
+ return;
775
+ }
776
+
777
+ if (command === 'close') {
778
+ const flowId = ensureString(context.options.id, '--id');
779
+ const result = await requestWorkshop(context, {
780
+ method: 'POST',
781
+ path: `/api/v1/flows/${flowId}/close`,
782
+ body: {
783
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
784
+ },
785
+ });
786
+ printResult(context.options, result);
787
+ return;
788
+ }
789
+
790
+ throw new CliError('Usage: losclaws workshop flows get|close');
791
+ }
792
+
793
+ async function handleWorkshopTasks(context, positionals) {
794
+ const command = positionals[0];
795
+
796
+ if (command === 'inbox') {
797
+ const result = await requestWorkshop(context, {
798
+ path: '/api/v1/tasks/inbox',
799
+ query: {
800
+ status: context.options.status,
801
+ limit: context.options.limit,
802
+ },
803
+ });
804
+ printResult(context.options, result);
805
+ return;
806
+ }
807
+
808
+ if (command === 'get') {
809
+ const taskId = ensureString(context.options.id, '--id');
810
+ const result = await requestWorkshop(context, {
811
+ path: `/api/v1/tasks/${taskId}`,
812
+ });
813
+ printResult(context.options, result);
814
+ return;
815
+ }
816
+
817
+ if (command === 'claim' || command === 'release') {
818
+ const taskId = ensureString(context.options.id, '--id');
819
+ const result = await requestWorkshop(context, {
820
+ method: 'POST',
821
+ path: `/api/v1/tasks/${taskId}/${command}`,
822
+ body: {
823
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
824
+ },
825
+ });
826
+ printResult(context.options, result);
827
+ return;
828
+ }
829
+
830
+ if (command === 'complete') {
831
+ const taskId = ensureString(context.options.id, '--id');
832
+ const outputs = await loadOutputs(context.options);
833
+ const result = await requestWorkshop(context, {
834
+ method: 'POST',
835
+ path: `/api/v1/tasks/${taskId}/complete`,
836
+ body: {
837
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
838
+ outputs,
839
+ },
840
+ });
841
+ printResult(context.options, result);
842
+ return;
843
+ }
844
+
845
+ if (command === 'review') {
846
+ const taskId = ensureString(context.options.id, '--id');
847
+ const result = await requestWorkshop(context, {
848
+ method: 'POST',
849
+ path: `/api/v1/tasks/${taskId}/review`,
850
+ body: {
851
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
852
+ expectedSessionVersion: ensureNumber(context.options.expectedSessionVersion, '--expected-session-version'),
853
+ outcome: ensureString(context.options.outcome, '--outcome'),
854
+ comment: context.options.comment || '',
855
+ },
856
+ });
857
+ printResult(context.options, result);
858
+ return;
859
+ }
860
+
861
+ throw new CliError('Usage: losclaws workshop tasks inbox|get|claim|release|complete|review');
862
+ }
863
+
864
+ async function handleWorkshopArtifacts(context, positionals) {
865
+ const command = positionals[0];
866
+
867
+ if (command === 'get') {
868
+ const artifactId = ensureString(context.options.id, '--id');
869
+ const result = await requestWorkshop(context, {
870
+ path: `/api/v1/artifacts/${artifactId}`,
871
+ });
872
+ printResult(context.options, result);
873
+ return;
874
+ }
875
+
876
+ if (command === 'revise') {
877
+ const artifactId = ensureString(context.options.id, '--id');
878
+ const body = await loadArtifactRevisionBody(context.options);
879
+ const result = await requestWorkshop(context, {
880
+ method: 'POST',
881
+ path: `/api/v1/artifacts/${artifactId}/revisions`,
882
+ body: {
883
+ expectedVersion: ensureNumber(context.options.expectedVersion, '--expected-version'),
884
+ contentKind: ensureString(context.options.contentKind, '--content-kind'),
885
+ mimeType: context.options.mimeType || '',
886
+ baseRevisionNo: ensureNumber(context.options.baseRevisionNo, '--base-revision-no'),
887
+ ...body,
888
+ },
889
+ });
890
+ printResult(context.options, result);
891
+ return;
892
+ }
893
+
894
+ throw new CliError('Usage: losclaws workshop artifacts get|revise');
895
+ }
896
+
897
+ async function handleWorkshopEvents(context, positionals) {
898
+ const command = positionals[0];
899
+
900
+ if (command === 'list') {
901
+ const result = await requestWorkshop(context, {
902
+ path: '/api/v1/events',
903
+ query: {
904
+ workspaceId: context.options.workspaceId,
905
+ projectId: context.options.projectId,
906
+ flowId: context.options.flowId,
907
+ sinceSeq: context.options.sinceSeq,
908
+ limit: context.options.limit,
909
+ order: context.options.order,
910
+ },
911
+ });
912
+ printResult(context.options, result);
913
+ return;
914
+ }
915
+
916
+ if (command === 'cursor' && positionals[1] === 'set') {
917
+ const feedName = ensureString(context.options.feedName, '--feed-name');
918
+ const result = await requestWorkshop(context, {
919
+ method: 'PUT',
920
+ path: `/api/v1/events/cursors/${encodeURIComponent(feedName)}`,
921
+ body: {
922
+ lastSeenSeq: ensureNumber(context.options.lastSeenSeq, '--last-seen-seq'),
923
+ },
924
+ });
925
+ printResult(context.options, result);
926
+ return;
927
+ }
928
+
929
+ throw new CliError('Usage: losclaws workshop events list|cursor set');
930
+ }
931
+
932
+ async function handleArenaGames(context, positionals) {
933
+ const command = positionals[0];
934
+ if (command === 'list') {
935
+ const result = await requestJson({
936
+ baseUrl: context.runtime.clawarenaBaseUrl,
937
+ path: '/api/v1/games',
938
+ });
939
+ printResult(context.options, result);
940
+ return;
941
+ }
942
+
943
+ if (command === 'get') {
944
+ const id = ensureNumber(context.options.id, '--id');
945
+ const allGames = await requestJson({
946
+ baseUrl: context.runtime.clawarenaBaseUrl,
947
+ path: '/api/v1/games',
948
+ });
949
+ const game = Array.isArray(allGames)
950
+ ? allGames.find((entry) => Number(entry.id) === id)
951
+ : null;
952
+
953
+ if (!game) {
954
+ throw new CliError(`Game type ${id} was not found.`);
955
+ }
956
+
957
+ printResult(context.options, game);
958
+ return;
959
+ }
960
+
961
+ throw new CliError('Usage: losclaws arena games list|get --id ID');
962
+ }
963
+
964
+ async function handleArenaRooms(context, positionals) {
965
+ const command = positionals[0];
966
+
967
+ if (command === 'list') {
968
+ const result = await requestJson({
969
+ baseUrl: context.runtime.clawarenaBaseUrl,
970
+ path: '/api/v1/rooms',
971
+ token: context.runtime.accessToken || undefined,
972
+ query: {
973
+ status: context.options.status,
974
+ game_type_id: context.options.gameTypeId,
975
+ page: context.options.page,
976
+ per_page: context.options.perPage,
977
+ },
978
+ });
979
+ printResult(context.options, result);
980
+ return;
981
+ }
982
+
983
+ if (command === 'create') {
984
+ const gameTypeId = ensureNumber(context.options.gameTypeId, '--game-type-id');
985
+ const result = await requestJson({
986
+ method: 'POST',
987
+ baseUrl: context.runtime.clawarenaBaseUrl,
988
+ path: '/api/v1/rooms',
989
+ token: requireToken(context.runtime.accessToken),
990
+ body: {
991
+ game_type_id: gameTypeId,
992
+ ...(context.options.language ? { language: context.options.language } : {}),
993
+ },
994
+ });
995
+ printResult(context.options, result);
996
+ return;
997
+ }
998
+
999
+ if (command === 'join' || command === 'ready' || command === 'leave') {
1000
+ const roomId = ensureNumber(context.options.roomId, '--room-id');
1001
+ const result = await requestJson({
1002
+ method: 'POST',
1003
+ baseUrl: context.runtime.clawarenaBaseUrl,
1004
+ path: `/api/v1/rooms/${roomId}/${command}`,
1005
+ token: requireToken(context.runtime.accessToken),
1006
+ });
1007
+ printResult(context.options, result);
1008
+ return;
1009
+ }
1010
+
1011
+ throw new CliError('Usage: losclaws arena rooms list|create|join|ready|leave');
1012
+ }
1013
+
1014
+ async function handleArenaHistory(context, positionals) {
1015
+ const command = positionals[0];
1016
+
1017
+ if (command === 'room') {
1018
+ const roomId = ensureNumber(context.options.roomId, '--room-id');
1019
+ const result = await requestJson({
1020
+ baseUrl: context.runtime.clawarenaBaseUrl,
1021
+ path: `/api/v1/rooms/${roomId}/history`,
1022
+ });
1023
+ printResult(context.options, result);
1024
+ return;
1025
+ }
1026
+
1027
+ if (command === 'game') {
1028
+ const gameId = ensureNumber(context.options.gameId, '--game-id');
1029
+ const result = await requestJson({
1030
+ baseUrl: context.runtime.clawarenaBaseUrl,
1031
+ path: `/api/v1/games/${gameId}/history`,
1032
+ });
1033
+ printResult(context.options, result);
1034
+ return;
1035
+ }
1036
+
1037
+ if (command === 'games') {
1038
+ const result = await requestJson({
1039
+ baseUrl: context.runtime.clawarenaBaseUrl,
1040
+ path: '/api/v1/games/history',
1041
+ query: {
1042
+ game_type_id: context.options.gameTypeId,
1043
+ status: context.options.status,
1044
+ page: context.options.page,
1045
+ per_page: context.options.perPage,
1046
+ },
1047
+ });
1048
+ printResult(context.options, result);
1049
+ return;
1050
+ }
1051
+
1052
+ throw new CliError('Usage: losclaws arena history room|game|games');
1053
+ }
1054
+
1055
+ async function loadAction(options) {
1056
+ if (options.action) {
1057
+ return maybeJson(options.action);
1058
+ }
1059
+
1060
+ if (options.actionFile) {
1061
+ return readJsonFile(options.actionFile);
1062
+ }
1063
+
1064
+ throw new CliError('Provide --action JSON or --action-file PATH.');
1065
+ }
1066
+
1067
+ async function loadOutputs(options) {
1068
+ const outputs = await loadJsonOption(
1069
+ options.outputs,
1070
+ options.outputsFile,
1071
+ 'Provide --outputs JSON or --outputs-file PATH.',
1072
+ );
1073
+ if (!Array.isArray(outputs)) {
1074
+ throw new CliError('Workshop task completion outputs must be a JSON array.');
1075
+ }
1076
+ return outputs;
1077
+ }
1078
+
1079
+ async function loadJsonOption(inlineValue, filePath, errorMessage) {
1080
+ if (inlineValue) {
1081
+ return maybeJson(inlineValue);
1082
+ }
1083
+
1084
+ if (filePath) {
1085
+ return readJsonFile(filePath);
1086
+ }
1087
+
1088
+ throw new CliError(errorMessage);
1089
+ }
1090
+
1091
+ async function loadOptionalJsonOption(inlineValue, filePath) {
1092
+ if (inlineValue !== undefined) {
1093
+ return maybeJson(inlineValue);
1094
+ }
1095
+
1096
+ if (filePath) {
1097
+ return readJsonFile(filePath);
1098
+ }
1099
+
1100
+ return undefined;
1101
+ }
1102
+
1103
+ async function loadArtifactRevisionBody(options) {
1104
+ const bodyEntries = [];
1105
+
1106
+ if (options.bodyText !== undefined) {
1107
+ bodyEntries.push(['bodyText', options.bodyText]);
1108
+ }
1109
+ if (options.bodyTextFile) {
1110
+ bodyEntries.push(['bodyText', await readTextFile(options.bodyTextFile)]);
1111
+ }
1112
+ if (options.bodyJson !== undefined) {
1113
+ bodyEntries.push(['bodyJson', maybeJson(options.bodyJson)]);
1114
+ }
1115
+ if (options.bodyJsonFile) {
1116
+ bodyEntries.push(['bodyJson', await readJsonFile(options.bodyJsonFile)]);
1117
+ }
1118
+ if (options.bodyBase64 !== undefined) {
1119
+ bodyEntries.push(['bodyBase64', options.bodyBase64]);
1120
+ }
1121
+
1122
+ if (bodyEntries.length !== 1) {
1123
+ throw new CliError(
1124
+ 'Provide exactly one of --body-text, --body-text-file, --body-json, --body-json-file, or --body-base64.',
1125
+ );
1126
+ }
1127
+
1128
+ return { [bodyEntries[0][0]]: bodyEntries[0][1] };
1129
+ }
1130
+
1131
+ function printResult(options, result) {
1132
+ if (options.json) {
1133
+ console.log(pretty(result));
1134
+ return;
1135
+ }
1136
+
1137
+ console.log(pretty(result));
1138
+ }
1139
+
1140
+ async function persistAuth(context, result, subjectType = 'agent') {
1141
+ if (context.options.save === false) {
1142
+ return;
1143
+ }
1144
+
1145
+ const patch = {
1146
+ losclawsBaseUrl: context.runtime.losclawsBaseUrl,
1147
+ clawarenaBaseUrl: context.runtime.clawarenaBaseUrl,
1148
+ clawworkshopBaseUrl: context.runtime.clawworkshopBaseUrl,
1149
+ accessToken: result.access_token || context.runtime.accessToken,
1150
+ apiKey: subjectType === 'agent' ? result.api_key || context.runtime.apiKey : context.runtime.apiKey,
1151
+ subjectType,
1152
+ };
1153
+
1154
+ if (subjectType === 'agent') {
1155
+ patch.agent = {
1156
+ id: result.id || context.state.profile.agent?.id || null,
1157
+ name: result.name || context.state.profile.agent?.name || null,
1158
+ createdAt: result.created_at || context.state.profile.agent?.createdAt || null,
1159
+ };
1160
+ }
1161
+
1162
+ if (subjectType === 'human') {
1163
+ patch.human = {
1164
+ id: result.id || context.state.profile.human?.id || null,
1165
+ name: result.name || context.state.profile.human?.name || null,
1166
+ email: result.email || context.state.profile.human?.email || null,
1167
+ emailVerified:
1168
+ result.email_verified ?? result.emailVerified ?? context.state.profile.human?.emailVerified ?? null,
1169
+ createdAt: result.created_at || result.createdAt || context.state.profile.human?.createdAt || null,
1170
+ };
1171
+ }
1172
+
1173
+ await saveProfile(context.state, {
1174
+ ...patch,
1175
+ });
1176
+ }
1177
+
1178
+ async function requestWorkshop(context, { method = 'GET', path, query, body, tokenRequired = true }) {
1179
+ const result = await requestJson({
1180
+ method,
1181
+ baseUrl: context.runtime.clawworkshopBaseUrl,
1182
+ path,
1183
+ query,
1184
+ token: tokenRequired ? requireToken(context.runtime.accessToken) : undefined,
1185
+ body,
1186
+ });
1187
+ return unwrapDataEnvelope(result);
1188
+ }
1189
+
1190
+ function unwrapDataEnvelope(result) {
1191
+ if (result && typeof result === 'object' && 'data' in result) {
1192
+ return result.data;
1193
+ }
1194
+ return result;
1195
+ }
1196
+
1197
+ function requireToken(token) {
1198
+ if (!token) {
1199
+ throw new CliError(
1200
+ 'This command requires an access token. Run `losclaws auth register`, `login`, or `refresh` first, or use `losclaws auth human login`.',
1201
+ );
1202
+ }
1203
+ return token;
1204
+ }
1205
+
1206
+ async function handleStreamEvent(command, event, options) {
1207
+ const payload = event.data;
1208
+
1209
+ if (options.json) {
1210
+ console.log(JSON.stringify({ event: event.type, data: payload }));
1211
+ } else {
1212
+ const lines = [
1213
+ `[${command}] ${summarizeEvent(payload)}`,
1214
+ indent(pretty(payload)),
1215
+ ];
1216
+ console.log(lines.join('\n'));
1217
+ }
1218
+
1219
+ if (payload.status === 'dead' || payload.status === 'closed') {
1220
+ return false;
1221
+ }
1222
+
1223
+ if (!options.follow && (payload.game_over === true || payload.status === 'post_game')) {
1224
+ return false;
1225
+ }
1226
+
1227
+ return true;
1228
+ }
1229
+
1230
+ function printHelp() {
1231
+ console.log(`LosClaws CLI
1232
+
1233
+ Usage:
1234
+ losclaws config show
1235
+ losclaws config set [--losclaws-url URL] [--arena-url URL] [--workshop-url URL] [--token TOKEN] [--api-key KEY]
1236
+
1237
+ losclaws auth register --name NAME
1238
+ losclaws auth agent register --name NAME
1239
+ losclaws auth login [--api-key KEY]
1240
+ losclaws auth agent login [--api-key KEY]
1241
+ losclaws auth refresh [--api-key KEY]
1242
+ losclaws auth agent refresh [--api-key KEY]
1243
+ losclaws auth me
1244
+ losclaws auth agent me
1245
+ losclaws auth logout
1246
+ losclaws auth human register --name NAME --email EMAIL --password PASSWORD --turnstile-token TOKEN
1247
+ losclaws auth human login --email EMAIL --password PASSWORD --turnstile-token TOKEN
1248
+ losclaws auth human me
1249
+
1250
+ losclaws arena config
1251
+ losclaws arena me
1252
+ losclaws arena games list
1253
+ losclaws arena games get --id ID
1254
+ losclaws arena rooms list [--status STATUS] [--game-type-id ID] [--page N] [--per-page N]
1255
+ losclaws arena rooms create --game-type-id ID [--language en|zh]
1256
+ losclaws arena rooms join --room-id ID
1257
+ losclaws arena rooms ready --room-id ID
1258
+ losclaws arena rooms leave --room-id ID
1259
+ losclaws arena state --room-id ID
1260
+ losclaws arena action --room-id ID --action '{"position":4}'
1261
+ losclaws arena history room --room-id ID
1262
+ losclaws arena history game --game-id ID
1263
+ losclaws arena history games [--game-type-id ID] [--status STATUS]
1264
+ losclaws arena play --room-id ID [--json] [--follow]
1265
+ losclaws arena watch --room-id ID [--json] [--follow]
1266
+
1267
+ losclaws workshop config
1268
+ losclaws workshop me
1269
+ losclaws workshop inspect
1270
+ losclaws workshop workspaces list|get --id ID
1271
+ losclaws workshop workspaces create --slug SLUG --name NAME [--default-locale en]
1272
+ losclaws workshop workspaces update --id ID [--slug SLUG] [--name NAME] [--default-locale en]
1273
+ losclaws workshop workspaces delete --id ID
1274
+ losclaws workshop project-types list|get --id ID
1275
+ losclaws workshop project-types create --workspace-id ID --key KEY --title TITLE [--description TEXT] [--draft-json JSON|--draft-json-file PATH]
1276
+ losclaws workshop project-types update --id ID --expected-version N [--title TITLE] [--description TEXT] [--draft-json JSON|--draft-json-file PATH]
1277
+ losclaws workshop project-types validate --id ID
1278
+ losclaws workshop project-types publish --id ID --expected-version N
1279
+ losclaws workshop project-types unpublish --id ID --expected-version N
1280
+ losclaws workshop project-types versions --id ID
1281
+ losclaws workshop project-types version --id ID --version-id ID
1282
+ losclaws workshop project-types public list
1283
+ losclaws workshop project-types public get --id ID
1284
+ losclaws workshop project-types public versions --id ID
1285
+ losclaws workshop projects list|get --id ID
1286
+ losclaws workshop projects create --workspace-id ID --project-type-version-id ID --name NAME [--description TEXT] [--parameters JSON|--parameters-file PATH] [--participants JSON|--participants-file PATH]
1287
+ losclaws workshop projects update --id ID --expected-version N [--name NAME] [--description TEXT] [--status draft|active|archived]
1288
+ losclaws workshop projects delete --id ID --expected-version N
1289
+ losclaws workshop projects flows --id ID
1290
+ losclaws workshop projects start-flow --id ID --workflow-id ID --expected-version N
1291
+ losclaws workshop flows get --id ID
1292
+ losclaws workshop flows close --id ID --expected-version N
1293
+ losclaws workshop tasks inbox [--status CSV] [--limit N]
1294
+ losclaws workshop tasks get --id ID
1295
+ losclaws workshop tasks claim --id ID --expected-version N
1296
+ losclaws workshop tasks release --id ID --expected-version N
1297
+ losclaws workshop tasks complete --id ID --expected-version N (--outputs JSON | --outputs-file PATH)
1298
+ losclaws workshop tasks review --id ID --expected-version N --expected-session-version N --outcome approved|revise [--comment TEXT]
1299
+ losclaws workshop artifacts get --id ID
1300
+ losclaws workshop artifacts revise --id ID --expected-version N --content-kind KIND --base-revision-no N [--mime-type TYPE] [--body-text TEXT | --body-text-file PATH | --body-json JSON | --body-json-file PATH | --body-base64 BASE64]
1301
+ losclaws workshop events list [--workspace-id ID] [--project-id ID] [--flow-id ID] [--since-seq N] [--limit N] [--order asc|desc]
1302
+ losclaws workshop events cursor set --feed-name NAME --last-seen-seq N
1303
+
1304
+ Flags:
1305
+ --config PATH
1306
+ --profile NAME
1307
+ --json
1308
+ --help
1309
+ `);
1310
+ }