@xrxs-ai/dsh-xrxs-skillhub 0.1.0
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/LICENSE +21 -0
- package/README.en.md +44 -0
- package/README.md +73 -0
- package/cordis.patch.yml +17 -0
- package/lib/api-client.d.ts +217 -0
- package/lib/api-client.d.ts.map +1 -0
- package/lib/api-client.js +265 -0
- package/lib/api-client.js.map +1 -0
- package/lib/atomic-file.d.ts +23 -0
- package/lib/atomic-file.d.ts.map +1 -0
- package/lib/atomic-file.js +62 -0
- package/lib/atomic-file.js.map +1 -0
- package/lib/client.d.ts +2 -0
- package/lib/client.js +423 -0
- package/lib/config.d.ts +60 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +71 -0
- package/lib/config.js.map +1 -0
- package/lib/index.d.ts +152 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +293 -0
- package/lib/index.js.map +1 -0
- package/lib/installer.d.ts +73 -0
- package/lib/installer.d.ts.map +1 -0
- package/lib/installer.js +206 -0
- package/lib/installer.js.map +1 -0
- package/lib/pack.d.ts +42 -0
- package/lib/pack.d.ts.map +1 -0
- package/lib/pack.js +117 -0
- package/lib/pack.js.map +1 -0
- package/lib/settings.d.ts +83 -0
- package/lib/settings.d.ts.map +1 -0
- package/lib/settings.js +154 -0
- package/lib/settings.js.map +1 -0
- package/lib/tools.d.ts +60 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +264 -0
- package/lib/tools.js.map +1 -0
- package/lib/web.d.ts +35 -0
- package/lib/web.d.ts.map +1 -0
- package/lib/web.js +874 -0
- package/lib/web.js.map +1 -0
- package/lib/zip.d.ts +36 -0
- package/lib/zip.d.ts.map +1 -0
- package/lib/zip.js +102 -0
- package/lib/zip.js.map +1 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xrxs-ai contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @xrxs-ai/dsh-xrxs-skillhub
|
|
2
|
+
|
|
3
|
+
> 中文文档:[README.md](README.md)
|
|
4
|
+
|
|
5
|
+
A DeepSeek Harness (Cordis) plugin for the private SkillHub registry: search, view, install, and manage your organization's agent skills inside DSH.
|
|
6
|
+
|
|
7
|
+
- **Registry**: defaults to `https://skillhub.xinrenxinshi.com` (editable on the settings page).
|
|
8
|
+
- **Auth**: the console-issued **API Token** (`sk_...`) — the same credential the official CLI uses. Anonymous callers only see PUBLIC skills.
|
|
9
|
+
- **Install target**: the DSH user-level skill scan root `~/.dsh/skills` by default, which hot-loads without a restart.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Model tools**: `skillhub_search` / `skillhub_view` / `skillhub_install` / `skillhub_installed` / `skillhub_remove`, with stable names across restarts.
|
|
14
|
+
- **Settings page + marketplace**: the built-in same-origin `/skillhub` page offers marketplace search (keyword / namespace / sort / pagination), skill detail (versions + file manifest + SKILL.md preview), one-click install, and an installed-list with uninstall.
|
|
15
|
+
- **Sidebar entry**: the browser half injects a "SkillHub" footer button (with a status dot) into the left sidebar; hosts without the slot system fall back to a better-sidebar tab.
|
|
16
|
+
- **Token precedence**: page-pasted token > loader patch config > anonymous. The plugin persists the token itself (`~/.xrxs/dsh-xrxs-skillhub/settings.json`, 0600, atomic); the page only ever sees a `hasToken` boolean.
|
|
17
|
+
- **Safe install flow**: resolve version → size-capped zip download → pure-JS fflate extraction (traversal/absolute-path/size guards, wrapper-dir stripping) → SKILL.md validation → backup + rename swap with rollback. Only plugin-owned directories are ever touched; the `.skillhub/metadata.json` marker matches the official CLI.
|
|
18
|
+
- **Zero startup side effects**: nothing dials the network during load; mounting failures degrade to log lines and can never take the profile down.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Loaded as an npm bundle row (the shipped patch pre-configures the default registry):
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
- id: skillhub
|
|
26
|
+
name: '@xrxs-ai/dsh-xrxs-skillhub'
|
|
27
|
+
config:
|
|
28
|
+
registry: 'https://skillhub.xinrenxinshi.com'
|
|
29
|
+
# token: !!js process.env.SKILLHUB_API_TOKEN
|
|
30
|
+
# installDir: /absolute/path
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Recommended: configure from the page.** Open `<local-web-url>/skillhub` (or the sidebar button), paste your API token, and hit "Test connection". Page settings override the patch config and hot-apply immediately.
|
|
34
|
+
|
|
35
|
+
## Development
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pnpm install
|
|
39
|
+
pnpm run typecheck
|
|
40
|
+
pnpm run test
|
|
41
|
+
pnpm run build # host half → lib/
|
|
42
|
+
pnpm run build:client # browser half → lib/client.js
|
|
43
|
+
pnpm pack # builds both halves into the installable .tgz
|
|
44
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @xrxs-ai/dsh-xrxs-skillhub
|
|
2
|
+
|
|
3
|
+
> English documentation: [README.en.md](README.en.md)
|
|
4
|
+
|
|
5
|
+
连接**私有 SkillHub 技能仓库**的 DeepSeek Harness(Cordis)插件:在 DSH 内搜索、查看、下载、管理组织的 Agent 技能。
|
|
6
|
+
|
|
7
|
+
- **仓库地址**:默认指向 `https://skillhub.xinrenxinshi.com`(可在设置页修改)
|
|
8
|
+
- **鉴权**:控制台签发的 **API Token**(`sk_...`),与官方 CLI `skillhub login --token` 同一把;匿名只能看公开技能
|
|
9
|
+
- **安装目标**:默认装到 DSH 用户级 skill 扫描根 `~/.dsh/skills`,**免重启热生效**
|
|
10
|
+
|
|
11
|
+
## 特性
|
|
12
|
+
|
|
13
|
+
- **模型工具**:注册 `skillhub_search` / `skillhub_view` / `skillhub_install` / `skillhub_installed` / `skillhub_remove` 五个稳定命名的工具,agent 可直接对话式搜索/查看/安装技能。
|
|
14
|
+
- **设置页 + 技能市场**:内置 `/skillhub` 同源页面——市场搜索(关键词/命名空间/排序/分页)、技能详情(版本列表 + 文件清单 + SKILL.md 在线预览)、一键安装、已安装列表与卸载。
|
|
15
|
+
- **侧栏入口**:浏览器半在左下侧栏 footer 注入「SkillHub」按钮(带状态点),点开浮动面板内嵌同源页面;不支持插槽系统的旧宿主回退到 better-sidebar 页签。
|
|
16
|
+
- **Token 三级来源**:设置页粘贴 > 加载器 patch 配置 > 匿名。Token 由插件自己持久化(`~/.xrxs/dsh-xrxs-skillhub/settings.json`,0600,原子写),页面只回显「是否已配置」,永不回显明文。
|
|
17
|
+
- **安全安装流程**:resolve 版本 → 64MB 上限下载 zip → fflate 纯 JS 解压(拒绝路径穿越/绝对路径/超限,自动剥包装目录)→ 校验 SKILL.md → 备份 + rename 原子替换(失败回滚)。只动自己名下的目录,与官方 CLI 同名安装记录(`.skillhub/metadata.json`)。
|
|
18
|
+
- **启动零副作用**:加载阶段不访问网络、不弹窗;工具或页面挂载失败只降级记日志,绝不拖垮 profile。
|
|
19
|
+
|
|
20
|
+
## 使用
|
|
21
|
+
|
|
22
|
+
npm 包形式随 profile 组合加载(bundle patch 已预置默认行,装上即生效):
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
- id: skillhub
|
|
26
|
+
name: '@xrxs-ai/dsh-xrxs-skillhub'
|
|
27
|
+
config:
|
|
28
|
+
registry: 'https://skillhub.xinrenxinshi.com' # 默认私有仓库
|
|
29
|
+
# token: !!js process.env.SKILLHUB_API_TOKEN # 可选:patch 注入(页面配置优先)
|
|
30
|
+
# installDir: /absolute/path # 可选:默认 ~/.dsh/skills
|
|
31
|
+
# requestTimeoutMs: 30000
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**推荐:从设置页配置。** 打开 `<local-web-url>/skillhub`(或点侧栏「SkillHub」按钮),粘贴 API Token,点「测试连接」即可;页面保存的配置覆盖 patch 配置,保存即时生效。
|
|
35
|
+
|
|
36
|
+
> 页面保存的设置存于 `~/.xrxs/dsh-xrxs-skillhub/settings.json`;「恢复默认」删除该文件回到 patch 配置。要覆盖 patch 行本身,请在 profile 自己的 `cordis.patch.yml` 中 override 行 id `skillhub`(override 会整体替换 `config`,需重述全部字段)。
|
|
37
|
+
|
|
38
|
+
## API Token 从哪来
|
|
39
|
+
|
|
40
|
+
SkillHub 控制台 → **API Tokens** → 生成 `sk_...`。这把 token 同时用于:
|
|
41
|
+
|
|
42
|
+
- 官方 CLI:`skillhub login --token sk_...`
|
|
43
|
+
- 本插件:设置页粘贴(推荐)
|
|
44
|
+
|
|
45
|
+
插件不会读取或改写 CLI 的凭证文件(`~/.skillhub/credentials.json`)——两者独立,用同一把 token 即可。
|
|
46
|
+
|
|
47
|
+
## 开发
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pnpm install
|
|
51
|
+
pnpm run typecheck # tsc --noEmit(strict)
|
|
52
|
+
pnpm run test # node --test tests/*.test.ts(Node 直接跑 .ts)
|
|
53
|
+
pnpm run build # host 半:lib/(ESM + d.ts)
|
|
54
|
+
pnpm run build:client # browser 半:tsconfig.client.json + scripts/build-client.mjs
|
|
55
|
+
pnpm pack # prepack 构建双半,产出可安装 .tgz
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 架构
|
|
59
|
+
|
|
60
|
+
- **Host 半(`src/`)**:`index.ts` 组装 controller(设置/测试/搜索/详情/安装/卸载)+ 模型工具 + web 页;`api-client.ts` 封装 REST(`/api/web/skills` 搜索、`/api/v1/skills/...` 详情/版本/文件/下载、`/api/v1/whoami` 校验);`installer.ts` 负责安装状态与原子替换;`zip.ts` 安全解压;`settings.ts` 页面设置(0600 原子写)。
|
|
61
|
+
- **Browser 半(`src/client/`)**:`sidebar.footer.action` 插槽按钮 + 主题跟随的 iframe 浮动面板。单独编译为 CommonJS 后由 `scripts/build-client.mjs` 包成 DSH client module-loader bundle(`lib/client.js`);**client bundle 纯净性**——运行时只允许 `require('react')`。
|
|
62
|
+
- **打包**:`dsh.bundle.patch` → `cordis.patch.yml`,`dsh.client` 声明 browser face。
|
|
63
|
+
|
|
64
|
+
### TypeScript 约定
|
|
65
|
+
|
|
66
|
+
`tsconfig.json` 全 strict(`exactOptionalPropertyTypes`、`noUncheckedIndexedAccess`、`erasableSyntaxOnly`、`verbatimModuleSyntax`);相对导入带 `.ts` 扩展名(emit 时改写为 `.js`),测试直接 import `../src/*.ts` 依赖 Node 类型剥离(需要 `engines` 声明的 Node 版本)。
|
|
67
|
+
|
|
68
|
+
### 不变量
|
|
69
|
+
|
|
70
|
+
- Token 不进日志;设置文件 0600 + 原子写;页面只见 `hasToken` 布尔值。
|
|
71
|
+
- 启动绝不访问网络;任何挂载失败只降级,绝不拖垮桌面。
|
|
72
|
+
- 安装只触碰状态文件(`~/.xrxs/dsh-xrxs-skillhub/installed.json`)登记过的目录;手工放置的同名目录是冲突报错,绝不覆盖;替换走 temp → validate → rename 带回滚,绝不在原位解压。
|
|
73
|
+
- 工具命名 `skillhub_*` 跨重启稳定。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# dsh-xrxs-skillhub bundle patch: insert the SkillHub connector into the profile.
|
|
2
|
+
#
|
|
3
|
+
# The row ships ENABLED and pre-configured with the private SkillHub registry.
|
|
4
|
+
# The plugin loads, registers its model tools and the `/skillhub` settings
|
|
5
|
+
# page, and works anonymously against PUBLIC skills until the user pastes an
|
|
6
|
+
# API token on the page (page-saved settings override this config and are
|
|
7
|
+
# hot-applied without a restart).
|
|
8
|
+
#
|
|
9
|
+
# Prefer editing files instead? Override row id `skillhub` from the profile's
|
|
10
|
+
# own `cordis.patch.yml` (applied after every bundle layer) — a config
|
|
11
|
+
# override replaces this row's whole `config`, so restate everything.
|
|
12
|
+
- insert:
|
|
13
|
+
- id: skillhub
|
|
14
|
+
name: '@xrxs-ai/dsh-xrxs-skillhub'
|
|
15
|
+
disabled: false
|
|
16
|
+
config:
|
|
17
|
+
registry: 'https://skillhub.xinrenxinshi.com'
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillHub REST API client for the private registry.
|
|
3
|
+
*
|
|
4
|
+
* Endpoints (all relative to the registry base URL):
|
|
5
|
+
* GET /api/v1/whoami — token check (401 when anonymous)
|
|
6
|
+
* GET /api/web/skills?q=&namespace=&sort=&page=&size= — search (enveloped, paged)
|
|
7
|
+
* GET /api/v1/skills/{ns}/{slug} — skill detail
|
|
8
|
+
* GET /api/v1/skills/{ns}/{slug}/versions — version list
|
|
9
|
+
* GET /api/v1/skills/{ns}/{slug}/versions/{v}/files — file manifest
|
|
10
|
+
* GET /api/v1/skills/{ns}/{slug}/versions/{v}/file?path= — raw file content
|
|
11
|
+
* GET /api/v1/skills/{ns}/{slug}/resolve?version= — version resolution
|
|
12
|
+
* GET /api/v1/skills/{ns}/{slug}/versions/{v}/download — zip package (binary)
|
|
13
|
+
*
|
|
14
|
+
* The token is the console-issued API token (`sk_...`); the CLI uses the
|
|
15
|
+
* same credential. Anonymous callers only see PUBLIC skills.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/** One skill as returned by the search endpoint. */
|
|
20
|
+
export interface SearchItem {
|
|
21
|
+
/** Web shape: the namespace slug as its own field. */
|
|
22
|
+
namespace?: string;
|
|
23
|
+
slug: string;
|
|
24
|
+
displayName?: string;
|
|
25
|
+
summary?: string;
|
|
26
|
+
/** Web shape (`/api/web/skills`): flat counters. */
|
|
27
|
+
downloadCount?: number;
|
|
28
|
+
starCount?: number;
|
|
29
|
+
/** Web shape: lifecycle projections carry the versions. */
|
|
30
|
+
publishedVersion?: {
|
|
31
|
+
version?: string;
|
|
32
|
+
} | null;
|
|
33
|
+
headlineVersion?: {
|
|
34
|
+
version?: string;
|
|
35
|
+
} | null;
|
|
36
|
+
/** ClawHub compat shape (`/api/v1/skills`): nested stats + latestVersion. */
|
|
37
|
+
stats?: {
|
|
38
|
+
downloads?: number;
|
|
39
|
+
stars?: number;
|
|
40
|
+
};
|
|
41
|
+
updatedAt?: number;
|
|
42
|
+
latestVersion?: {
|
|
43
|
+
version?: string;
|
|
44
|
+
createdAt?: number;
|
|
45
|
+
changelog?: string | null;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Download count across both search response shapes. */
|
|
49
|
+
export declare function downloadsOf(item: SearchItem): number;
|
|
50
|
+
/** Latest published version string across both search response shapes. */
|
|
51
|
+
export declare function versionOf(item: SearchItem): string;
|
|
52
|
+
/** The namespace slug across both shapes (compat items carry a composite slug). */
|
|
53
|
+
export declare function namespaceOf(item: SearchItem): string;
|
|
54
|
+
/** One namespace entry (list + me/namespaces endpoints). */
|
|
55
|
+
export interface NamespaceInfo {
|
|
56
|
+
slug?: string;
|
|
57
|
+
displayName?: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
type?: string;
|
|
60
|
+
currentUserRole?: string;
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
}
|
|
63
|
+
/** Search result page. */
|
|
64
|
+
export interface SearchPage {
|
|
65
|
+
items: SearchItem[];
|
|
66
|
+
total: number;
|
|
67
|
+
page: number;
|
|
68
|
+
size: number;
|
|
69
|
+
}
|
|
70
|
+
/** Skill detail (envelope `data`). */
|
|
71
|
+
export interface SkillDetail {
|
|
72
|
+
id?: number;
|
|
73
|
+
namespace?: string;
|
|
74
|
+
slug?: string;
|
|
75
|
+
displayName?: string;
|
|
76
|
+
ownerDisplayName?: string;
|
|
77
|
+
summary?: string;
|
|
78
|
+
visibility?: string;
|
|
79
|
+
latestVersion?: {
|
|
80
|
+
version?: string;
|
|
81
|
+
} | null;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
/** One version entry from the version list. */
|
|
85
|
+
export interface SkillVersion {
|
|
86
|
+
version: string;
|
|
87
|
+
status?: string;
|
|
88
|
+
changelog?: string | null;
|
|
89
|
+
fileCount?: number;
|
|
90
|
+
totalSize?: number;
|
|
91
|
+
publishedAt?: string;
|
|
92
|
+
downloadAvailable?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** One file entry from the manifest. */
|
|
95
|
+
export interface SkillFileInfo {
|
|
96
|
+
filePath: string;
|
|
97
|
+
fileSize?: number;
|
|
98
|
+
contentType?: string;
|
|
99
|
+
sha256?: string;
|
|
100
|
+
}
|
|
101
|
+
/** Resolve response (`data`). */
|
|
102
|
+
export interface ResolveData {
|
|
103
|
+
namespace?: string;
|
|
104
|
+
slug?: string;
|
|
105
|
+
version?: string;
|
|
106
|
+
versionId?: number;
|
|
107
|
+
fingerprint?: string;
|
|
108
|
+
downloadUrl?: string;
|
|
109
|
+
}
|
|
110
|
+
/** Whoami response, flattened from the compat `{user: {handle, ...}}` shape. */
|
|
111
|
+
export interface WhoAmI {
|
|
112
|
+
handle?: string;
|
|
113
|
+
displayName?: string;
|
|
114
|
+
image?: string;
|
|
115
|
+
email?: string;
|
|
116
|
+
[key: string]: unknown;
|
|
117
|
+
}
|
|
118
|
+
/** Registry unreachable, or the API returned a structured error envelope. */
|
|
119
|
+
export declare class SkillHubApiError extends Error {
|
|
120
|
+
/** HTTP status, when the failure was an HTTP-level one. */
|
|
121
|
+
readonly status: number;
|
|
122
|
+
/** Structured envelope `code`, when present (equals the HTTP status). */
|
|
123
|
+
readonly code: number;
|
|
124
|
+
constructor(message: string, status?: number, code?: number, options?: {
|
|
125
|
+
cause?: unknown;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Download cap: a bigger package is rejected before it fills the disk. */
|
|
129
|
+
export declare const MAX_DOWNLOAD_BYTES: number;
|
|
130
|
+
/** Read-back cap for file contents (page preview + model tools). */
|
|
131
|
+
export declare const MAX_FILE_BYTES: number;
|
|
132
|
+
/** Search parameters shared by the client, controller, and tool bridge. */
|
|
133
|
+
export interface SearchOptions {
|
|
134
|
+
q?: string | undefined;
|
|
135
|
+
namespace?: string | undefined;
|
|
136
|
+
sort?: string | undefined;
|
|
137
|
+
page?: number | undefined;
|
|
138
|
+
size?: number | undefined;
|
|
139
|
+
}
|
|
140
|
+
/** View parameters shared by the controller and the tool bridge. */
|
|
141
|
+
export interface ViewOptions {
|
|
142
|
+
namespace: string;
|
|
143
|
+
slug: string;
|
|
144
|
+
version?: string | undefined;
|
|
145
|
+
path?: string | undefined;
|
|
146
|
+
}
|
|
147
|
+
/** Install parameters shared by the controller and the tool bridge. */
|
|
148
|
+
export interface InstallOptions {
|
|
149
|
+
namespace: string;
|
|
150
|
+
slug: string;
|
|
151
|
+
version?: string | undefined;
|
|
152
|
+
}
|
|
153
|
+
export interface ApiClientOptions {
|
|
154
|
+
registry: string;
|
|
155
|
+
token?: string;
|
|
156
|
+
requestTimeoutMs?: number;
|
|
157
|
+
fetchImpl?: typeof fetch;
|
|
158
|
+
}
|
|
159
|
+
/** Normalized API client for one registry. */
|
|
160
|
+
export declare class SkillHubApiClient {
|
|
161
|
+
readonly registry: string;
|
|
162
|
+
private readonly token;
|
|
163
|
+
private readonly timeoutMs;
|
|
164
|
+
private readonly fetchImpl;
|
|
165
|
+
constructor(options: ApiClientOptions);
|
|
166
|
+
/**
|
|
167
|
+
* Verify the configured token (or probe anonymous access). A 401 means
|
|
168
|
+
* "not authenticated" — mapped to `undefined`, not an error, so callers
|
|
169
|
+
* can distinguish "no token" from "broken registry". The compat endpoint
|
|
170
|
+
* nests the user object; the result is flattened.
|
|
171
|
+
*/
|
|
172
|
+
whoami(): Promise<WhoAmI | undefined>;
|
|
173
|
+
/** Visible namespaces (market filter); anonymous callers see public ones. */
|
|
174
|
+
namespaces(limit?: number): Promise<NamespaceInfo[]>;
|
|
175
|
+
/** The authenticated user's namespaces (publish target picker); requires a token. */
|
|
176
|
+
myNamespaces(): Promise<NamespaceInfo[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Publish a packaged skill (zip with `SKILL.md` at its root) into a
|
|
179
|
+
* namespace. `confirmWarnings: true` acknowledges pre-check warnings —
|
|
180
|
+
* a 400 without it usually means the server wants explicit confirmation.
|
|
181
|
+
*/
|
|
182
|
+
publish(options: {
|
|
183
|
+
namespace: string;
|
|
184
|
+
zip: Uint8Array;
|
|
185
|
+
visibility: 'PUBLIC' | 'NAMESPACE_ONLY' | 'PRIVATE';
|
|
186
|
+
confirmWarnings?: boolean;
|
|
187
|
+
}): Promise<Record<string, unknown>>;
|
|
188
|
+
/** Full-text search with optional namespace filter, sort, and pagination. */
|
|
189
|
+
search(options?: SearchOptions): Promise<SearchPage>;
|
|
190
|
+
/** Skill detail. */
|
|
191
|
+
skill(namespace: string, slug: string): Promise<SkillDetail>;
|
|
192
|
+
/** Version list (newest first). */
|
|
193
|
+
versions(namespace: string, slug: string): Promise<SkillVersion[]>;
|
|
194
|
+
/** File manifest for one version. */
|
|
195
|
+
files(namespace: string, slug: string, version: string): Promise<SkillFileInfo[]>;
|
|
196
|
+
/** Raw file content for one version, capped at {@link MAX_FILE_BYTES}. */
|
|
197
|
+
file(namespace: string, slug: string, version: string, path: string): Promise<string>;
|
|
198
|
+
/** Resolve the effective version for a skill (latest published by default). */
|
|
199
|
+
resolve(namespace: string, slug: string, version?: string): Promise<ResolveData>;
|
|
200
|
+
/**
|
|
201
|
+
* Download the zip package for one version (latest published when
|
|
202
|
+
* `version` is omitted), size-capped at {@link MAX_DOWNLOAD_BYTES}.
|
|
203
|
+
*/
|
|
204
|
+
download(namespace: string, slug: string, version?: string): Promise<Buffer>;
|
|
205
|
+
/** GET a JSON endpoint and unwrap the `{code, msg, data}` envelope. */
|
|
206
|
+
private getJson;
|
|
207
|
+
/** GET with auth header, timeout, and HTTP error mapping. */
|
|
208
|
+
private request;
|
|
209
|
+
/**
|
|
210
|
+
* Map a non-OK response to SkillHubApiError. Failure envelopes use the
|
|
211
|
+
* same `{code, msg}` structure with code = HTTP status; the server's
|
|
212
|
+
* message is surfaced when one is present (publish pre-check warnings
|
|
213
|
+
* arrive exactly this way).
|
|
214
|
+
*/
|
|
215
|
+
private fail;
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillHub REST API client for the private registry.
|
|
3
|
+
*
|
|
4
|
+
* Endpoints (all relative to the registry base URL):
|
|
5
|
+
* GET /api/v1/whoami — token check (401 when anonymous)
|
|
6
|
+
* GET /api/web/skills?q=&namespace=&sort=&page=&size= — search (enveloped, paged)
|
|
7
|
+
* GET /api/v1/skills/{ns}/{slug} — skill detail
|
|
8
|
+
* GET /api/v1/skills/{ns}/{slug}/versions — version list
|
|
9
|
+
* GET /api/v1/skills/{ns}/{slug}/versions/{v}/files — file manifest
|
|
10
|
+
* GET /api/v1/skills/{ns}/{slug}/versions/{v}/file?path= — raw file content
|
|
11
|
+
* GET /api/v1/skills/{ns}/{slug}/resolve?version= — version resolution
|
|
12
|
+
* GET /api/v1/skills/{ns}/{slug}/versions/{v}/download — zip package (binary)
|
|
13
|
+
*
|
|
14
|
+
* The token is the console-issued API token (`sk_...`); the CLI uses the
|
|
15
|
+
* same credential. Anonymous callers only see PUBLIC skills.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/** Download count across both search response shapes. */
|
|
20
|
+
export function downloadsOf(item) {
|
|
21
|
+
return item.downloadCount ?? item.stats?.downloads ?? 0;
|
|
22
|
+
}
|
|
23
|
+
/** Latest published version string across both search response shapes. */
|
|
24
|
+
export function versionOf(item) {
|
|
25
|
+
return item.publishedVersion?.version ?? item.latestVersion?.version ?? item.headlineVersion?.version ?? '';
|
|
26
|
+
}
|
|
27
|
+
/** The namespace slug across both shapes (compat items carry a composite slug). */
|
|
28
|
+
export function namespaceOf(item) {
|
|
29
|
+
if (item.namespace !== undefined && item.namespace !== '')
|
|
30
|
+
return item.namespace;
|
|
31
|
+
const index = item.slug.indexOf('--');
|
|
32
|
+
return index > 0 ? item.slug.slice(0, index) : '';
|
|
33
|
+
}
|
|
34
|
+
/** Registry unreachable, or the API returned a structured error envelope. */
|
|
35
|
+
export class SkillHubApiError extends Error {
|
|
36
|
+
/** HTTP status, when the failure was an HTTP-level one. */
|
|
37
|
+
status;
|
|
38
|
+
/** Structured envelope `code`, when present (equals the HTTP status). */
|
|
39
|
+
code;
|
|
40
|
+
constructor(message, status = 0, code = 0, options) {
|
|
41
|
+
super(message, options);
|
|
42
|
+
this.name = 'SkillHubApiError';
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.code = code;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Download cap: a bigger package is rejected before it fills the disk. */
|
|
48
|
+
export const MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024;
|
|
49
|
+
/** Read-back cap for file contents (page preview + model tools). */
|
|
50
|
+
export const MAX_FILE_BYTES = 512 * 1024;
|
|
51
|
+
/** Normalized API client for one registry. */
|
|
52
|
+
export class SkillHubApiClient {
|
|
53
|
+
registry;
|
|
54
|
+
token;
|
|
55
|
+
timeoutMs;
|
|
56
|
+
fetchImpl;
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.registry = options.registry.replace(/\/+$/, '');
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = new URL(this.registry);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new SkillHubApiError(`invalid registry URL: ${options.registry}`);
|
|
65
|
+
}
|
|
66
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
67
|
+
throw new SkillHubApiError(`registry must use http or https: ${options.registry}`);
|
|
68
|
+
}
|
|
69
|
+
this.token = options.token ?? '';
|
|
70
|
+
this.timeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
71
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Verify the configured token (or probe anonymous access). A 401 means
|
|
75
|
+
* "not authenticated" — mapped to `undefined`, not an error, so callers
|
|
76
|
+
* can distinguish "no token" from "broken registry". The compat endpoint
|
|
77
|
+
* nests the user object; the result is flattened.
|
|
78
|
+
*/
|
|
79
|
+
async whoami() {
|
|
80
|
+
try {
|
|
81
|
+
const data = await this.getJson('/api/v1/whoami');
|
|
82
|
+
const record = typeof data === 'object' && data !== null ? data : {};
|
|
83
|
+
const user = typeof record.user === 'object' && record.user !== null ? record.user : record;
|
|
84
|
+
return user;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof SkillHubApiError && (error.status === 401 || error.status === 403))
|
|
88
|
+
return undefined;
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Visible namespaces (market filter); anonymous callers see public ones. */
|
|
93
|
+
async namespaces(limit = 100) {
|
|
94
|
+
const data = await this.getJson(`/api/v1/namespaces?page=0&size=${Math.min(200, Math.max(1, limit))}`);
|
|
95
|
+
return Array.isArray(data?.items) ? data.items : [];
|
|
96
|
+
}
|
|
97
|
+
/** The authenticated user's namespaces (publish target picker); requires a token. */
|
|
98
|
+
async myNamespaces() {
|
|
99
|
+
const data = await this.getJson('/api/v1/me/namespaces');
|
|
100
|
+
return Array.isArray(data) ? data : [];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Publish a packaged skill (zip with `SKILL.md` at its root) into a
|
|
104
|
+
* namespace. `confirmWarnings: true` acknowledges pre-check warnings —
|
|
105
|
+
* a 400 without it usually means the server wants explicit confirmation.
|
|
106
|
+
*/
|
|
107
|
+
async publish(options) {
|
|
108
|
+
const form = new FormData();
|
|
109
|
+
form.append('file', new Blob([options.zip], { type: 'application/zip' }), 'skill.zip');
|
|
110
|
+
form.append('visibility', options.visibility);
|
|
111
|
+
if (options.confirmWarnings === true)
|
|
112
|
+
form.append('confirmWarnings', 'true');
|
|
113
|
+
let response;
|
|
114
|
+
try {
|
|
115
|
+
response = await this.fetchImpl(`${this.registry}/api/v1/skills/${encodeURIComponent(options.namespace)}/publish`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: this.token !== '' ? { authorization: `Bearer ${this.token}` } : {},
|
|
118
|
+
body: form,
|
|
119
|
+
redirect: 'follow',
|
|
120
|
+
signal: AbortSignal.timeout(Math.max(this.timeoutMs, 120_000)),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw new SkillHubApiError(`registry unreachable: ${this.registry}`, 0, 0, { cause: error });
|
|
125
|
+
}
|
|
126
|
+
if (!response.ok)
|
|
127
|
+
await this.fail(response);
|
|
128
|
+
const body = await response.json();
|
|
129
|
+
if (typeof body === 'object' && body !== null && 'code' in body) {
|
|
130
|
+
const envelope = body;
|
|
131
|
+
if (envelope.code !== 0) {
|
|
132
|
+
throw new SkillHubApiError(typeof envelope.msg === 'string' && envelope.msg !== '' ? envelope.msg : `API error ${String(envelope.code)}`, response.status, typeof envelope.code === 'number' ? envelope.code : 0);
|
|
133
|
+
}
|
|
134
|
+
return (envelope.data ?? {});
|
|
135
|
+
}
|
|
136
|
+
return (body ?? {});
|
|
137
|
+
}
|
|
138
|
+
/** Full-text search with optional namespace filter, sort, and pagination. */
|
|
139
|
+
async search(options = {}) {
|
|
140
|
+
const params = new URLSearchParams();
|
|
141
|
+
if (options.q !== undefined && options.q !== '')
|
|
142
|
+
params.set('q', options.q);
|
|
143
|
+
if (options.namespace !== undefined && options.namespace !== '')
|
|
144
|
+
params.set('namespace', options.namespace);
|
|
145
|
+
if (options.sort !== undefined && options.sort !== '')
|
|
146
|
+
params.set('sort', options.sort);
|
|
147
|
+
params.set('page', String(Math.max(0, options.page ?? 0)));
|
|
148
|
+
params.set('size', String(Math.min(50, Math.max(1, options.size ?? 20))));
|
|
149
|
+
const data = await this.getJson(`/api/web/skills?${params.toString()}`);
|
|
150
|
+
return {
|
|
151
|
+
items: Array.isArray(data?.items) ? data.items : [],
|
|
152
|
+
total: typeof data?.total === 'number' ? data.total : 0,
|
|
153
|
+
page: typeof data?.page === 'number' ? data.page : 0,
|
|
154
|
+
size: typeof data?.size === 'number' ? data.size : 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/** Skill detail. */
|
|
158
|
+
async skill(namespace, slug) {
|
|
159
|
+
return await this.getJson(`/api/v1/skills/${encodeURIComponent(namespace)}/${encodeURIComponent(slug)}`);
|
|
160
|
+
}
|
|
161
|
+
/** Version list (newest first). */
|
|
162
|
+
async versions(namespace, slug) {
|
|
163
|
+
const data = await this.getJson(`/api/v1/skills/${encodeURIComponent(namespace)}/${encodeURIComponent(slug)}/versions`);
|
|
164
|
+
return Array.isArray(data?.items) ? data.items : [];
|
|
165
|
+
}
|
|
166
|
+
/** File manifest for one version. */
|
|
167
|
+
async files(namespace, slug, version) {
|
|
168
|
+
const data = await this.getJson(`/api/v1/skills/${encodeURIComponent(namespace)}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/files`);
|
|
169
|
+
return Array.isArray(data) ? data : [];
|
|
170
|
+
}
|
|
171
|
+
/** Raw file content for one version, capped at {@link MAX_FILE_BYTES}. */
|
|
172
|
+
async file(namespace, slug, version, path) {
|
|
173
|
+
const params = new URLSearchParams({ path });
|
|
174
|
+
const response = await this.request(`/api/v1/skills/${encodeURIComponent(namespace)}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/file?${params.toString()}`);
|
|
175
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
176
|
+
if (buffer.length > MAX_FILE_BYTES) {
|
|
177
|
+
throw new SkillHubApiError(`file larger than ${MAX_FILE_BYTES} bytes`, 0, 0);
|
|
178
|
+
}
|
|
179
|
+
return buffer.toString('utf8');
|
|
180
|
+
}
|
|
181
|
+
/** Resolve the effective version for a skill (latest published by default). */
|
|
182
|
+
async resolve(namespace, slug, version) {
|
|
183
|
+
const suffix = version !== undefined && version !== '' ? `?version=${encodeURIComponent(version)}` : '';
|
|
184
|
+
return await this.getJson(`/api/v1/skills/${encodeURIComponent(namespace)}/${encodeURIComponent(slug)}/resolve${suffix}`);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Download the zip package for one version (latest published when
|
|
188
|
+
* `version` is omitted), size-capped at {@link MAX_DOWNLOAD_BYTES}.
|
|
189
|
+
*/
|
|
190
|
+
async download(namespace, slug, version) {
|
|
191
|
+
const suffix = version !== undefined && version !== '' ? `versions/${encodeURIComponent(version)}/download` : 'download';
|
|
192
|
+
const response = await this.request(`/api/v1/skills/${encodeURIComponent(namespace)}/${encodeURIComponent(slug)}/${suffix}`);
|
|
193
|
+
const declared = Number(response.headers.get('content-length') ?? '');
|
|
194
|
+
if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
|
|
195
|
+
throw new SkillHubApiError(`package larger than ${MAX_DOWNLOAD_BYTES} bytes`);
|
|
196
|
+
}
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let total = 0;
|
|
199
|
+
for await (const chunk of response.body ?? []) {
|
|
200
|
+
const bytes = chunk;
|
|
201
|
+
total += bytes.length;
|
|
202
|
+
if (total > MAX_DOWNLOAD_BYTES)
|
|
203
|
+
throw new SkillHubApiError(`package larger than ${MAX_DOWNLOAD_BYTES} bytes`);
|
|
204
|
+
chunks.push(bytes);
|
|
205
|
+
}
|
|
206
|
+
return Buffer.concat(chunks);
|
|
207
|
+
}
|
|
208
|
+
/** GET a JSON endpoint and unwrap the `{code, msg, data}` envelope. */
|
|
209
|
+
async getJson(path) {
|
|
210
|
+
const response = await this.request(path);
|
|
211
|
+
const body = await response.json();
|
|
212
|
+
// Enveloped endpoints use {code:0, data}; tolerate bare payloads too.
|
|
213
|
+
if (typeof body === 'object' && body !== null && 'code' in body) {
|
|
214
|
+
const envelope = body;
|
|
215
|
+
if (envelope.code !== 0) {
|
|
216
|
+
throw new SkillHubApiError(typeof envelope.msg === 'string' && envelope.msg !== '' ? envelope.msg : `API error ${String(envelope.code)}`, response.status, typeof envelope.code === 'number' ? envelope.code : 0);
|
|
217
|
+
}
|
|
218
|
+
return envelope.data;
|
|
219
|
+
}
|
|
220
|
+
return body;
|
|
221
|
+
}
|
|
222
|
+
/** GET with auth header, timeout, and HTTP error mapping. */
|
|
223
|
+
async request(path) {
|
|
224
|
+
const url = `${this.registry}${path}`;
|
|
225
|
+
let response;
|
|
226
|
+
try {
|
|
227
|
+
response = await this.fetchImpl(url, {
|
|
228
|
+
headers: this.token !== '' ? { authorization: `Bearer ${this.token}` } : {},
|
|
229
|
+
redirect: 'follow',
|
|
230
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
throw new SkillHubApiError(`registry unreachable: ${url}`, 0, 0, { cause: error });
|
|
235
|
+
}
|
|
236
|
+
if (!response.ok)
|
|
237
|
+
await this.fail(response);
|
|
238
|
+
return response;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Map a non-OK response to SkillHubApiError. Failure envelopes use the
|
|
242
|
+
* same `{code, msg}` structure with code = HTTP status; the server's
|
|
243
|
+
* message is surfaced when one is present (publish pre-check warnings
|
|
244
|
+
* arrive exactly this way).
|
|
245
|
+
*/
|
|
246
|
+
async fail(response) {
|
|
247
|
+
let message = '';
|
|
248
|
+
try {
|
|
249
|
+
const body = await response.json();
|
|
250
|
+
if (typeof body?.msg === 'string' && body.msg !== '')
|
|
251
|
+
message = body.msg;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Not JSON — fall through to the generic messages.
|
|
255
|
+
}
|
|
256
|
+
if (response.status === 401 || response.status === 403) {
|
|
257
|
+
throw new SkillHubApiError(message !== '' ? message : 'authentication failed — token missing, invalid, or out of scope', response.status, response.status);
|
|
258
|
+
}
|
|
259
|
+
if (response.status === 404) {
|
|
260
|
+
throw new SkillHubApiError(message !== '' ? message : 'skill or version not found', response.status, response.status);
|
|
261
|
+
}
|
|
262
|
+
throw new SkillHubApiError(message !== '' ? message : `registry request failed with HTTP ${response.status}`, response.status, response.status);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=api-client.js.map
|