troxy-cli 1.30.19 → 1.30.21
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 +10 -3
- package/package.json +1 -1
- package/src/init.js +6 -5
- package/src/slim.js +60 -15
- package/src/tests/slim.test.js +14 -1
package/bin/troxy.js
CHANGED
|
@@ -161,11 +161,12 @@ switch (command) {
|
|
|
161
161
|
|
|
162
162
|
// ── Unused Claude Code tools (see src/slim.js) ───────────────
|
|
163
163
|
case 'slim': {
|
|
164
|
-
const { TOOLS, setToolOff, printToolsStatus } = await import('../src/slim.js');
|
|
164
|
+
const { TOOLS, setToolOff, printToolsStatus, reportSlim } = await import('../src/slim.js');
|
|
165
165
|
if (sub === 'status' || !sub) { printToolsStatus(); break; }
|
|
166
166
|
if (TOOLS.some(t => t.id === sub)) {
|
|
167
167
|
const undo = Boolean(flags.undo);
|
|
168
168
|
setToolOff(sub, !undo);
|
|
169
|
+
await reportSlim();
|
|
169
170
|
console.log(`\n ${sub} ${undo ? 'on' : 'off'} ✓ Start a new Claude Code session to apply.\n`);
|
|
170
171
|
break;
|
|
171
172
|
}
|
|
@@ -685,20 +686,26 @@ switch (command) {
|
|
|
685
686
|
process.stdout.write(' Installing... ');
|
|
686
687
|
try {
|
|
687
688
|
try {
|
|
688
|
-
|
|
689
|
+
// The exact version, fetched fresh: `@latest` resolves from npm's own
|
|
690
|
+
// cache, which can still point at the old version for minutes after a
|
|
691
|
+
// publish, and then this "updated" to the version already installed.
|
|
692
|
+
execSync(`npm install -g troxy-cli@${latest} --prefer-online`, { stdio: 'pipe' });
|
|
689
693
|
} catch (err) {
|
|
690
694
|
const stderr = err.stderr?.toString() || '';
|
|
691
695
|
if (stderr.includes('ENOTEMPTY')) {
|
|
692
696
|
// npm bug on macOS/Homebrew: rename fails because dir has contents.
|
|
693
697
|
// rm -rf the stale dir directly, then reinstall cleanly.
|
|
694
698
|
const globalRoot = execSync('npm root -g', { stdio: 'pipe' }).toString().trim();
|
|
695
|
-
execSync(`rm -rf "${globalRoot}/troxy-cli" && npm install -g troxy-cli
|
|
699
|
+
execSync(`rm -rf "${globalRoot}/troxy-cli" && npm install -g troxy-cli@${latest} --prefer-online`, { stdio: 'pipe', shell: true });
|
|
696
700
|
} else {
|
|
697
701
|
throw err;
|
|
698
702
|
}
|
|
699
703
|
}
|
|
700
704
|
// Also refresh the npx cache so MCP servers (e.g. OpenClaw) pick up the new version
|
|
701
705
|
try { execSync(`npx --yes troxy-cli@${latest} --version`, { stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
706
|
+
const globalRoot = execSync('npm root -g', { stdio: 'pipe' }).toString().trim();
|
|
707
|
+
const installed = JSON.parse((await import('fs')).readFileSync(`${globalRoot}/troxy-cli/package.json`, 'utf8')).version;
|
|
708
|
+
if (installed !== latest) throw new Error(`npm installed ${installed}, not ${latest}. Try again in a few minutes.`);
|
|
702
709
|
console.log(`✓\n\n Updated to ${latest}.`);
|
|
703
710
|
|
|
704
711
|
// Re-run the exact same client/hook provisioning `troxy init` does,
|
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
|
|
|
@@ -566,11 +572,6 @@ export async function runInit({ key, name, proxy, concise } = {}) {
|
|
|
566
572
|
|
|
567
573
|
await reprovisionKeyConsumers(key, agentName, proxyOptIn, conciseOptIn);
|
|
568
574
|
|
|
569
|
-
if (hasClaudeCode()) {
|
|
570
|
-
const { askToSlimTools } = await import('./slim.js');
|
|
571
|
-
await askToSlimTools();
|
|
572
|
-
}
|
|
573
|
-
|
|
574
575
|
// Scan for local AI coding tools so the dashboard's "My AI tools" view has
|
|
575
576
|
// something to show. Non-fatal - a scan failure never breaks setup.
|
|
576
577
|
try {
|
package/src/slim.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import fs
|
|
2
|
-
import
|
|
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';
|
|
3
7
|
|
|
4
8
|
// `troxy slim`: turn off Claude Code tools the user never uses. Every tool
|
|
5
9
|
// Claude Code loads is part of the fixed start of each request, re-read on
|
|
@@ -11,9 +15,16 @@ import { claudeCodeSettingsPath, prompt } from './init.js';
|
|
|
11
15
|
// chrome: env CLAUDE_CODE_ENABLE_CFC=0 (same as starting with --no-chrome)
|
|
12
16
|
// artifact: permissions.deny "Artifact" (same as --disallowedTools Artifact,
|
|
13
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.
|
|
14
23
|
export const TOOLS = [
|
|
15
|
-
{ id: 'chrome', label: 'Claude in Chrome (lets Claude control your browser)'
|
|
16
|
-
|
|
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"/ },
|
|
17
28
|
];
|
|
18
29
|
|
|
19
30
|
function read(file) {
|
|
@@ -54,16 +65,50 @@ export function printToolsStatus(file = claudeCodeSettingsPath()) {
|
|
|
54
65
|
console.log('');
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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`);
|
|
68
111
|
}
|
|
112
|
+
saveConfig({ ...loadConfig(), slimChecked: true });
|
|
113
|
+
await reportSlim(cfg.apiKey, file);
|
|
69
114
|
}
|
package/src/tests/slim.test.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
7
|
-
import { TOOLS, toolIsOff, setToolOff } from '../slim.js';
|
|
7
|
+
import { TOOLS, toolIsOff, setToolOff, usedTools } from '../slim.js';
|
|
8
8
|
|
|
9
9
|
// Turning a tool off and back on must leave the user's own settings.json
|
|
10
10
|
// exactly as it was, including their own env vars and deny rules.
|
|
@@ -40,4 +40,17 @@ describe('troxy slim', () => {
|
|
|
40
40
|
it('rejects an unknown tool', () => {
|
|
41
41
|
assert.throws(() => setToolOff('nope', true, file), /Unknown tool/);
|
|
42
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
|
+
});
|
|
43
56
|
});
|