tokendiet 1.0.0 → 1.2.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.

Potentially problematic release.


This version of tokendiet might be problematic. Click here for more details.

Files changed (52) hide show
  1. package/README.md +47 -0
  2. package/dist/cli.js +37 -23
  3. package/dist/commands/analytics-reader.d.ts +43 -0
  4. package/dist/commands/analytics-reader.js +160 -0
  5. package/dist/commands/start.d.ts +4 -1
  6. package/dist/commands/start.js +106 -41
  7. package/dist/commands/utils.d.ts +5 -3
  8. package/dist/commands/utils.js +63 -6
  9. package/dist/configure-app.d.ts +4 -0
  10. package/dist/configure-app.js +13 -0
  11. package/dist/main.js +9 -2
  12. package/dist/proxy/analytics.service.d.ts +35 -0
  13. package/dist/proxy/analytics.service.js +193 -0
  14. package/dist/proxy/cli-output.d.ts +5 -0
  15. package/dist/proxy/cli-output.js +56 -0
  16. package/dist/proxy/cloud-api.service.d.ts +53 -0
  17. package/dist/proxy/cloud-api.service.js +56 -0
  18. package/dist/proxy/proxy.controller.d.ts +23 -6
  19. package/dist/proxy/proxy.controller.js +149 -15
  20. package/dist/proxy/proxy.module.js +4 -2
  21. package/dist/proxy/proxy.service.d.ts +1 -0
  22. package/dist/proxy/proxy.service.js +29 -13
  23. package/dist/proxy/token-count.service.d.ts +41 -0
  24. package/dist/proxy/token-count.service.js +93 -0
  25. package/dist/token-stripper/provider-detector.util.d.ts +16 -0
  26. package/dist/token-stripper/provider-detector.util.js +120 -0
  27. package/dist/token-stripper/token-stripper.module.d.ts +2 -0
  28. package/dist/token-stripper/token-stripper.module.js +21 -0
  29. package/dist/token-stripper/token-stripper.service.d.ts +31 -0
  30. package/dist/token-stripper/token-stripper.service.js +102 -0
  31. package/dist/utils/content-byte-size.d.ts +8 -0
  32. package/dist/utils/content-byte-size.js +54 -0
  33. package/dist/utils/message-stripper.d.ts +1 -0
  34. package/dist/utils/message-stripper.js +31 -4
  35. package/dist/utils/system-tools-stripper.d.ts +10 -0
  36. package/dist/utils/system-tools-stripper.js +214 -0
  37. package/package.json +20 -9
  38. package/dist/app.controller.js.map +0 -1
  39. package/dist/app.module.js.map +0 -1
  40. package/dist/app.service.js.map +0 -1
  41. package/dist/cli.js.map +0 -1
  42. package/dist/commands/start.js.map +0 -1
  43. package/dist/commands/stop.d.ts +0 -6
  44. package/dist/commands/stop.js +0 -68
  45. package/dist/commands/stop.js.map +0 -1
  46. package/dist/commands/utils.js.map +0 -1
  47. package/dist/main.js.map +0 -1
  48. package/dist/proxy/proxy.controller.js.map +0 -1
  49. package/dist/proxy/proxy.module.js.map +0 -1
  50. package/dist/proxy/proxy.service.js.map +0 -1
  51. package/dist/tsconfig.build.tsbuildinfo +0 -1
  52. package/dist/utils/message-stripper.js.map +0 -1
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # tokendiet
2
+
3
+ Token optimization gateway CLI for Claude Code. Runs as a local proxy between Claude Code and the Anthropic API, automatically stripping unnecessary tokens before each request.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g tokendiet
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Start the gateway (foreground, Ctrl+C to stop)
15
+ tokendiet start --org-token <token>
16
+
17
+ # With custom cloud API URL
18
+ tokendiet start --org-token <token> --cloud-url https://api.tokendiet.dev
19
+ ```
20
+
21
+ ## How it works
22
+
23
+ 1. Starts a local proxy on `localhost:3100`
24
+ 2. Patches `~/.claude/settings.json` to route Claude Code traffic through it
25
+ 3. Applies stripping strategies to each request before forwarding to Anthropic
26
+ 4. Reports token savings to the cloud API
27
+ 5. Restores original Claude settings on exit (Ctrl+C)
28
+
29
+ ## Stripping strategies
30
+
31
+ | Strategy | What it removes |
32
+ |----------|----------------|
33
+ | Whitespace normalization | Unnecessary whitespace from message content and tool results |
34
+ | Schema metadata stripping | JSON Schema metadata (descriptions, titles, examples) from the `tools` array |
35
+
36
+ Both strategies are deterministic and idempotent.
37
+
38
+ ## Development
39
+
40
+ ```bash
41
+ # npm run start — builds and runs the CLI binary with dev defaults
42
+ npm run start
43
+ ```
44
+
45
+ ## License
46
+
47
+ MIT
package/dist/cli.js CHANGED
@@ -2,7 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const commander_1 = require("commander");
4
4
  const start_1 = require("./commands/start");
5
- const stop_1 = require("./commands/stop");
5
+ const utils_1 = require("./commands/utils");
6
+ async function resolveToken(cliToken) {
7
+ if (cliToken)
8
+ return cliToken;
9
+ const saved = (0, utils_1.loadSavedToken)();
10
+ if (saved) {
11
+ console.log(`[tokendiet] Using saved token (${saved.slice(0, 11)}...)`);
12
+ return saved;
13
+ }
14
+ return (0, utils_1.promptForToken)('Enter your API token: ');
15
+ }
6
16
  const program = new commander_1.Command();
7
17
  program
8
18
  .name('tokendiet')
@@ -11,26 +21,37 @@ program
11
21
  program
12
22
  .command('start')
13
23
  .description('Start the local TokenDiet gateway')
14
- .option('--api-key <key>', 'Your organization API key')
24
+ .option('--org-token <token>', 'Your organization API token')
25
+ .option('--cloud-url <url>', 'Cloud API URL', 'https://tokendiet-production.up.railway.app')
15
26
  .option('--port <port>', 'Port to listen on (default: 3100)', '3100')
27
+ .option('--verbose', 'Show full NestJS logs')
16
28
  .action(async (opts) => {
17
- const result = await (0, start_1.startGateway)({
18
- apiKey: opts.apiKey ?? '',
29
+ let orgToken = await resolveToken(opts.orgToken);
30
+ if (!orgToken) {
31
+ console.error('No token provided. Exiting.');
32
+ process.exit(1);
33
+ }
34
+ let result = await (0, start_1.startGateway)({
35
+ orgToken,
36
+ cloudUrl: opts.cloudUrl,
19
37
  port: parseInt(opts.port, 10),
38
+ verbose: opts.verbose ?? false,
20
39
  });
21
- if (result.success) {
22
- console.log(result.message);
40
+ while (result.unauthorized) {
41
+ (0, utils_1.clearSavedToken)();
42
+ console.error('[tokendiet] Token invalid.');
43
+ orgToken = await (0, utils_1.promptForToken)('Try again (or press Enter to quit): ');
44
+ if (!orgToken) {
45
+ console.error('No token provided. Exiting.');
46
+ process.exit(1);
47
+ }
48
+ result = await (0, start_1.startGateway)({
49
+ orgToken,
50
+ cloudUrl: opts.cloudUrl,
51
+ port: parseInt(opts.port, 10),
52
+ verbose: opts.verbose ?? false,
53
+ });
23
54
  }
24
- else {
25
- console.error(result.error);
26
- process.exit(1);
27
- }
28
- });
29
- program
30
- .command('stop')
31
- .description('Stop the local TokenDiet gateway')
32
- .action(async () => {
33
- const result = await (0, stop_1.stopGateway)();
34
55
  if (result.success) {
35
56
  console.log(result.message);
36
57
  }
@@ -39,12 +60,5 @@ program
39
60
  process.exit(1);
40
61
  }
41
62
  });
42
- program
43
- .command('status')
44
- .description('Check whether the TokenDiet gateway is running')
45
- .action(() => {
46
- console.log('status command not yet implemented');
47
- process.exit(0);
48
- });
49
63
  program.parse(process.argv);
50
64
  //# sourceMappingURL=cli.js.map
@@ -0,0 +1,43 @@
1
+ export interface AnalyticsEvent {
2
+ timestamp: string;
3
+ sessionId: string | null;
4
+ model: string;
5
+ preStripTokens: number;
6
+ postStripTotal: number;
7
+ tokensSaved: number;
8
+ tokenReductionRate: number;
9
+ actualCost: number;
10
+ hypotheticalCost: number;
11
+ dollarSavings: number;
12
+ cacheHitRatio: number | null;
13
+ countMethod?: string;
14
+ }
15
+ export interface CountMethodBreakdown {
16
+ api: number;
17
+ local: number;
18
+ unknown: number;
19
+ }
20
+ export interface AnalyticsSummary {
21
+ totalRequests: number;
22
+ totalPreStripTokens: number;
23
+ totalPostStripTokens: number;
24
+ totalTokensSaved: number;
25
+ overallReductionRate: number;
26
+ totalActualCost: number;
27
+ totalHypotheticalCost: number;
28
+ totalDollarSavings: number;
29
+ averageCacheHitRatio: number | null;
30
+ perModel: Map<string, ModelSummary>;
31
+ countMethodBreakdown: CountMethodBreakdown;
32
+ }
33
+ export interface ModelSummary {
34
+ requests: number;
35
+ tokensSaved: number;
36
+ actualCost: number;
37
+ hypotheticalCost: number;
38
+ dollarSavings: number;
39
+ }
40
+ export declare function readAnalytics(filePath: string): AnalyticsEvent[];
41
+ export declare function summarize(events: AnalyticsEvent[]): AnalyticsSummary;
42
+ export declare function summarizeSession(events: AnalyticsEvent[], sessionId: string): AnalyticsSummary;
43
+ export declare function formatSummary(summary: AnalyticsSummary): string;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readAnalytics = readAnalytics;
37
+ exports.summarize = summarize;
38
+ exports.summarizeSession = summarizeSession;
39
+ exports.formatSummary = formatSummary;
40
+ const fs = __importStar(require("fs"));
41
+ function readAnalytics(filePath) {
42
+ let content;
43
+ try {
44
+ content = fs.readFileSync(filePath, 'utf8');
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ const events = [];
50
+ for (const line of content.split('\n')) {
51
+ if (!line.trim())
52
+ continue;
53
+ try {
54
+ events.push(JSON.parse(line));
55
+ }
56
+ catch {
57
+ }
58
+ }
59
+ return events;
60
+ }
61
+ function summarize(events) {
62
+ const perModel = new Map();
63
+ const countMethodBreakdown = { api: 0, local: 0, unknown: 0 };
64
+ let totalPreStripTokens = 0;
65
+ let totalPostStripTokens = 0;
66
+ let totalTokensSaved = 0;
67
+ let totalActualCost = 0;
68
+ let totalHypotheticalCost = 0;
69
+ let totalDollarSavings = 0;
70
+ let cacheHitSum = 0;
71
+ let cacheHitCount = 0;
72
+ for (const event of events) {
73
+ totalPreStripTokens += event.preStripTokens;
74
+ totalPostStripTokens += event.postStripTotal;
75
+ totalTokensSaved += event.tokensSaved;
76
+ totalActualCost += event.actualCost;
77
+ totalHypotheticalCost += event.hypotheticalCost;
78
+ totalDollarSavings += event.dollarSavings;
79
+ if (event.cacheHitRatio != null) {
80
+ cacheHitSum += event.cacheHitRatio;
81
+ cacheHitCount++;
82
+ }
83
+ if (event.countMethod === 'api') {
84
+ countMethodBreakdown.api++;
85
+ }
86
+ else if (event.countMethod === 'local') {
87
+ countMethodBreakdown.local++;
88
+ }
89
+ else {
90
+ countMethodBreakdown.unknown++;
91
+ }
92
+ const existing = perModel.get(event.model);
93
+ if (existing) {
94
+ existing.requests++;
95
+ existing.tokensSaved += event.tokensSaved;
96
+ existing.actualCost += event.actualCost;
97
+ existing.hypotheticalCost += event.hypotheticalCost;
98
+ existing.dollarSavings += event.dollarSavings;
99
+ }
100
+ else {
101
+ perModel.set(event.model, {
102
+ requests: 1,
103
+ tokensSaved: event.tokensSaved,
104
+ actualCost: event.actualCost,
105
+ hypotheticalCost: event.hypotheticalCost,
106
+ dollarSavings: event.dollarSavings,
107
+ });
108
+ }
109
+ }
110
+ return {
111
+ totalRequests: events.length,
112
+ totalPreStripTokens,
113
+ totalPostStripTokens,
114
+ totalTokensSaved,
115
+ overallReductionRate: totalPreStripTokens > 0 ? totalTokensSaved / totalPreStripTokens : 0,
116
+ totalActualCost,
117
+ totalHypotheticalCost,
118
+ totalDollarSavings,
119
+ averageCacheHitRatio: cacheHitCount > 0 ? cacheHitSum / cacheHitCount : null,
120
+ perModel,
121
+ countMethodBreakdown,
122
+ };
123
+ }
124
+ function summarizeSession(events, sessionId) {
125
+ return summarize(events.filter((e) => e.sessionId === sessionId));
126
+ }
127
+ function formatSummary(summary) {
128
+ if (summary.totalRequests === 0) {
129
+ return 'No analytics data recorded yet.';
130
+ }
131
+ const lines = [
132
+ `Requests: ${summary.totalRequests}`,
133
+ `Tokens saved: ${summary.totalTokensSaved.toLocaleString()} (${(summary.overallReductionRate * 100).toFixed(1)}% reduction)`,
134
+ `Actual cost: $${summary.totalActualCost.toFixed(6)}`,
135
+ `Est. w/o diet: ~$${summary.totalHypotheticalCost.toFixed(6)}`,
136
+ `Est. savings: ~$${summary.totalDollarSavings.toFixed(6)}`,
137
+ ];
138
+ if (summary.averageCacheHitRatio != null) {
139
+ lines.push(`Avg cache hit: ${(summary.averageCacheHitRatio * 100).toFixed(1)}%`);
140
+ }
141
+ const { api, local, unknown } = summary.countMethodBreakdown;
142
+ const parts = [];
143
+ if (api > 0)
144
+ parts.push(`${api} API (exact)`);
145
+ if (local > 0)
146
+ parts.push(`${local} local (approx)`);
147
+ if (unknown > 0)
148
+ parts.push(`${unknown} unknown`);
149
+ if (parts.length > 0) {
150
+ lines.push(`Token counting: ${parts.join(', ')}`);
151
+ }
152
+ if (summary.perModel.size > 1) {
153
+ lines.push('', 'Per model:');
154
+ for (const [model, data] of summary.perModel) {
155
+ lines.push(` ${model}: ${data.requests} req, ${data.tokensSaved.toLocaleString()} tokens saved, ~$${data.dollarSavings.toFixed(6)} est. savings`);
156
+ }
157
+ }
158
+ return lines.join('\n');
159
+ }
160
+ //# sourceMappingURL=analytics-reader.js.map
@@ -1,9 +1,12 @@
1
1
  export interface StartOptions {
2
- apiKey: string;
2
+ orgToken: string;
3
+ cloudUrl: string;
3
4
  port: number;
5
+ verbose?: boolean;
4
6
  }
5
7
  export interface StartResult {
6
8
  success: boolean;
9
+ unauthorized?: boolean;
7
10
  message?: string;
8
11
  error?: string;
9
12
  }
@@ -36,9 +36,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.startGateway = startGateway;
37
37
  const fs = __importStar(require("fs"));
38
38
  const childProcess = __importStar(require("child_process"));
39
+ const crypto = __importStar(require("crypto"));
39
40
  const net = __importStar(require("net"));
40
41
  const path = __importStar(require("path"));
41
42
  const utils_1 = require("./utils");
43
+ function detectClientType() {
44
+ if (process.env.VSCODE_PID || process.env.TERM_PROGRAM === 'vscode') {
45
+ return 'VS_CODE';
46
+ }
47
+ if (process.env.CLAUDE_APP) {
48
+ return 'MAC_APP';
49
+ }
50
+ return 'TERMINAL';
51
+ }
42
52
  function isPortAvailable(port) {
43
53
  return new Promise((resolve) => {
44
54
  const server = net.createServer();
@@ -49,63 +59,118 @@ function isPortAvailable(port) {
49
59
  });
50
60
  });
51
61
  }
52
- async function startGateway(options) {
53
- const { apiKey, port } = options;
54
- if (!apiKey) {
55
- return {
56
- success: false,
57
- error: '--api-key is required. Usage: tokendiet start --api-key <key>',
58
- };
62
+ async function registerGatewayRun(cloudUrl, orgToken, clientType) {
63
+ try {
64
+ const response = await fetch(`${cloudUrl}/v1/gateway-runs`, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ Authorization: `Bearer ${orgToken}`,
69
+ },
70
+ body: JSON.stringify({ clientType, startedAt: new Date().toISOString() }),
71
+ });
72
+ if (response.status === 401) {
73
+ return 'unauthorized';
74
+ }
75
+ if (!response.ok) {
76
+ console.warn(`[tokendiet] Cloud API returned ${response.status} when registering gateway run`);
77
+ return null;
78
+ }
79
+ const data = (await response.json());
80
+ return data.gatewayRunId;
59
81
  }
60
- const existingState = (0, utils_1.readJsonFile)(utils_1.statePath);
61
- if (existingState.pid && (0, utils_1.isProcessAlive)(existingState.pid)) {
62
- return {
63
- success: false,
64
- error: `Gateway is already running (PID ${existingState.pid} on port ${existingState.port}).`,
65
- };
82
+ catch (err) {
83
+ console.warn(`[tokendiet] Could not register gateway run: ${err.message}`);
84
+ return null;
66
85
  }
67
- const portFree = await isPortAvailable(port);
86
+ }
87
+ async function startGateway(options) {
88
+ const { orgToken, cloudUrl, port } = options;
89
+ let portFree = await isPortAvailable(port);
68
90
  if (!portFree) {
91
+ const killed = (0, utils_1.killOrphanOnPort)(port);
92
+ if (killed) {
93
+ await new Promise((resolve) => setTimeout(resolve, 500));
94
+ portFree = await isPortAvailable(port);
95
+ }
96
+ if (!portFree) {
97
+ return {
98
+ success: false,
99
+ error: `Port ${port} is already in use. Choose a different port with --port.`,
100
+ };
101
+ }
102
+ }
103
+ const mainScript = path.join(__dirname, '..', 'main.js');
104
+ const sessionId = crypto.randomUUID();
105
+ const clientType = detectClientType();
106
+ let gatewayRunId = null;
107
+ gatewayRunId = await registerGatewayRun(cloudUrl, orgToken, clientType);
108
+ if (gatewayRunId === 'unauthorized') {
109
+ return { success: false, unauthorized: true };
110
+ }
111
+ if (!gatewayRunId) {
69
112
  return {
70
113
  success: false,
71
- error: `Port ${port} is already in use. Choose a different port with --port.`,
114
+ error: `Failed to register gateway run at ${cloudUrl}/v1/gateway-runs. Is the cloud API running?`,
72
115
  };
73
116
  }
117
+ (0, utils_1.saveToken)(orgToken);
118
+ console.log(`[tokendiet] Registered gateway run: ${gatewayRunId}`);
119
+ const env = {
120
+ ...process.env,
121
+ PORT: String(port),
122
+ TOKENDIET_ORG_TOKEN: orgToken,
123
+ TOKENDIET_CLOUD_URL: cloudUrl,
124
+ TOKENDIET_SESSION_ID: sessionId,
125
+ };
126
+ if (options.verbose)
127
+ env.TOKENDIET_VERBOSE = '1';
128
+ if (gatewayRunId)
129
+ env.TOKENDIET_GATEWAY_RUN_ID = gatewayRunId;
74
130
  const claudeSettings = (0, utils_1.readJsonFile)(utils_1.claudeSettingsPath);
75
131
  const previousBaseUrl = claudeSettings.env?.ANTHROPIC_BASE_URL;
76
- const mainScript = path.join(__dirname, '..', 'main.js');
77
- const child = childProcess.spawn(process.execPath, [mainScript], {
78
- detached: true,
79
- stdio: 'ignore',
80
- env: {
81
- ...process.env,
82
- PORT: String(port),
83
- ANTHROPIC_API_KEY: apiKey,
84
- },
85
- });
86
- child.unref();
87
- fs.mkdirSync(utils_1.stateDir, { recursive: true });
88
- const state = {
89
- pid: child.pid,
90
- port,
91
- apiKey,
92
- };
93
- if (previousBaseUrl) {
94
- state.previousBaseUrl = previousBaseUrl;
95
- }
96
- fs.writeFileSync(utils_1.statePath, JSON.stringify(state, null, 2));
97
132
  fs.mkdirSync(utils_1.claudeSettingsDir, { recursive: true });
98
- const updatedSettings = {
133
+ const patchedSettings = {
99
134
  ...claudeSettings,
100
135
  env: {
101
136
  ...claudeSettings.env,
102
137
  ANTHROPIC_BASE_URL: `http://localhost:${port}`,
