@biznetgio/neo-app 0.4.37 → 0.4.39

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 +83 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.37",
3
+ "version": "0.4.39",
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
@@ -77,9 +77,11 @@ async function confirmCost(api, args, spec) {
77
77
  let book = null;
78
78
  try { book = (await api.get(orgId ? `/api/v1/orgs/${orgId}/pricing` : '/api/v1/pricing')).data; }
79
79
  catch { try { book = (await api.get('/api/v1/pricing')).data; } catch { book = null; } }
80
+ let cost = null;
81
+ if (book) { try { cost = estimateCost(book, spec); } catch { cost = null; } } // never fail the gate on an estimate error
80
82
  return {
81
83
  requiresConfirmation: true,
82
- cost: book ? estimateCost(book, spec) : null,
84
+ cost,
83
85
  priceBook: orgId ? `org:${orgId}` : 'global-defaults',
84
86
  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.',
85
87
  };
@@ -140,10 +142,10 @@ export const TOOLS = [
140
142
  // Honest support matrix: what THIS build does, not the roadmap. `create`
141
143
  // is true, false, or 'via_folder' (created through neo_deploy_folder).
142
144
  const resourceTypes = [
143
- { type: 'web', create: true, deploy: true, scale: true, logs: true, variables: true, delete: true },
144
- { type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
145
- { 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' },
146
- { 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)' },
145
+ { type: 'web', create: true, deploy: true, scale: true, update: true, domains: true, health: true, autoscale: true, logs: true, variables: true, usage: true, delete: true, note: 'neo_get/update_service (compute/replicas/autoscale/port/command/domain/tls/path/health) + scale + add/remove_service_domain + get_logs + service_usage' },
146
+ { type: 'template', create: true, deploy: true, scale: false, update: true, logs: true, variables: true, delete: true },
147
+ { type: 'static_site', create: 'via_folder', deploy: true, scale: false, update: true, domains: true, logs: true, variables: true, delete: true, nativeStatic: true, note: 'neo_deploy_folder serviceType=static_site with buildCommand/publishDirectory → native nginx (0.4.27); neo_update_service for domain/tls/path' },
148
+ { type: 'worker', create: 'via_folder', deploy: true, scale: true, update: true, health: true, autoscale: true, logs: true, variables: true, usage: true, delete: true, note: 'neo_deploy_folder serviceType=worker (no public ingress) + neo_update_service (compute/replicas/autoscale/command/health)' },
147
149
  { type: 'database', create: true, deploy: true, get: true, list: true, update: true, backup: true, restore: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/deploy/list/get/update(resize)/delete_database + backup + restore (postgres, local backups) + verify (SQL round-trip) + enable/disable_db_public_access (handoff 4.4)' },
148
150
  { type: 'queue', create: true, deploy: true, get: true, list: true, update: true, verify: true, publicAccess: true, delete: true, note: 'neo_create/deploy/list/get/update/verify(publish→persist)/delete_queue + enable/disable_queue_public_access (handoff 4.5)' },
149
151
  { type: 'cron', create: true, get: true, list: true, run: true, runs: true, questionnaire: true, delete: true, note: 'neo_cron_questionnaire (guided) + create/list/get/run/list_runs/delete_cron (handoff 4.6)' },
@@ -155,7 +157,7 @@ export const TOOLS = [
155
157
  controlPlane,
156
158
  resourceTypes,
157
159
  // Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
158
- 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', 'cron_questionnaire', 'presign_object', 'backup', 'restore', 'delete', 'delete_project', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
160
+ 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', 'add_domain', 'remove_domain', 'logs', 'get_variables', 'set_variable', 'estimate', 'scan_repo', 'bind', 'unbind', 'run_cron', 'cron_questionnaire', 'presign_object', 'backup', 'restore', 'delete', 'delete_project', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
159
161
  bindings: { supported: true, targets: ['neo_db', 'neo_queue', 'neo_object_storage'], writeOnly: true, note: 'neo_bind_resource + neo_plan_stack bindings inject DATABASE_URL / NATS_URL (+ NATS_HOST/PORT/USER/PASSWORD) / S3_* server-side at deploy; secrets never returned (handoff 5)' },
160
162
  sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
161
163
  environments: ['staging', 'production'],
@@ -240,6 +242,81 @@ export const TOOLS = [
240
242
  return (await api.get(`/api/v1/projects/${args.projectId}/apps`)).data;
241
243
  },
242
244
  },
245
+ {
246
+ name: 'neo_get_service',
247
+ description: 'Get one service/app by id — its type, compute, replicas/autoscale, port, command, domain/TLS/custom domains, health-check paths, and env keys (secret values redacted). Redeploy for config changes to take effect.',
248
+ mutating: false,
249
+ schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)') }, required: ['appId'] },
250
+ async call(api, args) {
251
+ return redactSecrets((await api.get(`/api/v1/apps/${args.appId}`)).data);
252
+ },
253
+ },
254
+ {
255
+ name: 'neo_update_service',
256
+ description: 'Update a service/app config (handoff §4.1/§4.2). Exposes compute (computeTier OR cpuLimit+memoryLimit), replicas, autoscale, port, command, domain, tls, ingress path, and health/liveness/readiness/startup checks. Cost-changing fields (compute/replicas/autoscale) require confirm:true — without it the new cost is returned and nothing is applied. Redeploy the service for changes to take effect. Omitted fields are left unchanged.',
257
+ mutating: true,
258
+ schema: {
259
+ type: 'object',
260
+ properties: {
261
+ appId: str('Service/app id'),
262
+ computeTier: str('Bundled compute tier (micro | small | medium | large | xlarge)'),
263
+ cpuLimit: str('Custom CPU limit, e.g. "500m" (use with memoryLimit; unbundled)'),
264
+ memoryLimit: str('Custom memory limit, e.g. "512Mi" (unbundled)'),
265
+ replicas: int('Fixed replica count'),
266
+ autoscaleMin: int('Autoscale min replicas'),
267
+ autoscaleMax: int('Autoscale max replicas'),
268
+ autoscaleCpu: int('Autoscale target CPU %'),
269
+ autoscaleMemory: int('Autoscale target memory %'),
270
+ port: int('Container port'),
271
+ command: { type: 'array', items: { type: 'string' }, description: 'Override the image command (argv)' },
272
+ domain: str('Primary domain ("" clears it back to the default host)'),
273
+ tls: { type: 'boolean', description: 'Enable/disable managed TLS on the domain' },
274
+ path: str('Ingress path prefix'),
275
+ healthPath: str('HTTP health-check path'),
276
+ livenessPath: str('Liveness probe path'),
277
+ readinessPath: str('Readiness probe path'),
278
+ startupPath: str('Startup probe path'),
279
+ healthTimeoutSeconds: int('Health-check timeout (s)'),
280
+ healthGraceSeconds: int('Health-check grace period (s)'),
281
+ confirm: { type: 'boolean', description: 'Approve a cost change (compute/replicas/autoscale): without it the new cost is returned and nothing is applied.' },
282
+ },
283
+ required: ['appId'],
284
+ },
285
+ async call(api, args) {
286
+ const { appId, confirm, orgId, computeTier, ...rest } = args;
287
+ const body = { ...rest };
288
+ if (computeTier) { body.billingMode = 'bundled'; body.cpuLimit = computeTier; }
289
+ else if (args.cpuLimit || args.memoryLimit) { body.billingMode = 'unbundled'; }
290
+ const costChanging = computeTier || args.cpuLimit || args.memoryLimit || args.replicas !== undefined || args.autoscaleMax !== undefined;
291
+ if (costChanging && confirm !== true) {
292
+ const gate = await confirmCost(api, args, { computeTier, replicas: args.replicas ?? args.autoscaleMax });
293
+ if (gate) return { ...gate, change: 'update service compute/scaling', message: 'This changes cost — re-call with confirm:true to apply.' };
294
+ }
295
+ const { status, data } = await api.put(`/api/v1/apps/${appId}`, body);
296
+ return redactSecrets(result(status, data));
297
+ },
298
+ },
299
+ {
300
+ name: 'neo_add_service_domain',
301
+ description: 'Attach an additional custom ingress domain to a service (handoff §4.1). Point the domain\'s DNS at the platform first; managed TLS is issued automatically. The default platform host is preserved.',
302
+ mutating: true,
303
+ schema: { type: 'object', properties: { appId: str('Service/app id'), domain: str('Custom domain host, e.g. "app.example.com"') }, required: ['appId', 'domain'] },
304
+ async call(api, args) {
305
+ const { status, data } = await api.post(`/api/v1/apps/${args.appId}/domains`, { domain: args.domain });
306
+ return result(status, data);
307
+ },
308
+ },
309
+ {
310
+ name: 'neo_remove_service_domain',
311
+ description: 'Detach a custom domain from a service. The default platform host and other domains are untouched.',
312
+ mutating: true,
313
+ destructive: true,
314
+ schema: { type: 'object', properties: { appId: str('Service/app id'), domain: str('Custom domain host to remove') }, required: ['appId', 'domain'] },
315
+ async call(api, args) {
316
+ const { status, data } = await api.del(`/api/v1/apps/${args.appId}/domains`, { body: { domain: args.domain } });
317
+ return result(status, data);
318
+ },
319
+ },
243
320
  {
244
321
  name: 'neo_get_deployment',
245
322
  description: 'Get a deployment by id (poll this after neo_deploy until status is terminal).',