totemlayer 0.1.44 → 0.1.63

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.
Files changed (46) hide show
  1. package/README.md +48 -11
  2. package/bin/agent-memory.mjs +255 -0
  3. package/bin/agent-profile.mjs +42 -5
  4. package/bin/agent-runner.mjs +55 -12
  5. package/bin/agent-setup.mjs +136 -50
  6. package/bin/automatic-runtime.mjs +26 -7
  7. package/bin/chat-prompt-limit.mjs +67 -0
  8. package/bin/codex-chat-bridge.mjs +36 -8
  9. package/bin/codex-chat-readiness.mjs +7 -0
  10. package/bin/codex-human-chat-bridge.mjs +209 -0
  11. package/bin/collaboration-listener.mjs +2 -1
  12. package/bin/connected-agent-service.mjs +4 -1
  13. package/bin/inbox-queue.mjs +2 -1
  14. package/bin/inference-broker.mjs +361 -0
  15. package/bin/installation.mjs +92 -60
  16. package/bin/isolated-agent-entry.mjs +35 -0
  17. package/bin/local-mcp.mjs +43 -4
  18. package/bin/mcp-config.d.mts +11 -0
  19. package/bin/mcp-config.mjs +56 -0
  20. package/bin/mcp-install.mjs +143 -12
  21. package/bin/mcp-listener.d.mts +22 -0
  22. package/bin/mcp-listener.mjs +4 -0
  23. package/bin/mcp-runtime.mjs +3 -1
  24. package/bin/mcp.mjs +331 -94
  25. package/bin/mission-agent-service.mjs +2 -1
  26. package/bin/mission-worker.mjs +13 -2
  27. package/bin/model-adapter.mjs +245 -107
  28. package/bin/network-contract.mjs +254 -0
  29. package/bin/network-runtime.mjs +162 -47
  30. package/bin/network.mjs +32 -7
  31. package/bin/platform-client.mjs +25 -3
  32. package/bin/process-isolation.mjs +214 -0
  33. package/bin/provider-detection.mjs +1 -4
  34. package/bin/runtime-diagnostics.mjs +1 -0
  35. package/bin/sdk/index.d.ts +50 -6
  36. package/bin/sdk/index.js +1 -1
  37. package/bin/sdk/inspector.js +1 -1
  38. package/bin/sdk/listener.d.ts +57 -0
  39. package/bin/sdk/listener.js +1 -0
  40. package/bin/sdk/types.d.ts +62 -0
  41. package/bin/startup-diagnostics.mjs +168 -0
  42. package/bin/totem.mjs +17 -3
  43. package/bin/worker-core/adapters.mjs +30 -13
  44. package/bin/worker-core/errors.mjs +4 -0
  45. package/package.json +1 -1
  46. package/scripts/prepare.mjs +28 -8
@@ -1,3 +1,4 @@
1
+ import { startupDiagnostic } from "./startup-diagnostics.mjs";
1
2
  import { parseArgs } from "node:util";
2
3
  import { readFile, stat } from "node:fs/promises";
3
4
  import { privateRead, privateWrite } from "./runtime-installation.mjs";
@@ -13,72 +14,103 @@ import { setTimeout as sleep } from "node:timers/promises";
13
14
  import { detectInstalledProvider, providerArguments } from "./provider-detection.mjs";
14
15
 
