superacli 1.0.0

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 (69) hide show
  1. package/.env.example +14 -0
  2. package/README.md +173 -0
  3. package/cli/adapters/http.js +72 -0
  4. package/cli/adapters/mcp.js +193 -0
  5. package/cli/adapters/openapi.js +160 -0
  6. package/cli/ask.js +208 -0
  7. package/cli/config.js +133 -0
  8. package/cli/executor.js +117 -0
  9. package/cli/help-json.js +46 -0
  10. package/cli/mcp-local.js +72 -0
  11. package/cli/plan-runtime.js +32 -0
  12. package/cli/planner.js +67 -0
  13. package/cli/skills.js +240 -0
  14. package/cli/supercli.js +704 -0
  15. package/docs/features/adapters.md +25 -0
  16. package/docs/features/agent-friendly.md +28 -0
  17. package/docs/features/ask.md +32 -0
  18. package/docs/features/config-sync.md +22 -0
  19. package/docs/features/execution-plans.md +25 -0
  20. package/docs/features/observability.md +22 -0
  21. package/docs/features/skills.md +25 -0
  22. package/docs/features/storage.md +25 -0
  23. package/docs/features/workflows.md +33 -0
  24. package/docs/initial/AGENTS_FRIENDLY_TOOLS.md +553 -0
  25. package/docs/initial/agent-friendly.md +447 -0
  26. package/docs/initial/architecture.md +436 -0
  27. package/docs/initial/built-in-mcp-server.md +64 -0
  28. package/docs/initial/command-plan.md +532 -0
  29. package/docs/initial/core-features-2.md +428 -0
  30. package/docs/initial/core-features.md +366 -0
  31. package/docs/initial/dag.md +20 -0
  32. package/docs/initial/description.txt +9 -0
  33. package/docs/initial/idea.txt +564 -0
  34. package/docs/initial/initial-spec-details.md +726 -0
  35. package/docs/initial/initial-spec.md +731 -0
  36. package/docs/initial/mcp-local-mode.md +53 -0
  37. package/docs/initial/mcp-sse-mode.md +54 -0
  38. package/docs/initial/skills-support.md +246 -0
  39. package/docs/initial/storage-adapter-example.md +155 -0
  40. package/docs/initial/supercli-vs-gwc.md +109 -0
  41. package/examples/mcp-sse/install-demo.js +86 -0
  42. package/examples/mcp-sse/server.js +81 -0
  43. package/examples/mcp-stdio/install-demo.js +78 -0
  44. package/examples/mcp-stdio/server.js +50 -0
  45. package/package.json +21 -0
  46. package/server/app.js +59 -0
  47. package/server/public/app.js +18 -0
  48. package/server/routes/ask.js +92 -0
  49. package/server/routes/commands.js +126 -0
  50. package/server/routes/config.js +58 -0
  51. package/server/routes/jobs.js +122 -0
  52. package/server/routes/mcp.js +79 -0
  53. package/server/routes/plans.js +134 -0
  54. package/server/routes/specs.js +79 -0
  55. package/server/services/configService.js +88 -0
  56. package/server/storage/adapter.js +32 -0
  57. package/server/storage/file.js +64 -0
  58. package/server/storage/mongo.js +55 -0
  59. package/server/views/command-edit.ejs +110 -0
  60. package/server/views/commands.ejs +49 -0
  61. package/server/views/jobs.ejs +72 -0
  62. package/server/views/layout.ejs +42 -0
  63. package/server/views/mcp.ejs +80 -0
  64. package/server/views/partials/foot.ejs +5 -0
  65. package/server/views/partials/head.ejs +27 -0
  66. package/server/views/specs.ejs +91 -0
  67. package/tests/test-cli.js +367 -0
  68. package/tests/test-mcp.js +189 -0
  69. package/tests/test-openapi.js +101 -0
