@storyclaw/talenthub 0.3.0 → 0.3.2
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/commands/agent-install.js +18 -25
- package/dist/commands/agent-publish.js +1 -0
- package/dist/commands/agent-update.js +11 -29
- package/dist/lib/skill-sources.d.ts +28 -0
- package/dist/lib/skill-sources.js +107 -0
- package/dist/lib/skills.d.ts +51 -0
- package/dist/lib/skills.js +197 -0
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
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,35 +33,29 @@ export async function agentInstall(name, options) {
|
|
|
34
33
|
}
|
|
35
34
|
let installed = 0;
|
|
36
35
|
let failed = 0;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
44
|
+
const updatedCfg = addOrUpdateAgent(cfg, {
|
|
53
45
|
id: manifest.id,
|
|
54
46
|
name: manifest.name,
|
|
55
47
|
skills: manifest.skills,
|
|
56
|
-
model: manifest.model,
|
|
57
48
|
});
|
|
58
49
|
writeConfig(updatedCfg);
|
|
59
50
|
markInstalled(manifest.id, manifest.version);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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}.`);
|
|
67
60
|
console.log("Restart the OpenClaw gateway to apply changes.");
|
|
68
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 {
|
|
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,37 +26,16 @@ async function updateAgent(agentId) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
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, {
|
|
56
36
|
id: manifest.id,
|
|
57
37
|
name: manifest.name,
|
|
58
38
|
skills: manifest.skills,
|
|
59
|
-
model: manifest.model,
|
|
60
39
|
});
|
|
61
40
|
writeConfig(updatedCfg);
|
|
62
41
|
markInstalled(manifest.id, manifest.version);
|
|
@@ -69,8 +48,10 @@ export async function agentUpdate(name, options) {
|
|
|
69
48
|
console.log("All agents are up to date.");
|
|
70
49
|
return;
|
|
71
50
|
}
|
|
72
|
-
|
|
73
|
-
console.log(
|
|
51
|
+
// Update shared skills first
|
|
52
|
+
console.log("Updating shared skills...");
|
|
53
|
+
updateAllSkills();
|
|
54
|
+
console.log(`\nFound ${updates.length} update(s):\n`);
|
|
74
55
|
for (const u of updates) {
|
|
75
56
|
console.log(` Updating ${u.name}: ${u.currentVersion} → ${u.latestVersion}`);
|
|
76
57
|
await updateAgent(u.agentId);
|
|
@@ -84,7 +65,8 @@ export async function agentUpdate(name, options) {
|
|
|
84
65
|
console.error(`Agent "${name}" is not installed. Use "talenthub agent install ${name}" first.`);
|
|
85
66
|
process.exit(1);
|
|
86
67
|
}
|
|
87
|
-
|
|
68
|
+
console.log("Updating shared skills...");
|
|
69
|
+
updateAllSkills();
|
|
88
70
|
console.log(`Updating agent "${name}"...`);
|
|
89
71
|
await updateAgent(name);
|
|
90
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,51 @@
|
|
|
1
|
+
export type SkillSpec = {
|
|
2
|
+
repo: string;
|
|
3
|
+
skill: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Parse a fully-qualified skill string ("owner/repo@skill") into its parts.
|
|
7
|
+
* Returns undefined if the string is not in the expected format.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseSkillSpec(entry: string): SkillSpec | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Extract just the skill name (directory name) from a fully-qualified entry.
|
|
12
|
+
*/
|
|
13
|
+
export declare function skillName(entry: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Where installed skill folders live: `~/.openclaw/skills/<name>/`.
|
|
16
|
+
* The `skills` CLI creates a `skills/` subdirectory under its cwd,
|
|
17
|
+
* so we pass `resolveStateDir()` (~/.openclaw/) as the cwd.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveSharedSkillsDir(): string;
|
|
20
|
+
export declare function isSkillInstalled(name: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Install a single skill via `skills add` into the shared directory,
|
|
23
|
+
* then symlink it into the agent workspace.
|
|
24
|
+
*
|
|
25
|
+
* @param entry Fully-qualified skill string ("owner/repo@skill")
|
|
26
|
+
* @param workspaceDir Agent workspace directory
|
|
27
|
+
* Returns true on success, false on failure (logged, non-fatal).
|
|
28
|
+
*/
|
|
29
|
+
export declare function installSkill(entry: string, workspaceDir: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Install all skills for an agent: skip already-installed, batch-install
|
|
32
|
+
* missing skills grouped by repo (one clone per repo), then symlink
|
|
33
|
+
* everything into the workspace.
|
|
34
|
+
*
|
|
35
|
+
* @param skills Array of fully-qualified skill strings ("owner/repo@skill")
|
|
36
|
+
* Returns { installed, skipped, failed } counts.
|
|
37
|
+
*/
|
|
38
|
+
export declare function installAllSkills(skills: string[], workspaceDir: string): {
|
|
39
|
+
installed: number;
|
|
40
|
+
skipped: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Update all installed skills via `skills update`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function updateAllSkills(): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Re-sync symlinks for an agent workspace.
|
|
49
|
+
* Ensures every skill in the list is linked; adds new links, preserves existing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function syncSkillLinks(skills: string[], workspaceDir: string): void;
|
|
@@ -0,0 +1,197 @@
|
|
|
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 fully-qualified skill string ("owner/repo@skill") into its parts.
|
|
24
|
+
* Returns undefined if the string is not in the expected format.
|
|
25
|
+
*/
|
|
26
|
+
export function parseSkillSpec(entry) {
|
|
27
|
+
const atIdx = entry.lastIndexOf("@");
|
|
28
|
+
if (atIdx <= 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
const repo = entry.slice(0, atIdx);
|
|
31
|
+
const skill = entry.slice(atIdx + 1);
|
|
32
|
+
if (!repo.includes("/") || !skill)
|
|
33
|
+
return undefined;
|
|
34
|
+
return { repo, skill };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract just the skill name (directory name) from a fully-qualified entry.
|
|
38
|
+
*/
|
|
39
|
+
export function skillName(entry) {
|
|
40
|
+
const spec = parseSkillSpec(entry);
|
|
41
|
+
return spec ? spec.skill : entry;
|
|
42
|
+
}
|
|
43
|
+
// ── Shared skills directory ──────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Where installed skill folders live: `~/.openclaw/skills/<name>/`.
|
|
46
|
+
* The `skills` CLI creates a `skills/` subdirectory under its cwd,
|
|
47
|
+
* so we pass `resolveStateDir()` (~/.openclaw/) as the cwd.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveSharedSkillsDir() {
|
|
50
|
+
return path.join(resolveStateDir(), "skills");
|
|
51
|
+
}
|
|
52
|
+
function resolveSkillDir(name) {
|
|
53
|
+
return path.join(resolveSharedSkillsDir(), name);
|
|
54
|
+
}
|
|
55
|
+
export function isSkillInstalled(name) {
|
|
56
|
+
const dir = resolveSkillDir(name);
|
|
57
|
+
if (!fs.existsSync(dir))
|
|
58
|
+
return false;
|
|
59
|
+
return fs.existsSync(path.join(dir, "SKILL.md"));
|
|
60
|
+
}
|
|
61
|
+
// ── Install / link ───────────────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* Install one or more skills from a single repo via `skills add` into the
|
|
64
|
+
* shared directory. Accepts multiple skill names so only one clone is needed.
|
|
65
|
+
*
|
|
66
|
+
* Returns the list of skill names that were successfully installed.
|
|
67
|
+
*/
|
|
68
|
+
function installSkillsFromRepo(repo, skillNames) {
|
|
69
|
+
const skillFlag = skillNames.join(" ");
|
|
70
|
+
try {
|
|
71
|
+
const cmd = `node ${SKILLS_BIN} add ${repo} --skill ${skillFlag} --agent openclaw -y`;
|
|
72
|
+
execSync(cmd, { stdio: "inherit", cwd: resolveStateDir(), timeout: 300_000 });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
console.error(` Warning: failed to install skills from ${repo}: ${skillNames.join(", ")}`);
|
|
76
|
+
}
|
|
77
|
+
return skillNames.filter((s) => isSkillInstalled(s));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Install a single skill via `skills add` into the shared directory,
|
|
81
|
+
* then symlink it into the agent workspace.
|
|
82
|
+
*
|
|
83
|
+
* @param entry Fully-qualified skill string ("owner/repo@skill")
|
|
84
|
+
* @param workspaceDir Agent workspace directory
|
|
85
|
+
* Returns true on success, false on failure (logged, non-fatal).
|
|
86
|
+
*/
|
|
87
|
+
export function installSkill(entry, workspaceDir) {
|
|
88
|
+
const spec = parseSkillSpec(entry);
|
|
89
|
+
if (!spec) {
|
|
90
|
+
console.error(` Warning: invalid skill spec "${entry}" — expected "owner/repo@skill"`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (!isSkillInstalled(spec.skill)) {
|
|
94
|
+
const ok = installSkillsFromRepo(spec.repo, [spec.skill]);
|
|
95
|
+
if (ok.length === 0)
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (!isSkillInstalled(spec.skill)) {
|
|
99
|
+
console.error(` Warning: skill "${spec.skill}" not found after install`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
linkSkillToWorkspace(spec.skill, workspaceDir);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Create a symlink from the shared skill dir into the agent workspace.
|
|
107
|
+
*/
|
|
108
|
+
function linkSkillToWorkspace(name, workspaceDir) {
|
|
109
|
+
const target = resolveSkillDir(name);
|
|
110
|
+
const wsSkillsDir = path.join(workspaceDir, "skills");
|
|
111
|
+
fs.mkdirSync(wsSkillsDir, { recursive: true });
|
|
112
|
+
const link = path.join(wsSkillsDir, name);
|
|
113
|
+
if (fs.existsSync(link)) {
|
|
114
|
+
const stat = fs.lstatSync(link);
|
|
115
|
+
if (stat.isSymbolicLink()) {
|
|
116
|
+
const existing = fs.readlinkSync(link);
|
|
117
|
+
if (existing === target)
|
|
118
|
+
return;
|
|
119
|
+
fs.unlinkSync(link);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
fs.rmSync(link, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
fs.symlinkSync(target, link, "dir");
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Install all skills for an agent: skip already-installed, batch-install
|
|
129
|
+
* missing skills grouped by repo (one clone per repo), then symlink
|
|
130
|
+
* everything into the workspace.
|
|
131
|
+
*
|
|
132
|
+
* @param skills Array of fully-qualified skill strings ("owner/repo@skill")
|
|
133
|
+
* Returns { installed, skipped, failed } counts.
|
|
134
|
+
*/
|
|
135
|
+
export function installAllSkills(skills, workspaceDir) {
|
|
136
|
+
fs.mkdirSync(resolveStateDir(), { recursive: true });
|
|
137
|
+
let installed = 0;
|
|
138
|
+
let skipped = 0;
|
|
139
|
+
let failed = 0;
|
|
140
|
+
// Group missing skills by repo so each repo is cloned only once
|
|
141
|
+
const needInstall = new Map();
|
|
142
|
+
for (const entry of skills) {
|
|
143
|
+
const spec = parseSkillSpec(entry);
|
|
144
|
+
if (!spec) {
|
|
145
|
+
console.error(` Warning: invalid skill spec "${entry}" — expected "owner/repo@skill"`);
|
|
146
|
+
failed++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (isSkillInstalled(spec.skill)) {
|
|
150
|
+
linkSkillToWorkspace(spec.skill, workspaceDir);
|
|
151
|
+
skipped++;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const list = needInstall.get(spec.repo) ?? [];
|
|
155
|
+
list.push(spec.skill);
|
|
156
|
+
needInstall.set(spec.repo, list);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Batch install: one `skills add` per repo
|
|
160
|
+
for (const [repo, skillNames] of needInstall) {
|
|
161
|
+
const ok = installSkillsFromRepo(repo, skillNames);
|
|
162
|
+
installed += ok.length;
|
|
163
|
+
failed += skillNames.length - ok.length;
|
|
164
|
+
for (const name of ok) {
|
|
165
|
+
linkSkillToWorkspace(name, workspaceDir);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { installed, skipped, failed };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Update all installed skills via `skills update`.
|
|
172
|
+
*/
|
|
173
|
+
export function updateAllSkills() {
|
|
174
|
+
try {
|
|
175
|
+
execSync(`node ${SKILLS_BIN} update --agent openclaw`, {
|
|
176
|
+
stdio: "inherit",
|
|
177
|
+
cwd: resolveStateDir(),
|
|
178
|
+
});
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
console.error(" Warning: failed to update skills");
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Re-sync symlinks for an agent workspace.
|
|
188
|
+
* Ensures every skill in the list is linked; adds new links, preserves existing.
|
|
189
|
+
*/
|
|
190
|
+
export function syncSkillLinks(skills, workspaceDir) {
|
|
191
|
+
for (const entry of skills) {
|
|
192
|
+
const name = skillName(entry);
|
|
193
|
+
if (isSkillInstalled(name)) {
|
|
194
|
+
linkSkillToWorkspace(name, workspaceDir);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyclaw/talenthub",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|