@reclaimprotocol/agent 0.1.40 → 0.1.42

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.
Files changed (44) hide show
  1. package/README.md +32 -0
  2. package/lib/authoring/tools.js +4 -1
  3. package/lib/config/store.d.ts +7 -0
  4. package/lib/consts.d.ts +5 -0
  5. package/lib/consts.js +10 -0
  6. package/lib/mcp/api/operations.gen.js +6 -2
  7. package/lib/mcp/api/tools.js +4 -0
  8. package/lib/mcp/server.js +6 -1
  9. package/lib/mcp/start.js +27 -1
  10. package/lib/mcp/tools/agent/proof.js +4 -2
  11. package/lib/mcp/tools/agent/propose.js +7 -0
  12. package/lib/mcp/tools/agent/publish.js +4 -1
  13. package/lib/mcp/tools/agent/replay.js +3 -1
  14. package/lib/mcp/tools/agent/start-integration.d.ts +2 -0
  15. package/lib/mcp/tools/agent/start-integration.js +7 -0
  16. package/lib/mcp/tools/authenticate/authenticate.js +3 -0
  17. package/lib/telemetry/index.d.ts +9 -0
  18. package/lib/telemetry/index.js +13 -0
  19. package/lib/telemetry/telemetry.d.ts +54 -0
  20. package/lib/telemetry/telemetry.js +175 -0
  21. package/lib/telemetry/tool-call.d.ts +24 -0
  22. package/lib/telemetry/tool-call.js +102 -0
  23. package/package.json +2 -2
  24. package/skill/SKILL.builder.md +5 -0
  25. package/skill/SKILL.common.md +34 -3
  26. package/skill/SKILL.old.md +5 -0
  27. package/skill/builder-mode.md +1 -0
  28. package/skill/old-mode.md +1 -0
  29. package/src/authoring/tools.ts +4 -1
  30. package/src/config/store.ts +4 -0
  31. package/src/consts.ts +11 -0
  32. package/src/mcp/api/operations.gen.ts +6 -2
  33. package/src/mcp/api/tools.ts +4 -0
  34. package/src/mcp/server.ts +6 -1
  35. package/src/mcp/start.ts +34 -0
  36. package/src/mcp/tools/agent/proof.ts +4 -2
  37. package/src/mcp/tools/agent/propose.ts +8 -0
  38. package/src/mcp/tools/agent/publish.ts +4 -1
  39. package/src/mcp/tools/agent/replay.ts +3 -1
  40. package/src/mcp/tools/agent/start-integration.ts +10 -0
  41. package/src/mcp/tools/authenticate/authenticate.ts +3 -0
  42. package/src/telemetry/index.ts +18 -0
  43. package/src/telemetry/telemetry.ts +206 -0
  44. package/src/telemetry/tool-call.ts +115 -0
package/README.md CHANGED
@@ -70,6 +70,38 @@ Defaults target the Builder backend, so **no env is needed for the common case**
70
70
 