15
16
  export async function activateConnection(profile, env = process.env, options = {}) {
16
- const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(env);
17
- const { automaticEnabled, setAutomaticEnabled } = await import("./automatic-runtime.mjs");
18
- const enabled = await automaticEnabled(profile);
19
- if (enabled) await setAutomaticEnabled(profile, true);
20
- const service = await (options.installService ?? installRuntimeService)(runtime, {
21
- ...env,
22
- ...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
23
- });
24
- if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
25
- if (profile.interactiveRequired && !service.persistent)
26
- return {
27
- state: "needs_attention",
28
- ready: false,
29
- ...service,
30
- entry: runtime.cli,
31
- code: "HARNESS_SETUP_REQUIRED",
32
- message:
33
- "Automatic startup is not registered. Incoming work remains queued until host setup succeeds.",
34
- };
35
- const { verifyExecutorReady } = await import("./executor-readiness.mjs");
17
+ const health = await privateRead(join(profile.stateDirectory, "automatic-health.json")).catch(
18
+ (error) => {
19
+ if (error.code !== "ENOENT") throw error;
20
+ },
21
+ );
22
+ if (health?.retryable === false)
23
+ return { state: "needs_attention", ready: false, ...startupDiagnostic(health) };
36
24
  try {
37
- if (profile.codexThreadId) await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
38
- } catch (error) {
25
+ const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(env);
26
+ const { automaticEnabled } = await import("./automatic-runtime.mjs");
27
+ const enabled = await automaticEnabled(profile);
28
+
29
+ const service = await (options.installService ?? installRuntimeService)(runtime, {
30
+ ...env,
31
+ ...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
32
+ });
33
+ if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
34
+ if (profile.interactiveRequired && !service.persistent)
35
+ return {
36
+ state: "needs_attention",
37
+ ready: false,
38
+ ...service,
39
+ entry: runtime.cli,
40
+ code: "HARNESS_SETUP_REQUIRED",
41
+ message:
42
+ "Automatic startup is not registered. Incoming work remains queued until host setup succeeds.",
43
+ };
44
+ const { verifyExecutorReady } = await import("./executor-readiness.mjs");
45
+ try {
46
+ if (profile.codexThreadId)
47
+ await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
48
+ } catch (error) {
49
+ if (!startupDiagnostic(error).retryable) throw error;
50
+ return {
51
+ state: "needs_attention",
52
+ ...service,
53
+ entry: runtime.cli,
54
+ code: error.code ?? "EXECUTOR_NOT_READY",
55
+ ...startupDiagnostic(error),
56
+ message:
57
+ "Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker.",
58
+ };
59
+ }
60
+ if (profile.interactiveRequired) {
61
+ const { confirmHarnessStartup } = await import("./harness-bootstrap.mjs");
62
+ await confirmHarnessStartup(profile);
63
+ }
64
+ const start =
65
+ options.start ??
66
+ (
67
+ await import(
68
+ pathToFileURL(
69
+ join(
70
+ runtime.root,
71
+ "runtime-packages",
72
+ runtime.version,
73
+ "node_modules",
74
+ "totemlayer",
75
+ "bin",
76
+ "agent-runner.mjs",
77
+ ),
78
+ )
79
+ )
80
+ ).agentStartMain;
81
+ const result = await start(["--profile", profile.profileName, "--background", "--json"], {
82
+ env,
83
+ automatic: true,
84
+ stdout: { write() {} },
85
+ });
86
+ if (result) {
87
+ const failure = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
88
+ throw Object.assign(new Error("The listener could not start."), failure);
89
+ }
90
+ const status = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
39
91
  return {
40
- state: "needs_attention",
92
+ state: status.ready ? "ready" : "needs_attention",
93
+ ready: Boolean(status.ready),
41
94
  ...service,
42
95
  entry: runtime.cli,
43
- code: error.code ?? "EXECUTOR_NOT_READY",
44
- message:
45
- "Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker.",
46
96
  };
97
+ } catch (error) {
98
+ const diagnostic = startupDiagnostic(error);
99
+ if (!diagnostic.retryable)
100
+ await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
101
+ state: "needs_attention",
102
+ code: "LISTENER_START_FAILED",
103
+ ...diagnostic,
104
+ retryAt: null,
105
+ updatedAt: new Date().toISOString(),
106
+ });
107
+ throw Object.assign(
108
+ new Error(
109
+ `The listener could not start. ${diagnostic.cause} Recovery: ${diagnostic.recovery}`,
110
+ ),
111
+ { ...diagnostic, cause: error },
112
+ );
47
113
  }
