vault-go 0.27.0 → 0.29.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
@@ -148,6 +148,10 @@ Projects and sessions: `vault_go_projects`, `vault_go_project_create`, `vault_go
148
148
 
149
149
  Capture and recall: `vault_go_event`, `vault_go_remember`, `vault_go_forget`, `vault_go_search`, `vault_go_search_index`, `vault_go_retrieve`, `vault_go_observations`, `vault_go_timeline`, `vault_go_context`, `vault_go_file_context`, `vault_go_feed`, `vault_go_memory`, `vault_go_tool_uses`, `vault_go_stats`.
150
150
 
151
+ Projects are matched by path first and then by repository: vault-go reads the checkout's `origin` remote, normalizes it to `host/owner/repo` and records it as the project's `repoKey`. A clone on another machine or user joins the oldest project with the same `repoKey` instead of creating a new one, and projects created before this are backfilled the first time a capture matches them by path.
152
+
153
+ 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.
154
+
151
155
  `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.
152
156
 
153
157
  Knowledge bases: `vault_go_knowledge_bases`, `vault_go_knowledge_base`, `vault_go_knowledge_build`, `vault_go_knowledge_rebuild`, `vault_go_knowledge_query`, `vault_go_knowledge_delete`, `vault_go_prime_corpus`, `vault_go_query_corpus`, `vault_go_reprime_corpus`.
