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,105 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { redactAgentOutput } from "./agent-link.js";
|
|
5
|
+
import { warn } from "./format.js";
|
|
6
|
+
import { homeDir } from "./install.js";
|
|
7
|
+
const AGENT_ID = /^[a-z\d_-]+$/iu;
|
|
8
|
+
export function agentsDir(home = homeDir()) {
|
|
9
|
+
return join(home, ".talentos", "agents");
|
|
10
|
+
}
|
|
11
|
+
export function agentConfigPath(agentId, home = homeDir()) {
|
|
12
|
+
if (!AGENT_ID.test(agentId)) {
|
|
13
|
+
throw new Error("Invalid agent ID.");
|
|
14
|
+
}
|
|
15
|
+
return join(agentsDir(home), `${agentId}.json`);
|
|
16
|
+
}
|
|
17
|
+
export function agentWorkDir(agentId, home = homeDir()) {
|
|
18
|
+
agentConfigPath(agentId, home);
|
|
19
|
+
return join(agentsDir(home), agentId, "work");
|
|
20
|
+
}
|
|
21
|
+
export function agentLogPath(agentId, home = homeDir()) {
|
|
22
|
+
agentConfigPath(agentId, home);
|
|
23
|
+
return join(agentsDir(home), agentId, "gateway.log");
|
|
24
|
+
}
|
|
25
|
+
export async function writePrivateFile(path, content) {
|
|
26
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
27
|
+
try {
|
|
28
|
+
if ((await readFile(path, "utf8")) === content) {
|
|
29
|
+
await chmod(path, 0o600);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!isMissing(error)) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
39
|
+
try {
|
|
40
|
+
await writeFile(temporary, content, { mode: 0o600, flag: "wx" });
|
|
41
|
+
await rename(temporary, path);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await rm(temporary, { force: true });
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
export async function storeAgent(config, home = homeDir()) {
|
|
49
|
+
await mkdir(agentsDir(home), { recursive: true, mode: 0o700 });
|
|
50
|
+
await chmod(agentsDir(home), 0o700);
|
|
51
|
+
await writePrivateFile(agentConfigPath(config.agentId, home), `${JSON.stringify(config, null, 2)}\n`);
|
|
52
|
+
await mkdir(agentWorkDir(config.agentId, home), {
|
|
53
|
+
recursive: true,
|
|
54
|
+
mode: 0o700,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function localAgents(home = homeDir()) {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = await readdir(agentsDir(home));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (isMissing(error)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
const agents = [];
|
|
69
|
+
for (const entry of entries.sort()) {
|
|
70
|
+
if (!entry.startsWith(".") && entry.endsWith(".json")) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(await readFile(join(agentsDir(home), entry), "utf8"));
|
|
73
|
+
if (!isLinkedAgent(parsed) || entry !== `${parsed.agentId}.json`) {
|
|
74
|
+
throw new Error("Invalid agent configuration.");
|
|
75
|
+
}
|
|
76
|
+
agents.push(parsed);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
warn(`Skipping unreadable or invalid agent configuration ${redactAgentOutput(JSON.stringify(entry))}. Restore it to manage that agent.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return agents;
|
|
84
|
+
}
|
|
85
|
+
function isLinkedAgent(value) {
|
|
86
|
+
if (!value || typeof value !== "object") {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const config = value;
|
|
90
|
+
return (typeof config.agentId === "string" &&
|
|
91
|
+
AGENT_ID.test(config.agentId) &&
|
|
92
|
+
typeof config.apiKey === "string" &&
|
|
93
|
+
config.apiKey.startsWith("tos_") &&
|
|
94
|
+
typeof config.apiUrl === "string" &&
|
|
95
|
+
typeof config.name === "string" &&
|
|
96
|
+
typeof config.linkHash === "string" &&
|
|
97
|
+
typeof config.linkedAt === "number" &&
|
|
98
|
+
(config.provider === "claude_code" || config.provider === "codex"));
|
|
99
|
+
}
|
|
100
|
+
export function isMissing(error) {
|
|
101
|
+
return (error !== null &&
|
|
102
|
+
typeof error === "object" &&
|
|
103
|
+
"code" in error &&
|
|
104
|
+
error.code === "ENOENT");
|
|
105
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { isSecureUrl, } from "./agent-link.js";
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
4
|
+
const HEARTBEAT_WINDOW_MS = 60_000;
|
|
5
|
+
export class AgentHttpError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
retryAfterMs;
|
|
8
|
+
constructor(operation, response) {
|
|
9
|
+
super(`${operation} (HTTP ${response.status}). Ask for a fresh setup prompt if this link expired.`);
|
|
10
|
+
this.status = response.status;
|
|
11
|
+
const seconds = Number(response.headers.get("Retry-After"));
|
|
12
|
+
this.retryAfterMs = Number.isFinite(seconds)
|
|
13
|
+
? Math.max(0, Math.min(60_000, seconds * 1000))
|
|
14
|
+
: 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function apiOrigin(value) {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(value);
|
|
20
|
+
if (!(isSecureUrl(url) && url.pathname === "/" && !url.search && !url.hash)) {
|
|
21
|
+
throw new Error("Invalid origin");
|
|
22
|
+
}
|
|
23
|
+
return url.origin;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error("Use a secure TalentOS API origin with --api-url.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function setupApiOrigin(link, override) {
|
|
30
|
+
if (override) {
|
|
31
|
+
return apiOrigin(override);
|
|
32
|
+
}
|
|
33
|
+
const response = await safeFetch(link.setupUrl);
|
|
34
|
+
const apiUrl = response.headers.get("X-TalentOS-Api-Url");
|
|
35
|
+
if (!(response.ok && apiUrl)) {
|
|
36
|
+
throw new Error("The setup page is unavailable. Ask for a fresh prompt or supply its API origin with --api-url.");
|
|
37
|
+
}
|
|
38
|
+
return apiOrigin(apiUrl);
|
|
39
|
+
}
|
|
40
|
+
export function inspectProvider(provider) {
|
|
41
|
+
const binary = provider === "claude_code" ? "claude" : "codex";
|
|
42
|
+
const version = spawnSync(binary, ["--version"], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
45
|
+
});
|
|
46
|
+
if (version.status !== 0 || !version.stdout?.trim()) {
|
|
47
|
+
throw new Error(`Install the official ${binary} CLI and sign in before linking.`);
|
|
48
|
+
}
|
|
49
|
+
const args = provider === "claude_code"
|
|
50
|
+
? ["auth", "status", "--json"]
|
|
51
|
+
: ["login", "status"];
|
|
52
|
+
const login = spawnSync(binary, args, {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
55
|
+
});
|
|
56
|
+
if (login.status !== 0) {
|
|
57
|
+
throw new Error(`Sign in through the official ${binary} CLI before linking.`);
|
|
58
|
+
}
|
|
59
|
+
if (provider === "claude_code") {
|
|
60
|
+
let loggedIn = false;
|
|
61
|
+
try {
|
|
62
|
+
const status = JSON.parse(login.stdout);
|
|
63
|
+
loggedIn =
|
|
64
|
+
!!status &&
|
|
65
|
+
typeof status === "object" &&
|
|
66
|
+
"loggedIn" in status &&
|
|
67
|
+
status.loggedIn === true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
loggedIn = false;
|
|
71
|
+
}
|
|
72
|
+
if (!loggedIn) {
|
|
73
|
+
throw new Error("Sign in through the official claude CLI before linking.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return version.stdout.trim().split("\n")[0]?.slice(0, 200) ?? "unknown";
|
|
77
|
+
}
|
|
78
|
+
export async function redeemAgent(apiUrl, body) {
|
|
79
|
+
const response = await safeFetch(`${apiOrigin(apiUrl)}/agent-link/redeem`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new AgentHttpError("Could not redeem this link", response);
|
|
86
|
+
}
|
|
87
|
+
const value = await readJson(response);
|
|
88
|
+
if (!value ||
|
|
89
|
+
typeof value !== "object" ||
|
|
90
|
+
!("agentId" in value && "name" in value) ||
|
|
91
|
+
typeof value.agentId !== "string" ||
|
|
92
|
+
typeof value.name !== "string") {
|
|
93
|
+
throw new Error("TalentOS returned an invalid agent link. Ask for a fresh prompt.");
|
|
94
|
+
}
|
|
95
|
+
return { agentId: value.agentId, name: value.name };
|
|
96
|
+
}
|
|
97
|
+
export async function fetchAgentStatus(agent) {
|
|
98
|
+
const response = await safeFetch(`${apiOrigin(agent.apiUrl)}/agent-link/status`, { headers: { Authorization: `Bearer ${agent.apiKey}` } });
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new AgentHttpError("Could not check the agent", response);
|
|
101
|
+
}
|
|
102
|
+
const value = await readJson(response);
|
|
103
|
+
if (!value || typeof value !== "object") {
|
|
104
|
+
throw new Error("TalentOS returned an invalid agent status.");
|
|
105
|
+
}
|
|
106
|
+
const status = value;
|
|
107
|
+
if (typeof status.agentId !== "string" ||
|
|
108
|
+
(agent.agentId !== undefined && status.agentId !== agent.agentId) ||
|
|
109
|
+
status.provider !== agent.provider ||
|
|
110
|
+
typeof status.name !== "string" ||
|
|
111
|
+
!Number.isFinite(status.serverNow) ||
|
|
112
|
+
!(status.lastSeenAt === null || typeof status.lastSeenAt === "number") ||
|
|
113
|
+
!["online", "busy", "offline"].includes(String(status.presence))) {
|
|
114
|
+
throw new Error("TalentOS returned an invalid agent status.");
|
|
115
|
+
}
|
|
116
|
+
return status;
|
|
117
|
+
}
|
|
118
|
+
export async function unlinkRemoteAgent(agent) {
|
|
119
|
+
const response = await safeFetch(`${apiOrigin(agent.apiUrl)}/agent-link/unlink`, { method: "POST", headers: { Authorization: `Bearer ${agent.apiKey}` } });
|
|
120
|
+
if (response.status === 401) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new AgentHttpError("Could not disconnect the agent", response);
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
export async function waitForHeartbeat(agent, options = {}) {
|
|
129
|
+
const deadline = Date.now() + (options.timeoutMs ?? 10 * 60_000);
|
|
130
|
+
let serverStartedAt;
|
|
131
|
+
const wait = options.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
132
|
+
while (Date.now() < deadline) {
|
|
133
|
+
let delay = 10_000;
|
|
134
|
+
try {
|
|
135
|
+
const status = await fetchAgentStatus(agent);
|
|
136
|
+
serverStartedAt ??= status.serverNow;
|
|
137
|
+
if (status.lastSeenAt !== null &&
|
|
138
|
+
status.lastSeenAt > serverStartedAt &&
|
|
139
|
+
status.lastSeenAt <= status.serverNow &&
|
|
140
|
+
status.serverNow - status.lastSeenAt < HEARTBEAT_WINDOW_MS &&
|
|
141
|
+
status.presence !== "offline") {
|
|
142
|
+
return status;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof AgentHttpError && error.status === 401) {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
if (error instanceof AgentHttpError) {
|
|
150
|
+
delay = Math.max(delay, error.retryAfterMs);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
await wait(Math.max(0, Math.min(delay, deadline - Date.now())));
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
async function safeFetch(url, init = {}) {
|
|
158
|
+
try {
|
|
159
|
+
return await fetch(url, {
|
|
160
|
+
...init,
|
|
161
|
+
redirect: "error",
|
|
162
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new Error("Could not reach TalentOS. Check the network and try again.");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function readJson(response) {
|
|
170
|
+
try {
|
|
171
|
+
return await response.json();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
throw new Error("TalentOS returned an invalid response.");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const INVALID_LINK = "Get a new setup prompt from TalentOS and pass its complete URL with --url.";
|
|
2
|
+
const SETUP_PATH = /^\/agent-setup\/(claude_code|codex)\/?$/;
|
|
3
|
+
const FRAGMENT_TOKEN = /((?:#|%23)token(?:=|%3d))[^\s"'<>]+/giu;
|
|
4
|
+
const RAW_KEY = /\btos_[a-z\d_-]+/giu;
|
|
5
|
+
const BEARER = /\bBearer\s+\S+/giu;
|
|
6
|
+
const TOKEN_WHITESPACE = /\s/u;
|
|
7
|
+
export function parseAgentLink(value) {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
const provider = SETUP_PATH.exec(url.pathname)?.[1];
|
|
11
|
+
const fragment = new URLSearchParams(url.hash.slice(1));
|
|
12
|
+
const tokens = fragment.getAll("token");
|
|
13
|
+
const token = tokens[0];
|
|
14
|
+
if (!(isSecureUrl(url) &&
|
|
15
|
+
(provider === "claude_code" || provider === "codex") &&
|
|
16
|
+
!url.search &&
|
|
17
|
+
fragment.size === 1 &&
|
|
18
|
+
tokens.length === 1 &&
|
|
19
|
+
token &&
|
|
20
|
+
token.length <= 200 &&
|
|
21
|
+
!TOKEN_WHITESPACE.test(token))) {
|
|
22
|
+
throw new Error(INVALID_LINK);
|
|
23
|
+
}
|
|
24
|
+
url.hash = "";
|
|
25
|
+
return { provider, setupUrl: url.href, token };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(INVALID_LINK);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function isSecureUrl(url) {
|
|
32
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
33
|
+
return (!(url.username || url.password) &&
|
|
34
|
+
(url.protocol === "https:" || (url.protocol === "http:" && loopback)));
|
|
35
|
+
}
|
|
36
|
+
export function redactAgentOutput(value, secrets = []) {
|
|
37
|
+
let safe = value;
|
|
38
|
+
for (const secret of secrets) {
|
|
39
|
+
if (secret) {
|
|
40
|
+
safe = safe
|
|
41
|
+
.replaceAll(secret, "[redacted]")
|
|
42
|
+
.replaceAll(encodeURIComponent(secret), "[redacted]");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return safe
|
|
46
|
+
.replace(FRAGMENT_TOKEN, "$1[redacted]")
|
|
47
|
+
.replace(RAW_KEY, "[redacted key]")
|
|
48
|
+
.replace(BEARER, "Bearer [redacted]");
|
|
49
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
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 { fileURLToPath } from "node:url";
|
|
5
|
+
import { agentLogPath, agentWorkDir, writePrivateFile, } from "./agent-config.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
|
+
options.nodePath ?? process.execPath,
|
|
29
|
+
options.cliPath ?? fileURLToPath(new URL("./index.js", import.meta.url)),
|
|
30
|
+
"agent",
|
|
31
|
+
"serve",
|
|
32
|
+
"--agent",
|
|
33
|
+
agent.agentId,
|
|
34
|
+
];
|
|
35
|
+
const workDir = agentWorkDir(agent.agentId, home);
|
|
36
|
+
const content = platform === "darwin"
|
|
37
|
+
? launchdUnit(name, args, workDir, logPath)
|
|
38
|
+
: systemdUnit(args, workDir, logPath);
|
|
39
|
+
const changed = await writePrivateFile(path, content);
|
|
40
|
+
if (platform === "darwin") {
|
|
41
|
+
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
42
|
+
const loaded = run("launchctl", ["print", `${domain}/${name}`]) === 0;
|
|
43
|
+
if (loaded && !changed) {
|
|
44
|
+
return { path, started: true };
|
|
45
|
+
}
|
|
46
|
+
if (loaded) {
|
|
47
|
+
run("launchctl", ["bootout", domain, path]);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
path,
|
|
51
|
+
started: run("launchctl", ["bootstrap", domain, path]) === 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (changed && run("systemctl", ["--user", "daemon-reload"]) !== 0) {
|
|
55
|
+
return { path, started: false };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
path,
|
|
59
|
+
started: run("systemctl", [
|
|
60
|
+
"--user",
|
|
61
|
+
"enable",
|
|
62
|
+
"--now",
|
|
63
|
+
`talentos-agent@${agent.agentId}.service`,
|
|
64
|
+
]) === 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function uninstallAgentService(agent, options = {}) {
|
|
68
|
+
const platform = options.platform ?? process.platform;
|
|
69
|
+
const path = servicePath(agent.agentId, platform, options.home ?? homeDir());
|
|
70
|
+
if (!path) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const run = options.run ?? runCommand;
|
|
74
|
+
if (platform === "darwin") {
|
|
75
|
+
run("launchctl", [
|
|
76
|
+
"bootout",
|
|
77
|
+
`gui/${options.uid ?? process.getuid?.() ?? 0}`,
|
|
78
|
+
path,
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
run("systemctl", [
|
|
83
|
+
"--user",
|
|
84
|
+
"disable",
|
|
85
|
+
"--now",
|
|
86
|
+
`talentos-agent@${agent.agentId}.service`,
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
await rm(path, { force: true });
|
|
90
|
+
if (platform === "linux") {
|
|
91
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function serviceName(agentId) {
|
|
95
|
+
return `so.talentos.agent.${agentId}`;
|
|
96
|
+
}
|
|
97
|
+
function servicePath(agentId, platform, home) {
|
|
98
|
+
agentWorkDir(agentId, home);
|
|
99
|
+
if (platform === "darwin") {
|
|
100
|
+
return join(home, "Library", "LaunchAgents", `${serviceName(agentId)}.plist`);
|
|
101
|
+
}
|
|
102
|
+
return platform === "linux"
|
|
103
|
+
? join(home, ".config", "systemd", "user", `talentos-agent@${agentId}.service`)
|
|
104
|
+
: "";
|
|
105
|
+
}
|
|
106
|
+
function launchdUnit(name, args, workDir, logPath) {
|
|
107
|
+
const xml = (value) => value
|
|
108
|
+
.replaceAll("&", "&")
|
|
109
|
+
.replaceAll("<", "<")
|
|
110
|
+
.replaceAll(">", ">")
|
|
111
|
+
.replaceAll('"', """)
|
|
112
|
+
.replaceAll("'", "'");
|
|
113
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
114
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
115
|
+
<plist version="1.0"><dict>
|
|
116
|
+
<key>Label</key><string>${xml(name)}</string>
|
|
117
|
+
<key>ProgramArguments</key><array>${args.map((arg) => `<string>${xml(arg)}</string>`).join("")}</array>
|
|
118
|
+
<key>WorkingDirectory</key><string>${xml(workDir)}</string>
|
|
119
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin")}</string></dict>
|
|
120
|
+
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
|
|
121
|
+
<key>StandardOutPath</key><string>${xml(logPath)}</string>
|
|
122
|
+
<key>StandardErrorPath</key><string>${xml(logPath)}</string>
|
|
123
|
+
</dict></plist>
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
function systemdUnit(args, workDir, logPath) {
|
|
127
|
+
const quote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%").replaceAll("\n", "\\n").replaceAll("\r", "\\r")}"`;
|
|
128
|
+
return `[Unit]
|
|
129
|
+
Description=TalentOS agent gateway
|
|
130
|
+
After=network-online.target
|
|
131
|
+
|
|
132
|
+
[Service]
|
|
133
|
+
Type=simple
|
|
134
|
+
ExecStart=${args.map(quote).join(" ")}
|
|
135
|
+
WorkingDirectory=${quote(workDir)}
|
|
136
|
+
Environment=${quote(`PATH=${process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"}`)}
|
|
137
|
+
Restart=always
|
|
138
|
+
RestartSec=5
|
|
139
|
+
StandardOutput=${quote(`append:${logPath}`)}
|
|
140
|
+
StandardError=${quote(`append:${logPath}`)}
|
|
141
|
+
|
|
142
|
+
[Install]
|
|
143
|
+
WantedBy=default.target
|
|
144
|
+
`;
|
|
145
|
+
}
|
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,42 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { childEnvironment, verifyClaudeCode } from "../agent/adapters.js";
|
|
5
|
+
import { serveAgent } from "../agent/serve.js";
|
|
6
|
+
import { agentWorkDir, localAgents } from "../agent-config.js";
|
|
7
|
+
export async function runAgentServe(parsed) {
|
|
8
|
+
const id = parsed.options.agent;
|
|
9
|
+
const agents = await localAgents();
|
|
10
|
+
let config = agents.find((agent) => agent.agentId === id);
|
|
11
|
+
if (!id && agents.length === 1) {
|
|
12
|
+
config = agents[0];
|
|
13
|
+
}
|
|
14
|
+
if (!config) {
|
|
15
|
+
throw new Error("Choose a linked agent with `talentos agent serve --agent <id>`.");
|
|
16
|
+
}
|
|
17
|
+
if (config.provider === "claude_code") {
|
|
18
|
+
await verifyClaudeCode();
|
|
19
|
+
}
|
|
20
|
+
const stop = new AbortController();
|
|
21
|
+
const abort = () => stop.abort();
|
|
22
|
+
process.once("SIGTERM", abort);
|
|
23
|
+
process.once("SIGINT", abort);
|
|
24
|
+
const version = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
25
|
+
try {
|
|
26
|
+
const updated = await serveAgent(config, dirname(agentWorkDir(config.agentId)), version, stop.signal);
|
|
27
|
+
if (updated && !stop.signal.aborted) {
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
const child = spawn(process.execPath, [updated, "agent", "serve", "--agent", config.agentId], { env: childEnvironment(), stdio: "inherit", signal: stop.signal });
|
|
30
|
+
child.once("error", () => reject(new Error("Updated gateway could not start.")));
|
|
31
|
+
child.once("close", (code) => {
|
|
32
|
+
process.exitCode = code ?? 1;
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
process.removeListener("SIGTERM", abort);
|
|
40
|
+
process.removeListener("SIGINT", abort);
|
|
41
|
+
}
|
|
42
|
+
}
|