@biznetgio/neo-app 0.4.28 → 0.4.29

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/mcp/tools.js +113 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.28",
3
+ "version": "0.4.29",
4
4
  "description": "NEO App CLI — one-command agent onboarding, MCP server, and project ops for coding agents (Codex, Claude Code).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/mcp/tools.js CHANGED
@@ -39,6 +39,32 @@ function requireEnv(env) {
39
39
  return e;
40
40
  }
41
41
 
42
+ // gbFromSize parses "10Gi"/"10G"/"10" → 10 (integer GB), else undefined.
43
+ function gbFromSize(v) {
44
+ if (v == null) return undefined;
45
+ const m = String(v).match(/(\d+)/);
46
+ return m ? parseInt(m[1], 10) : undefined;
47
+ }
48
+
49
+ // confirmCost is the Approve & Create gate for billable one-shot creates: unless
50
+ // args.confirm===true it returns the live-priced cost and creates NOTHING — the
51
+ // agent shows the user, then re-calls with confirm:true. plan/apply remains the
52
+ // server-immutable costed path; this is the lighter client-side gate (handoff 3.3).
53
+ async function confirmCost(api, args, spec) {
54
+ if (args.confirm === true) return null;
55
+ let orgId = null;
56
+ try { orgId = await api.resolveOrgId(args.orgId); } catch { orgId = null; }
57
+ let book = null;
58
+ try { book = (await api.get(orgId ? `/api/v1/orgs/${orgId}/pricing` : '/api/v1/pricing')).data; }
59
+ catch { try { book = (await api.get('/api/v1/pricing')).data; } catch { book = null; } }
60
+ return {
61
+ requiresConfirmation: true,
62
+ cost: book ? estimateCost(book, spec) : null,
63
+ priceBook: orgId ? `org:${orgId}` : 'global-defaults',
64
+ message: 'Billable resource — show the user this cost, then re-call with confirm:true to create. For an immutable, server-pinned quote use neo_plan_stack → neo_apply_stack.',
65
+ };
66
+ }
67
+
42
68
  const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey)/i;
43
69
  function redactSecrets(value) {
44
70
  if (Array.isArray(value)) return value.map(redactSecrets);
@@ -94,8 +120,8 @@ export const TOOLS = [
94
120
  { type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
95
121
  { type: 'static_site', create: 'via_folder', deploy: true, scale: false, logs: true, variables: true, delete: true, nativeStatic: true, note: 'neo_deploy_folder serviceType=static_site with buildCommand/publishDirectory → native nginx static hosting (0.4.27); auto-detected for static-only folders' },
96
122
  { type: 'worker', create: 'via_folder', deploy: true, scale: true, logs: true, variables: true, delete: true, note: 'neo_deploy_folder serviceType=worker — no public ingress; repo auto-detection still TODO (handoff 6)' },
97
- { type: 'database', create: true, get: true, list: true, update: true, backup: true, verify: true, delete: true, note: 'neo_create/list/get/update(resize)/delete_database + backup + verify (SQL round-trip probe); restore still TODO (handoff 4.4)' },
98
- { type: 'queue', create: true, get: true, list: true, update: true, delete: true, note: 'neo_create/list/get/update/delete_queue (0.4.15-0.4.25) (handoff 4.5)' },
123
+ { type: 'database', create: true, get: true, list: true, update: true, backup: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/list/get/update(resize)/delete_database + backup + verify (SQL round-trip probe) + enable/disable_db_public_access; restore still TODO (handoff 4.4)' },
124
+ { type: 'queue', create: true, get: true, list: true, update: true, publicAccess: true, delete: true, note: 'neo_create/list/get/update/delete_queue + enable/disable_queue_public_access (handoff 4.5)' },
99
125
  { type: 'cron', create: true, get: true, list: true, run: true, runs: true, delete: true, note: 'neo_create/list/get/run/list_runs/delete_cron (0.4.15-0.4.21) (handoff 4.6)' },
100
126
  { type: 'object_storage', create: true, get: true, list: true, objects: true, upload: true, delete: true, note: 'neo_create/list/get/delete_bucket + list/upload/presign(get)/delete objects (0.4.15-0.4.25) (handoff 4.7)' },
101
127
  ];
@@ -105,7 +131,7 @@ export const TOOLS = [
105
131
  controlPlane,
106
132
  resourceTypes,
107
133
  // Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
108
- lifecycleVerbs: ['plan', 'apply', 'get_operation', 'wait_operation', 'delete_stack', 'reconcile_usage', 'service_usage', 'create', 'list', 'get', 'update', 'verify', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'presign_object', 'backup', 'delete', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
134
+ lifecycleVerbs: ['plan', 'apply', 'get_operation', 'wait_operation', 'delete_stack', 'reconcile_usage', 'service_usage', 'create', 'list', 'get', 'update', 'verify', 'enable_public_access', 'disable_public_access', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'presign_object', 'backup', 'delete', 'delete_project', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
109
135
  bindings: { supported: true, targets: ['neo_db', 'neo_queue'], writeOnly: true, note: 'neo_bind_resource injects DATABASE_URL/NATS_URL server-side at deploy; the secret is never returned (handoff 5)' },
110
136
  sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
111
137
  environments: ['staging', 'production'],
@@ -496,12 +522,15 @@ export const TOOLS = [
496
522
  volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
497
523
  replicas: int('Read/failover replicas for HA (optional; postgres/CNPG only)'),
498
524
  deploymentMode: { type: 'string', description: 'single | ha (optional)' },
525
+ confirm: { type: 'boolean', description: 'Approve & Create: without confirm:true the tool returns the cost and creates nothing. Re-call with confirm:true to actually create.' },
499
526
  },
500
527
  required: ['projectId', 'name', 'engine', 'environment'],
501
528
  },
502
529
  async call(api, args) {
503
530
  requireEnv(args.environment);
504
- const { projectId, ...body } = args;
531
+ const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas });
532
+ if (gate) return gate;
533
+ const { projectId, confirm, orgId, ...body } = args;
505
534
  const { status, data } = await api.post(`/api/v1/projects/${projectId}/databases`, body);
506
535
  return redactSecrets(result(status, data));
507
536
  },
@@ -606,13 +635,16 @@ export const TOOLS = [
606
635
  planClass: str('Plan/engine class (optional)'),
607
636
  environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
608
637
  volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
609
- dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional)' },
638
+ dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional, default true). Pass false to disable — this is now honored.' },
639
+ confirm: { type: 'boolean', description: 'Approve & Create: without confirm:true the tool returns the cost and creates nothing. Re-call with confirm:true to actually create.' },
610
640
  },
611
641
  required: ['projectId', 'name', 'environment'],
612
642
  },