package/dist/cloud.d.ts CHANGED
@@ -18,6 +18,7 @@ export interface VaultMemoryApi {
18
18
  health(): Promise<unknown>;
19
19
  projects(): Promise<unknown>;
20
20
  createProject(input: Record<string, unknown>): Promise<unknown>;
21
+ updateProject?(id: string, input: Record<string, unknown>): Promise<unknown>;
21
22
  startSession(input: Record<string, unknown>): Promise<unknown>;
22
23
  endSession(id: string): Promise<unknown>;
23
24
  event(input: Record<string, unknown>): Promise<unknown>;
@@ -65,6 +66,7 @@ export declare class VaultCloudClient implements VaultMemoryApi {
65
66
  rotateDevice(id: string): Promise<unknown>;
66
67
  deviceAction(id: string, token: string, action: 'heartbeat' | 'disconnect', body?: Record<string, unknown>): Promise<unknown>;
67
68
  projects(): Promise<unknown>;
69
+ updateProject(id: string, input: Record<string, unknown>): Promise<unknown>;
68
70
  createProject(input: Record<string, unknown>): Promise<unknown>;
69
71
  startSession(input: Record<string, unknown>): Promise<unknown>;
70
72
  endSession(id: string): Promise<unknown>;
package/dist/cloud.js CHANGED
@@ -80,6 +80,9 @@ export class VaultCloudClient {
80
80
  async projects() {
81
81
  return this.request('/memory/projects');
82
82
  }
83
+ async updateProject(id, input) {
84
+ return this.request(`/memory/projects/${encodeURIComponent(id)}`, { method: 'PATCH', body: input });
85
+ }
83
86
  async createProject(input) {
84
87
  return this.request('/memory/projects', { method: 'POST', body: input });
85
88
  }
package/dist/hooks.d.ts CHANGED
@@ -3,6 +3,8 @@ import { type VaultMemoryApi } from "./cloud.js";
3
3
  export declare const HOOK_EVENTS: readonly ["context", "session-init", "observation", "file-context", "summarize"];
4
4
  export type HookEvent = (typeof HOOK_EVENTS)[number];
5
5
  type RecordValue = Record<string, unknown>;
6
+ /** The existing project for a working directory (worktrees map to their main checkout); never creates one. */
7
+ export declare function findWorkspaceProject(cloud: Pick<VaultMemoryApi, "projects">, cwd: string): Promise<RecordValue | undefined>;
6
8
  export declare function normalizeHookInput(adapter: string, value: unknown): RecordValue;
7
9
  export declare function hookOutput(event: HookEvent, context?: string, adapter?: string): RecordValue;
8
10
  export declare function executeHook(adapter: string, event: HookEvent, value: unknown, cloud?: VaultMemoryApi, home?: string, options?: {
package/dist/hooks.js CHANGED
@@ -5,6 +5,7 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
5
5
  import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
6
6
  import { applyCustomModeContext } from './custom-modes.js';
7
7
  import { VaultCloudClient } from "./cloud.js";
8
+ import { repoKey } from "./repo-key.js";
8
9
  import { vaultHome } from "./config.js";
9
10
  import { enqueueGeneration } from "./hook-queue.js";
10
11
  import { transcriptCaptureEnabled } from './transcript-config.js';
@@ -66,6 +67,25 @@ function projectRoots(workspace) {
66
67
  return fallback;
67
68
  }
68
69
  }
70
+ function matchWorkspaceProject(projects, roots, key) {
71
+ if (!Array.isArray(projects))
72
+ throw new Error("Resposta de projetos inválida");
73
+ const byPath = roots.map((cwd) => projects.find((item) => item &&
74
+ typeof item === "object" &&
75
+ typeof item.rootPath === "string" &&
76
+ Boolean(String(item.rootPath).trim()) &&
77
+ resolvedPath(String(item.rootPath)) === cwd)).find(Boolean);
78
+ if (byPath || !key)
79
+ return byPath;
80
+ // Why: a clone on another machine or user shares the repository, not the path.
81
+ // Projects arrive newest first; the oldest one with this key is the canonical one.
82
+ return projects.filter((item) => item && typeof item === "object" && item.repoKey === key).at(-1);
83
+ }
84
+ /** The existing project for a working directory (worktrees map to their main checkout); never creates one. */
85
+ export async function findWorkspaceProject(cloud, cwd) {
86
+ const workspace = root(cwd);
87
+ return matchWorkspaceProject(await cloud.projects(), projectRoots(workspace), repoKey(workspace));
88
+ }
69
89
  function canonicalFile(file, workspace, project) {
70
90
  const absolute = resolve(workspace, file);
71
91
  for (const directory of [workspace, resolvedPath(workspace)]) {
@@ -173,11 +193,8 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
173
193
  const projects = await cloud.projects();
174
194
  if (!Array.isArray(projects))
175
195
  throw new Error("Resposta de projetos inválida");
176
- let project = roots.map((cwd) => projects.find((item) => item &&
177
- typeof item === "object" &&
178
- typeof item.rootPath === "string" &&
179
- Boolean(String(item.rootPath).trim()) &&
180
- resolvedPath(String(item.rootPath)) === cwd)).find(Boolean);
196
+ const key = repoKey(workspace);
197
+ let project = matchWorkspaceProject(projects, roots, key);
181
198
  const cwd = project ? resolvedPath(String(project.rootPath)) : roots[0];
182
199
  if (!project && reading)
183
200
  return hookOutput(event, "", adapter);
@@ -185,8 +202,13 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
185
202
  project = record(await cloud.createProject({
186
203
  name: (basename(cwd) || "workspace").padEnd(3, "_").slice(0, 80),
187
204
  rootPath: cwd,
205
+ ...(key ? { repoKey: key } : {}),
188
206
  }));
189
207
  }
208
+ else if (!reading && key && !project.repoKey && typeof project.id === "string" && cloud.updateProject) {
209
+ // Backfill projects created before repository keys; never blocks capture.
210
+ await cloud.updateProject(project.id, { repoKey: key }).catch(() => undefined);
211
+ }
190
212
  if (typeof project.id !== "string")
191
213
  throw new Error("Projeto inválido");
192
214
  const limits = {
@@ -10,7 +10,7 @@ export declare function primeKnowledgeAgent(home: string, cloud: VaultMemoryApi,
10
10
  primed: boolean;
11
11
  memoryCount: number;
12
12
  conversationReset: boolean;
13
- engine: "vault-ai-resume" | "claude-subscription" | "openai-subscription" | "openrouter" | "gemini";
13
+ engine: "gemini" | "vault-ai-resume" | "claude-subscription" | "openai-subscription" | "openrouter";
14
14
  model: string;
15
15
  }>;
16
16
  export declare function queryKnowledgeAgent(home: string, cloud: VaultMemoryApi, id: string, question: string, dependencies?: Dependencies): Promise<{
@@ -31,7 +31,7 @@ export declare function queryKnowledgeAgent(home: string, cloud: VaultMemoryApi,
31
31
  title: string;
32
32
  }[];
33
33
  generated: boolean;
34
- engine: "vault-ai-resume" | "claude-subscription" | "openai-subscription" | "openrouter" | "gemini";
34
+ engine: "gemini" | "vault-ai-resume" | "claude-subscription" | "openai-subscription" | "openrouter";
35
35
  model: string;
36
36
  conversationTurns: number;
37
37
  contextTruncated: boolean;
@@ -0,0 +1,13 @@
1
+ import type { VaultMemoryApi } from './cloud.js';
2
+ export type ProjectScope = 'project' | 'account';
3
+ type ScopedInput = Record<string, unknown> & {
4
+ projectId?: string | undefined;
5
+ scope?: ProjectScope | undefined;
6
+ };
7
+ /**
8
+ * Why: MCP reads used to span every project of the account, so an agent in one
9
+ * repository received memories from unrelated clients. Reads now default to the
10
+ * project of the MCP server's working directory; `scope: "account"` widens them.
11
+ */
12
+ export declare function createProjectScope(cloud: VaultMemoryApi, cwd?: () => string): (input: ScopedInput) => Promise<Record<string, unknown>>;
13
+ export {};
@@ -0,0 +1,22 @@
1
+ import { findWorkspaceProject } from './hooks.js';
2
+ const CACHE_MS = 60_000;
3
+ /**
4
+ * Why: MCP reads used to span every project of the account, so an agent in one
5
+ * repository received memories from unrelated clients. Reads now default to the
6
+ * project of the MCP server's working directory; `scope: "account"` widens them.
7
+ */
8
+ export function createProjectScope(cloud, cwd = () => process.cwd()) {
9
+ let cached;
10
+ return async (input) => {
11
+ const { scope = 'project', ...rest } = input;
12
+ if (rest.projectId || scope === 'account')
13
+ return rest;
14
+ const workspace = cwd();
15
+ if (!cached || cached.cwd !== workspace || Date.now() - cached.at > CACHE_MS) {
16
+ const project = await findWorkspaceProject(cloud, workspace);
17
+ cached = { cwd: workspace, at: Date.now(), projectId: typeof project?.id === 'string' ? project.id : undefined };
18
+ }
19
+ // Outside a known project there is nothing narrower to scope to.
20
+ return cached.projectId ? { ...rest, projectId: cached.projectId } : rest;
21
+ };
22
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Normalizes a git remote to `host/owner/repo` (no scheme, credentials, port or
3
+ * `.git`). Local paths and unrecognized forms return undefined.
4
+ */
5
+ export declare function normalizeRemote(remote: string): string | undefined;
6
+ /** Repository key for a checkout (worktrees share their main checkout's remote). */
7
+ export declare function repoKey(workspace: string): string | undefined;
@@ -0,0 +1,91 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ // Same shape the engine accepts: lowercase host plus 1–8 path segments.
4
+ const REPO_KEY = /^[a-z0-9][a-z0-9.-]{0,252}(\/[a-z0-9._~-]{1,200}){1,8}$/;
5
+ function smallFile(path, limit) {
6
+ try {
7
+ const file = statSync(path);
8
+ if (!file.isFile() || file.size > limit)
9
+ return undefined;
10
+ return readFileSync(path, 'utf8');
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ /** The shared git directory holding `config`; worktrees point to it through `commondir`. */
17
+ function commonGitDir(workspace) {
18
+ const dotGit = resolve(workspace, '.git');
19
+ try {
20
+ if (statSync(dotGit).isDirectory())
21
+ return dotGit;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ const marker = /^gitdir:\s*(.+)$/m.exec(smallFile(dotGit, 4096) ?? '');
27
+ if (!marker)
28
+ return undefined;
29
+ const gitdir = resolve(workspace, marker[1].trim());
30
+ const common = smallFile(resolve(gitdir, 'commondir'), 4096)?.trim();
31
+ return common ? resolve(gitdir, common) : gitdir;
32
+ }
33
+ function originUrl(config) {
34
+ let section = '';
35
+ let first;
36
+ for (const line of config.split(/\r?\n/)) {
37
+ const header = /^\s*\[([^\]]+)\]\s*$/.exec(line);
38
+ if (header) {
39
+ section = header[1].trim();
40
+ continue;
41
+ }
42
+ const url = /^\s*url\s*=\s*(.+?)\s*$/.exec(line);
43
+ if (!url || !section.startsWith('remote '))
44
+ continue;
45
+ if (section === 'remote "origin"')
46
+ return url[1];
47
+ first ??= url[1];
48
+ }
49
+ return first;
50
+ }
51
+ /**
52
+ * Normalizes a git remote to `host/owner/repo` (no scheme, credentials, port or
53
+ * `.git`). Local paths and unrecognized forms return undefined.
54
+ */
55
+ export function normalizeRemote(remote) {
56
+ const value = remote.trim();
57
+ let host;
58
+ let path;
59
+ const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(.+)$/.exec(value);
60
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
61
+ let url;
62
+ try {
63
+ url = new URL(value);
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ if (url.protocol === 'file:')
69
+ return undefined;
70
+ host = url.hostname;
71
+ path = url.pathname;
72
+ }
73
+ else if (scp) {
74
+ host = scp[1];
75
+ path = scp[2];
76
+ }
77
+ else {
78
+ return undefined;
79
+ }
80
+ const key = `${host}/${path.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')}`.toLowerCase();
81
+ return REPO_KEY.test(key) && !key.endsWith('.git') ? key : undefined;
82
+ }
83
+ /** Repository key for a checkout (worktrees share their main checkout's remote). */
84
+ export function repoKey(workspace) {
85
+ const gitDir = commonGitDir(workspace);
86
+ if (!gitDir || dirname(gitDir) === gitDir)
87
+ return undefined;
88
+ const config = smallFile(resolve(gitDir, 'config'), 64 * 1024);
89
+ const url = config ? originUrl(config) : undefined;
90
+ return url ? normalizeRemote(url) : undefined;
91
+ }
package/dist/server.js CHANGED
@@ -9,6 +9,7 @@ import { registerCrystalTools } from './crystal-tools.js';
9
9
  import { registerModeIntegrations } from './mode-integrations.js';
10
10
  import { registerKnowledgeAgentTools } from './knowledge-tools.js';
11
11
  import { redact } from './hooks.js';
12
+ import { createProjectScope } from './project-scope.js';
12
13
  import { VaultCloudClient } from './cloud.js';
13
14
  import { getStatus, vaultHome } from './config.js';
14
15
  import { createPrivateBackup, restorePrivateBackup } from './backup.js';
@@ -172,12 +173,23 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
172
173
  inputSchema: { id: uuid },
173
174
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
174
175
  }, async ({ id }) => run(() => cloud.forget(id)));
176
+ const scoped = createProjectScope(cloud);
177
+ const scope = z
178
+ .enum(['project', 'account'])
179
+ .default('project')
180
+ .describe('project (padrão): só o projeto do diretório atual quando projectId não é informado; account: todos os projetos da conta.');
181
+ const includePrivate = z
182
+ .boolean()
183
+ .default(false)
184
+ .describe('Inclui memórias marcadas como privadas (por padrão ficam de fora).');
175
185
  server.registerTool('vault_go_search', {
176
186
  title: 'Pesquisar no Vault',
177
187
  description: 'Pesquisa memórias persistidas na plataforma Vault.',
178
188
  inputSchema: {
179
189
  query: z.string().trim().max(1_000).default(''),
180
190
  projectId: uuid.optional(),
191
+ scope,
192
+ includePrivate,
181
193
  kind: z.string().trim().max(100).optional(),
182
194
  memoryType: z.string().trim().max(100).optional(),
183
195
  platformSource: z.string().trim().max(100).optional(),
@@ -190,23 +202,24 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
190
202
  orderBy: z.enum(['date_asc', 'date_desc']).optional(),
191
203
  },
192
204
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
193
- }, async (input) => run(() => cloud.search(input)));
205
+ }, async (input) => run(async () => cloud.search(await scoped(input))));
194
206
  server.registerTool('vault_go_retrieve', {
195
207
  title: 'Recuperação automática do Vault',
196
208
  description: 'Delega ao motor central a escolha tipada entre busca, contexto de arquivo, linha do tempo, base de conhecimento, grafo ou nenhum resultado. Falhas usam busca determinística.',
197
209
  inputSchema: {
198
210
  query: z.string().trim().min(1).max(1_000),
199
211
  projectId: uuid.optional(),
212
+ scope,
200
213
  files: z.array(z.string().trim().min(1).max(2_000)).max(50).default([]),
201
214
  knowledgeBaseId: uuid.optional(),
202
215
  limit: z.number().int().min(1).max(200).default(20),
203
216
  maxChars: z.number().int().min(1_000).max(100_000).optional(),
204
217
  },
205
218
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
206
- }, async (input) => run(() => {
219
+ }, async (input) => run(async () => {
207
220
  if (!cloud.retrieve)
208
221
  throw new Error('Native retrieval is unavailable on this deployment.');
209
- return cloud.retrieve(input);
222
+ return cloud.retrieve(await scoped(input));
210
223
  }));
211
224
  server.registerTool('vault_go_decision_status', {
212
225
  title: 'Status da camada de decisão',
@@ -258,6 +271,8 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
258
271
  inputSchema: {
259
272
  query: z.string().trim().max(1_000).default(''),
260
273
  projectId: uuid.optional(),
274
+ scope,
275
+ includePrivate,
261
276
  kind: z.string().trim().max(100).optional(),
262
277
  memoryType: z.string().trim().max(100).optional(),
263
278
  platformSource: z.string().trim().max(100).optional(),
@@ -270,7 +285,7 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
270
285
  orderBy: z.enum(['date_asc', 'date_desc']).optional(),
271
286
  },
272
287
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
273
- }, async (input) => run(() => cloud.searchIndex(input)));
288
+ }, async (input) => run(async () => cloud.searchIndex(await scoped(input))));
274
289
  server.registerTool('vault_go_observations', {
275
290
  title: 'Carregar observações por ID',
276
291
  description: 'Terceira camada de recuperação: carrega em lote somente as observações escolhidas no índice ou na linha do tempo.',
@@ -286,15 +301,18 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
286
301
  anchorId: uuid.optional(),
287
302
  query: z.string().trim().min(1).max(1_000).optional(),
288
303
  projectId: uuid.optional(),
304
+ scope,
289
305
  platformSource: z.string().trim().min(1).max(100).optional(),
290
306
  before: z.number().int().min(0).max(100).default(5),
291
307
  after: z.number().int().min(0).max(100).default(5),
292
308
  },
293
309
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
294
- }, async (input) => run(() => {
310
+ }, async (input) => run(async () => {
295
311
  if (Boolean(input.anchorId) === Boolean(input.query))
296
312
  throw new Error('Informe exatamente anchorId ou query.');
297
- return cloud.timeline(input);
313
+ // An anchor already identifies its project; only query anchors are scoped.
314
+ const { scope: _scope, ...anchored } = input;
315
+ return cloud.timeline(input.anchorId ? anchored : await scoped(input));
298
316
  }));
299
317
  server.registerTool('vault_go_context', {
300
318
  title: 'Montar contexto',
@@ -329,6 +347,8 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
329
347
  description: 'Lista memórias recentes com paginação estável por cursor.',
330
348
  inputSchema: {
331
349
  projectId: uuid.optional(),
350
+ scope,
351
+ includePrivate,
332
352
  kind: z.string().trim().max(100).optional(),
333
353
  memoryType: z.string().trim().max(100).optional(),
334
354
  platformSource: z.string().trim().max(100).optional(),
@@ -336,7 +356,7 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
336
356
  limit: z.number().int().min(1).max(100).default(20),
337
357
  },
338
358
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
339
- }, async (input) => run(() => cloud.feed(input)));
359
+ }, async (input) => run(async () => cloud.feed(await scoped(input))));
340
360
  server.registerTool('vault_go_knowledge_bases', {
341
361
  title: 'Listar bases de conhecimento',
342
362
  description: 'Lista bases filtradas e persistidas na plataforma Vault.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.27.0",
3
+ "version": "0.29.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": {
@@ -84,7 +84,11 @@
84
84
  "dist/mcp-clients.js",
85
85
  "dist/mcp-clients.d.ts",
86
86
  "dist/mcp-result.js",
87
+ "dist/project-scope.js",
87
88
  "dist/mcp-result.d.ts",
89
+ "dist/project-scope.d.ts",
90
+ "dist/repo-key.js",
91
+ "dist/repo-key.d.ts",
88
92
  "dist/parity-tools.js",
89
93
  "dist/parity-tools.d.ts",
90
94
  "dist/capabilities.js",
@@ -133,7 +137,7 @@
133
137
  ],
134
138
  "scripts": {
135
139
  "clean": "bun -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
136
- "build": "bun run clean && tsc -p tsconfig.json",
140
+ "build": "bun run clean && tsc -p tsconfig.build.json",
137
141
  "typecheck": "tsc -p tsconfig.json --noEmit",
138
142
  "test": "bun run build && bun test src",
139
143
  "pack:check": "npm pack --dry-run",