@agentrysh/mcp 0.0.13 → 0.0.15

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 CHANGED
@@ -6,22 +6,49 @@ import { api } from "./api.js";
6
6
  import { loadConfig, saveConfig } from "./config.js";
7
7
  import { getOnboardingHint } from "./onboarding.js";
8
8
  import { getMemoryPath, readCaseSection, upsertCaseSection, MEMORY_FILENAME, } from "./memory.js";
9
+ import * as frictionTracker from "./friction-tracker.js";
9
10
  import * as fs from "node:fs";
10
11
  export const TOOL_DESCRIPTORS = [
11
12
  {
12
13
  name: "agentry_status",
13
14
  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 },
15
+ "Use this first if you don't know whether the user has signed up or has a project. " +
16
+ "" +
17
+ "If shapes + flags are passed (same vocabulary as agentry_recipe_requirements), the " +
18
+ "response also includes a coverage_gap report — applicable_count, runnable_count, and " +
19
+ "the events / event-properties still missing for full recipe runnability. Useful for " +
20
+ "re-installs and post-install audits: 'ready, but seat-utilization is one property " +
21
+ "away from working' is a more honest status than just 'ready'.",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ shapes: {
26
+ type: "array",
27
+ items: { type: "string" },
28
+ description: "Optional. App shapes for coverage check (e.g. ['b2b-saas', 'devtools-api']). " +
29
+ "When passed, response includes coverage_gap. Without it, status is project-only.",
30
+ },
31
+ has_revenue: { type: "boolean" },
32
+ is_b2b: { type: "boolean" },
33
+ is_two_sided: { type: "boolean" },
34
+ project_id: {
35
+ type: "string",
36
+ description: "Project id for the coverage check. Defaults to default_project_id.",
37
+ },
38
+ },
39
+ additionalProperties: false,
40
+ },
16
41
  },
