@storyclaw/talenthub 0.3.1 → 0.3.3

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
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import dns from "node:dns";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  // IPv6 is unreachable on many networks (especially behind NAT/China);
4
7
  // try IPv4 first to avoid EHOSTUNREACH delays on every fetch.
5
8
  dns.setDefaultResultOrder("ipv4first");
@@ -13,11 +16,13 @@ import { agentUnpublish } from "./commands/agent-unpublish.js";
13
16
  import { agentUpdate } from "./commands/agent-update.js";
14
17
  import { login } from "./commands/login.js";
15
18
  import { logout } from "./commands/logout.js";
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf-8"));
16
21
  const program = new Command();
17
22
  program
18
23
  .name("talenthub")
19
24
  .description("Manage StoryClaw AI agents")
20
- .version("0.1.0");
25
+ .version(pkg.version);
21
26
  program.command("login").description("Authenticate with StoryClaw").action(login);
22
27
  program.command("logout").description("Remove stored credentials").action(logout);
23
28
  const agent = program.command("agent").description("Agent management commands");
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { isClawhubAvailable, installSkill } from "../lib/clawhub.js";
3
+ import { installAllSkills } from "../lib/skills.js";
4
4
  import { addOrUpdateAgent, findAgentEntry, readConfig, writeConfig } from "../lib/config.js";
5
5
  import { fetchCatalog, fetchManifest } from "../lib/registry.js";
6
6
  import { resolveWorkspaceDir } from "../lib/paths.js";
@@ -21,7 +21,6 @@ export async function agentInstall(name, options) {
21
21
  console.error(`Agent "${name}" already exists in config. Use --force to overwrite.`);
22
22
  process.exit(1);
23
23
  }
24
- const hasClawhub = isClawhubAvailable();
25
24
  const wsDir = resolveWorkspaceDir(name);
26
25
  fs.mkdirSync(wsDir, { recursive: true });
27
26
  console.log("Writing agent files...");
@@ -34,34 +33,29 @@ export async function agentInstall(name, options) {
34
33
  }
35
34
  let installed = 0;
36
35
  let failed = 0;
37
- if (hasClawhub && manifest.skills.length > 0) {
38
- console.log(`Installing ${manifest.skills.length} skills via clawhub...`);
39
- for (const skill of manifest.skills) {
40
- const ok = installSkill(skill, wsDir);
41
- if (ok)
42
- installed++;
43
- else
44
- failed++;
45
- }
46
- }
47
- else if (!hasClawhub && manifest.skills.length > 0) {
48
- console.log(`Skipping ${manifest.skills.length} skills (clawhub not installed).`);
49
- console.log("Install clawhub later: npm i -g clawhub");
50
- console.log(`Then run: talenthub agent update ${name}`);
36
+ let skipped = 0;
37
+ if (manifest.skills.length > 0) {
38
+ console.log(`Installing ${manifest.skills.length} skills via npx skills...`);
39
+ const result = installAllSkills(manifest.skills, wsDir);
40
+ installed = result.installed;
41
+ failed = result.failed;
42
+ skipped = result.skipped;
51
43
  }
52
- let updatedCfg = addOrUpdateAgent(cfg, {
44
+ const updatedCfg = addOrUpdateAgent(cfg, {
53
45
  id: manifest.id,
54
46
  name: manifest.name,
55
47
  skills: manifest.skills,
56
48
  });
57
49
  writeConfig(updatedCfg);
58
50
  markInstalled(manifest.id, manifest.version);
59
- console.log(`\n${manifest.emoji} Installed ${manifest.name}` +
60
- (installed > 0 ? ` with ${installed} skills` : "") +
61
- (failed > 0 ? ` (${failed} failed)` : "") +
62
- (!hasClawhub && manifest.skills.length > 0
63
- ? " (skills pending — install clawhub)"
64
- : "") +
65
- ".");
51
+ const parts = [];
52
+ if (installed > 0)
53
+ parts.push(`${installed} installed`);
54
+ if (skipped > 0)
55
+ parts.push(`${skipped} already present`);
56
+ if (failed > 0)
57
+ parts.push(`${failed} failed`);
58
+ const skillSummary = parts.length > 0 ? ` (skills: ${parts.join(", ")})` : "";
59
+ console.log(`\n${manifest.emoji} Installed ${manifest.name}${skillSummary}.`);
66
60
  console.log("Restart the OpenClaw gateway to apply changes.");
67
61
  }
@@ -102,6 +102,7 @@ export async function agentPublish(name, opts = {}) {
102
102
  agents_prompt: files["AGENTS.md"] || "",
103
103
  min_openclaw_version: manifest.minOpenClawVersion || null,
104
104
  avatar_url: manifest.avatarUrl || null,
105
+ is_public: true,
105
106
  };
106
107
  console.log(`\nPublishing ${payload.emoji || ""} ${payload.name} v${finalVersion}...`);
107
108
  const base = getRegistryBaseUrl();
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { ensureClawhub, installSkill, updateAllSkills } from "../lib/clawhub.js";
3
+ import { installAllSkills, updateAllSkills } from "../lib/skills.js";
4
4
  import { addOrUpdateAgent, readConfig, writeConfig } from "../lib/config.js";
5
5
  import { fetchManifest } from "../lib/registry.js";
6
6
  import { resolveWorkspaceDir } from "../lib/paths.js";
@@ -15,7 +15,7 @@ async function updateAgent(agentId) {
15
15
  if (fs.existsSync(backupDir)) {
16
16
  fs.rmSync(backupDir, { recursive: true, force: true });
17
17
  }
18
- fs.cpSync(wsDir, backupDir, { recursive: true });
18
+ fs.cpSync(wsDir, backupDir, { recursive: true, dereference: true });
19
19
  }
20
20
  fs.mkdirSync(wsDir, { recursive: true });
21
21
  // Write agent files from the registry response
@@ -26,30 +26,10 @@ async function updateAgent(agentId) {
26
26
  }
27
27
  }
28
28
  }
