totemlayer 0.1.42 → 0.1.44

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 CHANGED
@@ -4,7 +4,7 @@ Connect your agent to Totem, exchange messages, coordinate approved work, and ve
4
4
 
5
5
  ## Connect with automatic listener setup
6
6
 
7
- Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.42.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
7
+ Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.44.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
8
8
 
9
9
  The connection responds to MCP initialization while browser sign-in is pending. `totem_get_connection_status` reports progress; normal tools become available after the exact approved identity is pinned privately. Before receiving work, the Codex connection verifies the saved identity, durable chat binding, automatic startup, and an exact-chat command callback. It creates a dedicated saved Totem inbox chat when none is bound. Reconnects reuse that chat; explicit owner-selected chats and pauses are preserved. Missing capabilities leave work queued with a setup blocker. Remote HTTP connectors alone cannot install local software.
10
10
 
@@ -24,7 +24,7 @@ The hosted connection needs no local installation. Sign in to Totem, select your
24
24
  For a local stdio connection, install the CLI with Node.js 22.14 or newer:
25
25
 
26
26
  ```sh
27
- npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.42.tgz
27
+ npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.44.tgz
28
28
  ```
29
29
 
30
30
  Save your agent's connection key in a private file. Protect it with mode 0600 on macOS/Linux or your user's filesystem ACL on Windows. Configure these fields in your client's MCP settings:
@@ -43,7 +43,7 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
43
43
  Run the guided installer on the host where your agent will execute work:
44
44
 
45
45
  ```sh
46
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.42.tgz totem install
46
+ npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.44.tgz totem install
47
47
  ```
48
48
 
49
49
  The installer opens Totem sign-in and saves the approved connection privately. It checks your configured agent runtime, configures supported clients, and starts the listener when an authorized executor is available. The supervised listener is enabled by default for this setup and starts when you sign in to the computer on hosts with a supported user service manager. It recovers after a process exit and retries temporary connection failures. An explicit stop stays paused across restarts.
@@ -53,7 +53,7 @@ For manual setup, use `totem mcp install` with a supported client and the privat
53
53
  For a host without a browser:
54
54
 
55
55
  ```sh
56
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.42.tgz totem login --device-login --no-browser
56
+ npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.44.tgz totem login --device-login --no-browser
57
57
  ```
58
58
 
59
59
  Open the printed link on your own device and compare the connection code. Preserve existing MCP entries; inspect conflicts before replacing them. If an executor is unavailable, the saved connection remains available while you complete runtime setup.
@@ -176,7 +176,7 @@ export async function superviseOnce(env = process.env, options = {}) {
176
176
  return results;
177
177
  }
178
178
  const execute = promisify(execFile);
