vault-go 0.28.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,8 @@ 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
+
151
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.
152
154
 
153
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.
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.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,18 +67,24 @@ function projectRoots(workspace) {
66
67
  return fallback;
67
68
  }
68
69
  }
69
- function matchWorkspaceProject(projects, roots) {
70
+ function matchWorkspaceProject(projects, roots, key) {
70
71
  if (!Array.isArray(projects))
71
72
  throw new Error("Resposta de projetos inválida");
72
- return roots.map((cwd) => projects.find((item) => item &&
73
+ const byPath = roots.map((cwd) => projects.find((item) => item &&
73
74
  typeof item === "object" &&
74
75
  typeof item.rootPath === "string" &&
75
76
  Boolean(String(item.rootPath).trim()) &&
76
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);
77
83
  }
78
84
  /** The existing project for a working directory (worktrees map to their main checkout); never creates one. */
79
85
  export async function findWorkspaceProject(cloud, cwd) {
80
- return matchWorkspaceProject(await cloud.projects(), projectRoots(root(cwd)));
86
+ const workspace = root(cwd);
87
+ return matchWorkspaceProject(await cloud.projects(), projectRoots(workspace), repoKey(workspace));
81
88
  }
82
89
  function canonicalFile(file, workspace, project) {
83
90
  const absolute = resolve(workspace, file);
@@ -186,7 +193,8 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
186
193
  const projects = await cloud.projects();
187
194
  if (!Array.isArray(projects))
188
195
  throw new Error("Resposta de projetos inválida");
189
- let project = matchWorkspaceProject(projects, roots);
196
+ const key = repoKey(workspace);
197
+ let project = matchWorkspaceProject(projects, roots, key);
190
198
  const cwd = project ? resolvedPath(String(project.rootPath)) : roots[0];
191
199
  if (!project && reading)
192
200
  return hookOutput(event, "", adapter);
@@ -194,8 +202,13 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
194
202
  project = record(await cloud.createProject({
195
203
  name: (basename(cwd) || "workspace").padEnd(3, "_").slice(0, 80),
196
204
  rootPath: cwd,
205
+ ...(key ? { repoKey: key } : {}),
197
206
  }));
198
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
+ }
199
212
  if (typeof project.id !== "string")
200
213
  throw new Error("Projeto inválido");
201
214
  const limits = {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.28.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": {
@@ -87,6 +87,8 @@
87
87
  "dist/project-scope.js",
88
88
  "dist/mcp-result.d.ts",
89
89
  "dist/project-scope.d.ts",
90
+ "dist/repo-key.js",
91
+ "dist/repo-key.d.ts",
90
92
  "dist/parity-tools.js",
91
93
  "dist/parity-tools.d.ts",
92
94
  "dist/capabilities.js",