@adonis-agora/telescope 0.8.4 → 0.9.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.
@@ -0,0 +1,280 @@
1
+ ---
2
+ name: telescope-access-mcp
3
+ description: >-
4
+ Dashboard access control and the MCP endpoint in @adonis-agora/telescope —
5
+ config/telescope_ui.ts authorize(ctx) hook vs credentials { token, basic },
6
+ dashboardAuth { secret, ttl, login } login screen, defaultAuthorize (allow outside
7
+ production, deny in production), 401-vs-403 fail-closed guard semantics,
8
+ redirect-wins behaviour, replay/queueActions/cpuProfiling.armEnabled mutation
9
+ gates, and config/telescope_mcp.ts JSON-RPC tools behind the same guard. Use for
10
+ "lock down the dashboard", "dashboard open in production", "401 on /telescope",
11
+ "login wall", "expose telescope to Claude Code", "replay returns 403".
12
+ license: MIT
13
+ metadata:
14
+ type: core
15
+ library: "@adonis-agora/telescope"
16
+ library_version: "0.8.4"
17
+ framework: adonisjs
18
+ sources:
19
+ - "DavideCarvalho/adonis-telescope:docs/dashboard/auth.mdx"
20
+ - "DavideCarvalho/adonis-telescope:docs/packages/mcp.mdx"
21
+ - "DavideCarvalho/adonis-telescope:docs/packages/ui.mdx"
22
+ ---
23
+
24
+ # Dashboard access control & MCP
25
+
26
+ Every route serving recorded data runs an `authorize` guard first — every
27
+ `<path>/api/*` endpoint, the SSE stream, metrics and the SPA routes. Out of the box
28
+ the policy is **allow outside production, deny in production** until a credential or
29
+ hook permits it. The MCP subpath reuses exactly the same guard.
30
+
31
+ ## Setup
32
+
33
+ The simplest production gate is a built-in credential; either `token` or `basic`,
34
+ or both:
35
+
36
+ ```ts
37
+ // config/telescope_ui.ts
38
+ import env from '#start/env'
39
+ import { defineConfig } from '@adonis-agora/telescope/ui'
40
+
41
+ export default defineConfig({
42
+ enabled: true,
43
+ path: '/telescope',
44
+ credentials: { token: env.get('TELESCOPE_UI_TOKEN') },
45
+ })
46
+ ```
47
+
48
+ ```sh
49
+ curl -H 'Authorization: Bearer s3cr3t' http://localhost:3333/telescope/api/entries
50
+ open 'http://localhost:3333/telescope?token=s3cr3t' # ?token= is convenient but leaky
51
+ ```
52
+
53
+ Source: `docs/dashboard/auth.mdx` (Built-in credentials).
54
+
55
+ ## Core patterns
56
+
57
+ ### Pattern 1 — delegate to your app's auth
58
+
59
+ When you set `authorize`, the built-in `credentials` gate is ignored — your hook IS
60
+ the whole policy. It receives your **real** `HttpContext` through a framework-light
61
+ declared type, so narrow it to reach `auth`/`session` at runtime.
62
+
63
+ ```ts
64
+ // config/telescope_ui.ts
65
+ export default defineConfig({
66
+ async authorize(ctx) {
67
+ const { auth } = ctx as unknown as {
68
+ auth: { check(): Promise<boolean>; user?: { role?: string } }
69
+ }
70
+ await auth.check()
71
+ return auth.user?.role === 'admin'
72
+ },
73
+ })
74
+ ```
75
+
76
+ Guard outcomes: denied with no credential → `401` (+ `WWW-Authenticate`); denied
77
+ with a rejected credential → `403`; a throwing hook → `403` (fail-closed).
78
+
79
+ Source: `docs/dashboard/auth.mdx` (Delegate to your app's auth, Guard behaviour).
80
+
81
+ ### Pattern 2 — a real login wall with dashboardAuth
82
+
83
+ For shared human access: a server-rendered login page plus a signed session cookie.
84
+ It composes WITH `authorize` (both must pass). Missing `secret` or `login` throws at
85
+ boot rather than shipping an un-mintable gate.
86
+
87
+ ```ts
88
+ // config/telescope_ui.ts
89
+ import env from '#start/env'
90
+ import User from '#models/user'
91
+ import hash from '@adonisjs/core/services/hash'
92
+ import { defineConfig } from '@adonis-agora/telescope/ui'
93
+
94
+ export default defineConfig({
95
+ dashboardAuth: {
96
+ secret: env.get('TELESCOPE_DASHBOARD_SECRET'), // HMAC-SHA256 key, 32+ bytes — required
97
+ ttl: '8h',
98
+ async login(username, password) {
99
+ const user = await User.findBy('email', username)
100
+ if (!user || !(await hash.verify(user.password, password))) return null
101
+ if (!user.isAdmin) return null
102
+ return { id: String(user.id), name: user.fullName } // session user, or null to deny
103
+ },
104
+ },
105
+ })
106
+ ```
107
+
108
+ This mounts unguarded-by-necessity `GET/POST <path>/login` and `GET <path>/logout`
109
+ routes (they mint/clear the session only — no recorded data sits behind them).
110
+ An unauthenticated page navigation gets a `302` to the login; an unauthenticated API
111
+ call gets a plain `401`.
112
+
113
+ Source: `docs/dashboard/auth.mdx` (Built-in login screen).
114
+
115
+ ### Pattern 3 — redirect instead of answering JSON
116
+
117
+ If the hook sets a `location` header and then returns `false`, the guard leaves the
118
+ redirect alone instead of overwriting it with a `403` body.
119
+
120
+ ```ts
121
+ authorize: (ctx) => {
122
+ const { auth } = ctx as unknown as { auth: { user?: { isAdmin?: boolean } } }
123
+ if (auth.user?.isAdmin === true) return true
124
+ ctx.response.redirect('/login?next=/telescope')
125
+ return false // redirect wins only when the hook returns false
126
+ }
127
+ ```
128
+
129
+ Source: `docs/dashboard/auth.mdx` (Redirecting instead of answering JSON).
130
+
131
+ ### Pattern 4 — expose the store to coding agents over MCP
132
+
133
+ ```ts
134
+ // config/telescope_mcp.ts
135
+ import env from '#start/env'
136
+ import { defineConfig } from '@adonis-agora/telescope/mcp'
137
+
138
+ export default defineConfig({
139
+ enabled: true,
140
+ path: '/telescope/mcp',
141
+ credentials: { token: env.get('TELESCOPE_MCP_TOKEN') }, // same guard as the dashboard
142
+ // tools: ['list_entries', 'get_trace', 'get_health'], // hand agents a narrower surface
143
+ })
144
+ ```
145
+
146
+ ```sh
147
+ claude mcp add --transport http telescope https://your-app.example.com/telescope/mcp \
148
+ --header 'Authorization: Bearer <TELESCOPE_MCP_TOKEN>'
149
+ ```
150
+
151
+ Six read tools ship: `list_entries`, `get_entry`, `get_trace`, `get_waterfall`,
152
+ `get_health`, `diagnose_exception`. Redaction and sampling are never bypassed — an
153
+ agent sees exactly what the dashboard sees.
154
+
155
+ Source: `docs/packages/mcp.mdx`.
156
+
157
+ ### Pattern 5 — gate the dangerous mutations
158
+
159
+ Replay, CPU-arm and queue retry/enqueue are each disabled by default and answer
160
+ `403` until their ui-config gate is flipped — even when the underlying feature is
161
+ enabled.
162
+
163
+ ```ts
164
+ export default defineConfig({
165
+ replay: { enabled: true }, // POST <path>/api/requests/:id/replay — re-runs a real request!
166
+ cpuProfiling: { armEnabled: true }, // POST <path>/api/profiles/arm
167
+ queueActions: { enabled: true }, // queue console retry/enqueue
168
+ })
169
+ ```
170
+
171
+ Source: `docs/packages/ui.mdx` (Configuration, Request replay),
172
+ `docs/packages/cpu-profiling.mdx` (arming callout).
173
+
174
+ ## Common mistakes
175
+
176
+ ### CRITICAL Relying on an obscure path instead of the guard
177
+
178
+ ```ts
179
+ // Wrong — renaming the prefix is defense in depth, not authentication:
180
+ defineConfig({ path: '/__telescope' }) // production: denied anyway without a credential/hook
181
+ ```
182
+
183
+ ```ts
184
+ // Correct — make a deliberate choice for production:
185
+ defineConfig({
186
+ path: '/__telescope',
187
+ credentials: { token: env.get('TELESCOPE_UI_TOKEN') },
188
+ })
189
+ ```
190
+
191
+ Mechanism: the default policy denies production until a credential or `authorize`
192
+ hook permits it — but that deny is the *default*, never a guarantee you can skip
193
+ configuring; security notes call this out explicitly.
194
+ Source: `docs/dashboard/auth.mdx` (default policy warn + Security notes).
195
+
196
+ ### MEDIUM Setting credentials AND an authorize hook expecting both to run
197
+
198
+ ```ts
199
+ // Wrong — the token gate silently stops mattering once authorize is set.
200
+ defineConfig({
201
+ credentials: { token: env.get('TELESCOPE_UI_TOKEN') },
202
+ authorize: () => true, // now the whole policy; credentials ignored
203
+ })
204
+ ```
205
+
206
+ ```ts
207
+ // Correct — pick one: credentials for simple gates, authorize for real policies.
208
+ defineConfig({ credentials: { basic: { username: 'admin', password: env.get('TELESCOPE_UI_PASSWORD') } } })
209
+ ```
210
+
211
+ Mechanism: the docs state the built-in `credentials` gate is ignored whenever
212
+ `authorize` is set — your hook replaces it entirely (`dashboardAuth`, by contrast,
213
+ composes with `authorize`).
214
+ Source: `docs/dashboard/auth.mdx` (Delegate to your app's auth).
215
+
216
+ ### MEDIUM Reaching for ctx.auth without narrowing the context
217
+
218
+ ```ts
219
+ // Wrong — does not type-check: the declared parameter is a minimal HTTP slice.
220
+ authorize: (ctx) => ctx.auth.user?.isAdmin === true
221
+ ```
222
+
223
+ ```ts
224
+ // Correct — narrow to the slice you rely on; the REAL HttpContext passes through.
225
+ authorize: (ctx) => {
226
+ const { auth } = ctx as unknown as { auth: { user?: { isAdmin?: boolean } } }
227
+ return auth.user?.isAdmin === true
228
+ }
229
+ ```
230
+
231
+ Mechanism: the guard's declared shape is framework-light (`request.qs()`,
232
+ `request.header()`, `response`) so any server can satisfy it; `auth`/`session`
233
+ exist at runtime only after narrowing.
234
+ Source: `docs/dashboard/auth.mdx` (narrowing info callout).
235
+
236
+ ### HIGH Exposing MCP in production without a token credential
237
+
238
+ ```ts
239
+ // Wrong — relies on the dev-open default; prod denials surface as opaque JSON-RPC errors.
240
+ defineConfig({ enabled: true, path: '/telescope/mcp' })
241
+ ```
242
+
243
+ ```ts
244
+ // Correct — a token/basic credential for production.
245
+ defineConfig({
246
+ path: '/telescope/mcp',
247
+ credentials: { token: env.get('TELESCOPE_MCP_TOKEN') },
248
+ })
249
+ ```
250
+
251
+ Mechanism: MCP has no transport-level 401 — denial rides inside the JSON-RPC
252
+ envelope as `-32001` (`MCP_UNAUTHORIZED`), which compliant clients just report as a
253
+ failed call.
254
+ Source: `docs/packages/mcp.mdx` (guard info callout).
255
+
256
+ ### MEDIUM Configuring dashboardAuth without secret or login
257
+
258
+ ```ts
259
+ // Wrong — both keys are required; this throws at boot.
260
+ defineConfig({ dashboardAuth: { secret: env.get('TELESCOPE_DASHBOARD_SECRET') } })
261
+ defineConfig({ dashboardAuth: { login: verifyCreds } })
262
+ ```
263
+
264
+ ```ts
265
+ // Correct — non-empty secret AND a login hook.
266
+ defineConfig({
267
+ dashboardAuth: {
268
+ secret: env.get('TELESCOPE_DASHBOARD_SECRET'),
269
+ ttl: '8h',
270
+ login: verifyCreds,
271
+ },
272
+ })
273
+ ```
274
+
275
+ Mechanism: the cookie can never be minted without a signing key and a validator, so
276
+ the config fails closed at startup rather than shipping an un-loginable wall.
277
+ Source: `docs/dashboard/auth.mdx` (Built-in login screen table + warn callout).
278
+
279
+ See also: `telescope-ui-dashboard/SKILL.md` — the SPA behind these gates;
280
+ `telescope-alerts-ai/SKILL.md` — the diagnose route this guard covers.
@@ -0,0 +1,290 @@
1
+ ---
2
+ name: telescope-alerts-ai
3
+ description: >-
4
+ Alerting, AI diagnosis and client-error ingestion in @adonis-agora/telescope —
5
+ config/telescope_alerts.ts with AlertRule types (new-exception, every-exception,
6
+ exception-rate, stateful metric-threshold), channel helpers (slackChannel,
7
+ webhookChannel, consoleChannel, customChannel), every/cooldown/instanceId/
8
+ geoLookup; config/telescope_ai.ts + TelescopeAiDiagnoser Claude diagnosis cached
9
+ by familyHash (keyless = disabled); clientErrors sendBeacon endpoint. Use for
10
+ "Slack alert on new errors", "AI diagnose exceptions", "page on every error",
11
+ "report browser errors", "why didn't my alert fire".
12
+ license: MIT
13
+ metadata:
14
+ type: core
15
+ library: "@adonis-agora/telescope"
16
+ library_version: "0.8.4"
17
+ framework: adonisjs
18
+ sources:
19
+ - "DavideCarvalho/adonis-telescope:docs/packages/alerts.mdx"
20
+ - "DavideCarvalho/adonis-telescope:docs/packages/ai.mdx"
21
+ - "DavideCarvalho/adonis-telescope:docs/packages/client-errors.mdx"
22
+ - "DavideCarvalho/adonis-telescope:packages/core/src/alerts/index.ts"
23
+ ---
24
+
25
+ # Alerting, AI diagnosis & client errors
26
+
27
+ The alerter polls the store on an interval, evaluates rules over recorded
28
+ `exception` entries, and fans each fire out to every configured channel. The AI
29
+ subpath attaches a Claude root-cause diagnosis to an exception, cached by family.
30
+ Client-error ingestion lets browsers report their own failures into the same
31
+ pipeline. All three are subpaths of the one package — enable them via
32
+ `node ace configure @adonis-agora/telescope` (pick **Alerts** / **AI**).
33
+
34
+ ## Setup
35
+
36
+ ```ts
37
+ // config/telescope_alerts.ts
38
+ import env from '#start/env'
39
+ import { defineConfig } from '@adonis-agora/telescope/alerts'
40
+
41
+ export default defineConfig({
42
+ channels: [{ type: 'slack', url: env.get('TELESCOPE_SLACK_WEBHOOK') }],
43
+ rules: [{ type: 'new-exception', window: '1h' }],
44
+ dashboardUrl: 'https://telescope.example.com/', // enables Slack deep links
45
+ every: '30s', // poll cadence — alerts are as late as the interval
46
+ cooldown: '15m', // per-rule / per-family re-notify suppression
47
+ })
48
+ ```
49
+
50
+ ```ts
51
+ // config/telescope_ai.ts
52
+ import env from '#start/env'
53
+ import { defineConfig } from '@adonis-agora/telescope/ai'
54
+
55
+ export default defineConfig({
56
+ apiKey: env.get('ANTHROPIC_API_KEY'), // validate as Env.schema.string.optional()
57
+ model: 'claude-sonnet-4-6', // default; claude-opus-4-8 / claude-haiku-4-5-20251001
58
+ maxTokens: 1024,
59
+ })
60
+ ```
61
+
62
+ Durations are validated at boot — an unparseable `every`/`cooldown`/`window`
63
+ (`'15min'`, a typo) **throws**, fail-closed. Valid units: `ms`, `s`, `m`, `h`, `d`.
64
+
65
+ Source: `docs/packages/alerts.mdx` (Configuration),
66
+ `docs/packages/ai.mdx` (Configuration).
67
+
68
+ ## Core patterns
69
+
70
+ ### Pattern 1 — pick rules deliberately
71
+
72
+ ```ts
73
+ rules: [
74
+ { type: 'new-exception', window: '1h' }, // first sighting of a family (+ recurrences after it)
75
+ { type: 'every-exception', window: '15m' },// EVERY server + browser error, cooldown-limited per family;
76
+ // window is display-only (labels the occurrence count)
77
+ { type: 'exception-rate', window: '5m', threshold: 10 },
78
+ {
79
+ type: 'metric-threshold', // stateful raise/auto-resolve, evaluated on every poll
80
+ metric: 'request-p99-ms', // or query-p95-ms, cache-hit-rate, exception-count, ...
81
+ window: '5m',
82
+ comparator: 'gte',
83
+ threshold: 800,
84
+ minSamples: 10, // guard percentiles against a single slow request
85
+ },
86
+ ]
87
+ ```
88
+
89
+ `new-exception` dedup is a bounded per-process map — in a multi-replica deploy the
90
+ same family reports "new" once per replica, so budget `cooldown` accordingly.
91
+
92
+ Source: `docs/packages/alerts.mdx` (Rules, Deduplication).
93
+
94
+ ### Pattern 2 — diagnose an exception with its trace context
95
+
96
+ `TelescopeAiDiagnoser` is always resolvable from the container — even when disabled,
97
+ when `diagnose` resolves `null` — so you can inject it without a null check.
98
+
99
+ ```ts
100
+ // app/controllers/diagnose_controller.ts
101
+ import { inject } from '@adonisjs/core'
102
+ import type { HttpContext } from '@adonisjs/core/http'
103
+ import { TelescopeService } from '@adonis-agora/telescope'
104
+ import { TelescopeAiDiagnoser } from '@adonis-agora/telescope/ai'
105
+
106
+ @inject()
107
+ export default class DiagnoseController {
108
+ constructor(
109
+ private telescope: TelescopeService,
110
+ private diagnoser: TelescopeAiDiagnoser,
111
+ ) {}
112
+
113
+ async show({ params, response }: HttpContext) {
114
+ const entry = await this.telescope.find(params.id)
115
+ if (!entry || entry.type !== 'exception') {
116
+ return response.notFound({ error: 'Not an exception entry' })
117
+ }
118
+ const related = entry.traceId ? await this.telescope.byTrace(entry.traceId) : []
119
+ const diagnosis = await this.diagnoser.diagnose(entry, { related })
120
+ // → { cause, fix, confidence, model, cached } | null — never throws
121
+ return response.json(diagnosis)
122
+ }
123
+ }
124
+ ```
125
+
126
+ Diagnoses cache per `familyHash` (bounded LRU, 24h TTL); pass `{ force: true }` to
127
+ spend a fresh call after changing code.
128
+
129
+ Source: `docs/packages/ai.mdx` (Use it), `docs/recipes/ai-exception-diagnosis.mdx`.
130
+
131
+ ### Pattern 3 — a custom channel sink
132
+
133
+ Channels fan out concurrently; one failing never blocks the others.
134
+
135
+ ```ts
136
+ import { defineConfig, customChannel, slackChannel } from '@adonis-agora/telescope/alerts'
137
+ import env from '#start/env'
138
+
139
+ export default defineConfig({
140
+ channels: [
141
+ slackChannel(env.get('TELESCOPE_SLACK_WEBHOOK'), { username: 'Telescope' }),
142
+ customChannel(async (alert) => { await pagerduty.trigger(alert) }, 'pagerduty'),
143
+ ],
144
+ rules: [{ type: 'new-exception', window: '1h' }],
145
+ })
146
+ ```
147
+
148
+ Exception alerts carry `{ cause, fix, confidence, model }` when AI is configured —
149
+ a guarded hook, so a slow model never delays or breaks the page.
150
+
151
+ Source: `docs/packages/alerts.mdx` (Channels).
152
+
153
+ ### Pattern 4 — ingest browser errors
154
+
155
+ Disabled by default; the route only exists once you enable it. Report with
156
+ `navigator.sendBeacon`; entries become `client_exception` records through the same
157
+ redaction/sampling/prune pipeline as server exceptions.
158
+
159
+ ```ts
160
+ // config/telescope.ts
161
+ export default defineConfig({
162
+ clientErrors: {
163
+ enabled: true,
164
+ path: '/telescope/client-errors',
165
+ maxBodyBytes: 32_768, // larger bodies rejected 413 before validation
166
+ rateLimit: { perMinute: 60 }, // per-IP token bucket; over → 429
167
+ },
168
+ })
169
+ ```
170
+
171
+ ```ts
172
+ // Browser reporter
173
+ window.addEventListener('error', (event) => {
174
+ navigator.sendBeacon(
175
+ '/telescope/client-errors',
176
+ JSON.stringify({
177
+ message: event.message,
178
+ name: event.error?.name ?? null,
179
+ stack: event.error?.stack ?? null,
180
+ url: location.href,
181
+ userAgent: navigator.userAgent,
182
+ }),
183
+ )
184
+ })
185
+ ```
186
+
187
+ Only `message` is required; `clientIp` is filled server-side, never trusted from
188
+ the body.
189
+
190
+ Source: `docs/packages/client-errors.mdx`.
191
+
192
+ ## Common mistakes
193
+
194
+ ### HIGH Listing two rules of the same exception type
195
+
196
+ ```ts
197
+ // Wrong — the second new-exception rule is SILENTLY ignored.
198
+ rules: [
199
+ { type: 'new-exception', window: '1h' },
200
+ { type: 'new-exception', window: '24h' },
201
+ ]
202
+ ```
203
+
204
+ ```ts
205
+ // Correct — at most one of each exception rule; express variations via window/threshold.
206
+ rules: [
207
+ { type: 'new-exception', window: '1h' },
208
+ { type: 'exception-rate', window: '5m', threshold: 10 },
209
+ ]
210
+ ```
211
+
212
+ Mechanism: `new-exception`, `every-exception` and `exception-rate` are looked up
213
+ once by type; duplicates never evaluate (`metric-threshold` is exempt).
214
+ Source: `docs/packages/alerts.mdx` (When rules do and don't run).
215
+
216
+ ### HIGH Shipping AI config without a resolvable key
217
+
218
+ ```ts
219
+ // Wrong — keyless install looks configured but silently does nothing:
220
+ export default defineConfig({ model: 'claude-opus-4-8' }) // no apiKey, no client
221
+ const diagnosis = await diagnoser.diagnose(entry) // always null
222
+ ```
223
+
224
+ ```ts
225
+ // Correct — source the key from validated env; check isConfigured() when unsure.
226
+ export default defineConfig({ apiKey: env.get('ANTHROPIC_API_KEY') })
227
+ ```
228
+
229
+ Mechanism: a configured-but-keyless install is treated as disabled — `diagnose`
230
+ resolves `null` forever and the dashboard hides the Diagnose button, with no error
231
+ anywhere.
232
+ Source: `docs/packages/ai.mdx` (warn callout), `docs/reference/configuration.mdx` (AI).
233
+
234
+ ### MEDIUM Expecting sampled-out exceptions to page
235
+
236
+ ```ts
237
+ // Wrong — sampling drops most queries AND some exceptions before any rule sees them.
238
+ sampling: { default: 0.1 },
239
+ rules: [{ type: 'new-exception', window: '1h' }],
240
+ ```
241
+
242
+ ```ts
243
+ // Correct — tail-sampling keeps errors/slow regardless of rate.
244
+ sampling: { default: 0.1, exception: { rate: 0.1, keepErrors: true } },
245
+ ```
246
+
247
+ Mechanism: only what was recorded is alertable — sampling and the overload shed run
248
+ before the alerter's poll, and polling itself adds up to `every` of latency.
249
+ Source: `docs/packages/alerts.mdx` (How it hooks in).
250
+
251
+ ### MEDIUM Short cooldown with every-exception
252
+
253
+ ```ts
254
+ // Wrong — a tight loop of the same error re-pages once per cooldown... which is 1 minute.
255
+ rules: [{ type: 'every-exception' }],
256
+ cooldown: '1m',
257
+ ```
258
+
259
+ ```ts
260
+ // Correct — tune cooldown to your on-call tolerance; different families still page immediately.
261
+ rules: [{ type: 'every-exception', window: '15m' }],
262
+ cooldown: '10m',
263
+ ```
264
+
265
+ Mechanism: cooldown is the *only* thing rate-limiting repeats per family (on its own
266
+ independent clock from `new-exception`), so a short one on a noisy family is a
267
+ self-inflicted alert storm.
268
+ Source: `docs/packages/alerts.mdx` (cooldown info callout).
269
+
270
+ ### MEDIUM Probing for the client-error endpoint while it is disabled
271
+
272
+ ```sh
273
+ # Wrong — expecting a 404 that proves wiring:
274
+ curl -X POST http://localhost:3333/telescope/client-errors
275
+ # → generic route-not-found; can't tell whether telescope owns this prefix
276
+ ```
277
+
278
+ ```ts
279
+ // Correct — enable it, then verify the 204:
280
+ clientErrors: { enabled: true }
281
+ // curl -X POST ... → 204 No Content once registered
282
+ ```
283
+
284
+ Mechanism: unlike an always-mounted controller, the provider registers the POST
285
+ route only when `clientErrors.enabled` is true — while off the endpoint genuinely
286
+ does not exist.
287
+ Source: `docs/packages/client-errors.mdx` (Disabled by default warn callout).
288
+
289
+ See also: `telescope-watchers/SKILL.md` — capture gaps become alert gaps;
290
+ `telescope-access-mcp/SKILL.md` — the diagnose endpoint rides behind the dashboard guard.