@vascend/talos 0.1.1 → 0.1.3

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
@@ -1,15 +1,13 @@
1
- <p align="center">
2
- <img src="assets/branding/talos-hero.png" alt="Vascend Talos" width="880">
3
- </p>
4
-
5
1
  <h1 align="center">Vascend Talos</h1>
6
2
 
7
- <p align="center"><strong>Your models. Your tools. Your coding workflow.</strong><br>
8
- An extensible coding agent runtime for the terminal and the applications you build.</p>
3
+ <p align="center"><strong>The model is only half the agent.<br>Take control of the harness.</strong></p>
4
+
5
+ <p align="center">A coding agent built around your mission, your tools and your way of working.<br>
6
+ Epoch-based context engineering. Extensible workflows. One engine from terminal to product.</p>
9
7
 
10
8
  <p align="center">
11
- <a href="package.json"><img src="https://img.shields.io/badge/version-0.1.0-c5a55a?style=flat-square" alt="v0.1.0"></a>
12
- <a href="https://bun.sh"><img src="https://img.shields.io/badge/runtime-Bun-171717?style=flat-square&amp;logo=bun&amp;logoColor=white" alt="Bun"></a>
9
+ <a href="https://www.npmjs.com/package/@vascend/talos"><img src="https://img.shields.io/npm/v/@vascend/talos?style=flat-square&amp;color=c5a55a" alt="Latest npm release"></a>
10
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-20%2B-171717?style=flat-square&amp;logo=nodedotjs&amp;logoColor=white" alt="Node.js 20+"></a>
13
11
  <a href="src/sdk.ts"><img src="https://img.shields.io/badge/TypeScript-CLI_%2B_SDK-3178c6?style=flat-square&amp;logo=typescript&amp;logoColor=white" alt="TypeScript CLI + SDK"></a>
14
12
  <img src="https://img.shields.io/badge/Windows_%7C_macOS_%7C_Linux-555555?style=flat-square" alt="Windows, macOS, Linux">
15
13
  </p>
@@ -17,18 +15,20 @@ An extensible coding agent runtime for the terminal and the applications you bui
17
15
  <p align="center"><strong>English</strong> · <a href="readme/README.it.md">Italiano</a> · <a href="readme/README.es.md">Español</a> · <a href="readme/README.de.md">Deutsch</a></p>
18
16
 
19
17
  <p align="center">
20
- <a href="#quick-start">Quick start</a> · <a href="#why-talos">Features</a> · <a href="#cli--sdk">CLI &amp; SDK</a> · <a href="docs/README.md">Documentation</a> · <a href="https://github.com/VascendTechnologies/vascend-talos/issues">Issues</a>
18
+ <a href="#quick-start"><strong>Try Talos</strong></a> · <a href="#why-talos">Why switch?</a> · <a href="#epoch-based-context-engineering">Context Fabric</a> · <a href="#measured-context-control">Evidence</a> · <a href="#why-i-built-talos">The creator</a> · <a href="docs/README.md">Docs</a>
21
19
  </p>
22
20
 
23
21
  ---
24
22
 
25
- **Talos brings the agent loop, tools and session context into one runtime.** Work in your terminal, automate a task from a script, or embed the same engine in an application. Choose your provider, resume a conversation and extend the workflow with skills and MCP tools.
23
+ **Give your coding agent a mission. Keep the controls.** Talos combines an extensible agent runtime with **Context Fabric**: a system that tracks the current objective, gives temporary focus an expiry and lets you inspect how context is assembled.
26
24
 
27
- Talos also powers **Vascend Horizon**. The standalone CLI runs independently of the desktop application.
25
+ Choose the model. Shape the workflow. Pin what matters. Carry the same engine into your scripts and applications.
26
+
27
+ Created by **Lorenzo Daniel Danilov** at **Vascend Technologies**, an Italian AI lab. Talos also powers **Vascend Horizon** and runs independently as a standalone CLI.
28
28
 
29
29
  ## Quick start
30
30
 
