@nextclaw/server 0.6.11 → 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 +2246 -2091
  3. package/package.json +3 -7
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}`
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
+ };
2991
+ }
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
+ };
3013
+ }
3014
+ return {
3015
+ ok: true,
3016
+ data: typed.data
3017
+ };
3018
+ }
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)
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
1851
3081
  );
1852
3082
  }
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");
3083
+ return next;
3084
+ }
3085
+ function toPositiveInt(raw, fallback) {
3086
+ if (!raw) {
3087
+ return fallback;
3088
+ }
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
+ }
1858
3123
  }
1859
- setProviderApiKey({
1860
- configPath,
1861
- provider: providerName,
1862
- accessToken,
1863
- defaultApiBase: spec.defaultApiBase
1864
- });
1865
3124
  return {
1866
- provider: providerName,
1867
- status: "imported",
1868
- source: "cli",
1869
- expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
3125
+ ok: true,
3126
+ data: {
3127
+ sort,
3128
+ ...typeof query === "string" ? { query } : {},
3129
+ items
3130
+ }
1870
3131
  };
1871
3132
  }
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"
1879
- };
3133
+ async function fetchAllPluginMarketplaceItems(params) {
3134
+ return fetchAllMarketplaceItems({
3135
+ baseUrl: params.baseUrl,
3136
+ path: "/api/v1/plugins/items",
3137
+ query: params.query
3138
+ });
1880
3139
  }
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;
1892
- }
1893
- return new ctor(workspace);
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);
1894
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("@")) {
@@ -1952,296 +3216,56 @@ function readPluginOriginPriority(origin) {
1952
3216
  return 10;
1953
3217
  }
1954
3218
  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
- };
3219
+ const installScore = record.installPath ? 20 : 0;
3220
+ const timestampScore = record.installedAt ? 10 : 0;
3221
+ return readPluginRuntimeStatusPriority(record.runtimeStatus) + readPluginOriginPriority(record.origin) + installScore + timestampScore;
2168
3222
  }
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);
3223
+ function mergeInstalledPluginRecords(primary, secondary) {
2174
3224
  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
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
2182
3234
  };
2183
3235
  }
2184
- function toSseFrame(event, data) {
2185
- return `event: ${event}
2186
- data: ${JSON.stringify(data)}
2187
-
2188
- `;
2189
- }
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;
2195
- }
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();
2204
- }
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);
@@ -2385,199 +3409,49 @@ function collectPluginMarketplaceInstalledView(options) {
2385
3409
  total: installed.records.length,
2386
3410
  specs: installed.specs,
2387
3411
  records: installed.records
2388
- };
2389
- }
2390
- function collectSkillMarketplaceInstalledView(options) {
2391
- const installed = collectInstalledSkillRecords(options);
2392
- return {
2393
- type: "skill",
2394
- total: installed.records.length,
2395
- 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;
3412
+ };
2556
3413
  }
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)
3414
+ function collectSkillMarketplaceInstalledView(options) {
3415
+ const installed = collectInstalledSkillRecords(options);
3416
+ return {
3417
+ type: "skill",
3418
+ total: installed.records.length,
3419
+ specs: installed.specs,
3420
+ records: installed.records
2562
3421
  };
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;
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() : "";
@@ -2791,189 +3592,34 @@ async function manageMarketplacePlugin(params) {
2791
3592
  if (!installer.disablePlugin) {
2792
3593
  throw new Error("NOT_AVAILABLE:plugin disable is not configured");
2793
3594
  }
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);
3595
+ result = await installer.disablePlugin(targetId);
3596
+ } else {
3597
+ if (!installer.uninstallPlugin) {
3598
+ throw new Error("NOT_AVAILABLE:plugin uninstall is not configured");
2960
3599
  }
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
- });
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,814 +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("/api/health", (c) => c.json(ok({ status: "ok" })));
3146
- app.get("/api/app/meta", (c) => c.json(ok(buildAppMetaView(options))));
3147
- app.get("/api/config", (c) => {
3148
- const config = loadConfigOrDefault(options.configPath);
3149
- return c.json(ok(buildConfigView(config)));
3150
- });
3151
- app.get("/api/config/meta", (c) => {
3152
- const config = loadConfigOrDefault(options.configPath);
3153
- return c.json(ok(buildConfigMeta(config)));
3154
- });
3155
- app.get("/api/config/schema", (c) => {
3156
- const config = loadConfigOrDefault(options.configPath);
3157
- return c.json(ok(buildConfigSchemaView(config)));
3158
- });
3159
- app.put("/api/config/model", async (c) => {
3160
- const body = await readJson(c.req.raw);
3161
- if (!body.ok) {
3162
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3163
- }
3164
- const hasModel = typeof body.data.model === "string";
3165
- if (!hasModel) {
3166
- return c.json(err("INVALID_BODY", "model is required"), 400);
3167
- }
3168
- const view = updateModel(options.configPath, {
3169
- model: body.data.model
3170
- });
3171
- if (hasModel) {
3172
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.model" } });
3173
- }
3174
- return c.json(ok({
3175
- model: view.agents.defaults.model
3176
- }));
3177
- });
3178
- app.put("/api/config/search", async (c) => {
3179
- const body = await readJson(c.req.raw);
3180
- if (!body.ok) {
3181
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3182
- }
3183
- const result = updateSearch(options.configPath, body.data);
3184
- options.publish({ type: "config.updated", payload: { path: "search" } });
3185
- return c.json(ok(result));
3186
- });
3187
- app.put("/api/config/providers/:provider", async (c) => {
3188
- const provider = c.req.param("provider");
3189
- const body = await readJson(c.req.raw);
3190
- if (!body.ok) {
3191
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3192
- }
3193
- const result = updateProvider(options.configPath, provider, body.data);
3194
- if (!result) {
3195
- return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
3196
- }
3197
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3198
- return c.json(ok(result));
3199
- });
3200
- app.post("/api/config/providers", async (c) => {
3201
- const body = await readJson(c.req.raw);
3202
- if (!body.ok) {
3203
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3204
- }
3205
- const result = createCustomProvider(
3206
- options.configPath,
3207
- body.data
3208
- );
3209
- options.publish({ type: "config.updated", payload: { path: `providers.${result.name}` } });
3210
- return c.json(ok({
3211
- name: result.name,
3212
- provider: result.provider
3213
- }));
3214
- });
3215
- app.delete("/api/config/providers/:provider", async (c) => {
3216
- const provider = c.req.param("provider");
3217
- const result = deleteCustomProvider(options.configPath, provider);
3218
- if (result === null) {
3219
- return c.json(err("NOT_FOUND", `custom provider not found: ${provider}`), 404);
3220
- }
3221
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3222
- return c.json(ok({
3223
- deleted: true,
3224
- provider
3225
- }));
3226
- });
3227
- app.post("/api/config/providers/:provider/test", async (c) => {
3228
- const provider = c.req.param("provider");
3229
- const body = await readJson(c.req.raw);
3230
- if (!body.ok) {
3231
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3232
- }
3233
- const result = await testProviderConnection(
3234
- options.configPath,
3235
- provider,
3236
- body.data
3237
- );
3238
- if (!result) {
3239
- return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
3240
- }
3241
- return c.json(ok(result));
3242
- });
3243
- app.post("/api/config/providers/:provider/auth/start", async (c) => {
3244
- const provider = c.req.param("provider");
3245
- let payload = {};
3246
- const rawBody = await c.req.raw.text();
3247
- if (rawBody.trim().length > 0) {
3248
- try {
3249
- payload = JSON.parse(rawBody);
3250
- } catch {
3251
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3252
- }
3253
- }
3254
- const methodId = typeof payload.methodId === "string" ? payload.methodId.trim() : void 0;
3255
- try {
3256
- const result = await startProviderAuth(options.configPath, provider, {
3257
- methodId
3258
- });
3259
- if (!result) {
3260
- return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
3261
- }
3262
- return c.json(ok(result));
3263
- } catch (error) {
3264
- const message = error instanceof Error ? error.message : String(error);
3265
- return c.json(err("AUTH_START_FAILED", message), 400);
3266
- }
3267
- });
3268
- app.post("/api/config/providers/:provider/auth/poll", async (c) => {
3269
- const provider = c.req.param("provider");
3270
- const body = await readJson(c.req.raw);
3271
- if (!body.ok) {
3272
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3656
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3273
3657
  }
3274
- const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
3275
- if (!sessionId) {
3276
- 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);
3277
3661
  }
3278
- const result = await pollProviderAuth({
3279
- configPath: options.configPath,
3280
- providerName: provider,
3281
- 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}`
3282
3669
  });
