@nextclaw/server 0.5.0 → 0.5.2

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
@@ -215,17 +215,13 @@ type ConfigActionExecuteResult = {
215
215
  nextActions?: string[];
216
216
  };
217
217
  type MarketplaceItemType = "plugin" | "skill";
218
- type MarketplaceSort = "relevance" | "updated" | "downloads";
219
- type MarketplaceInstallKind = "npm" | "clawhub" | "git";
218
+ type MarketplaceSort = "relevance" | "updated";
219
+ type MarketplaceInstallKind = "npm" | "clawhub" | "git" | "builtin";
220
220
  type MarketplaceInstallSpec = {
221
221
  kind: MarketplaceInstallKind;
222
222
  spec: string;
223
223
  command: string;
224
224
  };
225
- type MarketplaceItemMetrics = {
226
- downloads30d?: number;
227
- stars?: number;
228
- };
229
225
  type MarketplaceItemSummary = {
230
226
  id: string;
231
227
  slug: string;
@@ -235,7 +231,6 @@ type MarketplaceItemSummary = {
235
231
  tags: string[];
236
232
  author: string;
237
233
  install: MarketplaceInstallSpec;
238
- metrics?: MarketplaceItemMetrics;
239
234
  updatedAt: string;
240
235
  };
241
236
  type MarketplaceItemView = MarketplaceItemSummary & {
@@ -262,10 +257,15 @@ type MarketplaceRecommendationView = {
262
257
  };
263
258
  type MarketplaceInstalledRecord = {
264
259
  type: MarketplaceItemType;
260
+ id?: string;
265
261
  spec: string;
266
262
  label?: string;
267
263
  source?: string;
268
264
  installedAt?: string;
265
+ enabled?: boolean;
266
+ runtimeStatus?: string;
267
+ origin?: string;
268
+ installPath?: string;
269
269
  };
270
270
  type MarketplaceInstalledView = {
271
271
  total: number;
@@ -276,6 +276,7 @@ type MarketplaceInstalledView = {
276
276
  type MarketplaceInstallRequest = {
277
277
  type: MarketplaceItemType;
278
278
  spec: string;
279
+ kind?: MarketplaceInstallKind;
279
280
  version?: string;
280
281
  registry?: string;
281
282
  force?: boolean;
@@ -288,10 +289,25 @@ type MarketplaceInstallResult = {
288
289
  };
289
290
  type MarketplaceInstallSkillParams = {
290
291
  slug: string;
292
+ kind?: MarketplaceInstallKind;
291
293
  version?: string;
292
294
  registry?: string;
293
295
  force?: boolean;
294
296
  };
297
+ type MarketplaceManageAction = "enable" | "disable" | "uninstall";
298
+ type MarketplaceManageRequest = {
299
+ type: MarketplaceItemType;
300
+ action: MarketplaceManageAction;
301
+ id?: string;
302
+ spec?: string;
303
+ };
304
+ type MarketplaceManageResult = {
305
+ type: MarketplaceItemType;
306
+ action: MarketplaceManageAction;
307
+ id: string;
308
+ message: string;
309
+ output?: string;
310
+ };
295
311
  type MarketplaceInstaller = {
296
312
  installPlugin?: (spec: string) => Promise<{
297
313
  message: string;
@@ -301,6 +317,22 @@ type MarketplaceInstaller = {
301
317
  message: string;
302
318
  output?: string;
303
319
  }>;
320
+ enablePlugin?: (id: string) => Promise<{
321
+ message: string;
322
+ output?: string;
323
+ }>;
324
+ disablePlugin?: (id: string) => Promise<{
325
+ message: string;
326
+ output?: string;
327
+ }>;
328
+ uninstallPlugin?: (id: string) => Promise<{
329
+ message: string;
330
+ output?: string;
331
+ }>;
332
+ uninstallSkill?: (slug: string) => Promise<{
333
+ message: string;
334
+ output?: string;
335
+ }>;
304
336
  };
305
337
  type MarketplaceApiConfig = {
306
338
  apiBaseUrl?: string;
@@ -378,4 +410,4 @@ declare function patchSession(configPath: string, key: string, patch: SessionPat
378
410
  declare function deleteSession(configPath: string, key: string): boolean;
379
411
  declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
380
412
 
381
- export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallRequest, type MarketplaceInstallResult, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemMetrics, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplaceRecommendationView, type MarketplaceSort, type ProviderConfigUpdate, type ProviderConfigView, type ProviderSpecView, type RuntimeConfigUpdate, type SessionConfigView, type SessionEntryView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, type SessionsListView, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createUiRouter, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, updateChannel, updateModel, updateProvider, updateRuntime };
413
+ export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallRequest, type MarketplaceInstallResult, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplaceManageAction, type MarketplaceManageRequest, type MarketplaceManageResult, type MarketplaceRecommendationView, type MarketplaceSort, type ProviderConfigUpdate, type ProviderConfigView, type ProviderSpecView, type RuntimeConfigUpdate, type SessionConfigView, type SessionEntryView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, type SessionsListView, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createUiRouter, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, updateChannel, updateModel, updateProvider, updateRuntime };
package/dist/index.js CHANGED
@@ -3,13 +3,14 @@ import { Hono as Hono2 } from "hono";
3
3
  import { cors } from "hono/cors";
4
4
  import { serve } from "@hono/node-server";
5
5
  import { WebSocketServer, WebSocket } from "ws";
6
- import { existsSync as existsSync2, readFileSync } from "fs";
6
+ import { existsSync, readFileSync } from "fs";
7
7
  import { readFile, stat } from "fs/promises";
8
- import { join as join2 } from "path";
8
+ import { join } from "path";
9
9
 
10
10
  // src/ui/router.ts
11
11
  import { Hono } from "hono";
12
- import { expandHome } from "@nextclaw/core";
12
+ import * as NextclawCore from "@nextclaw/core";
13
+ import { buildPluginStatusReport } from "@nextclaw/openclaw-compat";
13
14
 
14
15
  // src/ui/config.ts
15
16
  import {
@@ -569,9 +570,121 @@ function updateRuntime(configPath, patch) {
569
570
  }
570
571
 
571
572
  // src/ui/router.ts
572
- import { existsSync, readdirSync } from "fs";
573
- import { join, resolve } from "path";
574
- var DEFAULT_MARKETPLACE_API_BASE = "https://nextclaw-marketplace-api.15353764479037.workers.dev";
573
+ var DEFAULT_MARKETPLACE_API_BASE = "https://marketplace-api.nextclaw.io";
574
+ var NEXTCLAW_PLUGIN_NPM_PREFIX = "@nextclaw/channel-plugin-";
575
+ var CLAWBAY_CHANNEL_PLUGIN_NPM_SPEC = "@clawbay/clawbay-channel";
576
+ var BUILTIN_CHANNEL_PLUGIN_ID_PREFIX = "builtin-channel-";
577
+ var MARKETPLACE_REMOTE_PAGE_SIZE = 100;
578
+ var MARKETPLACE_REMOTE_MAX_PAGES = 20;
579
+ var getWorkspacePathFromConfig3 = NextclawCore.getWorkspacePathFromConfig;
580
+ function createSkillsLoader(workspace) {
581
+ const ctor = NextclawCore.SkillsLoader;
582
+ if (!ctor) {
583
+ return null;
584
+ }
585
+ return new ctor(workspace);
586
+ }
587
+ function normalizePluginNpmSpec(rawSpec) {
588
+ const spec = rawSpec.trim();
589
+ if (!spec.startsWith("@")) {
590
+ return spec;
591
+ }
592
+ const versionDelimiterIndex = spec.lastIndexOf("@");
593
+ if (versionDelimiterIndex <= 0) {
594
+ return spec;
595
+ }
596
+ const packageName = spec.slice(0, versionDelimiterIndex).trim();
597
+ if (!packageName.includes("/")) {
598
+ return spec;
599
+ }
600
+ return packageName;
601
+ }
602
+ function isSupportedMarketplacePluginSpec(rawSpec) {
603
+ const spec = normalizePluginNpmSpec(rawSpec);
604
+ return spec.startsWith(NEXTCLAW_PLUGIN_NPM_PREFIX) || spec === CLAWBAY_CHANNEL_PLUGIN_NPM_SPEC;
605
+ }
606
+ function resolvePluginCanonicalSpec(params) {
607
+ const rawInstallSpec = typeof params.installSpec === "string" ? params.installSpec.trim() : "";
608
+ if (rawInstallSpec.length > 0) {
609
+ return normalizePluginNpmSpec(rawInstallSpec);
610
+ }
611
+ if (params.pluginId.startsWith(BUILTIN_CHANNEL_PLUGIN_ID_PREFIX)) {
612
+ const channelSlug = params.pluginId.slice(BUILTIN_CHANNEL_PLUGIN_ID_PREFIX.length).trim();
613
+ if (channelSlug.length > 0) {
614
+ return `${NEXTCLAW_PLUGIN_NPM_PREFIX}${channelSlug}`;
615
+ }
616
+ }
617
+ return params.pluginId;
618
+ }
619
+ function readPluginRuntimeStatusPriority(status) {
620
+ if (status === "loaded") {
621
+ return 400;
622
+ }
623
+ if (status === "disabled") {
624
+ return 300;
625
+ }
626
+ if (status === "unresolved") {
627
+ return 200;
628
+ }
629
+ return 100;
630
+ }
631
+ function readPluginOriginPriority(origin) {
632
+ if (origin === "bundled") {
633
+ return 80;
634
+ }
635
+ if (origin === "workspace") {
636
+ return 70;
637
+ }
638
+ if (origin === "global") {
639
+ return 60;
640
+ }
641
+ if (origin === "config") {
642
+ return 50;
643
+ }
644
+ return 10;
645
+ }
646
+ function readInstalledPluginRecordPriority(record) {
647
+ const installScore = record.installPath ? 20 : 0;
648
+ const timestampScore = record.installedAt ? 10 : 0;
649
+ return readPluginRuntimeStatusPriority(record.runtimeStatus) + readPluginOriginPriority(record.origin) + installScore + timestampScore;
650
+ }
651
+ function mergeInstalledPluginRecords(primary, secondary) {
652
+ return {
653
+ ...primary,
654
+ id: primary.id ?? secondary.id,
655
+ label: primary.label ?? secondary.label,
656
+ source: primary.source ?? secondary.source,
657
+ installedAt: primary.installedAt ?? secondary.installedAt,
658
+ enabled: primary.enabled ?? secondary.enabled,
659
+ runtimeStatus: primary.runtimeStatus ?? secondary.runtimeStatus,
660
+ origin: primary.origin ?? secondary.origin,
661
+ installPath: primary.installPath ?? secondary.installPath
662
+ };
663
+ }
664
+ function dedupeInstalledPluginRecordsByCanonicalSpec(records) {
665
+ const deduped = /* @__PURE__ */ new Map();
666
+ for (const record of records) {
667
+ const canonicalSpec = normalizePluginNpmSpec(record.spec).trim();
668
+ if (!canonicalSpec) {
669
+ continue;
670
+ }
671
+ const key = canonicalSpec.toLowerCase();
672
+ const normalizedRecord = { ...record, spec: canonicalSpec };
673
+ const existing = deduped.get(key);
674
+ if (!existing) {
675
+ deduped.set(key, normalizedRecord);
676
+ continue;
677
+ }
678
+ const normalizedScore = readInstalledPluginRecordPriority(normalizedRecord);
679
+ const existingScore = readInstalledPluginRecordPriority(existing);
680
+ if (normalizedScore > existingScore) {
681
+ deduped.set(key, mergeInstalledPluginRecords(normalizedRecord, existing));
682
+ continue;
683
+ }
684
+ deduped.set(key, mergeInstalledPluginRecords(existing, normalizedRecord));
685
+ }
686
+ return Array.from(deduped.values());
687
+ }
575
688
  function ok(data) {
576
689
  return { ok: true, data };
577
690
  }
@@ -658,56 +771,122 @@ async function fetchMarketplaceData(params) {
658
771
  function collectMarketplaceInstalledView(options) {
659
772
  const config = loadConfigOrDefault(options.configPath);
660
773
  const pluginRecordsMap = config.plugins.installs ?? {};
774
+ const pluginEntries = config.plugins.entries ?? {};
661
775
  const pluginRecords = [];
662
- const pluginSpecSet = /* @__PURE__ */ new Set();
776
+ const seenPluginIds = /* @__PURE__ */ new Set();
777
+ let discoveredPlugins = [];
778
+ try {
779
+ const pluginReport = buildPluginStatusReport({
780
+ config,
781
+ workspaceDir: getWorkspacePathFromConfig3(config)
782
+ });
783
+ discoveredPlugins = pluginReport.plugins;
784
+ } catch {
785
+ discoveredPlugins = [];
786
+ }
787
+ const readPluginPriority = (plugin) => {
788
+ const hasInstallRecord = Boolean(pluginRecordsMap[plugin.id]);
789
+ const statusScore = plugin.status === "loaded" ? 300 : plugin.status === "disabled" ? 200 : 100;
790
+ let originScore = 0;
791
+ if (hasInstallRecord) {
792
+ originScore = plugin.origin === "workspace" ? 40 : plugin.origin === "global" ? 30 : plugin.origin === "config" ? 20 : 10;
793
+ } else {
794
+ originScore = plugin.origin === "bundled" ? 40 : plugin.origin === "workspace" ? 30 : plugin.origin === "global" ? 20 : 10;
795
+ }
796
+ return statusScore + originScore;
797
+ };
798
+ const discoveredById = /* @__PURE__ */ new Map();
799
+ for (const plugin of discoveredPlugins) {
800
+ const existing = discoveredById.get(plugin.id);
801
+ if (!existing) {
802
+ discoveredById.set(plugin.id, plugin);
803
+ continue;
804
+ }
805
+ if (readPluginPriority(plugin) > readPluginPriority(existing)) {
806
+ discoveredById.set(plugin.id, plugin);
807
+ }
808
+ }
809
+ for (const plugin of discoveredById.values()) {
810
+ const installRecord = pluginRecordsMap[plugin.id];
811
+ const entry = pluginEntries[plugin.id];
812
+ const normalizedSpec = resolvePluginCanonicalSpec({
813
+ pluginId: plugin.id,
814
+ installSpec: installRecord?.spec
815
+ });
816
+ const enabled = entry?.enabled === false ? false : plugin.enabled;
817
+ const runtimeStatus = entry?.enabled === false ? "disabled" : plugin.status;
818
+ pluginRecords.push({
819
+ type: "plugin",
820
+ id: plugin.id,
821
+ spec: normalizedSpec,
822
+ label: plugin.name && plugin.name.trim().length > 0 ? plugin.name : plugin.id,
823
+ source: plugin.source,
824
+ installedAt: installRecord?.installedAt,
825
+ enabled,
826
+ runtimeStatus,
827
+ origin: plugin.origin,
828
+ installPath: installRecord?.installPath
829
+ });
830
+ seenPluginIds.add(plugin.id);
831
+ }
663
832
  for (const [pluginId, installRecord] of Object.entries(pluginRecordsMap)) {
664
- const normalizedSpec = typeof installRecord.spec === "string" && installRecord.spec.trim().length > 0 ? installRecord.spec.trim() : pluginId;
833
+ if (seenPluginIds.has(pluginId)) {
834
+ continue;
835
+ }
836
+ const normalizedSpec = resolvePluginCanonicalSpec({
837
+ pluginId,
838
+ installSpec: installRecord.spec
839
+ });
840
+ const entry = pluginEntries[pluginId];
665
841
  pluginRecords.push({
666
842
  type: "plugin",
843
+ id: pluginId,
667
844
  spec: normalizedSpec,
668
845
  label: pluginId,
669
846
  source: installRecord.source,
670
- installedAt: installRecord.installedAt
847
+ installedAt: installRecord.installedAt,
848
+ enabled: entry?.enabled !== false,
849
+ runtimeStatus: entry?.enabled === false ? "disabled" : "unresolved",
850
+ installPath: installRecord.installPath
671
851
  });
672
- pluginSpecSet.add(normalizedSpec);
852
+ seenPluginIds.add(pluginId);
673
853
  }
674
- const pluginEntries = config.plugins.entries ?? {};
675
- for (const pluginId of Object.keys(pluginEntries)) {
676
- if (!pluginSpecSet.has(pluginId)) {
854
+ for (const [pluginId, entry] of Object.entries(pluginEntries)) {
855
+ if (!seenPluginIds.has(pluginId)) {
856
+ const normalizedSpec = resolvePluginCanonicalSpec({ pluginId });
677
857
  pluginRecords.push({
678
858
  type: "plugin",
679
- spec: pluginId,
859
+ id: pluginId,
860
+ spec: normalizedSpec,
680
861
  label: pluginId,
681
- source: "config"
862
+ source: "config",
863
+ enabled: entry?.enabled !== false,
864
+ runtimeStatus: entry?.enabled === false ? "disabled" : "unresolved"
682
865
  });
683
- pluginSpecSet.add(pluginId);
866
+ seenPluginIds.add(pluginId);
684
867
  }
685
868
  }
686
- const workspacePath = resolve(expandHome(config.agents.defaults.workspace));
687
- const skillsPath = join(workspacePath, "skills");
869
+ const dedupedPluginRecords = dedupeInstalledPluginRecordsByCanonicalSpec(pluginRecords);
870
+ const pluginSpecSet = new Set(dedupedPluginRecords.map((record) => record.spec));
871
+ const workspacePath = getWorkspacePathFromConfig3(config);
872
+ const skillsLoader = createSkillsLoader(workspacePath);
873
+ const availableSkillSet = new Set((skillsLoader?.listSkills(true) ?? []).map((skill) => skill.name));
874
+ const listedSkills = skillsLoader?.listSkills(false) ?? [];
688
875
  const skillSpecSet = /* @__PURE__ */ new Set();
689
- const skillRecords = [];
690
- if (existsSync(skillsPath)) {
691
- const entries = readdirSync(skillsPath, { withFileTypes: true });
692
- for (const entry of entries) {
693
- if (!entry.isDirectory()) {
694
- continue;
695
- }
696
- const skillSlug = entry.name;
697
- const skillFile = join(skillsPath, skillSlug, "SKILL.md");
698
- if (!existsSync(skillFile)) {
699
- continue;
700
- }
701
- skillRecords.push({
702
- type: "skill",
703
- spec: skillSlug,
704
- label: skillSlug,
705
- source: "workspace"
706
- });
707
- skillSpecSet.add(skillSlug);
708
- }
709
- }
710
- const records = [...pluginRecords, ...skillRecords].sort((left, right) => {
876
+ const skillRecords = listedSkills.map((skill) => {
877
+ const enabled = availableSkillSet.has(skill.name);
878
+ skillSpecSet.add(skill.name);
879
+ return {
880
+ type: "skill",
881
+ id: skill.name,
882
+ spec: skill.name,
883
+ label: skill.name,
884
+ source: skill.source,
885
+ enabled,
886
+ runtimeStatus: enabled ? "enabled" : "disabled"
887
+ };
888
+ });
889
+ const records = [...dedupedPluginRecords, ...skillRecords].sort((left, right) => {
711
890
  if (left.type !== right.type) {
712
891
  return left.type.localeCompare(right.type);
713
892
  }
@@ -720,6 +899,66 @@ function collectMarketplaceInstalledView(options) {
720
899
  records
721
900
  };
722
901
  }
902
+ function sanitizeMarketplaceItem(item) {
903
+ const next = { ...item };
904
+ delete next.metrics;
905
+ return next;
906
+ }
907
+ function toPositiveInt(raw, fallback) {
908
+ if (!raw) {
909
+ return fallback;
910
+ }
911
+ const parsed = Number.parseInt(raw, 10);
912
+ if (!Number.isFinite(parsed) || parsed <= 0) {
913
+ return fallback;
914
+ }
915
+ return parsed;
916
+ }
917
+ function collectKnownSkillNames(options) {
918
+ const config = loadConfigOrDefault(options.configPath);
919
+ const loader = createSkillsLoader(getWorkspacePathFromConfig3(config));
920
+ return new Set((loader?.listSkills(false) ?? []).map((skill) => skill.name));
921
+ }
922
+ function isSupportedMarketplaceItem(item, knownSkillNames) {
923
+ if (item.type === "plugin") {
924
+ return item.install.kind === "npm" && isSupportedMarketplacePluginSpec(item.install.spec);
925
+ }
926
+ return item.install.kind === "builtin" && knownSkillNames.has(item.install.spec);
927
+ }
928
+ async function fetchAllMarketplaceItems(params) {
929
+ const allItems = [];
930
+ let remotePage = 1;
931
+ let remoteTotalPages = 1;
932
+ let sort = "relevance";
933
+ let query;
934
+ while (remotePage <= remoteTotalPages && remotePage <= MARKETPLACE_REMOTE_MAX_PAGES) {
935
+ const result = await fetchMarketplaceData({
936
+ baseUrl: params.baseUrl,
937
+ path: "/api/v1/items",
938
+ query: {
939
+ ...params.query,
940
+ page: String(remotePage),
941
+ pageSize: String(MARKETPLACE_REMOTE_PAGE_SIZE)
942
+ }
943
+ });
944
+ if (!result.ok) {
945
+ return result;
946
+ }
947
+ allItems.push(...result.data.items);
948
+ remoteTotalPages = result.data.totalPages;
949
+ sort = result.data.sort;
950
+ query = result.data.query;
951
+ remotePage += 1;
952
+ }
953
+ return {
954
+ ok: true,
955
+ data: {
956
+ sort,
957
+ query,
958
+ items: allItems
959
+ }
960
+ };
961
+ }
723
962
  async function installMarketplaceItem(params) {
724
963
  const type = params.body.type;
725
964
  const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
@@ -742,6 +981,7 @@ async function installMarketplaceItem(params) {
742
981
  }
743
982
  result = await installer.installSkill({
744
983
  slug: spec,
984
+ kind: params.body.kind,
745
985
  version: params.body.version,
746
986
  registry: params.body.registry,
747
987
  force: params.body.force
@@ -755,15 +995,61 @@ async function installMarketplaceItem(params) {
755
995
  output: result.output
756
996
  };
757
997
  }
998
+ async function manageMarketplaceItem(params) {
999
+ const type = params.body.type;
1000
+ const action = params.body.action;
1001
+ 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() : "";
1002
+ if (type !== "plugin" && type !== "skill" || action !== "enable" && action !== "disable" && action !== "uninstall" || !targetId) {
1003
+ throw new Error("INVALID_BODY:type, action and non-empty id/spec are required");
1004
+ }
1005
+ const installer = params.options.marketplace?.installer;
1006
+ if (!installer) {
1007
+ throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
1008
+ }
1009
+ let result;
1010
+ if (type === "plugin") {
1011
+ if (action === "enable") {
1012
+ if (!installer.enablePlugin) {
1013
+ throw new Error("NOT_AVAILABLE:plugin enable is not configured");
1014
+ }
1015
+ result = await installer.enablePlugin(targetId);
1016
+ } else if (action === "disable") {
1017
+ if (!installer.disablePlugin) {
1018
+ throw new Error("NOT_AVAILABLE:plugin disable is not configured");
1019
+ }
1020
+ result = await installer.disablePlugin(targetId);
1021
+ } else {
1022
+ if (!installer.uninstallPlugin) {
1023
+ throw new Error("NOT_AVAILABLE:plugin uninstall is not configured");
1024
+ }
1025
+ result = await installer.uninstallPlugin(targetId);
1026
+ }
1027
+ } else {
1028
+ if (action !== "uninstall") {
1029
+ throw new Error("NOT_AVAILABLE:skill only supports uninstall action");
1030
+ }
1031
+ if (!installer.uninstallSkill) {
1032
+ throw new Error("NOT_AVAILABLE:skill uninstall is not configured");
1033
+ }
1034
+ result = await installer.uninstallSkill(targetId);
1035
+ }
1036
+ params.options.publish({ type: "config.updated", payload: { path: type === "plugin" ? "plugins" : "skills" } });
1037
+ return {
1038
+ type,
1039
+ action,
1040
+ id: targetId,
1041
+ message: result.message,
1042
+ output: result.output
1043
+ };
1044
+ }
758
1045
  function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
759
1046
  app.get("/api/marketplace/installed", (c) => {
760
1047
  return c.json(ok(collectMarketplaceInstalledView(options)));
761
1048
  });
762
1049
  app.get("/api/marketplace/items", async (c) => {
763
1050
  const query = c.req.query();
764
- const result = await fetchMarketplaceData({
1051
+ const result = await fetchAllMarketplaceItems({
765
1052
  baseUrl: marketplaceBaseUrl,
766
- path: "/api/v1/items",
767
1053
  query: {
768
1054
  q: query.q,
769
1055
  type: query.type,
@@ -776,7 +1062,21 @@ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
776
1062
  if (!result.ok) {
777
1063
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
778
1064
  }
779
- return c.json(ok(result.data));
1065
+ const knownSkillNames = collectKnownSkillNames(options);
1066
+ const filteredItems = result.data.items.map((item) => sanitizeMarketplaceItem(item)).filter((item) => isSupportedMarketplaceItem(item, knownSkillNames));
1067
+ const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
1068
+ const requestedPage = toPositiveInt(query.page, 1);
1069
+ const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
1070
+ const currentPage = totalPages === 0 ? 1 : Math.min(requestedPage, totalPages);
1071
+ return c.json(ok({
1072
+ total: filteredItems.length,
1073
+ page: currentPage,
1074
+ pageSize,
1075
+ totalPages,
1076
+ sort: result.data.sort,
1077
+ query: result.data.query,
1078
+ items: filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize)
1079
+ }));
780
1080
  });
781
1081
  app.get("/api/marketplace/items/:slug", async (c) => {
782
1082
  const slug = encodeURIComponent(c.req.param("slug"));
@@ -791,7 +1091,12 @@ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
791
1091
  if (!result.ok) {
792
1092
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
793
1093
  }
794
- return c.json(ok(result.data));
1094
+ const knownSkillNames = collectKnownSkillNames(options);
1095
+ const sanitized = sanitizeMarketplaceItem(result.data);
1096
+ if (!isSupportedMarketplaceItem(sanitized, knownSkillNames)) {
1097
+ return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
1098
+ }
1099
+ return c.json(ok(sanitized));
795
1100
  });
796
1101
  app.get("/api/marketplace/recommendations", async (c) => {
797
1102
  const query = c.req.query();
@@ -806,7 +1111,13 @@ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
806
1111
  if (!result.ok) {
807
1112
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
808
1113
  }
809
- return c.json(ok(result.data));
1114
+ const knownSkillNames = collectKnownSkillNames(options);
1115
+ const filteredItems = result.data.items.map((item) => sanitizeMarketplaceItem(item)).filter((item) => isSupportedMarketplaceItem(item, knownSkillNames));
1116
+ return c.json(ok({
1117
+ ...result.data,
1118
+ total: filteredItems.length,
1119
+ items: filteredItems
1120
+ }));
810
1121
  });
811
1122
  app.post("/api/marketplace/install", async (c) => {
812
1123
  const body = await readJson(c.req.raw);
@@ -827,6 +1138,25 @@ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
827
1138
  return c.json(err("INSTALL_FAILED", message), 400);
828
1139
  }
829
1140
  });
1141
+ app.post("/api/marketplace/manage", async (c) => {
1142
+ const body = await readJson(c.req.raw);
1143
+ if (!body.ok || !body.data || typeof body.data !== "object") {
1144
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
1145
+ }
1146
+ try {
1147
+ const payload = await manageMarketplaceItem({ options, body: body.data });
1148
+ return c.json(ok(payload));
1149
+ } catch (error) {
1150
+ const message = String(error);
1151
+ if (message.startsWith("INVALID_BODY:")) {
1152
+ return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
1153
+ }
1154
+ if (message.startsWith("NOT_AVAILABLE:")) {
1155
+ return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
1156
+ }
1157
+ return c.json(err("MANAGE_FAILED", message), 400);
1158
+ }
1159
+ });
830
1160
  }
831
1161
  function createUiRouter(options) {
832
1162
  const app = new Hono();
@@ -1003,13 +1333,13 @@ function startUiServer(options) {
1003
1333
  })
1004
1334
  );
1005
1335
  const staticDir = options.staticDir;
1006
- if (staticDir && existsSync2(join2(staticDir, "index.html"))) {
1007
- const indexHtml = readFileSync(join2(staticDir, "index.html"), "utf-8");
1336
+ if (staticDir && existsSync(join(staticDir, "index.html"))) {
1337
+ const indexHtml = readFileSync(join(staticDir, "index.html"), "utf-8");
1008
1338
  app.use(
1009
1339
  "/*",
1010
1340
  serveStatic({
1011
1341
  root: staticDir,
1012
- join: join2,
1342
+ join,
1013
1343
  getContent: async (path) => {
1014
1344
  try {
1015
1345
  return await readFile(path);
@@ -1051,9 +1381,9 @@ function startUiServer(options) {
1051
1381
  host: options.host,
1052
1382
  port: options.port,
1053
1383
  publish,
1054
- close: () => new Promise((resolve2) => {
1384
+ close: () => new Promise((resolve) => {
1055
1385
  wss.close(() => {
1056
- server.close(() => resolve2());
1386
+ server.close(() => resolve());
1057
1387
  });
1058
1388
  })
1059
1389
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/server",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Nextclaw UI/API server.",
6
6
  "type": "module",
@@ -15,9 +15,10 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@hono/node-server": "^1.13.3",
18
+ "@nextclaw/openclaw-compat": "^0.1.20",
18
19
  "hono": "^4.6.2",
19
20
  "ws": "^8.18.0",
20
- "@nextclaw/core": "^0.6.27"
21
+ "@nextclaw/core": "^0.6.28"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/node": "^20.17.6",