vault-go 0.31.2 → 0.32.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
@@ -53,7 +53,9 @@ The setup wizard:
53
53
  3. Leaves other client config in place.
54
54
  4. Installs the local dashboard on port `38850` (starts at login on macOS).
55
55
 
56
- Supported clients: Codex, ChatGPT Desktop, Claude Code, Claude Desktop, Grok, Agy / Antigravity, Gemini CLI, Cursor, Windsurf, VS Code + GitHub Copilot, Copilot CLI, Roo Code, OpenCode, and Vault Desktop (Crystal).
56
+ Supported clients: Codex, ChatGPT Desktop, Claude Code, Claude Desktop, Grok, Agy / Antigravity, Gemini CLI, Cursor, Windsurf, VS Code + GitHub Copilot, Copilot CLI, Roo Code, OpenCode, Warp, Goose, Hermes, and Vault Desktop (Crystal).
57
+
58
+ Goose and Hermes are configured by editing a single entry in their `config.yaml` (`extensions.vault-go` and `mcp_servers.vault-go`). Comments and other lines are left as they are. A file with a shape that cannot be edited safely, such as an inline mapping, is left unchanged and reported.
57
59
 
58
60
  ```sh
59
61
  vault-go setup
@@ -152,6 +154,8 @@ vault-go engine key openrouter
152
154
 
153
155
  MCP tools never accept secrets. Store keys with the CLI or the local dashboard. Provider usage limits apply. Managed Vault AI generation still depends on server configuration and quota.
154
156
 
157
+ When the background queue hits an exhausted quota, Vault AI Resume and the Claude subscription stand in for each other. Other failures never switch engines. Set `VAULT_GO_ENGINE_FALLBACK=off` to disable this.
158
+
155
159
  ## MCP tools
156
160
 
157
161
  Account and health: `vault_go_status`, `vault_go_cloud_health`, `vault_go_preferences`, `vault_go_preferences_update`.
@@ -212,6 +216,10 @@ The worker drains a private queue with the selected engine. Successful generatio
212
216
 
213
217
  Opt-in Claude/Codex transcript watchers and project context chains are configured with MCP/CLI and stay bound to the current account.
214
218
 
219
+ Helper threads that clients generate themselves are never captured: Codex titles, suggestions and memory consolidation, Grok research briefings, and turns that are only a `<task-notification>`.
220
+
221
+ **File Read Gate (Claude Code).** In some cases a whole-file `Read` is denied, and the denial lists the file's memories. This happens only when the file is a code file of 32 KB or more, inside the workspace, and Vault memories about it are newer than its last change. Claude is pointed to `vault_go_smart_outline` / `vault_go_smart_unfold` or to a `Read` with `offset`/`limit` instead; partial reads always pass. Subagents and other clients are never gated, and any lookup failure lets the read through. Turn it off with `VAULT_GO_FILE_READ_GATE=off` or the `fileReadGate: false` preference.
222
+
215
223
  ## Automatic updates
216
224
 
217
225
  The worker checks the official npm registry daily for a stable `vault-go` release, stages an exact version under `~/.memvault/releases` without running package install scripts, validates it, and activates when idle. A failed start rolls back. Auth, preferences, and the queue are kept. Open MCP clients must reconnect to load new tools.
@@ -1 +1,3 @@
1
+ /** True when nothing remains once memory tags are removed: such text must not be stored at all. */
2
+ export declare function isFullyPrivate(value: string): boolean;
1
3
  export declare function redact(value: unknown): unknown;
@@ -1,4 +1,24 @@
1
1
  const SENSITIVE_KEY = /api[_-]?key|secret|token|password|authorization|cookie|credential|private[_-]?key/i;
2
+ const MEMORY_TAGS = `(private|vault-private|${["claude", "mem", "context"].join("-")}|system_instruction|system-instruction|persisted-output|system-reminder)`;
3
+ // Credentials pasted in free text have no key to match. `(?!\w)` instead of a
4
+ // closing `\b`: these classes include `-`, and `\b` after a final `-` needs a
5
+ // following word character, so such a key was left (or kept its last `-`).
6
+ const TOKEN_PATTERNS = [
7
+ /AKIA[0-9A-Z]{16}/g,
8
+ /\bgh[oprsu]_[A-Za-z0-9]{36}\b|\bgithub_pat_[A-Za-z0-9_]{82}\b/g,
9
+ /\bsk-(?!ant-)[A-Za-z0-9_-]{20,}(?!\w)/g,
10
+ /\bsk-ant-[A-Za-z0-9_-]{20,}(?!\w)/g,
11
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}(?!\w)/g,
12
+ /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?!\w)/g,
13
+ /-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/g,
14
+ /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g,
15
+ /\bAIza[0-9A-Za-z_-]{35}(?!\w)/g,
16
+ /\bvg_device_[a-f0-9]{64}\b/g,
17
+ ];
18
+ /** True when nothing remains once memory tags are removed: such text must not be stored at all. */
19
+ export function isFullyPrivate(value) {
20
+ return value.trim() !== '' && !value.replace(new RegExp(`<${MEMORY_TAGS}\\b[^>]*>[\\s\\S]*?(?:<\\/\\1\\s*>|$)`, "gi"), "").trim();
21
+ }
2
22
  /** Keep quoted values together: whitespace/escaped quotes must not leave a secret tail. */
3
23
  function redactText(value, depth = 0) {
4
24
  // Nested stringified payloads are untrusted. Do not recurse indefinitely.
@@ -13,8 +33,8 @@ function redactText(value, depth = 0) {
13
33
  }
14
34
  catch { /* Partial JSON and pasted logs still use the text filter below. */ }
15
35
  }
16
- return value
17
- .replace(new RegExp(`<(private|vault-private|${["claude", "mem", "context"].join("-")}|system_instruction|system-instruction|persisted-output|system-reminder)\\b[^>]*>[\\s\\S]*?(?:<\\/\\1\\s*>|$)`, "gi"), "[redacted]")
36
+ return TOKEN_PATTERNS.reduce((text, pattern) => text.replace(pattern, '[redacted]'), value)
37
+ .replace(new RegExp(`<${MEMORY_TAGS}\\b[^>]*>[\\s\\S]*?(?:<\\/\\1\\s*>|$)`, "gi"), "[redacted]")
18
38
  // Raw HTTP headers can contain spaces, auth schemes and several cookies.
19
39
  // Stop at a shell argument's quote or a new line, preserving adjacent diagnostics.
20
40
  .replace(/(\b(?:authorization|proxy-authorization|cookie|set-cookie)[ \t]*:[ \t]*)[^\r\n'\"]+/gi, '$1[redacted]')
package/dist/cli.js CHANGED
@@ -358,7 +358,7 @@ Uso:
358
358
  Captura opt-in de transcripts pelo worker local, com checkpoints e privacidade.
359
359
  Setup aceita --watch-transcripts claude|codex --workspace /projeto --consent.
360
360
 
361
- vault-go context-chain status|set|clear --workspace /projeto [--roots /a,/b] [--worktree-parent] [--consent]
361
+ vault-go context-chain status|set|clear --workspace /projeto [--roots /a,/b] [--worktree-parent] [--all-projects] [--consent]
362
362
  Contexto do SessionStart em projetos relacionados; orçamento total e conta preservados.
363
363
 
364
364
  vault-go hook <adaptador> <evento>
@@ -493,7 +493,7 @@ export async function runContextChain(args, home = vaultHome()) {
493
493
  const workspace = optionValue(args, '--workspace');
494
494
  if (!workspace)
495
495
  throw new Error('Informe --workspace /projeto.');
496
- result = configureContextChain(home, { workspace, roots: command === 'clear' ? [] : (optionValue(args, '--roots') ?? '').split(',').filter(Boolean), consent: args.includes('--consent'), includeWorktreeParent: command === 'set' && args.includes('--worktree-parent') });
496
+ result = configureContextChain(home, { workspace, roots: command === 'clear' ? [] : (optionValue(args, '--roots') ?? '').split(',').filter(Boolean), consent: args.includes('--consent'), includeWorktreeParent: command === 'set' && args.includes('--worktree-parent'), allProjects: command === 'set' && args.includes('--all-projects') });
497
497
  }
498
498
  else
499
499
  throw new Error('vault-go context-chain status|set|clear');
@@ -0,0 +1,2 @@
1
+ /** Whether a prompt was generated by the client itself; the same text from another client is a person's prompt. */
2
+ export declare function isClientInternalPrompt(adapter: string, text: string): boolean;
@@ -0,0 +1,34 @@
1
+ // Clients send their own helper threads through the same prompt hook as a
2
+ // person's turn. Storing them creates sessions and spends generation on the
3
+ // client's internals, so they are recognised by their fixed opening lines,
4
+ // never by phrases a person would type.
5
+ const CODEX_INTERNAL_PROMPT_OPENINGS = [
6
+ // Codex app: names a task.
7
+ /^You are a helpful assistant\. You will be presented with a user prompt, and your job is to provide a short title/,
8
+ // Codex memories: the consolidation pass.
9
+ /^## Memory Writing Agent: Phase 2 \(Consolidation\)/,
10
+ // Codex app: onboarding suggestions.
11
+ /^# Overview\r?\n(?:\r?\n)?Generate 0 to 3 hyperpersonalized suggestions/,
12
+ // Codex app: suggests the next message for the empty composer.
13
+ /^This is an app-generated request for a suggested next user message\. The user did not write this message\./,
14
+ // Codex memories: folds rollout summaries into memory_summary.md.
15
+ /^Consolidate the supplied rollout summaries into `memory_summary\.md`/,
16
+ ];
17
+ const GROK_INTERNAL_PROMPT_OPENINGS = [
18
+ // Research sub-agent briefings.
19
+ /^Independently verify every candidate claim in the JSON packet below/,
20
+ /^Rewrite the verified research findings/,
21
+ /^Investigate the JSON-encoded question/,
22
+ /^Break the JSON-encoded research query/,
23
+ ];
24
+ // A turn made only of a protocol envelope (e.g. a background task notice) carries no request from a person.
25
+ const PROTOCOL_ONLY = /^\s*<(task-notification)\b[^>]*>(?:(?!<\1\b|<\/\1\b)[\s\S])*<\/\1\s*>\s*$/;
26
+ const MAX_PROTOCOL_PAYLOAD_CHARS = 256 * 1024;
27
+ /** Whether a prompt was generated by the client itself; the same text from another client is a person's prompt. */
28
+ export function isClientInternalPrompt(adapter, text) {
29
+ if (text.length <= MAX_PROTOCOL_PAYLOAD_CHARS && PROTOCOL_ONLY.test(text))
30
+ return true;
31
+ const opening = text.trimStart();
32
+ const openings = adapter === 'codex' ? CODEX_INTERNAL_PROMPT_OPENINGS : adapter === 'grok' ? GROK_INTERNAL_PROMPT_OPENINGS : [];
33
+ return openings.some(pattern => pattern.test(opening));
34
+ }
@@ -11,6 +11,7 @@ interface Link {
11
11
  workspace: string;
12
12
  roots: string[];
13
13
  includeWorktreeParent: boolean;
14
+ allProjects?: boolean;
14
15
  }
15
16
  export declare function contextChainStatus(home: string): {
16
17
  chains: Link[];
@@ -22,6 +23,7 @@ export declare function configureContextChain(home: string, input: {
22
23
  roots: string[];
23
24
  consent?: boolean;
24
25
  includeWorktreeParent?: boolean;
26
+ allProjects?: boolean;
25
27
  }): {
26
28
  chains: Link[];
27
29
  maxProjects: number;
@@ -16,7 +16,7 @@ function configuration(home) {
16
16
  }
17
17
  export function contextChainStatus(home) { return { chains: configuration(home).chains, maxProjects: 5, default: 'workspace-only' }; }
18
18
  export function configureContextChain(home, input) {
19
- if ((input.roots.length || input.includeWorktreeParent) && input.consent !== true)
19
+ if ((input.roots.length || input.includeWorktreeParent || input.allProjects) && input.consent !== true)
20
20
  throw new Error('Relacionar projetos exige consent=true.');
21
21
  if (!captureOwner(home))
22
22
  throw new Error('Autentique a conta antes de relacionar projetos.');
@@ -27,8 +27,9 @@ export function configureContextChain(home, input) {
27
27
  throw new Error('Diretório de projeto inexistente.');
28
28
  const config = configuration(home);
29
29
  config.chains = config.chains.filter(c => c.workspace !== workspace);
30
- if (roots.length || input.includeWorktreeParent)
31
- config.chains.push({ workspace, roots, includeWorktreeParent: input.includeWorktreeParent === true });
30
+ // allProjects is the account-wide opt-in: every non-private project of this account joins the context budget.
31
+ if (roots.length || input.includeWorktreeParent || input.allProjects)
32
+ config.chains.push({ workspace, roots, includeWorktreeParent: input.includeWorktreeParent === true, ...(input.allProjects === true ? { allProjects: true } : {}) });
32
33
  if (config.chains.length > 50)
33
34
  throw new Error('Limite de 50 cadeias de contexto.');
34
35
  privateJson(home, 'context-chains.json', config);
@@ -74,7 +75,14 @@ export async function loadProjectContext(home, cloud, input) {
74
75
  if (project && project.private !== true && project.isPrivate !== true && project.is_private !== true && !selected.some(p => p.id === project.id))
75
76
  selected.push(project);
76
77
  }
77
- // A worktree shares its canonical project; extra projects still require explicit links.
78
+ if (chain?.allProjects === true) {
79
+ for (const item of input.projects) {
80
+ const project = item && typeof item === 'object' ? item : undefined;
81
+ if (project && typeof project.id === 'string' && project.private !== true && project.isPrivate !== true && project.is_private !== true && !selected.some(p => p.id === project.id))
82
+ selected.push(project);
83
+ }
84
+ }
85
+ // A worktree shares its canonical project; extra projects still require explicit links or the allProjects opt-in.
78
86
  if (selected[0]?.id !== input.primary.id)
79
87
  return '';
80
88
  const projects = selected.slice(0, Math.min(5, input.limit)), parts = [];
@@ -9,6 +9,11 @@ export interface EngineModelOption {
9
9
  aliases?: readonly string[];
10
10
  }
11
11
  export declare const ENGINE_MODELS: Record<ContextEngineId, readonly EngineModelOption[]>;
12
+ export declare class ContextQuotaError extends Error {
13
+ readonly engine: ContextEngineId;
14
+ constructor(engine: ContextEngineId);
15
+ }
16
+ export declare function isQuotaFailure(error: unknown): boolean;
12
17
  export interface ContextResult {
13
18
  title: string;
14
19
  content: string;
@@ -78,3 +83,4 @@ export interface GenerateContextOptions {
78
83
  }) => Promise<unknown>;
79
84
  }
80
85
  export declare function generateContext(home: string, engine: unknown, text: unknown, options?: GenerateContextOptions): Promise<ContextResult>;
86
+ export declare function generateContextWithFallback(home: string, engine: unknown, text: unknown, options?: GenerateContextOptions): Promise<ContextResult>;
@@ -54,6 +54,23 @@ const KEY_ENV = {
54
54
  openrouter: ["VAULT_GO_OPENROUTER_API_KEY", "OPENROUTER_API_KEY"],
55
55
  gemini: ["VAULT_GO_GEMINI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY"],
56
56
  };
57
+ // Provider notices that mean the allowance ran out rather than a bad request.
58
+ // A spent Anthropic API key is reported as a 400 "credit balance is too low".
59
+ const QUOTA_NOTICE = /credit balance is too low|usage limit|rate[ _]limit|limit reached|quota|too many requests/i;
60
+ export class ContextQuotaError extends Error {
61
+ engine;
62
+ constructor(engine) {
63
+ super("Context engine quota exhausted.");
64
+ this.engine = engine;
65
+ this.name = "ContextQuotaError";
66
+ }
67
+ }
68
+ export function isQuotaFailure(error) {
69
+ if (error instanceof ContextQuotaError)
70
+ return true;
71
+ const status = error && typeof error === "object" ? error.status : undefined;
72
+ return status === 429 || status === 402 || (error instanceof Error && QUOTA_NOTICE.test(error.message));
73
+ }
57
74
  export function isContextEngine(value) {
58
75
  return (typeof value === "string" &&
59
76
  CONTEXT_ENGINE_IDS.includes(value));
@@ -267,7 +284,9 @@ export const runContextCommand = (request) => new Promise((resolve, reject) => {
267
284
  child.on("close", (code) => finish(failure ??
268
285
  (code === 0
269
286
  ? undefined
270
- : new Error("Context CLI failed. Check its login and subscription limits."))));
287
+ : new Error(QUOTA_NOTICE.test(stdout + stderr)
288
+ ? "Context CLI reported an exhausted quota."
289
+ : "Context CLI failed. Check its login and subscription limits."))));
271
290
  child.stdin.end(request.input ?? "");
272
291
  if (request.signal?.aborted)
273
292
  abort();
@@ -539,6 +558,8 @@ async function generateApiContext(home, engine, text, options, signal) {
539
558
  ? error
540
559
  : new Error("Context engine request failed.");
541
560
  }
561
+ if (response.status === 429 || response.status === 402)
562
+ throw new ContextQuotaError(engine);
542
563
  if (!response.ok) {
543
564
  throw new Error(engine === "openrouter"
544
565
  ? "OpenRouter request failed. Check the API key and usage limits."
@@ -582,7 +603,7 @@ export async function generateContext(home, engine, text, options = {}) {
582
603
  signal.addEventListener("abort", abort, { once: true });
583
604
  void Promise.resolve()
584
605
  .then(() => options.managedGenerate(text, { signal, ...(modeGuidance ? { modeGuidance } : {}) }))
585
- .then(resolve, reject)
606
+ .then(resolve, (error) => reject(isQuotaFailure(error) ? new ContextQuotaError(engine) : error))
586
607
  .finally(() => signal.removeEventListener("abort", abort));
587
608
  });
588
609
  return validateContextResult(value, engine);
@@ -658,10 +679,11 @@ export async function generateContext(home, engine, text, options = {}) {
658
679
  timeoutMs: 90000,
659
680
  signal,
660
681
  });
661
- let parsed;
682
+ let parsed, quota = false;
662
683
  try {
663
684
  const output = JSON.parse(result.stdout);
664
685
  if (engine === "claude-subscription") {
686
+ quota = output["is_error"] === true && QUOTA_NOTICE.test(String(output["result"]));
665
687
  if (output["is_error"] === true)
666
688
  throw new Error();
667
689
  parsed =
@@ -671,6 +693,8 @@ export async function generateContext(home, engine, text, options = {}) {
671
693
  parsed = output;
672
694
  }
673
695
  catch {
696
+ if (quota)
697
+ throw new ContextQuotaError(engine);
674
698
  throw new Error("Context engine returned an invalid response.");
675
699
  }
676
700
  return validateContextResult(parsed, engine);
@@ -679,3 +703,31 @@ export async function generateContext(home, engine, text, options = {}) {
679
703
  rmSync(cwd, { recursive: true, force: true });
680
704
  }
681
705
  }
706
+ // The two Anthropic allowances stand in for each other: the managed Vault AI
707
+ // Resume (an API key held by the server) and this machine's Claude subscription
708
+ // login. Only an exhausted quota hands over; any other failure surfaces as is.
709
+ const QUOTA_FALLBACK = {
710
+ "vault-ai-resume": "claude-subscription",
711
+ "claude-subscription": "vault-ai-resume",
712
+ };
713
+ export async function generateContextWithFallback(home, engine, text, options = {}) {
714
+ try {
715
+ return await generateContext(home, engine, text, options);
716
+ }
717
+ catch (error) {
718
+ const fallback = isContextEngine(engine) ? QUOTA_FALLBACK[engine] : undefined;
719
+ if (!fallback ||
720
+ !isQuotaFailure(error) ||
721
+ options.signal?.aborted ||
722
+ (options.env ?? process.env).VAULT_GO_ENGINE_FALLBACK === "off" ||
723
+ (fallback === "vault-ai-resume" && !options.managedGenerate))
724
+ throw error;
725
+ try {
726
+ return await generateContext(home, fallback, text, options);
727
+ }
728
+ catch {
729
+ // The stand-in being unavailable must not hide why the selected engine stopped.
730
+ throw error;
731
+ }
732
+ }
733
+ }
@@ -9,7 +9,8 @@ function readJsonObject(path) {
9
9
  return {};
10
10
  let parsed;
11
11
  try {
12
- parsed = JSON.parse(readFileSync(path, "utf8"));
12
+ // Windows editors may save a UTF-8 byte order mark, which JSON.parse rejects.
13
+ parsed = JSON.parse(readFileSync(path, "utf8").replace(/^\uFEFF/, ""));
13
14
  }
14
15
  catch (error) {
15
16
  throw new Error(`JSON inválido em ${path}; o arquivo não foi alterado.`, {
@@ -98,7 +99,9 @@ function agyHandler(event, configHome = vaultHome()) {
98
99
  }
99
100
  function agyHookManifest(configHome = vaultHome()) {
100
101
  return {
101
- PreInvocation: [agyHandler("context", configHome)],
102
+ // SessionStart injects memory once per session. On PreInvocation it was re-sent
103
+ // before every model call, invalidating the provider prompt cache each step.
104
+ SessionStart: [agyHandler("context", configHome)],
102
105
  PreToolUse: [{ matcher: "*", hooks: [agyHandler("file-context", configHome)] }],
103
106
  PostToolUse: [
104
107
  { matcher: "*", hooks: [agyHandler("observation", configHome)] },
@@ -1,5 +1,5 @@
1
1
  import { VaultCloudClient } from './cloud.js';
2
- import { generateContext, type ContextResult } from './context-engines.js';
2
+ import { generateContextWithFallback, type ContextResult } from './context-engines.js';
3
3
  import { type TelegramObservation } from './telegram.js';
4
4
  import { privateStore } from './private-store.js';
5
5
  import { type CrystalMirrorRecord } from './crystal-memory-mirror.js';
@@ -43,7 +43,7 @@ export declare function generationQueueStatus(home?: string): {
43
43
  legacyAwaitingOwnership: number;
44
44
  heldForOtherAccount: number;
45
45
  };
46
- export declare function processGenerationQueue(home?: string, cloud?: VaultCloudClient, generate?: typeof generateContext, options?: {
46
+ export declare function processGenerationQueue(home?: string, cloud?: VaultCloudClient, generate?: typeof generateContextWithFallback, options?: {
47
47
  notifyObservation?: (observation: TelegramObservation) => Promise<unknown>;
48
48
  crystalMirrorStore?: CrystalMirrorStoreFactory;
49
49
  }): Promise<boolean>;
@@ -3,7 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
4
  import { VaultCloudClient } from './cloud.js';
5
5
  import { loadConfig, loadTokens, vaultHome } from './config.js';
6
- import { generateContext, selectedContextEngine } from './context-engines.js';
6
+ import { generateContextWithFallback, isQuotaFailure, selectedContextEngine } from './context-engines.js';
7
7
  import { notifyTelegramObservation } from './telegram.js';
8
8
  import { pushAwareness } from './grok-awareness.js';
9
9
  import { privateStore } from './private-store.js';
@@ -88,6 +88,8 @@ function isUnusableGenerationText(text) {
88
88
  }
89
89
  function generationFailureCode(error) {
90
90
  const message = error instanceof Error ? error.message : '';
91
+ if (isQuotaFailure(error))
92
+ return 'engine_quota';
91
93
  if (message.includes('Official CLI or subscription login unavailable'))
92
94
  return 'engine_cli_unavailable';
93
95
  if (message.includes('Context engine timed out'))
@@ -238,7 +240,7 @@ export function generationQueueStatus(home = vaultHome()) {
238
240
  const jobs = readQueue(home), owner = ownerKey(home);
239
241
  return { pending: jobs.filter(j => j.owner === owner || !j.owner).length, failed: jobs.filter(j => j.owner === owner && j.lastError).length, legacyAwaitingOwnership: jobs.filter(j => !j.owner).length, heldForOtherAccount: jobs.filter(j => j.owner && j.owner !== owner).length };
240
242
  }
241
- export async function processGenerationQueue(home = vaultHome(), cloud = new VaultCloudClient(home), generate = generateContext, options = {}) {
243
+ export async function processGenerationQueue(home = vaultHome(), cloud = new VaultCloudClient(home), generate = generateContextWithFallback, options = {}) {
242
244
  const release = await lock(home, 'generation-worker', false);
243
245
  if (!release)
244
246
  return false;
package/dist/hooks.js CHANGED
@@ -1,4 +1,6 @@
1
- import { redact } from './capture-privacy.js';
1
+ import { isFullyPrivate, redact } from './capture-privacy.js';
2
+ import { isClientInternalPrompt } from './client-prompts.js';
3
+ import { newestMemoryEpoch, readGateCandidate, readGateReason } from './read-gate.js';
2
4
  export { redact } from './capture-privacy.js';
3
5
  import { createHash, randomUUID } from "node:crypto";
4
6
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
@@ -214,6 +216,9 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
214
216
  const sessionId = string(input.session_id);
215
217
  if (!reading && !sessionId)
216
218
  return hookOutput(event, "", adapter);
219
+ // Client helper threads never become sessions, projects or memories.
220
+ if (event === "session-init" && isClientInternalPrompt(adapter, string(input.prompt)))
221
+ return hookOutput(event, "", adapter);
217
222
  const workspace = root(string(input.cwd, input.workspace) || process.cwd());
218
223
  const roots = projectRoots(workspace);
219
224
  const projects = await cloud.projects();
@@ -254,6 +259,11 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
254
259
  })
255
260
  : { context: await loadProjectContext(home, cloud, { workspace, primary: { id: project.id }, projects, ...limits }) });
256
261
  let context = string(result.context).slice(0, limits.maxChars);
262
+ if (event === "file-context" && context) {
263
+ const denial = await fullReadDenial(cloud, { adapter, toolName, tool, agentId: string(input.agent_id, input.agentId), workspace, prefs, memoryIds: result.memoryIds ?? result.memory_ids, context, file });
264
+ if (denial)
265
+ return denial;
266
+ }
257
267
  if (event === "context") {
258
268
  // Skills do projeto entram depois da memória e só no espaço que sobrar; falha aqui nunca derruba o contexto.
259
269
  try {
@@ -276,8 +286,10 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
276
286
  }));
277
287
  if (typeof session.id !== "string")
278
288
  throw new Error("Sessão inválida");
279
- const prompt = string(input.prompt);
280
- const response = string(input.last_assistant_message);
289
+ // Text that is entirely <private> (or other memory tags) is dropped, never stored as a "[redacted]" placeholder.
290
+ const visible = (value) => isFullyPrivate(value) ? "" : value;
291
+ const prompt = visible(string(input.prompt));
292
+ const response = visible(string(input.last_assistant_message));
281
293
  const toolPayload = record(redact({
282
294
  toolName,
283
295
  toolInput: tool,
@@ -351,6 +363,31 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
351
363
  await cloud.endSession(session.id);
352
364
  return hookOutput(event, "", adapter);
353
365
  }
366
+ // Fails open: any missing evidence or lookup error lets the Read through.
367
+ async function fullReadDenial(cloud, request) {
368
+ const enabled = request.prefs.fileReadGate !== false && process.env.VAULT_GO_FILE_READ_GATE !== "off";
369
+ const candidate = readGateCandidate({ adapter: request.adapter, toolName: request.toolName, toolInput: request.tool, agentId: request.agentId, workspace: request.workspace, enabled });
370
+ const ids = Array.isArray(request.memoryIds) ? request.memoryIds.filter((id) => typeof id === "string").slice(0, 20) : [];
371
+ if (!candidate || !ids.length)
372
+ return undefined;
373
+ try {
374
+ // Memories older than the file's last change describe code that may be gone: read it.
375
+ if (newestMemoryEpoch(await cloud.observations(ids)) <= candidate.mtimeMs)
376
+ return undefined;
377
+ }
378
+ catch {
379
+ return undefined;
380
+ }
381
+ return {
382
+ continue: true,
383
+ suppressOutput: true,
384
+ hookSpecificOutput: {
385
+ hookEventName: "PreToolUse",
386
+ permissionDecision: "deny",
387
+ permissionDecisionReason: readGateReason(request.file, ids, request.context),
388
+ },
389
+ };
390
+ }
354
391
  function boundedPreference(value, fallback, min, max) {
355
392
  return typeof value === 'number' && Number.isInteger(value) ? Math.max(min, Math.min(max, value)) : fallback;
356
393
  }
@@ -1,6 +1,6 @@
1
1
  export declare const MCP_SERVER_NAME = "vault-go";
2
2
  export declare const MCP_SERVER_COMMAND: readonly ["bunx", "--bun", "vault-go@latest", "serve"];
3
- export declare const MCP_CLIENTS: readonly ["codex", "claude", "claude-desktop", "cursor", "vscode", "copilot", "windsurf", "roo", "opencode", "grok", "agy", "gemini", "vault-desktop"];
3
+ export declare const MCP_CLIENTS: readonly ["codex", "claude", "claude-desktop", "cursor", "vscode", "copilot", "windsurf", "roo", "opencode", "grok", "agy", "gemini", "warp", "goose", "hermes", "vault-desktop"];
4
4
  export type McpClient = (typeof MCP_CLIENTS)[number];
5
5
  export interface DetectedClient {
6
6
  client: McpClient;
package/dist/installer.js CHANGED
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import { vaultHome } from './config.js';
6
6
  import { stageLocalRuntime } from './local-install.js';
7
7
  import { installCaptureHooks, installClientHooks, installOpenCodePlugin } from './hook-install.js';
8
+ import { upsertYamlEntry } from './yaml-entry.js';
8
9
  import { installManagedSkills } from './skill-install.js';
9
10
  export const MCP_SERVER_NAME = 'vault-go';
10
11
  export const MCP_SERVER_COMMAND = ['bunx', '--bun', 'vault-go@latest', 'serve'];
@@ -21,6 +22,9 @@ export const MCP_CLIENTS = [
21
22
  'grok',
22
23
  'agy',
23
24
  'gemini',
25
+ 'warp',
26
+ 'goose',
27
+ 'hermes',
24
28
  'vault-desktop',
25
29
  ];
26
30
  const CLIENT_ALIASES = {
@@ -34,7 +38,8 @@ function readJsonObject(path) {
34
38
  return {};
35
39
  let parsed;
36
40
  try {
37
- parsed = JSON.parse(readFileSync(path, 'utf8'));
41
+ // Windows editors may save a UTF-8 byte order mark, which JSON.parse rejects.
42
+ parsed = JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''));
38
43
  }
39
44
  catch (error) {
40
45
  throw new Error(`JSON inválido em ${path}; o arquivo não foi alterado.`, { cause: error });
@@ -62,7 +67,8 @@ function formatTomlValue(value) {
62
67
  return `[${value.map((item) => JSON.stringify(item)).join(', ')}]`;
63
68
  }
64
69
  function upsertTomlTable(path, table, assignments) {
65
- const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
70
+ // A byte order mark would hide a table on the first line and duplicate it on append.
71
+ const previous = existsSync(path) ? readFileSync(path, 'utf8').replace(/^\uFEFF/, '') : '';
66
72
  const body = Object.entries(assignments)
67
73
  .map(([key, value]) => `${key} = ${formatTomlValue(value)}`)
68
74
  .join('\n');
@@ -76,6 +82,21 @@ function upsertTomlTable(path, table, assignments) {
76
82
  : block;
77
83
  atomicWriteText(path, next);
78
84
  }
85
+ function upsertYamlFile(path, topKey, fields) {
86
+ const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
87
+ let next;
88
+ try {
89
+ next = upsertYamlEntry(previous, topKey, MCP_SERVER_NAME, fields);
90
+ }
91
+ catch (error) {
92
+ throw new Error(`${error instanceof Error ? error.message : 'YAML inválido'} (${path})`, { cause: error });
93
+ }
94
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
95
+ const temporary = join(dirname(path), `.${process.pid}.${Date.now()}.tmp`);
96
+ writeFileSync(temporary, next, { mode: 0o600 });
97
+ renameSync(temporary, path);
98
+ chmodSync(path, 0o600);
99
+ }
79
100
  function standardServer() {
80
101
  return {
81
102
  command: MCP_SERVER_COMMAND[0],
@@ -219,6 +240,21 @@ export function installMcpClient(client, options = {}) {
219
240
  installClientHooks('gemini', home);
220
241
  return { client, status: 'installed', destination: path };
221
242
  }
243
+ if (client === 'goose') {
244
+ // Goose starts MCP servers only from the top-level `extensions` mapping, never `mcpServers`.
245
+ const destination = join(home, '.config', 'goose', 'config.yaml');
246
+ upsertYamlFile(destination, 'extensions', {
247
+ enabled: true, type: 'stdio', name: MCP_SERVER_NAME,
248
+ cmd: MCP_SERVER_COMMAND[0], args: MCP_SERVER_COMMAND.slice(1), envs: {}, timeout: 300,
249
+ });
250
+ return { client, status: 'installed', destination };
251
+ }
252
+ if (client === 'hermes') {
253
+ const destination = join(home, '.hermes', 'config.yaml');
254
+ // MCP only: Hermes loads tools from `mcp_servers`; its capture uses a provider plugin vault-go does not ship.
255
+ upsertYamlFile(destination, 'mcp_servers', { command: MCP_SERVER_COMMAND[0], args: MCP_SERVER_COMMAND.slice(1) });
256
+ return { client, status: 'installed', destination };
257
+ }
222
258
  if (client === 'vault-desktop') {
223
259
  const destination = vaultDesktopMcpPath(home);
224
260
  mergeServer(destination);
@@ -236,6 +272,9 @@ export function installMcpClient(client, options = {}) {
236
272
  path = join(home, '.codeium', 'windsurf', 'mcp_config.json');
237
273
  else if (client === 'roo')
238
274
  path = resolve(cwd, '.roo', 'mcp.json');
275
+ // Warp reads global MCP servers from the dotfile ~/.warp/.mcp.json, not ~/.warp/mcp.json.
276
+ else if (client === 'warp')
277
+ path = join(home, '.warp', '.mcp.json');
239
278
  else
240
279
  path = join(home, '.config', 'opencode', 'opencode.json');
241
280
  if (client === 'opencode') {
@@ -284,6 +323,9 @@ export function detectMcpClients(home = homedir()) {
284
323
  ['grok', ['grok'], [join(home, '.grok')]],
285
324
  ['agy', ['agy'], [join(home, '.gemini', 'antigravity-cli'), join(home, '.gemini', 'config'), join(home, '.gemini', 'antigravity')]],
286
325
  ['gemini', ['gemini'], [join(home, '.gemini', 'settings.json')]],
326
+ ['warp', [], [join(home, '.warp'), '/Applications/Warp.app']],
327
+ ['goose', ['goose'], [join(home, '.config', 'goose')]],
328
+ ['hermes', ['hermes'], [join(home, '.hermes')]],
287
329
  ['vault-desktop', [], [vaultDesktopMcpPath(home), vaultDesktopDotMcpPath(home), '/Applications/Vault Desktop.app']],
288
330
  ];
289
331
  return definitions.map(([client, commands, paths]) => {
@@ -6,7 +6,7 @@ export function dashboardHtml() {
6
6
 
7
7
  /* Same identity tokens and glass surfaces as the Vault portal. */
8
8
  :root{--c-canvas:8 11 18;--c-surface:24 29 45;--c-surface-2:32 38 56;--c-line:53 61 81;--c-fg:241 243 252;--c-muted:172 180 203;--c-faint:148 159 187;--c-accent:185 182 255;color:rgb(var(--c-fg));background:rgb(var(--c-canvas))}body{background:radial-gradient(ellipse at 85% 8%,#534ca02e,transparent 45%),linear-gradient(145deg,#080b12,#101321 58%,#080b12)}[hidden]{display:none!important}.shell{grid-template-columns:258px 1fr;max-width:1600px}.rail{background:linear-gradient(160deg,#1c2135f5,#101522f5);border-color:#cdd8ff26}.brand{font-size:15px;letter-spacing:-.3px;gap:10px;white-space:nowrap}.brand img,.onboarding-logo{object-fit:contain;filter:drop-shadow(0 5px 12px #7775c533)}.brand-sub{display:block;font-size:9px;letter-spacing:1.7px;color:#acb4cb;margin-top:7px}.card,.panel,.onboarding,.step,.notice{background:linear-gradient(135deg,#272c3ed1,#131724e0);border-color:#cdd8ff26;box-shadow:0 24px 70px -30px #0009,inset 0 1px 0 #ffffff0c;backdrop-filter:blur(28px) saturate(135%)}.nav a.active,.nav a:hover{background:#b9b6ff20;color:#d3d1ff}.eyebrow,.step-number,.empty-symbol{color:#b9b6ff}p,.card-label{color:#acb4cb}.card-note,.row-meta,.rail-footer,.rail-label,.footer{color:#949fbb}.pulse{color:#b9b6ff;border-color:#b9b6ff30;background:#b9b6ff0d}.button{background:#202638;color:#f1f3fc;border-color:#cdd8ff26;text-decoration:none;justify-content:center}.button:hover{border-color:#b9b6ff88}.primary{background:linear-gradient(135deg,#c8c5ff,#9d9bed);color:#15152f;border-color:transparent;font-weight:650;padding:13px 20px;font-size:14px}.primary:hover{background:#d3d1ff}.onboarding{border:1px solid #cdd8ff26;border-radius:20px;overflow:hidden;margin-top:30px;animation:arrive .6s ease both}.onboarding-hero{position:relative;padding:38px;background:radial-gradient(ellipse at 100% 0%,#7370d02c,transparent 62%)}.onboarding-logo{width:86px;height:86px;margin-bottom:24px}.onboarding h1{font-size:36px;max-width:600px;line-height:1.12;margin:12px 0 18px}.onboarding-hero>p{max-width:570px;font-size:14px;line-height:1.8}.actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:25px}.link{color:#d3d1ff;text-decoration:none;font-size:12px}.link:hover{text-decoration:underline}.onboarding-hint{font-size:11px!important;color:#949fbb;margin-top:15px!important}.steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border-top:1px solid #cdd8ff26}.step{padding:25px;background:#080b122b;box-shadow:none;border-right:1px solid #cdd8ff16}.step:last-child{border:0}.step-number{font-family:monospace;font-size:11px;margin-bottom:13px}.step h2{margin-bottom:9px}.step p{font-size:12px}.terminal-fallback{padding:22px 30px;border-top:1px solid #cdd8ff26;background:#080b1233}.terminal-fallback p{font-size:12px}.terminal-fallback code{display:block;margin-top:10px;font-size:12px;color:#d3d1ff;overflow-wrap:anywhere}.connection-message{margin-top:18px;font-size:12px;color:#d3d1ff;min-height:18px}.next-step{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-top:22px;padding:23px;border:1px solid #b9b6ff30;border-radius:14px;background:#b9b6ff09}.next-step h2{margin-bottom:8px}.next-step p{font-size:12px}.next-step .actions{margin:0;flex-shrink:0}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline-color:#b9b6ff}.notice code{color:#d3d1ff}.search{border-color:#cdd8ff26;background:#0307117a}.breadcrumb b{color:#f1f3fc}.rail-footer .dot{background:#b9b6ff}.footer{margin-top:28px}@media(max-width:1100px){.shell{grid-template-columns:225px 1fr}.brand{font-size:13px}.main{padding:25px}.onboarding-hero{padding:30px}.next-step{align-items:flex-start;flex-direction:column}}@media(max-width:680px){.shell{display:block}.brand{font-size:16px}.onboarding{margin-top:22px}.onboarding-hero{padding:26px}.onboarding h1{font-size:30px}.onboarding-logo{width:65px;height:65px;margin-bottom:15px}.steps{grid-template-columns:1fr}.step{padding:20px 26px;border-right:0;border-bottom:1px solid #cdd8ff16}.step-number{margin-bottom:8px}.terminal-fallback{padding:22px 26px}.main{padding:20px}.next-step .actions{flex-shrink:1}.onboarding .actions .button{width:100%}.onboarding .actions .link{padding:8px 0}.card-value{overflow-wrap:anywhere}.nav{padding-bottom:2px}}
9
- .danger{border-color:#ed9a9a66;color:#ffb5b5;background:#53272d55}.danger:hover{border-color:#ffb5b5}.danger-title{color:#ffb5b5}.pagination{display:flex;gap:9px;align-items:center;flex-wrap:wrap;border-top:1px solid #cdd8ff20;margin-top:15px;padding-top:15px;color:#acb4cb;font-size:11px}.pagination label{display:flex;align-items:center;gap:7px}.pagination .page-summary{flex:1;min-width:95px}.pagination select{padding:7px}.pagination .button{padding:7px 10px}.pagination button:disabled{cursor:default}.row-open{background:transparent;border:0;text-align:left;padding:0;color:#d3d1ff;cursor:pointer;font:inherit}.row-open:hover{text-decoration:underline}.row-actions{padding-top:5px}.detail-dialog{color:#f1f3fc;background:linear-gradient(135deg,#272c3e,#131724);border:1px solid #cdd8ff40;border-radius:18px;max-width:800px;width:calc(100% - 32px);max-height:85vh;padding:26px;box-shadow:0 30px 100px #0009}.detail-dialog::backdrop{background:#050710bd;backdrop-filter:blur(5px)}.detail-dialog h2{font-size:20px;overflow-wrap:anywhere}.detail-fields{margin:0}.detail-fields dt{color:#acb4cb;font-size:11px;margin:20px 0 8px}.detail-fields dd{margin:0;font-size:13px;line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere}.detail-fields pre{font:inherit;white-space:pre-wrap;margin:0}.engine-options{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-top:18px}.engine-keys{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:16px}.engine-keys label{display:block;font-size:12px;margin-bottom:8px}.engine-keys input{width:100%;background:#0307117a;color:#f1f3fc;border:1px solid #cdd8ff26;border-radius:9px;padding:10px 12px;font:inherit;font-size:13px;margin-bottom:8px}.engine-option{display:block;padding:17px;border:1px solid #cdd8ff26;border-radius:10px;background:#080b1233;cursor:pointer}.engine-option:has(input:checked){border-color:#b9b6ff;background:#b9b6ff12}.engine-option:has(input:disabled){opacity:.7;cursor:default}.engine-option input{accent-color:#b9b6ff;margin-right:8px}.engine-option strong{font-size:12px}.engine-option p{font-size:11px;margin-top:9px}.engine-message{margin-top:13px;font-size:12px;color:#d3d1ff}.context-editor{margin-top:20px}.context-editor label{display:block;font-size:12px;margin-bottom:8px}.context-editor textarea{width:100%;min-height:130px;resize:vertical;background:#0307117a;color:#f1f3fc;border:1px solid #cdd8ff26;border-radius:9px;padding:12px;font:inherit;font-size:13px}.context-editor textarea:focus-visible{outline:2px solid #b9b6ff;outline-offset:3px}.mcp-clients{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:14px}.mcp-client{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid #cdd8ff26;border-radius:10px;padding:10px 12px;background:#080b1233;font-size:13px;flex-wrap:wrap}.mcp-client strong{font-size:13px;font-weight:600}.mcp-state{font-size:11px;color:#8edbc8;white-space:nowrap}.mcp-dim{opacity:.75}.mcp-evidence{font-size:10px;color:#949fbb;width:100%}.mcp-note{color:#949fbb;font-size:12px}.mcp-message{color:#ed9a9a;font-size:12px;margin-top:10px}.mcp-message:empty{display:none}.mcp-fallback{color:#949fbb;font-size:11px;margin-top:12px}.mcp-fallback code{font-size:11px;color:#c7d4df;background:#0307117a;border-radius:6px;padding:2px 6px}@media(max-width:800px){.engine-options{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){.detail-dialog::backdrop{backdrop-filter:none}}</style><script src="/app.js" defer></script></head><body><div class="shell"><aside class="rail"><div class="brand"><img src="/logo.png" alt="" width="46" height="46"><div>AI Vault Memory<span class="brand-sub">LOCAL AGENT</span></div></div><div class="rail-label" data-i18n="workspace">WORKSPACE</div><nav class="nav" aria-label="Navigation"><a class="active" href="#overview"><span class="icon">◈</span><span data-i18n="overview">Overview</span></a><a href="#activity"><span class="icon">⌁</span><span data-i18n="activity">Activity</span></a><a href="#memories"><span class="icon">▤</span><span data-i18n="memories">Memories</span></a><a href="#context"><span class="icon">✧</span><span data-i18n="contextEngine">Context engine</span></a><a href="#projects"><span class="icon">▱</span><span data-i18n="projects">Projects</span></a></nav><div class="rail-footer"><span class="dot"></span><span data-i18n="localWorkspace">Your local workspace</span><br><span data-i18n="privacy">Activity metadata stays local.</span></div></aside><main class="main" id="overview"><header class="top"><div class="breadcrumb">Vault <span aria-hidden="true"> / </span><b data-i18n="localMonitor">Local monitor</b></div><div class="controls"><select id="locale" aria-label="Language"><option value="en">English</option><option value="pt">Português</option><option value="es">Español</option></select><button class="button" id="logout" type="button" hidden data-i18n="logout">Sign out of dashboard</button><button class="button" id="reconnect" type="button" hidden data-i18n="reconnect">Reconnect account</button><button class="button" id="refresh" type="button"><span aria-hidden="true">↻</span><span data-i18n="refresh">Refresh cloud</span></button></div></header><div id="error" class="error" role="status" aria-live="polite"></div><section class="onboarding" id="onboarding"><div class="onboarding-hero"><img class="onboarding-logo" src="/logo.png" alt="" width="86" height="86"><div class="eyebrow" data-i18n="connectEyebrow">AI VAULT MEMORY · LOCAL</div><h1 data-i18n="connectTitle">Your memory. Connected to this machine.</h1><p data-i18n="connectIntro">Connect your Vault account to see your memories, projects and recent activity in one place.</p><div class="actions"><button type="button" class="button primary" id="connect" data-i18n="connect">Connect to Vault</button><a class="link" href="${VAULT_WEB_APP_URL}" target="_blank" rel="noopener noreferrer" data-i18n="openPortal">Open Vault portal ↗</a></div><p class="onboarding-hint" data-i18n="connectHint">Sign in securely in a separate window. Your password stays with the Vault portal.</p><div class="connection-message" id="connection-message" role="status" aria-live="polite"></div><a id="auth-fallback" class="button" hidden target="_blank" rel="noopener noreferrer" data-i18n="continueAuth">Continue sign-in ↗</a></div><div class="steps"><article class="step"><div class="step-number">01 / VAULT</div><h2 data-i18n="stepAccount">Connect your account</h2><p data-i18n="stepAccountBody">Sign in to Vault and authorize this local agent.</p></article><article class="step"><div class="step-number">02 / MCP</div><h2 data-i18n="stepClient">Connect your AI clients</h2><p data-i18n="stepClientBody">After signing in, connect Claude, Codex and other clients with one click, right here.</p></article><article class="step"><div class="step-number">03 / MEMORY</div><h2 data-i18n="stepMemory">Build your first memory</h2><p data-i18n="stepMemoryBody">Ask your connected client to save useful context, then follow its activity here.</p></article></div><div class="terminal-fallback"><button class="button" id="verify-connection" type="button" data-i18n="verifyConnection">Already signed in? Check connection</button><p style="margin-top:16px" data-i18n="terminalFallback">Already connected through the CLI? Open a secure local session:</p><code>bunx --bun vault-go@latest local open</code></div></section><div id="dashboard" hidden><div class="welcome"><div><div class="eyebrow" data-i18n="eyebrow">YOUR KNOWLEDGE, CONNECTED</div><h1 data-i18n="title">Your local workspace.</h1><p data-i18n="subtitle">Your Vault account, memories and activity on this machine.</p></div><img src="/logo.png" alt="" width="56" height="56"></div><div id="session-notice" hidden></div><section class="cards" aria-label="Status"><article class="card"><div class="card-label"><span data-i18n="localService">Local service</span><span aria-hidden="true">◈</span></div><div class="card-value" id="local-status">—</div><div class="card-note" id="local-note">localhost:38850</div></article><article class="card"><div class="card-label"><span data-i18n="cloudConnection">Cloud connection</span><span aria-hidden="true">↗</span></div><div class="card-value" id="cloud-status">—</div><div class="card-note" id="cloud-note">—</div></article><article class="card"><div class="card-label"><span data-i18n="recentOperations">Recent operations</span><span aria-hidden="true">⌁</span></div><div class="card-value" id="activity-total">—</div><div class="card-note" data-i18n="operationsNote">Recent MCP request metadata</div></article></section><section class="next-step" id="first-memory"><div><h2 data-i18n="firstTitle">Connect your AI clients</h2><p data-i18n="firstBody">Connect Claude, Codex and other clients here in one click, then ask them to save context. Memories appear after a cloud refresh.</p><div class="mcp-clients" id="mcp-clients" role="group" aria-label="AI clients"></div><p id="mcp-message" role="status" aria-live="polite" class="mcp-message"></p><p data-i18n="rememberPrompt" style="margin-top:10px"></p><p class="mcp-fallback"><span data-i18n="mcpFallback"></span> <code>bunx --bun vault-go@latest install</code></p></div><div class="actions"><a class="link" href="${VAULT_WEB_MCP_URL}" target="_blank" rel="noopener noreferrer" data-i18n="otherClients">Other clients ↗</a><a class="link" href="${VAULT_WEB_MEMORY_URL}" target="_blank" rel="noopener noreferrer" data-i18n="openMemories">Open memories ↗</a></div></section><div class="layout"><section class="panel wide" id="security"><div class="panel-head"><h2 data-i18n="securityTitle">Security on this machine</h2></div><div id="security-facts"></div><div class="actions"><button class="button danger" id="disconnect-device" type="button" data-i18n="disconnectDevice">Disconnect device</button></div></section><section class="panel wide" id="context"><div class="panel-head"><h2 data-i18n="contextEngine">Context engine</h2></div><p data-i18n="engineIntro">Choose how Vault prepares your context.</p><div class="engine-options" id="engine-options" role="radiogroup" aria-label="Context engine"></div><div class="engine-options" id="model-options" role="radiogroup" aria-label="Context model"></div><div class="engine-keys" id="engine-keys"></div><div class="engine-message" id="engine-message" role="status" aria-live="polite"></div><div class="context-editor"><label for="context-text" data-i18n="contextText">Text to prepare</label><textarea id="context-text" maxlength="16000" aria-describedby="context-note"></textarea><p id="context-note" data-i18n="contextNote"></p><div class="actions"><button class="button primary" id="context-preview" type="button" data-i18n="contextPreview"></button><button class="button" id="context-stop" type="button" data-i18n="cancel" hidden></button></div><p id="context-progress" role="status" aria-live="polite"></p></div></section><section class="panel" id="activity"><div class="panel-head"><h2 data-i18n="activity">Activity</h2><span class="count" id="activity-count">0</span></div><input class="search" id="activity-search" type="search" data-placeholder="searchActivity" aria-label="Search activity"><div id="activity-list"></div><div class="pagination" id="activity-pagination"></div></section><section class="panel" id="memories"><div class="panel-head"><h2 data-i18n="memories">Memories</h2><span class="count" id="memory-count">0</span></div><input class="search" id="memory-search" type="search" data-placeholder="searchMemories" aria-label="Search memories"><div id="memory-list"></div><div class="pagination" id="memory-pagination"></div></section><section class="panel wide" id="projects"><div class="panel-head"><h2 data-i18n="projects">Projects</h2><span class="count" id="project-count">0</span></div><input class="search" id="project-search" type="search" data-placeholder="searchProjects" aria-label="Search projects"><div id="project-list"></div><div class="pagination" id="project-pagination"></div></section></div></div><footer class="footer"><span data-i18n="footer">Vault Local · Context with a place to call home.</span><span id="updated" aria-live="off">—</span></footer></main></div><dialog class="detail-dialog" id="detail-dialog" aria-labelledby="detail-title"><div class="panel-head"><h2 id="detail-title"></h2><button class="button" type="button" id="detail-copy" hidden data-i18n="copyResult">Copy result</button><button class="button" type="button" id="detail-close" data-i18n="close">Close</button></div><dl class="detail-fields" id="detail-fields"></dl></dialog><dialog class="detail-dialog" id="logout-dialog" aria-labelledby="logout-title" aria-describedby="logout-description"><h2 id="logout-title" data-i18n="logout"></h2><p id="logout-description" data-i18n="logoutDescription" style="margin-top:16px"></p><div class="actions"><button class="button" type="button" id="logout-cancel" data-i18n="cancel"></button><button class="button primary" type="button" id="logout-confirm" data-i18n="logoutConfirm"></button></div></dialog><dialog class="detail-dialog" id="context-confirm-dialog" aria-labelledby="context-confirm-title" aria-describedby="context-confirm-note"><h2 id="context-confirm-title" data-i18n="contextPreview"></h2><p id="context-confirm-engine" style="margin-top:16px"></p><p id="context-confirm-billing"></p><p id="context-confirm-note" data-i18n="contextConsentNote" style="margin-top:16px"></p><pre id="context-confirm-text" style="white-space:pre-wrap;overflow-wrap:anywhere;max-height:32vh;overflow:auto;font-size:12px;line-height:1.7"></pre><label style="display:flex;gap:10px;align-items:flex-start;font-size:12px"><input id="context-consent" type="checkbox"><span data-i18n="contextConsent"></span></label><div class="actions"><button class="button" id="context-cancel" type="button" data-i18n="cancel"></button><button class="button primary" id="context-generate" type="button" disabled data-i18n="contextGenerate"></button></div></dialog><dialog class="detail-dialog" id="disconnect-dialog" aria-labelledby="disconnect-title" aria-describedby="disconnect-description"><h2 class="danger-title" id="disconnect-title" data-i18n="disconnectDevice"></h2><p id="disconnect-description" data-i18n="disconnectDescription" style="margin-top:16px"></p><p id="disconnect-error" class="error" role="alert"></p><div class="actions"><button class="button" type="button" id="disconnect-cancel" data-i18n="cancel"></button><button class="button danger" type="button" id="disconnect-confirm" data-i18n="disconnectConfirm"></button></div></dialog></body></html>`;
9
+ .danger{border-color:#ed9a9a66;color:#ffb5b5;background:#53272d55}.danger:hover{border-color:#ffb5b5}.danger-title{color:#ffb5b5}.pagination{display:flex;gap:9px;align-items:center;flex-wrap:wrap;border-top:1px solid #cdd8ff20;margin-top:15px;padding-top:15px;color:#acb4cb;font-size:11px}.pagination label{display:flex;align-items:center;gap:7px}.pagination .page-summary{flex:1;min-width:95px}.pagination select{padding:7px}.pagination .button{padding:7px 10px}.pagination button:disabled{cursor:default}.row-open{background:transparent;border:0;text-align:left;padding:0;color:#d3d1ff;cursor:pointer;font:inherit}.row-open:hover{text-decoration:underline}.row-actions{padding-top:5px}.detail-dialog{color:#f1f3fc;background:linear-gradient(135deg,#272c3e,#131724);border:1px solid #cdd8ff40;border-radius:18px;max-width:800px;width:calc(100% - 32px);max-height:85vh;padding:26px;box-shadow:0 30px 100px #0009}.detail-dialog::backdrop{background:#050710bd;backdrop-filter:blur(5px)}.detail-dialog h2{font-size:20px;overflow-wrap:anywhere}.detail-fields{margin:0}.detail-fields dt{color:#acb4cb;font-size:11px;margin:20px 0 8px}.detail-fields dd{margin:0;font-size:13px;line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere}.detail-fields pre{font:inherit;white-space:pre-wrap;margin:0}.clamped{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:8;overflow:hidden}.row-title.clamped{-webkit-line-clamp:3}.show-more{background:transparent;border:0;padding:6px 0 0;color:#b9b6ff;font:inherit;font-size:12px;cursor:pointer}.show-more:hover{text-decoration:underline}.show-more:focus-visible{outline:2px solid #b9b6ff;outline-offset:3px;border-radius:4px}.engine-options{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-top:18px}.engine-keys{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:16px}.engine-keys label{display:block;font-size:12px;margin-bottom:8px}.engine-keys input{width:100%;background:#0307117a;color:#f1f3fc;border:1px solid #cdd8ff26;border-radius:9px;padding:10px 12px;font:inherit;font-size:13px;margin-bottom:8px}.engine-option{display:block;padding:17px;border:1px solid #cdd8ff26;border-radius:10px;background:#080b1233;cursor:pointer}.engine-option:has(input:checked){border-color:#b9b6ff;background:#b9b6ff12}.engine-option:has(input:disabled){opacity:.7;cursor:default}.engine-option input{accent-color:#b9b6ff;margin-right:8px}.engine-option strong{font-size:12px}.engine-option p{font-size:11px;margin-top:9px}.engine-message{margin-top:13px;font-size:12px;color:#d3d1ff}.context-editor{margin-top:20px}.context-editor label{display:block;font-size:12px;margin-bottom:8px}.context-editor textarea{width:100%;min-height:130px;resize:vertical;background:#0307117a;color:#f1f3fc;border:1px solid #cdd8ff26;border-radius:9px;padding:12px;font:inherit;font-size:13px}.context-editor textarea:focus-visible{outline:2px solid #b9b6ff;outline-offset:3px}.mcp-clients{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:14px}.mcp-client{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid #cdd8ff26;border-radius:10px;padding:10px 12px;background:#080b1233;font-size:13px;flex-wrap:wrap}.mcp-client strong{font-size:13px;font-weight:600}.mcp-state{font-size:11px;color:#8edbc8;white-space:nowrap}.mcp-dim{opacity:.75}.mcp-evidence{font-size:10px;color:#949fbb;width:100%}.mcp-note{color:#949fbb;font-size:12px}.mcp-message{color:#ed9a9a;font-size:12px;margin-top:10px}.mcp-message:empty{display:none}.mcp-fallback{color:#949fbb;font-size:11px;margin-top:12px}.mcp-fallback code{font-size:11px;color:#c7d4df;background:#0307117a;border-radius:6px;padding:2px 6px}@media(max-width:800px){.engine-options{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){.detail-dialog::backdrop{backdrop-filter:none}}</style><script src="/app.js" defer></script></head><body><div class="shell"><aside class="rail"><div class="brand"><img src="/logo.png" alt="" width="46" height="46"><div>AI Vault Memory<span class="brand-sub">LOCAL AGENT</span></div></div><div class="rail-label" data-i18n="workspace">WORKSPACE</div><nav class="nav" aria-label="Navigation"><a class="active" href="#overview"><span class="icon">◈</span><span data-i18n="overview">Overview</span></a><a href="#activity"><span class="icon">⌁</span><span data-i18n="activity">Activity</span></a><a href="#memories"><span class="icon">▤</span><span data-i18n="memories">Memories</span></a><a href="#context"><span class="icon">✧</span><span data-i18n="contextEngine">Context engine</span></a><a href="#projects"><span class="icon">▱</span><span data-i18n="projects">Projects</span></a></nav><div class="rail-footer"><span class="dot"></span><span data-i18n="localWorkspace">Your local workspace</span><br><span data-i18n="privacy">Activity metadata stays local.</span></div></aside><main class="main" id="overview"><header class="top"><div class="breadcrumb">Vault <span aria-hidden="true"> / </span><b data-i18n="localMonitor">Local monitor</b></div><div class="controls"><select id="locale" aria-label="Language"><option value="en">English</option><option value="pt">Português</option><option value="es">Español</option></select><button class="button" id="logout" type="button" hidden data-i18n="logout">Sign out of dashboard</button><button class="button" id="reconnect" type="button" hidden data-i18n="reconnect">Reconnect account</button><button class="button" id="refresh" type="button"><span aria-hidden="true">↻</span><span data-i18n="refresh">Refresh cloud</span></button></div></header><div id="error" class="error" role="status" aria-live="polite"></div><section class="onboarding" id="onboarding"><div class="onboarding-hero"><img class="onboarding-logo" src="/logo.png" alt="" width="86" height="86"><div class="eyebrow" data-i18n="connectEyebrow">AI VAULT MEMORY · LOCAL</div><h1 data-i18n="connectTitle">Your memory. Connected to this machine.</h1><p data-i18n="connectIntro">Connect your Vault account to see your memories, projects and recent activity in one place.</p><div class="actions"><button type="button" class="button primary" id="connect" data-i18n="connect">Connect to Vault</button><a class="link" href="${VAULT_WEB_APP_URL}" target="_blank" rel="noopener noreferrer" data-i18n="openPortal">Open Vault portal ↗</a></div><p class="onboarding-hint" data-i18n="connectHint">Sign in securely in a separate window. Your password stays with the Vault portal.</p><div class="connection-message" id="connection-message" role="status" aria-live="polite"></div><a id="auth-fallback" class="button" hidden target="_blank" rel="noopener noreferrer" data-i18n="continueAuth">Continue sign-in ↗</a></div><div class="steps"><article class="step"><div class="step-number">01 / VAULT</div><h2 data-i18n="stepAccount">Connect your account</h2><p data-i18n="stepAccountBody">Sign in to Vault and authorize this local agent.</p></article><article class="step"><div class="step-number">02 / MCP</div><h2 data-i18n="stepClient">Connect your AI clients</h2><p data-i18n="stepClientBody">After signing in, connect Claude, Codex and other clients with one click, right here.</p></article><article class="step"><div class="step-number">03 / MEMORY</div><h2 data-i18n="stepMemory">Build your first memory</h2><p data-i18n="stepMemoryBody">Ask your connected client to save useful context, then follow its activity here.</p></article></div><div class="terminal-fallback"><button class="button" id="verify-connection" type="button" data-i18n="verifyConnection">Already signed in? Check connection</button><p style="margin-top:16px" data-i18n="terminalFallback">Already connected through the CLI? Open a secure local session:</p><code>bunx --bun vault-go@latest local open</code></div></section><div id="dashboard" hidden><div class="welcome"><div><div class="eyebrow" data-i18n="eyebrow">YOUR KNOWLEDGE, CONNECTED</div><h1 data-i18n="title">Your local workspace.</h1><p data-i18n="subtitle">Your Vault account, memories and activity on this machine.</p></div><img src="/logo.png" alt="" width="56" height="56"></div><div id="session-notice" hidden></div><section class="cards" aria-label="Status"><article class="card"><div class="card-label"><span data-i18n="localService">Local service</span><span aria-hidden="true">◈</span></div><div class="card-value" id="local-status">—</div><div class="card-note" id="local-note">localhost:38850</div></article><article class="card"><div class="card-label"><span data-i18n="cloudConnection">Cloud connection</span><span aria-hidden="true">↗</span></div><div class="card-value" id="cloud-status">—</div><div class="card-note" id="cloud-note">—</div></article><article class="card"><div class="card-label"><span data-i18n="recentOperations">Recent operations</span><span aria-hidden="true">⌁</span></div><div class="card-value" id="activity-total">—</div><div class="card-note" data-i18n="operationsNote">Recent MCP request metadata</div></article></section><section class="next-step" id="first-memory"><div><h2 data-i18n="firstTitle">Connect your AI clients</h2><p data-i18n="firstBody">Connect Claude, Codex and other clients here in one click, then ask them to save context. Memories appear after a cloud refresh.</p><div class="mcp-clients" id="mcp-clients" role="group" aria-label="AI clients"></div><p id="mcp-message" role="status" aria-live="polite" class="mcp-message"></p><p data-i18n="rememberPrompt" style="margin-top:10px"></p><p class="mcp-fallback"><span data-i18n="mcpFallback"></span> <code>bunx --bun vault-go@latest install</code></p></div><div class="actions"><a class="link" href="${VAULT_WEB_MCP_URL}" target="_blank" rel="noopener noreferrer" data-i18n="otherClients">Other clients ↗</a><a class="link" href="${VAULT_WEB_MEMORY_URL}" target="_blank" rel="noopener noreferrer" data-i18n="openMemories">Open memories ↗</a></div></section><div class="layout"><section class="panel wide" id="security"><div class="panel-head"><h2 data-i18n="securityTitle">Security on this machine</h2></div><div id="security-facts"></div><div class="actions"><button class="button danger" id="disconnect-device" type="button" data-i18n="disconnectDevice">Disconnect device</button></div></section><section class="panel wide" id="context"><div class="panel-head"><h2 data-i18n="contextEngine">Context engine</h2></div><p data-i18n="engineIntro">Choose how Vault prepares your context.</p><div class="engine-options" id="engine-options" role="radiogroup" aria-label="Context engine"></div><div class="engine-options" id="model-options" role="radiogroup" aria-label="Context model"></div><div class="engine-keys" id="engine-keys"></div><div class="engine-message" id="engine-message" role="status" aria-live="polite"></div><div class="context-editor"><label for="context-text" data-i18n="contextText">Text to prepare</label><textarea id="context-text" maxlength="16000" aria-describedby="context-note"></textarea><p id="context-note" data-i18n="contextNote"></p><div class="actions"><button class="button primary" id="context-preview" type="button" data-i18n="contextPreview"></button><button class="button" id="context-stop" type="button" data-i18n="cancel" hidden></button></div><p id="context-progress" role="status" aria-live="polite"></p></div></section><section class="panel" id="activity"><div class="panel-head"><h2 data-i18n="activity">Activity</h2><span class="count" id="activity-count">0</span></div><input class="search" id="activity-search" type="search" data-placeholder="searchActivity" aria-label="Search activity"><div id="activity-list"></div><div class="pagination" id="activity-pagination"></div></section><section class="panel" id="memories"><div class="panel-head"><h2 data-i18n="memories">Memories</h2><span class="count" id="memory-count">0</span></div><input class="search" id="memory-search" type="search" data-placeholder="searchMemories" aria-label="Search memories"><div id="memory-list"></div><div class="pagination" id="memory-pagination"></div></section><section class="panel wide" id="projects"><div class="panel-head"><h2 data-i18n="projects">Projects</h2><span class="count" id="project-count">0</span></div><input class="search" id="project-search" type="search" data-placeholder="searchProjects" aria-label="Search projects"><div id="project-list"></div><div class="pagination" id="project-pagination"></div></section></div></div><footer class="footer"><span data-i18n="footer">Vault Local · Context with a place to call home.</span><span id="updated" aria-live="off">—</span></footer></main></div><dialog class="detail-dialog" id="detail-dialog" aria-labelledby="detail-title"><div class="panel-head"><h2 id="detail-title"></h2><button class="button" type="button" id="detail-copy" hidden data-i18n="copyResult">Copy result</button><button class="button" type="button" id="detail-close" data-i18n="close">Close</button></div><dl class="detail-fields" id="detail-fields"></dl></dialog><dialog class="detail-dialog" id="logout-dialog" aria-labelledby="logout-title" aria-describedby="logout-description"><h2 id="logout-title" data-i18n="logout"></h2><p id="logout-description" data-i18n="logoutDescription" style="margin-top:16px"></p><div class="actions"><button class="button" type="button" id="logout-cancel" data-i18n="cancel"></button><button class="button primary" type="button" id="logout-confirm" data-i18n="logoutConfirm"></button></div></dialog><dialog class="detail-dialog" id="context-confirm-dialog" aria-labelledby="context-confirm-title" aria-describedby="context-confirm-note"><h2 id="context-confirm-title" data-i18n="contextPreview"></h2><p id="context-confirm-engine" style="margin-top:16px"></p><p id="context-confirm-billing"></p><p id="context-confirm-note" data-i18n="contextConsentNote" style="margin-top:16px"></p><pre id="context-confirm-text" style="white-space:pre-wrap;overflow-wrap:anywhere;max-height:32vh;overflow:auto;font-size:12px;line-height:1.7"></pre><label style="display:flex;gap:10px;align-items:flex-start;font-size:12px"><input id="context-consent" type="checkbox"><span data-i18n="contextConsent"></span></label><div class="actions"><button class="button" id="context-cancel" type="button" data-i18n="cancel"></button><button class="button primary" id="context-generate" type="button" disabled data-i18n="contextGenerate"></button></div></dialog><dialog class="detail-dialog" id="disconnect-dialog" aria-labelledby="disconnect-title" aria-describedby="disconnect-description"><h2 class="danger-title" id="disconnect-title" data-i18n="disconnectDevice"></h2><p id="disconnect-description" data-i18n="disconnectDescription" style="margin-top:16px"></p><p id="disconnect-error" class="error" role="alert"></p><div class="actions"><button class="button" type="button" id="disconnect-cancel" data-i18n="cancel"></button><button class="button danger" type="button" id="disconnect-confirm" data-i18n="disconnectConfirm"></button></div></dialog></body></html>`;
10
10
  }