103
138
  },
104
139
  };
105
- fs.writeFileSync(utils_1.claudeSettingsPath, JSON.stringify(updatedSettings, null, 2));
106
- return {
107
- success: true,
108
- message: `Gateway started on port ${port} (PID ${child.pid}). Restart Claude Code to route through TokenDiet.`,
140
+ fs.writeFileSync(utils_1.claudeSettingsPath, JSON.stringify(patchedSettings, null, 2));
141
+ const restoreClaudeSettings = () => {
142
+ const currentSettings = (0, utils_1.readJsonFile)(utils_1.claudeSettingsPath);
143
+ if (currentSettings.env?.ANTHROPIC_BASE_URL !== `http://localhost:${port}`) {
144
+ return;
145
+ }
146
+ const restoredSettings = {
147
+ ...claudeSettings,
148
+ env: {
149
+ ...(claudeSettings.env || {}),
150
+ },
151
+ };
152
+ if (previousBaseUrl) {
153
+ restoredSettings.env.ANTHROPIC_BASE_URL = previousBaseUrl;
154
+ }
155
+ else {
156
+ delete restoredSettings.env.ANTHROPIC_BASE_URL;
157
+ }
158
+ fs.mkdirSync(utils_1.claudeSettingsDir, { recursive: true });
159
+ fs.writeFileSync(utils_1.claudeSettingsPath, JSON.stringify(restoredSettings, null, 2));
160
+ };
161
+ const child = childProcess.spawn(process.execPath, [mainScript], {
162
+ stdio: 'inherit',
163
+ env,
164
+ });
165
+ const cleanup = () => {
166
+ restoreClaudeSettings();
167
+ child.kill('SIGTERM');
168
+ process.exit(0);
109
169
  };
170
+ process.on('SIGINT', cleanup);
171
+ process.on('SIGTERM', cleanup);
172
+ await new Promise((resolve) => child.on('exit', resolve));
173
+ restoreClaudeSettings();
174
+ return { success: true, message: 'Gateway stopped.' };
110
175
  }
