vault-go 0.20.0 → 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
@@ -181,7 +181,7 @@ limitados.
181
181
 
182
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.
183
183
 
184
- 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.
185
185
 
186
186
  O recurso `vault-go://status` fornece o resumo sanitizado em JSON.
187
187
 
@@ -278,6 +278,10 @@ O runtime fica em `~/.memvault/local-runtime`, independente do cache bunx.
278
278
  `local stop` para o processo; `local uninstall` remove o início automático e
279
279
  preserva autenticação, cache e atividade. A atualização automática mantém o
280
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.
281
285
 
282
286
  A atividade registra método, rota, duração e resultado das chamadas feitas
283
287
  pelo MCP Vault, sem argumentos, consultas ou credenciais. Não captura toda a
@@ -322,6 +326,21 @@ conforme os eventos oferecidos pelo cliente. O hook anterior à leitura de arqui
322
326
  é síncrono para conseguir inserir o contexto. `vault_go_session_start_context`
323
327
  executa o mesmo leitor, respeitando `contextItems` e `contextMaxChars` da conta.
324
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
+
325
344
  A captura respeita `automaticCapture`, `captureMode` e a opção de incluir resultados
326
345
  de ferramentas. Uma falha ao consultar preferências desativa aquela captura.
327
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
  };
@@ -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")
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],
@@ -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.20.0",
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",