talos-code 0.1.0 → 0.2.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +9 -4
  3. package/dist/args.js +4 -1
  4. package/dist/commands/provider-cli.js +146 -87
  5. package/dist/config/types.js +1 -1
  6. package/dist/i18n/en/approval.js +3 -0
  7. package/dist/i18n/en/credentials.js +10 -0
  8. package/dist/i18n/en/errors.js +21 -0
  9. package/dist/i18n/en/firstrun.js +12 -0
  10. package/dist/i18n/en/screen.js +38 -0
  11. package/dist/i18n/error-view.js +1 -1
  12. package/dist/main.js +2 -1
  13. package/dist/provider/openrouter-login.js +172 -0
  14. package/dist/runtime/context-archive.js +69 -0
  15. package/dist/runtime/repo.js +11 -0
  16. package/dist/runtime/supervisor.js +5 -2
  17. package/dist/runtime/talos-composition.js +9 -3
  18. package/dist/tui/agent-view.js +51 -0
  19. package/dist/tui/app.js +364 -27
  20. package/dist/tui/catalog-service.js +2 -1
  21. package/dist/tui/components/status-indicator.js +10 -4
  22. package/dist/tui/components/terminal-shell.js +10 -1
  23. package/dist/tui/descendant-approvals.js +104 -0
  24. package/dist/tui/launch-state.js +9 -0
  25. package/dist/tui/live-activity.js +13 -2
  26. package/dist/tui/overlays/agent-tree.js +7 -3
  27. package/dist/tui/overlays/approval-dialog.js +3 -1
  28. package/dist/tui/overlays/model-picker.js +5 -0
  29. package/dist/tui/overlays/provider-picker.js +26 -0
  30. package/dist/tui/project-trust-prompt.js +5 -3
  31. package/dist/tui/session-controller.js +25 -12
  32. package/dist/tui/setup-wizard.js +46 -0
  33. package/dist/tui/slash-commands.js +3 -1
  34. package/dist/tui/theme-catalog.js +3 -1
  35. package/dist/version.js +1 -1
  36. package/dist/workspace/checkpoint-store.js +32 -1
  37. package/dist/workspace/checkpoint.js +40 -12
  38. package/package.json +3 -1
  39. package/vendor/harness-ui/src/kernel/talosHarness.mjs +149 -2
  40. package/vendor/manifest.json +2 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,112 @@
