vault-go 0.19.1 → 0.21.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
@@ -62,6 +62,13 @@ Execute em um terminal:
62
62
  bunx --bun vault-go@latest
63
63
  ```
64
64
 
65
+ A instalação abre com uma animação ASCII do Vault Go e mostra um indicador
66
+ enquanto verifica os motores disponíveis. Em terminais pequenos, a abertura usa
67
+ uma versão compacta. Para desativar, execute `vault-go setup --no-animation` ou
68
+ defina `VAULT_GO_NO_ANIMATION=1`. `NO_COLOR`, `TERM=dumb`, CI e saídas sem terminal
69
+ também usam a apresentação estática, sem códigos ANSI ou espera. O modo MCP
70
+ `serve` não exibe a animação.
71
+
65
72
  O assistente:
66
73
 
67
74
  1. valida uma sessão existente em `~/.memvault`;
@@ -174,7 +181,7 @@ limitados.
174
181
 
175
182
  O seletor oferece Codex (Terra, Luna ou Sol pela assinatura ChatGPT), Claude (CLI da assinatura), OpenRouter (chave), Gemini (chave) ou o Vault AI Resume da conta, quando disponível no servidor. Grave chaves com `vault-go engine key openrouter|gemini` ou no painel local; as ferramentas MCP não aceitam segredos.
176
183
 
177
- Na instalação, o Vault Go registra hooks no Claude Code, no Codex, no Grok e no Agy. Eles injetam contexto no início da sessão, capturam prompts e ferramentas, e o serviço local (`http://localhost:38850`) gera memórias com o motor escolhido — o mesmo ciclo do claude-mem, persistindo no Vault.
184
+ Na instalação, o Vault Go registra hooks no Claude Code, no Codex, no Grok, no Agy e no Gemini CLI. Eles carregam contexto e capturam os eventos oferecidos pelo cliente; o serviço local (`http://localhost:38850`) gera memórias com o motor escolhido, persistindo no Vault. No OpenCode 1.x, um plugin acrescenta contexto ao sistema antes das chamadas do modelo; esse adaptador não adiciona captura automática.
178
185
 
179
186
  O recurso `vault-go://status` fornece o resumo sanitizado em JSON.
180
187
 
@@ -271,6 +278,10 @@ O runtime fica em `~/.memvault/local-runtime`, independente do cache bunx.
271
278
  `local stop` para o processo; `local uninstall` remove o início automático e
272
279
  preserva autenticação, cache e atividade. A atualização automática mantém o
273
280
  serviço e os hooks atualizados; clientes MCP abertos precisam reconectar.
281
+ Após validar a nova versão do serviço, a atualização também repara os hooks do
282
+ Gemini e o plugin do OpenCode quando esses clientes já têm o MCP Vault configurado.
283
+ Plugins de terceiros são preservados. Reinicie clientes abertos para carregar
284
+ novos hooks ou o módulo atualizado do plugin.
274
285
 
275
286
  A atividade registra método, rota, duração e resultado das chamadas feitas
276
287
  pelo MCP Vault, sem argumentos, consultas ou credenciais. Não captura toda a
@@ -315,6 +326,21 @@ conforme os eventos oferecidos pelo cliente. O hook anterior à leitura de arqui
315
326
  é síncrono para conseguir inserir o contexto. `vault_go_session_start_context`
316
327
  executa o mesmo leitor, respeitando `contextItems` e `contextMaxChars` da conta.
317
328
 
