talentos 1.1.0 → 1.1.2
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/dist/agent/adapters.js +130 -20
- package/dist/agent/gateway.js +1 -1
- package/dist/agent/mcp-entry.js +2 -1
- package/dist/agent/serve.js +26 -39
- package/dist/agent-config.js +7 -4
- package/dist/agent-http.js +16 -4
- package/dist/agent-service.js +37 -20
- package/dist/commands/agent-serve.js +71 -17
- package/dist/commands/agent.js +72 -8
- package/dist/gateway-install.js +343 -0
- package/dist/gateway-version.js +45 -0
- package/dist/index.js +4 -2
- package/dist/provider-binary.js +47 -0
- package/package.json +1 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
3
2
|
import { dirname } from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import { verifyClaudeCode, verifyCodex } from "../agent/adapters.js";
|
|
4
|
+
import { AgentGateway, GatewayError } from "../agent/gateway.js";
|
|
5
5
|
import { serveAgent } from "../agent/serve.js";
|
|
6
|
-
import { agentWorkDir, localAgents } from "../agent-config.js";
|
|
7
|
-
|
|
6
|
+
import { agentWorkDir, localAgents, storeAgent, } from "../agent-config.js";
|
|
7
|
+
import { AgentHttpError, fetchAgentStatus } from "../agent-http.js";
|
|
8
|
+
import { installAgentService, uninstallAgentService, unloadLaunchAgent, } from "../agent-service.js";
|
|
9
|
+
import { installSharedGateway } from "../gateway-install.js";
|
|
10
|
+
import { ProviderBinaryError, resolveProviderBinary, } from "../provider-binary.js";
|
|
11
|
+
export async function runAgentServe(parsed, options = {}) {
|
|
8
12
|
const id = parsed.options.agent;
|
|
9
|
-
const agents = await localAgents();
|
|
13
|
+
const agents = await localAgents(options.home);
|
|
10
14
|
let config = agents.find((agent) => agent.agentId === id);
|
|
11
15
|
if (!id && agents.length === 1) {
|
|
12
16
|
config = agents[0];
|
|
@@ -14,29 +18,79 @@ export async function runAgentServe(parsed) {
|
|
|
14
18
|
if (!config) {
|
|
15
19
|
throw new Error("Choose a linked agent with `talentos agent serve --agent <id>`.");
|
|
16
20
|
}
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
try {
|
|
22
|
+
if ((options.platform ?? process.platform) !== "win32") {
|
|
23
|
+
await installSharedGateway({ home: options.home });
|
|
24
|
+
}
|
|
25
|
+
await installAgentService(config, { ...options, reload: false });
|
|
19
26
|
}
|
|
27
|
+
catch {
|
|
28
|
+
console.error("Gateway installation unavailable. Serving the loaded version; retry talentos agent repair when the installation is available.");
|
|
29
|
+
}
|
|
30
|
+
const agent = config;
|
|
20
31
|
const stop = new AbortController();
|
|
21
32
|
const abort = () => stop.abort();
|
|
22
33
|
process.once("SIGTERM", abort);
|
|
23
34
|
process.once("SIGINT", abort);
|
|
24
35
|
const version = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
25
36
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
// Authenticate without publishing presence, including when the binary is gone.
|
|
38
|
+
await fetchAgentStatus(config);
|
|
39
|
+
await serveAgent(config, dirname(agentWorkDir(config.agentId, options.home)), version, stop.signal, () => verifyAgent(agent, options));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if ((error instanceof GatewayError || error instanceof AgentHttpError) &&
|
|
43
|
+
(error.status === 401 || error.status === 410)) {
|
|
44
|
+
await uninstallAgentService(config, options);
|
|
45
|
+
console.error("Agent access ended. Removed this agent's local service; link it again from TalentOS to reconnect.");
|
|
46
|
+
return;
|
|
36
47
|
}
|
|
48
|
+
throw error;
|
|
37
49
|
}
|
|
38
50
|
finally {
|
|
39
51
|
process.removeListener("SIGTERM", abort);
|
|
40
52
|
process.removeListener("SIGINT", abort);
|
|
41
53
|
}
|
|
42
54
|
}
|
|
55
|
+
async function verifyAgent(config, options) {
|
|
56
|
+
try {
|
|
57
|
+
let binary;
|
|
58
|
+
try {
|
|
59
|
+
binary = resolveProviderBinary(config.provider, config.providerPath);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!(error instanceof ProviderBinaryError && config.providerPath)) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
binary = resolveProviderBinary(config.provider);
|
|
66
|
+
}
|
|
67
|
+
config.providerPath = binary;
|
|
68
|
+
await storeAgent(config, options.home);
|
|
69
|
+
if (config.provider === "claude_code") {
|
|
70
|
+
await verifyClaudeCode(binary);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
await verifyCodex(binary);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof ProviderBinaryError) {
|
|
78
|
+
try {
|
|
79
|
+
await new AgentGateway(config).request("/agent-gateway/provider-unavailable");
|
|
80
|
+
}
|
|
81
|
+
catch (reportError) {
|
|
82
|
+
if (reportError instanceof GatewayError &&
|
|
83
|
+
(reportError.status === 401 || reportError.status === 410)) {
|
|
84
|
+
throw reportError;
|
|
85
|
+
}
|
|
86
|
+
// Older servers may not support diagnostics; local shutdown still applies.
|
|
87
|
+
}
|
|
88
|
+
console.error(error.message);
|
|
89
|
+
// launchd cannot exclude exit 78; unload but retain the plist for repair.
|
|
90
|
+
if ((options.platform ?? process.platform) === "darwin") {
|
|
91
|
+
unloadLaunchAgent(config, options);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -7,9 +7,12 @@ import { AgentHttpError, fetchAgentStatus, inspectProvider, redeemAgent, setupAp
|
|
|
7
7
|
import { parseAgentLink, redactAgentOutput } from "../agent-link.js";
|
|
8
8
|
import { installAgentService, uninstallAgentService, } from "../agent-service.js";
|
|
9
9
|
import { emit, emitJson, renderTable, warn } from "../format.js";
|
|
10
|
+
import { installSharedGateway } from "../gateway-install.js";
|
|
10
11
|
import { homeDir } from "../install.js";
|
|
11
12
|
import { defaultDeviceName, hashKey, mintKey } from "../login.js";
|
|
12
|
-
|
|
13
|
+
import { resolveProviderBinary } from "../provider-binary.js";
|
|
14
|
+
const MAX_PREVIOUS_AGENT_IDS = 100;
|
|
15
|
+
export async function runAgent(parsed, dependencies = {}) {
|
|
13
16
|
switch (parsed.action) {
|
|
14
17
|
case "link": {
|
|
15
18
|
if (!(parsed.options.yes || (await approveInstall()))) {
|
|
@@ -18,7 +21,7 @@ export async function runAgent(parsed) {
|
|
|
18
21
|
const { agent, service } = await installLinkedAgent(parsed);
|
|
19
22
|
emit(`Linked ${redactAgentOutput(agent.name)} (${agent.agentId}). Waiting for agent service…`);
|
|
20
23
|
if (!service.started) {
|
|
21
|
-
warn(`Start it in the foreground: npx --yes talentos@
|
|
24
|
+
warn(`Start it in the foreground: npx --yes --prefer-online talentos@latest agent serve --agent ${agent.agentId}`);
|
|
22
25
|
}
|
|
23
26
|
const status = await waitForHeartbeat(agent);
|
|
24
27
|
if (!status) {
|
|
@@ -38,6 +41,45 @@ export async function runAgent(parsed) {
|
|
|
38
41
|
}
|
|
39
42
|
return await command.runAgentServe(parsed);
|
|
40
43
|
}
|
|
44
|
+
case "repair": {
|
|
45
|
+
if (process.platform === "win32") {
|
|
46
|
+
throw new Error("Windows agents run in the foreground. Use talentos agent serve.");
|
|
47
|
+
}
|
|
48
|
+
const agents = await selectAgents(parsed, false, dependencies.home);
|
|
49
|
+
if (!agents.length) {
|
|
50
|
+
throw new Error("No linked agent matches. Run talentos agent list.");
|
|
51
|
+
}
|
|
52
|
+
await installSharedGateway({
|
|
53
|
+
home: dependencies.home,
|
|
54
|
+
source: dependencies.gatewaySource,
|
|
55
|
+
});
|
|
56
|
+
let failed = 0;
|
|
57
|
+
for (const agent of agents) {
|
|
58
|
+
try {
|
|
59
|
+
agent.providerPath = (dependencies.resolveBinary ?? resolveProviderBinary)(agent.provider);
|
|
60
|
+
(dependencies.probe ?? inspectProvider)(agent.provider, agent.providerPath);
|
|
61
|
+
await storeAgent(agent, dependencies.home);
|
|
62
|
+
const service = await (dependencies.install ??
|
|
63
|
+
((config) => installAgentService(config, { home: dependencies.home })))(agent);
|
|
64
|
+
emit(`Repaired ${redactAgentOutput(agent.name)} (${agent.agentId}).${service.started ? "" : " Start talentos agent serve in the foreground."}`);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
failed++;
|
|
68
|
+
const reason = error instanceof Error
|
|
69
|
+
? error.message
|
|
70
|
+
: "Check the provider installation and login.";
|
|
71
|
+
warn(redactAgentOutput(`Could not repair ${agent.name} (${agent.agentId}): ${reason}`, [agent.apiKey]));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (failed) {
|
|
75
|
+
throw new Error(`${failed} agent${failed === 1 ? "" : "s"} could not be repaired. See the messages above.`);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
case "mcp": {
|
|
80
|
+
await import("../agent/mcp-entry.js");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
41
83
|
case "status":
|
|
42
84
|
case "list":
|
|
43
85
|
return await showAgents(parsed);
|
|
@@ -46,7 +88,7 @@ export async function runAgent(parsed) {
|
|
|
46
88
|
case "logs":
|
|
47
89
|
return await showLogs(parsed);
|
|
48
90
|
default:
|
|
49
|
-
throw new Error("Use talentos agent link | status | list | unlink | logs.");
|
|
91
|
+
throw new Error("Use talentos agent link | repair | status | list | unlink | logs.");
|
|
50
92
|
}
|
|
51
93
|
}
|
|
52
94
|
export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
@@ -56,12 +98,19 @@ export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
|
56
98
|
const link = parseAgentLink(parsed.options.url ?? "");
|
|
57
99
|
const home = dependencies.home ?? homeDir();
|
|
58
100
|
const linkHash = hashKey(link.token);
|
|
59
|
-
|
|
101
|
+
const providerPath = (dependencies.resolveBinary ?? resolveProviderBinary)(link.provider);
|
|
102
|
+
const savedAgents = await localAgents(home);
|
|
103
|
+
let agent = savedAgents.find((item) => item.linkHash === linkHash && item.provider === link.provider);
|
|
60
104
|
if (agent) {
|
|
105
|
+
agent.providerPath = providerPath;
|
|
61
106
|
await storeAgent(agent, home);
|
|
62
107
|
}
|
|
63
108
|
else {
|
|
64
109
|
const apiUrl = await setupApiOrigin(link, parsed.options["api-url"] ?? process.env.TALENTOS_API_URL);
|
|
110
|
+
const candidates = savedAgents
|
|
111
|
+
.filter((item) => item.provider === link.provider && item.apiUrl === apiUrl)
|
|
112
|
+
.sort((a, b) => b.linkedAt - a.linkedAt)
|
|
113
|
+
.slice(0, MAX_PREVIOUS_AGENT_IDS);
|
|
65
114
|
const pendingPath = join(agentsDir(home), `.pending-${linkHash}.json`);
|
|
66
115
|
let apiKey;
|
|
67
116
|
let recovered;
|
|
@@ -104,9 +153,10 @@ export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
|
104
153
|
const identity = recovered ??
|
|
105
154
|
(await redeemAgent(apiUrl, {
|
|
106
155
|
token: link.token,
|
|
156
|
+
previousAgentIds: candidates.map((item) => item.agentId),
|
|
107
157
|
keyHash: hashKey(apiKey),
|
|
108
158
|
provider: link.provider,
|
|
109
|
-
providerVersion: (dependencies.probe ?? inspectProvider)(link.provider),
|
|
159
|
+
providerVersion: (dependencies.probe ?? inspectProvider)(link.provider, providerPath),
|
|
110
160
|
deviceName: parsed.options.name ?? defaultDeviceName(),
|
|
111
161
|
os: process.platform,
|
|
112
162
|
cliVersion: packageVersion(),
|
|
@@ -115,15 +165,29 @@ export async function installLinkedAgent(parsed, dependencies = {}) {
|
|
|
115
165
|
agentId: identity.agentId,
|
|
116
166
|
name: identity.name,
|
|
117
167
|
provider: link.provider,
|
|
168
|
+
providerPath,
|
|
118
169
|
apiKey,
|
|
119
170
|
apiUrl,
|
|
120
171
|
linkHash,
|
|
121
172
|
linkedAt: Date.now(),
|
|
122
173
|
};
|
|
123
174
|
await storeAgent(agent, home);
|
|
175
|
+
for (const old of candidates) {
|
|
176
|
+
if (old.agentId !== agent.agentId &&
|
|
177
|
+
identity.retiredAgentIds?.includes(old.agentId)) {
|
|
178
|
+
await uninstallAgentService(old, { home });
|
|
179
|
+
await rm(agentConfigPath(old.agentId, home), { force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
124
182
|
await rm(pendingPath, { force: true });
|
|
125
183
|
}
|
|
126
|
-
const service = await (dependencies.install ??
|
|
184
|
+
const service = await (dependencies.install ??
|
|
185
|
+
(async (config) => {
|
|
186
|
+
if (process.platform !== "win32") {
|
|
187
|
+
await installSharedGateway({ home });
|
|
188
|
+
}
|
|
189
|
+
return await installAgentService(config, { home });
|
|
190
|
+
}))(agent);
|
|
127
191
|
return { agent, service };
|
|
128
192
|
}
|
|
129
193
|
async function showAgents(parsed) {
|
|
@@ -204,9 +268,9 @@ async function showLogs(parsed) {
|
|
|
204
268
|
await log.close();
|
|
205
269
|
}
|
|
206
270
|
}
|
|
207
|
-
async function selectAgents(parsed, single = false) {
|
|
271
|
+
async function selectAgents(parsed, single = false, home) {
|
|
208
272
|
const selector = parsed.options.agent ?? parsed.args[0];
|
|
209
|
-
const agents = (await localAgents()).filter((agent) => !selector || agent.agentId === selector || agent.name === selector);
|
|
273
|
+
const agents = (await localAgents(home)).filter((agent) => !selector || agent.agentId === selector || agent.name === selector);
|
|
210
274
|
if (single && agents.length > 1) {
|
|
211
275
|
throw new Error("Several agents are linked. Select one by name or --agent <id>.");
|
|
212
276
|
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { cp, link, mkdir, readdir, readFile, readlink, realpath, rename, rm, stat, symlink, } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { writePrivateFile } from "./agent-config.js";
|
|
9
|
+
import { compareGatewayVersions } from "./gateway-version.js";
|
|
10
|
+
import { homeDir } from "./install.js";
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
const TRANSIENT_BIN = /(?:\/_npx\/|node_modules\/\.bin|node-gyp-bin|\/Cellar\/|\/\.nvm\/versions\/)/;
|
|
13
|
+
const NPM_ENV = new Set([
|
|
14
|
+
"HOME",
|
|
15
|
+
"USER",
|
|
16
|
+
"LOGNAME",
|
|
17
|
+
"LANG",
|
|
18
|
+
"LC_ALL",
|
|
19
|
+
"TMPDIR",
|
|
20
|
+
"TEMP",
|
|
21
|
+
"TMP",
|
|
22
|
+
"SystemRoot",
|
|
23
|
+
"HTTP_PROXY",
|
|
24
|
+
"HTTPS_PROXY",
|
|
25
|
+
"NO_PROXY",
|
|
26
|
+
"NODE_EXTRA_CA_CERTS",
|
|
27
|
+
]);
|
|
28
|
+
export function gatewayLauncherPath(home = homeDir()) {
|
|
29
|
+
return join(home, ".talentos", "bin", "talentos-gateway");
|
|
30
|
+
}
|
|
31
|
+
export function servicePathEnv(home = homeDir()) {
|
|
32
|
+
return [
|
|
33
|
+
...new Set([
|
|
34
|
+
...(process.env.PATH ?? "")
|
|
35
|
+
.split(":")
|
|
36
|
+
.filter((entry) => entry.startsWith("/") && !TRANSIENT_BIN.test(entry)),
|
|
37
|
+
join(home, ".local/bin"),
|
|
38
|
+
join(home, ".npm-global/bin"),
|
|
39
|
+
"/opt/homebrew/bin",
|
|
40
|
+
"/usr/local/bin",
|
|
41
|
+
"/usr/bin",
|
|
42
|
+
"/bin",
|
|
43
|
+
]),
|
|
44
|
+
].join(":");
|
|
45
|
+
}
|
|
46
|
+
// No user paths are interpolated into executable shell text. The Node hint is data.
|
|
47
|
+
const LAUNCHER = `#!/bin/sh
|
|
48
|
+
set -f
|
|
49
|
+
root="\${0%/*}/.."
|
|
50
|
+
agent=
|
|
51
|
+
next_agent=
|
|
52
|
+
for argument in "$@"; do
|
|
53
|
+
if [ "$next_agent" = yes ]; then agent=$argument; break; fi
|
|
54
|
+
if [ "$argument" = --agent ]; then next_agent=yes; fi
|
|
55
|
+
done
|
|
56
|
+
node_missing=
|
|
57
|
+
case "$agent" in
|
|
58
|
+
''|*[!a-zA-Z0-9_-]*) ;;
|
|
59
|
+
*) node_missing="$root/agents/$agent/.node-missing" ;;
|
|
60
|
+
esac
|
|
61
|
+
qualifies() {
|
|
62
|
+
[ -n "$1" ] && [ -x "$1" ] && "$1" -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)' >/dev/null 2>&1
|
|
63
|
+
}
|
|
64
|
+
start() {
|
|
65
|
+
if [ -n "$node_missing" ]; then /bin/rm -f "$node_missing"; fi
|
|
66
|
+
node=$1
|
|
67
|
+
shift
|
|
68
|
+
PATH="\${node%/*}:\${PATH:-/usr/bin:/bin}"
|
|
69
|
+
export PATH
|
|
70
|
+
exec "$node" "$root/cli/current/node_modules/talentos/dist/index.js" "$@"
|
|
71
|
+
}
|
|
72
|
+
node_hint=
|
|
73
|
+
IFS= read -r node_hint < "$root/cli/node-path" || :
|
|
74
|
+
if qualifies "\${TALENTOS_NODE:-}"; then
|
|
75
|
+
start "$TALENTOS_NODE" "$@"
|
|
76
|
+
fi
|
|
77
|
+
if qualifies "$node_hint"; then
|
|
78
|
+
start "$node_hint" "$@"
|
|
79
|
+
fi
|
|
80
|
+
old_ifs=$IFS
|
|
81
|
+
IFS=:
|
|
82
|
+
for dir in \${PATH:-}; do
|
|
83
|
+
IFS=$old_ifs
|
|
84
|
+
if qualifies "$dir/node"; then
|
|
85
|
+
start "$dir/node" "$@"
|
|
86
|
+
fi
|
|
87
|
+
IFS=:
|
|
88
|
+
done
|
|
89
|
+
IFS=$old_ifs
|
|
90
|
+
for candidate in /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
91
|
+
if qualifies "$candidate"; then
|
|
92
|
+
start "$candidate" "$@"
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
# nvm has no stable system bin; discover surviving installations at startup.
|
|
96
|
+
set +f
|
|
97
|
+
for candidate in "$root"/../.nvm/versions/node/*/bin/node; do
|
|
98
|
+
if qualifies "$candidate"; then start "$candidate" "$@"; fi
|
|
99
|
+
done
|
|
100
|
+
message='TalentOS gateway requires Node.js 20 or newer. Install Node, then run talentos agent repair.'
|
|
101
|
+
if [ -z "$node_missing" ]; then
|
|
102
|
+
echo "$message" >&2
|
|
103
|
+
elif (set -C; : > "$node_missing") 2>/dev/null; then
|
|
104
|
+
echo "$message" >> "$root/agents/$agent/gateway.log"
|
|
105
|
+
fi
|
|
106
|
+
exit 78
|
|
107
|
+
`;
|
|
108
|
+
async function manifestVersion(root) {
|
|
109
|
+
return JSON.parse(await readFile(join(root, "package.json"), "utf8")).version;
|
|
110
|
+
}
|
|
111
|
+
async function copyPackage(source, destination) {
|
|
112
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
113
|
+
for (const name of ["package.json", "dist", "node_modules"]) {
|
|
114
|
+
try {
|
|
115
|
+
await cp(join(source, name), join(destination, name), {
|
|
116
|
+
recursive: true,
|
|
117
|
+
dereference: true,
|
|
118
|
+
force: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (name !== "node_modules" ||
|
|
123
|
+
error.code !== "ENOENT") {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function installed(root, version) {
|
|
130
|
+
try {
|
|
131
|
+
if ((await manifestVersion(root)) !== version ||
|
|
132
|
+
!(await readFile(join(root, "dist/index.js"))).length) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const { dependencies = {} } = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
136
|
+
// The SDK has no root entrypoint. Load the same exports the CLI uses;
|
|
137
|
+
// imports also catch missing transitive dependencies in partial installs.
|
|
138
|
+
const imports = Object.keys(dependencies).flatMap((name) => name === "@modelcontextprotocol/sdk"
|
|
139
|
+
? [
|
|
140
|
+
`${name}/client/index.js`,
|
|
141
|
+
`${name}/server/index.js`,
|
|
142
|
+
`${name}/types.js`,
|
|
143
|
+
]
|
|
144
|
+
: [name]);
|
|
145
|
+
if (imports.length) {
|
|
146
|
+
await exec(process.execPath, [
|
|
147
|
+
"--input-type=module",
|
|
148
|
+
"--eval",
|
|
149
|
+
`await Promise.all(${JSON.stringify(imports)}.map(name => import(name)))`,
|
|
150
|
+
], { cwd: root, timeout: 15_000 });
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error.killed) {
|
|
156
|
+
throw new Error("Gateway dependency check timed out. Keeping the current version; retry agent repair shortly.");
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function installLock(root, signal) {
|
|
162
|
+
const path = join(root, ".install-lock");
|
|
163
|
+
const pending = join(root, `.lock-${randomUUID()}`);
|
|
164
|
+
const contents = `${process.pid}\n${randomUUID()}`;
|
|
165
|
+
// Publish a populated lock atomically so a second installer never sees an
|
|
166
|
+
// empty new lock and mistakes it for one left behind by a crash.
|
|
167
|
+
await writePrivateFile(pending, contents);
|
|
168
|
+
try {
|
|
169
|
+
for (let attempt = 0; attempt < 600; attempt++) {
|
|
170
|
+
signal?.throwIfAborted();
|
|
171
|
+
try {
|
|
172
|
+
await link(pending, path);
|
|
173
|
+
return async () => {
|
|
174
|
+
if ((await readFile(path, "utf8").catch(() => "")) === contents) {
|
|
175
|
+
await rm(path, { force: true });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code !== "EEXIST") {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const lock = await readFile(path, "utf8");
|
|
185
|
+
const metadata = await stat(path);
|
|
186
|
+
const age = Date.now() - metadata.mtimeMs;
|
|
187
|
+
const pid = Number(lock.split("\n")[0]);
|
|
188
|
+
let stale = !lock.trim() || age > 5 * 60_000;
|
|
189
|
+
if (!stale && pid > 0) {
|
|
190
|
+
try {
|
|
191
|
+
process.kill(pid, 0);
|
|
192
|
+
}
|
|
193
|
+
catch (failure) {
|
|
194
|
+
stale = failure.code === "ESRCH";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (stale && (await readFile(path, "utf8")) === lock) {
|
|
198
|
+
const latest = await stat(path);
|
|
199
|
+
if (latest.dev === metadata.dev && latest.ino === metadata.ino) {
|
|
200
|
+
await rm(path, { force: true });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error.code !== "ENOENT") {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
await delay(200, undefined, { signal });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Another gateway installation is still running. Retry agent repair shortly.");
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await rm(pending, { force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export async function installSharedGateway(options = {}) {
|
|
221
|
+
const home = options.home ?? homeDir();
|
|
222
|
+
const source = options.source ?? fileURLToPath(new URL("../", import.meta.url));
|
|
223
|
+
const ownVersion = await manifestVersion(source);
|
|
224
|
+
let version = options.version ?? ownVersion;
|
|
225
|
+
if (compareGatewayVersions(version, version) === undefined) {
|
|
226
|
+
throw new Error("Unsupported gateway version.");
|
|
227
|
+
}
|
|
228
|
+
const root = join(home, ".talentos/cli");
|
|
229
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
230
|
+
const release = await installLock(root, options.signal);
|
|
231
|
+
const temporary = join(root, `.install-${randomUUID()}`);
|
|
232
|
+
const pointer = join(root, `.current-${randomUUID()}`);
|
|
233
|
+
let stagedVersion;
|
|
234
|
+
try {
|
|
235
|
+
const active = await readlink(join(root, "current")).catch(() => "");
|
|
236
|
+
if ((compareGatewayVersions(active, version) ?? 0) > 0 &&
|
|
237
|
+
(await installed(join(root, active, "node_modules/talentos"), active))) {
|
|
238
|
+
version = active;
|
|
239
|
+
}
|
|
240
|
+
const destination = join(root, version);
|
|
241
|
+
const pkg = join(destination, "node_modules/talentos");
|
|
242
|
+
if (!(await installed(pkg, version))) {
|
|
243
|
+
await mkdir(temporary, { mode: 0o700 });
|
|
244
|
+
// Local builds have not been published. Copy resolved dependencies too,
|
|
245
|
+
// so pnpm's symlinks cannot strand the installed gateway in a worktree.
|
|
246
|
+
if (version === ownVersion &&
|
|
247
|
+
!source.includes("/node_modules/talentos")) {
|
|
248
|
+
await copyPackage(source, join(temporary, "node_modules/talentos"));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const npmEnv = {
|
|
252
|
+
PATH: `${process.env.PATH ?? servicePathEnv()}:${dirname(process.execPath)}`,
|
|
253
|
+
};
|
|
254
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
255
|
+
if ((NPM_ENV.has(key) || key.toLowerCase().startsWith("npm_config_")) &&
|
|
256
|
+
value) {
|
|
257
|
+
npmEnv[key] = value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await exec("npm", [
|
|
262
|
+
"install",
|
|
263
|
+
"--prefix",
|
|
264
|
+
temporary,
|
|
265
|
+
"--omit=dev",
|
|
266
|
+
"--ignore-scripts",
|
|
267
|
+
"--no-audit",
|
|
268
|
+
"--no-fund",
|
|
269
|
+
`talentos@${version}`,
|
|
270
|
+
], { env: npmEnv, signal: options.signal, timeout: 90_000 });
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
if (version === ownVersion) {
|
|
274
|
+
await rm(temporary, { recursive: true, force: true });
|
|
275
|
+
await copyPackage(source, join(temporary, "node_modules/talentos"));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
throw new Error("Gateway update failed. Serving the current version; retrying when the network is available.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!(await installed(join(temporary, "node_modules/talentos"), version))) {
|
|
283
|
+
throw new Error("Gateway package is incomplete. Restore its runtime dependencies or retry agent repair online.");
|
|
284
|
+
}
|
|
285
|
+
await rm(destination, { recursive: true, force: true });
|
|
286
|
+
await rename(temporary, destination);
|
|
287
|
+
if (version !== active) {
|
|
288
|
+
stagedVersion = destination;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
let previous;
|
|
292
|
+
try {
|
|
293
|
+
previous = await readlink(join(root, "current"));
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
/* First install. */
|
|
297
|
+
}
|
|
298
|
+
await symlink(version, pointer);
|
|
299
|
+
await rename(pointer, join(root, "current"));
|
|
300
|
+
stagedVersion = undefined;
|
|
301
|
+
const launcher = gatewayLauncherPath(home);
|
|
302
|
+
await writePrivateFile(launcher, LAUNCHER, 0o700);
|
|
303
|
+
let nodePath = options.nodePath ?? process.execPath;
|
|
304
|
+
for (const stable of [
|
|
305
|
+
"/opt/homebrew/bin/node",
|
|
306
|
+
"/usr/local/bin/node",
|
|
307
|
+
"/usr/bin/node",
|
|
308
|
+
]) {
|
|
309
|
+
if ((await realpath(stable).catch(() => "")) === nodePath) {
|
|
310
|
+
nodePath = stable;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Preserve the link-time hint during updates and repairs.
|
|
315
|
+
const hint = join(root, "node-path");
|
|
316
|
+
try {
|
|
317
|
+
await readFile(hint);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
await writePrivateFile(hint, `${nodePath}\n`);
|
|
321
|
+
}
|
|
322
|
+
const keep = new Set([version, previous]);
|
|
323
|
+
const oldVersions = (await readdir(root))
|
|
324
|
+
.filter((entry) => compareGatewayVersions(entry, entry) !== undefined && !keep.has(entry))
|
|
325
|
+
.sort((a, b) => compareGatewayVersions(b, a) ?? 0);
|
|
326
|
+
// A repeat repair retains the other installed version.
|
|
327
|
+
if (previous === version) {
|
|
328
|
+
oldVersions.shift();
|
|
329
|
+
}
|
|
330
|
+
for (const old of oldVersions) {
|
|
331
|
+
await rm(join(root, old), { recursive: true, force: true });
|
|
332
|
+
}
|
|
333
|
+
return launcher;
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
await rm(temporary, { recursive: true, force: true });
|
|
337
|
+
await rm(pointer, { force: true });
|
|
338
|
+
if (stagedVersion) {
|
|
339
|
+
await rm(stagedVersion, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
await release();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/;
|
|
2
|
+
const NUMERIC_IDENTIFIER = /^\d+$/;
|
|
3
|
+
export function compareGatewayVersions(a, b) {
|
|
4
|
+
const left = VERSION.exec(a), right = VERSION.exec(b);
|
|
5
|
+
if (!(left && right)) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
for (let i = 1; i <= 3; i++) {
|
|
9
|
+
const difference = Number(left[i]) - Number(right[i]);
|
|
10
|
+
if (difference) {
|
|
11
|
+
return difference;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (left[4] === right[4]) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
if (!left[4]) {
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
if (!right[4]) {
|
|
21
|
+
return -1;
|
|
22
|
+
}
|
|
23
|
+
const l = left[4].split("."), r = right[4].split(".");
|
|
24
|
+
for (let i = 0; i < Math.max(l.length, r.length); i++) {
|
|
25
|
+
const x = l[i], y = r[i];
|
|
26
|
+
if (x === undefined) {
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
if (y === undefined) {
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
if (x === y) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const xn = NUMERIC_IDENTIFIER.test(x), yn = NUMERIC_IDENTIFIER.test(y);
|
|
36
|
+
if (xn && yn) {
|
|
37
|
+
return Number(x) - Number(y);
|
|
38
|
+
}
|
|
39
|
+
if (xn !== yn) {
|
|
40
|
+
return xn ? -1 : 1;
|
|
41
|
+
}
|
|
42
|
+
return x < y ? -1 : 1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|