@lazyingart/agintiflow 0.1.0
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/AGENTS.md +41 -0
- package/LICENSE +201 -0
- package/README.md +294 -0
- package/bin/aginti-cli.js +7 -0
- package/docker/sandbox.Dockerfile +22 -0
- package/docs/npm-publishing.md +45 -0
- package/i18n/README.ar.md +51 -0
- package/i18n/README.de.md +51 -0
- package/i18n/README.es.md +51 -0
- package/i18n/README.fr.md +51 -0
- package/i18n/README.ja.md +51 -0
- package/i18n/README.ko.md +51 -0
- package/i18n/README.ru.md +51 -0
- package/i18n/README.vi.md +51 -0
- package/i18n/README.zh-Hans.md +51 -0
- package/i18n/README.zh-Hant.md +171 -0
- package/logos/banner-opaque.png +0 -0
- package/logos/logo.png +0 -0
- package/package.json +62 -0
- package/public/app.js +1060 -0
- package/public/index.html +198 -0
- package/public/styles.css +305 -0
- package/run.js +6 -0
- package/scripts/install-docker-ubuntu.sh +140 -0
- package/src/agent-runner.js +686 -0
- package/src/cli.js +181 -0
- package/src/command-policy.js +185 -0
- package/src/config.js +89 -0
- package/src/docker-sandbox.js +309 -0
- package/src/guardrails.js +122 -0
- package/src/model-client.js +212 -0
- package/src/model-routing.js +135 -0
- package/src/redaction.js +29 -0
- package/src/session-store.js +73 -0
- package/src/snapshot.js +55 -0
- package/src/tool-wrappers.js +193 -0
- package/src/web-db.js +158 -0
- package/web.js +530 -0
package/web.js
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { runAgent } from "./src/agent-runner.js";
|
|
6
|
+
import { resolveRuntimeConfig } from "./src/config.js";
|
|
7
|
+
import { WebDatabase } from "./src/web-db.js";
|
|
8
|
+
import { SessionStore } from "./src/session-store.js";
|
|
9
|
+
import { getModelPresets, getProviderDefaults, normalizeRoutingMode } from "./src/model-routing.js";
|
|
10
|
+
import { listAgentWrappers } from "./src/tool-wrappers.js";
|
|
11
|
+
import { getDockerSandboxStatus, getSandboxLogs, runDockerPreflight } from "./src/docker-sandbox.js";
|
|
12
|
+
import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./src/command-policy.js";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const baseDir = process.cwd();
|
|
17
|
+
const sessionsDir = path.join(baseDir, ".sessions");
|
|
18
|
+
|
|
19
|
+
const app = express();
|
|
20
|
+
const port = Number(process.env.PORT || 3210);
|
|
21
|
+
const runs = new Map();
|
|
22
|
+
const db = new WebDatabase(baseDir);
|
|
23
|
+
const supportedLanguages = new Set([
|
|
24
|
+
"en",
|
|
25
|
+
"ar",
|
|
26
|
+
"es",
|
|
27
|
+
"fr",
|
|
28
|
+
"ja",
|
|
29
|
+
"ko",
|
|
30
|
+
"vi",
|
|
31
|
+
"zh-Hans",
|
|
32
|
+
"zh-Hant",
|
|
33
|
+
"de",
|
|
34
|
+
"ru",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function normalizeLanguage(language, fallback = "en") {
|
|
38
|
+
if (supportedLanguages.has(language)) return language;
|
|
39
|
+
return supportedLanguages.has(fallback) ? fallback : "en";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sessionStore(sessionId) {
|
|
43
|
+
return new SessionStore(sessionsDir, sessionId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mapEventLogs(events) {
|
|
47
|
+
return events.map((event) => ({
|
|
48
|
+
at: event.timestamp,
|
|
49
|
+
kind: "event",
|
|
50
|
+
message: event.type,
|
|
51
|
+
data: event.data || {},
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function loadStoredRun(sessionId) {
|
|
56
|
+
const meta = db.getSession(sessionId);
|
|
57
|
+
if (!meta) return null;
|
|
58
|
+
|
|
59
|
+
const store = sessionStore(sessionId);
|
|
60
|
+
const events = await store.loadEvents();
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
sessionId: meta.sessionId,
|
|
64
|
+
status: meta.status,
|
|
65
|
+
provider: meta.provider,
|
|
66
|
+
model: meta.model,
|
|
67
|
+
goal: meta.goal,
|
|
68
|
+
startedAt: meta.startedAt,
|
|
69
|
+
endedAt: meta.endedAt || null,
|
|
70
|
+
result: meta.result || "",
|
|
71
|
+
error: meta.error || "",
|
|
72
|
+
logs: mapEventLogs(events).slice(-300),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function serializeRun(run) {
|
|
77
|
+
return {
|
|
78
|
+
sessionId: run.sessionId,
|
|
79
|
+
status: run.status,
|
|
80
|
+
provider: run.provider,
|
|
81
|
+
model: run.model,
|
|
82
|
+
goal: run.goal,
|
|
83
|
+
startedAt: run.startedAt,
|
|
84
|
+
endedAt: run.endedAt || null,
|
|
85
|
+
result: run.result || "",
|
|
86
|
+
error: run.error || "",
|
|
87
|
+
logs: run.logs.slice(-300),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
|
|
92
|
+
const modelPresets = getModelPresets();
|
|
93
|
+
const routingMode = normalizeRoutingMode(body.routingMode || current.routingMode || "smart");
|
|
94
|
+
const provider =
|
|
95
|
+
body.provider === "openai" || body.provider === "deepseek" ? body.provider : current.provider || "deepseek";
|
|
96
|
+
const providerDefaults = getProviderDefaults(provider);
|
|
97
|
+
const parsedMaxSteps = Number(body.maxSteps);
|
|
98
|
+
const parsedWrapperTimeoutMs = Number(body.wrapperTimeoutMs);
|
|
99
|
+
const sandboxMode = normalizeSandboxMode(body.sandboxMode || current.sandboxMode || "docker-readonly");
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
routingMode,
|
|
103
|
+
provider,
|
|
104
|
+
model:
|
|
105
|
+
typeof body.model === "string" && body.model.trim()
|
|
106
|
+
? body.model.trim()
|
|
107
|
+
: current.provider !== provider
|
|
108
|
+
? providerDefaults.model
|
|
109
|
+
: current.model || modelPresets.fast.model || providerDefaults.model,
|
|
110
|
+
headless: typeof body.headless === "boolean" ? body.headless : Boolean(current.headless),
|
|
111
|
+
maxSteps: Number.isFinite(parsedMaxSteps) && parsedMaxSteps > 0 ? parsedMaxSteps : Number(current.maxSteps) || 15,
|
|
112
|
+
startUrl: typeof body.startUrl === "string" ? body.startUrl.trim() : current.startUrl || "",
|
|
113
|
+
allowedDomains:
|
|
114
|
+
typeof body.allowedDomains === "string" ? body.allowedDomains.trim() : current.allowedDomains || "",
|
|
115
|
+
commandCwd:
|
|
116
|
+
typeof body.commandCwd === "string" && body.commandCwd.trim()
|
|
117
|
+
? body.commandCwd.trim()
|
|
118
|
+
: current.commandCwd || path.resolve(baseDir, ".."),
|
|
119
|
+
allowShellTool: typeof body.allowShellTool === "boolean" ? body.allowShellTool : Boolean(current.allowShellTool),
|
|
120
|
+
allowWrapperTools:
|
|
121
|
+
typeof body.allowWrapperTools === "boolean" ? body.allowWrapperTools : Boolean(current.allowWrapperTools),
|
|
122
|
+
wrapperTimeoutMs:
|
|
123
|
+
Number.isFinite(parsedWrapperTimeoutMs) && parsedWrapperTimeoutMs >= 10000
|
|
124
|
+
? parsedWrapperTimeoutMs
|
|
125
|
+
: Number(current.wrapperTimeoutMs) || 120000,
|
|
126
|
+
useDockerSandbox:
|
|
127
|
+
sandboxMode !== "host"
|
|
128
|
+
? true
|
|
129
|
+
: typeof body.useDockerSandbox === "boolean"
|
|
130
|
+
? body.useDockerSandbox
|
|
131
|
+
: Boolean(current.useDockerSandbox),
|
|
132
|
+
sandboxMode,
|
|
133
|
+
packageInstallPolicy: normalizePackageInstallPolicy(
|
|
134
|
+
body.packageInstallPolicy || current.packageInstallPolicy || "prompt"
|
|
135
|
+
),
|
|
136
|
+
dockerSandboxImage:
|
|
137
|
+
typeof body.dockerSandboxImage === "string" && body.dockerSandboxImage.trim()
|
|
138
|
+
? body.dockerSandboxImage.trim()
|
|
139
|
+
: current.dockerSandboxImage || "agintiflow-sandbox:latest",
|
|
140
|
+
allowPasswords: typeof body.allowPasswords === "boolean" ? body.allowPasswords : Boolean(current.allowPasswords),
|
|
141
|
+
allowDestructive:
|
|
142
|
+
typeof body.allowDestructive === "boolean" ? body.allowDestructive : Boolean(current.allowDestructive),
|
|
143
|
+
language: normalizeLanguage(body.language, current.language),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function publicProviderDefault(provider) {
|
|
148
|
+
const defaults = getProviderDefaults(provider);
|
|
149
|
+
return {
|
|
150
|
+
provider: defaults.provider,
|
|
151
|
+
model: defaults.model,
|
|
152
|
+
baseURL: defaults.baseURL,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildRunConfig(body, overrides = {}) {
|
|
157
|
+
const preferences = normalizePreferencePayload(body, db.getPreferences());
|
|
158
|
+
const merged = {
|
|
159
|
+
...preferences,
|
|
160
|
+
...overrides,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return resolveRuntimeConfig(
|
|
164
|
+
{
|
|
165
|
+
goal: String(body.goal || "").trim(),
|
|
166
|
+
startUrl: merged.startUrl || "",
|
|
167
|
+
resume: overrides.resume || "",
|
|
168
|
+
sessionId: overrides.sessionId || "",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
provider: merged.provider,
|
|
172
|
+
model: merged.model || getProviderDefaults(merged.provider).model,
|
|
173
|
+
routingMode: merged.routingMode,
|
|
174
|
+
headless: merged.headless,
|
|
175
|
+
maxSteps: merged.maxSteps,
|
|
176
|
+
allowedDomains: String(merged.allowedDomains || "")
|
|
177
|
+
.split(",")
|
|
178
|
+
.map((item) => item.trim())
|
|
179
|
+
.filter(Boolean),
|
|
180
|
+
allowPasswords: merged.allowPasswords,
|
|
181
|
+
allowDestructive: merged.allowDestructive,
|
|
182
|
+
allowShellTool: merged.allowShellTool,
|
|
183
|
+
allowWrapperTools: merged.allowWrapperTools,
|
|
184
|
+
wrapperTimeoutMs: merged.wrapperTimeoutMs,
|
|
185
|
+
sandboxMode: merged.sandboxMode,
|
|
186
|
+
packageInstallPolicy: merged.packageInstallPolicy,
|
|
187
|
+
useDockerSandbox: merged.useDockerSandbox,
|
|
188
|
+
dockerSandboxImage: merged.dockerSandboxImage,
|
|
189
|
+
commandCwd: merged.commandCwd,
|
|
190
|
+
baseDir,
|
|
191
|
+
sessionId: overrides.sessionId,
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseToolContent(message) {
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(message.content);
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function deriveSessionRecordFromState(state, existing = null) {
|
|
205
|
+
const updatedAt = state.updatedAt || state.createdAt || existing?.updatedAt || new Date().toISOString();
|
|
206
|
+
const finishMessage = [...(state.messages || [])]
|
|
207
|
+
.reverse()
|
|
208
|
+
.find((message) => message.role === "tool" && typeof message.content === "string");
|
|
209
|
+
const finishPayload = finishMessage ? parseToolContent(finishMessage) : null;
|
|
210
|
+
const status = finishPayload?.done ? "finished" : existing?.status && existing.status !== "running" ? existing.status : "saved";
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
sessionId: state.sessionId,
|
|
214
|
+
provider: state.provider || existing?.provider || "deepseek",
|
|
215
|
+
model: state.model || existing?.model || getProviderDefaults(state.provider || existing?.provider || "deepseek").model,
|
|
216
|
+
goal: state.goal || existing?.goal || "",
|
|
217
|
+
status,
|
|
218
|
+
startedAt: state.createdAt || existing?.startedAt || new Date().toISOString(),
|
|
219
|
+
updatedAt,
|
|
220
|
+
endedAt: finishPayload?.done ? updatedAt : existing?.endedAt || null,
|
|
221
|
+
result: finishPayload?.done ? String(finishPayload.result || "") : existing?.result || "",
|
|
222
|
+
error: existing?.error || "",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function syncStoredSessions() {
|
|
227
|
+
const entries = await fs.readdir(sessionsDir, { withFileTypes: true }).catch(() => []);
|
|
228
|
+
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
if (!entry.isDirectory()) continue;
|
|
231
|
+
|
|
232
|
+
const store = sessionStore(entry.name);
|
|
233
|
+
const state = await store.loadState();
|
|
234
|
+
if (!state?.sessionId) continue;
|
|
235
|
+
|
|
236
|
+
const existing = db.getSession(state.sessionId);
|
|
237
|
+
db.upsertSession(deriveSessionRecordFromState(state, existing));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function deriveChatFromState(state, meta) {
|
|
242
|
+
if (!state) {
|
|
243
|
+
return meta?.goal
|
|
244
|
+
? [
|
|
245
|
+
{
|
|
246
|
+
role: "user",
|
|
247
|
+
content: meta.goal,
|
|
248
|
+
at: meta.startedAt || new Date().toISOString(),
|
|
249
|
+
},
|
|
250
|
+
]
|
|
251
|
+
: [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (Array.isArray(state.chat) && state.chat.length > 0) {
|
|
255
|
+
return state.chat;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const chat = [];
|
|
259
|
+
if (state.goal) {
|
|
260
|
+
chat.push({
|
|
261
|
+
role: "user",
|
|
262
|
+
content: state.goal,
|
|
263
|
+
at: state.createdAt || new Date().toISOString(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const finishTool = [...(state.messages || [])]
|
|
268
|
+
.reverse()
|
|
269
|
+
.find((message) => message.role === "tool" && typeof message.content === "string");
|
|
270
|
+
|
|
271
|
+
if (finishTool) {
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(finishTool.content);
|
|
274
|
+
if (parsed.done && parsed.result) {
|
|
275
|
+
chat.push({
|
|
276
|
+
role: "assistant",
|
|
277
|
+
content: parsed.result,
|
|
278
|
+
at: state.updatedAt || new Date().toISOString(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// Best-effort fallback only.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return chat;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function loadChat(sessionId) {
|
|
290
|
+
const store = sessionStore(sessionId);
|
|
291
|
+
const [state, meta] = await Promise.all([store.loadState(), Promise.resolve(db.getSession(sessionId))]);
|
|
292
|
+
|
|
293
|
+
if (!state && !meta) return null;
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
sessionId,
|
|
297
|
+
goal: state?.goal || meta?.goal || "",
|
|
298
|
+
provider: state?.provider || meta?.provider || "",
|
|
299
|
+
model: state?.model || meta?.model || "",
|
|
300
|
+
chat: deriveChatFromState(state, meta),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function ensureNotRunning(sessionId) {
|
|
305
|
+
const inMemory = runs.get(sessionId);
|
|
306
|
+
if (inMemory?.status === "running") {
|
|
307
|
+
throw new Error("This session is already running.");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const meta = db.getSession(sessionId);
|
|
311
|
+
if (meta?.status === "running") {
|
|
312
|
+
throw new Error("This session is already running.");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createRunRecord(config, goal, existingLogs = []) {
|
|
317
|
+
return {
|
|
318
|
+
sessionId: config.sessionId,
|
|
319
|
+
status: "running",
|
|
320
|
+
provider: config.provider,
|
|
321
|
+
model: config.model,
|
|
322
|
+
goal,
|
|
323
|
+
startedAt: new Date().toISOString(),
|
|
324
|
+
updatedAt: new Date().toISOString(),
|
|
325
|
+
endedAt: "",
|
|
326
|
+
result: "",
|
|
327
|
+
error: "",
|
|
328
|
+
logs: [...existingLogs],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function wireRun(record, config) {
|
|
333
|
+
const push = (kind, message, data = {}) => {
|
|
334
|
+
record.logs.push({
|
|
335
|
+
at: new Date().toISOString(),
|
|
336
|
+
kind,
|
|
337
|
+
message,
|
|
338
|
+
data,
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
push("session", `Started session ${record.sessionId}`);
|
|
343
|
+
record.updatedAt = new Date().toISOString();
|
|
344
|
+
db.upsertSession(record);
|
|
345
|
+
|
|
346
|
+
void runAgent({
|
|
347
|
+
...config,
|
|
348
|
+
onLog: (message, data = {}) => push("log", message, data),
|
|
349
|
+
onEvent: (type, data = {}) => push("event", type, data),
|
|
350
|
+
})
|
|
351
|
+
.then((result) => {
|
|
352
|
+
record.status = "finished";
|
|
353
|
+
record.result = result?.result || "";
|
|
354
|
+
record.updatedAt = new Date().toISOString();
|
|
355
|
+
record.endedAt = new Date().toISOString();
|
|
356
|
+
push("session", "Run finished", { result: record.result });
|
|
357
|
+
db.upsertSession(record);
|
|
358
|
+
})
|
|
359
|
+
.catch((error) => {
|
|
360
|
+
record.status = "failed";
|
|
361
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
362
|
+
record.updatedAt = new Date().toISOString();
|
|
363
|
+
record.endedAt = new Date().toISOString();
|
|
364
|
+
push("error", "Run failed", { error: record.error });
|
|
365
|
+
db.upsertSession(record);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
app.use(express.json({ limit: "1mb" }));
|
|
370
|
+
app.use(express.static(path.join(__dirname, "public")));
|
|
371
|
+
app.use("/logos", express.static(path.join(__dirname, "logos")));
|
|
372
|
+
|
|
373
|
+
app.get("/api/config", (_req, res) => {
|
|
374
|
+
const preferences = normalizePreferencePayload({}, db.getPreferences());
|
|
375
|
+
res.json({
|
|
376
|
+
defaults: {
|
|
377
|
+
openai: publicProviderDefault("openai"),
|
|
378
|
+
deepseek: publicProviderDefault("deepseek"),
|
|
379
|
+
headless: true,
|
|
380
|
+
maxSteps: 15,
|
|
381
|
+
},
|
|
382
|
+
routing: {
|
|
383
|
+
modes: ["smart", "fast", "complex", "manual"],
|
|
384
|
+
presets: getModelPresets(),
|
|
385
|
+
},
|
|
386
|
+
wrappers: listAgentWrappers(),
|
|
387
|
+
sandbox: {
|
|
388
|
+
logs: getSandboxLogs(),
|
|
389
|
+
},
|
|
390
|
+
preferences,
|
|
391
|
+
keyStatus: {
|
|
392
|
+
openai: Boolean(process.env.OPENAI_API_KEY),
|
|
393
|
+
deepseek: Boolean(process.env.DEEPSEEK_API_KEY),
|
|
394
|
+
},
|
|
395
|
+
sessions: db.listSessions(20),
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
app.get("/api/sandbox/status", async (_req, res) => {
|
|
400
|
+
const config = buildRunConfig({ ...db.getPreferences(), goal: "" });
|
|
401
|
+
const status = await getDockerSandboxStatus(config);
|
|
402
|
+
res.json({ ok: true, status });
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
app.post("/api/sandbox/preflight", async (req, res) => {
|
|
406
|
+
const body = {
|
|
407
|
+
...db.getPreferences(),
|
|
408
|
+
...(req.body || {}),
|
|
409
|
+
goal: "",
|
|
410
|
+
};
|
|
411
|
+
const config = buildRunConfig(body);
|
|
412
|
+
const result = await runDockerPreflight(config, { buildImage: Boolean(req.body?.buildImage) });
|
|
413
|
+
res.json(result);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
app.post("/api/preferences", (req, res) => {
|
|
417
|
+
const preferences = normalizePreferencePayload(req.body || {}, db.getPreferences());
|
|
418
|
+
db.savePreferences(preferences);
|
|
419
|
+
res.json({ ok: true, preferences });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
app.get("/api/sessions", (_req, res) => {
|
|
423
|
+
res.json({ sessions: db.listSessions(20) });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
app.get("/api/sessions/:sessionId/chat", async (req, res) => {
|
|
427
|
+
const data = await loadChat(req.params.sessionId);
|
|
428
|
+
if (!data) {
|
|
429
|
+
res.status(404).json({ error: "Session not found." });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
res.json(data);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
app.post("/api/runs", async (req, res) => {
|
|
436
|
+
const body = req.body || {};
|
|
437
|
+
const goal = String(body.goal || "").trim();
|
|
438
|
+
if (!goal) {
|
|
439
|
+
res.status(400).json({ error: "Goal is required." });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const config = buildRunConfig(body);
|
|
444
|
+
db.savePreferences(normalizePreferencePayload(body, db.getPreferences()));
|
|
445
|
+
|
|
446
|
+
if (!config.apiKey) {
|
|
447
|
+
res.status(400).json({ error: `Missing API key for ${config.provider}.` });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const run = createRunRecord(config, goal);
|
|
452
|
+
runs.set(run.sessionId, run);
|
|
453
|
+
wireRun(run, config);
|
|
454
|
+
res.json({ sessionId: run.sessionId });
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
app.post("/api/sessions/:sessionId/messages", async (req, res) => {
|
|
458
|
+
const sessionId = req.params.sessionId;
|
|
459
|
+
const content = String(req.body?.content || "").trim();
|
|
460
|
+
|
|
461
|
+
if (!content) {
|
|
462
|
+
res.status(400).json({ error: "Message content is required." });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
await ensureNotRunning(sessionId);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const meta = db.getSession(sessionId);
|
|
474
|
+
if (!meta) {
|
|
475
|
+
res.status(404).json({ error: "Session not found." });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const body = {
|
|
480
|
+
...db.getPreferences(),
|
|
481
|
+
...req.body,
|
|
482
|
+
goal: content,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const config = buildRunConfig(body, {
|
|
486
|
+
resume: sessionId,
|
|
487
|
+
sessionId,
|
|
488
|
+
provider: body.provider || meta.provider,
|
|
489
|
+
model: body.model || meta.model,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (!config.apiKey) {
|
|
493
|
+
res.status(400).json({ error: `Missing API key for ${config.provider}.` });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
db.savePreferences(normalizePreferencePayload(body, db.getPreferences()));
|
|
498
|
+
|
|
499
|
+
const existing = runs.get(sessionId);
|
|
500
|
+
const stored = existing || (await loadStoredRun(sessionId));
|
|
501
|
+
const run = createRunRecord(config, content, stored?.logs || []);
|
|
502
|
+
runs.set(sessionId, run);
|
|
503
|
+
wireRun(run, config);
|
|
504
|
+
res.json({ sessionId });
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
app.get("/api/runs/:sessionId", async (req, res) => {
|
|
508
|
+
const inMemory = runs.get(req.params.sessionId);
|
|
509
|
+
if (inMemory) {
|
|
510
|
+
res.json(serializeRun(inMemory));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const stored = await loadStoredRun(req.params.sessionId);
|
|
515
|
+
if (!stored) {
|
|
516
|
+
res.status(404).json({ error: "Run not found." });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
res.json(stored);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
app.get("/health", (_req, res) => {
|
|
523
|
+
res.json({ ok: true, port });
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await syncStoredSessions();
|
|
527
|
+
|
|
528
|
+
app.listen(port, () => {
|
|
529
|
+
console.log(`Website control agent UI running on http://127.0.0.1:${port}`);
|
|
530
|
+
});
|