talentos 0.3.0 → 1.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/README.md CHANGED
@@ -1,126 +1,168 @@
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 its own private Skills 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_skill`** (`{ name, summary, instructions, successCriteria?,
24
- requestedTools? }`) — create a new Skill from plain-text instructions.
25
- Returns the new `skillId` and its version. The Skill is private to the API
26
- key's owner, who curates it in their TalentOS Skills library.
27
- - **`update_skill`** (`{ skillId, name, summary, instructions,
28
- successCriteria?, requestedTools? }`) — replace one of the API key owner's
29
- private Skills with a complete new body. Only that owner's private Skills are
30
- writable; published and shared Skills stay human-owned.
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.
19
+
20
+ Release 1.1.0 includes Rooms agent linking, the gateway, and the `create_deck`
21
+ MCP permission. `talentos@1` picks it up automatically after publication.
51
22
 
52
23
  ```bash
53
- npx -y talentos
24
+ npx talentos@1 init
54
25
  ```
55
26
 
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.3.0` — before wiring it into an offline or CI environment.
27
+ `init` is the whole setup in one command. It:
28
+
29
+ 1. **Signs you in** — skipped when the key already on this machine still works.
30
+ 2. **Detects your coding agents** — Claude Code (the `claude` command on your
31
+ PATH, or `~/.claude`), Cursor (`~/.cursor` or `./.cursor`), Codex (`~/.codex`).
32
+ `--for claude|cursor|codex|all` overrides detection.
33
+ 3. **Writes the MCP config** for each of them, at user level (see the table below).
34
+ 4. **Pulls the company's skills** into `./.agents/skills` — the folder Cursor and
35
+ Codex auto-load — plus `./.claude/skills` when Claude Code is here. `--no-pull`
36
+ skips this.
37
+
38
+ Then ask your agent to call the `talentos` `whoami` tool. Re-running `init` is
39
+ safe: the config entry is replaced (so a new key takes effect) and skills are
40
+ overwritten by slug.
60
41
 
61
- ## Add it to Claude Code
42
+ | agent | what `init` writes |
43
+ | --- | --- |
44
+ | Claude Code | `claude mcp add --scope user --transport http talentos <url> --header ...` (prints the command if `claude` is not on your PATH) |
45
+ | Cursor | `~/.cursor/mcp.json` → `mcpServers.talentos` (other servers kept, mode 0600) |
46
+ | Codex | `~/.codex/config.toml` → `[mcp_servers.talentos]`, the stdio proxy carrying `TALENTOS_API_KEY` and `TALENTOS_API_URL` (mode 0600) |
47
+
48
+ **Nothing is written inside the repo except pulled skills.** Your API key lives in
49
+ `~/.config/talentos/config.json` and in those user-level agent configs, never in
50
+ a file your team could check in.
51
+
52
+ ## Login on its own
62
53
 
63
54
  ```bash
64
- claude mcp add talentos \
65
- -e TALENTOS_API_KEY=tos_your_key_here \
66
- -- npx -y talentos
55
+ npx talentos@1 login
67
56
  ```
