@uptimizr/agent-core 1.1.1 → 1.2.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.
Files changed (63) hide show
  1. package/AGENTS.md +221 -34
  2. package/README.md +57 -21
  3. package/dist/client.d.ts +22 -5
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +39 -10
  6. package/dist/client.js.map +1 -1
  7. package/dist/context.d.ts +93 -0
  8. package/dist/context.d.ts.map +1 -0
  9. package/dist/context.js +137 -0
  10. package/dist/context.js.map +1 -0
  11. package/dist/index.d.ts +10 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +20 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/nonRegistryTools.d.ts +34 -0
  16. package/dist/nonRegistryTools.d.ts.map +1 -0
  17. package/dist/nonRegistryTools.js +70 -0
  18. package/dist/nonRegistryTools.js.map +1 -0
  19. package/dist/prompt.d.ts +33 -0
  20. package/dist/prompt.d.ts.map +1 -0
  21. package/dist/prompt.js +49 -0
  22. package/dist/prompt.js.map +1 -0
  23. package/dist/provider.d.ts +18 -0
  24. package/dist/provider.d.ts.map +1 -1
  25. package/dist/providers/anthropic.d.ts +18 -1
  26. package/dist/providers/anthropic.d.ts.map +1 -1
  27. package/dist/providers/anthropic.js +35 -3
  28. package/dist/providers/anthropic.js.map +1 -1
  29. package/dist/providers/openai.d.ts +14 -1
  30. package/dist/providers/openai.d.ts.map +1 -1
  31. package/dist/providers/openai.js +23 -1
  32. package/dist/providers/openai.js.map +1 -1
  33. package/dist/queryTool.d.ts +36 -0
  34. package/dist/queryTool.d.ts.map +1 -0
  35. package/dist/queryTool.js +123 -0
  36. package/dist/queryTool.js.map +1 -0
  37. package/dist/registryTools.d.ts +23 -1
  38. package/dist/registryTools.d.ts.map +1 -1
  39. package/dist/registryTools.js +185 -31
  40. package/dist/registryTools.js.map +1 -1
  41. package/dist/skills.d.ts +105 -0
  42. package/dist/skills.d.ts.map +1 -0
  43. package/dist/skills.generated.d.ts +44 -0
  44. package/dist/skills.generated.d.ts.map +1 -0
  45. package/dist/skills.generated.js +511 -0
  46. package/dist/skills.generated.js.map +1 -0
  47. package/dist/skills.js +144 -0
  48. package/dist/skills.js.map +1 -0
  49. package/dist/tools.d.ts +55 -8
  50. package/dist/tools.d.ts.map +1 -1
  51. package/dist/tools.js +41 -1
  52. package/dist/tools.js.map +1 -1
  53. package/dist/writeTools.d.ts +68 -0
  54. package/dist/writeTools.d.ts.map +1 -0
  55. package/dist/writeTools.js +256 -0
  56. package/dist/writeTools.js.map +1 -0
  57. package/llms.txt +148 -20
  58. package/package.json +7 -5
  59. package/skills/attention-hotspots/SKILL.md +88 -0
  60. package/skills/conversion-investigation/SKILL.md +97 -0
  61. package/skills/performance-regression-triage/SKILL.md +106 -0
  62. package/skills/weekly-scene-health/SKILL.md +105 -0
  63. package/skills/xr-comfort-audit/SKILL.md +95 -0
package/AGENTS.md CHANGED
@@ -9,17 +9,25 @@ The **framework-agnostic, browser-safe core** shared by every Uptimizr analytics
9
9
  the agent tool surface **once** (ADR 0050 §1) so `@uptimizr/mcp`, the dashboard assistant, and the
10
10
  demo assistant never drift apart. It owns:
11
11
 
12
- - the **read-only tool catalog** (`readTools`) — one entry per documented collector query endpoint,
13
- **generated** from the `@uptimizr/metrics` semantic metric registry (ADR 0051 §1), so coverage of the
14
- collector's read surface cannot drift;
12
+ - the **read-only analytics catalog** (`readTools`) — one entry per documented collector query
13
+ endpoint, **generated** from the `@uptimizr/metrics` semantic metric registry (ADR 0051 §1), so
14
+ coverage of the collector's read surface cannot drift;
15
+ - the **project-metadata catalog** (`writeTools`) — `annotate`, `define_term`, `save_analysis`
16
+ (ADR 0051 §5), `pin_panel` and `unpin_panel` (ADR 0051 §7), plus the four reads that make them
17
+ usable (`list_annotations`, `list_glossary`, `list_analyses`, `list_panels`). Deliberately a
18
+ **separate export**, never folded into `readTools`, so an integration's read-only stance stays
19
+ inspectable at a glance;
15
20
  - a headless **LLM provider-adapter interface** (`LlmProvider`) — messages + tool schemas in, tool
16
21
  calls or final text out;
17
22
  - the headless **tool-calling loop** (`runAgent`) — LLM ↔ tools ↔ collector.
18
23
 
19
- The core ships **no model and no key**. It only ever reads a consumer's **own** collector via the
20
- `CollectorClient` (`GET`-only, `x-api-key`).
24
+ The core ships **no model and no key**. It only ever talks to a consumer's **own** collector via the
25
+ `CollectorClient` (`x-api-key`). **Events are read-only**: nothing in this package can write, alter
26
+ or delete an analytics event, and there is no ingestion path. The client's `post`/`put`/`delete`
27
+ methods exist for the metadata write tools alone, and the collector gates those behind the
28
+ `annotate` capability (ADR 0051 §9).
21
29
 
22
- ## Tool catalog (read-only)
30
+ ## Analytics tool catalog (read-only)
23
31
 
24
32
  <!-- generated:registry-tool-names:start — generated by `pnpm gen:docs`; edit the metric registry, not this list -->
25
33
 
@@ -29,36 +37,94 @@ The core ships **no model and no key**. It only ever reads a consumer's **own**
29
37
  `position_heatmap`, `session_trajectory`, `aggregate_paths`, `scene_coverage`, `camera_distance`,
30
38
  `click_rays`, `flow_links`, `top_meshes`, `mesh_sources`, `mesh_trend`, `mesh_dwell`,
31
39
  `mesh_blind_spots`, `mesh_interaction_kinds`, `mesh_reachability`, `dead_clicks`, `rage_clicks`,
