@agentlayer.tech/wallet 0.1.13 → 0.1.15

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.
@@ -1,6 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import crypto from "node:crypto";
3
3
  import fs from "node:fs";
4
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { promisify } from "node:util";
6
7
 
@@ -390,10 +391,16 @@ function resolvePythonBin(config) {
390
391
  return config.pythonBin || process.env.OPENCLAW_AGENT_WALLET_PYTHON || "python3";
391
392
  }
392
393
 
394
+ function resolveOpenclawHome(config) {
395
+ return path.resolve(config.openclawHome || process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw"));
396
+ }
397
+
393
398
  function resolvePackageRoot(config) {
399
+ const openclawHome = resolveOpenclawHome(config);
394
400
  const candidates = [
395
401
  config.packageRoot,
396
402
  process.env.OPENCLAW_AGENT_WALLET_PACKAGE_ROOT,
403
+ path.join(openclawHome, "agent-wallet-runtime/current/agent-wallet"),
397
404
  path.resolve(PLUGIN_ROOT, "../../../agent-wallet"),
398
405
  path.resolve(process.cwd(), "agent-wallet"),
399
406
  ].filter(Boolean);
@@ -405,7 +412,7 @@ function resolvePackageRoot(config) {
405
412
  }
406
413
  }
407
414
  throw new Error(
408
- "Could not resolve agent-wallet package root. Set plugins.entries.agent-wallet.config.packageRoot."
415
+ `Could not resolve agent-wallet package root. Checked ${path.join(openclawHome, "agent-wallet-runtime/current/agent-wallet")} and local workspace fallbacks. Set plugins.entries.agent-wallet.config.packageRoot if your runtime lives elsewhere.`
409
416
  );
410
417
  }
411
418
 
@@ -1,11 +1,50 @@
1
1
  {
2
- "name": "agent-wallet",
3
- "version": "0.1.0",
4
- "private": true,
2
+ "name": "@agentlayertech/agent-wallet-plugin",
3
+ "version": "0.1.15",
4
+ "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
+ "license": "SEE LICENSE IN ../../../LICENSE",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/lopushok9/Agent-Layer.git",
10
+ "directory": ".openclaw/extensions/agent-wallet"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/lopushok9/Agent-Layer/issues"
14
+ },
15
+ "homepage": "https://github.com/lopushok9/Agent-Layer/tree/main/.openclaw/extensions/agent-wallet#readme",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "files": [
20
+ "index.ts",
21
+ "dist/",
22
+ "openclaw.plugin.json",
23
+ "README.md",
24
+ "skills/"
25
+ ],
6
26
  "openclaw": {
7
27
  "extensions": [
8
28
  "./index.ts"
9
- ]
10
- }
29
+ ],
30
+ "runtimeExtensions": [
31
+ "./dist/index.js"
32
+ ],
33
+ "compat": {
34
+ "pluginApi": ">=2026.3.24-beta.2",
35
+ "minGatewayVersion": "2026.3.24-beta.2"
36
+ },
37
+ "build": {
38
+ "openclawVersion": "2026.3.24-beta.2",
39
+ "pluginSdkVersion": "2026.3.24-beta.2"
40
+ }
41
+ },
42
+ "keywords": [
43
+ "openclaw",
44
+ "plugin",
45
+ "wallet",
46
+ "solana",
47
+ "bitcoin",
48
+ "evm"
49
+ ]
11
50
  }