71
71
  See [Backends](#backends) for the full env-var list.
72
72
 
73
+ ### Usage data
74
+
75
+ The MCP server sends anonymous usage data to Builder so that Reclaim can see which tools developers use and where they get stuck.
76
+
77
+ **What the server records**
78
+
79
+ - When a session starts: the mode (Builder or devtools), whether the browser tools are on, your operating system family, your Node.js version, and the name and version of your MCP client, such as Claude Code.
80
+ - For each tool call: the tool name, whether it succeeded, how long it took, and an error category such as `http_401`.
81
+ - When you sign in.
82
+ - For three tools only, the text you give them. `start_integration` records `goal` and `userRequest`. `propose_provider` records `userRequest`. `discover_resolve` records the domain and data points you search for. Don't put secrets or personal data in these fields.
83
+ - A random client ID that the server saves in `~/.reclaim/config.json`. It isn't derived from your machine or account. After you sign in, Builder links earlier events from this client ID to your account.
84
+
85
+ **What the server never records**
86
+
87
+ - Any other tool argument.
88
+ - Tool output, captured requests or responses, or file contents.
89
+ - Error messages, tokens, keys, or cookies.
90
+
91
+ **Where the data goes**
92
+
93
+ Builder stores the events in its database. Builder forwards only a short list of event names and counts to Google Analytics, and never the text you typed.
94
+
95
+ **Turn it off**
96
+
97
+ Set `RECLAIM_TELEMETRY=0`, or set `DO_NOT_TRACK=1`, in the `env` block of your MCP client configuration. With either set, the server doesn't create a client ID and makes no analytics requests.
98
+
99
+ ```json
100
+ "env": {
101
+ "RECLAIM_TELEMETRY": "0"
102
+ }
103
+ ```
104
+
73
105
  ### Provider-authoring guide
74
106
 
75
107
  The MCP server is all you need — it ships a `how_it_works` tool that serves the capture → prove → publish guide **one topic at a time** (`overview`, `auth`, `browser`, `capture`, `draft`, `params`, `prove`, `publish`, `discover`, `user-script`, `interception`, `hash-validation`, `credentials`, `never-do`, `troubleshooting`). Call it with no arguments for the overview plus the topic index; every response repeats that index, so an agent that guessed wrong self-corrects without a second round-trip. Any MCP client can call it on demand; there's nothing extra to install.
@@ -394,7 +394,10 @@ export function authoringTools(resolveBackend) {
394
394
  + 'of the site, not a bug in the draft. HTTP 403/406/429/503 or a '
395
395
  + 'challenge page from the attestor while the replay succeeded '
396
396
  + 'means the site blocks non-browser clients: copying headers or '
397
- + 'cookies will not help, so try a different endpoint or stop.',
397
+ + 'cookies will not help. Try one other endpoint; if that is '
398
+ + 'blocked too, publish the version from the capture and have '
399
+ + 'the developer try a real verification '
400
+ + '(how_it_works({ topic: "prove" })). Do not keep retrying.',
398
401
  inputSchema: objectSchema({ draftId: { type: 'string' } }, ['draftId']),
399
402
  annotations: { openWorldHint: true, destructiveHint: false },
400
403
  },
@@ -16,6 +16,9 @@ export interface ReclaimAgentConfig {
16
16
  * - `old_devtools` — externally-supplied identity for OLD-devtools mode
17
17
  * (USE_OLD_DEVTOOLS); independent of `session`'s
18
18
  * builder token. Deletable with the rest of `src/old/`.
19
+ * - `analytics` — the random, anonymous client ID the usage data is
20
+ * sent under (see telemetry/telemetry.ts); holds no
21
+ * personal data
19
22
  * - `org_keys` — organization API keys saved by `issue_org_token`, so
20
23
  * the org-key tools work without the key in a transcript
21
24
  * Sections are persisted independently via `ConfigStore.write`, which
@@ -32,6 +35,10 @@ export interface ReclaimConfig {
32
35
  session?: CachedSession;
33
36
  old_devtools?: OldIdentity;
34
37
  org_keys?: SavedOrgKeys;
38
+ analytics?: {
39
+ clientId: string;
40
+ createdAt: string;
41
+ };
35
42
  }
36
43
  /** Organization API keys (`rorg_…`) saved by `issue_org_token`. */
37
44
  export interface SavedOrgKeys {
package/lib/consts.d.ts CHANGED
@@ -59,6 +59,11 @@ export declare function chromePath(): string | undefined;
59
59
  * deployments by setting `RECLAIM_AGENT_DISABLED=1`. Shared by mcp/start and
60
60
  * the old-mode authenticate tool. */
61
61
  export declare function agentDisabled(): boolean;
62
+ /**
63
+ * Whether the server may send anonymous usage data. Off when you set
64
+ * `RECLAIM_TELEMETRY=0` or the cross-tool `DO_NOT_TRACK=1` convention.
65
+ */
66
+ export declare function telemetryEnabled(): boolean;
62
67
  /** Old-devtools backend base URL. */
63
68
  export declare function oldApiUrl(): string;
64
69
  /** Old-devtools analytics-logs service base URL (separate host). */
package/lib/consts.js CHANGED
@@ -111,6 +111,16 @@ export function chromePath() {
111
111
  export function agentDisabled() {
112
112
  return process.env.RECLAIM_AGENT_DISABLED === '1';
113
113
  }
114
+ /**
115
+ * Whether the server may send anonymous usage data. Off when you set
116
+ * `RECLAIM_TELEMETRY=0` or the cross-tool `DO_NOT_TRACK=1` convention.
117
+ */
118
+ export function telemetryEnabled() {
119
+ const doNotTrack = process.env.DO_NOT_TRACK?.toLowerCase();
120
+ return process.env.RECLAIM_TELEMETRY !== '0'
121
+ && doNotTrack !== '1'
122
+ && doNotTrack !== 'true';
123
+ }
114
124
  /** Old-devtools backend base URL. */
115
125
  export function oldApiUrl() {
116
126
  return process.env.RECLAIM_OLD_API_URL || DEFAULT_OLD_API_URL;
@@ -1,5 +1,5 @@
1
1
  // AUTO-GENERATED by packages/app/scripts/mcp-tools-gen.ts
2
- // from packages/app/openapi.yaml (spec v0.1.0, source sha256:ecdb0ee6d82e).
2
+ // from packages/app/openapi.yaml (spec v0.1.0, source sha256:c3421d6bcf76).
3
3
  // The hash catches drift the version misses (a spec can change without a
4
4
  // version bump) — do not remove it as "redundant" with the version.
5
5
  // Run `npm run openapi:gen --workspace=packages/app` to regenerate. Do not edit by hand.
@@ -117,7 +117,11 @@ export const MCP_TOOLS = {
117
117
  "enum": [
118
118
  "providers",
119
119
  "revenue",
120
- "usage"
120
+ "usage",
121
+ "goals",
122
+ "tools",
123
+ "agents",
124
+ "funnel"
121
125
  ]
122
126
  }
123
127
  }
@@ -2,6 +2,7 @@ import { unauthorized } from '@hapi/boom';
2
2
  import { ReclaimClient } from '@reclaimprotocol/client/api';
3
3
  import assert from 'node:assert';
4
4
  import { orgSecret } from "../../consts.js";
5
+ import { startToolCall } from "../../telemetry/index.js";
5
6
  import { remapErrorAsResponse } from "../remap_errors.js";
6
7
  import { successResponse } from "../success-response.js";
7
8
  import { savedOrgKey } from "../tools/authenticate/org-key-store.js";
@@ -48,6 +49,7 @@ export function registerTools(server, client, orgClient = (orgToken) => new Recl
48
49
  // and generated names must stay disjoint (see start.ts), so a clash is a
49
50
  // bug to surface, not silently shadow.
50
51
  server.registerTool(toSnakeCase(opId), shape, async (input) => {
52
+ const call = startToolCall(toSnakeCase(opId), input);
51
53
  try {
52
54
  if (!isRecord(input)) {
53
55
  throw new Error(`Invalid generated MCP arguments for ${opId}`);
@@ -60,9 +62,11 @@ export function registerTools(server, client, orgClient = (orgToken) => new Recl
60
62
  ? orgClient(orgTokenFor(toSnakeCase(opId), orgToken, args))
61
63
  : client;
62
64
  const result = await caller.call(opId, args);
65
+ call.end();
63
66
  return successResponse(result.data);
64
67
  }
65
68
  catch (err) {
69
+ call.end(err);
66
70
  return remapErrorAsResponse(err);
67
71
  }
68
72
  });
package/lib/mcp/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { startToolCall } from "../telemetry/index.js";
1
2
  import { toZod } from "./api/to-zod.js";
2
3
  import { remapErrorAsResponse } from "./remap_errors.js";
3
4
  import { successResponse } from "./success-response.js";
@@ -19,10 +20,14 @@ export function registerToolList(server, tools) {
19
20
  ? { annotations: tool.definition.annotations }
20
21
  : {}),
21
22
  }, async (args) => {
23
+ const call = startToolCall(tool.definition.name, args);
22
24
  try {
23
- return successResponse(await tool.handler(args));
25
+ const result = successResponse(await tool.handler(args));
26
+ call.end();
27
+ return result;
24
28
  }
25
29
  catch (err) {
30
+ call.end(err);
26
31
  return remapErrorAsResponse(err);
27
32
  }
28
33
  });
package/lib/mcp/start.js CHANGED
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { ReclaimClient } from '@reclaimprotocol/client/api';
4
4
  import { ConfigStore } from "../config/store.js";
5
- import { agentDisabled, apiToken, apiUrl, oldApiUrl, oldLogsUrl, resolveOldMode, sdkApiUrl, } from "../consts.js";
5
+ import { agentDisabled, apiToken, apiUrl, oldApiUrl, oldLogsUrl, resolveOldMode, sdkApiUrl, telemetryEnabled, } from "../consts.js";
6
6
  import { LOGGER } from "../logger.js";
7
7
  import { ReclaimOldClient } from "../old/client.js";
8
8
  import { resolveInitialIdentity } from "../old/identity.js";
@@ -11,6 +11,7 @@ import { ReclaimSdkClient } from "../old/sdk-client.js";
11
11
  import { buildOldBackendTools } from "../old/tools/build.js";
12
12
  import { NAME, VERSION } from "../pkg.js";
13
13
  import { startZkFilesDownload } from "../proof/zk-files.js";
14
+ import { currentTelemetry, installTelemetry, Telemetry } from "../telemetry/index.js";
14
15
  import { registerTools } from "./api/tools.js";
15
16
  import { buildAgentState, buildAgentTools, buildBuilderAuthenticationTools, buildBuilderOrganizationTools, buildCredentialTools, buildLocalCredentialTools, buildModeTools, buildVerificationTools, disposeAgentState, readSession, releaseAllListeners, sessionRefreshingFetch, stopAllForwarders, } from "./tools/index.js";
16
17
  import { BUILDER_MODE_INSTRUCTIONS } from "./instructions.js";
@@ -47,6 +48,12 @@ export async function startMcpServer() {
47
48
  }),
48
49
  headers: { 'User-Agent': `${NAME}/${VERSION}` },
49
50
  });
51
+ // Anonymous usage data. Installed before any tool registers so every tool
52
+ // call is covered. When the developer opts out, nothing is created.
53
+ const mode = oldMode ? 'devtools' : 'builder';
54
+ if (telemetryEnabled()) {
55
+ installTelemetry(new Telemetry({ client: builderClient, mode }));
56
+ }
50
57
  // Backend-mode toggle — available in both modes and regardless of whether
51
58
  // the agent tools are enabled, so a user can always switch either direction.
52
59
  registerToolList(server, buildModeTools(config, oldMode));
@@ -106,6 +113,18 @@ export async function startMcpServer() {
106
113
  if (agentToolsEnabled || !oldMode) {
107
114
  installShutdownHooks(() => agentState);
108
115
  }
116
+ // The client's name and version arrive in the `initialize` handshake, so
117
+ // the session starts only after it completes.
118
+ server.server.oninitialized = () => {
119
+ const telemetry = currentTelemetry();
120
+ telemetry?.setAgent(server.server.getClientVersion());
121
+ telemetry?.track('session_start', {
122
+ mode,
123
+ agentTools: agentToolsEnabled,
124
+ os: knownOs(process.platform),
125
+ nodeVersion: process.versions.node,
126
+ });
127
+ };
109
128
  const transport = new StdioServerTransport();
110
129
  await server.connect(transport);
111
130
  // Fetch the proof files in the background so the first proof doesn't wait
@@ -143,6 +162,7 @@ function installShutdownHooks(state) {
143
162
  void Promise.all([
144
163
  stopAllForwarders(),
145
164
  releaseAllListeners(),
165
+ currentTelemetry()?.close(),
146
166
  current && disposeAgentState(current),
147
167
  ])
148
168
  .catch((err) => {
@@ -159,3 +179,9 @@ function installShutdownHooks(state) {
159
179
  process.stdin.once('end', () => shutdown('stdin ended'));
160
180
  process.stdin.once('close', () => shutdown('stdin closed'));
161
181
  }
182
+ /** Maps the platform to the values the analytics API accepts. */
183
+ function knownOs(platform) {
184
+ return platform === 'darwin' || platform === 'linux' || platform === 'win32'
185
+ ? platform
186
+ : 'other';
187
+ }
@@ -212,8 +212,10 @@ export function diagnoseBlockedClient(result, lastReplay, geoLocation) {
212
212
  + 'published version goes out from there too. Otherwise, try a '
213
213
  + 'different endpoint that returns the value, such as a JSON API the '
214
214
  + 'page calls (find_requests_containing), then propose and prove '
215
- + 'that. If none works, stop and report to the developer that this '
216
- + 'site blocks the attestor. Do not tweak headers.',
215
+ + 'that. If none works, stop retrying: publish the version from the '
216
+ + 'capture, unproven, and ask the developer to try a real '
217
+ + 'verification (how_it_works({ topic: "prove" })). Do not tweak '
218
+ + 'headers.',
217
219
  };
218
220
  }
219
221
  /** True when an attestor error embeds a response element (base64, from a
@@ -23,6 +23,13 @@ export function proposeTool(resolveBackend) {
23
23
  inputSchema: objectSchema({
24
24
  requestId: { type: 'string' },
25
25
  name: { type: 'string' },
26
+ userRequest: {
27
+ type: 'string',
28
+ maxLength: 500,
29
+ description: 'The developer\'s request in their own words, as they wrote it. '
30
+ + 'Reclaim records this text to improve the product, so leave '
31
+ + 'out secrets and personal data.',
32
+ },
26
33
  shortenNames: {
27
34
  type: 'boolean',
28
35
  description: 'Default true: shorten each target `name` to its last '
@@ -168,7 +168,10 @@ const BROWSER_REQUEST_ACTION_SCHEMA = {
168
168
  export function createVersionFromCaptureTool(client, attachRef, kept) {
169
169
  return defineTool({
170
170
  name: 'create_provider_version_from_capture',
171
- description: 'Creates a new immutable provider version after `run_proof` succeeds. '
171
+ description: 'Creates a new immutable provider version after `run_proof` succeeds, '
172
+ + 'or from the captured draft when the site keeps answering replay or '
173
+ + 'proof with 403 past the retry budget in '
174
+ + 'how_it_works({ topic: "prove" }). '
172
175
  + 'Publishes the navigation script kept by `test_user_script` as '
173
176
  + '`jsUserScripts` unless `jsUserScripts` is passed. Pass exactly '
174
177
  + 'one `provider` or `providers` value returned by '
@@ -18,7 +18,9 @@ export function replayTool(resolveBackend) {
18
18
  + 'no cookies or auth headers, like an anonymous visitor. If the '
19
19
  + 'values still extract (`valid: true`), the endpoint is public and '
20
20
  + 'the proof would not bind to the user: choose an auth-bound '
21
- + 'request instead.',
21
+ + 'request instead. A 403 that survives one retry and one '
22
+ + 'correction is a site defense, not a recipe bug: stop retrying '
23
+ + 'and publish from the capture (how_it_works({ topic: "prove" })).',
22
24
  inputSchema: objectSchema({
23
25
  draftId: { type: 'string' },
24
26
  withoutSecrets: { type: 'boolean' },
@@ -1,6 +1,8 @@
1
1
  import { type RegisteredTool } from '../../server.ts';
2
2
  interface StartIntegrationArgs {
3
3
  goal: string;
4
+ /** Recorded by telemetry only; the plan ignores it. */
5
+ userRequest?: string;
4
6
  orgId?: string;
5
7
  providerIds?: string[];
6
8
  }
@@ -30,6 +30,13 @@ export function startIntegrationTool() {
30
30
  description: 'What the developer\'s users prove, for example, "they have '
31
31
  + 'a GitHub account with 100 followers".',
32
32
  },
33
+ userRequest: {
34
+ type: 'string',
35
+ maxLength: 500,
36
+ description: 'The developer\'s request in their own words, as they wrote it. '
37
+ + 'Reclaim records this text to improve the product, so leave '
38
+ + 'out secrets and personal data.',
39
+ },
33
40
  orgId: {
34
41
  type: 'string',
35
42
  format: 'uuid',
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createServer } from 'node:http';
3
3
  import * as oauth from 'openid-client';
4
+ import { currentTelemetry } from "../../../telemetry/index.js";
4
5
  import { defineTool } from "../../server.js";
5
6
  import { authBaseFor, oauthRequestOptions, SCOPE, sessionFromTokens, } from "./oauth-config.js";
6
7
  import { readSession, writeSession } from "./session-cache.js";
@@ -181,6 +182,8 @@ async function beginSignIn(client, openBrowserFn) {
181
182
  });
182
183
  writeSession(session);
183
184
  client.setToken(session.token);
185
+ // Link this machine's earlier anonymous usage to the new account.
186
+ void currentTelemetry()?.signedIn();
184
187
  return { status: 'AUTHENTICATED', expiresAt: session.expiresAt };
185
188
  }
186
189
  catch (err) {
@@ -0,0 +1,9 @@
1
+ import type { Telemetry } from './telemetry.ts';
2
+ export { startToolCall } from './tool-call.ts';
3
+ export { Telemetry } from './telemetry.ts';
4
+ /**
5
+ * Makes `telemetry` the instance the hooks report to. Until you call this,
6
+ * the hooks do nothing, which is how opt-out and tests stay silent.
7
+ */
8
+ export declare function installTelemetry(telemetry: Telemetry | undefined): void;
9
+ export declare function currentTelemetry(): Telemetry | undefined;
@@ -0,0 +1,13 @@
1
+ export { startToolCall } from "./tool-call.js";
2
+ export { Telemetry } from "./telemetry.js";
3
+ let current;
4
+ /**
5
+ * Makes `telemetry` the instance the hooks report to. Until you call this,
6
+ * the hooks do nothing, which is how opt-out and tests stay silent.
7
+ */
8
+ export function installTelemetry(telemetry) {
9
+ current = telemetry;
10
+ }
11
+ export function currentTelemetry() {
12
+ return current;
13
+ }
@@ -0,0 +1,54 @@
1
+ import type { ReclaimClient } from '@reclaimprotocol/client/api';
2
+ import type { IOperationArgs } from '@reclaimprotocol/client/openapi';
3
+ import { ConfigStore } from '../config/store.ts';
4
+ type TrackArgs = IOperationArgs<'TrackAnalyticsEvents'>;
5
+ type Batch = TrackArgs['body'];
6
+ type Event = Batch['events'][number];
7
+ /** Backend mode, as the analytics API names it. */
8
+ export type TelemetryMode = 'builder' | 'devtools';
9
+ export interface TelemetryOptions {
10
+ client: ReclaimClient;
11
+ mode: TelemetryMode;
12
+ store?: ConfigStore;
13
+ /** How long to wait after the first queued event before sending. */
14
+ flushIntervalMs?: number;
15
+ }
16
+ /**
17
+ * Sends anonymous usage events to Builder. It never throws and never blocks
18
+ * a tool call: `track()` only queues, and a timer that doesn't keep the
19
+ * process alive sends the queue in the background. Failed batches are
20
+ * dropped, because usage data isn't worth retry loops in a tool server.
21
+ *
22
+ * Nothing is written to stdout. Stdio is the MCP transport.
23
+ */
24
+ export declare class Telemetry {
25
+ #private;
26
+ readonly mode: TelemetryMode;
27
+ readonly clientId: string;
28
+ readonly sessionId: string;
29
+ constructor({ client, mode, store, flushIntervalMs, }: TelemetryOptions);
30
+ /** Records the MCP client (for example, Claude Code) that connected. */
31
+ setAgent(info: {
32
+ name: string;
33
+ version?: string;
34
+ } | undefined): void;
35
+ /** Queues an event. Synchronous, cheap, and safe to call anywhere. */
36
+ track<N extends Event['name']>(name: N, properties: Extract<Event, {
37
+ name: N;
38
+ }>['properties']): void;
39
+ /** Sends everything queued. Never rejects. */
40
+ flush(): Promise<void>;
41
+ /**
42
+ * Links this client's earlier anonymous events to the signed-in user.
43
+ * Needs a bearer token, so call it after sign-in.
44
+ */
45
+ identify(): Promise<void>;
46
+ /**
47
+ * Call when sign-in finishes. Sends the `login` event first so that
48
+ * `identify()` can attribute it to the user.
49
+ */
50
+ signedIn(): Promise<void>;
51
+ /** Sends what's left, waiting at most two seconds. */
52
+ close(): Promise<void>;
53
+ }
54
+ export {};
@@ -0,0 +1,175 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _Telemetry_instances, _Telemetry_client, _Telemetry_flushIntervalMs, _Telemetry_queue, _Telemetry_timer, _Telemetry_agent, _Telemetry_sending, _Telemetry_schedule, _Telemetry_send;
13
+ import { randomUUID } from 'node:crypto';
14
+ import { ConfigStore } from "../config/store.js";
15
+ import { LOGGER } from "../logger.js";
16
+ import { VERSION } from "../pkg.js";
17
+ /** The API accepts up to 50 events per request. */
18
+ const MAX_BATCH = 50;
19
+ /** Bounds memory while the backend is unreachable. Oldest events go first. */
20
+ const MAX_QUEUE = 500;
21
+ /** How long `close()` waits, so a slow network can't hold up exit. */
22
+ const CLOSE_LIMIT_MS = 2000;
23
+ /**
24
+ * Sends anonymous usage events to Builder. It never throws and never blocks
25
+ * a tool call: `track()` only queues, and a timer that doesn't keep the
26
+ * process alive sends the queue in the background. Failed batches are
27
+ * dropped, because usage data isn't worth retry loops in a tool server.
28
+ *
29
+ * Nothing is written to stdout. Stdio is the MCP transport.
30
+ */
31
+ export class Telemetry {
32
+ constructor({ client, mode, store = new ConfigStore(), flushIntervalMs = 5000, }) {
33
+ _Telemetry_instances.add(this);
34
+ this.sessionId = String(Math.floor(Date.now() / 1000));
35
+ _Telemetry_client.set(this, void 0);
36
+ _Telemetry_flushIntervalMs.set(this, void 0);
37
+ _Telemetry_queue.set(this, []);
38
+ _Telemetry_timer.set(this, void 0);
39
+ _Telemetry_agent.set(this, void 0);
40
+ /** Serializes sends so batches go out in order. */
41
+ _Telemetry_sending.set(this, Promise.resolve());
42
+ __classPrivateFieldSet(this, _Telemetry_client, client, "f");
43
+ this.mode = mode;
44
+ __classPrivateFieldSet(this, _Telemetry_flushIntervalMs, flushIntervalMs, "f");
45
+ this.clientId = loadClientId(store);
46
+ }
47
+ /** Records the MCP client (for example, Claude Code) that connected. */
48
+ setAgent(info) {
49
+ if (info?.name) {
50
+ __classPrivateFieldSet(this, _Telemetry_agent, { name: info.name, version: info.version }, "f");
51
+ }
52
+ }
53
+ /** Queues an event. Synchronous, cheap, and safe to call anywhere. */
54
+ track(name, properties) {
55
+ try {
56
+ // The union is keyed by `name`; TypeScript can't correlate the two
57
+ // generic parameters, so build the event through the union type.
58
+ const event = {
59
+ id: randomUUID(),
60
+ name,
61
+ timestamp: new Date().toISOString(),
62
+ properties,
63
+ };
64
+ __classPrivateFieldGet(this, _Telemetry_queue, "f").push(event);
65
+ if (__classPrivateFieldGet(this, _Telemetry_queue, "f").length > MAX_QUEUE) {
66
+ __classPrivateFieldGet(this, _Telemetry_queue, "f").splice(0, __classPrivateFieldGet(this, _Telemetry_queue, "f").length - MAX_QUEUE);
67
+ }
68
+ __classPrivateFieldGet(this, _Telemetry_instances, "m", _Telemetry_schedule).call(this);
69
+ }
70
+ catch (err) {
71
+ LOGGER.debug({ err }, 'telemetry: track failed');
72
+ }
73
+ }
74
+ /** Sends everything queued. Never rejects. */
75
+ flush() {
76
+ clearTimeout(__classPrivateFieldGet(this, _Telemetry_timer, "f"));
77
+ __classPrivateFieldSet(this, _Telemetry_timer, undefined, "f");
78
+ const events = __classPrivateFieldGet(this, _Telemetry_queue, "f").splice(0);
79
+ if (!events.length) {
80
+ return __classPrivateFieldGet(this, _Telemetry_sending, "f");
81
+ }
82
+ __classPrivateFieldSet(this, _Telemetry_sending, __classPrivateFieldGet(this, _Telemetry_sending, "f").then(() => __classPrivateFieldGet(this, _Telemetry_instances, "m", _Telemetry_send).call(this, events)), "f");
83
+ return __classPrivateFieldGet(this, _Telemetry_sending, "f");
84
+ }
85
+ /**
86
+ * Links this client's earlier anonymous events to the signed-in user.
87
+ * Needs a bearer token, so call it after sign-in.
88
+ */
89
+ async identify() {
90
+ try {
91
+ await __classPrivateFieldGet(this, _Telemetry_client, "f").call('IdentifyAnalyticsClient', {
92
+ body: { clientId: this.clientId },
93
+ });
94
+ }
95
+ catch (err) {
96
+ LOGGER.debug({ err }, 'telemetry: identify failed');
97
+ }
98
+ }
99
+ /**
100
+ * Call when sign-in finishes. Sends the `login` event first so that
101
+ * `identify()` can attribute it to the user.
102
+ */
103
+ async signedIn() {
104
+ this.track('login', { method: 'oauth' });
105
+ await this.flush();
106
+ await this.identify();
107
+ }
108
+ /** Sends what's left, waiting at most two seconds. */
109
+ async close() {
110
+ let timer;
111
+ const limit = new Promise((resolve) => {
112
+ timer = setTimeout(resolve, CLOSE_LIMIT_MS);
113
+ timer.unref();
114
+ });
115
+ await Promise.race([this.flush(), limit]);
116
+ clearTimeout(timer);
117
+ }
118
+ }
119
+ _Telemetry_client = new WeakMap(), _Telemetry_flushIntervalMs = new WeakMap(), _Telemetry_queue = new WeakMap(), _Telemetry_timer = new WeakMap(), _Telemetry_agent = new WeakMap(), _Telemetry_sending = new WeakMap(), _Telemetry_instances = new WeakSet(), _Telemetry_schedule = function _Telemetry_schedule() {
120
+ if (__classPrivateFieldGet(this, _Telemetry_timer, "f")) {
121
+ return;
122
+ }
123
+ // Unref'd so a pending flush never keeps the process alive.
124
+ __classPrivateFieldSet(this, _Telemetry_timer, setTimeout(() => {
125
+ __classPrivateFieldSet(this, _Telemetry_timer, undefined, "f");
126
+ void this.flush();
127
+ }, __classPrivateFieldGet(this, _Telemetry_flushIntervalMs, "f")), "f");
128
+ __classPrivateFieldGet(this, _Telemetry_timer, "f").unref();
129
+ }, _Telemetry_send = async function _Telemetry_send(events) {
130
+ for (let i = 0; i < events.length; i += MAX_BATCH) {
131
+ try {
132
+ await __classPrivateFieldGet(this, _Telemetry_client, "f").call('TrackAnalyticsEvents', {
133
+ body: {
134
+ clientId: this.clientId,
135
+ source: 'mcp',
136
+ context: {
137
+ sessionId: this.sessionId,
138
+ ...(__classPrivateFieldGet(this, _Telemetry_agent, "f") ? { agent: __classPrivateFieldGet(this, _Telemetry_agent, "f") } : {}),
139
+ app: { version: VERSION },
140
+ },
141
+ events: events.slice(i, i + MAX_BATCH),
142
+ },
143
+ });
144
+ }
145
+ catch (err) {
146
+ LOGGER.debug({ err }, 'telemetry: send failed; dropping batch');
147
+ }
148
+ }
149
+ };
150
+ /**
151
+ * Reads the anonymous client ID from the config file, or creates it. The ID
152
+ * is random and carries no information about the developer. The first time
153
+ * the server creates it, it tells the developer how to opt out.
154
+ */
155
+ function loadClientId(store) {
156
+ try {
157
+ const existing = store.read()?.analytics?.clientId;
158
+ if (existing) {
159
+ return existing;
160
+ }
161
+ const clientId = randomUUID();
162
+ store.write({
163
+ analytics: { clientId, createdAt: new Date().toISOString() },
164
+ });
165
+ LOGGER.info('Reclaim records anonymous usage data to improve the product. '
166
+ + 'To turn it off, set RECLAIM_TELEMETRY=0 or DO_NOT_TRACK=1.');
167
+ return clientId;
168
+ }
169
+ catch (err) {
170
+ // An unwritable config file shouldn't disable the server. Use an ID
171
+ // that lasts for this process only.
172
+ LOGGER.debug({ err }, 'telemetry: could not persist client ID');
173
+ return randomUUID();
174
+ }
175
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Times a tool call and records it. Call `end()` once when the call
3
+ * finishes. It never throws, so it can't change the outcome of the call.
4
+ */
5
+ export declare function startToolCall(tool: string, args: unknown): {
6
+ end(err?: unknown): void;
7
+ };
8
+ /**
9
+ * A category for the error, such as `http_401`. It's never the message,
10
+ * because messages can contain URLs, tokens, or file paths.
11
+ */
12
+ export declare function errorCode(err: unknown): string;
13
+ /**
14
+ * The only arguments Reclaim records, and only for these tools. They are the
15
+ * developer's own description of what they want to build, which tells us
16
+ * which providers to add. Every other argument is dropped here, so a new tool
17
+ * records nothing until you add it to this list on purpose.
18
+ */
19
+ export declare function allowlistedInput(tool: string, args: unknown): {
20
+ userRequest: string | undefined;
21
+ } | {
22
+ domain: string | undefined;
23
+ dataPoints: string[] | undefined;
24
+ } | undefined;