32
- `hover_dwell`, `interaction_sources`, `top_input_actions`, `camera_gestures`, `navigation_stats`,
33
- `backtrack_ratio`, `perf_summary`, `render_scale_truth`, `perf_distribution`, `fps_histogram`,
34
- `frame_time_percentiles`, `jank_rate`, `perf_churn`, `perf_by_device`, `perf_by_scene`,
35
- `perf_heatmap`, `compile_stalls`, `resource_summary`, `resource_percentiles`, `stability_counts`,
36
- `graphics_diagnostics`, `error_heatmap`, `rendering_technology`, `capability_changes`,
37
- `xr_rotation`, `xr_sources`, `xr_abandonment`, `xr_locomotion`, `xr_tracking_quality`,
38
- `boundary_heatmap`, `boundary_heatmap_stats`, `xr_boundary_contacts`,
39
- `ar_placement_time_to_place`, `ar_placement_attempts`, `ar_placement_surfaces`, `funnel`,
40
- `scene_retention`, `load_bounce_funnel`, `variant_leaderboard`
40
+ `hover_dwell`, `interaction_sources`, `top_input_actions`, `custom_event_vocabulary`,
41
+ `camera_gestures`, `navigation_stats`, `backtrack_ratio`, `perf_summary`, `render_scale_truth`,
42
+ `perf_distribution`, `fps_histogram`, `frame_time_percentiles`, `jank_rate`, `perf_churn`,
43
+ `perf_by_device`, `perf_by_scene`, `perf_heatmap`, `compile_stalls`, `resource_summary`,
44
+ `resource_percentiles`, `stability_counts`, `graphics_diagnostics`, `error_heatmap`,
45
+ `rendering_technology`, `capability_changes`, `xr_rotation`, `xr_sources`, `xr_abandonment`,
46
+ `xr_locomotion`, `xr_tracking_quality`, `boundary_heatmap`, `boundary_heatmap_stats`,
47
+ `xr_boundary_contacts`, `ar_placement_time_to_place`, `ar_placement_attempts`,
48
+ `ar_placement_surfaces`, `funnel`, `scene_retention`, `load_bounce_funnel`, `variant_leaderboard`,
49
+ `insight_baseline`, `insight_movers`, `insight_anomalies`, `insight_significance`,
50
+ `insight_scene_health`
51
+
52
+ Only on a key holding `query:raw`, and only when the collector runs with
53
+ `ENABLE_RAW_SESSION_RETENTION` (ADR 0003):
54
+
55
+ `session_narrative`
41
56
 
42
57
  <!-- generated:registry-tool-names:end -->
43
58
 
44
59
  Each name is a metric in the collector's semantic metric registry (ADR 0051) and maps one-to-one to
45
60
  a documented query endpoint. Most accept `since`/`until` (epoch ms) plus endpoint-specific filters.
46
61
 
