troxy-cli 1.29.53 → 1.29.55

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.53",
3
+ "version": "1.29.55",
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/daemon.js CHANGED
@@ -23,7 +23,7 @@ import { api } from './api.js';
23
23
  import { createInterceptor } from './interceptor.js';
24
24
  import { enabledProviders, interceptHostsFor, troxyRouteFor } from './providers.js';
25
25
  import { ensureInterceptionCerts } from './tls-ca.js';
26
- import { migrateBaseUrlProxyToInterception, claudeCodeSettingsPath, refreshTroxyGuardMod } from './init.js';
26
+ import { migrateBaseUrlProxyToInterception, claudeCodeSettingsPath } from './init.js';
27
27
 
28
28
  const INTERVAL_MS = 60_000;
29
29
 
@@ -139,8 +139,6 @@ export async function runDaemon() {
139
139
  process.on('unhandledRejection', (err) => {
140
140
  process.stderr.write(`[troxy-daemon] unhandled rejection (continuing): ${err?.stack || err}\n`);
141
141
  });
142
- // A CLI upgrade (npm or troxy update) then a daemon restart brings the mod up to date.
143
- refreshTroxyGuardMod().catch((err) => process.stderr.write(`[troxy-daemon] mod refresh failed: ${err.message}\n`));
144
142
 
145
143
  const cfg = loadConfig();
146
144
  if (!cfg?.apiKey) {
package/src/init.js CHANGED
@@ -38,9 +38,10 @@ export async function installTroxyGuardMod(home = os.homedir(), detectFn = detec
38
38
  return true;
39
39
  }
40
40
 
41
- // Only `troxy init` used to install the mod, so a CLI upgrade left the old mod running.
42
- // Re-copy it when the installed one is stamped with a different CLI version; a machine
43
- // without the mod (never ran init, or uninstalled) is left alone.
41
+ // Only `troxy init` used to install the mod, so `troxy update` left the old mod running.
42
+ // Called from `troxy update` only: re-copies it when the installed one is stamped with a
43
+ // different CLI version. A machine without the mod (never ran init, or uninstalled) is
44
+ // left alone.
44
45
  export async function refreshTroxyGuardMod(home = os.homedir(), detectFn = detectInstalledClaudeCodeVersion) {
45
46
  const manifest = path.join(home, '.claude', 'skills', 'troxy-guard', '.claude-plugin', 'plugin.json');
46
47
  let installed;
@@ -118,9 +118,9 @@ function _readModRoute(dir, sessionId) {
118
118
  const arm = n.arm === 'treated' || n.arm === 'holdout' ? n.arm : null;
119
119
  const isFresh = Date.now() - n.ts < _MOD_ROUTE_MAX_AGE_MS;
120
120
  const routed = isFresh && typeof n.from === 'string' && typeof n.to === 'string';
121
- return { arm, from: routed ? n.from : null, to: routed ? n.to : null, compact: isFresh && n.compact === true };
121
+ return { arm, from: routed ? n.from : null, to: routed ? n.to : null, compact: isFresh && n.compact === true, cold: isFresh && typeof n.cold === 'boolean' ? n.cold : null };
122
122
  } catch {
123
- return { arm: null, from: null, to: null, compact: false };
123
+ return { arm: null, from: null, to: null, compact: false, cold: null };
124
124
  }
125
125
  }
126
126
 
@@ -183,6 +183,8 @@ export function buildTroxyForwardHeaders(incomingHeaders, troxyKey, sandboxState
183
183
  if (modRoute && modRoute.arm) headers['x-troxy-mod-arm'] = modRoute.arm;
184
184
  // The mod's idle compaction is in flight: this is its summary request.
185
185
  if (modRoute && modRoute.compact) headers['x-troxy-mod-compact'] = '1';
186
+ // Tagged: whether this request hit the cold or warm path (for measurement).
187
+ if (modRoute && typeof modRoute.cold === 'boolean') headers['x-troxy-switch'] = modRoute.cold ? 'cold' : 'warm';
186
188
  headers.authorization = `Bearer ${troxyKey}`;
187
189
  return headers;
188
190
  }
@@ -453,9 +453,10 @@ async function* routeStep($, e, next) {
453
453
  const h = held.get(sid) || { decision: null, prompts: 0 };
454
454
  const newPrompt = Boolean(decision) && decision !== h.decision;
455
455
  if (newPrompt) h.decision = decision;
456
+ const isCold = !ls || now - ls.at >= WARM_MS || coldNext.has(sid);
456
457
  const model = (await $.store.get('paused')) ? want : stepModel({
457
458
  arm, requested: want, switched,
458
- cold: !ls || now - ls.at >= WARM_MS || coldNext.has(sid),
459
+ cold: isCold,
459
460
  score: decision ? decision.score : null, warm: ls && ls.model,
460
461
  newPrompt, heldPrompts: h.prompts, tokens: decision ? decision.tokens : Infinity,
461
462
  });
@@ -470,7 +471,8 @@ async function* routeStep($, e, next) {
470
471
  const key = `${sid}:${arm}:${want}>${model}`;
471
472
  if (file && lastNote !== key) {
472
473
  lastNote = key;
473
- await $.fs.write(file, JSON.stringify(model === want ? { arm, ts: now } : { arm, from: want, to: model, ts: now }));
474
+ const route = model === want ? { arm, cold: isCold, ts: now } : { arm, from: want, to: model, cold: isCold, ts: now };
475
+ await $.fs.write(file, JSON.stringify(route));
474
476
  }
475
477
  try { await rememberAsked($, sid, want, model); } catch {} // a missed write only loses this on resume
476
478
  if (model !== want && turn) turn.ratio = PRICE[tierOf(want)] / PRICE[tierOf(model)];