vault-go 0.31.1 → 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 +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/crystal-tools.js +1 -1
- 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 +18 -3
- 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 +9 -1
- 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 +8 -2
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/crystal-tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import { lstatSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
const unavailable = '
|
|
5
|
+
const unavailable = 'Para usar o Crystal local neste computador, abra o Vault Desktop e ative a integração Vault MCP no Crystal. Em uma sessão nativa do Vault Cloud, use vault_crystal_status, vault_crystal_list, vault_crystal_read e vault_crystal_search; a conexão local do Vault Go não alcança outro computador.';
|
|
6
6
|
const invalidConnection = 'Conexão local do Crystal inválida. Reative a integração no Vault Desktop.';
|
|
7
7
|
const MAX_RESPONSE = 32 * 1024 * 1024;
|
|
8
8
|
export const CRYSTAL_LOCKED = 'CRYSTAL_LOCKED';
|
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]) => {
|
package/dist/local-dashboard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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')));
|
package/dist/mcp-clients.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/mcp-result.d.ts
CHANGED
|
@@ -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";
|
package/dist/mcp-result.js
CHANGED
|
@@ -3,13 +3,30 @@ export function jsonResult(value) {
|
|
|
3
3
|
? value
|
|
4
4
|
: { value };
|
|
5
5
|
return {
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/project-scope.js
CHANGED
|
@@ -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', ...
|
|
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);
|
package/dist/transcript-tools.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|
|
@@ -217,7 +223,7 @@
|
|
|
217
223
|
"@ast-grep/lang-toml": "0.0.9",
|
|
218
224
|
"@ast-grep/lang-yaml": "0.0.6",
|
|
219
225
|
"@ast-grep/napi": "0.45.3",
|
|
220
|
-
"@modelcontextprotocol/sdk": "1.
|
|
226
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
221
227
|
"@tree-sitter-grammars/tree-sitter-zig": "1.1.2",
|
|
222
228
|
"zod": "3.25.76"
|
|
223
229
|
},
|