@nextclaw/server 0.5.25 → 0.5.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ type ApiResponse<T> = {
15
15
  error: ApiError;
16
16
  };
17
17
  type ProviderConfigView = {
18
+ displayName?: string;
18
19
  apiKeySet: boolean;
19
20
  apiKeyMasked?: string;
20
21
  apiBase?: string | null;
@@ -23,6 +24,7 @@ type ProviderConfigView = {
23
24
  models?: string[];
24
25
  };
25
26
  type ProviderConfigUpdate = {
27
+ displayName?: string | null;
26
28
  apiKey?: string | null;
27
29
  apiBase?: string | null;
28
30
  extraHeaders?: Record<string, string> | null;
@@ -32,6 +34,15 @@ type ProviderConfigUpdate = {
32
34
  type ProviderConnectionTestRequest = ProviderConfigUpdate & {
33
35
  model?: string | null;
34
36
  };
37
+ type ProviderCreateRequest = ProviderConfigUpdate;
38
+ type ProviderCreateResult = {
39
+ name: string;
40
+ provider: ProviderConfigView;
41
+ };
42
+ type ProviderDeleteResult = {
43
+ deleted: boolean;
44
+ provider: string;
45
+ };
35
46
  type ProviderConnectionTestResult = {
36
47
  success: boolean;
37
48
  provider: string;
@@ -288,6 +299,7 @@ type ConfigView = {
288
299
  type ProviderSpecView = {
289
300
  name: string;
290
301
  displayName?: string;
302
+ isCustom?: boolean;
291
303
  modelPrefix?: string;
292
304
  keywords: string[];
293
305
  envKey: string;
@@ -387,12 +399,14 @@ type MarketplaceInstallSpec = {
387
399
  spec: string;
388
400
  command: string;
389
401
  };
402
+ type MarketplaceLocalizedTextMap = Record<string, string>;
390
403
  type MarketplaceItemSummary = {
391
404
  id: string;
392
405
  slug: string;
393
406
  type: MarketplaceItemType;
394
407
  name: string;
395
408
  summary: string;
409
+ summaryI18n: MarketplaceLocalizedTextMap;
396
410
  tags: string[];
397
411
  author: string;
398
412
  install: MarketplaceInstallSpec;
@@ -400,10 +414,33 @@ type MarketplaceItemSummary = {
400
414
  };
401
415
  type MarketplaceItemView = MarketplaceItemSummary & {
402
416
  description?: string;
417
+ descriptionI18n?: MarketplaceLocalizedTextMap;
403
418
  sourceRepo?: string;
404
419
  homepage?: string;
405
420
  publishedAt: string;
406
421
  };
422
+ type MarketplaceSkillContentView = {
423
+ type: "skill";
424
+ slug: string;
425
+ name: string;
426
+ install: MarketplaceInstallSpec;
427
+ source: "workspace" | "builtin" | "git" | "remote";
428
+ raw: string;
429
+ metadataRaw?: string;
430
+ bodyRaw: string;
431
+ sourceUrl?: string;
432
+ };
433
+ type MarketplacePluginContentView = {
434
+ type: "plugin";
435
+ slug: string;
436
+ name: string;
437
+ install: MarketplaceInstallSpec;
438
+ source: "npm" | "repo" | "remote";
439
+ raw?: string;
440
+ bodyRaw?: string;
441
+ metadataRaw?: string;
442
+ sourceUrl?: string;
443
+ };
407
444
  type MarketplaceListView = {
408
445
  total: number;
409
446
  page: number;
@@ -597,6 +634,11 @@ declare function updateModel(configPath: string, patch: {
597
634
  maxTokens?: number;
598
635
  }): ConfigView;
599
636
  declare function updateProvider(configPath: string, providerName: string, patch: ProviderConfigUpdate): ProviderConfigView | null;
637
+ declare function createCustomProvider(configPath: string, patch?: ProviderConfigUpdate): {
638
+ name: string;
639
+ provider: ProviderConfigView;
640
+ };
641
+ declare function deleteCustomProvider(configPath: string, providerName: string): boolean | null;
600
642
  declare function testProviderConnection(configPath: string, providerName: string, patch: ProviderConnectionTestRequest): Promise<ProviderConnectionTestResult | null>;
601
643
  declare function updateChannel(configPath: string, channelName: string, patch: Record<string, unknown>): Record<string, unknown> | null;
602
644
  declare function listSessions(configPath: string, query?: {
@@ -610,4 +652,4 @@ declare function deleteSession(configPath: string, key: string): boolean;
610
652
  declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
611
653
  declare function updateSecrets(configPath: string, patch: SecretsConfigUpdate): SecretsView;
612
654
 
613
- export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ChatTurnRequest, type ChatTurnResult, type ChatTurnStreamEvent, type ChatTurnView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type CronActionResult, type CronEnableRequest, type CronJobStateView, type CronJobView, type CronListView, type CronPayloadView, type CronRunRequest, type CronScheduleView, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderSpecView, type RuntimeConfigUpdate, type SecretProviderEnvView, type SecretProviderExecView, type SecretProviderFileView, type SecretProviderView, type SecretRefView, type SecretSourceView, type SecretsConfigUpdate, type SecretsView, type SessionConfigView, type SessionEntryView, type SessionEventView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, type SessionsListView, type UiChatRuntime, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createUiRouter, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSecrets };
655
+ export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ChatTurnRequest, type ChatTurnResult, type ChatTurnStreamEvent, type ChatTurnView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type CronActionResult, type CronEnableRequest, type CronJobStateView, type CronJobView, type CronListView, type CronPayloadView, type CronRunRequest, type CronScheduleView, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplaceLocalizedTextMap, type MarketplacePluginContentView, type MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillContentView, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderCreateRequest, type ProviderCreateResult, type ProviderDeleteResult, type ProviderSpecView, type RuntimeConfigUpdate, type SecretProviderEnvView, type SecretProviderExecView, type SecretProviderFileView, type SecretProviderView, type SecretRefView, type SecretSourceView, type SecretsConfigUpdate, type SecretsView, type SessionConfigView, type SessionEntryView, type SessionEventView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, type SessionsListView, type UiChatRuntime, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createCustomProvider, createUiRouter, deleteCustomProvider, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSecrets };
package/dist/index.js CHANGED
@@ -5,11 +5,12 @@ import { cors } from "hono/cors";
5
5
  import { serve } from "@hono/node-server";
6
6
  import { WebSocketServer, WebSocket } from "ws";
7
7
  import { existsSync, readFileSync } from "fs";
8
- import { readFile, stat } from "fs/promises";
8
+ import { readFile as readFile2, stat } from "fs/promises";
9
9
  import { join } from "path";
10
10
 
11
11
  // src/ui/router.ts
12
12
  import { Hono } from "hono";
13
+ import { readFile } from "fs/promises";
13
14
  import * as NextclawCore from "@nextclaw/core";
14
15
  import { buildPluginStatusReport } from "@nextclaw/openclaw-compat";
15
16
 
@@ -34,7 +35,7 @@ var MASK_MIN_LENGTH = 8;
34
35
  var EXTRA_SENSITIVE_PATH_PATTERNS = [/authorization/i, /cookie/i, /session/i, /bearer/i];
35
36
  var PROVIDER_TEST_MODEL_FALLBACKS = {
36
37
  openai: "gpt-5-mini",
37
- deepseek: "deepseek-v3.2",
38
+ deepseek: "deepseek-chat",
38
39
  gemini: "gemini-3-flash-preview",
39
40
  zhipu: "glm-5",
40
41
  dashscope: "qwen3.5-flash",
@@ -59,6 +60,53 @@ var PREFERRED_PROVIDER_ORDER = [
59
60
  var PREFERRED_PROVIDER_ORDER_INDEX = new Map(
60
61
  PREFERRED_PROVIDER_ORDER.map((name, index) => [name, index])
61
62
  );
63
+ var BUILTIN_PROVIDER_NAMES = new Set(PROVIDERS.map((spec) => spec.name));
64
+ var CUSTOM_PROVIDER_WIRE_API_OPTIONS = ["auto", "chat", "responses"];
65
+ var CUSTOM_PROVIDER_PREFIX = "custom-";
66
+ function normalizeOptionalDisplayName(value) {
67
+ if (typeof value !== "string") {
68
+ return null;
69
+ }
70
+ const trimmed = value.trim();
71
+ return trimmed.length > 0 ? trimmed : null;
72
+ }
73
+ function isCustomProviderName(name) {
74
+ return name.trim().length > 0 && !BUILTIN_PROVIDER_NAMES.has(name);
75
+ }
76
+ function resolveCustomProviderFallbackDisplayName(name) {
77
+ if (name.startsWith(CUSTOM_PROVIDER_PREFIX)) {
78
+ const suffix = name.slice(CUSTOM_PROVIDER_PREFIX.length);
79
+ if (/^\d+$/.test(suffix)) {
80
+ return `Custom ${suffix}`;
81
+ }
82
+ }
83
+ return name;
84
+ }
85
+ function resolveProviderDisplayName(providerName, provider, spec) {
86
+ const configDisplayName = normalizeOptionalDisplayName(provider?.displayName);
87
+ if (isCustomProviderName(providerName)) {
88
+ return configDisplayName ?? resolveCustomProviderFallbackDisplayName(providerName);
89
+ }
90
+ return spec?.displayName ?? configDisplayName ?? spec?.name;
91
+ }
92
+ function listCustomProviderNames(config) {
93
+ return Object.keys(config.providers).filter((name) => isCustomProviderName(name));
94
+ }
95
+ function findNextCustomProviderName(config) {
96
+ const providers = config.providers;
97
+ let index = 1;
98
+ while (providers[`${CUSTOM_PROVIDER_PREFIX}${index}`]) {
99
+ index += 1;
100
+ }
101
+ return `${CUSTOM_PROVIDER_PREFIX}${index}`;
102
+ }
103
+ function clearSecretRefsByPrefix(config, pathPrefix) {
104
+ for (const key of Object.keys(config.secrets.refs)) {
105
+ if (key === pathPrefix || key.startsWith(`${pathPrefix}.`)) {
106
+ delete config.secrets.refs[key];
107
+ }
108
+ }
109
+ }
62
110
  var DOCS_BASE_URL = "https://docs.nextclaw.io";
63
111
  var CHANNEL_TUTORIAL_URLS = {
64
112
  feishu: {
@@ -295,14 +343,16 @@ function toProviderView(config, provider, providerName, uiHints, spec) {
295
343
  uiHints
296
344
  ) : null;
297
345
  const view = {
346
+ displayName: resolveProviderDisplayName(providerName, provider, spec),
298
347
  apiKeySet: masked.apiKeySet || apiKeyRefSet,
299
348
  apiKeyMasked: masked.apiKeyMasked ?? (apiKeyRefSet ? "****" : void 0),
300
349
  apiBase: provider.apiBase ?? null,
301
350
  extraHeaders: extraHeaders && Object.keys(extraHeaders).length > 0 ? extraHeaders : null,
302
351
  models: normalizeModelList(provider.models ?? [])
303
352
  };
304
- if (spec?.supportsWireApi) {
305
- view.wireApi = provider.wireApi ?? spec.defaultWireApi ?? "auto";
353
+ const supportsWireApi = Boolean(spec?.supportsWireApi) || isCustomProviderName(providerName);
354
+ if (supportsWireApi) {
355
+ view.wireApi = provider.wireApi ?? spec?.defaultWireApi ?? "auto";
306
356
  }
307
357
  return view;
308
358
  }
@@ -340,20 +390,25 @@ function clearSecretRef(config, path) {
340
390
  }
341
391
  }
342
392
  function buildConfigMeta(config) {
343
- const providers = PROVIDERS.map((spec) => ({
344
- name: spec.name,
345
- displayName: spec.displayName,
346
- modelPrefix: spec.modelPrefix,
347
- keywords: spec.keywords,
348
- envKey: spec.envKey,
349
- isGateway: spec.isGateway,
350
- isLocal: spec.isLocal,
351
- defaultApiBase: spec.defaultApiBase,
352
- defaultModels: normalizeModelList(spec.defaultModels ?? []),
353
- supportsWireApi: spec.supportsWireApi,
354
- wireApiOptions: spec.wireApiOptions,
355
- defaultWireApi: spec.defaultWireApi
356
- })).sort((left, right) => {
393
+ const configProviders = config.providers;
394
+ const builtinProviders = PROVIDERS.map((spec) => {
395
+ const providerConfig = configProviders[spec.name];
396
+ return {
397
+ name: spec.name,
398
+ displayName: resolveProviderDisplayName(spec.name, providerConfig, spec),
399
+ isCustom: false,
400
+ modelPrefix: spec.modelPrefix,
401
+ keywords: spec.keywords,
402
+ envKey: spec.envKey,
403
+ isGateway: spec.isGateway,
404
+ isLocal: spec.isLocal,
405
+ defaultApiBase: spec.defaultApiBase,
406
+ defaultModels: normalizeModelList(spec.defaultModels ?? []),
407
+ supportsWireApi: spec.supportsWireApi,
408
+ wireApiOptions: spec.wireApiOptions,
409
+ defaultWireApi: spec.defaultWireApi
410
+ };
411
+ }).sort((left, right) => {
357
412
  const leftRank = PREFERRED_PROVIDER_ORDER_INDEX.get(left.name);
358
413
  const rightRank = PREFERRED_PROVIDER_ORDER_INDEX.get(right.name);
359
414
  if (leftRank !== void 0 && rightRank !== void 0) {
@@ -367,6 +422,26 @@ function buildConfigMeta(config) {
367
422
  }
368
423
  return left.name.localeCompare(right.name);
369
424
  });
425
+ const customProviders = listCustomProviderNames(config).sort((left, right) => left.localeCompare(right, void 0, { numeric: true, sensitivity: "base" })).map((name) => {
426
+ const providerConfig = configProviders[name];
427
+ const displayName = resolveProviderDisplayName(name, providerConfig);
428
+ return {
429
+ name,
430
+ displayName,
431
+ isCustom: true,
432
+ modelPrefix: name,
433
+ keywords: normalizeModelList([name, displayName ?? ""]),
434
+ envKey: "OPENAI_API_KEY",
435
+ isGateway: false,
436
+ isLocal: false,
437
+ defaultApiBase: void 0,
438
+ defaultModels: [],
439
+ supportsWireApi: true,
440
+ wireApiOptions: CUSTOM_PROVIDER_WIRE_API_OPTIONS,
441
+ defaultWireApi: "auto"
442
+ };
443
+ });
444
+ const providers = [...customProviders, ...builtinProviders];
370
445
  const channels = Object.keys(config.channels).map((name) => {
371
446
  const tutorialUrls = CHANNEL_TUTORIAL_URLS[name];
372
447
  const tutorialUrl = tutorialUrls?.default ?? tutorialUrls?.en ?? tutorialUrls?.zh;
@@ -457,6 +532,10 @@ function updateProvider(configPath, providerName, patch) {
457
532
  return null;
458
533
  }
459
534
  const spec = findProviderByName(providerName);
535
+ const isCustom = isCustomProviderName(providerName);
536
+ if (Object.prototype.hasOwnProperty.call(patch, "displayName") && isCustom) {
537
+ provider.displayName = normalizeOptionalDisplayName(patch.displayName) ?? "";
538
+ }
460
539
  if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
461
540
  provider.apiKey = patch.apiKey ?? "";
462
541
  clearSecretRef(config, `providers.${providerName}.apiKey`);
@@ -467,8 +546,8 @@ function updateProvider(configPath, providerName, patch) {
467
546
  if (Object.prototype.hasOwnProperty.call(patch, "extraHeaders")) {
468
547
  provider.extraHeaders = patch.extraHeaders ?? null;
469
548
  }
470
- if (Object.prototype.hasOwnProperty.call(patch, "wireApi") && spec?.supportsWireApi) {
471
- provider.wireApi = patch.wireApi ?? spec.defaultWireApi ?? "auto";
549
+ if (Object.prototype.hasOwnProperty.call(patch, "wireApi") && (spec?.supportsWireApi || isCustom)) {
550
+ provider.wireApi = patch.wireApi ?? spec?.defaultWireApi ?? "auto";
472
551
  }
473
552
  if (Object.prototype.hasOwnProperty.call(patch, "models")) {
474
553
  provider.models = normalizeModelList(patch.models ?? []);
@@ -479,6 +558,43 @@ function updateProvider(configPath, providerName, patch) {
479
558
  const updated = next.providers[providerName];
480
559
  return toProviderView(next, updated, providerName, uiHints, spec ?? void 0);
481
560
  }
561
+ function createCustomProvider(configPath, patch = {}) {
562
+ const config = loadConfigOrDefault(configPath);
563
+ const providerName = findNextCustomProviderName(config);
564
+ const providers = config.providers;
565
+ const generatedDisplayName = resolveCustomProviderFallbackDisplayName(providerName);
566
+ providers[providerName] = {
567
+ displayName: normalizeOptionalDisplayName(patch.displayName) ?? generatedDisplayName,
568
+ apiKey: normalizeOptionalString(patch.apiKey) ?? "",
569
+ apiBase: normalizeOptionalString(patch.apiBase),
570
+ extraHeaders: normalizeHeaders(patch.extraHeaders ?? null),
571
+ wireApi: patch.wireApi ?? "auto",
572
+ models: normalizeModelList(patch.models ?? [])
573
+ };
574
+ const next = ConfigSchema.parse(config);
575
+ saveConfig(next, configPath);
576
+ const uiHints = buildUiHints(next);
577
+ const created = next.providers[providerName];
578
+ return {
579
+ name: providerName,
580
+ provider: toProviderView(next, created, providerName, uiHints)
581
+ };
582
+ }
583
+ function deleteCustomProvider(configPath, providerName) {
584
+ if (!isCustomProviderName(providerName)) {
585
+ return null;
586
+ }
587
+ const config = loadConfigOrDefault(configPath);
588
+ const providers = config.providers;
589
+ if (!providers[providerName]) {
590
+ return null;
591
+ }
592
+ delete providers[providerName];
593
+ clearSecretRefsByPrefix(config, `providers.${providerName}`);
594
+ const next = ConfigSchema.parse(config);
595
+ saveConfig(next, configPath);
596
+ return true;
597
+ }
482
598
  function normalizeOptionalString(value) {
483
599
  if (typeof value !== "string") {
484
600
  return null;
@@ -496,10 +612,37 @@ function normalizeHeaders(input) {
496
612
  }
497
613
  return Object.fromEntries(entries);
498
614
  }
499
- function resolveTestModel(config, providerName, requestedModel) {
615
+ function buildScopedProviderModel(providerName, model, spec) {
616
+ const trimmed = model.trim();
617
+ if (!trimmed) {
618
+ return "";
619
+ }
620
+ if (trimmed.includes("/")) {
621
+ return trimmed;
622
+ }
623
+ if (isCustomProviderName(providerName)) {
624
+ return trimmed;
625
+ }
626
+ const prefix = (spec?.modelPrefix ?? providerName).trim();
627
+ if (!prefix) {
628
+ return trimmed;
629
+ }
630
+ return `${prefix}/${trimmed}`;
631
+ }
632
+ function resolveTestModel(config, providerName, requestedModel, provider, spec) {
500
633
  if (requestedModel) {
634
+ if (isCustomProviderName(providerName)) {
635
+ const prefix = `${providerName}/`;
636
+ if (requestedModel.startsWith(prefix)) {
637
+ return requestedModel.slice(prefix.length) || null;
638
+ }
639
+ }
501
640
  return requestedModel;
502
641
  }
642
+ const providerModels = normalizeModelList(provider.models ?? []).map((modelId) => buildScopedProviderModel(providerName, modelId, spec)).filter((modelId) => modelId.length > 0);
643
+ if (providerModels.length > 0) {
644
+ return providerModels[0];
645
+ }
503
646
  const defaultModel = normalizeOptionalString(config.agents.defaults.model);
504
647
  if (defaultModel) {
505
648
  const routedProvider = getProviderName(config, defaultModel);
@@ -507,6 +650,9 @@ function resolveTestModel(config, providerName, requestedModel) {
507
650
  return defaultModel;
508
651
  }
509
652
  }
653
+ if (isCustomProviderName(providerName)) {
654
+ return null;
655
+ }
510
656
  return PROVIDER_TEST_MODEL_FALLBACKS[providerName] ?? defaultModel ?? null;
511
657
  }
512
658
  function stringifyError(error) {
@@ -530,7 +676,8 @@ async function testProviderConnection(configPath, providerName, patch) {
530
676
  const apiBase = hasApiBasePatch ? patchedApiBase ?? spec?.defaultApiBase ?? null : currentApiBase ?? spec?.defaultApiBase ?? null;
531
677
  const hasHeadersPatch = Object.prototype.hasOwnProperty.call(patch, "extraHeaders");
532
678
  const extraHeaders = hasHeadersPatch ? normalizeHeaders(patch.extraHeaders ?? null) : normalizeHeaders(provider.extraHeaders ?? null);
533
- const wireApi = spec?.supportsWireApi ? patch.wireApi ?? provider.wireApi ?? spec.defaultWireApi ?? "auto" : null;
679
+ const isCustom = isCustomProviderName(providerName);
680
+ const wireApi = spec?.supportsWireApi || isCustom ? patch.wireApi ?? provider.wireApi ?? spec?.defaultWireApi ?? "auto" : null;
534
681
  if (!apiKey && !spec?.isLocal) {
535
682
  return {
536
683
  success: false,
@@ -540,13 +687,13 @@ async function testProviderConnection(configPath, providerName, patch) {
540
687
  };
541
688
  }
542
689
  const requestedModel = normalizeOptionalString(patch.model);
543
- const model = resolveTestModel(config, providerName, requestedModel);
690
+ const model = resolveTestModel(config, providerName, requestedModel, provider, spec ?? void 0);
544
691
  if (!model) {
545
692
  return {
546
693
  success: false,
547
694
  provider: providerName,
548
695
  latencyMs: 0,
549
- message: "No test model found. Set a default model first, then try again."
696
+ message: "No test model found. Configure provider models or set a default model for this provider, then try again."
550
697
  };
551
698
  }
552
699
  const probe = new LiteLLMProvider({
@@ -1287,6 +1434,141 @@ function sanitizeMarketplaceItem(item) {
1287
1434
  delete next.metrics;
1288
1435
  return next;
1289
1436
  }
1437
+ var MARKETPLACE_ZH_COPY_BY_SLUG = {
1438
+ weather: {
1439
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002",
1440
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u5FEB\u901F\u5929\u6C14\u67E5\u8BE2\u5DE5\u4F5C\u6D41\u3002"
1441
+ },
1442
+ summarize: {
1443
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7ED3\u6784\u5316\u6458\u8981\u3002",
1444
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u6587\u4EF6\u4E0E\u957F\u6587\u672C\u7684\u6458\u8981\u5DE5\u4F5C\u6D41\u3002"
1445
+ },
1446
+ github: {
1447
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E GitHub \u5DE5\u4F5C\u6D41\u3002",
1448
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B Issue\u3001PR \u4E0E\u4ED3\u5E93\u76F8\u5173\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
1449
+ },
1450
+ tmux: {
1451
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u7EC8\u7AEF/Tmux \u534F\u4F5C\u5DE5\u4F5C\u6D41\u3002",
1452
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u57FA\u4E8E Tmux \u7684\u4EFB\u52A1\u6267\u884C\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
1453
+ },
1454
+ gog: {
1455
+ summary: "NextClaw \u5185\u7F6E\u6280\u80FD\uFF0C\u7528\u4E8E\u56FE\u8C31\u5BFC\u5411\u751F\u6210\u5DE5\u4F5C\u6D41\u3002",
1456
+ description: "\u5728 NextClaw \u4E2D\u63D0\u4F9B\u56FE\u8C31\u4E0E\u89C4\u5212\u5BFC\u5411\u5DE5\u4F5C\u6D41\u6307\u5F15\u3002"
1457
+ },
1458
+ pdf: {
1459
+ summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E PDF \u8BFB\u53D6/\u5408\u5E76/\u62C6\u5206/OCR \u5DE5\u4F5C\u6D41\u3002",
1460
+ 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"
1461
+ },
1462
+ docx: {
1463
+ summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u521B\u5EFA\u548C\u7F16\u8F91 Word \u6587\u6863\u3002",
1464
+ description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u8BFB\u53D6\u3001\u7F16\u8F91\u5E76\u91CD\u6784 .docx \u6587\u6863\u3002"
1465
+ },
1466
+ pptx: {
1467
+ summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u6F14\u793A\u6587\u7A3F\u64CD\u4F5C\u3002",
1468
+ description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u521B\u5EFA\u3001\u89E3\u6790\u3001\u7F16\u8F91\u5E76\u91CD\u7EC4 .pptx \u6F14\u793A\u6587\u7A3F\u3002"
1469
+ },
1470
+ xlsx: {
1471
+ summary: "Anthropic \u6280\u80FD\uFF0C\u7528\u4E8E\u8868\u683C\u6587\u6863\u5DE5\u4F5C\u6D41\u3002",
1472
+ 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"
1473
+ },
1474
+ bird: {
1475
+ summary: "OpenClaw \u793E\u533A\u6280\u80FD\uFF0C\u7528\u4E8E X/Twitter \u8BFB\u53D6/\u641C\u7D22/\u53D1\u5E03\u5DE5\u4F5C\u6D41\u3002",
1476
+ 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"
1477
+ },
1478
+ "cloudflare-deploy": {
1479
+ summary: "OpenAI \u7CBE\u9009\u6280\u80FD\uFF0C\u7528\u4E8E\u5728 Cloudflare \u4E0A\u90E8\u7F72\u5E94\u7528\u4E0E\u57FA\u7840\u8BBE\u65BD\u3002",
1480
+ description: "\u4F7F\u7528\u8BE5\u6280\u80FD\u53EF\u9009\u62E9 Cloudflare \u4EA7\u54C1\u5E76\u90E8\u7F72 Workers\u3001Pages \u53CA\u76F8\u5173\u670D\u52A1\u3002"
1481
+ },
1482
+ "channel-plugin-discord": {
1483
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Discord \u6E20\u9053\u96C6\u6210\u3002",
1484
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Discord \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
1485
+ },
1486
+ "channel-plugin-telegram": {
1487
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Telegram \u6E20\u9053\u96C6\u6210\u3002",
1488
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Telegram \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
1489
+ },
1490
+ "channel-plugin-slack": {
1491
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Slack \u6E20\u9053\u96C6\u6210\u3002",
1492
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Slack \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
1493
+ },
1494
+ "channel-plugin-wecom": {
1495
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E\u4F01\u4E1A\u5FAE\u4FE1\u6E20\u9053\u96C6\u6210\u3002",
1496
+ 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"
1497
+ },
1498
+ "channel-plugin-email": {
1499
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E Email \u6E20\u9053\u96C6\u6210\u3002",
1500
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B Email \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
1501
+ },
1502
+ "channel-plugin-whatsapp": {
1503
+ summary: "NextClaw \u5B98\u65B9\u63D2\u4EF6\uFF0C\u7528\u4E8E WhatsApp \u6E20\u9053\u96C6\u6210\u3002",
1504
+ description: "\u901A\u8FC7 NextClaw \u63D2\u4EF6\u8FD0\u884C\u65F6\u63D0\u4F9B WhatsApp \u6E20\u9053\u7684\u5165\u7AD9/\u51FA\u7AD9\u652F\u6301\u3002"
1505
+ },
1506
+ "channel-plugin-clawbay": {
1507
+ summary: "Clawbay \u5B98\u65B9\u6E20\u9053\u63D2\u4EF6\uFF0C\u7528\u4E8E NextClaw \u96C6\u6210\u3002",
1508
+ description: "\u901A\u8FC7\u63D2\u4EF6\u8FD0\u884C\u65F6\u4E3A NextClaw \u63D0\u4F9B Clawbay \u6E20\u9053\u80FD\u529B\u3002"
1509
+ }
1510
+ };
1511
+ function readLocalizedMap(value) {
1512
+ const localized = {};
1513
+ if (!isRecord(value)) {
1514
+ return localized;
1515
+ }
1516
+ for (const [key, entry] of Object.entries(value)) {
1517
+ if (typeof entry !== "string" || entry.trim().length === 0) {
1518
+ continue;
1519
+ }
1520
+ localized[key] = entry.trim();
1521
+ }
1522
+ return localized;
1523
+ }
1524
+ function normalizeLocaleTag(value) {
1525
+ return value.trim().toLowerCase().replace(/_/g, "-");
1526
+ }
1527
+ function pickLocaleFamilyValue(localized, localeFamily) {
1528
+ const normalizedFamily = normalizeLocaleTag(localeFamily).split("-")[0];
1529
+ if (!normalizedFamily) {
1530
+ return void 0;
1531
+ }
1532
+ let familyMatch;
1533
+ for (const [locale, text] of Object.entries(localized)) {
1534
+ const normalizedLocale = normalizeLocaleTag(locale);
1535
+ if (!normalizedLocale) {
1536
+ continue;
1537
+ }
1538
+ if (normalizedLocale === normalizedFamily) {
1539
+ return text;
1540
+ }
1541
+ if (!familyMatch && normalizedLocale.startsWith(`${normalizedFamily}-`)) {
1542
+ familyMatch = text;
1543
+ }
1544
+ }
1545
+ return familyMatch;
1546
+ }
1547
+ function normalizeLocalizedTextMap(primaryText, localized, zhFallback) {
1548
+ const next = readLocalizedMap(localized);
1549
+ if (!next.en) {
1550
+ next.en = pickLocaleFamilyValue(next, "en") ?? primaryText;
1551
+ }
1552
+ if (!next.zh) {
1553
+ next.zh = pickLocaleFamilyValue(next, "zh") ?? (zhFallback && zhFallback.trim().length > 0 ? zhFallback.trim() : next.en);
1554
+ }
1555
+ return next;
1556
+ }
1557
+ function normalizeMarketplaceItemForUi(item) {
1558
+ const zhCopy = MARKETPLACE_ZH_COPY_BY_SLUG[item.slug];
1559
+ const next = {
1560
+ ...item,
1561
+ summaryI18n: normalizeLocalizedTextMap(item.summary, item.summaryI18n, zhCopy?.summary)
1562
+ };
1563
+ if ("description" in item && typeof item.description === "string" && item.description.trim().length > 0) {
1564
+ next.descriptionI18n = normalizeLocalizedTextMap(
1565
+ item.description,
1566
+ item.descriptionI18n,
1567
+ zhCopy?.description
1568
+ );
1569
+ }
1570
+ return next;
1571
+ }
1290
1572
  function toPositiveInt(raw, fallback) {
1291
1573
  if (!raw) {
1292
1574
  return fallback;
@@ -1314,6 +1596,192 @@ function isSupportedMarketplaceSkillItem(item, knownSkillNames) {
1314
1596
  }
1315
1597
  return item.install.kind === "builtin" && knownSkillNames.has(item.install.spec);
1316
1598
  }
1599
+ function splitMarkdownFrontmatter(raw) {
1600
+ const normalized = raw.replace(/\r\n/g, "\n");
1601
+ const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1602
+ if (!match) {
1603
+ return { bodyRaw: normalized };
1604
+ }
1605
+ return {
1606
+ metadataRaw: match[1]?.trim() || void 0,
1607
+ bodyRaw: match[2] ?? ""
1608
+ };
1609
+ }
1610
+ async function loadLocalSkillMarkdown(options, skillName) {
1611
+ const config = loadConfigOrDefault(options.configPath);
1612
+ const loader = createSkillsLoader(getWorkspacePathFromConfig3(config));
1613
+ if (!loader) {
1614
+ return null;
1615
+ }
1616
+ const skillInfo = loader.listSkills(false).find((skill) => skill.name === skillName);
1617
+ if (!skillInfo) {
1618
+ return null;
1619
+ }
1620
+ try {
1621
+ const raw = await readFile(skillInfo.path, "utf-8");
1622
+ return {
1623
+ raw,
1624
+ source: skillInfo.source
1625
+ };
1626
+ } catch {
1627
+ return null;
1628
+ }
1629
+ }
1630
+ function parseGitSkillSpec(rawSpec) {
1631
+ const spec = rawSpec.trim();
1632
+ if (!spec) {
1633
+ return null;
1634
+ }
1635
+ const segments = spec.split("/").filter(Boolean);
1636
+ if (segments.length < 3) {
1637
+ return null;
1638
+ }
1639
+ return {
1640
+ owner: segments[0] ?? "",
1641
+ repo: segments[1] ?? "",
1642
+ skillPath: segments.slice(2).join("/")
1643
+ };
1644
+ }
1645
+ async function fetchTextWithFallback(urls) {
1646
+ for (const url of urls) {
1647
+ try {
1648
+ const response = await fetch(url, {
1649
+ method: "GET",
1650
+ headers: {
1651
+ Accept: "text/plain, text/markdown, application/json"
1652
+ }
1653
+ });
1654
+ if (!response.ok) {
1655
+ continue;
1656
+ }
1657
+ const text = await response.text();
1658
+ if (text.trim().length === 0) {
1659
+ continue;
1660
+ }
1661
+ return { text, url };
1662
+ } catch {
1663
+ continue;
1664
+ }
1665
+ }
1666
+ return null;
1667
+ }
1668
+ async function loadGitSkillMarkdownFromSpec(rawSpec) {
1669
+ const parsed = parseGitSkillSpec(rawSpec);
1670
+ if (!parsed) {
1671
+ return null;
1672
+ }
1673
+ const candidates = [
1674
+ `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/${parsed.skillPath}/SKILL.md`,
1675
+ `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/master/${parsed.skillPath}/SKILL.md`
1676
+ ];
1677
+ const result = await fetchTextWithFallback(candidates);
1678
+ if (!result) {
1679
+ return null;
1680
+ }
1681
+ return {
1682
+ raw: result.text,
1683
+ sourceUrl: result.url
1684
+ };
1685
+ }
1686
+ async function loadPluginReadmeFromNpm(spec) {
1687
+ const encodedSpec = encodeURIComponent(spec);
1688
+ const registryUrl = `https://registry.npmjs.org/${encodedSpec}`;
1689
+ try {
1690
+ const response = await fetch(registryUrl, {
1691
+ headers: {
1692
+ Accept: "application/json"
1693
+ }
1694
+ });
1695
+ if (!response.ok) {
1696
+ return null;
1697
+ }
1698
+ const payload = await response.json();
1699
+ const readme = typeof payload.readme === "string" ? payload.readme : "";
1700
+ const latest = isRecord(payload["dist-tags"]) && typeof payload["dist-tags"].latest === "string" ? payload["dist-tags"].latest : void 0;
1701
+ const metadata = {
1702
+ name: typeof payload.name === "string" ? payload.name : spec,
1703
+ version: latest,
1704
+ description: typeof payload.description === "string" ? payload.description : void 0,
1705
+ homepage: typeof payload.homepage === "string" ? payload.homepage : void 0
1706
+ };
1707
+ if (readme.trim().length === 0) {
1708
+ return null;
1709
+ }
1710
+ return {
1711
+ readme,
1712
+ sourceUrl: registryUrl,
1713
+ metadataRaw: JSON.stringify(metadata, null, 2)
1714
+ };
1715
+ } catch {
1716
+ return null;
1717
+ }
1718
+ }
1719
+ async function buildSkillContentView(options, item) {
1720
+ const local = await loadLocalSkillMarkdown(options, item.install.spec);
1721
+ if (local) {
1722
+ const split = splitMarkdownFrontmatter(local.raw);
1723
+ return {
1724
+ type: "skill",
1725
+ slug: item.slug,
1726
+ name: item.name,
1727
+ install: item.install,
1728
+ source: local.source,
1729
+ raw: local.raw,
1730
+ metadataRaw: split.metadataRaw,
1731
+ bodyRaw: split.bodyRaw
1732
+ };
1733
+ }
1734
+ if (item.install.kind === "git") {
1735
+ const remote = await loadGitSkillMarkdownFromSpec(item.install.spec);
1736
+ if (remote) {
1737
+ const split = splitMarkdownFrontmatter(remote.raw);
1738
+ return {
1739
+ type: "skill",
1740
+ slug: item.slug,
1741
+ name: item.name,
1742
+ install: item.install,
1743
+ source: "git",
1744
+ raw: remote.raw,
1745
+ metadataRaw: split.metadataRaw,
1746
+ bodyRaw: split.bodyRaw,
1747
+ sourceUrl: remote.sourceUrl
1748
+ };
1749
+ }
1750
+ }
1751
+ return null;
1752
+ }
1753
+ async function buildPluginContentView(item) {
1754
+ if (item.install.kind === "npm") {
1755
+ const npm = await loadPluginReadmeFromNpm(item.install.spec);
1756
+ if (npm) {
1757
+ return {
1758
+ type: "plugin",
1759
+ slug: item.slug,
1760
+ name: item.name,
1761
+ install: item.install,
1762
+ source: "npm",
1763
+ raw: npm.readme,
1764
+ bodyRaw: npm.readme,
1765
+ metadataRaw: npm.metadataRaw,
1766
+ sourceUrl: npm.sourceUrl
1767
+ };
1768
+ }
1769
+ }
1770
+ return {
1771
+ type: "plugin",
1772
+ slug: item.slug,
1773
+ name: item.name,
1774
+ install: item.install,
1775
+ source: "remote",
1776
+ bodyRaw: item.description || item.summary || "",
1777
+ metadataRaw: JSON.stringify({
1778
+ name: item.name,
1779
+ author: item.author,
1780
+ sourceRepo: item.sourceRepo,
1781
+ homepage: item.homepage
1782
+ }, null, 2)
1783
+ };
1784
+ }
1317
1785
  async function fetchAllMarketplaceItems(params) {
1318
1786
  const allItems = [];
1319
1787
  let remotePage = 1;
@@ -1492,7 +1960,7 @@ function registerPluginMarketplaceRoutes(app, options, marketplaceBaseUrl) {
1492
1960
  if (!result.ok) {
1493
1961
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
1494
1962
  }
1495
- const filteredItems = result.data.items.map((item) => sanitizeMarketplaceItem(item)).filter((item) => isSupportedMarketplacePluginItem(item));
1963
+ const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplacePluginItem(item));
1496
1964
  const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
1497
1965
  const requestedPage = toPositiveInt(query.page, 1);
1498
1966
  const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
@@ -1516,12 +1984,28 @@ function registerPluginMarketplaceRoutes(app, options, marketplaceBaseUrl) {
1516
1984
  if (!result.ok) {
1517
1985
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
1518
1986
  }
1519
- const sanitized = sanitizeMarketplaceItem(result.data);
1987
+ const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
1520
1988
  if (!isSupportedMarketplacePluginItem(sanitized)) {
1521
1989
  return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
1522
1990
  }
1523
1991
  return c.json(ok(sanitized));
1524
1992
  });
1993
+ app.get("/api/marketplace/plugins/items/:slug/content", async (c) => {
1994
+ const slug = encodeURIComponent(c.req.param("slug"));
1995
+ const result = await fetchMarketplaceData({
1996
+ baseUrl: marketplaceBaseUrl,
1997
+ path: `/api/v1/plugins/items/${slug}`
1998
+ });
1999
+ if (!result.ok) {
2000
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
2001
+ }
2002
+ const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
2003
+ if (!isSupportedMarketplacePluginItem(sanitized)) {
2004
+ return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
2005
+ }
2006
+ const content = await buildPluginContentView(sanitized);
2007
+ return c.json(ok(content));
2008
+ });
1525
2009
  app.post("/api/marketplace/plugins/install", async (c) => {
1526
2010
  const body = await readJson(c.req.raw);
1527
2011
  if (!body.ok || !body.data || typeof body.data !== "object") {
@@ -1585,7 +2069,7 @@ function registerPluginMarketplaceRoutes(app, options, marketplaceBaseUrl) {
1585
2069
  if (!result.ok) {
1586
2070
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
1587
2071
  }
1588
- const filteredItems = result.data.items.map((item) => sanitizeMarketplaceItem(item)).filter((item) => isSupportedMarketplacePluginItem(item));
2072
+ const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplacePluginItem(item));
1589
2073
  return c.json(ok({
1590
2074
  ...result.data,
1591
2075
  total: filteredItems.length,
@@ -1613,7 +2097,7 @@ function registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl) {
1613
2097
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
1614
2098
  }
1615
2099
  const knownSkillNames = collectKnownSkillNames(options);
1616
- const filteredItems = result.data.items.map((item) => sanitizeMarketplaceItem(item)).filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
2100
+ const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
1617
2101
  const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
1618
2102
  const requestedPage = toPositiveInt(query.page, 1);
1619
2103
  const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
@@ -1638,12 +2122,32 @@ function registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl) {
1638
2122
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
1639
2123
  }
1640
2124
  const knownSkillNames = collectKnownSkillNames(options);
1641
- const sanitized = sanitizeMarketplaceItem(result.data);
2125
+ const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
1642
2126
  if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
1643
2127
  return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
1644
2128
  }
1645
2129
  return c.json(ok(sanitized));
1646
2130
  });
2131
+ app.get("/api/marketplace/skills/items/:slug/content", async (c) => {
2132
+ const slug = encodeURIComponent(c.req.param("slug"));
2133
+ const result = await fetchMarketplaceData({
2134
+ baseUrl: marketplaceBaseUrl,
2135
+ path: `/api/v1/skills/items/${slug}`
2136
+ });
2137
+ if (!result.ok) {
2138
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
2139
+ }
2140
+ const knownSkillNames = collectKnownSkillNames(options);
2141
+ const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(result.data));
2142
+ if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) {
2143
+ return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
2144
+ }
2145
+ const content = await buildSkillContentView(options, sanitized);
2146
+ if (!content) {
2147
+ return c.json(err("NOT_FOUND", "skill markdown content not found"), 404);
2148
+ }
2149
+ return c.json(ok(content));
2150
+ });
1647
2151
  app.post("/api/marketplace/skills/install", async (c) => {
1648
2152
  const body = await readJson(c.req.raw);
1649
2153
  if (!body.ok || !body.data || typeof body.data !== "object") {
@@ -1708,7 +2212,7 @@ function registerSkillMarketplaceRoutes(app, options, marketplaceBaseUrl) {
1708
2212
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
1709
2213
  }
1710
2214
  const knownSkillNames = collectKnownSkillNames(options);
1711
- const filteredItems = result.data.items.map((item) => sanitizeMarketplaceItem(item)).filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
2215
+ const filteredItems = result.data.items.map((item) => normalizeMarketplaceItemForUi(sanitizeMarketplaceItem(item))).filter((item) => isSupportedMarketplaceSkillItem(item, knownSkillNames));
1712
2216
  return c.json(ok({
1713
2217
  ...result.data,
1714
2218
  total: filteredItems.length,
@@ -1775,6 +2279,33 @@ function createUiRouter(options) {
1775
2279
  options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
1776
2280
  return c.json(ok(result));
1777
2281
  });
2282
+ app.post("/api/config/providers", async (c) => {
2283
+ const body = await readJson(c.req.raw);
2284
+ if (!body.ok) {
2285
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2286
+ }
2287
+ const result = createCustomProvider(
2288
+ options.configPath,
2289
+ body.data
2290
+ );
2291
+ options.publish({ type: "config.updated", payload: { path: `providers.${result.name}` } });
2292
+ return c.json(ok({
2293
+ name: result.name,
2294
+ provider: result.provider
2295
+ }));
2296
+ });
2297
+ app.delete("/api/config/providers/:provider", async (c) => {
2298
+ const provider = c.req.param("provider");
2299
+ const result = deleteCustomProvider(options.configPath, provider);
2300
+ if (result === null) {
2301
+ return c.json(err("NOT_FOUND", `custom provider not found: ${provider}`), 404);
2302
+ }
2303
+ options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
2304
+ return c.json(ok({
2305
+ deleted: true,
2306
+ provider
2307
+ }));
2308
+ });
1778
2309
  app.post("/api/config/providers/:provider/test", async (c) => {
1779
2310
  const provider = c.req.param("provider");
1780
2311
  const body = await readJson(c.req.raw);
@@ -2153,7 +2684,7 @@ function startUiServer(options) {
2153
2684
  join,
2154
2685
  getContent: async (path) => {
2155
2686
  try {
2156
- return await readFile(path);
2687
+ return await readFile2(path);
2157
2688
  } catch {
2158
2689
  return null;
2159
2690
  }
@@ -2203,7 +2734,9 @@ export {
2203
2734
  buildConfigMeta,
2204
2735
  buildConfigSchemaView,
2205
2736
  buildConfigView,
2737
+ createCustomProvider,
2206
2738
  createUiRouter,
2739
+ deleteCustomProvider,
2207
2740
  deleteSession,
2208
2741
  executeConfigAction,
2209
2742
  getSessionHistory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/server",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
4
4
  "private": false,
5
5
  "description": "Nextclaw UI/API server.",
6
6
  "type": "module",
@@ -15,10 +15,10 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@hono/node-server": "^1.13.3",
18
- "@nextclaw/openclaw-compat": "^0.1.30",
18
+ "@nextclaw/openclaw-compat": "^0.1.32",
19
19
  "hono": "^4.6.2",
20
20
  "ws": "^8.18.0",
21
- "@nextclaw/core": "^0.6.39"
21
+ "@nextclaw/core": "^0.6.43"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^20.17.6",