@biznetgio/neo-app 0.4.24 → 0.4.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
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/api.js CHANGED
@@ -58,6 +58,18 @@ export class NeoApi {
58
58
  return { status: res.status, data: payload };
59
59
  }
60
60
 
61
+ // postForm sends a multipart/form-data body (FormData); fetch sets the boundary.
62
+ async postForm(path, form) {
63
+ const headers = {};
64
+ if (this.token) headers.Authorization = `Bearer ${this.token}`;
65
+ const res = await fetch(this.baseUrl + path, { method: 'POST', headers, body: form });
66
+ const payload = await res.json().catch(() => null);
67
+ if (res.status >= 400) {
68
+ throw new ApiError(res.status, (payload && payload.error) || res.statusText, payload);
69
+ }
70
+ return { status: res.status, data: payload };
71
+ }
72
+
61
73
  get(path, opts) {
62
74
  return this.request('GET', path, opts);
63
75
  }
package/src/mcp/tools.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // map). Read-only vs mutating is annotated; mutating tools surface the server's
3
3
  // 202 pending_approval result verbatim instead of treating it as success/failure.
4
4
 
5
- import { resolve } from 'node:path';
6
- import { statSync } from 'node:fs';
5
+ import { resolve, basename } from 'node:path';
6
+ import { statSync, readFileSync } from 'node:fs';
7
7
  import { deploySource } from '../deploySource.js';
8
8
  import { pkgVersion } from '../util.js';
9
9
  import { estimateCost } from '../pricing.js';
