troxy-cli 1.1.0 → 1.2.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/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 } 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);
@@ -51,7 +51,7 @@ switch (command) {
51
51
  await runMcp();
52
52
  break;
53
53
 
54
- // ── Resources ─────────────────────────────────────────────────
54
+ // ── Resources (read-only: --key or saved config; write: login) ─
55
55
  case 'cards':
56
56
  await runCards(positional, flags);
57
57
  break;
@@ -60,24 +60,89 @@ switch (command) {
60
60
  await runPolicies(positional, flags);
61
61
  break;
62
62
 
63
+ case 'mcps':
64
+ await runMcps(positional, flags);
65
+ break;
66
+
63
67
  case 'activity':
64
68
  await runActivity(flags);
65
69
  break;
66
70
 
67
- // ── Shorthand: troxy list [cards|policies|activity] ───────────
71
+ case 'insights': {
72
+ const apiKey = requireKey(flags);
73
+ const period = Number(flags.period || 30);
74
+ const data = await api.agentInsights(apiKey, period);
75
+ const d = data;
76
+ console.log(`
77
+ Insights — last ${d.period_days} days
78
+ ──────────────────────────────────
79
+ Total requests: ${d.total_requests}
80
+ Total spent: $${Number(d.total_spent).toFixed(2)}
81
+ Total blocked: $${Number(d.total_blocked).toFixed(2)}
82
+
83
+ Decisions
84
+ ALLOW: ${d.decisions.ALLOW}
85
+ BLOCK: ${d.decisions.BLOCK}
86
+ ESCALATE: ${d.decisions.ESCALATE}
87
+ NOTIFY: ${d.decisions.NOTIFY}
88
+ `);
89
+ if (d.top_merchants?.length) {
90
+ console.log(' Top merchants');
91
+ table(
92
+ ['Merchant', 'Requests', 'Total Spend'],
93
+ d.top_merchants.map(m => [m.merchant, m.requests, `$${Number(m.total).toFixed(2)}`]),
94
+ );
95
+ }
96
+ break;
97
+ }
98
+
99
+ // ── Shorthand: troxy list [resource] ──────────────────────────
68
100
  case 'list':
69
- if (!sub || sub === 'cards') { await runCards(['list'], flags); break; }
101
+ if (!sub || sub === 'cards') { await runCards(['list'], flags); break; }
70
102
  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`);
103
+ if (sub === 'mcps') { await runMcps(['list'], flags); break; }
104
+ if (sub === 'activity') { await runActivity(flags); break; }
105
+ console.error(` Unknown resource: ${sub}. Try: cards, policies, mcps, activity\n`);
73
106
  process.exit(1);
74
107
 
75
108
  // ── Status ────────────────────────────────────────────────────
76
109
  case 'status': {
77
110
  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}\n`);
111
+ process.stdout.write(`\n API: ${health.status === 'ok' ? '✓ online' : '✗ ' + health.status}\n`);
112
+ process.stdout.write(` DB: ${health.db}\n`);
113
+ process.stdout.write(` Env: ${health.env}\n`);
114
+
115
+ // If we have a key, show enriched status
116
+ try {
117
+ const apiKey = requireKey(flags);
118
+ const data = await api.agentStatus(apiKey);
119
+ const { token, account } = data;
120
+ console.log(`
121
+ Token: ${token.prefix} (${token.name})
122
+ MCP: ${token.connected ? '● connected' : '○ offline'} last seen ${token.last_seen}
123
+ Fallback: ${token.default_action}
124
+
125
+ Account
126
+ Active policies: ${account.active_policies}
127
+ MCPs total: ${account.total_mcps} (${account.connected_mcps} connected)
128
+ Requests 24h: ${account.requests_24h}
129
+ Default action: ${account.default_action}
130
+ `);
131
+ } catch { console.log(); }
132
+
133
+ // Version check
134
+ try {
135
+ const { createRequire } = await import('module');
136
+ const require = createRequire(import.meta.url);
137
+ const { version: current } = require('../package.json');
138
+ const res = await fetch('https://registry.npmjs.org/troxy-cli/latest', { signal: AbortSignal.timeout(3000) });
139
+ const { version: latest } = await res.json();
140
+ if (current !== latest) {
141
+ console.log(` ⚠ New version available: ${latest} (you have ${current})`);
142
+ console.log(` Update with: sudo npm install -g troxy-cli@latest\n`);
143
+ }
144
+ } catch {}
145
+
81
146
  break;
82
147
  }
83
148
 