3283
- if (!result) {
3284
- return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
3285
- }
3286
- if (result.status === "authorized") {
3287
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3670
+ if (!result.ok) {
3671
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
3288
3672
  }
3289
- return c.json(ok(result));
3290
- });
3291
- app.post("/api/config/providers/:provider/auth/import-cli", async (c) => {
3292
- const provider = c.req.param("provider");
3293
- try {
3294
- const result = await importProviderAuthFromCli(options.configPath, provider);
3295
- if (!result) {
3296
- return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
3297
- }
3298
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3299
- return c.json(ok(result));
3300
- } catch (error) {
3301
- const message = error instanceof Error ? error.message : String(error);
3302
- 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);
3303
3676
  }
3304
- });
3305
- app.put("/api/config/channels/:channel", async (c) => {
3306
- const channel = c.req.param("channel");
3677
+ const content = await buildPluginContentView(sanitized);
3678
+ return c.json(ok(content));
3679
+ };
3680
+ install = async (c) => {
3307
3681
  const body = await readJson(c.req.raw);
3308
- if (!body.ok) {
3682
+ if (!body.ok || !body.data || typeof body.data !== "object") {
3309
3683
  return c.json(err("INVALID_BODY", "invalid json body"), 400);
3310
3684
  }
3311
- const result = updateChannel(options.configPath, channel, body.data);
3312
- if (!result) {
3313
- 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);
3314
3687
  }
3315
- options.publish({ type: "config.updated", payload: { path: `channels.${channel}` } });
3316
- return c.json(ok(result));
3317
- });
3318
- 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) => {
3319
3706
  const body = await readJson(c.req.raw);
3320
- if (!body.ok) {
3707
+ if (!body.ok || !body.data || typeof body.data !== "object") {
3321
3708
  return c.json(err("INVALID_BODY", "invalid json body"), 400);
3322
3709
  }
3323
- const result = updateSecrets(options.configPath, body.data);
3324
- options.publish({ type: "config.updated", payload: { path: "secrets" } });
3325
- return c.json(ok(result));
3326
- });
3327
- app.get("/api/chat/capabilities", async (c) => {
3328
- const chatRuntime = options.chatRuntime;
3329
- if (!chatRuntime) {
3330
- return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
3331
- }
3332
- const query = c.req.query();
3333
- const params = {
3334
- sessionKey: readNonEmptyString(query.sessionKey),
3335
- agentId: readNonEmptyString(query.agentId)
3336
- };
3337
- try {
3338
- const capabilities = chatRuntime.getCapabilities ? await chatRuntime.getCapabilities(params) : { stopSupported: Boolean(chatRuntime.stopTurn) };
3339
- return c.json(ok(capabilities));
3340
- } catch (error) {
3341
- 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);
3342
3712
  }
3343
- });
3344
- app.get("/api/chat/session-types", async (c) => {
3345
3713
  try {
3346
- const payload = await buildChatSessionTypesView(options.chatRuntime);
3714
+ const payload = await manageMarketplacePlugin({
3715
+ options: this.options,
3716
+ body: body.data
3717
+ });
3347
3718
  return c.json(ok(payload));
3348
3719
  } catch (error) {
3349
- 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);
3350
3728
  }
3351
- });
3352
- app.get("/api/chat/commands", async (c) => {
3353
- try {
3354
- const config = loadConfigOrDefault(options.configPath);
3355
- const registry = new NextclawCore.CommandRegistry(config);
3356
- const commands = registry.listSlashCommands().map((command) => ({
3357
- name: command.name,
3358
- description: command.description,
3359
- ...Array.isArray(command.options) && command.options.length > 0 ? {
3360
- options: command.options.map((option) => ({
3361
- name: option.name,
3362
- description: option.description,
3363
- type: option.type,
3364
- ...option.required === true ? { required: true } : {}
3365
- }))
3366
- } : {}
3367
- }));
3368
- const payload = {
3369
- commands,
3370
- total: commands.length
3371
- };
3372
- return c.json(ok(payload));
3373
- } catch (error) {
3374
- 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);
3375
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
3376
3771
  });
