troxy-cli 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # troxy-cli
2
2
 
3
- The official Troxy CLI — onboard AI agents, manage cards and policies from the terminal.
3
+ The official Troxy CLI — onboard AI agents, manage MCPs and policies from the terminal.
4
4
 
5
5
  ## Prerequisites
6
6
 
@@ -48,12 +48,13 @@ npx troxy-cli <command>
48
48
 
49
49
  | Command | Description |
50
50
  |---------|-------------|
51
- | `troxy init` | Connect an agent to Troxy — validates API key, sets agent name, writes config |
52
- | `troxy login` | Authenticate via magic-link code (enter the code from your email) |
53
- | `troxy cards` | List card aliases |
54
- | `troxy policies` | List policies |
51
+ | `troxy init` | Connect an agent to Troxy — validates API key, sets agent name, patches MCP configs |
52
+ | `troxy login` | Start a 12-hour CLI session (opens browser → copy code paste into terminal) |
53
+ | `troxy mcps` | List connected MCP agents and their status |
54
+ | `troxy policies` | List and manage policies |
55
55
  | `troxy activity` | View recent transaction audit log |
56
- | `troxy status` | Show current config and connection status |
56
+ | `troxy insights` | Spending stats and decision breakdown |
57
+ | `troxy status` | Show connection status and account overview |
57
58
 
58
59
  ## How it works
59
60
 
@@ -68,10 +69,11 @@ The CLI also ships an MCP server (`src/mcp-server.js`) that exposes Troxy as a t
68
69
 
69
70
  ## Auth flow
70
71
 
71
- `troxy login` triggers the same magic-link flow as the dashboard:
72
- - Sends a code to your email
73
- - Prompts you to enter the `XXXX-XXXX` code in the terminal
74
- - Stores the JWT locally for subsequent CLI calls
72
+ `troxy login` uses a device-code flow:
73
+ - Opens your browser to the Troxy login page
74
+ - You log in and copy the code shown on the page
75
+ - Paste the code into the terminal
76
+ - Stores the JWT locally for 12 hours
75
77
 
76
78
  ## Stack
77
79
 
