talentos 1.0.0 → 1.1.1

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.
@@ -0,0 +1,152 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { chmod, mkdir, open, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { agentLogPath, agentWorkDir, writePrivateFile, } from "./agent-config.js";
5
+ import { gatewayLauncherPath, servicePathEnv } from "./gateway-install.js";
6
+ import { homeDir } from "./install.js";
7
+ export function runCommand(command, args) {
8
+ return (spawnSync(command, args, { stdio: "ignore", timeout: 15_000 }).status ?? 1);
9
+ }
10
+ export async function installAgentService(agent, options = {}) {
11
+ const home = options.home ?? homeDir();
12
+ const platform = options.platform ?? process.platform;
13
+ const run = options.run ?? runCommand;
14
+ const name = serviceName(agent.agentId);
15
+ const path = servicePath(agent.agentId, platform, home);
16
+ const logPath = agentLogPath(agent.agentId, home);
17
+ await mkdir(agentWorkDir(agent.agentId, home), {
18
+ recursive: true,
19
+ mode: 0o700,
20
+ });
21
+ const log = await open(logPath, "a", 0o600);
22
+ await log.close();
23
+ await chmod(logPath, 0o600);
24
+ if (!path) {
25
+ return { path: "", started: false };
26
+ }
27
+ const args = [
28
+ gatewayLauncherPath(home),
29
+ "agent",
30
+ "serve",
31
+ "--agent",
32
+ agent.agentId,
33
+ ];
34
+ const workDir = agentWorkDir(agent.agentId, home);
35
+ const content = platform === "darwin"
36
+ ? launchdUnit(name, args, workDir, logPath, servicePathEnv(home))
37
+ : systemdUnit(args, workDir, logPath, servicePathEnv(home));
38
+ const changed = await writePrivateFile(path, content);
39
+ if (options.reload === false) {
40
+ if (platform === "linux" && changed) {
41
+ run("systemctl", ["--user", "daemon-reload"]);
42
+ }
43
+ return { path, started: true };
44
+ }
45
+ if (platform === "darwin") {
46
+ const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
47
+ const loaded = run("launchctl", ["print", `${domain}/${name}`]) === 0;
48
+ if (loaded && !changed) {
49
+ return { path, started: true };
50
+ }
51
+ if (loaded) {
52
+ run("launchctl", ["bootout", domain, path]);
53
+ }
54
+ return {
55
+ path,
56
+ started: run("launchctl", ["bootstrap", domain, path]) === 0,
57
+ };
58
+ }
59
+ if (changed && run("systemctl", ["--user", "daemon-reload"]) !== 0) {
60
+ return { path, started: false };
61
+ }
62
+ return {
63
+ path,
64
+ started: run("systemctl", [
65
+ "--user",
66
+ "enable",
67
+ "--now",
68
+ `talentos-agent@${agent.agentId}.service`,
69
+ ]) === 0,
70
+ };
71
+ }
72
+ export async function uninstallAgentService(agent, options = {}) {
73
+ const platform = options.platform ?? process.platform;
74
+ const path = servicePath(agent.agentId, platform, options.home ?? homeDir());
75
+ if (!path) {
76
+ return;
77
+ }
78
+ const run = options.run ?? runCommand;
79
+ if (platform === "darwin") {
80
+ run("launchctl", [
81
+ "bootout",
82
+ `gui/${options.uid ?? process.getuid?.() ?? 0}`,
83
+ path,
84
+ ]);
85
+ }
86
+ else {
87
+ run("systemctl", [
88
+ "--user",
89
+ "disable",
90
+ "--now",
91
+ `talentos-agent@${agent.agentId}.service`,
92
+ ]);
93
+ }
94
+ await rm(path, { force: true });
95
+ if (platform === "linux") {
96
+ run("systemctl", ["--user", "daemon-reload"]);
97
+ }
98
+ }
99
+ function serviceName(agentId) {
100
+ return `so.talentos.agent.${agentId}`;
101
+ }
102
+ function servicePath(agentId, platform, home) {
103
+ agentWorkDir(agentId, home);
104
+ if (platform === "darwin") {
105
+ return join(home, "Library", "LaunchAgents", `${serviceName(agentId)}.plist`);
106
+ }
107
+ return platform === "linux"
108
+ ? join(home, ".config", "systemd", "user", `talentos-agent@${agentId}.service`)
109
+ : "";
110
+ }
111
+ function launchdUnit(name, args, workDir, logPath, pathEnv) {
112
+ const xml = (value) => value
113
+ .replaceAll("&", "&")
114
+ .replaceAll("<", "&lt;")
115
+ .replaceAll(">", "&gt;")
116
+ .replaceAll('"', "&quot;")
117
+ .replaceAll("'", "&apos;");
118
+ return `<?xml version="1.0" encoding="UTF-8"?>
119
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
120
+ <plist version="1.0"><dict>
121
+ <key>Label</key><string>${xml(name)}</string>
122
+ <key>ProgramArguments</key><array>${args.map((arg) => `<string>${xml(arg)}</string>`).join("")}</array>
123
+ <key>WorkingDirectory</key><string>${xml(workDir)}</string>
124
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(pathEnv)}</string></dict>
125
+ <key>ThrottleInterval</key><integer>60</integer>
126
+ <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
127
+ <key>StandardOutPath</key><string>${xml(logPath)}</string>
128
+ <key>StandardErrorPath</key><string>${xml(logPath)}</string>
129
+ </dict></plist>
130
+ `;
131
+ }
132
+ function systemdUnit(args, workDir, logPath, pathEnv) {
133
+ const quote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%").replaceAll("\n", "\\n").replaceAll("\r", "\\r")}"`;
134
+ return `[Unit]
135
+ Description=TalentOS agent gateway
136
+ After=network-online.target
137
+
138
+ [Service]
139
+ Type=simple
140
+ ExecStart=${args.map(quote).join(" ")}
141
+ WorkingDirectory=${quote(workDir)}
142
+ Environment=${quote(`PATH=${pathEnv}`)}
143
+ Restart=always
144
+ RestartSec=60
145
+ RestartPreventExitStatus=78
146
+ StandardOutput=${quote(`append:${logPath}`)}
147
+ StandardError=${quote(`append:${logPath}`)}
148
+
149
+ [Install]
150
+ WantedBy=default.target
151
+ `;
152
+ }
package/dist/args.js CHANGED
@@ -19,9 +19,12 @@ const CLI_OPTIONS = {
19
19
  global: { type: "boolean" },
20
20
  apply: { type: "boolean" },
21
21
  "no-pull": { type: "boolean" },
22
+ url: { type: "string" },
23
+ yes: { type: "boolean" },
24
+ agent: { type: "string" },
22
25
  };
23
26
  /** Groups whose first positional is a sub-command rather than an argument. */
24
- const ACTION_GROUPS = new Set(["mcp", "skills", "memory", "routines"]);
27
+ const ACTION_GROUPS = new Set(["agent", "mcp", "skills", "memory", "routines"]);
25
28
  export function parseArgv(argv) {
26
29
  const { values, positionals } = parse(argv);
27
30
  const group = positionals[0];
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { verifyClaudeCode, verifyCodex } from "../agent/adapters.js";
4
+ import { serveAgent } from "../agent/serve.js";
5
+ import { agentWorkDir, localAgents } from "../agent-config.js";
6
+ import { installAgentService } from "../agent-service.js";
7
+ import { installSharedGateway } from "../gateway-install.js";
8
+ export async function runAgentServe(parsed) {
9
+ const id = parsed.options.agent;
10
+ const agents = await localAgents();
11
+ let config = agents.find((agent) => agent.agentId === id);
12
+ if (!id && agents.length === 1) {
13
+ config = agents[0];
14
+ }
15
+ if (!config) {
16
+ throw new Error("Choose a linked agent with `talentos agent serve --agent <id>`.");
17
+ }
18
+ try {
19
+ if (process.platform !== "win32") {
20
+ await installSharedGateway();
21
+ }
22
+ await installAgentService(config, { reload: false });
23
+ }
24
+ catch {
25
+ console.error("Gateway installation unavailable. Serving the loaded version; retry talentos agent repair when the installation is available.");
26
+ }
27
+ if (config.provider === "claude_code") {
28
+ await verifyClaudeCode();
29
+ }
30
+ else {
31
+ await verifyCodex();
32
+ }
33
+ const stop = new AbortController();
34
+ const abort = () => stop.abort();
35
+ process.once("SIGTERM", abort);
36
+ process.once("SIGINT", abort);
37
+ const version = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
38
+ try {
39
+ await serveAgent(config, dirname(agentWorkDir(config.agentId)), version, stop.signal);
40
+ }
41
+ finally {
42
+ process.removeListener("SIGTERM", abort);
43
+ process.removeListener("SIGINT", abort);
44
+ }
45
+ }
@@ -0,0 +1,278 @@
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 { installSharedGateway } from "../gateway-install.js";
11
+ import { homeDir } from "../install.js";
12
+ import { defaultDeviceName, hashKey, mintKey } from "../login.js";
13
+ const MAX_PREVIOUS_AGENT_IDS = 100;
14
+ export async function runAgent(parsed, dependencies = {}) {
15
+ switch (parsed.action) {
16
+ case "link": {
17
+ if (!(parsed.options.yes || (await approveInstall()))) {
18
+ throw new Error("Link cancelled. Re-run with --yes after approving the user service installation.");
19
+ }
20
+ const { agent, service } = await installLinkedAgent(parsed);
21
+ emit(`Linked ${redactAgentOutput(agent.name)} (${agent.agentId}). Waiting for agent service…`);
22
+ if (!service.started) {
23
+ warn(`Start it in the foreground: npx --yes talentos@1 agent serve --agent ${agent.agentId}`);
24
+ }
25
+ const status = await waitForHeartbeat(agent);
26
+ if (!status) {
27
+ 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.`);
28
+ }
29
+ emit(`Connected as ${redactAgentOutput(status.name)}.`);
30
+ return;
31
+ }
32
+ case "serve": {
33
+ let command;
34
+ try {
35
+ // S6 supplies this module; S3 can install the service before it ships.
36
+ command = await import(new URL("./agent-serve.js", import.meta.url).href);
37
+ }
38
+ catch {
39
+ 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.");
40
+ }
41
+ return await command.runAgentServe(parsed);
42
+ }
43
+ case "repair": {
44
+ if (process.platform === "win32") {
45
+ throw new Error("Windows agents run in the foreground. Use talentos agent serve.");
46
+ }
47
+ const agents = await selectAgents(parsed, false, dependencies.home);
48
+ if (!agents.length) {
49
+ throw new Error("No linked agent matches. Run talentos agent list.");
50
+ }
51
+ await installSharedGateway({
52
+ home: dependencies.home,
53
+ source: dependencies.gatewaySource,
54
+ });
55
+ for (const agent of agents) {
56
+ const service = await (dependencies.install ??
57
+ ((config) => installAgentService(config, { home: dependencies.home })))(agent);
58
+ emit(`Repaired ${redactAgentOutput(agent.name)} (${agent.agentId}).${service.started ? "" : " Start talentos agent serve in the foreground."}`);
59
+ }
60
+ return;
61
+ }
62
+ case "mcp": {
63
+ await import("../agent/mcp-entry.js");
64
+ return;
65
+ }
66
+ case "status":
67
+ case "list":
68
+ return await showAgents(parsed);
69
+ case "unlink":
70
+ return await unlinkLocal(parsed);
71
+ case "logs":
72
+ return await showLogs(parsed);
73
+ default:
74
+ throw new Error("Use talentos agent link | repair | status | list | unlink | logs.");
75
+ }
76
+ }
77
+ export async function installLinkedAgent(parsed, dependencies = {}) {
78
+ if (Number(process.versions.node.split(".")[0]) < 20) {
79
+ throw new Error("Agent linking requires Node.js 20 or newer.");
80
+ }
81
+ const link = parseAgentLink(parsed.options.url ?? "");
82
+ const home = dependencies.home ?? homeDir();
83
+ const linkHash = hashKey(link.token);
84
+ const savedAgents = await localAgents(home);
85
+ let agent = savedAgents.find((item) => item.linkHash === linkHash && item.provider === link.provider);
86
+ if (agent) {
87
+ await storeAgent(agent, home);
88
+ }
89
+ else {
90
+ const apiUrl = await setupApiOrigin(link, parsed.options["api-url"] ?? process.env.TALENTOS_API_URL);
91
+ const candidates = savedAgents
92
+ .filter((item) => item.provider === link.provider && item.apiUrl === apiUrl)
93
+ .sort((a, b) => b.linkedAt - a.linkedAt)
94
+ .slice(0, MAX_PREVIOUS_AGENT_IDS);
95
+ const pendingPath = join(agentsDir(home), `.pending-${linkHash}.json`);
96
+ let apiKey;
97
+ let recovered;
98
+ try {
99
+ const pending = JSON.parse(await readFile(pendingPath, "utf8"));
100
+ if (!pending ||
101
+ typeof pending !== "object" ||
102
+ !("apiKey" in pending &&
103
+ "apiUrl" in pending &&
104
+ "provider" in pending) ||
105
+ typeof pending.apiKey !== "string" ||
106
+ !pending.apiKey.startsWith("tos_") ||
107
+ pending.apiUrl !== apiUrl ||
108
+ pending.provider !== link.provider) {
109
+ throw new Error("The pending agent link is invalid. Ask for a fresh setup prompt.");
110
+ }
111
+ apiKey = pending.apiKey;
112
+ try {
113
+ recovered = await fetchAgentStatus({
114
+ apiKey,
115
+ apiUrl,
116
+ provider: link.provider,
117
+ });
118
+ }
119
+ catch (error) {
120
+ if (!(error instanceof AgentHttpError && error.status === 401)) {
121
+ throw error;
122
+ }
123
+ }
124
+ }
125
+ catch (error) {
126
+ if (!isMissing(error)) {
127
+ throw error;
128
+ }
129
+ apiKey = mintKey().rawKey;
130
+ await mkdir(agentsDir(home), { recursive: true, mode: 0o700 });
131
+ await chmod(agentsDir(home), 0o700);
132
+ await writePrivateFile(pendingPath, `${JSON.stringify({ apiKey, apiUrl, provider: link.provider })}\n`);
133
+ }
134
+ const identity = recovered ??
135
+ (await redeemAgent(apiUrl, {
136
+ token: link.token,
137
+ previousAgentIds: candidates.map((item) => item.agentId),
138
+ keyHash: hashKey(apiKey),
139
+ provider: link.provider,
140
+ providerVersion: (dependencies.probe ?? inspectProvider)(link.provider),
141
+ deviceName: parsed.options.name ?? defaultDeviceName(),
142
+ os: process.platform,
143
+ cliVersion: packageVersion(),
144
+ }));
145
+ agent = {
146
+ agentId: identity.agentId,
147
+ name: identity.name,
148
+ provider: link.provider,
149
+ apiKey,
150
+ apiUrl,
151
+ linkHash,
152
+ linkedAt: Date.now(),
153
+ };
154
+ await storeAgent(agent, home);
155
+ for (const old of candidates) {
156
+ if (old.agentId !== agent.agentId &&
157
+ identity.retiredAgentIds?.includes(old.agentId)) {
158
+ await uninstallAgentService(old, { home });
159
+ await rm(agentConfigPath(old.agentId, home), { force: true });
160
+ }
161
+ }
162
+ await rm(pendingPath, { force: true });
163
+ }
164
+ const service = await (dependencies.install ??
165
+ (async (config) => {
166
+ if (process.platform !== "win32") {
167
+ await installSharedGateway({ home });
168
+ }
169
+ return await installAgentService(config, { home });
170
+ }))(agent);
171
+ return { agent, service };
172
+ }
173
+ async function showAgents(parsed) {
174
+ const agents = await selectAgents(parsed);
175
+ const rows = [];
176
+ for (const agent of agents) {
177
+ try {
178
+ const status = await fetchAgentStatus(agent);
179
+ rows.push({
180
+ agentId: status.agentId,
181
+ name: redactAgentOutput(status.name),
182
+ provider: status.provider,
183
+ state: status.presence,
184
+ lastSeenAt: status.lastSeenAt,
185
+ });
186
+ }
187
+ catch (error) {
188
+ rows.push({
189
+ agentId: agent.agentId,
190
+ name: redactAgentOutput(agent.name),
191
+ provider: agent.provider,
192
+ state: error instanceof AgentHttpError && error.status === 401
193
+ ? "disconnected"
194
+ : "unreachable",
195
+ lastSeenAt: null,
196
+ });
197
+ }
198
+ }
199
+ if (parsed.options.json) {
200
+ emitJson(rows);
201
+ }
202
+ else {
203
+ emit(rows.length
204
+ ? renderTable(["Agent", "Provider", "Status", "ID"], rows.map((row) => [row.name, row.provider, row.state, row.agentId]))
205
+ : "No agents linked on this machine. Get a setup prompt from TalentOS.");
206
+ }
207
+ }
208
+ async function unlinkLocal(parsed) {
209
+ const agents = await selectAgents(parsed, true);
210
+ const agent = agents[0];
211
+ if (!agent) {
212
+ emit("Already unlinked on this machine.");
213
+ return;
214
+ }
215
+ const revoked = await unlinkRemoteAgent(agent);
216
+ await uninstallAgentService(agent);
217
+ await rm(agentConfigPath(agent.agentId), { force: true });
218
+ emit(`Unlinked ${redactAgentOutput(agent.name)} on this machine.`);
219
+ emit(redactAgentOutput(`Kept work folder ${agentWorkDir(agent.agentId)} and gateway log ${agentLogPath(agent.agentId)}.`));
220
+ if (!revoked) {
221
+ warn("The server could not confirm revocation. Disconnect the agent in TalentOS to confirm remote removal.");
222
+ }
223
+ }
224
+ async function showLogs(parsed) {
225
+ const agent = (await selectAgents(parsed, true))[0];
226
+ if (!agent) {
227
+ emit("No linked agent logs on this machine.");
228
+ return;
229
+ }
230
+ let log;
231
+ try {
232
+ log = await open(agentLogPath(agent.agentId), "r");
233
+ }
234
+ catch (error) {
235
+ if (isMissing(error)) {
236
+ emit("Waiting for agent service; no logs yet.");
237
+ return;
238
+ }
239
+ throw error;
240
+ }
241
+ try {
242
+ const size = (await log.stat()).size;
243
+ const buffer = Buffer.alloc(Math.min(size, 64 * 1024));
244
+ await log.read(buffer, 0, buffer.length, Math.max(0, size - buffer.length));
245
+ emit(redactAgentOutput(buffer.toString("utf8"), [agent.apiKey]));
246
+ }
247
+ finally {
248
+ await log.close();
249
+ }
250
+ }
251
+ async function selectAgents(parsed, single = false, home) {
252
+ const selector = parsed.options.agent ?? parsed.args[0];
253
+ const agents = (await localAgents(home)).filter((agent) => !selector || agent.agentId === selector || agent.name === selector);
254
+ if (single && agents.length > 1) {
255
+ throw new Error("Several agents are linked. Select one by name or --agent <id>.");
256
+ }
257
+ return agents;
258
+ }
259
+ async function approveInstall() {
260
+ if (!process.stdin.isTTY) {
261
+ return false;
262
+ }
263
+ const prompt = createInterface({
264
+ input: process.stdin,
265
+ output: process.stdout,
266
+ });
267
+ try {
268
+ return ((await prompt.question("Install a TalentOS user service on this machine? [y/N] "))
269
+ .trim()
270
+ .toLowerCase() === "y");
271
+ }
272
+ finally {
273
+ prompt.close();
274
+ }
275
+ }
276
+ function packageVersion() {
277
+ return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
278
+ }