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 +145 -103
- 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 +52 -0
- package/dist/client.js +87 -0
- package/dist/commands/agent-serve.js +42 -0
- package/dist/commands/agent.js +234 -0
- package/dist/commands/ask.js +74 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/login.js +125 -0
- package/dist/commands/mcp.js +48 -0
- package/dist/commands/memory.js +61 -0
- package/dist/commands/routines.js +44 -0
- package/dist/commands/skills.js +183 -0
- package/dist/commands/whoami.js +29 -0
- package/dist/config.js +70 -0
- package/dist/format.js +54 -0
- package/dist/index.js +139 -91
- package/dist/install.js +253 -0
- package/dist/login.js +86 -0
- package/dist/mcp-proxy.js +17 -0
- package/dist/skill-files.js +109 -0
- package/package.json +12 -4
- package/dist/api.js +0 -210
|
@@ -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
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
const CLI_OPTIONS = {
|
|
3
|
+
json: { type: "boolean" },
|
|
4
|
+
help: { type: "boolean", short: "h" },
|
|
5
|
+
version: { type: "boolean", short: "v" },
|
|
6
|
+
key: { type: "string" },
|
|
7
|
+
"api-url": { type: "string" },
|
|
8
|
+
name: { type: "string" },
|
|
9
|
+
out: { type: "string" },
|
|
10
|
+
to: { type: "string" },
|
|
11
|
+
id: { type: "string" },
|
|
12
|
+
mine: { type: "boolean" },
|
|
13
|
+
company: { type: "boolean" },
|
|
14
|
+
personal: { type: "boolean" },
|
|
15
|
+
query: { type: "string", short: "q" },
|
|
16
|
+
thread: { type: "string" },
|
|
17
|
+
"no-wait": { type: "boolean" },
|
|
18
|
+
for: { type: "string" },
|
|
19
|
+
global: { type: "boolean" },
|
|
20
|
+
apply: { type: "boolean" },
|
|
21
|
+
"no-pull": { type: "boolean" },
|
|
22
|
+
url: { type: "string" },
|
|
23
|
+
yes: { type: "boolean" },
|
|
24
|
+
agent: { type: "string" },
|
|
25
|
+
};
|
|
26
|
+
/** Groups whose first positional is a sub-command rather than an argument. */
|
|
27
|
+
const ACTION_GROUPS = new Set(["agent", "mcp", "skills", "memory", "routines"]);
|
|
28
|
+
export function parseArgv(argv) {
|
|
29
|
+
const { values, positionals } = parse(argv);
|
|
30
|
+
const group = positionals[0];
|
|
31
|
+
const hasAction = group !== undefined && ACTION_GROUPS.has(group);
|
|
32
|
+
const action = hasAction ? positionals[1] : undefined;
|
|
33
|
+
return {
|
|
34
|
+
group,
|
|
35
|
+
action,
|
|
36
|
+
args: positionals.slice(action === undefined ? 1 : 2),
|
|
37
|
+
options: values,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function parse(argv) {
|
|
41
|
+
try {
|
|
42
|
+
return parseArgs({
|
|
43
|
+
args: argv,
|
|
44
|
+
options: CLI_OPTIONS,
|
|
45
|
+
allowPositionals: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
50
|
+
throw new Error(`${message} Run \`talentos --help\`.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { requireCredentials, resolveApiUrl, } from "./config.js";
|
|
4
|
+
const CLIENT_INFO = { name: "talentos-cli", version: "1.0.0" };
|
|
5
|
+
const UNAUTHORIZED_HINT = /\b401\b|unauthoriz/i;
|
|
6
|
+
const UNREACHABLE_HINT = /fetch failed|ECONNREFUSED|ENOTFOUND/i;
|
|
7
|
+
export const UNAUTHORIZED = "Not logged in or key revoked. Run `talentos login`.";
|
|
8
|
+
/** MCP client for the remote TalentOS server, one process-lived connection. */
|
|
9
|
+
export class AgentClient {
|
|
10
|
+
credentials;
|
|
11
|
+
client;
|
|
12
|
+
constructor(credentials) {
|
|
13
|
+
this.credentials = credentials;
|
|
14
|
+
}
|
|
15
|
+
async call(name, args) {
|
|
16
|
+
const client = await this.connect();
|
|
17
|
+
const result = (await withFriendlyErrors(() => client.callTool({ name, arguments: args })));
|
|
18
|
+
const text = firstText(result);
|
|
19
|
+
if (result.isError) {
|
|
20
|
+
throw new Error(text || `Tool ${name} failed.`);
|
|
21
|
+
}
|
|
22
|
+
return { text, data: result.structuredContent };
|
|
23
|
+
}
|
|
24
|
+
async listTools() {
|
|
25
|
+
const client = await this.connect();
|
|
26
|
+
return await withFriendlyErrors(() => client.listTools());
|
|
27
|
+
}
|
|
28
|
+
async callTool(params) {
|
|
29
|
+
const client = await this.connect();
|
|
30
|
+
return (await withFriendlyErrors(() => client.callTool(params)));
|
|
31
|
+
}
|
|
32
|
+
async close() {
|
|
33
|
+
const client = this.client;
|
|
34
|
+
this.client = undefined;
|
|
35
|
+
await client?.close();
|
|
36
|
+
}
|
|
37
|
+
async connect() {
|
|
38
|
+
if (this.client) {
|
|
39
|
+
return this.client;
|
|
40
|
+
}
|
|
41
|
+
const client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
42
|
+
const transport = new StreamableHTTPClientTransport(new URL(`${this.credentials.apiUrl}/mcp`), {
|
|
43
|
+
requestInit: {
|
|
44
|
+
headers: { Authorization: `Bearer ${this.credentials.apiKey}` },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
await withFriendlyErrors(() => client.connect(transport));
|
|
48
|
+
this.client = client;
|
|
49
|
+
return client;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function withClient(apiUrlOverride, run) {
|
|
53
|
+
const credentials = await requireCredentials();
|
|
54
|
+
const client = new AgentClient({
|
|
55
|
+
...credentials,
|
|
56
|
+
apiUrl: resolveApiUrl(apiUrlOverride, credentials.apiUrl),
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
return await run(client);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await client.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function firstText(result) {
|
|
66
|
+
const block = result.content?.[0];
|
|
67
|
+
return block && block.type === "text" ? block.text : "";
|
|
68
|
+
}
|
|
69
|
+
async function withFriendlyErrors(run) {
|
|
70
|
+
try {
|
|
71
|
+
return await run();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new Error(describe(error));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function describe(error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
const code = error?.code;
|
|
80
|
+
if (code === 401 || UNAUTHORIZED_HINT.test(message)) {
|
|
81
|
+
return UNAUTHORIZED;
|
|
82
|
+
}
|
|
83
|
+
if (UNREACHABLE_HINT.test(message)) {
|
|
84
|
+
return "Cannot reach TalentOS. Check your connection or --api-url.";
|
|
85
|
+
}
|
|
86
|
+
return message;
|
|
87
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { withClient } from "../client.js";
|
|
2
|
+
import { dim, emit, emitJson, emitLink } from "../format.js";
|
|
3
|
+
const POLL_INTERVAL_MS = 2000;
|
|
4
|
+
const MAX_WAIT_MS = 10 * 60 * 1000;
|
|
5
|
+
// The two non-terminal webChannelMessages statuses; anything else means the
|
|
6
|
+
// Talent is done with that message.
|
|
7
|
+
const IN_FLIGHT_STATUSES = new Set(["queued", "claimed"]);
|
|
8
|
+
export function isTerminalStatus(status) {
|
|
9
|
+
return !IN_FLIGHT_STATUSES.has(status);
|
|
10
|
+
}
|
|
11
|
+
export function replyAfter(messages, messageId) {
|
|
12
|
+
const index = messages.findIndex((message) => message.id === messageId);
|
|
13
|
+
const candidates = messages
|
|
14
|
+
.slice(index + 1)
|
|
15
|
+
.filter((message) => message.role === "talent");
|
|
16
|
+
return candidates.at(-1);
|
|
17
|
+
}
|
|
18
|
+
export async function runAsk(parsed) {
|
|
19
|
+
const message = parsed.args.join(" ").trim();
|
|
20
|
+
if (message === "") {
|
|
21
|
+
throw new Error('Usage: talentos ask "<message>" [--thread <id>] [--no-wait]');
|
|
22
|
+
}
|
|
23
|
+
await withClient(parsed.options["api-url"], async (client) => {
|
|
24
|
+
const { data } = await client.call("ask_talent", {
|
|
25
|
+
message,
|
|
26
|
+
...(parsed.options.thread ? { threadId: parsed.options.thread } : {}),
|
|
27
|
+
});
|
|
28
|
+
const json = parsed.options.json === true;
|
|
29
|
+
if (parsed.options["no-wait"]) {
|
|
30
|
+
reportHandoff(data.threadId, json, data.url);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
emit(dim(`Asked your Talent. Thread ${data.threadId}`));
|
|
34
|
+
const reply = await waitForReply(client, data.threadId, data.messageId);
|
|
35
|
+
if (!reply) {
|
|
36
|
+
reportHandoff(data.threadId, json, data.url);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (json) {
|
|
40
|
+
emitJson({ threadId: data.threadId, message: reply });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
emit(reply.body);
|
|
44
|
+
emitLink(data.url);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function waitForReply(client, threadId, messageId) {
|
|
48
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
49
|
+
while (Date.now() < deadline) {
|
|
50
|
+
await sleep(POLL_INTERVAL_MS);
|
|
51
|
+
const { data } = await client.call("get_thread", { threadId });
|
|
52
|
+
const reply = replyAfter(data.messages, messageId);
|
|
53
|
+
if (reply && isTerminalStatus(reply.status)) {
|
|
54
|
+
return reply;
|
|
55
|
+
}
|
|
56
|
+
if (!data.active && reply) {
|
|
57
|
+
return reply;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
function reportHandoff(threadId, json, url) {
|
|
63
|
+
if (json) {
|
|
64
|
+
emitJson({ threadId });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
emit(`Your Talent is still working. Open thread ${threadId} in TalentOS for the reply, or keep the conversation going with \`talentos ask "<message>" --thread ${threadId}\`.`);
|
|
68
|
+
emitLink(url);
|
|
69
|
+
}
|
|
70
|
+
function sleep(ms) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
setTimeout(resolve, ms);
|
|
73
|
+
});
|
|
74
|
+
}
|