troxy-cli 1.30.18 → 1.30.20

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/bin/troxy.js CHANGED
@@ -159,6 +159,21 @@ switch (command) {
159
159
  process.exit(1);
160
160
  }
161
161
 
162
+ // ── Unused Claude Code tools (see src/slim.js) ───────────────
163
+ case 'slim': {
164
+ const { TOOLS, setToolOff, printToolsStatus, reportSlim } = await import('../src/slim.js');
165
+ if (sub === 'status' || !sub) { printToolsStatus(); break; }
166
+ if (TOOLS.some(t => t.id === sub)) {
167
+ const undo = Boolean(flags.undo);
168
+ setToolOff(sub, !undo);
169
+ await reportSlim();
170
+ console.log(`\n ${sub} ${undo ? 'on' : 'off'} ✓ Start a new Claude Code session to apply.\n`);
171
+ break;
172
+ }
173
+ console.error(` Usage: troxy slim [status] | troxy slim <${TOOLS.map(t => t.id).join('|')}> [--undo]\n`);
174
+ process.exit(1);
175
+ }
176
+
162
177
  // ── Auth ──────────────────────────────────────────────────────
163
178
  case 'login':
164
179
  await runLogin(flags);
@@ -775,6 +790,7 @@ switch (command) {
775
790
  troxy status Health and connection status
776
791
  troxy update Update to the latest version
777
792
  troxy uninstall Remove Troxy from this machine
793
+ troxy slim Turn off Claude Code tools you never use
778
794
  troxy login Start a 12h CLI session for the commands below
779
795
  troxy usage-bar [--period 7] Your usage and what Troxy saved
780
796
  troxy activity [--limit 50] Recent requests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.30.18",
3
+ "version": "1.30.20",
4
4
  "description": "Make your Claude Code usage go further: lighter requests, less usage, less money",
5
5
  "homepage": "https://troxy.io",
6
6
  "bugs": {
package/src/init.js CHANGED
@@ -203,6 +203,12 @@ export async function reprovisionKeyConsumers(key, agentName, proxyOptIn = null,
203
203
  if (hasClaude) {
204
204
  await useDirectAnthropic();
205
205
  }
206
+ if (hasClaude || hasDesktop) {
207
+ try {
208
+ const { autoSlim } = await import('./slim.js');
209
+ await autoSlim();
210
+ } catch {}
211
+ }
206
212
  console.log('\n Restart your MCP client to activate Troxy.');
207
213
  }
208
214
 
package/src/slim.js ADDED
@@ -0,0 +1,114 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { claudeCodeSettingsPath } from './init.js';
5
+ import { loadConfig, saveConfig } from './config.js';
6
+ import { api } from './api.js';
7
+
8
+ // `troxy slim`: turn off Claude Code tools the user never uses. Every tool
9
+ // Claude Code loads is part of the fixed start of each request, re-read on
10
+ // every call. Measured 2026-11-05: with Chrome and Artifact off, the first
11
+ // call of an interactive session went from about 52.2k to 37.5k tokens.
12
+ //
13
+ // Both live in ~/.claude/settings.json, so turning them back on is just
14
+ // removing what we added:
15
+ // chrome: env CLAUDE_CODE_ENABLE_CFC=0 (same as starting with --no-chrome)
16
+ // artifact: permissions.deny "Artifact" (same as --disallowedTools Artifact,
17
+ // which removes the tool from the prompt, not just blocks calls)
18
+ //
19
+ // `tokens` is what each one added to every call in that test (chrome alone:
20
+ // 52.2k -> 45.0k; then artifact: 45.0k -> 37.5k). `used` matches a real call
21
+ // of the tool in a transcript; tool definitions are in transcripts too, so
22
+ // matching the bare name would find every session.
23
+ export const TOOLS = [
24
+ { id: 'chrome', label: 'Claude in Chrome (lets Claude control your browser)', tokens: 7200,
25
+ used: /"type":"tool_use","id":"[^"]+","name":"mcp__claude-in-chrome__/ },
26
+ { id: 'artifact', label: 'Artifacts (lets Claude publish pages to claude.ai)', tokens: 7500,
27
+ used: /"type":"tool_use","id":"[^"]+","name":"Artifact"/ },
28
+ ];
29
+
30
+ function read(file) {
31
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return {}; }
32
+ }
33
+
34
+ function write(file, s) {
35
+ fs.writeFileSync(file, JSON.stringify(s, null, 2) + '\n');
36
+ }
37
+
38
+ export function toolIsOff(id, file = claudeCodeSettingsPath()) {
39
+ const s = read(file);
40
+ if (id === 'chrome') return s.env?.CLAUDE_CODE_ENABLE_CFC === '0';
41
+ if (id === 'artifact') return (s.permissions?.deny || []).includes('Artifact');
42
+ return false;
43
+ }
44
+
45
+ export function setToolOff(id, off, file = claudeCodeSettingsPath()) {
46
+ const s = read(file);
47
+ if (id === 'chrome') {
48
+ if (off) s.env = { ...s.env, CLAUDE_CODE_ENABLE_CFC: '0' };
49
+ else if (s.env) { delete s.env.CLAUDE_CODE_ENABLE_CFC; if (!Object.keys(s.env).length) delete s.env; }
50
+ } else if (id === 'artifact') {
51
+ const deny = (s.permissions?.deny || []).filter(t => t !== 'Artifact');
52
+ if (off) deny.push('Artifact');
53
+ s.permissions = { ...s.permissions, deny };
54
+ if (!deny.length) delete s.permissions.deny;
55
+ if (!Object.keys(s.permissions).length) delete s.permissions;
56
+ } else {
57
+ throw new Error(`Unknown tool "${id}". Try: ${TOOLS.map(t => t.id).join(', ')}`);
58
+ }
59
+ write(file, s);
60
+ }
61
+
62
+ export function printToolsStatus(file = claudeCodeSettingsPath()) {
63
+ console.log('');
64
+ for (const t of TOOLS) console.log(` ${t.id.padEnd(9)} ${toolIsOff(t.id, file) ? 'off' : 'on '} ${t.label}`);
65
+ console.log('');
66
+ }
67
+
68
+ /** Ids of the tools this user has ever called, from Claude Code's own transcripts. */
69
+ export function usedTools(dir = path.join(os.homedir(), '.claude', 'projects')) {
70
+ const found = new Set();
71
+ let files = [];
72
+ try { files = fs.readdirSync(dir, { recursive: true }).filter(f => f.endsWith('.jsonl')); } catch {}
73
+ for (const f of files) {
74
+ let fd;
75
+ try { fd = fs.openSync(path.join(dir, f), 'r'); } catch { continue; }
76
+ // Chunked so a large transcript is never held in memory; the tail
77
+ // carries over so a match split across two chunks is still found.
78
+ const buf = Buffer.alloc(1 << 20);
79
+ let tail = '', n;
80
+ while ((n = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
81
+ const text = tail + buf.toString('utf8', 0, n);
82
+ for (const t of TOOLS) if (t.used.test(text)) found.add(t.id);
83
+ if (found.size === TOOLS.length) break;
84
+ tail = text.slice(-200);
85
+ }
86
+ fs.closeSync(fd);
87
+ if (found.size === TOOLS.length) break;
88
+ }
89
+ return found;
90
+ }
91
+
92
+ /** Sends which tools are off to the dashboard. Never throws. */
93
+ export async function reportSlim(apiKey = loadConfig()?.apiKey, file = claudeCodeSettingsPath()) {
94
+ if (!apiKey) return;
95
+ const slim = Object.fromEntries(TOOLS.map(t => [t.id, toolIsOff(t.id, file) ? 'off' : 'on']));
96
+ try { await api.reportToolPlans(apiKey, [{ slug: 'claude_code', installed: true, slim }]); } catch {}
97
+ }
98
+
99
+ // Runs once per machine, from `troxy init` and `troxy update`: turns off each
100
+ // tool the user has never called, so nobody has to decide anything. A tool
101
+ // they have used stays on. Once done it never runs again, so a tool turned
102
+ // back on with --undo stays on.
103
+ export async function autoSlim({ file = claudeCodeSettingsPath(), projectsDir } = {}) {
104
+ const cfg = loadConfig();
105
+ if (!cfg || cfg.slimChecked) return;
106
+ const used = usedTools(projectsDir);
107
+ for (const t of TOOLS) {
108
+ if (used.has(t.id) || toolIsOff(t.id, file)) continue;
109
+ setToolOff(t.id, true, file);
110
+ console.log(` • Turned off ${t.label.split(' (')[0]} (never used), saves about ${+(t.tokens / 1000).toFixed(1)}k tokens per call. Undo: troxy slim ${t.id} --undo`);
111
+ }
112
+ saveConfig({ ...loadConfig(), slimChecked: true });
113
+ await reportSlim(cfg.apiKey, file);
114
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, it, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ import { TOOLS, toolIsOff, setToolOff, usedTools } from '../slim.js';
8
+
9
+ // Turning a tool off and back on must leave the user's own settings.json
10
+ // exactly as it was, including their own env vars and deny rules.
11
+ let dir, file;
12
+ beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'troxy-slim-')); file = path.join(dir, 'settings.json'); });
13
+ afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
14
+
15
+ const read = () => JSON.parse(fs.readFileSync(file, 'utf8'));
16
+
17
+ describe('troxy slim', () => {
18
+ it('turns each tool off and back on, keeping the user settings', () => {
19
+ const theirs = { env: { HTTPS_PROXY: 'x' }, permissions: { deny: ['Bash(rm:*)'], allow: ['Read'] }, model: 'opus' };
20
+ fs.writeFileSync(file, JSON.stringify(theirs));
21
+ for (const t of TOOLS) {
22
+ assert.equal(toolIsOff(t.id, file), false);
23
+ setToolOff(t.id, true, file);
24
+ setToolOff(t.id, true, file);
25
+ assert.equal(toolIsOff(t.id, file), true);
26
+ }
27
+ assert.equal(read().env.CLAUDE_CODE_ENABLE_CFC, '0');
28
+ assert.deepEqual(read().permissions.deny, ['Bash(rm:*)', 'Artifact']);
29
+ for (const t of TOOLS) setToolOff(t.id, false, file);
30
+ assert.deepEqual(read(), theirs);
31
+ });
32
+
33
+ it('cleans up empty keys it created', () => {
34
+ fs.writeFileSync(file, '{}');
35
+ for (const t of TOOLS) setToolOff(t.id, true, file);
36
+ for (const t of TOOLS) setToolOff(t.id, false, file);
37
+ assert.deepEqual(read(), {});
38
+ });
39
+
40
+ it('rejects an unknown tool', () => {
41
+ assert.throws(() => setToolOff('nope', true, file), /Unknown tool/);
42
+ });
43
+
44
+ it('finds only real tool calls in transcripts, across chunk boundaries', () => {
45
+ const proj = path.join(dir, 'projects', 'p');
46
+ fs.mkdirSync(proj, { recursive: true });
47
+ // A tool definition names the tool too; that alone is not a use.
48
+ fs.writeFileSync(path.join(proj, 'a.jsonl'), '{"tools":[{"name":"Artifact"},{"name":"mcp__claude-in-chrome__navigate"}]}\n');
49
+ assert.deepEqual([...usedTools(path.join(dir, 'projects'))], []);
50
+ // A real call that straddles the 1 MB read chunk.
51
+ const call = '{"type":"tool_use","id":"toolu_1","name":"mcp__claude-in-chrome__navigate"}';
52
+ fs.writeFileSync(path.join(proj, 'b.jsonl'), 'x'.repeat((1 << 20) - 20) + call);
53
+ assert.deepEqual([...usedTools(path.join(dir, 'projects'))], ['chrome']);
54
+ assert.deepEqual([...usedTools(path.join(dir, 'missing'))], []);
55
+ });
56
+ });
package/src/uninstall.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  uninstallTroxyGuardMod,
13
13
  } from './init.js';
14
14
  import { launchdUserDomain } from './launchd.js';
15
+ import { TOOLS, toolIsOff, setToolOff } from './slim.js';
15
16
 
16
17
  const MCP_CLIENTS = [
17
18
  {
@@ -149,6 +150,7 @@ export async function runUninstall() {
149
150
  unpatchClaudeCodeProxy(claudeCodeSettingsPath());
150
151
  unpatchClaudeCodeInterception(claudeCodeSettingsPath());
151
152
  uninstallTroxyGuardMod();
153
+ for (const t of TOOLS) if (toolIsOff(t.id)) setToolOff(t.id, false);
152
154
  console.log('✓');
153
155
  } catch (err) {
154
156
  console.log('✗');