@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.
@@ -16,6 +16,8 @@ export interface ToolRegistrationResult {
16
16
  error?: unknown;
17
17
  }
18
18
  export interface RegisterToolsOptions {
19
+ /** Called after the live name set changes, including initial registration and navigation. */
20
+ onChange?: (names: string[]) => void;
19
21
  /**
20
22
  * External lifetime for the registration (e.g. a component's unmount signal).
21
23
  * Aborting it unregisters every tool in this batch, same as `unregister()`.
@@ -44,10 +46,11 @@ export interface RegisterToolsOptions {
44
46
  *
45
47
  * The other two events are per-batch, so the option is read per call: a *later*
46
48
  * `registerTools` that omits it reports its own batch and calls. Cancelling
47
- * `sdk_init` also requires winning the race with its deferred flush a batch
48
- * registered at module scope does, one registered from a mount effect or a consent
49
- * callback may not. A site that wants the whole page silent regardless of where its
50
- * `registerTools` calls come from (generated code, a CDN snippet) should use a
49
+ * `sdk_init` also requires winning the race with its deferred flush. A batch
50
+ * registered at module scope or from an ordinary mount effect schedules the keyed
51
+ * batch flush; a later consent decision can still arrive after the bounded
52
+ * no-registration fallback. A site that wants the whole page silent regardless of
53
+ * when its `registerTools` calls run (generated code, a CDN snippet) should use a
51
54
  * page-level lever instead: `globalThis.__WEBMCP_TELEMETRY__ = false` — strictly
52
55
  * `false`, so a truthy `"0"` does not opt out — or Global Privacy Control
53
56
  * (`navigator.globalPrivacyControl === true`), both of which are read at emit time
@@ -63,18 +66,55 @@ export interface ToolRegistration {
63
66
  * cannot mask the rest.
64
67
  */
65
68
  ready: Promise<ToolRegistrationResult[]>;
69
+ /** Currently registered names, in declaration order; `ready` covers only the initial page. */
70
+ current(): string[];
66
71
  /** Unregister every tool in this batch. Idempotent. */
67
72
  unregister(): void;
68
73
  /** The signal carrying this registration's lifetime (aborted once unregistered). */
69
74
  signal: AbortSignal;
70
75
  }
76
+ /**
77
+ * Emits one invocation's authenticated-channel events. The `callId` correlating a
78
+ * `tool_call_request` with its `tool_call_response` is closed over, so one tracker
79
+ * is one call — never one tool, never one page.
80
+ */
81
+ export type CallTracker = (eventName: string, data: Record<string, unknown>) => void;
82
+ /**
83
+ * A tool a host observes but did not register through {@link registerTools} — the
84
+ * only identity it can offer is the wire `name` the agent surface already knows.
85
+ */
86
+ export interface TrackedCall {
87
+ name: string;
88
+ /** Optional per-tool version, surfaced on the events as `toolVersion`. */
89
+ version?: string;
90
+ }
91
+ /**
92
+ * Emitter for a call on a tool this SDK did not register, or `undefined` when the
93
+ * authenticated channel has no live output — the same `trackingOutputs` answer
94
+ * {@link registerTools} gates its own per-call emitter on, so the two cannot
95
+ * disagree about whether the channel is live.
96
+ *
97
+ * This exists for one caller shape: a host that owns the page's WebMCP surface and
98
+ * wraps tools registered on it directly (the CDN snippet's coexistence path). Those
99
+ * calls must land on `/v1/collect` as the same bytes an SDK-registered tool's do —
100
+ * same event names, same correlated pair, same anonymous identity — so the
101
+ * projection reads one shape rather than two. Everything else the host must do
102
+ * itself: call it once per invocation (a reused tracker collapses two calls into
103
+ * one `callId`), emit `tool_call_request` before the handler and
104
+ * `tool_call_response` after, and measure the `duration_ms` it reports.
105
+ *
106
+ * `stableKey` is the tool's `name`: a host-wrapped tool has no developer-authored
107
+ * durable identity to carry, and inventing one would key the same tool differently
108
+ * from every other reader of the same page.
109
+ */
110
+ export declare function createCallTracker(tool: TrackedCall, tracking: TrackingOptions): CallTracker | undefined;
71
111
  /**
72
112
  * Register a batch of `defineTool` tools on the page's WebMCP surface.
73
113
  *
74
- * Lifecycle: registration starts immediately; the tools stay live until
75
- * `unregister()` is called or the external `options.signal` aborts (both funnel
76
- * into one internal `AbortController`, the spec's only unregistration mechanism).
77
- * On browsers without a WebMCP surface this is a graceful no-op — generated code
78
- * can run unconditionally on every page.
114
+ * Registration starts immediately, filtered by `pages`; `ready` covers that initial set.
115
+ * Route changes reconcile scoped tools using per-tool signals (or legacy unregisterTool).
116
+ * A legacy surface that never reads the signal and has no unregister method retains a
117
+ * tool once registered. `unregister()`/external abort also removes the route subscription.
118
+ * Without a readable location (SSR), all tools are eligible; without a surface, no-op.
79
119
  */