@@ -0,0 +1,38 @@
1
+ # pay-bridge
2
+
3
+ Thin OpenClaw bridge to the locally installed `pay` CLI.
4
+
5
+ External install path:
6
+
7
+ ```bash
8
+ openclaw plugins install clawhub:@agentlayertech/pay-bridge-plugin
9
+ ```
10
+
11
+ This plugin is intentionally separate from `agent-wallet`:
12
+
13
+ - `agent-wallet` remains the execution wallet stack for Solana/EVM/BTC
14
+ - `pay-bridge` only discovers and calls paid APIs through `pay`
15
+ - the `pay` wallet stays separate from the AgentLayer wallet runtime
16
+
17
+ ## Exposed tools
18
+
19
+ - `pay_status`
20
+ - `pay_wallet_info`
21
+ - `pay_search_services`
22
+ - `pay_get_service_endpoints`
23
+ - `pay_api_request`
24
+
25
+ ## Intended workflow
26
+
27
+ 1. `pay_status`
28
+ 2. `pay_search_services`
29
+ 3. `pay_get_service_endpoints`
30
+ 4. `pay_api_request`
31
+
32
+ `pay_api_request` is deliberately narrow:
33
+
34
+ - it requires a `service_fqn`, `resource`, and `url`
35
+ - it validates the URL against `pay skills endpoints`
36
+ - it requires `purpose` and `user_confirmed=true`
37
+
38
+ This keeps the bridge thin and prevents it from becoming a generic arbitrary paid-curl launcher.
@@ -0,0 +1,287 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+ const ANSI_RE = /\u001b\[[0-9;]*m/g;
6
+
7
+ function stripAnsi(text) {
8
+ return String(text || "").replace(ANSI_RE, "");
9
+ }
10
+
11
+ function nonEmptyLines(text) {
12
+ return stripAnsi(text)
13
+ .split(/\r?\n/)
14
+ .map((line) => line.trim())
15
+ .filter(Boolean);
16
+ }
17
+
18
+ function extractLastJsonValue(text) {
19
+ const lines = nonEmptyLines(text);
20
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
21
+ const candidate = lines[i];
22
+ if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue;
23
+ try {
24
+ return JSON.parse(candidate);
25
+ } catch {
26
+ continue;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function collectStringLeaves(value, acc = new Set()) {
33
+ if (typeof value === "string") {
34
+ acc.add(value);
35
+ return acc;
36
+ }
37
+ if (Array.isArray(value)) {
38
+ for (const item of value) collectStringLeaves(item, acc);
39
+ return acc;
40
+ }
41
+ if (value && typeof value === "object") {
42
+ for (const item of Object.values(value)) collectStringLeaves(item, acc);
43
+ }
44
+ return acc;
45
+ }
46
+
47
+ function withAccountArgs(args, account) {
48
+ if (!account) return args;
49
+ return [...args, "--account", account];
50
+ }
51
+
52
+ export function resolvePayBinary(config = {}) {
53
+ return (
54
+ config.payBinary ||
55
+ process.env.OPENCLAW_PAY_BINARY ||
56
+ "pay"
57
+ );
58
+ }
59
+
60
+ export async function runPayCommand(payBinary, args, options = {}) {
61
+ const { cwd, input = null } = options;
62
+ let stdout = "";
63
+ let stderr = "";
64
+ try {
65
+ const result = await execFileAsync(payBinary, args, {
66
+ cwd,
67
+ env: { ...process.env },
68
+ input,
69
+ maxBuffer: 1024 * 1024 * 8,
70
+ });
71
+ stdout = result.stdout ?? "";
72
+ stderr = result.stderr ?? "";
73
+ } catch (error) {
74
+ stdout = typeof error?.stdout === "string" ? error.stdout : "";
75
+ stderr = typeof error?.stderr === "string" ? error.stderr : "";
76
+ const payload = extractLastJsonValue(stdout) || extractLastJsonValue(stderr);
77
+ const message =
78
+ payload?.error?.message ||
79
+ payload?.message ||
80
+ stripAnsi(stderr || stdout || error?.message || "pay command failed").trim() ||
81
+ "pay command failed";
82
+ const wrapped = new Error(message);
83
+ wrapped.stdout = stdout;
84
+ wrapped.stderr = stderr;
85
+ wrapped.details = payload && typeof payload === "object" ? payload : null;
86
+ throw wrapped;
87
+ }
88
+ return { stdout, stderr };
89
+ }
90
+
91
+ export function parseWhoamiOutput(stdout) {
92
+ const lines = nonEmptyLines(stdout);
93
+ const systemUser = lines[0] || null;
94
+ const noAccount = lines.some((line) => /no mainnet account/i.test(line));
95
+ return {
96
+ system_user: systemUser,
97
+ has_mainnet_account: !noAccount,
98
+ raw_lines: lines,
99
+ };
100
+ }
101
+
102
+ export function parseAccountListOutput(stdout) {
103
+ const lines = nonEmptyLines(stdout);
104
+ const noAccounts = lines.some((line) => /no accounts found/i.test(line));
105
+ return {
106
+ has_accounts: !noAccounts,
107
+ raw_lines: lines,
108
+ };
109
+ }
110
+
111
+ export async function getPayStatus(config = {}, options = {}) {
112
+ const payBinary = resolvePayBinary(config);
113
+ const versionResult = await runPayCommand(payBinary, ["--version"], options);
114
+ const whoamiResult = await runPayCommand(
115
+ payBinary,
116
+ withAccountArgs(["whoami"], config.defaultAccount),
117
+ options
118
+ );
119
+ const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
120
+ return {
121
+ installed: true,
122
+ pay_binary: payBinary,
123
+ version: stripAnsi(versionResult.stdout).trim() || null,
124
+ account_configured: parseWhoamiOutput(whoamiResult.stdout).has_mainnet_account,
125
+ has_any_accounts: parseAccountListOutput(accountListResult.stdout).has_accounts,
126
+ whoami: parseWhoamiOutput(whoamiResult.stdout),
127
+ accounts: parseAccountListOutput(accountListResult.stdout),
128
+ };
129
+ }
130
+
131
+ export async function getPayWalletInfo(config = {}, options = {}) {
132
+ const payBinary = resolvePayBinary(config);
133
+ const whoamiResult = await runPayCommand(
134
+ payBinary,
135
+ withAccountArgs(["whoami"], config.defaultAccount),
136
+ options
137
+ );
138
+ const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
139
+ return {
140
+ pay_binary: payBinary,
141
+ default_account: config.defaultAccount || null,
142
+ whoami: parseWhoamiOutput(whoamiResult.stdout),
143
+ accounts: parseAccountListOutput(accountListResult.stdout),
144
+ notes: [
145
+ "This wallet is managed by pay.sh and is separate from the AgentLayer execution wallet.",
146
+ ],
147
+ };
148
+ }
149
+
150
+ export async function searchPayServices(config = {}, params = {}, options = {}) {
151
+ const payBinary = resolvePayBinary(config);
152
+ const args = ["skills", "search"];
153
+ if (params.query) args.push(String(params.query));
154
+ if (params.category) args.push("--category", String(params.category));
155
+ args.push("--json");
156
+ const { stdout, stderr } = await runPayCommand(
157
+ payBinary,
158
+ withAccountArgs(args, params.account || config.defaultAccount),
159
+ options
160
+ );
161
+ const parsed = JSON.parse(stdout.trim() || "{}");
162
+ return {
163
+ query: params.query || "",
164
+ category: params.category || null,
165
+ results: parsed,
166
+ warnings: nonEmptyLines(stderr),
167
+ };
168
+ }
169
+
170
+ export async function getPayServiceEndpoints(config = {}, params = {}, options = {}) {
171
+ const payBinary = resolvePayBinary(config);
172
+ const args = [
173
+ "skills",
174
+ "endpoints",
175
+ String(params.service_fqn),
176
+ String(params.resource),
177
+ "--json",
178
+ ];
179
+ const { stdout, stderr } = await runPayCommand(
180
+ payBinary,
181
+ withAccountArgs(args, params.account || config.defaultAccount),
182
+ options
183
+ );
184
+ const parsed = JSON.parse(stdout.trim() || "{}");
185
+ return {
186
+ service_fqn: String(params.service_fqn),
187
+ resource: String(params.resource),
188
+ endpoints: parsed,
189
+ warnings: nonEmptyLines(stderr),
190
+ };
191
+ }
192
+
193
+ function ensureHttps(url, requireHttps = true) {
194
+ if (!requireHttps) return;
195
+ const parsed = new URL(url);
196
+ if (parsed.protocol !== "https:") {
197
+ throw new Error("pay_api_request only allows https URLs.");
198
+ }
199
+ }
200
+
201
+ function appendQuery(url, query) {
202
+ const parsed = new URL(url);
203
+ if (query && typeof query === "object") {
204
+ for (const [key, value] of Object.entries(query)) {
205
+ if (value === undefined || value === null) continue;
206
+ parsed.searchParams.set(key, String(value));
207
+ }
208
+ }
209
+ return parsed.toString();
210
+ }
211
+
212
+ export function endpointPayloadContainsUrl(endpointPayload, url) {
213
+ const strings = collectStringLeaves(endpointPayload);
214
+ return strings.has(url);
215
+ }
216
+
217
+ export async function executePayApiRequest(config = {}, params = {}, options = {}) {
218
+ if (params.user_confirmed !== true) {
219
+ throw new Error("pay_api_request requires user_confirmed=true.");
220
+ }
221
+ if (!params.purpose || !String(params.purpose).trim()) {
222
+ throw new Error("pay_api_request requires a non-empty purpose.");
223
+ }
224
+ if (!params.service_fqn || !params.resource || !params.url) {
225
+ throw new Error("pay_api_request requires service_fqn, resource, and url.");
226
+ }
227
+ if (params.json_body !== undefined && params.text_body !== undefined) {
228
+ throw new Error("Provide either json_body or text_body, not both.");
229
+ }
230
+
231
+ const endpointData = await getPayServiceEndpoints(config, {
232
+ account: params.account,
233
+ resource: params.resource,
234
+ service_fqn: params.service_fqn,
235
+ }, options);
236
+
237
+ const finalUrl = appendQuery(String(params.url), params.query);
238
+ ensureHttps(finalUrl, config.requireHttps !== false);
239
+ if (!endpointPayloadContainsUrl(endpointData.endpoints, String(params.url))) {
240
+ throw new Error("The requested URL is not present in pay_get_service_endpoints for this service/resource.");
241
+ }
242
+
243
+ const payBinary = resolvePayBinary(config);
244
+ const method = String(params.method || "GET").toUpperCase();
245
+ const args = ["curl"];
246
+ if (params.account || config.defaultAccount) {
247
+ args.push("--account", String(params.account || config.defaultAccount));
248
+ }
249
+ args.push("--request", method);
250
+
251
+ const headers = params.headers && typeof params.headers === "object" ? params.headers : {};
252
+ for (const [key, value] of Object.entries(headers)) {
253
+ args.push("--header", `${key}: ${String(value)}`);
254
+ }
255
+
256
+ if (params.json_body !== undefined) {
257
+ const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
258
+ if (!hasContentType) {
259
+ args.push("--header", "content-type: application/json");
260
+ }
261
+ args.push("--data", JSON.stringify(params.json_body));
262
+ } else if (params.text_body !== undefined) {
263
+ args.push("--data", String(params.text_body));
264
+ }
265
+
266
+ args.push(finalUrl);
267
+ const { stdout, stderr } = await runPayCommand(payBinary, args, options);
268
+ const trimmed = stdout.trim();
269
+ let responseBody = trimmed;
270
+ if (params.parse_json_response !== false) {
271
+ try {
272
+ responseBody = JSON.parse(trimmed);
273
+ } catch {
274
+ responseBody = trimmed;
275
+ }
276
+ }
277
+ return {
278
+ method,
279
+ purpose: String(params.purpose),
280
+ request_url: finalUrl,
281
+ service_fqn: String(params.service_fqn),
282
+ resource: String(params.resource),
283
+ response: responseBody,
284
+ raw_response_text: trimmed,
285
+ warnings: nonEmptyLines(stderr),
286
+ };
287
+ }
@@ -0,0 +1,287 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+ const ANSI_RE = /\u001b\[[0-9;]*m/g;
6
+
7
+ function stripAnsi(text) {
8
+ return String(text || "").replace(ANSI_RE, "");
9
+ }
10
+
11
+ function nonEmptyLines(text) {
12
+ return stripAnsi(text)
13
+ .split(/\r?\n/)
14
+ .map((line) => line.trim())
15
+ .filter(Boolean);
16
+ }
17
+
18
+ function extractLastJsonValue(text) {
19
+ const lines = nonEmptyLines(text);
20
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
21
+ const candidate = lines[i];
22
+ if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue;
23
+ try {
24
+ return JSON.parse(candidate);
25
+ } catch {
26
+ continue;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function collectStringLeaves(value, acc = new Set()) {
33
+ if (typeof value === "string") {
34
+ acc.add(value);
35
+ return acc;
36
+ }
37
+ if (Array.isArray(value)) {
38
+ for (const item of value) collectStringLeaves(item, acc);
39
+ return acc;
40
+ }
41
+ if (value && typeof value === "object") {
42
+ for (const item of Object.values(value)) collectStringLeaves(item, acc);
43
+ }
44
+ return acc;
45
+ }
46
+
47
+ function withAccountArgs(args, account) {
48
+ if (!account) return args;
49
+ return [...args, "--account", account];
50
+ }
51
+
52
+ export function resolvePayBinary(config = {}) {
53
+ return (
54
+ config.payBinary ||
55
+ process.env.OPENCLAW_PAY_BINARY ||
56
+ "pay"
57
+ );
58
+ }
59
+
60
+ export async function runPayCommand(payBinary, args, options = {}) {
61
+ const { cwd, input = null } = options;
62
+ let stdout = "";
63
+ let stderr = "";
64
+ try {
65
+ const result = await execFileAsync(payBinary, args, {
66
+ cwd,
67
+ env: { ...process.env },
68
+ input,
69
+ maxBuffer: 1024 * 1024 * 8,
70
+ });
71
+ stdout = result.stdout ?? "";
72
+ stderr = result.stderr ?? "";
73
+ } catch (error) {
74
+ stdout = typeof error?.stdout === "string" ? error.stdout : "";
75
+ stderr = typeof error?.stderr === "string" ? error.stderr : "";
76
+ const payload = extractLastJsonValue(stdout) || extractLastJsonValue(stderr);
77
+ const message =
78
+ payload?.error?.message ||
79
+ payload?.message ||
80
+ stripAnsi(stderr || stdout || error?.message || "pay command failed").trim() ||
81
+ "pay command failed";
82
+ const wrapped = new Error(message);
83
+ wrapped.stdout = stdout;
84
+ wrapped.stderr = stderr;
85
+ wrapped.details = payload && typeof payload === "object" ? payload : null;
86
+ throw wrapped;
87
+ }
88
+ return { stdout, stderr };
89
+ }
90
+
91
+ export function parseWhoamiOutput(stdout) {
92
+ const lines = nonEmptyLines(stdout);
93
+ const systemUser = lines[0] || null;
94
+ const noAccount = lines.some((line) => /no mainnet account/i.test(line));
95
+ return {
96
+ system_user: systemUser,
97
+ has_mainnet_account: !noAccount,
98
+ raw_lines: lines,
99
+ };
100
+ }
101
+
102
+ export function parseAccountListOutput(stdout) {
103
+ const lines = nonEmptyLines(stdout);
104
+ const noAccounts = lines.some((line) => /no accounts found/i.test(line));
105
+ return {
106
+ has_accounts: !noAccounts,
107
+ raw_lines: lines,
108
+ };
109
+ }
110
+
111
+ export async function getPayStatus(config = {}, options = {}) {
112
+ const payBinary = resolvePayBinary(config);
113
+ const versionResult = await runPayCommand(payBinary, ["--version"], options);
114
+ const whoamiResult = await runPayCommand(
115
+ payBinary,
116
+ withAccountArgs(["whoami"], config.defaultAccount),
117
+ options
118
+ );
119
+ const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
120
+ return {
121
+ installed: true,
122
+ pay_binary: payBinary,
123
+ version: stripAnsi(versionResult.stdout).trim() || null,
124
+ account_configured: parseWhoamiOutput(whoamiResult.stdout).has_mainnet_account,
125
+ has_any_accounts: parseAccountListOutput(accountListResult.stdout).has_accounts,
126
+ whoami: parseWhoamiOutput(whoamiResult.stdout),
127
+ accounts: parseAccountListOutput(accountListResult.stdout),
128
+ };
129
+ }
130
+
131
+ export async function getPayWalletInfo(config = {}, options = {}) {
132
+ const payBinary = resolvePayBinary(config);
133
+ const whoamiResult = await runPayCommand(
134
+ payBinary,
135
+ withAccountArgs(["whoami"], config.defaultAccount),
136
+ options
137
+ );
138
+ const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
139
+ return {
140
+ pay_binary: payBinary,
141
+ default_account: config.defaultAccount || null,
142
+ whoami: parseWhoamiOutput(whoamiResult.stdout),
143
+ accounts: parseAccountListOutput(accountListResult.stdout),
144
+ notes: [
145
+ "This wallet is managed by pay.sh and is separate from the AgentLayer execution wallet.",
146
+ ],
147
+ };
148
+ }
149
+
150
+ export async function searchPayServices(config = {}, params = {}, options = {}) {
151
+ const payBinary = resolvePayBinary(config);
152
+ const args = ["skills", "search"];
153
+ if (params.query) args.push(String(params.query));
154
+ if (params.category) args.push("--category", String(params.category));
155
+ args.push("--json");
156
+ const { stdout, stderr } = await runPayCommand(
157
+ payBinary,
158
+ withAccountArgs(args, params.account || config.defaultAccount),
159
+ options
160
+ );
161
+ const parsed = JSON.parse(stdout.trim() || "{}");
162
+ return {
163
+ query: params.query || "",
164
+ category: params.category || null,
165
+ results: parsed,
166
+ warnings: nonEmptyLines(stderr),
167
+ };
168
+ }
169
+
170
+ export async function getPayServiceEndpoints(config = {}, params = {}, options = {}) {
171
+ const payBinary = resolvePayBinary(config);
172
+ const args = [
173
+ "skills",
174
+ "endpoints",
175
+ String(params.service_fqn),
176
+ String(params.resource),
177
+ "--json",
178
+ ];
179
+ const { stdout, stderr } = await runPayCommand(
180
+ payBinary,
181
+ withAccountArgs(args, params.account || config.defaultAccount),
182
+ options
183
+ );
184
+ const parsed = JSON.parse(stdout.trim() || "{}");
185
+ return {
186
+ service_fqn: String(params.service_fqn),
187
+ resource: String(params.resource),
188
+ endpoints: parsed,
189
+ warnings: nonEmptyLines(stderr),
190
+ };
191
+ }
192
+
193
+ function ensureHttps(url, requireHttps = true) {
194
+ if (!requireHttps) return;
195
+ const parsed = new URL(url);
196
+ if (parsed.protocol !== "https:") {
197
+ throw new Error("pay_api_request only allows https URLs.");
198
+ }
199
+ }
200
+
201
+ function appendQuery(url, query) {
202
+ const parsed = new URL(url);
203
+ if (query && typeof query === "object") {
204
+ for (const [key, value] of Object.entries(query)) {
205
+ if (value === undefined || value === null) continue;
206
+ parsed.searchParams.set(key, String(value));
207
+ }
208
+ }
209
+ return parsed.toString();
210
+ }
211
+
212
+ export function endpointPayloadContainsUrl(endpointPayload, url) {
213
+ const strings = collectStringLeaves(endpointPayload);
214
+ return strings.has(url);
215
+ }
216
+
217
+ export async function executePayApiRequest(config = {}, params = {}, options = {}) {
218
+ if (params.user_confirmed !== true) {
219
+ throw new Error("pay_api_request requires user_confirmed=true.");
220
+ }
221
+ if (!params.purpose || !String(params.purpose).trim()) {
222
+ throw new Error("pay_api_request requires a non-empty purpose.");
223
+ }
224
+ if (!params.service_fqn || !params.resource || !params.url) {
225
+ throw new Error("pay_api_request requires service_fqn, resource, and url.");
226
+ }
227
+ if (params.json_body !== undefined && params.text_body !== undefined) {
228
+ throw new Error("Provide either json_body or text_body, not both.");
229
+ }
230
+
231
+ const endpointData = await getPayServiceEndpoints(config, {
232
+ account: params.account,
233
+ resource: params.resource,
234
+ service_fqn: params.service_fqn,
235
+ }, options);
236
+
237
+ const finalUrl = appendQuery(String(params.url), params.query);
238
+ ensureHttps(finalUrl, config.requireHttps !== false);
239
+ if (!endpointPayloadContainsUrl(endpointData.endpoints, String(params.url))) {
240
+ throw new Error("The requested URL is not present in pay_get_service_endpoints for this service/resource.");
241
+ }
242
+
243
+ const payBinary = resolvePayBinary(config);
244
+ const method = String(params.method || "GET").toUpperCase();
245
+ const args = ["curl"];
246
+ if (params.account || config.defaultAccount) {
247
+ args.push("--account", String(params.account || config.defaultAccount));
248
+ }
249
+ args.push("--request", method);
250
+
251
+ const headers = params.headers && typeof params.headers === "object" ? params.headers : {};
252
+ for (const [key, value] of Object.entries(headers)) {
253
+ args.push("--header", `${key}: ${String(value)}`);
254
+ }
255
+
256
+ if (params.json_body !== undefined) {
257
+ const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
258
+ if (!hasContentType) {
259
+ args.push("--header", "content-type: application/json");
260
+ }
261
+ args.push("--data", JSON.stringify(params.json_body));
262
+ } else if (params.text_body !== undefined) {
263
+ args.push("--data", String(params.text_body));
264
+ }
265
+
266
+ args.push(finalUrl);
267
+ const { stdout, stderr } = await runPayCommand(payBinary, args, options);
268
+ const trimmed = stdout.trim();
269
+ let responseBody = trimmed;
270
+ if (params.parse_json_response !== false) {
271
+ try {
272
+ responseBody = JSON.parse(trimmed);
273
+ } catch {
274
+ responseBody = trimmed;
275
+ }
276
+ }
277
+ return {
278
+ method,
279
+ purpose: String(params.purpose),
280
+ request_url: finalUrl,
281
+ service_fqn: String(params.service_fqn),
282
+ resource: String(params.resource),
283
+ response: responseBody,
284
+ raw_response_text: trimmed,
285
+ warnings: nonEmptyLines(stderr),
286
+ };
287
+ }