vault-go 0.34.0 → 0.36.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
@@ -168,7 +168,7 @@ Projects are matched by path first and then by repository: vault-go reads the ch
168
168
 
169
169
  A folder without git joins the nearest existing project above it instead of becoming a project of its own; a git checkout always keeps its own project.
170
170
 
171
- Read tools (`vault_go_search`, `vault_go_search_index`, `vault_go_feed`, `vault_go_retrieve` and query-anchored `vault_go_timeline`) are scoped to the project of the MCP server's working directory when no `projectId` is given; a git worktree maps to its main checkout. Pass `scope: "account"` to search every project of the account. Outside a known project they fall back to the account, and no project is created. Memories marked private stay out of search and feed results unless `includePrivate: true` is set.
171
+ Read tools (`vault_go_search`, `vault_go_search_index`, `vault_go_feed`, `vault_go_retrieve`, `vault_go_facts`, `vault_go_entities` and query-anchored `vault_go_timeline`) are scoped to the project of the MCP server's working directory when no `projectId` is given; a git worktree maps to its main checkout. Pass `scope: "account"` to search every project of the account. Outside a known project they fall back to the account, and no project is created. Memories marked private stay out of search and feed results unless `includePrivate: true` is set.
172
172
 
173
173
  `vault_go_retrieve` lets the central memory engine choose a bounded retrieval path while every explicit retrieval tool remains available. `vault_go_decision_status` reports sanitized rollout/circuit state. Jev and its credential remain in the engine; a healthy deployment does not by itself prove provider availability or decision quality. `vault_go_decide` sends a caller-authored state plus up to 8 typed questions (`noul`, `choice`, `score`) to the central Jev and returns probabilities; it runs only in `full` rollout, one call at a time, refuses secrets, e-mails, identifiers, URLs and absolute local paths before anything leaves Vault, and persists nothing. The caller still owns the decision.
174
174
 
