@storyclaw/talenthub 0.3.7 → 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 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,6 +31,11 @@ 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")
@@ -41,6 +47,7 @@ agent
41
47
  .command("update [name]")
42
48
  .description("Update an agent or all agents")
43
49
  .option("-a, --all", "Update all installed agents")
50
+ .option("--json", "Output structured JSONL progress for machine consumption", false)
44
51
  .action(agentUpdate);
45
52
  agent
46
53
  .command("uninstall <name>")
@@ -56,9 +63,11 @@ agent
56
63
  .description("Browse available agents")
57
64
  .action(agentSearch);
58
65
  agent
59
- .command("publish <name>")
66
+ .command("publish")
60
67
  .description("Publish a local agent to the registry")
61
- .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")
62
71
  .action(agentPublish);
63
72
  agent
64
73
  .command("unpublish <name>")
@@ -0,0 +1,3 @@
1
+ export declare function agentInit(opts?: {
2
+ dir?: string;
3
+ }): Promise<void>;
@@ -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
+ }
@@ -1,3 +1,5 @@
1
- export declare function agentPublish(name: string, opts?: {
1
+ export declare function agentPublish(opts?: {
2
2
  dir?: string;
3
+ name?: string;
4
+ id?: string;
3
5
  }): Promise<void>;
@@ -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
- try {
28
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
29
- }
30
- catch {
31
- console.warn("Warning: could not parse manifest.json.");
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(name, opts = {}) {
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, or install the agent first.");
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
- fallbackName = entry.name || name;
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
- const agentId = manifest.id || name;
85
- const currentVersion = manifest.version || "0.1.0";
86
- const version = await prompt(`Version [${currentVersion}]: `);
87
- const finalVersion = version || currentVersion;
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
- version: finalVersion,
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
- model: manifest.model || fallbackModel,
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
- ...(manifest.i18n && typeof manifest.i18n === "object"
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} v${finalVersion}...`);
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);
@@ -1,3 +1,4 @@
1
1
  export declare function agentUpdate(name?: string, options?: {
2
2
  all?: boolean;
3
+ json?: boolean;
3
4
  }): Promise<void>;
@@ -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
- async function updateAgent(agentId) {
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,13 +40,36 @@ async function updateAgent(agentId) {
26
40
  }
27
41
  }
28
42
  }
29
- // Install any new skills and re-sync symlinks for all
30
- if (manifest.skills.length > 0) {
31
- installAllSkills(manifest.skills, wsDir);
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
- // Write workspace path instead of skills — the gateway discovers skills
35
- // from <workspace>/skills/ symlinks that were created during skill install.
36
73
  const cfg = readConfig();
37
74
  const updatedCfg = addOrUpdateAgent(cfg, {
38
75
  id: manifest.id,
@@ -41,35 +78,51 @@ async function updateAgent(agentId) {
41
78
  });
42
79
  writeConfig(updatedCfg);
43
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
+ }
44
86
  return true;
45
87
  }
46
88
  export async function agentUpdate(name, options) {
89
+ const json = options?.json === true;
90
+ const log = json ? () => { } : console.log.bind(console);
47
91
  if (options?.all || !name) {
48
92
  const updates = await checkUpdates();
49
93
  if (updates.length === 0) {
50
- console.log("All agents are up to date.");
94
+ if (json)
95
+ jsonl({ event: "done", success: true, updated: 0 });
96
+ else
97
+ log("All agents are up to date.");
51
98
  return;
52
99
  }
53
100
  // Update shared skills first
54
- console.log("Updating shared skills...");
101
+ log("Updating shared skills...");
55
102
  updateAllSkills();
56
- console.log(`\nFound ${updates.length} update(s):\n`);
103
+ log(`\nFound ${updates.length} update(s):\n`);
57
104
  for (const u of updates) {
58
- console.log(` Updating ${u.name}: ${u.currentVersion} → ${u.latestVersion}`);
59
- await updateAgent(u.agentId);
60
- console.log(` ✓ ${u.name} updated to ${u.latestVersion}`);
105
+ log(` Updating ${u.name}: ${u.currentVersion} → ${u.latestVersion}`);
106
+ await updateAgent(u.agentId, json);
107
+ log(` ✓ ${u.name} updated to ${u.latestVersion}`);
61
108
  }
62
- console.log("\nAll done.");
109
+ if (json)
110
+ jsonl({ event: "done", success: true, updated: updates.length });
111
+ else
112
+ log("\nAll done.");
63
113
  return;
64
114
  }
65
115
  const state = readState();
66
116
  if (!state.agents[name]) {
67
- console.error(`Agent "${name}" is not installed. Use "talenthub agent install ${name}" first.`);
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.`);
68
121
  process.exit(1);
69
122
  }
70
- console.log("Updating shared skills...");
123
+ log("Updating shared skills...");
71
124
  updateAllSkills();
72
- console.log(`Updating agent "${name}"...`);
73
- await updateAgent(name);
74
- console.log(`✓ Agent "${name}" updated. All done.`);
125
+ log(`Updating agent "${name}"...`);
126
+ await updateAgent(name, json);
127
+ log(`✓ Agent "${name}" updated. All done.`);
75
128
  }
@@ -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 (remote.version !== installed.version) {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyclaw/talenthub",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "CLI tool to manage StoryClaw AI agents",
5
5
  "type": "module",
6
6
  "bin": {