@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.
- package/package.json +1 -1
- package/src/mcp/tools.js +38 -6
package/package.json
CHANGED
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, '
|
|
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:
|
|
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 (
|
|
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) };
|