@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/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(g = globalThis) {
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.com";
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.catch === "function") {
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": "application/json" };
108
- if (typeof apiKey === "string" && apiKey.trim().length > 0)
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: getOrCreateSessionId(namespace),
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.4.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, undefined, telemetryApiKey())
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, undefined, own ?? telemetryApiKey())
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
- sinks.sendTelemetry(boundEventPayload(pruneByAllowlist({ ...build() }, fields, toolFields)));
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
- afterDelay(() => flushInitEvent(), 0);
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 read2 = () => safe(() => now.call(perf));
1418
- const start2 = read2();
1419
- if (start2 !== undefined)
1420
- return () => elapsedMs(read2(), start2);
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
- return {
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 ? { telemetrySinks: batchTelemetrySinks(apiKey), telemetryKey: apiKey } : {}
1734
+ ...telemetryLive ? {
1735
+ telemetrySinks,
1736
+ telemetryKey: apiKey,
1737
+ telemetryEndpoint: endpoint
1738
+ } : {}
1550
1739
  };
1551
1740
  if (telemetryOption?.value === false)
1552
1741
  cancelInitEvent();
1553
- if (telemetryLive)
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 watch = channels.telemetry ? watchRegistration(tools, channels.tracking, channels.telemetrySinks) : undefined;
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: controller.signal
1793
+ get signal() {
1794
+ entry.removable = true;
1795
+ return entry.lifetime.signal;
1796
+ }
1580
1797
  }));
1581
- return result(tool, "registered");
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 (controller.signal.aborted)
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 ready = Promise.all(tools.map(async (tool, index) => {
1589
- const settled = await settle(tool);
1590
- watch?.record(index, settled);
1591
- return settled;
1592
- }));
1593
- if (watch)
1594
- ready.then(() => watch.emit());
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
- resolveModelContext,
1853
+ createCallTracker,
1854
+ currentPageKey,
1855
+ defineTool,
1856
+ matchPage,
1605
1857
  registerTools,
1606
- defineTool
1858
+ resolveModelContext
1607
1859
  };
@@ -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;