@storyclaw/talenthub 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -35,6 +35,7 @@ agent
35
35
  .description("Install an agent and its skills")
36
36
  .option("-f, --force", "Overwrite existing agent", false)
37
37
  .option("-t, --token <token>", "Authenticate with a th_* token for private agents")
38
+ .option("--json", "Output structured JSONL progress for machine consumption", false)
38
39
  .action(agentInstall);
39
40
  agent
40
41
  .command("update [name]")
@@ -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
  }
@@ -31,11 +31,13 @@ async function updateAgent(agentId) {
31
31
  installAllSkills(manifest.skills, wsDir);
32
32
  }
33
33
  // Update config
34
+ // Write workspace path instead of skills — the gateway discovers skills
35
+ // from <workspace>/skills/ symlinks that were created during skill install.
34
36
  const cfg = readConfig();
35
37
  const updatedCfg = addOrUpdateAgent(cfg, {
36
38
  id: manifest.id,
37
39
  name: manifest.name,
38
- skills: manifest.skills,
40
+ workspace: wsDir,
39
41
  });
40
42
  writeConfig(updatedCfg);
41
43
  markInstalled(manifest.id, manifest.version);
@@ -57,7 +59,7 @@ export async function agentUpdate(name, options) {
57
59
  await updateAgent(u.agentId);
58
60
  console.log(` ✓ ${u.name} updated to ${u.latestVersion}`);
59
61
  }
60
- console.log("\nRestart the OpenClaw gateway to apply changes.");
62
+ console.log("\nAll done.");
61
63
  return;
62
64
  }
63
65
  const state = readState();
@@ -69,5 +71,5 @@ export async function agentUpdate(name, options) {
69
71
  updateAllSkills();
70
72
  console.log(`Updating agent "${name}"...`);
71
73
  await updateAgent(name);
72
- console.log(`✓ Agent "${name}" updated. Restart the OpenClaw gateway to apply changes.`);
74
+ console.log(`✓ Agent "${name}" updated. All done.`);
73
75
  }
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyclaw/talenthub",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "CLI tool to manage StoryClaw AI agents",
5
5
  "type": "module",
6
6
  "bin": {