62
+ ## The `query` tool (the query DSL)
63
+
64
+ One more tool sits after the generated list, and it is **not** per-metric: `query` (ADR 0051 §3).
65
+ Its input _is_ the query DSL, so it can run any metric above with any filter that metric declares:
66
+
67
+ ```jsonc
68
+ {
69
+ "v": 1,
70
+ "metric": "mesh_sources",
71
+ "range": { "since": 1757000000000, "until": 1757600000000 },
72
+ "filters": { "scene": "lobby", "cameraMode": "first-person" },
73
+ "limit": 20,
74
+ "format": "summary",
75
+ }
76
+ ```
77
+
78
+ - **`range` is required** — both ends, epoch ms. There is no unbounded query.
79
+ - **`format` defaults to `table` here**, not `full`: this is a new, agent-facing surface, so the
80
+ envelope with the sample size and the truncation flag is the default.
81
+ - The grammar is **closed**. Metrics, dimensions and filters are exactly the registry's vocabulary;
82
+ naming something outside it returns a `400` whose `issues[].accepted` lists what would have
83
+ worked. Read that rather than guessing again.
84
+ - `dimensions` may be any subset a metric declares **when** its measure is a portable count —
85
+ event counts, mesh and interaction tallies, input actions, camera gestures. A spatial heatmap or a
86
+ percentile is computed at one fixed grain and refuses anything else, naming the grain it supports.
87
+ - **`compare`** — another `{ range }` or `{ segment }`; the result comes back joined on the
88
+ dimension key as `{ current, previous, delta, deltaPct }`, with a significance test where the
89
+ measure is a count and both windows clear the metric's minimum. Never subtract two results by hand.
90
+ - **`explain: true`** — the compiled plan instead of the rows: the tier, the SQL with its parameters
91
+ left unbound, `params` by name and type (never value), `rowsScanned`, and `warnings` (a capture
92
+ channel that produced nothing, a sample below the metric's minimum, a truncated result).
93
+ - **`drillQuery`** — every row of a `summary` carries the whole query narrowed to that row, ready to
94
+ send straight back.
95
+ - `order` takes a measure column, and only where the result is a ranked list.
96
+
97
+ It reaches the collector as `GET /api/v1/query?q=<url-encoded JSON>`, so `CollectorClient` stays
98
+ `GET`-only — an Uptimizr agent remains structurally incapable of writing.
99
+
100
+ ## Non-metric reads
101
+
102
+ `readTools` is generated from the metric registry, with one deliberate exception: collector reads
103
+ that are **configuration rather than measurements** and so have no registry entry
104
+ (`nonRegistryTools.ts`). Today that is `list_subscriptions` (ADR 0051 §6) — the project's
105
+ standing conditional subscriptions, what they watch for and how each last went. It takes no
106
+ arguments and no time range. Creating or deleting a subscription needs the `annotate` capability
107
+ and is not available as a tool; do it over plain HTTP.
108
+
47
109
  ## Result formats (`format`)
48
110
 
49
111
  Every **aggregate** tool in the catalog declares a `format` argument — `full | table | summary`.
50
112
  It picks the envelope the rows arrive in and filters nothing (ADR 0051 §2). The collector's
51
113
  `preSerialization` hook does the shaping, so the tool definitions stay pure `buildRequest` functions.
52
114
 
53
- | `format` | Returns | Use it when |
54
- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
55
- | `summary` | A bounded digest — `ranked` top rows, a `series` trend, merged spatial `clusters` or a single `record` — with shares, a sample size, the metric's `caveats` and a templated `reading` sentence. | **The default choice when a model will read the result.** |
56
- | `table` | `{ meta, rows }`: the rows plus metric, range, applied filters, sample size, row count, a truncation flag and the registry's limits. | You need every row _and_ the context to judge how far to trust it. |
57
- | `full` | The bare rows, unchanged. | You are post-processing the rows yourself. Today's default. |
115
+ **`buildRequest` applies `DEFAULT_TOOL_FORMAT` (`table`) when the caller names no format** and
116
+ sends it explicitly, so the collector's own default stays `full` and the dashboard is unaffected.
117
+ A tool's `outputSchema` describes all three envelopes, so whichever one comes back validates.
118
+
119
+ | `format` | Returns | Use it when |
120
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
121
+ | `summary` | A bounded digest — `ranked` top rows, a `series` trend, merged spatial `clusters` or a single `record` — with shares, a sample size, the metric's `caveats` and a templated `reading` sentence. | **Ask for it** whenever a model reads the result and you do not need every row. |
122
+ | `table` | `{ meta, rows }`: the rows plus metric, range, applied filters, sample size, row count, a truncation flag and the registry's limits. | **The default** — every row _and_ the context to judge it. |
123
+ | `full` | The bare rows, unchanged, with no envelope at all. | You are post-processing the rows yourself. |
58
124
 
59
125
  - `summary` is capped at the metric's `limits.maxSummaryRows`, which is what makes a heatmap, voxel
60
126
  cloud or long leaderboard safe to hand a model at all. This matters most for the **local** backend:
61
- a 4-bit 7–8B model's whole context can be filled by one `full` heatmap.
127
+ a 4-bit 7–8B model's whole context can be filled by one heatmap's rows, in `full` or in `table`.
62
128
  - `reading` and `caveats` are templated from the registry's column semantics by pure code — no model
63
129
  — so identical rows always produce identical words. Quote them rather than re-deriving them.
64
130
  - Shares appear only where the measure can honestly be summed; an FPS or ratio metric reports
@@ -66,17 +132,90 @@ It picks the envelope the rows arrive in and filters nothing (ADR 0051 §2). The
66
132
  `cellSize` for world space.
67
133
  - `session_meta` and `scene_representation` are single stored records, not aggregations, and declare
68
134
  no `format`.
69
- - `full` remains the generated catalog's default so that existing consumers (the dashboard) are
70
- unaffected. Pass `format: "summary"` explicitly from an agent path; the default flip is a separate,
71
- documented change.
135
+ - The **tools** default to `table`; the collector still defaults to `full`, so a consumer that calls
136
+ the HTTP endpoint directly (the dashboard) is unaffected. `table` is self-describing but not
137
+ bounded — its `meta` costs a fixed ~200 characters and the rows are still all of them — so pass
138
+ `format: "summary"` when the result could be large.
139
+
140
+ ## Project context (ADR 0051 §5)
141
+
142
+ Read `GET /api/v1/context` **before** the first question and put a rendering of it in the system
143
+ prompt. It is the only read that describes the project rather than the API: the real scene ids and
144
+ named region ids, the custom events the application emits with their `props` keys and coarse types,
145
+ top meshes, bound input actions, data freshness, retention flags, and `metrics.disabledByCapture` —
146
+ metrics that return empty because their capture channel is off, not because nothing happened.
147
+
148
+ `renderContextForPrompt(document)` turns it into a compact block (~1.5 k characters, capped and
149
+ truncated on a line boundary) that a 1–3 B local model can carry alongside the tool schemas. It is
150
+ pure, tolerant of a partial or unfamiliar document, and returns `""` when there is nothing to say —
151
+ so a collector too old to serve the endpoint degrades to the prompt you had before rather than
152
+ failing. Append it unconditionally.
153
+
154
+ ## Packaged methodology skills (ADR 0051 §7)
155
+
156
+ A prompt names tools; a **skill** carries the method. Each one is an Agent Skills file,
157
+ `skills/<name>/SKILL.md`, shipped in this tarball: frontmatter declaring the tools it relies on,
158
+ the key capabilities it needs and the arguments it takes, and a body of numbered steps — orient on
159
+ `uptimizr://context`, start from the score, test the change before calling it real, date it, and
160
+ end with recommendations plus something worth keeping. Read one before you improvise:
161
+ `skills/weekly-scene-health/SKILL.md`.
162
+
163
+ <!-- generated:registry-skill-names:start — generated by `pnpm gen:docs`; edit the SKILL.md files, not this list -->
164
+
165
+ - `attention_hotspots` (scene (required), range) — Find where visitors look and click in a scene: view-direction concentration, gaze→mesh flow, the objects that draw the most interaction, and the ones nobody ever notices. USE FOR: deciding where to put a call to action, finding ignored or invisible content, explaining why an object gets no clicks, laying out a scene around what people actually look at.
166
+ Method: `skills/attention-hotspots/SKILL.md`. Tools: `camera_heatmap`, `flow_links`, `click_rays`, `top_meshes`, `mesh_dwell`, `mesh_blind_spots`, `query`.
167
+ - `conversion_investigation` (scene, range) — Find out where a funnel loses people and whether the loss is real: step-by-step drop-off, the bounce that happens before the funnel even starts, scene-to-scene retention, variant performance, and the interaction failures (dead clicks, rage clicks, unreachable meshes) that explain a stalled step. USE FOR: a funnel that converts worse than expected, an A/B variant comparison, "where do people drop off", diagnosing a step nobody completes.
168
+ Method: `skills/conversion-investigation/SKILL.md`. Tools: `funnel`, `load_bounce_funnel`, `scene_retention`, `variant_leaderboard`, `dead_clicks`, `rage_clicks`, `mesh_reachability`, `flow_links`, `insight_significance`, `insight_movers`, `query`.
169
+ - `performance_regression_triage` (scene, range) — Triage a frame-rate or stability regression: confirm it moved, date it, locate it (which scene, device class, place in the scene), and name the mechanism — jank, shader compile stalls, memory pressure, a render-scale change or a rendering-technology shift. USE FOR: "the app got slower", a FPS drop after a release, stutter reports, deciding whether a regression is real or noise.
170
+ Method: `skills/performance-regression-triage/SKILL.md`. Tools: `insight_movers`, `insight_anomalies`, `insight_significance`, `insight_baseline`, `perf_summary`, `perf_distribution`, `frame_time_percentiles`, `jank_rate`, `perf_by_device`, `perf_by_scene`, `perf_heatmap`, `compile_stalls`, `resource_percentiles`, `render_scale_truth`, `rendering_technology`, `query`.
171
+ - `weekly_scene_health` (scene, range) — A weekly health check for a scene (or the whole project): a weighted health score with every factor traced back to the metric behind it, what changed against last week, traffic, event mix, performance, and the most-interacted meshes. USE FOR: the recurring "how is the scene doing?" review, a scheduled weekly or monthly report, a first look at a project you do not know yet, deciding which scene to investigate next.
172
+ Method: `skills/weekly-scene-health/SKILL.md`. Tools: `insight_scene_health`, `insight_movers`, `insight_baseline`, `insight_significance`, `insight_anomalies`, `event_counts`, `timeseries`, `perf_summary`, `top_meshes`, `list_sessions`, `query`.
173
+ - `xr_comfort_audit` (scene, range) — Audit VR/AR comfort for a scene (or the whole project): rapid head rotation, locomotion style, tracking quality, guardian/boundary contacts, input-source mix, and the short sessions that mean someone took the headset off. USE FOR: motion-sickness complaints, immersive sessions that end early, choosing a locomotion scheme, checking whether a play space is big enough.
174
+ Method: `skills/xr-comfort-audit/SKILL.md`. Tools: `xr_rotation`, `xr_locomotion`, `xr_abandonment`, `xr_sources`, `xr_tracking_quality`, `xr_boundary_contacts`, `boundary_heatmap_stats`, `insight_scene_health`, `insight_movers`, `query`.
175
+
176
+ <!-- generated:registry-skill-names:end -->
177
+
178
+ `AGENT_SKILLS` is those files compiled to data (`skills.generated.ts`, by
179
+ `scripts/gen-agent-skills.mjs`) — this package is browser-safe and never reads them from disk at
180
+ runtime. Each entry carries the title, description, tools, capabilities, arguments and a pure
181
+ `render(args)` producing the single user turn. `getAgentSkill(name)` resolves one by either
182
+ spelling (`xr_comfort_audit` or `xr-comfort-audit`), and `xr_comfort_review` still resolves to
183
+ `xr_comfort_audit` for clients that saved the pre-#316 name.
184
+
185
+ Three consumers drive the same text and must not drift: `@uptimizr/mcp` registers each as an MCP
186
+ prompt template, `uptimizr agent report --skill` seeds its headless transcript with it, and the
187
+ assistant offers it as a starter prompt. **Reword the SKILL.md and run `pnpm gen:skills`** — never
188
+ copy a skill's text anywhere else.
189
+
190
+ ## Shared system-prompt fragments
191
+
192
+ `ANALYTICS_AGENT_GUIDELINES` is what every analytics agent says about the _data_ — figures come
193
+ from tools and are never invented, everything is aggregate and privacy-preserving, timestamps are
194
+ epoch milliseconds, a tool error is explained rather than papered over. `renderCurrentTimeLine(nowMs)`
195
+ gives the model a clock so it can resolve "this week" into `since`/`until`. The browser assistant
196
+ and the report CLI both build their system prompt from these plus their own role sentence; only the
197
+ role and the output format legitimately differ.
72
198
 
73
199
  ## Rules for agents
74
200
 
75
- - **Read-only and privacy-preserving.** Never add ingestion, mutation, or raw per-session event
76
- tools. The surface is aggregate-only; no data leaves the consumer's infrastructure (ADR 0003 /
77
- ADR 0017). A new tool = a new **metric registry entry** in `@uptimizr/metrics` for a documented query
78
- endpoint never a hand-written catalog entry here and no aggregation/business logic (that lives
79
- in the collector, ADR 0005).
201
+ - **Events are read-only; privacy-preserving throughout.** Never add an ingestion tool, an
202
+ event-mutating tool, or a raw per-session event tool. The analytics surface is aggregate-only and
203
+ no data leaves the consumer's infrastructure (ADR 0003 / ADR 0017). A new analytics tool = a new
204
+ **metric registry entry** in `@uptimizr/metrics` for a documented query endpoint never a
205
+ hand-written catalog entry here — and no aggregation/business logic (that lives in the collector,
206
+ ADR 0005).
207
+ - **Writes stay in `writeTools`, and stay metadata.** The only writable surface is project metadata
208
+ (annotations, glossary, saved analyses, pinned panels), each call needs an `annotate` key, each is
209
+ bounded at the collector's edge and audited there. Never add a write tool to `readTools`, and
210
+ never add one that reaches a non-metadata endpoint.
211
+ - **A pinned panel is metadata too.** `pin_panel` stores a **spec** — a metric id, a chart name,
212
+ some column names — that the dashboard renders with panels it already ships; no module is loaded
213
+ and nothing is evaluated, so pinning does not widen the dashboard's trust boundary (ADR 0041).
214
+ Send the same `query` document the `query` tool takes, with `range: "inherit"` so the panel
215
+ follows the dashboard's own time filter, and a `chart` the metric's grain supports — the collector
216
+ refuses the rest and names the charts that would have worked. Read `list_panels` before pinning,
217
+ so you extend the dashboard rather than duplicate it; `unpin_panel` removes a panel for
218
+ **everyone** on the project, so never unpin one you have not read.
80
219
  - **Browser-safe.** No Node dependencies, no `types: ["node"]`. At runtime this package uses `zod`
