vault-go 0.10.1 → 0.12.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
@@ -8,7 +8,7 @@ Para trocar a conta: `bunx --bun vault-go@latest login --force`. Para chave de A
8
8
 
9
9
  Servidor MCP em Bun para pesquisar, contextualizar e registrar memória na
10
10
  plataforma Vault. A persistência fica no PostgreSQL do serviço Rust executado
11
- no EasyPanel; este pacote não cria banco local.
11
+ no EasyPanel; o painel local mantém um cache privado das consultas recentes.
12
12
 
13
13
  ## Pré-requisito
14
14
 
@@ -69,7 +69,8 @@ O assistente:
69
69
  `vg_live_...`;
70
70
  3. detecta os clientes instalados;
71
71
  4. permite escolher onde registrar o MCP;
72
- 5. preserva as outras configurações dos clientes.
72
+ 5. preserva as outras configurações dos clientes;
73
+ 6. instala e abre o painel local na porta 38850 (início automático no macOS).
73
74
 
74
75
  Clientes suportados:
75
76
 
@@ -195,7 +196,7 @@ consultas aplicam esse identificador, impedindo leitura entre contas.
195
196
  - Renova o access token usando o refresh token local com permissão privada.
196
197
  - Limita tamanhos, listas e janelas nos schemas das ferramentas.
197
198
  - Reserva `stdout` exclusivamente para o protocolo MCP.
198
- - Não persiste memória nem cria banco no dispositivo.
199
+ - O painel guarda cache de projetos/memórias e metadados de chamadas MCP em arquivos privados locais.
199
200
 
200
201
  Instale o MCP apenas em clientes confiáveis: o cliente conectado pode pedir
201
202
  busca, gravação e exclusão de memórias dentro da conta autenticada.
@@ -221,3 +222,71 @@ semântica, publicação no npm, tag e GitHub Release:
221
222
 
222
223
  O segredo `NPM_TOKEN` existe apenas no GitHub Actions e nunca deve ser salvo no
223
224
  repositório ou em logs.
