@biznetgio/neo-app 0.4.35 → 0.4.36

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 +26 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.35",
3
+ "version": "0.4.36",
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
@@ -333,7 +333,7 @@ export const TOOLS = [
333
333
  },
334
334
  {
335
335
  name: 'neo_scale_service',
336
- description: 'Scale a service to N replicas. May return pending_approval under org policy.',
336
+ description: 'Scale a service to N replicas. Scaling changes cost, so without confirm:true it returns the new estimated cost and scales nothing (re-call with confirm:true). May return pending_approval under org policy.',
337
337
  mutating: true,
338
338
  schema: {
339
339
  type: 'object',
@@ -341,10 +341,22 @@ export const TOOLS = [
341
341
  projectId: str('Project id'),
342
342
  service: str('Service/app name'),
343
343
  replicas: int('Desired replica count (>=0)'),
344
+ confirm: { type: 'boolean', description: 'Approve the cost change: without confirm:true the tool returns the new cost and scales nothing.' },
344
345
  },
345
346
  required: ['projectId', 'service', 'replicas'],
346
347
  },
347
348
  async call(api, args) {
349
+ if (args.confirm !== true) {
350
+ let computeTier;
351
+ try {
352
+ const apps = (await api.get(`/api/v1/projects/${args.projectId}/apps`)).data;
353
+ const list = Array.isArray(apps) ? apps : (apps.applications || []);
354
+ const svc = list.find((a) => a.name === args.service || a.id === args.service);
355
+ computeTier = svc && (svc.cpuLimit || svc.computeTier);
356
+ } catch { /* best effort — gate regardless */ }
357
+ const gate = await confirmCost(api, args, { computeTier, replicas: args.replicas });
358
+ if (gate) return { ...gate, change: `scale "${args.service}" to ${args.replicas} replica(s)`, message: 'Scaling changes cost — show the user and re-call with confirm:true to apply.' };
359
+ }
348
360
  const { status, data } = await api.post(
349
361
  `/api/v1/projects/${args.projectId}/services/${args.service}/scale`,
350
362
  { replicas: args.replicas }
@@ -641,10 +653,16 @@ export const TOOLS = [
641
653
  databaseId: str('Database id'),
642
654
  volumeSize: str('New storage size, e.g. "20Gi" — must be >= current (grow-only)'),
643
655
  replicas: int('Read/failover replicas (CNPG/postgres); total instances = 1 + replicas'),
656
+ confirm: { type: 'boolean', description: 'Approve the cost change: without confirm:true a storage/replica resize returns the new cost and applies nothing.' },
644
657
  },
645
658
  required: ['databaseId'],
646
659
  },
647
660
  async call(api, args) {
661
+ const costChanging = args.volumeSize || args.replicas !== undefined;
662
+ if (costChanging && args.confirm !== true) {
663
+ const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas });
664
+ if (gate) return { ...gate, change: 'resize database (storage/replicas)', message: 'Resizing changes cost — re-call with confirm:true to apply.' };
665
+ }
648
666
  const body = {};
649
667
  if (args.volumeSize) body.volumeSize = args.volumeSize;
650
668
  if (args.replicas !== undefined) body.replicas = args.replicas;
@@ -807,11 +825,17 @@ export const TOOLS = [
807
825
  dlqEnabled: { type: 'boolean', description: 'Enable/disable the dead-letter queue' },
808
826
  volumeSize: str('New storage size, e.g. "20Gi" — must be >= current (grow-only)'),
809
827
  retention: { type: 'object', description: 'Retention policy {maxAgeSeconds, maxBytes, maxMsgs}' },
828
+ confirm: { type: 'boolean', description: 'Approve the cost change: without confirm:true a storage/plan change returns the new cost and applies nothing. DLQ/retention-only edits apply directly.' },
810
829
  },
811
830
  required: ['queueId'],
812
831
  },
813
832
  async call(api, args) {
814
- const { queueId, ...body } = args;
833
+ const costChanging = args.volumeSize || args.planClass;
834
+ if (costChanging && args.confirm !== true) {
835
+ const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize) });
836
+ if (gate) return { ...gate, change: 'update queue (storage/plan)', message: 'This changes cost — re-call with confirm:true to apply.' };
837
+ }
838
+ const { queueId, confirm, orgId, ...body } = args;
815
839
  const { status, data } = await api.put(`/api/v1/queues/${queueId}`, body);
816
840
  return redactSecrets(result(status, data));
817
841
  },