totemlayer 0.1.6 → 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 +26 -44
- package/bin/agent-profile.mjs +18 -2
- package/bin/agent-runner.mjs +20 -10
- package/bin/agent-setup.mjs +141 -55
- package/bin/automatic-runtime.mjs +131 -0
- package/bin/cloud-bootstrap.mjs +64 -0
- package/bin/connected-agent-service.mjs +84 -0
- package/bin/device-login.mjs +168 -0
- package/bin/installation.mjs +160 -0
- package/bin/mcp-install.mjs +74 -18
- package/bin/mcp.mjs +55 -0
- package/bin/mission-agent-service.mjs +40 -2
- package/bin/mission-infrastructure.mjs +609 -0
- package/bin/mission-worker.mjs +59 -3
- package/bin/model-adapter.mjs +164 -0
- package/bin/network-runtime.mjs +58 -3
- package/bin/platform-client.mjs +3 -0
- package/bin/provider-detection.mjs +94 -0
- package/bin/runtime-installation.mjs +305 -0
- package/bin/sdk/index.js +5 -0
- package/bin/totem.mjs +13 -3
- package/bin/worker-core/adapters.mjs +279 -0
- package/bin/worker-core/errors.mjs +60 -0
- package/package.json +2 -1
- package/scripts/prepare.mjs +5 -0
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
|
-
```
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
52
|
-
totem
|
|
53
|
-
totem start --profile
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
package/bin/agent-profile.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/bin/agent-runner.mjs
CHANGED
|
@@ -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 ??
|
|
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 (
|
|
475
|
-
|
|
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
|
|
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`);
|
package/bin/agent-setup.mjs
CHANGED
|
@@ -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
|
|
32
|
-
|
|
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 ??
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
413
|
+
provider: mode,
|
|
414
|
+
...adapterSettings,
|
|
370
415
|
model,
|
|
371
416
|
qwenUrl,
|
|
372
417
|
repositoryRoots,
|
|
373
|
-
...(
|
|
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
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
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.
|
|
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: "
|
|
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
|
-
|
|
436
|
-
const response = await io.fetch(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
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,
|