@nextclaw/server 0.4.16 → 0.4.17

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
@@ -214,6 +214,98 @@ type ConfigActionExecuteResult = {
214
214
  patch?: Record<string, unknown>;
215
215
  nextActions?: string[];
216
216
  };
217
+ type MarketplaceItemType = "plugin" | "skill";
218
+ type MarketplaceSort = "relevance" | "updated" | "downloads";
219
+ type MarketplaceInstallKind = "npm" | "clawhub" | "git";
220
+ type MarketplaceInstallSpec = {
221
+ kind: MarketplaceInstallKind;
222
+ spec: string;
223
+ command: string;
224
+ };
225
+ type MarketplaceItemMetrics = {
226
+ downloads30d?: number;
227
+ stars?: number;
228
+ };
229
+ type MarketplaceItemSummary = {
230
+ id: string;
231
+ slug: string;
232
+ type: MarketplaceItemType;
233
+ name: string;
234
+ summary: string;
235
+ tags: string[];
236
+ author: string;
237
+ install: MarketplaceInstallSpec;
238
+ metrics?: MarketplaceItemMetrics;
239
+ updatedAt: string;
240
+ };
241
+ type MarketplaceItemView = MarketplaceItemSummary & {
242
+ description?: string;
243
+ sourceRepo?: string;
244
+ homepage?: string;
245
+ publishedAt: string;
246
+ };
247
+ type MarketplaceListView = {
248
+ total: number;
249
+ page: number;
250
+ pageSize: number;
251
+ totalPages: number;
252
+ sort: MarketplaceSort;
253
+ query?: string;
254
+ items: MarketplaceItemSummary[];
255
+ };
256
+ type MarketplaceRecommendationView = {
257
+ sceneId: string;
258
+ title: string;
259
+ description?: string;
260
+ total: number;
261
+ items: MarketplaceItemSummary[];
262
+ };
263
+ type MarketplaceInstalledRecord = {
264
+ type: MarketplaceItemType;
265
+ spec: string;
266
+ label?: string;
267
+ source?: string;
268
+ installedAt?: string;
269
+ };
270
+ type MarketplaceInstalledView = {
271
+ total: number;
272
+ pluginSpecs: string[];
273
+ skillSpecs: string[];
274
+ records: MarketplaceInstalledRecord[];
275
+ };
276
+ type MarketplaceInstallRequest = {
277
+ type: MarketplaceItemType;
278
+ spec: string;
279
+ version?: string;
280
+ registry?: string;
281
+ force?: boolean;
282
+ };
283
+ type MarketplaceInstallResult = {
284
+ type: MarketplaceItemType;
285
+ spec: string;
286
+ message: string;
287
+ output?: string;
288
+ };
289
+ type MarketplaceInstallSkillParams = {
290
+ slug: string;
291
+ version?: string;
292
+ registry?: string;
293
+ force?: boolean;
294
+ };
295
+ type MarketplaceInstaller = {
296
+ installPlugin?: (spec: string) => Promise<{
297
+ message: string;
298
+ output?: string;
299
+ }>;
300
+ installSkill?: (params: MarketplaceInstallSkillParams) => Promise<{
301
+ message: string;
302
+ output?: string;
303
+ }>;
304
+ };
305
+ type MarketplaceApiConfig = {
306
+ apiBaseUrl?: string;
307
+ installer?: MarketplaceInstaller;
308
+ };
217
309
  type UiServerEvent = {
218
310
  type: "config.updated";
219
311
  payload: {
@@ -238,6 +330,7 @@ type UiServerOptions = {
238
330
  configPath: string;
239
331
  corsOrigins?: string[] | "*";
240
332
  staticDir?: string;
333
+ marketplace?: MarketplaceApiConfig;
241
334
  };
242
335
  type UiServerHandle = {
243
336
  host: string;
@@ -251,6 +344,7 @@ declare function startUiServer(options: UiServerOptions): UiServerHandle;
251
344
  type UiRouterOptions = {
252
345
  configPath: string;
253
346
  publish: (event: UiServerEvent) => void;
347
+ marketplace?: MarketplaceApiConfig;
254
348
  };
255
349
  declare function createUiRouter(options: UiRouterOptions): Hono;
256
350
 
@@ -284,4 +378,4 @@ declare function patchSession(configPath: string, key: string, patch: SessionPat
284
378
  declare function deleteSession(configPath: string, key: string): boolean;
285
379
  declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
286
380
 
287
- 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 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 };
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 };
package/dist/index.js CHANGED
@@ -3,12 +3,13 @@ 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, readFileSync } from "fs";
6
+ import { existsSync as existsSync2, readFileSync } from "fs";
7
7
  import { readFile, stat } from "fs/promises";
8
- import { join } from "path";
8
+ import { join as join2 } from "path";
9
9
 
10
10
  // src/ui/router.ts
11
11
  import { Hono } from "hono";
12
+ import { expandHome } from "@nextclaw/core";
12
13
 
13
14
  // src/ui/config.ts
14
15
  import {
@@ -568,6 +569,9 @@ function updateRuntime(configPath, patch) {
568
569
  }
569
570
 
570
571
  // 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";
571
575
  function ok(data) {
572
576
  return { ok: true, data };
573
577
  }
@@ -582,8 +586,251 @@ async function readJson(req) {
582
586
  return { ok: false };
583
587
  }
584
588
  }
589
+ function isRecord(value) {
590
+ return typeof value === "object" && value !== null && !Array.isArray(value);
591
+ }
592
+ function readErrorMessage(value, fallback) {
593
+ if (!isRecord(value)) {
594
+ return fallback;
595
+ }
596
+ const maybeError = value.error;
597
+ if (!isRecord(maybeError)) {
598
+ return fallback;
599
+ }
600
+ return typeof maybeError.message === "string" && maybeError.message.trim().length > 0 ? maybeError.message : fallback;
601
+ }
602
+ function normalizeMarketplaceBaseUrl(options) {
603
+ const fromOptions = options.marketplace?.apiBaseUrl?.trim();
604
+ const fromEnv = process.env.NEXTCLAW_MARKETPLACE_API_BASE?.trim();
605
+ const value = fromOptions || fromEnv || DEFAULT_MARKETPLACE_API_BASE;
606
+ return value.endsWith("/") ? value.slice(0, -1) : value;
607
+ }
608
+ function toMarketplaceUrl(baseUrl, path, query = {}) {
609
+ const url = new URL(path, `${baseUrl}/`);
610
+ for (const [key, value] of Object.entries(query)) {
611
+ if (typeof value === "string" && value.trim().length > 0) {
612
+ url.searchParams.set(key, value);
613
+ }
614
+ }
615
+ return url.toString();
616
+ }
617
+ async function fetchMarketplaceData(params) {
618
+ const url = toMarketplaceUrl(params.baseUrl, params.path, params.query ?? {});
619
+ try {
620
+ const response = await fetch(url, {
621
+ method: "GET",
622
+ headers: {
623
+ Accept: "application/json"
624
+ }
625
+ });
626
+ let payload = null;
627
+ try {
628
+ payload = await response.json();
629
+ } catch {
630
+ payload = null;
631
+ }
632
+ if (!response.ok) {
633
+ return {
634
+ ok: false,
635
+ status: response.status,
636
+ message: readErrorMessage(payload, `marketplace request failed: ${response.status}`)
637
+ };
638
+ }
639
+ if (!isRecord(payload) || payload.ok !== true || !Object.prototype.hasOwnProperty.call(payload, "data")) {
640
+ return {
641
+ ok: false,
642
+ status: 502,
643
+ message: "invalid marketplace response"
644
+ };
645
+ }
646
+ return {
647
+ ok: true,
648
+ data: payload.data
649
+ };
650
+ } catch (error) {
651
+ return {
652
+ ok: false,
653
+ status: 502,
654
+ message: `marketplace fetch failed: ${String(error)}`
655
+ };
656
+ }
657
+ }
658
+ function collectMarketplaceInstalledView(options) {
659
+ const config = loadConfigOrDefault(options.configPath);
660
+ const pluginRecordsMap = config.plugins.installs ?? {};
661
+ const pluginRecords = [];
662
+ const pluginSpecSet = /* @__PURE__ */ new Set();
663
+ for (const [pluginId, installRecord] of Object.entries(pluginRecordsMap)) {
664
+ const normalizedSpec = typeof installRecord.spec === "string" && installRecord.spec.trim().length > 0 ? installRecord.spec.trim() : pluginId;
665
+ pluginRecords.push({
666
+ type: "plugin",
667
+ spec: normalizedSpec,
668
+ label: pluginId,
669
+ source: installRecord.source,
670
+ installedAt: installRecord.installedAt
671
+ });
672
+ pluginSpecSet.add(normalizedSpec);
673
+ }
674
+ const pluginEntries = config.plugins.entries ?? {};
675
+ for (const pluginId of Object.keys(pluginEntries)) {
676
+ if (!pluginSpecSet.has(pluginId)) {
677
+ pluginRecords.push({
678
+ type: "plugin",
679
+ spec: pluginId,
680
+ label: pluginId,
681
+ source: "config"
682
+ });
683
+ pluginSpecSet.add(pluginId);
684
+ }
685
+ }
686
+ const workspacePath = resolve(expandHome(config.agents.defaults.workspace));
687
+ const skillsPath = join(workspacePath, "skills");
688
+ 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) => {
711
+ if (left.type !== right.type) {
712
+ return left.type.localeCompare(right.type);
713
+ }
714
+ return left.spec.localeCompare(right.spec);
715
+ });
716
+ return {
717
+ total: records.length,
718
+ pluginSpecs: Array.from(pluginSpecSet).sort((left, right) => left.localeCompare(right)),
719
+ skillSpecs: Array.from(skillSpecSet).sort((left, right) => left.localeCompare(right)),
720
+ records
721
+ };
722
+ }
723
+ async function installMarketplaceItem(params) {
724
+ const type = params.body.type;
725
+ const spec = typeof params.body.spec === "string" ? params.body.spec.trim() : "";
726
+ if (type !== "plugin" && type !== "skill" || !spec) {
727
+ throw new Error("INVALID_BODY:type and non-empty spec are required");
728
+ }
729
+ const installer = params.options.marketplace?.installer;
730
+ if (!installer) {
731
+ throw new Error("NOT_AVAILABLE:marketplace installer is not configured");
732
+ }
733
+ let result;
734
+ if (type === "plugin") {
735
+ if (!installer.installPlugin) {
736
+ throw new Error("NOT_AVAILABLE:plugin installer is not configured");
737
+ }
738
+ result = await installer.installPlugin(spec);
739
+ } else {
740
+ if (!installer.installSkill) {
741
+ throw new Error("NOT_AVAILABLE:skill installer is not configured");
742
+ }
743
+ result = await installer.installSkill({
744
+ slug: spec,
745
+ version: params.body.version,
746
+ registry: params.body.registry,
747
+ force: params.body.force
748
+ });
749
+ }
750
+ params.options.publish({ type: "config.updated", payload: { path: type === "plugin" ? "plugins" : "skills" } });
751
+ return {
752
+ type,
753
+ spec,
754
+ message: result.message,
755
+ output: result.output
756
+ };
757
+ }
758
+ function registerMarketplaceRoutes(app, options, marketplaceBaseUrl) {
759
+ app.get("/api/marketplace/installed", (c) => {
760
+ return c.json(ok(collectMarketplaceInstalledView(options)));
761
+ });
762
+ app.get("/api/marketplace/items", async (c) => {
763
+ const query = c.req.query();
764
+ const result = await fetchMarketplaceData({
765
+ baseUrl: marketplaceBaseUrl,
766
+ path: "/api/v1/items",
767
+ query: {
768
+ q: query.q,
769
+ type: query.type,
770
+ tag: query.tag,
771
+ sort: query.sort,
772
+ page: query.page,
773
+ pageSize: query.pageSize
774
+ }
775
+ });
776
+ if (!result.ok) {
777
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
778
+ }
779
+ return c.json(ok(result.data));
780
+ });
781
+ app.get("/api/marketplace/items/:slug", async (c) => {
782
+ const slug = encodeURIComponent(c.req.param("slug"));
783
+ const type = c.req.query("type");
784
+ const result = await fetchMarketplaceData({
785
+ baseUrl: marketplaceBaseUrl,
786
+ path: `/api/v1/items/${slug}`,
787
+ query: {
788
+ type
789
+ }
790
+ });
791
+ if (!result.ok) {
792
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
793
+ }
794
+ return c.json(ok(result.data));
795
+ });
796
+ app.get("/api/marketplace/recommendations", async (c) => {
797
+ const query = c.req.query();
798
+ const result = await fetchMarketplaceData({
799
+ baseUrl: marketplaceBaseUrl,
800
+ path: "/api/v1/recommendations",
801
+ query: {
802
+ scene: query.scene,
803
+ limit: query.limit
804
+ }
805
+ });
806
+ if (!result.ok) {
807
+ return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
808
+ }
809
+ return c.json(ok(result.data));
810
+ });
811
+ app.post("/api/marketplace/install", async (c) => {
812
+ const body = await readJson(c.req.raw);
813
+ if (!body.ok || !body.data || typeof body.data !== "object") {
814
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
815
+ }
816
+ try {
817
+ const payload = await installMarketplaceItem({ options, body: body.data });
818
+ return c.json(ok(payload));
819
+ } catch (error) {
820
+ const message = String(error);
821
+ if (message.startsWith("INVALID_BODY:")) {
822
+ return c.json(err("INVALID_BODY", message.slice("INVALID_BODY:".length)), 400);
823
+ }
824
+ if (message.startsWith("NOT_AVAILABLE:")) {
825
+ return c.json(err("NOT_AVAILABLE", message.slice("NOT_AVAILABLE:".length)), 503);
826
+ }
827
+ return c.json(err("INSTALL_FAILED", message), 400);
828
+ }
829
+ });
830
+ }
585
831
  function createUiRouter(options) {
586
832
  const app = new Hono();
833
+ const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
587
834
  app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
588
835
  app.get("/api/health", (c) => c.json(ok({ status: "ok" })));
589
836
  app.get("/api/config", (c) => {
@@ -719,6 +966,7 @@ function createUiRouter(options) {
719
966
  }
720
967
  return c.json(ok(result.data));
721
968
  });
969
+ registerMarketplaceRoutes(app, options, marketplaceBaseUrl);
722
970
  return app;
723
971
  }
