@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 +1 -0
- package/dist/commands/agent-install.d.ts +1 -0
- package/dist/commands/agent-install.js +85 -29
- package/dist/commands/agent-update.js +5 -3
- package/dist/lib/skills.d.ts +9 -1
- package/dist/lib/skills.js +54 -22
- package/package.json +1 -1
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]")
|
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
+
log("Verifying token...");
|
|
17
27
|
try {
|
|
18
28
|
await verifyToken(token);
|
|
19
29
|
}
|
|
20
30
|
catch {
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
113
|
+
workspace: wsDir,
|
|
64
114
|
});
|
|
65
115
|
writeConfig(updatedCfg);
|
|
66
116
|
markInstalled(manifest.id, manifest.version);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
parts
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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("\
|
|
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.
|
|
74
|
+
console.log(`✓ Agent "${name}" updated. All done.`);
|
|
73
75
|
}
|
package/dist/lib/skills.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/lib/skills.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 };
|