vault-go 0.28.0 → 0.29.1

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
+ 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.
154
+
151
155
  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
156
 
153
157
  `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
@@ -1,10 +1,11 @@
1
1
  import { redact } from './capture-privacy.js';
2
2
  export { redact } from './capture-privacy.js';
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
4
+ import { basename, dirname, isAbsolute, relative, resolve, sep } 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,42 @@ function projectRoots(workspace) {
66
67
  return fallback;
67
68
  }
68
69
  }
69
- function matchWorkspaceProject(projects, roots) {
70
+ function matchWorkspaceProject(projects, roots, key, inGit = true) {
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)
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
+ const byRepo = key
83
+ ? projects.filter((item) => item && typeof item === "object" && item.repoKey === key).at(-1)
84
+ : undefined;
85
+ if (byRepo || inGit)
86
+ return byRepo;
87
+ // Why: a folder without git used to become its own project, splitting memories
88
+ // between an umbrella folder and its subfolders. Prefer the nearest existing ancestor.
89
+ const cwd = roots[0];
90
+ return projects
91
+ .filter((item) => {
92
+ if (!item || typeof item !== "object" || typeof item.rootPath !== "string")
93
+ return false;
94
+ const path = String(item.rootPath).trim();
95
+ if (!path)
96
+ return false;
97
+ const ancestor = relative(resolvedPath(path), cwd);
98
+ return Boolean(ancestor) && ancestor !== ".." && !ancestor.startsWith(`..${sep}`) && !isAbsolute(ancestor);
99
+ })
100
+ .sort((a, b) => resolvedPath(String(b.rootPath)).length - resolvedPath(String(a.rootPath)).length)[0];
77
101
  }
78
102
  /** The existing project for a working directory (worktrees map to their main checkout); never creates one. */
79
103
  export async function findWorkspaceProject(cloud, cwd) {
80
- return matchWorkspaceProject(await cloud.projects(), projectRoots(root(cwd)));
104
+ const workspace = root(cwd);
105
+ return matchWorkspaceProject(await cloud.projects(), projectRoots(workspace), repoKey(workspace), existsSync(resolve(workspace, ".git")));
81
106
  }
82
107
  function canonicalFile(file, workspace, project) {
83
108
  const absolute = resolve(workspace, file);
@@ -186,7 +211,8 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
186
211
  const projects = await cloud.projects();
187
212
  if (!Array.isArray(projects))
188
213
  throw new Error("Resposta de projetos inválida");
189
- let project = matchWorkspaceProject(projects, roots);
214
+ const key = repoKey(workspace);
215
+ let project = matchWorkspaceProject(projects, roots, key, existsSync(resolve(workspace, ".git")));
190
216
  const cwd = project ? resolvedPath(String(project.rootPath)) : roots[0];
191
217
  if (!project && reading)
192
218
  return hookOutput(event, "", adapter);
@@ -194,8 +220,13 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
194
220
  project = record(await cloud.createProject({
195
221
  name: (basename(cwd) || "workspace").padEnd(3, "_").slice(0, 80),
196
222
  rootPath: cwd,
223
+ ...(key ? { repoKey: key } : {}),
197
224
  }));
198
225
  }
226
+ else if (!reading && key && !project.repoKey && typeof project.id === "string" && cloud.updateProject) {
227
+ // Backfill projects created before repository keys; never blocks capture.
228
+ await cloud.updateProject(project.id, { repoKey: key }).catch(() => undefined);
229
+ }
199
230
  if (typeof project.id !== "string")
200
231
  throw new Error("Projeto inválido");
201
232
  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.1",
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",