@nextclaw/server 0.6.12 → 0.6.13

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 (3) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +2260 -2116
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -10,8 +10,80 @@ import { join } from "path";
10
10
 
11
11
  // src/ui/router.ts
12
12
  import { Hono } from "hono";
13
- import * as NextclawCore from "@nextclaw/core";
14
- import { buildPluginStatusReport } from "@nextclaw/openclaw-compat";
13
+
14
+ // src/ui/router/response.ts
15
+ function ok(data) {
16
+ return { ok: true, data };
17
+ }
18
+ function err(code, message, details) {
19
+ return { ok: false, error: { code, message, details } };
20
+ }
21
+ async function readJson(req) {
22
+ try {
23
+ const data = await req.json();
24
+ return { ok: true, data };
25
+ } catch {
26
+ return { ok: false };
27
+ }
28
+ }
29
+ function isRecord(value) {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+ function readErrorMessage(value, fallback) {
33
+ if (!isRecord(value)) {
34
+ return fallback;
35
+ }
36
+ const maybeError = value.error;
37
+ if (!isRecord(maybeError)) {
38
+ return fallback;
39
+ }
40
+ return typeof maybeError.message === "string" && maybeError.message.trim().length > 0 ? maybeError.message : fallback;
41
+ }
42
+ function readNonEmptyString(value) {
43
+ if (typeof value !== "string") {
44
+ return void 0;
45
+ }
46
+ const trimmed = value.trim();
47
+ return trimmed || void 0;
48
+ }
49
+ function formatUserFacingError(error, maxChars = 320) {
50
+ const raw = error instanceof Error ? error.message || error.name || "Unknown error" : String(error ?? "Unknown error");
51
+ const normalized = raw.replace(/\s+/g, " ").trim();
52
+ if (!normalized) {
53
+ return "Unknown error";
54
+ }
55
+ if (normalized.length <= maxChars) {
56
+ return normalized;
57
+ }
58
+ return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
59
+ }
60
+
61
+ // src/ui/router/app.controller.ts
62
+ function buildAppMetaView(options) {
63
+ const productVersion = options.productVersion?.trim();
64
+ return {
65
+ name: "NextClaw",
66
+ productVersion: productVersion && productVersion.length > 0 ? productVersion : "0.0.0"
67
+ };
68
+ }
69
+ var AppRoutesController = class {
70
+ constructor(options) {
71
+ this.options = options;
72
+ }
73
+ health = (c) => c.json(
74
+ ok({
75
+ status: "ok",
76
+ services: {
77
+ chatRuntime: this.options.chatRuntime ? "ready" : "unavailable",
78
+ cronService: this.options.cronService ? "ready" : "unavailable"
79
+ }
80
+ })
81
+ );
82
+ appMeta = (c) => c.json(ok(buildAppMetaView(this.options)));
83
+ };
84
+
85
+ // src/ui/router/chat.controller.ts
86
+ import * as NextclawCore2 from "@nextclaw/core";
15
87
 
16
88
  // src/ui/config.ts
17
89
  import {
@@ -1329,569 +1401,1761 @@ function updateSecrets(configPath, patch) {
1329
1401
  };
1330
1402
  }
1331
1403
 
1332
- // src/ui/provider-auth.ts
1333
- import { createHash, randomBytes, randomUUID } from "crypto";
1334
- import { readFile } from "fs/promises";
1335
- import { homedir } from "os";
1336
- import { isAbsolute, resolve } from "path";
1337
- import {
1338
- ConfigSchema as ConfigSchema2,
1339
- loadConfig as loadConfig2,
1340
- saveConfig as saveConfig2
1341
- } from "@nextclaw/core";
1342
- var authSessions = /* @__PURE__ */ new Map();
1343
- var DEFAULT_AUTH_INTERVAL_MS = 2e3;
1344
- var MAX_AUTH_INTERVAL_MS = 1e4;
1345
- function normalizePositiveInt(value, fallback) {
1346
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1347
- return fallback;
1348
- }
1349
- return Math.floor(value);
1404
+ // src/ui/router/chat-utils.ts
1405
+ import * as NextclawCore from "@nextclaw/core";
1406
+ function normalizeSessionType2(value) {
1407
+ return readNonEmptyString(value)?.toLowerCase();
1350
1408
  }
1351
- function normalizePositiveFloat(value) {
1352
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1353
- return null;
1409
+ function resolveSessionTypeLabel(sessionType) {
1410
+ if (sessionType === "native") {
1411
+ return "Native";
1354
1412
  }
1355
- return value;
1356
- }
1357
- function toBase64Url(buffer) {
1358
- return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1359
- }
1360
- function buildPkce() {
1361
- const verifier = toBase64Url(randomBytes(48));
1362
- const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
1363
- return { verifier, challenge };
1364
- }
1365
- function withTrailingSlash(value) {
1366
- return value.endsWith("/") ? value : `${value}/`;
1367
- }
1368
- function cleanupExpiredAuthSessions(now = Date.now()) {
1369
- for (const [sessionId, session] of authSessions.entries()) {
1370
- if (session.expiresAtMs <= now) {
1371
- authSessions.delete(sessionId);
1372
- }
1413
+ if (sessionType === "codex-sdk") {
1414
+ return "Codex";
1373
1415
  }
1374
- }
1375
- function resolveDeviceCodeEndpoints(baseUrl, deviceCodePath, tokenPath) {
1376
- const deviceCodeEndpoint = new URL(deviceCodePath, withTrailingSlash(baseUrl)).toString();
1377
- const tokenEndpoint = new URL(tokenPath, withTrailingSlash(baseUrl)).toString();
1378
- return { deviceCodeEndpoint, tokenEndpoint };
1379
- }
1380
- function resolveAuthNote(params) {
1381
- return params.zh ?? params.en;
1382
- }
1383
- function resolveLocalizedMethodLabel(method, fallbackId) {
1384
- return method.label?.zh ?? method.label?.en ?? fallbackId;
1385
- }
1386
- function resolveLocalizedMethodHint(method) {
1387
- return method.hint?.zh ?? method.hint?.en;
1388
- }
1389
- function normalizeMethodId(value) {
1390
- if (typeof value !== "string") {
1391
- return void 0;
1416
+ if (sessionType === "claude-agent-sdk") {
1417
+ return "Claude Code";
1392
1418
  }
1393
- const trimmed = value.trim();
1394
- return trimmed.length > 0 ? trimmed : void 0;
1419
+ return sessionType;
1395
1420
  }
1396
- function resolveAuthMethod(auth, requestedMethodId) {
1397
- const protocol = auth.protocol ?? "rfc8628";
1398
- const methods = (auth.methods ?? []).filter((entry) => normalizeMethodId(entry.id));
1399
- const cleanRequestedMethodId = normalizeMethodId(requestedMethodId);
1400
- if (methods.length === 0) {
1401
- if (cleanRequestedMethodId) {
1402
- throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
1403
- }
1421
+ async function buildChatSessionTypesView(chatRuntime) {
1422
+ if (!chatRuntime?.listSessionTypes) {
1404
1423
  return {
1405
- protocol,
1406
- baseUrl: auth.baseUrl,
1407
- deviceCodePath: auth.deviceCodePath,
1408
- tokenPath: auth.tokenPath,
1409
- clientId: auth.clientId,
1410
- scope: auth.scope,
1411
- grantType: auth.grantType,
1412
- usePkce: Boolean(auth.usePkce)
1424
+ defaultType: DEFAULT_SESSION_TYPE,
1425
+ options: [{ value: DEFAULT_SESSION_TYPE, label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE) }]
1413
1426
  };
1414
1427
  }
1415
- let selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === cleanRequestedMethodId);
1416
- if (!selectedMethod) {
1417
- const fallbackMethodId = normalizeMethodId(auth.defaultMethodId) ?? normalizeMethodId(methods[0]?.id);
1418
- selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === fallbackMethodId) ?? methods[0];
1428
+ const payload = await chatRuntime.listSessionTypes();
1429
+ const deduped = /* @__PURE__ */ new Map();
1430
+ for (const rawOption of payload.options ?? []) {
1431
+ const normalized = normalizeSessionType2(rawOption.value);
1432
+ if (!normalized) {
1433
+ continue;
1434
+ }
1435
+ deduped.set(normalized, {
1436
+ value: normalized,
1437
+ label: readNonEmptyString(rawOption.label) ?? resolveSessionTypeLabel(normalized)
1438
+ });
1419
1439
  }
1420
- const methodId = normalizeMethodId(selectedMethod?.id);
1421
- if (!selectedMethod || !methodId) {
1422
- throw new Error("provider auth method is not configured");
1440
+ if (!deduped.has(DEFAULT_SESSION_TYPE)) {
1441
+ deduped.set(DEFAULT_SESSION_TYPE, {
1442
+ value: DEFAULT_SESSION_TYPE,
1443
+ label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE)
1444
+ });
1423
1445
  }
1424
- if (cleanRequestedMethodId && methodId !== cleanRequestedMethodId) {
1425
- throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
1446
+ const defaultType = normalizeSessionType2(payload.defaultType) ?? DEFAULT_SESSION_TYPE;
1447
+ if (!deduped.has(defaultType)) {
1448
+ deduped.set(defaultType, {
1449
+ value: defaultType,
1450
+ label: resolveSessionTypeLabel(defaultType)
1451
+ });
1426
1452
  }
1453
+ const options = Array.from(deduped.values()).sort((left, right) => {
1454
+ if (left.value === DEFAULT_SESSION_TYPE) {
1455
+ return -1;
1456
+ }
1457
+ if (right.value === DEFAULT_SESSION_TYPE) {
1458
+ return 1;
1459
+ }
1460
+ return left.value.localeCompare(right.value);
1461
+ });
1427
1462
  return {
1428
- id: methodId,
1429
- protocol,
1430
- baseUrl: selectedMethod.baseUrl ?? auth.baseUrl,
1431
- deviceCodePath: selectedMethod.deviceCodePath ?? auth.deviceCodePath,
1432
- tokenPath: selectedMethod.tokenPath ?? auth.tokenPath,
1433
- clientId: selectedMethod.clientId ?? auth.clientId,
1434
- scope: selectedMethod.scope ?? auth.scope,
1435
- grantType: selectedMethod.grantType ?? auth.grantType,
1436
- usePkce: selectedMethod.usePkce ?? Boolean(auth.usePkce),
1437
- defaultApiBase: selectedMethod.defaultApiBase
1463
+ defaultType,
1464
+ options
1438
1465
  };
1439
1466
  }
1440
- function parseExpiresAtMs(value, fallbackFromNowMs) {
1441
- const normalized = normalizePositiveFloat(value);
1442
- if (normalized === null) {
1443
- return Date.now() + fallbackFromNowMs;
1444
- }
1445
- if (normalized >= 1e12) {
1446
- return Math.floor(normalized);
1447
- }
1448
- if (normalized >= 1e9) {
1449
- return Math.floor(normalized * 1e3);
1450
- }
1451
- return Date.now() + Math.floor(normalized * 1e3);
1467
+ function resolveAgentIdFromSessionKey(sessionKey) {
1468
+ const parsed = NextclawCore.parseAgentScopedSessionKey(sessionKey);
1469
+ const agentId = readNonEmptyString(parsed?.agentId);
1470
+ return agentId;
1452
1471
  }
1453
- function parsePollIntervalMs(value, fallbackMs) {
1454
- const normalized = normalizePositiveFloat(value);
1455
- if (normalized === null) {
1456
- return fallbackMs;
1457
- }
1458
- if (normalized <= 30) {
1459
- return Math.floor(normalized * 1e3);
1460
- }
1461
- return Math.floor(normalized);
1472
+ function createChatRunId() {
1473
+ const now = Date.now().toString(36);
1474
+ const rand = Math.random().toString(36).slice(2, 10);
1475
+ return `run-${now}-${rand}`;
1462
1476
  }
1463
- function buildMinimaxErrorMessage(payload, fallback) {
1464
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1465
- return fallback;
1466
- }
1467
- const record = payload;
1468
- if (typeof record.error_description === "string" && record.error_description.trim()) {
1469
- return record.error_description.trim();
1470
- }
1471
- if (typeof record.error === "string" && record.error.trim()) {
1472
- return record.error.trim();
1477
+ function isChatRunState(value) {
1478
+ return value === "queued" || value === "running" || value === "completed" || value === "failed" || value === "aborted";
1479
+ }
1480
+ function readChatRunStates(value) {
1481
+ if (typeof value !== "string") {
1482
+ return void 0;
1473
1483
  }
1474
- const baseMessage = record.base_resp?.status_msg;
1475
- if (typeof baseMessage === "string" && baseMessage.trim()) {
1476
- return baseMessage.trim();
1484
+ const values = value.split(",").map((item) => item.trim().toLowerCase()).filter((item) => Boolean(item) && isChatRunState(item));
1485
+ if (values.length === 0) {
1486
+ return void 0;
1477
1487
  }
1478
- return fallback;
1488
+ return Array.from(new Set(values));
1479
1489
  }
1480
- function classifyMiniMaxErrorStatus(message) {
1481
- const normalized = message.toLowerCase();
1482
- if (normalized.includes("deny") || normalized.includes("rejected")) {
1483
- return "denied";
1484
- }
1485
- if (normalized.includes("expired") || normalized.includes("timeout") || normalized.includes("timed out")) {
1486
- return "expired";
1487
- }
1488
- return "error";
1490
+ function buildChatTurnView(params) {
1491
+ const completedAt = /* @__PURE__ */ new Date();
1492
+ return {
1493
+ reply: String(params.result.reply ?? ""),
1494
+ sessionKey: readNonEmptyString(params.result.sessionKey) ?? params.fallbackSessionKey,
1495
+ ...readNonEmptyString(params.result.agentId) || params.requestedAgentId ? { agentId: readNonEmptyString(params.result.agentId) ?? params.requestedAgentId } : {},
1496
+ ...readNonEmptyString(params.result.model) || params.requestedModel ? { model: readNonEmptyString(params.result.model) ?? params.requestedModel } : {},
1497
+ requestedAt: params.requestedAt.toISOString(),
1498
+ completedAt: completedAt.toISOString(),
1499
+ durationMs: Math.max(0, completedAt.getTime() - params.startedAtMs)
1500
+ };
1489
1501
  }
1490
- function resolveHomePath(inputPath) {
1491
- const trimmed = inputPath.trim();
1492
- if (!trimmed) {
1493
- return trimmed;
1494
- }
1495
- if (trimmed === "~") {
1496
- return homedir();
1497
- }
1498
- if (trimmed.startsWith("~/")) {
1499
- return resolve(homedir(), trimmed.slice(2));
1500
- }
1501
- if (isAbsolute(trimmed)) {
1502
- return trimmed;
1503
- }
1504
- return resolve(trimmed);
1502
+ function buildChatTurnViewFromRun(params) {
1503
+ const requestedAt = readNonEmptyString(params.run.requestedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
1504
+ const completedAt = readNonEmptyString(params.run.completedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
1505
+ const requestedAtMs = Date.parse(requestedAt);
1506
+ const completedAtMs = Date.parse(completedAt);
1507
+ return {
1508
+ reply: readNonEmptyString(params.run.reply) ?? params.fallbackReply ?? "",
1509
+ sessionKey: readNonEmptyString(params.run.sessionKey) ?? params.fallbackSessionKey,
1510
+ ...readNonEmptyString(params.run.agentId) || params.fallbackAgentId ? { agentId: readNonEmptyString(params.run.agentId) ?? params.fallbackAgentId } : {},
1511
+ ...readNonEmptyString(params.run.model) || params.fallbackModel ? { model: readNonEmptyString(params.run.model) ?? params.fallbackModel } : {},
1512
+ requestedAt,
1513
+ completedAt,
1514
+ durationMs: Number.isFinite(requestedAtMs) && Number.isFinite(completedAtMs) ? Math.max(0, completedAtMs - requestedAtMs) : 0
1515
+ };
1505
1516
  }
1506
- function normalizeExpiresAt(value) {
1507
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
1508
- return Math.floor(value);
1509
- }
1510
- if (typeof value === "string" && value.trim()) {
1511
- const asNumber = Number(value);
1512
- if (Number.isFinite(asNumber) && asNumber > 0) {
1513
- return Math.floor(asNumber);
1514
- }
1515
- const parsedTime = Date.parse(value);
1516
- if (Number.isFinite(parsedTime) && parsedTime > 0) {
1517
- return parsedTime;
1518
- }
1519
- }
1520
- return null;
1517
+ function toSseFrame(event, data) {
1518
+ return `event: ${event}
1519
+ data: ${JSON.stringify(data)}
1520
+
1521
+ `;
1521
1522
  }
1522
- function readFieldAsString(source, fieldName) {
1523
- if (!fieldName) {
1524
- return null;
1525
- }
1526
- const rawValue = source[fieldName];
1527
- if (typeof rawValue !== "string") {
1528
- return null;
1523
+
1524
+ // src/ui/router/chat.controller.ts
1525
+ var ChatRoutesController = class {
1526
+ constructor(options) {
1527
+ this.options = options;
1529
1528
  }
1530
- const trimmed = rawValue.trim();
1531
- return trimmed.length > 0 ? trimmed : null;
1532
- }
1533
- function setProviderApiKey(params) {
1534
- const config = loadConfig2(params.configPath);
1535
- const providers = config.providers;
1536
- if (!providers[params.provider]) {
1537
- providers[params.provider] = {
1538
- displayName: "",
1539
- apiKey: "",
1540
- apiBase: null,
1541
- extraHeaders: null,
1542
- wireApi: "auto",
1543
- models: [],
1544
- modelThinking: {}
1529
+ getCapabilities = async (c) => {
1530
+ const chatRuntime = this.options.chatRuntime;
1531
+ if (!chatRuntime) {
1532
+ return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
1533
+ }
1534
+ const query = c.req.query();
1535
+ const params = {
1536
+ sessionKey: readNonEmptyString(query.sessionKey),
1537
+ agentId: readNonEmptyString(query.agentId)
1545
1538
  };
1546
- }
1547
- const target = providers[params.provider];
1548
- target.apiKey = params.accessToken;
1549
- if (!target.apiBase && params.defaultApiBase) {
1550
- target.apiBase = params.defaultApiBase;
1551
- }
1552
- const next = ConfigSchema2.parse(config);
1553
- saveConfig2(next, params.configPath);
1554
- }
1555
- async function startProviderAuth(configPath, providerName, options) {
1556
- cleanupExpiredAuthSessions();
1557
- const spec = findServerBuiltinProviderByName(providerName);
1558
- if (!spec?.auth || spec.auth.kind !== "device_code") {
1559
- return null;
1560
- }
1561
- const resolvedMethod = resolveAuthMethod(spec.auth, options?.methodId);
1562
- const { deviceCodeEndpoint, tokenEndpoint } = resolveDeviceCodeEndpoints(
1563
- resolvedMethod.baseUrl,
1564
- resolvedMethod.deviceCodePath,
1565
- resolvedMethod.tokenPath
1566
- );
1567
- const pkce = resolvedMethod.usePkce ? buildPkce() : null;
1568
- let authorizationCode = "";
1569
- let tokenCodeField = "device_code";
1570
- let userCode = "";
1571
- let verificationUri = "";
1572
- let intervalMs = DEFAULT_AUTH_INTERVAL_MS;
1573
- let expiresAtMs = Date.now() + 6e5;
1574
- if (resolvedMethod.protocol === "minimax_user_code") {
1575
- if (!pkce) {
1576
- throw new Error("MiniMax OAuth requires PKCE");
1539
+ try {
1540
+ const capabilities = chatRuntime.getCapabilities ? await chatRuntime.getCapabilities(params) : { stopSupported: Boolean(chatRuntime.stopTurn) };
1541
+ return c.json(ok(capabilities));
1542
+ } catch (error) {
1543
+ return c.json(err("CHAT_RUNTIME_FAILED", String(error)), 500);
1577
1544
  }
1578
- const state = toBase64Url(randomBytes(16));
1579
- const body = new URLSearchParams({
1580
- response_type: "code",
1581
- client_id: resolvedMethod.clientId,
1582
- scope: resolvedMethod.scope,
1583
- code_challenge: pkce.challenge,
1584
- code_challenge_method: "S256",
1585
- state
1586
- });
1587
- const response = await fetch(deviceCodeEndpoint, {
1588
- method: "POST",
1589
- headers: {
1590
- "Content-Type": "application/x-www-form-urlencoded",
1591
- Accept: "application/json",
1592
- "x-request-id": randomUUID()
1593
- },
1594
- body
1595
- });
1596
- const payload = await response.json().catch(() => ({}));
1597
- if (!response.ok) {
1598
- throw new Error(buildMinimaxErrorMessage(payload, response.statusText || "MiniMax OAuth start failed"));
1545
+ };
1546
+ getSessionTypes = async (c) => {
1547
+ try {
1548
+ const payload = await buildChatSessionTypesView(this.options.chatRuntime);
1549
+ return c.json(ok(payload));
1550
+ } catch (error) {
1551
+ return c.json(err("CHAT_SESSION_TYPES_FAILED", String(error)), 500);
1599
1552
  }
1600
- if (payload.state && payload.state !== state) {
1601
- throw new Error("MiniMax OAuth state mismatch");
1553
+ };
1554
+ getCommands = async (c) => {
1555
+ try {
1556
+ const config = loadConfigOrDefault(this.options.configPath);
1557
+ const registry = new NextclawCore2.CommandRegistry(config);
1558
+ const commands = registry.listSlashCommands().map((command) => ({
1559
+ name: command.name,
1560
+ description: command.description,
1561
+ ...Array.isArray(command.options) && command.options.length > 0 ? {
1562
+ options: command.options.map((option) => ({
1563
+ name: option.name,
1564
+ description: option.description,
1565
+ type: option.type,
1566
+ ...option.required === true ? { required: true } : {}
1567
+ }))
1568
+ } : {}
1569
+ }));
1570
+ const payload = {
1571
+ commands,
1572
+ total: commands.length
1573
+ };
1574
+ return c.json(ok(payload));
1575
+ } catch (error) {
1576
+ return c.json(err("CHAT_COMMANDS_FAILED", String(error)), 500);
1602
1577
  }
1603
- authorizationCode = payload.user_code?.trim() ?? "";
1604
- userCode = authorizationCode;
1605
- verificationUri = payload.verification_uri?.trim() ?? "";
1606
- if (!authorizationCode || !verificationUri) {
1607
- throw new Error("provider auth payload is incomplete");
1578
+ };
1579
+ processTurn = async (c) => {
1580
+ if (!this.options.chatRuntime) {
1581
+ return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
1608
1582
  }
1609
- tokenCodeField = "user_code";
1610
- intervalMs = Math.min(parsePollIntervalMs(payload.interval, DEFAULT_AUTH_INTERVAL_MS), MAX_AUTH_INTERVAL_MS);
1611
- expiresAtMs = parseExpiresAtMs(payload.expired_in, 6e5);
1612
- } else {
1613
- const body = new URLSearchParams({
1614
- client_id: resolvedMethod.clientId,
1615
- scope: resolvedMethod.scope
1616
- });
1617
- if (pkce) {
1618
- body.set("code_challenge", pkce.challenge);
1619
- body.set("code_challenge_method", "S256");
1583
+ const body = await readJson(c.req.raw);
1584
+ if (!body.ok) {
1585
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
1620
1586
  }
1621
- const response = await fetch(deviceCodeEndpoint, {
1622
- method: "POST",
1623
- headers: {
1624
- "Content-Type": "application/x-www-form-urlencoded",
1625
- Accept: "application/json"
1626
- },
1627
- body
1628
- });
1629
- const payload = await response.json().catch(() => ({}));
1630
- if (!response.ok) {
1631
- const message = payload.error_description || payload.error || response.statusText || "device code auth failed";
1632
- throw new Error(message);
1587
+ const message = readNonEmptyString(body.data.message);
1588
+ if (!message) {
1589
+ return c.json(err("INVALID_BODY", "message is required"), 400);
1633
1590
  }
1634
- authorizationCode = payload.device_code?.trim() ?? "";
1635
- userCode = payload.user_code?.trim() ?? "";
1636
- verificationUri = payload.verification_uri_complete?.trim() || payload.verification_uri?.trim() || "";
1637
- if (!authorizationCode || !userCode || !verificationUri) {
1638
- throw new Error("provider auth payload is incomplete");
1591
+ const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
1592
+ const requestedAt = /* @__PURE__ */ new Date();
1593
+ const startedAtMs = requestedAt.getTime();
1594
+ const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
1595
+ const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
1596
+ const requestedModel = readNonEmptyString(body.data.model);
1597
+ const request = {
1598
+ message,
1599
+ sessionKey,
1600
+ channel: readNonEmptyString(body.data.channel) ?? "ui",
1601
+ chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
1602
+ ...requestedAgentId ? { agentId: requestedAgentId } : {},
1603
+ ...requestedModel ? { model: requestedModel } : {},
1604
+ ...metadata ? { metadata } : {}
1605
+ };
1606
+ try {
1607
+ const result = await this.options.chatRuntime.processTurn(request);
1608
+ const response = buildChatTurnView({
1609
+ result,
1610
+ fallbackSessionKey: sessionKey,
1611
+ requestedAgentId,
1612
+ requestedModel,
1613
+ requestedAt,
1614
+ startedAtMs
1615
+ });
1616
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
1617
+ return c.json(ok(response));
1618
+ } catch (error) {
1619
+ return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
1639
1620
  }
1640
- intervalMs = normalizePositiveInt(payload.interval, DEFAULT_AUTH_INTERVAL_MS / 1e3) * 1e3;
1641
- const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
1642
- expiresAtMs = Date.now() + expiresInSec * 1e3;
1643
- }
1644
- const sessionId = randomUUID();
1645
- authSessions.set(sessionId, {
1646
- sessionId,
1647
- provider: providerName,
1648
- configPath,
1649
- authorizationCode,
1650
- tokenCodeField,
1651
- protocol: resolvedMethod.protocol,
1652
- methodId: resolvedMethod.id,
1653
- codeVerifier: pkce?.verifier,
1654
- tokenEndpoint,
1655
- clientId: resolvedMethod.clientId,
1656
- grantType: resolvedMethod.grantType,
1657
- defaultApiBase: resolvedMethod.defaultApiBase ?? spec.defaultApiBase,
1658
- expiresAtMs,
1659
- intervalMs
1660
- });
1661
- const methodConfig = (spec.auth.methods ?? []).find((entry) => normalizeMethodId(entry.id) === resolvedMethod.id);
1662
- const methodLabel = methodConfig ? resolveLocalizedMethodLabel(methodConfig, resolvedMethod.id ?? "") : void 0;
1663
- const methodHint = methodConfig ? resolveLocalizedMethodHint(methodConfig) : void 0;
1664
- return {
1665
- provider: providerName,
1666
- kind: "device_code",
1667
- methodId: resolvedMethod.id,
1668
- sessionId,
1669
- verificationUri,
1670
- userCode,
1671
- expiresAt: new Date(expiresAtMs).toISOString(),
1672
- intervalMs,
1673
- note: methodHint ?? methodLabel ?? resolveAuthNote(spec.auth.note ?? {})
1674
1621
  };
1675
- }
1676
- async function pollProviderAuth(params) {
1677
- cleanupExpiredAuthSessions();
1678
- const session = authSessions.get(params.sessionId);
1679
- if (!session || session.provider !== params.providerName || session.configPath !== params.configPath) {
1680
- return null;
1681
- }
1682
- if (Date.now() >= session.expiresAtMs) {
1683
- authSessions.delete(params.sessionId);
1684
- return {
1685
- provider: params.providerName,
1686
- status: "expired",
1687
- message: "authorization session expired"
1622
+ stopTurn = async (c) => {
1623
+ const chatRuntime = this.options.chatRuntime;
1624
+ if (!chatRuntime?.stopTurn) {
1625
+ return c.json(err("NOT_AVAILABLE", "chat turn stop is not supported by runtime"), 503);
1626
+ }
1627
+ const body = await readJson(c.req.raw);
1628
+ if (!body.ok || !body.data || typeof body.data !== "object") {
1629
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
1630
+ }
1631
+ const runId = readNonEmptyString(body.data.runId);
1632
+ if (!runId) {
1633
+ return c.json(err("INVALID_BODY", "runId is required"), 400);
1634
+ }
1635
+ const request = {
1636
+ runId,
1637
+ ...readNonEmptyString(body.data.sessionKey) ? { sessionKey: readNonEmptyString(body.data.sessionKey) } : {},
1638
+ ...readNonEmptyString(body.data.agentId) ? { agentId: readNonEmptyString(body.data.agentId) } : {}
1688
1639
  };
1689
- }
1690
- const body = new URLSearchParams({
1691
- grant_type: session.grantType,
1692
- client_id: session.clientId
1693
- });
1694
- body.set(session.tokenCodeField, session.authorizationCode);
1695
- if (session.codeVerifier) {
1696
- body.set("code_verifier", session.codeVerifier);
1697
- }
1698
- const response = await fetch(session.tokenEndpoint, {
1699
- method: "POST",
1700
- headers: {
1701
- "Content-Type": "application/x-www-form-urlencoded",
1702
- Accept: "application/json"
1703
- },
1704
- body
1705
- });
1706
- let accessToken = "";
1707
- if (session.protocol === "minimax_user_code") {
1708
- const raw = await response.text();
1709
- let payload = {};
1710
- if (raw) {
1711
- try {
1712
- payload = JSON.parse(raw);
1713
- } catch {
1714
- payload = {};
1715
- }
1640
+ try {
1641
+ const result = await chatRuntime.stopTurn(request);
1642
+ return c.json(ok(result));
1643
+ } catch (error) {
1644
+ return c.json(err("CHAT_TURN_STOP_FAILED", String(error)), 500);
1716
1645
  }
1717
- if (!response.ok) {
1718
- const message = buildMinimaxErrorMessage(payload, raw || response.statusText || "authorization failed");
1719
- return {
1720
- provider: params.providerName,
1721
- status: "error",
1722
- message
1723
- };
1646
+ };
1647
+ streamTurn = async (c) => {
1648
+ const chatRuntime = this.options.chatRuntime;
1649
+ if (!chatRuntime) {
1650
+ return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
1724
1651
  }
1725
- const status = payload.status?.trim().toLowerCase();
1726
- if (status === "success") {
1727
- accessToken = payload.access_token?.trim() ?? "";
1728
- if (!accessToken) {
1729
- return {
1730
- provider: params.providerName,
1731
- status: "error",
1732
- message: "provider token response missing access token"
1733
- };
1734
- }
1735
- } else if (status === "error") {
1736
- const message = buildMinimaxErrorMessage(payload, "authorization failed");
1737
- const classified = classifyMiniMaxErrorStatus(message);
1738
- if (classified === "denied" || classified === "expired") {
1739
- authSessions.delete(params.sessionId);
1740
- }
1741
- return {
1742
- provider: params.providerName,
1743
- status: classified,
1744
- message
1745
- };
1746
- } else {
1747
- const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
1748
- session.intervalMs = nextPollMs;
1749
- authSessions.set(params.sessionId, session);
1750
- return {
1751
- provider: params.providerName,
1752
- status: "pending",
1753
- nextPollMs
1754
- };
1652
+ const body = await readJson(c.req.raw);
1653
+ if (!body.ok) {
1654
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
1755
1655
  }
1756
- } else {
1757
- const payload = await response.json().catch(() => ({}));
1758
- if (!response.ok) {
1759
- const errorCode = payload.error?.trim().toLowerCase();
1760
- if (errorCode === "authorization_pending") {
1761
- return {
1762
- provider: params.providerName,
1763
- status: "pending",
1764
- nextPollMs: session.intervalMs
1765
- };
1766
- }
1767
- if (errorCode === "slow_down") {
1768
- const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
1769
- session.intervalMs = nextPollMs;
1770
- authSessions.set(params.sessionId, session);
1771
- return {
1772
- provider: params.providerName,
1773
- status: "pending",
1774
- nextPollMs
1656
+ const message = readNonEmptyString(body.data.message);
1657
+ if (!message) {
1658
+ return c.json(err("INVALID_BODY", "message is required"), 400);
1659
+ }
1660
+ const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
1661
+ const requestedAt = /* @__PURE__ */ new Date();
1662
+ const startedAtMs = requestedAt.getTime();
1663
+ const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
1664
+ const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
1665
+ const requestedModel = readNonEmptyString(body.data.model);
1666
+ let runId = createChatRunId();
1667
+ const supportsManagedRuns = Boolean(chatRuntime.startTurnRun && chatRuntime.streamRun);
1668
+ let stopCapabilities = { stopSupported: Boolean(chatRuntime.stopTurn) };
1669
+ if (chatRuntime.getCapabilities) {
1670
+ try {
1671
+ stopCapabilities = await chatRuntime.getCapabilities({
1672
+ sessionKey,
1673
+ ...requestedAgentId ? { agentId: requestedAgentId } : {}
1674
+ });
1675
+ } catch {
1676
+ stopCapabilities = {
1677
+ stopSupported: false,
1678
+ stopReason: "failed to resolve runtime stop capability"
1775
1679
  };
1776
1680
  }
1777
- if (errorCode === "access_denied") {
1778
- authSessions.delete(params.sessionId);
1779
- return {
1780
- provider: params.providerName,
1781
- status: "denied",
1782
- message: payload.error_description || "authorization denied"
1783
- };
1681
+ }
1682
+ const request = {
1683
+ message,
1684
+ sessionKey,
1685
+ channel: readNonEmptyString(body.data.channel) ?? "ui",
1686
+ chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
1687
+ runId,
1688
+ ...requestedAgentId ? { agentId: requestedAgentId } : {},
1689
+ ...requestedModel ? { model: requestedModel } : {},
1690
+ ...metadata ? { metadata } : {}
1691
+ };
1692
+ let managedRun = null;
1693
+ if (supportsManagedRuns && chatRuntime.startTurnRun) {
1694
+ try {
1695
+ managedRun = await chatRuntime.startTurnRun(request);
1696
+ } catch (error) {
1697
+ return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
1784
1698
  }
1785
- if (errorCode === "expired_token") {
1786
- authSessions.delete(params.sessionId);
1787
- return {
1788
- provider: params.providerName,
1789
- status: "expired",
1790
- message: payload.error_description || "authorization session expired"
1791
- };
1699
+ if (readNonEmptyString(managedRun.runId)) {
1700
+ runId = readNonEmptyString(managedRun.runId);
1792
1701
  }
1793
- return {
1794
- provider: params.providerName,
1795
- status: "error",
1796
- message: payload.error_description || payload.error || response.statusText || "authorization failed"
1797
- };
1798
- }
1799
- accessToken = payload.access_token?.trim() ?? "";
1800
- if (!accessToken) {
1801
- return {
1802
- provider: params.providerName,
1803
- status: "error",
1804
- message: "provider token response missing access token"
1702
+ stopCapabilities = {
1703
+ stopSupported: managedRun.stopSupported,
1704
+ ...readNonEmptyString(managedRun.stopReason) ? { stopReason: readNonEmptyString(managedRun.stopReason) } : {}
1805
1705
  };
1806
1706
  }
1807
- }
1808
- setProviderApiKey({
1809
- configPath: params.configPath,
1810
- provider: params.providerName,
1811
- accessToken,
1812
- defaultApiBase: session.defaultApiBase
1813
- });
1814
- authSessions.delete(params.sessionId);
1815
- return {
1816
- provider: params.providerName,
1817
- status: "authorized"
1818
- };
1819
- }
1820
- async function importProviderAuthFromCli(configPath, providerName) {
1821
- const spec = findServerBuiltinProviderByName(providerName);
1822
- if (!spec?.auth || spec.auth.kind !== "device_code" || !spec.auth.cliCredential) {
1823
- return null;
1824
- }
1825
- const credentialPath = resolveHomePath(spec.auth.cliCredential.path);
1826
- if (!credentialPath) {
1827
- throw new Error("provider cli credential path is empty");
1828
- }
1829
- let rawContent = "";
1830
- try {
1831
- rawContent = await readFile(credentialPath, "utf8");
1832
- } catch (error) {
1833
- const message = error instanceof Error ? error.message : String(error);
1834
- throw new Error(`failed to read CLI credential: ${message}`);
1835
- }
1836
- let payload;
1837
- try {
1838
- const parsed = JSON.parse(rawContent);
1707
+ const encoder = new TextEncoder();
1708
+ const stream = new ReadableStream({
1709
+ start: async (controller) => {
1710
+ const push = (event, data) => {
1711
+ controller.enqueue(encoder.encode(toSseFrame(event, data)));
1712
+ };
1713
+ try {
1714
+ push("ready", {
1715
+ sessionKey: managedRun?.sessionKey ?? sessionKey,
1716
+ requestedAt: managedRun?.requestedAt ?? requestedAt.toISOString(),
1717
+ runId,
1718
+ stopSupported: stopCapabilities.stopSupported,
1719
+ ...readNonEmptyString(stopCapabilities.stopReason) ? { stopReason: readNonEmptyString(stopCapabilities.stopReason) } : {}
1720
+ });
1721
+ if (supportsManagedRuns && chatRuntime.streamRun) {
1722
+ let hasFinal2 = false;
1723
+ for await (const event of chatRuntime.streamRun({ runId })) {
1724
+ const typed = event;
1725
+ if (typed.type === "delta") {
1726
+ if (typed.delta) {
1727
+ push("delta", { delta: typed.delta });
1728
+ }
1729
+ continue;
1730
+ }
1731
+ if (typed.type === "session_event") {
1732
+ push("session_event", typed.event);
1733
+ continue;
1734
+ }
1735
+ if (typed.type === "final") {
1736
+ const latestRun = chatRuntime.getRun ? await chatRuntime.getRun({ runId }) : null;
1737
+ const response = latestRun ? buildChatTurnViewFromRun({
1738
+ run: latestRun,
1739
+ fallbackSessionKey: sessionKey,
1740
+ fallbackAgentId: requestedAgentId,
1741
+ fallbackModel: requestedModel,
1742
+ fallbackReply: typed.result.reply
1743
+ }) : buildChatTurnView({
1744
+ result: typed.result,
1745
+ fallbackSessionKey: sessionKey,
1746
+ requestedAgentId,
1747
+ requestedModel,
1748
+ requestedAt,
1749
+ startedAtMs
1750
+ });
1751
+ hasFinal2 = true;
1752
+ push("final", response);
1753
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
1754
+ continue;
1755
+ }
1756
+ if (typed.type === "error") {
1757
+ push("error", {
1758
+ code: "CHAT_TURN_FAILED",
1759
+ message: formatUserFacingError(typed.error)
1760
+ });
1761
+ return;
1762
+ }
1763
+ }
1764
+ if (!hasFinal2) {
1765
+ push("error", {
1766
+ code: "CHAT_TURN_FAILED",
1767
+ message: "stream ended without a final result"
1768
+ });
1769
+ return;
1770
+ }
1771
+ push("done", { ok: true });
1772
+ return;
1773
+ }
1774
+ const streamTurn = chatRuntime.processTurnStream;
1775
+ if (!streamTurn) {
1776
+ const result = await chatRuntime.processTurn(request);
1777
+ const response = buildChatTurnView({
1778
+ result,
1779
+ fallbackSessionKey: sessionKey,
1780
+ requestedAgentId,
1781
+ requestedModel,
1782
+ requestedAt,
1783
+ startedAtMs
1784
+ });
1785
+ push("final", response);
1786
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
1787
+ push("done", { ok: true });
1788
+ return;
1789
+ }
1790
+ let hasFinal = false;
1791
+ for await (const event of streamTurn(request)) {
1792
+ const typed = event;
1793
+ if (typed.type === "delta") {
1794
+ if (typed.delta) {
1795
+ push("delta", { delta: typed.delta });
1796
+ }
1797
+ continue;
1798
+ }
1799
+ if (typed.type === "session_event") {
1800
+ push("session_event", typed.event);
1801
+ continue;
1802
+ }
1803
+ if (typed.type === "final") {
1804
+ const response = buildChatTurnView({
1805
+ result: typed.result,
1806
+ fallbackSessionKey: sessionKey,
1807
+ requestedAgentId,
1808
+ requestedModel,
1809
+ requestedAt,
1810
+ startedAtMs
1811
+ });
1812
+ hasFinal = true;
1813
+ push("final", response);
1814
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
1815
+ continue;
1816
+ }
1817
+ if (typed.type === "error") {
1818
+ push("error", {
1819
+ code: "CHAT_TURN_FAILED",
1820
+ message: formatUserFacingError(typed.error)
1821
+ });
1822
+ return;
1823
+ }
1824
+ }
1825
+ if (!hasFinal) {
1826
+ push("error", {
1827
+ code: "CHAT_TURN_FAILED",
1828
+ message: "stream ended without a final result"
1829
+ });
1830
+ return;
1831
+ }
1832
+ push("done", { ok: true });
1833
+ } catch (error) {
1834
+ push("error", {
1835
+ code: "CHAT_TURN_FAILED",
1836
+ message: formatUserFacingError(error)
1837
+ });
1838
+ } finally {
1839
+ controller.close();
1840
+ }
1841
+ }
1842
+ });
1843
+ return new Response(stream, {
1844
+ status: 200,
1845
+ headers: {
1846
+ "Content-Type": "text/event-stream; charset=utf-8",
1847
+ "Cache-Control": "no-cache, no-transform",
1848
+ "Connection": "keep-alive",
1849
+ "X-Accel-Buffering": "no"
1850
+ }
1851
+ });
1852
+ };
1853
+ listRuns = async (c) => {
1854
+ const chatRuntime = this.options.chatRuntime;
1855
+ if (!chatRuntime?.listRuns) {
1856
+ return c.json(err("NOT_AVAILABLE", "chat run management unavailable"), 503);
1857
+ }
1858
+ const query = c.req.query();
1859
+ const sessionKey = readNonEmptyString(query.sessionKey);
1860
+ const states = readChatRunStates(query.states);
1861
+ const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
1862
+ try {
1863
+ const data = await chatRuntime.listRuns({
1864
+ ...sessionKey ? { sessionKey } : {},
1865
+ ...states ? { states } : {},
1866
+ ...Number.isFinite(limit) ? { limit } : {}
1867
+ });
1868
+ return c.json(ok(data));
1869
+ } catch (error) {
1870
+ return c.json(err("CHAT_RUN_QUERY_FAILED", String(error)), 500);
1871
+ }
1872
+ };
1873
+ getRun = async (c) => {
1874
+ const chatRuntime = this.options.chatRuntime;
1875
+ if (!chatRuntime?.getRun) {
1876
+ return c.json(err("NOT_AVAILABLE", "chat run management unavailable"), 503);
1877
+ }
1878
+ const runId = readNonEmptyString(c.req.param("runId"));
1879
+ if (!runId) {
1880
+ return c.json(err("INVALID_PATH", "runId is required"), 400);
1881
+ }
1882
+ try {
1883
+ const run = await chatRuntime.getRun({ runId });
1884
+ if (!run) {
1885
+ return c.json(err("NOT_FOUND", `chat run not found: ${runId}`), 404);
1886
+ }
1887
+ return c.json(ok(run));
1888
+ } catch (error) {
1889
+ return c.json(err("CHAT_RUN_QUERY_FAILED", String(error)), 500);
1890
+ }
1891
+ };
1892
+ streamRun = async (c) => {
1893
+ const chatRuntime = this.options.chatRuntime;
1894
+ const streamRun = chatRuntime?.streamRun;
1895
+ const getRun = chatRuntime?.getRun;
1896
+ if (!streamRun || !getRun) {
1897
+ return c.json(err("NOT_AVAILABLE", "chat run stream unavailable"), 503);
1898
+ }
1899
+ const runId = readNonEmptyString(c.req.param("runId"));
1900
+ if (!runId) {
1901
+ return c.json(err("INVALID_PATH", "runId is required"), 400);
1902
+ }
1903
+ const query = c.req.query();
1904
+ const fromEventIndex = typeof query.fromEventIndex === "string" ? Number.parseInt(query.fromEventIndex, 10) : void 0;
1905
+ const run = await getRun({ runId });
1906
+ if (!run) {
1907
+ return c.json(err("NOT_FOUND", `chat run not found: ${runId}`), 404);
1908
+ }
1909
+ const encoder = new TextEncoder();
1910
+ const stream = new ReadableStream({
1911
+ start: async (controller) => {
1912
+ const push = (event, data) => {
1913
+ controller.enqueue(encoder.encode(toSseFrame(event, data)));
1914
+ };
1915
+ try {
1916
+ push("ready", {
1917
+ sessionKey: run.sessionKey,
1918
+ requestedAt: run.requestedAt,
1919
+ runId: run.runId,
1920
+ stopSupported: run.stopSupported,
1921
+ ...readNonEmptyString(run.stopReason) ? { stopReason: readNonEmptyString(run.stopReason) } : {}
1922
+ });
1923
+ let hasFinal = false;
1924
+ for await (const event of streamRun({
1925
+ runId: run.runId,
1926
+ ...Number.isFinite(fromEventIndex) ? { fromEventIndex } : {}
1927
+ })) {
1928
+ const typed = event;
1929
+ if (typed.type === "delta") {
1930
+ if (typed.delta) {
1931
+ push("delta", { delta: typed.delta });
1932
+ }
1933
+ continue;
1934
+ }
1935
+ if (typed.type === "session_event") {
1936
+ push("session_event", typed.event);
1937
+ continue;
1938
+ }
1939
+ if (typed.type === "final") {
1940
+ const latestRun = await getRun({ runId: run.runId });
1941
+ const response = latestRun ? buildChatTurnViewFromRun({
1942
+ run: latestRun,
1943
+ fallbackSessionKey: run.sessionKey,
1944
+ fallbackAgentId: run.agentId,
1945
+ fallbackModel: run.model,
1946
+ fallbackReply: typed.result.reply
1947
+ }) : buildChatTurnView({
1948
+ result: typed.result,
1949
+ fallbackSessionKey: run.sessionKey,
1950
+ requestedAgentId: run.agentId,
1951
+ requestedModel: run.model,
1952
+ requestedAt: new Date(run.requestedAt),
1953
+ startedAtMs: Date.parse(run.requestedAt)
1954
+ });
1955
+ hasFinal = true;
1956
+ push("final", response);
1957
+ continue;
1958
+ }
1959
+ if (typed.type === "error") {
1960
+ push("error", {
1961
+ code: "CHAT_TURN_FAILED",
1962
+ message: formatUserFacingError(typed.error)
1963
+ });
1964
+ return;
1965
+ }
1966
+ }
1967
+ if (!hasFinal) {
1968
+ const latestRun = await getRun({ runId: run.runId });
1969
+ if (latestRun?.state === "failed") {
1970
+ push("error", {
1971
+ code: "CHAT_TURN_FAILED",
1972
+ message: formatUserFacingError(latestRun.error ?? "chat run failed")
1973
+ });
1974
+ return;
1975
+ }
1976
+ }
1977
+ push("done", { ok: true });
1978
+ } catch (error) {
1979
+ push("error", {
1980
+ code: "CHAT_TURN_FAILED",
1981
+ message: formatUserFacingError(error)
1982
+ });
1983
+ } finally {
1984
+ controller.close();
1985
+ }
1986
+ }
1987
+ });
1988
+ return new Response(stream, {
1989
+ status: 200,
1990
+ headers: {
1991
+ "Content-Type": "text/event-stream; charset=utf-8",
1992
+ "Cache-Control": "no-cache, no-transform",
1993
+ "Connection": "keep-alive",
1994
+ "X-Accel-Buffering": "no"
1995
+ }
1996
+ });
1997
+ };
1998
+ };
1999
+
2000
+ // src/ui/provider-auth.ts
2001
+ import { createHash, randomBytes, randomUUID } from "crypto";
2002
+ import { readFile } from "fs/promises";
2003
+ import { homedir } from "os";
2004
+ import { isAbsolute, resolve } from "path";
2005
+ import {
2006
+ ConfigSchema as ConfigSchema2,
2007
+ loadConfig as loadConfig2,
2008
+ saveConfig as saveConfig2
2009
+ } from "@nextclaw/core";
2010
+ var authSessions = /* @__PURE__ */ new Map();
2011
+ var DEFAULT_AUTH_INTERVAL_MS = 2e3;
2012
+ var MAX_AUTH_INTERVAL_MS = 1e4;
2013
+ function normalizePositiveInt(value, fallback) {
2014
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2015
+ return fallback;
2016
+ }
2017
+ return Math.floor(value);
2018
+ }
2019
+ function normalizePositiveFloat(value) {
2020
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2021
+ return null;
2022
+ }
2023
+ return value;
2024
+ }
2025
+ function toBase64Url(buffer) {
2026
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
2027
+ }
2028
+ function buildPkce() {
2029
+ const verifier = toBase64Url(randomBytes(48));
2030
+ const challenge = toBase64Url(createHash("sha256").update(verifier).digest());
2031
+ return { verifier, challenge };
2032
+ }
2033
+ function withTrailingSlash(value) {
2034
+ return value.endsWith("/") ? value : `${value}/`;
2035
+ }
2036
+ function cleanupExpiredAuthSessions(now = Date.now()) {
2037
+ for (const [sessionId, session] of authSessions.entries()) {
2038
+ if (session.expiresAtMs <= now) {
2039
+ authSessions.delete(sessionId);
2040
+ }
2041
+ }
2042
+ }
2043
+ function resolveDeviceCodeEndpoints(baseUrl, deviceCodePath, tokenPath) {
2044
+ const deviceCodeEndpoint = new URL(deviceCodePath, withTrailingSlash(baseUrl)).toString();
2045
+ const tokenEndpoint = new URL(tokenPath, withTrailingSlash(baseUrl)).toString();
2046
+ return { deviceCodeEndpoint, tokenEndpoint };
2047
+ }
2048
+ function resolveAuthNote(params) {
2049
+ return params.zh ?? params.en;
2050
+ }
2051
+ function resolveLocalizedMethodLabel(method, fallbackId) {
2052
+ return method.label?.zh ?? method.label?.en ?? fallbackId;
2053
+ }
2054
+ function resolveLocalizedMethodHint(method) {
2055
+ return method.hint?.zh ?? method.hint?.en;
2056
+ }
2057
+ function normalizeMethodId(value) {
2058
+ if (typeof value !== "string") {
2059
+ return void 0;
2060
+ }
2061
+ const trimmed = value.trim();
2062
+ return trimmed.length > 0 ? trimmed : void 0;
2063
+ }
2064
+ function resolveAuthMethod(auth, requestedMethodId) {
2065
+ const protocol = auth.protocol ?? "rfc8628";
2066
+ const methods = (auth.methods ?? []).filter((entry) => normalizeMethodId(entry.id));
2067
+ const cleanRequestedMethodId = normalizeMethodId(requestedMethodId);
2068
+ if (methods.length === 0) {
2069
+ if (cleanRequestedMethodId) {
2070
+ throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
2071
+ }
2072
+ return {
2073
+ protocol,
2074
+ baseUrl: auth.baseUrl,
2075
+ deviceCodePath: auth.deviceCodePath,
2076
+ tokenPath: auth.tokenPath,
2077
+ clientId: auth.clientId,
2078
+ scope: auth.scope,
2079
+ grantType: auth.grantType,
2080
+ usePkce: Boolean(auth.usePkce)
2081
+ };
2082
+ }
2083
+ let selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === cleanRequestedMethodId);
2084
+ if (!selectedMethod) {
2085
+ const fallbackMethodId = normalizeMethodId(auth.defaultMethodId) ?? normalizeMethodId(methods[0]?.id);
2086
+ selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === fallbackMethodId) ?? methods[0];
2087
+ }
2088
+ const methodId = normalizeMethodId(selectedMethod?.id);
2089
+ if (!selectedMethod || !methodId) {
2090
+ throw new Error("provider auth method is not configured");
2091
+ }
2092
+ if (cleanRequestedMethodId && methodId !== cleanRequestedMethodId) {
2093
+ throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
2094
+ }
2095
+ return {
2096
+ id: methodId,
2097
+ protocol,
2098
+ baseUrl: selectedMethod.baseUrl ?? auth.baseUrl,
2099
+ deviceCodePath: selectedMethod.deviceCodePath ?? auth.deviceCodePath,
2100
+ tokenPath: selectedMethod.tokenPath ?? auth.tokenPath,
2101
+ clientId: selectedMethod.clientId ?? auth.clientId,
2102
+ scope: selectedMethod.scope ?? auth.scope,
2103
+ grantType: selectedMethod.grantType ?? auth.grantType,
2104
+ usePkce: selectedMethod.usePkce ?? Boolean(auth.usePkce),
2105
+ defaultApiBase: selectedMethod.defaultApiBase
2106
+ };
2107
+ }
2108
+ function parseExpiresAtMs(value, fallbackFromNowMs) {
2109
+ const normalized = normalizePositiveFloat(value);
2110
+ if (normalized === null) {
2111
+ return Date.now() + fallbackFromNowMs;
2112
+ }
2113
+ if (normalized >= 1e12) {
2114
+ return Math.floor(normalized);
2115
+ }
2116
+ if (normalized >= 1e9) {
2117
+ return Math.floor(normalized * 1e3);
2118
+ }
2119
+ return Date.now() + Math.floor(normalized * 1e3);
2120
+ }
2121
+ function parsePollIntervalMs(value, fallbackMs) {
2122
+ const normalized = normalizePositiveFloat(value);
2123
+ if (normalized === null) {
2124
+ return fallbackMs;
2125
+ }
2126
+ if (normalized <= 30) {
2127
+ return Math.floor(normalized * 1e3);
2128
+ }
2129
+ return Math.floor(normalized);
2130
+ }
2131
+ function buildMinimaxErrorMessage(payload, fallback) {
2132
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
2133
+ return fallback;
2134
+ }
2135
+ const record = payload;
2136
+ if (typeof record.error_description === "string" && record.error_description.trim()) {
2137
+ return record.error_description.trim();
2138
+ }
2139
+ if (typeof record.error === "string" && record.error.trim()) {
2140
+ return record.error.trim();
2141
+ }
2142
+ const baseMessage = record.base_resp?.status_msg;
2143
+ if (typeof baseMessage === "string" && baseMessage.trim()) {
2144
+ return baseMessage.trim();
2145
+ }
2146
+ return fallback;
2147
+ }
2148
+ function classifyMiniMaxErrorStatus(message) {
2149
+ const normalized = message.toLowerCase();
2150
+ if (normalized.includes("deny") || normalized.includes("rejected")) {
2151
+ return "denied";
2152
+ }
2153
+ if (normalized.includes("expired") || normalized.includes("timeout") || normalized.includes("timed out")) {
2154
+ return "expired";
2155
+ }
2156
+ return "error";
2157
+ }
2158
+ function resolveHomePath(inputPath) {
2159
+ const trimmed = inputPath.trim();
2160
+ if (!trimmed) {
2161
+ return trimmed;
2162
+ }
2163
+ if (trimmed === "~") {
2164
+ return homedir();
2165
+ }
2166
+ if (trimmed.startsWith("~/")) {
2167
+ return resolve(homedir(), trimmed.slice(2));
2168
+ }
2169
+ if (isAbsolute(trimmed)) {
2170
+ return trimmed;
2171
+ }
2172
+ return resolve(trimmed);
2173
+ }
2174
+ function normalizeExpiresAt(value) {
2175
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
2176
+ return Math.floor(value);
2177
+ }
2178
+ if (typeof value === "string" && value.trim()) {
2179
+ const asNumber = Number(value);
2180
+ if (Number.isFinite(asNumber) && asNumber > 0) {
2181
+ return Math.floor(asNumber);
2182
+ }
2183
+ const parsedTime = Date.parse(value);
2184
+ if (Number.isFinite(parsedTime) && parsedTime > 0) {
2185
+ return parsedTime;
2186
+ }
2187
+ }
2188
+ return null;
2189
+ }
2190
+ function readFieldAsString(source, fieldName) {
2191
+ if (!fieldName) {
2192
+ return null;
2193
+ }
2194
+ const rawValue = source[fieldName];
2195
+ if (typeof rawValue !== "string") {
2196
+ return null;
2197
+ }
2198
+ const trimmed = rawValue.trim();
2199
+ return trimmed.length > 0 ? trimmed : null;
2200
+ }
2201
+ function setProviderApiKey(params) {
2202
+ const config = loadConfig2(params.configPath);
2203
+ const providers = config.providers;
2204
+ if (!providers[params.provider]) {
2205
+ providers[params.provider] = {
2206
+ displayName: "",
2207
+ apiKey: "",
2208
+ apiBase: null,
2209
+ extraHeaders: null,
2210
+ wireApi: "auto",
2211
+ models: [],
2212
+ modelThinking: {}
2213
+ };
2214
+ }
2215
+ const target = providers[params.provider];
2216
+ target.apiKey = params.accessToken;
2217
+ if (!target.apiBase && params.defaultApiBase) {
2218
+ target.apiBase = params.defaultApiBase;
2219
+ }
2220
+ const next = ConfigSchema2.parse(config);
2221
+ saveConfig2(next, params.configPath);
2222
+ }
2223
+ async function startProviderAuth(configPath, providerName, options) {
2224
+ cleanupExpiredAuthSessions();
2225
+ const spec = findServerBuiltinProviderByName(providerName);
2226
+ if (!spec?.auth || spec.auth.kind !== "device_code") {
2227
+ return null;
2228
+ }
2229
+ const resolvedMethod = resolveAuthMethod(spec.auth, options?.methodId);
2230
+ const { deviceCodeEndpoint, tokenEndpoint } = resolveDeviceCodeEndpoints(
2231
+ resolvedMethod.baseUrl,
2232
+ resolvedMethod.deviceCodePath,
2233
+ resolvedMethod.tokenPath
2234
+ );
2235
+ const pkce = resolvedMethod.usePkce ? buildPkce() : null;
2236
+ let authorizationCode = "";
2237
+ let tokenCodeField = "device_code";
2238
+ let userCode = "";
2239
+ let verificationUri = "";
2240
+ let intervalMs = DEFAULT_AUTH_INTERVAL_MS;
2241
+ let expiresAtMs = Date.now() + 6e5;
2242
+ if (resolvedMethod.protocol === "minimax_user_code") {
2243
+ if (!pkce) {
2244
+ throw new Error("MiniMax OAuth requires PKCE");
2245
+ }
2246
+ const state = toBase64Url(randomBytes(16));
2247
+ const body = new URLSearchParams({
2248
+ response_type: "code",
2249
+ client_id: resolvedMethod.clientId,
2250
+ scope: resolvedMethod.scope,
2251
+ code_challenge: pkce.challenge,
2252
+ code_challenge_method: "S256",
2253
+ state
2254
+ });
2255
+ const response = await fetch(deviceCodeEndpoint, {
2256
+ method: "POST",
2257
+ headers: {
2258
+ "Content-Type": "application/x-www-form-urlencoded",
2259
+ Accept: "application/json",
2260
+ "x-request-id": randomUUID()
2261
+ },
2262
+ body
2263
+ });
2264
+ const payload = await response.json().catch(() => ({}));
2265
+ if (!response.ok) {
2266
+ throw new Error(buildMinimaxErrorMessage(payload, response.statusText || "MiniMax OAuth start failed"));
2267
+ }
2268
+ if (payload.state && payload.state !== state) {
2269
+ throw new Error("MiniMax OAuth state mismatch");
2270
+ }
2271
+ authorizationCode = payload.user_code?.trim() ?? "";
2272
+ userCode = authorizationCode;
2273
+ verificationUri = payload.verification_uri?.trim() ?? "";
2274
+ if (!authorizationCode || !verificationUri) {
2275
+ throw new Error("provider auth payload is incomplete");
2276
+ }
2277
+ tokenCodeField = "user_code";
2278
+ intervalMs = Math.min(parsePollIntervalMs(payload.interval, DEFAULT_AUTH_INTERVAL_MS), MAX_AUTH_INTERVAL_MS);
2279
+ expiresAtMs = parseExpiresAtMs(payload.expired_in, 6e5);
2280
+ } else {
2281
+ const body = new URLSearchParams({
2282
+ client_id: resolvedMethod.clientId,
2283
+ scope: resolvedMethod.scope
2284
+ });
2285
+ if (pkce) {
2286
+ body.set("code_challenge", pkce.challenge);
2287
+ body.set("code_challenge_method", "S256");
2288
+ }
2289
+ const response = await fetch(deviceCodeEndpoint, {
2290
+ method: "POST",
2291
+ headers: {
2292
+ "Content-Type": "application/x-www-form-urlencoded",
2293
+ Accept: "application/json"
2294
+ },
2295
+ body
2296
+ });
2297
+ const payload = await response.json().catch(() => ({}));
2298
+ if (!response.ok) {
2299
+ const message = payload.error_description || payload.error || response.statusText || "device code auth failed";
2300
+ throw new Error(message);
2301
+ }
2302
+ authorizationCode = payload.device_code?.trim() ?? "";
2303
+ userCode = payload.user_code?.trim() ?? "";
2304
+ verificationUri = payload.verification_uri_complete?.trim() || payload.verification_uri?.trim() || "";
2305
+ if (!authorizationCode || !userCode || !verificationUri) {
2306
+ throw new Error("provider auth payload is incomplete");
2307
+ }
2308
+ intervalMs = normalizePositiveInt(payload.interval, DEFAULT_AUTH_INTERVAL_MS / 1e3) * 1e3;
2309
+ const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
2310
+ expiresAtMs = Date.now() + expiresInSec * 1e3;
2311
+ }
2312
+ const sessionId = randomUUID();
2313
+ authSessions.set(sessionId, {
2314
+ sessionId,
2315
+ provider: providerName,
2316
+ configPath,
2317
+ authorizationCode,
2318
+ tokenCodeField,
2319
+ protocol: resolvedMethod.protocol,
2320
+ methodId: resolvedMethod.id,
2321
+ codeVerifier: pkce?.verifier,
2322
+ tokenEndpoint,
2323
+ clientId: resolvedMethod.clientId,
2324
+ grantType: resolvedMethod.grantType,
2325
+ defaultApiBase: resolvedMethod.defaultApiBase ?? spec.defaultApiBase,
2326
+ expiresAtMs,
2327
+ intervalMs
2328
+ });
2329
+ const methodConfig = (spec.auth.methods ?? []).find((entry) => normalizeMethodId(entry.id) === resolvedMethod.id);
2330
+ const methodLabel = methodConfig ? resolveLocalizedMethodLabel(methodConfig, resolvedMethod.id ?? "") : void 0;
2331
+ const methodHint = methodConfig ? resolveLocalizedMethodHint(methodConfig) : void 0;
2332
+ return {
2333
+ provider: providerName,
2334
+ kind: "device_code",
2335
+ methodId: resolvedMethod.id,
2336
+ sessionId,
2337
+ verificationUri,
2338
+ userCode,
2339
+ expiresAt: new Date(expiresAtMs).toISOString(),
2340
+ intervalMs,
2341
+ note: methodHint ?? methodLabel ?? resolveAuthNote(spec.auth.note ?? {})
2342
+ };
2343
+ }
2344
+ async function pollProviderAuth(params) {
2345
+ cleanupExpiredAuthSessions();
2346
+ const session = authSessions.get(params.sessionId);
2347
+ if (!session || session.provider !== params.providerName || session.configPath !== params.configPath) {
2348
+ return null;
2349
+ }
2350
+ if (Date.now() >= session.expiresAtMs) {
2351
+ authSessions.delete(params.sessionId);
2352
+ return {
2353
+ provider: params.providerName,
2354
+ status: "expired",
2355
+ message: "authorization session expired"
2356
+ };
2357
+ }
2358
+ const body = new URLSearchParams({
2359
+ grant_type: session.grantType,
2360
+ client_id: session.clientId
2361
+ });
2362
+ body.set(session.tokenCodeField, session.authorizationCode);
2363
+ if (session.codeVerifier) {
2364
+ body.set("code_verifier", session.codeVerifier);
2365
+ }
2366
+ const response = await fetch(session.tokenEndpoint, {
2367
+ method: "POST",
2368
+ headers: {
2369
+ "Content-Type": "application/x-www-form-urlencoded",
2370
+ Accept: "application/json"
2371
+ },
2372
+ body
2373
+ });
2374
+ let accessToken = "";
2375
+ if (session.protocol === "minimax_user_code") {
2376
+ const raw = await response.text();
2377
+ let payload = {};
2378
+ if (raw) {
2379
+ try {
2380
+ payload = JSON.parse(raw);
2381
+ } catch {
2382
+ payload = {};
2383
+ }
2384
+ }
2385
+ if (!response.ok) {
2386
+ const message = buildMinimaxErrorMessage(payload, raw || response.statusText || "authorization failed");
2387
+ return {
2388
+ provider: params.providerName,
2389
+ status: "error",
2390
+ message
2391
+ };
2392
+ }
2393
+ const status = payload.status?.trim().toLowerCase();
2394
+ if (status === "success") {
2395
+ accessToken = payload.access_token?.trim() ?? "";
2396
+ if (!accessToken) {
2397
+ return {
2398
+ provider: params.providerName,
2399
+ status: "error",
2400
+ message: "provider token response missing access token"
2401
+ };
2402
+ }
2403
+ } else if (status === "error") {
2404
+ const message = buildMinimaxErrorMessage(payload, "authorization failed");
2405
+ const classified = classifyMiniMaxErrorStatus(message);
2406
+ if (classified === "denied" || classified === "expired") {
2407
+ authSessions.delete(params.sessionId);
2408
+ }
2409
+ return {
2410
+ provider: params.providerName,
2411
+ status: classified,
2412
+ message
2413
+ };
2414
+ } else {
2415
+ const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
2416
+ session.intervalMs = nextPollMs;
2417
+ authSessions.set(params.sessionId, session);
2418
+ return {
2419
+ provider: params.providerName,
2420
+ status: "pending",
2421
+ nextPollMs
2422
+ };
2423
+ }
2424
+ } else {
2425
+ const payload = await response.json().catch(() => ({}));
2426
+ if (!response.ok) {
2427
+ const errorCode = payload.error?.trim().toLowerCase();
2428
+ if (errorCode === "authorization_pending") {
2429
+ return {
2430
+ provider: params.providerName,
2431
+ status: "pending",
2432
+ nextPollMs: session.intervalMs
2433
+ };
2434
+ }
2435
+ if (errorCode === "slow_down") {
2436
+ const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
2437
+ session.intervalMs = nextPollMs;
2438
+ authSessions.set(params.sessionId, session);
2439
+ return {
2440
+ provider: params.providerName,
2441
+ status: "pending",
2442
+ nextPollMs
2443
+ };
2444
+ }
2445
+ if (errorCode === "access_denied") {
2446
+ authSessions.delete(params.sessionId);
2447
+ return {
2448
+ provider: params.providerName,
2449
+ status: "denied",
2450
+ message: payload.error_description || "authorization denied"
2451
+ };
2452
+ }
2453
+ if (errorCode === "expired_token") {
2454
+ authSessions.delete(params.sessionId);
2455
+ return {
2456
+ provider: params.providerName,
2457
+ status: "expired",
2458
+ message: payload.error_description || "authorization session expired"
2459
+ };
2460
+ }
2461
+ return {
2462
+ provider: params.providerName,
2463
+ status: "error",
2464
+ message: payload.error_description || payload.error || response.statusText || "authorization failed"
2465
+ };
2466
+ }
2467
+ accessToken = payload.access_token?.trim() ?? "";
2468
+ if (!accessToken) {
2469
+ return {
2470
+ provider: params.providerName,
2471
+ status: "error",
2472
+ message: "provider token response missing access token"
2473
+ };
2474
+ }
2475
+ }
2476
+ setProviderApiKey({
2477
+ configPath: params.configPath,
2478
+ provider: params.providerName,
2479
+ accessToken,
2480
+ defaultApiBase: session.defaultApiBase
2481
+ });
2482
+ authSessions.delete(params.sessionId);
2483
+ return {
2484
+ provider: params.providerName,
2485
+ status: "authorized"
2486
+ };
2487
+ }
2488
+ async function importProviderAuthFromCli(configPath, providerName) {
2489
+ const spec = findServerBuiltinProviderByName(providerName);
2490
+ if (!spec?.auth || spec.auth.kind !== "device_code" || !spec.auth.cliCredential) {
2491
+ return null;
2492
+ }
2493
+ const credentialPath = resolveHomePath(spec.auth.cliCredential.path);
2494
+ if (!credentialPath) {
2495
+ throw new Error("provider cli credential path is empty");
2496
+ }
2497
+ let rawContent = "";
2498
+ try {
2499
+ rawContent = await readFile(credentialPath, "utf8");
2500
+ } catch (error) {
2501
+ const message = error instanceof Error ? error.message : String(error);
2502
+ throw new Error(`failed to read CLI credential: ${message}`);
2503
+ }
2504
+ let payload;
2505
+ try {
2506
+ const parsed = JSON.parse(rawContent);
1839
2507
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1840
2508
  throw new Error("credential payload is not an object");
1841
2509
  }
1842
- payload = parsed;
2510
+ payload = parsed;
2511
+ } catch (error) {
2512
+ const message = error instanceof Error ? error.message : String(error);
2513
+ throw new Error(`invalid CLI credential JSON: ${message}`);
2514
+ }
2515
+ const accessToken = readFieldAsString(payload, spec.auth.cliCredential.accessTokenField);
2516
+ if (!accessToken) {
2517
+ throw new Error(
2518
+ `CLI credential missing access token field: ${spec.auth.cliCredential.accessTokenField}`
2519
+ );
2520
+ }
2521
+ const expiresAtMs = normalizeExpiresAt(
2522
+ spec.auth.cliCredential.expiresAtField ? payload[spec.auth.cliCredential.expiresAtField] : void 0
2523
+ );
2524
+ if (typeof expiresAtMs === "number" && expiresAtMs <= Date.now()) {
2525
+ throw new Error("CLI credential has expired, please login again");
2526
+ }
2527
+ setProviderApiKey({
2528
+ configPath,
2529
+ provider: providerName,
2530
+ accessToken,
2531
+ defaultApiBase: spec.defaultApiBase
2532
+ });
2533
+ return {
2534
+ provider: providerName,
2535
+ status: "imported",
2536
+ source: "cli",
2537
+ expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
2538
+ };
2539
+ }
2540
+
2541
+ // src/ui/router/config.controller.ts
2542
+ var ConfigRoutesController = class {
2543
+ constructor(options) {
2544
+ this.options = options;
2545
+ }
2546
+ getConfig = (c) => {
2547
+ const config = loadConfigOrDefault(this.options.configPath);
2548
+ return c.json(ok(buildConfigView(config)));
2549
+ };
2550
+ getConfigMeta = (c) => {
2551
+ const config = loadConfigOrDefault(this.options.configPath);
2552
+ return c.json(ok(buildConfigMeta(config)));
2553
+ };
2554
+ getConfigSchema = (c) => {
2555
+ const config = loadConfigOrDefault(this.options.configPath);
2556
+ return c.json(ok(buildConfigSchemaView(config)));
2557
+ };
2558
+ updateConfigModel = async (c) => {
2559
+ const body = await readJson(c.req.raw);
2560
+ if (!body.ok) {
2561
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2562
+ }
2563
+ const hasModel = typeof body.data.model === "string";
2564
+ if (!hasModel) {
2565
+ return c.json(err("INVALID_BODY", "model is required"), 400);
2566
+ }
2567
+ const view = updateModel(this.options.configPath, {
2568
+ model: body.data.model
2569
+ });
2570
+ if (hasModel) {
2571
+ this.options.publish({ type: "config.updated", payload: { path: "agents.defaults.model" } });
2572
+ }
2573
+ return c.json(ok({
2574
+ model: view.agents.defaults.model
2575
+ }));
2576
+ };
2577
+ updateConfigSearch = async (c) => {
2578
+ const body = await readJson(c.req.raw);
2579
+ if (!body.ok) {
2580
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2581
+ }
2582
+ const result = updateSearch(this.options.configPath, body.data);
2583
+ this.options.publish({ type: "config.updated", payload: { path: "search" } });
2584
+ return c.json(ok(result));
2585
+ };
2586
+ updateProvider = async (c) => {
2587
+ const provider = c.req.param("provider");
2588
+ const body = await readJson(c.req.raw);
2589
+ if (!body.ok) {
2590
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2591
+ }
2592
+ const result = updateProvider(this.options.configPath, provider, body.data);
2593
+ if (!result) {
2594
+ return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
2595
+ }
2596
+ this.options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
2597
+ return c.json(ok(result));
2598
+ };
2599
+ createProvider = async (c) => {
2600
+ const body = await readJson(c.req.raw);
2601
+ if (!body.ok) {
2602
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2603
+ }
2604
+ const result = createCustomProvider(
2605
+ this.options.configPath,
2606
+ body.data
2607
+ );
2608
+ this.options.publish({ type: "config.updated", payload: { path: `providers.${result.name}` } });
2609
+ return c.json(ok({
2610
+ name: result.name,
2611
+ provider: result.provider
2612
+ }));
2613
+ };
2614
+ deleteProvider = async (c) => {
2615
+ const provider = c.req.param("provider");
2616
+ const result = deleteCustomProvider(this.options.configPath, provider);
2617
+ if (result === null) {
2618
+ return c.json(err("NOT_FOUND", `custom provider not found: ${provider}`), 404);
2619
+ }
2620
+ this.options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
2621
+ return c.json(ok({
2622
+ deleted: true,
2623
+ provider
2624
+ }));
2625
+ };
2626
+ testProviderConnection = async (c) => {
2627
+ const provider = c.req.param("provider");
2628
+ const body = await readJson(c.req.raw);
2629
+ if (!body.ok) {
2630
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2631
+ }
2632
+ const result = await testProviderConnection(
2633
+ this.options.configPath,
2634
+ provider,
2635
+ body.data
2636
+ );
2637
+ if (!result) {
2638
+ return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
2639
+ }
2640
+ return c.json(ok(result));
2641
+ };
2642
+ startProviderAuth = async (c) => {
2643
+ const provider = c.req.param("provider");
2644
+ let payload = {};
2645
+ const rawBody = await c.req.raw.text();
2646
+ if (rawBody.trim().length > 0) {
2647
+ try {
2648
+ payload = JSON.parse(rawBody);
2649
+ } catch {
2650
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2651
+ }
2652
+ }
2653
+ const methodId = typeof payload.methodId === "string" ? payload.methodId.trim() : void 0;
2654
+ try {
2655
+ const result = await startProviderAuth(this.options.configPath, provider, {
2656
+ methodId
2657
+ });
2658
+ if (!result) {
2659
+ return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
2660
+ }
2661
+ return c.json(ok(result));
2662
+ } catch (error) {
2663
+ const message = error instanceof Error ? error.message : String(error);
2664
+ return c.json(err("AUTH_START_FAILED", message), 400);
2665
+ }
2666
+ };
2667
+ pollProviderAuth = async (c) => {
2668
+ const provider = c.req.param("provider");
2669
+ const body = await readJson(c.req.raw);
2670
+ if (!body.ok) {
2671
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2672
+ }
2673
+ const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
2674
+ if (!sessionId) {
2675
+ return c.json(err("INVALID_BODY", "sessionId is required"), 400);
2676
+ }
2677
+ const result = await pollProviderAuth({
2678
+ configPath: this.options.configPath,
2679
+ providerName: provider,
2680
+ sessionId
2681
+ });
2682
+ if (!result) {
2683
+ return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
2684
+ }
2685
+ if (result.status === "authorized") {
2686
+ this.options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
2687
+ }
2688
+ return c.json(ok(result));
2689
+ };
2690
+ importProviderAuthFromCli = async (c) => {
2691
+ const provider = c.req.param("provider");
2692
+ try {
2693
+ const result = await importProviderAuthFromCli(this.options.configPath, provider);
2694
+ if (!result) {
2695
+ return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
2696
+ }
2697
+ this.options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
2698
+ return c.json(ok(result));
2699
+ } catch (error) {
2700
+ const message = error instanceof Error ? error.message : String(error);
2701
+ return c.json(err("AUTH_IMPORT_FAILED", message), 400);
2702
+ }
2703
+ };
2704
+ updateChannel = async (c) => {
2705
+ const channel = c.req.param("channel");
2706
+ const body = await readJson(c.req.raw);
2707
+ if (!body.ok) {
2708
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2709
+ }
2710
+ const result = updateChannel(this.options.configPath, channel, body.data);
2711
+ if (!result) {
2712
+ return c.json(err("NOT_FOUND", `unknown channel: ${channel}`), 404);
2713
+ }
2714
+ this.options.publish({ type: "config.updated", payload: { path: `channels.${channel}` } });
2715
+ return c.json(ok(result));
2716
+ };
2717
+ updateSecrets = async (c) => {
2718
+ const body = await readJson(c.req.raw);
2719
+ if (!body.ok) {
2720
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2721
+ }
2722
+ const result = updateSecrets(this.options.configPath, body.data);
2723
+ this.options.publish({ type: "config.updated", payload: { path: "secrets" } });
2724
+ return c.json(ok(result));
2725
+ };
2726
+ updateRuntime = async (c) => {
2727
+ const body = await readJson(c.req.raw);
2728
+ if (!body.ok || !body.data || typeof body.data !== "object") {
2729
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2730
+ }
2731
+ const result = updateRuntime(this.options.configPath, body.data);
2732
+ if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "contextTokens")) {
2733
+ this.options.publish({ type: "config.updated", payload: { path: "agents.defaults.contextTokens" } });
2734
+ }
2735
+ if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engine")) {
2736
+ this.options.publish({ type: "config.updated", payload: { path: "agents.defaults.engine" } });
2737
+ }
2738
+ if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engineConfig")) {
2739
+ this.options.publish({ type: "config.updated", payload: { path: "agents.defaults.engineConfig" } });
2740
+ }
2741
+ this.options.publish({ type: "config.updated", payload: { path: "agents.list" } });
2742
+ this.options.publish({ type: "config.updated", payload: { path: "bindings" } });
2743
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
2744
+ return c.json(ok(result));
2745
+ };
2746
+ executeAction = async (c) => {
2747
+ const actionId = c.req.param("actionId");
2748
+ const body = await readJson(c.req.raw);
2749
+ if (!body.ok) {
2750
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2751
+ }
2752
+ const result = await executeConfigAction(this.options.configPath, actionId, body.data ?? {});
2753
+ if (!result.ok) {
2754
+ return c.json(err(result.code, result.message, result.details), 400);
2755
+ }
2756
+ return c.json(ok(result.data));
2757
+ };
2758
+ };
2759
+
2760
+ // src/ui/router/cron.controller.ts
2761
+ function toIsoTime(value) {
2762
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2763
+ return null;
2764
+ }
2765
+ const date = new Date(value);
2766
+ if (Number.isNaN(date.getTime())) {
2767
+ return null;
2768
+ }
2769
+ return date.toISOString();
2770
+ }
2771
+ function buildCronJobView(job) {
2772
+ return {
2773
+ id: job.id,
2774
+ name: job.name,
2775
+ enabled: job.enabled,
2776
+ schedule: job.schedule,
2777
+ payload: job.payload,
2778
+ state: {
2779
+ nextRunAt: toIsoTime(job.state.nextRunAtMs),
2780
+ lastRunAt: toIsoTime(job.state.lastRunAtMs),
2781
+ lastStatus: job.state.lastStatus ?? null,
2782
+ lastError: job.state.lastError ?? null
2783
+ },
2784
+ createdAt: new Date(job.createdAtMs).toISOString(),
2785
+ updatedAt: new Date(job.updatedAtMs).toISOString(),
2786
+ deleteAfterRun: job.deleteAfterRun
2787
+ };
2788
+ }
2789
+ function findCronJob(service, id) {
2790
+ const jobs = service.listJobs(true);
2791
+ return jobs.find((job) => job.id === id) ?? null;
2792
+ }
2793
+ var CronRoutesController = class {
2794
+ constructor(options) {
2795
+ this.options = options;
2796
+ }
2797
+ listJobs = (c) => {
2798
+ if (!this.options.cronService) {
2799
+ return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
2800
+ }
2801
+ const query = c.req.query();
2802
+ const includeDisabled = query.all === "1" || query.all === "true" || query.all === "yes";
2803
+ const jobs = this.options.cronService.listJobs(includeDisabled).map((job) => buildCronJobView(job));
2804
+ return c.json(ok({ jobs, total: jobs.length }));
2805
+ };
2806
+ deleteJob = (c) => {
2807
+ if (!this.options.cronService) {
2808
+ return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
2809
+ }
2810
+ const id = decodeURIComponent(c.req.param("id"));
2811
+ const deleted = this.options.cronService.removeJob(id);
2812
+ if (!deleted) {
2813
+ return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
2814
+ }
2815
+ return c.json(ok({ deleted: true }));
2816
+ };
2817
+ enableJob = async (c) => {
2818
+ if (!this.options.cronService) {
2819
+ return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
2820
+ }
2821
+ const id = decodeURIComponent(c.req.param("id"));
2822
+ const body = await readJson(c.req.raw);
2823
+ if (!body.ok) {
2824
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2825
+ }
2826
+ if (typeof body.data.enabled !== "boolean") {
2827
+ return c.json(err("INVALID_BODY", "enabled must be boolean"), 400);
2828
+ }
2829
+ const job = this.options.cronService.enableJob(id, body.data.enabled);
2830
+ if (!job) {
2831
+ return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
2832
+ }
2833
+ const data = { job: buildCronJobView(job) };
2834
+ return c.json(ok(data));
2835
+ };
2836
+ runJob = async (c) => {
2837
+ if (!this.options.cronService) {
2838
+ return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
2839
+ }
2840
+ const id = decodeURIComponent(c.req.param("id"));
2841
+ const body = await readJson(c.req.raw);
2842
+ if (!body.ok) {
2843
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2844
+ }
2845
+ const existing = findCronJob(this.options.cronService, id);
2846
+ if (!existing) {
2847
+ return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
2848
+ }
2849
+ const executed = await this.options.cronService.runJob(id, Boolean(body.data.force));
2850
+ const after = findCronJob(this.options.cronService, id);
2851
+ const data = {
2852
+ job: after ? buildCronJobView(after) : null,
2853
+ executed
2854
+ };
2855
+ return c.json(ok(data));
2856
+ };
2857
+ };
2858
+
2859
+ // src/ui/router/marketplace/constants.ts
2860
+ var DEFAULT_MARKETPLACE_API_BASE = "https://marketplace-api.nextclaw.io";
2861
+ var NEXTCLAW_PLUGIN_NPM_PREFIX = "@nextclaw/channel-plugin-";
2862
+ var CLAWBAY_CHANNEL_PLUGIN_NPM_SPEC = "@clawbay/clawbay-channel";
2863
+ var BUILTIN_CHANNEL_PLUGIN_ID_PREFIX = "builtin-channel-";
2864
+ var MARKETPLACE_REMOTE_PAGE_SIZE = 100;
2865
+ var MARKETPLACE_REMOTE_MAX_PAGES = 20;
2866
+ var MARKETPLACE_ZH_COPY_BY_SLUG = {
2867
+ weather: {
2868
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002",
2869
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u5FEB\u901F\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002"
2870
+ },
2871
+ summarize: {
2872
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7ED3\u6784\u5316\u6458\u8981\u3002",
2873
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u6587\u4EF6\u4E0E\u957F\u6587\u672C\u7684\u6458\u8981\u5DE5\u4F5C\u6D41\u3002"
2874
+ },
2875
+ github: {
2876
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E GitHub \u5DE5\u4F5C\u6D41\u3002",
2877
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B Issue\u3001PR \u4E0E\u4ED3\u5E93\u76F8\u5173\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
2878
+ },
2879
+ tmux: {
2880
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7EC8\u7AEF/Tmux \u534F\u4F5C\u5DE5\u4F5C\u6D41\u3002",
2881
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u57FA\u4E8E Tmux \u7684\u4EFB\u52A1\u6267\u884C\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
2882
+ },
2883
+ gog: {
2884
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u56FE\u8C31\u5BFC\u5411\u751F\u6210\u5DE5\u4F5C\u6D41\u3002",
2885
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u56FE\u8C31\u4E0E\u89C4\u5212\u5BFC\u5411\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
2886
+ },
2887
+ pdf: {
2888
+ summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E PDF \u8BFB\u53D6/\u5408\u5E76/\u62C6\u5206/OCR \u5DE5\u4F5C\u6D41\u3002",
2889
+ description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u8BFB\u53D6\u3001\u63D0\u53D6\u3001\u5408\u5E76\u3001\u62C6\u5206\u3001\u65CB\u8F6C\u5E76\u5BF9 PDF \u6267\u884C OCR \u5904\u7406\u3002"
2890
+ },
2891
+ docx: {
2892
+ summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u521B\u5EFA\u548C\u7F16\u8F91 Word \u6587\u6863\u3002",
2893
+ description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u8BFB\u53D6\u3001\u7F16\u8F91\u5E76\u91CD\u6784 .docx \u6587\u6863\u3002"
2894
+ },
2895
+ pptx: {
2896
+ summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u6F14\u793A\u6587\u7A3F\u64CD\u4F5C\u3002",
2897
+ description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u89E3\u6790\u3001\u7F16\u8F91\u5E76\u91CD\u7EC4 .pptx \u6F14\u793A\u6587\u7A3F\u3002"
2898
+ },
2899
+ xlsx: {
2900
+ summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u8868\u683C\u6587\u6863\u5DE5\u4F5C\u6D41\u3002",
2901
+ description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u6253\u5F00\u3001\u7F16\u8F91\u3001\u6E05\u6D17\u5E76\u8F6C\u6362 .xlsx \u4E0E .csv \u7B49\u8868\u683C\u6587\u4EF6\u3002"
2902
+ },
2903
+ bird: {
2904
+ summary: "OpenClaw \u793E\u533A\u6280\u80FD\uFF0C\u7528\u4E8E X/Twitter \u8BFB\u53D6/\u641C\u7D22/\u53D1\u5E03\u5DE5\u4F5C\u6D41\u3002",
2905
+ description: "\u4F7F\u7528 bird CLI \u5728\u4EE3\u7406\u5DE5\u4F5C\u6D41\u4E2D\u8BFB\u53D6\u7EBF\u7A0B\u3001\u641C\u7D22\u5E16\u5B50\u5E76\u8D77\u8349\u63A8\u6587/\u56DE\u590D\u3002"
2906
+ },
2907
+ "cloudflare-deploy": {
2908
+ summary: "OpenAI \u7CBE\u9009\u6280\u80FD\uFF0C\u7528\u4E8E\u5728 Cloudflare \u4E0A\u90E8\u7F72\u5E94\u7528\u4E0E\u57FA\u7840\u8BBE\u65BD\u3002",
2909
+ description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u9009\u62E9 Cloudflare \u4EA7\u54C1\u5E76\u90E8\u7F72 Workers\u3001Pages \u53CA\u76F8\u5173\u670D\u52A1\u3002"
2910
+ },
2911
+ "channel-plugin-discord": {
2912
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Discord \u6E20\u9053\u96C6\u6210\u3002",
2913
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Discord \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2914
+ },
2915
+ "channel-plugin-telegram": {
2916
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Telegram \u6E20\u9053\u96C6\u6210\u3002",
2917
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Telegram \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2918
+ },
2919
+ "channel-plugin-slack": {
2920
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Slack \u6E20\u9053\u96C6\u6210\u3002",
2921
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Slack \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2922
+ },
2923
+ "channel-plugin-wecom": {
2924
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u96C6\u6210\u3002",
2925
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2926
+ },
2927
+ "channel-plugin-email": {
2928
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Email \u6E20\u9053\u96C6\u6210\u3002",
2929
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Email \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2930
+ },
2931
+ "channel-plugin-whatsapp": {
2932
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E WhatsApp \u6E20\u9053\u96C6\u6210\u3002",
2933
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B WhatsApp \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2934
+ },
2935
+ "channel-plugin-clawbay": {
2936
+ summary: "Clawbay \u5B98\u65B9\u6E20\u9053\u63D2\u4EF6\uFF0C\u7528\u4E8E NextClaw \u96C6\u6210\u3002",
2937
+ description: "\u901A\u8FC7\u63D2\u4EF6\u8FD0\u884C\u65F6\u4E3A NextClaw \u63D0\u4F9B Clawbay \u6E20\u9053\u80FD\u529B\u3002"
2938
+ }
2939
+ };
2940
+
2941
+ // src/ui/router/marketplace/catalog.ts
2942
+ function normalizeMarketplaceBaseUrl(options) {
2943
+ const configured = options.marketplace?.apiBaseUrl?.trim();
2944
+ if (!configured) {
2945
+ return DEFAULT_MARKETPLACE_API_BASE;
2946
+ }
2947
+ return configured.replace(/\/$/, "");
2948
+ }
2949
+ function toMarketplaceUrl(baseUrl, path, query = {}) {
2950
+ const url = new URL(path, `${baseUrl.replace(/\/$/, "")}/`);
2951
+ for (const [key, value] of Object.entries(query)) {
2952
+ if (typeof value === "string" && value.length > 0) {
2953
+ url.searchParams.set(key, value);
2954
+ }
2955
+ }
2956
+ return url.toString();
2957
+ }
2958
+ async function fetchMarketplaceData(params) {
2959
+ const endpoint = toMarketplaceUrl(params.baseUrl, params.path, params.query);
2960
+ let response;
2961
+ try {
2962
+ response = await fetch(endpoint, {
2963
+ method: "GET",
2964
+ headers: {
2965
+ Accept: "application/json"
2966
+ }
2967
+ });
1843
2968
  } catch (error) {
1844
- const message = error instanceof Error ? error.message : String(error);
1845
- throw new Error(`invalid CLI credential JSON: ${message}`);
2969
+ return {
2970
+ ok: false,
2971
+ status: 503,
2972
+ message: error instanceof Error ? error.message : String(error)
2973
+ };
1846
2974
  }
1847
- const accessToken = readFieldAsString(payload, spec.auth.cliCredential.accessTokenField);
1848
- if (!accessToken) {
1849
- throw new Error(
1850
- `CLI credential missing access token field: ${spec.auth.cliCredential.accessTokenField}`
1851
- );
2975
+ let payload = null;
2976
+ try {
2977
+ payload = await response.json();
2978
+ } catch {
2979
+ if (!response.ok) {
2980
+ return {
2981
+ ok: false,
2982
+ status: response.status,
2983
+ message: `marketplace request failed (${response.status})`
2984
+ };
2985
+ }
2986
+ return {
2987
+ ok: false,
2988
+ status: 502,
2989
+ message: "invalid marketplace response"
2990
+ };
1852
2991
  }
1853
- const expiresAtMs = normalizeExpiresAt(
1854
- spec.auth.cliCredential.expiresAtField ? payload[spec.auth.cliCredential.expiresAtField] : void 0
1855
- );
1856
- if (typeof expiresAtMs === "number" && expiresAtMs <= Date.now()) {
1857
- throw new Error("CLI credential has expired, please login again");
2992
+ if (!response.ok) {
2993
+ return {
2994
+ ok: false,
2995
+ status: response.status,
2996
+ message: readErrorMessage(payload, `marketplace request failed (${response.status})`)
2997
+ };
2998
+ }
2999
+ if (!payload || typeof payload !== "object" || !("ok" in payload)) {
3000
+ return {
3001
+ ok: false,
3002
+ status: 502,
3003
+ message: "invalid marketplace response"
3004
+ };
3005
+ }
3006
+ const typed = payload;
3007
+ if (!typed.ok) {
3008
+ return {
3009
+ ok: false,
3010
+ status: 502,
3011
+ message: readErrorMessage(payload, "marketplace response returned error")
3012
+ };
1858
3013
  }
1859
- setProviderApiKey({
1860
- configPath,
1861
- provider: providerName,
1862
- accessToken,
1863
- defaultApiBase: spec.defaultApiBase
1864
- });
1865
3014
  return {
1866
- provider: providerName,
1867
- status: "imported",
1868
- source: "cli",
1869
- expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
3015
+ ok: true,
3016
+ data: typed.data
1870
3017
  };
1871
3018
  }
1872
-
1873
- // src/ui/router.ts
1874
- function buildAppMetaView(options) {
1875
- const productVersion = options.productVersion?.trim();
1876
- return {
1877
- name: "NextClaw",
1878
- productVersion: productVersion && productVersion.length > 0 ? productVersion : "0.0.0"
3019
+ function sanitizeMarketplaceItem(item) {
3020
+ const next = { ...item };
3021
+ delete next.sourceType;
3022
+ return next;
3023
+ }
3024
+ function readLocalizedMap(value) {
3025
+ const localized = {};
3026
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3027
+ return localized;
3028
+ }
3029
+ for (const [key, entry] of Object.entries(value)) {
3030
+ if (typeof entry !== "string" || entry.trim().length === 0) {
3031
+ continue;
3032
+ }
3033
+ localized[key] = entry.trim();
3034
+ }
3035
+ return localized;
3036
+ }
3037
+ function normalizeLocaleTag(value) {
3038
+ return value.trim().toLowerCase().replace(/_/g, "-");
3039
+ }
3040
+ function pickLocaleFamilyValue(localized, localeFamily) {
3041
+ const normalizedFamily = normalizeLocaleTag(localeFamily).split("-")[0];
3042
+ if (!normalizedFamily) {
3043
+ return void 0;
3044
+ }
3045
+ let familyMatch;
3046
+ for (const [locale, text] of Object.entries(localized)) {
3047
+ const normalizedLocale = normalizeLocaleTag(locale);
3048
+ if (!normalizedLocale) {
3049
+ continue;
3050
+ }
3051
+ if (normalizedLocale === normalizedFamily) {
3052
+ return text;
3053
+ }
3054
+ if (!familyMatch && normalizedLocale.startsWith(`${normalizedFamily}-`)) {
3055
+ familyMatch = text;
3056
+ }
3057
+ }
3058
+ return familyMatch;
3059
+ }
3060
+ function normalizeLocalizedTextMap(primaryText, localized, zhFallback) {
3061
+ const next = readLocalizedMap(localized);
3062
+ if (!next.en) {
3063
+ next.en = pickLocaleFamilyValue(next, "en") ?? primaryText;
3064
+ }
3065
+ if (!next.zh) {
3066
+ next.zh = pickLocaleFamilyValue(next, "zh") ?? (zhFallback && zhFallback.trim().length > 0 ? zhFallback.trim() : next.en);
3067
+ }
3068
+ return next;
3069
+ }
3070
+ function normalizeMarketplaceItemForUi(item) {
3071
+ const zhCopy = MARKETPLACE_ZH_COPY_BY_SLUG[item.slug];
3072
+ const next = {
3073
+ ...item,
3074
+ summaryI18n: normalizeLocalizedTextMap(item.summary, item.summaryI18n, zhCopy?.summary)
1879
3075
  };
3076
+ if ("description" in item && typeof item.description === "string" && item.description.trim().length > 0) {
3077
+ next.descriptionI18n = normalizeLocalizedTextMap(
3078
+ item.description,
3079
+ item.descriptionI18n,
3080
+ zhCopy?.description
3081
+ );
3082
+ }
3083
+ return next;
1880
3084
  }
1881
- var DEFAULT_MARKETPLACE_API_BASE = "https://marketplace-api.nextclaw.io";
1882
- var NEXTCLAW_PLUGIN_NPM_PREFIX = "@nextclaw/channel-plugin-";
1883
- var CLAWBAY_CHANNEL_PLUGIN_NPM_SPEC = "@clawbay/clawbay-channel";
1884
- var BUILTIN_CHANNEL_PLUGIN_ID_PREFIX = "builtin-channel-";
1885
- var MARKETPLACE_REMOTE_PAGE_SIZE = 100;
1886
- var MARKETPLACE_REMOTE_MAX_PAGES = 20;
1887
- var getWorkspacePathFromConfig3 = NextclawCore.getWorkspacePathFromConfig;
1888
- function createSkillsLoader(workspace) {
1889
- const ctor = NextclawCore.SkillsLoader;
1890
- if (!ctor) {
1891
- return null;
3085
+ function toPositiveInt(raw, fallback) {
3086
+ if (!raw) {
3087
+ return fallback;
1892
3088
  }
1893
- return new ctor(workspace);
3089
+ const parsed = Number.parseInt(raw, 10);
3090
+ if (!Number.isFinite(parsed) || parsed <= 0) {
3091
+ return fallback;
3092
+ }
3093
+ return parsed;
3094
+ }
3095
+ async function fetchAllMarketplaceItems(params) {
3096
+ const items = [];
3097
+ let sort = "relevance";
3098
+ let query;
3099
+ for (let page = 1; page <= MARKETPLACE_REMOTE_MAX_PAGES; page += 1) {
3100
+ const result = await fetchMarketplaceData({
3101
+ baseUrl: params.baseUrl,
3102
+ path: params.path,
3103
+ query: {
3104
+ ...params.query,
3105
+ page: String(page),
3106
+ pageSize: String(MARKETPLACE_REMOTE_PAGE_SIZE)
3107
+ }
3108
+ });
3109
+ if (!result.ok) {
3110
+ return result;
3111
+ }
3112
+ const pageItems = Array.isArray(result.data.items) ? result.data.items : [];
3113
+ if (pageItems.length === 0) {
3114
+ break;
3115
+ }
3116
+ sort = result.data.sort;
3117
+ query = result.data.query;
3118
+ items.push(...pageItems);
3119
+ const pageSize = typeof result.data.pageSize === "number" && Number.isFinite(result.data.pageSize) && result.data.pageSize > 0 ? result.data.pageSize : MARKETPLACE_REMOTE_PAGE_SIZE;
3120
+ if (pageItems.length < pageSize) {
3121
+ break;
3122
+ }
3123
+ }
3124
+ return {
3125
+ ok: true,
3126
+ data: {
3127
+ sort,
3128
+ ...typeof query === "string" ? { query } : {},
3129
+ items
3130
+ }
3131
+ };
3132
+ }
3133
+ async function fetchAllPluginMarketplaceItems(params) {
3134
+ return fetchAllMarketplaceItems({
3135
+ baseUrl: params.baseUrl,
3136
+ path: "/api/v1/plugins/items",
3137
+ query: params.query
3138
+ });
1894
3139
  }
3140
+ async function fetchAllSkillMarketplaceItems(params) {
3141
+ return fetchAllMarketplaceItems({
3142
+ baseUrl: params.baseUrl,
3143
+ path: "/api/v1/skills/items",
3144
+ query: params.query
3145
+ });
3146
+ }
3147
+ function sanitizeMarketplaceListItems(items) {
3148
+ return items.map((item) => sanitizeMarketplaceItem(item));
3149
+ }
3150
+ function sanitizeMarketplaceItemView(item) {
3151
+ return sanitizeMarketplaceItem(item);
3152
+ }
3153
+
3154
+ // src/ui/router/marketplace/installed.ts
3155
+ import * as NextclawCore3 from "@nextclaw/core";
3156
+ import { buildPluginStatusReport } from "@nextclaw/openclaw-compat";
3157
+
3158
+ // src/ui/router/marketplace/spec.ts
1895
3159
  function normalizePluginNpmSpec(rawSpec) {
1896
3160
  const spec = rawSpec.trim();
1897
3161
  if (!spec.startsWith("@")) {
@@ -1940,308 +3204,68 @@ function readPluginOriginPriority(origin) {
1940
3204
  if (origin === "bundled") {
1941
3205
  return 80;
1942
3206
  }
1943
- if (origin === "workspace") {
1944
- return 70;
1945
- }
1946
- if (origin === "global") {
1947
- return 60;
1948
- }
1949
- if (origin === "config") {
1950
- return 50;
1951
- }
1952
- return 10;
1953
- }
1954
- function readInstalledPluginRecordPriority(record) {
1955
- const installScore = record.installPath ? 20 : 0;
1956
- const timestampScore = record.installedAt ? 10 : 0;
1957
- return readPluginRuntimeStatusPriority(record.runtimeStatus) + readPluginOriginPriority(record.origin) + installScore + timestampScore;
1958
- }
1959
- function mergeInstalledPluginRecords(primary, secondary) {
1960
- return {
1961
- ...primary,
1962
- id: primary.id ?? secondary.id,
1963
- label: primary.label ?? secondary.label,
1964
- source: primary.source ?? secondary.source,
1965
- installedAt: primary.installedAt ?? secondary.installedAt,
1966
- enabled: primary.enabled ?? secondary.enabled,
1967
- runtimeStatus: primary.runtimeStatus ?? secondary.runtimeStatus,
1968
- origin: primary.origin ?? secondary.origin,
1969
- installPath: primary.installPath ?? secondary.installPath
1970
- };
1971
- }
1972
- function dedupeInstalledPluginRecordsByCanonicalSpec(records) {
1973
- const deduped = /* @__PURE__ */ new Map();
1974
- for (const record of records) {
1975
- const canonicalSpec = normalizePluginNpmSpec(record.spec).trim();
1976
- if (!canonicalSpec) {
1977
- continue;
1978
- }
1979
- const key = canonicalSpec.toLowerCase();
1980
- const normalizedRecord = { ...record, spec: canonicalSpec };
1981
- const existing = deduped.get(key);
1982
- if (!existing) {
1983
- deduped.set(key, normalizedRecord);
1984
- continue;
1985
- }
1986
- const normalizedScore = readInstalledPluginRecordPriority(normalizedRecord);
1987
- const existingScore = readInstalledPluginRecordPriority(existing);
1988
- if (normalizedScore > existingScore) {
1989
- deduped.set(key, mergeInstalledPluginRecords(normalizedRecord, existing));
1990
- continue;
1991
- }
1992
- deduped.set(key, mergeInstalledPluginRecords(existing, normalizedRecord));
1993
- }
1994
- return Array.from(deduped.values());
1995
- }
1996
- function ok(data) {
1997
- return { ok: true, data };
1998
- }
1999
- function err(code, message, details) {
2000
- return { ok: false, error: { code, message, details } };
2001
- }
2002
- function toIsoTime(value) {
2003
- if (typeof value !== "number" || !Number.isFinite(value)) {
2004
- return null;
2005
- }
2006
- const date = new Date(value);
2007
- if (Number.isNaN(date.getTime())) {
2008
- return null;
2009
- }
2010
- return date.toISOString();
2011
- }
2012
- function buildCronJobView(job) {
2013
- return {
2014
- id: job.id,
2015
- name: job.name,
2016
- enabled: job.enabled,
2017
- schedule: job.schedule,
2018
- payload: job.payload,
2019
- state: {
2020
- nextRunAt: toIsoTime(job.state.nextRunAtMs),
2021
- lastRunAt: toIsoTime(job.state.lastRunAtMs),
2022
- lastStatus: job.state.lastStatus ?? null,
2023
- lastError: job.state.lastError ?? null
2024
- },
2025
- createdAt: new Date(job.createdAtMs).toISOString(),
2026
- updatedAt: new Date(job.updatedAtMs).toISOString(),
2027
- deleteAfterRun: job.deleteAfterRun
2028
- };
2029
- }
2030
- function findCronJob(service, id) {
2031
- const jobs = service.listJobs(true);
2032
- return jobs.find((job) => job.id === id) ?? null;
2033
- }
2034
- async function readJson(req) {
2035
- try {
2036
- const data = await req.json();
2037
- return { ok: true, data };
2038
- } catch {
2039
- return { ok: false };
2040
- }
2041
- }
2042
- function isRecord(value) {
2043
- return typeof value === "object" && value !== null && !Array.isArray(value);
2044
- }
2045
- function readErrorMessage(value, fallback) {
2046
- if (!isRecord(value)) {
2047
- return fallback;
2048
- }
2049
- const maybeError = value.error;
2050
- if (!isRecord(maybeError)) {
2051
- return fallback;
2052
- }
2053
- return typeof maybeError.message === "string" && maybeError.message.trim().length > 0 ? maybeError.message : fallback;
2054
- }
2055
- function readNonEmptyString(value) {
2056
- if (typeof value !== "string") {
2057
- return void 0;
2058
- }
2059
- const trimmed = value.trim();
2060
- return trimmed || void 0;
2061
- }
2062
- function formatUserFacingError(error, maxChars = 320) {
2063
- const raw = error instanceof Error ? error.message || error.name || "Unknown error" : String(error ?? "Unknown error");
2064
- const normalized = raw.replace(/\s+/g, " ").trim();
2065
- if (!normalized) {
2066
- return "Unknown error";
2067
- }
2068
- if (normalized.length <= maxChars) {
2069
- return normalized;
2070
- }
2071
- return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
2072
- }
2073
- function normalizeSessionType2(value) {
2074
- return readNonEmptyString(value)?.toLowerCase();
2075
- }
2076
- function resolveSessionTypeLabel(sessionType) {
2077
- if (sessionType === "native") {
2078
- return "Native";
2079
- }
2080
- if (sessionType === "codex-sdk") {
2081
- return "Codex";
2082
- }
2083
- if (sessionType === "claude-agent-sdk") {
2084
- return "Claude Code";
2085
- }
2086
- return sessionType;
2087
- }
2088
- async function buildChatSessionTypesView(chatRuntime) {
2089
- if (!chatRuntime?.listSessionTypes) {
2090
- return {
2091
- defaultType: DEFAULT_SESSION_TYPE,
2092
- options: [{ value: DEFAULT_SESSION_TYPE, label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE) }]
2093
- };
2094
- }
2095
- const payload = await chatRuntime.listSessionTypes();
2096
- const deduped = /* @__PURE__ */ new Map();
2097
- for (const rawOption of payload.options ?? []) {
2098
- const normalized = normalizeSessionType2(rawOption.value);
2099
- if (!normalized) {
2100
- continue;
2101
- }
2102
- deduped.set(normalized, {
2103
- value: normalized,
2104
- label: readNonEmptyString(rawOption.label) ?? resolveSessionTypeLabel(normalized)
2105
- });
2106
- }
2107
- if (!deduped.has(DEFAULT_SESSION_TYPE)) {
2108
- deduped.set(DEFAULT_SESSION_TYPE, {
2109
- value: DEFAULT_SESSION_TYPE,
2110
- label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE)
2111
- });
2112
- }
2113
- const defaultType = normalizeSessionType2(payload.defaultType) ?? DEFAULT_SESSION_TYPE;
2114
- if (!deduped.has(defaultType)) {
2115
- deduped.set(defaultType, {
2116
- value: defaultType,
2117
- label: resolveSessionTypeLabel(defaultType)
2118
- });
2119
- }
2120
- const options = Array.from(deduped.values()).sort((left, right) => {
2121
- if (left.value === DEFAULT_SESSION_TYPE) {
2122
- return -1;
2123
- }
2124
- if (right.value === DEFAULT_SESSION_TYPE) {
2125
- return 1;
2126
- }
2127
- return left.value.localeCompare(right.value);
2128
- });
2129
- return {
2130
- defaultType,
2131
- options
2132
- };
2133
- }
2134
- function resolveAgentIdFromSessionKey(sessionKey) {
2135
- const parsed = NextclawCore.parseAgentScopedSessionKey(sessionKey);
2136
- const agentId = readNonEmptyString(parsed?.agentId);
2137
- return agentId;
2138
- }
2139
- function createChatRunId() {
2140
- const now = Date.now().toString(36);
2141
- const rand = Math.random().toString(36).slice(2, 10);
2142
- return `run-${now}-${rand}`;
2143
- }
2144
- function isChatRunState(value) {
2145
- return value === "queued" || value === "running" || value === "completed" || value === "failed" || value === "aborted";
2146
- }
2147
- function readChatRunStates(value) {
2148
- if (typeof value !== "string") {
2149
- return void 0;
2150
- }
2151
- const values = value.split(",").map((item) => item.trim().toLowerCase()).filter((item) => Boolean(item) && isChatRunState(item));
2152
- if (values.length === 0) {
2153
- return void 0;
2154
- }
2155
- return Array.from(new Set(values));
2156
- }
2157
- function buildChatTurnView(params) {
2158
- const completedAt = /* @__PURE__ */ new Date();
2159
- return {
2160
- reply: String(params.result.reply ?? ""),
2161
- sessionKey: readNonEmptyString(params.result.sessionKey) ?? params.fallbackSessionKey,
2162
- ...readNonEmptyString(params.result.agentId) || params.requestedAgentId ? { agentId: readNonEmptyString(params.result.agentId) ?? params.requestedAgentId } : {},
2163
- ...readNonEmptyString(params.result.model) || params.requestedModel ? { model: readNonEmptyString(params.result.model) ?? params.requestedModel } : {},
2164
- requestedAt: params.requestedAt.toISOString(),
2165
- completedAt: completedAt.toISOString(),
2166
- durationMs: Math.max(0, completedAt.getTime() - params.startedAtMs)
2167
- };
2168
- }
2169
- function buildChatTurnViewFromRun(params) {
2170
- const requestedAt = readNonEmptyString(params.run.requestedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
2171
- const completedAt = readNonEmptyString(params.run.completedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
2172
- const requestedAtMs = Date.parse(requestedAt);
2173
- const completedAtMs = Date.parse(completedAt);
2174
- return {
2175
- reply: readNonEmptyString(params.run.reply) ?? params.fallbackReply ?? "",
2176
- sessionKey: readNonEmptyString(params.run.sessionKey) ?? params.fallbackSessionKey,
2177
- ...readNonEmptyString(params.run.agentId) || params.fallbackAgentId ? { agentId: readNonEmptyString(params.run.agentId) ?? params.fallbackAgentId } : {},
2178
- ...readNonEmptyString(params.run.model) || params.fallbackModel ? { model: readNonEmptyString(params.run.model) ?? params.fallbackModel } : {},
2179
- requestedAt,
2180
- completedAt,
2181
- durationMs: Number.isFinite(requestedAtMs) && Number.isFinite(completedAtMs) ? Math.max(0, completedAtMs - requestedAtMs) : 0
2182
- };
2183
- }
2184
- function toSseFrame(event, data) {
2185
- return `event: ${event}
2186
- data: ${JSON.stringify(data)}
2187
-
2188
- `;
3207
+ if (origin === "workspace") {
3208
+ return 70;
3209
+ }
3210
+ if (origin === "global") {
3211
+ return 60;
3212
+ }
3213
+ if (origin === "config") {
3214
+ return 50;
3215
+ }
3216
+ return 10;
2189
3217
  }
2190
- function normalizeMarketplaceBaseUrl(options) {
2191
- const fromOptions = options.marketplace?.apiBaseUrl?.trim();
2192
- const fromEnv = process.env.NEXTCLAW_MARKETPLACE_API_BASE?.trim();
2193
- const value = fromOptions || fromEnv || DEFAULT_MARKETPLACE_API_BASE;
2194
- return value.endsWith("/") ? value.slice(0, -1) : value;
3218
+ function readInstalledPluginRecordPriority(record) {
3219
+ const installScore = record.installPath ? 20 : 0;
3220
+ const timestampScore = record.installedAt ? 10 : 0;
3221
+ return readPluginRuntimeStatusPriority(record.runtimeStatus) + readPluginOriginPriority(record.origin) + installScore + timestampScore;
2195
3222
  }
2196
- function toMarketplaceUrl(baseUrl, path, query = {}) {
2197
- const url = new URL(path, `${baseUrl}/`);
2198
- for (const [key, value] of Object.entries(query)) {
2199
- if (typeof value === "string" && value.trim().length > 0) {
2200
- url.searchParams.set(key, value);
2201
- }
2202
- }
2203
- return url.toString();
3223
+ function mergeInstalledPluginRecords(primary, secondary) {
3224
+ return {
3225
+ ...primary,
3226
+ id: primary.id ?? secondary.id,
3227
+ label: primary.label ?? secondary.label,
3228
+ source: primary.source ?? secondary.source,
3229
+ installedAt: primary.installedAt ?? secondary.installedAt,
3230
+ enabled: primary.enabled ?? secondary.enabled,
3231
+ runtimeStatus: primary.runtimeStatus ?? secondary.runtimeStatus,
3232
+ origin: primary.origin ?? secondary.origin,
3233
+ installPath: primary.installPath ?? secondary.installPath
3234
+ };
2204
3235
  }
2205
- async function fetchMarketplaceData(params) {
2206
- const url = toMarketplaceUrl(params.baseUrl, params.path, params.query ?? {});
2207
- try {
2208
- const response = await fetch(url, {
2209
- method: "GET",
2210
- headers: {
2211
- Accept: "application/json"
2212
- }
2213
- });
2214
- let payload = null;
2215
- try {
2216
- payload = await response.json();
2217
- } catch {
2218
- payload = null;
3236
+ function dedupeInstalledPluginRecordsByCanonicalSpec(records) {
3237
+ const deduped = /* @__PURE__ */ new Map();
3238
+ for (const record of records) {
3239
+ const canonicalSpec = normalizePluginNpmSpec(record.spec).trim();
3240
+ if (!canonicalSpec) {
3241
+ continue;
2219
3242
  }
2220
- if (!response.ok) {
2221
- return {
2222
- ok: false,
2223
- status: response.status,
2224
- message: readErrorMessage(payload, `marketplace request failed: ${response.status}`)
2225
- };
3243
+ const key = canonicalSpec.toLowerCase();
3244
+ const normalizedRecord = { ...record, spec: canonicalSpec };
3245
+ const existing = deduped.get(key);
3246
+ if (!existing) {
3247
+ deduped.set(key, normalizedRecord);
3248
+ continue;
2226
3249
  }
2227
- if (!isRecord(payload) || payload.ok !== true || !Object.prototype.hasOwnProperty.call(payload, "data")) {
2228
- return {
2229
- ok: false,
2230
- status: 502,
2231
- message: "invalid marketplace response"
2232
- };
3250
+ const normalizedScore = readInstalledPluginRecordPriority(normalizedRecord);
3251
+ const existingScore = readInstalledPluginRecordPriority(existing);
3252
+ if (normalizedScore > existingScore) {
3253
+ deduped.set(key, mergeInstalledPluginRecords(normalizedRecord, existing));
3254
+ continue;
2233
3255
  }
2234
- return {
2235
- ok: true,
2236
- data: payload.data
2237
- };
2238
- } catch (error) {
2239
- return {
2240
- ok: false,
2241
- status: 502,
2242
- message: `marketplace fetch failed: ${String(error)}`
2243
- };
3256
+ deduped.set(key, mergeInstalledPluginRecords(existing, normalizedRecord));
3257
+ }
3258
+ return Array.from(deduped.values());
3259
+ }
3260
+
3261
+ // src/ui/router/marketplace/installed.ts
3262
+ var getWorkspacePathFromConfig3 = NextclawCore3.getWorkspacePathFromConfig;
3263
+ function createSkillsLoader(workspace) {
3264
+ const ctor = NextclawCore3.SkillsLoader;
3265
+ if (!ctor) {
3266
+ return null;
2244
3267
  }
3268
+ return new ctor(workspace);
2245
3269
  }
2246
3270
  function collectInstalledPluginRecords(options) {
2247
3271
  const config = loadConfigOrDefault(options.configPath);
@@ -2393,191 +3417,41 @@ function collectSkillMarketplaceInstalledView(options) {
2393
3417
  type: "skill",
2394
3418
  total: installed.records.length,
2395
3419
  specs: installed.specs,
2396
- records: installed.records
2397
- };
2398
- }
2399
- function resolvePluginManageTargetId(options, rawTargetId, rawSpec) {
2400
- const targetId = rawTargetId.trim();
2401
- if (!targetId && !rawSpec) {
2402
- return rawTargetId;
2403
- }
2404
- const normalizedTarget = targetId ? normalizePluginNpmSpec(targetId).toLowerCase() : "";
2405
- const normalizedSpec = rawSpec ? normalizePluginNpmSpec(rawSpec).toLowerCase() : "";
2406
- const pluginRecords = collectInstalledPluginRecords(options).records;
2407
- const lowerTargetId = targetId.toLowerCase();
2408
- for (const record of pluginRecords) {
2409
- const recordId = record.id?.trim();
2410
- if (recordId && recordId.toLowerCase() === lowerTargetId) {
2411
- return recordId;
2412
- }
2413
- }
2414
- if (normalizedTarget) {
2415
- for (const record of pluginRecords) {
2416
- const normalizedRecordSpec = normalizePluginNpmSpec(record.spec).toLowerCase();
2417
- if (normalizedRecordSpec === normalizedTarget && record.id && record.id.trim().length > 0) {
2418
- return record.id;
2419
- }
2420
- }
2421
- }
2422
- if (normalizedSpec && normalizedSpec !== normalizedTarget) {
2423
- for (const record of pluginRecords) {
2424
- const normalizedRecordSpec = normalizePluginNpmSpec(record.spec).toLowerCase();
2425
- if (normalizedRecordSpec === normalizedSpec && record.id && record.id.trim().length > 0) {
2426
- return record.id;
2427
- }
2428
- }
2429
- }
2430
- return targetId || rawSpec || rawTargetId;
2431
- }
2432
- function sanitizeMarketplaceItem(item) {
2433
- const next = { ...item };
2434
- delete next.metrics;
2435
- return next;
2436
- }
2437
- var MARKETPLACE_ZH_COPY_BY_SLUG = {
2438
- weather: {
2439
- summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002",
2440
- description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u5FEB\u901F\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002"
2441
- },
2442
- summarize: {
2443
- summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7ED3\u6784\u5316\u6458\u8981\u3002",
2444
- description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u6587\u4EF6\u4E0E\u957F\u6587\u672C\u7684\u6458\u8981\u5DE5\u4F5C\u6D41\u3002"
2445
- },
2446
- github: {
2447
- summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E GitHub \u5DE5\u4F5C\u6D41\u3002",
2448
- description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B Issue\u3001PR \u4E0E\u4ED3\u5E93\u76F8\u5173\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
2449
- },
2450
- tmux: {
2451
- summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7EC8\u7AEF/Tmux \u534F\u4F5C\u5DE5\u4F5C\u6D41\u3002",
2452
- description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u57FA\u4E8E Tmux \u7684\u4EFB\u52A1\u6267\u884C\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
2453
- },
2454
- gog: {
2455
- summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u56FE\u8C31\u5BFC\u5411\u751F\u6210\u5DE5\u4F5C\u6D41\u3002",
2456
- description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u56FE\u8C31\u4E0E\u89C4\u5212\u5BFC\u5411\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
2457
- },
2458
- pdf: {
2459
- summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E PDF \u8BFB\u53D6/\u5408\u5E76/\u62C6\u5206/OCR \u5DE5\u4F5C\u6D41\u3002",
2460
- description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u8BFB\u53D6\u3001\u63D0\u53D6\u3001\u5408\u5E76\u3001\u62C6\u5206\u3001\u65CB\u8F6C\u5E76\u5BF9 PDF \u6267\u884C OCR \u5904\u7406\u3002"
2461
- },
2462
- docx: {
2463
- summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u521B\u5EFA\u548C\u7F16\u8F91 Word \u6587\u6863\u3002",
2464
- description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u8BFB\u53D6\u3001\u7F16\u8F91\u5E76\u91CD\u6784 .docx \u6587\u6863\u3002"
2465
- },
2466
- pptx: {
2467
- summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u6F14\u793A\u6587\u7A3F\u64CD\u4F5C\u3002",
2468
- description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u89E3\u6790\u3001\u7F16\u8F91\u5E76\u91CD\u7EC4 .pptx \u6F14\u793A\u6587\u7A3F\u3002"
2469
- },
2470
- xlsx: {
2471
- summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u8868\u683C\u6587\u6863\u5DE5\u4F5C\u6D41\u3002",
2472
- description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u6253\u5F00\u3001\u7F16\u8F91\u3001\u6E05\u6D17\u5E76\u8F6C\u6362 .xlsx \u4E0E .csv \u7B49\u8868\u683C\u6587\u4EF6\u3002"
2473
- },
2474
- bird: {
2475
- summary: "OpenClaw \u793E\u533A\u6280\u80FD\uFF0C\u7528\u4E8E X/Twitter \u8BFB\u53D6/\u641C\u7D22/\u53D1\u5E03\u5DE5\u4F5C\u6D41\u3002",
2476
- description: "\u4F7F\u7528 bird CLI \u5728\u4EE3\u7406\u5DE5\u4F5C\u6D41\u4E2D\u8BFB\u53D6\u7EBF\u7A0B\u3001\u641C\u7D22\u5E16\u5B50\u5E76\u8D77\u8349\u63A8\u6587/\u56DE\u590D\u3002"
2477
- },
2478
- "cloudflare-deploy": {
2479
- summary: "OpenAI \u7CBE\u9009\u6280\u80FD\uFF0C\u7528\u4E8E\u5728 Cloudflare \u4E0A\u90E8\u7F72\u5E94\u7528\u4E0E\u57FA\u7840\u8BBE\u65BD\u3002",
2480
- description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u9009\u62E9 Cloudflare \u4EA7\u54C1\u5E76\u90E8\u7F72 Workers\u3001Pages \u53CA\u76F8\u5173\u670D\u52A1\u3002"
2481
- },
2482
- "channel-plugin-discord": {
2483
- summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Discord \u6E20\u9053\u96C6\u6210\u3002",
2484
- description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Discord \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2485
- },
2486
- "channel-plugin-telegram": {
2487
- summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Telegram \u6E20\u9053\u96C6\u6210\u3002",
2488
- description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Telegram \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2489
- },
2490
- "channel-plugin-slack": {
2491
- summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Slack \u6E20\u9053\u96C6\u6210\u3002",
2492
- description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Slack \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2493
- },
2494
- "channel-plugin-wecom": {
2495
- summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u96C6\u6210\u3002",
2496
- description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2497
- },
2498
- "channel-plugin-email": {
2499
- summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Email \u6E20\u9053\u96C6\u6210\u3002",
2500
- description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Email \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2501
- },
2502
- "channel-plugin-whatsapp": {
2503
- summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E WhatsApp \u6E20\u9053\u96C6\u6210\u3002",
2504
- description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B WhatsApp \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
2505
- },
2506
- "channel-plugin-clawbay": {
2507
- summary: "Clawbay \u5B98\u65B9\u6E20\u9053\u63D2\u4EF6\uFF0C\u7528\u4E8E NextClaw \u96C6\u6210\u3002",
2508
- description: "\u901A\u8FC7\u63D2\u4EF6\u8FD0\u884C\u65F6\u4E3A NextClaw \u63D0\u4F9B Clawbay \u6E20\u9053\u80FD\u529B\u3002"
2509
- }
2510
- };
2511
- function readLocalizedMap(value) {
2512
- const localized = {};
2513
- if (!isRecord(value)) {
2514
- return localized;
2515
- }
2516
- for (const [key, entry] of Object.entries(value)) {
2517
- if (typeof entry !== "string" || entry.trim().length === 0) {
2518
- continue;
2519
- }
2520
- localized[key] = entry.trim();
2521
- }
2522
- return localized;
2523
- }
2524
- function normalizeLocaleTag(value) {
2525
- return value.trim().toLowerCase().replace(/_/g, "-");
2526
- }
2527
- function pickLocaleFamilyValue(localized, localeFamily) {
2528
- const normalizedFamily = normalizeLocaleTag(localeFamily).split("-")[0];
2529
- if (!normalizedFamily) {
2530
- return void 0;
2531
- }
2532
- let familyMatch;
2533
- for (const [locale, text] of Object.entries(localized)) {
2534
- const normalizedLocale = normalizeLocaleTag(locale);
2535
- if (!normalizedLocale) {
2536
- continue;
2537
- }
2538
- if (normalizedLocale === normalizedFamily) {
2539
- return text;
2540
- }
2541
- if (!familyMatch && normalizedLocale.startsWith(`${normalizedFamily}-`)) {
2542
- familyMatch = text;
2543
- }
2544
- }
2545
- return familyMatch;
2546
- }
2547
- function normalizeLocalizedTextMap(primaryText, localized, zhFallback) {
2548
- const next = readLocalizedMap(localized);
2549
- if (!next.en) {
2550
- next.en = pickLocaleFamilyValue(next, "en") ?? primaryText;
2551
- }
2552
- if (!next.zh) {
2553
- next.zh = pickLocaleFamilyValue(next, "zh") ?? (zhFallback && zhFallback.trim().length > 0 ? zhFallback.trim() : next.en);
2554
- }
2555
- return next;
2556
- }
2557
- function normalizeMarketplaceItemForUi(item) {
2558
- const zhCopy = MARKETPLACE_ZH_COPY_BY_SLUG[item.slug];
2559
- const next = {
2560
- ...item,
2561
- summaryI18n: normalizeLocalizedTextMap(item.summary, item.summaryI18n, zhCopy?.summary)
2562
- };
2563
- if ("description" in item && typeof item.description === "string" && item.description.trim().length > 0) {
2564
- next.descriptionI18n = normalizeLocalizedTextMap(
2565
- item.description,
2566
- item.descriptionI18n,
2567
- zhCopy?.description
2568
- );
2569
- }
2570
- return next;
3420
+ records: installed.records
3421
+ };
2571
3422
  }
2572
- function toPositiveInt(raw, fallback) {
2573
- if (!raw) {
2574
- return fallback;
3423
+ function resolvePluginManageTargetId(options, rawTargetId, rawSpec) {
3424
+ const targetId = rawTargetId.trim();
3425
+ if (!targetId && !rawSpec) {
3426
+ return rawTargetId;
2575
3427
  }
2576
- const parsed = Number.parseInt(raw, 10);
2577
- if (!Number.isFinite(parsed) || parsed <= 0) {
2578
- return fallback;
3428
+ const normalizedTarget = targetId ? normalizePluginNpmSpec(targetId).toLowerCase() : "";
3429
+ const normalizedSpec = rawSpec ? normalizePluginNpmSpec(rawSpec).toLowerCase() : "";
3430
+ const pluginRecords = collectInstalledPluginRecords(options).records;
3431
+ const lowerTargetId = targetId.toLowerCase();
3432
+ for (const record of pluginRecords) {
3433
+ const recordId = record.id?.trim();
3434
+ if (recordId && recordId.toLowerCase() === lowerTargetId) {
3435
+ return recordId;
3436
+ }
2579
3437
  }
2580
- return parsed;
3438
+ if (normalizedTarget) {
3439
+ for (const record of pluginRecords) {
3440
+ const normalizedRecordSpec = normalizePluginNpmSpec(record.spec).toLowerCase();
3441
+ if (normalizedRecordSpec === normalizedTarget && record.id && record.id.trim().length > 0) {
3442
+ return record.id;
3443
+ }
3444
+ }
3445
+ }
3446
+ if (normalizedSpec && normalizedSpec !== normalizedTarget) {
3447
+ for (const record of pluginRecords) {
3448
+ const normalizedRecordSpec = normalizePluginNpmSpec(record.spec).toLowerCase();
3449
+ if (normalizedRecordSpec === normalizedSpec && record.id && record.id.trim().length > 0) {
3450
+ return record.id;
3451
+ }
3452
+ }
3453
+ }
3454
+ return targetId || rawSpec || rawTargetId;
2581
3455
  }
2582
3456
  function collectKnownSkillNames(options) {
2583
3457
  const config = loadConfigOrDefault(options.configPath);
@@ -2608,6 +3482,8 @@ function findUnsupportedSkillInstallKind(items) {
2608
3482
  }
2609
3483
  return null;
2610
3484
  }
3485
+
3486
+ // src/ui/router/marketplace/plugin.controller.ts
2611
3487
  async function loadPluginReadmeFromNpm(spec) {
2612
3488
  const encodedSpec = encodeURIComponent(spec);
2613
3489
  const registryUrl = `https://registry.npmjs.org/${encodedSpec}`;
@@ -2673,54 +3549,6 @@ async function buildPluginContentView(item) {
2673
3549
  }, null, 2)
2674
3550
  };
2675
3551
  }
2676
- async function fetchAllMarketplaceItems(params) {
2677
- const allItems = [];
2678
- let remotePage = 1;
2679
- let remoteTotalPages = 1;
2680
- let sort = "relevance";
2681
- let query;
2682
- while (remotePage <= remoteTotalPages && remotePage <= MARKETPLACE_REMOTE_MAX_PAGES) {
2683
- const result = await fetchMarketplaceData({
2684
- baseUrl: params.baseUrl,
2685
- path: `/api/v1/${params.segment}/items`,
2686
- query: {
2687
- ...params.query,
2688
- page: String(remotePage),
2689
- pageSize: String(MARKETPLACE_REMOTE_PAGE_SIZE)
2690
- }
2691
- });
2692
- if (!result.ok) {
2693
- return result;
2694
- }
2695
- allItems.push(...result.data.items);
2696
- remoteTotalPages = result.data.totalPages;
2697
- sort = result.data.sort;
2698
- query = result.data.query;
2699
- remotePage += 1;
2700
- }
2701
- return {
2702
- ok: true,
2703
- data: {
2704
- sort,
2705
- query,
2706
- items: allItems
2707
- }
2708
- };
2709
- }
2710
- async function fetchAllPluginMarketplaceItems(params) {
2711
- return await fetchAllMarketplaceItems({
2712
- baseUrl: params.baseUrl,
2713
- segment: "plugins",
2714
- query: params.query
2715
- });
2716
- }
2717
- async function fetchAllSkillMarketplaceItems(params) {
2718
- return await fetchAllMarketplaceItems({
2719
- baseUrl: params.baseUrl,
2720
- segment: "skills",
2721
- query: params.query
2722
- });
2723
- }
2724
3552
  async function installMarketplacePlugin(params) {
2725
3553
  const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
2726
3554
  if (!spec) {
@@ -2742,33 +3570,6 @@ async function installMarketplacePlugin(params) {
2742
3570
  output: result.output
2743
3571
  };
2744
3572
  }
2745
- async function installMarketplaceSkill(params) {
2746
- const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
2747
- if (!spec) {
2748
- throw new Error("INVALID_BODY:non-empty spec is required");
2749
- }
2750
- const installer = params.options.marketplace?.installer;
2751
- if (!installer) {
2752
- throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
2753
- }
2754
- if (!installer.installSkill) {
2755
- throw new Error("NOT_AVAILABLE:skill installer is not configured");
2756
- }
2757
- const result = await installer.installSkill({
2758
- slug: spec,
2759
- kind: params.body.kind,
2760
- skill: params.body.skill,
2761
- installPath: params.body.installPath,
2762
- force: params.body.force
2763
- });
2764
- params.options.publish({ type: "config.updated", payload: { path: "skills" } });
2765
- return {
2766
- type: "skill",
2767
- spec,
2768
- message: result.message,
2769
- output: result.output
2770
- };
2771
- }
2772
3573
  async function manageMarketplacePlugin(params) {
2773
3574
  const action = params.body.action;
2774
3575
  const requestedTargetId = typeof params.body.id === "string" && params.body.id.trim().length > 0 ? params.body.id.trim() : typeof params.body.spec === "string" && params.body.spec.trim().length > 0 ? params.body.spec.trim() : "";
@@ -2783,197 +3584,42 @@ async function manageMarketplacePlugin(params) {
2783
3584
  }
2784
3585
  let result;
2785
3586
  if (action === "enable") {
2786
- if (!installer.enablePlugin) {
2787
- throw new Error("NOT_AVAILABLE:plugin enable is not configured");
2788
- }
2789
- result = await installer.enablePlugin(targetId);
2790
- } else if (action === "disable") {
2791
- if (!installer.disablePlugin) {
2792
- throw new Error("NOT_AVAILABLE:plugin disable is not configured");
2793
- }
2794
- result = await installer.disablePlugin(targetId);
2795
- } else {
2796
- if (!installer.uninstallPlugin) {
2797
- throw new Error("NOT_AVAILABLE:plugin uninstall is not configured");
2798
- }
2799
- result = await installer.uninstallPlugin(targetId);
2800
- }
2801
- params.options.publish({ type: "config.updated", payload: { path: "plugins" } });
2802
- return {
2803
- type: "plugin",
2804
- action,
2805
- id: targetId,
2806
- message: result.message,
2807
- output: result.output
2808
- };
2809
- }
2810
- async function manageMarketplaceSkill(params) {
2811
- const action = params.body.action;
2812
- const targetId = typeof params.body.id === "string" && params.body.id.trim().length > 0 ? params.body.id.trim() : typeof params.body.spec === "string" && params.body.spec.trim().length > 0 ? params.body.spec.trim() : "";
2813
- if (action !== "uninstall" || !targetId) {
2814
- throw new Error("INVALID_BODY:skill manage requires uninstall action and non-empty id/spec");
2815
- }
2816
- const installer = params.options.marketplace?.installer;
2817
- if (!installer) {
2818
- throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
2819
- }
2820
- if (!installer.uninstallSkill) {
2821
- throw new Error("NOT_AVAILABLE:skill uninstall is not configured");
2822
- }
2823
- const result = await installer.uninstallSkill(targetId);
2824
- params.options.publish({ type: "config.updated", payload: { path: "skills" } });
2825
- return {
2826
- type: "skill",
2827
- action,
2828
- id: targetId,
2829
- message: result.message,
2830
- output: result.output
2831
- };
2832
- }
2833
- function registerPluginMarketplaceRoutes(app, options, marketplaceBaseUrl) {
2834
- app.get("/api/marketplace/plugins/installed", (c) => {
2835
- return c.json(ok(collectPluginMarketplaceInstalledView(options)));
2836
- });
2837
- app.get("/api/marketplace/plugins/items", async (c) => {
2838
- const query = c.req.query();
2839
- const result = await fetchAllPluginMarketplaceItems({
2840
- baseUrl: marketplaceBaseUrl,
2841
- query: {
2842
- q: query.q,
2843
- tag: query.tag,
2844
- sort: query.sort,
2845
- page: query.page,
2846
- pageSize: query.pageSize
2847
- }
2848
- });
2849
- if (!result.ok) {
2850
- return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
2851
- }
2852
- const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplacePluginItem(item));
2853
- const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
2854
- const requestedPage = toPositiveInt(query.page, 1);
2855
- const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
2856
- const currentPage = totalPages === 0 ? 1 : Math.min(requestedPage, totalPages);
2857
- return c.json(ok({
2858
- total: filteredItems.length,
2859
- page: currentPage,
2860
- pageSize,
2861
- totalPages,
2862
- sort: result.data.sort,
2863
- query: result.data.query,
2864
- items: filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize)
2865
- }));
2866
- });
2867
- app.get("/api/marketplace/plugins/items/:slug", async (c) => {
2868
- const slug = encodeURIComponent(c.req.param("slug"));
2869
- const result = await fetchMarketplaceData({
2870
- baseUrl: marketplaceBaseUrl,
2871
- path: `/api/v1/plugins/items/${slug}`
2872
- });
2873
- if (!result.ok) {
2874
- return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
2875
- }
2876
- const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
2877
- if (!isSupportedMarketplacePluginItem(sanitized)) {
2878
- return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
2879
- }
2880
- return c.json(ok(sanitized));
2881
- });
2882
- app.get("/api/marketplace/plugins/items/:slug/content", async (c) => {
2883
- const slug = encodeURIComponent(c.req.param("slug"));
2884
- const result = await fetchMarketplaceData({
2885
- baseUrl: marketplaceBaseUrl,
2886
- path: `/api/v1/plugins/items/${slug}`
2887
- });
2888
- if (!result.ok) {
2889
- return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
2890
- }
2891
- const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
2892
- if (!isSupportedMarketplacePluginItem(sanitized)) {
2893
- return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
2894
- }
2895
- const content = await buildPluginContentView(sanitized);
2896
- return c.json(ok(content));
2897
- });
2898
- app.post("/api/marketplace/plugins/install", async (c) => {
2899
- const body = await readJson(c.req.raw);
2900
- if (!body.ok || !body.data || typeof body.data !== "object") {
2901
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
2902
- }
2903
- if (body.data.type && body.data.type !== "plugin") {
2904
- return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
2905
- }
2906
- try {
2907
- const payload = await installMarketplacePlugin({
2908
- options,
2909
- body: body.data
2910
- });
2911
- return c.json(ok(payload));
2912
- } catch (error) {
2913
- const message = String(error);
2914
- if (message.startsWith("INVALID_BODY:")) {
2915
- return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
2916
- }
2917
- if (message.startsWith("NOT_AVAILABLE:")) {
2918
- return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
2919
- }
2920
- return c.json(err("INSTALL_FAILED", message), 400);
2921
- }
2922
- });
2923
- app.post("/api/marketplace/plugins/manage", async (c) => {
2924
- const body = await readJson(c.req.raw);
2925
- if (!body.ok || !body.data || typeof body.data !== "object") {
2926
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
2927
- }
2928
- if (body.data.type && body.data.type !== "plugin") {
2929
- return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
2930
- }
2931
- try {
2932
- const payload = await manageMarketplacePlugin({
2933
- options,
2934
- body: body.data
2935
- });
2936
- return c.json(ok(payload));
2937
- } catch (error) {
2938
- const message = String(error);
2939
- if (message.startsWith("INVALID_BODY:")) {
2940
- return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
2941
- }
2942
- if (message.startsWith("NOT_AVAILABLE:")) {
2943
- return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
2944
- }
2945
- return c.json(err("MANAGE_FAILED", message), 400);
2946
- }
2947
- });
2948
- app.get("/api/marketplace/plugins/recommendations", async (c) => {
2949
- const query = c.req.query();
2950
- const result = await fetchMarketplaceData({
2951
- baseUrl: marketplaceBaseUrl,
2952
- path: "/api/v1/plugins/recommendations",
2953
- query: {
2954
- scene: query.scene,
2955
- limit: query.limit
2956
- }
2957
- });
2958
- if (!result.ok) {
2959
- return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3587
+ if (!installer.enablePlugin) {
3588
+ throw new Error("NOT_AVAILABLE:plugin enable is not configured");
2960
3589
  }
2961
- const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplacePluginItem(item));
2962
- return c.json(ok({
2963
- ...result.data,
2964
- total: filteredItems.length,
2965
- items: filteredItems
2966
- }));
2967
- });
3590
+ result = await installer.enablePlugin(targetId);
3591
+ } else if (action === "disable") {
3592
+ if (!installer.disablePlugin) {
3593
+ throw new Error("NOT_AVAILABLE:plugin disable is not configured");
3594
+ }
3595
+ result = await installer.disablePlugin(targetId);
3596
+ } else {
3597
+ if (!installer.uninstallPlugin) {
3598
+ throw new Error("NOT_AVAILABLE:plugin uninstall is not configured");
3599
+ }
3600
+ result = await installer.uninstallPlugin(targetId);
3601
+ }
3602
+ params.options.publish({ type: "config.updated", payload: { path: "plugins" } });
3603
+ return {
3604
+ type: "plugin",
3605
+ action,
3606
+ id: targetId,
3607
+ message: result.message,
3608
+ output: result.output
3609
+ };
2968
3610
  }
2969
- function registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl) {
2970
- app.get("/api/marketplace/skills/installed", (c) => {
2971
- return c.json(ok(collectSkillMarketplaceInstalledView(options)));
2972
- });
2973
- app.get("/api/marketplace/skills/items", async (c) => {
3611
+ var PluginMarketplaceController = class {
3612
+ constructor(options, marketplaceBaseUrl) {
3613
+ this.options = options;
3614
+ this.marketplaceBaseUrl = marketplaceBaseUrl;
3615
+ }
3616
+ getInstalled = (c) => {
3617
+ return c.json(ok(collectPluginMarketplaceInstalledView(this.options)));
3618
+ };
3619
+ listItems = async (c) => {
2974
3620
  const query = c.req.query();
2975
- const result = await fetchAllSkillMarketplaceItems({
2976
- baseUrl: marketplaceBaseUrl,
3621
+ const result = await fetchAllPluginMarketplaceItems({
3622
+ baseUrl: this.marketplaceBaseUrl,
2977
3623
  query: {
2978
3624
  q: query.q,
2979
3625
  tag: query.tag,
@@ -2985,16 +3631,7 @@ function registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl) {
2985
3631
  if (!result.ok) {
2986
3632
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
2987
3633
  }
2988
- const normalizedItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item)));
2989
- const unsupportedKind = findUnsupportedSkillInstallKind(normalizedItems);
2990
- if (unsupportedKind) {
2991
- return c.json(
2992
- err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
2993
- 502
2994
- );
2995
- }
2996
- const knownSkillNames = collectKnownSkillNames(options);
2997
- const filteredItems = normalizedItems.filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
3634
+ const filteredItems = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item)).filter((item) => isSupportedMarketplacePluginItem(item));
2998
3635
  const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
2999
3636
  const requestedPage = toPositiveInt(query.page, 1);
3000
3637
  const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
@@ -3008,825 +3645,362 @@ function registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl) {
3008
3645
  query: result.data.query,
3009
3646
  items: filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize)
3010
3647
  }));
3011
- });
3012
- app.get("/api/marketplace/skills/items/:slug", async (c) => {
3013
- const slug = encodeURIComponent(c.req.param("slug"));
3014
- const result = await fetchMarketplaceData({
3015
- baseUrl: marketplaceBaseUrl,
3016
- path: `/api/v1/skills/items/${slug}`
3017
- });
3018
- if (!result.ok) {
3019
- return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3020
- }
3021
- const knownSkillNames = collectKnownSkillNames(options);
3022
- const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
3023
- const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
3024
- if (unsupportedKind) {
3025
- return c.json(
3026
- err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
3027
- 502
3028
- );
3029
- }
3030
- if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
3031
- return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
3032
- }
3033
- return c.json(ok(sanitized));
3034
- });
3035
- app.get("/api/marketplace/skills/items/:slug/content", async (c) => {
3648
+ };
3649
+ getItem = async (c) => {
3036
3650
  const slug = encodeURIComponent(c.req.param("slug"));
3037
3651
  const result = await fetchMarketplaceData({
3038
- baseUrl: marketplaceBaseUrl,
3039
- path: `/api/v1/skills/items/${slug}`
3040
- });
3041
- if (!result.ok) {
3042
- return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3043
- }
3044
- const knownSkillNames = collectKnownSkillNames(options);
3045
- const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
3046
- const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
3047
- if (unsupportedKind) {
3048
- return c.json(
3049
- err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
3050
- 502
3051
- );
3052
- }
3053
- if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
3054
- return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
3055
- }
3056
- const contentResult = await fetchMarketplaceData({
3057
- baseUrl: marketplaceBaseUrl,
3058
- path: `/api/v1/skills/items/${slug}/content`
3059
- });
3060
- if (!contentResult.ok) {
3061
- return c.json(err("MARKETPLACE_UNAVAILABLE", contentResult.message), contentResult.status);
3062
- }
3063
- return c.json(ok(contentResult.data));
3064
- });
3065
- app.post("/api/marketplace/skills/install", async (c) => {
3066
- const body = await readJson(c.req.raw);
3067
- if (!body.ok || !body.data || typeof body.data !== "object") {
3068
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3069
- }
3070
- if (body.data.type && body.data.type !== "skill") {
3071
- return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
3072
- }
3073
- try {
3074
- const payload = await installMarketplaceSkill({
3075
- options,
3076
- body: body.data
3077
- });
3078
- return c.json(ok(payload));
3079
- } catch (error) {
3080
- const message = String(error);
3081
- if (message.startsWith("INVALID_BODY:")) {
3082
- return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
3083
- }
3084
- if (message.startsWith("NOT_AVAILABLE:")) {
3085
- return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
3086
- }
3087
- return c.json(err("INSTALL_FAILED", message), 400);
3088
- }
3089
- });
3090
- app.post("/api/marketplace/skills/manage", async (c) => {
3091
- const body = await readJson(c.req.raw);
3092
- if (!body.ok || !body.data || typeof body.data !== "object") {
3093
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3094
- }
3095
- if (body.data.type && body.data.type !== "skill") {
3096
- return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
3097
- }
3098
- try {
3099
- const payload = await manageMarketplaceSkill({
3100
- options,
3101
- body: body.data
3102
- });
3103
- return c.json(ok(payload));
3104
- } catch (error) {
3105
- const message = String(error);
3106
- if (message.startsWith("INVALID_BODY:")) {
3107
- return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
3108
- }
3109
- if (message.startsWith("NOT_AVAILABLE:")) {
3110
- return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
3111
- }
3112
- return c.json(err("MANAGE_FAILED", message), 400);
3113
- }
3114
- });
3115
- app.get("/api/marketplace/skills/recommendations", async (c) => {
3116
- const query = c.req.query();
3117
- const result = await fetchMarketplaceData({
3118
- baseUrl: marketplaceBaseUrl,
3119
- path: "/api/v1/skills/recommendations",
3120
- query: {
3121
- scene: query.scene,
3122
- limit: query.limit
3123
- }
3652
+ baseUrl: this.marketplaceBaseUrl,
3653
+ path: `/api/v1/plugins/items/${slug}`
3124
3654
  });
3125
3655
  if (!result.ok) {
3126
- return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3127
- }
3128
- const knownSkillNames = collectKnownSkillNames(options);
3129
- const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
3130
- return c.json(ok({
3131
- ...result.data,
3132
- total: filteredItems.length,
3133
- items: filteredItems
3134
- }));
3135
- });
3136
- }
3137
- function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
3138
- registerPluginMarketplaceRoutes(app, options, marketplaceBaseUrl);
3139
- registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl);
3140
- }
3141
- function createUiRouter(options) {
3142
- const app = new Hono();
3143
- const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
3144
- app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
3145
- app.get(
3146
- "/api/health",
3147
- (c) => c.json(
3148
- ok({
3149
- status: "ok",
3150
- services: {
3151
- chatRuntime: options.chatRuntime ? "ready" : "unavailable",
3152
- cronService: options.cronService ? "ready" : "unavailable"
3153
- }
3154
- })
3155
- )
3156
- );
3157
- app.get("/api/app/meta", (c) => c.json(ok(buildAppMetaView(options))));
3158
- app.get("/api/config", (c) => {
3159
- const config = loadConfigOrDefault(options.configPath);
3160
- return c.json(ok(buildConfigView(config)));
3161
- });
3162
- app.get("/api/config/meta", (c) => {
3163
- const config = loadConfigOrDefault(options.configPath);
3164
- return c.json(ok(buildConfigMeta(config)));
3165
- });
3166
- app.get("/api/config/schema", (c) => {
3167
- const config = loadConfigOrDefault(options.configPath);
3168
- return c.json(ok(buildConfigSchemaView(config)));
3169
- });
3170
- app.put("/api/config/model", async (c) => {
3171
- const body = await readJson(c.req.raw);
3172
- if (!body.ok) {
3173
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3174
- }
3175
- const hasModel = typeof body.data.model === "string";
3176
- if (!hasModel) {
3177
- return c.json(err("INVALID_BODY", "model is required"), 400);
3178
- }
3179
- const view = updateModel(options.configPath, {
3180
- model: body.data.model
3181
- });
3182
- if (hasModel) {
3183
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.model" } });
3184
- }
3185
- return c.json(ok({
3186
- model: view.agents.defaults.model
3187
- }));
3188
- });
3189
- app.put("/api/config/search", async (c) => {
3190
- const body = await readJson(c.req.raw);
3191
- if (!body.ok) {
3192
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3193
- }
3194
- const result = updateSearch(options.configPath, body.data);
3195
- options.publish({ type: "config.updated", payload: { path: "search" } });
3196
- return c.json(ok(result));
3197
- });
3198
- app.put("/api/config/providers/:provider", async (c) => {
3199
- const provider = c.req.param("provider");
3200
- const body = await readJson(c.req.raw);
3201
- if (!body.ok) {
3202
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3203
- }
3204
- const result = updateProvider(options.configPath, provider, body.data);
3205
- if (!result) {
3206
- return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
3207
- }
3208
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3209
- return c.json(ok(result));
3210
- });
3211
- app.post("/api/config/providers", async (c) => {
3212
- const body = await readJson(c.req.raw);
3213
- if (!body.ok) {
3214
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3215
- }
3216
- const result = createCustomProvider(
3217
- options.configPath,
3218
- body.data
3219
- );
3220
- options.publish({ type: "config.updated", payload: { path: `providers.${result.name}` } });
3221
- return c.json(ok({
3222
- name: result.name,
3223
- provider: result.provider
3224
- }));
3225
- });
3226
- app.delete("/api/config/providers/:provider", async (c) => {
3227
- const provider = c.req.param("provider");
3228
- const result = deleteCustomProvider(options.configPath, provider);
3229
- if (result === null) {
3230
- return c.json(err("NOT_FOUND", `custom provider not found: ${provider}`), 404);
3231
- }
3232
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3233
- return c.json(ok({
3234
- deleted: true,
3235
- provider
3236
- }));
3237
- });
3238
- app.post("/api/config/providers/:provider/test", async (c) => {
3239
- const provider = c.req.param("provider");
3240
- const body = await readJson(c.req.raw);
3241
- if (!body.ok) {
3242
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3243
- }
3244
- const result = await testProviderConnection(
3245
- options.configPath,
3246
- provider,
3247
- body.data
3248
- );
3249
- if (!result) {
3250
- return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
3251
- }
3252
- return c.json(ok(result));
3253
- });
3254
- app.post("/api/config/providers/:provider/auth/start", async (c) => {
3255
- const provider = c.req.param("provider");
3256
- let payload = {};
3257
- const rawBody = await c.req.raw.text();
3258
- if (rawBody.trim().length > 0) {
3259
- try {
3260
- payload = JSON.parse(rawBody);
3261
- } catch {
3262
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3263
- }
3264
- }
3265
- const methodId = typeof payload.methodId === "string" ? payload.methodId.trim() : void 0;
3266
- try {
3267
- const result = await startProviderAuth(options.configPath, provider, {
3268
- methodId
3269
- });
3270
- if (!result) {
3271
- return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
3272
- }
3273
- return c.json(ok(result));
3274
- } catch (error) {
3275
- const message = error instanceof Error ? error.message : String(error);
3276
- return c.json(err("AUTH_START_FAILED", message), 400);
3277
- }
3278
- });
3279
- app.post("/api/config/providers/:provider/auth/poll", async (c) => {
3280
- const provider = c.req.param("provider");
3281
- const body = await readJson(c.req.raw);
3282
- if (!body.ok) {
3283
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3656
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3284
3657
  }
3285
- const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
3286
- if (!sessionId) {
3287
- return c.json(err("INVALID_BODY", "sessionId is required"), 400);
3658
+ const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
3659
+ if (!isSupportedMarketplacePluginItem(sanitized)) {
3660
+ return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
3288
3661
  }
3289
- const result = await pollProviderAuth({
3290
- configPath: options.configPath,
3291
- providerName: provider,
3292
- sessionId
3662
+ return c.json(ok(sanitized));
3663
+ };
3664
+ getItemContent = async (c) => {
3665
+ const slug = encodeURIComponent(c.req.param("slug"));
3666
+ const result = await fetchMarketplaceData({
3667
+ baseUrl: this.marketplaceBaseUrl,
3668
+ path: `/api/v1/plugins/items/${slug}`
3293
3669
  });
3294
- if (!result) {
3295
- return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
3296
- }
3297
- if (result.status === "authorized") {
3298
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3670
+ if (!result.ok) {
3671
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3299
3672
  }
3300
- return c.json(ok(result));
3301
- });
3302
- app.post("/api/config/providers/:provider/auth/import-cli", async (c) => {
3303
- const provider = c.req.param("provider");
3304
- try {
3305
- const result = await importProviderAuthFromCli(options.configPath, provider);
3306
- if (!result) {
3307
- return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
3308
- }
3309
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3310
- return c.json(ok(result));
3311
- } catch (error) {
3312
- const message = error instanceof Error ? error.message : String(error);
3313
- return c.json(err("AUTH_IMPORT_FAILED", message), 400);
3673
+ const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
3674
+ if (!isSupportedMarketplacePluginItem(sanitized)) {
3675
+ return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
3314
3676
  }
3315
- });
3316
- app.put("/api/config/channels/:channel", async (c) => {
3317
- const channel = c.req.param("channel");
3677
+ const content = await buildPluginContentView(sanitized);
3678
+ return c.json(ok(content));
3679
+ };
3680
+ install = async (c) => {
3318
3681
  const body = await readJson(c.req.raw);
3319
- if (!body.ok) {
3682
+ if (!body.ok || !body.data || typeof body.data !== "object") {
3320
3683
  return c.json(err("INVALID_BODY", "invalid json body"), 400);
3321
3684
  }
3322
- const result = updateChannel(options.configPath, channel, body.data);
3323
- if (!result) {
3324
- return c.json(err("NOT_FOUND", `unknown channel: ${channel}`), 404);
3685
+ if (body.data.type && body.data.type !== "plugin") {
3686
+ return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
3325
3687
  }
3326
- options.publish({ type: "config.updated", payload: { path: `channels.${channel}` } });
3327
- return c.json(ok(result));
3328
- });
3329
- app.put("/api/config/secrets", async (c) => {
3688
+ try {
3689
+ const payload = await installMarketplacePlugin({
3690
+ options: this.options,
3691
+ body: body.data
3692
+ });
3693
+ return c.json(ok(payload));
3694
+ } catch (error) {
3695
+ const message = String(error);
3696
+ if (message.startsWith("INVALID_BODY:")) {
3697
+ return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
3698
+ }
3699
+ if (message.startsWith("NOT_AVAILABLE:")) {
3700
+ return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
3701
+ }
3702
+ return c.json(err("INSTALL_FAILED", message), 400);
3703
+ }
3704
+ };
3705
+ manage = async (c) => {
3330
3706
  const body = await readJson(c.req.raw);
3331
- if (!body.ok) {
3707
+ if (!body.ok || !body.data || typeof body.data !== "object") {
3332
3708
  return c.json(err("INVALID_BODY", "invalid json body"), 400);
3333
3709
  }
3334
- const result = updateSecrets(options.configPath, body.data);
3335
- options.publish({ type: "config.updated", payload: { path: "secrets" } });
3336
- return c.json(ok(result));
3337
- });
3338
- app.get("/api/chat/capabilities", async (c) => {
3339
- const chatRuntime = options.chatRuntime;
3340
- if (!chatRuntime) {
3341
- return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
3342
- }
3343
- const query = c.req.query();
3344
- const params = {
3345
- sessionKey: readNonEmptyString(query.sessionKey),
3346
- agentId: readNonEmptyString(query.agentId)
3347
- };
3348
- try {
3349
- const capabilities = chatRuntime.getCapabilities ? await chatRuntime.getCapabilities(params) : { stopSupported: Boolean(chatRuntime.stopTurn) };
3350
- return c.json(ok(capabilities));
3351
- } catch (error) {
3352
- return c.json(err("CHAT_RUNTIME_FAILED", String(error)), 500);
3710
+ if (body.data.type && body.data.type !== "plugin") {
3711
+ return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
3353
3712
  }
3354
- });
3355
- app.get("/api/chat/session-types", async (c) => {
3356
3713
  try {
3357
- const payload = await buildChatSessionTypesView(options.chatRuntime);
3714
+ const payload = await manageMarketplacePlugin({
3715
+ options: this.options,
3716
+ body: body.data
3717
+ });
3358
3718
  return c.json(ok(payload));
3359
3719
  } catch (error) {
3360
- return c.json(err("CHAT_SESSION_TYPES_FAILED", String(error)), 500);
3720
+ const message = String(error);
3721
+ if (message.startsWith("INVALID_BODY:")) {
3722
+ return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
3723
+ }
3724
+ if (message.startsWith("NOT_AVAILABLE:")) {
3725
+ return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
3726
+ }
3727
+ return c.json(err("MANAGE_FAILED", message), 400);
3361
3728
  }
3362
- });
3363
- app.get("/api/chat/commands", async (c) => {
3364
- try {
3365
- const config = loadConfigOrDefault(options.configPath);
3366
- const registry = new NextclawCore.CommandRegistry(config);
3367
- const commands = registry.listSlashCommands().map((command) => ({
3368
- name: command.name,
3369
- description: command.description,
3370
- ...Array.isArray(command.options) && command.options.length > 0 ? {
3371
- options: command.options.map((option) => ({
3372
- name: option.name,
3373
- description: option.description,
3374
- type: option.type,
3375
- ...option.required === true ? { required: true } : {}
3376
- }))
3377
- } : {}
3378
- }));
3379
- const payload = {
3380
- commands,
3381
- total: commands.length
3382
- };
3383
- return c.json(ok(payload));
3384
- } catch (error) {
3385
- return c.json(err("CHAT_COMMANDS_FAILED", String(error)), 500);
3729
+ };
3730
+ getRecommendations = async (c) => {
3731
+ const query = c.req.query();
3732
+ const result = await fetchMarketplaceData({
3733
+ baseUrl: this.marketplaceBaseUrl,
3734
+ path: "/api/v1/plugins/recommendations",
3735
+ query: {
3736
+ scene: query.scene,
3737
+ limit: query.limit
3738
+ }
3739
+ });
3740
+ if (!result.ok) {
3741
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3386
3742
  }
3743
+ const filteredItems = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item)).filter((item) => isSupportedMarketplacePluginItem(item));
3744
+ return c.json(ok({
3745
+ ...result.data,
3746
+ total: filteredItems.length,
3747
+ items: filteredItems
3748
+ }));
3749
+ };
3750
+ };
3751
+
3752
+ // src/ui/router/marketplace/skill.controller.ts
3753
+ async function installMarketplaceSkill(params) {
3754
+ const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
3755
+ if (!spec) {
3756
+ throw new Error("INVALID_BODY:non-empty spec is required");
3757
+ }
3758
+ const installer = params.options.marketplace?.installer;
3759
+ if (!installer) {
3760
+ throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
3761
+ }
3762
+ if (!installer.installSkill) {
3763
+ throw new Error("NOT_AVAILABLE:skill installer is not configured");
3764
+ }
3765
+ const result = await installer.installSkill({
3766
+ slug: spec,
3767
+ kind: params.body.kind,
3768
+ skill: params.body.skill,
3769
+ installPath: params.body.installPath,
3770
+ force: params.body.force
3387
3771
  });
3388
- app.post("/api/chat/turn", async (c) => {
3389
- if (!options.chatRuntime) {
3390
- return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
3391
- }
3392
- const body = await readJson(c.req.raw);
3393
- if (!body.ok) {
3394
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3772
+ params.options.publish({ type: "config.updated", payload: { path: "skills" } });
3773
+ return {
3774
+ type: "skill",
3775
+ spec,
3776
+ message: result.message,
3777
+ output: result.output
3778
+ };
3779
+ }
3780
+ async function manageMarketplaceSkill(params) {
3781
+ const action = params.body.action;
3782
+ const targetId = typeof params.body.id === "string" && params.body.id.trim().length > 0 ? params.body.id.trim() : typeof params.body.spec === "string" && params.body.spec.trim().length > 0 ? params.body.spec.trim() : "";
3783
+ if (action !== "uninstall" || !targetId) {
3784
+ throw new Error("INVALID_BODY:skill manage requires uninstall action and non-empty id/spec");
3785
+ }
3786
+ const installer = params.options.marketplace?.installer;
3787
+ if (!installer) {
3788
+ throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
3789
+ }
3790
+ if (!installer.uninstallSkill) {
3791
+ throw new Error("NOT_AVAILABLE:skill uninstall is not configured");
3792
+ }
3793
+ const result = await installer.uninstallSkill(targetId);
3794
+ params.options.publish({ type: "config.updated", payload: { path: "skills" } });
3795
+ return {
3796
+ type: "skill",
3797
+ action,
3798
+ id: targetId,
3799
+ message: result.message,
3800
+ output: result.output
3801
+ };
3802
+ }
3803
+ var SkillMarketplaceController = class {
3804
+ constructor(options, marketplaceBaseUrl) {
3805
+ this.options = options;
3806
+ this.marketplaceBaseUrl = marketplaceBaseUrl;
3807
+ }
3808
+ getInstalled = (c) => {
3809
+ return c.json(ok(collectSkillMarketplaceInstalledView(this.options)));
3810
+ };
3811
+ listItems = async (c) => {
3812
+ const query = c.req.query();
3813
+ const result = await fetchAllSkillMarketplaceItems({
3814
+ baseUrl: this.marketplaceBaseUrl,
3815
+ query: {
3816
+ q: query.q,
3817
+ tag: query.tag,
3818
+ sort: query.sort,
3819
+ page: query.page,
3820
+ pageSize: query.pageSize
3821
+ }
3822
+ });
3823
+ if (!result.ok) {
3824
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3395
3825
  }
3396
- const message = readNonEmptyString(body.data.message);
3397
- if (!message) {
3398
- return c.json(err("INVALID_BODY", "message is required"), 400);
3826
+ const normalizedItems = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item));
3827
+ const unsupportedKind = findUnsupportedSkillInstallKind(normalizedItems);
3828
+ if (unsupportedKind) {
3829
+ return c.json(
3830
+ err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
3831
+ 502
3832
+ );
3399
3833
  }
3400
- const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
3401
- const requestedAt = /* @__PURE__ */ new Date();
3402
- const startedAtMs = requestedAt.getTime();
3403
- const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
3404
- const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
3405
- const requestedModel = readNonEmptyString(body.data.model);
3406
- const request = {
3407
- message,
3408
- sessionKey,
3409
- channel: readNonEmptyString(body.data.channel) ?? "ui",
3410
- chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
3411
- ...requestedAgentId ? { agentId: requestedAgentId } : {},
3412
- ...requestedModel ? { model: requestedModel } : {},
3413
- ...metadata ? { metadata } : {}
3414
- };
3415
- try {
3416
- const result = await options.chatRuntime.processTurn(request);
3417
- const response = buildChatTurnView({
3418
- result,
3419
- fallbackSessionKey: sessionKey,
3420
- requestedAgentId,
3421
- requestedModel,
3422
- requestedAt,
3423
- startedAtMs
3424
- });
3425
- options.publish({ type: "config.updated", payload: { path: "session" } });
3426
- return c.json(ok(response));
3427
- } catch (error) {
3428
- return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
3834
+ const knownSkillNames = collectKnownSkillNames(this.options);
3835
+ const filteredItems = normalizedItems.filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
3836
+ const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
3837
+ const requestedPage = toPositiveInt(query.page, 1);
3838
+ const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
3839
+ const currentPage = totalPages === 0 ? 1 : Math.min(requestedPage, totalPages);
3840
+ return c.json(ok({
3841
+ total: filteredItems.length,
3842
+ page: currentPage,
3843
+ pageSize,
3844
+ totalPages,
3845
+ sort: result.data.sort,
3846
+ query: result.data.query,
3847
+ items: filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize)
3848
+ }));
3849
+ };
3850
+ getItem = async (c) => {
3851
+ const slug = encodeURIComponent(c.req.param("slug"));
3852
+ const result = await fetchMarketplaceData({
3853
+ baseUrl: this.marketplaceBaseUrl,
3854
+ path: `/api/v1/skills/items/${slug}`
3855
+ });
3856
+ if (!result.ok) {
3857
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3429
3858
  }
3430
- });
3431
- app.post("/api/chat/turn/stop", async (c) => {
3432
- const chatRuntime = options.chatRuntime;
3433
- if (!chatRuntime?.stopTurn) {
3434
- return c.json(err("NOT_AVAILABLE", "chat turn stop is not supported by runtime"), 503);
3859
+ const knownSkillNames = collectKnownSkillNames(this.options);
3860
+ const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
3861
+ const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
3862
+ if (unsupportedKind) {
3863
+ return c.json(
3864
+ err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
3865
+ 502
3866
+ );
3435
3867
  }
3436
- const body = await readJson(c.req.raw);
3437
- if (!body.ok || !body.data || typeof body.data !== "object") {
3438
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3868
+ if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
3869
+ return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
3439
3870
  }
3440
- const runId = readNonEmptyString(body.data.runId);
3441
- if (!runId) {
3442
- return c.json(err("INVALID_BODY", "runId is required"), 400);
3871
+ return c.json(ok(sanitized));
3872
+ };
3873
+ getItemContent = async (c) => {
3874
+ const slug = encodeURIComponent(c.req.param("slug"));
3875
+ const result = await fetchMarketplaceData({
3876
+ baseUrl: this.marketplaceBaseUrl,
3877
+ path: `/api/v1/skills/items/${slug}`
3878
+ });
3879
+ if (!result.ok) {
3880
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3443
3881
  }
3444
- const request = {
3445
- runId,
3446
- ...readNonEmptyString(body.data.sessionKey) ? { sessionKey: readNonEmptyString(body.data.sessionKey) } : {},
3447
- ...readNonEmptyString(body.data.agentId) ? { agentId: readNonEmptyString(body.data.agentId) } : {}
3448
- };
3449
- try {
3450
- const result = await chatRuntime.stopTurn(request);
3451
- return c.json(ok(result));
3452
- } catch (error) {
3453
- return c.json(err("CHAT_TURN_STOP_FAILED", String(error)), 500);
3882
+ const knownSkillNames = collectKnownSkillNames(this.options);
3883
+ const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
3884
+ const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
3885
+ if (unsupportedKind) {
3886
+ return c.json(
3887
+ err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`),
3888
+ 502
3889
+ );
3454
3890
  }
3455
- });
3456
- app.post("/api/chat/turn/stream", async (c) => {
3457
- const chatRuntime = options.chatRuntime;
3458
- if (!chatRuntime) {
3459
- return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
3891
+ if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
3892
+ return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
3893
+ }
3894
+ const contentResult = await fetchMarketplaceData({
3895
+ baseUrl: this.marketplaceBaseUrl,
3896
+ path: `/api/v1/skills/items/${slug}/content`
3897
+ });
3898
+ if (!contentResult.ok) {
3899
+ return c.json(err("MARKETPLACE_UNAVAILABLE", contentResult.message), contentResult.status);
3460
3900
  }
3901
+ return c.json(ok(contentResult.data));
3902
+ };
3903
+ install = async (c) => {
3461
3904
  const body = await readJson(c.req.raw);
3462
- if (!body.ok) {
3905
+ if (!body.ok || !body.data || typeof body.data !== "object") {
3463
3906
  return c.json(err("INVALID_BODY", "invalid json body"), 400);
3464
3907
  }
3465
- const message = readNonEmptyString(body.data.message);
3466
- if (!message) {
3467
- return c.json(err("INVALID_BODY", "message is required"), 400);
3468
- }
3469
- const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
3470
- const requestedAt = /* @__PURE__ */ new Date();
3471
- const startedAtMs = requestedAt.getTime();
3472
- const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
3473
- const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
3474
- const requestedModel = readNonEmptyString(body.data.model);
3475
- let runId = createChatRunId();
3476
- const supportsManagedRuns = Boolean(chatRuntime.startTurnRun && chatRuntime.streamRun);
3477
- let stopCapabilities = { stopSupported: Boolean(chatRuntime.stopTurn) };
3478
- if (chatRuntime.getCapabilities) {
3479
- try {
3480
- stopCapabilities = await chatRuntime.getCapabilities({
3481
- sessionKey,
3482
- ...requestedAgentId ? { agentId: requestedAgentId } : {}
3483
- });
3484
- } catch {
3485
- stopCapabilities = {
3486
- stopSupported: false,
3487
- stopReason: "failed to resolve runtime stop capability"
3488
- };
3489
- }
3490
- }
3491
- const request = {
3492
- message,
3493
- sessionKey,
3494
- channel: readNonEmptyString(body.data.channel) ?? "ui",
3495
- chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
3496
- runId,
3497
- ...requestedAgentId ? { agentId: requestedAgentId } : {},
3498
- ...requestedModel ? { model: requestedModel } : {},
3499
- ...metadata ? { metadata } : {}
3500
- };
3501
- let managedRun = null;
3502
- if (supportsManagedRuns && chatRuntime.startTurnRun) {
3503
- try {
3504
- managedRun = await chatRuntime.startTurnRun(request);
3505
- } catch (error) {
3506
- return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
3507
- }
3508
- if (readNonEmptyString(managedRun.runId)) {
3509
- runId = readNonEmptyString(managedRun.runId);
3510
- }
3511
- stopCapabilities = {
3512
- stopSupported: managedRun.stopSupported,
3513
- ...readNonEmptyString(managedRun.stopReason) ? { stopReason: readNonEmptyString(managedRun.stopReason) } : {}
3514
- };
3515
- }
3516
- const encoder = new TextEncoder();
3517
- const stream = new ReadableStream({
3518
- start: async (controller) => {
3519
- const push = (event, data) => {
3520
- controller.enqueue(encoder.encode(toSseFrame(event, data)));
3521
- };
3522
- try {
3523
- push("ready", {
3524
- sessionKey: managedRun?.sessionKey ?? sessionKey,
3525
- requestedAt: managedRun?.requestedAt ?? requestedAt.toISOString(),
3526
- runId,
3527
- stopSupported: stopCapabilities.stopSupported,
3528
- ...readNonEmptyString(stopCapabilities.stopReason) ? { stopReason: readNonEmptyString(stopCapabilities.stopReason) } : {}
3529
- });
3530
- if (supportsManagedRuns && chatRuntime.streamRun) {
3531
- let hasFinal2 = false;
3532
- for await (const event of chatRuntime.streamRun({ runId })) {
3533
- const typed = event;
3534
- if (typed.type === "delta") {
3535
- if (typed.delta) {
3536
- push("delta", { delta: typed.delta });
3537
- }
3538
- continue;
3539
- }
3540
- if (typed.type === "session_event") {
3541
- push("session_event", typed.event);
3542
- continue;
3543
- }
3544
- if (typed.type === "final") {
3545
- const latestRun = chatRuntime.getRun ? await chatRuntime.getRun({ runId }) : null;
3546
- const response = latestRun ? buildChatTurnViewFromRun({
3547
- run: latestRun,
3548
- fallbackSessionKey: sessionKey,
3549
- fallbackAgentId: requestedAgentId,
3550
- fallbackModel: requestedModel,
3551
- fallbackReply: typed.result.reply
3552
- }) : buildChatTurnView({
3553
- result: typed.result,
3554
- fallbackSessionKey: sessionKey,
3555
- requestedAgentId,
3556
- requestedModel,
3557
- requestedAt,
3558
- startedAtMs
3559
- });
3560
- hasFinal2 = true;
3561
- push("final", response);
3562
- options.publish({ type: "config.updated", payload: { path: "session" } });
3563
- continue;
3564
- }
3565
- if (typed.type === "error") {
3566
- push("error", {
3567
- code: "CHAT_TURN_FAILED",
3568
- message: formatUserFacingError(typed.error)
3569
- });
3570
- return;
3571
- }
3572
- }
3573
- if (!hasFinal2) {
3574
- push("error", {
3575
- code: "CHAT_TURN_FAILED",
3576
- message: "stream ended without a final result"
3577
- });
3578
- return;
3579
- }
3580
- push("done", { ok: true });
3581
- return;
3582
- }
3583
- const streamTurn = chatRuntime.processTurnStream;
3584
- if (!streamTurn) {
3585
- const result = await chatRuntime.processTurn(request);
3586
- const response = buildChatTurnView({
3587
- result,
3588
- fallbackSessionKey: sessionKey,
3589
- requestedAgentId,
3590
- requestedModel,
3591
- requestedAt,
3592
- startedAtMs
3593
- });
3594
- push("final", response);
3595
- options.publish({ type: "config.updated", payload: { path: "session" } });
3596
- push("done", { ok: true });
3597
- return;
3598
- }
3599
- let hasFinal = false;
3600
- for await (const event of streamTurn(request)) {
3601
- const typed = event;
3602
- if (typed.type === "delta") {
3603
- if (typed.delta) {
3604
- push("delta", { delta: typed.delta });
3605
- }
3606
- continue;
3607
- }
3608
- if (typed.type === "session_event") {
3609
- push("session_event", typed.event);
3610
- continue;
3611
- }
3612
- if (typed.type === "final") {
3613
- const response = buildChatTurnView({
3614
- result: typed.result,
3615
- fallbackSessionKey: sessionKey,
3616
- requestedAgentId,
3617
- requestedModel,
3618
- requestedAt,
3619
- startedAtMs
3620
- });
3621
- hasFinal = true;
3622
- push("final", response);
3623
- options.publish({ type: "config.updated", payload: { path: "session" } });
3624
- continue;
3625
- }
3626
- if (typed.type === "error") {
3627
- push("error", {
3628
- code: "CHAT_TURN_FAILED",
3629
- message: formatUserFacingError(typed.error)
3630
- });
3631
- return;
3632
- }
3633
- }
3634
- if (!hasFinal) {
3635
- push("error", {
3636
- code: "CHAT_TURN_FAILED",
3637
- message: "stream ended without a final result"
3638
- });
3639
- return;
3640
- }
3641
- push("done", { ok: true });
3642
- } catch (error) {
3643
- push("error", {
3644
- code: "CHAT_TURN_FAILED",
3645
- message: formatUserFacingError(error)
3646
- });
3647
- } finally {
3648
- controller.close();
3649
- }
3650
- }
3651
- });
3652
- return new Response(stream, {
3653
- status: 200,
3654
- headers: {
3655
- "Content-Type": "text/event-stream; charset=utf-8",
3656
- "Cache-Control": "no-cache, no-transform",
3657
- "Connection": "keep-alive",
3658
- "X-Accel-Buffering": "no"
3659
- }
3660
- });
3661
- });
3662
- app.get("/api/chat/runs", async (c) => {
3663
- const chatRuntime = options.chatRuntime;
3664
- if (!chatRuntime?.listRuns) {
3665
- return c.json(err("NOT_AVAILABLE", "chat run management unavailable"), 503);
3908
+ if (body.data.type && body.data.type !== "skill") {
3909
+ return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
3666
3910
  }
3667
- const query = c.req.query();
3668
- const sessionKey = readNonEmptyString(query.sessionKey);
3669
- const states = readChatRunStates(query.states);
3670
- const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
3671
3911
  try {
3672
- const data = await chatRuntime.listRuns({
3673
- ...sessionKey ? { sessionKey } : {},
3674
- ...states ? { states } : {},
3675
- ...Number.isFinite(limit) ? { limit } : {}
3912
+ const payload = await installMarketplaceSkill({
3913
+ options: this.options,
3914
+ body: body.data
3676
3915
  });
3677
- return c.json(ok(data));
3916
+ return c.json(ok(payload));
3678
3917
  } catch (error) {
3679
- return c.json(err("CHAT_RUN_QUERY_FAILED", String(error)), 500);
3918
+ const message = String(error);
3919
+ if (message.startsWith("INVALID_BODY:")) {
3920
+ return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
3921
+ }
3922
+ if (message.startsWith("NOT_AVAILABLE:")) {
3923
+ return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
3924
+ }
3925
+ return c.json(err("INSTALL_FAILED", message), 400);
3680
3926
  }
3681
- });
3682
- app.get("/api/chat/runs/:runId", async (c) => {
3683
- const chatRuntime = options.chatRuntime;
3684
- if (!chatRuntime?.getRun) {
3685
- return c.json(err("NOT_AVAILABLE", "chat run management unavailable"), 503);
3927
+ };
3928
+ manage = async (c) => {
3929
+ const body = await readJson(c.req.raw);
3930
+ if (!body.ok || !body.data || typeof body.data !== "object") {
3931
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
3686
3932
  }
3687
- const runId = readNonEmptyString(c.req.param("runId"));
3688
- if (!runId) {
3689
- return c.json(err("INVALID_PATH", "runId is required"), 400);
3933
+ if (body.data.type && body.data.type !== "skill") {
3934
+ return c.json(err("INVALID_BODY", "body.type does not match route type"), 400);
3690
3935
  }
3691
3936
  try {
3692
- const run = await chatRuntime.getRun({ runId });
3693
- if (!run) {
3694
- return c.json(err("NOT_FOUND", `chat run not found: ${runId}`), 404);
3695
- }
3696
- return c.json(ok(run));
3937
+ const payload = await manageMarketplaceSkill({
3938
+ options: this.options,
3939
+ body: body.data
3940
+ });
3941
+ return c.json(ok(payload));
3697
3942
  } catch (error) {
3698
- return c.json(err("CHAT_RUN_QUERY_FAILED", String(error)), 500);
3699
- }
3700
- });
3701
- app.get("/api/chat/runs/:runId/stream", async (c) => {
3702
- const chatRuntime = options.chatRuntime;
3703
- const streamRun = chatRuntime?.streamRun;
3704
- const getRun = chatRuntime?.getRun;
3705
- if (!streamRun || !getRun) {
3706
- return c.json(err("NOT_AVAILABLE", "chat run stream unavailable"), 503);
3707
- }
3708
- const runId = readNonEmptyString(c.req.param("runId"));
3709
- if (!runId) {
3710
- return c.json(err("INVALID_PATH", "runId is required"), 400);
3943
+ const message = String(error);
3944
+ if (message.startsWith("INVALID_BODY:")) {
3945
+ return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
3946
+ }
3947
+ if (message.startsWith("NOT_AVAILABLE:")) {
3948
+ return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
3949
+ }
3950
+ return c.json(err("MANAGE_FAILED", message), 400);
3711
3951
  }
3952
+ };
3953
+ getRecommendations = async (c) => {
3712
3954
  const query = c.req.query();
3713
- const fromEventIndex = typeof query.fromEventIndex === "string" ? Number.parseInt(query.fromEventIndex, 10) : void 0;
3714
- const run = await getRun({ runId });
3715
- if (!run) {
3716
- return c.json(err("NOT_FOUND", `chat run not found: ${runId}`), 404);
3717
- }
3718
- const encoder = new TextEncoder();
3719
- const stream = new ReadableStream({
3720
- start: async (controller) => {
3721
- const push = (event, data) => {
3722
- controller.enqueue(encoder.encode(toSseFrame(event, data)));
3723
- };
3724
- try {
3725
- push("ready", {
3726
- sessionKey: run.sessionKey,
3727
- requestedAt: run.requestedAt,
3728
- runId: run.runId,
3729
- stopSupported: run.stopSupported,
3730
- ...readNonEmptyString(run.stopReason) ? { stopReason: readNonEmptyString(run.stopReason) } : {}
3731
- });
3732
- let hasFinal = false;
3733
- for await (const event of streamRun({
3734
- runId: run.runId,
3735
- ...Number.isFinite(fromEventIndex) ? { fromEventIndex } : {}
3736
- })) {
3737
- const typed = event;
3738
- if (typed.type === "delta") {
3739
- if (typed.delta) {
3740
- push("delta", { delta: typed.delta });
3741
- }
3742
- continue;
3743
- }
3744
- if (typed.type === "session_event") {
3745
- push("session_event", typed.event);
3746
- continue;
3747
- }
3748
- if (typed.type === "final") {
3749
- const latestRun = await getRun({ runId: run.runId });
3750
- const response = latestRun ? buildChatTurnViewFromRun({
3751
- run: latestRun,
3752
- fallbackSessionKey: run.sessionKey,
3753
- fallbackAgentId: run.agentId,
3754
- fallbackModel: run.model,
3755
- fallbackReply: typed.result.reply
3756
- }) : buildChatTurnView({
3757
- result: typed.result,
3758
- fallbackSessionKey: run.sessionKey,
3759
- requestedAgentId: run.agentId,
3760
- requestedModel: run.model,
3761
- requestedAt: new Date(run.requestedAt),
3762
- startedAtMs: Date.parse(run.requestedAt)
3763
- });
3764
- hasFinal = true;
3765
- push("final", response);
3766
- continue;
3767
- }
3768
- if (typed.type === "error") {
3769
- push("error", {
3770
- code: "CHAT_TURN_FAILED",
3771
- message: formatUserFacingError(typed.error)
3772
- });
3773
- return;
3774
- }
3775
- }
3776
- if (!hasFinal) {
3777
- const latestRun = await getRun({ runId: run.runId });
3778
- if (latestRun?.state === "failed") {
3779
- push("error", {
3780
- code: "CHAT_TURN_FAILED",
3781
- message: formatUserFacingError(latestRun.error ?? "chat run failed")
3782
- });
3783
- return;
3784
- }
3785
- }
3786
- push("done", { ok: true });
3787
- } catch (error) {
3788
- push("error", {
3789
- code: "CHAT_TURN_FAILED",
3790
- message: formatUserFacingError(error)
3791
- });
3792
- } finally {
3793
- controller.close();
3794
- }
3795
- }
3796
- });
3797
- return new Response(stream, {
3798
- status: 200,
3799
- headers: {
3800
- "Content-Type": "text/event-stream; charset=utf-8",
3801
- "Cache-Control": "no-cache, no-transform",
3802
- "Connection": "keep-alive",
3803
- "X-Accel-Buffering": "no"
3955
+ const result = await fetchMarketplaceData({
3956
+ baseUrl: this.marketplaceBaseUrl,
3957
+ path: "/api/v1/skills/recommendations",
3958
+ query: {
3959
+ scene: query.scene,
3960
+ limit: query.limit
3804
3961
  }
3805
3962
  });
3806
- });
3807
- app.get("/api/sessions", (c) => {
3963
+ if (!result.ok) {
3964
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3965
+ }
3966
+ const knownSkillNames = collectKnownSkillNames(this.options);
3967
+ const filteredItems = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item)).filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
3968
+ return c.json(ok({
3969
+ ...result.data,
3970
+ total: filteredItems.length,
3971
+ items: filteredItems
3972
+ }));
3973
+ };
3974
+ };
3975
+
3976
+ // src/ui/router/session.controller.ts
3977
+ var SessionRoutesController = class {
3978
+ constructor(options) {
3979
+ this.options = options;
3980
+ }
3981
+ listSessions = (c) => {
3808
3982
  const query = c.req.query();
3809
3983
  const q = typeof query.q === "string" ? query.q : void 0;
3810
3984
  const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
3811
3985
  const activeMinutes = typeof query.activeMinutes === "string" ? Number.parseInt(query.activeMinutes, 10) : void 0;
3812
- const data = listSessions(options.configPath, {
3986
+ const data = listSessions(this.options.configPath, {
3813
3987
  q,
3814
3988
  limit: Number.isFinite(limit) ? limit : void 0,
3815
3989
  activeMinutes: Number.isFinite(activeMinutes) ? activeMinutes : void 0
3816
3990
  });
3817
3991
  return c.json(ok(data));
3818
- });
3819
- app.get("/api/sessions/:key/history", (c) => {
3992
+ };
3993
+ getSessionHistory = (c) => {
3820
3994
  const key = decodeURIComponent(c.req.param("key"));
3821
3995
  const query = c.req.query();
3822
3996
  const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
3823
- const data = getSessionHistory(options.configPath, key, Number.isFinite(limit) ? limit : void 0);
3997
+ const data = getSessionHistory(this.options.configPath, key, Number.isFinite(limit) ? limit : void 0);
3824
3998
  if (!data) {
3825
3999
  return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
3826
4000
  }
3827
4001
  return c.json(ok(data));
3828
- });
3829
- app.put("/api/sessions/:key", async (c) => {
4002
+ };
4003
+ patchSession = async (c) => {
3830
4004
  const key = decodeURIComponent(c.req.param("key"));
3831
4005
  const body = await readJson(c.req.raw);
3832
4006
  if (!body.ok || !body.data || typeof body.data !== "object") {
@@ -3834,12 +4008,12 @@ function createUiRouter(options) {
3834
4008
  }
3835
4009
  let availableSessionTypes;
3836
4010
  if (Object.prototype.hasOwnProperty.call(body.data, "sessionType")) {
3837
- const sessionTypes = await buildChatSessionTypesView(options.chatRuntime);
4011
+ const sessionTypes = await buildChatSessionTypesView(this.options.chatRuntime);
3838
4012
  availableSessionTypes = sessionTypes.options.map((item) => item.value);
3839
4013
  }
3840
4014
  let data;
3841
4015
  try {
3842
- data = patchSession(options.configPath, key, body.data, {
4016
+ data = patchSession(this.options.configPath, key, body.data, {
3843
4017
  ...availableSessionTypes ? { availableSessionTypes } : {}
3844
4018
  });
3845
4019
  } catch (error) {
@@ -3854,111 +4028,81 @@ function createUiRouter(options) {
3854
4028
  if (!data) {
3855
4029
  return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
3856
4030
  }
3857
- options.publish({ type: "config.updated", payload: { path: "session" } });
4031
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
3858
4032
  return c.json(ok(data));
3859
- });
3860
- app.delete("/api/sessions/:key", (c) => {
4033
+ };
4034
+ deleteSession = (c) => {
3861
4035
  const key = decodeURIComponent(c.req.param("key"));
3862
- const deleted = deleteSession(options.configPath, key);
4036
+ const deleted = deleteSession(this.options.configPath, key);
3863
4037
  if (!deleted) {
3864
4038
  return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
3865
4039
  }
3866
- options.publish({ type: "config.updated", payload: { path: "session" } });
3867
- return c.json(ok({ deleted: true }));
3868
- });
3869
- app.get("/api/cron", (c) => {
3870
- if (!options.cronService) {
3871
- return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
3872
- }
3873
- const query = c.req.query();
3874
- const includeDisabled = query.all === "1" || query.all === "true" || query.all === "yes";
3875
- const jobs = options.cronService.listJobs(includeDisabled).map((job) => buildCronJobView(job));
3876
- return c.json(ok({ jobs, total: jobs.length }));
3877
- });
3878
- app.delete("/api/cron/:id", (c) => {
3879
- if (!options.cronService) {
3880
- return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
3881
- }
3882
- const id = decodeURIComponent(c.req.param("id"));
3883
- const deleted = options.cronService.removeJob(id);
3884
- if (!deleted) {
3885
- return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
3886
- }
4040
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
3887
4041
  return c.json(ok({ deleted: true }));
3888
- });
3889
- app.put("/api/cron/:id/enable", async (c) => {
3890
- if (!options.cronService) {
3891
- return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
3892
- }
3893
- const id = decodeURIComponent(c.req.param("id"));
3894
- const body = await readJson(c.req.raw);
3895
- if (!body.ok) {
3896
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3897
- }
3898
- if (typeof body.data.enabled !== "boolean") {
3899
- return c.json(err("INVALID_BODY", "enabled must be boolean"), 400);
3900
- }
3901
- const job = options.cronService.enableJob(id, body.data.enabled);
3902
- if (!job) {
3903
- return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
3904
- }
3905
- const data = { job: buildCronJobView(job) };
3906
- return c.json(ok(data));
3907
- });
3908
- app.post("/api/cron/:id/run", async (c) => {
3909
- if (!options.cronService) {
3910
- return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
3911
- }
3912
- const id = decodeURIComponent(c.req.param("id"));
3913
- const body = await readJson(c.req.raw);
3914
- if (!body.ok) {
3915
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3916
- }
3917
- const existing = findCronJob(options.cronService, id);
3918
- if (!existing) {
3919
- return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
3920
- }
3921
- const executed = await options.cronService.runJob(id, Boolean(body.data.force));
3922
- const after = findCronJob(options.cronService, id);
3923
- const data = {
3924
- job: after ? buildCronJobView(after) : null,
3925
- executed
3926
- };
3927
- return c.json(ok(data));
3928
- });
3929
- app.put("/api/config/runtime", async (c) => {
3930
- const body = await readJson(c.req.raw);
3931
- if (!body.ok || !body.data || typeof body.data !== "object") {
3932
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3933
- }
3934
- const result = updateRuntime(options.configPath, body.data);
3935
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "contextTokens")) {
3936
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.contextTokens" } });
3937
- }
3938
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engine")) {
3939
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.engine" } });
3940
- }
3941
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engineConfig")) {
3942
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.engineConfig" } });
3943
- }
3944
- options.publish({ type: "config.updated", payload: { path: "agents.list" } });
3945
- options.publish({ type: "config.updated", payload: { path: "bindings" } });
3946
- options.publish({ type: "config.updated", payload: { path: "session" } });
3947
- return c.json(ok(result));
3948
- });
3949
- app.post("/api/config/actions/:actionId/execute", async (c) => {
3950
- const actionId = c.req.param("actionId");
3951
- const body = await readJson(c.req.raw);
3952
- if (!body.ok) {
3953
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3954
- }
3955
- const result = await executeConfigAction(options.configPath, actionId, body.data ?? {});
3956
- if (!result.ok) {
3957
- return c.json(err(result.code, result.message, result.details), 400);
3958
- }
3959
- return c.json(ok(result.data));
3960
- });
3961
- registerMarketplaceRoutes(app, options, marketplaceBaseUrl);
4042
+ };
4043
+ };
4044
+
4045
+ // src/ui/router.ts
4046
+ function createUiRouter(options) {
4047
+ const app = new Hono();
4048
+ const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
4049
+ const appController = new AppRoutesController(options);
4050
+ const configController = new ConfigRoutesController(options);
4051
+ const chatController = new ChatRoutesController(options);
4052
+ const sessionController = new SessionRoutesController(options);
4053
+ const cronController = new CronRoutesController(options);
4054
+ const pluginMarketplaceController = new PluginMarketplaceController(options, marketplaceBaseUrl);
4055
+ const skillMarketplaceController = new SkillMarketplaceController(options, marketplaceBaseUrl);
4056
+ app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
4057
+ app.get("/api/health", appController.health);
4058
+ app.get("/api/app/meta", appController.appMeta);
4059
+ app.get("/api/config", configController.getConfig);
4060
+ app.get("/api/config/meta", configController.getConfigMeta);
4061
+ app.get("/api/config/schema", configController.getConfigSchema);
4062
+ app.put("/api/config/model", configController.updateConfigModel);
4063
+ app.put("/api/config/search", configController.updateConfigSearch);
4064
+ app.put("/api/config/providers/:provider", configController.updateProvider);
4065
+ app.post("/api/config/providers", configController.createProvider);
4066
+ app.delete("/api/config/providers/:provider", configController.deleteProvider);
4067
+ app.post("/api/config/providers/:provider/test", configController.testProviderConnection);
4068
+ app.post("/api/config/providers/:provider/auth/start", configController.startProviderAuth);
4069
+ app.post("/api/config/providers/:provider/auth/poll", configController.pollProviderAuth);
4070
+ app.post("/api/config/providers/:provider/auth/import-cli", configController.importProviderAuthFromCli);
4071
+ app.put("/api/config/channels/:channel", configController.updateChannel);
4072
+ app.put("/api/config/secrets", configController.updateSecrets);
4073
+ app.put("/api/config/runtime", configController.updateRuntime);
4074
+ app.post("/api/config/actions/:actionId/execute", configController.executeAction);
4075
+ app.get("/api/chat/capabilities", chatController.getCapabilities);
4076
+ app.get("/api/chat/session-types", chatController.getSessionTypes);
4077
+ app.get("/api/chat/commands", chatController.getCommands);
4078
+ app.post("/api/chat/turn", chatController.processTurn);
4079
+ app.post("/api/chat/turn/stop", chatController.stopTurn);
4080
+ app.post("/api/chat/turn/stream", chatController.streamTurn);
4081
+ app.get("/api/chat/runs", chatController.listRuns);
4082
+ app.get("/api/chat/runs/:runId", chatController.getRun);
4083
+ app.get("/api/chat/runs/:runId/stream", chatController.streamRun);
4084
+ app.get("/api/sessions", sessionController.listSessions);
4085
+ app.get("/api/sessions/:key/history", sessionController.getSessionHistory);
4086
+ app.put("/api/sessions/:key", sessionController.patchSession);
4087
+ app.delete("/api/sessions/:key", sessionController.deleteSession);
4088
+ app.get("/api/cron", cronController.listJobs);
4089
+ app.delete("/api/cron/:id", cronController.deleteJob);
4090
+ app.put("/api/cron/:id/enable", cronController.enableJob);
4091
+ app.post("/api/cron/:id/run", cronController.runJob);
4092
+ app.get("/api/marketplace/plugins/installed", pluginMarketplaceController.getInstalled);
4093
+ app.get("/api/marketplace/plugins/items", pluginMarketplaceController.listItems);
4094
+ app.get("/api/marketplace/plugins/items/:slug", pluginMarketplaceController.getItem);
4095
+ app.get("/api/marketplace/plugins/items/:slug/content", pluginMarketplaceController.getItemContent);
4096
+ app.post("/api/marketplace/plugins/install", pluginMarketplaceController.install);
4097
+ app.post("/api/marketplace/plugins/manage", pluginMarketplaceController.manage);
4098
+ app.get("/api/marketplace/plugins/recommendations", pluginMarketplaceController.getRecommendations);
4099
+ app.get("/api/marketplace/skills/installed", skillMarketplaceController.getInstalled);
4100
+ app.get("/api/marketplace/skills/items", skillMarketplaceController.listItems);
4101
+ app.get("/api/marketplace/skills/items/:slug", skillMarketplaceController.getItem);
4102
+ app.get("/api/marketplace/skills/items/:slug/content", skillMarketplaceController.getItemContent);
4103
+ app.post("/api/marketplace/skills/install", skillMarketplaceController.install);
4104
+ app.post("/api/marketplace/skills/manage", skillMarketplaceController.manage);
4105
+ app.get("/api/marketplace/skills/recommendations", skillMarketplaceController.getRecommendations);
3962
4106
  return app;
3963
4107
  }
3964
4108