31
- Install [Node.js 20 or newer](https://nodejs.org), then run:
31
+ **Put Talos to work on your next real task.** With [Node.js 20 or newer](https://nodejs.org), run:
32
32
 
33
33
  ```bash
34
34
  npm install -g @vascend/talos
@@ -57,12 +57,15 @@ Talos checks npm for new stable releases, downloads from Vascend with an officia
57
57
 
58
58
  The installers add `talos` to your user PATH. On Windows it is available in the current PowerShell window; on Bash/Zsh, open a new terminal or use the activation command printed by the installer. No administrator rights or changes to PowerShell execution policy are required.
59
59
 
60
- On first launch, follow the Vascend account sign-in flow. Use `/login` to configure provider access and `/models` to choose a model. Then give Talos a task:
60
+ On first launch, follow the guided Vascend account sign-in. Use `/login` to connect your provider and `/models` to choose a model. Try a concrete task:
61
61
 
62
62
  ```text
63
- Explain this repository, identify its entry points, and suggest where to add tests.
63
+ Find the cause of the failing tests in this repository, implement a fix,
64
+ and verify it. Preserve the existing public API.
64
65
  ```
65
66
 
67
+ Then open `/context explain` to inspect the mission, active focus and context budget. **See what your harness is doing while you work.**
68
+
66
69
  <details>
67
70
  <summary><strong>Run from source or update your installation</strong></summary>
68
71
 
@@ -95,14 +98,63 @@ Full configuration and API key examples: [CLI reference](docs/CLI-REFERENCE.it.m
95
98
 
96
99
  ## Why Talos
97
100
 
98
- | Capability | What it gives you |
101
+ **Choose Talos when the way your agent works matters as much as the model it uses.**
102
+
103
+ | What you want to improve | The Talos advantage |
104
+ | --- | --- |
105
+ | **Keep long tasks on mission** | Epochs preserve the active objective through follow-ups. A Goal Contract records corrections, constraints and success criteria. |
106
+ | **Decide what stays in focus** | Expiring leases remove old focus terms; explicit pins keep the terms you choose. Inspect the current decisions with `/context explain`. |
107
+ | **Make the agent fit your process** | Combine project instructions, skills, MCP tools, hooks and extensions. Select a working profile for debugging, research, security and more. |
108
+ | **Use the model that fits the task** | Choose Anthropic, OpenAI-compatible providers or local Ollama models from the same runtime. |
109
+ | **Turn your workflow into a product** | Use Talos interactively, automate it from the CLI, or embed the engine through the TypeScript SDK, JSONL RPC or Horizon Engine. |
110
+ | **Keep working while the agent works** | Draft your next message while questions wait above the prompt. Open full tool output on demand and resume saved sessions. |
111
+
112
+ ### Comparing your next coding agent?
113
+
114
+ Claude Code offers [skills, hooks, MCP and other extensions](https://code.claude.com/docs/en/features-overview), and OpenCode supports [custom tools](https://opencode.ai/docs/custom-tools/) and an [SDK](https://opencode.ai/docs/sdk/). The reason to try Talos is its particular combination: **explicit mission epochs, expiring focus, selectable context recipes and an inspectable receipt**, integrated into the engine you can also embed.
115
+
116
+ **Put context control on your comparison checklist:** can you inspect the current mission, see why a working profile was selected, pin a focus term and deliberately start a fresh epoch? In Talos, those are built-in controls you can try immediately.
117
+
118
+ ## Epoch-based context engineering
119
+
120
+ An **epoch** is a phase of work organized around a specific objective. Context Fabric tracks that objective across turns and builds the working context needed to pursue it.
121
+
122
+ | Concept | How it works |
99
123
  | --- | --- |
100
- | **One engine, multiple interfaces** | Interactive CLI, one-shot tasks, JSONL RPC, a TypeScript SDK and Horizon Engine. |
101
- | **Provider choice** | Anthropic, OpenAI-compatible providers and local Ollama models, with a model picker inside the session. |
102
- | **Resumable work** | Saved conversations, session branches, context inspection and project memory. |
103
- | **Extensible tools** | File and shell tools, MCP connections, skills, hooks and project instructions. |
104
- | **A readable terminal** | Compact operation previews, full details on demand and questions that leave your draft intact. |
105
- | **Explicit control** | Configurable permissions, separate desktop consent and teams started on request. |
124
+ | **Epoch — the objective** | A follow-up such as “continue” keeps the current mission. When Talos detects a new task, it opens a new epoch. A versioned **Goal Contract** tracks the objective, corrections, constraints and success criteria. |
125
+ | **Lease — the lifetime of focus** | Focus terms have an explicit lifetime: mission terms last for the epoch, while details from a turn or working mode expire after a few turns unless renewed. Explicitly pinned terms persist across epochs in the same session. |
126
+ | **Lens — the working profile** | Profiles such as `debug`, `research` and `security` set retrieval priorities, ranking weights, context budgets and evidence requirements. Choose one with `/vibe`, or let `auto` route the task. |
127
+
128
+ On each turn, Talos combines the current request, mission, goal revisions and active focus terms into a retrieval query. The **Context Window Compiler** retrieves relevant project memory, decisions and evidence, ranks and filters them, and assembles a **Prompt Pack** within the selected context budget. Epochs organize this working context alongside the saved conversation history and persistent project memory.
129
+
130
+ For example, “Fix the login bug” starts a mission. “Continue” keeps that mission; “Also handle login cancellation” refines it. “New task: review the deployment configuration” opens a new epoch, dropping the previous epoch's unpinned focus terms. Explicit pins remain, and stored information can be retrieved again when relevant.
131
+
132
+ Use `/context` or `/context explain` to inspect the active epoch, focus terms, routing reasons and context budget. Use `/context pin <term or path>` to retain a focus term and `/context unpin <term or path>` to release it. `/context reset` starts a new epoch on the next turn while preserving explicit pins.
133
+
134
+ See [Context Fabric](docs/CONTEXT-FABRIC.md) for the full lifecycle, persistence rules and implementation details.
135
+
136
+ ## Measured context control
137
+
138
+ Our **five-scenario, deterministic offline context benchmark**, rerun on September 11, 2026, covers follow-ups, task changes, session resumption, repository isolation and profile selection. It exercises Talos's context compiler against a documented local retrieval baseline.
139
+
140
+ | Metric in this fixture suite | Talos Context Fabric | Local retrieval proxy |
141
+ | --- | ---: | ---: |
142
+ | Precision of selected context | **100.0%** | 66.7% |
143
+ | Continuity across follow-ups and resumption | **100.0%** | 50.0% |
144
+ | Leakage of forbidden stale items — lower is better | **0.0%** | 52.8% |
145
+ | Reported context token count — total | **377** | 512 |
146
+
147
+ These are internal fixture results against the repository's `cursor-style-proxy`, **not measurements of the Cursor product or a live coding comparison with competing agents**. They measure context selection in this suite, not coding success, latency or your API bill. The value is reproducibility: inspect the [full report](docs/benchmark-results/context-gap-2026-09-11T08-44-47-116Z.md), [raw results](docs/benchmark-results/context-gap-2026-09-11T08-44-47-116Z.json) and [methodology](bench/README.md), or run `bun run bench:context` from a source checkout.
148
+
149
+ ## Why I built Talos
150
+
151
+ I'm **Lorenzo Daniel Danilov**, and I lead [Vascend Technologies](https://vascend.it), an Italian AI lab.
152
+
153
+ I created Talos because I found existing agent harnesses too restrictive for the way I wanted to work, especially when it came to customization. I wanted to shape the tools, workflows and context management around the task, with control over how the agent operates.
154
+
155
+ After months of research, I introduced a new approach to context engineering in Talos: **epoch-based context engineering**, implemented through **Context Fabric** and **Epoch–Lease–Lens**. My aim is to give developers a harness they can understand, adapt and build on.
156
+
157
+ **Bring your next bug, refactor or investigation to Talos.** [Install it](#quick-start), choose your model and put its context controls to the test on your own project.
106
158
 
107
159
  ## Keep your place in the conversation
108
160
 
@@ -199,7 +251,9 @@ Report bugs in [Issues](https://github.com/VascendTechnologies/vascend-talos/iss
199
251
 
200
252
  ## Project and license
201
253
 
202
- Built by [Vascend Technologies](https://vascend.it). Inspired by the extensible agent philosophy of [Pi](https://pi.dev).
254
+ Created by **Lorenzo Daniel Danilov** at [Vascend Technologies](https://vascend.it), an Italian AI lab. Inspired by the extensible agent philosophy of [Pi](https://pi.dev).
255
+
256
+ **Contact:** [LinkedIn](https://www.linkedin.com/in/lorenzo-danilov/) · [Instagram @LorenzoDanilov](https://www.instagram.com/LorenzoDanilov/) · [l.danilov@vascend.it](mailto:l.danilov@vascend.it)
203
257
 
204
258
  The package is marked **`UNLICENSED`**. This repository does not grant an open-source license; use and distribution require permission from Vascend Technologies.
205
259
 
@@ -0,0 +1,4 @@
1
+ /** A user-requested stop, not a fault to send to crash diagnostics. */
2
+ export declare class CliCancelled extends Error {
3
+ constructor(message: string);
4
+ }
@@ -0,0 +1,7 @@
1
+ /** A user-requested stop, not a fault to send to crash diagnostics. */
2
+ export class CliCancelled extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "CliCancelled";
6
+ }
7
+ }
@@ -49,7 +49,9 @@ export async function fetchReleaseBytes(url, limit, options = {}) {
49
49
  const controller = new AbortController();
50
50
  const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 20_000);
51
51
  try {
52
- const response = await (options.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal, headers: { Accept: "application/octet-stream" } });
52
+ // npm version endpoints return JSON; forcing octet-stream can serialize them
53
+ // as "[object Object]". Accept each endpoint's native metadata/archive type.
54
+ const response = await (options.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal, headers: { Accept: "*/*" } });
53
55
  if (!response.ok || !response.body) {
54
56
  await response.body?.cancel();
55
57
  throw new Error("Release download is unavailable.");
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
3
  import { installCrashMonitor, writeCrashReport } from "./core/crash-report.js";
4
+ import { CliCancelled } from "./core/cli-cancelled.js";
4
5
  import { TALOS_VERSION } from "./core/version.js";
5
6
  installCrashMonitor();
6
7
  import { bootstrap } from "./core/bootstrap.js";
@@ -225,6 +226,10 @@ async function main() {
225
226
  recoverQueue: !args.sessionId && !args.continueLast && !args.resumePicker && !args.name });
226
227
  }
227
228
  main().catch((err) => {
229
+ if (err instanceof CliCancelled) {
230
+ process.stdout.write(`\n ${err.message}\n\n`, () => process.exit(130));
231
+ return;
232
+ }
228
233
  const report = writeCrashReport(err, "main");
229
234
  process.stderr.write(`fatale: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
230
235
  if (report)
package/dist/install.mjs CHANGED
@@ -48,7 +48,7 @@ async function fetchReleaseBytes(url, limit, options = {}) {
48
48
  const controller = new AbortController;
49
49
  const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 20000);
50
50
  try {
51
- const response = await (options.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal, headers: { Accept: "application/octet-stream" } });
51
+ const response = await (options.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal, headers: { Accept: "*/*" } });
52
52
  if (!response.ok || !response.body) {
53
53
  await response.body?.cancel();
54
54
  throw new Error("Release download is unavailable.");
@@ -1 +1,2 @@
1
- export declare function enterAccount(env: NodeJS.ProcessEnv): Promise<string>;
1
+ import type { ThemeSpec } from "./theme.js";
2
+ export declare function enterAccount(env: NodeJS.ProcessEnv, theme?: ThemeSpec): Promise<string>;
@@ -1,12 +1,13 @@
1
1
  import * as readline from "node:readline";
2
2
  import { stdin, stdout } from "node:process";
3
- import { openUrl } from "../providers/open-browser.js";
4
3
  import { tr } from "./i18n.js";
5
4
  import { initializeLanguage, onboardingLanguage } from "./language-preference.js";
6
5
  import { leggiOnboarding, salvaOnboarding, serveTermini, TESTI, consensoAffermativo, TERMINI_VERSIONE, LINK_TERMINI, LINK_PRIVACY } from "./onboarding.js";
7
6
  import { decisioneCancello, tokenVascend, statoSessione, valutaLicenzaLocale, rimuoviTokenVascend, salvaTokenVascend, rinnovaProvaOffline } from "./vascend-login.js";
8
7
  import { startWebLogin, waitForWebLogin, WebLoginError } from "./web-login.js";
9
- export async function enterAccount(env) {
8
+ import { showWebLoginScreen } from "./web-login-screen.js";
9
+ import { CliCancelled } from "../core/cli-cancelled.js";
10
+ export async function enterAccount(env, theme) {
10
11
  const language = initializeLanguage(env);
11
12
  const state = onboardingLanguage(leggiOnboarding(env), env);
12
13
  // Host/test flags only control this local UI. Server authorization always
@@ -29,30 +30,40 @@ export async function enterAccount(env) {
29
30
  return offline?.ok ? tr("Account offline · prova firmata valida", "Offline account · signed proof valid") : note();
30
31
  }
31
32
  const abort = new AbortController();
32
- const rl = readline.createInterface({ input: stdin, output: stdout });
33
+ let rl;
33
34
  const cancel = () => abort.abort();
34
- rl.on("SIGINT", cancel);
35
+ process.on("SIGINT", cancel);
36
+ if (process.platform === "win32")
37
+ process.on("SIGBREAK", cancel);
35
38
  const write = (text) => { stdout.write(` ${text}\n`); };
36
39
  try {
37
40
  if (decision.azione === "login") {
38
41
  if (saved)
39
42
  rimuoviTokenVascend(env);
40
- write(tr("Accesso a Vascend Talos", "Sign in to Vascend Talos"));
41
- write(tr("Accedi sul sito e conferma il collegamento a Talos.", "Sign in on the website and approve the connection to Talos."));
42
- const grant = await startWebLogin(env, language, {}, abort.signal);
43
- write(grant.url);
44
- write(`${tr("Codice", "Code")}: ${grant.userCode.slice(0, 5)}-${grant.userCode.slice(5)}`);
45
- write(tr("Confronta il codice sul sito. In attesa della conferma · Ctrl+C annulla", "Compare the code on the website. Waiting for approval · Ctrl+C cancel"));
46
- await openUrl(grant.url, { print: () => { } });
47
- const account = await waitForWebLogin(env, grant, {}, abort.signal);
48
- if (!salvaTokenVascend(env, account.token))
49
- throw new Error(TESTI[language].tokenNonSalvabile);
50
- token = account.token;
51
- tier = account.tier;
52
- write(TESTI[language].benvenuto(account.email));
43
+ stdout.write(`\n ${tr("Preparazione dell'accesso a Talos...", "Preparing Talos sign-in...")}`);
44
+ const grant = await startWebLogin(env, language, {}, abort.signal)
45
+ .finally(() => { stdout.write(env.TERM === "dumb" ? "\n" : "\r\x1b[2K"); });
46
+ const finish = showWebLoginScreen(grant, abort, { env, theme });
47
+ try {
48
+ const account = await waitForWebLogin(env, grant, {}, abort.signal);
49
+ if (!salvaTokenVascend(env, account.token))
50
+ throw new Error(TESTI[language].tokenNonSalvabile);
51
+ token = account.token;
52
+ tier = account.tier;
53
+ finish(true);
54
+ write(TESTI[language].benvenuto(account.email));
55
+ }
56
+ finally {
57
+ finish();
58
+ }
53
59
  await rinnovaProvaOffline(env, token).catch(() => { });
54
60
  }
61
+ if (abort.signal.aborted)
62
+ throw new WebLoginError("cancelled");
55
63
  if (serveTermini(state)) {
64
+ rl = readline.createInterface({ input: stdin, output: stdout });
65
+ rl.on("SIGINT", cancel);
66
+ rl.on("close", cancel);
56
67
  const t = TESTI[language];
57
68
  write(t.terminiTitolo);
58
69
  write(t.terminiCorpo);
@@ -71,23 +82,28 @@ export async function enterAccount(env) {
71
82
  });
72
83
  });
73
84
  if (!consensoAffermativo(answer, language))
74
- throw new Error(t.terminiRifiutati);
85
+ throw new CliCancelled(t.terminiRifiutati);
75
86
  salvaOnboarding(env, { ...state, lingua: language, terminiVersione: TERMINI_VERSIONE, terminiQuando: new Date().toISOString() });
76
87
  }
77
88
  return note();
78
89
  }
79
90
  catch (e) {
80
91
  if (e instanceof WebLoginError) {
92
+ if (e.code === "cancelled")
93
+ throw new CliCancelled(tr("Accesso annullato.", "Sign-in cancelled."));
94
+ if (e.code === "access_denied")
95
+ throw new CliCancelled(tr("Collegamento annullato dal sito.", "Connection cancelled on the website."));
81
96
  const messages = {
82
- cancelled: tr("Accesso annullato.", "Sign-in cancelled."),
83
97
  expired_token: tr("Link scaduto. Riavvia Talos per riprovare.", "Link expired. Restart Talos to try again."),
84
- access_denied: tr("Collegamento annullato dal sito.", "Connection cancelled on the website."),
85
98
  };
86
99
  throw new Error(messages[e.code] ?? tr("Accesso dal sito non disponibile. Riprova fra poco.", "Website sign-in is unavailable. Please try again shortly."));
87
100
  }
88
101
  throw e;
89
102
  }
90
103
  finally {
91
- rl.close();
104
+ rl?.close();
105
+ process.removeListener("SIGINT", cancel);
106
+ if (process.platform === "win32")
107
+ process.removeListener("SIGBREAK", cancel);
92
108
  }
93
109
  }
