ima2-gen 1.0.3 → 1.0.5
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/bin/commands/show.js +4 -13
- package/bin/ima2.js +41 -23
- package/bin/lib/platform.js +97 -0
- package/lib/assetLifecycle.js +120 -0
- package/lib/codexDetect.js +69 -0
- package/lib/db.js +92 -0
- package/lib/inflight.js +57 -0
- package/lib/nodeStore.js +66 -0
- package/lib/sessionStore.js +182 -0
- package/package.json +7 -5
- package/server.js +113 -19
- package/ui/dist/assets/index-BlTTpUh8.js +16 -0
- package/ui/dist/assets/index-BlTTpUh8.js.map +1 -0
- package/ui/dist/assets/index-fsgUenJk.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-CGvmo0q2.js +0 -16
- package/ui/dist/assets/index-CGvmo0q2.js.map +0 -1
- package/ui/dist/assets/index-Dr1O_KZg.css +0 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { ulid } from "ulid";
|
|
2
|
+
import { getDb } from "./db.js";
|
|
3
|
+
|
|
4
|
+
function now() {
|
|
5
|
+
return Date.now();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createSession({ title = "Untitled" } = {}) {
|
|
9
|
+
const db = getDb();
|
|
10
|
+
const id = "s_" + ulid();
|
|
11
|
+
const t = now();
|
|
12
|
+
db.prepare(
|
|
13
|
+
"INSERT INTO sessions (id, title, created_at, updated_at, graph_version) VALUES (?, ?, ?, ?, 0)",
|
|
14
|
+
).run(id, title, t, t);
|
|
15
|
+
return { id, title, createdAt: t, updatedAt: t, graphVersion: 0 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function listSessions() {
|
|
19
|
+
const db = getDb();
|
|
20
|
+
const rows = db
|
|
21
|
+
.prepare(
|
|
22
|
+
"SELECT id, title, created_at AS createdAt, updated_at AS updatedAt, graph_version AS graphVersion FROM sessions ORDER BY updated_at DESC",
|
|
23
|
+
)
|
|
24
|
+
.all();
|
|
25
|
+
return rows.map((r) => ({
|
|
26
|
+
...r,
|
|
27
|
+
nodeCount: db
|
|
28
|
+
.prepare("SELECT COUNT(*) AS c FROM nodes WHERE session_id = ?")
|
|
29
|
+
.get(r.id).c,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getSession(id) {
|
|
34
|
+
const db = getDb();
|
|
35
|
+
const session = db
|
|
36
|
+
.prepare(
|
|
37
|
+
"SELECT id, title, created_at AS createdAt, updated_at AS updatedAt, graph_version AS graphVersion FROM sessions WHERE id = ?",
|
|
38
|
+
)
|
|
39
|
+
.get(id);
|
|
40
|
+
if (!session) return null;
|
|
41
|
+
const nodes = db
|
|
42
|
+
.prepare("SELECT id, x, y, data FROM nodes WHERE session_id = ?")
|
|
43
|
+
.all(id)
|
|
44
|
+
.map((n) => ({ id: n.id, x: n.x, y: n.y, data: safeParse(n.data) }));
|
|
45
|
+
const edges = db
|
|
46
|
+
.prepare("SELECT id, source, target, data FROM edges WHERE session_id = ?")
|
|
47
|
+
.all(id)
|
|
48
|
+
.map((e) => ({
|
|
49
|
+
id: e.id,
|
|
50
|
+
source: e.source,
|
|
51
|
+
target: e.target,
|
|
52
|
+
data: safeParse(e.data),
|
|
53
|
+
}));
|
|
54
|
+
return { ...session, nodes, edges };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renameSession(id, title) {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
const res = db
|
|
60
|
+
.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
|
|
61
|
+
.run(title, now(), id);
|
|
62
|
+
return res.changes > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function deleteSession(id) {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
const res = db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
68
|
+
return res.changes > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const MAX_STR = 10_000;
|
|
72
|
+
|
|
73
|
+
function cleanStr(v) {
|
|
74
|
+
if (typeof v !== "string") return "";
|
|
75
|
+
return v.length > MAX_STR ? v.slice(0, MAX_STR) : v;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cleanData(v) {
|
|
79
|
+
try {
|
|
80
|
+
const json = JSON.stringify(v ?? {});
|
|
81
|
+
return json.length > MAX_STR * 10 ? "{}" : json;
|
|
82
|
+
} catch {
|
|
83
|
+
return "{}";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function saveGraph(sessionId, { nodes = [], edges = [], expectedVersion = null }) {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
const sessionExists = db
|
|
90
|
+
.prepare("SELECT 1 FROM sessions WHERE id = ?")
|
|
91
|
+
.get(sessionId);
|
|
92
|
+
if (!sessionExists) {
|
|
93
|
+
const err = new Error(`Session not found: ${sessionId}`);
|
|
94
|
+
err.code = "SESSION_NOT_FOUND";
|
|
95
|
+
err.status = 404;
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const versionRow = db
|
|
100
|
+
.prepare("SELECT graph_version AS graphVersion FROM sessions WHERE id = ?")
|
|
101
|
+
.get(sessionId);
|
|
102
|
+
const currentVersion = versionRow?.graphVersion ?? 0;
|
|
103
|
+
if (
|
|
104
|
+
typeof expectedVersion === "number" &&
|
|
105
|
+
Number.isFinite(expectedVersion) &&
|
|
106
|
+
expectedVersion !== currentVersion
|
|
107
|
+
) {
|
|
108
|
+
const err = new Error(
|
|
109
|
+
`Graph version conflict for session ${sessionId}: expected ${expectedVersion}, got ${currentVersion}`,
|
|
110
|
+
);
|
|
111
|
+
err.code = "GRAPH_VERSION_CONFLICT";
|
|
112
|
+
err.status = 409;
|
|
113
|
+
err.currentVersion = currentVersion;
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate edges reference existing nodes (drop dangling).
|
|
118
|
+
const nodeIds = new Set(nodes.map((n) => n?.id).filter(Boolean).map(String));
|
|
119
|
+
const cleanEdges = edges.filter(
|
|
120
|
+
(e) => e?.id && e?.source && e?.target && nodeIds.has(String(e.source)) && nodeIds.has(String(e.target)),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const tx = db.transaction(() => {
|
|
124
|
+
db.prepare("DELETE FROM nodes WHERE session_id = ?").run(sessionId);
|
|
125
|
+
db.prepare("DELETE FROM edges WHERE session_id = ?").run(sessionId);
|
|
126
|
+
|
|
127
|
+
const insNode = db.prepare(
|
|
128
|
+
"INSERT INTO nodes (session_id, id, x, y, data) VALUES (?, ?, ?, ?, ?)",
|
|
129
|
+
);
|
|
130
|
+
for (const n of nodes) {
|
|
131
|
+
if (!n?.id) continue;
|
|
132
|
+
const x = Number(n.x ?? n.position?.x ?? 0);
|
|
133
|
+
const y = Number(n.y ?? n.position?.y ?? 0);
|
|
134
|
+
insNode.run(
|
|
135
|
+
sessionId,
|
|
136
|
+
cleanStr(String(n.id)),
|
|
137
|
+
Number.isFinite(x) ? x : 0,
|
|
138
|
+
Number.isFinite(y) ? y : 0,
|
|
139
|
+
cleanData(n.data),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const insEdge = db.prepare(
|
|
144
|
+
"INSERT INTO edges (session_id, id, source, target, data) VALUES (?, ?, ?, ?, ?)",
|
|
145
|
+
);
|
|
146
|
+
for (const e of cleanEdges) {
|
|
147
|
+
insEdge.run(
|
|
148
|
+
sessionId,
|
|
149
|
+
cleanStr(String(e.id)),
|
|
150
|
+
cleanStr(String(e.source)),
|
|
151
|
+
cleanStr(String(e.target)),
|
|
152
|
+
cleanData(e.data),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
db.prepare("UPDATE sessions SET updated_at = ?, graph_version = graph_version + 1 WHERE id = ?").run(
|
|
157
|
+
now(),
|
|
158
|
+
sessionId,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return db
|
|
162
|
+
.prepare("SELECT graph_version AS graphVersion FROM sessions WHERE id = ?")
|
|
163
|
+
.get(sessionId).graphVersion;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const nextVersion = tx();
|
|
167
|
+
return { ok: true, graphVersion: nextVersion };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function safeParse(json) {
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(json);
|
|
173
|
+
} catch {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function ensureDefaultSession() {
|
|
179
|
+
const sessions = listSessions();
|
|
180
|
+
if (sessions.length > 0) return sessions[0];
|
|
181
|
+
return createSession({ title: "My first graph" });
|
|
182
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "GPT Image 2 generator with OAuth & API key support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/ima2.js serve",
|
|
11
|
-
"dev": "node
|
|
11
|
+
"dev": "node scripts/dev.mjs",
|
|
12
|
+
"dev:server": "node --watch server.js",
|
|
12
13
|
"ui:install": "cd ui && npm install",
|
|
13
14
|
"ui:dev": "cd ui && npm run dev",
|
|
14
15
|
"ui:build": "cd ui && npm run build",
|
|
15
16
|
"build": "npm run ui:build",
|
|
16
|
-
"test": "node
|
|
17
|
+
"test": "node scripts/run-tests.mjs",
|
|
17
18
|
"setup": "node bin/ima2.js setup",
|
|
18
19
|
"prepublishOnly": "npm run build && npm run lint:pkg",
|
|
19
|
-
"lint:pkg": "node -e \"const p=require('./package.json'); if(!p.
|
|
20
|
+
"lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/','lib/','server.js']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
|
|
20
21
|
"release:patch": "npm version patch && npm publish && git push origin main --tags",
|
|
21
22
|
"release:minor": "npm version minor && npm publish && git push origin main --tags",
|
|
22
23
|
"release:major": "npm version major && npm publish && git push origin main --tags"
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
38
|
"bin/",
|
|
39
|
+
"lib/",
|
|
38
40
|
"ui/dist/",
|
|
39
41
|
"assets/",
|
|
40
42
|
"server.js",
|
|
@@ -42,7 +44,7 @@
|
|
|
42
44
|
"README.md"
|
|
43
45
|
],
|
|
44
46
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
47
|
+
"node": ">=20"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
48
50
|
"better-sqlite3": "^12.9.0",
|
package/server.js
CHANGED
|
@@ -4,8 +4,10 @@ import { writeFile, mkdir, readFile, readdir, stat } from "fs/promises";
|
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { spawn } from "child_process";
|
|
7
|
+
import { spawnBin, onShutdown } from "./bin/lib/platform.js";
|
|
7
8
|
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync as fsReadFileSync } from "fs";
|
|
8
9
|
import { homedir } from "os";
|
|
10
|
+
import { randomBytes } from "crypto";
|
|
9
11
|
import { newNodeId, saveNode, loadNodeB64, loadNodeMeta, loadAssetB64 } from "./lib/nodeStore.js";
|
|
10
12
|
import { startJob, finishJob, listJobs, setJobPhase } from "./lib/inflight.js";
|
|
11
13
|
import {
|
|
@@ -17,18 +19,25 @@ import {
|
|
|
17
19
|
saveGraph,
|
|
18
20
|
ensureDefaultSession,
|
|
19
21
|
} from "./lib/sessionStore.js";
|
|
22
|
+
import { trashAsset, restoreAsset } from "./lib/assetLifecycle.js";
|
|
20
23
|
|
|
21
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
25
|
const app = express();
|
|
23
26
|
|
|
24
|
-
// Load API key from env or
|
|
27
|
+
// Load API key from env or ${IMA2_CONFIG_DIR || ~/.ima2}/config.json
|
|
28
|
+
// (with legacy fallback to <packageRoot>/.ima2/config.json for existing installs)
|
|
25
29
|
let apiKey = process.env.OPENAI_API_KEY;
|
|
26
30
|
if (!apiKey) {
|
|
27
|
-
const
|
|
28
|
-
|
|
31
|
+
const configDir = process.env.IMA2_CONFIG_DIR || join(homedir(), ".ima2");
|
|
32
|
+
const candidates = [
|
|
33
|
+
join(configDir, "config.json"),
|
|
34
|
+
join(__dirname, ".ima2", "config.json"),
|
|
35
|
+
];
|
|
36
|
+
for (const cfgPath of candidates) {
|
|
37
|
+
if (!existsSync(cfgPath)) continue;
|
|
29
38
|
try {
|
|
30
39
|
const cfg = JSON.parse(await readFile(cfgPath, "utf-8"));
|
|
31
|
-
if (cfg.apiKey) apiKey = cfg.apiKey;
|
|
40
|
+
if (cfg.apiKey) { apiKey = cfg.apiKey; break; }
|
|
32
41
|
} catch {}
|
|
33
42
|
}
|
|
34
43
|
}
|
|
@@ -271,6 +280,7 @@ async function listImages(baseDir) {
|
|
|
271
280
|
async function walk(dir, depth) {
|
|
272
281
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
273
282
|
for (const e of entries) {
|
|
283
|
+
if (e.name === ".trash") continue;
|
|
274
284
|
const full = join(dir, e.name);
|
|
275
285
|
if (e.isDirectory() && depth > 0) {
|
|
276
286
|
await walk(full, depth - 1);
|
|
@@ -287,7 +297,14 @@ app.get("/api/history", async (req, res) => {
|
|
|
287
297
|
try {
|
|
288
298
|
const dir = join(__dirname, "generated");
|
|
289
299
|
await mkdir(dir, { recursive: true });
|
|
290
|
-
const
|
|
300
|
+
const limitRaw = parseInt(req.query.limit);
|
|
301
|
+
const limit = Math.min(Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 50, 500);
|
|
302
|
+
const beforeTs = parseInt(req.query.before);
|
|
303
|
+
const beforeFn = typeof req.query.beforeFilename === "string" ? req.query.beforeFilename : null;
|
|
304
|
+
const sinceTs = parseInt(req.query.since);
|
|
305
|
+
const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId : null;
|
|
306
|
+
const groupBy = req.query.groupBy === "session" ? "session" : null;
|
|
307
|
+
|
|
291
308
|
const imgs = await listImages(dir);
|
|
292
309
|
const rows = await Promise.all(imgs.map(async ({ full, rel, name }) => {
|
|
293
310
|
const st = await stat(full).catch(() => null);
|
|
@@ -309,16 +326,91 @@ app.get("/api/history", async (req, res) => {
|
|
|
309
326
|
provider: meta?.provider || "oauth",
|
|
310
327
|
usage: meta?.usage || null,
|
|
311
328
|
webSearchCalls: meta?.webSearchCalls || 0,
|
|
329
|
+
sessionId: meta?.sessionId || null,
|
|
330
|
+
nodeId: meta?.nodeId || null,
|
|
331
|
+
parentNodeId: meta?.parentNodeId || null,
|
|
332
|
+
clientNodeId: meta?.clientNodeId || null,
|
|
333
|
+
kind: meta?.kind || null,
|
|
312
334
|
};
|
|
313
335
|
}));
|
|
314
|
-
|
|
315
|
-
|
|
336
|
+
|
|
337
|
+
// composite sort: createdAt DESC, filename DESC (stable tiebreaker)
|
|
338
|
+
rows.sort((a, b) => {
|
|
339
|
+
if (b.createdAt !== a.createdAt) return b.createdAt - a.createdAt;
|
|
340
|
+
return b.filename < a.filename ? -1 : b.filename > a.filename ? 1 : 0;
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
let filtered = rows;
|
|
344
|
+
if (Number.isFinite(sinceTs)) {
|
|
345
|
+
filtered = filtered.filter((r) => r.createdAt > sinceTs);
|
|
346
|
+
}
|
|
347
|
+
if (Number.isFinite(beforeTs)) {
|
|
348
|
+
filtered = filtered.filter((r) => {
|
|
349
|
+
if (r.createdAt < beforeTs) return true;
|
|
350
|
+
if (r.createdAt === beforeTs && beforeFn) return r.filename < beforeFn;
|
|
351
|
+
return false;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (sessionId) {
|
|
355
|
+
filtered = filtered.filter((r) => r.sessionId === sessionId);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const page = filtered.slice(0, limit);
|
|
359
|
+
const nextCursor = page.length === limit && filtered.length > limit
|
|
360
|
+
? { before: page[page.length - 1].createdAt, beforeFilename: page[page.length - 1].filename }
|
|
361
|
+
: null;
|
|
362
|
+
|
|
363
|
+
if (groupBy === "session") {
|
|
364
|
+
// Group by sessionId while preserving createdAt DESC order overall.
|
|
365
|
+
const groups = new Map(); // sessionId|null -> { sessionId, items, lastUsedAt }
|
|
366
|
+
const loose = [];
|
|
367
|
+
for (const r of page) {
|
|
368
|
+
if (r.sessionId) {
|
|
369
|
+
let g = groups.get(r.sessionId);
|
|
370
|
+
if (!g) {
|
|
371
|
+
g = { sessionId: r.sessionId, items: [], lastUsedAt: r.createdAt };
|
|
372
|
+
groups.set(r.sessionId, g);
|
|
373
|
+
}
|
|
374
|
+
g.items.push(r);
|
|
375
|
+
if (r.createdAt > g.lastUsedAt) g.lastUsedAt = r.createdAt;
|
|
376
|
+
} else {
|
|
377
|
+
loose.push(r);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const sessions = Array.from(groups.values()).sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
381
|
+
return res.json({ sessions, loose, total: rows.length, nextCursor });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
res.json({ items: page, total: rows.length, nextCursor });
|
|
316
385
|
} catch (err) {
|
|
317
386
|
console.error("[history] error:", err.message);
|
|
318
387
|
res.status(500).json({ error: err.message });
|
|
319
388
|
}
|
|
320
389
|
});
|
|
321
390
|
|
|
391
|
+
// ── Asset lifecycle: soft-delete to .trash/, auto-purge after TTL ──
|
|
392
|
+
app.delete("/api/history/:filename", async (req, res) => {
|
|
393
|
+
try {
|
|
394
|
+
const filename = decodeURIComponent(req.params.filename);
|
|
395
|
+
const result = await trashAsset(__dirname, filename);
|
|
396
|
+
res.json(result);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
res.status(err.status || 500).json({ error: err.message, code: err.code });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
app.post("/api/history/:filename/restore", async (req, res) => {
|
|
403
|
+
try {
|
|
404
|
+
const filename = decodeURIComponent(req.params.filename);
|
|
405
|
+
const trashId = typeof req.body?.trashId === "string" ? req.body.trashId : null;
|
|
406
|
+
if (!trashId) return res.status(400).json({ error: "trashId required" });
|
|
407
|
+
const result = await restoreAsset(__dirname, trashId, filename);
|
|
408
|
+
res.json(result);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
res.status(err.status || 500).json({ error: err.message });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
322
414
|
// ── OAuth status ──
|
|
323
415
|
app.get("/api/oauth/status", async (_req, res) => {
|
|
324
416
|
try {
|
|
@@ -426,7 +518,8 @@ app.post("/api/generate", async (req, res) => {
|
|
|
426
518
|
let totalWebSearchCalls = 0;
|
|
427
519
|
for (const r of results) {
|
|
428
520
|
if (r.status === "fulfilled" && r.value.b64) {
|
|
429
|
-
const
|
|
521
|
+
const rand = randomBytes(4).toString("hex");
|
|
522
|
+
const filename = `${Date.now()}_${rand}_${images.length}.${format}`;
|
|
430
523
|
await writeFile(join(__dirname, "generated", filename), Buffer.from(r.value.b64, "base64"));
|
|
431
524
|
// Sidecar metadata for /api/history reconstruction
|
|
432
525
|
const meta = {
|
|
@@ -574,7 +667,7 @@ app.post("/api/edit", async (req, res) => {
|
|
|
574
667
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
575
668
|
|
|
576
669
|
await mkdir(join(__dirname, "generated"), { recursive: true });
|
|
577
|
-
const filename = `${Date.now()}.png`;
|
|
670
|
+
const filename = `${Date.now()}_${randomBytes(4).toString("hex")}.png`;
|
|
578
671
|
await writeFile(join(__dirname, "generated", filename), Buffer.from(resultB64, "base64"));
|
|
579
672
|
const meta = {
|
|
580
673
|
prompt,
|
|
@@ -705,6 +798,8 @@ app.post("/api/node/generate", async (req, res) => {
|
|
|
705
798
|
const meta = {
|
|
706
799
|
nodeId,
|
|
707
800
|
parentNodeId,
|
|
801
|
+
sessionId,
|
|
802
|
+
clientNodeId,
|
|
708
803
|
prompt,
|
|
709
804
|
options: { quality, size, format },
|
|
710
805
|
createdAt: Date.now(),
|
|
@@ -918,7 +1013,7 @@ app.get("/api/billing", async (_req, res) => {
|
|
|
918
1013
|
// ── Start OAuth proxy as child process ──
|
|
919
1014
|
function startOAuthProxy() {
|
|
920
1015
|
console.log(`Starting openai-oauth on port ${OAUTH_PORT}...`);
|
|
921
|
-
const child =
|
|
1016
|
+
const child = spawnBin("npx", ["openai-oauth", "--port", String(OAUTH_PORT)], {
|
|
922
1017
|
stdio: ["ignore", "pipe", "pipe"],
|
|
923
1018
|
env: { ...process.env },
|
|
924
1019
|
});
|
|
@@ -943,7 +1038,12 @@ function startOAuthProxy() {
|
|
|
943
1038
|
|
|
944
1039
|
// ── Boot ──
|
|
945
1040
|
const PORT = process.env.PORT || 3333;
|
|
946
|
-
|
|
1041
|
+
// Tests (and some CI contexts) can opt out of the OAuth proxy subprocess.
|
|
1042
|
+
// The proxy is a user-facing login helper, not required for /api/health or
|
|
1043
|
+
// offline unit tests, and starting it on Windows CI can add 7-10s latency.
|
|
1044
|
+
const oauthChild = process.env.IMA2_NO_OAUTH_PROXY === "1"
|
|
1045
|
+
? null
|
|
1046
|
+
: startOAuthProxy();
|
|
947
1047
|
|
|
948
1048
|
// CLI discovery: advertise running server under ~/.ima2/server.json
|
|
949
1049
|
const __advertisePath = join(homedir(), ".ima2", "server.json");
|
|
@@ -971,15 +1071,9 @@ function __unadvertise() {
|
|
|
971
1071
|
} catch {}
|
|
972
1072
|
}
|
|
973
1073
|
|
|
974
|
-
|
|
975
|
-
__unadvertise();
|
|
976
|
-
oauthChild.kill();
|
|
977
|
-
process.exit();
|
|
978
|
-
});
|
|
979
|
-
process.on("SIGTERM", () => {
|
|
1074
|
+
onShutdown(() => {
|
|
980
1075
|
__unadvertise();
|
|
981
|
-
oauthChild
|
|
982
|
-
process.exit();
|
|
1076
|
+
try { oauthChild?.kill(); } catch {}
|
|
983
1077
|
});
|
|
984
1078
|
process.on("exit", __unadvertise);
|
|
985
1079
|
|