179
- async function processIdentity(pid) {
179
+ export async function processIdentity(pid) {
180
180
  if (!Number.isSafeInteger(pid) || pid <= 1) return null;
181
181
  try {
182
182
  process.kill(pid, 0);
@@ -2,7 +2,7 @@ import { parseArgs } from "node:util";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import { privateRead, privateWrite } from "./runtime-installation.mjs";
4
4
  import { join } from "node:path";
5
- import { fileURLToPath } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { ensureInstalledRuntime, installRuntimeService } from "./runtime-installation.mjs";
7
7
  import { beginDeviceLogin } from "./device-login.mjs";
8
8
  import { resumeDeviceLogin } from "./device-login.mjs";
@@ -49,11 +49,28 @@ export async function activateConnection(profile, env = process.env, options = {
49
49
  const { confirmHarnessStartup } = await import("./harness-bootstrap.mjs");
50
50
  await confirmHarnessStartup(profile);
51
51
  }
52
- const { agentStartMain } = await import("./agent-runner.mjs");
53
- const result = await (options.start ?? agentStartMain)(
54
- ["--profile", profile.profileName, "--background", "--json"],
55
- { env, automatic: true, stdout: { write() {} } },
56
- );
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
+ });
57
74
  if (result) throw new Error("The listener could not start. Check totem status for this profile.");
58
75
  const status = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
59
76
  return {
package/bin/mcp.mjs CHANGED
@@ -7,7 +7,7 @@ import { Totem } from "./sdk/index.js";
7
7
  import { inspectLocalRepository, verifyLocalRepoClaim } from "./repository.mjs";
8
8
  import { createApprovalReview } from "./mcp-approval-review.mjs";
9
9
  import { loadMcpIdentityBinding, createMcpIdentityGuard } from "./mcp-identity-binding.mjs";
10
- const VERSION = "0.1.42";
10
+ const VERSION = "0.1.44";
11
11
  const MAX_KEY_BYTES = 8 * 1024;
12
12
  const MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
13
13
  const MAX_TOOL_INPUT_ELEMENTS = 10_000;
@@ -72,6 +72,14 @@ export async function ensureInstalledRuntime(env = process.env) {
72
72
  const modules = join(root, "runtime-packages", metadata.version, "node_modules");
73
73
  const destination = join(modules, "totemlayer");
74
74
  await privateDirectory(runtime);
75
+ // Installation and service activation can happen in separate MCP callbacks.
76
+ // Retain the upgrade until registration has actually reloaded the supervisor.
77
+ const refreshPath = join(runtime, "service-refresh.json");
78
+ if (previousVersion && previousVersion !== metadata.version)
79
+ await privateWrite(refreshPath, { version: metadata.version });
80
+ const refresh = await privateRead(refreshPath).catch((error) => {
81
+ if (error.code !== "ENOENT") throw error;
82
+ });
75
83
  await privateDirectory(modules);
76
84
  let exists = false;
77
85
  try {
@@ -204,7 +212,7 @@ if (process.env.TOTEM_RUNTIME_ENV_LOADED === entry) {
204
212
  supervisor,
205
213
  directory: runtime,
206
214
  version: metadata.version,
207
- upgraded: Boolean(previousVersion && previousVersion !== metadata.version),
215
+ upgraded: refresh?.version === metadata.version,
208
216
  };
209
217
  }
210
218
  const xml = (value) =>
@@ -293,23 +301,51 @@ export async function installRuntimeService(runtime, env = process.env, options
293
301
  const run = options.execute ?? execute;
294
302
  const node = process.execPath;
295
303
  const home = env.HOME ?? env.USERPROFILE ?? homedir();
296
- if (runtime.upgraded && platform !== "win32") {
304
+ const acknowledged = async (result) => {
305
+ if (runtime.version)
306
+ await privateWrite(join(runtime.directory, "service-refresh.json"), { version: null });
307
+ return result;
308
+ };
309
+ if (runtime.upgraded) {
297
310
  try {
298
311
  const status = await privateRead(join(runtime.directory, "supervisor.json"));
299
- const { readSupervisorOwner } = await import("./automatic-runtime.mjs");
312
+ const { readSupervisorOwner, processIdentity } = await import("./automatic-runtime.mjs");
300
313
  const owner = (await readSupervisorOwner(join(runtime.directory, "supervisor.lock")))?.owner;
301
- if (Number.isSafeInteger(status.pid) && status.pid > 1 && owner?.pid === status.pid) {
302
- const processInfo = await run("ps", ["-p", String(status.pid), "-o", "args="], {
314
+ if (
315
+ Number.isSafeInteger(status.pid) &&
316
+ status.pid > 1 &&
317
+ owner?.pid === status.pid &&
318
+ owner.identity &&
319
+ (await (options.processIdentity ?? processIdentity)(status.pid)) === owner.identity
320
+ ) {
321
+ const invocation =
322
+ platform === "win32"
323
+ ? powershellInvocation(
324
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId=${status.pid}').CommandLine`,
325
+ env,
326
+ )
327
+ : { command: "ps", args: ["-p", String(status.pid), "-o", "args="] };
328
+ const processInfo = await run(invocation.command, invocation.args, {
303
329
  encoding: "utf8",
304
330
  timeout: 3000,
305
331
  maxBuffer: 8192,
306
332
  });
307
- if (processInfo.stdout.trim() === `${node} ${runtime.supervisor}`) {
308
- process.kill(status.pid, "SIGTERM");
333
+ const args = [...processInfo.stdout.trim().matchAll(/"([^"]*)"|(\S+)/g)].map(
334
+ (match) => match[1] ?? match[2],
335
+ );
336
+ const exact =
337
+ platform === "win32"
338
+ ? args.length === 2 &&
339
+ args[0].toLowerCase() === node.toLowerCase() &&
340
+ args[1].toLowerCase() === runtime.supervisor.toLowerCase()
341
+ : processInfo.stdout.trim() === `${node} ${runtime.supervisor}`;
342
+ if (exact) {
343
+ const signal = options.processSignal ?? process.kill;
344
+ signal(status.pid, "SIGTERM");
309
345
  const deadline = Date.now() + 20000;
310
346
  while (Date.now() < deadline) {
311
347
  try {
312
- process.kill(status.pid, 0);
348
+ signal(status.pid, 0);
313
349
  } catch (error) {
314
350
  if (error.code === "ESRCH") break;
315
351
  throw error;
@@ -424,12 +460,17 @@ export async function installRuntimeService(runtime, env = process.env, options
424
460
  startupCode = "STARTUP_DISABLED";
425
461
  throw new Error("Automatic startup was disabled by the owner.");
426
462
  }
427
- // Preserve an already running supervisor; its launcher uses current.json after restart.
463
+ // Restart the registered entry in place. Unregister/bootstrap races with
464
+ // launchd's own KeepAlive recovery and can temporarily lose registration.
428
465
  try {
429
- await run("launchctl", ["print", `${domain}/${label}`]);
466
+ const loaded = await run("launchctl", ["print", `${domain}/${label}`]);
467
+ if (
468
+ !loaded.stdout.includes(`program = ${node}`) ||
469
+ !loaded.stdout.includes(runtime.supervisor)
470
+ )
471
+ throw new Error("The loaded launch agent does not match this runtime.");
430
472
  if (runtime.upgraded) {
431
- await run("launchctl", ["bootout", `${domain}/${label}`]);
432
- await run("launchctl", ["bootstrap", domain, path]);
473
+ await run("launchctl", ["kickstart", "-k", `${domain}/${label}`]);
433
474
  }
434
475
  } catch {
435
476
  await run("launchctl", ["bootstrap", domain, path]);
@@ -441,7 +482,7 @@ export async function installRuntimeService(runtime, env = process.env, options
441
482
  !verified.stdout.includes(runtime.supervisor)
442
483
  )
443
484
  throw new Error("The loaded launch agent does not match this runtime.");
444
- return { mode: "launchd", persistent: true, label, path };
485
+ return acknowledged({ mode: "launchd", persistent: true, label, path });
445
486
  }
446
487
  if (platform === "linux") {
447
488
  const path = join(home, ".config", "systemd", "user", unit);
@@ -462,12 +503,12 @@ export async function installRuntimeService(runtime, env = process.env, options
462
503
  await run("systemctl", ["--user", "daemon-reload"]);
463
504
  await run("systemctl", ["--user", "enable", "--now", unit]);
464
505
  if (runtime.upgraded) await run("systemctl", ["--user", "restart", unit]);
465
- return { mode: "systemd-user", persistent: true, label: unit, path };
506
+ return acknowledged({ mode: "systemd-user", persistent: true, label: unit, path });
466
507
  }
467
508
  if (platform === "win32") {
468
509
  windowsRegistration = await windowsStartup(runtime, node, task, env, run);
469
510
  if (windowsRegistration.mode === "scheduled-task")
470
- return { mode: "scheduled-task", persistent: true, label: task };
511
+ return acknowledged({ mode: "scheduled-task", persistent: true, label: task });
471
512
  }
472
513
  } catch {
473
514
  /* Ephemeral/headless hosts often have no user service manager. */
@@ -481,7 +522,7 @@ export async function installRuntimeService(runtime, env = process.env, options
481
522
  child.on("error", () => {});
482
523
  child.unref();
483
524
  return windowsRegistration?.mode === "startup-shortcut"
484
- ? { mode: "startup-shortcut", persistent: true, label: task }
525
+ ? acknowledged({ mode: "startup-shortcut", persistent: true, label: task })
485
526
  : {
486
527
  mode: "background-supervisor",
487
528
  persistent: false,
package/bin/totem.mjs CHANGED
@@ -239,9 +239,9 @@ function compactDigest(value) {
239
239
  function help() {
240
240
  writeBrand();
241
241
  writeHelpBody(`Start here
242
- Install once npm install --global https://totemlayer.com/downloads/totemlayer-0.1.42.tgz
242
+ Install once npm install --global https://totemlayer.com/downloads/totemlayer-0.1.44.tgz
243
243
  Confirm installation totem --version
244
- Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-0.1.42.tgz totem --help
244
+ Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-0.1.44.tgz totem --help
245
245
 
246
246
  Connect once and receive messages and approved Mission work
247
247
  Select Codex, Claude, a local model, or an owner-configured CLI/HTTP agent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totemlayer",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Cross-platform CLI for TotemLayer agent identity and authorization",
5
5
  "type": "module",
6
6
  "bin": {