29
- // Determine new skills to install
30
- const lockPath = path.join(wsDir, ".clawhub", "lock.json");
31
- const existingSkills = new Set();
32
- if (fs.existsSync(lockPath)) {
33
- try {
34
- const lock = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
35
- for (const entry of lock.skills ?? []) {
36
- if (entry.slug)
37
- existingSkills.add(entry.slug);
38
- }
39
- }
40
- catch {
41
- // Ignore parse errors
42
- }
43
- }
44
- const newSkills = manifest.skills.filter((s) => !existingSkills.has(s));
45
- if (newSkills.length > 0) {
46
- ensureClawhub();
47
- for (const skill of newSkills) {
48
- installSkill(skill, wsDir);
49
- }
29
+ // Install any new skills and re-sync symlinks for all
30
+ if (manifest.skills.length > 0) {
31
+ installAllSkills(manifest.skills, wsDir);
50
32
  }
51
- // Update all existing skills
52
- updateAllSkills(wsDir);
53
33
  // Update config
54
34
  const cfg = readConfig();
55
35
  const updatedCfg = addOrUpdateAgent(cfg, {
@@ -68,8 +48,10 @@ export async function agentUpdate(name, options) {
68
48
  console.log("All agents are up to date.");
69
49
  return;
70
50
  }
71
- ensureClawhub();
72
- console.log(`Found ${updates.length} update(s):\n`);
51
+ // Update shared skills first
52
+ console.log("Updating shared skills...");
53
+ updateAllSkills();
54
+ console.log(`\nFound ${updates.length} update(s):\n`);
73
55
  for (const u of updates) {
74
56
  console.log(` Updating ${u.name}: ${u.currentVersion} → ${u.latestVersion}`);
75
57
  await updateAgent(u.agentId);
@@ -83,7 +65,8 @@ export async function agentUpdate(name, options) {
83
65
  console.error(`Agent "${name}" is not installed. Use "talenthub agent install ${name}" first.`);
84
66
  process.exit(1);
85
67
  }
86
- ensureClawhub();
68
+ console.log("Updating shared skills...");
69
+ updateAllSkills();
87
70
  console.log(`Updating agent "${name}"...`);
88
71
  await updateAgent(name);
89
72
  console.log(`✓ Agent "${name}" updated. Restart the OpenClaw gateway to apply changes.`);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Mapping of skill slugs (as used in agent manifests) to their GitHub source
3
+ * repositories, verified via `npx skills find <slug>`.
4
+ *
5
+ * Used by: `node skills add <repo> --skill <skill> --agent openclaw -y`
6
+ *
7
+ * When the skill name in the repo differs from the manifest slug, the `skill`
8
+ * field provides the repo-side name.
9
+ */
10
+ export type SkillSource = {
11
+ repo: string;
12
+ /** Override when the skill name in the repo differs from the manifest slug. */
13
+ skill?: string;
14
+ };
15
+ /**
16
+ * Canonical source map.
17
+ * Key = slug used in agent manifests.
18
+ * Value = { repo, skill? } for `skills add <repo> --skill <skill|key>`.
19
+ *
20
+ * Sources picked by highest install count from `npx skills find`,
21
+ * except StoryClaw-owned skills which always come from the talenthub repo.
22
+ */
23
+ export declare const SKILL_SOURCES: Record<string, SkillSource>;
24
+ export declare function getSkillSource(slug: string): SkillSource | undefined;
25
+ export declare function getInstallArgs(slug: string): {
26
+ repo: string;
27
+ skill: string;
28
+ } | undefined;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Mapping of skill slugs (as used in agent manifests) to their GitHub source
3
+ * repositories, verified via `npx skills find <slug>`.
4
+ *
5
+ * Used by: `node skills add <repo> --skill <skill> --agent openclaw -y`
6
+ *
7
+ * When the skill name in the repo differs from the manifest slug, the `skill`
8
+ * field provides the repo-side name.
9
+ */
10
+ const TALENTHUB = "storyclaw-official/talenthub";
11
+ /**
12
+ * Canonical source map.
13
+ * Key = slug used in agent manifests.
14
+ * Value = { repo, skill? } for `skills add <repo> --skill <skill|key>`.
15
+ *
16
+ * Sources picked by highest install count from `npx skills find`,
17
+ * except StoryClaw-owned skills which always come from the talenthub repo.
18
+ */
19
+ export const SKILL_SOURCES = {
20
+ // ── storyclaw-official/talenthub (our own skills) ──────────────────────
21
+ "storyclaw-x2c-publish": { repo: TALENTHUB },
22
+ "storyclaw-x-manager": { repo: TALENTHUB },
23
+ "giggle-files-management": { repo: TALENTHUB },
24
+ "storyclaw-alpaca-trading": { repo: TALENTHUB },
25
+ "storyclaw-polymarket-trading": { repo: TALENTHUB },
26
+ "storyclaw-trade-executor": { repo: TALENTHUB },
27
+ "giggle-market-monitor": { repo: TALENTHUB },
28
+ // ── browsing & search ──────────────────────────────────────────────────
29
+ // browser-use/browser-use@browser-use 49.6K installs
30
+ "browser-use": { repo: "browser-use/browser-use" },
31
+ // inferen-sh/skills@web-search 5.3K installs
32
+ "web-search": { repo: "inferen-sh/skills" },
33
+ // jamditis/claude-skills-journalism@web-scraping 1.5K installs
34
+ "web-scraping": { repo: "jamditis/claude-skills-journalism" },
35
+ // ── tavily suite (tavily-ai/skills uses short names) ───────────────────
36
+ // tavily-ai/skills@search 11.4K installs
37
+ "tavily-search": { repo: "tavily-ai/skills", skill: "search" },
38
+ // tavily-ai/skills@research 6.4K installs
39
+ "tavily-research": { repo: "tavily-ai/skills", skill: "research" },
40
+ // tavily-ai/skills@extract 4.7K installs
41
+ "tavily-extract": { repo: "tavily-ai/skills", skill: "extract" },
42
+ // tavily-ai/skills@crawl 3.4K installs
43
+ "tavily-crawl": { repo: "tavily-ai/skills", skill: "crawl" },
44
+ // ── documents (anthropics/skills) ──────────────────────────────────────
45
+ // anthropics/skills@pdf 38.8K installs
46
+ pdf: { repo: "anthropics/skills" },
47
+ // anthropics/skills@docx 30.6K installs
48
+ docx: { repo: "anthropics/skills" },
49
+ // anthropics/skills@xlsx 28.2K installs
50
+ xlsx: { repo: "anthropics/skills" },
51
+ // anthropics/skills@pptx 34.6K installs
52
+ pptx: { repo: "anthropics/skills" },
53
+ // ── design & creative (anthropics/skills) ──────────────────────────────
54
+ // anthropics/skills@frontend-design 159.8K installs
55
+ "frontend-design": { repo: "anthropics/skills" },
56
+ // anthropics/skills@canvas-design 19K installs
57
+ "canvas-design": { repo: "anthropics/skills" },
58
+ // anthropics/skills@skill-creator 83.9K installs
59
+ "skill-creator": { repo: "anthropics/skills" },
60
+ // ── media ──────────────────────────────────────────────────────────────
61
+ // remotion-dev/skills@remotion-best-practices 147.6K installs
62
+ remotion: { repo: "remotion-dev/skills", skill: "remotion-best-practices" },
63
+ // steipete/clawdis@video-frames 286 installs
64
+ "video-frames": { repo: "steipete/clawdis" },
65
+ // steipete/clawdis@openai-image-gen 198 installs
66
+ "openai-image-gen": { repo: "steipete/clawdis" },
67
+ // inferen-sh/skills@text-to-speech 2.5K installs
68
+ speech: { repo: "inferen-sh/skills", skill: "text-to-speech" },
69
+ // elevenlabs/skills@speech-to-text 1.6K installs
70
+ "speech-to-text": { repo: "elevenlabs/skills" },
71
+ // github/awesome-copilot@nano-banana-pro-openrouter 7.3K installs
72
+ "nano-banana-pro": { repo: "github/awesome-copilot", skill: "nano-banana-pro-openrouter" },
73
+ // ── productivity ───────────────────────────────────────────────────────
74
+ // steipete/clawdis@summarize 3.6K installs
75
+ summarize: { repo: "steipete/clawdis" },
76
+ // steipete/clawdis@weather 1.6K installs
77
+ weather: { repo: "steipete/clawdis" },
78
+ // steipete/clawdis@session-logs 322 installs
79
+ "session-logs": { repo: "steipete/clawdis" },
80
+ // boomsystel-code/openclaw-workspace@imap-smtp-email 432 installs
81
+ "imap-smtp-email": { repo: "boomsystel-code/openclaw-workspace" },
82
+ // netease-youdao/lobsterai@scheduled-task 34 installs
83
+ "scheduled-task": { repo: "netease-youdao/lobsterai" },
84
+ // netease-youdao/lobsterai@local-tools 27 installs
85
+ "local-tools": { repo: "netease-youdao/lobsterai" },
86
+ // ── agent system ───────────────────────────────────────────────────────
87
+ // cantinaxyz/clawdstrike@clawdstrike 351 installs
88
+ clawdstrike: { repo: "cantinaxyz/clawdstrike" },
89
+ // vercel-labs/skills@find-skills 562.5K installs
90
+ "find-skills": { repo: "vercel-labs/skills" },
91
+ // ── third-party agent tools ────────────────────────────────────────────
92
+ // panniantong/agent-reach@agent-reach 1.9K installs
93
+ "agent-reach": { repo: "panniantong/agent-reach" },
94
+ // netease-youdao/lobsterai@films-search 41 installs
95
+ "films-search": { repo: "netease-youdao/lobsterai" },
96
+ // netease-youdao/lobsterai@music-search 36 installs
97
+ "music-search": { repo: "netease-youdao/lobsterai" },
98
+ };
99
+ export function getSkillSource(slug) {
100
+ return SKILL_SOURCES[slug];
101
+ }
102
+ export function getInstallArgs(slug) {
103
+ const src = SKILL_SOURCES[slug];
104
+ if (!src)
105
+ return undefined;
106
+ return { repo: src.repo, skill: src.skill ?? slug };
107
+ }
@@ -0,0 +1,53 @@
1
+ export type SkillSpec = {
2
+ repo: string;
3
+ skill: string;
4
+ };
5
+ /**
6
+ * Parse a skill URL string into repo source and skill name.
7
+ * Accepts "https://github.com/owner/repo@skill" (preferred) and
8
+ * legacy "owner/repo@skill" format.
9
+ * Returns undefined if the string is not in the expected format.
10
+ */
11
+ export declare function parseSkillSpec(entry: string): SkillSpec | undefined;
12
+ /**
13
+ * Extract just the skill name (directory name) from a fully-qualified entry.
14
+ */
15
+ export declare function skillName(entry: string): string;
16
+ /**
17
+ * Where installed skill folders live: `~/.openclaw/skills/<name>/`.
18
+ * The `skills` CLI creates a `skills/` subdirectory under its cwd,
19
+ * so we pass `resolveStateDir()` (~/.openclaw/) as the cwd.
20
+ */
21
+ export declare function resolveSharedSkillsDir(): string;
22
+ export declare function isSkillInstalled(name: string): boolean;
23
+ /**
24
+ * Install a single skill via `skills add` into the shared directory,
25
+ * then symlink it into the agent workspace.
26
+ *
27
+ * @param entry Skill URL string ("https://github.com/owner/repo@skill")
28
+ * @param workspaceDir Agent workspace directory
29
+ * Returns true on success, false on failure (logged, non-fatal).
30
+ */
31
+ export declare function installSkill(entry: string, workspaceDir: string): boolean;
32
+ /**
33
+ * Install all skills for an agent: skip already-installed, batch-install
34
+ * missing skills grouped by repo (one clone per repo), then symlink
35
+ * everything into the workspace.
36
+ *
37
+ * @param skills Array of skill URL strings ("https://github.com/owner/repo@skill")
38
+ * Returns { installed, skipped, failed } counts.
39
+ */
40
+ export declare function installAllSkills(skills: string[], workspaceDir: string): {
41
+ installed: number;
42
+ skipped: number;
43
+ failed: number;
44
+ };
45
+ /**
46
+ * Update all installed skills via `skills update`.
47
+ */
48
+ export declare function updateAllSkills(): boolean;
49
+ /**
50
+ * Re-sync symlinks for an agent workspace.
51
+ * Ensures every skill in the list is linked; adds new links, preserves existing.
52
+ */
53
+ export declare function syncSkillLinks(skills: string[], workspaceDir: string): void;
@@ -0,0 +1,199 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+ import { resolveStateDir } from "./paths.js";
6
+ /**
7
+ * Resolve the `skills` binary shipped as a dependency rather than relying on
8
+ * `npx` (which re-downloads on every invocation). Falls back to bare `skills`
9
+ * on PATH if the local binary cannot be resolved.
10
+ */
11
+ function resolveSkillsBin() {
12
+ try {
13
+ const require = createRequire(import.meta.url);
14
+ const pkgJson = require.resolve("skills/package.json");
15
+ return path.join(path.dirname(pkgJson), "bin", "cli.mjs");
16
+ }
17
+ catch {
18
+ return "skills";
19
+ }
20
+ }
21
+ const SKILLS_BIN = resolveSkillsBin();
22
+ /**
23
+ * Parse a skill URL string into repo source and skill name.
24
+ * Accepts "https://github.com/owner/repo@skill" (preferred) and
25
+ * legacy "owner/repo@skill" format.
26
+ * Returns undefined if the string is not in the expected format.
27
+ */
28
+ export function parseSkillSpec(entry) {
29
+ const atIdx = entry.lastIndexOf("@");
30
+ if (atIdx <= 0)
31
+ return undefined;
32
+ const repo = entry.slice(0, atIdx);
33
+ const skill = entry.slice(atIdx + 1);
34
+ if (!repo.includes("/") || !skill)
35
+ return undefined;
36
+ return { repo, skill };
37
+ }
38
+ /**
39
+ * Extract just the skill name (directory name) from a fully-qualified entry.
40
+ */
41
+ export function skillName(entry) {
42
+ const spec = parseSkillSpec(entry);
43
+ return spec ? spec.skill : entry;
44
+ }
45
+ // ── Shared skills directory ──────────────────────────────────────────────────
46
+ /**
47
+ * Where installed skill folders live: `~/.openclaw/skills/<name>/`.
48
+ * The `skills` CLI creates a `skills/` subdirectory under its cwd,
49
+ * so we pass `resolveStateDir()` (~/.openclaw/) as the cwd.
50
+ */
51
+ export function resolveSharedSkillsDir() {
52
+ return path.join(resolveStateDir(), "skills");
53
+ }
54
+ function resolveSkillDir(name) {
55
+ return path.join(resolveSharedSkillsDir(), name);
56
+ }
57
+ export function isSkillInstalled(name) {
58
+ const dir = resolveSkillDir(name);
59
+ if (!fs.existsSync(dir))
60
+ return false;
61
+ return fs.existsSync(path.join(dir, "SKILL.md"));
62
+ }
63
+ // ── Install / link ───────────────────────────────────────────────────────────
64
+ /**
65
+ * Install one or more skills from a single repo via `skills add` into the
66
+ * shared directory. Accepts multiple skill names so only one clone is needed.
67
+ *
68
+ * Returns the list of skill names that were successfully installed.
69
+ */
70
+ function installSkillsFromRepo(repo, skillNames) {
71
+ const skillFlag = skillNames.join(" ");
72
+ try {
73
+ const cmd = `node ${SKILLS_BIN} add ${repo} --skill ${skillFlag} --agent openclaw -y`;
74
+ execSync(cmd, { stdio: "inherit", cwd: resolveStateDir(), timeout: 300_000 });
75
+ }
76
+ catch {
77
+ console.error(` Warning: failed to install skills from ${repo}: ${skillNames.join(", ")}`);
78
+ }
79
+ return skillNames.filter((s) => isSkillInstalled(s));
80
+ }
81
+ /**
82
+ * Install a single skill via `skills add` into the shared directory,
83
+ * then symlink it into the agent workspace.
84
+ *
85
+ * @param entry Skill URL string ("https://github.com/owner/repo@skill")
86
+ * @param workspaceDir Agent workspace directory
87
+ * Returns true on success, false on failure (logged, non-fatal).
88
+ */
89
+ export function installSkill(entry, workspaceDir) {
90
+ const spec = parseSkillSpec(entry);
91
+ if (!spec) {
92
+ console.error(` Warning: invalid skill spec "${entry}" — expected "https://github.com/owner/repo@skill"`);
93
+ return false;
94
+ }
95
+ if (!isSkillInstalled(spec.skill)) {
96
+ const ok = installSkillsFromRepo(spec.repo, [spec.skill]);
97
+ if (ok.length === 0)
98
+ return false;
99
+ }
100
+ if (!isSkillInstalled(spec.skill)) {
101
+ console.error(` Warning: skill "${spec.skill}" not found after install`);
102
+ return false;
103
+ }
104
+ linkSkillToWorkspace(spec.skill, workspaceDir);
105
+ return true;
106
+ }
107
+ /**
108
+ * Create a symlink from the shared skill dir into the agent workspace.
109
+ */
110
+ function linkSkillToWorkspace(name, workspaceDir) {
111
+ const target = resolveSkillDir(name);
112
+ const wsSkillsDir = path.join(workspaceDir, "skills");
113
+ fs.mkdirSync(wsSkillsDir, { recursive: true });
114
+ const link = path.join(wsSkillsDir, name);
115
+ if (fs.existsSync(link)) {
116
+ const stat = fs.lstatSync(link);
117
+ if (stat.isSymbolicLink()) {
118
+ const existing = fs.readlinkSync(link);
119
+ if (existing === target)
120
+ return;
121
+ fs.unlinkSync(link);
122
+ }
123
+ else {
124
+ fs.rmSync(link, { recursive: true, force: true });
125
+ }
126
+ }
127
+ fs.symlinkSync(target, link, "dir");
128
+ }
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) {
138
+ fs.mkdirSync(resolveStateDir(), { recursive: true });
139
+ let installed = 0;
140
+ let skipped = 0;
141
+ let failed = 0;
142
+ // Group missing skills by repo so each repo is cloned only once
143
+ const needInstall = new Map();
144
+ for (const entry of skills) {
145
+ const spec = parseSkillSpec(entry);
146
+ if (!spec) {
147
+ console.error(` Warning: invalid skill spec "${entry}" — expected "https://github.com/owner/repo@skill"`);
148
+ failed++;
149
+ continue;
150
+ }
151
+ if (isSkillInstalled(spec.skill)) {
152
+ linkSkillToWorkspace(spec.skill, workspaceDir);
153
+ skipped++;
154
+ }
155
+ else {
156
+ const list = needInstall.get(spec.repo) ?? [];
157
+ list.push(spec.skill);
158
+ needInstall.set(spec.repo, list);
159
+ }
160
+ }
161
+ // Batch install: one `skills add` per repo
162
+ 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);
168
+ }
169
+ }
170
+ return { installed, skipped, failed };
171
+ }
172
+ /**
173
+ * Update all installed skills via `skills update`.
174
+ */
175
+ export function updateAllSkills() {
176
+ try {
177
+ execSync(`node ${SKILLS_BIN} update --agent openclaw`, {
178
+ stdio: "inherit",
179
+ cwd: resolveStateDir(),
180
+ });
181
+ return true;
182
+ }
183
+ catch {
184
+ console.error(" Warning: failed to update skills");
185
+ return false;
186
+ }
187
+ }
188
+ /**
189
+ * Re-sync symlinks for an agent workspace.
190
+ * Ensures every skill in the list is linked; adds new links, preserves existing.
191
+ */
192
+ export function syncSkillLinks(skills, workspaceDir) {
193
+ for (const entry of skills) {
194
+ const name = skillName(entry);
195
+ if (isSkillInstalled(name)) {
196
+ linkSkillToWorkspace(name, workspaceDir);
197
+ }
198
+ }
199
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyclaw/talenthub",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "CLI tool to manage StoryClaw AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,8 @@
16
16
  "test:watch": "vitest"
17
17
  },
18
18
  "dependencies": {
19
- "commander": "^13.0.0"
19
+ "commander": "^13.0.0",
20
+ "skills": "^1.4.5"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@types/node": "^22.0.0",