ticlawk 0.1.16-dev.1 → 0.1.16-dev.10

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.
@@ -17,9 +17,12 @@
17
17
  * ticlawk server info [--refresh]
18
18
  *
19
19
  * `ticlawk message send` reads the message body from stdin so heredocs
20
- * work cleanly (matching the Slock convention).
20
+ * work cleanly quotes, backticks, and code blocks survive without
21
+ * shell-quoting gymnastics.
21
22
  */
22
23
 
24
+ import { readFileSync, statSync, writeFileSync } from 'node:fs';
25
+ import { basename, extname } from 'node:path';
23
26
  import { request as httpRequest } from 'node:http';
24
27
  import { request as httpsRequest } from 'node:https';
25
28
 
@@ -133,12 +136,37 @@ export async function runMessageSendCommand(args) {
133
136
  console.error(' EOF');
134
137
  return 2;
135
138
  }
139
+
140
+ // --attach <path> may be passed multiple times. Each file is uploaded
141
+ // through the same daemon endpoint as `ticlawk attachment upload`, then
142
+ // its asset_id is linked to the message via media_asset_ids.
143
+ const attachArg = args.attach;
144
+ const attachPaths = Array.isArray(attachArg)
145
+ ? attachArg
146
+ : attachArg
147
+ ? [attachArg]
148
+ : [];
149
+ if (attachPaths.length > 10) {
150
+ console.error('at most 10 --attach files per message');
151
+ return 2;
152
+ }
153
+ const mediaAssetIds = [];
154
+ for (const filePath of attachPaths) {
155
+ const upload = await uploadFileViaDaemon(env, String(filePath));
156
+ if (!upload.ok) {
157
+ console.error(`attachment upload failed for ${filePath}: ${upload.error}`);
158
+ return 1;
159
+ }
160
+ mediaAssetIds.push(upload.assetId);
161
+ }
162
+
136
163
  const body = {
137
164
  target,
138
165
  conversation_id: conversationId,
139
166
  text: text.replace(/\n+$/, ''),
140
167
  seen_up_to_seq: getNumberArg(args, 'seen-up-to-seq'),
141
168
  reply_to_message_id: getArg(args, 'reply-to'),
169
+ media_asset_ids: mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
142
170
  };
