canary-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,906 @@
1
+ // src/buffer.ts
2
+ import { gzipSync } from "zlib";
3
+ import https from "https";
4
+ import http from "http";
5
+ var MAX_LEN = 1e4;
6
+ var BATCH_SIZE = 100;
7
+ var FLUSH_INTERVAL_MS = 1e4;
8
+ var MAX_EVENTS_PER_SESSION = 500;
9
+ var MAX_SESSION_CACHE_EVENTS = 5e3;
10
+ var RETRY_ATTEMPTS = 3;
11
+ var RETRY_BACKOFF_MS = [1e3, 2e3, 4e3];
12
+ var EventBuffer = class {
13
+ _buffer = [];
14
+ _sessionCache = /* @__PURE__ */ new Map();
15
+ _sessionCacheEventCount = 0;
16
+ _apiKey;
17
+ _endpoint;
18
+ _timer = null;
19
+ _stopped = false;
20
+ /** Reference to the original (unpatched) https.request for anti-recursion. */
21
+ _originalRequest;
22
+ constructor(apiKey, endpoint = "http://localhost:8000", autoFlush = true, originalRequest) {
23
+ this._apiKey = apiKey;
24
+ this._endpoint = endpoint.replace(/\/+$/, "");
25
+ this._originalRequest = originalRequest ?? https.request;
26
+ if (autoFlush) {
27
+ this._timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
28
+ this._timer.unref();
29
+ }
30
+ }
31
+ /** Add an event to the ring buffer. */
32
+ push(event) {
33
+ if (this._buffer.length >= MAX_LEN) {
34
+ this._buffer.shift();
35
+ }
36
+ this._buffer.push(event);
37
+ const sessionId = event.framework_session_id;
38
+ if (typeof sessionId === "string" && sessionId) {
39
+ if (this._sessionCacheEventCount < MAX_SESSION_CACHE_EVENTS) {
40
+ let sessionEvents = this._sessionCache.get(sessionId);
41
+ if (!sessionEvents) {
42
+ sessionEvents = [];
43
+ this._sessionCache.set(sessionId, sessionEvents);
44
+ }
45
+ if (sessionEvents.length < MAX_EVENTS_PER_SESSION) {
46
+ sessionEvents.push(event);
47
+ this._sessionCacheEventCount++;
48
+ }
49
+ }
50
+ }
51
+ }
52
+ /** Return a copy of events for the given session from the cache. */
53
+ getSessionEvents(sessionId) {
54
+ return [...this._sessionCache.get(sessionId) ?? []];
55
+ }
56
+ /** Remove session events from the cache. */
57
+ clearSession(sessionId) {
58
+ const events = this._sessionCache.get(sessionId);
59
+ if (events) {
60
+ this._sessionCacheEventCount = Math.max(
61
+ 0,
62
+ this._sessionCacheEventCount - events.length
63
+ );
64
+ this._sessionCache.delete(sessionId);
65
+ }
66
+ }
67
+ /** Remove and return up to `count` events from the buffer. */
68
+ drain(count) {
69
+ const n = Math.min(count, this._buffer.length);
70
+ return this._buffer.splice(0, n);
71
+ }
72
+ /** Get all buffered events (without draining). For reporter access. */
73
+ peek() {
74
+ return [...this._buffer];
75
+ }
76
+ /** Number of buffered events. */
77
+ get length() {
78
+ return this._buffer.length;
79
+ }
80
+ /** Drain a batch and send to backend. */
81
+ async flush() {
82
+ const events = this.drain(BATCH_SIZE);
83
+ if (events.length === 0) return;
84
+ for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt++) {
85
+ try {
86
+ await this._send(events);
87
+ return;
88
+ } catch {
89
+ if (attempt < RETRY_ATTEMPTS - 1) {
90
+ await sleep(RETRY_BACKOFF_MS[attempt]);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ /** Stop flush timer and drain all remaining events. */
96
+ async shutdown() {
97
+ this._stopped = true;
98
+ if (this._timer) {
99
+ clearInterval(this._timer);
100
+ this._timer = null;
101
+ }
102
+ while (this._buffer.length > 0) {
103
+ const events = this.drain(BATCH_SIZE);
104
+ if (events.length === 0) break;
105
+ for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt++) {
106
+ try {
107
+ await this._send(events);
108
+ break;
109
+ } catch {
110
+ if (attempt === RETRY_ATTEMPTS - 1) {
111
+ } else {
112
+ await sleep(RETRY_BACKOFF_MS[attempt]);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ /** POST events to the backend with gzip compression. */
119
+ _send(events) {
120
+ const payload = JSON.stringify({
121
+ events,
122
+ sdk_version: "0.1.0"
123
+ });
124
+ const compressed = gzipSync(Buffer.from(payload, "utf-8"));
125
+ const url = new URL(`${this._endpoint}/v1/events`);
126
+ const isHttps = url.protocol === "https:";
127
+ const requestFn = isHttps ? this._originalRequest : http.request;
128
+ return new Promise((resolve, reject) => {
129
+ const req = requestFn(
130
+ {
131
+ hostname: url.hostname,
132
+ port: url.port || (isHttps ? 443 : 80),
133
+ path: url.pathname,
134
+ method: "POST",
135
+ headers: {
136
+ "Content-Type": "application/json",
137
+ "Content-Encoding": "gzip",
138
+ Authorization: `Bearer ${this._apiKey}`,
139
+ "Content-Length": compressed.length
140
+ },
141
+ timeout: 5e3
142
+ },
143
+ (res) => {
144
+ res.resume();
145
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
146
+ resolve();
147
+ } else {
148
+ reject(new Error(`HTTP ${res.statusCode}`));
149
+ }
150
+ }
151
+ );
152
+ req.on("error", reject);
153
+ req.on("timeout", () => {
154
+ req.destroy();
155
+ reject(new Error("Request timeout"));
156
+ });
157
+ req.write(compressed);
158
+ req.end();
159
+ });
160
+ }
161
+ };
162
+ function sleep(ms) {
163
+ return new Promise((resolve) => setTimeout(resolve, ms));
164
+ }
165
+
166
+ // src/interceptors/http.ts
167
+ import http2 from "http";
168
+ import https2 from "https";
169
+ import { performance } from "perf_hooks";
170
+
171
+ // src/domains.ts
172
+ var COMMON_API_DOMAINS = {
173
+ // Payments
174
+ "api.stripe.com": "stripe",
175
+ "hooks.stripe.com": "stripe",
176
+ "api.squareup.com": "square",
177
+ "api.paypal.com": "paypal",
178
+ "api.braintreegateway.com": "braintree",
179
+ // AI / ML
180
+ "api.openai.com": "openai",
181
+ "api.anthropic.com": "anthropic",
182
+ "api.cohere.ai": "cohere",
183
+ "generativelanguage.googleapis.com": "google-ai",
184
+ "api.replicate.com": "replicate",
185
+ "api-inference.huggingface.co": "huggingface",
186
+ "api.mistral.ai": "mistral",
187
+ // Communication
188
+ "api.twilio.com": "twilio",
189
+ "api.sendgrid.com": "sendgrid",
190
+ "api.mailgun.net": "mailgun",
191
+ "api.postmarkapp.com": "postmark",
192
+ "api.resend.com": "resend",
193
+ // Developer tools
194
+ "api.github.com": "github",
195
+ "gitlab.com": "gitlab",
196
+ "api.slack.com": "slack",
197
+ "discord.com": "discord",
198
+ "api.linear.app": "linear",
199
+ // Cloud / Infrastructure
200
+ "api.cloudflare.com": "cloudflare",
201
+ "api.vercel.com": "vercel",
202
+ "api.heroku.com": "heroku",
203
+ "api.netlify.com": "netlify",
204
+ // Data / Analytics
205
+ "api.segment.io": "segment",
206
+ "api.mixpanel.com": "mixpanel",
207
+ "api.amplitude.com": "amplitude",
208
+ // Productivity / SaaS
209
+ "api.notion.so": "notion",
210
+ "api.airtable.com": "airtable",
211
+ "api.hubspot.com": "hubspot",
212
+ "api.salesforce.com": "salesforce",
213
+ // Social
214
+ "graph.facebook.com": "facebook",
215
+ "api.twitter.com": "twitter",
216
+ "api.x.com": "twitter",
217
+ // Finance
218
+ "api.plaid.com": "plaid",
219
+ "sandbox.plaid.com": "plaid",
220
+ // Commerce
221
+ "api.shopify.com": "shopify",
222
+ // Maps / Location
223
+ "maps.googleapis.com": "google-maps",
224
+ // Auth
225
+ "api.auth0.com": "auth0",
226
+ // Search
227
+ "api.algolia.com": "algolia",
228
+ // Storage
229
+ "api.supabase.co": "supabase",
230
+ "api.firebase.google.com": "firebase"
231
+ };
232
+ var DOMAIN_SUFFIXES = {};
233
+ for (const [domain, provider] of Object.entries(COMMON_API_DOMAINS)) {
234
+ const parts = domain.split(".");
235
+ if (parts.length >= 2) {
236
+ const registrable = parts.slice(-2).join(".");
237
+ DOMAIN_SUFFIXES[registrable] = provider;
238
+ }
239
+ }
240
+ function matchProvider(host) {
241
+ host = host.toLowerCase().trim();
242
+ if (host in COMMON_API_DOMAINS) {
243
+ return COMMON_API_DOMAINS[host];
244
+ }
245
+ const parts = host.split(".");
246
+ if (parts.length >= 2) {
247
+ const registrable = parts.slice(-2).join(".");
248
+ if (registrable in DOMAIN_SUFFIXES) {
249
+ return DOMAIN_SUFFIXES[registrable];
250
+ }
251
+ }
252
+ return null;
253
+ }
254
+
255
+ // src/normalize.ts
256
+ var UUID_RE = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
257
+ var STRIPE_ID_RE = /^[a-z]{2,4}_[A-Za-z0-9]*\d[A-Za-z0-9]*$/;
258
+ var NUMERIC_RE = /^\d{2,}$/;
259
+ function normalizeEndpoint(path) {
260
+ path = path.split("?")[0].split("#")[0];
261
+ path = path.replace(UUID_RE, "{id}");
262
+ const parts = path.split("/");
263
+ const normalized = [];
264
+ for (const part of parts) {
265
+ if (!part) {
266
+ normalized.push(part);
267
+ } else if (part === "{id}") {
268
+ normalized.push(part);
269
+ } else if (STRIPE_ID_RE.test(part)) {
270
+ normalized.push("{id}");
271
+ } else if (NUMERIC_RE.test(part)) {
272
+ normalized.push("{id}");
273
+ } else {
274
+ normalized.push(part);
275
+ }
276
+ }
277
+ return normalized.join("/");
278
+ }
279
+
280
+ // src/extract.ts
281
+ var COMMON_RESPONSE_KEYS = /* @__PURE__ */ new Set([
282
+ "model",
283
+ "status",
284
+ "object",
285
+ "type",
286
+ "finish_reason",
287
+ "stop_reason",
288
+ "error_code",
289
+ "error_message",
290
+ "usage",
291
+ "created",
292
+ "currency",
293
+ "livemode"
294
+ ]);
295
+ var USAGE_KEYS = /* @__PURE__ */ new Set([
296
+ "prompt_tokens",
297
+ "completion_tokens",
298
+ "total_tokens",
299
+ "input_tokens",
300
+ "output_tokens"
301
+ ]);
302
+ var MAX_BODY_SIZE = 4096;
303
+ function extractResponseFields(body) {
304
+ if (body.length > MAX_BODY_SIZE) {
305
+ body = body.slice(0, MAX_BODY_SIZE);
306
+ }
307
+ let data;
308
+ try {
309
+ data = JSON.parse(body);
310
+ } catch {
311
+ return void 0;
312
+ }
313
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
314
+ return void 0;
315
+ }
316
+ const obj = data;
317
+ const result = {};
318
+ for (const key of COMMON_RESPONSE_KEYS) {
319
+ if (key in obj) {
320
+ const val = obj[key];
321
+ if (key === "usage" && typeof val === "object" && val !== null && !Array.isArray(val)) {
322
+ const usageObj = val;
323
+ const usage = {};
324
+ for (const uk of USAGE_KEYS) {
325
+ if (uk in usageObj) {
326
+ usage[uk] = usageObj[uk];
327
+ }
328
+ }
329
+ if (Object.keys(usage).length > 0) {
330
+ result["usage"] = usage;
331
+ }
332
+ } else {
333
+ result[key] = val;
334
+ }
335
+ }
336
+ }
337
+ if (!("finish_reason" in result) && "choices" in obj) {
338
+ const choices = obj["choices"];
339
+ if (Array.isArray(choices) && choices.length > 0) {
340
+ const choice = choices[0];
341
+ if (typeof choice === "object" && choice !== null && "finish_reason" in choice) {
342
+ result["finish_reason"] = choice["finish_reason"];
343
+ }
344
+ }
345
+ }
346
+ return Object.keys(result).length > 0 ? result : void 0;
347
+ }
348
+
349
+ // src/context.ts
350
+ import { AsyncLocalStorage } from "async_hooks";
351
+ import { randomUUID } from "crypto";
352
+
353
+ // src/agent-detect.ts
354
+ var AGENT_ENV_MAP = [
355
+ { envKey: "CLAUDE_CODE", name: "claude_code" },
356
+ { envKey: "CURSOR_TRACE_ID", name: "cursor" },
357
+ { envKey: "OPENCLAW_SESSION_ID", name: "openclaw", versionKey: "OPENCLAW_VERSION" },
358
+ { envKey: "WINDSURF_SESSION_ID", name: "windsurf" },
359
+ { envKey: "CLINE_TASK_ID", name: "cline" },
360
+ { envKey: "AIDER_VERSION", name: "aider", versionKey: "AIDER_VERSION" },
361
+ { envKey: "DEVIN_SESSION_ID", name: "devin" },
362
+ { envKey: "CODEX_CLI", name: "codex" },
363
+ { envKey: "GITHUB_COPILOT", name: "github_copilot" },
364
+ { envKey: "CODY_SESSION_ID", name: "sourcegraph_cody" }
365
+ ];
366
+ var cachedInfo = null;
367
+ function detectAgent() {
368
+ if (cachedInfo) return cachedInfo;
369
+ for (const entry of AGENT_ENV_MAP) {
370
+ const val = process.env[entry.envKey];
371
+ if (val !== void 0) {
372
+ cachedInfo = {
373
+ agent_name: entry.name,
374
+ agent_version: entry.versionKey ? process.env[entry.versionKey] ?? null : null
375
+ };
376
+ return cachedInfo;
377
+ }
378
+ }
379
+ cachedInfo = { agent_name: null, agent_version: null };
380
+ return cachedInfo;
381
+ }
382
+ function resetAgentCache() {
383
+ cachedInfo = null;
384
+ }
385
+
386
+ // src/context.ts
387
+ var sessionStorage = new AsyncLocalStorage();
388
+ var sdkInstanceId = randomUUID().replace(/-/g, "");
389
+ var callSequence = 0;
390
+ function getSessionSignals() {
391
+ callSequence++;
392
+ const store = sessionStorage.getStore();
393
+ const signals = {
394
+ sdk_instance_id: sdkInstanceId,
395
+ process_id: process.pid,
396
+ call_sequence: callSequence
397
+ };
398
+ if (store?.frameworkSessionId) {
399
+ signals.framework_session_id = store.frameworkSessionId;
400
+ }
401
+ const agentInfo = detectAgent();
402
+ if (agentInfo.agent_name) {
403
+ signals.agent_name = agentInfo.agent_name;
404
+ }
405
+ if (agentInfo.agent_version) {
406
+ signals.agent_version = agentInfo.agent_version;
407
+ }
408
+ return signals;
409
+ }
410
+ function setFrameworkSession(sessionId) {
411
+ const store = sessionStorage.getStore();
412
+ if (store) {
413
+ store.frameworkSessionId = sessionId;
414
+ }
415
+ }
416
+ function clearFrameworkSession() {
417
+ const store = sessionStorage.getStore();
418
+ if (store) {
419
+ store.frameworkSessionId = "";
420
+ }
421
+ }
422
+ function runWithSession(sessionId, fn) {
423
+ return sessionStorage.run({ frameworkSessionId: sessionId }, fn);
424
+ }
425
+
426
+ // src/interceptors/http.ts
427
+ var MAX_BODY_CAPTURE = 4096;
428
+ var originalHttpRequest = http2.request;
429
+ var originalHttpGet = http2.get;
430
+ var originalHttpsRequest = https2.request;
431
+ var originalHttpsGet = https2.get;
432
+ var _buffer = null;
433
+ function wrapRequest(original, defaultProtocol) {
434
+ return function patchedRequest(...args) {
435
+ let url;
436
+ let options;
437
+ let callback;
438
+ if (typeof args[0] === "string" || args[0] instanceof URL) {
439
+ url = args[0];
440
+ if (typeof args[1] === "function") {
441
+ callback = args[1];
442
+ } else {
443
+ options = args[1];
444
+ callback = args[2];
445
+ }
446
+ } else {
447
+ options = args[0];
448
+ callback = args[1];
449
+ url = "";
450
+ }
451
+ let hostname;
452
+ let pathname = "/";
453
+ let method = "GET";
454
+ if (typeof url === "string" && url) {
455
+ try {
456
+ const parsed = new URL(url);
457
+ hostname = parsed.hostname;
458
+ pathname = parsed.pathname;
459
+ } catch {
460
+ hostname = void 0;
461
+ }
462
+ } else if (url instanceof URL) {
463
+ hostname = url.hostname;
464
+ pathname = url.pathname;
465
+ }
466
+ if (options) {
467
+ hostname = hostname || options.hostname || options.host;
468
+ if (options.path) pathname = options.path;
469
+ if (options.method) method = options.method;
470
+ }
471
+ if (hostname && hostname.includes(":")) {
472
+ hostname = hostname.split(":")[0];
473
+ }
474
+ const provider = hostname ? matchProvider(hostname) : null;
475
+ if (!provider || !_buffer) {
476
+ return original.apply(this, args);
477
+ }
478
+ const start = performance.now();
479
+ const signals = getSessionSignals();
480
+ const wrappedCallback = (res) => {
481
+ const chunks = [];
482
+ let totalSize = 0;
483
+ res.on("data", (chunk) => {
484
+ if (totalSize < MAX_BODY_CAPTURE) {
485
+ chunks.push(chunk);
486
+ totalSize += chunk.length;
487
+ }
488
+ });
489
+ res.on("end", () => {
490
+ const latencyMs = performance.now() - start;
491
+ const status = res.statusCode ?? 0;
492
+ const body = Buffer.concat(chunks).toString("utf-8").slice(0, MAX_BODY_CAPTURE);
493
+ const event = {
494
+ event_type: "http",
495
+ provider,
496
+ endpoint_pattern: normalizeEndpoint(pathname),
497
+ method: method.toUpperCase(),
498
+ status,
499
+ latency_ms: Math.round(latencyMs * 100) / 100,
500
+ ts: Date.now() / 1e3,
501
+ ...signals
502
+ };
503
+ if (status >= 400) {
504
+ event.error_body = body;
505
+ } else if (status >= 200 && status < 300) {
506
+ const fields = extractResponseFields(body);
507
+ if (fields) {
508
+ event.response_fields = fields;
509
+ }
510
+ }
511
+ _buffer.push(event);
512
+ });
513
+ if (callback) callback(res);
514
+ };
515
+ if (typeof args[0] === "string" || args[0] instanceof URL) {
516
+ if (options) {
517
+ return original.call(this, args[0], options, wrappedCallback);
518
+ } else {
519
+ return original.call(this, args[0], wrappedCallback);
520
+ }
521
+ } else {
522
+ return original.call(this, options, wrappedCallback);
523
+ }
524
+ };
525
+ }
526
+ function patchHttp(buffer) {
527
+ _buffer = buffer;
528
+ http2.request = wrapRequest(originalHttpRequest, "http:");
529
+ http2.get = wrapRequest(originalHttpGet, "http:");
530
+ https2.request = wrapRequest(originalHttpsRequest, "https:");
531
+ https2.get = wrapRequest(originalHttpsGet, "https:");
532
+ }
533
+ function unpatchHttp() {
534
+ _buffer = null;
535
+ http2.request = originalHttpRequest;
536
+ http2.get = originalHttpGet;
537
+ https2.request = originalHttpsRequest;
538
+ https2.get = originalHttpsGet;
539
+ }
540
+
541
+ // src/interceptors/fetch.ts
542
+ import { performance as performance2 } from "perf_hooks";
543
+ var MAX_BODY_CAPTURE2 = 4096;
544
+ var CLONE_READ_TIMEOUT_MS = 5e3;
545
+ var _originalFetch = null;
546
+ var _buffer2 = null;
547
+ function patchFetch(buffer) {
548
+ if (typeof globalThis.fetch === "undefined") return;
549
+ _originalFetch = globalThis.fetch;
550
+ _buffer2 = buffer;
551
+ globalThis.fetch = async function patchedFetch(input, init2) {
552
+ let url;
553
+ let method = "GET";
554
+ try {
555
+ if (typeof input === "string") {
556
+ url = new URL(input);
557
+ } else if (input instanceof URL) {
558
+ url = input;
559
+ } else if (input instanceof Request) {
560
+ url = new URL(input.url);
561
+ method = input.method;
562
+ } else {
563
+ return _originalFetch(input, init2);
564
+ }
565
+ } catch {
566
+ return _originalFetch(input, init2);
567
+ }
568
+ if (init2?.method) method = init2.method;
569
+ const provider = matchProvider(url.hostname);
570
+ if (!provider || !_buffer2) {
571
+ return _originalFetch(input, init2);
572
+ }
573
+ const start = performance2.now();
574
+ const signals = getSessionSignals();
575
+ const response = await _originalFetch(input, init2);
576
+ const latencyMs = performance2.now() - start;
577
+ const status = response.status;
578
+ let body = "";
579
+ try {
580
+ const clone = response.clone();
581
+ const text = await Promise.race([
582
+ clone.text(),
583
+ new Promise(
584
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), CLONE_READ_TIMEOUT_MS)
585
+ )
586
+ ]);
587
+ body = text.slice(0, MAX_BODY_CAPTURE2);
588
+ } catch {
589
+ }
590
+ const event = {
591
+ event_type: "http",
592
+ provider,
593
+ endpoint_pattern: normalizeEndpoint(url.pathname),
594
+ method: method.toUpperCase(),
595
+ status,
596
+ latency_ms: Math.round(latencyMs * 100) / 100,
597
+ ts: Date.now() / 1e3,
598
+ ...signals
599
+ };
600
+ if (status >= 400) {
601
+ event.error_body = body;
602
+ } else if (status >= 200 && status < 300 && body) {
603
+ const fields = extractResponseFields(body);
604
+ if (fields) {
605
+ event.response_fields = fields;
606
+ }
607
+ }
608
+ _buffer2.push(event);
609
+ return response;
610
+ };
611
+ }
612
+ function unpatchFetch() {
613
+ if (_originalFetch) {
614
+ globalThis.fetch = _originalFetch;
615
+ _originalFetch = null;
616
+ }
617
+ _buffer2 = null;
618
+ }
619
+
620
+ // src/reporter.ts
621
+ var MAX_FRICTION_POINTS = 10;
622
+ var MAX_FRICTION_LENGTH = 200;
623
+ var SessionReporter = class {
624
+ /**
625
+ * Analyze events and return feedback events for each session.
626
+ */
627
+ generateFeedback(events) {
628
+ const sessions = /* @__PURE__ */ new Map();
629
+ for (const event of events) {
630
+ if (event.event_type === "feedback") continue;
631
+ const sid = event.framework_session_id;
632
+ let list = sessions.get(sid);
633
+ if (!list) {
634
+ list = [];
635
+ sessions.set(sid, list);
636
+ }
637
+ list.push(event);
638
+ }
639
+ const feedbackEvents = [];
640
+ for (const [sessionId, sessionEvents] of sessions) {
641
+ const feedback = this._analyzeSession(sessionId, sessionEvents);
642
+ if (feedback) feedbackEvents.push(feedback);
643
+ }
644
+ return feedbackEvents;
645
+ }
646
+ _analyzeSession(sessionId, events) {
647
+ const httpEvents = events.filter((e) => e.event_type === "http");
648
+ const actionEvents = events.filter(
649
+ (e) => e.event_type === "action"
650
+ );
651
+ if (httpEvents.length === 0 && actionEvents.length === 0) return null;
652
+ const errorEvents = httpEvents.filter(
653
+ (e) => typeof e.status === "number" && e.status >= 400
654
+ );
655
+ const totalCalls = httpEvents.length;
656
+ const errorCount = errorEvents.length;
657
+ const errorRate = totalCalls > 0 ? errorCount / totalCalls : 0;
658
+ const worked = errorRate < 0.5;
659
+ const frictionPoints = this._extractFrictionPoints(errorEvents);
660
+ const context = this._buildContext(httpEvents, actionEvents, errorCount);
661
+ const errorProviders = [
662
+ ...new Set(
663
+ errorEvents.map((e) => e.provider).filter((p) => typeof p === "string")
664
+ )
665
+ ].sort();
666
+ const feedback = {
667
+ event_type: "feedback",
668
+ source: "auto_report",
669
+ worked,
670
+ context,
671
+ ts: Date.now() / 1e3
672
+ };
673
+ if (frictionPoints.length > 0) {
674
+ feedback.friction_points = frictionPoints;
675
+ }
676
+ if (sessionId) {
677
+ feedback.framework_session_id = sessionId;
678
+ }
679
+ if (errorProviders.length > 0) {
680
+ feedback.provider = errorProviders[0];
681
+ }
682
+ return feedback;
683
+ }
684
+ _extractFrictionPoints(errorEvents) {
685
+ const frictionPoints = [];
686
+ const seen = /* @__PURE__ */ new Set();
687
+ for (const event of errorEvents) {
688
+ const provider = event.provider ?? "unknown";
689
+ const endpoint = event.endpoint_pattern ?? "";
690
+ const errorBody = event.error_body ?? "";
691
+ const status = event.status ?? 0;
692
+ let point;
693
+ if (!errorBody) {
694
+ point = `${provider} ${endpoint}: HTTP ${status}`;
695
+ } else {
696
+ const message = this._extractErrorMessage(errorBody);
697
+ point = `${provider} ${endpoint}: ${message}`;
698
+ }
699
+ point = point.slice(0, MAX_FRICTION_LENGTH);
700
+ if (!seen.has(point)) {
701
+ seen.add(point);
702
+ frictionPoints.push(point);
703
+ }
704
+ if (frictionPoints.length >= MAX_FRICTION_POINTS) break;
705
+ }
706
+ return frictionPoints;
707
+ }
708
+ _extractErrorMessage(errorBody) {
709
+ try {
710
+ const data = JSON.parse(errorBody);
711
+ if (typeof data === "object" && data !== null) {
712
+ for (const key of ["message", "error", "detail", "error_message"]) {
713
+ const val = data[key];
714
+ if (typeof val === "string") return val;
715
+ if (typeof val === "object" && val !== null) {
716
+ const msg = val.message ?? val.type;
717
+ if (msg) return String(msg);
718
+ }
719
+ }
720
+ }
721
+ } catch {
722
+ }
723
+ const firstLine = errorBody.trim().split("\n")[0];
724
+ return firstLine.slice(0, MAX_FRICTION_LENGTH);
725
+ }
726
+ _buildContext(httpEvents, actionEvents, errorCount) {
727
+ const parts = [];
728
+ const providers = [
729
+ ...new Set(
730
+ httpEvents.map((e) => e.provider).filter((p) => typeof p === "string")
731
+ )
732
+ ].sort();
733
+ if (providers.length > 0) {
734
+ parts.push(`APIs: ${providers.join(", ")}`);
735
+ }
736
+ const pages = actionEvents.filter((e) => e.action_type === "page_navigate" && e.action_args).map((e) => e.action_args?.url).filter((u) => typeof u === "string" && u.length > 0);
737
+ if (pages.length > 0) {
738
+ parts.push(`${pages.length} pages visited`);
739
+ }
740
+ if (httpEvents.length > 0) {
741
+ parts.push(`${httpEvents.length} HTTP calls, ${errorCount} errors`);
742
+ }
743
+ if (actionEvents.length > 0) {
744
+ parts.push(`${actionEvents.length} browser actions`);
745
+ }
746
+ return parts.length > 0 ? parts.join("; ") : "Session completed";
747
+ }
748
+ };
749
+
750
+ // src/adapters/mcp.ts
751
+ import { performance as performance3 } from "perf_hooks";
752
+ var MAX_RESULT_SIZE = 4096;
753
+ var CanaryMCPMiddleware = class {
754
+ _buffer;
755
+ _sessionId;
756
+ constructor(buffer, sessionId) {
757
+ this._buffer = buffer;
758
+ this._sessionId = sessionId;
759
+ }
760
+ /**
761
+ * Wrap a tool handler to capture execution time, arguments, and results.
762
+ */
763
+ wrap(handler, toolName) {
764
+ const self = this;
765
+ const wrapped = async function(...args) {
766
+ const start = performance3.now();
767
+ let result;
768
+ let error;
769
+ try {
770
+ result = await handler.apply(this, args);
771
+ } catch (err) {
772
+ error = err instanceof Error ? err.message : String(err);
773
+ self._pushEvent(toolName, args, void 0, error, start);
774
+ throw err;
775
+ }
776
+ self._pushEvent(toolName, args, result, void 0, start);
777
+ return result;
778
+ };
779
+ return wrapped;
780
+ }
781
+ /**
782
+ * Manually record a tool call event.
783
+ */
784
+ onToolCall(toolName, args, result, error, latencyMs) {
785
+ const event = {
786
+ event_type: "action",
787
+ source: "mcp",
788
+ action_type: toolName,
789
+ action_args: args,
790
+ latency_ms: latencyMs ?? 0,
791
+ ts: Date.now() / 1e3
792
+ };
793
+ if (error) {
794
+ event.action_result = { error };
795
+ event.status = 500;
796
+ } else if (result !== void 0) {
797
+ const serialized = JSON.stringify(result);
798
+ event.action_result = {
799
+ value: serialized.length > MAX_RESULT_SIZE ? serialized.slice(0, MAX_RESULT_SIZE) : serialized
800
+ };
801
+ }
802
+ if (this._sessionId) {
803
+ event.framework_session_id = this._sessionId;
804
+ }
805
+ this._buffer.push(event);
806
+ }
807
+ _pushEvent(toolName, args, result, error, startTime) {
808
+ const latencyMs = performance3.now() - startTime;
809
+ let argsObj = {};
810
+ if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) {
811
+ argsObj = args[0];
812
+ } else if (args.length > 0) {
813
+ argsObj = { args };
814
+ }
815
+ this.onToolCall(toolName, argsObj, result, error, latencyMs);
816
+ }
817
+ };
818
+
819
+ // src/index.ts
820
+ var _initialized = false;
821
+ var _buffer3 = null;
822
+ var _autoReport = true;
823
+ function init(config) {
824
+ if (_initialized) return;
825
+ if (!config.apiKey.startsWith("cnry_sk_")) {
826
+ throw new Error(
827
+ "Invalid API key: must start with 'cnry_sk_'. Get your key at https://app.canary.dev"
828
+ );
829
+ }
830
+ const endpoint = config.endpoint ?? "http://localhost:8000";
831
+ _autoReport = config.autoReport ?? true;
832
+ const autoFlush = config.autoFlush ?? true;
833
+ _buffer3 = new EventBuffer(
834
+ config.apiKey,
835
+ endpoint,
836
+ autoFlush,
837
+ originalHttpsRequest
838
+ );
839
+ patchHttp(_buffer3);
840
+ patchFetch(_buffer3);
841
+ _initialized = true;
842
+ }
843
+ async function shutdown() {
844
+ if (!_initialized || !_buffer3) return;
845
+ unpatchHttp();
846
+ unpatchFetch();
847
+ if (_autoReport) {
848
+ const reporter = new SessionReporter();
849
+ const allEvents = _buffer3.peek();
850
+ const feedbackEvents = reporter.generateFeedback(allEvents);
851
+ for (const event of feedbackEvents) {
852
+ _buffer3.push(event);
853
+ }
854
+ }
855
+ await _buffer3.shutdown();
856
+ _buffer3 = null;
857
+ _initialized = false;
858
+ }
859
+ function survey(options) {
860
+ if (!_buffer3) {
861
+ throw new Error("Canary SDK not initialized. Call init() first.");
862
+ }
863
+ const agentInfo = detectAgent();
864
+ const event = {
865
+ event_type: "feedback",
866
+ source: "manual",
867
+ worked: options.worked ?? true,
868
+ context: options.context ?? "",
869
+ ts: Date.now() / 1e3,
870
+ agent_name: agentInfo.agent_name ?? void 0,
871
+ agent_version: agentInfo.agent_version ?? void 0
872
+ };
873
+ if (options.frictionPoints && options.frictionPoints.length > 0) {
874
+ event.friction_points = options.frictionPoints;
875
+ }
876
+ if (options.provider) {
877
+ event.provider = options.provider;
878
+ }
879
+ if (options.sessionId) {
880
+ event.framework_session_id = options.sessionId;
881
+ }
882
+ _buffer3.push(event);
883
+ }
884
+ function getBuffer() {
885
+ return _buffer3;
886
+ }
887
+ export {
888
+ COMMON_API_DOMAINS,
889
+ CanaryMCPMiddleware,
890
+ EventBuffer,
891
+ SessionReporter,
892
+ clearFrameworkSession,
893
+ detectAgent,
894
+ extractResponseFields,
895
+ getBuffer,
896
+ getSessionSignals,
897
+ init,
898
+ matchProvider,
899
+ normalizeEndpoint,
900
+ resetAgentCache,
901
+ runWithSession,
902
+ setFrameworkSession,
903
+ shutdown,
904
+ survey
905
+ };
906
+ //# sourceMappingURL=index.js.map