@biznetgio/neo-app 0.4.34 → 0.4.35

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 +46 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.34",
3
+ "version": "0.4.35",
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
@@ -40,6 +40,26 @@ function requireEnv(env) {
40
40
  return e;
41
41
  }
42
42
 
43
+ // deleteAndConverge deletes a resource, then polls its endpoint until the inventory
44
+ // actually converges (GET → 404), so a delete isn't reported done while the record
45
+ // still exists. Returns a durable operation reference (`del-<id>`) + convergence
46
+ // state (handoff 3.6; 0.4.34 retest §9 "every delete returns a durable operation ID").
47
+ async function deleteAndConverge(api, path, resourceId, kind) {
48
+ const { status, data } = await api.del(path);
49
+ let converged = false, checks = 0;
50
+ for (let i = 0; i < 12; i++) {
51
+ checks++;
52
+ try {
53
+ await api.get(path);
54
+ await new Promise((r) => setTimeout(r, 500)); // still present — keep polling
55
+ } catch (e) {
56
+ if (e && e.status === 404) { converged = true; break; }
57
+ await new Promise((r) => setTimeout(r, 500));
58
+ }
59
+ }
60
+ return { operationId: 'del-' + resourceId, kind, status: (data && data.status) || 'deleted', converged, convergenceChecks: checks };
61
+ }
62
+
43
63
  // gbFromSize parses "10Gi"/"10G"/"10" → 10 (integer GB), else undefined.
