vault-go 0.31.2 → 0.33.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 +9 -1
- package/dist/capture-privacy.d.ts +2 -0
- package/dist/capture-privacy.js +22 -2
- package/dist/cli.js +2 -2
- package/dist/client-prompts.d.ts +2 -0
- package/dist/client-prompts.js +34 -0
- package/dist/context-chain.d.ts +2 -0
- package/dist/context-chain.js +12 -4
- package/dist/context-engines.d.ts +6 -0
- package/dist/context-engines.js +55 -3
- package/dist/hook-install.js +5 -2
- package/dist/hook-queue.d.ts +2 -2
- package/dist/hook-queue.js +4 -2
- package/dist/hooks.js +40 -3
- package/dist/installer.d.ts +1 -1
- package/dist/installer.js +44 -2
- package/dist/local-dashboard.js +62 -8
- package/dist/mcp-clients.js +27 -2
- package/dist/mcp-result.d.ts +1 -0
- package/dist/mcp-result.js +20 -3
- package/dist/project-scope.js +5 -1
- package/dist/read-gate.d.ts +25 -0
- package/dist/read-gate.js +121 -0
- package/dist/server.js +22 -2
- package/dist/transcript-tools.js +2 -2
- package/dist/yaml-entry.d.ts +6 -0
- package/dist/yaml-entry.js +113 -0
- package/package.json +7 -1
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.
|
package/dist/capture-privacy.js
CHANGED
|
@@ -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(
|
|
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,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
|
+
}
|
package/dist/context-chain.d.ts
CHANGED
|
@@ -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;
|
package/dist/context-chain.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/context-engines.js
CHANGED
|
@@ -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(
|
|
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
|
+
}
|
package/dist/hook-install.js
CHANGED
|
@@ -9,7 +9,8 @@ function readJsonObject(path) {
|
|
|
9
9
|
return {};
|
|
10
10
|
let parsed;
|
|
11
11
|
try {
|
|
12
|
-
|
|
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
|
|
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)] },
|
package/dist/hook-queue.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VaultCloudClient } from './cloud.js';
|
|
2
|
-
import {
|
|
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
|
|
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>;
|
package/dist/hook-queue.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
280
|
-
const
|
|
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
|
}
|
package/dist/installer.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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]) => {
|