111
176
  //# sourceMappingURL=start.js.map
@@ -1,6 +1,8 @@
1
- export declare const stateDir: string;
2
- export declare const statePath: string;
3
1
  export declare const claudeSettingsDir: string;
4
2
  export declare const claudeSettingsPath: string;
5
3
  export declare function readJsonFile(filePath: string): Record<string, unknown>;
6
- export declare function isProcessAlive(pid: number): boolean;
4
+ export declare function loadSavedToken(): string | null;
5
+ export declare function saveToken(token: string): void;
6
+ export declare function promptForToken(message?: string): Promise<string | null>;
7
+ export declare function clearSavedToken(): void;
8
+ export declare function killOrphanOnPort(port: number): boolean;
@@ -33,14 +33,18 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.claudeSettingsPath = exports.claudeSettingsDir = exports.statePath = exports.stateDir = void 0;
36
+ exports.claudeSettingsPath = exports.claudeSettingsDir = void 0;
37
37
  exports.readJsonFile = readJsonFile;
38
- exports.isProcessAlive = isProcessAlive;
38
+ exports.loadSavedToken = loadSavedToken;
39
+ exports.saveToken = saveToken;
40
+ exports.promptForToken = promptForToken;
41
+ exports.clearSavedToken = clearSavedToken;
42
+ exports.killOrphanOnPort = killOrphanOnPort;
39
43
  const fs = __importStar(require("fs"));