81
220
  plus the pure, data-only `@uptimizr/metrics` package — **never** `@uptimizr/db`, which owns the
82
221
  DuckDB store and its ~37 MB native binding. `src/__tests__/browserSafety.test.ts` bundles the
@@ -84,21 +223,69 @@ It picks the envelope the rows arrive in and filters nothing (ADR 0051 §2). The
84
223
  if `@uptimizr/db` (or anything else with a native/optional binary dependency) reappears in the
85
224
  manifest. Anything that needs `process.env`, stdio, or the filesystem belongs in a consumer
86
225
  package (e.g. `@uptimizr/mcp`), not here.
226
+ - **Start from an insight primitive on an open-ended question.** "How are things?" does not mean
227
+ "call thirty tools": `insight_scene_health` says _which scene_ to look at (six weighted factors,
228
+ each naming the metric, raw value and project baseline behind it; 50 is the project norm, not a
229
+ pass mark), `insight_movers` compares every comparable metric with the previous equal window and
230
+ ranks the changes by how unusual each is, and `insight_baseline` says whether a level is outside
231
+ normal for that scene. Two fields decide whether a mover is reportable: `direction` is the
232
+ registry's opinion of what a _rise_ means (so a rise in a `down` metric is a regression, not an
233
+ improvement), and `aboveMinSample: false` means the delta is arithmetic but not evidence — those
234
+ rows are returned rather than dropped, and must never be reported as findings.
235
+ - **Then `insight_anomalies` to put a date on it.** `insight_movers` compares two windows you
236
+ chose; `insight_anomalies` walks one metric's whole series and names the buckets that do not
237
+ belong — `spike` / `drop` for a single bucket far from the ones before it, and `shift` at the
238
+ bucket where the level moved and _stayed_ moved, which is the shape a release regression has and
239
+ the one no per-bucket threshold can see. Quote `bucketStart`, and read `contributor`: where the
240
+ metric declares a dimension it can be split by, the row names the mesh, source, input action,
241
+ event type or scene holding the largest share of the excess. Its `z` is in standard deviations
242
+ while `insight_movers`' is the same ratio unscaled, so the two columns are not comparable.
243
+ - **Call `insight_significance` before calling a single change real.** It reports the effect, a 95%
244
+ interval and a p-value for one metric across two windows, with the test chosen from what the
245
+ measure is. Read `ci95` before `p` — an interval straddling 0 means you cannot tell yet — and
246
+ read `powerNote`, which distinguishes "no effect" from "not enough data". It compares two
247
+ _windows_, not two segments.
87
248
  - Tool definitions are pure (`buildRequest`) and must stay unit-testable without a live collector.
