@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 +40 -8
- package/dist/index.js +380 -50
- package/package.json +3 -2
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"
|
|
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
|
|
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
|
|
6
|
+
import { existsSync, readFileSync } from "fs";
|
|
7
7
|
import { readFile, stat } from "fs/promises";
|
|
8
|
-
import { join
|
|
8
|
+
import { join } from "path";
|
|
9
9
|
|
|
10
10
|
// src/ui/router.ts
|
|
11
11
|
import { Hono } from "hono";
|
|
12
|
-
import
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
var
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
852
|
+
seenPluginIds.add(pluginId);
|
|
673
853
|
}
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
866
|
+
seenPluginIds.add(pluginId);
|
|
684
867
|
}
|
|
685
868
|
}
|
|
686
|
-
const
|
|
687
|
-
const
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
1007
|
-
const indexHtml = readFileSync(
|
|
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
|
|
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((
|
|
1384
|
+
close: () => new Promise((resolve) => {
|
|
1055
1385
|
wss.close(() => {
|
|
1056
|
-
server.close(() =>
|
|
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.
|
|
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.
|
|
21
|
+
"@nextclaw/core": "^0.6.28"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@types/node": "^20.17.6",
|