troxy-cli 1.29.41 → 1.29.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.29.41",
3
+ "version": "1.29.42",
4
4
  "description": "A secure control layer for AI agents: policies across payments, messages, logins, destructive actions, model usage, and secrets, all enforceable from the CLI",
5
5
  "homepage": "https://troxy.io",
6
6
  "bugs": {
package/src/init.js CHANGED
@@ -31,6 +31,10 @@ export async function installTroxyGuardMod(home = os.homedir(), detectFn = detec
31
31
  const destDir = path.join(home, '.claude', 'skills', 'troxy-guard');
32
32
  fs.rmSync(destDir, { recursive: true, force: true });
33
33
  fs.cpSync(TROXY_GUARD_MOD_SOURCE_DIR, destDir, { recursive: true });
34
+ // The mod can't see the CLI's version on its own; /troxy status reads it from here.
35
+ const manifest = path.join(destDir, '.claude-plugin', 'plugin.json');
36
+ const { version: troxyVersion } = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
37
+ fs.writeFileSync(manifest, JSON.stringify({ ...JSON.parse(fs.readFileSync(manifest, 'utf8')), version: troxyVersion }, null, 2) + '\n');
34
38
  return true;
35
39
  }
36
40
 
@@ -157,6 +157,19 @@ function sessionLine(t) {
157
157
  Object.entries(t.meter).map(([kind, percentUsed]) => ({ kind, percentUsed })));
158
158
  }
159
159
 
160
+ // /troxy status: everything about this session's Troxy in one place.
161
+ function statusText({ version, paused, totals, rateLimits = [] }) {
162
+ const lines = [`Troxy ${version || ''}`.trim() + ` · routing is ${paused ? 'off' : 'on'}`];
163
+ lines.push(sessionLine(totals) ? `This session: ${sessionLine(totals).replace(/^.*?This session: /, '')}`
164
+ : 'This session: nothing moved to a cheaper model yet');
165
+ const meter = rateLimits.filter(w => WINDOW_LABEL[w.kind]).map(w => `${WINDOW_LABEL[w.kind]} ${Math.round(w.percentUsed)}%`);
166
+ if (meter.length) lines.push(`Plan usage now: ${meter.join(' · ')}`);
167
+ lines.push(`Prompts with over ${ROUTE_CONTEXT_FLOOR_TOKENS / 1000}k tokens of context stay on your model (switching would rewrite the cache).`);
168
+ lines.push('All sessions: dash.troxy.io');
169
+ lines.push(paused ? 'Type /troxy on to resume.' : 'Type /troxy off to pause routing.');
170
+ return lines.join('\n');
171
+ }
172
+
160
173
  async function snapshot($) {
161
174
  const u = await $.session.usage();
162
175
  const meter = {};
@@ -246,7 +259,10 @@ export function register(on) {
246
259
  on('command.run', { command: 'troxy' }, async ($, e) => {
247
260
  const r = troxyCommand(e.args, Boolean(await $.store.get('paused')));
248
261
  await $.store.set('paused', r.paused);
249
- return { text: r.text };
262
+ if (/^\s*(on|off)\s*$/i.test(String(e.args || ''))) return { text: r.text };
263
+ const manifest = JSON.parse(await $.fs.read(`${$.plugin.root}/.claude-plugin/plugin.json`).catch(() => '{}'));
264
+ const { value: totals } = await $.state.get(SAVED);
265
+ return { text: statusText({ version: manifest.version, paused: r.paused, totals, rateLimits: (await $.session.usage()).rateLimits }) };
250
266
  });
251
267
  // Routing fails open: a scorer or hook error must never break the session,
252
268
  // it just means the requested model is used as-is.
@@ -277,4 +293,4 @@ export function register(on) {
277
293
  }
278
294
 
279
295
  // Exported for tests only - not part of the mod's runtime contract.
280
- export const _internal = { toDomainAndFields, guardBash, failClosed, parseScore, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn };
296
+ export const _internal = { toDomainAndFields, guardBash, failClosed, parseScore, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, statusText };
@@ -12,6 +12,9 @@ test('installTroxyGuardMod: copies the mod when the version supports it', async
12
12
  assert.equal(installed, true);
13
13
  assert.ok(fs.existsSync(path.join(tmpHome, '.claude', 'skills', 'troxy-guard', '.claude-plugin', 'plugin.json')));
14
14
  assert.ok(fs.existsSync(path.join(tmpHome, '.claude', 'skills', 'troxy-guard', 'hooks', 'register.js')));
15
+ // stamped with the CLI's version, which /troxy status shows
16
+ const manifest = JSON.parse(fs.readFileSync(path.join(tmpHome, '.claude', 'skills', 'troxy-guard', '.claude-plugin', 'plugin.json'), 'utf8'));
17
+ assert.equal(manifest.version, JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version);
15
18
  } finally {
16
19
  fs.rmSync(tmpHome, { recursive: true, force: true });
17
20
  }
@@ -90,3 +90,16 @@ test('addTurn: a session total from its own turns, a routed one counted at the a
90
90
  // No routed turn yet: no band.
91
91
  assert.equal(sessionLine(addTurn(undefined, { cost: 0, meter: {} }, { cost: 1, meter: {} }, 1)), undefined);
92
92
  });
93
+
94
+ test('statusText: version, routing, this session, plan usage, where the account view is', () => {
95
+ const { statusText } = _internal;
96
+ const totals = { paid: 0.03, full: 0.12, meter: {} };
97
+ assert.equal(statusText({ version: '1.29.42', paused: false, totals, rateLimits: [{ kind: 'five_hour', percentUsed: 23.4 }, { kind: 'seven_day', percentUsed: 77 }] }),
98
+ 'Troxy 1.29.42 · routing is on\n'
99
+ + 'This session: your bar lasts 4.0x longer · $0.09 saved of $0.12 at list price · estimate\n'
100
+ + 'Plan usage now: 5h 23% · 7d 77%\n'
101
+ + 'Prompts with over 50k tokens of context stay on your model (switching would rewrite the cache).\n'
102
+ + 'All sessions: dash.troxy.io\n'
103
+ + 'Type /troxy off to pause routing.');
104
+ assert.match(statusText({ paused: true }), /^Troxy · routing is off\nThis session: nothing moved to a cheaper model yet\n/);
105
+ });