88
249
  - The 20 tool names (and argument schemas) that shipped before the registry are a public contract:
89
250
  `src/__tests__/shippedToolCompat.test.ts` pins them against a frozen fixture. Widening a tool with
90
251
  a new **optional** argument is fine; renaming one or making an argument required is not.
91
252
  - Keep provider adapters thin and out of this package: implement `LlmProvider` in the consumer.
253
+ - `ProviderResponse.usage` is **optional on every field**: a provider may report neither, one or
254
+ both token counts, and a local backend reports nothing. Treat a missing number as "not reported",
255
+ never as zero — a cost line that prints `0` when nothing was measured is a lie.
92
256
 
93
257
  ## Programmatic API
94
258
 
95
- `readTools`, `coreReadTools`, `selectReadTools(kind)`, `filterReadTools(names)`,
259
+ `readTools`, `rawTools`, `coreReadTools`, `selectReadTools(kind)`, `filterReadTools(names)`,
96
260
  `registryToTools(metrics?)`, `createCollectorClient(config)`, `toToolSchemas(tools?)`,
97
- `runAgent(options)`, plus the `LlmProvider` / `AgentMessage` / `AgentToolCall` /
98
- `ProviderResponse` types.
261
+ `runAgent(options)`, `renderContextForPrompt(context, nowMs?)`, `AGENT_SKILLS` /
262
+ `getAgentSkill(name)` / `renderAgentSkill(name, args)`, `ANALYTICS_AGENT_GUIDELINES` /
263
+ `renderCurrentTimeLine(nowMs)`, plus the `LlmProvider` / `AgentMessage` / `AgentToolCall` /
264
+ `ProviderResponse` / `ProviderUsage` / `AgentSkill` / `PromptContextDocument` types.
265
+
266
+ Metadata writes (ADR 0051 §5/§7): `writeTools`, `mutatingWriteTools`, the individual
267
+ `annotateTool` / `defineTermTool` / `saveAnalysisTool` / `pinPanelTool` / `unpinPanelTool` /
268
+ `listAnnotationsTool` / `listGlossaryTool` / `listAnalysesTool` / `listPanelsTool`, the `WriteTool`
269
+ type and `WriteNotSupportedError` (thrown when a hand-built read-only client has no write
270
+ transport). Each has an `execute(client, args)` rather than a `buildRequest`, because a write is one
271
+ call rather than a request description. `pin_panel` and `unpin_panel` mutate; `list_panels` does
272
+ not, and `mutatingWriteTools` is the filtered view. `unpin_panel` is the only tool here that uses
273
+ `CollectorClient.delete`.
274
+
275
+ The catalog is 77 tools on a plain `query` key. A small local model cannot hold every schema in its
276
+ function-calling prompt — hand a run `coreReadTools` or `filterReadTools([...])` rather than the
277
+ full catalog.
278
+
279
+ ### Capability-gated tools
99
280
 
100
- The catalog is ~69 tools. A small local model cannot hold every schema in its function-calling
101
- prompt hand a run `coreReadTools` or `filterReadTools([...])` rather than the full catalog.
281
+ `readTools` is the **`query`** surface every tool a plain read key may call. `rawTools` is
282
+ generated from the registry metrics whose endpoint declares `capability: "query:raw"` (today just
283
+ `session_narrative`, the compacted account of one session) and is kept separate on purpose: the
284
+ capability belongs to the _key the agent was handed_, not to the agent. Register it only after
285
+ confirming the key holds `query:raw` via `GET /api/v1/whoami` — a tool that always answers `403`
286
+ costs a model a turn and invites a retry. The collector refuses those endpoints unless it also runs
287
+ with `ENABLE_RAW_SESSION_RETENTION`, so the capability is necessary but never sufficient
288
+ (ADR 0003 / ADR 0051 §7).
102
289
 
103
290
  ## More
104
291
 
package/README.md CHANGED
@@ -3,21 +3,27 @@
3
3
  The **framework-agnostic, browser-safe core** for Uptimizr analytics agents. It defines the agent
4
4
  tool surface **once** so every consumer — the [`@uptimizr/mcp`](../mcp/README.md) server, the
5
5
  dashboard assistant, and the backend-less demo assistant — asks the same questions of the same
6
- read-only collector query API (ADR 0050 §1).
6
+ collector API (ADR 0050 §1).
7
7
 
8
- It owns three things:
8
+ It owns four things:
9
9
 
10
- 1. **The read-only tool catalog** (`readTools`) — one entry per documented aggregate query
10
+ 1. **The read-only analytics catalog** (`readTools`) — one entry per documented aggregate query
11
11
  endpoint. Pure Zod shapes, no Node dependencies, unit-testable without a live collector.
12
- 2. **A headless LLM provider-adapter interface** (`LlmProvider`) — send messages + tool schemas,
12
+ 2. **The project-metadata catalog** (`writeTools`) — `annotate`, `define_term`, `save_analysis` and
13
+ their three read siblings (ADR 0051 §5). A deliberately **separate** export, never folded into
14
+ `readTools`, so an integration's read-only stance stays inspectable at a glance.
15
+ 3. **A headless LLM provider-adapter interface** (`LlmProvider`) — send messages + tool schemas,
13
16
  receive tool calls or a final answer. The core ships **no model and no key**; adapters
14
17
  (WebLLM/WebGPU, an OpenAI-compatible or Anthropic endpoint) are user-selected and
15
18
  user-controlled (ADR 0050 §4).
16
- 3. **The headless tool-calling loop** (`runAgent`) — drives the LLM ↔ tools ↔ collector round trip,
19
+ 4. **The headless tool-calling loop** (`runAgent`) — drives the LLM ↔ tools ↔ collector round trip,
17
20
  framework-agnostic and runnable in a browser, a Node service, a CLI, or a bot.
