vault-go 0.11.0 → 0.13.0

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
@@ -89,6 +89,9 @@ bunx --bun vault-go@latest setup
89
89
  bunx --bun vault-go@latest setup --clients codex,claude,copilot
90
90
  bunx --bun vault-go@latest login --force
91
91
  bunx --bun vault-go@latest serve
92
+ bunx --bun vault-go@latest engine list
93
+ bunx --bun vault-go@latest engine use openrouter
94
+ bunx --bun vault-go@latest engine key openrouter
92
95
  ```
93
96
 
94
97
  As credenciais são gravadas em `~/.memvault/auth.json` com permissão `0600`;
@@ -159,6 +162,11 @@ limitados.
159
162
  - `vault_go_knowledge_query`: recupera contexto fundamentado e citações dentro da base.
160
163
  - `vault_go_knowledge_delete`: remove a base e preserva as memórias originais.
161
164
  - `vault_go_forget`: exclusão explícita de uma memória.
165
+ - `vault_go_engines`: lista motores de contexto (Vault AI, Claude, OpenRouter, Gemini, ChatGPT/Codex) sem revelar chaves.
166
+ - `vault_go_engine_select`: escolhe o motor local (`vault-ai-resume`, `claude-subscription`, `openai-subscription`, `openrouter`, `gemini`).
167
+ - `vault_go_generate_context`: gera título, fatos e conceitos com o motor escolhido; não persiste memória sozinho.
168
+
169
+ O seletor de motor segue o mesmo modelo do claude-mem: Claude (CLI da assinatura), OpenRouter (chave), Gemini (chave) ou o Vault AI Resume da conta. Grave chaves com `vault-go engine key openrouter|gemini` ou no painel local; as ferramentas MCP não aceitam segredos.
162
170
 
163
171
  O recurso `vault-go://status` fornece o resumo sanitizado em JSON.
164
172
 
@@ -241,6 +249,8 @@ local, atividade MCP recente, projetos e memórias consultados na nuvem.
241
249
  Atualiza a interface a cada 5 segundos enquanto visível e consulta a nuvem
242
250
  a cada 15 segundos. Sem conexão, conserva o último cache da mesma conta.
243
251
 
252
+ Abra http://localhost:38850 e clique em **Conectar ao Vault** para entrar pelo navegador. A senha permanece no portal; o painel libera a sessão somente após confirmar o retorno OAuth com PKCE. A tela oferece próximos passos para configurar clientes e registrar a primeira memória. A logo e as cores são as mesmas do portal.
253
+
244
254
  O serviço escuta exclusivamente em 127.0.0.1:38850. `local open` verifica
245
255
  uma prova criptográfica do serviço e abre uma sessão privada no navegador;
246
256
  o segredo local é removido do fragmento imediatamente e trocado por cookie
@@ -259,3 +269,32 @@ pelo MCP Vault, sem argumentos, consultas ou credenciais. Não captura toda a
259
269
  atividade da máquina nem importa automaticamente o banco do claude-mem em
260
270
  37701. A sincronização deste painel é de leitura da nuvem; as ferramentas
261
271
  MCP continuam responsáveis pelas gravações na API.
