@aeon-ai-pay/aigateway 0.1.4 → 0.1.6
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 +48 -8
- package/bin/cli.mjs +23 -34
- package/docs/exit-codes.md +2 -1
- package/docs/release-process.md +9 -7
- package/package.json +1 -1
- package/scripts/postinstall.mjs +6 -6
- package/skills/aigateway/SKILL.md +369 -272
- package/src/balance.mjs +7 -7
- package/src/catalog.mjs +38 -0
- package/src/commands/clean.mjs +5 -5
- package/src/commands/sb-invoke.mjs +407 -0
- package/src/commands/sb-tools.mjs +37 -0
- package/src/commands/wallet-gas.mjs +2 -1
- package/src/commands/wallet-init.mjs +20 -21
- package/src/commands/wallet-topup.mjs +15 -15
- package/src/commands/wallet-withdraw.mjs +7 -7
- package/src/config.mjs +24 -24
- package/src/error-codes.mjs +22 -14
- package/src/funding.mjs +2 -2
- package/src/inputs-validator.mjs +125 -0
- package/src/output.mjs +19 -16
- package/src/tools-download.mjs +264 -0
- package/src/update-check.mjs +50 -47
- package/src/walletconnect.mjs +65 -63
- package/src/x402.mjs +13 -10
- 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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools-download: shared binary download + metadata helpers used by
|
|
3
|
+
* sb-invoke (any AI tool returning image / video / audio URLs).
|
|
4
|
+
*
|
|
5
|
+
* Public API:
|
|
6
|
+
* - extractOutputs(rawResponseData) → { kind, items }
|
|
7
|
+
* Walks the upstream tool response and returns a normalized list of
|
|
8
|
+
* downloadable URLs along with the inferred media kind ("image" | "video"
|
|
9
|
+
* | "audio" | null).
|
|
10
|
+
* - resolveOutputDir(opts.output, kind) → absolute path
|
|
11
|
+
* Per-kind default: ~/aigateway-{kind}s/, override with `--output`.
|
|
12
|
+
* - downloadOutputs(items, dir) → DownloadedItem[]
|
|
13
|
+
* Downloads every item to `dir`, parses metadata for known image formats.
|
|
14
|
+
* - DEFAULT_DIRS, downloadFile, readImageMeta, humanSize
|
|
15
|
+
* Exposed for advanced reuse / tests.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdirSync, createWriteStream, existsSync, unlinkSync, openSync, readSync, closeSync, statSync } from "node:fs";
|
|
18
|
+
import { join, basename, extname } from "node:path";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { URL } from "node:url";
|
|
21
|
+
import { get as httpsGet } from "node:https";
|
|
22
|
+
import { get as httpGet } from "node:http";
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_DIRS = {
|
|
25
|
+
image: join(homedir(), "aigateway-images"),
|
|
26
|
+
video: join(homedir(), "aigateway-videos"),
|
|
27
|
+
audio: join(homedir(), "aigateway-audio"),
|
|
28
|
+
unknown: join(homedir(), "aigateway-downloads"),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const IMAGE_EXT = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"]);
|
|
32
|
+
const VIDEO_EXT = new Set(["mp4", "webm", "mov", "mkv", "avi", "m4v"]);
|
|
33
|
+
const AUDIO_EXT = new Set(["mp3", "wav", "ogg", "flac", "m4a", "aac", "opus"]);
|
|
34
|
+
|
|
35
|
+
function extFromUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
const p = new URL(url).pathname;
|
|
38
|
+
const e = extname(p).replace(/^\./, "").toLowerCase();
|
|
39
|
+
return e || null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function kindFromExt(ext) {
|
|
46
|
+
if (!ext) return null;
|
|
47
|
+
if (IMAGE_EXT.has(ext)) return "image";
|
|
48
|
+
if (VIDEO_EXT.has(ext)) return "video";
|
|
49
|
+
if (AUDIO_EXT.has(ext)) return "audio";
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalize an upstream tool response into a list of downloadable items.
|
|
55
|
+
* Tries the common locations: data.images[], data.video.url, data.audio.url,
|
|
56
|
+
* data.url, data.output_url, plus raw top-level fallbacks.
|
|
57
|
+
*/
|
|
58
|
+
export function extractOutputs(responseData) {
|
|
59
|
+
const items = [];
|
|
60
|
+
let kind = null;
|
|
61
|
+
|
|
62
|
+
const inner = responseData?.data ?? responseData;
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(inner?.images)) {
|
|
65
|
+
for (const img of inner.images) {
|
|
66
|
+
if (img?.url) items.push({ url: img.url });
|
|
67
|
+
}
|
|
68
|
+
if (items.length) kind = "image";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pushSingle = (url, inferred) => {
|
|
72
|
+
if (!url || typeof url !== "string") return;
|
|
73
|
+
items.push({ url });
|
|
74
|
+
if (!kind && inferred) kind = inferred;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (!items.length) {
|
|
78
|
+
pushSingle(inner?.video?.url, "video");
|
|
79
|
+
pushSingle(inner?.video_url, "video");
|
|
80
|
+
}
|
|
81
|
+
if (!items.length) {
|
|
82
|
+
pushSingle(inner?.audio?.url, "audio");
|
|
83
|
+
pushSingle(inner?.audio_url, "audio");
|
|
84
|
+
}
|
|
85
|
+
if (!items.length) {
|
|
86
|
+
const generic = inner?.url || inner?.output_url || inner?.file_url || inner?.image_url;
|
|
87
|
+
if (typeof generic === "string") {
|
|
88
|
+
const k = kindFromExt(extFromUrl(generic));
|
|
89
|
+
pushSingle(generic, k);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!items.length && Array.isArray(inner?.outputs)) {
|
|
93
|
+
for (const o of inner.outputs) {
|
|
94
|
+
const u = typeof o === "string" ? o : o?.url;
|
|
95
|
+
if (typeof u === "string") {
|
|
96
|
+
items.push({ url: u });
|
|
97
|
+
const k = kindFromExt(extFromUrl(u));
|
|
98
|
+
if (!kind && k) kind = k;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!kind && items.length) {
|
|
104
|
+
const first = items[0]?.url;
|
|
105
|
+
kind = kindFromExt(extFromUrl(first)) ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { kind, items };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveOutputDir(userOverride, kind) {
|
|
112
|
+
if (userOverride) return userOverride;
|
|
113
|
+
return DEFAULT_DIRS[kind] ?? DEFAULT_DIRS.unknown;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Download every item to `dir`, parse image metadata where possible.
|
|
118
|
+
* Returns an array of { url, localPath, format?, width?, height?, sizeBytes, sizeHuman }
|
|
119
|
+
* with `error` populated on per-item failures (the loop never throws).
|
|
120
|
+
*/
|
|
121
|
+
export async function downloadOutputs(items, dir) {
|
|
122
|
+
if (!items.length) return [];
|
|
123
|
+
mkdirSync(dir, { recursive: true });
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
if (!item?.url) continue;
|
|
127
|
+
try {
|
|
128
|
+
const localPath = await downloadFile(item.url, dir);
|
|
129
|
+
const meta = readImageMeta(localPath);
|
|
130
|
+
out.push({
|
|
131
|
+
url: item.url,
|
|
132
|
+
localPath,
|
|
133
|
+
format: meta.format,
|
|
134
|
+
width: meta.width,
|
|
135
|
+
height: meta.height,
|
|
136
|
+
sizeBytes: meta.sizeBytes,
|
|
137
|
+
sizeHuman: meta.sizeHuman,
|
|
138
|
+
});
|
|
139
|
+
} catch (e) {
|
|
140
|
+
out.push({ url: item.url, error: e.message });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function readImageMeta(filePath) {
|
|
147
|
+
const sizeBytes = statSync(filePath).size;
|
|
148
|
+
const sizeHuman = humanSize(sizeBytes);
|
|
149
|
+
let format = null, width = null, height = null;
|
|
150
|
+
const fd = openSync(filePath, "r");
|
|
151
|
+
try {
|
|
152
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
153
|
+
const len = readSync(fd, buf, 0, buf.length, 0);
|
|
154
|
+
if (len >= 24 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) {
|
|
155
|
+
format = "png";
|
|
156
|
+
width = buf.readUInt32BE(16);
|
|
157
|
+
height = buf.readUInt32BE(20);
|
|
158
|
+
} else if (len >= 4 && buf[0] === 0xFF && buf[1] === 0xD8 && buf[2] === 0xFF) {
|
|
159
|
+
format = "jpeg";
|
|
160
|
+
let i = 2;
|
|
161
|
+
while (i + 9 < len) {
|
|
162
|
+
if (buf[i] !== 0xFF) { i++; continue; }
|
|
163
|
+
while (i < len && buf[i] === 0xFF) i++;
|
|
164
|
+
const marker = buf[i];
|
|
165
|
+
i++;
|
|
166
|
+
if (marker === 0xD8 || marker === 0xD9) continue;
|
|
167
|
+
const segLen = buf.readUInt16BE(i);
|
|
168
|
+
if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) {
|
|
169
|
+
height = buf.readUInt16BE(i + 3);
|
|
170
|
+
width = buf.readUInt16BE(i + 5);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
i += segLen;
|
|
174
|
+
}
|
|
175
|
+
} else if (len >= 30 && buf.slice(0, 4).toString("ascii") === "RIFF" && buf.slice(8, 12).toString("ascii") === "WEBP") {
|
|
176
|
+
format = "webp";
|
|
177
|
+
const fourCC = buf.slice(12, 16).toString("ascii");
|
|
178
|
+
if (fourCC === "VP8 ") {
|
|
179
|
+
width = buf.readUInt16LE(26) & 0x3FFF;
|
|
180
|
+
height = buf.readUInt16LE(28) & 0x3FFF;
|
|
181
|
+
} else if (fourCC === "VP8L") {
|
|
182
|
+
const b0 = buf[21], b1 = buf[22], b2 = buf[23], b3 = buf[24];
|
|
183
|
+
width = ((b1 & 0x3F) << 8 | b0) + 1;
|
|
184
|
+
height = ((b3 & 0x0F) << 10 | b2 << 2 | (b1 & 0xC0) >> 6) + 1;
|
|
185
|
+
} else if (fourCC === "VP8X") {
|
|
186
|
+
width = (buf[24] | (buf[25] << 8) | (buf[26] << 16)) + 1;
|
|
187
|
+
height = (buf[27] | (buf[28] << 8) | (buf[29] << 16)) + 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// leave null on parse failure
|
|
192
|
+
} finally {
|
|
193
|
+
closeSync(fd);
|
|
194
|
+
}
|
|
195
|
+
return { format, width, height, sizeBytes, sizeHuman };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function humanSize(bytes) {
|
|
199
|
+
if (!Number.isFinite(bytes)) return "?";
|
|
200
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
201
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
202
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
203
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function downloadFile(fileUrl, outputDir, { maxRedirects = 5, timeoutMs = 60_000 } = {}) {
|
|
207
|
+
let filename;
|
|
208
|
+
try {
|
|
209
|
+
filename = basename(new URL(fileUrl).pathname) || `download-${Date.now()}`;
|
|
210
|
+
} catch {
|
|
211
|
+
filename = `download-${Date.now()}`;
|
|
212
|
+
}
|
|
213
|
+
if (!extname(filename)) filename += ".bin";
|
|
214
|
+
|
|
215
|
+
let target = join(outputDir, filename);
|
|
216
|
+
if (existsSync(target)) {
|
|
217
|
+
const ext = extname(filename);
|
|
218
|
+
const stem = filename.slice(0, filename.length - ext.length);
|
|
219
|
+
let i = 1;
|
|
220
|
+
while (existsSync(join(outputDir, `${stem}-${i}${ext}`))) i++;
|
|
221
|
+
target = join(outputDir, `${stem}-${i}${ext}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const fetchOnce = (currentUrl, redirectsLeft) => {
|
|
226
|
+
let parsed;
|
|
227
|
+
try {
|
|
228
|
+
parsed = new URL(currentUrl);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return reject(new Error(`Invalid URL: ${currentUrl}`));
|
|
231
|
+
}
|
|
232
|
+
const httpModule = parsed.protocol === "http:" ? httpGet : httpsGet;
|
|
233
|
+
const req = httpModule(currentUrl, { timeout: timeoutMs }, (res) => {
|
|
234
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
235
|
+
res.resume();
|
|
236
|
+
if (redirectsLeft <= 0) return reject(new Error("Too many redirects"));
|
|
237
|
+
const nextUrl = new URL(res.headers.location, currentUrl).toString();
|
|
238
|
+
return fetchOnce(nextUrl, redirectsLeft - 1);
|
|
239
|
+
}
|
|
240
|
+
if (res.statusCode !== 200) {
|
|
241
|
+
res.resume();
|
|
242
|
+
return reject(new Error(`HTTP ${res.statusCode} from ${currentUrl}`));
|
|
243
|
+
}
|
|
244
|
+
const file = createWriteStream(target);
|
|
245
|
+
res.pipe(file);
|
|
246
|
+
file.on("finish", () => file.close(() => resolve(target)));
|
|
247
|
+
file.on("error", (err) => {
|
|
248
|
+
try { unlinkSync(target); } catch {}
|
|
249
|
+
reject(err);
|
|
250
|
+
});
|
|
251
|
+
res.on("error", (err) => {
|
|
252
|
+
file.destroy();
|
|
253
|
+
try { unlinkSync(target); } catch {}
|
|
254
|
+
reject(err);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
req.on("error", reject);
|
|
258
|
+
req.on("timeout", () => {
|
|
259
|
+
req.destroy(new Error(`Download timed out after ${timeoutMs}ms`));
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
fetchOnce(fileUrl, maxRedirects);
|
|
263
|
+
});
|
|
264
|
+
}
|
package/src/update-check.mjs
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Synchronous version check + foreground upgrade.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Why foreground (not background-detached): a detached `npm install -g` mid-command
|
|
5
|
+
* can leave the globally installed package in a half-replaced state — `bin/cli.mjs`
|
|
6
|
+
* may already be the new version while `src/commands/*` is still the old one (or
|
|
7
|
+
* vice versa), causing `ERR_MODULE_NOT_FOUND` on the very next invocation.
|
|
8
|
+
*
|
|
9
|
+
* Synchronous upgrade keeps the package consistent:
|
|
10
|
+
* - upgrade succeeds → exit with UPDATE_APPLIED so the caller (or agent) reruns
|
|
11
|
+
* the previous command on the new version
|
|
12
|
+
* - upgrade fails → log the failure and continue on the current version
|
|
13
|
+
*
|
|
14
|
+
* Output on upgrade: an envelope-shaped error so agents can detect / handle it
|
|
15
|
+
* uniformly via `envelope.error.code === "UPDATE_APPLIED"`.
|
|
9
16
|
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { emitErr } from "./output.mjs";
|
|
12
20
|
|
|
13
21
|
const PKG_NAME = "@aeon-ai-pay/aigateway";
|
|
14
22
|
|
|
15
|
-
/**
|
|
16
|
-
* 启动时调用:同步检查版本 + 后台升级
|
|
17
|
-
* @param {string} currentVersion
|
|
18
|
-
*/
|
|
19
23
|
export function checkForUpdates(currentVersion) {
|
|
20
|
-
// 同步快速检查最新版本(超时短,不阻塞太久)
|
|
21
24
|
let latest;
|
|
22
25
|
try {
|
|
23
26
|
latest = execFileSync("npm", ["view", PKG_NAME, "version"], {
|
|
@@ -25,45 +28,45 @@ export function checkForUpdates(currentVersion) {
|
|
|
25
28
|
stdio: ["ignore", "pipe", "ignore"],
|
|
26
29
|
}).toString().trim();
|
|
27
30
|
} catch {
|
|
28
|
-
return; //
|
|
31
|
+
return; // no network / npm unavailable — silently keep going
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
if (!latest || latest === currentVersion) return;
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
console.error(`[update] ${PKG_NAME} ${currentVersion} → ${latest}, upgrading (foreground)...`);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
execFileSync("npm", ["install", "-g", `${PKG_NAME}@${latest}`], {
|
|
40
|
+
timeout: 120000,
|
|
41
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
42
|
+
});
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(`[update] Upgrade failed: ${(e && e.message) || e}. Continuing on ${currentVersion}.`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Re-run the new version's postinstall so the SKILL.md copies in
|
|
49
|
+
// ~/.claude/skills/, .cursor/rules/, etc. get refreshed too.
|
|
50
|
+
try {
|
|
51
|
+
const root = execFileSync("npm", ["root", "-g"], {
|
|
52
|
+
timeout: 10000,
|
|
53
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
54
|
+
}).toString().trim();
|
|
55
|
+
const postinstall = join(root, PKG_NAME, "scripts", "postinstall.mjs");
|
|
56
|
+
execFileSync("node", [postinstall], {
|
|
57
|
+
timeout: 30000,
|
|
58
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
59
|
+
});
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(`[update] postinstall failed: ${(e && e.message) || e}`);
|
|
62
|
+
}
|
|
35
63
|
|
|
36
|
-
|
|
37
|
-
const script = `
|
|
38
|
-
const { execFileSync } = require("child_process");
|
|
39
|
-
const { join } = require("path");
|
|
40
|
-
const { appendFileSync, mkdirSync } = require("fs");
|
|
41
|
-
const { homedir } = require("os");
|
|
42
|
-
const pkg = ${JSON.stringify(PKG_NAME)};
|
|
43
|
-
const ver = ${JSON.stringify(latest)};
|
|
44
|
-
const logDir = join(homedir(), ".aigateway");
|
|
45
|
-
const logFile = join(logDir, "update.log");
|
|
46
|
-
function log(msg) {
|
|
47
|
-
try {
|
|
48
|
-
mkdirSync(logDir, { recursive: true });
|
|
49
|
-
appendFileSync(logFile, new Date().toISOString() + " " + msg + "\\n");
|
|
50
|
-
} catch {}
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
log("Upgrading " + pkg + " to " + ver + "...");
|
|
54
|
-
execFileSync("npm", ["install", "-g", pkg + "@" + ver], { timeout: 120000 });
|
|
55
|
-
const root = execFileSync("npm", ["root", "-g"], { timeout: 10000 }).toString().trim();
|
|
56
|
-
const postinstall = join(root, pkg, "scripts", "postinstall.mjs");
|
|
57
|
-
execFileSync("node", [postinstall], { timeout: 30000 });
|
|
58
|
-
log("Upgrade to " + ver + " succeeded.");
|
|
59
|
-
} catch (e) {
|
|
60
|
-
log("Upgrade to " + ver + " failed: " + (e.message || e));
|
|
61
|
-
}
|
|
62
|
-
`;
|
|
64
|
+
console.error(`[update] Upgraded to ${latest}. Please rerun the previous command on the new version.`);
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// Emit the envelope so agents can detect it programmatically, then exit.
|
|
67
|
+
emitErr("update-check", "UPDATE_APPLIED", {
|
|
68
|
+
message: `Upgraded ${PKG_NAME} ${currentVersion} → ${latest}. Rerun the previous command.`,
|
|
69
|
+
from: currentVersion,
|
|
70
|
+
to: latest,
|
|
67
71
|
});
|
|
68
|
-
child.unref();
|
|
69
72
|
}
|