vault-go 0.8.1 → 0.10.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
@@ -1,3 +1,9 @@
1
+ ## Login no navegador
2
+
3
+ `bunx --bun vault-go@latest` abre o autenticador do AI Vault Memory no navegador. E-mail, senha e MFA são informados apenas no portal. Ao concluir, o terminal recebe um código de uso único por `127.0.0.1` e o troca com PKCE; tokens não aparecem na URL nem no console. O listener fecha ao concluir ou após cinco minutos.
4
+
5
+ Para trocar a conta: `bunx --bun vault-go@latest login --force`. Para chave de API explícita: `bunx --bun vault-go@latest login --force --api-key`. Se a abertura automática falhar, use o endereço exibido no mesmo computador. Requer uma sessão local com navegador para o retorno loopback.
6
+
1
7
  # vault-go
2
8
 
3
9
  Servidor MCP em Bun para pesquisar, contextualizar e registrar memória na
@@ -50,23 +56,43 @@ bases, escrita, importação ou exclusão, use o cliente Bun completo abaixo.
50
56
 
51
57
  ### Opção Bun completa
52
58
 
53
- Para instalar e registrar no Codex, execute em um terminal:
59
+ Execute em um terminal:
54
60
 
55
61
  ```sh
56
62
  bunx --bun vault-go@latest
57
63
  ```
58
64
 
59
- Quando executado diretamente em um terminal, o pacote detecta o modo
60
- interativo e chama `codex mcp add` com os argumentos corretos. O mesmo fluxo
61
- pode ser solicitado explicitamente:
65
+ O assistente:
66
+
67
+ 1. valida uma sessão existente em `~/.memvault`;
68
+ 2. se necessário, abre o login do AI Vault Memory no navegador (chave de API somente com `--api-key`)
69
+ `vg_live_...`;
70
+ 3. detecta os clientes instalados;
71
+ 4. permite escolher onde registrar o MCP;
72
+ 5. preserva as outras configurações dos clientes.
73
+
74
+ Clientes suportados:
75
+
76
+ - Codex e ChatGPT Desktop local;
77
+ - Claude Code e Claude Desktop;
78
+ - Cursor e Windsurf;
79
+ - VS Code com GitHub Copilot;
80
+ - GitHub Copilot CLI;
81
+ - Roo Code;
82
+ - OpenCode.
83
+
84
+ Comandos explícitos:
62
85
 
63
86
  ```sh
64
- bunx --bun vault-go@latest install
87
+ bunx --bun vault-go@latest setup
88
+ bunx --bun vault-go@latest setup --clients codex,claude,copilot
89
+ bunx --bun vault-go@latest login --force
90
+ bunx --bun vault-go@latest serve
65
91
  ```
66
92
 
67
- O `bunx` sozinho baixa o pacote para o cache; o instalador da biblioteca é que
68
- registra o servidor na configuração do Codex. Reinicie o Codex depois da
69
- primeira instalação.
93
+ As credenciais são gravadas em `~/.memvault/auth.json` com permissão `0600`;
94
+ o diretório usa `0700`. Senhas e códigos MFA não são persistidos. Reinicie os
95
+ clientes que estavam abertos depois da primeira instalação.
70
96
 
71
97
  Configuração MCP genérica:
72
98
 
@@ -75,22 +101,22 @@ Configuração MCP genérica:
75
101
  "mcpServers": {
76
102
  "vault-go": {
77
103
  "command": "bunx",
78
- "args": ["--bun", "vault-go@latest"]
104
+ "args": ["--bun", "vault-go@latest", "serve"]
79
105
  }
80
106
  }
81
107
  }
82
108
  ```
83
109
 
84
- Por padrão, o MCP reutiliza `~/.memvault/config.json` e
85
- `~/.memvault/auth.json`, criados pelo comando `memvault login`. Também é
86
- possível apontar outro diretório:
110
+ Por padrão, todos os clientes reutilizam `~/.memvault/config.json` e
111
+ `~/.memvault/auth.json`, criados pelo assistente. Também é possível apontar
112
+ outro diretório:
87
113
 
88
114
  ```json
