teleton 0.4.0 → 0.5.1

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.
Files changed (33) hide show
  1. package/README.md +88 -13
  2. package/dist/BigInteger-DQ33LTTE.js +5 -0
  3. package/dist/chunk-4DU3C27M.js +30 -0
  4. package/dist/chunk-5WWR4CU3.js +124 -0
  5. package/dist/{chunk-7UPH62J2.js → chunk-BYTHCDZA.js} +261 -255
  6. package/dist/{chunk-E2NXSWOS.js → chunk-NUGDTPE4.js} +24 -64
  7. package/dist/{chunk-OA5L7GM6.js → chunk-O4R7V5Y2.js} +37 -5
  8. package/dist/chunk-QUAPFI2N.js +42 -0
  9. package/dist/{chunk-QU4ZOR35.js → chunk-RRB6BWU7.js} +3108 -3345
  10. package/dist/chunk-TSKJCWQQ.js +1263 -0
  11. package/dist/{chunk-B2PRMXOH.js → chunk-WL2Q3VRD.js} +0 -2
  12. package/dist/{chunk-OQGNS2FV.js → chunk-YBA6IBGT.js} +20 -5
  13. package/dist/cli/index.js +39 -172
  14. package/dist/{endpoint-FT2B2RZ2.js → endpoint-FLYNEZ2F.js} +1 -1
  15. package/dist/{get-my-gifts-AFKBG4YQ.js → get-my-gifts-KVULMBJ3.js} +1 -1
  16. package/dist/index.js +12 -12
  17. package/dist/{memory-SYTQ5P7P.js → memory-657W5AS6.js} +4 -5
  18. package/dist/{migrate-ITXMRRSZ.js → migrate-PMB2JVXH.js} +4 -5
  19. package/dist/server-BQY7CM2N.js +1120 -0
  20. package/dist/{task-dependency-resolver-GY6PEBIS.js → task-dependency-resolver-TRPILAHM.js} +2 -2
  21. package/dist/{task-executor-4QKTZZ3P.js → task-executor-N7XNVK5N.js} +1 -1
  22. package/dist/{tasks-M3QDPTGY.js → tasks-QSCWSMPS.js} +1 -1
  23. package/dist/{transcript-DF2Y6CFY.js → transcript-7V4UNID4.js} +1 -1
  24. package/dist/web/assets/index-CDMbujHf.css +1 -0
  25. package/dist/web/assets/index-DDX8oQ2z.js +67 -0
  26. package/dist/web/index.html +16 -0
  27. package/dist/web/logo_dark.png +0 -0
  28. package/package.json +23 -4
  29. package/dist/chunk-67QC5FBN.js +0 -60
  30. package/dist/chunk-A64NPEFL.js +0 -74
  31. package/dist/chunk-DUW5VBAZ.js +0 -133
  32. package/dist/chunk-QBGUCUOW.js +0 -16
  33. 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
+ };