@biznetgio/neo-app 0.4.65 → 0.4.67
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 +2 -1
- package/src/mcp/tools.js +33 -5
- package/src/skill/SKILL.md +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biznetgio/neo-app",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.67",
|
|
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": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "node --test",
|
|
19
19
|
"e2e": "node e2e/mcp-regression.mjs",
|
|
20
|
+
"release:status": "node scripts/release-status.mjs",
|
|
20
21
|
"prepublishOnly": "node --test"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
package/src/mcp/tools.js
CHANGED
|
@@ -178,7 +178,7 @@ async function confirmCost(api, args, spec) {
|
|
|
178
178
|
// { requiresConfirmation } — a preview; CREATES NOTHING
|
|
179
179
|
// A confirmationId re-plays an already-approved confirmation; confirm:true is a one-call
|
|
180
180
|
// shortcut that mints + auto-approves one; neither returns the cost + a pending confirmationId.
|
|
181
|
-
async function billableGate(api, { projectId, kind, name, confirmationId, confirm }, spec) {
|
|
181
|
+
async function billableGate(api, { projectId, kind, name, confirmationId, confirm, environment }, spec) {
|
|
182
182
|
if (confirmationId) return { query: `?confirmationId=${encodeURIComponent(confirmationId)}` };
|
|
183
183
|
|
|
184
184
|
// Estimate the cost (best-effort) for display and the confirmation summary.
|
|
@@ -197,7 +197,7 @@ async function billableGate(api, { projectId, kind, name, confirmationId, confir
|
|
|
197
197
|
// from failing an otherwise-fine create (same spirit as the api-layer idempotent retry).
|
|
198
198
|
let mint;
|
|
199
199
|
for (let attempt = 1; ; attempt++) {
|
|
200
|
-
try { mint = (await api.post('/api/v1/confirmations', { action: 'billable_create', projectId, kind, name, estimatedCost: costNote })).data; break; }
|
|
200
|
+
try { mint = (await api.post('/api/v1/confirmations', { action: 'billable_create', projectId, kind, name, environment, estimatedCost: costNote })).data; break; }
|
|
201
201
|
catch (e) {
|
|
202
202
|
if (attempt < 3 && (e?.status >= 500 || e?.status === 0)) { await new Promise((r) => setTimeout(r, 300 * attempt)); continue; }
|
|
203
203
|
throw e;
|
|
@@ -313,7 +313,8 @@ export const TOOLS = [
|
|
|
313
313
|
controlPlane,
|
|
314
314
|
resourceTypes,
|
|
315
315
|
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
|
|
316
|
-
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'],
|
|
316
|
+
lifecycleVerbs: ['plan', 'apply', 'plan_promotion', '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', 'domain_status', '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'],
|
|
317
|
+
promotion: { supported: true, byCopy: true, tool: 'neo_plan_promotion', note: 'staging→production promotion by copy (JNG #112 §8.7): an immutable costed prod stack plan; applied with neo_apply_stack; copies build/compute/env/bound foundations, never custom domains or secret values, never mutates the source.' },
|
|
317
318
|
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)' },
|
|
318
319
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
319
320
|
environments: ['staging', 'production'],
|
|
@@ -509,6 +510,15 @@ export const TOOLS = [
|
|
|
509
510
|
return result(status, data);
|
|
510
511
|
},
|
|
511
512
|
},
|
|
513
|
+
{
|
|
514
|
+
name: 'neo_get_domain_status',
|
|
515
|
+
description: 'Check a custom domain\'s readiness to a TERMINAL state (JNG #112 §8.6): whether it is attached, whether DNS resolves to the expected CNAME target (with an expected-vs-observed diagnostic), whether managed TLS has been issued, and whether HTTPS actually serves a valid certificate for the host. Poll this after neo_add_service_domain + redeploy until ready:true — do NOT assume "attach accepted" means live. Returns {attached, expectedTarget, dns:{pointed,observed,diagnostic}, tls:{issued}, https:{ok,statusCode,certHostnameValid}, ready}.',
|
|
516
|
+
mutating: false,
|
|
517
|
+
schema: { type: 'object', properties: { appId: str('Service/app id'), domain: str('Custom domain host to check, e.g. "app.example.com"') }, required: ['appId', 'domain'] },
|
|
518
|
+
async call(api, args) {
|
|
519
|
+
return (await api.get(`/api/v1/apps/${args.appId}/domains/status`, { query: { domain: args.domain } })).data;
|
|
520
|
+
},
|
|
521
|
+
},
|
|
512
522
|
{
|
|
513
523
|
name: 'neo_remove_service_domain',
|
|
514
524
|
description: 'Detach a custom domain from a service (JNG #112). SAFE BY DEFAULT: without confirm:true it returns a preview (what will be detached) and removes nothing; re-call with confirm:true to detach — dropping its ingress + managed TLS on the next deploy. The default platform host and other domains are untouched.',
|
|
@@ -1088,7 +1098,7 @@ export const TOOLS = [
|
|
|
1088
1098
|
async call(api, args) {
|
|
1089
1099
|
const environment = requireEnv(args.environment, args.allowCrossEnvironment); // explicit, else the session pin (JNG #136)
|
|
1090
1100
|
validateDbArgs(args); // reject sub-10Gi / EOL config before pricing (JNG #132)
|
|
1091
|
-
const gate = await billableGate(api, { projectId: args.projectId, kind: 'database', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm }, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas });
|
|
1101
|
+
const gate = await billableGate(api, { projectId: args.projectId, kind: 'database', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm, environment }, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas });
|
|
1092
1102
|
if (gate.requiresConfirmation) return { environment, ...gate };
|
|
1093
1103
|
const { projectId, confirm, confirmationId, orgId, ...body } = args;
|
|
1094
1104
|
body.environment = environment; // send the resolved env so the pin actually targets it
|
|
@@ -1261,7 +1271,7 @@ export const TOOLS = [
|
|
|
1261
1271
|
},
|
|
1262
1272
|
async call(api, args) {
|
|
1263
1273
|
const environment = requireEnv(args.environment, args.allowCrossEnvironment); // explicit, else the session pin (JNG #136)
|
|
1264
|
-
const gate = await billableGate(api, { projectId: args.projectId, kind: 'queue', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm }, { storageGB: gbFromSize(args.volumeSize) });
|
|
1274
|
+
const gate = await billableGate(api, { projectId: args.projectId, kind: 'queue', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm, environment }, { storageGB: gbFromSize(args.volumeSize) });
|
|
1265
1275
|
if (gate.requiresConfirmation) return { environment, ...gate };
|
|
1266
1276
|
const { projectId, confirm, confirmationId, orgId, ...body } = args;
|
|
1267
1277
|
body.environment = environment; // send the resolved env so the pin actually targets it
|
|
@@ -1755,6 +1765,24 @@ export const TOOLS = [
|
|
|
1755
1765
|
return result(status, data);
|
|
1756
1766
|
},
|
|
1757
1767
|
},
|
|
1768
|
+
{
|
|
1769
|
+
name: 'neo_plan_promotion',
|
|
1770
|
+
description:
|
|
1771
|
+
'Plan a COPY of a non-production service (and the foundations it binds) INTO production — staging→prod promotion by copy (JNG #112 §8.7). Zero-write: it returns an immutable costed production stack plan (planId, planHash, incremental cost, expiry) — it creates nothing and NEVER changes the source service. Copies the build/image spec, compute, port, command, replicas, non-secret env, and its bound database/queue/bucket (fresh prod copies + bindings). Does NOT copy custom domains, and never copies secret values — the response `secrets.requiresReentry` lists secret env keys to set with neo_set_service_variable after prod is healthy. Review the cost + graph with the user, then apply with neo_apply_stack (planId + planHash). Returns distinct production resources; the staging service stays live and unchanged.',
|
|
1772
|
+
mutating: false,
|
|
1773
|
+
schema: {
|
|
1774
|
+
type: 'object',
|
|
1775
|
+
properties: {
|
|
1776
|
+
projectId: str('Project id of the source (non-production) service'),
|
|
1777
|
+
appId: str('The source service/app id to promote (from neo_list_services)'),
|
|
1778
|
+
},
|
|
1779
|
+
required: ['projectId', 'appId'],
|
|
1780
|
+
},
|
|
1781
|
+
async call(api, args) {
|
|
1782
|
+
const { status, data } = await api.post(`/api/v1/projects/${args.projectId}/promotions`, { appId: args.appId });
|
|
1783
|
+
return result(status, data);
|
|
1784
|
+
},
|
|
1785
|
+
},
|
|
1758
1786
|
{
|
|
1759
1787
|
name: 'neo_get_operation',
|
|
1760
1788
|
description: 'Get the status of a stack apply (the operationId/planId from neo_apply_stack). Overall status: applying | applied | partial | failed. Per-resource status: creating | deploying | deployed | ready | blocked | failed, plus a `healthy` flag + `url` for services (apply probes the public URL — "ready" means it actually responded) and `blockedBy` when a consumer\'s foundation failed. Poll until terminal.',
|
package/src/skill/SKILL.md
CHANGED
|
@@ -23,6 +23,20 @@ NEO App is Biznet Gio's managed-app platform. You operate it two ways:
|
|
|
23
23
|
- Prefer **staging** for every change: deploy, test, and read logs on staging before touching production.
|
|
24
24
|
- Only deploy to production when the user explicitly asks (pin `production` deliberately), and expect it to require approval (see below).
|
|
25
25
|
|
|
26
|
+
## Analyzing a repo & asking questions (vibecoder-friendly)
|
|
27
|
+
|
|
28
|
+
- `neo_scan_repo` detects deployable components, service/build types, ports, and managed-resource `deps` (database/queue/object_storage). Treat manifests as **optional evidence, not prerequisites** — a repo without a lockfile or Dockerfile is still deployable.
|
|
29
|
+
- **Ask only on genuine ambiguity** (several deployable components, or an unclear target). When you must ask, ask **one short question at a time**, and give the **recommended answer first**. Don't batch a questionnaire.
|
|
30
|
+
- **Follow the user's language.** If they write in Bahasa Indonesia, ask in clear Bahasa Indonesia.
|
|
31
|
+
- **Don't ask a vibecoder about platform internals** (ingress, build contexts, runtime classes, Dockerfile paths, resource classes). Infer sensible defaults and proceed; expose those only if an advanced user asks.
|
|
32
|
+
- Preserve the selected root path, build context, and Dockerfile path as-is. Store the confirmed topology **with the plan** (`neo_plan_stack` resource `deps` + bindings) — never modify the repo to record it.
|
|
33
|
+
- Wire detected dependencies: if a component `deps` on a queue/database, create it and bind it, or the plan is flagged `incompleteTopology` and apply refuses it.
|
|
34
|
+
|
|
35
|
+
## Promotion (staging → production, by copy)
|
|
36
|
+
|
|
37
|
+
- To copy a healthy staging service to production, use `neo_plan_promotion({projectId, appId})` → review the incremental cost + graph with the user → apply with `neo_apply_stack` (planId + planHash). It never changes staging.
|
|
38
|
+
- After production is healthy: re-enter secret env (`secrets.requiresReentry`) with `neo_set_service_variable(secret:true)`, then attach any custom domain (`neo_add_service_domain`) and confirm it with `neo_get_domain_status`. Domains and secret values are never copied.
|
|
39
|
+
|
|
26
40
|
## Deployments
|
|
27
41
|
|
|
28
42
|
- Deploy with `neo_deploy` (or `neo-app deploy`), passing the `projectId` and environment.
|