@nextclaw/server 0.5.0 → 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 +40 -8
- package/dist/index.js +364 -50
- package/package.json +2 -1
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 { 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
|
-
|
|
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 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
|
|
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
|
-
|
|
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
|
-
|
|
836
|
+
seenPluginIds.add(pluginId);
|
|
673
837
|
}
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
850
|
+
seenPluginIds.add(pluginId);
|
|
684
851
|
}
|
|
685
852
|
}
|
|
686
|
-
const
|
|
687
|
-
const
|
|
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
|
-
|
|
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) => {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
1007
|
-
const indexHtml = readFileSync(
|
|
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
|
|
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((
|
|
1368
|
+
close: () => new Promise((resolve) => {
|
|
1055
1369
|
wss.close(() => {
|
|
1056
|
-
server.close(() =>
|
|
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.5.
|
|
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"
|