11
11
  export function dashboardScript() {
12
12
  return `"use strict";
@@ -34,7 +34,8 @@ export function dashboardScript() {
34
34
  en:{engineUpdateCli:'Update the official CLI to a version supporting isolated context generation.',engineClaudeLogin:'Sign in with a Claude subscription using claude auth login.',engineCodexLogin:'Sign in with ChatGPT using codex login.',engineCliUnavailable:'Official CLI or subscription login unavailable. Install or sign in using the provider CLI.',engineVaultUnavailable:'Vault AI Resume is not configured or unavailable for this account.',engineProviderCheck:'Provider readiness is checked when generating.',engineObserverMissing:'Managed observer is not configured.',engineOpenrouterKey:'Configure an OpenRouter API key with vault-go engine key openrouter.',engineGeminiKey:'Configure a Gemini API key with vault-go engine key gemini.',billingVault:'Uses the Vault observer allowance.',billingClaude:'Uses the provider allowance for Claude Code non-interactive runs; limits/additional charges may apply.',billingOpenai:'Uses your ChatGPT subscription through Codex. Provider limits or credits may apply.',billingOpenrouter:'Uses your OpenRouter key and credits. Provider limits may apply.',billingGemini:'Uses your Gemini API key. Google rate limits or billing may apply.',openrouterKey:'OpenRouter API key',geminiKey:'Gemini API key',saveKey:'Save key',keyConfigured:'Key configured on this machine',keyMissing:'Paste the key and save',keySaved:'Key stored with private file permissions.',keyFailed:'Could not store the key. Try again.',keyOs:'Operating system keychain',keyFile:'Local file protected by permissions',keyEphemeral:'In memory for this run only',keyUnavailable:'Unavailable'},
35
35
  es:{engineUpdateCli:'Actualiza el CLI oficial a una versión compatible con la generación aislada de contexto.',engineClaudeLogin:'Inicia sesión con tu suscripción Claude usando claude auth login.',engineCodexLogin:'Inicia sesión con tu cuenta ChatGPT usando codex login.',engineCliUnavailable:'CLI oficial o sesión de la suscripción no disponible. Instala el CLI del proveedor o inicia sesión mediante él.',engineVaultUnavailable:'Vault AI Resume no está configurado o no está disponible para esta cuenta.',engineProviderCheck:'La disponibilidad del proveedor se comprobará al generar.',engineObserverMissing:'El observador administrado no está configurado.',engineOpenrouterKey:'Configura una clave OpenRouter con vault-go engine key openrouter.',engineGeminiKey:'Configura una clave Gemini con vault-go engine key gemini.',billingVault:'Usa la cuota del observador de Vault.',billingClaude:'Usa la cuota del proveedor para ejecuciones no interactivas de Claude Code; pueden aplicarse límites y cargos adicionales.',billingOpenai:'Usa tu suscripción ChatGPT mediante Codex. Pueden aplicarse límites o créditos de uso del proveedor.',billingOpenrouter:'Usa tu clave y créditos de OpenRouter. Pueden aplicarse límites del proveedor.',billingGemini:'Usa tu clave de la API Gemini. Pueden aplicarse límites o facturación de Google.',openrouterKey:'Clave OpenRouter',geminiKey:'Clave Gemini',saveKey:'Guardar clave',keyConfigured:'Clave configurada en este equipo',keyMissing:'Pega la clave y guarda',keySaved:'Clave guardada con permisos privados.',keyFailed:'No se pudo guardar la clave. Inténtalo de nuevo.',keyOs:'Llavero del sistema operativo',keyFile:'Archivo local protegido por permisos',keyEphemeral:'Solo en memoria durante esta ejecución',keyUnavailable:'No disponible'}
36
36
  };
37
- for (const language of Object.keys(strings)) Object.assign(strings[language],onboardingCopy[language],detailCopy[language],engineCopy[language]);
37
+ const disclosureCopy={en:{showMore:'Show more',showLess:'Show less'},pt:{showMore:'Mostrar mais',showLess:'Mostrar menos'},es:{showMore:'Mostrar más',showLess:'Mostrar menos'}};
38
+ for (const language of Object.keys(strings)) Object.assign(strings[language],onboardingCopy[language],detailCopy[language],engineCopy[language],disclosureCopy[language]);
38
39
  let locale = navigator.language.slice(0,2);
39
40
  try { locale = localStorage.getItem('vault-local-locale') || locale; } catch {}
40
41
  if (!strings[locale]) locale = 'en';
@@ -54,6 +55,17 @@ export function dashboardScript() {
54
55
  let mcpClients = null, mcpChecked = 0, mcpMessage = '';
55
56
  const mcpBusy = {};
56
57
  let contextPreview=null, contextJob=null, contextTimer=null, contextStarted=0, contextMessage='', contextResult=null, contextStarting=false;
58
+ // Machine-driven sessions produce multi-thousand-character prompts and payloads.
59
+ // Past these bounds a value is clamped behind a Show more disclosure instead of
60
+ // pushing every other field (or row) off screen.
61
+ const CLAMP_LINES=8, CLAMP_MIN_CHARS=600;
62
+ const isLong=value=>value.split('\\n').length>CLAMP_LINES||value.length>CLAMP_MIN_CHARS;
63
+ function disclosure(target) {
64
+ target.classList.add('clamped');
65
+ const toggle=node('button','show-more',t('showMore'));toggle.type='button';toggle.setAttribute('aria-expanded','false');
66
+ toggle.addEventListener('click',()=>{const expanded=target.classList.toggle('clamped')===false;toggle.setAttribute('aria-expanded',String(expanded));toggle.textContent=t(expanded?'showLess':'showMore');});
67
+ return toggle;
68
+ }
57
69
  function showDetails(title,item,trigger) {
58
70
  contextResult=null;el('detail-copy').hidden=true;
59
71
  detailFocus=trigger.id; text('detail-title',title);
@@ -61,7 +73,9 @@ export function dashboardScript() {
61
73
  const entries=Object.entries(object(item));
62
74
  for(const [key,value] of entries.length ? entries : [[t('details'),item]]) {
63
75
  const term=node('dt','',key),definition=node('dd','');
64
- definition.textContent=typeof value==='string' ? value : JSON.stringify(value,null,2);
76
+ const content=node('div','',typeof value==='string' ? value : JSON.stringify(value,null,2));
77
+ definition.append(content);
78
+ if(isLong(content.textContent))definition.append(disclosure(content));
65
79
  fields.append(term,definition);
66
80
  }
67
81
  el('detail-dialog').showModal(); el('detail-close').focus();
@@ -101,6 +115,7 @@ export function dashboardScript() {
101
115
  const wrap=node('div','row'),body=node('div','row-body'),title=node('button','row-title row-open',row.title.slice(0,600));
102
116
  title.type='button';title.addEventListener('click',()=>showDetails(row.title,row.item,button));
103
117
  body.append(title);
118
+ if(row.title.length>200)title.classList.add('clamped');
104
119
  if(kind==='activity') {
105
120
  const item=object(row.item);body.append(node('div','row-meta',(item.ok===true?t('success'):t('failed'))+(typeof item.durationMs==='number'?' · '+Math.round(item.durationMs)+' ms':'')));
106
121
  wrap.append(node('span','row-marker'+(item.ok===true?'':' failed')));
@@ -3,6 +3,7 @@ import { homedir, platform } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { detectMcpClients, installMcpClient, MCP_CLIENTS, vaultDesktopDotMcpPath, vaultDesktopMcpPath, } from "./installer.js";
6
+ import { hasYamlEntry } from "./yaml-entry.js";
6
7
  export const MCP_CLIENT_LABELS = {
7
8
  codex: "Codex",
8
9
  claude: "Claude Code",
@@ -16,6 +17,9 @@ export const MCP_CLIENT_LABELS = {
16
17
  grok: "Grok",
17
18
  agy: "Antigravity",
18
19
  gemini: "Gemini CLI",
20
+ warp: "Warp",
21
+ goose: "Goose",
22
+ hermes: "Hermes",
19
23
  "vault-desktop": "Vault Desktop",
20
24
  };
21
25
  function vscodeMcpPaths(home) {
@@ -65,6 +69,12 @@ function configLocations(client, home, cwd) {
65
69
  ];
66
70
  case "gemini":
67
71
  return [[join(home, ".gemini", "settings.json"), "mcpServers"]];
72
+ case "warp":
73
+ return [[join(home, ".warp", ".mcp.json"), "mcpServers"]];
74
+ case "goose":
75
+ return [[join(home, ".config", "goose", "config.yaml"), "yaml:extensions"]];
76
+ case "hermes":
77
+ return [[join(home, ".hermes", "config.yaml"), "yaml:mcp_servers"]];
68
78
  case "vault-desktop":
69
79
  return [
70
80
  [vaultDesktopMcpPath(home), "mcpServers"],
@@ -76,7 +86,7 @@ function serverConfiguredInJson(path, rootKey) {
76
86
  if (!existsSync(path))
77
87
  return false;
78
88
  try {
79
- const parsed = JSON.parse(readFileSync(path, "utf8"));
89
+ const parsed = JSON.parse(readFileSync(path, "utf8").replace(/^\uFEFF/, ""));
80
90
  const servers = parsed && typeof parsed === "object" ? parsed[rootKey] : undefined;
81
91
  return Boolean(servers && typeof servers === "object" && "vault-go" in servers);
82
92
  }
@@ -84,6 +94,16 @@ function serverConfiguredInJson(path, rootKey) {
84
94
  return false;
85
95
  }
86
96
  }
97
+ function serverConfiguredInYaml(path, topKey) {
98
+ if (!existsSync(path))
99
+ return false;
100
+ try {
101
+ return hasYamlEntry(readFileSync(path, "utf8"), topKey, "vault-go");
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
87
107
  function serverConfiguredInToml(path) {
88
108
  if (!existsSync(path))
89
109
  return false;
@@ -122,7 +142,12 @@ export function mcpClientConnected(client, home = homedir(), cwd = process.cwd()
122
142
  if (client === "codex" || client === "claude")
123
143
  return clientCliInstalled(client, home);
124
144
  for (const [path, rootKey] of configLocations(client, home, cwd)) {
125
- if (rootKey === "toml" ? serverConfiguredInToml(path) : serverConfiguredInJson(path, rootKey)) {
145
+ const configured = rootKey === "toml"
146
+ ? serverConfiguredInToml(path)
147
+ : rootKey.startsWith("yaml:")
148
+ ? serverConfiguredInYaml(path, rootKey.slice(5))
149
+ : serverConfiguredInJson(path, rootKey);
150
+ if (configured) {
126
151
  return true;
127
152
  }
128
153
  }
@@ -5,6 +5,7 @@ export declare function jsonResult(value: unknown): {
5
5
  }[];
6
6
  structuredContent: Record<string, unknown>;
7
7
  };
8
+ export declare function errorMessage(error: unknown): string;
8
9
  export declare function run(operation: () => Promise<unknown>): Promise<{
9
10
  content: {
10
11
  type: "text";
@@ -3,13 +3,30 @@ export function jsonResult(value) {
3
3
  ? value
4
4
  : { value };
5
5
  return {
6
- content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
6
+ // Compact: indentation only spends the client's context window; structuredContent carries the same value.
7
+ content: [{ type: 'text', text: JSON.stringify(value) ?? 'null' }],
7
8
  structuredContent,
8
9
  };
9
10
  }
10
- function errorResult(error) {
11
+ // What the agent can do next for each cloud status, so a failure is actionable instead of a bare code.
12
+ const STATUS_HINTS = {
13
+ 401: 'A sessão do Vault expirou ou não existe. Peça ao usuário para rodar: bunx --bun vault-go@latest login',
14
+ 403: 'Esta conta não tem acesso a esse recurso.',
15
+ 404: 'Não encontrado nesta conta. Confira o ID ou use scope "account".',
16
+ 409: 'O recurso mudou ou ainda não está pronto; leia o estado atual e tente de novo.',
17
+ 413: 'Entrada grande demais; reduza o texto, a lista ou o limite.',
18
+ 429: 'Limite de uso atingido; tente novamente em instantes.',
19
+ };
20
+ export function errorMessage(error) {
11
21
  const message = error instanceof Error ? error.message : 'Erro inesperado.';
12
- return { content: [{ type: 'text', text: message }], isError: true };
22
+ const status = error && typeof error === 'object' ? error.status : undefined;
23
+ if (typeof status !== 'number')
24
+ return message;
25
+ const hint = STATUS_HINTS[status] ?? (status >= 500 ? 'O Vault está temporariamente indisponível; tente novamente em instantes.' : '');
26
+ return hint && !message.includes(hint) ? `${message} — ${hint}` : message;
27
+ }
28
+ function errorResult(error) {
29
+ return { content: [{ type: 'text', text: errorMessage(error) }], isError: true };
13
30
  }
14
31
  export async function run(operation) {
15
32
  try {
@@ -1,5 +1,7 @@
1
1
  import { findWorkspaceProject } from './hooks.js';
2
2
  const CACHE_MS = 60_000;
3
+ const FILTER_KEYS = new Set(['kind', 'memoryType', 'platformSource', 'type']);
4
+ const ALL = /^\s*(all|any|\*|todos?|todas?)\s*$/i;
3
5
  /**
4
6
  * Why: MCP reads used to span every project of the account, so an agent in one
5
7
  * repository received memories from unrelated clients. Reads now default to the
@@ -8,7 +10,9 @@ const CACHE_MS = 60_000;
8
10
  export function createProjectScope(cloud, cwd = () => process.cwd()) {
9
11
  let cached;
10
12
  return async (input) => {
11
- const { scope = 'project', ...rest } = input;
13
+ const { scope = 'project', ...given } = input;
14
+ // Agents often send "all" to mean "no filter"; as a literal value it matches no memory.
15
+ const rest = Object.fromEntries(Object.entries(given).filter(([key, value]) => !(FILTER_KEYS.has(key) && typeof value === 'string' && ALL.test(value))));
12
16
  if (rest.projectId || scope === 'account')
13
17
  return rest;
14
18
  const workspace = cwd();
@@ -0,0 +1,25 @@
1
+ export declare const FILE_READ_GATE_MIN_BYTES: number;
2
+ export interface ReadGateRequest {
3
+ adapter: string;
4
+ toolName: string;
5
+ toolInput: Record<string, unknown>;
6
+ agentId: string;
7
+ workspace: string;
8
+ enabled: boolean;
9
+ }
10
+ /** Whether the file has more lines than `count`, reading one chunk at a time. Null when unreadable. */
11
+ export declare function fileHasMoreLinesThan(path: string, count: number): boolean | null;
12
+ /**
13
+ * The file a Read would gate, or undefined when any cheap condition fails:
14
+ * Claude Code main session (never subagents or other clients), gate enabled, a
15
+ * single file_path Read starting at line 1, a code file inside the workspace
16
+ * between 32 KB and the smart-tool limit, and a Read that returns the whole file.
17
+ */
18
+ export declare function readGateCandidate(request: ReadGateRequest): {
19
+ path: string;
20
+ mtimeMs: number;
21
+ } | undefined;
22
+ /** Newest update time among the memories, in epoch milliseconds; 0 when none is known. */
23
+ export declare function newestMemoryEpoch(value: unknown): number;
24
+ /** The deny reason shown to Claude. Memory titles are history, never the file's current contents. */
25
+ export declare function readGateReason(requestedPath: string, memoryIds: readonly string[], context: string): string;
@@ -0,0 +1,121 @@
1
+ import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
+ import { extname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ // File Read Gate: a whole-file Read of a large code file that already has
4
+ // memory history is denied with that history, and Claude is routed to the
5
+ // structural tools or a targeted Read. Below this size reading the file costs
6
+ // about what the detour would, so small files are always read normally.
7
+ export const FILE_READ_GATE_MIN_BYTES = 32 * 1024;
8
+ // The smart tools refuse files above this, so the gate never routes Claude to them.
9
+ const SMART_TOOL_MAX_BYTES = 1_048_576;
10
+ const LINE_COUNT_CHUNK_BYTES = 64 * 1024;
11
+ // Code the smart tools outline by symbol. Markup, data and prose stay readable.
12
+ const GATED_EXTENSIONS = new Set([
13
+ ".js", ".jsx", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx",
14
+ ".py", ".pyw", ".go", ".rs", ".rb", ".java", ".c", ".h", ".cpp", ".cc", ".cxx", ".hpp", ".hh",
15
+ ".kt", ".kts", ".swift", ".php", ".lua", ".scala", ".sc", ".hs", ".zig",
16
+ ]);
17
+ function windowValue(value) {
18
+ const numeric = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
19
+ return typeof numeric === "number" && Number.isFinite(numeric) && numeric >= 0 ? numeric : undefined;
20
+ }
21
+ /** Whether the file has more lines than `count`, reading one chunk at a time. Null when unreadable. */
22
+ export function fileHasMoreLinesThan(path, count) {
23
+ let descriptor;
24
+ try {
25
+ descriptor = openSync(path, "r");
26
+ const chunk = Buffer.allocUnsafe(LINE_COUNT_CHUNK_BYTES);
27
+ let newlines = 0, endsWithNewline = false;
28
+ for (;;) {
29
+ const read = readSync(descriptor, chunk, 0, chunk.length, null);
30
+ if (read === 0)
31
+ break;
32
+ const bytes = chunk.subarray(0, read);
33
+ for (let index = bytes.indexOf(0x0a); index !== -1; index = bytes.indexOf(0x0a, index + 1)) {
34
+ if (++newlines > count)
35
+ return true;
36
+ }
37
+ endsWithNewline = bytes[read - 1] === 0x0a;
38
+ }
39
+ return (endsWithNewline ? newlines : newlines + 1) > count;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ finally {
45
+ if (descriptor !== undefined)
46
+ closeSync(descriptor);
47
+ }
48
+ }
49
+ /**
50
+ * The file a Read would gate, or undefined when any cheap condition fails:
51
+ * Claude Code main session (never subagents or other clients), gate enabled, a
52
+ * single file_path Read starting at line 1, a code file inside the workspace
53
+ * between 32 KB and the smart-tool limit, and a Read that returns the whole file.
54
+ */
55
+ export function readGateCandidate(request) {
56
+ if (!request.enabled || request.adapter !== "claude" || request.agentId || request.toolName !== "Read")
57
+ return undefined;
58
+ const filePath = request.toolInput.file_path;
59
+ if (typeof filePath !== "string" || !filePath.trim())
60
+ return undefined;
61
+ if ((windowValue(request.toolInput.offset) ?? 0) > 1)
62
+ return undefined;
63
+ if (!GATED_EXTENSIONS.has(extname(filePath).toLowerCase()))
64
+ return undefined;
65
+ let path, root, size, mtimeMs;
66
+ try {
67
+ root = realpathSync(request.workspace);
68
+ path = realpathSync(isAbsolute(filePath) ? filePath : resolve(request.workspace, filePath));
69
+ const stats = statSync(path);
70
+ if (!stats.isFile())
71
+ return undefined;
72
+ size = stats.size;
73
+ mtimeMs = stats.mtimeMs;
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ // Same containment the smart tools apply: a symlink leading outside the workspace is outside.
79
+ const inside = relative(root, path);
80
+ if (!inside || inside.startsWith(".." + sep) || inside === ".." || isAbsolute(inside))
81
+ return undefined;
82
+ if (size < FILE_READ_GATE_MIN_BYTES || size > SMART_TOOL_MAX_BYTES)
83
+ return undefined;
84
+ const limit = windowValue(request.toolInput.limit);
85
+ if (limit !== undefined && fileHasMoreLinesThan(path, limit) !== false)
86
+ return undefined;
87
+ return { path, mtimeMs };
88
+ }
89
+ /** Newest update time among the memories, in epoch milliseconds; 0 when none is known. */
90
+ export function newestMemoryEpoch(value) {
91
+ const record = value && typeof value === "object" ? value : {};
92
+ const rows = Array.isArray(value) ? value : Array.isArray(record.memories) ? record.memories : Array.isArray(record.value) ? record.value : [];
93
+ let newest = 0;
94
+ for (const row of rows) {
95
+ const item = row && typeof row === "object" ? row : {};
96
+ for (const key of ["updatedAtEpoch", "createdAtEpoch"]) {
97
+ if (typeof item[key] === "number" && Number.isFinite(item[key]))
98
+ newest = Math.max(newest, item[key]);
99
+ }
100
+ for (const key of ["updatedAt", "updated_at", "createdAt", "created_at"]) {
101
+ const parsed = typeof item[key] === "string" ? Date.parse(item[key]) : NaN;
102
+ if (Number.isFinite(parsed))
103
+ newest = Math.max(newest, parsed);
104
+ }
105
+ }
106
+ return newest;
107
+ }
108
+ /** The deny reason shown to Claude. Memory titles are history, never the file's current contents. */
109
+ export function readGateReason(requestedPath, memoryIds, context) {
110
+ const shown = requestedPath;
111
+ const safe = JSON.stringify(requestedPath);
112
+ return [
113
+ `Full-file Read blocked by Vault Memory: ${shown} is a large file with prior memories (listed below). Get what you need without reading the whole file:`,
114
+ `- Current code: call vault_go_smart_outline(filePath=${safe}) for its symbols and line numbers, then vault_go_smart_unfold(filePath=${safe}, symbolName="<name>") for the ones you need (MCP tools mcp__vault-go__vault_go_smart_outline / __vault_go_smart_unfold; load them with ToolSearch if they are deferred).`,
115
+ `- Past work: call vault_go_observations(ids=${JSON.stringify(memoryIds.slice(0, 20))}) for the memories that matter.`,
116
+ `- Exact lines, e.g. before an Edit: call Read on this file again with offset and limit. Partial reads are allowed and satisfy Edit's read requirement.`,
117
+ `The memories below are history from earlier sessions, not the file's current contents.`,
118
+ "",
119
+ context,
120
+ ].join("\n");
121
+ }
package/dist/server.js CHANGED
@@ -18,6 +18,14 @@ import { estimateAgentCost } from './agent-cost.js';
18
18
  import { flattenArchive, groupArchive, recordsFromExport } from './memory-archive.js';