18
21
 
19
- Everything here is **strictly read-only**: the loop only ever issues collector `GET`s through the
20
- catalog. There are **no** ingestion, mutation, or raw per-session event tools (ADR 0003 / ADR 0017).
22
+ **Events are read-only**: nothing here can write, alter or delete an analytics event, and there are
23
+ no ingestion or raw per-session event tools (ADR 0003 / ADR 0017 / ADR 0051 §9). The agent loop
24
+ issues collector `GET`s only. The metadata tools are the single writable surface — they reach three
25
+ metadata endpoints and nothing else, each call needs an `annotate` key, and the collector bounds and
26
+ audits every one.
21
27
 
22
28
  ## Install
23
29
 
@@ -59,7 +65,7 @@ const result = await runAgent({
59
65
  console.log(result.content); // the model's final answer
60
66
  ```
61
67
 
62
- ## Tool catalog (read-only)
68
+ ## Analytics tool catalog (read-only)
63
69
 
64
70
  <!-- generated:registry-tool-names:start — generated by `pnpm gen:docs`; edit the metric registry, not this table -->
65
71
 
@@ -69,21 +75,29 @@ console.log(result.content); // the model's final answer
69
75
  `position_heatmap`, `session_trajectory`, `aggregate_paths`, `scene_coverage`, `camera_distance`,
70
76
  `click_rays`, `flow_links`, `top_meshes`, `mesh_sources`, `mesh_trend`, `mesh_dwell`,
71
77
  `mesh_blind_spots`, `mesh_interaction_kinds`, `mesh_reachability`, `dead_clicks`, `rage_clicks`,
72
- `hover_dwell`, `interaction_sources`, `top_input_actions`, `camera_gestures`, `navigation_stats`,
73
- `backtrack_ratio`, `perf_summary`, `render_scale_truth`, `perf_distribution`, `fps_histogram`,
74
- `frame_time_percentiles`, `jank_rate`, `perf_churn`, `perf_by_device`, `perf_by_scene`,
75
- `perf_heatmap`, `compile_stalls`, `resource_summary`, `resource_percentiles`, `stability_counts`,
76
- `graphics_diagnostics`, `error_heatmap`, `rendering_technology`, `capability_changes`,
77
- `xr_rotation`, `xr_sources`, `xr_abandonment`, `xr_locomotion`, `xr_tracking_quality`,
78
- `boundary_heatmap`, `boundary_heatmap_stats`, `xr_boundary_contacts`,
79
- `ar_placement_time_to_place`, `ar_placement_attempts`, `ar_placement_surfaces`, `funnel`,
80
- `scene_retention`, `load_bounce_funnel`, `variant_leaderboard`
78
+ `hover_dwell`, `interaction_sources`, `top_input_actions`, `custom_event_vocabulary`,
79
+ `camera_gestures`, `navigation_stats`, `backtrack_ratio`, `perf_summary`, `render_scale_truth`,
80
+ `perf_distribution`, `fps_histogram`, `frame_time_percentiles`, `jank_rate`, `perf_churn`,
81
+ `perf_by_device`, `perf_by_scene`, `perf_heatmap`, `compile_stalls`, `resource_summary`,
82
+ `resource_percentiles`, `stability_counts`, `graphics_diagnostics`, `error_heatmap`,
83
+ `rendering_technology`, `capability_changes`, `xr_rotation`, `xr_sources`, `xr_abandonment`,
84
+ `xr_locomotion`, `xr_tracking_quality`, `boundary_heatmap`, `boundary_heatmap_stats`,
85
+ `xr_boundary_contacts`, `ar_placement_time_to_place`, `ar_placement_attempts`,
86
+ `ar_placement_surfaces`, `funnel`, `scene_retention`, `load_bounce_funnel`, `variant_leaderboard`,
87
+ `insight_baseline`, `insight_movers`, `insight_anomalies`, `insight_significance`,
88
+ `insight_scene_health`
89
+
90
+ Only on a key holding `query:raw`, and only when the collector runs with
91
+ `ENABLE_RAW_SESSION_RETENTION` (ADR 0003):
92
+
93
+ `session_narrative`
81
94
 
82
95
  <!-- generated:registry-tool-names:end -->
83
96
 
84
97
  `readTools` is **generated** from the semantic metric registry in `@uptimizr/metrics`
85
98
  ([ADR 0051](https://github.com/RaananW/Uptimizr/blob/main/docs/adr/0051-ai-first-analytics-layer.md) §1):
86
- one tool per metric the collector serves on a read endpoint — **69** today, covering sessions and
99
+ one tool per metric the collector serves on a read endpoint — **75** on a plain `query` key today,
100
+ plus `session_narrative` behind `query:raw` — covering sessions and
87
101
  scenes, pointer/world/gaze/camera heatmaps, mesh attention and blind spots, dead and rage clicks,
88
102
  navigation and desire lines, performance (FPS distribution, jank, compile stalls, per-device and
89
103
  per-scene), errors and stability, XR/AR comfort and placement, and conversion (funnel, scene
@@ -100,8 +114,10 @@ Each tool carries:
100
114
  caveats (sample-size limits, which capture channel must be on);
101
115
  - an **input schema** built from the endpoint's filters — most accept `since`/`until` (epoch ms)
102
116
  plus `scene`, `session`, `source`, `bins`, `cellSize`, `limit`, `cameraMode`, `region`, …;
103
- - an **output schema** (`{ rows: Row[] }`) derived from the metric's row schema, which
104
- `@uptimizr/mcp` registers as the MCP `outputSchema`.
117
+ - an **output schema** derived from the metric's row schema and the `format` result envelopes —
118
+ `{ rows }`, the `{ meta, rows }` table envelope, or a `summary` digest — which `@uptimizr/mcp`
119
+ registers as the MCP `outputSchema`. A tool asks for `table` (`DEFAULT_TOOL_FORMAT`) unless the
120
+ caller names another format.
105
121
 
106
122
  The registry lives in `@uptimizr/metrics`, a dependency-free package (`zod` +
107
123
  `@uptimizr/schema`), so installing this one never downloads a database driver: `@uptimizr/db` and
@@ -109,11 +125,31 @@ its ~37 MB native DuckDB binding are **not** a dependency. Two tests keep that t
109
125
  asserts no `node:` built-in or DuckDB driver can reach a browser build, and a manifest test fails
110
126
  if any package with a native or optional binary dependency reappears.
111
127
 
128
+ ## Methodology skills
129
+
130
+ The packaged investigations (ADR 0051 §7) — the same ones `@uptimizr/mcp` serves as prompt
131
+ templates and `uptimizr agent report --skill` runs headlessly. Each is an Agent Skills file under
132
+ `skills/<name>/SKILL.md` in this package; `AGENT_SKILLS` is those files compiled to data, and
133
+ `getAgentSkill(name).render({ scene })` returns the user turn. `*` marks a required argument.
134
+
135
+ <!-- generated:registry-skills:start — generated by `pnpm gen:docs`; edit the SKILL.md files, not this list -->
136
+
137
+ | Skill | Arguments | What it produces, and when to use it | Tools its method names |
138
+ | ------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
139
+ | `attention_hotspots` | `scene*`, `range?` | Find where visitors look and click in a scene: view-direction concentration, gaze→mesh flow, the objects that draw the most interaction, and the ones nobody ever notices. USE FOR: deciding where to put a call to action, finding ignored or invisible content, explaining why an object gets no clicks, laying out a scene around what people actually look at. | `camera_heatmap`, `flow_links`, `click_rays`, `top_meshes`, `mesh_dwell`, `mesh_blind_spots`, `query` |
140
+ | `conversion_investigation` | `scene?`, `range?` | Find out where a funnel loses people and whether the loss is real: step-by-step drop-off, the bounce that happens before the funnel even starts, scene-to-scene retention, variant performance, and the interaction failures (dead clicks, rage clicks, unreachable meshes) that explain a stalled step. USE FOR: a funnel that converts worse than expected, an A/B variant comparison, "where do people drop off", diagnosing a step nobody completes. | `funnel`, `load_bounce_funnel`, `scene_retention`, `variant_leaderboard`, `dead_clicks`, `rage_clicks`, `mesh_reachability`, `flow_links`, `insight_significance`, `insight_movers`, `query` |
141
+ | `performance_regression_triage` | `scene?`, `range?` | Triage a frame-rate or stability regression: confirm it moved, date it, locate it (which scene, device class, place in the scene), and name the mechanism — jank, shader compile stalls, memory pressure, a render-scale change or a rendering-technology shift. USE FOR: "the app got slower", a FPS drop after a release, stutter reports, deciding whether a regression is real or noise. | `insight_movers`, `insight_anomalies`, `insight_significance`, `insight_baseline`, `perf_summary`, `perf_distribution`, `frame_time_percentiles`, `jank_rate`, `perf_by_device`, `perf_by_scene`, `perf_heatmap`, `compile_stalls`, `resource_percentiles`, `render_scale_truth`, `rendering_technology`, `query` |
142
+ | `weekly_scene_health` | `scene?`, `range?` | A weekly health check for a scene (or the whole project): a weighted health score with every factor traced back to the metric behind it, what changed against last week, traffic, event mix, performance, and the most-interacted meshes. USE FOR: the recurring "how is the scene doing?" review, a scheduled weekly or monthly report, a first look at a project you do not know yet, deciding which scene to investigate next. | `insight_scene_health`, `insight_movers`, `insight_baseline`, `insight_significance`, `insight_anomalies`, `event_counts`, `timeseries`, `perf_summary`, `top_meshes`, `list_sessions`, `query` |
143
+ | `xr_comfort_audit` | `scene?`, `range?` | Audit VR/AR comfort for a scene (or the whole project): rapid head rotation, locomotion style, tracking quality, guardian/boundary contacts, input-source mix, and the short sessions that mean someone took the headset off. USE FOR: motion-sickness complaints, immersive sessions that end early, choosing a locomotion scheme, checking whether a play space is big enough. | `xr_rotation`, `xr_locomotion`, `xr_abandonment`, `xr_sources`, `xr_tracking_quality`, `xr_boundary_contacts`, `boundary_heatmap_stats`, `insight_scene_health`, `insight_movers`, `query` |
144
+
145
+ <!-- generated:registry-skills:end -->
146
+
112
147
  ## API
113
148
 
114
149
  | Export | Purpose |
115
150
  | ------------------------------ | -------------------------------------------------------------------- |
116
- | `readTools` | The read-only tool catalog (one entry per query endpoint). |
151
+ | `readTools` | The read-only analytics catalog (one entry per query endpoint). |
152
+ | `writeTools` | The `annotate`-gated project-metadata tools (ADR 0051 §5). |
117
153
  | `coreReadTools` | Focused subset for small local models (a filtered view, not a copy). |
118
154
  | `selectReadTools(kind)` | Pick `"core"` or `"full"`. |
119
155
  | `filterReadTools(names)` | Narrow the catalog to specific tool names, in catalog order. |
package/dist/client.d.ts CHANGED
@@ -1,13 +1,26 @@
1
1
  /** A query-string parameter map. `undefined` values are omitted from the request. */
2
2
  export type QueryParams = Record<string, string | number | undefined>;
3
3
  /**
4
- * A minimal **read-only** HTTP client over the collector query API. It performs
5
- * `GET` requests only and holds no business logic it is a thin transport that
6
- * mirrors the dashboard's `CollectorApi` so an agent reads the same aggregated
7
- * results a human would (ADR 0005 / ADR 0017).
4
+ * A minimal HTTP client over the collector API. It holds no business logic — it
5
+ * is a thin transport that mirrors the dashboard's `CollectorApi` so an agent
6
+ * reads the same aggregated results a human would (ADR 0005 / ADR 0017).
7
+ *
8
+ * `get` is the whole analytics surface: **events are read-only**, and nothing
9
+ * here can write, alter or delete one (ADR 0051 §9). The three write methods
10
+ * exist for exactly one caller — the metadata write tools of #310 (annotations,
11
+ * glossary, saved analyses) — which the MCP server registers only when the key
12
+ * holds `annotate`. They are optional so a hand-built read-only client is still
13
+ * a valid `CollectorClient`; {@link writeTools} reports a clear error instead of
14
+ * calling a method that is not there.
8
15
  */
9
16
  export interface CollectorClient {
10
17
  get(path: string, params?: QueryParams): Promise<unknown>;
18
+ /** Create a metadata row. Used only by the `annotate` write tools. */
19
+ post?(path: string, body: unknown): Promise<unknown>;
20
+ /** Upsert a metadata row (the glossary). Used only by the `annotate` write tools. */
21
+ put?(path: string, body: unknown): Promise<unknown>;
22
+ /** Delete a metadata row. Used only by the `annotate` write tools. */
23
+ delete?(path: string): Promise<unknown>;
11
24
  }
12
25
  /**
13
26
  * The `(collectorUrl, apiKey)` pair a collector client binds to. Every consumer
@@ -26,8 +39,12 @@ export declare class CollectorError extends Error {
26
39
  constructor(message: string, status: number);
27
40
  }
28
41
  /**
29
- * Build a read-only collector client bound to one `(collectorUrl, apiKey)` pair.
42
+ * Build a collector client bound to one `(collectorUrl, apiKey)` pair.
30
43
  * `fetchImpl` is injectable for testing; it defaults to the global `fetch`.
44
+ *
45
+ * The write methods are the narrow metadata path of #310 and nothing else:
46
+ * whether they are allowed at all is decided by the collector from the key's
47
+ * capabilities (`403` without `annotate`), not here.
31
48
  */
32
49
  export declare function createCollectorClient(config: CollectorClientConfig, fetchImpl?: typeof fetch): CollectorClient;
33
50
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3D;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gEAAgE;AAChE,qBAAa,cAAe,SAAQ,KAAK;IAGrC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;CAK1B;AAMD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,qBAAqB,EAC7B,SAAS,GAAE,OAAO,KAAa,GAC9B,eAAe,CAmBjB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;AAEtE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,sEAAsE;IACtE,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,qFAAqF;IACrF,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,sEAAsE;IACtE,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACzC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gEAAgE;AAChE,qBAAa,cAAe,SAAQ,KAAK;IAGrC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;CAK1B;AAMD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,qBAAqB,EAC7B,SAAS,GAAE,OAAO,KAAa,GAC9B,eAAe,CA6CjB"}
package/dist/client.js CHANGED
@@ -11,27 +11,56 @@ function ensureTrailingSlash(url) {
11
11
  return url.endsWith("/") ? url : `${url}/`;
12
12
  }
13
13
  /**
14
- * Build a read-only collector client bound to one `(collectorUrl, apiKey)` pair.
14
+ * Build a collector client bound to one `(collectorUrl, apiKey)` pair.
15
15
  * `fetchImpl` is injectable for testing; it defaults to the global `fetch`.
16
+ *
17
+ * The write methods are the narrow metadata path of #310 and nothing else:
18
+ * whether they are allowed at all is decided by the collector from the key's
19
+ * capabilities (`403` without `annotate`), not here.
16
20
  */
17
21
  export function createCollectorClient(config, fetchImpl = fetch) {
18
22
  const base = ensureTrailingSlash(config.collectorUrl);
23
+ const resolve = (path) => new URL(path.replace(/^\//, ""), base);
24
+ /** Send a request and turn a non-2xx response into a {@link CollectorError}. */
25
+ async function send(url, init) {
26
+ const res = await fetchImpl(url, {
27
+ ...init,
28
+ headers: { "x-api-key": config.apiKey, ...init.headers },
29
+ });
30
+ if (!res.ok) {
31
+ const body = await res.text().catch(() => "");
32
+ throw new CollectorError(body || res.statusText, res.status);
33
+ }
34
+ // A successful delete answers `204 No Content`, which has no body to parse.
35
+ if (res.status === 204)
36
+ return null;
37
+ return res.json();
38
+ }
19
39
  return {
20
40
  async get(path, params = {}) {
21
- const url = new URL(path.replace(/^\//, ""), base);
41
+ const url = resolve(path);
22
42
  for (const [key, value] of Object.entries(params)) {
23
43
  if (value != null)
24
44
  url.searchParams.set(key, String(value));
25
45
  }
26
- const res = await fetchImpl(url, {
27
- method: "GET",
28
- headers: { "x-api-key": config.apiKey },
46
+ return send(url, { method: "GET" });
47
+ },
48
+ async post(path, body) {
49
+ return send(resolve(path), {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json" },
52
+ body: JSON.stringify(body),
29
53
  });
30
- if (!res.ok) {
31
- const body = await res.text().catch(() => "");
32
- throw new CollectorError(body || res.statusText, res.status);
33
- }
34
- return res.json();
54
+ },
55
+ async put(path, body) {
56
+ return send(resolve(path), {
57
+ method: "PUT",
58
+ headers: { "content-type": "application/json" },
59
+ body: JSON.stringify(body),
60
+ });
61
+ },
62
+ async delete(path) {
63
+ return send(resolve(path), { method: "DELETE" });
35
64
  },
36
65
  };
37
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAyBA,gEAAgE;AAChE,MAAM,OAAO,cAAe,SAAQ,KAAK;IAG5B;IAFX,YACE,OAAe,EACN,MAAc;QAEvB,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,WAAM,GAAN,MAAM,CAAQ;QAGvB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED,SAAS,mBAAmB,CAAC,GAAW;IACtC,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAA6B,EAC7B,YAA0B,KAAK;IAE/B,MAAM,IAAI,GAAG,mBAAmB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACtD,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,IAAY,EAAE,SAAsB,EAAE;YAC9C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;YACnD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClD,IAAI,KAAK,IAAI,IAAI;oBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC9D,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;gBAC/B,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;aACxC,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;gBAC9C,MAAM,IAAI,cAAc,CAAC,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;YAC/D,CAAC;YACD,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;QACpB,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAsCA,gEAAgE;AAChE,MAAM,OAAO,cAAe,SAAQ,KAAK;IAG5B;IAFX,YACE,OAAe,EACN,MAAc;QAEvB,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,WAAM,GAAN,MAAM,CAAQ;QAGvB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED,SAAS,mBAAmB,CAAC,GAAW;IACtC,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC;AAC7C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAA6B,EAC7B,YAA0B,KAAK;IAE/B,MAAM,IAAI,GAAG,mBAAmB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,CAAC,IAAY,EAAO,EAAE,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IAE9E,gFAAgF;IAChF,KAAK,UAAU,IAAI,CAAC,GAAQ,EAAE,IAAiB;QAC7C,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;YAC/B,GAAG,IAAI;YACP,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAI,IAAI,CAAC,OAAkC,EAAE;SACrF,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9C,MAAM,IAAI,cAAc,CAAC,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/D,CAAC;QACD,4EAA4E;QAC5E,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACpC,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,IAAY,EAAE,SAAsB,EAAE;YAC9C,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClD,IAAI,KAAK,IAAI,IAAI;oBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC9D,CAAC;YACD,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACtC,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,IAAa;YACpC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;gBACzB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,IAAY,EAAE,IAAa;YACnC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;gBACzB,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,IAAY;YACvB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QACnD,CAAC;KACF,CAAC;AACJ,CAAC"}