@storyclaw/talenthub 0.3.2 → 0.3.4

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,12 +16,18 @@ 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");
21
- program.command("login").description("Authenticate with StoryClaw").action(login);
25
+ .version(pkg.version);
26
+ program
27
+ .command("login")
28
+ .description("Authenticate with StoryClaw")
29
+ .option("-t, --token <token>", "Authenticate directly with an sc_token")
30
+ .action(login);
22
31
  program.command("logout").description("Remove stored credentials").action(logout);
23
32
  const agent = program.command("agent").description("Agent management commands");
24
33
  agent
@@ -1 +1,3 @@
1
- export declare function login(): Promise<void>;
1
+ export declare function login(options: {
2
+ token?: string;
3
+ }): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
- import { readAuth, requestDeviceCode, pollForToken, writeAuth } from "../lib/auth.js";
2
+ import { readAuth, requestDeviceCode, pollForToken, exchangeToken, writeAuth } from "../lib/auth.js";
3
3
  function openUrl(url) {
4
4
  try {
5
5
  const cmd = process.platform === "darwin"
@@ -13,7 +13,22 @@ function openUrl(url) {
13
13
  // Browser open is best-effort
14
14
  }
15
15
  }
16
- export async function login() {
16
+ export async function login(options) {
17
+ // Direct token exchange: skip device-code flow entirely
18
+ if (options.token) {
19
+ console.log("Exchanging token...");
20
+ try {
21
+ const { access_token, user_id, expires_at } = await exchangeToken(options.token);
22
+ writeAuth({ token: access_token, user_id, expires_at });
23
+ console.log(`✓ Logged in successfully.`);
24
+ console.log(` User ID: ${user_id}`);
25
+ }
26
+ catch (err) {
27
+ console.error(`✗ Login failed: ${err instanceof Error ? err.message : err}`);
28
+ process.exit(1);
29
+ }
30
+ return;
31
+ }
17
32
  const existing = readAuth();
18
33
  if (existing) {
19
34
  console.log("Already logged in.");
@@ -20,4 +20,8 @@ export type TokenResponse = {
20
20
  user_id: string;
21
21
  expires_at: string;
22
22
  };
23
+ /**
24
+ * Exchange an sc_token (web session cookie) for a CLI-compatible th_* token.
25
+ */
26
+ export declare function exchangeToken(scToken: string): Promise<TokenResponse>;
23
27
  export declare function pollForToken(deviceCode: string, interval: number, maxWaitMs: number): Promise<TokenResponse>;
package/dist/lib/auth.js CHANGED
@@ -48,6 +48,22 @@ export async function requestDeviceCode() {
48
48
  }
49
49
  return res.json();
50
50
  }
51
+ /**
52
+ * Exchange an sc_token (web session cookie) for a CLI-compatible th_* token.
53
+ */
54
+ export async function exchangeToken(scToken) {
55
+ const base = getRegistryBaseUrl();
56
+ const res = await fetchRetry(`${base}/api/talenthub/auth/token-exchange`, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify({ token: scToken }),
60
+ });
61
+ if (!res.ok) {
62
+ const body = await res.json().catch(() => ({ error: "unknown" }));
63
+ throw new Error(`Token exchange failed (${res.status}): ${body.error ?? "unknown error"}`);
64
+ }
65
+ return res.json();
66
+ }
51
67
  export async function pollForToken(deviceCode, interval, maxWaitMs) {
52
68
  const base = getRegistryBaseUrl();
53
69
  const deadline = Date.now() + maxWaitMs;
@@ -1,12 +1,19 @@
1
- import { getRegistryBaseUrl } from "./auth.js";
1
+ import { getRegistryBaseUrl, readAuth } from "./auth.js";
2
2
  import { fetchRetry } from "./fetch.js";
3
3
  function apiUrl(path) {
4
4
  const base = getRegistryBaseUrl().replace(/\/$/, "");
5
5
  return `${base}/api/talenthub/registry${path}`;
6
6
  }
7
+ function authHeaders() {
8
+ const auth = readAuth();
9
+ if (auth?.token) {
10
+ return { Authorization: `Bearer ${auth.token}` };
11
+ }
12
+ return {};
13
+ }
7
14
  export async function fetchCatalog() {
8
15
  const url = apiUrl("/catalog");
9
- const res = await fetchRetry(url);
16
+ const res = await fetchRetry(url, { headers: authHeaders() });
10
17
  if (!res.ok) {
11
18
  throw new Error(`Failed to fetch catalog: ${res.status} ${res.statusText}`);
12
19
  }
@@ -14,7 +21,7 @@ export async function fetchCatalog() {
14
21
  }
15
22
  export async function fetchManifest(agentId) {
16
23
  const url = apiUrl(`/${agentId}`);
17
- const res = await fetchRetry(url);
24
+ const res = await fetchRetry(url, { headers: authHeaders() });
18
25
  if (!res.ok) {
19
26
  throw new Error(`Agent "${agentId}" not found in registry (${res.status})`);
20
27
  }
@@ -3,7 +3,9 @@ export type SkillSpec = {
3
3
  skill: string;
4
4
  };
5
5
  /**
6
- * Parse a fully-qualified skill string ("owner/repo@skill") into its parts.
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.
7
9
  * Returns undefined if the string is not in the expected format.
8
10
  */
9
11
  export declare function parseSkillSpec(entry: string): SkillSpec | undefined;
@@ -22,7 +24,7 @@ export declare function isSkillInstalled(name: string): boolean;
22
24
  * Install a single skill via `skills add` into the shared directory,
23
25
  * then symlink it into the agent workspace.
24
26
  *
25
- * @param entry Fully-qualified skill string ("owner/repo@skill")
27
+ * @param entry Skill URL string ("https://github.com/owner/repo@skill")
26
28
  * @param workspaceDir Agent workspace directory
27
29
  * Returns true on success, false on failure (logged, non-fatal).
28
30
  */
@@ -32,7 +34,7 @@ export declare function installSkill(entry: string, workspaceDir: string): boole
32
34
  * missing skills grouped by repo (one clone per repo), then symlink
33
35
  * everything into the workspace.
34
36
  *
35
- * @param skills Array of fully-qualified skill strings ("owner/repo@skill")
37
+ * @param skills Array of skill URL strings ("https://github.com/owner/repo@skill")
36
38
  * Returns { installed, skipped, failed } counts.
37
39
  */
38
40
  export declare function installAllSkills(skills: string[], workspaceDir: string): {
@@ -20,7 +20,9 @@ function resolveSkillsBin() {
20
20
  }
21
21
  const SKILLS_BIN = resolveSkillsBin();
22
22
  /**
23
- * Parse a fully-qualified skill string ("owner/repo@skill") into its parts.
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.
24
26
  * Returns undefined if the string is not in the expected format.
25
27
  */
26
28
  export function parseSkillSpec(entry) {
@@ -80,14 +82,14 @@ function installSkillsFromRepo(repo, skillNames) {
80
82
  * Install a single skill via `skills add` into the shared directory,
81
83
  * then symlink it into the agent workspace.
82
84
  *
83
- * @param entry Fully-qualified skill string ("owner/repo@skill")
85
+ * @param entry Skill URL string ("https://github.com/owner/repo@skill")
84
86
  * @param workspaceDir Agent workspace directory
85
87
  * Returns true on success, false on failure (logged, non-fatal).
86
88
  */
87
89
  export function installSkill(entry, workspaceDir) {
88
90
  const spec = parseSkillSpec(entry);
89
91
  if (!spec) {
90
- console.error(` Warning: invalid skill spec "${entry}" — expected "owner/repo@skill"`);
92
+ console.error(` Warning: invalid skill spec "${entry}" — expected "https://github.com/owner/repo@skill"`);
91
93
  return false;
92
94
  }
93
95
  if (!isSkillInstalled(spec.skill)) {
@@ -129,7 +131,7 @@ function linkSkillToWorkspace(name, workspaceDir) {
129
131
  * missing skills grouped by repo (one clone per repo), then symlink
130
132
  * everything into the workspace.
131
133
  *
132
- * @param skills Array of fully-qualified skill strings ("owner/repo@skill")
134
+ * @param skills Array of skill URL strings ("https://github.com/owner/repo@skill")
133
135
  * Returns { installed, skipped, failed } counts.
134
136
  */
135
137
  export function installAllSkills(skills, workspaceDir) {
@@ -142,7 +144,7 @@ export function installAllSkills(skills, workspaceDir) {
142
144
  for (const entry of skills) {
143
145
  const spec = parseSkillSpec(entry);
144
146
  if (!spec) {
145
- console.error(` Warning: invalid skill spec "${entry}" — expected "owner/repo@skill"`);
147
+ console.error(` Warning: invalid skill spec "${entry}" — expected "https://github.com/owner/repo@skill"`);
146
148
  failed++;
147
149
  continue;
148
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyclaw/talenthub",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "CLI tool to manage StoryClaw AI agents",
5
5
  "type": "module",
6
6
  "bin": {