package/bin/troxy.js CHANGED
@@ -31,13 +31,16 @@ try { await _run(); } catch (err) { _handleError(err); }
31
31
  function _handleError(err) {
32
32
  if (err.code === 'UNAUTHORIZED') {
33
33
  const source = getKeySource();
34
- if (source === 'config') {
34
+ if (!source) {
35
+ // JWT-based command — session expired or not logged in
36
+ console.error('\n Session expired or not logged in. Run: troxy login\n');
37
+ } else if (source === 'config') {
35
38
  console.error('\n API key revoked or invalid.');
36
39
  console.error(' Your saved key is no longer accepted by Troxy.');
37
40
  console.error(' Run: npx troxy init --key <new-key> to reconnect.\n');
38
41
  } else {
39
42
  console.error('\n API key invalid or revoked.');
40
- console.error(' Check the key in your Troxy dashboard → Connections.\n');
43
+ console.error(' Check the key in your Troxy dashboard → API Keys.\n');
41
44
  }
42
45
  } else {
43
46
  console.error(`\n Error: ${err.message}\n`);
@@ -137,7 +140,8 @@ switch (command) {
137
140
  const category = flags.category;
138
141
  if (!merchant) { console.error(' --merchant is required\n'); process.exit(1); }
139
142
  if (isNaN(amount)){ console.error(' --amount is required\n'); process.exit(1); }
140
- const body = { card_alias: card, merchant_name: merchant, amount, agent: 'troxy-cli' };
143
+ const agentName = loadConfig()?.agentName || 'troxy-cli';
144
+ const body = { card_alias: card, merchant_name: merchant, amount, agent: agentName };
141
145
  if (category) body.merchant_category = category;
142
146
  const result = await api.evaluate(body, apiKey);
143
147
  const ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~' };
@@ -251,6 +255,10 @@ switch (command) {
251
255
  console.log(`
252
256
  Troxy — AI payment control
253
257
 
258
+ First time? Run these two commands in order:
259
+ 1) npx troxy-cli init --key txy-... (get key from https://dash.troxy.io)
260
+ 2) troxy login (start a 12h CLI session)
261
+
254
262
  MCP setup (once per machine): troxy init --key <api-key>
255
263
  Login for CLI commands (12h): troxy login
256
264
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "AI payment control — protect your agent's payments with policies",
5
5
  "type": "module",
6
6
  "bin": {
package/src/auth.js CHANGED
@@ -68,6 +68,10 @@ function loadConfig() {
68
68
  }
69
69
 
70
70
  function _openBrowser(url) {
71
+ const isHeadless = process.platform === 'linux'
72
+ && !process.env.DISPLAY
73
+ && !process.env.WAYLAND_DISPLAY;
74
+ if (isHeadless) return;
71
75
  const cmd = process.platform === 'darwin' ? `open "${url}"`
72
76
  : process.platform === 'win32' ? `start "" "${url}"`
73
77
  : `xdg-open "${url}"`;
@@ -85,16 +89,53 @@ export async function runLogin() {
85
89
  process.exit(1);
86
90
  }
87
91
 
88
- // 2. Open browser
89
- console.log('\n Opening browser to complete login...');
90
- console.log(` If it didn't open, visit:\n ${session.url}\n`);
91
- _openBrowser(session.url);
92
+ // 2. Open browser (or print URL on headless servers)
93
+ const isHeadless = process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
94
+ if (isHeadless) {
95
+ console.log('\n Open this URL in your browser to get a login code:\n');
96
+ console.log(` ${session.url}`);
97
+ console.log('\n (You\'ll need to sign in to your Troxy account, then copy the displayed code.)\n');
98
+ } else {
99
+ console.log('\n Opening browser to complete login...');
100
+ console.log(` If it didn't open, visit:\n ${session.url}\n`);
101
+ _openBrowser(session.url);
102
+ }
92
103
 
93
- // 3. Prompt for the code shown in the browser
94
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
95
- const code = await new Promise(resolve =>
96
- rl.question(' Paste the code from your browser: ', ans => { rl.close(); resolve(ans.trim()); })
97
- );
104
+ // 3. Prompt for the code shown in the browser (masked like a password,
105
+ // one bullet per char so the terminal doesn't look frozen)
106
+ const code = await new Promise(resolve => {
107
+ const rl = readline.createInterface({ input: process.stdin, output: null });
108
+ process.stdout.write(' Paste the code from your browser: ');
109
+ let buf = '';
110
+ process.stdin.setRawMode(true);
111
+ process.stdin.resume();
112
+ process.stdin.setEncoding('utf8');
113
+ const onData = chunk => {
114
+ for (const ch of chunk) {
115
+ if (ch === '\r' || ch === '\n') {
116
+ process.stdin.setRawMode(false);
117
+ process.stdin.pause();
118
+ process.stdin.removeListener('data', onData);
119
+ rl.close();
120
+ process.stdout.write('\n');
121
+ resolve(buf.trim());
122
+ return;
123
+ } else if (ch === '\u0003') { // Ctrl-C
124
+ process.stdout.write('\n');
125
+ process.exit(0);
126
+ } else if (ch === '\u007f' || ch === '\b') { // backspace
127
+ if (buf.length > 0) {
128
+ buf = buf.slice(0, -1);
129
+ process.stdout.write('\b \b');
130
+ }
131
+ } else if (ch >= ' ') {
132
+ buf += ch;
133
+ process.stdout.write('•');
134
+ }
135
+ }
136
+ };
137
+ process.stdin.on('data', onData);
138
+ });
98
139
 
99
140
  if (!code) {
100
141
  console.error('\n No code entered. Run troxy login to try again.\n');
package/src/init.js CHANGED
@@ -73,12 +73,10 @@ export async function runInit({ key } = {}) {
73
73
  console.log('✓');
74
74
  }
75
75
 
76
- // Ask for agent name
77
- const agentName = await prompt(' What would you like to name this agent? (e.g. "Shopping Bot"): ');
78
- if (!agentName) {
79
- console.error('\n Error: agent name is required.\n');
80
- process.exit(1);
81
- }
76
+ // Ask for agent name (default to hostname so users don't get stuck)
77
+ const defaultName = os.hostname() || 'my-agent';
78
+ const answer = await prompt(` Name this agent (press Enter for "${defaultName}"): `);
79
+ const agentName = answer || defaultName;
82
80
 
83
81
  // Save config
84
82
  saveConfig({ apiKey: key, agentName });
@@ -121,7 +119,10 @@ export async function runInit({ key } = {}) {
121
119
  }
122
120
 
123
121
  console.log('\n Your payments are now protected.');
124
- console.log(' Dashboard → https://dash.troxy.io\n');
122
+ console.log(' Dashboard → https://dash.troxy.io');
123
+ console.log('\n Try it:');
124
+ console.log(` troxy pay --merchant "Test" --amount 10`);
125
+ console.log(' This will appear in your dashboard within seconds.\n');
125
126
  }
126
127
 
127
128
  function installService(apiKey, agentName) {
package/src/policies.js CHANGED
@@ -126,9 +126,11 @@ export async function runPolicies([sub, ...args], flags) {
126
126
  }
127
127
  }
128
128
 
129
+ const _isAny = x => !x.field || x.field === 'any' || x.operator === 'any';
130
+
129
131
  function _condSummary(p) {
130
- const c = p.conditions || [];
131
- const o = p.or_conditions || [];
132
+ const c = (p.conditions || []).filter(x => !_isAny(x));
133
+ const o = (p.or_conditions || []).filter(row => (row.conditions || []).some(x => !_isAny(x)));
132
134
  const total = c.length + o.length;
133
135
  if (total === 0) return 'always';
134
136
  return `${total} condition${total > 1 ? 's' : ''}`;
@@ -138,13 +140,15 @@ function _condDetail(p) {
138
140
  const c = p.conditions || [];
139
141
  const or = p.or_conditions || [];
140
142
  const parts = [];
141
- if (c.length) {
142
- parts.push(c.map(x => `${x.field} ${x.operator} ${x.value || ''}${x.value2 ? '–'+x.value2 : ''}`).join(' AND '));
143
+ const real = c.filter(x => !_isAny(x));
144
+ if (real.length) {
145
+ parts.push(real.map(x => `${x.field} ${x.operator} ${x.value || ''}${x.value2 ? '–'+x.value2 : ''}`).join(' AND '));
143
146
  }
144
147
  if (or.length) {
145
148
  or.forEach(row => {
146
- const conds = (row.conditions || []).map(x => `${x.field} ${x.operator} ${x.value || ''}`).join(' AND ');
147
- parts.push(`${row.action}${conds ? ' if ' + conds : ''}`);
149
+ const realConds = (row.conditions || []).filter(x => !_isAny(x));
150
+ const conds = realConds.map(x => `${x.field} ${x.operator} ${x.value || ''}`).join(' AND ');
151
+ parts.push(`${row.action || ''}${conds ? ' if ' + conds : ''}`);
148
152
  });
149
153
  }
150
154
  return parts.length ? parts.join('\n ') : 'none (always matches)';