@arinova-ai/agent-sdk 0.0.15 → 0.0.16-staging.1
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 +276 -0
- package/dist/client.d.ts +177 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +698 -10
- package/dist/client.js.map +1 -1
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/client.test.js +91 -0
- package/dist/client.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +166 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const DEFAULT_RECONNECT_INTERVAL = 5_000;
|
|
2
2
|
const DEFAULT_PING_INTERVAL = 30_000;
|
|
3
3
|
const TASK_HEARTBEAT_INTERVAL = 60_000;
|
|
4
|
+
const MAX_QUEUE_SIZE = 10;
|
|
4
5
|
export class ArinovaAgent {
|
|
5
6
|
serverUrl;
|
|
6
7
|
botToken;
|
|
@@ -9,10 +10,14 @@ export class ArinovaAgent {
|
|
|
9
10
|
pingInterval;
|
|
10
11
|
ws = null;
|
|
11
12
|
pingTimer = null;
|
|
13
|
+
commandHeartbeatTimer = null;
|
|
12
14
|
reconnectTimer = null;
|
|
13
15
|
stopped = false;
|
|
16
|
+
agentId = null;
|
|
14
17
|
taskHandler = null;
|
|
15
18
|
taskAbortControllers = new Map();
|
|
19
|
+
activeConversationTasks = new Map(); // conversationId → taskId
|
|
20
|
+
conversationQueues = new Map(); // conversationId → queued task data
|
|
16
21
|
listeners = {
|
|
17
22
|
connected: [],
|
|
18
23
|
disconnected: [],
|
|
@@ -69,7 +74,7 @@ export class ArinovaAgent {
|
|
|
69
74
|
const httpUrl = this.serverUrl
|
|
70
75
|
.replace(/^ws:/, "http:")
|
|
71
76
|
.replace(/^wss:/, "https:");
|
|
72
|
-
const res = await fetch(`${httpUrl}/api/
|
|
77
|
+
const res = await fetch(`${httpUrl}/api/v1/messages/send`, {
|
|
73
78
|
method: "POST",
|
|
74
79
|
headers: {
|
|
75
80
|
"Authorization": `Bearer ${this.botToken}`,
|
|
@@ -82,6 +87,20 @@ export class ArinovaAgent {
|
|
|
82
87
|
throw new Error(`sendMessage failed (${res.status}): ${body}`);
|
|
83
88
|
}
|
|
84
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Send a telemetry event to the server.
|
|
92
|
+
* Silently no-ops if WebSocket is not connected.
|
|
93
|
+
*/
|
|
94
|
+
sendTelemetry(event, data) {
|
|
95
|
+
this.send({ type: "agent_telemetry", event, data });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Send HUD data to the server for display in the office HUD bar.
|
|
99
|
+
* The server forwards this to the agent owner's frontend.
|
|
100
|
+
*/
|
|
101
|
+
sendHud(data) {
|
|
102
|
+
this.send({ type: "hud_update", data });
|
|
103
|
+
}
|
|
85
104
|
emit(event, ...args) {
|
|
86
105
|
for (const listener of this.listeners[event] ?? []) {
|
|
87
106
|
listener(...args);
|
|
@@ -97,6 +116,10 @@ export class ArinovaAgent {
|
|
|
97
116
|
clearInterval(this.pingTimer);
|
|
98
117
|
this.pingTimer = null;
|
|
99
118
|
}
|
|
119
|
+
if (this.commandHeartbeatTimer) {
|
|
120
|
+
clearInterval(this.commandHeartbeatTimer);
|
|
121
|
+
this.commandHeartbeatTimer = null;
|
|
122
|
+
}
|
|
100
123
|
if (this.reconnectTimer) {
|
|
101
124
|
clearTimeout(this.reconnectTimer);
|
|
102
125
|
this.reconnectTimer = null;
|
|
@@ -108,6 +131,14 @@ export class ArinovaAgent {
|
|
|
108
131
|
catch { }
|
|
109
132
|
this.ws = null;
|
|
110
133
|
}
|
|
134
|
+
// Clear queues BEFORE aborting — abort triggers markFinished → processNextTask,
|
|
135
|
+
// which would dequeue and start tasks during disconnect if queues aren't empty.
|
|
136
|
+
this.conversationQueues.clear();
|
|
137
|
+
this.activeConversationTasks.clear();
|
|
138
|
+
for (const controller of this.taskAbortControllers.values()) {
|
|
139
|
+
controller.abort();
|
|
140
|
+
}
|
|
141
|
+
this.taskAbortControllers.clear();
|
|
111
142
|
}
|
|
112
143
|
scheduleReconnect() {
|
|
113
144
|
if (this.stopped)
|
|
@@ -145,7 +176,27 @@ export class ArinovaAgent {
|
|
|
145
176
|
try {
|
|
146
177
|
const data = JSON.parse(String(event.data));
|
|
147
178
|
if (data.type === "auth_ok") {
|
|
179
|
+
this.agentId = data.agentId ?? null;
|
|
148
180
|
this.emit("connected");
|
|
181
|
+
// Register SDK runtime commands from skills
|
|
182
|
+
if (this.skills.length > 0 && this.agentId) {
|
|
183
|
+
this.send({
|
|
184
|
+
type: "register_commands",
|
|
185
|
+
agentId: this.agentId,
|
|
186
|
+
commands: this.skills.map((s) => ({
|
|
187
|
+
name: s.id ?? s.name,
|
|
188
|
+
description: s.description ?? "",
|
|
189
|
+
})),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// Start heartbeat to extend Redis TTL every 60s
|
|
193
|
+
if (this.commandHeartbeatTimer)
|
|
194
|
+
clearInterval(this.commandHeartbeatTimer);
|
|
195
|
+
if (this.skills.length > 0 && this.agentId) {
|
|
196
|
+
this.commandHeartbeatTimer = setInterval(() => {
|
|
197
|
+
this.send({ type: "heartbeat_commands", agentId: this.agentId });
|
|
198
|
+
}, 60_000);
|
|
199
|
+
}
|
|
149
200
|
// Resolve the connect() promise on first successful auth
|
|
150
201
|
if (this.connectResolve) {
|
|
151
202
|
this.connectResolve();
|
|
@@ -177,6 +228,17 @@ export class ArinovaAgent {
|
|
|
177
228
|
}
|
|
178
229
|
if (data.type === "cancel_task") {
|
|
179
230
|
const taskId = data.taskId;
|
|
231
|
+
// Check if the task is still queued (not yet started)
|
|
232
|
+
for (const [convId, queue] of this.conversationQueues) {
|
|
233
|
+
const idx = queue.findIndex((t) => t.taskId === taskId);
|
|
234
|
+
if (idx !== -1) {
|
|
235
|
+
queue.splice(idx, 1);
|
|
236
|
+
if (queue.length === 0)
|
|
237
|
+
this.conversationQueues.delete(convId);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Active task — abort it (processNextTask will be called via markFinished)
|
|
180
242
|
const controller = this.taskAbortControllers.get(taskId);
|
|
181
243
|
if (controller) {
|
|
182
244
|
controller.abort();
|
|
@@ -215,7 +277,7 @@ export class ArinovaAgent {
|
|
|
215
277
|
formData.append("conversationId", conversationId);
|
|
216
278
|
const blob = new Blob([new Uint8Array(file)], { type: mime });
|
|
217
279
|
formData.append("file", blob, fileName);
|
|
218
|
-
const res = await fetch(`${httpUrl}/api/
|
|
280
|
+
const res = await fetch(`${httpUrl}/api/v1/files/upload`, {
|
|
219
281
|
method: "POST",
|
|
220
282
|
headers: {
|
|
221
283
|
Authorization: `Bearer ${this.botToken}`,
|
|
@@ -247,7 +309,7 @@ export class ArinovaAgent {
|
|
|
247
309
|
if (options?.limit != null)
|
|
248
310
|
params.set("limit", String(options.limit));
|
|
249
311
|
const qs = params.toString();
|
|
250
|
-
const url = `${httpUrl}/api/
|
|
312
|
+
const url = `${httpUrl}/api/v1/messages/${conversationId}${qs ? `?${qs}` : ""}`;
|
|
251
313
|
const res = await fetch(url, {
|
|
252
314
|
method: "GET",
|
|
253
315
|
headers: {
|
|
@@ -274,8 +336,12 @@ export class ArinovaAgent {
|
|
|
274
336
|
params.set("before", options.before);
|
|
275
337
|
if (options?.limit != null)
|
|
276
338
|
params.set("limit", String(options.limit));
|
|
339
|
+
if (options?.tags?.length)
|
|
340
|
+
params.set("tags", options.tags.join(","));
|
|
341
|
+
if (options?.archived)
|
|
342
|
+
params.set("archived", "true");
|
|
277
343
|
const qs = params.toString();
|
|
278
|
-
const url = `${httpUrl}/api/
|
|
344
|
+
const url = `${httpUrl}/api/v1/notes${qs ? `?${qs}` : ""}`;
|
|
279
345
|
const res = await fetch(url, {
|
|
280
346
|
method: "GET",
|
|
281
347
|
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
@@ -295,7 +361,7 @@ export class ArinovaAgent {
|
|
|
295
361
|
const httpUrl = this.serverUrl
|
|
296
362
|
.replace(/^ws:/, "http:")
|
|
297
363
|
.replace(/^wss:/, "https:");
|
|
298
|
-
const res = await fetch(`${httpUrl}/api/
|
|
364
|
+
const res = await fetch(`${httpUrl}/api/v1/notes`, {
|
|
299
365
|
method: "POST",
|
|
300
366
|
headers: {
|
|
301
367
|
Authorization: `Bearer ${this.botToken}`,
|
|
@@ -319,7 +385,7 @@ export class ArinovaAgent {
|
|
|
319
385
|
const httpUrl = this.serverUrl
|
|
320
386
|
.replace(/^ws:/, "http:")
|
|
321
387
|
.replace(/^wss:/, "https:");
|
|
322
|
-
const res = await fetch(`${httpUrl}/api/
|
|
388
|
+
const res = await fetch(`${httpUrl}/api/v1/notes/${noteId}`, {
|
|
323
389
|
method: "PATCH",
|
|
324
390
|
headers: {
|
|
325
391
|
Authorization: `Bearer ${this.botToken}`,
|
|
@@ -342,7 +408,7 @@ export class ArinovaAgent {
|
|
|
342
408
|
const httpUrl = this.serverUrl
|
|
343
409
|
.replace(/^ws:/, "http:")
|
|
344
410
|
.replace(/^wss:/, "https:");
|
|
345
|
-
const res = await fetch(`${httpUrl}/api/
|
|
411
|
+
const res = await fetch(`${httpUrl}/api/v1/notes/${noteId}`, {
|
|
346
412
|
method: "DELETE",
|
|
347
413
|
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
348
414
|
});
|
|
@@ -351,12 +417,621 @@ export class ArinovaAgent {
|
|
|
351
417
|
throw new Error(`deleteNote failed (${res.status}): ${text}`);
|
|
352
418
|
}
|
|
353
419
|
}
|
|
420
|
+
// ── Kanban API ────────────────────────────────────────────────
|
|
421
|
+
/**
|
|
422
|
+
* List the owner's kanban boards.
|
|
423
|
+
* Returns an array of boards with id, name, and createdAt.
|
|
424
|
+
*/
|
|
425
|
+
async listBoards() {
|
|
426
|
+
const httpUrl = this.serverUrl
|
|
427
|
+
.replace(/^ws:/, "http:")
|
|
428
|
+
.replace(/^wss:/, "https:");
|
|
429
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards`, {
|
|
430
|
+
method: "GET",
|
|
431
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
432
|
+
});
|
|
433
|
+
if (!res.ok) {
|
|
434
|
+
const body = await res.text();
|
|
435
|
+
throw new Error(`listBoards failed (${res.status}): ${body}`);
|
|
436
|
+
}
|
|
437
|
+
return res.json();
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Create a kanban card on the owner's board.
|
|
441
|
+
* The card is automatically assigned to the calling agent.
|
|
442
|
+
* @param body - Card title and optional description, priority, column.
|
|
443
|
+
*/
|
|
444
|
+
async createCard(body) {
|
|
445
|
+
const httpUrl = this.serverUrl
|
|
446
|
+
.replace(/^ws:/, "http:")
|
|
447
|
+
.replace(/^wss:/, "https:");
|
|
448
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards`, {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: {
|
|
451
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
452
|
+
"Content-Type": "application/json",
|
|
453
|
+
},
|
|
454
|
+
body: JSON.stringify(body),
|
|
455
|
+
});
|
|
456
|
+
if (!res.ok) {
|
|
457
|
+
const text = await res.text();
|
|
458
|
+
throw new Error(`createCard failed (${res.status}): ${text}`);
|
|
459
|
+
}
|
|
460
|
+
return res.json();
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Update a kanban card.
|
|
464
|
+
* @param cardId - The card ID to update.
|
|
465
|
+
* @param body - Fields to update (title, description, priority, columnId, sortOrder).
|
|
466
|
+
*/
|
|
467
|
+
async updateCard(cardId, body) {
|
|
468
|
+
const httpUrl = this.serverUrl
|
|
469
|
+
.replace(/^ws:/, "http:")
|
|
470
|
+
.replace(/^wss:/, "https:");
|
|
471
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}`, {
|
|
472
|
+
method: "PATCH",
|
|
473
|
+
headers: {
|
|
474
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
475
|
+
"Content-Type": "application/json",
|
|
476
|
+
},
|
|
477
|
+
body: JSON.stringify(body),
|
|
478
|
+
});
|
|
479
|
+
if (!res.ok) {
|
|
480
|
+
const text = await res.text();
|
|
481
|
+
throw new Error(`updateCard failed (${res.status}): ${text}`);
|
|
482
|
+
}
|
|
483
|
+
return res.json();
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Create a new kanban board.
|
|
487
|
+
* @param body - Board name and optional initial columns.
|
|
488
|
+
*/
|
|
489
|
+
async createBoard(body) {
|
|
490
|
+
const httpUrl = this.serverUrl
|
|
491
|
+
.replace(/^ws:/, "http:")
|
|
492
|
+
.replace(/^wss:/, "https:");
|
|
493
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards`, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers: {
|
|
496
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
497
|
+
"Content-Type": "application/json",
|
|
498
|
+
},
|
|
499
|
+
body: JSON.stringify(body),
|
|
500
|
+
});
|
|
501
|
+
if (!res.ok) {
|
|
502
|
+
const text = await res.text();
|
|
503
|
+
throw new Error(`createBoard failed (${res.status}): ${text}`);
|
|
504
|
+
}
|
|
505
|
+
return res.json();
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Update a kanban board.
|
|
509
|
+
* @param boardId - The board ID to update.
|
|
510
|
+
* @param body - Fields to update.
|
|
511
|
+
*/
|
|
512
|
+
async updateBoard(boardId, body) {
|
|
513
|
+
const httpUrl = this.serverUrl
|
|
514
|
+
.replace(/^ws:/, "http:")
|
|
515
|
+
.replace(/^wss:/, "https:");
|
|
516
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}`, {
|
|
517
|
+
method: "PATCH",
|
|
518
|
+
headers: {
|
|
519
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
520
|
+
"Content-Type": "application/json",
|
|
521
|
+
},
|
|
522
|
+
body: JSON.stringify(body),
|
|
523
|
+
});
|
|
524
|
+
if (!res.ok) {
|
|
525
|
+
const text = await res.text();
|
|
526
|
+
throw new Error(`updateBoard failed (${res.status}): ${text}`);
|
|
527
|
+
}
|
|
528
|
+
return res.json();
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Archive a kanban board.
|
|
532
|
+
* @param boardId - The board ID to archive.
|
|
533
|
+
*/
|
|
534
|
+
async archiveBoard(boardId) {
|
|
535
|
+
const httpUrl = this.serverUrl
|
|
536
|
+
.replace(/^ws:/, "http:")
|
|
537
|
+
.replace(/^wss:/, "https:");
|
|
538
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/archive`, {
|
|
539
|
+
method: "POST",
|
|
540
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
541
|
+
});
|
|
542
|
+
if (!res.ok) {
|
|
543
|
+
const text = await res.text();
|
|
544
|
+
throw new Error(`archiveBoard failed (${res.status}): ${text}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* List columns for a board.
|
|
549
|
+
* @param boardId - The board ID.
|
|
550
|
+
*/
|
|
551
|
+
async listColumns(boardId) {
|
|
552
|
+
const httpUrl = this.serverUrl
|
|
553
|
+
.replace(/^ws:/, "http:")
|
|
554
|
+
.replace(/^wss:/, "https:");
|
|
555
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/columns`, {
|
|
556
|
+
method: "GET",
|
|
557
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
558
|
+
});
|
|
559
|
+
if (!res.ok) {
|
|
560
|
+
const text = await res.text();
|
|
561
|
+
throw new Error(`listColumns failed (${res.status}): ${text}`);
|
|
562
|
+
}
|
|
563
|
+
return res.json();
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Create a column in a board.
|
|
567
|
+
* @param boardId - The board ID.
|
|
568
|
+
* @param body - Column name and optional sort order.
|
|
569
|
+
*/
|
|
570
|
+
async createColumn(boardId, body) {
|
|
571
|
+
const httpUrl = this.serverUrl
|
|
572
|
+
.replace(/^ws:/, "http:")
|
|
573
|
+
.replace(/^wss:/, "https:");
|
|
574
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/columns`, {
|
|
575
|
+
method: "POST",
|
|
576
|
+
headers: {
|
|
577
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
578
|
+
"Content-Type": "application/json",
|
|
579
|
+
},
|
|
580
|
+
body: JSON.stringify(body),
|
|
581
|
+
});
|
|
582
|
+
if (!res.ok) {
|
|
583
|
+
const text = await res.text();
|
|
584
|
+
throw new Error(`createColumn failed (${res.status}): ${text}`);
|
|
585
|
+
}
|
|
586
|
+
return res.json();
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Update a column.
|
|
590
|
+
* @param columnId - The column ID to update.
|
|
591
|
+
* @param body - Fields to update (name, sortOrder).
|
|
592
|
+
*/
|
|
593
|
+
async updateColumn(columnId, body) {
|
|
594
|
+
const httpUrl = this.serverUrl
|
|
595
|
+
.replace(/^ws:/, "http:")
|
|
596
|
+
.replace(/^wss:/, "https:");
|
|
597
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/columns/${columnId}`, {
|
|
598
|
+
method: "PATCH",
|
|
599
|
+
headers: {
|
|
600
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
601
|
+
"Content-Type": "application/json",
|
|
602
|
+
},
|
|
603
|
+
body: JSON.stringify(body),
|
|
604
|
+
});
|
|
605
|
+
if (!res.ok) {
|
|
606
|
+
const text = await res.text();
|
|
607
|
+
throw new Error(`updateColumn failed (${res.status}): ${text}`);
|
|
608
|
+
}
|
|
609
|
+
return res.json();
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Delete a column.
|
|
613
|
+
* @param columnId - The column ID to delete.
|
|
614
|
+
*/
|
|
615
|
+
async deleteColumn(columnId) {
|
|
616
|
+
const httpUrl = this.serverUrl
|
|
617
|
+
.replace(/^ws:/, "http:")
|
|
618
|
+
.replace(/^wss:/, "https:");
|
|
619
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/columns/${columnId}`, {
|
|
620
|
+
method: "DELETE",
|
|
621
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
622
|
+
});
|
|
623
|
+
if (!res.ok) {
|
|
624
|
+
const text = await res.text();
|
|
625
|
+
throw new Error(`deleteColumn failed (${res.status}): ${text}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Reorder columns in a board.
|
|
630
|
+
* @param boardId - The board ID.
|
|
631
|
+
* @param columnIds - Ordered array of column IDs.
|
|
632
|
+
*/
|
|
633
|
+
async reorderColumns(boardId, columnIds) {
|
|
634
|
+
const httpUrl = this.serverUrl
|
|
635
|
+
.replace(/^ws:/, "http:")
|
|
636
|
+
.replace(/^wss:/, "https:");
|
|
637
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/columns/reorder`, {
|
|
638
|
+
method: "POST",
|
|
639
|
+
headers: {
|
|
640
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
641
|
+
"Content-Type": "application/json",
|
|
642
|
+
},
|
|
643
|
+
body: JSON.stringify({ columnIds }),
|
|
644
|
+
});
|
|
645
|
+
if (!res.ok) {
|
|
646
|
+
const text = await res.text();
|
|
647
|
+
throw new Error(`reorderColumns failed (${res.status}): ${text}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* List all kanban cards for the agent's owner.
|
|
652
|
+
*/
|
|
653
|
+
async listCards() {
|
|
654
|
+
const httpUrl = this.serverUrl
|
|
655
|
+
.replace(/^ws:/, "http:")
|
|
656
|
+
.replace(/^wss:/, "https:");
|
|
657
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards`, {
|
|
658
|
+
method: "GET",
|
|
659
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
660
|
+
});
|
|
661
|
+
if (!res.ok) {
|
|
662
|
+
const text = await res.text();
|
|
663
|
+
throw new Error(`listCards failed (${res.status}): ${text}`);
|
|
664
|
+
}
|
|
665
|
+
return res.json();
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Mark a card as complete (moves it to the Done column).
|
|
669
|
+
* @param cardId - The card ID to complete.
|
|
670
|
+
*/
|
|
671
|
+
async completeCard(cardId) {
|
|
672
|
+
const httpUrl = this.serverUrl
|
|
673
|
+
.replace(/^ws:/, "http:")
|
|
674
|
+
.replace(/^wss:/, "https:");
|
|
675
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/complete`, {
|
|
676
|
+
method: "POST",
|
|
677
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
678
|
+
});
|
|
679
|
+
if (!res.ok) {
|
|
680
|
+
const text = await res.text();
|
|
681
|
+
throw new Error(`completeCard failed (${res.status}): ${text}`);
|
|
682
|
+
}
|
|
683
|
+
return res.json();
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* List archived cards for a board.
|
|
687
|
+
* @param boardId - The board ID.
|
|
688
|
+
* @param options - Pagination options (page, limit).
|
|
689
|
+
*/
|
|
690
|
+
async listArchivedCards(boardId, options) {
|
|
691
|
+
const httpUrl = this.serverUrl
|
|
692
|
+
.replace(/^ws:/, "http:")
|
|
693
|
+
.replace(/^wss:/, "https:");
|
|
694
|
+
const params = new URLSearchParams();
|
|
695
|
+
if (options?.page != null)
|
|
696
|
+
params.set("page", String(options.page));
|
|
697
|
+
if (options?.limit != null)
|
|
698
|
+
params.set("limit", String(options.limit));
|
|
699
|
+
const qs = params.toString();
|
|
700
|
+
const url = `${httpUrl}/api/v1/kanban/boards/${boardId}/archived-cards${qs ? `?${qs}` : ""}`;
|
|
701
|
+
const res = await fetch(url, {
|
|
702
|
+
method: "GET",
|
|
703
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
704
|
+
});
|
|
705
|
+
if (!res.ok) {
|
|
706
|
+
const text = await res.text();
|
|
707
|
+
throw new Error(`listArchivedCards failed (${res.status}): ${text}`);
|
|
708
|
+
}
|
|
709
|
+
return res.json();
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Add a commit link to a card.
|
|
713
|
+
* @param cardId - The card ID.
|
|
714
|
+
* @param body - Commit hash and optional message.
|
|
715
|
+
*/
|
|
716
|
+
async addCardCommit(cardId, body) {
|
|
717
|
+
const httpUrl = this.serverUrl
|
|
718
|
+
.replace(/^ws:/, "http:")
|
|
719
|
+
.replace(/^wss:/, "https:");
|
|
720
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/commits`, {
|
|
721
|
+
method: "POST",
|
|
722
|
+
headers: {
|
|
723
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
724
|
+
"Content-Type": "application/json",
|
|
725
|
+
},
|
|
726
|
+
body: JSON.stringify(body),
|
|
727
|
+
});
|
|
728
|
+
if (!res.ok) {
|
|
729
|
+
const text = await res.text();
|
|
730
|
+
throw new Error(`addCardCommit failed (${res.status}): ${text}`);
|
|
731
|
+
}
|
|
732
|
+
return res.json();
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* List commits linked to a card.
|
|
736
|
+
* @param cardId - The card ID.
|
|
737
|
+
*/
|
|
738
|
+
async listCardCommits(cardId) {
|
|
739
|
+
const httpUrl = this.serverUrl
|
|
740
|
+
.replace(/^ws:/, "http:")
|
|
741
|
+
.replace(/^wss:/, "https:");
|
|
742
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/commits`, {
|
|
743
|
+
method: "GET",
|
|
744
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
745
|
+
});
|
|
746
|
+
if (!res.ok) {
|
|
747
|
+
const text = await res.text();
|
|
748
|
+
throw new Error(`listCardCommits failed (${res.status}): ${text}`);
|
|
749
|
+
}
|
|
750
|
+
return res.json();
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Link a note to a card.
|
|
754
|
+
* @param cardId - The card ID.
|
|
755
|
+
* @param noteId - The note ID to link.
|
|
756
|
+
*/
|
|
757
|
+
async linkCardNote(cardId, noteId) {
|
|
758
|
+
const httpUrl = this.serverUrl
|
|
759
|
+
.replace(/^ws:/, "http:")
|
|
760
|
+
.replace(/^wss:/, "https:");
|
|
761
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/notes`, {
|
|
762
|
+
method: "POST",
|
|
763
|
+
headers: {
|
|
764
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
765
|
+
"Content-Type": "application/json",
|
|
766
|
+
},
|
|
767
|
+
body: JSON.stringify({ noteId }),
|
|
768
|
+
});
|
|
769
|
+
if (!res.ok) {
|
|
770
|
+
const text = await res.text();
|
|
771
|
+
throw new Error(`linkCardNote failed (${res.status}): ${text}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Unlink a note from a card.
|
|
776
|
+
* @param cardId - The card ID.
|
|
777
|
+
* @param noteId - The note ID to unlink.
|
|
778
|
+
*/
|
|
779
|
+
async unlinkCardNote(cardId, noteId) {
|
|
780
|
+
const httpUrl = this.serverUrl
|
|
781
|
+
.replace(/^ws:/, "http:")
|
|
782
|
+
.replace(/^wss:/, "https:");
|
|
783
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/notes/${noteId}`, {
|
|
784
|
+
method: "DELETE",
|
|
785
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
786
|
+
});
|
|
787
|
+
if (!res.ok) {
|
|
788
|
+
const text = await res.text();
|
|
789
|
+
throw new Error(`unlinkCardNote failed (${res.status}): ${text}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* List notes linked to a card.
|
|
794
|
+
* @param cardId - The card ID.
|
|
795
|
+
*/
|
|
796
|
+
async listCardNotes(cardId) {
|
|
797
|
+
const httpUrl = this.serverUrl
|
|
798
|
+
.replace(/^ws:/, "http:")
|
|
799
|
+
.replace(/^wss:/, "https:");
|
|
800
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/notes`, {
|
|
801
|
+
method: "GET",
|
|
802
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
803
|
+
});
|
|
804
|
+
if (!res.ok) {
|
|
805
|
+
const text = await res.text();
|
|
806
|
+
throw new Error(`listCardNotes failed (${res.status}): ${text}`);
|
|
807
|
+
}
|
|
808
|
+
return res.json();
|
|
809
|
+
}
|
|
810
|
+
// ── Label API ────────────────────────────────────────────────
|
|
811
|
+
/**
|
|
812
|
+
* List labels for a board.
|
|
813
|
+
* @param boardId - The board ID.
|
|
814
|
+
*/
|
|
815
|
+
async listLabels(boardId) {
|
|
816
|
+
const httpUrl = this.serverUrl
|
|
817
|
+
.replace(/^ws:/, "http:")
|
|
818
|
+
.replace(/^wss:/, "https:");
|
|
819
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/labels`, {
|
|
820
|
+
method: "GET",
|
|
821
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
822
|
+
});
|
|
823
|
+
if (!res.ok) {
|
|
824
|
+
const text = await res.text();
|
|
825
|
+
throw new Error(`listLabels failed (${res.status}): ${text}`);
|
|
826
|
+
}
|
|
827
|
+
return res.json();
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Create a label on a board.
|
|
831
|
+
* @param boardId - The board ID.
|
|
832
|
+
* @param body - Label name and optional color.
|
|
833
|
+
*/
|
|
834
|
+
async createLabel(boardId, body) {
|
|
835
|
+
const httpUrl = this.serverUrl
|
|
836
|
+
.replace(/^ws:/, "http:")
|
|
837
|
+
.replace(/^wss:/, "https:");
|
|
838
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/labels`, {
|
|
839
|
+
method: "POST",
|
|
840
|
+
headers: {
|
|
841
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
842
|
+
"Content-Type": "application/json",
|
|
843
|
+
},
|
|
844
|
+
body: JSON.stringify(body),
|
|
845
|
+
});
|
|
846
|
+
if (!res.ok) {
|
|
847
|
+
const text = await res.text();
|
|
848
|
+
throw new Error(`createLabel failed (${res.status}): ${text}`);
|
|
849
|
+
}
|
|
850
|
+
return res.json();
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Update a label.
|
|
854
|
+
* @param labelId - The label ID to update.
|
|
855
|
+
* @param body - Fields to update (name, color).
|
|
856
|
+
*/
|
|
857
|
+
async updateLabel(labelId, body) {
|
|
858
|
+
const httpUrl = this.serverUrl
|
|
859
|
+
.replace(/^ws:/, "http:")
|
|
860
|
+
.replace(/^wss:/, "https:");
|
|
861
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/labels/${labelId}`, {
|
|
862
|
+
method: "PATCH",
|
|
863
|
+
headers: {
|
|
864
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
865
|
+
"Content-Type": "application/json",
|
|
866
|
+
},
|
|
867
|
+
body: JSON.stringify(body),
|
|
868
|
+
});
|
|
869
|
+
if (!res.ok) {
|
|
870
|
+
const text = await res.text();
|
|
871
|
+
throw new Error(`updateLabel failed (${res.status}): ${text}`);
|
|
872
|
+
}
|
|
873
|
+
return res.json();
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Delete a label.
|
|
877
|
+
* @param labelId - The label ID to delete.
|
|
878
|
+
*/
|
|
879
|
+
async deleteLabel(labelId) {
|
|
880
|
+
const httpUrl = this.serverUrl
|
|
881
|
+
.replace(/^ws:/, "http:")
|
|
882
|
+
.replace(/^wss:/, "https:");
|
|
883
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/labels/${labelId}`, {
|
|
884
|
+
method: "DELETE",
|
|
885
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
886
|
+
});
|
|
887
|
+
if (!res.ok) {
|
|
888
|
+
const text = await res.text();
|
|
889
|
+
throw new Error(`deleteLabel failed (${res.status}): ${text}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Add a label to a card.
|
|
894
|
+
* @param cardId - The card ID.
|
|
895
|
+
* @param labelId - The label ID to add.
|
|
896
|
+
*/
|
|
897
|
+
async addCardLabel(cardId, labelId) {
|
|
898
|
+
const httpUrl = this.serverUrl
|
|
899
|
+
.replace(/^ws:/, "http:")
|
|
900
|
+
.replace(/^wss:/, "https:");
|
|
901
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/labels`, {
|
|
902
|
+
method: "POST",
|
|
903
|
+
headers: {
|
|
904
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
905
|
+
"Content-Type": "application/json",
|
|
906
|
+
},
|
|
907
|
+
body: JSON.stringify({ labelId }),
|
|
908
|
+
});
|
|
909
|
+
if (!res.ok) {
|
|
910
|
+
const text = await res.text();
|
|
911
|
+
throw new Error(`addCardLabel failed (${res.status}): ${text}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Remove a label from a card.
|
|
916
|
+
* @param cardId - The card ID.
|
|
917
|
+
* @param labelId - The label ID to remove.
|
|
918
|
+
*/
|
|
919
|
+
async removeCardLabel(cardId, labelId) {
|
|
920
|
+
const httpUrl = this.serverUrl
|
|
921
|
+
.replace(/^ws:/, "http:")
|
|
922
|
+
.replace(/^wss:/, "https:");
|
|
923
|
+
const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/labels/${labelId}`, {
|
|
924
|
+
method: "DELETE",
|
|
925
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
926
|
+
});
|
|
927
|
+
if (!res.ok) {
|
|
928
|
+
const text = await res.text();
|
|
929
|
+
throw new Error(`removeCardLabel failed (${res.status}): ${text}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// ── Memory API ───────────────────────────────────────────────
|
|
933
|
+
/**
|
|
934
|
+
* Search memories across all memory capsules granted to this agent.
|
|
935
|
+
* Uses hybrid search (embedding + text) to find relevant memories.
|
|
936
|
+
* @param options - Query string and optional limit.
|
|
937
|
+
*/
|
|
938
|
+
async queryMemory(options) {
|
|
939
|
+
const httpUrl = this.serverUrl
|
|
940
|
+
.replace(/^ws:/, "http:")
|
|
941
|
+
.replace(/^wss:/, "https:");
|
|
942
|
+
const params = new URLSearchParams();
|
|
943
|
+
params.set("query", options.query);
|
|
944
|
+
if (options.limit != null)
|
|
945
|
+
params.set("limit", String(options.limit));
|
|
946
|
+
const res = await fetch(`${httpUrl}/api/v1/capsules?${params}`, {
|
|
947
|
+
method: "GET",
|
|
948
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
949
|
+
});
|
|
950
|
+
if (!res.ok) {
|
|
951
|
+
const body = await res.text();
|
|
952
|
+
throw new Error(`queryMemory failed (${res.status}): ${body}`);
|
|
953
|
+
}
|
|
954
|
+
// Server returns snake_case, map to camelCase
|
|
955
|
+
const raw = (await res.json());
|
|
956
|
+
return raw.map((r) => ({
|
|
957
|
+
content: r.content,
|
|
958
|
+
capsuleName: r.capsule_name,
|
|
959
|
+
capsuleId: r.capsule_id,
|
|
960
|
+
score: r.score,
|
|
961
|
+
importance: r.importance,
|
|
962
|
+
}));
|
|
963
|
+
}
|
|
964
|
+
// ── Skill Prompt API ─────────────────────────────────────────
|
|
965
|
+
/**
|
|
966
|
+
* Fetch the full prompt content for an installed skill by slug.
|
|
967
|
+
* Use this when the agent decides to trigger a skill from availableSkills.
|
|
968
|
+
* @param skillSlug - The skill slug (e.g. "draw", "proactive-agent").
|
|
969
|
+
*/
|
|
970
|
+
async fetchSkillPrompt(skillSlug) {
|
|
971
|
+
const httpUrl = this.serverUrl
|
|
972
|
+
.replace(/^ws:/, "http:")
|
|
973
|
+
.replace(/^wss:/, "https:");
|
|
974
|
+
const res = await fetch(`${httpUrl}/api/v1/skills/${encodeURIComponent(skillSlug)}/prompt`, {
|
|
975
|
+
method: "GET",
|
|
976
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
977
|
+
});
|
|
978
|
+
if (!res.ok) {
|
|
979
|
+
const body = await res.text();
|
|
980
|
+
throw new Error(`fetchSkillPrompt failed (${res.status}): ${body}`);
|
|
981
|
+
}
|
|
982
|
+
return (await res.json());
|
|
983
|
+
}
|
|
984
|
+
// ── Note Share API ───────────────────────────────────────────
|
|
985
|
+
/**
|
|
986
|
+
* Share a note as a message in a conversation.
|
|
987
|
+
* Creates a rich preview card visible to all conversation members.
|
|
988
|
+
* @param conversationId - The conversation to share into.
|
|
989
|
+
* @param noteId - The note ID to share.
|
|
990
|
+
*/
|
|
991
|
+
async shareNote(conversationId, noteId) {
|
|
992
|
+
const httpUrl = this.serverUrl
|
|
993
|
+
.replace(/^ws:/, "http:")
|
|
994
|
+
.replace(/^wss:/, "https:");
|
|
995
|
+
const res = await fetch(`${httpUrl}/api/v1/notes/${noteId}/share`, {
|
|
996
|
+
method: "POST",
|
|
997
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
998
|
+
});
|
|
999
|
+
if (!res.ok) {
|
|
1000
|
+
const text = await res.text();
|
|
1001
|
+
throw new Error(`shareNote failed (${res.status}): ${text}`);
|
|
1002
|
+
}
|
|
1003
|
+
return res.json();
|
|
1004
|
+
}
|
|
354
1005
|
handleTask(data) {
|
|
1006
|
+
if (!this.taskHandler)
|
|
1007
|
+
return;
|
|
1008
|
+
const conversationId = data.conversationId;
|
|
1009
|
+
const activeTaskId = this.activeConversationTasks.get(conversationId);
|
|
1010
|
+
// If this conversation already has an active task, queue the new one
|
|
1011
|
+
if (activeTaskId && this.taskAbortControllers.has(activeTaskId)) {
|
|
1012
|
+
let queue = this.conversationQueues.get(conversationId);
|
|
1013
|
+
if (!queue) {
|
|
1014
|
+
queue = [];
|
|
1015
|
+
this.conversationQueues.set(conversationId, queue);
|
|
1016
|
+
}
|
|
1017
|
+
// Overflow: drop oldest queued task when queue is full
|
|
1018
|
+
if (queue.length >= MAX_QUEUE_SIZE) {
|
|
1019
|
+
const dropped = queue.shift();
|
|
1020
|
+
this.send({ type: "agent_error", taskId: dropped.taskId, error: "queue_overflow" });
|
|
1021
|
+
}
|
|
1022
|
+
queue.push(data);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
this.executeTask(data);
|
|
1026
|
+
}
|
|
1027
|
+
executeTask(data) {
|
|
355
1028
|
if (!this.taskHandler)
|
|
356
1029
|
return;
|
|
357
1030
|
const taskId = data.taskId;
|
|
1031
|
+
const conversationId = data.conversationId;
|
|
358
1032
|
const abortController = new AbortController();
|
|
359
1033
|
this.taskAbortControllers.set(taskId, abortController);
|
|
1034
|
+
this.activeConversationTasks.set(conversationId, taskId);
|
|
360
1035
|
// Auto heartbeat: keep task alive while processing
|
|
361
1036
|
const heartbeatTimer = setInterval(() => {
|
|
362
1037
|
this.send({ type: "agent_heartbeat", taskId });
|
|
@@ -372,11 +1047,13 @@ export class ArinovaAgent {
|
|
|
372
1047
|
taskFinished = true;
|
|
373
1048
|
stopHeartbeat();
|
|
374
1049
|
this.taskAbortControllers.delete(taskId);
|
|
1050
|
+
this.activeConversationTasks.delete(conversationId);
|
|
1051
|
+
this.processNextTask(conversationId);
|
|
375
1052
|
return true;
|
|
376
1053
|
};
|
|
377
1054
|
const ctx = {
|
|
378
1055
|
taskId,
|
|
379
|
-
conversationId
|
|
1056
|
+
conversationId,
|
|
380
1057
|
content: data.content,
|
|
381
1058
|
conversationType: data.conversationType,
|
|
382
1059
|
senderUserId: data.senderUserId,
|
|
@@ -406,8 +1083,8 @@ export class ArinovaAgent {
|
|
|
406
1083
|
this.send({ type: "agent_error", taskId, error });
|
|
407
1084
|
},
|
|
408
1085
|
signal: abortController.signal,
|
|
409
|
-
uploadFile: (file, fileName, fileType) => this.uploadFile(
|
|
410
|
-
fetchHistory: (options) => this.fetchHistory(
|
|
1086
|
+
uploadFile: (file, fileName, fileType) => this.uploadFile(conversationId, file, fileName, fileType),
|
|
1087
|
+
fetchHistory: (options) => this.fetchHistory(conversationId, options),
|
|
411
1088
|
};
|
|
412
1089
|
// When task is aborted (user cancelled), immediately send cancellation
|
|
413
1090
|
// error so the server knows this agent is free for new tasks.
|
|
@@ -421,6 +1098,17 @@ export class ArinovaAgent {
|
|
|
421
1098
|
ctx.sendError(errorMsg);
|
|
422
1099
|
});
|
|
423
1100
|
}
|
|
1101
|
+
processNextTask(conversationId) {
|
|
1102
|
+
const queue = this.conversationQueues.get(conversationId);
|
|
1103
|
+
if (!queue || queue.length === 0) {
|
|
1104
|
+
this.conversationQueues.delete(conversationId);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const nextTask = queue.shift();
|
|
1108
|
+
if (queue.length === 0)
|
|
1109
|
+
this.conversationQueues.delete(conversationId);
|
|
1110
|
+
this.executeTask(nextTask);
|
|
1111
|
+
}
|
|
424
1112
|
}
|
|
425
1113
|
const MIME_TYPES = {
|
|
426
1114
|
jpg: "image/jpeg",
|