talentos 1.0.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 +31 -0
- package/dist/agent/adapters.js +260 -0
- package/dist/agent/gateway.js +70 -0
- package/dist/agent/mcp-entry.js +10 -0
- package/dist/agent/serve.js +269 -0
- package/dist/agent/sessions.js +36 -0
- package/dist/agent-config.js +105 -0
- package/dist/agent-http.js +176 -0
- package/dist/agent-link.js +49 -0
- package/dist/agent-service.js +145 -0
- package/dist/args.js +4 -1
- package/dist/commands/agent-serve.js +42 -0
- package/dist/commands/agent.js +234 -0
- package/dist/index.js +23 -1
- package/package.json +2 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { chmod, mkdir, open, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { agentConfigPath, agentLogPath, agentsDir, agentWorkDir, isMissing, localAgents, storeAgent, writePrivateFile, } from "../agent-config.js";
|
|
6
|
+
import { AgentHttpError, fetchAgentStatus, inspectProvider, redeemAgent, setupApiOrigin, unlinkRemoteAgent, waitForHeartbeat, } from "../agent-http.js";
|
|
7
|
+
import { parseAgentLink, redactAgentOutput } from "../agent-link.js";
|
|
8
|
+
import { installAgentService, uninstallAgentService, } from "../agent-service.js";
|
|
9
|
+
import { emit, emitJson, renderTable, warn } from "../format.js";
|
|
10
|
+
import { homeDir } from "../install.js";
|
|
11
|
+
import { defaultDeviceName, hashKey, mintKey } from "../login.js";
|
|
12
|
+
export async function runAgent(parsed) {
|
|
13
|
+
switch (parsed.action) {
|
|
14
|
+
case "link": {
|
|
15
|
+
if (!(parsed.options.yes || (await approveInstall()))) {
|
|
16
|
+
throw new Error("Link cancelled. Re-run with --yes after approving the user service installation.");
|
|
17
|
+
}
|
|
18
|
+
const { agent, service } = await installLinkedAgent(parsed);
|
|
19
|
+
emit(`Linked ${redactAgentOutput(agent.name)} (${agent.agentId}). Waiting for agent service…`);
|
|
20
|
+
if (!service.started) {
|
|
21
|
+
warn(`Start it in the foreground: npx --yes talentos@1 agent serve --agent ${agent.agentId}`);
|
|
22
|
+
}
|
|
23
|
+
const status = await waitForHeartbeat(agent);
|
|
24
|
+
if (!status) {
|
|
25
|
+
throw new Error(`Still waiting for agent service (${agent.agentId}). Check talentos agent logs or start talentos agent serve --agent ${agent.agentId}; then run talentos agent status.`);
|
|
26
|
+
}
|
|
27
|
+
emit(`Connected as ${redactAgentOutput(status.name)}.`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
case "serve": {
|
|
31
|
+
let command;
|
|
32
|
+
try {
|
|
33
|
+
// S6 supplies this module; S3 can install the service before it ships.
|
|
34
|
+
command = await import(new URL("./agent-serve.js", import.meta.url).href);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error("Waiting for agent service: this CLI release does not include the gateway yet. Install the published S6 release, then re-run agent link.");
|
|
38
|
+
}
|
|
39
|
+
return await command.runAgentServe(parsed);
|
|
40
|
+
}
|
|
41
|
+
case "status":
|
|
42
|
+
case "list":
|
|
43
|
+
return await showAgents(parsed);
|
|
44
|
+
case "unlink":
|
|
45
|
+
return await unlinkLocal(parsed);
|
|
46
|
+
case "logs":
|
|
47
|
+
return await showLogs(parsed);
|
|
48
|
+
default:
|
|
49
|
+
throw new Error("Use talentos agent link | status | list | unlink | logs.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
53
|
+
if (Number(process.versions.node.split(".")[0]) < 20) {
|
|
54
|
+
throw new Error("Agent linking requires Node.js 20 or newer.");
|
|
55
|
+
}
|
|
56
|
+
const link = parseAgentLink(parsed.options.url ?? "");
|
|
57
|
+
const home = dependencies.home ?? homeDir();
|
|
58
|
+
const linkHash = hashKey(link.token);
|
|
59
|
+
let agent = (await localAgents(home)).find((item) => item.linkHash === linkHash && item.provider === link.provider);
|
|
60
|
+
if (agent) {
|
|
61
|
+
await storeAgent(agent, home);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const apiUrl = await setupApiOrigin(link, parsed.options["api-url"] ?? process.env.TALENTOS_API_URL);
|
|
65
|
+
const pendingPath = join(agentsDir(home), `.pending-${linkHash}.json`);
|
|
66
|
+
let apiKey;
|
|
67
|
+
let recovered;
|
|
68
|
+
try {
|
|
69
|
+
const pending = JSON.parse(await readFile(pendingPath, "utf8"));
|
|
70
|
+
if (!pending ||
|
|
71
|
+
typeof pending !== "object" ||
|
|
72
|
+
!("apiKey" in pending &&
|
|
73
|
+
"apiUrl" in pending &&
|
|
74
|
+
"provider" in pending) ||
|
|
75
|
+
typeof pending.apiKey !== "string" ||
|
|
76
|
+
!pending.apiKey.startsWith("tos_") ||
|
|
77
|
+
pending.apiUrl !== apiUrl ||
|
|
78
|
+
pending.provider !== link.provider) {
|
|
79
|
+
throw new Error("The pending agent link is invalid. Ask for a fresh setup prompt.");
|
|
80
|
+
}
|
|
81
|
+
apiKey = pending.apiKey;
|
|
82
|
+
try {
|
|
83
|
+
recovered = await fetchAgentStatus({
|
|
84
|
+
apiKey,
|
|
85
|
+
apiUrl,
|
|
86
|
+
provider: link.provider,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!(error instanceof AgentHttpError && error.status === 401)) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (!isMissing(error)) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
apiKey = mintKey().rawKey;
|
|
100
|
+
await mkdir(agentsDir(home), { recursive: true, mode: 0o700 });
|
|
101
|
+
await chmod(agentsDir(home), 0o700);
|
|
102
|
+
await writePrivateFile(pendingPath, `${JSON.stringify({ apiKey, apiUrl, provider: link.provider })}\n`);
|
|
103
|
+
}
|
|
104
|
+
const identity = recovered ??
|
|
105
|
+
(await redeemAgent(apiUrl, {
|
|
106
|
+
token: link.token,
|
|
107
|
+
keyHash: hashKey(apiKey),
|
|
108
|
+
provider: link.provider,
|
|
109
|
+
providerVersion: (dependencies.probe ?? inspectProvider)(link.provider),
|
|
110
|
+
deviceName: parsed.options.name ?? defaultDeviceName(),
|
|
111
|
+
os: process.platform,
|
|
112
|
+
cliVersion: packageVersion(),
|
|
113
|
+
}));
|
|
114
|
+
agent = {
|
|
115
|
+
agentId: identity.agentId,
|
|
116
|
+
name: identity.name,
|
|
117
|
+
provider: link.provider,
|
|
118
|
+
apiKey,
|
|
119
|
+
apiUrl,
|
|
120
|
+
linkHash,
|
|
121
|
+
linkedAt: Date.now(),
|
|
122
|
+
};
|
|
123
|
+
await storeAgent(agent, home);
|
|
124
|
+
await rm(pendingPath, { force: true });
|
|
125
|
+
}
|
|
126
|
+
const service = await (dependencies.install ?? ((config) => installAgentService(config, { home })))(agent);
|
|
127
|
+
return { agent, service };
|
|
128
|
+
}
|
|
129
|
+
async function showAgents(parsed) {
|
|
130
|
+
const agents = await selectAgents(parsed);
|
|
131
|
+
const rows = [];
|
|
132
|
+
for (const agent of agents) {
|
|
133
|
+
try {
|
|
134
|
+
const status = await fetchAgentStatus(agent);
|
|
135
|
+
rows.push({
|
|
136
|
+
agentId: status.agentId,
|
|
137
|
+
name: redactAgentOutput(status.name),
|
|
138
|
+
provider: status.provider,
|
|
139
|
+
state: status.presence,
|
|
140
|
+
lastSeenAt: status.lastSeenAt,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
rows.push({
|
|
145
|
+
agentId: agent.agentId,
|
|
146
|
+
name: redactAgentOutput(agent.name),
|
|
147
|
+
provider: agent.provider,
|
|
148
|
+
state: error instanceof AgentHttpError && error.status === 401
|
|
149
|
+
? "disconnected"
|
|
150
|
+
: "unreachable",
|
|
151
|
+
lastSeenAt: null,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (parsed.options.json) {
|
|
156
|
+
emitJson(rows);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
emit(rows.length
|
|
160
|
+
? renderTable(["Agent", "Provider", "Status", "ID"], rows.map((row) => [row.name, row.provider, row.state, row.agentId]))
|
|
161
|
+
: "No agents linked on this machine. Get a setup prompt from TalentOS.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function unlinkLocal(parsed) {
|
|
165
|
+
const agents = await selectAgents(parsed, true);
|
|
166
|
+
const agent = agents[0];
|
|
167
|
+
if (!agent) {
|
|
168
|
+
emit("Already unlinked on this machine.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const revoked = await unlinkRemoteAgent(agent);
|
|
172
|
+
await uninstallAgentService(agent);
|
|
173
|
+
await rm(agentConfigPath(agent.agentId), { force: true });
|
|
174
|
+
emit(`Unlinked ${redactAgentOutput(agent.name)} on this machine.`);
|
|
175
|
+
emit(redactAgentOutput(`Kept work folder ${agentWorkDir(agent.agentId)} and gateway log ${agentLogPath(agent.agentId)}.`));
|
|
176
|
+
if (!revoked) {
|
|
177
|
+
warn("The server could not confirm revocation. Disconnect the agent in TalentOS to confirm remote removal.");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function showLogs(parsed) {
|
|
181
|
+
const agent = (await selectAgents(parsed, true))[0];
|
|
182
|
+
if (!agent) {
|
|
183
|
+
emit("No linked agent logs on this machine.");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let log;
|
|
187
|
+
try {
|
|
188
|
+
log = await open(agentLogPath(agent.agentId), "r");
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (isMissing(error)) {
|
|
192
|
+
emit("Waiting for agent service; no logs yet.");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const size = (await log.stat()).size;
|
|
199
|
+
const buffer = Buffer.alloc(Math.min(size, 64 * 1024));
|
|
200
|
+
await log.read(buffer, 0, buffer.length, Math.max(0, size - buffer.length));
|
|
201
|
+
emit(redactAgentOutput(buffer.toString("utf8"), [agent.apiKey]));
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await log.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function selectAgents(parsed, single = false) {
|
|
208
|
+
const selector = parsed.options.agent ?? parsed.args[0];
|
|
209
|
+
const agents = (await localAgents()).filter((agent) => !selector || agent.agentId === selector || agent.name === selector);
|
|
210
|
+
if (single && agents.length > 1) {
|
|
211
|
+
throw new Error("Several agents are linked. Select one by name or --agent <id>.");
|
|
212
|
+
}
|
|
213
|
+
return agents;
|
|
214
|
+
}
|
|
215
|
+
async function approveInstall() {
|
|
216
|
+
if (!process.stdin.isTTY) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const prompt = createInterface({
|
|
220
|
+
input: process.stdin,
|
|
221
|
+
output: process.stdout,
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
return ((await prompt.question("Install a TalentOS user service on this machine? [y/N] "))
|
|
225
|
+
.trim()
|
|
226
|
+
.toLowerCase() === "y");
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
prompt.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function packageVersion() {
|
|
233
|
+
return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
234
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { redactAgentOutput } from "./agent-link.js";
|
|
3
4
|
import { parseArgv } from "./args.js";
|
|
4
5
|
import { emit } from "./format.js";
|
|
5
6
|
const USAGE = `talentos — your team's skills, memory and Talent, from any coding agent.
|
|
@@ -8,6 +9,7 @@ Usage: talentos <command> [options]
|
|
|
8
9
|
|
|
9
10
|
Commands:
|
|
10
11
|
init Connect this machine's agents to TalentOS
|
|
12
|
+
agent <link|status|list|unlink|logs> Bring your own agent into Rooms
|
|
11
13
|
login Authorize this machine
|
|
12
14
|
logout Forget the key stored on this machine
|
|
13
15
|
whoami Show the member and org this key acts as
|
|
@@ -26,6 +28,16 @@ Options:
|
|
|
26
28
|
|
|
27
29
|
Run \`talentos <command> --help\` for command options.`;
|
|
28
30
|
const GROUP_USAGE = {
|
|
31
|
+
agent: `talentos agent link --url <setup-url-with-#token> [--yes] [--name <device>]
|
|
32
|
+
talentos agent status | list [--json] [--agent <id>]
|
|
33
|
+
talentos agent unlink [name] [--agent <id>]
|
|
34
|
+
talentos agent logs [--agent <id>]
|
|
35
|
+
talentos agent serve --agent <id>
|
|
36
|
+
|
|
37
|
+
The link command checks the provider's CLI login, mints the key locally, and
|
|
38
|
+
installs one user service per agent. Only the key hash is sent to redeem.
|
|
39
|
+
Config is private under ~/.talentos/agents. Never print the setup fragment.
|
|
40
|
+
Serve is supplied by the S6 gateway release; Connected requires its heartbeat.`,
|
|
29
41
|
init: `talentos init [--for claude|cursor|codex|all] [--no-pull] [--name <device>]
|
|
30
42
|
|
|
31
43
|
One command: signs you in (skipped when your stored key still works), writes the
|
|
@@ -88,6 +100,8 @@ function helpFor(group) {
|
|
|
88
100
|
}
|
|
89
101
|
async function dispatch(parsed) {
|
|
90
102
|
switch (parsed.group) {
|
|
103
|
+
case "agent":
|
|
104
|
+
return await (await import("./commands/agent.js")).runAgent(parsed);
|
|
91
105
|
case "init":
|
|
92
106
|
return await (await import("./commands/init.js")).runInit(parsed);
|
|
93
107
|
case "login":
|
|
@@ -120,6 +134,14 @@ function version() {
|
|
|
120
134
|
}
|
|
121
135
|
main(process.argv.slice(2)).catch((error) => {
|
|
122
136
|
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
-
process.
|
|
137
|
+
const fragments = process.argv.flatMap((arg) => {
|
|
138
|
+
try {
|
|
139
|
+
return new URLSearchParams(new URL(arg).hash.slice(1)).getAll("token");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
process.stderr.write(`${redactAgentOutput(message, fragments).split("\n")[0]}\n`);
|
|
124
146
|
process.exit(1);
|
|
125
147
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talentos",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TalentOS CLI: push and pull skills, read and write team memory, and delegate work to your Talent from any coding agent.",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"prepack": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"convex": "^1.43.0",
|
|
28
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
30
|
"zod": "^4.4.3"
|
|
30
31
|
},
|