@@ -87,27 +152,31 @@ switch (command) {
87
152
  console.log(`
88
153
  Troxy — AI payment control
89
154
 
155
+ Read-only commands work with just an API key (--key txy-... or TROXY_API_KEY env).
156
+ Write commands (create/delete/enable/disable) require: npx troxy login
157
+
90
158
  Setup
91
- npx troxy init --key <api-key> Initialize and patch MCP clients
92
- npx troxy uninstall Remove Troxy from this machine
93
- npx troxy login Log in to your dashboard account
94
- npx troxy logout Clear local session
95
- npx troxy status Check API health
96
-
97
- Cards
98
- npx troxy cards list
99
- npx troxy cards create --name "Personal" [--budget 500] [--provider stripe]
100
- npx troxy cards delete --name "Personal"
101
-
102
- Policies
103
- npx troxy policies list
104
- npx troxy policies create --name "Block high" --action BLOCK --field amount --operator gte --value 100
105
- npx troxy policies enable --name "Block high"
106
- npx troxy policies disable --name "Block high"
107
- npx troxy policies delete --name "Block high"
108
-
109
- Activity
110
- npx troxy activity [--limit 50]
159
+ troxy init --key <api-key> Initialize agent on this machine
160
+ troxy uninstall Remove Troxy from this machine
161
+ troxy login Log in for write access
162
+ troxy logout Clear session
163
+ troxy status [--key <key>] API health + account summary
164
+
165
+ Inspect (API key only — works from EC2)
166
+ troxy policies list [--key] All policies with scope + conditions
167
+ troxy policies describe --name "X" [--key]
168
+ troxy mcps list [--key] All MCP connections + status
169
+ troxy cards list [--key] Cards with budget usage
170
+ troxy activity [--key] [--limit 50] [--mine] Recent decisions
171
+ troxy insights [--key] [--period 7] Spend stats
172
+
173
+ Manage (requires login)
174
+ troxy policies create --name "X" --action BLOCK --field amount --operator gte --value 500
175
+ troxy policies enable --name "X"
176
+ troxy policies disable --name "X"
177
+ troxy policies delete --name "X"
178
+ troxy cards create --name "Personal" [--budget 500]
179
+ troxy cards delete --name "Personal"
111
180
  `);
112
181
  process.exit(command ? 1 : 0);
113
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI payment control — protect your agent's payments with policies",
5
5
  "type": "module",
6
6
  "bin": {
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
@@ -50,6 +50,14 @@ export const api = {
50
50
 
51
51
  // MCP heartbeat (agent API key)
52
52
  mcpHeartbeat: (apiKey, agentName) => request('POST', '/mcp/heartbeat', { apiKey, body: agentName ? { agent_name: agentName } : undefined }),
53
+
54
+ // Agent read-only API (API key auth — no JWT required)
55
+ agentStatus: (apiKey) => request('GET', '/agent/status', { apiKey }),
56
+ agentPolicies: (apiKey) => request('GET', '/agent/policies', { apiKey }),
57
+ agentMcps: (apiKey) => request('GET', '/agent/mcps', { apiKey }),
58
+ agentCards: (apiKey) => request('GET', '/agent/cards', { apiKey }),
59
+ agentActivity: (apiKey, limit, mine) => request('GET', `/agent/activity?limit=${limit || 20}${mine ? '&mine=true' : ''}`, { apiKey }),
60
+ agentInsights: (apiKey, period) => request('GET', `/agent/insights?period=${period || 30}`, { apiKey }),
53
61
  };
54
62
 
55
63
  // Named export for backwards compat with init.js + mcp-server.js
package/src/auth.js CHANGED
@@ -33,6 +33,24 @@ export function requireJwt() {
33
33
  return session.jwt;
34
34
  }
35
35
 
36
+ /**
37
+ * Resolve API key: --key flag → TROXY_API_KEY env → saved config.
38
+ * Exits with a helpful message if nothing is found.
39
+ */
40
+ export function requireKey(flags = {}) {
41
+ const key = flags.key || process.env.TROXY_API_KEY || loadConfig()?.apiKey;
42
+ if (!key) {
43
+ console.error('\n No API key found. Pass --key txy-... or run: npx troxy init\n');
44
+ process.exit(1);
45
+ }
46
+ return key;
47
+ }
48
+
49
+ function loadConfig() {
50
+ const p = path.join(os.homedir(), '.troxy', 'config.json');
51
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
52
+ }
53
+
36
54
  /** Interactive magic-link login flow. */
37
55
  export async function runLogin({ email } = {}) {
38
56
  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,65 @@
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
+ const SCOPE_COLOR = { 'all MCPs': 'all MCPs', 'this MCP': '→ me', 'other MCPs': 'other' };
4
7
 
5
8
  export async function runPolicies([sub, ...args], flags) {
6
- const jwt = requireJwt();
9
+ // Read-only subcommands work with just an API key
10
+ const readOnly = !sub || sub === 'list' || sub === 'describe';
7
11
 
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;
12
+ if (readOnly) {
13
+ const apiKey = requireKey(flags);
14
+ switch (sub || 'list') {
15
+ case 'list': {
16
+ const data = await api.agentPolicies(apiKey);
17
+ const policies = data?.policies || [];
18
+ if (!policies.length) { console.log('\n No policies yet.\n'); return; }
19
+ console.log();
20
+ table(
21
+ ['#', 'Name', 'Action', 'Scope', 'Status', 'Conditions', 'Applies to me'],
22
+ policies.map(p => [
23
+ p.priority,
24
+ p.name,
25
+ p.action,
26
+ SCOPE_COLOR[p.scope] || p.scope,
27
+ p.enabled ? 'enabled' : 'disabled',
28
+ _condSummary(p),
29
+ p.applies_to_me ? '✓' : '—',
30
+ ]),
31
+ );
32
+ break;
33
+ }
34
+
35
+ case 'describe': {
36
+ const name = flags.name;
37
+ if (!name) { console.error(' --name is required\n'); process.exit(1); }
38
+ const data = await api.agentPolicies(apiKey);
39
+ const p = (data?.policies || []).find(x => x.name.toLowerCase() === name.toLowerCase());
40
+ if (!p) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
41
+
42
+ console.log(`
43
+ Name: ${p.name}
44
+ Action: ${p.action}
45
+ Priority: ${p.priority}
46
+ Status: ${p.enabled ? 'enabled' : 'disabled'}
47
+ Scope: ${p.scope}
48
+ Applies here: ${p.applies_to_me ? 'yes' : 'no'}
49
+ MCPs: ${p.mcps.length ? p.mcps.map(m => m.name).join(', ') : p.global ? 'all MCPs' : 'none'}
50
+ Conditions: ${_condDetail(p)}
51
+ Created: ${new Date(p.created_at).toLocaleDateString()}
52
+ `);
53
+ break;
54
+ }
26
55
  }
56
+ return;
57
+ }
58
+
59
+ // Write subcommands need JWT
60
+ const jwt = requireJwt();
27
61
 
62
+ switch (sub) {
28
63
  case 'create': {
29
64
  const name = flags.name;
30
65
  const action = (flags.action || '').toUpperCase();
@@ -34,8 +69,6 @@ export async function runPolicies([sub, ...args], flags) {
34
69
  console.error(' --action must be ALLOW, BLOCK, ESCALATE, or NOTIFY\n');
35
70
  process.exit(1);
36
71
  }
37
-
38
- // Build a single condition if --field/--operator/--value are provided
39
72
  const conditions = [];
40
73
  if (flags.field) {
41
74
  if (!flags.operator) { console.error(' --operator is required with --field\n'); process.exit(1); }
@@ -44,16 +77,7 @@ export async function runPolicies([sub, ...args], flags) {
44
77
  if (flags.value2) cond.value2 = flags.value2;
45
78
  conditions.push(cond);
46
79
  }
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);
80
+ const policy = await api.createPolicy(jwt, { name, action, conditions, enabled: true });
57
81
  console.log(`\n Policy "${policy.name}" created ✓ (priority: ${policy.priority})\n`);
58
82
  break;
59
83
  }
@@ -62,7 +86,7 @@ export async function runPolicies([sub, ...args], flags) {
62
86
  const name = flags.name;
63
87
  if (!name) { console.error(' --name is required\n'); process.exit(1); }
64
88
  const { policies = [] } = await api.listPolicies(jwt);
65
- const policy = policies.find(p => p.name === name);
89
+ const policy = policies.find(p => p.name === name);
66
90
  if (!policy) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
67
91
  await api.deletePolicy(jwt, policy.id);
68
92
  console.log(`\n Policy "${name}" deleted ✓\n`);
@@ -71,10 +95,10 @@ export async function runPolicies([sub, ...args], flags) {
71
95
 
72
96
  case 'enable':
73
97
  case 'disable': {
74
- const name = flags.name;
98
+ const name = flags.name;
75
99
  if (!name) { console.error(' --name is required\n'); process.exit(1); }
76
100
  const { policies = [] } = await api.listPolicies(jwt);
77
- const policy = policies.find(p => p.name === name);
101
+ const policy = policies.find(p => p.name === name);
78
102
  if (!policy) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
79
103
  await api.updatePolicy(jwt, policy.id, { enabled: sub === 'enable' });
80
104
  console.log(`\n Policy "${name}" ${sub}d ✓\n`);
@@ -83,7 +107,21 @@ export async function runPolicies([sub, ...args], flags) {
83
107
 
84
108
  default:
85
109
  console.error(` Unknown subcommand: ${sub}`);
86
- console.error(' Usage: npx troxy policies [list|create|delete|enable|disable]\n');
110
+ console.error(' Usage: troxy policies [list|describe|create|delete|enable|disable]\n');
87
111
  process.exit(1);
88
112
  }
89
113
  }
114
+
115
+ function _condSummary(p) {
116
+ const c = p.conditions || [];
117
+ const o = p.or_conditions || [];
118
+ const total = c.length + o.length;
119
+ if (total === 0) return 'always';
120
+ return `${total} condition${total > 1 ? 's' : ''}`;
121
+ }
122
+
123
+ function _condDetail(p) {
124
+ const c = p.conditions || [];
125
+ if (!c.length) return 'none (always matches)';
126
+ return c.map(x => `${x.field} ${x.operator} ${x.value || ''}${x.value2 ? '–'+x.value2 : ''}`).join(' AND ');
127
+ }