19
19
  import { CONTEXT_ENGINE_IDS, generateContext, listContextEngines, parseEngineModel, selectContextEngine, selectEngineModel, selectedContextEngine, selectedEngineModel, } from './context-engines.js';
20
20
  import packageJson from '../package.json' with { type: 'json' };
21
+ // Sent once at initialize; clients put it in the model's context, so it stays short.
22
+ const SERVER_INSTRUCTIONS = [
23
+ 'Vault Memory: persistent memory of past sessions, scoped to the project of the current directory (scope "account" widens a read to every project).',
24
+ '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.',
25
+ '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.',
26
+ 'Save durable decisions, discoveries and fixes with vault_go_remember. Never store secrets; text inside <private> tags is never stored.',
27
+ 'When a tool fails with an expired session, ask the user to run: bunx --bun vault-go@latest login',
28
+ ].join('\n');
21
29
  export const VERSION = packageJson.version;
22
30
  const uuid = z.string().uuid();
23
31
  const knowledgeFilters = z.object({
@@ -54,7 +62,7 @@ async function managedEngineStatus(cloud) {
54
62
  }
55
63
  }
56
64
  export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudClient(home)) {
57
- const server = new McpServer({ name: 'vault-go', version: VERSION });
65
+ const server = new McpServer({ name: 'vault-go', version: VERSION }, { instructions: SERVER_INSTRUCTIONS });
58
66
  registerVaultModes(server);
59
67
  registerCrystalTools(server, home);
60
68
  registerKnowledgeAgentTools(server, home, cloud);
@@ -14,8 +14,8 @@ export function registerTranscriptTools(server, home, cloud) {
14
14
  server.registerTool('vault_go_transcripts_poll', { description: 'Processa um lote limitado dos watchers já consentidos. Retomada e idempotência persistem entre reinícios; não ativa captura nem muda o motor.', inputSchema: {}, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true } }, () => run(() => pollTranscripts(home, cloud)));
15
15
  server.registerTool('vault_go_context_chain', { description: 'Lista vínculos locais entre workspaces para contexto multi-project do SessionStart. Vínculos são específicos desta conta.', inputSchema: {}, annotations: { readOnlyHint: true, openWorldHint: false } }, () => run(async () => contextChainStatus(home)));
16
16
  server.registerTool('vault_go_context_chain_configure', {
17
- description: 'Define até quatro raízes relacionadas e/ou o checkout pai de um worktree. Exige consentimento; só consulta projetos já existentes e acessíveis na conta, dentro do orçamento total de contexto. Lista vazia remove os vínculos.',
18
- inputSchema: { workspace: path, roots: z.array(path).max(4), consent: z.boolean().default(false), includeWorktreeParent: z.boolean().default(false) },
17
+ description: 'Define até quatro raízes relacionadas, o checkout pai de um worktree e/ou allProjects (todos os projetos não privados da conta, até cinco no total). Exige consentimento; só consulta projetos já existentes e acessíveis na conta, dentro do orçamento total de contexto. Lista vazia remove os vínculos.',
18
+ inputSchema: { workspace: path, roots: z.array(path).max(4), consent: z.boolean().default(false), includeWorktreeParent: z.boolean().default(false), allProjects: z.boolean().default(false) },
19
19
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
20
20
  }, input => run(async () => configureContextChain(home, input)));
21
21
  }