68
57
 
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
- }
58
+ `login` mints an API key **on your machine**, registers only its SHA-256 hash,
59
+ and asks you to approve the device in TalentOS. The raw key is written to
60
+ `~/.config/talentos/config.json` (mode 0600) and never leaves the machine.
61
+
62
+ It prints an eight-character code and, when the server returns an approval link,
63
+ opens it. Otherwise open TalentOS yourself, go to **Settings → Agents & CLI**, and
64
+ enter the code there.
65
+
66
+ Already have a key? `npx talentos@1 login --key tos_...` — it is checked for shape
67
+ before it is sent, and stored only once TalentOS accepts it.
68
+
69
+ ## Connect one agent by hand
70
+
71
+ `talentos mcp install <client>` prints the snippet for your client; add
72
+ `--apply` to write it for you (the same user-level write `init` does):
73
+
74
+ | client | what you get |
75
+ | --- | --- |
76
+ | `claude` | a `claude mcp add --scope user --transport http` command |
77
+ | `cursor` | the `~/.cursor/mcp.json` entry |
78
+ | `codex` | the `~/.codex/config.toml` entry (stdio, via `npx -y talentos@1 mcp`, with the API url in its env) |
79
+ | `json` | a generic `mcpServers` entry (print only) |
80
+
81
+ ## Commands
82
+
83
+ ### Agents in Rooms
84
+
85
+ `talentos agent serve --agent <id>` runs the linked provider in its private
86
+ work folder. It keeps one turn active, resumes the same thread from
87
+ `~/.talentos/agents/<id>/sessions.json`, and posts final text automatically.
88
+ The service heartbeats every 20 seconds and stops the child if its lease is
89
+ lost. A required CLI update installs under the agent directory between turns.
90
+ Signing-key provisioning, real staging latency and provider resume proofs are
91
+ tracked in [Agent access](../../docs/features/agent-access/README.md).
92
+
93
+ Use the setup prompt from TalentOS to link Claude Code or Codex. The fragment
94
+ token is private; never paste it into logs or commits. Provider CLI login is
95
+ required even when its desktop app is signed in. Approve the user service
96
+ installation interactively or pass `--yes` after approval.
97
+
98
+ Linking waits for a recent gateway heartbeat before reporting Connected. Real
99
+ provider connection, latency and resume proofs remain staged after publication.
100
+ If the service cannot start, use the printed foreground
101
+ `talentos agent serve --agent <id>` command.
102
+
103
+ `agent status` and `agent list` show agents linked on this machine. Select one
104
+ with `--agent <id>` for `agent logs` or `agent unlink`. Unlink disconnects the
105
+ agent remotely and removes its local service and configuration.
106
+
107
+ Re-running the same link repairs the install and reuses its key. Keep the
108
+ private pending receipt if a response is lost; it permits recovery after the
109
+ one-use link has been consumed.
110
+
88
111
  ```
112
+ talentos init [--for claude|cursor|codex|all] [--no-pull] [--name <device>]
113
+
114
+ talentos login [--key <key>] [--api-url <url>] [--name <device>]
115
+ talentos logout
116
+ talentos whoami
89
117
 
90
- ## Five-minute smoke test
118
+ talentos mcp # stdio proxy to the remote server
119
+ talentos mcp install <claude|cursor|codex|json> [--apply]
91
120
 
92
- 1. **Create a key.** In the TalentOS admin UI, create an API key. Copy the
93
- `tos_...` value (you only see it once).
121
+ talentos skills list [--mine|--company] [-q <query>]
122
+ talentos skills get <id|slug> [--out <dir>] # prints SKILL.md, or writes the package
123
+ talentos skills push <SKILL.md|dir> [--id <skillId>]
124
+ talentos skills pull [--for claude|cursor|codex|all] [--global] [--to <dir>] [--mine|--company]
125
+ talentos skills share <id> # refuses: Share this Skill from TalentOS.
126
+
127
+ talentos memory search "<query>" [--personal|--company]
128
+ talentos memory add "<fact>" [--company]
129
+
130
+ talentos ask "<message>" [--thread <id>] [--no-wait]
131
+ talentos routines list
132
+ talentos routines run <id>
133
+ ```
94
134
 
95
- 2. **Curl the list endpoint** to confirm the key and URL work before involving an
96
- agent:
135
+ `skills pull` writes to `./.agents/skills` by default. `--for claude` writes to
136
+ `./.claude/skills`, `--for all` writes both, and `--global` puts the same folders
137
+ under your home directory; an explicit `--to` wins over all of them.
97
138
 
