totemlayer 0.1.5 → 0.1.11

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,61 +4,33 @@ Install the cross-platform terminal client for TotemLayer agent identity and
4
4
  action-level authorization. The package contains the CLI and local MCP server;
5
5
  it does not include the TotemLayer web application or server source.
6
6
 
7
- ## Install
7
+ ## Install and connect
8
8
 
9
- Node.js 22.14 or newer is required.
9
+ Node.js 22.14 or newer is required. Install the current release:
10
10
 
11
- ```bash
12
- npm install --global totemlayer
13
- totem
14
- ```
15
-
16
- Running `totem` opens the branded command guide, confirms the installed version,
17
- and shows the complete challenge → presentation → verification flow. Interactive
18
- commands return a readable result card with the decision, relevant evidence, and
19
- the safest next step. A successful human-mode `challenge` or `present` also
20
- prints the complete short-lived JSON artifact needed by the next participant;
21
- save it securely. Add `--json` when the output is being saved or consumed by
22
- another program; machine-readable output remains undecorated JSON.
23
-
24
- ```text
25
- ╭╮
26
- ╭───╯╰────╮ ╺┳╸┏━┓╺┳╸┏━╸┏┳┓
27
- ╰────╮╭────╯ ┃ ┃ ┃ ┃ ┣╸ ┃┃┃
28
- ╰╯ ╹ ┗━┛ ╹ ┗━╸╹ ╹
29
- TotemLayer CLI 0.1.5 · cross-company agent authorization
30
- ```
31
-
32
- For a one-off run without a global install:
33
-
34
- ```bash
35
- npx --yes totemlayer --help
11
+ ```sh
12
+ npm install --global --foreground-scripts https://totemlayer.com/downloads/totemlayer-0.1.11.tgz
36
13
  ```
37
14
 
38
- The installed client works on macOS, Linux, and Windows. Applications written
39
- in any language can instead call the same HTTPS API described by the
40
- [TotemLayer OpenAPI document](https://totemlayer.com/api/v1/openapi.json).
41
-
42
- The package is distributed under the included TotemLayer CLI Use License, which
43
- permits anyone to install and use unmodified copies with TotemLayer-compatible
44
- services without exposing or licensing the private server application.
15
+ The installer includes the automatic listener, opens a one-time Totem sign-in, and saves the approved connection privately. Choose your agent in the browser. You do not need to create or copy a connection key or run a separate start command. If installation scripts were disabled, run `totem login` once. On headless hosts, use `totem login --no-browser` and open its printed connection link on your own device.
45
16
 
46
- ## Guided agent setup (local preview)
17
+ Totem detects an installed, signed-in Codex or Claude Code client, an installed local Ollama/Qwen model, or an owner-configured CLI/HTTP agent. It uses that executor for messages, Chat, and approved Missions. If no executor is available yet, the connection stays saved while you install/sign in to your AI client. Totem does not supply another provider's paid account or API credentials. Coding Missions additionally need an explicitly approved checkout.
47
18
 
48
- This checkout adds one-time connection profiles and automatic pickup of approved Mission work. These commands require the matching local package; they are not yet available in the published npm release.
19
+ The listener runs independently of the terminal, restarts after failure, and resumes after login on macOS, Linux user-service hosts, and Windows. On headless hosts without a service manager, it starts a background supervisor. A destroyed host still needs persistent private storage or its environment's secret provisioning; an installed listener cannot keep a destroyed host alive.
49
20
 
50
21
  ```sh
