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 +114 -104
- package/dist/args.js +49 -0
- package/dist/client.js +87 -0
- package/dist/commands/ask.js +74 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/login.js +125 -0
- package/dist/commands/mcp.js +48 -0
- package/dist/commands/memory.js +61 -0
- package/dist/commands/routines.js +44 -0
- package/dist/commands/skills.js +183 -0
- package/dist/commands/whoami.js +29 -0
- package/dist/config.js +70 -0
- package/dist/format.js +54 -0
- package/dist/index.js +118 -151
- package/dist/install.js +253 -0
- package/dist/login.js +86 -0
- package/dist/mcp-proxy.js +17 -0
- package/dist/skill-files.js +109 -0
- package/package.json +13 -6
- package/dist/api.js +0 -209
package/README.md
CHANGED
|
@@ -1,127 +1,137 @@
|
|
|
1
1
|
# talentos
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
21
|
+
npx talentos@1 init
|
|
54
22
|
```
|
|
55
23
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
-e TALENTOS_API_KEY=tos_your_key_here \
|
|
66
|
-
-- npx -y talentos
|
|
52
|
+
npx talentos@1 login
|
|
67
53
|
```
|
|
68
54
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
83
|
+
talentos login [--key <key>] [--api-url <url>] [--name <device>]
|
|
84
|
+
talentos logout
|
|
85
|
+
talentos whoami
|
|
91
86
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
> a new draft workflow for candidate outreach.
|
|
123
|
+
## Config and environment
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
##
|
|
132
|
+
## Exit codes
|
|
118
133
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|