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.
- package/README.md +47 -0
- package/dist/cli.js +37 -23
- package/dist/commands/analytics-reader.d.ts +43 -0
- package/dist/commands/analytics-reader.js +160 -0
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +106 -41
- package/dist/commands/utils.d.ts +5 -3
- package/dist/commands/utils.js +63 -6
- package/dist/configure-app.d.ts +4 -0
- package/dist/configure-app.js +13 -0
- package/dist/main.js +9 -2
- package/dist/proxy/analytics.service.d.ts +35 -0
- package/dist/proxy/analytics.service.js +193 -0
- package/dist/proxy/cli-output.d.ts +5 -0
- package/dist/proxy/cli-output.js +56 -0
- package/dist/proxy/cloud-api.service.d.ts +53 -0
- package/dist/proxy/cloud-api.service.js +56 -0
- package/dist/proxy/proxy.controller.d.ts +23 -6
- package/dist/proxy/proxy.controller.js +149 -15
- package/dist/proxy/proxy.module.js +4 -2
- package/dist/proxy/proxy.service.d.ts +1 -0
- package/dist/proxy/proxy.service.js +29 -13
- package/dist/proxy/token-count.service.d.ts +41 -0
- package/dist/proxy/token-count.service.js +93 -0
- package/dist/token-stripper/provider-detector.util.d.ts +16 -0
- package/dist/token-stripper/provider-detector.util.js +120 -0
- package/dist/token-stripper/token-stripper.module.d.ts +2 -0
- package/dist/token-stripper/token-stripper.module.js +21 -0
- package/dist/token-stripper/token-stripper.service.d.ts +31 -0
- package/dist/token-stripper/token-stripper.service.js +102 -0
- package/dist/utils/content-byte-size.d.ts +8 -0
- package/dist/utils/content-byte-size.js +54 -0
- package/dist/utils/message-stripper.d.ts +1 -0
- package/dist/utils/message-stripper.js +31 -4
- package/dist/utils/system-tools-stripper.d.ts +10 -0
- package/dist/utils/system-tools-stripper.js +214 -0
- package/package.json +20 -9
- package/dist/app.controller.js.map +0 -1
- package/dist/app.module.js.map +0 -1
- package/dist/app.service.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/start.js.map +0 -1
- package/dist/commands/stop.d.ts +0 -6
- package/dist/commands/stop.js +0 -68
- package/dist/commands/stop.js.map +0 -1
- package/dist/commands/utils.js.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/proxy/proxy.controller.js.map +0 -1
- package/dist/proxy/proxy.module.js.map +0 -1
- package/dist/proxy/proxy.service.js.map +0 -1
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- 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
|
|
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('--
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
package/dist/commands/start.d.ts
CHANGED
package/dist/commands/start.js
CHANGED
|
@@ -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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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: `
|
|
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
|
|
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(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
package/dist/commands/utils.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/commands/utils.js
CHANGED
|
@@ -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 =
|
|
36
|
+
exports.claudeSettingsPath = exports.claudeSettingsDir = void 0;
|
|
37
37
|
exports.readJsonFile = readJsonFile;
|
|
38
|
-
exports.
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
60
|
+
function findPidOnPort(port) {
|
|
57
61
|
try {
|
|
58
|
-
|
|
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,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,
|
|
7
|
-
|
|
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
|