@@ -0,0 +1,6 @@
1
+ /** Scalars and string lists rendered as JSON, which YAML reads as the same values. */
2
+ export type YamlEntryValue = string | number | boolean | readonly string[] | Readonly<Record<string, string>>;
3
+ /** `text` with `<topKey>.<name>` set to `fields`, replacing an entry of that name. */
4
+ export declare function upsertYamlEntry(text: string, topKey: string, name: string, fields: Record<string, YamlEntryValue>): string;
5
+ /** Whether `<topKey>.<name>` exists as a block entry. */
6
+ export declare function hasYamlEntry(text: string, topKey: string, name: string): boolean;
@@ -0,0 +1,113 @@
1
+ // Line edits for one named entry inside a top-level YAML mapping, such as
2
+ // Goose's `extensions.vault-go` or Hermes's `mcp_servers.vault-go`. Users keep
3
+ // comments in these files, so they are edited line by line instead of parsed
4
+ // and re-dumped: every line vault-go does not own stays byte for byte. A shape
5
+ // these edits cannot follow throws and the file is left untouched.
6
+ const TOP_LEVEL_KEY = /^(["']?)([A-Za-z_][\w-]*)\1(\s*):(.*)$/;
7
+ function isContent(line) {
8
+ const trimmed = line.trim();
9
+ return trimmed !== "" && !trimmed.startsWith("#");
10
+ }
11
+ function indentOf(line) {
12
+ return line.length - line.trimStart().length;
13
+ }
14
+ function stripComment(value) {
15
+ const match = /(^|\s)#/.exec(value);
16
+ return (match ? value.slice(0, match.index) : value).trim();
17
+ }
18
+ function render(fields, indent) {
19
+ const lines = [];
20
+ for (const [key, value] of Object.entries(fields)) {
21
+ if (Array.isArray(value)) {
22
+ lines.push(`${indent}${key}:`, ...value.map(item => `${indent} - ${JSON.stringify(item)}`));
23
+ }
24
+ else if (value && typeof value === "object") {
25
+ const entries = Object.entries(value);
26
+ if (!entries.length)
27
+ lines.push(`${indent}${key}: {}`);
28
+ else
29
+ lines.push(`${indent}${key}:`, ...entries.map(([name, item]) => `${indent} ${name}: ${JSON.stringify(item)}`));
30
+ }
31
+ else {
32
+ lines.push(`${indent}${key}: ${typeof value === "string" ? JSON.stringify(value) : String(value)}`);
33
+ }
34
+ }
35
+ return lines;
36
+ }
37
+ /** `text` with `<topKey>.<name>` set to `fields`, replacing an entry of that name. */
38
+ export function upsertYamlEntry(text, topKey, name, fields) {
39
+ // Windows editors save a byte order mark; it is not part of the first key and is kept.
40
+ const bom = text.startsWith("") ? "" : "";
41
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
42
+ const lines = text.slice(bom.length).split(/\r?\n/);
43
+ if (lines.length && lines[lines.length - 1] === "")
44
+ lines.pop();
45
+ const firstContent = lines.find(line => isContent(line) && !/^(---(\s|$)|%)/.test(line));
46
+ if (firstContent !== undefined && indentOf(firstContent) > 0)
47
+ throw new Error("O nível superior do YAML está indentado; o arquivo não foi alterado.");
48
+ const headers = lines.flatMap((line, index) => TOP_LEVEL_KEY.exec(line)?.[2] === topKey ? [index] : []);
49
+ if (headers.length > 1)
50
+ throw new Error(`Chave ${topKey} duplicada no YAML; o arquivo não foi alterado.`);
51
+ if (!headers.length) {
52
+ while (lines.length && !lines[lines.length - 1].trim())
53
+ lines.pop();
54
+ const documentEnd = lines.findIndex(line => /^\.\.\.(\s|$)/.test(line));
55
+ const block = [`${topKey}:`, ` ${name}:`, ...render(fields, " ")];
56
+ if (documentEnd === -1)
57
+ lines.push(...block);
58
+ else
59
+ lines.splice(documentEnd, 0, ...block);
60
+ return bom + lines.join(eol) + eol;
61
+ }
62
+ const header = headers[0];
63
+ const match = TOP_LEVEL_KEY.exec(lines[header]);
64
+ const inline = stripComment(match[4]);
65
+ if (inline !== "" && inline !== "{}" && inline !== "null" && inline !== "~") {
66
+ throw new Error(`${topKey} usa valor inline no YAML; escreva como mapeamento em bloco. O arquivo não foi alterado.`);
67
+ }
68
+ if (inline)
69
+ lines[header] = lines[header].replace(/:[ \t]*(?:\{\}|null|~)/, ":");
70
+ let end = header + 1;
71
+ while (end < lines.length && !(isContent(lines[end]) && indentOf(lines[end]) === 0))
72
+ end++;
73
+ while (end > header + 1 && !isContent(lines[end - 1]))
74
+ end--;
75
+ const firstChild = lines.slice(header + 1, end).find(isContent);
76
+ if (firstChild?.startsWith("\t"))
77
+ throw new Error(`${topKey} está indentado com tabs; o arquivo não foi alterado.`);
78
+ const indent = " ".repeat(firstChild ? indentOf(firstChild) : 2);
79
+ const entry = [`${indent}${name}:`, ...render(fields, indent + " ")];
80
+ const existing = lines.findIndex((line, index) => index > header && index < end && indentOf(line) === indent.length && new RegExp(`^(["']?)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\1\\s*:`).test(line.trimStart()));
81
+ if (existing === -1) {
82
+ lines.splice(header + 1, 0, ...entry);
83
+ }
84
+ else {
85
+ let entryEnd = existing + 1;
86
+ while (entryEnd < end && !(isContent(lines[entryEnd]) && indentOf(lines[entryEnd]) <= indent.length))
87
+ entryEnd++;
88
+ while (entryEnd > existing + 1 && !isContent(lines[entryEnd - 1]))
89
+ entryEnd--;
90
+ lines.splice(existing, entryEnd - existing, ...entry);
91
+ }
92
+ return bom + lines.join(eol) + eol;
93
+ }
94
+ /** Whether `<topKey>.<name>` exists as a block entry. */
95
+ export function hasYamlEntry(text, topKey, name) {
96
+ const lines = text.replace(/^/, "").split(/\r?\n/);
97
+ const header = lines.findIndex(line => TOP_LEVEL_KEY.exec(line)?.[2] === topKey);
98
+ if (header === -1)
99
+ return false;
100
+ let childIndent = -1;
101
+ for (let index = header + 1; index < lines.length; index++) {
102
+ const line = lines[index];
103
+ if (!isContent(line))
104
+ continue;
105
+ if (indentOf(line) === 0)
106
+ break;
107
+ if (childIndent === -1)
108
+ childIndent = indentOf(line);
109
+ if (indentOf(line) === childIndent && new RegExp(`^(["']?)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\1\\s*:`).test(line.trimStart()))
110
+ return true;
111
+ }
112
+ return false;
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.31.2",
3
+ "version": "0.32.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": {
@@ -139,6 +139,12 @@
139
139
  "dist/context-chain.d.ts",
140
140
  "dist/capture-privacy.js",
141
141
  "dist/capture-privacy.d.ts",
142
+ "dist/client-prompts.js",
143
+ "dist/read-gate.js",
144
+ "dist/yaml-entry.js",
145
+ "dist/yaml-entry.d.ts",
146
+ "dist/read-gate.d.ts",
147
+ "dist/client-prompts.d.ts",
142
148
  "dist/deployment-verification.js",
143
149
  "dist/deployment-verification.d.ts",
144
150
  "dist/grok-awareness.js",