@biznetgio/neo-app 0.4.40 → 0.4.41

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.40",
3
+ "version": "0.4.41",
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/api.js CHANGED
@@ -3,6 +3,8 @@
3
3
  // All routes are under /api/v1. A 202 + {status:"pending_approval"} is a normal
4
4
  // result (org approval policy), NOT an error, so only >=400 throws.
5
5
 
6
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7
+
6
8
  export class ApiError extends Error {
7
9
  constructor(status, message, body) {
8
10
  super(message);
@@ -28,12 +30,28 @@ export class NeoApi {
28
30
  const headers = { Accept: accept || 'application/json' };
29
31
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
30
32
  if (body !== undefined) headers['Content-Type'] = 'application/json';
33
+ const init = { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined };
31
34
 
32
- const res = await fetch(url, {
33
- method,
34
- headers,
35
- body: body !== undefined ? JSON.stringify(body) : undefined,
36
- });
35
+ // Transient-failure retry: a long stack apply polls for minutes, and a single
36
+ // dropped connection ("fetch failed") or a brief 502/503/504 shouldn't fail the
37
+ // whole operation. Only retry IDEMPOTENT methods automatically — a POST/PUT that
38
+ // may have been processed must not be silently re-sent.
39
+ const idempotent = method === 'GET' || method === 'DELETE';
40
+ const maxTries = idempotent ? 4 : 1;
41
+ let res, lastErr;
42
+ for (let attempt = 1; attempt <= maxTries; attempt++) {
43
+ try {
44
+ res = await fetch(url, init);
45
+ } catch (e) {
46
+ lastErr = e; // connection never completed — transient
47
+ if (idempotent && attempt < maxTries) { await sleep(250 * 2 ** (attempt - 1)); continue; }
48
+ throw new ApiError(0, `network error contacting ${this.baseUrl}: ${e?.message || e}`, null);
49
+ }
50
+ if (idempotent && [502, 503, 504].includes(res.status) && attempt < maxTries) {
51
+ await sleep(250 * 2 ** (attempt - 1)); continue;
52
+ }
53
+ break;
54
+ }
37
55
 
38
56
  const ctype = res.headers.get('content-type') || '';
39
57
  const isJson = ctype.includes('application/json');
package/src/mcp/tools.js CHANGED
@@ -1242,6 +1242,13 @@ export const TOOLS = [
1242
1242
  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)'),
1243
1243
  targetPath: str('cron http_endpoint: path appended to the targetService URL (e.g. "/cron")'),
1244
1244
  method: str('cron http_endpoint: HTTP method'),
1245
+ headers: { type: 'object', description: 'cron http_endpoint: request headers (secret values redacted on read-back)' },
1246
+ body: str('cron http_endpoint: request body'),
1247
+ contentType: str('cron http_endpoint: Content-Type for the body (merged into headers)'),
1248
+ expectedStatus: int('cron http_endpoint: exact HTTP status required for a run to succeed (default any 2xx)'),
1249
+ timeoutSeconds: int('cron: per-run timeout (seconds)'),
1250
+ retryCount: int('cron: retries on failure'),
1251
+ concurrencyPolicy: { type: 'string', enum: ['Allow', 'Forbid', 'Replace'], description: 'cron: overlap policy (default Allow)' },
1245
1252
  },
1246
1253
  required: ['kind', 'name'],
1247
1254
  },