@arinova-ai/agent-sdk 0.0.13 → 0.0.15-staging.0

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/dist/client.js CHANGED
@@ -82,6 +82,20 @@ export class ArinovaAgent {
82
82
  throw new Error(`sendMessage failed (${res.status}): ${body}`);
83
83
  }
84
84
  }
85
+ /**
86
+ * Send a telemetry event to the server.
87
+ * Silently no-ops if WebSocket is not connected.
88
+ */
89
+ sendTelemetry(event, data) {
90
+ this.send({ type: "agent_telemetry", event, data });
91
+ }
92
+ /**
93
+ * Send HUD data to the server for display in the office HUD bar.
94
+ * The server forwards this to the agent owner's frontend.
95
+ */
96
+ sendHud(data) {
97
+ this.send({ type: "hud_update", data });
98
+ }
85
99
  emit(event, ...args) {
86
100
  for (const listener of this.listeners[event] ?? []) {
87
101
  listener(...args);
@@ -228,6 +242,718 @@ export class ArinovaAgent {
228
242
  }
229
243
  return res.json();
230
244
  }
245
+ /**
246
+ * Fetch conversation history via the agent messages endpoint.
247
+ * @param conversationId - The conversation to fetch messages from.
248
+ * @param options - Pagination options (before, after, around, limit).
249
+ */
250
+ async fetchHistory(conversationId, options) {
251
+ const httpUrl = this.serverUrl
252
+ .replace(/^ws:/, "http:")
253
+ .replace(/^wss:/, "https:");
254
+ const params = new URLSearchParams();
255
+ if (options?.before)
256
+ params.set("before", options.before);
257
+ if (options?.after)
258
+ params.set("after", options.after);
259
+ if (options?.around)
260
+ params.set("around", options.around);
261
+ if (options?.limit != null)
262
+ params.set("limit", String(options.limit));
263
+ const qs = params.toString();
264
+ const url = `${httpUrl}/api/agent/messages/${conversationId}${qs ? `?${qs}` : ""}`;
265
+ const res = await fetch(url, {
266
+ method: "GET",
267
+ headers: {
268
+ Authorization: `Bearer ${this.botToken}`,
269
+ },
270
+ });
271
+ if (!res.ok) {
272
+ const body = await res.text();
273
+ throw new Error(`fetchHistory failed (${res.status}): ${body}`);
274
+ }
275
+ return res.json();
276
+ }
277
+ /**
278
+ * List notes in a conversation.
279
+ * @param conversationId - The conversation to list notes from.
280
+ * @param options - Pagination options (before, limit).
281
+ */
282
+ async listNotes(conversationId, options) {
283
+ const httpUrl = this.serverUrl
284
+ .replace(/^ws:/, "http:")
285
+ .replace(/^wss:/, "https:");
286
+ const params = new URLSearchParams();
287
+ if (options?.before)
288
+ params.set("before", options.before);
289
+ if (options?.limit != null)
290
+ params.set("limit", String(options.limit));
291
+ if (options?.tags?.length)
292
+ params.set("tags", options.tags.join(","));
293
+ if (options?.archived)
294
+ params.set("archived", "true");
295
+ const qs = params.toString();
296
+ const url = `${httpUrl}/api/agent/conversations/${conversationId}/notes${qs ? `?${qs}` : ""}`;
297
+ const res = await fetch(url, {
298
+ method: "GET",
299
+ headers: { Authorization: `Bearer ${this.botToken}` },
300
+ });
301
+ if (!res.ok) {
302
+ const body = await res.text();
303
+ throw new Error(`listNotes failed (${res.status}): ${body}`);
304
+ }
305
+ return res.json();
306
+ }
307
+ /**
308
+ * Create a note in a conversation.
309
+ * @param conversationId - The conversation to create the note in.
310
+ * @param body - Note title and optional content.
311
+ */
312
+ async createNote(conversationId, body) {
313
+ const httpUrl = this.serverUrl
314
+ .replace(/^ws:/, "http:")
315
+ .replace(/^wss:/, "https:");
316
+ const res = await fetch(`${httpUrl}/api/agent/conversations/${conversationId}/notes`, {
317
+ method: "POST",
318
+ headers: {
319
+ Authorization: `Bearer ${this.botToken}`,
320
+ "Content-Type": "application/json",
321
+ },
322
+ body: JSON.stringify(body),
323
+ });
324
+ if (!res.ok) {
325
+ const text = await res.text();
326
+ throw new Error(`createNote failed (${res.status}): ${text}`);
327
+ }
328
+ return res.json();
329
+ }
330
+ /**
331
+ * Update a note in a conversation.
332
+ * @param conversationId - The conversation the note belongs to.
333
+ * @param noteId - The note ID to update.
334
+ * @param body - Fields to update (title and/or content).
335
+ */
336
+ async updateNote(conversationId, noteId, body) {
337
+ const httpUrl = this.serverUrl
338
+ .replace(/^ws:/, "http:")
339
+ .replace(/^wss:/, "https:");
340
+ const res = await fetch(`${httpUrl}/api/agent/conversations/${conversationId}/notes/${noteId}`, {
341
+ method: "PATCH",
342
+ headers: {
343
+ Authorization: `Bearer ${this.botToken}`,
344
+ "Content-Type": "application/json",
345
+ },
346
+ body: JSON.stringify(body),
347
+ });
348
+ if (!res.ok) {
349
+ const text = await res.text();
350
+ throw new Error(`updateNote failed (${res.status}): ${text}`);
351
+ }
352
+ return res.json();
353
+ }
354
+ /**
355
+ * Delete a note from a conversation.
356
+ * @param conversationId - The conversation the note belongs to.
357
+ * @param noteId - The note ID to delete.
358
+ */
359
+ async deleteNote(conversationId, noteId) {
360
+ const httpUrl = this.serverUrl
361
+ .replace(/^ws:/, "http:")
362
+ .replace(/^wss:/, "https:");
363
+ const res = await fetch(`${httpUrl}/api/agent/conversations/${conversationId}/notes/${noteId}`, {
364
+ method: "DELETE",
365
+ headers: { Authorization: `Bearer ${this.botToken}` },
366
+ });
367
+ if (!res.ok) {
368
+ const text = await res.text();
369
+ throw new Error(`deleteNote failed (${res.status}): ${text}`);
370
+ }
371
+ }
372
+ // ── Kanban API ────────────────────────────────────────────────
373
+ /**
374
+ * List the owner's kanban boards.
375
+ * Returns an array of boards with id, name, and createdAt.
376
+ */
377
+ async listBoards() {
378
+ const httpUrl = this.serverUrl
379
+ .replace(/^ws:/, "http:")
380
+ .replace(/^wss:/, "https:");
381
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards`, {
382
+ method: "GET",
383
+ headers: { Authorization: `Bearer ${this.botToken}` },
384
+ });
385
+ if (!res.ok) {
386
+ const body = await res.text();
387
+ throw new Error(`listBoards failed (${res.status}): ${body}`);
388
+ }
389
+ return res.json();
390
+ }
391
+ /**
392
+ * Create a kanban card on the owner's board.
393
+ * The card is automatically assigned to the calling agent.
394
+ * @param body - Card title and optional description, priority, column.
395
+ */
396
+ async createCard(body) {
397
+ const httpUrl = this.serverUrl
398
+ .replace(/^ws:/, "http:")
399
+ .replace(/^wss:/, "https:");
400
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards`, {
401
+ method: "POST",
402
+ headers: {
403
+ Authorization: `Bearer ${this.botToken}`,
404
+ "Content-Type": "application/json",
405
+ },
406
+ body: JSON.stringify(body),
407
+ });
408
+ if (!res.ok) {
409
+ const text = await res.text();
410
+ throw new Error(`createCard failed (${res.status}): ${text}`);
411
+ }
412
+ return res.json();
413
+ }
414
+ /**
415
+ * Update a kanban card.
416
+ * @param cardId - The card ID to update.
417
+ * @param body - Fields to update (title, description, priority, columnId, sortOrder).
418
+ */
419
+ async updateCard(cardId, body) {
420
+ const httpUrl = this.serverUrl
421
+ .replace(/^ws:/, "http:")
422
+ .replace(/^wss:/, "https:");
423
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}`, {
424
+ method: "PATCH",
425
+ headers: {
426
+ Authorization: `Bearer ${this.botToken}`,
427
+ "Content-Type": "application/json",
428
+ },
429
+ body: JSON.stringify(body),
430
+ });
431
+ if (!res.ok) {
432
+ const text = await res.text();
433
+ throw new Error(`updateCard failed (${res.status}): ${text}`);
434
+ }
435
+ return res.json();
436
+ }
437
+ /**
438
+ * Create a new kanban board.
439
+ * @param body - Board name and optional initial columns.
440
+ */
441
+ async createBoard(body) {
442
+ const httpUrl = this.serverUrl
443
+ .replace(/^ws:/, "http:")
444
+ .replace(/^wss:/, "https:");
445
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards`, {
446
+ method: "POST",
447
+ headers: {
448
+ Authorization: `Bearer ${this.botToken}`,
449
+ "Content-Type": "application/json",
450
+ },
451
+ body: JSON.stringify(body),
452
+ });
453
+ if (!res.ok) {
454
+ const text = await res.text();
455
+ throw new Error(`createBoard failed (${res.status}): ${text}`);
456
+ }
457
+ return res.json();
458
+ }
459
+ /**
460
+ * Update a kanban board.
461
+ * @param boardId - The board ID to update.
462
+ * @param body - Fields to update.
463
+ */
464
+ async updateBoard(boardId, body) {
465
+ const httpUrl = this.serverUrl
466
+ .replace(/^ws:/, "http:")
467
+ .replace(/^wss:/, "https:");
468
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards/${boardId}`, {
469
+ method: "PATCH",
470
+ headers: {
471
+ Authorization: `Bearer ${this.botToken}`,
472
+ "Content-Type": "application/json",
473
+ },
474
+ body: JSON.stringify(body),
475
+ });
476
+ if (!res.ok) {
477
+ const text = await res.text();
478
+ throw new Error(`updateBoard failed (${res.status}): ${text}`);
479
+ }
480
+ return res.json();
481
+ }
482
+ /**
483
+ * Archive a kanban board.
484
+ * @param boardId - The board ID to archive.
485
+ */
486
+ async archiveBoard(boardId) {
487
+ const httpUrl = this.serverUrl
488
+ .replace(/^ws:/, "http:")
489
+ .replace(/^wss:/, "https:");
490
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards/${boardId}/archive`, {
491
+ method: "POST",
492
+ headers: { Authorization: `Bearer ${this.botToken}` },
493
+ });
494
+ if (!res.ok) {
495
+ const text = await res.text();
496
+ throw new Error(`archiveBoard failed (${res.status}): ${text}`);
497
+ }
498
+ }
499
+ /**
500
+ * List columns for a board.
501
+ * @param boardId - The board ID.
502
+ */
503
+ async listColumns(boardId) {
504
+ const httpUrl = this.serverUrl
505
+ .replace(/^ws:/, "http:")
506
+ .replace(/^wss:/, "https:");
507
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards/${boardId}/columns`, {
508
+ method: "GET",
509
+ headers: { Authorization: `Bearer ${this.botToken}` },
510
+ });
511
+ if (!res.ok) {
512
+ const text = await res.text();
513
+ throw new Error(`listColumns failed (${res.status}): ${text}`);
514
+ }
515
+ return res.json();
516
+ }
517
+ /**
518
+ * Create a column in a board.
519
+ * @param boardId - The board ID.
520
+ * @param body - Column name and optional sort order.
521
+ */
522
+ async createColumn(boardId, body) {
523
+ const httpUrl = this.serverUrl
524
+ .replace(/^ws:/, "http:")
525
+ .replace(/^wss:/, "https:");
526
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards/${boardId}/columns`, {
527
+ method: "POST",
528
+ headers: {
529
+ Authorization: `Bearer ${this.botToken}`,
530
+ "Content-Type": "application/json",
531
+ },
532
+ body: JSON.stringify(body),
533
+ });
534
+ if (!res.ok) {
535
+ const text = await res.text();
536
+ throw new Error(`createColumn failed (${res.status}): ${text}`);
537
+ }
538
+ return res.json();
539
+ }
540
+ /**
541
+ * Update a column.
542
+ * @param columnId - The column ID to update.
543
+ * @param body - Fields to update (name, sortOrder).
544
+ */
545
+ async updateColumn(columnId, body) {
546
+ const httpUrl = this.serverUrl
547
+ .replace(/^ws:/, "http:")
548
+ .replace(/^wss:/, "https:");
549
+ const res = await fetch(`${httpUrl}/api/agent/kanban/columns/${columnId}`, {
550
+ method: "PATCH",
551
+ headers: {
552
+ Authorization: `Bearer ${this.botToken}`,
553
+ "Content-Type": "application/json",
554
+ },
555
+ body: JSON.stringify(body),
556
+ });
557
+ if (!res.ok) {
558
+ const text = await res.text();
559
+ throw new Error(`updateColumn failed (${res.status}): ${text}`);
560
+ }
561
+ return res.json();
562
+ }
563
+ /**
564
+ * Delete a column.
565
+ * @param columnId - The column ID to delete.
566
+ */
567
+ async deleteColumn(columnId) {
568
+ const httpUrl = this.serverUrl
569
+ .replace(/^ws:/, "http:")
570
+ .replace(/^wss:/, "https:");
571
+ const res = await fetch(`${httpUrl}/api/agent/kanban/columns/${columnId}`, {
572
+ method: "DELETE",
573
+ headers: { Authorization: `Bearer ${this.botToken}` },
574
+ });
575
+ if (!res.ok) {
576
+ const text = await res.text();
577
+ throw new Error(`deleteColumn failed (${res.status}): ${text}`);
578
+ }
579
+ }
580
+ /**
581
+ * Reorder columns in a board.
582
+ * @param boardId - The board ID.
583
+ * @param columnIds - Ordered array of column IDs.
584
+ */
585
+ async reorderColumns(boardId, columnIds) {
586
+ const httpUrl = this.serverUrl
587
+ .replace(/^ws:/, "http:")
588
+ .replace(/^wss:/, "https:");
589
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards/${boardId}/columns/reorder`, {
590
+ method: "POST",
591
+ headers: {
592
+ Authorization: `Bearer ${this.botToken}`,
593
+ "Content-Type": "application/json",
594
+ },
595
+ body: JSON.stringify({ columnIds }),
596
+ });
597
+ if (!res.ok) {
598
+ const text = await res.text();
599
+ throw new Error(`reorderColumns failed (${res.status}): ${text}`);
600
+ }
601
+ }
602
+ /**
603
+ * List all kanban cards for the agent's owner.
604
+ */
605
+ async listCards() {
606
+ const httpUrl = this.serverUrl
607
+ .replace(/^ws:/, "http:")
608
+ .replace(/^wss:/, "https:");
609
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards`, {
610
+ method: "GET",
611
+ headers: { Authorization: `Bearer ${this.botToken}` },
612
+ });
613
+ if (!res.ok) {
614
+ const text = await res.text();
615
+ throw new Error(`listCards failed (${res.status}): ${text}`);
616
+ }
617
+ return res.json();
618
+ }
619
+ /**
620
+ * Mark a card as complete (moves it to the Done column).
621
+ * @param cardId - The card ID to complete.
622
+ */
623
+ async completeCard(cardId) {
624
+ const httpUrl = this.serverUrl
625
+ .replace(/^ws:/, "http:")
626
+ .replace(/^wss:/, "https:");
627
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}/complete`, {
628
+ method: "POST",
629
+ headers: { Authorization: `Bearer ${this.botToken}` },
630
+ });
631
+ if (!res.ok) {
632
+ const text = await res.text();
633
+ throw new Error(`completeCard failed (${res.status}): ${text}`);
634
+ }
635
+ return res.json();
636
+ }
637
+ /**
638
+ * List archived cards for a board.
639
+ * @param boardId - The board ID.
640
+ * @param options - Pagination options (page, limit).
641
+ */
642
+ async listArchivedCards(boardId, options) {
643
+ const httpUrl = this.serverUrl
644
+ .replace(/^ws:/, "http:")
645
+ .replace(/^wss:/, "https:");
646
+ const params = new URLSearchParams();
647
+ if (options?.page != null)
648
+ params.set("page", String(options.page));
649
+ if (options?.limit != null)
650
+ params.set("limit", String(options.limit));
651
+ const qs = params.toString();
652
+ const url = `${httpUrl}/api/agent/kanban/boards/${boardId}/archived-cards${qs ? `?${qs}` : ""}`;
653
+ const res = await fetch(url, {
654
+ method: "GET",
655
+ headers: { Authorization: `Bearer ${this.botToken}` },
656
+ });
657
+ if (!res.ok) {
658
+ const text = await res.text();
659
+ throw new Error(`listArchivedCards failed (${res.status}): ${text}`);
660
+ }
661
+ return res.json();
662
+ }
663
+ /**
664
+ * Add a commit link to a card.
665
+ * @param cardId - The card ID.
666
+ * @param body - Commit hash and optional message.
667
+ */
668
+ async addCardCommit(cardId, body) {
669
+ const httpUrl = this.serverUrl
670
+ .replace(/^ws:/, "http:")
671
+ .replace(/^wss:/, "https:");
672
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}/commits`, {
673
+ method: "POST",
674
+ headers: {
675
+ Authorization: `Bearer ${this.botToken}`,
676
+ "Content-Type": "application/json",
677
+ },
678
+ body: JSON.stringify(body),
679
+ });
680
+ if (!res.ok) {
681
+ const text = await res.text();
682
+ throw new Error(`addCardCommit failed (${res.status}): ${text}`);
683
+ }
684
+ return res.json();
685
+ }
686
+ /**
687
+ * List commits linked to a card.
688
+ * @param cardId - The card ID.
689
+ */
690
+ async listCardCommits(cardId) {
691
+ const httpUrl = this.serverUrl
692
+ .replace(/^ws:/, "http:")
693
+ .replace(/^wss:/, "https:");
694
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}/commits`, {
695
+ method: "GET",
696
+ headers: { Authorization: `Bearer ${this.botToken}` },
697
+ });
698
+ if (!res.ok) {
699
+ const text = await res.text();
700
+ throw new Error(`listCardCommits failed (${res.status}): ${text}`);
701
+ }
702
+ return res.json();
703
+ }
704
+ /**
705
+ * Link a note to a card.
706
+ * @param cardId - The card ID.
707
+ * @param noteId - The note ID to link.
708
+ */
709
+ async linkCardNote(cardId, noteId) {
710
+ const httpUrl = this.serverUrl
711
+ .replace(/^ws:/, "http:")
712
+ .replace(/^wss:/, "https:");
713
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}/notes`, {
714
+ method: "POST",
715
+ headers: {
716
+ Authorization: `Bearer ${this.botToken}`,
717
+ "Content-Type": "application/json",
718
+ },
719
+ body: JSON.stringify({ noteId }),
720
+ });
721
+ if (!res.ok) {
722
+ const text = await res.text();
723
+ throw new Error(`linkCardNote failed (${res.status}): ${text}`);
724
+ }
725
+ }
726
+ /**
727
+ * Unlink a note from a card.
728
+ * @param cardId - The card ID.
729
+ * @param noteId - The note ID to unlink.
730
+ */
731
+ async unlinkCardNote(cardId, noteId) {
732
+ const httpUrl = this.serverUrl
733
+ .replace(/^ws:/, "http:")
734
+ .replace(/^wss:/, "https:");
735
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}/notes/${noteId}`, {
736
+ method: "DELETE",
737
+ headers: { Authorization: `Bearer ${this.botToken}` },
738
+ });
739
+ if (!res.ok) {
740
+ const text = await res.text();
741
+ throw new Error(`unlinkCardNote failed (${res.status}): ${text}`);
742
+ }
743
+ }
744
+ /**
745
+ * List notes linked to a card.
746
+ * @param cardId - The card ID.
747
+ */
748
+ async listCardNotes(cardId) {
749
+ const httpUrl = this.serverUrl
750
+ .replace(/^ws:/, "http:")
751
+ .replace(/^wss:/, "https:");
752
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}/notes`, {
753
+ method: "GET",
754
+ headers: { Authorization: `Bearer ${this.botToken}` },
755
+ });
756
+ if (!res.ok) {
757
+ const text = await res.text();
758
+ throw new Error(`listCardNotes failed (${res.status}): ${text}`);
759
+ }
760
+ return res.json();
761
+ }
762
+ // ── Label API ────────────────────────────────────────────────
763
+ /**
764
+ * List labels for a board.
765
+ * @param boardId - The board ID.
766
+ */
767
+ async listLabels(boardId) {
768
+ const httpUrl = this.serverUrl
769
+ .replace(/^ws:/, "http:")
770
+ .replace(/^wss:/, "https:");
771
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards/${boardId}/labels`, {
772
+ method: "GET",
773
+ headers: { Authorization: `Bearer ${this.botToken}` },
774
+ });
775
+ if (!res.ok) {
776
+ const text = await res.text();
777
+ throw new Error(`listLabels failed (${res.status}): ${text}`);
778
+ }
779
+ return res.json();
780
+ }
781
+ /**
782
+ * Create a label on a board.
783
+ * @param boardId - The board ID.
784
+ * @param body - Label name and optional color.
785
+ */
786
+ async createLabel(boardId, body) {
787
+ const httpUrl = this.serverUrl
788
+ .replace(/^ws:/, "http:")
789
+ .replace(/^wss:/, "https:");
790
+ const res = await fetch(`${httpUrl}/api/agent/kanban/boards/${boardId}/labels`, {
791
+ method: "POST",
792
+ headers: {
793
+ Authorization: `Bearer ${this.botToken}`,
794
+ "Content-Type": "application/json",
795
+ },
796
+ body: JSON.stringify(body),
797
+ });
798
+ if (!res.ok) {
799
+ const text = await res.text();
800
+ throw new Error(`createLabel failed (${res.status}): ${text}`);
801
+ }
802
+ return res.json();
803
+ }
804
+ /**
805
+ * Update a label.
806
+ * @param labelId - The label ID to update.
807
+ * @param body - Fields to update (name, color).
808
+ */
809
+ async updateLabel(labelId, body) {
810
+ const httpUrl = this.serverUrl
811
+ .replace(/^ws:/, "http:")
812
+ .replace(/^wss:/, "https:");
813
+ const res = await fetch(`${httpUrl}/api/agent/kanban/labels/${labelId}`, {
814
+ method: "PATCH",
815
+ headers: {
816
+ Authorization: `Bearer ${this.botToken}`,
817
+ "Content-Type": "application/json",
818
+ },
819
+ body: JSON.stringify(body),
820
+ });
821
+ if (!res.ok) {
822
+ const text = await res.text();
823
+ throw new Error(`updateLabel failed (${res.status}): ${text}`);
824
+ }
825
+ return res.json();
826
+ }
827
+ /**
828
+ * Delete a label.
829
+ * @param labelId - The label ID to delete.
830
+ */
831
+ async deleteLabel(labelId) {
832
+ const httpUrl = this.serverUrl
833
+ .replace(/^ws:/, "http:")
834
+ .replace(/^wss:/, "https:");
835
+ const res = await fetch(`${httpUrl}/api/agent/kanban/labels/${labelId}`, {
836
+ method: "DELETE",
837
+ headers: { Authorization: `Bearer ${this.botToken}` },
838
+ });
839
+ if (!res.ok) {
840
+ const text = await res.text();
841
+ throw new Error(`deleteLabel failed (${res.status}): ${text}`);
842
+ }
843
+ }
844
+ /**
845
+ * Add a label to a card.
846
+ * @param cardId - The card ID.
847
+ * @param labelId - The label ID to add.
848
+ */
849
+ async addCardLabel(cardId, labelId) {
850
+ const httpUrl = this.serverUrl
851
+ .replace(/^ws:/, "http:")
852
+ .replace(/^wss:/, "https:");
853
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}/labels`, {
854
+ method: "POST",
855
+ headers: {
856
+ Authorization: `Bearer ${this.botToken}`,
857
+ "Content-Type": "application/json",
858
+ },
859
+ body: JSON.stringify({ labelId }),
860
+ });
861
+ if (!res.ok) {
862
+ const text = await res.text();
863
+ throw new Error(`addCardLabel failed (${res.status}): ${text}`);
864
+ }
865
+ }
866
+ /**
867
+ * Remove a label from a card.
868
+ * @param cardId - The card ID.
869
+ * @param labelId - The label ID to remove.
870
+ */
871
+ async removeCardLabel(cardId, labelId) {
872
+ const httpUrl = this.serverUrl
873
+ .replace(/^ws:/, "http:")
874
+ .replace(/^wss:/, "https:");
875
+ const res = await fetch(`${httpUrl}/api/agent/kanban/cards/${cardId}/labels/${labelId}`, {
876
+ method: "DELETE",
877
+ headers: { Authorization: `Bearer ${this.botToken}` },
878
+ });
879
+ if (!res.ok) {
880
+ const text = await res.text();
881
+ throw new Error(`removeCardLabel failed (${res.status}): ${text}`);
882
+ }
883
+ }
884
+ // ── Memory API ───────────────────────────────────────────────
885
+ /**
886
+ * Search memories across all memory capsules granted to this agent.
887
+ * Uses hybrid search (embedding + text) to find relevant memories.
888
+ * @param options - Query string and optional limit.
889
+ */
890
+ async queryMemory(options) {
891
+ const httpUrl = this.serverUrl
892
+ .replace(/^ws:/, "http:")
893
+ .replace(/^wss:/, "https:");
894
+ const params = new URLSearchParams();
895
+ params.set("query", options.query);
896
+ if (options.limit != null)
897
+ params.set("limit", String(options.limit));
898
+ const res = await fetch(`${httpUrl}/api/agent/capsules?${params}`, {
899
+ method: "GET",
900
+ headers: { Authorization: `Bearer ${this.botToken}` },
901
+ });
902
+ if (!res.ok) {
903
+ const body = await res.text();
904
+ throw new Error(`queryMemory failed (${res.status}): ${body}`);
905
+ }
906
+ // Server returns snake_case, map to camelCase
907
+ const raw = (await res.json());
908
+ return raw.map((r) => ({
909
+ content: r.content,
910
+ capsuleName: r.capsule_name,
911
+ capsuleId: r.capsule_id,
912
+ score: r.score,
913
+ importance: r.importance,
914
+ }));
915
+ }
916
+ // ── Skill Prompt API ─────────────────────────────────────────
917
+ /**
918
+ * Fetch the full prompt content for an installed skill by slug.
919
+ * Use this when the agent decides to trigger a skill from availableSkills.
920
+ * @param skillSlug - The skill slug (e.g. "draw", "proactive-agent").
921
+ */
922
+ async fetchSkillPrompt(skillSlug) {
923
+ const httpUrl = this.serverUrl
924
+ .replace(/^ws:/, "http:")
925
+ .replace(/^wss:/, "https:");
926
+ const res = await fetch(`${httpUrl}/api/agent/skills/${encodeURIComponent(skillSlug)}/prompt`, {
927
+ method: "GET",
928
+ headers: { Authorization: `Bearer ${this.botToken}` },
929
+ });
930
+ if (!res.ok) {
931
+ const body = await res.text();
932
+ throw new Error(`fetchSkillPrompt failed (${res.status}): ${body}`);
933
+ }
934
+ return (await res.json());
935
+ }
936
+ // ── Note Share API ───────────────────────────────────────────
937
+ /**
938
+ * Share a note as a message in a conversation.
939
+ * Creates a rich preview card visible to all conversation members.
940
+ * @param conversationId - The conversation to share into.
941
+ * @param noteId - The note ID to share.
942
+ */
943
+ async shareNote(conversationId, noteId) {
944
+ const httpUrl = this.serverUrl
945
+ .replace(/^ws:/, "http:")
946
+ .replace(/^wss:/, "https:");
947
+ const res = await fetch(`${httpUrl}/api/agent/conversations/${conversationId}/notes/${noteId}/share`, {
948
+ method: "POST",
949
+ headers: { Authorization: `Bearer ${this.botToken}` },
950
+ });
951
+ if (!res.ok) {
952
+ const text = await res.text();
953
+ throw new Error(`shareNote failed (${res.status}): ${text}`);
954
+ }
955
+ return res.json();
956
+ }
231
957
  handleTask(data) {
232
958
  if (!this.taskHandler)
233
959
  return;
@@ -239,6 +965,18 @@ export class ArinovaAgent {
239
965
  this.send({ type: "agent_heartbeat", taskId });
240
966
  }, TASK_HEARTBEAT_INTERVAL);