1
+ # TALOS CLI changelog
2
+
3
+ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Versions follow
4
+ [SemVer](https://semver.org/): in 0.y.z anything may still change. The version lives in `package.json` and
5
+ `src/version.ts`, and in the `talos-cli-vX.Y.Z` tag. The package is published on npm as
6
+ [`talos-code`](https://www.npmjs.com/package/talos-code); the command is `talos`.
7
+
8
+ ## talos-cli-v0.2.1 — 2026-09-26
9
+
10
+ `npm i -g talos-code@0.2.1`, or `talos update` to see it.
11
+
12
+ ### Changed
13
+ - Faster turns in large folders. The workspace snapshot that makes `/undo` possible no longer reads, rewrites and syncs
14
+ every file at every turn: a file that has not changed since the last turn reuses what was stored. A turn in a plain
15
+ folder of 6,000 files went from 39 s to 1.8 s, and from 2.6 s to 1.4 s in a git repository of the same size. What
16
+ `/undo` covers is unchanged.
17
+ - Faster and complete project search. `cerca` runs ripgrep, shipped inside the package for your platform (no download),
18
+ and each match now comes with its line: `path:line:text`. Four searches over 6,000 files took 9.3 s; now about 0.2 s.
19
+ Before, a search stopped after reading 5,000 files and could miss matches in a bigger project. Several searches asked
20
+ in the same answer run together. Without ripgrep, the previous search still works.
21
+
22
+ ### Known limits
23
+ - Tested on Windows 11 x64 only; Node.js 24 (24.18 or later in the 24 line).
24
+ - Shell commands still run one at a time, each in its sandbox; a start of about one second remains.
25
+
26
+ ### Verification
27
+ - Measured on a hermetic bench (`scripts/bench-harness.ts`): a local fake provider, no network, TALOS against Pi, Codex
28
+ and OpenCode on the same turns.
29
+ - CLI unit suite: 1876 tests, 1872 pass, 4 skip, 0 fail. Acceptance on the built CLI in a synthetic terminal: 263/263.
30
+ - The package proof (`scripts/prove-standalone.ts`), 16/16 on `talos-code-0.2.1.tgz` (1.76 MB); the installed package
31
+ resolves its own ripgrep.
32
+
33
+ ## talos-cli-v0.2.0 — 2026-09-26
34
+
35
+ `npm i -g talos-code@0.2.0`, or `talos update` to see it from 0.1.0.
36
+
37
+ ### Added
38
+ - Watch a sub-agent live: Alt+A, then Enter on an agent, and its own transcript takes the main view as it streams,
39
+ read-only. Esc goes back to your session without stopping the turn; an agent that finishes stays on screen as done.
40
+ While agents work, the status line says "Alt+A to watch".
41
+ - A guided setup on the first launch: trust this folder, then a provider (a local engine that runs comes
42
+ first), its key or local engine, and a model, one window at a time with "Step N of 4"; Esc skips, and only what is
43
+ missing is asked. It ends with "Ready" and the model. `/setup` or `talos setup` runs it again.
44
+ - Sign in to OpenRouter without pasting a key: choose OpenRouter, then "Sign in with OpenRouter (browser)". TALOS
45
+ opens the browser and waits on 127.0.0.1; on another machine, paste the code OpenRouter shows. Also
46
+ `talos provider login openrouter` (`--paste` for the code). The key that comes back is tested and saved like a pasted one.
47
+
48
+ ### Changed
49
+ - Forge is the default theme. A theme you chose stays yours.
50
+
51
+ ### Fixed
52
+ - A sub-agent that needed your approval (a command, a write) used to wait forever with nothing on screen. Its
53
+ approval now opens like any other and says it comes from the agent and its task; the answer goes to that agent.
54
+ - While delegated sub-agents work, the status line says how many agents are working instead of "thinking" with a
55
+ running clock; any tool that runs silently without asking is shown as running.
56
+ - Changing the theme with `/theme` no longer throws the conversation away. Crossing the colourless `mono` theme used to
57
+ restart the whole screen and lose the session.
58
+ - A turn interrupted while a tool was about to run no longer breaks the session: resuming it used to fail at every turn
59
+ with `CTX_HISTORY_DIVERGED`. A session already broken that way now says so in plain words and offers `/fork`, which
60
+ continues the conversation in a new session.
61
+
62
+ ### Known limits
63
+ - Tested on Windows 11 x64 only; Node.js 24 (24.18 or later in the 24 line).
64
+ - Context compaction with a local Ollama model is not yet verified end to end.
65
+ - A local provider that answers 401/403 without needing a key can be set aside as if its key were wrong.
66
+ - The OpenRouter sign-in is tested against a stand-in for OpenRouter; the exchange with the real service follows its
67
+ published contract.
68
+
69
+ ### Verification
70
+ - CLI unit suite on the release tree: 1870 tests, 1866 pass, 4 skip, 0 fail. Acceptance on the built CLI in a synthetic
71
+ terminal: 263/263.
72
+ - The package proof (`scripts/prove-standalone.ts`), 16/16 on `talos-code-0.2.0.tgz` (1.76 MB, 407 files): installed
73
+ into an empty prefix outside the repository, hermetic profile, fake provider on 127.0.0.1; every loaded module
74
+ resolved inside the package; nothing written inside the package; no stale build output; no outbound request.
75
+
76
+ ## talos-cli-v0.1.0 — 2026-09-25
77
+
78
+ The first public release: `npm i -g talos-code`, then `talos`.
79
+
80
+ ### Added
81
+ - An interactive terminal screen and a headless mode (`talos -p`, `--json`, `--output-format stream-json`) on the
82
+ TALOS kernel, the same one the desktop runs, carried inside the package: it reads no file of a TALOS checkout.
83
+ - Providers from the TALOS registry (OpenAI, Anthropic, Google Gemini, DeepSeek, OpenRouter, Mistral, Groq, xAI and
84
+ more) and local engines with no key (Ollama, LM Studio, llama.cpp). Keys are typed in hidden input and kept in the
85
+ operating system's credential store; a key already in the environment is used only after you agree once.
86
+ - Per-tool permissions (allow, ask, deny) with six modes, project trust for hooks, MCP servers, plugins and commands.
87
+ - `/undo` restores the files a turn changed, from checkpoints kept outside the repository and outside Git.
88
+ - Sessions: `/resume`, `/fork`, export and import (an imported session never runs anything by itself), prompt history.
89
+ - Context: with a known window of 64k tokens or more, compaction at half of it, said on screen; after a summary the
90
+ model did not write properly, automatic compaction pauses (60 s, 5 min, 15 min) and says so, while `/compact` runs
91
+ at once.
92
+ - `/theme` with ten accents, `/help` by group, `/status`, `/retry`, `/copy`, `@` file completion that respects
93
+ `.gitignore`, `Ctrl+R` history search.
94
+ - `talos update` asks the npm registry whether a newer version exists and prints the npm command; it changes nothing.
95
+ - `talos doctor` and a redacted `talos diagnostic bundle`. No telemetry.
96
+
97
+ ### Known limits
98
+ - Tested on Windows 11 x64 only; Node.js 24 (24.18 or later in the 24 line).
99
+ - Context compaction with a local Ollama model is not yet verified end to end.
100
+ - A local provider that answers 401/403 without needing a key can be set aside as if its key were wrong.
101
+ - Reported by the owner after the release, to fix in 0.1.x: changing the theme can crash the process; resuming a
102
+ session can end with `CTX_HISTORY_DIVERGED`; sub-agents cannot yet be opened to watch their conversation live, and
103
+ the thinking counter keeps running while they work.
104
+
105
+ ### Verification
106
+ - CLI unit suite on the released tree: 1839 tests, 1835 pass, 4 skip, 0 fail. Acceptance on the built CLI in a
107
+ synthetic terminal: 246/247 on the aligned kernel (the one red is a known idle-frame flake).
108
+ - The package proof (`scripts/prove-standalone.ts`), 16/16 on the tarball itself: installed into an empty prefix
109
+ outside the repository, hermetic profile, fake provider on 127.0.0.1; every loaded module resolved inside the
110
+ package; nothing written inside the package; no stale build output.
111
+ - Installed from the registry after publication: `talos --version` answered `0.1.0`, `talos update --check` answered
112
+ up to date.
package/README.md CHANGED
@@ -26,9 +26,11 @@ Open a terminal in a project and run:
26
26
  talos
27
27
  ```
28
28
 
29
- The first time, TALOS asks whether to trust the folder, then which provider and model to use. A key already in your
30
- environment (for example `OPENAI_API_KEY`) is used only after you agree once; otherwise you type it in hidden input and it
31
- is kept in the operating system's credential store, never in a file.
29
+ The first time, a short guided setup asks only what is missing: whether to trust the folder, then a provider (a local
30
+ engine that is running comes first), its key, and a model, one step at a time; Esc skips a step. `/setup` or
31
+ `talos setup` runs it again. A key already in your environment (for example `OPENAI_API_KEY`) is used only after you
32
+ agree once; otherwise you type it in hidden input and it is kept in the operating system's credential store, never in a
33
+ file. For OpenRouter you can also sign in in the browser instead of pasting a key (`talos provider login openrouter`).
32
34
 
33
35
  For a one-shot task, without the interactive screen:
34
36
 
@@ -52,6 +54,7 @@ Vertex AI. Local engines: Ollama, LM Studio and llama.cpp, with no key. TALOS ne
52
54
  one on its own.
53
55
 
54
56
  `/provider` sets up a provider, `/model` (or `Alt+P`) picks the model, and `talos doctor` says what is ready.
57
+ While sub-agents work, `Alt+A` lists them and Enter shows one agent's conversation live, read-only.
55
58
 
56
59
  ## What you can rely on
57
60
 
@@ -94,7 +97,7 @@ talos update --check
94
97
  With an npm install, `talos update` tells you if a newer version exists and the command to run
95
98
  (`npm i -g talos-code@latest`). It does not change anything by itself.
96
99
 
97
- ## Known limits in 0.1.0
100
+ ## Known limits in 0.2.0
98
101
 
99
102
  - Only Windows 11 x64 is tested.
100
103
  - Context compaction with a local Ollama model is not yet verified end to end.
@@ -106,6 +109,8 @@ With an npm install, `talos update` tells you if a newer version exists and the
106
109
  missing when something does not work. `talos diagnostic bundle --out talos-diagnostic.zip` makes a redacted report for a
107
110
  bug; it contains no prompt or file content.
108
111
 
112
+ What changed in each version is in `CHANGELOG.md`.
113
+
109
114
  ## License
110
115
 
111
116
  AGPL-3.0-only. See `LICENSE` and `THIRD_PARTY_NOTICES.md` in this package.
package/dist/args.js CHANGED
@@ -82,12 +82,15 @@ export function parseCliArgs(argv) {
82
82
  positionals: argv.slice(subcommandIndex),
83
83
  };
84
84
  }
85
- const promptParts = values.print !== undefined ? [values.print] : positionals;
85
+ /* P17 (owner decision 4): `talos setup` alone is the guided setup, not a prompt that says "setup". */
86
+ const setup = values.print === undefined && outputFormat === 'text' && positionals.length === 1 && positionals[0] === 'setup';
87
+ const promptParts = values.print !== undefined ? [values.print] : setup ? [] : positionals;
86
88
  const prompt = promptParts.length ? promptParts.join(' ') : undefined;
87
89
  const interactive = values.print === undefined && outputFormat === 'text';
88
90
  const timeoutSeconds = positiveInt(values.timeout, '--timeout');
89
91
  return {
90
92
  command: 'run', interactive, outputFormat, protocolVersion,
93
+ ...(setup ? { setup: true } : {}),
91
94
  ...(prompt !== undefined ? { prompt } : {}), ...(rawMode !== undefined ? { permissionMode: rawMode } : {}),
92
95
  ...(values.project !== undefined ? { project: values.project } : {}), ...(values.model !== undefined ? { model: values.model } : {}),
93
96
  ...(values.resume !== undefined ? { resume: values.resume } : {}), ...(values.fork !== undefined ? { fork: values.fork } : {}),
@@ -11,101 +11,160 @@ import { t } from "../i18n/index.js";
11
11
  */
12
12
  async function runtimeContext(ctx) { return ctx.createRuntimeContext({ projectRoot: ctx.projectRoot, paths: ctx.paths, repoRoot: ctx.repoRoot, model: ctx.invocation.model ?? 'openai:gpt-5-mini', environmentKeys: 'consent' }); }
13
13
  function environmentKeyOf(row) { return row && row.environmentVariable ? { variable: String(row.environmentVariable), state: row.keySource === 'environment' ? 'approved' : row.keySource === 'environment-declined' ? 'declined' : 'unanswered' } : null; }
14
- export async function runProviderCommand(ctx, args0) { const args = [...args0]; const op = args.shift() ?? 'list'; const rc = await runtimeContext(ctx); const store = rc.providerStore; const catalog = rc.tuiCatalog; if (op === 'list') {
15
- assertNoUnknownOptions(args);
16
- if (typeof rc.providerControlPlane?.list === 'function') {
17
- writeValue(ctx, rc.providerControlPlane.list());
14
+ export async function runProviderCommand(ctx, args0) {
15
+ const args = [...args0];
16
+ const op = args.shift() ?? 'list';
17
+ const rc = await runtimeContext(ctx);
18
+ const store = rc.providerStore;
19
+ const catalog = rc.tuiCatalog;
20
+ if (op === 'list') {
21
+ assertNoUnknownOptions(args);
22
+ if (typeof rc.providerControlPlane?.list === 'function') {
23
+ writeValue(ctx, rc.providerControlPlane.list());
24
+ return 0;
25
+ }
26
+ const rows = store.listPublic();
27
+ let providers = [];
28
+ try {
29
+ providers = await catalog?.listProviders?.() ?? [];
30
+ }
31
+ catch {
32
+ providers = [];
33
+ }
34
+ writeValue(ctx, rows.map((row) => ({ ...row, environmentKey: environmentKeyOf(providers.find((p) => p.id === row?.id)) })));
18
35
  return 0;
19
36
  }
20
- const rows = store.listPublic();
21
- let providers = [];
22
- try {
23
- providers = await catalog?.listProviders?.() ?? [];
37
+ const id = args.shift();
38
+ if (!id)
39
+ throw new Error('PROVIDER_REQUIRED');
40
+ /* P21 (owner decisions 2026-09-26): `talos provider login openrouter` signs in in the browser (the loopback waits for it);
41
+ `--paste`, or a remote session, uses the code OpenRouter shows. Interactive only (clig.dev: never require a prompt in a
42
+ script). The key that comes back is tested and saved like `set-key`'s; it is never printed. */
43
+ if (op === 'login') {
44
+ const paste = takeFlag(args, '--paste');
45
+ assertNoUnknownOptions(args);
46
+ if (id !== 'openrouter')
47
+ throw Object.assign(new Error('PROVIDER_LOGIN_UNSUPPORTED'), { code: 'PROVIDER_LOGIN_UNSUPPORTED' });
48
+ if (typeof catalog?.startOpenRouterLogin !== 'function' || typeof catalog?.setProviderKey !== 'function')
49
+ throw Object.assign(new Error('PROVIDER_LOGIN_UNAVAILABLE'), { code: 'PROVIDER_LOGIN_UNAVAILABLE' });
50
+ if (!ctx.io.stdinIsTTY || !ctx.io.stdoutIsTTY)
51
+ throw Object.assign(new Error('LOGIN_REQUIRES_TTY'), { code: 'LOGIN_REQUIRES_TTY' });
52
+ const login = await catalog.startOpenRouterLogin(paste ? { paste: true } : {});
53
+ if (login.mode === 'paste')
54
+ ctx.io.writeErr(`${t('key.login.paste')}\n${login.url}\n`);
55
+ else
56
+ ctx.io.writeErr(`${t(login.opened ? 'key.login.opened' : 'key.login.open')}\n${login.url}\n${t('key.login.waiting')}\n`);
57
+ let key = '';
58
+ try {
59
+ if (login.mode === 'paste') {
60
+ if (!ctx.io.readSecret)
61
+ throw Object.assign(new Error('SECRET_INPUT_REQUIRES_TTY'), { code: 'SECRET_INPUT_REQUIRES_TTY' });
62
+ key = await login.submit(await ctx.io.readSecret(t('key.login.codePrompt')));
63
+ }
64
+ else
65
+ key = await login.key;
66
+ }
67
+ catch (error) {
68
+ login.cancel();
69
+ const code = String(error?.code ?? 'OAUTH_FAILED');
70
+ writeValue(ctx, { ok: false, provider: id, code, ...(error?.status ? { httpStatus: error.status } : {}) }, `${t('key.login.failed', { code })}\n`);
71
+ return 11;
72
+ }
73
+ let result;
74
+ try {
75
+ result = await catalog.setProviderKey(id, key);
76
+ }
77
+ finally {
78
+ key = '';
79
+ }
80
+ const saved = result.saved === true;
81
+ writeValue(ctx, { ok: saved, provider: id, outcome: result.outcome, httpStatus: result.httpStatus, saved }, `${t('key.login.signedIn', { message: result.message, saved: t(saved ? 'key.login.saved' : 'key.login.notSaved') })}\n`);
82
+ return saved ? 0 : 11;
24
83
  }
25
- catch {
26
- providers = [];
84
+ if (op === 'set-key') {
85
+ assertNoUnknownOptions(args);
86
+ if (!ctx.io.readSecret)
87
+ throw new Error('SECRET_INPUT_REQUIRES_TTY');
88
+ if (typeof catalog?.setProviderKey !== 'function')
89
+ throw Object.assign(new Error('PROVIDER_KEY_TEST_UNAVAILABLE'), { code: 'PROVIDER_KEY_TEST_UNAVAILABLE' }); /* a one-token key test is stated before the key is read, so before anything is sent; nothing further is asked */
90
+ const notice = typeof catalog.keyTestNotice === 'function' ? catalog.keyTestNotice(id) : null;
91
+ if (notice)
92
+ ctx.io.writeErr(`${notice}\n`);
93
+ let secret = await ctx.io.readSecret(`Key for ${id}: `);
94
+ let result;
95
+ try {
96
+ result = await catalog.setProviderKey(id, secret);
97
+ }
98
+ finally {
99
+ secret = '';
100
+ }
101
+ const saved = result.saved === true;
102
+ writeValue(ctx, { ok: saved, provider: id, outcome: result.outcome, httpStatus: result.httpStatus, saved }, `${result.message} ${saved ? 'Saved.' : 'The key was not saved.'}\n`);
103
+ return saved ? 0 : 11;
27
104
  }
28
- writeValue(ctx, rows.map((row) => ({ ...row, environmentKey: environmentKeyOf(providers.find((p) => p.id === row?.id)) })));
29
- return 0;
30
- } const id = args.shift(); if (!id)
31
- throw new Error('PROVIDER_REQUIRED'); if (op === 'set-key') {
32
- assertNoUnknownOptions(args);
33
- if (!ctx.io.readSecret)
34
- throw new Error('SECRET_INPUT_REQUIRES_TTY');
35
- if (typeof catalog?.setProviderKey !== 'function')
36
- throw Object.assign(new Error('PROVIDER_KEY_TEST_UNAVAILABLE'), { code: 'PROVIDER_KEY_TEST_UNAVAILABLE' }); /* a one-token key test is stated before the key is read, so before anything is sent; nothing further is asked */
37
- const notice = typeof catalog.keyTestNotice === 'function' ? catalog.keyTestNotice(id) : null;
38
- if (notice)
39
- ctx.io.writeErr(`${notice}\n`);
40
- let secret = await ctx.io.readSecret(`Key for ${id}: `);
41
- let result;
42
- try {
43
- result = await catalog.setProviderKey(id, secret);
105
+ if (op === 'env-key') {
106
+ const use = takeFlag(args, '--use');
107
+ const decline = takeFlag(args, '--decline');
108
+ assertNoUnknownOptions(args);
109
+ if (use && decline)
110
+ throw new Error('Use either --use or --decline');
111
+ const row = (await catalog.listProviders()).find((p) => p.id === id);
112
+ if (!row)
113
+ throw Object.assign(new Error('PROVIDER_INVALID'), { code: 'PROVIDER_INVALID' });
114
+ const answering = use || decline;
115
+ if (answering && row.environmentVariable)
116
+ await catalog.answerEnvironmentKey(id, use ? 'yes' : 'no');
117
+ const after = answering ? (await catalog.listProviders()).find((p) => p.id === id) : row;
118
+ const state = environmentKeyOf(after);
119
+ if (!state) {
120
+ writeValue(ctx, { ok: !answering, provider: id, environmentKey: null }, `No key for ${after?.label ?? id} was found in the environment.\n`);
121
+ return answering ? 2 : 0;
122
+ }
123
+ writeValue(ctx, { ok: true, provider: id, environmentKey: state }, `${state.variable}: ${state.state === 'approved' ? 'used for ' + after.label : state.state === 'declined' ? 'declined, not used' : 'not used until approved (talos provider env-key ' + id + ' --use)'}\n`);
124
+ return 0;
44
125
  }
45
- finally {
46
- secret = '';
126
+ if (op === 'remove-key') {
127
+ assertNoUnknownOptions(args);
128
+ store.clearKey(id);
129
+ rc.providerControlPlane?.invalidate?.(id);
130
+ writeValue(ctx, { ok: true, provider: id }, `Removed key for ${id}\n`);
131
+ return 0;
47
132
  }
48
- const saved = result.saved === true;
49
- writeValue(ctx, { ok: saved, provider: id, outcome: result.outcome, httpStatus: result.httpStatus, saved }, `${result.message} ${saved ? 'Saved.' : 'The key was not saved.'}\n`);
50
- return saved ? 0 : 11;
51
- } if (op === 'env-key') {
52
- const use = takeFlag(args, '--use');
53
- const decline = takeFlag(args, '--decline');
54
- assertNoUnknownOptions(args);
55
- if (use && decline)
56
- throw new Error('Use either --use or --decline');
57
- const row = (await catalog.listProviders()).find((p) => p.id === id);
58
- if (!row)
59
- throw Object.assign(new Error('PROVIDER_INVALID'), { code: 'PROVIDER_INVALID' });
60
- const answering = use || decline;
61
- if (answering && row.environmentVariable)
62
- await catalog.answerEnvironmentKey(id, use ? 'yes' : 'no');
63
- const after = answering ? (await catalog.listProviders()).find((p) => p.id === id) : row;
64
- const state = environmentKeyOf(after);
65
- if (!state) {
66
- writeValue(ctx, { ok: !answering, provider: id, environmentKey: null }, `No key for ${after?.label ?? id} was found in the environment.\n`);
67
- return answering ? 2 : 0;
133
+ if (op === 'configure') {
134
+ const endpoint = takeOption(args, '--endpoint');
135
+ const timeoutRaw = takeOption(args, '--timeout');
136
+ assertNoUnknownOptions(args);
137
+ const current = store.getRuntime(id) ?? {};
138
+ const timeoutSeconds = timeoutRaw === undefined ? current.timeoutSeconds : Number(timeoutRaw);
139
+ if (timeoutRaw !== undefined && (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 5 || timeoutSeconds > 300))
140
+ throw new Error('PROVIDER_TIMEOUT_INVALID');
141
+ if (endpoint === undefined && timeoutRaw === undefined)
142
+ throw new Error('PROVIDER_CONFIG_REQUIRED');
143
+ store.setRuntime(id, { ...current, ...(endpoint !== undefined ? { endpoint } : {}), ...(timeoutSeconds !== undefined ? { timeoutSeconds } : {}) });
144
+ rc.providerControlPlane?.invalidate?.(id);
145
+ writeValue(ctx, { ok: true, provider: id, runtime: store.getRuntime(id) }, `Configured ${id}\n`);
146
+ return 0;
68
147
  }
69
- writeValue(ctx, { ok: true, provider: id, environmentKey: state }, `${state.variable}: ${state.state === 'approved' ? 'used for ' + after.label : state.state === 'declined' ? 'declined, not used' : 'not used until approved (talos provider env-key ' + id + ' --use)'}\n`);
70
- return 0;
71
- } if (op === 'remove-key') {
72
- assertNoUnknownOptions(args);
73
- store.clearKey(id);
74
- rc.providerControlPlane?.invalidate?.(id);
75
- writeValue(ctx, { ok: true, provider: id }, `Removed key for ${id}\n`);
76
- return 0;
77
- } if (op === 'configure') {
78
- const endpoint = takeOption(args, '--endpoint');
79
- const timeoutRaw = takeOption(args, '--timeout');
80
- assertNoUnknownOptions(args);
81
- const current = store.getRuntime(id) ?? {};
82
- const timeoutSeconds = timeoutRaw === undefined ? current.timeoutSeconds : Number(timeoutRaw);
83
- if (timeoutRaw !== undefined && (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 5 || timeoutSeconds > 300))
84
- throw new Error('PROVIDER_TIMEOUT_INVALID');
85
- if (endpoint === undefined && timeoutRaw === undefined)
86
- throw new Error('PROVIDER_CONFIG_REQUIRED');
87
- store.setRuntime(id, { ...current, ...(endpoint !== undefined ? { endpoint } : {}), ...(timeoutSeconds !== undefined ? { timeoutSeconds } : {}) });
88
- rc.providerControlPlane?.invalidate?.(id);
89
- writeValue(ctx, { ok: true, provider: id, runtime: store.getRuntime(id) }, `Configured ${id}\n`);
90
- return 0;
91
- } if (op === 'test') {
92
- assertNoUnknownOptions(args); /* M5-A/M5-C: ordinary health testing never opts into token generation, and remediation is the control-plane decision. */
93
- if (typeof rc.providerControlPlane?.probe === 'function') {
94
- const profile = await rc.providerControlPlane.probe(id);
95
- const health = profile.lastHealth ?? {};
96
- const latency = Number.isFinite(health.latencyMs) ? ` · ${health.latencyMs} ms` : ''; /* R2 (E8 / Q-R2-7): a missing key is the one sentence, as a line of its own; the probe's free-text reason (the kernel's, Italian) is mapped by the remediation code and not printed. */
97
- const missingKey = profile.remediation?.code === 'add-key';
98
- const remedy = missingKey ? `${profile.remediation.message}\n` : profile.remediation?.code && profile.remediation.code !== 'none' ? `Remedy: ${profile.remediation.message}\n` : ''; /* R4 Q-R4-16 (a): the kernel's sentence in English by a pinned template, or not printed. */
99
- const mapped = health.detail && !missingKey ? kernelProbeDetail(String(health.detail), { label: String(profile.label ?? id) }) : null;
100
- const detail = mapped ? `${mapped}\n` : '';
101
- writeValue(ctx, missingKey ? { ...profile, code: 'PROVIDER_KEY_MISSING' } : profile, `${profile.label ?? id}: ${health.state ?? 'unknown'}${latency}\n${detail}${remedy}`);
102
- return health.state === 'available' ? 0 : 11;
148
+ if (op === 'test') {
149
+ assertNoUnknownOptions(args); /* M5-A/M5-C: ordinary health testing never opts into token generation, and remediation is the control-plane decision. */
150
+ if (typeof rc.providerControlPlane?.probe === 'function') {
151
+ const profile = await rc.providerControlPlane.probe(id);
152
+ const health = profile.lastHealth ?? {};
153
+ const latency = Number.isFinite(health.latencyMs) ? ` · ${health.latencyMs} ms` : ''; /* R2 (E8 / Q-R2-7): a missing key is the one sentence, as a line of its own; the probe's free-text reason (the kernel's, Italian) is mapped by the remediation code and not printed. */
154
+ const missingKey = profile.remediation?.code === 'add-key';
155
+ const remedy = missingKey ? `${profile.remediation.message}\n` : profile.remediation?.code && profile.remediation.code !== 'none' ? `Remedy: ${profile.remediation.message}\n` : ''; /* R4 Q-R4-16 (a): the kernel's sentence in English by a pinned template, or not printed. */
156
+ const mapped = health.detail && !missingKey ? kernelProbeDetail(String(health.detail), { label: String(profile.label ?? id) }) : null;
157
+ const detail = mapped ? `${mapped}\n` : '';
158
+ writeValue(ctx, missingKey ? { ...profile, code: 'PROVIDER_KEY_MISSING' } : profile, `${profile.label ?? id}: ${health.state ?? 'unknown'}${latency}\n${detail}${remedy}`);
159
+ return health.state === 'available' ? 0 : 11;
160
+ }
161
+ const probe = typeof rc.providerProbe === 'function' ? rc.providerProbe() : (await importTalosModule(ctx.repoRoot, 'provider-probe.mjs')).createProviderProbe({ leggiChiave: (p) => store.getKey(p), leggiRuntime: (p) => store.getRuntime(p) });
162
+ const result = await probe.prova(id);
163
+ writeValue(ctx, result);
164
+ return result?.esito === 'collegato' ? 0 : 11;
103
165
  }
104
- const probe = typeof rc.providerProbe === 'function' ? rc.providerProbe() : (await importTalosModule(ctx.repoRoot, 'provider-probe.mjs')).createProviderProbe({ leggiChiave: (p) => store.getKey(p), leggiRuntime: (p) => store.getRuntime(p) });
105
- const result = await probe.prova(id);
106
- writeValue(ctx, result);
107
- return result?.esito === 'collegato' ? 0 : 11;
108
- } throw new Error(`Unsupported provider operation: ${op}`); }
166
+ throw new Error(`Unsupported provider operation: ${op}`);
167
+ }
109
168
  /*
110
169
  * R2 (gate E14, E-03): `talos model list` for a person — one row per model under English columns, never the raw JSON array; the
111
170
  * MODEL column is exactly what `talos model use` takes (OpenCode `opencode models` prints one id per line and JSON only with
@@ -8,4 +8,4 @@ export const UI_REASONING_MODES = Object.freeze(['collapsed', 'visible']);
8
8
  export const MODEL_WINDOW_MIN = 1_024;
9
9
  export const MODEL_WINDOW_MAX = 10_000_000;
10
10
  export const REASONING_EFFORTS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
11
- export const DEFAULT_CONFIG = { version: 2, permissionMode: 'default', outputFormat: 'text', updateChannel: 'stable', permissions: { allow: [], ask: [], deny: [] }, ui: { theme: 'calm', markdown: true, exitOutput: 'transcript', busyInput: 'queue', reasoning: 'collapsed' }, extensions: { enabled: [] } };
11
+ export const DEFAULT_CONFIG = { version: 2, permissionMode: 'default', outputFormat: 'text', updateChannel: 'stable', permissions: { allow: [], ask: [], deny: [] }, ui: { theme: 'forge', markdown: true, exitOutput: 'transcript', busyInput: 'queue', reasoning: 'collapsed' }, extensions: { enabled: [] } };
@@ -13,6 +13,9 @@ export const approval = {
13
13
  'approval.question.TalosService': 'Allow this?',
14
14
  /* Q-R3-13 (R0 decision 4): the title of a command approval on a host with no verified sandbox. */
15
15
  'approval.unsandboxed': '(unsandboxed)',
16
+ /* P13 (owner decision 8): who asks, when a sub-agent does. */
17
+ 'approval.origin.agent': '· from agent · {task}',
18
+ 'approval.origin.agentUnnamed': '· from an agent',
16
19
  /* Q-R3-6: four choices, Deny highlighted; "deny always" only in the full review (or /permissions). */
17
20
  'approval.choice.once': 'Allow once',
18
21
  'approval.choice.session': 'Allow for this session',
@@ -79,4 +79,14 @@ export const credentials = {
79
79
  'key.models.source.unknown': 'not verified',
80
80
  'key.models.none': 'No models are known for this provider.',
81
81
  'key.models.use': 'Use one with: talos model use <MODEL>',
82
+ /* P21: `talos provider login openrouter`. */
83
+ 'key.login.paste': 'Open this address in a browser on any machine, sign in to OpenRouter, then paste the code it shows:',
84
+ 'key.login.opened': 'Opening OpenRouter in your browser. If it did not open, use this address:',
85
+ 'key.login.open': 'Open this address in your browser and sign in to OpenRouter:',
86
+ 'key.login.waiting': 'Waiting for the browser (10 min). Ctrl+C cancels; on another machine, run it again with --paste.',
87
+ 'key.login.codePrompt': 'Code: ',
88
+ 'key.login.failed': 'OpenRouter sign-in did not complete ({code}).',
89
+ 'key.login.signedIn': 'Signed in to OpenRouter. {message} {saved}',
90
+ 'key.login.saved': 'Saved.',
91
+ 'key.login.notSaved': 'The key was not saved.',
82
92
  };
@@ -240,6 +240,27 @@ export const errors = {
240
240
  'error.code.CTX_COMPACTION_FAILED.todo': 'Try /compact again; if it keeps happening, start a new conversation with /clear.',
241
241
  'error.code.CTX_JOB_INTERRUPTED.what': 'A compaction was interrupted because the TALOS process running it stopped; nothing was changed.',
242
242
  'error.code.CTX_JOB_INTERRUPTED.todo': 'Nothing to fix: the next request or /compact starts it again.',
243
+ /* P12: a session whose context archive no longer matches its history (before 0.2.0 an interrupted tool turn could
244
+ cause it). Nothing is lost; the way on is a new session that carries the conversation. */
245
+ /* P21: OpenRouter sign-in. */
246
+ 'error.code.LOGIN_REQUIRES_TTY.what': 'Signing in needs an interactive terminal.',
247
+ 'error.code.LOGIN_REQUIRES_TTY.todo': 'Run talos provider login openrouter in a terminal, or save a key with talos provider set-key openrouter.',
248
+ 'error.code.PROVIDER_LOGIN_UNSUPPORTED.what': 'Only OpenRouter offers a sign-in; other providers use a key.',
249
+ 'error.code.PROVIDER_LOGIN_UNSUPPORTED.todo': 'Save a key with talos provider set-key <provider>.',
250
+ 'error.code.PROVIDER_LOGIN_UNAVAILABLE.what': 'The OpenRouter sign-in is not available in this installation.',
251
+ 'error.code.PROVIDER_LOGIN_UNAVAILABLE.todo': 'Save a key with talos provider set-key openrouter.',
252
+ 'error.code.OAUTH_CALLBACK_UNAVAILABLE.what': 'TALOS could not listen for OpenRouter on this machine.',
253
+ 'error.code.OAUTH_CALLBACK_UNAVAILABLE.todo': 'Sign in with the code instead: talos provider login openrouter --paste.',
254
+ 'error.code.OAUTH_CANCELLED.what': 'The sign-in was cancelled.',
255
+ 'error.code.OAUTH_CANCELLED.todo': 'Start it again when you are ready.',
256
+ 'error.code.OAUTH_CODE_MISSING.what': 'That is not a code from OpenRouter.',
257
+ 'error.code.OAUTH_CODE_MISSING.todo': 'Paste the code OpenRouter shows, or the whole address the browser landed on.',
258
+ 'error.code.OAUTH_DENIED.what': 'The sign-in was cancelled in the browser.',
259
+ 'error.code.OAUTH_DENIED.todo': 'Start it again and allow access on the OpenRouter page.',
260
+ 'error.code.OAUTH_TIMEOUT.what': 'The sign-in took longer than 10 minutes.',
261
+ 'error.code.OAUTH_TIMEOUT.todo': 'Start it again; the code OpenRouter gives lasts 10 minutes.',
262
+ 'error.code.CTX_HISTORY_DIVERGED.what': 'This conversation no longer matches its saved context, so TALOS stopped before sending anything. Nothing was lost.',
263
+ 'error.code.CTX_HISTORY_DIVERGED.todo': 'Continue it in a new session with /fork (it keeps the whole conversation), or start fresh with /clear.',
243
264
  'error.where.context': '/context shows the window TALOS measured and how compaction is set.',
244
265
  'error.code.MODELS_DEV_TOO_LARGE.what': 'The models.dev catalogue answered with more data than TALOS accepts, so it was not used.',
245
266
  'error.code.MODELS_DEV_TOO_LARGE.todo': 'Nothing to do: model windows come from the other sources until the next try.',
@@ -44,6 +44,18 @@ export const firstrun = {
44
44
  'firstrun.setup.model': 'Choose the model TALOS should use for this project.',
45
45
  'firstrun.setup.key': 'The chosen provider has no usable key; add one.',
46
46
  'firstrun.setup.loaded': 'Loaded {files}',
47
+ /* P17 (owner decisions 2026-09-26): the guided setup of the first launch, and /setup. */
48
+ 'firstrun.setup.stepLine': 'Setup · Step {index} of {total} · {name} · Esc skips this step',
49
+ 'firstrun.setup.stepLineOnly': 'Setup · Step {index} of {total} · {name}',
50
+ 'firstrun.setup.stepName.trust': 'Trust',
51
+ 'firstrun.setup.stepName.provider': 'Provider',
52
+ 'firstrun.setup.stepName.key': 'Key or local engine',
53
+ 'firstrun.setup.stepName.model': 'Model',
54
+ 'firstrun.setup.trustHeading': 'TALOS · Setup · Step 1 of 4 · {title}',
55
+ 'firstrun.setup.retryHint': 'R retries · Esc back to the providers',
56
+ 'firstrun.setup.skipped': 'Setup skipped. /setup starts it again; /provider and /model change one thing.',
57
+ 'firstrun.setup.ready': '✓ Ready · {model} · type your first message',
58
+ 'firstrun.setup.readyHints': '/help commands · Shift+Tab permissions · /setup to change this',
47
59
  /* Q-R4-11: the provider list marks the chosen provider and says whether a local engine is running. */
48
60
  'firstrun.providers.chosen': 'chosen',
49
61
  'firstrun.providers.running': 'running',
@@ -266,6 +266,11 @@ export const screen = {
266
266
  /* R3 Q-R3-7 (gate E7): the status words while a turn runs and after it. */
267
267
  'screen.busy.tool': 'running',
268
268
  'screen.busy.toolNamed': 'running {tool}',
269
+ /* P14: the parent waits on delegated sub-agents. */
270
+ 'screen.busy.agents': 'agents working',
271
+ 'screen.busy.agentsCount.one': '{count} agent working',
272
+ 'screen.busy.agentsCount.other': '{count} agents working',
273
+ 'screen.busy.agentsWatch': 'Alt+A to watch',
269
274
  'screen.busy.writing': 'writing',
270
275
  'screen.busy.waiting': 'waiting for you',
271
276
  'screen.busy.done': 'done',
@@ -304,6 +309,28 @@ export const screen = {
304
309
  'screen.help.close': 'Esc / Enter / ? closes help',
305
310
  'screen.slash.help': 'Show commands',
306
311
  'screen.slash.provider': 'Choose the AI provider',
312
+ /* P21 (owner decisions 2026-09-26): OpenRouter sign-in. */
313
+ 'screen.provider.loginBrowser': 'Sign in with {provider} (browser)',
314
+ 'screen.provider.loginPasteKey': 'Paste an API key',
315
+ 'screen.provider.loginChoiceKeys': '↑↓ choose · Enter confirms · Esc cancels',
316
+ 'screen.provider.oauthOpened': 'Opening {provider} in your browser. If it did not open, use this address:',
317
+ 'screen.provider.oauthNotOpened': 'Open this address in your browser and sign in to {provider}:',
318
+ 'screen.provider.oauthRemote': 'Remote session: open this address in a browser on any machine, sign in to {provider}, then paste the code it shows.',
319
+ 'screen.provider.oauthPaste': 'Code: {masked}',
320
+ 'screen.provider.oauthWaiting': 'Waiting for the browser (10 min) · C paste a code instead · Esc cancels',
321
+ 'screen.provider.oauthPasteKeys': 'Paste the code (or the whole address) · Enter signs in · Esc cancels',
322
+ 'screen.provider.oauthExchanging': 'Signing in…',
323
+ 'screen.provider.oauthFailedKeys': 'Enter starts it again · Esc cancels',
324
+ 'screen.provider.oauthAddress': 'Sign in to {provider} at this address (it is also in the window below): {url}',
325
+ 'screen.provider.oauthSaved': '✓ Signed in to {provider} · key saved',
326
+ 'screen.provider.oauthError.OAUTH_TIMEOUT': 'The sign-in took longer than 10 minutes.',
327
+ 'screen.provider.oauthError.OAUTH_SCAMBIO_RIFIUTATO': '{provider} refused this sign-in (HTTP {status}): the code may be expired or already used.',
328
+ 'screen.provider.oauthError.OAUTH_RETE': '{provider} could not be reached.',
329
+ 'screen.provider.oauthError.OAUTH_RISPOSTA_INATTESA': '{provider} answered without a key.',
330
+ 'screen.provider.oauthError.OAUTH_DENIED': 'The sign-in was cancelled in the browser.',
331
+ 'screen.provider.oauthError.OAUTH_CODE_MISSING': 'That is not a code from {provider}; paste the code, or the whole address the browser landed on.',
332
+ 'screen.provider.oauthError.other': 'The sign-in did not complete.',
333
+ 'screen.slash.setup': 'Set up provider, key and model, step by step',
307
334
  'screen.slash.model': 'Choose a model from the chosen provider',
308
335
  'screen.slash.queue': 'Show or manage queued follow-ups',
309
336
  'screen.slash.permissions': 'Show permission mode',
@@ -472,6 +499,17 @@ export const screen = {
472
499
  'screen.agents.empty': 'No native agent rows. · Esc close',
473
500
  'screen.agents.steerHint': 'Alt+S steer active session from composer · Esc close',
474
501
  'screen.agents.inspectOnly': 'inspection only · steering unavailable for selected session · Esc close',
502
+ /* P13 (owner decisions 2026-09-26): Enter watches an agent live, read-only; Esc goes back without stopping the turn. */
503
+ 'screen.agents.watchHint': 'Enter watch live · Esc close',
504
+ 'screen.agents.backHint': 'Enter back to main · Esc close',
505
+ 'screen.agents.watchingHint': 'watching now · Esc close',
506
+ 'screen.agentView.watching': 'viewing agent · {task} · Esc back to main',
507
+ 'screen.agentView.done': 'agent · {task} · done · Esc back to main',
508
+ 'screen.agentView.unnamed': 'unnamed task',
509
+ 'screen.agentView.readOnly': 'read-only while viewing an agent · Esc back to main',
510
+ 'screen.agentView.task': 'Task from main',
511
+ 'screen.agentView.agent': 'Agent',
512
+ 'screen.agentView.shortcuts': 'Esc back to main · Alt+A agents · Ctrl+C stops the turn · PageUp/PageDown transcript',
475
513
  'screen.automation.title': 'Automation Center',
476
514
  'screen.automation.empty': '(no automations)',
477
515
  'screen.automation.keys': '↑/↓ select · d dry-run/detail · u refresh · Esc/q close',
@@ -160,7 +160,7 @@ const CODE_WHERE = {
160
160
  /* R5b (R5a open 4): the Context Engine's failures point at /context. */
161
161
  CTX_CONTEXT_OVERFLOW: 'error.where.context', CTX_TRUNCATED_SUMMARY: 'error.where.context', CTX_SUMMARY_RESPONSE_INVALID: 'error.where.context', CTX_SEGMENT_TOO_LARGE: 'error.where.context',
162
162
  CTX_INVALID_SUMMARY: 'error.where.context', CTX_EMPTY_SUMMARY: 'error.where.context', CTX_NO_REDUCTION: 'error.where.context', CTX_NOTHING_TO_COMPACT: 'error.where.context',
163
- CTX_CONTEXT_TOO_SMALL: 'error.where.context', CTX_COMPACTION_FAILED: 'error.where.context', CTX_JOB_INTERRUPTED: 'error.where.context',
163
+ CTX_CONTEXT_TOO_SMALL: 'error.where.context', CTX_COMPACTION_FAILED: 'error.where.context', CTX_JOB_INTERRUPTED: 'error.where.context', CTX_HISTORY_DIVERGED: 'error.where.context',
164
164
  };
165
165
  function familyTodo(fam) { return fam === 'interrupted' ? null : `error.family.${fam}.todo`; }
166
166
  const RULE_CODES = new Set(['RULE_COMMAND_UNSUPPORTED', 'RULE_COMPOUND_COMMAND', 'RULE_UNREADABLE', 'RULE_MATCHES_NOTHING']);
package/dist/main.js CHANGED
@@ -14,6 +14,7 @@ import { createLocalAutoClassifier } from "./security/auto-classifier.js";
14
14
  import { abortIsolationChecks, cliShellIsolation } from "./security/execution-backends.js";
15
15
  import { inertTerminalText, renderSafeTalosFailure } from "./errors.js";
16
16
  import { registerActiveRun } from "./runtime/active-run.js";
17
+ import { isFirstLaunch } from "./tui/launch-state.js"; /* P17: the first launch's trust question is step 1 of the setup */
17
18
  import { initializeDevelopmentLogging, developmentLog, developmentLogError, closeDevelopmentLogging, developmentTextEvidence } from "./diagnostics/development-log.js";
18
19
  import { assessProjectTrust, assertProjectTrusted, needsTrustQuestion, nestedRepositoriesForProjectTrust, projectTrustFailure, trustProjectAssessment } from "./security/project-trust-gate.js";
19
20
  import { findTalosRepoRoot, importTalosModule } from "./runtime/repo.js";
@@ -181,7 +182,7 @@ export async function main(argv, deps = {}) {
181
182
  const assessment = await (deps.assessProjectTrust ?? assessProjectTrust)(trustInput); /* R4 Q-R4-1: every folder with no saved decision is asked, with or without project files; headless keeps its rule (E3). */
182
183
  if (needsTrustQuestion(assessment)) {
183
184
  const nestedRepositories = await nestedRepositoriesForProjectTrust(assessment);
184
- const choice = await (deps.promptProjectTrust ?? defaultPromptProjectTrust)({ assessment, nestedRepositories, color: inv.color !== false, ...await userThemeForTrust(paths, projectRoot) });
185
+ const choice = await (deps.promptProjectTrust ?? defaultPromptProjectTrust)({ assessment, nestedRepositories, color: inv.color !== false, ...await userThemeForTrust(paths, projectRoot), ...(isFirstLaunch(paths.dataRoot) ? { setupStep: true } : {}) });
185
186
  if (choice !== 'trust' && choice !== 'trust-parent')
186
187
  throw projectTrustFailure(assessment);
187
188
  const folder = folderTargetFor(choice, assessment); /* Session only with nothing to trust: nothing is written. */