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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/index.cjs +959 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +250 -0
- package/dist/index.d.ts +250 -0
- package/dist/index.js +906 -0
- package/dist/index.js.map +1 -0
- package/dist/journal/server.cjs +418 -0
- package/dist/journal/server.cjs.map +1 -0
- package/dist/journal/server.d.cts +1 -0
- package/dist/journal/server.d.ts +1 -0
- package/dist/journal/server.js +426 -0
- package/dist/journal/server.js.map +1 -0
- package/dist/openclaw.cjs +843 -0
- package/dist/openclaw.cjs.map +1 -0
- package/dist/openclaw.d.cts +2 -0
- package/dist/openclaw.d.ts +2 -0
- package/dist/openclaw.js +804 -0
- package/dist/openclaw.js.map +1 -0
- package/package.json +66 -0
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
|