@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.
- package/CHANGELOG.md +22 -0
- package/bin/cli.mjs +20 -32
- package/docs/exit-codes.md +2 -1
- package/docs/release-process.md +9 -7
- package/package.json +1 -1
- package/skills/aigateway/SKILL.md +369 -267
- package/src/catalog.mjs +38 -0
- package/src/commands/sb-invoke.mjs +407 -0
- package/src/commands/sb-tools.mjs +37 -0
- package/src/commands/wallet-init.mjs +1 -3
- package/src/config.mjs +21 -22
- package/src/error-codes.mjs +11 -3
- package/src/funding.mjs +2 -2
- package/src/inputs-validator.mjs +125 -0
- package/src/output.mjs +1 -1
- package/src/tools-download.mjs +264 -0
- package/src/update-check.mjs +50 -47
- package/src/x402.mjs +1 -1
- package/skills/aigateway/references/check-status.md +0 -68
- package/skills/aigateway/references/create-card.md +0 -114
- package/skills/aigateway/references/store.md +0 -87
- package/src/commands/create-card-status.mjs +0 -67
- package/src/commands/create-card.mjs +0 -352
- package/src/commands/create-image.mjs +0 -428
- package/src/sanitize.mjs +0 -48
package/src/catalog.mjs
ADDED
|
@@ -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
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
+
return CONFIG_FILE;
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
export function isSessionKeyMode() {
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
return config.mode === "session-key";
|
|
51
50
|
}
|
package/src/error-codes.mjs
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
6
|
-
*
|
|
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,
|