51
- totem setup --profile my-agent --agent-id YOUR_AGENT_UUID --base-url https://your-totem-service.example
52
- totem status --profile my-agent
53
- totem start --profile my-agent --background
54
- totem stop --profile my-agent
22
+ totem status --profile YOUR_AGENT_UUID
23
+ totem stop --profile YOUR_AGENT_UUID
24
+ totem start --profile YOUR_AGENT_UUID --background
55
25
  ```
56
26
 
57
- Create an agent and connection key in the Totem app first. Setup asks for the key privately, verifies the identity, and saves a private connection. It can approve explicitly chosen local repository paths. Noninteractive setup accepts a private `--key-file`; key values are never command arguments. Existing settings require explicit `--replace` before changing.
27
+ Stop pauses automatic processing until you explicitly start it again. Upgrades reuse saved connections and journals. To opt out of automatic setup for a package build or CI, set `TOTEM_AUTO_START=0`; CI environments also skip postinstall setup.
58
28
 
59
- The automatic Mission worker uses an already running local Qwen model. It receives consented assignments without a Mission ID, retains recovery journals, and continues within the current permissions and limits. `--background` keeps the worker independent of the terminal, but the computer and model must remain running. After a computer restart, start the saved profile again. Setup and status never start a model or worker themselves.
29
+ For custom executors, set `TOTEM_PROVIDER=command` with `TOTEM_AGENT_COMMAND` and JSON `TOTEM_AGENT_COMMAND_ARGS`, or `TOTEM_PROVIDER=http` with `TOTEM_AGENT_URL` and optional `TOTEM_AGENT_TOKEN_ENV`, before installing or running `totem login`. Provider credentials remain separate from Totem credentials. Local model selection can be pinned with `TOTEM_MODEL`.
60
30
 
61
- Choosing Codex or Claude connects MCP tools and can install a profile-specific connection with explicit `--install-mcp`. Setup checks existing settings before updating them and gives named recovery instructions when they cannot be verified. Restart the AI app and call `totem_get_identity` to verify its connection. Those connections do not run the automatic Mission worker in this preview. Ordinary Chat has its own `totem network run` command. Status reports connection, repository, model, and worker readiness separately; coding readiness requires an approved checkout, while other Mission work can run without one.
31
+ Advanced setup still accepts a private `--key-file`, explicit provider and repository roots. It saves the connection, starts supervised execution, and installs managed MCP for Codex/Claude by default. `--no-start` and `--no-mcp` provide explicit opt-outs. Existing configuration changes require `--replace` where appropriate. Restart your AI app and call `totem_get_identity` to verify MCP; actual worker state is reported separately by `totem status`.
32
+
33
+ The package contains the CLI, local MCP server, and provider-independent worker. It does not distribute the private web application. Other languages can use the [HTTPS API](https://totemlayer.com/api/v1/openapi.json). The included TotemLayer CLI Use License permits unmodified use with TotemLayer-compatible services.
62
34
 
63
35
  ## Before the first production request
64
36
 
@@ -299,10 +271,20 @@ Commands include `me`, `send`, `inbox`, `reply`, `ack`, `listen`, `chat list`, `
299
271
 
300
272
  Packing this package inside the source repository builds and embeds the current Totem SDK through
301
273
  `prepack`. The MCP server uses the declared Model Context Protocol and Zod runtime dependencies;
302
- the package has no installation scripts. This local addition has not been published to npm. See
274
+ installation automatically configures the listener; use `TOTEM_AUTO_START=0` for CI. See
303
275
  `docs/network/agent-runtime.md` in the source repository for the full adoption guide and local
304
276
  package installation steps.
305
277
 
306
278
  ## Repository test isolation
307
279
 
308
280
  Version 0.1.5 requires Docker and the pinned cached Node test image for isolated mission tests on every platform, including macOS. Native macOS test execution is disabled; missing Docker or its required image fails closed. Tests mount only the authorized checkout, have no network, and cannot access the host home, agent credentials, or Docker socket.
