@curatedmcp/tokenshield-core 0.2.1 → 0.3.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/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export { StreamUsageAccumulator, usageFromJson } from "./proxy/usage.js";
8
8
  export { handleAnthropicRequest, setProcessorEnabled, getProcessorEnabledIds, getResponseCacheStats, } from "./proxy/anthropic-passthrough.js";
9
9
  export { providerForPath, anthropic } from "./providers/registry.js";
10
10
  export type { Provider, Conversation, ConvMessage, ConvBlock, ProviderId } from "./providers/types.js";
11
+ export { telemetry, Telemetry, isTelemetryEnabled, setTelemetryEnabled, isFirstRun, markFirstRunComplete, firstRunBanner, getAnonId, } from "./telemetry.js";
12
+ export type { TelemetryRecord } from "./telemetry.js";
11
13
  export { Pipeline } from "./processors/pipeline.js";
12
14
  export { conversationDedup } from "./processors/conversation-dedup.js";
13
15
  export { ResponseCache } from "./processors/response-cache.js";
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { SSEParser } from "./proxy/sse.js";
5
5
  export { StreamUsageAccumulator, usageFromJson } from "./proxy/usage.js";
6
6
  export { handleAnthropicRequest, setProcessorEnabled, getProcessorEnabledIds, getResponseCacheStats, } from "./proxy/anthropic-passthrough.js";
7
7
  export { providerForPath, anthropic } from "./providers/registry.js";
8
+ export { telemetry, Telemetry, isTelemetryEnabled, setTelemetryEnabled, isFirstRun, markFirstRunComplete, firstRunBanner, getAnonId, } from "./telemetry.js";
8
9
  export { Pipeline } from "./processors/pipeline.js";
9
10
  export { conversationDedup } from "./processors/conversation-dedup.js";
10
11
  export { ResponseCache } from "./processors/response-cache.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,EACL,SAAS,EACT,SAAS,EACT,kBAAkB,EAClB,mBAAmB,EACnB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,SAAS,GACV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC"}
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createServer } from "node:http";
2
2
  import { handleAnthropicRequest } from "./proxy/anthropic-passthrough.js";
3
3
  import { Ledger } from "./ledger.js";