98
- ```bash
99
- curl -H "Authorization: Bearer tos_your_key_here" \
100
- https://your-deployment.convex.site/api/v0/workflows
101
- ```
139
+ One pull fetches at most 100 skills; if it hits that ceiling it says so on stderr
140
+ — narrow it with `--mine`, `--company` or `-q <query>`. Two skills that share a
141
+ slug both land: the second goes to `<slug>-<last 6 of its id>`.
102
142
 
103
- You should get `{ "workflows": [ ... ] }`. A `401` means the key is wrong;
104
- make sure you used the `.convex.site` domain, not `.convex.cloud`.
143
+ After `skills push`, `skills get`, `ask` and `routines run`, the last line is
144
+ `Open: <url>` — the deep link to that skill or thread in TalentOS.
105
145
 
106
- 3. **Add the server** to your MCP client using a snippet above, with your real
107
- key (the API URL defaults to production).
146
+ Every command takes `--json` to print the raw structured result instead of text.
147
+ `talentos --help` and `talentos <group> --help` print usage.
108
148
 
109
- 4. **Ask the agent** to list your workflows, then create a Skill:
149
+ A skill package is a `SKILL.md` (YAML frontmatter with `name` and `description`,
150
+ body = the instructions) plus any supporting files next to it. `push` accepts the
151
+ file or the folder that contains it; a folder is walked recursively, skipping
152
+ `node_modules` and `.git`, capped at 100 text files and 512 KiB.
110
153
 
111
- > List my TalentOS workflows, show me the first one as markdown, then create
112
- > a Skill for candidate outreach.
154
+ ## Config and environment
113
155
 
114
- The agent calls `list_workflows`, `get_workflow`, and `create_skill`. The new
115
- Skill lands as a private Skill in TalentOS for its owner to review.
156
+ | | |
157
+ | --- | --- |
158
+ | `~/.config/talentos/config.json` | `{ apiUrl, apiKey }`, mode 0600 (honours `XDG_CONFIG_HOME`) |
159
+ | `TALENTOS_API_KEY` | overrides the stored key |
160
+ | `TALENTOS_API_URL` | overrides the stored API url (default `https://woozy-oyster-769.convex.site`) |
161
+ | `--api-url` | overrides both, for one command |
116
162
 
117
- ## Errors you might see
163
+ ## Exit codes
118
164
 