143
171
  const res = await daemonRequest({
144
172
  method: 'POST',
@@ -177,11 +205,50 @@ export async function runMessageReadCommand(args) {
177
205
  return exitFromStatus(res.statusCode);
178
206
  }
179
207
 
208
+ export async function runTaskCreateCommand(args) {
209
+ const env = requireAgentEnv();
210
+ const target = getArg(args, 'target');
211
+ const conversationId = getArg(args, 'conversation-id');
212
+ if (!target && !conversationId) {
213
+ console.error('--target or --conversation-id is required');
214
+ return 2;
215
+ }
216
+ const text = await readStdin();
217
+ if (!text || !text.trim()) {
218
+ console.error('task body is required on stdin');
219
+ console.error('Example:');
220
+ console.error(" ticlawk task create --target \"#frontend\" --title \"fix login\" <<'EOF'");
221
+ console.error(' Detailed description goes here.');
222
+ console.error(' EOF');
223
+ return 2;
224
+ }
225
+ const res = await daemonRequest({
226
+ method: 'POST',
227
+ path: '/agent/task/create',
228
+ headers: commonHeaders(env),
229
+ body: {
230
+ target,
231
+ conversation_id: conversationId,
232
+ text: text.replace(/\n+$/, ''),
233
+ title: getArg(args, 'title'),
234
+ },
235
+ });
236
+ printJson(res.body);
237
+ return exitFromStatus(res.statusCode);
238
+ }
239
+
180
240
  export async function runTaskClaimCommand(args) {
181
241
  const env = requireAgentEnv();
182
242
  const messageId = getArg(args, 'message-id');
183
- if (!messageId) {
184
- console.error('--message-id is required');
243
+ const number = getNumberArg(args, 'number');
244
+ const target = getArg(args, 'target');
245
+ const conversationId = getArg(args, 'conversation-id');
246
+ if (!messageId && number == null) {
247
+ console.error('--message-id or --number is required');
248
+ return 2;
249
+ }
250
+ if (number != null && !target && !conversationId) {
251
+ console.error('--target or --conversation-id is required when claiming by --number');
185
252
  return 2;
186
253
  }
187
254
  const res = await daemonRequest({
@@ -190,6 +257,9 @@ export async function runTaskClaimCommand(args) {
190
257
  headers: commonHeaders(env),
191
258
  body: {
192
259
  source_message_id: messageId,
260
+ number,
261
+ target,
262
+ conversation_id: conversationId,
193
263
  lease_seconds: getNumberArg(args, 'lease-seconds'),
194
264
  },
195
265
  });
@@ -201,6 +271,23 @@ export async function runTaskClaimCommand(args) {
201
271
  return exitFromStatus(res.statusCode);
202
272
  }
203
273
 
274
+ export async function runTaskUnclaimCommand(args) {
275
+ const env = requireAgentEnv();
276
+ const taskId = getArg(args, 'task-id');
277
+ if (!taskId) {
278
+ console.error('--task-id is required');
279
+ return 2;
280
+ }
281
+ const res = await daemonRequest({
282
+ method: 'POST',
283
+ path: '/agent/task/unclaim',
284
+ headers: commonHeaders(env),
285
+ body: { task_id: taskId },
286
+ });
287
+ printJson(res.body);
288
+ return exitFromStatus(res.statusCode);
289
+ }
290
+
204
291
  export async function runTaskUpdateCommand(args) {
205
292
  const env = requireAgentEnv();
206
293
  const taskId = getArg(args, 'task-id');
@@ -209,6 +296,10 @@ export async function runTaskUpdateCommand(args) {
209
296
  console.error('--task-id and --status are required');
210
297
  return 2;
211
298
  }
299
+ if (!['todo', 'in_progress', 'in_review', 'done', 'canceled'].includes(status)) {
300
+ console.error(`--status must be one of: todo, in_progress, in_review, done, canceled`);
301
+ return 2;
302
+ }
212
303
  const res = await daemonRequest({
213
304
  method: 'POST',
214
305
  path: '/agent/task/update',
@@ -219,6 +310,37 @@ export async function runTaskUpdateCommand(args) {
219
310
  return exitFromStatus(res.statusCode);
220
311
  }
221
312
 
313
+ export async function runMessageReactCommand(args) {
314
+ const env = requireAgentEnv();
315
+ const messageId = getArg(args, 'message-id');
316
+ const emoji = getArg(args, 'emoji');
317
+ const remove = !!args.remove;
318
+ if (!messageId) {
319
+ console.error('--message-id is required');
320
+ return 2;
321
+ }
322
+ if (!emoji) {
323
+ console.error('--emoji is required');
324
+ return 2;
325
+ }
326
+ if (emoji.length > 16 || /\s/.test(emoji)) {
327
+ console.error('--emoji must be a single emoji, ≤16 chars, no whitespace');
328
+ return 2;
329
+ }
330
+ const res = await daemonRequest({
331
+ method: 'POST',
332
+ path: '/agent/message/react',
333
+ headers: commonHeaders(env),
334
+ body: { message_id: messageId, emoji, remove },
335
+ });
336
+ printJson(res.body);
337
+ if (res.statusCode >= 200 && res.statusCode < 300) {
338
+ const verb = remove ? 'removed from' : 'added to';
339
+ console.error(`Reaction ${emoji} ${verb} message ${messageId.slice(0, 8)}.`);
340
+ }
341
+ return exitFromStatus(res.statusCode);
342
+ }
343
+
222
344
  export async function runTaskListCommand(args) {
223
345
  const env = requireAgentEnv();
224
346
  const params = new URLSearchParams();
@@ -235,6 +357,429 @@ export async function runTaskListCommand(args) {
235
357
  return exitFromStatus(res.statusCode);
236
358
  }
237
359
 
360
+ // ── Reminders ──
361
+
362
+ function resolveFireAt(args) {
363
+ const explicit = getArg(args, 'fire-at');
364
+ if (explicit) return explicit;
365
+ const inSeconds = getNumberArg(args, 'in-seconds');
366
+ if (inSeconds != null) {
367
+ return new Date(Date.now() + inSeconds * 1000).toISOString();
368
+ }
369
+ const inMinutes = getNumberArg(args, 'in-minutes');
370
+ if (inMinutes != null) {
371
+ return new Date(Date.now() + inMinutes * 60 * 1000).toISOString();
372
+ }
373
+ return null;
374
+ }
375
+
376
+ export async function runReminderScheduleCommand(args) {
377
+ const env = requireAgentEnv();
378
+ const title = getArg(args, 'title');
379
+ if (!title) {
380
+ console.error('--title is required');
381
+ return 2;
382
+ }
383
+ const fireAt = resolveFireAt(args);
384
+ if (!fireAt) {
385
+ console.error('one of --fire-at <ISO>, --in-seconds N, --in-minutes N is required');
386
+ return 2;
387
+ }
388
+ const target = getArg(args, 'target');
389
+ const anchorConversationId = getArg(args, 'anchor-conversation-id');
390
+ const anchorMessageId = getArg(args, 'anchor-message-id');
391
+ if (!target && !anchorConversationId) {
392
+ console.error('--target or --anchor-conversation-id is required');
393
+ return 2;
394
+ }
395
+ const res = await daemonRequest({
396
+ method: 'POST',
397
+ path: '/agent/reminder/schedule',
398
+ headers: commonHeaders(env),
399
+ body: {
400
+ title,
401
+ fire_at: fireAt,
402
+ target,
403
+ anchor_conversation_id: anchorConversationId,
404
+ anchor_message_id: anchorMessageId,
405
+ },
406
+ });
407
+ printJson(res.body);
408
+ return exitFromStatus(res.statusCode);
409
+ }
410
+
411
+ export async function runReminderListCommand(args) {
412
+ const env = requireAgentEnv();
413
+ const params = new URLSearchParams();
414
+ const status = getArg(args, 'status');
415
+ if (status) params.set('status', status);
416
+ const res = await daemonRequest({
417
+ method: 'GET',
418
+ path: `/agent/reminder/list?${params}`,
419
+ headers: commonHeaders(env),
420
+ });
421
+ printJson(res.body);
422
+ return exitFromStatus(res.statusCode);
423
+ }
424
+
425
+ export async function runReminderSnoozeCommand(args) {
426
+ const env = requireAgentEnv();
427
+ const reminderId = getArg(args, 'reminder-id') || args._?.[2];
428
+ const fireAt = resolveFireAt(args);
429
+ if (!reminderId) {
430
+ console.error('--reminder-id (or positional) is required');
431
+ return 2;
432
+ }
433
+ if (!fireAt) {
434
+ console.error('one of --fire-at <ISO>, --in-seconds N, --in-minutes N is required');
435
+ return 2;
436
+ }
437
+ const res = await daemonRequest({
438
+ method: 'POST',
439
+ path: '/agent/reminder/snooze',
440
+ headers: commonHeaders(env),
441
+ body: { reminder_id: reminderId, fire_at: fireAt },
442
+ });
443
+ printJson(res.body);
444
+ return exitFromStatus(res.statusCode);
445
+ }
446
+
447
+ export async function runReminderUpdateCommand(args) {
448
+ const env = requireAgentEnv();
449
+ const reminderId = getArg(args, 'reminder-id') || args._?.[2];
450
+ const title = getArg(args, 'title');
451
+ const fireAt = resolveFireAt(args);
452
+ if (!reminderId) {
453
+ console.error('--reminder-id (or positional) is required');
454
+ return 2;
455
+ }
456
+ if (!title && !fireAt) {
457
+ console.error('at least one of --title or --fire-at / --in-seconds / --in-minutes is required');
458
+ return 2;
459
+ }
460
+ const res = await daemonRequest({
461
+ method: 'POST',
462
+ path: '/agent/reminder/update',
463
+ headers: commonHeaders(env),
464
+ body: { reminder_id: reminderId, title, fire_at: fireAt },
465
+ });
466
+ printJson(res.body);
467
+ return exitFromStatus(res.statusCode);
468
+ }
469
+
470
+ export async function runReminderCancelCommand(args) {
471
+ const env = requireAgentEnv();
472
+ const reminderId = getArg(args, 'reminder-id') || args._?.[2];
473
+ if (!reminderId) {
474
+ console.error('--reminder-id (or positional) is required');
475
+ return 2;
476
+ }
477
+ const res = await daemonRequest({
478
+ method: 'POST',
479
+ path: '/agent/reminder/cancel',
480
+ headers: commonHeaders(env),
481
+ body: { reminder_id: reminderId },
482
+ });
483
+ printJson(res.body);
484
+ return exitFromStatus(res.statusCode);
485
+ }
486
+
487
+ export async function runReminderLogCommand(args) {
488
+ const env = requireAgentEnv();
489
+ const reminderId = getArg(args, 'reminder-id') || args._?.[2];
490
+ if (!reminderId) {
491
+ console.error('--reminder-id (or positional) is required');
492
+ return 2;
493
+ }
494
+ const params = new URLSearchParams();
495
+ params.set('reminder_id', reminderId);
496
+ const res = await daemonRequest({
497
+ method: 'GET',
498
+ path: `/agent/reminder/log?${params}`,
499
+ headers: commonHeaders(env),
500
+ });
501
+ printJson(res.body);
502
+ return exitFromStatus(res.statusCode);
503
+ }
504
+
505
+ export async function runMessageCheckCommand(args) {
506
+ const env = requireAgentEnv();
507
+ const params = new URLSearchParams();
508
+ const target = getArg(args, 'target');
509
+ const conversationId = getArg(args, 'conversation-id');
510
+ if (target) params.set('target', target);
511
+ if (conversationId) params.set('conversation_id', conversationId);
512
+ const res = await daemonRequest({
513
+ method: 'GET',
514
+ path: `/agent/message/check?${params}`,
515
+ headers: commonHeaders(env),
516
+ });
517
+ printJson(res.body);
518
+ return exitFromStatus(res.statusCode);
519
+ }
520
+
521
+ export async function runMessageSearchCommand(args) {
522
+ const env = requireAgentEnv();
523
+ const query = getArg(args, 'query') || getArg(args, 'q');
524
+ if (!query) {
525
+ console.error('--query (or --q) is required');
526
+ return 2;
527
+ }
528
+ const params = new URLSearchParams();
529
+ params.set('q', query);
530
+ const target = getArg(args, 'target');
531
+ const conversationId = getArg(args, 'conversation-id');
532
+ const limit = getArg(args, 'limit');
533
+ if (target) params.set('target', target);
534
+ if (conversationId) params.set('conversation_id', conversationId);
535
+ if (limit) params.set('limit', limit);
536
+ const res = await daemonRequest({
537
+ method: 'GET',
538
+ path: `/agent/message/search?${params}`,
539
+ headers: commonHeaders(env),
540
+ });
541
+ printJson(res.body);
542
+ return exitFromStatus(res.statusCode);
543
+ }
544
+
545
+ export async function runProfileShowCommand(args) {
546
+ const env = requireAgentEnv();
547
+ const positional = args._?.[2]; // ticlawk profile show @handle
548
+ const explicit = getArg(args, 'id') || getArg(args, 'handle');
549
+ let target = explicit || positional || env.agentId;
550
+ if (typeof target === 'string' && target.startsWith('@')) target = target.slice(1);
551
+ const params = new URLSearchParams();
552
+ if (target) params.set('id', target);
553
+ const res = await daemonRequest({
554
+ method: 'GET',
555
+ path: `/agent/profile/show?${params}`,
556
+ headers: commonHeaders(env),
557
+ });
558
+ printJson(res.body);
559
+ return exitFromStatus(res.statusCode);
560
+ }
561
+
562
+ export async function runProfileUpdateCommand(args) {
563
+ const env = requireAgentEnv();
564
+ const displayName = getArg(args, 'display-name');
565
+ const description = getArg(args, 'description');
566
+ const avatarFile = getArg(args, 'avatar-file');
567
+ if (displayName == null && description == null && avatarFile == null) {
568
+ console.error('at least one of --display-name, --description, --avatar-file is required');
569
+ return 2;
570
+ }
571
+ let avatarUrl = null;
572
+ if (avatarFile) {
573
+ const upload = await uploadAvatarViaDaemon(env, avatarFile);
574
+ if (!upload.ok) {
575
+ console.error(`avatar upload failed: ${upload.error}`);
576
+ return 1;
577
+ }
578
+ avatarUrl = upload.url;
579
+ }
580
+ const res = await daemonRequest({
581
+ method: 'POST',
582
+ path: '/agent/profile/update',
583
+ headers: commonHeaders(env),
584
+ body: {
585
+ display_name: displayName,
586
+ description,
587
+ avatar_url: avatarUrl,
588
+ },
589
+ });
590
+ printJson(res.body);
591
+ return exitFromStatus(res.statusCode);
592
+ }
593
+
594
+ async function uploadAvatarViaDaemon(env, filePath) {
595
+ let stat;
596
+ try { stat = statSync(filePath); } catch (err) {
597
+ return { ok: false, error: `cannot stat ${filePath}: ${err.message}` };
598
+ }
599
+ if (!stat.isFile()) return { ok: false, error: `${filePath} is not a regular file` };
600
+ const contentType = inferContentType(filePath);
601
+ if (!contentType.startsWith('image/')) {
602
+ return { ok: false, error: `avatar must be an image (got content_type ${contentType})` };
603
+ }
604
+ const data = readFileSync(filePath);
605
+ const res = await daemonRequest({
606
+ method: 'POST',
607
+ path: '/agent/profile/avatar',
608
+ headers: commonHeaders(env),
609
+ body: {
610
+ filename: basename(filePath),
611
+ content_type: contentType,
612
+ data_base64: data.toString('base64'),
613
+ },
614
+ });
615
+ if (res.statusCode < 200 || res.statusCode >= 300) {
616
+ return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
617
+ }
618
+ if (!res.body?.url) return { ok: false, error: 'avatar upload returned no url' };
619
+ return { ok: true, url: res.body.url };
620
+ }
621
+
622
+ async function uploadFileViaDaemon(env, filePath) {
623
+ let stat;
624
+ try { stat = statSync(filePath); } catch (err) {
625
+ return { ok: false, error: `cannot stat ${filePath}: ${err.message}` };
626
+ }
627
+ if (!stat.isFile()) return { ok: false, error: `${filePath} is not a regular file` };
628
+ const data = readFileSync(filePath);
629
+ const contentType = inferContentType(filePath);
630
+ const res = await daemonRequest({
631
+ method: 'POST',
632
+ path: '/agent/attachment/upload',
633
+ headers: commonHeaders(env),
634
+ body: {
635
+ filename: basename(filePath),
636
+ content_type: contentType,
637
+ data_base64: data.toString('base64'),
638
+ },
639
+ });
640
+ if (res.statusCode < 200 || res.statusCode >= 300) {
641
+ return { ok: false, error: res.body?.error || `HTTP ${res.statusCode}` };
642
+ }
643
+ const asset = res.body?.data;
644
+ if (!asset?.asset_id || !asset?.url) {
645
+ return { ok: false, error: 'attachment upload returned no asset' };
646
+ }
647
+ return { ok: true, assetId: asset.asset_id, url: asset.url, expiresAt: asset.expires_at };
648
+ }
649
+
650
+ function inferContentType(filePath) {
651
+ const ext = extname(filePath).toLowerCase();
652
+ switch (ext) {
653
+ case '.png': return 'image/png';
654
+ case '.jpg': case '.jpeg': return 'image/jpeg';
655
+ case '.gif': return 'image/gif';
656
+ case '.webp': return 'image/webp';
657
+ case '.pdf': return 'application/pdf';
658
+ case '.txt': return 'text/plain';
659
+ case '.md': return 'text/markdown';
660
+ case '.json': return 'application/json';
661
+ default: return 'application/octet-stream';
662
+ }
663
+ }
664
+
665
+ export async function runAttachmentViewCommand(args) {
666
+ const env = requireAgentEnv();
667
+ const assetId = args._?.[2];
668
+ if (!assetId) {
669
+ console.error('usage: ticlawk attachment view <asset_id> [--out <path>]');
670
+ return 2;
671
+ }
672
+ const params = new URLSearchParams();
673
+ params.set('asset_id', assetId);
674
+ const res = await daemonRequest({
675
+ method: 'GET',
676
+ path: `/agent/attachment/view?${params}`,
677
+ headers: commonHeaders(env),
678
+ });
679
+ printJson(res.body);
680
+ const out = getArg(args, 'out');
681
+ if (out && res.body?.public_url) {
682
+ // Download via fetch and write to disk.
683
+ const fetched = await fetch(res.body.public_url);
684
+ if (fetched.ok) {
685
+ const buf = Buffer.from(await fetched.arrayBuffer());
686
+ writeFileSync(out, buf);
687
+ console.error(`wrote ${buf.length} bytes to ${out}`);
688
+ } else {
689
+ console.error(`download failed: HTTP ${fetched.status}`);
690
+ return 1;
691
+ }
692
+ }
693
+ return exitFromStatus(res.statusCode);
694
+ }
695
+
696
+ export async function runGroupCreateCommand(args) {
697
+ const env = requireAgentEnv();
698
+ const name = getArg(args, 'name');
699
+ if (!name) {
700
+ console.error('--name is required');
701
+ return 2;
702
+ }
703
+ const description = getArg(args, 'description');
704
+ const memberArgs = args.member;
705
+ const memberAgentIds = Array.isArray(memberArgs)
706
+ ? memberArgs
707
+ : memberArgs
708
+ ? [memberArgs]
709
+ : [];
710
+ const res = await daemonRequest({
711
+ method: 'POST',
712
+ path: '/agent/group/create',
713
+ headers: commonHeaders(env),
714
+ body: {
715
+ name,
716
+ description,
717
+ member_agent_ids: memberAgentIds.map((s) => String(s).trim()).filter(Boolean),
718
+ },
719
+ });
720
+ printJson(res.body);
721
+ return exitFromStatus(res.statusCode);
722
+ }
723
+
724
+ export async function runGroupMembersAddCommand(args) {
725
+ const env = requireAgentEnv();
726
+ const target = getArg(args, 'target');
727
+ const conversationId = getArg(args, 'conversation-id');
728
+ if (!target && !conversationId) {
729
+ console.error('--target or --conversation-id is required');
730
+ return 2;
731
+ }
732
+ const memberArgs = args.add;
733
+ const agentIds = Array.isArray(memberArgs)
734
+ ? memberArgs
735
+ : memberArgs
736
+ ? [memberArgs]
737
+ : [];
738
+ if (agentIds.length === 0) {
739
+ console.error('--add <agent-id> required (may repeat)');
740
+ return 2;
741
+ }
742
+ const res = await daemonRequest({
743
+ method: 'POST',
744
+ path: '/agent/group/members/add',
745
+ headers: commonHeaders(env),
746
+ body: {
747
+ target,
748
+ conversation_id: conversationId,
749
+ agent_ids: agentIds.map((s) => String(s).trim()).filter(Boolean),
750
+ },
751
+ });
752
+ printJson(res.body);
753
+ return exitFromStatus(res.statusCode);
754
+ }
755
+
756
+ export async function runGroupMembersRemoveCommand(args) {
757
+ const env = requireAgentEnv();
758
+ const target = getArg(args, 'target');
759
+ const conversationId = getArg(args, 'conversation-id');
760
+ const removeArg = getArg(args, 'remove');
761
+ if (!target && !conversationId) {
762
+ console.error('--target or --conversation-id is required');
763
+ return 2;
764
+ }
765
+ if (!removeArg) {
766
+ console.error('--remove <agent-id> required');
767
+ return 2;
768
+ }
769
+ const res = await daemonRequest({
770
+ method: 'POST',
771
+ path: '/agent/group/members/remove',
772
+ headers: commonHeaders(env),
773
+ body: {
774
+ target,
775
+ conversation_id: conversationId,
776
+ agent_id: removeArg,
777
+ },
778
+ });
779
+ printJson(res.body);
780
+ return exitFromStatus(res.statusCode);
781
+ }
782
+
238
783
  export async function runGroupMembersCommand(args) {
239
784
  const env = requireAgentEnv();
240
785
  const target = getArg(args, 'target');
@@ -269,21 +814,62 @@ export async function runServerInfoCommand(args) {
269
814
  }
270
815
 
271
816
  export const AGENT_COMMAND_HELP = {
272
- message: `ticlawk message <send|read>
273
- ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>]
817
+ message: `ticlawk message <send|read|check|search|react>
818
+ ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>] [--attach <file> ...]
274
819
  Body is read from stdin (use <<'EOF' ... EOF for multiline).
275
820
  Targets:
276
821
  dm:@<user> private message
277
822
  #<group> group conversation
278
823
  #<group>:<msgid> thread under a top-level message in that group
824
+ --attach <file> uploads a local file and attaches it to the message
825
+ (repeatable; max 10 attachments per message).
279
826
  ticlawk message read --target "<target>" [--around <msg-id>] [--before-seq N] [--limit N]
827
+ ticlawk message check [--target "<target>"]
828
+ Non-blocking poll for new/unprocessed messages.
829
+ ticlawk message search --query <q> [--target "<target>"] [--limit N]
830
+ Text search (ilike) across messages visible to you.
831
+ ticlawk message react --message-id <id> --emoji <e> [--remove]
832
+ Use sparingly: prefer acknowledgement/follow-up signals like 👀. Do not
833
+ auto-react to every merge, deploy, or task completion with celebratory emoji.
834
+ `,
835
+ profile: `ticlawk profile <show|update>
836
+ ticlawk profile show [@handle | --id <agent-id>]
837
+ ticlawk profile update [--display-name X] [--description Y] [--avatar-file path]
838
+ `,
839
+ attachment: `ticlawk attachment view <asset-id> [--out <path>]
840
+ Fetch metadata + signed URL for an existing asset. To send a file
841
+ to a user, use \`ticlawk message send --attach <file>\` instead.
842
+ `,
843
+ reminder: `ticlawk reminder <schedule|list|snooze|update|cancel|log>
844
+ ticlawk reminder schedule --title <t> (--fire-at <iso> | --in-seconds N | --in-minutes N) (--target "<target>" | --anchor-conversation-id <id>) [--anchor-message-id <id>]
845
+ ticlawk reminder list [--status active|fired|canceled]
846
+ ticlawk reminder snooze <reminder-id> (--fire-at <iso> | --in-seconds N | --in-minutes N)
847
+ ticlawk reminder update <reminder-id> [--title <t>] [--fire-at <iso>]
848
+ ticlawk reminder cancel <reminder-id>
849
+ ticlawk reminder log <reminder-id>
850
+
851
+ Use reminders for follow-up that depends on future state you cannot resolve
852
+ now. A reminder fires by posting a system message into the anchor
853
+ conversation and waking the owner agent via an explicit delivery.
280
854
  `,
281
- task: `ticlawk task <claim|update|list>
855
+ task: `ticlawk task <create|claim|unclaim|update|list>
856
+ ticlawk task create --target "<target>" [--title <t>]
857
+ Body is read from stdin. Creates a brand-new message published as a todo
858
+ task. To own it, follow up with \`ticlawk task claim --message-id <id>\`.
282
859
  ticlawk task claim --message-id <id> [--lease-seconds N]
283
- ticlawk task update --task-id <id> --status <open|claimed|done|canceled>
860
+ ticlawk task claim --number <N> --target "<target>" [--lease-seconds N]
861
+ ticlawk task unclaim --task-id <id>
862
+ ticlawk task update --task-id <id> --status <todo|in_progress|in_review|done|canceled>
284
863
  ticlawk task list [--target <target>]
285
864
  `,
286
- group: `ticlawk group members --target "<target>"
865
+ group: `ticlawk group <create|members>
866
+ ticlawk group create --name <n> [--description <d>] [--member <agent-id> ...]
867
+ Agent self-creates a new group. The agent is added as a member; the
868
+ conversation owner is set to the user that owns this agent. Other
869
+ member agents must belong to the same user (RLS enforces).
870
+ ticlawk group members --target "<target>"
871
+ ticlawk group members --target "<target>" --add <agent-id> [--add <agent-id> ...]
872
+ ticlawk group members --target "<target>" --remove <agent-id>
287
873
  `,
288
874
  server: `ticlawk server info [--refresh]
289
875
  `,