troxy-cli 1.30.17 → 1.30.19

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,20 @@ 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 } = 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
+ console.log(`\n ${sub} ${undo ? 'on' : 'off'} ✓ Start a new Claude Code session to apply.\n`);
170
+ break;
171
+ }
172
+ console.error(` Usage: troxy slim [status] | troxy slim <${TOOLS.map(t => t.id).join('|')}> [--undo]\n`);
173
+ process.exit(1);
174
+ }
175
+
162
176
  // ── Auth ──────────────────────────────────────────────────────
163
177
  case 'login':
164
178
  await runLogin(flags);
@@ -775,6 +789,7 @@ switch (command) {
775
789
  troxy status Health and connection status
776
790
  troxy update Update to the latest version
777
791
  troxy uninstall Remove Troxy from this machine
792
+ troxy slim Turn off Claude Code tools you never use
778
793
  troxy login Start a 12h CLI session for the commands below
779
794
  troxy usage-bar [--period 7] Your usage and what Troxy saved
780
795
  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.17",
3
+ "version": "1.30.19",
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": {
@@ -116,12 +116,10 @@ const RULES = [
116
116
  const paths = pathArgs(segment, 2).filter((w) => !w.startsWith('-'));
117
117
  return { action_verb: 'delete', resource: summarizeResource(paths, 'a branch'), item_count: 1 };
118
118
  }
119
- if (/^git\s+push\b/.test(segment) && (/\s--delete\b/.test(segment) || /\s\S*:\S+/.test(segment))) {
120
- // `git push origin :branch` (empty source = delete) and
121
- // `git push origin local:branch` both use the same refspec shape;
122
- // either way the part after the colon is the remote branch being
123
- // touched, which is what matters here regardless of which it deletes.
124
- const target = segment.match(/--delete\s+(\S+)/)?.[1] || segment.match(/\s\S*:(\S+)/)?.[1];
119
+ if (/^git\s+push\b/.test(segment) && (/\s--delete\b/.test(segment) || /\s:\S+/.test(segment))) {
120
+ // Only an empty source (`git push origin :branch`) deletes.
121
+ // `git push origin HEAD:main` is a normal push, not a delete.
122
+ const target = segment.match(/--delete\s+(\S+)/)?.[1] || segment.match(/\s:(\S+)/)?.[1];
125
123
  return { action_verb: 'delete', resource: target || 'a remote branch', item_count: 1 };
126
124
  }
127
125
  return null;
package/src/init.js CHANGED
@@ -566,6 +566,11 @@ export async function runInit({ key, name, proxy, concise } = {}) {
566
566
 
567
567
  await reprovisionKeyConsumers(key, agentName, proxyOptIn, conciseOptIn);
568
568
 
569
+ if (hasClaudeCode()) {
570
+ const { askToSlimTools } = await import('./slim.js');
571
+ await askToSlimTools();
572
+ }
573
+
569
574
  // Scan for local AI coding tools so the dashboard's "My AI tools" view has
570
575
  // something to show. Non-fatal - a scan failure never breaks setup.
571
576
  try {
package/src/slim.js ADDED
@@ -0,0 +1,69 @@
1
+ import fs from 'fs';
2
+ import { claudeCodeSettingsPath, prompt } from './init.js';
3
+
4
+ // `troxy slim`: turn off Claude Code tools the user never uses. Every tool
5
+ // Claude Code loads is part of the fixed start of each request, re-read on
6
+ // every call. Measured 2026-11-05: with Chrome and Artifact off, the first
7
+ // call of an interactive session went from about 52.2k to 37.5k tokens.
8
+ //
9
+ // Both live in ~/.claude/settings.json, so turning them back on is just
10
+ // removing what we added:
11
+ // chrome: env CLAUDE_CODE_ENABLE_CFC=0 (same as starting with --no-chrome)
12
+ // artifact: permissions.deny "Artifact" (same as --disallowedTools Artifact,
13
+ // which removes the tool from the prompt, not just blocks calls)
14
+ 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)' },
17
+ ];
18
+
19
+ function read(file) {
20
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return {}; }
21
+ }
22
+
23
+ function write(file, s) {
24
+ fs.writeFileSync(file, JSON.stringify(s, null, 2) + '\n');
25
+ }
26
+
27
+ export function toolIsOff(id, file = claudeCodeSettingsPath()) {
28
+ const s = read(file);
29
+ if (id === 'chrome') return s.env?.CLAUDE_CODE_ENABLE_CFC === '0';
30
+ if (id === 'artifact') return (s.permissions?.deny || []).includes('Artifact');
31
+ return false;
32
+ }
33
+
34
+ export function setToolOff(id, off, file = claudeCodeSettingsPath()) {
35
+ const s = read(file);
36
+ if (id === 'chrome') {
37
+ if (off) s.env = { ...s.env, CLAUDE_CODE_ENABLE_CFC: '0' };
38
+ else if (s.env) { delete s.env.CLAUDE_CODE_ENABLE_CFC; if (!Object.keys(s.env).length) delete s.env; }
39
+ } else if (id === 'artifact') {
40
+ const deny = (s.permissions?.deny || []).filter(t => t !== 'Artifact');
41
+ if (off) deny.push('Artifact');
42
+ s.permissions = { ...s.permissions, deny };
43
+ if (!deny.length) delete s.permissions.deny;
44
+ if (!Object.keys(s.permissions).length) delete s.permissions;
45
+ } else {
46
+ throw new Error(`Unknown tool "${id}". Try: ${TOOLS.map(t => t.id).join(', ')}`);
47
+ }
48
+ write(file, s);
49
+ }
50
+
51
+ export function printToolsStatus(file = claudeCodeSettingsPath()) {
52
+ console.log('');
53
+ for (const t of TOOLS) console.log(` ${t.id.padEnd(9)} ${toolIsOff(t.id, file) ? 'off' : 'on '} ${t.label}`);
54
+ console.log('');
55
+ }
56
+
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
+ }
69
+ }
@@ -86,6 +86,10 @@ describe('actionFor - git force push / branch delete', () => {
86
86
  assert.equal(bash('git push origin --delete old-feature').action_verb, 'delete');
87
87
  assert.equal(bash('git push origin :old-feature').action_verb, 'delete');
88
88
  });
89
+
90
+ it('does not treat a normal src:dst push as a delete', () => {
91
+ assert.equal(bash('git push -q origin HEAD:main'), null);
92
+ });
89
93
  });
90
94
 
91
95
  describe('actionFor - terraform / kubectl / docker / aws', () => {
@@ -0,0 +1,43 @@
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 } 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
+ });
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('✗');