ticlawk 0.1.16-dev.1 → 0.1.16-dev.3

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.
@@ -20,6 +20,8 @@
20
20
  * work cleanly (matching the Slock convention).
21
21
  */
22
22
 
23
+ import { readFileSync, statSync, writeFileSync } from 'node:fs';
24
+ import { basename, extname } from 'node:path';
23
25
  import { request as httpRequest } from 'node:http';
24
26
  import { request as httpsRequest } from 'node:https';
25
27
 
@@ -177,11 +179,50 @@ export async function runMessageReadCommand(args) {
177
179
  return exitFromStatus(res.statusCode);
178
180
  }
179
181
 
182
+ export async function runTaskCreateCommand(args) {
183
+ const env = requireAgentEnv();
184
+ const target = getArg(args, 'target');
185
+ const conversationId = getArg(args, 'conversation-id');
186
+ if (!target && !conversationId) {
187
+ console.error('--target or --conversation-id is required');
188
+ return 2;
189
+ }
190
+ const text = await readStdin();
191
+ if (!text || !text.trim()) {
192
+ console.error('task body is required on stdin');
193
+ console.error('Example:');
194
+ console.error(" ticlawk task create --target \"#frontend\" --title \"fix login\" <<'EOF'");
195
+ console.error(' Detailed description goes here.');
196
+ console.error(' EOF');
197
+ return 2;
198
+ }
199
+ const res = await daemonRequest({
200
+ method: 'POST',
201
+ path: '/agent/task/create',
202
+ headers: commonHeaders(env),
203
+ body: {
204
+ target,
205
+ conversation_id: conversationId,
206
+ text: text.replace(/\n+$/, ''),
207
+ title: getArg(args, 'title'),
208
+ },
209
+ });
210
+ printJson(res.body);
211
+ return exitFromStatus(res.statusCode);
212
+ }
213
+
180
214
  export async function runTaskClaimCommand(args) {
181
215
  const env = requireAgentEnv();
182
216
  const messageId = getArg(args, 'message-id');
183
- if (!messageId) {
184
- console.error('--message-id is required');
217
+ const number = getNumberArg(args, 'number');
218
+ const target = getArg(args, 'target');
219
+ const conversationId = getArg(args, 'conversation-id');
220
+ if (!messageId && number == null) {
221
+ console.error('--message-id or --number is required');
222
+ return 2;
223
+ }
224
+ if (number != null && !target && !conversationId) {
225
+ console.error('--target or --conversation-id is required when claiming by --number');
185
226
  return 2;
186
227
  }
187
228
  const res = await daemonRequest({
@@ -190,6 +231,9 @@ export async function runTaskClaimCommand(args) {
190
231
  headers: commonHeaders(env),
191
232
  body: {
192
233
  source_message_id: messageId,
234
+ number,
235
+ target,
236
+ conversation_id: conversationId,
193
237
  lease_seconds: getNumberArg(args, 'lease-seconds'),
194
238
  },
195
239
  });
@@ -201,6 +245,23 @@ export async function runTaskClaimCommand(args) {
201
245
  return exitFromStatus(res.statusCode);
202
246
  }
203
247
 
248
+ export async function runTaskUnclaimCommand(args) {
249
+ const env = requireAgentEnv();
250
+ const taskId = getArg(args, 'task-id');
251
+ if (!taskId) {
252
+ console.error('--task-id is required');
253
+ return 2;
254
+ }
255
+ const res = await daemonRequest({
256
+ method: 'POST',
257
+ path: '/agent/task/unclaim',
258
+ headers: commonHeaders(env),
259
+ body: { task_id: taskId },
260
+ });
261
+ printJson(res.body);
262
+ return exitFromStatus(res.statusCode);
263
+ }
264
+
204
265
  export async function runTaskUpdateCommand(args) {
205
266
  const env = requireAgentEnv();
206
267
  const taskId = getArg(args, 'task-id');
@@ -209,6 +270,10 @@ export async function runTaskUpdateCommand(args) {
209
270
  console.error('--task-id and --status are required');
210
271
  return 2;
211
272
  }
273
+ if (!['todo', 'in_progress', 'in_review', 'done', 'canceled'].includes(status)) {
274
+ console.error(`--status must be one of: todo, in_progress, in_review, done, canceled`);
275
+ return 2;
276
+ }
212
277
  const res = await daemonRequest({
213
278
  method: 'POST',
214
279
  path: '/agent/task/update',
@@ -219,6 +284,37 @@ export async function runTaskUpdateCommand(args) {
219
284
  return exitFromStatus(res.statusCode);
220
285
  }
221
286
 
287
+ export async function runMessageReactCommand(args) {
288
+ const env = requireAgentEnv();
289
+ const messageId = getArg(args, 'message-id');
290
+ const emoji = getArg(args, 'emoji');
291
+ const remove = !!args.remove;
292
+ if (!messageId) {
293
+ console.error('--message-id is required');
294
+ return 2;
295
+ }
296
+ if (!emoji) {
297
+ console.error('--emoji is required');
298
+ return 2;
299
+ }
300
+ if (emoji.length > 16 || /\s/.test(emoji)) {
301
+ console.error('--emoji must be a single emoji, ≤16 chars, no whitespace');
302
+ return 2;
303
+ }
304
+ const res = await daemonRequest({
305
+ method: 'POST',
306
+ path: '/agent/message/react',
307
+ headers: commonHeaders(env),
308
+ body: { message_id: messageId, emoji, remove },
309
+ });
310
+ printJson(res.body);
311
+ if (res.statusCode >= 200 && res.statusCode < 300) {
312
+ const verb = remove ? 'removed from' : 'added to';
313
+ console.error(`Reaction ${emoji} ${verb} message ${messageId.slice(0, 8)}.`);
314
+ }
315
+ return exitFromStatus(res.statusCode);
316
+ }
317
+
222
318
  export async function runTaskListCommand(args) {
223
319
  const env = requireAgentEnv();
224
320
  const params = new URLSearchParams();
@@ -235,6 +331,410 @@ export async function runTaskListCommand(args) {
235
331
  return exitFromStatus(res.statusCode);
236
332
  }
237
333
 
334
+ // ── Reminders ──
335
+
336
+ function resolveFireAt(args) {
337
+ const explicit = getArg(args, 'fire-at');
338
+ if (explicit) return explicit;
339
+ const inSeconds = getNumberArg(args, 'in-seconds');
340
+ if (inSeconds != null) {
341
+ return new Date(Date.now() + inSeconds * 1000).toISOString();
342
+ }
343
+ const inMinutes = getNumberArg(args, 'in-minutes');
344
+ if (inMinutes != null) {
345
+ return new Date(Date.now() + inMinutes * 60 * 1000).toISOString();
346
+ }
347
+ return null;
348
+ }
349
+
350
+ export async function runReminderScheduleCommand(args) {
351
+ const env = requireAgentEnv();
352
+ const title = getArg(args, 'title');
353
+ if (!title) {
354
+ console.error('--title is required');
355
+ return 2;
356
+ }
357
+ const fireAt = resolveFireAt(args);
358
+ if (!fireAt) {
359
+ console.error('one of --fire-at <ISO>, --in-seconds N, --in-minutes N is required');
360
+ return 2;
361
+ }
362
+ const target = getArg(args, 'target');
363
+ const anchorConversationId = getArg(args, 'anchor-conversation-id');
364
+ const anchorMessageId = getArg(args, 'anchor-message-id');
365
+ if (!target && !anchorConversationId) {
366
+ console.error('--target or --anchor-conversation-id is required');
367
+ return 2;
368
+ }
369
+ const res = await daemonRequest({
370
+ method: 'POST',
371
+ path: '/agent/reminder/schedule',
372
+ headers: commonHeaders(env),
373
+ body: {
374
+ title,
375
+ fire_at: fireAt,
376
+ target,
377
+ anchor_conversation_id: anchorConversationId,
378
+ anchor_message_id: anchorMessageId,
379
+ },
380
+ });
381
+ printJson(res.body);
382
+ return exitFromStatus(res.statusCode);
383
+ }
384
+
385
+ export async function runReminderListCommand(args) {
386
+ const env = requireAgentEnv();
387
+ const params = new URLSearchParams();
388
+ const status = getArg(args, 'status');
389
+ if (status) params.set('status', status);
390
+ const res = await daemonRequest({
391
+ method: 'GET',
392
+ path: `/agent/reminder/list?${params}`,
393
+ headers: commonHeaders(env),
394
+ });
395
+ printJson(res.body);
396
+ return exitFromStatus(res.statusCode);
397
+ }
398
+
399
+ export async function runReminderSnoozeCommand(args) {
400
+ const env = requireAgentEnv();
401
+ const reminderId = getArg(args, 'reminder-id') || args._?.[2];
402
+ const fireAt = resolveFireAt(args);
403
+ if (!reminderId) {
404
+ console.error('--reminder-id (or positional) is required');
405
+ return 2;
406
+ }
407
+ if (!fireAt) {
408
+ console.error('one of --fire-at <ISO>, --in-seconds N, --in-minutes N is required');
409
+ return 2;
410
+ }
411
+ const res = await daemonRequest({
412
+ method: 'POST',
413
+ path: '/agent/reminder/snooze',
414
+ headers: commonHeaders(env),
415
+ body: { reminder_id: reminderId, fire_at: fireAt },
416
+ });
417
+ printJson(res.body);
418
+ return exitFromStatus(res.statusCode);
419
+ }
420
+
421
+ export async function runReminderUpdateCommand(args) {
422
+ const env = requireAgentEnv();
423
+ const reminderId = getArg(args, 'reminder-id') || args._?.[2];
424
+ const title = getArg(args, 'title');
425
+ const fireAt = resolveFireAt(args);
426
+ if (!reminderId) {
427
+ console.error('--reminder-id (or positional) is required');
428
+ return 2;
429
+ }
430
+ if (!title && !fireAt) {
431
+ console.error('at least one of --title or --fire-at / --in-seconds / --in-minutes is required');
432
+ return 2;
433
+ }
434
+ const res = await daemonRequest({
435
+ method: 'POST',
436
+ path: '/agent/reminder/update',
437
+ headers: commonHeaders(env),
438
+ body: { reminder_id: reminderId, title, fire_at: fireAt },
439
+ });
440
+ printJson(res.body);
441
+ return exitFromStatus(res.statusCode);
442
+ }
443
+
444
+ export async function runReminderCancelCommand(args) {
445
+ const env = requireAgentEnv();
446
+ const reminderId = getArg(args, 'reminder-id') || args._?.[2];
447
+ if (!reminderId) {
448
+ console.error('--reminder-id (or positional) is required');
449
+ return 2;
450
+ }
451
+ const res = await daemonRequest({
452
+ method: 'POST',
453
+ path: '/agent/reminder/cancel',
454
+ headers: commonHeaders(env),
455
+ body: { reminder_id: reminderId },
456
+ });
457
+ printJson(res.body);
458
+ return exitFromStatus(res.statusCode);
459
+ }
460
+
461
+ export async function runReminderLogCommand(args) {
462
+ const env = requireAgentEnv();
463
+ const reminderId = getArg(args, 'reminder-id') || args._?.[2];
464
+ if (!reminderId) {
465
+ console.error('--reminder-id (or positional) is required');
466
+ return 2;
467
+ }
468
+ const params = new URLSearchParams();
469
+ params.set('reminder_id', reminderId);
470
+ const res = await daemonRequest({
471
+ method: 'GET',
472
+ path: `/agent/reminder/log?${params}`,
473
+ headers: commonHeaders(env),
474
+ });
475
+ printJson(res.body);
476
+ return exitFromStatus(res.statusCode);
477
+ }
478
+
479
+ export async function runMessageCheckCommand(args) {
480
+ const env = requireAgentEnv();
481
+ const params = new URLSearchParams();
482
+ const target = getArg(args, 'target');
483
+ const conversationId = getArg(args, 'conversation-id');
484
+ if (target) params.set('target', target);
485
+ if (conversationId) params.set('conversation_id', conversationId);
486
+ const res = await daemonRequest({
487
+ method: 'GET',
488
+ path: `/agent/message/check?${params}`,
489
+ headers: commonHeaders(env),
490
+ });
491
+ printJson(res.body);
492
+ return exitFromStatus(res.statusCode);
493
+ }
494
+
495
+ export async function runMessageSearchCommand(args) {
496
+ const env = requireAgentEnv();
497
+ const query = getArg(args, 'query') || getArg(args, 'q');
498
+ if (!query) {
499
+ console.error('--query (or --q) is required');
500
+ return 2;
501
+ }
502
+ const params = new URLSearchParams();
503
+ params.set('q', query);
504
+ const target = getArg(args, 'target');
505
+ const conversationId = getArg(args, 'conversation-id');
506
+ const limit = getArg(args, 'limit');
507
+ if (target) params.set('target', target);
508
+ if (conversationId) params.set('conversation_id', conversationId);
509
+ if (limit) params.set('limit', limit);
510
+ const res = await daemonRequest({
511
+ method: 'GET',
512
+ path: `/agent/message/search?${params}`,
513
+ headers: commonHeaders(env),
514
+ });
515
+ printJson(res.body);
516
+ return exitFromStatus(res.statusCode);
517
+ }
518
+
519
+ export async function runProfileShowCommand(args) {
520
+ const env = requireAgentEnv();
521
+ const positional = args._?.[2]; // ticlawk profile show @handle
522
+ const explicit = getArg(args, 'id') || getArg(args, 'handle');
523
+ let target = explicit || positional || env.agentId;
524
+ if (typeof target === 'string' && target.startsWith('@')) target = target.slice(1);
525
+ const params = new URLSearchParams();
526
+ if (target) params.set('id', target);
527
+ const res = await daemonRequest({
528
+ method: 'GET',
529
+ path: `/agent/profile/show?${params}`,
530
+ headers: commonHeaders(env),
531
+ });
532
+ printJson(res.body);
533
+ return exitFromStatus(res.statusCode);
534
+ }
535
+
536
+ export async function runProfileUpdateCommand(args) {
537
+ const env = requireAgentEnv();
538
+ const displayName = getArg(args, 'display-name');
539
+ const description = getArg(args, 'description');
540
+ const avatarFile = getArg(args, 'avatar-file');
541
+ if (displayName == null && description == null && avatarFile == null) {
542
+ console.error('at least one of --display-name, --description, --avatar-file is required');
543
+ return 2;
544
+ }
545
+ let avatarUrl = null;
546
+ if (avatarFile) {
547
+ // Re-use the attachment upload path then set the public_url as the avatar.
548
+ const upload = await uploadFileViaDaemon(env, avatarFile);
549
+ if (!upload.ok) {
550
+ console.error(`avatar upload failed: ${upload.error}`);
551
+ return 1;
552
+ }
553
+ avatarUrl = upload.publicUrl;
554
+ }
555
+ const res = await daemonRequest({
556
+ method: 'POST',
557
+ path: '/agent/profile/update',
558
+ headers: commonHeaders(env),
559
+ body: {
560
+ display_name: displayName,
561
+ description,
562
+ avatar_url: avatarUrl,
563
+ },
564
+ });
565
+ printJson(res.body);
566
+ return exitFromStatus(res.statusCode);
567
+ }
568
+
569
+ async function uploadFileViaDaemon(env, filePath) {
570
+ let stat;
571
+ try { stat = statSync(filePath); } catch (err) {
572
+ return { ok: false, error: `cannot stat ${filePath}: ${err.message}` };
573
+ }
574
+ if (!stat.isFile()) return { ok: false, error: `${filePath} is not a regular file` };
575
+ const data = readFileSync(filePath);
576
+ const contentType = inferContentType(filePath);
577
+ const res = await daemonRequest({
578
+ method: 'POST',
579
+ path: '/agent/attachment/upload',
580
+ headers: commonHeaders(env),
581
+ body: {
582
+ filename: basename(filePath),
583
+ content_type: contentType,
584
+ data_base64: data.toString('base64'),
585
+ },
586
+ });
587
+ if (res.statusCode < 200 || res.statusCode >= 300) {
588
+ return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
589
+ }
590
+ return { ok: true, assetId: res.body?.asset?.asset_id, publicUrl: res.body?.public_url };
591
+ }
592
+
593
+ function inferContentType(filePath) {
594
+ const ext = extname(filePath).toLowerCase();
595
+ switch (ext) {
596
+ case '.png': return 'image/png';
597
+ case '.jpg': case '.jpeg': return 'image/jpeg';
598
+ case '.gif': return 'image/gif';
599
+ case '.webp': return 'image/webp';
600
+ case '.pdf': return 'application/pdf';
601
+ case '.txt': return 'text/plain';
602
+ case '.md': return 'text/markdown';
603
+ case '.json': return 'application/json';
604
+ default: return 'application/octet-stream';
605
+ }
606
+ }
607
+
608
+ export async function runAttachmentUploadCommand(args) {
609
+ const env = requireAgentEnv();
610
+ const file = args._?.[2];
611
+ if (!file) {
612
+ console.error('usage: ticlawk attachment upload <file>');
613
+ return 2;
614
+ }
615
+ const upload = await uploadFileViaDaemon(env, file);
616
+ printJson(upload);
617
+ return upload.ok ? 0 : 1;
618
+ }
619
+
620
+ export async function runAttachmentViewCommand(args) {
621
+ const env = requireAgentEnv();
622
+ const assetId = args._?.[2];
623
+ if (!assetId) {
624
+ console.error('usage: ticlawk attachment view <asset_id> [--out <path>]');
625
+ return 2;
626
+ }
627
+ const params = new URLSearchParams();
628
+ params.set('asset_id', assetId);
629
+ const res = await daemonRequest({
630
+ method: 'GET',
631
+ path: `/agent/attachment/view?${params}`,
632
+ headers: commonHeaders(env),
633
+ });
634
+ printJson(res.body);
635
+ const out = getArg(args, 'out');
636
+ if (out && res.body?.public_url) {
637
+ // Download via fetch and write to disk.
638
+ const fetched = await fetch(res.body.public_url);
639
+ if (fetched.ok) {
640
+ const buf = Buffer.from(await fetched.arrayBuffer());
641
+ writeFileSync(out, buf);
642
+ console.error(`wrote ${buf.length} bytes to ${out}`);
643
+ } else {
644
+ console.error(`download failed: HTTP ${fetched.status}`);
645
+ return 1;
646
+ }
647
+ }
648
+ return exitFromStatus(res.statusCode);
649
+ }
650
+
651
+ export async function runGroupCreateCommand(args) {
652
+ const env = requireAgentEnv();
653
+ const name = getArg(args, 'name');
654
+ if (!name) {
655
+ console.error('--name is required');
656
+ return 2;
657
+ }
658
+ const description = getArg(args, 'description');
659
+ const memberArgs = args.member;
660
+ const memberAgentIds = Array.isArray(memberArgs)
661
+ ? memberArgs
662
+ : memberArgs
663
+ ? [memberArgs]
664
+ : [];
665
+ const res = await daemonRequest({
666
+ method: 'POST',
667
+ path: '/agent/group/create',
668
+ headers: commonHeaders(env),
669
+ body: {
670
+ name,
671
+ description,
672
+ member_agent_ids: memberAgentIds.map((s) => String(s).trim()).filter(Boolean),
673
+ },
674
+ });
675
+ printJson(res.body);
676
+ return exitFromStatus(res.statusCode);
677
+ }
678
+
679
+ export async function runGroupMembersAddCommand(args) {
680
+ const env = requireAgentEnv();
681
+ const target = getArg(args, 'target');
682
+ const conversationId = getArg(args, 'conversation-id');
683
+ if (!target && !conversationId) {
684
+ console.error('--target or --conversation-id is required');
685
+ return 2;
686
+ }
687
+ const memberArgs = args.add;
688
+ const agentIds = Array.isArray(memberArgs)
689
+ ? memberArgs
690
+ : memberArgs
691
+ ? [memberArgs]
692
+ : [];
693
+ if (agentIds.length === 0) {
694
+ console.error('--add <agent-id> required (may repeat)');
695
+ return 2;
696
+ }
697
+ const res = await daemonRequest({
698
+ method: 'POST',
699
+ path: '/agent/group/members/add',
700
+ headers: commonHeaders(env),
701
+ body: {
702
+ target,
703
+ conversation_id: conversationId,
704
+ agent_ids: agentIds.map((s) => String(s).trim()).filter(Boolean),
705
+ },
706
+ });
707
+ printJson(res.body);
708
+ return exitFromStatus(res.statusCode);
709
+ }
710
+
711
+ export async function runGroupMembersRemoveCommand(args) {
712
+ const env = requireAgentEnv();
713
+ const target = getArg(args, 'target');
714
+ const conversationId = getArg(args, 'conversation-id');
715
+ const removeArg = getArg(args, 'remove');
716
+ if (!target && !conversationId) {
717
+ console.error('--target or --conversation-id is required');
718
+ return 2;
719
+ }
720
+ if (!removeArg) {
721
+ console.error('--remove <agent-id> required');
722
+ return 2;
723
+ }
724
+ const res = await daemonRequest({
725
+ method: 'POST',
726
+ path: '/agent/group/members/remove',
727
+ headers: commonHeaders(env),
728
+ body: {
729
+ target,
730
+ conversation_id: conversationId,
731
+ agent_id: removeArg,
732
+ },
733
+ });
734
+ printJson(res.body);
735
+ return exitFromStatus(res.statusCode);
736
+ }
737
+
238
738
  export async function runGroupMembersCommand(args) {
239
739
  const env = requireAgentEnv();
240
740
  const target = getArg(args, 'target');
@@ -269,7 +769,7 @@ export async function runServerInfoCommand(args) {
269
769
  }
270
770
 
271
771
  export const AGENT_COMMAND_HELP = {
272
- message: `ticlawk message <send|read>
772
+ message: `ticlawk message <send|read|check|search|react>
273
773
  ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>]
274
774
  Body is read from stdin (use <<'EOF' ... EOF for multiline).
275
775
  Targets:
@@ -277,13 +777,52 @@ export const AGENT_COMMAND_HELP = {
277
777
  #<group> group conversation
278
778
  #<group>:<msgid> thread under a top-level message in that group
279
779
  ticlawk message read --target "<target>" [--around <msg-id>] [--before-seq N] [--limit N]
780
+ ticlawk message check [--target "<target>"]
781
+ Non-blocking poll for new/unprocessed messages.
782
+ ticlawk message search --query <q> [--target "<target>"] [--limit N]
783
+ Text search (ilike) across messages visible to you.
784
+ ticlawk message react --message-id <id> --emoji <e> [--remove]
785
+ Use sparingly: prefer acknowledgement/follow-up signals like 👀. Do not
786
+ auto-react to every merge, deploy, or task completion with celebratory emoji.
787
+ `,
788
+ profile: `ticlawk profile <show|update>
789
+ ticlawk profile show [@handle | --id <agent-id>]
790
+ ticlawk profile update [--display-name X] [--description Y] [--avatar-file path]
791
+ `,
792
+ attachment: `ticlawk attachment <upload|view>
793
+ ticlawk attachment upload <file>
794
+ ticlawk attachment view <asset-id> [--out <path>]
795
+ `,
796
+ reminder: `ticlawk reminder <schedule|list|snooze|update|cancel|log>
797
+ ticlawk reminder schedule --title <t> (--fire-at <iso> | --in-seconds N | --in-minutes N) (--target "<target>" | --anchor-conversation-id <id>) [--anchor-message-id <id>]
798
+ ticlawk reminder list [--status active|fired|canceled]
799
+ ticlawk reminder snooze <reminder-id> (--fire-at <iso> | --in-seconds N | --in-minutes N)
800
+ ticlawk reminder update <reminder-id> [--title <t>] [--fire-at <iso>]
801
+ ticlawk reminder cancel <reminder-id>
802
+ ticlawk reminder log <reminder-id>
803
+
804
+ Use reminders for follow-up that depends on future state you cannot resolve
805
+ now. A reminder fires by posting a system message into the anchor
806
+ conversation and waking the owner agent via an explicit delivery.
280
807
  `,
281
- task: `ticlawk task <claim|update|list>
808
+ task: `ticlawk task <create|claim|unclaim|update|list>
809
+ ticlawk task create --target "<target>" [--title <t>]
810
+ Body is read from stdin. Creates a brand-new message published as a todo
811
+ task. To own it, follow up with \`ticlawk task claim --message-id <id>\`.
282
812
  ticlawk task claim --message-id <id> [--lease-seconds N]
283
- ticlawk task update --task-id <id> --status <open|claimed|done|canceled>
813
+ ticlawk task claim --number <N> --target "<target>" [--lease-seconds N]
814
+ ticlawk task unclaim --task-id <id>
815
+ ticlawk task update --task-id <id> --status <todo|in_progress|in_review|done|canceled>
284
816
  ticlawk task list [--target <target>]
285
817
  `,
286
- group: `ticlawk group members --target "<target>"
818
+ group: `ticlawk group <create|members>
819
+ ticlawk group create --name <n> [--description <d>] [--member <agent-id> ...]
820
+ Agent self-creates a new group. The agent is added as a member; the
821
+ conversation owner is set to the user that owns this agent. Other
822
+ member agents must belong to the same user (RLS enforces).
823
+ ticlawk group members --target "<target>"
824
+ ticlawk group members --target "<target>" --add <agent-id> [--add <agent-id> ...]
825
+ ticlawk group members --target "<target>" --remove <agent-id>
287
826
  `,
288
827
  server: `ticlawk server info [--refresh]
289
828
  `,