3377
- app.post("/api/chat/turn", async (c) => {
3378
- if (!options.chatRuntime) {
3379
- return c.json(err("NOT_AVAILABLE", "chat runtime unavailable"), 503);
3380
- }
3381
- const body = await readJson(c.req.raw);
3382
- if (!body.ok) {
3383
- 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);
3384
3825
  }
3385
- const message = readNonEmptyString(body.data.message);
3386
- if (!message) {
3387
- 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
+ );
3388
3833
  }
3389
- const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
3390
- const requestedAt = /* @__PURE__ */ new Date();
3391
- const startedAtMs = requestedAt.getTime();
3392
- const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
3393
- const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
3394
- const requestedModel = readNonEmptyString(body.data.model);
3395
- const request = {
3396
- message,
3397
- sessionKey,
3398
- channel: readNonEmptyString(body.data.channel) ?? "ui",
3399
- chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
3400
- ...requestedAgentId ? { agentId: requestedAgentId } : {},
3401
- ...requestedModel ? { model: requestedModel } : {},
3402
- ...metadata ? { metadata } : {}
3403
- };
3404
- try {
3405
- const result = await options.chatRuntime.processTurn(request);
3406
- const response = buildChatTurnView({
3407
- result,
3408
- fallbackSessionKey: sessionKey,
3409
- requestedAgentId,
3410
- requestedModel,
3411
- requestedAt,
3412
- startedAtMs
3413
- });
3414
- options.publish({ type: "config.updated", payload: { path: "session" } });
3415
- return c.json(ok(response));
3416
- } catch (error) {
3417
- 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);
3418
3858
  }