724
972
 
@@ -750,17 +998,18 @@ function startUiServer(options) {
750
998
  "/",
751
999
  createUiRouter({
752
1000
  configPath: options.configPath,
753
- publish
1001
+ publish,
1002
+ marketplace: options.marketplace
754
1003
  })
755
1004
  );
756
1005
  const staticDir = options.staticDir;
757
- if (staticDir && existsSync(join(staticDir, "index.html"))) {
758
- const indexHtml = readFileSync(join(staticDir, "index.html"), "utf-8");
1006
+ if (staticDir && existsSync2(join2(staticDir, "index.html"))) {
1007
+ const indexHtml = readFileSync(join2(staticDir, "index.html"), "utf-8");
759
1008
  app.use(
760
1009
  "/*",
761
1010
  serveStatic({
762
1011
  root: staticDir,
763
- join,
1012
+ join: join2,
764
1013
  getContent: async (path) => {
765
1014
  try {
766
1015
  return await readFile(path);
@@ -802,9 +1051,9 @@ function startUiServer(options) {
802
1051
  host: options.host,
803
1052
  port: options.port,
804
1053
  publish,
805
- close: () => new Promise((resolve) => {
1054
+ close: () => new Promise((resolve2) => {
806
1055
  wss.close(() => {
807
- server.close(() => resolve());
1056
+ server.close(() => resolve2());
808
1057
  });
809
1058
  })
810
1059
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/server",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "private": false,
5
5
  "description": "Nextclaw UI/API server.",
6
6
  "type": "module",