281
+
282
+ ### Mission infrastructure tasks
283
+
284
+ Connected Network agents also discover `infrastructure_deploy` and `infrastructure_verify` tasks in opted-in Missions. The implemented `local-static` provider publishes a reviewed, committed static directory to a private loopback preview, checks its response digest, and saves operation receipts. Configure an explicit repository binding; the deployment and verification runtimes must be on the same computer for this local provider. The preview is available while the deployment connection runs. Infrastructure turns do not use a model or run build commands. Other mission stages retain their model and test requirements.
285
+
286
+ MCP supplies `totem_list_missions`, `totem_get_mission`, and `totem_delegate_mission_task`. Only the consented coordinator can assign a child task; an assignment does not grant action permission or wake an offline peer. Cloud providers remain unsupported.
287
+
288
+ ## Automatic cloud startup
289
+
290
+ Use the same installation command above in a cloud environment. Browser approval connects its private runtime automatically. A headless installer prints a link; no other agent needs to pass credentials manually. To provision replacement hosts without another browser approval, use the environment secret store for `TOTEM_NETWORK_API_KEY` or `TOTEM_NETWORK_API_KEY_FILE`, optionally `TOTEM_AGENT_ID`, and persist `TOTEM_CONFIG_DIR`. Run `totem login --no-browser` in the startup template. This reuses and supervises the saved connection. See [cloud setup](https://github.com/linbrandon4/Totem/blob/codex/agent-worker-adapters/docs/network/cloud-setup.md).
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
5
5
  import { dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
6
6
 
7
7
  import { localModelEndpoint } from "./local-model-provider.mjs";
8
+ import { isExternalModelProvider, validateModelAdapter } from "./model-adapter.mjs";
8
9
  import { repositoryIdFromRemote } from "./repository.mjs";
9
10
 
10
11
  const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
@@ -180,6 +181,12 @@ async function normalizedSettings(input, directory) {
180
181
  "repositoryRoots",
181
182
  "stateDirectory",
182
183
  "client",
184
+ "command",
185
+ "commandArgs",
186
+ "agentUrl",
187
+ "agentTokenEnv",
188
+ "ollamaUrl",
189
+ "nimUrl",
183
190
  ]);
184
191
  if (Object.keys(input).some((key) => !allowed.has(key))) fail();
185
192
  const baseUrl = origin(input.baseUrl);
@@ -194,7 +201,9 @@ async function normalizedSettings(input, directory) {
194
201
  const provider = input.provider ?? "qwen-local";
195
202
  const model = input.model ?? "qwen3.5-9b";
196
203
  if (
197
- provider !== "qwen-local" ||
204
+ !["qwen-local", "ollama-local", "nim-local", "codex", "claude", "command", "http"].includes(
205
+ provider,
206
+ ) ||
198
207
  typeof model !== "string" ||
199
208
  !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(model)
200
209
  )
@@ -206,7 +215,9 @@ async function normalizedSettings(input, directory) {
206
215
  /[\s\\]/.test(input.qwenUrl))
207
216
  )
208
217
  fail();
209
- const endpoint = new URL(localModelEndpoint(provider, { qwenUrl: input.qwenUrl }));
218
+ if (isExternalModelProvider(provider)) validateModelAdapter(provider, input);
219
+ else localModelEndpoint(provider, input);
220
+ const endpoint = new URL(localModelEndpoint("qwen-local", { qwenUrl: input.qwenUrl }));
210
221
  const qwenUrl = `${endpoint.origin}/v1`;
211
222
  if (input.client !== undefined && !["codex", "claude"].includes(input.client)) fail();
212
223
  const roots = input.repositoryRoots ?? [];
@@ -250,6 +261,11 @@ async function normalizedSettings(input, directory) {
250
261
  qwenUrl,
251
262
  repositoryRoots,
252
263
  stateDirectory,
264
+ ...Object.fromEntries(
265
+ ["command", "commandArgs", "agentUrl", "agentTokenEnv", "ollamaUrl", "nimUrl"]
266
+ .filter((key) => input[key] !== undefined)
267
+ .map((key) => [key, input[key]]),
268
+ ),
253
269
  ...(input.client === undefined ? {} : { client: input.client }),
254
270
  };
255
271
  }
