@biznetgio/neo-app 0.4.13 → 0.4.15

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.13",
3
+ "version": "0.4.15",
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/server.js CHANGED
@@ -29,7 +29,7 @@ export async function runMcpServer() {
29
29
  name: t.name,
30
30
  description: t.description,
31
31
  inputSchema: t.schema,
32
- annotations: { readOnlyHint: !t.mutating, destructiveHint: false },
32
+ annotations: { readOnlyHint: !t.mutating, destructiveHint: !!t.destructive },
33
33
  })),
34
34
  }));
35
35
 
package/src/mcp/tools.js CHANGED
@@ -23,6 +23,23 @@ function result(status, data) {
23
23
  return data;
24
24
  }
25
25
 
26
+ // Credentials are write-only (handoff 4.4/4.5/4.7/5): never hand a raw secret
27
+ // (password, connection string, access/secret key, token) back to the agent.
28
+ // Recursively replace any secret-looking value with a reference marker so the
29
+ // agent still sees the resource's shape and non-secret identifiers.
30
+ const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey)/i;
31
+ function redactSecrets(value) {
32
+ if (Array.isArray(value)) return value.map(redactSecrets);
33
+ if (value && typeof value === 'object') {
34
+ const out = {};
35
+ for (const [k, v] of Object.entries(value)) {
36
+ out[k] = SECRET_KEY.test(k) ? '[redacted — credentials are write-only; bind them to a consumer instead]' : redactSecrets(v);
37
+ }
38
+ return out;
39
+ }
40
+ return value;
41
+ }
42
+
26
43
  export const TOOLS = [
27
44
  {
28
45
  name: 'neo_whoami',
@@ -61,22 +78,22 @@ export const TOOLS = [
61
78
  // Honest support matrix: what THIS build does, not the roadmap. `create`
62
79
  // is true, false, or 'via_folder' (created through neo_deploy_folder).
63
80
  const resourceTypes = [
64
- { type: 'web', create: true, deploy: true, scale: true, logs: true, variables: true, delete: false },
65
- { type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: false },
66
- { type: 'static_site', create: 'via_folder', deploy: true, scale: false, logs: true, variables: true, delete: false, note: 'neo_deploy_folder serviceType=static_site (auto-detected for static-only folders); auto-detection from a repo/monorepo is still TODO (handoff 6)' },
67
- { type: 'worker', create: 'via_folder', deploy: true, scale: true, logs: true, variables: true, delete: false, note: 'neo_deploy_folder serviceType=worker — no public ingress; repo auto-detection still TODO (handoff 6)' },
68
- { type: 'database', create: false, deploy: false, delete: false, note: 'REST exists; no MCP tool yet (handoff 4.4)' },
69
- { type: 'queue', create: false, deploy: false, delete: false, note: 'REST exists; no MCP tool yet (handoff 4.5)' },
70
- { type: 'cron', create: false, deploy: false, delete: false, note: 'REST exists; no MCP tool yet (handoff 4.6)' },
71
- { type: 'object_storage', create: false, deploy: false, delete: false, note: 'REST exists; no MCP tool yet (handoff 4.7)' },
81
+ { type: 'web', create: true, deploy: true, scale: true, logs: true, variables: true, delete: true },
82
+ { type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
83
+ { 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)' },
84
+ { 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)' },
85
+ { type: 'database', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_database (0.4.15); update/plan still TODO (handoff 4.4)' },
86
+ { 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)' },
87
+ { type: 'cron', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_cron (0.4.15); manual-run/history still TODO (handoff 4.6)' },
88
+ { type: 'object_storage', create: true, get: true, list: true, delete: true, note: 'neo_create/list/get/delete_bucket (0.4.15); object put/get still TODO (handoff 4.7)' },
72
89
  ];
73
90
 
74
91
  return {
75
92
  mcp: { name: 'neo-app', version: pkgVersion() },
76
93
  controlPlane,
77
94
  resourceTypes,
78
- // Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no delete, no costed plan/apply yet).
79
- lifecycleVerbs: ['list', 'get', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
95
+ // Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
96
+ lifecycleVerbs: ['create', 'list', 'get', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'delete', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
80
97
  sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
81
98
  environments: ['staging', 'production'],
82
99
  pricing,
@@ -327,4 +344,233 @@ export const TOOLS = [
327
344
  return found;
328
345
  },
329
346
  },
347
+ // --- Managed resources (handoff 4.4-4.7): create/list/get for DB, Queue, Cron,
348
+ // Object Storage. Credentials in responses are redacted (write-only). A member's
349
+ // create may return pending_approval (202) — surfaced verbatim, not an error. ---
350
+ {
351
+ name: 'neo_create_database',
352
+ description: 'Create a managed database in a project (handoff 4.4). Returns the resource (credentials redacted); poll neo_get_database for readiness, then deploy via REST/portal if needed. May return pending_approval.',
353
+ mutating: true,
354
+ schema: {
355
+ type: 'object',
356
+ properties: {
357
+ projectId: str('Project id'),
358
+ name: str('Database name'),
359
+ engine: { type: 'string', description: 'postgres | mysql (engine)' },
360
+ version: str('Engine version, e.g. "16" (optional)'),
361
+ environment: str('production or staging (optional, default production)'),
362
+ volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
363
+ replicas: int('Read/failover replicas for HA (optional; postgres/CNPG only)'),
364
+ deploymentMode: { type: 'string', description: 'single | ha (optional)' },
365
+ },
366
+ required: ['projectId', 'name', 'engine'],
367
+ },
368
+ async call(api, args) {
369
+ const { projectId, ...body } = args;
370
+ const { status, data } = await api.post(`/api/v1/projects/${projectId}/databases`, body);
371
+ return redactSecrets(result(status, data));
372
+ },
373
+ },
374
+ {
375
+ name: 'neo_list_databases',
376
+ description: 'List managed databases in a project (credentials redacted).',
377
+ mutating: false,
378
+ schema: { type: 'object', properties: { projectId: str('Project id') }, required: ['projectId'] },
379
+ async call(api, args) {
380
+ return redactSecrets((await api.get(`/api/v1/projects/${args.projectId}/databases`)).data);
381
+ },
382
+ },
383
+ {
384
+ name: 'neo_get_database',
385
+ description: 'Get one managed database by id, including provisioning/readiness state (credentials redacted — bind them to a consumer instead).',
386
+ mutating: false,
387
+ schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
388
+ async call(api, args) {
389
+ return redactSecrets((await api.get(`/api/v1/databases/${args.databaseId}`)).data);
390
+ },
391
+ },
392
+ {
393
+ name: 'neo_create_queue',
394
+ description: 'Create a managed queue in a project (handoff 4.5). Credentials redacted. May return pending_approval.',
395
+ mutating: true,
396
+ schema: {
397
+ type: 'object',
398
+ properties: {
399
+ projectId: str('Project id'),
400
+ name: str('Queue name'),
401
+ planClass: str('Plan/engine class (optional)'),
402
+ environment: str('production or staging (optional, default production)'),
403
+ volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
404
+ dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional)' },
405
+ },
406
+ required: ['projectId', 'name'],
407
+ },
408
+ async call(api, args) {
409
+ const { projectId, ...body } = args;
410
+ const { status, data } = await api.post(`/api/v1/projects/${projectId}/queues`, body);
411
+ return redactSecrets(result(status, data));
412
+ },
413
+ },
414
+ {
415
+ name: 'neo_list_queues',
416
+ description: 'List managed queues in a project (credentials redacted).',
417
+ mutating: false,
418
+ schema: { type: 'object', properties: { projectId: str('Project id') }, required: ['projectId'] },
419
+ async call(api, args) {
420
+ return redactSecrets((await api.get(`/api/v1/projects/${args.projectId}/queues`)).data);
421
+ },
422
+ },
423
+ {
424
+ name: 'neo_get_queue',
425
+ description: 'Get one managed queue by id, including readiness (credentials redacted).',
426
+ mutating: false,
427
+ schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
428
+ async call(api, args) {
429
+ return redactSecrets((await api.get(`/api/v1/queues/${args.queueId}`)).data);
430
+ },
431
+ },
432
+ {
433
+ name: 'neo_create_cron',
434
+ 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.',
435
+ mutating: true,
436
+ schema: {
437
+ type: 'object',
438
+ properties: {
439
+ projectId: str('Project id'),
440
+ name: str('Job name'),
441
+ mode: { type: 'string', description: 'app_command | http_endpoint' },
442
+ scheduleType: { type: 'string', description: 'cron | interval | once | seconds' },
443
+ schedule: str('Cron expression (when scheduleType=cron), e.g. "0 * * * *"'),
444
+ intervalMinutes: int('Interval in minutes (when scheduleType=interval)'),
445
+ timezone: str('IANA timezone, e.g. "Asia/Jakarta" (optional)'),
446
+ serviceId: str('Target service id (mode=app_command)'),
447
+ image: str('Image to run (mode=app_command, if not targeting a service)'),
448
+ command: { type: 'array', items: { type: 'string' }, description: 'Command argv (mode=app_command)' },
449
+ url: str('Target URL (mode=http_endpoint)'),
450
+ method: str('HTTP method (mode=http_endpoint), e.g. GET/POST'),
451
+ environment: str('production or staging (optional, default production)'),
452
+ },
453
+ required: ['projectId', 'name', 'mode', 'scheduleType'],
454
+ },
455
+ async call(api, args) {
456
+ const { projectId, ...body } = args;
457
+ const { status, data } = await api.post(`/api/v1/projects/${projectId}/crons`, body);
458
+ return redactSecrets(result(status, data));
459
+ },
460
+ },
461
+ {
462
+ name: 'neo_list_crons',
463
+ description: 'List scheduled jobs (crons) in a project.',
464
+ mutating: false,
465
+ schema: { type: 'object', properties: { projectId: str('Project id') }, required: ['projectId'] },
466
+ async call(api, args) {
467
+ return redactSecrets((await api.get(`/api/v1/projects/${args.projectId}/crons`)).data);
468
+ },
469
+ },
470
+ {
471
+ name: 'neo_get_cron',
472
+ description: 'Get one scheduled job (cron) by id, including its schedule and last run.',
473
+ mutating: false,
474
+ schema: { type: 'object', properties: { cronId: str('Cron/scheduled-job id') }, required: ['cronId'] },
475
+ async call(api, args) {
476
+ return redactSecrets((await api.get(`/api/v1/crons/${args.cronId}`)).data);
477
+ },
478
+ },
479
+ {
480
+ name: 'neo_create_bucket',
481
+ description: 'Create an S3-compatible object-storage bucket in a project (handoff 4.7). Access keys are provisioned server-side and are NOT returned (write-only). May return pending_approval.',
482
+ mutating: true,
483
+ schema: {
484
+ type: 'object',
485
+ properties: {
486
+ projectId: str('Project id'),
487
+ name: str('Bucket name'),
488
+ region: str('Region (optional; see neo_list_regions on the portal)'),
489
+ acl: { type: 'string', description: 'private | public-read (optional, default private)' },
490
+ quotaGB: int('Storage quota in GB (optional)'),
491
+ },
492
+ required: ['projectId', 'name'],
493
+ },
494
+ async call(api, args) {
495
+ const { projectId, ...body } = args;
496
+ const { status, data } = await api.post(`/api/v1/projects/${projectId}/buckets`, body);
497
+ return redactSecrets(result(status, data));
498
+ },
499
+ },
500
+ {
501
+ name: 'neo_list_buckets',
502
+ description: 'List object-storage buckets in a project.',
503
+ mutating: false,
504
+ schema: { type: 'object', properties: { projectId: str('Project id') }, required: ['projectId'] },
505
+ async call(api, args) {
506
+ return redactSecrets((await api.get(`/api/v1/projects/${args.projectId}/buckets`)).data);
507
+ },
508
+ },
509
+ {
510
+ name: 'neo_get_bucket',
511
+ description: 'Get one object-storage bucket by id (access keys not returned — write-only).',
512
+ mutating: false,
513
+ schema: { type: 'object', properties: { bucketId: str('Bucket id') }, required: ['bucketId'] },
514
+ async call(api, args) {
515
+ return redactSecrets((await api.get(`/api/v1/buckets/${args.bucketId}`)).data);
516
+ },
517
+ },
518
+ // --- Cleanup (handoff 3.6): MCP can delete every resource type it can reach, so
519
+ // an agent never leaks a resource it created. These are DESTRUCTIVE and permanent;
520
+ // the server hard-deletes the record and tears down the running workload. ---
521
+ {
522
+ name: 'neo_delete_service',
523
+ description: 'Permanently delete a service (web / worker / static_site) and tear down its running workload. Destructive and irreversible.',
524
+ mutating: true,
525
+ destructive: true,
526
+ schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)') }, required: ['appId'] },
527
+ async call(api, args) {
528
+ const { status, data } = await api.del(`/api/v1/apps/${args.appId}`);
529
+ return result(status, data);
530
+ },
531
+ },
532
+ {
533
+ name: 'neo_delete_database',
534
+ description: 'Permanently delete a managed database and its instance/volume. Destructive and irreversible — the data is not recoverable.',
535
+ mutating: true,
536
+ destructive: true,
537
+ schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
538
+ async call(api, args) {
539
+ const { status, data } = await api.del(`/api/v1/databases/${args.databaseId}`);
540
+ return result(status, data);
541
+ },
542
+ },
543
+ {
544
+ name: 'neo_delete_queue',
545
+ description: 'Permanently delete a managed queue and its instance. Destructive and irreversible.',
546
+ mutating: true,
547
+ destructive: true,
548
+ schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
549
+ async call(api, args) {
550
+ const { status, data } = await api.del(`/api/v1/queues/${args.queueId}`);
551
+ return result(status, data);
552
+ },
553
+ },
554
+ {
555
+ name: 'neo_delete_cron',
556
+ description: 'Permanently delete a scheduled job (cron). Destructive and irreversible.',
557
+ mutating: true,
558
+ destructive: true,
559
+ schema: { type: 'object', properties: { cronId: str('Cron/scheduled-job id') }, required: ['cronId'] },
560
+ async call(api, args) {
561
+ const { status, data } = await api.del(`/api/v1/crons/${args.cronId}`);
562
+ return result(status, data);
563
+ },
564
+ },
565
+ {
566
+ name: 'neo_delete_bucket',
567
+ description: 'Permanently delete an object-storage bucket. Destructive and irreversible — bucket contents are lost.',
568
+ mutating: true,
569
+ destructive: true,
570
+ schema: { type: 'object', properties: { bucketId: str('Bucket id') }, required: ['bucketId'] },
571
+ async call(api, args) {
572
+ const { status, data } = await api.del(`/api/v1/buckets/${args.bucketId}`);
573
+ return result(status, data);
574
+ },
575
+ },
330
576
  ];