89
115
  {
90
116
  "mcpServers": {
91
117
  "vault-go": {
92
118
  "command": "bunx",
93
- "args": ["--bun", "vault-go@latest"],
119
+ "args": ["--bun", "vault-go@latest", "serve"],
94
120
  "env": {
95
121
  "VAULT_GO_HOME": "/caminho/privado/.memvault"
96
122
  }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export interface AuthenticationResult {
2
+ email: string;
3
+ method: 'password' | 'browser' | 'api-key' | 'existing';
4
+ }
5
+ type Fetcher = typeof fetch;
6
+ export declare function authenticateWithPassword(email: string, password: string, requestMfa: () => Promise<string>, home: string, fetcher?: Fetcher): Promise<AuthenticationResult>;
7
+ export declare function authenticateWithApiKey(apiKey: string, home: string, fetcher?: Fetcher): Promise<AuthenticationResult>;
8
+ export declare function validateExistingAuthentication(home: string, fetcher?: Fetcher): Promise<AuthenticationResult | null>;
9
+ export declare function openVaultBrowser(url: string): Promise<void>;
10
+ export declare function authenticateWithBrowser(home: string, options?: {
11
+ fetcher?: Fetcher;
12
+ openBrowser?: (url: string) => Promise<void>;
13
+ timeoutMs?: number;
14
+ report?: (message: string) => void;
15
+ }): Promise<AuthenticationResult>;
16
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,201 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
3
+ import { execFile } from 'node:child_process';
4
+ import { loadConfig, loadTokens, saveConfig, saveTokens, } from './config.js';
5
+ function endpoint(home, path) {
6
+ const apiUrl = new URL(loadConfig(home).apiUrl);
7
+ return new URL(`${apiUrl.pathname.replace(/\/$/u, '')}${path}`, apiUrl.origin);
8
+ }
9
+ async function errorMessage(response) {
10
+ const body = (await response.json().catch(() => ({})));
11
+ return body.error || body.code || `HTTP ${response.status}`;
12
+ }
13
+ async function accountForToken(token, home, fetcher) {
14
+ const response = await fetcher(endpoint(home, '/me'), {
15
+ headers: { authorization: `Bearer ${token}` },
16
+ redirect: 'error',
17
+ signal: AbortSignal.timeout(10_000),
18
+ });
19
+ if (response.status === 401 || response.status === 403)
20
+ return null;
21
+ if (!response.ok)
22
+ throw new Error(`Falha ao validar a conta Vault: ${await errorMessage(response)}`);
23
+ const account = (await response.json());
24
+ if (!account.email)
25
+ throw new Error('A API Vault retornou uma conta sem e-mail.');
26
+ return account;
27
+ }
28
+ function persist(tokens, email, home) {
29
+ saveTokens(tokens, home);
30
+ saveConfig({ ...loadConfig(home), email }, home);
31
+ }
32
+ function normalizeTokens(value) {
33
+ if (!value || typeof value !== 'object') {
34
+ throw new Error('A API Vault retornou credenciais inválidas.');
35
+ }
36
+ const tokens = value;
37
+ if (typeof tokens.accessToken !== 'string' || tokens.accessToken.length === 0) {
38
+ throw new Error('A API Vault retornou credenciais sem access token.');
39
+ }
40
+ const normalized = { accessToken: tokens.accessToken };
41
+ if (typeof tokens.refreshToken === 'string')
42
+ normalized.refreshToken = tokens.refreshToken;
43
+ if (typeof tokens.expiresIn === 'number')
44
+ normalized.expiresIn = tokens.expiresIn;
45
+ normalized.expiresAt = Date.now() + (normalized.expiresIn ?? 900) * 1000;
46
+ return normalized;
47
+ }
48
+ export async function authenticateWithPassword(email, password, requestMfa, home, fetcher = fetch) {
49
+ const login = await fetcher(endpoint(home, '/auth/login'), {
50
+ method: 'POST',
51
+ headers: { 'content-type': 'application/json' },
52
+ body: JSON.stringify({ email, password }),
53
+ signal: AbortSignal.timeout(15_000),
54
+ });
55
+ if (!login.ok)
56
+ throw new Error(`Falha no login: ${await errorMessage(login)}`);
57
+ let body = (await login.json());
58
+ if ('twoFactorRequired' in body) {
59
+ const code = await requestMfa();
60
+ const verification = await fetcher(endpoint(home, '/auth/2fa/verify'), {
61
+ method: 'POST',
62
+ headers: { 'content-type': 'application/json' },
63
+ body: JSON.stringify({ ticket: body.ticket, code }),
64
+ signal: AbortSignal.timeout(15_000),
65
+ });
66
+ if (!verification.ok) {
67
+ throw new Error(`Falha na verificação MFA: ${await errorMessage(verification)}`);
68
+ }
69
+ body = (await verification.json());
70
+ }
71
+ const tokens = normalizeTokens(body);
72
+ persist(tokens, email, home);
73
+ return { email, method: 'password' };
74
+ }
75
+ export async function authenticateWithApiKey(apiKey, home, fetcher = fetch) {
76
+ const account = await accountForToken(apiKey, home, fetcher);
77
+ if (!account)
78
+ throw new Error('Chave de API Vault inválida ou sem permissão de leitura.');
79
+ persist({ accessToken: apiKey }, account.email, home);
80
+ return { email: account.email, method: 'api-key' };
81
+ }
82
+ export async function validateExistingAuthentication(home, fetcher = fetch) {
83
+ let tokens = loadTokens(home);
84
+ if (!tokens)
85
+ return null;
86
+ let account = await accountForToken(tokens.accessToken, home, fetcher);
87
+ if (!account && tokens.refreshToken) {
88
+ const refreshed = await fetcher(endpoint(home, '/auth/refresh'), {
89
+ method: 'POST',
90
+ headers: { 'content-type': 'application/json' },
91
+ body: JSON.stringify({ refreshToken: tokens.refreshToken }),
92
+ signal: AbortSignal.timeout(15_000),
93
+ });
94
+ if (refreshed.ok) {
95
+ tokens = normalizeTokens(await refreshed.json());
96
+ account = await accountForToken(tokens.accessToken, home, fetcher);
97
+ }
98
+ }
99
+ if (!account)
100
+ return null;
101
+ persist(tokens, account.email, home);
102
+ return { email: account.email, method: 'existing' };
103
+ }
104
+ export function openVaultBrowser(url) {
105
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open';
106
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
107
+ return new Promise((resolve, reject) => { execFile(command, args, { timeout: 10_000 }, error => error ? reject(error) : resolve()); });
108
+ }
109
+ export async function authenticateWithBrowser(home, options = {}) {
110
+ const fetcher = options.fetcher ?? fetch;
111
+ const report = options.report ?? (message => process.stderr.write(message + '\n'));
112
+ const start = endpoint(home, '/auth/oauth/resolveup/start');
113
+ if (start.protocol !== 'https:' && !(start.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(start.hostname)))
114
+ throw new Error('A autenticação Vault requer HTTPS.');
115
+ const verifier = randomBytes(32).toString('base64url');
116
+ const state = randomBytes(32).toString('base64url');
117
+ let port = 0;
118
+ let claim;
119
+ let rejectClaim;
120
+ let accepted = false;
121
+ const received = new Promise((resolve, reject) => { claim = resolve; rejectClaim = reject; });
122
+ // A timeout can occur while the browser opener is still running.
123
+ void received.catch(() => { });
124
+ const server = createServer((req, res) => {
125
+ res.setHeader('Cache-Control', 'no-store');
126
+ res.setHeader('Referrer-Policy', 'no-referrer');
127
+ res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'");
128
+ let url;
129
+ try {
130
+ url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
131
+ }
132
+ catch {
133
+ res.writeHead(400).end();
134
+ return;
135
+ }
136
+ if (req.method !== 'GET' || req.headers.host !== `127.0.0.1:${port}` || url.pathname !== '/callback') {
137
+ res.writeHead(404).end();
138
+ return;
139
+ }
140
+ const incoming = url.searchParams.get('state') ?? '';
141
+ const validState = /^[A-Za-z0-9_-]{43}$/.test(incoming) && timingSafeEqual(Buffer.from(incoming), Buffer.from(state));
142
+ const code = url.searchParams.get('code');
143
+ if (!validState || url.searchParams.getAll('state').length !== 1 || accepted) {
144
+ res.writeHead(400).end('Retorno de autenticação inválido.');
145
+ return;
146
+ }
147
+ if (url.searchParams.has('error')) {
148
+ accepted = true;
149
+ res.writeHead(400).end('Login não concluído. Volte ao terminal.');
150
+ rejectClaim?.(new Error('Login cancelado ou recusado no Vault.'));
151
+ return;
152
+ }
153
+ if (!code || !/^[A-Za-z0-9_-]{43}$/.test(code) || url.searchParams.getAll('code').length !== 1) {
154
+ res.writeHead(400).end('Código inválido.');
155
+ return;
156
+ }
157
+ accepted = true;
158
+ 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>');
160
+ claim?.(code);
161
+ });
162
+ let timer;
163
+ try {
164
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
165
+ const address = server.address();
166
+ if (!address || typeof address === 'string')
167
+ throw new Error('Não foi possível iniciar o retorno local.');
168
+ port = address.port;
169
+ start.searchParams.set('mobile_challenge', createHash('sha256').update(verifier).digest('base64url'));
170
+ start.searchParams.set('mobile_state', state);
171
+ 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}`);
175
+ try {
176
+ await (options.openBrowser ?? openVaultBrowser)(start.href);
177
+ }
178
+ catch {
179
+ report('Não foi possível abrir o navegador automaticamente. Use o endereço acima.');
180
+ }
181
+ const code = await received;
182
+ const response = await fetcher(endpoint(home, '/auth/mobile/exchange'), {
183
+ method: 'POST', headers: { 'content-type': 'application/json' },
184
+ body: JSON.stringify({ code, verifier }), redirect: 'error', signal: AbortSignal.timeout(15_000),
185
+ });
186
+ if (!response.ok)
187
+ throw new Error('Não foi possível concluir o login do Vault. Tente novamente.');
188
+ const tokens = normalizeTokens(await response.json());
189
+ const account = await accountForToken(tokens.accessToken, home, fetcher);
190
+ if (!account)
191
+ throw new Error('O Vault não confirmou a conta autenticada.');
192
+ persist(tokens, account.email, home);
193
+ return { email: account.email, method: 'browser' };
194
+ }
195
+ finally {
196
+ if (timer)
197
+ clearTimeout(timer);
198
+ server.closeAllConnections();
199
+ await new Promise(resolve => server.close(() => resolve()));
200
+ }
201
+ }
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export declare const CODEX_MCP_NAME = "vault-go";
2
- export declare const CODEX_MCP_COMMAND: readonly ["bunx", "--bun", "vault-go@latest"];
3
- export declare function shouldInstallFromCli(argument: string | undefined, stdinIsTTY: boolean, stderrIsTTY: boolean): boolean;
4
- export declare function installCodexMcp(): number;
1
+ export type CliMode = 'help' | 'version' | 'setup' | 'login' | 'serve' | 'invalid';
2
+ export declare function resolveCliMode(argument: string | undefined, stdinIsTTY: boolean, stderrIsTTY: boolean): CliMode;
3
+ export declare function runLogin(args: string[]): Promise<number>;
4
+ export declare function runSetup(args: string[]): Promise<number>;
5
+ export declare function helpText(version: string): string;
package/dist/cli.js CHANGED
@@ -1,34 +1,126 @@
1
- import { spawnSync } from 'node:child_process';
2
- export const CODEX_MCP_NAME = 'vault-go';
3
- export const CODEX_MCP_COMMAND = ['bunx', '--bun', 'vault-go@latest'];
4
- export function shouldInstallFromCli(argument, stdinIsTTY, stderrIsTTY) {
5
- if (argument === 'install' || argument === 'setup' || argument === '--install') {
6
- return true;
1
+ import { authenticateWithApiKey, authenticateWithBrowser, validateExistingAuthentication, } from './auth.js';
2
+ import { vaultHome } from './config.js';
3
+ import { detectMcpClients, installMcpClient, MCP_CLIENTS, parseClientSelection, } from './installer.js';
4
+ import { promptSecret, promptText } from './prompt.js';
5
+ export function resolveCliMode(argument, stdinIsTTY, stderrIsTTY) {
6
+ if (argument === '--help' || argument === '-h')
7
+ return 'help';
8
+ if (argument === '--version' || argument === '-v')
9
+ return 'version';
10
+ if (argument === 'setup' || argument === 'install' || argument === '--install')
11
+ return 'setup';
12
+ if (argument === 'login' || argument === 'auth')
13
+ return 'login';
14
+ if (argument === 'serve')
15
+ return 'serve';
16
+ if (argument === undefined)
17
+ return stdinIsTTY && stderrIsTTY ? 'setup' : 'serve';
18
+ return 'invalid';
19
+ }
20
+ function optionValue(args, name) {
21
+ const exact = args.indexOf(name);
22
+ if (exact >= 0)
23
+ return args[exact + 1];
24
+ const prefix = `${name}=`;
25
+ return args.find((argument) => argument.startsWith(prefix))?.slice(prefix.length);
26
+ }
27
+ async function ensureAuthentication(home, force = false, apiKeyMode = false) {
28
+ if (!force) {
29
+ const existing = await validateExistingAuthentication(home);
30
+ if (existing) {
31
+ process.stderr.write(`✓ Autenticado no Vault como ${existing.email}.\n`);
32
+ return existing;
33
+ }
7
34
  }
8
- return argument === undefined && stdinIsTTY && stderrIsTTY;
9
- }
10
- export function installCodexMcp() {
11
- const existing = spawnSync('codex', ['mcp', 'get', CODEX_MCP_NAME], {
12
- encoding: 'utf8',
13
- });
14
- if (existing.error && 'code' in existing.error && existing.error.code === 'ENOENT') {
15
- process.stderr.write('vault-go: o comando "codex" não foi encontrado. Instale o Codex CLI e tente novamente.\n');
16
- return 1;
35
+ if (apiKeyMode) {
36
+ const result = await authenticateWithApiKey(await promptSecret('Chave de API: '), home);
37
+ process.stderr.write(`✓ Chave validada para ${result.email}.\n`);
38
+ return result;
17
39
  }
18
- if (existing.status === 0) {
19
- process.stderr.write(`vault-go: o MCP "${CODEX_MCP_NAME}" já está registrado no Codex.\n`);
20
- return 0;
40
+ const result = await authenticateWithBrowser(home);
41
+ process.stderr.write(`✓ Login realizado como ${result.email}.\n`);
42
+ return result;
43
+ }
44
+ function defaultClients() {
45
+ const detected = detectMcpClients().filter((item) => item.detected);
46
+ if (detected.length > 0)
47
+ return detected.map((item) => item.client);
48
+ return ['codex', 'claude', 'cursor', 'vscode', 'copilot'];
49
+ }
50
+ async function chooseClients(args) {
51
+ const configured = optionValue(args, '--clients');
52
+ if (configured)
53
+ return parseClientSelection(configured);
54
+ const detected = detectMcpClients();
55
+ process.stderr.write('\nClientes MCP disponíveis:\n');
56
+ for (const item of detected) {
57
+ process.stderr.write(` - ${item.client}${item.detected ? ` (detectado: ${item.evidence})` : ''}\n`);
21
58
  }
22
- process.stderr.write('vault-go: registrando o MCP no Codex...\n');
23
- const installed = spawnSync('codex', ['mcp', 'add', CODEX_MCP_NAME, '--', ...CODEX_MCP_COMMAND], { stdio: 'inherit' });
24
- if (installed.error) {
25
- process.stderr.write(`vault-go: não foi possível executar o instalador: ${installed.error.message}\n`);
26
- return 1;
59
+ const defaults = defaultClients();
60
+ const answer = await promptText(`Clientes separados por vírgula [${defaults.join(',')}]: `);
61
+ return answer ? parseClientSelection(answer) : defaults;
62
+ }
63
+ export async function runLogin(args) {
64
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
65
+ throw new Error('O login requer um terminal interativo.');
27
66
  }
28
- if (installed.status !== 0) {
29
- process.stderr.write('vault-go: o Codex não conseguiu registrar o MCP.\n');
30
- return installed.status ?? 1;
67
+ await ensureAuthentication(vaultHome(), args.includes('--force'), args.includes('--api-key'));
68
+ return 0;
69
+ }
70
+ export async function runSetup(args) {
71
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
72
+ throw new Error('O setup requer um terminal interativo.');
73
+ }
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);
77
+ if (clients.length === 0)
78
+ throw new Error('Nenhum cliente MCP selecionado.');
79
+ process.stderr.write('\nInstalação MCP\n');
80
+ let failures = 0;
81
+ for (const client of clients) {
82
+ try {
83
+ const result = installMcpClient(client);
84
+ const marker = result.status === 'already-installed' ? '=' : '✓';
85
+ process.stderr.write(`${marker} ${client}: ${result.destination}\n`);
86
+ }
87
+ catch (error) {
88
+ failures += 1;
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ process.stderr.write(`✗ ${client}: ${message}\n`);
91
+ }
31
92
  }
32
- process.stderr.write('vault-go: MCP instalado. Reinicie o Codex para carregá-lo.\n');
93
+ if (failures > 0) {
94
+ process.stderr.write(`\n${failures} cliente(s) não puderam ser configurados.\n`);
95
+ return 1;
96
+ }
97
+ process.stderr.write('\nSetup concluído. Reinicie os clientes abertos para carregar o Vault.\n');
33
98
  return 0;
34
99
  }
100
+ export function helpText(version) {
101
+ return `vault-go ${version}
102
+
103
+ Servidor MCP universal para a plataforma Vault.
104
+
105
+ Uso:
106
+ bunx --bun vault-go@latest
107
+ Abre o login Vault no navegador e instala nos clientes detectados.
108
+
109
+ bunx --bun vault-go@latest setup [--clients lista] [--reauth]
110
+ Executa o assistente de autenticação e instalação.
111
+
112
+ bunx --bun vault-go@latest login [--force] [--api-key]
113
+ Autentica sem alterar clientes MCP.
114
+
115
+ bunx --bun vault-go@latest serve
116
+ Inicia o servidor MCP por stdio.
117
+
118
+ Clientes:
119
+ ${MCP_CLIENTS.join(', ')}
120
+
121
+ Variáveis:
122
+ VAULT_GO_HOME Diretório de configuração (padrão: ~/.memvault)
123
+ MEMVAULT_CONFIG_DIR Diretório compartilhado com o agente MemVault
124
+ VAULT_GO_API_URL Endpoint alternativo da API Vault
125
+ `;
126
+ }
package/dist/cloud.js CHANGED
@@ -123,14 +123,15 @@ export class VaultCloudClient {
123
123
  return environmentToken;
124
124
  const tokens = loadTokens(this.home);
125
125
  if (!tokens) {
126
- throw new Error('Vault não autenticado. Execute `memvault login` antes de iniciar o MCP.');
126
+ throw new Error('Vault não autenticado. Execute `bunx --bun vault-go@latest login`.');
127
127
  }
128
128
  const valid = !forceRefresh &&
129
129
  (!tokens.expiresAt || Date.now() < tokens.expiresAt - 60_000);
130
130
  if (valid)
131
131
  return tokens.accessToken;
132
- if (!tokens.refreshToken)
133
- throw new Error('Sessão expirada. Execute `memvault login` novamente.');
132
+ if (!tokens.refreshToken) {
133
+ throw new Error('Sessão expirada. Execute `bunx --bun vault-go@latest login --force`.');
134
+ }
134
135
  const response = await this.cloudFetch(this.endpoint('/auth/refresh'), {
135
136
  method: 'POST',
136
137
  headers: { 'content-type': 'application/json' },
@@ -138,7 +139,7 @@ export class VaultCloudClient {
138
139
  signal: AbortSignal.timeout(10_000),
139
140
  });
140
141
  if (!response.ok) {
141
- throw new Error('Sessão expirada. Execute `memvault login` novamente.');
142
+ throw new Error('Sessão expirada. Execute `bunx --bun vault-go@latest login --force`.');
142
143
  }
143
144
  const refreshed = (await response.json());
144
145
  const stored = {
package/dist/config.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare function configPath(home?: string): string;
23
23
  export declare function authPath(home?: string): string;
24
24
  export declare function loadConfig(home?: string): VaultConfig;
25
25
  export declare function loadTokens(home?: string): StoredTokens | null;
26
+ export declare function saveConfig(config: VaultConfig, home?: string): void;
26
27
  export declare function saveTokens(tokens: StoredTokens, home?: string): void;
27
28
  export declare function getStatus(home?: string): VaultStatus;
28
29
  export declare function validateApiUrl(value: string): URL;
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
- import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { dirname, join, resolve } from 'node:path';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
4
  export const DEFAULT_API_URL = 'https://vault.resolveup.com.br/api';
5
5
  const DEFAULT_CONFIG = {
6
6
  apiUrl: DEFAULT_API_URL,
@@ -51,12 +51,22 @@ export function loadTokens(home = vaultHome()) {
51
51
  return null;
52
52
  }
53
53
  }
54
+ function ensureVaultHome(home) {
55
+ mkdirSync(home, { recursive: true, mode: 0o700 });
56
+ chmodSync(home, 0o700);
57
+ }
58
+ function atomicWrite(path, value, home) {
59
+ ensureVaultHome(home);
60
+ const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
61
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
62
+ renameSync(temporary, path);
63
+ chmodSync(path, 0o600);
64
+ }
65
+ export function saveConfig(config, home = vaultHome()) {
66
+ atomicWrite(configPath(home), config, home);
67
+ }
54
68
  export function saveTokens(tokens, home = vaultHome()) {
55
- const destination = authPath(home);
56
- const temporary = join(dirname(destination), `.auth.${process.pid}.tmp`);
57
- writeFileSync(temporary, `${JSON.stringify(tokens, null, 2)}\n`, { mode: 0o600 });
58
- renameSync(temporary, destination);
59
- chmodSync(destination, 0o600);
69
+ atomicWrite(authPath(home), tokens, home);
60
70
  }
61
71
  export function getStatus(home = vaultHome()) {
62
72
  const config = loadConfig(home);
package/dist/index.js CHANGED
@@ -1,28 +1,35 @@
1
1
  #!/usr/bin/env bun
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { installCodexMcp, shouldInstallFromCli } from './cli.js';
3
+ import { helpText, resolveCliMode, runLogin, runSetup } from './cli.js';
4
4
  import { createVaultGoServer, VERSION } from './server.js';
5
5
  const argument = process.argv[2];
6
- if (argument === '--version' || argument === '-v') {
7
- process.stdout.write(`${VERSION}\n`);
8
- process.exit(0);
9
- }
10
- if (argument === '--help' || argument === '-h') {
11
- process.stdout.write(`vault-go ${VERSION}\n\nServidor MCP cloud por stdio, executado com Bun.\n\nUso:\n bunx --bun vault-go@latest Instala no Codex quando executado em um terminal\n bunx --bun vault-go@latest install Instala no Codex explicitamente\n bunx --bun vault-go@latest serve Inicia o servidor MCP por stdio\n\nVariáveis:\n VAULT_GO_HOME Diretório de configuração (padrão: ~/.memvault)\n MEMVAULT_CONFIG_DIR Mesmo diretório usado pelo agente\n`);
12
- process.exit(0);
13
- }
6
+ const mode = resolveCliMode(argument, process.stdin.isTTY === true, process.stderr.isTTY === true);
14
7
  async function main() {
8
+ if (mode === 'version') {
9
+ process.stdout.write(`${VERSION}\n`);
10
+ return;
11
+ }
12
+ if (mode === 'help') {
13
+ process.stdout.write(helpText(VERSION));
14
+ return;
15
+ }
16
+ if (mode === 'invalid') {
17
+ throw new Error(`Comando desconhecido: ${argument}\n\n${helpText(VERSION)}`);
18
+ }
19
+ if (mode === 'setup') {
20
+ process.exitCode = await runSetup(process.argv.slice(3));
21
+ return;
22
+ }
23
+ if (mode === 'login') {
24
+ process.exitCode = await runLogin(process.argv.slice(3));
25
+ return;
26
+ }
15
27
  const server = createVaultGoServer();
16
28
  const transport = new StdioServerTransport();
17
29
  await server.connect(transport);
18
30
  }
19
- if (shouldInstallFromCli(argument, process.stdin.isTTY === true, process.stderr.isTTY === true)) {
20
- process.exitCode = installCodexMcp();
21
- }
22
- else {
23
- main().catch((error) => {
24
- const message = error instanceof Error ? error.stack ?? error.message : String(error);
25
- process.stderr.write(`vault-go falhou: ${message}\n`);
26
- process.exitCode = 1;
27
- });
28
- }
31
+ main().catch((error) => {
32
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
33
+ process.stderr.write(`vault-go falhou: ${message}\n`);
34
+ process.exitCode = 1;
35
+ });
@@ -0,0 +1,27 @@
1
+ export declare const MCP_SERVER_NAME = "vault-go";
2
+ export declare const MCP_SERVER_COMMAND: readonly ["bunx", "--bun", "vault-go@latest", "serve"];
3
+ export declare const MCP_CLIENTS: readonly ["codex", "claude", "claude-desktop", "cursor", "vscode", "copilot", "windsurf", "roo", "opencode"];
4
+ export type McpClient = (typeof MCP_CLIENTS)[number];
5
+ export interface DetectedClient {
6
+ client: McpClient;
7
+ detected: boolean;
8
+ evidence?: string;
9
+ }
10
+ export interface InstallResult {
11
+ client: McpClient;
12
+ status: 'installed' | 'already-installed';
13
+ destination: string;
14
+ }
15
+ type CommandResult = {
16
+ status: number | null;
17
+ error?: Error;
18
+ };
19
+ type CommandRunner = (command: string, args: readonly string[]) => CommandResult;
20
+ export declare function installMcpClient(client: McpClient, options?: {
21
+ home?: string;
22
+ cwd?: string;
23
+ runner?: CommandRunner;
24
+ }): InstallResult;
25
+ export declare function detectMcpClients(home?: string): DetectedClient[];
26
+ export declare function parseClientSelection(value: string): McpClient[];
27
+ export {};
@@ -0,0 +1,188 @@
1
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
2
+ import { homedir, platform } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ export const MCP_SERVER_NAME = 'vault-go';
6
+ export const MCP_SERVER_COMMAND = ['bunx', '--bun', 'vault-go@latest', 'serve'];
7
+ export const MCP_CLIENTS = [
8
+ 'codex',
9
+ 'claude',
10
+ 'claude-desktop',
11
+ 'cursor',
12
+ 'vscode',
13
+ 'copilot',
14
+ 'windsurf',
15
+ 'roo',
16
+ 'opencode',
17
+ ];
18
+ const runCommand = (command, args) => spawnSync(command, [...args], { stdio: 'ignore' });
19
+ function readJsonObject(path) {
20
+ if (!existsSync(path))
21
+ return {};
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
25
+ }
26
+ catch (error) {
27
+ throw new Error(`JSON inválido em ${path}; o arquivo não foi alterado.`, { cause: error });
28
+ }
29
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
30
+ throw new Error(`Configuração inválida em ${path}; o arquivo não foi alterado.`);
31
+ }
32
+ return parsed;
33
+ }
34
+ function atomicWriteJson(path, value) {
35
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
36
+ const temporary = join(dirname(path), `.${process.pid}.${Date.now()}.tmp`);
37
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
38
+ renameSync(temporary, path);
39
+ chmodSync(path, 0o600);
40
+ }
41
+ function standardServer() {
42
+ return {
43
+ command: MCP_SERVER_COMMAND[0],
44
+ args: MCP_SERVER_COMMAND.slice(1),
45
+ };
46
+ }
47
+ function mergeServer(path, rootKey = 'mcpServers', server = standardServer()) {
48
+ const config = readJsonObject(path);
49
+ const existing = config[rootKey];
50
+ if (existing !== undefined && (!existing || typeof existing !== 'object' || Array.isArray(existing))) {
51
+ throw new Error(`Campo ${rootKey} inválido em ${path}; o arquivo não foi alterado.`);
52
+ }
53
+ atomicWriteJson(path, {
54
+ ...config,
55
+ [rootKey]: {
56
+ ...existing,
57
+ [MCP_SERVER_NAME]: server,
58
+ },
59
+ });
60
+ }
61
+ function claudeDesktopPath(home) {
62
+ if (platform() === 'darwin') {
63
+ return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
64
+ }
65
+ if (platform() === 'win32') {
66
+ return join(process.env['APPDATA'] ?? join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
67
+ }
68
+ return join(home, '.config', 'Claude', 'claude_desktop_config.json');
69
+ }
70
+ function installViaCli(client, command, getArgs, addArgs, runner) {
71
+ const existing = runner(command, getArgs);
72
+ if (existing.status === 0) {
73
+ return { client, status: 'already-installed', destination: `${command} MCP config` };
74
+ }
75
+ const installed = runner(command, addArgs);
76
+ if (installed.error || installed.status !== 0) {
77
+ throw new Error(`Não foi possível configurar ${client}. Confirme que o comando "${command}" está instalado.`, { cause: installed.error });
78
+ }
79
+ return { client, status: 'installed', destination: `${command} MCP config` };
80
+ }
81
+ export function installMcpClient(client, options = {}) {
82
+ const home = options.home ?? homedir();
83
+ const cwd = options.cwd ?? process.cwd();
84
+ const runner = options.runner ?? runCommand;
85
+ if (client === 'codex') {
86
+ return installViaCli(client, 'codex', ['mcp', 'get', MCP_SERVER_NAME], ['mcp', 'add', MCP_SERVER_NAME, '--', ...MCP_SERVER_COMMAND], runner);
87
+ }
88
+ if (client === 'claude') {
89
+ return installViaCli(client, 'claude', ['mcp', 'get', MCP_SERVER_NAME], [
90
+ 'mcp',
91
+ 'add',
92
+ '--scope',
93
+ 'user',
94
+ '--transport',
95
+ 'stdio',
96
+ MCP_SERVER_NAME,
97
+ '--',
98
+ ...MCP_SERVER_COMMAND,
99
+ ], runner);
100
+ }
101
+ if (client === 'vscode') {
102
+ const payload = JSON.stringify({
103
+ name: MCP_SERVER_NAME,
104
+ command: MCP_SERVER_COMMAND[0],
105
+ args: MCP_SERVER_COMMAND.slice(1),
106
+ });
107
+ const installed = runner('code', ['--add-mcp', payload]);
108
+ if (installed.error || installed.status !== 0) {
109
+ throw new Error('Não foi possível configurar VS Code. Confirme que o comando "code" está instalado.');
110
+ }
111
+ return { client, status: 'installed', destination: 'VS Code user profile' };
112
+ }
113
+ let path;
114
+ if (client === 'claude-desktop')
115
+ path = claudeDesktopPath(home);
116
+ else if (client === 'cursor')
117
+ path = join(home, '.cursor', 'mcp.json');
118
+ else if (client === 'copilot')
119
+ path = join(home, '.copilot', 'mcp-config.json');
120
+ else if (client === 'windsurf')
121
+ path = join(home, '.codeium', 'windsurf', 'mcp_config.json');
122
+ else if (client === 'roo')
123
+ path = resolve(cwd, '.roo', 'mcp.json');
124
+ else
125
+ path = join(home, '.config', 'opencode', 'opencode.json');
126
+ if (client === 'opencode') {
127
+ mergeServer(path, 'mcp', {
128
+ type: 'local',
129
+ command: [...MCP_SERVER_COMMAND],
130
+ enabled: true,
131
+ });
132
+ }
133
+ else if (client === 'copilot') {
134
+ mergeServer(path, 'mcpServers', {
135
+ type: 'local',
136
+ ...standardServer(),
137
+ env: {},
138
+ tools: ['*'],
139
+ });
140
+ }
141
+ else {
142
+ mergeServer(path);
143
+ }
144
+ return { client, status: 'installed', destination: path };
145
+ }
146
+ export function detectMcpClients(home = homedir()) {
147
+ const definitions = [
148
+ ['codex', ['codex'], [join(home, '.codex')]],
149
+ ['claude', ['claude'], [join(home, '.claude')]],
150
+ ['claude-desktop', [], [claudeDesktopPath(home), '/Applications/Claude.app']],
151
+ ['cursor', ['cursor'], [join(home, '.cursor')]],
152
+ ['vscode', ['code'], [join(home, '.vscode')]],
153
+ ['copilot', ['copilot'], [join(home, '.copilot')]],
154
+ ['windsurf', ['windsurf'], [join(home, '.codeium', 'windsurf')]],
155
+ ['roo', [], []],
156
+ ['opencode', ['opencode'], [join(home, '.config', 'opencode')]],
157
+ ];
158
+ return definitions.map(([client, commands, paths]) => {
159
+ const command = commands.find((candidate) => Bun.which(candidate));
160
+ if (command)
161
+ return { client, detected: true, evidence: `comando ${command}` };
162
+ if (client === 'roo') {
163
+ const extensions = join(home, '.vscode', 'extensions');
164
+ try {
165
+ const detected = readdirSync(extensions).some((entry) => entry.toLowerCase().includes('roo-code'));
166
+ if (detected)
167
+ return { client, detected: true, evidence: extensions };
168
+ }
169
+ catch {
170
+ // VS Code ou Roo não estão instalados.
171
+ }
172
+ }
173
+ const path = paths.find((candidate) => existsSync(candidate));
174
+ return path
175
+ ? { client, detected: true, evidence: path }
176
+ : { client, detected: false };
177
+ });
178
+ }
179
+ export function parseClientSelection(value) {
180
+ const normalized = value.trim().toLowerCase();
181
+ if (normalized === 'all' || normalized === 'todos')
182
+ return [...MCP_CLIENTS];
183
+ const selected = [...new Set(normalized.split(',').map((item) => item.trim()).filter(Boolean))];
184
+ const invalid = selected.filter((item) => !MCP_CLIENTS.includes(item));
185
+ if (invalid.length > 0)
186
+ throw new Error(`Clientes MCP desconhecidos: ${invalid.join(', ')}`);
187
+ return selected;
188
+ }
@@ -0,0 +1,3 @@
1
+ import type { ReadStream, WriteStream } from 'node:tty';
2
+ export declare function promptText(question: string, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<string>;
3
+ export declare function promptSecret(question: string, input?: ReadStream, output?: WriteStream): Promise<string>;
package/dist/prompt.js ADDED
@@ -0,0 +1,51 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ export async function promptText(question, input = process.stdin, output = process.stderr) {
3
+ const readline = createInterface({ input, output, terminal: true });
4
+ try {
5
+ return (await readline.question(question)).trim();
6
+ }
7
+ finally {
8
+ readline.close();
9
+ }
10
+ }
11
+ export async function promptSecret(question, input = process.stdin, output = process.stderr) {
12
+ if (!input.isTTY || typeof input.setRawMode !== 'function') {
13
+ throw new Error('A autenticação interativa requer um terminal TTY.');
14
+ }
15
+ output.write(question);
16
+ const wasRaw = input.isRaw;
17
+ const wasPaused = input.isPaused();
18
+ input.setRawMode(true);
19
+ input.resume();
20
+ return new Promise((resolve, reject) => {
21
+ let value = '';
22
+ const cleanup = () => {
23
+ input.off('data', onData);
24
+ input.setRawMode(wasRaw);
25
+ if (wasPaused)
26
+ input.pause();
27
+ output.write('\n');
28
+ };
29
+ const onData = (chunk) => {
30
+ for (const character of String(chunk)) {
31
+ if (character === '\u0003') {
32
+ cleanup();
33
+ reject(new Error('Operação cancelada.'));
34
+ return;
35
+ }
36
+ if (character === '\r' || character === '\n') {
37
+ cleanup();
38
+ resolve(value);
39
+ return;
40
+ }
41
+ if (character === '\u007f' || character === '\b') {
42
+ value = value.slice(0, -1);
43
+ continue;
44
+ }
45
+ if (character >= ' ')
46
+ value += character;
47
+ }
48
+ };
49
+ input.on('data', onData);
50
+ });
51
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.8.1",
4
- "description": "Servidor MCP Bun para pesquisar e registrar memória na plataforma Vault.",
3
+ "version": "0.10.0",
4
+ "description": "Servidor MCP universal com autenticação e instalação multi-cliente para a plataforma Vault.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vault-go": "dist/index.js",
@@ -24,6 +24,12 @@
24
24
  "dist/index.d.ts",
25
25
  "dist/cli.js",
26
26
  "dist/cli.d.ts",
27
+ "dist/auth.js",
28
+ "dist/auth.d.ts",
29
+ "dist/installer.js",
30
+ "dist/installer.d.ts",
31
+ "dist/prompt.js",
32
+ "dist/prompt.d.ts",
27
33
  "dist/server.js",
28
34
  "dist/server.d.ts",
29
35
  "dist/config.js",
@@ -52,10 +58,10 @@
52
58
  },
53
59
  "repository": {
54
60
  "type": "git",
55
- "url": "git+https://github.com/GutierrezHenrique/vault-go.git"
61
+ "url": "git+https://github.com/resolveup-cloud/vault-go.git"
56
62
  },
57
63
  "bugs": {
58
- "url": "https://github.com/GutierrezHenrique/vault-go/issues"
64
+ "url": "https://github.com/resolveup-cloud/vault-go/issues"
59
65
  },
60
66
  "homepage": "https://vault.resolveup.com.br",
61
67
  "keywords": [
@@ -64,7 +70,11 @@
64
70
  "memory",
65
71
  "vault",
66
72
  "memvault",
67
- "knowledge"
73
+ "knowledge",
74
+ "codex",
75
+ "claude",
76
+ "copilot",
77
+ "cursor"
68
78
  ],
69
79
  "author": "Gutierrez Henrique",
70
80
  "license": "MIT",