dashclaw 1.7.0 → 1.7.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.
Files changed (3) hide show
  1. package/README.md +104 -103
  2. package/dashclaw.js +465 -465
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Full reference for the DashClaw SDK (Node.js). For Python, see the [Python SDK docs](../sdk-python/README.md).
4
4
 
5
- Install, configure, and instrument your AI agents with 60+ methods across action recording, behavior guard, context management, session handoffs, security scanning, and more.
5
+ Install, configure, and instrument your AI agents with 60+ methods across action recording, behavior guard, context management, session handoffs, security scanning, and more.
6
6
 
7
7
  ---
8
8
 
@@ -19,7 +19,8 @@ npm install dashclaw
19
19
  import { DashClaw } from 'dashclaw';
20
20
 
21
21
  const claw = new DashClaw({
22
- baseUrl: 'https://your-dashboard.vercel.app',
22
+ baseUrl: process.env.DASHCLAW_BASE_URL || 'http://localhost:3000',
23
+ // Use http://localhost:3000 for local, or https://your-app.vercel.app for cloud
23
24
  apiKey: process.env.DASHCLAW_API_KEY,
24
25
  agentId: 'my-agent',
25
26
  agentName: 'My Agent',
@@ -51,52 +52,52 @@ await claw.updateOutcome(action_id, {
51
52
 
52
53
  Create a DashClaw instance. Requires Node 18+ (native fetch).
53
54
 
54
- ```javascript
55
- const claw = new DashClaw({
56
- baseUrl,
57
- apiKey,
58
- agentId,
59
- agentName,
60
- swarmId,
61
- guardMode,
62
- guardCallback,
63
- autoRecommend,
64
- recommendationConfidenceMin,
65
- recommendationCallback,
66
- hitlMode,
67
- });
68
- ```
55
+ ```javascript
56
+ const claw = new DashClaw({
57
+ baseUrl,
58
+ apiKey,
59
+ agentId,
60
+ agentName,
61
+ swarmId,
62
+ guardMode,
63
+ guardCallback,
64
+ autoRecommend,
65
+ recommendationConfidenceMin,
66
+ recommendationCallback,
67
+ hitlMode,
68
+ });
69
+ ```
69
70
 
70
71
  ### Parameters
71
72
  | Parameter | Type | Required | Description |
72
73
  |-----------|------|----------|-------------|
73
- | baseUrl | string | Yes | DashClaw dashboard URL (e.g. "https://your-app.vercel.app") |
74
+ | baseUrl | string | Yes | DashClaw dashboard URL (e.g. "http://localhost:3000" or "https://your-app.vercel.app") |
74
75
  | apiKey | string | Yes | API key for authentication (determines which org\'s data you access) |
75
76
  | agentId | string | Yes | Unique identifier for this agent |
76
77
  | agentName | string | No | Human-readable agent name |
77
- | swarmId | string | No | Swarm/group identifier if part of a multi-agent system |
78
- | guardMode | string | No | Auto guard check before createAction/track: "off" (default), "warn" (log + proceed), "enforce" (throw on block) |
79
- | guardCallback | Function | No | Called with guard decision object when guardMode is active |
80
- | autoRecommend | string | No | Recommendation auto-adapt mode: "off" (default), "warn" (record override), "enforce" (apply safe hints) |
81
- | recommendationConfidenceMin | number | No | Min recommendation confidence required for auto-adapt in enforce mode (default 70) |
82
- | recommendationCallback | Function | No | Called with recommendation adaptation details when autoRecommend is active |
83
- | hitlMode | string | No | HITL behavior: "off" (default - return 202 immediately), "wait" (automatically block and poll until approved/denied) |
84
-
85
- ### Guard Mode, Auto-Recommend, and HITL
86
- When enabled, every call to `createAction()` can run recommendation adaptation and guard checks before submission.
78
+ | swarmId | string | No | Swarm/group identifier if part of a multi-agent system |
79
+ | guardMode | string | No | Auto guard check before createAction/track: "off" (default), "warn" (log + proceed), "enforce" (throw on block) |
80
+ | guardCallback | Function | No | Called with guard decision object when guardMode is active |
81
+ | autoRecommend | string | No | Recommendation auto-adapt mode: "off" (default), "warn" (record override), "enforce" (apply safe hints) |
82
+ | recommendationConfidenceMin | number | No | Min recommendation confidence required for auto-adapt in enforce mode (default 70) |
83
+ | recommendationCallback | Function | No | Called with recommendation adaptation details when autoRecommend is active |
84
+ | hitlMode | string | No | HITL behavior: "off" (default - return 202 immediately), "wait" (automatically block and poll until approved/denied) |
85
+
86
+ ### Guard Mode, Auto-Recommend, and HITL
87
+ When enabled, every call to `createAction()` can run recommendation adaptation and guard checks before submission.
87
88
 
88
89
  ```javascript
89
90
  import { DashClaw, GuardBlockedError, ApprovalDeniedError } from 'dashclaw';
90
91
 
91
- const claw = new DashClaw({
92
- baseUrl: 'https://your-app.vercel.app',
93
- apiKey: process.env.DASHCLAW_API_KEY,
94
- agentId: 'my-agent',
95
- autoRecommend: 'enforce', // apply safe recommendation hints
96
- recommendationConfidenceMin: 80,
97
- guardMode: 'enforce', // throws GuardBlockedError on block
98
- hitlMode: 'wait', // poll until approved or throw ApprovalDeniedError
99
- });
92
+ const claw = new DashClaw({
93
+ baseUrl: 'http://localhost:3000',
94
+ apiKey: process.env.DASHCLAW_API_KEY,
95
+ agentId: 'my-agent',
96
+ autoRecommend: 'enforce', // apply safe recommendation hints
97
+ recommendationConfidenceMin: 80,
98
+ guardMode: 'enforce', // throws GuardBlockedError on block
99
+ hitlMode: 'wait', // poll until approved or throw ApprovalDeniedError
100
+ });
100
101
 
101
102
  try {
102
103
  await claw.createAction({ action_type: 'deploy', declared_goal: 'Ship v2' });
@@ -356,7 +357,7 @@ Get drift report for assumptions with risk scoring. Shows which assumptions are
356
357
 
357
358
  ## Signals
358
359
 
359
- Automatic detection of problematic agent behavior. Seven signal types fire based on action patterns no configuration required.
360
+ Automatic detection of problematic agent behavior. Seven signal types fire based on action patterns - no configuration required.
360
361
 
361
362
  ### claw.getSignals()
362
363
  Get current risk signals across all agents. Returns 7 signal types: autonomy_spike, high_impact_low_oversight, repeated_failures, stale_loop, assumption_drift, stale_assumption, and stale_running_action.
@@ -411,8 +412,8 @@ Retrieve recent guard evaluation decisions for audit and review.
411
412
 
412
413
  Push data from your agent directly to the DashClaw dashboard. All methods auto-attach the agent's agentId.
413
414
 
414
- ### claw.recordDecision(entry)
415
- Record a decision for the learning database. Track what your agent decides and why.
415
+ ### claw.recordDecision(entry)
416
+ Record a decision for the learning database. Track what your agent decides and why.
416
417
 
417
418
  **Parameters:**
418
419
  | Parameter | Type | Required | Description |
@@ -423,69 +424,69 @@ Record a decision for the learning database. Track what your agent decides and w
423
424
  | outcome | string | No | "success", "failure", or "pending" |
424
425
  | confidence | number | No | Confidence level 0-100 |
425
426
 
426
- **Returns:** `Promise<{ decision: Object }>`
427
-
428
- ### claw.getRecommendations(filters?)
429
- Get adaptive recommendations synthesized from scored historical episodes.
430
-
431
- **Parameters:**
432
- | Parameter | Type | Required | Description |
433
- |-----------|------|----------|-------------|
434
- | filters.action_type | string | No | Filter by action type |
435
- | filters.agent_id | string | No | Override agent scope (defaults to SDK agent) |
436
- | filters.include_inactive | boolean | No | Include disabled recommendations (admin/service only) |
437
- | filters.track_events | boolean | No | Record fetched telemetry (default true) |
438
- | filters.include_metrics | boolean | No | Include computed metrics in response |
439
- | filters.lookback_days | number | No | Lookback window for include_metrics |
440
- | filters.limit | number | No | Max results (default 50) |
441
-
442
- **Returns:** `Promise<{ recommendations: Object[], metrics?: Object, total: number }>`
443
-
444
- ### claw.getRecommendationMetrics(filters?)
445
- Get recommendation telemetry and effectiveness deltas.
446
-
447
- **Parameters:**
448
- | Parameter | Type | Required | Description |
449
- |-----------|------|----------|-------------|
450
- | filters.action_type | string | No | Filter by action type |
451
- | filters.agent_id | string | No | Override agent scope (defaults to SDK agent) |
452
- | filters.lookback_days | number | No | Lookback window (default 30) |
453
- | filters.limit | number | No | Max recommendations to evaluate (default 100) |
454
- | filters.include_inactive | boolean | No | Include disabled recommendations (admin/service only) |
455
-
456
- **Returns:** `Promise<{ metrics: Object[], summary: Object, lookback_days: number }>`
457
-
458
- ### claw.recordRecommendationEvents(events)
459
- Write recommendation telemetry events (single event or batch).
460
-
461
- **Returns:** `Promise<{ created: Object[], created_count: number }>`
462
-
463
- ### claw.setRecommendationActive(recommendationId, active)
464
- Enable or disable one recommendation.
465
-
466
- **Returns:** `Promise<{ recommendation: Object }>`
467
-
468
- ### claw.rebuildRecommendations(options?)
469
- Recompute recommendations from recent learning episodes.
470
-
471
- **Parameters:**
472
- | Parameter | Type | Required | Description |
473
- |-----------|------|----------|-------------|
474
- | options.action_type | string | No | Restrict rebuild to one action type |
475
- | options.lookback_days | number | No | Episode history window (default 30) |
476
- | options.min_samples | number | No | Minimum samples per recommendation (default 5) |
477
- | options.episode_limit | number | No | Episode scan cap (default 5000) |
478
- | options.action_id | string | No | Score this action before rebuilding |
479
-
480
- **Returns:** `Promise<{ recommendations: Object[], total: number, episodes_scanned: number }>`
481
-
482
- ### claw.recommendAction(action)
483
- Apply top recommendation hints to an action payload without mutating the original object.
484
-
485
- **Returns:** `Promise<{ action: Object, recommendation: Object|null, adapted_fields: string[] }>`
486
-
487
- ### claw.createGoal(goal)
488
- Create a goal in the goals tracker.
427
+ **Returns:** `Promise<{ decision: Object }>`
428
+
429
+ ### claw.getRecommendations(filters?)
430
+ Get adaptive recommendations synthesized from scored historical episodes.
431
+
432
+ **Parameters:**
433
+ | Parameter | Type | Required | Description |
434
+ |-----------|------|----------|-------------|
435
+ | filters.action_type | string | No | Filter by action type |
436
+ | filters.agent_id | string | No | Override agent scope (defaults to SDK agent) |
437
+ | filters.include_inactive | boolean | No | Include disabled recommendations (admin/service only) |
438
+ | filters.track_events | boolean | No | Record fetched telemetry (default true) |
439
+ | filters.include_metrics | boolean | No | Include computed metrics in response |
440
+ | filters.lookback_days | number | No | Lookback window for include_metrics |
441
+ | filters.limit | number | No | Max results (default 50) |
442
+
443
+ **Returns:** `Promise<{ recommendations: Object[], metrics?: Object, total: number }>`
444
+
445
+ ### claw.getRecommendationMetrics(filters?)
446
+ Get recommendation telemetry and effectiveness deltas.
447
+
448
+ **Parameters:**
449
+ | Parameter | Type | Required | Description |
450
+ |-----------|------|----------|-------------|
451
+ | filters.action_type | string | No | Filter by action type |
452
+ | filters.agent_id | string | No | Override agent scope (defaults to SDK agent) |
453
+ | filters.lookback_days | number | No | Lookback window (default 30) |
454
+ | filters.limit | number | No | Max recommendations to evaluate (default 100) |
455
+ | filters.include_inactive | boolean | No | Include disabled recommendations (admin/service only) |
456
+
457
+ **Returns:** `Promise<{ metrics: Object[], summary: Object, lookback_days: number }>`
458
+
459
+ ### claw.recordRecommendationEvents(events)
460
+ Write recommendation telemetry events (single event or batch).
461
+
462
+ **Returns:** `Promise<{ created: Object[], created_count: number }>`
463
+
464
+ ### claw.setRecommendationActive(recommendationId, active)
465
+ Enable or disable one recommendation.
466
+
467
+ **Returns:** `Promise<{ recommendation: Object }>`
468
+
469
+ ### claw.rebuildRecommendations(options?)
470
+ Recompute recommendations from recent learning episodes.
471
+
472
+ **Parameters:**
473
+ | Parameter | Type | Required | Description |
474
+ |-----------|------|----------|-------------|
475
+ | options.action_type | string | No | Restrict rebuild to one action type |
476
+ | options.lookback_days | number | No | Episode history window (default 30) |
477
+ | options.min_samples | number | No | Minimum samples per recommendation (default 5) |
478
+ | options.episode_limit | number | No | Episode scan cap (default 5000) |
479
+ | options.action_id | string | No | Score this action before rebuilding |
480
+
481
+ **Returns:** `Promise<{ recommendations: Object[], total: number, episodes_scanned: number }>`
482
+
483
+ ### claw.recommendAction(action)
484
+ Apply top recommendation hints to an action payload without mutating the original object.
485
+
486
+ **Returns:** `Promise<{ action: Object, recommendation: Object|null, adapted_fields: string[] }>`
487
+
488
+ ### claw.createGoal(goal)
489
+ Create a goal in the goals tracker.
489
490
 
490
491
  **Parameters:**
491
492
  | Parameter | Type | Required | Description |
package/dashclaw.js CHANGED
@@ -3,11 +3,11 @@
3
3
  * Full-featured agent toolkit for the DashClaw platform.
4
4
  * Zero-dependency ESM SDK — requires Node 18+ (native fetch).
5
5
  *
6
- * 60+ methods across 13+ categories:
7
- * - Action Recording (7)
8
- * - Loops & Assumptions (7)
9
- * - Signals (1)
10
- * - Dashboard Data (9)
6
+ * 60+ methods across 13+ categories:
7
+ * - Action Recording (7)
8
+ * - Loops & Assumptions (7)
9
+ * - Signals (1)
10
+ * - Dashboard Data (9)
11
11
  * - Session Handoffs (3)
12
12
  * - Context Manager (7)
13
13
  * - Automation Snippets (4)
@@ -19,63 +19,63 @@
19
19
  * - Bulk Sync (1)
20
20
  */
21
21
 
22
- class DashClaw {
22
+ class DashClaw {
23
23
  /**
24
24
  * @param {Object} options
25
- * @param {string} options.baseUrl - DashClaw base URL (e.g. "https://your-app.vercel.app")
25
+ * @param {string} options.baseUrl - DashClaw base URL (e.g. "http://localhost:3000" or "https://your-app.vercel.app")
26
26
  * @param {string} options.apiKey - API key for authentication (determines which org's data you access)
27
27
  * @param {string} options.agentId - Unique identifier for this agent
28
28
  * @param {string} [options.agentName] - Human-readable agent name
29
29
  * @param {string} [options.swarmId] - Swarm/group identifier if part of a multi-agent system
30
- * @param {string} [options.guardMode='off'] - Auto guard check before createAction: 'off' | 'warn' | 'enforce'
31
- * @param {Function} [options.guardCallback] - Called with guard decision object when guardMode is active
32
- * @param {string} [options.autoRecommend='off'] - Recommendation mode: 'off' | 'warn' | 'enforce'
33
- * @param {number} [options.recommendationConfidenceMin=70] - Minimum recommendation confidence to auto-apply in enforce mode
34
- * @param {Function} [options.recommendationCallback] - Called with recommendation adaptation details when autoRecommend is active
35
- * @param {string} [options.hitlMode='off'] - How to handle pending approvals: 'off' (return immediately) | 'wait' (block and poll)
36
- * @param {CryptoKey} [options.privateKey] - Web Crypto API Private Key for signing actions
37
- */
38
- constructor({
39
- baseUrl,
40
- apiKey,
41
- agentId,
42
- agentName,
43
- swarmId,
44
- guardMode,
45
- guardCallback,
46
- autoRecommend,
47
- recommendationConfidenceMin,
48
- recommendationCallback,
49
- hitlMode,
50
- privateKey
51
- }) {
30
+ * @param {string} [options.guardMode='off'] - Auto guard check before createAction: 'off' | 'warn' | 'enforce'
31
+ * @param {Function} [options.guardCallback] - Called with guard decision object when guardMode is active
32
+ * @param {string} [options.autoRecommend='off'] - Recommendation mode: 'off' | 'warn' | 'enforce'
33
+ * @param {number} [options.recommendationConfidenceMin=70] - Minimum recommendation confidence to auto-apply in enforce mode
34
+ * @param {Function} [options.recommendationCallback] - Called with recommendation adaptation details when autoRecommend is active
35
+ * @param {string} [options.hitlMode='off'] - How to handle pending approvals: 'off' (return immediately) | 'wait' (block and poll)
36
+ * @param {CryptoKey} [options.privateKey] - Web Crypto API Private Key for signing actions
37
+ */
38
+ constructor({
39
+ baseUrl,
40
+ apiKey,
41
+ agentId,
42
+ agentName,
43
+ swarmId,
44
+ guardMode,
45
+ guardCallback,
46
+ autoRecommend,
47
+ recommendationConfidenceMin,
48
+ recommendationCallback,
49
+ hitlMode,
50
+ privateKey
51
+ }) {
52
52
  if (!baseUrl) throw new Error('baseUrl is required');
53
53
  if (!apiKey) throw new Error('apiKey is required');
54
54
  if (!agentId) throw new Error('agentId is required');
55
55
 
56
56
  const validModes = ['off', 'warn', 'enforce'];
57
- if (guardMode && !validModes.includes(guardMode)) {
58
- throw new Error(`guardMode must be one of: ${validModes.join(', ')}`);
59
- }
60
- if (autoRecommend && !validModes.includes(autoRecommend)) {
61
- throw new Error(`autoRecommend must be one of: ${validModes.join(', ')}`);
62
- }
63
-
64
- this.baseUrl = baseUrl.replace(/\/$/, '');
65
- this.apiKey = apiKey;
57
+ if (guardMode && !validModes.includes(guardMode)) {
58
+ throw new Error(`guardMode must be one of: ${validModes.join(', ')}`);
59
+ }
60
+ if (autoRecommend && !validModes.includes(autoRecommend)) {
61
+ throw new Error(`autoRecommend must be one of: ${validModes.join(', ')}`);
62
+ }
63
+
64
+ this.baseUrl = baseUrl.replace(/\/$/, '');
65
+ this.apiKey = apiKey;
66
66
  this.agentId = agentId;
67
67
  this.agentName = agentName || null;
68
- this.swarmId = swarmId || null;
69
- this.guardMode = guardMode || 'off';
70
- this.guardCallback = guardCallback || null;
71
- this.autoRecommend = autoRecommend || 'off';
72
- const parsedConfidenceMin = Number(recommendationConfidenceMin);
73
- this.recommendationConfidenceMin = Number.isFinite(parsedConfidenceMin)
74
- ? Math.max(0, Math.min(parsedConfidenceMin, 100))
75
- : 70;
76
- this.recommendationCallback = recommendationCallback || null;
77
- this.hitlMode = hitlMode || 'off';
78
- this.privateKey = privateKey || null;
68
+ this.swarmId = swarmId || null;
69
+ this.guardMode = guardMode || 'off';
70
+ this.guardCallback = guardCallback || null;
71
+ this.autoRecommend = autoRecommend || 'off';
72
+ const parsedConfidenceMin = Number(recommendationConfidenceMin);
73
+ this.recommendationConfidenceMin = Number.isFinite(parsedConfidenceMin)
74
+ ? Math.max(0, Math.min(parsedConfidenceMin, 100))
75
+ : 70;
76
+ this.recommendationCallback = recommendationCallback || null;
77
+ this.hitlMode = hitlMode || 'off';
78
+ this.privateKey = privateKey || null;
79
79
 
80
80
  // Auto-import JWK if passed as plain object
81
81
  if (this.privateKey && typeof this.privateKey === 'object' && this.privateKey.kty) {
@@ -83,7 +83,7 @@ class DashClaw {
83
83
  }
84
84
  }
85
85
 
86
- async _importJwk(jwk) {
86
+ async _importJwk(jwk) {
87
87
  try {
88
88
  const cryptoSubtle = globalThis.crypto?.subtle || (await import('node:crypto')).webcrypto.subtle;
89
89
  this.privateKey = await cryptoSubtle.importKey(
@@ -100,7 +100,7 @@ class DashClaw {
100
100
  }
101
101
  }
102
102
 
103
- async _request(path, method, body) {
103
+ async _request(path, method, body) {
104
104
  const url = `${this.baseUrl}${path}`;
105
105
  const headers = {
106
106
  'Content-Type': 'application/json',
@@ -123,74 +123,74 @@ class DashClaw {
123
123
  }
124
124
 
125
125
  return data;
126
- }
127
-
128
- /**
129
- * Create an agent pairing request (returns a link the user can click to approve).
130
- *
131
- * @param {Object} options
132
- * @param {string} options.publicKeyPem - PEM public key (SPKI) to register for this agent.
133
- * @param {string} [options.algorithm='RSASSA-PKCS1-v1_5']
134
- * @param {string} [options.agentName]
135
- * @returns {Promise<{pairing: Object, pairing_url: string}>}
136
- */
137
- async createPairing({ publicKeyPem, algorithm = 'RSASSA-PKCS1-v1_5', agentName } = {}) {
138
- if (!publicKeyPem) throw new Error('publicKeyPem is required');
139
- return this._request('/api/pairings', 'POST', {
140
- agent_id: this.agentId,
141
- agent_name: agentName || this.agentName,
142
- public_key: publicKeyPem,
143
- algorithm,
144
- });
145
- }
146
-
147
- async _derivePublicKeyPemFromPrivateJwk(privateJwk) {
148
- // Node-only helper (works in the typical agent runtime).
149
- const { createPrivateKey, createPublicKey } = await import('node:crypto');
150
- const priv = createPrivateKey({ key: privateJwk, format: 'jwk' });
151
- const pub = createPublicKey(priv);
152
- return pub.export({ type: 'spki', format: 'pem' });
153
- }
154
-
155
- /**
156
- * Convenience: derive public PEM from a private JWK and create a pairing request.
157
- * @param {Object} privateJwk
158
- * @param {Object} [options]
159
- * @param {string} [options.agentName]
160
- */
161
- async createPairingFromPrivateJwk(privateJwk, { agentName } = {}) {
162
- if (!privateJwk) throw new Error('privateJwk is required');
163
- const publicKeyPem = await this._derivePublicKeyPemFromPrivateJwk(privateJwk);
164
- return this.createPairing({ publicKeyPem, agentName });
165
- }
166
-
167
- /**
168
- * Poll a pairing until it is approved/expired.
169
- * @param {string} pairingId
170
- * @param {Object} [options]
171
- * @param {number} [options.timeout=300000] - Max wait time (5 min)
172
- * @param {number} [options.interval=2000] - Poll interval
173
- * @returns {Promise<Object>} pairing object
174
- */
175
- async waitForPairing(pairingId, { timeout = 300000, interval = 2000 } = {}) {
176
- const start = Date.now();
177
- while (Date.now() - start < timeout) {
178
- const res = await this._request(`/api/pairings/${encodeURIComponent(pairingId)}`, 'GET');
179
- const pairing = res.pairing;
180
- if (!pairing) throw new Error('Pairing response missing pairing');
181
- if (pairing.status === 'approved') return pairing;
182
- if (pairing.status === 'expired') throw new Error('Pairing expired');
183
- await new Promise((r) => setTimeout(r, interval));
184
- }
185
- throw new Error('Timed out waiting for pairing approval');
186
- }
187
-
188
- /**
189
- * Internal: check guard policies before action creation.
190
- * Only active when guardMode is 'warn' or 'enforce'.
191
- * @param {Object} actionDef - Action definition from createAction()
192
- */
193
- async _guardCheck(actionDef) {
126
+ }
127
+
128
+ /**
129
+ * Create an agent pairing request (returns a link the user can click to approve).
130
+ *
131
+ * @param {Object} options
132
+ * @param {string} options.publicKeyPem - PEM public key (SPKI) to register for this agent.
133
+ * @param {string} [options.algorithm='RSASSA-PKCS1-v1_5']
134
+ * @param {string} [options.agentName]
135
+ * @returns {Promise<{pairing: Object, pairing_url: string}>}
136
+ */
137
+ async createPairing({ publicKeyPem, algorithm = 'RSASSA-PKCS1-v1_5', agentName } = {}) {
138
+ if (!publicKeyPem) throw new Error('publicKeyPem is required');
139
+ return this._request('/api/pairings', 'POST', {
140
+ agent_id: this.agentId,
141
+ agent_name: agentName || this.agentName,
142
+ public_key: publicKeyPem,
143
+ algorithm,
144
+ });
145
+ }
146
+
147
+ async _derivePublicKeyPemFromPrivateJwk(privateJwk) {
148
+ // Node-only helper (works in the typical agent runtime).
149
+ const { createPrivateKey, createPublicKey } = await import('node:crypto');
150
+ const priv = createPrivateKey({ key: privateJwk, format: 'jwk' });
151
+ const pub = createPublicKey(priv);
152
+ return pub.export({ type: 'spki', format: 'pem' });
153
+ }
154
+
155
+ /**
156
+ * Convenience: derive public PEM from a private JWK and create a pairing request.
157
+ * @param {Object} privateJwk
158
+ * @param {Object} [options]
159
+ * @param {string} [options.agentName]
160
+ */
161
+ async createPairingFromPrivateJwk(privateJwk, { agentName } = {}) {
162
+ if (!privateJwk) throw new Error('privateJwk is required');
163
+ const publicKeyPem = await this._derivePublicKeyPemFromPrivateJwk(privateJwk);
164
+ return this.createPairing({ publicKeyPem, agentName });
165
+ }
166
+
167
+ /**
168
+ * Poll a pairing until it is approved/expired.
169
+ * @param {string} pairingId
170
+ * @param {Object} [options]
171
+ * @param {number} [options.timeout=300000] - Max wait time (5 min)
172
+ * @param {number} [options.interval=2000] - Poll interval
173
+ * @returns {Promise<Object>} pairing object
174
+ */
175
+ async waitForPairing(pairingId, { timeout = 300000, interval = 2000 } = {}) {
176
+ const start = Date.now();
177
+ while (Date.now() - start < timeout) {
178
+ const res = await this._request(`/api/pairings/${encodeURIComponent(pairingId)}`, 'GET');
179
+ const pairing = res.pairing;
180
+ if (!pairing) throw new Error('Pairing response missing pairing');
181
+ if (pairing.status === 'approved') return pairing;
182
+ if (pairing.status === 'expired') throw new Error('Pairing expired');
183
+ await new Promise((r) => setTimeout(r, interval));
184
+ }
185
+ throw new Error('Timed out waiting for pairing approval');
186
+ }
187
+
188
+ /**
189
+ * Internal: check guard policies before action creation.
190
+ * Only active when guardMode is 'warn' or 'enforce'.
191
+ * @param {Object} actionDef - Action definition from createAction()
192
+ */
193
+ async _guardCheck(actionDef) {
194
194
  if (this.guardMode === 'off') return;
195
195
 
196
196
  const context = {
@@ -223,172 +223,172 @@ class DashClaw {
223
223
  return;
224
224
  }
225
225
 
226
- if (this.guardMode === 'enforce' && isBlocked) {
227
- throw new GuardBlockedError(decision);
228
- }
229
- }
230
-
231
- _canonicalJsonStringify(value) {
232
- const canonicalize = (v) => {
233
- if (v === null) return 'null';
234
-
235
- const t = typeof v;
236
- if (t === 'string' || t === 'number' || t === 'boolean') return JSON.stringify(v);
237
-
238
- if (t === 'undefined') return 'null';
239
-
240
- if (Array.isArray(v)) {
241
- return `[${v.map((x) => (typeof x === 'undefined' ? 'null' : canonicalize(x))).join(',')}]`;
242
- }
243
-
244
- if (t === 'object') {
245
- const keys = Object.keys(v)
246
- .filter((k) => typeof v[k] !== 'undefined')
247
- .sort();
248
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`).join(',')}}`;
249
- }
250
-
251
- return 'null';
252
- };
253
-
254
- return canonicalize(value);
255
- }
256
-
257
- _toBase64(bytes) {
258
- if (typeof btoa === 'function') {
259
- return btoa(String.fromCharCode(...bytes));
260
- }
261
- return Buffer.from(bytes).toString('base64');
262
- }
263
-
264
- _isRestrictiveDecision(decision) {
265
- return decision?.decision === 'block' || decision?.decision === 'require_approval';
266
- }
267
-
268
- _buildGuardContext(actionDef) {
269
- return {
270
- action_type: actionDef.action_type,
271
- risk_score: actionDef.risk_score,
272
- systems_touched: actionDef.systems_touched,
273
- reversible: actionDef.reversible,
274
- declared_goal: actionDef.declared_goal,
275
- agent_id: this.agentId,
276
- };
277
- }
278
-
279
- async _reportRecommendationEvent(event) {
280
- try {
281
- await this._request('/api/learning/recommendations/events', 'POST', {
282
- ...event,
283
- agent_id: event.agent_id || this.agentId,
284
- });
285
- } catch {
286
- // Telemetry should never break action execution
287
- }
288
- }
289
-
290
- async _autoRecommend(actionDef) {
291
- if (this.autoRecommend === 'off' || !actionDef?.action_type) {
292
- return { action: actionDef, recommendation: null, adapted_fields: [] };
293
- }
294
-
295
- let result;
296
- try {
297
- result = await this.recommendAction(actionDef);
298
- } catch (err) {
299
- console.warn(`[DashClaw] Recommendation fetch failed (proceeding): ${err.message}`);
300
- return { action: actionDef, recommendation: null, adapted_fields: [] };
301
- }
302
-
303
- if (this.recommendationCallback) {
304
- try { this.recommendationCallback(result); } catch { /* ignore callback errors */ }
305
- }
306
-
307
- const recommendation = result.recommendation || null;
308
- if (!recommendation) return result;
309
-
310
- const confidence = Number(recommendation.confidence || 0);
311
- if (confidence < this.recommendationConfidenceMin) {
312
- const override_reason = `confidence_below_threshold:${confidence}<${this.recommendationConfidenceMin}`;
313
- await this._reportRecommendationEvent({
314
- recommendation_id: recommendation.id,
315
- event_type: 'overridden',
316
- details: { action_type: actionDef.action_type, reason: override_reason },
317
- });
318
- return {
319
- ...result,
320
- action: {
321
- ...actionDef,
322
- recommendation_id: recommendation.id,
323
- recommendation_applied: false,
324
- recommendation_override_reason: override_reason,
325
- },
326
- };
327
- }
328
-
329
- let guardDecision = null;
330
- try {
331
- guardDecision = await this.guard(this._buildGuardContext(result.action || actionDef));
332
- } catch (err) {
333
- console.warn(`[DashClaw] Recommendation guard probe failed: ${err.message}`);
334
- }
335
-
336
- if (this._isRestrictiveDecision(guardDecision)) {
337
- const override_reason = `guard_restrictive:${guardDecision.decision}`;
338
- await this._reportRecommendationEvent({
339
- recommendation_id: recommendation.id,
340
- event_type: 'overridden',
341
- details: { action_type: actionDef.action_type, reason: override_reason },
342
- });
343
- return {
344
- ...result,
345
- action: {
346
- ...actionDef,
347
- recommendation_id: recommendation.id,
348
- recommendation_applied: false,
349
- recommendation_override_reason: override_reason,
350
- },
351
- };
352
- }
353
-
354
- if (this.autoRecommend === 'warn') {
355
- const override_reason = 'warn_mode_no_autoadapt';
356
- await this._reportRecommendationEvent({
357
- recommendation_id: recommendation.id,
358
- event_type: 'overridden',
359
- details: { action_type: actionDef.action_type, reason: override_reason },
360
- });
361
- return {
362
- ...result,
363
- action: {
364
- ...actionDef,
365
- recommendation_id: recommendation.id,
366
- recommendation_applied: false,
367
- recommendation_override_reason: override_reason,
368
- },
369
- };
370
- }
371
-
372
- await this._reportRecommendationEvent({
373
- recommendation_id: recommendation.id,
374
- event_type: 'applied',
375
- details: {
376
- action_type: actionDef.action_type,
377
- adapted_fields: result.adapted_fields || [],
378
- confidence,
379
- },
380
- });
381
-
382
- return {
383
- ...result,
384
- action: {
385
- ...(result.action || actionDef),
386
- recommendation_id: recommendation.id,
387
- recommendation_applied: true,
388
- recommendation_override_reason: null,
389
- },
390
- };
391
- }
226
+ if (this.guardMode === 'enforce' && isBlocked) {
227
+ throw new GuardBlockedError(decision);
228
+ }
229
+ }
230
+
231
+ _canonicalJsonStringify(value) {
232
+ const canonicalize = (v) => {
233
+ if (v === null) return 'null';
234
+
235
+ const t = typeof v;
236
+ if (t === 'string' || t === 'number' || t === 'boolean') return JSON.stringify(v);
237
+
238
+ if (t === 'undefined') return 'null';
239
+
240
+ if (Array.isArray(v)) {
241
+ return `[${v.map((x) => (typeof x === 'undefined' ? 'null' : canonicalize(x))).join(',')}]`;
242
+ }
243
+
244
+ if (t === 'object') {
245
+ const keys = Object.keys(v)
246
+ .filter((k) => typeof v[k] !== 'undefined')
247
+ .sort();
248
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`).join(',')}}`;
249
+ }
250
+
251
+ return 'null';
252
+ };
253
+
254
+ return canonicalize(value);
255
+ }
256
+
257
+ _toBase64(bytes) {
258
+ if (typeof btoa === 'function') {
259
+ return btoa(String.fromCharCode(...bytes));
260
+ }
261
+ return Buffer.from(bytes).toString('base64');
262
+ }
263
+
264
+ _isRestrictiveDecision(decision) {
265
+ return decision?.decision === 'block' || decision?.decision === 'require_approval';
266
+ }
267
+
268
+ _buildGuardContext(actionDef) {
269
+ return {
270
+ action_type: actionDef.action_type,
271
+ risk_score: actionDef.risk_score,
272
+ systems_touched: actionDef.systems_touched,
273
+ reversible: actionDef.reversible,
274
+ declared_goal: actionDef.declared_goal,
275
+ agent_id: this.agentId,
276
+ };
277
+ }
278
+
279
+ async _reportRecommendationEvent(event) {
280
+ try {
281
+ await this._request('/api/learning/recommendations/events', 'POST', {
282
+ ...event,
283
+ agent_id: event.agent_id || this.agentId,
284
+ });
285
+ } catch {
286
+ // Telemetry should never break action execution
287
+ }
288
+ }
289
+
290
+ async _autoRecommend(actionDef) {
291
+ if (this.autoRecommend === 'off' || !actionDef?.action_type) {
292
+ return { action: actionDef, recommendation: null, adapted_fields: [] };
293
+ }
294
+
295
+ let result;
296
+ try {
297
+ result = await this.recommendAction(actionDef);
298
+ } catch (err) {
299
+ console.warn(`[DashClaw] Recommendation fetch failed (proceeding): ${err.message}`);
300
+ return { action: actionDef, recommendation: null, adapted_fields: [] };
301
+ }
302
+
303
+ if (this.recommendationCallback) {
304
+ try { this.recommendationCallback(result); } catch { /* ignore callback errors */ }
305
+ }
306
+
307
+ const recommendation = result.recommendation || null;
308
+ if (!recommendation) return result;
309
+
310
+ const confidence = Number(recommendation.confidence || 0);
311
+ if (confidence < this.recommendationConfidenceMin) {
312
+ const override_reason = `confidence_below_threshold:${confidence}<${this.recommendationConfidenceMin}`;
313
+ await this._reportRecommendationEvent({
314
+ recommendation_id: recommendation.id,
315
+ event_type: 'overridden',
316
+ details: { action_type: actionDef.action_type, reason: override_reason },
317
+ });
318
+ return {
319
+ ...result,
320
+ action: {
321
+ ...actionDef,
322
+ recommendation_id: recommendation.id,
323
+ recommendation_applied: false,
324
+ recommendation_override_reason: override_reason,
325
+ },
326
+ };
327
+ }
328
+
329
+ let guardDecision = null;
330
+ try {
331
+ guardDecision = await this.guard(this._buildGuardContext(result.action || actionDef));
332
+ } catch (err) {
333
+ console.warn(`[DashClaw] Recommendation guard probe failed: ${err.message}`);
334
+ }
335
+
336
+ if (this._isRestrictiveDecision(guardDecision)) {
337
+ const override_reason = `guard_restrictive:${guardDecision.decision}`;
338
+ await this._reportRecommendationEvent({
339
+ recommendation_id: recommendation.id,
340
+ event_type: 'overridden',
341
+ details: { action_type: actionDef.action_type, reason: override_reason },
342
+ });
343
+ return {
344
+ ...result,
345
+ action: {
346
+ ...actionDef,
347
+ recommendation_id: recommendation.id,
348
+ recommendation_applied: false,
349
+ recommendation_override_reason: override_reason,
350
+ },
351
+ };
352
+ }
353
+
354
+ if (this.autoRecommend === 'warn') {
355
+ const override_reason = 'warn_mode_no_autoadapt';
356
+ await this._reportRecommendationEvent({
357
+ recommendation_id: recommendation.id,
358
+ event_type: 'overridden',
359
+ details: { action_type: actionDef.action_type, reason: override_reason },
360
+ });
361
+ return {
362
+ ...result,
363
+ action: {
364
+ ...actionDef,
365
+ recommendation_id: recommendation.id,
366
+ recommendation_applied: false,
367
+ recommendation_override_reason: override_reason,
368
+ },
369
+ };
370
+ }
371
+
372
+ await this._reportRecommendationEvent({
373
+ recommendation_id: recommendation.id,
374
+ event_type: 'applied',
375
+ details: {
376
+ action_type: actionDef.action_type,
377
+ adapted_fields: result.adapted_fields || [],
378
+ confidence,
379
+ },
380
+ });
381
+
382
+ return {
383
+ ...result,
384
+ action: {
385
+ ...(result.action || actionDef),
386
+ recommendation_id: recommendation.id,
387
+ recommendation_applied: true,
388
+ recommendation_override_reason: null,
389
+ },
390
+ };
391
+ }
392
392
 
393
393
  // ══════════════════════════════════════════════
394
394
  // Category 1: Action Recording (6 methods)
@@ -411,39 +411,39 @@ class DashClaw {
411
411
  * @param {number} [action.confidence=50] - Confidence level 0-100
412
412
  * @returns {Promise<{action: Object, action_id: string}>}
413
413
  */
414
- async createAction(action) {
415
- const recommendationResult = await this._autoRecommend(action);
416
- const finalAction = recommendationResult.action || action;
417
-
418
- await this._guardCheck(finalAction);
419
- if (this._pendingKeyImport) await this._pendingKeyImport;
420
-
421
- const payload = {
422
- agent_id: this.agentId,
423
- agent_name: this.agentName,
424
- swarm_id: this.swarmId,
425
- ...finalAction
426
- };
427
-
428
- let signature = null;
429
- if (this.privateKey) {
430
- try {
431
- const encoder = new TextEncoder();
432
- const data = encoder.encode(this._canonicalJsonStringify(payload));
433
- // Use global crypto or fallback to node:crypto
434
- const cryptoSubtle = globalThis.crypto?.subtle || (await import('node:crypto')).webcrypto.subtle;
435
-
436
- const sigBuffer = await cryptoSubtle.sign(
437
- { name: "RSASSA-PKCS1-v1_5" },
438
- this.privateKey,
439
- data
440
- );
441
- // Base64 encode signature
442
- signature = this._toBase64(new Uint8Array(sigBuffer));
443
- } catch (err) {
444
- throw new Error(`Failed to sign action: ${err.message}`);
445
- }
446
- }
414
+ async createAction(action) {
415
+ const recommendationResult = await this._autoRecommend(action);
416
+ const finalAction = recommendationResult.action || action;
417
+
418
+ await this._guardCheck(finalAction);
419
+ if (this._pendingKeyImport) await this._pendingKeyImport;
420
+
421
+ const payload = {
422
+ agent_id: this.agentId,
423
+ agent_name: this.agentName,
424
+ swarm_id: this.swarmId,
425
+ ...finalAction
426
+ };
427
+
428
+ let signature = null;
429
+ if (this.privateKey) {
430
+ try {
431
+ const encoder = new TextEncoder();
432
+ const data = encoder.encode(this._canonicalJsonStringify(payload));
433
+ // Use global crypto or fallback to node:crypto
434
+ const cryptoSubtle = globalThis.crypto?.subtle || (await import('node:crypto')).webcrypto.subtle;
435
+
436
+ const sigBuffer = await cryptoSubtle.sign(
437
+ { name: "RSASSA-PKCS1-v1_5" },
438
+ this.privateKey,
439
+ data
440
+ );
441
+ // Base64 encode signature
442
+ signature = this._toBase64(new Uint8Array(sigBuffer));
443
+ } catch (err) {
444
+ throw new Error(`Failed to sign action: ${err.message}`);
445
+ }
446
+ }
447
447
 
448
448
  const res = await this._request('/api/actions', 'POST', {
449
449
  ...payload,
@@ -725,153 +725,153 @@ class DashClaw {
725
725
  * @param {number} [entry.confidence] - Confidence level 0-100
726
726
  * @returns {Promise<{decision: Object}>}
727
727
  */
728
- async recordDecision(entry) {
729
- return this._request('/api/learning', 'POST', {
730
- ...entry,
731
- agent_id: this.agentId
732
- });
733
- }
734
-
735
- /**
736
- * Get adaptive learning recommendations derived from prior episodes.
737
- * @param {Object} [filters]
738
- * @param {string} [filters.action_type] - Filter by action type
739
- * @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
740
- * @param {boolean} [filters.include_inactive] - Include disabled recommendations (admin/service only)
741
- * @param {boolean} [filters.track_events=true] - Record recommendation fetched telemetry
742
- * @param {boolean} [filters.include_metrics] - Include computed metrics in the response payload
743
- * @param {number} [filters.limit=50] - Max recommendations to return
744
- * @param {number} [filters.lookback_days=30] - Lookback days used when include_metrics=true
745
- * @returns {Promise<{recommendations: Object[], metrics?: Object, total: number, lastUpdated: string}>}
746
- */
747
- async getRecommendations(filters = {}) {
748
- const params = new URLSearchParams({
749
- agent_id: filters.agent_id || this.agentId,
750
- });
751
- if (filters.action_type) params.set('action_type', filters.action_type);
752
- if (filters.limit) params.set('limit', String(filters.limit));
753
- if (filters.include_inactive) params.set('include_inactive', 'true');
754
- if (filters.track_events !== false) params.set('track_events', 'true');
755
- if (filters.include_metrics) params.set('include_metrics', 'true');
756
- if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
757
- return this._request(`/api/learning/recommendations?${params}`, 'GET');
758
- }
759
-
760
- /**
761
- * Get recommendation effectiveness metrics and telemetry aggregates.
762
- * @param {Object} [filters]
763
- * @param {string} [filters.action_type] - Filter by action type
764
- * @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
765
- * @param {number} [filters.lookback_days=30] - Lookback window for episodes/events
766
- * @param {number} [filters.limit=100] - Max recommendations considered
767
- * @param {boolean} [filters.include_inactive] - Include inactive recommendations (admin/service only)
768
- * @returns {Promise<{metrics: Object[], summary: Object, lookback_days: number, lastUpdated: string}>}
769
- */
770
- async getRecommendationMetrics(filters = {}) {
771
- const params = new URLSearchParams({
772
- agent_id: filters.agent_id || this.agentId,
773
- });
774
- if (filters.action_type) params.set('action_type', filters.action_type);
775
- if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
776
- if (filters.limit) params.set('limit', String(filters.limit));
777
- if (filters.include_inactive) params.set('include_inactive', 'true');
778
- return this._request(`/api/learning/recommendations/metrics?${params}`, 'GET');
779
- }
780
-
781
- /**
782
- * Record recommendation telemetry events (single event or batch).
783
- * @param {Object|Object[]} events
784
- * @returns {Promise<{created: Object[], created_count: number}>}
785
- */
786
- async recordRecommendationEvents(events) {
787
- if (Array.isArray(events)) {
788
- return this._request('/api/learning/recommendations/events', 'POST', { events });
789
- }
790
- return this._request('/api/learning/recommendations/events', 'POST', events || {});
791
- }
792
-
793
- /**
794
- * Enable or disable a recommendation.
795
- * @param {string} recommendationId - Recommendation ID
796
- * @param {boolean} active - Desired active state
797
- * @returns {Promise<{recommendation: Object}>}
798
- */
799
- async setRecommendationActive(recommendationId, active) {
800
- return this._request(`/api/learning/recommendations/${recommendationId}`, 'PATCH', { active: !!active });
801
- }
802
-
803
- /**
804
- * Rebuild recommendations from scored learning episodes.
805
- * @param {Object} [options]
806
- * @param {string} [options.action_type] - Scope rebuild to one action type
807
- * @param {string} [options.agent_id] - Override agent_id (defaults to SDK agent)
808
- * @param {number} [options.lookback_days=30] - Days of episode history to analyze
809
- * @param {number} [options.min_samples=5] - Minimum episodes required per recommendation
810
- * @param {number} [options.episode_limit=5000] - Episode scan cap
811
- * @param {string} [options.action_id] - Optionally score this action before rebuild
812
- * @returns {Promise<{recommendations: Object[], total: number, episodes_scanned: number}>}
813
- */
814
- async rebuildRecommendations(options = {}) {
815
- return this._request('/api/learning/recommendations', 'POST', {
816
- agent_id: options.agent_id || this.agentId,
817
- action_type: options.action_type,
818
- lookback_days: options.lookback_days,
819
- min_samples: options.min_samples,
820
- episode_limit: options.episode_limit,
821
- action_id: options.action_id,
822
- });
823
- }
824
-
825
- /**
826
- * Apply top recommendation hints to an action definition (non-destructive).
827
- * @param {Object} action - Action payload compatible with createAction()
828
- * @returns {Promise<{action: Object, recommendation: Object|null, adapted_fields: string[]}>}
829
- */
830
- async recommendAction(action) {
831
- if (!action?.action_type) {
832
- return { action, recommendation: null, adapted_fields: [] };
833
- }
834
-
835
- const response = await this.getRecommendations({ action_type: action.action_type, limit: 1 });
836
- const recommendation = response.recommendations?.[0] || null;
837
- if (!recommendation) {
838
- return { action, recommendation: null, adapted_fields: [] };
839
- }
840
-
841
- const adapted = { ...action };
842
- const adaptedFields = [];
843
- const hints = recommendation.hints || {};
844
-
845
- if (
846
- typeof hints.preferred_risk_cap === 'number' &&
847
- (adapted.risk_score === undefined || adapted.risk_score > hints.preferred_risk_cap)
848
- ) {
849
- adapted.risk_score = hints.preferred_risk_cap;
850
- adaptedFields.push('risk_score');
851
- }
852
-
853
- if (hints.prefer_reversible === true && adapted.reversible === undefined) {
854
- adapted.reversible = true;
855
- adaptedFields.push('reversible');
856
- }
857
-
858
- if (
859
- typeof hints.confidence_floor === 'number' &&
860
- (adapted.confidence === undefined || adapted.confidence < hints.confidence_floor)
861
- ) {
862
- adapted.confidence = hints.confidence_floor;
863
- adaptedFields.push('confidence');
864
- }
865
-
866
- return {
867
- action: adapted,
868
- recommendation,
869
- adapted_fields: adaptedFields,
870
- };
871
- }
872
-
873
- /**
874
- * Create a goal.
728
+ async recordDecision(entry) {
729
+ return this._request('/api/learning', 'POST', {
730
+ ...entry,
731
+ agent_id: this.agentId
732
+ });
733
+ }
734
+
735
+ /**
736
+ * Get adaptive learning recommendations derived from prior episodes.
737
+ * @param {Object} [filters]
738
+ * @param {string} [filters.action_type] - Filter by action type
739
+ * @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
740
+ * @param {boolean} [filters.include_inactive] - Include disabled recommendations (admin/service only)
741
+ * @param {boolean} [filters.track_events=true] - Record recommendation fetched telemetry
742
+ * @param {boolean} [filters.include_metrics] - Include computed metrics in the response payload
743
+ * @param {number} [filters.limit=50] - Max recommendations to return
744
+ * @param {number} [filters.lookback_days=30] - Lookback days used when include_metrics=true
745
+ * @returns {Promise<{recommendations: Object[], metrics?: Object, total: number, lastUpdated: string}>}
746
+ */
747
+ async getRecommendations(filters = {}) {
748
+ const params = new URLSearchParams({
749
+ agent_id: filters.agent_id || this.agentId,
750
+ });
751
+ if (filters.action_type) params.set('action_type', filters.action_type);
752
+ if (filters.limit) params.set('limit', String(filters.limit));
753
+ if (filters.include_inactive) params.set('include_inactive', 'true');
754
+ if (filters.track_events !== false) params.set('track_events', 'true');
755
+ if (filters.include_metrics) params.set('include_metrics', 'true');
756
+ if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
757
+ return this._request(`/api/learning/recommendations?${params}`, 'GET');
758
+ }
759
+
760
+ /**
761
+ * Get recommendation effectiveness metrics and telemetry aggregates.
762
+ * @param {Object} [filters]
763
+ * @param {string} [filters.action_type] - Filter by action type
764
+ * @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
765
+ * @param {number} [filters.lookback_days=30] - Lookback window for episodes/events
766
+ * @param {number} [filters.limit=100] - Max recommendations considered
767
+ * @param {boolean} [filters.include_inactive] - Include inactive recommendations (admin/service only)
768
+ * @returns {Promise<{metrics: Object[], summary: Object, lookback_days: number, lastUpdated: string}>}
769
+ */
770
+ async getRecommendationMetrics(filters = {}) {
771
+ const params = new URLSearchParams({
772
+ agent_id: filters.agent_id || this.agentId,
773
+ });
774
+ if (filters.action_type) params.set('action_type', filters.action_type);
775
+ if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
776
+ if (filters.limit) params.set('limit', String(filters.limit));
777
+ if (filters.include_inactive) params.set('include_inactive', 'true');
778
+ return this._request(`/api/learning/recommendations/metrics?${params}`, 'GET');
779
+ }
780
+
781
+ /**
782
+ * Record recommendation telemetry events (single event or batch).
783
+ * @param {Object|Object[]} events
784
+ * @returns {Promise<{created: Object[], created_count: number}>}
785
+ */
786
+ async recordRecommendationEvents(events) {
787
+ if (Array.isArray(events)) {
788
+ return this._request('/api/learning/recommendations/events', 'POST', { events });
789
+ }
790
+ return this._request('/api/learning/recommendations/events', 'POST', events || {});
791
+ }
792
+
793
+ /**
794
+ * Enable or disable a recommendation.
795
+ * @param {string} recommendationId - Recommendation ID
796
+ * @param {boolean} active - Desired active state
797
+ * @returns {Promise<{recommendation: Object}>}
798
+ */
799
+ async setRecommendationActive(recommendationId, active) {
800
+ return this._request(`/api/learning/recommendations/${recommendationId}`, 'PATCH', { active: !!active });
801
+ }
802
+
803
+ /**
804
+ * Rebuild recommendations from scored learning episodes.
805
+ * @param {Object} [options]
806
+ * @param {string} [options.action_type] - Scope rebuild to one action type
807
+ * @param {string} [options.agent_id] - Override agent_id (defaults to SDK agent)
808
+ * @param {number} [options.lookback_days=30] - Days of episode history to analyze
809
+ * @param {number} [options.min_samples=5] - Minimum episodes required per recommendation
810
+ * @param {number} [options.episode_limit=5000] - Episode scan cap
811
+ * @param {string} [options.action_id] - Optionally score this action before rebuild
812
+ * @returns {Promise<{recommendations: Object[], total: number, episodes_scanned: number}>}
813
+ */
814
+ async rebuildRecommendations(options = {}) {
815
+ return this._request('/api/learning/recommendations', 'POST', {
816
+ agent_id: options.agent_id || this.agentId,
817
+ action_type: options.action_type,
818
+ lookback_days: options.lookback_days,
819
+ min_samples: options.min_samples,
820
+ episode_limit: options.episode_limit,
821
+ action_id: options.action_id,
822
+ });
823
+ }
824
+
825
+ /**
826
+ * Apply top recommendation hints to an action definition (non-destructive).
827
+ * @param {Object} action - Action payload compatible with createAction()
828
+ * @returns {Promise<{action: Object, recommendation: Object|null, adapted_fields: string[]}>}
829
+ */
830
+ async recommendAction(action) {
831
+ if (!action?.action_type) {
832
+ return { action, recommendation: null, adapted_fields: [] };
833
+ }
834
+
835
+ const response = await this.getRecommendations({ action_type: action.action_type, limit: 1 });
836
+ const recommendation = response.recommendations?.[0] || null;
837
+ if (!recommendation) {
838
+ return { action, recommendation: null, adapted_fields: [] };
839
+ }
840
+
841
+ const adapted = { ...action };
842
+ const adaptedFields = [];
843
+ const hints = recommendation.hints || {};
844
+
845
+ if (
846
+ typeof hints.preferred_risk_cap === 'number' &&
847
+ (adapted.risk_score === undefined || adapted.risk_score > hints.preferred_risk_cap)
848
+ ) {
849
+ adapted.risk_score = hints.preferred_risk_cap;
850
+ adaptedFields.push('risk_score');
851
+ }
852
+
853
+ if (hints.prefer_reversible === true && adapted.reversible === undefined) {
854
+ adapted.reversible = true;
855
+ adaptedFields.push('reversible');
856
+ }
857
+
858
+ if (
859
+ typeof hints.confidence_floor === 'number' &&
860
+ (adapted.confidence === undefined || adapted.confidence < hints.confidence_floor)
861
+ ) {
862
+ adapted.confidence = hints.confidence_floor;
863
+ adaptedFields.push('confidence');
864
+ }
865
+
866
+ return {
867
+ action: adapted,
868
+ recommendation,
869
+ adapted_fields: adaptedFields,
870
+ };
871
+ }
872
+
873
+ /**
874
+ * Create a goal.
875
875
  * @param {Object} goal
876
876
  * @param {string} goal.title - Goal title
877
877
  * @param {string} [goal.category] - Goal category
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "name": "dashclaw",
3
- "version": "1.7.0",
4
- "description": "Full-featured agent toolkit for the DashClaw platform. 60+ methods for action recording, context management, session handoffs, security scanning, behavior guard, bulk sync, and more.",
5
- "type": "module",
2
+ "name": "dashclaw",
3
+ "version": "1.7.1",
4
+ "description": "Full-featured agent toolkit for the DashClaw platform. 60+ methods for action recording, context management, session handoffs, security scanning, behavior guard, bulk sync, and more.",
5
+ "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },