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 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
- execSync('npm install -g troxy-cli@latest', { stdio: 'pipe' });
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@latest`, { stdio: 'pipe', shell: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.30.19",
3
+ "version": "1.30.21",
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
 
@@ -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 from 'fs';
2
- import { claudeCodeSettingsPath, prompt } from './init.js';
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
- { id: 'artifact', label: 'Artifacts (lets Claude publish pages to claude.ai)' },
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
- // Asked once from `troxy init`. Default is keep: a tool turned off by surprise
58
- // is worse than a few thousand tokens.
59
- export async function askToSlimTools(file = claudeCodeSettingsPath()) {
60
- const left = TOOLS.filter(t => !toolIsOff(t.id, file));
61
- if (!left.length || !process.stdin.isTTY) return;
62
- console.log('\n Claude Code loads these tools into every request, even when unused.');
63
- console.log(' Turning off the ones you never use makes each call lighter.');
64
- console.log(' Turn back on any time with: troxy slim <name> --undo\n');
65
- for (const t of left) {
66
- const ans = (await prompt(` Turn off ${t.label}? (y/N): `)).toLowerCase();
67
- if (ans === 'y') { setToolOff(t.id, true, file); console.log(` ${t.id} off ✓`); }
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
  }
@@ -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
  });