@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.
@@ -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
+ }
@@ -1,23 +1,26 @@
1
1
  /**
2
- * 自动版本检查 + 静默后台升级
2
+ * Synchronous version check + foreground upgrade.
3
3
  *
4
- * 策略:
5
- * 1. 同步快速检查(npm view)— 发现新版本时输出提示
6
- * 2. spawn 后台子进程执行 npm install -g 升级
7
- * 3. 升级后执行 postinstall.mjs(skills CLI 安装到所有工具)
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 { execFileSync, spawn } from "node:child_process";
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
- console.error(`[update] ${PKG_NAME} ${currentVersion} → ${latest}, upgrading in background...`);
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
- const child = spawn("node", ["-e", script], {
65
- stdio: "ignore",
66
- detached: true,
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
  }