@biznetgio/neo-app 0.4.31 → 0.4.32

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 +38 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
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
@@ -65,13 +65,17 @@ async function confirmCost(api, args, spec) {
65
65
  };
66
66
  }
67
67
 
68
- const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey)/i;
68
+ const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey|authorization)/i;
69
+ // Deployment snapshots (exact keys) embed rendered configs with plaintext creds — a
70
+ // queue's `--pass`, a DB's POSTGRES_PASSWORD. Redact by EXACT key so we don't also
71
+ // hit resourceId/source-of-truth-style names (substring match would catch "reSOURCEid").
72
+ const SECRET_EXACT = new Set(['source', 'installed']);
69
73
  function redactSecrets(value) {
70
74
  if (Array.isArray(value)) return value.map(redactSecrets);
71
75
  if (value && typeof value === 'object') {
72
76
  const out = {};
73
77
  for (const [k, v] of Object.entries(value)) {
74
- out[k] = SECRET_KEY.test(k) ? '[redacted — credentials are write-only; bind them to a consumer instead]' : redactSecrets(v);
78
+ out[k] = (SECRET_KEY.test(k) || SECRET_EXACT.has(k.toLowerCase())) ? '[redacted — credentials are write-only; bind them to a consumer instead]' : redactSecrets(v);
75
79
  }
76
80
  return out;
77
81
  }
@@ -152,6 +156,25 @@ export const TOOLS = [
152
156
  return (await api.get(`/api/v1/orgs/${orgId}/projects`)).data;
153
157
  },
154
158
  },
159
+ {
160
+ name: 'neo_create_project',
161
+ description: 'Create a new project in an organization — use it for a disposable project (e.g. isolated testing) or a fresh project to deploy into. orgId defaults to your primary org. Returns the new project (id, name).',
162
+ mutating: true,
163
+ schema: {
164
+ type: 'object',
165
+ properties: {
166
+ name: str('Project name'),
167
+ description: str('Project description (optional)'),
168
+ orgId: str('Organization id (optional; defaults to your primary org)'),
169
+ },
170
+ required: ['name'],
171
+ },
172
+ async call(api, args) {
173
+ const orgId = await api.resolveOrgId(args.orgId);
174
+ const { status, data } = await api.post(`/api/v1/orgs/${orgId}/projects`, { name: args.name, description: args.description || '' });
175
+ return result(status, data);
176
+ },
177
+ },
155
178
  {
156
179
  name: 'neo_get_project',
157
180
  description: 'Get one project by id.',
@@ -399,6 +422,7 @@ export const TOOLS = [
399
422
  // root suggests worker; static → static_site; else web.
400
423
  suggestedServiceType: support ? null : (role === 'worker' ? 'worker' : (s.type === 'static' ? 'static_site' : 'web')),
401
424
  suggestedBuildType: s.type === 'docker' ? 'dockerfile' : 'railpack',
425
+ suggestedPort: s.port || undefined, // from the Dockerfile EXPOSE, when present
402
426
  contextDir: s.path || undefined,
403
427
  detectedDependencies: s.deps || [], // database | queue | object_storage, inferred from the manifest
404
428
  // Confidence (handoff 6): a Dockerfile/index.html is high; a worker guess from
@@ -1301,25 +1325,33 @@ export const TOOLS = [
1301
1325
  ]);
1302
1326
  const arr = (x, k) => (Array.isArray(x) ? x : (x && Array.isArray(x[k]) ? x[k] : []));
1303
1327
  const inv = {
1304
- services: arr(apps, 'apps').map((a) => a.name || a.id),
1328
+ services: arr(apps, 'applications').map((a) => a.name || a.id), // list endpoint wraps as {applications:[…]}
1305
1329
  databases: arr(dbs, 'databases').map((d) => d.name || d.id),
1306
1330
  queues: arr(queues, 'queues').map((q) => q.name || q.id),
1307
1331
  crons: arr(crons, 'crons').map((c) => c.name || c.id),
1308
1332
  buckets: arr(buckets, 'buckets').map((b) => b.name || b.id),
1309
1333
  };
1310
1334
  const total = Object.values(inv).reduce((n, a) => n + a.length, 0);
1335
+ // Fail-safe: if any inventory fetch FAILED (null, not empty), we can't prove the
1336
+ // project is empty — treat it as non-empty so the empty-path can't silently
1337
+ // delete hidden resources (the preview must be trustworthy).
1338
+ const inventoryIncomplete = [apps, dbs, queues, crons, buckets].some((x) => x === null);
1311
1339
  const preview = {
1312
1340
  project: proj ? { id: proj.id, name: proj.name, organizationId: proj.organizationId } : { id: pid },
1313
1341
  resourceInventory: inv,
1314
1342
  resourceCount: total,
1315
1343
  meteringSnapshot: usage ? { period: usage.period, total: usage.total ?? usage.totalCost, currency: usage.currency } : null,
1344
+ inventoryIncomplete: inventoryIncomplete || undefined,
1316
1345
  warning: 'IRREVERSIBLE: deletes the project, every resource above, and their data (database/queue volumes included). No refund for prepaid or accrued usage.',
1317
1346
  };
1347
+ // Treat an unverifiable inventory as non-empty — never allow the no-cascade path
1348
+ // when we could not confirm the project is actually empty.
1349
+ const requireCascade = total > 0 || inventoryIncomplete;
1318
1350
  if (args.confirm !== true) {
1319
- return { ...preview, deleted: false, requiresConfirmation: true, message: total > 0 ? 'Show this inventory to the user. To delete, re-call with confirm:true AND cascade:true.' : 'Empty project. To delete, re-call with confirm:true.' };
1351
+ return { ...preview, deleted: false, requiresConfirmation: true, message: requireCascade ? 'Show this inventory to the user. To delete, re-call with confirm:true AND cascade:true.' : 'Empty project. To delete, re-call with confirm:true.' };
1320
1352
  }
1321
- if (total > 0 && args.cascade !== true) {
1322
- return { ...preview, deleted: false, error: 'cascade_required', message: `This project has ${total} resource(s). Re-call with confirm:true AND cascade:true to delete them all.` };
1353
+ if (requireCascade && args.cascade !== true) {
1354
+ return { ...preview, deleted: false, error: 'cascade_required', message: inventoryIncomplete ? 'Could not fully verify the project inventory — re-call with confirm:true AND cascade:true to delete anyway.' : `This project has ${total} resource(s). Re-call with confirm:true AND cascade:true to delete them all.` };
1323
1355
  }
1324
1356
  const { status, data } = await api.del(`/api/v1/projects/${pid}`);
1325
1357
  return { deleted: true, project: preview.project, removed: inv, resourceCount: total, meteringSnapshot: preview.meteringSnapshot, result: result(status, data) };