@vascend/talos 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +37 -12
- package/dist/core/agent.d.ts +16 -3
- package/dist/core/agent.js +170 -70
- package/dist/core/background.d.ts +10 -3
- package/dist/core/background.js +55 -9
- package/dist/core/bootstrap.js +1 -0
- package/dist/core/compaction.js +1 -1
- package/dist/core/config-storage.d.ts +5 -0
- package/dist/core/config-storage.js +83 -0
- package/dist/core/config.d.ts +11 -2
- package/dist/core/config.js +24 -8
- package/dist/core/context-ledger.js +2 -1
- package/dist/core/context-policy.d.ts +1 -1
- package/dist/core/context-policy.js +10 -7
- package/dist/core/credentials.js +29 -30
- package/dist/core/effort.d.ts +1 -1
- package/dist/core/effort.js +5 -1
- package/dist/core/execution-guidance.d.ts +1 -1
- package/dist/core/execution-guidance.js +2 -0
- package/dist/core/file-coordination.d.ts +3 -0
- package/dist/core/file-coordination.js +28 -0
- package/dist/core/file-lease-store.d.ts +3 -0
- package/dist/core/file-lease-store.js +28 -12
- package/dist/core/fusion.d.ts +24 -0
- package/dist/core/fusion.js +80 -0
- package/dist/core/model-limits.d.ts +1 -0
- package/dist/core/model-limits.js +5 -1
- package/dist/core/permission-queue.js +15 -15
- package/dist/core/request-plan.js +4 -3
- package/dist/core/session-naming.d.ts +3 -0
- package/dist/core/session-naming.js +47 -0
- package/dist/core/session-usage.js +4 -1
- package/dist/core/session.d.ts +5 -2
- package/dist/core/session.js +22 -7
- package/dist/core/settings.d.ts +2 -0
- package/dist/core/settings.js +5 -16
- package/dist/core/step-policy.d.ts +18 -0
- package/dist/core/step-policy.js +6 -0
- package/dist/core/storage-health.d.ts +24 -0
- package/dist/core/storage-health.js +72 -0
- package/dist/core/tool-artifacts.d.ts +2 -0
- package/dist/core/tool-artifacts.js +21 -3
- package/dist/core/tool-result-index.js +29 -33
- package/dist/core/tool-result-store.js +6 -5
- package/dist/core/types.d.ts +26 -2
- package/dist/core/user-inbox.d.ts +9 -0
- package/dist/core/user-inbox.js +22 -3
- package/dist/core/user-input.js +1 -1
- package/dist/fusion/autopilot-fast.d.ts +59 -0
- package/dist/fusion/autopilot-fast.js +213 -0
- package/dist/fusion/autopilot.d.ts +75 -0
- package/dist/fusion/autopilot.js +308 -0
- package/dist/fusion/focus.d.ts +30 -0
- package/dist/fusion/focus.js +122 -0
- package/dist/fusion/gate.d.ts +30 -0
- package/dist/fusion/gate.js +95 -0
- package/dist/fusion/jev.d.ts +71 -0
- package/dist/fusion/jev.js +164 -0
- package/dist/fusion/receipts.d.ts +25 -0
- package/dist/fusion/receipts.js +34 -0
- package/dist/fusion/route.d.ts +25 -0
- package/dist/fusion/route.js +84 -0
- package/dist/fusion/run.d.ts +4 -0
- package/dist/fusion/run.js +186 -0
- package/dist/horizon/engine.d.ts +20 -1
- package/dist/horizon/engine.js +57 -0
- package/dist/horizon/index.d.ts +2 -2
- package/dist/horizon/index.js +1 -1
- package/dist/horizon/types.d.ts +29 -0
- package/dist/index.js +12 -0
- package/dist/providers/anthropic-oauth.d.ts +8 -3
- package/dist/providers/anthropic-oauth.js +13 -10
- package/dist/providers/anthropic.d.ts +9 -2
- package/dist/providers/anthropic.js +64 -21
- package/dist/providers/cli-models.d.ts +30 -0
- package/dist/providers/cli-models.js +194 -0
- package/dist/providers/model-effort.js +1 -1
- package/dist/providers/openai-oauth.d.ts +8 -3
- package/dist/providers/openai-oauth.js +9 -3
- package/dist/providers/openrouter-manual-fallback.d.ts +3 -1
- package/dist/providers/openrouter-manual-fallback.js +20 -20
- package/dist/providers/registry.js +3 -2
- package/dist/providers/retry.d.ts +3 -0
- package/dist/providers/retry.js +44 -1
- package/dist/remote/cifratura.d.ts +12 -0
- package/dist/remote/cifratura.js +66 -0
- package/dist/remote/index.d.ts +15 -0
- package/dist/remote/index.js +65 -0
- package/dist/remote/relay-client.d.ts +58 -0
- package/dist/remote/relay-client.js +315 -0
- package/dist/remote/sessione-remota.d.ts +60 -0
- package/dist/remote/sessione-remota.js +266 -0
- package/dist/sdk.d.ts +7 -2
- package/dist/sdk.js +3 -1
- package/dist/tools/builtin.js +41 -20
- package/dist/tools/capabilities.d.ts +3 -1
- package/dist/tools/capabilities.js +5 -2
- package/dist/tools/computer-use.js +2 -1
- package/dist/tools/pagination.js +1 -1
- package/dist/tools/program.js +34 -21
- package/dist/tools/team-tools.js +11 -6
- package/dist/tui/account-entry.js +14 -9
- package/dist/tui/agent-label.d.ts +2 -0
- package/dist/tui/agent-label.js +5 -0
- package/dist/tui/agent-sessions-screen.d.ts +35 -0
- package/dist/tui/agent-sessions-screen.js +178 -0
- package/dist/tui/agent-sessions.d.ts +53 -0
- package/dist/tui/agent-sessions.js +236 -0
- package/dist/tui/bottom-input.d.ts +57 -2
- package/dist/tui/bottom-input.js +449 -54
- package/dist/tui/branding.js +8 -4
- package/dist/tui/capability-lists.d.ts +10 -0
- package/dist/tui/capability-lists.js +41 -0
- package/dist/tui/command-output-registry.js +4 -0
- package/dist/tui/command-transcript.d.ts +1 -1
- package/dist/tui/command-transcript.js +17 -21
- package/dist/tui/commands.js +6 -2
- package/dist/tui/compaction-status.d.ts +7 -0
- package/dist/tui/compaction-status.js +39 -0
- package/dist/tui/config-recovery.d.ts +12 -0
- package/dist/tui/config-recovery.js +54 -0
- package/dist/tui/context-meter.d.ts +3 -0
- package/dist/tui/context-meter.js +35 -8
- package/dist/tui/diff.d.ts +9 -0
- package/dist/tui/diff.js +43 -19
- package/dist/tui/fusion-menu.d.ts +5 -0
- package/dist/tui/fusion-menu.js +89 -0
- package/dist/tui/i18n-catalog.js +351 -2
- package/dist/tui/live-command.d.ts +1 -0
- package/dist/tui/live-command.js +9 -0
- package/dist/tui/live-output.d.ts +1 -0
- package/dist/tui/live-output.js +11 -0
- package/dist/tui/logo.js +3 -0
- package/dist/tui/model-details.js +13 -12
- package/dist/tui/model-picker.d.ts +2 -0
- package/dist/tui/model-picker.js +59 -0
- package/dist/tui/model-selection.js +19 -10
- package/dist/tui/native-scrollback.d.ts +2 -1
- package/dist/tui/native-scrollback.js +31 -6
- package/dist/tui/prompt-queue-store.d.ts +1 -0
- package/dist/tui/prompt-queue-store.js +4 -0
- package/dist/tui/prompt-queue.d.ts +3 -0
- package/dist/tui/prompt-queue.js +19 -1
- package/dist/tui/provider-label.d.ts +2 -0
- package/dist/tui/provider-label.js +9 -0
- package/dist/tui/provider-menu.js +25 -24
- package/dist/tui/queue-screen.js +2 -1
- package/dist/tui/queued-questions.js +2 -4
- package/dist/tui/quick-help.d.ts +1 -1
- package/dist/tui/quick-help.js +3 -0
- package/dist/tui/remote-command.d.ts +7 -0
- package/dist/tui/remote-command.js +79 -0
- package/dist/tui/repl.d.ts +11 -2
- package/dist/tui/repl.js +457 -327
- package/dist/tui/review-store.d.ts +8 -1
- package/dist/tui/review-store.js +64 -11
- package/dist/tui/review-team.js +4 -3
- package/dist/tui/runtime-error.d.ts +2 -0
- package/dist/tui/runtime-error.js +26 -0
- package/dist/tui/scrollback-archive.d.ts +16 -0
- package/dist/tui/scrollback-archive.js +90 -0
- package/dist/tui/selector-mouse.d.ts +1 -0
- package/dist/tui/selector-mouse.js +1 -1
- package/dist/tui/selector.d.ts +11 -0
- package/dist/tui/selector.js +28 -4
- package/dist/tui/session-controls.d.ts +26 -0
- package/dist/tui/session-controls.js +65 -0
- package/dist/tui/session-naming-notice.d.ts +14 -0
- package/dist/tui/session-naming-notice.js +59 -0
- package/dist/tui/session-picker.d.ts +2 -0
- package/dist/tui/session-picker.js +32 -0
- package/dist/tui/session-replay.d.ts +1 -0
- package/dist/tui/session-replay.js +101 -32
- package/dist/tui/session-transcript-layout.d.ts +9 -0
- package/dist/tui/session-transcript-layout.js +41 -0
- package/dist/tui/startup.js +63 -33
- package/dist/tui/status-line.d.ts +1 -1
- package/dist/tui/status-line.js +20 -12
- package/dist/tui/storage-warning.d.ts +2 -0
- package/dist/tui/storage-warning.js +35 -0
- package/dist/tui/talos-art-data.d.ts +1 -0
- package/dist/tui/talos-art-data.js +2 -0
- package/dist/tui/talos-art.d.ts +1 -0
- package/dist/tui/talos-art.js +9 -0
- package/dist/tui/task-labels.d.ts +1 -0
- package/dist/tui/task-labels.js +8 -0
- package/dist/tui/terminal-text.d.ts +2 -1
- package/dist/tui/terminal-text.js +28 -2
- package/dist/tui/theme.js +2 -2
- package/dist/tui/tool-presentation.d.ts +3 -0
- package/dist/tui/tool-presentation.js +98 -0
- package/dist/tui/tool-style.js +4 -0
- package/dist/tui/transcript-store.d.ts +25 -0
- package/dist/tui/transcript-store.js +151 -0
- package/dist/tui/usage-footer.d.ts +1 -1
- package/dist/tui/usage-footer.js +3 -1
- package/dist/tui/vascend-id.d.ts +49 -0
- package/dist/tui/vascend-id.js +321 -0
- package/dist/tui/web-login-screen.js +4 -3
- package/dist/tui/web-login.d.ts +7 -15
- package/dist/tui/web-login.js +38 -78
- package/dist/tui/worker-transcripts.d.ts +31 -0
- package/dist/tui/worker-transcripts.js +91 -0
- package/dist/vascend/candidate-collector.d.ts +6 -5
- package/dist/vascend/candidate-collector.js +44 -43
- package/dist/vascend/context-feedback-loop.js +2 -0
- package/dist/vascend/context-lint.js +32 -2
- package/dist/vascend/context-market-selection.js +12 -5
- package/dist/vascend/context-pack-render.js +10 -4
- package/dist/vascend/context-packet-mirrors.d.ts +8 -0
- package/dist/vascend/context-packet-mirrors.js +24 -0
- package/dist/vascend/context-plan-events.d.ts +4 -0
- package/dist/vascend/context-plan-events.js +44 -0
- package/dist/vascend/context-snapshot.d.ts +1 -0
- package/dist/vascend/context-snapshot.js +8 -5
- package/dist/vascend/context-store-reader.d.ts +20 -0
- package/dist/vascend/context-store-reader.js +46 -0
- package/dist/vascend/context-window-compiler.d.ts +1 -0
- package/dist/vascend/context-window-compiler.js +1 -0
- package/dist/vascend/file-lock.d.ts +1 -0
- package/dist/vascend/file-lock.js +26 -0
- package/dist/vascend/granular-kingdom.d.ts +1 -0
- package/dist/vascend/granular-kingdom.js +52 -17
- package/dist/vascend/kingdom-bus.d.ts +1 -0
- package/dist/vascend/kingdom-bus.js +3 -0
- package/dist/vascend/kingdom-channel.d.ts +2 -0
- package/dist/vascend/kingdom-channel.js +3 -0
- package/dist/vascend/kingdom-live.d.ts +32 -4
- package/dist/vascend/kingdom-live.js +149 -13
- package/dist/vascend/kingdom-run.d.ts +5 -1
- package/dist/vascend/kingdom-run.js +6 -2
- package/dist/vascend/kingdom.d.ts +3 -0
- package/dist/vascend/kingdom.js +22 -11
- package/dist/vascend/memory-packet.d.ts +2 -1
- package/dist/vascend/memory-packet.js +31 -44
- package/dist/vascend/memory-store.d.ts +1 -0
- package/dist/vascend/memory-store.js +12 -4
- package/dist/vascend/plan-store.d.ts +3 -1
- package/dist/vascend/plan-store.js +6 -4
- package/dist/vascend/room-memory.d.ts +13 -2
- package/dist/vascend/room-memory.js +48 -6
- package/dist/vascend/runtime.d.ts +3 -11
- package/dist/vascend/runtime.js +11 -123
- package/dist/vascend/session-search.js +2 -0
- package/dist/vascend/team-session.js +3 -2
- package/dist/vascend/vision.d.ts +3 -1
- package/dist/vascend/vision.js +6 -4
- package/dist/vascend/worker-session.d.ts +20 -0
- package/dist/vascend/worker-session.js +83 -0
- package/package.json +2 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { talosConfigDir } from "../core/env-paths.js";
|
|
6
|
+
const QUERY_TIMEOUT_MS = 20_000;
|
|
7
|
+
function cachePath(env) {
|
|
8
|
+
if (env["TALOS_NO_GLOBAL_CONFIG"] === "1" || env["TALOS_DISABLE_CLI_MODELS"] === "1")
|
|
9
|
+
return null;
|
|
10
|
+
return join(talosConfigDir(env), "cli-models.json");
|
|
11
|
+
}
|
|
12
|
+
/** Primo CLI nel PATH con l'impronta del binario reale (symlink risolti), o null se non installato. */
|
|
13
|
+
export function findCli(name, env) {
|
|
14
|
+
const names = process.platform === "win32" ? [`${name}.exe`, `${name}.cmd`] : [name];
|
|
15
|
+
for (const directory of String(env["PATH"] ?? env["Path"] ?? "").split(delimiter)) {
|
|
16
|
+
if (!directory || !isAbsolute(directory))
|
|
17
|
+
continue;
|
|
18
|
+
for (const file of names) {
|
|
19
|
+
const candidate = join(directory, file);
|
|
20
|
+
if (!existsSync(candidate))
|
|
21
|
+
continue;
|
|
22
|
+
try {
|
|
23
|
+
const real = realpathSync(candidate);
|
|
24
|
+
return { executable: candidate, stamp: `${real}@${statSync(real).mtimeMs}` };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Voce del PATH non leggibile: si prova la successiva.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function readCache(env) {
|
|
34
|
+
const path = cachePath(env);
|
|
35
|
+
if (!path || !existsSync(path))
|
|
36
|
+
return {};
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
39
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Modelli in cache per il CLI installato ora, o undefined se assenti o superati da un aggiornamento. */
|
|
46
|
+
export function cachedCliModels(name, env) {
|
|
47
|
+
if (!cachePath(env))
|
|
48
|
+
return undefined;
|
|
49
|
+
const cli = findCli(name, env);
|
|
50
|
+
const entry = readCache(env)[name];
|
|
51
|
+
if (!cli || !entry || entry.stamp !== cli.stamp)
|
|
52
|
+
return undefined;
|
|
53
|
+
const models = Array.isArray(entry.models) ? entry.models.filter((model) => typeof model?.id === "string" && model.id && typeof model.label === "string") : [];
|
|
54
|
+
return models.length ? models : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Righe di `initialize` del CLI Claude Code. Gli alias (default, opus, sonnet)
|
|
58
|
+
* diventano il modello versionato che eseguono: una voce per modello reale,
|
|
59
|
+
* nell'ordine del CLI.
|
|
60
|
+
*/
|
|
61
|
+
export function claudeModelsFromInitialize(models) {
|
|
62
|
+
const out = new Map();
|
|
63
|
+
for (const raw of Array.isArray(models) ? models : []) {
|
|
64
|
+
const row = raw;
|
|
65
|
+
if (row["value"] === "default")
|
|
66
|
+
continue;
|
|
67
|
+
const id = String(row["resolvedModel"] || row["value"] || "").trim();
|
|
68
|
+
if (!id || out.has(id))
|
|
69
|
+
continue;
|
|
70
|
+
const label = String(row["displayName"] || id).trim();
|
|
71
|
+
const description = String(row["description"] ?? "").trim();
|
|
72
|
+
out.set(id, { id, label, ...(description ? { description } : {}) });
|
|
73
|
+
}
|
|
74
|
+
return [...out.values()];
|
|
75
|
+
}
|
|
76
|
+
/** Righe di `model/list` dell'app-server Codex, senza i modelli nascosti. */
|
|
77
|
+
export function codexModelsFromList(data) {
|
|
78
|
+
const out = new Map();
|
|
79
|
+
for (const raw of Array.isArray(data) ? data : []) {
|
|
80
|
+
const row = raw;
|
|
81
|
+
if (row["hidden"] === true)
|
|
82
|
+
continue;
|
|
83
|
+
const id = String(row["model"] || row["id"] || "").trim();
|
|
84
|
+
if (!id || out.has(id))
|
|
85
|
+
continue;
|
|
86
|
+
const label = String(row["displayName"] || id).trim();
|
|
87
|
+
const description = String(row["description"] ?? "").trim();
|
|
88
|
+
out.set(id, { id, label, ...(description ? { description } : {}) });
|
|
89
|
+
}
|
|
90
|
+
return [...out.values()];
|
|
91
|
+
}
|
|
92
|
+
// Avvia il CLI, scrive le richieste JSONL e attende la prima riga che `pick`
|
|
93
|
+
// riconosce. Il processo viene sempre terminato, anche su timeout o errore.
|
|
94
|
+
function queryCli(executable, args, requests, pick, env, timeoutMs) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const child = spawn(executable, [...args], {
|
|
97
|
+
env, cwd: tmpdir(), stdio: ["pipe", "pipe", "ignore"], windowsHide: true,
|
|
98
|
+
shell: process.platform === "win32" && executable.toLowerCase().endsWith(".cmd"),
|
|
99
|
+
});
|
|
100
|
+
let buffer = "";
|
|
101
|
+
let settled = false;
|
|
102
|
+
const finish = (error, value) => {
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
child.kill();
|
|
108
|
+
if (error)
|
|
109
|
+
reject(error);
|
|
110
|
+
else
|
|
111
|
+
resolve(value);
|
|
112
|
+
};
|
|
113
|
+
const timer = setTimeout(() => finish(new Error(`${executable} did not answer in time`)), timeoutMs);
|
|
114
|
+
child.on("error", (error) => finish(error));
|
|
115
|
+
child.on("close", () => finish(new Error(`${executable} exited before answering`)));
|
|
116
|
+
child.stdout.setEncoding("utf8");
|
|
117
|
+
child.stdout.on("data", (chunk) => {
|
|
118
|
+
buffer += chunk;
|
|
119
|
+
let newline;
|
|
120
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
121
|
+
const line = buffer.slice(0, newline).trim();
|
|
122
|
+
buffer = buffer.slice(newline + 1);
|
|
123
|
+
if (!line.startsWith("{"))
|
|
124
|
+
continue;
|
|
125
|
+
try {
|
|
126
|
+
const value = pick(JSON.parse(line));
|
|
127
|
+
if (value !== undefined)
|
|
128
|
+
finish(null, value);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Righe non JSON (log) ignorate.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
child.stdin.on("error", () => { });
|
|
136
|
+
child.stdin.write(requests.map((request) => JSON.stringify(request)).join("\n") + "\n");
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async function queryClaudeModels(executable, env, timeoutMs) {
|
|
140
|
+
const response = await queryCli(executable, ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"], [{ type: "control_request", request_id: "talos-models", request: { subtype: "initialize" } }], (message) => {
|
|
141
|
+
const body = message["response"];
|
|
142
|
+
return message["type"] === "control_response" && body?.["request_id"] === "talos-models" ? body : undefined;
|
|
143
|
+
}, env, timeoutMs);
|
|
144
|
+
if (response["subtype"] !== "success")
|
|
145
|
+
throw new Error(String(response["error"] ?? "Claude Code initialize failed"));
|
|
146
|
+
return claudeModelsFromInitialize(response["response"]?.["models"]);
|
|
147
|
+
}
|
|
148
|
+
async function queryCodexModels(executable, env, timeoutMs) {
|
|
149
|
+
const response = await queryCli(executable, ["app-server"], [
|
|
150
|
+
{ id: 1, method: "initialize", params: { clientInfo: { name: "talos", version: "1" } } },
|
|
151
|
+
{ method: "initialized" },
|
|
152
|
+
{ id: 2, method: "model/list", params: {} },
|
|
153
|
+
], (message) => message["id"] === 2 ? message : undefined, env, timeoutMs);
|
|
154
|
+
if (response["error"])
|
|
155
|
+
throw new Error(JSON.stringify(response["error"]));
|
|
156
|
+
return codexModelsFromList(response["result"]?.["data"]);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Rilegge i modelli dai CLI installati la cui cache manca o e' superata, e la
|
|
160
|
+
* salva. Un CLI assente, lento o in errore lascia la cache com'e': il catalogo
|
|
161
|
+
* ripiega su quello statico.
|
|
162
|
+
*/
|
|
163
|
+
export async function refreshCliModels(env, options = {}) {
|
|
164
|
+
const path = cachePath(env);
|
|
165
|
+
if (!path)
|
|
166
|
+
return;
|
|
167
|
+
const timeoutMs = options.timeoutMs ?? QUERY_TIMEOUT_MS;
|
|
168
|
+
const queries = { claude: queryClaudeModels, codex: queryCodexModels };
|
|
169
|
+
const updates = await Promise.all(Object.keys(queries).map(async (name) => {
|
|
170
|
+
if (!options.force && cachedCliModels(name, env))
|
|
171
|
+
return null;
|
|
172
|
+
const cli = findCli(name, env);
|
|
173
|
+
if (!cli)
|
|
174
|
+
return null;
|
|
175
|
+
try {
|
|
176
|
+
const models = await queries[name](cli.executable, env, timeoutMs);
|
|
177
|
+
return models.length ? [name, { stamp: cli.stamp, models }] : null;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
const fresh = updates.filter((update) => update !== null);
|
|
184
|
+
if (!fresh.length)
|
|
185
|
+
return;
|
|
186
|
+
const cache = { ...readCache(env), ...Object.fromEntries(fresh) };
|
|
187
|
+
try {
|
|
188
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
189
|
+
writeFileSync(path, JSON.stringify(cache, null, 2) + "\n", { mode: 0o600 });
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Cache non scrivibile: il catalogo resta quello statico.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -26,7 +26,7 @@ export function modelEfforts(fullModel, metadata = modelCapabilities(fullModel))
|
|
|
26
26
|
return ["", "low", "medium", "high"];
|
|
27
27
|
}
|
|
28
28
|
if (provider === "openai") {
|
|
29
|
-
if (/gpt-6
|
|
29
|
+
if (/gpt-6/.test(id))
|
|
30
30
|
return ["", "low", "medium", "high", "xhigh", "max"];
|
|
31
31
|
if (/gpt-5\.[456]/.test(id))
|
|
32
32
|
return ["", "low", "medium", "high", "xhigh"];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CliModel } from "./cli-models.js";
|
|
1
2
|
import type { CompletionRequest, ModelTurn, Provider } from "../core/types.js";
|
|
2
3
|
import { type OAuthCreds } from "./oauth-store.js";
|
|
3
4
|
export declare class OpenAIOAuthProvider implements Provider {
|
|
@@ -5,10 +6,14 @@ export declare class OpenAIOAuthProvider implements Provider {
|
|
|
5
6
|
private readonly env;
|
|
6
7
|
private readonly now;
|
|
7
8
|
readonly id = "chatgpt";
|
|
8
|
-
readonly label = "ChatGPT
|
|
9
|
-
readonly models: readonly [
|
|
9
|
+
readonly label = "ChatGPT";
|
|
10
|
+
readonly models: readonly string[];
|
|
11
|
+
readonly modelLabels?: ReadonlyMap<string, {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly description?: string;
|
|
14
|
+
}>;
|
|
10
15
|
readonly supportsFastMode = true;
|
|
11
|
-
constructor(creds: OAuthCreds, env: NodeJS.ProcessEnv, now?: () => number);
|
|
16
|
+
constructor(creds: OAuthCreds, env: NodeJS.ProcessEnv, now?: () => number, cliModels?: readonly CliModel[]);
|
|
12
17
|
private ensureToken;
|
|
13
18
|
complete(req: CompletionRequest): Promise<ModelTurn>;
|
|
14
19
|
}
|
|
@@ -105,13 +105,19 @@ export class OpenAIOAuthProvider {
|
|
|
105
105
|
env;
|
|
106
106
|
now;
|
|
107
107
|
id = "chatgpt";
|
|
108
|
-
label = "ChatGPT
|
|
109
|
-
models
|
|
108
|
+
label = "ChatGPT";
|
|
109
|
+
models;
|
|
110
|
+
modelLabels;
|
|
110
111
|
supportsFastMode = true;
|
|
111
|
-
constructor(creds, env, now = () => Date.now()
|
|
112
|
+
constructor(creds, env, now = () => Date.now(),
|
|
113
|
+
// Modelli del CLI ufficiale installato; senza, il catalogo statico.
|
|
114
|
+
cliModels) {
|
|
112
115
|
this.creds = creds;
|
|
113
116
|
this.env = env;
|
|
114
117
|
this.now = now;
|
|
118
|
+
this.models = cliModels?.length ? cliModels.map((model) => model.id) : CHATGPT_MODELS;
|
|
119
|
+
if (cliModels?.length)
|
|
120
|
+
this.modelLabels = new Map(cliModels.map((model) => [model.id, model]));
|
|
115
121
|
}
|
|
116
122
|
async ensureToken(signal) {
|
|
117
123
|
if (!isExpired(this.creds, this.now()) || !this.creds.refreshToken) {
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import type { OAuthFlow } from "./openrouter-auth.js";
|
|
2
|
+
export type ManualTranslator = (italian: string, english: string, values?: Readonly<Record<string, string | number>>) => string;
|
|
2
3
|
export declare function normalizeAuthCode(raw: string): string;
|
|
3
4
|
export declare function isLikelyAuthCode(value: string): boolean;
|
|
4
5
|
export interface ManualInstructions {
|
|
5
6
|
readonly lines: readonly string[];
|
|
6
7
|
readonly authUrl: string;
|
|
7
8
|
}
|
|
8
|
-
export declare function buildManualInstructions(authUrl: string, reason?: string): ManualInstructions;
|
|
9
|
+
export declare function buildManualInstructions(authUrl: string, reason?: string, tr?: ManualTranslator): ManualInstructions;
|
|
9
10
|
export interface ManualFallbackIo {
|
|
10
11
|
print(line: string): void;
|
|
11
12
|
prompt(label: string): Promise<string | null>;
|
|
12
13
|
}
|
|
13
14
|
export interface ManualFallbackOptions {
|
|
15
|
+
readonly translate?: ManualTranslator;
|
|
14
16
|
readonly reason?: string;
|
|
15
17
|
readonly maxAttempts?: number;
|
|
16
18
|
readonly signal?: AbortSignal;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// (niente accesso diretto a stdin/stdout/console), e lo scambio del code arriva
|
|
14
14
|
// dall'OAuthFlow.complete gia' fornito da openrouter-auth.ts. Nessun side effect
|
|
15
15
|
// nascosto, niente dipendenze a runtime.
|
|
16
|
+
const defaultTranslation = (italian, _english, values) => italian.replace(/\{(\w+)\}/g, (match, key) => String(values?.[key] ?? match));
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Validazione / normalizzazione del code incollato
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -91,26 +92,24 @@ export function isLikelyAuthCode(value) {
|
|
|
91
92
|
// Costruisce le istruzioni per il percorso manuale. `reason` (opzionale) spiega
|
|
92
93
|
// perche' siamo qui (es. "porta 8976 occupata", "nessun browser disponibile"):
|
|
93
94
|
// se assente usiamo un messaggio neutro.
|
|
94
|
-
export function buildManualInstructions(authUrl, reason) {
|
|
95
|
+
export function buildManualInstructions(authUrl, reason, tr = defaultTranslation) {
|
|
95
96
|
const why = (reason ?? "").trim();
|
|
96
97
|
const lines = [];
|
|
97
|
-
lines.push("Login OpenRouter — modalita' manuale");
|
|
98
|
+
lines.push(tr("Login OpenRouter — modalita' manuale", "OpenRouter sign-in — manual mode"));
|
|
98
99
|
if (why) {
|
|
99
|
-
lines.push(
|
|
100
|
+
lines.push(tr("(callback automatico non disponibile: {reason})", "(automatic callback unavailable: {reason})", { reason: why }));
|
|
100
101
|
}
|
|
101
102
|
else {
|
|
102
|
-
lines.push("(callback automatico non disponibile: completiamo a mano)");
|
|
103
|
+
lines.push(tr("(callback automatico non disponibile: completiamo a mano)", "(automatic callback unavailable: complete sign-in manually)"));
|
|
103
104
|
}
|
|
104
105
|
lines.push("");
|
|
105
|
-
lines.push("1. Apri questo link nel browser (copialo se non e' cliccabile):");
|
|
106
|
+
lines.push(tr("1. Apri questo link nel browser (copialo se non e' cliccabile):", "1. Open this link in the browser (copy it if it is not clickable):"));
|
|
106
107
|
lines.push(` ${authUrl}`);
|
|
107
108
|
lines.push("");
|
|
108
|
-
lines.push("2. Autorizza l'accesso su OpenRouter.");
|
|
109
|
-
lines.push("3. Verrai rediretto a un indirizzo localhost che potrebbe non");
|
|
110
|
-
lines.push(
|
|
111
|
-
lines.push("
|
|
112
|
-
lines.push(" (oppure incolla l'intero URL: estraggo io il code).");
|
|
113
|
-
lines.push("5. Incollalo qui sotto e premi Invio.");
|
|
109
|
+
lines.push(tr("2. Autorizza l'accesso su OpenRouter.", "2. Authorize access on OpenRouter."));
|
|
110
|
+
lines.push(tr("3. Verrai rediretto a un indirizzo localhost che potrebbe non aprirsi: e' normale.", "3. You will be redirected to a localhost address that may not open: this is normal."));
|
|
111
|
+
lines.push(tr("4. Copia il parametro `code` dalla barra degli indirizzi (oppure incolla l'intero URL).", "4. Copy the `code` parameter from the address bar (or paste the entire URL)."));
|
|
112
|
+
lines.push(tr("5. Incollalo qui sotto e premi Invio.", "5. Paste it below and press Enter."));
|
|
114
113
|
return { lines, authUrl };
|
|
115
114
|
}
|
|
116
115
|
// Esegue il percorso di fallback manuale: mostra link+istruzioni, chiede il
|
|
@@ -118,15 +117,16 @@ export function buildManualInstructions(authUrl, reason) {
|
|
|
118
117
|
// input invalido o scambio fallito, fino a maxAttempts o annullamento.
|
|
119
118
|
export async function runManualCodeFallback(io, flow, opts = {}) {
|
|
120
119
|
const maxAttempts = Math.max(1, Math.floor(opts.maxAttempts ?? 3));
|
|
121
|
-
const
|
|
120
|
+
const tr = opts.translate ?? defaultTranslation;
|
|
121
|
+
const instructions = buildManualInstructions(flow.authUrl, opts.reason, tr);
|
|
122
122
|
for (const line of instructions.lines)
|
|
123
123
|
io.print(line);
|
|
124
124
|
let attempts = 0;
|
|
125
125
|
let lastError = "";
|
|
126
126
|
while (attempts < maxAttempts) {
|
|
127
127
|
const remaining = maxAttempts - attempts;
|
|
128
|
-
const hint = attempts === 0 ? "" :
|
|
129
|
-
const raw = await io.prompt(
|
|
128
|
+
const hint = attempts === 0 ? "" : tr(" (tentativi rimasti: {count})", " (attempts remaining: {count})", { count: remaining });
|
|
129
|
+
const raw = await io.prompt(tr("Incolla il code{hint}", "Paste the code{hint}", { hint }));
|
|
130
130
|
// Annullamento esplicito (Esc/EOF): usciamo subito senza consumare logica.
|
|
131
131
|
if (raw === null) {
|
|
132
132
|
return { ok: false, error: "cancelled", attempts };
|
|
@@ -134,9 +134,9 @@ export async function runManualCodeFallback(io, flow, opts = {}) {
|
|
|
134
134
|
attempts += 1;
|
|
135
135
|
const code = normalizeAuthCode(raw);
|
|
136
136
|
if (!isLikelyAuthCode(code)) {
|
|
137
|
-
lastError = "code non riconosciuto";
|
|
137
|
+
lastError = tr("code non riconosciuto", "unrecognized code");
|
|
138
138
|
if (attempts < maxAttempts) {
|
|
139
|
-
io.print("Input non valido: incolla il code (o l'intero URL del callback). Riprova.");
|
|
139
|
+
io.print(tr("Input non valido: incolla il code (o l'intero URL del callback). Riprova.", "Invalid input: paste the code (or the entire callback URL). Try again."));
|
|
140
140
|
}
|
|
141
141
|
continue;
|
|
142
142
|
}
|
|
@@ -148,14 +148,14 @@ export async function runManualCodeFallback(io, flow, opts = {}) {
|
|
|
148
148
|
result = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
149
149
|
}
|
|
150
150
|
if (result.ok && result.apiKey) {
|
|
151
|
-
io.print("Autenticazione completata: API key ottenuta e salvata.");
|
|
151
|
+
io.print(tr("Autenticazione completata: API key ottenuta e salvata.", "Authentication complete: API key obtained and saved."));
|
|
152
152
|
return { ok: true, apiKey: result.apiKey, attempts };
|
|
153
153
|
}
|
|
154
|
-
lastError = result.error ?? "scambio fallito";
|
|
154
|
+
lastError = result.error ?? tr("scambio fallito", "exchange failed");
|
|
155
155
|
if (attempts < maxAttempts) {
|
|
156
|
-
io.print(
|
|
156
|
+
io.print(tr("Scambio fallito: {error}. Verifica di aver copiato il code giusto e riprova.", "Exchange failed: {error}. Check that you copied the correct code and try again.", { error: lastError }));
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
io.print("Tentativi esauriti: login manuale non riuscito.");
|
|
159
|
+
io.print(tr("Tentativi esauriti: login manuale non riuscito.", "No attempts left: manual sign-in failed."));
|
|
160
160
|
return { ok: false, error: lastError || "exhausted", attempts };
|
|
161
161
|
}
|
|
@@ -3,6 +3,7 @@ import { AnthropicOAuthProvider } from "./anthropic-oauth.js";
|
|
|
3
3
|
import { OpenAIOAuthProvider } from "./openai-oauth.js";
|
|
4
4
|
import { OpenAICompatProvider } from "./openai-compat.js";
|
|
5
5
|
import { readClaudeCreds, readCodexCreds } from "./oauth-store.js";
|
|
6
|
+
import { cachedCliModels } from "./cli-models.js";
|
|
6
7
|
import { readOpenRouterCreds } from "./openrouter-auth.js";
|
|
7
8
|
import { PROVIDER_CATALOG } from "./catalog.js";
|
|
8
9
|
import { retryConfigFromEnv, withRetry } from "./retry.js";
|
|
@@ -202,11 +203,11 @@ export function defaultRegistry(env, opts = {}) {
|
|
|
202
203
|
if (opts.detectOAuth !== false) {
|
|
203
204
|
const claude = readClaudeCreds(env);
|
|
204
205
|
if (claude)
|
|
205
|
-
add(new AnthropicOAuthProvider(claude, env));
|
|
206
|
+
add(new AnthropicOAuthProvider(claude, env, undefined, cachedCliModels("claude", env)));
|
|
206
207
|
if (env["TALOS_DISABLE_CHATGPT_OAUTH"] !== "1") {
|
|
207
208
|
const codex = readCodexCreds(env);
|
|
208
209
|
if (codex)
|
|
209
|
-
add(new OpenAIOAuthProvider(codex, env));
|
|
210
|
+
add(new OpenAIOAuthProvider(codex, env, undefined, cachedCliModels("codex", env)));
|
|
210
211
|
}
|
|
211
212
|
}
|
|
212
213
|
return reg;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CompletionRequest, ModelTurn, PlanLimits, Provider } from "../core/types.js";
|
|
2
2
|
export interface RetryConfig {
|
|
3
|
+
readonly autoResumeSession?: boolean;
|
|
3
4
|
readonly maxRetries: number;
|
|
4
5
|
readonly baseDelayMs: number;
|
|
5
6
|
readonly timeoutMs: number;
|
|
@@ -25,12 +26,14 @@ export declare class RetryProvider implements Provider {
|
|
|
25
26
|
get id(): string;
|
|
26
27
|
get label(): string | undefined;
|
|
27
28
|
get models(): readonly string[];
|
|
29
|
+
get modelLabels(): Provider["modelLabels"];
|
|
28
30
|
get supportsFastMode(): boolean;
|
|
29
31
|
private get netMax();
|
|
30
32
|
private get netDelay();
|
|
31
33
|
private get overloadMaxAttempts();
|
|
32
34
|
private overloadDelay;
|
|
33
35
|
private waitForOverload;
|
|
36
|
+
private waitForSessionReset;
|
|
34
37
|
complete(req: CompletionRequest): Promise<ModelTurn>;
|
|
35
38
|
private backoff;
|
|
36
39
|
}
|
package/dist/providers/retry.js
CHANGED
|
@@ -194,6 +194,9 @@ export class RetryProvider {
|
|
|
194
194
|
get models() {
|
|
195
195
|
return this.inner.models;
|
|
196
196
|
}
|
|
197
|
+
get modelLabels() {
|
|
198
|
+
return this.inner.modelLabels;
|
|
199
|
+
}
|
|
197
200
|
get supportsFastMode() { return this.inner.supportsFastMode === true; }
|
|
198
201
|
get netMax() {
|
|
199
202
|
return this.cfg.networkMaxRetries ?? 0;
|
|
@@ -218,6 +221,31 @@ export class RetryProvider {
|
|
|
218
221
|
req.onActivity?.(`Modello sovraccarico · nuovo tentativo (${nextAttempt}/${this.overloadMaxAttempts}) · ${seconds}s`);
|
|
219
222
|
});
|
|
220
223
|
}
|
|
224
|
+
async waitForSessionReset(req, limits) {
|
|
225
|
+
if (this.cfg.autoResumeSession === false)
|
|
226
|
+
return false;
|
|
227
|
+
const rejected = rejectedQuotaWindows(limits);
|
|
228
|
+
const resetAt = quotaResetAt(limits);
|
|
229
|
+
// Never guess a reset or wait on credit/weekly exhaustion. Every rejected
|
|
230
|
+
// window must be the session window, with a future reset supplied by the provider.
|
|
231
|
+
const session = rejected.find(w => w.id === "5h");
|
|
232
|
+
// Claude also rejects the aggregate unified window when its 5h window
|
|
233
|
+
// is exhausted. It is not an independent weekly/credit limit.
|
|
234
|
+
if (!session || rejected.some(w => w.id !== "5h" && w.id !== "unified") || resetAt === undefined || resetAt <= Date.now())
|
|
235
|
+
return false;
|
|
236
|
+
if (req.signal.aborted)
|
|
237
|
+
return true;
|
|
238
|
+
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
239
|
+
const time = new Date(resetAt).toLocaleTimeString("it-IT", { hour: "2-digit", minute: "2-digit", timeZone: zone });
|
|
240
|
+
req.onActivity?.(`Limite sessione di 5 ore raggiunto · reset alle ${time} (${zone}) · riprendo automaticamente al reset · Esc per annullare`);
|
|
241
|
+
// Short slices avoid timer overflow and remain responsive to clock changes.
|
|
242
|
+
while (!req.signal.aborted && Date.now() < resetAt) {
|
|
243
|
+
await delay(Math.min(60_000, resetAt - Date.now()), req.signal);
|
|
244
|
+
}
|
|
245
|
+
if (!req.signal.aborted)
|
|
246
|
+
req.onActivity?.("Limite sessione reimpostato · riprendo automaticamente il lavoro");
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
221
249
|
async complete(req) {
|
|
222
250
|
let netAttempt = 0;
|
|
223
251
|
let genAttempt = 0;
|
|
@@ -248,13 +276,26 @@ export class RetryProvider {
|
|
|
248
276
|
delivered = true;
|
|
249
277
|
req.onToolDelta(event);
|
|
250
278
|
} } : {}),
|
|
279
|
+
...(req.onReasoning ? { onReasoning: (chunk) => {
|
|
280
|
+
if (!active || tm.signal.aborted)
|
|
281
|
+
return;
|
|
282
|
+
if (chunk)
|
|
283
|
+
delivered = true;
|
|
284
|
+
req.onReasoning(chunk);
|
|
285
|
+
} } : {}),
|
|
286
|
+
...(req.onActivity ? { onActivity: (label) => {
|
|
287
|
+
if (active && !tm.signal.aborted)
|
|
288
|
+
req.onActivity(label);
|
|
289
|
+
} } : {}),
|
|
251
290
|
});
|
|
252
291
|
active = false;
|
|
253
292
|
tm.cleanup();
|
|
254
293
|
if (result.stopReason === "error") {
|
|
255
|
-
// 1.
|
|
294
|
+
// 1. Sessione 5h: ripresa al reset solo se lo stream non e' iniziato.
|
|
256
295
|
if (isQuotaError(result.text) || rejectedQuotaWindows(result.limits).length > 0) {
|
|
257
296
|
const limits = quotaLimitsFromError(this.id, result.text, result.limits);
|
|
297
|
+
if (!delivered && await this.waitForSessionReset(req, limits))
|
|
298
|
+
continue;
|
|
258
299
|
return { ...result, toolCalls: [], text: quotaMessage(this.id, result.text, limits), ...(limits ? { limits } : {}) };
|
|
259
300
|
}
|
|
260
301
|
if (delivered)
|
|
@@ -301,6 +342,8 @@ export class RetryProvider {
|
|
|
301
342
|
const thrownText = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
302
343
|
if (!userAbort && isQuotaError(thrownText)) {
|
|
303
344
|
const limits = quotaLimitsFromError(this.id, thrownText);
|
|
345
|
+
if (!delivered && await this.waitForSessionReset(req, limits))
|
|
346
|
+
continue;
|
|
304
347
|
return { text: quotaMessage(this.id, thrownText, limits), toolCalls: [], stopReason: "error", ...(limits ? { limits } : {}) };
|
|
305
348
|
}
|
|
306
349
|
if (delivered)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type Direzione = "ev" | "cmd";
|
|
2
|
+
export declare class CifraturaError extends Error {
|
|
3
|
+
readonly code: "chiave" | "corpo" | "autenticazione";
|
|
4
|
+
constructor(code: "chiave" | "corpo" | "autenticazione");
|
|
5
|
+
}
|
|
6
|
+
export declare function nuovaChiave(): Promise<{
|
|
7
|
+
readonly chiave: CryptoKey;
|
|
8
|
+
readonly testo: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function importaChiave(testo: string): Promise<CryptoKey>;
|
|
11
|
+
export declare function cifra(chiave: CryptoKey, sessione: string, direzione: Direzione, dati: unknown): Promise<string>;
|
|
12
|
+
export declare function decifra(chiave: CryptoKey, sessione: string, direzione: Direzione, b: string): Promise<unknown>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Cifratura fra Talos e i dispositivi abbinati (telefono, web): AES-256-GCM.
|
|
2
|
+
//
|
|
3
|
+
// La chiave nasce qui, una per sessione, e arriva al telefono solo dentro il
|
|
4
|
+
// link di abbinamento, dopo il "#": il browser non manda mai quella parte a
|
|
5
|
+
// nessun server. Relay, Redis, Kafka e Flink vedono solo testo cifrato.
|
|
6
|
+
//
|
|
7
|
+
// Formato del campo `b`: base64url(iv 12 byte || testo cifrato con tag).
|
|
8
|
+
// I dati aggiuntivi autenticati legano ogni messaggio alla sessione e alla
|
|
9
|
+
// direzione: il relay non puo' spostare un evento in un'altra sessione ne'
|
|
10
|
+
// far passare un evento di Talos per un comando del telefono.
|
|
11
|
+
const TE = new TextEncoder();
|
|
12
|
+
const TD = new TextDecoder();
|
|
13
|
+
const VERSIONE = "vascend-relay/1";
|
|
14
|
+
export class CifraturaError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code) {
|
|
17
|
+
super(code);
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function subtle() {
|
|
22
|
+
const s = globalThis.crypto?.subtle;
|
|
23
|
+
if (!s)
|
|
24
|
+
throw new CifraturaError("chiave");
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
function aad(sessione, direzione) {
|
|
28
|
+
return TE.encode(`${VERSIONE}|${sessione}|${direzione}`);
|
|
29
|
+
}
|
|
30
|
+
// Chiave nuova e la sua forma da mettere nel link (base64url, 43 caratteri).
|
|
31
|
+
export async function nuovaChiave() {
|
|
32
|
+
const grezza = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
33
|
+
const chiave = await subtle().importKey("raw", grezza, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
34
|
+
return { chiave, testo: Buffer.from(grezza).toString("base64url") };
|
|
35
|
+
}
|
|
36
|
+
export async function importaChiave(testo) {
|
|
37
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(testo))
|
|
38
|
+
throw new CifraturaError("chiave");
|
|
39
|
+
return subtle().importKey("raw", Buffer.from(testo, "base64url"), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
40
|
+
}
|
|
41
|
+
export async function cifra(chiave, sessione, direzione, dati) {
|
|
42
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
43
|
+
const ct = new Uint8Array(await subtle().encrypt({ name: "AES-GCM", iv, additionalData: aad(sessione, direzione) }, chiave, TE.encode(JSON.stringify(dati))));
|
|
44
|
+
const out = new Uint8Array(12 + ct.length);
|
|
45
|
+
out.set(iv);
|
|
46
|
+
out.set(ct, 12);
|
|
47
|
+
return Buffer.from(out).toString("base64url");
|
|
48
|
+
}
|
|
49
|
+
export async function decifra(chiave, sessione, direzione, b) {
|
|
50
|
+
if (typeof b !== "string" || b.length < 40 || !/^[A-Za-z0-9_-]+$/.test(b))
|
|
51
|
+
throw new CifraturaError("corpo");
|
|
52
|
+
const raw = Buffer.from(b, "base64url");
|
|
53
|
+
let chiaro;
|
|
54
|
+
try {
|
|
55
|
+
chiaro = await subtle().decrypt({ name: "AES-GCM", iv: raw.subarray(0, 12), additionalData: aad(sessione, direzione) }, chiave, raw.subarray(12));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new CifraturaError("autenticazione");
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(TD.decode(chiaro));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new CifraturaError("corpo");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentEvent } from "../core/types.js";
|
|
2
|
+
import { type StatoRelay } from "./relay-client.js";
|
|
3
|
+
import { SessioneRemota, type ManiRemote } from "./sessione-remota.js";
|
|
4
|
+
export { RelayError } from "./relay-client.js";
|
|
5
|
+
export type { StatoRelay } from "./relay-client.js";
|
|
6
|
+
export declare function collegaMani(m: ManiRemote | null): void;
|
|
7
|
+
export declare function sessioneRemota(): SessioneRemota | null;
|
|
8
|
+
export declare function avviaRemoto(env: NodeJS.ProcessEnv, cwd: string, eventi?: {
|
|
9
|
+
readonly onStato?: (stato: StatoRelay, motivo?: string) => void;
|
|
10
|
+
readonly onPresenza?: (n: number) => void;
|
|
11
|
+
}): Promise<SessioneRemota>;
|
|
12
|
+
export declare function fermaRemoto(): Promise<boolean>;
|
|
13
|
+
export declare function osservaEvento(event: AgentEvent): void;
|
|
14
|
+
export declare function messaggioUtente(testo: string): void;
|
|
15
|
+
export declare function permessoConGara(azione: string, dettaglio: string, signal: AbortSignal, tui: (signal: AbortSignal) => Promise<boolean>): Promise<boolean>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Controllo remoto di Talos: una sessione per processo, accesa con /remote.
|
|
2
|
+
// La TUI chiama queste funzioni nei suoi punti di passaggio (eventi
|
|
3
|
+
// dell'agente, messaggi dell'utente, permessi): a controllo spento non fanno
|
|
4
|
+
// niente e non costano niente.
|
|
5
|
+
import { RELAY_RESOURCE, tokenRisorsa } from "../tui/vascend-id.js";
|
|
6
|
+
import { RelayError, relayOf } from "./relay-client.js";
|
|
7
|
+
import { SessioneRemota } from "./sessione-remota.js";
|
|
8
|
+
export { RelayError } from "./relay-client.js";
|
|
9
|
+
let mani = null;
|
|
10
|
+
let attiva = null;
|
|
11
|
+
let avvio = null;
|
|
12
|
+
// La TUI si presenta una volta: senza mani (input classico) il remoto non parte.
|
|
13
|
+
export function collegaMani(m) { mani = m; }
|
|
14
|
+
export function sessioneRemota() { return attiva; }
|
|
15
|
+
export async function avviaRemoto(env, cwd, eventi = {}) {
|
|
16
|
+
if (attiva)
|
|
17
|
+
return attiva;
|
|
18
|
+
if (avvio)
|
|
19
|
+
return avvio;
|
|
20
|
+
if (!mani)
|
|
21
|
+
throw new RelayError("risposta");
|
|
22
|
+
const token = async () => {
|
|
23
|
+
const esito = await tokenRisorsa(env, RELAY_RESOURCE);
|
|
24
|
+
if (esito.token)
|
|
25
|
+
return esito.token;
|
|
26
|
+
throw new RelayError(esito.errore === "rete" ? "rete" : "token");
|
|
27
|
+
};
|
|
28
|
+
avvio = SessioneRemota.avvia({
|
|
29
|
+
base: relayOf(env),
|
|
30
|
+
cwd,
|
|
31
|
+
mani,
|
|
32
|
+
token,
|
|
33
|
+
onStato: (stato, motivo) => {
|
|
34
|
+
// Chiusa dal relay (altro dispositivo, piano scaduto): il remoto si spegne.
|
|
35
|
+
if (stato === "chiuso" && attiva)
|
|
36
|
+
attiva = null;
|
|
37
|
+
eventi.onStato?.(stato, motivo);
|
|
38
|
+
},
|
|
39
|
+
onPresenza: eventi.onPresenza,
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
attiva = await avvio;
|
|
43
|
+
return attiva;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
avvio = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function fermaRemoto() {
|
|
50
|
+
const s = attiva;
|
|
51
|
+
attiva = null;
|
|
52
|
+
if (!s)
|
|
53
|
+
return false;
|
|
54
|
+
await s.chiudi();
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
export function osservaEvento(event) {
|
|
58
|
+
attiva?.osserva(event);
|
|
59
|
+
}
|
|
60
|
+
export function messaggioUtente(testo) {
|
|
61
|
+
attiva?.messaggioUtente(testo);
|
|
62
|
+
}
|
|
63
|
+
export function permessoConGara(azione, dettaglio, signal, tui) {
|
|
64
|
+
return attiva ? attiva.permessoConGara(azione, dettaglio, signal, tui) : tui(signal);
|
|
65
|
+
}
|