teleton 0.4.0 → 0.5.2
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/README.md +88 -13
- package/dist/BigInteger-DQ33LTTE.js +5 -0
- package/dist/chunk-4DU3C27M.js +30 -0
- package/dist/chunk-5WWR4CU3.js +124 -0
- package/dist/{chunk-E2NXSWOS.js → chunk-NUGDTPE4.js} +24 -64
- package/dist/{chunk-OA5L7GM6.js → chunk-O4R7V5Y2.js} +37 -5
- package/dist/chunk-QUAPFI2N.js +42 -0
- package/dist/chunk-TSKJCWQQ.js +1263 -0
- package/dist/{chunk-B2PRMXOH.js → chunk-WL2Q3VRD.js} +0 -2
- package/dist/{chunk-QU4ZOR35.js → chunk-WOXBZOQX.js} +3179 -3368
- package/dist/{chunk-7UPH62J2.js → chunk-WUTMT6DW.js} +293 -261
- package/dist/{chunk-OQGNS2FV.js → chunk-YBA6IBGT.js} +20 -5
- package/dist/cli/index.js +41 -172
- package/dist/{endpoint-FT2B2RZ2.js → endpoint-FLYNEZ2F.js} +1 -1
- package/dist/{get-my-gifts-AFKBG4YQ.js → get-my-gifts-KVULMBJ3.js} +1 -1
- package/dist/index.js +12 -12
- package/dist/{memory-SYTQ5P7P.js → memory-Y5J7CXAR.js} +4 -5
- package/dist/{migrate-ITXMRRSZ.js → migrate-UEQCDWL2.js} +4 -5
- package/dist/server-BQY7CM2N.js +1120 -0
- package/dist/{task-dependency-resolver-GY6PEBIS.js → task-dependency-resolver-TRPILAHM.js} +2 -2
- package/dist/{task-executor-4QKTZZ3P.js → task-executor-N7XNVK5N.js} +1 -1
- package/dist/{tasks-M3QDPTGY.js → tasks-QSCWSMPS.js} +1 -1
- package/dist/{transcript-DF2Y6CFY.js → transcript-7V4UNID4.js} +1 -1
- package/dist/web/assets/index-CDMbujHf.css +1 -0
- package/dist/web/assets/index-DDX8oQ2z.js +67 -0
- package/dist/web/index.html +16 -0
- package/dist/web/logo_dark.png +0 -0
- package/package.json +23 -6
- package/dist/chunk-67QC5FBN.js +0 -60
- package/dist/chunk-A64NPEFL.js +0 -74
- package/dist/chunk-DUW5VBAZ.js +0 -133
- package/dist/chunk-QBGUCUOW.js +0 -16
- package/dist/scraper-SH7GS7TO.js +0 -282
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WorkspaceSecurityError,
|
|
3
|
+
validateDirectory,
|
|
4
|
+
validatePath,
|
|
5
|
+
validateReadPath,
|
|
6
|
+
validateWritePath
|
|
7
|
+
} from "./chunk-5WWR4CU3.js";
|
|
8
|
+
import "./chunk-O4R7V5Y2.js";
|
|
9
|
+
import {
|
|
10
|
+
WORKSPACE_ROOT
|
|
11
|
+
} from "./chunk-EYWNOHMJ.js";
|
|
12
|
+
import {
|
|
13
|
+
getTaskStore
|
|
14
|
+
} from "./chunk-NUGDTPE4.js";
|
|
15
|
+
import "./chunk-QGM4M3NI.js";
|
|
16
|
+
|
|
17
|
+
// src/webui/server.ts
|
|
18
|
+
import { Hono as Hono9 } from "hono";
|
|
19
|
+
import { serve } from "@hono/node-server";
|
|
20
|
+
import { cors } from "hono/cors";
|
|
21
|
+
import { bodyLimit } from "hono/body-limit";
|
|
22
|
+
import { setCookie, getCookie, deleteCookie } from "hono/cookie";
|
|
23
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
24
|
+
import { join as join3, dirname, resolve, relative as relative2 } from "path";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
26
|
+
|
|
27
|
+
// src/webui/middleware/auth.ts
|
|
28
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
29
|
+
var COOKIE_NAME = "teleton_session";
|
|
30
|
+
var COOKIE_MAX_AGE = 7 * 24 * 60 * 60;
|
|
31
|
+
function generateToken() {
|
|
32
|
+
return randomBytes(32).toString("base64url");
|
|
33
|
+
}
|
|
34
|
+
function maskToken(token) {
|
|
35
|
+
if (token.length < 12) return "****";
|
|
36
|
+
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
37
|
+
}
|
|
38
|
+
function safeCompare(a, b) {
|
|
39
|
+
if (!a || !b) return false;
|
|
40
|
+
const bufA = Buffer.from(a);
|
|
41
|
+
const bufB = Buffer.from(b);
|
|
42
|
+
if (bufA.length !== bufB.length) return false;
|
|
43
|
+
return timingSafeEqual(bufA, bufB);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/webui/log-interceptor.ts
|
|
47
|
+
var LogInterceptor = class {
|
|
48
|
+
listeners = /* @__PURE__ */ new Set();
|
|
49
|
+
isPatched = false;
|
|
50
|
+
originalMethods = {
|
|
51
|
+
log: console.log,
|
|
52
|
+
warn: console.warn,
|
|
53
|
+
error: console.error
|
|
54
|
+
};
|
|
55
|
+
install() {
|
|
56
|
+
if (this.isPatched) return;
|
|
57
|
+
const levels = ["log", "warn", "error"];
|
|
58
|
+
for (const level of levels) {
|
|
59
|
+
const original = this.originalMethods[level];
|
|
60
|
+
console[level] = (...args) => {
|
|
61
|
+
original.apply(console, args);
|
|
62
|
+
if (this.listeners.size > 0) {
|
|
63
|
+
const entry = {
|
|
64
|
+
level,
|
|
65
|
+
message: args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" "),
|
|
66
|
+
timestamp: Date.now()
|
|
67
|
+
};
|
|
68
|
+
for (const listener of this.listeners) {
|
|
69
|
+
try {
|
|
70
|
+
listener(entry);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
original.call(console, "\u274C Log listener error:", err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
this.isPatched = true;
|
|
79
|
+
}
|
|
80
|
+
uninstall() {
|
|
81
|
+
if (!this.isPatched) return;
|
|
82
|
+
console.log = this.originalMethods.log;
|
|
83
|
+
console.warn = this.originalMethods.warn;
|
|
84
|
+
console.error = this.originalMethods.error;
|
|
85
|
+
this.isPatched = false;
|
|
86
|
+
}
|
|
87
|
+
addListener(listener) {
|
|
88
|
+
this.listeners.add(listener);
|
|
89
|
+
return () => this.listeners.delete(listener);
|
|
90
|
+
}
|
|
91
|
+
removeListener(listener) {
|
|
92
|
+
this.listeners.delete(listener);
|
|
93
|
+
}
|
|
94
|
+
clear() {
|
|
95
|
+
this.listeners.clear();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var logInterceptor = new LogInterceptor();
|
|
99
|
+
|
|
100
|
+
// src/webui/routes/status.ts
|
|
101
|
+
import { Hono } from "hono";
|
|
102
|
+
function createStatusRoutes(deps) {
|
|
103
|
+
const app = new Hono();
|
|
104
|
+
app.get("/", (c) => {
|
|
105
|
+
try {
|
|
106
|
+
const config = deps.agent.getConfig();
|
|
107
|
+
const sessionCountRow = deps.memory.db.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
108
|
+
const data = {
|
|
109
|
+
uptime: process.uptime(),
|
|
110
|
+
model: config.agent.model,
|
|
111
|
+
provider: config.agent.provider,
|
|
112
|
+
sessionCount: sessionCountRow?.count ?? 0,
|
|
113
|
+
paused: false,
|
|
114
|
+
// TODO: get from message handler
|
|
115
|
+
toolCount: deps.toolRegistry.getAll().length
|
|
116
|
+
};
|
|
117
|
+
const response = {
|
|
118
|
+
success: true,
|
|
119
|
+
data
|
|
120
|
+
};
|
|
121
|
+
return c.json(response);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const response = {
|
|
124
|
+
success: false,
|
|
125
|
+
error: error instanceof Error ? error.message : String(error)
|
|
126
|
+
};
|
|
127
|
+
return c.json(response, 500);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return app;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/webui/routes/tools.ts
|
|
134
|
+
import { Hono as Hono2 } from "hono";
|
|
135
|
+
function createToolsRoutes(deps) {
|
|
136
|
+
const app = new Hono2();
|
|
137
|
+
app.get("/", (c) => {
|
|
138
|
+
try {
|
|
139
|
+
const allTools = deps.toolRegistry.getAll();
|
|
140
|
+
const modules = deps.toolRegistry.getAvailableModules();
|
|
141
|
+
const toolMap = new Map(allTools.map((t) => [t.name, t]));
|
|
142
|
+
const moduleData = modules.map((moduleName) => {
|
|
143
|
+
const moduleToolNames = deps.toolRegistry.getModuleTools(moduleName);
|
|
144
|
+
const toolsInfo = moduleToolNames.map((toolEntry) => {
|
|
145
|
+
const tool = toolMap.get(toolEntry.name);
|
|
146
|
+
if (!tool) return null;
|
|
147
|
+
const config = deps.toolRegistry.getToolConfig(toolEntry.name);
|
|
148
|
+
return {
|
|
149
|
+
name: tool.name,
|
|
150
|
+
description: tool.description || "",
|
|
151
|
+
module: moduleName,
|
|
152
|
+
scope: config?.scope ?? toolEntry.scope,
|
|
153
|
+
category: deps.toolRegistry.getToolCategory(tool.name),
|
|
154
|
+
enabled: config?.enabled ?? true
|
|
155
|
+
};
|
|
156
|
+
}).filter((t) => t !== null);
|
|
157
|
+
return {
|
|
158
|
+
name: moduleName,
|
|
159
|
+
toolCount: moduleToolNames.length,
|
|
160
|
+
tools: toolsInfo,
|
|
161
|
+
isPlugin: deps.toolRegistry.isPluginModule(moduleName)
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
const response = {
|
|
165
|
+
success: true,
|
|
166
|
+
data: moduleData
|
|
167
|
+
};
|
|
168
|
+
return c.json(response);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const response = {
|
|
171
|
+
success: false,
|
|
172
|
+
error: error instanceof Error ? error.message : String(error)
|
|
173
|
+
};
|
|
174
|
+
return c.json(response, 500);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
app.put("/:name", async (c) => {
|
|
178
|
+
try {
|
|
179
|
+
const toolName = c.req.param("name");
|
|
180
|
+
const body = await c.req.json();
|
|
181
|
+
if (!deps.toolRegistry.has(toolName)) {
|
|
182
|
+
const response2 = {
|
|
183
|
+
success: false,
|
|
184
|
+
error: `Tool "${toolName}" not found`
|
|
185
|
+
};
|
|
186
|
+
return c.json(response2, 404);
|
|
187
|
+
}
|
|
188
|
+
const { enabled, scope } = body;
|
|
189
|
+
const VALID_SCOPES = ["always", "dm-only", "group-only", "admin-only"];
|
|
190
|
+
if (scope !== void 0 && !VALID_SCOPES.includes(scope)) {
|
|
191
|
+
const response2 = {
|
|
192
|
+
success: false,
|
|
193
|
+
error: `Invalid scope "${scope}". Must be one of: ${VALID_SCOPES.join(", ")}`
|
|
194
|
+
};
|
|
195
|
+
return c.json(response2, 400);
|
|
196
|
+
}
|
|
197
|
+
if (enabled !== void 0) {
|
|
198
|
+
const success = deps.toolRegistry.setToolEnabled(toolName, enabled);
|
|
199
|
+
if (!success) {
|
|
200
|
+
const response2 = {
|
|
201
|
+
success: false,
|
|
202
|
+
error: "Failed to update tool enabled status"
|
|
203
|
+
};
|
|
204
|
+
return c.json(response2, 500);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (scope !== void 0) {
|
|
208
|
+
const success = deps.toolRegistry.updateToolScope(
|
|
209
|
+
toolName,
|
|
210
|
+
scope
|
|
211
|
+
);
|
|
212
|
+
if (!success) {
|
|
213
|
+
const response2 = {
|
|
214
|
+
success: false,
|
|
215
|
+
error: "Failed to update tool scope"
|
|
216
|
+
};
|
|
217
|
+
return c.json(response2, 500);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const config = deps.toolRegistry.getToolConfig(toolName);
|
|
221
|
+
const response = {
|
|
222
|
+
success: true,
|
|
223
|
+
data: {
|
|
224
|
+
tool: toolName,
|
|
225
|
+
enabled: config?.enabled ?? true,
|
|
226
|
+
scope: config?.scope ?? "always"
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
return c.json(response);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const response = {
|
|
232
|
+
success: false,
|
|
233
|
+
error: error instanceof Error ? error.message : String(error)
|
|
234
|
+
};
|
|
235
|
+
return c.json(response, 500);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
app.get("/:name/config", (c) => {
|
|
239
|
+
try {
|
|
240
|
+
const toolName = c.req.param("name");
|
|
241
|
+
if (!deps.toolRegistry.has(toolName)) {
|
|
242
|
+
const response2 = {
|
|
243
|
+
success: false,
|
|
244
|
+
error: `Tool "${toolName}" not found`
|
|
245
|
+
};
|
|
246
|
+
return c.json(response2, 404);
|
|
247
|
+
}
|
|
248
|
+
const config = deps.toolRegistry.getToolConfig(toolName);
|
|
249
|
+
const response = {
|
|
250
|
+
success: true,
|
|
251
|
+
data: {
|
|
252
|
+
tool: toolName,
|
|
253
|
+
enabled: config?.enabled ?? true,
|
|
254
|
+
scope: config?.scope ?? "always"
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
return c.json(response);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const response = {
|
|
260
|
+
success: false,
|
|
261
|
+
error: error instanceof Error ? error.message : String(error)
|
|
262
|
+
};
|
|
263
|
+
return c.json(response, 500);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
app.get("/:module", (c) => {
|
|
267
|
+
try {
|
|
268
|
+
const moduleName = c.req.param("module");
|
|
269
|
+
const allTools = deps.toolRegistry.getAll();
|
|
270
|
+
const toolMap = new Map(allTools.map((t) => [t.name, t]));
|
|
271
|
+
const moduleToolNames = deps.toolRegistry.getModuleTools(moduleName);
|
|
272
|
+
const toolsInfo = moduleToolNames.map((toolEntry) => {
|
|
273
|
+
const tool = toolMap.get(toolEntry.name);
|
|
274
|
+
if (!tool) return null;
|
|
275
|
+
const config = deps.toolRegistry.getToolConfig(toolEntry.name);
|
|
276
|
+
return {
|
|
277
|
+
name: tool.name,
|
|
278
|
+
description: tool.description || "",
|
|
279
|
+
module: moduleName,
|
|
280
|
+
scope: config?.scope ?? toolEntry.scope,
|
|
281
|
+
category: deps.toolRegistry.getToolCategory(tool.name),
|
|
282
|
+
enabled: config?.enabled ?? true
|
|
283
|
+
};
|
|
284
|
+
}).filter((t) => t !== null);
|
|
285
|
+
const response = {
|
|
286
|
+
success: true,
|
|
287
|
+
data: toolsInfo
|
|
288
|
+
};
|
|
289
|
+
return c.json(response);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const response = {
|
|
292
|
+
success: false,
|
|
293
|
+
error: error instanceof Error ? error.message : String(error)
|
|
294
|
+
};
|
|
295
|
+
return c.json(response, 500);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return app;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/webui/routes/logs.ts
|
|
302
|
+
import { Hono as Hono3 } from "hono";
|
|
303
|
+
import { streamSSE } from "hono/streaming";
|
|
304
|
+
function createLogsRoutes(_deps) {
|
|
305
|
+
const app = new Hono3();
|
|
306
|
+
app.get("/stream", (c) => {
|
|
307
|
+
return streamSSE(c, async (stream) => {
|
|
308
|
+
let cleanup;
|
|
309
|
+
let aborted = false;
|
|
310
|
+
stream.onAbort(() => {
|
|
311
|
+
aborted = true;
|
|
312
|
+
if (cleanup) cleanup();
|
|
313
|
+
});
|
|
314
|
+
cleanup = logInterceptor.addListener((entry) => {
|
|
315
|
+
if (!aborted) {
|
|
316
|
+
stream.writeSSE({
|
|
317
|
+
data: JSON.stringify(entry),
|
|
318
|
+
event: "log"
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
await stream.writeSSE({
|
|
323
|
+
data: JSON.stringify({
|
|
324
|
+
level: "log",
|
|
325
|
+
message: "\u{1F310} WebUI log stream connected",
|
|
326
|
+
timestamp: Date.now()
|
|
327
|
+
}),
|
|
328
|
+
event: "log"
|
|
329
|
+
});
|
|
330
|
+
await new Promise((resolve2) => {
|
|
331
|
+
if (aborted) return resolve2();
|
|
332
|
+
stream.onAbort(() => resolve2());
|
|
333
|
+
});
|
|
334
|
+
if (cleanup) cleanup();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
return app;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/webui/routes/memory.ts
|
|
341
|
+
import { Hono as Hono4 } from "hono";
|
|
342
|
+
function createMemoryRoutes(deps) {
|
|
343
|
+
const app = new Hono4();
|
|
344
|
+
app.get("/search", async (c) => {
|
|
345
|
+
try {
|
|
346
|
+
const query = c.req.query("q") || "";
|
|
347
|
+
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
348
|
+
if (!query) {
|
|
349
|
+
const response2 = {
|
|
350
|
+
success: false,
|
|
351
|
+
error: "Query parameter 'q' is required"
|
|
352
|
+
};
|
|
353
|
+
return c.json(response2, 400);
|
|
354
|
+
}
|
|
355
|
+
const sanitizedQuery = '"' + query.replace(/"/g, '""') + '"';
|
|
356
|
+
const results = deps.memory.db.prepare(
|
|
357
|
+
`
|
|
358
|
+
SELECT
|
|
359
|
+
k.id,
|
|
360
|
+
k.text,
|
|
361
|
+
k.source,
|
|
362
|
+
k.path,
|
|
363
|
+
bm25(knowledge_fts) as score
|
|
364
|
+
FROM knowledge_fts
|
|
365
|
+
JOIN knowledge k ON knowledge_fts.rowid = k.rowid
|
|
366
|
+
WHERE knowledge_fts MATCH ?
|
|
367
|
+
ORDER BY score DESC
|
|
368
|
+
LIMIT ?
|
|
369
|
+
`
|
|
370
|
+
).all(sanitizedQuery, limit);
|
|
371
|
+
const searchResults = results.map((row) => ({
|
|
372
|
+
id: row.id,
|
|
373
|
+
text: row.text,
|
|
374
|
+
source: row.path || row.source,
|
|
375
|
+
score: Math.max(0, 1 - row.score / 10),
|
|
376
|
+
// Normalize BM25 score to 0-1 range
|
|
377
|
+
keywordScore: Math.max(0, 1 - row.score / 10)
|
|
378
|
+
}));
|
|
379
|
+
const response = {
|
|
380
|
+
success: true,
|
|
381
|
+
data: searchResults
|
|
382
|
+
};
|
|
383
|
+
return c.json(response);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const response = {
|
|
386
|
+
success: false,
|
|
387
|
+
error: error instanceof Error ? error.message : String(error)
|
|
388
|
+
};
|
|
389
|
+
return c.json(response, 500);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
app.get("/sessions", (c) => {
|
|
393
|
+
try {
|
|
394
|
+
const rows = deps.memory.db.prepare(
|
|
395
|
+
`
|
|
396
|
+
SELECT
|
|
397
|
+
chat_id,
|
|
398
|
+
id,
|
|
399
|
+
message_count,
|
|
400
|
+
context_tokens,
|
|
401
|
+
updated_at
|
|
402
|
+
FROM sessions
|
|
403
|
+
ORDER BY updated_at DESC
|
|
404
|
+
`
|
|
405
|
+
).all();
|
|
406
|
+
const sessions = rows.map((row) => ({
|
|
407
|
+
chatId: row.chat_id,
|
|
408
|
+
sessionId: row.id,
|
|
409
|
+
messageCount: row.message_count,
|
|
410
|
+
contextTokens: row.context_tokens,
|
|
411
|
+
lastActivity: row.updated_at
|
|
412
|
+
}));
|
|
413
|
+
const response = {
|
|
414
|
+
success: true,
|
|
415
|
+
data: sessions
|
|
416
|
+
};
|
|
417
|
+
return c.json(response);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
const response = {
|
|
420
|
+
success: false,
|
|
421
|
+
error: error instanceof Error ? error.message : String(error)
|
|
422
|
+
};
|
|
423
|
+
return c.json(response, 500);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
app.get("/stats", (c) => {
|
|
427
|
+
try {
|
|
428
|
+
const stats = {
|
|
429
|
+
knowledge: deps.memory.db.prepare("SELECT COUNT(*) as count FROM knowledge").get().count,
|
|
430
|
+
sessions: deps.memory.db.prepare("SELECT COUNT(*) as count FROM sessions").get().count,
|
|
431
|
+
messages: deps.memory.db.prepare("SELECT COUNT(*) as count FROM tg_messages").get().count,
|
|
432
|
+
chats: deps.memory.db.prepare("SELECT COUNT(*) as count FROM tg_chats").get().count
|
|
433
|
+
};
|
|
434
|
+
const response = {
|
|
435
|
+
success: true,
|
|
436
|
+
data: stats
|
|
437
|
+
};
|
|
438
|
+
return c.json(response);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
const response = {
|
|
441
|
+
success: false,
|
|
442
|
+
error: error instanceof Error ? error.message : String(error)
|
|
443
|
+
};
|
|
444
|
+
return c.json(response, 500);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
return app;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/webui/routes/soul.ts
|
|
451
|
+
import { Hono as Hono5 } from "hono";
|
|
452
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
453
|
+
import { join } from "path";
|
|
454
|
+
var SOUL_FILES = ["SOUL.md", "SECURITY.md", "STRATEGY.md", "MEMORY.md"];
|
|
455
|
+
function isSoulFile(filename) {
|
|
456
|
+
return SOUL_FILES.includes(filename);
|
|
457
|
+
}
|
|
458
|
+
function createSoulRoutes(_deps) {
|
|
459
|
+
const app = new Hono5();
|
|
460
|
+
app.get("/:file", (c) => {
|
|
461
|
+
try {
|
|
462
|
+
const filename = c.req.param("file");
|
|
463
|
+
if (!isSoulFile(filename)) {
|
|
464
|
+
const response = {
|
|
465
|
+
success: false,
|
|
466
|
+
error: `Invalid soul file. Must be one of: ${SOUL_FILES.join(", ")}`
|
|
467
|
+
};
|
|
468
|
+
return c.json(response, 400);
|
|
469
|
+
}
|
|
470
|
+
const filePath = join(WORKSPACE_ROOT, filename);
|
|
471
|
+
try {
|
|
472
|
+
const content = readFileSync(filePath, "utf-8");
|
|
473
|
+
const response = {
|
|
474
|
+
success: true,
|
|
475
|
+
data: { content }
|
|
476
|
+
};
|
|
477
|
+
return c.json(response);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (error.code === "ENOENT") {
|
|
480
|
+
const response = {
|
|
481
|
+
success: true,
|
|
482
|
+
data: { content: "" }
|
|
483
|
+
};
|
|
484
|
+
return c.json(response);
|
|
485
|
+
}
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
const response = {
|
|
490
|
+
success: false,
|
|
491
|
+
error: error instanceof Error ? error.message : String(error)
|
|
492
|
+
};
|
|
493
|
+
return c.json(response, 500);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
app.put("/:file", async (c) => {
|
|
497
|
+
try {
|
|
498
|
+
const filename = c.req.param("file");
|
|
499
|
+
if (!isSoulFile(filename)) {
|
|
500
|
+
const response2 = {
|
|
501
|
+
success: false,
|
|
502
|
+
error: `Invalid soul file. Must be one of: ${SOUL_FILES.join(", ")}`
|
|
503
|
+
};
|
|
504
|
+
return c.json(response2, 400);
|
|
505
|
+
}
|
|
506
|
+
const body = await c.req.json();
|
|
507
|
+
if (typeof body.content !== "string") {
|
|
508
|
+
const response2 = {
|
|
509
|
+
success: false,
|
|
510
|
+
error: "Request body must contain 'content' field with string value"
|
|
511
|
+
};
|
|
512
|
+
return c.json(response2, 400);
|
|
513
|
+
}
|
|
514
|
+
const MAX_SOUL_SIZE = 1024 * 1024;
|
|
515
|
+
if (Buffer.byteLength(body.content, "utf-8") > MAX_SOUL_SIZE) {
|
|
516
|
+
const response2 = {
|
|
517
|
+
success: false,
|
|
518
|
+
error: "Soul file content exceeds 1MB limit"
|
|
519
|
+
};
|
|
520
|
+
return c.json(response2, 413);
|
|
521
|
+
}
|
|
522
|
+
const filePath = join(WORKSPACE_ROOT, filename);
|
|
523
|
+
writeFileSync(filePath, body.content, "utf-8");
|
|
524
|
+
const response = {
|
|
525
|
+
success: true,
|
|
526
|
+
data: { message: `${filename} updated successfully` }
|
|
527
|
+
};
|
|
528
|
+
return c.json(response);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const response = {
|
|
531
|
+
success: false,
|
|
532
|
+
error: error instanceof Error ? error.message : String(error)
|
|
533
|
+
};
|
|
534
|
+
return c.json(response, 500);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
return app;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/webui/routes/plugins.ts
|
|
541
|
+
import { Hono as Hono6 } from "hono";
|
|
542
|
+
function createPluginsRoutes(deps) {
|
|
543
|
+
const app = new Hono6();
|
|
544
|
+
app.get("/", (c) => {
|
|
545
|
+
const response = {
|
|
546
|
+
success: true,
|
|
547
|
+
data: deps.plugins
|
|
548
|
+
};
|
|
549
|
+
return c.json(response);
|
|
550
|
+
});
|
|
551
|
+
return app;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/webui/routes/workspace.ts
|
|
555
|
+
import { Hono as Hono7 } from "hono";
|
|
556
|
+
import {
|
|
557
|
+
readFileSync as readFileSync2,
|
|
558
|
+
writeFileSync as writeFileSync2,
|
|
559
|
+
mkdirSync,
|
|
560
|
+
rmSync,
|
|
561
|
+
renameSync,
|
|
562
|
+
readdirSync,
|
|
563
|
+
statSync,
|
|
564
|
+
existsSync
|
|
565
|
+
} from "fs";
|
|
566
|
+
import { join as join2, relative } from "path";
|
|
567
|
+
function errorResponse(c, error, status = 500) {
|
|
568
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
569
|
+
const code = error instanceof WorkspaceSecurityError ? 403 : status;
|
|
570
|
+
const response = { success: false, error: message };
|
|
571
|
+
return c.json(response, code);
|
|
572
|
+
}
|
|
573
|
+
function getWorkspaceStats(dir) {
|
|
574
|
+
let files = 0;
|
|
575
|
+
let size = 0;
|
|
576
|
+
if (!existsSync(dir)) return { files, size };
|
|
577
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
578
|
+
const fullPath = join2(dir, entry.name);
|
|
579
|
+
if (entry.isDirectory()) {
|
|
580
|
+
const sub = getWorkspaceStats(fullPath);
|
|
581
|
+
files += sub.files;
|
|
582
|
+
size += sub.size;
|
|
583
|
+
} else if (entry.isFile()) {
|
|
584
|
+
files++;
|
|
585
|
+
try {
|
|
586
|
+
size += statSync(fullPath).size;
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { files, size };
|
|
592
|
+
}
|
|
593
|
+
function listDir(absPath, recursive) {
|
|
594
|
+
if (!existsSync(absPath)) return [];
|
|
595
|
+
const entries = [];
|
|
596
|
+
for (const entry of readdirSync(absPath, { withFileTypes: true })) {
|
|
597
|
+
const fullPath = join2(absPath, entry.name);
|
|
598
|
+
const relPath = relative(WORKSPACE_ROOT, fullPath);
|
|
599
|
+
try {
|
|
600
|
+
const stats = statSync(fullPath);
|
|
601
|
+
entries.push({
|
|
602
|
+
name: entry.name,
|
|
603
|
+
path: relPath,
|
|
604
|
+
isDirectory: entry.isDirectory(),
|
|
605
|
+
size: entry.isDirectory() ? 0 : stats.size,
|
|
606
|
+
mtime: stats.mtime.toISOString()
|
|
607
|
+
});
|
|
608
|
+
if (recursive && entry.isDirectory()) {
|
|
609
|
+
entries.push(...listDir(fullPath, true));
|
|
610
|
+
}
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return entries;
|
|
615
|
+
}
|
|
616
|
+
function createWorkspaceRoutes(_deps) {
|
|
617
|
+
const app = new Hono7();
|
|
618
|
+
app.get("/", (c) => {
|
|
619
|
+
try {
|
|
620
|
+
const subpath = c.req.query("path") || "";
|
|
621
|
+
const recursive = c.req.query("recursive") === "true";
|
|
622
|
+
const validated = subpath ? validateDirectory(subpath) : {
|
|
623
|
+
absolutePath: WORKSPACE_ROOT,
|
|
624
|
+
relativePath: "",
|
|
625
|
+
exists: existsSync(WORKSPACE_ROOT),
|
|
626
|
+
isDirectory: true,
|
|
627
|
+
extension: "",
|
|
628
|
+
filename: ""
|
|
629
|
+
};
|
|
630
|
+
if (!validated.exists) {
|
|
631
|
+
const response2 = { success: true, data: [] };
|
|
632
|
+
return c.json(response2);
|
|
633
|
+
}
|
|
634
|
+
const entries = listDir(validated.absolutePath, recursive);
|
|
635
|
+
entries.sort((a, b) => {
|
|
636
|
+
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
|
637
|
+
return a.name.localeCompare(b.name);
|
|
638
|
+
});
|
|
639
|
+
const response = { success: true, data: entries };
|
|
640
|
+
return c.json(response);
|
|
641
|
+
} catch (error) {
|
|
642
|
+
return errorResponse(c, error);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
app.get("/read", (c) => {
|
|
646
|
+
try {
|
|
647
|
+
const path = c.req.query("path");
|
|
648
|
+
if (!path) {
|
|
649
|
+
const response2 = { success: false, error: "Missing 'path' query parameter" };
|
|
650
|
+
return c.json(response2, 400);
|
|
651
|
+
}
|
|
652
|
+
const validated = validateReadPath(path);
|
|
653
|
+
const stats = statSync(validated.absolutePath);
|
|
654
|
+
if (stats.size > 1024 * 1024) {
|
|
655
|
+
const response2 = { success: false, error: "File too large to read (max 1MB)" };
|
|
656
|
+
return c.json(response2, 413);
|
|
657
|
+
}
|
|
658
|
+
const content = readFileSync2(validated.absolutePath, "utf-8");
|
|
659
|
+
const response = {
|
|
660
|
+
success: true,
|
|
661
|
+
data: { content, size: stats.size }
|
|
662
|
+
};
|
|
663
|
+
return c.json(response);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
return errorResponse(c, error);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
app.post("/write", async (c) => {
|
|
669
|
+
try {
|
|
670
|
+
const body = await c.req.json();
|
|
671
|
+
if (!body.path || typeof body.content !== "string") {
|
|
672
|
+
const response2 = {
|
|
673
|
+
success: false,
|
|
674
|
+
error: "Request body must contain 'path' and 'content'"
|
|
675
|
+
};
|
|
676
|
+
return c.json(response2, 400);
|
|
677
|
+
}
|
|
678
|
+
const validated = validateWritePath(body.path);
|
|
679
|
+
const parentDir = join2(validated.absolutePath, "..");
|
|
680
|
+
mkdirSync(parentDir, { recursive: true });
|
|
681
|
+
writeFileSync2(validated.absolutePath, body.content, "utf-8");
|
|
682
|
+
const response = {
|
|
683
|
+
success: true,
|
|
684
|
+
data: { message: `File saved: ${validated.relativePath}` }
|
|
685
|
+
};
|
|
686
|
+
return c.json(response);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
return errorResponse(c, error);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
app.post("/mkdir", async (c) => {
|
|
692
|
+
try {
|
|
693
|
+
const body = await c.req.json();
|
|
694
|
+
if (!body.path) {
|
|
695
|
+
const response2 = { success: false, error: "Request body must contain 'path'" };
|
|
696
|
+
return c.json(response2, 400);
|
|
697
|
+
}
|
|
698
|
+
const validated = validateDirectory(body.path);
|
|
699
|
+
mkdirSync(validated.absolutePath, { recursive: true });
|
|
700
|
+
const response = {
|
|
701
|
+
success: true,
|
|
702
|
+
data: { message: `Directory created: ${validated.relativePath}` }
|
|
703
|
+
};
|
|
704
|
+
return c.json(response);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return errorResponse(c, error);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
app.delete("/", async (c) => {
|
|
710
|
+
try {
|
|
711
|
+
const body = await c.req.json();
|
|
712
|
+
if (!body.path) {
|
|
713
|
+
const response2 = { success: false, error: "Request body must contain 'path'" };
|
|
714
|
+
return c.json(response2, 400);
|
|
715
|
+
}
|
|
716
|
+
const validated = validatePath(body.path, false);
|
|
717
|
+
if (validated.isDirectory && !body.recursive) {
|
|
718
|
+
const contents = readdirSync(validated.absolutePath);
|
|
719
|
+
if (contents.length > 0) {
|
|
720
|
+
const response2 = {
|
|
721
|
+
success: false,
|
|
722
|
+
error: "Directory is not empty. Set recursive=true to delete recursively."
|
|
723
|
+
};
|
|
724
|
+
return c.json(response2, 400);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
rmSync(validated.absolutePath, { recursive: !!body.recursive });
|
|
728
|
+
const response = {
|
|
729
|
+
success: true,
|
|
730
|
+
data: { message: `Deleted: ${validated.relativePath}` }
|
|
731
|
+
};
|
|
732
|
+
return c.json(response);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
return errorResponse(c, error);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
app.post("/rename", async (c) => {
|
|
738
|
+
try {
|
|
739
|
+
const body = await c.req.json();
|
|
740
|
+
if (!body.from || !body.to) {
|
|
741
|
+
const response2 = {
|
|
742
|
+
success: false,
|
|
743
|
+
error: "Request body must contain 'from' and 'to'"
|
|
744
|
+
};
|
|
745
|
+
return c.json(response2, 400);
|
|
746
|
+
}
|
|
747
|
+
const fromValidated = validatePath(body.from, false);
|
|
748
|
+
const toValidated = validatePath(body.to, true);
|
|
749
|
+
const parentDir = join2(toValidated.absolutePath, "..");
|
|
750
|
+
mkdirSync(parentDir, { recursive: true });
|
|
751
|
+
renameSync(fromValidated.absolutePath, toValidated.absolutePath);
|
|
752
|
+
const response = {
|
|
753
|
+
success: true,
|
|
754
|
+
data: { message: `Renamed: ${fromValidated.relativePath} \u2192 ${toValidated.relativePath}` }
|
|
755
|
+
};
|
|
756
|
+
return c.json(response);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
return errorResponse(c, error);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
app.get("/info", (c) => {
|
|
762
|
+
try {
|
|
763
|
+
const stats = getWorkspaceStats(WORKSPACE_ROOT);
|
|
764
|
+
const response = {
|
|
765
|
+
success: true,
|
|
766
|
+
data: {
|
|
767
|
+
root: WORKSPACE_ROOT,
|
|
768
|
+
totalFiles: stats.files,
|
|
769
|
+
totalSize: stats.size
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
return c.json(response);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
return errorResponse(c, error);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
return app;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/webui/routes/tasks.ts
|
|
781
|
+
import { Hono as Hono8 } from "hono";
|
|
782
|
+
var VALID_STATUSES = ["pending", "in_progress", "done", "failed", "cancelled"];
|
|
783
|
+
function createTasksRoutes(deps) {
|
|
784
|
+
const app = new Hono8();
|
|
785
|
+
function store() {
|
|
786
|
+
return getTaskStore(deps.memory.db);
|
|
787
|
+
}
|
|
788
|
+
app.get("/", (c) => {
|
|
789
|
+
try {
|
|
790
|
+
const status = c.req.query("status");
|
|
791
|
+
const filter = status && VALID_STATUSES.includes(status) ? { status } : void 0;
|
|
792
|
+
const tasks = store().listTasks(filter);
|
|
793
|
+
const enriched = tasks.map((t) => ({
|
|
794
|
+
...t,
|
|
795
|
+
createdAt: t.createdAt.toISOString(),
|
|
796
|
+
startedAt: t.startedAt?.toISOString() ?? null,
|
|
797
|
+
completedAt: t.completedAt?.toISOString() ?? null,
|
|
798
|
+
scheduledFor: t.scheduledFor?.toISOString() ?? null,
|
|
799
|
+
dependencies: store().getDependencies(t.id),
|
|
800
|
+
dependents: store().getDependents(t.id)
|
|
801
|
+
}));
|
|
802
|
+
const response = { success: true, data: enriched };
|
|
803
|
+
return c.json(response);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
const response = {
|
|
806
|
+
success: false,
|
|
807
|
+
error: error instanceof Error ? error.message : String(error)
|
|
808
|
+
};
|
|
809
|
+
return c.json(response, 500);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
app.get("/:id", (c) => {
|
|
813
|
+
try {
|
|
814
|
+
const task = store().getTask(c.req.param("id"));
|
|
815
|
+
if (!task) {
|
|
816
|
+
const response2 = { success: false, error: "Task not found" };
|
|
817
|
+
return c.json(response2, 404);
|
|
818
|
+
}
|
|
819
|
+
const enriched = {
|
|
820
|
+
...task,
|
|
821
|
+
createdAt: task.createdAt.toISOString(),
|
|
822
|
+
startedAt: task.startedAt?.toISOString() ?? null,
|
|
823
|
+
completedAt: task.completedAt?.toISOString() ?? null,
|
|
824
|
+
scheduledFor: task.scheduledFor?.toISOString() ?? null,
|
|
825
|
+
dependencies: store().getDependencies(task.id),
|
|
826
|
+
dependents: store().getDependents(task.id)
|
|
827
|
+
};
|
|
828
|
+
const response = { success: true, data: enriched };
|
|
829
|
+
return c.json(response);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
const response = {
|
|
832
|
+
success: false,
|
|
833
|
+
error: error instanceof Error ? error.message : String(error)
|
|
834
|
+
};
|
|
835
|
+
return c.json(response, 500);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
app.delete("/:id", (c) => {
|
|
839
|
+
try {
|
|
840
|
+
const deleted = store().deleteTask(c.req.param("id"));
|
|
841
|
+
if (!deleted) {
|
|
842
|
+
const response2 = { success: false, error: "Task not found" };
|
|
843
|
+
return c.json(response2, 404);
|
|
844
|
+
}
|
|
845
|
+
const response = { success: true, data: { message: "Task deleted" } };
|
|
846
|
+
return c.json(response);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
const response = {
|
|
849
|
+
success: false,
|
|
850
|
+
error: error instanceof Error ? error.message : String(error)
|
|
851
|
+
};
|
|
852
|
+
return c.json(response, 500);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
app.post("/clean-done", (c) => {
|
|
856
|
+
try {
|
|
857
|
+
const doneTasks = store().listTasks({ status: "done" });
|
|
858
|
+
let deleted = 0;
|
|
859
|
+
for (const t of doneTasks) {
|
|
860
|
+
if (store().deleteTask(t.id)) deleted++;
|
|
861
|
+
}
|
|
862
|
+
const response = { success: true, data: { deleted } };
|
|
863
|
+
return c.json(response);
|
|
864
|
+
} catch (error) {
|
|
865
|
+
const response = {
|
|
866
|
+
success: false,
|
|
867
|
+
error: error instanceof Error ? error.message : String(error)
|
|
868
|
+
};
|
|
869
|
+
return c.json(response, 500);
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
app.post("/:id/cancel", (c) => {
|
|
873
|
+
try {
|
|
874
|
+
const updated = store().cancelTask(c.req.param("id"));
|
|
875
|
+
if (!updated) {
|
|
876
|
+
const response2 = { success: false, error: "Task not found" };
|
|
877
|
+
return c.json(response2, 404);
|
|
878
|
+
}
|
|
879
|
+
const response = { success: true, data: updated };
|
|
880
|
+
return c.json(response);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
const response = {
|
|
883
|
+
success: false,
|
|
884
|
+
error: error instanceof Error ? error.message : String(error)
|
|
885
|
+
};
|
|
886
|
+
return c.json(response, 500);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
return app;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/webui/server.ts
|
|
893
|
+
function findWebDist() {
|
|
894
|
+
const candidates = [
|
|
895
|
+
resolve("dist/web"),
|
|
896
|
+
// npm start / teleton start (from project root)
|
|
897
|
+
resolve("web")
|
|
898
|
+
// fallback
|
|
899
|
+
];
|
|
900
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
901
|
+
candidates.push(
|
|
902
|
+
resolve(__dirname, "web"),
|
|
903
|
+
// dist/web when __dirname = dist/
|
|
904
|
+
resolve(__dirname, "../dist/web")
|
|
905
|
+
// when running with tsx from src/
|
|
906
|
+
);
|
|
907
|
+
for (const candidate of candidates) {
|
|
908
|
+
if (existsSync2(join3(candidate, "index.html"))) {
|
|
909
|
+
return candidate;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
var WebUIServer = class {
|
|
915
|
+
app;
|
|
916
|
+
server = null;
|
|
917
|
+
deps;
|
|
918
|
+
authToken;
|
|
919
|
+
constructor(deps) {
|
|
920
|
+
this.deps = deps;
|
|
921
|
+
this.app = new Hono9();
|
|
922
|
+
this.authToken = deps.config.auth_token || generateToken();
|
|
923
|
+
this.setupMiddleware();
|
|
924
|
+
this.setupRoutes();
|
|
925
|
+
}
|
|
926
|
+
/** Set an HttpOnly session cookie */
|
|
927
|
+
setSessionCookie(c) {
|
|
928
|
+
setCookie(c, COOKIE_NAME, this.authToken, {
|
|
929
|
+
path: "/",
|
|
930
|
+
httpOnly: true,
|
|
931
|
+
sameSite: "Strict",
|
|
932
|
+
secure: false,
|
|
933
|
+
// localhost is HTTP
|
|
934
|
+
maxAge: COOKIE_MAX_AGE
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
setupMiddleware() {
|
|
938
|
+
this.app.use(
|
|
939
|
+
"*",
|
|
940
|
+
cors({
|
|
941
|
+
origin: this.deps.config.cors_origins,
|
|
942
|
+
credentials: true,
|
|
943
|
+
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
|
|
944
|
+
allowHeaders: ["Content-Type", "Authorization"],
|
|
945
|
+
maxAge: 3600
|
|
946
|
+
})
|
|
947
|
+
);
|
|
948
|
+
if (this.deps.config.log_requests) {
|
|
949
|
+
this.app.use("*", async (c, next) => {
|
|
950
|
+
const start = Date.now();
|
|
951
|
+
await next();
|
|
952
|
+
const duration = Date.now() - start;
|
|
953
|
+
console.log(`\u{1F4E1} ${c.req.method} ${c.req.path} \u2192 ${c.res.status} (${duration}ms)`);
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
this.app.use(
|
|
957
|
+
"*",
|
|
958
|
+
bodyLimit({
|
|
959
|
+
maxSize: 2 * 1024 * 1024,
|
|
960
|
+
// 2MB
|
|
961
|
+
onError: (c) => c.json({ success: false, error: "Request body too large (max 2MB)" }, 413)
|
|
962
|
+
})
|
|
963
|
+
);
|
|
964
|
+
this.app.use("*", async (c, next) => {
|
|
965
|
+
await next();
|
|
966
|
+
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
967
|
+
c.res.headers.set("X-Frame-Options", "DENY");
|
|
968
|
+
c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
969
|
+
});
|
|
970
|
+
this.app.use("/api/*", async (c, next) => {
|
|
971
|
+
const cookieToken = getCookie(c, COOKIE_NAME);
|
|
972
|
+
if (cookieToken && safeCompare(cookieToken, this.authToken)) {
|
|
973
|
+
return next();
|
|
974
|
+
}
|
|
975
|
+
const authHeader = c.req.header("Authorization");
|
|
976
|
+
if (authHeader) {
|
|
977
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
978
|
+
if (match && safeCompare(match[1], this.authToken)) {
|
|
979
|
+
return next();
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const queryToken = c.req.query("token");
|
|
983
|
+
if (queryToken && safeCompare(queryToken, this.authToken)) {
|
|
984
|
+
return next();
|
|
985
|
+
}
|
|
986
|
+
return c.json({ success: false, error: "Unauthorized" }, 401);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
setupRoutes() {
|
|
990
|
+
this.app.get("/health", (c) => c.json({ status: "ok" }));
|
|
991
|
+
this.app.get("/auth/exchange", (c) => {
|
|
992
|
+
const token = c.req.query("token");
|
|
993
|
+
if (!token || !safeCompare(token, this.authToken)) {
|
|
994
|
+
return c.json({ success: false, error: "Invalid token" }, 401);
|
|
995
|
+
}
|
|
996
|
+
this.setSessionCookie(c);
|
|
997
|
+
return c.redirect("/");
|
|
998
|
+
});
|
|
999
|
+
this.app.post("/auth/login", async (c) => {
|
|
1000
|
+
try {
|
|
1001
|
+
const body = await c.req.json();
|
|
1002
|
+
if (!body.token || !safeCompare(body.token, this.authToken)) {
|
|
1003
|
+
return c.json({ success: false, error: "Invalid token" }, 401);
|
|
1004
|
+
}
|
|
1005
|
+
this.setSessionCookie(c);
|
|
1006
|
+
return c.json({ success: true });
|
|
1007
|
+
} catch {
|
|
1008
|
+
return c.json({ success: false, error: "Invalid request body" }, 400);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
this.app.post("/auth/logout", (c) => {
|
|
1012
|
+
deleteCookie(c, COOKIE_NAME, { path: "/" });
|
|
1013
|
+
return c.json({ success: true });
|
|
1014
|
+
});
|
|
1015
|
+
this.app.get("/auth/check", (c) => {
|
|
1016
|
+
const cookieToken = getCookie(c, COOKIE_NAME);
|
|
1017
|
+
const authenticated = !!(cookieToken && safeCompare(cookieToken, this.authToken));
|
|
1018
|
+
return c.json({ success: true, data: { authenticated } });
|
|
1019
|
+
});
|
|
1020
|
+
this.app.route("/api/status", createStatusRoutes(this.deps));
|
|
1021
|
+
this.app.route("/api/tools", createToolsRoutes(this.deps));
|
|
1022
|
+
this.app.route("/api/logs", createLogsRoutes(this.deps));
|
|
1023
|
+
this.app.route("/api/memory", createMemoryRoutes(this.deps));
|
|
1024
|
+
this.app.route("/api/soul", createSoulRoutes(this.deps));
|
|
1025
|
+
this.app.route("/api/plugins", createPluginsRoutes(this.deps));
|
|
1026
|
+
this.app.route("/api/workspace", createWorkspaceRoutes(this.deps));
|
|
1027
|
+
this.app.route("/api/tasks", createTasksRoutes(this.deps));
|
|
1028
|
+
const webDist = findWebDist();
|
|
1029
|
+
if (webDist) {
|
|
1030
|
+
const indexHtml = readFileSync3(join3(webDist, "index.html"), "utf-8");
|
|
1031
|
+
const mimeTypes = {
|
|
1032
|
+
js: "application/javascript",
|
|
1033
|
+
css: "text/css",
|
|
1034
|
+
svg: "image/svg+xml",
|
|
1035
|
+
png: "image/png",
|
|
1036
|
+
jpg: "image/jpeg",
|
|
1037
|
+
jpeg: "image/jpeg",
|
|
1038
|
+
ico: "image/x-icon",
|
|
1039
|
+
json: "application/json",
|
|
1040
|
+
woff2: "font/woff2",
|
|
1041
|
+
woff: "font/woff"
|
|
1042
|
+
};
|
|
1043
|
+
this.app.get("*", (c) => {
|
|
1044
|
+
const filePath = resolve(join3(webDist, c.req.path));
|
|
1045
|
+
const rel = relative2(webDist, filePath);
|
|
1046
|
+
if (rel.startsWith("..") || resolve(filePath) !== filePath) {
|
|
1047
|
+
return c.html(indexHtml);
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const content = readFileSync3(filePath);
|
|
1051
|
+
const ext = filePath.split(".").pop() || "";
|
|
1052
|
+
if (mimeTypes[ext]) {
|
|
1053
|
+
const immutable = c.req.path.startsWith("/assets/");
|
|
1054
|
+
return c.body(content, 200, {
|
|
1055
|
+
"Content-Type": mimeTypes[ext],
|
|
1056
|
+
"Cache-Control": immutable ? "public, max-age=31536000, immutable" : "public, max-age=3600"
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
return c.html(indexHtml);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
this.app.onError((err, c) => {
|
|
1065
|
+
console.error("WebUI error:", err);
|
|
1066
|
+
return c.json(
|
|
1067
|
+
{
|
|
1068
|
+
success: false,
|
|
1069
|
+
error: err.message || "Internal server error"
|
|
1070
|
+
},
|
|
1071
|
+
500
|
|
1072
|
+
);
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
async start() {
|
|
1076
|
+
return new Promise((resolve2, reject) => {
|
|
1077
|
+
try {
|
|
1078
|
+
logInterceptor.install();
|
|
1079
|
+
this.server = serve(
|
|
1080
|
+
{
|
|
1081
|
+
fetch: this.app.fetch,
|
|
1082
|
+
hostname: this.deps.config.host,
|
|
1083
|
+
port: this.deps.config.port
|
|
1084
|
+
},
|
|
1085
|
+
(info) => {
|
|
1086
|
+
const url = `http://${info.address}:${info.port}`;
|
|
1087
|
+
console.log(`
|
|
1088
|
+
\u{1F310} WebUI server running`);
|
|
1089
|
+
console.log(` URL: ${url}/auth/exchange?token=${this.authToken}`);
|
|
1090
|
+
console.log(
|
|
1091
|
+
` Token: ${maskToken(this.authToken)} (use Bearer header for API access)
|
|
1092
|
+
`
|
|
1093
|
+
);
|
|
1094
|
+
resolve2();
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
logInterceptor.uninstall();
|
|
1099
|
+
reject(error);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
async stop() {
|
|
1104
|
+
if (this.server) {
|
|
1105
|
+
return new Promise((resolve2) => {
|
|
1106
|
+
this.server.close(() => {
|
|
1107
|
+
logInterceptor.uninstall();
|
|
1108
|
+
console.log("\u{1F310} WebUI server stopped");
|
|
1109
|
+
resolve2();
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
getToken() {
|
|
1115
|
+
return this.authToken;
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
export {
|
|
1119
|
+
WebUIServer
|
|
1120
|
+
};
|