dashclaw 1.8.1 → 1.8.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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dashclaw.js +95 -21
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1026,6 +1026,7 @@ Send a message to another agent or broadcast.
1026
1026
  | threadId | string | No | Thread ID to attach to |
1027
1027
  | urgent | boolean | No | Mark as urgent |
1028
1028
  | docRef | string | No | Reference to a shared doc ID |
1029
+ | attachments | Array<{filename, mime_type, data}> | No | File attachments (base64, max 3, max 5MB each) |
1029
1030
 
1030
1031
  **Returns:** `Promise<{message: Object, message_id: string}>`
1031
1032
 
@@ -1119,6 +1120,38 @@ Create or update a shared workspace document. Upserts by name.
1119
1120
 
1120
1121
  **Returns:** `Promise<{doc: Object, doc_id: string}>`
1121
1122
 
1123
+ ### claw.getAttachmentUrl(attachmentId)
1124
+
1125
+ Get a URL to download an attachment.
1126
+
1127
+ | Parameter | Type | Description |
1128
+ |---|---|---|
1129
+ | `attachmentId` | `string` | Attachment ID (`att_*`) |
1130
+
1131
+ **Returns:** `string` — URL to fetch the attachment binary
1132
+
1133
+ ---
1134
+
1135
+ ### claw.getAttachment(attachmentId)
1136
+
1137
+ Download an attachment as a Buffer.
1138
+
1139
+ | Parameter | Type | Description |
1140
+ |---|---|---|
1141
+ | `attachmentId` | `string` | Attachment ID (`att_*`) |
1142
+
1143
+ **Returns:** `Promise<{ data: Buffer, filename: string, mimeType: string }>`
1144
+
1145
+ ```js
1146
+ const inbox = await claw.getInbox();
1147
+ for (const msg of inbox.messages) {
1148
+ for (const att of msg.attachments || []) {
1149
+ const { data, filename } = await claw.getAttachment(att.id);
1150
+ fs.writeFileSync(filename, data);
1151
+ }
1152
+ }
1153
+ ```
1154
+
1122
1155
  ---
1123
1156
 
1124
1157
  ## Agent Pairing