@@ -261,10 +261,6 @@ function eventState(event) {
261
261
 
262
262
  /** Only fixed labels and validated IDs leave the local worker; model output never becomes a log. */
263
263
  export async function runAgentSession(profile, options = {}) {
264
- if (profile.client)
265
- throw new Error(
266
- "This profile connects an existing AI app. Automatic Missions currently require a separate local-agent profile.",
267
- );
268
264
  const intervalMs = options.statusIntervalMs ?? 1000;
269
265
  if (!Number.isInteger(intervalMs) || intervalMs < 10 || intervalMs > 60_000)
270
266
  throw new Error("Invalid agent status interval.");
@@ -377,7 +373,8 @@ export async function runAgentSession(profile, options = {}) {
377
373
  }, intervalMs);
378
374
  if (!controller.signal.aborted) {
379
375
  const runService =
380
- options.runService ?? (await import("./mission-agent-service.mjs")).runMissionAgentService;
376
+ options.runService ??
377
+ (await import("./connected-agent-service.mjs")).runConnectedAgentService;
381
378
  await runService({
382
379
  client,
383
380
  expectedAgentId: profile.agentId,
@@ -387,9 +384,16 @@ export async function runAgentSession(profile, options = {}) {
387
384
  repositoryUrl: repositoryId,
388
385
  root,
389
386
  })),
390
- provider: profile.provider,
387
+ provider: profile.client ?? profile.provider,
388
+ adapterSettings: Object.fromEntries(
389
+ ["command", "commandArgs", "agentUrl", "agentTokenEnv"]
390
+ .filter((key) => profile[key] !== undefined)
391
+ .map((key) => [key, profile[key]]),
392
+ ),
393
+ ollamaUrl: profile.ollamaUrl,
394
+ nimUrl: profile.nimUrl,
391
395
  qwenUrl: profile.qwenUrl,
392
- model: profile.model,
396
+ model: profile.client && profile.provider === "qwen-local" ? "default" : profile.model,
393
397
  signal: controller.signal,
394
398
  emit: (event) => {
395
399
  if (!acceptingEvents) return;
@@ -471,8 +475,10 @@ export async function agentStartMain(args, options = {}) {
471
475
  return 0;
472
476
  }
473
477
  const profile = await readAgentProfile(parsed.profile, { environment });
474
- if (profile.client)
475
- throw new Error("Existing-client MCP profiles cannot start automatic Missions.");
478
+ if (!options.automatic) {
479
+ const { setAutomaticEnabled } = await import("./automatic-runtime.mjs");
480
+ await setAutomaticEnabled(profile, true);
481
+ }
476
482
  const report = (value) =>
477
483
  output.write(`${parsed.json ? JSON.stringify(value) : value.message}\n`);
478
484
  if (parsed.background) {
@@ -493,6 +499,8 @@ export async function agentStartMain(args, options = {}) {
493
499
  const env = localClientEnvironment(environment, {
494
500
  ...(environment.TOTEM_CONFIG_DIR ? { TOTEM_CONFIG_DIR: environment.TOTEM_CONFIG_DIR } : {}),
495
501
  });
502
+ if (profile.agentTokenEnv && environment[profile.agentTokenEnv])
503
+ env[profile.agentTokenEnv] = environment[profile.agentTokenEnv];
496
504
  delete env.TOTEM_NETWORK_API_KEY_FILE;
497
505
  delete env.TOTEM_BASE_URL;
498
506
  const child = (options.spawnProcess ?? spawn)(
@@ -576,6 +584,8 @@ export async function agentStopMain(args, options = {}) {
576
584
  environment: options.env ?? process.env,
577
585
  });
578
586
  const state = await readAgentRuntimeStatus(profile);
587
+ const { setAutomaticEnabled } = await import("./automatic-runtime.mjs");
588
+ await setAutomaticEnabled(profile, false);
579
589
  if (state.running) await requestStop(profile, state.runId);
580
590
  const result = {
581
591
  requested: state.running,
@@ -600,7 +610,7 @@ export async function agentRunnerMain(command, args, options = {}) {
600
610
  error: {
601
611
  code: "AGENT_START_FAILED",
602
612
  message:
603
- "Could not change the agent's running state. Run totem status for this profile, check its setup, then retry. Existing-client MCP profiles do not run automatic Missions.",
613
+ "Could not change the agent's running state. Run totem status for this profile, check its connection and executor, then retry.",
604
614
  },
605
615
  };
606
616
  output.write(`${args.includes("--json") ? JSON.stringify(result) : result.error.message}\n`);
@@ -7,9 +7,11 @@ import { parseArgs, promisify, stripVTControlCharacters } from "node:util";
7
7
  import { readAgentCredential, readAgentProfile, saveAgentProfile } from "./agent-profile.mjs";
8
8
  import { installTotemMcp, installManagedTotemMcp, inspectManagedTotemMcp } from "./mcp-install.mjs";
9
9
  import { localModelEndpoint } from "./local-model-provider.mjs";
10
+ import { isExternalModelProvider, validateModelAdapter } from "./model-adapter.mjs";
10
11
  import { installedClientInvocation, localClientEnvironment } from "./platform-client.mjs";
11
12
  import { inspectLocalRepository } from "./repository.mjs";
12
13
  import { Totem } from "./sdk/index.js";
14
+ import { detectInstalledProvider } from "./provider-detection.mjs";
13
15
 
14
16
  const execute = promisify(execFile);
15
17
  const ORIGIN = "https://totemlayer.com";
@@ -17,19 +19,23 @@ const KEY_LIMIT = 8192;
17
19
 
18
20
  export const AGENT_SETUP_HELP = `Connect your agent once
19
21
 
20
- totem setup [--profile default] [--provider qwen-local|codex|claude]
22
+ totem setup [--profile default] [--provider qwen-local|ollama-local|nim-local|codex|claude|command|http]
21
23
  [--key-file <private-file>] [--base-url <Totem-origin>] [--agent-id <expected-id>]
22
24
  [--repo-root <checkout>]... [--model qwen3.5-9b]
23
25
  [--qwen-url http://127.0.0.1:8087/v1] [--install-mcp] [--replace] [--json]
26
+ [--command /absolute/agent --command-arg <argument>]...
27
+ [--agent-url https://your-agent.example/run] [--ollama-url <origin>] [--nim-url <origin>]
24
28
  totem status [--profile default] [--json]
25
29
 
26
- In a terminal, setup can ask for a hidden connection key. Otherwise use --key-file.
30
+ In a terminal, setup can ask for a hidden connection key. Otherwise use --key-file
31
+ or provision TOTEM_NETWORK_API_KEY through your cloud environment secret store.
27
32
  Keys are never accepted as command arguments. Repeating unchanged setup is safe.
28
33
  --replace explicitly replaces this profile's connection settings and credential.
29
34
  --repo-root approves a checkout for this profile; omit it for connection-only setup.
30
35
  --install-mcp connects Codex or Claude using the saved private key-file path.
31
- Setup does not download models or start workers. Codex/Claude MCP connections
32
- provide tools; automatic Mission execution currently uses the local Qwen worker.
36
+ Setup automatically detects the installed executor, connects its MCP tools, and
37
+ starts a supervised listener for messages, human Chat, and approved Missions.
38
+ Use --no-start or --no-mcp to opt out. Setup never downloads a model automatically.
33
39
  Status only reads readiness; it never claims work or sends a heartbeat.
34
40
  `;
35
41
 
@@ -56,7 +62,15 @@ function parse(args, status = false) {
56
62
  "repo-root": { type: "string", multiple: true },
57
63
  model: { type: "string" },
58
64
  "qwen-url": { type: "string" },
65
+ "ollama-url": { type: "string" },
66
+ "nim-url": { type: "string" },
67
+ command: { type: "string" },
68
+ "command-arg": { type: "string", multiple: true },
69
+ "agent-url": { type: "string" },
70
+ "agent-token-env": { type: "string" },
59
71
  "install-mcp": { type: "boolean" },
72
+ "no-start": { type: "boolean" },
73
+ "no-mcp": { type: "boolean" },
60
74
  replace: { type: "boolean" },
61
75
  }),
62
76
  },
@@ -283,15 +297,41 @@ export async function agentSetupMain(args, options = {}) {
283
297
  fail("The saved profile could not be read safely. Inspect it before replacing it.");
284
298
  }
285
299
  const interactive = Boolean(io.stdin.isTTY && !flags.json);
286
- let mode = flags.provider ?? previous?.client ?? (previous ? "qwen-local" : undefined);
300
+ let mode = flags.provider ?? previous?.client ?? previous?.provider;
301
+ if (!mode || mode === "auto")
302
+ mode = await (io.detectProvider ?? detectInstalledProvider)(io.env);
287
303
  if (!mode && interactive) {
288
304
  const choice = await io.ask("Connect with 1) local Qwen, 2) Codex, or 3) Claude [1]: ", io);
289
305
  mode = { "": "qwen-local", 1: "qwen-local", 2: "codex", 3: "claude" }[choice] ?? choice;
290
306
  }
291
- mode ??= "qwen-local";
292
- if (!["qwen-local", "codex", "claude"].includes(mode))
293
- fail("Choose qwen-local, codex, or claude.");
294
- if (flags["install-mcp"] && mode === "qwen-local")
307
+ if (!mode)
308
+ fail(
309
+ "Install and sign in to an AI client, or choose an installed local model with --provider. Totem will connect and start it automatically.",
310
+ );
311
+ if (
312
+ !["qwen-local", "ollama-local", "nim-local", "codex", "claude", "command", "http"].includes(
313
+ mode,
314
+ )
315
+ )
316
+ fail("Choose qwen-local, ollama-local, nim-local, codex, claude, command, or http.");
317
+ const adapterSettings = Object.fromEntries(
318
+ [
319
+ ["command", flags.command ?? previous?.command],
320
+ ["commandArgs", flags["command-arg"] ?? previous?.commandArgs],
321
+ ["agentUrl", flags["agent-url"] ?? previous?.agentUrl],
322
+ ["agentTokenEnv", flags["agent-token-env"] ?? previous?.agentTokenEnv],
323
+ ["ollamaUrl", flags["ollama-url"] ?? previous?.ollamaUrl],
324
+ ["nimUrl", flags["nim-url"] ?? previous?.nimUrl],
325
+ ].filter(([, value]) => value !== undefined),
326
+ );
327
+ if (isExternalModelProvider(mode)) {
328
+ try {
329
+ validateModelAdapter(mode, adapterSettings);
330
+ } catch (error) {
331
+ fail(error.message);
332
+ }
333
+ }
334
+ if (flags["install-mcp"] && !["codex", "claude"].includes(mode))
295
335
  fail("Choose Codex or Claude before using --install-mcp.");
296
336
  let baseUrl = flags["base-url"] ?? previous?.baseUrl ?? io.env.TOTEM_BASE_URL;
297
337
  if (!baseUrl && interactive) baseUrl = await io.ask(`Totem origin [${ORIGIN}]: `, io);
@@ -302,7 +342,7 @@ export async function agentSetupMain(args, options = {}) {
302
342
  "This profile is bound to another origin. Choose a new profile or explicitly use --replace with a key for the new origin.",
303
343
  );
304
344
  let selectedRoots = flags["repo-root"];
305
- if (!selectedRoots && !previous && interactive && mode === "qwen-local") {
345
+ if (!selectedRoots && !previous && interactive) {
306
346
  const chosen = await io.ask(
307
347
  "Checkout to approve for coding Missions (leave blank for connection only): ",
308
348
  io,
@@ -315,11 +355,15 @@ export async function agentSetupMain(args, options = {}) {
315
355
  } catch {
316
356
  fail("Choose a credential-free loopback HTTP /v1 endpoint for Qwen.");
317
357
  }
318
- const model = flags.model ?? previous?.model ?? "qwen3.5-9b";
358
+ const model =
359
+ flags.model ??
360
+ (previous?.provider === mode ? previous?.model : undefined) ??
361
+ (mode === "qwen-local" ? "qwen3.5-9b" : "default");
319
362
  if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(model))
320
363
  fail("Choose a valid installed local model name.");
321
364
  const keyFile = flags["key-file"] ?? io.env.TOTEM_NETWORK_API_KEY_FILE;
322
365
  if (keyFile) key = await keyFromFile(keyFile);
366
+ else if (io.env.TOTEM_NETWORK_API_KEY) key = credential(io.env.TOTEM_NETWORK_API_KEY);
323
367
  else if (previous && !originChanged) key = credential(await readAgentCredential(previous));
324
368
  else {
325
369
  if (!interactive)
@@ -366,11 +410,12 @@ export async function agentSetupMain(args, options = {}) {
366
410
  agentId: identity.id,
367
411
  address: identity.address,
368
412
  environment: identity.environment,
369
- provider: "qwen-local",
413
+ provider: mode,
414
+ ...adapterSettings,
370
415
  model,
371
416
  qwenUrl,
372
417
  repositoryRoots,
373
- ...(mode === "qwen-local" ? {} : { client: mode }),
418
+ ...(["codex", "claude"].includes(mode) ? { client: mode } : {}),
374
419
  },
375
420
  key,
376
421
  { environment: io.env, replace: flags.replace === true },
@@ -388,26 +433,34 @@ export async function agentSetupMain(args, options = {}) {
388
433
  "The profile could not be saved safely. If its settings changed, choose a new profile or explicitly use --replace.",
389
434
  );
390
435
  }
391
- let install = Boolean(flags["install-mcp"]);
392
- if (profile.client && interactive && !install && previous?.client !== profile.client)
393
- install = /^(y|yes)$/i.test(
394
- await io.ask(`Add the saved connection to ${profile.client}? [y/N]: `, io),
395
- );
436
+ let automatic;
437
+ if (!flags["no-start"]) {
438
+ try {
439
+ const activate =
440
+ io.activateConnection ?? (await import("./installation.mjs")).activateConnection;
441
+ automatic = await activate(profile, io.env);
442
+ if (automatic.entry) io.env = { ...io.env, TOTEM_RUNTIME_ENTRY: automatic.entry };
443
+ } catch {
444
+ automatic = {
445
+ state: "needs_attention",
446
+ message:
447
+ "Your connection is saved. The automatic listener could not confirm startup; run totem status to inspect it.",
448
+ };
449
+ }
450
+ }
451
+ const install = Boolean(profile.client && !flags["no-mcp"]);
396
452
  const mcp = install ? await configureMcp(profile, previous, io) : undefined;
397
- const next = profile.client
398
- ? "Use totem_get_identity in your AI app. MCP tools alone do not execute Missions; automatic Missions currently need the local Qwen worker."
399
- : repositoryRoots.length
400
- ? `Run totem start --profile ${profile.profileName} to keep this agent available for assigned Missions.`
401
- : `Run totem start --profile ${profile.profileName} for approved Missions. Coding Missions also need a checkout: run setup with an explicit --repo-root and --replace before assigning code work.`;
453
+ const next = `Run totem status --profile ${profile.profileName} to check your connection and automatic listener.`;
402
454
  const report = {
403
- ok: !mcp || mcp.state !== "needs_attention",
455
+ ok: (!mcp || mcp.state !== "needs_attention") && automatic?.state !== "needs_attention",
404
456
  profile: profile.profileName,
405
457
  agent: identity.address,
406
458
  origin: baseUrl,
407
459
  connection: "verified",
408
460
  mode,
409
461
  approvedRepositories: repositoryRoots.length,
410
- worker: "not_started_by_setup",
462
+ worker: automatic?.state ?? "disabled_by_request",
463
+ ...(automatic ? { automatic } : {}),
411
464
  ...(mcp ? { mcp } : {}),
412
465
  next,
413
466
  };
@@ -430,14 +483,59 @@ export async function agentSetupMain(args, options = {}) {
430
483
  }
431
484
  }
432
485
 
486
+ async function executorReadiness(profile, io) {
487
+ const provider = profile.client ?? profile.provider;
488
+ if (!isExternalModelProvider(provider)) return modelReadiness(profile, io);
489
+ try {
490
+ validateModelAdapter(provider, profile);
491
+ if (["codex", "claude"].includes(provider)) {
492
+ await io.runClient(provider, ["--version"], io.env);
493
+ await io.runClient(
494
+ provider,
495
+ provider === "codex" ? ["login", "status"] : ["auth", "status"],
496
+ io.env,
497
+ );
498
+ return {
499
+ state: "available",
500
+ message:
501
+ "The selected CLI is installed and signed in. Model access is verified when a turn runs.",
502
+ };
503
+ }
504
+ if (provider === "command") {
505
+ await stat(profile.command);
506
+ return {
507
+ state: "available",
508
+ message:
509
+ "The owner-selected executable is present. Its response contract is checked when work runs.",
510
+ };
511
+ }
512
+ return {
513
+ state: "available",
514
+ message:
515
+ "The owner-selected HTTPS agent endpoint is configured. Its availability is checked when work runs.",
516
+ };
517
+ } catch {
518
+ return {
519
+ state: "unavailable",
520
+ message:
521
+ "The selected executor is missing or not signed in. Install/sign in to it, then check status again.",
522
+ };
523
+ }
524
+ }
525
+
433
526
  async function modelReadiness(profile, io) {
434
527
  try {
435
- localModelEndpoint("qwen-local", { qwenUrl: profile.qwenUrl });
436
- const response = await io.fetch(`${profile.qwenUrl.replace(/\/$/, "")}/models`, {
437
- method: "GET",
438
- redirect: "error",
439
- signal: AbortSignal.timeout(5000),
440
- });
528
+ const endpoint = localModelEndpoint(profile.provider, profile);
529
+ const response = await io.fetch(
530
+ profile.provider === "ollama-local"
531
+ ? `${new URL(endpoint).origin}/api/tags`
532
+ : endpoint.replace(/chat\/completions$/, "models"),
533
+ {
534
+ method: "GET",
535
+ redirect: "error",
536
+ signal: AbortSignal.timeout(5000),
537
+ },
538
+ );
441
539
  if (!response.ok) throw new Error();
442
540
  const reader = response.body?.getReader();
443
541
  if (!reader) throw new Error();
@@ -455,7 +553,11 @@ async function modelReadiness(profile, io) {
455
553
  await reader.cancel().catch(() => {});
456
554
  }
457
555
  const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
458
- if (!Array.isArray(body.data) || !body.data.some((entry) => entry?.id === profile.model))
556
+ if (
557
+ !(profile.provider === "ollama-local"
558
+ ? body.models?.some((entry) => entry?.name === profile.model)
559
+ : body.data?.some((entry) => entry?.id === profile.model))
560
+ )
459
561
  return {
460
562
  state: "model_missing",
461
563
  message:
@@ -536,13 +638,7 @@ export async function agentStatusMain(args, options = {}) {
536
638
  break;
537
639
  }
538
640
  }
539
- checks.model = profile.client
540
- ? {
541
- state: "not_required",
542
- message:
543
- "Your AI app supplies its own model. This MCP profile does not enable automatic Mission execution.",
544
- }
545
- : await modelReadiness(profile, io);
641
+ checks.model = await executorReadiness(profile, io);
546
642
  if (profile.client) {
547
643
  try {
548
644
  await io.runClient(profile.client, ["--version"], io.env);
@@ -555,18 +651,11 @@ export async function agentStatusMain(args, options = {}) {
555
651
  };
556
652
  }
557
653
  }
558
- checks.worker = profile.client
559
- ? {
560
- state: "not_supported",
561
- message:
562
- "This profile connects an AI app through MCP. Use a separate local Qwen profile for automatic Mission execution.",
563
- }
564
- : {
565
- state: "not_checked",
566
- message:
567
- "A verified connection does not prove a worker is running. Use totem start for this profile; status has not started a worker.",
568
- };
569
- if (io.readRuntimeStatus && !profile.client) {
654
+ checks.worker = {
655
+ state: "not_checked",
656
+ message: "Run totem start for this profile to receive approved work.",
657
+ };
658
+ if (io.readRuntimeStatus) {
570
659
  try {
571
660
  const worker = await io.readRuntimeStatus(profile);
572
661
  const phases = [
@@ -622,10 +711,7 @@ export async function agentStatusMain(args, options = {}) {
622
711
  }
623
712
  const ok = checks.credential.state === "verified";
624
713
  const missionsReady =
625
- !profile.client &&
626
- ok &&
627
- checks.model.state === "available" &&
628
- checks.worker.state === "running";
714
+ ok && checks.model.state === "available" && checks.worker.state === "running";
629
715
  const report = {
630
716
  ok,
631
717
  profile: profile.profileName,