troxy-cli 1.29.43 → 1.29.44
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.
|
|
3
|
+
"version": "1.29.44",
|
|
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": {
|
|
@@ -24,7 +24,7 @@ async function guardBash($, e, next) {
|
|
|
24
24
|
body: JSON.stringify({ domain: mapped.domain, fields: mapped.fields }),
|
|
25
25
|
signal: controller.signal,
|
|
26
26
|
});
|
|
27
|
-
result = res.ok ?
|
|
27
|
+
result = res.ok ? JSON.parse(res.text) : { decision: 'ESCALATE', reason: 'policy service returned an error' };
|
|
28
28
|
} finally {
|
|
29
29
|
clearTimeout(timer);
|
|
30
30
|
}
|
|
@@ -121,6 +121,7 @@ const PRICE = { haiku: 1, sonnet: 2, opus: 4 }; // list $/MTok in; output keeps
|
|
|
121
121
|
const WINDOW_LABEL = { five_hour: '5h', seven_day: '7d' };
|
|
122
122
|
// Drawn in Troxy's teal: the status line can't be colored and shows as a yellow warning.
|
|
123
123
|
const LINE = { plugin: 'troxy-guard', key: 'line' };
|
|
124
|
+
const SAVINGS = { plugin: 'troxy-guard', key: 'savings' }; // the band's compression + trimming text
|
|
124
125
|
const SAVED = { plugin: 'troxy-guard', key: 'saved' }; // { paid, full, meter }; survives a troxy update
|
|
125
126
|
const TROXY_TEAL = '#0a9e88';
|
|
126
127
|
|
|
@@ -157,6 +158,39 @@ function sessionLine(t) {
|
|
|
157
158
|
Object.entries(t.meter).map(([kind, percentUsed]) => ({ kind, percentUsed })));
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
// Tokens Troxy kept out of the requests of this session (compression + trimming), counted
|
|
162
|
+
// by the server. undefined until something was saved.
|
|
163
|
+
function savingsText(s) {
|
|
164
|
+
const c = (s && s.compressed_tokens) || 0, t = (s && s.trimmed_tokens) || 0;
|
|
165
|
+
if (!(c + t > 0)) return undefined;
|
|
166
|
+
const k = n => n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
|
|
167
|
+
return `${k(c + t)} tokens saved by compression and trimming`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// This session's savings from the server, or null when there is no key, no session id or
|
|
171
|
+
// the call fails (the band then keeps what it showed).
|
|
172
|
+
async function fetchSavings($) {
|
|
173
|
+
const sid = await $.session.id();
|
|
174
|
+
if (!/^[A-Za-z0-9-]{8,64}$/.test(String(sid))) return null;
|
|
175
|
+
let key = await $.env.get('TROXY_API_KEY');
|
|
176
|
+
if (!key) {
|
|
177
|
+
const home = await $.env.get('HOME');
|
|
178
|
+
if (!home) return null;
|
|
179
|
+
key = JSON.parse(await $.fs.read(`${home}/.troxy/config.json`)).apiKey;
|
|
180
|
+
}
|
|
181
|
+
if (!key) return null;
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
184
|
+
try {
|
|
185
|
+
const res = await $.http.fetch(`https://api.troxy.io/mcp/session-savings?session_id=${sid}`, {
|
|
186
|
+
headers: { 'x-troxy-key': key }, signal: controller.signal,
|
|
187
|
+
});
|
|
188
|
+
return res.ok ? JSON.parse(res.text) : null;
|
|
189
|
+
} finally {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
160
194
|
// /troxy status: everything about this session's Troxy in one place.
|
|
161
195
|
function statusText({ version, paused, totals, rateLimits = [], compactOff = false }) {
|
|
162
196
|
const lines = [`Troxy ${version || ''}`.trim() + ` · routing is ${paused ? 'off' : 'on'}`];
|
|
@@ -325,9 +359,10 @@ export function register(on) {
|
|
|
325
359
|
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
|
326
360
|
const { value: line } = await $.state.get(LINE); // subscribes the band to the line
|
|
327
361
|
const { value: note } = await $.state.get(COMPACTED);
|
|
328
|
-
|
|
362
|
+
const { value: kept } = await $.state.get(SAVINGS);
|
|
363
|
+
if ((!line && !kept && !note) || e.props.hasSurvey) return next(e);
|
|
329
364
|
const { Text } = $.ui.resolve(e);
|
|
330
|
-
return h(Text, { color: TROXY_TEAL }, `Troxy ${[line, note].filter(Boolean).join(' · ')}`);
|
|
365
|
+
return h(Text, { color: TROXY_TEAL }, `Troxy ${[line, kept, note].filter(Boolean).join(' · ')}`);
|
|
331
366
|
});
|
|
332
367
|
// After the answer, so counting never delays a prompt.
|
|
333
368
|
on('turn.complete', async ($, e, next) => {
|
|
@@ -343,10 +378,14 @@ export function register(on) {
|
|
|
343
378
|
if (line) await $.state.set(LINE, line);
|
|
344
379
|
} catch {} // a missed turn only leaves the band as it was
|
|
345
380
|
}
|
|
381
|
+
try {
|
|
382
|
+
const text = savingsText(await fetchSavings($));
|
|
383
|
+
if (text) await $.state.set(SAVINGS, text);
|
|
384
|
+
} catch {} // a missed fetch only leaves the band as it was
|
|
346
385
|
armCompact($, COMPACT_IDLE_MS);
|
|
347
386
|
return r;
|
|
348
387
|
});
|
|
349
388
|
}
|
|
350
389
|
|
|
351
390
|
// Exported for tests only - not part of the mod's runtime contract.
|
|
352
|
-
export const _internal = { toDomainAndFields, guardBash, failClosed, parseScore, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, statusText, compactPlan };
|
|
391
|
+
export const _internal = { toDomainAndFields, guardBash, failClosed, parseScore, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, statusText, compactPlan, savingsText, fetchSavings };
|
|
@@ -3,6 +3,6 @@ export type SessionSaved = { paid: number; full: number; meter: Record<string, n
|
|
|
3
3
|
|
|
4
4
|
declare module 'claude-code' {
|
|
5
5
|
interface PluginState {
|
|
6
|
-
'troxy-guard': { line: BarLine; saved: SessionSaved; compacted: BarLine }
|
|
6
|
+
'troxy-guard': { line: BarLine; saved: SessionSaved; compacted: BarLine; savings: BarLine }
|
|
7
7
|
}
|
|
8
8
|
}
|
|
@@ -118,3 +118,27 @@ test('compactPlan: big idle sessions compact, small ones and a nearly full bar d
|
|
|
118
118
|
// nearly full with no known reset: never spend it
|
|
119
119
|
assert.deepEqual(compactPlan({ tokens: 210000, now, rateLimits: [{ kind: 'seven_day', percentUsed: 99 }] }), { action: 'skip' });
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
test('savingsText: shows compression + trimming together, nothing when nothing was saved', () => {
|
|
123
|
+
const { savingsText } = _internal;
|
|
124
|
+
assert.equal(savingsText({ compressed_tokens: 5200, trimmed_tokens: 40100 }), '45k tokens saved by compression and trimming');
|
|
125
|
+
assert.equal(savingsText({ compressed_tokens: 300, trimmed_tokens: 0 }), '300 tokens saved by compression and trimming');
|
|
126
|
+
assert.equal(savingsText({ compressed_tokens: 0, trimmed_tokens: 0 }), undefined);
|
|
127
|
+
assert.equal(savingsText(null), undefined);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('fetchSavings: sends the saved key and the session id; null without a key or on an error', async () => {
|
|
131
|
+
const { fetchSavings } = _internal;
|
|
132
|
+
const calls = [];
|
|
133
|
+
const mk = ({ env = {}, config, res = { ok: true, text: '{"compressed_tokens":1}' } } = {}) => ({
|
|
134
|
+
session: { id: async () => 'abc-12345678' },
|
|
135
|
+
env: { get: async n => env[n] },
|
|
136
|
+
fs: { read: async p => { if (!config) throw new Error('missing'); calls.push(p); return JSON.stringify(config); } },
|
|
137
|
+
http: { fetch: async (url, init) => { calls.push([url, init.headers]); return res; } },
|
|
138
|
+
});
|
|
139
|
+
assert.deepEqual(await fetchSavings(mk({ env: { HOME: '/h' }, config: { apiKey: 'txy-1' } })), { compressed_tokens: 1 });
|
|
140
|
+
assert.deepEqual(calls, ['/h/.troxy/config.json', ['https://api.troxy.io/mcp/session-savings?session_id=abc-12345678', { 'x-troxy-key': 'txy-1' }]]);
|
|
141
|
+
assert.equal(await fetchSavings(mk({ env: { TROXY_API_KEY: 'txy-2' }, res: { ok: false } })), null);
|
|
142
|
+
assert.equal(await fetchSavings(mk({ env: { HOME: '/h' }, config: {} })), null);
|
|
143
|
+
assert.equal(await fetchSavings({ ...mk(), session: { id: async () => 'x' } }), null);
|
|
144
|
+
});
|