mergen-server 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +57 -0
- package/README.md +96 -0
- package/dist/__stubs__/calibration.js +471 -0
- package/dist/__stubs__/causal.js +601 -0
- package/dist/__stubs__/closed-source.js +150 -0
- package/dist/__stubs__/license.js +35 -0
- package/dist/__stubs__/plans.js +197 -0
- package/dist/app.js +467 -0
- package/dist/cli.js +250 -0
- package/dist/commands/agent-identity.js +79 -0
- package/dist/commands/gate.js +107 -0
- package/dist/commands/github.js +632 -0
- package/dist/commands/incident.js +644 -0
- package/dist/commands/policy.js +710 -0
- package/dist/commands/scan.js +243 -0
- package/dist/commands/setup.js +1218 -0
- package/dist/commands/shared.js +109 -0
- package/dist/commands/team.js +809 -0
- package/dist/datadog/blame-attribution.js +228 -0
- package/dist/datadog/client.js +159 -0
- package/dist/datadog/compactor.js +169 -0
- package/dist/datadog/fingerprinter.js +34 -0
- package/dist/datadog/incident-state.js +21 -0
- package/dist/datadog/line-matcher.js +56 -0
- package/dist/datadog/memory-store.js +399 -0
- package/dist/datadog/otel-trace.js +67 -0
- package/dist/index.js +505 -0
- package/dist/intelligence/action-risk.js +132 -0
- package/dist/intelligence/activity-feed.js +40 -0
- package/dist/intelligence/agent-identity.js +115 -0
- package/dist/intelligence/agent-pipeline.js +329 -0
- package/dist/intelligence/agent-profiles.js +81 -0
- package/dist/intelligence/ai-commit.js +16 -0
- package/dist/intelligence/approval-analytics.js +42 -0
- package/dist/intelligence/approval-events.js +5 -0
- package/dist/intelligence/arch-boundaries.js +94 -0
- package/dist/intelligence/arch-graph.js +117 -0
- package/dist/intelligence/autonomy.js +397 -0
- package/dist/intelligence/azure-broker.js +57 -0
- package/dist/intelligence/azure-proxy-sessions.js +49 -0
- package/dist/intelligence/baseline.js +1 -0
- package/dist/intelligence/behavior-baseline.js +134 -0
- package/dist/intelligence/billing.js +1 -0
- package/dist/intelligence/blast-radius.js +224 -0
- package/dist/intelligence/bypass.js +157 -0
- package/dist/intelligence/calibration-classifier.js +77 -0
- package/dist/intelligence/calibration-git-sync.js +74 -0
- package/dist/intelligence/calibration.js +1 -0
- package/dist/intelligence/cascade-detector.js +58 -0
- package/dist/intelligence/case-study-generator.js +126 -0
- package/dist/intelligence/causal-graph.js +126 -0
- package/dist/intelligence/causal.js +1 -0
- package/dist/intelligence/change-risk.js +102 -0
- package/dist/intelligence/compliance-report.js +134 -0
- package/dist/intelligence/confidence-report.js +85 -0
- package/dist/intelligence/corpus-to-policy.js +140 -0
- package/dist/intelligence/credential-broker.js +112 -0
- package/dist/intelligence/credential-misuse.js +39 -0
- package/dist/intelligence/debug-sessions.js +108 -0
- package/dist/intelligence/decision-escalation.js +36 -0
- package/dist/intelligence/decision-memory.js +47 -0
- package/dist/intelligence/decision-similarity.js +85 -0
- package/dist/intelligence/default-safety-tests.js +53 -0
- package/dist/intelligence/degradation-watcher.js +108 -0
- package/dist/intelligence/detector-plugins.js +70 -0
- package/dist/intelligence/detectors.js +1 -0
- package/dist/intelligence/device-auth.js +138 -0
- package/dist/intelligence/diff-size.js +42 -0
- package/dist/intelligence/ebpf-verify.js +45 -0
- package/dist/intelligence/enterprise-policy-engine.js +1122 -0
- package/dist/intelligence/error-fingerprint.js +1 -0
- package/dist/intelligence/execution-gate.js +100 -0
- package/dist/intelligence/execution-mode.js +22 -0
- package/dist/intelligence/gate-analytics.js +256 -0
- package/dist/intelligence/gate-decision.js +64 -0
- package/dist/intelligence/gcp-broker.js +50 -0
- package/dist/intelligence/git-adr-sync.js +207 -0
- package/dist/intelligence/hitl-hold.js +460 -0
- package/dist/intelligence/hypothesis-history.js +93 -0
- package/dist/intelligence/idp-identity.js +83 -0
- package/dist/intelligence/impl-critic.js +216 -0
- package/dist/intelligence/incident-autopilot.js +622 -0
- package/dist/intelligence/incident-replay.js +162 -0
- package/dist/intelligence/incident-result-cache.js +33 -0
- package/dist/intelligence/incident-similarity.js +58 -0
- package/dist/intelligence/infra-detectors.js +312 -0
- package/dist/intelligence/license.js +185 -0
- package/dist/intelligence/llm-spokesperson.js +62 -0
- package/dist/intelligence/mcp-prompts.js +320 -0
- package/dist/intelligence/mcp-resources.js +171 -0
- package/dist/intelligence/normalize.js +108 -0
- package/dist/intelligence/notifications.js +83 -0
- package/dist/intelligence/oidc-issuer.js +87 -0
- package/dist/intelligence/override-corpus.js +597 -0
- package/dist/intelligence/planning-gate.js +84 -0
- package/dist/intelligence/plans.js +223 -0
- package/dist/intelligence/platt-scaling.js +164 -0
- package/dist/intelligence/policy-proposals.js +84 -0
- package/dist/intelligence/policy-suggester.js +161 -0
- package/dist/intelligence/policy-sync.js +103 -0
- package/dist/intelligence/policy-test-runner.js +35 -0
- package/dist/intelligence/postmortem-parser.js +181 -0
- package/dist/intelligence/postmortem-retrieval.js +224 -0
- package/dist/intelligence/postmortem-store.js +596 -0
- package/dist/intelligence/pr-commenter.js +81 -0
- package/dist/intelligence/pr-policy-gate.js +38 -0
- package/dist/intelligence/pr-shadow-analyzer.js +229 -0
- package/dist/intelligence/process-tree.js +108 -0
- package/dist/intelligence/prompts.js +1 -0
- package/dist/intelligence/replay.js +38 -0
- package/dist/intelligence/repro-steps.js +1 -0
- package/dist/intelligence/resource-extractors.js +0 -0
- package/dist/intelligence/resource-file-context.js +138 -0
- package/dist/intelligence/risk-score.js +22 -0
- package/dist/intelligence/rollback.js +137 -0
- package/dist/intelligence/runbook-updater.js +143 -0
- package/dist/intelligence/sandbox.js +194 -0
- package/dist/intelligence/script-trust.js +104 -0
- package/dist/intelligence/session-metrics.js +67 -0
- package/dist/intelligence/session-threat-tracker.js +231 -0
- package/dist/intelligence/shadow-digest-cron.js +81 -0
- package/dist/intelligence/shadow-log.js +284 -0
- package/dist/intelligence/shell-ast.js +145 -0
- package/dist/intelligence/siem-forward.js +104 -0
- package/dist/intelligence/slack-digest.js +151 -0
- package/dist/intelligence/slack-override-loop.js +217 -0
- package/dist/intelligence/slack-routing.js +67 -0
- package/dist/intelligence/slack.js +900 -0
- package/dist/intelligence/sql-ast.js +31 -0
- package/dist/intelligence/team.js +1 -0
- package/dist/intelligence/telemetry.js +76 -0
- package/dist/intelligence/threshold-optimizer.js +75 -0
- package/dist/intelligence/token-budget.js +66 -0
- package/dist/intelligence/tool-guard.js +708 -0
- package/dist/intelligence/tool-manifest 2.js +74 -0
- package/dist/intelligence/tool-manifest.js +125 -0
- package/dist/intelligence/tools-analysis.js +857 -0
- package/dist/intelligence/tools-arch.js +152 -0
- package/dist/intelligence/tools-autonomy.js +495 -0
- package/dist/intelligence/tools-blast-radius.js +140 -0
- package/dist/intelligence/tools-browser.js +431 -0
- package/dist/intelligence/tools-change-timeline.js +154 -0
- package/dist/intelligence/tools-credentials.js +117 -0
- package/dist/intelligence/tools-datadog.js +233 -0
- package/dist/intelligence/tools-debug-sessions.js +209 -0
- package/dist/intelligence/tools-discovery.js +284 -0
- package/dist/intelligence/tools-infra.js +612 -0
- package/dist/intelligence/tools-intent.js +128 -0
- package/dist/intelligence/tools-memory.js +514 -0
- package/dist/intelligence/tools-runbook.js +951 -0
- package/dist/intelligence/tools-sessions.js +107 -0
- package/dist/intelligence/tools-state.js +232 -0
- package/dist/intelligence/tools-utility.js +430 -0
- package/dist/intelligence/tools-validate.js +215 -0
- package/dist/intelligence/tools.js +1 -0
- package/dist/intelligence/trace-context.js +11 -0
- package/dist/intelligence/unclassified-clusters.js +87 -0
- package/dist/intelligence/usage.js +195 -0
- package/dist/routes/active-authors.js +63 -0
- package/dist/routes/activity-feed.js +21 -0
- package/dist/routes/adr.js +117 -0
- package/dist/routes/agent-activity.js +184 -0
- package/dist/routes/agent-blunders.js +163 -0
- package/dist/routes/agents.js +175 -0
- package/dist/routes/api-keys.js +65 -0
- package/dist/routes/arch.js +75 -0
- package/dist/routes/audit-export.js +93 -0
- package/dist/routes/billing-dashboard.js +158 -0
- package/dist/routes/billing-outcome.js +77 -0
- package/dist/routes/calibration.js +305 -0
- package/dist/routes/ci-gate-history.js +20 -0
- package/dist/routes/ci-gate.js +232 -0
- package/dist/routes/ci.js +293 -0
- package/dist/routes/compliance-report.js +24 -0
- package/dist/routes/confidence.js +30 -0
- package/dist/routes/credentials.js +119 -0
- package/dist/routes/dashboard.js +1407 -0
- package/dist/routes/decision-memory.js +42 -0
- package/dist/routes/demo.js +844 -0
- package/dist/routes/device-auth-stub.js +173 -0
- package/dist/routes/explain-why.js +39 -0
- package/dist/routes/gate-analytics.js +213 -0
- package/dist/routes/gate.js +28 -0
- package/dist/routes/github-webhook.js +364 -0
- package/dist/routes/habituation.js +30 -0
- package/dist/routes/health-integrations.js +243 -0
- package/dist/routes/heartbeats.js +45 -0
- package/dist/routes/hitl.js +364 -0
- package/dist/routes/impact-report.js +645 -0
- package/dist/routes/incident-webhook.js +63 -0
- package/dist/routes/incidents.js +366 -0
- package/dist/routes/layers.js +62 -0
- package/dist/routes/license.js +187 -0
- package/dist/routes/oidc-issuer.js +30 -0
- package/dist/routes/onboarding.js +170 -0
- package/dist/routes/otel.js +67 -0
- package/dist/routes/otlp-receiver.js +179 -0
- package/dist/routes/overrides.js +241 -0
- package/dist/routes/pagerduty.js +258 -0
- package/dist/routes/policies.js +809 -0
- package/dist/routes/policy-nl.js +169 -0
- package/dist/routes/postmortem.js +102 -0
- package/dist/routes/pr-shadow.js +47 -0
- package/dist/routes/rbac.js +61 -0
- package/dist/routes/replay.js +17 -0
- package/dist/routes/risk-report.js +164 -0
- package/dist/routes/runbooks.js +125 -0
- package/dist/routes/safety-test.js +174 -0
- package/dist/routes/sdk.js +222 -0
- package/dist/routes/sensor.js +819 -0
- package/dist/routes/sentry.js +140 -0
- package/dist/routes/sessions.js +43 -0
- package/dist/routes/setup-ui.js +594 -0
- package/dist/routes/shadow-report.js +107 -0
- package/dist/routes/slack-routing.js +171 -0
- package/dist/routes/team-usage.js +71 -0
- package/dist/routes/telemetry.js +28 -0
- package/dist/routes/tenants.js +199 -0
- package/dist/routes/tickets.js +167 -0
- package/dist/routes/validate.js +13 -0
- package/dist/routes/war-room.js +113 -0
- package/dist/scripts/check-arch.js +22 -0
- package/dist/seeds/community-corpus.js +176 -0
- package/dist/seeds/corpus.js +1224 -0
- package/dist/sensor/action-ledger.js +327 -0
- package/dist/sensor/adr-store.js +140 -0
- package/dist/sensor/agent-blunder-store.js +370 -0
- package/dist/sensor/agent-context-store.js +225 -0
- package/dist/sensor/agent-memory-store.js +231 -0
- package/dist/sensor/audit-fetch.js +23 -0
- package/dist/sensor/audit-log.js +273 -0
- package/dist/sensor/buffer-schemas.js +225 -0
- package/dist/sensor/buffer.js +599 -0
- package/dist/sensor/bypass-tracker.js +93 -0
- package/dist/sensor/ci-gate-history.js +70 -0
- package/dist/sensor/cloud-auth.js +183 -0
- package/dist/sensor/commit-context-store.js +235 -0
- package/dist/sensor/docker-log-stream.js +164 -0
- package/dist/sensor/docker-monitor.js +122 -0
- package/dist/sensor/extended-buffer.js +46 -0
- package/dist/sensor/feedback-token.js +41 -0
- package/dist/sensor/file-lock.js +6 -0
- package/dist/sensor/fs-watcher.js +106 -0
- package/dist/sensor/gate-heartbeat.js +122 -0
- package/dist/sensor/git-suspect.js +136 -0
- package/dist/sensor/habituation-store.js +77 -0
- package/dist/sensor/heartbeat-monitor.js +133 -0
- package/dist/sensor/incident-store.js +340 -0
- package/dist/sensor/infra-normalizer.js +513 -0
- package/dist/sensor/ingest.js +339 -0
- package/dist/sensor/jest-reporter.js +52 -0
- package/dist/sensor/k8s-events.js +132 -0
- package/dist/sensor/layer2-store.js +111 -0
- package/dist/sensor/layer3-store.js +230 -0
- package/dist/sensor/layer4-store.js +147 -0
- package/dist/sensor/logger.js +13 -0
- package/dist/sensor/otel-exporter.js +161 -0
- package/dist/sensor/paths.js +71 -0
- package/dist/sensor/policy-history.js +147 -0
- package/dist/sensor/pr-shadow-store.js +83 -0
- package/dist/sensor/process-watcher.js +162 -0
- package/dist/sensor/rbac.js +92 -0
- package/dist/sensor/redact.js +114 -0
- package/dist/sensor/redis-store.js +191 -0
- package/dist/sensor/route-reachability.js +64 -0
- package/dist/sensor/security-utils.js +18 -0
- package/dist/sensor/service-graph.js +170 -0
- package/dist/sensor/service-topology.js +227 -0
- package/dist/sensor/session-history.js +74 -0
- package/dist/sensor/session-persist.js +38 -0
- package/dist/sensor/shadow-promote.js +122 -0
- package/dist/sensor/sourcemap.js +281 -0
- package/dist/sensor/sqlite-store.js +205 -0
- package/dist/sensor/sso.js +164 -0
- package/dist/sensor/vitest-reporter.js +65 -0
- package/dist/sensor/watcher.js +48 -0
- package/dist/storage/interfaces.js +0 -0
- package/dist/storage/pg/pg-action-ledger.js +138 -0
- package/dist/storage/pg/pg-approval-store.js +107 -0
- package/dist/storage/pg/pg-blunder-store.js +193 -0
- package/dist/storage/pg/pg-ci-gate-history.js +83 -0
- package/dist/storage/pg/pg-client.js +24 -0
- package/dist/storage/pg/pg-event-store.js +63 -0
- package/dist/storage/pg/pg-incident-store.js +190 -0
- package/dist/storage/pg/pg-migrations.js +26 -0
- package/dist/storage/pg/pg-override-corpus.js +447 -0
- package/dist/storage/pg/pg-shadow-log.js +129 -0
- package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
- package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
- package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
- package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
- package/dist/storage/sqlite/sqlite-event-store.js +29 -0
- package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
- package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
- package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
- package/dist/storage/store-factory.js +55 -0
- package/dist/storage/store-registry.js +31 -0
- package/dist/update-checker.js +107 -0
- package/dist/workers/autopilot-worker.js +28 -0
- package/dist/workers/notification-worker.js +28 -0
- package/dist/workers/queues.js +58 -0
- package/dist/workers/validation-worker.js +24 -0
- package/dist/workers/worker-registry.js +18 -0
- package/package.json +123 -0
- package/sdk/mergen-inject.js +260 -0
- package/sdk/node.js +313 -0
- package/sdk/vite-plugin.ts +57 -0
- package/sdk/webpack-plugin.js +106 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { store } from "../sensor/buffer.js";
|
|
3
|
+
import { memoryStore } from "../datadog/memory-store.js";
|
|
4
|
+
import { trackCall, withTierGate } from "./tools-state.js";
|
|
5
|
+
import { getTierForTool } from "./tool-manifest.js";
|
|
6
|
+
function registerBlastRadiusTools(server) {
|
|
7
|
+
_registerBlastRadius(server);
|
|
8
|
+
_registerAttributionAccuracy(server);
|
|
9
|
+
}
|
|
10
|
+
function _registerBlastRadius(server) {
|
|
11
|
+
server.tool(
|
|
12
|
+
"get_blast_radius",
|
|
13
|
+
'Quantify the user impact of current errors: unique sessions affected, user count, browser/OS segments, first-seen time, and the likely causal deploy. Use this to answer "how many users are broken right now?" during an incident.',
|
|
14
|
+
{
|
|
15
|
+
since: z.number().optional().describe("Unix ms \u2014 only count errors after this timestamp. Omit to use all buffered events."),
|
|
16
|
+
error_pattern: z.string().optional().describe("Regex pattern to filter errors by message text (case-insensitive). Omit for all errors.")
|
|
17
|
+
},
|
|
18
|
+
withTierGate(getTierForTool("get_blast_radius"), async ({ since, error_pattern }) => {
|
|
19
|
+
const report = store.getBlastRadius({
|
|
20
|
+
since,
|
|
21
|
+
errorPattern: error_pattern
|
|
22
|
+
});
|
|
23
|
+
const text = formatBlastRadius(report, { since, errorPattern: error_pattern });
|
|
24
|
+
return { content: [{ type: "text", text }] };
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
function formatBlastRadius(r, opts) {
|
|
29
|
+
if (r.errorCount === 0) {
|
|
30
|
+
const window = opts.since ? `since ${new Date(opts.since).toISOString()}` : "in the current buffer";
|
|
31
|
+
const pattern = opts.errorPattern ? ` matching \`${opts.errorPattern}\`` : "";
|
|
32
|
+
return `## Blast Radius
|
|
33
|
+
|
|
34
|
+
No errors found${pattern} ${window}.`;
|
|
35
|
+
}
|
|
36
|
+
const lines = ["## Blast Radius"];
|
|
37
|
+
const hasSessionIds = r.affectedSessions > 0;
|
|
38
|
+
const hasUserIds = r.affectedUsers > 0;
|
|
39
|
+
if (hasSessionIds) {
|
|
40
|
+
const dedupNote = r.returningUserSessions > 0 ? ` (~${r.affectedSessions - r.returningUserSessions} deduplicated \u2014 ${r.returningUserSessions} extra session${r.returningUserSessions !== 1 ? "s" : ""} from returning users)` : "";
|
|
41
|
+
lines.push(`**${r.affectedSessions} session${r.affectedSessions !== 1 ? "s" : ""} affected**` + (hasUserIds ? ` (${r.affectedUsers} authenticated user${r.affectedUsers !== 1 ? "s" : ""})` : "") + dedupNote);
|
|
42
|
+
} else {
|
|
43
|
+
lines.push(`**${r.errorCount} error event${r.errorCount !== 1 ? "s" : ""}** (no session IDs \u2014 count is occurrences, not unique users)`);
|
|
44
|
+
}
|
|
45
|
+
if (r.firstSeenAt) {
|
|
46
|
+
const firstStr = new Date(r.firstSeenAt).toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
47
|
+
const duration = r.durationMs !== null ? ` \u2014 active for **${formatDuration(r.durationMs)}**` : "";
|
|
48
|
+
lines.push(`First seen: **${firstStr}**${duration}`);
|
|
49
|
+
}
|
|
50
|
+
const browsers = Object.entries(r.browserSegments).sort((a, b) => b[1] - a[1]);
|
|
51
|
+
const oses = Object.entries(r.osSegments).sort((a, b) => b[1] - a[1]);
|
|
52
|
+
if (browsers.length > 0) {
|
|
53
|
+
const browserStr = browsers.map(([b, n]) => `${b}: ${n}`).join(", ");
|
|
54
|
+
lines.push(`Browsers: ${browserStr}`);
|
|
55
|
+
}
|
|
56
|
+
if (oses.length > 0) {
|
|
57
|
+
const osStr = oses.map(([o, n]) => `${o}: ${n}`).join(", ");
|
|
58
|
+
lines.push(`OS: ${osStr}`);
|
|
59
|
+
}
|
|
60
|
+
if (browsers.length === 0 && oses.length === 0) {
|
|
61
|
+
lines.push("_No userAgent data \u2014 update extension or SDK to enable browser segmentation_");
|
|
62
|
+
}
|
|
63
|
+
if (r.correlatedDeploy) {
|
|
64
|
+
lines.push(`Likely causal deploy: **\`${r.correlatedDeploy}\`** _(most recent successful deploy before first error \u2014 hypothesis, not confirmed)_`);
|
|
65
|
+
} else {
|
|
66
|
+
lines.push("No deploy event found prior to first error \u2014 check CI integration.");
|
|
67
|
+
}
|
|
68
|
+
if (r.topErrors.length > 0) {
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("### Top Errors");
|
|
71
|
+
for (const { message, count } of r.topErrors) {
|
|
72
|
+
lines.push(`- \`${message}\` \u2014 ${count}\xD7`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!hasSessionIds) {
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("> **Note:** Session IDs not present. Numbers reflect event count, not unique user count.");
|
|
78
|
+
lines.push("> Ensure the Mergen extension is updated (v1.1+) or the production SDK is injected.");
|
|
79
|
+
}
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
function formatDuration(ms) {
|
|
83
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
84
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
85
|
+
return `${(ms / 36e5).toFixed(1)}h`;
|
|
86
|
+
}
|
|
87
|
+
function _registerAttributionAccuracy(server) {
|
|
88
|
+
server.tool(
|
|
89
|
+
"get_attribution_accuracy",
|
|
90
|
+
"Show the historical accuracy of Mergen's causal blame attribution. Displays how often the attributed deploy SHA matched the actual fix PR SHA, broken down by confidence band. Use this to validate whether attribution scores are trustworthy for your codebase.",
|
|
91
|
+
{},
|
|
92
|
+
withTierGate(getTierForTool("get_attribution_accuracy"), async () => {
|
|
93
|
+
trackCall("get_attribution_accuracy");
|
|
94
|
+
const all = memoryStore.findSimilar("", 200);
|
|
95
|
+
const resolved = all.filter(
|
|
96
|
+
(r) => r.attributionConfidence !== null && r.attributionValidated !== null
|
|
97
|
+
);
|
|
98
|
+
if (resolved.length === 0) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: "## Attribution Accuracy\n\nNo validated attributions yet. Attribution accuracy is recorded automatically when incidents resolve with a correlated GitHub PR.\n\nAccuracy data accumulates as incidents are resolved."
|
|
103
|
+
}]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const bands = [
|
|
107
|
+
{ label: "HIGH (\u22650.80)", min: 0.8, max: 1.01 },
|
|
108
|
+
{ label: "MEDIUM (0.60\u20130.79)", min: 0.6, max: 0.8 },
|
|
109
|
+
{ label: "LOW (<0.60)", min: 0, max: 0.6 }
|
|
110
|
+
];
|
|
111
|
+
const lines = ["## Attribution Accuracy", ""];
|
|
112
|
+
let totalCorrect = 0;
|
|
113
|
+
let totalValidated = 0;
|
|
114
|
+
for (const band of bands) {
|
|
115
|
+
const inBand = resolved.filter(
|
|
116
|
+
(r) => r.attributionConfidence >= band.min && r.attributionConfidence < band.max
|
|
117
|
+
);
|
|
118
|
+
if (inBand.length === 0) continue;
|
|
119
|
+
const correct = inBand.filter((r) => r.attributionValidated === 1).length;
|
|
120
|
+
const pct = Math.round(correct / inBand.length * 100);
|
|
121
|
+
totalCorrect += correct;
|
|
122
|
+
totalValidated += inBand.length;
|
|
123
|
+
lines.push(`**${band.label}:** ${correct}/${inBand.length} correct (${pct}%)`);
|
|
124
|
+
}
|
|
125
|
+
const overallPct = Math.round(totalCorrect / totalValidated * 100);
|
|
126
|
+
lines.push("", `**Overall:** ${totalCorrect}/${totalValidated} (${overallPct}%)`);
|
|
127
|
+
if (overallPct >= 80) {
|
|
128
|
+
lines.push("", "\u2705 Attribution accuracy is strong \u2014 HIGH confidence scores are reliable enough for autonomous action.");
|
|
129
|
+
} else if (overallPct >= 60) {
|
|
130
|
+
lines.push("", "\u26A0\uFE0F Attribution accuracy is moderate \u2014 use HIGH confidence for suggestions, require human approval for actions.");
|
|
131
|
+
} else {
|
|
132
|
+
lines.push("", "\u274C Attribution accuracy is low \u2014 review signal weights. Possible causes: missing buildSha, git not available, or deploys not posted to Mergen.");
|
|
133
|
+
}
|
|
134
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
registerBlastRadiusTools
|
|
140
|
+
};
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { store } from "../sensor/buffer.js";
|
|
3
|
+
import { serviceTopology } from "../sensor/service-topology.js";
|
|
4
|
+
import { truncateToTokenBudget } from "./token-budget.js";
|
|
5
|
+
import { getActivePlanId } from "./license.js";
|
|
6
|
+
import { getPlan } from "./plans.js";
|
|
7
|
+
import { trackCall } from "./tools-state.js";
|
|
8
|
+
import logger from "../sensor/logger.js";
|
|
9
|
+
function registerBrowserTools(server) {
|
|
10
|
+
server.registerTool(
|
|
11
|
+
"get_recent_logs",
|
|
12
|
+
{
|
|
13
|
+
description: 'Retrieves runtime events from the event buffer \u2014 recent console events captured from the runtime. Default min_severity is WARN \u2014 console.log spam is filtered out automatically. Use exclude_patterns to suppress known noise (e.g. ["HMR", "\\[vite\\]", "\\[Fast Refresh\\]"]). Use this to drill into specific errors after get_unified_timeline has shown the cross-signal picture. Always lead response with: total errors, total warnings, most critical first.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
limit: z.number().int().min(1).max(200).optional().describe("Max events to return (default 50)"),
|
|
16
|
+
level: z.enum(["error", "warn", "log"]).optional().describe("Filter by log level"),
|
|
17
|
+
since: z.number().int().optional().describe("Only return events after this Unix timestamp in ms"),
|
|
18
|
+
min_severity: z.enum(["log", "warn", "error"]).optional().describe("Minimum severity level (default: warn). Filters out low-priority noise like console.log spam."),
|
|
19
|
+
exclude_patterns: z.array(z.string()).optional().describe('Regex patterns to exclude, e.g. ["HMR", "webpack", "vite", "\\[Fast Refresh\\]"]. Case-insensitive.'),
|
|
20
|
+
max_tokens: z.number().int().min(100).max(1e4).optional().describe("Soft token limit for response. Will truncate if exceeded.")
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
async ({ limit, level, since, min_severity, exclude_patterns, max_tokens }) => {
|
|
24
|
+
let events = store.getLogs(limit ?? 50, level, since);
|
|
25
|
+
const severityThreshold = min_severity || "warn";
|
|
26
|
+
const severityOrder = { log: 0, warn: 1, error: 2 };
|
|
27
|
+
events = events.filter((e) => severityOrder[e.level] >= severityOrder[severityThreshold]);
|
|
28
|
+
if (exclude_patterns && exclude_patterns.length > 0) {
|
|
29
|
+
try {
|
|
30
|
+
const regexes = exclude_patterns.map((p) => new RegExp(p, "i"));
|
|
31
|
+
events = events.filter((e) => {
|
|
32
|
+
const message = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
33
|
+
return !regexes.some((re) => re.test(message));
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.warn({ err, exclude_patterns }, "invalid regex in exclude_patterns");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (events.length === 0) {
|
|
40
|
+
return { content: [{ type: "text", text: "No console events match filters." }] };
|
|
41
|
+
}
|
|
42
|
+
const errors = events.filter((e) => e.level === "error").length;
|
|
43
|
+
const warns = events.filter((e) => e.level === "warn").length;
|
|
44
|
+
const header = `Buffer: ${store.size()} total events. Showing ${events.length} \u2014 ${errors} error(s), ${warns} warning(s).
|
|
45
|
+
|
|
46
|
+
`;
|
|
47
|
+
const lines = events.map((e) => {
|
|
48
|
+
const ts = new Date(e.timestamp).toISOString();
|
|
49
|
+
const args = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
50
|
+
const stack = e.stack ? `
|
|
51
|
+
Stack: ${e.stack}` : "";
|
|
52
|
+
const blame = e.gitSuspect ? `
|
|
53
|
+
Git: ${e.gitSuspect.sha.slice(0, 7)} by ${e.gitSuspect.author} \u2014 "${e.gitSuspect.summary}"` : "";
|
|
54
|
+
return `[${ts}] [${e.level.toUpperCase()}] ${args}${stack}${blame}`;
|
|
55
|
+
});
|
|
56
|
+
const { result, truncated, omitted, estimatedTokens } = truncateToTokenBudget(lines, max_tokens, "\n");
|
|
57
|
+
if (truncated) logger.info({ tool: "get_recent_logs", omitted, estimatedTokens }, "response truncated");
|
|
58
|
+
return { content: [{ type: "text", text: header + result }] };
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
server.registerTool(
|
|
62
|
+
"get_network_activity",
|
|
63
|
+
{
|
|
64
|
+
description: "Returns intercepted fetch/XHR events. 404 = missing asset or API call; 500 = critical server error. Use max_tokens to control response size.",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
limit: z.number().int().min(1).max(200).optional().describe("Max events to return (default 50)"),
|
|
67
|
+
status_filter: z.number().int().optional().describe("Filter to a specific HTTP status code (e.g. 404, 500)"),
|
|
68
|
+
since: z.number().int().optional().describe("Only return events after this Unix timestamp in ms"),
|
|
69
|
+
max_tokens: z.number().int().min(100).max(1e4).optional().describe("Soft token limit for response. Will truncate if exceeded.")
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async ({ limit, status_filter, since, max_tokens }) => {
|
|
73
|
+
const events = store.getNetwork(limit ?? 50, status_filter, since);
|
|
74
|
+
if (events.length === 0) {
|
|
75
|
+
const msg = status_filter ? `No network events with status ${status_filter}.` : "No network events in buffer.";
|
|
76
|
+
return { content: [{ type: "text", text: msg }] };
|
|
77
|
+
}
|
|
78
|
+
const lines = events.map((e) => {
|
|
79
|
+
const ts = new Date(e.timestamp).toISOString();
|
|
80
|
+
const flag = e.status >= 500 ? " [CRITICAL]" : e.status >= 400 ? " [ERROR]" : "";
|
|
81
|
+
const body = e.error ? ` | error: ${e.error}` : e.responseBody ? ` | response: ${JSON.stringify(e.responseBody).slice(0, 200)}` : "";
|
|
82
|
+
const trace = e.traceId ? ` | trace: ${e.traceId}` : "";
|
|
83
|
+
const tracestate = e.tracestate ? ` | tracestate: ${e.tracestate}` : "";
|
|
84
|
+
const baggage = e.baggage ? ` | baggage: ${JSON.stringify(e.baggage)}` : "";
|
|
85
|
+
const userId = e.userId ? ` | user: ${e.userId}` : "";
|
|
86
|
+
return `[${ts}] ${e.method} ${e.url} \u2192 ${e.status} ${e.statusText} (${e.duration}ms)${flag}${body}${trace}${tracestate}${baggage}${userId}`;
|
|
87
|
+
});
|
|
88
|
+
const { result, truncated, omitted, estimatedTokens } = truncateToTokenBudget(lines, max_tokens, "\n");
|
|
89
|
+
if (truncated) logger.info({ tool: "get_network_activity", omitted, estimatedTokens }, "response truncated");
|
|
90
|
+
return { content: [{ type: "text", text: result }] };
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
server.registerTool(
|
|
94
|
+
"get_dom_context",
|
|
95
|
+
{
|
|
96
|
+
description: "Returns DOM and storage snapshots captured at the exact millisecond of each console.error. Shows the page URL, title, focused element, React/Vue component, localStorage, and sessionStorage. Use this to understand what the user was doing and what state the app was in when an error fired. Use focused_element_only=true to minimize token usage.",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max snapshots to return (default 10)"),
|
|
99
|
+
since: z.number().int().optional().describe("Only return snapshots after this Unix timestamp in ms"),
|
|
100
|
+
focused_element_only: z.boolean().optional().describe("If true, only return focused element and component (skip storage). Reduces token usage by ~80%."),
|
|
101
|
+
max_tokens: z.number().int().min(100).max(1e4).optional().describe("Soft token limit for response. Will truncate if exceeded.")
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
async ({ limit, since, focused_element_only, max_tokens }) => {
|
|
105
|
+
const snapshots = store.getContext(limit ?? 10, since);
|
|
106
|
+
if (snapshots.length === 0) {
|
|
107
|
+
return { content: [{ type: "text", text: "No context snapshots yet. They are captured automatically on every console.error." }] };
|
|
108
|
+
}
|
|
109
|
+
const lines = snapshots.map((s) => {
|
|
110
|
+
const ts = new Date(s.timestamp).toISOString();
|
|
111
|
+
const parts = [
|
|
112
|
+
`[${ts}] ${s.url}`,
|
|
113
|
+
` Page: ${s.title}`
|
|
114
|
+
];
|
|
115
|
+
if (s.activeElement) parts.push(` Focused element: ${s.activeElement}`);
|
|
116
|
+
if (s.component) parts.push(` Component: ${s.component}`);
|
|
117
|
+
if (focused_element_only) return parts.join("\n");
|
|
118
|
+
const { full: ls, changed: lsChanged } = store.getLocalStorageDiff(s.localStorage, s.url);
|
|
119
|
+
const lsEntries = Object.entries(ls);
|
|
120
|
+
if (lsEntries.length > 0) {
|
|
121
|
+
const changedCount = lsChanged.size;
|
|
122
|
+
const header = changedCount > 0 ? ` localStorage (${lsEntries.length} keys, ${changedCount} changed):` : ` localStorage (${lsEntries.length} keys):`;
|
|
123
|
+
parts.push(header);
|
|
124
|
+
for (const [k, v] of lsEntries) {
|
|
125
|
+
const badge = lsChanged.has(k) ? "\u{1F504} " : " ";
|
|
126
|
+
parts.push(` ${badge}${k} = ${v}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const ssEntries = Object.entries(s.sessionStorage);
|
|
130
|
+
if (ssEntries.length > 0) {
|
|
131
|
+
const showing = ssEntries.slice(0, 10);
|
|
132
|
+
const header = ssEntries.length > 10 ? ` sessionStorage (showing 10 of ${ssEntries.length}):` : ` sessionStorage (${ssEntries.length} keys):`;
|
|
133
|
+
parts.push(header);
|
|
134
|
+
for (const [k, v] of showing) {
|
|
135
|
+
parts.push(` ${k} = ${v}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return parts.join("\n");
|
|
139
|
+
});
|
|
140
|
+
const { result, truncated, omitted, estimatedTokens } = truncateToTokenBudget(lines, max_tokens, "\n\n");
|
|
141
|
+
if (truncated) logger.info({ tool: "get_dom_context", omitted, estimatedTokens }, "response truncated");
|
|
142
|
+
return { content: [{ type: "text", text: result }] };
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
server.registerTool(
|
|
146
|
+
"get_websocket_activity",
|
|
147
|
+
{
|
|
148
|
+
description: "Returns WebSocket connection events with message frames. Use this to debug real-time features like chat, live dashboards, or multiplayer games. Shows connection status (open/closed/error), last sent/received frames, and connection duration.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
limit: z.number().int().min(1).max(200).optional().describe("Max connections to return (default 50)"),
|
|
151
|
+
connection_url: z.string().optional().describe('Filter by WebSocket URL (partial match, e.g. "wss://api.example.com")'),
|
|
152
|
+
since: z.number().int().optional().describe("Only return events after this Unix timestamp in ms"),
|
|
153
|
+
max_tokens: z.number().int().min(100).max(1e4).optional().describe("Soft token limit for response. Will truncate if exceeded.")
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
async ({ limit, connection_url, since, max_tokens }) => {
|
|
157
|
+
trackCall("get_websocket_activity");
|
|
158
|
+
if (!getPlan(getActivePlanId()).websocketInspection) {
|
|
159
|
+
return {
|
|
160
|
+
content: [{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: `\u26D4 **WebSocket Inspection** is a Pro feature.
|
|
163
|
+
|
|
164
|
+
Upgrade to **Pro ($29/mo)** to inspect WebSocket connections and message frames.
|
|
165
|
+
|
|
166
|
+
\u2192 https://mergen.app/pricing`
|
|
167
|
+
}],
|
|
168
|
+
isError: true
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const events = store.getWebSockets(limit ?? 50, connection_url, since);
|
|
172
|
+
if (events.length === 0) {
|
|
173
|
+
const msg = connection_url ? `No WebSocket activity for URL containing "${connection_url}".` : "No WebSocket activity in buffer.";
|
|
174
|
+
return { content: [{ type: "text", text: msg }] };
|
|
175
|
+
}
|
|
176
|
+
const lines = events.map((e) => {
|
|
177
|
+
const ts = new Date(e.timestamp).toISOString();
|
|
178
|
+
const parts = [
|
|
179
|
+
`[${ts}] WebSocket: ${e.url}`,
|
|
180
|
+
` Connection ID: ${e.connectionId}`,
|
|
181
|
+
` Status: ${e.status.toUpperCase()}`
|
|
182
|
+
];
|
|
183
|
+
if (e.code !== void 0) parts.push(` Close code: ${e.code}`);
|
|
184
|
+
if (e.reason) parts.push(` Close reason: ${e.reason}`);
|
|
185
|
+
if (e.error) parts.push(` Error: ${e.error}`);
|
|
186
|
+
if (e.frames && e.frames.length > 0) {
|
|
187
|
+
parts.push(` Frames captured: ${e.frames.length}`);
|
|
188
|
+
parts.push(` Recent frames:`);
|
|
189
|
+
const recentFrames = e.frames.slice(-5);
|
|
190
|
+
for (const frame of recentFrames) {
|
|
191
|
+
const direction = frame.direction === "sent" ? "\u2192" : "\u2190";
|
|
192
|
+
const frameTs = new Date(frame.timestamp).toISOString().split("T")[1].split(".")[0];
|
|
193
|
+
parts.push(` ${direction} [${frameTs}] ${frame.data}`);
|
|
194
|
+
}
|
|
195
|
+
if (e.frames.length > 5) parts.push(` ... (${e.frames.length - 5} more frames)`);
|
|
196
|
+
}
|
|
197
|
+
return parts.join("\n");
|
|
198
|
+
});
|
|
199
|
+
const { result, truncated, omitted, estimatedTokens } = truncateToTokenBudget(lines, max_tokens, "\n\n");
|
|
200
|
+
if (truncated) logger.info({ tool: "get_websocket_activity", omitted, estimatedTokens }, "response truncated");
|
|
201
|
+
return { content: [{ type: "text", text: result }] };
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
server.registerTool(
|
|
205
|
+
"get_sse_activity",
|
|
206
|
+
{
|
|
207
|
+
description: "Returns Server-Sent Events (EventSource) activity. Use this to debug real-time server push features. Shows connection status and recent messages.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
limit: z.number().int().min(1).max(200).optional().describe("Max connections to return (default 50)"),
|
|
210
|
+
connection_url: z.string().optional().describe("Filter by SSE URL (partial match)"),
|
|
211
|
+
since: z.number().int().optional().describe("Only return events after this Unix timestamp in ms"),
|
|
212
|
+
max_tokens: z.number().int().min(100).max(1e4).optional().describe("Soft token limit for response. Will truncate if exceeded.")
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
async ({ limit, connection_url, since, max_tokens }) => {
|
|
216
|
+
trackCall("get_sse_activity");
|
|
217
|
+
if (!getPlan(getActivePlanId()).websocketInspection) {
|
|
218
|
+
return {
|
|
219
|
+
content: [{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: `\u26D4 **SSE / EventSource Inspection** is a Pro feature.
|
|
222
|
+
|
|
223
|
+
Upgrade to **Pro ($29/mo)** to inspect Server-Sent Events streams.
|
|
224
|
+
|
|
225
|
+
\u2192 https://mergen.app/pricing`
|
|
226
|
+
}],
|
|
227
|
+
isError: true
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const events = store.getSSE(limit ?? 50, connection_url, since);
|
|
231
|
+
if (events.length === 0) {
|
|
232
|
+
const msg = connection_url ? `No SSE activity for URL containing "${connection_url}".` : "No SSE activity in buffer.";
|
|
233
|
+
return { content: [{ type: "text", text: msg }] };
|
|
234
|
+
}
|
|
235
|
+
const lines = events.map((e) => {
|
|
236
|
+
const ts = new Date(e.timestamp).toISOString();
|
|
237
|
+
const parts = [
|
|
238
|
+
`[${ts}] SSE: ${e.url}`,
|
|
239
|
+
` Connection ID: ${e.connectionId}`,
|
|
240
|
+
` Status: ${e.status.toUpperCase()}`
|
|
241
|
+
];
|
|
242
|
+
if (e.messages && e.messages.length > 0) {
|
|
243
|
+
parts.push(` Messages received: ${e.messages.length}`);
|
|
244
|
+
parts.push(` Recent messages:`);
|
|
245
|
+
const recentMessages = e.messages.slice(-5);
|
|
246
|
+
for (const msg of recentMessages) {
|
|
247
|
+
const msgTs = new Date(msg.timestamp).toISOString().split("T")[1].split(".")[0];
|
|
248
|
+
parts.push(` [${msgTs}] ${msg.data}`);
|
|
249
|
+
}
|
|
250
|
+
if (e.messages.length > 5) parts.push(` ... (${e.messages.length - 5} more messages)`);
|
|
251
|
+
}
|
|
252
|
+
return parts.join("\n");
|
|
253
|
+
});
|
|
254
|
+
const { result, truncated, omitted, estimatedTokens } = truncateToTokenBudget(lines, max_tokens, "\n\n");
|
|
255
|
+
if (truncated) logger.info({ tool: "get_sse_activity", omitted, estimatedTokens }, "response truncated");
|
|
256
|
+
return { content: [{ type: "text", text: result }] };
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
server.registerTool(
|
|
260
|
+
"get_component_tree",
|
|
261
|
+
{
|
|
262
|
+
description: '\u{1F333} PRO \xB7 Returns React or Vue component tree with props, state, and hooks. Use this to debug "component not re-rendering", "props not updating", or "infinite render loop" issues. Shows component hierarchy, current prop/state values, and hook dependencies.',
|
|
263
|
+
inputSchema: {
|
|
264
|
+
component_name: z.string().optional().describe('Optional: filter to a specific component name (e.g., "UserProfile", "LoginForm")'),
|
|
265
|
+
max_depth: z.number().int().min(1).max(10).optional().describe("Max tree depth to traverse (default: 5)"),
|
|
266
|
+
since: z.number().int().optional().describe("Only return component snapshots after this Unix timestamp in ms"),
|
|
267
|
+
max_tokens: z.number().int().min(100).max(1e4).optional().describe("Soft token limit for response. Will truncate if exceeded.")
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
async ({ component_name, max_depth, since, max_tokens }) => {
|
|
271
|
+
trackCall("get_component_tree");
|
|
272
|
+
if (!getPlan(getActivePlanId()).componentTree) {
|
|
273
|
+
return {
|
|
274
|
+
content: [{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: `\u26D4 **Component Tree Inspection** is a Pro feature.
|
|
277
|
+
|
|
278
|
+
Upgrade to **Pro ($29/mo)** to inspect React and Vue component trees, props, state, and hooks.
|
|
279
|
+
|
|
280
|
+
\u2192 https://mergen.app/pricing`
|
|
281
|
+
}],
|
|
282
|
+
isError: true
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
void component_name;
|
|
286
|
+
const contexts = store.getContext(50, since);
|
|
287
|
+
const withTrees = contexts.filter((c) => c.componentTree);
|
|
288
|
+
if (withTrees.length === 0) {
|
|
289
|
+
return {
|
|
290
|
+
content: [{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: "No component trees captured yet. Component trees are automatically captured on console.error events.\n\nTo manually capture, trigger an error in your app or wait for the next error to occur."
|
|
293
|
+
}]
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const depth = max_depth ?? 5;
|
|
297
|
+
function formatReactNode(node, d) {
|
|
298
|
+
if (d > depth) return [];
|
|
299
|
+
const indent = " ".repeat(d);
|
|
300
|
+
const out = [];
|
|
301
|
+
const renderBadge = node.renderCount && node.renderCount > 0 ? ` (renders: ${node.renderCount}${node.renderCount > 10 ? " \u26A0" : ""})` : "";
|
|
302
|
+
out.push(`${indent}${node.name}${renderBadge}`);
|
|
303
|
+
if (node.props && Object.keys(node.props).length > 0) {
|
|
304
|
+
out.push(`${indent} props:`);
|
|
305
|
+
for (const [key, val] of Object.entries(node.props).slice(0, 5))
|
|
306
|
+
out.push(`${indent} ${key}: ${String(val).slice(0, 100)}`);
|
|
307
|
+
}
|
|
308
|
+
if (node.state && Object.keys(node.state).length > 0) {
|
|
309
|
+
out.push(`${indent} state:`);
|
|
310
|
+
for (const [key, val] of Object.entries(node.state).slice(0, 5))
|
|
311
|
+
out.push(`${indent} ${key}: ${String(val).slice(0, 100)}`);
|
|
312
|
+
}
|
|
313
|
+
if (node.hooks && node.hooks.length > 0) {
|
|
314
|
+
out.push(`${indent} hooks:`);
|
|
315
|
+
for (const hook of node.hooks.slice(0, 5))
|
|
316
|
+
out.push(`${indent} [${hook.index}]: ${hook.value.slice(0, 100)}`);
|
|
317
|
+
}
|
|
318
|
+
for (const child of node.children ?? []) out.push(...formatReactNode(child, d + 1));
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
function formatVueNode(node, d) {
|
|
322
|
+
if (d > depth) return [];
|
|
323
|
+
const indent = " ".repeat(d);
|
|
324
|
+
const out = [];
|
|
325
|
+
out.push(`${indent}${node.name}`);
|
|
326
|
+
if (node.props && Object.keys(node.props).length > 0) {
|
|
327
|
+
out.push(`${indent} props:`);
|
|
328
|
+
for (const [key, val] of Object.entries(node.props).slice(0, 5))
|
|
329
|
+
out.push(`${indent} ${key}: ${String(val).slice(0, 100)}`);
|
|
330
|
+
}
|
|
331
|
+
const dataSource = node.state ?? node.data;
|
|
332
|
+
if (dataSource && Object.keys(dataSource).length > 0) {
|
|
333
|
+
out.push(`${indent} ${node.state ? "state" : "data"}:`);
|
|
334
|
+
for (const [key, val] of Object.entries(dataSource).slice(0, 5))
|
|
335
|
+
out.push(`${indent} ${key}: ${String(val).slice(0, 100)}`);
|
|
336
|
+
}
|
|
337
|
+
for (const child of node.children ?? []) out.push(...formatVueNode(child, d + 1));
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
const lines = ["## \u{1F332} Component Tree", ""];
|
|
341
|
+
for (const ctx of withTrees.slice(-3)) {
|
|
342
|
+
const tree = ctx.componentTree;
|
|
343
|
+
if (!tree) continue;
|
|
344
|
+
lines.push(`**Captured at:** ${new Date(ctx.timestamp).toISOString()}`);
|
|
345
|
+
lines.push(`**Framework:** ${tree.framework}`);
|
|
346
|
+
lines.push(`**URL:** ${ctx.url}`);
|
|
347
|
+
lines.push("");
|
|
348
|
+
if (tree.framework === "React" && tree.tree) {
|
|
349
|
+
lines.push("```");
|
|
350
|
+
lines.push(...formatReactNode(tree.tree, 0));
|
|
351
|
+
lines.push("```");
|
|
352
|
+
}
|
|
353
|
+
if ((tree.framework === "Vue" || tree.framework === "Vue3") && tree.tree) {
|
|
354
|
+
lines.push("```");
|
|
355
|
+
lines.push(...formatVueNode(tree.tree, 0));
|
|
356
|
+
lines.push("```");
|
|
357
|
+
}
|
|
358
|
+
lines.push("");
|
|
359
|
+
}
|
|
360
|
+
lines.push("---");
|
|
361
|
+
lines.push("**Debug hints:**");
|
|
362
|
+
lines.push("- `renders: N \u26A0` means this component type committed >10 times \u2014 check for missing deps in useEffect/useMemo");
|
|
363
|
+
lines.push("- Missing props? Check parent component's render method");
|
|
364
|
+
lines.push("- State not updating? Verify setState/dispatch calls");
|
|
365
|
+
const { result, truncated, omitted, estimatedTokens } = truncateToTokenBudget(lines, max_tokens, "\n");
|
|
366
|
+
if (truncated) logger.info({ tool: "get_component_tree", omitted, estimatedTokens }, "response truncated");
|
|
367
|
+
return { content: [{ type: "text", text: result }] };
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
server.registerTool(
|
|
371
|
+
"get_service_topology",
|
|
372
|
+
{
|
|
373
|
+
description: '\u26A1 FREE \xB7 Returns the persistent service dependency graph as structured JSON \u2014 every service Mergen has observed (browser, API, database, queue, cache, infra), their dependencies, call counts, error rates, and p99 latencies. Built incrementally from backend spans and W3C trace joins. Survives server restarts. Use this to understand system structure before diagnosing an incident: "what services exist?", "what calls what?", "which service has the most errors?". Returns empty graph if no backend spans have been received yet \u2014 install the Node.js or Python SDK to start populating it.'
|
|
374
|
+
},
|
|
375
|
+
async () => {
|
|
376
|
+
trackCall("get_service_topology");
|
|
377
|
+
const snap = serviceTopology.snapshot();
|
|
378
|
+
if (snap.nodes.length === 0) {
|
|
379
|
+
return {
|
|
380
|
+
content: [{
|
|
381
|
+
type: "text",
|
|
382
|
+
text: [
|
|
383
|
+
"## Service Topology\n",
|
|
384
|
+
"> No services observed yet. The topology is built from backend spans.",
|
|
385
|
+
"> Install the Mergen SDK in your backend services to start populating it:",
|
|
386
|
+
"> - Node.js: `npm install @mergen/node`",
|
|
387
|
+
"> - Python: `pip install mergen`",
|
|
388
|
+
"",
|
|
389
|
+
"Browser-side network events are already captured.",
|
|
390
|
+
"Service-to-service edges appear once backend spans arrive."
|
|
391
|
+
].join("\n")
|
|
392
|
+
}]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const lines = ["## Service Topology\n"];
|
|
396
|
+
lines.push(`**${snap.summary.totalServices} services \xB7 ${snap.summary.totalEdges} edges**
|
|
397
|
+
`);
|
|
398
|
+
if (snap.summary.criticalPath.length > 0) {
|
|
399
|
+
lines.push(`**Critical path:** ${snap.summary.criticalPath.join(" \u2192 ")}
|
|
400
|
+
`);
|
|
401
|
+
}
|
|
402
|
+
if (snap.summary.errorHotspot) {
|
|
403
|
+
lines.push(`**Error hotspot:** \`${snap.summary.errorHotspot}\``);
|
|
404
|
+
}
|
|
405
|
+
if (snap.summary.slowestEdge) {
|
|
406
|
+
const e = snap.summary.slowestEdge;
|
|
407
|
+
lines.push(`**Slowest edge:** \`${e.from} \u2192 ${e.to}\` (${e.avgDurationMs}ms avg)
|
|
408
|
+
`);
|
|
409
|
+
}
|
|
410
|
+
lines.push("### Services\n");
|
|
411
|
+
lines.push("| Service | Type | Calls | Errors | Avg ms | p99 ms |");
|
|
412
|
+
lines.push("|---------|------|-------|--------|--------|--------|");
|
|
413
|
+
for (const n of snap.nodes.sort((a, b) => b.spanCount - a.spanCount)) {
|
|
414
|
+
const errPct = n.spanCount > 0 ? (n.errorCount / n.spanCount * 100).toFixed(1) : "0.0";
|
|
415
|
+
const errCell = n.errorCount > 0 ? `${n.errorCount} (${errPct}%)` : "0";
|
|
416
|
+
lines.push(`| \`${n.id}\` | ${n.type} | ${n.spanCount} | ${errCell} | ${n.avgDurationMs} | ${n.p99DurationMs} |`);
|
|
417
|
+
}
|
|
418
|
+
lines.push("\n### Dependencies\n");
|
|
419
|
+
for (const e of snap.edges.sort((a, b) => b.callCount - a.callCount)) {
|
|
420
|
+
const errInfo = e.errorCount > 0 ? ` \xB7 \u26A0\uFE0F ${e.errorCount} errors` : "";
|
|
421
|
+
lines.push(`- \`${e.from}\` \u2192 \`${e.to}\` (${e.callCount} calls \xB7 ${e.avgDurationMs}ms avg${errInfo})`);
|
|
422
|
+
}
|
|
423
|
+
lines.push("\n---");
|
|
424
|
+
lines.push(`*Updated: ${snap.capturedAt} \xB7 Raw JSON: GET /topology*`);
|
|
425
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
export {
|
|
430
|
+
registerBrowserTools
|
|
431
|
+
};
|