@agentrysh/mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js ADDED
@@ -0,0 +1,1906 @@
1
+ // MCP tool definitions + dispatch.
2
+ // Each tool's response is shaped to give the calling agent enough context to
3
+ // choose its next action without re-asking the user.
4
+ import { parseDsn } from "@agentrysh/shared";
5
+ import { api } from "./api.js";
6
+ import { loadConfig, saveConfig } from "./config.js";
7
+ import { getOnboardingHint } from "./onboarding.js";
8
+ import { getMemoryPath, readCaseSection, upsertCaseSection, MEMORY_FILENAME, } from "./memory.js";
9
+ import * as fs from "node:fs";
10
+ export const TOOL_DESCRIPTORS = [
11
+ {
12
+ name: "agentry_status",
13
+ description: "Show what's set up locally and what to do next. Always safe to call. " +
14
+ "Use this first if you don't know whether the user has signed up or has a project.",
15
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
16
+ },
17
+ {
18
+ name: "agentry_login",
19
+ description: "Authenticate the user via GitHub device flow. " +
20
+ "Starts the flow, shows the user a verification URL and a short code, then polls until they authorize. " +
21
+ "Returns an API key and stores it locally. " +
22
+ "By default the tool blocks until success or timeout; " +
23
+ "use `mode: 'start_only'` to return the verification details without polling, or `mode: 'poll_once'` with `device_code` to do a single non-blocking poll.",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {
27
+ mode: {
28
+ type: "string",
29
+ enum: ["full", "start_only", "poll_once"],
30
+ description: "'full' (default) starts and polls until done. " +
31
+ "'start_only' returns the verification URL + code + device_code immediately. " +
32
+ "'poll_once' takes device_code and does a single poll.",
33
+ },
34
+ device_code: {
35
+ type: "string",
36
+ description: "Required when mode='poll_once'.",
37
+ },
38
+ timeout_seconds: {
39
+ type: "number",
40
+ description: "Cap on total polling time when mode='full'. Defaults to 180.",
41
+ },
42
+ },
43
+ additionalProperties: false,
44
+ },
45
+ },
46
+ {
47
+ name: "agentry_rotate_key",
48
+ description: "Rotate the current API key. The old key is revoked. The new one is stored locally.",
49
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
50
+ },
51
+ {
52
+ name: "agentry_list_projects",
53
+ description: "List all projects belonging to the authenticated user. " +
54
+ "Each entry is enriched with `local_path` from local config so the agent knows where to `cd`.",
55
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
56
+ },
57
+ {
58
+ name: "agentry_create_project",
59
+ description: "Create a new project. Returns the DSN and SDK install snippet ready to paste. " +
60
+ "Pass `local_path` (the absolute path to the repo on disk) so future cases route back to the right directory.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ name: { type: "string", description: "Project name (e.g. 'musicvideogen-prod')" },
65
+ repo_url: { type: "string", description: "Optional git URL (e.g. https://github.com/user/repo)" },
66
+ local_path: { type: "string", description: "Absolute filesystem path to the repo on disk" },
67
+ default_branch: { type: "string", description: "Default branch (defaults to 'main')" },
68
+ },
69
+ required: ["name"],
70
+ additionalProperties: false,
71
+ },
72
+ },
73
+ {
74
+ name: "agentry_install_sdk",
75
+ description: "Get the SDK install snippet for a language. Returns code + env vars the user should paste into their app.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ language: {
80
+ type: "string",
81
+ description: "Language target. Defaults to 'node' (the only supported option in v0).",
82
+ },
83
+ },
84
+ additionalProperties: false,
85
+ },
86
+ },
87
+ {
88
+ name: "agentry_list_cases",
89
+ description: "List error cases for a project. Defaults to the local default project + status='open'. " +
90
+ "Each entry is enriched with `local_path` so the agent knows where to `cd` to investigate.",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ project_id: {
95
+ type: "string",
96
+ description: "Project id. If omitted, uses the local default project.",
97
+ },
98
+ status: {
99
+ type: "string",
100
+ enum: ["open", "investigating", "resolved", "spurious", "ignored"],
101
+ description: "Filter by status. Defaults to 'open'.",
102
+ },
103
+ },
104
+ additionalProperties: false,
105
+ },
106
+ },
107
+ {
108
+ name: "agentry_get_case",
109
+ description: "Get full detail for a case — stack trace, deploy SHA, suppression hints, and `local_path`. " +
110
+ "Surface `next_actions` to the agent.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ case_id: { type: "string", description: "Case id" },
115
+ },
116
+ required: ["case_id"],
117
+ additionalProperties: false,
118
+ },
119
+ },
120
+ {
121
+ name: "agentry_resolve_case",
122
+ description: "Mark a case as resolved. Pass an optional summary and PR url so the team has audit trail.",
123
+ inputSchema: {
124
+ type: "object",
125
+ properties: {
126
+ case_id: { type: "string" },
127
+ summary: { type: "string", description: "Short markdown summary of the fix" },
128
+ pr_url: { type: "string", description: "PR URL if you opened one" },
129
+ },
130
+ required: ["case_id"],
131
+ additionalProperties: false,
132
+ },
133
+ },
134
+ {
135
+ name: "agentry_mark_spurious",
136
+ description: "Mark a case as spurious (not a real bug). Optionally provide `suppress_pattern` and a reason; " +
137
+ "if set, also records a suppression rule so future matching events are auto-ignored.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ case_id: { type: "string" },
142
+ reason: { type: "string", description: "Why this is noise" },
143
+ suppress_pattern: {
144
+ type: "string",
145
+ description: "Optional substring pattern to match the fingerprint, auto-ignoring future matches",
146
+ },
147
+ },
148
+ required: ["case_id"],
149
+ additionalProperties: false,
150
+ },
151
+ },
152
+ {
153
+ name: "agentry_record_suppression",
154
+ description: "Record a noise-suppression rule for a project. Matches future events by fingerprint pattern. " +
155
+ "Actions: 'auto_ignore' (drop), 'auto_resolve' (mark resolved silently), 'prompt_hint' (attach hint to case).",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ project_id: {
160
+ type: "string",
161
+ description: "Project id. If omitted, uses the local default project.",
162
+ },
163
+ fingerprint_pattern: {
164
+ type: "string",
165
+ description: "Substring match against the case fingerprint",
166
+ },
167
+ action: {
168
+ type: "string",
169
+ enum: ["auto_ignore", "auto_resolve", "prompt_hint"],
170
+ },
171
+ reason: { type: "string" },
172
+ hint_text: {
173
+ type: "string",
174
+ description: "Required when action is 'prompt_hint' — the hint surfaced via agentry_get_case",
175
+ },
176
+ },
177
+ required: ["fingerprint_pattern", "action"],
178
+ additionalProperties: false,
179
+ },
180
+ },
181
+ {
182
+ name: "agentry_capture_test_event",
183
+ description: "Fire a synthetic Sentry-shaped event at the project's ingest endpoint to verify ingest works end-to-end. " +
184
+ "Returns the event id and (if the API surfaces it) the case id so you can immediately call agentry_get_case.",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: {
188
+ project_id: {
189
+ type: "string",
190
+ description: "Project id. If omitted, uses the local default project.",
191
+ },
192
+ },
193
+ additionalProperties: false,
194
+ },
195
+ },
196
+ {
197
+ name: "agentry_record_deploy",
198
+ description: "Record a deploy event. Useful when CI doesn't call the SDK directly. Cases ingested after this " +
199
+ "will surface the deploy in their `recent_deploys` so the agent can attribute regressions.",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: {
203
+ project_id: { type: "string" },
204
+ sha: { type: "string", description: "Git SHA of the deployed commit" },
205
+ branch: { type: "string" },
206
+ environment: { type: "string", description: "e.g. 'production', 'staging'" },
207
+ message: { type: "string", description: "Commit / deploy message" },
208
+ url: { type: "string", description: "Deploy or commit URL" },
209
+ actor: { type: "string", description: "Person or service that triggered the deploy" },
210
+ },
211
+ required: ["sha"],
212
+ additionalProperties: false,
213
+ },
214
+ },
215
+ {
216
+ name: "agentry_list_deploys",
217
+ description: "List recent deploys for a project. Useful for cross-referencing case timestamps with deploy timestamps.",
218
+ inputSchema: {
219
+ type: "object",
220
+ properties: {
221
+ project_id: { type: "string" },
222
+ limit: { type: "number", description: "Defaults to 20" },
223
+ since: { type: "number", description: "Unix seconds — only return deploys after this" },
224
+ },
225
+ additionalProperties: false,
226
+ },
227
+ },
228
+ {
229
+ name: "agentry_track_test_event",
230
+ description: "Fire a synthetic analytics event through the agentry track endpoint to verify the PostHog forwarding " +
231
+ "is wired up. Returns whether the forwarding succeeded.",
232
+ inputSchema: {
233
+ type: "object",
234
+ properties: {
235
+ project_id: { type: "string" },
236
+ event: {
237
+ type: "string",
238
+ description: "Event name. Defaults to 'agentry_verify'.",
239
+ },
240
+ },
241
+ additionalProperties: false,
242
+ },
243
+ },
244
+ {
245
+ name: "agentry_analytics_query",
246
+ description: "Run a HogQL query against the user's PostHog project. The agent uses this to investigate funnels, " +
247
+ "retention, paths, anomalies — anything PostHog can express in HogQL. " +
248
+ "Examples: SELECT count() FROM events WHERE event = 'signup_completed' AND timestamp > now() - INTERVAL 7 DAY",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ project_id: { type: "string" },
253
+ query: { type: "string", description: "HogQL query string" },
254
+ },
255
+ required: ["query"],
256
+ additionalProperties: false,
257
+ },
258
+ },
259
+ {
260
+ name: "agentry_install_guide",
261
+ description: "Get the comprehensive, framework-aware install checklist. Returns ordered steps with file hints, " +
262
+ "code snippets, and validation criteria. The agent should read this BEFORE editing any customer code.",
263
+ inputSchema: {
264
+ type: "object",
265
+ properties: {
266
+ framework: {
267
+ type: "string",
268
+ enum: ["node", "next", "express"],
269
+ description: "Detected from package.json. Defaults to 'node'.",
270
+ },
271
+ signal_types: {
272
+ type: "array",
273
+ items: { type: "string", enum: ["errors", "analytics", "deploys"] },
274
+ description: "Subset of signals to include. Defaults to all three.",
275
+ },
276
+ },
277
+ additionalProperties: false,
278
+ },
279
+ },
280
+ {
281
+ name: "agentry_verify_install",
282
+ description: "Comprehensive sanity check: fires a synthetic error, a synthetic analytics event, and (if requested) " +
283
+ "a synthetic deploy event, then reports which signal types reached agentry. Run this AFTER walking " +
284
+ "through agentry_install_guide. Errors that don't error and analytics that don't fire aren't useful — " +
285
+ "this is the only proof the install actually works.",
286
+ inputSchema: {
287
+ type: "object",
288
+ properties: {
289
+ project_id: { type: "string" },
290
+ skip: {
291
+ type: "array",
292
+ items: { type: "string", enum: ["errors", "analytics", "deploys"] },
293
+ description: "Signal types to skip (e.g. if the customer hasn't wired analytics yet)",
294
+ },
295
+ },
296
+ additionalProperties: false,
297
+ },
298
+ },
299
+ {
300
+ name: "agentry_list_recipes",
301
+ description: "List the canonical query recipes that answer common questions ('how many DAU?', " +
302
+ "'show me the funnel drop-off', 'errors after the last deploy?'). Each recipe has a " +
303
+ "HogQL/SQL template, parameters with defaults, expected columns, and a render_hint. " +
304
+ "Use this BEFORE composing ad-hoc HogQL — agentry has no dashboard, so the agent IS the dashboard.",
305
+ inputSchema: {
306
+ type: "object",
307
+ properties: {
308
+ category: {
309
+ type: "string",
310
+ enum: ["users", "retention", "funnels", "events", "errors", "deploys"],
311
+ description: "Optional category filter.",
312
+ },
313
+ },
314
+ additionalProperties: false,
315
+ },
316
+ },
317
+ {
318
+ name: "agentry_run_recipe",
319
+ description: "Run a recipe by id. Returns rows + a render_hint the agent uses to format the answer " +
320
+ "(markdown table / ASCII bar chart / funnel breakdown / scalar). " +
321
+ "If no recipe fits the user's question, fall back to `agentry_analytics_query` with hand-rolled HogQL.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ recipe_id: { type: "string", description: "Recipe id from agentry_list_recipes." },
326
+ project_id: { type: "string", description: "Defaults to the local default project." },
327
+ params: {
328
+ type: "object",
329
+ description: "Parameter values keyed by name. Defaults are applied for any omitted.",
330
+ additionalProperties: true,
331
+ },
332
+ },
333
+ required: ["recipe_id"],
334
+ additionalProperties: false,
335
+ },
336
+ },
337
+ {
338
+ name: "agentry_query_docs",
339
+ description: "Return markdown documentation of the queryable schema (analytics events table, errors, " +
340
+ "deploys) plus a HogQL primer plus visualization hints. Read this when the user's question " +
341
+ "doesn't match any recipe and the agent needs to compose ad-hoc HogQL.",
342
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
343
+ },
344
+ {
345
+ name: "agentry_project_health",
346
+ description: "Heartbeat / state-of-project view. Returns last_event_received_at, last_deploy_at, " +
347
+ "events_last_hour, open_cases count, usage_this_month with caps and percentages, and per-webhook " +
348
+ "last_status. Use this when the user asks 'is everything working?' or to detect ingest gaps " +
349
+ "('we shipped 2h ago and no events have come in — something broke').",
350
+ inputSchema: {
351
+ type: "object",
352
+ properties: { project_id: { type: "string" } },
353
+ additionalProperties: false,
354
+ },
355
+ },
356
+ {
357
+ name: "agentry_create_alert",
358
+ description: "Store an alert definition: a recipe + parameters + threshold + which webhook to fire. " +
359
+ "agentry doesn't run the schedule for you — your cron / GitHub Actions / Cloudflare Cron " +
360
+ "calls POST /alerts/:id/evaluate when you want the check run. On threshold cross, agentry " +
361
+ "fires the linked webhook so your endpoint reacts.",
362
+ inputSchema: {
363
+ type: "object",
364
+ properties: {
365
+ name: { type: "string" },
366
+ recipe_id: { type: "string", description: "Analytics-backend recipes only for v0." },
367
+ threshold_column: { type: "string" },
368
+ threshold_op: { type: "string", enum: ["gt", "gte", "lt", "lte", "eq"] },
369
+ threshold_value: { type: "number" },
370
+ params: { type: "object", additionalProperties: true },
371
+ description: { type: "string" },
372
+ webhook_id: { type: "string" },
373
+ project_id: { type: "string" },
374
+ },
375
+ required: ["name", "recipe_id", "threshold_column", "threshold_op", "threshold_value"],
376
+ additionalProperties: false,
377
+ },
378
+ },
379
+ {
380
+ name: "agentry_list_alerts",
381
+ description: "List configured alerts with last_evaluated_at / last_triggered_at / last_value.",
382
+ inputSchema: {
383
+ type: "object",
384
+ properties: { project_id: { type: "string" } },
385
+ additionalProperties: false,
386
+ },
387
+ },
388
+ {
389
+ name: "agentry_evaluate_alert",
390
+ description: "Run an alert's recipe NOW, compare against the threshold, fire the linked webhook if crossed. " +
391
+ "Returns {triggered, fired, current_value}. Call this from your scheduler.",
392
+ inputSchema: {
393
+ type: "object",
394
+ properties: {
395
+ alert_id: { type: "string" },
396
+ project_id: { type: "string" },
397
+ },
398
+ required: ["alert_id"],
399
+ additionalProperties: false,
400
+ },
401
+ },
402
+ {
403
+ name: "agentry_delete_alert",
404
+ description: "Remove an alert definition.",
405
+ inputSchema: {
406
+ type: "object",
407
+ properties: {
408
+ alert_id: { type: "string" },
409
+ project_id: { type: "string" },
410
+ },
411
+ required: ["alert_id"],
412
+ additionalProperties: false,
413
+ },
414
+ },
415
+ {
416
+ name: "agentry_remember",
417
+ description: "Append/update a markdown section about a case in the local agentry_memory.md file at the project's local_path. " +
418
+ "Use this when you've learned something investigating a case (root cause, suspect deploy, the fix you applied, " +
419
+ "what to watch out for). Future investigations of similar cases will see these notes via the agent's file-reading " +
420
+ "tools — this is the agent's persistent memory. Safe to commit. " +
421
+ "If the case_id section already exists, it's overwritten.",
422
+ inputSchema: {
423
+ type: "object",
424
+ properties: {
425
+ case_id: { type: "string" },
426
+ summary: {
427
+ type: "string",
428
+ description: "Markdown body — what was learned. Be specific: 'introduced by deploy abc123 which removed null guard from user.email; fixed in PR #91 by restoring guard + adding test'.",
429
+ },
430
+ fingerprint: { type: "string" },
431
+ status: { type: "string", description: "open / investigating / resolved / spurious" },
432
+ error_type: { type: "string" },
433
+ pr_url: { type: "string" },
434
+ watch_for: {
435
+ type: "string",
436
+ description: "Heuristic for spotting similar bugs in the future ('check notification_service.ts:142 for the same pattern').",
437
+ },
438
+ tags: { type: "array", items: { type: "string" } },
439
+ project_id: {
440
+ type: "string",
441
+ description: "Defaults to the local default project (the file lives in that project's local_path).",
442
+ },
443
+ },
444
+ required: ["case_id", "summary"],
445
+ additionalProperties: false,
446
+ },
447
+ },
448
+ {
449
+ name: "agentry_recall",
450
+ description: "Read the current contents of `<local_path>/agentry_memory.md`. " +
451
+ "Useful when investigating a new case to see if a similar one was handled before. " +
452
+ "You can also use the standard file-reading tools directly — this is just a convenience.",
453
+ inputSchema: {
454
+ type: "object",
455
+ properties: {
456
+ case_id: { type: "string", description: "If set, returns just this case's section." },
457
+ project_id: { type: "string" },
458
+ },
459
+ additionalProperties: false,
460
+ },
461
+ },
462
+ {
463
+ name: "agentry_register_webhook",
464
+ description: "Register a webhook URL to receive signed POSTs when interesting things happen. " +
465
+ "Events: 'case.created' (new error fingerprint), 'case.resolved' (case status flips to resolved), " +
466
+ "'deploy.recorded'. Returns the signing_secret ONCE — store it. The customer's endpoint must " +
467
+ "verify X-Agentry-Signature: t=<unix>,v1=<hex> using HMAC-SHA256(rawBody, signing_secret). " +
468
+ "This is the foundation for automation flows like auto-fix-on-error.",
469
+ inputSchema: {
470
+ type: "object",
471
+ properties: {
472
+ url: { type: "string", description: "https:// URL to receive deliveries" },
473
+ events: {
474
+ type: "array",
475
+ items: { type: "string", enum: ["case.created", "case.resolved", "deploy.recorded"] },
476
+ description: "Defaults to all three.",
477
+ },
478
+ description: { type: "string" },
479
+ project_id: { type: "string", description: "Defaults to local default project." },
480
+ },
481
+ required: ["url"],
482
+ additionalProperties: false,
483
+ },
484
+ },
485
+ {
486
+ name: "agentry_list_webhooks",
487
+ description: "List webhooks registered on a project, including last_status / last_error so the agent " +
488
+ "can tell if the customer's endpoint is healthy.",
489
+ inputSchema: {
490
+ type: "object",
491
+ properties: { project_id: { type: "string" } },
492
+ additionalProperties: false,
493
+ },
494
+ },
495
+ {
496
+ name: "agentry_test_webhook",
497
+ description: "Fire a synthetic test event to a registered webhook. Useful right after registration to " +
498
+ "confirm the signing secret + endpoint are wired up correctly.",
499
+ inputSchema: {
500
+ type: "object",
501
+ properties: {
502
+ webhook_id: { type: "string" },
503
+ project_id: { type: "string" },
504
+ },
505
+ required: ["webhook_id"],
506
+ additionalProperties: false,
507
+ },
508
+ },
509
+ {
510
+ name: "agentry_delete_webhook",
511
+ description: "Remove a webhook subscription. No further deliveries.",
512
+ inputSchema: {
513
+ type: "object",
514
+ properties: {
515
+ webhook_id: { type: "string" },
516
+ project_id: { type: "string" },
517
+ },
518
+ required: ["webhook_id"],
519
+ additionalProperties: false,
520
+ },
521
+ },
522
+ {
523
+ name: "agentry_automation_docs",
524
+ description: "Return markdown documentation showing common automation patterns built on agentry's webhooks: " +
525
+ "auto-fix-on-error, deploy regression alerts, weekly digests, etc. Each pattern includes a " +
526
+ "ready-to-deploy Cloudflare Worker / Vercel function template the agent can drop into the customer's repo.",
527
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
528
+ },
529
+ {
530
+ name: "agentry_suggested_next_steps",
531
+ description: "After install/verify, surface this to the user: a curated list of 'what would you like to do " +
532
+ "next?' prompts with paste-ready templates ('Build a customized analytics dashboard', " +
533
+ "'Build an error monitoring dashboard', 'Generate this week's review post', etc.). " +
534
+ "Each suggestion lists the recipes/tools the agent will use, so the response is predictable. " +
535
+ "State-aware: only suggestions whose prerequisites are met (analytics configured, errors present, " +
536
+ "deploys recorded) are returned.",
537
+ inputSchema: {
538
+ type: "object",
539
+ properties: {
540
+ project_id: { type: "string", description: "Defaults to the local default project." },
541
+ },
542
+ additionalProperties: false,
543
+ },
544
+ },
545
+ ];
546
+ // ---------------------------------------------------------------------------
547
+ // Helpers shared across tool handlers
548
+ // ---------------------------------------------------------------------------
549
+ function summarizeApiError(err) {
550
+ if (err && typeof err === "object" && "code" in err && "status" in err) {
551
+ const e = err;
552
+ return {
553
+ error: {
554
+ code: e.code,
555
+ message: e.message,
556
+ ...(e.next_action ? { next_action: e.next_action } : {}),
557
+ ...(e.details ? { details: e.details } : {}),
558
+ },
559
+ };
560
+ }
561
+ const msg = err instanceof Error ? err.message : String(err);
562
+ return {
563
+ error: {
564
+ code: "client_error",
565
+ message: msg,
566
+ next_action: "This was a local/MCP-side error, not an API error. Check `agentry_status` and try again.",
567
+ },
568
+ };
569
+ }
570
+ function pickProject(cfg, projectId) {
571
+ const id = projectId ?? cfg.default_project_id;
572
+ if (!id)
573
+ return null;
574
+ const project = cfg.projects[id] ?? null;
575
+ return { id, project };
576
+ }
577
+ function projectByLookup(cfg, projectId) {
578
+ return cfg.projects[projectId] ?? null;
579
+ }
580
+ function persistKeyResponse(cfg, resp) {
581
+ const next = {
582
+ ...cfg,
583
+ api_key: resp.api_key,
584
+ };
585
+ saveConfig(next);
586
+ return next;
587
+ }
588
+ function requireProjectFromConfig(cfg, projectId) {
589
+ const picked = pickProject(cfg, projectId);
590
+ if (!picked || !picked.project) {
591
+ return {
592
+ error: {
593
+ error: {
594
+ code: "no_project",
595
+ message: "No project found locally. Either create one with agentry_create_project or pass project_id explicitly " +
596
+ "(noting: this MCP only knows about projects it created locally — DSNs aren't fetched from the API).",
597
+ next_action: "Call agentry_create_project first.",
598
+ },
599
+ },
600
+ };
601
+ }
602
+ return { id: picked.id, project: picked.project };
603
+ }
604
+ // Build a Sentry-shaped synthetic event suitable for hitting /v1/logs/:id/.
605
+ function buildSyntheticEvent() {
606
+ const eventId = `agentrytest-${Date.now().toString(36)}`;
607
+ return {
608
+ event_id: eventId,
609
+ timestamp: Math.floor(Date.now() / 1000),
610
+ platform: "node",
611
+ level: "error",
612
+ environment: "agentry-mcp-test",
613
+ release: "agentry-mcp-test",
614
+ message: "Synthetic test event from agentry_capture_test_event",
615
+ exception: {
616
+ values: [
617
+ {
618
+ type: "AgentryTestError",
619
+ value: "This is a synthetic event triggered from the agentry MCP server.",
620
+ stacktrace: {
621
+ frames: [
622
+ {
623
+ filename: "agentry-mcp/src/tools.ts",
624
+ function: "agentry_capture_test_event",
625
+ lineno: 1,
626
+ in_app: true,
627
+ },
628
+ ],
629
+ },
630
+ },
631
+ ],
632
+ },
633
+ tags: { synthetic: "true" },
634
+ };
635
+ }
636
+ export async function dispatchTool(name, args) {
637
+ const a = args ?? {};
638
+ try {
639
+ switch (name) {
640
+ case "agentry_status":
641
+ return handleStatus();
642
+ case "agentry_login":
643
+ return await handleLogin({
644
+ mode: a.mode === "start_only" || a.mode === "poll_once"
645
+ ? a.mode
646
+ : "full",
647
+ device_code: a.device_code ? String(a.device_code) : undefined,
648
+ timeout_seconds: typeof a.timeout_seconds === "number" ? a.timeout_seconds : undefined,
649
+ });
650
+ case "agentry_rotate_key":
651
+ return await handleRotateKey();
652
+ case "agentry_list_projects":
653
+ return await handleListProjects();
654
+ case "agentry_create_project":
655
+ return await handleCreateProject({
656
+ name: String(a.name ?? ""),
657
+ repo_url: a.repo_url ? String(a.repo_url) : undefined,
658
+ local_path: a.local_path ? String(a.local_path) : undefined,
659
+ default_branch: a.default_branch ? String(a.default_branch) : undefined,
660
+ });
661
+ case "agentry_install_sdk":
662
+ return await handleInstallSdk(a.language ? String(a.language) : "node");
663
+ case "agentry_list_cases":
664
+ return await handleListCases({
665
+ project_id: a.project_id ? String(a.project_id) : undefined,
666
+ status: a.status ? String(a.status) : undefined,
667
+ });
668
+ case "agentry_get_case":
669
+ return await handleGetCase(String(a.case_id ?? ""));
670
+ case "agentry_resolve_case":
671
+ return await handleResolveCase({
672
+ case_id: String(a.case_id ?? ""),
673
+ summary: a.summary ? String(a.summary) : undefined,
674
+ pr_url: a.pr_url ? String(a.pr_url) : undefined,
675
+ });
676
+ case "agentry_mark_spurious":
677
+ return await handleMarkSpurious({
678
+ case_id: String(a.case_id ?? ""),
679
+ reason: a.reason ? String(a.reason) : undefined,
680
+ suppress_pattern: a.suppress_pattern ? String(a.suppress_pattern) : undefined,
681
+ });
682
+ case "agentry_record_suppression":
683
+ return await handleRecordSuppression({
684
+ project_id: a.project_id ? String(a.project_id) : undefined,
685
+ fingerprint_pattern: String(a.fingerprint_pattern ?? ""),
686
+ action: String(a.action ?? ""),
687
+ reason: a.reason ? String(a.reason) : undefined,
688
+ hint_text: a.hint_text ? String(a.hint_text) : undefined,
689
+ });
690
+ case "agentry_capture_test_event":
691
+ return await handleCaptureTestEvent(a.project_id ? String(a.project_id) : undefined);
692
+ case "agentry_record_deploy":
693
+ return await handleRecordDeploy({
694
+ project_id: a.project_id ? String(a.project_id) : undefined,
695
+ sha: String(a.sha ?? ""),
696
+ branch: a.branch ? String(a.branch) : undefined,
697
+ environment: a.environment ? String(a.environment) : undefined,
698
+ message: a.message ? String(a.message) : undefined,
699
+ url: a.url ? String(a.url) : undefined,
700
+ actor: a.actor ? String(a.actor) : undefined,
701
+ });
702
+ case "agentry_list_deploys":
703
+ return await handleListDeploys({
704
+ project_id: a.project_id ? String(a.project_id) : undefined,
705
+ limit: typeof a.limit === "number" ? a.limit : undefined,
706
+ since: typeof a.since === "number" ? a.since : undefined,
707
+ });
708
+ case "agentry_track_test_event":
709
+ return await handleTrackTestEvent({
710
+ project_id: a.project_id ? String(a.project_id) : undefined,
711
+ event: a.event ? String(a.event) : "agentry_verify",
712
+ });
713
+ case "agentry_analytics_query":
714
+ return await handleAnalyticsQuery({
715
+ project_id: a.project_id ? String(a.project_id) : undefined,
716
+ query: String(a.query ?? ""),
717
+ });
718
+ case "agentry_install_guide":
719
+ return await handleInstallGuide({
720
+ framework: a.framework ? String(a.framework) : "node",
721
+ signal_types: Array.isArray(a.signal_types)
722
+ ? a.signal_types.map(String)
723
+ : ["errors", "analytics", "deploys"],
724
+ });
725
+ case "agentry_verify_install":
726
+ return await handleVerifyInstall({
727
+ project_id: a.project_id ? String(a.project_id) : undefined,
728
+ skip: Array.isArray(a.skip) ? a.skip.map(String) : [],
729
+ });
730
+ case "agentry_list_recipes":
731
+ return await handleListRecipes(a.category ? String(a.category) : undefined);
732
+ case "agentry_run_recipe":
733
+ return await handleRunRecipe({
734
+ recipe_id: String(a.recipe_id ?? ""),
735
+ project_id: a.project_id ? String(a.project_id) : undefined,
736
+ params: a.params && typeof a.params === "object"
737
+ ? a.params
738
+ : {},
739
+ });
740
+ case "agentry_query_docs":
741
+ return await handleQueryDocs();
742
+ case "agentry_suggested_next_steps":
743
+ return await handleSuggestedNextSteps(a.project_id ? String(a.project_id) : undefined);
744
+ case "agentry_project_health":
745
+ return await handleProjectHealth(a.project_id ? String(a.project_id) : undefined);
746
+ case "agentry_create_alert":
747
+ return await handleCreateAlert({
748
+ name: String(a.name ?? ""),
749
+ recipe_id: String(a.recipe_id ?? ""),
750
+ threshold_column: String(a.threshold_column ?? ""),
751
+ threshold_op: String(a.threshold_op ?? ""),
752
+ threshold_value: typeof a.threshold_value === "number" ? a.threshold_value : Number(a.threshold_value),
753
+ params: a.params && typeof a.params === "object" ? a.params : {},
754
+ description: a.description ? String(a.description) : undefined,
755
+ webhook_id: a.webhook_id ? String(a.webhook_id) : undefined,
756
+ project_id: a.project_id ? String(a.project_id) : undefined,
757
+ });
758
+ case "agentry_list_alerts":
759
+ return await handleListAlerts(a.project_id ? String(a.project_id) : undefined);
760
+ case "agentry_evaluate_alert":
761
+ return await handleEvaluateAlert({
762
+ alert_id: String(a.alert_id ?? ""),
763
+ project_id: a.project_id ? String(a.project_id) : undefined,
764
+ });
765
+ case "agentry_delete_alert":
766
+ return await handleDeleteAlert({
767
+ alert_id: String(a.alert_id ?? ""),
768
+ project_id: a.project_id ? String(a.project_id) : undefined,
769
+ });
770
+ case "agentry_remember":
771
+ return await handleRemember({
772
+ case_id: String(a.case_id ?? ""),
773
+ summary: String(a.summary ?? ""),
774
+ fingerprint: a.fingerprint ? String(a.fingerprint) : undefined,
775
+ status: a.status ? String(a.status) : undefined,
776
+ error_type: a.error_type ? String(a.error_type) : undefined,
777
+ pr_url: a.pr_url ? String(a.pr_url) : undefined,
778
+ watch_for: a.watch_for ? String(a.watch_for) : undefined,
779
+ tags: Array.isArray(a.tags) ? a.tags.map(String) : undefined,
780
+ project_id: a.project_id ? String(a.project_id) : undefined,
781
+ });
782
+ case "agentry_recall":
783
+ return handleRecall({
784
+ case_id: a.case_id ? String(a.case_id) : undefined,
785
+ project_id: a.project_id ? String(a.project_id) : undefined,
786
+ });
787
+ case "agentry_register_webhook":
788
+ return await handleRegisterWebhook({
789
+ url: String(a.url ?? ""),
790
+ events: Array.isArray(a.events) ? a.events.map(String) : undefined,
791
+ description: a.description ? String(a.description) : undefined,
792
+ project_id: a.project_id ? String(a.project_id) : undefined,
793
+ });
794
+ case "agentry_list_webhooks":
795
+ return await handleListWebhooks(a.project_id ? String(a.project_id) : undefined);
796
+ case "agentry_test_webhook":
797
+ return await handleTestWebhook({
798
+ webhook_id: String(a.webhook_id ?? ""),
799
+ project_id: a.project_id ? String(a.project_id) : undefined,
800
+ });
801
+ case "agentry_delete_webhook":
802
+ return await handleDeleteWebhook({
803
+ webhook_id: String(a.webhook_id ?? ""),
804
+ project_id: a.project_id ? String(a.project_id) : undefined,
805
+ });
806
+ case "agentry_automation_docs":
807
+ return await handleAutomationDocs();
808
+ default:
809
+ return {
810
+ error: {
811
+ code: "unknown_tool",
812
+ message: `Unknown tool: ${name}`,
813
+ next_action: "Call ListTools to see available tools.",
814
+ },
815
+ };
816
+ }
817
+ }
818
+ catch (err) {
819
+ return summarizeApiError(err);
820
+ }
821
+ }
822
+ // ---------------------------------------------------------------------------
823
+ // Tool handlers
824
+ // ---------------------------------------------------------------------------
825
+ function handleStatus() {
826
+ const cfg = loadConfig();
827
+ const hint = getOnboardingHint(cfg);
828
+ const projectIds = Object.keys(cfg.projects);
829
+ return {
830
+ server_url: cfg.server_url,
831
+ has_api_key: Boolean(cfg.api_key),
832
+ api_key_prefix: cfg.api_key ? `${cfg.api_key.slice(0, 8)}…` : null,
833
+ default_project_id: cfg.default_project_id,
834
+ project_count: projectIds.length,
835
+ projects: projectIds.map((id) => {
836
+ const p = cfg.projects[id];
837
+ return { id, name: p.name, local_path: p.local_path };
838
+ }),
839
+ onboarding: hint,
840
+ next_steps: [hint.message, hint.next_action],
841
+ };
842
+ }
843
+ async function handleLogin(input) {
844
+ const cfg = loadConfig();
845
+ if (input.mode === "start_only") {
846
+ const start = await api.startDeviceFlow(cfg);
847
+ return {
848
+ mode: "start_only",
849
+ verification_uri: start.verification_uri,
850
+ user_code: start.user_code,
851
+ device_code: start.device_code,
852
+ interval: start.interval,
853
+ expires_in: start.expires_in,
854
+ next_action: `Tell the user: "Open ${start.verification_uri} and enter the code ${start.user_code}." ` +
855
+ "Once they confirm they've authorized, call agentry_login again with mode='poll_once' and this device_code.",
856
+ };
857
+ }
858
+ if (input.mode === "poll_once") {
859
+ if (!input.device_code) {
860
+ return {
861
+ error: {
862
+ code: "missing_device_code",
863
+ message: "mode='poll_once' requires device_code from the start_only call.",
864
+ next_action: "Either call agentry_login with mode='start_only' first, or use mode='full' and let the tool handle the loop.",
865
+ },
866
+ };
867
+ }
868
+ const result = await api.pollDeviceFlow(cfg, input.device_code);
869
+ if ("api_key" in result) {
870
+ const next = persistKeyResponse(cfg, result);
871
+ return {
872
+ ok: true,
873
+ user_id: result.user_id,
874
+ github: result.github,
875
+ api_key_prefix: result.prefix,
876
+ persisted_to: "local config",
877
+ server_url: next.server_url,
878
+ next_action: "Authenticated. Call `agentry_create_project` with a project name (and local_path of the repo) to mint a DSN.",
879
+ };
880
+ }
881
+ return {
882
+ ok: false,
883
+ status: result.status,
884
+ next_action: result.next_action ??
885
+ "Wait and call agentry_login again with mode='poll_once' and the same device_code.",
886
+ };
887
+ }
888
+ // mode === "full" — do the entire flow inside one tool call.
889
+ const start = await api.startDeviceFlow(cfg);
890
+ const intervalMs = Math.max(1, start.interval) * 1000;
891
+ const deadline = Date.now() + (input.timeout_seconds ?? 180) * 1000;
892
+ // We can't wait for the agent to confirm the user authorized, so the agent's
893
+ // calling convention here is "tell the user the code, then keep polling
894
+ // automatically." Surface the verification info up front in the response
895
+ // metadata via instructions so the calling agent can show it before the
896
+ // poll completes — the JSON we return at the end includes it as well.
897
+ let lastStatus = null;
898
+ while (Date.now() < deadline) {
899
+ const result = await api.pollDeviceFlow(cfg, start.device_code);
900
+ if ("api_key" in result) {
901
+ const next = persistKeyResponse(cfg, result);
902
+ return {
903
+ ok: true,
904
+ user_id: result.user_id,
905
+ github: result.github,
906
+ api_key_prefix: result.prefix,
907
+ persisted_to: "local config",
908
+ server_url: next.server_url,
909
+ verification_uri_used: start.verification_uri,
910
+ user_code_used: start.user_code,
911
+ next_action: "Authenticated. Call `agentry_create_project` with a project name (and local_path of the repo) to mint a DSN.",
912
+ };
913
+ }
914
+ lastStatus = result.status;
915
+ if (result.status === "expired" || result.status === "denied") {
916
+ return {
917
+ ok: false,
918
+ status: result.status,
919
+ verification_uri: start.verification_uri,
920
+ user_code: start.user_code,
921
+ next_action: result.next_action ??
922
+ (result.status === "expired"
923
+ ? "Device code expired. Call `agentry_login` again to start a fresh flow."
924
+ : "User declined. Confirm with them and call `agentry_login` again if they want to proceed."),
925
+ };
926
+ }
927
+ const delay = result.status === "slow_down" ? intervalMs + 5000 : intervalMs;
928
+ await sleep(delay);
929
+ }
930
+ return {
931
+ ok: false,
932
+ status: "timeout",
933
+ last_status: lastStatus,
934
+ verification_uri: start.verification_uri,
935
+ user_code: start.user_code,
936
+ device_code: start.device_code,
937
+ next_action: `Timed out after ${input.timeout_seconds ?? 180}s. ` +
938
+ `Confirm the user opened ${start.verification_uri} and entered ${start.user_code}, then call agentry_login again with mode='poll_once' and device_code='${start.device_code}'.`,
939
+ };
940
+ }
941
+ function sleep(ms) {
942
+ return new Promise((r) => setTimeout(r, ms));
943
+ }
944
+ async function handleRotateKey() {
945
+ const cfg = loadConfig();
946
+ if (!cfg.api_key) {
947
+ return {
948
+ error: {
949
+ code: "no_key",
950
+ message: "No API key to rotate.",
951
+ next_action: "Call `agentry_login` to authenticate via GitHub first.",
952
+ },
953
+ };
954
+ }
955
+ const resp = await api.rotateKey(cfg);
956
+ persistKeyResponse(cfg, resp);
957
+ return {
958
+ ok: true,
959
+ user_id: resp.user_id,
960
+ api_key_prefix: resp.prefix,
961
+ next_action: resp.next_action ??
962
+ "New key stored locally. Old key is revoked — update any places it was pasted (CI envs, etc).",
963
+ };
964
+ }
965
+ async function handleListProjects() {
966
+ const cfg = loadConfig();
967
+ if (!cfg.api_key) {
968
+ return {
969
+ error: {
970
+ code: "no_key",
971
+ message: "No API key on file.",
972
+ next_action: "Call `agentry_login` first.",
973
+ },
974
+ };
975
+ }
976
+ const resp = await api.listProjects(cfg);
977
+ const enriched = resp.projects.map((p) => {
978
+ const local = cfg.projects[p.id];
979
+ return {
980
+ ...p,
981
+ local_path: local?.local_path ?? null,
982
+ dsn_known_locally: Boolean(local?.dsn),
983
+ is_default: cfg.default_project_id === p.id,
984
+ };
985
+ });
986
+ return {
987
+ projects: enriched,
988
+ default_project_id: cfg.default_project_id,
989
+ next_action: enriched.length === 0
990
+ ? "No projects yet. Call `agentry_create_project` to make one."
991
+ : "Pick a project and call `agentry_list_cases` to see open errors.",
992
+ };
993
+ }
994
+ async function handleCreateProject(input) {
995
+ if (!input.name) {
996
+ return {
997
+ error: {
998
+ code: "missing_name",
999
+ message: "name is required",
1000
+ next_action: "Ask the user for a project name.",
1001
+ },
1002
+ };
1003
+ }
1004
+ const cfg = loadConfig();
1005
+ if (!cfg.api_key) {
1006
+ return {
1007
+ error: {
1008
+ code: "no_key",
1009
+ message: "No API key on file.",
1010
+ next_action: "Call `agentry_login` first.",
1011
+ },
1012
+ };
1013
+ }
1014
+ const resp = await api.createProject(cfg, {
1015
+ name: input.name,
1016
+ repo_url: input.repo_url,
1017
+ local_path: input.local_path,
1018
+ default_branch: input.default_branch,
1019
+ });
1020
+ // Persist DSN + local_path locally so subsequent tool calls can route back.
1021
+ const projectConfig = {
1022
+ id: resp.id,
1023
+ name: resp.name,
1024
+ dsn: resp.dsn,
1025
+ local_path: input.local_path ?? null,
1026
+ default_branch: resp.default_branch ?? input.default_branch ?? "main",
1027
+ };
1028
+ const nextCfg = {
1029
+ ...cfg,
1030
+ default_project_id: cfg.default_project_id ?? resp.id,
1031
+ projects: { ...cfg.projects, [resp.id]: projectConfig },
1032
+ };
1033
+ saveConfig(nextCfg);
1034
+ // Try to also pull the install snippet so the agent has it without a second roundtrip.
1035
+ let install = null;
1036
+ try {
1037
+ install = await api.getInstallSnippet(cfg, "node");
1038
+ }
1039
+ catch {
1040
+ // Non-fatal — the agent can call agentry_install_sdk separately.
1041
+ }
1042
+ return {
1043
+ ok: true,
1044
+ project: {
1045
+ id: resp.id,
1046
+ name: resp.name,
1047
+ dsn: resp.dsn,
1048
+ default_branch: projectConfig.default_branch,
1049
+ local_path: projectConfig.local_path,
1050
+ // First-party typed endpoints — agent should prefer these over /v1/log/.
1051
+ // Same DSN authenticates all three. POST any-language HTTP client; no SDK.
1052
+ logs_url: resp.logs_url,
1053
+ analytics_url: resp.analytics_url,
1054
+ deploys_url: resp.deploys_url,
1055
+ // Sentry-DSN URL kept for drop-in into existing Sentry SDKs.
1056
+ sentry_dsn_url: resp.sentry_dsn_url,
1057
+ },
1058
+ install_snippet: install,
1059
+ next_action: resp.next_action ??
1060
+ "DSN stored locally. Three typed endpoints (logs_url / analytics_url / deploys_url) " +
1061
+ "all authenticate with this DSN. Paste the install snippet, set AGENTRY_DSN, then call " +
1062
+ "`agentry_capture_test_event` to verify ingest.",
1063
+ };
1064
+ }
1065
+ async function handleInstallSdk(language) {
1066
+ const cfg = loadConfig();
1067
+ const resp = await api.getInstallSnippet(cfg, language);
1068
+ return {
1069
+ language: resp.language,
1070
+ code: resp.code,
1071
+ env_vars: resp.env_vars,
1072
+ next_action: "Paste `code` into the user's project, then set the env_vars (AGENTRY_DSN especially). " +
1073
+ "Then call `agentry_capture_test_event` to verify ingest.",
1074
+ };
1075
+ }
1076
+ async function handleListCases(input) {
1077
+ const cfg = loadConfig();
1078
+ if (!cfg.api_key) {
1079
+ return {
1080
+ error: {
1081
+ code: "no_key",
1082
+ message: "No API key on file.",
1083
+ next_action: "Call `agentry_login` first.",
1084
+ },
1085
+ };
1086
+ }
1087
+ const picked = pickProject(cfg, input.project_id);
1088
+ if (!picked) {
1089
+ return {
1090
+ error: {
1091
+ code: "no_project",
1092
+ message: "No project_id given and no default project set.",
1093
+ next_action: "Either pass project_id, or call `agentry_create_project` to create one (which becomes default).",
1094
+ },
1095
+ };
1096
+ }
1097
+ const status = input.status ?? "open";
1098
+ const resp = await api.listCases(cfg, picked.id, status);
1099
+ const local = picked.project;
1100
+ const enriched = resp.cases.map((c) => ({
1101
+ ...c,
1102
+ local_path: local?.local_path ?? null,
1103
+ }));
1104
+ return {
1105
+ project_id: picked.id,
1106
+ status,
1107
+ cases: enriched,
1108
+ next_action: enriched.length === 0
1109
+ ? "No cases match. Either trigger one with `agentry_capture_test_event`, or wait for a real one."
1110
+ : "For each case, `cd` to its `local_path` and call `agentry_get_case` for the stack + suppression hints.",
1111
+ };
1112
+ }
1113
+ async function handleGetCase(caseId) {
1114
+ if (!caseId) {
1115
+ return {
1116
+ error: {
1117
+ code: "missing_case_id",
1118
+ message: "case_id is required",
1119
+ next_action: "Pass case_id from the output of `agentry_list_cases`.",
1120
+ },
1121
+ };
1122
+ }
1123
+ const cfg = loadConfig();
1124
+ const detail = await api.getCase(cfg, caseId);
1125
+ // Enrich with local_path looked up by project_id.
1126
+ const localProject = projectByLookup(cfg, detail.project_id);
1127
+ const localPath = localProject?.local_path ?? detail.local_path ?? null;
1128
+ return {
1129
+ ...detail,
1130
+ local_path: localPath,
1131
+ next_action: detail.next_actions && detail.next_actions.length > 0
1132
+ ? detail.next_actions.join(" / ")
1133
+ : `Investigate at ${localPath ?? "(local_path unknown — store it via agentry_create_project)"}.`,
1134
+ };
1135
+ }
1136
+ async function handleResolveCase(input) {
1137
+ if (!input.case_id) {
1138
+ return {
1139
+ error: { code: "missing_case_id", message: "case_id is required" },
1140
+ };
1141
+ }
1142
+ const cfg = loadConfig();
1143
+ const updated = await api.updateCase(cfg, input.case_id, {
1144
+ status: "resolved",
1145
+ agent_summary: input.summary,
1146
+ pr_url: input.pr_url,
1147
+ });
1148
+ return {
1149
+ ok: true,
1150
+ case: updated,
1151
+ next_action: "Case resolved. If this fingerprint is likely to recur and you want auto-suppression, " +
1152
+ "call `agentry_record_suppression` with action 'auto_resolve' or 'auto_ignore'.",
1153
+ };
1154
+ }
1155
+ async function handleMarkSpurious(input) {
1156
+ if (!input.case_id) {
1157
+ return { error: { code: "missing_case_id", message: "case_id is required" } };
1158
+ }
1159
+ const cfg = loadConfig();
1160
+ const updated = await api.updateCase(cfg, input.case_id, {
1161
+ status: "spurious",
1162
+ agent_summary: input.reason,
1163
+ });
1164
+ let suppression_id = null;
1165
+ if (input.suppress_pattern) {
1166
+ try {
1167
+ const r = await api.recordSuppression(cfg, updated.project_id, {
1168
+ fingerprint_pattern: input.suppress_pattern,
1169
+ action: "auto_ignore",
1170
+ reason: input.reason,
1171
+ });
1172
+ suppression_id = r.id;
1173
+ }
1174
+ catch (err) {
1175
+ // Surface the suppression failure but don't undo the spurious mark.
1176
+ return {
1177
+ ok: true,
1178
+ case: updated,
1179
+ suppression_error: summarizeApiError(err).error,
1180
+ next_action: "Case marked spurious, but suppression rule failed to save. " +
1181
+ "Retry `agentry_record_suppression` independently.",
1182
+ };
1183
+ }
1184
+ }
1185
+ return {
1186
+ ok: true,
1187
+ case: updated,
1188
+ suppression_id,
1189
+ next_action: suppression_id
1190
+ ? "Case is spurious and a noise rule is in place. Future matching events will be auto-ignored."
1191
+ : "Case is spurious. To prevent future noise, call `agentry_record_suppression`.",
1192
+ };
1193
+ }
1194
+ async function handleRecordSuppression(input) {
1195
+ if (!input.fingerprint_pattern) {
1196
+ return { error: { code: "missing_pattern", message: "fingerprint_pattern is required" } };
1197
+ }
1198
+ if (!input.action) {
1199
+ return { error: { code: "missing_action", message: "action is required" } };
1200
+ }
1201
+ if (input.action === "prompt_hint" && !input.hint_text) {
1202
+ return {
1203
+ error: {
1204
+ code: "missing_hint_text",
1205
+ message: "hint_text is required when action is 'prompt_hint'",
1206
+ next_action: "Provide hint_text — it's the message attached to future matching cases.",
1207
+ },
1208
+ };
1209
+ }
1210
+ const cfg = loadConfig();
1211
+ const picked = pickProject(cfg, input.project_id);
1212
+ if (!picked) {
1213
+ return {
1214
+ error: {
1215
+ code: "no_project",
1216
+ message: "No project_id given and no default project set.",
1217
+ next_action: "Pass project_id or call `agentry_create_project` first.",
1218
+ },
1219
+ };
1220
+ }
1221
+ const resp = await api.recordSuppression(cfg, picked.id, {
1222
+ fingerprint_pattern: input.fingerprint_pattern,
1223
+ action: input.action,
1224
+ reason: input.reason,
1225
+ hint_text: input.hint_text,
1226
+ });
1227
+ return {
1228
+ ok: true,
1229
+ suppression_id: resp.id,
1230
+ project_id: picked.id,
1231
+ next_action: "Future matching events will follow the suppression rule.",
1232
+ };
1233
+ }
1234
+ async function handleCaptureTestEvent(projectId) {
1235
+ const cfg = loadConfig();
1236
+ const picked = pickProject(cfg, projectId);
1237
+ if (!picked || !picked.project) {
1238
+ return {
1239
+ error: {
1240
+ code: "no_project",
1241
+ message: "No project found locally. The DSN is stored locally only for projects created via this MCP — " +
1242
+ "if the project was created elsewhere, call `agentry_list_projects` and `agentry_install_sdk` first, " +
1243
+ "or recreate the project here.",
1244
+ next_action: "Call `agentry_create_project` with a name to mint a fresh DSN.",
1245
+ },
1246
+ };
1247
+ }
1248
+ const dsn = picked.project.dsn;
1249
+ const parsed = parseDsn(dsn);
1250
+ if (!parsed) {
1251
+ return {
1252
+ error: {
1253
+ code: "bad_dsn",
1254
+ message: `Stored DSN for project ${picked.id} is not parseable.`,
1255
+ next_action: "Recreate the project via `agentry_create_project`.",
1256
+ },
1257
+ };
1258
+ }
1259
+ const event = buildSyntheticEvent();
1260
+ // Pass the full DSN as auth — the API accepts either the token alone or the full DSN form.
1261
+ const resp = await api.storeEvent(cfg, parsed.projectId, dsn, event);
1262
+ return {
1263
+ ok: true,
1264
+ project_id: picked.id,
1265
+ event_id: resp.id,
1266
+ case_id: resp.case_id ?? null,
1267
+ next_action: resp.case_id
1268
+ ? `Event ingested. Call \`agentry_get_case\` with case_id="${resp.case_id}" to see how it looks to the agent flow.`
1269
+ : "Event ingested. The case may take a moment to materialize — call `agentry_list_cases` shortly.",
1270
+ };
1271
+ }
1272
+ // ---------------------------------------------------------------------------
1273
+ // Deploy events
1274
+ // ---------------------------------------------------------------------------
1275
+ async function handleRecordDeploy(input) {
1276
+ if (!input.sha) {
1277
+ return {
1278
+ error: {
1279
+ code: "missing_sha",
1280
+ message: "sha is required",
1281
+ next_action: "Pass the git SHA of the deployed commit.",
1282
+ },
1283
+ };
1284
+ }
1285
+ const cfg = loadConfig();
1286
+ const r = requireProjectFromConfig(cfg, input.project_id);
1287
+ if ("error" in r)
1288
+ return r.error;
1289
+ const parsed = parseDsn(r.project.dsn);
1290
+ if (!parsed) {
1291
+ return {
1292
+ error: {
1293
+ code: "bad_dsn",
1294
+ message: `Stored DSN for project ${r.id} is not parseable.`,
1295
+ next_action: "Recreate the project via agentry_create_project.",
1296
+ },
1297
+ };
1298
+ }
1299
+ const resp = await api.recordDeploy(cfg, parsed.projectId, r.project.dsn, {
1300
+ sha: input.sha,
1301
+ ...(input.branch !== undefined ? { branch: input.branch } : {}),
1302
+ ...(input.environment !== undefined ? { environment: input.environment } : {}),
1303
+ ...(input.message !== undefined ? { message: input.message } : {}),
1304
+ ...(input.url !== undefined ? { url: input.url } : {}),
1305
+ ...(input.actor !== undefined ? { actor: input.actor } : {}),
1306
+ });
1307
+ return {
1308
+ ok: true,
1309
+ deploy_id: resp.id,
1310
+ received_at: resp.received_at,
1311
+ next_action: "Deploy recorded. Future cases ingested after this timestamp will surface this deploy in their recent_deploys.",
1312
+ };
1313
+ }
1314
+ async function handleListDeploys(input) {
1315
+ const cfg = loadConfig();
1316
+ if (!cfg.api_key) {
1317
+ return {
1318
+ error: {
1319
+ code: "no_key",
1320
+ message: "No API key on file.",
1321
+ next_action: "Call `agentry_login` first.",
1322
+ },
1323
+ };
1324
+ }
1325
+ const projectId = input.project_id ?? cfg.default_project_id;
1326
+ if (!projectId) {
1327
+ return {
1328
+ error: {
1329
+ code: "no_project",
1330
+ message: "No project specified and no default project set.",
1331
+ next_action: "Pass project_id, or set a default by creating a project.",
1332
+ },
1333
+ };
1334
+ }
1335
+ const optsArg = {};
1336
+ if (input.limit !== undefined)
1337
+ optsArg.limit = input.limit;
1338
+ if (input.since !== undefined)
1339
+ optsArg.since = input.since;
1340
+ const resp = await api.listDeploys(cfg, projectId, optsArg);
1341
+ return {
1342
+ project_id: projectId,
1343
+ ...resp,
1344
+ next_action: "Cross-reference deploy received_at with case last_seen_at to attribute regressions.",
1345
+ };
1346
+ }
1347
+ // ---------------------------------------------------------------------------
1348
+ // Analytics
1349
+ // ---------------------------------------------------------------------------
1350
+ async function handleTrackTestEvent(input) {
1351
+ const cfg = loadConfig();
1352
+ const r = requireProjectFromConfig(cfg, input.project_id);
1353
+ if ("error" in r)
1354
+ return r.error;
1355
+ const parsed = parseDsn(r.project.dsn);
1356
+ if (!parsed) {
1357
+ return {
1358
+ error: {
1359
+ code: "bad_dsn",
1360
+ message: `Stored DSN for project ${r.id} is not parseable.`,
1361
+ next_action: "Recreate the project via agentry_create_project.",
1362
+ },
1363
+ };
1364
+ }
1365
+ const eventName = input.event || "agentry_verify";
1366
+ const resp = await api.trackEvent(cfg, parsed.projectId, r.project.dsn, {
1367
+ event: eventName,
1368
+ distinct_id: "agentry-mcp-test",
1369
+ properties: { source: "agentry_track_test_event", ts: Math.floor(Date.now() / 1000) },
1370
+ });
1371
+ return {
1372
+ ok: resp.ok ?? true,
1373
+ event: eventName,
1374
+ next_action: "Event forwarded to PostHog. Use agentry_analytics_query to verify it landed " +
1375
+ `(SELECT count() FROM events WHERE event='${eventName}' AND timestamp > now() - INTERVAL 5 MINUTE).`,
1376
+ };
1377
+ }
1378
+ async function handleAnalyticsQuery(input) {
1379
+ if (!input.query) {
1380
+ return {
1381
+ error: {
1382
+ code: "missing_query",
1383
+ message: "query (HogQL) is required",
1384
+ next_action: "Pass a HogQL query string.",
1385
+ },
1386
+ };
1387
+ }
1388
+ const cfg = loadConfig();
1389
+ if (!cfg.api_key) {
1390
+ return {
1391
+ error: {
1392
+ code: "no_key",
1393
+ message: "No API key on file.",
1394
+ next_action: "Call `agentry_login` first.",
1395
+ },
1396
+ };
1397
+ }
1398
+ const projectId = input.project_id ?? cfg.default_project_id;
1399
+ if (!projectId) {
1400
+ return {
1401
+ error: {
1402
+ code: "no_project",
1403
+ message: "No project specified and no default project set.",
1404
+ next_action: "Pass project_id, or create a project.",
1405
+ },
1406
+ };
1407
+ }
1408
+ const resp = await api.analyticsQuery(cfg, projectId, input.query);
1409
+ return {
1410
+ project_id: projectId,
1411
+ ...resp,
1412
+ next_action: "Interpret the rows. If you suspect a regression, call agentry_list_deploys to see if a deploy correlates.",
1413
+ };
1414
+ }
1415
+ // ---------------------------------------------------------------------------
1416
+ // Install guide + comprehensive verification
1417
+ // ---------------------------------------------------------------------------
1418
+ async function handleInstallGuide(input) {
1419
+ const cfg = loadConfig();
1420
+ const guide = await api.getInstallGuide(cfg, input.framework);
1421
+ // Filter steps if signal_types is a strict subset.
1422
+ const wanted = new Set(input.signal_types);
1423
+ const filteredSteps = guide.steps.filter((s) => {
1424
+ // common + verify steps always shown
1425
+ if (["install_sdk", "set_env_vars", "verify_install"].includes(s.id) ||
1426
+ s.action === "verify")
1427
+ return true;
1428
+ if (s.id.startsWith("init_") || s.id.includes("error") || s.id.includes("uncaught") ||
1429
+ s.id.includes("middleware") || s.id.includes("error_boundary"))
1430
+ return wanted.has("errors");
1431
+ if (s.id.startsWith("track_") || s.id.includes("analytics"))
1432
+ return wanted.has("analytics");
1433
+ if (s.id.startsWith("fire_deploy") || s.id.includes("deploy"))
1434
+ return wanted.has("deploys");
1435
+ return true;
1436
+ });
1437
+ return {
1438
+ ...guide,
1439
+ steps: filteredSteps,
1440
+ next_action: "Read each step in order. For 'edit' steps, find the file matching `file_hint` in the customer's repo " +
1441
+ "and apply `code`. For 'run' steps, execute `command`. After all steps, call agentry_verify_install — " +
1442
+ "that's the only proof the install actually works.",
1443
+ };
1444
+ }
1445
+ async function handleVerifyInstall(input) {
1446
+ const cfg = loadConfig();
1447
+ const r = requireProjectFromConfig(cfg, input.project_id);
1448
+ if ("error" in r)
1449
+ return r.error;
1450
+ const skip = new Set(input.skip);
1451
+ const checks = {};
1452
+ if (!skip.has("errors")) {
1453
+ try {
1454
+ const resp = await handleCaptureTestEvent(r.id);
1455
+ const ok = resp.ok === true;
1456
+ checks.errors = {
1457
+ ok,
1458
+ detail: ok
1459
+ ? `synthetic error landed → case_id=${resp.case_id ?? "(pending)"}`
1460
+ : `failed: ${JSON.stringify(resp.error ?? resp)}`,
1461
+ };
1462
+ }
1463
+ catch (err) {
1464
+ checks.errors = { ok: false, detail: err instanceof Error ? err.message : String(err) };
1465
+ }
1466
+ }
1467
+ if (!skip.has("analytics")) {
1468
+ try {
1469
+ const resp = await handleTrackTestEvent({ project_id: r.id, event: "agentry_verify_install" });
1470
+ const ok = resp.ok === true;
1471
+ checks.analytics = {
1472
+ ok,
1473
+ detail: ok
1474
+ ? "synthetic analytics event forwarded to PostHog (verify with agentry_analytics_query if needed)"
1475
+ : `failed: ${JSON.stringify(resp.error ?? resp)}`,
1476
+ };
1477
+ }
1478
+ catch (err) {
1479
+ checks.analytics = { ok: false, detail: err instanceof Error ? err.message : String(err) };
1480
+ }
1481
+ }
1482
+ if (!skip.has("deploys")) {
1483
+ try {
1484
+ const resp = await handleRecordDeploy({
1485
+ project_id: r.id,
1486
+ sha: `agentry-verify-${Date.now().toString(36)}`,
1487
+ branch: "agentry-verify",
1488
+ environment: "agentry-verify",
1489
+ message: "synthetic deploy from agentry_verify_install",
1490
+ });
1491
+ const ok = resp.ok === true;
1492
+ checks.deploys = {
1493
+ ok,
1494
+ detail: ok
1495
+ ? "synthetic deploy recorded"
1496
+ : `failed: ${JSON.stringify(resp.error ?? resp)}`,
1497
+ };
1498
+ }
1499
+ catch (err) {
1500
+ checks.deploys = { ok: false, detail: err instanceof Error ? err.message : String(err) };
1501
+ }
1502
+ }
1503
+ const passed = Object.entries(checks).filter(([, v]) => v.ok).map(([k]) => k);
1504
+ const failed = Object.entries(checks).filter(([, v]) => !v.ok).map(([k]) => k);
1505
+ // Mark install_verified locally so the onboarding state machine moves to "ready".
1506
+ if (failed.length === 0) {
1507
+ const updated = loadConfig();
1508
+ const proj = updated.projects[r.id];
1509
+ if (proj) {
1510
+ updated.projects[r.id] = { ...proj, install_verified: true };
1511
+ saveConfig(updated);
1512
+ }
1513
+ }
1514
+ // Pull in suggested next-steps so the agent can immediately surface a menu.
1515
+ let nextSuggestions = [];
1516
+ if (failed.length === 0) {
1517
+ try {
1518
+ const ns = await api.getNextSteps(loadConfig(), r.id);
1519
+ nextSuggestions = ns.suggestions;
1520
+ }
1521
+ catch {
1522
+ /* non-fatal */
1523
+ }
1524
+ }
1525
+ const baseAction = failed.length === 0
1526
+ ? "Install verified. Errors land in agentry_list_cases; analytics flow to PostHog; deploys via agentry_list_deploys."
1527
+ : `Install incomplete. Failed signal types: ${failed.join(", ")}. ` +
1528
+ "For each failed type, re-read its corresponding step in agentry_install_guide and fix.";
1529
+ return {
1530
+ ok: failed.length === 0,
1531
+ summary: `${passed.length}/${Object.keys(checks).length} signal types verified`,
1532
+ passed,
1533
+ failed,
1534
+ checks,
1535
+ suggested_next_steps: nextSuggestions,
1536
+ next_action: failed.length === 0 && nextSuggestions.length > 0
1537
+ ? baseAction +
1538
+ " Now offer the user this menu of post-install prompts:\n" +
1539
+ nextSuggestions
1540
+ .slice(0, 5)
1541
+ .map((s, i) => ` ${i + 1}. ${s.title} — ${s.description}`)
1542
+ .join("\n") +
1543
+ "\nWhen the user picks one, paste its `prompt_template` as their next prompt (or just execute the listed `uses`)."
1544
+ : baseAction,
1545
+ };
1546
+ }
1547
+ // ---------------------------------------------------------------------------
1548
+ // Recipes / query docs
1549
+ // ---------------------------------------------------------------------------
1550
+ async function handleListRecipes(category) {
1551
+ const cfg = loadConfig();
1552
+ const resp = await api.listRecipes(cfg, category);
1553
+ return {
1554
+ ...resp,
1555
+ next_action: "Pick a recipe whose `description` or `example_user_question` matches what the user asked. " +
1556
+ "Then call `agentry_run_recipe` with its id and params (defaults are filled in if omitted). " +
1557
+ "If nothing matches, call `agentry_query_docs` to compose ad-hoc HogQL via `agentry_analytics_query`.",
1558
+ };
1559
+ }
1560
+ async function handleRunRecipe(input) {
1561
+ if (!input.recipe_id) {
1562
+ return {
1563
+ error: {
1564
+ code: "missing_recipe_id",
1565
+ message: "recipe_id is required",
1566
+ next_action: "Call `agentry_list_recipes` to see available recipes.",
1567
+ },
1568
+ };
1569
+ }
1570
+ const cfg = loadConfig();
1571
+ if (!cfg.api_key) {
1572
+ return {
1573
+ error: {
1574
+ code: "no_key",
1575
+ message: "No API key on file.",
1576
+ next_action: "Call `agentry_login` first.",
1577
+ },
1578
+ };
1579
+ }
1580
+ const projectId = input.project_id ?? cfg.default_project_id;
1581
+ if (!projectId) {
1582
+ return {
1583
+ error: {
1584
+ code: "no_project",
1585
+ message: "No project_id specified and no default project set.",
1586
+ next_action: "Pass project_id, or create a project first.",
1587
+ },
1588
+ };
1589
+ }
1590
+ const resp = await api.runRecipe(cfg, projectId, input.recipe_id, input.params);
1591
+ return {
1592
+ project_id: projectId,
1593
+ ...resp,
1594
+ };
1595
+ }
1596
+ async function handleQueryDocs() {
1597
+ const cfg = loadConfig();
1598
+ const md = await api.getQueryDocs(cfg);
1599
+ return {
1600
+ docs_markdown: md,
1601
+ next_action: "Read the schema + HogQL primer. Compose your query, then call `agentry_analytics_query` " +
1602
+ "with project_id and the HogQL string. For errors/deploys, use the relevant recipe or typed endpoints.",
1603
+ };
1604
+ }
1605
+ async function handleSuggestedNextSteps(projectId) {
1606
+ const cfg = loadConfig();
1607
+ if (!cfg.api_key) {
1608
+ return {
1609
+ error: {
1610
+ code: "no_key",
1611
+ message: "No API key on file.",
1612
+ next_action: "Call `agentry_login` first.",
1613
+ },
1614
+ };
1615
+ }
1616
+ const id = projectId ?? cfg.default_project_id;
1617
+ if (!id) {
1618
+ return {
1619
+ error: {
1620
+ code: "no_project",
1621
+ message: "No project specified and no default project set.",
1622
+ next_action: "Pass project_id, or create a project first.",
1623
+ },
1624
+ };
1625
+ }
1626
+ const resp = await api.getNextSteps(cfg, id);
1627
+ return {
1628
+ project_id: id,
1629
+ ...resp,
1630
+ next_action: "Surface these to the user as numbered options. Use this format:\n\n" +
1631
+ " Now that you're set up, want to:\n" +
1632
+ " 1. <title> — <description>\n" +
1633
+ " 2. <title> — <description>\n" +
1634
+ " 3. <title> — <description>\n\n" +
1635
+ "When the user picks one, paste the matching `prompt_template` as the user's next prompt " +
1636
+ "(or just execute the listed `uses` directly).",
1637
+ };
1638
+ }
1639
+ // ---------------------------------------------------------------------------
1640
+ // Webhooks
1641
+ // ---------------------------------------------------------------------------
1642
+ function pickProjectId(cfg, projectId) {
1643
+ return projectId ?? cfg.default_project_id ?? null;
1644
+ }
1645
+ async function handleRegisterWebhook(input) {
1646
+ if (!input.url || !/^https?:\/\//.test(input.url)) {
1647
+ return {
1648
+ error: {
1649
+ code: "missing_url",
1650
+ message: "url is required and must be http(s)",
1651
+ next_action: "Ask the user for the receiving URL (their Worker / Function endpoint).",
1652
+ },
1653
+ };
1654
+ }
1655
+ const cfg = loadConfig();
1656
+ if (!cfg.api_key) {
1657
+ return { error: { code: "no_key", message: "No API key on file.", next_action: "Call `agentry_login` first." } };
1658
+ }
1659
+ const pid = pickProjectId(cfg, input.project_id);
1660
+ if (!pid) {
1661
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1662
+ }
1663
+ const body = { url: input.url };
1664
+ if (input.events)
1665
+ body.events = input.events;
1666
+ if (input.description)
1667
+ body.description = input.description;
1668
+ const resp = await api.registerWebhook(cfg, pid, body);
1669
+ return {
1670
+ ...resp,
1671
+ project_id: pid,
1672
+ next_action: "STORE THE signing_secret NOW — it won't be shown again. " +
1673
+ "Tell the user to add it to their endpoint's env. Then call agentry_test_webhook to verify the wiring.",
1674
+ };
1675
+ }
1676
+ async function handleListWebhooks(projectId) {
1677
+ const cfg = loadConfig();
1678
+ if (!cfg.api_key) {
1679
+ return { error: { code: "no_key", message: "No API key.", next_action: "Call `agentry_login`." } };
1680
+ }
1681
+ const pid = pickProjectId(cfg, projectId);
1682
+ if (!pid)
1683
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1684
+ const resp = await api.listWebhooks(cfg, pid);
1685
+ return { project_id: pid, ...resp };
1686
+ }
1687
+ async function handleTestWebhook(input) {
1688
+ if (!input.webhook_id) {
1689
+ return { error: { code: "missing_webhook_id", message: "webhook_id is required.", next_action: "Pass webhook id from agentry_list_webhooks." } };
1690
+ }
1691
+ const cfg = loadConfig();
1692
+ if (!cfg.api_key)
1693
+ return { error: { code: "no_key", message: "No API key.", next_action: "Call `agentry_login`." } };
1694
+ const pid = pickProjectId(cfg, input.project_id);
1695
+ if (!pid)
1696
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1697
+ const resp = await api.testWebhook(cfg, pid, input.webhook_id);
1698
+ return {
1699
+ ...resp,
1700
+ project_id: pid,
1701
+ next_action: "Test fired. Call agentry_list_webhooks to see last_status — should be 200 if your endpoint accepted it.",
1702
+ };
1703
+ }
1704
+ async function handleDeleteWebhook(input) {
1705
+ if (!input.webhook_id) {
1706
+ return { error: { code: "missing_webhook_id", message: "webhook_id is required.", next_action: "Pass webhook id." } };
1707
+ }
1708
+ const cfg = loadConfig();
1709
+ if (!cfg.api_key)
1710
+ return { error: { code: "no_key", message: "No API key.", next_action: "Call `agentry_login`." } };
1711
+ const pid = pickProjectId(cfg, input.project_id);
1712
+ if (!pid)
1713
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1714
+ const resp = await api.deleteWebhook(cfg, pid, input.webhook_id);
1715
+ return { ...resp, project_id: pid };
1716
+ }
1717
+ async function handleAutomationDocs() {
1718
+ const cfg = loadConfig();
1719
+ const md = await api.getAutomationDocs(cfg);
1720
+ return {
1721
+ docs_markdown: md,
1722
+ next_action: "Read the automation patterns. Each pattern includes a paste-ready Worker template the agent " +
1723
+ "can drop into the customer's repo. After deploying their endpoint, call agentry_register_webhook " +
1724
+ "with the URL, then agentry_test_webhook to confirm.",
1725
+ };
1726
+ }
1727
+ // ---------------------------------------------------------------------------
1728
+ // Local memory file (agentry_memory.md)
1729
+ // ---------------------------------------------------------------------------
1730
+ function resolveLocalPath(cfg, projectId) {
1731
+ const id = projectId ?? cfg.default_project_id;
1732
+ if (!id)
1733
+ return null;
1734
+ const proj = cfg.projects[id];
1735
+ if (!proj || !proj.local_path)
1736
+ return null;
1737
+ return { id, localPath: proj.local_path };
1738
+ }
1739
+ async function handleRemember(input) {
1740
+ if (!input.case_id || !input.summary) {
1741
+ return {
1742
+ error: {
1743
+ code: "missing_input",
1744
+ message: "case_id and summary are required",
1745
+ next_action: "Pass both. Use agentry_get_case to find case_id; summary should describe what you learned.",
1746
+ },
1747
+ };
1748
+ }
1749
+ const cfg = loadConfig();
1750
+ const resolved = resolveLocalPath(cfg, input.project_id);
1751
+ if (!resolved) {
1752
+ return {
1753
+ error: {
1754
+ code: "no_local_path",
1755
+ message: "No local_path stored for this project — agentry_memory.md needs a local repo to write into.",
1756
+ next_action: "Recreate the project with `local_path` set (the absolute path to the repo on disk), then retry.",
1757
+ },
1758
+ };
1759
+ }
1760
+ const filePath = getMemoryPath(resolved.localPath);
1761
+ if (!filePath) {
1762
+ return { error: { code: "no_path", message: "Could not resolve memory file path.", next_action: "Check local_path." } };
1763
+ }
1764
+ const action = upsertCaseSection(filePath, {
1765
+ case_id: input.case_id,
1766
+ summary: input.summary,
1767
+ ...(input.fingerprint !== undefined ? { fingerprint: input.fingerprint } : {}),
1768
+ ...(input.status !== undefined ? { status: input.status } : {}),
1769
+ ...(input.error_type !== undefined ? { error_type: input.error_type } : {}),
1770
+ ...(input.pr_url !== undefined ? { pr_url: input.pr_url } : {}),
1771
+ ...(input.watch_for !== undefined ? { watch_for: input.watch_for } : {}),
1772
+ ...(input.tags !== undefined ? { tags: input.tags } : {}),
1773
+ });
1774
+ return {
1775
+ ok: true,
1776
+ file_path: filePath,
1777
+ action,
1778
+ next_action: "Memory updated. Future investigations can read this file directly (it's just markdown). " +
1779
+ "Consider committing agentry_memory.md so the team and future agents see prior context.",
1780
+ };
1781
+ }
1782
+ function handleRecall(input) {
1783
+ const cfg = loadConfig();
1784
+ const resolved = resolveLocalPath(cfg, input.project_id);
1785
+ if (!resolved) {
1786
+ return {
1787
+ error: {
1788
+ code: "no_local_path",
1789
+ message: "No local_path stored for this project.",
1790
+ next_action: "Recreate the project with `local_path` set.",
1791
+ },
1792
+ };
1793
+ }
1794
+ const filePath = getMemoryPath(resolved.localPath);
1795
+ if (!fs.existsSync(filePath)) {
1796
+ return {
1797
+ file_path: filePath,
1798
+ content: null,
1799
+ next_action: `No ${MEMORY_FILENAME} yet. Call agentry_remember after your next investigation to start the memory file.`,
1800
+ };
1801
+ }
1802
+ if (input.case_id) {
1803
+ const section = readCaseSection(filePath, input.case_id);
1804
+ return {
1805
+ file_path: filePath,
1806
+ case_id: input.case_id,
1807
+ content: section,
1808
+ next_action: section
1809
+ ? "Read the section before investigating — prior knowledge is in there."
1810
+ : "No prior memory for this case_id. Investigate fresh and call agentry_remember when done.",
1811
+ };
1812
+ }
1813
+ const content = fs.readFileSync(filePath, "utf8");
1814
+ return {
1815
+ file_path: filePath,
1816
+ content,
1817
+ next_action: "Use this as context. For a specific case, pass case_id to filter. " +
1818
+ "You can also Read/Grep this file directly with the agent's file tools.",
1819
+ };
1820
+ }
1821
+ // ---------------------------------------------------------------------------
1822
+ // Project health + alerts
1823
+ // ---------------------------------------------------------------------------
1824
+ async function handleProjectHealth(projectId) {
1825
+ const cfg = loadConfig();
1826
+ if (!cfg.api_key)
1827
+ return { error: { code: "no_key", message: "No API key.", next_action: "Call `agentry_login`." } };
1828
+ const pid = projectId ?? cfg.default_project_id;
1829
+ if (!pid)
1830
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1831
+ const resp = await api.getProjectHealth(cfg, pid);
1832
+ return resp;
1833
+ }
1834
+ async function handleCreateAlert(input) {
1835
+ if (!input.name || !input.recipe_id || !input.threshold_column || !input.threshold_op || !Number.isFinite(input.threshold_value)) {
1836
+ return {
1837
+ error: {
1838
+ code: "missing_input",
1839
+ message: "name, recipe_id, threshold_column, threshold_op, threshold_value are required",
1840
+ next_action: "Pass all five. Use agentry_list_recipes to find a recipe with the column you want to threshold.",
1841
+ },
1842
+ };
1843
+ }
1844
+ const cfg = loadConfig();
1845
+ if (!cfg.api_key)
1846
+ return { error: { code: "no_key", message: "No API key.", next_action: "Call `agentry_login`." } };
1847
+ const pid = input.project_id ?? cfg.default_project_id;
1848
+ if (!pid)
1849
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1850
+ const body = {
1851
+ name: input.name,
1852
+ recipe_id: input.recipe_id,
1853
+ threshold_column: input.threshold_column,
1854
+ threshold_op: input.threshold_op,
1855
+ threshold_value: input.threshold_value,
1856
+ params: input.params,
1857
+ };
1858
+ if (input.description)
1859
+ body.description = input.description;
1860
+ if (input.webhook_id)
1861
+ body.webhook_id = input.webhook_id;
1862
+ const resp = await api.createAlert(cfg, pid, body);
1863
+ return {
1864
+ ...resp,
1865
+ project_id: pid,
1866
+ next_action: "Alert stored. Tell the user to call agentry_evaluate_alert from their cron / GitHub Actions / Cloudflare Cron " +
1867
+ "(say every 5 minutes). When threshold crosses, agentry fires the linked webhook.",
1868
+ };
1869
+ }
1870
+ async function handleListAlerts(projectId) {
1871
+ const cfg = loadConfig();
1872
+ if (!cfg.api_key)
1873
+ return { error: { code: "no_key", message: "No API key.", next_action: "Call `agentry_login`." } };
1874
+ const pid = projectId ?? cfg.default_project_id;
1875
+ if (!pid)
1876
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1877
+ const resp = await api.listAlerts(cfg, pid);
1878
+ return { project_id: pid, ...resp };
1879
+ }
1880
+ async function handleEvaluateAlert(input) {
1881
+ if (!input.alert_id) {
1882
+ return { error: { code: "missing_alert_id", message: "alert_id is required.", next_action: "Pass alert id from agentry_list_alerts." } };
1883
+ }
1884
+ const cfg = loadConfig();
1885
+ if (!cfg.api_key)
1886
+ return { error: { code: "no_key", message: "No API key.", next_action: "Call `agentry_login`." } };
1887
+ const pid = input.project_id ?? cfg.default_project_id;
1888
+ if (!pid)
1889
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1890
+ const resp = await api.evaluateAlert(cfg, pid, input.alert_id);
1891
+ return { project_id: pid, ...resp };
1892
+ }
1893
+ async function handleDeleteAlert(input) {
1894
+ if (!input.alert_id) {
1895
+ return { error: { code: "missing_alert_id", message: "alert_id is required.", next_action: "Pass alert id." } };
1896
+ }
1897
+ const cfg = loadConfig();
1898
+ if (!cfg.api_key)
1899
+ return { error: { code: "no_key", message: "No API key.", next_action: "Call `agentry_login`." } };
1900
+ const pid = input.project_id ?? cfg.default_project_id;
1901
+ if (!pid)
1902
+ return { error: { code: "no_project", message: "No project specified.", next_action: "Pass project_id." } };
1903
+ const resp = await api.deleteAlert(cfg, pid, input.alert_id);
1904
+ return { ...resp, project_id: pid };
1905
+ }
1906
+ //# sourceMappingURL=tools.js.map