troxy-cli 1.7.4 → 1.8.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 +75 -9
- package/package.json +1 -1
- package/src/account.js +52 -0
- package/src/api.js +28 -3
- package/src/approvals.js +60 -0
- package/src/cards.js +107 -0
- package/src/chat-budget.js +73 -0
- package/src/config.js +5 -0
- package/src/init.js +21 -5
- package/src/mcps.js +111 -18
- package/src/policies.js +81 -23
- package/src/secrets.js +101 -0
- package/src/settings.js +86 -0
- package/src/uninstall.js +1 -0
package/bin/troxy.js
CHANGED
|
@@ -8,6 +8,12 @@ import { loadConfig } from '../src/config.js';
|
|
|
8
8
|
import { runPolicies } from '../src/policies.js';
|
|
9
9
|
import { runMcps } from '../src/mcps.js';
|
|
10
10
|
import { runActivity } from '../src/activity.js';
|
|
11
|
+
import { runCards } from '../src/cards.js';
|
|
12
|
+
import { runSettings } from '../src/settings.js';
|
|
13
|
+
import { runChatBudget } from '../src/chat-budget.js';
|
|
14
|
+
import { runApprovals } from '../src/approvals.js';
|
|
15
|
+
import { runAccount } from '../src/account.js';
|
|
16
|
+
import { runSecrets } from '../src/secrets.js';
|
|
11
17
|
import { api } from '../src/api.js';
|
|
12
18
|
import { table } from '../src/print.js';
|
|
13
19
|
|
|
@@ -250,6 +256,30 @@ switch (command) {
|
|
|
250
256
|
await runActivity(flags);
|
|
251
257
|
break;
|
|
252
258
|
|
|
259
|
+
case 'cards':
|
|
260
|
+
await runCards(positional, flags);
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
case 'settings':
|
|
264
|
+
await runSettings(positional, flags);
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case 'chat-budget':
|
|
268
|
+
await runChatBudget(positional, flags);
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case 'approvals':
|
|
272
|
+
await runApprovals(positional, flags);
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case 'account':
|
|
276
|
+
await runAccount(positional, flags);
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
case 'secrets':
|
|
280
|
+
await runSecrets(positional, flags);
|
|
281
|
+
break;
|
|
282
|
+
|
|
253
283
|
case 'insights': {
|
|
254
284
|
if (flags.help || flags.h) {
|
|
255
285
|
console.log(`
|
|
@@ -298,7 +328,10 @@ switch (command) {
|
|
|
298
328
|
if (!sub || sub === 'policies') { await runPolicies(['list'], flags); break; }
|
|
299
329
|
if (sub === 'mcps') { await runMcps(['list'], flags); break; }
|
|
300
330
|
if (sub === 'activity') { await runActivity(flags); break; }
|
|
301
|
-
|
|
331
|
+
if (sub === 'cards') { await runCards(['list'], flags); break; }
|
|
332
|
+
if (sub === 'approvals') { await runApprovals(['list'], flags); break; }
|
|
333
|
+
if (sub === 'secrets') { await runSecrets(['list'], flags); break; }
|
|
334
|
+
console.error(` Unknown resource: ${sub}. Try: policies, mcps, activity, cards, approvals, secrets\n`);
|
|
302
335
|
process.exit(1);
|
|
303
336
|
|
|
304
337
|
// ── Status ────────────────────────────────────────────────────
|
|
@@ -485,10 +518,15 @@ switch (command) {
|
|
|
485
518
|
Everything else requires: troxy login
|
|
486
519
|
|
|
487
520
|
MCP
|
|
488
|
-
troxy pause
|
|
489
|
-
troxy resume
|
|
490
|
-
troxy mcps list
|
|
491
|
-
troxy mcps rename --name "x"
|
|
521
|
+
troxy pause Pause this MCP (blocks all payments)
|
|
522
|
+
troxy resume Resume this MCP
|
|
523
|
+
troxy mcps list All MCPs in your account
|
|
524
|
+
troxy mcps rename --name "x" Rename this machine's MCP
|
|
525
|
+
troxy mcps create --name "x" Create a key for another agent/machine
|
|
526
|
+
troxy mcps revoke --mcp "x" Revoke an MCP's key
|
|
527
|
+
troxy mcps pause/resume --mcp "x" Pause/resume any MCP (not just this one)
|
|
528
|
+
troxy mcps budget --mcp "x" --currency USD --limit 500
|
|
529
|
+
troxy mcps set-default-action --mcp "x" --action BLOCK
|
|
492
530
|
|
|
493
531
|
Keys
|
|
494
532
|
troxy rotate-key Rotate MCP key (revokes old, saves new locally)
|
|
@@ -496,12 +534,40 @@ switch (command) {
|
|
|
496
534
|
Policies
|
|
497
535
|
troxy policies list
|
|
498
536
|
troxy policies describe --name "Block Amazon"
|
|
537
|
+
troxy policies create --describe "block Amazon purchases over $200" (AI builder)
|
|
499
538
|
troxy policies create --name "X" --action BLOCK --field amount --operator gte --value 500
|
|
500
|
-
troxy policies create --name "X" --action BLOCK --mcp "My Laptop" (scoped to
|
|
539
|
+
troxy policies create --name "X" --action BLOCK --mcp "My Laptop,Server" (scoped to specific MCPs)
|
|
540
|
+
troxy policies create --name "X" --action ESCALATE --chat (scoped to Troxy Chat only)
|
|
501
541
|
troxy policies set-priority --name "X" --priority 10
|
|
502
|
-
troxy policies
|
|
503
|
-
troxy policies
|
|
504
|
-
troxy policies delete
|
|
542
|
+
troxy policies pause --name "X"
|
|
543
|
+
troxy policies resume --name "X"
|
|
544
|
+
troxy policies delete --name "X"
|
|
545
|
+
|
|
546
|
+
Cards
|
|
547
|
+
troxy cards list
|
|
548
|
+
troxy cards create --name "Work" --last4 1234 --budget 2000
|
|
549
|
+
troxy cards update --name "Work" --budget 3000
|
|
550
|
+
troxy cards delete --name "Work"
|
|
551
|
+
|
|
552
|
+
Approvals (ESCALATE holds)
|
|
553
|
+
troxy approvals list
|
|
554
|
+
troxy approvals approve --id <id>
|
|
555
|
+
troxy approvals decline --id <id>
|
|
556
|
+
|
|
557
|
+
Settings
|
|
558
|
+
troxy settings show
|
|
559
|
+
troxy settings set --default-action BLOCK --approval-timeout 4
|
|
560
|
+
troxy chat-budget show
|
|
561
|
+
troxy chat-budget set --currency USD --limit 500
|
|
562
|
+
|
|
563
|
+
LLM keys (Troxy Chat)
|
|
564
|
+
troxy secrets list
|
|
565
|
+
troxy secrets set --name "OpenAI"
|
|
566
|
+
troxy secrets delete --name "OpenAI"
|
|
567
|
+
|
|
568
|
+
Account
|
|
569
|
+
troxy account clear-data Wipe activity/policies, keep account + keys
|
|
570
|
+
troxy account delete Permanently delete your account (irreversible)
|
|
505
571
|
|
|
506
572
|
Activity & insights
|
|
507
573
|
troxy activity [--limit 50] [--mine]
|
package/package.json
CHANGED
package/src/account.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { api } from './api.js';
|
|
3
|
+
import { requireJwt, clearSession } from './auth.js';
|
|
4
|
+
|
|
5
|
+
function prompt(question) {
|
|
6
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const HELP = {
|
|
11
|
+
delete: ` troxy account delete\n\n PERMANENTLY deletes your Troxy account: all activity, policies, cards,\n API keys, and the account itself. This cannot be undone. You'll be asked\n to type your account email to confirm.\n`,
|
|
12
|
+
'clear-data': ` troxy account clear-data\n\n Deletes all activity, policies, and pending approvals. Keeps your account\n and API keys intact, so your MCP connections keep working.\n`,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function runAccount([sub, ...args], flags) {
|
|
16
|
+
if (flags.help || flags.h) {
|
|
17
|
+
console.log('\n' + (HELP[sub] || ` troxy account <subcommand>\n\n Subcommands:\n delete Permanently delete your account and all data\n clear-data Wipe activity/policies but keep the account and API keys\n\n Run 'troxy account <subcommand> --help' for subcommand help.\n`));
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const jwt = requireJwt();
|
|
22
|
+
|
|
23
|
+
switch (sub) {
|
|
24
|
+
case 'delete': {
|
|
25
|
+
const s = await api.getSettings(jwt);
|
|
26
|
+
console.log(`\n This PERMANENTLY deletes your Troxy account (${s.email}):`);
|
|
27
|
+
console.log(' all activity, policies, cards, and API keys. This cannot be undone.\n');
|
|
28
|
+
const typed = await prompt(` Type your account email to confirm: `);
|
|
29
|
+
if (typed.trim().toLowerCase() !== s.email.toLowerCase()) {
|
|
30
|
+
console.log('\n Email did not match. Cancelled.\n');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
await api.deleteAccount(jwt);
|
|
34
|
+
clearSession();
|
|
35
|
+
console.log('\n Account deleted. Run troxy uninstall to remove Troxy from this machine.\n');
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case 'clear-data': {
|
|
40
|
+
const answer = await prompt(' This deletes all activity, policies, and pending approvals (keeps your account and API keys). Continue? (y/N): ');
|
|
41
|
+
if (answer.toLowerCase() !== 'y') { console.log('\n Cancelled.\n'); process.exit(0); }
|
|
42
|
+
await api.clearData(jwt);
|
|
43
|
+
console.log('\n Activity, policies, and pending approvals cleared ✓\n');
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
default:
|
|
48
|
+
console.error(` Unknown subcommand: ${sub || '(none)'}`);
|
|
49
|
+
console.error(' Usage: troxy account [delete|clear-data]\n');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/api.js
CHANGED
|
@@ -51,6 +51,29 @@ export const api = {
|
|
|
51
51
|
listTokens: (jwt) => request('GET', '/tokens', { jwt }),
|
|
52
52
|
createToken: (jwt, b) => request('POST', '/tokens', { jwt, body: b }),
|
|
53
53
|
revokeToken: (jwt, id) => request('DELETE', `/tokens/${id}`, { jwt }),
|
|
54
|
+
updateTokenBudget: (jwt, id, b) => request('PATCH', `/tokens/${id}/budget`, { jwt, body: b }),
|
|
55
|
+
updateTokenDefaultAction: (jwt, id, action) => request('PATCH', `/tokens/${id}`, { jwt, body: { default_action: action } }),
|
|
56
|
+
|
|
57
|
+
// Account settings
|
|
58
|
+
getSettings: (jwt) => request('GET', '/dashboard/settings', { jwt }),
|
|
59
|
+
updateSettings: (jwt, b) => request('PATCH', '/dashboard/settings', { jwt, body: b }),
|
|
60
|
+
updateChatBudget: (jwt, b) => request('PATCH', '/dashboard/chat-budget', { jwt, body: b }),
|
|
61
|
+
|
|
62
|
+
// Approvals
|
|
63
|
+
listApprovals: (jwt) => request('GET', '/dashboard/approvals', { jwt }),
|
|
64
|
+
resolveApproval: (token, resolved) => request('POST', `/approvals/${encodeURIComponent(token)}/${resolved ? 'approve' : 'reject'}`, {}),
|
|
65
|
+
|
|
66
|
+
// Account
|
|
67
|
+
deleteAccount: (jwt) => request('DELETE', '/account', { jwt }),
|
|
68
|
+
clearData: (jwt) => request('DELETE', '/account/data', { jwt }),
|
|
69
|
+
|
|
70
|
+
// Secrets (LLM provider keys)
|
|
71
|
+
listSecrets: (jwt) => request('GET', '/secrets', { jwt }),
|
|
72
|
+
saveSecret: (jwt, b) => request('POST', '/secrets', { jwt, body: b }),
|
|
73
|
+
deleteSecret: (jwt, id) => request('DELETE', `/secrets/${id}`, { jwt }),
|
|
74
|
+
|
|
75
|
+
// AI policy builder
|
|
76
|
+
aiPolicy: (jwt, b) => request('POST', '/dashboard/ai-policy', { jwt, body: b }),
|
|
54
77
|
|
|
55
78
|
// Evaluate + confirm (agent API key)
|
|
56
79
|
evaluate: (body, apiKey) => request('POST', '/evaluate', { apiKey, body }),
|
|
@@ -69,9 +92,11 @@ export const api = {
|
|
|
69
92
|
renameToken: (jwt, id, name) => request('PATCH', `/tokens/${id}/name`, { jwt, body: { name } }),
|
|
70
93
|
|
|
71
94
|
// Agent read-only API (JWT session auth — run: troxy login)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
// tokenPrefix (this machine's saved key, first 11 chars) lets the backend
|
|
96
|
+
// resolve which MCP is "me" for the applies_to_me / is_me fields.
|
|
97
|
+
agentStatus: (jwt, tokenPrefix) => request('GET', `/agent/status${tokenPrefix ? `?token_prefix=${encodeURIComponent(tokenPrefix)}` : ''}`, { jwt }),
|
|
98
|
+
agentPolicies: (jwt, tokenPrefix) => request('GET', `/agent/policies${tokenPrefix ? `?token_prefix=${encodeURIComponent(tokenPrefix)}` : ''}`, { jwt }),
|
|
99
|
+
agentMcps: (jwt, tokenPrefix) => request('GET', `/agent/mcps${tokenPrefix ? `?token_prefix=${encodeURIComponent(tokenPrefix)}` : ''}`, { jwt }),
|
|
75
100
|
agentCards: (jwt) => request('GET', '/agent/cards', { jwt }),
|
|
76
101
|
agentActivity: (jwt, limit, mine, tokenPrefix) => request('GET', `/agent/activity?limit=${limit || 20}${mine ? `&mine=true&token_prefix=${encodeURIComponent(tokenPrefix || '')}` : ''}`, { jwt }),
|
|
77
102
|
agentInsights: (jwt, period) => request('GET', `/agent/insights?period=${period || 30}`, { jwt }),
|
package/src/approvals.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
|
+
const HELP = {
|
|
6
|
+
list: ` troxy approvals list\n\n Lists pending ESCALATE holds waiting for your approval.\n`,
|
|
7
|
+
approve: ` troxy approvals approve --id <id>\n\n Approves a pending transaction. Use the ID shown by 'troxy approvals list'\n (an unambiguous prefix is enough).\n`,
|
|
8
|
+
decline: ` troxy approvals decline --id <id>\n\n Declines a pending transaction. Use the ID shown by 'troxy approvals list'\n (an unambiguous prefix is enough).\n`,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function _find(jwt, idPrefix) {
|
|
12
|
+
if (!idPrefix) { console.error(' --id is required\n'); process.exit(1); }
|
|
13
|
+
const { items: approvals = [] } = await api.listApprovals(jwt);
|
|
14
|
+
const matches = approvals.filter(a => a.id.startsWith(idPrefix));
|
|
15
|
+
if (matches.length === 0) { console.error(` No pending approval starting with "${idPrefix}". Run: troxy approvals list\n`); process.exit(1); }
|
|
16
|
+
if (matches.length > 1) { console.error(` "${idPrefix}" matches ${matches.length} pending approvals — use a longer prefix\n`); process.exit(1); }
|
|
17
|
+
return matches[0];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runApprovals([sub, ...args], flags) {
|
|
21
|
+
if (flags.help || flags.h) {
|
|
22
|
+
console.log('\n' + (HELP[sub] || ` troxy approvals <subcommand> [options]\n\n Subcommands:\n list List pending approvals\n approve Approve a pending transaction\n decline Decline a pending transaction\n\n Run 'troxy approvals <subcommand> --help' for subcommand help.\n`));
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const jwt = requireJwt();
|
|
27
|
+
|
|
28
|
+
switch (sub || 'list') {
|
|
29
|
+
case 'list': {
|
|
30
|
+
const { items: approvals = [] } = await api.listApprovals(jwt);
|
|
31
|
+
if (!approvals.length) { console.log('\n No pending approvals.\n'); return; }
|
|
32
|
+
console.log();
|
|
33
|
+
table(
|
|
34
|
+
['ID', 'Merchant', 'Amount', 'Agent', 'Card', 'Expires'],
|
|
35
|
+
approvals.map(a => [
|
|
36
|
+
a.id.slice(0, 8),
|
|
37
|
+
a.merchant_name,
|
|
38
|
+
`$${Number(a.amount).toFixed(2)} ${a.currency}`,
|
|
39
|
+
a.agent_name,
|
|
40
|
+
a.card_name,
|
|
41
|
+
new Date(a.expires_at).toLocaleString(),
|
|
42
|
+
]),
|
|
43
|
+
);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case 'approve':
|
|
48
|
+
case 'decline': {
|
|
49
|
+
const approval = await _find(jwt, flags.id);
|
|
50
|
+
await api.resolveApproval(approval.approval_token, sub === 'approve');
|
|
51
|
+
console.log(`\n ${approval.merchant_name} ($${Number(approval.amount).toFixed(2)}) ${sub === 'approve' ? 'approved' : 'declined'} ✓\n`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
default:
|
|
56
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
57
|
+
console.error(' Usage: troxy approvals [list|approve|decline]\n');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/cards.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireJwt } from './auth.js';
|
|
3
|
+
import { table } from './print.js';
|
|
4
|
+
|
|
5
|
+
const HELP = {
|
|
6
|
+
list: ` troxy cards list\n\n Lists all cards (budget/spend aliases) on your account.\n`,
|
|
7
|
+
create: ` troxy cards create --name <name> --last4 <4 digits> [options]\n\n Creates a new card alias. Login required.\n\n Required:\n --name Card alias name (e.g. "Work")\n --last4 Last 4 digits of the card\n\n Optional:\n --type Card type label (default: virtual)\n --budget <n> Monthly budget amount\n --reset-day <n> Day of month the budget resets (default: 1)\n --default-action ALLOW, BLOCK, ESCALATE, or NOTIFY (default: ALLOW)\n --notes <text> Free text notes\n\n Example:\n troxy cards create --name "Work" --last4 1234 --budget 2000\n`,
|
|
8
|
+
update: ` troxy cards update --name <name> [options]\n\n Updates an existing card. Login required.\n\n Options:\n --new-name <name> Rename the card\n --budget <n> Set the monthly budget\n --clear-budget Remove the monthly budget\n --reset-day <n> Day of month the budget resets\n --status <status> active or paused\n --default-action ALLOW, BLOCK, ESCALATE, or NOTIFY\n --notes <text> Free text notes\n\n Example:\n troxy cards update --name "Work" --budget 3000\n`,
|
|
9
|
+
delete: ` troxy cards delete --name <name>\n\n Permanently deletes a card. Login required.\n`,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function runCards([sub, ...args], flags) {
|
|
13
|
+
if (flags.help || flags.h) {
|
|
14
|
+
console.log('\n' + (HELP[sub] || ` troxy cards <subcommand> [options]\n\n Subcommands:\n list List all cards\n create Create a new card\n update Update an existing card\n delete Delete a card\n\n Run 'troxy cards <subcommand> --help' for subcommand help.\n`));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const jwt = requireJwt();
|
|
19
|
+
|
|
20
|
+
switch (sub || 'list') {
|
|
21
|
+
case 'list': {
|
|
22
|
+
const { cards = [] } = await api.listCards(jwt);
|
|
23
|
+
if (!cards.length) { console.log('\n No cards yet.\n'); return; }
|
|
24
|
+
console.log();
|
|
25
|
+
table(
|
|
26
|
+
['Name', 'Last 4', 'Type', 'Budget', 'Used', 'Reset day', 'Status', 'Default action'],
|
|
27
|
+
cards.map(c => [
|
|
28
|
+
c.name,
|
|
29
|
+
c.last4,
|
|
30
|
+
c.type,
|
|
31
|
+
c.budget != null ? `$${Number(c.budget).toFixed(2)}` : '—',
|
|
32
|
+
c.budget != null ? `$${Number(c.budget_used).toFixed(2)}` : '—',
|
|
33
|
+
c.reset_day,
|
|
34
|
+
c.status,
|
|
35
|
+
c.default_action,
|
|
36
|
+
]),
|
|
37
|
+
);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
case 'create': {
|
|
42
|
+
const name = flags.name;
|
|
43
|
+
const last4 = String(flags.last4 ?? '');
|
|
44
|
+
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
45
|
+
if (!/^\d{4}$/.test(last4)) { console.error(' --last4 must be exactly 4 digits\n'); process.exit(1); }
|
|
46
|
+
const action = flags['default-action'] ? flags['default-action'].toUpperCase() : undefined;
|
|
47
|
+
if (action && !['ALLOW', 'BLOCK', 'ESCALATE', 'NOTIFY'].includes(action)) {
|
|
48
|
+
console.error(' --default-action must be ALLOW, BLOCK, ESCALATE, or NOTIFY\n'); process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const body = {
|
|
51
|
+
name, last4,
|
|
52
|
+
type: flags.type || undefined,
|
|
53
|
+
budget: flags.budget != null ? parseFloat(flags.budget) : undefined,
|
|
54
|
+
reset_day: flags['reset-day'] != null ? parseInt(flags['reset-day'], 10) : undefined,
|
|
55
|
+
default_action: action,
|
|
56
|
+
notes: flags.notes || undefined,
|
|
57
|
+
};
|
|
58
|
+
const card = await api.createCard(jwt, body);
|
|
59
|
+
console.log(`\n Card "${card.name}" created ✓ (•••• ${card.last4})\n`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case 'update': {
|
|
64
|
+
const name = flags.name;
|
|
65
|
+
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
66
|
+
const { cards = [] } = await api.listCards(jwt);
|
|
67
|
+
const card = cards.find(c => c.name.toLowerCase() === name.toLowerCase());
|
|
68
|
+
if (!card) { console.error(` Card "${name}" not found\n`); process.exit(1); }
|
|
69
|
+
|
|
70
|
+
const body = {};
|
|
71
|
+
if (flags['new-name']) body.name = flags['new-name'];
|
|
72
|
+
if (flags['clear-budget']) body.budget = null;
|
|
73
|
+
else if (flags.budget != null) body.budget = parseFloat(flags.budget);
|
|
74
|
+
if (flags['reset-day'] != null) body.reset_day = parseInt(flags['reset-day'], 10);
|
|
75
|
+
if (flags.status) body.status = flags.status;
|
|
76
|
+
if (flags['default-action']) {
|
|
77
|
+
const action = flags['default-action'].toUpperCase();
|
|
78
|
+
if (!['ALLOW', 'BLOCK', 'ESCALATE', 'NOTIFY'].includes(action)) {
|
|
79
|
+
console.error(' --default-action must be ALLOW, BLOCK, ESCALATE, or NOTIFY\n'); process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
body.default_action = action;
|
|
82
|
+
}
|
|
83
|
+
if (flags.notes != null) body.notes = flags.notes;
|
|
84
|
+
if (Object.keys(body).length === 0) { console.error(' Nothing to update — pass at least one option\n'); process.exit(1); }
|
|
85
|
+
|
|
86
|
+
await api.updateCard(jwt, card.id, body);
|
|
87
|
+
console.log(`\n Card "${name}" updated ✓\n`);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'delete': {
|
|
92
|
+
const name = flags.name;
|
|
93
|
+
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
94
|
+
const { cards = [] } = await api.listCards(jwt);
|
|
95
|
+
const card = cards.find(c => c.name.toLowerCase() === name.toLowerCase());
|
|
96
|
+
if (!card) { console.error(` Card "${name}" not found\n`); process.exit(1); }
|
|
97
|
+
await api.deleteCard(jwt, card.id);
|
|
98
|
+
console.log(`\n Card "${name}" deleted ✓\n`);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
default:
|
|
103
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
104
|
+
console.error(' Usage: troxy cards [list|create|update|delete]\n');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireJwt } from './auth.js';
|
|
3
|
+
import { table } from './print.js';
|
|
4
|
+
|
|
5
|
+
const VALID_CURRENCIES = ['USD', 'ILS', 'EUR'];
|
|
6
|
+
const VALID_ACTIONS = ['block', 'allow', 'notify', 'escalate'];
|
|
7
|
+
|
|
8
|
+
const HELP = {
|
|
9
|
+
show: ` troxy chat-budget show\n\n Shows Troxy Chat's monthly budget limits and what happens when they're hit.\n`,
|
|
10
|
+
set: ` troxy chat-budget set [options]\n\n Updates Troxy Chat's monthly budget. Login required.\n\n --currency <cur> USD, ILS, or EUR (default: USD)\n --limit <n> Monthly limit for that currency\n --clear Remove the limit for that currency\n --action <action> block, allow, notify, or escalate — what happens once the limit is hit\n\n Examples:\n troxy chat-budget set --currency USD --limit 500\n troxy chat-budget set --action notify\n troxy chat-budget set --currency EUR --clear\n`,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function runChatBudget([sub, ...args], flags) {
|
|
14
|
+
if (flags.help || flags.h) {
|
|
15
|
+
console.log('\n' + (HELP[sub] || ` troxy chat-budget <subcommand> [options]\n\n Subcommands:\n show Show Troxy Chat's budget\n set Update Troxy Chat's budget\n\n Run 'troxy chat-budget <subcommand> --help' for subcommand help.\n`));
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const jwt = requireJwt();
|
|
20
|
+
|
|
21
|
+
switch (sub || 'show') {
|
|
22
|
+
case 'show': {
|
|
23
|
+
const s = await api.getSettings(jwt);
|
|
24
|
+
console.log(`\n Action when budget is hit: ${s.chat_budget_action}\n`);
|
|
25
|
+
if (!s.chat_budget_limits.length) {
|
|
26
|
+
console.log(' No monthly limits set.\n');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
table(
|
|
30
|
+
['Currency', 'Limit', 'Used', 'Remaining'],
|
|
31
|
+
s.chat_budget_limits.map(b => [b.currency, b.limit, b.used, (b.limit - b.used).toFixed(2)]),
|
|
32
|
+
);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case 'set': {
|
|
37
|
+
const body = {};
|
|
38
|
+
|
|
39
|
+
if (flags.action) {
|
|
40
|
+
const action = flags.action.toLowerCase();
|
|
41
|
+
if (!VALID_ACTIONS.includes(action)) {
|
|
42
|
+
console.error(` --action must be one of: ${VALID_ACTIONS.join(', ')}\n`); process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
body.budget_action = action;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (flags.limit != null || flags.clear || flags.currency) {
|
|
48
|
+
const currency = (flags.currency || 'USD').toUpperCase();
|
|
49
|
+
if (!VALID_CURRENCIES.includes(currency)) {
|
|
50
|
+
console.error(` --currency must be one of: ${VALID_CURRENCIES.join(', ')}\n`); process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
body.currency = currency;
|
|
53
|
+
if (flags.clear) body.limit = null;
|
|
54
|
+
else if (flags.limit != null) body.limit = parseFloat(flags.limit);
|
|
55
|
+
else { console.error(' --limit is required (or pass --clear to remove the limit)\n'); process.exit(1); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Object.keys(body).length === 0) {
|
|
59
|
+
console.error(' Nothing to update — pass at least one option. Run troxy chat-budget set --help\n');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await api.updateChatBudget(jwt, body);
|
|
64
|
+
console.log('\n Chat budget updated ✓\n');
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
default:
|
|
69
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
70
|
+
console.error(' Usage: troxy chat-budget [show|set]\n');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/config.js
CHANGED
|
@@ -13,6 +13,11 @@ export function loadConfig() {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** First 11 chars of this machine's saved API key — used to identify "me" to the API. */
|
|
17
|
+
export function localTokenPrefix() {
|
|
18
|
+
return (loadConfig()?.apiKey || '').substring(0, 11) || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export function saveConfig(data) {
|
|
17
22
|
// Owner-only permissions on dir + file — matches what AWS CLI does with
|
|
18
23
|
// ~/.aws/credentials. Stops other users on the same machine from reading
|
package/src/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import readline from 'readline';
|
|
5
|
-
import { execSync }
|
|
5
|
+
import { execSync, execFileSync } from 'child_process';
|
|
6
6
|
import { saveConfig } from './config.js';
|
|
7
7
|
import { evaluatePayment } from './api.js';
|
|
8
8
|
|
|
@@ -138,7 +138,9 @@ export async function runInit({ key } = {}) {
|
|
|
138
138
|
if (hasOpenClaw) {
|
|
139
139
|
try {
|
|
140
140
|
const entry = JSON.stringify({ command: 'npx', args: ['troxy-cli', 'mcp'], env: { TROXY_API_KEY: key } });
|
|
141
|
-
|
|
141
|
+
// execFileSync passes args directly to the process (no shell), so the
|
|
142
|
+
// key can't break out of a quoted string and inject shell commands.
|
|
143
|
+
execFileSync('openclaw', ['mcp', 'set', 'troxy', entry], { stdio: 'ignore' });
|
|
142
144
|
console.log(` • OpenClaw ✓`);
|
|
143
145
|
} catch (err) {
|
|
144
146
|
console.log(` • OpenClaw ✗ (${err.message})`);
|
|
@@ -174,6 +176,17 @@ function installService(apiKey, agentName) {
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
if (platform === 'linux') {
|
|
179
|
+
// The API key lives in a separate root-owned, 600-permission file loaded via
|
|
180
|
+
// EnvironmentFile= — unit files under /etc/systemd/system are world-readable
|
|
181
|
+
// (mode 644), so putting the key directly in `Environment=` there would leak
|
|
182
|
+
// it to every local user on the machine.
|
|
183
|
+
const envFilePath = '/etc/troxy-mcp.env';
|
|
184
|
+
const envFile = `TROXY_API_KEY=${apiKey}\nTROXY_AGENT_NAME="${agentName}"\n`;
|
|
185
|
+
fs.writeFileSync('/tmp/troxy-mcp.env', envFile, { mode: 0o600 });
|
|
186
|
+
execSync('sudo mv /tmp/troxy-mcp.env ' + envFilePath);
|
|
187
|
+
execSync(`sudo chown root:root ${envFilePath}`);
|
|
188
|
+
execSync(`sudo chmod 600 ${envFilePath}`);
|
|
189
|
+
|
|
177
190
|
const unit = `[Unit]
|
|
178
191
|
Description=Troxy MCP Server
|
|
179
192
|
After=network.target
|
|
@@ -183,8 +196,7 @@ ExecStart=${troxy} daemon
|
|
|
183
196
|
Restart=always
|
|
184
197
|
RestartSec=10
|
|
185
198
|
User=${os.userInfo().username}
|
|
186
|
-
|
|
187
|
-
Environment=TROXY_AGENT_NAME="${agentName}"
|
|
199
|
+
EnvironmentFile=${envFilePath}
|
|
188
200
|
|
|
189
201
|
[Install]
|
|
190
202
|
WantedBy=multi-user.target
|
|
@@ -225,7 +237,11 @@ WantedBy=multi-user.target
|
|
|
225
237
|
`;
|
|
226
238
|
const plistPath = path.join(os.homedir(), 'Library/LaunchAgents/ai.troxy.mcp.plist');
|
|
227
239
|
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
228
|
-
|
|
240
|
+
// The plist embeds the API key in plaintext (launchd has no EnvironmentFile
|
|
241
|
+
// equivalent) — default file perms are world-readable, so lock it to the
|
|
242
|
+
// owner only. launchd runs as this same user and can still read it.
|
|
243
|
+
fs.writeFileSync(plistPath, plist, { mode: 0o600 });
|
|
244
|
+
fs.chmodSync(plistPath, 0o600);
|
|
229
245
|
try { execSync(`launchctl unload ${plistPath} 2>/dev/null`); } catch {}
|
|
230
246
|
execSync(`launchctl load ${plistPath}`);
|
|
231
247
|
|
package/src/mcps.js
CHANGED
|
@@ -1,23 +1,50 @@
|
|
|
1
|
-
import { api }
|
|
2
|
-
import { loadConfig, saveConfig }
|
|
3
|
-
import { requireJwt }
|
|
4
|
-
import { table }
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { loadConfig, saveConfig, localTokenPrefix } from './config.js';
|
|
3
|
+
import { requireJwt } from './auth.js';
|
|
4
|
+
import { table } from './print.js';
|
|
5
5
|
|
|
6
6
|
const HELP = {
|
|
7
|
-
list:
|
|
8
|
-
rename:
|
|
7
|
+
list: ` troxy mcps list\n\n Lists all MCP connections on your account — name, prefix, status, last seen,\n policies assigned, default action, budgets, and which one is this machine.\n`,
|
|
8
|
+
rename: ` troxy mcps rename --name <new-name> [--mcp <target>]\n\n Renames an MCP. Without --mcp, renames this machine's MCP.\n\n Options:\n --name New name\n --mcp Name/prefix of the MCP to rename (default: this machine)\n\n Example:\n troxy mcps rename --name "My Laptop"\n troxy mcps rename --mcp "old-name" --name "Staging Server"\n`,
|
|
9
|
+
create: ` troxy mcps create --name <name>\n\n Creates a new API key for another agent/machine. The key is shown once —\n save it, then run 'troxy init --key <key>' on that machine.\n\n Options:\n --name Label for the new key\n`,
|
|
10
|
+
revoke: ` troxy mcps revoke --mcp <name>\n\n Permanently revokes an MCP's API key. That machine will stop being able\n to evaluate payments until reconnected with a new key.\n`,
|
|
11
|
+
pause: ` troxy pause / troxy mcps pause [--mcp <name>]\n\n Pauses an MCP (blocks all its payment evaluations). Without --mcp, pauses\n this machine's MCP — same as running 'troxy pause'.\n`,
|
|
12
|
+
resume: ` troxy resume / troxy mcps resume [--mcp <name>]\n\n Resumes a paused MCP. Without --mcp, resumes this machine's MCP.\n`,
|
|
13
|
+
budget: ` troxy mcps budget --mcp <name> [options]\n\n Sets or clears a monthly spend limit for a specific MCP.\n\n Options:\n --mcp <name> MCP to configure (required)\n --currency <cur> USD, ILS, or EUR (default: USD)\n --limit <n> Monthly limit\n --clear Remove the limit for that currency\n --action <action> block, escalate, notify, or allow — what happens once hit\n --reset-usage Reset this cycle's usage back to 0\n\n Example:\n troxy mcps budget --mcp "My Laptop" --currency USD --limit 500\n`,
|
|
14
|
+
'set-default-action': ` troxy mcps set-default-action --mcp <name> --action <action>\n\n Sets the fallback decision for an MCP when no policy matches.\n\n Options:\n --mcp <name> MCP to configure (required)\n --action <action> ALLOW, BLOCK, ESCALATE, or NOTIFY\n`,
|
|
9
15
|
};
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
async function _findToken(jwt, needle) {
|
|
18
|
+
const { tokens = [] } = await api.listTokens(jwt);
|
|
19
|
+
const n = needle.toLowerCase();
|
|
20
|
+
const match = tokens.find(t =>
|
|
21
|
+
(t.name && t.name.toLowerCase() === n) ||
|
|
22
|
+
(t.agent_name && t.agent_name.toLowerCase() === n) ||
|
|
23
|
+
(t.prefix && t.prefix.toLowerCase().startsWith(n))
|
|
24
|
+
);
|
|
25
|
+
if (!match) { console.error(`\n MCP "${needle}" not found. Run: troxy mcps list\n`); process.exit(1); }
|
|
26
|
+
return match;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function _localToken(jwt) {
|
|
30
|
+
const prefix = localTokenPrefix();
|
|
31
|
+
if (!prefix) { console.error('\n No API key found. Run: troxy init --key txy-...\n'); process.exit(1); }
|
|
32
|
+
const { tokens = [] } = await api.listTokens(jwt);
|
|
33
|
+
const tok = tokens.find(t => t.prefix === prefix);
|
|
34
|
+
if (!tok) { console.error('\n Could not find this machine\'s MCP. Run: troxy init --key txy-...\n'); process.exit(1); }
|
|
35
|
+
return tok;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runMcps([sub, ...args], flags) {
|
|
12
39
|
if (flags.help || flags.h) {
|
|
13
|
-
console.log('\n' + (HELP[sub] || ` troxy mcps <subcommand> [options]\n\n Subcommands:\n list
|
|
40
|
+
console.log('\n' + (HELP[sub] || ` troxy mcps <subcommand> [options]\n\n Subcommands:\n list List all MCP connections\n rename Rename an MCP\n create Create a new API key for another agent\n revoke Revoke an MCP's key\n pause / resume Pause or resume an MCP\n budget Set/clear a per-MCP monthly budget\n set-default-action Set the fallback decision for an MCP\n\n Run 'troxy mcps <subcommand> --help' for subcommand help.\n`));
|
|
14
41
|
process.exit(0);
|
|
15
42
|
}
|
|
16
43
|
|
|
17
44
|
switch (sub || 'list') {
|
|
18
45
|
case 'list': {
|
|
19
46
|
const jwt = requireJwt();
|
|
20
|
-
const data = await api.agentMcps(jwt);
|
|
47
|
+
const data = await api.agentMcps(jwt, localTokenPrefix());
|
|
21
48
|
const mcps = data?.mcps || [];
|
|
22
49
|
if (!mcps.length) { console.log('\n No MCP connections yet.\n'); return; }
|
|
23
50
|
console.log();
|
|
@@ -38,25 +65,91 @@ export async function runMcps([sub], flags) {
|
|
|
38
65
|
|
|
39
66
|
case 'rename': {
|
|
40
67
|
const name = flags.name;
|
|
41
|
-
if (!name) { console.error('\n Usage: troxy mcps rename --name "new-name"\n'); process.exit(1); }
|
|
68
|
+
if (!name) { console.error('\n Usage: troxy mcps rename --name "new-name" [--mcp <target>]\n'); process.exit(1); }
|
|
42
69
|
const jwt = requireJwt();
|
|
43
|
-
const config = loadConfig();
|
|
44
|
-
const prefix = (config?.apiKey || '').substring(0, 11);
|
|
45
|
-
if (!prefix) { console.error('\n No API key found. Run: troxy init --key txy-...\n'); process.exit(1); }
|
|
46
70
|
process.stdout.write(`\n Renaming MCP to "${name}"... `);
|
|
47
|
-
const
|
|
48
|
-
const tok = tokens.find(t => t.prefix === prefix);
|
|
49
|
-
if (!tok) { console.error('\nCould not find this machine\'s MCP. Run: troxy init --key txy-...\n'); process.exit(1); }
|
|
71
|
+
const tok = flags.mcp ? await _findToken(jwt, flags.mcp) : await _localToken(jwt);
|
|
50
72
|
await api.renameToken(jwt, tok.id, name);
|
|
51
|
-
|
|
73
|
+
if (!flags.mcp) {
|
|
74
|
+
const config = loadConfig();
|
|
75
|
+
saveConfig({ ...config, agentName: name });
|
|
76
|
+
}
|
|
52
77
|
console.log('✓');
|
|
53
78
|
console.log(' Dashboard and future heartbeats will use the new name.\n');
|
|
54
79
|
break;
|
|
55
80
|
}
|
|
56
81
|
|
|
82
|
+
case 'create': {
|
|
83
|
+
const name = flags.name;
|
|
84
|
+
if (!name) { console.error('\n --name is required\n'); process.exit(1); }
|
|
85
|
+
const jwt = requireJwt();
|
|
86
|
+
const result = await api.createToken(jwt, { name });
|
|
87
|
+
console.log(`\n API key created for "${name}":\n`);
|
|
88
|
+
console.log(` ${result.key}\n`);
|
|
89
|
+
console.log(' Shown once — save it now. Run "troxy init --key <key>" on that machine to connect it.\n');
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'revoke': {
|
|
94
|
+
if (!flags.mcp) { console.error('\n --mcp is required\n'); process.exit(1); }
|
|
95
|
+
const jwt = requireJwt();
|
|
96
|
+
const tok = await _findToken(jwt, flags.mcp);
|
|
97
|
+
await api.revokeToken(jwt, tok.id);
|
|
98
|
+
console.log(`\n MCP "${tok.name || tok.prefix}" revoked ✓\n`);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'pause':
|
|
103
|
+
case 'resume': {
|
|
104
|
+
const jwt = requireJwt();
|
|
105
|
+
const tok = flags.mcp ? await _findToken(jwt, flags.mcp) : await _localToken(jwt);
|
|
106
|
+
if (sub === 'pause') await api.pauseToken(jwt, tok.id);
|
|
107
|
+
else await api.resumeToken(jwt, tok.id);
|
|
108
|
+
console.log(`\n MCP "${tok.name || tok.prefix}" ${sub}d ✓\n`);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'budget': {
|
|
113
|
+
if (!flags.mcp) { console.error('\n --mcp is required\n'); process.exit(1); }
|
|
114
|
+
const jwt = requireJwt();
|
|
115
|
+
const tok = await _findToken(jwt, flags.mcp);
|
|
116
|
+
const body = {};
|
|
117
|
+
if (flags.action) {
|
|
118
|
+
const action = flags.action.toLowerCase();
|
|
119
|
+
if (!['block', 'escalate', 'notify', 'allow'].includes(action)) {
|
|
120
|
+
console.error(' --action must be block, escalate, notify, or allow\n'); process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
body.budget_action = action;
|
|
123
|
+
}
|
|
124
|
+
if (flags.limit != null || flags.clear || flags.currency) {
|
|
125
|
+
body.currency = (flags.currency || 'USD').toUpperCase();
|
|
126
|
+
if (flags.clear) body.limit = null;
|
|
127
|
+
else if (flags.limit != null) body.limit = parseFloat(flags.limit);
|
|
128
|
+
else { console.error(' --limit is required (or pass --clear)\n'); process.exit(1); }
|
|
129
|
+
}
|
|
130
|
+
if (flags['reset-usage']) body.reset_usage = true;
|
|
131
|
+
if (Object.keys(body).length === 0) { console.error(' Nothing to update — pass at least one option\n'); process.exit(1); }
|
|
132
|
+
await api.updateTokenBudget(jwt, tok.id, body);
|
|
133
|
+
console.log(`\n Budget updated for "${tok.name || tok.prefix}" ✓\n`);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'set-default-action': {
|
|
138
|
+
if (!flags.mcp) { console.error('\n --mcp is required\n'); process.exit(1); }
|
|
139
|
+
const action = (flags.action || '').toUpperCase();
|
|
140
|
+
if (!['ALLOW', 'BLOCK', 'ESCALATE', 'NOTIFY'].includes(action)) {
|
|
141
|
+
console.error(' --action must be ALLOW, BLOCK, ESCALATE, or NOTIFY\n'); process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const jwt = requireJwt();
|
|
144
|
+
const tok = await _findToken(jwt, flags.mcp);
|
|
145
|
+
await api.updateTokenDefaultAction(jwt, tok.id, action);
|
|
146
|
+
console.log(`\n Default action for "${tok.name || tok.prefix}" set to ${action} ✓\n`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
|
|
57
150
|
default:
|
|
58
151
|
console.error(` Unknown subcommand: ${sub}`);
|
|
59
|
-
console.error(' Usage: troxy mcps [list|rename]\n');
|
|
152
|
+
console.error(' Usage: troxy mcps [list|rename|create|revoke|pause|resume|budget|set-default-action]\n');
|
|
60
153
|
process.exit(1);
|
|
61
154
|
}
|
|
62
155
|
}
|
package/src/policies.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
1
2
|
import { api } from './api.js';
|
|
2
3
|
import { requireJwt } from './auth.js';
|
|
3
4
|
import { table } from './print.js';
|
|
5
|
+
import { localTokenPrefix } from './config.js';
|
|
4
6
|
|
|
5
7
|
const DECISION_ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~', TIERED: '⊕' };
|
|
6
8
|
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return 'no MCPs applied';
|
|
9
|
+
function _promptYN(question) {
|
|
10
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim().toLowerCase() === 'y'); }));
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const HELP = {
|
|
14
15
|
list: ` troxy policies list\n\n Lists all policies in your account with their action, scope, status, and conditions.\n`,
|
|
15
16
|
describe: ` troxy policies describe --name <policy-name>\n\n Shows full details for a single policy.\n\n Options:\n --name Name of the policy (use single quotes for names with special chars)\n`,
|
|
16
|
-
create: ` troxy policies create --name <name> --action <action> [options]\n\n Creates a new policy. Login required.\n\n
|
|
17
|
+
create: ` troxy policies create --name <name> --action <action> [options]\n troxy policies create --describe "<plain English>"\n\n Creates a new policy. Login required.\n\n AI builder:\n --describe "<text>" Describe the policy in plain English (e.g. "block\n Amazon purchases over $200") and Troxy AI drafts\n it for you to confirm. Rate-limited per day.\n\n Manual — required:\n --name Policy name\n --action ALLOW, BLOCK, NOTIFY, or ESCALATE\n\n Manual — optional conditions:\n --field Field to match: amount, merchant_name, tx_per_day\n --operator eq, neq, gt, gte, lt, lte, contains, starts_with, not_contains, between\n --value Comparison value (e.g. 500, amazon)\n --value2 Upper bound for 'between' operator\n\n Priority:\n --priority <n> Explicit priority number (default: auto, max+10)\n Lower number = higher priority (evaluated first).\n\n Scope (default: all agents — every MCP plus Troxy Chat):\n --mcp <name> Scope to one or more MCPs (comma-separated).\n Run 'troxy mcps list' to see MCP names.\n --chat Include Troxy Chat. Implied by default; combine with\n --mcp to scope to specific MCPs *and* chat, or use\n alone to scope to Troxy Chat only (no MCPs).\n --no-chat Exclude Troxy Chat from this policy's scope.\n\n Examples:\n troxy policies create --describe "block Amazon purchases over $200"\n troxy policies create --name "Block large" --action BLOCK --field amount --operator gte --value 500\n troxy policies create --name "Block Amazon" --action BLOCK --field merchant_name --operator contains --value amazon\n troxy policies create --name "Cap volume" --action BLOCK --field tx_per_day --operator gt --value 20\n troxy policies create --name "Allow Wiki" --action ALLOW --priority 5 --field merchant_name --operator contains --value Wiki\n troxy policies create --name "Laptop only" --action BLOCK --mcp "My Laptop" --field amount --operator gte --value 100\n troxy policies create --name "Multi-agent" --action BLOCK --mcp "My Laptop,Server" --field amount --operator gte --value 100\n troxy policies create --name "Chat only" --action ESCALATE --chat --field amount --operator gte --value 200\n troxy policies create --name "MCPs, no chat" --action BLOCK --no-chat --field merchant_name --operator contains --value casino\n`,
|
|
17
18
|
'set-priority': ` troxy policies set-priority --name <policy-name> --priority <n>\n\n Changes the priority of a policy. Lower number = higher priority (evaluated first).\n\n Options:\n --name Name of the policy\n --priority New priority number (e.g. 10, 20, 50)\n\n Example:\n troxy policies set-priority --name "Block Wiki" --priority 5\n`,
|
|
18
19
|
resume: ` troxy policies resume --name <policy-name>\n\n Resumes a paused policy.\n\n Options:\n --name Name of the policy to resume\n`,
|
|
19
20
|
pause: ` troxy policies pause --name <policy-name>\n\n Pauses a policy without deleting it. The policy stops firing until you resume it.\n\n Options:\n --name Name of the policy to pause\n`,
|
|
@@ -33,7 +34,7 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
33
34
|
const jwt = requireJwt();
|
|
34
35
|
switch (sub || 'list') {
|
|
35
36
|
case 'list': {
|
|
36
|
-
const data = await api.agentPolicies(jwt);
|
|
37
|
+
const data = await api.agentPolicies(jwt, localTokenPrefix());
|
|
37
38
|
const policies = data?.policies || [];
|
|
38
39
|
if (!policies.length) { console.log('\n No policies yet.\n'); return; }
|
|
39
40
|
console.log();
|
|
@@ -43,7 +44,7 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
43
44
|
p.priority,
|
|
44
45
|
p.name,
|
|
45
46
|
p.action,
|
|
46
|
-
|
|
47
|
+
p.scope,
|
|
47
48
|
p.enabled ? 'active' : 'paused',
|
|
48
49
|
_condSummary(p),
|
|
49
50
|
p.applies_to_me ? '✓' : '—',
|
|
@@ -55,10 +56,11 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
55
56
|
case 'describe': {
|
|
56
57
|
const name = flags.name;
|
|
57
58
|
if (!name) { console.error(' --name is required (tip: use single quotes for names with special chars)\n'); process.exit(1); }
|
|
58
|
-
const data = await api.agentPolicies(jwt);
|
|
59
|
+
const data = await api.agentPolicies(jwt, localTokenPrefix());
|
|
59
60
|
const p = (data?.policies || []).find(x => x.name.toLowerCase() === name.toLowerCase());
|
|
60
61
|
if (!p) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
|
|
61
62
|
|
|
63
|
+
const mcpList = p.mcps.length ? p.mcps.map(m => m.name).join(', ') : (p.global ? 'all MCPs' : 'none');
|
|
62
64
|
console.log(`
|
|
63
65
|
Name: ${p.name}
|
|
64
66
|
Action: ${p.action}
|
|
@@ -66,7 +68,8 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
66
68
|
Status: ${p.enabled ? 'active' : 'paused'}
|
|
67
69
|
Scope: ${p.scope}
|
|
68
70
|
Applies here: ${p.applies_to_me ? 'yes' : 'no'}
|
|
69
|
-
MCPs: ${
|
|
71
|
+
MCPs: ${mcpList}
|
|
72
|
+
Troxy Chat: ${p.applies_to_chat ? 'yes' : 'no'}
|
|
70
73
|
Conditions: ${_condDetail(p)}
|
|
71
74
|
Created: ${new Date(p.created_at).toLocaleDateString()}
|
|
72
75
|
`);
|
|
@@ -81,6 +84,35 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
81
84
|
|
|
82
85
|
switch (sub) {
|
|
83
86
|
case 'create': {
|
|
87
|
+
if (flags.describe) {
|
|
88
|
+
process.stdout.write('\n Asking Troxy AI to build this policy... ');
|
|
89
|
+
const { tokens = [] } = await api.listTokens(jwt);
|
|
90
|
+
const mcps = tokens.map(t => ({ id: t.id, name: t.name || t.agent_name || t.prefix }));
|
|
91
|
+
const draft = await api.aiPolicy(jwt, { text: flags.describe, mcps });
|
|
92
|
+
if (draft.error) { console.log('✗'); console.error(`\n ${draft.error}\n`); process.exit(1); }
|
|
93
|
+
console.log('✓\n');
|
|
94
|
+
console.log(` Name: ${draft.name}`);
|
|
95
|
+
console.log(` Action: ${draft.action}`);
|
|
96
|
+
if (draft.conditions?.length) console.log(` Conditions: ${_condDetail(draft)}`);
|
|
97
|
+
if (draft.mcp_ids?.length) console.log(` Scope: ${draft.mcp_ids.length} MCP(s)`);
|
|
98
|
+
if (draft.warnings?.length) console.log(` Warnings: ${draft.warnings.join('; ')}`);
|
|
99
|
+
if (draft.note) console.log(` Note: ${draft.note}`);
|
|
100
|
+
const ok = await _promptYN('\n Create this policy? (y/N): ');
|
|
101
|
+
if (!ok) { console.log('\n Cancelled.\n'); process.exit(0); }
|
|
102
|
+
const body = {
|
|
103
|
+
name: draft.name, action: draft.action,
|
|
104
|
+
conditions: draft.conditions || [], or_conditions: draft.or_conditions || [],
|
|
105
|
+
enabled: true,
|
|
106
|
+
global: !(draft.mcp_ids && draft.mcp_ids.length),
|
|
107
|
+
mcp_ids: draft.mcp_ids || [],
|
|
108
|
+
applies_to_chat: true,
|
|
109
|
+
source: 'ai',
|
|
110
|
+
};
|
|
111
|
+
const policy = await api.createPolicy(jwt, body);
|
|
112
|
+
console.log(`\n Policy "${policy.name}" created ✓ (priority: ${policy.priority})\n`);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
84
116
|
const name = flags.name;
|
|
85
117
|
const action = (flags.action || '').toUpperCase();
|
|
86
118
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
@@ -114,34 +146,60 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
114
146
|
conditions.push(cond);
|
|
115
147
|
}
|
|
116
148
|
|
|
117
|
-
|
|
149
|
+
if (flags.chat && flags['no-chat']) {
|
|
150
|
+
console.error(' --chat and --no-chat cannot both be set\n'); process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --mcp "name" or "name1,name2": scope policy to specific MCPs instead of all.
|
|
154
|
+
// --chat / --no-chat: include or exclude Troxy Chat (default: included).
|
|
118
155
|
let isGlobal = true;
|
|
119
156
|
let mcpIds = [];
|
|
157
|
+
let mcpNames = [];
|
|
120
158
|
if (flags.mcp) {
|
|
121
159
|
const { tokens = [] } = await api.listTokens(jwt);
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
160
|
+
const requested = String(flags.mcp).split(',').map(s => s.trim()).filter(Boolean);
|
|
161
|
+
for (const raw of requested) {
|
|
162
|
+
const needle = raw.toLowerCase();
|
|
163
|
+
const match = tokens.find(t =>
|
|
164
|
+
(t.name && t.name.toLowerCase() === needle) ||
|
|
165
|
+
(t.agent_name && t.agent_name.toLowerCase() === needle) ||
|
|
166
|
+
(t.prefix && t.prefix.toLowerCase().startsWith(needle))
|
|
167
|
+
);
|
|
168
|
+
if (!match) {
|
|
169
|
+
console.error(`\n MCP "${raw}" not found. Run: troxy mcps list\n`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
mcpIds.push(match.id);
|
|
173
|
+
mcpNames.push(match.name || match.agent_name || match.prefix);
|
|
131
174
|
}
|
|
132
175
|
isGlobal = false;
|
|
133
|
-
|
|
134
|
-
|
|
176
|
+
console.log(`\n Scoping to MCP${mcpNames.length > 1 ? 's' : ''}: ${mcpNames.join(', ')}`);
|
|
177
|
+
} else if (flags.chat) {
|
|
178
|
+
// --chat with no --mcp: scope to Troxy Chat only, no MCPs
|
|
179
|
+
isGlobal = false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const appliesToChat = flags['no-chat'] ? false : true;
|
|
183
|
+
|
|
184
|
+
if (!isGlobal && mcpIds.length === 0 && !appliesToChat) {
|
|
185
|
+
console.error(' Nothing to scope to — pass --mcp, --chat, or drop --no-chat\n'); process.exit(1);
|
|
135
186
|
}
|
|
136
187
|
|
|
137
188
|
const priority = flags.priority != null ? parseInt(flags.priority, 10) : undefined;
|
|
138
189
|
if (flags.priority != null && isNaN(priority)) {
|
|
139
190
|
console.error(' --priority must be a number\n'); process.exit(1);
|
|
140
191
|
}
|
|
141
|
-
const body = { name, action, conditions, enabled: true, global: isGlobal, mcp_ids: mcpIds };
|
|
192
|
+
const body = { name, action, conditions, enabled: true, global: isGlobal, mcp_ids: mcpIds, applies_to_chat: appliesToChat };
|
|
142
193
|
if (priority != null) body.priority = priority;
|
|
143
194
|
const policy = await api.createPolicy(jwt, body);
|
|
144
|
-
|
|
195
|
+
let scope;
|
|
196
|
+
if (isGlobal) {
|
|
197
|
+
scope = appliesToChat ? 'all agents' : 'all MCPs';
|
|
198
|
+
} else {
|
|
199
|
+
const parts = mcpNames.slice();
|
|
200
|
+
if (appliesToChat) parts.push('Troxy Chat');
|
|
201
|
+
scope = parts.length ? parts.join(', ') : 'no agents';
|
|
202
|
+
}
|
|
145
203
|
console.log(`\n Policy "${policy.name}" created ✓ (priority: ${policy.priority}, scope: ${scope})\n`);
|
|
146
204
|
break;
|
|
147
205
|
}
|
package/src/secrets.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { api } from './api.js';
|
|
3
|
+
import { requireJwt } from './auth.js';
|
|
4
|
+
import { table } from './print.js';
|
|
5
|
+
|
|
6
|
+
const HELP = {
|
|
7
|
+
list: ` troxy secrets list\n\n Lists your saved LLM provider keys (used by Troxy Chat). Values are\n never shown, only a masked preview.\n`,
|
|
8
|
+
set: ` troxy secrets set --name <name> [--type api_key|text]\n\n Saves or updates an LLM provider key. You'll be prompted for the value\n so it never appears in your shell history. Login required.\n\n Options:\n --name Label for this key (e.g. "OpenAI", "Anthropic")\n --type api_key (default) or text\n\n Card secrets are dashboard-only — troxy secrets does not handle them.\n\n Example:\n troxy secrets set --name "OpenAI"\n`,
|
|
9
|
+
delete: ` troxy secrets delete --name <name>\n\n Deletes a saved key. Login required.\n`,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const CTRL_C = '';
|
|
13
|
+
const BACKSPACE = '';
|
|
14
|
+
|
|
15
|
+
function promptHidden(question) {
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: null });
|
|
18
|
+
process.stdout.write(question);
|
|
19
|
+
let buf = '';
|
|
20
|
+
process.stdin.setRawMode(true);
|
|
21
|
+
process.stdin.resume();
|
|
22
|
+
process.stdin.setEncoding('utf8');
|
|
23
|
+
const onData = chunk => {
|
|
24
|
+
for (const ch of chunk) {
|
|
25
|
+
if (ch === '\r' || ch === '\n') {
|
|
26
|
+
process.stdin.setRawMode(false);
|
|
27
|
+
process.stdin.pause();
|
|
28
|
+
process.stdin.removeListener('data', onData);
|
|
29
|
+
rl.close();
|
|
30
|
+
process.stdout.write('\n');
|
|
31
|
+
resolve(buf.trim());
|
|
32
|
+
return;
|
|
33
|
+
} else if (ch === CTRL_C) {
|
|
34
|
+
process.stdout.write('\n');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
} else if (ch === BACKSPACE || ch === '\b') {
|
|
37
|
+
if (buf.length > 0) { buf = buf.slice(0, -1); process.stdout.write('\b \b'); }
|
|
38
|
+
} else if (ch >= ' ') {
|
|
39
|
+
buf += ch;
|
|
40
|
+
process.stdout.write('•');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
process.stdin.on('data', onData);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runSecrets([sub, ...args], flags) {
|
|
49
|
+
if (flags.help || flags.h) {
|
|
50
|
+
console.log('\n' + (HELP[sub] || ` troxy secrets <subcommand> [options]\n\n Subcommands:\n list List saved LLM provider keys\n set Save or update a key\n delete Delete a key\n\n Run 'troxy secrets <subcommand> --help' for subcommand help.\n`));
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const jwt = requireJwt();
|
|
55
|
+
|
|
56
|
+
switch (sub || 'list') {
|
|
57
|
+
case 'list': {
|
|
58
|
+
const { secrets = [] } = await api.listSecrets(jwt);
|
|
59
|
+
if (!secrets.length) { console.log('\n No secrets saved yet.\n'); return; }
|
|
60
|
+
console.log();
|
|
61
|
+
table(
|
|
62
|
+
['Name', 'Type', 'Preview', 'Updated'],
|
|
63
|
+
secrets.map(s => [s.name, s.secret_type, s.masked, new Date(s.updated_at).toLocaleDateString()]),
|
|
64
|
+
);
|
|
65
|
+
if (secrets.some(s => s.secret_type === 'credit_card')) {
|
|
66
|
+
console.log(' Card secrets can only be managed from the dashboard.\n');
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case 'set': {
|
|
72
|
+
const name = flags.name;
|
|
73
|
+
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
74
|
+
const type = (flags.type || 'api_key').toLowerCase();
|
|
75
|
+
if (!['api_key', 'text'].includes(type)) {
|
|
76
|
+
console.error(' --type must be api_key or text. Card secrets are dashboard-only.\n'); process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const value = await promptHidden(` Value for "${name}": `);
|
|
79
|
+
if (!value) { console.error(' No value entered. Cancelled.\n'); process.exit(1); }
|
|
80
|
+
await api.saveSecret(jwt, { name, secret_type: type, value });
|
|
81
|
+
console.log(`\n Secret "${name}" saved ✓\n`);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'delete': {
|
|
86
|
+
const name = flags.name;
|
|
87
|
+
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
88
|
+
const { secrets = [] } = await api.listSecrets(jwt);
|
|
89
|
+
const secret = secrets.find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
90
|
+
if (!secret) { console.error(` Secret "${name}" not found\n`); process.exit(1); }
|
|
91
|
+
await api.deleteSecret(jwt, secret.kind);
|
|
92
|
+
console.log(`\n Secret "${name}" deleted ✓\n`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
98
|
+
console.error(' Usage: troxy secrets [list|set|delete]\n');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/settings.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireJwt } from './auth.js';
|
|
3
|
+
|
|
4
|
+
const VALID_TIMEOUTS = [0, 0.083, 1, 4, 8, 24, 72];
|
|
5
|
+
|
|
6
|
+
const HELP = {
|
|
7
|
+
show: ` troxy settings show\n\n Shows your account settings: default action, approval timeout, and\n notification preferences.\n`,
|
|
8
|
+
set: ` troxy settings set [options]\n\n Updates account settings. Login required. Pass any combination of:\n\n --default-action <action> ALLOW, BLOCK, ESCALATE, or NOTIFY\n (used when no policy matches a transaction)\n --approval-timeout <hours> ${VALID_TIMEOUTS.join(', ')}\n (how long an ESCALATE hold waits before auto-resolving)\n --notify-email <email> Where to send decision notification emails\n --notify-on-block Email me when a transaction is blocked\n --no-notify-on-block\n --notify-on-escalate Email me when a transaction is escalated\n --no-notify-on-escalate\n --notify-on-notify Email me on NOTIFY decisions\n --no-notify-on-notify\n --notify-on-allow Email me when a transaction is allowed\n --no-notify-on-allow\n\n Example:\n troxy settings set --default-action BLOCK --approval-timeout 4\n`,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function runSettings([sub, ...args], flags) {
|
|
12
|
+
if (flags.help || flags.h) {
|
|
13
|
+
console.log('\n' + (HELP[sub] || ` troxy settings <subcommand> [options]\n\n Subcommands:\n show Show current settings\n set Update settings\n\n Run 'troxy settings <subcommand> --help' for subcommand help.\n`));
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const jwt = requireJwt();
|
|
18
|
+
|
|
19
|
+
switch (sub || 'show') {
|
|
20
|
+
case 'show': {
|
|
21
|
+
const s = await api.getSettings(jwt);
|
|
22
|
+
console.log(`
|
|
23
|
+
Email: ${s.email}
|
|
24
|
+
Default action: ${s.default_action}
|
|
25
|
+
Approval timeout: ${s.approval_timeout_hours} hour${s.approval_timeout_hours === 1 ? '' : 's'}
|
|
26
|
+
|
|
27
|
+
Notifications
|
|
28
|
+
On block: ${s.notify_prefs.email_on_block ? 'yes' : 'no'}
|
|
29
|
+
On escalate: ${s.notify_prefs.email_on_escalate ? 'yes' : 'no'}
|
|
30
|
+
On notify: ${s.notify_prefs.email_on_notify ? 'yes' : 'no'}
|
|
31
|
+
On allow: ${s.notify_prefs.email_on_allow ? 'yes' : 'no'}
|
|
32
|
+
Notify email: ${s.notify_prefs.notify_email || '(account email)'}
|
|
33
|
+
|
|
34
|
+
Chat budget action: ${s.chat_budget_action}
|
|
35
|
+
Chat budget limits: ${s.chat_budget_limits.length ? s.chat_budget_limits.map(b => `${b.currency} ${b.used}/${b.limit}`).join(', ') : 'none set — run troxy chat-budget'}
|
|
36
|
+
`);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case 'set': {
|
|
41
|
+
const body = {};
|
|
42
|
+
|
|
43
|
+
if (flags['default-action']) {
|
|
44
|
+
const action = flags['default-action'].toUpperCase();
|
|
45
|
+
if (!['ALLOW', 'BLOCK', 'ESCALATE', 'NOTIFY'].includes(action)) {
|
|
46
|
+
console.error(' --default-action must be ALLOW, BLOCK, ESCALATE, or NOTIFY\n'); process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
body.default_action = action;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (flags['approval-timeout'] != null) {
|
|
52
|
+
const timeout = Number(flags['approval-timeout']);
|
|
53
|
+
if (!VALID_TIMEOUTS.includes(timeout)) {
|
|
54
|
+
console.error(` --approval-timeout must be one of: ${VALID_TIMEOUTS.join(', ')}\n`); process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
body.approval_timeout_hours = timeout;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const notify_prefs = {};
|
|
60
|
+
if (flags['notify-email'] != null) notify_prefs.notify_email = flags['notify-email'];
|
|
61
|
+
if (flags['notify-on-block']) notify_prefs.email_on_block = true;
|
|
62
|
+
if (flags['no-notify-on-block']) notify_prefs.email_on_block = false;
|
|
63
|
+
if (flags['notify-on-escalate']) notify_prefs.email_on_escalate = true;
|
|
64
|
+
if (flags['no-notify-on-escalate']) notify_prefs.email_on_escalate = false;
|
|
65
|
+
if (flags['notify-on-notify']) notify_prefs.email_on_notify = true;
|
|
66
|
+
if (flags['no-notify-on-notify']) notify_prefs.email_on_notify = false;
|
|
67
|
+
if (flags['notify-on-allow']) notify_prefs.email_on_allow = true;
|
|
68
|
+
if (flags['no-notify-on-allow']) notify_prefs.email_on_allow = false;
|
|
69
|
+
if (Object.keys(notify_prefs).length) body.notify_prefs = notify_prefs;
|
|
70
|
+
|
|
71
|
+
if (Object.keys(body).length === 0) {
|
|
72
|
+
console.error(' Nothing to update — pass at least one option. Run troxy settings set --help\n');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await api.updateSettings(jwt, body);
|
|
77
|
+
console.log('\n Settings updated ✓\n');
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
default:
|
|
82
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
83
|
+
console.error(' Usage: troxy settings [show|set]\n');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/uninstall.js
CHANGED
|
@@ -44,6 +44,7 @@ function removeService() {
|
|
|
44
44
|
try { execSync('sudo systemctl stop troxy-mcp 2>/dev/null'); } catch {}
|
|
45
45
|
try { execSync('sudo systemctl disable troxy-mcp 2>/dev/null'); } catch {}
|
|
46
46
|
try { execSync('sudo rm -f /etc/systemd/system/troxy-mcp.service'); } catch {}
|
|
47
|
+
try { execSync('sudo rm -f /etc/troxy-mcp.env'); } catch {}
|
|
47
48
|
try { execSync('sudo systemctl daemon-reload'); } catch {}
|
|
48
49
|
return true;
|
|
49
50
|
|