teleton 0.5.2 → 0.6.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.
@@ -1,27 +1,45 @@
1
+ import {
2
+ CONFIGURABLE_KEYS,
3
+ deleteNestedValue,
4
+ getNestedValue,
5
+ readRawConfig,
6
+ setNestedValue,
7
+ writeRawConfig
8
+ } from "./chunk-2QUJLHCZ.js";
1
9
  import {
2
10
  WorkspaceSecurityError,
11
+ adaptPlugin,
12
+ deletePluginSecret,
13
+ ensurePluginDeps,
14
+ listPluginSecretKeys,
3
15
  validateDirectory,
4
16
  validatePath,
5
17
  validateReadPath,
6
- validateWritePath
7
- } from "./chunk-5WWR4CU3.js";
8
- import "./chunk-O4R7V5Y2.js";
18
+ validateWritePath,
19
+ writePluginSecret
20
+ } from "./chunk-4IPJ25HE.js";
21
+ import "./chunk-ECSCVEQQ.js";
22
+ import "./chunk-GDCODBNO.js";
23
+ import "./chunk-4DU3C27M.js";
24
+ import "./chunk-RO62LO6Z.js";
9
25
  import {
26
+ WORKSPACE_PATHS,
10
27
  WORKSPACE_ROOT
11
28
  } from "./chunk-EYWNOHMJ.js";
12
29
  import {
13
30
  getTaskStore
14
31
  } from "./chunk-NUGDTPE4.js";
32
+ import "./chunk-QUAPFI2N.js";
15
33
  import "./chunk-QGM4M3NI.js";
16
34
 
17
35
  // src/webui/server.ts
18
- import { Hono as Hono9 } from "hono";
36
+ import { Hono as Hono12 } from "hono";
19
37
  import { serve } from "@hono/node-server";
20
38
  import { cors } from "hono/cors";
21
39
  import { bodyLimit } from "hono/body-limit";
22
40
  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";
41
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
42
+ import { join as join4, dirname, resolve as resolve2, relative as relative2 } from "path";
25
43
  import { fileURLToPath } from "url";
26
44
 
27
45
  // src/webui/middleware/auth.ts
@@ -174,6 +192,55 @@ function createToolsRoutes(deps) {
174
192
  return c.json(response, 500);
175
193
  }
176
194
  });
195
+ app.get("/rag", (c) => {
196
+ try {
197
+ const config = deps.agent.getConfig();
198
+ const toolIndex = deps.toolRegistry.getToolIndex();
199
+ const response = {
200
+ success: true,
201
+ data: {
202
+ enabled: config.tool_rag.enabled,
203
+ indexed: toolIndex?.isIndexed ?? false,
204
+ topK: config.tool_rag.top_k,
205
+ totalTools: deps.toolRegistry.count,
206
+ alwaysInclude: config.tool_rag.always_include,
207
+ skipUnlimitedProviders: config.tool_rag.skip_unlimited_providers
208
+ }
209
+ };
210
+ return c.json(response);
211
+ } catch (error) {
212
+ return c.json({ success: false, error: String(error) }, 500);
213
+ }
214
+ });
215
+ app.put("/rag", async (c) => {
216
+ try {
217
+ const config = deps.agent.getConfig();
218
+ const body = await c.req.json();
219
+ const { enabled, topK } = body;
220
+ if (enabled !== void 0) {
221
+ config.tool_rag.enabled = enabled;
222
+ }
223
+ if (topK !== void 0) {
224
+ if (topK < 5 || topK > 200) {
225
+ return c.json({ success: false, error: "topK must be between 5 and 200" }, 400);
226
+ }
227
+ config.tool_rag.top_k = topK;
228
+ }
229
+ const toolIndex = deps.toolRegistry.getToolIndex();
230
+ const response = {
231
+ success: true,
232
+ data: {
233
+ enabled: config.tool_rag.enabled,
234
+ indexed: toolIndex?.isIndexed ?? false,
235
+ topK: config.tool_rag.top_k,
236
+ totalTools: deps.toolRegistry.count
237
+ }
238
+ };
239
+ return c.json(response);
240
+ } catch (error) {
241
+ return c.json({ success: false, error: String(error) }, 500);
242
+ }
243
+ });
177
244
  app.put("/:name", async (c) => {
178
245
  try {
179
246
  const toolName = c.req.param("name");
@@ -327,9 +394,9 @@ function createLogsRoutes(_deps) {
327
394
  }),
328
395
  event: "log"
329
396
  });