@@ -94,10 +94,10 @@ export const TOOLS = [
94
94
  { type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
95
95
  { type: 'static_site', create: 'via_folder', deploy: true, scale: false, logs: true, variables: true, delete: true, note: 'neo_deploy_folder serviceType=static_site (auto-detected for static-only folders); auto-detection from a repo/monorepo is still TODO (handoff 6)' },
96
96
  { 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, delete: true, note: 'neo_create/list/get/update(resize)/delete_database (0.4.15-0.4.21); backup/restore still TODO (handoff 4.4)' },
98
- { type: 'queue', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_queue (0.4.15); update/plan still TODO (handoff 4.5)' },
97
+ { type: 'database', create: true, get: true, list: true, update: true, backup: true, delete: true, note: 'neo_create/list/get/update(resize)/delete_database + backup (list/create/run); 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)' },
99
99
  { 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
- { type: 'object_storage', create: true, get: true, list: true, objects: true, delete: true, note: 'neo_create/list/get/delete_bucket + list/delete objects (0.4.15-0.4.21); object upload still TODO (handoff 4.7)' },
100
+ { 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
101
  ];
102
102
 
103
103
  return {
@@ -517,6 +517,45 @@ export const TOOLS = [
517
517
  return redactSecrets(result(status, data));
518
518
  },
519
519
  },
520
+ {
521
+ name: 'neo_list_backups',
522
+ description: 'List configured database backups in a project (handoff 4.4): schedule, destination, last run status.',
523
+ mutating: false,
524
+ schema: { type: 'object', properties: { projectId: str('Project id') }, required: ['projectId'] },
525
+ async call(api, args) {
526
+ return redactSecrets((await api.get(`/api/v1/projects/${args.projectId}/backups`)).data);
527
+ },
528
+ },
529
+ {
530
+ name: 'neo_create_backup',
531
+ description: 'Configure a backup for a managed database (handoff 4.4). `cron` sets a schedule (empty = manual only); `destination` is local (default) or s3. Trigger it with neo_run_backup.',
532
+ mutating: true,
533
+ schema: {
534
+ type: 'object',
535
+ properties: {
536
+ projectId: str('Project id'),
537
+ databaseId: str('Database id to back up'),
538
+ cron: str('Cron schedule (optional; empty = manual)'),
539
+ destination: { type: 'string', enum: ['local', 's3'], description: 'local (default) | s3' },
540
+ },
541
+ required: ['projectId', 'databaseId'],
542
+ },
543
+ async call(api, args) {
544
+ const { projectId, ...body } = args;
545
+ const { status, data } = await api.post(`/api/v1/projects/${projectId}/backups`, body);
546
+ return redactSecrets(result(status, data));
547
+ },
548
+ },
549
+ {
550
+ name: 'neo_run_backup',
551
+ description: 'Trigger a configured database backup to run now (handoff 4.4).',
552
+ mutating: true,
553
+ schema: { type: 'object', properties: { backupId: str('Backup id (from neo_list_backups)') }, required: ['backupId'] },
554
+ async call(api, args) {
555
+ const { status, data } = await api.post(`/api/v1/backups/${args.backupId}/run`);
556
+ return redactSecrets(result(status, data));
557
+ },
558
+ },
520
559
  {
521
560
  name: 'neo_create_queue',
522
561
  description: 'Create a managed queue in a project (handoff 4.5). Credentials redacted. May return pending_approval.',
@@ -558,6 +597,27 @@ export const TOOLS = [
558
597
  return redactSecrets((await api.get(`/api/v1/queues/${args.queueId}`)).data);
559
598
  },
560
599
  },
600
+ {
601
+ name: 'neo_update_queue',
602
+ description: 'Update a managed queue (handoff 4.5): plan class, DLQ, retention, and storage (grow-only — a smaller volumeSize is rejected). Redeploy the queue for it to take effect.',
603
+ mutating: true,
604
+ schema: {
605
+ type: 'object',
606
+ properties: {
607
+ queueId: str('Queue id'),
608
+ planClass: str('dev | standard'),
609
+ dlqEnabled: { type: 'boolean', description: 'Enable/disable the dead-letter queue' },
610
+ volumeSize: str('New storage size, e.g. "20Gi" — must be >= current (grow-only)'),
611
+ retention: { type: 'object', description: 'Retention policy {maxAgeSeconds, maxBytes, maxMsgs}' },
612
+ },
613
+ required: ['queueId'],
614
+ },
615
+ async call(api, args) {
616
+ const { queueId, ...body } = args;
617
+ const { status, data } = await api.put(`/api/v1/queues/${queueId}`, body);
618
+ return redactSecrets(result(status, data));
619
+ },
620
+ },
561
621
  {
562
622
  name: 'neo_create_cron',
563
623
  description: 'Create a scheduled job (cron) in a project (handoff 4.6). Target is either an existing service command (mode=app_command, serviceId+command) or an HTTP endpoint (mode=http_endpoint, url+method). Schedule via scheduleType (cron|interval|once|seconds) + schedule/intervalMinutes. May return pending_approval.',
@@ -693,6 +753,31 @@ export const TOOLS = [
693
753
  return (await api.get(`/api/v1/buckets/${args.bucketId}/objects/presign`, { query: { object: args.object } })).data;
694
754
  },
695
755
  },
756
+ {
757
+ name: 'neo_upload_bucket_object',
758
+ description: 'Upload a LOCAL file to an object-storage bucket (handoff 4.7 — object put). The path is on the machine running this MCP server. objectName defaults to the file\'s basename.',
759
+ mutating: true,
760
+ schema: {
761
+ type: 'object',
762
+ properties: {
763
+ bucketId: str('Bucket id'),
764
+ filePath: str('Local file path to upload'),
765
+ objectName: str('Object key/name in the bucket (optional; default the file basename)'),
766
+ },
767
+ required: ['bucketId', 'filePath'],
768
+ },
769
+ async call(api, args) {
770
+ const p = resolve(args.filePath);
771
+ const st = statSync(p);
772
+ if (!st.isFile()) throw new Error(`not a file: ${args.filePath}`);
773
+ const name = args.objectName || basename(p);
774
+ const form = new FormData();
775
+ form.append('object_name', name);
776
+ form.append('file', new Blob([readFileSync(p)]), name);
777
+ const { status, data } = await api.postForm(`/api/v1/buckets/${args.bucketId}/objects`, form);
778
+ return result(status, data);
779
+ },
780
+ },
696
781
  // --- Typed bindings (handoff 5): attach a DB/queue to a consumer service. The
697
782
  // connection string is injected as an env var SERVER-SIDE at the consumer's next
698
783
  // deploy (marked a secret) — it is never returned to the agent (write-only). ---