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 +16 -0
- package/package.json +1 -1
- package/src/init.js +6 -0
- package/src/slim.js +114 -0
- package/src/tests/slim.test.js +56 -0
- package/src/uninstall.js +2 -0
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
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('✗');
|