@nekuda/webmcp-sdk 0.4.0 → 0.6.0-dev.17.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.
- package/CHANGELOG.md +116 -6
- package/README.md +32 -0
- package/dist/define.d.ts +39 -0
- package/dist/index.d.ts +14 -6
- package/dist/index.js +303 -51
- package/dist/pages.d.ts +6 -0
- package/dist/register.d.ts +49 -9
- package/dist/spec.d.ts +15 -4
- package/dist/telemetry-events.d.ts +26 -2
- package/dist/telemetry-fields.d.ts +4 -0
- package/dist/telemetry.d.ts +47 -2
- package/dist/tracking.d.ts +31 -0
- package/dist/transport.d.ts +24 -0
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined")
|
|
5
|
-
return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
1
|
// src/define.ts
|
|
10
2
|
var NAME_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
11
3
|
var STABLE_KEY_PATTERN = /^[a-z0-9_]+(\.[a-z0-9_]+)+$/;
|
|
@@ -57,22 +49,97 @@ function defineTool(definition) {
|
|
|
57
49
|
fail(field, `must be one of ${allowed.join(" | ")} when present (got ${JSON.stringify(value)})`);
|
|
58
50
|
}
|
|
59
51
|
}
|
|
52
|
+
if (definition.pages !== undefined && (!Array.isArray(definition.pages) || definition.pages.some((page) => typeof page !== "string"))) {
|
|
53
|
+
fail("pages", "must be an array of strings when present");
|
|
54
|
+
}
|
|
60
55
|
if (typeof execute !== "function") {
|
|
61
56
|
fail("execute", "must be a function");
|
|
62
57
|
}
|
|
63
58
|
return Object.freeze({ ...definition, name });
|
|
64
59
|
}
|
|
60
|
+
// src/pages.ts
|
|
61
|
+
function pageKey(value) {
|
|
62
|
+
const [path = "", hash = ""] = value.split("#", 2);
|
|
63
|
+
const key = path.split("?", 1)[0] + (hash.startsWith("/") ? `#${hash.split("?", 1)[0]}` : "");
|
|
64
|
+
return `/${key}`.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
65
|
+
}
|
|
66
|
+
function matchPage(patterns, location) {
|
|
67
|
+
if (!patterns?.length)
|
|
68
|
+
return true;
|
|
69
|
+
const key = pageKey(location);
|
|
70
|
+
return patterns.some((pattern) => {
|
|
71
|
+
if (!pattern)
|
|
72
|
+
return true;
|
|
73
|
+
const expression = pageKey(pattern).split("/").slice(1).map((segment) => segment === "**" ? "(?:/[^/]+)*" : `/${segment.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")}`).join("");
|
|
74
|
+
return new RegExp(`^${expression}$`).test(key);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function currentPageKey() {
|
|
78
|
+
try {
|
|
79
|
+
const location = globalThis.location;
|
|
80
|
+
return location ? `${location.pathname}${location.search ?? ""}${location.hash ?? ""}` : undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
var ROUTES = Symbol.for("webmcp.sdk.page-routes");
|
|
86
|
+
function onPageChange(listener) {
|
|
87
|
+
if (typeof globalThis.addEventListener !== "function" || !globalThis.history)
|
|
88
|
+
return () => {};
|
|
89
|
+
const history = globalThis.history;
|
|
90
|
+
let routes = history[ROUTES];
|
|
91
|
+
if (!routes) {
|
|
92
|
+
const listeners = new Set;
|
|
93
|
+
routes = {
|
|
94
|
+
listeners,
|
|
95
|
+
notify: () => {
|
|
96
|
+
for (const run of listeners) {
|
|
97
|
+
try {
|
|
98
|
+
run();
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const { notify } = routes;
|
|
104
|
+
let patched = false;
|
|
105
|
+
try {
|
|
106
|
+
history[ROUTES] = routes;
|
|
107
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
108
|
+
const original = history[method];
|
|
109
|
+
history[method] = function(...args) {
|
|
110
|
+
const result = original.apply(this, args);
|
|
111
|
+
if (patched)
|
|
112
|
+
notify();
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
patched = true;
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
const { listeners, notify } = routes;
|
|
120
|
+
if (listeners.size === 0) {
|
|
121
|
+
globalThis.addEventListener("popstate", notify);
|
|
122
|
+
globalThis.addEventListener("hashchange", notify);
|
|
123
|
+
}
|
|
124
|
+
listeners.add(listener);
|
|
125
|
+
return () => {
|
|
126
|
+
listeners.delete(listener);
|
|
127
|
+
if (listeners.size === 0) {
|
|
128
|
+
globalThis.removeEventListener("popstate", notify);
|
|
129
|
+
globalThis.removeEventListener("hashchange", notify);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
65
133
|
// src/spec.ts
|
|
66
|
-
function resolveModelContext(
|
|
67
|
-
const scope = g;
|
|
134
|
+
function resolveModelContext(scope = globalThis) {
|
|
68
135
|
return scope.document?.modelContext ?? scope.navigator?.modelContext;
|
|
69
136
|
}
|
|
70
137
|
|
|
71
138
|
// src/transport.ts
|
|
72
|
-
var INGEST_BASE = "https://ingest.agentlane.
|
|
139
|
+
var INGEST_BASE = "https://ingest.agentlane.dev";
|
|
73
140
|
var DEFAULT_COLLECT_ENDPOINT = `${INGEST_BASE}/v1/collect`;
|
|
74
141
|
var DEFAULT_TELEMETRY_ENDPOINT = `${INGEST_BASE}/v1/telemetry`;
|
|
75
|
-
function tryFetch(scope, url, headers, json) {
|
|
142
|
+
function tryFetch(scope, url, headers, json, onResponse) {
|
|
76
143
|
const f = scope.fetch;
|
|
77
144
|
if (typeof f !== "function")
|
|
78
145
|
return;
|
|
@@ -83,11 +150,25 @@ function tryFetch(scope, url, headers, json) {
|
|
|
83
150
|
headers,
|
|
84
151
|
body: json
|
|
85
152
|
});
|
|
86
|
-
if (result && typeof result.
|
|
87
|
-
result.catch(() => {});
|
|
153
|
+
if (result && typeof result.then === "function") {
|
|
154
|
+
Promise.resolve(result).then((response) => onResponse?.(response)).catch(() => {});
|
|
88
155
|
}
|
|
89
156
|
} catch {}
|
|
90
157
|
}
|
|
158
|
+
var HTTP_UNAUTHORIZED = 401;
|
|
159
|
+
var KEY_REJECTED_MESSAGE = "[@nekuda/webmcp-sdk] The configured publishable key was not accepted. " + "Usage is still being recorded, but anonymously. " + "Re-run Connect to restore attributed reporting.";
|
|
160
|
+
var warnedScopes = new WeakSet;
|
|
161
|
+
function warnKeyRejected(scope) {
|
|
162
|
+
if (warnedScopes.has(scope))
|
|
163
|
+
return;
|
|
164
|
+
warnedScopes.add(scope);
|
|
165
|
+
const write = scope.console?.error;
|
|
166
|
+
if (typeof write === "function")
|
|
167
|
+
write.call(scope.console, KEY_REJECTED_MESSAGE);
|
|
168
|
+
}
|
|
169
|
+
function isUnauthorized(response) {
|
|
170
|
+
return typeof response === "object" && response !== null && response.status === HTTP_UNAUTHORIZED;
|
|
171
|
+
}
|
|
91
172
|
function sendToCollect(event, config, scope = globalThis) {
|
|
92
173
|
try {
|
|
93
174
|
const json = JSON.stringify(event);
|
|
@@ -98,16 +179,21 @@ function sendToCollect(event, config, scope = globalThis) {
|
|
|
98
179
|
tryFetch(scope, url, headers, json);
|
|
99
180
|
} catch {}
|
|
100
181
|
}
|
|
182
|
+
var TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
101
183
|
function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
|
|
102
184
|
try {
|
|
103
185
|
const json = JSON.stringify(event);
|
|
104
186
|
if (json === undefined)
|
|
105
187
|
return;
|
|
106
188
|
const url = endpoint || DEFAULT_TELEMETRY_ENDPOINT;
|
|
107
|
-
const headers = { "content-type":
|
|
108
|
-
|
|
189
|
+
const headers = { "content-type": TELEMETRY_CONTENT_TYPE };
|
|
190
|
+
const authenticated = typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
191
|
+
if (authenticated)
|
|
109
192
|
headers["x-api-key"] = apiKey;
|
|
110
|
-
tryFetch(scope, url, headers, json)
|
|
193
|
+
tryFetch(scope, url, headers, json, authenticated ? (response) => {
|
|
194
|
+
if (isUnauthorized(response))
|
|
195
|
+
warnKeyRejected(scope);
|
|
196
|
+
} : undefined);
|
|
111
197
|
} catch {}
|
|
112
198
|
}
|
|
113
199
|
var OTEL_LOGGER_NAME = "@nekuda/webmcp-sdk";
|
|
@@ -281,6 +367,14 @@ function getOrCreateSessionId(namespace) {
|
|
|
281
367
|
return id;
|
|
282
368
|
return fallbackId(memorySession, namespace, id);
|
|
283
369
|
}
|
|
370
|
+
function resolveSessionId(options, namespace) {
|
|
371
|
+
try {
|
|
372
|
+
const supplied = options.sessionId;
|
|
373
|
+
if (typeof supplied === "string" && usableId(supplied))
|
|
374
|
+
return supplied;
|
|
375
|
+
} catch {}
|
|
376
|
+
return getOrCreateSessionId(namespace);
|
|
377
|
+
}
|
|
284
378
|
function trackingOutputs(options) {
|
|
285
379
|
try {
|
|
286
380
|
if (!options || options.disabled)
|
|
@@ -331,9 +425,17 @@ function buildEventPayload(params) {
|
|
|
331
425
|
eventName: params.eventName,
|
|
332
426
|
ts: new Date().toISOString(),
|
|
333
427
|
...pageFields(),
|
|
334
|
-
...params.data
|
|
428
|
+
...params.data,
|
|
429
|
+
...isSyntheticTester() ? { syntheticTester: true } : {}
|
|
335
430
|
};
|
|
336
431
|
}
|
|
432
|
+
function isSyntheticTester() {
|
|
433
|
+
try {
|
|
434
|
+
return /^nekuda-synthetic-tester\/[0-9]/i.test(globalThis.navigator?.userAgent ?? "");
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
337
439
|
var MAX_EVENT_BYTES = 64 * 1024;
|
|
338
440
|
var MAX_ERROR_BYTES = 16 * 1024;
|
|
339
441
|
var TRUNCATABLE = ["response", "input", "error"];
|
|
@@ -469,7 +571,7 @@ function track(options, eventName, data, sinks = defaultSinks) {
|
|
|
469
571
|
const namespace = storageNamespace(options.apiKey);
|
|
470
572
|
const event = boundEventPayload(buildEventPayload({
|
|
471
573
|
visitorId: getOrCreateVisitorId(namespace),
|
|
472
|
-
sessionId:
|
|
574
|
+
sessionId: resolveSessionId(options, namespace),
|
|
473
575
|
eventName,
|
|
474
576
|
data
|
|
475
577
|
}));
|
|
@@ -761,6 +863,7 @@ var TELEMETRY_FIELDS = {
|
|
|
761
863
|
event: true,
|
|
762
864
|
ts: true,
|
|
763
865
|
sessionId: true,
|
|
866
|
+
sampleRate: true,
|
|
764
867
|
"sdk.name": true,
|
|
765
868
|
"sdk.version": true,
|
|
766
869
|
"sdk.installMode": true,
|
|
@@ -785,6 +888,7 @@ var TELEMETRY_FIELDS = {
|
|
|
785
888
|
"config.trackingEnabled": true,
|
|
786
889
|
"config.otelEnabled": true,
|
|
787
890
|
"config.customEndpoint": true,
|
|
891
|
+
"config.builtWith": true,
|
|
788
892
|
tools: true,
|
|
789
893
|
callId: true,
|
|
790
894
|
callIndex: true,
|
|
@@ -805,6 +909,8 @@ var TELEMETRY_FIELDS = {
|
|
|
805
909
|
var TELEMETRY_TOOL_FIELDS = {
|
|
806
910
|
name: true,
|
|
807
911
|
stableKey: true,
|
|
912
|
+
inventoryToolId: true,
|
|
913
|
+
contractRevision: true,
|
|
808
914
|
version: true,
|
|
809
915
|
schemaHash: true,
|
|
810
916
|
source: true,
|
|
@@ -999,9 +1105,20 @@ function shapeMetrics(inputSchema) {
|
|
|
999
1105
|
|
|
1000
1106
|
// src/telemetry.ts
|
|
1001
1107
|
var SDK_NAME = "@nekuda/webmcp-sdk";
|
|
1002
|
-
var SDK_VERSION = "0.
|
|
1108
|
+
var SDK_VERSION = "0.6.0-dev.17.1";
|
|
1003
1109
|
var INSTALL_MODES = ["npm", "cdn_snippet"];
|
|
1004
1110
|
var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
|
|
1111
|
+
function parseSampleRate(raw) {
|
|
1112
|
+
const rate = typeof raw === "string" ? Number(raw) : Number.NaN;
|
|
1113
|
+
return Number.isFinite(rate) && rate > 0 && rate < 1 ? rate : 1;
|
|
1114
|
+
}
|
|
1115
|
+
var SDK_TELEMETRY_SAMPLE_RATE = parseSampleRate(typeof __WEBMCP_TELEMETRY_SAMPLE_RATE__ === "string" ? __WEBMCP_TELEMETRY_SAMPLE_RATE__ : undefined);
|
|
1116
|
+
var SAMPLED_EVENTS = new Set(["sdk_init", "tool_registration"]);
|
|
1117
|
+
function pageSampled(sessionId, rate = SDK_TELEMETRY_SAMPLE_RATE) {
|
|
1118
|
+
if (rate >= 1)
|
|
1119
|
+
return true;
|
|
1120
|
+
return Number.parseInt(fnv1a(`sample:${sessionId}`), 16) / 4294967296 < rate;
|
|
1121
|
+
}
|
|
1005
1122
|
function isFieldParent(value) {
|
|
1006
1123
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1007
1124
|
}
|
|
@@ -1122,12 +1239,15 @@ function buildInitEvent(params = {}) {
|
|
|
1122
1239
|
}
|
|
1123
1240
|
function configFields(tracking) {
|
|
1124
1241
|
const { toBackend, toOtel } = trackingOutputs(tracking);
|
|
1242
|
+
const builtWith = safe(() => tracking?.builtWith);
|
|
1125
1243
|
return {
|
|
1126
1244
|
trackingEnabled: toBackend,
|
|
1127
1245
|
otelEnabled: toOtel,
|
|
1128
|
-
customEndpoint: Boolean(safe(() => tracking?.endpoint))
|
|
1246
|
+
customEndpoint: Boolean(safe(() => tracking?.endpoint)),
|
|
1247
|
+
...typeof builtWith === "string" && builtWith.length > 0 && builtWith.length <= MAX_BUILT_WITH_LENGTH ? { builtWith } : {}
|
|
1129
1248
|
};
|
|
1130
1249
|
}
|
|
1250
|
+
var MAX_BUILT_WITH_LENGTH = 64;
|
|
1131
1251
|
var MAX_TOOL_FIELD_BYTES = 4 * 1024;
|
|
1132
1252
|
function toolString(value) {
|
|
1133
1253
|
return sliceToBytes(value, MAX_TOOL_FIELD_BYTES);
|
|
@@ -1135,6 +1255,9 @@ function toolString(value) {
|
|
|
1135
1255
|
function toolEnum(allowed, value) {
|
|
1136
1256
|
return allowed.find((candidate) => candidate === value);
|
|
1137
1257
|
}
|
|
1258
|
+
function toolRevision(value) {
|
|
1259
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
1260
|
+
}
|
|
1138
1261
|
var ANNOTATION_HINTS = ["readOnlyHint", "untrustedContentHint"];
|
|
1139
1262
|
function annotationHints(annotations) {
|
|
1140
1263
|
const hints = {};
|
|
@@ -1156,6 +1279,8 @@ function toolEntry(entry) {
|
|
|
1156
1279
|
return {
|
|
1157
1280
|
name: toolString(tool.name),
|
|
1158
1281
|
stableKey: toolString(tool.stableKey),
|
|
1282
|
+
...typeof tool.inventoryToolId === "string" ? { inventoryToolId: toolString(tool.inventoryToolId) } : {},
|
|
1283
|
+
...toolRevision(tool.contractRevision) !== undefined ? { contractRevision: toolRevision(tool.contractRevision) } : {},
|
|
1159
1284
|
...tool.version !== undefined ? { version: toolString(tool.version) } : {},
|
|
1160
1285
|
...hash !== undefined ? { schemaHash: hash } : {},
|
|
1161
1286
|
...source !== undefined ? { source } : {},
|
|
@@ -1253,6 +1378,8 @@ function buildToolCallEvent(params) {
|
|
|
1253
1378
|
...sinceInit !== undefined ? { timeSinceInitMs: sinceInit } : {},
|
|
1254
1379
|
tool: {
|
|
1255
1380
|
stableKey: toolString(params.tool.stableKey),
|
|
1381
|
+
...typeof params.tool.inventoryToolId === "string" ? { inventoryToolId: toolString(params.tool.inventoryToolId) } : {},
|
|
1382
|
+
...toolRevision(params.tool.contractRevision) !== undefined ? { contractRevision: toolRevision(params.tool.contractRevision) } : {},
|
|
1256
1383
|
...hash !== undefined ? { schemaHash: hash } : {},
|
|
1257
1384
|
...intent !== undefined ? { intent } : {}
|
|
1258
1385
|
},
|
|
@@ -1334,12 +1461,13 @@ function pruneByAllowlist(event, fields = TELEMETRY_FIELDS, toolFields = TELEMET
|
|
|
1334
1461
|
return pruned;
|
|
1335
1462
|
}
|
|
1336
1463
|
var defaultSinks2 = {
|
|
1337
|
-
sendTelemetry: (event) => sendTelemetry(event, globalThis,
|
|
1464
|
+
sendTelemetry: (event) => sendTelemetry(event, globalThis, telemetryEndpoint(), telemetryApiKey())
|
|
1338
1465
|
};
|
|
1339
|
-
function batchTelemetrySinks(apiKey) {
|
|
1466
|
+
function batchTelemetrySinks(apiKey, endpoint) {
|
|
1340
1467
|
const own = batchApiKey(apiKey);
|
|
1468
|
+
const ownEndpoint = telemetryEndpointFrom(endpoint);
|
|
1341
1469
|
return {
|
|
1342
|
-
sendTelemetry: (event) => sendTelemetry(event, globalThis,
|
|
1470
|
+
sendTelemetry: (event) => sendTelemetry(event, globalThis, ownEndpoint ?? telemetryEndpoint(), own ?? telemetryApiKey())
|
|
1343
1471
|
};
|
|
1344
1472
|
}
|
|
1345
1473
|
function batchApiKey(apiKey) {
|
|
@@ -1352,16 +1480,24 @@ function telemetryTenantScope(apiKey) {
|
|
|
1352
1480
|
const resolved = resolveTelemetryKey(apiKey);
|
|
1353
1481
|
return resolved === undefined ? "" : fnv1a(resolved);
|
|
1354
1482
|
}
|
|
1355
|
-
function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS, toolFields = TELEMETRY_TOOL_FIELDS) {
|
|
1483
|
+
function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS, toolFields = TELEMETRY_TOOL_FIELDS, sampleRate = SDK_TELEMETRY_SAMPLE_RATE) {
|
|
1356
1484
|
try {
|
|
1357
1485
|
if (!telemetryEnabled())
|
|
1358
1486
|
return;
|
|
1359
|
-
|
|
1487
|
+
const event = { ...build() };
|
|
1488
|
+
if (SAMPLED_EVENTS.has(String(event.event)) && sampleRate < 1) {
|
|
1489
|
+
if (!pageSampled(String(event.sessionId), sampleRate))
|
|
1490
|
+
return;
|
|
1491
|
+
event.sampleRate = sampleRate;
|
|
1492
|
+
}
|
|
1493
|
+
sinks.sendTelemetry(boundEventPayload(pruneByAllowlist(event, fields, toolFields)));
|
|
1360
1494
|
} catch {}
|
|
1361
1495
|
}
|
|
1362
1496
|
var initCancelled = false;
|
|
1363
1497
|
var initFlushed = false;
|
|
1498
|
+
var cancelInitFallback = () => {};
|
|
1364
1499
|
var capturedApiKey;
|
|
1500
|
+
var capturedEndpoint;
|
|
1365
1501
|
function captureTelemetryApiKey(apiKey) {
|
|
1366
1502
|
if (typeof apiKey === "string" && apiKey.trim().length > 0)
|
|
1367
1503
|
capturedApiKey = apiKey;
|
|
@@ -1369,17 +1505,44 @@ function captureTelemetryApiKey(apiKey) {
|
|
|
1369
1505
|
function telemetryApiKey() {
|
|
1370
1506
|
return capturedApiKey;
|
|
1371
1507
|
}
|
|
1508
|
+
function telemetryEndpointFrom(endpoint) {
|
|
1509
|
+
if (typeof endpoint !== "string")
|
|
1510
|
+
return;
|
|
1511
|
+
return safe(() => {
|
|
1512
|
+
const url = new URL(endpoint);
|
|
1513
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.pathname.endsWith("/v1/collect") || url.search !== "" || url.hash !== "") {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
url.pathname = `${url.pathname.slice(0, -"/v1/collect".length)}/v1/telemetry`;
|
|
1517
|
+
return url.toString();
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
function captureTelemetryEndpoint(endpoint) {
|
|
1521
|
+
const derived = telemetryEndpointFrom(endpoint);
|
|
1522
|
+
if (derived !== undefined)
|
|
1523
|
+
capturedEndpoint = derived;
|
|
1524
|
+
}
|
|
1525
|
+
function telemetryEndpoint() {
|
|
1526
|
+
return capturedEndpoint;
|
|
1527
|
+
}
|
|
1372
1528
|
function cancelInitEvent() {
|
|
1373
1529
|
initCancelled = true;
|
|
1530
|
+
cancelInitFallback();
|
|
1374
1531
|
}
|
|
1375
1532
|
function flushInitEvent(sinks) {
|
|
1376
1533
|
if (initFlushed)
|
|
1377
1534
|
return;
|
|
1378
1535
|
initFlushed = true;
|
|
1536
|
+
cancelInitFallback();
|
|
1379
1537
|
if (initCancelled)
|
|
1380
1538
|
return;
|
|
1381
1539
|
emitTelemetry(() => buildInitEvent(), sinks);
|
|
1382
1540
|
}
|
|
1541
|
+
function deferInitEventForBatch(sinks) {
|
|
1542
|
+
if (initFlushed || initCancelled)
|
|
1543
|
+
return;
|
|
1544
|
+
afterDelay(() => flushInitEvent(sinks), 0);
|
|
1545
|
+
}
|
|
1383
1546
|
function afterDelay(run, ms) {
|
|
1384
1547
|
const schedule = safe(() => globalThis.setTimeout);
|
|
1385
1548
|
if (typeof schedule !== "function")
|
|
@@ -1392,7 +1555,8 @@ function afterDelay(run, ms) {
|
|
|
1392
1555
|
safe(() => clear.call(globalThis, handle));
|
|
1393
1556
|
};
|
|
1394
1557
|
}
|
|
1395
|
-
|
|
1558
|
+
var INIT_FALLBACK_MS = 1000;
|
|
1559
|
+
cancelInitFallback = afterDelay(() => flushInitEvent(), INIT_FALLBACK_MS);
|
|
1396
1560
|
|
|
1397
1561
|
// src/register.ts
|
|
1398
1562
|
function isContentResult(value) {
|
|
@@ -1414,15 +1578,18 @@ function clock() {
|
|
|
1414
1578
|
const perf = safe(() => globalThis.performance);
|
|
1415
1579
|
const now = safe(() => perf?.now);
|
|
1416
1580
|
if (typeof now === "function") {
|
|
1417
|
-
const
|
|
1418
|
-
const
|
|
1419
|
-
if (
|
|
1420
|
-
return () => elapsedMs(
|
|
1581
|
+
const read = () => safe(() => now.call(perf));
|
|
1582
|
+
const start = read();
|
|
1583
|
+
if (start !== undefined)
|
|
1584
|
+
return () => elapsedMs(read(), start);
|
|
1421
1585
|
}
|
|
1422
1586
|
const read = () => safe(() => Date.now());
|
|
1423
1587
|
const start = read();
|
|
1424
1588
|
return () => elapsedMs(read(), start);
|
|
1425
1589
|
}
|
|
1590
|
+
function createCallTracker(tool, tracking) {
|
|
1591
|
+
return trackerFor({ ...tool, stableKey: tool.name }, tracking);
|
|
1592
|
+
}
|
|
1426
1593
|
function trackerFor(tool, tracking) {
|
|
1427
1594
|
if (!tracking)
|
|
1428
1595
|
return;
|
|
@@ -1437,9 +1604,24 @@ function trackerFor(tool, tracking) {
|
|
|
1437
1604
|
};
|
|
1438
1605
|
return (eventName, data) => track(tracking, eventName, { ...shared, ...data });
|
|
1439
1606
|
}
|
|
1607
|
+
var TRACKED_TOOL_FLAG = Symbol.for("webmcp.sdk.tracked");
|
|
1608
|
+
function markTracked(spec, tracking) {
|
|
1609
|
+
const { toBackend } = trackingOutputs(tracking);
|
|
1610
|
+
if (!toBackend)
|
|
1611
|
+
return spec;
|
|
1612
|
+
try {
|
|
1613
|
+
Object.defineProperty(spec, TRACKED_TOOL_FLAG, {
|
|
1614
|
+
value: true,
|
|
1615
|
+
writable: false,
|
|
1616
|
+
configurable: true,
|
|
1617
|
+
enumerable: false
|
|
1618
|
+
});
|
|
1619
|
+
} catch {}
|
|
1620
|
+
return spec;
|
|
1621
|
+
}
|
|
1440
1622
|
function toSpecTool(tool, channels) {
|
|
1441
|
-
const { tracking, telemetry, telemetrySinks, telemetryKey } = channels;
|
|
1442
|
-
|
|
1623
|
+
const { tracking, telemetry, telemetrySinks, telemetryKey, telemetryEndpoint } = channels;
|
|
1624
|
+
const spec = {
|
|
1443
1625
|
name: tool.name,
|
|
1444
1626
|
...tool.title !== undefined ? { title: tool.title } : {},
|
|
1445
1627
|
description: tool.description,
|
|
@@ -1451,7 +1633,7 @@ function toSpecTool(tool, channels) {
|
|
|
1451
1633
|
return normalizeResult(await tool.execute(input));
|
|
1452
1634
|
const callKey = telemetry ? resolveTelemetryKey(telemetryKey) : undefined;
|
|
1453
1635
|
const sequence = telemetry ? nextCall(tool.stableKey, telemetryTenantScope(callKey)) : undefined;
|
|
1454
|
-
const callSinks = sequence ? batchTelemetrySinks(callKey) : telemetrySinks;
|
|
1636
|
+
const callSinks = sequence ? batchTelemetrySinks(callKey, telemetryEndpoint) : telemetrySinks;
|
|
1455
1637
|
const elapsed = clock();
|
|
1456
1638
|
trackCall?.("tool_call_request", { input });
|
|
1457
1639
|
try {
|
|
@@ -1475,6 +1657,7 @@ function toSpecTool(tool, channels) {
|
|
|
1475
1657
|
}
|
|
1476
1658
|
}
|
|
1477
1659
|
};
|
|
1660
|
+
return markTracked(spec, tracking);
|
|
1478
1661
|
}
|
|
1479
1662
|
var REGISTRATION_TIMEOUT_MS = 2000;
|
|
1480
1663
|
var PAGEHIDE = "pagehide";
|
|
@@ -1490,7 +1673,7 @@ function onPagehide(run) {
|
|
|
1490
1673
|
safe(() => remove.call(g, PAGEHIDE, listener));
|
|
1491
1674
|
};
|
|
1492
1675
|
}
|
|
1493
|
-
function watchRegistration(tools, tracking, sinks) {
|
|
1676
|
+
function watchRegistration(tools, tracking, sinks, navigation = false) {
|
|
1494
1677
|
const entries = tools.map((tool) => ({ tool, outcome: "pending" }));
|
|
1495
1678
|
const elapsed = clock();
|
|
1496
1679
|
const { registrationIndex, trigger } = nextRegistration();
|
|
@@ -1505,7 +1688,7 @@ function watchRegistration(tools, tracking, sinks) {
|
|
|
1505
1688
|
const settleMs = elapsed();
|
|
1506
1689
|
emitTelemetry(() => buildToolRegistrationEvent({
|
|
1507
1690
|
registrationIndex,
|
|
1508
|
-
trigger,
|
|
1691
|
+
trigger: navigation ? "spa_navigation" : trigger,
|
|
1509
1692
|
settleMs,
|
|
1510
1693
|
tools: entries,
|
|
1511
1694
|
tracking
|
|
@@ -1543,15 +1726,24 @@ function registerTools(tools, options = {}) {
|
|
|
1543
1726
|
const tracking = safe(() => options.tracking);
|
|
1544
1727
|
const telemetryLive = telemetryOption !== undefined && telemetryEnabled(telemetryOption.value);
|
|
1545
1728
|
const apiKey = telemetryLive ? safe(() => tracking?.apiKey) : undefined;
|
|
1729
|
+
const endpoint = telemetryLive ? safe(() => tracking?.endpoint) : undefined;
|
|
1730
|
+
const telemetrySinks = telemetryLive ? batchTelemetrySinks(apiKey, endpoint) : undefined;
|
|
1546
1731
|
const channels = {
|
|
1547
1732
|
tracking,
|
|
1548
1733
|
telemetry: telemetryLive,
|
|
1549
|
-
...telemetryLive ? {
|
|
1734
|
+
...telemetryLive ? {
|
|
1735
|
+
telemetrySinks,
|
|
1736
|
+
telemetryKey: apiKey,
|
|
1737
|
+
telemetryEndpoint: endpoint
|
|
1738
|
+
} : {}
|
|
1550
1739
|
};
|
|
1551
1740
|
if (telemetryOption?.value === false)
|
|
1552
1741
|
cancelInitEvent();
|
|
1553
|
-
if (
|
|
1742
|
+
if (telemetrySinks) {
|
|
1554
1743
|
captureTelemetryApiKey(apiKey);
|
|
1744
|
+
captureTelemetryEndpoint(endpoint);
|
|
1745
|
+
deferInitEventForBatch(telemetrySinks);
|
|
1746
|
+
}
|
|
1555
1747
|
assertUniqueIdentities(tools);
|
|
1556
1748
|
const controller = new AbortController;
|
|
1557
1749
|
const external = options.signal;
|
|
@@ -1568,32 +1760,89 @@ function registerTools(tools, options = {}) {
|
|
|
1568
1760
|
state,
|
|
1569
1761
|
...error !== undefined ? { error } : {}
|
|
1570
1762
|
});
|
|
1571
|
-
const
|
|
1763
|
+
const active = new Map;
|
|
1764
|
+
const current = () => tools.filter((tool) => active.get(tool)?.registered).map((tool) => tool.name);
|
|
1765
|
+
const changed = () => {
|
|
1766
|
+
safe(() => options.onChange?.(current()));
|
|
1767
|
+
};
|
|
1768
|
+
const eligible = (tool) => {
|
|
1769
|
+
const page = currentPageKey();
|
|
1770
|
+
return page === undefined || matchPage(tool.pages, page);
|
|
1771
|
+
};
|
|
1772
|
+
const remove = (tool) => {
|
|
1773
|
+
const entry = active.get(tool);
|
|
1774
|
+
if (!entry?.removable)
|
|
1775
|
+
return;
|
|
1776
|
+
entry.lifetime.abort();
|
|
1777
|
+
safe(() => modelContext?.unregisterTool?.(tool.name));
|
|
1778
|
+
active.delete(tool);
|
|
1779
|
+
};
|
|
1572
1780
|
const settle = async (tool) => {
|
|
1573
1781
|
if (!modelContext)
|
|
1574
1782
|
return result(tool, "unsupported");
|
|
1575
1783
|
if (controller.signal.aborted)
|
|
1576
1784
|
return result(tool, "aborted");
|
|
1785
|
+
const entry = {
|
|
1786
|
+
lifetime: tool.pages?.length ? new AbortController : controller,
|
|
1787
|
+
removable: typeof modelContext.unregisterTool === "function",
|
|
1788
|
+
registered: false
|
|
1789
|
+
};
|
|
1790
|
+
active.set(tool, entry);
|
|
1577
1791
|
try {
|
|
1578
1792
|
await Promise.resolve(modelContext.registerTool(toSpecTool(tool, channels), {
|
|
1579
|
-
signal
|
|
1793
|
+
get signal() {
|
|
1794
|
+
entry.removable = true;
|
|
1795
|
+
return entry.lifetime.signal;
|
|
1796
|
+
}
|
|
1580
1797
|
}));
|
|
1581
|
-
|
|
1798
|
+
if (entry.lifetime.signal.aborted)
|
|
1799
|
+
return result(tool, "aborted");
|
|
1800
|
+
entry.registered = true;
|
|
1801
|
+
if (!eligible(tool))
|
|
1802
|
+
remove(tool);
|
|
1803
|
+
changed();
|
|
1804
|
+
return result(tool, entry.lifetime.signal.aborted ? "aborted" : "registered");
|
|
1582
1805
|
} catch (error) {
|
|
1583
|
-
if (
|
|
1806
|
+
if (active.get(tool) === entry)
|
|
1807
|
+
active.delete(tool);
|
|
1808
|
+
if (entry.lifetime.signal.aborted)
|
|
1584
1809
|
return result(tool, "aborted");
|
|
1585
1810
|
return result(tool, "failed", error);
|
|
1586
1811
|
}
|
|
1587
1812
|
};
|
|
1588
|
-
const
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1813
|
+
const register = (batch, navigation = false) => {
|
|
1814
|
+
const watch = channels.telemetry ? watchRegistration(batch, channels.tracking, channels.telemetrySinks, navigation) : undefined;
|
|
1815
|
+
const ready = Promise.all(batch.map(async (tool, index) => {
|
|
1816
|
+
const settled = await settle(tool);
|
|
1817
|
+
watch?.record(index, settled);
|
|
1818
|
+
return settled;
|
|
1819
|
+
}));
|
|
1820
|
+
if (watch)
|
|
1821
|
+
ready.then(() => watch.emit());
|
|
1822
|
+
return ready;
|
|
1823
|
+
};
|
|
1824
|
+
const reconcile = () => {
|
|
1825
|
+
if (controller.signal.aborted)
|
|
1826
|
+
return;
|
|
1827
|
+
for (const tool of active.keys())
|
|
1828
|
+
if (!eligible(tool))
|
|
1829
|
+
remove(tool);
|
|
1830
|
+
changed();
|
|
1831
|
+
const added = tools.filter((tool) => eligible(tool) && !active.has(tool));
|
|
1832
|
+
if (added.length)
|
|
1833
|
+
register(added, true);
|
|
1834
|
+
};
|
|
1835
|
+
const stop = !controller.signal.aborted && tools.some((tool) => tool.pages?.length) ? onPageChange(reconcile) : () => {};
|
|
1836
|
+
controller.signal.addEventListener("abort", () => {
|
|
1837
|
+
stop();
|
|
1838
|
+
for (const tool of active.keys())
|
|
1839
|
+
remove(tool);
|
|
1840
|
+
changed();
|
|
1841
|
+
}, { once: true });
|
|
1842
|
+
const ready = register(tools.filter(eligible));
|
|
1595
1843
|
return {
|
|
1596
1844
|
ready,
|
|
1845
|
+
current,
|
|
1597
1846
|
signal: controller.signal,
|
|
1598
1847
|
unregister() {
|
|
1599
1848
|
controller.abort();
|
|
@@ -1601,7 +1850,10 @@ function registerTools(tools, options = {}) {
|
|
|
1601
1850
|
};
|
|
1602
1851
|
}
|
|
1603
1852
|
export {
|
|
1604
|
-
|
|
1853
|
+
createCallTracker,
|
|
1854
|
+
currentPageKey,
|
|
1855
|
+
defineTool,
|
|
1856
|
+
matchPage,
|
|
1605
1857
|
registerTools,
|
|
1606
|
-
|
|
1858
|
+
resolveModelContext
|
|
1607
1859
|
};
|
package/dist/pages.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Match any page pattern; undefined/empty lists (and empty patterns) mean everywhere. */
|
|
2
|
+
export declare function matchPage(patterns: readonly string[] | undefined, location: string): boolean;
|
|
3
|
+
/** No readable location (SSR) means cannot evaluate: register all tools, not a root-page match. */
|
|
4
|
+
export declare function currentPageKey(): string | undefined;
|
|
5
|
+
/** One shared browser listener/History patch, including across duplicated SDK bundles. */
|
|
6
|
+
export declare function onPageChange(listener: () => void): () => void;
|