241
967
  const stopHeartbeat = () => clearInterval(heartbeatTimer);
968
+ // Guard: ensure sendComplete/sendError only fires once per task.
969
+ // After cancel_task, the background handler may still call sendComplete
970
+ // when the LLM finishes — the guard prevents duplicate events.
971
+ let taskFinished = false;
972
+ const markFinished = () => {
973
+ if (taskFinished)
974
+ return false;
975
+ taskFinished = true;
976
+ stopHeartbeat();
977
+ this.taskAbortControllers.delete(taskId);
978
+ return true;
979
+ };
242
980
  const ctx = {
243
981
  taskId,
244
982
  conversationId: data.conversationId,
@@ -250,10 +988,14 @@ export class ArinovaAgent {
250
988
  replyTo: data.replyTo,
251
989
  history: data.history,
252
990
  attachments: data.attachments,
253
- sendChunk: (delta) => this.send({ type: "agent_chunk", taskId, chunk: delta }),
991
+ sendChunk: (delta) => {
992
+ if (taskFinished)
993
+ return;
994
+ this.send({ type: "agent_chunk", taskId, chunk: delta });
995
+ },
254
996
  sendComplete: (fullContent, options) => {
255
- stopHeartbeat();
256
- this.taskAbortControllers.delete(taskId);
997
+ if (!markFinished())
998
+ return;
257
999
  this.send({
258
1000
  type: "agent_complete",
259
1001
  taskId,
@@ -262,15 +1004,21 @@ export class ArinovaAgent {
262
1004
  });
263
1005
  },
264
1006
  sendError: (error) => {
265
- stopHeartbeat();
266
- this.taskAbortControllers.delete(taskId);
1007
+ if (!markFinished())
1008
+ return;
267
1009
  this.send({ type: "agent_error", taskId, error });
268
1010
  },
269
1011
  signal: abortController.signal,
270
1012
  uploadFile: (file, fileName, fileType) => this.uploadFile(data.conversationId, file, fileName, fileType),
1013
+ fetchHistory: (options) => this.fetchHistory(data.conversationId, options),
271
1014
  };
272
- // Stop heartbeat if task is aborted (user cancelled)
273
- abortController.signal.addEventListener("abort", stopHeartbeat, { once: true });
1015
+ // When task is aborted (user cancelled), immediately send cancellation
1016
+ // error so the server knows this agent is free for new tasks.
1017
+ abortController.signal.addEventListener("abort", () => {
1018
+ if (!markFinished())
1019
+ return;
1020
+ this.send({ type: "agent_error", taskId, error: "cancelled" });
1021
+ }, { once: true });
274
1022
  Promise.resolve(this.taskHandler(ctx)).catch((err) => {
275
1023
  const errorMsg = err instanceof Error ? err.message : String(err);
276
1024
  ctx.sendError(errorMsg);