225
+
226
+
227
+ ## Painel local · localhost:38850
228
+
229
+ ```sh
230
+ bunx --bun vault-go@latest setup --lang pt
231
+ bunx --bun vault-go@latest local install
232
+ bunx --bun vault-go@latest local open
233
+ bunx --bun vault-go@latest local status
234
+ bunx --bun vault-go@latest local stop
235
+ bunx --bun vault-go@latest local uninstall
236
+ ```
237
+
238
+ O assistente e a página de retorno do login suportam `--lang pt|en|es`.
239
+ O painel detecta o idioma do navegador e oferece seletor PT/EN/ES, busca
240
+ local, atividade MCP recente, projetos e memórias consultados na nuvem.
241
+ Atualiza a interface a cada 5 segundos enquanto visível e consulta a nuvem
242
+ a cada 15 segundos. Sem conexão, conserva o último cache da mesma conta.
243
+
244
+ 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.
245
+
246
+ O serviço escuta exclusivamente em 127.0.0.1:38850. `local open` verifica
247
+ uma prova criptográfica do serviço e abre uma sessão privada no navegador;
248
+ o segredo local é removido do fragmento imediatamente e trocado por cookie
249
+ HttpOnly/SameSite=Strict. Não há CORS nem acesso direto da nuvem ao localhost.
250
+ O portal `/app/local` abre o painel da máquina atual.
251
+
252
+ No macOS, `local install` registra um LaunchAgent do usuário. Em Linux e
253
+ Windows, inicia o serviço nesta sessão; após reiniciar, use `local start`.
254
+ O runtime fica em `~/.memvault/local-runtime`, independente do cache bunx.
255
+ `local stop` para o processo; `local uninstall` remove o início automático e
256
+ preserva autenticação, cache e atividade. Execute `local install` novamente
257
+ após atualizar o pacote para atualizar o serviço persistente.
258
+
259
+ A atividade registra método, rota, duração e resultado das chamadas feitas
260
+ pelo MCP Vault, sem argumentos, consultas ou credenciais. Não captura toda a
261
+ atividade da máquina nem importa automaticamente o banco do claude-mem em
262
+ 37701. A sincronização deste painel é de leitura da nuvem; as ferramentas
263
+ MCP continuam responsáveis pelas gravações na API.
264
+
265
+ ### Sessões, dispositivos e proteção local
266
+
267
+ **Sair** encerra a sessão desta página e cancela suas prévias de contexto.
268
+ **Desconectar dispositivo** pede confirmação, revoga as credenciais do agente
269
+ na nuvem e remove a autenticação e o cache local após sucesso. Os arquivos
270
+ pessoais permanecem intactos. Para reconectar, entre novamente pelo navegador.
271
+ O portal mostra último contato, permissões e permite revogar cada agente.
272
+
273
+ O cache e a credencial de monitoramento usam AES-256-GCM. No macOS, a chave
274
+ fica no Keychain; nos demais sistemas, em arquivo privado `0600`. Os tokens
275
+ MCP existentes continuam em arquivo privado. O painel informa essas diferenças:
276
+ cache cifrado não significa que todas as memórias sejam criptografadas ponta a
277
+ ponta. O cofre de arquivos e o canal do terminal têm seus próprios protocolos.
278
+
279
+ ### Motor de contexto
280
+
281
+ Escolha **Vault AI Resume**, **Claude Subscription** ou **OpenAI Subscription**
282
+ no painel local. A disponibilidade é verificada antes da geração. Claude usa
283
+ `claude auth login`; OpenAI usa `codex login` com uma conta ChatGPT. O agente
284
+ não pede nem copia credenciais desses provedores. O modelo gerenciado depende
285
+ de configuração e quota no servidor Vault.
286
+
287
+ A geração exige texto e consentimento explícito, usa diretório temporário,
288
+ limites de entrada/saída e timeout, e permite cancelamento. CLIs recebem apenas
289
+ o texto informado, com ferramentas/configurações locais desativadas conforme
290
+ os recursos suportados. O resultado é uma prévia privada da sessão, disponível
291
+ para copiar; não é salvo automaticamente como memória. Os planos e limites do
292
+ provedor escolhido se aplicam. Cancelar não garante estorno de uso já iniciado.
Binary file
@@ -0,0 +1,9 @@
1
+ export interface Activity {
2
+ id: string;
3
+ time: string;
4
+ operation: string;
5
+ ok: boolean;
6
+ durationMs: number;
7
+ }
8
+ export declare function recordActivity(home: string, operation: string, ok: boolean, durationMs: number): void;
9
+ export declare function readActivity(home: string): Activity[];
@@ -0,0 +1,46 @@
1
+ import { appendFileSync, chmodSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ export function recordActivity(home, operation, ok, durationMs) {
5
+ try {
6
+ mkdirSync(home, { recursive: true, mode: 0o700 });
7
+ const file = join(home, "local-activity.jsonl");
8
+ // Bounded journal contains metadata only: never arguments, content or credentials.
9
+ try {
10
+ if (statSync(file).size > 2 * 1024 * 1024)
11
+ writeFileSync(file, "", { mode: 0o600 });
12
+ }
13
+ catch { }
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 });
21
+ chmodSync(file, 0o600);
22
+ }
23
+ catch {
24
+ /* Observability must never prevent an MCP operation. */
25
+ }
26
+ }
27
+ export function readActivity(home) {
28
+ try {
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();
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
package/dist/auth.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Locale } from './locale.js';
1
2
  export interface AuthenticationResult {
2
3
  email: string;
3
4
  method: 'password' | 'browser' | 'api-key' | 'existing';
@@ -8,6 +9,9 @@ export declare function authenticateWithApiKey(apiKey: string, home: string, fet
8
9
  export declare function validateExistingAuthentication(home: string, fetcher?: Fetcher): Promise<AuthenticationResult | null>;
9
10
  export declare function openVaultBrowser(url: string): Promise<void>;
10
11
  export declare function authenticateWithBrowser(home: string, options?: {
12
+ locale?: Locale;
13
+ callbackHtml?: string;
14
+ signal?: AbortSignal;
11
15
  fetcher?: Fetcher;
12
16
  openBrowser?: (url: string) => Promise<void>;
13
17
  timeoutMs?: number;
package/dist/auth.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { callbackPage } from './callback-page.js';
2
+ import { copy, resolveLocale } from './locale.js';
1
3
  import { createServer } from 'node:http';
2
4
  import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
3
5
  import { execFile } from 'node:child_process';
@@ -107,6 +109,8 @@ export function openVaultBrowser(url) {
107
109
  return new Promise((resolve, reject) => { execFile(command, args, { timeout: 10_000 }, error => error ? reject(error) : resolve()); });
108
110
  }
109
111
  export async function authenticateWithBrowser(home, options = {}) {
112
+ const locale = options.locale ?? resolveLocale();
113
+ const t = copy[locale];
110
114
  const fetcher = options.fetcher ?? fetch;
111
115
  const report = options.report ?? (message => process.stderr.write(message + '\n'));
112
116
  const start = endpoint(home, '/auth/oauth/resolveup/start');
@@ -124,7 +128,7 @@ export async function authenticateWithBrowser(home, options = {}) {
124
128
  const server = createServer((req, res) => {
125
129
  res.setHeader('Cache-Control', 'no-store');
126
130
  res.setHeader('Referrer-Policy', 'no-referrer');
127
- res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors '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'");
128
132
  let url;
129
133
  try {
130
134
  url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
@@ -147,7 +151,7 @@ export async function authenticateWithBrowser(home, options = {}) {
147
151
  if (url.searchParams.has('error')) {
148
152
  accepted = true;
149
153
  res.writeHead(400).end('Login não concluído. Volte ao terminal.');
150
- rejectClaim?.(new Error('Login cancelado ou recusado no Vault.'));
154
+ rejectClaim?.(new Error(t.denied));
151
155
  return;
152
156
  }
153
157
  if (!code || !/^[A-Za-z0-9_-]{43}$/.test(code) || url.searchParams.getAll('code').length !== 1) {
@@ -156,11 +160,15 @@ export async function authenticateWithBrowser(home, options = {}) {
156
160
  }
157
161
  accepted = true;
158
162
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
159
- res.end('<!doctype html><html lang="pt-BR"><title>AI Vault Memory</title><h1>AI Vault Memory</h1><p>Retorno recebido. Volte ao terminal para concluir.</p></html>');
163
+ res.end(options.callbackHtml ?? callbackPage(locale));
160
164
  claim?.(code);
161
165
  });
162
166
  let timer;
167
+ const abort = () => rejectClaim?.(new Error(t.denied));
168
+ options.signal?.addEventListener('abort', abort, { once: true });
163
169
  try {
170
+ if (options.signal?.aborted)
171
+ throw new Error(t.denied);
164
172
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
165
173
  const address = server.address();
166
174
  if (!address || typeof address === 'string')
@@ -169,14 +177,14 @@ export async function authenticateWithBrowser(home, options = {}) {
169
177
  start.searchParams.set('mobile_challenge', createHash('sha256').update(verifier).digest('base64url'));
170
178
  start.searchParams.set('mobile_state', state);
171
179
  start.searchParams.set('cli_port', String(port));
172
- timer = setTimeout(() => rejectClaim?.(new Error('Tempo de login esgotado. Execute vault-go login novamente.')), options.timeoutMs ?? 300_000);
173
- report('Abrindo o autenticador do AI Vault Memory no navegador…');
174
- report(`Se o navegador não abrir, acesse neste computador: ${start.href}`);
180
+ timer = setTimeout(() => rejectClaim?.(new Error(t.timeout)), options.timeoutMs ?? 300_000);
181
+ report(t.opening);
182
+ report(`${t.fallback} ${start.href}`);
175
183
  try {
176
184
  await (options.openBrowser ?? openVaultBrowser)(start.href);
177
185
  }
178
186
  catch {
179
- report('Não foi possível abrir o navegador automaticamente. Use o endereço acima.');
187
+ report(t.openFailed);
180
188
  }
181
189
  const code = await received;
182
190
  const response = await fetcher(endpoint(home, '/auth/mobile/exchange'), {
@@ -184,7 +192,7 @@ export async function authenticateWithBrowser(home, options = {}) {
184
192
  body: JSON.stringify({ code, verifier }), redirect: 'error', signal: AbortSignal.timeout(15_000),
185
193
  });
186
194
  if (!response.ok)
187
- throw new Error('Não foi possível concluir o login do Vault. Tente novamente.');
195
+ throw new Error(t.retry);
188
196
  const tokens = normalizeTokens(await response.json());
189
197
  const account = await accountForToken(tokens.accessToken, home, fetcher);
190
198
  if (!account)
@@ -193,6 +201,7 @@ export async function authenticateWithBrowser(home, options = {}) {
193
201
  return { email: account.email, method: 'browser' };
194
202
  }
195
203
  finally {
204
+ options.signal?.removeEventListener('abort', abort);
196
205
  if (timer)
197
206
  clearTimeout(timer);
198
207
  server.closeAllConnections();
@@ -0,0 +1,3 @@
1
+ import { type Locale } from "./locale.js";
2
+ /** All interpolated content is from the static locale catalog, never the callback URL. */
3
+ export declare function callbackPage(locale: Locale, dashboardUrl?: string): string;
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { copy } from "./locale.js";
3
+ /** All interpolated content is from the static locale catalog, never the callback URL. */
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");
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>
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}}
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>`;
33
+ }
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,12 @@
1
- export type CliMode = 'help' | 'version' | 'setup' | 'login' | 'serve' | 'invalid';
1
+ import { type McpClient } from './installer.js';
2
+ export type CliMode = 'help' | 'version' | 'setup' | 'login' | 'serve' | 'local' | 'invalid';
2
3
  export declare function resolveCliMode(argument: string | undefined, stdinIsTTY: boolean, stderrIsTTY: boolean): CliMode;
4
+ export declare function parseNumberedClients(value: string): McpClient[];
5
+ export declare function cliArguments(args: string[]): {
6
+ argument: string | undefined;
7
+ options: string[];
8
+ };
9
+ export declare function runLocal(args: string[]): Promise<number>;
3
10
  export declare function runLogin(args: string[]): Promise<number>;
4
11
  export declare function runSetup(args: string[]): Promise<number>;
5
12
  export declare function helpText(version: string): string;
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 { copy, resolveLocale } from './locale.js';
6
+ import { installLocal, localStatus, localUrl, openLocal, startLocal, stopLocal, uninstallLocal } from './local-install.js';
5
7
  export function resolveCliMode(argument, stdinIsTTY, stderrIsTTY) {
6
8
  if (argument === '--help' || argument === '-h')
7
9
  return 'help';
@@ -13,6 +15,8 @@ export function resolveCliMode(argument, stdinIsTTY, stderrIsTTY) {
13
15
  return 'login';
14
16
  if (argument === 'serve')
15
17
  return 'serve';
18
+ if (argument === 'local')
19
+ return 'local';
16
20
  if (argument === undefined)
17
21
  return stdinIsTTY && stderrIsTTY ? 'setup' : 'serve';
18
22
  return 'invalid';
@@ -24,21 +28,22 @@ function optionValue(args, name) {
24
28
  const prefix = `${name}=`;
25
29
  return args.find((argument) => argument.startsWith(prefix))?.slice(prefix.length);
26
30
  }
27
- async function ensureAuthentication(home, force = false, apiKeyMode = false) {
31
+ async function ensureAuthentication(home, force = false, apiKeyMode = false, locale = 'pt') {
32
+ const t = copy[locale];
28
33
  if (!force) {
29
34
  const existing = await validateExistingAuthentication(home);
30
35
  if (existing) {
31
- process.stderr.write(`✓ Autenticado no Vault como ${existing.email}.\n`);
36
+ process.stderr.write(` ✓ ${t.connected}: ${existing.email}\n`);
32
37
  return existing;
33
38
  }
34
39
  }
35
40
  if (apiKeyMode) {
36
- const result = await authenticateWithApiKey(await promptSecret('Chave de API: '), home);
37
- process.stderr.write(`✓ Chave validada para ${result.email}.\n`);
41
+ const result = await authenticateWithApiKey(await promptSecret(`${t.key}: `), home);
42
+ process.stderr.write(` ✓ ${t.connected}: ${result.email}\n`);
38
43
  return result;
39
44
  }
40
- const result = await authenticateWithBrowser(home);
41
- process.stderr.write(`✓ Login realizado como ${result.email}.\n`);
45
+ const result = await authenticateWithBrowser(home, { locale });
46
+ process.stderr.write(` ✓ ${t.connected}: ${result.email}\n`);
42
47
  return result;
43
48
  }
44
49
  function defaultClients() {
@@ -47,55 +52,153 @@ function defaultClients() {
47
52
  return detected.map((item) => item.client);
48
53
  return ['codex', 'claude', 'cursor', 'vscode', 'copilot'];
49
54
  }
50
- async function chooseClients(args) {
55
+ export function parseNumberedClients(value) {
56
+ return parseClientSelection(value.split(',').map(item => {
57
+ const trimmed = item.trim();
58
+ if (!/^\d+$/.test(trimmed))
59
+ return trimmed;
60
+ const client = MCP_CLIENTS[Number(trimmed) - 1];
61
+ if (!client)
62
+ throw new Error(`Invalid assistant number: ${trimmed}`);
63
+ return client;
64
+ }).join(','));
65
+ }
66
+ async function chooseClients(args, locale) {
67
+ const t = copy[locale];
51
68
  const configured = optionValue(args, '--clients');
52
69
  if (configured)
53
- return parseClientSelection(configured);
70
+ return parseNumberedClients(configured);
54
71
  const detected = detectMcpClients();
55
- process.stderr.write('\nClientes MCP disponíveis:\n');
56
72
  for (const item of detected) {
57
- process.stderr.write(` - ${item.client}${item.detected ? ` (detectado: ${item.evidence})` : ''}\n`);
73
+ process.stderr.write(` ${MCP_CLIENTS.indexOf(item.client) + 1}. ${item.client.padEnd(12)} ${item.detected ? `● ${t.detected}` : `○ ${t.available}`}\n`);
58
74
  }
59
75
  const defaults = defaultClients();
60
- const answer = await promptText(`Clientes separados por vírgula [${defaults.join(',')}]: `);
61
- return answer ? parseClientSelection(answer) : defaults;
76
+ const answer = await promptText(`${t.choose} [${defaults.join(',')}]: `);
77
+ return answer ? parseNumberedClients(answer) : defaults;
78
+ }
79
+ export function cliArguments(args) {
80
+ const remaining = [...args];
81
+ for (let i = 0; i < remaining.length; i++) {
82
+ if (remaining[i] === '--lang') {
83
+ if (!remaining[i + 1] || !['pt', 'en', 'es'].includes(remaining[i + 1]))
84
+ throw new Error('--lang: pt, en, es');
85
+ remaining.splice(i, 2);
86
+ i--;
87
+ }
88
+ else if (remaining[i]?.startsWith('--lang=')) {
89
+ if (!['pt', 'en', 'es'].includes(remaining[i].slice(7)))
90
+ throw new Error('--lang: pt, en, es');
91
+ remaining.splice(i, 1);
92
+ i--;
93
+ }
94
+ }
95
+ return { argument: remaining[0], options: args.filter((_, index) => index !== args.indexOf(remaining[0])) };
96
+ }
97
+ async function chooseLocale(args) {
98
+ const configured = optionValue(args, '--lang');
99
+ if (configured)
100
+ return resolveLocale(configured);
101
+ const detected = resolveLocale();
102
+ const answer = await promptText(` 1. Português 2. English 3. Español [${detected}]: `);
103
+ if (!answer)
104
+ return detected;
105
+ const selected = { '1': 'pt', '2': 'en', '3': 'es' }[answer] ?? answer.toLowerCase();
106
+ if (!['pt', 'en', 'es'].includes(selected))
107
+ throw new Error('--lang: pt, en, es');
108
+ return resolveLocale(selected);
109
+ }
110
+ const localCopy = {
111
+ pt: { step: 'Instalar painel local', startup: 'Início automático ativado', manual: 'Início automático indisponível neste sistema. Após reiniciar, execute: vault-go local start', running: 'Painel local disponível', stopped: 'Serviço local parado', removed: 'Início automático removido; dados e credenciais preservados', retry: 'Tente novamente: vault-go local install; vault-go local open' },
112
+ en: { step: 'Install local dashboard', startup: 'Automatic startup enabled', manual: 'Automatic startup is unavailable on this system. After restarting, run: vault-go local start', running: 'Local dashboard available', stopped: 'Local service stopped', removed: 'Automatic startup removed; data and credentials preserved', retry: 'Retry: vault-go local install; vault-go local open' },
113
+ es: { step: 'Instalar panel local', startup: 'Inicio automático activado', manual: 'Inicio automático no disponible en este sistema. Después de reiniciar, ejecuta: vault-go local start', running: 'Panel local disponible', stopped: 'Servicio local detenido', removed: 'Inicio automático eliminado; datos y credenciales conservados', retry: 'Reintenta: vault-go local install; vault-go local open' },
114
+ };
115
+ export async function runLocal(args) {
116
+ const t = localCopy[resolveLocale(optionValue(args, '--lang'))];
117
+ const command = cliArguments(args).argument ?? 'open';
118
+ if (command === 'status') {
119
+ const status = await localStatus();
120
+ process.stdout.write(status ? `${t.running}: ${localUrl} (v${status.version})\n` : `${t.stopped}\n`);
121
+ return status ? 0 : 1;
122
+ }
123
+ if (command === 'stop') {
124
+ await stopLocal();
125
+ process.stderr.write(`${t.stopped}\n`);
126
+ return 0;
127
+ }
128
+ if (command === 'uninstall') {
129
+ await uninstallLocal();
130
+ process.stderr.write(`${t.removed}\n`);
131
+ return 0;
132
+ }
133
+ if (command === 'serve') {
134
+ const { startLocalService } = await import('./local-service.js');
135
+ await startLocalService({ home: vaultHome() });
136
+ process.stderr.write(`${t.running}: ${localUrl}\n`);
137
+ return 0;
138
+ }
139
+ if (command === 'install') {
140
+ const result = await installLocal();
141
+ process.stderr.write(`${result.startup ? t.startup : t.manual}\n`);
142
+ }
143
+ else if (command === 'start')
144
+ await startLocal();
145
+ else if (command === 'open')
146
+ await openLocal();
147
+ else
148
+ throw new Error('vault-go local [install|start|open|status|stop|uninstall|serve]');
149
+ process.stderr.write(`${t.running}: ${localUrl}\n`);
150
+ return 0;
62
151
  }
63
152
  export async function runLogin(args) {
153
+ const locale = resolveLocale(optionValue(args, '--lang'));
64
154
  if (!process.stdin.isTTY || !process.stderr.isTTY) {
65
- throw new Error('O login requer um terminal interativo.');
155
+ throw new Error(copy[locale].interactive);
66
156
  }
67
- await ensureAuthentication(vaultHome(), args.includes('--force'), args.includes('--api-key'));
157
+ await ensureAuthentication(vaultHome(), args.includes('--force'), args.includes('--api-key'), locale);
68
158
  return 0;
69
159
  }
70
160
  export async function runSetup(args) {
71
161
  if (!process.stdin.isTTY || !process.stderr.isTTY) {
72
162
  throw new Error('O setup requer um terminal interativo.');
73
163
  }
74
- process.stderr.write('vault-go — configuração MCP universal\n');
75
- await ensureAuthentication(vaultHome(), args.includes('--reauth'), args.includes('--api-key'));
76
- const clients = await chooseClients(args);
164
+ process.stderr.write('\n ◆ VAULT GO\n ────────────────────────────────────────\n');
165
+ const locale = await chooseLocale(args);
166
+ const t = copy[locale];
167
+ const local = localCopy[locale];
168
+ process.env['VAULT_GO_LANG'] = locale;
169
+ process.stderr.write(` ${t.subtitle}\n\n [1/4] ${t.auth}\n`);
170
+ await ensureAuthentication(vaultHome(), args.includes('--reauth'), args.includes('--api-key'), locale);
171
+ process.stderr.write(`\n [2/4] ${t.clients}\n`);
172
+ const clients = await chooseClients(args, locale);
77
173
  if (clients.length === 0)
78
- throw new Error('Nenhum cliente MCP selecionado.');
79
- process.stderr.write('\nInstalação MCP\n');
174
+ throw new Error(t.none);
175
+ process.stderr.write(`\n [3/4] ${t.install}\n`);
80
176
  let failures = 0;
81
177
  for (const client of clients) {
82
178
  try {
83
179
  const result = installMcpClient(client);
84
180
  const marker = result.status === 'already-installed' ? '=' : '✓';
85
- process.stderr.write(`${marker} ${client}: ${result.destination}\n`);
181
+ process.stderr.write(` ${marker} ${client}: ${result.status === 'already-installed' ? t.already : t.installed}\n ${result.destination}\n`);
86
182
  }
87
183
  catch (error) {
88
184
  failures += 1;
89
185
  const message = error instanceof Error ? error.message : String(error);
90
- process.stderr.write(`✗ ${client}: ${message}\n`);
186
+ process.stderr.write(` ✗ ${t.failed} ${client}: ${message}\n`);
91
187
  }
92
188
  }
93
- if (failures > 0) {
94
- process.stderr.write(`\n${failures} cliente(s) não puderam ser configurados.\n`);
95
- return 1;
189
+ process.stderr.write(`\n [4/4] ${local.step}\n`);
190
+ try {
191
+ const result = await installLocal();
192
+ process.stderr.write(` ✓ ${result.startup ? local.startup : local.manual}\n`);
193
+ await openLocal();
194
+ process.stderr.write(` ✓ ${local.running}: ${localUrl}\n`);
96
195
  }
97
- process.stderr.write('\nSetup concluído. Reinicie os clientes abertos para carregar o Vault.\n');
98
- return 0;
196
+ catch (error) {
197
+ failures++;
198
+ process.stderr.write(` ✗ ${error instanceof Error ? error.message : String(error)}\n ${local.retry}\n`);
199
+ }
200
+ process.stderr.write(`\n ${failures ? `${t.failed}: ${failures}` : `✓ ${t.done}`}\n ${t.restart}\n\n`);
201
+ return failures ? 1 : 0;
99
202
  }
100
203
  export function helpText(version) {
101
204
  return `vault-go ${version}
@@ -115,6 +218,12 @@ Uso:
115
218
  bunx --bun vault-go@latest serve
116
219
  Inicia o servidor MCP por stdio.
117
220
 
221
+ vault-go local [install|start|open|status|stop|uninstall|serve]
222
+ Painel local em http://localhost:38850; install ativa início automático no macOS.
223
+
224
+ --lang pt|en|es
225
+ Idioma do assistente (padrão: idioma do sistema).
226
+
118
227
  Clientes:
119
228
  ${MCP_CLIENTS.join(', ')}
120
229
 
package/dist/cloud.d.ts CHANGED
@@ -32,8 +32,14 @@ export interface VaultMemoryApi {
32
32
  export declare class VaultCloudClient implements VaultMemoryApi {
33
33
  private readonly home;
34
34
  private readonly cloudFetch;
35
- constructor(home?: string, cloudFetch?: typeof fetch);
35
+ private readonly journal;
36
+ constructor(home?: string, cloudFetch?: typeof fetch, journal?: boolean);
36
37
  health(): Promise<unknown>;
38
+ observerStatus(): Promise<unknown>;
39
+ observerGenerate(text: string, signal?: AbortSignal): Promise<unknown>;
40
+ registerDevice(input: Record<string, unknown>): Promise<unknown>;
41
+ rotateDevice(id: string): Promise<unknown>;
42
+ deviceAction(id: string, token: string, action: 'heartbeat' | 'disconnect', body?: Record<string, unknown>): Promise<unknown>;
37
43
  projects(): Promise<unknown>;
38
44
  createProject(input: Record<string, unknown>): Promise<unknown>;
39
45
  startSession(input: Record<string, unknown>): Promise<unknown>;
@@ -65,4 +71,9 @@ export declare class VaultCloudClient implements VaultMemoryApi {
65
71
  private endpoint;
66
72
  private accessToken;
67
73
  private request;
74
+ private performRequest;
75
+ }
76
+ export declare class VaultCloudError extends Error {
77
+ readonly status: number;
78
+ constructor(status: number, message: string);
68
79
  }