@biznetgio/neo-app 0.4.33 → 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 +50 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.33",
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
  },
@@ -1046,7 +1085,7 @@ export const TOOLS = [
1046
1085
  {
1047
1086
  name: 'neo_plan_stack',
1048
1087
  description:
1049
- 'Create an immutable, COSTED plan for a whole stack (services + databases/queues/buckets + scheduled jobs + bindings) WITHOUT creating anything — zero-write (handoff 3.3). Supports kind: app|database|queue|bucket|cron and bindings to neo_db|neo_queue|neo_object_storage. Returns planId, planHash, itemized monthly/hourly cost, and ordered phases. Show the cost to the user for approval, then call neo_apply_stack with planId+planHash. The plan expires in 15 minutes.',
1088
+ 'Create an immutable, COSTED plan for a whole stack (services + databases/queues/buckets + scheduled jobs + bindings) WITHOUT creating anything — zero-write (handoff 3.3). Supports kind: app|database|queue|bucket|cron and bindings to neo_db|neo_queue|neo_object_storage. An http_endpoint cron can target a same-stack web app by name (targetService+targetPath) — its URL is resolved at apply. Returns planId, planHash, itemized monthly/hourly cost, and ordered phases. Show the cost to the user for approval, then call neo_apply_stack with planId+planHash. The plan expires in 15 minutes.',
1050
1089
  mutating: false,
1051
1090
  schema: {
1052
1091
  type: 'object',
@@ -1081,7 +1120,9 @@ export const TOOLS = [
1081
1120
  timezone: str('cron: IANA timezone (optional)'),
1082
1121
  serviceId: str('cron app_command: target service — another resource NAME in this stack, or a literal service id'),
1083
1122
  command: { type: 'array', items: { type: 'string' }, description: 'cron app_command: command argv' },
1084
- url: str('cron http_endpoint: target URL'),
1123
+ url: str('cron http_endpoint: literal target URL'),
1124
+ targetService: str('cron http_endpoint: name of a web app resource IN THIS STACK to hit — its deployed URL is resolved at apply (use instead of a literal url for a same-stack service)'),
1125
+ targetPath: str('cron http_endpoint: path appended to the targetService URL (e.g. "/cron")'),
1085
1126
  method: str('cron http_endpoint: HTTP method'),
1086
1127
  },
1087
1128
  required: ['kind', 'name'],
@@ -1226,8 +1267,7 @@ export const TOOLS = [
1226
1267
  destructive: true,
1227
1268
  schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)') }, required: ['appId'] },
1228
1269
  async call(api, args) {
1229
- const { status, data } = await api.del(`/api/v1/apps/${args.appId}`);
1230
- return result(status, data);
1270
+ return deleteAndConverge(api, `/api/v1/apps/${args.appId}`, args.appId, 'service');
1231
1271
  },
1232
1272
  },
1233
1273
  {
@@ -1237,8 +1277,7 @@ export const TOOLS = [
1237
1277
  destructive: true,
1238
1278
  schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
1239
1279
  async call(api, args) {
1240
- const { status, data } = await api.del(`/api/v1/databases/${args.databaseId}`);
1241
- return result(status, data);
1280
+ return deleteAndConverge(api, `/api/v1/databases/${args.databaseId}`, args.databaseId, 'database');
1242
1281
  },
1243
1282
  },
1244
1283
  {
@@ -1248,8 +1287,7 @@ export const TOOLS = [
1248
1287
  destructive: true,
1249
1288
  schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
1250
1289
  async call(api, args) {
1251
- const { status, data } = await api.del(`/api/v1/queues/${args.queueId}`);
1252
- return result(status, data);
1290
+ return deleteAndConverge(api, `/api/v1/queues/${args.queueId}`, args.queueId, 'queue');
1253
1291
  },
1254
1292
  },
1255
1293
  {
@@ -1259,8 +1297,7 @@ export const TOOLS = [
1259
1297
  destructive: true,
1260
1298
  schema: { type: 'object', properties: { cronId: str('Cron/scheduled-job id') }, required: ['cronId'] },
1261
1299
  async call(api, args) {
1262
- const { status, data } = await api.del(`/api/v1/crons/${args.cronId}`);
1263
- return result(status, data);
1300
+ return deleteAndConverge(api, `/api/v1/crons/${args.cronId}`, args.cronId, 'cron');
1264
1301
  },
1265
1302
  },
1266
1303
  {
@@ -1270,8 +1307,7 @@ export const TOOLS = [
1270
1307
  destructive: true,
1271
1308
  schema: { type: 'object', properties: { bucketId: str('Bucket id') }, required: ['bucketId'] },
1272
1309
  async call(api, args) {
1273
- const { status, data } = await api.del(`/api/v1/buckets/${args.bucketId}`);
1274
- return result(status, data);
1310
+ return deleteAndConverge(api, `/api/v1/buckets/${args.bucketId}`, args.bucketId, 'bucket');
1275
1311
  },
1276
1312
  },
1277
1313
  {