@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/README.md +1 -1
- package/dist/api.d.ts +83 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +25 -2
- package/dist/api.js.map +1 -1
- package/dist/friction-tracker.d.ts +77 -0
- package/dist/friction-tracker.d.ts.map +1 -0
- package/dist/friction-tracker.js +202 -0
- package/dist/friction-tracker.js.map +1 -0
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +14 -6
- package/dist/onboarding.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +625 -50
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
"
|
|
24
|
-
"
|
|
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
|
|
610
|
-
"
|
|
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
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
983
|
-
"
|
|
984
|
-
"agentry_install_guide. " +
|
|
985
|
-
"
|
|
986
|
-
"
|
|
987
|
-
"
|
|
988
|
-
"
|
|
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
|
|
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;
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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}
|
|
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
|
-
|
|
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("
|
|
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
|
-
"(
|
|
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
|
|
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:
|
|
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:
|
|
3335
|
-
?
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
.join("
|
|
3341
|
-
|
|
3342
|
-
|
|
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
|