17
42
  {
18
43
  name: "agentry_login",
19
- description: "Authenticate the user via GitHub device flow. Returns an API key, stored locally. " +
44
+ description: "Authenticate the user via the agentry sign-in page (GitHub, Google, or magic link — " +
45
+ "user picks any provider). Returns an API key, stored locally. " +
20
46
  "" +
21
47
  "RECOMMENDED two-call sequence for interactive sessions: " +
22
- " 1. Call with mode='start_only' → returns user_code + verification_uri + device_code. " +
23
- " Show the user the code and URL (DO NOT ask them to confirm authorization — they'll " +
24
- " just open the URL, paste the code, and you'll poll). " +
48
+ " 1. Call with mode='start_only' → returns user_code + verification_uri + device_code " +
49
+ " (plus verification_uri_complete with the code pre-filled). Show the user the URL " +
50
+ " and code (DO NOT ask them to confirm authorization — they'll open the URL, sign in " +
51
+ " with their provider of choice, and you'll poll). " +
25
52
  " 2. IMMEDIATELY call again with mode='full' + the device_code from step 1. " +
26
53
  " This blocks and auto-polls every ~5s for up to timeout_seconds (default 300 = 5min). " +
27
54
  " Returns the api_key when the user authorizes, or status='expired'/'denied' on failure. " +
@@ -606,15 +633,20 @@ export const TOOL_DESCRIPTORS = [
606
633
  },
607
634
  {
608
635
  name: "agentry_repair_analytics",
609
- description: "Re-attempt PostHog provisioning for the authenticated user. Idempotent — if the user " +
610
- "already has a PostHog project, returns its id without recreating. Use this when " +
636
+ description: "Re-attempt PostHog provisioning for one project. Idempotent — if the project already " +
637
+ "has a PostHog team binding, returns its id without recreating. Use this when " +
611
638
  "agentry_verify_install reports analytics ❌ with reason 'no_posthog_project' OR when a " +
612
639
  "/v1/track/ call returns 503 with that code. " +
613
640
  "" +
614
641
  "DO NOT re-run agentry_login for this failure mode — that mints a new api_key and " +
615
- "churns the user's local config. This tool runs only the provisioning step that was " +
616
- "supposed to happen at login.",
617
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
642
+ "churns the user's local config. This tool repairs the project-scoped analytics binding only.",
643
+ inputSchema: {
644
+ type: "object",
645
+ properties: {
646
+ project_id: { type: "string", description: "Project to repair. Defaults to local default_project_id." },
647
+ },
648
+ additionalProperties: false,
649
+ },
618
650
  },
619
651
  {
620
652
  name: "agentry_rotate_key",
@@ -979,13 +1011,16 @@ export const TOOL_DESCRIPTORS = [
979
1011
  },
980
1012
  {
981
1013
  name: "agentry_verify_install",
982
- description: "Comprehensive sanity check: fires a synthetic error, a synthetic analytics event, and a synthetic " +
983
- "deploy event, then reports which signal types reached agentry. Run this AFTER walking through " +
984
- "agentry_install_guide. " +
985
- "MUST be run with NO skipped signal types on a first-time install — the install is not done until " +
986
- "all three return OK. If any signal returns FAIL, the corresponding wire_* step was skipped or " +
987
- "wired incorrectly; the agent must go back and fix it before declaring the install complete. Do not " +
988
- "report 'installed' to the user with one or two ✅s and a ❌; that's a half-install.",
1014
+ description: "Comprehensive sanity check. Fires a synthetic error, analytics event, and deploy event AND " +
1015
+ " if shapes are provided checks PostHog for the events the user's applicable recipes need. " +
1016
+ "Run this AFTER walking through agentry_install_guide. " +
1017
+ "" +
1018
+ "MUST be run with NO skipped signal types on a first-time install. The install is not done " +
1019
+ "until all three signals are OK AND every event from `required_events` (derived from the app's " +
1020
+ "shape via agentry_recipe_requirements) is actually firing in PostHog. " +
1021
+ "" +
1022
+ "Do not report 'installed' to the user with one or two ✅s and a ❌, or with synthetic ✅s but " +
1023
+ "missing recipe events — that's a half-install.",
989
1024
  inputSchema: {
990
1025
  type: "object",
991
1026
  properties: {
@@ -996,6 +1031,26 @@ export const TOOL_DESCRIPTORS = [
996
1031
  description: "ONLY for re-runs after a partial install has already been verified. On a first install, " +
997
1032
  "leave this empty — all three signal types must verify before the install counts as done.",
998
1033
  },
1034
+ shapes: {
1035
+ type: "array",
1036
+ items: { type: "string" },
1037
+ description: "App shape(s) the agent detected during investigate_app_structure (e.g. ['b2b-saas', " +
1038
+ "'devtools-api']). When passed, verify_install also queries PostHog for the events the " +
1039
+ "applicable docs recipes need and reports which recipes are blocked. Strongly recommended " +
1040
+ "on first install — without it you only know the pipe works, not that the recipes will run.",
1041
+ },
1042
+ has_revenue: {
1043
+ type: "boolean",
1044
+ description: "Payment processor detected. Used with shapes to filter applicable recipes.",
1045
+ },
1046
+ is_b2b: {
1047
+ type: "boolean",
1048
+ description: "Workspace/account/seat model present. Used with shapes to filter applicable recipes.",
1049
+ },
1050
+ is_two_sided: {
1051
+ type: "boolean",
1052
+ description: "Two-sided marketplace. Used with shapes to filter applicable recipes.",
1053
+ },
999
1054
  },
1000
1055
  additionalProperties: false,
1001
1056
  },
@@ -1018,6 +1073,49 @@ export const TOOL_DESCRIPTORS = [
1018
1073
  additionalProperties: false,
1019
1074
  },
1020
1075
  },
1076
+ {
1077
+ name: "agentry_recipe_requirements",
1078
+ description: "Recipe-driven install gate. Given the user's detected app shape(s) and revenue/B2B/two-sided " +
1079
+ "flags, returns the *applicable* docs recipes and the UNION of events they need. The install guide " +
1080
+ "uses this output as the canonical event inventory: every event in `required_events` MUST be wired, " +
1081
+ "with the listed `required_event_properties` attached, before agentry_verify_install passes. " +
1082
+ "" +
1083
+ "CALL THIS during agentry_install_guide's inventory step, AFTER categorizing the app, BEFORE writing " +
1084
+ "any track() calls. Don't hand-build an AARRR inventory — let the recipe catalog drive what to " +
1085
+ "instrument so the user can actually run the recipes a week / month later. " +
1086
+ "" +
1087
+ "shapes: comma-sep app shapes from the install-guide vocabulary (b2c-saas, b2b-saas, ecommerce, " +
1088
+ "marketplace, content-media, devtools-api, games, internal-tool, single-purchase, open-source, " +
1089
+ "enterprise-sales, mobile-app). 'universal' recipes always match. " +
1090
+ "has_revenue: true if the app has a payment processor (stripe/paddle/etc.) — gates revenue recipes. " +
1091
+ "is_b2b: true if the app has workspaces/accounts/seats — gates B2B recipes. " +
1092
+ "is_two_sided: true if the app is a marketplace with supply/demand — gates marketplace recipes.",
1093
+ inputSchema: {
1094
+ type: "object",
1095
+ properties: {
1096
+ shapes: {
1097
+ type: "array",
1098
+ items: { type: "string" },
1099
+ description: "App shape(s) detected during investigate_app_structure. Apps can straddle (e.g. a B2B SaaS " +
1100
+ "that's also a devtools API). Pass every shape that fits — applicable recipes are the union.",
1101
+ },
1102
+ has_revenue: {
1103
+ type: "boolean",
1104
+ description: "Payment processor present in the codebase. Gates trial/subscription/churn recipes.",
1105
+ },
1106
+ is_b2b: {
1107
+ type: "boolean",
1108
+ description: "Workspace/account/seat model present. Gates account-health, seat-utilization, etc.",
1109
+ },
1110
+ is_two_sided: {
1111
+ type: "boolean",
1112
+ description: "Two-sided marketplace. Gates supply-side/liquidity recipes.",
1113
+ },
1114
+ },
1115
+ required: ["shapes"],
1116
+ additionalProperties: false,
1117
+ },
1118
+ },
1021
1119
  {
1022
1120
  name: "agentry_run_recipe",
1023
1121
  description: "Run a recipe by id. Returns rows + a render_hint the agent uses to format the answer " +
@@ -1257,12 +1355,19 @@ export const TOOL_DESCRIPTORS = [
1257
1355
  {
1258
1356
  name: "agentry_send_feedback",
1259
1357
  description: "File feedback for the agentry team when something doesn't work or is missing. " +
1260
- "Call this in exactly two situations: " +
1358
+ "Call this in three situations: " +
1261
1359
  "(1) the user explicitly asks for a feature that doesn't exist or expresses frustration that agentry " +
1262
1360
  "isn't doing what they want ('I wish it could…', 'why doesn't it…', 'this doesn't work', " +
1263
1361
  "'feature request: …'); " +
1264
1362
  "(2) you have made 2+ failed attempts at the same task — same MCP tool returning errors, or repeatedly " +
1265
- "failing to find a recipe/route for what the user asked. " +
1363
+ "failing to find a recipe/route for what the user asked; " +
1364
+ "(3) RECIPE PROPOSAL — during agentry_install_guide's plan-confirmation step, the user named a " +
1365
+ "question they want to be able to ask that ISN'T in the docs recipe catalog. Use " +
1366
+ "`kind: 'recipe_proposal'`, quote the user's question in `message`, and put a structured " +
1367
+ "proposal in `agent_note` (the JSON shape described under that field). This is the only path " +
1368
+ "from a customer's install back into the recipe catalog — without it, every novel question " +
1369
+ "stays stranded in chat history. " +
1370
+ "" +
1266
1371
  "File it ONCE per distinct issue per session — don't spam. Quote the user verbatim in `message` where " +
1267
1372
  "possible. Don't apologize repeatedly to the user; just tell them you've logged it.",
1268
1373
  inputSchema: {
@@ -1270,9 +1375,13 @@ export const TOOL_DESCRIPTORS = [
1270
1375
  properties: {
1271
1376
  kind: {
1272
1377
  type: "string",
1273
- enum: ["missing_feature", "bug", "ux_friction", "other"],
1378
+ enum: ["missing_feature", "bug", "ux_friction", "recipe_proposal", "other"],
1274
1379
  description: "missing_feature = capability doesn't exist; bug = something behaves wrong; " +
1275
- "ux_friction = works but is awkward/confusing; other = anything else.",
1380
+ "ux_friction = works but is awkward/confusing; " +
1381
+ "recipe_proposal = user named a question to ask that no docs recipe covers — " +
1382
+ "use during install flow; agent_note MUST include the proposed applies_to / " +
1383
+ "required_events / required_event_properties so the catalog can absorb it; " +
1384
+ "other = anything else.",
1276
1385
  },
1277
1386
  message: {
1278
1387
  type: "string",
@@ -1282,7 +1391,17 @@ export const TOOL_DESCRIPTORS = [
1282
1391
  agent_note: {
1283
1392
  type: "string",
1284
1393
  description: "Optional: what YOU (the agent) were trying to do, which tools you called, what failed. " +
1285
- "Helps the agentry team reproduce.",
1394
+ "Helps the agentry team reproduce. " +
1395
+ "" +
1396
+ "FOR kind='recipe_proposal' THIS FIELD IS REQUIRED and must be JSON-shaped, e.g.: " +
1397
+ "{\"applies_to\": [\"b2b-saas\"], \"requires_revenue\": true, \"required_events\": " +
1398
+ "[\"workspace_created\", \"template_first_action\"], \"required_event_properties\": " +
1399
+ "{\"workspace_created\": [\"starter_template\"]}, \"category\": \"growth\", " +
1400
+ "\"suggested_title\": \"Which starter template has the highest 30-day retention?\", " +
1401
+ "\"detected_shape_evidence\": \"workspaces.starter_template column present at " +
1402
+ "db/schema.ts:23\", \"new_events_wired\": [\"workspace_created.starter_template\", " +
1403
+ "\"template_first_action\"]}. " +
1404
+ "The catalog maintainers paste this shape into a new recipe.md with minimal editing.",
1286
1405
  },
1287
1406
  tool_name: {
1288
1407
  type: "string",
@@ -1339,6 +1458,59 @@ export const TOOL_DESCRIPTORS = [
1339
1458
  additionalProperties: false,
1340
1459
  },
1341
1460
  },
1461
+ {
1462
+ name: "agentry_invite_teammate",
1463
+ description: "Mint a one-shot invite URL granting a teammate access to a project. Owner-only. " +
1464
+ "The recipient opens the URL, signs in with any provider (GitHub / Google / magic link), " +
1465
+ "and gets their own agentry account + API key automatically scoped to this project — they " +
1466
+ "do NOT use your API key. " +
1467
+ "" +
1468
+ "After this returns, share `invite_url` with your teammate (Slack, email — manual for v1). " +
1469
+ "Invite expires in 7 days, single-use.",
1470
+ inputSchema: {
1471
+ type: "object",
1472
+ properties: {
1473
+ project_id: { type: "string", description: "Defaults to the local default project." },
1474
+ email: {
1475
+ type: "string",
1476
+ description: "Optional — shown on the invite landing page as a hint to the recipient. " +
1477
+ "Does NOT restrict who can consume the invite; the link is the credential.",
1478
+ },
1479
+ role: {
1480
+ type: "string",
1481
+ enum: ["member", "owner"],
1482
+ description: "Defaults to 'member'. 'owner' is rare — only add other owners deliberately.",
1483
+ },
1484
+ },
1485
+ additionalProperties: false,
1486
+ },
1487
+ },
1488
+ {
1489
+ name: "agentry_list_members",
1490
+ description: "List members of a project + any pending (un-consumed, un-expired) invites. " +
1491
+ "Any member can see the full roster; remove members via agentry_remove_member.",
1492
+ inputSchema: {
1493
+ type: "object",
1494
+ properties: {
1495
+ project_id: { type: "string", description: "Defaults to the local default project." },
1496
+ },
1497
+ additionalProperties: false,
1498
+ },
1499
+ },
1500
+ {
1501
+ name: "agentry_remove_member",
1502
+ description: "Remove a teammate from a project. Owner-only. Cannot remove the last remaining owner, " +
1503
+ "and an owner cannot remove themselves (transfer ownership first — TBD).",
1504
+ inputSchema: {
1505
+ type: "object",
1506
+ properties: {
1507
+ project_id: { type: "string", description: "Defaults to the local default project." },
1508
+ user_id: { type: "string", description: "The user_id from agentry_list_members." },
1509
+ },
1510
+ required: ["user_id"],
1511
+ additionalProperties: false,
1512
+ },
1513
+ },
1342
1514
  ];
1343
1515
  // ---------------------------------------------------------------------------
1344
1516
  // Helpers shared across tool handlers
@@ -1438,10 +1610,49 @@ function buildSyntheticEvent() {
1438
1610
  }
1439
1611
  export async function dispatchTool(name, args) {
1440
1612
  const a = args ?? {};
1613
+ let result;
1614
+ try {
1615
+ result = await dispatchToolInner(name, a);
1616
+ }
1617
+ catch (err) {
1618
+ frictionTracker.recordCall({
1619
+ tool: name,
1620
+ ts: Date.now(),
1621
+ ok: false,
1622
+ errorCode: err instanceof Error ? err.message.slice(0, 80) : String(err).slice(0, 80),
1623
+ });
1624
+ // Repeated-failure detector — fires when same tool errors N times in a row.
1625
+ try {
1626
+ frictionTracker.detectRepeatedFailure(loadConfig(), name);
1627
+ }
1628
+ catch { /* swallow — observability never breaks the request */ }
1629
+ return summarizeApiError(err);
1630
+ }
1631
+ // Record success/failure outcome. `error` shape on the result means handler
1632
+ // returned an error envelope without throwing.
1633
+ const ok = !("error" in (result ?? {}));
1634
+ frictionTracker.recordCall({ tool: name, ts: Date.now(), ok });
1635
+ try {
1636
+ if (!ok)
1637
+ frictionTracker.detectRepeatedFailure(loadConfig(), name);
1638
+ if (name === "agentry_install_guide") {
1639
+ frictionTracker.detectGuideRefetched(loadConfig());
1640
+ }
1641
+ }
1642
+ catch { /* swallow */ }
1643
+ return result;
1644
+ }
1645
+ async function dispatchToolInner(name, a) {
1441
1646
  try {
1442
1647
  switch (name) {
1443
1648
  case "agentry_status":
1444
- return handleStatus();
1649
+ return await handleStatus({
1650
+ shapes: Array.isArray(a.shapes) ? a.shapes.map(String) : undefined,
1651
+ has_revenue: a.has_revenue === true,
1652
+ is_b2b: a.is_b2b === true,
1653
+ is_two_sided: a.is_two_sided === true,
1654
+ project_id: a.project_id ? String(a.project_id) : undefined,
1655
+ });
1445
1656
  case "agentry_login":
1446
1657
  return await handleLogin({
1447
1658
  mode: a.mode === "start_only" || a.mode === "poll_once"
@@ -1453,7 +1664,7 @@ export async function dispatchTool(name, args) {
1453
1664
  case "agentry_rotate_key":
1454
1665
  return await handleRotateKey();
1455
1666
  case "agentry_repair_analytics":
1456
- return await handleRepairAnalytics();
1667
+ return await handleRepairAnalytics(a.project_id ? String(a.project_id) : undefined);
1457
1668
  case "agentry_publish_query":
1458
1669
  return await handlePublishQuery({
1459
1670
  project_id: a.project_id ? String(a.project_id) : undefined,
@@ -1724,9 +1935,20 @@ export async function dispatchTool(name, args) {
1724
1935
  return await handleVerifyInstall({
1725
1936
  project_id: a.project_id ? String(a.project_id) : undefined,
1726
1937
  skip: Array.isArray(a.skip) ? a.skip.map(String) : [],
1938
+ shapes: Array.isArray(a.shapes) ? a.shapes.map(String) : undefined,
1939
+ has_revenue: a.has_revenue === true,
1940
+ is_b2b: a.is_b2b === true,
1941
+ is_two_sided: a.is_two_sided === true,
1727
1942
  });
1728
1943
  case "agentry_list_recipes":
1729
1944
  return await handleListRecipes(a.category ? String(a.category) : undefined);
1945
+ case "agentry_recipe_requirements":
1946
+ return await handleRecipeRequirements({
1947
+ shapes: Array.isArray(a.shapes) ? a.shapes.map(String) : [],
1948
+ has_revenue: a.has_revenue === true,
1949
+ is_b2b: a.is_b2b === true,
1950
+ is_two_sided: a.is_two_sided === true,
1951
+ });
1730
1952
  case "agentry_run_recipe":
1731
1953
  return await handleRunRecipe({
1732
1954
  recipe_id: String(a.recipe_id ?? ""),
@@ -1821,6 +2043,19 @@ export async function dispatchTool(name, args) {
1821
2043
  kind: a.kind ? String(a.kind) : undefined,
1822
2044
  resolved: typeof a.resolved === "boolean" ? a.resolved : undefined,
1823
2045
  });
2046
+ case "agentry_invite_teammate":
2047
+ return await handleInviteTeammate({
2048
+ project_id: a.project_id ? String(a.project_id) : undefined,
2049
+ email: a.email ? String(a.email) : undefined,
2050
+ role: a.role === "owner" ? "owner" : "member",
2051
+ });
2052
+ case "agentry_list_members":
2053
+ return await handleListMembers(a.project_id ? String(a.project_id) : undefined);
2054
+ case "agentry_remove_member":
2055
+ return await handleRemoveMember({
2056
+ project_id: a.project_id ? String(a.project_id) : undefined,
2057
+ user_id: String(a.user_id ?? ""),
2058
+ });
1824
2059
  default:
1825
2060
  return {
1826
2061
  error: {
@@ -1835,14 +2070,15 @@ export async function dispatchTool(name, args) {
1835
2070
  return summarizeApiError(err);
1836
2071
  }
1837
2072
  }
2073
+ // dispatchToolInner is closed above. Below: handlers.
1838
2074
  // ---------------------------------------------------------------------------
1839
2075
  // Tool handlers
1840
2076
  // ---------------------------------------------------------------------------
1841
- function handleStatus() {
2077
+ async function handleStatus(input) {
1842
2078
  const cfg = loadConfig();
1843
2079
  const hint = getOnboardingHint(cfg);
1844
2080
  const projectIds = Object.keys(cfg.projects);
1845
- return {
2081
+ const baseResult = {
1846
2082
  server_url: cfg.server_url,
1847
2083
  has_api_key: Boolean(cfg.api_key),
1848
2084
  api_key_prefix: cfg.api_key ? `${cfg.api_key.slice(0, 8)}…` : null,
@@ -1850,24 +2086,114 @@ function handleStatus() {
1850
2086
  project_count: projectIds.length,
1851
2087
  projects: projectIds.map((id) => {
1852
2088
  const p = cfg.projects[id];
1853
- return { id, name: p.name, local_path: p.local_path };
2089
+ return {
2090
+ id,
2091
+ name: p.name,
2092
+ local_path: p.local_path,
2093
+ analytics_ready: p.analytics_ready ?? null,
2094
+ posthog_project_id: p.posthog_project_id ?? null,
2095
+ };
1854
2096
  }),
1855
2097
  onboarding: hint,
1856
- next_steps: [hint.message, hint.next_action],
1857
2098
  };
2099
+ // Coverage check — only runs when shapes are passed AND we have a project + api key.
2100
+ // Independent of the onboarding hint above: status can be "ready" (synthetic signals
2101
+ // verified) while still having coverage gaps (events firing without required props).
2102
+ if (input?.shapes && input.shapes.length > 0 && cfg.api_key) {
2103
+ const projectId = input.project_id ?? cfg.default_project_id;
2104
+ if (projectId) {
2105
+ try {
2106
+ const reqs = await api.recipeRequirements(cfg, {
2107
+ shapes: input.shapes,
2108
+ has_revenue: input.has_revenue === true,
2109
+ is_b2b: input.is_b2b === true,
2110
+ is_two_sided: input.is_two_sided === true,
2111
+ });
2112
+ const events = await api.listEventNames(cfg, projectId);
2113
+ const eventNames = new Set([
2114
+ ...(events.server_emitted ?? []),
2115
+ ...(events.analytics_events ?? []).map((e) => e.event),
2116
+ ]);
2117
+ // Sample property keys for events that ARE present and have required props.
2118
+ const checkProps = new Set();
2119
+ for (const rec of reqs.applicable) {
2120
+ for (const ev of Object.keys(rec.required_event_properties || {})) {
2121
+ if (eventNames.has(ev))
2122
+ checkProps.add(ev);
2123
+ }
2124
+ }
2125
+ let sampledKeys = {};
2126
+ if (checkProps.size > 0) {
2127
+ try {
2128
+ const r = await api.eventPropertyKeys(cfg, projectId, [...checkProps]);
2129
+ sampledKeys = r.keys ?? {};
2130
+ }
2131
+ catch { /* best-effort */ }
2132
+ }
2133
+ const blocked = [];
2134
+ const runnableIds = [];
2135
+ for (const rec of reqs.applicable) {
2136
+ const missingEv = rec.required_events.filter((e) => !eventNames.has(e));
2137
+ const missingProp = {};
2138
+ for (const [ev, props] of Object.entries(rec.required_event_properties || {})) {
2139
+ if (!eventNames.has(ev))
2140
+ continue;
2141
+ const present = new Set(sampledKeys[ev] ?? []);
2142
+ const m = props.filter((p) => !present.has(p));
2143
+ if (m.length > 0)
2144
+ missingProp[ev] = m;
2145
+ }
2146
+ if (missingEv.length === 0 && Object.keys(missingProp).length === 0)
2147
+ runnableIds.push(rec.id);
2148
+ else
2149
+ blocked.push({ id: rec.id, title: rec.title, missing_events: missingEv, missing_properties: missingProp });
2150
+ }
2151
+ const missingEvUnion = new Set();
2152
+ const missingPropUnion = new Set();
2153
+ for (const b of blocked) {
2154
+ for (const e of b.missing_events)
2155
+ missingEvUnion.add(e);
2156
+ for (const [ev, props] of Object.entries(b.missing_properties))
2157
+ for (const p of props)
2158
+ missingPropUnion.add(`${ev}.${p}`);
2159
+ }
2160
+ baseResult.coverage_gap = {
2161
+ shapes: input.shapes,
2162
+ applicable_count: reqs.applicable_count,
2163
+ runnable_count: runnableIds.length,
2164
+ missing_events: [...missingEvUnion].sort(),
2165
+ missing_event_properties: [...missingPropUnion].sort(),
2166
+ blocked_recipes: blocked.map((b) => b.id),
2167
+ summary: `${runnableIds.length}/${reqs.applicable_count} applicable recipes runnable. ` +
2168
+ (missingEvUnion.size > 0 ? `Missing events: ${[...missingEvUnion].sort().join(", ")}. ` : "") +
2169
+ (missingPropUnion.size > 0 ? `Missing properties: ${[...missingPropUnion].sort().join(", ")}.` : ""),
2170
+ };
2171
+ }
2172
+ catch (err) {
2173
+ baseResult.coverage_gap = {
2174
+ error: err instanceof Error ? err.message : String(err),
2175
+ note: "coverage_gap requires logged-in api key + an existing project. Skipping.",
2176
+ };
2177
+ }
2178
+ }
2179
+ }
2180
+ baseResult.next_steps = [hint.message, hint.next_action];
2181
+ return baseResult;
1858
2182
  }
1859
2183
  async function handleLogin(input) {
1860
2184
  const cfg = loadConfig();
1861
2185
  if (input.mode === "start_only") {
1862
2186
  const start = await api.startDeviceFlow(cfg);
2187
+ const openUrl = start.verification_uri_complete ?? start.verification_uri;
1863
2188
  return {
1864
2189
  mode: "start_only",
1865
2190
  verification_uri: start.verification_uri,
2191
+ verification_uri_complete: start.verification_uri_complete,
1866
2192
  user_code: start.user_code,
1867
2193
  device_code: start.device_code,
1868
2194
  interval: start.interval,
1869
2195
  expires_in: start.expires_in,
1870
- next_action: `Show the user: "Open ${start.verification_uri} and enter the code ${start.user_code}." ` +
2196
+ next_action: `Show the user a clickable link: ${openUrl} one click takes them to sign-in, no code entry. ` +
1871
2197
  `Then IMMEDIATELY call agentry_login again with mode='full' and device_code='${start.device_code}'. ` +
1872
2198
  `That call will block and auto-poll for up to ${start.expires_in}s — DO NOT ask the user to confirm authorization before polling.`,
1873
2199
  };
@@ -1966,7 +2292,7 @@ async function handleLogin(input) {
1966
2292
  device_code: deviceCode,
1967
2293
  next_action: `Timed out after ${input.timeout_seconds ?? 300}s. ` +
1968
2294
  (verificationUri && userCode
1969
- ? `Confirm the user opened ${verificationUri} and entered ${userCode}, `
2295
+ ? `Confirm the user opened ${verificationUri}?code=${userCode}, `
1970
2296
  : "Confirm the user has authorized, ") +
1971
2297
  `then call agentry_login again with mode='full' and device_code='${deviceCode}' to resume polling.`,
1972
2298
  };
@@ -1995,7 +2321,7 @@ async function handleRotateKey() {
1995
2321
  "New key stored locally. Old key is revoked — update any places it was pasted (CI envs, etc).",
1996
2322
  };
1997
2323
  }
1998
- async function handleRepairAnalytics() {
2324
+ async function handleRepairAnalytics(projectId) {
1999
2325
  const cfg = loadConfig();
2000
2326
  if (!cfg.api_key) {
2001
2327
  return {
@@ -2006,9 +2332,32 @@ async function handleRepairAnalytics() {
2006
2332
  },
2007
2333
  };
2008
2334
  }
2335
+ const picked = pickProject(cfg, projectId);
2336
+ if (!picked || !picked.project) {
2337
+ return {
2338
+ error: {
2339
+ code: "no_project",
2340
+ message: "No local project selected for analytics repair.",
2341
+ next_action: "Create a project first or pass a project_id from agentry_list_projects.",
2342
+ },
2343
+ };
2344
+ }
2009
2345
  try {
2010
- const resp = await api.repairAnalyticsBackend(cfg);
2011
- return resp;
2346
+ const resp = await api.repairAnalyticsBackend(cfg, picked.id);
2347
+ const updatedProject = {
2348
+ ...picked.project,
2349
+ analytics_ready: resp.posthog_project_id !== null,
2350
+ posthog_project_id: resp.posthog_project_id,
2351
+ };
2352
+ const nextCfg = {
2353
+ ...cfg,
2354
+ projects: {
2355
+ ...cfg.projects,
2356
+ [picked.id]: updatedProject,
2357
+ },
2358
+ };
2359
+ saveConfig(nextCfg);
2360
+ return { project_id: picked.id, ...resp };
2012
2361
  }
2013
2362
  catch (err) {
2014
2363
  return {
@@ -2016,7 +2365,7 @@ async function handleRepairAnalytics() {
2016
2365
  code: "repair_failed",
2017
2366
  message: err instanceof Error ? err.message : String(err),
2018
2367
  next_action: "Upstream PostHog provisioning failed. If the error mentions 5xx / timeouts / rate " +
2019
- "limits, wait 30–60s and call agentry_repair_analytics again. Errors and deploys are " +
2368
+ "limits, wait 30–60s and call agentry_repair_analytics again for the same project. Errors and deploys are " +
2020
2369
  "unaffected; only analytics ingest needs PostHog.",
2021
2370
  },
2022
2371
  };
@@ -2425,6 +2774,8 @@ async function handleListProjects() {
2425
2774
  ...p,
2426
2775
  local_path: local?.local_path ?? null,
2427
2776
  dsn_known_locally: Boolean(local?.dsn),
2777
+ analytics_ready: local?.analytics_ready ?? null,
2778
+ posthog_project_id: local?.posthog_project_id ?? null,
2428
2779
  is_default: cfg.default_project_id === p.id,
2429
2780
  };
2430
2781
  });
@@ -2469,6 +2820,8 @@ async function handleCreateProject(input) {
2469
2820
  dsn: resp.dsn,
2470
2821
  local_path: input.local_path ?? null,
2471
2822
  default_branch: resp.default_branch ?? input.default_branch ?? "main",
2823
+ analytics_ready: resp.analytics?.ready,
2824
+ posthog_project_id: resp.analytics?.posthog_project_id ?? null,
2472
2825
  };
2473
2826
  const nextCfg = {
2474
2827
  ...cfg,
@@ -2497,6 +2850,7 @@ async function handleCreateProject(input) {
2497
2850
  logs_url: resp.logs_url,
2498
2851
  analytics_url: resp.analytics_url,
2499
2852
  deploys_url: resp.deploys_url,
2853
+ analytics: resp.analytics,
2500
2854
  },
2501
2855
  install_snippet: install,
2502
2856
  next_action: resp.next_action ??
@@ -3188,10 +3542,21 @@ async function handleAnalyticsQuery(input) {
3188
3542
  };
3189
3543
  }
3190
3544
  const resp = await api.analyticsQuery(cfg, projectId, input.query);
3545
+ // Friction: query references a named event in WHERE and got 0 rows back.
3546
+ // Heuristic-driven (detector inspects the query string); fires only when
3547
+ // a specific event name returned empty — most likely a missing-instrumentation
3548
+ // or wrong-event-name signal worth flagging for AI review.
3549
+ const rowsReturned = Array.isArray(resp.results) ? resp.results.length : 0;
3550
+ const frictionFp = frictionTracker.detectEmptyAnalyticsQuery(cfg, {
3551
+ query: input.query,
3552
+ project_id: projectId,
3553
+ rows_returned: rowsReturned,
3554
+ });
3191
3555
  return {
3192
3556
  project_id: projectId,
3193
3557
  ...resp,
3194
- next_action: "Interpret the rows. If you suspect a regression, call agentry_list_deploys to see if a deploy correlates.",
3558
+ next_action: "Interpret the rows. If you suspect a regression, call agentry_list_deploys to see if a deploy correlates." +
3559
+ frictionTracker.frictionNoticeSuffix(frictionFp),
3195
3560
  };
3196
3561
  }
3197
3562
  // ---------------------------------------------------------------------------
@@ -3310,36 +3675,165 @@ async function handleVerifyInstall(input) {
3310
3675
  const analyticsDetail = checks.analytics?.detail ?? "";
3311
3676
  const noPosthogProject = !checks.analytics?.ok &&
3312
3677
  (analyticsDetail.includes("no_posthog_project") ||
3313
- analyticsDetail.includes("user has no PostHog project provisioned"));
3678
+ analyticsDetail.includes("project has no PostHog project provisioned"));
3314
3679
  const baseAction = failed.length === 0
3315
3680
  ? "Install verified. Errors land in agentry_list_cases; analytics flow to PostHog; deploys via agentry_list_deploys."
3316
3681
  : noPosthogProject && failed.length === 1
3317
3682
  ? "Analytics is the only failed signal AND the cause is missing PostHog provisioning " +
3318
- "(first-login provisioning was best-effort and failed). Call agentry_repair_analytics " +
3683
+ "(the project binding is missing). Call agentry_repair_analytics " +
3319
3684
  "— it's idempotent and runs the same provisioning step. Then re-run agentry_verify_install. " +
3320
3685
  "DO NOT re-run agentry_login for this."
3321
3686
  : `Install incomplete. Failed signal types: ${failed.join(", ")}. ` +
3322
3687
  (noPosthogProject
3323
- ? "Analytics failed because the user has no PostHog project provisioned — " +
3688
+ ? "Analytics failed because the project has no PostHog project provisioned — " +
3324
3689
  "call agentry_repair_analytics, then re-run verify. "
3325
3690
  : "") +
3326
3691
  "For each failed type, re-read its corresponding step in agentry_install_guide and fix.";
3692
+ // ── Recipe runnability check ───────────────────────────────────────────
3693
+ // If the agent passed shapes, derive the required-events set and check
3694
+ // PostHog for each. Report per-recipe runnability so the agent knows
3695
+ // which docs recipes the user will actually be able to run.
3696
+ let recipeCheck = {
3697
+ requested: false,
3698
+ applicable_count: 0,
3699
+ runnable_count: 0,
3700
+ runnable: [],
3701
+ blocked: [],
3702
+ missing_events: [],
3703
+ missing_event_properties: [],
3704
+ };
3705
+ if (input.shapes && input.shapes.length > 0 && checks.analytics?.ok) {
3706
+ try {
3707
+ const cfg2 = loadConfig();
3708
+ const reqs = await api.recipeRequirements(cfg2, {
3709
+ shapes: input.shapes,
3710
+ has_revenue: input.has_revenue === true,
3711
+ is_b2b: input.is_b2b === true,
3712
+ is_two_sided: input.is_two_sided === true,
3713
+ });
3714
+ // Snapshot the distinct event names currently firing in this project.
3715
+ // listEventNames returns both server-emitted (PostHog $events) and analytics_events
3716
+ // (PostHog person events with counts). Union them — recipes can need either.
3717
+ const events = await api.listEventNames(cfg2, r.id);
3718
+ const eventNames = new Set([
3719
+ ...(events.server_emitted ?? []),
3720
+ ...(events.analytics_events ?? []).map((e) => e.event),
3721
+ ]);
3722
+ // Sample property keys for the union of required events present, so we
3723
+ // can confirm not just event presence but that the properties recipes
3724
+ // depend on are actually being attached.
3725
+ const eventsNeedingProps = [];
3726
+ const propsByEvent = {};
3727
+ for (const rec of reqs.applicable) {
3728
+ for (const [ev, props] of Object.entries(rec.required_event_properties || {})) {
3729
+ if (!eventNames.has(ev))
3730
+ continue; // event missing entirely; separate check handles
3731
+ eventsNeedingProps.push(ev);
3732
+ for (const p of props) {
3733
+ propsByEvent[ev] = propsByEvent[ev] || [];
3734
+ if (!propsByEvent[ev].includes(p))
3735
+ propsByEvent[ev].push(p);
3736
+ }
3737
+ }
3738
+ }
3739
+ let sampledKeys = {};
3740
+ if (eventsNeedingProps.length > 0) {
3741
+ try {
3742
+ const resp = await api.eventPropertyKeys(cfg2, r.id, [...new Set(eventsNeedingProps)]);
3743
+ sampledKeys = resp.keys ?? {};
3744
+ }
3745
+ catch {
3746
+ // best-effort — if property sampling fails, fall back to event-presence-only
3747
+ }
3748
+ }
3749
+ const runnable = [];
3750
+ const blocked = [];
3751
+ for (const rec of reqs.applicable) {
3752
+ const missingEvents = rec.required_events.filter((ev) => !eventNames.has(ev));
3753
+ const missingProps = {};
3754
+ for (const [ev, props] of Object.entries(rec.required_event_properties || {})) {
3755
+ if (!eventNames.has(ev))
3756
+ continue;
3757
+ const present = new Set(sampledKeys[ev] ?? []);
3758
+ const missing = props.filter((p) => !present.has(p));
3759
+ if (missing.length > 0)
3760
+ missingProps[ev] = missing;
3761
+ }
3762
+ if (missingEvents.length === 0 && Object.keys(missingProps).length === 0) {
3763
+ runnable.push(rec.id);
3764
+ }
3765
+ else {
3766
+ blocked.push({
3767
+ id: rec.id,
3768
+ title: rec.title,
3769
+ missing_events: missingEvents,
3770
+ missing_properties: missingProps,
3771
+ });
3772
+ }
3773
+ }
3774
+ const missingUnion = new Set();
3775
+ const missingPropUnion = new Set();
3776
+ for (const b of blocked) {
3777
+ for (const m of b.missing_events)
3778
+ missingUnion.add(m);
3779
+ for (const [ev, props] of Object.entries(b.missing_properties)) {
3780
+ for (const p of props)
3781
+ missingPropUnion.add(`${ev}.${p}`);
3782
+ }
3783
+ }
3784
+ recipeCheck = {
3785
+ requested: true,
3786
+ applicable_count: reqs.applicable_count,
3787
+ runnable_count: runnable.length,
3788
+ runnable,
3789
+ blocked,
3790
+ missing_events: [...missingUnion].sort(),
3791
+ missing_event_properties: [...missingPropUnion].sort(),
3792
+ };
3793
+ }
3794
+ catch (err) {
3795
+ recipeCheck.skipped_reason = err instanceof Error ? err.message : String(err);
3796
+ }
3797
+ }
3798
+ else if (input.shapes && input.shapes.length > 0) {
3799
+ recipeCheck.skipped_reason = "analytics signal failed — fix analytics first, then re-run verify with shapes";
3800
+ }
3801
+ else {
3802
+ recipeCheck.skipped_reason =
3803
+ "no shapes provided — pass shapes/has_revenue/is_b2b/is_two_sided to verify recipe runnability";
3804
+ }
3805
+ const recipeFailing = recipeCheck.requested && recipeCheck.runnable_count < recipeCheck.applicable_count;
3806
+ const allOk = failed.length === 0 && !recipeFailing;
3327
3807
  return {
3328
- ok: failed.length === 0,
3329
- summary: `${passed.length}/${Object.keys(checks).length} signal types verified`,
3808
+ ok: allOk,
3809
+ summary: `${passed.length}/${Object.keys(checks).length} signal types verified` +
3810
+ (recipeCheck.requested
3811
+ ? `; ${recipeCheck.runnable_count}/${recipeCheck.applicable_count} applicable recipes runnable`
3812
+ : ""),
3330
3813
  passed,
3331
3814
  failed,
3332
3815
  checks,
3816
+ recipe_check: recipeCheck,
3333
3817
  suggested_next_steps: nextSuggestions,
3334
- next_action: failed.length === 0 && nextSuggestions.length > 0
3335
- ? baseAction +
3336
- " Now offer the user this menu of post-install prompts:\n" +
3337
- nextSuggestions
3338
- .slice(0, 5)
3339
- .map((s, i) => ` ${i + 1}. ${s.title} ${s.description}`)
3340
- .join("\n") +
3341
- "\nWhen the user picks one, paste its `prompt_template` as their next prompt (or just execute the listed `uses`)."
3342
- : baseAction,
3818
+ next_action: recipeFailing
3819
+ ? `Install is HALF-DONE. Synthetic signals work, but ${recipeCheck.applicable_count - recipeCheck.runnable_count} of ${recipeCheck.applicable_count} applicable recipes are blocked.` +
3820
+ (recipeCheck.missing_events.length > 0
3821
+ ? ` Missing events: ${recipeCheck.missing_events.join(", ")}.`
3822
+ : "") +
3823
+ (recipeCheck.missing_event_properties.length > 0
3824
+ ? ` Missing required properties (event.property): ${recipeCheck.missing_event_properties.join(", ")} — events fire but lack required properties.`
3825
+ : "") + " " +
3826
+ `Go back to the install-guide step \`track_comprehensively_for_this_app\` and wire the missing events. ` +
3827
+ `Re-run agentry_verify_install with the same shapes when done. Do NOT tell the user 'installed' until this is 0 missing.`
3828
+ : failed.length === 0 && nextSuggestions.length > 0
3829
+ ? baseAction +
3830
+ " Now offer the user this menu of post-install prompts:\n" +
3831
+ nextSuggestions
3832
+ .slice(0, 5)
3833
+ .map((s, i) => ` ${i + 1}. ${s.title} — ${s.description}`)
3834
+ .join("\n") +
3835
+ "\nWhen the user picks one, paste its `prompt_template` as their next prompt (or just execute the listed `uses`)."
3836
+ : baseAction,
3343
3837
  };
3344
3838
  }
3345
3839
  // ---------------------------------------------------------------------------
@@ -3355,6 +3849,20 @@ async function handleListRecipes(category) {
3355
3849
  "If nothing matches, call `agentry_query_docs` to compose ad-hoc HogQL via `agentry_analytics_query`.",
3356
3850
  };
3357
3851
  }
3852
+ async function handleRecipeRequirements(input) {
3853
+ if (input.shapes.length === 0) {
3854
+ return {
3855
+ error: {
3856
+ code: "missing_shapes",
3857
+ message: "shapes is required (e.g. ['b2c-saas'], ['devtools-api', 'b2b-saas'])",
3858
+ next_action: "Re-call agentry_recipe_requirements with the app shape(s) detected during " +
3859
+ "investigate_app_structure. See the description for the allowed vocabulary.",
3860
+ },
3861
+ };
3862
+ }
3863
+ const cfg = loadConfig();
3864
+ return await api.recipeRequirements(cfg, input);
3865
+ }
3358
3866
  async function handleRunRecipe(input) {
3359
3867
  if (!input.recipe_id) {
3360
3868
  return {
@@ -3386,9 +3894,19 @@ async function handleRunRecipe(input) {
3386
3894
  };
3387
3895
  }
3388
3896
  const resp = await api.runRecipe(cfg, projectId, input.recipe_id, input.params);
3897
+ // Aggressive friction detection: a recipe that runs without errors but
3898
+ // returns 0 rows almost always means required events aren't firing. Fire
3899
+ // feedback to the agentry team for AI review.
3900
+ const rowsReturned = Array.isArray(resp.rows) ? resp.rows.length : 0;
3901
+ const frictionFp = frictionTracker.detectEmptyRecipe(cfg, {
3902
+ recipe_id: input.recipe_id,
3903
+ project_id: projectId,
3904
+ rows_returned: rowsReturned,
3905
+ });
3389
3906
  return {
3390
3907
  project_id: projectId,
3391
3908
  ...resp,
3909
+ next_action: (resp.next_action ?? "") + frictionTracker.frictionNoticeSuffix(frictionFp),
3392
3910
  };
3393
3911
  }
3394
3912
  async function handleQueryDocs() {
@@ -3774,4 +4292,61 @@ async function handleListFeedback(input) {
3774
4292
  const resp = await api.listFeedback(cfg, opts);
3775
4293
  return resp;
3776
4294
  }
4295
+ // ---------------------------------------------------------------------------
4296
+ // Team invites + member management (Phase 4)
4297
+ // ---------------------------------------------------------------------------
4298
+ async function handleInviteTeammate(input) {
4299
+ const cfg = loadConfig();
4300
+ const picked = pickProject(cfg, input.project_id);
4301
+ if (!picked) {
4302
+ return {
4303
+ error: {
4304
+ code: "no_project",
4305
+ message: "No project_id given and no default project set.",
4306
+ next_action: "Pass project_id, or call agentry_create_project.",
4307
+ },
4308
+ };
4309
+ }
4310
+ const body = { role: input.role };
4311
+ if (input.email)
4312
+ body.email = input.email;
4313
+ return await api.inviteTeammate(cfg, picked.id, body);
4314
+ }
4315
+ async function handleListMembers(projectId) {
4316
+ const cfg = loadConfig();
4317
+ const picked = pickProject(cfg, projectId);
4318
+ if (!picked) {
4319
+ return {
4320
+ error: {
4321
+ code: "no_project",
4322
+ message: "No project_id given and no default project set.",
4323
+ next_action: "Pass project_id.",
4324
+ },
4325
+ };
4326
+ }
4327
+ return await api.listMembers(cfg, picked.id);
4328
+ }
4329
+ async function handleRemoveMember(input) {
4330
+ const cfg = loadConfig();
4331
+ const picked = pickProject(cfg, input.project_id);
4332
+ if (!picked) {
4333
+ return {
4334
+ error: {
4335
+ code: "no_project",
4336
+ message: "No project_id given and no default project set.",
4337
+ next_action: "Pass project_id.",
4338
+ },
4339
+ };
4340
+ }
4341
+ if (!input.user_id) {
4342
+ return {
4343
+ error: {
4344
+ code: "invalid_payload",
4345
+ message: "user_id is required.",
4346
+ next_action: "Call agentry_list_members to find the user_id to remove.",
4347
+ },
4348
+ };
4349
+ }
4350
+ return await api.removeMember(cfg, picked.id, input.user_id);
4351
+ }
3777
4352
  //# sourceMappingURL=tools.js.map