@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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JSON Schema validator for tools-catalog inputs schemas.
|
|
3
|
+
*
|
|
4
|
+
* Supports the subset of JSON Schema actually used in tools-catalog.json:
|
|
5
|
+
* - type: object | string | number | integer | boolean | array
|
|
6
|
+
* - required: string[]
|
|
7
|
+
* - properties: { fieldName: <fieldSchema> }
|
|
8
|
+
* - enum: any[]
|
|
9
|
+
* - minimum / maximum (number, integer)
|
|
10
|
+
* - items (for arrays)
|
|
11
|
+
* - oneOf: [schema, ...] (treated as "any-of" — passes if any branch matches)
|
|
12
|
+
*
|
|
13
|
+
* We intentionally avoid pulling in ajv to keep the CLI dep tree small.
|
|
14
|
+
*
|
|
15
|
+
* @returns {{ ok: boolean, errors: Array<{ field: string, message: string, kind: "missing"|"type"|"enum"|"range"|"other" }> }}
|
|
16
|
+
*/
|
|
17
|
+
export function validateInputs(inputs, schema) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
if (!schema) return { ok: true, errors };
|
|
20
|
+
|
|
21
|
+
// root must be an object
|
|
22
|
+
if (schema.type === "object" || schema.properties || schema.required) {
|
|
23
|
+
if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) {
|
|
24
|
+
errors.push({ field: "(root)", kind: "type", message: "inputs must be a JSON object" });
|
|
25
|
+
return { ok: false, errors };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Required fields
|
|
30
|
+
if (Array.isArray(schema.required)) {
|
|
31
|
+
for (const field of schema.required) {
|
|
32
|
+
const v = inputs[field];
|
|
33
|
+
if (v === undefined || v === null || (typeof v === "string" && v.trim() === "")) {
|
|
34
|
+
errors.push({ field, kind: "missing", message: `required field "${field}" is missing` });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Per-field validation
|
|
40
|
+
if (schema.properties) {
|
|
41
|
+
for (const [field, fieldSchema] of Object.entries(schema.properties)) {
|
|
42
|
+
if (inputs[field] === undefined || inputs[field] === null) continue;
|
|
43
|
+
errors.push(...validateField(field, inputs[field], fieldSchema));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { ok: errors.length === 0, errors };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateField(field, value, schema) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
|
|
53
|
+
// oneOf: pass if any branch accepts (no errors).
|
|
54
|
+
if (Array.isArray(schema.oneOf)) {
|
|
55
|
+
const branchErrors = schema.oneOf.map((sub) => validateField(field, value, sub));
|
|
56
|
+
const anyPass = branchErrors.some((errs) => errs.length === 0);
|
|
57
|
+
if (!anyPass) {
|
|
58
|
+
errors.push({
|
|
59
|
+
field,
|
|
60
|
+
kind: "type",
|
|
61
|
+
message: `value does not match any of oneOf schemas (tried ${schema.oneOf.length} branches)`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Type check
|
|
68
|
+
if (schema.type) {
|
|
69
|
+
if (!checkType(value, schema.type)) {
|
|
70
|
+
errors.push({
|
|
71
|
+
field,
|
|
72
|
+
kind: "type",
|
|
73
|
+
message: `expected type "${schema.type}", got "${actualType(value)}"`,
|
|
74
|
+
});
|
|
75
|
+
return errors; // bail out on type mismatch
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Enum
|
|
80
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
|
|
81
|
+
errors.push({
|
|
82
|
+
field,
|
|
83
|
+
kind: "enum",
|
|
84
|
+
message: `value ${JSON.stringify(value)} must be one of [${schema.enum.map((v) => JSON.stringify(v)).join(", ")}]`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Number range
|
|
89
|
+
if ((schema.type === "number" || schema.type === "integer") && typeof value === "number") {
|
|
90
|
+
if (schema.minimum != null && value < schema.minimum) {
|
|
91
|
+
errors.push({ field, kind: "range", message: `value ${value} is below minimum ${schema.minimum}` });
|
|
92
|
+
}
|
|
93
|
+
if (schema.maximum != null && value > schema.maximum) {
|
|
94
|
+
errors.push({ field, kind: "range", message: `value ${value} is above maximum ${schema.maximum}` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Array items
|
|
99
|
+
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
100
|
+
value.forEach((item, i) => {
|
|
101
|
+
errors.push(...validateField(`${field}[${i}]`, item, schema.items));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return errors;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function checkType(value, type) {
|
|
109
|
+
switch (type) {
|
|
110
|
+
case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
111
|
+
case "array": return Array.isArray(value);
|
|
112
|
+
case "string": return typeof value === "string";
|
|
113
|
+
case "number": return typeof value === "number" && !Number.isNaN(value);
|
|
114
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
115
|
+
case "boolean": return typeof value === "boolean";
|
|
116
|
+
case "null": return value === null;
|
|
117
|
+
default: return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function actualType(value) {
|
|
122
|
+
if (value === null) return "null";
|
|
123
|
+
if (Array.isArray(value)) return "array";
|
|
124
|
+
return typeof value;
|
|
125
|
+
}
|
package/src/output.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export function isVerboseMode() { return VERBOSE_MODE; }
|
|
|
33
33
|
/**
|
|
34
34
|
* Emit a success result. Callers should let the function return naturally
|
|
35
35
|
* (do not call process.exit afterwards).
|
|
36
|
-
* @param {string} command - command name, e.g. "
|
|
36
|
+
* @param {string} command - command name, e.g. "sb-invoke" / "wallet-init"
|
|
37
37
|
* @param {object} data - placed under envelope.data
|
|
38
38
|
* @param {object} [legacyShape] - the legacy-mode payload; if omitted, `data` is used
|
|
39
39
|
*/
|
|
@@ -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
|
-
* Called at CLI startup: synchronous version probe + detached upgrade.
|
|
17
|
-
* @param {string} currentVersion
|
|
18
|
-
*/
|
|
19
23
|
export function checkForUpdates(currentVersion) {
|
|
20
|
-
// Synchronous fast probe (short timeout so it does not block too long)
|
|
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; // network unavailable — silently
|
|
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
|
}
|
package/src/x402.mjs
CHANGED
|
@@ -70,7 +70,7 @@ export function createX402Api(privateKey) {
|
|
|
70
70
|
* sign manually later.
|
|
71
71
|
* Field names follow the x402 v2 PaymentRequirements standard: asset, payTo, amount.
|
|
72
72
|
*
|
|
73
|
-
* Supports both GET
|
|
73
|
+
* Supports both GET and POST entry points.
|
|
74
74
|
*
|
|
75
75
|
* @param {string} url
|
|
76
76
|
* @param {{ method?: "GET"|"POST", data?: any, headers?: object }} [options]
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# Check Card Status
|
|
2
|
-
|
|
3
|
-
## Command
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
# Single query
|
|
7
|
-
npx @aeon-ai-pay/aigateway create-card-status --order-no <orderNo>
|
|
8
|
-
|
|
9
|
-
# Poll until terminal status (SUCCESS or FAIL)
|
|
10
|
-
npx @aeon-ai-pay/aigateway create-card-status --order-no <orderNo> --poll
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Response Format
|
|
14
|
-
|
|
15
|
-
```json
|
|
16
|
-
{
|
|
17
|
-
"code": "0",
|
|
18
|
-
"msg": "success",
|
|
19
|
-
"model": {
|
|
20
|
-
"orderNo": "300217748668047431791",
|
|
21
|
-
"orderStatus": "SUCCESS",
|
|
22
|
-
"channelStatus": "COMPLETED",
|
|
23
|
-
"orderAmount": 0.6,
|
|
24
|
-
"txHash": "0xabc...def",
|
|
25
|
-
"cardLastFour": "4321",
|
|
26
|
-
"cardBin": "485932",
|
|
27
|
-
"cardScheme": "VISA",
|
|
28
|
-
"cardBalance": 0.6,
|
|
29
|
-
"cardStatus": "ACTIVE"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Status Values
|
|
35
|
-
|
|
36
|
-
### orderStatus (Order Status)
|
|
37
|
-
|
|
38
|
-
| Status | Meaning | Action |
|
|
39
|
-
|------|------|------|
|
|
40
|
-
| `INIT` | Order created, not yet paid | Wait |
|
|
41
|
-
| `PENDING` | Payment submitted, awaiting on-chain confirmation | Continue polling |
|
|
42
|
-
| `SUCCESS` | Card created successfully | Show card details |
|
|
43
|
-
| `FAIL` | Failed | Show error, suggest retry |
|
|
44
|
-
|
|
45
|
-
### channelStatus (Channel Status)
|
|
46
|
-
|
|
47
|
-
| Status | Meaning |
|
|
48
|
-
|------|------|
|
|
49
|
-
| `INIT` | Not yet sent to card provider |
|
|
50
|
-
| `PROCESSING` | Provider is creating the card |
|
|
51
|
-
| `COMPLETED` | Card is ready |
|
|
52
|
-
| `FAILED` | Card creation failed |
|
|
53
|
-
|
|
54
|
-
### cardStatus (Card Status)
|
|
55
|
-
|
|
56
|
-
| Status | Meaning |
|
|
57
|
-
|------|------|
|
|
58
|
-
| `PENDING` | Being provisioned |
|
|
59
|
-
| `ACTIVE` | Ready to use |
|
|
60
|
-
| `FROZEN` | Frozen |
|
|
61
|
-
| `CANCELLED` | Cancelled |
|
|
62
|
-
|
|
63
|
-
## Polling Behavior
|
|
64
|
-
|
|
65
|
-
With `--poll`:
|
|
66
|
-
- Up to **42 attempts** (first 5 at **2-second** intervals, then every **5 seconds**)
|
|
67
|
-
- Stops on `SUCCESS`, `FAIL`, or `cardStatus=ACTIVE`
|
|
68
|
-
- If timed out, notify user and provide manual query command
|