totemlayer 0.1.42 → 0.1.59
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 +48 -11
- package/bin/agent-memory.mjs +244 -0
- package/bin/agent-profile.mjs +26 -4
- package/bin/agent-runner.mjs +43 -9
- package/bin/agent-setup.mjs +136 -50
- package/bin/automatic-runtime.mjs +27 -8
- package/bin/chat-prompt-limit.mjs +67 -0
- package/bin/codex-chat-bridge.mjs +36 -8
- package/bin/codex-chat-readiness.mjs +7 -0
- package/bin/codex-human-chat-bridge.mjs +209 -0
- package/bin/collaboration-listener.mjs +2 -1
- package/bin/connected-agent-service.mjs +4 -1
- package/bin/inbox-queue.mjs +2 -1
- package/bin/installation.mjs +93 -44
- package/bin/local-mcp.mjs +43 -4
- package/bin/mcp-config.d.mts +11 -0
- package/bin/mcp-config.mjs +56 -0
- package/bin/mcp-install.mjs +143 -12
- package/bin/mcp-listener.d.mts +22 -0
- package/bin/mcp-listener.mjs +4 -0
- package/bin/mcp-runtime.mjs +3 -1
- package/bin/mcp.mjs +331 -94
- package/bin/mission-agent-service.mjs +2 -1
- package/bin/mission-worker.mjs +13 -2
- package/bin/model-adapter.mjs +1 -4
- package/bin/network-contract.mjs +254 -0
- package/bin/network-runtime.mjs +160 -25
- package/bin/network.mjs +30 -5
- package/bin/platform-client.mjs +25 -3
- package/bin/provider-detection.mjs +1 -4
- package/bin/runtime-diagnostics.mjs +1 -0
- package/bin/runtime-installation.mjs +57 -16
- package/bin/sdk/index.d.ts +50 -6
- package/bin/sdk/index.js +1 -1
- package/bin/sdk/listener.d.ts +57 -0
- package/bin/sdk/listener.js +1 -0
- package/bin/sdk/types.d.ts +62 -0
- package/bin/startup-diagnostics.mjs +168 -0
- package/bin/totem.mjs +17 -3
- package/bin/worker-core/adapters.mjs +4 -4
- package/package.json +1 -1
- package/scripts/prepare.mjs +28 -8
package/bin/mcp-install.mjs
CHANGED
|
@@ -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
|
|
25
|
-
--key-file <path>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/bin/mcp-runtime.mjs
CHANGED
|
@@ -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 {
|