119
- - **Unauthorized (401)** — `TALENTOS_API_KEY` is missing, wrong, or revoked.
120
- - **Not found (404)** — the workflow or skill `id` doesn't exist, or it is not
121
- visible to this API key. `update_skill` only reaches the key owner's own
122
- private Skills.
123
- - **Rate limited (429)** — the key is over its requests-per-minute budget;
124
- wait a moment and retry.
125
- - **Could not reach the API** — `TALENTOS_API_URL` is wrong or unreachable;
126
- confirm it is the `.convex.site` URL.
165
+ | code | meaning |
166
+ | --- | --- |
167
+ | 0 | success |
168
+ | 1 | any failure — one line on stderr, nothing on stdout |
@@ -0,0 +1,260 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { rm } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ import { writePrivateFile } from "../agent-config.js";
8
+ import { redactAgentOutput, } from "../agent-link.js";
9
+ const CHILD_ENV = [
10
+ "HOME",
11
+ "PATH",
12
+ "USER",
13
+ "LOGNAME",
14
+ "LANG",
15
+ "LC_ALL",
16
+ "TMPDIR",
17
+ "TEMP",
18
+ "TMP",
19
+ "SystemRoot",
20
+ "XDG_CONFIG_HOME",
21
+ "XDG_CACHE_HOME",
22
+ "CODEX_HOME",
23
+ "CLAUDE_CONFIG_DIR",
24
+ ];
25
+ const CLAUDE_VERSION = /^\d+\.\d+\.\d+/u;
26
+ const DONT_ASK = /\bdontAsk\b/u;
27
+ const CLAUDE_TOOLS = [
28
+ "read_room",
29
+ "list_room_files",
30
+ "read_file",
31
+ "suggest_change",
32
+ "create_file",
33
+ "create_deck",
34
+ "post_message",
35
+ "share_link",
36
+ ]
37
+ .map((tool) => `mcp__talentos__${tool}`)
38
+ .join(",");
39
+ export function childEnvironment(source = process.env) {
40
+ return Object.fromEntries(CHILD_ENV.filter((key) => source[key] !== undefined).map((key) => [
41
+ key,
42
+ source[key],
43
+ ]));
44
+ }
45
+ export async function verifyClaudeCode() {
46
+ const execute = promisify(execFile);
47
+ const options = {
48
+ env: childEnvironment(),
49
+ timeout: 5000,
50
+ maxBuffer: 256_000,
51
+ };
52
+ let version = "unknown";
53
+ try {
54
+ const detected = await execute("claude", ["--version"], options);
55
+ version = CLAUDE_VERSION.exec(detected.stdout.trim())?.[0] ?? "unknown";
56
+ const help = await execute("claude", ["--help"], options);
57
+ if (version !== "unknown" && DONT_ASK.test(help.stdout)) {
58
+ return;
59
+ }
60
+ }
61
+ catch {
62
+ // Never include provider stderr, which may contain login details.
63
+ }
64
+ throw new Error(`Update Claude Code (${version}) to a version that supports --permission-mode dontAsk.`);
65
+ }
66
+ export function readProviderEvent(provider, value) {
67
+ if (!value || typeof value !== "object") {
68
+ return {};
69
+ }
70
+ const event = value;
71
+ if (provider === "claude_code") {
72
+ if (event.type === "result") {
73
+ if (event.is_error) {
74
+ return { failed: true };
75
+ }
76
+ return {
77
+ ...(typeof event.session_id === "string"
78
+ ? { sessionId: event.session_id }
79
+ : {}),
80
+ ...(typeof event.result === "string" ? { text: event.result } : {}),
81
+ };
82
+ }
83
+ const message = event.message;
84
+ if (event.type === "assistant" &&
85
+ Array.isArray(message?.content) &&
86
+ message.content.some((part) => part.type === "tool_use")) {
87
+ return { status: "Using room tools…" };
88
+ }
89
+ }
90
+ else {
91
+ if (event.type === "thread.started" &&
92
+ typeof event.thread_id === "string") {
93
+ return { sessionId: event.thread_id };
94
+ }
95
+ if (event.type === "turn.failed" || event.type === "error") {
96
+ return { failed: true };
97
+ }
98
+ const item = event.item;
99
+ if (event.type === "item.completed" &&
100
+ item?.type === "agent_message" &&
101
+ typeof item.text === "string") {
102
+ return { text: item.text };
103
+ }
104
+ if (event.type === "item.started" && item?.type === "mcp_tool_call") {
105
+ return { status: "Using room tools…" };
106
+ }
107
+ }
108
+ return {};
109
+ }
110
+ export async function runProvider(input) {
111
+ const { config, job, workDir, sessionId, signal } = input;
112
+ const prompt = JSON.stringify(job.packet), mcpPath = join(dirname(workDir), ".talentos-mcp.json");
113
+ let command, args;
114
+ if (config.provider === "claude_code") {
115
+ await writePrivateFile(mcpPath, JSON.stringify({
116
+ mcpServers: {
117
+ talentos: {
118
+ type: "http",
119
+ url: new URL("/mcp", config.apiUrl).href,
120
+ headers: { Authorization: `Bearer ${config.apiKey}` },
121
+ },
122
+ },
123
+ }));
124
+ command = "claude";
125
+ args = [
126
+ "-p",
127
+ prompt,
128
+ "--output-format",
129
+ "stream-json",
130
+ "--verbose",
131
+ "--mcp-config",
132
+ mcpPath,
133
+ "--strict-mcp-config",
134
+ "--append-system-prompt",
135
+ job.packet.instructions,
136
+ "--tools",
137
+ "",
138
+ "--allowedTools",
139
+ CLAUDE_TOOLS,
140
+ "--disallowedTools",
141
+ "Bash,Agent,Task",
142
+ "--permission-mode",
143
+ "dontAsk",
144
+ ];
145
+ if (sessionId) {
146
+ args.push("--resume", sessionId);
147
+ }
148
+ }
149
+ else {
150
+ command = "codex";
151
+ const entry = fileURLToPath(new URL("./mcp-entry.js", import.meta.url));
152
+ // Resume has a different flag surface. Root overrides are inherited by
153
+ // both exec paths; shell_tool=false enforces ADR 0093's default no-Bash.
154
+ args = [
155
+ "-c",
156
+ 'sandbox_mode="workspace-write"',
157
+ "-c",
158
+ 'approval_policy="never"',
159
+ "-c",
160
+ "features.shell_tool=false",
161
+ "-c",
162
+ "features.unified_exec=false",
163
+ "-c",
164
+ `mcp_servers.talentos.command=${JSON.stringify(process.execPath)}`,
165
+ "-c",
166
+ `mcp_servers.talentos.args=${JSON.stringify([entry, config.agentId])}`,
167
+ "exec",
168
+ ];
169
+ if (sessionId) {
170
+ args.push("resume", sessionId);
171
+ }
172
+ args.push("--json", "--skip-git-repo-check", "--ignore-user-config", "--ignore-rules", prompt);
173
+ }
174
+ const child = spawn(command, args, {
175
+ cwd: workDir,
176
+ env: childEnvironment(),
177
+ stdio: ["ignore", "pipe", "ignore"],
178
+ detached: process.platform !== "win32",
179
+ });
180
+ let killTimer;
181
+ const stop = () => {
182
+ const pid = child.pid;
183
+ if (!pid || child.exitCode !== null || child.signalCode !== null) {
184
+ return;
185
+ }
186
+ const kill = (signal) => {
187
+ try {
188
+ if (process.platform === "win32") {
189
+ child.kill(signal);
190
+ }
191
+ else {
192
+ process.kill(-pid, signal);
193
+ }
194
+ }
195
+ catch {
196
+ /* The process group may have already exited. */
197
+ }
198
+ };
199
+ kill("SIGTERM");
200
+ killTimer ??= setTimeout(() => kill("SIGKILL"), 5000);
201
+ };
202
+ const ended = new Promise((resolve) => {
203
+ child.once("error", () => resolve(null));
204
+ child.once("close", resolve);
205
+ });
206
+ signal.addEventListener("abort", stop, { once: true });
207
+ if (signal.aborted) {
208
+ stop();
209
+ }
210
+ try {
211
+ let text = "", failed = false, sawResult = false;
212
+ if (!child.stdout) {
213
+ throw new Error("Provider output is unavailable.");
214
+ }
215
+ const lines = createInterface({ input: child.stdout });
216
+ for await (const line of lines) {
217
+ if (line.length > 1_000_000) {
218
+ throw new Error("Provider output exceeded its limit.");
219
+ }
220
+ let value;
221
+ try {
222
+ value = JSON.parse(line);
223
+ }
224
+ catch {
225
+ continue;
226
+ }
227
+ const event = readProviderEvent(config.provider, value);
228
+ if (event.failed) {
229
+ failed = true;
230
+ }
231
+ if (event.sessionId) {
232
+ await input.remember(event.sessionId);
233
+ }
234
+ if (event.status) {
235
+ input.status(event.status);
236
+ }
237
+ if (event.text !== undefined) {
238
+ text = event.text.slice(0, 12_000);
239
+ }
240
+ const type = value?.type;
241
+ if (type === "result" || type === "turn.completed") {
242
+ sawResult = true;
243
+ }
244
+ }
245
+ const code = await ended;
246
+ if (code !== 0 || failed || !sawResult || signal.aborted) {
247
+ throw new Error("Provider could not finish this turn. Check its login and quota.");
248
+ }
249
+ return { text: redactAgentOutput(text, [config.apiKey]) };
250
+ }
251
+ finally {
252
+ signal.removeEventListener("abort", stop);
253
+ stop();
254
+ await ended;
255
+ if (killTimer) {
256
+ clearTimeout(killTimer);
257
+ }
258
+ await rm(mcpPath, { force: true });
259
+ }
260
+ }
@@ -0,0 +1,70 @@
1
+ import { ConvexClient } from "convex/browser";
2
+ import { makeFunctionReference } from "convex/server";
3
+ import { isSecureUrl } from "../agent-link.js";
4
+ export class GatewayError extends Error {
5
+ status;
6
+ constructor(status) {
7
+ super(status === 401 || status === 409
8
+ ? "Agent is no longer available. Re-link it from TalentOS."
9
+ : "Agent gateway request failed; retrying.");
10
+ this.status = status;
11
+ }
12
+ }
13
+ export class AgentGateway {
14
+ config;
15
+ constructor(config) {
16
+ if (!isSecureUrl(new URL(config.apiUrl))) {
17
+ throw new Error("Agent gateway needs HTTPS or localhost.");
18
+ }
19
+ this.config = config;
20
+ }
21
+ async request(path, body = {}, signal) {
22
+ const response = await fetch(new URL(path, this.config.apiUrl), {
23
+ method: "POST",
24
+ headers: {
25
+ Authorization: `Bearer ${this.config.apiKey}`,
26
+ "Content-Type": "application/json",
27
+ },
28
+ body: JSON.stringify(body),
29
+ signal: signal
30
+ ? AbortSignal.any([signal, AbortSignal.timeout(15_000)])
31
+ : AbortSignal.timeout(15_000),
32
+ redirect: "error",
33
+ });
34
+ if (!response.ok) {
35
+ throw new GatewayError(response.status);
36
+ }
37
+ return response.status === 204 ? null : (await response.json());
38
+ }
39
+ async subscribe(wake, fallback) {
40
+ const initial = await this.request("/agent-gateway/token");
41
+ if (!(initial && isSecureUrl(new URL(initial.convexUrl)))) {
42
+ throw new Error("Agent push is unavailable.");
43
+ }
44
+ const client = new ConvexClient(initial.convexUrl, { logger: false });
45
+ let cached = initial;
46
+ client.setAuth(async ({ forceRefreshToken }) => {
47
+ if (forceRefreshToken || cached.expiresAt - Date.now() < 60_000) {
48
+ const renewed = await this.request("/agent-gateway/token");
49
+ if (!renewed) {
50
+ return null;
51
+ }
52
+ cached = renewed;
53
+ }
54
+ return cached.token;
55
+ }, (authenticated) => {
56
+ if (!authenticated) {
57
+ fallback();
58
+ }
59
+ });
60
+ const unsubscribe = client.onUpdate(makeFunctionReference("externalAgents/jobs:pendingForAgent"), {}, (count) => {
61
+ if (count > 0) {
62
+ wake();
63
+ }
64
+ }, () => fallback());
65
+ return async () => {
66
+ unsubscribe();
67
+ await client.close();
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,10 @@
1
+ import { localAgents } from "../agent-config.js";
2
+ import { AgentClient } from "../client.js";
3
+ import { startMcpProxy } from "../mcp-proxy.js";
4
+ // A local stdio entry avoids putting the bearer key in Codex's argv or child
5
+ // environment. Only the MCP proxy reads the linked-agent credential file.
6
+ const config = (await localAgents()).find((agent) => agent.agentId === process.argv[2]);
7
+ if (!config) {
8
+ throw new Error("Linked agent configuration is unavailable.");
9
+ }
10
+ await startMcpProxy(new AgentClient({ apiUrl: config.apiUrl, apiKey: config.apiKey }));