dashclaw 2.0.3 → 2.1.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/dashclaw.js CHANGED
@@ -1,2885 +1,137 @@
1
- /**
2
- * DashClaw SDK
3
- * Full-featured agent toolkit for the DashClaw platform.
4
- * Zero-dependency ESM SDK. Requires Node 18+ (native fetch).
5
- *
6
- * 178+ methods across 30+ categories:
7
- * - Action Recording (7)
8
- * - Loops & Assumptions (7)
9
- * - Signals (1)
10
- * - Dashboard Data (9)
11
- * - Session Handoffs (3)
12
- * - Context Manager (7)
13
- * - Automation Snippets (5)
14
- * - User Preferences (6)
15
- * - Daily Digest (1)
16
- * - Security Scanning (3)
17
- * - Agent Messaging (12)
18
- * - Behavior Guard (2)
19
- * - Agent Pairing (3)
20
- * - Identity Binding (2)
21
- * - Organization Management (5)
22
- * - Activity Logs (1)
23
- * - Webhooks (5)
24
- * - Bulk Sync (1)
25
- * - Policy Testing (3)
26
- * - Compliance Engine (5)
27
- * - Task Routing (10)
28
- * - Real-Time Events (1)
29
- * - Evaluations (12)
30
- * - Scorer Management (8)
31
- * - Eval Runs (6)
32
- * - Scoring Profiles (17)
33
- * - Learning Analytics (6)
34
- * - Behavioral Drift (9)
35
- * - Prompt Management (12)
36
- * - Feedback Loops (10)
37
- */
38
-
39
- class DashClaw {
40
- /**
41
- * @param {Object} options
42
- * @param {string} options.baseUrl - DashClaw base URL (e.g. "http://localhost:3000" or "https://your-app.vercel.app")
43
- * @param {string} options.apiKey - API key for authentication (determines which org's data you access)
44
- * @param {string} options.agentId - Unique identifier for this agent
45
- * @param {string} [options.agentName] - Human-readable agent name
46
- * @param {string} [options.swarmId] - Swarm/group identifier if part of a multi-agent system
47
- * @param {string} [options.guardMode='off'] - Auto guard check before createAction: 'off' | 'warn' | 'enforce'
48
- * @param {Function} [options.guardCallback] - Called with guard decision object when guardMode is active
49
- * @param {string} [options.autoRecommend='off'] - Recommendation mode: 'off' | 'warn' | 'enforce'
50
- * @param {number} [options.recommendationConfidenceMin=70] - Minimum recommendation confidence to auto-apply in enforce mode
51
- * @param {Function} [options.recommendationCallback] - Called with recommendation adaptation details when autoRecommend is active
52
- * @param {string} [options.hitlMode='off'] - How to handle pending approvals: 'off' (return immediately) | 'wait' (block and poll)
53
- * @param {CryptoKey} [options.privateKey] - Web Crypto API Private Key for signing actions
54
- */
55
- constructor({
56
- baseUrl,
57
- apiKey,
58
- agentId,
59
- agentName,
60
- swarmId,
61
- guardMode,
62
- guardCallback,
63
- autoRecommend,
64
- recommendationConfidenceMin,
65
- recommendationCallback,
66
- hitlMode,
67
- privateKey
68
- }) {
69
- if (!baseUrl) throw new Error('baseUrl is required');
70
- if (!apiKey) throw new Error('apiKey is required');
71
- if (!agentId) throw new Error('agentId is required');
72
-
73
- const validModes = ['off', 'warn', 'enforce'];
74
- if (guardMode && !validModes.includes(guardMode)) {
75
- throw new Error(`guardMode must be one of: ${validModes.join(', ')}`);
76
- }
77
- if (autoRecommend && !validModes.includes(autoRecommend)) {
78
- throw new Error(`autoRecommend must be one of: ${validModes.join(', ')}`);
79
- }
80
-
81
- this.baseUrl = baseUrl.replace(/\/$/, '');
82
- if (!this.baseUrl.startsWith('https://') && !this.baseUrl.includes('localhost') && !this.baseUrl.includes('127.0.0.1')) {
83
- console.warn('[DashClaw] WARNING: baseUrl does not use HTTPS. API keys will be sent in plaintext. Use HTTPS in production.');
84
- }
85
- this.apiKey = apiKey;
86
- this.agentId = agentId;
87
- this.agentName = agentName || null;
88
- this.swarmId = swarmId || null;
89
- this.guardMode = guardMode || 'off';
90
- this.guardCallback = guardCallback || null;
91
- this.autoRecommend = autoRecommend || 'off';
92
- const parsedConfidenceMin = Number(recommendationConfidenceMin);
93
- this.recommendationConfidenceMin = Number.isFinite(parsedConfidenceMin)
94
- ? Math.max(0, Math.min(parsedConfidenceMin, 100))
95
- : 70;
96
- this.recommendationCallback = recommendationCallback || null;
97
- this.hitlMode = hitlMode || 'off';
98
- this.privateKey = privateKey || null;
99
-
100
- // Auto-import JWK if passed as plain object
101
- if (this.privateKey && typeof this.privateKey === 'object' && this.privateKey.kty) {
102
- this._pendingKeyImport = this._importJwk(this.privateKey);
103
- }
104
- }
105
-
106
- async _importJwk(jwk) {
107
- try {
108
- const cryptoSubtle = globalThis.crypto?.subtle || (await import('node:crypto')).webcrypto.subtle;
109
- this.privateKey = await cryptoSubtle.importKey(
110
- "jwk",
111
- jwk,
112
- { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
113
- false,
114
- ["sign"]
115
- );
116
- delete this._pendingKeyImport;
117
- } catch (err) {
118
- console.warn(`[DashClaw] Failed to auto-import privateKey JWK: ${err.message}`);
119
- this.privateKey = null;
120
- }
121
- }
122
-
123
- async _request(pathOrMethod, methodOrPath, body, params) {
124
- let path, method;
125
- if (typeof pathOrMethod === 'string' && pathOrMethod.startsWith('/')) {
126
- path = pathOrMethod;
127
- method = methodOrPath || 'GET';
128
- } else {
129
- method = pathOrMethod;
130
- path = methodOrPath;
131
- }
132
-
133
- if (params) {
134
- const qs = new URLSearchParams();
135
- for (const [k, v] of Object.entries(params)) {
136
- if (v !== undefined && v !== null) qs.append(k, String(v));
137
- }
138
- const qsStr = qs.toString();
139
- if (qsStr) {
140
- path += (path.includes('?') ? '&' : '?') + qsStr;
141
- }
142
- }
143
-
144
- const url = `${this.baseUrl}${path}`;
145
- const headers = {
146
- 'Content-Type': 'application/json',
147
- 'x-api-key': this.apiKey
148
- };
149
-
150
- const res = await fetch(url, {
151
- method,
152
- headers,
153
- body: body ? JSON.stringify(body) : undefined
154
- });
155
-
156
- const data = await res.json();
157
-
158
- if (!res.ok) {
159
- const err = new Error(data.error || `Request failed with status ${res.status}`);
160
- err.status = res.status;
161
- err.details = data.details;
162
- throw err;
163
- }
164
-
165
- return data;
166
- }
167
-
168
- /**
169
- * Create an agent pairing request (returns a link the user can click to approve).
170
- *
171
- * @param {Object} options
172
- * @param {string} options.publicKeyPem - PEM public key (SPKI) to register for this agent.
173
- * @param {string} [options.algorithm='RSASSA-PKCS1-v1_5']
174
- * @param {string} [options.agentName]
175
- * @returns {Promise<{pairing: Object, pairing_url: string}>}
176
- */
177
- async createPairing({ publicKeyPem, algorithm = 'RSASSA-PKCS1-v1_5', agentName } = {}) {
178
- if (!publicKeyPem) throw new Error('publicKeyPem is required');
179
- return this._request('/api/pairings', 'POST', {
180
- agent_id: this.agentId,
181
- agent_name: agentName || this.agentName,
182
- public_key: publicKeyPem,
183
- algorithm,
184
- });
185
- }
186
-
187
- async _derivePublicKeyPemFromPrivateJwk(privateJwk) {
188
- // Node-only helper (works in the typical agent runtime).
189
- const { createPrivateKey, createPublicKey } = await import('node:crypto');
190
- const priv = createPrivateKey({ key: privateJwk, format: 'jwk' });
191
- const pub = createPublicKey(priv);
192
- return pub.export({ type: 'spki', format: 'pem' });
193
- }
194
-
195
- /**
196
- * Convenience: derive public PEM from a private JWK and create a pairing request.
197
- * @param {Object} privateJwk
198
- * @param {Object} [options]
199
- * @param {string} [options.agentName]
200
- */
201
- async createPairingFromPrivateJwk(privateJwk, { agentName } = {}) {
202
- if (!privateJwk) throw new Error('privateJwk is required');
203
- const publicKeyPem = await this._derivePublicKeyPemFromPrivateJwk(privateJwk);
204
- return this.createPairing({ publicKeyPem, agentName });
205
- }
206
-
207
- /**
208
- * Poll a pairing until it is approved/expired.
209
- * @param {string} pairingId
210
- * @param {Object} [options]
211
- * @param {number} [options.timeout=300000] - Max wait time (5 min)
212
- * @param {number} [options.interval=2000] - Poll interval
213
- * @returns {Promise<Object>} pairing object
214
- */
215
- async waitForPairing(pairingId, { timeout = 300000, interval = 2000 } = {}) {
216
- const start = Date.now();
217
- while (Date.now() - start < timeout) {
218
- const res = await this._request(`/api/pairings/${encodeURIComponent(pairingId)}`, 'GET');
219
- const pairing = res.pairing;
220
- if (!pairing) throw new Error('Pairing response missing pairing');
221
- if (pairing.status === 'approved') return pairing;
222
- if (pairing.status === 'expired') throw new Error('Pairing expired');
223
- await new Promise((r) => setTimeout(r, interval));
224
- }
225
- throw new Error('Timed out waiting for pairing approval');
226
- }
227
-
228
- /**
229
- * Get a pairing request by ID.
230
- * @param {string} pairingId
231
- * @returns {Promise<{pairing: Object}>}
232
- */
233
- async getPairing(pairingId) {
234
- return this._request(`/api/pairings/${encodeURIComponent(pairingId)}`, 'GET');
235
- }
236
-
237
- /**
238
- * Internal: check guard policies before action creation.
239
- * Only active when guardMode is 'warn' or 'enforce'.
240
- * @param {Object} actionDef - Action definition from createAction()
241
- */
242
- async _guardCheck(actionDef) {
243
- if (this.guardMode === 'off') return;
244
-
245
- const context = {
246
- action_type: actionDef.action_type,
247
- risk_score: actionDef.risk_score,
248
- systems_touched: actionDef.systems_touched,
249
- reversible: actionDef.reversible,
250
- declared_goal: actionDef.declared_goal,
251
- };
252
-
253
- let decision;
254
- try {
255
- decision = await this.guard(context);
256
- } catch (err) {
257
- // Guard API failure is fail-open: log and proceed
258
- console.warn(`[DashClaw] Guard check failed (proceeding): ${err.message}`);
259
- return;
260
- }
261
-
262
- if (this.guardCallback) {
263
- try { this.guardCallback(decision); } catch { /* ignore callback errors */ }
264
- }
265
-
266
- const isBlocked = decision.decision === 'block' || decision.decision === 'require_approval';
267
-
268
- if (this.guardMode === 'warn' && isBlocked) {
269
- console.warn(
270
- `[DashClaw] Guard ${decision.decision}: ${decision.reasons.join('; ') || 'no reason'}. Proceeding in warn mode.`
271
- );
272
- return;
273
- }
274
-
275
- if (this.guardMode === 'enforce' && isBlocked) {
276
- throw new GuardBlockedError(decision);
277
- }
278
- }
279
-
280
- _canonicalJsonStringify(value) {
281
- const canonicalize = (v) => {
282
- if (v === null) return 'null';
283
-
284
- const t = typeof v;
285
- if (t === 'string' || t === 'number' || t === 'boolean') return JSON.stringify(v);
286
-
287
- if (t === 'undefined') return 'null';
288
-
289
- if (Array.isArray(v)) {
290
- return `[${v.map((x) => (typeof x === 'undefined' ? 'null' : canonicalize(x))).join(',')}]`;
291
- }
292
-
293
- if (t === 'object') {
294
- const keys = Object.keys(v)
295
- .filter((k) => typeof v[k] !== 'undefined')
296
- .sort();
297
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`).join(',')}}`;
298
- }
299
-
300
- return 'null';
301
- };
302
-
303
- return canonicalize(value);
304
- }
305
-
306
- _toBase64(bytes) {
307
- if (typeof btoa === 'function') {
308
- return btoa(String.fromCharCode(...bytes));
309
- }
310
- return Buffer.from(bytes).toString('base64');
311
- }
312
-
313
- _isRestrictiveDecision(decision) {
314
- return decision?.decision === 'block' || decision?.decision === 'require_approval';
315
- }
316
-
317
- _buildGuardContext(actionDef) {
318
- return {
319
- action_type: actionDef.action_type,
320
- risk_score: actionDef.risk_score,
321
- systems_touched: actionDef.systems_touched,
322
- reversible: actionDef.reversible,
323
- declared_goal: actionDef.declared_goal,
324
- agent_id: this.agentId,
325
- };
326
- }
327
-
328
- async _reportRecommendationEvent(event) {
329
- try {
330
- await this._request('/api/learning/recommendations/events', 'POST', {
331
- ...event,
332
- agent_id: event.agent_id || this.agentId,
333
- });
334
- } catch {
335
- // Telemetry should never break action execution
336
- }
337
- }
338
-
339
- async _autoRecommend(actionDef) {
340
- if (this.autoRecommend === 'off' || !actionDef?.action_type) {
341
- return { action: actionDef, recommendation: null, adapted_fields: [] };
342
- }
343
-
344
- let result;
345
- try {
346
- result = await this.recommendAction(actionDef);
347
- } catch (err) {
348
- console.warn(`[DashClaw] Recommendation fetch failed (proceeding): ${err.message}`);
349
- return { action: actionDef, recommendation: null, adapted_fields: [] };
350
- }
351
-
352
- if (this.recommendationCallback) {
353
- try { this.recommendationCallback(result); } catch { /* ignore callback errors */ }
354
- }
355
-
356
- const recommendation = result.recommendation || null;
357
- if (!recommendation) return result;
358
-
359
- const confidence = Number(recommendation.confidence || 0);
360
- if (confidence < this.recommendationConfidenceMin) {
361
- const override_reason = `confidence_below_threshold:${confidence}<${this.recommendationConfidenceMin}`;
362
- await this._reportRecommendationEvent({
363
- recommendation_id: recommendation.id,
364
- event_type: 'overridden',
365
- details: { action_type: actionDef.action_type, reason: override_reason },
366
- });
367
- return {
368
- ...result,
369
- action: {
370
- ...actionDef,
371
- recommendation_id: recommendation.id,
372
- recommendation_applied: false,
373
- recommendation_override_reason: override_reason,
374
- },
375
- };
376
- }
377
-
378
- let guardDecision = null;
379
- try {
380
- guardDecision = await this.guard(this._buildGuardContext(result.action || actionDef));
381
- } catch (err) {
382
- console.warn(`[DashClaw] Recommendation guard probe failed: ${err.message}`);
383
- }
384
-
385
- if (this._isRestrictiveDecision(guardDecision)) {
386
- const override_reason = `guard_restrictive:${guardDecision.decision}`;
387
- await this._reportRecommendationEvent({
388
- recommendation_id: recommendation.id,
389
- event_type: 'overridden',
390
- details: { action_type: actionDef.action_type, reason: override_reason },
391
- });
392
- return {
393
- ...result,
394
- action: {
395
- ...actionDef,
396
- recommendation_id: recommendation.id,
397
- recommendation_applied: false,
398
- recommendation_override_reason: override_reason,
399
- },
400
- };
401
- }
402
-
403
- if (this.autoRecommend === 'warn') {
404
- const override_reason = 'warn_mode_no_autoadapt';
405
- await this._reportRecommendationEvent({
406
- recommendation_id: recommendation.id,
407
- event_type: 'overridden',
408
- details: { action_type: actionDef.action_type, reason: override_reason },
409
- });
410
- return {
411
- ...result,
412
- action: {
413
- ...actionDef,
414
- recommendation_id: recommendation.id,
415
- recommendation_applied: false,
416
- recommendation_override_reason: override_reason,
417
- },
418
- };
419
- }
420
-
421
- await this._reportRecommendationEvent({
422
- recommendation_id: recommendation.id,
423
- event_type: 'applied',
424
- details: {
425
- action_type: actionDef.action_type,
426
- adapted_fields: result.adapted_fields || [],
427
- confidence,
428
- },
429
- });
430
-
431
- return {
432
- ...result,
433
- action: {
434
- ...(result.action || actionDef),
435
- recommendation_id: recommendation.id,
436
- recommendation_applied: true,
437
- recommendation_override_reason: null,
438
- },
439
- };
440
- }
441
-
442
- // ══════════════════════════════════════════════
443
- // Category 1: Decision Recording (6 methods)
444
- // ══════════════════════════════════════════════
445
-
446
- /**
447
- * Record a governed decision. Every action is a decision with a full audit trail: goal, reasoning, assumptions, and policy compliance.
448
- * @param {Object} action
449
- * @param {string} action.action_type - One of: build, deploy, post, apply, security, message, api, calendar, research, review, fix, refactor, test, config, monitor, alert, cleanup, sync, migrate, other
450
- * @param {string} action.declared_goal - What this action aims to accomplish
451
- * @param {string} [action.action_id] - Custom action ID (auto-generated if omitted)
452
- * @param {string} [action.reasoning] - Why the agent decided to take this action
453
- * @param {string} [action.authorization_scope] - What permissions were granted
454
- * @param {string} [action.trigger] - What triggered this action
455
- * @param {string[]} [action.systems_touched] - Systems this action interacts with
456
- * @param {string} [action.input_summary] - Summary of input data
457
- * @param {string} [action.parent_action_id] - Parent action if this is a sub-action
458
- * @param {boolean} [action.reversible=true] - Whether this action can be undone
459
- * @param {number} [action.risk_score=0] - Risk score 0-100
460
- * @param {number} [action.confidence=50] - Confidence level 0-100
461
- * @returns {Promise<{action: Object, action_id: string}>}
462
- */
463
- async createAction(action) {
464
- const recommendationResult = await this._autoRecommend(action);
465
- const finalAction = recommendationResult.action || action;
466
-
467
- await this._guardCheck(finalAction);
468
- if (this._pendingKeyImport) await this._pendingKeyImport;
469
-
470
- const payload = {
471
- agent_id: this.agentId,
472
- agent_name: this.agentName,
473
- swarm_id: this.swarmId,
474
- ...finalAction
475
- };
476
-
477
- let signature = null;
478
- if (this.privateKey) {
479
- try {
480
- const encoder = new TextEncoder();
481
- const data = encoder.encode(this._canonicalJsonStringify(payload));
482
- // Use global crypto or fallback to node:crypto
483
- const cryptoSubtle = globalThis.crypto?.subtle || (await import('node:crypto')).webcrypto.subtle;
484
-
485
- const sigBuffer = await cryptoSubtle.sign(
486
- { name: "RSASSA-PKCS1-v1_5" },
487
- this.privateKey,
488
- data
489
- );
490
- // Base64 encode signature
491
- signature = this._toBase64(new Uint8Array(sigBuffer));
492
- } catch (err) {
493
- throw new Error(`Failed to sign action: ${err.message}`);
494
- }
495
- }
496
-
497
- const res = await this._request('/api/actions', 'POST', {
498
- ...payload,
499
- _signature: signature
500
- });
501
-
502
- // Handle HITL Approval
503
- if (res.action?.status === 'pending_approval' && this.hitlMode === 'wait') {
504
- console.log(`[DashClaw] Action ${res.action_id} requires human approval. Waiting...`);
505
- return this.waitForApproval(res.action_id);
506
- }
507
-
508
- return res;
509
- }
510
-
511
- /**
512
- * Poll for human approval of a pending action.
513
- * @param {string} actionId
514
- * @param {Object} [options]
515
- * @param {number} [options.timeout=300000] - Max wait time (5 min)
516
- * @param {number} [options.interval=5000] - Poll interval
517
- * @param {boolean} [options.useEvents=false] - Use SSE stream instead of polling
518
- */
519
- async waitForApproval(actionId, { timeout = 300000, interval = 5000, useEvents = false } = {}) {
520
- if (!useEvents) {
521
- return this._waitForApprovalPolling(actionId, timeout, interval);
522
- }
523
-
524
- return new Promise((resolve, reject) => {
525
- const stream = this.events();
526
- const timeoutId = setTimeout(() => {
527
- stream.close();
528
- reject(new Error(`Timed out waiting for approval of action ${actionId}`));
529
- }, timeout);
530
-
531
- stream.on('action.updated', (data) => {
532
- if (data.action_id !== actionId) return;
533
- if (data.status === 'running') {
534
- clearTimeout(timeoutId);
535
- stream.close();
536
- resolve({ action: data, action_id: actionId });
537
- } else if (data.status === 'failed' || data.status === 'cancelled') {
538
- clearTimeout(timeoutId);
539
- stream.close();
540
- reject(new ApprovalDeniedError(data.error_message || 'Operator denied the action.'));
541
- }
542
- });
543
-
544
- stream.on('error', (err) => {
545
- clearTimeout(timeoutId);
546
- stream.close();
547
- reject(err);
548
- });
549
- });
550
- }
551
-
552
- /** @private Polling-based waitForApproval implementation. */
553
- async _waitForApprovalPolling(actionId, timeout, interval) {
554
- const startTime = Date.now();
555
-
556
- while (Date.now() - startTime < timeout) {
557
- const { action } = await this.getAction(actionId);
558
-
559
- if (action.status === 'running') {
560
- console.log(`[DashClaw] Action ${actionId} approved by operator.`);
561
- return { action, action_id: actionId };
562
- }
563
-
564
- if (action.status === 'failed' || action.status === 'cancelled') {
565
- throw new ApprovalDeniedError(action.error_message || 'Operator denied the action.');
566
- }
567
-
568
- await new Promise(r => setTimeout(r, interval));
569
- }
570
-
571
- throw new Error(`[DashClaw] Timed out waiting for approval of action ${actionId}`);
572
- }
573
-
574
- /**
575
- * Approve or deny a pending action as a human operator.
576
- * @param {string} actionId - The action ID to approve or deny
577
- * @param {'allow'|'deny'} decision - The approval decision
578
- * @param {string} [reasoning] - Optional reasoning for the decision
579
- * @returns {Promise<{action: Object}>}
580
- */
581
- async approveAction(actionId, decision, reasoning) {
582
- if (!['allow', 'deny'].includes(decision)) {
583
- throw new Error("decision must be either 'allow' or 'deny'");
584
- }
585
- const payload = { decision };
586
- if (reasoning !== undefined) payload.reasoning = reasoning;
587
- return this._request(`/api/actions/${encodeURIComponent(actionId)}/approve`, 'POST', payload);
588
- }
589
-
590
- /**
591
- * Get all actions currently pending human approval.
592
- * @param {Object} [params]
593
- * @param {number} [params.limit=20]
594
- * @param {number} [params.offset=0]
595
- * @returns {Promise<{actions: Object[], total: number}>}
596
- */
597
- async getPendingApprovals({ limit = 20, offset = 0 } = {}) {
598
- return this.getActions({ status: 'pending_approval', limit, offset });
599
- }
600
-
601
- // ══════════════════════════════════════════════
602
- // Real-Time Events (1 method)
603
- // ══════════════════════════════════════════════
604
-
605
- /**
606
- * Subscribe to real-time SSE events from the DashClaw server.
607
- * Uses fetch-based SSE parsing for Node 18+ compatibility (no native EventSource required).
608
- *
609
- * @param {Object} [options]
610
- * @param {boolean} [options.reconnect=true] - Auto-reconnect on disconnect (resumes from last event ID)
611
- * @param {number} [options.maxRetries=Infinity] - Max reconnection attempts before giving up
612
- * @param {number} [options.retryInterval=3000] - Milliseconds between reconnection attempts
613
- * @returns {{ on(eventType: string, callback: Function): this, close(): void, _promise: Promise<void> }}
614
- *
615
- * @example
616
- * const stream = client.events();
617
- * stream
618
- * .on('action.created', (data) => console.log('New action:', data))
619
- * .on('action.updated', (data) => console.log('Action updated:', data))
620
- * .on('loop.created', (data) => console.log('New loop:', data))
621
- * .on('loop.updated', (data) => console.log('Loop updated:', data))
622
- * .on('goal.created', (data) => console.log('New goal:', data))
623
- * .on('goal.updated', (data) => console.log('Goal updated:', data))
624
- * .on('policy.updated', (data) => console.log('Policy changed:', data))
625
- * .on('task.assigned', (data) => console.log('Task assigned:', data))
626
- * .on('task.completed', (data) => console.log('Task done:', data))
627
- * .on('reconnecting', ({ attempt }) => console.log(`Reconnecting #${attempt}...`))
628
- * .on('error', (err) => console.error('Stream error:', err));
629
- *
630
- * // Later:
631
- * stream.close();
632
- */
633
- events({ reconnect = true, maxRetries = Infinity, retryInterval = 3000 } = {}) {
634
- const url = `${this.baseUrl}/api/stream`;
635
- const apiKey = this.apiKey;
636
-
637
- const handlers = new Map();
638
- let closed = false;
639
- let controller = null;
640
- let lastEventId = null;
641
- let retryCount = 0;
642
-
643
- const emit = (eventType, data) => {
644
- const cbs = handlers.get(eventType) || [];
645
- for (const cb of cbs) {
646
- try { cb(data); } catch { /* ignore handler errors */ }
647
- }
648
- };
649
-
650
- const connect = async () => {
651
- controller = new AbortController();
652
- const headers = { 'x-api-key': apiKey };
653
- if (lastEventId) headers['last-event-id'] = lastEventId;
654
-
655
- const res = await fetch(url, {
656
- headers,
657
- signal: controller.signal,
658
- });
659
-
660
- if (!res.ok) {
661
- throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);
662
- }
663
-
664
- retryCount = 0; // Reset on successful connection
665
-
666
- const reader = res.body.getReader();
667
- const decoder = new TextDecoder();
668
- let buffer = '';
669
- // Persist across reads so frames split across chunks are handled correctly
670
- let currentEvent = null;
671
- let currentData = '';
672
-
673
- while (!closed) {
674
- const { done, value } = await reader.read();
675
- if (done) break;
676
- buffer += decoder.decode(value, { stream: true });
677
-
678
- // Parse SSE frames from buffer
679
- const lines = buffer.split('\n');
680
- buffer = lines.pop(); // Keep incomplete line in buffer
681
-
682
- for (const line of lines) {
683
- if (line.startsWith('id: ')) {
684
- lastEventId = line.slice(4).trim();
685
- } else if (line.startsWith('event: ')) {
686
- currentEvent = line.slice(7).trim();
687
- } else if (line.startsWith('data: ')) {
688
- currentData += line.slice(6);
689
- } else if (line.startsWith(':')) {
690
- // SSE comment (keepalive heartbeat). Ignore.
691
- } else if (line === '' && currentEvent) {
692
- // End of SSE frame. Dispatch.
693
- if (currentData) {
694
- try {
695
- const parsed = JSON.parse(currentData);
696
- emit(currentEvent, parsed);
697
- } catch { /* ignore parse errors */ }
698
- }
699
- currentEvent = null;
700
- currentData = '';
701
- } else if (line === '') {
702
- // Blank line without a pending event. Reset partial state.
703
- currentEvent = null;
704
- currentData = '';
705
- }
706
- }
707
- }
708
- };
709
-
710
- const connectLoop = async () => {
711
- while (!closed) {
712
- try {
713
- await connect();
714
- } catch (err) {
715
- if (closed) return;
716
- emit('error', err);
717
- }
718
- // Stream ended (server closed, network drop, etc.)
719
- if (closed) return;
720
- if (!reconnect || retryCount >= maxRetries) {
721
- emit('error', new Error('SSE stream ended'));
722
- return;
723
- }
724
- retryCount++;
725
- emit('reconnecting', { attempt: retryCount, maxRetries });
726
- await new Promise((r) => setTimeout(r, retryInterval));
727
- }
728
- };
729
-
730
- const connectionPromise = connectLoop();
731
-
732
- const handle = {
733
- on(eventType, callback) {
734
- if (!handlers.has(eventType)) handlers.set(eventType, []);
735
- handlers.get(eventType).push(callback);
736
- return handle;
737
- },
738
- close() {
739
- closed = true;
740
- if (controller) controller.abort();
741
- },
742
- _promise: connectionPromise,
743
- };
744
-
745
- return handle;
746
- }
747
-
748
- /**
749
- * Report agent presence and health.
750
- * @param {Object} [options]
751
- * @param {'online'|'busy'|'error'} [options.status='online']
752
- * @param {string} [options.currentTaskId]
753
- * @param {Object} [options.metadata]
754
- * @returns {Promise<{status: string, timestamp: string}>}
755
- */
756
- async heartbeat({ status = 'online', currentTaskId, metadata } = {}) {
757
- return this._request('/api/agents/heartbeat', 'POST', {
758
- agent_id: this.agentId,
759
- agent_name: this.agentName,
760
- status,
761
- current_task_id: currentTaskId,
762
- metadata,
763
- });
764
- }
765
-
766
- /**
767
- * Start an automatic heartbeat timer.
768
- * @param {Object} [options]
769
- * @param {number} [options.interval=60000] - Interval in ms
770
- */
771
- startHeartbeat(options = {}) {
772
- if (this._heartbeatTimer) return;
773
- const interval = options.interval || 60000;
774
- this.heartbeat(options).catch(() => {}); // Initial heartbeat
775
- this._heartbeatTimer = setInterval(() => {
776
- this.heartbeat(options).catch(() => {});
777
- }, interval);
778
- }
779
-
780
- /**
781
- * Stop the automatic heartbeat timer.
782
- */
783
- stopHeartbeat() {
784
- if (this._heartbeatTimer) {
785
- clearInterval(this._heartbeatTimer);
786
- this._heartbeatTimer = null;
787
- }
788
- }
789
-
790
- /**
791
- * Update the outcome of an existing action.
792
- * @param {string} actionId - The action_id to update
793
- * @param {Object} outcome
794
- * @param {string} [outcome.status] - New status: completed, failed, cancelled
795
- * @param {string} [outcome.output_summary] - What happened
796
- * @param {string[]} [outcome.side_effects] - Unintended consequences
797
- * @param {string[]} [outcome.artifacts_created] - Files, records, etc. created
798
- * @param {string} [outcome.error_message] - Error details if failed
799
- * @param {number} [outcome.duration_ms] - How long it took
800
- * @param {number} [outcome.cost_estimate] - Estimated cost in USD
801
- * @returns {Promise<{action: Object}>}
802
- */
803
- async updateOutcome(actionId, outcome) {
804
- return this._request(`/api/actions/${actionId}`, 'PATCH', {
805
- ...outcome,
806
- timestamp_end: outcome.timestamp_end || new Date().toISOString()
807
- });
808
- }
809
-
810
- /**
811
- * Get a list of actions with optional filters.
812
- * @param {Object} [filters]
813
- * @param {string} [filters.agent_id] - Filter by agent
814
- * @param {string} [filters.swarm_id] - Filter by swarm
815
- * @param {string} [filters.status] - Filter by status
816
- * @param {string} [filters.action_type] - Filter by type
817
- * @param {number} [filters.risk_min] - Minimum risk score
818
- * @param {number} [filters.limit=50] - Max results
819
- * @param {number} [filters.offset=0] - Pagination offset
820
- * @returns {Promise<{actions: Object[], total: number, stats: Object}>}
821
- */
822
- async getActions(filters = {}) {
823
- const params = new URLSearchParams();
824
- for (const [key, value] of Object.entries(filters)) {
825
- if (value !== undefined && value !== null && value !== '') {
826
- params.set(key, String(value));
827
- }
828
- }
829
- return this._request(`/api/actions?${params}`, 'GET');
830
- }
831
-
832
- /**
833
- * Get a single action with its open loops and assumptions.
834
- * @param {string} actionId
835
- * @returns {Promise<{action: Object, open_loops: Object[], assumptions: Object[]}>}
836
- */
837
- async getAction(actionId) {
838
- return this._request(`/api/actions/${actionId}`, 'GET');
839
- }
840
-
841
- /**
842
- * Get root-cause trace for an action.
843
- * @param {string} actionId
844
- * @returns {Promise<{action: Object, trace: Object}>}
845
- */
846
- async getActionTrace(actionId) {
847
- return this._request(`/api/actions/${actionId}/trace`, 'GET');
848
- }
849
-
850
- /**
851
- * Helper: Create an action, run a function, and auto-update the outcome.
852
- * @param {Object} actionDef - Action definition (same as createAction)
853
- * @param {Function} fn - Async function to execute. Receives { action_id } as argument.
854
- * @returns {Promise<*>} - The return value of fn
855
- */
856
- async track(actionDef, fn) {
857
- const startTime = Date.now();
858
- const { action_id } = await this.createAction(actionDef);
859
-
860
- try {
861
- const result = await fn({ action_id });
862
- await this.updateOutcome(action_id, {
863
- status: 'completed',
864
- duration_ms: Date.now() - startTime,
865
- output_summary: typeof result === 'string' ? result : JSON.stringify(result)
866
- });
867
- return result;
868
- } catch (error) {
869
- await this.updateOutcome(action_id, {
870
- status: 'failed',
871
- duration_ms: Date.now() - startTime,
872
- error_message: error.message || String(error)
873
- }).catch(() => {}); // Don't throw if outcome update fails
874
- throw error;
875
- }
876
- }
877
-
878
- // ══════════════════════════════════════════════
879
- // Category 2: Decision Integrity (Loops & Assumptions) (7 methods)
880
- // ══════════════════════════════════════════════
881
-
882
- /**
883
- * Register an unresolved dependency for a decision. Open loops track work that must be completed before the decision can be considered fully resolved.
884
- * @param {Object} loop
885
- * @param {string} loop.action_id - Parent action ID
886
- * @param {string} loop.loop_type - One of: followup, question, dependency, approval, review, handoff, other
887
- * @param {string} loop.description - What needs to be resolved
888
- * @param {string} [loop.priority='medium'] - One of: low, medium, high, critical
889
- * @param {string} [loop.owner] - Who is responsible for resolving this
890
- * @returns {Promise<{loop: Object, loop_id: string}>}
891
- */
892
- async registerOpenLoop(loop) {
893
- return this._request('/api/actions/loops', 'POST', loop);
894
- }
895
-
896
- /**
897
- * Resolve or cancel an open loop.
898
- * @param {string} loopId - The loop_id to resolve
899
- * @param {string} status - 'resolved' or 'cancelled'
900
- * @param {string} [resolution] - Resolution description (required when resolving)
901
- * @returns {Promise<{loop: Object}>}
902
- */
903
- async resolveOpenLoop(loopId, status, resolution) {
904
- return this._request(`/api/actions/loops/${loopId}`, 'PATCH', {
905
- status,
906
- resolution
907
- });
908
- }
909
-
910
- /**
911
- * Get open loops with optional filters.
912
- * @param {Object} [filters]
913
- * @param {string} [filters.status] - Filter by status (open, resolved, cancelled)
914
- * @param {string} [filters.loop_type] - Filter by loop type
915
- * @param {string} [filters.priority] - Filter by priority
916
- * @param {number} [filters.limit=50] - Max results
917
- * @returns {Promise<{loops: Object[], total: number, stats: Object}>}
918
- */
919
- async getOpenLoops(filters = {}) {
920
- const params = new URLSearchParams();
921
- for (const [key, value] of Object.entries(filters)) {
922
- if (value !== undefined && value !== null && value !== '') {
923
- params.set(key, String(value));
924
- }
925
- }
926
- return this._request(`/api/actions/loops?${params}`, 'GET');
927
- }
928
-
929
- /**
930
- * Register assumptions underlying a decision. Assumptions are the decision basis. They must be validated or invalidated to maintain decision integrity.
931
- * @param {Object} assumption
932
- * @param {string} assumption.action_id - Parent action ID
933
- * @param {string} assumption.assumption - The assumption being made
934
- * @param {string} [assumption.basis] - Evidence or reasoning for the assumption
935
- * @param {boolean} [assumption.validated=false] - Whether this has been validated
936
- * @returns {Promise<{assumption: Object, assumption_id: string}>}
937
- */
938
- async registerAssumption(assumption) {
939
- return this._request('/api/actions/assumptions', 'POST', assumption);
940
- }
941
-
942
- /**
943
- * Get a single assumption by ID.
944
- * @param {string} assumptionId
945
- * @returns {Promise<{assumption: Object}>}
946
- */
947
- async getAssumption(assumptionId) {
948
- return this._request(`/api/actions/assumptions/${assumptionId}`, 'GET');
949
- }
950
-
951
- /**
952
- * Validate or invalidate an assumption.
953
- * @param {string} assumptionId - The assumption_id to update
954
- * @param {boolean} validated - true to validate, false to invalidate
955
- * @param {string} [invalidated_reason] - Required when invalidating
956
- * @returns {Promise<{assumption: Object}>}
957
- */
958
- async validateAssumption(assumptionId, validated, invalidated_reason) {
959
- if (typeof validated !== 'boolean') throw new Error('validated must be a boolean');
960
- if (validated === false && !invalidated_reason) {
961
- throw new Error('invalidated_reason is required when invalidating an assumption');
962
- }
963
- const body = { validated };
964
- if (invalidated_reason !== undefined) body.invalidated_reason = invalidated_reason;
965
- return this._request(`/api/actions/assumptions/${assumptionId}`, 'PATCH', body);
966
- }
967
-
968
- /**
969
- * Get drift report for assumptions with risk scoring.
970
- * @param {Object} [filters]
971
- * @param {string} [filters.action_id] - Filter by action
972
- * @param {number} [filters.limit=50] - Max results
973
- * @returns {Promise<{assumptions: Object[], drift_summary: Object}>}
974
- */
975
- async getDriftReport(filters = {}) {
976
- const params = new URLSearchParams({ drift: 'true' });
977
- for (const [key, value] of Object.entries(filters)) {
978
- if (value !== undefined && value !== null && value !== '') {
979
- params.set(key, String(value));
980
- }
981
- }
982
- return this._request(`/api/actions/assumptions?${params}`, 'GET');
983
- }
984
-
985
- // ══════════════════════════════════════════════
986
- // Category 3: Decision Integrity Signals (1 method)
987
- // ══════════════════════════════════════════════
988
-
989
- /**
990
- * Get current decision integrity signals. Returns autonomy breaches, logic drift, and governance violations.
991
- * @returns {Promise<{signals: Object[], counts: {red: number, amber: number, total: number}}>}
992
- */
993
- async getSignals() {
994
- return this._request('/api/actions/signals', 'GET');
995
- }
996
-
997
- // ══════════════════════════════════════════════
998
- // Category 4: Dashboard Data (9 methods)
999
- // ══════════════════════════════════════════════
1000
-
1001
- /**
1002
- * Report token usage snapshot (disabled in dashboard, API still functional).
1003
- * @param {Object} usage
1004
- * @param {number} usage.tokens_in - Input tokens consumed
1005
- * @param {number} usage.tokens_out - Output tokens generated
1006
- * @param {number} [usage.context_used] - Context window tokens used
1007
- * @param {number} [usage.context_max] - Context window max capacity
1008
- * @param {string} [usage.model] - Model name
1009
- * @returns {Promise<{snapshot: Object}>}
1010
- */
1011
- async reportTokenUsage(usage) {
1012
- return this._request('/api/tokens', 'POST', {
1013
- ...usage,
1014
- agent_id: this.agentId
1015
- });
1016
- }
1017
-
1018
- /**
1019
- * Internal: fire-and-forget token report extracted from an LLM response.
1020
- * @private
1021
- */
1022
- async _reportTokenUsageFromLLM({ tokens_in, tokens_out, model }) {
1023
- if (tokens_in == null && tokens_out == null) return;
1024
- try {
1025
- await this._request('/api/tokens', 'POST', {
1026
- tokens_in: tokens_in || 0,
1027
- tokens_out: tokens_out || 0,
1028
- model: model || undefined,
1029
- agent_id: this.agentId,
1030
- });
1031
- } catch (_) {
1032
- // fire-and-forget: never let telemetry break the caller
1033
- }
1034
- }
1035
-
1036
- /**
1037
- * Wrap an Anthropic or OpenAI client to auto-report token usage after each call.
1038
- * Returns the same client instance (mutated) for fluent usage.
1039
- *
1040
- * @param {Object} llmClient - An Anthropic or OpenAI SDK client instance
1041
- * @param {Object} [options]
1042
- * @param {'anthropic'|'openai'} [options.provider] - Force provider detection
1043
- * @returns {Object} The wrapped client
1044
- *
1045
- * @example
1046
- * const anthropic = claw.wrapClient(new Anthropic());
1047
- * const msg = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [...] });
1048
- * // Token usage is auto-reported to DashClaw
1049
- */
1050
- wrapClient(llmClient, { provider } = {}) {
1051
- if (llmClient._dashclawWrapped) return llmClient;
1052
-
1053
- const detected = provider
1054
- || (llmClient.messages?.create ? 'anthropic' : null)
1055
- || (llmClient.chat?.completions?.create ? 'openai' : null);
1056
-
1057
- if (!detected) {
1058
- throw new Error(
1059
- 'DashClaw.wrapClient: unable to detect provider. Pass { provider: "anthropic" } or { provider: "openai" }.'
1060
- );
1061
- }
1062
-
1063
- if (detected === 'anthropic') {
1064
- const original = llmClient.messages.create.bind(llmClient.messages);
1065
- llmClient.messages.create = async (...args) => {
1066
- const response = await original(...args);
1067
- this._reportTokenUsageFromLLM({
1068
- tokens_in: response?.usage?.input_tokens ?? null,
1069
- tokens_out: response?.usage?.output_tokens ?? null,
1070
- model: response?.model ?? null,
1071
- });
1072
- return response;
1073
- };
1074
- } else if (detected === 'openai') {
1075
- const original = llmClient.chat.completions.create.bind(llmClient.chat.completions);
1076
- llmClient.chat.completions.create = async (...args) => {
1077
- const response = await original(...args);
1078
- this._reportTokenUsageFromLLM({
1079
- tokens_in: response?.usage?.prompt_tokens ?? null,
1080
- tokens_out: response?.usage?.completion_tokens ?? null,
1081
- model: response?.model ?? null,
1082
- });
1083
- return response;
1084
- };
1085
- }
1086
-
1087
- llmClient._dashclawWrapped = true;
1088
- return llmClient;
1089
- }
1090
-
1091
- /**
1092
- * Record a decision for the learning database.
1093
- * @param {Object} entry
1094
- * @param {string} entry.decision - What was decided
1095
- * @param {string} [entry.context] - Context around the decision
1096
- * @param {string} [entry.reasoning] - Why this decision was made
1097
- * @param {string} [entry.outcome] - 'success', 'failure', or 'pending'
1098
- * @param {number} [entry.confidence] - Confidence level 0-100
1099
- * @returns {Promise<{decision: Object}>}
1100
- */
1101
- async recordDecision(entry) {
1102
- return this._request('/api/learning', 'POST', {
1103
- ...entry,
1104
- agent_id: this.agentId
1105
- });
1106
- }
1107
-
1108
- /**
1109
- * Get adaptive learning recommendations derived from prior episodes.
1110
- * @param {Object} [filters]
1111
- * @param {string} [filters.action_type] - Filter by action type
1112
- * @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
1113
- * @param {boolean} [filters.include_inactive] - Include disabled recommendations (admin/service only)
1114
- * @param {boolean} [filters.track_events=true] - Record recommendation fetched telemetry
1115
- * @param {boolean} [filters.include_metrics] - Include computed metrics in the response payload
1116
- * @param {number} [filters.limit=50] - Max recommendations to return
1117
- * @param {number} [filters.lookback_days=30] - Lookback days used when include_metrics=true
1118
- * @returns {Promise<{recommendations: Object[], metrics?: Object, total: number, lastUpdated: string}>}
1119
- */
1120
- async getRecommendations(filters = {}) {
1121
- const params = new URLSearchParams({
1122
- agent_id: filters.agent_id || this.agentId,
1123
- });
1124
- if (filters.action_type) params.set('action_type', filters.action_type);
1125
- if (filters.limit) params.set('limit', String(filters.limit));
1126
- if (filters.include_inactive) params.set('include_inactive', 'true');
1127
- if (filters.track_events !== false) params.set('track_events', 'true');
1128
- if (filters.include_metrics) params.set('include_metrics', 'true');
1129
- if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
1130
- return this._request(`/api/learning/recommendations?${params}`, 'GET');
1131
- }
1132
-
1133
- /**
1134
- * Get recommendation effectiveness metrics and telemetry aggregates.
1135
- * @param {Object} [filters]
1136
- * @param {string} [filters.action_type] - Filter by action type
1137
- * @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
1138
- * @param {number} [filters.lookback_days=30] - Lookback window for episodes/events
1139
- * @param {number} [filters.limit=100] - Max recommendations considered
1140
- * @param {boolean} [filters.include_inactive] - Include inactive recommendations (admin/service only)
1141
- * @returns {Promise<{metrics: Object[], summary: Object, lookback_days: number, lastUpdated: string}>}
1142
- */
1143
- async getRecommendationMetrics(filters = {}) {
1144
- const params = new URLSearchParams({
1145
- agent_id: filters.agent_id || this.agentId,
1146
- });
1147
- if (filters.action_type) params.set('action_type', filters.action_type);
1148
- if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
1149
- if (filters.limit) params.set('limit', String(filters.limit));
1150
- if (filters.include_inactive) params.set('include_inactive', 'true');
1151
- return this._request(`/api/learning/recommendations/metrics?${params}`, 'GET');
1152
- }
1153
-
1154
- /**
1155
- * Record recommendation telemetry events (single event or batch).
1156
- * @param {Object|Object[]} events
1157
- * @returns {Promise<{created: Object[], created_count: number}>}
1158
- */
1159
- async recordRecommendationEvents(events) {
1160
- if (Array.isArray(events)) {
1161
- return this._request('/api/learning/recommendations/events', 'POST', { events });
1162
- }
1163
- return this._request('/api/learning/recommendations/events', 'POST', events || {});
1164
- }
1165
-
1166
- /**
1167
- * Enable or disable a recommendation.
1168
- * @param {string} recommendationId - Recommendation ID
1169
- * @param {boolean} active - Desired active state
1170
- * @returns {Promise<{recommendation: Object}>}
1171
- */
1172
- async setRecommendationActive(recommendationId, active) {
1173
- return this._request(`/api/learning/recommendations/${recommendationId}`, 'PATCH', { active: !!active });
1174
- }
1175
-
1176
- /**
1177
- * Rebuild recommendations from scored learning episodes.
1178
- * @param {Object} [options]
1179
- * @param {string} [options.action_type] - Scope rebuild to one action type
1180
- * @param {string} [options.agent_id] - Override agent_id (defaults to SDK agent)
1181
- * @param {number} [options.lookback_days=30] - Days of episode history to analyze
1182
- * @param {number} [options.min_samples=5] - Minimum episodes required per recommendation
1183
- * @param {number} [options.episode_limit=5000] - Episode scan cap
1184
- * @param {string} [options.action_id] - Optionally score this action before rebuild
1185
- * @returns {Promise<{recommendations: Object[], total: number, episodes_scanned: number}>}
1186
- */
1187
- async rebuildRecommendations(options = {}) {
1188
- return this._request('/api/learning/recommendations', 'POST', {
1189
- agent_id: options.agent_id || this.agentId,
1190
- action_type: options.action_type,
1191
- lookback_days: options.lookback_days,
1192
- min_samples: options.min_samples,
1193
- episode_limit: options.episode_limit,
1194
- action_id: options.action_id,
1195
- });
1196
- }
1197
-
1198
- /**
1199
- * Apply top recommendation hints to an action definition (non-destructive).
1200
- * @param {Object} action - Action payload compatible with createAction()
1201
- * @returns {Promise<{action: Object, recommendation: Object|null, adapted_fields: string[]}>}
1202
- */
1203
- async recommendAction(action) {
1204
- if (!action?.action_type) {
1205
- return { action, recommendation: null, adapted_fields: [] };
1206
- }
1207
-
1208
- const response = await this.getRecommendations({ action_type: action.action_type, limit: 1 });
1209
- const recommendation = response.recommendations?.[0] || null;
1210
- if (!recommendation) {
1211
- return { action, recommendation: null, adapted_fields: [] };
1212
- }
1213
-
1214
- const adapted = { ...action };
1215
- const adaptedFields = [];
1216
- const hints = recommendation.hints || {};
1217
-
1218
- if (
1219
- typeof hints.preferred_risk_cap === 'number' &&
1220
- (adapted.risk_score === undefined || adapted.risk_score > hints.preferred_risk_cap)
1221
- ) {
1222
- adapted.risk_score = hints.preferred_risk_cap;
1223
- adaptedFields.push('risk_score');
1224
- }
1225
-
1226
- if (hints.prefer_reversible === true && adapted.reversible === undefined) {
1227
- adapted.reversible = true;
1228
- adaptedFields.push('reversible');
1229
- }
1230
-
1231
- if (
1232
- typeof hints.confidence_floor === 'number' &&
1233
- (adapted.confidence === undefined || adapted.confidence < hints.confidence_floor)
1234
- ) {
1235
- adapted.confidence = hints.confidence_floor;
1236
- adaptedFields.push('confidence');
1237
- }
1238
-
1239
- return {
1240
- action: adapted,
1241
- recommendation,
1242
- adapted_fields: adaptedFields,
1243
- };
1244
- }
1245
-
1246
- /**
1247
- * Create a goal.
1248
- * @param {Object} goal
1249
- * @param {string} goal.title - Goal title
1250
- * @param {string} [goal.category] - Goal category
1251
- * @param {string} [goal.description] - Detailed description
1252
- * @param {string} [goal.target_date] - Target completion date (ISO string)
1253
- * @param {number} [goal.progress] - Progress 0-100
1254
- * @param {string} [goal.status] - 'active', 'completed', 'paused'
1255
- * @returns {Promise<{goal: Object}>}
1256
- */
1257
- async createGoal(goal) {
1258
- return this._request('/api/goals', 'POST', {
1259
- ...goal,
1260
- agent_id: this.agentId
1261
- });
1262
- }
1263
-
1264
- /**
1265
- * Record content creation.
1266
- * @param {Object} content
1267
- * @param {string} content.title - Content title
1268
- * @param {string} [content.platform] - Platform (e.g., 'linkedin', 'twitter')
1269
- * @param {string} [content.status] - 'draft' or 'published'
1270
- * @param {string} [content.url] - Published URL
1271
- * @returns {Promise<{content: Object}>}
1272
- */
1273
- async recordContent(content) {
1274
- return this._request('/api/content', 'POST', {
1275
- ...content,
1276
- agent_id: this.agentId
1277
- });
1278
- }
1279
-
1280
- /**
1281
- * Record a relationship interaction.
1282
- * @param {Object} interaction
1283
- * @param {string} interaction.summary - What happened
1284
- * @param {string} [interaction.contact_name] - Contact name (auto-resolves to contact_id)
1285
- * @param {string} [interaction.contact_id] - Direct contact ID
1286
- * @param {string} [interaction.direction] - 'inbound' or 'outbound'
1287
- * @param {string} [interaction.type] - Interaction type
1288
- * @param {string} [interaction.platform] - Platform used
1289
- * @returns {Promise<{interaction: Object}>}
1290
- */
1291
- async recordInteraction(interaction) {
1292
- return this._request('/api/relationships', 'POST', {
1293
- ...interaction,
1294
- agent_id: this.agentId
1295
- });
1296
- }
1297
-
1298
- /**
1299
- * Create a calendar event.
1300
- * @param {Object} event
1301
- * @param {string} event.summary - Event title/summary
1302
- * @param {string} event.start_time - Start time (ISO string)
1303
- * @param {string} [event.end_time] - End time (ISO string)
1304
- * @param {string} [event.location] - Event location
1305
- * @param {string} [event.description] - Event description
1306
- * @returns {Promise<{event: Object}>}
1307
- */
1308
- async createCalendarEvent(event) {
1309
- return this._request('/api/calendar', 'POST', event);
1310
- }
1311
-
1312
- /**
1313
- * Record an idea/inspiration.
1314
- * @param {Object} idea
1315
- * @param {string} idea.title - Idea title
1316
- * @param {string} [idea.description] - Detailed description
1317
- * @param {string} [idea.category] - Category
1318
- * @param {number} [idea.score] - Priority/quality score 0-100
1319
- * @param {string} [idea.status] - 'pending', 'in_progress', 'shipped', 'rejected'
1320
- * @param {string} [idea.source] - Where this idea came from
1321
- * @returns {Promise<{idea: Object}>}
1322
- */
1323
- async recordIdea(idea) {
1324
- return this._request('/api/inspiration', 'POST', idea);
1325
- }
1326
-
1327
- /**
1328
- * Report memory health snapshot with entities and topics.
1329
- * @param {Object} report
1330
- * @param {Object} report.health - Health metrics
1331
- * @param {number} report.health.score - Health score 0-100
1332
- * @param {Object[]} [report.entities] - Key entities found in memory
1333
- * @param {Object[]} [report.topics] - Topics/themes found in memory
1334
- * @returns {Promise<{snapshot: Object, entities_count: number, topics_count: number}>}
1335
- */
1336
- async reportMemoryHealth(report) {
1337
- return this._request('/api/memory', 'POST', report);
1338
- }
1339
-
1340
- /**
1341
- * Report active connections/integrations for this agent.
1342
- * @param {Object[]} connections - Array of connection objects
1343
- * @param {string} connections[].provider - Service name (e.g., 'anthropic', 'github')
1344
- * @param {string} [connections[].authType] - Auth method
1345
- * @param {string} [connections[].planName] - Plan name
1346
- * @param {string} [connections[].status] - Connection status: active, inactive, error
1347
- * @param {Object|string} [connections[].metadata] - Optional metadata
1348
- * @returns {Promise<{connections: Object[], created: number}>}
1349
- */
1350
- async reportConnections(connections) {
1351
- return this._request('/api/agents/connections', 'POST', {
1352
- agent_id: this.agentId,
1353
- connections: connections.map(c => ({
1354
- provider: c.provider,
1355
- auth_type: c.authType || c.auth_type || 'api_key',
1356
- plan_name: c.planName || c.plan_name || null,
1357
- status: c.status || 'active',
1358
- metadata: c.metadata || null
1359
- }))
1360
- });
1361
- }
1362
-
1363
- // ══════════════════════════════════════════════
1364
- // Category 5: Session Handoffs (3 methods)
1365
- // ══════════════════════════════════════════════
1366
-
1367
- /**
1368
- * Create a session handoff document.
1369
- * @param {Object} handoff
1370
- * @param {string} handoff.summary - Session summary
1371
- * @param {string} [handoff.session_date] - Date string (defaults to today)
1372
- * @param {string[]} [handoff.key_decisions] - Key decisions made
1373
- * @param {string[]} [handoff.open_tasks] - Tasks still open
1374
- * @param {string} [handoff.mood_notes] - Mood/energy observations
1375
- * @param {string[]} [handoff.next_priorities] - What to focus on next
1376
- * @returns {Promise<{handoff: Object, handoff_id: string}>}
1377
- */
1378
- async createHandoff(handoff) {
1379
- return this._request('/api/handoffs', 'POST', {
1380
- agent_id: this.agentId,
1381
- ...handoff
1382
- });
1383
- }
1384
-
1385
- /**
1386
- * Get handoffs with optional filters.
1387
- * @param {Object} [filters]
1388
- * @param {string} [filters.date] - Filter by session_date
1389
- * @param {number} [filters.limit] - Max results
1390
- * @returns {Promise<{handoffs: Object[], total: number}>}
1391
- */
1392
- async getHandoffs(filters = {}) {
1393
- const params = new URLSearchParams({ agent_id: this.agentId });
1394
- if (filters.date) params.set('date', filters.date);
1395
- if (filters.limit) params.set('limit', String(filters.limit));
1396
- return this._request(`/api/handoffs?${params}`, 'GET');
1397
- }
1398
-
1399
- /**
1400
- * Get the most recent handoff for this agent.
1401
- * @returns {Promise<{handoff: Object|null}>}
1402
- */
1403
- async getLatestHandoff() {
1404
- return this._request(`/api/handoffs?agent_id=${this.agentId}&latest=true`, 'GET');
1405
- }
1406
-
1407
- // ══════════════════════════════════════════════
1408
- // Category 6: Context Manager (7 methods)
1409
- // ══════════════════════════════════════════════
1410
-
1411
- /**
1412
- * Capture a key point from the current session.
1413
- * @param {Object} point
1414
- * @param {string} point.content - The key point content
1415
- * @param {string} [point.category] - One of: decision, task, insight, question, general
1416
- * @param {number} [point.importance] - Importance 1-10 (default 5)
1417
- * @param {string} [point.session_date] - Date string (defaults to today)
1418
- * @returns {Promise<{point: Object, point_id: string}>}
1419
- */
1420
- async captureKeyPoint(point) {
1421
- return this._request('/api/context/points', 'POST', {
1422
- agent_id: this.agentId,
1423
- ...point
1424
- });
1425
- }
1426
-
1427
- /**
1428
- * Get key points with optional filters.
1429
- * @param {Object} [filters]
1430
- * @param {string} [filters.category] - Filter by category
1431
- * @param {string} [filters.session_date] - Filter by date
1432
- * @param {number} [filters.limit] - Max results
1433
- * @returns {Promise<{points: Object[], total: number}>}
1434
- */
1435
- async getKeyPoints(filters = {}) {
1436
- const params = new URLSearchParams({ agent_id: this.agentId });
1437
- if (filters.category) params.set('category', filters.category);
1438
- if (filters.session_date) params.set('session_date', filters.session_date);
1439
- if (filters.limit) params.set('limit', String(filters.limit));
1440
- return this._request(`/api/context/points?${params}`, 'GET');
1441
- }
1442
-
1443
- /**
1444
- * Create a context thread for tracking a topic across entries.
1445
- * @param {Object} thread
1446
- * @param {string} thread.name - Thread name (unique per agent per org)
1447
- * @param {string} [thread.summary] - Initial summary
1448
- * @returns {Promise<{thread: Object, thread_id: string}>}
1449
- */
1450
- async createThread(thread) {
1451
- return this._request('/api/context/threads', 'POST', {
1452
- agent_id: this.agentId,
1453
- ...thread
1454
- });
1455
- }
1456
-
1457
- /**
1458
- * Add an entry to an existing thread.
1459
- * @param {string} threadId - The thread ID
1460
- * @param {string} content - Entry content
1461
- * @param {string} [entryType] - Entry type (default: 'note')
1462
- * @returns {Promise<{entry: Object, entry_id: string}>}
1463
- */
1464
- async addThreadEntry(threadId, content, entryType) {
1465
- return this._request(`/api/context/threads/${threadId}/entries`, 'POST', {
1466
- content,
1467
- entry_type: entryType || 'note'
1468
- });
1469
- }
1470
-
1471
- /**
1472
- * Close a thread with an optional summary.
1473
- * @param {string} threadId - The thread ID
1474
- * @param {string} [summary] - Final summary
1475
- * @returns {Promise<{thread: Object}>}
1476
- */
1477
- async closeThread(threadId, summary) {
1478
- const body = { status: 'closed' };
1479
- if (summary) body.summary = summary;
1480
- return this._request(`/api/context/threads/${threadId}`, 'PATCH', body);
1481
- }
1482
-
1483
- /**
1484
- * Get threads with optional filters.
1485
- * @param {Object} [filters]
1486
- * @param {string} [filters.status] - Filter by status (active, closed)
1487
- * @param {number} [filters.limit] - Max results
1488
- * @returns {Promise<{threads: Object[], total: number}>}
1489
- */
1490
- async getThreads(filters = {}) {
1491
- const params = new URLSearchParams({ agent_id: this.agentId });
1492
- if (filters.status) params.set('status', filters.status);
1493
- if (filters.limit) params.set('limit', String(filters.limit));
1494
- return this._request(`/api/context/threads?${params}`, 'GET');
1495
- }
1496
-
1497
- /**
1498
- * Get a combined context summary: today's key points + active threads.
1499
- * @returns {Promise<{points: Object[], threads: Object[]}>}
1500
- */
1501
- async getContextSummary() {
1502
- const today = new Date().toISOString().split('T')[0];
1503
- const [pointsResult, threadsResult] = await Promise.all([
1504
- this.getKeyPoints({ session_date: today }),
1505
- this.getThreads({ status: 'active' }),
1506
- ]);
1507
- return {
1508
- points: pointsResult.points,
1509
- threads: threadsResult.threads,
1510
- };
1511
- }
1512
-
1513
- // ══════════════════════════════════════════════
1514
- // Category 7: Automation Snippets (5 methods)
1515
- // ══════════════════════════════════════════════
1516
-
1517
- /**
1518
- * Save or update a reusable code snippet.
1519
- * @param {Object} snippet
1520
- * @param {string} snippet.name - Snippet name (unique per org, upserts on conflict)
1521
- * @param {string} snippet.code - The snippet code
1522
- * @param {string} [snippet.description] - What this snippet does
1523
- * @param {string} [snippet.language] - Programming language
1524
- * @param {string[]} [snippet.tags] - Tags for categorization
1525
- * @returns {Promise<{snippet: Object, snippet_id: string}>}
1526
- */
1527
- async saveSnippet(snippet) {
1528
- return this._request('/api/snippets', 'POST', {
1529
- agent_id: this.agentId,
1530
- ...snippet
1531
- });
1532
- }
1533
-
1534
- /**
1535
- * Search and list snippets.
1536
- * @param {Object} [filters]
1537
- * @param {string} [filters.search] - Search name/description
1538
- * @param {string} [filters.tag] - Filter by tag
1539
- * @param {string} [filters.language] - Filter by language
1540
- * @param {number} [filters.limit] - Max results
1541
- * @returns {Promise<{snippets: Object[], total: number}>}
1542
- */
1543
- async getSnippets(filters = {}) {
1544
- const params = new URLSearchParams();
1545
- if (filters.search) params.set('search', filters.search);
1546
- if (filters.tag) params.set('tag', filters.tag);
1547
- if (filters.language) params.set('language', filters.language);
1548
- if (filters.limit) params.set('limit', String(filters.limit));
1549
- return this._request(`/api/snippets?${params}`, 'GET');
1550
- }
1551
-
1552
- /**
1553
- * Fetch a single snippet by ID.
1554
- * @param {string} snippetId - The snippet ID
1555
- * @returns {Promise<{snippet: Object}>}
1556
- */
1557
- async getSnippet(snippetId) {
1558
- return this._request(`/api/snippets/${snippetId}`, 'GET');
1559
- }
1560
-
1561
- /**
1562
- * Mark a snippet as used (increments use_count).
1563
- * @param {string} snippetId - The snippet ID
1564
- * @returns {Promise<{snippet: Object}>}
1565
- */
1566
- async useSnippet(snippetId) {
1567
- return this._request(`/api/snippets/${snippetId}/use`, 'POST');
1568
- }
1569
-
1570
- /**
1571
- * Delete a snippet.
1572
- * @param {string} snippetId - The snippet ID
1573
- * @returns {Promise<{deleted: boolean, id: string}>}
1574
- */
1575
- async deleteSnippet(snippetId) {
1576
- return this._request(`/api/snippets?id=${snippetId}`, 'DELETE');
1577
- }
1578
-
1579
- // ══════════════════════════════════════════════
1580
- // Category 8: User Preferences (6 methods)
1581
- // ══════════════════════════════════════════════
1582
-
1583
- /**
1584
- * Log a user observation (what you noticed about the user).
1585
- * @param {Object} obs
1586
- * @param {string} obs.observation - The observation text
1587
- * @param {string} [obs.category] - Category tag
1588
- * @param {number} [obs.importance] - Importance 1-10
1589
- * @returns {Promise<{observation: Object, observation_id: string}>}
1590
- */
1591
- async logObservation(obs) {
1592
- return this._request('/api/preferences', 'POST', {
1593
- type: 'observation',
1594
- agent_id: this.agentId,
1595
- ...obs
1596
- });
1597
- }
1598
-
1599
- /**
1600
- * Set a learned user preference.
1601
- * @param {Object} pref
1602
- * @param {string} pref.preference - The preference description
1603
- * @param {string} [pref.category] - Category tag
1604
- * @param {number} [pref.confidence] - Confidence 0-100
1605
- * @returns {Promise<{preference: Object, preference_id: string}>}
1606
- */
1607
- async setPreference(pref) {
1608
- return this._request('/api/preferences', 'POST', {
1609
- type: 'preference',
1610
- agent_id: this.agentId,
1611
- ...pref
1612
- });
1613
- }
1614
-
1615
- /**
1616
- * Log user mood/energy for a session.
1617
- * @param {Object} entry
1618
- * @param {string} entry.mood - Mood description (e.g., 'focused', 'frustrated')
1619
- * @param {string} [entry.energy] - Energy level (e.g., 'high', 'low')
1620
- * @param {string} [entry.notes] - Additional notes
1621
- * @returns {Promise<{mood: Object, mood_id: string}>}
1622
- */
1623
- async logMood(entry) {
1624
- return this._request('/api/preferences', 'POST', {
1625
- type: 'mood',
1626
- agent_id: this.agentId,
1627
- ...entry
1628
- });
1629
- }
1630
-
1631
- /**
1632
- * Track an approach and whether it succeeded or failed.
1633
- * @param {Object} entry
1634
- * @param {string} entry.approach - The approach description
1635
- * @param {string} [entry.context] - Context for when to use this approach
1636
- * @param {boolean} [entry.success] - true = worked, false = failed, undefined = just recording
1637
- * @returns {Promise<{approach: Object, approach_id: string}>}
1638
- */
1639
- async trackApproach(entry) {
1640
- return this._request('/api/preferences', 'POST', {
1641
- type: 'approach',
1642
- agent_id: this.agentId,
1643
- ...entry
1644
- });
1645
- }
1646
-
1647
- /**
1648
- * Get a summary of all user preference data.
1649
- * @returns {Promise<{summary: Object}>}
1650
- */
1651
- async getPreferenceSummary() {
1652
- return this._request(`/api/preferences?type=summary&agent_id=${this.agentId}`, 'GET');
1653
- }
1654
-
1655
- /**
1656
- * Get tracked approaches with success/fail counts.
1657
- * @param {Object} [filters]
1658
- * @param {number} [filters.limit] - Max results
1659
- * @returns {Promise<{approaches: Object[], total: number}>}
1660
- */
1661
- async getApproaches(filters = {}) {
1662
- const params = new URLSearchParams({ type: 'approaches', agent_id: this.agentId });
1663
- if (filters.limit) params.set('limit', String(filters.limit));
1664
- return this._request(`/api/preferences?${params}`, 'GET');
1665
- }
1666
-
1667
- // ══════════════════════════════════════════════
1668
- // Category 9: Daily Digest (1 method)
1669
- // ══════════════════════════════════════════════
1670
-
1671
- /**
1672
- * Get a daily activity digest aggregated from all data sources.
1673
- * @param {string} [date] - Date string YYYY-MM-DD (defaults to today)
1674
- * @returns {Promise<{date: string, digest: Object, summary: Object}>}
1675
- */
1676
- async getDailyDigest(date) {
1677
- const params = new URLSearchParams({ agent_id: this.agentId });
1678
- if (date) params.set('date', date);
1679
- return this._request(`/api/digest?${params}`, 'GET');
1680
- }
1681
-
1682
- // ══════════════════════════════════════════════
1683
- // Category 10: Security Scanning (3 methods)
1684
- // ══════════════════════════════════════════════
1685
-
1686
- /**
1687
- * Scan text for sensitive data (API keys, tokens, PII, etc.).
1688
- * Returns findings and redacted text. Does NOT store the original content.
1689
- * @param {string} text - Text to scan
1690
- * @param {string} [destination] - Where this text is headed (for context)
1691
- * @returns {Promise<{clean: boolean, findings_count: number, findings: Object[], redacted_text: string}>}
1692
- */
1693
- async scanContent(text, destination) {
1694
- return this._request('/api/security/scan', 'POST', {
1695
- text,
1696
- destination,
1697
- agent_id: this.agentId,
1698
- store: false,
1699
- });
1700
- }
1701
-
1702
- /**
1703
- * Scan text and store finding metadata (never the content itself).
1704
- * Use this for audit trails of security scans.
1705
- * @param {string} text - Text to scan
1706
- * @param {string} [destination] - Where this text is headed
1707
- * @returns {Promise<{clean: boolean, findings_count: number, findings: Object[], redacted_text: string}>}
1708
- */
1709
- async reportSecurityFinding(text, destination) {
1710
- return this._request('/api/security/scan', 'POST', {
1711
- text,
1712
- destination,
1713
- agent_id: this.agentId,
1714
- store: true,
1715
- });
1716
- }
1717
-
1718
- /**
1719
- * Scan text for prompt injection attacks (role overrides, delimiter injection,
1720
- * instruction smuggling, data exfiltration attempts, etc.).
1721
- * @param {string} text - Text to scan
1722
- * @param {Object} [options]
1723
- * @param {string} [options.source] - Where this text came from (for context)
1724
- * @returns {Promise<{clean: boolean, risk_level: string, recommendation: string, findings_count: number, critical_count: number, categories: string[], findings: Object[]}>}
1725
- */
1726
- async scanPromptInjection(text, options = {}) {
1727
- return this._request('/api/security/prompt-injection', 'POST', {
1728
- text,
1729
- source: options.source,
1730
- agent_id: this.agentId,
1731
- });
1732
- }
1733
-
1734
- // ══════════════════════════════════════════════
1735
- // Category 11: Agent Messaging (11 methods)
1736
- // ══════════════════════════════════════════════
1737
-
1738
- /**
1739
- * Send a message to another agent or broadcast to all.
1740
- * @param {Object} params
1741
- * @param {string} [params.to] - Target agent ID (omit for broadcast)
1742
- * @param {string} [params.type='info'] - Message type: action|info|lesson|question|status
1743
- * @param {string} [params.subject] - Subject line (max 200 chars)
1744
- * @param {string} params.body - Message body (max 2000 chars)
1745
- * @param {string} [params.threadId] - Thread ID to attach message to
1746
- * @param {boolean} [params.urgent=false] - Mark as urgent
1747
- * @param {string} [params.docRef] - Reference to a shared doc ID
1748
- * @param {Array<{filename: string, mime_type: string, data: string}>} [params.attachments] - File attachments (base64 data, max 3, max 5MB each)
1749
- * @returns {Promise<{message: Object, message_id: string}>}
1750
- */
1751
- async sendMessage({ to, type, subject, body, threadId, urgent, docRef, attachments }) {
1752
- const payload = {
1753
- from_agent_id: this.agentId,
1754
- to_agent_id: to || null,
1755
- message_type: type || 'info',
1756
- subject,
1757
- body,
1758
- thread_id: threadId,
1759
- urgent,
1760
- doc_ref: docRef,
1761
- };
1762
- if (attachments?.length) payload.attachments = attachments;
1763
- return this._request('/api/messages', 'POST', payload);
1764
- }
1765
-
1766
- /**
1767
- * Get inbox messages for this agent.
1768
- * @param {Object} [params]
1769
- * @param {string} [params.type] - Filter by message type
1770
- * @param {boolean} [params.unread] - Only unread messages
1771
- * @param {string} [params.threadId] - Filter by thread
1772
- * @param {number} [params.limit=50] - Max messages to return
1773
- * @returns {Promise<{messages: Object[], total: number, unread_count: number}>}
1774
- */
1775
- async getInbox({ type, unread, threadId, limit } = {}) {
1776
- const params = new URLSearchParams({
1777
- agent_id: this.agentId,
1778
- direction: 'inbox',
1779
- });
1780
- if (type) params.set('type', type);
1781
- if (unread) params.set('unread', 'true');
1782
- if (threadId) params.set('thread_id', threadId);
1783
- if (limit) params.set('limit', String(limit));
1784
- return this._request(`/api/messages?${params}`, 'GET');
1785
- }
1786
-
1787
- /**
1788
- * Mark messages as read.
1789
- * @param {string[]} messageIds - Array of message IDs to mark read
1790
- * @returns {Promise<{updated: number}>}
1791
- */
1792
- /**
1793
- * Get sent messages from this agent.
1794
- * @param {Object} [params]
1795
- * @param {string} [params.type] - Filter by message type
1796
- * @param {string} [params.threadId] - Filter by thread
1797
- * @param {number} [params.limit=50] - Max messages to return
1798
- * @returns {Promise<{messages: Object[], total: number}>}
1799
- */
1800
- async getSentMessages({ type, threadId, limit } = {}) {
1801
- const params = new URLSearchParams({
1802
- agent_id: this.agentId,
1803
- direction: 'sent',
1804
- });
1805
- if (type) params.set('type', type);
1806
- if (threadId) params.set('thread_id', threadId);
1807
- if (limit) params.set('limit', String(limit));
1808
- return this._request(`/api/messages?${params}`, 'GET');
1809
- }
1810
-
1811
- /**
1812
- * Get all messages (inbox + sent) with full filter control.
1813
- * @param {Object} [params]
1814
- * @param {string} [params.direction='all'] - 'inbox' | 'sent' | 'all'
1815
- * @param {string} [params.type] - Filter by message type
1816
- * @param {boolean} [params.unread] - Only unread messages
1817
- * @param {string} [params.threadId] - Filter by thread
1818
- * @param {number} [params.limit=50] - Max messages to return
1819
- * @returns {Promise<{messages: Object[], total: number, unread_count: number}>}
1820
- */
1821
- async getMessages({ direction, type, unread, threadId, limit } = {}) {
1822
- const params = new URLSearchParams({ agent_id: this.agentId });
1823
- if (direction) params.set('direction', direction);
1824
- if (type) params.set('type', type);
1825
- if (unread) params.set('unread', 'true');
1826
- if (threadId) params.set('thread_id', threadId);
1827
- if (limit) params.set('limit', String(limit));
1828
- return this._request(`/api/messages?${params}`, 'GET');
1829
- }
1830
-
1831
- /**
1832
- * Get a single message by ID.
1833
- * @param {string} messageId - The message ID (msg_*)
1834
- * @returns {Promise<{message: Object}>}
1835
- */
1836
- async getMessage(messageId) {
1837
- return this._request(`/api/messages/${encodeURIComponent(messageId)}`, 'GET');
1838
- }
1839
-
1840
- /**
1841
- * Mark messages as read.
1842
- * @param {string[]} messageIds - Array of message IDs to mark read
1843
- * @returns {Promise<{updated: number}>}
1844
- */
1845
- async markRead(messageIds) {
1846
- return this._request('/api/messages', 'PATCH', {
1847
- message_ids: messageIds,
1848
- action: 'read',
1849
- agent_id: this.agentId,
1850
- });
1851
- }
1852
-
1853
- /**
1854
- * Archive messages.
1855
- * @param {string[]} messageIds - Array of message IDs to archive
1856
- * @returns {Promise<{updated: number}>}
1857
- */
1858
- async archiveMessages(messageIds) {
1859
- return this._request('/api/messages', 'PATCH', {
1860
- message_ids: messageIds,
1861
- action: 'archive',
1862
- agent_id: this.agentId,
1863
- });
1864
- }
1865
-
1866
- /**
1867
- * Broadcast a message to all agents in the organization.
1868
- * @param {Object} params
1869
- * @param {string} [params.type='info'] - Message type
1870
- * @param {string} [params.subject] - Subject line
1871
- * @param {string} params.body - Message body
1872
- * @param {string} [params.threadId] - Thread ID
1873
- * @returns {Promise<{message: Object, message_id: string}>}
1874
- */
1875
- async broadcast({ type, subject, body, threadId }) {
1876
- return this.sendMessage({ to: null, type, subject, body, threadId });
1877
- }
1878
-
1879
- /**
1880
- * Create a new message thread for multi-turn conversations.
1881
- * @param {Object} params
1882
- * @param {string} params.name - Thread name
1883
- * @param {string[]} [params.participants] - Agent IDs (null = open to all)
1884
- * @returns {Promise<{thread: Object, thread_id: string}>}
1885
- */
1886
- async createMessageThread({ name, participants }) {
1887
- return this._request('/api/messages/threads', 'POST', {
1888
- name,
1889
- participants,
1890
- created_by: this.agentId,
1891
- });
1892
- }
1893
-
1894
- /**
1895
- * List message threads.
1896
- * @param {Object} [params]
1897
- * @param {string} [params.status] - Filter by status: open|resolved|archived
1898
- * @param {number} [params.limit=20] - Max threads to return
1899
- * @returns {Promise<{threads: Object[], total: number}>}
1900
- */
1901
- async getMessageThreads({ status, limit } = {}) {
1902
- const params = new URLSearchParams({ agent_id: this.agentId });
1903
- if (status) params.set('status', status);
1904
- if (limit) params.set('limit', String(limit));
1905
- return this._request(`/api/messages/threads?${params}`, 'GET');
1906
- }
1907
-
1908
- /**
1909
- * Resolve (close) a message thread.
1910
- * @param {string} threadId - Thread ID to resolve
1911
- * @param {string} [summary] - Resolution summary
1912
- * @returns {Promise<{thread: Object}>}
1913
- */
1914
- async resolveMessageThread(threadId, summary) {
1915
- return this._request('/api/messages/threads', 'PATCH', {
1916
- thread_id: threadId,
1917
- status: 'resolved',
1918
- summary,
1919
- });
1920
- }
1921
-
1922
- /**
1923
- * Create or update a shared workspace document.
1924
- * Upserts by (org_id, name). Updates increment the version.
1925
- * @param {Object} params
1926
- * @param {string} params.name - Document name (unique per org)
1927
- * @param {string} params.content - Document content
1928
- * @returns {Promise<{doc: Object, doc_id: string}>}
1929
- */
1930
- async saveSharedDoc({ name, content }) {
1931
- return this._request('/api/messages/docs', 'POST', {
1932
- name,
1933
- content,
1934
- agent_id: this.agentId,
1935
- });
1936
- }
1937
-
1938
- /**
1939
- * Get an attachment's download URL or fetch its binary data.
1940
- * @param {string} attachmentId - Attachment ID (att_*)
1941
- * @returns {string} URL to fetch the attachment
1942
- */
1943
- getAttachmentUrl(attachmentId) {
1944
- return `${this.baseUrl}/api/messages/attachments?id=${encodeURIComponent(attachmentId)}`;
1945
- }
1946
-
1947
- /**
1948
- * Download an attachment as a Buffer.
1949
- * @param {string} attachmentId - Attachment ID (att_*)
1950
- * @returns {Promise<{data: Buffer, filename: string, mimeType: string}>}
1951
- */
1952
- async getAttachment(attachmentId) {
1953
- const url = this.getAttachmentUrl(attachmentId);
1954
- const res = await fetch(url, {
1955
- headers: { 'x-api-key': this.apiKey },
1956
- });
1957
- if (!res.ok) {
1958
- const err = await res.json().catch(() => ({}));
1959
- throw new Error(err.error || `Attachment fetch failed: ${res.status}`);
1960
- }
1961
- const data = Buffer.from(await res.arrayBuffer());
1962
- const cd = res.headers.get('content-disposition') || '';
1963
- const match = cd.match(/filename="(.+?)"/);
1964
- return {
1965
- data,
1966
- filename: match ? match[1] : attachmentId,
1967
- mimeType: res.headers.get('content-type') || 'application/octet-stream',
1968
- };
1969
- }
1970
-
1971
- // ══════════════════════════════════════════════
1972
- // Category 13: Policy Enforcement (Guard) (2 methods)
1973
- // ══════════════════════════════════════════════
1974
-
1975
- /**
1976
- * Enforce policies before a decision executes. Guard is the heart of DashClaw. It intercepts intent and returns allow/warn/block/require_approval.
1977
- * @param {Object} context
1978
- * @param {string} context.action_type - Action type (required)
1979
- * @param {number} [context.risk_score] - Risk score 0-100
1980
- * @param {string[]} [context.systems_touched] - Systems involved
1981
- * @param {boolean} [context.reversible] - Whether the action is reversible
1982
- * @param {string} [context.declared_goal] - What the action aims to do
1983
- * @param {Object} [options]
1984
- * @param {boolean} [options.includeSignals=false] - Include live signal warnings
1985
- * @returns {Promise<{decision: string, reasons: string[], warnings: string[], matched_policies: string[], evaluated_at: string}>}
1986
- */
1987
- async guard(context, options = {}) {
1988
- const params = new URLSearchParams();
1989
- if (options.includeSignals) params.set('include_signals', 'true');
1990
- const qs = params.toString();
1991
- return this._request(`/api/guard${qs ? `?${qs}` : ''}`, 'POST', {
1992
- ...context,
1993
- agent_id: context.agent_id || this.agentId,
1994
- });
1995
- }
1996
-
1997
- /**
1998
- * Get recent guard decisions (audit log).
1999
- * @param {Object} [filters]
2000
- * @param {string} [filters.decision] - Filter by decision: allow|warn|block|require_approval
2001
- * @param {number} [filters.limit=20] - Max results
2002
- * @param {number} [filters.offset=0] - Pagination offset
2003
- * @returns {Promise<{decisions: Object[], total: number, stats: Object}>}
2004
- */
2005
- async getGuardDecisions(filters = {}) {
2006
- const params = new URLSearchParams({ agent_id: this.agentId });
2007
- if (filters.decision) params.set('decision', filters.decision);
2008
- if (filters.limit) params.set('limit', String(filters.limit));
2009
- if (filters.offset) params.set('offset', String(filters.offset));
2010
- return this._request(`/api/guard?${params}`, 'GET');
2011
- }
2012
-
2013
- // ══════════════════════════════════════════════════════════
2014
- // Category 14: Policy Testing (3 methods)
2015
- // ══════════════════════════════════════════════════════════
2016
-
2017
- /**
2018
- * Run guardrails tests against all active policies for this org.
2019
- * @returns {Promise<{success: boolean, total_policies: number, total_tests: number, passed: number, failed: number, details: Object[]}>}
2020
- */
2021
- async testPolicies() {
2022
- return this._request('/api/policies/test', 'POST', {
2023
- agent_id: this.agentId,
2024
- });
2025
- }
2026
-
2027
- /**
2028
- * Generate a compliance proof report from active policies.
2029
- * @param {Object} [options]
2030
- * @param {string} [options.format='json'] - 'json' or 'md'
2031
- * @returns {Promise<Object|string>}
2032
- */
2033
- async getProofReport(options = {}) {
2034
- const params = new URLSearchParams();
2035
- if (options.format) params.set('format', options.format);
2036
- return this._request(`/api/policies/proof?${params}`, 'GET');
2037
- }
2038
-
2039
- /**
2040
- * Import a policy pack or raw YAML into the org's guard policies.
2041
- * Requires admin role.
2042
- * @param {Object} options
2043
- * @param {string} [options.pack] - Pack name: enterprise-strict, smb-safe, startup-growth, development
2044
- * @param {string} [options.yaml] - Raw YAML string of policies to import
2045
- * @returns {Promise<{imported: number, skipped: number, errors: string[], policies: Object[]}>}
2046
- */
2047
- async importPolicies({ pack, yaml } = {}) {
2048
- return this._request('/api/policies/import', 'POST', { pack, yaml });
2049
- }
2050
-
2051
- // ══════════════════════════════════════════════════════════
2052
- // Category 15: Compliance Engine (5 methods)
2053
- // ══════════════════════════════════════════════════════════
2054
-
2055
- /**
2056
- * Map active policies to a compliance framework's controls.
2057
- * @param {string} framework - Framework ID: soc2, iso27001, gdpr, nist-ai-rmf, imda-agentic
2058
- * @returns {Promise<Object>} Compliance map with controls, coverage, and gaps
2059
- */
2060
- async mapCompliance(framework) {
2061
- return this._request(`/api/compliance/map?framework=${encodeURIComponent(framework)}`, 'GET');
2062
- }
2063
-
2064
- /**
2065
- * Run gap analysis on a compliance framework mapping.
2066
- * @param {string} framework - Framework ID
2067
- * @returns {Promise<Object>} Gap analysis with remediation plan and risk assessment
2068
- */
2069
- async analyzeGaps(framework) {
2070
- return this._request(`/api/compliance/gaps?framework=${encodeURIComponent(framework)}`, 'GET');
2071
- }
2072
-
2073
- /**
2074
- * Generate a full compliance report (markdown or JSON) and save a snapshot.
2075
- * @param {string} framework - Framework ID
2076
- * @param {Object} [options]
2077
- * @param {string} [options.format='json'] - 'json' or 'md'
2078
- * @returns {Promise<Object>}
2079
- */
2080
- async getComplianceReport(framework, options = {}) {
2081
- const params = new URLSearchParams({ framework });
2082
- if (options.format) params.set('format', options.format);
2083
- return this._request(`/api/compliance/report?${params}`, 'GET');
2084
- }
2085
-
2086
- /**
2087
- * List available compliance frameworks.
2088
- * @returns {Promise<{frameworks: Object[]}>}
2089
- */
2090
- async listFrameworks() {
2091
- return this._request('/api/compliance/frameworks', 'GET');
2092
- }
2093
-
2094
- /**
2095
- * Get live compliance evidence from guard decisions and action records.
2096
- * @param {Object} [options]
2097
- * @param {string} [options.window='30d'] - Time window (e.g., '7d', '30d', '90d')
2098
- * @returns {Promise<{evidence: Object}>}
2099
- */
2100
- async getComplianceEvidence(options = {}) {
2101
- const params = new URLSearchParams();
2102
- if (options.window) params.set('window', options.window);
2103
- return this._request(`/api/compliance/evidence?${params}`, 'GET');
2104
- }
2105
-
2106
- // ══════════════════════════════════════════════════════════
2107
- // Category 16: Task Routing (10 methods)
2108
- // ══════════════════════════════════════════════════════════
2109
-
2110
- /**
2111
- * List routing agents registered in this org.
2112
- * @param {Object} [filters]
2113
- * @param {string} [filters.status] - Filter by status: available, busy, offline
2114
- * @returns {Promise<{agents: Object[]}>}
2115
- */
2116
- async listRoutingAgents(filters = {}) {
2117
- const params = new URLSearchParams();
2118
- if (filters.status) params.set('status', filters.status);
2119
- return this._request(`/api/routing/agents?${params}`, 'GET');
2120
- }
2121
-
2122
- /**
2123
- * Register an agent for task routing.
2124
- * @param {Object} agent
2125
- * @param {string} agent.name - Agent name
2126
- * @param {Array} [agent.capabilities] - Skills/capabilities (strings or {skill, priority} objects)
2127
- * @param {number} [agent.maxConcurrent=3] - Max concurrent tasks
2128
- * @param {string} [agent.endpoint] - Webhook endpoint for task dispatch
2129
- * @returns {Promise<{agent: Object}>}
2130
- */
2131
- async registerRoutingAgent(agent) {
2132
- return this._request('/api/routing/agents', 'POST', agent);
2133
- }
2134
-
2135
- /**
2136
- * Get a single routing agent by ID.
2137
- * @param {string} agentId - Routing agent ID
2138
- * @returns {Promise<{agent: Object, metrics: Object[]}>}
2139
- */
2140
- async getRoutingAgent(agentId) {
2141
- return this._request(`/api/routing/agents/${encodeURIComponent(agentId)}`, 'GET');
2142
- }
2143
-
2144
- /**
2145
- * Update routing agent status.
2146
- * @param {string} agentId - Routing agent ID
2147
- * @param {string} status - New status: available, busy, offline
2148
- * @returns {Promise<{agent: Object}>}
2149
- */
2150
- async updateRoutingAgentStatus(agentId, status) {
2151
- return this._request(`/api/routing/agents/${encodeURIComponent(agentId)}`, 'PATCH', { status });
2152
- }
2153
-
2154
- /**
2155
- * Unregister (delete) a routing agent.
2156
- * @param {string} agentId - Routing agent ID
2157
- * @returns {Promise<{deleted: Object}>}
2158
- */
2159
- async deleteRoutingAgent(agentId) {
2160
- return this._request(`/api/routing/agents/${encodeURIComponent(agentId)}`, 'DELETE');
2161
- }
2162
-
2163
- /**
2164
- * List routing tasks with optional filters.
2165
- * @param {Object} [filters]
2166
- * @param {string} [filters.status] - Filter by status
2167
- * @param {string} [filters.assignedTo] - Filter by assigned agent
2168
- * @param {number} [filters.limit=50] - Max results
2169
- * @returns {Promise<{tasks: Object[]}>}
2170
- */
2171
- async listRoutingTasks(filters = {}) {
2172
- const params = new URLSearchParams();
2173
- if (filters.status) params.set('status', filters.status);
2174
- if (filters.assignedTo) params.set('assigned_to', filters.assignedTo);
2175
- if (filters.limit) params.set('limit', String(filters.limit));
2176
- return this._request(`/api/routing/tasks?${params}`, 'GET');
2177
- }
2178
-
2179
- /**
2180
- * Submit a task for auto-routing to the best available agent.
2181
- * @param {Object} task
2182
- * @param {string} task.title - Task title
2183
- * @param {string} [task.description] - Task description
2184
- * @param {string[]} [task.requiredSkills] - Skills needed to complete this task
2185
- * @param {string} [task.urgency='normal'] - Urgency: low, normal, high, critical
2186
- * @param {number} [task.timeoutSeconds=3600] - Timeout in seconds
2187
- * @param {number} [task.maxRetries=2] - Max retry attempts
2188
- * @param {string} [task.callbackUrl] - Webhook URL for task completion callback
2189
- * @returns {Promise<{task: Object, routing: Object}>}
2190
- */
2191
- async submitRoutingTask(task) {
2192
- return this._request('/api/routing/tasks', 'POST', task);
2193
- }
2194
-
2195
- /**
2196
- * Complete a routing task.
2197
- * @param {string} taskId - Task ID
2198
- * @param {Object} [result]
2199
- * @param {boolean} [result.success=true] - Whether task succeeded
2200
- * @param {Object} [result.result] - Task result data
2201
- * @param {string} [result.error] - Error message if failed
2202
- * @returns {Promise<{task: Object, routing: Object}>}
2203
- */
2204
- async completeRoutingTask(taskId, result = {}) {
2205
- return this._request(`/api/routing/tasks/${encodeURIComponent(taskId)}/complete`, 'POST', result);
2206
- }
2207
-
2208
- /**
2209
- * Get routing statistics for the org.
2210
- * @returns {Promise<{agents: Object, tasks: Object, routing: Object}>}
2211
- */
2212
- async getRoutingStats() {
2213
- return this._request('/api/routing/stats', 'GET');
2214
- }
2215
-
2216
- /**
2217
- * Get routing system health status.
2218
- * @returns {Promise<{status: string, agents: Object, tasks: Object}>}
2219
- */
2220
- async getRoutingHealth() {
2221
- return this._request('/api/routing/health', 'GET');
2222
- }
2223
-
2224
- // ══════════════════════════════════════════════════════════
2225
- // Agent Pairing (3 methods)
2226
- // ══════════════════════════════════════════════════════════
2227
-
2228
- // createPairing, createPairingFromPrivateJwk, waitForPairing
2229
- // (defined near the top of the class)
2230
-
2231
- // ══════════════════════════════════════════════════════════
2232
- // Identity Binding (2 methods)
2233
- // ══════════════════════════════════════════════════════════
2234
-
2235
- /**
2236
- * Register or update an agent's public key for identity verification.
2237
- * Requires admin API key.
2238
- * @param {Object} identity
2239
- * @param {string} identity.agent_id - Agent ID to register
2240
- * @param {string} identity.public_key - PEM public key (SPKI format)
2241
- * @param {string} [identity.algorithm='RSASSA-PKCS1-v1_5'] - Signing algorithm
2242
- * @returns {Promise<{identity: Object}>}
2243
- */
2244
- async registerIdentity(identity) {
2245
- return this._request('/api/identities', 'POST', identity);
2246
- }
2247
-
2248
- /**
2249
- * List all registered agent identities for this org.
2250
- * @returns {Promise<{identities: Object[]}>}
2251
- */
2252
- async getIdentities() {
2253
- return this._request('/api/identities', 'GET');
2254
- }
2255
-
2256
- // ══════════════════════════════════════════════════════════
2257
- // Organization Management (5 methods)
2258
- // ══════════════════════════════════════════════════════════
2259
-
2260
- /**
2261
- * Get the current organization's details. Requires admin API key.
2262
- * @returns {Promise<{organizations: Object[]}>}
2263
- */
2264
- async getOrg() {
2265
- return this._request('/api/orgs', 'GET');
2266
- }
2267
-
2268
- /**
2269
- * Create a new organization with an initial admin API key. Requires admin API key.
2270
- * @param {Object} org
2271
- * @param {string} org.name - Organization name
2272
- * @param {string} org.slug - URL-safe slug (lowercase alphanumeric + hyphens)
2273
- * @returns {Promise<{organization: Object, api_key: Object}>}
2274
- */
2275
- async createOrg(org) {
2276
- return this._request('/api/orgs', 'POST', org);
2277
- }
2278
-
2279
- /**
2280
- * Get organization details by ID. Requires admin API key.
2281
- * @param {string} orgId - Organization ID
2282
- * @returns {Promise<{organization: Object}>}
2283
- */
2284
- async getOrgById(orgId) {
2285
- return this._request(`/api/orgs/${encodeURIComponent(orgId)}`, 'GET');
2286
- }
2287
-
2288
- /**
2289
- * Update organization details. Requires admin API key.
2290
- * @param {string} orgId - Organization ID
2291
- * @param {Object} updates - Fields to update (name, slug)
2292
- * @returns {Promise<{organization: Object}>}
2293
- */
2294
- async updateOrg(orgId, updates) {
2295
- return this._request(`/api/orgs/${encodeURIComponent(orgId)}`, 'PATCH', updates);
2296
- }
2297
-
2298
- /**
2299
- * List API keys for an organization. Requires admin API key.
2300
- * @param {string} orgId - Organization ID
2301
- * @returns {Promise<{keys: Object[]}>}
2302
- */
2303
- async getOrgKeys(orgId) {
2304
- return this._request(`/api/orgs/${encodeURIComponent(orgId)}/keys`, 'GET');
2305
- }
2306
-
2307
- // ══════════════════════════════════════════════════════════
2308
- // Activity Logs (1 method)
2309
- // ══════════════════════════════════════════════════════════
2310
-
2311
- /**
2312
- * Get activity/audit logs for the organization.
2313
- * @param {Object} [filters]
2314
- * @param {string} [filters.action] - Filter by action type
2315
- * @param {string} [filters.actor_id] - Filter by actor
2316
- * @param {string} [filters.resource_type] - Filter by resource type
2317
- * @param {string} [filters.before] - Before timestamp (ISO string)
2318
- * @param {string} [filters.after] - After timestamp (ISO string)
2319
- * @param {number} [filters.limit=50] - Max results (max 200)
2320
- * @param {number} [filters.offset=0] - Pagination offset
2321
- * @returns {Promise<{logs: Object[], stats: Object, pagination: Object}>}
2322
- */
2323
- async getActivityLogs(filters = {}) {
2324
- const params = new URLSearchParams();
2325
- for (const [key, value] of Object.entries(filters)) {
2326
- if (value !== undefined && value !== null && value !== '') {
2327
- params.set(key, String(value));
2328
- }
2329
- }
2330
- return this._request(`/api/activity?${params}`, 'GET');
2331
- }
2332
-
2333
- // ══════════════════════════════════════════════════════════
2334
- // Webhooks (5 methods)
2335
- // ══════════════════════════════════════════════════════════
2336
-
2337
- /**
2338
- * List all webhooks for this org.
2339
- * @returns {Promise<{webhooks: Object[]}>}
2340
- */
2341
- async getWebhooks() {
2342
- return this._request('/api/webhooks', 'GET');
2343
- }
2344
-
2345
- /**
2346
- * Create a new webhook subscription.
2347
- * @param {Object} webhook
2348
- * @param {string} webhook.url - Webhook endpoint URL
2349
- * @param {string[]} [webhook.events] - Event types to subscribe to
2350
- * @returns {Promise<{webhook: Object}>}
2351
- */
2352
- async createWebhook(webhook) {
2353
- return this._request('/api/webhooks', 'POST', webhook);
2354
- }
2355
-
2356
- /**
2357
- * Delete a webhook.
2358
- * @param {string} webhookId - Webhook ID
2359
- * @returns {Promise<{deleted: boolean}>}
2360
- */
2361
- async deleteWebhook(webhookId) {
2362
- return this._request(`/api/webhooks?id=${encodeURIComponent(webhookId)}`, 'DELETE');
2363
- }
2364
-
2365
- /**
2366
- * Send a test event to a webhook.
2367
- * @param {string} webhookId - Webhook ID
2368
- * @returns {Promise<{delivery: Object}>}
2369
- */
2370
- async testWebhook(webhookId) {
2371
- return this._request(`/api/webhooks/${encodeURIComponent(webhookId)}/test`, 'POST');
2372
- }
2373
-
2374
- /**
2375
- * Get delivery history for a webhook.
2376
- * @param {string} webhookId - Webhook ID
2377
- * @returns {Promise<{deliveries: Object[]}>}
2378
- */
2379
- async getWebhookDeliveries(webhookId) {
2380
- return this._request(`/api/webhooks/${encodeURIComponent(webhookId)}/deliveries`, 'GET');
2381
- }
2382
-
2383
- // ─── Bulk Sync ────────────────────────────────────────────
2384
-
2385
- /**
2386
- * Sync multiple data categories in a single request.
2387
- * Every key is optional. Only provided categories are processed.
2388
- * @param {Object} state - Data to sync (connections, memory, goals, learning, content, inspiration, context_points, context_threads, handoffs, preferences, snippets)
2389
- * @returns {Promise<{results: Object, total_synced: number, total_errors: number, duration_ms: number}>}
2390
- */
2391
- async syncState(state) {
2392
- return this._request('/api/sync', 'POST', {
2393
- agent_id: this.agentId,
2394
- ...state,
2395
- });
2396
- }
2397
-
2398
- // ----------------------------------------------
2399
- // Category: Evaluations
2400
- // ----------------------------------------------
2401
-
2402
- /**
2403
- * Create an evaluation score for an action.
2404
- * @param {Object} params
2405
- * @param {string} params.actionId - Action record ID
2406
- * @param {string} params.scorerName - Name of the scorer
2407
- * @param {number} params.score - Score between 0.0 and 1.0
2408
- * @param {string} [params.label] - Category label (e.g., 'correct', 'incorrect')
2409
- * @param {string} [params.reasoning] - Explanation of the score
2410
- * @param {string} [params.evaluatedBy] - 'auto', 'human', or 'llm_judge'
2411
- * @param {Object} [params.metadata] - Additional metadata
2412
- * @returns {Promise<Object>}
2413
- */
2414
- async createScore({ actionId, scorerName, score, label, reasoning, evaluatedBy, metadata }) {
2415
- return this._request('/api/evaluations', 'POST', {
2416
- action_id: actionId,
2417
- scorer_name: scorerName,
2418
- score,
2419
- label,
2420
- reasoning,
2421
- evaluated_by: evaluatedBy,
2422
- metadata,
2423
- });
2424
- }
2425
-
2426
- /**
2427
- * List evaluation scores with optional filters.
2428
- * @param {Object} [filters] - { action_id, scorer_name, evaluated_by, min_score, max_score, limit, offset, agent_id }
2429
- * @returns {Promise<{ scores: Object[], total: number }>}
2430
- */
2431
- async getScores(filters = {}) {
2432
- const params = new URLSearchParams();
2433
- for (const [key, value] of Object.entries(filters)) {
2434
- if (value !== undefined && value !== null && value !== '') {
2435
- params.set(key, String(value));
2436
- }
2437
- }
2438
- return this._request(`/api/evaluations?${params}`, 'GET');
2439
- }
2440
-
2441
- /**
2442
- * Create a reusable scorer definition.
2443
- * @param {Object} params
2444
- * @param {string} params.name - Scorer name (unique per org)
2445
- * @param {string} params.scorerType - 'regex', 'contains', 'numeric_range', 'custom_function', or 'llm_judge'
2446
- * @param {Object} params.config - Scorer configuration
2447
- * @param {string} [params.description] - Description
2448
- * @returns {Promise<Object>}
2449
- */
2450
- async createScorer({ name, scorerType, config, description }) {
2451
- return this._request('/api/evaluations/scorers', 'POST', {
2452
- name,
2453
- scorer_type: scorerType,
2454
- config,
2455
- description,
2456
- });
2457
- }
2458
-
2459
- /**
2460
- * List all scorers for this org.
2461
- * @returns {Promise<{ scorers: Object[], llm_available: boolean }>}
2462
- */
2463
- async getScorers() {
2464
- return this._request('/api/evaluations/scorers', 'GET');
2465
- }
2466
-
2467
- /**
2468
- * Update a scorer.
2469
- * @param {string} scorerId
2470
- * @param {Object} updates - { name?, description?, config? }
2471
- * @returns {Promise<Object>}
2472
- */
2473
- async updateScorer(scorerId, updates) {
2474
- return this._request(`/api/evaluations/scorers/${scorerId}`, 'PATCH', updates);
2475
- }
2476
-
2477
- /**
2478
- * Delete a scorer.
2479
- * @param {string} scorerId
2480
- * @returns {Promise<Object>}
2481
- */
2482
- async deleteScorer(scorerId) {
2483
- return this._request(`/api/evaluations/scorers/${scorerId}`, 'DELETE');
2484
- }
2485
-
2486
- /**
2487
- * Create and start an evaluation run.
2488
- * @param {Object} params
2489
- * @param {string} params.name - Run name
2490
- * @param {string} params.scorerId - Scorer to use
2491
- * @param {Object} [params.actionFilters] - Filters for which actions to evaluate
2492
- * @returns {Promise<Object>}
2493
- */
2494
- async createEvalRun({ name, scorerId, actionFilters }) {
2495
- return this._request('/api/evaluations/runs', 'POST', {
2496
- name,
2497
- scorer_id: scorerId,
2498
- action_filters: actionFilters,
2499
- });
2500
- }
2501
-
2502
- /**
2503
- * List evaluation runs.
2504
- * @param {Object} [filters] - { status, limit, offset }
2505
- * @returns {Promise<{ runs: Object[] }>}
2506
- */
2507
- async getEvalRuns(filters = {}) {
2508
- const params = new URLSearchParams();
2509
- for (const [key, value] of Object.entries(filters)) {
2510
- if (value !== undefined && value !== null && value !== '') {
2511
- params.set(key, String(value));
2512
- }
2513
- }
2514
- return this._request(`/api/evaluations/runs?${params}`, 'GET');
2515
- }
2516
-
2517
- /**
2518
- * Get details of an evaluation run.
2519
- * @param {string} runId
2520
- * @returns {Promise<{ run: Object, distribution: Object[] }>}
2521
- */
2522
- async getEvalRun(runId) {
2523
- return this._request(`/api/evaluations/runs/${runId}`, 'GET');
2524
- }
2525
-
2526
- /**
2527
- * Get aggregate evaluation statistics.
2528
- * @param {Object} [filters] - { agent_id, scorer_name, days }
2529
- * @returns {Promise<Object>}
2530
- */
2531
- async getEvalStats(filters = {}) {
2532
- const params = new URLSearchParams();
2533
- for (const [key, value] of Object.entries(filters)) {
2534
- if (value !== undefined && value !== null && value !== '') {
2535
- params.set(key, String(value));
2536
- }
2537
- }
2538
- return this._request(`/api/evaluations/stats?${params}`, 'GET');
2539
- }
2540
-
2541
- // -----------------------------------------------
2542
- // Prompt Management
2543
- // -----------------------------------------------
2544
-
2545
- async listPromptTemplates({ category } = {}) {
2546
- const params = category ? `?category=${encodeURIComponent(category)}` : '';
2547
- return this._request(`/api/prompts/templates${params}`, 'GET');
2548
- }
2549
-
2550
- async createPromptTemplate({ name, description, category }) {
2551
- return this._request('/api/prompts/templates', 'POST', { name, description, category });
2552
- }
2553
-
2554
- async getPromptTemplate(templateId) {
2555
- return this._request(`/api/prompts/templates/${templateId}`, 'GET');
2556
- }
2557
-
2558
- async updatePromptTemplate(templateId, fields) {
2559
- return this._request(`/api/prompts/templates/${templateId}`, 'PATCH', fields);
2560
- }
2561
-
2562
- async deletePromptTemplate(templateId) {
2563
- return this._request(`/api/prompts/templates/${templateId}`, 'DELETE');
2564
- }
2565
-
2566
- async listPromptVersions(templateId) {
2567
- return this._request(`/api/prompts/templates/${templateId}/versions`, 'GET');
2568
- }
2569
-
2570
- async createPromptVersion(templateId, { content, model_hint, parameters, changelog }) {
2571
- return this._request(`/api/prompts/templates/${templateId}/versions`, 'POST', { content, model_hint, parameters, changelog });
2572
- }
2573
-
2574
- async getPromptVersion(templateId, versionId) {
2575
- return this._request(`/api/prompts/templates/${templateId}/versions/${versionId}`, 'GET');
2576
- }
2577
-
2578
- async activatePromptVersion(templateId, versionId) {
2579
- return this._request(`/api/prompts/templates/${templateId}/versions/${versionId}`, 'POST');
2580
- }
2581
-
2582
- async renderPrompt({ template_id, version_id, variables, action_id, agent_id, record }) {
2583
- return this._request('/api/prompts/render', 'POST', { template_id, version_id, variables, action_id, agent_id, record });
2584
- }
2585
-
2586
- async listPromptRuns({ template_id, version_id, limit } = {}) {
2587
- const params = new URLSearchParams();
2588
- if (template_id) params.set('template_id', template_id);
2589
- if (version_id) params.set('version_id', version_id);
2590
- if (limit) params.set('limit', String(limit));
2591
- const qs = params.toString() ? `?${params.toString()}` : '';
2592
- return this._request(`/api/prompts/runs${qs}`, 'GET');
2593
- }
2594
-
2595
- async getPromptStats({ template_id } = {}) {
2596
- const params = template_id ? `?template_id=${encodeURIComponent(template_id)}` : '';
2597
- return this._request(`/api/prompts/stats${params}`, 'GET');
2598
- }
2599
-
2600
- // -----------------------------------------------
2601
- // User Feedback
2602
- // -----------------------------------------------
2603
-
2604
- async submitFeedback({ action_id, agent_id, rating, comment, category, tags, metadata }) {
2605
- return this._request('/api/feedback', 'POST', { action_id, agent_id, rating, comment, category, tags, metadata, source: 'sdk' });
2606
- }
2607
-
2608
- async listFeedback({ action_id, agent_id, category, sentiment, resolved, limit, offset } = {}) {
2609
- const params = new URLSearchParams();
2610
- if (action_id) params.set('action_id', action_id);
2611
- if (agent_id) params.set('agent_id', agent_id);
2612
- if (category) params.set('category', category);
2613
- if (sentiment) params.set('sentiment', sentiment);
2614
- if (resolved !== undefined) params.set('resolved', String(resolved));
2615
- if (limit) params.set('limit', String(limit));
2616
- if (offset) params.set('offset', String(offset));
2617
- const qs = params.toString() ? `?${params.toString()}` : '';
2618
- return this._request(`/api/feedback${qs}`, 'GET');
2619
- }
2620
-
2621
- async getFeedback(feedbackId) {
2622
- return this._request(`/api/feedback/${feedbackId}`, 'GET');
2623
- }
2624
-
2625
- async resolveFeedback(feedbackId) {
2626
- return this._request(`/api/feedback/${feedbackId}`, 'PATCH', { resolved_by: 'sdk' });
2627
- }
2628
-
2629
- async deleteFeedback(feedbackId) {
2630
- return this._request(`/api/feedback/${feedbackId}`, 'DELETE');
2631
- }
2632
-
2633
- async getFeedbackStats({ agent_id } = {}) {
2634
- const params = agent_id ? `?agent_id=${encodeURIComponent(agent_id)}` : '';
2635
- return this._request(`/api/feedback/stats${params}`, 'GET');
2636
- }
2637
-
2638
- // -----------------------------------------------
2639
- // Compliance Export
2640
- // -----------------------------------------------
2641
-
2642
- async createComplianceExport({ name, frameworks, format, window_days, include_evidence, include_remediation, include_trends }) {
2643
- return this._request('/api/compliance/exports', 'POST', { name, frameworks, format, window_days, include_evidence, include_remediation, include_trends });
2644
- }
2645
-
2646
- async listComplianceExports({ limit } = {}) {
2647
- const params = limit ? `?limit=${limit}` : '';
2648
- return this._request(`/api/compliance/exports${params}`, 'GET');
2649
- }
2650
-
2651
- async getComplianceExport(exportId) {
2652
- return this._request(`/api/compliance/exports/${exportId}`, 'GET');
2653
- }
2654
-
2655
- async downloadComplianceExport(exportId) {
2656
- return this._request(`/api/compliance/exports/${exportId}/download`, 'GET');
2657
- }
2658
-
2659
- async deleteComplianceExport(exportId) {
2660
- return this._request(`/api/compliance/exports/${exportId}`, 'DELETE');
2661
- }
2662
-
2663
- async createComplianceSchedule({ name, frameworks, format, window_days, cron_expression, include_evidence, include_remediation, include_trends }) {
2664
- return this._request('/api/compliance/schedules', 'POST', { name, frameworks, format, window_days, cron_expression, include_evidence, include_remediation, include_trends });
2665
- }
2666
-
2667
- async listComplianceSchedules() {
2668
- return this._request('/api/compliance/schedules', 'GET');
2669
- }
2670
-
2671
- async updateComplianceSchedule(scheduleId, fields) {
2672
- return this._request(`/api/compliance/schedules/${scheduleId}`, 'PATCH', fields);
2673
- }
2674
-
2675
- async deleteComplianceSchedule(scheduleId) {
2676
- return this._request(`/api/compliance/schedules/${scheduleId}`, 'DELETE');
2677
- }
2678
-
2679
- async getComplianceTrends({ framework, limit } = {}) {
2680
- const params = new URLSearchParams();
2681
- if (framework) params.set('framework', framework);
2682
- if (limit) params.set('limit', String(limit));
2683
- const qs = params.toString() ? `?${params.toString()}` : '';
2684
- return this._request(`/api/compliance/trends${qs}`, 'GET');
2685
- }
2686
-
2687
- // -----------------------------------------------
2688
- // Drift Detection
2689
- // -----------------------------------------------
2690
-
2691
- async computeDriftBaselines({ agent_id, lookback_days } = {}) {
2692
- return this._request('/api/drift/alerts', 'POST', { action: 'compute_baselines', agent_id, lookback_days });
2693
- }
2694
-
2695
- async detectDrift({ agent_id, window_days } = {}) {
2696
- return this._request('/api/drift/alerts', 'POST', { action: 'detect', agent_id, window_days });
2697
- }
2698
-
2699
- async recordDriftSnapshots() {
2700
- return this._request('/api/drift/alerts', 'POST', { action: 'record_snapshots' });
2701
- }
2702
-
2703
- async listDriftAlerts({ agent_id, severity, acknowledged, limit } = {}) {
2704
- const params = new URLSearchParams();
2705
- if (agent_id) params.set('agent_id', agent_id);
2706
- if (severity) params.set('severity', severity);
2707
- if (acknowledged !== undefined) params.set('acknowledged', String(acknowledged));
2708
- if (limit) params.set('limit', String(limit));
2709
- const qs = params.toString() ? `?${params.toString()}` : '';
2710
- return this._request(`/api/drift/alerts${qs}`, 'GET');
2711
- }
2712
-
2713
- async acknowledgeDriftAlert(alertId) {
2714
- return this._request(`/api/drift/alerts/${alertId}`, 'PATCH');
2715
- }
2716
-
2717
- async deleteDriftAlert(alertId) {
2718
- return this._request(`/api/drift/alerts/${alertId}`, 'DELETE');
2719
- }
2720
-
2721
- async getDriftStats({ agent_id } = {}) {
2722
- const params = agent_id ? `?agent_id=${encodeURIComponent(agent_id)}` : '';
2723
- return this._request(`/api/drift/stats${params}`, 'GET');
2724
- }
2725
-
2726
- async getDriftSnapshots({ agent_id, metric, limit } = {}) {
2727
- const params = new URLSearchParams();
2728
- if (agent_id) params.set('agent_id', agent_id);
2729
- if (metric) params.set('metric', metric);
2730
- if (limit) params.set('limit', String(limit));
2731
- const qs = params.toString() ? `?${params.toString()}` : '';
2732
- return this._request(`/api/drift/snapshots${qs}`, 'GET');
2733
- }
2734
-
2735
- async getDriftMetrics() {
2736
- return this._request('/api/drift/metrics', 'GET');
2737
- }
2738
-
2739
- // -----------------------------------------------
2740
- // Learning Analytics
2741
- // -----------------------------------------------
2742
-
2743
- async computeLearningVelocity({ agent_id, lookback_days, period } = {}) {
2744
- return this._request('/api/learning/analytics/velocity', 'POST', { agent_id, lookback_days, period });
2745
- }
2746
-
2747
- async getLearningVelocity({ agent_id, limit } = {}) {
2748
- const params = new URLSearchParams();
2749
- if (agent_id) params.set('agent_id', agent_id);
2750
- if (limit) params.set('limit', String(limit));
2751
- const qs = params.toString() ? `?${params.toString()}` : '';
2752
- return this._request(`/api/learning/analytics/velocity${qs}`, 'GET');
2753
- }
2754
-
2755
- async computeLearningCurves({ agent_id, lookback_days } = {}) {
2756
- return this._request('/api/learning/analytics/curves', 'POST', { agent_id, lookback_days });
2757
- }
2758
-
2759
- async getLearningCurves({ agent_id, action_type, limit } = {}) {
2760
- const params = new URLSearchParams();
2761
- if (agent_id) params.set('agent_id', agent_id);
2762
- if (action_type) params.set('action_type', action_type);
2763
- if (limit) params.set('limit', String(limit));
2764
- const qs = params.toString() ? `?${params.toString()}` : '';
2765
- return this._request(`/api/learning/analytics/curves${qs}`, 'GET');
2766
- }
2767
-
2768
- async getLearningAnalyticsSummary({ agent_id } = {}) {
2769
- const params = agent_id ? `?agent_id=${encodeURIComponent(agent_id)}` : '';
2770
- return this._request(`/api/learning/analytics/summary${params}`, 'GET');
2771
- }
2772
-
2773
- async getMaturityLevels() {
2774
- return this._request('/api/learning/analytics/maturity', 'GET');
2775
- }
2776
-
2777
- // --- Scoring Profiles -----------------------------------
2778
-
2779
- async createScoringProfile(data) {
2780
- return this._request('POST', '/api/scoring/profiles', data);
2781
- }
2782
-
2783
- async listScoringProfiles(params = {}) {
2784
- return this._request('GET', '/api/scoring/profiles', null, params);
2785
- }
2786
-
2787
- async getScoringProfile(profileId) {
2788
- return this._request('GET', `/api/scoring/profiles/${profileId}`);
2789
- }
2790
-
2791
- async updateScoringProfile(profileId, data) {
2792
- return this._request('PATCH', `/api/scoring/profiles/${profileId}`, data);
2793
- }
2794
-
2795
- async deleteScoringProfile(profileId) {
2796
- return this._request('DELETE', `/api/scoring/profiles/${profileId}`);
2797
- }
2798
-
2799
- async addScoringDimension(profileId, data) {
2800
- return this._request('POST', `/api/scoring/profiles/${profileId}/dimensions`, data);
2801
- }
2802
-
2803
- async updateScoringDimension(profileId, dimensionId, data) {
2804
- return this._request('PATCH', `/api/scoring/profiles/${profileId}/dimensions/${dimensionId}`, data);
2805
- }
2806
-
2807
- async deleteScoringDimension(profileId, dimensionId) {
2808
- return this._request('DELETE', `/api/scoring/profiles/${profileId}/dimensions/${dimensionId}`);
2809
- }
2810
-
2811
- async scoreWithProfile(profileId, action) {
2812
- return this._request('POST', '/api/scoring/score', { profile_id: profileId, action });
2813
- }
2814
-
2815
- async batchScoreWithProfile(profileId, actions) {
2816
- return this._request('POST', '/api/scoring/score', { profile_id: profileId, actions });
2817
- }
2818
-
2819
- async getProfileScores(params = {}) {
2820
- return this._request('GET', '/api/scoring/score', null, params);
2821
- }
2822
-
2823
- async getProfileScoreStats(profileId) {
2824
- return this._request('GET', '/api/scoring/score', null, { profile_id: profileId, view: 'stats' });
2825
- }
2826
-
2827
- // --- Risk Templates ------------------------------------
2828
-
2829
- async createRiskTemplate(data) {
2830
- return this._request('POST', '/api/scoring/risk-templates', data);
2831
- }
2832
-
2833
- async listRiskTemplates(params = {}) {
2834
- return this._request('GET', '/api/scoring/risk-templates', null, params);
2835
- }
2836
-
2837
- async updateRiskTemplate(templateId, data) {
2838
- return this._request('PATCH', `/api/scoring/risk-templates/${templateId}`, data);
2839
- }
2840
-
2841
- async deleteRiskTemplate(templateId) {
2842
- return this._request('DELETE', `/api/scoring/risk-templates/${templateId}`);
2843
- }
2844
-
2845
- // --- Auto-Calibration ----------------------------------
2846
-
2847
- async autoCalibrate(options = {}) {
2848
- return this._request('POST', '/api/scoring/calibrate', options);
2849
- }
2850
- }
2851
-
2852
- /**
2853
- * Error thrown when guardMode is 'enforce' and guard blocks an action.
2854
- */
2855
- class GuardBlockedError extends Error {
2856
- /**
2857
- * @param {Object} decision - Guard decision object
2858
- */
2859
- constructor(decision) {
2860
- const reasons = (decision.reasons || []).join('; ') || 'no reason';
2861
- super(`Guard blocked action: ${decision.decision}. Reasons: ${reasons}`);
2862
- this.name = 'GuardBlockedError';
2863
- this.decision = decision.decision;
2864
- this.reasons = decision.reasons || [];
2865
- this.warnings = decision.warnings || [];
2866
- this.matchedPolicies = decision.matched_policies || [];
2867
- this.riskScore = decision.risk_score ?? null;
2868
- }
2869
- }
2870
-
2871
- /**
2872
- * Error thrown when a human operator denies an action.
2873
- */
2874
- class ApprovalDeniedError extends Error {
2875
- constructor(message) {
2876
- super(message);
2877
- this.name = 'ApprovalDeniedError';
2878
- }
2879
- }
2880
-
2881
- // Backward compatibility alias (Legacy)
2882
- const OpenClawAgent = DashClaw;
2883
-
2884
- export default DashClaw;
2885
- export { DashClaw, OpenClawAgent, GuardBlockedError, ApprovalDeniedError };
1
+ /**
2
+ * DashClaw SDK v2 (Stable Runtime API)
3
+ * Focused governance runtime client for AI agents.
4
+ */
5
+
6
+ class ApprovalDeniedError extends Error {
7
+ constructor(message, decision) {
8
+ super(message);
9
+ this.name = 'ApprovalDeniedError';
10
+ this.decision = decision;
11
+ }
12
+ }
13
+
14
+ class GuardBlockedError extends Error {
15
+ constructor(decision) {
16
+ super(decision.reason || 'Action blocked by policy');
17
+ this.name = 'GuardBlockedError';
18
+ this.decision = decision;
19
+ }
20
+ }
21
+
22
+ class DashClaw {
23
+ /**
24
+ * @param {Object} options
25
+ * @param {string} options.baseUrl - DashClaw base URL
26
+ * @param {string} options.apiKey - API key for authentication
27
+ * @param {string} options.agentId - Unique identifier for this agent
28
+ */
29
+ constructor({ baseUrl, apiKey, agentId }) {
30
+ if (!baseUrl) throw new Error('baseUrl is required');
31
+ if (!apiKey) throw new Error('apiKey is required');
32
+ if (!agentId) throw new Error('agentId is required');
33
+
34
+ this.baseUrl = baseUrl.replace(/\/$/, '');
35
+ this.apiKey = apiKey;
36
+ this.agentId = agentId;
37
+ }
38
+
39
+ async _request(path, method = 'GET', body = null, params = null) {
40
+ let url = `${this.baseUrl}${path}`;
41
+ if (params) {
42
+ const qs = new URLSearchParams(params).toString();
43
+ if (qs) url += `?${qs}`;
44
+ }
45
+
46
+ const headers = {
47
+ 'Content-Type': 'application/json',
48
+ 'x-api-key': this.apiKey
49
+ };
50
+
51
+ const res = await fetch(url, {
52
+ method,
53
+ headers,
54
+ body: body ? JSON.stringify(body) : undefined
55
+ });
56
+
57
+ const data = await res.json();
58
+
59
+ if (!res.ok) {
60
+ const err = new Error(data.error || `Request failed with status ${res.status}`);
61
+ err.status = res.status;
62
+ err.details = data.details;
63
+ err.decision = data.decision;
64
+ throw err;
65
+ }
66
+
67
+ return data;
68
+ }
69
+
70
+ /**
71
+ * POST /api/guard "Can I do X?"
72
+ * @param {Object} context
73
+ * @param {string} context.action - Action type (e.g. "deploy")
74
+ * @param {string} [context.intent] - What the action aims to do
75
+ * @param {number} [context.risk_score] - Risk score 0-100
76
+ * @returns {Promise<{decision: 'allow'|'block'|'require_approval', action_id: string, reason: string, signals: string[]}>}
77
+ */
78
+ async guard(context) {
79
+ return this._request('/api/guard', 'POST', {
80
+ ...context,
81
+ agent_id: context.agent_id || this.agentId,
82
+ });
83
+ }
84
+
85
+ /**
86
+ * POST /api/actions — "I am attempting X."
87
+ * @param {Object} action
88
+ * @param {string} action.action_type - e.g. "deploy"
89
+ * @param {string} action.declared_goal - e.g. "deploy to production"
90
+ * @returns {Promise<{action: Object, action_id: string}>}
91
+ */
92
+ async createAction(action) {
93
+ const res = await this._request('/api/actions', 'POST', {
94
+ ...action,
95
+ agent_id: this.agentId,
96
+ });
97
+ return res;
98
+ }
99
+
100
+ /**
101
+ * PATCH /api/actions/:id "X finished with result Y."
102
+ * @param {string} actionId
103
+ * @param {Object} outcome
104
+ */
105
+ async updateOutcome(actionId, outcome) {
106
+ return this._request(`/api/actions/${actionId}`, 'PATCH', {
107
+ ...outcome,
108
+ timestamp_end: outcome.timestamp_end || new Date().toISOString()
109
+ });
110
+ }
111
+
112
+ /**
113
+ * POST /api/assumptions — "I believe Z is true while doing X."
114
+ * @param {Object} assumption
115
+ */
116
+ async recordAssumption(assumption) {
117
+ return this._request('/api/assumptions', 'POST', assumption);
118
+ }
119
+
120
+ /**
121
+ * GET /api/actions/:id — Polling helper for human approval.
122
+ */
123
+ async waitForApproval(actionId, { timeout = 300000, interval = 5000 } = {}) {
124
+ const startTime = Date.now();
125
+ while (Date.now() - startTime < timeout) {
126
+ const { action } = await this._request(`/api/actions/${actionId}`, 'GET');
127
+ if (action.status === 'running' || action.status === 'completed') return action;
128
+ if (action.status === 'failed' || action.status === 'cancelled') {
129
+ throw new ApprovalDeniedError(action.error_message || 'Operator denied the action.', action.status);
130
+ }
131
+ await new Promise(r => setTimeout(r, interval));
132
+ }
133
+ throw new Error(`Timed out waiting for approval of action ${actionId}`);
134
+ }
135
+ }
136
+
137
+ export { DashClaw, ApprovalDeniedError, GuardBlockedError };