troxy-cli 1.1.1 → 1.2.1
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 +87 -0
- package/bin/troxy.js +138 -42
- package/package.json +8 -2
- package/src/activity.js +24 -17
- package/src/api.js +13 -1
- package/src/auth.js +33 -0
- package/src/cards.js +28 -25
- package/src/mcps.js +35 -0
- package/src/policies.js +81 -38
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# troxy-cli
|
|
2
|
+
|
|
3
|
+
The official Troxy CLI — onboard AI agents, manage cards and policies from the terminal.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Node.js 18+ is required. Run `node -v` — if you get a version, skip this.
|
|
8
|
+
|
|
9
|
+
**Amazon Linux / RHEL / CentOS**
|
|
10
|
+
```bash
|
|
11
|
+
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
|
|
12
|
+
sudo yum install -y nodejs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Ubuntu / Debian**
|
|
16
|
+
```bash
|
|
17
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
|
|
18
|
+
sudo apt-get install -y nodejs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**macOS**
|
|
22
|
+
```bash
|
|
23
|
+
brew install node
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Any system (nvm)**
|
|
27
|
+
```bash
|
|
28
|
+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
|
29
|
+
source ~/.bashrc
|
|
30
|
+
nvm install --lts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Verify: `node --version` and `npx --version` should both return a version.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g troxy-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or run without installing:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx troxy-cli <command>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
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 |
|
|
55
|
+
| `troxy activity` | View recent transaction audit log |
|
|
56
|
+
| `troxy status` | Show current config and connection status |
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
1. User runs `troxy init --key txy-...` in their agent project
|
|
61
|
+
2. CLI validates the key against `api.troxy.io`, prompts for agent name
|
|
62
|
+
3. Writes a `.troxy/config.json` to the project directory
|
|
63
|
+
4. Agent uses the config to call `/evaluate` before every payment
|
|
64
|
+
|
|
65
|
+
## MCP Server
|
|
66
|
+
|
|
67
|
+
The CLI also ships an MCP server (`src/mcp-server.js`) that exposes Troxy as a tool for Claude and other MCP-compatible agents.
|
|
68
|
+
|
|
69
|
+
## Auth flow
|
|
70
|
+
|
|
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
|
|
75
|
+
|
|
76
|
+
## Stack
|
|
77
|
+
|
|
78
|
+
- Node.js 18+ (ESM)
|
|
79
|
+
- Zero runtime dependencies except `@modelcontextprotocol/sdk`
|
|
80
|
+
- Published to npm as `troxy-cli`
|
|
81
|
+
|
|
82
|
+
## Related repos
|
|
83
|
+
|
|
84
|
+
| Repo | Description |
|
|
85
|
+
|------|-------------|
|
|
86
|
+
| [troxy-tf-live](https://github.com/troxy-hq/troxy-tf-live) | Backend API the CLI talks to |
|
|
87
|
+
| [troxy-dashboard](https://github.com/troxy-hq/troxy-dashboard) | Web dashboard alternative |
|
package/bin/troxy.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runInit }
|
|
3
|
-
import { runUninstall }
|
|
4
|
-
import { runMcp }
|
|
5
|
-
import { runLogin, clearSession } from '../src/auth.js';
|
|
6
|
-
import { runCards }
|
|
7
|
-
import { runPolicies }
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { table }
|
|
2
|
+
import { runInit } from '../src/init.js';
|
|
3
|
+
import { runUninstall } from '../src/uninstall.js';
|
|
4
|
+
import { runMcp } from '../src/mcp-server.js';
|
|
5
|
+
import { runLogin, clearSession, requireKey, getKeySource } from '../src/auth.js';
|
|
6
|
+
import { runCards } from '../src/cards.js';
|
|
7
|
+
import { runPolicies } from '../src/policies.js';
|
|
8
|
+
import { runMcps } from '../src/mcps.js';
|
|
9
|
+
import { runActivity } from '../src/activity.js';
|
|
10
|
+
import { api } from '../src/api.js';
|
|
11
|
+
import { table } from '../src/print.js';
|
|
12
12
|
|
|
13
13
|
const [,, command, sub, ...rest] = process.argv;
|
|
14
14
|
const allArgs = [sub, ...rest].filter(Boolean);
|
|
@@ -26,6 +26,26 @@ for (let i = 0; i < allArgs.length; i++) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
try { await _run(); } catch (err) { _handleError(err); }
|
|
30
|
+
|
|
31
|
+
function _handleError(err) {
|
|
32
|
+
if (err.code === 'UNAUTHORIZED') {
|
|
33
|
+
const source = getKeySource();
|
|
34
|
+
if (source === 'config') {
|
|
35
|
+
console.error('\n API key revoked or invalid.');
|
|
36
|
+
console.error(' Your saved key is no longer accepted by Troxy.');
|
|
37
|
+
console.error(' Run: npx troxy init --key <new-key> to reconnect.\n');
|
|
38
|
+
} else {
|
|
39
|
+
console.error('\n API key invalid or revoked.');
|
|
40
|
+
console.error(' Check the key in your Troxy dashboard → Connections.\n');
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
44
|
+
}
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function _run() {
|
|
29
49
|
switch (command) {
|
|
30
50
|
// ── Setup ─────────────────────────────────────────────────────
|
|
31
51
|
case 'init':
|
|
@@ -37,6 +57,22 @@ switch (command) {
|
|
|
37
57
|
break;
|
|
38
58
|
|
|
39
59
|
// ── Auth ──────────────────────────────────────────────────────
|
|
60
|
+
case 'connect': {
|
|
61
|
+
const k = flags.key;
|
|
62
|
+
if (!k || !k.startsWith('txy-')) {
|
|
63
|
+
console.error('\n Usage: troxy connect --key txy-...\n');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
// Validate key before saving
|
|
67
|
+
process.stdout.write('\n Validating key... ');
|
|
68
|
+
await api.agentStatus(k);
|
|
69
|
+
console.log('✓');
|
|
70
|
+
const { saveConfig } = await import('../src/config.js');
|
|
71
|
+
saveConfig({ apiKey: k });
|
|
72
|
+
console.log(' Key saved to ~/.troxy/config.json\n');
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
case 'login':
|
|
41
77
|
await runLogin(flags);
|
|
42
78
|
break;
|
|
@@ -51,7 +87,7 @@ switch (command) {
|
|
|
51
87
|
await runMcp();
|
|
52
88
|
break;
|
|
53
89
|
|
|
54
|
-
// ── Resources
|
|
90
|
+
// ── Resources (read-only: --key or saved config; write: login) ─
|
|
55
91
|
case 'cards':
|
|
56
92
|
await runCards(positional, flags);
|
|
57
93
|
break;
|
|
@@ -60,26 +96,81 @@ switch (command) {
|
|
|
60
96
|
await runPolicies(positional, flags);
|
|
61
97
|
break;
|
|
62
98
|
|
|
99
|
+
case 'mcps':
|
|
100
|
+
await runMcps(positional, flags);
|
|
101
|
+
break;
|
|
102
|
+
|
|
63
103
|
case 'activity':
|
|
64
104
|
await runActivity(flags);
|
|
65
105
|
break;
|
|
66
106
|
|
|
67
|
-
|
|
107
|
+
case 'insights': {
|
|
108
|
+
const apiKey = requireKey(flags);
|
|
109
|
+
const period = Number(flags.period || 30);
|
|
110
|
+
const data = await api.agentInsights(apiKey, period);
|
|
111
|
+
const d = data;
|
|
112
|
+
console.log(`
|
|
113
|
+
Insights — last ${d.period_days} days
|
|
114
|
+
──────────────────────────────────
|
|
115
|
+
Total requests: ${d.total_requests}
|
|
116
|
+
Total spent: $${Number(d.total_spent).toFixed(2)}
|
|
117
|
+
Total blocked: $${Number(d.total_blocked).toFixed(2)}
|
|
118
|
+
|
|
119
|
+
Decisions
|
|
120
|
+
ALLOW: ${d.decisions.ALLOW}
|
|
121
|
+
BLOCK: ${d.decisions.BLOCK}
|
|
122
|
+
ESCALATE: ${d.decisions.ESCALATE}
|
|
123
|
+
NOTIFY: ${d.decisions.NOTIFY}
|
|
124
|
+
`);
|
|
125
|
+
if (d.top_merchants?.length) {
|
|
126
|
+
console.log(' Top merchants');
|
|
127
|
+
table(
|
|
128
|
+
['Merchant', 'Requests', 'Total Spend'],
|
|
129
|
+
d.top_merchants.map(m => [m.merchant, m.requests, `$${Number(m.total).toFixed(2)}`]),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Shorthand: troxy list [resource] ──────────────────────────
|
|
68
136
|
case 'list':
|
|
69
|
-
if (!sub || sub === 'cards') { await runCards(['list'], flags);
|
|
137
|
+
if (!sub || sub === 'cards') { await runCards(['list'], flags); break; }
|
|
70
138
|
if (sub === 'policies') { await runPolicies(['list'], flags); break; }
|
|
71
|
-
if (sub === '
|
|
72
|
-
|
|
139
|
+
if (sub === 'mcps') { await runMcps(['list'], flags); break; }
|
|
140
|
+
if (sub === 'activity') { await runActivity(flags); break; }
|
|
141
|
+
console.error(` Unknown resource: ${sub}. Try: cards, policies, mcps, activity\n`);
|
|
73
142
|
process.exit(1);
|
|
74
143
|
|
|
75
144
|
// ── Status ────────────────────────────────────────────────────
|
|
76
145
|
case 'status': {
|
|
77
146
|
const health = await api.health();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
147
|
+
process.stdout.write(`\n API: ${health.status === 'ok' ? '✓ online' : '✗ ' + health.status}\n`);
|
|
148
|
+
process.stdout.write(` DB: ${health.db}\n`);
|
|
149
|
+
process.stdout.write(` Env: ${health.env}\n`);
|
|
81
150
|
|
|
82
|
-
//
|
|
151
|
+
// If we have a key, show enriched status
|
|
152
|
+
try {
|
|
153
|
+
const apiKey = requireKey(flags);
|
|
154
|
+
const source = getKeySource();
|
|
155
|
+
const data = await api.agentStatus(apiKey);
|
|
156
|
+
const { token, account } = data;
|
|
157
|
+
const keyNote = source === 'config'
|
|
158
|
+
? '(saved — run `troxy init` to change)'
|
|
159
|
+
: source === 'env' ? '(from TROXY_API_KEY env)' : '(passed via --key)';
|
|
160
|
+
console.log(`
|
|
161
|
+
Key: ${token.prefix} ${keyNote}
|
|
162
|
+
MCP: ${token.connected ? '● connected' : '○ offline'} last seen ${token.last_seen}
|
|
163
|
+
Fallback: ${token.default_action}
|
|
164
|
+
|
|
165
|
+
Account
|
|
166
|
+
Active policies: ${account.active_policies}
|
|
167
|
+
MCPs total: ${account.total_mcps} (${account.connected_mcps} connected)
|
|
168
|
+
Requests 24h: ${account.requests_24h}
|
|
169
|
+
Default action: ${account.default_action}
|
|
170
|
+
`);
|
|
171
|
+
} catch (err) { if (err.code === 'UNAUTHORIZED') throw err; console.log(); }
|
|
172
|
+
|
|
173
|
+
// Version check
|
|
83
174
|
try {
|
|
84
175
|
const { createRequire } = await import('module');
|
|
85
176
|
const require = createRequire(import.meta.url);
|
|
@@ -87,12 +178,11 @@ switch (command) {
|
|
|
87
178
|
const res = await fetch('https://registry.npmjs.org/troxy-cli/latest', { signal: AbortSignal.timeout(3000) });
|
|
88
179
|
const { version: latest } = await res.json();
|
|
89
180
|
if (current !== latest) {
|
|
90
|
-
console.log(
|
|
91
|
-
console.log(` Update with: sudo npm install -g troxy-cli@latest`);
|
|
181
|
+
console.log(` ⚠ New version available: ${latest} (you have ${current})`);
|
|
182
|
+
console.log(` Update with: sudo npm install -g troxy-cli@latest\n`);
|
|
92
183
|
}
|
|
93
184
|
} catch {}
|
|
94
185
|
|
|
95
|
-
console.log();
|
|
96
186
|
break;
|
|
97
187
|
}
|
|
98
188
|
|
|
@@ -102,27 +192,33 @@ switch (command) {
|
|
|
102
192
|
console.log(`
|
|
103
193
|
Troxy — AI payment control
|
|
104
194
|
|
|
195
|
+
First time on a machine? Run: npx troxy init --key <api-key>
|
|
196
|
+
This saves your key to ~/.troxy/config.json — no need to pass --key again.
|
|
197
|
+
|
|
105
198
|
Setup
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
199
|
+
troxy connect --key <api-key> Save API key (CLI only — no MCP setup)
|
|
200
|
+
troxy init --key <api-key> Full setup: save key + configure MCP
|
|
201
|
+
troxy uninstall Remove Troxy from this machine
|
|
202
|
+
troxy status API health + which key is in use
|
|
203
|
+
|
|
204
|
+
Inspect (uses saved key — no flags needed after init)
|
|
205
|
+
troxy policies list
|
|
206
|
+
troxy policies describe --name "Block Amazon"
|
|
207
|
+
troxy mcps list
|
|
208
|
+
troxy cards list
|
|
209
|
+
troxy activity [--limit 50] [--mine]
|
|
210
|
+
troxy insights [--period 7]
|
|
211
|
+
|
|
212
|
+
Manage (requires: npx troxy login)
|
|
213
|
+
troxy policies create --name "X" --action BLOCK --field amount --operator gte --value 500
|
|
214
|
+
troxy policies enable --name "X"
|
|
215
|
+
troxy policies disable --name "X"
|
|
216
|
+
troxy policies delete --name "X"
|
|
217
|
+
troxy cards create --name "Personal" [--budget 500]
|
|
218
|
+
troxy cards delete --name "Personal"
|
|
219
|
+
|
|
220
|
+
Override key for a single command: --key txy-...
|
|
126
221
|
`);
|
|
127
222
|
process.exit(command ? 1 : 0);
|
|
128
223
|
}
|
|
224
|
+
} // end _run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "troxy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "AI payment control — protect your agent's payments with policies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
"bin/",
|
|
20
20
|
"src/"
|
|
21
21
|
],
|
|
22
|
-
"keywords": [
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"ai",
|
|
25
|
+
"payments",
|
|
26
|
+
"policy",
|
|
27
|
+
"agents"
|
|
28
|
+
],
|
|
23
29
|
"license": "MIT"
|
|
24
30
|
}
|
package/src/activity.js
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import { api }
|
|
2
|
-
import {
|
|
3
|
-
import { table }
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireKey } from './auth.js';
|
|
3
|
+
import { table } from './print.js';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~' };
|
|
6
6
|
|
|
7
7
|
export async function runActivity(flags) {
|
|
8
|
-
const
|
|
9
|
-
const limit
|
|
10
|
-
const
|
|
11
|
-
|
|
8
|
+
const apiKey = requireKey(flags);
|
|
9
|
+
const limit = Number(flags.limit || 20);
|
|
10
|
+
const mine = !!flags.mine;
|
|
11
|
+
|
|
12
|
+
const data = await api.agentActivity(apiKey, limit, mine);
|
|
13
|
+
const rows = data?.activity || [];
|
|
12
14
|
|
|
13
15
|
if (!rows.length) { console.log('\n No activity yet.\n'); return; }
|
|
14
16
|
|
|
15
17
|
console.log();
|
|
16
18
|
table(
|
|
17
|
-
['Decision', '
|
|
18
|
-
rows.map(r =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
['Decision', 'Merchant', 'Category', 'Amount', 'Policy', 'Card', 'Agent', 'When'],
|
|
20
|
+
rows.map(r => {
|
|
21
|
+
const icon = ICON[r.decision?.split('→')[0]] || ' ';
|
|
22
|
+
return [
|
|
23
|
+
`${icon} ${r.decision}`,
|
|
24
|
+
r.merchant,
|
|
25
|
+
r.category,
|
|
26
|
+
r.amount ? `$${Number(r.amount).toFixed(2)}` : '—',
|
|
27
|
+
r.policy,
|
|
28
|
+
r.card,
|
|
29
|
+
r.agent,
|
|
30
|
+
r.when,
|
|
31
|
+
];
|
|
32
|
+
}),
|
|
26
33
|
);
|
|
27
34
|
}
|
package/src/api.js
CHANGED
|
@@ -14,7 +14,11 @@ async function request(method, path, { apiKey, jwt, body } = {}) {
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const data = await res.json();
|
|
17
|
-
if (!res.ok)
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const err = new Error(data?.error || `HTTP ${res.status}`);
|
|
19
|
+
if (res.status === 401) err.code = 'UNAUTHORIZED';
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
18
22
|
return data;
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -50,6 +54,14 @@ export const api = {
|
|
|
50
54
|
|
|
51
55
|
// MCP heartbeat (agent API key)
|
|
52
56
|
mcpHeartbeat: (apiKey, agentName) => request('POST', '/mcp/heartbeat', { apiKey, body: agentName ? { agent_name: agentName } : undefined }),
|
|
57
|
+
|
|
58
|
+
// Agent read-only API (API key auth — no JWT required)
|
|
59
|
+
agentStatus: (apiKey) => request('GET', '/agent/status', { apiKey }),
|
|
60
|
+
agentPolicies: (apiKey) => request('GET', '/agent/policies', { apiKey }),
|
|
61
|
+
agentMcps: (apiKey) => request('GET', '/agent/mcps', { apiKey }),
|
|
62
|
+
agentCards: (apiKey) => request('GET', '/agent/cards', { apiKey }),
|
|
63
|
+
agentActivity: (apiKey, limit, mine) => request('GET', `/agent/activity?limit=${limit || 20}${mine ? '&mine=true' : ''}`, { apiKey }),
|
|
64
|
+
agentInsights: (apiKey, period) => request('GET', `/agent/insights?period=${period || 30}`, { apiKey }),
|
|
53
65
|
};
|
|
54
66
|
|
|
55
67
|
// Named export for backwards compat with init.js + mcp-server.js
|
package/src/auth.js
CHANGED
|
@@ -33,6 +33,39 @@ export function requireJwt() {
|
|
|
33
33
|
return session.jwt;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Tracks how the last key was resolved — read by bin/troxy.js error handler
|
|
37
|
+
let _lastKeySource = null;
|
|
38
|
+
|
|
39
|
+
export function getKeySource() { return _lastKeySource; }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve API key: --key flag → TROXY_API_KEY env → saved config (~/.troxy/config.json).
|
|
43
|
+
* Exits with a helpful message if nothing is found.
|
|
44
|
+
*/
|
|
45
|
+
export function requireKey(flags = {}) {
|
|
46
|
+
if (flags.key) {
|
|
47
|
+
_lastKeySource = 'flag';
|
|
48
|
+
return flags.key;
|
|
49
|
+
}
|
|
50
|
+
if (process.env.TROXY_API_KEY) {
|
|
51
|
+
_lastKeySource = 'env';
|
|
52
|
+
return process.env.TROXY_API_KEY;
|
|
53
|
+
}
|
|
54
|
+
const saved = loadConfig()?.apiKey;
|
|
55
|
+
if (saved) {
|
|
56
|
+
_lastKeySource = 'config';
|
|
57
|
+
return saved;
|
|
58
|
+
}
|
|
59
|
+
console.error('\n No API key found.');
|
|
60
|
+
console.error(' Run: npx troxy init --key txy-... to connect this machine.\n');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadConfig() {
|
|
65
|
+
const p = path.join(os.homedir(), '.troxy', 'config.json');
|
|
66
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
67
|
+
}
|
|
68
|
+
|
|
36
69
|
/** Interactive magic-link login flow. */
|
|
37
70
|
export async function runLogin({ email } = {}) {
|
|
38
71
|
if (!email) {
|
package/src/cards.js
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
|
-
import { api }
|
|
2
|
-
import { requireJwt } from './auth.js';
|
|
3
|
-
import { table }
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireJwt, requireKey } from './auth.js';
|
|
3
|
+
import { table } from './print.js';
|
|
4
4
|
|
|
5
5
|
export async function runCards([sub, ...args], flags) {
|
|
6
|
+
// list is read-only — works with API key
|
|
7
|
+
if (!sub || sub === 'list') {
|
|
8
|
+
const apiKey = requireKey(flags);
|
|
9
|
+
const data = await api.agentCards(apiKey);
|
|
10
|
+
const cards = data?.cards || [];
|
|
11
|
+
if (!cards.length) { console.log('\n No cards yet.\n'); return; }
|
|
12
|
+
console.log();
|
|
13
|
+
table(
|
|
14
|
+
['Name', 'Last 4', 'Status', 'Budget', 'Used', 'Remaining', 'Default Action'],
|
|
15
|
+
cards.map(c => [
|
|
16
|
+
c.name,
|
|
17
|
+
c.last_four ? `···${c.last_four}` : '—',
|
|
18
|
+
c.status,
|
|
19
|
+
c.monthly_budget ? `$${c.monthly_budget}` : 'no limit',
|
|
20
|
+
`$${Number(c.budget_used || 0).toFixed(2)}`,
|
|
21
|
+
c.budget_remaining != null ? `$${Number(c.budget_remaining).toFixed(2)}` : '—',
|
|
22
|
+
c.default_action || 'ALLOW',
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Write operations need JWT
|
|
6
29
|
const jwt = requireJwt();
|
|
7
30
|
|
|
8
31
|
switch (sub) {
|
|
9
|
-
case 'list':
|
|
10
|
-
case undefined: {
|
|
11
|
-
const data = await api.listCards(jwt);
|
|
12
|
-
const cards = data?.cards || [];
|
|
13
|
-
if (!cards.length) { console.log('\n No cards yet.\n'); return; }
|
|
14
|
-
console.log();
|
|
15
|
-
table(
|
|
16
|
-
['Name', 'Last 4', 'Status', 'Budget', 'Used'],
|
|
17
|
-
cards.map(c => [
|
|
18
|
-
c.name,
|
|
19
|
-
c.last4 ? `···${c.last4}` : '—',
|
|
20
|
-
c.status,
|
|
21
|
-
c.budget ? `$${c.budget}` : 'no limit',
|
|
22
|
-
`$${Number(c.budget_used || 0).toFixed(2)}`,
|
|
23
|
-
]),
|
|
24
|
-
);
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
32
|
case 'create': {
|
|
29
33
|
const name = flags.name;
|
|
30
34
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
@@ -44,8 +48,7 @@ export async function runCards([sub, ...args], flags) {
|
|
|
44
48
|
const name = flags.name;
|
|
45
49
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
46
50
|
const data = await api.listCards(jwt);
|
|
47
|
-
const
|
|
48
|
-
const card = cards.find(c => c.name === name);
|
|
51
|
+
const card = (data?.cards || []).find(c => c.name === name);
|
|
49
52
|
if (!card) { console.error(` Card "${name}" not found\n`); process.exit(1); }
|
|
50
53
|
await api.deleteCard(jwt, card.id);
|
|
51
54
|
console.log(`\n Card "${name}" deleted ✓\n`);
|
|
@@ -54,7 +57,7 @@ export async function runCards([sub, ...args], flags) {
|
|
|
54
57
|
|
|
55
58
|
default:
|
|
56
59
|
console.error(` Unknown subcommand: ${sub}`);
|
|
57
|
-
console.error(' Usage:
|
|
60
|
+
console.error(' Usage: troxy cards [list|create|delete]\n');
|
|
58
61
|
process.exit(1);
|
|
59
62
|
}
|
|
60
63
|
}
|
package/src/mcps.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
import { requireKey } from './auth.js';
|
|
4
|
+
import { table } from './print.js';
|
|
5
|
+
|
|
6
|
+
export async function runMcps([sub], flags) {
|
|
7
|
+
const apiKey = requireKey(flags);
|
|
8
|
+
|
|
9
|
+
switch (sub || 'list') {
|
|
10
|
+
case 'list': {
|
|
11
|
+
const data = await api.agentMcps(apiKey);
|
|
12
|
+
const mcps = data?.mcps || [];
|
|
13
|
+
if (!mcps.length) { console.log('\n No MCP connections yet.\n'); return; }
|
|
14
|
+
console.log();
|
|
15
|
+
table(
|
|
16
|
+
['Name', 'Prefix', 'Status', 'Last Seen', 'Policies', 'Default Action', 'Me'],
|
|
17
|
+
mcps.map(m => [
|
|
18
|
+
m.name,
|
|
19
|
+
m.token_prefix,
|
|
20
|
+
m.connected ? '● connected' : '○ offline',
|
|
21
|
+
m.last_seen,
|
|
22
|
+
m.policies_assigned,
|
|
23
|
+
m.default_action,
|
|
24
|
+
m.is_me ? '← you' : '',
|
|
25
|
+
]),
|
|
26
|
+
);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
default:
|
|
31
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
32
|
+
console.error(' Usage: troxy mcps [list]\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/policies.js
CHANGED
|
@@ -1,30 +1,70 @@
|
|
|
1
|
-
import { api }
|
|
2
|
-
import { requireJwt } from './auth.js';
|
|
3
|
-
import { table }
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireJwt, requireKey } from './auth.js';
|
|
3
|
+
import { table } from './print.js';
|
|
4
|
+
|
|
5
|
+
const DECISION_ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~', TIERED: '⊕' };
|
|
6
|
+
|
|
7
|
+
function _scope(p) {
|
|
8
|
+
if (p.global !== false) return 'all MCPs';
|
|
9
|
+
if (p.mcps && p.mcps.length > 0) return p.mcps.map(m => m.name || m.token_prefix || 'MCP').join(', ');
|
|
10
|
+
return 'no MCPs applied';
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
export async function runPolicies([sub, ...args], flags) {
|
|
6
|
-
|
|
14
|
+
// Read-only subcommands work with just an API key
|
|
15
|
+
const readOnly = !sub || sub === 'list' || sub === 'describe';
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
p
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
if (readOnly) {
|
|
18
|
+
const apiKey = requireKey(flags);
|
|
19
|
+
switch (sub || 'list') {
|
|
20
|
+
case 'list': {
|
|
21
|
+
const data = await api.agentPolicies(apiKey);
|
|
22
|
+
const policies = data?.policies || [];
|
|
23
|
+
if (!policies.length) { console.log('\n No policies yet.\n'); return; }
|
|
24
|
+
console.log();
|
|
25
|
+
table(
|
|
26
|
+
['#', 'Name', 'Action', 'Scope', 'Status', 'Conditions', 'Applies to me'],
|
|
27
|
+
policies.map(p => [
|
|
28
|
+
p.priority,
|
|
29
|
+
p.name,
|
|
30
|
+
p.action,
|
|
31
|
+
_scope(p),
|
|
32
|
+
p.enabled ? 'enabled' : 'disabled',
|
|
33
|
+
_condSummary(p),
|
|
34
|
+
p.applies_to_me ? '✓' : '—',
|
|
35
|
+
]),
|
|
36
|
+
);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case 'describe': {
|
|
41
|
+
const name = flags.name;
|
|
42
|
+
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
43
|
+
const data = await api.agentPolicies(apiKey);
|
|
44
|
+
const p = (data?.policies || []).find(x => x.name.toLowerCase() === name.toLowerCase());
|
|
45
|
+
if (!p) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
|
|
46
|
+
|
|
47
|
+
console.log(`
|
|
48
|
+
Name: ${p.name}
|
|
49
|
+
Action: ${p.action}
|
|
50
|
+
Priority: ${p.priority}
|
|
51
|
+
Status: ${p.enabled ? 'enabled' : 'disabled'}
|
|
52
|
+
Scope: ${p.scope}
|
|
53
|
+
Applies here: ${p.applies_to_me ? 'yes' : 'no'}
|
|
54
|
+
MCPs: ${p.mcps.length ? p.mcps.map(m => m.name).join(', ') : p.global ? 'all MCPs' : 'none'}
|
|
55
|
+
Conditions: ${_condDetail(p)}
|
|
56
|
+
Created: ${new Date(p.created_at).toLocaleDateString()}
|
|
57
|
+
`);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
26
60
|
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
27
63
|
|
|
64
|
+
// Write subcommands need JWT
|
|
65
|
+
const jwt = requireJwt();
|
|
66
|
+
|
|
67
|
+
switch (sub) {
|
|
28
68
|
case 'create': {
|
|
29
69
|
const name = flags.name;
|
|
30
70
|
const action = (flags.action || '').toUpperCase();
|
|
@@ -34,8 +74,6 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
34
74
|
console.error(' --action must be ALLOW, BLOCK, ESCALATE, or NOTIFY\n');
|
|
35
75
|
process.exit(1);
|
|
36
76
|
}
|
|
37
|
-
|
|
38
|
-
// Build a single condition if --field/--operator/--value are provided
|
|
39
77
|
const conditions = [];
|
|
40
78
|
if (flags.field) {
|
|
41
79
|
if (!flags.operator) { console.error(' --operator is required with --field\n'); process.exit(1); }
|
|
@@ -44,16 +82,7 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
44
82
|
if (flags.value2) cond.value2 = flags.value2;
|
|
45
83
|
conditions.push(cond);
|
|
46
84
|
}
|
|
47
|
-
|
|
48
|
-
const body = {
|
|
49
|
-
name,
|
|
50
|
-
action,
|
|
51
|
-
conditions,
|
|
52
|
-
conditions_logic: (flags.logic || 'AND').toUpperCase(),
|
|
53
|
-
enabled: true,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const policy = await api.createPolicy(jwt, body);
|
|
85
|
+
const policy = await api.createPolicy(jwt, { name, action, conditions, enabled: true });
|
|
57
86
|
console.log(`\n Policy "${policy.name}" created ✓ (priority: ${policy.priority})\n`);
|
|
58
87
|
break;
|
|
59
88
|
}
|
|
@@ -62,7 +91,7 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
62
91
|
const name = flags.name;
|
|
63
92
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
64
93
|
const { policies = [] } = await api.listPolicies(jwt);
|
|
65
|
-
const policy
|
|
94
|
+
const policy = policies.find(p => p.name === name);
|
|
66
95
|
if (!policy) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
|
|
67
96
|
await api.deletePolicy(jwt, policy.id);
|
|
68
97
|
console.log(`\n Policy "${name}" deleted ✓\n`);
|
|
@@ -71,10 +100,10 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
71
100
|
|
|
72
101
|
case 'enable':
|
|
73
102
|
case 'disable': {
|
|
74
|
-
const name
|
|
103
|
+
const name = flags.name;
|
|
75
104
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
76
105
|
const { policies = [] } = await api.listPolicies(jwt);
|
|
77
|
-
const policy
|
|
106
|
+
const policy = policies.find(p => p.name === name);
|
|
78
107
|
if (!policy) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
|
|
79
108
|
await api.updatePolicy(jwt, policy.id, { enabled: sub === 'enable' });
|
|
80
109
|
console.log(`\n Policy "${name}" ${sub}d ✓\n`);
|
|
@@ -83,7 +112,21 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
83
112
|
|
|
84
113
|
default:
|
|
85
114
|
console.error(` Unknown subcommand: ${sub}`);
|
|
86
|
-
console.error(' Usage:
|
|
115
|
+
console.error(' Usage: troxy policies [list|describe|create|delete|enable|disable]\n');
|
|
87
116
|
process.exit(1);
|
|
88
117
|
}
|
|
89
118
|
}
|
|
119
|
+
|
|
120
|
+
function _condSummary(p) {
|
|
121
|
+
const c = p.conditions || [];
|
|
122
|
+
const o = p.or_conditions || [];
|
|
123
|
+
const total = c.length + o.length;
|
|
124
|
+
if (total === 0) return 'always';
|
|
125
|
+
return `${total} condition${total > 1 ? 's' : ''}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _condDetail(p) {
|
|
129
|
+
const c = p.conditions || [];
|
|
130
|
+
if (!c.length) return 'none (always matches)';
|
|
131
|
+
return c.map(x => `${x.field} ${x.operator} ${x.value || ''}${x.value2 ? '–'+x.value2 : ''}`).join(' AND ');
|
|
132
|
+
}
|