tokendiet 1.4.0 → 1.6.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/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ program
26
26
  .option('--cloud-url <url>', 'Cloud API URL', 'https://tokendiet-production.up.railway.app')
27
27
  .option('--port <port>', 'Port to listen on (default: 3100)', '3100')
28
28
  .option('--verbose', 'Show full NestJS logs')
29
+ .option('--dry-run', 'Disable all stripping (baseline measurement)')
29
30
  .action(async (opts) => {
30
31
  let orgToken = await resolveToken(opts.orgToken);
31
32
  if (!orgToken) {
@@ -37,6 +38,7 @@ program
37
38
  cloudUrl: opts.cloudUrl,
38
39
  port: parseInt(opts.port, 10),
39
40
  verbose: opts.verbose ?? false,
41
+ dryRun: opts.dryRun ?? false,
40
42
  });
41
43
  while (result.unauthorized) {
42
44
  (0, utils_1.clearSavedToken)();
@@ -51,6 +53,7 @@ program
51
53
  cloudUrl: opts.cloudUrl,
52
54
  port: parseInt(opts.port, 10),
53
55
  verbose: opts.verbose ?? false,
56
+ dryRun: opts.dryRun ?? false,
54
57
  });
55
58
  }
56
59
  if (result.success) {
@@ -3,6 +3,7 @@ export interface StartOptions {
3
3
  cloudUrl: string;
4
4
  port: number;
5
5
  verbose?: boolean;
6
+ dryRun?: boolean;
6
7
  }
7
8
  export interface StartResult {
8
9
  success: boolean;
@@ -125,6 +125,8 @@ async function startGateway(options) {
125
125
  };
126
126
  if (options.verbose)
127
127
  env.TOKENDIET_VERBOSE = '1';
128
+ if (options.dryRun)
129
+ env.TOKENDIET_DRY_RUN = '1';
128
130
  if (gatewayRunId)
129
131
  env.TOKENDIET_GATEWAY_RUN_ID = gatewayRunId;
130
132
  const claudeSettings = (0, utils_1.readJsonFile)(utils_1.claudeSettingsPath);
package/dist/main.js CHANGED
@@ -10,7 +10,7 @@ async function bootstrap() {
10
10
  const port = process.env.PORT ?? 3100;
11
11
  await app.listen(port);
12
12
  if (!process.env.TOKENDIET_VERBOSE) {
13
- (0, cli_output_1.printBanner)(Number(port));
13
+ (0, cli_output_1.printBanner)(Number(port), process.env.TOKENDIET_DRY_RUN === '1');
14
14
  }
15
15
  }
16
16
  bootstrap();
@@ -25,6 +25,7 @@ export declare class AnalyticsService {
25
25
  private readonly gatewayRunId;
26
26
  private readonly upstreamUrl;
27
27
  private readonly analyticsPath;
28
+ private readonly isDryRun;
28
29
  private sessionTotals;
29
30
  constructor(config: ConfigService, cloudApiService: CloudApiService);
30
31
  getSessionTotals(): SessionTotals;
@@ -71,6 +71,7 @@ let AnalyticsService = AnalyticsService_1 = class AnalyticsService {
71
71
  gatewayRunId;
72
72
  upstreamUrl;
73
73
  analyticsPath;
74
+ isDryRun;
74
75
  sessionTotals = { tokensSaved: 0, dollarSavings: 0, requestCount: 0 };
75
76
  constructor(config, cloudApiService) {
76
77
  this.config = config;
@@ -78,6 +79,7 @@ let AnalyticsService = AnalyticsService_1 = class AnalyticsService {
78
79
  this.sessionId = this.config.get('TOKENDIET_SESSION_ID') ?? null;
79
80
  this.gatewayRunId = this.config.get('TOKENDIET_GATEWAY_RUN_ID') ?? null;
80
81
  this.upstreamUrl = this.config.get('PROXY_TARGET_URL') ?? DEFAULT_UPSTREAM;
82
+ this.isDryRun = this.config.get('TOKENDIET_DRY_RUN') === '1';
81
83
  this.analyticsPath = ANALYTICS_PATH;
82
84
  }
83
85
  getSessionTotals() {
@@ -99,6 +101,7 @@ let AnalyticsService = AnalyticsService_1 = class AnalyticsService {
99
101
  strippingApplied: this.buildStrippingEntries(preStripResult, usage),
100
102
  originalRequestTokenCount: preStripResult.input_tokens,
101
103
  countMethod: preStripResult.countMethod,
104
+ isDryRun: this.isDryRun,
102
105
  upstreamResponseUsage: {
103
106
  input_tokens: usage.input_tokens,
104
107
  output_tokens: usage.output_tokens ?? 0,
@@ -1,9 +1,9 @@
1
- export declare function printBanner(port: number): void;
1
+ export declare function printBanner(port: number, dryRun?: boolean): void;
2
2
  interface Totals {
3
3
  tokensSaved: number;
4
4
  dollarSavings: number;
5
5
  requestCount: number;
6
6
  }
7
- export declare function printRequestSavings(model: string, tokensSaved: number, costSaved: number, totals: Totals): void;
7
+ export declare function printRequestSavings(model: string, tokensSaved: number, costSaved: number, originalTokenCount: number, totals: Totals, dryRun?: boolean): void;
8
8
  export declare function _reset(): void;
9
9
  export {};
@@ -11,12 +11,14 @@ const MAX_VISIBLE = 5;
11
11
  const COL_MODEL = 30;
12
12
  const COL_TOKENS = 12;
13
13
  const COL_COST = 10;
14
- const TABLE_WIDTH = 2 + COL_MODEL + 2 + COL_TOKENS + 2 + COL_COST + 2;
14
+ const COL_PCT = 6;
15
+ const TABLE_WIDTH = 2 + COL_MODEL + 2 + COL_TOKENS + 2 + COL_COST + 2 + COL_PCT + 2;
15
16
  const recentRequests = [];
16
17
  let linesWritten = 0;
17
- function printBanner(port) {
18
+ function printBanner(port, dryRun = false) {
18
19
  console.log('');
19
- console.log(chalk_1.default.bold.green(' TokenDiet') + ' running on port ' + chalk_1.default.cyan(String(port)));
20
+ const tag = dryRun ? chalk_1.default.bold.yellow(' [DRY RUN]') : '';
21
+ console.log(chalk_1.default.bold.green(' TokenDiet') + tag + ' running on port ' + chalk_1.default.cyan(String(port)));
20
22
  console.log('');
21
23
  }
22
24
  function formatNumber(n) {
@@ -34,7 +36,8 @@ function formatHeader() {
34
36
  return chalk_1.default.dim(' ' +
35
37
  'Model'.padEnd(COL_MODEL) + ' ' +
36
38
  'Tokens saved'.padStart(COL_TOKENS) + ' ' +
37
- 'Cost saved'.padStart(COL_COST));
39
+ 'Cost saved'.padStart(COL_COST) + ' ' +
40
+ '%'.padStart(COL_PCT));
38
41
  }
39
42
  function formatSeparator() {
40
43
  return chalk_1.default.dim(' ' + '\u2500'.repeat(TABLE_WIDTH - 2));
@@ -43,12 +46,14 @@ function formatRequest(entry, dim) {
43
46
  const model = truncateModel(entry.model);
44
47
  const tokens = formatNumber(entry.tokensSaved);
45
48
  const cost = formatCost(entry.costSaved);
49
+ const pct = entry.pctSaved.toFixed(2) + '%';
46
50
  const wrap = dim ? chalk_1.default.dim : (s) => s;
47
51
  const arrow = dim ? chalk_1.default.dim('\u2193 ') : chalk_1.default.green('\u2193 ');
48
52
  return (arrow +
49
53
  wrap(model.padEnd(COL_MODEL)) + ' ' +
50
54
  (dim ? chalk_1.default.dim : chalk_1.default.yellow)(tokens.padStart(COL_TOKENS)) + ' ' +
51
- (dim ? chalk_1.default.dim : chalk_1.default.green)(cost.padStart(COL_COST)));
55
+ (dim ? chalk_1.default.dim : chalk_1.default.green)(cost.padStart(COL_COST)) + ' ' +
56
+ (dim ? chalk_1.default.dim : chalk_1.default.cyan)(pct.padStart(COL_PCT)));
52
57
  }
53
58
  function formatTotals(totals) {
54
59
  const label = `Total (${totals.requestCount} reqs)`;
@@ -58,13 +63,17 @@ function formatTotals(totals) {
58
63
  chalk_1.default.yellow(tokens.padStart(COL_TOKENS)) + ' ' +
59
64
  chalk_1.default.green(cost.padStart(COL_COST)));
60
65
  }
61
- function printRequestSavings(model, tokensSaved, costSaved, totals) {
62
- recentRequests.unshift({ model, tokensSaved, costSaved });
66
+ function printRequestSavings(model, tokensSaved, costSaved, originalTokenCount, totals, dryRun = false) {
67
+ const pctSaved = originalTokenCount > 0 ? (tokensSaved / originalTokenCount) * 100 : 0;
68
+ recentRequests.unshift({ model, tokensSaved, costSaved, pctSaved });
63
69
  if (recentRequests.length > MAX_VISIBLE)
64
70
  recentRequests.length = MAX_VISIBLE;
65
71
  if (linesWritten > 0) {
66
72
  process.stdout.write(`\x1b[${linesWritten}A\x1b[0J`);
67
73
  }
74
+ if (dryRun) {
75
+ console.log(chalk_1.default.yellow.bold(' [DRY RUN] ') + chalk_1.default.dim('Stripping disabled — baseline measurement'));
76
+ }
68
77
  console.log(formatHeader());
69
78
  console.log(formatSeparator());
70
79
  for (let i = 0; i < recentRequests.length; i++) {
@@ -72,7 +81,7 @@ function printRequestSavings(model, tokensSaved, costSaved, totals) {
72
81
  }
73
82
  console.log(formatSeparator());
74
83
  console.log(formatTotals(totals));
75
- linesWritten = recentRequests.length + 4;
84
+ linesWritten = recentRequests.length + 4 + (dryRun ? 1 : 0);
76
85
  }
77
86
  function _reset() {
78
87
  recentRequests.length = 0;
@@ -24,6 +24,7 @@ export interface CloudApiRequestPayload {
24
24
  }>;
25
25
  originalRequestTokenCount: number;
26
26
  countMethod: string;
27
+ isDryRun: boolean;
27
28
  upstreamResponseUsage: {
28
29
  input_tokens: number;
29
30
  output_tokens: number;
@@ -40,7 +40,8 @@ let CloudApiService = CloudApiService_1 = class CloudApiService {
40
40
  this.logger.warn(`Cloud API returned ${response.status}`);
41
41
  return null;
42
42
  }
43
- return (await response.json());
43
+ const body = (await response.json());
44
+ return body;
44
45
  }
45
46
  catch (err) {
46
47
  this.logger.warn(`Cloud API request failed: ${err.message}`);
@@ -19,6 +19,7 @@ export declare class ProxyController {
19
19
  private readonly analyticsService;
20
20
  private readonly logger;
21
21
  private readonly proxy;
22
+ private readonly isDryRun;
22
23
  constructor(config: ConfigService, tokenCountService: TokenCountService, analyticsService: AnalyticsService);
23
24
  messages(req: Request, res: Response, next: NextFunction): Promise<void>;
24
25
  countTokens(req: Request, res: Response, next: NextFunction): Promise<void>;
@@ -81,10 +81,12 @@ let ProxyController = ProxyController_1 = class ProxyController {
81
81
  analyticsService;
82
82
  logger = new common_1.Logger(ProxyController_1.name);
83
83
  proxy;
84
+ isDryRun;
84
85
  constructor(config, tokenCountService, analyticsService) {
85
86
  this.config = config;
86
87
  this.tokenCountService = tokenCountService;
87
88
  this.analyticsService = analyticsService;
89
+ this.isDryRun = this.config.get('TOKENDIET_DRY_RUN') === '1';
88
90
  this.proxy = (0, http_proxy_middleware_1.createProxyMiddleware)({
89
91
  target: this.config.get('PROXY_TARGET_URL') ?? DEFAULT_UPSTREAM,
90
92
  selfHandleResponse: true,
@@ -95,8 +97,11 @@ let ProxyController = ProxyController_1 = class ProxyController {
95
97
  req.__requestedAt = new Date().toISOString();
96
98
  req.__requestPath = req.originalUrl ?? req.path;
97
99
  req.__preStripCountPromise = this.tokenCountService.count(req.__originalBody);
98
- req.body = (0, message_stripper_1.stripMessageBody)(req.body);
99
- req.body = (0, system_tools_stripper_1.stripSystemAndTools)(req.body);
100
+ if (!this.isDryRun) {
101
+ req.body = (0, message_stripper_1.stripMessageBody)(req.body);
102
+ req.body = (0, system_tools_stripper_1.stripSystemAndTools)(req.body);
103
+ }
104
+ req.__localDelta = this.tokenCountService.countLocalDelta(req.__originalBody, req.body);
100
105
  (0, http_proxy_middleware_1.fixRequestBody)(_proxyReq, req);
101
106
  },
102
107
  proxyRes: (0, http_proxy_middleware_1.responseInterceptor)(async (responseBuffer, proxyRes, req) => {
@@ -120,6 +125,17 @@ let ProxyController = ProxyController_1 = class ProxyController {
120
125
  }
121
126
  if (!preStripResult)
122
127
  return responseBuffer;
128
+ this.logger.debug(`countMethod: ${preStripResult.countMethod}`);
129
+ const localDelta = req.__localDelta;
130
+ if (preStripResult.countMethod === 'local' && localDelta) {
131
+ const usageTotal = usage.input_tokens
132
+ + (usage.cache_read_input_tokens ?? 0)
133
+ + (usage.cache_creation_input_tokens ?? 0);
134
+ preStripResult = {
135
+ input_tokens: usageTotal + localDelta.delta,
136
+ countMethod: 'delta',
137
+ };
138
+ }
123
139
  try {
124
140
  const cloudResponse = await this.analyticsService.record({
125
141
  model,
@@ -132,7 +148,7 @@ let ProxyController = ProxyController_1 = class ProxyController {
132
148
  requestedAt,
133
149
  });
134
150
  if (cloudResponse) {
135
- (0, cli_output_1.printRequestSavings)(model, cloudResponse.savings?.tokensSaved ?? 0, cloudResponse.savings?.costSaved ?? 0, this.analyticsService.getSessionTotals());
151
+ (0, cli_output_1.printRequestSavings)(model, cloudResponse.savings?.tokensSaved ?? 0, cloudResponse.savings?.costSaved ?? 0, cloudResponse.originalRequestTokenCount ?? 0, this.analyticsService.getSessionTotals(), this.isDryRun);
136
152
  }
137
153
  }
138
154
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { ConfigService } from '@nestjs/config';
2
- export type CountMethod = 'api' | 'local';
2
+ export type CountMethod = 'api' | 'local' | 'delta';
3
3
  export interface TokenCountResult {
4
4
  input_tokens: number;
5
5
  countMethod: CountMethod;
@@ -28,6 +28,24 @@ export declare class TokenCountService {
28
28
  system?: any;
29
29
  tools?: any;
30
30
  }): Promise<TokenCountResult>;
31
+ countLocalDelta(originalBody: {
32
+ messages?: Array<{
33
+ role: string;
34
+ content: string | any[];
35
+ }>;
36
+ system?: any;
37
+ tools?: any;
38
+ }, strippedBody: {
39
+ messages?: Array<{
40
+ role: string;
41
+ content: string | any[];
42
+ }>;
43
+ system?: any;
44
+ tools?: any;
45
+ }): {
46
+ delta: number;
47
+ countMethod: 'delta';
48
+ };
31
49
  countLocally(body: {
32
50
  messages?: Array<{
33
51
  role: string;
@@ -65,9 +65,14 @@ let TokenCountService = TokenCountService_1 = class TokenCountService {
65
65
  params.tools = sanitized.tools;
66
66
  }
67
67
  const result = await this.client.messages.countTokens(params);
68
- this.logger.log(`count_tokens response: ${JSON.stringify(result)}`);
68
+ this.logger.debug(`count_tokens response: ${JSON.stringify(result)}`);
69
69
  return { input_tokens: result.input_tokens, countMethod: 'api' };
70
70
  }
71
+ countLocalDelta(originalBody, strippedBody) {
72
+ const originalCount = this.countLocally(originalBody).input_tokens;
73
+ const strippedCount = this.countLocally(strippedBody).input_tokens;
74
+ return { delta: Math.max(0, originalCount - strippedCount), countMethod: 'delta' };
75
+ }
71
76
  countLocally(body) {
72
77
  const { encode } = require('gpt-tokenizer');
73
78
  let total = 0;
@@ -9,8 +9,3 @@ export interface LLMProvider {
9
9
  modelInUrlPath?: boolean;
10
10
  modelUrlPattern?: RegExp;
11
11
  }
12
- export declare class ProviderDetector {
13
- detect(hostname: string, path: string): LLMProvider | null;
14
- isLLMHost(hostname: string): boolean;
15
- getAllHosts(): string[];
16
- }
@@ -1,13 +1,5 @@
1
1
  "use strict";
2
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
- return c > 3 && r && Object.defineProperty(target, key, r), r;
7
- };
8
2
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.ProviderDetector = void 0;
10
- const common_1 = require("@nestjs/common");
11
3
  const KNOWN_PROVIDERS = [
12
4
  {
13
5
  name: 'anthropic',
@@ -93,28 +85,4 @@ const KNOWN_PROVIDERS = [
93
85
  modelUrlPattern: /\/v1\/engines\/([^/]+)\/completions/,
94
86
  },
95
87
  ];
96
- let ProviderDetector = class ProviderDetector {
97
- detect(hostname, path) {
98
- for (const provider of KNOWN_PROVIDERS) {
99
- if (!hostname.includes(provider.host)) {
100
- continue;
101
- }
102
- const pathMatches = provider.requestPaths.some((validPath) => path.startsWith(validPath));
103
- if (pathMatches) {
104
- return provider;
105
- }
106
- }
107
- return null;
108
- }
109
- isLLMHost(hostname) {
110
- return KNOWN_PROVIDERS.some((p) => hostname.includes(p.host));
111
- }
112
- getAllHosts() {
113
- return KNOWN_PROVIDERS.map((p) => p.host);
114
- }
115
- };
116
- exports.ProviderDetector = ProviderDetector;
117
- exports.ProviderDetector = ProviderDetector = __decorate([
118
- (0, common_1.Injectable)()
119
- ], ProviderDetector);
120
88
  //# sourceMappingURL=provider-detector.util.js.map
@@ -1,3 +1,5 @@
1
+ export declare function isCatNFormatted(text: string): boolean;
2
+ export declare function stripReadPrefixes(text: string): string;
1
3
  export declare function normalizeWhitespace(text: string): string;
2
4
  export declare function normalizeToolResultWhitespace(text: string): string;
3
5
  export declare function stripMessageBody(body: unknown): unknown;
@@ -1,8 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCatNFormatted = isCatNFormatted;
4
+ exports.stripReadPrefixes = stripReadPrefixes;
3
5
  exports.normalizeWhitespace = normalizeWhitespace;
4
6
  exports.normalizeToolResultWhitespace = normalizeToolResultWhitespace;
5
7
  exports.stripMessageBody = stripMessageBody;
8
+ const CAT_N_PREFIX = /^[ \t]*\d+[\t→]/;
9
+ function isCatNFormatted(text) {
10
+ const lines = text.split('\n');
11
+ let checked = 0;
12
+ for (const line of lines) {
13
+ if (line.trim() === '')
14
+ continue;
15
+ if (!CAT_N_PREFIX.test(line))
16
+ return false;
17
+ checked++;
18
+ if (checked >= 3)
19
+ break;
20
+ }
21
+ return checked > 0;
22
+ }
23
+ function stripReadPrefixes(text) {
24
+ if (!isCatNFormatted(text))
25
+ return normalizeToolResultWhitespace(text);
26
+ return text
27
+ .split('\n')
28
+ .map((line) => line.replace(CAT_N_PREFIX, ''))
29
+ .join('\n')
30
+ .replace(/[ \t]+$/gm, '')
31
+ .replace(/\n{3,}/g, '\n\n')
32
+ .replace(/^\n+/, '')
33
+ .replace(/\n+$/, '');
34
+ }
6
35
  function normalizeWhitespace(text) {
7
36
  return text
8
37
  .replace(/[ \t]+/g, ' ')
@@ -22,7 +51,9 @@ function stripMessageBody(body) {
22
51
  const obj = body;
23
52
  if (!Array.isArray(obj.messages))
24
53
  return body;
25
- return {
54
+ let catNStripped = 0;
55
+ let toolResultsTotal = 0;
56
+ const result = {
26
57
  ...obj,
27
58
  messages: obj.messages.map((msg) => {
28
59
  if (!msg || typeof msg !== 'object')
@@ -32,40 +63,55 @@ function stripMessageBody(body) {
32
63
  return { ...m, content: normalizeWhitespace(m.content) };
33
64
  }
34
65
  if (Array.isArray(m.content)) {
35
- return {
36
- ...m,
37
- content: m.content.map((block) => {
38
- if (!block || typeof block !== 'object')
39
- return block;
40
- const b = block;
41
- if (b.type === 'text') {
42
- return { ...b, text: normalizeWhitespace(b.text) };
66
+ const normalized = m.content.map((block) => {
67
+ if (!block || typeof block !== 'object')
68
+ return block;
69
+ const b = block;
70
+ if (b.type === 'text') {
71
+ const text = normalizeWhitespace(b.text);
72
+ if (text === '')
73
+ return null;
74
+ return { ...b, text };
75
+ }
76
+ if (b.type === 'tool_result') {
77
+ toolResultsTotal++;
78
+ if (typeof b.content === 'string') {
79
+ const wasCatN = isCatNFormatted(b.content);
80
+ if (wasCatN)
81
+ catNStripped++;
82
+ return { ...b, content: stripReadPrefixes(b.content) };
43
83
  }
44
- if (b.type === 'tool_result') {
45
- if (typeof b.content === 'string') {
46
- return { ...b, content: normalizeToolResultWhitespace(b.content) };
47
- }
48
- if (Array.isArray(b.content)) {
49
- return {
50
- ...b,
51
- content: b.content.map((sub) => {
52
- if (sub &&
53
- typeof sub === 'object' &&
54
- sub.type === 'text' &&
55
- typeof sub.text === 'string') {
56
- return { ...sub, text: normalizeToolResultWhitespace(sub.text) };
57
- }
58
- return sub;
59
- }),
60
- };
61
- }
84
+ if (Array.isArray(b.content)) {
85
+ return {
86
+ ...b,
87
+ content: b.content.map((sub) => {
88
+ if (sub &&
89
+ typeof sub === 'object' &&
90
+ sub.type === 'text' &&
91
+ typeof sub.text === 'string') {
92
+ const wasCatN = isCatNFormatted(sub.text);
93
+ if (wasCatN)
94
+ catNStripped++;
95
+ return { ...sub, text: stripReadPrefixes(sub.text) };
96
+ }
97
+ return sub;
98
+ }),
99
+ };
62
100
  }
63
- return block;
64
- }),
101
+ }
102
+ return block;
103
+ }).filter((block) => block !== null);
104
+ return {
105
+ ...m,
106
+ content: normalized,
65
107
  };
66
108
  }
67
109
  return msg;
68
110
  }),
69
111
  };
112
+ if (toolResultsTotal > 0) {
113
+ console.log(`[stripMessageBody] tool_results: ${toolResultsTotal}, cat-n stripped: ${catNStripped}`);
114
+ }
115
+ return result;
70
116
  }
71
117
  //# sourceMappingURL=message-stripper.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokendiet",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Token optimization gateway CLI for Claude Code",
5
5
  "author": "vippelina",
6
6
  "private": false,