329
+ Claude, Codex e Gemini mostram uma confirmação quando o contexto é carregado,
330
+ sem exibir conteúdo da memória nessa confirmação. Worktrees Git reutilizam o
331
+ projeto do repositório principal; se ele ainda não estiver cadastrado, um projeto
332
+ existente do próprio worktree continua legível. Submódulos mantêm seu projeto.
333
+
334
+ O Gemini usa `SessionStart`, `BeforeAgent`, `AfterTool` e `AfterAgent`, conforme
335
+ seu [contrato de hooks](https://geminicli.com/docs/hooks/reference/). Não há
336
+ injeção de histórico de arquivo em `BeforeTool`, pois esse evento não oferece
337
+ esse contrato. O plugin OpenCode usa `experimental.chat.system.transform`
338
+ do [contrato 1.x](https://github.com/anomalyco/opencode/blob/v1.18.31/packages/plugin/src/index.ts),
339
+ com prazo de três segundos e continuação normal em caso de falha. Consultas
340
+ simultâneas da mesma sessão são compartilhadas; respostas de uma credencial
341
+ substituída não são injetadas. Clientes sem esses adaptadores acessam memória
342
+ pelas ferramentas MCP, sem promessa de contexto automático.
343
+
318
344
  A captura respeita `automaticCapture`, `captureMode` e a opção de incluir resultados
319
345
  de ferramentas. Uma falha ao consultar preferências desativa aquela captura.
320
346
  Eventos privados não são enviados; campos sensíveis e marcadores privados são
@@ -6,7 +6,7 @@ export const CAPABILITIES = {
6
6
  { upstream: ['important_workflow'], status: 'available', tools: ['vault_go_workflow'] },
7
7
  { upstream: ['search', 'get_observations', 'timeline'], status: 'partial', tools: ['vault_go_search_index', 'vault_go_observations', 'vault_go_timeline', 'vault_go_feed'], difference: 'UUID identifiers; search requires query; feed provides cursor pagination. No offset/orderBy or automatic timeline anchor.' },
8
8
  { upstream: ['get_tool_uses'], status: 'available', tools: ['vault_go_tool_uses'], requires: 'Engine with POST /v1/events/batch. Historical output is unavailable unless it was captured. Private data is redacted.' },
9
- { upstream: ['session_start_context'], status: 'partial', tools: ['vault_go_session_start_context', 'vault_go_context', 'vault_go_file_context'], difference: 'Exact Vault SessionStart hook output and configured context budget. No upstream multi-project chain renderer.' },
9
+ { upstream: ['session_start_context'], status: 'partial', tools: ['vault_go_session_start_context', 'vault_go_context', 'vault_go_file_context'], difference: 'Vault SessionStart output and configured budget; canonical Git worktree projects. Native hooks for Claude/Codex/Grok/Agy/Gemini; context-only OpenCode 1.x system plugin. No upstream multi-project chain renderer.' },
10
10
  { upstream: ['smart_search', 'smart_outline', 'smart_unfold'], status: 'partial', tools: ['vault_go_smart_search', 'vault_go_smart_outline', 'vault_go_smart_unfold'], difference: 'Local AST with bounded workspace reads; supported languages are returned by the tools, not all upstream grammars.' },
11
11
  { upstream: ['observation_add', 'observation_record_event', 'observation_search', 'observation_context'], status: 'available', tools: ['vault_go_remember', 'vault_go_event', 'vault_go_search', 'vault_go_context'], requires: 'generate:false requires updated engine and prevents duplicate server processing. summaryMode=off preserves raw memory without invoking a model.' },
12
12
  { upstream: ['automatic updates'], status: 'available', tools: ['vault_go_update_status', 'vault_go_update_check', 'vault_go_update_install', 'vault_go_update_configure'], requires: 'Local worker running, npm installed and official registry reachable. Daily stable-release check; idle activation and runtime rollback. Reconnect MCP clients to use new tool definitions.' },
@@ -16,7 +16,7 @@ export const CAPABILITIES = {
16
16
  { upstream: ['provider selection', 'automatic observation processing'], status: 'available', tools: ['vault_go_engines', 'vault_go_engine_select', 'vault_go_generate_context', 'vault_go_processing_status'], requires: 'Local worker running and selected provider authenticated. Codex Terra/Luna/Sol use ChatGPT subscription CLI. Retried persistence is idempotent.' },
17
17
  { upstream: ['privacy', 'capture modes', 'context settings'], status: 'partial', tools: ['vault_go_preferences', 'vault_go_preferences_update'], difference: 'Built-in capture modes and private-tag redaction; no custom mode inheritance/taxonomy. Preference update requires session authentication.' },
18
18
  { upstream: ['import', 'export', 'cloud sync'], status: 'partial', tools: ['vault_go_import', 'vault_go_export'], difference: 'Vault PostgreSQL persistence; upstream SQLite exports are a different format. Export is a bounded batch, not a complete backup. No offline replicated memory store.' },
19
- { upstream: ['Telegram', 'Grok awareness push', 'CCS Align', 'transcript watchers'], status: 'unsupported', tools: [], difference: 'Not ported. Native capture hooks currently target Claude, Codex, Grok and Agy; other clients use MCP.' },
19
+ { upstream: ['Telegram', 'Grok awareness push', 'CCS Align', 'transcript watchers'], status: 'unsupported', tools: [], difference: 'Not ported. Native capture hooks target Claude, Codex, Grok, Agy and Gemini. OpenCode has context-only integration; remaining clients use MCP.' },
20
20
  { upstream: ['development/report/presentation skills', 'babysit'], status: 'partial', tools: ['vault_go_modes', 'vault_go_mode'], difference: '20 original Vault workflow prompts, also exposed as tools for clients without prompt support. Requirements and missing runtimes are explicit. Loading a mode does not start background agents or execute external actions.' },
21
21
  ],
22
22
  };
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@ 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 { playInstallBanner, withInstallProgress } from './install-ui.js';
5
6
  import { CONTEXT_ENGINE_IDS, ENGINE_NAMES, engineModels, isApiEngine, listContextEngines, loadEngineKey, parseEngineModel, saveEngineKey, selectContextEngine, selectEngineModel, selectedContextEngine, selectedEngineModel, } from './context-engines.js';
6
7
  import { VaultCloudClient } from './cloud.js';
7
8
  import { copy, resolveLocale } from './locale.js';
@@ -111,7 +112,7 @@ async function chooseClients(args, locale) {
111
112
  async function chooseEngine(args, locale, home) {
112
113
  const t = copy[locale];
113
114
  const configured = optionValue(args, '--engine');
114
- const engine = configured ? parseEngineChoice(configured) : await promptEngine(home, locale);
115
+ const engine = configured ? parseEngineChoice(configured) : await promptEngine(home, locale, { disabled: args.includes('--no-animation') });
115
116
  selectContextEngine(home, engine);
116
117
  process.stderr.write(` ✓ ${t.engineSaved}: ${ENGINE_NAMES[engine]}\n`);
117
118
  await chooseModel(args, locale, home, engine);
@@ -132,11 +133,11 @@ async function chooseEngine(args, locale, home) {
132
133
  }
133
134
  return engine;
134
135
  }
135
- async function promptEngine(home, locale) {
136
+ async function promptEngine(home, locale, ui) {
136
137
  const t = copy[locale];
137
138
  process.stderr.write(` ${t.engineHelp}\n`);
138
139
  try {
139
- const listed = await listContextEngines(home, await managedStatus(home));
140
+ const listed = await withInstallProgress(t.detectingEngines, async () => listContextEngines(home, await managedStatus(home)), ui);
140
141
  for (const [index, engine] of listed.engines.entries()) {
141
142
  const mark = engine.available ? `● ${t.detected}` : `○ ${t.available}`;
142
143
  process.stderr.write(` ${index + 1}. ${ENGINE_NAMES[engine.id].padEnd(18)} ${mark}\n`);
@@ -234,6 +235,7 @@ export async function runLocal(args) {
234
235
  return 0;
235
236
  }
236
237
  if (command === 'install') {
238
+ await playInstallBanner({ disabled: args.includes('--no-animation') });
237
239
  const result = await installLocal();
238
240
  process.stderr.write(`${result.startup ? t.startup : t.manual}\n`);
239
241
  }
@@ -258,7 +260,7 @@ export async function runSetup(args) {
258
260
  if (!process.stdin.isTTY || !process.stderr.isTTY) {
259
261
  throw new Error('O setup requer um terminal interativo.');
260
262
  }
261
- process.stderr.write('\n ◆ VAULT GO\n ────────────────────────────────────────\n');
263
+ await playInstallBanner({ disabled: args.includes('--no-animation') });
262
264
  const locale = await chooseLocale(args);
263
265
  const t = copy[locale];
264
266
  const local = localCopy[locale];
@@ -308,7 +310,7 @@ Uso:
308
310
  bunx --bun vault-go@latest
309
311
  Abre o login Vault no navegador e instala nos clientes detectados.
310
312
 
311
- bunx --bun vault-go@latest setup [--clients lista] [--engine motor] [--model modelo] [--reauth]
313
+ bunx --bun vault-go@latest setup [--clients lista] [--engine motor] [--model modelo] [--reauth] [--no-animation]
312
314
  Executa o assistente de autenticação e instalação. Pergunta o motor
313
315
  (Vault AI, Claude, OpenRouter, Gemini ou ChatGPT/Codex) e o modelo
314
316
  (Terra, Luna, Sol no Codex; Haiku, Sonnet, Opus no Claude).
@@ -342,6 +344,7 @@ Variáveis:
342
344
  VAULT_GO_HOME Diretório de configuração (padrão: ~/.memvault)
343
345
  MEMVAULT_CONFIG_DIR Diretório compartilhado com o agente MemVault
344
346
  VAULT_GO_API_URL Endpoint alternativo da API Vault
347
+ VAULT_GO_NO_ANIMATION Desativa a animação de instalação (também: --no-animation)
345
348
  VAULT_GO_OPENROUTER_API_KEY / OPENROUTER_API_KEY
346
349
  VAULT_GO_GEMINI_API_KEY / GEMINI_API_KEY / GOOGLE_API_KEY
347
350
  `;
@@ -9,6 +9,11 @@ export declare const HOOK_EVENTS: {
9
9
  export declare function quoteShell(value: string): string;
10
10
  export declare function hookEntryPath(configHome?: string): string;
11
11
  export declare function hookCommand(adapter: string, event: string, configHome?: string): string;
12
- export type HookClient = "claude" | "codex" | "grok" | "agy";
12
+ export type HookClient = "claude" | "codex" | "grok" | "agy" | "gemini";
13
13
  export declare function installClientHooks(client: HookClient, userHome?: string, configHome?: string): void;
14
- export declare function repairClientHooks(userHome?: string, configHome?: string): void;
14
+ export declare function installOpenCodePlugin(userHome?: string, configHome?: string): void;
15
+ /** Repairs only clients explicitly connected to Vault; never installs a CLI. */
16
+ export declare function repairAdditionalClientHooks(userHome?: string, configHome?: string, report?: typeof reportRepairWarning): void;
17
+ declare function reportRepairWarning(message: string): void;
18
+ export declare function repairClientHooks(userHome?: string, configHome?: string, report?: typeof reportRepairWarning): void;
19
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { vaultHome } from "./config.js";
6
6
  function readJsonObject(path) {
7
7
  if (!existsSync(path))
@@ -21,9 +21,12 @@ function readJsonObject(path) {
21
21
  return parsed;
22
22
  }
23
23
  function atomicWriteJson(path, value) {
24
+ atomicWriteText(path, `${JSON.stringify(value, null, 2)}\n`);
25
+ }
26
+ function atomicWriteText(path, text) {
24
27
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
25
28
  const temporary = join(dirname(path), `.${process.pid}.${Date.now()}.tmp`);
26
- writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
29
+ writeFileSync(temporary, text, {
27
30
  mode: 0o600,
28
31
  });
29
32
  renameSync(temporary, path);
@@ -37,6 +40,12 @@ export const HOOK_EVENTS = {
37
40
  PostToolUse: "observation",
38
41
  Stop: "summarize",
39
42
  };
43
+ const GEMINI_EVENTS = {
44
+ SessionStart: "context",
45
+ BeforeAgent: "session-init",
46
+ AfterTool: "observation",
47
+ AfterAgent: "summarize",
48
+ };
40
49
  export function quoteShell(value) {
41
50
  return "'" + value.replaceAll("'", "'\\''") + "'";
42
51
  }
@@ -51,7 +60,7 @@ export function hookCommand(adapter, event, configHome = vaultHome()) {
51
60
  }
52
61
  function hookManifest(adapter, configHome = vaultHome()) {
53
62
  return {
54
- hooks: Object.fromEntries(Object.entries(HOOK_EVENTS).map(([name, event]) => [
63
+ hooks: Object.fromEntries(Object.entries(adapter === "gemini" ? GEMINI_EVENTS : HOOK_EVENTS).map(([name, event]) => [
55
64
  name,
56
65
  [
57
66
  {
@@ -59,10 +68,15 @@ function hookManifest(adapter, configHome = vaultHome()) {
59
68
  {
60
69
  type: "command",
61
70
  command: hookCommand(adapter, event, configHome),
62
- timeout: event === "context" ? 60 : 20,
63
- statusMessage: HOOK_OWNER,
64
- ...(event === "observation" ||
65
- event === "summarize"
71
+ ...(adapter === "gemini" ? {
72
+ timeout: event === "context" ? 60000 : 20000,
73
+ name: `vault-go:${event}`,
74
+ description: HOOK_OWNER,
75
+ } : {
76
+ timeout: event === "context" ? 60 : 20,
77
+ statusMessage: HOOK_OWNER,
78
+ }),
79
+ ...(adapter !== "gemini" && (event === "observation" || event === "summarize")
66
80
  ? { async: true }
67
81
  : {}),
68
82
  },
@@ -100,7 +114,7 @@ export function installClientHooks(client, userHome = homedir(), configHome = va
100
114
  atomicWriteJson(path, { ...config, "vault-go": agyHookManifest(configHome) });
101
115
  return;
102
116
  }
103
- const path = join(userHome, client === "claude" ? ".claude/settings.json" : ".codex/hooks.json");
117
+ const path = join(userHome, client === "claude" ? ".claude/settings.json" : client === "gemini" ? ".gemini/settings.json" : ".codex/hooks.json");
104
118
  const config = readJsonObject(path);
105
119
  const hooks = {
106
120
  ...(config.hooks &&
@@ -110,7 +124,7 @@ export function installClientHooks(client, userHome = homedir(), configHome = va
110
124
  : {}),
111
125
  };
112
126
  const desired = hookManifest(client, configHome).hooks;
113
- for (const event of Object.keys(HOOK_EVENTS)) {
127
+ for (const event of Object.keys(desired)) {
114
128
  const current = hooks[event];
115
129
  const retained = Array.isArray(current)
116
130
  ? current
@@ -121,7 +135,8 @@ export function installClientHooks(client, userHome = homedir(), configHome = va
121
135
  return group;
122
136
  const filtered = group.hooks.filter((hook) => !hook ||
123
137
  typeof hook !== "object" ||
124
- hook.statusMessage !== HOOK_OWNER);
138
+ (hook.statusMessage !== HOOK_OWNER &&
139
+ !(client === "gemini" && hook.name === `vault-go:${GEMINI_EVENTS[event]}`)));
125
140
  return { ...group, hooks: filtered };
126
141
  })
127
142
  .filter((group) => !group ||
@@ -133,15 +148,56 @@ export function installClientHooks(client, userHome = homedir(), configHome = va
133
148
  }
134
149
  atomicWriteJson(path, { ...config, hooks });
135
150
  }
136
- export function repairClientHooks(userHome = homedir(), configHome = vaultHome()) {
137
- installClientHooks("claude", userHome, configHome);
138
- installClientHooks("codex", userHome, configHome);
151
+ const OPENCODE_OWNER = "// Managed by vault-go: automatic memory context";
152
+ export function installOpenCodePlugin(userHome = homedir(), configHome = vaultHome()) {
153
+ const path = join(userHome, ".config", "opencode", "plugins", "vault-go-memory.js");
154
+ if (existsSync(path) && !readFileSync(path, "utf8").startsWith(OPENCODE_OWNER + "\n")) {
155
+ throw new Error(`Refusing to replace an unowned plugin: ${path}`);
156
+ }
157
+ const runtime = pathToFileURL(join(configHome, "local-runtime", "dist", "opencode-plugin.js")).href;
158
+ atomicWriteText(path, `${OPENCODE_OWNER}\nexport default async function(input) {\n try {\n const runtime = await import(${JSON.stringify(runtime)});\n return runtime.createOpenCodePlugin(input, { home: ${JSON.stringify(configHome)} });\n } catch { return {}; }\n}\n`);
159
+ }
160
+ /** Repairs only clients explicitly connected to Vault; never installs a CLI. */
161
+ export function repairAdditionalClientHooks(userHome = homedir(), configHome = vaultHome(), report = reportRepairWarning) {
162
+ repairClient("gemini", () => {
163
+ const gemini = readJsonObject(join(userHome, ".gemini", "settings.json"));
164
+ const server = gemini.mcpServers?.["vault-go"];
165
+ if (server && server.enabled !== false && server.disabled !== true) {
166
+ installClientHooks("gemini", userHome, configHome);
167
+ }
168
+ }, report);
169
+ repairClient("opencode", () => {
170
+ const opencode = readJsonObject(join(userHome, ".config", "opencode", "opencode.json"));
171
+ const server = opencode.mcp?.["vault-go"];
172
+ if (server && server.enabled !== false)
173
+ installOpenCodePlugin(userHome, configHome);
174
+ }, report);
175
+ }
176
+ function reportRepairWarning(message) {
177
+ process.stderr.write(message + "\n");
178
+ }
179
+ function repairClient(client, repair, report) {
180
+ try {
181
+ repair();
182
+ }
183
+ catch {
184
+ // Client configuration failures must not stop another client's repair or reveal its contents.
185
+ try {
186
+ report(`Vault Go: reparo de ${client} pendente; configuração existente preservada.`);
187
+ }
188
+ catch { }
189
+ }
190
+ }
191
+ export function repairClientHooks(userHome = homedir(), configHome = vaultHome(), report = reportRepairWarning) {
192
+ repairAdditionalClientHooks(userHome, configHome, report);
193
+ repairClient("claude", () => installClientHooks("claude", userHome, configHome), report);
194
+ repairClient("codex", () => installClientHooks("codex", userHome, configHome), report);
139
195
  if (existsSync(join(userHome, ".grok"))) {
140
- installClientHooks("grok", userHome, configHome);
196
+ repairClient("grok", () => installClientHooks("grok", userHome, configHome), report);
141
197
  }
142
198
  if (existsSync(join(userHome, ".gemini", "config")) ||
143
199
  existsSync(join(userHome, ".gemini", "antigravity")) ||
144
200
  existsSync(join(userHome, ".gemini", "antigravity-cli"))) {
145
- installClientHooks("agy", userHome, configHome);
201
+ repairClient("agy", () => installClientHooks("agy", userHome, configHome), report);
146
202
  }
147
203
  }
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { basename, dirname, resolve } from "node:path";
3
- import { existsSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
3
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
4
4
  import { VaultCloudClient } from "./cloud.js";
5
5
  import { vaultHome } from "./config.js";
6
6
  import { enqueueGeneration } from "./hook-queue.js";
@@ -16,6 +16,14 @@ const record = (value) => value && typeof value === "object" && !Array.isArray(v
16
16
  : {};
17
17
  const string = (...values) => values.find((value) => typeof value === "string" && value.trim()) ||
18
18
  "";
19
+ function resolvedPath(path) {
20
+ try {
21
+ return realpathSync(resolve(path));
22
+ }
23
+ catch {
24
+ return resolve(path);
25
+ }
26
+ }
19
27
  function root(cwd) {
20
28
  const initial = resolve(cwd);
21
29
  let dir = initial;
@@ -26,6 +34,43 @@ function root(cwd) {
26
34
  }
27
35
  return initial;
28
36
  }
37
+ function gitMetadata(path) {
38
+ const file = statSync(path);
39
+ if (!file.isFile() || file.size > 4096)
40
+ return "";
41
+ return readFileSync(path, "utf8").trim();
42
+ }
43
+ function projectRoots(workspace) {
44
+ const fallback = [resolvedPath(workspace)];
45
+ try {
46
+ const marker = /^gitdir:\s*(.+)$/.exec(gitMetadata(resolve(workspace, ".git")));
47
+ if (!marker)
48
+ return fallback;
49
+ const gitdir = resolve(workspace, marker[1]);
50
+ const common = gitMetadata(resolve(gitdir, "commondir"));
51
+ // A submodule or separate git directory has no shared worktree metadata.
52
+ if (!common)
53
+ return fallback;
54
+ const shared = resolvedPath(resolve(gitdir, common));
55
+ if (basename(shared) !== ".git" || !statSync(shared).isDirectory())
56
+ return fallback;
57
+ return [...new Set([dirname(shared), ...fallback])];
58
+ }
59
+ catch {
60
+ // Keep existing projects usable if Git metadata is absent or unreadable.
61
+ return fallback;
62
+ }
63
+ }
64
+ function canonicalFile(file, workspace, project) {
65
+ const absolute = resolve(workspace, file);
66
+ for (const directory of [workspace, resolvedPath(workspace)]) {
67
+ const path = relative(directory, absolute);
68
+ if (path !== ".." && !path.startsWith("../") && !path.startsWith("..\\") && !isAbsolute(path)) {
69
+ return resolve(project, path);
70
+ }
71
+ }
72
+ return absolute;
73
+ }
29
74
  export function redact(value) {
30
75
  if (typeof value === "string") {
31
76
  return value
@@ -58,7 +103,7 @@ export function normalizeHookInput(adapter, value) {
58
103
  cwd: string(input.cwd, input.workspace, input.workspaceRoot, workspace),
59
104
  tool_name: string(input.tool_name, input.toolName, toolCall.name),
60
105
  tool_input: record(input.tool_input ?? input.toolInput ?? toolCall.args),
61
- last_assistant_message: string(input.last_assistant_message, input.lastAssistantMessage, input.response, input.summary),
106
+ last_assistant_message: string(input.last_assistant_message, input.lastAssistantMessage, input.prompt_response, input.response, input.summary),
62
107
  prompt: string(input.prompt, input.user_prompt, input.userPrompt),
63
108
  };
64
109
  }
@@ -73,19 +118,24 @@ export function hookOutput(event, context = "", adapter = "claude") {
73
118
  return { decision: "allow" };
74
119
  return {};
75
120
  }
76
- const result = { continue: true, suppressOutput: true };
121
+ const result = adapter === "codex"
122
+ ? {}
123
+ : { continue: true, suppressOutput: true };
77
124
  const name = event === "context"
78
125
  ? "SessionStart"
79
126
  : event === "file-context"
80
127
  ? "PreToolUse"
81
128
  : event === "session-init"
82
- ? "UserPromptSubmit"
129
+ ? adapter === "gemini" ? "BeforeAgent" : "UserPromptSubmit"
83
130
  : "";
84
131
  if (name && context) {
85
132
  result.hookSpecificOutput = {
86
133
  hookEventName: name,
87
134
  additionalContext: context,
88
135
  };
136
+ if (event === "context" && ["claude", "codex", "gemini"].includes(adapter)) {
137
+ result.systemMessage = "Vault Memory: contexto do projeto carregado.";
138
+ }
89
139
  }
90
140
  return result;
91
141
  }
@@ -130,15 +180,17 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
130
180
  const sessionId = string(input.session_id);
131
181
  if (!reading && !sessionId)
132
182
  return hookOutput(event, "", adapter);
133
- const cwd = root(string(input.cwd, input.workspace) || process.cwd());
183
+ const workspace = root(string(input.cwd, input.workspace) || process.cwd());
184
+ const roots = projectRoots(workspace);
134
185
  const projects = await cloud.projects();
135
186
  if (!Array.isArray(projects))
136
187
  throw new Error("Resposta de projetos inválida");
137
- let project = projects.find((item) => item &&
188
+ let project = roots.map((cwd) => projects.find((item) => item &&
138
189
  typeof item === "object" &&
139
190
  typeof item.rootPath === "string" &&
140
191
  Boolean(String(item.rootPath).trim()) &&
141
- resolve(String(item.rootPath)) === cwd);
192
+ resolvedPath(String(item.rootPath)) === cwd)).find(Boolean);
193
+ const cwd = project ? resolvedPath(String(project.rootPath)) : roots[0];
142
194
  if (!project && reading)
143
195
  return hookOutput(event, "", adapter);
144
196
  if (!project) {
@@ -154,11 +206,15 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
154
206
  limit: boundedPreference(prefs.contextItems, 20, 1, 100),
155
207
  maxChars: boundedPreference(prefs.contextMaxChars, 24_000, 1_000, 100_000),
156
208
  };
209
+ const projectFile = file ? canonicalFile(file, workspace, cwd) : "";
210
+ const files = file
211
+ ? [...new Set([file, ...(resolvedPath(workspace) !== cwd ? [projectFile] : [])])]
212
+ : [];
157
213
  if (reading) {
158
214
  const result = record(event === "file-context"
159
215
  ? await cloud.fileContext({
160
216
  ...limits,
161
- paths: [...new Set([file, resolve(cwd, file)])],
217
+ paths: [...new Set([file, resolve(workspace, file), projectFile])],
162
218
  })
163
219
  : await cloud.context(limits));
164
220
  return hookOutput(event, string(result.context).slice(0, limits.maxChars), adapter);
@@ -218,8 +274,8 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
218
274
  title: toolName ||
219
275
  (event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
220
276
  content,
221
- filesRead: !modifies && file ? [file] : [],
222
- filesModified: modifies && file ? [file] : [],
277
+ filesRead: !modifies ? files : [],
278
+ filesModified: modifies ? files : [],
223
279
  },
224
280
  occurredAtEpoch: Date.now(),
225
281
  });
@@ -233,8 +289,8 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
233
289
  (event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
234
290
  kind: event === "summarize" ? "summary" : event === "session-init" ? "prompt" : "observation",
235
291
  maxChars: boundedPreference(prefs.summaryMaxChars, 4_000, 256, 16_000),
236
- filesRead: !modifies && file ? [file] : [],
237
- filesModified: modifies && file ? [file] : [],
292
+ filesRead: !modifies ? files : [],
293
+ filesModified: modifies ? files : [],
238
294
  });
239
295
  }
240
296
  if (event === "summarize")
@@ -0,0 +1,14 @@
1
+ type Terminal = Pick<NodeJS.WriteStream, 'write' | 'isTTY' | 'columns' | 'rows'> & {
2
+ on(event: 'resize', listener: () => void): unknown;
3
+ off(event: 'resize', listener: () => void): unknown;
4
+ };
5
+ export interface InstallUiOptions {
6
+ disabled?: boolean;
7
+ stream?: Terminal;
8
+ env?: NodeJS.ProcessEnv;
9
+ }
10
+ /** Original Vault artwork: a turning vault dial and a left-to-right wordmark reveal. */
11
+ export declare function playInstallBanner(options?: InstallUiOptions): Promise<void>;
12
+ /** Use only around silent operations, ending before any prompt or result is printed. */
13
+ export declare function withInstallProgress<T>(label: string, operation: () => Promise<T>, options?: InstallUiOptions): Promise<T>;
14
+ export {};
@@ -0,0 +1,95 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ const RESET = '\x1b[0m';
3
+ const CLEAR_LINE = '\r\x1b[2K';
4
+ const FRAMES = 24;
5
+ const WORDMARK = [
6
+ 'V V AAA U U L TTTTT GGGG OOO ',
7
+ 'V V A A U U L T G O O',
8
+ 'V V AAAAA U U L T G GG O O',
9
+ ' V V A A U U L T G G O O',
10
+ ' V A A UUU LLLLL T GGG OOO ',
11
+ ];
12
+ function terminal(options) {
13
+ // stdout belongs to the MCP transport, including when redirected to a file.
14
+ return options.stream ?? process.stderr;
15
+ }
16
+ function animate(options) {
17
+ const env = options.env ?? process.env;
18
+ return terminal(options).isTTY === true && !options.disabled && !env.CI &&
19
+ env.TERM !== 'dumb' && env.NO_COLOR === undefined && !env.VAULT_GO_NO_ANIMATION;
20
+ }
21
+ function columns(stream) {
22
+ // Leave the final column unused so repainting cannot wrap a line.
23
+ return Math.max(1, (stream.columns || 80) - 1);
24
+ }
25
+ function bannerLines(stream, frame) {
26
+ const width = columns(stream);
27
+ if (width < 46 || (stream.rows || 24) < 16) {
28
+ return [` ${['◇', '◈', '◆', '◈'][Math.floor(frame / 3) % 4]} VAULT GO`.slice(0, width)];
29
+ }
30
+ const wheel = ['+', '×', '✳', '◆'][Math.floor(frame / 3) % 4];
31
+ const art = [
32
+ ' .-----------------.',
33
+ ' /_________________/|',
34
+ ' | .-----------. ||',
35
+ ` | | ${wheel} | ||`,
36
+ ' | | VAULT | |/',
37
+ " |__'-----------'__|",
38
+ '',
39
+ ...WORDMARK.map(row => ` ${row}`),
40
+ ];
41
+ const revealed = Math.ceil((frame + 1) / FRAMES * 44);
42
+ return art.map(row => row.slice(0, revealed));
43
+ }
44
+ /** Original Vault artwork: a turning vault dial and a left-to-right wordmark reveal. */
45
+ export async function playInstallBanner(options = {}) {
46
+ const stream = terminal(options);
47
+ if (!animate(options)) {
48
+ stream.write('\n' + ' ◆ VAULT GO'.slice(0, columns(stream)) + '\n\n');
49
+ return;
50
+ }
51
+ let resized = false;
52
+ const onResize = () => { resized = true; };
53
+ stream.on('resize', onResize);
54
+ stream.write('\n');
55
+ try {
56
+ for (let frame = 0; frame < FRAMES; frame++) {
57
+ // Stop repainting after a resize: previously drawn lines may have reflowed.
58
+ if (resized)
59
+ break;
60
+ const lines = bannerLines(stream, frame);
61
+ if (frame > 0)
62
+ stream.write(`\x1b[${lines.length}F`);
63
+ stream.write(lines.map((line, row) => `${CLEAR_LINE}${row < 7 ? '\x1b[96m' : '\x1b[95m'}${line}${RESET}\n`).join(''));
64
+ if (frame < FRAMES - 1)
65
+ await delay(45);
66
+ }
67
+ }
68
+ finally {
69
+ stream.off('resize', onResize);
70
+ // Never hide the cursor or enter an alternate screen: interruption stays safe.
71
+ stream.write('\n');
72
+ }
73
+ }
74
+ /** Use only around silent operations, ending before any prompt or result is printed. */
75
+ export async function withInstallProgress(label, operation, options = {}) {
76
+ if (!animate(options))
77
+ return operation();
78
+ const stream = terminal(options);
79
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
80
+ let frame = 0;
81
+ const draw = () => {
82
+ const line = ` ${frames[frame++ % frames.length]} ${label}`.slice(0, columns(stream));
83
+ stream.write(`${CLEAR_LINE}\x1b[96m${line}${RESET}`);
84
+ };
85
+ draw();
86
+ const timer = setInterval(draw, 80);
87
+ timer.unref();
88
+ try {
89
+ return await operation();
90
+ }
91
+ finally {
92
+ clearInterval(timer);
93
+ stream.write(CLEAR_LINE);
94
+ }
95
+ }
package/dist/installer.js CHANGED
@@ -4,7 +4,7 @@ import { dirname, join, resolve } from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { vaultHome } from './config.js';
6
6
  import { stageLocalRuntime } from './local-install.js';
7
- import { installClientHooks } from './hook-install.js';
7
+ import { installClientHooks, installOpenCodePlugin } from './hook-install.js';
8
8
  export const MCP_SERVER_NAME = 'vault-go';
9
9
  export const MCP_SERVER_COMMAND = ['bunx', '--bun', 'vault-go@latest', 'serve'];
10
10
  export const MCP_CLIENTS = [
@@ -199,6 +199,11 @@ export function installMcpClient(client, options = {}) {
199
199
  if (client === 'gemini') {
200
200
  const path = join(home, '.gemini', 'settings.json');
201
201
  mergeServer(path);
202
+ try {
203
+ stageLocalRuntime(vaultHome());
204
+ }
205
+ catch { /* incomplete installs still write hooks */ }
206
+ installClientHooks('gemini', home);
202
207
  return { client, status: 'installed', destination: path };
203
208
  }
204
209
  if (client === 'vault-desktop') {
@@ -221,6 +226,11 @@ export function installMcpClient(client, options = {}) {
221
226
  else
222
227
  path = join(home, '.config', 'opencode', 'opencode.json');
223
228
  if (client === 'opencode') {
229
+ try {
230
+ stageLocalRuntime(vaultHome());
231
+ }
232
+ catch { /* plugin becomes active when runtime is available */ }
233
+ installOpenCodePlugin(home);
224
234
  mergeServer(path, 'mcp', {
225
235
  type: 'local',
226
236
  command: [...MCP_SERVER_COMMAND],
package/dist/locale.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare const copy: {
12
12
  connected: string;
13
13
  key: string;
14
14
  engine: string;
15
+ detectingEngines: string;
15
16
  engineHelp: string;
16
17
  engineSaved: string;
17
18
  engineKey: string;
@@ -51,6 +52,7 @@ export declare const copy: {
51
52
  connected: string;
52
53
  key: string;
53
54
  engine: string;
55
+ detectingEngines: string;
54
56
  engineHelp: string;
55
57
  engineSaved: string;
56
58
  engineKey: string;
@@ -90,6 +92,7 @@ export declare const copy: {
90
92
  connected: string;
91
93
  key: string;
92
94
  engine: string;
95
+ detectingEngines: string;
93
96
  engineHelp: string;
94
97
  engineSaved: string;
95
98
  engineKey: string;
package/dist/locale.js CHANGED
@@ -21,6 +21,7 @@ export const copy = {
21
21
  connected: "Conta conectada",
22
22
  key: "Chave de API",
23
23
  engine: "Escolher motor de contexto",
24
+ detectingEngines: "Verificando motores disponíveis…",
24
25
  engineHelp: "Como o Vault prepara memórias: Claude, OpenRouter, Gemini ou Vault AI.",
25
26
  engineSaved: "Motor selecionado",
26
27
  engineKey: "Cole a chave (não será exibida). Enter para pular",
@@ -60,6 +61,7 @@ export const copy = {
60
61
  connected: "Account connected",
61
62
  key: "API key",
62
63
  engine: "Choose context engine",
64
+ detectingEngines: "Checking available engines…",
63
65
  engineHelp: "How Vault prepares memories: Claude, OpenRouter, Gemini, or Vault AI.",
64
66
  engineSaved: "Engine selected",
65
67
  engineKey: "Paste the API key (it will not be shown). Enter to skip",
@@ -99,6 +101,7 @@ export const copy = {
99
101
  connected: "Cuenta conectada",
100
102
  key: "Clave de API",
101
103
  engine: "Elegir motor de contexto",
104
+ detectingEngines: "Comprobando motores disponibles…",
102
105
  engineHelp: "Cómo Vault prepara memorias: Claude, OpenRouter, Gemini o Vault AI.",
103
106
  engineSaved: "Motor seleccionado",
104
107
  engineKey: "Pega la clave (no se mostrará). Enter para omitir",
@@ -0,0 +1,16 @@
1
+ type Options = {
2
+ home?: string;
3
+ fetch?: typeof fetch;
4
+ timeoutMs?: number;
5
+ };
6
+ /** OpenCode 1.x plugin contract: context only; capture stays with the user's existing integrations. */
7
+ export declare function createOpenCodePlugin(input: {
8
+ directory: string;
9
+ }, options?: Options): {
10
+ 'experimental.chat.system.transform': (request: {
11
+ sessionID?: string;
12
+ }, output: {
13
+ system: string[];
14
+ }) => Promise<void>;
15
+ };
16
+ export {};
@@ -0,0 +1,73 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { loadConfig, loadTokens, vaultHome } from './config.js';
3
+ import { VaultCloudClient } from './cloud.js';
4
+ import { executeHook } from './hooks.js';
5
+ function owner(home) {
6
+ return createHash('sha256').update(JSON.stringify([
7
+ loadConfig(home).apiUrl,
8
+ process.env.VAULT_GO_TOKEN || loadTokens(home)?.accessToken || '',
9
+ ])).digest('hex');
10
+ }
11
+ /** OpenCode 1.x plugin contract: context only; capture stays with the user's existing integrations. */
12
+ export function createOpenCodePlugin(input, options = {}) {
13
+ const home = options.home ?? vaultHome();
14
+ const pending = new Map();
15
+ const empty = () => ({ context: '', owner: '' });
16
+ async function readContext(sessionID) {
17
+ const controller = new AbortController();
18
+ const fetcher = options.fetch ?? globalThis.fetch;
19
+ const boundedFetch = (async (url, init) => {
20
+ controller.signal.throwIfAborted();
21
+ return fetcher(url, {
22
+ ...init,
23
+ signal: init?.signal ? AbortSignal.any([controller.signal, init.signal]) : controller.signal,
24
+ });
25
+ });
26
+ const cloud = new VaultCloudClient(home, boundedFetch, false);
27
+ let timer;
28
+ const load = async () => {
29
+ // Authenticate/refresh before taking the ownership snapshot. No model or capture writes run here.
30
+ const preferences = await cloud.preferences();
31
+ controller.signal.throwIfAborted();
32
+ const requestOwner = owner(home);
33
+ cloud.preferences = async () => preferences;
34
+ const output = await executeHook('opencode', 'context', { cwd: input.directory, session_id: sessionID }, cloud, home);
35
+ const context = output.hookSpecificOutput?.additionalContext;
36
+ if (controller.signal.aborted || requestOwner !== owner(home) || typeof context !== 'string')
37
+ return empty();
38
+ return { context, owner: requestOwner };
39
+ };
40
+ try {
41
+ return await Promise.race([
42
+ load(),
43
+ new Promise(resolve => {
44
+ timer = setTimeout(() => { controller.abort(); resolve(empty()); }, options.timeoutMs ?? 3000);
45
+ }),
46
+ ]);
47
+ }
48
+ catch {
49
+ return empty();
50
+ }
51
+ finally {
52
+ if (timer)
53
+ clearTimeout(timer);
54
+ controller.abort();
55
+ }
56
+ }
57
+ return {
58
+ 'experimental.chat.system.transform': async (request, output) => {
59
+ if (!request.sessionID || !input.directory)
60
+ return;
61
+ const key = JSON.stringify([request.sessionID, owner(home)]);
62
+ let loading = pending.get(key);
63
+ if (!loading) {
64
+ loading = readContext(request.sessionID).finally(() => pending.delete(key));
65
+ pending.set(key, loading);
66
+ }
67
+ const result = await loading;
68
+ if (result.context && result.owner === owner(home) && !output.system.includes(result.context)) {
69
+ output.system.push(result.context);
70
+ }
71
+ },
72
+ };
73
+ }
@@ -3,6 +3,8 @@ import { type UpdateState } from './updater.js';
3
3
  export declare function activatePreparedUpdate(home: string, state: UpdateState, operations?: {
4
4
  stop: (home: string) => Promise<boolean>;
5
5
  start: (home: string) => Promise<LocalHealth>;
6
+ repair?: (home: string) => void;
7
+ warn?: (message: string) => void;
6
8
  }, options?: {
7
9
  automatic?: boolean;
8
10
  }): Promise<LocalHealth>;
@@ -2,10 +2,12 @@ import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { existsSync, openSync, closeSync, readFileSync, realpathSync, renameSync, rmSync, lstatSync, unlinkSync, writeFileSync } from 'node:fs';
4
4
  import { join, relative, isAbsolute } from 'node:path';
5
+ import { homedir } from 'node:os';
5
6
  import { stageLocalRuntime, startLocal, stopLocal } from './local-install.js';
6
7
  import { installUpdate, updateStatus } from './updater.js';
7
8
  import { vaultHome } from './config.js';
8
9
  import { generationQueueStatus } from './hook-queue.js';
10
+ import { repairAdditionalClientHooks } from './hook-install.js';
9
11
  function preparedPackage(home, state) {
10
12
  if (state.status !== 'ready' || !state.packageRoot || !state.latest)
11
13
  throw new Error('No validated update is ready.');
@@ -18,8 +20,20 @@ function preparedPackage(home, state) {
18
20
  throw new Error('Invalid prepared update.');
19
21
  return root;
20
22
  }
21
- export async function activatePreparedUpdate(home, state, operations = { stop: stopLocal, start: startLocal }, options = {}) {
23
+ export async function activatePreparedUpdate(home, state, operations = { stop: stopLocal, start: startLocal, repair: home => repairAdditionalClientHooks(homedir(), home) }, options = {}) {
22
24
  const release = await acquireActivationLock(home);
25
+ const repair = () => {
26
+ try {
27
+ operations.repair?.(home);
28
+ }
29
+ catch {
30
+ const message = 'Vault Go: runtime atualizado; reparo dos hooks dos clientes pendente.';
31
+ try {
32
+ (operations.warn ?? (warning => { process.stderr.write(warning + '\n'); }))(message);
33
+ }
34
+ catch { }
35
+ }
36
+ };
23
37
  try {
24
38
  if (options.automatic && !updateStatus(home).enabled)
25
39
  throw new Error('Automatic updates are disabled.');
@@ -29,11 +43,14 @@ export async function activatePreparedUpdate(home, state, operations = { stop: s
29
43
  const health = await operations.start(home);
30
44
  if (health.version !== state.latest)
31
45
  throw new Error('Active worker version mismatch.');
46
+ repair();
32
47
  return health;
33
48
  }
34
49
  if (active && compareVersions(active, state.latest) > 0)
35
50
  throw new Error('Refusing to activate an older runtime.');
36
- return await replaceRuntime(home, state, source, operations);
51
+ const health = await replaceRuntime(home, state, source, operations);
52
+ repair();
53
+ return health;
37
54
  }
38
55
  finally {
39
56
  release();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.19.1",
3
+ "version": "0.21.0",
4
4
  "description": "Servidor MCP universal com autenticação e instalação multi-cliente para a plataforma Vault.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,6 +20,8 @@
20
20
  }
21
21
  },
22
22
  "files": [
23
+ "dist/opencode-plugin.js",
24
+ "dist/opencode-plugin.d.ts",
23
25
  "dist/activity.js",
24
26
  "dist/activity.d.ts",
25
27
  "dist/auth.js",
@@ -46,6 +48,8 @@
46
48
  "dist/index.d.ts",
47
49
  "dist/installer.js",
48
50
  "dist/installer.d.ts",
51
+ "dist/install-ui.js",
52
+ "dist/install-ui.d.ts",
49
53
  "dist/local-dashboard.js",
50
54
  "dist/local-dashboard.d.ts",
51
55
  "dist/local-device.js",