totemlayer 0.1.4 → 0.1.6

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.
@@ -7,9 +7,9 @@ import { setTimeout as sleep } from "node:timers/promises";
7
7
 
8
8
  import { inspectLocalRepository } from "./repository.mjs";
9
9
  import { localClientEnvironment, streamingClientInvocation } from "./platform-client.mjs";
10
+ import { runFile } from "./mission-worker.mjs";
10
11
 
11
12
  const MAX_RUNS_PER_TASK = 16;
12
- const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
13
13
  const MAX_PROMPT_BYTES = 32 * 1024;
14
14
  const DEFAULT_RUN_TIMEOUT_MS = 15 * 60 * 1000;
15
15
 
@@ -84,7 +84,10 @@ function runnerCommand(provider, sessionId, platform = process.platform) {
84
84
  if (provider === "codex")
85
85
  invocation = sessionId
86
86
  ? { command: "codex", args: ["exec", "resume", "--json", sessionId, "-"] }
87
- : { command: "codex", args: ["exec", "--sandbox", "workspace-write", "--json", "-"] };
87
+ : {
88
+ command: "codex",
89
+ args: ["exec", "--sandbox", "workspace-write", "--json", "-"],
90
+ };
88
91
  else if (provider === "claude")
89
92
  invocation = {
90
93
  command: "claude",
@@ -106,7 +109,9 @@ function runnerCommand(provider, sessionId, platform = process.platform) {
106
109
  }
107
110
 
108
111
  function runnerEnvironment(repositoryRoot, environment = process.env) {
109
- return localClientEnvironment(environment, { TOTEM_REPOSITORY_ROOT: repositoryRoot });
112
+ return localClientEnvironment(environment, {
113
+ TOTEM_REPOSITORY_ROOT: repositoryRoot,
114
+ });
110
115
  }
111
116
 
112
117
  export async function runLocalCollaborationAgent({
@@ -120,79 +125,42 @@ export async function runLocalCollaborationAgent({
120
125
  spawnProcess = spawn,
121
126
  }) {
122
127
  const { command, args } = runnerCommand(provider, sessionId, platform);
123
- return new Promise((resolveRun, rejectRun) => {
124
- const child = spawnProcess(command, args, {
125
- cwd: repositoryRoot,
126
- env: runnerEnvironment(repositoryRoot),
127
- stdio: ["pipe", "pipe", "pipe"],
128
- shell: false,
129
- });
130
- let output = "";
131
- let totalOutputBytes = 0;
132
- let observedSession = sessionId ?? null;
133
- let finalResponse = "";
134
- let finished = false;
135
- const fail = (error) => {
136
- if (finished) return;
137
- finished = true;
138
- clearTimeout(timer);
139
- signal?.removeEventListener("abort", abort);
140
- rejectRun(error);
141
- };
142
- const abort = () => child.kill("SIGTERM");
143
- const timer = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
144
- signal?.addEventListener("abort", abort, { once: true });
145
- if (signal?.aborted) abort();
146
- child.stdout.on("data", (chunk) => {
147
- totalOutputBytes += chunk.byteLength;
148
- output += chunk.toString("utf8");
149
- if (totalOutputBytes > MAX_OUTPUT_BYTES) child.kill("SIGTERM");
150
- if (provider !== "codex") return;
151
- let newline;
152
- while ((newline = output.indexOf("\n")) >= 0) {
153
- const line = output.slice(0, newline);
154
- output = output.slice(newline + 1);
155
- try {
156
- const event = JSON.parse(line);
157
- if (event.type === "thread.started" && typeof event.thread_id === "string")
158
- observedSession = event.thread_id;
159
- if (event.type === "item.completed" && event.item?.type === "agent_message")
160
- finalResponse = String(event.item.text ?? "");
161
- } catch {
162
- // Ignore non-JSON progress lines; never echo model output to CLI logs.
163
- }
164
- }
165
- });
166
- child.stderr.on("data", () => {});
167
- child.once("error", (error) => fail(error));
168
- child.once("close", (code) => {
169
- if (finished) return;
170
- if (signal?.aborted) return fail(new Error("The collaboration listener was stopped."));
171
- if (code !== 0)
172
- return fail(
173
- new Error(
174
- `The ${provider} run exited with status ${code ?? "unknown"}. Check that client's local logs and permissions.`,
175
- ),
176
- );
177
- if (provider === "claude") {
178
- try {
179
- const result = JSON.parse(output);
180
- observedSession = result.session_id ?? observedSession;
181
- finalResponse = typeof result.result === "string" ? result.result : "";
182
- if (result.is_error) return fail(new Error("Claude reported a failed run."));
183
- } catch {
184
- return fail(new Error("Claude returned an invalid JSON result."));
185
- }
186
- }
187
- if (!observedSession)
188
- return fail(new Error(`${provider} did not return a resumable session ID.`));
189
- finished = true;
190
- clearTimeout(timer);
191
- signal?.removeEventListener("abort", abort);
192
- resolveRun({ sessionId: observedSession, finalResponse });
193
- });
194
- child.stdin.end(prompt);
128
+ const { stdout } = await runFile(command, args, {
129
+ cwd: repositoryRoot,
130
+ env: runnerEnvironment(repositoryRoot),
131
+ input: prompt,
132
+ signal,
133
+ timeoutMs,
134
+ platform,
135
+ spawnProcess,
195
136
  });
137
+ let observedSession = sessionId ?? null;
138
+ let finalResponse = "";
139
+ if (provider === "claude") {
140
+ let result;
141
+ try {
142
+ result = JSON.parse(stdout);
143
+ } catch {
144
+ throw new Error("Claude returned an invalid JSON result.");
145
+ }
146
+ observedSession = result.session_id ?? observedSession;
147
+ finalResponse = typeof result.result === "string" ? result.result : "";
148
+ if (result.is_error) throw new Error("Claude reported a failed run.");
149
+ } else {
150
+ for (const line of stdout.split("\n")) {
151
+ try {
152
+ const event = JSON.parse(line);
153
+ if (event.type === "thread.started" && typeof event.thread_id === "string")
154
+ observedSession = event.thread_id;
155
+ if (event.type === "item.completed" && event.item?.type === "agent_message")
156
+ finalResponse = String(event.item.text ?? "");
157
+ } catch {
158
+ // Ignore non-JSON progress lines; never echo model output to CLI logs.
159
+ }
160
+ }
161
+ }
162
+ if (!observedSession) throw new Error(`${provider} did not return a resumable session ID.`);
163
+ return { sessionId: observedSession, finalResponse };
196
164
  }
197
165
 
198
166
  async function readJournal(file) {
@@ -216,7 +184,12 @@ async function latestOutboundTaskCursor(client, taskId, cursor, signal) {
216
184
  let latest = cursor;
217
185
  for (let page = 0; page < 20; page++) {
218
186
  const result = await client.messages.list(
219
- { direction: "outbound", taskId, limit: 100, ...(latest ? { cursor: latest } : {}) },
187
+ {
188
+ direction: "outbound",
189
+ taskId,
190
+ limit: 100,
191
+ ...(latest ? { cursor: latest } : {}),
192
+ },
220
193
  { signal },
221
194
  );
222
195
  const messages = result.data ?? [];
@@ -247,7 +220,9 @@ export async function runCollaborationListener({
247
220
  if (!Number.isInteger(pollMs) || pollMs < 1000 || pollMs > 60000)
248
221
  throw new Error("--poll-ms must be between 1000 and 60000.");
249
222
  const root = resolve(repositoryRoot);
250
- const repository = await inspectLocalRepository({ TOTEM_REPOSITORY_ROOT: root });
223
+ const repository = await inspectLocalRepository({
224
+ TOTEM_REPOSITORY_ROOT: root,
225
+ });
251
226
  const identity = await client.agent({ signal });
252
227
  const peer = await client.resolve(peerAddress, { signal });
253
228
  if (!peer?.id || peer.address !== peerAddress) throw new Error("The allowed peer was not found.");
@@ -267,7 +242,12 @@ export async function runCollaborationListener({
267
242
  }
268
243
  try {
269
244
  const journal = await readJournal(journalFile);
270
- const binding = { agentId: identity.id, peerAddress, repository: repository.repository, root };
245
+ const binding = {
246
+ agentId: identity.id,
247
+ peerAddress,
248
+ repository: repository.repository,
249
+ root,
250
+ };
271
251
  if (journal.binding && JSON.stringify(journal.binding) !== JSON.stringify(binding))
272
252
  throw new Error("This journal belongs to a different agent, peer, or checkout.");
273
253
  if (!journal.binding) {
@@ -301,7 +281,11 @@ export async function runCollaborationListener({
301
281
  continue;
302
282
  }
303
283
  if (prior?.status === "started" || prior?.status === "failed") {
304
- emit({ type: "needs_attention", messageId: message.id, taskId: message.taskId });
284
+ emit({
285
+ type: "needs_attention",
286
+ messageId: message.id,
287
+ taskId: message.taskId,
288
+ });
305
289
  continue;
306
290
  }
307
291
  const task = await client.tasks.get(message.taskId, { signal });
@@ -352,7 +336,12 @@ export async function runCollaborationListener({
352
336
  const outboundBefore = await latestOutboundTaskCursor(client, task.id, undefined, signal);
353
337
  journal.messages[message.id] = { status: "started", taskId: task.id };
354
338
  await saveJournal(journalFile, journal);
355
- emit({ type: "started", messageId: message.id, taskId: task.id, provider });
339
+ emit({
340
+ type: "started",
341
+ messageId: message.id,
342
+ taskId: task.id,
343
+ provider,
344
+ });
356
345
  try {
357
346
  const result = await runAgent({
358
347
  provider,
@@ -377,12 +366,23 @@ export async function runCollaborationListener({
377
366
  sessionId: result.sessionId,
378
367
  runCount: (savedTask?.runCount ?? 0) + 1,
379
368
  };
380
- journal.messages[message.id] = { status: "completed", taskId: task.id };
369
+ journal.messages[message.id] = {
370
+ status: "completed",
371
+ taskId: task.id,
372
+ };
381
373
  await saveJournal(journalFile, journal);
382
374
  await client.messages.ack(message.id, { signal });
383
- emit({ type: "completed", messageId: message.id, taskId: task.id, provider });
375
+ emit({
376
+ type: "completed",
377
+ messageId: message.id,
378
+ taskId: task.id,
379
+ provider,
380
+ });
384
381
  } catch (error) {
385
- journal.messages[message.id] = { status: "failed", taskId: task.id };
382
+ journal.messages[message.id] = {
383
+ status: "failed",
384
+ taskId: task.id,
385
+ };
386
386
  await saveJournal(journalFile, journal);
387
387
  await client.tasks
388
388
  .postUpdate(task.id, listenerStatusText(provider, "failed"), {
@@ -0,0 +1,61 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { realpath } from "node:fs/promises";
3
+
4
+ export const NODE_TEST_IMAGE =
5
+ "node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43";
6
+
7
+ /** Only an isolated repository is mounted. No credentials or Docker socket enter tests. */
8
+ export async function runDockerNodeTests(checkout, signal, runFile) {
9
+ const root = await realpath(checkout);
10
+ if (root.includes(",") || /[\r\n]/.test(root)) throw new Error("Unsupported sandbox mount path.");
11
+ const name = `totem-node-test-${randomUUID()}`;
12
+ const uid = typeof process.getuid === "function" ? process.getuid() : 1000;
13
+ const gid = typeof process.getgid === "function" ? process.getgid() : 1000;
14
+ try {
15
+ const result = await runFile(
16
+ "docker",
17
+ [
18
+ "run",
19
+ "--rm",
20
+ "--pull=never",
21
+ "--name",
22
+ name,
23
+ "--network=none",
24
+ "--read-only",
25
+ "--cap-drop=ALL",
26
+ "--security-opt=no-new-privileges",
27
+ "--pids-limit=64",
28
+ "--memory=512m",
29
+ "--cpus=1",
30
+ "--user",
31
+ `${uid}:${gid}`,
32
+ "--tmpfs",
33
+ "/tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777",
34
+ "--env",
35
+ "HOME=/tmp",
36
+ "--env",
37
+ "TMPDIR=/tmp",
38
+ "--mount",
39
+ `type=bind,source=${root},target=/workspace,readonly`,
40
+ "--workdir",
41
+ "/workspace",
42
+ NODE_TEST_IMAGE,
43
+ "node",
44
+ "--test",
45
+ ],
46
+ { signal, timeoutMs: 120000, acceptNonZero: true },
47
+ );
48
+ return {
49
+ command: "node --test (Docker; no network, credentials, or host home)",
50
+ exitCode: result.exitCode,
51
+ output: `${result.stdout}\n${result.stderr}`.slice(0, 8000),
52
+ };
53
+ } finally {
54
+ // Interrupting the Docker client does not reliably stop its container.
55
+ // Cleanup targets only our random name and deliberately ignores the aborted signal.
56
+ await runFile("docker", ["rm", "--force", name], {
57
+ timeoutMs: 10000,
58
+ acceptNonZero: true,
59
+ });
60
+ }
61
+ }
@@ -0,0 +1,29 @@
1
+ /** Resolve only local, credential-free HTTP model endpoints. Never follow redirects. */
2
+ export function localModelEndpoint(provider = "qwen-local", { ollamaUrl, qwenUrl, nimUrl } = {}) {
3
+ if (!["qwen-local", "ollama", "ollama-local", "nim-local"].includes(provider))
4
+ throw new Error("Choose qwen-local, ollama-local, or nim-local.");
5
+ const qwen = provider === "qwen-local";
6
+ const openai = qwen || provider === "nim-local";
7
+ const url = new URL(
8
+ openai
9
+ ? qwen
10
+ ? (qwenUrl ?? "http://127.0.0.1:8087/v1")
11
+ : (nimUrl ?? "http://127.0.0.1:8000/v1")
12
+ : (ollamaUrl ?? "http://127.0.0.1:11434"),
13
+ );
14
+ if (
15
+ !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) ||
16
+ url.protocol !== "http:" ||
17
+ url.username ||
18
+ url.password ||
19
+ url.search ||
20
+ url.hash ||
21
+ !(openai ? ["/v1", "/v1/"].includes(url.pathname) : url.pathname === "/")
22
+ )
23
+ throw new Error(
24
+ openai
25
+ ? `The ${qwen ? "Qwen" : "NIM"} model must use a credential-free local HTTP /v1 endpoint.`
26
+ : "The Ollama model must use a credential-free local HTTP origin.",
27
+ );
28
+ return `${url.origin}${openai ? "/v1/chat/completions" : "/api/chat"}`;
29
+ }
@@ -1,9 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { stat } from "node:fs/promises";
3
4
  import { resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
6
- import { installedClientInvocation } from "./platform-client.mjs";
7
+ import { installedClientInvocation, localClientEnvironment } from "./platform-client.mjs";
7
8
 
8
9
  const DEFAULT_ORIGIN = "https://totemlayer.com";
9
10
  const DEFAULT_NAME = "totem-local";
@@ -157,3 +158,241 @@ export async function installTotemMcp(args, environment = process.env, runtime =
157
158
  );
158
159
  return `Connected ${options.name} to ${command} at ${options.baseUrl}. Restart ${command}, then ask it to call totem_get_identity. The Totem CLI package must remain installed.\n`;
159
160
  }
161
+
162
+ /** Guided setup owns a distinct entry per profile; the manual installer retains its default. */
163
+ export function managedMcpName(profileName) {
164
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(profileName) || profileName.length > 64)
165
+ fail("Choose a valid saved profile before configuring MCP.");
166
+ const plain = `totem-${profileName}`;
167
+ return plain.length <= 64
168
+ ? plain
169
+ : `totem_${profileName.slice(0, 24)}_${createHash("sha256").update(profileName).digest("hex").slice(0, 32)}`;
170
+ }
171
+
172
+ function managedArgs(profile, name) {
173
+ const variables = [
174
+ "--env",
175
+ `TOTEM_BASE_URL=${profile.baseUrl}`,
176
+ "--env",
177
+ `TOTEM_NETWORK_API_KEY_FILE=${profile.credentialFile}`,
178
+ ];
179
+ return profile.client === "codex"
180
+ ? ["mcp", "add", name, ...variables, "--", process.execPath, SERVER_ENTRY, "mcp"]
181
+ : [
182
+ "mcp",
183
+ "add",
184
+ ...variables,
185
+ "--scope",
186
+ "user",
187
+ "--transport",
188
+ "stdio",
189
+ name,
190
+ "--",
191
+ process.execPath,
192
+ SERVER_ENTRY,
193
+ "mcp",
194
+ ];
195
+ }
196
+
197
+ function matchesManagedEntry(entry, profile, name) {
198
+ const transport = entry?.transport;
199
+ const expectedEnvironment = {
200
+ TOTEM_BASE_URL: profile.baseUrl,
201
+ TOTEM_NETWORK_API_KEY_FILE: profile.credentialFile,
202
+ };
203
+ return (
204
+ entry?.name === name &&
205
+ entry.enabled !== false &&
206
+ !entry.disabled_reason &&
207
+ ["enabled_tools", "disabled_tools", "startup_timeout_sec", "tool_timeout_sec"].every(
208
+ (key) => entry[key] == null,
209
+ ) &&
210
+ transport?.type === "stdio" &&
211
+ transport.command === process.execPath &&
212
+ JSON.stringify(transport.args) === JSON.stringify([SERVER_ENTRY, "mcp"]) &&
213
+ transport.cwd == null &&
214
+ (transport.env_vars == null ||
215
+ (Array.isArray(transport.env_vars) && transport.env_vars.length === 0)) &&
216
+ transport.env &&
217
+ Object.keys(transport.env).length === 2 &&
218
+ Object.entries(expectedEnvironment).every(([key, value]) => transport.env[key] === value) &&
219
+ Object.keys(transport).every((key) =>
220
+ ["type", "command", "args", "env", "env_vars", "cwd"].includes(key),
221
+ ) &&
222
+ Object.keys(entry).every((key) =>
223
+ [
224
+ "name",
225
+ "enabled",
226
+ "disabled_reason",
227
+ "transport",
228
+ "enabled_tools",
229
+ "disabled_tools",
230
+ "startup_timeout_sec",
231
+ "tool_timeout_sec",
232
+ ].includes(key),
233
+ )
234
+ );
235
+ }
236
+
237
+ async function managedCall(profile, args, environment, runtime) {
238
+ try {
239
+ const result = runtime.runClient
240
+ ? await runtime.runClient(profile.client, args, environment)
241
+ : runClient(profile.client, args, environment, {
242
+ platform: process.platform,
243
+ spawnProcess: spawnSync,
244
+ ...runtime,
245
+ });
246
+ return { ...result, status: result.status ?? 0 };
247
+ } catch (error) {
248
+ return { status: 1, stdout: "", stderr: typeof error?.stderr === "string" ? error.stderr : "" };
249
+ }
250
+ }
251
+
252
+ async function inspectEntry(profile, environment, runtime) {
253
+ const name = managedMcpName(profile.profileName);
254
+ const response = await managedCall(
255
+ profile,
256
+ ["mcp", "get", name, ...(profile.client === "codex" ? ["--json"] : [])],
257
+ environment,
258
+ runtime,
259
+ );
260
+ if (response.status !== 0)
261
+ return {
262
+ name,
263
+ state: /no mcp server|not found|does not exist/i.test(
264
+ `${response.stdout}\n${response.stderr}`,
265
+ )
266
+ ? "not_configured"
267
+ : "unavailable",
268
+ };
269
+ if (profile.client !== "codex") return { name, state: "unverified" };
270
+ try {
271
+ return { name, state: "present", entry: JSON.parse(response.stdout) };
272
+ } catch {
273
+ return { name, state: "unverified" };
274
+ }
275
+ }
276
+
277
+ function reconnectAdvice(profile, name) {
278
+ return `Inspect ${name} with \`${profile.client} mcp get ${name}\`. To replace that exact entry after reviewing it, run \`${profile.client} mcp remove ${name}\`, then \`totem setup --profile ${profile.profileName} --install-mcp\`. Other entries are preserved.`;
279
+ }
280
+
281
+ export async function inspectManagedTotemMcp(profile, environment = process.env, runtime = {}) {
282
+ const inspected = await inspectEntry(profile, localClientEnvironment(environment), runtime);
283
+ if (
284
+ inspected.state === "present" &&
285
+ matchesManagedEntry(inspected.entry, profile, inspected.name)
286
+ )
287
+ return {
288
+ name: inspected.name,
289
+ state: "configured_unverified",
290
+ message: `The ${inspected.name} settings match this profile. Verify client sign-in and tool connectivity in the AI app with totem_get_identity.`,
291
+ };
292
+ if (inspected.state === "not_configured")
293
+ return {
294
+ name: inspected.name,
295
+ state: "not_configured",
296
+ message: `Run totem setup --profile ${profile.profileName} --install-mcp to add this profile's connection.`,
297
+ };
298
+ return {
299
+ name: inspected.name,
300
+ state: "needs_attention",
301
+ message: `${inspected.state === "unavailable" ? "The client configuration could not be read." : "The existing entry could not be verified against this saved profile."} ${reconnectAdvice(profile, inspected.name)}`,
302
+ };
303
+ }
304
+
305
+ export async function installManagedTotemMcp(
306
+ profile,
307
+ previousProfile,
308
+ environment = process.env,
309
+ runtime = {},
310
+ ) {
311
+ if (!["codex", "claude"].includes(profile.client))
312
+ fail("Choose an existing AI app for MCP setup.");
313
+ try {
314
+ await validateKeyFile(profile.credentialFile, runtime.platform ?? process.platform);
315
+ } catch {
316
+ return {
317
+ name: managedMcpName(profile.profileName),
318
+ state: "needs_attention",
319
+ message:
320
+ "The saved private key file is unavailable or unsafe. Check this profile before changing MCP settings.",
321
+ };
322
+ }
323
+ const safeEnvironment = localClientEnvironment(environment);
324
+ const inspected = await inspectEntry(profile, safeEnvironment, runtime);
325
+ const { name } = inspected;
326
+ const guidance = reconnectAdvice(profile, name);
327
+ if (inspected.state === "not_configured") {
328
+ try {
329
+ await (runtime.install ?? installTotemMcp)(
330
+ [
331
+ profile.client,
332
+ "--key-file",
333
+ profile.credentialFile,
334
+ "--base-url",
335
+ profile.baseUrl,
336
+ "--name",
337
+ name,
338
+ ],
339
+ safeEnvironment,
340
+ runtime,
341
+ );
342
+ if (profile.client === "codex") {
343
+ const after = await inspectEntry(profile, safeEnvironment, runtime);
344
+ if (after.state !== "present" || !matchesManagedEntry(after.entry, profile, name))
345
+ return {
346
+ name,
347
+ state: "needs_attention",
348
+ message: `The new MCP settings could not be verified against this profile. ${guidance}`,
349
+ };
350
+ }
351
+ return {
352
+ name,
353
+ state: "installed_unverified",
354
+ message: `${name} settings were saved. Restart the AI app and call totem_get_identity to verify its connection.`,
355
+ };
356
+ } catch {
357
+ return {
358
+ name,
359
+ state: "needs_attention",
360
+ message: `The connection is saved, but MCP installation could not be completed. ${guidance}`,
361
+ };
362
+ }
363
+ }
364
+ if (inspected.state === "present" && matchesManagedEntry(inspected.entry, profile, name))
365
+ return {
366
+ name,
367
+ state: "configured_unverified",
368
+ message: `${name} already matches this profile; nothing changed. Verify its identity in your AI app with totem_get_identity.`,
369
+ };
370
+ const ownsPrevious =
371
+ previousProfile?.profileName === profile.profileName &&
372
+ previousProfile.client === profile.client &&
373
+ inspected.state === "present" &&
374
+ matchesManagedEntry(inspected.entry, previousProfile, name);
375
+ if (!ownsPrevious)
376
+ return {
377
+ name,
378
+ state: "needs_attention",
379
+ message: `The existing entry was preserved because its ownership and settings could not be verified. ${guidance}`,
380
+ };
381
+ // Codex add can update this exact named entry. No remove occurs: unsupported client
382
+ // versions fail safely, and an exact read-back is required before reporting an update.
383
+ const updated = await managedCall(profile, managedArgs(profile, name), safeEnvironment, runtime);
384
+ if (updated.status === 0) {
385
+ const after = await inspectEntry(profile, safeEnvironment, runtime);
386
+ if (after.state === "present" && matchesManagedEntry(after.entry, profile, name))
387
+ return {
388
+ name,
389
+ state: "updated_unverified",
390
+ message: `${name} now references this profile's current private key. Restart the AI app and verify it with totem_get_identity.`,
391
+ };
392
+ }
393
+ return {
394
+ name,
395
+ state: "needs_attention",
396
+ message: `The managed MCP update could not be verified. ${guidance}`,
397
+ };
398
+ }
package/bin/mcp.mjs CHANGED
@@ -8,7 +8,7 @@ import * as z from "zod/v4";
8
8
  import { Totem } from "./sdk/index.js";
9
9
  import { inspectLocalRepository, verifyLocalRepoClaim } from "./repository.mjs";
10
10
 
11
- const VERSION = "0.1.4";
11
+ const VERSION = "0.1.6";
12
12
  const MAX_KEY_BYTES = 8 * 1024;
13
13
  const MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
14
14
  const collaborationSchema = z