613
643
  async call(api, args) {
614
644
  requireEnv(args.environment);
615
- const { projectId, ...body } = args;
645
+ const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize) });
646
+ if (gate) return gate;
647
+ const { projectId, confirm, orgId, ...body } = args;
616
648
  const { status, data } = await api.post(`/api/v1/projects/${projectId}/queues`, body);
617
649
  return redactSecrets(result(status, data));
618
650
  },
@@ -1085,4 +1117,79 @@ export const TOOLS = [
1085
1117
  return result(status, data);
1086
1118
  },
1087
1119
  },
1120
+ {
1121
+ name: 'neo_enable_db_public_access',
1122
+ description: 'Enable internet access to a managed postgres database through the TLS gateway (phase 3). Provisions a least-privilege external role, assigns a public host, and flips publicEnabled. The DB must be DEPLOYED first. Optionally restrict to allowlistCidrs. Credentials are write-only — retrieve the full external connection string from the dashboard; this returns the public host/port, external username, and CA URL.',
1123
+ mutating: true,
1124
+ schema: {
1125
+ type: 'object',
1126
+ properties: {
1127
+ databaseId: str('Database id (postgres, already deployed)'),
1128
+ allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR allowlist, e.g. ["203.0.113.0/24"] (optional; empty = allow any source IP)' },
1129
+ },
1130
+ required: ['databaseId'],
1131
+ },
1132
+ async call(api, args) {
1133
+ const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
1134
+ return redactSecrets(result(status, data));
1135
+ },
1136
+ },
1137
+ {
1138
+ name: 'neo_disable_db_public_access',
1139
+ description: 'Disable internet access to a managed database. Non-destructive: the external role is set NOLOGIN and its objects are preserved; routing stops.',
1140
+ mutating: true,
1141
+ schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
1142
+ async call(api, args) {
1143
+ const { status, data } = await api.del(`/api/v1/databases/${args.databaseId}/public-access`);
1144
+ return result(status, data);
1145
+ },
1146
+ },
1147
+ {
1148
+ name: 'neo_enable_queue_public_access',
1149
+ description: 'Enable internet access to a managed queue through the TLS gateway (phase 3). Assigns a public host and flips publicEnabled. The queue must be DEPLOYED first. Optionally restrict to allowlistCidrs. Credentials are write-only — retrieve the full connection string from the dashboard.',
1150
+ mutating: true,
1151
+ schema: {
1152
+ type: 'object',
1153
+ properties: {
1154
+ queueId: str('Queue id (already deployed)'),
1155
+ allowlistCidrs: { type: 'array', items: { type: 'string' }, description: 'CIDR allowlist (optional; empty = allow any source IP)' },
1156
+ },
1157
+ required: ['queueId'],
1158
+ },
1159
+ async call(api, args) {
1160
+ const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
1161
+ return redactSecrets(result(status, data));
1162
+ },
1163
+ },
1164
+ {
1165
+ name: 'neo_disable_queue_public_access',
1166
+ description: 'Disable internet access to a managed queue. Non-destructive: routing stops; the queue keeps running for in-cluster consumers.',
1167
+ mutating: true,
1168
+ schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
1169
+ async call(api, args) {
1170
+ const { status, data } = await api.del(`/api/v1/queues/${args.queueId}/public-access`);
1171
+ return result(status, data);
1172
+ },
1173
+ },
1174
+ {
1175
+ name: 'neo_delete_project',
1176
+ description: 'Permanently delete a project and ALL its resources (apps, databases, queues, crons, buckets, and the project namespace with its PVCs/CNPG clusters). Destructive and irreversible — nothing is recoverable. Requires confirm:true. Only the project creator or an org owner/admin may delete it.',
1177
+ mutating: true,
1178
+ destructive: true,
1179
+ schema: {
1180
+ type: 'object',
1181
+ properties: {
1182
+ projectId: str('Project id'),
1183
+ confirm: { type: 'boolean', description: 'Must be true to proceed — guards against accidental deletion of an entire project.' },
1184
+ },
1185
+ required: ['projectId', 'confirm'],
1186
+ },
1187
+ async call(api, args) {
1188
+ if (args.confirm !== true) {
1189
+ return { error: 'refused', message: 'Deleting a project removes ALL its resources and is irreversible. Re-call with confirm:true to proceed.' };
1190
+ }
1191
+ const { status, data } = await api.del(`/api/v1/projects/${args.projectId}`);
1192
+ return result(status, data);
1193
+ },
1194
+ },
1088
1195
  ];