voroa-cli 0.0.0-stage → 0.3.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -2
  3. package/package.json +41 -4
  4. package/src/agent.js +95 -0
  5. package/src/api.js +552 -0
  6. package/src/commands/db.js +99 -0
  7. package/src/commands/deploy.js +130 -0
  8. package/src/commands/domains.js +122 -0
  9. package/src/commands/env.js +143 -0
  10. package/src/commands/lifecycle.js +40 -0
  11. package/src/commands/link.js +92 -0
  12. package/src/commands/login.js +104 -0
  13. package/src/commands/logs.js +304 -0
  14. package/src/commands/services.js +72 -0
  15. package/src/commands/ssh.js +46 -0
  16. package/src/commands/status.js +50 -0
  17. package/src/commands/tokens.js +81 -0
  18. package/src/commands/workspace.js +63 -0
  19. package/src/config.js +108 -0
  20. package/src/errors.js +53 -0
  21. package/src/flags.js +32 -0
  22. package/src/index.js +212 -0
  23. package/src/interactive.js +34 -0
  24. package/src/link.js +72 -0
  25. package/src/mcp/build.js +72 -0
  26. package/src/mcp/kernel/index.js +59 -0
  27. package/src/mcp/kernel/logger.js +28 -0
  28. package/src/mcp/lib/config.js +72 -0
  29. package/src/mcp/lib/credentials.js +51 -0
  30. package/src/mcp/lib/http.js +136 -0
  31. package/src/mcp/lib/response.js +14 -0
  32. package/src/mcp/lib/reversibility.js +33 -0
  33. package/src/mcp/reversibilityMap.json +77 -0
  34. package/src/mcp/tools/_action.js +49 -0
  35. package/src/mcp/tools/_ids.js +42 -0
  36. package/src/mcp/tools/_shape.js +318 -0
  37. package/src/mcp/tools/actions.js +94 -0
  38. package/src/mcp/tools/getBuildLog.js +39 -0
  39. package/src/mcp/tools/getDeployHistory.js +42 -0
  40. package/src/mcp/tools/getRuntimeLogs.js +44 -0
  41. package/src/mcp/tools/getServiceStatus.js +41 -0
  42. package/src/mcp/tools/getUsage.js +34 -0
  43. package/src/mcp/tools/listDatabases.js +25 -0
  44. package/src/mcp/tools/listDomains.js +33 -0
  45. package/src/mcp/tools/listServices.js +20 -0
  46. package/src/mcp/tools/propose.js +124 -0
  47. package/src/mcp/tools/reads.js +756 -0
  48. package/src/mcp/tools/writes.js +632 -0
  49. package/src/mcpTools.js +292 -0
  50. package/src/output.js +145 -0
  51. package/src/prompt.js +229 -0
  52. package/src/region.js +5 -0
  53. package/src/resolve.js +64 -0
  54. package/src/session.js +270 -0
  55. package/src/status.js +199 -0
  56. package/src/stepup.js +69 -0