package/dashclaw.js CHANGED
@@ -542,6 +542,10 @@ class DashClaw {
542
542
  * Subscribe to real-time SSE events from the DashClaw server.
543
543
  * Uses fetch-based SSE parsing for Node 18+ compatibility (no native EventSource required).
544
544
  *
545
+ * @param {Object} [options]
546
+ * @param {boolean} [options.reconnect=true] - Auto-reconnect on disconnect (resumes from last event ID)
547
+ * @param {number} [options.maxRetries=Infinity] - Max reconnection attempts before giving up
548
+ * @param {number} [options.retryInterval=3000] - Milliseconds between reconnection attempts
545
549
  * @returns {{ on(eventType: string, callback: Function): this, close(): void, _promise: Promise<void> }}
546
550
  *
547
551
  * @example
@@ -552,23 +556,36 @@ class DashClaw {
552
556
  * .on('policy.updated', (data) => console.log('Policy changed:', data))
553
557
  * .on('task.assigned', (data) => console.log('Task assigned:', data))
554
558
  * .on('task.completed', (data) => console.log('Task done:', data))
559
+ * .on('reconnecting', ({ attempt }) => console.log(`Reconnecting #${attempt}...`))
555
560
  * .on('error', (err) => console.error('Stream error:', err));
556
561
  *
557
562
  * // Later:
558
563
  * stream.close();
559
564
  */
560
- events() {
565
+ events({ reconnect = true, maxRetries = Infinity, retryInterval = 3000 } = {}) {
561
566
  const url = `${this.baseUrl}/api/stream`;
562
567
  const apiKey = this.apiKey;
563
568
 
564
569
  const handlers = new Map();
565
570
  let closed = false;
566
571
  let controller = null;
572
+ let lastEventId = null;
573
+ let retryCount = 0;
574
+
575
+ const emit = (eventType, data) => {
576
+ const cbs = handlers.get(eventType) || [];
577
+ for (const cb of cbs) {
578
+ try { cb(data); } catch { /* ignore handler errors */ }
579
+ }
580
+ };
567
581
 
568
582
  const connect = async () => {
569
583
  controller = new AbortController();
584
+ const headers = { 'x-api-key': apiKey };
585
+ if (lastEventId) headers['last-event-id'] = lastEventId;
586
+
570
587
  const res = await fetch(url, {
571
- headers: { 'x-api-key': apiKey },
588
+ headers,
572
589
  signal: controller.signal,
573
590
  });
574
591
 
@@ -576,9 +593,14 @@ class DashClaw {
576
593
  throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);
577
594
  }
578
595
 
596
+ retryCount = 0; // Reset on successful connection
597
+
579
598
  const reader = res.body.getReader();
580
599
  const decoder = new TextDecoder();
581
600
  let buffer = '';
601
+ // Persist across reads so frames split across chunks are handled correctly
602
+ let currentEvent = null;
603
+ let currentData = '';
582
604
 
583
605
  while (!closed) {
584
606
  const { done, value } = await reader.read();
@@ -589,39 +611,55 @@ class DashClaw {
589
611
  const lines = buffer.split('\n');
590
612
  buffer = lines.pop(); // Keep incomplete line in buffer
591
613
 
592
- let currentEvent = null;
593
- let currentData = '';
594
-
595
614
  for (const line of lines) {
596
- if (line.startsWith('event: ')) {
615
+ if (line.startsWith('id: ')) {
616
+ lastEventId = line.slice(4).trim();
617
+ } else if (line.startsWith('event: ')) {
597
618
  currentEvent = line.slice(7).trim();
598
619
  } else if (line.startsWith('data: ')) {
599
620
  currentData += line.slice(6);
621
+ } else if (line.startsWith(':')) {
622
+ // SSE comment (keepalive heartbeat) — ignore
600
623
  } else if (line === '' && currentEvent) {
601
624
  // End of SSE frame — dispatch
602
- const eventHandlers = handlers.get(currentEvent) || [];
603
- if (eventHandlers.length > 0 && currentData) {
625
+ if (currentData) {
604
626
  try {
605
627
  const parsed = JSON.parse(currentData);
606
- for (const cb of eventHandlers) {
607
- try { cb(parsed); } catch { /* ignore handler errors */ }
608
- }
628
+ emit(currentEvent, parsed);
609
629
  } catch { /* ignore parse errors */ }
610
630
  }
611
631
  currentEvent = null;
612
632
  currentData = '';
633
+ } else if (line === '') {
634
+ // Blank line without a pending event — reset partial state
635
+ currentEvent = null;
636
+ currentData = '';
613
637
  }
614
638
  }
615
639
  }
616
640
  };
617
641
 
618
- const connectionPromise = connect().catch((err) => {
619
- if (closed) return; // Abort is expected on close
620
- const errorHandlers = handlers.get('error') || [];
621
- for (const cb of errorHandlers) {
622
- try { cb(err); } catch { /* ignore */ }
642
+ const connectLoop = async () => {
643
+ while (!closed) {
644
+ try {
645
+ await connect();
646
+ } catch (err) {
647
+ if (closed) return;
648
+ emit('error', err);
649
+ }
650
+ // Stream ended (server closed, network drop, etc.)
651
+ if (closed) return;
652
+ if (!reconnect || retryCount >= maxRetries) {
653
+ emit('error', new Error('SSE stream ended'));
654
+ return;
655
+ }
656
+ retryCount++;
657
+ emit('reconnecting', { attempt: retryCount, maxRetries });
658
+ await new Promise((r) => setTimeout(r, retryInterval));
623
659
  }
624
- });
660
+ };
661
+
662
+ const connectionPromise = connectLoop();
625
663
 
626
664
  const handle = {
627
665
  on(eventType, callback) {
@@ -1495,7 +1533,7 @@ class DashClaw {
1495
1533
  }
1496
1534
 
1497
1535
  // ══════════════════════════════════════════════
1498
- // Category 11: Agent Messaging (9 methods)
1536
+ // Category 11: Agent Messaging (11 methods)
1499
1537
  // ══════════════════════════════════════════════
1500
1538
 
1501
1539
  /**
@@ -1508,10 +1546,11 @@ class DashClaw {
1508
1546
  * @param {string} [params.threadId] - Thread ID to attach message to
1509
1547
  * @param {boolean} [params.urgent=false] - Mark as urgent
1510
1548
  * @param {string} [params.docRef] - Reference to a shared doc ID
1549
+ * @param {Array<{filename: string, mime_type: string, data: string}>} [params.attachments] - File attachments (base64 data, max 3, max 5MB each)
1511
1550
  * @returns {Promise<{message: Object, message_id: string}>}
1512
1551
  */
1513
- async sendMessage({ to, type, subject, body, threadId, urgent, docRef }) {
1514
- return this._request('/api/messages', 'POST', {
1552
+ async sendMessage({ to, type, subject, body, threadId, urgent, docRef, attachments }) {
1553
+ const payload = {
1515
1554
  from_agent_id: this.agentId,
1516
1555
  to_agent_id: to || null,
1517
1556
  message_type: type || 'info',
@@ -1520,7 +1559,9 @@ class DashClaw {
1520
1559
  thread_id: threadId,
1521
1560
  urgent,
1522
1561
  doc_ref: docRef,
1523
- });
1562
+ };
1563
+ if (attachments?.length) payload.attachments = attachments;
1564
+ return this._request('/api/messages', 'POST', payload);
1524
1565
  }
1525
1566
 
1526
1567
  /**
@@ -1642,6 +1683,39 @@ class DashClaw {
1642
1683
  });
1643
1684
  }
1644
1685
 
1686
+ /**
1687
+ * Get an attachment's download URL or fetch its binary data.
1688
+ * @param {string} attachmentId - Attachment ID (att_*)
1689
+ * @returns {string} URL to fetch the attachment
1690
+ */
1691
+ getAttachmentUrl(attachmentId) {
1692
+ return `${this.baseUrl}/api/messages/attachments?id=${encodeURIComponent(attachmentId)}`;
1693
+ }
1694
+
1695
+ /**
1696
+ * Download an attachment as a Buffer.
1697
+ * @param {string} attachmentId - Attachment ID (att_*)
1698
+ * @returns {Promise<{data: Buffer, filename: string, mimeType: string}>}
1699
+ */
1700
+ async getAttachment(attachmentId) {
1701
+ const url = this.getAttachmentUrl(attachmentId);
1702
+ const res = await fetch(url, {
1703
+ headers: { 'x-api-key': this.apiKey },
1704
+ });
1705
+ if (!res.ok) {
1706
+ const err = await res.json().catch(() => ({}));
1707
+ throw new Error(err.error || `Attachment fetch failed: ${res.status}`);
1708
+ }
1709
+ const data = Buffer.from(await res.arrayBuffer());
1710
+ const cd = res.headers.get('content-disposition') || '';
1711
+ const match = cd.match(/filename="(.+?)"/);
1712
+ return {
1713
+ data,
1714
+ filename: match ? match[1] : attachmentId,
1715
+ mimeType: res.headers.get('content-type') || 'application/octet-stream',
1716
+ };
1717
+ }
1718
+
1645
1719
  // ══════════════════════════════════════════════
1646
1720
  // Category 13: Behavior Guard (2 methods)
1647
1721
  // ══════════════════════════════════════════════
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dashclaw",
3
- "version": "1.8.1",
4
- "description": "Full-featured agent toolkit for the DashClaw platform. 95+ methods across 21+ categories for action recording, context management, session handoffs, security scanning, behavior guard, compliance, task routing, identity binding, organization management, webhooks, bulk sync, and more.",
3
+ "version": "1.8.2",
4
+ "description": "Full-featured agent toolkit for the DashClaw platform. 96+ methods across 22+ categories for action recording, context management, session handoffs, security scanning, behavior guard, compliance, task routing, identity binding, organization management, webhooks, bulk sync, and more.",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"