@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 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 <name>")
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,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,4 +1,5 @@
1
1
  export declare function agentInstall(name: string, options: {
2
2
  force?: boolean;
3
3
  token?: string;
4
+ json?: boolean;
4
5
  }): Promise<void>;
@@ -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
- console.error("Invalid token format. Token must start with 'th_'.");
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
- console.log("Verifying token...");
26
+ log("Verifying token...");
17
27
  try {
18
28
  await verifyToken(token);
19
29
  }
20
30
  catch {
21
- console.error("Token verification failed. Please check your token and try again.");
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
- console.log(`Looking up agent "${name}"...`);
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
- console.error(`Agent "${name}" not found. Available: ${available}`);
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
- console.log(`Found ${manifest.emoji} ${manifest.name} v${manifest.version} (${manifest.skills.length} skills)`);
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
- console.error(`Agent "${name}" already exists in config. Use --force to overwrite.`);
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
- console.log("Writing agent files...");
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
- if (manifest.skills.length > 0) {
54
- console.log(`Installing ${manifest.skills.length} skills via npx skills...`);
55
- const result = installAllSkills(manifest.skills, wsDir);
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
- skills: manifest.skills,
113
+ workspace: wsDir,
64
114
  });
65
115
  writeConfig(updatedCfg);
66
116
  markInstalled(manifest.id, manifest.version);
67
- const parts = [];
68
- if (installed > 0)
69
- parts.push(`${installed} installed`);
70
- if (skipped > 0)
71
- parts.push(`${skipped} already present`);
72
- if (failed > 0)
73
- parts.push(`${failed} failed`);
74
- const skillSummary = parts.length > 0 ? ` (skills: ${parts.join(", ")})` : "";
75
- console.log(`\n${manifest.emoji} Installed ${manifest.name}${skillSummary}.`);
76
- console.log("Restart the OpenClaw gateway to apply changes.");
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,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,48 +40,89 @@ 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
73
  const cfg = readConfig();
35
74
  const updatedCfg = addOrUpdateAgent(cfg, {
36
75
  id: manifest.id,
37
76
  name: manifest.name,
38
- skills: manifest.skills,
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
- 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.");
49
98
  return;
50
99
  }
51
100
  // Update shared skills first
52
- console.log("Updating shared skills...");
101
+ log("Updating shared skills...");
53
102
  updateAllSkills();
54
- console.log(`\nFound ${updates.length} update(s):\n`);
103
+ log(`\nFound ${updates.length} update(s):\n`);
55
104
  for (const u of updates) {
56
- console.log(` Updating ${u.name}: ${u.currentVersion} → ${u.latestVersion}`);
57
- await updateAgent(u.agentId);
58
- 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}`);
59
108
  }
60
- console.log("\nRestart the OpenClaw gateway to apply changes.");
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
- 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.`);
66
121
  process.exit(1);
67
122
  }
68
- console.log("Updating shared skills...");
123
+ log("Updating shared skills...");
69
124
  updateAllSkills();
70
- console.log(`Updating agent "${name}"...`);
71
- await updateAgent(name);
72
- console.log(`✓ Agent "${name}" updated. Restart the OpenClaw gateway to apply changes.`);
125
+ log(`Updating agent "${name}"...`);
126
+ await updateAgent(name, json);
127
+ log(`✓ Agent "${name}" updated. All done.`);
73
128
  }
@@ -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 declare function installAllSkills(skills: string[], workspaceDir: string): {
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;
@@ -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
- console.error(` Warning: failed to install skills from ${repo}: ${skillNames.join(", ")}`);
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 ok = installSkillsFromRepo(spec.repo, [spec.skill]);
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 ok = installSkillsFromRepo(repo, skillNames);
164
- installed += ok.length;
165
- failed += skillNames.length - ok.length;
166
- for (const name of ok) {
167
- linkSkillToWorkspace(name, workspaceDir);
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 };
@@ -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.6",
3
+ "version": "0.3.8",
4
4
  "description": "CLI tool to manage StoryClaw AI agents",
5
5
  "type": "module",
6
6
  "bin": {