ticlawk 0.1.15 → 0.1.16-dev.2

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.
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Daemon-side handlers for the agent CLI surface.
3
+ *
4
+ * The CLI subcommands (`ticlawk message send`, `ticlawk task claim`, ...)
5
+ * post to the local daemon at 127.0.0.1:8741. The daemon validates the
6
+ * caller's TICLAWK_RUNTIME_AGENT_ID against the active binding store
7
+ * and forwards to the ticlawk backend using the connector API key.
8
+ *
9
+ * Targets are parsed in the daemon (not on the wire to backend) so the
10
+ * CLI can speak `#<group>` / `dm:@<user>` / `#<group>:<short-msg-id>`
11
+ * while backend keeps a flat conversation_id contract.
12
+ */
13
+
14
+ import { debugError, debugLog } from './logger.mjs';
15
+ import * as api from '../adapters/ticlawk/api.mjs';
16
+
17
+ const SERVER_INFO_CACHE_MS = 30 * 1000;
18
+ const serverInfoCacheByAgent = new Map(); // agentId -> { ts, info }
19
+
20
+ async function getCachedServerInfo(actingAgentId) {
21
+ const now = Date.now();
22
+ const cached = serverInfoCacheByAgent.get(actingAgentId);
23
+ if (cached && now - cached.ts < SERVER_INFO_CACHE_MS) {
24
+ return cached.info;
25
+ }
26
+ const info = await api.getAgentServerInfo({ actingAgentId });
27
+ serverInfoCacheByAgent.set(actingAgentId, { ts: now, info });
28
+ return info;
29
+ }
30
+
31
+ export function invalidateServerInfoCache(actingAgentId = null) {
32
+ if (actingAgentId) serverInfoCacheByAgent.delete(actingAgentId);
33
+ else serverInfoCacheByAgent.clear();
34
+ }
35
+
36
+ /**
37
+ * Parse a target string into { conversationId, threadRootMsgId } using a
38
+ * cached server-info lookup. Returns null fields if the target cannot be
39
+ * resolved; callers should treat that as a 404.
40
+ *
41
+ * Target syntax:
42
+ * dm:<uuid> -> conversation_id = <uuid>
43
+ * dm:@<handle> -> find DM conversation whose other member is <handle>
44
+ * #<uuid> -> conversation_id = <uuid>
45
+ * #<group-name> -> find group conversation by name
46
+ * <foo>:<short-msg-id> -> thread under <foo>, root = first message whose
47
+ * id startsWith <short-msg-id>
48
+ */
49
+ export async function resolveTarget(actingAgentId, target) {
50
+ if (!target) return { conversationId: null, threadRootMsgId: null, error: 'target is required' };
51
+
52
+ // Strip optional thread suffix.
53
+ let base = target;
54
+ let threadShort = null;
55
+ const colonIdx = target.indexOf(':', target.startsWith('dm:') ? 3 : 1);
56
+ if (colonIdx > 0 && colonIdx < target.length - 1) {
57
+ threadShort = target.slice(colonIdx + 1);
58
+ base = target.slice(0, colonIdx);
59
+ }
60
+
61
+ if (/^[0-9a-f-]{36}$/i.test(base)) {
62
+ return { conversationId: base, threadRootMsgId: threadShort, error: null };
63
+ }
64
+ if (base.startsWith('dm:') && /^[0-9a-f-]{36}$/i.test(base.slice(3))) {
65
+ return { conversationId: base.slice(3), threadRootMsgId: threadShort, error: null };
66
+ }
67
+ if (base.startsWith('#') && /^[0-9a-f-]{36}$/i.test(base.slice(1))) {
68
+ return { conversationId: base.slice(1), threadRootMsgId: threadShort, error: null };
69
+ }
70
+
71
+ const info = await getCachedServerInfo(actingAgentId);
72
+ const convs = Array.isArray(info?.conversations) ? info.conversations : [];
73
+
74
+ if (base.startsWith('dm:@')) {
75
+ const handle = base.slice(4).toLowerCase();
76
+ const match = convs.find((c) =>
77
+ c.type === 'dm' && (String(c.display_name || c.name || '').toLowerCase() === handle)
78
+ );
79
+ if (match) return { conversationId: match.id, threadRootMsgId: threadShort, error: null };
80
+ return { conversationId: null, threadRootMsgId: null, error: `unknown dm target: ${target}` };
81
+ }
82
+ if (base.startsWith('#')) {
83
+ const name = base.slice(1).toLowerCase();
84
+ const match = convs.find((c) =>
85
+ c.type === 'group' && (String(c.name || c.display_name || '').toLowerCase() === name)
86
+ );
87
+ if (match) return { conversationId: match.id, threadRootMsgId: threadShort, error: null };
88
+ return { conversationId: null, threadRootMsgId: null, error: `unknown group target: ${target}` };
89
+ }
90
+
91
+ return { conversationId: null, threadRootMsgId: null, error: `invalid target syntax: ${target}` };
92
+ }
93
+
94
+ function getActingAgentId(req, body = {}) {
95
+ const fromHeader = req.headers['x-ticlawk-acting-agent-id'];
96
+ if (typeof fromHeader === 'string' && fromHeader.trim()) return fromHeader.trim();
97
+ if (typeof body?.acting_as_agent_id === 'string' && body.acting_as_agent_id.trim()) {
98
+ return body.acting_as_agent_id.trim();
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function getRuntimeHostId(req, body = {}) {
104
+ const fromHeader = req.headers['x-ticlawk-runtime-host-id'];
105
+ if (typeof fromHeader === 'string' && fromHeader.trim()) return fromHeader.trim();
106
+ if (typeof body?.runtime_host_id === 'string' && body.runtime_host_id.trim()) {
107
+ return body.runtime_host_id.trim();
108
+ }
109
+ return null;
110
+ }
111
+
112
+ function validateActingAgent(actingAgentId, ctx) {
113
+ if (!actingAgentId) {
114
+ return { ok: false, status: 400, error: 'TICLAWK_RUNTIME_AGENT_ID required (passed via X-Ticlawk-Acting-Agent-Id or body.acting_as_agent_id)' };
115
+ }
116
+ if (typeof ctx.listBindings === 'function') {
117
+ const local = ctx.listBindings({ adapter: 'ticlawk' }) || [];
118
+ const match = local.find((b) => b?.id === actingAgentId);
119
+ if (!match) {
120
+ return { ok: false, status: 401, error: `agent ${actingAgentId} is not bound on this host` };
121
+ }
122
+ }
123
+ return { ok: true };
124
+ }
125
+
126
+ export async function handleMessageSend(req, body, ctx) {
127
+ const actingAgentId = getActingAgentId(req, body);
128
+ const v = validateActingAgent(actingAgentId, ctx);
129
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
130
+
131
+ const text = String(body?.text || '').trim();
132
+ if (!text) return { status: 400, body: { error: 'text is required' } };
133
+
134
+ let conversationId = body?.conversation_id || null;
135
+ let threadRootMsgId = null;
136
+ if (!conversationId && body?.target) {
137
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
138
+ if (resolved.error) {
139
+ return { status: 404, body: { error: resolved.error } };
140
+ }
141
+ conversationId = resolved.conversationId;
142
+ threadRootMsgId = resolved.threadRootMsgId;
143
+ }
144
+ if (!conversationId) {
145
+ return { status: 400, body: { error: 'target or conversation_id is required' } };
146
+ }
147
+
148
+ try {
149
+ const data = await api.sendAgentMessage({
150
+ actingAgentId,
151
+ conversationId,
152
+ text,
153
+ seenUpToSeq: body?.seen_up_to_seq,
154
+ replyToMessageId: body?.reply_to_message_id || threadRootMsgId || null,
155
+ runtimeHostId: getRuntimeHostId(req, body),
156
+ });
157
+ debugLog('agent-cli', 'send.ok', {
158
+ actingAgentId,
159
+ conversationId,
160
+ messageId: data?.id,
161
+ seq: data?.seq,
162
+ bodyChars: text.length,
163
+ });
164
+ return { status: 200, body: { ok: true, data } };
165
+ } catch (err) {
166
+ debugError('agent-cli', 'send.failed', {
167
+ actingAgentId,
168
+ conversationId,
169
+ error: err?.message || String(err),
170
+ status: err?.status || null,
171
+ });
172
+ return { status: err?.status || 500, body: { error: err?.message || 'send failed', payload: err?.payload || null } };
173
+ }
174
+ }
175
+
176
+ export async function handleMessageRead(req, query, ctx) {
177
+ const actingAgentId = getActingAgentId(req, query);
178
+ const v = validateActingAgent(actingAgentId, ctx);
179
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
180
+
181
+ let conversationId = query?.conversation_id || null;
182
+ if (!conversationId && query?.target) {
183
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
184
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
185
+ conversationId = resolved.conversationId;
186
+ }
187
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
188
+
189
+ try {
190
+ const data = await api.readAgentMessages({
191
+ actingAgentId,
192
+ conversationId,
193
+ aroundMessageId: query?.around_message_id || null,
194
+ beforeSeq: query?.before_seq != null ? Number(query.before_seq) : null,
195
+ limit: query?.limit != null ? Number(query.limit) : null,
196
+ });
197
+ return { status: 200, body: { data } };
198
+ } catch (err) {
199
+ return { status: err?.status || 500, body: { error: err?.message || 'read failed' } };
200
+ }
201
+ }
202
+
203
+ export async function handleTaskCreate(req, body, ctx) {
204
+ const actingAgentId = getActingAgentId(req, body);
205
+ const v = validateActingAgent(actingAgentId, ctx);
206
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
207
+
208
+ const text = String(body?.text || '').trim();
209
+ if (!text) return { status: 400, body: { error: 'text is required' } };
210
+
211
+ let conversationId = body?.conversation_id || null;
212
+ if (!conversationId && body?.target) {
213
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
214
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
215
+ conversationId = resolved.conversationId;
216
+ }
217
+ if (!conversationId) {
218
+ return { status: 400, body: { error: 'target or conversation_id is required' } };
219
+ }
220
+
221
+ try {
222
+ const data = await api.createAgentTask({
223
+ actingAgentId,
224
+ conversationId,
225
+ text,
226
+ title: body?.title ?? null,
227
+ });
228
+ debugLog('agent-cli', 'task.create', {
229
+ actingAgentId,
230
+ conversationId,
231
+ messageId: data?.message?.id,
232
+ taskNumber: data?.task?.number,
233
+ });
234
+ return { status: 200, body: data };
235
+ } catch (err) {
236
+ debugError('agent-cli', 'task.create.failed', {
237
+ actingAgentId,
238
+ conversationId,
239
+ error: err?.message || String(err),
240
+ });
241
+ return { status: err?.status || 500, body: { error: err?.message || 'task create failed' } };
242
+ }
243
+ }
244
+
245
+ export async function handleTaskClaim(req, body, ctx) {
246
+ const actingAgentId = getActingAgentId(req, body);
247
+ const v = validateActingAgent(actingAgentId, ctx);
248
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
249
+ const sourceMessageId = body?.source_message_id || body?.message_id || null;
250
+ const number = body?.number != null ? Number(body.number) : null;
251
+
252
+ // Number-based claim needs the conversation_id to disambiguate. Allow
253
+ // the caller to pass either conversation_id directly or a target.
254
+ let conversationId = body?.conversation_id || null;
255
+ if (number != null && !conversationId && body?.target) {
256
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
257
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
258
+ conversationId = resolved.conversationId;
259
+ }
260
+
261
+ if (!sourceMessageId && number == null) {
262
+ return { status: 400, body: { error: '--message-id or --number is required' } };
263
+ }
264
+ if (number != null && !conversationId) {
265
+ return { status: 400, body: { error: '--target or --conversation-id is required when claiming by number' } };
266
+ }
267
+
268
+ try {
269
+ const data = await api.claimAgentTask({
270
+ actingAgentId,
271
+ sourceMessageId,
272
+ conversationId,
273
+ number,
274
+ leaseSeconds: body?.lease_seconds,
275
+ });
276
+ debugLog('agent-cli', 'task.claim', {
277
+ actingAgentId,
278
+ sourceMessageId: sourceMessageId || null,
279
+ number: number ?? null,
280
+ conversationId: conversationId || null,
281
+ claimed: data?.claimed,
282
+ taskNumber: data?.task?.number || null,
283
+ });
284
+ return { status: data?.claimed === false ? 409 : 200, body: data };
285
+ } catch (err) {
286
+ return { status: err?.status || 500, body: { error: err?.message || 'claim failed' } };
287
+ }
288
+ }
289
+
290
+ export async function handleTaskUnclaim(req, body, ctx) {
291
+ const actingAgentId = getActingAgentId(req, body);
292
+ const v = validateActingAgent(actingAgentId, ctx);
293
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
294
+ if (!body?.task_id) return { status: 400, body: { error: 'task_id is required' } };
295
+ try {
296
+ const data = await api.unclaimAgentTask({
297
+ actingAgentId,
298
+ taskId: body.task_id,
299
+ });
300
+ debugLog('agent-cli', 'task.unclaim', {
301
+ actingAgentId,
302
+ taskId: body.task_id,
303
+ ok: data?.ok,
304
+ });
305
+ return { status: data?.ok ? 200 : 409, body: data };
306
+ } catch (err) {
307
+ return { status: err?.status || 500, body: { error: err?.message || 'unclaim failed' } };
308
+ }
309
+ }
310
+
311
+ export async function handleTaskUpdate(req, body, ctx) {
312
+ const actingAgentId = getActingAgentId(req, body);
313
+ const v = validateActingAgent(actingAgentId, ctx);
314
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
315
+ if (!body?.task_id) return { status: 400, body: { error: 'task_id is required' } };
316
+ try {
317
+ const data = await api.updateAgentTask({
318
+ actingAgentId,
319
+ taskId: body.task_id,
320
+ status: body.status || null,
321
+ });
322
+ return { status: 200, body: data };
323
+ } catch (err) {
324
+ return { status: err?.status || 500, body: { error: err?.message || 'update failed' } };
325
+ }
326
+ }
327
+
328
+ export async function handleTaskList(req, query, ctx) {
329
+ const actingAgentId = getActingAgentId(req, query);
330
+ const v = validateActingAgent(actingAgentId, ctx);
331
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
332
+ let conversationId = query?.conversation_id || null;
333
+ if (!conversationId && query?.target) {
334
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
335
+ if (!resolved.error) conversationId = resolved.conversationId;
336
+ }
337
+ try {
338
+ const data = await api.listAgentTasks({ actingAgentId, conversationId });
339
+ return { status: 200, body: { data } };
340
+ } catch (err) {
341
+ return { status: err?.status || 500, body: { error: err?.message || 'list failed' } };
342
+ }
343
+ }
344
+
345
+ export async function handleGroupMembers(req, query, ctx) {
346
+ const actingAgentId = getActingAgentId(req, query);
347
+ const v = validateActingAgent(actingAgentId, ctx);
348
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
349
+ let conversationId = query?.conversation_id || null;
350
+ if (!conversationId && query?.target) {
351
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
352
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
353
+ conversationId = resolved.conversationId;
354
+ }
355
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
356
+ try {
357
+ const res = await api.getConversationMembers({ actingAgentId, conversationId });
358
+ return { status: 200, body: res };
359
+ } catch (err) {
360
+ return { status: err?.status || 500, body: { error: err?.message || 'members lookup failed' } };
361
+ }
362
+ }
363
+
364
+ export async function handleMessageReact(req, body, ctx) {
365
+ const actingAgentId = getActingAgentId(req, body);
366
+ const v = validateActingAgent(actingAgentId, ctx);
367
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
368
+ const messageId = body?.message_id;
369
+ const emoji = body?.emoji;
370
+ const remove = !!body?.remove;
371
+ if (!messageId) return { status: 400, body: { error: 'message_id is required' } };
372
+ if (!emoji) return { status: 400, body: { error: 'emoji is required' } };
373
+ try {
374
+ const data = await api.reactToMessage({
375
+ actingAgentId, messageId, emoji, remove,
376
+ });
377
+ debugLog('agent-cli', remove ? 'message.unreact' : 'message.react', {
378
+ actingAgentId, messageId, emoji,
379
+ });
380
+ return { status: 200, body: data };
381
+ } catch (err) {
382
+ return { status: err?.status || 500, body: { error: err?.message || 'react failed' } };
383
+ }
384
+ }
385
+
386
+ export async function handleReminderSchedule(req, body, ctx) {
387
+ const actingAgentId = getActingAgentId(req, body);
388
+ const v = validateActingAgent(actingAgentId, ctx);
389
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
390
+ if (!body?.title) return { status: 400, body: { error: 'title is required' } };
391
+ if (!body?.fire_at) return { status: 400, body: { error: 'fire_at (ISO timestamp) is required' } };
392
+
393
+ let anchorConversationId = body?.anchor_conversation_id || null;
394
+ if (!anchorConversationId && body?.target) {
395
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
396
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
397
+ anchorConversationId = resolved.conversationId;
398
+ }
399
+ if (!anchorConversationId) {
400
+ return { status: 400, body: { error: '--target or --anchor-conversation-id required' } };
401
+ }
402
+
403
+ try {
404
+ const data = await api.scheduleAgentReminder({
405
+ actingAgentId,
406
+ title: body.title,
407
+ fireAt: body.fire_at,
408
+ anchorConversationId,
409
+ anchorMessageId: body?.anchor_message_id || null,
410
+ });
411
+ debugLog('agent-cli', 'reminder.schedule', {
412
+ actingAgentId,
413
+ reminderId: data?.data?.id,
414
+ fireAt: body.fire_at,
415
+ });
416
+ return { status: 200, body: data };
417
+ } catch (err) {
418
+ return { status: err?.status || 500, body: { error: err?.message || 'schedule failed' } };
419
+ }
420
+ }
421
+
422
+ export async function handleReminderList(req, query, ctx) {
423
+ const actingAgentId = getActingAgentId(req, query);
424
+ const v = validateActingAgent(actingAgentId, ctx);
425
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
426
+ try {
427
+ const data = await api.listAgentReminders({
428
+ actingAgentId,
429
+ status: query?.status || null,
430
+ });
431
+ return { status: 200, body: { data } };
432
+ } catch (err) {
433
+ return { status: err?.status || 500, body: { error: err?.message || 'list failed' } };
434
+ }
435
+ }
436
+
437
+ export async function handleReminderSnooze(req, body, ctx) {
438
+ const actingAgentId = getActingAgentId(req, body);
439
+ const v = validateActingAgent(actingAgentId, ctx);
440
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
441
+ if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
442
+ if (!body?.fire_at) return { status: 400, body: { error: 'fire_at is required' } };
443
+ try {
444
+ const data = await api.snoozeAgentReminder({
445
+ actingAgentId,
446
+ reminderId: body.reminder_id,
447
+ fireAt: body.fire_at,
448
+ });
449
+ return { status: 200, body: data };
450
+ } catch (err) {
451
+ return { status: err?.status || 500, body: { error: err?.message || 'snooze failed' } };
452
+ }
453
+ }
454
+
455
+ export async function handleReminderUpdate(req, body, ctx) {
456
+ const actingAgentId = getActingAgentId(req, body);
457
+ const v = validateActingAgent(actingAgentId, ctx);
458
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
459
+ if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
460
+ try {
461
+ const data = await api.updateAgentReminder({
462
+ actingAgentId,
463
+ reminderId: body.reminder_id,
464
+ title: body?.title || null,
465
+ fireAt: body?.fire_at || null,
466
+ });
467
+ return { status: 200, body: data };
468
+ } catch (err) {
469
+ return { status: err?.status || 500, body: { error: err?.message || 'update failed' } };
470
+ }
471
+ }
472
+
473
+ export async function handleReminderCancel(req, body, ctx) {
474
+ const actingAgentId = getActingAgentId(req, body);
475
+ const v = validateActingAgent(actingAgentId, ctx);
476
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
477
+ if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
478
+ try {
479
+ const data = await api.cancelAgentReminder({
480
+ actingAgentId,
481
+ reminderId: body.reminder_id,
482
+ });
483
+ return { status: 200, body: data };
484
+ } catch (err) {
485
+ return { status: err?.status || 500, body: { error: err?.message || 'cancel failed' } };
486
+ }
487
+ }
488
+
489
+ export async function handleReminderLog(req, query, ctx) {
490
+ const actingAgentId = getActingAgentId(req, query);
491
+ const v = validateActingAgent(actingAgentId, ctx);
492
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
493
+ if (!query?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
494
+ try {
495
+ const data = await api.getAgentReminderLog({
496
+ actingAgentId,
497
+ reminderId: query.reminder_id,
498
+ });
499
+ return { status: 200, body: { data } };
500
+ } catch (err) {
501
+ return { status: err?.status || 500, body: { error: err?.message || 'log failed' } };
502
+ }
503
+ }
504
+
505
+ export async function handleMessageCheck(req, query, ctx) {
506
+ const actingAgentId = getActingAgentId(req, query);
507
+ const v = validateActingAgent(actingAgentId, ctx);
508
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
509
+ let conversationId = query?.conversation_id || null;
510
+ if (!conversationId && query?.target) {
511
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
512
+ if (!resolved.error) conversationId = resolved.conversationId;
513
+ }
514
+ try {
515
+ const data = await api.checkAgentMessages({ actingAgentId, conversationId });
516
+ return { status: 200, body: data };
517
+ } catch (err) {
518
+ return { status: err?.status || 500, body: { error: err?.message || 'check failed' } };
519
+ }
520
+ }
521
+
522
+ export async function handleMessageSearch(req, query, ctx) {
523
+ const actingAgentId = getActingAgentId(req, query);
524
+ const v = validateActingAgent(actingAgentId, ctx);
525
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
526
+ if (!query?.q) return { status: 400, body: { error: 'q is required' } };
527
+ let conversationId = query?.conversation_id || null;
528
+ if (!conversationId && query?.target) {
529
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
530
+ if (!resolved.error) conversationId = resolved.conversationId;
531
+ }
532
+ try {
533
+ const data = await api.searchAgentMessages({
534
+ actingAgentId,
535
+ query: query.q,
536
+ conversationId,
537
+ limit: query?.limit != null ? Number(query.limit) : null,
538
+ });
539
+ return { status: 200, body: { data } };
540
+ } catch (err) {
541
+ return { status: err?.status || 500, body: { error: err?.message || 'search failed' } };
542
+ }
543
+ }
544
+
545
+ export async function handleProfileShow(req, query, ctx) {
546
+ const actingAgentId = getActingAgentId(req, query);
547
+ const v = validateActingAgent(actingAgentId, ctx);
548
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
549
+ const idOrHandle = query?.id || query?.handle || actingAgentId;
550
+ try {
551
+ const data = await api.getAgentProfile({ actingAgentId, idOrHandle });
552
+ return { status: 200, body: data };
553
+ } catch (err) {
554
+ return { status: err?.status || 500, body: { error: err?.message || 'profile show failed' } };
555
+ }
556
+ }
557
+
558
+ export async function handleProfileUpdate(req, body, ctx) {
559
+ const actingAgentId = getActingAgentId(req, body);
560
+ const v = validateActingAgent(actingAgentId, ctx);
561
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
562
+ try {
563
+ const data = await api.patchAgentProfile({
564
+ actingAgentId,
565
+ displayName: body?.display_name ?? null,
566
+ description: body?.description ?? null,
567
+ avatarUrl: body?.avatar_url ?? null,
568
+ });
569
+ debugLog('agent-cli', 'profile.update', { actingAgentId, keys: Object.keys(body || {}) });
570
+ return { status: 200, body: data };
571
+ } catch (err) {
572
+ return { status: err?.status || 500, body: { error: err?.message || 'profile update failed' } };
573
+ }
574
+ }
575
+
576
+ export async function handleAttachmentUpload(req, body, ctx) {
577
+ const actingAgentId = getActingAgentId(req, body);
578
+ const v = validateActingAgent(actingAgentId, ctx);
579
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
580
+ if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
581
+ try {
582
+ const data = await api.uploadAgentAttachment({
583
+ actingAgentId,
584
+ filename: body?.filename || 'attachment.bin',
585
+ contentType: body?.content_type || 'application/octet-stream',
586
+ dataBase64: body.data_base64,
587
+ });
588
+ debugLog('agent-cli', 'attachment.upload', {
589
+ actingAgentId,
590
+ asset_id: data?.asset?.asset_id,
591
+ bytes: data?.asset?.size_bytes,
592
+ });
593
+ return { status: 200, body: data };
594
+ } catch (err) {
595
+ return { status: err?.status || 500, body: { error: err?.message || 'attachment upload failed' } };
596
+ }
597
+ }
598
+
599
+ export async function handleAttachmentView(req, query, ctx) {
600
+ const actingAgentId = getActingAgentId(req, query);
601
+ const v = validateActingAgent(actingAgentId, ctx);
602
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
603
+ const assetId = query?.asset_id;
604
+ if (!assetId) return { status: 400, body: { error: 'asset_id is required' } };
605
+ try {
606
+ const data = await api.viewAgentAttachment({ actingAgentId, assetId });
607
+ return { status: 200, body: data };
608
+ } catch (err) {
609
+ return { status: err?.status || 500, body: { error: err?.message || 'attachment view failed' } };
610
+ }
611
+ }
612
+
613
+ export async function handleGroupCreate(req, body, ctx) {
614
+ const actingAgentId = getActingAgentId(req, body);
615
+ const v = validateActingAgent(actingAgentId, ctx);
616
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
617
+ const name = String(body?.name || '').trim();
618
+ if (!name) return { status: 400, body: { error: 'name is required' } };
619
+ try {
620
+ const data = await api.createAgentGroup({
621
+ actingAgentId,
622
+ name,
623
+ description: body?.description || null,
624
+ memberAgentIds: Array.isArray(body?.member_agent_ids) ? body.member_agent_ids : [],
625
+ });
626
+ invalidateServerInfoCache(actingAgentId);
627
+ debugLog('agent-cli', 'group.create', {
628
+ actingAgentId,
629
+ conversationId: data?.conversation?.id,
630
+ members: data?.member_agent_ids,
631
+ });
632
+ return { status: 200, body: data };
633
+ } catch (err) {
634
+ return { status: err?.status || 500, body: { error: err?.message || 'group create failed' } };
635
+ }
636
+ }
637
+
638
+ export async function handleGroupMembersAdd(req, body, ctx) {
639
+ const actingAgentId = getActingAgentId(req, body);
640
+ const v = validateActingAgent(actingAgentId, ctx);
641
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
642
+ let conversationId = body?.conversation_id || null;
643
+ if (!conversationId && body?.target) {
644
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
645
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
646
+ conversationId = resolved.conversationId;
647
+ }
648
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
649
+ const agentIds = Array.isArray(body?.agent_ids) ? body.agent_ids : [];
650
+ if (agentIds.length === 0) return { status: 400, body: { error: 'agent_ids must be non-empty' } };
651
+ try {
652
+ const data = await api.addAgentGroupMembers({
653
+ actingAgentId, conversationId, agentIds,
654
+ });
655
+ invalidateServerInfoCache(actingAgentId);
656
+ debugLog('agent-cli', 'group.members.add', {
657
+ actingAgentId, conversationId, agentIds,
658
+ });
659
+ return { status: 200, body: data };
660
+ } catch (err) {
661
+ return { status: err?.status || 500, body: { error: err?.message || 'add failed' } };
662
+ }
663
+ }
664
+
665
+ export async function handleGroupMembersRemove(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 agentId = body?.agent_id;
677
+ if (!agentId) return { status: 400, body: { error: 'agent_id is required' } };
678
+ try {
679
+ const data = await api.removeAgentGroupMember({
680
+ actingAgentId, conversationId, agentId,
681
+ });
682
+ invalidateServerInfoCache(actingAgentId);
683
+ debugLog('agent-cli', 'group.members.remove', {
684
+ actingAgentId, conversationId, agentId,
685
+ });
686
+ return { status: 200, body: data };
687
+ } catch (err) {
688
+ return { status: err?.status || 500, body: { error: err?.message || 'remove failed' } };
689
+ }
690
+ }
691
+
692
+ export async function handleServerInfo(req, query, ctx) {
693
+ const actingAgentId = getActingAgentId(req, query);
694
+ const v = validateActingAgent(actingAgentId, ctx);
695
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
696
+ // Bust cache when CLI explicitly asks
697
+ if (query?.refresh === '1') invalidateServerInfoCache(actingAgentId);
698
+ try {
699
+ const info = await getCachedServerInfo(actingAgentId);
700
+ return { status: 200, body: info };
701
+ } catch (err) {
702
+ return { status: err?.status || 500, body: { error: err?.message || 'server-info failed' } };
703
+ }
704
+ }