80
120
  export declare function registerTools(tools: readonly AnyWebMCPTool[], options?: RegisterToolsOptions): ToolRegistration;
package/dist/spec.d.ts CHANGED
@@ -11,9 +11,9 @@
11
11
  * - `registerTool(tool, { signal })` returns a promise that settles when the
12
12
  * registration completes; it rejects on a duplicate name, an invalid tool, an
13
13
  * inactive document, or an abort.
14
- * - Unregistration happens ONLY by aborting the `AbortSignal` passed at
15
- * registration. `unregisterTool()` and `provideContext()` are dead APIs and are
16
- * never referenced.
14
+ * - Current unregistration is the `AbortSignal` passed at registration (also
15
+ * verified against the 2026-09-10 draft). Legacy surfaces may instead expose
16
+ * `unregisterTool(name)`; a surface supporting neither retains registered tools.
17
17
  * - Tool names: 1–128 chars of [A-Za-z0-9_\-.].
18
18
  * - `annotations.readOnlyHint` / `annotations.untrustedContentHint`.
19
19
  *
@@ -48,9 +48,20 @@ export interface RegisterToolOptions {
48
48
  */
49
49
  export interface ModelContextLike {
50
50
  registerTool(tool: SpecTool, options?: RegisterToolOptions): Promise<void>;
51
+ /** Pre-draft compatibility only; current native surfaces use the registration signal. */
52
+ unregisterTool?(name: string): void;
53
+ }
54
+ interface GlobalWithModelContext {
55
+ document?: {
56
+ modelContext?: ModelContextLike;
57
+ };
58
+ navigator?: {
59
+ modelContext?: ModelContextLike;
60
+ };
51
61
  }
52
62
  /**
53
63
  * Resolve the page's WebMCP surface; `undefined` on non-supporting browsers.
54
64
  * Injectable global scope for tests.
55
65
  */
56
- export declare function resolveModelContext(g?: object): ModelContextLike | undefined;
66
+ export declare function resolveModelContext(scope?: GlobalWithModelContext): ModelContextLike | undefined;
67
+ export {};
@@ -81,7 +81,17 @@ export interface ClientContext {
81
81
  * even when nobody ever calls `registerTools`: a site that loads the SDK and never
82
82
  * registers is a broken integration, and this event is the only way to see it.
83
83
  */
84
- export interface SdkInitEvent extends TelemetryEnvelope {
84
+ /**
85
+ * Present only when a build sampled the page-level events: the rate this page was kept
86
+ * at, strictly between 0 and 1. Absent means every such event on the page was sent. A
87
+ * consumer counting page loads weights each sampled event by `1 / sampleRate`. Not on
88
+ * the envelope, because `tool_call` never carries it — sampling is per page, and a
89
+ * tool invocation is not a page-level fact.
90
+ */
91
+ export interface SampledEvent {
92
+ sampleRate?: number;
93
+ }
94
+ export interface SdkInitEvent extends TelemetryEnvelope, SampledEvent {
85
95
  event: "sdk_init";
86
96
  sdk: SdkInfo;
87
97
  surface: SurfaceInfo;
@@ -108,6 +118,8 @@ export interface TrackingConfigInfo {
108
118
  trackingEnabled: boolean;
109
119
  otelEnabled: boolean;
110
120
  customEndpoint: boolean;
121
+ /** `TrackingOptions.builtWith`, verbatim, when set and within its length cap. */
122
+ builtWith?: string;
111
123
  }
112
124
  /**
113
125
  * One tool in a `tool_registration` batch. The shape metrics are inherited as
@@ -117,6 +129,14 @@ export interface TrackingConfigInfo {
117
129
  export interface RegisteredToolEntry extends Partial<ToolShapeMetrics> {
118
130
  name: string;
119
131
  stableKey: string;
132
+ /**
133
+ * The connected platform's own id for this tool, when the host supplied one. Opaque
134
+ * and optional: a tool declared in a codebase that has never been connected has none,
135
+ * and its absence is the honest reading rather than a degraded one.
136
+ */
137
+ inventoryToolId?: string;
138
+ /** The contract revision the host believed this tool matched, when it knew one. */
139
+ contractRevision?: number;
120
140
  version?: string;
121
141
  schemaHash?: string;
122
142
  source?: ToolSource;
@@ -154,7 +174,7 @@ export interface TruncatedTools {
154
174
  * outcomes v1 computed and threw away. A site where 3 of 8 tools fail on a
155
175
  * duplicate name is the failure mode this event exists to make visible.
156
176
  */
157
- export interface ToolRegistrationEvent extends TelemetryEnvelope {
177
+ export interface ToolRegistrationEvent extends TelemetryEnvelope, SampledEvent {
158
178
  event: "tool_registration";
159
179
  /** 1-based position of this batch within the page load. */
160
180
  registrationIndex: number;
@@ -176,6 +196,10 @@ export type ToolCallOutcome = "success" | "error";
176
196
  /** `tool.*` on `tool_call` — identity and the two fields worth joining calls on. */
177
197
  export interface ToolCallToolInfo {
178
198
  stableKey: string;
199
+ /** The connected platform's own id, when the host supplied one. See the registration
200
+ * entry's field of the same name — same value, same optionality, same opacity. */
201
+ inventoryToolId?: string;
202
+ contractRevision?: number;
179
203
  schemaHash?: string;
180
204
  intent?: ToolIntent;
181
205
  }
@@ -40,6 +40,7 @@ export declare const TELEMETRY_FIELDS: {
40
40
  readonly event: true;
41
41
  readonly ts: true;
42
42
  readonly sessionId: true;
43
+ readonly sampleRate: true;
43
44
  readonly "sdk.name": true;
44
45
  readonly "sdk.version": true;
45
46
  readonly "sdk.installMode": true;
@@ -64,6 +65,7 @@ export declare const TELEMETRY_FIELDS: {
64
65
  readonly "config.trackingEnabled": true;
65
66
  readonly "config.otelEnabled": true;
66
67
  readonly "config.customEndpoint": true;
68
+ readonly "config.builtWith": true;
67
69
  readonly tools: true;
68
70
  readonly callId: true;
69
71
  readonly callIndex: true;
@@ -90,6 +92,8 @@ export declare const TELEMETRY_FIELDS: {
90
92
  export declare const TELEMETRY_TOOL_FIELDS: {
91
93
  readonly name: true;
92
94
  readonly stableKey: true;
95
+ readonly inventoryToolId: true;
96
+ readonly contractRevision: true;
93
97
  readonly version: true;
94
98
  readonly schemaHash: true;
95
99
  readonly source: true;
@@ -43,6 +43,22 @@ export declare const SDK_VERSION: string;
43
43
  * the overwhelming majority rather than emit a value no consumer can read.
44
44
  */
45
45
  export declare const SDK_INSTALL_MODE: InstallMode;
46
+ /**
47
+ * Parse a sampling rate. Anything that is not a number strictly between 0 and 1 means
48
+ * "send everything": an unset define is how every npm build reaches a page, and a
49
+ * mistyped one must degrade to complete data rather than to silence.
50
+ */
51
+ export declare function parseSampleRate(raw: unknown): number;
52
+ /** The rate this build samples page-level events at; `1` sends everything. */
53
+ export declare const SDK_TELEMETRY_SAMPLE_RATE: number;
54
+ /**
55
+ * Whether this page load is in the sample. Decided from the session id, which every
56
+ * event on the page shares, so `sdk_init` and the `tool_registration`s beside it are
57
+ * kept or dropped together — a sampled page is a complete page, never half of one. The
58
+ * hash is the same FNV-1a the channel already uses for `schemaHash`; its top 32 bits
59
+ * over 2^32 is a uniform enough coin for a rate that only has to hold on average.
60
+ */
61
+ export declare function pageSampled(sessionId: string, rate?: number): boolean;
46
62
  /**
47
63
  * Read a value that may not exist, collapsing both an absent property and a
48
64
  * throwing getter to `undefined`. Every browser global this module touches is
@@ -173,6 +189,9 @@ export declare function buildInitEvent(params?: InitEventParams): SdkInitEvent;
173
189
  export interface RegisteredTelemetryTool {
174
190
  name: string;
175
191
  stableKey: string;
192
+ /** The connected platform's own id for this tool; opaque, copied, never interpreted. */
193
+ inventoryToolId?: string;
194
+ contractRevision?: number;
176
195
  version?: string;
177
196
  description?: string;
178
197
  inputSchema?: unknown;
@@ -187,6 +206,12 @@ export interface RegisteredToolParams {
187
206
  /** What the surface rejected with; templated into `failureSignature` when `failed`. */
188
207
  error?: unknown;
189
208
  }
209
+ /**
210
+ * Cap on `config.builtWith`, in characters. Sized for `<tool>@<semver-with-prerelease>`
211
+ * with room to spare; it is the one caller-supplied string on `tool_registration`
212
+ * that is not copied off a tool, so the per-entry byte cap below does not cover it.
213
+ */
214
+ export declare const MAX_BUILT_WITH_LENGTH = 64;
190
215
  /** Where a batch sits in the page load, and what caused it. */
191
216
  export interface RegistrationSequence {
192
217
  /** 1-based position among the batches this page load reports. */
@@ -278,6 +303,9 @@ export declare function nextCall(stableKey: string, tenantScope?: string): ToolC
278
303
  /** What `tool_call` reads off a tool — the two fields worth joining calls on. */
279
304
  export interface CalledTelemetryTool {
280
305
  stableKey: string;
306
+ /** The connected platform's own id for this tool; opaque, copied, never interpreted. */
307
+ inventoryToolId?: string;
308
+ contractRevision?: number;
281
309
  /** Reported as a hash only; the schema itself never leaves the page. */
282
310
  inputSchema?: unknown;
283
311
  intent?: ToolIntent;
@@ -360,7 +388,7 @@ export interface TelemetrySinks {
360
388
  * the raw batch option: a key resolved to a value pins it, closing the window in which
361
389
  * the module key changes between the claim and the emit.
362
390
  */
363
- export declare function batchTelemetrySinks(apiKey: unknown): TelemetrySinks;
391
+ export declare function batchTelemetrySinks(apiKey: unknown, endpoint?: unknown): TelemetrySinks;
364
392
  /**
365
393
  * The key a batch's beacons authenticate with, resolved now instead of at emit time:
366
394
  * its own when it supplied one, otherwise the page's.
@@ -421,7 +449,7 @@ export declare function telemetryTenantScope(apiKey: unknown): string;
421
449
  * global the builders read is guarded individually too; this is the backstop that
422
450
  * keeps the guarantee true of assembly as a whole, not of each read in turn.
423
451
  */
424
- export declare function emitTelemetry(build: () => TelemetryEvent, sinks?: TelemetrySinks, fields?: TelemetryFieldMap, toolFields?: TelemetryToolFieldMap): void;
452
+ export declare function emitTelemetry(build: () => TelemetryEvent, sinks?: TelemetrySinks, fields?: TelemetryFieldMap, toolFields?: TelemetryToolFieldMap, sampleRate?: number): void;
425
453
  /**
426
454
  * Record the `apiKey` of a `registerTools` call so the deferred `sdk_init` flush —
427
455
  * which runs with no batch in hand, and may run before any batch exists — can
@@ -438,6 +466,15 @@ export declare function emitTelemetry(build: () => TelemetryEvent, sinks?: Telem
438
466
  export declare function captureTelemetryApiKey(apiKey: unknown): void;
439
467
  /** The key {@link captureTelemetryApiKey} last recorded, if any. */
440
468
  export declare function telemetryApiKey(): string | undefined;
469
+ /**
470
+ * Record the telemetry endpoint for the deferred `sdk_init` flush. Like the captured
471
+ * key, a later usable value overwrites an earlier one while an absent or malformed
472
+ * value leaves the last usable value in place. URL parsing is best-effort because an
473
+ * observability option must never break the visitor's page.
474
+ */
475
+ export declare function captureTelemetryEndpoint(endpoint: unknown): void;
476
+ /** The endpoint {@link captureTelemetryEndpoint} last recorded, if any. */
477
+ export declare function telemetryEndpoint(): string | undefined;
441
478
  /**
442
479
  * Cancel the deferred `sdk_init` below. Called by `registerTools({ telemetry: false })`,
443
480
  * whose opt-out is page-level and not merely batch-level: the flush describes the
@@ -462,6 +499,12 @@ export declare function cancelInitEvent(): void;
462
499
  * Exported for tests; the scheduled timer below is the one that runs it on a real page.
463
500
  */
464
501
  export declare function flushInitEvent(sinks?: TelemetrySinks): void;
502
+ /**
503
+ * Give a telemetry-live registration batch the next task's `sdk_init` flush. This
504
+ * keeps the batch's key and endpoint together while leaving the rest of the current
505
+ * task available for a page-level opt-out or another synchronous registration.
506
+ */
507
+ export declare function deferInitEventForBatch(sinks: TelemetrySinks): void;
465
508
  /**
466
509
  * Run `run` after `ms`, returning a canceller. Guarded end to end: a runtime with no
467
510
  * timers (SSR) and a hostile `setTimeout` (an extension, a fake-timer harness) both
@@ -469,3 +512,5 @@ export declare function flushInitEvent(sinks?: TelemetrySinks): void;
469
512
  * `registerTools`. The canceller is safe to call when nothing was scheduled.
470
513
  */
471
514
  export declare function afterDelay(run: () => void, ms: number): () => void;
515
+ /** How long a page that never registers gets to expose a real batch configuration. */
516
+ export declare const INIT_FALLBACK_MS = 1000;
@@ -81,8 +81,39 @@ export declare function getOrCreateSessionId(namespace: string): string;
81
81
  export interface TrackingOptions {
82
82
  /** Presence (non-empty) enables the backend transport and namespaces storage. */
83
83
  apiKey?: string;
84
+ /**
85
+ * What generated this integration, as `<tool>[/<path>]@<version>` —
86
+ * `webmcp-kit/implement@<plugin version>` for the plugin's codegen,
87
+ * `webmcp-kit/connect-existing-tools@<plugin version>` for its migration of
88
+ * hand-written tools. Reported once per batch as `config.builtWith` on the
89
+ * default-on channel so kit-built sites are countable without a Connect; a
90
+ * hand-written integration leaves it unset. Free text under {@link MAX_BUILT_WITH_LENGTH}
91
+ * characters; anything else is dropped rather than truncated, because a partial
92
+ * `<tool>@<vers` is a wrong answer, not a shorter one.
93
+ */
94
+ builtWith?: string;
84
95
  /** Override the collect endpoint; ignored without `apiKey`. */
85
96
  endpoint?: string;
97
+ /**
98
+ * Report under a session identity the host already owns, verbatim, instead of
99
+ * the one this module mints in `sessionStorage`. For a host that is itself the
100
+ * page's session authority — the CDN snippet, whose tab session predates any
101
+ * `registerTools` call, or a Journey runner replaying under a synthetic `syn_…`
102
+ * id — the storage-minted id would split one visit into two sessions the
103
+ * pipeline cannot rejoin, because it lives in an `apiKey`-derived namespace the
104
+ * host does not share.
105
+ *
106
+ * Supplying it takes this module out of the session business entirely for that
107
+ * batch: nothing is read from or written to `sessionStorage`, including
108
+ * `last_seen`, so the 30-minute inactivity boundary is the host's to enforce.
109
+ * The `visitorId` is unaffected — a different lifetime, still this module's.
110
+ *
111
+ * Validated like a stored id ({@link MAX_ID_LENGTH}), because identity fields are
112
+ * copied onto every event without passing through the truncation ladder: an
113
+ * empty, oversized or non-string value falls back to the minted session rather
114
+ * than breaking `boundEventPayload`'s fit guarantee.
115
+ */
116
+ sessionId?: string;
86
117
  /** Emit each event as an OTEL LogRecord via the global `LoggerProvider`. */
87
118
  otel?: boolean;
88
119
  /**
@@ -23,6 +23,11 @@
23
23
  * pages that never key the authenticated one, so a keyless send is the normal
24
24
  * case, not a failure — the backend attributes those by CORS `Origin` instead.
25
25
  *
26
+ * That path has exactly one observable failure signal, and it is a `console.error`,
27
+ * never a throw or a retry: a *keyed* beacon whose key the backend cannot resolve
28
+ * is still recorded (anonymously) and answered `401`, so a site shipping a broken
29
+ * key is otherwise indistinguishable from a working one. See {@link warnKeyRejected}.
30
+ *
26
31
  * The second, independent output is OTEL (`emitOtelLog`): each event becomes a
27
32
  * LogRecord on the global `LoggerProvider` via the optional peer dep
28
33
  * `@opentelemetry/api-logs`. The host app owns exporters and processing; a
@@ -65,9 +70,28 @@ export declare function sendToCollect(event: TrackingEvent, config: CollectConfi
65
70
  * wire. Note that adding the header makes the request non-simple under CORS, so
66
71
  * an authenticated beacon costs a preflight the anonymous one does not.
67
72
  *
73
+ * The body is sent as `text/plain`, not `application/json`, and that is a cost
74
+ * decision, not a formatting one: `text/plain` is a CORS-safelisted content type,
75
+ * so an anonymous beacon is a "simple request" and the browser sends it with no
76
+ * OPTIONS preflight. With `application/json` every page load paid two requests
77
+ * per event at the edge; at 2026-09 volume that preflight was roughly half of the
78
+ * seven million requests a day the collect API billed. The edge maps `text/plain`
79
+ * to the same template as JSON (ingestion-edge/telemetry.tf), so nothing changes
80
+ * on the wire but the header. The keyed beacon keeps `x-api-key`, which forces a
81
+ * preflight regardless of content type; that path is a small fraction of traffic.
82
+ *
83
+ * A `401` on this path means the key that WAS sent resolved to nothing; the beacon
84
+ * itself was still recorded, anonymously. That is reported through
85
+ * {@link warnKeyRejected} and nothing else — no throw, no retry, no second send.
86
+ * The response is only inspected for an authenticated beacon: an anonymous one has
87
+ * no key to be wrong about, so a `401` there would be a backend fault the developer
88
+ * can do nothing with, and blaming their key for it is worse than silence.
89
+ *
68
90
  * The event stays a bare `object` here so this module never has to import the
69
91
  * assembled shape from `telemetry.ts` (which imports this function).
70
92
  */
93
+ /** CORS-safelisted, so an anonymous beacon needs no preflight — see sendTelemetry. */
94
+ export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
71
95
  export declare function sendTelemetry(event: object, scope?: object, endpoint?: string, apiKey?: unknown): void;
72
96
  /** The `Logger.emit` slice we use — structural, no hard OTEL type coupling. */
73
97
  interface LoggerLike {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nekuda/webmcp-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.0-dev.17.1",
4
4
  "type": "module",
5
5
  "description": "Phase-1 WebMCP SDK: a thin wrapper over document.modelContext that plugin-generated code targets — defineTool + register/unregister lifecycle. This package pins the plugin↔SDK seam; anonymous tool-call tracking (backend transport via apiKey, OTEL LogRecords via otel) is opt-in through registerTools and default-silent, while anonymous usage telemetry is a separate unauthenticated channel that is on by default (opt out with telemetry: false).",
6
6
  "main": "./dist/index.js",
@@ -11,7 +11,10 @@
11
11
  "default": "./dist/index.js"
12
12
  }
13
13
  },
14
- "files": ["dist", "CHANGELOG.md"],
14
+ "files": [
15
+ "dist",
16
+ "CHANGELOG.md"
17
+ ],
15
18
  "publishConfig": {
16
19
  "access": "public"
17
20
  },
@@ -36,5 +39,7 @@
36
39
  "optional": true
37
40
  }
38
41
  },
39
- "trustedDependencies": ["@biomejs/biome"]
42
+ "trustedDependencies": [
43
+ "@biomejs/biome"
44
+ ]
40
45
  }