48
- if (profile.interactiveRequired) {
49
- const { confirmHarnessStartup } = await import("./harness-bootstrap.mjs");
50
- await confirmHarnessStartup(profile);
51
- }
52
- const start =
53
- options.start ??
54
- (
55
- await import(
56
- pathToFileURL(
57
- join(
58
- runtime.root,
59
- "runtime-packages",
60
- runtime.version,
61
- "node_modules",
62
- "totemlayer",
63
- "bin",
64
- "agent-runner.mjs",
65
- ),
66
- )
67
- )
68
- ).agentStartMain;
69
- const result = await start(["--profile", profile.profileName, "--background", "--json"], {
70
- env,
71
- automatic: true,
72
- stdout: { write() {} },
73
- });
74
- if (result) throw new Error("The listener could not start. Check totem status for this profile.");
75
- const status = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
76
- return {
77
- state: status.ready ? "ready" : "needs_attention",
78
- ready: Boolean(status.ready),
79
- ...service,
80
- entry: runtime.cli,
81
- };
82
114
  }
83
115
 
84
116
  export async function installationMain(args = [], options = {}) {
@@ -0,0 +1,35 @@
1
+ // Linux's private network namespace can reach only this relay and the parent broker socket.
2
+ import { createServer } from "node:net";
3
+ import { createConnection } from "node:net";
4
+ import { spawn } from "node:child_process";
5
+
6
+ const config = JSON.parse(process.env.AGENT_SESSION_CONFIG);
7
+ const server = createServer();
8
+ const sockets = new Set();
9
+ server.on("connection", (socket) => {
10
+ sockets.add(socket);
11
+ const parent = createConnection(config.socketPath);
12
+ socket.pipe(parent).pipe(socket);
13
+ parent.on("error", () => socket.destroy());
14
+ socket.on("error", () => parent.destroy());
15
+ socket.on("close", () => {
16
+ sockets.delete(socket);
17
+ parent.destroy();
18
+ });
19
+ });
20
+ await new Promise((resolve) => server.listen(16443, "127.0.0.1", resolve));
21
+ const env = { ...process.env };
22
+ delete env.AGENT_SESSION_CONFIG;
23
+ const child = spawn(config.command, config.args, {
24
+ env,
25
+ cwd: config.cwd,
26
+ stdio: "inherit",
27
+ shell: false,
28
+ });
29
+ const stop = (code) => {
30
+ process.exitCode = code ?? 1;
31
+ for (const socket of sockets) socket.destroy();
32
+ server.close();
33
+ };
34
+ child.on("error", () => stop(1));
35
+ child.on("exit", stop);
package/bin/local-mcp.mjs CHANGED
@@ -1,5 +1,8 @@
1
- import { serveStdio } from "@modelcontextprotocol/server/stdio";
1
+ import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
2
2
  import * as z from "zod/v4";
3
+ import { Totem } from "./sdk/index.js";
4
+ import { createAutomaticMcpListener } from "./mcp-listener.mjs";
5
+ import { createMcpIdentityGuard } from "./mcp-identity-binding.mjs";
3
6
 
4
7
  import {
5
8
  managedCollaborationStatus,
@@ -76,25 +79,54 @@ export async function mcpMain(environment = process.env) {
76
79
  try {
77
80
  const config = await loadTotemMcpConfig(environment);
78
81
  let listener = { state: "checking", ready: false };
82
+ const client = new Totem(config);
83
+ const transport = new StdioServerTransport();
84
+ const automaticEnabled = async () => {
85
+ const status = await (
86
+ await import("./mcp-runtime.mjs")
87
+ ).ensureMcpListener(config, environment, {
88
+ keyFile: environment.TOTEM_NETWORK_API_KEY_FILE?.trim(),
89
+ connectionOnly: true,
90
+ });
91
+ return ["setup_required", "managed"].includes(status.state);
92
+ };
79
93
  const registerListener = (server, client, register) => {
80
94
  register(
81
95
  server,
82
96
  "totem_get_listener_status",
83
97
  {
84
- title: "Check this computer's Totem listener",
98
+ title: "Check this agent host’s Totem listener",
85
99
  description:
86
100
  "Show local listener setup, explicit stop, login recovery and readiness. MCP connectivity alone is not receive/wake/reply readiness and grants no work permission.",
87
101
  inputSchema: z.object({}).strict(),
88
102
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
89
103
  },
90
104
  async () =>
91
- (await import("./mcp-runtime.mjs")).readMcpListenerStatus(config, environment, listener),
105
+ automatic?.status().listenerReady
106
+ ? automatic.status()
107
+ : (await import("./mcp-runtime.mjs")).readMcpListenerStatus(
108
+ config,
109
+ environment,
110
+ listener,
111
+ ),
92
112
  );
93
113
  registerLocalCollaborationTools(server, client, register, {
94
114
  ...config,
95
115
  localKeyFile: environment.TOTEM_NETWORK_API_KEY_FILE?.trim(),
96
116
  });
97
117
  };
118
+ const automatic = createAutomaticMcpListener({
119
+ client,
120
+ authorize: createMcpIdentityGuard({ client, ...config }),
121
+ enabled: automaticEnabled,
122
+ hostKind: environment.TOTEM_MCP_HOST_KIND === "hosted" ? "hosted" : "local",
123
+ notify: (logger, data) =>
124
+ transport.send({
125
+ jsonrpc: "2.0",
126
+ method: "notifications/message",
127
+ params: { level: "info", logger, data },
128
+ }),
129
+ });
98
130
  serveStdio(
99
131
  () =>
100
132
  createTotemMcpServer({
@@ -103,8 +135,15 @@ export async function mcpMain(environment = process.env) {
103
135
  localKeyFile: environment.TOTEM_NETWORK_API_KEY_FILE?.trim(),
104
136
  registerLocalTools: registerListener,
105
137
  }),
106
- { onerror: (error) => console.error(`Totem MCP: ${error.message}`) },
138
+ { transport, onerror: (error) => console.error(`Totem MCP: ${error.message}`) },
107
139
  );
140
+ const closed = transport.onclose;
141
+ transport.onclose = () => {
142
+ void automatic.stop();
143
+ closed?.();
144
+ };
145
+ automatic.start();
146
+ process.stdin.once("end", () => void automatic?.stop());
108
147
  console.error("Totem MCP server is ready on stdio.");
109
148
  // Do not delay the MCP handshake. Reconnect repairs only an already bound
110
149
  // profile; creating a host/executor binding remains part of explicit install.
@@ -0,0 +1,11 @@
1
+ export function buildMcpConfig(options?: {
2
+ baseUrl?: string;
3
+ transport?: string;
4
+ format?: string;
5
+ name?: string;
6
+ keyFile?: string;
7
+ expectedAgentId?: string;
8
+ expectedEnvironment?: string;
9
+ command?: string;
10
+ args?: string[];
11
+ }): Record<string, Record<string, Record<string, unknown>>>;
@@ -0,0 +1,56 @@
1
+ /** Connection settings only. No model selection, client detection, or runtime installation. */
2
+ export function buildMcpConfig({
3
+ baseUrl = "https://totemlayer.com",
4
+ transport = "http",
5
+ format = "generic",
6
+ name = "totem",
7
+ keyFile,
8
+ expectedAgentId,
9
+ expectedEnvironment,
10
+ command = "totem",
11
+ args = ["mcp"],
12
+ } = {}) {
13
+ const url = new URL(baseUrl);
14
+ if (
15
+ url.username ||
16
+ url.password ||
17
+ url.search ||
18
+ url.hash ||
19
+ url.pathname !== "/" ||
20
+ (url.protocol !== "https:" &&
21
+ !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
22
+ )
23
+ throw new Error("Use a credential-free HTTPS origin, or HTTP loopback for development.");
24
+ if (!["http", "stdio"].includes(transport)) throw new Error("Choose --transport http or stdio.");
25
+ if (!["generic", "muse"].includes(format)) throw new Error("Choose --format generic or muse.");
26
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name))
27
+ throw new Error("Choose a valid MCP server name.");
28
+ if (transport === "stdio" && !keyFile) throw new Error("Pass --key-file for a stdio connection.");
29
+ if (expectedAgentId && !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(expectedAgentId))
30
+ throw new Error("Use a valid agent UUID.");
31
+ if (expectedEnvironment && !["PRODUCTION", "SANDBOX"].includes(expectedEnvironment))
32
+ throw new Error("Choose PRODUCTION or SANDBOX.");
33
+ const endpoint = new URL("/mcp", url);
34
+ if (expectedAgentId) endpoint.searchParams.set("agent", expectedAgentId);
35
+ if (expectedEnvironment) endpoint.searchParams.set("environment", expectedEnvironment);
36
+ const server =
37
+ transport === "http"
38
+ ? { url: endpoint.toString() }
39
+ : {
40
+ command,
41
+ args,
42
+ env: {
43
+ TOTEM_BASE_URL: url.origin,
44
+ TOTEM_NETWORK_API_KEY_FILE: keyFile,
45
+ ...(expectedAgentId ? { TOTEM_EXPECTED_AGENT_ID: expectedAgentId } : {}),
46
+ ...(expectedEnvironment ? { TOTEM_EXPECTED_ENVIRONMENT: expectedEnvironment } : {}),
47
+ },
48
+ };
49
+ return format === "muse"
50
+ ? {
51
+ mcp_servers: {
52
+ [name]: { transport: transport === "http" ? "streamable_http" : "stdio", ...server },
53
+ },
54
+ }
55
+ : { mcpServers: { [name]: { ...(transport === "http" ? { type: "http" } : {}), ...server } } };
56
+ }
@@ -1,3 +1,5 @@
1
+ import { parseArgs } from "node:util";
2
+ import { buildMcpConfig } from "./mcp-config.mjs";
1
3
  import { spawnSync } from "node:child_process";
2
4
  import { createHash } from "node:crypto";
3
5
  import { stat, lstat } from "node:fs/promises";
@@ -15,21 +17,33 @@ const SERVER_ENTRY = fileURLToPath(new URL("./totem.mjs", import.meta.url));
15
17
  export const MCP_INSTALL_HELP = `Connect Totem to an AI app
16
18
 
17
19
  Usage:
20
+ totem mcp install claude Add hosted MCP and open one OAuth approval
21
+ totem mcp install codex Add hosted MCP and open one OAuth approval
22
+ totem mcp install generic HTTP settings for other hosts
23
+ totem mcp config HTTP settings for any MCP client
24
+ totem mcp config --transport stdio --key-file /absolute/path/to/agent.key
25
+ totem mcp config --format muse Muse Code settings
18
26
  totem mcp install generic --key-file /absolute/path/to/agent.key
19
27
  totem mcp install codex --key-file /absolute/path/to/agent.key
20
28
  totem mcp install claude --key-file /absolute/path/to/agent.key
21
29
 
22
30
  Options:
23
31
  --base-url <origin> Totem origin (default: https://totemlayer.com)
24
- --name <name> MCP server name (default: totem-local)
25
- --key-file <path> Existing private agent key file (required)
32
+ --name <name> MCP name (default: totem, or totem-local with --key-file)
33
+ --key-file <path> Use an existing private agent key for local stdio instead
26
34
  --agent-id <UUID> Require this credential-derived agent identity
27
35
  --environment <env> Require PRODUCTION or SANDBOX
28
36
  --codex-thread <ID> Bind a new Codex listener to this owner-selected chat
29
37
  --no-start Configure tools only; do not start a local listener
30
38
  --help Show this guide
31
39
 
32
- This installs a user-level local stdio MCP connection. It never puts the key
40
+ Without --key-file, this uses the client's native hosted MCP and OAuth support.
41
+ The client saves credentials privately. No Totem website guide, copied key,
42
+ local worker, or model change is required. Existing connections are preserved.
43
+ The host may require reloading tools after adding a server. Call
44
+ totem_get_identity in that host to verify the selected identity and access.
45
+
46
+ With --key-file, this installs a user-level local stdio MCP connection. It never puts the key
33
47
  value in a command, client config, or repository. It will not overwrite an
34
48
  existing MCP server. Restart the AI app afterward and ask for totem_get_identity.
35
49
  Use --agent-id and --environment to reject an unintended agent or environment.
@@ -82,8 +96,7 @@ function parseInstallArgs(args, environment) {
82
96
  values[option] = value;
83
97
  }
84
98
  const keyFile = values["--key-file"] ?? environment.TOTEM_NETWORK_API_KEY_FILE;
85
- if (!keyFile) fail("Pass --key-file with the path to your own Totem agent key file.");
86
- const name = values["--name"] ?? DEFAULT_NAME;
99
+ const name = values["--name"] ?? (keyFile ? DEFAULT_NAME : "totem");
87
100
  if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name))
88
101
  fail(
89
102
  "The MCP name must be 1-64 letters, numbers, hyphens or underscores and start with a letter.",
@@ -118,7 +131,7 @@ function parseInstallArgs(args, environment) {
118
131
  codexThreadId: values["--codex-thread"],
119
132
  configDirectory: environment.TOTEM_CONFIG_DIR ? runtimeRoot(environment) : undefined,
120
133
  noStart: values["--no-start"] === true || environment.TOTEM_AUTO_START === "0",
121
- keyFile: resolve(keyFile),
134
+ keyFile: keyFile ? resolve(keyFile) : undefined,
122
135
  name,
123
136
  baseUrl: url.origin,
124
137
  ...loadMcpIdentityBinding({
@@ -152,16 +165,46 @@ async function validateKeyFile(path, platform) {
152
165
  fail("The Totem agent key file must be private (mode 0600). Fix its permissions and retry.");
153
166
  }
154
167
 
168
+ export async function configureTotemMcp(args, environment = process.env, runtime = {}) {
169
+ const flags = parseArgs({
170
+ args,
171
+ strict: true,
172
+ options: {
173
+ transport: { type: "string", default: "http" },
174
+ format: { type: "string", default: "generic" },
175
+ name: { type: "string", default: "totem" },
176
+ "base-url": { type: "string" },
177
+ "key-file": { type: "string" },
178
+ "agent-id": { type: "string" },
179
+ environment: { type: "string" },
180
+ help: { type: "boolean", short: "h" },
181
+ },
182
+ }).values;
183
+ if (flags.help) return MCP_INSTALL_HELP;
184
+ const keyFile = flags["key-file"] ?? environment.TOTEM_NETWORK_API_KEY_FILE;
185
+ const config = buildMcpConfig({
186
+ baseUrl: flags["base-url"] ?? environment.TOTEM_BASE_URL ?? DEFAULT_ORIGIN,
187
+ transport: flags.transport,
188
+ format: flags.format,
189
+ name: flags.name,
190
+ keyFile: keyFile ? resolve(keyFile) : undefined,
191
+ expectedAgentId: flags["agent-id"] ?? environment.TOTEM_EXPECTED_AGENT_ID,
192
+ expectedEnvironment: flags.environment ?? environment.TOTEM_EXPECTED_ENVIRONMENT,
193
+ command: process.execPath,
194
+ args: [runtime.serverEntry ?? SERVER_ENTRY, "mcp"],
195
+ });
196
+ if (flags.transport === "stdio")
197
+ await validateKeyFile(resolve(keyFile), runtime.platform ?? process.platform);
198
+ return JSON.stringify(config, null, 2) + "\n";
199
+ }
200
+
155
201
  function runClient(command, args, environment, runtime) {
156
- const invocation =
157
- runtime.platform === "win32"
158
- ? installedClientInvocation(command, args, environment)
159
- : { command, args };
202
+ const invocation = installedClientInvocation(command, args, environment, runtime);
160
203
  const result = runtime.spawnProcess(invocation.command, invocation.args, {
161
204
  encoding: "utf8",
162
205
  env: environment,
163
- stdio: ["ignore", "pipe", "pipe"],
164
- timeout: 20_000,
206
+ stdio: runtime.stdio ?? ["ignore", "pipe", "pipe"],
207
+ timeout: runtime.timeoutMs ?? 20_000,
165
208
  });
166
209
  if (result.error?.code === "ENOENT")
167
210
  fail(
@@ -171,10 +214,98 @@ function runClient(command, args, environment, runtime) {
171
214
  return result;
172
215
  }
173
216
 
217
+ function hostedEntryMatches(result, client, endpoint) {
218
+ if (result.status !== 0) return false;
219
+ if (client === "codex") {
220
+ try {
221
+ const entry = JSON.parse(result.stdout);
222
+ const transport = entry.transport;
223
+ return (
224
+ entry.enabled !== false &&
225
+ !entry.disabled_reason &&
226
+ transport?.type === "streamable_http" &&
227
+ transport.url === endpoint &&
228
+ !transport.bearer_token_env_var &&
229
+ Object.keys(transport.http_headers ?? {}).length === 0 &&
230
+ Object.keys(transport.env_http_headers ?? {}).length === 0
231
+ );
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+ // Claude currently exposes human-readable get output rather than a JSON export.
237
+ const output = String(result.stdout ?? "").replace(/\u001b\[[0-9;]*m/g, "");
238
+ const field = (name) => output.match(new RegExp(`^\\s*${name}:\\s*(.*)$`, "mi"))?.[1]?.trim();
239
+ return (
240
+ field("Type") === "http" &&
241
+ field("URL") === endpoint &&
242
+ !/^.*(?:disabled|rejected|pending approval)/im.test(field("Status") ?? "") &&
243
+ !/^\s*(?:Headers?|Authorization|Environment|headersHelper):/im.test(output)
244
+ );
245
+ }
246
+
247
+ async function installHostedTotemMcp(options, environment, runtime) {
248
+ const config = buildMcpConfig({
249
+ baseUrl: options.baseUrl,
250
+ name: options.name,
251
+ expectedAgentId: options.expectedAgentId,
252
+ expectedEnvironment: options.expectedEnvironment,
253
+ });
254
+ if (options.client === "generic") return JSON.stringify(config, null, 2) + "\n";
255
+ if (options.codexThreadId)
256
+ fail(
257
+ "--codex-thread requires local stdio setup with --key-file. Hosted MCP does not bind a desktop chat.",
258
+ );
259
+ const endpoint = config.mcpServers[options.name].url;
260
+ const env = localClientEnvironment(environment);
261
+ const getArgs = ["mcp", "get", options.name, ...(options.client === "codex" ? ["--json"] : [])];
262
+ const inspect = () => runClient(options.client, getArgs, env, runtime);
263
+ let entry = inspect();
264
+ if (entry.status === 0 && !hostedEntryMatches(entry, options.client, endpoint))
265
+ fail(
266
+ `${options.name} already exists with different or disabled settings. It was preserved; inspect that exact connection before continuing.`,
267
+ );
268
+ if (entry.status !== 0) {
269
+ if (!/no mcp server|not found|does not exist/i.test(`${entry.stderr}\n${entry.stdout}`))
270
+ fail("The client could not check its existing MCP settings. No configuration was changed.");
271
+ const args =
272
+ options.client === "claude"
273
+ ? ["mcp", "add", "--transport", "http", "--scope", "user", options.name, endpoint]
274
+ : ["mcp", "add", options.name, "--url", endpoint];
275
+ if (runClient(options.client, args, env, runtime).status !== 0)
276
+ fail(
277
+ "The host could not save Totem's MCP connection. Check its MCP configuration permissions.",
278
+ );
279
+ entry = inspect();
280
+ if (!hostedEntryMatches(entry, options.client, endpoint))
281
+ fail(
282
+ "Totem's saved MCP settings could not be verified. No sign-in was started; inspect the exact entry before retrying.",
283
+ );
284
+ }
285
+ const connected =
286
+ options.client === "claude" &&
287
+ /^\s*Status:\s*(?:✔\s*)?Connected\s*$/im.test(
288
+ String(entry.stdout ?? "").replace(/\u001b\[[0-9;]*m/g, ""),
289
+ );
290
+ if (!connected) {
291
+ const login = runClient(options.client, ["mcp", "login", options.name], env, {
292
+ ...runtime,
293
+ timeoutMs: 600_000,
294
+ stdio: "inherit",
295
+ });
296
+ if (login.status !== 0)
297
+ fail(
298
+ `Totem's settings are saved, but approval is incomplete. Finish this server's native MCP sign-in in ${options.client}; do not create another connection or switch identities. Older hosts may require their MCP sign-in panel.`,
299
+ );
300
+ }
301
+ return `Totem's hosted MCP settings are saved in ${options.client}. ${connected ? "The existing authenticated connection was reused." : "The client's OAuth approval completed; credentials stay in its private store."} Reload this host's tools if needed, then call totem_get_identity to verify the agent, organization and environment. Check incoming chat delivery separately.\n`;
302
+ }
303
+
174
304
  export async function installTotemMcp(args, environment = process.env, runtime = {}) {
175
305
  const options = parseInstallArgs(args, environment);
176
306
  if (options.help) return MCP_INSTALL_HELP;
177
307
  runtime = { platform: process.platform, spawnProcess: spawnSync, ...runtime };
308
+ if (!options.keyFile) return installHostedTotemMcp(options, environment, runtime);
178
309
  await validateKeyFile(options.keyFile, runtime.platform);
179
310
 
180
311
  if (options.client === "generic")
@@ -0,0 +1,22 @@
1
+ import type { Totem } from "./sdk/index.js";
2
+ export function createAutomaticMcpListener(options: {
3
+ client: Totem;
4
+ authorize?: (signal: AbortSignal) => Promise<unknown>;
5
+ enabled?: () => Promise<boolean>;
6
+ notify: (logger: string, data: unknown, cursor?: string) => Promise<void>;
7
+ hostKind?: "local" | "hosted";
8
+ pollMs?: number;
9
+ cursors?: { cursor?: string; messageCursor?: string };
10
+ signal?: AbortSignal;
11
+ }): {
12
+ start(): void;
13
+ stop(): Promise<void>;
14
+ done(): Promise<void>;
15
+ status(): {
16
+ state: string;
17
+ listenerReady: boolean;
18
+ executionAuthorized: boolean;
19
+ runId?: string;
20
+ code?: string;
21
+ };
22
+ };
@@ -0,0 +1,4 @@
1
+ export {
2
+ createAutomaticNetworkListener as createAutomaticMcpListener,
3
+ readTotemActivity,
4
+ } from "./sdk/listener.js";
@@ -17,7 +17,7 @@ function listenerBinding(profile) {
17
17
 
18
18
  /** Recover only the exact credential-bound profile. MCP transport grants no new host authority. */
19
19
  export async function ensureMcpListener(config, environment = process.env, options = {}) {
20
- if (environment.TOTEM_AUTO_START === "0" || environment.CI)
20
+ if (environment.TOTEM_AUTO_START === "0" || (environment.CI && !options.connectionOnly))
21
21
  return { state: "disabled_by_request", ready: false, persistent: false };
22
22
  if (!options.keyFile)
23
23
  return {
@@ -122,6 +122,8 @@ export async function ensureMcpListener(config, environment = process.env, optio
122
122
  message:
123
123
  "This listener was explicitly stopped. Its opt-out remains in effect; totem start can resume it.",
124
124
  };
125
+ if (options.connectionOnly)
126
+ return { state: "managed", ready: false, ...listenerBinding(profile) };
125
127
  const activate = options.activate ?? (await import("./installation.mjs")).activateConnection;
126
128
  if (["codex", "claude"].includes(profile.client ?? profile.provider)) {
127
129
  try {