talentos 0.2.0 → 1.0.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/README.md CHANGED
@@ -1,127 +1,137 @@
1
1
  # talentos
2
2
 
3
- A thin [stdio MCP](https://modelcontextprotocol.io) server that wraps the TalentOS
4
- external HTTP API (`/api/v0/*`). Point any MCP-capable agent (Claude Code, Claude
5
- Desktop, Cursor, etc.) at it and the agent can read your org's workflows and
6
- nodes, then create or replace workflow drafts through the same API key.
7
-
8
- Published to npm as [`talentos`](https://www.npmjs.com/package/talentos) — run it
9
- with `npx -y talentos`, no checkout or build required.
10
-
11
- It exposes six tools:
12
-
13
- - **`get_context`** (no arguments) — one compact org summary: company profile
14
- basics plus the names of the org's published workflows and nodes.
15
- Good first call to prime an agent.
16
- - **`list_workflows`** (no arguments) — published workflows plus drafts created
17
- by the API key's owner as a compact text list (status, title, id, step count,
18
- department, description, ISO `updatedAt`).
19
- - **`get_workflow`** (`{ workflowId: string }`) — one workflow as an
20
- agent-ready Markdown runbook, including its `**Version:**` stamp.
21
- - **`list_nodes`** (no arguments) — company-wide reusable nodes as name, id,
22
- step type, summary, and tools.
23
- - **`create_workflow`** (`{ title, steps, connections }`) — create a new draft
24
- workflow from a full plan. Steps may include `outputs`, `successCriteria`, and
25
- `runtime` contract fields. Writes always create drafts; a human publishes in
26
- the app.
27
- - **`update_workflow`** (`{ workflowId, title, steps, connections }`) —
28
- replace an existing draft workflow from a full plan. Steps may include
29
- `outputs`, `successCriteria`, and `runtime`; published workflows return a
30
- clear conflict instead of being edited.
31
-
32
- The server holds no state and stores nothing. Every call is a single
33
- authenticated request to the HTTP API; all authorization and visibility rules
34
- live server-side.
35
-
36
- ## Configuration
37
-
38
- Only `TALENTOS_API_KEY` is required — the server exits immediately with a message
39
- on stderr if it's missing. `TALENTOS_API_URL` is optional: it defaults to the
40
- production TalentOS deployment, so most users never set it.
41
-
42
- | Variable | Required | Description |
43
- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
44
- | `TALENTOS_API_KEY` | yes | An API key you create in the TalentOS admin UI. Sent as `Authorization: Bearer <key>`. Never logged. |
45
- | `TALENTOS_API_URL` | no | Override the API base URL to target a non-production deployment. Defaults to the production `https://<name>.convex.site` origin. Must be the `.convex.site` domain, **not** `.convex.cloud`. |
46
-
47
- ## Install
48
-
49
- Nothing to install ahead of time — `npx` fetches the published package on first
50
- run and caches it:
3
+ The TalentOS CLI. It connects the coding agent on your machine to your team's
4
+ governed brain, for four jobs:
5
+
6
+ 1. **Push** a skill your agent wrote into TalentOS, then share it with the company.
7
+ 2. **Pull** the company's skills into your agent's skill folder.
8
+ 3. **Memory** — read and write what you and the team know.
9
+ 4. **Delegate** work to your Talent so it runs inside TalentOS governance
10
+ (connectors, approvals, audit) and reports back.
11
+
12
+ It is an MCP client for the remote TalentOS server. `talentos mcp` also runs that
13
+ same server over stdio for clients that cannot speak streamable HTTP.
14
+
15
+ ## Quick start
16
+
17
+ Every command below pins `talentos@1`, the major that speaks the hosted MCP endpoint.
18
+ The `0.x` releases are the retired stdio wrapper and no longer work.
51
19
 
52
20
  ```bash
53
- npx -y talentos
21
+ npx talentos@1 init
54
22
  ```
55
23
 
56
- The `-y` skips the install prompt so noninteractive MCP clients don't hang. The
57
- first launch does a one-time network fetch to populate the npx cache (then exits
58
- asking for `TALENTOS_API_KEY`); run it once by hand — or pin a version like
59
- `talentos@0.2.0` — before wiring it into an offline or CI environment.
24
+ `init` is the whole setup in one command. It:
25
+
26
+ 1. **Signs you in** — skipped when the key already on this machine still works.
27
+ 2. **Detects your coding agents** — Claude Code (the `claude` command on your
28
+ PATH, or `~/.claude`), Cursor (`~/.cursor` or `./.cursor`), Codex (`~/.codex`).
29
+ `--for claude|cursor|codex|all` overrides detection.
30
+ 3. **Writes the MCP config** for each of them, at user level (see the table below).
31
+ 4. **Pulls the company's skills** into `./.agents/skills` — the folder Cursor and
32
+ Codex auto-load — plus `./.claude/skills` when Claude Code is here. `--no-pull`
33
+ skips this.
60
34
 
61
- ## Add it to Claude Code
35
+ Then ask your agent to call the `talentos` `whoami` tool. Re-running `init` is
36
+ safe: the config entry is replaced (so a new key takes effect) and skills are
37
+ overwritten by slug.
38
+
39
+ | agent | what `init` writes |
40
+ | --- | --- |
41
+ | Claude Code | `claude mcp add --scope user --transport http talentos <url> --header ...` (prints the command if `claude` is not on your PATH) |
42
+ | Cursor | `~/.cursor/mcp.json` → `mcpServers.talentos` (other servers kept, mode 0600) |
43
+ | Codex | `~/.codex/config.toml` → `[mcp_servers.talentos]`, the stdio proxy carrying `TALENTOS_API_KEY` and `TALENTOS_API_URL` (mode 0600) |
44
+
45
+ **Nothing is written inside the repo except pulled skills.** Your API key lives in
46
+ `~/.config/talentos/config.json` and in those user-level agent configs, never in
47
+ a file your team could check in.
48
+
49
+ ## Login on its own
62
50
 
63
51
  ```bash
64
- claude mcp add talentos \
65
- -e TALENTOS_API_KEY=tos_your_key_here \
66
- -- npx -y talentos
52
+ npx talentos@1 login
67
53
  ```
68
54
 
69
- Then ask Claude Code to *"list our TalentOS workflows and show me one as
70
- markdown"* — it calls `list_workflows`, picks an id, and calls `get_workflow`.
71
-
72
- ## Add it to any other MCP client
73
-
74
- Add this to your MCP client config (`.mcp.json`, Claude Desktop config, etc.):
75
-
76
- ```json
77
- {
78
- "mcpServers": {
79
- "talentos": {
80
- "command": "npx",
81
- "args": ["-y", "talentos"],
82
- "env": {
83
- "TALENTOS_API_KEY": "tos_your_key_here"
84
- }
85
- }
86
- }
87
- }
55
+ `login` mints an API key **on your machine**, registers only its SHA-256 hash,
56
+ and asks you to approve the device in TalentOS. The raw key is written to
57
+ `~/.config/talentos/config.json` (mode 0600) and never leaves the machine.
58
+
59
+ It prints an eight-character code and, when the server returns an approval link,
60
+ opens it. Otherwise open TalentOS yourself, go to **Settings → Agents & CLI**, and
61
+ enter the code there.
62
+
63
+ Already have a key? `npx talentos@1 login --key tos_...` — it is checked for shape
64
+ before it is sent, and stored only once TalentOS accepts it.
65
+
66
+ ## Connect one agent by hand
67
+
68
+ `talentos mcp install <client>` prints the snippet for your client; add
69
+ `--apply` to write it for you (the same user-level write `init` does):
70
+
71
+ | client | what you get |
72
+ | --- | --- |
73
+ | `claude` | a `claude mcp add --scope user --transport http` command |
74
+ | `cursor` | the `~/.cursor/mcp.json` entry |
75
+ | `codex` | the `~/.codex/config.toml` entry (stdio, via `npx -y talentos@1 mcp`, with the API url in its env) |
76
+ | `json` | a generic `mcpServers` entry (print only) |
77
+
78
+ ## Commands
79
+
88
80
  ```
81
+ talentos init [--for claude|cursor|codex|all] [--no-pull] [--name <device>]
89
82
 
90
- ## Five-minute smoke test
83
+ talentos login [--key <key>] [--api-url <url>] [--name <device>]
84
+ talentos logout
85
+ talentos whoami
91
86
 
92
- 1. **Create a key.** In the TalentOS admin UI, create an API key. Copy the
93
- `tos_...` value (you only see it once).
87
+ talentos mcp # stdio proxy to the remote server
88
+ talentos mcp install <claude|cursor|codex|json> [--apply]
89
+
90
+ talentos skills list [--mine|--company] [-q <query>]
91
+ talentos skills get <id|slug> [--out <dir>] # prints SKILL.md, or writes the package
92
+ talentos skills push <SKILL.md|dir> [--id <skillId>]
93
+ talentos skills pull [--for claude|cursor|codex|all] [--global] [--to <dir>] [--mine|--company]
94
+ talentos skills share <id> # refuses: Share this Skill from TalentOS.
95
+
96
+ talentos memory search "<query>" [--personal|--company]
97
+ talentos memory add "<fact>" [--company]
98
+
99
+ talentos ask "<message>" [--thread <id>] [--no-wait]
100
+ talentos routines list
101
+ talentos routines run <id>
102
+ ```
94
103
 
95
- 2. **Curl the list endpoint** to confirm the key and URL work before involving an
96
- agent:
104
+ `skills pull` writes to `./.agents/skills` by default. `--for claude` writes to
105
+ `./.claude/skills`, `--for all` writes both, and `--global` puts the same folders
106
+ under your home directory; an explicit `--to` wins over all of them.
97
107
 
98
- ```bash
99
- curl -H "Authorization: Bearer tos_your_key_here" \
100
- https://your-deployment.convex.site/api/v0/workflows
101
- ```
108
+ One pull fetches at most 100 skills; if it hits that ceiling it says so on stderr
109
+ — narrow it with `--mine`, `--company` or `-q <query>`. Two skills that share a
110
+ slug both land: the second goes to `<slug>-<last 6 of its id>`.
102
111
 
103
- You should get `{ "workflows": [ ... ] }`. A `401` means the key is wrong;
104
- make sure you used the `.convex.site` domain, not `.convex.cloud`.
112
+ After `skills push`, `skills get`, `ask` and `routines run`, the last line is
113
+ `Open: <url>` — the deep link to that skill or thread in TalentOS.
105
114
 
106
- 3. **Add the server** to your MCP client using a snippet above, with your real
107
- key (the API URL defaults to production).
115
+ Every command takes `--json` to print the raw structured result instead of text.
116
+ `talentos --help` and `talentos <group> --help` print usage.
108
117
 
109
- 4. **Ask the agent** to list your workflows, then create a draft:
118
+ A skill package is a `SKILL.md` (YAML frontmatter with `name` and `description`,
119
+ body = the instructions) plus any supporting files next to it. `push` accepts the
120
+ file or the folder that contains it; a folder is walked recursively, skipping
121
+ `node_modules` and `.git`, capped at 100 text files and 512 KiB.
110
122
 
111
- > List my TalentOS workflows, show me the first one as markdown, then create
112
- > a new draft workflow for candidate outreach.
123
+ ## Config and environment
113
124
 
114
- The agent calls `list_workflows`, `get_workflow`, and `create_workflow`. The
115
- new workflow lands as a draft in TalentOS for a human to review and publish.
125
+ | | |
126
+ | --- | --- |
127
+ | `~/.config/talentos/config.json` | `{ apiUrl, apiKey }`, mode 0600 (honours `XDG_CONFIG_HOME`) |
128
+ | `TALENTOS_API_KEY` | overrides the stored key |
129
+ | `TALENTOS_API_URL` | overrides the stored API url (default `https://woozy-oyster-769.convex.site`) |
130
+ | `--api-url` | overrides both, for one command |
116
131
 
117
- ## Errors you might see
132
+ ## Exit codes
118
133
 
119
- - **Unauthorized (401)** — `TALENTOS_API_KEY` is missing, wrong, or revoked.
120
- - **Workflow not found (404)** — the `id` doesn't exist or it is not visible to
121
- this API key.
122
- - **Published workflow conflict (409)** — `update_workflow` only edits drafts;
123
- published workflows must be duplicated or edited in TalentOS.
124
- - **Rate limited (429)** — the key is over its requests-per-minute budget;
125
- wait a moment and retry.
126
- - **Could not reach the API** — `TALENTOS_API_URL` is wrong or unreachable;
127
- confirm it is the `.convex.site` URL.
134
+ | code | meaning |
135
+ | --- | --- |
136
+ | 0 | success |
137
+ | 1 | any failure — one line on stderr, nothing on stdout |
package/dist/args.js ADDED
@@ -0,0 +1,49 @@
1
+ import { parseArgs } from "node:util";
2
+ const CLI_OPTIONS = {
3
+ json: { type: "boolean" },
4
+ help: { type: "boolean", short: "h" },
5
+ version: { type: "boolean", short: "v" },
6
+ key: { type: "string" },
7
+ "api-url": { type: "string" },
8
+ name: { type: "string" },
9
+ out: { type: "string" },
10
+ to: { type: "string" },
11
+ id: { type: "string" },
12
+ mine: { type: "boolean" },
13
+ company: { type: "boolean" },
14
+ personal: { type: "boolean" },
15
+ query: { type: "string", short: "q" },
16
+ thread: { type: "string" },
17
+ "no-wait": { type: "boolean" },
18
+ for: { type: "string" },
19
+ global: { type: "boolean" },
20
+ apply: { type: "boolean" },
21
+ "no-pull": { type: "boolean" },
22
+ };
23
+ /** Groups whose first positional is a sub-command rather than an argument. */
24
+ const ACTION_GROUPS = new Set(["mcp", "skills", "memory", "routines"]);
25
+ export function parseArgv(argv) {
26
+ const { values, positionals } = parse(argv);
27
+ const group = positionals[0];
28
+ const hasAction = group !== undefined && ACTION_GROUPS.has(group);
29
+ const action = hasAction ? positionals[1] : undefined;
30
+ return {
31
+ group,
32
+ action,
33
+ args: positionals.slice(action === undefined ? 1 : 2),
34
+ options: values,
35
+ };
36
+ }
37
+ function parse(argv) {
38
+ try {
39
+ return parseArgs({
40
+ args: argv,
41
+ options: CLI_OPTIONS,
42
+ allowPositionals: true,
43
+ });
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message.split("\n")[0] : String(error);
47
+ throw new Error(`${message} Run \`talentos --help\`.`);
48
+ }
49
+ }
package/dist/client.js ADDED
@@ -0,0 +1,87 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { requireCredentials, resolveApiUrl, } from "./config.js";
4
+ const CLIENT_INFO = { name: "talentos-cli", version: "1.0.0" };
5
+ const UNAUTHORIZED_HINT = /\b401\b|unauthoriz/i;
6
+ const UNREACHABLE_HINT = /fetch failed|ECONNREFUSED|ENOTFOUND/i;
7
+ export const UNAUTHORIZED = "Not logged in or key revoked. Run `talentos login`.";
8
+ /** MCP client for the remote TalentOS server, one process-lived connection. */
9
+ export class AgentClient {
10
+ credentials;
11
+ client;
12
+ constructor(credentials) {
13
+ this.credentials = credentials;
14
+ }
15
+ async call(name, args) {
16
+ const client = await this.connect();
17
+ const result = (await withFriendlyErrors(() => client.callTool({ name, arguments: args })));
18
+ const text = firstText(result);
19
+ if (result.isError) {
20
+ throw new Error(text || `Tool ${name} failed.`);
21
+ }
22
+ return { text, data: result.structuredContent };
23
+ }
24
+ async listTools() {
25
+ const client = await this.connect();
26
+ return await withFriendlyErrors(() => client.listTools());
27
+ }
28
+ async callTool(params) {
29
+ const client = await this.connect();
30
+ return (await withFriendlyErrors(() => client.callTool(params)));
31
+ }
32
+ async close() {
33
+ const client = this.client;
34
+ this.client = undefined;
35
+ await client?.close();
36
+ }
37
+ async connect() {
38
+ if (this.client) {
39
+ return this.client;
40
+ }
41
+ const client = new Client(CLIENT_INFO, { capabilities: {} });
42
+ const transport = new StreamableHTTPClientTransport(new URL(`${this.credentials.apiUrl}/mcp`), {
43
+ requestInit: {
44
+ headers: { Authorization: `Bearer ${this.credentials.apiKey}` },
45
+ },
46
+ });
47
+ await withFriendlyErrors(() => client.connect(transport));
48
+ this.client = client;
49
+ return client;
50
+ }
51
+ }
52
+ export async function withClient(apiUrlOverride, run) {
53
+ const credentials = await requireCredentials();
54
+ const client = new AgentClient({
55
+ ...credentials,
56
+ apiUrl: resolveApiUrl(apiUrlOverride, credentials.apiUrl),
57
+ });
58
+ try {
59
+ return await run(client);
60
+ }
61
+ finally {
62
+ await client.close();
63
+ }
64
+ }
65
+ function firstText(result) {
66
+ const block = result.content?.[0];
67
+ return block && block.type === "text" ? block.text : "";
68
+ }
69
+ async function withFriendlyErrors(run) {
70
+ try {
71
+ return await run();
72
+ }
73
+ catch (error) {
74
+ throw new Error(describe(error));
75
+ }
76
+ }
77
+ function describe(error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ const code = error?.code;
80
+ if (code === 401 || UNAUTHORIZED_HINT.test(message)) {
81
+ return UNAUTHORIZED;
82
+ }
83
+ if (UNREACHABLE_HINT.test(message)) {
84
+ return "Cannot reach TalentOS. Check your connection or --api-url.";
85
+ }
86
+ return message;
87
+ }
@@ -0,0 +1,74 @@
1
+ import { withClient } from "../client.js";
2
+ import { dim, emit, emitJson, emitLink } from "../format.js";
3
+ const POLL_INTERVAL_MS = 2000;
4
+ const MAX_WAIT_MS = 10 * 60 * 1000;
5
+ // The two non-terminal webChannelMessages statuses; anything else means the
6
+ // Talent is done with that message.
7
+ const IN_FLIGHT_STATUSES = new Set(["queued", "claimed"]);
8
+ export function isTerminalStatus(status) {
9
+ return !IN_FLIGHT_STATUSES.has(status);
10
+ }
11
+ export function replyAfter(messages, messageId) {
12
+ const index = messages.findIndex((message) => message.id === messageId);
13
+ const candidates = messages
14
+ .slice(index + 1)
15
+ .filter((message) => message.role === "talent");
16
+ return candidates.at(-1);
17
+ }
18
+ export async function runAsk(parsed) {
19
+ const message = parsed.args.join(" ").trim();
20
+ if (message === "") {
21
+ throw new Error('Usage: talentos ask "<message>" [--thread <id>] [--no-wait]');
22
+ }
23
+ await withClient(parsed.options["api-url"], async (client) => {
24
+ const { data } = await client.call("ask_talent", {
25
+ message,
26
+ ...(parsed.options.thread ? { threadId: parsed.options.thread } : {}),
27
+ });
28
+ const json = parsed.options.json === true;
29
+ if (parsed.options["no-wait"]) {
30
+ reportHandoff(data.threadId, json, data.url);
31
+ return;
32
+ }
33
+ emit(dim(`Asked your Talent. Thread ${data.threadId}`));
34
+ const reply = await waitForReply(client, data.threadId, data.messageId);
35
+ if (!reply) {
36
+ reportHandoff(data.threadId, json, data.url);
37
+ return;
38
+ }
39
+ if (json) {
40
+ emitJson({ threadId: data.threadId, message: reply });
41
+ return;
42
+ }
43
+ emit(reply.body);
44
+ emitLink(data.url);
45
+ });
46
+ }
47
+ async function waitForReply(client, threadId, messageId) {
48
+ const deadline = Date.now() + MAX_WAIT_MS;
49
+ while (Date.now() < deadline) {
50
+ await sleep(POLL_INTERVAL_MS);
51
+ const { data } = await client.call("get_thread", { threadId });
52
+ const reply = replyAfter(data.messages, messageId);
53
+ if (reply && isTerminalStatus(reply.status)) {
54
+ return reply;
55
+ }
56
+ if (!data.active && reply) {
57
+ return reply;
58
+ }
59
+ }
60
+ return;
61
+ }
62
+ function reportHandoff(threadId, json, url) {
63
+ if (json) {
64
+ emitJson({ threadId });
65
+ return;
66
+ }
67
+ emit(`Your Talent is still working. Open thread ${threadId} in TalentOS for the reply, or keep the conversation going with \`talentos ask "<message>" --thread ${threadId}\`.`);
68
+ emitLink(url);
69
+ }
70
+ function sleep(ms) {
71
+ return new Promise((resolve) => {
72
+ setTimeout(resolve, ms);
73
+ });
74
+ }
@@ -0,0 +1,98 @@
1
+ import { withClient } from "../client.js";
2
+ import { bold, dim, emit } from "../format.js";
3
+ import { agentLabel, agentTargetsFor, applyInstall, buildInstallSnippet, defaultProbe, detectAgents, homeDir, pullDirs, } from "../install.js";
4
+ import { authorize, currentSession } from "./login.js";
5
+ import { pullInto } from "./skills.js";
6
+ import { memberLabel } from "./whoami.js";
7
+ const DONE = "✓";
8
+ const SKIPPED = "·";
9
+ const TRAILING_PUNCTUATION = /[.:\s]+$/;
10
+ /**
11
+ * The one command a new member runs: sign in, configure every coding agent on
12
+ * this machine at user level, and drop the company's skills where those agents
13
+ * already look for them.
14
+ */
15
+ export async function runInit(parsed) {
16
+ const session = await signIn(parsed);
17
+ const targets = chosenTargets(parsed.options.for);
18
+ if (targets.length === 0) {
19
+ step(false, "No coding agent found. Re-run with --for claude|cursor|codex.");
20
+ }
21
+ const configured = [];
22
+ for (const target of targets) {
23
+ if (await configure(target, session.credentials)) {
24
+ configured.push(target);
25
+ }
26
+ }
27
+ const pulled = parsed.options["no-pull"]
28
+ ? undefined
29
+ : await pullShared(parsed, targets);
30
+ emit(summary(configured, pulled));
31
+ }
32
+ async function signIn(parsed) {
33
+ const stored = await currentSession(parsed);
34
+ const session = stored ?? (await authorize(parsed));
35
+ const member = memberLabel(session.who.member);
36
+ const organization = session.who.organization.name;
37
+ step(true, `${stored ? "Already signed in" : "Signed in"} as ${member} · ${organization}`);
38
+ return { credentials: session.credentials, member, organization };
39
+ }
40
+ function chosenTargets(forOption) {
41
+ return forOption === undefined
42
+ ? detectAgents(defaultProbe())
43
+ : agentTargetsFor(forOption);
44
+ }
45
+ async function configure(target, credentials) {
46
+ const label = agentLabel(target);
47
+ const snippet = () => buildInstallSnippet(target, credentials);
48
+ try {
49
+ const outcome = await applyInstall(target, credentials);
50
+ if (outcome.applied) {
51
+ step(true, `${label} → ${outcome.detail}`);
52
+ return true;
53
+ }
54
+ emit(manualInstallNotice(label, outcome.detail, snippet()));
55
+ }
56
+ catch (error) {
57
+ emit(manualInstallNotice(label, messageOf(error), snippet()));
58
+ }
59
+ return false;
60
+ }
61
+ /** Every failure ends the same way: why, then the command to run by hand. */
62
+ export function manualInstallNotice(label, reason, snippet) {
63
+ const why = reason.replace(TRAILING_PUNCTUATION, "");
64
+ return `${SKIPPED} ${label}: ${why}. Run it yourself:\n ${snippet}`;
65
+ }
66
+ /**
67
+ * Cursor and Codex both auto-load `./.agents/skills`; Claude Code needs its own
68
+ * folder as well. Both live in the working tree, the only place `init` writes
69
+ * inside a repo.
70
+ */
71
+ async function pullShared(parsed, targets) {
72
+ const dirs = pullDirs({
73
+ home: homeDir(),
74
+ target: targets.includes("claude") ? "all" : undefined,
75
+ });
76
+ const pulled = await withClient(parsed.options["api-url"], (client) => pullInto(client, dirs, "company"));
77
+ step(pulled.length > 0, pulled.length === 0
78
+ ? "No company skills to pull yet."
79
+ : `${pulled.length} skill(s) → ${dirs.join(", ")}`);
80
+ return pulled.length;
81
+ }
82
+ function summary(configured, pulled) {
83
+ const agents = configured.length === 0 ? "none" : configured.map(agentLabel).join(", ");
84
+ return [
85
+ "",
86
+ bold("TalentOS is connected."),
87
+ `Agents configured: ${agents}`,
88
+ `Skills written: ${pulled ?? "skipped (--no-pull)"}`,
89
+ dim("Next: ask your agent to call the talentos whoami tool."),
90
+ ].join("\n");
91
+ }
92
+ function step(done, text) {
93
+ emit(`${done ? DONE : SKIPPED} ${text}`);
94
+ }
95
+ function messageOf(error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ return message.split("\n")[0] ?? message;
98
+ }