40
44
  const os = __importStar(require("os"));
41
45
  const path = __importStar(require("path"));
42
- exports.stateDir = path.join(os.homedir(), '.tokendiet');
43
- exports.statePath = path.join(exports.stateDir, 'state.json');
46
+ const readline = __importStar(require("readline"));
47
+ const child_process_1 = require("child_process");
44
48
  exports.claudeSettingsDir = path.join(os.homedir(), '.claude');
45
49
  exports.claudeSettingsPath = path.join(exports.claudeSettingsDir, 'settings.json');
46
50
  function readJsonFile(filePath) {
@@ -53,9 +57,62 @@ function readJsonFile(filePath) {
53
57
  }
54
58
  return {};
55
59
  }
56
- function isProcessAlive(pid) {
60
+ function findPidOnPort(port) {
57
61
  try {
58
- process.kill(pid, 0);
62
+ const output = (0, child_process_1.execSync)(`lsof -i :${port} -t`, { encoding: 'utf8' }).trim();
63
+ const pid = parseInt(output.split('\n')[0], 10);
64
+ return Number.isFinite(pid) ? pid : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ const tokendietDir = path.join(os.homedir(), '.tokendiet');
71
+ const tokenEnvPath = path.join(tokendietDir, '.env');
72
+ function loadSavedToken() {
73
+ try {
74
+ if (!fs.existsSync(tokenEnvPath))
75
+ return null;
76
+ const content = fs.readFileSync(tokenEnvPath, 'utf-8');
77
+ const match = content.match(/^TOKENDIET_ORG_TOKEN=(.+)$/m);
78
+ return match ? match[1].trim() : null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ function saveToken(token) {
85
+ fs.mkdirSync(tokendietDir, { recursive: true });
86
+ fs.writeFileSync(tokenEnvPath, `TOKENDIET_ORG_TOKEN=${token}\n`, { mode: 0o600 });
87
+ }
88
+ function prompt(question) {
89
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
90
+ return new Promise((resolve) => {
91
+ rl.question(question, (answer) => {
92
+ rl.close();
93
+ resolve(answer.trim());
94
+ });
95
+ });
96
+ }
97
+ async function promptForToken(message = 'Enter your API token: ') {
98
+ const token = await prompt(message);
99
+ return token || null;
100
+ }
101
+ function clearSavedToken() {
102
+ try {
103
+ if (fs.existsSync(tokenEnvPath))
104
+ fs.unlinkSync(tokenEnvPath);
105
+ }
106
+ catch {
107
+ }
108
+ }
109
+ function killOrphanOnPort(port) {
110
+ const pid = findPidOnPort(port);
111
+ if (!pid)
112
+ return false;
113
+ try {
114
+ process.kill(pid, 'SIGTERM');
115
+ console.log(`[tokendiet] Killed orphan process (PID ${pid}) on port ${port}`);
59
116
  return true;
60
117
  }
61
118
  catch {
@@ -0,0 +1,4 @@
1
+ import { NestApplicationOptions } from '@nestjs/common';
2
+ import { NestExpressApplication } from '@nestjs/platform-express';
3
+ export declare function configureApp(app: NestExpressApplication): void;
4
+ export declare const options: NestApplicationOptions;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.options = void 0;
4
+ exports.configureApp = configureApp;
5
+ const BODY_LIMIT = 10_485_760;
6
+ function configureApp(app) {
7
+ app.useBodyParser('json', { limit: BODY_LIMIT });
8
+ }
9
+ exports.options = {
10
+ rawBody: true,
11
+ ...(process.env.TOKENDIET_VERBOSE ? {} : { logger: ['error'] }),
12
+ };
13
+ //# sourceMappingURL=configure-app.js.map
package/dist/main.js CHANGED
@@ -2,9 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@nestjs/core");
4
4
  const app_module_1 = require("./app.module");
5
+ const configure_app_1 = require("./configure-app");
6
+ const cli_output_1 = require("./proxy/cli-output");
5
7
  async function bootstrap() {
6
- const app = await core_1.NestFactory.create(app_module_1.AppModule, { rawBody: true });
7
- await app.listen(process.env.PORT ?? 3100);
8
+ const app = await core_1.NestFactory.create(app_module_1.AppModule, configure_app_1.options);
9
+ (0, configure_app_1.configureApp)(app);
10
+ const port = process.env.PORT ?? 3100;
11
+ await app.listen(port);
12
+ if (!process.env.TOKENDIET_VERBOSE) {
13
+ (0, cli_output_1.printBanner)(Number(port));
14
+ }
8
15
  }
9
16
  bootstrap();
10
17
  //# sourceMappingURL=main.js.map