@nextclaw/server 0.4.17 → 0.5.1

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 { SkillsLoader, getWorkspacePathFromConfig as getWorkspacePathFromConfig2 } from "@nextclaw/core";
13
+ import { buildPluginStatusReport } from "@nextclaw/openclaw-compat";
13
14
 
14
15
  // src/ui/config.ts
15
16
  import {
@@ -569,9 +570,105 @@ 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 BUILTIN_CHANNEL_PLUGIN_ID_PREFIX = "builtin-channel-";
576
+ var MARKETPLACE_REMOTE_PAGE_SIZE = 100;
577
+ var MARKETPLACE_REMOTE_MAX_PAGES = 20;
578
+ function normalizeChannelPluginNpmSpec(rawSpec) {
579
+ const spec = rawSpec.trim();
580
+ if (!spec.startsWith(NEXTCLAW_PLUGIN_NPM_PREFIX)) {
581
+ return spec;
582
+ }
583
+ const versionDelimiterIndex = spec.lastIndexOf("@");
584
+ if (versionDelimiterIndex <= 0) {
585
+ return spec;
586
+ }
587
+ const packageName = spec.slice(0, versionDelimiterIndex).trim();
588
+ return packageName.startsWith(NEXTCLAW_PLUGIN_NPM_PREFIX) ? packageName : spec;
589
+ }
590
+ function resolvePluginCanonicalSpec(params) {
591
+ const rawInstallSpec = typeof params.installSpec === "string" ? params.installSpec.trim() : "";
592
+ if (rawInstallSpec.length > 0) {
593
+ return normalizeChannelPluginNpmSpec(rawInstallSpec);
594
+ }
595
+ if (params.pluginId.startsWith(BUILTIN_CHANNEL_PLUGIN_ID_PREFIX)) {
596
+ const channelSlug = params.pluginId.slice(BUILTIN_CHANNEL_PLUGIN_ID_PREFIX.length).trim();
597
+ if (channelSlug.length > 0) {
598
+ return `${NEXTCLAW_PLUGIN_NPM_PREFIX}${channelSlug}`;
599
+ }
600
+ }
601
+ return params.pluginId;
602
+ }
603
+ function readPluginRuntimeStatusPriority(status) {
604
+ if (status === "loaded") {
605
+ return 400;
606
+ }
607
+ if (status === "disabled") {
608
+ return 300;
609
+ }
610
+ if (status === "unresolved") {
611
+ return 200;
612
+ }
613
+ return 100;
614
+ }
615
+ function readPluginOriginPriority(origin) {
616
+ if (origin === "bundled") {
617
+ return 80;
618
+ }
619
+ if (origin === "workspace") {
620
+ return 70;
621
+ }
622
+ if (origin === "global") {
623
+ return 60;
624
+ }
625
+ if (origin === "config") {
626
+ return 50;
627
+ }
628
+ return 10;
629
+ }
630
+ function readInstalledPluginRecordPriority(record) {
631
+ const installScore = record.installPath ? 20 : 0;
632
+ const timestampScore = record.installedAt ? 10 : 0;
633
+ return readPluginRuntimeStatusPriority(record.runtimeStatus) + readPluginOriginPriority(record.origin) + installScore + timestampScore;
634
+ }
635
+ function mergeInstalledPluginRecords(primary, secondary) {
636
+ return {
637
+ ...primary,
638
+ id: primary.id ?? secondary.id,
639
+ label: primary.label ?? secondary.label,
640
+ source: primary.source ?? secondary.source,
641
+ installedAt: primary.installedAt ?? secondary.installedAt,
642
+ enabled: primary.enabled ?? secondary.enabled,
643
+ runtimeStatus: primary.runtimeStatus ?? secondary.runtimeStatus,
644
+ origin: primary.origin ?? secondary.origin,
645
+ installPath: primary.installPath ?? secondary.installPath
646
+ };
647
+ }
648
+ function dedupeInstalledPluginRecordsByCanonicalSpec(records) {
649
+ const deduped = /* @__PURE__ */ new Map();
650
+ for (const record of records) {
651
+ const canonicalSpec = normalizeChannelPluginNpmSpec(record.spec).trim();
652
+ if (!canonicalSpec) {
653
+ continue;
654
+ }
655
+ const key = canonicalSpec.toLowerCase();
656
+ const normalizedRecord = { ...record, spec: canonicalSpec };
657
+ const existing = deduped.get(key);
658
+ if (!existing) {
659
+ deduped.set(key, normalizedRecord);
660
+ continue;
661
+ }
662
+ const normalizedScore = readInstalledPluginRecordPriority(normalizedRecord);
663
+ const existingScore = readInstalledPluginRecordPriority(existing);
664
+ if (normalizedScore > existingScore) {
665
+ deduped.set(key, mergeInstalledPluginRecords(normalizedRecord, existing));
666
+ continue;
667
+ }
668
+ deduped.set(key, mergeInstalledPluginRecords(existing, normalizedRecord));
669
+ }
670
+ return Array.from(deduped.values());
671
+ }
575
672
  function ok(data) {
576
673
  return { ok: true, data };
577
674
  }
@@ -658,56 +755,122 @@ async function fetchMarketplaceData(params) {
658
755
  function collectMarketplaceInstalledView(options) {
659
756
  const config = loadConfigOrDefault(options.configPath);
660
757
  const pluginRecordsMap = config.plugins.installs ?? {};
758
+ const pluginEntries = config.plugins.entries ?? {};
661
759
  const pluginRecords = [];
662
- const pluginSpecSet = /* @__PURE__ */ new Set();
760
+ const seenPluginIds = /* @__PURE__ */ new Set();
761
+ let discoveredPlugins = [];
762
+ try {
763
+ const pluginReport = buildPluginStatusReport({
764
+ config,
765
+ workspaceDir: getWorkspacePathFromConfig2(config)
766
+ });
767
+ discoveredPlugins = pluginReport.plugins;
768
+ } catch {
769
+ discoveredPlugins = [];
770
+ }
771
+ const readPluginPriority = (plugin) => {
772
+ const hasInstallRecord = Boolean(pluginRecordsMap[plugin.id]);
773
+ const statusScore = plugin.status === "loaded" ? 300 : plugin.status === "disabled" ? 200 : 100;
774
+ let originScore = 0;
775
+ if (hasInstallRecord) {
776
+ originScore = plugin.origin === "workspace" ? 40 : plugin.origin === "global" ? 30 : plugin.origin === "config" ? 20 : 10;
777
+ } else {
778
+ originScore = plugin.origin === "bundled" ? 40 : plugin.origin === "workspace" ? 30 : plugin.origin === "global" ? 20 : 10;
779
+ }
780
+ return statusScore + originScore;
781
+ };
782
+ const discoveredById = /* @__PURE__ */ new Map();
783
+ for (const plugin of discoveredPlugins) {
784
+ const existing = discoveredById.get(plugin.id);
785
+ if (!existing) {
786
+ discoveredById.set(plugin.id, plugin);
787
+ continue;
788
+ }
789
+ if (readPluginPriority(plugin) > readPluginPriority(existing)) {
790
+ discoveredById.set(plugin.id, plugin);
791
+ }
792
+ }
793
+ for (const plugin of discoveredById.values()) {
794
+ const installRecord = pluginRecordsMap[plugin.id];
795
+ const entry = pluginEntries[plugin.id];
796
+ const normalizedSpec = resolvePluginCanonicalSpec({
797
+ pluginId: plugin.id,
798
+ installSpec: installRecord?.spec
799
+ });
800
+ const enabled = entry?.enabled === false ? false : plugin.enabled;
801
+ const runtimeStatus = entry?.enabled === false ? "disabled" : plugin.status;
802
+ pluginRecords.push({
803
+ type: "plugin",
804
+ id: plugin.id,
805
+ spec: normalizedSpec,
806
+ label: plugin.name && plugin.name.trim().length > 0 ? plugin.name : plugin.id,
807
+ source: plugin.source,
808
+ installedAt: installRecord?.installedAt,
809
+ enabled,
810
+ runtimeStatus,
811
+ origin: plugin.origin,
812
+ installPath: installRecord?.installPath
813
+ });
814
+ seenPluginIds.add(plugin.id);
815
+ }
663
816
  for (const [pluginId, installRecord] of Object.entries(pluginRecordsMap)) {
664
- const normalizedSpec = typeof installRecord.spec === "string" && installRecord.spec.trim().length > 0 ? installRecord.spec.trim() : pluginId;
817
+ if (seenPluginIds.has(pluginId)) {
818
+ continue;
819
+ }
820
+ const normalizedSpec = resolvePluginCanonicalSpec({
821
+ pluginId,
822
+ installSpec: installRecord.spec
823
+ });
824
+ const entry = pluginEntries[pluginId];
665
825
  pluginRecords.push({
666
826
  type: "plugin",
827
+ id: pluginId,
667
828
  spec: normalizedSpec,
668
829
  label: pluginId,
669
830
  source: installRecord.source,
670
- installedAt: installRecord.installedAt
831
+ installedAt: installRecord.installedAt,
832
+ enabled: entry?.enabled !== false,
833
+ runtimeStatus: entry?.enabled === false ? "disabled" : "unresolved",
834
+ installPath: installRecord.installPath
671
835
  });
672
- pluginSpecSet.add(normalizedSpec);
836
+ seenPluginIds.add(pluginId);
673
837
  }
674
- const pluginEntries = config.plugins.entries ?? {};
675
- for (const pluginId of Object.keys(pluginEntries)) {
676
- if (!pluginSpecSet.has(pluginId)) {
838
+ for (const [pluginId, entry] of Object.entries(pluginEntries)) {
839
+ if (!seenPluginIds.has(pluginId)) {
840
+ const normalizedSpec = resolvePluginCanonicalSpec({ pluginId });
677
841
  pluginRecords.push({
678
842
  type: "plugin",
679
- spec: pluginId,
843
+ id: pluginId,
844
+ spec: normalizedSpec,
680
845
  label: pluginId,
681
- source: "config"
846
+ source: "config",
847
+ enabled: entry?.enabled !== false,
848
+ runtimeStatus: entry?.enabled === false ? "disabled" : "unresolved"
682
849
  });
683
- pluginSpecSet.add(pluginId);
850
+ seenPluginIds.add(pluginId);
684
851
  }
685
852
  }
686
- const workspacePath = resolve(expandHome(config.agents.defaults.workspace));
687
- const skillsPath = join(workspacePath, "skills");
853
+ const dedupedPluginRecords = dedupeInstalledPluginRecordsByCanonicalSpec(pluginRecords);
854
+ const pluginSpecSet = new Set(dedupedPluginRecords.map((record) => record.spec));
855
+ const workspacePath = getWorkspacePathFromConfig2(config);
856
+ const skillsLoader = new SkillsLoader(workspacePath);
857
+ const availableSkillSet = new Set(skillsLoader.listSkills(true).map((skill) => skill.name));
858
+ const listedSkills = skillsLoader.listSkills(false);
688
859
  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) => {
860
+ const skillRecords = listedSkills.map((skill) => {
861
+ const enabled = availableSkillSet.has(skill.name);
862
+ skillSpecSet.add(skill.name);
863
+ return {
864
+ type: "skill",
865
+ id: skill.name,
866
+ spec: skill.name,
867
+ label: skill.name,
868
+ source: skill.source,
869
+ enabled,
870
+ runtimeStatus: enabled ? "enabled" : "disabled"
871
+ };
872
+ });
873
+ const records = [...dedupedPluginRecords, ...skillRecords].sort((left, right) => {
711
874
  if (left.type !== right.type) {
712
875
  return left.type.localeCompare(right.type);
713
876
  }
@@ -720,6 +883,66 @@ function collectMarketplaceInstalledView(options) {
720
883
  records
721
884
  };
722
885
  }
886
+ function sanitizeMarketplaceItem(item) {
887
+ const next = { ...item };
888
+ delete next.metrics;
889
+ return next;
890
+ }
891
+ function toPositiveInt(raw, fallback) {
892
+ if (!raw) {
893
+ return fallback;
894
+ }
895
+ const parsed = Number.parseInt(raw, 10);
896
+ if (!Number.isFinite(parsed) || parsed <= 0) {
897
+ return fallback;
898
+ }
899
+ return parsed;
900
+ }
901
+ function collectKnownSkillNames(options) {
902
+ const config = loadConfigOrDefault(options.configPath);
903
+ const loader = new SkillsLoader(getWorkspacePathFromConfig2(config));
904
+ return new Set(loader.listSkills(false).map((skill) => skill.name));
905
+ }
906
+ function isSupportedMarketplaceItem(item, knownSkillNames) {
907
+ if (item.type === "plugin") {
908
+ return item.install.kind === "npm" && item.install.spec.startsWith(NEXTCLAW_PLUGIN_NPM_PREFIX);
909
+ }
910
+ return item.install.kind === "builtin" && knownSkillNames.has(item.install.spec);
911
+ }
912
+ async function fetchAllMarketplaceItems(params) {
913
+ const allItems = [];
914
+ let remotePage = 1;
915
+ let remoteTotalPages = 1;
916
+ let sort = "relevance";
917
+ let query;
918
+ while (remotePage <= remoteTotalPages && remotePage <= MARKETPLACE_REMOTE_MAX_PAGES) {
919
+ const result = await fetchMarketplaceData({
920
+ baseUrl: params.baseUrl,
921
+ path: "/api/v1/items",
922
+ query: {
923
+ ...params.query,
924
+ page: String(remotePage),
925
+ pageSize: String(MARKETPLACE_REMOTE_PAGE_SIZE)
926
+ }
927
+ });
928
+ if (!result.ok) {
929
+ return result;
930
+ }
931
+ allItems.push(...result.data.items);
932
+ remoteTotalPages = result.data.totalPages;
933
+ sort = result.data.sort;
934
+ query = result.data.query;
935
+ remotePage += 1;
936
+ }
937
+ return {
938
+ ok: true,
939
+ data: {
940
+ sort,
941
+ query,
942
+ items: allItems
943
+ }
944
+ };
945
+ }
723
946
  async function installMarketplaceItem(params) {
724
947
  const type = params.body.type;
725
948
  const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
@@ -742,6 +965,7 @@ async function installMarketplaceItem(params) {
742
965
  }
743
966
  result = await installer.installSkill({
744
967
  slug: spec,
968
+ kind: params.body.kind,
745
969
  version: params.body.version,
746
970
  registry: params.body.registry,
747
971
  force: params.body.force
@@ -755,15 +979,61 @@ async function installMarketplaceItem(params) {
755
979
  output: result.output
756
980
  };
757
981
  }
982
+ async function manageMarketplaceItem(params) {
983
+ const type = params.body.type;
984
+ const action = params.body.action;
985
+ 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() : "";
986
+ if (type !== "plugin" && type !== "skill" || action !== "enable" && action !== "disable" && action !== "uninstall" || !targetId) {
987
+ throw new Error("INVALID_BODY:type, action and non-empty id/spec are required");
988
+ }
989
+ const installer = params.options.marketplace?.installer;
990
+ if (!installer) {
991
+ throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
992
+ }
993
+ let result;
994
+ if (type === "plugin") {
995
+ if (action === "enable") {
996
+ if (!installer.enablePlugin) {
997
+ throw new Error("NOT_AVAILABLE:plugin enable is not configured");
998
+ }
999
+ result = await installer.enablePlugin(targetId);
1000
+ } else if (action === "disable") {
1001
+ if (!installer.disablePlugin) {
1002
+ throw new Error("NOT_AVAILABLE:plugin disable is not configured");
1003
+ }
1004
+ result = await installer.disablePlugin(targetId);
1005
+ } else {
1006
+ if (!installer.uninstallPlugin) {
1007
+ throw new Error("NOT_AVAILABLE:plugin uninstall is not configured");
1008
+ }
1009
+ result = await installer.uninstallPlugin(targetId);
1010
+ }
1011
+ } else {
1012
+ if (action !== "uninstall") {
1013
+ throw new Error("NOT_AVAILABLE:skill only supports uninstall action");
1014
+ }
1015
+ if (!installer.uninstallSkill) {
1016
+ throw new Error("NOT_AVAILABLE:skill uninstall is not configured");
1017
+ }
1018
+ result = await installer.uninstallSkill(targetId);
1019
+ }
1020
+ params.options.publish({ type: "config.updated", payload: { path: type === "plugin" ? "plugins" : "skills" } });
1021
+ return {
1022
+ type,
1023
+ action,
1024
+ id: targetId,
1025
+ message: result.message,
1026
+ output: result.output
1027
+ };
1028
+ }
758
1029
  function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
759
1030
  app.get("/api/marketplace/installed", (c) => {
760
1031
  return c.json(ok(collectMarketplaceInstalledView(options)));
761
1032
  });
762
1033
  app.get("/api/marketplace/items", async (c) => {
763
1034
  const query = c.req.query();
764
- const result = await fetchMarketplaceData({
1035
+ const result = await fetchAllMarketplaceItems({
765
1036
  baseUrl: marketplaceBaseUrl,
766
- path: "/api/v1/items",
767
1037
  query: {
768
1038
  q: query.q,
769
1039
  type: query.type,
@@ -776,7 +1046,21 @@ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
776
1046
  if (!result.ok) {
777
1047
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
778
1048
  }
779
- return c.json(ok(result.data));
1049
+ const knownSkillNames = collectKnownSkillNames(options);
1050
+ const filteredItems = result.data.items.map((item) => sanitizeMarketplaceItem(item)).filter((item) => isSupportedMarketplaceItem(item, knownSkillNames));
1051
+ const pageSize = Math.min(100, toPositiveInt(query.pageSize, 20));
1052
+ const requestedPage = toPositiveInt(query.page, 1);
1053
+ const totalPages = filteredItems.length === 0 ? 0 : Math.ceil(filteredItems.length / pageSize);
1054
+ const currentPage = totalPages === 0 ? 1 : Math.min(requestedPage, totalPages);
1055
+ return c.json(ok({
1056
+ total: filteredItems.length,
1057
+ page: currentPage,
1058
+ pageSize,
1059
+ totalPages,
1060
+ sort: result.data.sort,
1061
+ query: result.data.query,
1062
+ items: filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize)
1063
+ }));
780
1064
  });
781
1065
  app.get("/api/marketplace/items/:slug", async (c) => {
782
1066
  const slug = encodeURIComponent(c.req.param("slug"));
@@ -791,7 +1075,12 @@ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
791
1075
  if (!result.ok) {
792
1076
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
793
1077
  }
794
- return c.json(ok(result.data));
1078
+ const knownSkillNames = collectKnownSkillNames(options);
1079
+ const sanitized = sanitizeMarketplaceItem(result.data);
1080
+ if (!isSupportedMarketplaceItem(sanitized, knownSkillNames)) {
1081
+ return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
1082
+ }
1083
+ return c.json(ok(sanitized));
795
1084
  });
796
1085
  app.get("/api/marketplace/recommendations", async (c) => {
797
1086
  const query = c.req.query();
@@ -806,7 +1095,13 @@ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
806
1095
  if (!result.ok) {
807
1096
  return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
808
1097
  }
809
- return c.json(ok(result.data));
1098
+ const knownSkillNames = collectKnownSkillNames(options);
1099
+ const filteredItems = result.data.items.map((item) => sanitizeMarketplaceItem(item)).filter((item) => isSupportedMarketplaceItem(item, knownSkillNames));
1100
+ return c.json(ok({
1101
+ ...result.data,
1102
+ total: filteredItems.length,
1103
+ items: filteredItems
1104
+ }));
810
1105
  });
811
1106
  app.post("/api/marketplace/install", async (c) => {
812
1107
  const body = await readJson(c.req.raw);
@@ -827,6 +1122,25 @@ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
827
1122
  return c.json(err("INSTALL_FAILED", message), 400);
828
1123
  }
829
1124
  });
1125
+ app.post("/api/marketplace/manage", async (c) => {
1126
+ const body = await readJson(c.req.raw);
1127
+ if (!body.ok || !body.data || typeof body.data !== "object") {
1128
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
1129
+ }
1130
+ try {
1131
+ const payload = await manageMarketplaceItem({ options, body: body.data });
1132
+ return c.json(ok(payload));
1133
+ } catch (error) {
1134
+ const message = String(error);
1135
+ if (message.startsWith("INVALID_BODY:")) {
1136
+ return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
1137
+ }
1138
+ if (message.startsWith("NOT_AVAILABLE:")) {
1139
+ return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
1140
+ }
1141
+ return c.json(err("MANAGE_FAILED", message), 400);
1142
+ }
1143
+ });
830
1144
  }
831
1145
  function createUiRouter(options) {
832
1146
  const app = new Hono();
@@ -1003,13 +1317,13 @@ function startUiServer(options) {
1003
1317
  })
1004
1318
  );
1005
1319
  const staticDir = options.staticDir;
1006
- if (staticDir && existsSync2(join2(staticDir, "index.html"))) {
1007
- const indexHtml = readFileSync(join2(staticDir, "index.html"), "utf-8");
1320
+ if (staticDir && existsSync(join(staticDir, "index.html"))) {
1321
+ const indexHtml = readFileSync(join(staticDir, "index.html"), "utf-8");
1008
1322
  app.use(
1009
1323
  "/*",
1010
1324
  serveStatic({
1011
1325
  root: staticDir,
1012
- join: join2,
1326
+ join,
1013
1327
  getContent: async (path) => {
1014
1328
  try {
1015
1329
  return await readFile(path);
@@ -1051,9 +1365,9 @@ function startUiServer(options) {
1051
1365
  host: options.host,
1052
1366
  port: options.port,
1053
1367
  publish,
1054
- close: () => new Promise((resolve2) => {
1368
+ close: () => new Promise((resolve) => {
1055
1369
  wss.close(() => {
1056
- server.close(() => resolve2());
1370
+ server.close(() => resolve());
1057
1371
  });
1058
1372
  })
1059
1373
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/server",
3
- "version": "0.4.17",
3
+ "version": "0.5.1",
4
4
  "private": false,
5
5
  "description": "Nextclaw UI/API server.",
6
6
  "type": "module",
@@ -15,6 +15,7 @@
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
21
  "@nextclaw/core": "^0.6.27"