272
+
273
+ ### Sessões, dispositivos e proteção local
274
+
275
+ **Sair** encerra a sessão desta página e cancela suas prévias de contexto.
276
+ **Desconectar dispositivo** pede confirmação, revoga as credenciais do agente
277
+ na nuvem e remove a autenticação e o cache local após sucesso. Os arquivos
278
+ pessoais permanecem intactos. Para reconectar, entre novamente pelo navegador.
279
+ O portal mostra último contato, permissões e permite revogar cada agente.
280
+
281
+ O cache e a credencial de monitoramento usam AES-256-GCM. No macOS, a chave
282
+ fica no Keychain; nos demais sistemas, em arquivo privado `0600`. Os tokens
283
+ MCP existentes continuam em arquivo privado. O painel informa essas diferenças:
284
+ cache cifrado não significa que todas as memórias sejam criptografadas ponta a
285
+ ponta. O cofre de arquivos e o canal do terminal têm seus próprios protocolos.
286
+
287
+ ### Motor de contexto
288
+
289
+ Escolha **Vault AI Resume**, **Claude Subscription** ou **OpenAI Subscription**
290
+ no painel local. A disponibilidade é verificada antes da geração. Claude usa
291
+ `claude auth login`; OpenAI usa `codex login` com uma conta ChatGPT. O agente
292
+ não pede nem copia credenciais desses provedores. O modelo gerenciado depende
293
+ de configuração e quota no servidor Vault.
294
+
295
+ A geração exige texto e consentimento explícito, usa diretório temporário,
296
+ limites de entrada/saída e timeout, e permite cancelamento. CLIs recebem apenas
297
+ o texto informado, com ferramentas/configurações locais desativadas conforme
298
+ os recursos suportados. O resultado é uma prévia privada da sessão, disponível
299
+ para copiar; não é salvo automaticamente como memória. Os planos e limites do
300
+ provedor escolhido se aplicam. Cancelar não garante estorno de uso já iniciado.
Binary file
package/dist/activity.js CHANGED
@@ -1,29 +1,44 @@
1
- import { appendFileSync, chmodSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { randomUUID } from 'node:crypto';
1
+ import { appendFileSync, chmodSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
4
  export function recordActivity(home, operation, ok, durationMs) {
5
5
  try {
6
6
  mkdirSync(home, { recursive: true, mode: 0o700 });
7
- const file = join(home, 'local-activity.jsonl');
7
+ const file = join(home, "local-activity.jsonl");
8
8
  // Bounded journal contains metadata only: never arguments, content or credentials.
9
9
  try {
10
10
  if (statSync(file).size > 2 * 1024 * 1024)
11
- writeFileSync(file, '', { mode: 0o600 });
11
+ writeFileSync(file, "", { mode: 0o600 });
12
12
  }
13
13
  catch { }
14
- appendFileSync(file, JSON.stringify({ id: randomUUID(), time: new Date().toISOString(), operation: operation.slice(0, 180), ok, durationMs: Math.round(durationMs) }) + '\n', { mode: 0o600 });
14
+ appendFileSync(file, JSON.stringify({
15
+ id: randomUUID(),
16
+ time: new Date().toISOString(),
17
+ operation: operation.slice(0, 180),
18
+ ok,
19
+ durationMs: Math.round(durationMs),
20
+ }) + "\n", { mode: 0o600 });
15
21
  chmodSync(file, 0o600);
16
22
  }
17
- catch { /* Observability must never prevent an MCP operation. */ }
23
+ catch {
24
+ /* Observability must never prevent an MCP operation. */
25
+ }
18
26
  }
19
27
  export function readActivity(home) {
20
28
  try {
21
- return readFileSync(join(home, 'local-activity.jsonl'), 'utf8').split('\n').filter(Boolean).slice(-100).flatMap(line => { try {
22
- return [JSON.parse(line)];
23
- }
24
- catch {
25
- return [];
26
- } }).reverse();
29
+ return readFileSync(join(home, "local-activity.jsonl"), "utf8")
30
+ .split("\n")
31
+ .filter(Boolean)
32
+ .slice(-100)
33
+ .flatMap((line) => {
34
+ try {
35
+ return [JSON.parse(line)];
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ })
41
+ .reverse();
27
42
  }
28
43
  catch {
29
44
  return [];
package/dist/auth.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare function validateExistingAuthentication(home: string, fetcher?: F
10
10
  export declare function openVaultBrowser(url: string): Promise<void>;
11
11
  export declare function authenticateWithBrowser(home: string, options?: {
12
12
  locale?: Locale;
13
+ callbackHtml?: string;
14
+ signal?: AbortSignal;
13
15
  fetcher?: Fetcher;
14
16
  openBrowser?: (url: string) => Promise<void>;
15
17
  timeoutMs?: number;
package/dist/auth.js CHANGED
@@ -128,7 +128,7 @@ export async function authenticateWithBrowser(home, options = {}) {
128
128
  const server = createServer((req, res) => {
129
129
  res.setHeader('Cache-Control', 'no-store');
130
130
  res.setHeader('Referrer-Policy', 'no-referrer');
131
- res.setHeader('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'");
131
+ res.setHeader('Content-Security-Policy', "default-src 'none'; img-src data:; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'");
132
132
  let url;
133
133
  try {
134
134
  url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
@@ -160,11 +160,15 @@ export async function authenticateWithBrowser(home, options = {}) {
160
160
  }
161
161
  accepted = true;
162
162
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
163
- res.end(callbackPage(locale));
163
+ res.end(options.callbackHtml ?? callbackPage(locale));
164
164
  claim?.(code);
165
165
  });
166
166
  let timer;
167
+ const abort = () => rejectClaim?.(new Error(t.denied));
168
+ options.signal?.addEventListener('abort', abort, { once: true });
167
169
  try {
170
+ if (options.signal?.aborted)
171
+ throw new Error(t.denied);
168
172
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
169
173
  const address = server.address();
170
174
  if (!address || typeof address === 'string')
@@ -197,6 +201,7 @@ export async function authenticateWithBrowser(home, options = {}) {
197
201
  return { email: account.email, method: 'browser' };
198
202
  }
199
203
  finally {
204
+ options.signal?.removeEventListener('abort', abort);
200
205
  if (timer)
201
206
  clearTimeout(timer);
202
207
  server.closeAllConnections();
@@ -1,3 +1,3 @@
1
- import { type Locale } from './locale.js';
1
+ import { type Locale } from "./locale.js";
2
2
  /** All interpolated content is from the static locale catalog, never the callback URL. */
3
- export declare function callbackPage(locale: Locale): string;
3
+ export declare function callbackPage(locale: Locale, dashboardUrl?: string): string;
@@ -1,8 +1,33 @@
1
- import { copy } from './locale.js';
1
+ import { readFileSync } from "node:fs";
2
+ import { copy } from "./locale.js";
2
3
  /** All interpolated content is from the static locale catalog, never the callback URL. */
3
- export function callbackPage(locale) {
4
- const t = copy[locale];
4
+ export function callbackPage(locale, dashboardUrl) {
5
+ const base = copy[locale];
6
+ const local = {
7
+ pt: {
8
+ body: "Sua conexão está sendo concluída. Volte ao painel local para acompanhar suas memórias.",
9
+ step2: "Retorno seguro recebido nesta máquina",
10
+ step3: "Abra o painel para acompanhar a conexão",
11
+ back: "Voltar ao painel local",
12
+ },
13
+ en: {
14
+ body: "Your connection is being completed. Return to the local dashboard to follow your memories.",
15
+ step2: "Secure callback received on this machine",
16
+ step3: "Open the dashboard to follow the connection",
17
+ back: "Return to local dashboard",
18
+ },
19
+ es: {
20
+ body: "Tu conexión se está completando. Vuelve al panel local para consultar tus memorias.",
21
+ step2: "Retorno seguro recibido en esta máquina",
22
+ step3: "Abre el panel para consultar la conexión",
23
+ back: "Volver al panel local",
24
+ },
25
+ };
26
+ const t = dashboardUrl ? { ...base, ...local[locale] } : base;
27
+ if (dashboardUrl && !/^http:\/\/localhost:\d+\/$/.test(dashboardUrl))
28
+ throw new Error("Invalid dashboard callback");
29
+ const logo = readFileSync(new URL("../assets/vault-logo-glass.png", import.meta.url)).toString("base64");
5
30
  return `<!doctype html><html lang="${locale}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>AI Vault Memory · ${t.received}</title><style>
6
31
  *{box-sizing:border-box}body{margin:0;min-height:100svh;display:grid;place-items:center;padding:24px;color:#f1f3fc;background:radial-gradient(ellipse at 20% 10%,#38305a60,transparent 50%),radial-gradient(ellipse at 90% 90%,#20394c40,transparent 55%),#080b12;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}.panel{width:min(100%,520px);padding:44px;border:1px solid #cdd8ff26;border-radius:32px;background:linear-gradient(135deg,#272c3edb,#131724ed);box-shadow:0 32px 100px #0008,inset 0 1px #ffffff10;animation:arrive .6s cubic-bezier(.2,.8,.2,1) both}.mark{display:block;margin:0 auto 28px;width:92px;height:92px;filter:drop-shadow(0 8px 25px #9990ff40)}.ribbon{stroke-dasharray:180;animation:draw 1.1s ease both}.ribbon:nth-child(2){animation-delay:.12s}.ribbon:nth-child(3){animation-delay:.24s}.brand{text-align:center;color:#b9b6ff;font-size:10px;letter-spacing:.22em;font-weight:650}h1{text-align:center;font-size:34px;letter-spacing:-.045em;line-height:1.1;margin:16px 0}.intro{text-align:center;color:#acb4cb;font-size:15px;line-height:1.7;margin:0 0 30px}.steps{list-style:none;padding:0;margin:0;border-top:1px solid #cdd8ff20}.steps li{display:flex;align-items:center;gap:14px;padding:15px 0;border-bottom:1px solid #cdd8ff15;font-size:13px;animation:arrive .5s both}.steps li:nth-child(2){animation-delay:.15s}.steps li:nth-child(3){animation-delay:.3s}.step{display:grid;place-items:center;flex-shrink:0;width:28px;height:28px;border:1px solid #b9b6ff40;background:#b9b6ff12;border-radius:9px;color:#c8c5ff;font-size:11px}.return{margin:26px 0 0;padding:16px 18px;background:#b9b6ff14;border:1px solid #b9b6ff30;border-radius:16px;color:#d4d1ff;font-size:14px;line-height:1.6}.privacy{margin:22px 0 0;color:#929db6;font-size:11px;line-height:1.7;text-align:center}@keyframes arrive{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes draw{from{stroke-dashoffset:180;opacity:0}to{stroke-dashoffset:0;opacity:1}}@media(max-width:480px){.panel{padding:30px 24px}h1{font-size:30px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important}}@media(forced-colors:active){.panel,.step,.return{border:1px solid CanvasText}svg{stroke:CanvasText}}
7
- </style></head><body><main class="panel"><svg class="mark" viewBox="0 0 100 100" fill="none" aria-hidden="true"><defs><linearGradient id="glass" x1="10" y1="10" x2="85" y2="90" gradientUnits="userSpaceOnUse"><stop stop-color="#e2edff"/><stop offset=".48" stop-color="#8b88df"/><stop offset="1" stop-color="#c4e6ff"/></linearGradient></defs><g stroke="url(#glass)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"><path class="ribbon" d="M12 20 50 84 88 20"/><path class="ribbon" d="M25 20 50 63 75 20"/><path class="ribbon" d="M38 20 50 41 62 20"/></g></svg><p class="brand">AI VAULT MEMORY</p><h1>${t.received}</h1><p class="intro">${t.body}</p><ol class="steps"><li><span class="step">01</span>${t.step1}</li><li><span class="step">02</span>${t.step2}</li><li><span class="step">03</span>${t.step3}</li></ol><p class="return">↗ ${t.back}</p><p class="privacy">${t.privacy}</p></main></body></html>`;
32
+ </style></head><body><main class="panel"><img class="mark" src="data:image/png;base64,${logo}" alt="AI Vault Memory"><p class="brand">AI VAULT MEMORY</p><h1>${t.received}</h1><p class="intro">${t.body}</p><ol class="steps"><li><span class="step">01</span>${t.step1}</li><li><span class="step">02</span>${t.step2}</li><li><span class="step">03</span>${t.step3}</li></ol><div class="return">${dashboardUrl ? `<a style="color:inherit" href="${dashboardUrl}">↗ ${t.back}</a>` : `↗ ${t.back}`}</div><p class="privacy">${t.privacy}</p></main></body></html>`;
8
33
  }
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type McpClient } from './installer.js';
2
- export type CliMode = 'help' | 'version' | 'setup' | 'login' | 'serve' | 'local' | 'invalid';
2
+ export type CliMode = 'help' | 'version' | 'setup' | 'login' | 'serve' | 'local' | 'engine' | 'invalid';
3
3
  export declare function resolveCliMode(argument: string | undefined, stdinIsTTY: boolean, stderrIsTTY: boolean): CliMode;
4
4
  export declare function parseNumberedClients(value: string): McpClient[];
5
5
  export declare function cliArguments(args: string[]): {
@@ -10,3 +10,4 @@ export declare function runLocal(args: string[]): Promise<number>;
10
10
  export declare function runLogin(args: string[]): Promise<number>;
11
11
  export declare function runSetup(args: string[]): Promise<number>;
12
12
  export declare function helpText(version: string): string;
13
+ export declare function runEngine(args: string[]): Promise<number>;
package/dist/cli.js CHANGED
@@ -2,6 +2,8 @@ import { authenticateWithApiKey, authenticateWithBrowser, validateExistingAuthen
2
2
  import { vaultHome } from './config.js';
3
3
  import { detectMcpClients, installMcpClient, MCP_CLIENTS, parseClientSelection, } from './installer.js';
4
4
  import { promptSecret, promptText } from './prompt.js';
5
+ import { CONTEXT_ENGINE_IDS, isApiEngine, listContextEngines, saveEngineKey, selectContextEngine, } from './context-engines.js';
6
+ import { VaultCloudClient } from './cloud.js';
5
7
  import { copy, resolveLocale } from './locale.js';
6
8
  import { installLocal, localStatus, localUrl, openLocal, startLocal, stopLocal, uninstallLocal } from './local-install.js';
7
9
  export function resolveCliMode(argument, stdinIsTTY, stderrIsTTY) {
@@ -17,6 +19,8 @@ export function resolveCliMode(argument, stdinIsTTY, stderrIsTTY) {
17
19
  return 'serve';
18
20
  if (argument === 'local')
19
21
  return 'local';
22
+ if (argument === 'engine')
23
+ return 'engine';
20
24
  if (argument === undefined)
21
25
  return stdinIsTTY && stderrIsTTY ? 'setup' : 'serve';
22
26
  return 'invalid';
@@ -221,6 +225,10 @@ Uso:
221
225
  vault-go local [install|start|open|status|stop|uninstall|serve]
222
226
  Painel local em http://localhost:38850; install ativa início automático no macOS.
223
227
 
228
+ vault-go engine list|use <motor>|key openrouter|gemini
229
+ Escolhe o motor de contexto: vault-ai-resume, claude-subscription,
230
+ openai-subscription, openrouter ou gemini. key grava a chave em prompt oculto.
231
+
224
232
  --lang pt|en|es
225
233
  Idioma do assistente (padrão: idioma do sistema).
226
234
 
@@ -231,5 +239,53 @@ Variáveis:
231
239
  VAULT_GO_HOME Diretório de configuração (padrão: ~/.memvault)
232
240
  MEMVAULT_CONFIG_DIR Diretório compartilhado com o agente MemVault
233
241
  VAULT_GO_API_URL Endpoint alternativo da API Vault
242
+ VAULT_GO_OPENROUTER_API_KEY / OPENROUTER_API_KEY
243
+ VAULT_GO_GEMINI_API_KEY / GEMINI_API_KEY / GOOGLE_API_KEY
234
244
  `;
235
245
  }
246
+ async function managedStatus(home) {
247
+ try {
248
+ const status = (await new VaultCloudClient(home).observerStatus());
249
+ return {
250
+ available: status.configured === true && status.entitlement?.allowed === true,
251
+ reason: status.configured
252
+ ? 'Provider readiness is checked when generating.'
253
+ : 'Managed observer is not configured.',
254
+ billing: 'Uses the Vault observer allowance.',
255
+ };
256
+ }
257
+ catch {
258
+ return {
259
+ available: false,
260
+ reason: 'Vault AI Resume is not configured or unavailable for this account.',
261
+ billing: 'Uses the Vault observer allowance.',
262
+ };
263
+ }
264
+ }
265
+ export async function runEngine(args) {
266
+ const home = vaultHome();
267
+ const positional = args.filter((item) => !item.startsWith('--'));
268
+ const action = positional[0] ?? 'list';
269
+ if (action === 'list') {
270
+ const listed = await listContextEngines(home, await managedStatus(home));
271
+ process.stdout.write(`${JSON.stringify(listed, null, 2)}\n`);
272
+ return 0;
273
+ }
274
+ if (action === 'use') {
275
+ const engine = positional[1];
276
+ if (!engine || !CONTEXT_ENGINE_IDS.includes(engine)) {
277
+ throw new Error(`Motor inválido. Use: ${CONTEXT_ENGINE_IDS.join(', ')}`);
278
+ }
279
+ process.stdout.write(`${selectContextEngine(home, engine)}\n`);
280
+ return 0;
281
+ }
282
+ if (action === 'key') {
283
+ const engine = positional[1];
284
+ if (!isApiEngine(engine))
285
+ throw new Error('Use vault-go engine key openrouter|gemini');
286
+ saveEngineKey(home, engine, await promptSecret(`Chave ${engine}: `));
287
+ process.stderr.write(`Chave ${engine} gravada com permissão privada.\n`);
288
+ return 0;
289
+ }
290
+ throw new Error('vault-go engine list|use <motor>|key openrouter|gemini');
291
+ }
package/dist/cloud.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export interface VaultMemoryApi {
2
+ observerStatus?(): Promise<unknown>;
3
+ observerGenerate?(text: string, signal?: AbortSignal): Promise<unknown>;
2
4
  health(): Promise<unknown>;
3
5
  projects(): Promise<unknown>;
4
6
  createProject(input: Record<string, unknown>): Promise<unknown>;
@@ -35,6 +37,11 @@ export declare class VaultCloudClient implements VaultMemoryApi {
35
37
  private readonly journal;
36
38
  constructor(home?: string, cloudFetch?: typeof fetch, journal?: boolean);
37
39
  health(): Promise<unknown>;
40
+ observerStatus(): Promise<unknown>;
41
+ observerGenerate(text: string, signal?: AbortSignal): Promise<unknown>;
42
+ registerDevice(input: Record<string, unknown>): Promise<unknown>;
43
+ rotateDevice(id: string): Promise<unknown>;
44
+ deviceAction(id: string, token: string, action: 'heartbeat' | 'disconnect', body?: Record<string, unknown>): Promise<unknown>;
38
45
  projects(): Promise<unknown>;
39
46
  createProject(input: Record<string, unknown>): Promise<unknown>;
40
47
  startSession(input: Record<string, unknown>): Promise<unknown>;
@@ -68,3 +75,7 @@ export declare class VaultCloudClient implements VaultMemoryApi {
68
75
  private request;
69
76
  private performRequest;
70
77
  }
78
+ export declare class VaultCloudError extends Error {
79
+ readonly status: number;
80
+ constructor(status: number, message: string);
81
+ }
package/dist/cloud.js CHANGED
@@ -12,6 +12,26 @@ export class VaultCloudClient {
12
12
  async health() {
13
13
  return this.request('/health', { authenticated: false });
14
14
  }
15
+ async observerStatus() { return this.request('/memory/observer/status'); }
16
+ async observerGenerate(text, signal) {
17
+ return this.request('/memory/observer/generate', { method: 'POST', body: { text }, timeoutMs: 60_000, ...(signal ? { signal } : {}) });
18
+ }
19
+ async registerDevice(input) {
20
+ return this.request('/devices/register', { method: 'POST', body: input });
21
+ }
22
+ async rotateDevice(id) {
23
+ if (!/^[a-f0-9-]{36}$/.test(id))
24
+ throw new Error('Invalid device identifier.');
25
+ return this.request(`/devices/${id}/rotate`, { method: 'POST', body: {} });
26
+ }
27
+ async deviceAction(id, token, action, body = {}) {
28
+ if (!/^[a-f0-9-]{36}$/.test(id) || !/^vg_device_[a-f0-9]{64}$/.test(token))
29
+ throw new Error('Invalid device credential.');
30
+ const response = await this.cloudFetch(this.endpoint(`/devices/${id}/${action}`), { method: 'POST', redirect: 'error', headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(10_000) });
31
+ if (!response.ok)
32
+ throw new VaultCloudError(response.status, 'device_request_failed');
33
+ return response.status === 204 ? null : response.json();
34
+ }
15
35
  async projects() {
16
36
  return this.request('/memory/projects');
17
37
  }
@@ -146,6 +166,12 @@ export class VaultCloudClient {
146
166
  throw new Error('Sessão expirada. Execute `bunx --bun vault-go@latest login --force`.');
147
167
  }
148
168
  const refreshed = (await response.json());
169
+ // A refresh already in flight must not overwrite a newer login or logout.
170
+ const latest = loadTokens(this.home);
171
+ if (!latest)
172
+ throw new Error('Vault session was disconnected.');
173
+ if (latest.accessToken !== tokens.accessToken || latest.refreshToken !== tokens.refreshToken)
174
+ return latest.accessToken;
149
175
  const stored = {
150
176
  ...refreshed,
151
177
  expiresAt: Date.now() + (refreshed.expiresIn ?? 900) * 1000,
@@ -177,7 +203,7 @@ export class VaultCloudClient {
177
203
  method: options.method ?? 'GET',
178
204
  redirect: 'error',
179
205
  headers,
180
- signal: AbortSignal.timeout(15_000),
206
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(options.timeoutMs ?? 15_000)]) : AbortSignal.timeout(options.timeoutMs ?? 15_000),
181
207
  };
182
208
  if (options.body)
183
209
  request.body = JSON.stringify(options.body);
@@ -197,11 +223,19 @@ export class VaultCloudClient {
197
223
  const message = payload && typeof payload === 'object' && 'error' in payload
198
224
  ? String(payload.error)
199
225
  : `Vault respondeu HTTP ${response.status}`;
200
- throw new Error(message);
226
+ throw new VaultCloudError(response.status, message);
201
227
  }
202
228
  return payload;
203
229
  }
204
230
  }
231
+ export class VaultCloudError extends Error {
232
+ status;
233
+ constructor(status, message) {
234
+ super(message);
235
+ this.status = status;
236
+ this.name = 'VaultCloudError';
237
+ }
238
+ }
205
239
  function safeJson(text) {
206
240
  try {
207
241
  return JSON.parse(text);
@@ -0,0 +1,60 @@
1
+ export declare const CONTEXT_ENGINE_IDS: readonly ["vault-ai-resume", "claude-subscription", "openai-subscription", "openrouter", "gemini"];
2
+ export declare const API_ENGINE_IDS: readonly ["openrouter", "gemini"];
3
+ export type ContextEngineId = (typeof CONTEXT_ENGINE_IDS)[number];
4
+ export type ApiEngineId = (typeof API_ENGINE_IDS)[number];
5
+ export interface ContextResult {
6
+ title: string;
7
+ content: string;
8
+ facts: string[];
9
+ concepts: string[];
10
+ engine: ContextEngineId;
11
+ }
12
+ export interface EngineAvailability {
13
+ available: boolean;
14
+ reason?: string;
15
+ billing?: string;
16
+ }
17
+ export interface ContextEngine extends EngineAvailability {
18
+ id: ContextEngineId;
19
+ name: string;
20
+ }
21
+ export interface CommandRequest {
22
+ command: "claude" | "codex";
23
+ args: string[];
24
+ cwd: string;
25
+ env: NodeJS.ProcessEnv;
26
+ input?: string;
27
+ timeoutMs: number;
28
+ signal?: AbortSignal;
29
+ }
30
+ export type CommandRunner = (request: CommandRequest) => Promise<{
31
+ stdout: string;
32
+ stderr: string;
33
+ }>;
34
+ export declare function isContextEngine(value: unknown): value is ContextEngineId;
35
+ export declare function isApiEngine(value: unknown): value is ApiEngineId;
36
+ export declare function selectedContextEngine(home: string): ContextEngineId;
37
+ export declare function selectContextEngine(home: string, engine: unknown): ContextEngineId;
38
+ export declare function saveEngineKey(home: string, engine: unknown, key: unknown): ApiEngineId;
39
+ export declare function loadEngineKey(home: string, engine: ApiEngineId, environment?: NodeJS.ProcessEnv): string | undefined;
40
+ export declare function contextEnvironment(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
41
+ /** Never includes process stderr or model input in an error message. */
42
+ export declare const runContextCommand: CommandRunner;
43
+ export declare function listContextEngines(home: string, managedStatus?: EngineAvailability, options?: {
44
+ runner?: CommandRunner;
45
+ env?: NodeJS.ProcessEnv;
46
+ }): Promise<{
47
+ selected: ContextEngineId;
48
+ engines: ContextEngine[];
49
+ }>;
50
+ export declare function validateContextResult(value: unknown, engine: ContextEngineId): ContextResult;
51
+ export interface GenerateContextOptions {
52
+ signal?: AbortSignal;
53
+ runner?: CommandRunner;
54
+ httpFetch?: (input: string, init?: RequestInit) => Promise<Response>;
55
+ env?: NodeJS.ProcessEnv;
56
+ managedGenerate?: (text: string, options: {
57
+ signal?: AbortSignal;
58
+ }) => Promise<unknown>;
59
+ }
60
+ export declare function generateContext(home: string, engine: unknown, text: unknown, options?: GenerateContextOptions): Promise<ContextResult>;