3419
- });
3420
- app.post("/api/chat/turn/stop", async (c) => {
3421
- const chatRuntime = options.chatRuntime;
3422
- if (!chatRuntime?.stopTurn) {
3423
- 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
+ );
3424
3867
  }
3425
- const body = await readJson(c.req.raw);
3426
- if (!body.ok || !body.data || typeof body.data !== "object") {
3427
- 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);
3428
3870
  }
3429
- const runId = readNonEmptyString(body.data.runId);
3430
- if (!runId) {
3431
- 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);
3432
3881
  }
3433
- const request = {
3434
- runId,
3435
- ...readNonEmptyString(body.data.sessionKey) ? { sessionKey: readNonEmptyString(body.data.sessionKey) } : {},
3436
- ...readNonEmptyString(body.data.agentId) ? { agentId: readNonEmptyString(body.data.agentId) } : {}
3437
- };
3438
- try {
3439
- const result = await chatRuntime.stopTurn(request);
3440
- return c.json(ok(result));
3441
- } catch (error) {
3442
- 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
+ );
3443
3890
  }
3444
- });
3445
- app.post("/api/chat/turn/stream", async (c) => {
3446
- const chatRuntime = options.chatRuntime;
3447
- if (!chatRuntime) {
3448
- 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);
3449
3900
  }
3901
+ return c.json(ok(contentResult.data));
3902
+ };
3903
+ install = async (c) => {
3450
3904
  const body = await readJson(c.req.raw);
3451
- if (!body.ok) {
3905
+ if (!body.ok || !body.data || typeof body.data !== "object") {
3452
3906
  return c.json(err("INVALID_BODY", "invalid json body"), 400);
3453
3907
  }
3454
- const message = readNonEmptyString(body.data.message);
3455
- if (!message) {
3456
- return c.json(err("INVALID_BODY", "message is required"), 400);
3457
- }
3458
- const sessionKey = readNonEmptyString(body.data.sessionKey) ?? `ui:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
3459
- const requestedAt = /* @__PURE__ */ new Date();
3460
- const startedAtMs = requestedAt.getTime();
3461
- const metadata = isRecord(body.data.metadata) ? body.data.metadata : void 0;
3462
- const requestedAgentId = readNonEmptyString(body.data.agentId) ?? resolveAgentIdFromSessionKey(sessionKey);
3463
- const requestedModel = readNonEmptyString(body.data.model);
3464
- let runId = createChatRunId();
3465
- const supportsManagedRuns = Boolean(chatRuntime.startTurnRun && chatRuntime.streamRun);
3466
- let stopCapabilities = { stopSupported: Boolean(chatRuntime.stopTurn) };
3467
- if (chatRuntime.getCapabilities) {
3468
- try {
3469
- stopCapabilities = await chatRuntime.getCapabilities({
3470
- sessionKey,
3471
- ...requestedAgentId ? { agentId: requestedAgentId } : {}
3472
- });
3473
- } catch {
3474
- stopCapabilities = {
3475
- stopSupported: false,
3476
- stopReason: "failed to resolve runtime stop capability"
3477
- };
3478
- }
3479
- }
3480
- const request = {
3481
- message,
3482
- sessionKey,
3483
- channel: readNonEmptyString(body.data.channel) ?? "ui",
3484
- chatId: readNonEmptyString(body.data.chatId) ?? "web-ui",
3485
- runId,
3486
- ...requestedAgentId ? { agentId: requestedAgentId } : {},
3487
- ...requestedModel ? { model: requestedModel } : {},
3488
- ...metadata ? { metadata } : {}
3489
- };
3490
- let managedRun = null;
3491
- if (supportsManagedRuns && chatRuntime.startTurnRun) {
3492
- try {
3493
- managedRun = await chatRuntime.startTurnRun(request);
3494
- } catch (error) {
3495
- return c.json(err("CHAT_TURN_FAILED", formatUserFacingError(error)), 500);
3496
- }
3497
- if (readNonEmptyString(managedRun.runId)) {
3498
- runId = readNonEmptyString(managedRun.runId);
3499
- }
3500
- stopCapabilities = {
3501
- stopSupported: managedRun.stopSupported,
3502
- ...readNonEmptyString(managedRun.stopReason) ? { stopReason: readNonEmptyString(managedRun.stopReason) } : {}
3503
- };
3504
- }
3505
- const encoder = new TextEncoder();
3506
- const stream = new ReadableStream({
3507
- start: async (controller) => {
3508
- const push = (event, data) => {
3509
- controller.enqueue(encoder.encode(toSseFrame(event, data)));
3510
- };
3511
- try {
3512
- push("ready", {
3513
- sessionKey: managedRun?.sessionKey ?? sessionKey,
3514
- requestedAt: managedRun?.requestedAt ?? requestedAt.toISOString(),
3515
- runId,
3516
- stopSupported: stopCapabilities.stopSupported,
3517
- ...readNonEmptyString(stopCapabilities.stopReason) ? { stopReason: readNonEmptyString(stopCapabilities.stopReason) } : {}
3518
- });
3519
- if (supportsManagedRuns && chatRuntime.streamRun) {
3520
- let hasFinal2 = false;
3521
- for await (const event of chatRuntime.streamRun({ runId })) {
3522
- const typed = event;
3523
- if (typed.type === "delta") {
3524
- if (typed.delta) {
3525
- push("delta", { delta: typed.delta });
3526
- }
3527
- continue;
3528
- }
3529
- if (typed.type === "session_event") {
3530
- push("session_event", typed.event);
3531
- continue;
3532
- }
3533
- if (typed.type === "final") {
3534
- const latestRun = chatRuntime.getRun ? await chatRuntime.getRun({ runId }) : null;
3535
- const response = latestRun ? buildChatTurnViewFromRun({
3536
- run: latestRun,
3537
- fallbackSessionKey: sessionKey,
3538
- fallbackAgentId: requestedAgentId,
3539
- fallbackModel: requestedModel,
3540
- fallbackReply: typed.result.reply
3541
- }) : buildChatTurnView({
3542
- result: typed.result,
3543
- fallbackSessionKey: sessionKey,
3544
- requestedAgentId,
3545
- requestedModel,
3546
- requestedAt,
3547
- startedAtMs
3548
- });
3549
- hasFinal2 = true;
3550
- push("final", response);
3551
- options.publish({ type: "config.updated", payload: { path: "session" } });
3552
- continue;
3553
- }
3554
- if (typed.type === "error") {
3555
- push("error", {
3556
- code: "CHAT_TURN_FAILED",
3557
- message: formatUserFacingError(typed.error)
3558
- });
3559
- return;
3560
- }
3561
- }
3562
- if (!hasFinal2) {
3563
- push("error", {
3564
- code: "CHAT_TURN_FAILED",
3565
- message: "stream ended without a final result"
3566
- });
3567
- return;
3568
- }
3569
- push("done", { ok: true });
3570
- return;
3571
- }
3572
- const streamTurn = chatRuntime.processTurnStream;
3573
- if (!streamTurn) {
3574
- const result = await chatRuntime.processTurn(request);
3575
- const response = buildChatTurnView({
3576
- result,
3577
- fallbackSessionKey: sessionKey,
3578
- requestedAgentId,
3579
- requestedModel,
3580
- requestedAt,
3581
- startedAtMs
3582
- });
3583
- push("final", response);
3584
- options.publish({ type: "config.updated", payload: { path: "session" } });
3585
- push("done", { ok: true });
3586
- return;
3587
- }
3588
- let hasFinal = false;
3589
- for await (const event of streamTurn(request)) {
3590
- const typed = event;
3591
- if (typed.type === "delta") {
3592
- if (typed.delta) {
3593
- push("delta", { delta: typed.delta });
3594
- }
3595
- continue;
3596
- }
3597
- if (typed.type === "session_event") {
3598
- push("session_event", typed.event);
3599
- continue;
3600
- }
3601
- if (typed.type === "final") {
3602
- const response = buildChatTurnView({
3603
- result: typed.result,
3604
- fallbackSessionKey: sessionKey,
3605
- requestedAgentId,
3606
- requestedModel,
3607
- requestedAt,
3608
- startedAtMs
3609
- });
3610
- hasFinal = true;
3611
- push("final", response);
3612
- options.publish({ type: "config.updated", payload: { path: "session" } });
3613
- continue;
3614
- }
3615
- if (typed.type === "error") {
3616
- push("error", {
3617
- code: "CHAT_TURN_FAILED",
3618
- message: formatUserFacingError(typed.error)
3619
- });
3620
- return;
3621
- }
3622
- }
3623
- if (!hasFinal) {
3624
- push("error", {
3625
- code: "CHAT_TURN_FAILED",
3626
- message: "stream ended without a final result"
3627
- });
3628
- return;
3629
- }
3630
- push("done", { ok: true });
3631
- } catch (error) {
3632
- push("error", {
3633
- code: "CHAT_TURN_FAILED",
3634
- message: formatUserFacingError(error)
3635
- });
3636
- } finally {
3637
- controller.close();
3638
- }
3639
- }
3640
- });
3641
- return new Response(stream, {
3642
- status: 200,
3643
- headers: {
3644
- "Content-Type": "text/event-stream; charset=utf-8",
3645
- "Cache-Control": "no-cache, no-transform",
3646
- "Connection": "keep-alive",
3647
- "X-Accel-Buffering": "no"
3648
- }
3649
- });
3650
- });
3651
- app.get("/api/chat/runs", async (c) => {
3652
- const chatRuntime = options.chatRuntime;
3653
- if (!chatRuntime?.listRuns) {
3654
- 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);
3655
3910
  }
3656
- const query = c.req.query();
3657
- const sessionKey = readNonEmptyString(query.sessionKey);
3658
- const states = readChatRunStates(query.states);
3659
- const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
3660
3911
  try {
3661
- const data = await chatRuntime.listRuns({
3662
- ...sessionKey ? { sessionKey } : {},
3663
- ...states ? { states } : {},
3664
- ...Number.isFinite(limit) ? { limit } : {}
3912
+ const payload = await installMarketplaceSkill({
3913
+ options: this.options,
3914
+ body: body.data
3665
3915
  });
3666
- return c.json(ok(data));
3916
+ return c.json(ok(payload));
3667
3917
  } catch (error) {
3668
- 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);
3669
3926
  }
3670
- });
3671
- app.get("/api/chat/runs/:runId", async (c) => {
3672
- const chatRuntime = options.chatRuntime;
3673
- if (!chatRuntime?.getRun) {
3674
- 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);
3675
3932
  }
3676
- const runId = readNonEmptyString(c.req.param("runId"));
3677
- if (!runId) {
3678
- 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);
3679
3935
  }
3680
3936
  try {
3681
- const run = await chatRuntime.getRun({ runId });
3682
- if (!run) {
3683
- return c.json(err("NOT_FOUND", `chat run not found: ${runId}`), 404);
3684
- }
3685
- 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));
3686
3942
  } catch (error) {
3687
- return c.json(err("CHAT_RUN_QUERY_FAILED", String(error)), 500);
3688
- }
3689
- });
3690
- app.get("/api/chat/runs/:runId/stream", async (c) => {
3691
- const chatRuntime = options.chatRuntime;
3692
- const streamRun = chatRuntime?.streamRun;
3693
- const getRun = chatRuntime?.getRun;
3694
- if (!streamRun || !getRun) {
3695
- return c.json(err("NOT_AVAILABLE", "chat run stream unavailable"), 503);
3696
- }
3697
- const runId = readNonEmptyString(c.req.param("runId"));
3698
- if (!runId) {
3699
- 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);
3700
3951
  }
3952
+ };
3953
+ getRecommendations = async (c) => {
3701
3954
  const query = c.req.query();
3702
- const fromEventIndex = typeof query.fromEventIndex === "string" ? Number.parseInt(query.fromEventIndex, 10) : void 0;
3703
- const run = await getRun({ runId });
3704
- if (!run) {
3705
- return c.json(err("NOT_FOUND", `chat run not found: ${runId}`), 404);
3706
- }
3707
- const encoder = new TextEncoder();
3708
- const stream = new ReadableStream({
3709
- start: async (controller) => {
3710
- const push = (event, data) => {
3711
- controller.enqueue(encoder.encode(toSseFrame(event, data)));
3712
- };
3713
- try {
3714
- push("ready", {
3715
- sessionKey: run.sessionKey,
3716
- requestedAt: run.requestedAt,
3717
- runId: run.runId,
3718
- stopSupported: run.stopSupported,
3719
- ...readNonEmptyString(run.stopReason) ? { stopReason: readNonEmptyString(run.stopReason) } : {}
3720
- });
3721
- let hasFinal = false;
3722
- for await (const event of streamRun({
3723
- runId: run.runId,
3724
- ...Number.isFinite(fromEventIndex) ? { fromEventIndex } : {}
3725
- })) {
3726
- const typed = event;
3727
- if (typed.type === "delta") {
3728
- if (typed.delta) {
3729
- push("delta", { delta: typed.delta });
3730
- }
3731
- continue;
3732
- }
3733
- if (typed.type === "session_event") {
3734
- push("session_event", typed.event);
3735
- continue;
3736
- }
3737
- if (typed.type === "final") {
3738
- const latestRun = await getRun({ runId: run.runId });
3739
- const response = latestRun ? buildChatTurnViewFromRun({
3740
- run: latestRun,
3741
- fallbackSessionKey: run.sessionKey,
3742
- fallbackAgentId: run.agentId,
3743
- fallbackModel: run.model,
3744
- fallbackReply: typed.result.reply
3745
- }) : buildChatTurnView({
3746
- result: typed.result,
3747
- fallbackSessionKey: run.sessionKey,
3748
- requestedAgentId: run.agentId,
3749
- requestedModel: run.model,
3750
- requestedAt: new Date(run.requestedAt),
3751
- startedAtMs: Date.parse(run.requestedAt)
3752
- });
3753
- hasFinal = true;
3754
- push("final", response);
3755
- continue;
3756
- }
3757
- if (typed.type === "error") {
3758
- push("error", {
3759
- code: "CHAT_TURN_FAILED",
3760
- message: formatUserFacingError(typed.error)
3761
- });
3762
- return;
3763
- }
3764
- }
3765
- if (!hasFinal) {
3766
- const latestRun = await getRun({ runId: run.runId });
3767
- if (latestRun?.state === "failed") {
3768
- push("error", {
3769
- code: "CHAT_TURN_FAILED",
3770
- message: formatUserFacingError(latestRun.error ?? "chat run failed")
3771
- });
3772
- return;
3773
- }
3774
- }
3775
- push("done", { ok: true });
3776
- } catch (error) {
3777
- push("error", {
3778
- code: "CHAT_TURN_FAILED",
3779
- message: formatUserFacingError(error)
3780
- });
3781
- } finally {
3782
- controller.close();
3783
- }
3784
- }
3785
- });
3786
- return new Response(stream, {
3787
- status: 200,
3788
- headers: {
3789
- "Content-Type": "text/event-stream; charset=utf-8",
3790
- "Cache-Control": "no-cache, no-transform",
3791
- "Connection": "keep-alive",
3792
- "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
3793
3961
  }
3794
3962
  });
3795
- });
3796
- 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) => {
3797
3982
  const query = c.req.query();
3798
3983
  const q = typeof query.q === "string" ? query.q : void 0;
3799
3984
  const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
3800
3985
  const activeMinutes = typeof query.activeMinutes === "string" ? Number.parseInt(query.activeMinutes, 10) : void 0;
3801
- const data = listSessions(options.configPath, {
3986
+ const data = listSessions(this.options.configPath, {
3802
3987
  q,
3803
3988
  limit: Number.isFinite(limit) ? limit : void 0,
3804
3989
  activeMinutes: Number.isFinite(activeMinutes) ? activeMinutes : void 0
3805
3990
  });
3806
3991
  return c.json(ok(data));
3807
- });
3808
- app.get("/api/sessions/:key/history", (c) => {
3992
+ };
3993
+ getSessionHistory = (c) => {
3809
3994
  const key = decodeURIComponent(c.req.param("key"));
3810
3995
  const query = c.req.query();
3811
3996
  const limit = typeof query.limit === "string" ? Number.parseInt(query.limit, 10) : void 0;
3812
- 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);
3813
3998
  if (!data) {
3814
3999
  return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
3815
4000
  }
3816
4001
  return c.json(ok(data));
3817
- });
3818
- app.put("/api/sessions/:key", async (c) => {
4002
+ };
4003
+ patchSession = async (c) => {
3819
4004
  const key = decodeURIComponent(c.req.param("key"));
3820
4005
  const body = await readJson(c.req.raw);
3821
4006
  if (!body.ok || !body.data || typeof body.data !== "object") {
@@ -3823,12 +4008,12 @@ function createUiRouter(options) {
3823
4008
  }
3824
4009
  let availableSessionTypes;
3825
4010
  if (Object.prototype.hasOwnProperty.call(body.data, "sessionType")) {
3826
- const sessionTypes = await buildChatSessionTypesView(options.chatRuntime);
4011
+ const sessionTypes = await buildChatSessionTypesView(this.options.chatRuntime);
3827
4012
  availableSessionTypes = sessionTypes.options.map((item) => item.value);
3828
4013
  }
3829
4014
  let data;
3830
4015
  try {
3831
- data = patchSession(options.configPath, key, body.data, {
4016
+ data = patchSession(this.options.configPath, key, body.data, {
3832
4017
  ...availableSessionTypes ? { availableSessionTypes } : {}
3833
4018
  });
3834
4019
  } catch (error) {
@@ -3843,111 +4028,81 @@ function createUiRouter(options) {
3843
4028
  if (!data) {
3844
4029
  return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
3845
4030
  }
3846
- options.publish({ type: "config.updated", payload: { path: "session" } });
4031
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
3847
4032
  return c.json(ok(data));
3848
- });
3849
- app.delete("/api/sessions/:key", (c) => {
4033
+ };
4034
+ deleteSession = (c) => {
3850
4035
  const key = decodeURIComponent(c.req.param("key"));
3851
- const deleted = deleteSession(options.configPath, key);
4036
+ const deleted = deleteSession(this.options.configPath, key);
3852
4037
  if (!deleted) {
3853
4038
  return c.json(err("NOT_FOUND", `session not found: ${key}`), 404);
3854
4039
  }
3855
- options.publish({ type: "config.updated", payload: { path: "session" } });
3856
- return c.json(ok({ deleted: true }));
3857
- });
3858
- app.get("/api/cron", (c) => {
3859
- if (!options.cronService) {
3860
- return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
3861
- }
3862
- const query = c.req.query();
3863
- const includeDisabled = query.all === "1" || query.all === "true" || query.all === "yes";
3864
- const jobs = options.cronService.listJobs(includeDisabled).map((job) => buildCronJobView(job));
3865
- return c.json(ok({ jobs, total: jobs.length }));
3866
- });
3867
- app.delete("/api/cron/:id", (c) => {
3868
- if (!options.cronService) {
3869
- return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
3870
- }
3871
- const id = decodeURIComponent(c.req.param("id"));
3872
- const deleted = options.cronService.removeJob(id);
3873
- if (!deleted) {
3874
- return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
3875
- }
4040
+ this.options.publish({ type: "config.updated", payload: { path: "session" } });
3876
4041
  return c.json(ok({ deleted: true }));
3877
- });
3878
- app.put("/api/cron/:id/enable", async (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 body = await readJson(c.req.raw);
3884
- if (!body.ok) {
3885
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3886
- }
3887
- if (typeof body.data.enabled !== "boolean") {
3888
- return c.json(err("INVALID_BODY", "enabled must be boolean"), 400);
3889
- }
3890
- const job = options.cronService.enableJob(id, body.data.enabled);
3891
- if (!job) {
3892
- return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
3893
- }
3894
- const data = { job: buildCronJobView(job) };
3895
- return c.json(ok(data));
3896
- });
3897
- app.post("/api/cron/:id/run", async (c) => {
3898
- if (!options.cronService) {
3899
- return c.json(err("NOT_AVAILABLE", "cron service unavailable"), 503);
3900
- }
3901
- const id = decodeURIComponent(c.req.param("id"));
3902
- const body = await readJson(c.req.raw);
3903
- if (!body.ok) {
3904
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3905
- }
3906
- const existing = findCronJob(options.cronService, id);
3907
- if (!existing) {
3908
- return c.json(err("NOT_FOUND", `cron job not found: ${id}`), 404);
3909
- }
3910
- const executed = await options.cronService.runJob(id, Boolean(body.data.force));
3911
- const after = findCronJob(options.cronService, id);
3912
- const data = {
3913
- job: after ? buildCronJobView(after) : null,
3914
- executed
3915
- };
3916
- return c.json(ok(data));
3917
- });
3918
- app.put("/api/config/runtime", async (c) => {
3919
- const body = await readJson(c.req.raw);
3920
- if (!body.ok || !body.data || typeof body.data !== "object") {
3921
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3922
- }
3923
- const result = updateRuntime(options.configPath, body.data);
3924
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "contextTokens")) {
3925
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.contextTokens" } });
3926
- }
3927
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engine")) {
3928
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.engine" } });
3929
- }
3930
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engineConfig")) {
3931
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.engineConfig" } });
3932
- }
3933
- options.publish({ type: "config.updated", payload: { path: "agents.list" } });
3934
- options.publish({ type: "config.updated", payload: { path: "bindings" } });
3935
- options.publish({ type: "config.updated", payload: { path: "session" } });
3936
- return c.json(ok(result));
3937
- });
3938
- app.post("/api/config/actions/:actionId/execute", async (c) => {
3939
- const actionId = c.req.param("actionId");
3940
- const body = await readJson(c.req.raw);
3941
- if (!body.ok) {
3942
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3943
- }
3944
- const result = await executeConfigAction(options.configPath, actionId, body.data ?? {});
3945
- if (!result.ok) {
3946
- return c.json(err(result.code, result.message, result.details), 400);
3947
- }
3948
- return c.json(ok(result.data));
3949
- });
3950
- 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);
3951
4106
  return app;
3952
4107
  }
3953
4108