vault-go 0.1.0 → 0.3.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,22 +1,23 @@
1
1
  # vault-go
2
2
 
3
- Servidor MCP local para pesquisar, ler e registrar conhecimento sincronizado
4
- pelo MemVault. Usa transporte `stdio`, roda com Node.js e trabalha somente nos
5
- caminhos permitidos em `~/.memvault/config.json`.
3
+ Servidor MCP em Bun para pesquisar, contextualizar e registrar memória na
4
+ plataforma Vault. A persistência fica no PostgreSQL do serviço Rust executado
5
+ no EasyPanel; este pacote não cria banco local.
6
6
 
7
- ## Instalação
7
+ ## Pré-requisito
8
8
 
9
- Execução direta:
9
+ Instale o [Bun](https://bun.sh/) e confirme:
10
10
 
11
11
  ```sh
12
- npx -y vault-go@latest
12
+ bun --version
13
13
  ```
14
14
 
15
- Instalação global:
15
+ ## Instalação MCP
16
+
17
+ Execução direta:
16
18
 
17
19
  ```sh
18
- npm install --global vault-go
19
- vault-go --version
20
+ bunx --bun vault-go@latest
20
21
  ```
21
22
 
22
23
  Configuração MCP genérica:
@@ -25,21 +26,23 @@ Configuração MCP genérica:
25
26
  {
26
27
  "mcpServers": {
27
28
  "vault-go": {
28
- "command": "npx",
29
- "args": ["-y", "vault-go@latest"]
29
+ "command": "bunx",
30
+ "args": ["--bun", "vault-go@latest"]
30
31
  }
31
32
  }
32
33
  }
33
34
  ```
34
35
 
35
- Para usar outro diretório local:
36
+ Por padrão, o MCP reutiliza `~/.memvault/config.json` e
37
+ `~/.memvault/auth.json`, criados pelo comando `memvault login`. Também é
38
+ possível apontar outro diretório:
36
39
 
37
40
  ```json
38
41
  {
39
42
  "mcpServers": {
40
43
  "vault-go": {
41
- "command": "npx",
42
- "args": ["-y", "vault-go@latest"],
44
+ "command": "bunx",
45
+ "args": ["--bun", "vault-go@latest"],
43
46
  "env": {
44
47
  "VAULT_GO_HOME": "/caminho/privado/.memvault"
45
48
  }
@@ -48,70 +51,69 @@ Para usar outro diretório local:
48
51
  }
49
52
  ```
50
53
 
51
- ## Ferramentas MCP
52
-
53
- - `vault_go_status`: configuração sanitizada, autenticação e caminhos.
54
- - `vault_go_list_files`: lista arquivos textuais monitorados.
55
- - `vault_go_search`: busca texto e retorna linha/trecho.
56
- - `vault_go_read_file`: lê até 1 MiB dentro dos caminhos permitidos.
57
- - `vault_go_write_note`: cria ou acrescenta Markdown em `mcp-notes`.
58
- - `vault_go_cloud_health`: testa a API pública sem enviar tokens ou conteúdo.
59
-
60
- O recurso `vault-go://status` fornece o mesmo resumo sanitizado em formato JSON.
61
-
62
- ## Integração com a nuvem
54
+ Para ambientes efêmeros, `VAULT_GO_TOKEN` aceita um access token e
55
+ `VAULT_GO_API_URL` troca o endpoint. Prefira o login normal, porque ele permite
56
+ renovação automática da sessão.
63
57
 
64
- O servidor MCP não implementa uma segunda sincronização e não lê chaves do
65
- cofre. Ao criar a primeira nota, ele adiciona `~/.memvault/mcp-notes` aos
66
- caminhos monitorados. O agente local existente:
67
-
68
- 1. detecta a nota;
69
- 2. cifra o conteúdo no dispositivo;
70
- 3. envia somente o blob cifrado para `vault.resolveup.com.br`;
71
- 4. sincroniza o arquivo com os outros dispositivos autorizados.
72
-
73
- Mantenha o agente em execução para sincronização contínua:
58
+ ## Ferramentas MCP
74
59
 
75
- ```sh
76
- memvault start
60
+ - `vault_go_status`: conexão e autenticação sem expor credenciais.
61
+ - `vault_go_cloud_health`: disponibilidade da plataforma.
62
+ - `vault_go_projects` e `vault_go_project_create`: projetos da conta.
63
+ - `vault_go_session_start` e `vault_go_session_end`: ciclo de sessão.
64
+ - `vault_go_event`: eventos de prompt, ferramenta e resultado.
65
+ - `vault_go_remember`: observações, decisões, descobertas e resumos.
66
+ - `vault_go_search`: busca textual com ranking e trechos.
67
+ - `vault_go_timeline`: contexto anterior e posterior a uma memória.
68
+ - `vault_go_context`: contexto progressivo com limite de caracteres.
69
+ - `vault_go_file_context`: histórico ligado aos arquivos lidos ou modificados.
70
+ - `vault_go_stats`: contagens isoladas da conta.
71
+ - `vault_go_forget`: exclusão explícita de uma memória.
72
+
73
+ O recurso `vault-go://status` fornece o resumo sanitizado em JSON.
74
+
75
+ ## Fluxo integrado
76
+
77
+ ```text
78
+ Cliente MCP -> vault-go (Bun) -> API autenticada (Bun)
79
+ -> engine interno (Rust)
80
+ -> PostgreSQL no EasyPanel
77
81
  ```
78
82
 
83
+ O engine não possui rota pública. A API valida o JWT do usuário, substitui-o
84
+ por uma credencial interna e informa ao engine a conta autenticada. Todas as
85
+ consultas aplicam esse identificador, impedindo leitura entre contas.
86
+
79
87
  ## Segurança
80
88
 
81
- - Nunca retorna access token ou refresh token.
82
- - Restringe leitura aos caminhos configurados.
83
- - Resolve caminhos reais e bloqueia travessia/symlinks externos.
84
- - Ignora diretórios ocultos, `node_modules`, `dist` e `.cache`.
85
- - Limita leitura e busca por arquivo a 1 MiB.
86
- - Grava notas somente em `mcp-notes`, com permissões privadas.
87
- - Usa HTTPS para health remoto; HTTP só é aceito em localhost.
88
- - Não escreve logs no `stdout`, que é reservado ao protocolo MCP.
89
+ - Não retorna access token nem refresh token em ferramentas ou recursos.
90
+ - Aceita HTTPS; HTTP somente em localhost.
91
+ - Renova o access token usando o refresh token local com permissão privada.
92
+ - Limita tamanhos, listas e janelas nos schemas das ferramentas.
93
+ - Reserva `stdout` exclusivamente para o protocolo MCP.
94
+ - Não persiste memória nem cria banco no dispositivo.
89
95
 
90
- O cliente MCP conectado pode receber o conteúdo dos arquivos que solicitar.
91
- Instale este servidor apenas em clientes confiáveis e revise as permissões antes
92
- de autorizar chamadas de leitura ou escrita.
96
+ Instale o MCP apenas em clientes confiáveis: o cliente conectado pode pedir
97
+ busca, gravação e exclusão de memórias dentro da conta autenticada.
93
98
 
94
99
  ## Desenvolvimento
95
100
 
96
101
  ```sh
97
102
  npm install
98
- npm run typecheck
99
- npm test
103
+ bun run typecheck
104
+ bun test src
105
+ bun run build
100
106
  npm run pack:check
101
107
  ```
102
108
 
103
- Requer Node.js `>= 20`.
104
-
105
109
  ## Publicação automática
106
110
 
107
- O GitHub Actions executa testes em cada pull request e push para `main`. Depois
108
- da versão inicial, o workflow de release interpreta commits convencionais,
109
- atualiza a versão, publica no npm e cria a release e a tag no GitHub:
111
+ Pushes em `main` passam por CI. Commits convencionais acionam release
112
+ semântica, publicação no npm, tag e GitHub Release:
110
113
 
111
- - `fix:` publica uma versão patch;
112
- - `feat:` publica uma versão minor;
113
- - `feat!:` ou `BREAKING CHANGE:` publica uma versão major;
114
- - `docs:`, `test:`, `chore:` e `ci:` não publicam uma nova versão.
114
+ - `fix:` gera patch;
115
+ - `feat:` gera minor;
116
+ - `feat!:` ou `BREAKING CHANGE:` gera major.
115
117
 
116
- O repositório precisa do segredo `NPM_TOKEN`; ele não deve ser salvo em arquivo,
117
- commit ou log.
118
+ O segredo `NPM_TOKEN` existe apenas no GitHub Actions e nunca deve ser salvo no
119
+ repositório ou em logs.
@@ -0,0 +1,36 @@
1
+ export interface VaultMemoryApi {
2
+ health(): Promise<unknown>;
3
+ projects(): Promise<unknown>;
4
+ createProject(input: Record<string, unknown>): Promise<unknown>;
5
+ startSession(input: Record<string, unknown>): Promise<unknown>;
6
+ endSession(id: string): Promise<unknown>;
7
+ event(input: Record<string, unknown>): Promise<unknown>;
8
+ remember(input: Record<string, unknown>): Promise<unknown>;
9
+ forget(id: string): Promise<unknown>;
10
+ search(input: Record<string, unknown>): Promise<unknown>;
11
+ timeline(input: Record<string, unknown>): Promise<unknown>;
12
+ context(input: Record<string, unknown>): Promise<unknown>;
13
+ fileContext(input: Record<string, unknown>): Promise<unknown>;
14
+ stats(): Promise<unknown>;
15
+ }
16
+ export declare class VaultCloudClient implements VaultMemoryApi {
17
+ private readonly home;
18
+ private readonly cloudFetch;
19
+ constructor(home?: string, cloudFetch?: typeof fetch);
20
+ health(): Promise<unknown>;
21
+ projects(): Promise<unknown>;
22
+ createProject(input: Record<string, unknown>): Promise<unknown>;
23
+ startSession(input: Record<string, unknown>): Promise<unknown>;
24
+ endSession(id: string): Promise<unknown>;
25
+ event(input: Record<string, unknown>): Promise<unknown>;
26
+ remember(input: Record<string, unknown>): Promise<unknown>;
27
+ forget(id: string): Promise<unknown>;
28
+ search(input: Record<string, unknown>): Promise<unknown>;
29
+ timeline(input: Record<string, unknown>): Promise<unknown>;
30
+ context(input: Record<string, unknown>): Promise<unknown>;
31
+ fileContext(input: Record<string, unknown>): Promise<unknown>;
32
+ stats(): Promise<unknown>;
33
+ private endpoint;
34
+ private accessToken;
35
+ private request;
36
+ }
package/dist/cloud.js ADDED
@@ -0,0 +1,121 @@
1
+ import { loadConfig, loadTokens, saveTokens, validateApiUrl, vaultHome, } from './config.js';
2
+ export class VaultCloudClient {
3
+ home;
4
+ cloudFetch;
5
+ constructor(home = vaultHome(), cloudFetch = globalThis.fetch) {
6
+ this.home = home;
7
+ this.cloudFetch = cloudFetch;
8
+ }
9
+ async health() {
10
+ return this.request('/health', { authenticated: false });
11
+ }
12
+ async projects() {
13
+ return this.request('/memory/projects');
14
+ }
15
+ async createProject(input) {
16
+ return this.request('/memory/projects', { method: 'POST', body: input });
17
+ }
18
+ async startSession(input) {
19
+ return this.request('/memory/sessions/start', { method: 'POST', body: input });
20
+ }
21
+ async endSession(id) {
22
+ return this.request(`/memory/sessions/${encodeURIComponent(id)}/end`, { method: 'POST' });
23
+ }
24
+ async event(input) {
25
+ return this.request('/memory/events', { method: 'POST', body: input });
26
+ }
27
+ async remember(input) {
28
+ return this.request('/memory/memories', { method: 'POST', body: input });
29
+ }
30
+ async forget(id) {
31
+ await this.request(`/memory/memories/${encodeURIComponent(id)}`, { method: 'DELETE' });
32
+ return { deleted: true, id };
33
+ }
34
+ async search(input) {
35
+ return this.request('/memory/search', { method: 'POST', body: input });
36
+ }
37
+ async timeline(input) {
38
+ return this.request('/memory/timeline', { method: 'POST', body: input });
39
+ }
40
+ async context(input) {
41
+ return this.request('/memory/context', { method: 'POST', body: input });
42
+ }
43
+ async fileContext(input) {
44
+ return this.request('/memory/files/context', { method: 'POST', body: input });
45
+ }
46
+ async stats() {
47
+ return this.request('/memory/stats');
48
+ }
49
+ endpoint(path) {
50
+ const base = validateApiUrl(loadConfig(this.home).apiUrl);
51
+ const prefix = base.pathname.replace(/\/$/u, '');
52
+ return new URL(`${prefix}${path}`, base.origin);
53
+ }
54
+ async accessToken(forceRefresh = false) {
55
+ const environmentToken = process.env['VAULT_GO_TOKEN'];
56
+ if (environmentToken)
57
+ return environmentToken;
58
+ const tokens = loadTokens(this.home);
59
+ if (!tokens) {
60
+ throw new Error('Vault não autenticado. Execute `memvault login` antes de iniciar o MCP.');
61
+ }
62
+ const valid = !forceRefresh &&
63
+ (!tokens.expiresAt || Date.now() < tokens.expiresAt - 60_000);
64
+ if (valid)
65
+ return tokens.accessToken;
66
+ if (!tokens.refreshToken)
67
+ throw new Error('Sessão expirada. Execute `memvault login` novamente.');
68
+ const response = await this.cloudFetch(this.endpoint('/auth/refresh'), {
69
+ method: 'POST',
70
+ headers: { 'content-type': 'application/json' },
71
+ body: JSON.stringify({ refreshToken: tokens.refreshToken }),
72
+ signal: AbortSignal.timeout(10_000),
73
+ });
74
+ if (!response.ok) {
75
+ throw new Error('Sessão expirada. Execute `memvault login` novamente.');
76
+ }
77
+ const refreshed = (await response.json());
78
+ const stored = {
79
+ ...refreshed,
80
+ expiresAt: Date.now() + (refreshed.expiresIn ?? 900) * 1000,
81
+ };
82
+ saveTokens(stored, this.home);
83
+ return stored.accessToken;
84
+ }
85
+ async request(path, options = {}) {
86
+ const headers = new Headers({ accept: 'application/json' });
87
+ if (options.body)
88
+ headers.set('content-type', 'application/json');
89
+ if (options.authenticated !== false) {
90
+ headers.set('authorization', `Bearer ${await this.accessToken(options.retried)}`);
91
+ }
92
+ const request = {
93
+ method: options.method ?? 'GET',
94
+ headers,
95
+ signal: AbortSignal.timeout(15_000),
96
+ };
97
+ if (options.body)
98
+ request.body = JSON.stringify(options.body);
99
+ const response = await this.cloudFetch(this.endpoint(path), request);
100
+ if (response.status === 401 && options.authenticated !== false && !options.retried) {
101
+ return this.request(path, { ...options, retried: true });
102
+ }
103
+ const text = await response.text();
104
+ const payload = text ? safeJson(text) : null;
105
+ if (!response.ok) {
106
+ const message = payload && typeof payload === 'object' && 'error' in payload
107
+ ? String(payload.error)
108
+ : `Vault respondeu HTTP ${response.status}`;
109
+ throw new Error(message);
110
+ }
111
+ return payload;
112
+ }
113
+ }
114
+ function safeJson(text) {
115
+ try {
116
+ return JSON.parse(text);
117
+ }
118
+ catch {
119
+ return text;
120
+ }
121
+ }
package/dist/config.d.ts CHANGED
@@ -6,20 +6,23 @@ export interface VaultConfig {
6
6
  intervalSeconds: number;
7
7
  watchPaths: string[];
8
8
  }
9
+ export interface StoredTokens {
10
+ accessToken: string;
11
+ refreshToken?: string;
12
+ expiresIn?: number;
13
+ expiresAt?: number;
14
+ }
9
15
  export interface VaultStatus {
10
- vaultHome: string;
11
16
  apiUrl: string;
12
17
  configured: boolean;
13
18
  authenticated: boolean;
14
- watchPaths: string[];
15
- notesPath: string;
19
+ account?: string;
16
20
  }
17
21
  export declare function vaultHome(environment?: NodeJS.ProcessEnv): string;
18
22
  export declare function configPath(home?: string): string;
19
23
  export declare function authPath(home?: string): string;
20
- export declare function notesPath(home?: string): string;
21
- export declare function ensurePrivateDirectory(directory: string): void;
22
24
  export declare function loadConfig(home?: string): VaultConfig;
23
- export declare function saveConfig(config: VaultConfig, home?: string): void;
24
- export declare function ensureNotesPath(home?: string): string;
25
+ export declare function loadTokens(home?: string): StoredTokens | null;
26
+ export declare function saveTokens(tokens: StoredTokens, home?: string): void;
25
27
  export declare function getStatus(home?: string): VaultStatus;
28
+ export declare function validateApiUrl(value: string): URL;
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
1
+ import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  export const DEFAULT_API_URL = 'https://vault.resolveup.com.br/api';
@@ -8,7 +8,9 @@ const DEFAULT_CONFIG = {
8
8
  watchPaths: [],
9
9
  };
10
10
  export function vaultHome(environment = process.env) {
11
- const configured = environment['VAULT_GO_HOME'] ?? environment['MEMVAULT_HOME'];
11
+ const configured = environment['VAULT_GO_HOME'] ??
12
+ environment['MEMVAULT_CONFIG_DIR'] ??
13
+ environment['MEMVAULT_HOME'];
12
14
  return resolve(configured || join(homedir(), '.memvault'));
13
15
  }
14
16
  export function configPath(home = vaultHome()) {
@@ -17,59 +19,61 @@ export function configPath(home = vaultHome()) {
17
19
  export function authPath(home = vaultHome()) {
18
20
  return join(home, 'auth.json');
19
21
  }
20
- export function notesPath(home = vaultHome()) {
21
- return join(home, 'mcp-notes');
22
- }
23
- export function ensurePrivateDirectory(directory) {
24
- mkdirSync(directory, { recursive: true, mode: 0o700 });
25
- chmodSync(directory, 0o700);
26
- }
27
22
  export function loadConfig(home = vaultHome()) {
28
23
  const file = configPath(home);
29
- if (!existsSync(file))
30
- return { ...DEFAULT_CONFIG, watchPaths: [] };
24
+ const environmentApiUrl = process.env['VAULT_GO_API_URL'];
25
+ if (!existsSync(file)) {
26
+ return {
27
+ ...DEFAULT_CONFIG,
28
+ apiUrl: environmentApiUrl || DEFAULT_API_URL,
29
+ watchPaths: [],
30
+ };
31
+ }
31
32
  try {
32
33
  const parsed = JSON.parse(readFileSync(file, 'utf8'));
33
- const watchPaths = Array.isArray(parsed.watchPaths)
34
- ? parsed.watchPaths.filter((entry) => typeof entry === 'string')
35
- : [];
36
34
  return {
37
35
  ...DEFAULT_CONFIG,
38
36
  ...parsed,
39
- apiUrl: parsed.apiUrl || DEFAULT_API_URL,
40
- watchPaths,
37
+ apiUrl: environmentApiUrl || parsed.apiUrl || DEFAULT_API_URL,
38
+ watchPaths: Array.isArray(parsed.watchPaths) ? parsed.watchPaths : [],
41
39
  };
42
40
  }
43
41
  catch {
44
42
  return { ...DEFAULT_CONFIG, watchPaths: [] };
45
43
  }
46
44
  }
47
- export function saveConfig(config, home = vaultHome()) {
48
- ensurePrivateDirectory(home);
49
- const destination = configPath(home);
50
- const temporary = join(dirname(destination), `.config.${process.pid}.tmp`);
51
- writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
45
+ export function loadTokens(home = vaultHome()) {
46
+ try {
47
+ const parsed = JSON.parse(readFileSync(authPath(home), 'utf8'));
48
+ return typeof parsed.accessToken === 'string' ? parsed : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ 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 });
52
58
  renameSync(temporary, destination);
53
59
  chmodSync(destination, 0o600);
54
60
  }
55
- export function ensureNotesPath(home = vaultHome()) {
56
- const directory = notesPath(home);
57
- ensurePrivateDirectory(directory);
58
- const config = loadConfig(home);
59
- const normalized = new Set(config.watchPaths.map((entry) => resolve(entry)));
60
- if (!normalized.has(resolve(directory))) {
61
- saveConfig({ ...config, watchPaths: [...config.watchPaths, directory] }, home);
62
- }
63
- return directory;
64
- }
65
61
  export function getStatus(home = vaultHome()) {
66
62
  const config = loadConfig(home);
67
- return {
68
- vaultHome: home,
63
+ const status = {
69
64
  apiUrl: config.apiUrl,
70
65
  configured: existsSync(configPath(home)),
71
- authenticated: existsSync(authPath(home)),
72
- watchPaths: config.watchPaths.map((entry) => resolve(entry)),
73
- notesPath: notesPath(home),
66
+ authenticated: Boolean(process.env['VAULT_GO_TOKEN']) || loadTokens(home) !== null,
74
67
  };
68
+ if (config.email)
69
+ status.account = config.email;
70
+ return status;
71
+ }
72
+ export function validateApiUrl(value) {
73
+ const url = new URL(value);
74
+ const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
75
+ if (url.protocol !== 'https:' && !(local && url.protocol === 'http:')) {
76
+ throw new Error('A API deve usar HTTPS, exceto em localhost.');
77
+ }
78
+ return url;
75
79
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  export {};
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { createVaultGoServer, VERSION } from './server.js';
4
4
  const argument = process.argv[2];
@@ -7,7 +7,7 @@ if (argument === '--version' || argument === '-v') {
7
7
  process.exit(0);
8
8
  }
9
9
  if (argument === '--help' || argument === '-h') {
10
- process.stdout.write(`vault-go ${VERSION}\n\nServidor MCP local por stdio.\n\nVariáveis:\n VAULT_GO_HOME Diretório local do vault (padrão: ~/.memvault)\n MEMVAULT_HOME Alias compatível com o agente local\n`);
10
+ process.stdout.write(`vault-go ${VERSION}\n\nServidor MCP cloud por stdio, executado com Bun.\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`);
11
11
  process.exit(0);
12
12
  }
13
13
  async function main() {
package/dist/server.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare const VERSION = "0.1.0";
3
- export declare function createVaultGoServer(home?: string): McpServer;
2
+ import { type VaultMemoryApi } from './cloud.js';
3
+ export declare const VERSION: string;
4
+ export declare function createVaultGoServer(home?: string, cloud?: VaultMemoryApi): McpServer;
package/dist/server.js CHANGED
@@ -1,137 +1,177 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { ensureNotesPath, getStatus, loadConfig, notesPath, vaultHome, } from './config.js';
4
- import { listVaultFiles, readVaultFile, searchVaultFiles, writeVaultNote, } from './files.js';
5
- export const VERSION = '0.1.0';
3
+ import { VaultCloudClient } from './cloud.js';
4
+ import { getStatus, vaultHome } from './config.js';
5
+ import packageJson from '../package.json' with { type: 'json' };
6
+ export const VERSION = packageJson.version;
6
7
  function jsonResult(value) {
7
- const text = JSON.stringify(value, null, 2);
8
+ const structuredContent = value !== null && typeof value === 'object' && !Array.isArray(value)
9
+ ? value
10
+ : { value };
8
11
  return {
9
- content: [{ type: 'text', text }],
10
- structuredContent: value,
12
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
13
+ structuredContent,
11
14
  };
12
15
  }
13
16
  function errorResult(error) {
14
17
  const message = error instanceof Error ? error.message : 'Erro inesperado.';
15
- return {
16
- content: [{ type: 'text', text: message }],
17
- isError: true,
18
- };
19
- }
20
- function rootsFor(home) {
21
- const config = loadConfig(home);
22
- return [...config.watchPaths, notesPath(home)];
18
+ return { content: [{ type: 'text', text: message }], isError: true };
23
19
  }
24
- function validateApiUrl(value) {
25
- const url = new URL(value);
26
- const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
27
- if (url.protocol !== 'https:' && !(local && url.protocol === 'http:')) {
28
- throw new Error('A API deve usar HTTPS, exceto em localhost.');
20
+ async function run(operation) {
21
+ try {
22
+ return jsonResult(await operation());
23
+ }
24
+ catch (error) {
25
+ return errorResult(error);
29
26
  }
30
- return url;
31
27
  }
32
- export function createVaultGoServer(home = vaultHome()) {
28
+ const uuid = z.string().uuid();
29
+ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudClient(home)) {
33
30
  const server = new McpServer({ name: 'vault-go', version: VERSION });
34
31
  server.registerTool('vault_go_status', {
35
32
  title: 'Status do Vault',
36
- description: 'Mostra configuração local, autenticação e caminhos monitorados sem revelar tokens.',
33
+ description: 'Mostra conexão e autenticação sem revelar credenciais.',
37
34
  inputSchema: {},
38
35
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
39
36
  }, async () => jsonResult(getStatus(home)));
40
- server.registerTool('vault_go_list_files', {
41
- title: 'Listar arquivos do Vault',
42
- description: 'Lista arquivos textuais dentro dos caminhos monitorados.',
37
+ server.registerTool('vault_go_cloud_health', {
38
+ title: 'Verificar nuvem do Vault',
39
+ description: 'Verifica a disponibilidade da plataforma Vault.',
40
+ inputSchema: {},
41
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
42
+ }, async () => run(() => cloud.health()));
43
+ server.registerTool('vault_go_projects', {
44
+ title: 'Listar projetos',
45
+ description: 'Lista os projetos de memória da conta autenticada.',
46
+ inputSchema: {},
47
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
48
+ }, async () => run(() => cloud.projects()));
49
+ server.registerTool('vault_go_project_create', {
50
+ title: 'Criar projeto',
51
+ description: 'Cria um projeto de memória no Vault.',
43
52
  inputSchema: {
44
- query: z.string().trim().optional().describe('Filtro opcional aplicado ao caminho relativo.'),
45
- limit: z.number().int().min(1).max(200).default(50),
53
+ name: z.string().trim().min(1).max(200),
54
+ rootPath: z.string().trim().max(2_000).optional(),
46
55
  },
47
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
48
- }, async ({ query, limit }) => {
49
- try {
50
- const options = query === undefined ? { limit } : { query, limit };
51
- return jsonResult({ files: listVaultFiles(rootsFor(home), options) });
52
- }
53
- catch (error) {
54
- return errorResult(error);
55
- }
56
- });
56
+ annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
57
+ }, async (input) => run(() => cloud.createProject(input)));
58
+ server.registerTool('vault_go_session_start', {
59
+ title: 'Iniciar sessão',
60
+ description: 'Inicia ou recupera uma sessão de trabalho no Vault.',
61
+ inputSchema: {
62
+ projectId: uuid,
63
+ externalSessionId: z.string().trim().max(500).optional(),
64
+ platformSource: z.string().trim().min(1).max(100).default('mcp'),
65
+ },
66
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
67
+ }, async (input) => run(() => cloud.startSession(input)));
68
+ server.registerTool('vault_go_session_end', {
69
+ title: 'Encerrar sessão',
70
+ description: 'Marca uma sessão como concluída.',
71
+ inputSchema: { id: uuid },
72
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
73
+ }, async ({ id }) => run(() => cloud.endSession(id)));
74
+ server.registerTool('vault_go_event', {
75
+ title: 'Registrar evento',
76
+ description: 'Registra um evento de prompt, ferramenta ou resultado na sessão.',
77
+ inputSchema: {
78
+ projectId: uuid,
79
+ sessionId: uuid.optional(),
80
+ sourceAdapter: z.string().trim().min(1).max(100).default('mcp'),
81
+ sourceEventId: z.string().trim().max(500).optional(),
82
+ eventType: z.string().trim().min(1).max(100),
83
+ payload: z.record(z.unknown()).default({}),
84
+ occurredAtEpoch: z.number().int().positive().optional(),
85
+ },
86
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
87
+ }, async (input) => run(() => cloud.event(input)));
88
+ server.registerTool('vault_go_remember', {
89
+ title: 'Registrar memória',
90
+ description: 'Persiste uma observação, decisão, descoberta ou resumo no Vault.',
91
+ inputSchema: {
92
+ projectId: uuid,
93
+ sessionId: uuid.optional(),
94
+ kind: z.string().trim().min(1).max(100).default('observation'),
95
+ memoryType: z.string().trim().min(1).max(100).default('discovery'),
96
+ title: z.string().trim().max(500).optional(),
97
+ content: z.string().trim().min(1).max(200_000),
98
+ facts: z.array(z.string().trim().min(1).max(2_000)).max(200).default([]),
99
+ concepts: z.array(z.string().trim().min(1).max(500)).max(200).default([]),
100
+ filesRead: z.array(z.string().trim().min(1).max(2_000)).max(500).default([]),
101
+ filesModified: z.array(z.string().trim().min(1).max(2_000)).max(500).default([]),
102
+ },
103
+ annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
104
+ }, async (input) => run(() => cloud.remember(input)));
105
+ server.registerTool('vault_go_forget', {
106
+ title: 'Excluir memória',
107
+ description: 'Exclui uma memória da conta autenticada.',
108
+ inputSchema: { id: uuid },
109
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
110
+ }, async ({ id }) => run(() => cloud.forget(id)));
57
111
  server.registerTool('vault_go_search', {
58
112
  title: 'Pesquisar no Vault',
59
- description: 'Pesquisa texto nos arquivos monitorados e retorna trechos com número da linha.',
113
+ description: 'Pesquisa memórias persistidas na plataforma Vault.',
60
114
  inputSchema: {
61
- query: z.string().trim().min(1).max(500),
62
- limit: z.number().int().min(1).max(200).default(50),
115
+ query: z.string().trim().min(1).max(1_000),
116
+ projectId: uuid.optional(),
117
+ kind: z.string().trim().max(100).optional(),
118
+ memoryType: z.string().trim().max(100).optional(),
119
+ limit: z.number().int().min(1).max(200).default(20),
63
120
  },
64
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
65
- }, async ({ query, limit }) => {
66
- try {
67
- return jsonResult({ matches: searchVaultFiles(rootsFor(home), query, limit) });
68
- }
69
- catch (error) {
70
- return errorResult(error);
71
- }
72
- });
73
- server.registerTool('vault_go_read_file', {
74
- title: 'Ler arquivo do Vault',
75
- description: 'Lê um arquivo textual permitido, limitado a 1 MiB e sem atravessar os caminhos monitorados.',
121
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
122
+ }, async (input) => run(() => cloud.search(input)));
123
+ server.registerTool('vault_go_timeline', {
124
+ title: 'Abrir linha do tempo',
125
+ description: 'Retorna memórias anteriores e posteriores a uma memória âncora.',
76
126
  inputSchema: {
77
- path: z.string().trim().min(1).describe('Caminho absoluto ou relativo a um diretório monitorado.'),
127
+ anchorId: uuid,
128
+ before: z.number().int().min(1).max(100).default(5),
129
+ after: z.number().int().min(1).max(100).default(5),
78
130
  },
79
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
80
- }, async ({ path }) => {
81
- try {
82
- return jsonResult(readVaultFile(path, rootsFor(home)));
83
- }
84
- catch (error) {
85
- return errorResult(error);
86
- }
87
- });
88
- server.registerTool('vault_go_write_note', {
89
- title: 'Registrar nota no Vault',
90
- description: 'Cria ou acrescenta uma nota Markdown em mcp-notes, monitorada pelo agente de sincronização.',
131
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
132
+ }, async (input) => run(() => cloud.timeline(input)));
133
+ server.registerTool('vault_go_context', {
134
+ title: 'Montar contexto',
135
+ description: 'Monta contexto progressivo, limitado e pronto para injeção no cliente.',
91
136
  inputSchema: {
92
- title: z.string().trim().min(1).max(200),
93
- content: z.string().trim().min(1).max(200_000),
94
- mode: z.enum(['create', 'append']).default('create'),
137
+ projectId: uuid,
138
+ query: z.string().trim().max(1_000).optional(),
139
+ limit: z.number().int().min(1).max(100).default(20),
140
+ maxChars: z.number().int().min(1_000).max(100_000).default(24_000),
95
141
  },
96
- annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: false },
97
- }, async ({ title, content, mode }) => {
98
- try {
99
- const directory = ensureNotesPath(home);
100
- return jsonResult({ path: writeVaultNote(directory, title, content, mode), mode });
101
- }
102
- catch (error) {
103
- return errorResult(error);
104
- }
105
- });
106
- server.registerTool('vault_go_cloud_health', {
107
- title: 'Verificar nuvem do Vault',
108
- description: 'Verifica o health público da API configurada sem enviar arquivos, notas ou tokens.',
142
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
143
+ }, async (input) => run(() => cloud.context(input)));
144
+ server.registerTool('vault_go_file_context', {
145
+ title: 'Contexto de arquivos',
146
+ description: 'Recupera memórias anteriores associadas a um ou mais arquivos do projeto.',
147
+ inputSchema: {
148
+ projectId: uuid,
149
+ paths: z.array(z.string().trim().min(1).max(2_000)).min(1).max(20),
150
+ limit: z.number().int().min(1).max(100).default(20),
151
+ maxChars: z.number().int().min(1_000).max(100_000).default(16_000),
152
+ },
153
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
154
+ }, async (input) => run(() => cloud.fileContext(input)));
155
+ server.registerTool('vault_go_stats', {
156
+ title: 'Estatísticas do Vault',
157
+ description: 'Mostra contagens da conta autenticada.',
109
158
  inputSchema: {},
110
159
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
111
- }, async () => {
112
- try {
113
- const status = getStatus(home);
114
- const base = validateApiUrl(status.apiUrl);
115
- const healthUrl = new URL(`${base.pathname.replace(/\/$/u, '')}/health`, base.origin);
116
- const response = await fetch(healthUrl, {
117
- headers: { Accept: 'application/json' },
118
- signal: AbortSignal.timeout(10_000),
119
- });
120
- const body = await response.text();
121
- return jsonResult({ ok: response.ok, status: response.status, url: healthUrl.href, body });
122
- }
123
- catch (error) {
124
- return errorResult(error);
125
- }
126
- });
160
+ }, async () => run(() => cloud.stats()));
127
161
  server.registerResource('vault-go-status', 'vault-go://status', {
128
162
  title: 'Status do Vault Go',
129
- description: 'Resumo sanitizado da configuração local.',
163
+ description: 'Resumo sanitizado da conexão cloud.',
130
164
  mimeType: 'application/json',
131
165
  }, async (uri) => {
132
166
  const status = getStatus(home);
133
167
  return {
134
- contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(status, null, 2) }],
168
+ contents: [
169
+ {
170
+ uri: uri.href,
171
+ mimeType: 'application/json',
172
+ text: JSON.stringify(status, null, 2),
173
+ },
174
+ ],
135
175
  };
136
176
  });
137
177
  return server;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.1.0",
4
- "description": "Servidor MCP local para pesquisar, ler e registrar conhecimento sincronizado pelo MemVault.",
3
+ "version": "0.3.0",
4
+ "description": "Servidor MCP Bun para pesquisar e registrar memória na plataforma Vault.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vault-go": "dist/index.js",
@@ -26,21 +26,22 @@
26
26
  "dist/server.d.ts",
27
27
  "dist/config.js",
28
28
  "dist/config.d.ts",
29
- "dist/files.js",
30
- "dist/files.d.ts",
29
+ "dist/cloud.js",
30
+ "dist/cloud.d.ts",
31
31
  "README.md",
32
32
  "LICENSE"
33
33
  ],
34
34
  "scripts": {
35
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
36
- "build": "npm run clean && tsc -p tsconfig.json",
35
+ "clean": "bun -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
36
+ "build": "bun run clean && tsc -p tsconfig.json",
37
37
  "typecheck": "tsc -p tsconfig.json --noEmit",
38
- "test": "npm run build && node --test dist/*.test.js",
38
+ "test": "bun run build && bun test src",
39
39
  "pack:check": "npm pack --dry-run",
40
40
  "release": "semantic-release",
41
- "prepublishOnly": "npm test && npm run pack:check"
41
+ "prepublishOnly": "bun test src && npm run pack:check"
42
42
  },
43
43
  "engines": {
44
+ "bun": ">=1.3.0",
44
45
  "node": ">=20"
45
46
  },
46
47
  "publishConfig": {
@@ -74,6 +75,7 @@
74
75
  "@semantic-release/github": "12.0.9",
75
76
  "@semantic-release/npm": "13.1.5",
76
77
  "@semantic-release/release-notes-generator": "14.1.1",
78
+ "@types/bun": "^1.3.0",
77
79
  "@types/node": "^22.15.0",
78
80
  "semantic-release": "25.0.8",
79
81
  "typescript": "^5.9.3"
package/dist/files.d.ts DELETED
@@ -1,25 +0,0 @@
1
- export declare const MAX_READ_BYTES = 1048576;
2
- export interface VaultFile {
3
- path: string;
4
- root: string;
5
- relativePath: string;
6
- size: number;
7
- modifiedAt: string;
8
- }
9
- export interface SearchMatch {
10
- path: string;
11
- relativePath: string;
12
- line: number;
13
- snippet: string;
14
- }
15
- export declare function resolveAllowedFile(input: string, roots: string[]): string;
16
- export declare function listVaultFiles(roots: string[], options?: {
17
- query?: string;
18
- limit?: number;
19
- }): VaultFile[];
20
- export declare function readVaultFile(input: string, roots: string[]): {
21
- path: string;
22
- text: string;
23
- };
24
- export declare function searchVaultFiles(roots: string[], query: string, limit?: number): SearchMatch[];
25
- export declare function writeVaultNote(notesRoot: string, title: string, content: string, mode?: 'create' | 'append'): string;
package/dist/files.js DELETED
@@ -1,161 +0,0 @@
1
- import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync, } from 'node:fs';
2
- import { basename, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
- export const MAX_READ_BYTES = 1_048_576;
4
- const DEFAULT_LIMIT = 50;
5
- const MAX_LIMIT = 200;
6
- const TEXT_EXTENSIONS = new Set([
7
- '.md',
8
- '.mdx',
9
- '.txt',
10
- '.json',
11
- '.jsonl',
12
- '.yaml',
13
- '.yml',
14
- '.toml',
15
- '.csv',
16
- '.log',
17
- ]);
18
- const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', '.cache']);
19
- function boundedLimit(limit) {
20
- if (!limit || !Number.isFinite(limit))
21
- return DEFAULT_LIMIT;
22
- return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(limit)));
23
- }
24
- function isTextFile(file) {
25
- return TEXT_EXTENSIONS.has(extname(file).toLowerCase());
26
- }
27
- function normalizedRoots(roots) {
28
- return [...new Set(roots.map((root) => resolve(root)))].filter((root) => {
29
- try {
30
- return statSync(root).isDirectory();
31
- }
32
- catch {
33
- return false;
34
- }
35
- });
36
- }
37
- function isInside(candidate, root) {
38
- return candidate === root || candidate.startsWith(`${root}${sep}`);
39
- }
40
- export function resolveAllowedFile(input, roots) {
41
- const availableRoots = normalizedRoots(roots);
42
- const candidates = isAbsolute(input)
43
- ? [resolve(input)]
44
- : availableRoots.map((root) => resolve(root, input));
45
- for (const candidate of candidates) {
46
- if (!existsSync(candidate))
47
- continue;
48
- const actual = realpathSync(candidate);
49
- if (!lstatSync(actual).isFile())
50
- continue;
51
- if (availableRoots.some((root) => isInside(actual, realpathSync(root))))
52
- return actual;
53
- }
54
- throw new Error('Arquivo inexistente ou fora dos caminhos monitorados.');
55
- }
56
- export function listVaultFiles(roots, options = {}) {
57
- const limit = boundedLimit(options.limit);
58
- const query = options.query?.trim().toLocaleLowerCase();
59
- const results = [];
60
- for (const root of normalizedRoots(roots)) {
61
- const stack = [root];
62
- while (stack.length > 0 && results.length < limit) {
63
- const current = stack.pop();
64
- if (!current)
65
- break;
66
- const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
67
- for (const entry of entries) {
68
- if (entry.name.startsWith('.'))
69
- continue;
70
- const absolute = join(current, entry.name);
71
- if (entry.isSymbolicLink())
72
- continue;
73
- if (entry.isDirectory()) {
74
- if (!IGNORED_DIRECTORIES.has(entry.name))
75
- stack.push(absolute);
76
- continue;
77
- }
78
- if (!entry.isFile() || !isTextFile(entry.name))
79
- continue;
80
- const relativePath = relative(root, absolute);
81
- if (query && !relativePath.toLocaleLowerCase().includes(query))
82
- continue;
83
- const stats = statSync(absolute);
84
- results.push({
85
- path: absolute,
86
- root,
87
- relativePath,
88
- size: stats.size,
89
- modifiedAt: stats.mtime.toISOString(),
90
- });
91
- if (results.length >= limit)
92
- break;
93
- }
94
- }
95
- if (results.length >= limit)
96
- break;
97
- }
98
- return results;
99
- }
100
- export function readVaultFile(input, roots) {
101
- const file = resolveAllowedFile(input, roots);
102
- const size = statSync(file).size;
103
- if (size > MAX_READ_BYTES) {
104
- throw new Error(`Arquivo excede o limite de ${MAX_READ_BYTES} bytes.`);
105
- }
106
- if (!isTextFile(file))
107
- throw new Error('Tipo de arquivo não permitido para leitura textual.');
108
- return { path: file, text: readFileSync(file, 'utf8') };
109
- }
110
- export function searchVaultFiles(roots, query, limit) {
111
- const normalizedQuery = query.trim().toLocaleLowerCase();
112
- if (!normalizedQuery)
113
- throw new Error('A consulta não pode ser vazia.');
114
- const maxResults = boundedLimit(limit);
115
- const files = listVaultFiles(roots, { limit: MAX_LIMIT });
116
- const matches = [];
117
- for (const file of files) {
118
- if (file.size > MAX_READ_BYTES)
119
- continue;
120
- const lines = readFileSync(file.path, 'utf8').split(/\r?\n/u);
121
- for (let index = 0; index < lines.length; index += 1) {
122
- const line = lines[index] ?? '';
123
- if (!line.toLocaleLowerCase().includes(normalizedQuery))
124
- continue;
125
- matches.push({
126
- path: file.path,
127
- relativePath: file.relativePath,
128
- line: index + 1,
129
- snippet: line.trim().slice(0, 500),
130
- });
131
- if (matches.length >= maxResults)
132
- return matches;
133
- }
134
- }
135
- return matches;
136
- }
137
- function safeSlug(title) {
138
- const slug = title
139
- .normalize('NFKD')
140
- .replace(/[\u0300-\u036f]/gu, '')
141
- .toLocaleLowerCase()
142
- .replace(/[^a-z0-9]+/gu, '-')
143
- .replace(/^-+|-+$/gu, '')
144
- .slice(0, 80);
145
- return slug || `note-${Date.now()}`;
146
- }
147
- export function writeVaultNote(notesRoot, title, content, mode = 'create') {
148
- mkdirSync(notesRoot, { recursive: true, mode: 0o700 });
149
- const file = join(notesRoot, `${safeSlug(title)}.md`);
150
- const body = `# ${title.trim()}\n\n${content.trim()}\n`;
151
- if (mode === 'create' && existsSync(file)) {
152
- throw new Error(`A nota ${basename(file)} já existe; use mode=append.`);
153
- }
154
- if (mode === 'append' && existsSync(file)) {
155
- appendFileSync(file, `\n${content.trim()}\n`, { encoding: 'utf8', mode: 0o600 });
156
- }
157
- else {
158
- writeFileSync(file, body, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
159
- }
160
- return file;
161
- }