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.
- package/README.md +13 -0
- package/bin/ticlawk.mjs +116 -0
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +226 -28
- package/src/adapters/ticlawk/index.mjs +258 -113
- package/src/cli/agent-commands.mjs +594 -8
- package/src/core/agent-cli-handlers.mjs +443 -3
- package/src/core/agent-home.mjs +85 -0
- package/src/core/http.mjs +121 -0
- package/src/core/reminder-ticker.mjs +70 -0
- package/src/core/runtime-contract.mjs +1 -1
- package/src/core/runtime-support.mjs +31 -59
- package/src/core/ticlawk-control.mjs +3 -3
- package/src/migrate/write-initial-memory.mjs +101 -0
- package/src/runtimes/_shared/standing-prompt.mjs +296 -77
- package/src/runtimes/claude-code/index.mjs +28 -131
- package/src/runtimes/codex/index.mjs +15 -39
- package/src/runtimes/openclaw/index.mjs +39 -30
- package/src/runtimes/openclaw/target.mjs +0 -30
- package/src/runtimes/opencode/index.mjs +19 -54
- package/src/runtimes/pi/index.mjs +16 -49
- package/ticlawk.mjs +31 -6
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
|
@@ -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
|
|
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
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
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
|
`,
|