teleton 0.5.2 → 0.7.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/README.md +243 -105
- package/dist/chunk-FNV5FF35.js +331 -0
- package/dist/chunk-LRCPA7SC.js +149 -0
- package/dist/{chunk-WUTMT6DW.js → chunk-N3F7E7DR.js} +114 -566
- package/dist/chunk-ND2X5FWB.js +368 -0
- package/dist/chunk-NERLQY2H.js +421 -0
- package/dist/{chunk-YBA6IBGT.js → chunk-OCLG5GKI.js} +24 -24
- package/dist/{chunk-WOXBZOQX.js → chunk-OGIG552S.js} +2152 -4488
- package/dist/chunk-RCMD3U65.js +141 -0
- package/dist/{chunk-O4R7V5Y2.js → chunk-RO62LO6Z.js} +11 -1
- package/dist/chunk-TCD4NZDA.js +3226 -0
- package/dist/chunk-UCN6TI25.js +143 -0
- package/dist/{chunk-WL2Q3VRD.js → chunk-UDD7FYOU.js} +12 -4
- package/dist/chunk-VAUJSSD3.js +20 -0
- package/dist/chunk-XBE4JB7C.js +8 -0
- package/dist/chunk-XBKSS6DM.js +58 -0
- package/dist/cli/index.js +1179 -412
- package/dist/client-3VWE7NC4.js +29 -0
- package/dist/{get-my-gifts-KVULMBJ3.js → get-my-gifts-RI7FAXAL.js} +3 -1
- package/dist/index.js +17 -8
- package/dist/{memory-Y5J7CXAR.js → memory-RD7ZSTRV.js} +16 -10
- package/dist/{migrate-UEQCDWL2.js → migrate-GO4NOBT7.js} +14 -6
- package/dist/{server-BQY7CM2N.js → server-OWVEZTR3.js} +869 -93
- package/dist/setup-server-C7ZTPHD5.js +934 -0
- package/dist/{task-dependency-resolver-TRPILAHM.js → task-dependency-resolver-WKZWJLLM.js} +19 -15
- package/dist/{task-executor-N7XNVK5N.js → task-executor-PD3H4MLO.js} +5 -2
- package/dist/tool-adapter-Y3TCEQOC.js +145 -0
- package/dist/tool-index-MIVK3D7H.js +250 -0
- package/dist/{transcript-7V4UNID4.js → transcript-UDJZP6NK.js} +2 -1
- package/dist/web/assets/complete-fZLnb5Ot.js +1 -0
- package/dist/web/assets/index-B_FcaX5D.css +1 -0
- package/dist/web/assets/index-CbeAP4_n.js +67 -0
- package/dist/web/assets/index.es-oXiZF7Hc.js +11 -0
- package/dist/web/assets/login-telegram-BP7CJDmx.js +1 -0
- package/dist/web/assets/run-DOrDowjK.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +21 -15
- package/dist/chunk-5WWR4CU3.js +0 -124
- package/dist/web/assets/index-CDMbujHf.css +0 -1
- package/dist/web/assets/index-DDX8oQ2z.js +0 -67
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CONFIGURABLE_KEYS,
|
|
2
3
|
WorkspaceSecurityError,
|
|
4
|
+
adaptPlugin,
|
|
5
|
+
clearPromptCache,
|
|
6
|
+
deleteNestedValue,
|
|
7
|
+
deletePluginSecret,
|
|
8
|
+
ensurePluginDeps,
|
|
9
|
+
getNestedValue,
|
|
10
|
+
listPluginSecretKeys,
|
|
11
|
+
readRawConfig,
|
|
12
|
+
setNestedValue,
|
|
3
13
|
validateDirectory,
|
|
4
14
|
validatePath,
|
|
5
15
|
validateReadPath,
|
|
6
|
-
validateWritePath
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
validateWritePath,
|
|
17
|
+
writePluginSecret,
|
|
18
|
+
writeRawConfig
|
|
19
|
+
} from "./chunk-TCD4NZDA.js";
|
|
20
|
+
import "./chunk-NERLQY2H.js";
|
|
21
|
+
import "./chunk-QUAPFI2N.js";
|
|
22
|
+
import "./chunk-TSKJCWQQ.js";
|
|
9
23
|
import {
|
|
24
|
+
getErrorMessage
|
|
25
|
+
} from "./chunk-XBE4JB7C.js";
|
|
26
|
+
import "./chunk-LRCPA7SC.js";
|
|
27
|
+
import "./chunk-UCN6TI25.js";
|
|
28
|
+
import "./chunk-XBKSS6DM.js";
|
|
29
|
+
import "./chunk-RO62LO6Z.js";
|
|
30
|
+
import "./chunk-VAUJSSD3.js";
|
|
31
|
+
import "./chunk-4DU3C27M.js";
|
|
32
|
+
import {
|
|
33
|
+
WORKSPACE_PATHS,
|
|
10
34
|
WORKSPACE_ROOT
|
|
11
35
|
} from "./chunk-EYWNOHMJ.js";
|
|
36
|
+
import {
|
|
37
|
+
addLogListener,
|
|
38
|
+
clearLogListeners,
|
|
39
|
+
createLogger
|
|
40
|
+
} from "./chunk-RCMD3U65.js";
|
|
12
41
|
import {
|
|
13
42
|
getTaskStore
|
|
14
43
|
} from "./chunk-NUGDTPE4.js";
|
|
15
44
|
import "./chunk-QGM4M3NI.js";
|
|
16
45
|
|
|
17
46
|
// src/webui/server.ts
|
|
18
|
-
import { Hono as
|
|
47
|
+
import { Hono as Hono12 } from "hono";
|
|
19
48
|
import { serve } from "@hono/node-server";
|
|
20
49
|
import { cors } from "hono/cors";
|
|
21
50
|
import { bodyLimit } from "hono/body-limit";
|
|
22
51
|
import { setCookie, getCookie, deleteCookie } from "hono/cookie";
|
|
23
|
-
import { existsSync as
|
|
24
|
-
import { join as
|
|
52
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
53
|
+
import { join as join4, dirname, resolve as resolve2, relative as relative2 } from "path";
|
|
25
54
|
import { fileURLToPath } from "url";
|
|
26
55
|
|
|
27
56
|
// src/webui/middleware/auth.ts
|
|
@@ -45,54 +74,43 @@ function safeCompare(a, b) {
|
|
|
45
74
|
|
|
46
75
|
// src/webui/log-interceptor.ts
|
|
47
76
|
var LogInterceptor = class {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
77
|
+
cleanups = /* @__PURE__ */ new Map();
|
|
78
|
+
installed = false;
|
|
79
|
+
/**
|
|
80
|
+
* Install the interceptor. Now a lightweight no-op since pino streams
|
|
81
|
+
* are always active — kept for API compat with server.ts start/stop.
|
|
82
|
+
*/
|
|
55
83
|
install() {
|
|
56
|
-
|
|
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;
|
|
84
|
+
this.installed = true;
|
|
79
85
|
}
|
|
80
86
|
uninstall() {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.
|
|
87
|
+
for (const cleanup of this.cleanups.values()) {
|
|
88
|
+
cleanup();
|
|
89
|
+
}
|
|
90
|
+
this.cleanups.clear();
|
|
91
|
+
this.installed = false;
|
|
86
92
|
}
|
|
87
93
|
addListener(listener) {
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
const cleanup = addLogListener(listener);
|
|
95
|
+
this.cleanups.set(listener, cleanup);
|
|
96
|
+
return () => {
|
|
97
|
+
cleanup();
|
|
98
|
+
this.cleanups.delete(listener);
|
|
99
|
+
};
|
|
90
100
|
}
|
|
91
101
|
removeListener(listener) {
|
|
92
|
-
this.
|
|
102
|
+
const cleanup = this.cleanups.get(listener);
|
|
103
|
+
if (cleanup) {
|
|
104
|
+
cleanup();
|
|
105
|
+
this.cleanups.delete(listener);
|
|
106
|
+
}
|
|
93
107
|
}
|
|
94
108
|
clear() {
|
|
95
|
-
this.
|
|
109
|
+
for (const cleanup of this.cleanups.values()) {
|
|
110
|
+
cleanup();
|
|
111
|
+
}
|
|
112
|
+
this.cleanups.clear();
|
|
113
|
+
clearLogListeners();
|
|
96
114
|
}
|
|
97
115
|
};
|
|
98
116
|
var logInterceptor = new LogInterceptor();
|
|
@@ -110,8 +128,6 @@ function createStatusRoutes(deps) {
|
|
|
110
128
|
model: config.agent.model,
|
|
111
129
|
provider: config.agent.provider,
|
|
112
130
|
sessionCount: sessionCountRow?.count ?? 0,
|
|
113
|
-
paused: false,
|
|
114
|
-
// TODO: get from message handler
|
|
115
131
|
toolCount: deps.toolRegistry.getAll().length
|
|
116
132
|
};
|
|
117
133
|
const response = {
|
|
@@ -122,7 +138,7 @@ function createStatusRoutes(deps) {
|
|
|
122
138
|
} catch (error) {
|
|
123
139
|
const response = {
|
|
124
140
|
success: false,
|
|
125
|
-
error:
|
|
141
|
+
error: getErrorMessage(error)
|
|
126
142
|
};
|
|
127
143
|
return c.json(response, 500);
|
|
128
144
|
}
|
|
@@ -169,11 +185,60 @@ function createToolsRoutes(deps) {
|
|
|
169
185
|
} catch (error) {
|
|
170
186
|
const response = {
|
|
171
187
|
success: false,
|
|
172
|
-
error:
|
|
188
|
+
error: getErrorMessage(error)
|
|
173
189
|
};
|
|
174
190
|
return c.json(response, 500);
|
|
175
191
|
}
|
|
176
192
|
});
|
|
193
|
+
app.get("/rag", (c) => {
|
|
194
|
+
try {
|
|
195
|
+
const config = deps.agent.getConfig();
|
|
196
|
+
const toolIndex = deps.toolRegistry.getToolIndex();
|
|
197
|
+
const response = {
|
|
198
|
+
success: true,
|
|
199
|
+
data: {
|
|
200
|
+
enabled: config.tool_rag.enabled,
|
|
201
|
+
indexed: toolIndex?.isIndexed ?? false,
|
|
202
|
+
topK: config.tool_rag.top_k,
|
|
203
|
+
totalTools: deps.toolRegistry.count,
|
|
204
|
+
alwaysInclude: config.tool_rag.always_include,
|
|
205
|
+
skipUnlimitedProviders: config.tool_rag.skip_unlimited_providers
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
return c.json(response);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return c.json({ success: false, error: String(error) }, 500);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
app.put("/rag", async (c) => {
|
|
214
|
+
try {
|
|
215
|
+
const config = deps.agent.getConfig();
|
|
216
|
+
const body = await c.req.json();
|
|
217
|
+
const { enabled, topK } = body;
|
|
218
|
+
if (enabled !== void 0) {
|
|
219
|
+
config.tool_rag.enabled = enabled;
|
|
220
|
+
}
|
|
221
|
+
if (topK !== void 0) {
|
|
222
|
+
if (topK < 5 || topK > 200) {
|
|
223
|
+
return c.json({ success: false, error: "topK must be between 5 and 200" }, 400);
|
|
224
|
+
}
|
|
225
|
+
config.tool_rag.top_k = topK;
|
|
226
|
+
}
|
|
227
|
+
const toolIndex = deps.toolRegistry.getToolIndex();
|
|
228
|
+
const response = {
|
|
229
|
+
success: true,
|
|
230
|
+
data: {
|
|
231
|
+
enabled: config.tool_rag.enabled,
|
|
232
|
+
indexed: toolIndex?.isIndexed ?? false,
|
|
233
|
+
topK: config.tool_rag.top_k,
|
|
234
|
+
totalTools: deps.toolRegistry.count
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
return c.json(response);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return c.json({ success: false, error: String(error) }, 500);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
177
242
|
app.put("/:name", async (c) => {
|
|
178
243
|
try {
|
|
179
244
|
const toolName = c.req.param("name");
|
|
@@ -230,7 +295,7 @@ function createToolsRoutes(deps) {
|
|
|
230
295
|
} catch (error) {
|
|
231
296
|
const response = {
|
|
232
297
|
success: false,
|
|
233
|
-
error:
|
|
298
|
+
error: getErrorMessage(error)
|
|
234
299
|
};
|
|
235
300
|
return c.json(response, 500);
|
|
236
301
|
}
|
|
@@ -258,7 +323,7 @@ function createToolsRoutes(deps) {
|
|
|
258
323
|
} catch (error) {
|
|
259
324
|
const response = {
|
|
260
325
|
success: false,
|
|
261
|
-
error:
|
|
326
|
+
error: getErrorMessage(error)
|
|
262
327
|
};
|
|
263
328
|
return c.json(response, 500);
|
|
264
329
|
}
|
|
@@ -290,7 +355,7 @@ function createToolsRoutes(deps) {
|
|
|
290
355
|
} catch (error) {
|
|
291
356
|
const response = {
|
|
292
357
|
success: false,
|
|
293
|
-
error:
|
|
358
|
+
error: getErrorMessage(error)
|
|
294
359
|
};
|
|
295
360
|
return c.json(response, 500);
|
|
296
361
|
}
|
|
@@ -327,9 +392,9 @@ function createLogsRoutes(_deps) {
|
|
|
327
392
|
}),
|
|
328
393
|
event: "log"
|
|
329
394
|
});
|
|
330
|
-
await new Promise((
|
|
331
|
-
if (aborted) return
|
|
332
|
-
stream.onAbort(() =>
|
|
395
|
+
await new Promise((resolve3) => {
|
|
396
|
+
if (aborted) return resolve3();
|
|
397
|
+
stream.onAbort(() => resolve3());
|
|
333
398
|
});
|
|
334
399
|
if (cleanup) cleanup();
|
|
335
400
|
});
|
|
@@ -384,7 +449,7 @@ function createMemoryRoutes(deps) {
|
|
|
384
449
|
} catch (error) {
|
|
385
450
|
const response = {
|
|
386
451
|
success: false,
|
|
387
|
-
error:
|
|
452
|
+
error: getErrorMessage(error)
|
|
388
453
|
};
|
|
389
454
|
return c.json(response, 500);
|
|
390
455
|
}
|
|
@@ -418,7 +483,7 @@ function createMemoryRoutes(deps) {
|
|
|
418
483
|
} catch (error) {
|
|
419
484
|
const response = {
|
|
420
485
|
success: false,
|
|
421
|
-
error:
|
|
486
|
+
error: getErrorMessage(error)
|
|
422
487
|
};
|
|
423
488
|
return c.json(response, 500);
|
|
424
489
|
}
|
|
@@ -439,7 +504,7 @@ function createMemoryRoutes(deps) {
|
|
|
439
504
|
} catch (error) {
|
|
440
505
|
const response = {
|
|
441
506
|
success: false,
|
|
442
|
-
error:
|
|
507
|
+
error: getErrorMessage(error)
|
|
443
508
|
};
|
|
444
509
|
return c.json(response, 500);
|
|
445
510
|
}
|
|
@@ -488,7 +553,7 @@ function createSoulRoutes(_deps) {
|
|
|
488
553
|
} catch (error) {
|
|
489
554
|
const response = {
|
|
490
555
|
success: false,
|
|
491
|
-
error:
|
|
556
|
+
error: getErrorMessage(error)
|
|
492
557
|
};
|
|
493
558
|
return c.json(response, 500);
|
|
494
559
|
}
|
|
@@ -521,6 +586,7 @@ function createSoulRoutes(_deps) {
|
|
|
521
586
|
}
|
|
522
587
|
const filePath = join(WORKSPACE_ROOT, filename);
|
|
523
588
|
writeFileSync(filePath, body.content, "utf-8");
|
|
589
|
+
clearPromptCache();
|
|
524
590
|
const response = {
|
|
525
591
|
success: true,
|
|
526
592
|
data: { message: `${filename} updated successfully` }
|
|
@@ -529,7 +595,7 @@ function createSoulRoutes(_deps) {
|
|
|
529
595
|
} catch (error) {
|
|
530
596
|
const response = {
|
|
531
597
|
success: false,
|
|
532
|
-
error:
|
|
598
|
+
error: getErrorMessage(error)
|
|
533
599
|
};
|
|
534
600
|
return c.json(response, 500);
|
|
535
601
|
}
|
|
@@ -551,8 +617,124 @@ function createPluginsRoutes(deps) {
|
|
|
551
617
|
return app;
|
|
552
618
|
}
|
|
553
619
|
|
|
554
|
-
// src/webui/routes/
|
|
620
|
+
// src/webui/routes/mcp.ts
|
|
555
621
|
import { Hono as Hono7 } from "hono";
|
|
622
|
+
var SAFE_PACKAGE_RE = /^[@a-zA-Z0-9._\/-]+$/;
|
|
623
|
+
var SAFE_ARG_RE = /^[a-zA-Z0-9._\/:=@-]+$/;
|
|
624
|
+
function createMcpRoutes(deps) {
|
|
625
|
+
const app = new Hono7();
|
|
626
|
+
app.get("/", (c) => {
|
|
627
|
+
const response = {
|
|
628
|
+
success: true,
|
|
629
|
+
data: deps.mcpServers
|
|
630
|
+
};
|
|
631
|
+
return c.json(response);
|
|
632
|
+
});
|
|
633
|
+
app.post("/", async (c) => {
|
|
634
|
+
try {
|
|
635
|
+
const body = await c.req.json();
|
|
636
|
+
if (!body.package && !body.url) {
|
|
637
|
+
return c.json(
|
|
638
|
+
{ success: false, error: "Either 'package' or 'url' is required" },
|
|
639
|
+
400
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
if (body.package && !SAFE_PACKAGE_RE.test(body.package)) {
|
|
643
|
+
return c.json(
|
|
644
|
+
{
|
|
645
|
+
success: false,
|
|
646
|
+
error: "Invalid package name \u2014 only alphanumeric, @, /, ., - allowed"
|
|
647
|
+
},
|
|
648
|
+
400
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (body.args) {
|
|
652
|
+
for (const arg of body.args) {
|
|
653
|
+
if (!SAFE_ARG_RE.test(arg)) {
|
|
654
|
+
return c.json(
|
|
655
|
+
{
|
|
656
|
+
success: false,
|
|
657
|
+
error: `Invalid argument "${arg}" \u2014 only alphanumeric, ., /, :, =, @, - allowed`
|
|
658
|
+
},
|
|
659
|
+
400
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const raw = readRawConfig(deps.configPath);
|
|
665
|
+
if (!raw.mcp || typeof raw.mcp !== "object") raw.mcp = { servers: {} };
|
|
666
|
+
const mcp = raw.mcp;
|
|
667
|
+
if (!mcp.servers || typeof mcp.servers !== "object") mcp.servers = {};
|
|
668
|
+
const servers = mcp.servers;
|
|
669
|
+
const serverName = body.name || deriveServerName(body.package || body.url || "unknown");
|
|
670
|
+
if (servers[serverName]) {
|
|
671
|
+
return c.json(
|
|
672
|
+
{ success: false, error: `Server "${serverName}" already exists` },
|
|
673
|
+
409
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
const entry = {};
|
|
677
|
+
if (body.url) {
|
|
678
|
+
entry.url = body.url;
|
|
679
|
+
} else {
|
|
680
|
+
entry.command = "npx";
|
|
681
|
+
entry.args = ["-y", body.package, ...body.args || []];
|
|
682
|
+
}
|
|
683
|
+
if (body.scope && body.scope !== "always") entry.scope = body.scope;
|
|
684
|
+
if (body.env && Object.keys(body.env).length > 0) entry.env = body.env;
|
|
685
|
+
servers[serverName] = entry;
|
|
686
|
+
writeRawConfig(raw, deps.configPath);
|
|
687
|
+
return c.json({
|
|
688
|
+
success: true,
|
|
689
|
+
data: { name: serverName, message: "Server added. Restart teleton to connect." }
|
|
690
|
+
});
|
|
691
|
+
} catch (error) {
|
|
692
|
+
return c.json(
|
|
693
|
+
{
|
|
694
|
+
success: false,
|
|
695
|
+
error: getErrorMessage(error)
|
|
696
|
+
},
|
|
697
|
+
500
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
app.delete("/:name", (c) => {
|
|
702
|
+
try {
|
|
703
|
+
const name = c.req.param("name");
|
|
704
|
+
const raw = readRawConfig(deps.configPath);
|
|
705
|
+
const mcp = raw.mcp || {};
|
|
706
|
+
const servers = mcp.servers || {};
|
|
707
|
+
if (!servers[name]) {
|
|
708
|
+
return c.json(
|
|
709
|
+
{ success: false, error: `Server "${name}" not found` },
|
|
710
|
+
404
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
delete servers[name];
|
|
714
|
+
writeRawConfig(raw, deps.configPath);
|
|
715
|
+
return c.json({
|
|
716
|
+
success: true,
|
|
717
|
+
data: { name, message: "Server removed. Restart teleton to apply." }
|
|
718
|
+
});
|
|
719
|
+
} catch (error) {
|
|
720
|
+
return c.json(
|
|
721
|
+
{
|
|
722
|
+
success: false,
|
|
723
|
+
error: getErrorMessage(error)
|
|
724
|
+
},
|
|
725
|
+
500
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
return app;
|
|
730
|
+
}
|
|
731
|
+
function deriveServerName(pkg) {
|
|
732
|
+
const unscoped = pkg.includes("/") ? pkg.split("/").pop() : pkg;
|
|
733
|
+
return unscoped.replace(/^server-/, "").replace(/^mcp-server-/, "").replace(/^mcp-/, "");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/webui/routes/workspace.ts
|
|
737
|
+
import { Hono as Hono8 } from "hono";
|
|
556
738
|
import {
|
|
557
739
|
readFileSync as readFileSync2,
|
|
558
740
|
writeFileSync as writeFileSync2,
|
|
@@ -565,7 +747,7 @@ import {
|
|
|
565
747
|
} from "fs";
|
|
566
748
|
import { join as join2, relative } from "path";
|
|
567
749
|
function errorResponse(c, error, status = 500) {
|
|
568
|
-
const message =
|
|
750
|
+
const message = getErrorMessage(error);
|
|
569
751
|
const code = error instanceof WorkspaceSecurityError ? 403 : status;
|
|
570
752
|
const response = { success: false, error: message };
|
|
571
753
|
return c.json(response, code);
|
|
@@ -614,7 +796,7 @@ function listDir(absPath, recursive) {
|
|
|
614
796
|
return entries;
|
|
615
797
|
}
|
|
616
798
|
function createWorkspaceRoutes(_deps) {
|
|
617
|
-
const app = new
|
|
799
|
+
const app = new Hono8();
|
|
618
800
|
app.get("/", (c) => {
|
|
619
801
|
try {
|
|
620
802
|
const subpath = c.req.query("path") || "";
|
|
@@ -778,10 +960,10 @@ function createWorkspaceRoutes(_deps) {
|
|
|
778
960
|
}
|
|
779
961
|
|
|
780
962
|
// src/webui/routes/tasks.ts
|
|
781
|
-
import { Hono as
|
|
963
|
+
import { Hono as Hono9 } from "hono";
|
|
782
964
|
var VALID_STATUSES = ["pending", "in_progress", "done", "failed", "cancelled"];
|
|
783
965
|
function createTasksRoutes(deps) {
|
|
784
|
-
const app = new
|
|
966
|
+
const app = new Hono9();
|
|
785
967
|
function store() {
|
|
786
968
|
return getTaskStore(deps.memory.db);
|
|
787
969
|
}
|
|
@@ -804,7 +986,7 @@ function createTasksRoutes(deps) {
|
|
|
804
986
|
} catch (error) {
|
|
805
987
|
const response = {
|
|
806
988
|
success: false,
|
|
807
|
-
error:
|
|
989
|
+
error: getErrorMessage(error)
|
|
808
990
|
};
|
|
809
991
|
return c.json(response, 500);
|
|
810
992
|
}
|
|
@@ -830,7 +1012,7 @@ function createTasksRoutes(deps) {
|
|
|
830
1012
|
} catch (error) {
|
|
831
1013
|
const response = {
|
|
832
1014
|
success: false,
|
|
833
|
-
error:
|
|
1015
|
+
error: getErrorMessage(error)
|
|
834
1016
|
};
|
|
835
1017
|
return c.json(response, 500);
|
|
836
1018
|
}
|
|
@@ -847,7 +1029,7 @@ function createTasksRoutes(deps) {
|
|
|
847
1029
|
} catch (error) {
|
|
848
1030
|
const response = {
|
|
849
1031
|
success: false,
|
|
850
|
-
error:
|
|
1032
|
+
error: getErrorMessage(error)
|
|
851
1033
|
};
|
|
852
1034
|
return c.json(response, 500);
|
|
853
1035
|
}
|
|
@@ -864,7 +1046,7 @@ function createTasksRoutes(deps) {
|
|
|
864
1046
|
} catch (error) {
|
|
865
1047
|
const response = {
|
|
866
1048
|
success: false,
|
|
867
|
-
error:
|
|
1049
|
+
error: getErrorMessage(error)
|
|
868
1050
|
};
|
|
869
1051
|
return c.json(response, 500);
|
|
870
1052
|
}
|
|
@@ -881,7 +1063,7 @@ function createTasksRoutes(deps) {
|
|
|
881
1063
|
} catch (error) {
|
|
882
1064
|
const response = {
|
|
883
1065
|
success: false,
|
|
884
|
-
error:
|
|
1066
|
+
error: getErrorMessage(error)
|
|
885
1067
|
};
|
|
886
1068
|
return c.json(response, 500);
|
|
887
1069
|
}
|
|
@@ -889,23 +1071,618 @@ function createTasksRoutes(deps) {
|
|
|
889
1071
|
return app;
|
|
890
1072
|
}
|
|
891
1073
|
|
|
1074
|
+
// src/webui/routes/config.ts
|
|
1075
|
+
import { Hono as Hono10 } from "hono";
|
|
1076
|
+
function createConfigRoutes(deps) {
|
|
1077
|
+
const app = new Hono10();
|
|
1078
|
+
app.get("/", (c) => {
|
|
1079
|
+
try {
|
|
1080
|
+
const raw = readRawConfig(deps.configPath);
|
|
1081
|
+
const data = Object.entries(CONFIGURABLE_KEYS).map(([key, meta]) => {
|
|
1082
|
+
const value = getNestedValue(raw, key);
|
|
1083
|
+
const isSet = value != null && value !== "";
|
|
1084
|
+
return {
|
|
1085
|
+
key,
|
|
1086
|
+
set: isSet,
|
|
1087
|
+
value: isSet ? meta.mask(String(value)) : null,
|
|
1088
|
+
sensitive: meta.sensitive,
|
|
1089
|
+
type: meta.type,
|
|
1090
|
+
category: meta.category,
|
|
1091
|
+
description: meta.description,
|
|
1092
|
+
...meta.options ? { options: meta.options } : {}
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
const response = { success: true, data };
|
|
1096
|
+
return c.json(response);
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
return c.json(
|
|
1099
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1100
|
+
500
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
app.put("/:key", async (c) => {
|
|
1105
|
+
const key = c.req.param("key");
|
|
1106
|
+
const meta = CONFIGURABLE_KEYS[key];
|
|
1107
|
+
if (!meta) {
|
|
1108
|
+
const allowed = Object.keys(CONFIGURABLE_KEYS).join(", ");
|
|
1109
|
+
return c.json(
|
|
1110
|
+
{
|
|
1111
|
+
success: false,
|
|
1112
|
+
error: `Key "${key}" is not configurable. Allowed: ${allowed}`
|
|
1113
|
+
},
|
|
1114
|
+
400
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
let body;
|
|
1118
|
+
try {
|
|
1119
|
+
body = await c.req.json();
|
|
1120
|
+
} catch {
|
|
1121
|
+
return c.json({ success: false, error: "Invalid JSON body" }, 400);
|
|
1122
|
+
}
|
|
1123
|
+
const value = body.value;
|
|
1124
|
+
if (value == null || typeof value !== "string") {
|
|
1125
|
+
return c.json(
|
|
1126
|
+
{ success: false, error: "Missing or invalid 'value' field" },
|
|
1127
|
+
400
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
const validationErr = meta.validate(value);
|
|
1131
|
+
if (validationErr) {
|
|
1132
|
+
return c.json(
|
|
1133
|
+
{ success: false, error: `Invalid value for ${key}: ${validationErr}` },
|
|
1134
|
+
400
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
try {
|
|
1138
|
+
const parsed = meta.parse(value);
|
|
1139
|
+
const raw = readRawConfig(deps.configPath);
|
|
1140
|
+
setNestedValue(raw, key, parsed);
|
|
1141
|
+
writeRawConfig(raw, deps.configPath);
|
|
1142
|
+
const runtimeConfig = deps.agent.getConfig();
|
|
1143
|
+
setNestedValue(runtimeConfig, key, parsed);
|
|
1144
|
+
const result = {
|
|
1145
|
+
key,
|
|
1146
|
+
set: true,
|
|
1147
|
+
value: meta.mask(value),
|
|
1148
|
+
sensitive: meta.sensitive,
|
|
1149
|
+
type: meta.type,
|
|
1150
|
+
category: meta.category,
|
|
1151
|
+
description: meta.description,
|
|
1152
|
+
...meta.options ? { options: meta.options } : {}
|
|
1153
|
+
};
|
|
1154
|
+
return c.json({ success: true, data: result });
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
return c.json(
|
|
1157
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1158
|
+
500
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
app.delete("/:key", (c) => {
|
|
1163
|
+
const key = c.req.param("key");
|
|
1164
|
+
const meta = CONFIGURABLE_KEYS[key];
|
|
1165
|
+
if (!meta) {
|
|
1166
|
+
const allowed = Object.keys(CONFIGURABLE_KEYS).join(", ");
|
|
1167
|
+
return c.json(
|
|
1168
|
+
{
|
|
1169
|
+
success: false,
|
|
1170
|
+
error: `Key "${key}" is not configurable. Allowed: ${allowed}`
|
|
1171
|
+
},
|
|
1172
|
+
400
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
const raw = readRawConfig(deps.configPath);
|
|
1177
|
+
deleteNestedValue(raw, key);
|
|
1178
|
+
writeRawConfig(raw, deps.configPath);
|
|
1179
|
+
const runtimeConfig = deps.agent.getConfig();
|
|
1180
|
+
deleteNestedValue(runtimeConfig, key);
|
|
1181
|
+
const result = {
|
|
1182
|
+
key,
|
|
1183
|
+
set: false,
|
|
1184
|
+
value: null,
|
|
1185
|
+
sensitive: meta.sensitive,
|
|
1186
|
+
type: meta.type,
|
|
1187
|
+
category: meta.category,
|
|
1188
|
+
description: meta.description,
|
|
1189
|
+
...meta.options ? { options: meta.options } : {}
|
|
1190
|
+
};
|
|
1191
|
+
return c.json({ success: true, data: result });
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
return c.json(
|
|
1194
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1195
|
+
500
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
return app;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/webui/routes/marketplace.ts
|
|
1203
|
+
import { Hono as Hono11 } from "hono";
|
|
1204
|
+
|
|
1205
|
+
// src/webui/services/marketplace.ts
|
|
1206
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
|
|
1207
|
+
import { join as join3, resolve } from "path";
|
|
1208
|
+
import { pathToFileURL } from "url";
|
|
1209
|
+
var log = createLogger("WebUI");
|
|
1210
|
+
var REGISTRY_URL = "https://raw.githubusercontent.com/TONresistor/teleton-plugins/main/registry.json";
|
|
1211
|
+
var PLUGIN_BASE_URL = "https://raw.githubusercontent.com/TONresistor/teleton-plugins/main";
|
|
1212
|
+
var GITHUB_API_BASE = "https://api.github.com/repos/TONresistor/teleton-plugins/contents";
|
|
1213
|
+
var CACHE_TTL = 5 * 60 * 1e3;
|
|
1214
|
+
var PLUGINS_DIR = WORKSPACE_PATHS.PLUGINS_DIR;
|
|
1215
|
+
var VALID_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
1216
|
+
var MarketplaceService = class {
|
|
1217
|
+
deps;
|
|
1218
|
+
cache = null;
|
|
1219
|
+
fetchPromise = null;
|
|
1220
|
+
manifestCache = /* @__PURE__ */ new Map();
|
|
1221
|
+
installing = /* @__PURE__ */ new Set();
|
|
1222
|
+
constructor(deps) {
|
|
1223
|
+
this.deps = deps;
|
|
1224
|
+
}
|
|
1225
|
+
// ── Registry ────────────────────────────────────────────────────────
|
|
1226
|
+
async getRegistry(forceRefresh = false) {
|
|
1227
|
+
if (!forceRefresh && this.cache && Date.now() - this.cache.fetchedAt < CACHE_TTL) {
|
|
1228
|
+
return this.cache.entries;
|
|
1229
|
+
}
|
|
1230
|
+
if (this.fetchPromise) return this.fetchPromise;
|
|
1231
|
+
this.fetchPromise = this.fetchRegistry();
|
|
1232
|
+
try {
|
|
1233
|
+
const entries = await this.fetchPromise;
|
|
1234
|
+
this.cache = { entries, fetchedAt: Date.now() };
|
|
1235
|
+
return entries;
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
if (this.cache) {
|
|
1238
|
+
log.warn({ err }, "Registry fetch failed, using stale cache");
|
|
1239
|
+
return this.cache.entries;
|
|
1240
|
+
}
|
|
1241
|
+
throw err;
|
|
1242
|
+
} finally {
|
|
1243
|
+
this.fetchPromise = null;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
async fetchRegistry() {
|
|
1247
|
+
const res = await fetch(REGISTRY_URL);
|
|
1248
|
+
if (!res.ok) throw new Error(`Registry fetch failed: ${res.status} ${res.statusText}`);
|
|
1249
|
+
const data = await res.json();
|
|
1250
|
+
const plugins = Array.isArray(data) ? data : data?.plugins;
|
|
1251
|
+
if (!Array.isArray(plugins)) throw new Error("Registry has no plugins array");
|
|
1252
|
+
const VALID_PATH = /^[a-zA-Z0-9][a-zA-Z0-9._\/-]*$/;
|
|
1253
|
+
for (const entry of plugins) {
|
|
1254
|
+
if (!entry.id || !entry.name || !entry.path) {
|
|
1255
|
+
throw new Error(`Invalid registry entry: missing required fields (id=${entry.id ?? "?"})`);
|
|
1256
|
+
}
|
|
1257
|
+
if (!VALID_PATH.test(entry.path) || entry.path.includes("..")) {
|
|
1258
|
+
throw new Error(`Invalid registry path for "${entry.id}": "${entry.path}"`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return plugins;
|
|
1262
|
+
}
|
|
1263
|
+
// ── Remote manifest ─────────────────────────────────────────────────
|
|
1264
|
+
async fetchRemoteManifest(entry) {
|
|
1265
|
+
const cached = this.manifestCache.get(entry.id);
|
|
1266
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
|
|
1267
|
+
return cached.data;
|
|
1268
|
+
}
|
|
1269
|
+
const url = `${PLUGIN_BASE_URL}/${entry.path}/manifest.json`;
|
|
1270
|
+
const res = await fetch(url);
|
|
1271
|
+
if (!res.ok) {
|
|
1272
|
+
return {
|
|
1273
|
+
name: entry.name,
|
|
1274
|
+
version: "0.0.0",
|
|
1275
|
+
description: entry.description,
|
|
1276
|
+
author: entry.author
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
const raw = await res.json();
|
|
1280
|
+
const data = {
|
|
1281
|
+
...raw,
|
|
1282
|
+
author: normalizeAuthor(raw.author)
|
|
1283
|
+
};
|
|
1284
|
+
this.manifestCache.set(entry.id, { data, fetchedAt: Date.now() });
|
|
1285
|
+
return data;
|
|
1286
|
+
}
|
|
1287
|
+
// ── List plugins (combined view) ────────────────────────────────────
|
|
1288
|
+
async listPlugins(forceRefresh = false) {
|
|
1289
|
+
const registry = await this.getRegistry(forceRefresh);
|
|
1290
|
+
const results = [];
|
|
1291
|
+
const manifests = await Promise.allSettled(
|
|
1292
|
+
registry.map((entry) => this.fetchRemoteManifest(entry))
|
|
1293
|
+
);
|
|
1294
|
+
for (let i = 0; i < registry.length; i++) {
|
|
1295
|
+
const entry = registry[i];
|
|
1296
|
+
const manifestResult = manifests[i];
|
|
1297
|
+
const manifest = manifestResult.status === "fulfilled" ? manifestResult.value : {
|
|
1298
|
+
name: entry.name,
|
|
1299
|
+
version: "0.0.0",
|
|
1300
|
+
description: entry.description,
|
|
1301
|
+
author: entry.author
|
|
1302
|
+
};
|
|
1303
|
+
const installed = this.deps.modules.find((m) => m.name === entry.id || m.name === entry.name);
|
|
1304
|
+
const installedVersion = installed?.version ?? null;
|
|
1305
|
+
const remoteVersion = manifest.version || "0.0.0";
|
|
1306
|
+
let status = "available";
|
|
1307
|
+
if (installedVersion) {
|
|
1308
|
+
status = installedVersion !== remoteVersion ? "updatable" : "installed";
|
|
1309
|
+
}
|
|
1310
|
+
let toolCount = manifest.tools?.length ?? 0;
|
|
1311
|
+
let tools = manifest.tools ?? [];
|
|
1312
|
+
if (installed) {
|
|
1313
|
+
const moduleTools = this.deps.toolRegistry.getModuleTools(installed.name);
|
|
1314
|
+
const allToolDefs = this.deps.toolRegistry.getAll();
|
|
1315
|
+
const toolMap = new Map(allToolDefs.map((t) => [t.name, t]));
|
|
1316
|
+
tools = moduleTools.map((mt) => ({
|
|
1317
|
+
name: mt.name,
|
|
1318
|
+
description: toolMap.get(mt.name)?.description ?? ""
|
|
1319
|
+
}));
|
|
1320
|
+
toolCount = tools.length;
|
|
1321
|
+
}
|
|
1322
|
+
results.push({
|
|
1323
|
+
id: entry.id,
|
|
1324
|
+
name: entry.name,
|
|
1325
|
+
description: manifest.description || entry.description,
|
|
1326
|
+
author: manifest.author || entry.author,
|
|
1327
|
+
tags: entry.tags,
|
|
1328
|
+
remoteVersion,
|
|
1329
|
+
installedVersion,
|
|
1330
|
+
status,
|
|
1331
|
+
toolCount,
|
|
1332
|
+
tools,
|
|
1333
|
+
secrets: manifest.secrets
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
return results;
|
|
1337
|
+
}
|
|
1338
|
+
// ── Install ─────────────────────────────────────────────────────────
|
|
1339
|
+
async installPlugin(pluginId) {
|
|
1340
|
+
this.validateId(pluginId);
|
|
1341
|
+
if (this.installing.has(pluginId)) {
|
|
1342
|
+
throw new ConflictError(`Plugin "${pluginId}" is already being installed`);
|
|
1343
|
+
}
|
|
1344
|
+
const existing = this.findModuleByPluginId(pluginId);
|
|
1345
|
+
if (existing) {
|
|
1346
|
+
throw new ConflictError(`Plugin "${pluginId}" is already installed`);
|
|
1347
|
+
}
|
|
1348
|
+
this.installing.add(pluginId);
|
|
1349
|
+
const pluginDir = join3(PLUGINS_DIR, pluginId);
|
|
1350
|
+
try {
|
|
1351
|
+
const registry = await this.getRegistry();
|
|
1352
|
+
const entry = registry.find((e) => e.id === pluginId);
|
|
1353
|
+
if (!entry) throw new Error(`Plugin "${pluginId}" not found in registry`);
|
|
1354
|
+
const manifest = await this.fetchRemoteManifest(entry);
|
|
1355
|
+
mkdirSync2(pluginDir, { recursive: true });
|
|
1356
|
+
await this.downloadDir(entry.path, pluginDir);
|
|
1357
|
+
await ensurePluginDeps(pluginDir, pluginId);
|
|
1358
|
+
const indexPath = join3(pluginDir, "index.js");
|
|
1359
|
+
const moduleUrl = pathToFileURL(indexPath).href + `?t=${Date.now()}`;
|
|
1360
|
+
const mod = await import(moduleUrl);
|
|
1361
|
+
const adapted = adaptPlugin(
|
|
1362
|
+
mod,
|
|
1363
|
+
pluginId,
|
|
1364
|
+
this.deps.config,
|
|
1365
|
+
this.deps.loadedModuleNames,
|
|
1366
|
+
this.deps.sdkDeps
|
|
1367
|
+
);
|
|
1368
|
+
adapted.migrate?.(this.deps.pluginContext.db);
|
|
1369
|
+
const tools = adapted.tools(this.deps.config);
|
|
1370
|
+
const toolCount = this.deps.toolRegistry.registerPluginTools(adapted.name, tools);
|
|
1371
|
+
await adapted.start?.(this.deps.pluginContext);
|
|
1372
|
+
this.deps.modules.push(adapted);
|
|
1373
|
+
this.deps.rewireHooks();
|
|
1374
|
+
return {
|
|
1375
|
+
name: adapted.name,
|
|
1376
|
+
version: adapted.version,
|
|
1377
|
+
toolCount
|
|
1378
|
+
};
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
if (existsSync2(pluginDir)) {
|
|
1381
|
+
try {
|
|
1382
|
+
rmSync2(pluginDir, { recursive: true, force: true });
|
|
1383
|
+
} catch (cleanupErr) {
|
|
1384
|
+
log.error({ err: cleanupErr }, `Failed to cleanup ${pluginDir}`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
throw err;
|
|
1388
|
+
} finally {
|
|
1389
|
+
this.installing.delete(pluginId);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
// ── Uninstall ───────────────────────────────────────────────────────
|
|
1393
|
+
async uninstallPlugin(pluginId) {
|
|
1394
|
+
this.validateId(pluginId);
|
|
1395
|
+
if (this.installing.has(pluginId)) {
|
|
1396
|
+
throw new ConflictError(`Plugin "${pluginId}" has an operation in progress`);
|
|
1397
|
+
}
|
|
1398
|
+
const mod = this.findModuleByPluginId(pluginId);
|
|
1399
|
+
if (!mod) {
|
|
1400
|
+
throw new Error(`Plugin "${pluginId}" is not installed`);
|
|
1401
|
+
}
|
|
1402
|
+
const moduleName = mod.name;
|
|
1403
|
+
const idx = this.deps.modules.indexOf(mod);
|
|
1404
|
+
this.installing.add(pluginId);
|
|
1405
|
+
try {
|
|
1406
|
+
await mod.stop?.();
|
|
1407
|
+
this.deps.toolRegistry.removePluginTools(moduleName);
|
|
1408
|
+
if (idx >= 0) this.deps.modules.splice(idx, 1);
|
|
1409
|
+
this.deps.rewireHooks();
|
|
1410
|
+
const pluginDir = join3(PLUGINS_DIR, pluginId);
|
|
1411
|
+
if (existsSync2(pluginDir)) {
|
|
1412
|
+
rmSync2(pluginDir, { recursive: true, force: true });
|
|
1413
|
+
}
|
|
1414
|
+
return { message: `Plugin "${pluginId}" uninstalled successfully` };
|
|
1415
|
+
} finally {
|
|
1416
|
+
this.installing.delete(pluginId);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// ── Update ──────────────────────────────────────────────────────────
|
|
1420
|
+
async updatePlugin(pluginId) {
|
|
1421
|
+
await this.uninstallPlugin(pluginId);
|
|
1422
|
+
return this.installPlugin(pluginId);
|
|
1423
|
+
}
|
|
1424
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
1425
|
+
/**
|
|
1426
|
+
* Resolve a registry plugin ID to the actual loaded module.
|
|
1427
|
+
* Handles name mismatch: registry id "fragment" → module name "Fragment Marketplace".
|
|
1428
|
+
*/
|
|
1429
|
+
findModuleByPluginId(pluginId) {
|
|
1430
|
+
let mod = this.deps.modules.find((m) => m.name === pluginId);
|
|
1431
|
+
if (mod) return mod;
|
|
1432
|
+
const entry = this.cache?.entries.find((e) => e.id === pluginId);
|
|
1433
|
+
if (entry) {
|
|
1434
|
+
mod = this.deps.modules.find((m) => m.name === entry.name);
|
|
1435
|
+
}
|
|
1436
|
+
return mod ?? null;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Recursively download a GitHub directory to a local path.
|
|
1440
|
+
* Uses the GitHub Contents API to list files, then fetches each via raw.githubusercontent.
|
|
1441
|
+
*/
|
|
1442
|
+
async downloadDir(remotePath, localDir, depth = 0) {
|
|
1443
|
+
if (depth > 5) throw new Error("Plugin directory too deeply nested");
|
|
1444
|
+
const res = await fetch(`${GITHUB_API_BASE}/${remotePath}`);
|
|
1445
|
+
if (!res.ok) throw new Error(`Failed to list directory "${remotePath}": ${res.status}`);
|
|
1446
|
+
const entries = await res.json();
|
|
1447
|
+
for (const item of entries) {
|
|
1448
|
+
if (!item.name || /[/\\]/.test(item.name) || item.name === ".." || item.name === ".") {
|
|
1449
|
+
throw new Error(`Invalid entry name in plugin directory: "${item.name}"`);
|
|
1450
|
+
}
|
|
1451
|
+
const target = resolve(localDir, item.name);
|
|
1452
|
+
if (!target.startsWith(resolve(PLUGINS_DIR))) {
|
|
1453
|
+
throw new Error(`Path escape detected: ${target}`);
|
|
1454
|
+
}
|
|
1455
|
+
if (item.type === "dir") {
|
|
1456
|
+
mkdirSync2(target, { recursive: true });
|
|
1457
|
+
await this.downloadDir(item.path, target, depth + 1);
|
|
1458
|
+
} else if (item.type === "file" && item.download_url) {
|
|
1459
|
+
const url = new URL(item.download_url);
|
|
1460
|
+
if (!url.hostname.endsWith("githubusercontent.com") && !url.hostname.endsWith("github.com")) {
|
|
1461
|
+
throw new Error(`Untrusted download host: ${url.hostname}`);
|
|
1462
|
+
}
|
|
1463
|
+
const fileRes = await fetch(item.download_url);
|
|
1464
|
+
if (!fileRes.ok) throw new Error(`Failed to download ${item.name}: ${fileRes.status}`);
|
|
1465
|
+
const content = await fileRes.text();
|
|
1466
|
+
writeFileSync3(target, content, "utf-8");
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
validateId(id) {
|
|
1471
|
+
if (!VALID_ID.test(id)) {
|
|
1472
|
+
throw new Error(`Invalid plugin ID: "${id}"`);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
function normalizeAuthor(author) {
|
|
1477
|
+
if (typeof author === "string") return author;
|
|
1478
|
+
if (author && typeof author === "object" && "name" in author) {
|
|
1479
|
+
return String(author.name);
|
|
1480
|
+
}
|
|
1481
|
+
return "unknown";
|
|
1482
|
+
}
|
|
1483
|
+
var ConflictError = class extends Error {
|
|
1484
|
+
constructor(message) {
|
|
1485
|
+
super(message);
|
|
1486
|
+
this.name = "ConflictError";
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
// src/webui/routes/marketplace.ts
|
|
1491
|
+
var VALID_ID2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
1492
|
+
var VALID_KEY = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
1493
|
+
function createMarketplaceRoutes(deps) {
|
|
1494
|
+
const app = new Hono11();
|
|
1495
|
+
let service = null;
|
|
1496
|
+
const getService = () => {
|
|
1497
|
+
if (!deps.marketplace) return null;
|
|
1498
|
+
service ??= new MarketplaceService({ ...deps.marketplace, toolRegistry: deps.toolRegistry });
|
|
1499
|
+
return service;
|
|
1500
|
+
};
|
|
1501
|
+
app.get("/", async (c) => {
|
|
1502
|
+
const svc = getService();
|
|
1503
|
+
if (!svc) {
|
|
1504
|
+
return c.json({ success: false, error: "Marketplace not configured" }, 501);
|
|
1505
|
+
}
|
|
1506
|
+
try {
|
|
1507
|
+
const refresh = c.req.query("refresh") === "true";
|
|
1508
|
+
const plugins = await svc.listPlugins(refresh);
|
|
1509
|
+
return c.json({ success: true, data: plugins });
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
return c.json(
|
|
1512
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1513
|
+
500
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
app.post("/install", async (c) => {
|
|
1518
|
+
const svc = getService();
|
|
1519
|
+
if (!svc) {
|
|
1520
|
+
return c.json({ success: false, error: "Marketplace not configured" }, 501);
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
const body = await c.req.json();
|
|
1524
|
+
if (!body.id) {
|
|
1525
|
+
return c.json({ success: false, error: "Missing plugin id" }, 400);
|
|
1526
|
+
}
|
|
1527
|
+
const result = await svc.installPlugin(body.id);
|
|
1528
|
+
deps.plugins.length = 0;
|
|
1529
|
+
deps.plugins.push(
|
|
1530
|
+
...deps.marketplace.modules.filter((m) => deps.toolRegistry.isPluginModule(m.name)).map((m) => ({ name: m.name, version: m.version ?? "0.0.0" }))
|
|
1531
|
+
);
|
|
1532
|
+
return c.json({ success: true, data: result });
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
const status = err instanceof ConflictError ? 409 : 500;
|
|
1535
|
+
return c.json(
|
|
1536
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1537
|
+
status
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
app.post("/uninstall", async (c) => {
|
|
1542
|
+
const svc = getService();
|
|
1543
|
+
if (!svc) {
|
|
1544
|
+
return c.json({ success: false, error: "Marketplace not configured" }, 501);
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
const body = await c.req.json();
|
|
1548
|
+
if (!body.id) {
|
|
1549
|
+
return c.json({ success: false, error: "Missing plugin id" }, 400);
|
|
1550
|
+
}
|
|
1551
|
+
const result = await svc.uninstallPlugin(body.id);
|
|
1552
|
+
deps.plugins.length = 0;
|
|
1553
|
+
deps.plugins.push(
|
|
1554
|
+
...deps.marketplace.modules.filter((m) => deps.toolRegistry.isPluginModule(m.name)).map((m) => ({ name: m.name, version: m.version ?? "0.0.0" }))
|
|
1555
|
+
);
|
|
1556
|
+
return c.json({ success: true, data: result });
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
const status = err instanceof ConflictError ? 409 : 500;
|
|
1559
|
+
return c.json(
|
|
1560
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1561
|
+
status
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
app.post("/update", async (c) => {
|
|
1566
|
+
const svc = getService();
|
|
1567
|
+
if (!svc) {
|
|
1568
|
+
return c.json({ success: false, error: "Marketplace not configured" }, 501);
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
const body = await c.req.json();
|
|
1572
|
+
if (!body.id) {
|
|
1573
|
+
return c.json({ success: false, error: "Missing plugin id" }, 400);
|
|
1574
|
+
}
|
|
1575
|
+
const result = await svc.updatePlugin(body.id);
|
|
1576
|
+
deps.plugins.length = 0;
|
|
1577
|
+
deps.plugins.push(
|
|
1578
|
+
...deps.marketplace.modules.filter((m) => deps.toolRegistry.isPluginModule(m.name)).map((m) => ({ name: m.name, version: m.version ?? "0.0.0" }))
|
|
1579
|
+
);
|
|
1580
|
+
return c.json({ success: true, data: result });
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
const status = err instanceof ConflictError ? 409 : 500;
|
|
1583
|
+
return c.json(
|
|
1584
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1585
|
+
status
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
app.get("/secrets/:pluginId", async (c) => {
|
|
1590
|
+
const svc = getService();
|
|
1591
|
+
if (!svc) {
|
|
1592
|
+
return c.json({ success: false, error: "Marketplace not configured" }, 501);
|
|
1593
|
+
}
|
|
1594
|
+
const pluginId = c.req.param("pluginId");
|
|
1595
|
+
if (!VALID_ID2.test(pluginId)) {
|
|
1596
|
+
return c.json({ success: false, error: "Invalid plugin ID" }, 400);
|
|
1597
|
+
}
|
|
1598
|
+
try {
|
|
1599
|
+
const plugins = await svc.listPlugins();
|
|
1600
|
+
const plugin = plugins.find((p) => p.id === pluginId);
|
|
1601
|
+
const declared = plugin?.secrets ?? {};
|
|
1602
|
+
const configured = listPluginSecretKeys(pluginId);
|
|
1603
|
+
return c.json({ success: true, data: { declared, configured } });
|
|
1604
|
+
} catch (err) {
|
|
1605
|
+
return c.json(
|
|
1606
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1607
|
+
500
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
app.put("/secrets/:pluginId/:key", async (c) => {
|
|
1612
|
+
const pluginId = c.req.param("pluginId");
|
|
1613
|
+
const key = c.req.param("key");
|
|
1614
|
+
if (!VALID_ID2.test(pluginId)) {
|
|
1615
|
+
return c.json({ success: false, error: "Invalid plugin ID" }, 400);
|
|
1616
|
+
}
|
|
1617
|
+
if (!key || !VALID_KEY.test(key)) {
|
|
1618
|
+
return c.json(
|
|
1619
|
+
{ success: false, error: "Invalid key name \u2014 use letters, digits, underscores" },
|
|
1620
|
+
400
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
try {
|
|
1624
|
+
const body = await c.req.json();
|
|
1625
|
+
if (typeof body.value !== "string" || !body.value) {
|
|
1626
|
+
return c.json({ success: false, error: "Missing or invalid value" }, 400);
|
|
1627
|
+
}
|
|
1628
|
+
writePluginSecret(pluginId, key, body.value);
|
|
1629
|
+
return c.json({
|
|
1630
|
+
success: true,
|
|
1631
|
+
data: { key, set: true }
|
|
1632
|
+
});
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
return c.json(
|
|
1635
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1636
|
+
500
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
app.delete("/secrets/:pluginId/:key", async (c) => {
|
|
1641
|
+
const pluginId = c.req.param("pluginId");
|
|
1642
|
+
const key = c.req.param("key");
|
|
1643
|
+
if (!VALID_ID2.test(pluginId)) {
|
|
1644
|
+
return c.json({ success: false, error: "Invalid plugin ID" }, 400);
|
|
1645
|
+
}
|
|
1646
|
+
if (!key || !VALID_KEY.test(key)) {
|
|
1647
|
+
return c.json(
|
|
1648
|
+
{ success: false, error: "Invalid key name \u2014 use letters, digits, underscores" },
|
|
1649
|
+
400
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
try {
|
|
1653
|
+
deletePluginSecret(pluginId, key);
|
|
1654
|
+
return c.json({
|
|
1655
|
+
success: true,
|
|
1656
|
+
data: { key, set: false }
|
|
1657
|
+
});
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
return c.json(
|
|
1660
|
+
{ success: false, error: err instanceof Error ? err.message : String(err) },
|
|
1661
|
+
500
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
return app;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
892
1668
|
// src/webui/server.ts
|
|
1669
|
+
var log2 = createLogger("WebUI");
|
|
893
1670
|
function findWebDist() {
|
|
894
1671
|
const candidates = [
|
|
895
|
-
|
|
1672
|
+
resolve2("dist/web"),
|
|
896
1673
|
// npm start / teleton start (from project root)
|
|
897
|
-
|
|
1674
|
+
resolve2("web")
|
|
898
1675
|
// fallback
|
|
899
1676
|
];
|
|
900
1677
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
901
1678
|
candidates.push(
|
|
902
|
-
|
|
1679
|
+
resolve2(__dirname, "web"),
|
|
903
1680
|
// dist/web when __dirname = dist/
|
|
904
|
-
|
|
1681
|
+
resolve2(__dirname, "../dist/web")
|
|
905
1682
|
// when running with tsx from src/
|
|
906
1683
|
);
|
|
907
1684
|
for (const candidate of candidates) {
|
|
908
|
-
if (
|
|
1685
|
+
if (existsSync3(join4(candidate, "index.html"))) {
|
|
909
1686
|
return candidate;
|
|
910
1687
|
}
|
|
911
1688
|
}
|
|
@@ -918,7 +1695,7 @@ var WebUIServer = class {
|
|
|
918
1695
|
authToken;
|
|
919
1696
|
constructor(deps) {
|
|
920
1697
|
this.deps = deps;
|
|
921
|
-
this.app = new
|
|
1698
|
+
this.app = new Hono12();
|
|
922
1699
|
this.authToken = deps.config.auth_token || generateToken();
|
|
923
1700
|
this.setupMiddleware();
|
|
924
1701
|
this.setupRoutes();
|
|
@@ -950,7 +1727,7 @@ var WebUIServer = class {
|
|
|
950
1727
|
const start = Date.now();
|
|
951
1728
|
await next();
|
|
952
1729
|
const duration = Date.now() - start;
|
|
953
|
-
|
|
1730
|
+
log2.info(`${c.req.method} ${c.req.path} \u2192 ${c.res.status} (${duration}ms)`);
|
|
954
1731
|
});
|
|
955
1732
|
}
|
|
956
1733
|
this.app.use(
|
|
@@ -1023,11 +1800,14 @@ var WebUIServer = class {
|
|
|
1023
1800
|
this.app.route("/api/memory", createMemoryRoutes(this.deps));
|
|
1024
1801
|
this.app.route("/api/soul", createSoulRoutes(this.deps));
|
|
1025
1802
|
this.app.route("/api/plugins", createPluginsRoutes(this.deps));
|
|
1803
|
+
this.app.route("/api/mcp", createMcpRoutes(this.deps));
|
|
1026
1804
|
this.app.route("/api/workspace", createWorkspaceRoutes(this.deps));
|
|
1027
1805
|
this.app.route("/api/tasks", createTasksRoutes(this.deps));
|
|
1806
|
+
this.app.route("/api/config", createConfigRoutes(this.deps));
|
|
1807
|
+
this.app.route("/api/marketplace", createMarketplaceRoutes(this.deps));
|
|
1028
1808
|
const webDist = findWebDist();
|
|
1029
1809
|
if (webDist) {
|
|
1030
|
-
const indexHtml = readFileSync3(
|
|
1810
|
+
const indexHtml = readFileSync3(join4(webDist, "index.html"), "utf-8");
|
|
1031
1811
|
const mimeTypes = {
|
|
1032
1812
|
js: "application/javascript",
|
|
1033
1813
|
css: "text/css",
|
|
@@ -1041,9 +1821,9 @@ var WebUIServer = class {
|
|
|
1041
1821
|
woff: "font/woff"
|
|
1042
1822
|
};
|
|
1043
1823
|
this.app.get("*", (c) => {
|
|
1044
|
-
const filePath =
|
|
1824
|
+
const filePath = resolve2(join4(webDist, c.req.path));
|
|
1045
1825
|
const rel = relative2(webDist, filePath);
|
|
1046
|
-
if (rel.startsWith("..") ||
|
|
1826
|
+
if (rel.startsWith("..") || resolve2(filePath) !== filePath) {
|
|
1047
1827
|
return c.html(indexHtml);
|
|
1048
1828
|
}
|
|
1049
1829
|
try {
|
|
@@ -1062,7 +1842,7 @@ var WebUIServer = class {
|
|
|
1062
1842
|
});
|
|
1063
1843
|
}
|
|
1064
1844
|
this.app.onError((err, c) => {
|
|
1065
|
-
|
|
1845
|
+
log2.error({ err }, "WebUI error");
|
|
1066
1846
|
return c.json(
|
|
1067
1847
|
{
|
|
1068
1848
|
success: false,
|
|
@@ -1073,7 +1853,7 @@ var WebUIServer = class {
|
|
|
1073
1853
|
});
|
|
1074
1854
|
}
|
|
1075
1855
|
async start() {
|
|
1076
|
-
return new Promise((
|
|
1856
|
+
return new Promise((resolve3, reject) => {
|
|
1077
1857
|
try {
|
|
1078
1858
|
logInterceptor.install();
|
|
1079
1859
|
this.server = serve(
|
|
@@ -1084,14 +1864,10 @@ var WebUIServer = class {
|
|
|
1084
1864
|
},
|
|
1085
1865
|
(info) => {
|
|
1086
1866
|
const url = `http://${info.address}:${info.port}`;
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
` Token: ${maskToken(this.authToken)} (use Bearer header for API access)
|
|
1092
|
-
`
|
|
1093
|
-
);
|
|
1094
|
-
resolve2();
|
|
1867
|
+
log2.info(`WebUI server running`);
|
|
1868
|
+
log2.info(`URL: ${url}/auth/exchange?token=${this.authToken}`);
|
|
1869
|
+
log2.info(`Token: ${maskToken(this.authToken)} (use Bearer header for API access)`);
|
|
1870
|
+
resolve3();
|
|
1095
1871
|
}
|
|
1096
1872
|
);
|
|
1097
1873
|
} catch (error) {
|
|
@@ -1102,11 +1878,11 @@ var WebUIServer = class {
|
|
|
1102
1878
|
}
|
|
1103
1879
|
async stop() {
|
|
1104
1880
|
if (this.server) {
|
|
1105
|
-
return new Promise((
|
|
1881
|
+
return new Promise((resolve3) => {
|
|
1106
1882
|
this.server.close(() => {
|
|
1107
1883
|
logInterceptor.uninstall();
|
|
1108
|
-
|
|
1109
|
-
|
|
1884
|
+
log2.info("WebUI server stopped");
|
|
1885
|
+
resolve3();
|
|
1110
1886
|
});
|
|
1111
1887
|
});
|
|
1112
1888
|
}
|