package/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ # Storage Configuration
2
+ # Set to "true" to use MongoDB, otherwise uses local JSON files
3
+ SUPERCLI_USE_MONGO=false
4
+ SUPERCLI_STORAGE_DIR=./supercli_storage
5
+
6
+ # MongoDB connection (only used if SUPERCLI_USE_MONGO=true)
7
+ MONGO_URL=mongodb://127.0.0.1:27017
8
+ SUPERCLI_DB=supercli
9
+
10
+ # Server port
11
+ PORT=3000
12
+
13
+ # CLI server URL (used by CLI runtime)
14
+ SUPERCLI_SERVER=http://127.0.0.1:3000
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # SuperCLI
2
+
3
+ Config-driven, AI-friendly CLI that dynamically generates commands from cloud configuration.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Web UI (EJS + Vue3 + DaisyUI)
9
+
10
+ REST API
11
+
12
+ NodeJS + MongoDB
13
+
14
+ CLI Runtime
15
+
16
+ ┌────────┼────────┐
17
+ OpenAPI HTTP MCP
18
+ Adapter Adapter Adapter
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```bash
24
+ # Quick usage (no install, local-only by default)
25
+ npx supercli help
26
+ npx supercli skills teach
27
+
28
+ # Install
29
+ npm install
30
+
31
+ # Configure (copy and edit)
32
+ cp .env.example .env
33
+
34
+ # Start server (defaults to local JSON files, no MongoDB required!)
35
+ npm start
36
+ # Or alternatively, start via CLI:
37
+ # supercli --server
38
+
39
+ # Open Web UI
40
+ open http://localhost:3000
41
+
42
+ # CLI usage
43
+ node cli/supercli.js help
44
+ node cli/supercli.js commands
45
+ node cli/supercli.js <namespace> <resource> <action> [--args]
46
+
47
+ # Optional: sync commands from a remote SuperCLI server
48
+ export SUPERCLI_SERVER=http://localhost:3000
49
+ node cli/supercli.js sync
50
+ ```
51
+
52
+ ## CLI Usage
53
+
54
+ ```bash
55
+ # Discovery
56
+ supercli help # List namespaces
57
+ supercli <namespace> # List resources
58
+ supercli <namespace> <resource> # List actions
59
+
60
+ # Inspection
61
+ supercli inspect <ns> <res> <act> # Command details + schema
62
+ supercli <ns> <res> <act> --schema # Input/output schema
63
+
64
+ # Execution
65
+ supercli <ns> <res> <act> --arg value # Execute command
66
+ supercli <ns> <res> <act> --compact # Token-optimized output
67
+
68
+ # Plans (DAG)
69
+ supercli plan <ns> <res> <act> [--args] # Dry-run execution plan
70
+ supercli execute <plan_id> # Execute stored plan
71
+
72
+ # Skills (LLM bootstrap)
73
+ supercli skills list --json # Minimal skill metadata (name, description)
74
+ supercli skills get <ns.res.act> # Emit SKILL.md (default format)
75
+ supercli skills teach # Emit starter meta-skill (default format)
76
+ supercli skills get <ns.res.act> --show-dag
77
+
78
+ # Natural Language (AI)
79
+ export OPENAI_BASE_URL=https://api.openai.com/v1 # Enable local AI resolution
80
+ supercli ask "list the posts and summarize them" # Execute natural language queries
81
+
82
+ # Config & Server
83
+ supercli sync # Sync local cache from SUPERCLI_SERVER (when set)
84
+ supercli config show # Show cache info
85
+ supercli --server # Start the SuperCLI backend server directly
86
+
87
+ # Local MCP registry (no server required)
88
+ supercli mcp list
89
+ supercli mcp add summarize-local --url http://127.0.0.1:8787
90
+ supercli mcp remove summarize-local
91
+
92
+ # Stdio MCP demo (no server required)
93
+ node examples/mcp-stdio/install-demo.js
94
+ supercli ai text summarize --text "Hello world" --json
95
+
96
+ # Remote MCP SSE/HTTP demo
97
+ node examples/mcp-sse/server.js
98
+ node examples/mcp-sse/install-demo.js
99
+ supercli ai text summarize_remote --text "Hello world" --json
100
+
101
+ # Agent capability discovery
102
+ supercli --help-json # Machine-readable capabilities
103
+ ```
104
+
105
+ ## Output Modes
106
+
107
+ | Flag | Output |
108
+ |-------------|-------------------------------------------|
109
+ | (default) | JSON if piped, human-readable if TTY |
110
+ | `--json` | Structured JSON envelope |
111
+ | `--human` | Formatted tables and key-value output |
112
+ | `--compact` | Compressed JSON (shortened keys) |
113
+
114
+ ## Output Envelope
115
+
116
+ Every command returns a deterministic envelope:
117
+
118
+ ```json
119
+ {
120
+ "version": "1.0",
121
+ "command": "namespace.resource.action",
122
+ "duration_ms": 42,
123
+ "data": { ... }
124
+ }
125
+ ```
126
+
127
+ ## Exit Codes
128
+
129
+ | Code | Type | Action |
130
+ |---------|---------------------|----------------------------|
131
+ | 0 | success | Proceed |
132
+ | 82 | validation_error | Fix input |
133
+ | 85 | invalid_argument | Fix argument |
134
+ | 92 | resource_not_found | Try different resource |
135
+ | 105 | integration_error | Retry with backoff |
136
+ | 110 | internal_error | Report bug |
137
+
138
+ ## API Endpoints
139
+
140
+ | Method | Endpoint | Description |
141
+ |--------|-------------------------------|--------------------------|
142
+ | GET | `/api/config` | Full CLI config |
143
+ | GET | `/api/tree` | List namespaces |
144
+ | GET | `/api/tree/:ns` | List resources |
145
+ | GET | `/api/tree/:ns/:res` | List actions |
146
+ | GET | `/api/command/:ns/:res/:act` | Full command spec |
147
+ | CRUD | `/api/commands` | Manage commands |
148
+ | CRUD | `/api/specs` | Manage OpenAPI specs |
149
+ | CRUD | `/api/mcp` | Manage MCP servers |
150
+ | CRUD | `/api/plans` | Execution plans |
151
+ | GET | `/api/jobs` | Execution history |
152
+ | GET | `/api/jobs/stats` | Aggregate stats |
153
+
154
+ ## Adapters
155
+
156
+ - **http** — Raw HTTP requests (method, url, headers)
157
+ - **openapi** — Resolves operation from OpenAPI spec
158
+ - **mcp** — Calls MCP server tools (supports both HTTP endpoints and local Stdio processes)
159
+
160
+ ## Tech Stack
161
+
162
+ - NodeJS + Express
163
+ - Pluggable KV Storage (Local JSON files by default, MongoDB optional)
164
+ - EJS + Vue3 CDN + Tailwind CDN + DaisyUI CDN
165
+ - Zero build tools
166
+
167
+ ## Contributors
168
+
169
+ Contributions are welcome! If you have ideas for improvements, new adapters, or bug fixes, just send a Pull Request (PR) and I will review it.
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,72 @@
1
+ // HTTP Adapter
2
+ // Raw HTTP calls based on adapterConfig: method, url, headers, body
3
+
4
+ async function execute(cmd, flags, context) {
5
+ const config = cmd.adapterConfig || {}
6
+ let url = config.url
7
+ let method = (config.method || "GET").toUpperCase()
8
+ const headers = { ...(config.headers || {}) }
9
+
10
+ if (!url) {
11
+ throw new Error("HTTP adapter requires 'url' in adapterConfig")
12
+ }
13
+
14
+ // Replace {param} placeholders in URL with flag values
15
+ for (const [k, v] of Object.entries(flags)) {
16
+ if (["human", "json", "compact"].includes(k)) continue
17
+ url = url.replace(`{${k}}`, encodeURIComponent(v))
18
+ }
19
+
20
+ // Build query string from flags for GET requests
21
+ if (method === "GET") {
22
+ const queryFlags = Object.entries(flags).filter(([k]) =>
23
+ !["human", "json", "compact"].includes(k) && !url.includes(encodeURIComponent(flags[k]))
24
+ )
25
+ if (queryFlags.length > 0 && !url.includes("?")) {
26
+ // Only add flags that weren't used as path params
27
+ const remaining = queryFlags.filter(([k]) => !config.url.includes(`{${k}}`))
28
+ if (remaining.length > 0) {
29
+ url += "?" + remaining.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")
30
+ }
31
+ }
32
+ }
33
+
34
+ const fetchOpts = { method, headers }
35
+
36
+ // Build body for non-GET methods
37
+ if (["POST", "PUT", "PATCH"].includes(method)) {
38
+ if (config.body) {
39
+ fetchOpts.body = JSON.stringify(config.body)
40
+ } else {
41
+ const bodyObj = {}
42
+ for (const [k, v] of Object.entries(flags)) {
43
+ if (!["human", "json", "compact"].includes(k)) {
44
+ bodyObj[k] = v
45
+ }
46
+ }
47
+ if (Object.keys(bodyObj).length > 0) {
48
+ fetchOpts.body = JSON.stringify(bodyObj)
49
+ headers["Content-Type"] = "application/json"
50
+ }
51
+ }
52
+ }
53
+
54
+ const r = await fetch(url, fetchOpts)
55
+
56
+ if (!r.ok) {
57
+ const text = await r.text().catch(() => "")
58
+ throw Object.assign(new Error(`HTTP request failed: ${r.status} ${r.statusText} ${text}`), {
59
+ code: r.status >= 500 ? 105 : 92,
60
+ type: r.status >= 500 ? "integration_error" : "resource_not_found",
61
+ recoverable: r.status >= 500
62
+ })
63
+ }
64
+
65
+ const contentType = r.headers.get("content-type") || ""
66
+ if (contentType.includes("json")) {
67
+ return r.json()
68
+ }
69
+ return { raw: await r.text() }
70
+ }
71
+
72
+ module.exports = { execute }
@@ -0,0 +1,193 @@
1
+ // MCP Adapter
2
+ // Supports HTTP MCP endpoints and local stdio MCP commands.
3
+
4
+ const { spawn } = require("child_process");
5
+
6
+ async function callStdioTool(command, args, payload, timeoutMs) {
7
+ return new Promise((resolve, reject) => {
8
+ const child = spawn(command, args || [], {
9
+ stdio: ["pipe", "pipe", "pipe"],
10
+ shell: !Array.isArray(args) || args.length === 0,
11
+ });
12
+
13
+ let stdout = "";
14
+ let stderr = "";
15
+ let settled = false;
16
+
17
+ const timer = setTimeout(() => {
18
+ if (settled) return;
19
+ settled = true;
20
+ child.kill("SIGTERM");
21
+ reject(
22
+ Object.assign(
23
+ new Error(`MCP stdio call timed out after ${timeoutMs}ms`),
24
+ {
25
+ code: 105,
26
+ type: "integration_error",
27
+ recoverable: true,
28
+ },
29
+ ),
30
+ );
31
+ }, timeoutMs);
32
+
33
+ child.stdout.setEncoding("utf-8");
34
+ child.stderr.setEncoding("utf-8");
35
+ child.stdout.on("data", (chunk) => {
36
+ stdout += chunk;
37
+ });
38
+ child.stderr.on("data", (chunk) => {
39
+ stderr += chunk;
40
+ });
41
+
42
+ child.on("error", (err) => {
43
+ if (settled) return;
44
+ settled = true;
45
+ clearTimeout(timer);
46
+ reject(
47
+ Object.assign(
48
+ new Error(`Failed to start MCP stdio command: ${err.message}`),
49
+ {
50
+ code: 105,
51
+ type: "integration_error",
52
+ recoverable: true,
53
+ },
54
+ ),
55
+ );
56
+ });
57
+
58
+ child.on("close", (code) => {
59
+ if (settled) return;
60
+ settled = true;
61
+ clearTimeout(timer);
62
+ if (code !== 0) {
63
+ reject(
64
+ Object.assign(
65
+ new Error(
66
+ `MCP stdio command exited with code ${code}: ${stderr.trim()}`,
67
+ ),
68
+ {
69
+ code: 105,
70
+ type: "integration_error",
71
+ recoverable: true,
72
+ },
73
+ ),
74
+ );
75
+ return;
76
+ }
77
+ try {
78
+ resolve(JSON.parse(stdout.trim()));
79
+ } catch {
80
+ reject(
81
+ Object.assign(
82
+ new Error(`MCP stdio response is not valid JSON: ${stdout.trim()}`),
83
+ {
84
+ code: 105,
85
+ type: "integration_error",
86
+ recoverable: true,
87
+ },
88
+ ),
89
+ );
90
+ }
91
+ });
92
+
93
+ child.stdin.write(JSON.stringify(payload));
94
+ child.stdin.end();
95
+ });
96
+ }
97
+
98
+ async function resolveHttpServerUrl(config, context) {
99
+ if (config.url) return config.url;
100
+
101
+ if (context.config && Array.isArray(context.config.mcp_servers)) {
102
+ const local = context.config.mcp_servers.find(
103
+ (s) => s && s.name === config.server,
104
+ );
105
+ if (local) return local.url;
106
+ }
107
+
108
+ if (context.server) {
109
+ const r = await fetch(`${context.server}/api/mcp?format=json`);
110
+ if (!r.ok) {
111
+ throw Object.assign(
112
+ new Error(`Failed to fetch MCP servers list: ${r.status}`),
113
+ {
114
+ code: 105,
115
+ type: "integration_error",
116
+ recoverable: true,
117
+ },
118
+ );
119
+ }
120
+ const servers = await r.json();
121
+ const srv = servers.find((s) => s.name === config.server);
122
+ if (srv) return srv.url;
123
+ }
124
+
125
+ throw Object.assign(
126
+ new Error(
127
+ `MCP server '${config.server}' not found in local config. Add one with: supercli mcp add ${config.server} --url <mcp_url> or run supercli sync`,
128
+ ),
129
+ {
130
+ code: 85,
131
+ type: "invalid_argument",
132
+ recoverable: false,
133
+ },
134
+ );
135
+ }
136
+
137
+ async function execute(cmd, flags, context) {
138
+ const config = cmd.adapterConfig || {};
139
+ const toolName = config.tool;
140
+ const hasHttpSource = !!(config.server || config.url);
141
+ const hasStdioSource = !!config.command;
142
+
143
+ if (!toolName || (!hasHttpSource && !hasStdioSource)) {
144
+ throw new Error(
145
+ "MCP adapter requires 'tool' and one of: 'server', 'url', or 'command' in adapterConfig",
146
+ );
147
+ }
148
+
149
+ const input = {};
150
+ for (const [k, v] of Object.entries(flags)) {
151
+ if (!["human", "json", "compact"].includes(k)) {
152
+ input[k] = v;
153
+ }
154
+ }
155
+
156
+ if (hasStdioSource) {
157
+ const commandArgs = Array.isArray(config.commandArgs)
158
+ ? config.commandArgs
159
+ : [];
160
+ const timeoutMs =
161
+ Number(config.timeout_ms) > 0 ? Number(config.timeout_ms) : 10000;
162
+ return callStdioTool(
163
+ config.command,
164
+ commandArgs,
165
+ { tool: toolName, input },
166
+ timeoutMs,
167
+ );
168
+ }
169
+
170
+ const resolvedUrl = await resolveHttpServerUrl(config, context);
171
+ const toolUrl = resolvedUrl.replace(/\/+$/, "");
172
+ const tr = await fetch(`${toolUrl}/tool`, {
173
+ method: "POST",
174
+ headers: { "Content-Type": "application/json" },
175
+ body: JSON.stringify({ tool: toolName, input }),
176
+ });
177
+
178
+ if (!tr.ok) {
179
+ const text = await tr.text().catch(() => "");
180
+ throw Object.assign(
181
+ new Error(`MCP tool call failed: ${tr.status} ${text}`),
182
+ {
183
+ code: 105,
184
+ type: "integration_error",
185
+ recoverable: true,
186
+ },
187
+ );
188
+ }
189
+
190
+ return tr.json();
191
+ }
192
+
193
+ module.exports = { execute };
@@ -0,0 +1,160 @@
1
+ // OpenAPI Adapter
2
+ // Fetches spec, resolves operation by operationId, builds URL, executes HTTP call
3
+
4
+ const specCache = {};
5
+
6
+ async function fetchSpec(specName, context) {
7
+ if (specCache[specName]) return specCache[specName];
8
+
9
+ const localSpecs =
10
+ context.config && Array.isArray(context.config.specs)
11
+ ? context.config.specs
12
+ : [];
13
+ const localSpec = localSpecs.find((s) => s && s.name === specName);
14
+ if (localSpec) {
15
+ const sr = await fetch(localSpec.url);
16
+ if (!sr.ok)
17
+ throw new Error(
18
+ `Failed to fetch OpenAPI spec from ${localSpec.url}: ${sr.status}`,
19
+ );
20
+ const specDoc = await sr.json();
21
+ specCache[specName] = { ...specDoc, _auth: localSpec.auth };
22
+ return specCache[specName];
23
+ }
24
+
25
+ if (!context.server) {
26
+ throw Object.assign(
27
+ new Error(
28
+ `OpenAPI spec '${specName}' not found in local config. Run supercli sync to load specs.`,
29
+ ),
30
+ {
31
+ code: 85,
32
+ type: "invalid_argument",
33
+ recoverable: false,
34
+ },
35
+ );
36
+ }
37
+
38
+ // Fetch spec URL from server
39
+ const r = await fetch(`${context.server}/api/specs?format=json`);
40
+ if (!r.ok) throw new Error(`Failed to fetch specs list: ${r.status}`);
41
+ const specs = await r.json();
42
+ const spec = specs.find((s) => s.name === specName);
43
+ if (!spec) throw new Error(`OpenAPI spec '${specName}' not found`);
44
+
45
+ // Fetch the actual OpenAPI spec
46
+ const sr = await fetch(spec.url);
47
+ if (!sr.ok)
48
+ throw new Error(
49
+ `Failed to fetch OpenAPI spec from ${spec.url}: ${sr.status}`,
50
+ );
51
+ const specDoc = await sr.json();
52
+ specCache[specName] = { ...specDoc, _auth: spec.auth };
53
+ return specCache[specName];
54
+ }
55
+
56
+ function findOperation(spec, operationId) {
57
+ const paths = spec.paths || {};
58
+ for (const [pathStr, methods] of Object.entries(paths)) {
59
+ for (const [method, op] of Object.entries(methods)) {
60
+ if (op.operationId === operationId) {
61
+ return { path: pathStr, method: method.toUpperCase(), operation: op };
62
+ }
63
+ }
64
+ }
65
+ throw new Error(`Operation '${operationId}' not found in spec`);
66
+ }
67
+
68
+ function buildUrl(baseUrl, pathStr, method, operation, flags) {
69
+ let url = pathStr;
70
+
71
+ // Replace path parameters
72
+ const pathParams = (operation.parameters || []).filter(
73
+ (p) => p.in === "path",
74
+ );
75
+ for (const p of pathParams) {
76
+ if (flags[p.name] !== undefined) {
77
+ url = url.replace(`{${p.name}}`, encodeURIComponent(flags[p.name]));
78
+ }
79
+ }
80
+
81
+ // Build query parameters
82
+ const queryParams = (operation.parameters || []).filter(
83
+ (p) => p.in === "query",
84
+ );
85
+ const query = [];
86
+ for (const p of queryParams) {
87
+ if (flags[p.name] !== undefined) {
88
+ query.push(
89
+ `${encodeURIComponent(p.name)}=${encodeURIComponent(flags[p.name])}`,
90
+ );
91
+ }
92
+ }
93
+
94
+ let fullUrl = baseUrl.replace(/\/+$/, "") + url;
95
+ if (query.length > 0) {
96
+ fullUrl += "?" + query.join("&");
97
+ }
98
+ return { url: fullUrl, method };
99
+ }
100
+
101
+ async function execute(cmd, flags, context) {
102
+ const config = cmd.adapterConfig || {};
103
+ const specName = config.spec;
104
+ const operationId = config.operationId;
105
+
106
+ if (!specName || !operationId) {
107
+ throw new Error(
108
+ "OpenAPI adapter requires 'spec' and 'operationId' in adapterConfig",
109
+ );
110
+ }
111
+
112
+ const spec = await fetchSpec(specName, context);
113
+ const { path: pathStr, method, operation } = findOperation(spec, operationId);
114
+
115
+ // Determine base URL from spec
116
+ const baseUrl =
117
+ (spec.servers && spec.servers[0] && spec.servers[0].url) || "";
118
+ const { url, method: httpMethod } = buildUrl(
119
+ baseUrl,
120
+ pathStr,
121
+ method,
122
+ operation,
123
+ flags,
124
+ );
125
+
126
+ const fetchOpts = { method: httpMethod, headers: {} };
127
+
128
+ // Handle request body for POST/PUT/PATCH
129
+ if (["POST", "PUT", "PATCH"].includes(httpMethod)) {
130
+ const bodyObj = {};
131
+ for (const [k, v] of Object.entries(flags)) {
132
+ if (!["human", "json", "compact"].includes(k)) {
133
+ bodyObj[k] = v;
134
+ }
135
+ }
136
+ fetchOpts.body = JSON.stringify(bodyObj);
137
+ fetchOpts.headers["Content-Type"] = "application/json";
138
+ }
139
+
140
+ const r = await fetch(url, fetchOpts);
141
+ if (!r.ok) {
142
+ const text = await r.text().catch(() => "");
143
+ throw Object.assign(
144
+ new Error(`API call failed: ${r.status} ${r.statusText} ${text}`),
145
+ {
146
+ code: r.status >= 500 ? 105 : 92,
147
+ type: r.status >= 500 ? "integration_error" : "resource_not_found",
148
+ recoverable: r.status >= 500,
149
+ },
150
+ );
151
+ }
152
+
153
+ const contentType = r.headers.get("content-type") || "";
154
+ if (contentType.includes("json")) {
155
+ return r.json();
156
+ }
157
+ return { raw: await r.text() };
158
+ }
159
+
160
+ module.exports = { execute };