troxy-cli 1.21.1 → 1.23.0

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/src/init.js CHANGED
@@ -185,7 +185,7 @@ export function writeAgentsFile(dir, text) {
185
185
  return file;
186
186
  }
187
187
 
188
- function prompt(question) {
188
+ export function prompt(question) {
189
189
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
190
190
  return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
191
191
  }
@@ -302,6 +302,17 @@ export async function runInit({ key, name } = {}) {
302
302
 
303
303
  await reprovisionKeyConsumers(key, agentName);
304
304
 
305
+ // Scan for local AI coding tools and (interactively) confirm the plan for
306
+ // each, so the dashboard shows real subscription cost, not token math.
307
+ // Non-fatal + TTY-gated inside runToolDetection - a scripted init never
308
+ // hangs and a scan failure never breaks setup.
309
+ try {
310
+ const { runToolDetection } = await import('./tool_detect.js');
311
+ await runToolDetection(key, { interactive: true });
312
+ } catch {
313
+ // Tool detection is a bonus, never a blocker for a successful init.
314
+ }
315
+
305
316
  console.log('\n Your payments are now protected.');
306
317
  console.log(' Dashboard → https://dash.troxy.io');
307
318
  console.log('\n For more information, visit https://docs.troxy.io\n');
@@ -553,7 +564,7 @@ export function patchClaudeCodeHooks(
553
564
  // single marker *file* whose mere existence means "Claude Code is
554
565
  // installed" - `~/.claude.json` and `~/.claude/settings.json` are both
555
566
  // created lazily and may not exist yet on a fresh install.
556
- function hasClaudeCode() {
567
+ export function hasClaudeCode() {
557
568
  try {
558
569
  execSync('claude --version', { stdio: 'ignore' });
559
570
  return true;
package/src/mcp-server.js CHANGED
@@ -12,6 +12,13 @@ const evaluateLogin = (body, apiKey) => api.evaluateLogin(body, apiKey);
12
12
  const evaluateAction = (body, apiKey) => api.evaluateAction(body, apiKey);
13
13
  const evaluateModel = (body, apiKey) => api.evaluateModel(body, apiKey);
14
14
  const reportModelUsage = (body, apiKey) => api.reportModelUsage(body, apiKey);
15
+ // Was missing entirely until 2026-08-15: every other checkpoint had an MCP
16
+ // tool, this one did not, so no MCP-connected agent could ever call
17
+ // /evaluate/secrets - not "the agent chose not to," there was nothing to
18
+ // call. evaluate.py's other_checkpoints discovery (every response advertises
19
+ // the other five) does mention it, but as a raw HTTP URL; useless to an
20
+ // agent whose only way to reach Troxy is through the tools listed here.
21
+ const evaluateSecrets = (body, apiKey) => api.evaluateSecrets(body, apiKey);
15
22
 
16
23
  export async function runMcp() {
17
24
  const config = loadConfig();
@@ -74,7 +81,7 @@ export async function runMcp() {
74
81
  // Token Optimization is supposed to be a precise record, not a mood.
75
82
  // The trigger below is mechanical instead: did this turn use a tool,
76
83
  // yes or no. That is a fact you can check, not a judgment you make.
77
- 'You MUST also call the evaluate_model tool at the start of ANY turn where you will use one or more tools of any kind — reading or writing a file, running a command, browsing, calling an external API, anything beyond a plain text reply — and whenever you switch model or effort level partway through the turn. This is not a judgment call about how the turn feels: if you are about to use a tool, call evaluate_model first, every time, regardless of how small the turn seems, and WAIT for the decision before proceeding. Give the exact model id and how many tokens you expect to use. Do not use any tool without first receiving an ALLOW or NOTIFY decision; if BLOCK, pick a cheaper or smaller model and call evaluate_model again with the new id; if ESCALATE, call wait_for_approval and do not proceed until it resolves. Then, at the end of that same turn, you MUST call report_model_usage with the audit_id from that decision and the real token total from your model API. Both halves are required: without the second call Troxy has only your prediction, and the account owner is shown that this agent\'s costs are unverified. Report the true number even when it is far above your estimate. ' +
84
+ 'You MUST also call the evaluate_model tool at the start of ANY turn where you will use one or more tools of any kind — reading or writing a file, running a command, browsing, calling an external API, anything beyond a plain text reply — and whenever you switch model or effort level partway through the turn. This is not a judgment call about how the turn feels: if you are about to use a tool, call evaluate_model first, every time, regardless of how small the turn seems, and WAIT for the decision before proceeding. Give the exact model id, how many tokens you expect to use, and how many tool calls you expect to make this turn (estimated_tool_count). Do not use any tool without first receiving an ALLOW or NOTIFY decision; if BLOCK, pick a cheaper or smaller model and call evaluate_model again with the new id; if ESCALATE, call wait_for_approval and do not proceed until it resolves. Then, at the end of that same turn, you MUST call report_model_usage with the audit_id from that decision and the real token total from your model API. Both halves are required: without the second call Troxy has only your prediction, and the account owner is shown that this agent\'s costs are unverified. Report the true number even when it is far above your estimate. ' +
78
85
  (hookCaptureActive
79
86
  // Claude Code's own Stop hook already reports real, host-captured
80
87
  // usage for every turn in this session, chat-only included - see
@@ -323,6 +330,10 @@ export async function runMcp() {
323
330
  type: 'number',
324
331
  description: 'How many tokens you expect the task to use in total, input plus output. Troxy prices this itself, so an estimate in tokens is enough; do not convert it to dollars.',
325
332
  },
333
+ estimated_tool_count: {
334
+ type: 'number',
335
+ description: 'How many tool calls you expect to make this turn (reading/writing files, running commands, browsing, calling external APIs, etc). Report your honest expectation, not a running total across the session - Troxy uses this alongside estimated_tokens to judge how complex the task actually is.',
336
+ },
326
337
  effort: {
327
338
  type: 'string',
328
339
  enum: ['low', 'medium', 'high'],
@@ -374,6 +385,48 @@ export async function runMcp() {
374
385
  },
375
386
  },
376
387
  },
388
+ {
389
+ name: 'evaluate_secrets',
390
+ description:
391
+ 'Evaluate whether content you are about to send, write, or publish should be allowed, blocked, or ' +
392
+ 'escalated based on your Troxy policies. Troxy scans the content server-side for API keys, credit ' +
393
+ 'card numbers, passwords, private keys, tokens, and personally identifiable information - you do ' +
394
+ 'not pre-classify it yourself. Call this BEFORE you share a file, send a message, push code, upload ' +
395
+ 'data, or write content to any external destination - anything that leaves the current context. ' +
396
+ 'If you are unsure whether something counts as sensitive, call this anyway.',
397
+ inputSchema: {
398
+ type: 'object',
399
+ required: ['content'],
400
+ properties: {
401
+ content: {
402
+ type: 'string',
403
+ description: 'The text, code, or data you are about to send or write.',
404
+ },
405
+ destination: {
406
+ type: 'string',
407
+ description: 'Where it is going: a person, a repo, a channel, a service, or "public" if anyone can see it.',
408
+ },
409
+ action: {
410
+ type: 'string',
411
+ enum: ['send', 'push', 'upload', 'write', 'share'],
412
+ description: 'What you are doing with it.',
413
+ },
414
+ content_type: {
415
+ type: 'string',
416
+ enum: ['code', 'message', 'file', 'config', 'log', 'other'],
417
+ description: 'What it is.',
418
+ },
419
+ agent: {
420
+ type: 'string',
421
+ description: 'Name of the agent (optional).',
422
+ },
423
+ approval_token: {
424
+ type: 'string',
425
+ description: 'Approval token from a previous ESCALATE response. Include this to proceed after the user has approved.',
426
+ },
427
+ },
428
+ },
429
+ },
377
430
  ],
378
431
  }));
379
432
 
@@ -608,6 +661,38 @@ export async function runMcp() {
608
661
  };
609
662
  }
610
663
 
664
+ if (toolName === 'evaluate_secrets') {
665
+ if (agentName && !args.agent) args.agent = agentName;
666
+ let result;
667
+ try {
668
+ result = await evaluateSecrets(args, apiKey);
669
+ } catch (err) {
670
+ return { content: [{ type: 'text', text: `Troxy error: ${err.message}` }], isError: true };
671
+ }
672
+ if (result.error) {
673
+ return { content: [{ type: 'text', text: `Troxy error: ${result.error}` }], isError: true };
674
+ }
675
+ const { decision, reason, audit_id, approval_token, detections } = result;
676
+ const found = (detections || []).map(d => d.type).join(', ');
677
+ const detail = found ? ` Found: ${found}.` : '';
678
+ let secretsText;
679
+ switch (decision) {
680
+ case 'ALLOW':
681
+ case 'NOTIFY':
682
+ secretsText = `✓ Approved.${reason ? ` ${reason}` : ''}${detail} You may proceed. (audit: ${audit_id})`;
683
+ break;
684
+ case 'BLOCK':
685
+ secretsText = `✗ Blocked.${reason ? ` ${reason}` : ''}${detail} Do not send, write, or publish this content as-is. (audit: ${audit_id})`;
686
+ break;
687
+ case 'ESCALATE':
688
+ secretsText = `⏳ This requires human approval; a request has been sent to the account owner.${detail}\n\nApproval token: ${approval_token}\n\nNow call wait_for_approval(approval_token="${approval_token}") to automatically detect approval, then call evaluate_secrets again with the same arguments PLUS this approval_token. Do not proceed until it returns approved.`;
689
+ break;
690
+ default:
691
+ secretsText = JSON.stringify(result);
692
+ }
693
+ return { content: [{ type: 'text', text: secretsText }], isError: decision === 'BLOCK' };
694
+ }
695
+
611
696
  if (toolName !== 'evaluate_payment') {
612
697
  throw new Error(`Unknown tool: ${toolName}`);
613
698
  }
package/src/mcps.js CHANGED
@@ -60,6 +60,7 @@ export async function runMcps([sub, ...args], flags) {
60
60
  m.is_me ? '← you' : '',
61
61
  ]),
62
62
  );
63
+ console.log(' Policies: org + space policies apply account-wide, so this count is the same for every agent.\n');
63
64
  break;
64
65
  }
65
66