330
- await new Promise((resolve2) => {
331
- if (aborted) return resolve2();
332
- stream.onAbort(() => resolve2());
397
+ await new Promise((resolve3) => {
398
+ if (aborted) return resolve3();
399
+ stream.onAbort(() => resolve3());
333
400
  });
334
401
  if (cleanup) cleanup();
335
402
  });
@@ -551,8 +618,124 @@ function createPluginsRoutes(deps) {
551
618
  return app;
552
619
  }
553
620
 
554
- // src/webui/routes/workspace.ts
621
+ // src/webui/routes/mcp.ts
555
622
  import { Hono as Hono7 } from "hono";
623
+ var SAFE_PACKAGE_RE = /^[@a-zA-Z0-9._\/-]+$/;
624
+ var SAFE_ARG_RE = /^[a-zA-Z0-9._\/:=@-]+$/;
625
+ function createMcpRoutes(deps) {
626
+ const app = new Hono7();
627
+ app.get("/", (c) => {
628
+ const response = {
629
+ success: true,
630
+ data: deps.mcpServers
631
+ };
632
+ return c.json(response);
633
+ });
634
+ app.post("/", async (c) => {
635
+ try {
636
+ const body = await c.req.json();
637
+ if (!body.package && !body.url) {
638
+ return c.json(
639
+ { success: false, error: "Either 'package' or 'url' is required" },
640
+ 400
641
+ );
642
+ }
643
+ if (body.package && !SAFE_PACKAGE_RE.test(body.package)) {
644
+ return c.json(
645
+ {
646
+ success: false,
647
+ error: "Invalid package name \u2014 only alphanumeric, @, /, ., - allowed"
648
+ },
649
+ 400
650
+ );
651
+ }
652
+ if (body.args) {
653
+ for (const arg of body.args) {
654
+ if (!SAFE_ARG_RE.test(arg)) {
655
+ return c.json(
656
+ {
657
+ success: false,
658
+ error: `Invalid argument "${arg}" \u2014 only alphanumeric, ., /, :, =, @, - allowed`
659
+ },
660
+ 400
661
+ );
662
+ }
663
+ }
664
+ }
665
+ const raw = readRawConfig(deps.configPath);
666
+ if (!raw.mcp || typeof raw.mcp !== "object") raw.mcp = { servers: {} };
667
+ const mcp = raw.mcp;
668
+ if (!mcp.servers || typeof mcp.servers !== "object") mcp.servers = {};
669
+ const servers = mcp.servers;
670
+ const serverName = body.name || deriveServerName(body.package || body.url || "unknown");
671
+ if (servers[serverName]) {
672
+ return c.json(
673
+ { success: false, error: `Server "${serverName}" already exists` },
674
+ 409
675
+ );
676
+ }
677
+ const entry = {};
678
+ if (body.url) {
679
+ entry.url = body.url;
680
+ } else {
681
+ entry.command = "npx";
682
+ entry.args = ["-y", body.package, ...body.args || []];
683
+ }
684
+ if (body.scope && body.scope !== "always") entry.scope = body.scope;
685
+ if (body.env && Object.keys(body.env).length > 0) entry.env = body.env;
686
+ servers[serverName] = entry;
687
+ writeRawConfig(raw, deps.configPath);
688
+ return c.json({
689
+ success: true,
690
+ data: { name: serverName, message: "Server added. Restart teleton to connect." }
691
+ });
692
+ } catch (error) {
693
+ return c.json(
694
+ {
695
+ success: false,
696
+ error: error instanceof Error ? error.message : String(error)
697
+ },
698
+ 500
699
+ );
700
+ }
701
+ });
702
+ app.delete("/:name", (c) => {
703
+ try {
704
+ const name = c.req.param("name");
705
+ const raw = readRawConfig(deps.configPath);
706
+ const mcp = raw.mcp || {};
707
+ const servers = mcp.servers || {};
708
+ if (!servers[name]) {
709
+ return c.json(
710
+ { success: false, error: `Server "${name}" not found` },
711
+ 404
712
+ );
713
+ }
714
+ delete servers[name];
715
+ writeRawConfig(raw, deps.configPath);
716
+ return c.json({
717
+ success: true,
718
+ data: { name, message: "Server removed. Restart teleton to apply." }
719
+ });
720
+ } catch (error) {
721
+ return c.json(
722
+ {
723
+ success: false,
724
+ error: error instanceof Error ? error.message : String(error)
725
+ },
726
+ 500
727
+ );
728
+ }
729
+ });
730
+ return app;
731
+ }
732
+ function deriveServerName(pkg) {
733
+ const unscoped = pkg.includes("/") ? pkg.split("/").pop() : pkg;
734
+ return unscoped.replace(/^server-/, "").replace(/^mcp-server-/, "").replace(/^mcp-/, "");
735
+ }
736
+
737
+ // src/webui/routes/workspace.ts
738
+ import { Hono as Hono8 } from "hono";
556
739
  import {
557
740
  readFileSync as readFileSync2,
558
741
  writeFileSync as writeFileSync2,
@@ -614,7 +797,7 @@ function listDir(absPath, recursive) {
614
797
  return entries;
615
798
  }
616
799
  function createWorkspaceRoutes(_deps) {
617
- const app = new Hono7();
800
+ const app = new Hono8();
618
801
  app.get("/", (c) => {
619
802
  try {
620
803
  const subpath = c.req.query("path") || "";
@@ -778,10 +961,10 @@ function createWorkspaceRoutes(_deps) {
778
961
  }
779
962
 
780
963
  // src/webui/routes/tasks.ts
781
- import { Hono as Hono8 } from "hono";
964
+ import { Hono as Hono9 } from "hono";
782
965
  var VALID_STATUSES = ["pending", "in_progress", "done", "failed", "cancelled"];
783
966
  function createTasksRoutes(deps) {
784
- const app = new Hono8();
967
+ const app = new Hono9();
785
968
  function store() {
786
969
  return getTaskStore(deps.memory.db);
787
970
  }
@@ -889,23 +1072,616 @@ function createTasksRoutes(deps) {
889
1072
  return app;
890
1073
  }
891
1074
 
1075
+ // src/webui/routes/config.ts
1076
+ import { Hono as Hono10 } from "hono";
1077
+ function createConfigRoutes(deps) {
1078
+ const app = new Hono10();
1079
+ app.get("/", (c) => {
1080
+ try {
1081
+ const raw = readRawConfig(deps.configPath);
1082
+ const data = Object.entries(CONFIGURABLE_KEYS).map(([key, meta]) => {
1083
+ const value = getNestedValue(raw, key);
1084
+ const isSet = value != null && value !== "";
1085
+ return {
1086
+ key,
1087
+ set: isSet,
1088
+ value: isSet ? meta.mask(String(value)) : null,
1089
+ sensitive: meta.sensitive,
1090
+ type: meta.type,
1091
+ category: meta.category,
1092
+ description: meta.description,
1093
+ ...meta.options ? { options: meta.options } : {}
1094
+ };
1095
+ });
1096
+ const response = { success: true, data };
1097
+ return c.json(response);
1098
+ } catch (err) {
1099
+ return c.json(
1100
+ { success: false, error: err instanceof Error ? err.message : String(err) },
1101
+ 500
1102
+ );
1103
+ }
1104
+ });
1105
+ app.put("/:key", async (c) => {
1106
+ const key = c.req.param("key");
1107
+ const meta = CONFIGURABLE_KEYS[key];
1108
+ if (!meta) {
1109
+ const allowed = Object.keys(CONFIGURABLE_KEYS).join(", ");
1110
+ return c.json(
1111
+ {
1112
+ success: false,
1113
+ error: `Key "${key}" is not configurable. Allowed: ${allowed}`
1114
+ },
1115
+ 400
1116
+ );
1117
+ }
1118
+ let body;
1119
+ try {
1120
+ body = await c.req.json();
1121
+ } catch {
1122
+ return c.json({ success: false, error: "Invalid JSON body" }, 400);
1123
+ }
1124
+ const value = body.value;
1125
+ if (value == null || typeof value !== "string") {
1126
+ return c.json(
1127
+ { success: false, error: "Missing or invalid 'value' field" },
1128
+ 400
1129
+ );
1130
+ }
1131
+ const validationErr = meta.validate(value);
1132
+ if (validationErr) {
1133
+ return c.json(
1134
+ { success: false, error: `Invalid value for ${key}: ${validationErr}` },
1135
+ 400
1136
+ );
1137
+ }
1138
+ try {
1139
+ const parsed = meta.parse(value);
1140
+ const raw = readRawConfig(deps.configPath);
1141
+ setNestedValue(raw, key, parsed);
1142
+ writeRawConfig(raw, deps.configPath);
1143
+ const runtimeConfig = deps.agent.getConfig();
1144
+ setNestedValue(runtimeConfig, key, parsed);
1145
+ const result = {
1146
+ key,
1147
+ set: true,
1148
+ value: meta.mask(value),
1149
+ sensitive: meta.sensitive,
1150
+ type: meta.type,
1151
+ category: meta.category,
1152
+ description: meta.description,
1153
+ ...meta.options ? { options: meta.options } : {}
1154
+ };
1155
+ return c.json({ success: true, data: result });
1156
+ } catch (err) {
1157
+ return c.json(
1158
+ { success: false, error: err instanceof Error ? err.message : String(err) },
1159
+ 500
1160
+ );
1161
+ }
1162
+ });
1163
+ app.delete("/:key", (c) => {
1164
+ const key = c.req.param("key");
1165
+ const meta = CONFIGURABLE_KEYS[key];
1166
+ if (!meta) {
1167
+ const allowed = Object.keys(CONFIGURABLE_KEYS).join(", ");
1168
+ return c.json(
1169
+ {
1170
+ success: false,
1171
+ error: `Key "${key}" is not configurable. Allowed: ${allowed}`
1172
+ },
1173
+ 400
1174
+ );
1175
+ }
1176
+ try {
1177
+ const raw = readRawConfig(deps.configPath);
1178
+ deleteNestedValue(raw, key);
1179
+ writeRawConfig(raw, deps.configPath);
1180
+ const runtimeConfig = deps.agent.getConfig();
1181
+ deleteNestedValue(runtimeConfig, key);
1182
+ const result = {
1183
+ key,
1184
+ set: false,
1185
+ value: null,
1186
+ sensitive: meta.sensitive,
1187
+ type: meta.type,
1188
+ category: meta.category,
1189
+ description: meta.description,
1190
+ ...meta.options ? { options: meta.options } : {}
1191
+ };
1192
+ return c.json({ success: true, data: result });
1193
+ } catch (err) {
1194
+ return c.json(
1195
+ { success: false, error: err instanceof Error ? err.message : String(err) },
1196
+ 500
1197
+ );
1198
+ }
1199
+ });
1200
+ return app;
1201
+ }
1202
+
1203
+ // src/webui/routes/marketplace.ts
1204
+ import { Hono as Hono11 } from "hono";
1205
+
1206
+ // src/webui/services/marketplace.ts
1207
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
1208
+ import { join as join3, resolve } from "path";
1209
+ import { pathToFileURL } from "url";
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
+ console.warn("[marketplace] Registry fetch failed, using stale cache:", err);
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
+ console.error(`[marketplace] Failed to cleanup ${pluginDir}:`, cleanupErr);
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
893
1669
  function findWebDist() {
894
1670
  const candidates = [
895
- resolve("dist/web"),
1671
+ resolve2("dist/web"),
896
1672
  // npm start / teleton start (from project root)
897
- resolve("web")
1673
+ resolve2("web")
898
1674
  // fallback
899
1675
  ];
900
1676
  const __dirname = dirname(fileURLToPath(import.meta.url));
901
1677
  candidates.push(
902
- resolve(__dirname, "web"),
1678
+ resolve2(__dirname, "web"),
903
1679
  // dist/web when __dirname = dist/
904
- resolve(__dirname, "../dist/web")
1680
+ resolve2(__dirname, "../dist/web")
905
1681
  // when running with tsx from src/
906
1682
  );
907
1683
  for (const candidate of candidates) {
908
- if (existsSync2(join3(candidate, "index.html"))) {
1684
+ if (existsSync3(join4(candidate, "index.html"))) {
909
1685
  return candidate;
910
1686
  }
911
1687
  }
@@ -918,7 +1694,7 @@ var WebUIServer = class {
918
1694
  authToken;
919
1695
  constructor(deps) {
920
1696
  this.deps = deps;
921
- this.app = new Hono9();
1697
+ this.app = new Hono12();
922
1698
  this.authToken = deps.config.auth_token || generateToken();
923
1699
  this.setupMiddleware();
924
1700
  this.setupRoutes();
@@ -1023,11 +1799,14 @@ var WebUIServer = class {
1023
1799
  this.app.route("/api/memory", createMemoryRoutes(this.deps));
1024
1800
  this.app.route("/api/soul", createSoulRoutes(this.deps));
1025
1801
  this.app.route("/api/plugins", createPluginsRoutes(this.deps));
1802
+ this.app.route("/api/mcp", createMcpRoutes(this.deps));
1026
1803
  this.app.route("/api/workspace", createWorkspaceRoutes(this.deps));
1027
1804
  this.app.route("/api/tasks", createTasksRoutes(this.deps));
1805
+ this.app.route("/api/config", createConfigRoutes(this.deps));
1806
+ this.app.route("/api/marketplace", createMarketplaceRoutes(this.deps));
1028
1807
  const webDist = findWebDist();
1029
1808
  if (webDist) {
1030
- const indexHtml = readFileSync3(join3(webDist, "index.html"), "utf-8");
1809
+ const indexHtml = readFileSync3(join4(webDist, "index.html"), "utf-8");
1031
1810
  const mimeTypes = {
1032
1811
  js: "application/javascript",
1033
1812
  css: "text/css",
@@ -1041,9 +1820,9 @@ var WebUIServer = class {
1041
1820
  woff: "font/woff"
1042
1821
  };
1043
1822
  this.app.get("*", (c) => {
1044
- const filePath = resolve(join3(webDist, c.req.path));
1823
+ const filePath = resolve2(join4(webDist, c.req.path));
1045
1824
  const rel = relative2(webDist, filePath);
1046
- if (rel.startsWith("..") || resolve(filePath) !== filePath) {
1825
+ if (rel.startsWith("..") || resolve2(filePath) !== filePath) {
1047
1826
  return c.html(indexHtml);
1048
1827
  }
1049
1828
  try {
@@ -1073,7 +1852,7 @@ var WebUIServer = class {
1073
1852
  });
1074
1853
  }
1075
1854
  async start() {
1076
- return new Promise((resolve2, reject) => {
1855
+ return new Promise((resolve3, reject) => {
1077
1856
  try {
1078
1857
  logInterceptor.install();
1079
1858
  this.server = serve(
@@ -1091,7 +1870,7 @@ var WebUIServer = class {
1091
1870
  ` Token: ${maskToken(this.authToken)} (use Bearer header for API access)
1092
1871
  `
1093
1872
  );
1094
- resolve2();
1873
+ resolve3();
1095
1874
  }
1096
1875
  );
1097
1876
  } catch (error) {
@@ -1102,11 +1881,11 @@ var WebUIServer = class {
1102
1881
  }
1103
1882
  async stop() {
1104
1883
  if (this.server) {
1105
- return new Promise((resolve2) => {
1884
+ return new Promise((resolve3) => {
1106
1885
  this.server.close(() => {
1107
1886
  logInterceptor.uninstall();
1108
1887
  console.log("\u{1F310} WebUI server stopped");
1109
- resolve2();
1888
+ resolve3();
1110
1889
  });
1111
1890
  });
1112
1891
  }