@@ -0,0 +1,33 @@
1
+ // list_domains - READ (scope services:read). A service's addresses and their certificate state.
2
+ // Maps to GET /apps/:uuid/domains.
3
+ //
4
+ // Registrar DNS instructions and the platform A-record address are deliberately NOT surfaced (see
5
+ // _shape.js `domains`). Adding a domain is not on this surface either - it is a dashboard action.
6
+
7
+ import { jsonResponse, badInput } from '../lib/response.js';
8
+ import { serviceId } from './_ids.js';
9
+ import { domains } from './_shape.js';
10
+
11
+ export const listDomains = {
12
+ name: 'list_domains',
13
+ kind: 'read',
14
+ scope: 'services:read',
15
+ description:
16
+ 'List a service\'s addresses: its default Voroa address plus any custom domains, with each domain\'s '
17
+ + 'routing state and certificate state. Read-only - adding or removing a domain is done in your Voroa '
18
+ + 'dashboard, not from here.',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ serviceId: { type: 'string', description: 'The service id, from list_services.' },
23
+ },
24
+ required: ['serviceId'],
25
+ additionalProperties: false,
26
+ },
27
+ async handler(args, ctx) {
28
+ const id = serviceId(args.serviceId);
29
+ if (!id) throw badInput('serviceId is required and must be a valid service id from list_services.');
30
+ const data = await ctx.http.get(`/apps/${encodeURIComponent(id)}/domains`);
31
+ return jsonResponse(domains(data));
32
+ },
33
+ };
@@ -0,0 +1,20 @@
1
+ // list_services - READ (scope services:read). Every service in the workspace this token is pinned to.
2
+ // Maps to GET /apps.
3
+
4
+ import { jsonResponse } from '../lib/response.js';
5
+ import { service } from './_shape.js';
6
+
7
+ export const listServices = {
8
+ name: 'list_services',
9
+ kind: 'read',
10
+ scope: 'services:read',
11
+ description:
12
+ 'List the services in your Voroa workspace, with each service id, name, repository, branch, type and '
13
+ + 'default address. Start here - every other tool takes a service id from this list. Read-only.',
14
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
15
+ async handler(_args, ctx) {
16
+ const data = await ctx.http.get('/apps');
17
+ const apps = Array.isArray(data && data.apps) ? data.apps : [];
18
+ return jsonResponse({ services: apps.map(service), count: apps.length });
19
+ },
20
+ };
@@ -0,0 +1,124 @@
1
+ // Phase D - the two tools of the propose tier (services:admin).
2
+ //
3
+ // There is ONE propose tool with an action name, not twenty. The actions are a closed set the server owns
4
+ // (backend/src/lib/customerMcpProposals.js), and twenty near-identical tools would be twenty places for the
5
+ // list to drift out of step with it. The enum below is what the model chooses from, and the server refuses
6
+ // anything it does not recognise - so the worst a stale enum can do is offer something that 404s, never
7
+ // something that acts.
8
+ //
9
+ // NEITHER TOOL CAN CHANGE ANYTHING, and that is the point of the tier rather than a limitation of it. A
10
+ // propose call records a request and returns a summary; the service, database or setting it names is
11
+ // untouched until a person approves it in the Voroa dashboard, on a surface this token cannot reach. Both
12
+ // are therefore reversible:true in the reversibility map - an autopilot may call them freely, because
13
+ // calling them makes nothing happen.
14
+
15
+ import { jsonResponse, badInput } from '../lib/response.js';
16
+ // Projected through _shape.js like every other tool - tests/staticScan refuses a tool module that shapes
17
+ // its output inline, because an inline projection is invisible to the value-level scan.
18
+ import { pick } from './_shape.js';
19
+
20
+ // Kept in step with the server registry by the capability freeze and route-parity guards: an action here
21
+ // that the server does not know 404s, and a server action nobody can name is unreachable. Grouped in the
22
+ // description so the model can pick the right one without reading twenty schemas.
23
+ const ACTIONS = [
24
+ 'create_service', 'create_static_site', 'create_worker', 'create_cron_job', 'create_database',
25
+ 'delete_service', 'delete_database', 'delete_domain', 'delete_env_var', 'delete_secret_file',
26
+ 'delete_scheduled_task', 'delete_env_group', 'delete_environment', 'delete_database_user',
27
+ 'rotate_database_credentials', 'import_database',
28
+ 'enable_database_external_access', 'set_database_allowlist', 'add_inbound_rule', 'remove_inbound_rule',
29
+ 'unprotect_environment',
30
+ // Wave 2 (plan 2026-10-27) - billable, destructive, credential-granting or irreversible settings that
31
+ // shipped after Phase D. A credential (deploy hook URL, bucket key) is shown ONCE in the dashboard when the
32
+ // person approves; this tool never sees it.
33
+ 'delete_project',
34
+ 'create_bucket', 'delete_bucket', 'delete_bucket_files', 'regenerate_bucket_key',
35
+ 'set_pitr', 'pitr_restore', 'start_pitr_restore', 'restore_database_backup', 'link_database_to_service',
36
+ 'cancel_database_import', 'disable_database_backups', 'set_database_config', 'enable_database_extension',
37
+ 'set_hsts', 'set_edge_cache_static', 'enable_edge_cache_all', 'set_edge_cache_off',
38
+ 'set_autoscale', 'remove_autoscale', 'create_deploy_hook', 'delete_deploy_hook',
39
+ 'delete_preview', 'delete_preview_variable', 'delete_domain_claim',
40
+ 'delete_env_group_variable', 'unlink_env_group',
41
+ // Access REMOVAL only (CTO decision 2026-10-27). Inviting someone, raising a role and adding an SSH key
42
+ // are not on this surface at all.
43
+ 'remove_member', 'change_member_role', 'delete_ssh_key',
44
+ ];
45
+
46
+ export const propose = {
47
+ name: 'propose',
48
+ kind: 'action',
49
+ scope: 'services:admin',
50
+ reversible: true,
51
+ description:
52
+ 'ASK for a change that cannot be undone - creating a service or database, deleting something, rotating '
53
+ + 'a database password, importing over live data, or opening a database to the internet. This tool '
54
+ + 'CANNOT carry any of them out. It records the request and returns a summary; the change happens only '
55
+ + 'if a person approves it in their Voroa dashboard, which you cannot do. Tell the customer plainly '
56
+ + 'that you have asked and that it is waiting for them. Arguments go in `args`: the ids the action '
57
+ + 'needs (serviceId, databaseId, projectId, groupId, bucketId, backupId, jobId, previewId, claimId, '
58
+ + 'key, name...) plus its own fields, such as sourceUrl for import_database, targetAt for pitr_restore, '
59
+ + 'enabled for set_pitr / set_hsts, extension, evictionPolicy, keys for delete_bucket_files, '
60
+ + 'name and plan for create_bucket, minInstances / maxInstances / cpuTargetPct / memTargetPct for '
61
+ + 'set_autoscale, appUuid and envKey for link_database_to_service, customerId (+ a LOWER role) for remove_member / change_member_role, keyId for delete_ssh_key. Anything whose result is a one-time '
62
+ + 'credential (a deploy hook URL, a bucket key) is shown to the person in the dashboard, never to you. '
63
+ + 'Use discard_proposal to take back a request you made in error.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ action: { type: 'string', enum: ACTIONS, description: 'What to ask for.' },
68
+ args: {
69
+ type: 'object',
70
+ description:
71
+ 'The ids and fields this action needs. Ids come from the list tools - list_services, '
72
+ + 'list_databases, list_projects, list_env_groups, list_domains, list_scheduled_tasks.',
73
+ },
74
+ },
75
+ required: ['action', 'args'],
76
+ additionalProperties: false,
77
+ },
78
+ async handler(args, ctx) {
79
+ const action = String(args.action || '').trim();
80
+ if (!ACTIONS.includes(action)) {
81
+ throw badInput(`action must be one of: ${ACTIONS.join(', ')}`);
82
+ }
83
+ if (!args.args || typeof args.args !== 'object' || Array.isArray(args.args)) {
84
+ throw badInput('args must be an object of the ids and fields this action needs.');
85
+ }
86
+ const data = await ctx.http.post(`/propose/${encodeURIComponent(action)}`, args.args);
87
+ const shaped = pick(data || {}, ['summary', 'expiresAt', 'nonce', 'whatHappensNext']);
88
+ return jsonResponse({
89
+ asked: true,
90
+ performed: false,
91
+ summary: shaped.summary,
92
+ expiresAt: shaped.expiresAt,
93
+ proposalId: shaped.nonce,
94
+ whatHappensNext: shaped.whatHappensNext,
95
+ });
96
+ },
97
+ };
98
+
99
+ export const discardProposal = {
100
+ name: 'discard_proposal',
101
+ kind: 'action',
102
+ scope: 'services:admin',
103
+ reversible: true,
104
+ description:
105
+ 'Withdraw a request you raised with propose, so it stops waiting in the customer\'s dashboard. Only '
106
+ + 'works on a request this same token made. The only possible outcome is that nothing happens; propose '
107
+ + 'again if it was withdrawn by mistake.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ proposalId: { type: 'string', description: 'The proposalId returned by propose.' },
112
+ },
113
+ required: ['proposalId'],
114
+ additionalProperties: false,
115
+ },
116
+ async handler(args, ctx) {
117
+ const id = String(args.proposalId || '').trim();
118
+ if (!id) throw badInput('proposalId is required - it is returned by propose.');
119
+ await ctx.http.post(`/propose/${encodeURIComponent(id)}/discard`, {});
120
+ return jsonResponse({ withdrawn: true });
121
+ },
122
+ };
123
+
124
+ export const PROPOSE_TOOLS = [propose, discardProposal];