dashclaw 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2887 @@
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((outcomeErr) => {
874
+ console.warn(`[DashClaw] Failed to close action ${action_id}: ${outcomeErr.message || outcomeErr}`);
875
+ });
876
+ throw error;
877
+ }
878
+ }
879
+
880
+ // ══════════════════════════════════════════════
881
+ // Category 2: Decision Integrity (Loops & Assumptions) (7 methods)
882
+ // ══════════════════════════════════════════════
883
+
884
+ /**
885
+ * Register an unresolved dependency for a decision. Open loops track work that must be completed before the decision can be considered fully resolved.
886
+ * @param {Object} loop
887
+ * @param {string} loop.action_id - Parent action ID
888
+ * @param {string} loop.loop_type - One of: followup, question, dependency, approval, review, handoff, other
889
+ * @param {string} loop.description - What needs to be resolved
890
+ * @param {string} [loop.priority='medium'] - One of: low, medium, high, critical
891
+ * @param {string} [loop.owner] - Who is responsible for resolving this
892
+ * @returns {Promise<{loop: Object, loop_id: string}>}
893
+ */
894
+ async registerOpenLoop(loop) {
895
+ return this._request('/api/actions/loops', 'POST', loop);
896
+ }
897
+
898
+ /**
899
+ * Resolve or cancel an open loop.
900
+ * @param {string} loopId - The loop_id to resolve
901
+ * @param {string} status - 'resolved' or 'cancelled'
902
+ * @param {string} [resolution] - Resolution description (required when resolving)
903
+ * @returns {Promise<{loop: Object}>}
904
+ */
905
+ async resolveOpenLoop(loopId, status, resolution) {
906
+ return this._request(`/api/actions/loops/${loopId}`, 'PATCH', {
907
+ status,
908
+ resolution
909
+ });
910
+ }
911
+
912
+ /**
913
+ * Get open loops with optional filters.
914
+ * @param {Object} [filters]
915
+ * @param {string} [filters.status] - Filter by status (open, resolved, cancelled)
916
+ * @param {string} [filters.loop_type] - Filter by loop type
917
+ * @param {string} [filters.priority] - Filter by priority
918
+ * @param {number} [filters.limit=50] - Max results
919
+ * @returns {Promise<{loops: Object[], total: number, stats: Object}>}
920
+ */
921
+ async getOpenLoops(filters = {}) {
922
+ const params = new URLSearchParams();
923
+ for (const [key, value] of Object.entries(filters)) {
924
+ if (value !== undefined && value !== null && value !== '') {
925
+ params.set(key, String(value));
926
+ }
927
+ }
928
+ return this._request(`/api/actions/loops?${params}`, 'GET');
929
+ }
930
+
931
+ /**
932
+ * Register assumptions underlying a decision. Assumptions are the decision basis. They must be validated or invalidated to maintain decision integrity.
933
+ * @param {Object} assumption
934
+ * @param {string} assumption.action_id - Parent action ID
935
+ * @param {string} assumption.assumption - The assumption being made
936
+ * @param {string} [assumption.basis] - Evidence or reasoning for the assumption
937
+ * @param {boolean} [assumption.validated=false] - Whether this has been validated
938
+ * @returns {Promise<{assumption: Object, assumption_id: string}>}
939
+ */
940
+ async registerAssumption(assumption) {
941
+ return this._request('/api/actions/assumptions', 'POST', assumption);
942
+ }
943
+
944
+ /**
945
+ * Get a single assumption by ID.
946
+ * @param {string} assumptionId
947
+ * @returns {Promise<{assumption: Object}>}
948
+ */
949
+ async getAssumption(assumptionId) {
950
+ return this._request(`/api/actions/assumptions/${assumptionId}`, 'GET');
951
+ }
952
+
953
+ /**
954
+ * Validate or invalidate an assumption.
955
+ * @param {string} assumptionId - The assumption_id to update
956
+ * @param {boolean} validated - true to validate, false to invalidate
957
+ * @param {string} [invalidated_reason] - Required when invalidating
958
+ * @returns {Promise<{assumption: Object}>}
959
+ */
960
+ async validateAssumption(assumptionId, validated, invalidated_reason) {
961
+ if (typeof validated !== 'boolean') throw new Error('validated must be a boolean');
962
+ if (validated === false && !invalidated_reason) {
963
+ throw new Error('invalidated_reason is required when invalidating an assumption');
964
+ }
965
+ const body = { validated };
966
+ if (invalidated_reason !== undefined) body.invalidated_reason = invalidated_reason;
967
+ return this._request(`/api/actions/assumptions/${assumptionId}`, 'PATCH', body);
968
+ }
969
+
970
+ /**
971
+ * Get drift report for assumptions with risk scoring.
972
+ * @param {Object} [filters]
973
+ * @param {string} [filters.action_id] - Filter by action
974
+ * @param {number} [filters.limit=50] - Max results
975
+ * @returns {Promise<{assumptions: Object[], drift_summary: Object}>}
976
+ */
977
+ async getDriftReport(filters = {}) {
978
+ const params = new URLSearchParams({ drift: 'true' });
979
+ for (const [key, value] of Object.entries(filters)) {
980
+ if (value !== undefined && value !== null && value !== '') {
981
+ params.set(key, String(value));
982
+ }
983
+ }
984
+ return this._request(`/api/actions/assumptions?${params}`, 'GET');
985
+ }
986
+
987
+ // ══════════════════════════════════════════════
988
+ // Category 3: Decision Integrity Signals (1 method)
989
+ // ══════════════════════════════════════════════
990
+
991
+ /**
992
+ * Get current decision integrity signals. Returns autonomy breaches, logic drift, and governance violations.
993
+ * @returns {Promise<{signals: Object[], counts: {red: number, amber: number, total: number}}>}
994
+ */
995
+ async getSignals() {
996
+ return this._request('/api/actions/signals', 'GET');
997
+ }
998
+
999
+ // ══════════════════════════════════════════════
1000
+ // Category 4: Dashboard Data (9 methods)
1001
+ // ══════════════════════════════════════════════
1002
+
1003
+ /**
1004
+ * Report token usage snapshot (disabled in dashboard, API still functional).
1005
+ * @param {Object} usage
1006
+ * @param {number} usage.tokens_in - Input tokens consumed
1007
+ * @param {number} usage.tokens_out - Output tokens generated
1008
+ * @param {number} [usage.context_used] - Context window tokens used
1009
+ * @param {number} [usage.context_max] - Context window max capacity
1010
+ * @param {string} [usage.model] - Model name
1011
+ * @returns {Promise<{snapshot: Object}>}
1012
+ */
1013
+ async reportTokenUsage(usage) {
1014
+ return this._request('/api/tokens', 'POST', {
1015
+ ...usage,
1016
+ agent_id: this.agentId
1017
+ });
1018
+ }
1019
+
1020
+ /**
1021
+ * Internal: fire-and-forget token report extracted from an LLM response.
1022
+ * @private
1023
+ */
1024
+ async _reportTokenUsageFromLLM({ tokens_in, tokens_out, model }) {
1025
+ if (tokens_in == null && tokens_out == null) return;
1026
+ try {
1027
+ await this._request('/api/tokens', 'POST', {
1028
+ tokens_in: tokens_in || 0,
1029
+ tokens_out: tokens_out || 0,
1030
+ model: model || undefined,
1031
+ agent_id: this.agentId,
1032
+ });
1033
+ } catch (_) {
1034
+ // fire-and-forget: never let telemetry break the caller
1035
+ }
1036
+ }
1037
+
1038
+ /**
1039
+ * Wrap an Anthropic or OpenAI client to auto-report token usage after each call.
1040
+ * Returns the same client instance (mutated) for fluent usage.
1041
+ *
1042
+ * @param {Object} llmClient - An Anthropic or OpenAI SDK client instance
1043
+ * @param {Object} [options]
1044
+ * @param {'anthropic'|'openai'} [options.provider] - Force provider detection
1045
+ * @returns {Object} The wrapped client
1046
+ *
1047
+ * @example
1048
+ * const anthropic = claw.wrapClient(new Anthropic());
1049
+ * const msg = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [...] });
1050
+ * // Token usage is auto-reported to DashClaw
1051
+ */
1052
+ wrapClient(llmClient, { provider } = {}) {
1053
+ if (llmClient._dashclawWrapped) return llmClient;
1054
+
1055
+ const detected = provider
1056
+ || (llmClient.messages?.create ? 'anthropic' : null)
1057
+ || (llmClient.chat?.completions?.create ? 'openai' : null);
1058
+
1059
+ if (!detected) {
1060
+ throw new Error(
1061
+ 'DashClaw.wrapClient: unable to detect provider. Pass { provider: "anthropic" } or { provider: "openai" }.'
1062
+ );
1063
+ }
1064
+
1065
+ if (detected === 'anthropic') {
1066
+ const original = llmClient.messages.create.bind(llmClient.messages);
1067
+ llmClient.messages.create = async (...args) => {
1068
+ const response = await original(...args);
1069
+ this._reportTokenUsageFromLLM({
1070
+ tokens_in: response?.usage?.input_tokens ?? null,
1071
+ tokens_out: response?.usage?.output_tokens ?? null,
1072
+ model: response?.model ?? null,
1073
+ });
1074
+ return response;
1075
+ };
1076
+ } else if (detected === 'openai') {
1077
+ const original = llmClient.chat.completions.create.bind(llmClient.chat.completions);
1078
+ llmClient.chat.completions.create = async (...args) => {
1079
+ const response = await original(...args);
1080
+ this._reportTokenUsageFromLLM({
1081
+ tokens_in: response?.usage?.prompt_tokens ?? null,
1082
+ tokens_out: response?.usage?.completion_tokens ?? null,
1083
+ model: response?.model ?? null,
1084
+ });
1085
+ return response;
1086
+ };
1087
+ }
1088
+
1089
+ llmClient._dashclawWrapped = true;
1090
+ return llmClient;
1091
+ }
1092
+
1093
+ /**
1094
+ * Record a decision for the learning database.
1095
+ * @param {Object} entry
1096
+ * @param {string} entry.decision - What was decided
1097
+ * @param {string} [entry.context] - Context around the decision
1098
+ * @param {string} [entry.reasoning] - Why this decision was made
1099
+ * @param {string} [entry.outcome] - 'success', 'failure', or 'pending'
1100
+ * @param {number} [entry.confidence] - Confidence level 0-100
1101
+ * @returns {Promise<{decision: Object}>}
1102
+ */
1103
+ async recordDecision(entry) {
1104
+ return this._request('/api/learning', 'POST', {
1105
+ ...entry,
1106
+ agent_id: this.agentId
1107
+ });
1108
+ }
1109
+
1110
+ /**
1111
+ * Get adaptive learning recommendations derived from prior episodes.
1112
+ * @param {Object} [filters]
1113
+ * @param {string} [filters.action_type] - Filter by action type
1114
+ * @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
1115
+ * @param {boolean} [filters.include_inactive] - Include disabled recommendations (admin/service only)
1116
+ * @param {boolean} [filters.track_events=true] - Record recommendation fetched telemetry
1117
+ * @param {boolean} [filters.include_metrics] - Include computed metrics in the response payload
1118
+ * @param {number} [filters.limit=50] - Max recommendations to return
1119
+ * @param {number} [filters.lookback_days=30] - Lookback days used when include_metrics=true
1120
+ * @returns {Promise<{recommendations: Object[], metrics?: Object, total: number, lastUpdated: string}>}
1121
+ */
1122
+ async getRecommendations(filters = {}) {
1123
+ const params = new URLSearchParams({
1124
+ agent_id: filters.agent_id || this.agentId,
1125
+ });
1126
+ if (filters.action_type) params.set('action_type', filters.action_type);
1127
+ if (filters.limit) params.set('limit', String(filters.limit));
1128
+ if (filters.include_inactive) params.set('include_inactive', 'true');
1129
+ if (filters.track_events !== false) params.set('track_events', 'true');
1130
+ if (filters.include_metrics) params.set('include_metrics', 'true');
1131
+ if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
1132
+ return this._request(`/api/learning/recommendations?${params}`, 'GET');
1133
+ }
1134
+
1135
+ /**
1136
+ * Get recommendation effectiveness metrics and telemetry aggregates.
1137
+ * @param {Object} [filters]
1138
+ * @param {string} [filters.action_type] - Filter by action type
1139
+ * @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
1140
+ * @param {number} [filters.lookback_days=30] - Lookback window for episodes/events
1141
+ * @param {number} [filters.limit=100] - Max recommendations considered
1142
+ * @param {boolean} [filters.include_inactive] - Include inactive recommendations (admin/service only)
1143
+ * @returns {Promise<{metrics: Object[], summary: Object, lookback_days: number, lastUpdated: string}>}
1144
+ */
1145
+ async getRecommendationMetrics(filters = {}) {
1146
+ const params = new URLSearchParams({
1147
+ agent_id: filters.agent_id || this.agentId,
1148
+ });
1149
+ if (filters.action_type) params.set('action_type', filters.action_type);
1150
+ if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
1151
+ if (filters.limit) params.set('limit', String(filters.limit));
1152
+ if (filters.include_inactive) params.set('include_inactive', 'true');
1153
+ return this._request(`/api/learning/recommendations/metrics?${params}`, 'GET');
1154
+ }
1155
+
1156
+ /**
1157
+ * Record recommendation telemetry events (single event or batch).
1158
+ * @param {Object|Object[]} events
1159
+ * @returns {Promise<{created: Object[], created_count: number}>}
1160
+ */
1161
+ async recordRecommendationEvents(events) {
1162
+ if (Array.isArray(events)) {
1163
+ return this._request('/api/learning/recommendations/events', 'POST', { events });
1164
+ }
1165
+ return this._request('/api/learning/recommendations/events', 'POST', events || {});
1166
+ }
1167
+
1168
+ /**
1169
+ * Enable or disable a recommendation.
1170
+ * @param {string} recommendationId - Recommendation ID
1171
+ * @param {boolean} active - Desired active state
1172
+ * @returns {Promise<{recommendation: Object}>}
1173
+ */
1174
+ async setRecommendationActive(recommendationId, active) {
1175
+ return this._request(`/api/learning/recommendations/${recommendationId}`, 'PATCH', { active: !!active });
1176
+ }
1177
+
1178
+ /**
1179
+ * Rebuild recommendations from scored learning episodes.
1180
+ * @param {Object} [options]
1181
+ * @param {string} [options.action_type] - Scope rebuild to one action type
1182
+ * @param {string} [options.agent_id] - Override agent_id (defaults to SDK agent)
1183
+ * @param {number} [options.lookback_days=30] - Days of episode history to analyze
1184
+ * @param {number} [options.min_samples=5] - Minimum episodes required per recommendation
1185
+ * @param {number} [options.episode_limit=5000] - Episode scan cap
1186
+ * @param {string} [options.action_id] - Optionally score this action before rebuild
1187
+ * @returns {Promise<{recommendations: Object[], total: number, episodes_scanned: number}>}
1188
+ */
1189
+ async rebuildRecommendations(options = {}) {
1190
+ return this._request('/api/learning/recommendations', 'POST', {
1191
+ agent_id: options.agent_id || this.agentId,
1192
+ action_type: options.action_type,
1193
+ lookback_days: options.lookback_days,
1194
+ min_samples: options.min_samples,
1195
+ episode_limit: options.episode_limit,
1196
+ action_id: options.action_id,
1197
+ });
1198
+ }
1199
+
1200
+ /**
1201
+ * Apply top recommendation hints to an action definition (non-destructive).
1202
+ * @param {Object} action - Action payload compatible with createAction()
1203
+ * @returns {Promise<{action: Object, recommendation: Object|null, adapted_fields: string[]}>}
1204
+ */
1205
+ async recommendAction(action) {
1206
+ if (!action?.action_type) {
1207
+ return { action, recommendation: null, adapted_fields: [] };
1208
+ }
1209
+
1210
+ const response = await this.getRecommendations({ action_type: action.action_type, limit: 1 });
1211
+ const recommendation = response.recommendations?.[0] || null;
1212
+ if (!recommendation) {
1213
+ return { action, recommendation: null, adapted_fields: [] };
1214
+ }
1215
+
1216
+ const adapted = { ...action };
1217
+ const adaptedFields = [];
1218
+ const hints = recommendation.hints || {};
1219
+
1220
+ if (
1221
+ typeof hints.preferred_risk_cap === 'number' &&
1222
+ (adapted.risk_score === undefined || adapted.risk_score > hints.preferred_risk_cap)
1223
+ ) {
1224
+ adapted.risk_score = hints.preferred_risk_cap;
1225
+ adaptedFields.push('risk_score');
1226
+ }
1227
+
1228
+ if (hints.prefer_reversible === true && adapted.reversible === undefined) {
1229
+ adapted.reversible = true;
1230
+ adaptedFields.push('reversible');
1231
+ }
1232
+
1233
+ if (
1234
+ typeof hints.confidence_floor === 'number' &&
1235
+ (adapted.confidence === undefined || adapted.confidence < hints.confidence_floor)
1236
+ ) {
1237
+ adapted.confidence = hints.confidence_floor;
1238
+ adaptedFields.push('confidence');
1239
+ }
1240
+
1241
+ return {
1242
+ action: adapted,
1243
+ recommendation,
1244
+ adapted_fields: adaptedFields,
1245
+ };
1246
+ }
1247
+
1248
+ /**
1249
+ * Create a goal.
1250
+ * @param {Object} goal
1251
+ * @param {string} goal.title - Goal title
1252
+ * @param {string} [goal.category] - Goal category
1253
+ * @param {string} [goal.description] - Detailed description
1254
+ * @param {string} [goal.target_date] - Target completion date (ISO string)
1255
+ * @param {number} [goal.progress] - Progress 0-100
1256
+ * @param {string} [goal.status] - 'active', 'completed', 'paused'
1257
+ * @returns {Promise<{goal: Object}>}
1258
+ */
1259
+ async createGoal(goal) {
1260
+ return this._request('/api/goals', 'POST', {
1261
+ ...goal,
1262
+ agent_id: this.agentId
1263
+ });
1264
+ }
1265
+
1266
+ /**
1267
+ * Record content creation.
1268
+ * @param {Object} content
1269
+ * @param {string} content.title - Content title
1270
+ * @param {string} [content.platform] - Platform (e.g., 'linkedin', 'twitter')
1271
+ * @param {string} [content.status] - 'draft' or 'published'
1272
+ * @param {string} [content.url] - Published URL
1273
+ * @returns {Promise<{content: Object}>}
1274
+ */
1275
+ async recordContent(content) {
1276
+ return this._request('/api/content', 'POST', {
1277
+ ...content,
1278
+ agent_id: this.agentId
1279
+ });
1280
+ }
1281
+
1282
+ /**
1283
+ * Record a relationship interaction.
1284
+ * @param {Object} interaction
1285
+ * @param {string} interaction.summary - What happened
1286
+ * @param {string} [interaction.contact_name] - Contact name (auto-resolves to contact_id)
1287
+ * @param {string} [interaction.contact_id] - Direct contact ID
1288
+ * @param {string} [interaction.direction] - 'inbound' or 'outbound'
1289
+ * @param {string} [interaction.type] - Interaction type
1290
+ * @param {string} [interaction.platform] - Platform used
1291
+ * @returns {Promise<{interaction: Object}>}
1292
+ */
1293
+ async recordInteraction(interaction) {
1294
+ return this._request('/api/relationships', 'POST', {
1295
+ ...interaction,
1296
+ agent_id: this.agentId
1297
+ });
1298
+ }
1299
+
1300
+ /**
1301
+ * Create a calendar event.
1302
+ * @param {Object} event
1303
+ * @param {string} event.summary - Event title/summary
1304
+ * @param {string} event.start_time - Start time (ISO string)
1305
+ * @param {string} [event.end_time] - End time (ISO string)
1306
+ * @param {string} [event.location] - Event location
1307
+ * @param {string} [event.description] - Event description
1308
+ * @returns {Promise<{event: Object}>}
1309
+ */
1310
+ async createCalendarEvent(event) {
1311
+ return this._request('/api/calendar', 'POST', event);
1312
+ }
1313
+
1314
+ /**
1315
+ * Record an idea/inspiration.
1316
+ * @param {Object} idea
1317
+ * @param {string} idea.title - Idea title
1318
+ * @param {string} [idea.description] - Detailed description
1319
+ * @param {string} [idea.category] - Category
1320
+ * @param {number} [idea.score] - Priority/quality score 0-100
1321
+ * @param {string} [idea.status] - 'pending', 'in_progress', 'shipped', 'rejected'
1322
+ * @param {string} [idea.source] - Where this idea came from
1323
+ * @returns {Promise<{idea: Object}>}
1324
+ */
1325
+ async recordIdea(idea) {
1326
+ return this._request('/api/inspiration', 'POST', idea);
1327
+ }
1328
+
1329
+ /**
1330
+ * Report memory health snapshot with entities and topics.
1331
+ * @param {Object} report
1332
+ * @param {Object} report.health - Health metrics
1333
+ * @param {number} report.health.score - Health score 0-100
1334
+ * @param {Object[]} [report.entities] - Key entities found in memory
1335
+ * @param {Object[]} [report.topics] - Topics/themes found in memory
1336
+ * @returns {Promise<{snapshot: Object, entities_count: number, topics_count: number}>}
1337
+ */
1338
+ async reportMemoryHealth(report) {
1339
+ return this._request('/api/memory', 'POST', report);
1340
+ }
1341
+
1342
+ /**
1343
+ * Report active connections/integrations for this agent.
1344
+ * @param {Object[]} connections - Array of connection objects
1345
+ * @param {string} connections[].provider - Service name (e.g., 'anthropic', 'github')
1346
+ * @param {string} [connections[].authType] - Auth method
1347
+ * @param {string} [connections[].planName] - Plan name
1348
+ * @param {string} [connections[].status] - Connection status: active, inactive, error
1349
+ * @param {Object|string} [connections[].metadata] - Optional metadata
1350
+ * @returns {Promise<{connections: Object[], created: number}>}
1351
+ */
1352
+ async reportConnections(connections) {
1353
+ return this._request('/api/agents/connections', 'POST', {
1354
+ agent_id: this.agentId,
1355
+ connections: connections.map(c => ({
1356
+ provider: c.provider,
1357
+ auth_type: c.authType || c.auth_type || 'api_key',
1358
+ plan_name: c.planName || c.plan_name || null,
1359
+ status: c.status || 'active',
1360
+ metadata: c.metadata || null
1361
+ }))
1362
+ });
1363
+ }
1364
+
1365
+ // ══════════════════════════════════════════════
1366
+ // Category 5: Session Handoffs (3 methods)
1367
+ // ══════════════════════════════════════════════
1368
+
1369
+ /**
1370
+ * Create a session handoff document.
1371
+ * @param {Object} handoff
1372
+ * @param {string} handoff.summary - Session summary
1373
+ * @param {string} [handoff.session_date] - Date string (defaults to today)
1374
+ * @param {string[]} [handoff.key_decisions] - Key decisions made
1375
+ * @param {string[]} [handoff.open_tasks] - Tasks still open
1376
+ * @param {string} [handoff.mood_notes] - Mood/energy observations
1377
+ * @param {string[]} [handoff.next_priorities] - What to focus on next
1378
+ * @returns {Promise<{handoff: Object, handoff_id: string}>}
1379
+ */
1380
+ async createHandoff(handoff) {
1381
+ return this._request('/api/handoffs', 'POST', {
1382
+ agent_id: this.agentId,
1383
+ ...handoff
1384
+ });
1385
+ }
1386
+
1387
+ /**
1388
+ * Get handoffs with optional filters.
1389
+ * @param {Object} [filters]
1390
+ * @param {string} [filters.date] - Filter by session_date
1391
+ * @param {number} [filters.limit] - Max results
1392
+ * @returns {Promise<{handoffs: Object[], total: number}>}
1393
+ */
1394
+ async getHandoffs(filters = {}) {
1395
+ const params = new URLSearchParams({ agent_id: this.agentId });
1396
+ if (filters.date) params.set('date', filters.date);
1397
+ if (filters.limit) params.set('limit', String(filters.limit));
1398
+ return this._request(`/api/handoffs?${params}`, 'GET');
1399
+ }
1400
+
1401
+ /**
1402
+ * Get the most recent handoff for this agent.
1403
+ * @returns {Promise<{handoff: Object|null}>}
1404
+ */
1405
+ async getLatestHandoff() {
1406
+ return this._request(`/api/handoffs?agent_id=${this.agentId}&latest=true`, 'GET');
1407
+ }
1408
+
1409
+ // ══════════════════════════════════════════════
1410
+ // Category 6: Context Manager (7 methods)
1411
+ // ══════════════════════════════════════════════
1412
+
1413
+ /**
1414
+ * Capture a key point from the current session.
1415
+ * @param {Object} point
1416
+ * @param {string} point.content - The key point content
1417
+ * @param {string} [point.category] - One of: decision, task, insight, question, general
1418
+ * @param {number} [point.importance] - Importance 1-10 (default 5)
1419
+ * @param {string} [point.session_date] - Date string (defaults to today)
1420
+ * @returns {Promise<{point: Object, point_id: string}>}
1421
+ */
1422
+ async captureKeyPoint(point) {
1423
+ return this._request('/api/context/points', 'POST', {
1424
+ agent_id: this.agentId,
1425
+ ...point
1426
+ });
1427
+ }
1428
+
1429
+ /**
1430
+ * Get key points with optional filters.
1431
+ * @param {Object} [filters]
1432
+ * @param {string} [filters.category] - Filter by category
1433
+ * @param {string} [filters.session_date] - Filter by date
1434
+ * @param {number} [filters.limit] - Max results
1435
+ * @returns {Promise<{points: Object[], total: number}>}
1436
+ */
1437
+ async getKeyPoints(filters = {}) {
1438
+ const params = new URLSearchParams({ agent_id: this.agentId });
1439
+ if (filters.category) params.set('category', filters.category);
1440
+ if (filters.session_date) params.set('session_date', filters.session_date);
1441
+ if (filters.limit) params.set('limit', String(filters.limit));
1442
+ return this._request(`/api/context/points?${params}`, 'GET');
1443
+ }
1444
+
1445
+ /**
1446
+ * Create a context thread for tracking a topic across entries.
1447
+ * @param {Object} thread
1448
+ * @param {string} thread.name - Thread name (unique per agent per org)
1449
+ * @param {string} [thread.summary] - Initial summary
1450
+ * @returns {Promise<{thread: Object, thread_id: string}>}
1451
+ */
1452
+ async createThread(thread) {
1453
+ return this._request('/api/context/threads', 'POST', {
1454
+ agent_id: this.agentId,
1455
+ ...thread
1456
+ });
1457
+ }
1458
+
1459
+ /**
1460
+ * Add an entry to an existing thread.
1461
+ * @param {string} threadId - The thread ID
1462
+ * @param {string} content - Entry content
1463
+ * @param {string} [entryType] - Entry type (default: 'note')
1464
+ * @returns {Promise<{entry: Object, entry_id: string}>}
1465
+ */
1466
+ async addThreadEntry(threadId, content, entryType) {
1467
+ return this._request(`/api/context/threads/${threadId}/entries`, 'POST', {
1468
+ content,
1469
+ entry_type: entryType || 'note'
1470
+ });
1471
+ }
1472
+
1473
+ /**
1474
+ * Close a thread with an optional summary.
1475
+ * @param {string} threadId - The thread ID
1476
+ * @param {string} [summary] - Final summary
1477
+ * @returns {Promise<{thread: Object}>}
1478
+ */
1479
+ async closeThread(threadId, summary) {
1480
+ const body = { status: 'closed' };
1481
+ if (summary) body.summary = summary;
1482
+ return this._request(`/api/context/threads/${threadId}`, 'PATCH', body);
1483
+ }
1484
+
1485
+ /**
1486
+ * Get threads with optional filters.
1487
+ * @param {Object} [filters]
1488
+ * @param {string} [filters.status] - Filter by status (active, closed)
1489
+ * @param {number} [filters.limit] - Max results
1490
+ * @returns {Promise<{threads: Object[], total: number}>}
1491
+ */
1492
+ async getThreads(filters = {}) {
1493
+ const params = new URLSearchParams({ agent_id: this.agentId });
1494
+ if (filters.status) params.set('status', filters.status);
1495
+ if (filters.limit) params.set('limit', String(filters.limit));
1496
+ return this._request(`/api/context/threads?${params}`, 'GET');
1497
+ }
1498
+
1499
+ /**
1500
+ * Get a combined context summary: today's key points + active threads.
1501
+ * @returns {Promise<{points: Object[], threads: Object[]}>}
1502
+ */
1503
+ async getContextSummary() {
1504
+ const today = new Date().toISOString().split('T')[0];
1505
+ const [pointsResult, threadsResult] = await Promise.all([
1506
+ this.getKeyPoints({ session_date: today }),
1507
+ this.getThreads({ status: 'active' }),
1508
+ ]);
1509
+ return {
1510
+ points: pointsResult.points,
1511
+ threads: threadsResult.threads,
1512
+ };
1513
+ }
1514
+
1515
+ // ══════════════════════════════════════════════
1516
+ // Category 7: Automation Snippets (5 methods)
1517
+ // ══════════════════════════════════════════════
1518
+
1519
+ /**
1520
+ * Save or update a reusable code snippet.
1521
+ * @param {Object} snippet
1522
+ * @param {string} snippet.name - Snippet name (unique per org, upserts on conflict)
1523
+ * @param {string} snippet.code - The snippet code
1524
+ * @param {string} [snippet.description] - What this snippet does
1525
+ * @param {string} [snippet.language] - Programming language
1526
+ * @param {string[]} [snippet.tags] - Tags for categorization
1527
+ * @returns {Promise<{snippet: Object, snippet_id: string}>}
1528
+ */
1529
+ async saveSnippet(snippet) {
1530
+ return this._request('/api/snippets', 'POST', {
1531
+ agent_id: this.agentId,
1532
+ ...snippet
1533
+ });
1534
+ }
1535
+
1536
+ /**
1537
+ * Search and list snippets.
1538
+ * @param {Object} [filters]
1539
+ * @param {string} [filters.search] - Search name/description
1540
+ * @param {string} [filters.tag] - Filter by tag
1541
+ * @param {string} [filters.language] - Filter by language
1542
+ * @param {number} [filters.limit] - Max results
1543
+ * @returns {Promise<{snippets: Object[], total: number}>}
1544
+ */
1545
+ async getSnippets(filters = {}) {
1546
+ const params = new URLSearchParams();
1547
+ if (filters.search) params.set('search', filters.search);
1548
+ if (filters.tag) params.set('tag', filters.tag);
1549
+ if (filters.language) params.set('language', filters.language);
1550
+ if (filters.limit) params.set('limit', String(filters.limit));
1551
+ return this._request(`/api/snippets?${params}`, 'GET');
1552
+ }
1553
+
1554
+ /**
1555
+ * Fetch a single snippet by ID.
1556
+ * @param {string} snippetId - The snippet ID
1557
+ * @returns {Promise<{snippet: Object}>}
1558
+ */
1559
+ async getSnippet(snippetId) {
1560
+ return this._request(`/api/snippets/${snippetId}`, 'GET');
1561
+ }
1562
+
1563
+ /**
1564
+ * Mark a snippet as used (increments use_count).
1565
+ * @param {string} snippetId - The snippet ID
1566
+ * @returns {Promise<{snippet: Object}>}
1567
+ */
1568
+ async useSnippet(snippetId) {
1569
+ return this._request(`/api/snippets/${snippetId}/use`, 'POST');
1570
+ }
1571
+
1572
+ /**
1573
+ * Delete a snippet.
1574
+ * @param {string} snippetId - The snippet ID
1575
+ * @returns {Promise<{deleted: boolean, id: string}>}
1576
+ */
1577
+ async deleteSnippet(snippetId) {
1578
+ return this._request(`/api/snippets?id=${snippetId}`, 'DELETE');
1579
+ }
1580
+
1581
+ // ══════════════════════════════════════════════
1582
+ // Category 8: User Preferences (6 methods)
1583
+ // ══════════════════════════════════════════════
1584
+
1585
+ /**
1586
+ * Log a user observation (what you noticed about the user).
1587
+ * @param {Object} obs
1588
+ * @param {string} obs.observation - The observation text
1589
+ * @param {string} [obs.category] - Category tag
1590
+ * @param {number} [obs.importance] - Importance 1-10
1591
+ * @returns {Promise<{observation: Object, observation_id: string}>}
1592
+ */
1593
+ async logObservation(obs) {
1594
+ return this._request('/api/preferences', 'POST', {
1595
+ type: 'observation',
1596
+ agent_id: this.agentId,
1597
+ ...obs
1598
+ });
1599
+ }
1600
+
1601
+ /**
1602
+ * Set a learned user preference.
1603
+ * @param {Object} pref
1604
+ * @param {string} pref.preference - The preference description
1605
+ * @param {string} [pref.category] - Category tag
1606
+ * @param {number} [pref.confidence] - Confidence 0-100
1607
+ * @returns {Promise<{preference: Object, preference_id: string}>}
1608
+ */
1609
+ async setPreference(pref) {
1610
+ return this._request('/api/preferences', 'POST', {
1611
+ type: 'preference',
1612
+ agent_id: this.agentId,
1613
+ ...pref
1614
+ });
1615
+ }
1616
+
1617
+ /**
1618
+ * Log user mood/energy for a session.
1619
+ * @param {Object} entry
1620
+ * @param {string} entry.mood - Mood description (e.g., 'focused', 'frustrated')
1621
+ * @param {string} [entry.energy] - Energy level (e.g., 'high', 'low')
1622
+ * @param {string} [entry.notes] - Additional notes
1623
+ * @returns {Promise<{mood: Object, mood_id: string}>}
1624
+ */
1625
+ async logMood(entry) {
1626
+ return this._request('/api/preferences', 'POST', {
1627
+ type: 'mood',
1628
+ agent_id: this.agentId,
1629
+ ...entry
1630
+ });
1631
+ }
1632
+
1633
+ /**
1634
+ * Track an approach and whether it succeeded or failed.
1635
+ * @param {Object} entry
1636
+ * @param {string} entry.approach - The approach description
1637
+ * @param {string} [entry.context] - Context for when to use this approach
1638
+ * @param {boolean} [entry.success] - true = worked, false = failed, undefined = just recording
1639
+ * @returns {Promise<{approach: Object, approach_id: string}>}
1640
+ */
1641
+ async trackApproach(entry) {
1642
+ return this._request('/api/preferences', 'POST', {
1643
+ type: 'approach',
1644
+ agent_id: this.agentId,
1645
+ ...entry
1646
+ });
1647
+ }
1648
+
1649
+ /**
1650
+ * Get a summary of all user preference data.
1651
+ * @returns {Promise<{summary: Object}>}
1652
+ */
1653
+ async getPreferenceSummary() {
1654
+ return this._request(`/api/preferences?type=summary&agent_id=${this.agentId}`, 'GET');
1655
+ }
1656
+
1657
+ /**
1658
+ * Get tracked approaches with success/fail counts.
1659
+ * @param {Object} [filters]
1660
+ * @param {number} [filters.limit] - Max results
1661
+ * @returns {Promise<{approaches: Object[], total: number}>}
1662
+ */
1663
+ async getApproaches(filters = {}) {
1664
+ const params = new URLSearchParams({ type: 'approaches', agent_id: this.agentId });
1665
+ if (filters.limit) params.set('limit', String(filters.limit));
1666
+ return this._request(`/api/preferences?${params}`, 'GET');
1667
+ }
1668
+
1669
+ // ══════════════════════════════════════════════
1670
+ // Category 9: Daily Digest (1 method)
1671
+ // ══════════════════════════════════════════════
1672
+
1673
+ /**
1674
+ * Get a daily activity digest aggregated from all data sources.
1675
+ * @param {string} [date] - Date string YYYY-MM-DD (defaults to today)
1676
+ * @returns {Promise<{date: string, digest: Object, summary: Object}>}
1677
+ */
1678
+ async getDailyDigest(date) {
1679
+ const params = new URLSearchParams({ agent_id: this.agentId });
1680
+ if (date) params.set('date', date);
1681
+ return this._request(`/api/digest?${params}`, 'GET');
1682
+ }
1683
+
1684
+ // ══════════════════════════════════════════════
1685
+ // Category 10: Security Scanning (3 methods)
1686
+ // ══════════════════════════════════════════════
1687
+
1688
+ /**
1689
+ * Scan text for sensitive data (API keys, tokens, PII, etc.).
1690
+ * Returns findings and redacted text. Does NOT store the original content.
1691
+ * @param {string} text - Text to scan
1692
+ * @param {string} [destination] - Where this text is headed (for context)
1693
+ * @returns {Promise<{clean: boolean, findings_count: number, findings: Object[], redacted_text: string}>}
1694
+ */
1695
+ async scanContent(text, destination) {
1696
+ return this._request('/api/security/scan', 'POST', {
1697
+ text,
1698
+ destination,
1699
+ agent_id: this.agentId,
1700
+ store: false,
1701
+ });
1702
+ }
1703
+
1704
+ /**
1705
+ * Scan text and store finding metadata (never the content itself).
1706
+ * Use this for audit trails of security scans.
1707
+ * @param {string} text - Text to scan
1708
+ * @param {string} [destination] - Where this text is headed
1709
+ * @returns {Promise<{clean: boolean, findings_count: number, findings: Object[], redacted_text: string}>}
1710
+ */
1711
+ async reportSecurityFinding(text, destination) {
1712
+ return this._request('/api/security/scan', 'POST', {
1713
+ text,
1714
+ destination,
1715
+ agent_id: this.agentId,
1716
+ store: true,
1717
+ });
1718
+ }
1719
+
1720
+ /**
1721
+ * Scan text for prompt injection attacks (role overrides, delimiter injection,
1722
+ * instruction smuggling, data exfiltration attempts, etc.).
1723
+ * @param {string} text - Text to scan
1724
+ * @param {Object} [options]
1725
+ * @param {string} [options.source] - Where this text came from (for context)
1726
+ * @returns {Promise<{clean: boolean, risk_level: string, recommendation: string, findings_count: number, critical_count: number, categories: string[], findings: Object[]}>}
1727
+ */
1728
+ async scanPromptInjection(text, options = {}) {
1729
+ return this._request('/api/security/prompt-injection', 'POST', {
1730
+ text,
1731
+ source: options.source,
1732
+ agent_id: this.agentId,
1733
+ });
1734
+ }
1735
+
1736
+ // ══════════════════════════════════════════════
1737
+ // Category 11: Agent Messaging (11 methods)
1738
+ // ══════════════════════════════════════════════
1739
+
1740
+ /**
1741
+ * Send a message to another agent or broadcast to all.
1742
+ * @param {Object} params
1743
+ * @param {string} [params.to] - Target agent ID (omit for broadcast)
1744
+ * @param {string} [params.type='info'] - Message type: action|info|lesson|question|status
1745
+ * @param {string} [params.subject] - Subject line (max 200 chars)
1746
+ * @param {string} params.body - Message body (max 2000 chars)
1747
+ * @param {string} [params.threadId] - Thread ID to attach message to
1748
+ * @param {boolean} [params.urgent=false] - Mark as urgent
1749
+ * @param {string} [params.docRef] - Reference to a shared doc ID
1750
+ * @param {Array<{filename: string, mime_type: string, data: string}>} [params.attachments] - File attachments (base64 data, max 3, max 5MB each)
1751
+ * @returns {Promise<{message: Object, message_id: string}>}
1752
+ */
1753
+ async sendMessage({ to, type, subject, body, threadId, urgent, docRef, attachments }) {
1754
+ const payload = {
1755
+ from_agent_id: this.agentId,
1756
+ to_agent_id: to || null,
1757
+ message_type: type || 'info',
1758
+ subject,
1759
+ body,
1760
+ thread_id: threadId,
1761
+ urgent,
1762
+ doc_ref: docRef,
1763
+ };
1764
+ if (attachments?.length) payload.attachments = attachments;
1765
+ return this._request('/api/messages', 'POST', payload);
1766
+ }
1767
+
1768
+ /**
1769
+ * Get inbox messages for this agent.
1770
+ * @param {Object} [params]
1771
+ * @param {string} [params.type] - Filter by message type
1772
+ * @param {boolean} [params.unread] - Only unread messages
1773
+ * @param {string} [params.threadId] - Filter by thread
1774
+ * @param {number} [params.limit=50] - Max messages to return
1775
+ * @returns {Promise<{messages: Object[], total: number, unread_count: number}>}
1776
+ */
1777
+ async getInbox({ type, unread, threadId, limit } = {}) {
1778
+ const params = new URLSearchParams({
1779
+ agent_id: this.agentId,
1780
+ direction: 'inbox',
1781
+ });
1782
+ if (type) params.set('type', type);
1783
+ if (unread) params.set('unread', 'true');
1784
+ if (threadId) params.set('thread_id', threadId);
1785
+ if (limit) params.set('limit', String(limit));
1786
+ return this._request(`/api/messages?${params}`, 'GET');
1787
+ }
1788
+
1789
+ /**
1790
+ * Mark messages as read.
1791
+ * @param {string[]} messageIds - Array of message IDs to mark read
1792
+ * @returns {Promise<{updated: number}>}
1793
+ */
1794
+ /**
1795
+ * Get sent messages from this agent.
1796
+ * @param {Object} [params]
1797
+ * @param {string} [params.type] - Filter by message type
1798
+ * @param {string} [params.threadId] - Filter by thread
1799
+ * @param {number} [params.limit=50] - Max messages to return
1800
+ * @returns {Promise<{messages: Object[], total: number}>}
1801
+ */
1802
+ async getSentMessages({ type, threadId, limit } = {}) {
1803
+ const params = new URLSearchParams({
1804
+ agent_id: this.agentId,
1805
+ direction: 'sent',
1806
+ });
1807
+ if (type) params.set('type', type);
1808
+ if (threadId) params.set('thread_id', threadId);
1809
+ if (limit) params.set('limit', String(limit));
1810
+ return this._request(`/api/messages?${params}`, 'GET');
1811
+ }
1812
+
1813
+ /**
1814
+ * Get all messages (inbox + sent) with full filter control.
1815
+ * @param {Object} [params]
1816
+ * @param {string} [params.direction='all'] - 'inbox' | 'sent' | 'all'
1817
+ * @param {string} [params.type] - Filter by message type
1818
+ * @param {boolean} [params.unread] - Only unread messages
1819
+ * @param {string} [params.threadId] - Filter by thread
1820
+ * @param {number} [params.limit=50] - Max messages to return
1821
+ * @returns {Promise<{messages: Object[], total: number, unread_count: number}>}
1822
+ */
1823
+ async getMessages({ direction, type, unread, threadId, limit } = {}) {
1824
+ const params = new URLSearchParams({ agent_id: this.agentId });
1825
+ if (direction) params.set('direction', direction);
1826
+ if (type) params.set('type', type);
1827
+ if (unread) params.set('unread', 'true');
1828
+ if (threadId) params.set('thread_id', threadId);
1829
+ if (limit) params.set('limit', String(limit));
1830
+ return this._request(`/api/messages?${params}`, 'GET');
1831
+ }
1832
+
1833
+ /**
1834
+ * Get a single message by ID.
1835
+ * @param {string} messageId - The message ID (msg_*)
1836
+ * @returns {Promise<{message: Object}>}
1837
+ */
1838
+ async getMessage(messageId) {
1839
+ return this._request(`/api/messages/${encodeURIComponent(messageId)}`, 'GET');
1840
+ }
1841
+
1842
+ /**
1843
+ * Mark messages as read.
1844
+ * @param {string[]} messageIds - Array of message IDs to mark read
1845
+ * @returns {Promise<{updated: number}>}
1846
+ */
1847
+ async markRead(messageIds) {
1848
+ return this._request('/api/messages', 'PATCH', {
1849
+ message_ids: messageIds,
1850
+ action: 'read',
1851
+ agent_id: this.agentId,
1852
+ });
1853
+ }
1854
+
1855
+ /**
1856
+ * Archive messages.
1857
+ * @param {string[]} messageIds - Array of message IDs to archive
1858
+ * @returns {Promise<{updated: number}>}
1859
+ */
1860
+ async archiveMessages(messageIds) {
1861
+ return this._request('/api/messages', 'PATCH', {
1862
+ message_ids: messageIds,
1863
+ action: 'archive',
1864
+ agent_id: this.agentId,
1865
+ });
1866
+ }
1867
+
1868
+ /**
1869
+ * Broadcast a message to all agents in the organization.
1870
+ * @param {Object} params
1871
+ * @param {string} [params.type='info'] - Message type
1872
+ * @param {string} [params.subject] - Subject line
1873
+ * @param {string} params.body - Message body
1874
+ * @param {string} [params.threadId] - Thread ID
1875
+ * @returns {Promise<{message: Object, message_id: string}>}
1876
+ */
1877
+ async broadcast({ type, subject, body, threadId }) {
1878
+ return this.sendMessage({ to: null, type, subject, body, threadId });
1879
+ }
1880
+
1881
+ /**
1882
+ * Create a new message thread for multi-turn conversations.
1883
+ * @param {Object} params
1884
+ * @param {string} params.name - Thread name
1885
+ * @param {string[]} [params.participants] - Agent IDs (null = open to all)
1886
+ * @returns {Promise<{thread: Object, thread_id: string}>}
1887
+ */
1888
+ async createMessageThread({ name, participants }) {
1889
+ return this._request('/api/messages/threads', 'POST', {
1890
+ name,
1891
+ participants,
1892
+ created_by: this.agentId,
1893
+ });
1894
+ }
1895
+
1896
+ /**
1897
+ * List message threads.
1898
+ * @param {Object} [params]
1899
+ * @param {string} [params.status] - Filter by status: open|resolved|archived
1900
+ * @param {number} [params.limit=20] - Max threads to return
1901
+ * @returns {Promise<{threads: Object[], total: number}>}
1902
+ */
1903
+ async getMessageThreads({ status, limit } = {}) {
1904
+ const params = new URLSearchParams({ agent_id: this.agentId });
1905
+ if (status) params.set('status', status);
1906
+ if (limit) params.set('limit', String(limit));
1907
+ return this._request(`/api/messages/threads?${params}`, 'GET');
1908
+ }
1909
+
1910
+ /**
1911
+ * Resolve (close) a message thread.
1912
+ * @param {string} threadId - Thread ID to resolve
1913
+ * @param {string} [summary] - Resolution summary
1914
+ * @returns {Promise<{thread: Object}>}
1915
+ */
1916
+ async resolveMessageThread(threadId, summary) {
1917
+ return this._request('/api/messages/threads', 'PATCH', {
1918
+ thread_id: threadId,
1919
+ status: 'resolved',
1920
+ summary,
1921
+ });
1922
+ }
1923
+
1924
+ /**
1925
+ * Create or update a shared workspace document.
1926
+ * Upserts by (org_id, name). Updates increment the version.
1927
+ * @param {Object} params
1928
+ * @param {string} params.name - Document name (unique per org)
1929
+ * @param {string} params.content - Document content
1930
+ * @returns {Promise<{doc: Object, doc_id: string}>}
1931
+ */
1932
+ async saveSharedDoc({ name, content }) {
1933
+ return this._request('/api/messages/docs', 'POST', {
1934
+ name,
1935
+ content,
1936
+ agent_id: this.agentId,
1937
+ });
1938
+ }
1939
+
1940
+ /**
1941
+ * Get an attachment's download URL or fetch its binary data.
1942
+ * @param {string} attachmentId - Attachment ID (att_*)
1943
+ * @returns {string} URL to fetch the attachment
1944
+ */
1945
+ getAttachmentUrl(attachmentId) {
1946
+ return `${this.baseUrl}/api/messages/attachments?id=${encodeURIComponent(attachmentId)}`;
1947
+ }
1948
+
1949
+ /**
1950
+ * Download an attachment as a Buffer.
1951
+ * @param {string} attachmentId - Attachment ID (att_*)
1952
+ * @returns {Promise<{data: Buffer, filename: string, mimeType: string}>}
1953
+ */
1954
+ async getAttachment(attachmentId) {
1955
+ const url = this.getAttachmentUrl(attachmentId);
1956
+ const res = await fetch(url, {
1957
+ headers: { 'x-api-key': this.apiKey },
1958
+ });
1959
+ if (!res.ok) {
1960
+ const err = await res.json().catch(() => ({}));
1961
+ throw new Error(err.error || `Attachment fetch failed: ${res.status}`);
1962
+ }
1963
+ const data = Buffer.from(await res.arrayBuffer());
1964
+ const cd = res.headers.get('content-disposition') || '';
1965
+ const match = cd.match(/filename="(.+?)"/);
1966
+ return {
1967
+ data,
1968
+ filename: match ? match[1] : attachmentId,
1969
+ mimeType: res.headers.get('content-type') || 'application/octet-stream',
1970
+ };
1971
+ }
1972
+
1973
+ // ══════════════════════════════════════════════
1974
+ // Category 13: Policy Enforcement (Guard) (2 methods)
1975
+ // ══════════════════════════════════════════════
1976
+
1977
+ /**
1978
+ * Enforce policies before a decision executes. Guard is the heart of DashClaw. It intercepts intent and returns allow/warn/block/require_approval.
1979
+ * @param {Object} context
1980
+ * @param {string} context.action_type - Action type (required)
1981
+ * @param {number} [context.risk_score] - Risk score 0-100
1982
+ * @param {string[]} [context.systems_touched] - Systems involved
1983
+ * @param {boolean} [context.reversible] - Whether the action is reversible
1984
+ * @param {string} [context.declared_goal] - What the action aims to do
1985
+ * @param {Object} [options]
1986
+ * @param {boolean} [options.includeSignals=false] - Include live signal warnings
1987
+ * @returns {Promise<{decision: string, reasons: string[], warnings: string[], matched_policies: string[], evaluated_at: string}>}
1988
+ */
1989
+ async guard(context, options = {}) {
1990
+ const params = new URLSearchParams();
1991
+ if (options.includeSignals) params.set('include_signals', 'true');
1992
+ const qs = params.toString();
1993
+ return this._request(`/api/guard${qs ? `?${qs}` : ''}`, 'POST', {
1994
+ ...context,
1995
+ agent_id: context.agent_id || this.agentId,
1996
+ });
1997
+ }
1998
+
1999
+ /**
2000
+ * Get recent guard decisions (audit log).
2001
+ * @param {Object} [filters]
2002
+ * @param {string} [filters.decision] - Filter by decision: allow|warn|block|require_approval
2003
+ * @param {number} [filters.limit=20] - Max results
2004
+ * @param {number} [filters.offset=0] - Pagination offset
2005
+ * @returns {Promise<{decisions: Object[], total: number, stats: Object}>}
2006
+ */
2007
+ async getGuardDecisions(filters = {}) {
2008
+ const params = new URLSearchParams({ agent_id: this.agentId });
2009
+ if (filters.decision) params.set('decision', filters.decision);
2010
+ if (filters.limit) params.set('limit', String(filters.limit));
2011
+ if (filters.offset) params.set('offset', String(filters.offset));
2012
+ return this._request(`/api/guard?${params}`, 'GET');
2013
+ }
2014
+
2015
+ // ══════════════════════════════════════════════════════════
2016
+ // Category 14: Policy Testing (3 methods)
2017
+ // ══════════════════════════════════════════════════════════
2018
+
2019
+ /**
2020
+ * Run guardrails tests against all active policies for this org.
2021
+ * @returns {Promise<{success: boolean, total_policies: number, total_tests: number, passed: number, failed: number, details: Object[]}>}
2022
+ */
2023
+ async testPolicies() {
2024
+ return this._request('/api/policies/test', 'POST', {
2025
+ agent_id: this.agentId,
2026
+ });
2027
+ }
2028
+
2029
+ /**
2030
+ * Generate a compliance proof report from active policies.
2031
+ * @param {Object} [options]
2032
+ * @param {string} [options.format='json'] - 'json' or 'md'
2033
+ * @returns {Promise<Object|string>}
2034
+ */
2035
+ async getProofReport(options = {}) {
2036
+ const params = new URLSearchParams();
2037
+ if (options.format) params.set('format', options.format);
2038
+ return this._request(`/api/policies/proof?${params}`, 'GET');
2039
+ }
2040
+
2041
+ /**
2042
+ * Import a policy pack or raw YAML into the org's guard policies.
2043
+ * Requires admin role.
2044
+ * @param {Object} options
2045
+ * @param {string} [options.pack] - Pack name: enterprise-strict, smb-safe, startup-growth, development
2046
+ * @param {string} [options.yaml] - Raw YAML string of policies to import
2047
+ * @returns {Promise<{imported: number, skipped: number, errors: string[], policies: Object[]}>}
2048
+ */
2049
+ async importPolicies({ pack, yaml } = {}) {
2050
+ return this._request('/api/policies/import', 'POST', { pack, yaml });
2051
+ }
2052
+
2053
+ // ══════════════════════════════════════════════════════════
2054
+ // Category 15: Compliance Engine (5 methods)
2055
+ // ══════════════════════════════════════════════════════════
2056
+
2057
+ /**
2058
+ * Map active policies to a compliance framework's controls.
2059
+ * @param {string} framework - Framework ID: soc2, iso27001, gdpr, nist-ai-rmf, imda-agentic
2060
+ * @returns {Promise<Object>} Compliance map with controls, coverage, and gaps
2061
+ */
2062
+ async mapCompliance(framework) {
2063
+ return this._request(`/api/compliance/map?framework=${encodeURIComponent(framework)}`, 'GET');
2064
+ }
2065
+
2066
+ /**
2067
+ * Run gap analysis on a compliance framework mapping.
2068
+ * @param {string} framework - Framework ID
2069
+ * @returns {Promise<Object>} Gap analysis with remediation plan and risk assessment
2070
+ */
2071
+ async analyzeGaps(framework) {
2072
+ return this._request(`/api/compliance/gaps?framework=${encodeURIComponent(framework)}`, 'GET');
2073
+ }
2074
+
2075
+ /**
2076
+ * Generate a full compliance report (markdown or JSON) and save a snapshot.
2077
+ * @param {string} framework - Framework ID
2078
+ * @param {Object} [options]
2079
+ * @param {string} [options.format='json'] - 'json' or 'md'
2080
+ * @returns {Promise<Object>}
2081
+ */
2082
+ async getComplianceReport(framework, options = {}) {
2083
+ const params = new URLSearchParams({ framework });
2084
+ if (options.format) params.set('format', options.format);
2085
+ return this._request(`/api/compliance/report?${params}`, 'GET');
2086
+ }
2087
+
2088
+ /**
2089
+ * List available compliance frameworks.
2090
+ * @returns {Promise<{frameworks: Object[]}>}
2091
+ */
2092
+ async listFrameworks() {
2093
+ return this._request('/api/compliance/frameworks', 'GET');
2094
+ }
2095
+
2096
+ /**
2097
+ * Get live compliance evidence from guard decisions and action records.
2098
+ * @param {Object} [options]
2099
+ * @param {string} [options.window='30d'] - Time window (e.g., '7d', '30d', '90d')
2100
+ * @returns {Promise<{evidence: Object}>}
2101
+ */
2102
+ async getComplianceEvidence(options = {}) {
2103
+ const params = new URLSearchParams();
2104
+ if (options.window) params.set('window', options.window);
2105
+ return this._request(`/api/compliance/evidence?${params}`, 'GET');
2106
+ }
2107
+
2108
+ // ══════════════════════════════════════════════════════════
2109
+ // Category 16: Task Routing (10 methods)
2110
+ // ══════════════════════════════════════════════════════════
2111
+
2112
+ /**
2113
+ * List routing agents registered in this org.
2114
+ * @param {Object} [filters]
2115
+ * @param {string} [filters.status] - Filter by status: available, busy, offline
2116
+ * @returns {Promise<{agents: Object[]}>}
2117
+ */
2118
+ async listRoutingAgents(filters = {}) {
2119
+ const params = new URLSearchParams();
2120
+ if (filters.status) params.set('status', filters.status);
2121
+ return this._request(`/api/routing/agents?${params}`, 'GET');
2122
+ }
2123
+
2124
+ /**
2125
+ * Register an agent for task routing.
2126
+ * @param {Object} agent
2127
+ * @param {string} agent.name - Agent name
2128
+ * @param {Array} [agent.capabilities] - Skills/capabilities (strings or {skill, priority} objects)
2129
+ * @param {number} [agent.maxConcurrent=3] - Max concurrent tasks
2130
+ * @param {string} [agent.endpoint] - Webhook endpoint for task dispatch
2131
+ * @returns {Promise<{agent: Object}>}
2132
+ */
2133
+ async registerRoutingAgent(agent) {
2134
+ return this._request('/api/routing/agents', 'POST', agent);
2135
+ }
2136
+
2137
+ /**
2138
+ * Get a single routing agent by ID.
2139
+ * @param {string} agentId - Routing agent ID
2140
+ * @returns {Promise<{agent: Object, metrics: Object[]}>}
2141
+ */
2142
+ async getRoutingAgent(agentId) {
2143
+ return this._request(`/api/routing/agents/${encodeURIComponent(agentId)}`, 'GET');
2144
+ }
2145
+
2146
+ /**
2147
+ * Update routing agent status.
2148
+ * @param {string} agentId - Routing agent ID
2149
+ * @param {string} status - New status: available, busy, offline
2150
+ * @returns {Promise<{agent: Object}>}
2151
+ */
2152
+ async updateRoutingAgentStatus(agentId, status) {
2153
+ return this._request(`/api/routing/agents/${encodeURIComponent(agentId)}`, 'PATCH', { status });
2154
+ }
2155
+
2156
+ /**
2157
+ * Unregister (delete) a routing agent.
2158
+ * @param {string} agentId - Routing agent ID
2159
+ * @returns {Promise<{deleted: Object}>}
2160
+ */
2161
+ async deleteRoutingAgent(agentId) {
2162
+ return this._request(`/api/routing/agents/${encodeURIComponent(agentId)}`, 'DELETE');
2163
+ }
2164
+
2165
+ /**
2166
+ * List routing tasks with optional filters.
2167
+ * @param {Object} [filters]
2168
+ * @param {string} [filters.status] - Filter by status
2169
+ * @param {string} [filters.assignedTo] - Filter by assigned agent
2170
+ * @param {number} [filters.limit=50] - Max results
2171
+ * @returns {Promise<{tasks: Object[]}>}
2172
+ */
2173
+ async listRoutingTasks(filters = {}) {
2174
+ const params = new URLSearchParams();
2175
+ if (filters.status) params.set('status', filters.status);
2176
+ if (filters.assignedTo) params.set('assigned_to', filters.assignedTo);
2177
+ if (filters.limit) params.set('limit', String(filters.limit));
2178
+ return this._request(`/api/routing/tasks?${params}`, 'GET');
2179
+ }
2180
+
2181
+ /**
2182
+ * Submit a task for auto-routing to the best available agent.
2183
+ * @param {Object} task
2184
+ * @param {string} task.title - Task title
2185
+ * @param {string} [task.description] - Task description
2186
+ * @param {string[]} [task.requiredSkills] - Skills needed to complete this task
2187
+ * @param {string} [task.urgency='normal'] - Urgency: low, normal, high, critical
2188
+ * @param {number} [task.timeoutSeconds=3600] - Timeout in seconds
2189
+ * @param {number} [task.maxRetries=2] - Max retry attempts
2190
+ * @param {string} [task.callbackUrl] - Webhook URL for task completion callback
2191
+ * @returns {Promise<{task: Object, routing: Object}>}
2192
+ */
2193
+ async submitRoutingTask(task) {
2194
+ return this._request('/api/routing/tasks', 'POST', task);
2195
+ }
2196
+
2197
+ /**
2198
+ * Complete a routing task.
2199
+ * @param {string} taskId - Task ID
2200
+ * @param {Object} [result]
2201
+ * @param {boolean} [result.success=true] - Whether task succeeded
2202
+ * @param {Object} [result.result] - Task result data
2203
+ * @param {string} [result.error] - Error message if failed
2204
+ * @returns {Promise<{task: Object, routing: Object}>}
2205
+ */
2206
+ async completeRoutingTask(taskId, result = {}) {
2207
+ return this._request(`/api/routing/tasks/${encodeURIComponent(taskId)}/complete`, 'POST', result);
2208
+ }
2209
+
2210
+ /**
2211
+ * Get routing statistics for the org.
2212
+ * @returns {Promise<{agents: Object, tasks: Object, routing: Object}>}
2213
+ */
2214
+ async getRoutingStats() {
2215
+ return this._request('/api/routing/stats', 'GET');
2216
+ }
2217
+
2218
+ /**
2219
+ * Get routing system health status.
2220
+ * @returns {Promise<{status: string, agents: Object, tasks: Object}>}
2221
+ */
2222
+ async getRoutingHealth() {
2223
+ return this._request('/api/routing/health', 'GET');
2224
+ }
2225
+
2226
+ // ══════════════════════════════════════════════════════════
2227
+ // Agent Pairing (3 methods)
2228
+ // ══════════════════════════════════════════════════════════
2229
+
2230
+ // createPairing, createPairingFromPrivateJwk, waitForPairing
2231
+ // (defined near the top of the class)
2232
+
2233
+ // ══════════════════════════════════════════════════════════
2234
+ // Identity Binding (2 methods)
2235
+ // ══════════════════════════════════════════════════════════
2236
+
2237
+ /**
2238
+ * Register or update an agent's public key for identity verification.
2239
+ * Requires admin API key.
2240
+ * @param {Object} identity
2241
+ * @param {string} identity.agent_id - Agent ID to register
2242
+ * @param {string} identity.public_key - PEM public key (SPKI format)
2243
+ * @param {string} [identity.algorithm='RSASSA-PKCS1-v1_5'] - Signing algorithm
2244
+ * @returns {Promise<{identity: Object}>}
2245
+ */
2246
+ async registerIdentity(identity) {
2247
+ return this._request('/api/identities', 'POST', identity);
2248
+ }
2249
+
2250
+ /**
2251
+ * List all registered agent identities for this org.
2252
+ * @returns {Promise<{identities: Object[]}>}
2253
+ */
2254
+ async getIdentities() {
2255
+ return this._request('/api/identities', 'GET');
2256
+ }
2257
+
2258
+ // ══════════════════════════════════════════════════════════
2259
+ // Organization Management (5 methods)
2260
+ // ══════════════════════════════════════════════════════════
2261
+
2262
+ /**
2263
+ * Get the current organization's details. Requires admin API key.
2264
+ * @returns {Promise<{organizations: Object[]}>}
2265
+ */
2266
+ async getOrg() {
2267
+ return this._request('/api/orgs', 'GET');
2268
+ }
2269
+
2270
+ /**
2271
+ * Create a new organization with an initial admin API key. Requires admin API key.
2272
+ * @param {Object} org
2273
+ * @param {string} org.name - Organization name
2274
+ * @param {string} org.slug - URL-safe slug (lowercase alphanumeric + hyphens)
2275
+ * @returns {Promise<{organization: Object, api_key: Object}>}
2276
+ */
2277
+ async createOrg(org) {
2278
+ return this._request('/api/orgs', 'POST', org);
2279
+ }
2280
+
2281
+ /**
2282
+ * Get organization details by ID. Requires admin API key.
2283
+ * @param {string} orgId - Organization ID
2284
+ * @returns {Promise<{organization: Object}>}
2285
+ */
2286
+ async getOrgById(orgId) {
2287
+ return this._request(`/api/orgs/${encodeURIComponent(orgId)}`, 'GET');
2288
+ }
2289
+
2290
+ /**
2291
+ * Update organization details. Requires admin API key.
2292
+ * @param {string} orgId - Organization ID
2293
+ * @param {Object} updates - Fields to update (name, slug)
2294
+ * @returns {Promise<{organization: Object}>}
2295
+ */
2296
+ async updateOrg(orgId, updates) {
2297
+ return this._request(`/api/orgs/${encodeURIComponent(orgId)}`, 'PATCH', updates);
2298
+ }
2299
+
2300
+ /**
2301
+ * List API keys for an organization. Requires admin API key.
2302
+ * @param {string} orgId - Organization ID
2303
+ * @returns {Promise<{keys: Object[]}>}
2304
+ */
2305
+ async getOrgKeys(orgId) {
2306
+ return this._request(`/api/orgs/${encodeURIComponent(orgId)}/keys`, 'GET');
2307
+ }
2308
+
2309
+ // ══════════════════════════════════════════════════════════
2310
+ // Activity Logs (1 method)
2311
+ // ══════════════════════════════════════════════════════════
2312
+
2313
+ /**
2314
+ * Get activity/audit logs for the organization.
2315
+ * @param {Object} [filters]
2316
+ * @param {string} [filters.action] - Filter by action type
2317
+ * @param {string} [filters.actor_id] - Filter by actor
2318
+ * @param {string} [filters.resource_type] - Filter by resource type
2319
+ * @param {string} [filters.before] - Before timestamp (ISO string)
2320
+ * @param {string} [filters.after] - After timestamp (ISO string)
2321
+ * @param {number} [filters.limit=50] - Max results (max 200)
2322
+ * @param {number} [filters.offset=0] - Pagination offset
2323
+ * @returns {Promise<{logs: Object[], stats: Object, pagination: Object}>}
2324
+ */
2325
+ async getActivityLogs(filters = {}) {
2326
+ const params = new URLSearchParams();
2327
+ for (const [key, value] of Object.entries(filters)) {
2328
+ if (value !== undefined && value !== null && value !== '') {
2329
+ params.set(key, String(value));
2330
+ }
2331
+ }
2332
+ return this._request(`/api/activity?${params}`, 'GET');
2333
+ }
2334
+
2335
+ // ══════════════════════════════════════════════════════════
2336
+ // Webhooks (5 methods)
2337
+ // ══════════════════════════════════════════════════════════
2338
+
2339
+ /**
2340
+ * List all webhooks for this org.
2341
+ * @returns {Promise<{webhooks: Object[]}>}
2342
+ */
2343
+ async getWebhooks() {
2344
+ return this._request('/api/webhooks', 'GET');
2345
+ }
2346
+
2347
+ /**
2348
+ * Create a new webhook subscription.
2349
+ * @param {Object} webhook
2350
+ * @param {string} webhook.url - Webhook endpoint URL
2351
+ * @param {string[]} [webhook.events] - Event types to subscribe to
2352
+ * @returns {Promise<{webhook: Object}>}
2353
+ */
2354
+ async createWebhook(webhook) {
2355
+ return this._request('/api/webhooks', 'POST', webhook);
2356
+ }
2357
+
2358
+ /**
2359
+ * Delete a webhook.
2360
+ * @param {string} webhookId - Webhook ID
2361
+ * @returns {Promise<{deleted: boolean}>}
2362
+ */
2363
+ async deleteWebhook(webhookId) {
2364
+ return this._request(`/api/webhooks?id=${encodeURIComponent(webhookId)}`, 'DELETE');
2365
+ }
2366
+
2367
+ /**
2368
+ * Send a test event to a webhook.
2369
+ * @param {string} webhookId - Webhook ID
2370
+ * @returns {Promise<{delivery: Object}>}
2371
+ */
2372
+ async testWebhook(webhookId) {
2373
+ return this._request(`/api/webhooks/${encodeURIComponent(webhookId)}/test`, 'POST');
2374
+ }
2375
+
2376
+ /**
2377
+ * Get delivery history for a webhook.
2378
+ * @param {string} webhookId - Webhook ID
2379
+ * @returns {Promise<{deliveries: Object[]}>}
2380
+ */
2381
+ async getWebhookDeliveries(webhookId) {
2382
+ return this._request(`/api/webhooks/${encodeURIComponent(webhookId)}/deliveries`, 'GET');
2383
+ }
2384
+
2385
+ // ─── Bulk Sync ────────────────────────────────────────────
2386
+
2387
+ /**
2388
+ * Sync multiple data categories in a single request.
2389
+ * Every key is optional. Only provided categories are processed.
2390
+ * @param {Object} state - Data to sync (connections, memory, goals, learning, content, inspiration, context_points, context_threads, handoffs, preferences, snippets)
2391
+ * @returns {Promise<{results: Object, total_synced: number, total_errors: number, duration_ms: number}>}
2392
+ */
2393
+ async syncState(state) {
2394
+ return this._request('/api/sync', 'POST', {
2395
+ agent_id: this.agentId,
2396
+ ...state,
2397
+ });
2398
+ }
2399
+
2400
+ // ----------------------------------------------
2401
+ // Category: Evaluations
2402
+ // ----------------------------------------------
2403
+
2404
+ /**
2405
+ * Create an evaluation score for an action.
2406
+ * @param {Object} params
2407
+ * @param {string} params.actionId - Action record ID
2408
+ * @param {string} params.scorerName - Name of the scorer
2409
+ * @param {number} params.score - Score between 0.0 and 1.0
2410
+ * @param {string} [params.label] - Category label (e.g., 'correct', 'incorrect')
2411
+ * @param {string} [params.reasoning] - Explanation of the score
2412
+ * @param {string} [params.evaluatedBy] - 'auto', 'human', or 'llm_judge'
2413
+ * @param {Object} [params.metadata] - Additional metadata
2414
+ * @returns {Promise<Object>}
2415
+ */
2416
+ async createScore({ actionId, scorerName, score, label, reasoning, evaluatedBy, metadata }) {
2417
+ return this._request('/api/evaluations', 'POST', {
2418
+ action_id: actionId,
2419
+ scorer_name: scorerName,
2420
+ score,
2421
+ label,
2422
+ reasoning,
2423
+ evaluated_by: evaluatedBy,
2424
+ metadata,
2425
+ });
2426
+ }
2427
+
2428
+ /**
2429
+ * List evaluation scores with optional filters.
2430
+ * @param {Object} [filters] - { action_id, scorer_name, evaluated_by, min_score, max_score, limit, offset, agent_id }
2431
+ * @returns {Promise<{ scores: Object[], total: number }>}
2432
+ */
2433
+ async getScores(filters = {}) {
2434
+ const params = new URLSearchParams();
2435
+ for (const [key, value] of Object.entries(filters)) {
2436
+ if (value !== undefined && value !== null && value !== '') {
2437
+ params.set(key, String(value));
2438
+ }
2439
+ }
2440
+ return this._request(`/api/evaluations?${params}`, 'GET');
2441
+ }
2442
+
2443
+ /**
2444
+ * Create a reusable scorer definition.
2445
+ * @param {Object} params
2446
+ * @param {string} params.name - Scorer name (unique per org)
2447
+ * @param {string} params.scorerType - 'regex', 'contains', 'numeric_range', 'custom_function', or 'llm_judge'
2448
+ * @param {Object} params.config - Scorer configuration
2449
+ * @param {string} [params.description] - Description
2450
+ * @returns {Promise<Object>}
2451
+ */
2452
+ async createScorer({ name, scorerType, config, description }) {
2453
+ return this._request('/api/evaluations/scorers', 'POST', {
2454
+ name,
2455
+ scorer_type: scorerType,
2456
+ config,
2457
+ description,
2458
+ });
2459
+ }
2460
+
2461
+ /**
2462
+ * List all scorers for this org.
2463
+ * @returns {Promise<{ scorers: Object[], llm_available: boolean }>}
2464
+ */
2465
+ async getScorers() {
2466
+ return this._request('/api/evaluations/scorers', 'GET');
2467
+ }
2468
+
2469
+ /**
2470
+ * Update a scorer.
2471
+ * @param {string} scorerId
2472
+ * @param {Object} updates - { name?, description?, config? }
2473
+ * @returns {Promise<Object>}
2474
+ */
2475
+ async updateScorer(scorerId, updates) {
2476
+ return this._request(`/api/evaluations/scorers/${scorerId}`, 'PATCH', updates);
2477
+ }
2478
+
2479
+ /**
2480
+ * Delete a scorer.
2481
+ * @param {string} scorerId
2482
+ * @returns {Promise<Object>}
2483
+ */
2484
+ async deleteScorer(scorerId) {
2485
+ return this._request(`/api/evaluations/scorers/${scorerId}`, 'DELETE');
2486
+ }
2487
+
2488
+ /**
2489
+ * Create and start an evaluation run.
2490
+ * @param {Object} params
2491
+ * @param {string} params.name - Run name
2492
+ * @param {string} params.scorerId - Scorer to use
2493
+ * @param {Object} [params.actionFilters] - Filters for which actions to evaluate
2494
+ * @returns {Promise<Object>}
2495
+ */
2496
+ async createEvalRun({ name, scorerId, actionFilters }) {
2497
+ return this._request('/api/evaluations/runs', 'POST', {
2498
+ name,
2499
+ scorer_id: scorerId,
2500
+ action_filters: actionFilters,
2501
+ });
2502
+ }
2503
+
2504
+ /**
2505
+ * List evaluation runs.
2506
+ * @param {Object} [filters] - { status, limit, offset }
2507
+ * @returns {Promise<{ runs: Object[] }>}
2508
+ */
2509
+ async getEvalRuns(filters = {}) {
2510
+ const params = new URLSearchParams();
2511
+ for (const [key, value] of Object.entries(filters)) {
2512
+ if (value !== undefined && value !== null && value !== '') {
2513
+ params.set(key, String(value));
2514
+ }
2515
+ }
2516
+ return this._request(`/api/evaluations/runs?${params}`, 'GET');
2517
+ }
2518
+
2519
+ /**
2520
+ * Get details of an evaluation run.
2521
+ * @param {string} runId
2522
+ * @returns {Promise<{ run: Object, distribution: Object[] }>}
2523
+ */
2524
+ async getEvalRun(runId) {
2525
+ return this._request(`/api/evaluations/runs/${runId}`, 'GET');
2526
+ }
2527
+
2528
+ /**
2529
+ * Get aggregate evaluation statistics.
2530
+ * @param {Object} [filters] - { agent_id, scorer_name, days }
2531
+ * @returns {Promise<Object>}
2532
+ */
2533
+ async getEvalStats(filters = {}) {
2534
+ const params = new URLSearchParams();
2535
+ for (const [key, value] of Object.entries(filters)) {
2536
+ if (value !== undefined && value !== null && value !== '') {
2537
+ params.set(key, String(value));
2538
+ }
2539
+ }
2540
+ return this._request(`/api/evaluations/stats?${params}`, 'GET');
2541
+ }
2542
+
2543
+ // -----------------------------------------------
2544
+ // Prompt Management
2545
+ // -----------------------------------------------
2546
+
2547
+ async listPromptTemplates({ category } = {}) {
2548
+ const params = category ? `?category=${encodeURIComponent(category)}` : '';
2549
+ return this._request(`/api/prompts/templates${params}`, 'GET');
2550
+ }
2551
+
2552
+ async createPromptTemplate({ name, description, category }) {
2553
+ return this._request('/api/prompts/templates', 'POST', { name, description, category });
2554
+ }
2555
+
2556
+ async getPromptTemplate(templateId) {
2557
+ return this._request(`/api/prompts/templates/${templateId}`, 'GET');
2558
+ }
2559
+
2560
+ async updatePromptTemplate(templateId, fields) {
2561
+ return this._request(`/api/prompts/templates/${templateId}`, 'PATCH', fields);
2562
+ }
2563
+
2564
+ async deletePromptTemplate(templateId) {
2565
+ return this._request(`/api/prompts/templates/${templateId}`, 'DELETE');
2566
+ }
2567
+
2568
+ async listPromptVersions(templateId) {
2569
+ return this._request(`/api/prompts/templates/${templateId}/versions`, 'GET');
2570
+ }
2571
+
2572
+ async createPromptVersion(templateId, { content, model_hint, parameters, changelog }) {
2573
+ return this._request(`/api/prompts/templates/${templateId}/versions`, 'POST', { content, model_hint, parameters, changelog });
2574
+ }
2575
+
2576
+ async getPromptVersion(templateId, versionId) {
2577
+ return this._request(`/api/prompts/templates/${templateId}/versions/${versionId}`, 'GET');
2578
+ }
2579
+
2580
+ async activatePromptVersion(templateId, versionId) {
2581
+ return this._request(`/api/prompts/templates/${templateId}/versions/${versionId}`, 'POST');
2582
+ }
2583
+
2584
+ async renderPrompt({ template_id, version_id, variables, action_id, agent_id, record }) {
2585
+ return this._request('/api/prompts/render', 'POST', { template_id, version_id, variables, action_id, agent_id, record });
2586
+ }
2587
+
2588
+ async listPromptRuns({ template_id, version_id, limit } = {}) {
2589
+ const params = new URLSearchParams();
2590
+ if (template_id) params.set('template_id', template_id);
2591
+ if (version_id) params.set('version_id', version_id);
2592
+ if (limit) params.set('limit', String(limit));
2593
+ const qs = params.toString() ? `?${params.toString()}` : '';
2594
+ return this._request(`/api/prompts/runs${qs}`, 'GET');
2595
+ }
2596
+
2597
+ async getPromptStats({ template_id } = {}) {
2598
+ const params = template_id ? `?template_id=${encodeURIComponent(template_id)}` : '';
2599
+ return this._request(`/api/prompts/stats${params}`, 'GET');
2600
+ }
2601
+
2602
+ // -----------------------------------------------
2603
+ // User Feedback
2604
+ // -----------------------------------------------
2605
+
2606
+ async submitFeedback({ action_id, agent_id, rating, comment, category, tags, metadata }) {
2607
+ return this._request('/api/feedback', 'POST', { action_id, agent_id, rating, comment, category, tags, metadata, source: 'sdk' });
2608
+ }
2609
+
2610
+ async listFeedback({ action_id, agent_id, category, sentiment, resolved, limit, offset } = {}) {
2611
+ const params = new URLSearchParams();
2612
+ if (action_id) params.set('action_id', action_id);
2613
+ if (agent_id) params.set('agent_id', agent_id);
2614
+ if (category) params.set('category', category);
2615
+ if (sentiment) params.set('sentiment', sentiment);
2616
+ if (resolved !== undefined) params.set('resolved', String(resolved));
2617
+ if (limit) params.set('limit', String(limit));
2618
+ if (offset) params.set('offset', String(offset));
2619
+ const qs = params.toString() ? `?${params.toString()}` : '';
2620
+ return this._request(`/api/feedback${qs}`, 'GET');
2621
+ }
2622
+
2623
+ async getFeedback(feedbackId) {
2624
+ return this._request(`/api/feedback/${feedbackId}`, 'GET');
2625
+ }
2626
+
2627
+ async resolveFeedback(feedbackId) {
2628
+ return this._request(`/api/feedback/${feedbackId}`, 'PATCH', { resolved_by: 'sdk' });
2629
+ }
2630
+
2631
+ async deleteFeedback(feedbackId) {
2632
+ return this._request(`/api/feedback/${feedbackId}`, 'DELETE');
2633
+ }
2634
+
2635
+ async getFeedbackStats({ agent_id } = {}) {
2636
+ const params = agent_id ? `?agent_id=${encodeURIComponent(agent_id)}` : '';
2637
+ return this._request(`/api/feedback/stats${params}`, 'GET');
2638
+ }
2639
+
2640
+ // -----------------------------------------------
2641
+ // Compliance Export
2642
+ // -----------------------------------------------
2643
+
2644
+ async createComplianceExport({ name, frameworks, format, window_days, include_evidence, include_remediation, include_trends }) {
2645
+ return this._request('/api/compliance/exports', 'POST', { name, frameworks, format, window_days, include_evidence, include_remediation, include_trends });
2646
+ }
2647
+
2648
+ async listComplianceExports({ limit } = {}) {
2649
+ const params = limit ? `?limit=${limit}` : '';
2650
+ return this._request(`/api/compliance/exports${params}`, 'GET');
2651
+ }
2652
+
2653
+ async getComplianceExport(exportId) {
2654
+ return this._request(`/api/compliance/exports/${exportId}`, 'GET');
2655
+ }
2656
+
2657
+ async downloadComplianceExport(exportId) {
2658
+ return this._request(`/api/compliance/exports/${exportId}/download`, 'GET');
2659
+ }
2660
+
2661
+ async deleteComplianceExport(exportId) {
2662
+ return this._request(`/api/compliance/exports/${exportId}`, 'DELETE');
2663
+ }
2664
+
2665
+ async createComplianceSchedule({ name, frameworks, format, window_days, cron_expression, include_evidence, include_remediation, include_trends }) {
2666
+ return this._request('/api/compliance/schedules', 'POST', { name, frameworks, format, window_days, cron_expression, include_evidence, include_remediation, include_trends });
2667
+ }
2668
+
2669
+ async listComplianceSchedules() {
2670
+ return this._request('/api/compliance/schedules', 'GET');
2671
+ }
2672
+
2673
+ async updateComplianceSchedule(scheduleId, fields) {
2674
+ return this._request(`/api/compliance/schedules/${scheduleId}`, 'PATCH', fields);
2675
+ }
2676
+
2677
+ async deleteComplianceSchedule(scheduleId) {
2678
+ return this._request(`/api/compliance/schedules/${scheduleId}`, 'DELETE');
2679
+ }
2680
+
2681
+ async getComplianceTrends({ framework, limit } = {}) {
2682
+ const params = new URLSearchParams();
2683
+ if (framework) params.set('framework', framework);
2684
+ if (limit) params.set('limit', String(limit));
2685
+ const qs = params.toString() ? `?${params.toString()}` : '';
2686
+ return this._request(`/api/compliance/trends${qs}`, 'GET');
2687
+ }
2688
+
2689
+ // -----------------------------------------------
2690
+ // Drift Detection
2691
+ // -----------------------------------------------
2692
+
2693
+ async computeDriftBaselines({ agent_id, lookback_days } = {}) {
2694
+ return this._request('/api/drift/alerts', 'POST', { action: 'compute_baselines', agent_id, lookback_days });
2695
+ }
2696
+
2697
+ async detectDrift({ agent_id, window_days } = {}) {
2698
+ return this._request('/api/drift/alerts', 'POST', { action: 'detect', agent_id, window_days });
2699
+ }
2700
+
2701
+ async recordDriftSnapshots() {
2702
+ return this._request('/api/drift/alerts', 'POST', { action: 'record_snapshots' });
2703
+ }
2704
+
2705
+ async listDriftAlerts({ agent_id, severity, acknowledged, limit } = {}) {
2706
+ const params = new URLSearchParams();
2707
+ if (agent_id) params.set('agent_id', agent_id);
2708
+ if (severity) params.set('severity', severity);
2709
+ if (acknowledged !== undefined) params.set('acknowledged', String(acknowledged));
2710
+ if (limit) params.set('limit', String(limit));
2711
+ const qs = params.toString() ? `?${params.toString()}` : '';
2712
+ return this._request(`/api/drift/alerts${qs}`, 'GET');
2713
+ }
2714
+
2715
+ async acknowledgeDriftAlert(alertId) {
2716
+ return this._request(`/api/drift/alerts/${alertId}`, 'PATCH');
2717
+ }
2718
+
2719
+ async deleteDriftAlert(alertId) {
2720
+ return this._request(`/api/drift/alerts/${alertId}`, 'DELETE');
2721
+ }
2722
+
2723
+ async getDriftStats({ agent_id } = {}) {
2724
+ const params = agent_id ? `?agent_id=${encodeURIComponent(agent_id)}` : '';
2725
+ return this._request(`/api/drift/stats${params}`, 'GET');
2726
+ }
2727
+
2728
+ async getDriftSnapshots({ agent_id, metric, limit } = {}) {
2729
+ const params = new URLSearchParams();
2730
+ if (agent_id) params.set('agent_id', agent_id);
2731
+ if (metric) params.set('metric', metric);
2732
+ if (limit) params.set('limit', String(limit));
2733
+ const qs = params.toString() ? `?${params.toString()}` : '';
2734
+ return this._request(`/api/drift/snapshots${qs}`, 'GET');
2735
+ }
2736
+
2737
+ async getDriftMetrics() {
2738
+ return this._request('/api/drift/metrics', 'GET');
2739
+ }
2740
+
2741
+ // -----------------------------------------------
2742
+ // Learning Analytics
2743
+ // -----------------------------------------------
2744
+
2745
+ async computeLearningVelocity({ agent_id, lookback_days, period } = {}) {
2746
+ return this._request('/api/learning/analytics/velocity', 'POST', { agent_id, lookback_days, period });
2747
+ }
2748
+
2749
+ async getLearningVelocity({ agent_id, limit } = {}) {
2750
+ const params = new URLSearchParams();
2751
+ if (agent_id) params.set('agent_id', agent_id);
2752
+ if (limit) params.set('limit', String(limit));
2753
+ const qs = params.toString() ? `?${params.toString()}` : '';
2754
+ return this._request(`/api/learning/analytics/velocity${qs}`, 'GET');
2755
+ }
2756
+
2757
+ async computeLearningCurves({ agent_id, lookback_days } = {}) {
2758
+ return this._request('/api/learning/analytics/curves', 'POST', { agent_id, lookback_days });
2759
+ }
2760
+
2761
+ async getLearningCurves({ agent_id, action_type, limit } = {}) {
2762
+ const params = new URLSearchParams();
2763
+ if (agent_id) params.set('agent_id', agent_id);
2764
+ if (action_type) params.set('action_type', action_type);
2765
+ if (limit) params.set('limit', String(limit));
2766
+ const qs = params.toString() ? `?${params.toString()}` : '';
2767
+ return this._request(`/api/learning/analytics/curves${qs}`, 'GET');
2768
+ }
2769
+
2770
+ async getLearningAnalyticsSummary({ agent_id } = {}) {
2771
+ const params = agent_id ? `?agent_id=${encodeURIComponent(agent_id)}` : '';
2772
+ return this._request(`/api/learning/analytics/summary${params}`, 'GET');
2773
+ }
2774
+
2775
+ async getMaturityLevels() {
2776
+ return this._request('/api/learning/analytics/maturity', 'GET');
2777
+ }
2778
+
2779
+ // --- Scoring Profiles -----------------------------------
2780
+
2781
+ async createScoringProfile(data) {
2782
+ return this._request('POST', '/api/scoring/profiles', data);
2783
+ }
2784
+
2785
+ async listScoringProfiles(params = {}) {
2786
+ return this._request('GET', '/api/scoring/profiles', null, params);
2787
+ }
2788
+
2789
+ async getScoringProfile(profileId) {
2790
+ return this._request('GET', `/api/scoring/profiles/${profileId}`);
2791
+ }
2792
+
2793
+ async updateScoringProfile(profileId, data) {
2794
+ return this._request('PATCH', `/api/scoring/profiles/${profileId}`, data);
2795
+ }
2796
+
2797
+ async deleteScoringProfile(profileId) {
2798
+ return this._request('DELETE', `/api/scoring/profiles/${profileId}`);
2799
+ }
2800
+
2801
+ async addScoringDimension(profileId, data) {
2802
+ return this._request('POST', `/api/scoring/profiles/${profileId}/dimensions`, data);
2803
+ }
2804
+
2805
+ async updateScoringDimension(profileId, dimensionId, data) {
2806
+ return this._request('PATCH', `/api/scoring/profiles/${profileId}/dimensions/${dimensionId}`, data);
2807
+ }
2808
+
2809
+ async deleteScoringDimension(profileId, dimensionId) {
2810
+ return this._request('DELETE', `/api/scoring/profiles/${profileId}/dimensions/${dimensionId}`);
2811
+ }
2812
+
2813
+ async scoreWithProfile(profileId, action) {
2814
+ return this._request('POST', '/api/scoring/score', { profile_id: profileId, action });
2815
+ }
2816
+
2817
+ async batchScoreWithProfile(profileId, actions) {
2818
+ return this._request('POST', '/api/scoring/score', { profile_id: profileId, actions });
2819
+ }
2820
+
2821
+ async getProfileScores(params = {}) {
2822
+ return this._request('GET', '/api/scoring/score', null, params);
2823
+ }
2824
+
2825
+ async getProfileScoreStats(profileId) {
2826
+ return this._request('GET', '/api/scoring/score', null, { profile_id: profileId, view: 'stats' });
2827
+ }
2828
+
2829
+ // --- Risk Templates ------------------------------------
2830
+
2831
+ async createRiskTemplate(data) {
2832
+ return this._request('POST', '/api/scoring/risk-templates', data);
2833
+ }
2834
+
2835
+ async listRiskTemplates(params = {}) {
2836
+ return this._request('GET', '/api/scoring/risk-templates', null, params);
2837
+ }
2838
+
2839
+ async updateRiskTemplate(templateId, data) {
2840
+ return this._request('PATCH', `/api/scoring/risk-templates/${templateId}`, data);
2841
+ }
2842
+
2843
+ async deleteRiskTemplate(templateId) {
2844
+ return this._request('DELETE', `/api/scoring/risk-templates/${templateId}`);
2845
+ }
2846
+
2847
+ // --- Auto-Calibration ----------------------------------
2848
+
2849
+ async autoCalibrate(options = {}) {
2850
+ return this._request('POST', '/api/scoring/calibrate', options);
2851
+ }
2852
+ }
2853
+
2854
+ /**
2855
+ * Error thrown when guardMode is 'enforce' and guard blocks an action.
2856
+ */
2857
+ class GuardBlockedError extends Error {
2858
+ /**
2859
+ * @param {Object} decision - Guard decision object
2860
+ */
2861
+ constructor(decision) {
2862
+ const reasons = (decision.reasons || []).join('; ') || 'no reason';
2863
+ super(`Guard blocked action: ${decision.decision}. Reasons: ${reasons}`);
2864
+ this.name = 'GuardBlockedError';
2865
+ this.decision = decision.decision;
2866
+ this.reasons = decision.reasons || [];
2867
+ this.warnings = decision.warnings || [];
2868
+ this.matchedPolicies = decision.matched_policies || [];
2869
+ this.riskScore = decision.risk_score ?? null;
2870
+ }
2871
+ }
2872
+
2873
+ /**
2874
+ * Error thrown when a human operator denies an action.
2875
+ */
2876
+ class ApprovalDeniedError extends Error {
2877
+ constructor(message) {
2878
+ super(message);
2879
+ this.name = 'ApprovalDeniedError';
2880
+ }
2881
+ }
2882
+
2883
+ // Backward compatibility alias (Legacy)
2884
+ const OpenClawAgent = DashClaw;
2885
+
2886
+ export default DashClaw;
2887
+ export { DashClaw, OpenClawAgent, GuardBlockedError, ApprovalDeniedError };