@storyclaw/talenthub 0.1.0

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.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { agentInstall } from "./commands/agent-install.js";
4
+ import { agentList } from "./commands/agent-list.js";
5
+ import { agentPublish } from "./commands/agent-publish.js";
6
+ import { agentSearch } from "./commands/agent-search.js";
7
+ import { agentUninstall } from "./commands/agent-uninstall.js";
8
+ import { agentUnpublish } from "./commands/agent-unpublish.js";
9
+ import { agentUpdate } from "./commands/agent-update.js";
10
+ import { login } from "./commands/login.js";
11
+ import { logout } from "./commands/logout.js";
12
+ const program = new Command();
13
+ program
14
+ .name("talenthub")
15
+ .description("Manage StoryClaw AI agents")
16
+ .version("0.1.0");
17
+ program.command("login").description("Authenticate with StoryClaw").action(login);
18
+ program.command("logout").description("Remove stored credentials").action(logout);
19
+ const agent = program.command("agent").description("Agent management commands");
20
+ agent
21
+ .command("install <name>")
22
+ .description("Install an agent and its skills")
23
+ .option("-f, --force", "Overwrite existing agent", false)
24
+ .action(agentInstall);
25
+ agent
26
+ .command("update [name]")
27
+ .description("Update an agent or all agents")
28
+ .option("-a, --all", "Update all installed agents")
29
+ .action(agentUpdate);
30
+ agent
31
+ .command("uninstall <name>")
32
+ .description("Remove an installed agent")
33
+ .option("-y, --yes", "Skip confirmation prompt")
34
+ .action(agentUninstall);
35
+ agent
36
+ .command("list")
37
+ .description("List installed agents and check for updates")
38
+ .action(agentList);
39
+ agent
40
+ .command("search [query]")
41
+ .description("Browse available agents")
42
+ .action(agentSearch);
43
+ agent
44
+ .command("publish <name>")
45
+ .description("Publish a local agent to the registry")
46
+ .option("-d, --dir <path>", "Agent directory containing manifest.json and .md files")
47
+ .action(agentPublish);
48
+ agent
49
+ .command("unpublish <name>")
50
+ .description("Archive an agent from the registry")
51
+ .action(agentUnpublish);
52
+ program.parse();
@@ -0,0 +1,3 @@
1
+ export declare function agentInstall(name: string, options: {
2
+ force?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,68 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isClawhubAvailable, installSkill } from "../lib/clawhub.js";
4
+ import { addOrUpdateAgent, findAgentEntry, readConfig, writeConfig } from "../lib/config.js";
5
+ import { fetchCatalog, fetchManifest } from "../lib/registry.js";
6
+ import { resolveWorkspaceDir } from "../lib/paths.js";
7
+ import { markInstalled } from "../lib/state.js";
8
+ export async function agentInstall(name, options) {
9
+ console.log(`Looking up agent "${name}"...`);
10
+ const catalog = await fetchCatalog();
11
+ if (!catalog.agents[name]) {
12
+ const available = Object.keys(catalog.agents).join(", ");
13
+ console.error(`Agent "${name}" not found. Available: ${available}`);
14
+ process.exit(1);
15
+ }
16
+ const manifest = await fetchManifest(name);
17
+ console.log(`Found ${manifest.emoji} ${manifest.name} v${manifest.version} (${manifest.skills.length} skills)`);
18
+ const cfg = readConfig();
19
+ const existing = findAgentEntry(cfg, name);
20
+ if (existing && !options.force) {
21
+ console.error(`Agent "${name}" already exists in config. Use --force to overwrite.`);
22
+ process.exit(1);
23
+ }
24
+ const hasClawhub = isClawhubAvailable();
25
+ const wsDir = resolveWorkspaceDir(name);
26
+ fs.mkdirSync(wsDir, { recursive: true });
27
+ console.log("Writing agent files...");
28
+ if (manifest.files) {
29
+ for (const [filename, content] of Object.entries(manifest.files)) {
30
+ if (content) {
31
+ fs.writeFileSync(path.join(wsDir, filename), content, "utf-8");
32
+ }
33
+ }
34
+ }
35
+ let installed = 0;
36
+ 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}`);
51
+ }
52
+ let updatedCfg = addOrUpdateAgent(cfg, {
53
+ id: manifest.id,
54
+ name: manifest.name,
55
+ skills: manifest.skills,
56
+ model: manifest.model,
57
+ });
58
+ writeConfig(updatedCfg);
59
+ markInstalled(manifest.id, manifest.version);
60
+ console.log(`\n${manifest.emoji} Installed ${manifest.name}` +
61
+ (installed > 0 ? ` with ${installed} skills` : "") +
62
+ (failed > 0 ? ` (${failed} failed)` : "") +
63
+ (!hasClawhub && manifest.skills.length > 0
64
+ ? " (skills pending — install clawhub)"
65
+ : "") +
66
+ ".");
67
+ console.log("Restart the OpenClaw gateway to apply changes.");
68
+ }
@@ -0,0 +1 @@
1
+ export declare function agentList(): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { readState } from "../lib/state.js";
2
+ import { getCatalogCached } from "../lib/update-check.js";
3
+ export async function agentList() {
4
+ const state = readState();
5
+ const agents = Object.entries(state.agents);
6
+ if (agents.length === 0) {
7
+ console.log("No agents installed via talenthub.");
8
+ console.log('Run "talenthub agent search" to see available agents.');
9
+ return;
10
+ }
11
+ let catalog;
12
+ try {
13
+ catalog = await getCatalogCached();
14
+ }
15
+ catch {
16
+ catalog = null;
17
+ }
18
+ const header = padRow("Agent", "Version", "Skills", "Status");
19
+ console.log(header);
20
+ console.log("-".repeat(header.length));
21
+ for (const [id, info] of agents) {
22
+ const remote = catalog?.agents[id];
23
+ const skillCount = remote?.skillCount?.toString() ?? "?";
24
+ let status = "Up to date";
25
+ if (remote && remote.version !== info.version) {
26
+ status = `Update available (${remote.version})`;
27
+ }
28
+ else if (!remote) {
29
+ status = "Not in registry";
30
+ }
31
+ console.log(padRow(id, info.version, skillCount, status));
32
+ }
33
+ }
34
+ function padRow(name, version, skills, status) {
35
+ return ` ${name.padEnd(16)} ${version.padEnd(14)} ${skills.padEnd(8)} ${status}`;
36
+ }
@@ -0,0 +1,3 @@
1
+ export declare function agentPublish(name: string, opts: {
2
+ dir?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,121 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import readline from "node:readline";
4
+ import { readAuth, getRegistryBaseUrl } from "../lib/auth.js";
5
+ import { findAgentEntry, readConfig } from "../lib/config.js";
6
+ import { resolveWorkspaceDir } from "../lib/paths.js";
7
+ function prompt(question) {
8
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
9
+ return new Promise((resolve) => {
10
+ rl.question(question, (answer) => {
11
+ rl.close();
12
+ resolve(answer.trim());
13
+ });
14
+ });
15
+ }
16
+ const MAX_FILE_SIZE = 200 * 1024;
17
+ const MAX_TOTAL_SIZE = 1024 * 1024;
18
+ function readAgentDir(dir) {
19
+ if (!fs.existsSync(dir)) {
20
+ console.error(`Directory not found: ${dir}`);
21
+ process.exit(1);
22
+ }
23
+ let manifest = {};
24
+ const manifestPath = path.join(dir, "manifest.json");
25
+ if (fs.existsSync(manifestPath)) {
26
+ try {
27
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
28
+ }
29
+ catch {
30
+ console.warn("Warning: could not parse manifest.json.");
31
+ }
32
+ }
33
+ const files = {};
34
+ let totalSize = 0;
35
+ for (const filename of ["IDENTITY.md", "USER.md", "SOUL.md", "AGENTS.md"]) {
36
+ const p = path.join(dir, filename);
37
+ if (fs.existsSync(p)) {
38
+ const content = fs.readFileSync(p, "utf-8");
39
+ const size = Buffer.byteLength(content, "utf-8");
40
+ if (size > MAX_FILE_SIZE) {
41
+ console.error(`${filename} exceeds 200KB limit (${Math.round(size / 1024)}KB).`);
42
+ process.exit(1);
43
+ }
44
+ totalSize += size;
45
+ files[filename] = content;
46
+ }
47
+ }
48
+ if (totalSize > MAX_TOTAL_SIZE) {
49
+ console.error(`Total prompt content exceeds 1MB limit (${Math.round(totalSize / 1024)}KB).`);
50
+ process.exit(1);
51
+ }
52
+ return { manifest, files };
53
+ }
54
+ export async function agentPublish(name, opts) {
55
+ const auth = readAuth();
56
+ if (!auth) {
57
+ console.error("Not logged in. Run \"talenthub login\" first.");
58
+ process.exit(1);
59
+ }
60
+ let manifest;
61
+ let files;
62
+ let fallbackName = name;
63
+ let fallbackModel = "claude-sonnet-4-5";
64
+ let fallbackSkills = [];
65
+ if (opts.dir) {
66
+ const agentDir = path.resolve(opts.dir);
67
+ ({ manifest, files } = readAgentDir(agentDir));
68
+ }
69
+ else {
70
+ const cfg = readConfig();
71
+ const entry = findAgentEntry(cfg, name);
72
+ if (!entry) {
73
+ console.error(`Agent "${name}" not found in openclaw config.`);
74
+ console.error("Use --dir to publish from an agent directory, or install the agent first.");
75
+ process.exit(1);
76
+ }
77
+ fallbackName = entry.name || name;
78
+ fallbackModel = entry.model || fallbackModel;
79
+ fallbackSkills = entry.skills || [];
80
+ const wsDir = resolveWorkspaceDir(name);
81
+ ({ manifest, files } = readAgentDir(wsDir));
82
+ }
83
+ const agentId = manifest.id || name;
84
+ const currentVersion = manifest.version || "0.1.0";
85
+ const version = await prompt(`Version [${currentVersion}]: `);
86
+ const finalVersion = version || currentVersion;
87
+ const payload = {
88
+ id: agentId,
89
+ version: finalVersion,
90
+ name: manifest.name || fallbackName,
91
+ emoji: manifest.emoji || "",
92
+ role: manifest.role || "",
93
+ tagline: manifest.tagline || "",
94
+ description: manifest.description || "",
95
+ category: manifest.category || "productivity",
96
+ model: manifest.model || fallbackModel,
97
+ skills: manifest.skills || fallbackSkills,
98
+ identity_prompt: files["IDENTITY.md"] || "",
99
+ user_prompt: files["USER.md"] || "",
100
+ soul_prompt: files["SOUL.md"] || "",
101
+ agents_prompt: files["AGENTS.md"] || "",
102
+ min_openclaw_version: manifest.minOpenClawVersion || null,
103
+ avatar_url: manifest.avatarUrl || null,
104
+ };
105
+ console.log(`\nPublishing ${payload.emoji || ""} ${payload.name} v${finalVersion}...`);
106
+ const base = getRegistryBaseUrl();
107
+ const res = await fetch(`${base}/api/talent/registry/publish`, {
108
+ method: "POST",
109
+ headers: {
110
+ "Content-Type": "application/json",
111
+ Authorization: `Bearer ${auth.token}`,
112
+ },
113
+ body: JSON.stringify(payload),
114
+ });
115
+ const result = await res.json().catch(() => ({ error: "Unknown error" }));
116
+ if (!res.ok) {
117
+ console.error(`\n✗ Publish failed: ${result.error}`);
118
+ process.exit(1);
119
+ }
120
+ console.log(`\n✓ ${result.message}`);
121
+ }
@@ -0,0 +1 @@
1
+ export declare function agentSearch(query?: string): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { readState } from "../lib/state.js";
2
+ import { getCatalogCached } from "../lib/update-check.js";
3
+ export async function agentSearch(query) {
4
+ const catalog = await getCatalogCached();
5
+ const state = readState();
6
+ let entries = Object.entries(catalog.agents);
7
+ if (query) {
8
+ const q = query.toLowerCase();
9
+ entries = entries.filter(([id, a]) => id.includes(q) ||
10
+ a.name.toLowerCase().includes(q) ||
11
+ a.category.toLowerCase().includes(q));
12
+ }
13
+ if (entries.length === 0) {
14
+ console.log(query ? `No agents matching "${query}".` : "No agents available.");
15
+ return;
16
+ }
17
+ const header = padRow("", "Agent", "Name", "Category", "Skills", "Version");
18
+ console.log(header);
19
+ console.log("-".repeat(header.length));
20
+ for (const [id, agent] of entries) {
21
+ const installed = state.agents[id] ? "✓" : " ";
22
+ console.log(padRow(installed, id, `${agent.emoji} ${agent.name}`, agent.category, agent.skillCount.toString(), agent.version));
23
+ }
24
+ console.log(`\n${entries.length} agent(s) found. ✓ = installed`);
25
+ }
26
+ function padRow(mark, id, name, category, skills, version) {
27
+ return ` ${mark.padEnd(2)} ${id.padEnd(14)} ${name.padEnd(32)} ${category.padEnd(14)} ${skills.padEnd(8)} ${version}`;
28
+ }
@@ -0,0 +1,3 @@
1
+ export declare function agentUninstall(name: string, options: {
2
+ yes?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import readline from "node:readline";
3
+ import { readConfig, removeAgent, writeConfig } from "../lib/config.js";
4
+ import { resolveWorkspaceDir } from "../lib/paths.js";
5
+ import { markUninstalled, readState } from "../lib/state.js";
6
+ function confirm(question) {
7
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8
+ return new Promise((resolve) => {
9
+ rl.question(question, (answer) => {
10
+ rl.close();
11
+ resolve(answer.toLowerCase().startsWith("y"));
12
+ });
13
+ });
14
+ }
15
+ export async function agentUninstall(name, options) {
16
+ const state = readState();
17
+ if (!state.agents[name]) {
18
+ console.error(`Agent "${name}" is not installed via talenthub.`);
19
+ process.exit(1);
20
+ }
21
+ if (!options.yes) {
22
+ const ok = await confirm(`Remove agent "${name}"? This will archive the workspace. [y/N] `);
23
+ if (!ok) {
24
+ console.log("Cancelled.");
25
+ return;
26
+ }
27
+ }
28
+ // Archive workspace
29
+ const wsDir = resolveWorkspaceDir(name);
30
+ if (fs.existsSync(wsDir)) {
31
+ const backupDir = `${wsDir}.bak`;
32
+ if (fs.existsSync(backupDir)) {
33
+ fs.rmSync(backupDir, { recursive: true, force: true });
34
+ }
35
+ fs.renameSync(wsDir, backupDir);
36
+ console.log(`Workspace archived to ${backupDir}`);
37
+ }
38
+ // Remove from config
39
+ const cfg = readConfig();
40
+ const { config: updatedCfg, removedBindings } = removeAgentFromConfig(cfg, name);
41
+ writeConfig(updatedCfg);
42
+ markUninstalled(name);
43
+ console.log(`Removed agent "${name}"` +
44
+ (removedBindings > 0 ? ` (${removedBindings} binding(s) cleaned)` : "") +
45
+ ".");
46
+ console.log("Restart the OpenClaw gateway to apply changes.");
47
+ }
48
+ function removeAgentFromConfig(cfg, agentId) {
49
+ const result = removeAgent(cfg, agentId);
50
+ const originalBindings = cfg.bindings?.length ?? 0;
51
+ const newBindings = (result.bindings ?? []).length;
52
+ return {
53
+ config: result,
54
+ removedBindings: originalBindings - newBindings,
55
+ };
56
+ }
@@ -0,0 +1 @@
1
+ export declare function agentUnpublish(name: string): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import readline from "node:readline";
2
+ import { readAuth, getRegistryBaseUrl } from "../lib/auth.js";
3
+ function confirm(question) {
4
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5
+ return new Promise((resolve) => {
6
+ rl.question(question, (answer) => {
7
+ rl.close();
8
+ resolve(answer.toLowerCase().startsWith("y"));
9
+ });
10
+ });
11
+ }
12
+ export async function agentUnpublish(name) {
13
+ const auth = readAuth();
14
+ if (!auth) {
15
+ console.error("Not logged in. Run \"talenthub login\" first.");
16
+ process.exit(1);
17
+ }
18
+ const ok = await confirm(`Unpublish agent "${name}"? It will be hidden but data is preserved. [y/N] `);
19
+ if (!ok) {
20
+ console.log("Cancelled.");
21
+ return;
22
+ }
23
+ const base = getRegistryBaseUrl();
24
+ const res = await fetch(`${base}/api/talent/registry/${name}/unpublish`, {
25
+ method: "POST",
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ Authorization: `Bearer ${auth.token}`,
29
+ },
30
+ });
31
+ const result = await res.json().catch(() => ({ error: "Unknown error" }));
32
+ if (!res.ok) {
33
+ console.error(`✗ Unpublish failed: ${result.error}`);
34
+ process.exit(1);
35
+ }
36
+ console.log(`✓ ${result.message}`);
37
+ }
@@ -0,0 +1,3 @@
1
+ export declare function agentUpdate(name?: string, options?: {
2
+ all?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ensureClawhub, installSkill, updateAllSkills } from "../lib/clawhub.js";
4
+ import { addOrUpdateAgent, readConfig, writeConfig } from "../lib/config.js";
5
+ import { fetchManifest } from "../lib/registry.js";
6
+ import { resolveWorkspaceDir } from "../lib/paths.js";
7
+ import { markInstalled, readState } from "../lib/state.js";
8
+ import { checkUpdates } from "../lib/update-check.js";
9
+ async function updateAgent(agentId) {
10
+ const manifest = await fetchManifest(agentId);
11
+ const wsDir = resolveWorkspaceDir(agentId);
12
+ // Backup workspace
13
+ const backupDir = `${wsDir}.bak`;
14
+ if (fs.existsSync(wsDir)) {
15
+ if (fs.existsSync(backupDir)) {
16
+ fs.rmSync(backupDir, { recursive: true, force: true });
17
+ }
18
+ fs.cpSync(wsDir, backupDir, { recursive: true });
19
+ }
20
+ fs.mkdirSync(wsDir, { recursive: true });
21
+ // Write agent files from the registry response
22
+ if (manifest.files) {
23
+ for (const [filename, content] of Object.entries(manifest.files)) {
24
+ if (content) {
25
+ fs.writeFileSync(path.join(wsDir, filename), content, "utf-8");
26
+ }
27
+ }
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
+ }
50
+ }
51
+ // Update all existing skills
52
+ updateAllSkills(wsDir);
53
+ // Update config
54
+ const cfg = readConfig();
55
+ const updatedCfg = addOrUpdateAgent(cfg, {
56
+ id: manifest.id,
57
+ name: manifest.name,
58
+ skills: manifest.skills,
59
+ model: manifest.model,
60
+ });
61
+ writeConfig(updatedCfg);
62
+ markInstalled(manifest.id, manifest.version);
63
+ return true;
64
+ }
65
+ export async function agentUpdate(name, options) {
66
+ if (options?.all || !name) {
67
+ const updates = await checkUpdates();
68
+ if (updates.length === 0) {
69
+ console.log("All agents are up to date.");
70
+ return;
71
+ }
72
+ ensureClawhub();
73
+ console.log(`Found ${updates.length} update(s):\n`);
74
+ for (const u of updates) {
75
+ console.log(` Updating ${u.name}: ${u.currentVersion} → ${u.latestVersion}`);
76
+ await updateAgent(u.agentId);
77
+ console.log(` ✓ ${u.name} updated to ${u.latestVersion}`);
78
+ }
79
+ console.log("\nRestart the OpenClaw gateway to apply changes.");
80
+ return;
81
+ }
82
+ const state = readState();
83
+ if (!state.agents[name]) {
84
+ console.error(`Agent "${name}" is not installed. Use "talenthub agent install ${name}" first.`);
85
+ process.exit(1);
86
+ }
87
+ ensureClawhub();
88
+ console.log(`Updating agent "${name}"...`);
89
+ await updateAgent(name);
90
+ console.log(`✓ Agent "${name}" updated. Restart the OpenClaw gateway to apply changes.`);
91
+ }
@@ -0,0 +1 @@
1
+ export declare function login(): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { execSync } from "node:child_process";
2
+ import { readAuth, requestDeviceCode, pollForToken, writeAuth } from "../lib/auth.js";
3
+ function openUrl(url) {
4
+ try {
5
+ const cmd = process.platform === "darwin"
6
+ ? `open "${url}"`
7
+ : process.platform === "win32"
8
+ ? `start "${url}"`
9
+ : `xdg-open "${url}"`;
10
+ execSync(cmd, { stdio: "ignore" });
11
+ }
12
+ catch {
13
+ // Browser open is best-effort
14
+ }
15
+ }
16
+ export async function login() {
17
+ const existing = readAuth();
18
+ if (existing) {
19
+ console.log("Already logged in.");
20
+ console.log(` User ID: ${existing.user_id}`);
21
+ console.log(` Expires: ${existing.expires_at}`);
22
+ console.log('Run "talenthub logout" first to re-authenticate.');
23
+ return;
24
+ }
25
+ console.log("Requesting device code...");
26
+ const { device_code, user_code, verification_url, interval, expires_in } = await requestDeviceCode();
27
+ console.log(`\nOpen this URL in your browser to authorize:\n`);
28
+ console.log(` ${verification_url}\n`);
29
+ console.log(`Your code: ${user_code}\n`);
30
+ console.log("Waiting for authorization...");
31
+ openUrl(verification_url);
32
+ try {
33
+ const { access_token, user_id, expires_at } = await pollForToken(device_code, interval, expires_in * 1000);
34
+ writeAuth({ token: access_token, user_id, expires_at });
35
+ console.log(`\n✓ Logged in successfully.`);
36
+ console.log(` User ID: ${user_id}`);
37
+ }
38
+ catch (err) {
39
+ console.error(`\n✗ Login failed: ${err instanceof Error ? err.message : err}`);
40
+ process.exit(1);
41
+ }
42
+ }
@@ -0,0 +1 @@
1
+ export declare function logout(): Promise<void>;
@@ -0,0 +1,10 @@
1
+ import { clearAuth, readAuth } from "../lib/auth.js";
2
+ export async function logout() {
3
+ const existing = readAuth();
4
+ if (!existing) {
5
+ console.log("Not logged in.");
6
+ return;
7
+ }
8
+ clearAuth();
9
+ console.log("✓ Logged out. Token removed.");
10
+ }
@@ -0,0 +1,23 @@
1
+ export type AuthData = {
2
+ token: string;
3
+ user_id: string;
4
+ expires_at: string;
5
+ };
6
+ export declare function readAuth(): AuthData | null;
7
+ export declare function writeAuth(data: AuthData): void;
8
+ export declare function clearAuth(): void;
9
+ export declare function getRegistryBaseUrl(): string;
10
+ export type DeviceCodeResponse = {
11
+ device_code: string;
12
+ user_code: string;
13
+ verification_url: string;
14
+ expires_in: number;
15
+ interval: number;
16
+ };
17
+ export declare function requestDeviceCode(): Promise<DeviceCodeResponse>;
18
+ export type TokenResponse = {
19
+ access_token: string;
20
+ user_id: string;
21
+ expires_at: string;
22
+ };
23
+ export declare function pollForToken(deviceCode: string, interval: number, maxWaitMs: number): Promise<TokenResponse>;
@@ -0,0 +1,73 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveStateDir } from "./paths.js";
4
+ function authFilePath() {
5
+ return path.join(resolveStateDir(), "talenthub-auth.json");
6
+ }
7
+ export function readAuth() {
8
+ const p = authFilePath();
9
+ if (!fs.existsSync(p))
10
+ return null;
11
+ try {
12
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
13
+ if (data.expires_at && new Date(data.expires_at) < new Date()) {
14
+ fs.unlinkSync(p);
15
+ return null;
16
+ }
17
+ return data;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export function writeAuth(data) {
24
+ const p = authFilePath();
25
+ fs.mkdirSync(path.dirname(p), { recursive: true });
26
+ fs.writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
27
+ }
28
+ export function clearAuth() {
29
+ const p = authFilePath();
30
+ if (fs.existsSync(p)) {
31
+ fs.unlinkSync(p);
32
+ }
33
+ }
34
+ export function getRegistryBaseUrl() {
35
+ return (process.env.TALENTHUB_URL?.trim() ||
36
+ process.env.TALENTHUB_REGISTRY?.trim() ||
37
+ "https://app.storyclaw.com");
38
+ }
39
+ export async function requestDeviceCode() {
40
+ const base = getRegistryBaseUrl();
41
+ const res = await fetch(`${base}/api/talent/auth/device-code`, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ });
45
+ if (!res.ok) {
46
+ throw new Error(`Failed to request device code: ${res.status}`);
47
+ }
48
+ return res.json();
49
+ }
50
+ export async function pollForToken(deviceCode, interval, maxWaitMs) {
51
+ const base = getRegistryBaseUrl();
52
+ const deadline = Date.now() + maxWaitMs;
53
+ while (Date.now() < deadline) {
54
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
55
+ const res = await fetch(`${base}/api/talent/auth/token`, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify({ device_code: deviceCode }),
59
+ });
60
+ if (res.ok) {
61
+ return res.json();
62
+ }
63
+ const body = await res.json().catch(() => ({ error: "unknown" }));
64
+ if (body.error === "authorization_pending") {
65
+ continue;
66
+ }
67
+ if (body.error === "expired_token") {
68
+ throw new Error("Device code expired. Please try again.");
69
+ }
70
+ throw new Error(`Token exchange failed: ${body.error}`);
71
+ }
72
+ throw new Error("Timed out waiting for authorization.");
73
+ }
@@ -0,0 +1,4 @@
1
+ export declare function ensureClawhub(): void;
2
+ export declare function isClawhubAvailable(): boolean;
3
+ export declare function installSkill(slug: string, workdir: string): boolean;
4
+ export declare function updateAllSkills(workdir: string): boolean;
@@ -0,0 +1,45 @@
1
+ import { execSync } from "node:child_process";
2
+ function hasClawhub() {
3
+ try {
4
+ execSync("clawhub --version", { stdio: "pipe" });
5
+ return true;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ export function ensureClawhub() {
12
+ if (!hasClawhub()) {
13
+ console.error("Error: clawhub CLI is not installed.\n" +
14
+ "Install it with: npm i -g clawhub\n" +
15
+ "Then retry.");
16
+ process.exit(1);
17
+ }
18
+ }
19
+ export function isClawhubAvailable() {
20
+ return hasClawhub();
21
+ }
22
+ export function installSkill(slug, workdir) {
23
+ try {
24
+ execSync(`clawhub install ${slug} --workdir "${workdir}" --no-input`, {
25
+ stdio: "inherit",
26
+ });
27
+ return true;
28
+ }
29
+ catch {
30
+ console.error(` Warning: failed to install skill "${slug}"`);
31
+ return false;
32
+ }
33
+ }
34
+ export function updateAllSkills(workdir) {
35
+ try {
36
+ execSync(`clawhub update --all --workdir "${workdir}" --no-input`, {
37
+ stdio: "inherit",
38
+ });
39
+ return true;
40
+ }
41
+ catch {
42
+ console.error(` Warning: failed to update skills in ${workdir}`);
43
+ return false;
44
+ }
45
+ }
@@ -0,0 +1,34 @@
1
+ export type AgentEntry = {
2
+ id: string;
3
+ name?: string;
4
+ workspace?: string;
5
+ agentDir?: string;
6
+ model?: string;
7
+ skills?: string[];
8
+ [key: string]: unknown;
9
+ };
10
+ export type OpenClawConfig = {
11
+ agents?: {
12
+ list?: AgentEntry[];
13
+ defaults?: Record<string, unknown>;
14
+ [key: string]: unknown;
15
+ };
16
+ bindings?: Array<{
17
+ agentId: string;
18
+ [key: string]: unknown;
19
+ }>;
20
+ tools?: {
21
+ agentToAgent?: {
22
+ allow?: string[];
23
+ [key: string]: unknown;
24
+ };
25
+ [key: string]: unknown;
26
+ };
27
+ [key: string]: unknown;
28
+ };
29
+ export declare function readConfig(): OpenClawConfig;
30
+ export declare function writeConfig(cfg: OpenClawConfig): void;
31
+ export declare function findAgentEntry(cfg: OpenClawConfig, agentId: string): AgentEntry | undefined;
32
+ export declare function findAgentIndex(cfg: OpenClawConfig, agentId: string): number;
33
+ export declare function addOrUpdateAgent(cfg: OpenClawConfig, entry: AgentEntry): OpenClawConfig;
34
+ export declare function removeAgent(cfg: OpenClawConfig, agentId: string): OpenClawConfig;
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs";
2
+ import { resolveConfigPath } from "./paths.js";
3
+ export function readConfig() {
4
+ const configPath = resolveConfigPath();
5
+ if (!fs.existsSync(configPath))
6
+ return {};
7
+ const raw = fs.readFileSync(configPath, "utf-8");
8
+ // JSON5-like: strip comments, trailing commas (basic approach)
9
+ try {
10
+ return JSON.parse(raw);
11
+ }
12
+ catch {
13
+ // Try stripping single-line comments and trailing commas
14
+ const cleaned = raw
15
+ .replace(/\/\/.*$/gm, "")
16
+ .replace(/\/\*[\s\S]*?\*\//g, "")
17
+ .replace(/,(\s*[}\]])/g, "$1");
18
+ return JSON.parse(cleaned);
19
+ }
20
+ }
21
+ export function writeConfig(cfg) {
22
+ const configPath = resolveConfigPath();
23
+ fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
24
+ }
25
+ export function findAgentEntry(cfg, agentId) {
26
+ const id = agentId.toLowerCase();
27
+ return cfg.agents?.list?.find((e) => e.id?.toLowerCase() === id);
28
+ }
29
+ export function findAgentIndex(cfg, agentId) {
30
+ const id = agentId.toLowerCase();
31
+ return cfg.agents?.list?.findIndex((e) => e.id?.toLowerCase() === id) ?? -1;
32
+ }
33
+ export function addOrUpdateAgent(cfg, entry) {
34
+ const list = [...(cfg.agents?.list ?? [])];
35
+ const idx = findAgentIndex(cfg, entry.id);
36
+ if (idx >= 0) {
37
+ list[idx] = { ...list[idx], ...entry };
38
+ }
39
+ else {
40
+ list.push(entry);
41
+ }
42
+ return { ...cfg, agents: { ...cfg.agents, list } };
43
+ }
44
+ export function removeAgent(cfg, agentId) {
45
+ const id = agentId.toLowerCase();
46
+ const list = (cfg.agents?.list ?? []).filter((e) => e.id?.toLowerCase() !== id);
47
+ const bindings = (cfg.bindings ?? []).filter((b) => b.agentId?.toLowerCase() !== id);
48
+ const allow = (cfg.tools?.agentToAgent?.allow ?? []).filter((a) => a.toLowerCase() !== id);
49
+ return {
50
+ ...cfg,
51
+ agents: { ...cfg.agents, list: list.length > 0 ? list : undefined },
52
+ bindings: bindings.length > 0 ? bindings : undefined,
53
+ tools: cfg.tools
54
+ ? {
55
+ ...cfg.tools,
56
+ agentToAgent: cfg.tools.agentToAgent
57
+ ? { ...cfg.tools.agentToAgent, allow: allow.length > 0 ? allow : undefined }
58
+ : undefined,
59
+ }
60
+ : undefined,
61
+ };
62
+ }
@@ -0,0 +1,26 @@
1
+ export type CatalogAgent = {
2
+ version: string;
3
+ name: string;
4
+ emoji: string;
5
+ category: string;
6
+ skillCount: number;
7
+ };
8
+ export type Catalog = {
9
+ catalogVersion: number;
10
+ updatedAt: string;
11
+ agents: Record<string, CatalogAgent>;
12
+ };
13
+ export type AgentManifest = {
14
+ id: string;
15
+ version: string;
16
+ name: string;
17
+ emoji: string;
18
+ model: string;
19
+ category: string;
20
+ minOpenClawVersion: string;
21
+ skills: string[];
22
+ files: string[];
23
+ };
24
+ export declare function fetchCatalog(): Promise<Catalog>;
25
+ export declare function fetchManifest(agentId: string): Promise<AgentManifest>;
26
+ export declare function fetchAgentFile(agentId: string, filename: string): Promise<string>;
@@ -0,0 +1,29 @@
1
+ const DEFAULT_REGISTRY = "https://app.storyclaw.com/agents";
2
+ function registryUrl(filePath) {
3
+ const base = process.env.TALENTHUB_REGISTRY?.trim() || DEFAULT_REGISTRY;
4
+ return `${base.replace(/\/$/, "")}/${filePath}`;
5
+ }
6
+ export async function fetchCatalog() {
7
+ const url = registryUrl("catalog.json");
8
+ const res = await fetch(url);
9
+ if (!res.ok) {
10
+ throw new Error(`Failed to fetch catalog: ${res.status} ${res.statusText}`);
11
+ }
12
+ return res.json();
13
+ }
14
+ export async function fetchManifest(agentId) {
15
+ const url = registryUrl(`${agentId}/manifest.json`);
16
+ const res = await fetch(url);
17
+ if (!res.ok) {
18
+ throw new Error(`Agent "${agentId}" not found in registry (${res.status})`);
19
+ }
20
+ return res.json();
21
+ }
22
+ export async function fetchAgentFile(agentId, filename) {
23
+ const url = registryUrl(`${agentId}/${filename}`);
24
+ const res = await fetch(url);
25
+ if (!res.ok) {
26
+ throw new Error(`Failed to fetch ${filename} for agent "${agentId}" (${res.status})`);
27
+ }
28
+ return res.text();
29
+ }
@@ -0,0 +1,4 @@
1
+ export declare function resolveStateDir(): string;
2
+ export declare function resolveConfigPath(): string;
3
+ export declare function resolveWorkspaceDir(agentId: string): string;
4
+ export declare function resolveTalentHubStatePath(): string;
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ function resolveHomeDir() {
5
+ return process.env.OPENCLAW_HOME?.trim() || os.homedir();
6
+ }
7
+ export function resolveStateDir() {
8
+ const override = process.env.OPENCLAW_STATE_DIR?.trim();
9
+ if (override) {
10
+ return path.resolve(override.replace(/^~/, resolveHomeDir()));
11
+ }
12
+ const home = resolveHomeDir();
13
+ const newDir = path.join(home, ".openclaw");
14
+ if (fs.existsSync(newDir))
15
+ return newDir;
16
+ const legacy = path.join(home, ".clawdbot");
17
+ if (fs.existsSync(legacy))
18
+ return legacy;
19
+ return newDir;
20
+ }
21
+ export function resolveConfigPath() {
22
+ const override = process.env.OPENCLAW_CONFIG_PATH?.trim();
23
+ if (override) {
24
+ return path.resolve(override.replace(/^~/, resolveHomeDir()));
25
+ }
26
+ const stateDir = resolveStateDir();
27
+ const candidates = ["openclaw.json", "clawdbot.json"];
28
+ for (const name of candidates) {
29
+ const p = path.join(stateDir, name);
30
+ if (fs.existsSync(p))
31
+ return p;
32
+ }
33
+ return path.join(stateDir, "openclaw.json");
34
+ }
35
+ export function resolveWorkspaceDir(agentId) {
36
+ const stateDir = resolveStateDir();
37
+ if (agentId === "main") {
38
+ return process.env.OPENCLAW_WORKSPACE?.trim()
39
+ ? path.resolve(process.env.OPENCLAW_WORKSPACE.replace(/^~/, resolveHomeDir()))
40
+ : path.join(stateDir, "workspace");
41
+ }
42
+ return path.join(stateDir, `workspace-${agentId}`);
43
+ }
44
+ export function resolveTalentHubStatePath() {
45
+ return path.join(resolveStateDir(), "talenthub.json");
46
+ }
@@ -0,0 +1,31 @@
1
+ export type CatalogAgent = {
2
+ version: string;
3
+ name: string;
4
+ emoji: string;
5
+ category: string;
6
+ role: string;
7
+ tagline: string;
8
+ skillCount: number;
9
+ };
10
+ export type Catalog = {
11
+ catalogVersion: number;
12
+ updatedAt: string;
13
+ agents: Record<string, CatalogAgent>;
14
+ };
15
+ export type AgentManifest = {
16
+ id: string;
17
+ version: string;
18
+ name: string;
19
+ emoji: string;
20
+ model: string;
21
+ category: string;
22
+ role: string;
23
+ tagline: string;
24
+ description: string;
25
+ minOpenClawVersion: string;
26
+ skills: string[];
27
+ avatarUrl: string | null;
28
+ files: Record<string, string>;
29
+ };
30
+ export declare function fetchCatalog(): Promise<Catalog>;
31
+ export declare function fetchManifest(agentId: string): Promise<AgentManifest>;
@@ -0,0 +1,21 @@
1
+ import { getRegistryBaseUrl } from "./auth.js";
2
+ function apiUrl(path) {
3
+ const base = getRegistryBaseUrl().replace(/\/$/, "");
4
+ return `${base}/api/talent/registry${path}`;
5
+ }
6
+ export async function fetchCatalog() {
7
+ const url = apiUrl("/catalog");
8
+ const res = await fetch(url);
9
+ if (!res.ok) {
10
+ throw new Error(`Failed to fetch catalog: ${res.status} ${res.statusText}`);
11
+ }
12
+ return res.json();
13
+ }
14
+ export async function fetchManifest(agentId) {
15
+ const url = apiUrl(`/${agentId}`);
16
+ const res = await fetch(url);
17
+ if (!res.ok) {
18
+ throw new Error(`Agent "${agentId}" not found in registry (${res.status})`);
19
+ }
20
+ return res.json();
21
+ }
@@ -0,0 +1,12 @@
1
+ export type InstalledAgent = {
2
+ version: string;
3
+ installedAt: string;
4
+ };
5
+ export type TalentHubState = {
6
+ agents: Record<string, InstalledAgent>;
7
+ lastUpdateCheck?: string;
8
+ };
9
+ export declare function readState(): TalentHubState;
10
+ export declare function writeState(state: TalentHubState): void;
11
+ export declare function markInstalled(agentId: string, version: string): void;
12
+ export declare function markUninstalled(agentId: string): void;
@@ -0,0 +1,31 @@
1
+ import fs from "node:fs";
2
+ import { resolveTalentHubStatePath } from "./paths.js";
3
+ export function readState() {
4
+ const statePath = resolveTalentHubStatePath();
5
+ if (!fs.existsSync(statePath)) {
6
+ return { agents: {} };
7
+ }
8
+ try {
9
+ return JSON.parse(fs.readFileSync(statePath, "utf-8"));
10
+ }
11
+ catch {
12
+ return { agents: {} };
13
+ }
14
+ }
15
+ export function writeState(state) {
16
+ const statePath = resolveTalentHubStatePath();
17
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
18
+ }
19
+ export function markInstalled(agentId, version) {
20
+ const state = readState();
21
+ state.agents[agentId] = {
22
+ version,
23
+ installedAt: new Date().toISOString(),
24
+ };
25
+ writeState(state);
26
+ }
27
+ export function markUninstalled(agentId) {
28
+ const state = readState();
29
+ delete state.agents[agentId];
30
+ writeState(state);
31
+ }
@@ -0,0 +1,9 @@
1
+ import { type Catalog } from "./registry.js";
2
+ export type UpdateInfo = {
3
+ agentId: string;
4
+ currentVersion: string;
5
+ latestVersion: string;
6
+ name: string;
7
+ };
8
+ export declare function getCatalogCached(): Promise<Catalog>;
9
+ export declare function checkUpdates(): Promise<UpdateInfo[]>;
@@ -0,0 +1,34 @@
1
+ import { fetchCatalog } from "./registry.js";
2
+ import { readState, writeState } from "./state.js";
3
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
4
+ export async function getCatalogCached() {
5
+ const state = readState();
6
+ const lastCheck = state.lastUpdateCheck ? new Date(state.lastUpdateCheck).getTime() : 0;
7
+ const now = Date.now();
8
+ if (now - lastCheck < CACHE_TTL_MS) {
9
+ // Within cache window; still fetch but don't worry about staleness
10
+ }
11
+ const catalog = await fetchCatalog();
12
+ state.lastUpdateCheck = new Date().toISOString();
13
+ writeState(state);
14
+ return catalog;
15
+ }
16
+ export async function checkUpdates() {
17
+ const state = readState();
18
+ const catalog = await getCatalogCached();
19
+ const updates = [];
20
+ for (const [agentId, installed] of Object.entries(state.agents)) {
21
+ const remote = catalog.agents[agentId];
22
+ if (!remote)
23
+ continue;
24
+ if (remote.version !== installed.version) {
25
+ updates.push({
26
+ agentId,
27
+ currentVersion: installed.version,
28
+ latestVersion: remote.version,
29
+ name: remote.name,
30
+ });
31
+ }
32
+ }
33
+ return updates;
34
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@storyclaw/talenthub",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to manage StoryClaw AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "talenthub": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest"
17
+ },
18
+ "dependencies": {
19
+ "commander": "^13.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.0.0",
23
+ "typescript": "^5.7.0",
24
+ "vitest": "^4.0.18"
25
+ },
26
+ "engines": {
27
+ "node": ">=22"
28
+ },
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/storyclaw-official/agents"
33
+ }
34
+ }