@aeon-ai-pay/aigateway 0.1.5 → 0.2.1

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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * tools-catalog client. **No caching** — every call hits the server.
3
+ *
4
+ * The server-side catalog (resources/skillboss/tools-catalog.json) is the
5
+ * single source of truth. Each invocation reads the current state, so model
6
+ * additions / schema changes take effect immediately with no stale-cache risk.
7
+ */
8
+ import axios from "axios";
9
+
10
+ /** Fetch catalog from server. Throws on network / HTTP failure. */
11
+ export async function fetchCatalog(serviceUrl) {
12
+ if (!serviceUrl) throw new Error("serviceUrl is required");
13
+ const url = `${serviceUrl}/open/api/skillBoss/tools-catalog`;
14
+ const resp = await axios.get(url, { timeout: 15_000 });
15
+ return resp.data;
16
+ }
17
+
18
+ /**
19
+ * Find a model entry in the catalog by id.
20
+ * Returns { category, model, effectiveSchema } or null.
21
+ * effectiveSchema = model.inputsOverride ?? category.defaultInputsSchema ?? null
22
+ */
23
+ export function findModel(catalog, modelId) {
24
+ if (!catalog || !Array.isArray(catalog.categories)) return null;
25
+ for (const cat of catalog.categories) {
26
+ if (!Array.isArray(cat.models)) continue;
27
+ for (const m of cat.models) {
28
+ if (m.id === modelId) {
29
+ return {
30
+ category: cat,
31
+ model: m,
32
+ effectiveSchema: m.inputsOverride || cat.defaultInputsSchema || null,
33
+ };
34
+ }
35
+ }
36
+ }
37
+ return null;
38
+ }
@@ -0,0 +1,407 @@
1
+ /**
2
+ * sb-invoke: invoke any AI tool through the x402 protocol.
3
+ *
4
+ * Server endpoint: GET {serviceUrl}/open/ai/x402/skillBoss/create?body=<urlencoded-json>&appId=<merchant>
5
+ * (legacy server path name; client-side abstraction is vendor-agnostic.)
6
+ * Body shape: { "model": "<model_id>", "inputs": { /* tool-specific *​/ } }
7
+ *
8
+ * This module exposes two surfaces:
9
+ * - invoke(opts) → core logic, returns a result object. No emit.
10
+ * Reusable from any future thin wrapper.
11
+ * - sbInvokeCommand(opts) → commander action handler; runs invoke() and
12
+ * emits the universal envelope.
13
+ */
14
+ import { readFileSync, existsSync } from "node:fs";
15
+ import axios from "axios";
16
+ import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
17
+ import { resolve } from "../config.mjs";
18
+ import { getWalletBalance, getAllowance } from "../balance.mjs";
19
+ import {
20
+ fundSessionKey,
21
+ promptTopupAmount,
22
+ MIN_TOPUP_USDT,
23
+ TOPUP_PRESETS,
24
+ } from "../funding.mjs";
25
+ import { WalletConnectError } from "../walletconnect.mjs";
26
+ import { emitOk, emitErr, logInfo } from "../output.mjs";
27
+ import { extractOutputs, resolveOutputDir, downloadOutputs } from "../tools-download.mjs";
28
+ import { fetchCatalog, findModel } from "../catalog.mjs";
29
+ import { validateInputs } from "../inputs-validator.mjs";
30
+
31
+ /**
32
+ * Parse `--inputs` value: either a JSON literal or `@path/to/file.json`.
33
+ * Returns the parsed object on success.
34
+ * Throws { code: 'INPUTS_FILE_NOT_FOUND' | 'INVALID_INPUTS_JSON', message } on failure.
35
+ */
36
+ function parseInputs(raw) {
37
+ if (raw == null || raw === "") {
38
+ const err = new Error("Missing --inputs.");
39
+ err.code = "MISSING_INPUTS";
40
+ throw err;
41
+ }
42
+ if (typeof raw === "object") return raw;
43
+
44
+ let text = String(raw);
45
+ if (text.startsWith("@")) {
46
+ const path = text.slice(1);
47
+ if (!existsSync(path)) {
48
+ const err = new Error(`Inputs file not found: ${path}`);
49
+ err.code = "INPUTS_FILE_NOT_FOUND";
50
+ err.path = path;
51
+ throw err;
52
+ }
53
+ text = readFileSync(path, "utf-8");
54
+ }
55
+ try {
56
+ return JSON.parse(text);
57
+ } catch (e) {
58
+ const err = new Error(`Failed to parse --inputs as JSON: ${e.message}`);
59
+ err.code = "INVALID_INPUTS_JSON";
60
+ throw err;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Core invocation. Returns:
66
+ * { ok: true, data: { model, inputs, transaction, downloaded, raw, balance, paymentResponse } }
67
+ * { ok: false, code, details } — caller decides whether to emit
68
+ *
69
+ * Never calls emitOk / emitErr / process.exit directly. Suitable for reuse from
70
+ * any thin wrapper that needs to remap the envelope shape.
71
+ */
72
+ export async function invoke(opts) {
73
+ const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
74
+ const privateKey = resolve(opts.privateKey, "EVM_PRIVATE_KEY", "privateKey");
75
+ const { appId, model } = opts;
76
+
77
+ if (!serviceUrl) {
78
+ return { ok: false, code: "SERVICE_URL_MISSING", details: { appId } };
79
+ }
80
+ if (!privateKey) {
81
+ return { ok: false, code: "WALLET_NOT_CONFIGURED", details: { appId } };
82
+ }
83
+ if (!model || !String(model).trim()) {
84
+ return { ok: false, code: "MISSING_MODEL", details: { appId } };
85
+ }
86
+
87
+ let inputs;
88
+ try {
89
+ inputs = parseInputs(opts.inputs);
90
+ } catch (e) {
91
+ return {
92
+ ok: false,
93
+ code: e.code || "INVALID_INPUTS_JSON",
94
+ details: { message: e.message, path: e.path, appId },
95
+ };
96
+ }
97
+
98
+ // ─── Phase 3.2 + 3.3 client-side validation (live catalog from server) ─────
99
+ // Catches model typos & missing/invalid inputs *before* any x402 round-trip.
100
+ // No cache — always fetches fresh catalog. Falls back gracefully on network
101
+ // failure (warn + skip; server still validates).
102
+ let catalog = null;
103
+ try {
104
+ catalog = await fetchCatalog(serviceUrl);
105
+ } catch (e) {
106
+ logInfo(`Warn: catalog fetch failed (${e.message}); skipping client-side validation — server will still check.`);
107
+ }
108
+
109
+ if (catalog) {
110
+ const found = findModel(catalog, model);
111
+ if (!found) {
112
+ return {
113
+ ok: false,
114
+ code: "INVALID_MODEL_ID",
115
+ details: {
116
+ message: `Model "${model}" not found in catalog. Run \`aigateway sb tools\` to see the current list.`,
117
+ model,
118
+ appId,
119
+ },
120
+ };
121
+ }
122
+
123
+ if (found.effectiveSchema) {
124
+ const { ok: validOk, errors } = validateInputs(inputs, found.effectiveSchema);
125
+ if (!validOk) {
126
+ const missingFields = errors.filter((e) => e.kind === "missing").map((e) => e.field);
127
+ const code = missingFields.length > 0 ? "MISSING_INPUTS" : "INVALID_INPUTS";
128
+ return {
129
+ ok: false,
130
+ code,
131
+ details: {
132
+ message: `Inputs validation failed for ${model}: ${errors.map((e) => `[${e.field}] ${e.message}`).join("; ")}`,
133
+ errors,
134
+ required: found.effectiveSchema.required || [],
135
+ properties: Object.keys(found.effectiveSchema.properties || {}),
136
+ category: found.category.key,
137
+ model,
138
+ appId,
139
+ },
140
+ };
141
+ }
142
+ }
143
+ }
144
+ // ─── End client-side validation ────────────────────────────────────────────
145
+
146
+ const bodyPayload = { model, inputs };
147
+ const bodyParam = encodeURIComponent(JSON.stringify(bodyPayload));
148
+ const url = `${serviceUrl}/open/ai/x402/skillBoss/create?body=${bodyParam}&appId=${encodeURIComponent(appId)}`;
149
+
150
+ logInfo(`Invoking ${model}...`);
151
+ logInfo("Fetching payment requirements...");
152
+ let paymentReq;
153
+ let requiredUsdt;
154
+ try {
155
+ paymentReq = await fetchPaymentRequirements(url);
156
+ requiredUsdt = paymentReq.amountUsdt;
157
+ logInfo(`Required: ${requiredUsdt} USDT (pay to ${paymentReq.payTo})`);
158
+ } catch (e) {
159
+ // Server may return HTTP 400 with structured { code, msg } for pricing / body errors.
160
+ // Surface that code as-is so the agent can react (e.g. MODEL_PRICING_NOT_CONFIGURED).
161
+ const serverData = e.response?.data;
162
+ const serverCode = serverData?.code || serverData?.error;
163
+ const serverMsg = serverData?.msg || serverData?.message;
164
+ if (e.response?.status === 400 && serverCode && /^[A-Z_]+$/.test(serverCode)) {
165
+ return {
166
+ ok: false,
167
+ code: serverCode,
168
+ details: { message: serverMsg || e.message, model, appId, serverStatus: 400 },
169
+ };
170
+ }
171
+ return {
172
+ ok: false,
173
+ code: "PAYMENT_FETCH_FAILED",
174
+ details: { message: `Failed to fetch payment requirements: ${e.message}`, model, appId },
175
+ };
176
+ }
177
+
178
+ // Balance / allowance / funding decision
179
+ logInfo("Checking wallet...");
180
+ let needTopup = false;
181
+ let needGas = false;
182
+ let sessionAddress;
183
+ let topupAmount = null;
184
+ let balanceInitialUsdt = null;
185
+ let balanceBeforeChargeUsdt = null;
186
+
187
+ try {
188
+ const { address, usdt, bnb, bnbRaw } = await getWalletBalance(privateKey);
189
+ sessionAddress = address;
190
+ balanceInitialUsdt = usdt;
191
+ balanceBeforeChargeUsdt = usdt;
192
+ const usdtNum = parseFloat(usdt);
193
+
194
+ logInfo(`Wallet: ${address}`);
195
+ logInfo(`Balance: ${usdt} USDT, ${bnb} BNB`);
196
+
197
+ const allowance = await getAllowance(address);
198
+ const requiredWei = BigInt(paymentReq.amountWei);
199
+ if (requiredWei === 0n) {
200
+ return {
201
+ ok: false,
202
+ code: "INVALID_PAYMENT_AMOUNT",
203
+ details: { message: "Server returned invalid payment amount (0). Please retry later.", appId },
204
+ };
205
+ }
206
+ if (allowance >= requiredWei) {
207
+ logInfo("Allowance sufficient, no approve needed.");
208
+ } else {
209
+ logInfo(`Allowance ${allowance} < required ${requiredWei}; approve needed.`);
210
+ if (bnbRaw === 0n) {
211
+ needGas = true;
212
+ logInfo("No BNB for approve gas, will request BNB transfer.");
213
+ }
214
+ }
215
+
216
+ if (usdtNum < requiredUsdt) {
217
+ needTopup = true;
218
+ const shortfall = requiredUsdt - usdtNum;
219
+ const minTopup = Math.max(MIN_TOPUP_USDT, Math.ceil(shortfall));
220
+ logInfo(`USDT insufficient: have ${usdtNum}, need ${requiredUsdt}, shortfall ${shortfall.toFixed(6)} (top-up minimum: ${minTopup} USDT)`);
221
+
222
+ if (opts.topupAmount != null && String(opts.topupAmount).trim() !== "") {
223
+ const amt = Number(opts.topupAmount);
224
+ if (!Number.isFinite(amt) || amt <= 0) {
225
+ return {
226
+ ok: false,
227
+ code: "AMOUNT_INVALID",
228
+ details: { message: `Invalid --topup-amount: ${opts.topupAmount}`, appId },
229
+ };
230
+ }
231
+ if (amt < minTopup) {
232
+ return {
233
+ ok: false,
234
+ code: "TOPUP_AMOUNT_TOO_SMALL",
235
+ details: { message: `--topup-amount ${amt} USDT is below the ${minTopup} USDT minimum for this call.`, minTopup, appId },
236
+ };
237
+ }
238
+ topupAmount = String(opts.topupAmount);
239
+ logInfo(`Using --topup-amount: ${topupAmount} USDT`);
240
+ } else if (process.stdin.isTTY) {
241
+ topupAmount = await promptTopupAmount(minTopup);
242
+ logInfo(`Selected top-up amount: ${topupAmount} USDT`);
243
+ } else {
244
+ const presets = TOPUP_PRESETS.filter((v) => v >= minTopup);
245
+ return {
246
+ ok: false,
247
+ code: "TOPUP_REQUIRED",
248
+ details: {
249
+ message: `USDT balance is below the ${minTopup} USDT minimum for this call. Choose a top-up amount and rerun with --topup-amount <usdt>.`,
250
+ minTopup,
251
+ required: requiredUsdt,
252
+ currentBalance: balanceInitialUsdt,
253
+ address: sessionAddress,
254
+ appId,
255
+ presets,
256
+ hint: `Rerun: aigateway wallet-topup --amount <usdt> --app-id ${appId}`,
257
+ },
258
+ };
259
+ }
260
+ }
261
+ } catch (e) {
262
+ return {
263
+ ok: false,
264
+ code: "BALANCE_CHECK_FAILED",
265
+ details: { message: `Balance check failed: ${e.message}`, appId },
266
+ };
267
+ }
268
+
269
+ if (needTopup || needGas) {
270
+ logInfo("Funding flow triggered...");
271
+ try {
272
+ await fundSessionKey({
273
+ sessionAddress,
274
+ usdtAmount: needTopup ? topupAmount : null,
275
+ needGas,
276
+ });
277
+ } catch (e) {
278
+ if (e instanceof WalletConnectError) {
279
+ return { ok: false, code: e.code, details: { message: e.message, address: sessionAddress, appId } };
280
+ }
281
+ return { ok: false, code: "FUNDING_FAILED", details: { message: e.message, address: sessionAddress, appId } };
282
+ }
283
+
284
+ logInfo("Re-checking wallet balance...");
285
+ try {
286
+ const { usdt, bnbRaw } = await getWalletBalance(privateKey);
287
+ balanceBeforeChargeUsdt = usdt;
288
+ const usdtNum = parseFloat(usdt);
289
+ if (needGas && bnbRaw === 0n) {
290
+ return {
291
+ ok: false,
292
+ code: "INSUFFICIENT_BNB",
293
+ details: { message: "No BNB for approve transaction after funding. Run 'aigateway wallet-gas' to add BNB manually.", address: sessionAddress, appId },
294
+ };
295
+ }
296
+ if (usdtNum < requiredUsdt) {
297
+ return {
298
+ ok: false,
299
+ code: "INSUFFICIENT_USDT",
300
+ details: { message: "Still insufficient USDT after funding.", required: `${requiredUsdt} USDT`, available: `${usdt} USDT`, address: sessionAddress, appId },
301
+ };
302
+ }
303
+ } catch (e) {
304
+ return { ok: false, code: "BALANCE_CHECK_FAILED", details: { message: `Balance re-check failed: ${e.message}`, appId } };
305
+ }
306
+ }
307
+
308
+ // Sign x402 payment & retry the request.
309
+ const { client } = createX402Api(privateKey);
310
+ logInfo(`Submitting payment & request: ${url}`);
311
+
312
+ let response;
313
+ let paymentResponse;
314
+ try {
315
+ const { x402HTTPClient } = await import("@aeon-ai-pay/core/client");
316
+ const httpClient = new x402HTTPClient(client);
317
+
318
+ const raw402 = paymentReq.raw402Response;
319
+ const getHeader = (name) => {
320
+ const value = raw402.headers[name] ?? raw402.headers[name.toLowerCase()];
321
+ return typeof value === "string" ? value : undefined;
322
+ };
323
+ const paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, raw402.data);
324
+ const paymentPayload = await client.createPaymentPayload(paymentRequired);
325
+ const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
326
+
327
+ response = await axios.get(url, {
328
+ headers: {
329
+ ...paymentHeaders,
330
+ "Access-Control-Expose-Headers": "PAYMENT-RESPONSE",
331
+ },
332
+ });
333
+ paymentResponse = decodePaymentResponse(response.headers);
334
+ } catch (error) {
335
+ return {
336
+ ok: false,
337
+ code: "PAYMENT_FAILED",
338
+ details: {
339
+ message: error.message,
340
+ status: error.response?.status,
341
+ data: error.response?.data,
342
+ appId,
343
+ },
344
+ };
345
+ }
346
+
347
+ const transaction = response.data?.transaction || paymentResponse?.txHash || null;
348
+
349
+ // Detect downloadable outputs and fetch them locally (unless --raw).
350
+ let downloaded = [];
351
+ if (!opts.raw) {
352
+ const { kind, items } = extractOutputs(response.data);
353
+ if (items.length) {
354
+ const outputDir = resolveOutputDir(opts.output, kind);
355
+ downloaded = await downloadOutputs(items, outputDir);
356
+ for (const d of downloaded) {
357
+ if (d.error) {
358
+ logInfo(`Failed to download ${d.url}: ${d.error}`);
359
+ } else {
360
+ logInfo(`Saved: ${d.localPath} (${d.format || "?"}, ${d.width || "?"}×${d.height || "?"}, ${d.sizeHuman})`);
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ // Post-payment balance probe (best effort).
367
+ let balanceAfterUsdt = null;
368
+ try {
369
+ const after = await getWalletBalance(privateKey);
370
+ balanceAfterUsdt = after.usdt;
371
+ } catch (e) {
372
+ logInfo(`Post-payment balance check failed: ${e.message}`);
373
+ }
374
+
375
+ return {
376
+ ok: true,
377
+ data: {
378
+ model,
379
+ inputs,
380
+ transaction,
381
+ downloaded,
382
+ // unwrap server envelope: { payer, transaction, data: <upstream-response> } → <upstream-response>
383
+ raw: response.data?.data ?? response.data,
384
+ paymentResponse,
385
+ balance: {
386
+ initial: balanceInitialUsdt,
387
+ before: balanceBeforeChargeUsdt,
388
+ after: balanceAfterUsdt,
389
+ charged: requiredUsdt,
390
+ topup: topupAmount,
391
+ },
392
+ },
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Commander action handler for `aigateway sb invoke`.
398
+ * Emits the universal envelope; errors are emitted via emitErr (which exits).
399
+ */
400
+ export async function sbInvokeCommand(opts) {
401
+ const result = await invoke(opts);
402
+ if (result.ok) {
403
+ emitOk("sb-invoke", result.data, { success: true, ...result.data });
404
+ return;
405
+ }
406
+ emitErr("sb-invoke", result.code, result.details);
407
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * sb-tools: fetch and display the AI tool catalog from the server.
3
+ *
4
+ * aigateway sb tools
5
+ *
6
+ * No caching — always hits the server. Server-side `tools-catalog.json` is the
7
+ * single source of truth. Stdout is the full envelope with `data` = catalog.
8
+ *
9
+ * Endpoint: GET {serviceUrl}/open/api/skillBoss/tools-catalog
10
+ * (free, no x402; price fields are stripped server-side)
11
+ */
12
+ import { resolve } from "../config.mjs";
13
+ import { emitOk, emitErr, logInfo } from "../output.mjs";
14
+ import { fetchCatalog } from "../catalog.mjs";
15
+
16
+ export async function sbTools(opts) {
17
+ const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
18
+ if (!serviceUrl) {
19
+ emitErr("sb-tools", "SERVICE_URL_MISSING", {});
20
+ return;
21
+ }
22
+
23
+ logInfo("Fetching tools catalog from server...");
24
+ let data;
25
+ try {
26
+ data = await fetchCatalog(serviceUrl);
27
+ } catch (e) {
28
+ emitErr("sb-tools", "CATALOG_FETCH_FAILED", {
29
+ message: `Failed to fetch tools catalog: ${e.message}`,
30
+ url: `${serviceUrl}/open/api/skillBoss/tools-catalog`,
31
+ status: e.response?.status,
32
+ });
33
+ return;
34
+ }
35
+
36
+ emitOk("sb-tools", data, { success: true, ...data });
37
+ }
@@ -10,10 +10,9 @@
10
10
  * Design intent: with a single wallet-init call, the agent gets every decision input it needs:
11
11
  * - data.ready=true → the session private key is usable
12
12
  * - data.needsTopup=true → wallet-topup must run first (the envelope includes presets / minTopup / reason)
13
- * - data.needsTopup=false → can proceed directly to create-card / create-image
13
+ * - data.needsTopup=false → can proceed directly to sb invoke
14
14
  */
15
15
  import { loadConfig, saveConfig } from "../config.mjs";
16
- import { MIN_AMOUNT, MAX_AMOUNT } from "../constants.mjs";
17
16
  import { getWalletBalance, getAllowance } from "../balance.mjs";
18
17
  import {
19
18
  LOW_BALANCE_THRESHOLD,
@@ -106,7 +105,6 @@ export async function initWallet(opts) {
106
105
  topupReason, // "first_time" | "low_balance" | "no_approve" | "chain_check_failed" | null
107
106
  minTopup: MIN_TOPUP_USDT,
108
107
  presets: TOPUP_PRESETS,
109
- amountLimits: { min: MIN_AMOUNT, max: MAX_AMOUNT },
110
108
  chainCheck: chainCheckOk ? "ok" : { error: chainCheckError },
111
109
  };
112
110
  emitOk("wallet-init", data, data);
package/src/config.mjs CHANGED
@@ -2,50 +2,49 @@
2
2
  * Config management: ~/.aigateway/config.json
3
3
  * Resolution priority: CLI args > env vars > config.json
4
4
  *
5
- * AEON AI Gateway uses a single x402 service (ai-api.aeon.xyz);
6
- * different capabilities (virtual card / Skill Boss calls) share the host
7
- * but use distinct path prefixes.
5
+ * AEON AI Gateway uses a single x402 service (ai-api.aeon.xyz).
8
6
  */
9
- import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
10
- import { join } from "path";
11
- import { homedir } from "os";
7
+ import {readFileSync, writeFileSync, mkdirSync, chmodSync} from "fs";
8
+ import {join} from "path";
9
+ import {homedir} from "os";
12
10
 
13
11
  const CONFIG_DIR = join(homedir(), ".aigateway");
14
12
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
15
13
 
16
14
  const DEFAULTS = {
17
- serviceUrl: "https://ai-api.aeon.xyz",
15
+ serviceUrl: "https://ai-api-dev.aeon.xyz",
16
+ // serviceUrl: "https://ai-api.aeon.xyz",
18
17
  };
19
18
 
20
19
  export function loadConfig() {
21
- try {
22
- return { ...DEFAULTS, ...JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) };
23
- } catch {
24
- return { ...DEFAULTS };
25
- }
20
+ try {
21
+ return {...DEFAULTS, ...JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))};
22
+ } catch {
23
+ return {...DEFAULTS};
24
+ }
26
25
  }
27
26
 
28
27
  export function saveConfig(config) {
29
- mkdirSync(CONFIG_DIR, { recursive: true });
30
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
31
- chmodSync(CONFIG_FILE, 0o600);
28
+ mkdirSync(CONFIG_DIR, {recursive: true});
29
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {mode: 0o600});
30
+ chmodSync(CONFIG_FILE, 0o600);
32
31
  }
33
32
 
34
33
  /**
35
34
  * Resolve a value with priority: cliValue > envKey > config[configKey]
36
35
  */
37
36
  export function resolve(cliValue, envKey, configKey) {
38
- if (cliValue) return cliValue;
39
- if (process.env[envKey]) return process.env[envKey];
40
- const cfg = loadConfig();
41
- return cfg[configKey] || undefined;
37
+ if (cliValue) return cliValue;
38
+ if (process.env[envKey]) return process.env[envKey];
39
+ const cfg = loadConfig();
40
+ return cfg[configKey] || undefined;
42
41
  }
43
42
 
44
43
  export function getConfigPath() {
45
- return CONFIG_FILE;
44
+ return CONFIG_FILE;
46
45
  }
47
46
 
48
47
  export function isSessionKeyMode() {
49
- const config = loadConfig();
50
- return config.mode === "session-key";
48
+ const config = loadConfig();
49
+ return config.mode === "session-key";
51
50
  }
@@ -14,13 +14,19 @@ export const ERROR_CODES = {
14
14
  WALLET_NOT_CONFIGURED: { exit: 1, message: "Wallet not configured. Run: aigateway wallet-init" },
15
15
  SERVICE_URL_MISSING: { exit: 1, message: "Service URL not configured." },
16
16
  AMOUNT_INVALID: { exit: 1, message: "Invalid amount." },
17
- AMOUNT_OUT_OF_RANGE: { exit: 1, message: "Amount is outside the allowed range." },
18
17
  AMOUNT_EXCEEDS_BALANCE: { exit: 1, message: "Requested amount exceeds available balance." },
19
18
  INSUFFICIENT_USDT: { exit: 1, message: "Insufficient USDT balance." },
20
19
  INSUFFICIENT_BNB: { exit: 1, message: "Insufficient BNB for gas." },
21
20
  NO_FUNDS: { exit: 1, message: "No funds available." },
22
21
  NO_MAIN_WALLET: { exit: 1, message: "No main wallet address configured. Use --to <address>." },
23
- MISSING_PROMPT: { exit: 1, message: "Missing --prompt. Provide a non-empty image prompt." },
22
+ MISSING_MODEL: { exit: 1, message: "Missing --model. Provide a tool model id (see references/tools.md)." },
23
+ MISSING_INPUTS: { exit: 1, message: "Missing --inputs. Provide a JSON object or @path/to/file.json." },
24
+ INVALID_INPUTS_JSON: { exit: 1, message: "Failed to parse --inputs as JSON." },
25
+ INVALID_INPUTS: { exit: 1, message: "Inputs failed schema validation." },
26
+ INPUTS_FILE_NOT_FOUND: { exit: 1, message: "Inputs file (passed via --inputs @path) not found." },
27
+ INVALID_MODEL_ID: { exit: 1, message: "Server rejected the model id." },
28
+ MODEL_PRICING_NOT_CONFIGURED: { exit: 1, message: "This model is not yet priced on the gateway. Ask the operator to add it to skillboss-pricing.json." },
29
+ INVALID_BODY: { exit: 1, message: "Server rejected the request body." },
24
30
  TOPUP_REQUIRED: { exit: 1, message: "Wallet top-up required. Choose an amount and rerun with --topup-amount <usdt>." },
25
31
  TOPUP_AMOUNT_TOO_SMALL: { exit: 1, message: "Top-up amount is below the minimum." },
26
32
  PAYMENT_REJECTED: { exit: 1, message: "Payment approval was rejected. Please try again if you'd like to proceed." },
@@ -28,12 +34,13 @@ export const ERROR_CODES = {
28
34
  // ===== Timeout (exit 2) =====
29
35
  PAYMENT_TIMEOUT: { exit: 2, message: "Payment approval timed out. Please try again." },
30
36
  WC_SESSION_EXPIRED: { exit: 2, message: "WalletConnect session expired." },
31
- POLL_TIMEOUT: { exit: 2, message: "Polling timed out. Card may still be provisioning." },
32
37
  TX_TIMEOUT: { exit: 2, message: "On-chain transaction timed out." },
38
+ UPDATE_APPLIED: { exit: 2, message: "Package was just upgraded. Rerun the previous command on the new version." },
33
39
 
34
40
  // ===== Service / network (exit 3) =====
35
41
  SERVICE_UNAVAILABLE: { exit: 3, message: "Service unavailable or network error." },
36
42
  PAYMENT_FETCH_FAILED: { exit: 3, message: "Failed to fetch payment requirements." },
43
+ CATALOG_FETCH_FAILED: { exit: 3, message: "Failed to fetch tools catalog from the server." },
37
44
  BALANCE_CHECK_FAILED: { exit: 3, message: "Failed to check balance." },
38
45
  ALLOWANCE_CHECK_FAILED: { exit: 3, message: "Failed to check allowance." },
39
46
  TX_REVERTED: { exit: 3, message: "On-chain transaction reverted." },
@@ -42,6 +49,7 @@ export const ERROR_CODES = {
42
49
  INVALID_PAYMENT_AMOUNT: { exit: 3, message: "Server returned invalid payment amount." },
43
50
  PAYMENT_FAILED: { exit: 3, message: "Payment request failed." },
44
51
  IMAGE_DOWNLOAD_FAILED: { exit: 3, message: "Image download failed." },
52
+ DOWNLOAD_FAILED: { exit: 3, message: "Output file download failed." },
45
53
  FUNDING_FAILED: { exit: 3, message: "Funding flow failed." },
46
54
 
47
55
  // ===== Internal (exit 4) =====
package/src/funding.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
  * Funding flow: WalletConnect-based USDT/BNB transfer to session key,
3
3
  * plus on-chain pre-authorization (ERC20.approve facilitator).
4
4
  *
5
- * Shared by: create-image (lazy top-up when balance is short),
6
- * prepare (proactive pre-flight before any image work).
5
+ * Shared by: sb-invoke (lazy top-up when balance is short),
6
+ * wallet-topup (proactive pre-flight).
7
7
  */
8
8
  import {
9
9
  withWallet,