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 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 } from '../src/init.js';
3
- import { runUninstall } from '../src/uninstall.js';
4
- import { runMcp } from '../src/mcp-server.js';
5
- import { runLogin, clearSession } from '../src/auth.js';
6
- import { runCards } from '../src/cards.js';
7
- import { runPolicies } from '../src/policies.js';
8
- import { runActivity } from '../src/activity.js';
9
- import { api } from '../src/api.js';
10
- import { requireJwt } from '../src/auth.js';
11
- import { table } from '../src/print.js';
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
- // ── Shorthand: troxy list [cards|policies|activity] ───────────
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); break; }
137
+ if (!sub || sub === 'cards') { await runCards(['list'], flags); break; }
70
138
  if (sub === 'policies') { await runPolicies(['list'], flags); break; }
71
- if (sub === 'activity') { await runActivity(flags); break; }
72
- console.error(` Unknown resource: ${sub}. Try: cards, policies, activity\n`);
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
- console.log(`\n API: ${health.status === 'ok' ? '✓ online' : '✗ ' + health.status}`);
79
- console.log(` DB: ${health.db}`);
80
- console.log(` Env: ${health.env}`);
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
- // Check for newer version on npm (best-effort, don't fail if offline)
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(`\n ⚠ New version available: ${latest} (you have ${current})`);
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
- npx troxy init --key <api-key> Initialize and patch MCP clients
107
- npx troxy uninstall Remove Troxy from this machine
108
- npx troxy login Log in to your dashboard account
109
- npx troxy logout Clear local session
110
- npx troxy status Check API health
111
-
112
- Cards
113
- npx troxy cards list
114
- npx troxy cards create --name "Personal" [--budget 500] [--provider stripe]
115
- npx troxy cards delete --name "Personal"
116
-
117
- Policies
118
- npx troxy policies list
119
- npx troxy policies create --name "Block high" --action BLOCK --field amount --operator gte --value 100
120
- npx troxy policies enable --name "Block high"
121
- npx troxy policies disable --name "Block high"
122
- npx troxy policies delete --name "Block high"
123
-
124
- Activity
125
- npx troxy activity [--limit 50]
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.1.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": ["mcp", "ai", "payments", "policy", "agents"],
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 } from './api.js';
2
- import { requireJwt } from './auth.js';
3
- import { table } from './print.js';
1
+ import { api } from './api.js';
2
+ import { requireKey } from './auth.js';
3
+ import { table } from './print.js';
4
4
 
5
- const DECISION_ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~' };
5
+ const ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~' };
6
6
 
7
7
  export async function runActivity(flags) {
8
- const jwt = requireJwt();
9
- const limit = Number(flags.limit || 20);
10
- const data = await api.activity(jwt, limit);
11
- const rows = data?.items || [];
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', 'Agent', 'Merchant', 'Amount', 'Policy', 'Time'],
18
- rows.map(r => [
19
- `${DECISION_ICON[r.decision] || ' '} ${r.decision}`,
20
- r.agent_name || 'unknown',
21
- r.merchant_name || '—',
22
- r.amount ? `$${Number(r.amount).toFixed(2)}` : '—',
23
- r.policy_name || '—',
24
- new Date(r.created_at).toLocaleString(),
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) throw new Error(data?.error || `HTTP ${res.status}`);
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 } from './api.js';
2
- import { requireJwt } from './auth.js';
3
- import { table } from './print.js';
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 cards = data?.cards || [];
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: npx troxy cards [list|create|delete]\n');
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 } from './api.js';
2
- import { requireJwt } from './auth.js';
3
- import { table } from './print.js';
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
- const jwt = requireJwt();
14
+ // Read-only subcommands work with just an API key
15
+ const readOnly = !sub || sub === 'list' || sub === 'describe';
7
16
 
8
- switch (sub) {
9
- case 'list':
10
- case undefined: {
11
- const data = await api.listPolicies(jwt);
12
- const policies = data?.policies || [];
13
- if (!policies.length) { console.log('\n No policies yet.\n'); return; }
14
- console.log();
15
- table(
16
- ['Name', 'Action', 'Priority', 'Status', 'Conditions'],
17
- policies.map(p => [
18
- p.name,
19
- p.action,
20
- p.priority,
21
- p.enabled ? 'enabled' : 'disabled',
22
- Array.isArray(p.conditions) ? `${p.conditions.length} condition(s)` : '—',
23
- ]),
24
- );
25
- break;
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 = policies.find(p => p.name === name);
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 = flags.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 = policies.find(p => p.name === name);
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: npx troxy policies [list|create|delete|enable|disable]\n');
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
+ }