@curatedmcp/tokenshield-core 0.2.1 → 1.0.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) => {
@@ -45,6 +71,15 @@ export async function start(opts) {
45
71
  res.end(JSON.stringify({ ok: true, version: "0.1.0" }));
46
72
  return;
47
73
  }
74
+ // Friendly landing page for humans who hit the proxy port in a browser.
75
+ // The proxy itself only speaks /v1/messages, /v1/messages/stream, etc.
76
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
77
+ res.statusCode = 200;
78
+ res.setHeader("content-type", "text/html; charset=utf-8");
79
+ res.setHeader("cache-control", "no-store");
80
+ res.end(proxyLandingHtml(opts.config));
81
+ return;
82
+ }
48
83
  handleAnthropicRequest(req, res, opts.config, sink).catch((err) => {
49
84
  if (!res.headersSent) {
50
85
  res.statusCode = 500;
@@ -118,6 +153,7 @@ export async function start(opts) {
118
153
  ledger,
119
154
  close: async () => {
120
155
  clearInterval(retentionInterval);
156
+ telemetry.stop();
121
157
  await Promise.all([closeServer(proxy), closeServer(dashboard)]);
122
158
  ledger.close();
123
159
  },
@@ -127,4 +163,67 @@ function defaultDashboardHtml() {
127
163
  return `<!doctype html><meta charset="utf-8"><title>TokenShield</title>
128
164
  <body><h1>TokenShield</h1><p>Dashboard renderer not provided.</p></body>`;
129
165
  }
166
+ function proxyLandingHtml(config) {
167
+ const dashUrl = `http://${config.bind === "0.0.0.0" ? "127.0.0.1" : config.bind}:${config.dashboardPort}`;
168
+ const exportLine = `export ANTHROPIC_BASE_URL=http://${config.bind === "0.0.0.0" ? "127.0.0.1" : config.bind}:${config.port}`;
169
+ return `<!doctype html>
170
+ <html lang="en"><head>
171
+ <meta charset="utf-8">
172
+ <title>TokenShield proxy</title>
173
+ <meta name="viewport" content="width=device-width,initial-scale=1">
174
+ <style>
175
+ *{box-sizing:border-box}
176
+ body{font:15px/1.55 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0b0d12;color:#e6e7ea;margin:0;padding:40px 20px;display:flex;justify-content:center}
177
+ main{max-width:640px;width:100%}
178
+ .pill{display:inline-flex;align-items:center;gap:6px;background:rgba(34,197,94,.12);color:#4ade80;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600}
179
+ .pill::before{content:'';width:6px;height:6px;border-radius:50%;background:#22c55e;animation:pulse 2s ease-in-out infinite}
180
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
181
+ h1{font-size:28px;margin:16px 0 8px;font-weight:700}
182
+ .subtitle{color:#9ca3af;margin:0 0 28px}
183
+ .card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:20px 22px;margin:14px 0}
184
+ .card h2{font-size:14px;text-transform:uppercase;letter-spacing:.06em;color:#a5b4fc;margin:0 0 8px}
185
+ code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:rgba(255,255,255,.07);padding:2px 6px;border-radius:4px;font-size:13px;color:#fbbf24}
186
+ pre{background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:14px 16px;overflow-x:auto;margin:8px 0 0}
187
+ pre code{background:none;padding:0;color:#86efac}
188
+ a{color:#a5b4fc;text-decoration:none;border-bottom:1px solid rgba(165,180,252,.3)}
189
+ a:hover{border-color:#a5b4fc}
190
+ .footer{margin-top:32px;font-size:12px;color:#6b7280;text-align:center}
191
+ .dash-cta{display:inline-block;background:#22c55e;color:#0b0d12;font-weight:600;padding:10px 20px;border-radius:8px;text-decoration:none;border:0;margin-top:6px}
192
+ .dash-cta:hover{background:#16a34a}
193
+ </style>
194
+ </head><body><main>
195
+ <span class="pill">TokenShield is running</span>
196
+ <h1>This is the proxy, not the dashboard.</h1>
197
+ <p class="subtitle">You hit port ${config.port} — that's where Claude Code (or Cursor, Windsurf, Aider) sends its API requests. The proxy doesn't serve a UI; it forwards traffic to Anthropic.</p>
198
+
199
+ <div class="card">
200
+ <h2>→ Want the dashboard?</h2>
201
+ <p style="margin:0 0 12px">Live spend, requests, savings, and recent traffic:</p>
202
+ <a class="dash-cta" href="${dashUrl}">Open dashboard →</a>
203
+ </div>
204
+
205
+ <div class="card">
206
+ <h2>→ Route Claude Code through this proxy</h2>
207
+ <p style="margin:0 0 4px">In the shell where you run <code>claude</code>:</p>
208
+ <pre><code>${exportLine}</code></pre>
209
+ <p style="margin:12px 0 0;font-size:13px;color:#9ca3af">Your <code>ANTHROPIC_API_KEY</code> stays where it is. TokenShield never reads it.</p>
210
+ </div>
211
+
212
+ <div class="card">
213
+ <h2>→ Other clients</h2>
214
+ <p style="margin:0;font-size:13px;color:#9ca3af">
215
+ <strong style="color:#e6e7ea">Cursor / Windsurf:</strong> Settings → AI → Custom Base URL = <code>http://${config.bind === "0.0.0.0" ? "127.0.0.1" : config.bind}:${config.port}</code>
216
+ <br><br>
217
+ <strong style="color:#e6e7ea">Anthropic SDK:</strong> set <code>baseURL</code> when instantiating the client.
218
+ <br><br>
219
+ <strong style="color:#e6e7ea">Auto-configure your shell:</strong> <code>tokenshield integrations enable claude-code</code>
220
+ </p>
221
+ </div>
222
+
223
+ <p class="footer">
224
+ Docs: <a href="https://curatedmcp.com/tokenshield">curatedmcp.com/tokenshield</a> &nbsp;·&nbsp;
225
+ Source: <a href="https://github.com/oneprofile-dev/tokenshield">github.com/oneprofile-dev/tokenshield</a>
226
+ </p>
227
+ </main></body></html>`;
228
+ }
130
229
  //# sourceMappingURL=server.js.map
@@ -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": "1.0.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
 
@@ -66,6 +91,15 @@ export async function start(opts: StartOptions): Promise<ProxyServerHandle> {
66
91
  res.end(JSON.stringify({ ok: true, version: "0.1.0" }));
67
92
  return;
68
93
  }
94
+ // Friendly landing page for humans who hit the proxy port in a browser.
95
+ // The proxy itself only speaks /v1/messages, /v1/messages/stream, etc.
96
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
97
+ res.statusCode = 200;
98
+ res.setHeader("content-type", "text/html; charset=utf-8");
99
+ res.setHeader("cache-control", "no-store");
100
+ res.end(proxyLandingHtml(opts.config));
101
+ return;
102
+ }
69
103
  handleAnthropicRequest(req, res, opts.config, sink).catch((err: unknown) => {
70
104
  if (!res.headersSent) {
71
105
  res.statusCode = 500;
@@ -142,6 +176,7 @@ export async function start(opts: StartOptions): Promise<ProxyServerHandle> {
142
176
  ledger,
143
177
  close: async () => {
144
178
  clearInterval(retentionInterval);
179
+ telemetry.stop();
145
180
  await Promise.all([closeServer(proxy), closeServer(dashboard)]);
146
181
  ledger.close();
147
182
  },
@@ -152,3 +187,67 @@ function defaultDashboardHtml(): string {
152
187
  return `<!doctype html><meta charset="utf-8"><title>TokenShield</title>
153
188
  <body><h1>TokenShield</h1><p>Dashboard renderer not provided.</p></body>`;
154
189
  }
190
+
191
+ function proxyLandingHtml(config: ProxyConfig): string {
192
+ const dashUrl = `http://${config.bind === "0.0.0.0" ? "127.0.0.1" : config.bind}:${config.dashboardPort}`;
193
+ const exportLine = `export ANTHROPIC_BASE_URL=http://${config.bind === "0.0.0.0" ? "127.0.0.1" : config.bind}:${config.port}`;
194
+ return `<!doctype html>
195
+ <html lang="en"><head>
196
+ <meta charset="utf-8">
197
+ <title>TokenShield proxy</title>
198
+ <meta name="viewport" content="width=device-width,initial-scale=1">
199
+ <style>
200
+ *{box-sizing:border-box}
201
+ body{font:15px/1.55 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0b0d12;color:#e6e7ea;margin:0;padding:40px 20px;display:flex;justify-content:center}
202
+ main{max-width:640px;width:100%}
203
+ .pill{display:inline-flex;align-items:center;gap:6px;background:rgba(34,197,94,.12);color:#4ade80;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600}
204
+ .pill::before{content:'';width:6px;height:6px;border-radius:50%;background:#22c55e;animation:pulse 2s ease-in-out infinite}
205
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
206
+ h1{font-size:28px;margin:16px 0 8px;font-weight:700}
207
+ .subtitle{color:#9ca3af;margin:0 0 28px}
208
+ .card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:20px 22px;margin:14px 0}
209
+ .card h2{font-size:14px;text-transform:uppercase;letter-spacing:.06em;color:#a5b4fc;margin:0 0 8px}
210
+ code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:rgba(255,255,255,.07);padding:2px 6px;border-radius:4px;font-size:13px;color:#fbbf24}
211
+ pre{background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:14px 16px;overflow-x:auto;margin:8px 0 0}
212
+ pre code{background:none;padding:0;color:#86efac}
213
+ a{color:#a5b4fc;text-decoration:none;border-bottom:1px solid rgba(165,180,252,.3)}
214
+ a:hover{border-color:#a5b4fc}
215
+ .footer{margin-top:32px;font-size:12px;color:#6b7280;text-align:center}
216
+ .dash-cta{display:inline-block;background:#22c55e;color:#0b0d12;font-weight:600;padding:10px 20px;border-radius:8px;text-decoration:none;border:0;margin-top:6px}
217
+ .dash-cta:hover{background:#16a34a}
218
+ </style>
219
+ </head><body><main>
220
+ <span class="pill">TokenShield is running</span>
221
+ <h1>This is the proxy, not the dashboard.</h1>
222
+ <p class="subtitle">You hit port ${config.port} — that's where Claude Code (or Cursor, Windsurf, Aider) sends its API requests. The proxy doesn't serve a UI; it forwards traffic to Anthropic.</p>
223
+
224
+ <div class="card">
225
+ <h2>→ Want the dashboard?</h2>
226
+ <p style="margin:0 0 12px">Live spend, requests, savings, and recent traffic:</p>
227
+ <a class="dash-cta" href="${dashUrl}">Open dashboard →</a>
228
+ </div>
229
+
230
+ <div class="card">
231
+ <h2>→ Route Claude Code through this proxy</h2>
232
+ <p style="margin:0 0 4px">In the shell where you run <code>claude</code>:</p>
233
+ <pre><code>${exportLine}</code></pre>
234
+ <p style="margin:12px 0 0;font-size:13px;color:#9ca3af">Your <code>ANTHROPIC_API_KEY</code> stays where it is. TokenShield never reads it.</p>
235
+ </div>
236
+
237
+ <div class="card">
238
+ <h2>→ Other clients</h2>
239
+ <p style="margin:0;font-size:13px;color:#9ca3af">
240
+ <strong style="color:#e6e7ea">Cursor / Windsurf:</strong> Settings → AI → Custom Base URL = <code>http://${config.bind === "0.0.0.0" ? "127.0.0.1" : config.bind}:${config.port}</code>
241
+ <br><br>
242
+ <strong style="color:#e6e7ea">Anthropic SDK:</strong> set <code>baseURL</code> when instantiating the client.
243
+ <br><br>
244
+ <strong style="color:#e6e7ea">Auto-configure your shell:</strong> <code>tokenshield integrations enable claude-code</code>
245
+ </p>
246
+ </div>
247
+
248
+ <p class="footer">
249
+ Docs: <a href="https://curatedmcp.com/tokenshield">curatedmcp.com/tokenshield</a> &nbsp;·&nbsp;
250
+ Source: <a href="https://github.com/oneprofile-dev/tokenshield">github.com/oneprofile-dev/tokenshield</a>
251
+ </p>
252
+ </main></body></html>`;
253
+ }
@@ -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();