44
64
  function gbFromSize(v) {
45
65
  if (v == null) return undefined;
@@ -820,8 +840,16 @@ export const TOOLS = [
820
840
  ],
821
841
  branches: {
822
842
  app_command: 'Provide serviceId (an existing service) OR image, plus command (argv array).',
823
- http_endpoint: 'Provide url and method (GET/POST/…).',
843
+ http_endpoint: 'Provide url and method (GET/POST/…). Optional: headers (object), body + contentType, expectedStatus (exact code required for success), timeoutSeconds, retryCount, concurrencyPolicy (Allow|Forbid|Replace).',
824
844
  },
845
+ httpOptions: [
846
+ { field: 'headers', ask: 'Any request headers? (e.g. Authorization) — object', optional: true },
847
+ { field: 'body', ask: 'A request body? (+ contentType, e.g. application/json)', optional: true },
848
+ { field: 'expectedStatus', ask: 'A specific expected HTTP status for success? (default: any 2xx)', optional: true },
849
+ { field: 'timeoutSeconds', ask: 'A per-run timeout (seconds)?', optional: true },
850
+ { field: 'retryCount', ask: 'How many retries on failure?', optional: true },
851
+ { field: 'concurrencyPolicy', ask: 'If a run overruns the next schedule: Allow | Forbid | Replace?', optional: true },
852
+ ],
825
853
  examples: [
826
854
  { intent: 'Run `node cleanup.js` in my web service every night at 2am Jakarta', args: { name: 'nightly-cleanup', mode: 'app_command', scheduleType: 'cron', schedule: '0 2 * * *', timezone: 'Asia/Jakarta', serviceId: '<web service id>', command: ['node', 'cleanup.js'], environment: 'staging' } },
827
855
  { intent: 'Ping a health URL every 15 minutes', args: { name: 'healthcheck', mode: 'http_endpoint', scheduleType: 'interval', intervalMinutes: 15, url: 'https://example.app/health', method: 'GET', environment: 'production' } },
@@ -849,6 +877,13 @@ export const TOOLS = [
849
877
  command: { type: 'array', items: { type: 'string' }, description: 'Command argv (mode=app_command)' },
850
878
  url: str('Target URL (mode=http_endpoint)'),
851
879
  method: str('HTTP method (mode=http_endpoint), e.g. GET/POST'),
880
+ headers: { type: 'object', description: 'HTTP request headers (mode=http_endpoint), e.g. {"Authorization":"Bearer …"}. Secret header values are redacted when read back.' },
881
+ body: str('HTTP request body (mode=http_endpoint)'),
882
+ contentType: str('Content-Type for the body (mode=http_endpoint), e.g. "application/json" — merged into headers'),
883
+ expectedStatus: int('Exact HTTP status a run must receive to count as success (mode=http_endpoint; default: any 2xx)'),
884
+ timeoutSeconds: int('Per-run timeout in seconds (optional) → activeDeadlineSeconds'),
885
+ retryCount: int('Retries on failure (optional) → backoffLimit'),
886
+ concurrencyPolicy: { type: 'string', enum: ['Allow', 'Forbid', 'Replace'], description: 'Overlap policy if a run is still going when the next is due (optional, default Allow)' },
852
887
  environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
853
888
  confirm: { type: 'boolean', description: 'Approve & Create: without confirm:true the tool returns the cost (crons are billed per run — variable) and creates nothing. Re-call with confirm:true to create.' },
854
889
  },
@@ -858,7 +893,11 @@ export const TOOLS = [
858
893
  requireEnv(args.environment);
859
894
  const gate = await confirmCost(api, args, {}); // cron cost is variable (per-run) — gate surfaces the rate
860
895
  if (gate) return gate;
861
- const { projectId, confirm, orgId, ...body } = args;
896
+ // Merge contentType into headers for convenience (unless already set).
897
+ if (args.contentType) {
898
+ args = { ...args, headers: { 'Content-Type': args.contentType, ...(args.headers || {}) } };
899
+ }
900
+ const { projectId, confirm, orgId, contentType, ...body } = args;
862
901
  const { status, data } = await api.post(`/api/v1/projects/${projectId}/crons`, body);
863
902
  return redactSecrets(result(status, data));
864
903
  },
@@ -1228,8 +1267,7 @@ export const TOOLS = [
1228
1267
  destructive: true,
1229
1268
  schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)') }, required: ['appId'] },
1230
1269
  async call(api, args) {
1231
- const { status, data } = await api.del(`/api/v1/apps/${args.appId}`);
1232
- return result(status, data);
1270
+ return deleteAndConverge(api, `/api/v1/apps/${args.appId}`, args.appId, 'service');
1233
1271
  },
1234
1272
  },
1235
1273
  {
@@ -1239,8 +1277,7 @@ export const TOOLS = [
1239
1277
  destructive: true,
1240
1278
  schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
1241
1279
  async call(api, args) {
1242
- const { status, data } = await api.del(`/api/v1/databases/${args.databaseId}`);
1243
- return result(status, data);
1280
+ return deleteAndConverge(api, `/api/v1/databases/${args.databaseId}`, args.databaseId, 'database');
1244
1281
  },
1245
1282
  },
1246
1283
  {
@@ -1250,8 +1287,7 @@ export const TOOLS = [
1250
1287
  destructive: true,
1251
1288
  schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
1252
1289
  async call(api, args) {
1253
- const { status, data } = await api.del(`/api/v1/queues/${args.queueId}`);
1254
- return result(status, data);
1290
+ return deleteAndConverge(api, `/api/v1/queues/${args.queueId}`, args.queueId, 'queue');
1255
1291
  },
1256
1292
  },
1257
1293
  {
@@ -1261,8 +1297,7 @@ export const TOOLS = [
1261
1297
  destructive: true,
1262
1298
  schema: { type: 'object', properties: { cronId: str('Cron/scheduled-job id') }, required: ['cronId'] },
1263
1299
  async call(api, args) {
1264
- const { status, data } = await api.del(`/api/v1/crons/${args.cronId}`);
1265
- return result(status, data);
1300
+ return deleteAndConverge(api, `/api/v1/crons/${args.cronId}`, args.cronId, 'cron');
1266
1301
  },
1267
1302
  },
1268
1303
  {
@@ -1272,8 +1307,7 @@ export const TOOLS = [
1272
1307
  destructive: true,
1273
1308
  schema: { type: 'object', properties: { bucketId: str('Bucket id') }, required: ['bucketId'] },
1274
1309
  async call(api, args) {
1275
- const { status, data } = await api.del(`/api/v1/buckets/${args.bucketId}`);
1276
- return result(status, data);
1310
+ return deleteAndConverge(api, `/api/v1/buckets/${args.bucketId}`, args.bucketId, 'bucket');
1277
1311
  },
1278
1312
  },
1279
1313
  {