ima2-gen 1.0.6 → 1.0.8
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/.env.example +49 -2
- package/README.md +192 -152
- package/bin/commands/edit.js +1 -1
- package/bin/commands/gen.js +1 -1
- package/bin/ima2.js +15 -7
- package/bin/lib/star-prompt.js +97 -0
- package/config.js +167 -0
- package/lib/assetLifecycle.js +9 -6
- package/lib/db.js +11 -6
- package/lib/errorClassify.js +62 -0
- package/lib/historyList.js +67 -0
- package/lib/inflight.js +70 -6
- package/lib/logger.js +116 -0
- package/lib/nodeStore.js +9 -7
- package/lib/oauthLauncher.js +31 -0
- package/lib/oauthNormalize.js +30 -0
- package/lib/oauthProxy.js +311 -0
- package/lib/refs.js +35 -0
- package/lib/sessionStore.js +49 -0
- package/lib/storageMigration.js +41 -0
- package/lib/styleSheet.js +128 -0
- package/package.json +4 -2
- package/routes/edit.js +171 -0
- package/routes/generate.js +254 -0
- package/routes/health.js +89 -0
- package/routes/history.js +102 -0
- package/routes/index.js +16 -0
- package/routes/nodes.js +340 -0
- package/routes/sessions.js +281 -0
- package/server.js +121 -1083
- package/ui/dist/assets/index-CBrmEeD7.css +1 -0
- package/ui/dist/assets/index-DRST1V_0.js +22 -0
- package/ui/dist/assets/index-DRST1V_0.js.map +1 -0
- package/ui/dist/index.html +18 -2
- package/ui/dist/assets/index-B66MK5qN.css +0 -1
- package/ui/dist/assets/index-BIwLnT0j.js +0 -16
- package/ui/dist/assets/index-BIwLnT0j.js.map +0 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSession,
|
|
3
|
+
listSessions,
|
|
4
|
+
getSession,
|
|
5
|
+
renameSession,
|
|
6
|
+
deleteSession,
|
|
7
|
+
saveGraph,
|
|
8
|
+
getStyleSheet,
|
|
9
|
+
setStyleSheet,
|
|
10
|
+
setStyleSheetEnabled,
|
|
11
|
+
} from "../lib/sessionStore.js";
|
|
12
|
+
import { extractStyleSheet } from "../lib/styleSheet.js";
|
|
13
|
+
import { logError, logEvent } from "../lib/logger.js";
|
|
14
|
+
|
|
15
|
+
function safeJsonChars(value) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(value ?? null).length;
|
|
18
|
+
} catch {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerSessionRoutes(app, ctx) {
|
|
24
|
+
app.get("/api/sessions", (_req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
res.json({ sessions: listSessions() });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
res.status(500).json({ error: { code: "DB_ERROR", message: err.message } });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.post("/api/sessions", (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const title = (req.body?.title || "Untitled").slice(0, 200);
|
|
35
|
+
const session = createSession({ title });
|
|
36
|
+
logEvent("session", "create", {
|
|
37
|
+
sessionId: session.id,
|
|
38
|
+
titleChars: session.title.length,
|
|
39
|
+
});
|
|
40
|
+
res.status(201).json({ session });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
res.status(500).json({ error: { code: "DB_ERROR", message: err.message } });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
app.get("/api/sessions/:id", (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
const session = getSession(req.params.id);
|
|
49
|
+
if (!session) {
|
|
50
|
+
return res.status(404).json({
|
|
51
|
+
error: { code: "SESSION_NOT_FOUND", message: "Session not found" },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
res.json({ session });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
res.status(500).json({ error: { code: "DB_ERROR", message: err.message } });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
app.patch("/api/sessions/:id", (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const title = req.body?.title;
|
|
63
|
+
if (typeof title !== "string" || !title.trim()) {
|
|
64
|
+
return res.status(400).json({
|
|
65
|
+
error: { code: "INVALID_TITLE", message: "Title required" },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const ok = renameSession(req.params.id, title.slice(0, 200));
|
|
69
|
+
if (!ok) {
|
|
70
|
+
return res.status(404).json({
|
|
71
|
+
error: { code: "SESSION_NOT_FOUND", message: "Session not found" },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
logEvent("session", "rename", {
|
|
75
|
+
sessionId: req.params.id,
|
|
76
|
+
titleChars: title.slice(0, 200).length,
|
|
77
|
+
});
|
|
78
|
+
res.json({ ok: true });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
res.status(500).json({ error: { code: "DB_ERROR", message: err.message } });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.delete("/api/sessions/:id", (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const ok = deleteSession(req.params.id);
|
|
87
|
+
if (!ok) {
|
|
88
|
+
return res.status(404).json({
|
|
89
|
+
error: { code: "SESSION_NOT_FOUND", message: "Session not found" },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
logEvent("session", "delete", { sessionId: req.params.id });
|
|
93
|
+
res.json({ ok: true });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
res.status(500).json({ error: { code: "DB_ERROR", message: err.message } });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
app.get("/api/sessions/:id/style-sheet", (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const data = getStyleSheet(req.params.id);
|
|
102
|
+
if (!data) {
|
|
103
|
+
return res.status(404).json({
|
|
104
|
+
error: { code: "SESSION_NOT_FOUND", message: "Session not found" },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
logEvent("session", "stylesheet_get", {
|
|
108
|
+
sessionId: req.params.id,
|
|
109
|
+
enabled: data.enabled,
|
|
110
|
+
hasSheet: !!data.styleSheet,
|
|
111
|
+
sheetChars: safeJsonChars(data.styleSheet),
|
|
112
|
+
});
|
|
113
|
+
res.json(data);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
res.status(500).json({ error: { code: "DB_ERROR", message: err.message } });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.put("/api/sessions/:id/style-sheet", (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const { styleSheet, enabled } = req.body || {};
|
|
122
|
+
if (styleSheet !== null && (typeof styleSheet !== "object" || Array.isArray(styleSheet))) {
|
|
123
|
+
return res.status(400).json({
|
|
124
|
+
error: { code: "INVALID_SHEET", message: "styleSheet must be an object or null" },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
128
|
+
return res.status(400).json({
|
|
129
|
+
error: { code: "INVALID_ENABLED", message: "enabled must be boolean when provided" },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const ok = setStyleSheet(req.params.id, styleSheet);
|
|
133
|
+
if (!ok) {
|
|
134
|
+
return res.status(404).json({
|
|
135
|
+
error: { code: "SESSION_NOT_FOUND", message: "Session not found" },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (typeof enabled === "boolean") setStyleSheetEnabled(req.params.id, enabled);
|
|
139
|
+
logEvent("session", "stylesheet_save", {
|
|
140
|
+
sessionId: req.params.id,
|
|
141
|
+
enabled: typeof enabled === "boolean" ? enabled : undefined,
|
|
142
|
+
hasSheet: !!styleSheet,
|
|
143
|
+
sheetChars: safeJsonChars(styleSheet),
|
|
144
|
+
});
|
|
145
|
+
res.json({ ok: true });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
res.status(500).json({ error: { code: "DB_ERROR", message: err.message } });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
app.patch("/api/sessions/:id/style-sheet/enabled", (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const { enabled } = req.body || {};
|
|
154
|
+
if (typeof enabled !== "boolean") {
|
|
155
|
+
return res.status(400).json({
|
|
156
|
+
error: { code: "INVALID_ENABLED", message: "enabled must be boolean" },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const ok = setStyleSheetEnabled(req.params.id, enabled);
|
|
160
|
+
if (!ok) {
|
|
161
|
+
return res.status(404).json({
|
|
162
|
+
error: { code: "SESSION_NOT_FOUND", message: "Session not found" },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
logEvent("session", "stylesheet_toggle", {
|
|
166
|
+
sessionId: req.params.id,
|
|
167
|
+
enabled,
|
|
168
|
+
});
|
|
169
|
+
res.json({ ok: true, enabled });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
res.status(500).json({ error: { code: "DB_ERROR", message: err.message } });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
app.post("/api/sessions/:id/style-sheet/extract", async (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
if (!ctx.openai) {
|
|
178
|
+
return res.status(400).json({
|
|
179
|
+
error: {
|
|
180
|
+
code: "STYLE_SHEET_NO_KEY",
|
|
181
|
+
message: "Style-sheet extraction requires an OpenAI API key. Connect one via setup.",
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const { prompt, referenceDataUrl } = req.body || {};
|
|
186
|
+
if (typeof prompt !== "string" || !prompt.trim()) {
|
|
187
|
+
return res.status(400).json({
|
|
188
|
+
error: { code: "STYLE_SHEET_BAD_INPUT", message: "prompt required" },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (!getSession(req.params.id)) {
|
|
192
|
+
return res.status(404).json({
|
|
193
|
+
error: { code: "SESSION_NOT_FOUND", message: "Session not found" },
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
logEvent("session", "stylesheet_extract_start", {
|
|
197
|
+
sessionId: req.params.id,
|
|
198
|
+
promptChars: prompt.length,
|
|
199
|
+
hasReference: typeof referenceDataUrl === "string" && referenceDataUrl.length > 0,
|
|
200
|
+
});
|
|
201
|
+
const sheet = await extractStyleSheet(ctx.openai, {
|
|
202
|
+
prompt: prompt.slice(0, 4000),
|
|
203
|
+
referenceDataUrl: typeof referenceDataUrl === "string" ? referenceDataUrl : undefined,
|
|
204
|
+
});
|
|
205
|
+
const persisted = setStyleSheet(req.params.id, sheet);
|
|
206
|
+
if (!persisted) {
|
|
207
|
+
return res.status(404).json({
|
|
208
|
+
error: { code: "SESSION_NOT_FOUND", message: "Session not found" },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
logEvent("session", "stylesheet_extract_done", {
|
|
212
|
+
sessionId: req.params.id,
|
|
213
|
+
sheetChars: safeJsonChars(sheet),
|
|
214
|
+
});
|
|
215
|
+
res.json({ styleSheet: sheet });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const code = err.code || "STYLE_SHEET_ERROR";
|
|
218
|
+
const status =
|
|
219
|
+
code === "STYLE_SHEET_NO_KEY" || code === "STYLE_SHEET_BAD_INPUT"
|
|
220
|
+
? 400
|
|
221
|
+
: code === "STYLE_SHEET_EMPTY" || code === "STYLE_SHEET_PARSE" || code === "STYLE_SHEET_SHAPE"
|
|
222
|
+
? 422
|
|
223
|
+
: 500;
|
|
224
|
+
logError("session", "stylesheet_extract_error", err, { sessionId: req.params.id, code });
|
|
225
|
+
res.status(status).json({ error: { code, message: err.message } });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
app.put("/api/sessions/:id/graph", (req, res) => {
|
|
230
|
+
try {
|
|
231
|
+
const { nodes, edges } = req.body || {};
|
|
232
|
+
const rawIfMatch = req.get("If-Match");
|
|
233
|
+
if (!Array.isArray(nodes) || !Array.isArray(edges)) {
|
|
234
|
+
return res.status(400).json({
|
|
235
|
+
error: { code: "INVALID_GRAPH", message: "nodes and edges arrays required" },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (!rawIfMatch) {
|
|
239
|
+
return res.status(428).json({
|
|
240
|
+
error: { code: "GRAPH_VERSION_REQUIRED", message: "If-Match header required" },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (nodes.length > 500 || edges.length > 1000) {
|
|
244
|
+
return res.status(413).json({
|
|
245
|
+
error: {
|
|
246
|
+
code: "GRAPH_TOO_LARGE",
|
|
247
|
+
message: `Graph too large (max 500 nodes / 1000 edges), got ${nodes.length}/${edges.length}`,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const expectedVersion = Number(String(rawIfMatch).replace(/"/g, ""));
|
|
252
|
+
if (!Number.isFinite(expectedVersion)) {
|
|
253
|
+
return res.status(400).json({
|
|
254
|
+
error: { code: "INVALID_GRAPH_VERSION", message: "If-Match must be a finite integer" },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const result = saveGraph(req.params.id, { nodes, edges, expectedVersion });
|
|
258
|
+
logEvent("session", "graph_save", {
|
|
259
|
+
sessionId: req.params.id,
|
|
260
|
+
nodes: nodes.length,
|
|
261
|
+
edges: edges.length,
|
|
262
|
+
graphVersion: result.graphVersion,
|
|
263
|
+
});
|
|
264
|
+
res.json({ ok: true, nodes: nodes.length, edges: edges.length, graphVersion: result.graphVersion });
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const code = err.code || "DB_ERROR";
|
|
267
|
+
const payload = { error: { code, message: err.message } };
|
|
268
|
+
if (typeof err.currentVersion === "number") payload.currentVersion = err.currentVersion;
|
|
269
|
+
if (code === "GRAPH_VERSION_CONFLICT") {
|
|
270
|
+
logEvent("session", "graph_conflict", {
|
|
271
|
+
sessionId: req.params.id,
|
|
272
|
+
code,
|
|
273
|
+
currentVersion: err.currentVersion,
|
|
274
|
+
});
|
|
275
|
+
} else {
|
|
276
|
+
logError("session", "graph_error", err, { sessionId: req.params.id, code });
|
|
277
|
+
}
|
|
278
|
+
res.status(err.status || 500).json(payload);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|