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 +12 -10
- package/bin/troxy.js +11 -3
- package/package.json +1 -1
- package/src/auth.js +50 -9
- package/src/init.js +8 -7
- package/src/policies.js +10 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# troxy-cli
|
|
2
2
|
|
|
3
|
-
The official Troxy CLI — onboard AI agents, manage
|
|
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,
|
|
52
|
-
| `troxy login` |
|
|
53
|
-
| `troxy
|
|
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
|
|
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`
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
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
|
|
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 →
|
|
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
|
|
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
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
const code = await new Promise(resolve =>
|
|
96
|
-
rl.
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
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
|
|
147
|
-
|
|
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)';
|