totemlayer 0.1.6 → 0.1.12
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 +26 -44
- package/bin/agent-profile.mjs +18 -2
- package/bin/agent-runner.mjs +25 -17
- package/bin/agent-setup.mjs +141 -55
- package/bin/automatic-runtime.mjs +131 -0
- package/bin/cloud-bootstrap.mjs +64 -0
- package/bin/connected-agent-service.mjs +84 -0
- package/bin/device-login.mjs +168 -0
- package/bin/installation.mjs +160 -0
- package/bin/mcp-install.mjs +74 -18
- package/bin/mcp.mjs +55 -0
- package/bin/mission-agent-service.mjs +40 -2
- package/bin/mission-infrastructure.mjs +609 -0
- package/bin/mission-worker.mjs +59 -3
- package/bin/model-adapter.mjs +164 -0
- package/bin/network-runtime.mjs +58 -3
- package/bin/platform-client.mjs +3 -0
- package/bin/provider-detection.mjs +94 -0
- package/bin/runtime-installation.mjs +305 -0
- package/bin/sdk/index.js +5 -0
- package/bin/totem.mjs +13 -3
- package/bin/worker-core/adapters.mjs +279 -0
- package/bin/worker-core/errors.mjs +60 -0
- package/package.json +2 -1
- package/scripts/prepare.mjs +5 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { opendir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { readAgentProfile } from "./agent-profile.mjs";
|
|
6
|
+
import { agentStartMain, readAgentRuntimeStatus } from "./agent-runner.mjs";
|
|
7
|
+
import {
|
|
8
|
+
privateDirectory,
|
|
9
|
+
privateRead,
|
|
10
|
+
privateWrite,
|
|
11
|
+
runtimeRoot,
|
|
12
|
+
} from "./runtime-installation.mjs";
|
|
13
|
+
import { resumeDeviceLogin } from "./device-login.mjs";
|
|
14
|
+
|
|
15
|
+
export async function setAutomaticEnabled(profile, enabled) {
|
|
16
|
+
await privateWrite(join(profile.stateDirectory, "automatic.json"), { enabled });
|
|
17
|
+
}
|
|
18
|
+
export async function automaticEnabled(profile) {
|
|
19
|
+
try {
|
|
20
|
+
return (await privateRead(join(profile.stateDirectory, "automatic.json"))).enabled === true;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error.code === "ENOENT") return true;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function superviseOnce(env = process.env, options = {}) {
|
|
27
|
+
const directory = join(runtimeRoot(env), "runtime");
|
|
28
|
+
await Promise.resolve((options.resumeLogin ?? resumeDeviceLogin)({ directory }, env)).catch(
|
|
29
|
+
() => {},
|
|
30
|
+
);
|
|
31
|
+
let profiles;
|
|
32
|
+
try {
|
|
33
|
+
profiles = await opendir(join(runtimeRoot(env), "connections"));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === "ENOENT") return [];
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
const results = [];
|
|
39
|
+
for await (const entry of profiles) {
|
|
40
|
+
if (!entry.isDirectory()) continue;
|
|
41
|
+
try {
|
|
42
|
+
const profile = await readAgentProfile(entry.name, { environment: env });
|
|
43
|
+
if (!(await automaticEnabled(profile))) continue;
|
|
44
|
+
const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
45
|
+
if (state.running) continue;
|
|
46
|
+
// The run-local journal decides whether any interrupted work can safely resume.
|
|
47
|
+
await (options.start ?? agentStartMain)(["--profile", entry.name, "--background", "--json"], {
|
|
48
|
+
env,
|
|
49
|
+
automatic: true,
|
|
50
|
+
stdout: { write() {} },
|
|
51
|
+
});
|
|
52
|
+
results.push(entry.name);
|
|
53
|
+
} catch {
|
|
54
|
+
/* One offline/revoked connection cannot stop other profiles. */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
export async function automaticRuntimeMain(env = process.env, options = {}) {
|
|
60
|
+
const directory = join(runtimeRoot(env), "runtime");
|
|
61
|
+
await privateDirectory(directory);
|
|
62
|
+
const lock = join(directory, "supervisor.lock");
|
|
63
|
+
await import("node:fs/promises").then(async ({ mkdir }) => {
|
|
64
|
+
try {
|
|
65
|
+
await mkdir(lock, { mode: 0o700 });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code !== "EEXIST") throw error;
|
|
68
|
+
const current = await privateRead(join(lock, "owner.json")).catch(() => null);
|
|
69
|
+
if (!current || !Number.isInteger(current.pid))
|
|
70
|
+
throw new Error("Another supervisor is starting. Wait before retrying.");
|
|
71
|
+
try {
|
|
72
|
+
process.kill(current.pid, 0);
|
|
73
|
+
throw new Error("The Totem supervisor is already running.");
|
|
74
|
+
} catch (failure) {
|
|
75
|
+
if (failure.code !== "ESRCH") throw failure;
|
|
76
|
+
}
|
|
77
|
+
await rm(lock, { recursive: true });
|
|
78
|
+
await mkdir(lock, { mode: 0o700 });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
await privateWrite(join(lock, "owner.json"), { pid: process.pid });
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const stop = () => controller.abort();
|
|
84
|
+
process.once("SIGTERM", stop);
|
|
85
|
+
process.once("SIGINT", stop);
|
|
86
|
+
try {
|
|
87
|
+
while (!controller.signal.aborted && !options.signal?.aborted) {
|
|
88
|
+
let environment = { ...env };
|
|
89
|
+
try {
|
|
90
|
+
environment = {
|
|
91
|
+
...environment,
|
|
92
|
+
...(await privateRead(join(directory, "environment.json"))),
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error.code !== "ENOENT") throw error;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
environment = {
|
|
99
|
+
...environment,
|
|
100
|
+
...(await privateRead(join(directory, "provider-environment.json"))),
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.code !== "ENOENT") throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await privateWrite(join(directory, "supervisor.json"), {
|
|
107
|
+
pid: process.pid,
|
|
108
|
+
updatedAt: new Date().toISOString(),
|
|
109
|
+
});
|
|
110
|
+
await superviseOnce(environment, options);
|
|
111
|
+
await sleep(options.intervalMs ?? 5000, undefined, {
|
|
112
|
+
signal: options.signal
|
|
113
|
+
? AbortSignal.any([controller.signal, options.signal])
|
|
114
|
+
: controller.signal,
|
|
115
|
+
}).catch(() => {});
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
process.removeListener("SIGTERM", stop);
|
|
119
|
+
process.removeListener("SIGINT", stop);
|
|
120
|
+
await rm(lock, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
process.argv[1] === fileURLToPath(import.meta.url) ||
|
|
125
|
+
process.argv[1]?.endsWith("/runtime/supervisor.mjs") ||
|
|
126
|
+
process.argv[1]?.endsWith("\\runtime\\supervisor.mjs")
|
|
127
|
+
) {
|
|
128
|
+
await automaticRuntimeMain().catch(() => {
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { agentSetupMain, agentStatusMain } from "./agent-setup.mjs";
|
|
2
|
+
import { agentStartMain } from "./agent-runner.mjs";
|
|
3
|
+
import { installationMain } from "./installation.mjs";
|
|
4
|
+
|
|
5
|
+
/** Run from the cloud environment's startup command; secrets stay in its store. */
|
|
6
|
+
export async function cloudBootstrapMain(args = [], options = {}) {
|
|
7
|
+
const env = options.env ?? process.env;
|
|
8
|
+
const output = options.stdout ?? process.stdout;
|
|
9
|
+
if (args.includes("--help")) {
|
|
10
|
+
output.write(
|
|
11
|
+
"totem cloud setup [--provider codex|claude|qwen-local|command|http] [--repo-root <checkout>] [--install-mcp]\nProvision TOTEM_NETWORK_API_KEY or TOTEM_NETWORK_API_KEY_FILE and TOTEM_AGENT_ID in the environment secret/settings store. The setup command verifies the exact identity, saves a private profile, and starts its message and Mission worker.\n",
|
|
12
|
+
);
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
if (!env.TOTEM_NETWORK_API_KEY && !env.TOTEM_NETWORK_API_KEY_FILE) {
|
|
16
|
+
const loginArgs = ["--no-browser"];
|
|
17
|
+
const selected = args.indexOf("--provider");
|
|
18
|
+
if (selected >= 0 && args[selected + 1]) loginArgs.push("--provider", args[selected + 1]);
|
|
19
|
+
if (args.includes("--json")) loginArgs.push("--json");
|
|
20
|
+
return installationMain(loginArgs, options);
|
|
21
|
+
}
|
|
22
|
+
const profile = env.TOTEM_AGENT_ID ?? "default";
|
|
23
|
+
const setup = [
|
|
24
|
+
"--profile",
|
|
25
|
+
profile,
|
|
26
|
+
...(env.TOTEM_AGENT_ID ? ["--agent-id", env.TOTEM_AGENT_ID] : []),
|
|
27
|
+
"--base-url",
|
|
28
|
+
env.TOTEM_BASE_URL ?? "https://totemlayer.com",
|
|
29
|
+
"--json",
|
|
30
|
+
...args,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
if (await agentSetupMain(setup, options)) return 1;
|
|
34
|
+
let readiness;
|
|
35
|
+
const probe = {
|
|
36
|
+
...options,
|
|
37
|
+
stdout: {
|
|
38
|
+
write(value) {
|
|
39
|
+
output.write(value);
|
|
40
|
+
try {
|
|
41
|
+
readiness = JSON.parse(value);
|
|
42
|
+
} catch {
|
|
43
|
+
/* Only status JSON is inspected. */
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
if (await agentStatusMain(["--profile", profile, "--json"], probe)) return 1;
|
|
49
|
+
if (readiness?.checks?.model?.state !== "available") {
|
|
50
|
+
output.write(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: {
|
|
54
|
+
code: "CLOUD_EXECUTOR_UNAVAILABLE",
|
|
55
|
+
message:
|
|
56
|
+
"The cloud connection is saved, but its selected executor is unavailable. Install and authenticate that executor before startup.",
|
|
57
|
+
},
|
|
58
|
+
}) + "\n",
|
|
59
|
+
);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
if (await agentStartMain(["--profile", profile, "--background", "--json"], options)) return 1;
|
|
63
|
+
return agentStatusMain(["--profile", profile, "--json"], options);
|
|
64
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { runMissionAgentService } from "./mission-agent-service.mjs";
|
|
3
|
+
import { runRuntime } from "./network-runtime.mjs";
|
|
4
|
+
|
|
5
|
+
// Both queues use the same verified identity and private profile. A stopped or
|
|
6
|
+
// revoked connection cancels both consumers; Missions retains its own leases.
|
|
7
|
+
export async function runConnectedAgentService(settings, dependencies = {}) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const signal = settings.signal
|
|
10
|
+
? AbortSignal.any([settings.signal, controller.signal])
|
|
11
|
+
: controller.signal;
|
|
12
|
+
const emit = settings.emit ?? (() => {});
|
|
13
|
+
const phases = { messages: "starting", missions: "starting" };
|
|
14
|
+
const report = (queue, event) => {
|
|
15
|
+
phases[queue] = event.type === "completed" ? "waiting" : event.type;
|
|
16
|
+
const values = Object.values(phases);
|
|
17
|
+
const type =
|
|
18
|
+
["reconnecting", "starting", "blocked", "working"].find((phase) => values.includes(phase)) ??
|
|
19
|
+
"waiting";
|
|
20
|
+
emit({ ...event, type });
|
|
21
|
+
};
|
|
22
|
+
const messages = async () => {
|
|
23
|
+
let attempts = 0;
|
|
24
|
+
while (!signal.aborted) {
|
|
25
|
+
try {
|
|
26
|
+
await (dependencies.runRuntime ?? runRuntime)({
|
|
27
|
+
...settings,
|
|
28
|
+
signal,
|
|
29
|
+
stateDir: settings.stateDirectory,
|
|
30
|
+
allowAgents: [],
|
|
31
|
+
modelTimeoutMs: 900000,
|
|
32
|
+
replyTimeoutMs: 90000,
|
|
33
|
+
pollMs: settings.pollMs ?? 5000,
|
|
34
|
+
emit(event) {
|
|
35
|
+
if (event.type === "runtime_started") {
|
|
36
|
+
attempts = 0;
|
|
37
|
+
report("messages", { type: "waiting" });
|
|
38
|
+
} else if (event.type === "runtime_error")
|
|
39
|
+
report("messages", { type: "blocked", code: event.error?.code });
|
|
40
|
+
else if (["message_handled", "chat_completed"].includes(event.type))
|
|
41
|
+
report("messages", { type: "waiting" });
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (!signal.aborted) throw new Error("The message runtime stopped unexpectedly.");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (signal.aborted) return;
|
|
47
|
+
if (
|
|
48
|
+
error.status === 401 ||
|
|
49
|
+
error.status === 403 ||
|
|
50
|
+
error.code === "AGENT_IDENTITY_MISMATCH"
|
|
51
|
+
)
|
|
52
|
+
throw error;
|
|
53
|
+
report("messages", { type: "reconnecting" });
|
|
54
|
+
await sleep(
|
|
55
|
+
error.status === 429 ? 30000 : Math.min(30000, 1000 * 2 ** Math.min(attempts++, 5)),
|
|
56
|
+
undefined,
|
|
57
|
+
{ signal },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const guarded = async (run) => {
|
|
63
|
+
try {
|
|
64
|
+
await run();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!signal.aborted) {
|
|
67
|
+
controller.abort();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const results = await Promise.allSettled([
|
|
73
|
+
guarded(messages),
|
|
74
|
+
guarded(() =>
|
|
75
|
+
(dependencies.runMissions ?? runMissionAgentService)({
|
|
76
|
+
...settings,
|
|
77
|
+
signal,
|
|
78
|
+
emit: (event) => report("missions", event),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
]);
|
|
82
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
83
|
+
if (failure) throw failure.reason;
|
|
84
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
6
|
+
import { detectInstalledProvider, providerArguments } from "./provider-detection.mjs";
|
|
7
|
+
|
|
8
|
+
function origin(value) {
|
|
9
|
+
const url = new URL(value ?? "https://totemlayer.com");
|
|
10
|
+
if (
|
|
11
|
+
url.username ||
|
|
12
|
+
url.password ||
|
|
13
|
+
url.search ||
|
|
14
|
+
url.hash ||
|
|
15
|
+
url.pathname !== "/" ||
|
|
16
|
+
(url.protocol !== "https:" &&
|
|
17
|
+
!(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
18
|
+
)
|
|
19
|
+
throw new Error("Choose an HTTPS Totem origin or local development origin.");
|
|
20
|
+
return url.origin;
|
|
21
|
+
}
|
|
22
|
+
async function request(baseUrl, path, options = {}, fetcher = fetch) {
|
|
23
|
+
const response = await fetcher(baseUrl + path, {
|
|
24
|
+
...options,
|
|
25
|
+
redirect: "error",
|
|
26
|
+
signal: AbortSignal.timeout(15000),
|
|
27
|
+
});
|
|
28
|
+
const result = await response.json();
|
|
29
|
+
if (!response.ok && response.status !== 410)
|
|
30
|
+
throw new Error("Totem could not complete this installation request.");
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
export async function beginDeviceLogin(runtime, env = process.env, options = {}) {
|
|
34
|
+
const baseUrl = origin(env.TOTEM_BASE_URL);
|
|
35
|
+
let pending;
|
|
36
|
+
try {
|
|
37
|
+
pending = await privateRead(join(runtime.directory, "connection-request.json"));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== "ENOENT") throw error;
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
!pending ||
|
|
43
|
+
pending.baseUrl !== baseUrl ||
|
|
44
|
+
pending.requestedAgentId !== env.TOTEM_AGENT_ID ||
|
|
45
|
+
pending.provider !== (env.TOTEM_PROVIDER ?? "auto") ||
|
|
46
|
+
new Date(pending.expiresAt) <= new Date()
|
|
47
|
+
) {
|
|
48
|
+
const result = await request(
|
|
49
|
+
baseUrl,
|
|
50
|
+
"/api/installations",
|
|
51
|
+
{
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
deviceName: hostname().slice(0, 100),
|
|
56
|
+
provider: env.TOTEM_PROVIDER ?? "auto",
|
|
57
|
+
...(env.TOTEM_AGENT_ID ? { agentId: env.TOTEM_AGENT_ID } : {}),
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
options.fetch,
|
|
61
|
+
);
|
|
62
|
+
if (
|
|
63
|
+
!/^[a-f0-9-]{36}$/.test(result.id) ||
|
|
64
|
+
!/^[A-Za-z0-9_-]{43}$/.test(result.token) ||
|
|
65
|
+
!/^[A-Z2-9]{12}$/.test(result.code) ||
|
|
66
|
+
!Number.isFinite(Date.parse(result.expiresAt))
|
|
67
|
+
)
|
|
68
|
+
throw new Error("Totem returned an invalid installation request.");
|
|
69
|
+
pending = {
|
|
70
|
+
...result,
|
|
71
|
+
baseUrl,
|
|
72
|
+
requestedAgentId: env.TOTEM_AGENT_ID,
|
|
73
|
+
provider: env.TOTEM_PROVIDER ?? "auto",
|
|
74
|
+
};
|
|
75
|
+
await privateWrite(join(runtime.directory, "connection-request.json"), pending);
|
|
76
|
+
}
|
|
77
|
+
const url = `${baseUrl}/connect?code=${pending.code}`;
|
|
78
|
+
if (options.openBrowser !== false) {
|
|
79
|
+
const command =
|
|
80
|
+
process.platform === "darwin"
|
|
81
|
+
? ["open", [url]]
|
|
82
|
+
: process.platform === "win32"
|
|
83
|
+
? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]]
|
|
84
|
+
: ["xdg-open", [url]];
|
|
85
|
+
await (options.execute ?? promisify(execFile))(command[0], command[1], { timeout: 5000 }).catch(
|
|
86
|
+
() => {},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return { url, code: pending.code, expiresAt: pending.expiresAt };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function resumeDeviceLogin(runtime, env = process.env, options = {}) {
|
|
93
|
+
let staged;
|
|
94
|
+
try {
|
|
95
|
+
staged = await privateRead(join(runtime.directory, "connection-ready.json"));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code !== "ENOENT") throw error;
|
|
98
|
+
}
|
|
99
|
+
if (!staged) {
|
|
100
|
+
let pending;
|
|
101
|
+
try {
|
|
102
|
+
pending = await privateRead(join(runtime.directory, "connection-request.json"));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error.code === "ENOENT") return;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = await request(
|
|
109
|
+
origin(pending.baseUrl),
|
|
110
|
+
`/api/installations/${pending.id}`,
|
|
111
|
+
{ headers: { "x-totem-installation-token": pending.token } },
|
|
112
|
+
options.fetch,
|
|
113
|
+
);
|
|
114
|
+
if (result.status !== "approved") return;
|
|
115
|
+
if (
|
|
116
|
+
!/^[a-f0-9-]{36}$/.test(result.agentId) ||
|
|
117
|
+
!/^tn_(?:test|live)_[A-Za-z0-9_-]{43}$/.test(result.key)
|
|
118
|
+
)
|
|
119
|
+
throw new Error("Totem returned an invalid connection.");
|
|
120
|
+
staged = {
|
|
121
|
+
agentId: result.agentId,
|
|
122
|
+
key: result.key,
|
|
123
|
+
baseUrl: pending.baseUrl,
|
|
124
|
+
requestId: pending.id,
|
|
125
|
+
token: pending.token,
|
|
126
|
+
};
|
|
127
|
+
// Persist privately before acknowledging, so an interrupted setup never loses the key.
|
|
128
|
+
await privateWrite(join(runtime.directory, "connection-ready.json"), staged);
|
|
129
|
+
}
|
|
130
|
+
if (staged.requestId) {
|
|
131
|
+
const receipt = await request(
|
|
132
|
+
staged.baseUrl,
|
|
133
|
+
`/api/installations/${staged.requestId}`,
|
|
134
|
+
{ method: "POST", headers: { "x-totem-installation-token": staged.token } },
|
|
135
|
+
options.fetch,
|
|
136
|
+
);
|
|
137
|
+
if (receipt.status !== "completed")
|
|
138
|
+
throw new Error("This connection expired before it was saved. Run totem login again.");
|
|
139
|
+
delete staged.requestId;
|
|
140
|
+
delete staged.token;
|
|
141
|
+
await privateWrite(join(runtime.directory, "connection-ready.json"), staged);
|
|
142
|
+
}
|
|
143
|
+
const provider = await (options.detectProvider ?? detectInstalledProvider)(env);
|
|
144
|
+
if (!provider) return { state: "executor_needed" };
|
|
145
|
+
const { agentSetupMain } = await import("./agent-setup.mjs");
|
|
146
|
+
const status = await (options.setup ?? agentSetupMain)(
|
|
147
|
+
[
|
|
148
|
+
"--profile",
|
|
149
|
+
staged.agentId ?? "default",
|
|
150
|
+
...(staged.agentId ? ["--agent-id", staged.agentId] : []),
|
|
151
|
+
"--base-url",
|
|
152
|
+
origin(staged.baseUrl),
|
|
153
|
+
...(await providerArguments(provider, env)),
|
|
154
|
+
"--no-start",
|
|
155
|
+
"--json",
|
|
156
|
+
],
|
|
157
|
+
{
|
|
158
|
+
env: { ...env, TOTEM_NETWORK_API_KEY: staged.key },
|
|
159
|
+
stdout: { write() {} },
|
|
160
|
+
stderr: { write() {} },
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
if (status) throw new Error("The approved connection could not be saved.");
|
|
164
|
+
const { rm } = await import("node:fs/promises");
|
|
165
|
+
await rm(join(runtime.directory, "connection-ready.json"));
|
|
166
|
+
await rm(join(runtime.directory, "connection-request.json"), { force: true });
|
|
167
|
+
return { state: "connected", profile: staged.agentId };
|
|
168
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ensureInstalledRuntime, installRuntimeService } from "./runtime-installation.mjs";
|
|
7
|
+
import { beginDeviceLogin } from "./device-login.mjs";
|
|
8
|
+
import { detectInstalledProvider, providerArguments } from "./provider-detection.mjs";
|
|
9
|
+
|
|
10
|
+
export async function activateConnection(profile, env = process.env, options = {}) {
|
|
11
|
+
const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(env);
|
|
12
|
+
const { automaticEnabled, setAutomaticEnabled } = await import("./automatic-runtime.mjs");
|
|
13
|
+
const enabled = await automaticEnabled(profile);
|
|
14
|
+
if (enabled) await setAutomaticEnabled(profile, true);
|
|
15
|
+
const service = await (options.installService ?? installRuntimeService)(runtime, {
|
|
16
|
+
...env,
|
|
17
|
+
...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
|
|
18
|
+
});
|
|
19
|
+
if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
|
|
20
|
+
const { agentStartMain } = await import("./agent-runner.mjs");
|
|
21
|
+
await (options.start ?? agentStartMain)(
|
|
22
|
+
["--profile", profile.profileName, "--background", "--json"],
|
|
23
|
+
{ env, automatic: true, stdout: { write() {} } },
|
|
24
|
+
);
|
|
25
|
+
return { state: "running", ...service, entry: runtime.cli };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function installationMain(args = [], options = {}) {
|
|
29
|
+
const env = options.env ?? process.env;
|
|
30
|
+
const stdout = options.stdout ?? process.stdout;
|
|
31
|
+
const flags = parseArgs({
|
|
32
|
+
args,
|
|
33
|
+
strict: true,
|
|
34
|
+
options: {
|
|
35
|
+
provider: { type: "string" },
|
|
36
|
+
"agent-id": { type: "string" },
|
|
37
|
+
"base-url": { type: "string" },
|
|
38
|
+
"no-browser": { type: "boolean" },
|
|
39
|
+
help: { type: "boolean", short: "h" },
|
|
40
|
+
json: { type: "boolean" },
|
|
41
|
+
},
|
|
42
|
+
}).values;
|
|
43
|
+
if (flags.help) {
|
|
44
|
+
stdout.write(
|
|
45
|
+
"totem login [--provider codex|claude|qwen-local|ollama-local|nim-local|command|http] [--no-browser]\nConnect this installation once. Totem securely saves its connection and automatically runs and restarts the listener.\n",
|
|
46
|
+
);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
const environment = {
|
|
50
|
+
...env,
|
|
51
|
+
...(flags.provider ? { TOTEM_PROVIDER: flags.provider } : {}),
|
|
52
|
+
...(flags["agent-id"] ? { TOTEM_AGENT_ID: flags["agent-id"] } : {}),
|
|
53
|
+
...(flags["base-url"] ? { TOTEM_BASE_URL: flags["base-url"] } : {}),
|
|
54
|
+
};
|
|
55
|
+
const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(environment);
|
|
56
|
+
const service = await (options.installService ?? installRuntimeService)(runtime, environment);
|
|
57
|
+
if (environment.TOTEM_NETWORK_API_KEY || environment.TOTEM_NETWORK_API_KEY_FILE) {
|
|
58
|
+
const provider = await (options.detectProvider ?? detectInstalledProvider)(environment);
|
|
59
|
+
if (!provider) {
|
|
60
|
+
let key = environment.TOTEM_NETWORK_API_KEY;
|
|
61
|
+
if (!key && environment.TOTEM_NETWORK_API_KEY_FILE) {
|
|
62
|
+
const info = await stat(environment.TOTEM_NETWORK_API_KEY_FILE);
|
|
63
|
+
if (
|
|
64
|
+
!info.isFile() ||
|
|
65
|
+
info.size > 8192 ||
|
|
66
|
+
(process.platform !== "win32" && info.mode & 0o077)
|
|
67
|
+
)
|
|
68
|
+
throw new Error("Use a private connection key file.");
|
|
69
|
+
key = (await readFile(environment.TOTEM_NETWORK_API_KEY_FILE, "utf8")).trim();
|
|
70
|
+
}
|
|
71
|
+
if (!/^tn_(?:test|live)_[A-Za-z0-9_-]{43}$/.test(key ?? ""))
|
|
72
|
+
throw new Error("The connection key is invalid.");
|
|
73
|
+
await privateWrite(join(runtime.directory, "connection-ready.json"), {
|
|
74
|
+
key,
|
|
75
|
+
baseUrl: environment.TOTEM_BASE_URL ?? "https://totemlayer.com",
|
|
76
|
+
agentId: environment.TOTEM_AGENT_ID,
|
|
77
|
+
});
|
|
78
|
+
stdout.write(
|
|
79
|
+
"Totem's automatic runtime is installed. Install/sign in to your AI client, then run totem login again.\n",
|
|
80
|
+
);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
const { agentSetupMain } = await import("./agent-setup.mjs");
|
|
84
|
+
return agentSetupMain(
|
|
85
|
+
[
|
|
86
|
+
"--profile",
|
|
87
|
+
environment.TOTEM_AGENT_ID ?? "default",
|
|
88
|
+
...(environment.TOTEM_AGENT_ID ? ["--agent-id", environment.TOTEM_AGENT_ID] : []),
|
|
89
|
+
...(await providerArguments(provider, environment)),
|
|
90
|
+
...(flags.json ? ["--json"] : []),
|
|
91
|
+
],
|
|
92
|
+
{
|
|
93
|
+
...options,
|
|
94
|
+
env: environment,
|
|
95
|
+
activateConnection: options.activateConnection ?? activateConnection,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await privateRead(join(runtime.directory, "connection-ready.json"));
|
|
101
|
+
stdout.write(
|
|
102
|
+
"Your Totem connection is saved privately. Sign in to your AI client; the listener connects automatically.\n",
|
|
103
|
+
);
|
|
104
|
+
return 0;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error.code !== "ENOENT") throw error;
|
|
107
|
+
}
|
|
108
|
+
// An upgrade reuses private profiles and starts their existing connections.
|
|
109
|
+
const { opendir } = await import("node:fs/promises");
|
|
110
|
+
let saved = false;
|
|
111
|
+
try {
|
|
112
|
+
const profiles = await opendir(join(runtime.root, "connections"));
|
|
113
|
+
for await (const entry of profiles)
|
|
114
|
+
if (
|
|
115
|
+
entry.isDirectory() &&
|
|
116
|
+
(!environment.TOTEM_AGENT_ID || entry.name === environment.TOTEM_AGENT_ID)
|
|
117
|
+
)
|
|
118
|
+
saved = true;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error.code !== "ENOENT") throw error;
|
|
121
|
+
}
|
|
122
|
+
if (saved && options.forceLogin !== true) {
|
|
123
|
+
const report = { installed: true, connection: "saved", automatic: true, service };
|
|
124
|
+
stdout.write(
|
|
125
|
+
flags.json
|
|
126
|
+
? JSON.stringify(report) + "\n"
|
|
127
|
+
: "Totem's automatic listener is installed and reuses your saved connections.\n",
|
|
128
|
+
);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
const connection = await (options.beginLogin ?? beginDeviceLogin)(runtime, environment, {
|
|
132
|
+
openBrowser: !flags["no-browser"] && environment.TOTEM_NO_BROWSER !== "1",
|
|
133
|
+
});
|
|
134
|
+
const report = {
|
|
135
|
+
installed: true,
|
|
136
|
+
connection: "sign_in_required",
|
|
137
|
+
automatic: true,
|
|
138
|
+
service,
|
|
139
|
+
...connection,
|
|
140
|
+
};
|
|
141
|
+
stdout.write(
|
|
142
|
+
flags.json
|
|
143
|
+
? JSON.stringify(report) + "\n"
|
|
144
|
+
: `Totem is installed. Connect your agent once at:\n${connection.url}\nConnection code: ${connection.code.match(/.{1,4}/g).join(" ")}\nThe listener starts automatically after you connect.\n`,
|
|
145
|
+
);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
export async function postinstallMain(env = process.env) {
|
|
149
|
+
if (env.TOTEM_AUTO_START === "0" || env.CI) return 0;
|
|
150
|
+
try {
|
|
151
|
+
return await installationMain([], { env });
|
|
152
|
+
} catch {
|
|
153
|
+
process.stdout.write(
|
|
154
|
+
"Totem is installed. Automatic connection setup needs attention; run totem login to finish it.\n",
|
|
155
|
+
);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (process.argv[1] === fileURLToPath(import.meta.url) && process.argv[2] === "--postinstall")
|
|
160
|
+
await postinstallMain();
|