4
+ import { telemetry } from "./telemetry.js";
4
5
  export function defaultConfig(overrides = {}) {
5
6
  const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".";
6
7
  return {
@@ -29,6 +30,7 @@ async function closeServer(server) {
29
30
  }
30
31
  export async function start(opts) {
31
32
  const ledger = new Ledger(opts.config.ledgerPath);
33
+ const isTeamDeployment = opts.config.bind === "0.0.0.0";
32
34
  const sink = (r) => {
33
35
  try {
34
36
  ledger.record(r);
@@ -36,6 +38,30 @@ export async function start(opts) {
36
38
  catch {
37
39
  // ledger errors must never break the request path
38
40
  }
41
+ try {
42
+ // Approximate byte counts from token estimates (industry rule-of-thumb)
43
+ const tokensIn = r.usageRaw.inputTokens + r.usageRaw.cacheReadInputTokens;
44
+ const tokensOut = r.usageSent.inputTokens + r.usageSent.cacheReadInputTokens;
45
+ const TOKEN_TO_BYTE = 3.5;
46
+ const bytesIn = Math.round(tokensIn * TOKEN_TO_BYTE);
47
+ const bytesOut = Math.round(tokensOut * TOKEN_TO_BYTE);
48
+ telemetry.record({
49
+ bytesIn,
50
+ bytesOut,
51
+ bytesSaved: Math.max(0, bytesIn - bytesOut),
52
+ inputTokens: r.usageRaw.inputTokens,
53
+ outputTokens: r.usageRaw.outputTokens,
54
+ dollarsEstimate: r.dollarsRaw,
55
+ dollarsSaved: r.dollarsSaved,
56
+ provider: "anthropic",
57
+ model: r.model,
58
+ client: null,
59
+ teamDeployment: isTeamDeployment,
60
+ });
61
+ }
62
+ catch {
63
+ // telemetry must never break the request path
64
+ }
39
65
  opts.onRecord?.(r);
40
66
  };
41
67
  const proxy = createServer((req, res) => {
@@ -118,6 +144,7 @@ export async function start(opts) {
118
144
  ledger,
119
145
  close: async () => {
120
146
  clearInterval(retentionInterval);
147
+ telemetry.stop();
121
148
  await Promise.all([closeServer(proxy), closeServer(dashboard)]);
122
149
  ledger.close();
123
150
  },
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export declare function isTelemetryEnabled(): boolean;
2
+ export declare function setTelemetryEnabled(on: boolean): void;
3
+ export declare function getAnonId(): string;
4
+ export declare function isFirstRun(): boolean;
5
+ export declare function markFirstRunComplete(): void;
6
+ export declare function firstRunBanner(): string;
7
+ export interface TelemetryRecord {
8
+ bytesIn: number;
9
+ bytesOut: number;
10
+ bytesSaved: number;
11
+ inputTokens: number;
12
+ outputTokens: number;
13
+ dollarsEstimate: number;
14
+ dollarsSaved: number;
15
+ provider: "anthropic" | "openai" | "gemini" | null;
16
+ model: string | null;
17
+ client: string | null;
18
+ teamDeployment: boolean;
19
+ }
20
+ export declare class Telemetry {
21
+ private counters;
22
+ private timer;
23
+ private flushing;
24
+ start(): void;
25
+ stop(): void;
26
+ record(r: TelemetryRecord): void;
27
+ flush(): Promise<void>;
28
+ }
29
+ export declare const telemetry: Telemetry;
@@ -0,0 +1,218 @@
1
+ import { createHash } from "node:crypto";
2
+ import { hostname, platform, userInfo } from "node:os";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ const ENDPOINT = process.env["TOKENSHIELD_TELEMETRY_URL"] ?? "https://curatedmcp.com/api/v1/tokenshield/telemetry";
6
+ // Flush every N requests OR every M minutes, whichever first. Keeps the
7
+ // network footprint tiny and ensures dev sessions still report at the end.
8
+ const FLUSH_EVERY_REQUESTS = 50;
9
+ const FLUSH_EVERY_MS = 5 * 60 * 1000;
10
+ function telemetryDir() {
11
+ const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".";
12
+ return join(home, ".tokenshield");
13
+ }
14
+ function settingsFile() {
15
+ return join(telemetryDir(), "settings.json");
16
+ }
17
+ function loadSettings() {
18
+ const file = settingsFile();
19
+ if (existsSync(file)) {
20
+ try {
21
+ const raw = readFileSync(file, "utf8");
22
+ const parsed = JSON.parse(raw);
23
+ return {
24
+ telemetry: parsed.telemetry === "off" ? "off" : "on",
25
+ anonId: parsed.anonId ?? generateAnonId(),
26
+ firstRunCompleted: parsed.firstRunCompleted ?? false,
27
+ };
28
+ }
29
+ catch {
30
+ // fall through
31
+ }
32
+ }
33
+ return {
34
+ telemetry: "on",
35
+ anonId: generateAnonId(),
36
+ firstRunCompleted: false,
37
+ };
38
+ }
39
+ function saveSettings(s) {
40
+ const dir = telemetryDir();
41
+ if (!existsSync(dir))
42
+ mkdirSync(dir, { recursive: true });
43
+ writeFileSync(settingsFile(), JSON.stringify(s, null, 2), "utf8");
44
+ }
45
+ function generateAnonId() {
46
+ // Deterministic per-machine: sha256 of hostname + username.
47
+ // Cannot be reversed to a person; cannot be cross-correlated with other
48
+ // CuratedMCP products without explicit account linking.
49
+ const seed = `${hostname()}::${userInfo().username}::tokenshield`;
50
+ return createHash("sha256").update(seed).digest("hex");
51
+ }
52
+ export function isTelemetryEnabled() {
53
+ // Hard kill switches — these win over settings file
54
+ if (process.env["TOKENSHIELD_TELEMETRY"] === "0" || process.env["TOKENSHIELD_TELEMETRY"] === "off")
55
+ return false;
56
+ if (process.env["DO_NOT_TRACK"] === "1")
57
+ return false;
58
+ if (process.env["CI"] === "true")
59
+ return false;
60
+ const s = loadSettings();
61
+ return s.telemetry === "on";
62
+ }
63
+ export function setTelemetryEnabled(on) {
64
+ const s = loadSettings();
65
+ s.telemetry = on ? "on" : "off";
66
+ s.firstRunCompleted = true;
67
+ saveSettings(s);
68
+ }
69
+ export function getAnonId() {
70
+ return loadSettings().anonId;
71
+ }
72
+ export function isFirstRun() {
73
+ return !loadSettings().firstRunCompleted;
74
+ }
75
+ export function markFirstRunComplete() {
76
+ const s = loadSettings();
77
+ s.firstRunCompleted = true;
78
+ saveSettings(s);
79
+ }
80
+ export function firstRunBanner() {
81
+ return [
82
+ "",
83
+ " ┌──────────────────────────────────────────────────────────────────┐",
84
+ " │ TokenShield collects anonymous usage stats by default: │",
85
+ " │ • Aggregate token counts and $ saved │",
86
+ " │ • CLI version, Node version, OS │",
87
+ " │ • Provider (anthropic/openai/gemini) + most-used model │",
88
+ " │ │",
89
+ " │ Never sent: prompts, responses, file contents, API keys, │",
90
+ " │ IP address, hostname, username, file paths. │",
91
+ " │ │",
92
+ " │ Disable any time: tokenshield telemetry off │",
93
+ " │ Or via env: TOKENSHIELD_TELEMETRY=0 (or DO_NOT_TRACK=1) │",
94
+ " │ Source: https://github.com/oneprofile-dev/tokenshield │",
95
+ " └──────────────────────────────────────────────────────────────────┘",
96
+ "",
97
+ ].join("\n");
98
+ }
99
+ function emptyCounters() {
100
+ return {
101
+ requests: 0,
102
+ bytesIn: 0,
103
+ bytesOut: 0,
104
+ bytesSaved: 0,
105
+ inputTokens: 0,
106
+ outputTokens: 0,
107
+ dollarsEstimate: 0,
108
+ dollarsSaved: 0,
109
+ modelCounts: new Map(),
110
+ provider: null,
111
+ client: null,
112
+ teamDeployment: false,
113
+ };
114
+ }
115
+ export class Telemetry {
116
+ counters = emptyCounters();
117
+ timer = null;
118
+ flushing = false;
119
+ start() {
120
+ if (!isTelemetryEnabled())
121
+ return;
122
+ if (this.timer)
123
+ return;
124
+ this.timer = setInterval(() => {
125
+ void this.flush();
126
+ }, FLUSH_EVERY_MS);
127
+ this.timer.unref?.();
128
+ }
129
+ stop() {
130
+ if (this.timer) {
131
+ clearInterval(this.timer);
132
+ this.timer = null;
133
+ }
134
+ // Best-effort final flush — fire and forget
135
+ void this.flush();
136
+ }
137
+ record(r) {
138
+ if (!isTelemetryEnabled())
139
+ return;
140
+ const c = this.counters;
141
+ c.requests += 1;
142
+ c.bytesIn += r.bytesIn;
143
+ c.bytesOut += r.bytesOut;
144
+ c.bytesSaved += r.bytesSaved;
145
+ c.inputTokens += r.inputTokens;
146
+ c.outputTokens += r.outputTokens;
147
+ c.dollarsEstimate += r.dollarsEstimate;
148
+ c.dollarsSaved += r.dollarsSaved;
149
+ if (r.provider)
150
+ c.provider = r.provider;
151
+ if (r.client)
152
+ c.client = r.client;
153
+ if (r.teamDeployment)
154
+ c.teamDeployment = true;
155
+ if (r.model)
156
+ c.modelCounts.set(r.model, (c.modelCounts.get(r.model) ?? 0) + 1);
157
+ if (c.requests >= FLUSH_EVERY_REQUESTS) {
158
+ void this.flush();
159
+ }
160
+ }
161
+ async flush() {
162
+ if (this.flushing)
163
+ return;
164
+ if (!isTelemetryEnabled())
165
+ return;
166
+ if (this.counters.requests === 0)
167
+ return;
168
+ this.flushing = true;
169
+ const snap = this.counters;
170
+ this.counters = emptyCounters();
171
+ let topModel = null;
172
+ let topModelCount = 0;
173
+ for (const [model, count] of snap.modelCounts) {
174
+ if (count > topModelCount) {
175
+ topModel = model;
176
+ topModelCount = count;
177
+ }
178
+ }
179
+ const cliVersion = process.env["npm_package_version"] ?? "unknown";
180
+ const payload = {
181
+ anonId: getAnonId(),
182
+ cliVersion,
183
+ nodeVersion: process.version,
184
+ platform: platform(),
185
+ requests: snap.requests,
186
+ bytesIn: snap.bytesIn,
187
+ bytesOut: snap.bytesOut,
188
+ bytesSaved: snap.bytesSaved,
189
+ inputTokens: snap.inputTokens,
190
+ outputTokens: snap.outputTokens,
191
+ dollarsEstimate: Math.round(snap.dollarsEstimate * 1e6) / 1e6,
192
+ dollarsSaved: Math.round(snap.dollarsSaved * 1e6) / 1e6,
193
+ provider: snap.provider ?? undefined,
194
+ topModel: topModel ?? undefined,
195
+ client: snap.client ?? undefined,
196
+ teamDeployment: snap.teamDeployment,
197
+ };
198
+ try {
199
+ const controller = new AbortController();
200
+ const t = setTimeout(() => controller.abort(), 5000);
201
+ await fetch(ENDPOINT, {
202
+ method: "POST",
203
+ headers: { "Content-Type": "application/json" },
204
+ body: JSON.stringify(payload),
205
+ signal: controller.signal,
206
+ });
207
+ clearTimeout(t);
208
+ }
209
+ catch {
210
+ // Telemetry must NEVER affect user experience. Drop the batch silently.
211
+ }
212
+ finally {
213
+ this.flushing = false;
214
+ }
215
+ }
216
+ }
217
+ export const telemetry = new Telemetry();
218
+ //# sourceMappingURL=telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curatedmcp/tokenshield-core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "TokenShield proxy engine — Anthropic API-layer middleware with token accounting",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -13,6 +13,17 @@ export {
13
13
  } from "./proxy/anthropic-passthrough.js";
14
14
  export { providerForPath, anthropic } from "./providers/registry.js";
15
15
  export type { Provider, Conversation, ConvMessage, ConvBlock, ProviderId } from "./providers/types.js";
16
+ export {
17
+ telemetry,
18
+ Telemetry,
19
+ isTelemetryEnabled,
20
+ setTelemetryEnabled,
21
+ isFirstRun,
22
+ markFirstRunComplete,
23
+ firstRunBanner,
24
+ getAnonId,
25
+ } from "./telemetry.js";
26
+ export type { TelemetryRecord } from "./telemetry.js";
16
27
  export { Pipeline } from "./processors/pipeline.js";
17
28
  export { conversationDedup } from "./processors/conversation-dedup.js";
18
29
  export { ResponseCache } from "./processors/response-cache.js";
package/src/server.ts CHANGED
@@ -2,6 +2,7 @@ import { createServer, IncomingMessage, ServerResponse, Server } from "node:http
2
2
  import type { ProxyConfig, RequestRecord } from "./types.js";
3
3
  import { handleAnthropicRequest } from "./proxy/anthropic-passthrough.js";
4
4
  import { Ledger } from "./ledger.js";
5
+ import { telemetry } from "./telemetry.js";
5
6
 
6
7
  export interface ProxyServerHandle {
7
8
  proxy: Server;
@@ -50,12 +51,36 @@ async function closeServer(server: Server): Promise<void> {
50
51
  export async function start(opts: StartOptions): Promise<ProxyServerHandle> {
51
52
  const ledger = new Ledger(opts.config.ledgerPath);
52
53
 
54
+ const isTeamDeployment = opts.config.bind === "0.0.0.0";
53
55
  const sink = (r: RequestRecord): void => {
54
56
  try {
55
57
  ledger.record(r);
56
58
  } catch {
57
59
  // ledger errors must never break the request path
58
60
  }
61
+ try {
62
+ // Approximate byte counts from token estimates (industry rule-of-thumb)
63
+ const tokensIn = r.usageRaw.inputTokens + r.usageRaw.cacheReadInputTokens;
64
+ const tokensOut = r.usageSent.inputTokens + r.usageSent.cacheReadInputTokens;
65
+ const TOKEN_TO_BYTE = 3.5;
66
+ const bytesIn = Math.round(tokensIn * TOKEN_TO_BYTE);
67
+ const bytesOut = Math.round(tokensOut * TOKEN_TO_BYTE);
68
+ telemetry.record({
69
+ bytesIn,
70
+ bytesOut,
71
+ bytesSaved: Math.max(0, bytesIn - bytesOut),
72
+ inputTokens: r.usageRaw.inputTokens,
73
+ outputTokens: r.usageRaw.outputTokens,
74
+ dollarsEstimate: r.dollarsRaw,
75
+ dollarsSaved: r.dollarsSaved,
76
+ provider: "anthropic",
77
+ model: r.model,
78
+ client: null,
79
+ teamDeployment: isTeamDeployment,
80
+ });
81
+ } catch {
82
+ // telemetry must never break the request path
83
+ }
59
84
  opts.onRecord?.(r);
60
85
  };
61
86
 
@@ -142,6 +167,7 @@ export async function start(opts: StartOptions): Promise<ProxyServerHandle> {
142
167
  ledger,
143
168
  close: async () => {
144
169
  clearInterval(retentionInterval);
170
+ telemetry.stop();
145
171
  await Promise.all([closeServer(proxy), closeServer(dashboard)]);
146
172
  ledger.close();
147
173
  },
@@ -0,0 +1,265 @@
1
+ import { createHash } from "node:crypto";
2
+ import { hostname, platform, userInfo } from "node:os";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const ENDPOINT = process.env["TOKENSHIELD_TELEMETRY_URL"] ?? "https://curatedmcp.com/api/v1/tokenshield/telemetry";
7
+
8
+ // Flush every N requests OR every M minutes, whichever first. Keeps the
9
+ // network footprint tiny and ensures dev sessions still report at the end.
10
+ const FLUSH_EVERY_REQUESTS = 50;
11
+ const FLUSH_EVERY_MS = 5 * 60 * 1000;
12
+
13
+ function telemetryDir(): string {
14
+ const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".";
15
+ return join(home, ".tokenshield");
16
+ }
17
+
18
+ function settingsFile(): string {
19
+ return join(telemetryDir(), "settings.json");
20
+ }
21
+
22
+ interface Settings {
23
+ telemetry: "on" | "off";
24
+ anonId: string;
25
+ firstRunCompleted: boolean;
26
+ }
27
+
28
+ function loadSettings(): Settings {
29
+ const file = settingsFile();
30
+ if (existsSync(file)) {
31
+ try {
32
+ const raw = readFileSync(file, "utf8");
33
+ const parsed = JSON.parse(raw) as Partial<Settings>;
34
+ return {
35
+ telemetry: parsed.telemetry === "off" ? "off" : "on",
36
+ anonId: parsed.anonId ?? generateAnonId(),
37
+ firstRunCompleted: parsed.firstRunCompleted ?? false,
38
+ };
39
+ } catch {
40
+ // fall through
41
+ }
42
+ }
43
+ return {
44
+ telemetry: "on",
45
+ anonId: generateAnonId(),
46
+ firstRunCompleted: false,
47
+ };
48
+ }
49
+
50
+ function saveSettings(s: Settings): void {
51
+ const dir = telemetryDir();
52
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
53
+ writeFileSync(settingsFile(), JSON.stringify(s, null, 2), "utf8");
54
+ }
55
+
56
+ function generateAnonId(): string {
57
+ // Deterministic per-machine: sha256 of hostname + username.
58
+ // Cannot be reversed to a person; cannot be cross-correlated with other
59
+ // CuratedMCP products without explicit account linking.
60
+ const seed = `${hostname()}::${userInfo().username}::tokenshield`;
61
+ return createHash("sha256").update(seed).digest("hex");
62
+ }
63
+
64
+ export function isTelemetryEnabled(): boolean {
65
+ // Hard kill switches — these win over settings file
66
+ if (process.env["TOKENSHIELD_TELEMETRY"] === "0" || process.env["TOKENSHIELD_TELEMETRY"] === "off") return false;
67
+ if (process.env["DO_NOT_TRACK"] === "1") return false;
68
+ if (process.env["CI"] === "true") return false;
69
+
70
+ const s = loadSettings();
71
+ return s.telemetry === "on";
72
+ }
73
+
74
+ export function setTelemetryEnabled(on: boolean): void {
75
+ const s = loadSettings();
76
+ s.telemetry = on ? "on" : "off";
77
+ s.firstRunCompleted = true;
78
+ saveSettings(s);
79
+ }
80
+
81
+ export function getAnonId(): string {
82
+ return loadSettings().anonId;
83
+ }
84
+
85
+ export function isFirstRun(): boolean {
86
+ return !loadSettings().firstRunCompleted;
87
+ }
88
+
89
+ export function markFirstRunComplete(): void {
90
+ const s = loadSettings();
91
+ s.firstRunCompleted = true;
92
+ saveSettings(s);
93
+ }
94
+
95
+ export function firstRunBanner(): string {
96
+ return [
97
+ "",
98
+ " ┌──────────────────────────────────────────────────────────────────┐",
99
+ " │ TokenShield collects anonymous usage stats by default: │",
100
+ " │ • Aggregate token counts and $ saved │",
101
+ " │ • CLI version, Node version, OS │",
102
+ " │ • Provider (anthropic/openai/gemini) + most-used model │",
103
+ " │ │",
104
+ " │ Never sent: prompts, responses, file contents, API keys, │",
105
+ " │ IP address, hostname, username, file paths. │",
106
+ " │ │",
107
+ " │ Disable any time: tokenshield telemetry off │",
108
+ " │ Or via env: TOKENSHIELD_TELEMETRY=0 (or DO_NOT_TRACK=1) │",
109
+ " │ Source: https://github.com/oneprofile-dev/tokenshield │",
110
+ " └──────────────────────────────────────────────────────────────────┘",
111
+ "",
112
+ ].join("\n");
113
+ }
114
+
115
+ // ─── Batched counters ────────────────────────────────────────────────────────
116
+
117
+ interface Counters {
118
+ requests: number;
119
+ bytesIn: number;
120
+ bytesOut: number;
121
+ bytesSaved: number;
122
+ inputTokens: number;
123
+ outputTokens: number;
124
+ dollarsEstimate: number;
125
+ dollarsSaved: number;
126
+ modelCounts: Map<string, number>;
127
+ provider: string | null;
128
+ client: string | null;
129
+ teamDeployment: boolean;
130
+ }
131
+
132
+ function emptyCounters(): Counters {
133
+ return {
134
+ requests: 0,
135
+ bytesIn: 0,
136
+ bytesOut: 0,
137
+ bytesSaved: 0,
138
+ inputTokens: 0,
139
+ outputTokens: 0,
140
+ dollarsEstimate: 0,
141
+ dollarsSaved: 0,
142
+ modelCounts: new Map(),
143
+ provider: null,
144
+ client: null,
145
+ teamDeployment: false,
146
+ };
147
+ }
148
+
149
+ export interface TelemetryRecord {
150
+ bytesIn: number;
151
+ bytesOut: number;
152
+ bytesSaved: number;
153
+ inputTokens: number;
154
+ outputTokens: number;
155
+ dollarsEstimate: number;
156
+ dollarsSaved: number;
157
+ provider: "anthropic" | "openai" | "gemini" | null;
158
+ model: string | null;
159
+ client: string | null;
160
+ teamDeployment: boolean;
161
+ }
162
+
163
+ export class Telemetry {
164
+ private counters = emptyCounters();
165
+ private timer: NodeJS.Timeout | null = null;
166
+ private flushing = false;
167
+
168
+ start(): void {
169
+ if (!isTelemetryEnabled()) return;
170
+ if (this.timer) return;
171
+ this.timer = setInterval(() => {
172
+ void this.flush();
173
+ }, FLUSH_EVERY_MS);
174
+ this.timer.unref?.();
175
+ }
176
+
177
+ stop(): void {
178
+ if (this.timer) {
179
+ clearInterval(this.timer);
180
+ this.timer = null;
181
+ }
182
+ // Best-effort final flush — fire and forget
183
+ void this.flush();
184
+ }
185
+
186
+ record(r: TelemetryRecord): void {
187
+ if (!isTelemetryEnabled()) return;
188
+
189
+ const c = this.counters;
190
+ c.requests += 1;
191
+ c.bytesIn += r.bytesIn;
192
+ c.bytesOut += r.bytesOut;
193
+ c.bytesSaved += r.bytesSaved;
194
+ c.inputTokens += r.inputTokens;
195
+ c.outputTokens += r.outputTokens;
196
+ c.dollarsEstimate += r.dollarsEstimate;
197
+ c.dollarsSaved += r.dollarsSaved;
198
+ if (r.provider) c.provider = r.provider;
199
+ if (r.client) c.client = r.client;
200
+ if (r.teamDeployment) c.teamDeployment = true;
201
+ if (r.model) c.modelCounts.set(r.model, (c.modelCounts.get(r.model) ?? 0) + 1);
202
+
203
+ if (c.requests >= FLUSH_EVERY_REQUESTS) {
204
+ void this.flush();
205
+ }
206
+ }
207
+
208
+ async flush(): Promise<void> {
209
+ if (this.flushing) return;
210
+ if (!isTelemetryEnabled()) return;
211
+ if (this.counters.requests === 0) return;
212
+
213
+ this.flushing = true;
214
+ const snap = this.counters;
215
+ this.counters = emptyCounters();
216
+
217
+ let topModel: string | null = null;
218
+ let topModelCount = 0;
219
+ for (const [model, count] of snap.modelCounts) {
220
+ if (count > topModelCount) {
221
+ topModel = model;
222
+ topModelCount = count;
223
+ }
224
+ }
225
+
226
+ const cliVersion = process.env["npm_package_version"] ?? "unknown";
227
+
228
+ const payload = {
229
+ anonId: getAnonId(),
230
+ cliVersion,
231
+ nodeVersion: process.version,
232
+ platform: platform(),
233
+ requests: snap.requests,
234
+ bytesIn: snap.bytesIn,
235
+ bytesOut: snap.bytesOut,
236
+ bytesSaved: snap.bytesSaved,
237
+ inputTokens: snap.inputTokens,
238
+ outputTokens: snap.outputTokens,
239
+ dollarsEstimate: Math.round(snap.dollarsEstimate * 1e6) / 1e6,
240
+ dollarsSaved: Math.round(snap.dollarsSaved * 1e6) / 1e6,
241
+ provider: snap.provider ?? undefined,
242
+ topModel: topModel ?? undefined,
243
+ client: snap.client ?? undefined,
244
+ teamDeployment: snap.teamDeployment,
245
+ };
246
+
247
+ try {
248
+ const controller = new AbortController();
249
+ const t = setTimeout(() => controller.abort(), 5000);
250
+ await fetch(ENDPOINT, {
251
+ method: "POST",
252
+ headers: { "Content-Type": "application/json" },
253
+ body: JSON.stringify(payload),
254
+ signal: controller.signal,
255
+ });
256
+ clearTimeout(t);
257
+ } catch {
258
+ // Telemetry must NEVER affect user experience. Drop the batch silently.
259
+ } finally {
260
+ this.flushing = false;
261
+ }
262
+ }
263
+ }
264
+
265
+ export const telemetry = new Telemetry();