ticlawk 0.1.16-dev.1 → 0.1.16-dev.11
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/argv.mjs +11 -1
- 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
|
@@ -145,6 +145,10 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
145
145
|
return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
const mediaAssetIds = Array.isArray(body?.media_asset_ids)
|
|
149
|
+
? body.media_asset_ids.map((v) => String(v).trim()).filter(Boolean)
|
|
150
|
+
: [];
|
|
151
|
+
|
|
148
152
|
try {
|
|
149
153
|
const data = await api.sendAgentMessage({
|
|
150
154
|
actingAgentId,
|
|
@@ -153,6 +157,7 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
153
157
|
seenUpToSeq: body?.seen_up_to_seq,
|
|
154
158
|
replyToMessageId: body?.reply_to_message_id || threadRootMsgId || null,
|
|
155
159
|
runtimeHostId: getRuntimeHostId(req, body),
|
|
160
|
+
mediaAssetIds,
|
|
156
161
|
});
|
|
157
162
|
debugLog('agent-cli', 'send.ok', {
|
|
158
163
|
actingAgentId,
|
|
@@ -200,22 +205,86 @@ export async function handleMessageRead(req, query, ctx) {
|
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
export async function handleTaskCreate(req, body, ctx) {
|
|
209
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
210
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
211
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
212
|
+
|
|
213
|
+
const text = String(body?.text || '').trim();
|
|
214
|
+
if (!text) return { status: 400, body: { error: 'text is required' } };
|
|
215
|
+
|
|
216
|
+
let conversationId = body?.conversation_id || null;
|
|
217
|
+
if (!conversationId && body?.target) {
|
|
218
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
219
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
220
|
+
conversationId = resolved.conversationId;
|
|
221
|
+
}
|
|
222
|
+
if (!conversationId) {
|
|
223
|
+
return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const data = await api.createAgentTask({
|
|
228
|
+
actingAgentId,
|
|
229
|
+
conversationId,
|
|
230
|
+
text,
|
|
231
|
+
title: body?.title ?? null,
|
|
232
|
+
});
|
|
233
|
+
debugLog('agent-cli', 'task.create', {
|
|
234
|
+
actingAgentId,
|
|
235
|
+
conversationId,
|
|
236
|
+
messageId: data?.message?.id,
|
|
237
|
+
taskNumber: data?.task?.number,
|
|
238
|
+
});
|
|
239
|
+
return { status: 200, body: data };
|
|
240
|
+
} catch (err) {
|
|
241
|
+
debugError('agent-cli', 'task.create.failed', {
|
|
242
|
+
actingAgentId,
|
|
243
|
+
conversationId,
|
|
244
|
+
error: err?.message || String(err),
|
|
245
|
+
});
|
|
246
|
+
return { status: err?.status || 500, body: { error: err?.message || 'task create failed' } };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
203
250
|
export async function handleTaskClaim(req, body, ctx) {
|
|
204
251
|
const actingAgentId = getActingAgentId(req, body);
|
|
205
252
|
const v = validateActingAgent(actingAgentId, ctx);
|
|
206
253
|
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
207
|
-
const sourceMessageId = body?.source_message_id || body?.message_id;
|
|
208
|
-
|
|
254
|
+
const sourceMessageId = body?.source_message_id || body?.message_id || null;
|
|
255
|
+
const number = body?.number != null ? Number(body.number) : null;
|
|
256
|
+
|
|
257
|
+
// Number-based claim needs the conversation_id to disambiguate. Allow
|
|
258
|
+
// the caller to pass either conversation_id directly or a target.
|
|
259
|
+
let conversationId = body?.conversation_id || null;
|
|
260
|
+
if (number != null && !conversationId && body?.target) {
|
|
261
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
262
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
263
|
+
conversationId = resolved.conversationId;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!sourceMessageId && number == null) {
|
|
267
|
+
return { status: 400, body: { error: '--message-id or --number is required' } };
|
|
268
|
+
}
|
|
269
|
+
if (number != null && !conversationId) {
|
|
270
|
+
return { status: 400, body: { error: '--target or --conversation-id is required when claiming by number' } };
|
|
271
|
+
}
|
|
272
|
+
|
|
209
273
|
try {
|
|
210
274
|
const data = await api.claimAgentTask({
|
|
211
275
|
actingAgentId,
|
|
212
276
|
sourceMessageId,
|
|
277
|
+
conversationId,
|
|
278
|
+
number,
|
|
213
279
|
leaseSeconds: body?.lease_seconds,
|
|
214
280
|
});
|
|
215
281
|
debugLog('agent-cli', 'task.claim', {
|
|
216
282
|
actingAgentId,
|
|
217
|
-
sourceMessageId,
|
|
283
|
+
sourceMessageId: sourceMessageId || null,
|
|
284
|
+
number: number ?? null,
|
|
285
|
+
conversationId: conversationId || null,
|
|
218
286
|
claimed: data?.claimed,
|
|
287
|
+
taskNumber: data?.task?.number || null,
|
|
219
288
|
});
|
|
220
289
|
return { status: data?.claimed === false ? 409 : 200, body: data };
|
|
221
290
|
} catch (err) {
|
|
@@ -223,6 +292,27 @@ export async function handleTaskClaim(req, body, ctx) {
|
|
|
223
292
|
}
|
|
224
293
|
}
|
|
225
294
|
|
|
295
|
+
export async function handleTaskUnclaim(req, body, ctx) {
|
|
296
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
297
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
298
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
299
|
+
if (!body?.task_id) return { status: 400, body: { error: 'task_id is required' } };
|
|
300
|
+
try {
|
|
301
|
+
const data = await api.unclaimAgentTask({
|
|
302
|
+
actingAgentId,
|
|
303
|
+
taskId: body.task_id,
|
|
304
|
+
});
|
|
305
|
+
debugLog('agent-cli', 'task.unclaim', {
|
|
306
|
+
actingAgentId,
|
|
307
|
+
taskId: body.task_id,
|
|
308
|
+
ok: data?.ok,
|
|
309
|
+
});
|
|
310
|
+
return { status: data?.ok ? 200 : 409, body: data };
|
|
311
|
+
} catch (err) {
|
|
312
|
+
return { status: err?.status || 500, body: { error: err?.message || 'unclaim failed' } };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
226
316
|
export async function handleTaskUpdate(req, body, ctx) {
|
|
227
317
|
const actingAgentId = getActingAgentId(req, body);
|
|
228
318
|
const v = validateActingAgent(actingAgentId, ctx);
|
|
@@ -276,6 +366,356 @@ export async function handleGroupMembers(req, query, ctx) {
|
|
|
276
366
|
}
|
|
277
367
|
}
|
|
278
368
|
|
|
369
|
+
export async function handleMessageReact(req, body, ctx) {
|
|
370
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
371
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
372
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
373
|
+
const messageId = body?.message_id;
|
|
374
|
+
const emoji = body?.emoji;
|
|
375
|
+
const remove = !!body?.remove;
|
|
376
|
+
if (!messageId) return { status: 400, body: { error: 'message_id is required' } };
|
|
377
|
+
if (!emoji) return { status: 400, body: { error: 'emoji is required' } };
|
|
378
|
+
try {
|
|
379
|
+
const data = await api.reactToMessage({
|
|
380
|
+
actingAgentId, messageId, emoji, remove,
|
|
381
|
+
});
|
|
382
|
+
debugLog('agent-cli', remove ? 'message.unreact' : 'message.react', {
|
|
383
|
+
actingAgentId, messageId, emoji,
|
|
384
|
+
});
|
|
385
|
+
return { status: 200, body: data };
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return { status: err?.status || 500, body: { error: err?.message || 'react failed' } };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function handleReminderSchedule(req, body, ctx) {
|
|
392
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
393
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
394
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
395
|
+
if (!body?.title) return { status: 400, body: { error: 'title is required' } };
|
|
396
|
+
if (!body?.fire_at) return { status: 400, body: { error: 'fire_at (ISO timestamp) is required' } };
|
|
397
|
+
|
|
398
|
+
let anchorConversationId = body?.anchor_conversation_id || null;
|
|
399
|
+
if (!anchorConversationId && body?.target) {
|
|
400
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
401
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
402
|
+
anchorConversationId = resolved.conversationId;
|
|
403
|
+
}
|
|
404
|
+
if (!anchorConversationId) {
|
|
405
|
+
return { status: 400, body: { error: '--target or --anchor-conversation-id required' } };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const data = await api.scheduleAgentReminder({
|
|
410
|
+
actingAgentId,
|
|
411
|
+
title: body.title,
|
|
412
|
+
fireAt: body.fire_at,
|
|
413
|
+
anchorConversationId,
|
|
414
|
+
anchorMessageId: body?.anchor_message_id || null,
|
|
415
|
+
});
|
|
416
|
+
debugLog('agent-cli', 'reminder.schedule', {
|
|
417
|
+
actingAgentId,
|
|
418
|
+
reminderId: data?.data?.id,
|
|
419
|
+
fireAt: body.fire_at,
|
|
420
|
+
});
|
|
421
|
+
return { status: 200, body: data };
|
|
422
|
+
} catch (err) {
|
|
423
|
+
return { status: err?.status || 500, body: { error: err?.message || 'schedule failed' } };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function handleReminderList(req, query, ctx) {
|
|
428
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
429
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
430
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
431
|
+
try {
|
|
432
|
+
const data = await api.listAgentReminders({
|
|
433
|
+
actingAgentId,
|
|
434
|
+
status: query?.status || null,
|
|
435
|
+
});
|
|
436
|
+
return { status: 200, body: { data } };
|
|
437
|
+
} catch (err) {
|
|
438
|
+
return { status: err?.status || 500, body: { error: err?.message || 'list failed' } };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function handleReminderSnooze(req, body, ctx) {
|
|
443
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
444
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
445
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
446
|
+
if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
|
|
447
|
+
if (!body?.fire_at) return { status: 400, body: { error: 'fire_at is required' } };
|
|
448
|
+
try {
|
|
449
|
+
const data = await api.snoozeAgentReminder({
|
|
450
|
+
actingAgentId,
|
|
451
|
+
reminderId: body.reminder_id,
|
|
452
|
+
fireAt: body.fire_at,
|
|
453
|
+
});
|
|
454
|
+
return { status: 200, body: data };
|
|
455
|
+
} catch (err) {
|
|
456
|
+
return { status: err?.status || 500, body: { error: err?.message || 'snooze failed' } };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export async function handleReminderUpdate(req, body, ctx) {
|
|
461
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
462
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
463
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
464
|
+
if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
|
|
465
|
+
try {
|
|
466
|
+
const data = await api.updateAgentReminder({
|
|
467
|
+
actingAgentId,
|
|
468
|
+
reminderId: body.reminder_id,
|
|
469
|
+
title: body?.title || null,
|
|
470
|
+
fireAt: body?.fire_at || null,
|
|
471
|
+
});
|
|
472
|
+
return { status: 200, body: data };
|
|
473
|
+
} catch (err) {
|
|
474
|
+
return { status: err?.status || 500, body: { error: err?.message || 'update failed' } };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function handleReminderCancel(req, body, ctx) {
|
|
479
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
480
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
481
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
482
|
+
if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
|
|
483
|
+
try {
|
|
484
|
+
const data = await api.cancelAgentReminder({
|
|
485
|
+
actingAgentId,
|
|
486
|
+
reminderId: body.reminder_id,
|
|
487
|
+
});
|
|
488
|
+
return { status: 200, body: data };
|
|
489
|
+
} catch (err) {
|
|
490
|
+
return { status: err?.status || 500, body: { error: err?.message || 'cancel failed' } };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function handleReminderLog(req, query, ctx) {
|
|
495
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
496
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
497
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
498
|
+
if (!query?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
|
|
499
|
+
try {
|
|
500
|
+
const data = await api.getAgentReminderLog({
|
|
501
|
+
actingAgentId,
|
|
502
|
+
reminderId: query.reminder_id,
|
|
503
|
+
});
|
|
504
|
+
return { status: 200, body: { data } };
|
|
505
|
+
} catch (err) {
|
|
506
|
+
return { status: err?.status || 500, body: { error: err?.message || 'log failed' } };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function handleMessageCheck(req, query, ctx) {
|
|
511
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
512
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
513
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
514
|
+
let conversationId = query?.conversation_id || null;
|
|
515
|
+
if (!conversationId && query?.target) {
|
|
516
|
+
const resolved = await resolveTarget(actingAgentId, String(query.target));
|
|
517
|
+
if (!resolved.error) conversationId = resolved.conversationId;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const data = await api.checkAgentMessages({ actingAgentId, conversationId });
|
|
521
|
+
return { status: 200, body: data };
|
|
522
|
+
} catch (err) {
|
|
523
|
+
return { status: err?.status || 500, body: { error: err?.message || 'check failed' } };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export async function handleMessageSearch(req, query, ctx) {
|
|
528
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
529
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
530
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
531
|
+
if (!query?.q) return { status: 400, body: { error: 'q is required' } };
|
|
532
|
+
let conversationId = query?.conversation_id || null;
|
|
533
|
+
if (!conversationId && query?.target) {
|
|
534
|
+
const resolved = await resolveTarget(actingAgentId, String(query.target));
|
|
535
|
+
if (!resolved.error) conversationId = resolved.conversationId;
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const data = await api.searchAgentMessages({
|
|
539
|
+
actingAgentId,
|
|
540
|
+
query: query.q,
|
|
541
|
+
conversationId,
|
|
542
|
+
limit: query?.limit != null ? Number(query.limit) : null,
|
|
543
|
+
});
|
|
544
|
+
return { status: 200, body: { data } };
|
|
545
|
+
} catch (err) {
|
|
546
|
+
return { status: err?.status || 500, body: { error: err?.message || 'search failed' } };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function handleProfileShow(req, query, ctx) {
|
|
551
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
552
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
553
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
554
|
+
const idOrHandle = query?.id || query?.handle || actingAgentId;
|
|
555
|
+
try {
|
|
556
|
+
const data = await api.getAgentProfile({ actingAgentId, idOrHandle });
|
|
557
|
+
return { status: 200, body: data };
|
|
558
|
+
} catch (err) {
|
|
559
|
+
return { status: err?.status || 500, body: { error: err?.message || 'profile show failed' } };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export async function handleProfileUpdate(req, body, ctx) {
|
|
564
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
565
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
566
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
567
|
+
try {
|
|
568
|
+
const data = await api.patchAgentProfile({
|
|
569
|
+
actingAgentId,
|
|
570
|
+
displayName: body?.display_name ?? null,
|
|
571
|
+
description: body?.description ?? null,
|
|
572
|
+
avatarUrl: body?.avatar_url ?? null,
|
|
573
|
+
});
|
|
574
|
+
debugLog('agent-cli', 'profile.update', { actingAgentId, keys: Object.keys(body || {}) });
|
|
575
|
+
return { status: 200, body: data };
|
|
576
|
+
} catch (err) {
|
|
577
|
+
return { status: err?.status || 500, body: { error: err?.message || 'profile update failed' } };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function handleAttachmentUpload(req, body, ctx) {
|
|
582
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
583
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
584
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
585
|
+
if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
|
|
586
|
+
try {
|
|
587
|
+
const data = await api.uploadAgentAttachment({
|
|
588
|
+
actingAgentId,
|
|
589
|
+
filename: body?.filename || 'attachment.bin',
|
|
590
|
+
contentType: body?.content_type || 'application/octet-stream',
|
|
591
|
+
dataBase64: body.data_base64,
|
|
592
|
+
});
|
|
593
|
+
debugLog('agent-cli', 'attachment.upload', {
|
|
594
|
+
actingAgentId,
|
|
595
|
+
asset_id: data?.data?.asset_id,
|
|
596
|
+
bytes: data?.data?.size_bytes,
|
|
597
|
+
});
|
|
598
|
+
return { status: 200, body: data };
|
|
599
|
+
} catch (err) {
|
|
600
|
+
return { status: err?.status || 500, body: { error: err?.message || 'attachment upload failed' } };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export async function handleProfileAvatarUpload(req, body, ctx) {
|
|
605
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
606
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
607
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
608
|
+
if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
|
|
609
|
+
if (!body?.content_type || !String(body.content_type).startsWith('image/')) {
|
|
610
|
+
return { status: 400, body: { error: 'content_type must be image/*' } };
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const data = await api.uploadAgentAvatar({
|
|
614
|
+
actingAgentId,
|
|
615
|
+
filename: body?.filename || 'avatar.bin',
|
|
616
|
+
contentType: body.content_type,
|
|
617
|
+
dataBase64: body.data_base64,
|
|
618
|
+
});
|
|
619
|
+
debugLog('agent-cli', 'avatar.upload', { actingAgentId, url: data?.url });
|
|
620
|
+
return { status: 200, body: data };
|
|
621
|
+
} catch (err) {
|
|
622
|
+
return { status: err?.status || 500, body: { error: err?.message || 'avatar upload failed' } };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export async function handleAttachmentView(req, query, ctx) {
|
|
627
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
628
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
629
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
630
|
+
const assetId = query?.asset_id;
|
|
631
|
+
if (!assetId) return { status: 400, body: { error: 'asset_id is required' } };
|
|
632
|
+
try {
|
|
633
|
+
const data = await api.viewAgentAttachment({ actingAgentId, assetId });
|
|
634
|
+
return { status: 200, body: data };
|
|
635
|
+
} catch (err) {
|
|
636
|
+
return { status: err?.status || 500, body: { error: err?.message || 'attachment view failed' } };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export async function handleGroupCreate(req, body, ctx) {
|
|
641
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
642
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
643
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
644
|
+
const name = String(body?.name || '').trim();
|
|
645
|
+
if (!name) return { status: 400, body: { error: 'name is required' } };
|
|
646
|
+
try {
|
|
647
|
+
const data = await api.createAgentGroup({
|
|
648
|
+
actingAgentId,
|
|
649
|
+
name,
|
|
650
|
+
description: body?.description || null,
|
|
651
|
+
memberAgentIds: Array.isArray(body?.member_agent_ids) ? body.member_agent_ids : [],
|
|
652
|
+
});
|
|
653
|
+
invalidateServerInfoCache(actingAgentId);
|
|
654
|
+
debugLog('agent-cli', 'group.create', {
|
|
655
|
+
actingAgentId,
|
|
656
|
+
conversationId: data?.conversation?.id,
|
|
657
|
+
members: data?.member_agent_ids,
|
|
658
|
+
});
|
|
659
|
+
return { status: 200, body: data };
|
|
660
|
+
} catch (err) {
|
|
661
|
+
return { status: err?.status || 500, body: { error: err?.message || 'group create failed' } };
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export async function handleGroupMembersAdd(req, body, ctx) {
|
|
666
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
667
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
668
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
669
|
+
let conversationId = body?.conversation_id || null;
|
|
670
|
+
if (!conversationId && body?.target) {
|
|
671
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
672
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
673
|
+
conversationId = resolved.conversationId;
|
|
674
|
+
}
|
|
675
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
676
|
+
const agentIds = Array.isArray(body?.agent_ids) ? body.agent_ids : [];
|
|
677
|
+
if (agentIds.length === 0) return { status: 400, body: { error: 'agent_ids must be non-empty' } };
|
|
678
|
+
try {
|
|
679
|
+
const data = await api.addAgentGroupMembers({
|
|
680
|
+
actingAgentId, conversationId, agentIds,
|
|
681
|
+
});
|
|
682
|
+
invalidateServerInfoCache(actingAgentId);
|
|
683
|
+
debugLog('agent-cli', 'group.members.add', {
|
|
684
|
+
actingAgentId, conversationId, agentIds,
|
|
685
|
+
});
|
|
686
|
+
return { status: 200, body: data };
|
|
687
|
+
} catch (err) {
|
|
688
|
+
return { status: err?.status || 500, body: { error: err?.message || 'add failed' } };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export async function handleGroupMembersRemove(req, body, ctx) {
|
|
693
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
694
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
695
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
696
|
+
let conversationId = body?.conversation_id || null;
|
|
697
|
+
if (!conversationId && body?.target) {
|
|
698
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
699
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
700
|
+
conversationId = resolved.conversationId;
|
|
701
|
+
}
|
|
702
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
703
|
+
const agentId = body?.agent_id;
|
|
704
|
+
if (!agentId) return { status: 400, body: { error: 'agent_id is required' } };
|
|
705
|
+
try {
|
|
706
|
+
const data = await api.removeAgentGroupMember({
|
|
707
|
+
actingAgentId, conversationId, agentId,
|
|
708
|
+
});
|
|
709
|
+
invalidateServerInfoCache(actingAgentId);
|
|
710
|
+
debugLog('agent-cli', 'group.members.remove', {
|
|
711
|
+
actingAgentId, conversationId, agentId,
|
|
712
|
+
});
|
|
713
|
+
return { status: 200, body: data };
|
|
714
|
+
} catch (err) {
|
|
715
|
+
return { status: err?.status || 500, body: { error: err?.message || 'remove failed' } };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
279
719
|
export async function handleServerInfo(req, query, ctx) {
|
|
280
720
|
const actingAgentId = getActingAgentId(req, query);
|
|
281
721
|
const v = validateActingAgent(actingAgentId, ctx);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent home directory: ~/.ticlawk/agents/<agent_id>/
|
|
3
|
+
*
|
|
4
|
+
* The agent's authoritative workspace. The daemon spawns every runtime
|
|
5
|
+
* with this as cwd; the agent's MEMORY.md, notes/, and any artifacts
|
|
6
|
+
* it produces live here. No project binding — one MEMORY.md per agent,
|
|
7
|
+
* lives in cwd, agent reads it via `cat MEMORY.md`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { AF_HOME } from './config.mjs';
|
|
13
|
+
|
|
14
|
+
export const AF_AGENTS_DIR = join(AF_HOME, 'agents');
|
|
15
|
+
|
|
16
|
+
export function getAgentHome(agentId) {
|
|
17
|
+
if (!agentId) throw new Error('getAgentHome: agentId is required');
|
|
18
|
+
return join(AF_AGENTS_DIR, String(agentId));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getAgentMemoryPath(agentId) {
|
|
22
|
+
return join(getAgentHome(agentId), 'MEMORY.md');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Make sure the agent home dir exists + has a starter MEMORY.md. Idempotent;
|
|
27
|
+
* safe to call on every spawn. The daemon does this before driver.spawn()
|
|
28
|
+
* so cwd is always a real, writable directory.
|
|
29
|
+
*
|
|
30
|
+
* Pass `displayName` only when seeding for the first time — existing
|
|
31
|
+
* MEMORY.md is never overwritten (the agent owns it).
|
|
32
|
+
*/
|
|
33
|
+
export function ensureAgentHome(agentId, { displayName } = {}) {
|
|
34
|
+
const home = getAgentHome(agentId);
|
|
35
|
+
mkdirSync(home, { recursive: true });
|
|
36
|
+
const memoryPath = getAgentMemoryPath(agentId);
|
|
37
|
+
if (!existsSync(memoryPath)) {
|
|
38
|
+
writeFileSync(memoryPath, buildInitialMemoryMd({ displayName, home }), 'utf8');
|
|
39
|
+
}
|
|
40
|
+
return home;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildInitialMemoryMd({ displayName, home }) {
|
|
44
|
+
const lines = [
|
|
45
|
+
`# ${displayName || 'Agent'}`,
|
|
46
|
+
'',
|
|
47
|
+
'## Role',
|
|
48
|
+
'<your role definition, evolved over time>',
|
|
49
|
+
'',
|
|
50
|
+
'## Workspace',
|
|
51
|
+
home,
|
|
52
|
+
'',
|
|
53
|
+
];
|
|
54
|
+
lines.push(
|
|
55
|
+
'## Key Knowledge',
|
|
56
|
+
'- (none yet — populate as you learn the user, project, and domain)',
|
|
57
|
+
'',
|
|
58
|
+
'## Active Context',
|
|
59
|
+
'- (none)',
|
|
60
|
+
'',
|
|
61
|
+
'## How to update this file',
|
|
62
|
+
'MEMORY.md is your entry point. Read it at the top of every turn. Add a',
|
|
63
|
+
'`notes/<topic>.md` for each long-lived knowledge area and link it here.',
|
|
64
|
+
'',
|
|
65
|
+
);
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Best-effort read of the "## Workspace" line from a MEMORY.md. Useful
|
|
71
|
+
* if some code wants to know where the agent currently believes it
|
|
72
|
+
* works (e.g. for UI display). The daemon does NOT use this for spawn
|
|
73
|
+
* cwd — spawn cwd is always getAgentHome(id).
|
|
74
|
+
*/
|
|
75
|
+
export function readWorkspaceFromMemory(agentId) {
|
|
76
|
+
const memoryPath = getAgentMemoryPath(agentId);
|
|
77
|
+
if (!existsSync(memoryPath)) return null;
|
|
78
|
+
try {
|
|
79
|
+
const text = readFileSync(memoryPath, 'utf8');
|
|
80
|
+
const m = text.match(/^##\s+Workspace\s*\n([^\n]+)/m);
|
|
81
|
+
return m ? m[1].trim() : null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/core/argv.mjs
CHANGED
|
@@ -29,7 +29,17 @@ export function parseOptionArgs(argv = []) {
|
|
|
29
29
|
const value = inlineValue !== undefined
|
|
30
30
|
? inlineValue
|
|
31
31
|
: argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[++i] : true;
|
|
32
|
-
|
|
32
|
+
// Repeated flags collect into an array so `--attach a --attach b`
|
|
33
|
+
// surfaces as ['a','b'] to callers that expect repeatable input
|
|
34
|
+
// (--attach on message send, --member on group create, etc).
|
|
35
|
+
// First occurrence stays scalar so single-value callers don't
|
|
36
|
+
// have to learn array-or-string.
|
|
37
|
+
if (Object.prototype.hasOwnProperty.call(args, rawKey)) {
|
|
38
|
+
const existing = args[rawKey];
|
|
39
|
+
args[rawKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
40
|
+
} else {
|
|
41
|
+
args[rawKey] = value;
|
|
42
|
+
}
|
|
33
43
|
continue;
|
|
34
44
|
}
|
|
35
45
|
args._.push(arg);
|