troxy-cli 1.0.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/bin/troxy.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { runInit } from '../src/init.js';
3
+ import { runMcp } from '../src/mcp-server.js';
4
+ import { runLogin, clearSession } from '../src/auth.js';
5
+ import { runCards } from '../src/cards.js';
6
+ import { runPolicies } from '../src/policies.js';
7
+ import { runActivity } from '../src/activity.js';
8
+ import { api } from '../src/api.js';
9
+ import { requireJwt } from '../src/auth.js';
10
+ import { table } from '../src/print.js';
11
+
12
+ const [,, command, sub, ...rest] = process.argv;
13
+ const allArgs = [sub, ...rest].filter(Boolean);
14
+
15
+ // Parse --flag value pairs
16
+ const flags = {};
17
+ const positional = [];
18
+ for (let i = 0; i < allArgs.length; i++) {
19
+ if (allArgs[i].startsWith('--')) {
20
+ const key = allArgs[i].slice(2);
21
+ const next = allArgs[i + 1];
22
+ flags[key] = next && !next.startsWith('--') ? allArgs[++i] : true;
23
+ } else {
24
+ positional.push(allArgs[i]);
25
+ }
26
+ }
27
+
28
+ switch (command) {
29
+ // ── Setup ─────────────────────────────────────────────────────
30
+ case 'init':
31
+ await runInit(flags);
32
+ break;
33
+
34
+ // ── Auth ──────────────────────────────────────────────────────
35
+ case 'login':
36
+ await runLogin(flags);
37
+ break;
38
+
39
+ case 'logout':
40
+ clearSession();
41
+ console.log('\n Logged out ✓\n');
42
+ break;
43
+
44
+ // ── MCP server (started by MCP clients) ───────────────────────
45
+ case 'mcp':
46
+ await runMcp();
47
+ break;
48
+
49
+ // ── Resources ─────────────────────────────────────────────────
50
+ case 'cards':
51
+ await runCards(positional, flags);
52
+ break;
53
+
54
+ case 'policies':
55
+ await runPolicies(positional, flags);
56
+ break;
57
+
58
+ case 'activity':
59
+ await runActivity(flags);
60
+ break;
61
+
62
+ // ── Shorthand: troxy list [cards|policies|activity] ───────────
63
+ case 'list':
64
+ if (!sub || sub === 'cards') { await runCards(['list'], flags); break; }
65
+ if (sub === 'policies') { await runPolicies(['list'], flags); break; }
66
+ if (sub === 'activity') { await runActivity(flags); break; }
67
+ console.error(` Unknown resource: ${sub}. Try: cards, policies, activity\n`);
68
+ process.exit(1);
69
+
70
+ // ── Status ────────────────────────────────────────────────────
71
+ case 'status': {
72
+ const health = await api.health();
73
+ console.log(`\n API: ${health.status === 'ok' ? '✓ online' : '✗ ' + health.status}`);
74
+ console.log(` DB: ${health.db}`);
75
+ console.log(` Env: ${health.env}\n`);
76
+ break;
77
+ }
78
+
79
+ // ── Help / default ────────────────────────────────────────────
80
+ default:
81
+ if (command) console.error(` Unknown command: ${command}\n`);
82
+ console.log(`
83
+ Troxy — AI payment control
84
+
85
+ Setup
86
+ npx troxy init --key <api-key> Initialize and patch MCP clients
87
+ npx troxy login Log in to your dashboard account
88
+ npx troxy logout Clear local session
89
+ npx troxy status Check API health
90
+
91
+ Cards
92
+ npx troxy cards list
93
+ npx troxy cards create --name "Personal" [--budget 500] [--provider stripe]
94
+ npx troxy cards delete --name "Personal"
95
+
96
+ Policies
97
+ npx troxy policies list
98
+ npx troxy policies create --name "Block high" --action BLOCK --field amount --operator gte --value 100
99
+ npx troxy policies enable --name "Block high"
100
+ npx troxy policies disable --name "Block high"
101
+ npx troxy policies delete --name "Block high"
102
+
103
+ Activity
104
+ npx troxy activity [--limit 50]
105
+ `);
106
+ process.exit(command ? 1 : 0);
107
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "troxy-cli",
3
+ "version": "1.0.0",
4
+ "description": "AI payment control — protect your agent's payments with policies",
5
+ "type": "module",
6
+ "bin": {
7
+ "troxy": "bin/troxy.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test src/tests/*.test.js"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.10.2"
14
+ },
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "files": [
19
+ "bin/",
20
+ "src/"
21
+ ],
22
+ "keywords": ["mcp", "ai", "payments", "policy", "agents"],
23
+ "license": "MIT"
24
+ }
@@ -0,0 +1,26 @@
1
+ import { api } from './api.js';
2
+ import { requireJwt } from './auth.js';
3
+ import { table } from './print.js';
4
+
5
+ const DECISION_ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~' };
6
+
7
+ export async function runActivity(flags) {
8
+ const jwt = requireJwt();
9
+ const limit = Number(flags.limit || 20);
10
+ const rows = await api.activity(jwt, limit);
11
+
12
+ if (!rows.length) { console.log('\n No activity yet.\n'); return; }
13
+
14
+ console.log();
15
+ table(
16
+ ['Decision', 'Agent', 'Merchant', 'Amount', 'Policy', 'Time'],
17
+ rows.map(r => [
18
+ `${DECISION_ICON[r.decision] || ' '} ${r.decision}`,
19
+ r.agent_name || 'unknown',
20
+ r.merchant_name || '—',
21
+ r.amount ? `$${Number(r.amount).toFixed(2)}` : '—',
22
+ r.policy_name || '—',
23
+ new Date(r.created_at).toLocaleString(),
24
+ ]),
25
+ );
26
+ }
package/src/api.js ADDED
@@ -0,0 +1,56 @@
1
+ export const BASE_URL =
2
+ process.env.TROXY_API_URL ||
3
+ 'https://wuxyx33bka.execute-api.us-east-1.amazonaws.com';
4
+
5
+ async function request(method, path, { apiKey, jwt, body } = {}) {
6
+ const headers = { 'Content-Type': 'application/json' };
7
+ if (apiKey) headers['X-Troxy-Key'] = apiKey;
8
+ if (jwt) headers['Authorization'] = `Bearer ${jwt}`;
9
+
10
+ const res = await fetch(`${BASE_URL}${path}`, {
11
+ method,
12
+ headers,
13
+ body: body ? JSON.stringify(body) : undefined,
14
+ });
15
+
16
+ const data = await res.json();
17
+ if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
18
+ return data;
19
+ }
20
+
21
+ export const api = {
22
+ // Auth
23
+ health: () => request('GET', '/health'),
24
+ magicLink: (email) => request('POST', '/auth/magic-link', { body: { email } }),
25
+ verify: (token) => request('POST', '/auth/verify', { body: { token } }),
26
+
27
+ // Cards
28
+ listCards: (jwt) => request('GET', '/cards', { jwt }),
29
+ createCard: (jwt, b) => request('POST', '/cards', { jwt, body: b }),
30
+ updateCard: (jwt, id, b) => request('PUT', `/cards/${id}`, { jwt, body: b }),
31
+ deleteCard: (jwt, id) => request('DELETE', `/cards/${id}`, { jwt }),
32
+
33
+ // Policies
34
+ listPolicies: (jwt) => request('GET', '/dashboard/policies', { jwt }),
35
+ createPolicy: (jwt, b) => request('POST', '/dashboard/policies', { jwt, body: b }),
36
+ updatePolicy: (jwt, id, b) => request('PATCH', `/dashboard/policies/${id}`, { jwt, body: b }),
37
+ deletePolicy: (jwt, id) => request('DELETE', `/dashboard/policies/${id}`, { jwt }),
38
+
39
+ // Activity + insights
40
+ activity: (jwt, limit) => request('GET', `/dashboard/activity?limit=${limit || 20}`, { jwt }),
41
+ insights: (jwt) => request('GET', '/dashboard/insights', { jwt }),
42
+
43
+ // Tokens
44
+ listTokens: (jwt) => request('GET', '/tokens', { jwt }),
45
+ createToken: (jwt, b) => request('POST', '/tokens', { jwt, body: b }),
46
+ revokeToken: (jwt, id) => request('DELETE', `/tokens/${id}`, { jwt }),
47
+
48
+ // Evaluate (agent API key)
49
+ evaluate: (body, apiKey) => request('POST', '/evaluate', { apiKey, body }),
50
+
51
+ // MCP heartbeat (agent API key)
52
+ mcpHeartbeat: (apiKey) => request('POST', '/mcp/heartbeat', { apiKey }),
53
+ };
54
+
55
+ // Named export for backwards compat with init.js + mcp-server.js
56
+ export const evaluatePayment = (body, apiKey) => api.evaluate(body, apiKey);
package/src/auth.js ADDED
@@ -0,0 +1,69 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import readline from 'readline';
5
+ import { api } from './api.js';
6
+
7
+ const SESSION_FILE = path.join(os.homedir(), '.troxy', 'session.json');
8
+
9
+ export function loadSession() {
10
+ try {
11
+ return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ export function saveSession(data) {
18
+ fs.mkdirSync(path.dirname(SESSION_FILE), { recursive: true });
19
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
20
+ }
21
+
22
+ export function clearSession() {
23
+ try { fs.unlinkSync(SESSION_FILE); } catch {}
24
+ }
25
+
26
+ /** Require a valid JWT session or exit with a helpful message. */
27
+ export function requireJwt() {
28
+ const session = loadSession();
29
+ if (!session?.jwt) {
30
+ console.error('\n Not logged in. Run: npx troxy login\n');
31
+ process.exit(1);
32
+ }
33
+ return session.jwt;
34
+ }
35
+
36
+ /** Interactive magic-link login flow. */
37
+ export async function runLogin({ email } = {}) {
38
+ if (!email) {
39
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
40
+ email = await new Promise(resolve => rl.question(' Email: ', ans => { rl.close(); resolve(ans.trim()); }));
41
+ }
42
+
43
+ process.stdout.write(`\n Sending magic link to ${email}... `);
44
+ try {
45
+ await api.magicLink(email);
46
+ console.log('✓');
47
+ } catch (err) {
48
+ console.log('✗');
49
+ console.error(` Error: ${err.message}\n`);
50
+ process.exit(1);
51
+ }
52
+
53
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
54
+ const token = await new Promise(resolve =>
55
+ rl.question(' Enter the code from your email: ', ans => { rl.close(); resolve(ans.trim()); })
56
+ );
57
+
58
+ process.stdout.write(' Verifying... ');
59
+ try {
60
+ const result = await api.verify(token);
61
+ saveSession({ jwt: result.access_token, email });
62
+ console.log('✓');
63
+ console.log(`\n Logged in as ${email}\n`);
64
+ } catch (err) {
65
+ console.log('✗');
66
+ console.error(` Error: ${err.message}\n`);
67
+ process.exit(1);
68
+ }
69
+ }
package/src/cards.js ADDED
@@ -0,0 +1,60 @@
1
+ import { api } from './api.js';
2
+ import { requireJwt } from './auth.js';
3
+ import { table } from './print.js';
4
+
5
+ export async function runCards([sub, ...args], flags) {
6
+ const jwt = requireJwt();
7
+
8
+ 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.last_four ? `···${c.last_four}` : '—',
20
+ c.status,
21
+ c.monthly_budget ? `$${c.monthly_budget}` : 'no limit',
22
+ `$${Number(c.budget_used || 0).toFixed(2)}`,
23
+ ]),
24
+ );
25
+ break;
26
+ }
27
+
28
+ case 'create': {
29
+ const name = flags.name;
30
+ if (!name) { console.error(' --name is required\n'); process.exit(1); }
31
+ const body = {
32
+ alias_name: name,
33
+ monthly_budget: flags.budget ? Number(flags.budget) : null,
34
+ provider: flags.provider || null,
35
+ card_number: flags['card-number'] || null,
36
+ status: 'active',
37
+ };
38
+ const card = await api.createCard(jwt, body);
39
+ console.log(`\n Card "${card.alias_name}" created ✓\n`);
40
+ break;
41
+ }
42
+
43
+ case 'delete': {
44
+ const name = flags.name;
45
+ if (!name) { console.error(' --name is required\n'); process.exit(1); }
46
+ const data = await api.listCards(jwt);
47
+ const cards = data?.cards || [];
48
+ const card = cards.find(c => c.name === name);
49
+ if (!card) { console.error(` Card "${name}" not found\n`); process.exit(1); }
50
+ await api.deleteCard(jwt, card.id);
51
+ console.log(`\n Card "${name}" deleted ✓\n`);
52
+ break;
53
+ }
54
+
55
+ default:
56
+ console.error(` Unknown subcommand: ${sub}`);
57
+ console.error(' Usage: npx troxy cards [list|create|delete]\n');
58
+ process.exit(1);
59
+ }
60
+ }
package/src/config.js ADDED
@@ -0,0 +1,19 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.troxy');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ export function loadConfig() {
9
+ try {
10
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ export function saveConfig(data) {
17
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
18
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
19
+ }
package/src/init.js ADDED
@@ -0,0 +1,125 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { saveConfig } from './config.js';
5
+ import { evaluatePayment } from './api.js';
6
+
7
+ // MCP config locations per client and platform
8
+ const MCP_CLIENTS = [
9
+ {
10
+ name: 'Claude Desktop',
11
+ path: {
12
+ darwin: path.join(os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json'),
13
+ win32: path.join(process.env.APPDATA || os.homedir(), 'Claude/claude_desktop_config.json'),
14
+ linux: path.join(os.homedir(), '.config/claude/claude_desktop_config.json'),
15
+ },
16
+ },
17
+ {
18
+ name: 'Cursor',
19
+ path: {
20
+ darwin: path.join(os.homedir(), '.cursor/mcp.json'),
21
+ win32: path.join(os.homedir(), '.cursor/mcp.json'),
22
+ linux: path.join(os.homedir(), '.cursor/mcp.json'),
23
+ },
24
+ },
25
+ {
26
+ name: 'Windsurf',
27
+ path: {
28
+ darwin: path.join(os.homedir(), '.codeium/windsurf/mcp_config.json'),
29
+ win32: path.join(os.homedir(), '.codeium/windsurf/mcp_config.json'),
30
+ linux: path.join(os.homedir(), '.codeium/windsurf/mcp_config.json'),
31
+ },
32
+ },
33
+ ];
34
+
35
+ export async function runInit({ key } = {}) {
36
+ if (!key || !key.startsWith('txy-')) {
37
+ console.error('\n Error: --key is required and must start with txy-');
38
+ console.error(' Usage: npx troxy init --key txy-...\n');
39
+ process.exit(1);
40
+ }
41
+
42
+ console.log('\n Troxy — AI payment control\n');
43
+
44
+ // Validate the key by hitting /evaluate (404 card = key is valid, TypeError = network failure)
45
+ process.stdout.write(' Validating API key... ');
46
+ try {
47
+ await evaluatePayment(
48
+ { agent: 'troxy-init', card_alias_name: '__ping__', amount: 0 },
49
+ key,
50
+ );
51
+ console.log('✓');
52
+ } catch (err) {
53
+ if (err instanceof TypeError) {
54
+ // Actual network failure — couldn't reach the API at all
55
+ console.log('✗');
56
+ console.error('\n Error: Could not reach Troxy API. Check your internet connection.\n');
57
+ process.exit(1);
58
+ }
59
+ // API was reachable — check what it said
60
+ if (err.message?.toLowerCase().includes('invalid') || err.message?.toLowerCase().includes('revoked')) {
61
+ console.log('✗');
62
+ console.error('\n Error: Invalid or revoked API key.\n');
63
+ process.exit(1);
64
+ }
65
+ // 404 (card not found) or any other API error = key is valid, API is up
66
+ console.log('✓');
67
+ }
68
+
69
+ // Save config
70
+ saveConfig({ apiKey: key });
71
+ console.log(' Config saved (~/.troxy/config.json) ✓');
72
+
73
+ // Detect and patch MCP clients
74
+ const platform = process.platform;
75
+ const detected = MCP_CLIENTS.filter(c => {
76
+ const p = c.path[platform] ?? c.path.linux;
77
+ return fs.existsSync(p);
78
+ });
79
+
80
+ if (detected.length === 0) {
81
+ console.log('\n No MCP clients detected (Claude Desktop, Cursor, Windsurf).');
82
+ console.log(' Troxy MCP server config:\n');
83
+ console.log(JSON.stringify(mcpEntry(key), null, 4));
84
+ console.log('\n Add the above to your MCP client\'s config under "mcpServers".\n');
85
+ } else {
86
+ console.log('\n MCP clients found:');
87
+ for (const client of detected) {
88
+ const configPath = client.path[platform] ?? client.path.linux;
89
+ try {
90
+ patchMcpConfig(configPath, key);
91
+ console.log(` • ${client.name} ✓`);
92
+ } catch (err) {
93
+ console.log(` • ${client.name} ✗ (${err.message})`);
94
+ }
95
+ }
96
+ console.log('\n Restart your MCP client to activate Troxy.');
97
+ }
98
+
99
+ console.log('\n Your payments are now protected.');
100
+ console.log(' Dashboard → https://dashboard.troxy.ai\n');
101
+ }
102
+
103
+ function mcpEntry(apiKey) {
104
+ return {
105
+ troxy: {
106
+ command: 'npx',
107
+ args: ['troxy', 'mcp'],
108
+ env: { TROXY_API_KEY: apiKey },
109
+ },
110
+ };
111
+ }
112
+
113
+ function patchMcpConfig(configPath, apiKey) {
114
+ let config = {};
115
+ try {
116
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
117
+ } catch {
118
+ // file exists but is empty or malformed — start fresh
119
+ }
120
+
121
+ if (!config.mcpServers) config.mcpServers = {};
122
+ config.mcpServers.troxy = mcpEntry(apiKey).troxy;
123
+
124
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
125
+ }
@@ -0,0 +1,120 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js';
7
+ import { loadConfig } from './config.js';
8
+ import { evaluatePayment, api } from './api.js';
9
+
10
+ export async function runMcp() {
11
+ const config = loadConfig();
12
+ const apiKey = process.env.TROXY_API_KEY || config?.apiKey;
13
+
14
+ if (!apiKey) {
15
+ process.stderr.write(
16
+ 'Troxy: no API key found. Run: npx troxy init --key txy-...\n',
17
+ );
18
+ process.exit(1);
19
+ }
20
+
21
+ const server = new Server(
22
+ { name: 'troxy', version: '0.1.0' },
23
+ { capabilities: { tools: {} } },
24
+ );
25
+
26
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
27
+ tools: [
28
+ {
29
+ name: 'evaluate_payment',
30
+ description:
31
+ 'Evaluate whether a payment should be allowed, blocked, or escalated ' +
32
+ 'based on your Troxy policies. Call this before initiating any payment.',
33
+ inputSchema: {
34
+ type: 'object',
35
+ required: ['card_alias', 'merchant_name', 'amount'],
36
+ properties: {
37
+ card_alias: {
38
+ type: 'string',
39
+ description: 'Card alias to charge (e.g. "Personal", "Business")',
40
+ },
41
+ merchant_name: {
42
+ type: 'string',
43
+ description: 'Name of the merchant or service',
44
+ },
45
+ amount: {
46
+ type: 'number',
47
+ description: 'Payment amount',
48
+ },
49
+ agent: {
50
+ type: 'string',
51
+ description: 'Name of the agent making the payment (optional)',
52
+ },
53
+ merchant_category: {
54
+ type: 'string',
55
+ description: 'Merchant category, e.g. "software", "travel" (optional)',
56
+ },
57
+ currency: {
58
+ type: 'string',
59
+ description: 'Currency code, defaults to USD (optional)',
60
+ },
61
+ },
62
+ },
63
+ },
64
+ ],
65
+ }));
66
+
67
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
68
+ if (request.params.name !== 'evaluate_payment') {
69
+ throw new Error(`Unknown tool: ${request.params.name}`);
70
+ }
71
+
72
+ const args = request.params.arguments ?? {};
73
+ const result = await evaluatePayment(args, apiKey);
74
+
75
+ if (result.error) {
76
+ return {
77
+ content: [{ type: 'text', text: `Troxy error: ${result.error}` }],
78
+ isError: true,
79
+ };
80
+ }
81
+
82
+ const { decision, policy, audit_id } = result;
83
+ let text;
84
+
85
+ switch (decision) {
86
+ case 'ALLOW':
87
+ text = `✓ Payment approved.${policy ? ` Policy matched: "${policy}".` : ''} (audit: ${audit_id})`;
88
+ break;
89
+ case 'BLOCK':
90
+ text = `✗ Payment blocked by policy "${policy}". Do not proceed with this payment. (audit: ${audit_id})`;
91
+ break;
92
+ case 'ESCALATE':
93
+ text = `⏳ Payment requires human approval — a request has been sent to the account owner. Do not proceed until approved. (audit: ${audit_id})`;
94
+ break;
95
+ case 'NOTIFY':
96
+ text = `✓ Payment approved with notification. Policy matched: "${policy}". (audit: ${audit_id})`;
97
+ break;
98
+ default:
99
+ text = JSON.stringify(result);
100
+ }
101
+
102
+ return {
103
+ content: [{ type: 'text', text }],
104
+ isError: decision === 'BLOCK',
105
+ };
106
+ });
107
+
108
+ // Heartbeat: tell the dashboard this MCP server is active.
109
+ // Must be set up before server.connect() since stdio transport keeps the
110
+ // event loop running but connect() may not return in all environments.
111
+ const sendHeartbeat = () =>
112
+ api.mcpHeartbeat(apiKey)
113
+ .then(() => process.stderr.write('[troxy] heartbeat ok\n'))
114
+ .catch(err => process.stderr.write(`[troxy] heartbeat failed: ${err.message}\n`));
115
+ sendHeartbeat();
116
+ setInterval(sendHeartbeat, 60_000);
117
+
118
+ const transport = new StdioServerTransport();
119
+ await server.connect(transport);
120
+ }
@@ -0,0 +1,89 @@
1
+ import { api } from './api.js';
2
+ import { requireJwt } from './auth.js';
3
+ import { table } from './print.js';
4
+
5
+ export async function runPolicies([sub, ...args], flags) {
6
+ const jwt = requireJwt();
7
+
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;
26
+ }
27
+
28
+ case 'create': {
29
+ const name = flags.name;
30
+ const action = (flags.action || '').toUpperCase();
31
+ if (!name) { console.error(' --name is required\n'); process.exit(1); }
32
+ if (!action) { console.error(' --action is required\n'); process.exit(1); }
33
+ if (!['ALLOW','BLOCK','ESCALATE','NOTIFY'].includes(action)) {
34
+ console.error(' --action must be ALLOW, BLOCK, ESCALATE, or NOTIFY\n');
35
+ process.exit(1);
36
+ }
37
+
38
+ // Build a single condition if --field/--operator/--value are provided
39
+ const conditions = [];
40
+ if (flags.field) {
41
+ if (!flags.operator) { console.error(' --operator is required with --field\n'); process.exit(1); }
42
+ const cond = { field: flags.field, operator: flags.operator };
43
+ if (flags.value) cond.value = flags.value;
44
+ if (flags.value2) cond.value2 = flags.value2;
45
+ conditions.push(cond);
46
+ }
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);
57
+ console.log(`\n Policy "${policy.name}" created ✓ (priority: ${policy.priority})\n`);
58
+ break;
59
+ }
60
+
61
+ case 'delete': {
62
+ const name = flags.name;
63
+ if (!name) { console.error(' --name is required\n'); process.exit(1); }
64
+ const { policies = [] } = await api.listPolicies(jwt);
65
+ const policy = policies.find(p => p.name === name);
66
+ if (!policy) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
67
+ await api.deletePolicy(jwt, policy.id);
68
+ console.log(`\n Policy "${name}" deleted ✓\n`);
69
+ break;
70
+ }
71
+
72
+ case 'enable':
73
+ case 'disable': {
74
+ const name = flags.name;
75
+ if (!name) { console.error(' --name is required\n'); process.exit(1); }
76
+ const { policies = [] } = await api.listPolicies(jwt);
77
+ const policy = policies.find(p => p.name === name);
78
+ if (!policy) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
79
+ await api.updatePolicy(jwt, policy.id, { enabled: sub === 'enable' });
80
+ console.log(`\n Policy "${name}" ${sub}d ✓\n`);
81
+ break;
82
+ }
83
+
84
+ default:
85
+ console.error(` Unknown subcommand: ${sub}`);
86
+ console.error(' Usage: npx troxy policies [list|create|delete|enable|disable]\n');
87
+ process.exit(1);
88
+ }
89
+ }
package/src/print.js ADDED
@@ -0,0 +1,16 @@
1
+ /** Print a simple ASCII table. */
2
+ export function table(headers, rows) {
3
+ const cols = headers.length;
4
+ const widths = headers.map((h, i) =>
5
+ Math.max(String(h).length, ...rows.map(r => String(r[i] ?? '').length))
6
+ );
7
+
8
+ const line = () => ' ' + widths.map(w => '─'.repeat(w + 2)).join('┼') + '';
9
+ const row = (cells, pad = ' ') =>
10
+ ' ' + cells.map((c, i) => ` ${String(c ?? '').padEnd(widths[i], pad)} `).join('│');
11
+
12
+ console.log(row(headers));
13
+ console.log(' ' + widths.map(w => '─'.repeat(w + 2)).join('┼'));
14
+ rows.forEach(r => console.log(row(r)));
15
+ console.log();
16
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ // Point config at a temp dir so tests don't touch ~/.troxy
8
+ const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'troxy-test-'));
9
+ process.env.HOME = TMP;
10
+
11
+ const { loadConfig, saveConfig } = await import('../config.js');
12
+
13
+ describe('config', () => {
14
+ after(() => fs.rmSync(TMP, { recursive: true, force: true }));
15
+
16
+ it('returns null when no config exists', () => {
17
+ assert.equal(loadConfig(), null);
18
+ });
19
+
20
+ it('saves and loads config', () => {
21
+ saveConfig({ apiKey: 'txy-test123' });
22
+ const cfg = loadConfig();
23
+ assert.equal(cfg.apiKey, 'txy-test123');
24
+ });
25
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ describe('flag validation', () => {
5
+ it('rejects missing key', async () => {
6
+ // Dynamically import and test the validation logic
7
+ // We test the guard condition directly rather than calling runInit
8
+ // (which would make real network calls)
9
+ const key = undefined;
10
+ assert.ok(!key || !String(key).startsWith('txy-'));
11
+ });
12
+
13
+ it('rejects key without txy- prefix', () => {
14
+ const key = 'sk-notavalid';
15
+ assert.ok(!key.startsWith('txy-'));
16
+ });
17
+
18
+ it('accepts valid key format', () => {
19
+ const key = 'txy-abc123xyz';
20
+ assert.ok(key.startsWith('txy-'));
21
+ });
22
+ });