@@ -1,6 +1,22 @@
1
1
  // English keys correspond to tr()'s stable fallback. User text, tool output,
2
2
  // command names and model identifiers never pass through this catalogue.
3
3
  export const TRANSLATION_ROWS = [
4
+ ["Connect your account and get started.", "Conecta tu cuenta y empieza.", "Konto verbinden und loslegen."],
5
+ ["1. Sign in in your browser", "1. Inicia sesión en el navegador", "1. Im Browser anmelden"],
6
+ ["vascend.it will open to continue.", "Se abrirá vascend.it para continuar.", "vascend.it wird zum Fortfahren geöffnet."],
7
+ ["2. Match the code and approve", "2. Compara el código y confirma", "2. Code vergleichen und bestätigen"],
8
+ ["Talos will continue automatically.", "Talos continuará automáticamente.", "Talos fährt automatisch fort."],
9
+ ["If the browser does not open, use this link:", "Si el navegador no se abre, usa este enlace:", "Falls der Browser nicht startet, diesen Link öffnen:"],
10
+ ["Enter open browser / C copy code", "Intro abrir navegador / C copiar código", "Enter Browser öffnen / C Code kopieren"],
11
+ ["L copy link / Esc or Ctrl+C cancel", "L copiar enlace / Esc o Ctrl+C cancelar", "L Link kopieren / Esc oder Ctrl+C abbrechen"],
12
+ ["Waiting for approval", "Esperando confirmación", "Warte auf Bestätigung"],
13
+ ["expires in {time}", "caduca en {time}", "läuft in {time} ab"],
14
+ ["Open the link above", "Abre el enlace de arriba", "Link oben öffnen"],
15
+ ["Link copied", "Enlace copiado", "Link kopiert"],
16
+ ["Code copied", "Código copiado", "Code kopiert"],
17
+ ["Select and copy the text above", "Selecciona y copia el texto de arriba", "Text oben markieren und kopieren"],
18
+ ["Account connected. You're ready!", "Cuenta conectada. ¡Todo listo!", "Konto verbunden. Alles bereit!"],
19
+ ["Preparing Talos sign-in...", "Preparando el acceso a Talos...", "Talos-Anmeldung wird vorbereitet..."],
4
20
  ["Talos update available", "Actualización de Talos disponible", "Talos-Update verfügbar"],
5
21
  ["Skip for now", "Omitir por ahora", "Vorerst überspringen"],
6
22
  ["Update now", "Actualizar ahora", "Jetzt aktualisieren"],
package/dist/tui/repl.js CHANGED
@@ -433,7 +433,7 @@ export async function startRepl(deps) {
433
433
  // chiave pubblica compilata, orologio ancorato): una "sessione" scritta a
434
434
  // mano nel config non supera la firma.
435
435
  const ingressoPrimaDellaTui = async () => {
436
- const note = await enterAccount(env);
436
+ const note = await enterAccount(env, activeSpec);
437
437
  accessoNota = note ? theme.muted(" " + note) : "";
438
438
  };
439
439
  // Registro degli allegati immagine della sessione TUI: incollare un path/data
@@ -0,0 +1,31 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import { type ThemeSpec } from "./theme.js";
3
+ import { type WebGrant } from "./web-login.js";
4
+ interface Appearance {
5
+ readonly columns?: number;
6
+ readonly color?: boolean;
7
+ readonly unicode?: boolean;
8
+ readonly theme?: ThemeSpec;
9
+ }
10
+ export declare function renderWebLoginScreen(grant: Pick<WebGrant, "userCode" | "url">, options?: Appearance): string;
11
+ export declare function renderWebLoginStatus(seconds: number, frame: number, options?: Appearance, message?: string): string;
12
+ type LoginInput = Readable & {
13
+ isRaw?: boolean;
14
+ setRawMode?: (raw: boolean) => unknown;
15
+ };
16
+ type LoginOutput = Writable & {
17
+ columns?: number;
18
+ isTTY?: boolean;
19
+ };
20
+ interface ScreenOptions {
21
+ readonly input?: LoginInput;
22
+ readonly output?: LoginOutput;
23
+ readonly env?: NodeJS.ProcessEnv;
24
+ readonly theme?: ThemeSpec;
25
+ readonly open?: (url: string) => Promise<boolean>;
26
+ readonly copy?: (text: string) => Promise<boolean>;
27
+ readonly now?: () => number;
28
+ }
29
+ /** Only the public code/link reach this UI; credentials stay in the login flow. */
30
+ export declare function showWebLoginScreen(grant: WebGrant, abort: AbortController, options?: ScreenOptions): (connected?: boolean) => void;
31
+ export {};
@@ -0,0 +1,165 @@
1
+ import { stdin, stdout } from "node:process";
2
+ import { openUrl } from "../providers/open-browser.js";
3
+ import { copyText } from "./clipboard-text.js";
4
+ import { tr } from "./i18n.js";
5
+ import { fitLine, plainText, terminalWidth, wrapText } from "./terminal-text.js";
6
+ import { painter, VASCEND_DARK } from "./theme.js";
7
+ import { verificationUrl } from "./web-login.js";
8
+ function presentation(options) {
9
+ const width = Math.max(16, Math.min(76, (options.columns || 80) - 4));
10
+ const colors = painter(options.theme ?? VASCEND_DARK);
11
+ const paint = (token, text) => options.color === false ? plainText(text) : colors(token, text);
12
+ const bold = (text) => options.color === false ? text : `\x1b[1m${text}\x1b[22m`;
13
+ return { width, paint, bold };
14
+ }
15
+ export function renderWebLoginScreen(grant, options = {}) {
16
+ const { width, paint, bold } = presentation(options);
17
+ const lines = ["", ` ${paint("accent", bold("TALOS"))} ${paint("muted", "VASCEND")}`];
18
+ const add = (text, token = "text", indent = 2) => {
19
+ for (const line of wrapText(text, width - indent + 2, true))
20
+ lines.push(" ".repeat(indent) + paint(token, line));
21
+ };
22
+ add(tr("Collega il tuo account e inizia.", "Connect your account and get started."), "muted");
23
+ lines.push("");
24
+ add(tr("1. Accedi nel browser", "1. Sign in in your browser"));
25
+ add(tr("Si aprirà vascend.it per continuare.", "vascend.it will open to continue."), "muted", 5);
26
+ lines.push("");
27
+ add(tr("2. Confronta il codice e conferma", "2. Match the code and approve"));
28
+ const code = plainText(grant.userCode).replace(/^(.{5})(.+)$/, "$1-$2");
29
+ const edge = options.unicode === false ? ["+", "-", "+", "|", "+", "+"] : ["╭", "─", "╮", "│", "╰", "╯"];
30
+ const codeWidth = Math.min(25, width - 5);
31
+ const padding = Math.max(0, Math.floor((codeWidth - code.length) / 2));
32
+ const row = " ".repeat(padding) + code + " ".repeat(Math.max(0, codeWidth - code.length - padding));
33
+ lines.push(` ${paint("borderAccent", edge[0] + edge[1].repeat(codeWidth) + edge[2])}`, ` ${paint("borderAccent", edge[3])}${paint("accent", bold(row))}${paint("borderAccent", edge[3])}`, ` ${paint("borderAccent", edge[4] + edge[1].repeat(codeWidth) + edge[5])}`);
34
+ add(tr("Talos continuerà automaticamente.", "Talos will continue automatically."), "muted", 5);
35
+ lines.push("");
36
+ add(tr("Se il browser non si apre, usa questo link:", "If the browser does not open, use this link:"), "muted");
37
+ for (const line of wrapText(grant.url, width))
38
+ lines.push(" " + paint("accent", line));
39
+ lines.push("");
40
+ add(tr("Invio apri browser / C copia codice", "Enter open browser / C copy code"), "muted");
41
+ add(tr("L copia link / Esc o Ctrl+C annulla", "L copy link / Esc or Ctrl+C cancel"), "muted");
42
+ lines.push("");
43
+ return lines.join("\n") + "\n";
44
+ }
45
+ export function renderWebLoginStatus(seconds, frame, options = {}, message) {
46
+ const { width, paint } = presentation(options);
47
+ const remaining = Math.max(0, Math.ceil(seconds));
48
+ const clock = `${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")}`;
49
+ const countdown = width >= 50 ? tr("scade tra {time}", "expires in {time}", { time: clock }) : clock;
50
+ const spinner = options.unicode === false ? ["-", "\\", "|", "/"] : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
51
+ const label = plainText(fitLine(message ?? tr("In attesa di conferma", "Waiting for approval"), width - terminalWidth(countdown) - 7));
52
+ return ` ${paint("accent", spinner[frame % spinner.length])} ${paint("text", label)} ${paint("muted", ` / ${countdown}`)}`;
53
+ }
54
+ /** Only the public code/link reach this UI; credentials stay in the login flow. */
55
+ export function showWebLoginScreen(grant, abort, options = {}) {
56
+ const input = options.input ?? stdin;
57
+ const output = options.output ?? stdout;
58
+ const env = options.env ?? process.env;
59
+ const now = options.now ?? Date.now;
60
+ const url = verificationUrl(grant.url, grant.userCode);
61
+ const animated = Boolean(output.isTTY) && env.TERM !== "dumb";
62
+ const appearance = () => ({ columns: output.columns, color: animated && env.NO_COLOR === undefined,
63
+ unicode: env.TERM !== "dumb", theme: options.theme });
64
+ const open = options.open ?? (async (value) => (await openUrl(value, { print: () => { } })).opened);
65
+ const copy = options.copy ?? ((value) => copyText(value, undefined, process.platform, env));
66
+ const wasRaw = Boolean(input.isRaw);
67
+ const wasPaused = input.isPaused();
68
+ let closed = false, opening = false, copying = false, frame = 0;
69
+ let feedback = "", feedbackUntil = 0;
70
+ const deadline = now() + grant.expiresIn * 1000;
71
+ let timer;
72
+ const draw = () => {
73
+ if (closed)
74
+ return;
75
+ const line = renderWebLoginStatus((deadline - now()) / 1000, frame++, appearance(), now() < feedbackUntil ? feedback : undefined);
76
+ output.write((animated ? "\r\x1b[2K" : "") + line + (animated ? "" : "\n"));
77
+ };
78
+ const notify = (message) => { feedback = message; feedbackUntil = now() + 4000; draw(); };
79
+ const launch = async () => {
80
+ if (closed || opening)
81
+ return;
82
+ opening = true;
83
+ try {
84
+ const opened = await open(url);
85
+ if (!closed && !opened)
86
+ notify(tr("Apri il link qui sopra", "Open the link above"));
87
+ }
88
+ catch {
89
+ if (!closed)
90
+ notify(tr("Apri il link qui sopra", "Open the link above"));
91
+ }
92
+ finally {
93
+ opening = false;
94
+ }
95
+ };
96
+ const copyValue = async (link) => {
97
+ if (closed || copying)
98
+ return;
99
+ copying = true;
100
+ try {
101
+ const ok = await copy(link ? url : grant.userCode);
102
+ if (!closed)
103
+ notify(ok ? (link ? tr("Link copiato", "Link copied") : tr("Codice copiato", "Code copied"))
104
+ : tr("Seleziona e copia il testo qui sopra", "Select and copy the text above"));
105
+ }
106
+ catch {
107
+ if (!closed)
108
+ notify(tr("Seleziona e copia il testo qui sopra", "Select and copy the text above"));
109
+ }
110
+ finally {
111
+ copying = false;
112
+ }
113
+ };
114
+ const cancel = () => abort.abort();
115
+ const onData = (chunk) => {
116
+ const key = chunk.toString();
117
+ if (["\x03", "\x04", "\x1b"].includes(key))
118
+ cancel();
119
+ else if (key === "\r" || key === "\n")
120
+ void launch();
121
+ else if (key.toLowerCase() === "c")
122
+ void copyValue(false);
123
+ else if (key.toLowerCase() === "l")
124
+ void copyValue(true);
125
+ };
126
+ const finish = (connected = false) => {
127
+ if (closed)
128
+ return;
129
+ closed = true;
130
+ clearInterval(timer);
131
+ input.removeListener("data", onData);
132
+ input.removeListener("end", cancel);
133
+ abort.signal.removeEventListener("abort", onAbort);
134
+ input.setRawMode?.(wasRaw);
135
+ if (wasPaused)
136
+ input.pause();
137
+ if (animated)
138
+ output.write("\r\x1b[2K\x1b[?25h");
139
+ if (connected)
140
+ output.write(" " + presentation(appearance()).paint("success", tr("Account collegato. Ci siamo!", "Account connected. You're ready!")));
141
+ output.write("\n");
142
+ };
143
+ const onAbort = () => finish();
144
+ if (abort.signal.aborted)
145
+ return () => { };
146
+ output.write(renderWebLoginScreen(grant, appearance()));
147
+ if (animated)
148
+ output.write("\x1b[?25l");
149
+ try {
150
+ input.setRawMode?.(true);
151
+ input.on("data", onData);
152
+ input.on("end", cancel);
153
+ input.resume();
154
+ abort.signal.addEventListener("abort", onAbort, { once: true });
155
+ draw();
156
+ if (animated)
157
+ timer = setInterval(draw, 160);
158
+ void launch();
159
+ }
160
+ catch (error) {
161
+ finish();
162
+ throw error;
163
+ }
164
+ return finish;
165
+ }
@@ -10,18 +10,21 @@ export class WebLoginError extends Error {
10
10
  }
11
11
  async function post(env, path, body, deps, signal) {
12
12
  let response;
13
+ let raw;
13
14
  try {
14
15
  response = await (deps.fetchImpl ?? fetch)(baseOf(deps, env) + "/auth/device/" + path, {
15
16
  method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
16
17
  redirect: "error", signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(12_000)]) : AbortSignal.timeout(12_000),
17
18
  });
19
+ raw = await response.text();
20
+ if (signal?.aborted)
21
+ throw new WebLoginError("cancelled");
18
22
  }
19
23
  catch {
20
24
  if (signal?.aborted)
21
25
  throw new WebLoginError("cancelled");
22
26
  throw new WebLoginError("temporarily_unavailable");
23
27
  }
24
- const raw = await response.text();
25
28
  if (raw.length > 16_384)
26
29
  throw new WebLoginError("invalid_response");
27
30
  let data;
package/install.ps1 CHANGED
@@ -144,7 +144,7 @@ async function fetchReleaseBytes(url, limit, options = {}) {
144
144
  const controller = new AbortController;
145
145
  const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 20000);
146
146
  try {
147
- const response = await (options.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal, headers: { Accept: "application/octet-stream" } });
147
+ const response = await (options.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal, headers: { Accept: "*/*" } });
148
148
  if (!response.ok || !response.body) {
149
149
  await response.body?.cancel();
150
150
  throw new Error("Release download is unavailable.");
package/install.sh CHANGED
@@ -137,7 +137,7 @@ async function fetchReleaseBytes(url, limit, options = {}) {
137
137
  const controller = new AbortController;
138
138
  const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 20000);
139
139
  try {
140
- const response = await (options.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal, headers: { Accept: "application/octet-stream" } });
140
+ const response = await (options.fetch ?? fetch)(url, { redirect: "error", signal: controller.signal, headers: { Accept: "*/*" } });
141
141
  if (!response.ok || !response.body) {
142
142
  await response.body?.cancel();
143
143
  throw new Error("Release download is unavailable.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vascend/talos",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Vascend Talos - extensible coding agent for your terminal",
5
5
  "repository": {
6
6
  "type": "git",