@storyclaw/talenthub 0.3.6 → 0.3.8
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/README.md +273 -0
- package/dist/cli.js +12 -2
- package/dist/commands/agent-init.d.ts +3 -0
- package/dist/commands/agent-init.js +110 -0
- package/dist/commands/agent-install.d.ts +1 -0
- package/dist/commands/agent-install.js +85 -29
- package/dist/commands/agent-publish.d.ts +3 -1
- package/dist/commands/agent-publish.js +37 -42
- package/dist/commands/agent-uninstall.js +0 -1
- package/dist/commands/agent-update.d.ts +1 -0
- package/dist/commands/agent-update.js +72 -17
- package/dist/lib/skills.d.ts +9 -1
- package/dist/lib/skills.js +54 -22
- package/dist/lib/update-check.js +2 -1
- package/dist/lib/version.d.ts +21 -0
- package/dist/lib/version.js +53 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# @storyclaw/talenthub
|
|
2
|
+
|
|
3
|
+
CLI tool to manage StoryClaw AI agents — publish, install, update, and browse the TalentHub marketplace.
|
|
4
|
+
|
|
5
|
+
[中文文档](#中文文档)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @storyclaw/talenthub
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Login
|
|
17
|
+
talenthub login
|
|
18
|
+
|
|
19
|
+
# Initialize an agent
|
|
20
|
+
talenthub agent init --dir ~/.openclaw/workspace-my-agent
|
|
21
|
+
|
|
22
|
+
# Publish it
|
|
23
|
+
talenthub agent publish --dir ~/.openclaw/workspace-my-agent
|
|
24
|
+
|
|
25
|
+
# Browse & install agents
|
|
26
|
+
talenthub agent search
|
|
27
|
+
talenthub agent install <agent-name>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
| Command | Description |
|
|
33
|
+
|---------|-------------|
|
|
34
|
+
| `talenthub login` | Authenticate with StoryClaw |
|
|
35
|
+
| `talenthub logout` | Remove stored credentials |
|
|
36
|
+
| `talenthub agent init` | Initialize a new agent with manifest.json and prompt files |
|
|
37
|
+
| `talenthub agent publish` | Publish a local agent to the registry |
|
|
38
|
+
| `talenthub agent unpublish <name>` | Archive an agent from the registry |
|
|
39
|
+
| `talenthub agent install <name>` | Install an agent and its skills |
|
|
40
|
+
| `talenthub agent update [name]` | Update an agent or all agents (`--all`, `--json`) |
|
|
41
|
+
| `talenthub agent uninstall <name>` | Remove an installed agent |
|
|
42
|
+
| `talenthub agent list` | List installed agents and check for updates |
|
|
43
|
+
| `talenthub agent search [query]` | Browse available agents |
|
|
44
|
+
|
|
45
|
+
## Agent Structure
|
|
46
|
+
|
|
47
|
+
A publishable agent directory contains:
|
|
48
|
+
|
|
49
|
+
| File | Required | Description |
|
|
50
|
+
|------|----------|-------------|
|
|
51
|
+
| `manifest.json` | **Required** | Agent metadata (id, name, category, etc.) |
|
|
52
|
+
| `IDENTITY.md` | Yes | Core identity and personality prompt |
|
|
53
|
+
| `USER.md` | Optional | User-facing instructions |
|
|
54
|
+
| `SOUL.md` | Optional | Deep personality and behavioral guidelines |
|
|
55
|
+
| `AGENTS.md` | Optional | Multi-agent coordination instructions |
|
|
56
|
+
|
|
57
|
+
### manifest.json
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"id": "my-agent",
|
|
62
|
+
"name": "My Agent",
|
|
63
|
+
"emoji": "🤖",
|
|
64
|
+
"role": "Assistant",
|
|
65
|
+
"tagline": "A helpful AI assistant",
|
|
66
|
+
"description": "Longer description of what this agent does.",
|
|
67
|
+
"category": "productivity",
|
|
68
|
+
"skills": [],
|
|
69
|
+
"i18n": {},
|
|
70
|
+
"minOpenClawVersion": "2026.3.1"
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Field | Type | Description |
|
|
75
|
+
|-------|------|-------------|
|
|
76
|
+
| `id` | string | Unique identifier (lowercase alphanumeric + hyphens) |
|
|
77
|
+
| `name` | string | Human-readable display name |
|
|
78
|
+
| `emoji` | string | Single emoji icon |
|
|
79
|
+
| `role` | string | Short role description |
|
|
80
|
+
| `tagline` | string | One-line summary for search results |
|
|
81
|
+
| `description` | string | Longer description for detail page |
|
|
82
|
+
| `category` | string | One of: `creative`, `finance`, `productivity`, `companion`, `research`, `engineering` |
|
|
83
|
+
| `skills` | string[] | Skill specs (e.g. `https://github.com/owner/repo@skill-name`) |
|
|
84
|
+
| `i18n` | object | Translations keyed by locale (`zh-CN`, `zh-TW`, `ja`). Fill via web interface. |
|
|
85
|
+
| `minOpenClawVersion` | string | Minimum StoryClaw version required |
|
|
86
|
+
| `avatarUrl` | string \| null | Optional avatar image URL |
|
|
87
|
+
|
|
88
|
+
> **Note:** Version is not in the manifest — it is generated automatically by the server.
|
|
89
|
+
|
|
90
|
+
### Size Limits
|
|
91
|
+
|
|
92
|
+
- Each file: **200 KB** max
|
|
93
|
+
- Total across all files: **1 MB** max
|
|
94
|
+
|
|
95
|
+
## Publishing
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# From a directory (recommended)
|
|
99
|
+
talenthub agent publish --dir /path/to/agent
|
|
100
|
+
|
|
101
|
+
# From an installed agent
|
|
102
|
+
talenthub agent publish --name my-agent
|
|
103
|
+
|
|
104
|
+
# From the current directory
|
|
105
|
+
talenthub agent publish
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Option | Description |
|
|
109
|
+
|--------|-------------|
|
|
110
|
+
| `--dir <path>` | Agent directory with manifest.json and .md files |
|
|
111
|
+
| `--name <name>` | Agent name in openclaw config (resolves workspace dir) |
|
|
112
|
+
| `--id <id>` | Override agent ID from manifest |
|
|
113
|
+
|
|
114
|
+
## Versioning
|
|
115
|
+
|
|
116
|
+
Versions are fully server-managed in `YYYY.MM.DD-X` format (e.g. `2026.04.13-1`).
|
|
117
|
+
|
|
118
|
+
- **New agent** → `YYYY.MM.DD-1`
|
|
119
|
+
- **Core field changes** (skills, USER.md, SOUL.md, AGENTS.md) → version auto-bumps
|
|
120
|
+
- **Metadata-only changes** (emoji, role, tagline, etc.) → version stays the same
|
|
121
|
+
|
|
122
|
+
Previous versions are automatically saved as snapshots.
|
|
123
|
+
|
|
124
|
+
## Environment Variables
|
|
125
|
+
|
|
126
|
+
| Variable | Description |
|
|
127
|
+
|----------|-------------|
|
|
128
|
+
| `TALENTHUB_URL` | Override registry base URL (default: `https://app.storyclaw.com`) |
|
|
129
|
+
| `TALENTHUB_REGISTRY` | Alias for `TALENTHUB_URL` |
|
|
130
|
+
|
|
131
|
+
## Docs
|
|
132
|
+
|
|
133
|
+
- [Publishing Guide (English)](https://github.com/storyclaw-official/talenthub/blob/main/docs/publishing-guide.md)
|
|
134
|
+
- [Publishing Guide (中文)](https://github.com/storyclaw-official/talenthub/blob/main/docs/publishing-guide.zh-CN.md)
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 中文文档
|
|
143
|
+
|
|
144
|
+
StoryClaw AI Agent 管理工具 — 发布、安装、更新和浏览 TalentHub 市场。
|
|
145
|
+
|
|
146
|
+
[English](#storyclawtalenthub)
|
|
147
|
+
|
|
148
|
+
### 安装
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm install -g @storyclaw/talenthub
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 快速开始
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# 登录
|
|
158
|
+
talenthub login
|
|
159
|
+
|
|
160
|
+
# 初始化 Agent
|
|
161
|
+
talenthub agent init --dir ~/.openclaw/workspace-my-agent
|
|
162
|
+
|
|
163
|
+
# 发布
|
|
164
|
+
talenthub agent publish --dir ~/.openclaw/workspace-my-agent
|
|
165
|
+
|
|
166
|
+
# 浏览和安装
|
|
167
|
+
talenthub agent search
|
|
168
|
+
talenthub agent install <agent-name>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 命令列表
|
|
172
|
+
|
|
173
|
+
| 命令 | 说明 |
|
|
174
|
+
|------|------|
|
|
175
|
+
| `talenthub login` | 登录 StoryClaw |
|
|
176
|
+
| `talenthub logout` | 退出登录 |
|
|
177
|
+
| `talenthub agent init` | 初始化新 Agent(生成 manifest.json 和提示词文件) |
|
|
178
|
+
| `talenthub agent publish` | 发布 Agent 到注册中心 |
|
|
179
|
+
| `talenthub agent unpublish <name>` | 下架 Agent |
|
|
180
|
+
| `talenthub agent install <name>` | 安装 Agent 及其技能 |
|
|
181
|
+
| `talenthub agent update [name]` | 更新 Agent(`--all` 更新全部,`--json` 结构化输出) |
|
|
182
|
+
| `talenthub agent uninstall <name>` | 卸载 Agent |
|
|
183
|
+
| `talenthub agent list` | 列出已安装的 Agent |
|
|
184
|
+
| `talenthub agent search [query]` | 搜索可用 Agent |
|
|
185
|
+
|
|
186
|
+
### Agent 目录结构
|
|
187
|
+
|
|
188
|
+
| 文件 | 是否必须 | 说明 |
|
|
189
|
+
|------|----------|------|
|
|
190
|
+
| `manifest.json` | **必须** | Agent 元数据(ID、名称、分类等) |
|
|
191
|
+
| `IDENTITY.md` | 必须 | 核心身份和人设提示词 |
|
|
192
|
+
| `USER.md` | 可选 | 面向用户的使用说明 |
|
|
193
|
+
| `SOUL.md` | 可选 | 深层性格和行为准则 |
|
|
194
|
+
| `AGENTS.md` | 可选 | 多 Agent 协作指令 |
|
|
195
|
+
|
|
196
|
+
### manifest.json
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"id": "my-agent",
|
|
201
|
+
"name": "My Agent",
|
|
202
|
+
"emoji": "🤖",
|
|
203
|
+
"role": "Assistant",
|
|
204
|
+
"tagline": "一句话描述你的 Agent",
|
|
205
|
+
"description": "更详细的介绍。",
|
|
206
|
+
"category": "productivity",
|
|
207
|
+
"skills": [],
|
|
208
|
+
"i18n": {},
|
|
209
|
+
"minOpenClawVersion": "2026.3.1"
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
| 字段 | 类型 | 说明 |
|
|
214
|
+
|------|------|------|
|
|
215
|
+
| `id` | string | 唯一标识符,小写字母加连字符 |
|
|
216
|
+
| `name` | string | 显示名称 |
|
|
217
|
+
| `emoji` | string | 一个 emoji 图标 |
|
|
218
|
+
| `role` | string | 角色简述 |
|
|
219
|
+
| `tagline` | string | 一行摘要 |
|
|
220
|
+
| `description` | string | 详细描述 |
|
|
221
|
+
| `category` | string | `creative`、`finance`、`productivity`、`companion`、`research`、`engineering` |
|
|
222
|
+
| `skills` | string[] | 技能包列表(如 `https://github.com/owner/repo@skill-name`) |
|
|
223
|
+
| `i18n` | object | 按语言代码存放翻译(`zh-CN`、`zh-TW`、`ja`),可通过网页界面填写 |
|
|
224
|
+
| `minOpenClawVersion` | string | 最低 StoryClaw 版本 |
|
|
225
|
+
| `avatarUrl` | string \| null | 头像 URL(可选) |
|
|
226
|
+
|
|
227
|
+
> **注意:** manifest 中没有 `version` 字段,版本由服务器自动生成。
|
|
228
|
+
|
|
229
|
+
### 发布
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# 从目录发布(推荐)
|
|
233
|
+
talenthub agent publish --dir /path/to/agent
|
|
234
|
+
|
|
235
|
+
# 从已安装的 Agent 发布
|
|
236
|
+
talenthub agent publish --name my-agent
|
|
237
|
+
|
|
238
|
+
# 从当前目录发布
|
|
239
|
+
talenthub agent publish
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| 选项 | 说明 |
|
|
243
|
+
|------|------|
|
|
244
|
+
| `--dir <path>` | Agent 目录 |
|
|
245
|
+
| `--name <name>` | openclaw 配置中的 Agent 名称 |
|
|
246
|
+
| `--id <id>` | 覆盖 manifest 中的 Agent ID |
|
|
247
|
+
|
|
248
|
+
### 版本管理
|
|
249
|
+
|
|
250
|
+
版本完全由服务器管理,格式为 `YYYY.MM.DD-X`(如 `2026.04.13-1`)。
|
|
251
|
+
|
|
252
|
+
- **新 Agent** → `YYYY.MM.DD-1`
|
|
253
|
+
- **核心字段变更**(skills、USER.md、SOUL.md、AGENTS.md)→ 版本自动递增
|
|
254
|
+
- **仅元数据变更**(emoji、role、tagline 等)→ 版本不变
|
|
255
|
+
|
|
256
|
+
每次版本递增时自动保存上一版本快照。
|
|
257
|
+
|
|
258
|
+
### 文件大小限制
|
|
259
|
+
|
|
260
|
+
- 单个文件:最大 **200 KB**
|
|
261
|
+
- 所有文件总计:最大 **1 MB**
|
|
262
|
+
|
|
263
|
+
### 环境变量
|
|
264
|
+
|
|
265
|
+
| 变量 | 说明 |
|
|
266
|
+
|------|------|
|
|
267
|
+
| `TALENTHUB_URL` | 覆盖注册中心地址(默认:`https://app.storyclaw.com`) |
|
|
268
|
+
| `TALENTHUB_REGISTRY` | `TALENTHUB_URL` 的别名 |
|
|
269
|
+
|
|
270
|
+
### 详细文档
|
|
271
|
+
|
|
272
|
+
- [发布指南(中文)](https://github.com/storyclaw-official/talenthub/blob/main/docs/publishing-guide.zh-CN.md)
|
|
273
|
+
- [Publishing Guide (English)](https://github.com/storyclaw-official/talenthub/blob/main/docs/publishing-guide.md)
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
// try IPv4 first to avoid EHOSTUNREACH delays on every fetch.
|
|
8
8
|
dns.setDefaultResultOrder("ipv4first");
|
|
9
9
|
import { Command } from "commander";
|
|
10
|
+
import { agentInit } from "./commands/agent-init.js";
|
|
10
11
|
import { agentInstall } from "./commands/agent-install.js";
|
|
11
12
|
import { agentList } from "./commands/agent-list.js";
|
|
12
13
|
import { agentPublish } from "./commands/agent-publish.js";
|
|
@@ -30,16 +31,23 @@ program
|
|
|
30
31
|
.action(login);
|
|
31
32
|
program.command("logout").description("Remove stored credentials").action(logout);
|
|
32
33
|
const agent = program.command("agent").description("Agent management commands");
|
|
34
|
+
agent
|
|
35
|
+
.command("init")
|
|
36
|
+
.description("Initialize a new agent with manifest.json and prompt files")
|
|
37
|
+
.option("-d, --dir <path>", "Target directory (defaults to current directory)")
|
|
38
|
+
.action(agentInit);
|
|
33
39
|
agent
|
|
34
40
|
.command("install <name>")
|
|
35
41
|
.description("Install an agent and its skills")
|
|
36
42
|
.option("-f, --force", "Overwrite existing agent", false)
|
|
37
43
|
.option("-t, --token <token>", "Authenticate with a th_* token for private agents")
|
|
44
|
+
.option("--json", "Output structured JSONL progress for machine consumption", false)
|
|
38
45
|
.action(agentInstall);
|
|
39
46
|
agent
|
|
40
47
|
.command("update [name]")
|
|
41
48
|
.description("Update an agent or all agents")
|
|
42
49
|
.option("-a, --all", "Update all installed agents")
|
|
50
|
+
.option("--json", "Output structured JSONL progress for machine consumption", false)
|
|
43
51
|
.action(agentUpdate);
|
|
44
52
|
agent
|
|
45
53
|
.command("uninstall <name>")
|
|
@@ -55,9 +63,11 @@ agent
|
|
|
55
63
|
.description("Browse available agents")
|
|
56
64
|
.action(agentSearch);
|
|
57
65
|
agent
|
|
58
|
-
.command("publish
|
|
66
|
+
.command("publish")
|
|
59
67
|
.description("Publish a local agent to the registry")
|
|
60
|
-
.option("-d, --dir <path>", "Agent directory containing manifest.json and .md files")
|
|
68
|
+
.option("-d, --dir <path>", "Agent directory containing manifest.json and .md files (defaults to current directory)")
|
|
69
|
+
.option("-n, --name <name>", "Agent name in openclaw config (used to resolve workspace dir)")
|
|
70
|
+
.option("--id <id>", "Override agent ID from manifest")
|
|
61
71
|
.action(agentPublish);
|
|
62
72
|
agent
|
|
63
73
|
.command("unpublish <name>")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
function prompt(question) {
|
|
5
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
rl.question(question, (answer) => {
|
|
8
|
+
rl.close();
|
|
9
|
+
resolve(answer.trim());
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse IDENTITY.md to extract Name and Emoji fields.
|
|
15
|
+
*
|
|
16
|
+
* Expected format:
|
|
17
|
+
* - **Name:** Aria
|
|
18
|
+
* - **Emoji:** 🎭
|
|
19
|
+
*/
|
|
20
|
+
function parseIdentityMd(dir) {
|
|
21
|
+
const filePath = path.join(dir, "IDENTITY.md");
|
|
22
|
+
if (!fs.existsSync(filePath))
|
|
23
|
+
return {};
|
|
24
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
25
|
+
const result = {};
|
|
26
|
+
const nameMatch = content.match(/\*\*Name:\*\*\s*(.+)/i);
|
|
27
|
+
if (nameMatch) {
|
|
28
|
+
// Take first part before | or — (e.g. "AI Assistant | AI 助理" → "AI Assistant")
|
|
29
|
+
result.name = nameMatch[1].split(/\s*[|—]\s*/)[0].trim();
|
|
30
|
+
}
|
|
31
|
+
const emojiMatch = content.match(/\*\*Emoji:\*\*\s*(.+)/i);
|
|
32
|
+
if (emojiMatch) {
|
|
33
|
+
const val = emojiMatch[1].trim();
|
|
34
|
+
// Skip if it says "none" or similar
|
|
35
|
+
if (val && !/^none/i.test(val)) {
|
|
36
|
+
result.emoji = val;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
const VALID_CATEGORIES = ["creative", "finance", "productivity", "companion", "research", "engineering"];
|
|
42
|
+
export async function agentInit(opts = {}) {
|
|
43
|
+
const dir = path.resolve(opts.dir || ".");
|
|
44
|
+
const manifestPath = path.join(dir, "manifest.json");
|
|
45
|
+
if (fs.existsSync(manifestPath)) {
|
|
46
|
+
console.error(`manifest.json already exists in ${dir}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (!fs.existsSync(dir)) {
|
|
50
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
// Derive defaults from directory name and IDENTITY.md
|
|
53
|
+
// Agent dirs are typically "workspace-xxx" — extract "xxx" as the id
|
|
54
|
+
const baseName = path.basename(dir);
|
|
55
|
+
const dirName = (baseName.startsWith("workspace-") ? baseName.slice("workspace-".length) : baseName)
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9-]/g, "-");
|
|
58
|
+
const identity = parseIdentityMd(dir);
|
|
59
|
+
// --- ID ---
|
|
60
|
+
const defaultId = /^[a-z0-9-]+$/.test(dirName) ? dirName : "";
|
|
61
|
+
const idInput = await prompt(defaultId ? `Agent ID [${defaultId}]: ` : "Agent ID (lowercase alphanumeric + hyphens): ");
|
|
62
|
+
const id = idInput || defaultId;
|
|
63
|
+
if (!id || !/^[a-z0-9-]+$/.test(id)) {
|
|
64
|
+
console.error("Invalid agent ID. Must be lowercase alphanumeric + hyphens only.");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
// --- Name ---
|
|
68
|
+
const defaultName = identity.name || "";
|
|
69
|
+
const nameInput = await prompt(defaultName ? `Agent name [${defaultName}]: ` : "Agent name: ");
|
|
70
|
+
const name = nameInput || defaultName;
|
|
71
|
+
if (!name) {
|
|
72
|
+
console.error("Agent name is required.");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
// --- Emoji ---
|
|
76
|
+
const defaultEmoji = identity.emoji || "";
|
|
77
|
+
const emojiInput = await prompt(defaultEmoji ? `Emoji [${defaultEmoji}]: ` : "Emoji (optional): ");
|
|
78
|
+
const emoji = emojiInput || defaultEmoji;
|
|
79
|
+
// --- Category ---
|
|
80
|
+
const category = await prompt(`Category (${VALID_CATEGORIES.join(", ")}) [productivity]: `);
|
|
81
|
+
const finalCategory = category || "productivity";
|
|
82
|
+
if (!VALID_CATEGORIES.includes(finalCategory)) {
|
|
83
|
+
console.error(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const role = await prompt("Role (optional): ");
|
|
87
|
+
const tagline = await prompt("Tagline (optional): ");
|
|
88
|
+
const manifest = {
|
|
89
|
+
id,
|
|
90
|
+
name,
|
|
91
|
+
category: finalCategory,
|
|
92
|
+
...(emoji && { emoji }),
|
|
93
|
+
...(role && { role }),
|
|
94
|
+
...(tagline && { tagline }),
|
|
95
|
+
description: "",
|
|
96
|
+
skills: [],
|
|
97
|
+
i18n: {},
|
|
98
|
+
};
|
|
99
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
100
|
+
console.log(`\n✓ Created ${manifestPath}`);
|
|
101
|
+
// Create placeholder prompt files if they don't exist
|
|
102
|
+
for (const file of ["IDENTITY.md", "USER.md", "SOUL.md", "AGENTS.md"]) {
|
|
103
|
+
const filePath = path.join(dir, file);
|
|
104
|
+
if (!fs.existsSync(filePath)) {
|
|
105
|
+
fs.writeFileSync(filePath, `# ${file.replace(".md", "")}\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log("✓ Created prompt files (IDENTITY.md, USER.md, SOUL.md, AGENTS.md)");
|
|
109
|
+
console.log(`\nNext: edit the prompt files, then run "talenthub agent publish ${id} --dir ${dir}"`);
|
|
110
|
+
}
|
|
@@ -6,72 +6,128 @@ import { addOrUpdateAgent, findAgentEntry, readConfig, writeConfig } from "../li
|
|
|
6
6
|
import { fetchCatalog, fetchManifest } from "../lib/registry.js";
|
|
7
7
|
import { resolveWorkspaceDir } from "../lib/paths.js";
|
|
8
8
|
import { markInstalled } from "../lib/state.js";
|
|
9
|
+
function jsonl(obj) {
|
|
10
|
+
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
11
|
+
}
|
|
12
|
+
// Weight breakdown: manifest 0→5%, skills 5→90%, files 90→95%, config 95→100%
|
|
13
|
+
const WEIGHT = { manifest: 5, skillsStart: 5, skillsEnd: 90, files: 95, config: 100 };
|
|
9
14
|
export async function agentInstall(name, options) {
|
|
10
15
|
const token = options.token;
|
|
16
|
+
const json = options.json === true;
|
|
17
|
+
const log = json ? () => { } : console.log.bind(console);
|
|
11
18
|
if (token) {
|
|
12
19
|
if (!token.startsWith("th_")) {
|
|
13
|
-
|
|
20
|
+
if (json)
|
|
21
|
+
jsonl({ event: "error", message: "Invalid token format" });
|
|
22
|
+
else
|
|
23
|
+
console.error("Invalid token format. Token must start with 'th_'.");
|
|
14
24
|
process.exit(1);
|
|
15
25
|
}
|
|
16
|
-
|
|
26
|
+
log("Verifying token...");
|
|
17
27
|
try {
|
|
18
28
|
await verifyToken(token);
|
|
19
29
|
}
|
|
20
30
|
catch {
|
|
21
|
-
|
|
31
|
+
if (json)
|
|
32
|
+
jsonl({ event: "error", message: "Token verification failed" });
|
|
33
|
+
else
|
|
34
|
+
console.error("Token verification failed. Please check your token and try again.");
|
|
22
35
|
process.exit(1);
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
|
-
|
|
38
|
+
log(`Looking up agent "${name}"...`);
|
|
26
39
|
const catalog = await fetchCatalog(token);
|
|
27
40
|
if (!catalog.agents[name]) {
|
|
28
41
|
const available = Object.keys(catalog.agents).join(", ");
|
|
29
|
-
|
|
42
|
+
if (json)
|
|
43
|
+
jsonl({ event: "error", message: `Agent "${name}" not found`, available });
|
|
44
|
+
else
|
|
45
|
+
console.error(`Agent "${name}" not found. Available: ${available}`);
|
|
30
46
|
process.exit(1);
|
|
31
47
|
}
|
|
32
48
|
const manifest = await fetchManifest(name, token);
|
|
33
|
-
|
|
49
|
+
log(`Found ${manifest.emoji} ${manifest.name} v${manifest.version} (${manifest.skills.length} skills)`);
|
|
50
|
+
if (json)
|
|
51
|
+
jsonl({
|
|
52
|
+
event: "start",
|
|
53
|
+
agentId: manifest.id, name: manifest.name, emoji: manifest.emoji,
|
|
54
|
+
version: manifest.version, skillCount: manifest.skills.length,
|
|
55
|
+
});
|
|
56
|
+
if (json)
|
|
57
|
+
jsonl({ event: "progress", phase: "manifest", percent: WEIGHT.manifest });
|
|
34
58
|
const cfg = readConfig();
|
|
35
59
|
const existing = findAgentEntry(cfg, name);
|
|
36
60
|
if (existing && !options.force) {
|
|
37
|
-
|
|
61
|
+
if (json)
|
|
62
|
+
jsonl({ event: "error", message: `Agent "${name}" already exists. Use --force.` });
|
|
63
|
+
else
|
|
64
|
+
console.error(`Agent "${name}" already exists in config. Use --force to overwrite.`);
|
|
38
65
|
process.exit(1);
|
|
39
66
|
}
|
|
40
67
|
const wsDir = resolveWorkspaceDir(name);
|
|
41
68
|
fs.mkdirSync(wsDir, { recursive: true });
|
|
42
|
-
|
|
43
|
-
if (manifest.files) {
|
|
44
|
-
for (const [filename, content] of Object.entries(manifest.files)) {
|
|
45
|
-
if (content) {
|
|
46
|
-
fs.writeFileSync(path.join(wsDir, filename), content, "utf-8");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
69
|
+
// Install skills first — this is the slowest step (5% → 90%)
|
|
50
70
|
let installed = 0;
|
|
51
71
|
let failed = 0;
|
|
52
72
|
let skipped = 0;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
const warnings = [];
|
|
74
|
+
const skillTotal = manifest.skills.length;
|
|
75
|
+
if (skillTotal > 0) {
|
|
76
|
+
log(`Installing ${skillTotal} skills via npx skills...`);
|
|
77
|
+
const skillWeight = WEIGHT.skillsEnd - WEIGHT.skillsStart;
|
|
78
|
+
const result = installAllSkills(manifest.skills, wsDir, json ? (evt) => {
|
|
79
|
+
const percent = WEIGHT.skillsStart + Math.round((evt.current / evt.total) * skillWeight);
|
|
80
|
+
jsonl({
|
|
81
|
+
event: "progress", phase: "skills", percent,
|
|
82
|
+
detail: evt.name, current: evt.current, total: evt.total, status: evt.status,
|
|
83
|
+
});
|
|
84
|
+
if (evt.status === "failed") {
|
|
85
|
+
warnings.push(`${evt.name}: ${evt.error ?? "install failed"}`);
|
|
86
|
+
}
|
|
87
|
+
} : undefined, json);
|
|
56
88
|
installed = result.installed;
|
|
57
89
|
failed = result.failed;
|
|
58
90
|
skipped = result.skipped;
|
|
59
91
|
}
|
|
92
|
+
else {
|
|
93
|
+
if (json)
|
|
94
|
+
jsonl({ event: "progress", phase: "skills", percent: WEIGHT.skillsEnd });
|
|
95
|
+
}
|
|
96
|
+
// Write agent files (90% → 95%)
|
|
97
|
+
log("Writing agent files...");
|
|
98
|
+
if (manifest.files) {
|
|
99
|
+
for (const [filename, content] of Object.entries(manifest.files)) {
|
|
100
|
+
if (content) {
|
|
101
|
+
fs.writeFileSync(path.join(wsDir, filename), content, "utf-8");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (json)
|
|
106
|
+
jsonl({ event: "progress", phase: "files", percent: WEIGHT.files });
|
|
107
|
+
// Update config (95% → 100%)
|
|
108
|
+
// Write workspace path instead of skills — the gateway discovers skills
|
|
109
|
+
// from <workspace>/skills/ symlinks that were created during skill install.
|
|
60
110
|
const updatedCfg = addOrUpdateAgent(cfg, {
|
|
61
111
|
id: manifest.id,
|
|
62
112
|
name: manifest.name,
|
|
63
|
-
|
|
113
|
+
workspace: wsDir,
|
|
64
114
|
});
|
|
65
115
|
writeConfig(updatedCfg);
|
|
66
116
|
markInstalled(manifest.id, manifest.version);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
parts
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
117
|
+
if (json)
|
|
118
|
+
jsonl({ event: "progress", phase: "config", percent: WEIGHT.config });
|
|
119
|
+
if (json) {
|
|
120
|
+
jsonl({ event: "done", success: true, installed, skipped, failed, warnings, workspace: wsDir });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const parts = [];
|
|
124
|
+
if (installed > 0)
|
|
125
|
+
parts.push(`${installed} installed`);
|
|
126
|
+
if (skipped > 0)
|
|
127
|
+
parts.push(`${skipped} already present`);
|
|
128
|
+
if (failed > 0)
|
|
129
|
+
parts.push(`${failed} failed`);
|
|
130
|
+
const skillSummary = parts.length > 0 ? ` (skills: ${parts.join(", ")})` : "";
|
|
131
|
+
console.log(`\n${manifest.emoji} Installed ${manifest.name}${skillSummary}.`);
|
|
132
|
+
}
|
|
77
133
|
}
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import readline from "node:readline";
|
|
4
3
|
import { readAuth, getRegistryBaseUrl } from "../lib/auth.js";
|
|
5
4
|
import { findAgentEntry, readConfig } from "../lib/config.js";
|
|
6
5
|
import { fetchRetry } from "../lib/fetch.js";
|
|
7
6
|
import { resolveWorkspaceDir } from "../lib/paths.js";
|
|
8
|
-
function prompt(question) {
|
|
9
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
rl.question(question, (answer) => {
|
|
12
|
-
rl.close();
|
|
13
|
-
resolve(answer.trim());
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
7
|
const MAX_FILE_SIZE = 200 * 1024;
|
|
18
8
|
const MAX_TOTAL_SIZE = 1024 * 1024;
|
|
19
9
|
function readAgentDir(dir) {
|
|
@@ -21,15 +11,19 @@ function readAgentDir(dir) {
|
|
|
21
11
|
console.error(`Directory not found: ${dir}`);
|
|
22
12
|
process.exit(1);
|
|
23
13
|
}
|
|
24
|
-
let manifest = {};
|
|
25
14
|
const manifestPath = path.join(dir, "manifest.json");
|
|
26
|
-
if (fs.existsSync(manifestPath)) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
15
|
+
if (!fs.existsSync(manifestPath)) {
|
|
16
|
+
console.error(`manifest.json not found in ${dir}`);
|
|
17
|
+
console.error("Create a manifest.json with at least: { \"id\": \"...\", \"name\": \"...\", \"category\": \"...\" }");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
let manifest;
|
|
21
|
+
try {
|
|
22
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.error("Failed to parse manifest.json.");
|
|
26
|
+
process.exit(1);
|
|
33
27
|
}
|
|
34
28
|
const files = {};
|
|
35
29
|
let totalSize = 0;
|
|
@@ -52,7 +46,7 @@ function readAgentDir(dir) {
|
|
|
52
46
|
}
|
|
53
47
|
return { manifest, files };
|
|
54
48
|
}
|
|
55
|
-
export async function agentPublish(
|
|
49
|
+
export async function agentPublish(opts = {}) {
|
|
56
50
|
const auth = readAuth();
|
|
57
51
|
if (!auth) {
|
|
58
52
|
console.error("Not logged in. Run \"talenthub login\" first.");
|
|
@@ -60,42 +54,45 @@ export async function agentPublish(name, opts = {}) {
|
|
|
60
54
|
}
|
|
61
55
|
let manifest;
|
|
62
56
|
let files;
|
|
63
|
-
let fallbackName = name;
|
|
64
|
-
let fallbackModel = "claude-sonnet-4-5";
|
|
65
|
-
let fallbackSkills = [];
|
|
66
57
|
if (opts.dir) {
|
|
67
58
|
const agentDir = path.resolve(opts.dir);
|
|
68
59
|
({ manifest, files } = readAgentDir(agentDir));
|
|
69
60
|
}
|
|
70
|
-
else {
|
|
61
|
+
else if (opts.name) {
|
|
71
62
|
const cfg = readConfig();
|
|
72
|
-
const entry = findAgentEntry(cfg, name);
|
|
63
|
+
const entry = findAgentEntry(cfg, opts.name);
|
|
73
64
|
if (!entry) {
|
|
74
|
-
console.error(`Agent "${name}" not found in openclaw config.`);
|
|
75
|
-
console.error("Use --dir to publish from an agent directory
|
|
65
|
+
console.error(`Agent "${opts.name}" not found in openclaw config.`);
|
|
66
|
+
console.error("Use --dir to publish from an agent directory.");
|
|
76
67
|
process.exit(1);
|
|
77
68
|
}
|
|
78
|
-
|
|
79
|
-
fallbackModel = entry.model || fallbackModel;
|
|
80
|
-
fallbackSkills = entry.skills || [];
|
|
81
|
-
const wsDir = resolveWorkspaceDir(name);
|
|
69
|
+
const wsDir = resolveWorkspaceDir(opts.name);
|
|
82
70
|
({ manifest, files } = readAgentDir(wsDir));
|
|
83
71
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
else {
|
|
73
|
+
// Default to current directory
|
|
74
|
+
;
|
|
75
|
+
({ manifest, files } = readAgentDir(path.resolve(".")));
|
|
76
|
+
}
|
|
77
|
+
const agentId = opts.id || manifest.id;
|
|
78
|
+
if (!agentId) {
|
|
79
|
+
console.error("No agent ID found. Set \"id\" in manifest.json or use --id.");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const agentName = manifest.name;
|
|
83
|
+
if (!agentName) {
|
|
84
|
+
console.error("No agent name found. Set \"name\" in manifest.json.");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
88
87
|
const payload = {
|
|
89
88
|
id: agentId,
|
|
90
|
-
|
|
91
|
-
name: manifest.name || fallbackName,
|
|
89
|
+
name: agentName,
|
|
92
90
|
emoji: manifest.emoji || "",
|
|
93
91
|
role: manifest.role || "",
|
|
94
92
|
tagline: manifest.tagline || "",
|
|
95
93
|
description: manifest.description || "",
|
|
96
94
|
category: manifest.category || "productivity",
|
|
97
|
-
|
|
98
|
-
skills: manifest.skills || fallbackSkills,
|
|
95
|
+
skills: manifest.skills || [],
|
|
99
96
|
identity_prompt: files["IDENTITY.md"] || "",
|
|
100
97
|
user_prompt: files["USER.md"] || "",
|
|
101
98
|
soul_prompt: files["SOUL.md"] || "",
|
|
@@ -103,11 +100,9 @@ export async function agentPublish(name, opts = {}) {
|
|
|
103
100
|
min_openclaw_version: manifest.minOpenClawVersion || null,
|
|
104
101
|
avatar_url: manifest.avatarUrl || null,
|
|
105
102
|
is_public: true,
|
|
106
|
-
|
|
107
|
-
? { i18n: manifest.i18n }
|
|
108
|
-
: {}),
|
|
103
|
+
i18n: manifest.i18n && typeof manifest.i18n === "object" ? manifest.i18n : {},
|
|
109
104
|
};
|
|
110
|
-
console.log(`\nPublishing ${payload.emoji || ""} ${payload.name}
|
|
105
|
+
console.log(`\nPublishing ${payload.emoji || ""} ${payload.name}...`);
|
|
111
106
|
const base = getRegistryBaseUrl();
|
|
112
107
|
const url = `${base}/api/talenthub/registry/publish`;
|
|
113
108
|
const res = await fetchRetry(url, {
|
|
@@ -43,7 +43,6 @@ export async function agentUninstall(name, options) {
|
|
|
43
43
|
console.log(`Removed agent "${name}"` +
|
|
44
44
|
(removedBindings > 0 ? ` (${removedBindings} binding(s) cleaned)` : "") +
|
|
45
45
|
".");
|
|
46
|
-
console.log("Restart the OpenClaw gateway to apply changes.");
|
|
47
46
|
}
|
|
48
47
|
function removeAgentFromConfig(cfg, agentId) {
|
|
49
48
|
const result = removeAgent(cfg, agentId);
|
|
@@ -6,9 +6,22 @@ import { fetchManifest } from "../lib/registry.js";
|
|
|
6
6
|
import { resolveWorkspaceDir } from "../lib/paths.js";
|
|
7
7
|
import { markInstalled, readState } from "../lib/state.js";
|
|
8
8
|
import { checkUpdates } from "../lib/update-check.js";
|
|
9
|
-
|
|
9
|
+
function jsonl(obj) {
|
|
10
|
+
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
11
|
+
}
|
|
12
|
+
const WEIGHT = { manifest: 10, skillsStart: 10, skillsEnd: 80, files: 90, config: 100 };
|
|
13
|
+
async function updateAgent(agentId, json) {
|
|
14
|
+
const log = json ? () => { } : console.log.bind(console);
|
|
10
15
|
const manifest = await fetchManifest(agentId);
|
|
11
16
|
const wsDir = resolveWorkspaceDir(agentId);
|
|
17
|
+
if (json)
|
|
18
|
+
jsonl({
|
|
19
|
+
event: "start",
|
|
20
|
+
agentId: manifest.id, name: manifest.name, emoji: manifest.emoji,
|
|
21
|
+
version: manifest.version, skillCount: manifest.skills.length,
|
|
22
|
+
});
|
|
23
|
+
if (json)
|
|
24
|
+
jsonl({ event: "progress", phase: "manifest", percent: WEIGHT.manifest });
|
|
12
25
|
// Backup workspace
|
|
13
26
|
const backupDir = `${wsDir}.bak`;
|
|
14
27
|
if (fs.existsSync(wsDir)) {
|
|
@@ -19,6 +32,7 @@ async function updateAgent(agentId) {
|
|
|
19
32
|
}
|
|
20
33
|
fs.mkdirSync(wsDir, { recursive: true });
|
|
21
34
|
// Write agent files from the registry response
|
|
35
|
+
log(" Writing agent files...");
|
|
22
36
|
if (manifest.files) {
|
|
23
37
|
for (const [filename, content] of Object.entries(manifest.files)) {
|
|
24
38
|
if (content) {
|
|
@@ -26,48 +40,89 @@ async function updateAgent(agentId) {
|
|
|
26
40
|
}
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
if (json)
|
|
44
|
+
jsonl({ event: "progress", phase: "files", percent: WEIGHT.files });
|
|
45
|
+
// Install any new skills (existing skills are skipped)
|
|
46
|
+
let installed = 0;
|
|
47
|
+
let failed = 0;
|
|
48
|
+
let skipped = 0;
|
|
49
|
+
const warnings = [];
|
|
50
|
+
const skillTotal = manifest.skills.length;
|
|
51
|
+
if (skillTotal > 0) {
|
|
52
|
+
log(" Installing new skills...");
|
|
53
|
+
const skillWeight = WEIGHT.skillsEnd - WEIGHT.skillsStart;
|
|
54
|
+
const result = installAllSkills(manifest.skills, wsDir, json ? (evt) => {
|
|
55
|
+
const percent = WEIGHT.skillsStart + Math.round((evt.current / evt.total) * skillWeight);
|
|
56
|
+
jsonl({
|
|
57
|
+
event: "progress", phase: "skills", percent,
|
|
58
|
+
detail: evt.name, current: evt.current, total: evt.total, status: evt.status,
|
|
59
|
+
});
|
|
60
|
+
if (evt.status === "failed") {
|
|
61
|
+
warnings.push(`${evt.name}: ${evt.error ?? "install failed"}`);
|
|
62
|
+
}
|
|
63
|
+
} : undefined, json);
|
|
64
|
+
installed = result.installed;
|
|
65
|
+
failed = result.failed;
|
|
66
|
+
skipped = result.skipped;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
if (json)
|
|
70
|
+
jsonl({ event: "progress", phase: "skills", percent: WEIGHT.skillsEnd });
|
|
32
71
|
}
|
|
33
72
|
// Update config
|
|
34
73
|
const cfg = readConfig();
|
|
35
74
|
const updatedCfg = addOrUpdateAgent(cfg, {
|
|
36
75
|
id: manifest.id,
|
|
37
76
|
name: manifest.name,
|
|
38
|
-
|
|
77
|
+
workspace: wsDir,
|
|
39
78
|
});
|
|
40
79
|
writeConfig(updatedCfg);
|
|
41
80
|
markInstalled(manifest.id, manifest.version);
|
|
81
|
+
if (json)
|
|
82
|
+
jsonl({ event: "progress", phase: "config", percent: WEIGHT.config });
|
|
83
|
+
if (json) {
|
|
84
|
+
jsonl({ event: "done", agentId, success: true, version: manifest.version, installed, skipped, failed, warnings });
|
|
85
|
+
}
|
|
42
86
|
return true;
|
|
43
87
|
}
|
|
44
88
|
export async function agentUpdate(name, options) {
|
|
89
|
+
const json = options?.json === true;
|
|
90
|
+
const log = json ? () => { } : console.log.bind(console);
|
|
45
91
|
if (options?.all || !name) {
|
|
46
92
|
const updates = await checkUpdates();
|
|
47
93
|
if (updates.length === 0) {
|
|
48
|
-
|
|
94
|
+
if (json)
|
|
95
|
+
jsonl({ event: "done", success: true, updated: 0 });
|
|
96
|
+
else
|
|
97
|
+
log("All agents are up to date.");
|
|
49
98
|
return;
|
|
50
99
|
}
|
|
51
100
|
// Update shared skills first
|
|
52
|
-
|
|
101
|
+
log("Updating shared skills...");
|
|
53
102
|
updateAllSkills();
|
|
54
|
-
|
|
103
|
+
log(`\nFound ${updates.length} update(s):\n`);
|
|
55
104
|
for (const u of updates) {
|
|
56
|
-
|
|
57
|
-
await updateAgent(u.agentId);
|
|
58
|
-
|
|
105
|
+
log(` Updating ${u.name}: ${u.currentVersion} → ${u.latestVersion}`);
|
|
106
|
+
await updateAgent(u.agentId, json);
|
|
107
|
+
log(` ✓ ${u.name} updated to ${u.latestVersion}`);
|
|
59
108
|
}
|
|
60
|
-
|
|
109
|
+
if (json)
|
|
110
|
+
jsonl({ event: "done", success: true, updated: updates.length });
|
|
111
|
+
else
|
|
112
|
+
log("\nAll done.");
|
|
61
113
|
return;
|
|
62
114
|
}
|
|
63
115
|
const state = readState();
|
|
64
116
|
if (!state.agents[name]) {
|
|
65
|
-
|
|
117
|
+
if (json)
|
|
118
|
+
jsonl({ event: "error", message: `Agent "${name}" is not installed` });
|
|
119
|
+
else
|
|
120
|
+
console.error(`Agent "${name}" is not installed. Use "talenthub agent install ${name}" first.`);
|
|
66
121
|
process.exit(1);
|
|
67
122
|
}
|
|
68
|
-
|
|
123
|
+
log("Updating shared skills...");
|
|
69
124
|
updateAllSkills();
|
|
70
|
-
|
|
71
|
-
await updateAgent(name);
|
|
72
|
-
|
|
125
|
+
log(`Updating agent "${name}"...`);
|
|
126
|
+
await updateAgent(name, json);
|
|
127
|
+
log(`✓ Agent "${name}" updated. All done.`);
|
|
73
128
|
}
|
package/dist/lib/skills.d.ts
CHANGED
|
@@ -37,7 +37,15 @@ export declare function installSkill(entry: string, workspaceDir: string): boole
|
|
|
37
37
|
* @param skills Array of skill URL strings ("https://github.com/owner/repo@skill")
|
|
38
38
|
* Returns { installed, skipped, failed } counts.
|
|
39
39
|
*/
|
|
40
|
-
export
|
|
40
|
+
export type SkillProgressCallback = (event: {
|
|
41
|
+
name: string;
|
|
42
|
+
repo: string;
|
|
43
|
+
status: "skipped" | "installing" | "done" | "failed";
|
|
44
|
+
current: number;
|
|
45
|
+
total: number;
|
|
46
|
+
error?: string;
|
|
47
|
+
}) => void;
|
|
48
|
+
export declare function installAllSkills(skills: string[], workspaceDir: string, onProgress?: SkillProgressCallback, quiet?: boolean): {
|
|
41
49
|
installed: number;
|
|
42
50
|
skipped: number;
|
|
43
51
|
failed: number;
|
package/dist/lib/skills.js
CHANGED
|
@@ -19,6 +19,18 @@ function resolveSkillsBin() {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
const SKILLS_BIN = resolveSkillsBin();
|
|
22
|
+
/**
|
|
23
|
+
* Replace `https://github.com/...` URLs with a mirror when TALENTHUB_GITHUB_URL is set.
|
|
24
|
+
* Example: TALENTHUB_GITHUB_URL=https://gitmirror.com
|
|
25
|
+
* https://github.com/owner/repo → https://gitmirror.com/owner/repo
|
|
26
|
+
*/
|
|
27
|
+
function applyGithubMirror(url) {
|
|
28
|
+
const mirror = process.env.TALENTHUB_GITHUB_URL?.trim();
|
|
29
|
+
if (!mirror)
|
|
30
|
+
return url;
|
|
31
|
+
const base = mirror.replace(/\/+$/, "");
|
|
32
|
+
return url.replace(/^https?:\/\/github\.com/i, base);
|
|
33
|
+
}
|
|
22
34
|
/**
|
|
23
35
|
* Parse a skill URL string into repo source and skill name.
|
|
24
36
|
* Accepts "https://github.com/owner/repo@skill" (preferred) and
|
|
@@ -67,16 +79,22 @@ export function isSkillInstalled(name) {
|
|
|
67
79
|
*
|
|
68
80
|
* Returns the list of skill names that were successfully installed.
|
|
69
81
|
*/
|
|
70
|
-
function installSkillsFromRepo(repo, skillNames) {
|
|
82
|
+
function installSkillsFromRepo(repo, skillNames, quiet = false) {
|
|
71
83
|
const skillFlag = skillNames.join(" ");
|
|
72
84
|
try {
|
|
73
|
-
const cmd = `node ${SKILLS_BIN} add ${repo} --skill ${skillFlag} --agent openclaw -y`;
|
|
74
|
-
execSync(cmd, { stdio: "inherit", cwd: resolveStateDir(), timeout: 300_000 });
|
|
85
|
+
const cmd = `node ${SKILLS_BIN} add ${applyGithubMirror(repo)} --skill ${skillFlag} --agent openclaw -y`;
|
|
86
|
+
execSync(cmd, { stdio: quiet ? "pipe" : "inherit", cwd: resolveStateDir(), timeout: 300_000 });
|
|
75
87
|
}
|
|
76
|
-
catch {
|
|
77
|
-
|
|
88
|
+
catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
// Extract stderr from ExecSyncError if available
|
|
91
|
+
const stderr = err?.stderr?.toString().trim();
|
|
92
|
+
const detail = stderr || msg;
|
|
93
|
+
if (!quiet)
|
|
94
|
+
console.error(` Warning: failed to install skills from ${repo}: ${skillNames.join(", ")}`);
|
|
95
|
+
return { ok: skillNames.filter((s) => isSkillInstalled(s)), error: detail };
|
|
78
96
|
}
|
|
79
|
-
return skillNames.filter((s) => isSkillInstalled(s));
|
|
97
|
+
return { ok: skillNames.filter((s) => isSkillInstalled(s)) };
|
|
80
98
|
}
|
|
81
99
|
/**
|
|
82
100
|
* Install a single skill via `skills add` into the shared directory,
|
|
@@ -93,8 +111,8 @@ export function installSkill(entry, workspaceDir) {
|
|
|
93
111
|
return false;
|
|
94
112
|
}
|
|
95
113
|
if (!isSkillInstalled(spec.skill)) {
|
|
96
|
-
const
|
|
97
|
-
if (ok.length === 0)
|
|
114
|
+
const result = installSkillsFromRepo(spec.repo, [spec.skill]);
|
|
115
|
+
if (result.ok.length === 0)
|
|
98
116
|
return false;
|
|
99
117
|
}
|
|
100
118
|
if (!isSkillInstalled(spec.skill)) {
|
|
@@ -126,30 +144,30 @@ function linkSkillToWorkspace(name, workspaceDir) {
|
|
|
126
144
|
}
|
|
127
145
|
fs.symlinkSync(target, link, "dir");
|
|
128
146
|
}
|
|
129
|
-
|
|
130
|
-
* Install all skills for an agent: skip already-installed, batch-install
|
|
131
|
-
* missing skills grouped by repo (one clone per repo), then symlink
|
|
132
|
-
* everything into the workspace.
|
|
133
|
-
*
|
|
134
|
-
* @param skills Array of skill URL strings ("https://github.com/owner/repo@skill")
|
|
135
|
-
* Returns { installed, skipped, failed } counts.
|
|
136
|
-
*/
|
|
137
|
-
export function installAllSkills(skills, workspaceDir) {
|
|
147
|
+
export function installAllSkills(skills, workspaceDir, onProgress, quiet = false) {
|
|
138
148
|
fs.mkdirSync(resolveStateDir(), { recursive: true });
|
|
139
149
|
let installed = 0;
|
|
140
150
|
let skipped = 0;
|
|
141
151
|
let failed = 0;
|
|
152
|
+
let current = 0;
|
|
153
|
+
const total = skills.length;
|
|
142
154
|
// Group missing skills by repo so each repo is cloned only once
|
|
143
155
|
const needInstall = new Map();
|
|
156
|
+
const skillRepoMap = new Map();
|
|
144
157
|
for (const entry of skills) {
|
|
145
158
|
const spec = parseSkillSpec(entry);
|
|
146
159
|
if (!spec) {
|
|
160
|
+
current++;
|
|
147
161
|
console.error(` Warning: invalid skill spec "${entry}" — expected "https://github.com/owner/repo@skill"`);
|
|
162
|
+
onProgress?.({ name: entry, repo: "", status: "failed", current, total, error: "invalid spec" });
|
|
148
163
|
failed++;
|
|
149
164
|
continue;
|
|
150
165
|
}
|
|
166
|
+
skillRepoMap.set(spec.skill, spec.repo);
|
|
151
167
|
if (isSkillInstalled(spec.skill)) {
|
|
168
|
+
current++;
|
|
152
169
|
linkSkillToWorkspace(spec.skill, workspaceDir);
|
|
170
|
+
onProgress?.({ name: spec.skill, repo: spec.repo, status: "skipped", current, total });
|
|
153
171
|
skipped++;
|
|
154
172
|
}
|
|
155
173
|
else {
|
|
@@ -160,11 +178,25 @@ export function installAllSkills(skills, workspaceDir) {
|
|
|
160
178
|
}
|
|
161
179
|
// Batch install: one `skills add` per repo
|
|
162
180
|
for (const [repo, skillNames] of needInstall) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
for (const name of skillNames) {
|
|
182
|
+
current++;
|
|
183
|
+
onProgress?.({ name, repo, status: "installing", current, total });
|
|
184
|
+
}
|
|
185
|
+
// Rewind current so we can report done/failed per skill
|
|
186
|
+
current -= skillNames.length;
|
|
187
|
+
const result = installSkillsFromRepo(repo, skillNames, quiet);
|
|
188
|
+
const okSet = new Set(result.ok);
|
|
189
|
+
for (const name of skillNames) {
|
|
190
|
+
current++;
|
|
191
|
+
if (okSet.has(name)) {
|
|
192
|
+
installed++;
|
|
193
|
+
linkSkillToWorkspace(name, workspaceDir);
|
|
194
|
+
onProgress?.({ name, repo, status: "done", current, total });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
failed++;
|
|
198
|
+
onProgress?.({ name, repo, status: "failed", current, total, error: result.error ?? "install failed" });
|
|
199
|
+
}
|
|
168
200
|
}
|
|
169
201
|
}
|
|
170
202
|
return { installed, skipped, failed };
|
package/dist/lib/update-check.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchCatalog } from "./registry.js";
|
|
2
2
|
import { readState, writeState } from "./state.js";
|
|
3
|
+
import { isOlderVersion } from "./version.js";
|
|
3
4
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
4
5
|
export async function getCatalogCached() {
|
|
5
6
|
const state = readState();
|
|
@@ -21,7 +22,7 @@ export async function checkUpdates() {
|
|
|
21
22
|
const remote = catalog.agents[agentId];
|
|
22
23
|
if (!remote)
|
|
23
24
|
continue;
|
|
24
|
-
if (
|
|
25
|
+
if (isOlderVersion(installed.version, remote.version)) {
|
|
25
26
|
updates.push({
|
|
26
27
|
agentId,
|
|
27
28
|
currentVersion: installed.version,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent version utilities.
|
|
3
|
+
*
|
|
4
|
+
* Format: YYYY.MM.DD-X
|
|
5
|
+
* - YYYY: 4-digit year
|
|
6
|
+
* - MM: 2-digit zero-padded month (01-12)
|
|
7
|
+
* - DD: 2-digit zero-padded day (01-31)
|
|
8
|
+
* - X: positive integer counter (1, 2, 3, ...)
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Compare two version strings.
|
|
12
|
+
* Returns -1 if a < b, 0 if a === b, 1 if a > b.
|
|
13
|
+
*
|
|
14
|
+
* Falls back to string comparison if either version doesn't match the
|
|
15
|
+
* YYYY.MM.DD-X format (for backwards compatibility with old versions).
|
|
16
|
+
*/
|
|
17
|
+
export declare function compareVersions(a: string, b: string): -1 | 0 | 1;
|
|
18
|
+
/**
|
|
19
|
+
* Check if version `a` is older than version `b`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isOlderVersion(a: string, b: string): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent version utilities.
|
|
3
|
+
*
|
|
4
|
+
* Format: YYYY.MM.DD-X
|
|
5
|
+
* - YYYY: 4-digit year
|
|
6
|
+
* - MM: 2-digit zero-padded month (01-12)
|
|
7
|
+
* - DD: 2-digit zero-padded day (01-31)
|
|
8
|
+
* - X: positive integer counter (1, 2, 3, ...)
|
|
9
|
+
*/
|
|
10
|
+
const VERSION_RE = /^(\d{4})\.(\d{2})\.(\d{2})-(\d+)$/;
|
|
11
|
+
function parseVersion(v) {
|
|
12
|
+
const m = VERSION_RE.exec(v);
|
|
13
|
+
if (!m)
|
|
14
|
+
return null;
|
|
15
|
+
const month = Number(m[2]);
|
|
16
|
+
const day = Number(m[3]);
|
|
17
|
+
const counter = Number(m[4]);
|
|
18
|
+
if (month < 1 || month > 12 || day < 1 || day > 31 || counter < 1)
|
|
19
|
+
return null;
|
|
20
|
+
return { year: Number(m[1]), month, day, counter };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compare two version strings.
|
|
24
|
+
* Returns -1 if a < b, 0 if a === b, 1 if a > b.
|
|
25
|
+
*
|
|
26
|
+
* Falls back to string comparison if either version doesn't match the
|
|
27
|
+
* YYYY.MM.DD-X format (for backwards compatibility with old versions).
|
|
28
|
+
*/
|
|
29
|
+
export function compareVersions(a, b) {
|
|
30
|
+
const pa = parseVersion(a);
|
|
31
|
+
const pb = parseVersion(b);
|
|
32
|
+
// If both parse, compare structurally
|
|
33
|
+
if (pa && pb) {
|
|
34
|
+
const fields = ["year", "month", "day", "counter"];
|
|
35
|
+
for (const field of fields) {
|
|
36
|
+
if (pa[field] < pb[field])
|
|
37
|
+
return -1;
|
|
38
|
+
if (pa[field] > pb[field])
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
// Fallback: string comparison (handles old "2026.3.16" style versions)
|
|
44
|
+
if (a === b)
|
|
45
|
+
return 0;
|
|
46
|
+
return a < b ? -1 : 1;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if version `a` is older than version `b`.
|
|
50
|
+
*/
|
|
51
|
+
export function isOlderVersion(a, b) {
|
|
52
|
+
return compareVersions(a, b) === -1;
|
|
53
|
+
}
|