@@ -178,9 +178,11 @@ Jobs and backup: `vault_go_jobs`, `vault_go_job`, `vault_go_job_retry`, `vault_g
178
178
 
179
179
  Engines and updates: `vault_go_engines`, `vault_go_engine_select`, `vault_go_generate_context`, `vault_go_update_status`, `vault_go_update_check`, `vault_go_update_install`, `vault_go_update_configure`.
180
180
 
181
+ Temporal knowledge graph: `vault_go_facts`, `vault_go_entities`, `vault_go_entity_facts`, `vault_go_graph_status`. Facts are `subject —relation→ object` with a validity window; `vault_go_facts` answers "what is true now", "what changed" (`includeExpired: true` marks ended and superseded facts) and "what was true on date X" (`asOf`, an ISO date or datetime; a bare date means the end of that day, UTC). Facts and entities follow the same `scope`/`projectId` rules as the other read tools, and each fact carries the `memoryIds` to load with `vault_go_observations`. Until the graph is enabled for the account, these tools answer with a plain notice and point to `vault_go_search`.
182
+
181
183
  Graph and local code: `vault_go_graph`, `vault_go_graph_link`, `vault_go_graph_link_delete`, `vault_go_graph_presets`, `vault_go_graph_preset_save`, `vault_go_smart_outline`, `vault_go_smart_unfold`, `vault_go_smart_search`, `vault_go_smart_languages`.
182
184
 
183
- Computer Use by Vault: `vault_go_computer_status`, `vault_go_computer_permissions`, `vault_go_computer_list_apps`, `vault_go_computer_list_windows`, `vault_go_computer_get_app_state`, `vault_go_computer_query`, `vault_go_computer_click`, `vault_go_computer_perform_secondary_action`, `vault_go_computer_scroll`, `vault_go_computer_drag`, `vault_go_computer_type_text`, `vault_go_computer_paste_text`, `vault_go_computer_set_value`, `vault_go_computer_press_key`, `vault_go_computer_hotkey`. They drive local apps through the Vault Desktop native helper (installed and running), so every MCP client shares its permissions; on macOS grant Accessibility and Screen Recording to "Vault Desktop Computer Use" once. `get_app_state` returns the accessibility tree plus the window screenshot as an MCP image; `query` finds controls by text or role without a screenshot. Element indexes share one session per server. Typed text and values travel over stdin, never the process arguments. Set `VAULT_DESKTOP_CLI` to point at a custom CLI path, or `VAULT_GO_COMPUTER_USE=off` to hide the tools. The tool surface follows [open-computer-use](https://github.com/iFurySt/open-computer-use) (MIT).
185
+ Computer Use by Vault: `vault_go_computer_status`, `vault_go_computer_permissions`, `vault_go_computer_list_apps`, `vault_go_computer_list_windows`, `vault_go_computer_get_app_state`, `vault_go_computer_query`, `vault_go_computer_click`, `vault_go_computer_perform_secondary_action`, `vault_go_computer_scroll`, `vault_go_computer_drag`, `vault_go_computer_type_text`, `vault_go_computer_paste_text`, `vault_go_computer_set_value`, `vault_go_computer_press_key`, `vault_go_computer_hotkey`. They drive local apps through the Vault Desktop native helper (installed and running), so every MCP client shares its permissions; on macOS grant Accessibility and Screen Recording to "Vault Desktop Computer Use" once. `get_app_state` returns the accessibility tree plus the window screenshot as an MCP image; `query` finds controls by text or role without a screenshot. With Vault Desktop 0.11.73 or later, actions run in the background on the target window: the user's pointer and frontmost app never change, and a dedicated agent cursor is drawn only over that window. Element indexes share one session per server. Typed text and values travel over stdin, never the process arguments. Set `VAULT_DESKTOP_CLI` to point at a custom CLI path, or `VAULT_GO_COMPUTER_USE=off` to hide the tools. The tool surface follows [open-computer-use](https://github.com/iFurySt/open-computer-use) (MIT).
184
186
 
185
187
  Workflows: `vault_go_workflow`, `vault_go_modes`, `vault_go_mode`, plus named prompts such as `vault_go_babysit`. Loading a mode gives the agent instructions; it does not start a background job.
186
188
 
package/dist/auth.d.ts CHANGED
@@ -16,5 +16,6 @@ export declare function authenticateWithBrowser(home: string, options?: {
16
16
  openBrowser?: (url: string) => Promise<void>;
17
17
  timeoutMs?: number;
18
18
  report?: (message: string) => void;
19
+ pasteInput?: NodeJS.ReadableStream | null;
19
20
  }): Promise<AuthenticationResult>;
20
21
  export {};
package/dist/auth.js CHANGED
@@ -3,6 +3,7 @@ import { copy, resolveLocale } from './locale.js';
3
3
  import { createServer } from 'node:http';
4
4
  import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
5
5
  import { execFile } from 'node:child_process';
6
+ import { createInterface } from 'node:readline';
6
7
  import { credentialFilesGeneration, loadConfig, loadTokens, saveConfig, saveTokens, validateApiUrl, } from './config.js';
7
8
  function endpoint(home, path) {
8
9
  const apiUrl = validateApiUrl(loadConfig(home).apiUrl);
@@ -149,6 +150,25 @@ export async function authenticateWithBrowser(home, options = {}) {
149
150
  const received = new Promise((resolve, reject) => { claim = resolve; rejectClaim = reject; });
150
151
  // A timeout can occur while the browser opener is still running.
151
152
  void received.catch(() => { });
153
+ // Why: the same validation serves the local callback and a callback URL pasted in the
154
+ // terminal (login over SSH, where the browser's 127.0.0.1 is another machine).
155
+ const acceptCallback = (url) => {
156
+ const incoming = url.searchParams.get('state') ?? '';
157
+ const validState = /^[A-Za-z0-9_-]{43}$/.test(incoming) && timingSafeEqual(Buffer.from(incoming), Buffer.from(state));
158
+ const code = url.searchParams.get('code');
159
+ if (!validState || url.searchParams.getAll('state').length !== 1 || accepted)
160
+ return { kind: 'invalid', message: 'Retorno de autenticação inválido.' };
161
+ if (url.searchParams.has('error')) {
162
+ accepted = true;
163
+ rejectClaim?.(new Error(t.denied));
164
+ return { kind: 'denied' };
165
+ }
166
+ if (!code || !/^[A-Za-z0-9_-]{43}$/.test(code) || url.searchParams.getAll('code').length !== 1)
167
+ return { kind: 'invalid', message: 'Código inválido.' };
168
+ accepted = true;
169
+ claim?.(code);
170
+ return { kind: 'accepted' };
171
+ };
152
172
  const server = createServer((req, res) => {
153
173
  res.setHeader('Cache-Control', 'no-store');
154
174
  res.setHeader('Referrer-Policy', 'no-referrer');
@@ -165,29 +185,20 @@ export async function authenticateWithBrowser(home, options = {}) {
165
185
  res.writeHead(404).end();
166
186
  return;
167
187
  }
168
- const incoming = url.searchParams.get('state') ?? '';
169
- const validState = /^[A-Za-z0-9_-]{43}$/.test(incoming) && timingSafeEqual(Buffer.from(incoming), Buffer.from(state));
170
- const code = url.searchParams.get('code');
171
- if (!validState || url.searchParams.getAll('state').length !== 1 || accepted) {
172
- res.writeHead(400).end('Retorno de autenticação inválido.');
188
+ const outcome = acceptCallback(url);
189
+ if (outcome.kind === 'invalid') {
190
+ res.writeHead(400).end(outcome.message);
173
191
  return;
174
192
  }
175
- if (url.searchParams.has('error')) {
176
- accepted = true;
193
+ if (outcome.kind === 'denied') {
177
194
  res.writeHead(400).end('Login não concluído. Volte ao terminal.');
178
- rejectClaim?.(new Error(t.denied));
179
195
  return;
180
196
  }
181
- if (!code || !/^[A-Za-z0-9_-]{43}$/.test(code) || url.searchParams.getAll('code').length !== 1) {
182
- res.writeHead(400).end('Código inválido.');
183
- return;
184
- }
185
- accepted = true;
186
197
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
187
198
  res.end(options.callbackHtml ?? callbackPage(locale));
188
- claim?.(code);
189
199
  });
190
200
  let timer;
201
+ let pasted;
191
202
  const abort = () => rejectClaim?.(new Error(t.denied));
192
203
  options.signal?.addEventListener('abort', abort, { once: true });
193
204
  try {
@@ -210,6 +221,31 @@ export async function authenticateWithBrowser(home, options = {}) {
210
221
  catch {
211
222
  report(t.openFailed);
212
223
  }
224
+ const pasteInput = options.pasteInput === undefined ? (process.stdin.isTTY ? process.stdin : null) : options.pasteInput;
225
+ if (pasteInput) {
226
+ report(t.pasteCallback);
227
+ pasted = createInterface({ input: pasteInput, terminal: false });
228
+ pasted.on('line', line => {
229
+ const text = line.trim();
230
+ if (!text || accepted)
231
+ return;
232
+ let url;
233
+ try {
234
+ url = new URL(text);
235
+ }
236
+ catch {
237
+ report(t.pasteInvalid);
238
+ return;
239
+ }
240
+ if (url.pathname !== '/callback' || !['127.0.0.1', 'localhost'].includes(url.hostname)) {
241
+ report(t.pasteInvalid);
242
+ return;
243
+ }
244
+ const outcome = acceptCallback(url);
245
+ if (outcome.kind === 'invalid')
246
+ report(`${t.pasteInvalid} (${outcome.message})`);
247
+ });
248
+ }
213
249
  const code = await received;
214
250
  assertCurrent();
215
251
  const response = await fetcher(endpoint(home, '/auth/mobile/exchange'), {
@@ -231,6 +267,9 @@ export async function authenticateWithBrowser(home, options = {}) {
231
267
  options.signal?.removeEventListener('abort', abort);
232
268
  if (timer)
233
269
  clearTimeout(timer);
270
+ pasted?.close();
271
+ if (options.pasteInput === undefined && process.stdin.isTTY)
272
+ process.stdin.pause();
234
273
  server.closeAllConnections();
235
274
  await new Promise(resolve => server.close(() => resolve()));
236
275
  }
package/dist/cloud.d.ts CHANGED
@@ -15,6 +15,10 @@ export interface VaultMemoryApi {
15
15
  saveGraphPreset?(input: Record<string, unknown>): Promise<unknown>;
16
16
  createGraphLink?(input: Record<string, unknown>): Promise<unknown>;
17
17
  deleteGraphLink?(id: string): Promise<unknown>;
18
+ temporalFacts?(input: Record<string, unknown>): Promise<unknown>;
19
+ temporalEntities?(input: Record<string, unknown>): Promise<unknown>;
20
+ temporalEntityFacts?(input: Record<string, unknown>): Promise<unknown>;
21
+ temporalStatus?(): Promise<unknown>;
18
22
  health(): Promise<unknown>;
19
23
  projects(): Promise<unknown>;
20
24
  createProject(input: Record<string, unknown>): Promise<unknown>;
@@ -131,6 +135,10 @@ export declare class VaultCloudClient implements VaultMemoryApi {
131
135
  saveGraphPreset(input: Record<string, unknown>): Promise<unknown>;
132
136
  createGraphLink(input: Record<string, unknown>): Promise<unknown>;
133
137
  deleteGraphLink(id: string): Promise<unknown>;
138
+ temporalFacts(input: Record<string, unknown>): Promise<unknown>;
139
+ temporalEntities(input: Record<string, unknown>): Promise<unknown>;
140
+ temporalEntityFacts(input: Record<string, unknown>): Promise<unknown>;
141
+ temporalStatus(): Promise<unknown>;
134
142
  private endpoint;
135
143
  private accessToken;
136
144
  private request;
@@ -138,5 +146,6 @@ export declare class VaultCloudClient implements VaultMemoryApi {
138
146
  }
139
147
  export declare class VaultCloudError extends Error {
140
148
  readonly status: number;
141
- constructor(status: number, message: string);
149
+ readonly code?: string | undefined;
150
+ constructor(status: number, message: string, code?: string | undefined);
142
151
  }
package/dist/cloud.js CHANGED
@@ -283,6 +283,17 @@ export class VaultCloudClient {
283
283
  await this.request(`/memory/graph/links/${encodeURIComponent(id)}`, { method: 'DELETE' });
284
284
  return { id, deleted: true };
285
285
  }
286
+ // Temporal graph reads are POST only to carry the body; they are safe to retry after a token refresh.
287
+ async temporalFacts(input) {
288
+ return this.request('/memory/temporal/facts', { retryRead: true, method: 'POST', body: input });
289
+ }
290
+ async temporalEntities(input) {
291
+ return this.request('/memory/temporal/entities', { retryRead: true, method: 'POST', body: input });
292
+ }
293
+ async temporalEntityFacts(input) {
294
+ return this.request('/memory/temporal/entity-facts', { retryRead: true, method: 'POST', body: input });
295
+ }
296
+ async temporalStatus() { return this.request('/memory/temporal/status'); }
286
297
  endpoint(path) {
287
298
  const base = validateApiUrl(loadConfig(this.home).apiUrl);
288
299
  const prefix = base.pathname.replace(/\/$/u, '');
@@ -398,10 +409,7 @@ export class VaultCloudClient {
398
409
  assertSession(this.home, session);
399
410
  const payload = text ? safeJson(text) : null;
400
411
  if (!response.ok) {
401
- const message = payload && typeof payload === 'object' && 'error' in payload
402
- ? String(payload.error)
403
- : `Vault respondeu HTTP ${response.status}`;
404
- throw new VaultCloudError(response.status, message);
412
+ throw new VaultCloudError(response.status, cloudErrorMessage(payload, response.status), cloudErrorCode(payload));
405
413
  }
406
414
  return payload;
407
415
  }
@@ -448,12 +456,39 @@ async function readResponseText(response, maxBytes) {
448
456
  }
449
457
  export class VaultCloudError extends Error {
450
458
  status;
451
- constructor(status, message) {
459
+ code;
460
+ constructor(status, message, code) {
452
461
  super(message);
453
462
  this.status = status;
463
+ this.code = code;
454
464
  this.name = 'VaultCloudError';
455
465
  }
456
466
  }
467
+ // The API answers `{error: "text"}` on older routes and `{error: {code, message}}` or `{error: "code", message}` on newer ones.
468
+ function cloudErrorMessage(payload, status) {
469
+ if (!payload || typeof payload !== 'object' || !('error' in payload))
470
+ return `Vault respondeu HTTP ${status}`;
471
+ const body = payload;
472
+ if (body.error && typeof body.error === 'object') {
473
+ const nested = body.error;
474
+ if (typeof nested.message === 'string' && nested.message)
475
+ return nested.message;
476
+ if (typeof nested.code === 'string' && nested.code)
477
+ return nested.code;
478
+ return `Vault respondeu HTTP ${status}`;
479
+ }
480
+ return typeof body.message === 'string' && body.message ? body.message : String(body.error);
481
+ }
482
+ function cloudErrorCode(payload) {
483
+ if (!payload || typeof payload !== 'object')
484
+ return undefined;
485
+ const body = payload;
486
+ if (typeof body.code === 'string')
487
+ return body.code;
488
+ if (body.error && typeof body.error === 'object' && typeof body.error.code === 'string')
489
+ return body.error.code;
490
+ return typeof body.error === 'string' && /^[a-z][a-z0-9_]{1,63}$/.test(body.error) ? body.error : undefined;
491
+ }
457
492
  function safeJson(text) {
458
493
  try {
459
494
  return JSON.parse(text);
@@ -13,7 +13,7 @@ const observe = {
13
13
  };
14
14
  const ACTION = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
15
15
  const READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
16
- const SAFETY = 'Não envie, compre, apague nem altere contas sem pedido explícito do usuário. Ação não verificada (verification.state=unverified) não é sucesso comprovado.';
16
+ const SAFETY = 'Roda em segundo plano na janela alvo: não move o mouse do usuário nem tira o foco do app em que ele está, e um cursor dedicado do agente aparece só sobre aquela janela (Vault Desktop 0.11.73+). Não envie, compre, apague nem altere contas sem pedido explícito do usuário. Ação não verificada (verification.state=unverified) não é sucesso comprovado.';
17
17
  function observeFlags(input, bridge) {
18
18
  return {
19
19
  app: undefined,
@@ -63,7 +63,7 @@ export function registerComputerTools(server, bridge = new ComputerUseBridge(),
63
63
  inputSchema: { app }, annotations: READ,
64
64
  }, ({ app: target }) => invoke(() => bridge.call('list-windows', { app: target })));
65
65
  server.registerTool('vault_go_computer_get_app_state', {
66
- description: 'Lê a árvore de acessibilidade da janela de um app e anexa um screenshot. Chame uma vez por turno antes de agir; os números da árvore são os elementIndex das ações e expiram após mudanças na UI.',
66
+ description: 'Lê a árvore de acessibilidade da janela de um app e anexa um screenshot, sem trazer a janela para frente. Chame uma vez por turno antes de agir; os números da árvore são os elementIndex das ações e expiram após mudanças na UI.',
67
67
  inputSchema: { app, ...observe }, annotations: READ,
68
68
  }, (input) => action('get-app-state', input, {}));
69
69
  server.registerTool('vault_go_computer_query', {
@@ -87,7 +87,7 @@ export async function callCrystalTool(home, name, args) {
87
87
  result: z.object({
88
88
  isError: z.boolean().optional(),
89
89
  content: z.array(z.object({ type: z.literal('text'), text: z.string() })),
90
- structuredContent: z.record(z.unknown()).optional(),
90
+ structuredContent: z.record(z.string(), z.unknown()).optional(),
91
91
  }),
92
92
  }).safeParse(payload);
93
93
  if (!envelope.success)
package/dist/locale.d.ts CHANGED
@@ -9,6 +9,8 @@ export declare const copy: {
9
9
  opening: string;
10
10
  fallback: string;
11
11
  openFailed: string;
12
+ pasteCallback: string;
13
+ pasteInvalid: string;
12
14
  connected: string;
13
15
  key: string;
14
16
  engine: string;
@@ -49,6 +51,8 @@ export declare const copy: {
49
51
  opening: string;
50
52
  fallback: string;
51
53
  openFailed: string;
54
+ pasteCallback: string;
55
+ pasteInvalid: string;
52
56
  connected: string;
53
57
  key: string;
54
58
  engine: string;
@@ -89,6 +93,8 @@ export declare const copy: {
89
93
  opening: string;
90
94
  fallback: string;
91
95
  openFailed: string;
96
+ pasteCallback: string;
97
+ pasteInvalid: string;
92
98
  connected: string;
93
99
  key: string;
94
100
  engine: string;
package/dist/locale.js CHANGED
@@ -18,6 +18,8 @@ export const copy = {
18
18
  opening: "Abrindo o autenticador do AI Vault Memory no navegador…",
19
19
  fallback: "Se necessário, abra este endereço no mesmo computador:",
20
20
  openFailed: "Não foi possível abrir automaticamente. Use o endereço acima.",
21
+ pasteCallback: "Em outro computador (SSH)? Depois de entrar, copie o endereço 127.0.0.1 que não abriu e cole aqui:",
22
+ pasteInvalid: "Endereço inválido. Cole o endereço completo que começa com http://127.0.0.1",
21
23
  connected: "Conta conectada",
22
24
  key: "Chave de API",
23
25
  engine: "Escolher motor de contexto",
@@ -58,6 +60,8 @@ export const copy = {
58
60
  opening: "Opening the AI Vault Memory authenticator in your browser…",
59
61
  fallback: "If needed, open this address on the same computer:",
60
62
  openFailed: "Could not open automatically. Use the address above.",
63
+ pasteCallback: "On another computer (SSH)? After signing in, copy the 127.0.0.1 address that failed to load and paste it here:",
64
+ pasteInvalid: "Invalid address. Paste the full address starting with http://127.0.0.1",
61
65
  connected: "Account connected",
62
66
  key: "API key",
63
67
  engine: "Choose context engine",
@@ -98,6 +102,8 @@ export const copy = {
98
102
  opening: "Abriendo el autenticador de AI Vault Memory en el navegador…",
99
103
  fallback: "Si es necesario, abre esta dirección en el mismo equipo:",
100
104
  openFailed: "No se pudo abrir automáticamente. Usa la dirección anterior.",
105
+ pasteCallback: "¿En otro equipo (SSH)? Después de entrar, copia la dirección 127.0.0.1 que no abrió y pégala aquí:",
106
+ pasteInvalid: "Dirección inválida. Pega la dirección completa que empieza con http://127.0.0.1",
101
107
  connected: "Cuenta conectada",
102
108
  key: "Clave de API",
103
109
  engine: "Elegir motor de contexto",
@@ -73,7 +73,7 @@ export function registerParityTools(server, home, cloud) {
73
73
  add('graph_link_delete', 'Exclui uma ligação manual da conta pelo UUID.', { id: uuid }, ({ id }) => cloud.deleteGraphLink ? cloud.deleteGraphLink(id) : unavailable(), false, true);
74
74
  add('graph_presets', 'Lista configurações salvas do grafo.', {}, () => cloud.graphPresets ? cloud.graphPresets() : unavailable());
75
75
  add('graph_preset_save', 'Salva uma configuração do grafo por nome.', {
76
- name: z.string().trim().min(1).max(80), settings: z.record(z.unknown()).refine(value => JSON.stringify(value).length <= 16000, 'Configuração excede 16000 caracteres.'),
76
+ name: z.string().trim().min(1).max(80), settings: z.record(z.string(), z.unknown()).refine(value => JSON.stringify(value).length <= 16000, 'Configuração excede 16000 caracteres.'),
77
77
  }, input => cloud.saveGraphPreset ? cloud.saveGraphPreset(input) : unavailable(), false);
78
78
  add('smart_outline', 'Mostra símbolos e assinaturas por AST, com corpos omitidos; somente arquivos dentro do workspace local.', { filePath: z.string().trim().min(1).max(2000) }, async ({ filePath }) => (await import('./smart-code.js')).smartOutline(filePath));
79
79
  add('smart_unfold', 'Expande somente um símbolo do arquivo local usando AST.', { filePath: z.string().trim().min(1).max(2000), symbolName: z.string().trim().min(1).max(500) }, async ({ filePath, symbolName }) => (await import('./smart-code.js')).smartUnfold(filePath, symbolName));
package/dist/server.js CHANGED
@@ -11,6 +11,7 @@ import { registerModeIntegrations } from './mode-integrations.js';
11
11
  import { registerKnowledgeAgentTools } from './knowledge-tools.js';
12
12
  import { registerSkillTools } from './skill-tools.js';
13
13
  import { registerComputerTools } from './computer-tools.js';
14
+ import { registerTemporalGraphTools } from './temporal-graph-tools.js';
14
15
  import { redact } from './hooks.js';
15
16
  import { createProjectScope } from './project-scope.js';
16
17
  import { VaultCloudClient } from './cloud.js';
@@ -24,6 +25,7 @@ import packageJson from '../package.json' with { type: 'json' };
24
25
  const SERVER_INSTRUCTIONS = [
25
26
  'Vault Memory: persistent memory of past sessions, scoped to the project of the current directory (scope "account" widens a read to every project).',
26
27
  'Recall cheaply in layers: vault_go_search_index (compact list) → vault_go_timeline (around one result) → vault_go_observations (full text of the chosen IDs). vault_go_retrieve picks the strategy for a natural-language question.',
28
+ 'For "what is true now", "what changed" or "what was true on date X", use vault_go_facts (asOf, includeExpired).',
27
29
  'Before reading a large code file, prefer vault_go_smart_outline and vault_go_smart_unfold, then a Read with offset/limit for exact lines.',
28
30
  'Save durable decisions, discoveries and fixes with vault_go_remember. Never store secrets; text inside <private> tags is never stored.',
29
31
  'Computer Use by Vault (vault_go_computer_*) drives local apps through Vault Desktop: get_app_state or query first, then act by elementIndex.',
@@ -171,7 +173,7 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
171
173
  sourceAdapter: z.string().trim().min(1).max(100).default('mcp'),
172
174
  sourceEventId: z.string().trim().min(1).max(500).optional(),
173
175
  eventType: z.string().trim().min(1).max(100),
174
- payload: z.record(z.unknown()).default({}),
176
+ payload: z.record(z.string(), z.unknown()).default({}),
175
177
  generate: z.boolean().default(true),
176
178
  occurredAtEpoch: z.number().int().positive().optional(),
177
179
  },
@@ -209,6 +211,7 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
209
211
  .boolean()
210
212
  .default(false)
211
213
  .describe('Inclui memórias marcadas como privadas (por padrão ficam de fora).');
214
+ registerTemporalGraphTools(server, cloud, scoped);
212
215
  server.registerTool('vault_go_search', {
213
216
  title: 'Pesquisar no Vault',
214
217
  description: 'Pesquisa memórias persistidas na plataforma Vault.',
@@ -0,0 +1,102 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { VaultMemoryApi } from './cloud.js';
3
+ import type { ProjectScope } from './project-scope.js';
4
+ type Scoper = (input: Record<string, unknown> & {
5
+ projectId?: string | undefined;
6
+ scope?: ProjectScope | undefined;
7
+ }) => Promise<Record<string, unknown>>;
8
+ export type TemporalFact = {
9
+ id: string;
10
+ fact: string;
11
+ relation: string;
12
+ source: {
13
+ id: string;
14
+ name: string;
15
+ };
16
+ target: {
17
+ id: string;
18
+ name: string;
19
+ };
20
+ validAt: string | null;
21
+ invalidAt: string | null;
22
+ createdAt: string;
23
+ expiredAt: string | null;
24
+ memoryIds: string[];
25
+ projectIds: string[];
26
+ };
27
+ export type FactStatus = 'current' | 'ended' | 'superseded' | 'not_yet_valid';
28
+ export declare const GRAPH_UNAVAILABLE_MESSAGE = "O grafo temporal ainda n\u00E3o est\u00E1 habilitado nesta conta (temporal graph not enabled yet). Nada est\u00E1 errado: use vault_go_search ou vault_go_search_index para recuperar mem\u00F3rias.";
29
+ /**
30
+ * "What was true on 2026-03-01" means the end of that day, so a bare date
31
+ * becomes its last UTC millisecond; full datetimes pass through unchanged.
32
+ */
33
+ export declare function normalizeAsOf(value: string | undefined): string | undefined;
34
+ /** Classifies a fact against the reference instant (asOf or now). */
35
+ export declare function factStatus(fact: Pick<TemporalFact, 'validAt' | 'invalidAt' | 'expiredAt'>, reference: number): FactStatus;
36
+ /** Compact text for the model plus the full structured payload for clients that read structuredContent. */
37
+ export declare function formatFacts(facts: TemporalFact[], options: {
38
+ asOf?: string | undefined;
39
+ includeExpired: boolean;
40
+ header?: string;
41
+ }): {
42
+ text: string;
43
+ structured: {
44
+ facts: {
45
+ status: FactStatus;
46
+ id: string;
47
+ fact: string;
48
+ relation: string;
49
+ source: {
50
+ id: string;
51
+ name: string;
52
+ };
53
+ target: {
54
+ id: string;
55
+ name: string;
56
+ };
57
+ validAt: string | null;
58
+ invalidAt: string | null;
59
+ createdAt: string;
60
+ expiredAt: string | null;
61
+ memoryIds: string[];
62
+ projectIds: string[];
63
+ }[];
64
+ count: number;
65
+ asOf: string | null;
66
+ includeExpired: boolean;
67
+ memoryIds: string[];
68
+ };
69
+ };
70
+ type Entity = {
71
+ id: string;
72
+ name: string;
73
+ summary?: string;
74
+ labels?: string[];
75
+ createdAt?: string;
76
+ };
77
+ export declare function formatEntities(value: unknown): {
78
+ text: string;
79
+ structured: {
80
+ entities: Entity[];
81
+ count: number;
82
+ };
83
+ };
84
+ type ErrorContext = {
85
+ notFound?: string | undefined;
86
+ };
87
+ /** Maps API failures: graph_unavailable is an expected state, not an error; validation errors stay actionable. */
88
+ export declare function temporalErrorResult(error: unknown, context?: ErrorContext): {
89
+ content: {
90
+ type: "text";
91
+ text: string;
92
+ }[];
93
+ structuredContent: Record<string, unknown>;
94
+ } | {
95
+ content: {
96
+ type: "text";
97
+ text: string;
98
+ }[];
99
+ isError: boolean;
100
+ };
101
+ export declare function registerTemporalGraphTools(server: McpServer, cloud: VaultMemoryApi, scoped: Scoper): void;
102
+ export {};
@@ -0,0 +1,234 @@
1
+ import { z } from 'zod';
2
+ import { errorMessage } from './mcp-result.js';
3
+ export const GRAPH_UNAVAILABLE_MESSAGE = 'O grafo temporal ainda não está habilitado nesta conta (temporal graph not enabled yet). Nada está errado: use vault_go_search ou vault_go_search_index para recuperar memórias.';
4
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
5
+ /**
6
+ * "What was true on 2026-03-01" means the end of that day, so a bare date
7
+ * becomes its last UTC millisecond; full datetimes pass through unchanged.
8
+ */
9
+ export function normalizeAsOf(value) {
10
+ if (!value)
11
+ return undefined;
12
+ return DATE_ONLY.test(value) ? `${value}T23:59:59.999Z` : value;
13
+ }
14
+ const asOfSchema = z
15
+ .string()
16
+ .trim()
17
+ .refine((value) => DATE_ONLY.test(value) || z.string().datetime({ offset: true }).safeParse(value).success, 'asOf deve ser uma data ISO (2026-03-01) ou data-hora ISO (2026-03-01T12:00:00Z).')
18
+ .refine((value) => !Number.isNaN(Date.parse(normalizeAsOf(value))), 'asOf inválido.')
19
+ .optional()
20
+ .describe('Data ISO: retorna o que era verdade naquele momento. Uma data sem hora vale até o fim do dia (UTC). Omitido: agora.');
21
+ const includeExpiredSchema = z
22
+ .boolean()
23
+ .default(false)
24
+ .describe('false (padrão): só fatos válidos no momento consultado. true: inclui fatos encerrados ou substituídos, marcados como tal.');
25
+ const time = (value) => {
26
+ if (!value)
27
+ return undefined;
28
+ const parsed = Date.parse(value);
29
+ return Number.isNaN(parsed) ? undefined : parsed;
30
+ };
31
+ /** Classifies a fact against the reference instant (asOf or now). */
32
+ export function factStatus(fact, reference) {
33
+ const expired = time(fact.expiredAt);
34
+ if (expired !== undefined && expired <= reference)
35
+ return 'superseded';
36
+ const invalid = time(fact.invalidAt);
37
+ if (invalid !== undefined && invalid <= reference)
38
+ return 'ended';
39
+ const valid = time(fact.validAt);
40
+ if (valid !== undefined && valid > reference)
41
+ return 'not_yet_valid';
42
+ return 'current';
43
+ }
44
+ const day = (value) => {
45
+ if (!value)
46
+ return '?';
47
+ return /^\d{4}-\d{2}-\d{2}T00:00:00(\.0+)?(Z|[+-]00:?00)$/.test(value) ? value.slice(0, 10) : value.replace(/\.\d+(?=Z|[+-])/, '');
48
+ };
49
+ const STATUS_LABEL = {
50
+ current: () => 'VIGENTE',
51
+ ended: (fact) => `ENCERRADO em ${day(fact.invalidAt)}`,
52
+ superseded: (fact) => `SUBSTITUÍDO em ${day(fact.expiredAt)}`,
53
+ not_yet_valid: (fact) => `AINDA NÃO VIGENTE (a partir de ${day(fact.validAt)})`,
54
+ };
55
+ function asFacts(value) {
56
+ if (!value || typeof value !== 'object' || !Array.isArray(value.facts)) {
57
+ throw new Error('Resposta do grafo temporal inválida.');
58
+ }
59
+ return value.facts;
60
+ }
61
+ function factLine(fact, index, reference) {
62
+ const status = factStatus(fact, reference);
63
+ const window = `válido ${day(fact.validAt)} → ${fact.invalidAt ? day(fact.invalidAt) : 'hoje'}`;
64
+ const memories = fact.memoryIds?.length ? ` | memoryIds: ${fact.memoryIds.join(', ')}` : '';
65
+ return `${index + 1}. [${STATUS_LABEL[status](fact)}] ${fact.source?.name ?? '?'} —${fact.relation}→ ${fact.target?.name ?? '?'}: ${fact.fact} (${window})${memories}`;
66
+ }
67
+ /** Compact text for the model plus the full structured payload for clients that read structuredContent. */
68
+ export function formatFacts(facts, options) {
69
+ const reference = options.asOf ? Date.parse(options.asOf) : Date.now();
70
+ const enriched = facts.map((fact) => ({ ...fact, status: factStatus(fact, reference) }));
71
+ const moment = options.asOf ? `em ${options.asOf}` : 'agora';
72
+ const filter = options.includeExpired ? 'incluindo encerrados/substituídos' : 'só vigentes';
73
+ const lines = [
74
+ ...(options.header ? [options.header] : []),
75
+ `${facts.length} fato(s) ${moment}, ${filter}.`,
76
+ ...facts.map((fact, index) => factLine(fact, index, reference)),
77
+ ];
78
+ const memoryIds = [...new Set(facts.flatMap((fact) => fact.memoryIds ?? []))];
79
+ if (memoryIds.length)
80
+ lines.push(`Fontes: vault_go_observations ids=${JSON.stringify(memoryIds.slice(0, 100))}`);
81
+ else if (!facts.length)
82
+ lines.push('Nenhum fato encontrado; tente outra consulta, scope "account" ou vault_go_search.');
83
+ return {
84
+ text: lines.join('\n'),
85
+ structured: { facts: enriched, count: facts.length, asOf: options.asOf ?? null, includeExpired: options.includeExpired, memoryIds },
86
+ };
87
+ }
88
+ export function formatEntities(value) {
89
+ if (!value || typeof value !== 'object' || !Array.isArray(value.entities)) {
90
+ throw new Error('Resposta do grafo temporal inválida.');
91
+ }
92
+ const entities = value.entities;
93
+ const lines = [
94
+ `${entities.length} entidade(s).`,
95
+ ...entities.map((entity, index) => {
96
+ const labels = entity.labels?.filter((label) => label !== 'Entity').join(', ');
97
+ return `${index + 1}. ${entity.name}${labels ? ` [${labels}]` : ''} (entityId: ${entity.id})${entity.summary ? ` — ${entity.summary}` : ''}`;
98
+ }),
99
+ entities.length
100
+ ? 'Use vault_go_entity_facts com entityId para ver os fatos de uma entidade.'
101
+ : 'Nenhuma entidade encontrada; tente outra consulta, scope "account" ou vault_go_search.',
102
+ ];
103
+ return { text: lines.join('\n'), structured: { entities, count: entities.length } };
104
+ }
105
+ function textResult(text, structuredContent) {
106
+ return { content: [{ type: 'text', text }], structuredContent };
107
+ }
108
+ function isGraphUnavailable(error, context) {
109
+ if (!error || typeof error !== 'object')
110
+ return false;
111
+ const { status, code, message } = error;
112
+ if (code === 'graph_unavailable' || (typeof message === 'string' && message.includes('graph_unavailable')))
113
+ return true;
114
+ // 503 is the documented answer; a 404 on a search route means a deployment that predates the temporal routes.
115
+ return status === 503 || (status === 404 && !context.notFound);
116
+ }
117
+ /** Maps API failures: graph_unavailable is an expected state, not an error; validation errors stay actionable. */
118
+ export function temporalErrorResult(error, context = {}) {
119
+ if (isGraphUnavailable(error, context)) {
120
+ return textResult(GRAPH_UNAVAILABLE_MESSAGE, { available: false, enabled: false, reason: 'graph_unavailable', fallback: 'vault_go_search' });
121
+ }
122
+ const status = error && typeof error === 'object' ? error.status : undefined;
123
+ const message = errorMessage(error);
124
+ const text = status === 400
125
+ ? `Requisição inválida para o grafo temporal: ${message}`
126
+ : status === 404 && context.notFound ? context.notFound : message;
127
+ return { content: [{ type: 'text', text }], isError: true };
128
+ }
129
+ async function guarded(operation, context = {}) {
130
+ try {
131
+ const { text, structured } = await operation();
132
+ return textResult(text, structured);
133
+ }
134
+ catch (error) {
135
+ return temporalErrorResult(error, context);
136
+ }
137
+ }
138
+ const unavailable = () => {
139
+ throw Object.assign(new Error('graph_unavailable'), { status: 503, code: 'graph_unavailable' });
140
+ };
141
+ export function registerTemporalGraphTools(server, cloud, scoped) {
142
+ const uuid = z.string().uuid();
143
+ const annotations = { readOnlyHint: true, idempotentHint: true, openWorldHint: true };
144
+ const scope = z
145
+ .enum(['project', 'account'])
146
+ .default('project')
147
+ .describe('project (padrão): só o projeto do diretório atual quando projectId não é informado; account: todos os projetos da conta.');
148
+ const query = z.string().trim().min(1).max(1_000);
149
+ // The temporal API filters by a list; the MCP keeps the single-project convention of the other read tools.
150
+ const projectIds = async (input) => {
151
+ const resolved = await scoped({ projectId: input.projectId, scope: input.scope });
152
+ return typeof resolved.projectId === 'string' ? { projectIds: [resolved.projectId] } : {};
153
+ };
154
+ server.registerTool('vault_go_facts', {
155
+ title: 'Fatos do grafo temporal',
156
+ description: 'Pesquisa fatos (sujeito —relação→ objeto) com janela de validade no grafo temporal. Use para "o que é verdade agora", "o que mudou" ou "o que era verdade na data X" (asOf). '
157
+ + 'Fatos encerrados ou substituídos aparecem marcados quando includeExpired=true. Cada fato traz memoryIds para carregar as fontes com vault_go_observations.',
158
+ inputSchema: {
159
+ query,
160
+ asOf: asOfSchema,
161
+ includeExpired: includeExpiredSchema,
162
+ projectId: uuid.optional(),
163
+ scope,
164
+ limit: z.number().int().min(1).max(50).default(20),
165
+ },
166
+ annotations,
167
+ }, async ({ query, asOf, includeExpired, projectId, scope, limit }) => guarded(async () => {
168
+ if (!cloud.temporalFacts)
169
+ unavailable();
170
+ const at = normalizeAsOf(asOf);
171
+ const response = await cloud.temporalFacts({
172
+ query,
173
+ ...(await projectIds({ projectId, scope })),
174
+ ...(at ? { asOf: at } : {}),
175
+ includeExpired,
176
+ limit,
177
+ });
178
+ return formatFacts(asFacts(response), { asOf: at, includeExpired });
179
+ }));
180
+ server.registerTool('vault_go_entities', {
181
+ title: 'Entidades do grafo temporal',
182
+ description: 'Pesquisa entidades do grafo temporal (pessoas, projetos, serviços, arquivos...) com resumo e rótulos. Use o entityId retornado em vault_go_entity_facts.',
183
+ inputSchema: {
184
+ query,
185
+ projectId: uuid.optional(),
186
+ scope,
187
+ limit: z.number().int().min(1).max(50).default(20),
188
+ },
189
+ annotations,
190
+ }, async ({ query, projectId, scope, limit }) => guarded(async () => {
191
+ if (!cloud.temporalEntities)
192
+ unavailable();
193
+ return formatEntities(await cloud.temporalEntities({ query, ...(await projectIds({ projectId, scope })), limit }));
194
+ }));
195
+ server.registerTool('vault_go_entity_facts', {
196
+ title: 'Fatos de uma entidade',
197
+ description: 'Lista os fatos em torno de uma entidade (entityId de vault_go_entities), com asOf para "o que era verdade na data X" e includeExpired para ver o histórico de mudanças.',
198
+ inputSchema: {
199
+ entityId: uuid,
200
+ asOf: asOfSchema,
201
+ includeExpired: includeExpiredSchema,
202
+ limit: z.number().int().min(1).max(100).default(50),
203
+ },
204
+ annotations,
205
+ }, async ({ entityId, asOf, includeExpired, limit }) => guarded(async () => {
206
+ if (!cloud.temporalEntityFacts)
207
+ unavailable();
208
+ const at = normalizeAsOf(asOf);
209
+ const response = await cloud.temporalEntityFacts({ entityId, ...(at ? { asOf: at } : {}), includeExpired, limit });
210
+ const entity = response?.entity;
211
+ const header = entity
212
+ ? `Entidade: ${entity.name} (entityId: ${entity.id})${entity.summary ? ` — ${entity.summary}` : ''}`
213
+ : undefined;
214
+ const formatted = formatFacts(asFacts(response), { asOf: at, includeExpired, ...(header ? { header } : {}) });
215
+ return { text: formatted.text, structured: { entity: entity ?? null, ...formatted.structured } };
216
+ }, { notFound: 'Entidade não encontrada nesta conta; obtenha o entityId com vault_go_entities.' }));
217
+ server.registerTool('vault_go_graph_status', {
218
+ title: 'Status do grafo temporal',
219
+ description: 'Mostra se o grafo temporal está habilitado e o andamento da ingestão de memórias (episódios concluídos, pendentes, com falha e ignorados).',
220
+ inputSchema: {},
221
+ annotations,
222
+ }, async () => guarded(async () => {
223
+ if (!cloud.temporalStatus)
224
+ unavailable();
225
+ const status = (await cloud.temporalStatus());
226
+ if (!status || typeof status !== 'object')
227
+ throw new Error('Resposta do grafo temporal inválida.');
228
+ const episodes = status.episodes ?? {};
229
+ const text = status.enabled
230
+ ? `Grafo temporal habilitado. Episódios: ${episodes.done ?? 0} concluídos, ${episodes.pending ?? 0} pendentes, ${episodes.failed ?? 0} com falha, ${episodes.skipped ?? 0} ignorados. Último processamento: ${status.lastProcessedAt ?? 'nunca'}.`
231
+ : GRAPH_UNAVAILABLE_MESSAGE;
232
+ return { text, structured: status };
233
+ }));
234
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "MCP server and installer for Vault Memory: search, capture, and use account memory across AI clients.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -161,6 +161,8 @@
161
161
  "dist/memory-archive.d.ts",
162
162
  "dist/agent-cost.js",
163
163
  "dist/agent-cost.d.ts",
164
+ "dist/temporal-graph-tools.js",
165
+ "dist/temporal-graph-tools.d.ts",
164
166
  "grammars/"
165
167
  ],
166
168
  "scripts": {
@@ -229,7 +231,7 @@
229
231
  "@ast-grep/napi": "0.45.3",
230
232
  "@modelcontextprotocol/sdk": "1.32.1",
231
233
  "@tree-sitter-grammars/tree-sitter-zig": "1.1.2",
232
- "zod": "3.25.76"
234
+ "zod": "4.6.7"
233
235
  },
234
236
  "devDependencies": {
235
237
  "@semantic-release/commit-analyzer": "13.0.1",
@@ -242,6 +244,6 @@
242
244
  "typescript": "^5.9.3"
243
245
  },
244
246
  "overrides": {
245
- "@hono/node-server": "2.0.11"
247
+ "@hono/node-server": "2.1.3"
246
248
  }
247
249
  }