vault-go 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -180,6 +180,8 @@ Engines and updates: `vault_go_engines`, `vault_go_engine_select`, `vault_go_gen
180
180
 
181
181
  Graph and local code: `vault_go_graph`, `vault_go_graph_link`, `vault_go_graph_link_delete`, `vault_go_graph_presets`, `vault_go_graph_preset_save`, `vault_go_smart_outline`, `vault_go_smart_unfold`, `vault_go_smart_search`, `vault_go_smart_languages`.
182
182
 
183
+ Computer Use by Vault: `vault_go_computer_status`, `vault_go_computer_permissions`, `vault_go_computer_list_apps`, `vault_go_computer_list_windows`, `vault_go_computer_get_app_state`, `vault_go_computer_query`, `vault_go_computer_click`, `vault_go_computer_perform_secondary_action`, `vault_go_computer_scroll`, `vault_go_computer_drag`, `vault_go_computer_type_text`, `vault_go_computer_paste_text`, `vault_go_computer_set_value`, `vault_go_computer_press_key`, `vault_go_computer_hotkey`. They drive local apps through the Vault Desktop native helper (installed and running), so every MCP client shares its permissions; on macOS grant Accessibility and Screen Recording to "Vault Desktop Computer Use" once. `get_app_state` returns the accessibility tree plus the window screenshot as an MCP image; `query` finds controls by text or role without a screenshot. Element indexes share one session per server. Typed text and values travel over stdin, never the process arguments. Set `VAULT_DESKTOP_CLI` to point at a custom CLI path, or `VAULT_GO_COMPUTER_USE=off` to hide the tools. The tool surface follows [open-computer-use](https://github.com/iFurySt/open-computer-use) (MIT).
184
+
183
185
  Workflows: `vault_go_workflow`, `vault_go_modes`, `vault_go_mode`, plus named prompts such as `vault_go_babysit`. Loading a mode gives the agent instructions; it does not start a background job.
184
186
 
185
187
  Optional local integrations (explicit consent): Telegram alerts, custom mode taxonomies, Claude/Codex transcript watchers, multi-project context chains, and Grok awareness logs. See `vault_go_capabilities` for what is configured on this machine.
@@ -110,6 +110,11 @@ export declare const CAPABILITIES: {
110
110
  readonly status: "available";
111
111
  readonly tools: readonly ["vault_go_modes", "vault_go_mode", "vault_go_agent_cost_report"];
112
112
  readonly difference: "Setup writes managed SKILL.md files for Claude, Grok and shared agent skill directories. mem-search uses vault_go_search_index → vault_go_timeline → vault_go_observations → vault_go_tool_uses. babysit is a session loop with gh, not a Vault background daemon. agent-cost-report prices local JSONL usage with a published list rate and reports missing usage as unavailable.";
113
+ }, {
114
+ readonly upstream: readonly ["open-computer-use: list_apps, get_app_state, query, click, drag, scroll, type_text, press_key, set_value, perform_secondary_action"];
115
+ readonly status: "available";
116
+ readonly tools: readonly ["vault_go_computer_status", "vault_go_computer_permissions", "vault_go_computer_list_apps", "vault_go_computer_list_windows", "vault_go_computer_get_app_state", "vault_go_computer_query", "vault_go_computer_click", "vault_go_computer_perform_secondary_action", "vault_go_computer_scroll", "vault_go_computer_drag", "vault_go_computer_type_text", "vault_go_computer_paste_text", "vault_go_computer_set_value", "vault_go_computer_press_key", "vault_go_computer_hotkey"];
117
+ readonly requires: "Computer Use by Vault runs through the installed Vault Desktop helper and its macOS Accessibility and Screen Recording permissions; Vault Desktop must be running. select_text, background SkyLight input and Locked Use are not ported. VAULT_GO_COMPUTER_USE=off hides the tools.";
113
118
  }, {
114
119
  readonly upstream: readonly ["viewer", "statusline"];
115
120
  readonly status: "available";
@@ -234,6 +239,11 @@ export declare function getCapabilities(home: string, cloud: VaultMemoryApi, ver
234
239
  readonly status: "available";
235
240
  readonly tools: readonly ["vault_go_modes", "vault_go_mode", "vault_go_agent_cost_report"];
236
241
  readonly difference: "Setup writes managed SKILL.md files for Claude, Grok and shared agent skill directories. mem-search uses vault_go_search_index → vault_go_timeline → vault_go_observations → vault_go_tool_uses. babysit is a session loop with gh, not a Vault background daemon. agent-cost-report prices local JSONL usage with a published list rate and reports missing usage as unavailable.";
242
+ }, {
243
+ readonly upstream: readonly ["open-computer-use: list_apps, get_app_state, query, click, drag, scroll, type_text, press_key, set_value, perform_secondary_action"];
244
+ readonly status: "available";
245
+ readonly tools: readonly ["vault_go_computer_status", "vault_go_computer_permissions", "vault_go_computer_list_apps", "vault_go_computer_list_windows", "vault_go_computer_get_app_state", "vault_go_computer_query", "vault_go_computer_click", "vault_go_computer_perform_secondary_action", "vault_go_computer_scroll", "vault_go_computer_drag", "vault_go_computer_type_text", "vault_go_computer_paste_text", "vault_go_computer_set_value", "vault_go_computer_press_key", "vault_go_computer_hotkey"];
246
+ readonly requires: "Computer Use by Vault runs through the installed Vault Desktop helper and its macOS Accessibility and Screen Recording permissions; Vault Desktop must be running. select_text, background SkyLight input and Locked Use are not ported. VAULT_GO_COMPUTER_USE=off hides the tools.";
237
247
  }, {
238
248
  readonly upstream: readonly ["viewer", "statusline"];
239
249
  readonly status: "available";
@@ -29,6 +29,7 @@ export const CAPABILITIES = {
29
29
  { upstream: ['transcript watchers'], status: 'available', tools: ['vault_go_transcripts_status', 'vault_go_transcripts_configure', 'vault_go_transcripts_poll'], difference: 'Persistent, consented and resumable watchers read Claude, Codex and Cursor JSONL transcripts. Cursor rows use the Claude message shape. Other transcript formats are not claimed.' },
30
30
  { upstream: ['Grok awareness push'], status: 'available', tools: ['vault_go_grok_awareness_status', 'vault_go_grok_awareness_configure', 'vault_go_grok_awareness_push'], requires: 'Explicit consent, an allowlisted agent UUID and a local agent-data root. Monthly awareness logs are bounded, private and separate from Telegram.' },
31
31
  { upstream: ['development/report/presentation skills', 'babysit', 'mem-search', 'agent-cost-report'], status: 'available', tools: ['vault_go_modes', 'vault_go_mode', 'vault_go_agent_cost_report'], difference: 'Setup writes managed SKILL.md files for Claude, Grok and shared agent skill directories. mem-search uses vault_go_search_index → vault_go_timeline → vault_go_observations → vault_go_tool_uses. babysit is a session loop with gh, not a Vault background daemon. agent-cost-report prices local JSONL usage with a published list rate and reports missing usage as unavailable.' },
32
+ { upstream: ['open-computer-use: list_apps, get_app_state, query, click, drag, scroll, type_text, press_key, set_value, perform_secondary_action'], status: 'available', tools: ['vault_go_computer_status', 'vault_go_computer_permissions', 'vault_go_computer_list_apps', 'vault_go_computer_list_windows', 'vault_go_computer_get_app_state', 'vault_go_computer_query', 'vault_go_computer_click', 'vault_go_computer_perform_secondary_action', 'vault_go_computer_scroll', 'vault_go_computer_drag', 'vault_go_computer_type_text', 'vault_go_computer_paste_text', 'vault_go_computer_set_value', 'vault_go_computer_press_key', 'vault_go_computer_hotkey'], requires: 'Computer Use by Vault runs through the installed Vault Desktop helper and its macOS Accessibility and Screen Recording permissions; Vault Desktop must be running. select_text, background SkyLight input and Locked Use are not ported. VAULT_GO_COMPUTER_USE=off hides the tools.' },
32
33
  { upstream: ['viewer', 'statusline'], status: 'available', tools: [], difference: 'The local worker serves /viewer and /statusline on its loopback port. Semantic search stays on the Vault account index. A second local vector database is not shipped.' },
33
34
  { upstream: ['private skills library'], status: 'available', tools: ['vault_go_skills', 'vault_go_skill_folders', 'vault_go_skill_read', 'vault_go_skill_file', 'vault_go_skill_save', 'vault_go_skill_organize', 'vault_go_skill_install', 'vault_go_skill_sync'], requires: 'Account skill packages organized by folders and linked to projects (/v1/skills). Saving publishes a new immutable version; deleting is only available in the Vault apps. Local install verifies sha256 and manifest digest and never replaces a skill the Vault did not install.' },
34
35
  ],
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ComputerUseBridge } from './computer-use.js';
3
+ /** Computer Use by Vault: native app control through the Vault Desktop helper (opt out with VAULT_GO_COMPUTER_USE=off). */
4
+ export declare function registerComputerTools(server: McpServer, bridge?: ComputerUseBridge, env?: NodeJS.ProcessEnv): void;
@@ -0,0 +1,135 @@
1
+ import { z } from 'zod';
2
+ import { errorMessage } from './mcp-result.js';
3
+ import { ComputerUseBridge, computerUseEnabled, queryTree } from './computer-use.js';
4
+ const app = z.string().trim().min(1).max(500).describe('Nome do app, bundle ID (preferível, ex.: com.apple.TextEdit) ou pid:N, como em vault_go_computer_list_apps.');
5
+ const elementIndex = z.number().int().min(0).max(1_000_000).describe('Índice do elemento na árvore de acessibilidade do último get_app_state/query.');
6
+ const coordinate = z.number().finite().min(-100_000).max(100_000);
7
+ const observe = {
8
+ windowId: z.number().int().min(0).optional().describe('ID da janela retornado por vault_go_computer_list_windows.'),
9
+ windowIndex: z.number().int().min(0).optional().describe('Índice da janela quando o ID for none.'),
10
+ restoreWindow: z.boolean().optional().describe('Traz o app/janela para frente antes da operação.'),
11
+ screenshot: z.boolean().default(true).describe('Anexa um screenshot da janela depois da operação. false é mais rápido.'),
12
+ session: z.string().trim().min(1).max(200).optional().describe('Namespace dos índices de elementos. Por padrão, um por servidor MCP.'),
13
+ };
14
+ const ACTION = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
15
+ const READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
16
+ const SAFETY = 'Não envie, compre, apague nem altere contas sem pedido explícito do usuário. Ação não verificada (verification.state=unverified) não é sucesso comprovado.';
17
+ function observeFlags(input, bridge) {
18
+ return {
19
+ app: undefined,
20
+ session: input.session ?? bridge.sessionId,
21
+ 'window-id': input.windowId,
22
+ 'window-index': input.windowIndex,
23
+ 'restore-window': input.restoreWindow,
24
+ 'no-screenshot': input.screenshot === false,
25
+ };
26
+ }
27
+ function toolResult(result) {
28
+ const content = [{ type: 'text', text: JSON.stringify(result.value) }];
29
+ if (result.image)
30
+ content.push({ type: 'image', data: result.image.data, mimeType: result.image.mimeType });
31
+ return { content, structuredContent: result.value };
32
+ }
33
+ async function invoke(operation) {
34
+ try {
35
+ return toolResult(await operation());
36
+ }
37
+ catch (error) {
38
+ return { content: [{ type: 'text', text: errorMessage(error) }], isError: true };
39
+ }
40
+ }
41
+ /** Computer Use by Vault: native app control through the Vault Desktop helper (opt out with VAULT_GO_COMPUTER_USE=off). */
42
+ export function registerComputerTools(server, bridge = new ComputerUseBridge(), env = process.env) {
43
+ if (!computerUseEnabled(env))
44
+ return;
45
+ const action = (command, input, extra, stdin) => invoke(() => bridge.call(command, { ...observeFlags(input, bridge), app: input.app, ...extra }, stdin));
46
+ server.registerTool('vault_go_computer_status', {
47
+ description: 'Computer Use by Vault: verifica se o Vault Desktop e o helper nativo estão disponíveis e quais ações a plataforma suporta. Rode antes da primeira ação; em erro de permissão use vault_go_computer_permissions.',
48
+ inputSchema: {}, annotations: READ,
49
+ }, () => invoke(async () => {
50
+ const result = await bridge.call('capabilities');
51
+ return { ...result, value: { cli: bridge.cliPath(), session: bridge.sessionId, ...result.value } };
52
+ }));
53
+ server.registerTool('vault_go_computer_permissions', {
54
+ description: 'Abre a configuração de permissões do Computer Use by Vault (macOS: Acessibilidade e Gravação de Tela para "Vault Desktop Computer Use") e informa o estado de cada uma.',
55
+ inputSchema: { id: z.enum(['accessibility', 'screenshots']).optional() }, annotations: ACTION,
56
+ }, ({ id }) => invoke(() => bridge.call('permissions', { id })));
57
+ server.registerTool('vault_go_computer_list_apps', {
58
+ description: 'Lista os apps em execução que o Computer Use by Vault pode operar, com bundle ID e pid.',
59
+ inputSchema: {}, annotations: READ,
60
+ }, () => invoke(() => bridge.call('list-apps')));
61
+ server.registerTool('vault_go_computer_list_windows', {
62
+ description: 'Lista as janelas de um app (id, índice, título, posição) para mirar uma janela específica.',
63
+ inputSchema: { app }, annotations: READ,
64
+ }, ({ app: target }) => invoke(() => bridge.call('list-windows', { app: target })));
65
+ server.registerTool('vault_go_computer_get_app_state', {
66
+ description: 'Lê a árvore de acessibilidade da janela de um app e anexa um screenshot. Chame uma vez por turno antes de agir; os números da árvore são os elementIndex das ações e expiram após mudanças na UI.',
67
+ inputSchema: { app, ...observe }, annotations: READ,
68
+ }, (input) => action('get-app-state', input, {}));
69
+ server.registerTool('vault_go_computer_query', {
70
+ description: 'Busca direcionada: encontra controles por texto e/ou papel (button, text field, AXButton) sem screenshot e sem devolver a árvore inteira. Os índices retornados servem direto para click/set_value/scroll na mesma sessão.',
71
+ inputSchema: {
72
+ app,
73
+ text: z.string().trim().max(1_000).optional().describe('Texto no título, descrição, valor ou conteúdo do elemento (sem diferenciar maiúsculas).'),
74
+ role: z.string().trim().max(100).optional().describe('Papel de acessibilidade, ex.: button, text field ou AXButton.'),
75
+ exact: z.boolean().default(false).describe('Exige o nome inteiro em vez de substring.'),
76
+ limit: z.number().int().min(1).max(100).default(20),
77
+ windowId: observe.windowId, windowIndex: observe.windowIndex, session: observe.session,
78
+ },
79
+ annotations: READ,
80
+ }, (input) => invoke(async () => {
81
+ if (!input.text && !input.role)
82
+ throw new Error('Informe text e/ou role.');
83
+ const state = await bridge.call('get-app-state', { ...observeFlags({ ...input, screenshot: false }, bridge), app: input.app });
84
+ const snapshot = (state.value.snapshot ?? {});
85
+ const found = queryTree(typeof snapshot.treeText === 'string' ? snapshot.treeText : '', input);
86
+ return { value: { snapshotId: snapshot.id ?? null, app: snapshot.app ?? null, window: snapshot.window ?? null, ...found, treeTruncated: snapshot.truncation?.truncated === true }, image: null };
87
+ }));
88
+ server.registerTool('vault_go_computer_click', {
89
+ description: `Clica em um elemento (elementIndex) ou em coordenadas da janela (x, y). Suporta clique duplo, botão direito/meio e modificadores atômicos (ex.: CmdOrCtrl+Shift). ${SAFETY}`,
90
+ inputSchema: {
91
+ app, elementIndex: elementIndex.optional(), x: coordinate.optional(), y: coordinate.optional(),
92
+ clickCount: z.number().int().min(1).max(3).optional(), mouseButton: z.enum(['left', 'right', 'middle']).optional(),
93
+ modifiers: z.string().trim().max(100).optional(), ...observe,
94
+ },
95
+ annotations: ACTION,
96
+ }, (input) => action('click', input, { 'element-index': input.elementIndex, x: input.x, y: input.y, 'click-count': input.clickCount, 'mouse-button': input.mouseButton, modifiers: input.modifiers }));
97
+ server.registerTool('vault_go_computer_perform_secondary_action', {
98
+ description: `Executa uma ação secundária de acessibilidade listada no elemento (ex.: "show menu", "increment"). ${SAFETY}`,
99
+ inputSchema: { app, elementIndex, action: z.string().trim().min(1).max(200), ...observe }, annotations: ACTION,
100
+ }, (input) => action('perform-secondary-action', input, { 'element-index': input.elementIndex, action: input.action }));
101
+ server.registerTool('vault_go_computer_scroll', {
102
+ description: 'Rola um elemento ou ponto da janela em páginas (frações permitidas).',
103
+ inputSchema: { app, direction: z.enum(['up', 'down', 'left', 'right']), elementIndex: elementIndex.optional(), x: coordinate.optional(), y: coordinate.optional(), pages: z.number().positive().max(50).optional(), ...observe },
104
+ annotations: ACTION,
105
+ }, (input) => action('scroll', input, { direction: input.direction, 'element-index': input.elementIndex, x: input.x, y: input.y, pages: input.pages }));
106
+ server.registerTool('vault_go_computer_drag', {
107
+ description: `Arrasta entre dois elementos (fromElementIndex/toElementIndex) ou entre coordenadas da janela. ${SAFETY}`,
108
+ inputSchema: {
109
+ app, fromElementIndex: elementIndex.optional(), toElementIndex: elementIndex.optional(),
110
+ fromX: coordinate.optional(), fromY: coordinate.optional(), toX: coordinate.optional(), toY: coordinate.optional(), ...observe,
111
+ },
112
+ annotations: ACTION,
113
+ }, (input) => action('drag', input, { 'from-element-index': input.fromElementIndex, 'to-element-index': input.toElementIndex, 'from-x': input.fromX, 'from-y': input.fromY, 'to-x': input.toX, 'to-y': input.toY }));
114
+ const text = z.string().max(100_000);
115
+ server.registerTool('vault_go_computer_type_text', {
116
+ description: `Digita texto literal no foco atual do app. Prefira set_value em campos editáveis; o texto vai por stdin e não aparece na lista de processos. ${SAFETY}`,
117
+ inputSchema: { app, text, ...observe }, annotations: ACTION,
118
+ }, (input) => action('type-text', input, { 'text-stdin': true }, input.text));
119
+ server.registerTool('vault_go_computer_paste_text', {
120
+ description: `Cola texto exato no foco atual (via área de transferência), útil para textos longos ou com acentos. ${SAFETY}`,
121
+ inputSchema: { app, text, ...observe }, annotations: ACTION,
122
+ }, (input) => action('paste-text', input, { 'text-stdin': true }, input.text));
123
+ server.registerTool('vault_go_computer_set_value', {
124
+ description: `Define o valor de um elemento editável (campo de texto, slider, busca) diretamente pela acessibilidade, com verificação por leitura. ${SAFETY}`,
125
+ inputSchema: { app, elementIndex, value: text, ...observe }, annotations: ACTION,
126
+ }, (input) => action('set-value', input, { 'element-index': input.elementIndex, 'value-stdin': true }, input.value));
127
+ server.registerTool('vault_go_computer_press_key', {
128
+ description: `Pressiona uma tecla única: Return, Escape, Tab, setas, PageUp, Delete. Para combinações use vault_go_computer_hotkey. ${SAFETY}`,
129
+ inputSchema: { app, key: z.string().trim().min(1).max(50), ...observe }, annotations: ACTION,
130
+ }, (input) => action('press-key', input, { key: input.key }));
131
+ server.registerTool('vault_go_computer_hotkey', {
132
+ description: `Pressiona um atalho com modificadores e uma tecla, ex.: CmdOrCtrl+A, CmdOrCtrl+Shift+P. ${SAFETY}`,
133
+ inputSchema: { app, key: z.string().trim().min(1).max(100), ...observe }, annotations: ACTION,
134
+ }, (input) => action('hotkey', input, { key: input.key }));
135
+ }
@@ -0,0 +1,60 @@
1
+ export type CommandOutput = {
2
+ stdout: string;
3
+ stderr: string;
4
+ code: number | null;
5
+ };
6
+ export type CommandRunner = (file: string, args: string[], stdin?: string) => Promise<CommandOutput>;
7
+ export type ComputerUseOptions = {
8
+ env?: NodeJS.ProcessEnv;
9
+ platform?: NodeJS.Platform;
10
+ home?: string;
11
+ exists?: (path: string) => boolean;
12
+ runner?: CommandRunner;
13
+ readFile?: (path: string) => Buffer;
14
+ sessionId?: string;
15
+ };
16
+ export type ComputerImage = {
17
+ data: string;
18
+ mimeType: 'image/png';
19
+ };
20
+ export type ComputerCallResult = {
21
+ value: Record<string, unknown>;
22
+ image: ComputerImage | null;
23
+ };
24
+ export declare function computerUseEnabled(env?: NodeJS.ProcessEnv): boolean;
25
+ /** Finds the Vault Desktop CLI: explicit override, the installed app bundle, then PATH. */
26
+ export declare function resolveDesktopCli(options?: ComputerUseOptions): string | null;
27
+ export declare const defaultRunner: CommandRunner;
28
+ export declare class ComputerUseError extends Error {
29
+ readonly code: string;
30
+ readonly nextSteps: string[];
31
+ constructor(code: string, message: string, nextSteps?: string[]);
32
+ }
33
+ export type Flags = Record<string, string | number | boolean | undefined>;
34
+ /** Runs one `vault-desktop computer <command>` call and returns its JSON result. */
35
+ export declare class ComputerUseBridge {
36
+ readonly sessionId: string;
37
+ private readonly options;
38
+ constructor(options?: ComputerUseOptions);
39
+ cliPath(): string | null;
40
+ call(command: string, flags?: Flags, stdin?: string): Promise<ComputerCallResult>;
41
+ /** Moves screenshot pixels out of the JSON so the client receives an MCP image instead of base64 text. */
42
+ private extractScreenshot;
43
+ }
44
+ export type TreeQuery = {
45
+ text?: string | undefined;
46
+ role?: string | undefined;
47
+ exact?: boolean | undefined;
48
+ limit?: number | undefined;
49
+ };
50
+ export type TreeMatch = {
51
+ index: number;
52
+ depth: number;
53
+ line: string;
54
+ };
55
+ export declare function normalizeRole(role: string): string;
56
+ export declare function queryTree(treeText: string, query: TreeQuery): {
57
+ matches: TreeMatch[];
58
+ truncated: boolean;
59
+ visitedNodes: number;
60
+ };
@@ -0,0 +1,205 @@
1
+ // Computer Use by Vault: bridge from MCP to the Vault Desktop native helper through its CLI.
2
+ // The tool surface follows iFurySt/open-computer-use (MIT); input, permissions and the
3
+ // accessibility runtime stay in Vault Desktop so every client shares one signed helper.
4
+ import { spawn } from 'node:child_process';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { delimiter, join } from 'node:path';
9
+ const COMMAND_TIMEOUT_MS = 90_000;
10
+ const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
11
+ const MAX_SCREENSHOT_BYTES = 20 * 1024 * 1024;
12
+ const MAX_TREE_QUERY_LIMIT = 100;
13
+ const DEFAULT_TREE_QUERY_LIMIT = 20;
14
+ export function computerUseEnabled(env = process.env) {
15
+ const value = env.VAULT_GO_COMPUTER_USE?.trim().toLowerCase();
16
+ return !(value === 'off' || value === '0' || value === 'false' || value === 'disabled');
17
+ }
18
+ /** Finds the Vault Desktop CLI: explicit override, the installed app bundle, then PATH. */
19
+ export function resolveDesktopCli(options = {}) {
20
+ const env = options.env ?? process.env;
21
+ const platform = options.platform ?? process.platform;
22
+ const home = options.home ?? homedir();
23
+ const exists = options.exists ?? existsSync;
24
+ const override = env.VAULT_DESKTOP_CLI?.trim();
25
+ if (override)
26
+ return exists(override) ? override : null;
27
+ const candidates = [];
28
+ if (platform === 'darwin') {
29
+ const bundled = 'Vault Desktop.app/Contents/Resources/bin/vault-desktop';
30
+ candidates.push(join('/Applications', bundled), join(home, 'Applications', bundled));
31
+ }
32
+ if (platform === 'win32' && env.LOCALAPPDATA) {
33
+ const bundled = join('resources', 'bin', 'vault-desktop.exe');
34
+ candidates.push(join(env.LOCALAPPDATA, 'Programs', 'Vault Desktop', bundled), join(env.LOCALAPPDATA, 'Vault Desktop', bundled));
35
+ }
36
+ // Why: only the native .exe launcher on Windows; a .cmd shim would need a shell and let
37
+ // tool arguments such as an app name be interpreted as shell syntax.
38
+ const names = platform === 'win32' ? ['vault-desktop.exe'] : ['vault-desktop', 'vault-ide'];
39
+ for (const dir of (env.PATH ?? '').split(delimiter).filter(Boolean)) {
40
+ for (const name of names)
41
+ candidates.push(join(dir, name));
42
+ }
43
+ return candidates.find((candidate) => exists(candidate)) ?? null;
44
+ }
45
+ export const defaultRunner = (file, args, stdin) => new Promise((resolve, reject) => {
46
+ const child = spawn(file, args, { stdio: ['pipe', 'pipe', 'pipe'], shell: false, windowsHide: true });
47
+ const stdout = [], stderr = [];
48
+ let size = 0;
49
+ const timer = setTimeout(() => child.kill('SIGKILL'), COMMAND_TIMEOUT_MS);
50
+ const collect = (target) => (chunk) => {
51
+ size += chunk.length;
52
+ if (size > MAX_OUTPUT_BYTES)
53
+ child.kill('SIGKILL');
54
+ else
55
+ target.push(chunk);
56
+ };
57
+ child.stdout.on('data', collect(stdout));
58
+ child.stderr.on('data', collect(stderr));
59
+ child.on('error', (error) => { clearTimeout(timer); reject(error); });
60
+ child.on('close', (code) => {
61
+ clearTimeout(timer);
62
+ resolve({ stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), code });
63
+ });
64
+ child.stdin.end(stdin ?? '');
65
+ });
66
+ export class ComputerUseError extends Error {
67
+ code;
68
+ nextSteps;
69
+ constructor(code, message, nextSteps = []) {
70
+ super(nextSteps.length ? `${message}\nPróximos passos: ${nextSteps.join(' ')}` : message);
71
+ this.code = code;
72
+ this.nextSteps = nextSteps;
73
+ }
74
+ }
75
+ /** Runs one `vault-desktop computer <command>` call and returns its JSON result. */
76
+ export class ComputerUseBridge {
77
+ sessionId;
78
+ options;
79
+ constructor(options = {}) {
80
+ this.options = options;
81
+ this.sessionId = options.sessionId ?? `vault-go-${randomUUID().slice(0, 8)}`;
82
+ }
83
+ cliPath() {
84
+ return resolveDesktopCli(this.options);
85
+ }
86
+ async call(command, flags = {}, stdin) {
87
+ const cli = this.cliPath();
88
+ if (!cli) {
89
+ throw new ComputerUseError('desktop_not_found', 'Vault Desktop não foi encontrado nesta máquina.', [
90
+ 'Instale e abra o Vault Desktop (https://vaultmemoryai.com) ou defina VAULT_DESKTOP_CLI com o caminho da CLI vault-desktop.',
91
+ ]);
92
+ }
93
+ const args = ['computer', command, ...flagArgs(flags), '--json'];
94
+ const output = await (this.options.runner ?? defaultRunner)(cli, args, stdin);
95
+ const envelope = parseEnvelope(output);
96
+ if (!envelope.ok) {
97
+ const error = envelope.error ?? {};
98
+ const steps = Array.isArray(error.data?.nextSteps) ? error.data.nextSteps.filter((s) => typeof s === 'string') : [];
99
+ throw new ComputerUseError(typeof error.code === 'string' ? error.code : 'runtime_error', typeof error.message === 'string' ? error.message : 'Falha no Computer Use by Vault.', steps);
100
+ }
101
+ const value = envelope.result && typeof envelope.result === 'object' && !Array.isArray(envelope.result) ? { ...envelope.result } : { value: envelope.result };
102
+ return { value, image: this.extractScreenshot(value) };
103
+ }
104
+ /** Moves screenshot pixels out of the JSON so the client receives an MCP image instead of base64 text. */
105
+ extractScreenshot(value) {
106
+ const screenshot = value.screenshot;
107
+ if (!screenshot || typeof screenshot !== 'object')
108
+ return null;
109
+ const shot = { ...screenshot };
110
+ let data = typeof shot.data === 'string' && shot.data ? shot.data : null;
111
+ if (!data && typeof shot.path === 'string') {
112
+ try {
113
+ const bytes = (this.options.readFile ?? readFileSync)(shot.path);
114
+ if (bytes.length <= MAX_SCREENSHOT_BYTES)
115
+ data = bytes.toString('base64');
116
+ }
117
+ catch {
118
+ data = null;
119
+ }
120
+ }
121
+ delete shot.data;
122
+ delete shot.path;
123
+ shot.attached = data !== null;
124
+ value.screenshot = shot;
125
+ return data ? { data, mimeType: 'image/png' } : null;
126
+ }
127
+ }
128
+ function flagArgs(flags) {
129
+ const args = [];
130
+ for (const [name, value] of Object.entries(flags)) {
131
+ if (value === undefined || value === false)
132
+ continue;
133
+ // Why: `--flag=value` keeps values that start with "--" from being read as flags.
134
+ args.push(value === true ? `--${name}` : `--${name}=${String(value)}`);
135
+ }
136
+ return args;
137
+ }
138
+ function parseEnvelope(output) {
139
+ const text = output.stdout.trim();
140
+ const start = text.indexOf('{');
141
+ if (start >= 0) {
142
+ try {
143
+ const parsed = JSON.parse(text.slice(start));
144
+ if (parsed && typeof parsed === 'object' && typeof parsed.ok === 'boolean')
145
+ return parsed;
146
+ }
147
+ catch {
148
+ // fall through to the stderr message below
149
+ }
150
+ }
151
+ const detail = (output.stderr.trim() || text).split('\n').slice(0, 5).join(' ').slice(0, 500);
152
+ if (/not running|ECONNREFUSED|runtime.*unavailable|could not connect/i.test(detail)) {
153
+ throw new ComputerUseError('desktop_not_running', 'O Vault Desktop não está aberto.', ['Abra o Vault Desktop e tente novamente.']);
154
+ }
155
+ throw new ComputerUseError('runtime_error', `A CLI do Vault Desktop respondeu sem JSON (código ${output.code ?? 'desconhecido'}): ${detail || 'sem saída'}`);
156
+ }
157
+ const TREE_LINE = /^(\s*)(\d+)(?:\s+(.*))?$/;
158
+ const LABELLED_SEGMENT = /,\s*(?:Description|Value|Placeholder|Text|Secondary Actions):\s*/;
159
+ export function normalizeRole(role) {
160
+ return role.trim().replace(/^AX(?=[A-Z])/, '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').toLowerCase();
161
+ }
162
+ export function queryTree(treeText, query) {
163
+ const limit = Math.min(query.limit ?? DEFAULT_TREE_QUERY_LIMIT, MAX_TREE_QUERY_LIMIT);
164
+ const text = query.text?.trim().toLowerCase() ?? '';
165
+ const role = query.role ? normalizeRole(query.role) : '';
166
+ const matches = [];
167
+ let visitedNodes = 0, truncated = false;
168
+ for (const raw of treeText.split('\n')) {
169
+ const parsed = TREE_LINE.exec(raw);
170
+ if (!parsed)
171
+ continue;
172
+ visitedNodes += 1;
173
+ const body = (parsed[3] ?? '').trim();
174
+ if (role && !startsWithWord(body.toLowerCase(), role))
175
+ continue;
176
+ if (text && !matchesText(body, text, query.exact === true))
177
+ continue;
178
+ if (matches.length >= limit) {
179
+ truncated = true;
180
+ break;
181
+ }
182
+ matches.push({ index: Number(parsed[2]), depth: Math.floor((parsed[1] ?? '').replace(/\t/g, ' ').length / 2), line: body });
183
+ }
184
+ return { matches, truncated, visitedNodes };
185
+ }
186
+ function startsWithWord(body, prefix) {
187
+ if (!body.startsWith(prefix))
188
+ return false;
189
+ const next = body.charAt(prefix.length);
190
+ return next === '' || next === ' ' || next === '(' || next === ',';
191
+ }
192
+ function matchesText(body, needle, exact) {
193
+ if (!exact)
194
+ return body.toLowerCase().includes(needle);
195
+ const [head = '', ...rest] = body.split(LABELLED_SEGMENT);
196
+ if (rest.some((segment) => segment.trim().toLowerCase() === needle))
197
+ return true;
198
+ const name = head.trim(), lowered = name.toLowerCase();
199
+ if (lowered === needle)
200
+ return true;
201
+ if (!lowered.endsWith(` ${needle}`))
202
+ return false;
203
+ // The head is "<role> (traits) <name>": accept the suffix only after a role-like prefix.
204
+ return /^[\p{Ll} ]+(?:\([^)]*\))?$/u.test(name.slice(0, name.length - needle.length).trim());
205
+ }
@@ -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}.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>`;
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}.row-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.tag{font-size:10px;letter-spacing:.02em;color:#c9c6ff;background:#b9b6ff14;border:1px solid #b9b6ff33;border-radius:999px;padding:2px 8px}.detail-list{margin:0;padding-left:18px}.detail-list li{margin:2px 0}.detail-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:22px}.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><a class="button" id="detail-open" hidden target="_blank" rel="noopener noreferrer" data-i18n="openInVault">Open in Vault ↗</a><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";
@@ -35,7 +35,12 @@ export function dashboardScript() {
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
+ const memoryCopy={
39
+ en:{openInVault:'Open in Vault ↗',fieldContent:'Content',fieldFacts:'Facts',fieldConcepts:'Concepts',fieldFilesRead:'Files read',fieldFilesModified:'Files modified',fieldType:'Type',fieldProject:'Project',fieldSource:'Source',fieldCreated:'Created',fieldId:'ID',typeDecision:'Decision',typeDiscovery:'Discovery',typeFix:'Fix',typeFeature:'Feature',typeRefactor:'Refactor',typeChange:'Change',typeTask:'Task',typeQuestion:'Question',typeInstruction:'Instruction',kindPrompt:'Prompt',kindSummary:'Summary',kindObservation:'Observation'},
40
+ pt:{openInVault:'Abrir no Vault ↗',fieldContent:'Conteúdo',fieldFacts:'Fatos',fieldConcepts:'Conceitos',fieldFilesRead:'Arquivos lidos',fieldFilesModified:'Arquivos alterados',fieldType:'Tipo',fieldProject:'Projeto',fieldSource:'Origem',fieldCreated:'Criada em',fieldId:'ID',typeDecision:'Decisão',typeDiscovery:'Descoberta',typeFix:'Correção',typeFeature:'Funcionalidade',typeRefactor:'Refatoração',typeChange:'Mudança',typeTask:'Tarefa',typeQuestion:'Pergunta',typeInstruction:'Instrução',kindPrompt:'Pedido',kindSummary:'Resumo',kindObservation:'Observação'},
41
+ es:{openInVault:'Abrir en Vault ↗',fieldContent:'Contenido',fieldFacts:'Hechos',fieldConcepts:'Conceptos',fieldFilesRead:'Archivos leídos',fieldFilesModified:'Archivos modificados',fieldType:'Tipo',fieldProject:'Proyecto',fieldSource:'Origen',fieldCreated:'Creada',fieldId:'ID',typeDecision:'Decisión',typeDiscovery:'Descubrimiento',typeFix:'Corrección',typeFeature:'Funcionalidad',typeRefactor:'Refactorización',typeChange:'Cambio',typeTask:'Tarea',typeQuestion:'Pregunta',typeInstruction:'Instrucción',kindPrompt:'Petición',kindSummary:'Resumen',kindObservation:'Observación'}
42
+ };
43
+ for (const language of Object.keys(strings)) Object.assign(strings[language],onboardingCopy[language],detailCopy[language],engineCopy[language],disclosureCopy[language],memoryCopy[language]);
39
44
  let locale = navigator.language.slice(0,2);
40
45
  try { locale = localStorage.getItem('vault-local-locale') || locale; } catch {}
41
46
  if (!strings[locale]) locale = 'en';
@@ -66,8 +71,33 @@ export function dashboardScript() {
66
71
  toggle.addEventListener('click',()=>{const expanded=target.classList.toggle('clamped')===false;toggle.setAttribute('aria-expanded',String(expanded));toggle.textContent=t(expanded?'showLess':'showMore');});
67
72
  return toggle;
68
73
  }
69
- function showDetails(title,item,trigger) {
74
+ // The cloud feed wraps each memory as {memory, platformSource}; older caches hold the memory itself.
75
+ const memoryOf=item=>{const record=object(item),inner=object(record.memory);return Object.keys(inner).length?{...inner,platformSource:inner.platformSource||record.platformSource}:record;};
76
+ const SOURCE_NAMES={claude:'Claude Code','claude-code':'Claude Code',codex:'Codex',cursor:'Cursor',windsurf:'Windsurf',agy:'Antigravity',antigravity:'Antigravity',gemini:'Gemini CLI',grok:'Grok',opencode:'OpenCode',copilot:'GitHub Copilot',vscode:'VS Code',warp:'Warp',goose:'Goose',hermes:'Hermes','vault-desktop':'Vault Desktop','vault-go':'Vault Go',chatgpt:'ChatGPT'};
77
+ const sourceName=value=>SOURCE_NAMES[scalar(value).toLowerCase()]||scalar(value);
78
+ const typeName=memory=>{const type=scalar(memory.memoryType||memory.type).toLowerCase(),kind=scalar(memory.kind).toLowerCase();const key=type?'type'+type[0].toUpperCase()+type.slice(1):'';const label=key&&strings[locale][key];return label||(kind&&strings[locale]['kind'+kind[0].toUpperCase()+kind.slice(1)])||type||kind;};
79
+ const projectName=id=>{const project=array(state&&state.projects).map(object).find(item=>item.id===id);return project?first(project,['name','title']):'';};
80
+ const epochDate=value=>typeof value==='number'?date(value):date(scalar(value));
81
+ const vaultLink=(params)=>'${VAULT_WEB_MEMORY_URL}?'+new URLSearchParams(params).toString();
82
+ const uuid=/^[0-9a-f-]{36}$/i;
83
+ function showMemory(memory,trigger) {
70
84
  contextResult=null;el('detail-copy').hidden=true;
85
+ detailFocus=trigger.id;text('detail-title',first(memory,['title','summary'])||t('untitled'));
86
+ const fields=el('detail-fields');fields.replaceChildren();
87
+ const add=(label,value)=>{if(value===undefined||value===null||value===''||(Array.isArray(value)&&!value.length))return;const term=node('dt','',t(label)),definition=node('dd','');
88
+ if(Array.isArray(value)){const list=node('ul','detail-list');for(const item of value.slice(0,200))list.append(node('li','',scalar(item)));definition.append(list);}
89
+ else{const content=node('div','',String(value));definition.append(content);if(isLong(content.textContent))definition.append(disclosure(content));}
90
+ fields.append(term,definition);};
91
+ add('fieldContent',scalar(memory.content));add('fieldFacts',array(memory.facts));add('fieldConcepts',array(memory.concepts).length?array(memory.concepts).map(scalar).join(' · '):'');
92
+ add('fieldFilesModified',array(memory.filesModified));add('fieldFilesRead',array(memory.filesRead));
93
+ add('fieldType',typeName(memory));add('fieldProject',projectName(memory.projectId));add('fieldSource',sourceName(memory.platformSource));
94
+ add('fieldCreated',epochDate(memory.createdAtEpoch||memory.createdAt));add('fieldId',scalar(memory.id));
95
+ const open=el('detail-open');const id=scalar(memory.id),projectId=scalar(memory.projectId);
96
+ open.hidden=!uuid.test(id);if(!open.hidden)open.href=vaultLink({memory:id,...(uuid.test(projectId)?{project:projectId}:{})});
97
+ el('detail-dialog').showModal();el('detail-close').focus();
98
+ }
99
+ function showDetails(title,item,trigger) {
100
+ contextResult=null;el('detail-copy').hidden=true;el('detail-open').hidden=true;
71
101
  detailFocus=trigger.id; text('detail-title',title);
72
102
  const fields=el('detail-fields'); fields.replaceChildren();
73
103
  const entries=Object.entries(object(item));
@@ -83,7 +113,7 @@ export function dashboardScript() {
83
113
  function viewButton(title,item,kind,index) {
84
114
  const button=node('button','button',t('view'));button.type='button';button.id=kind+'-view-'+index;
85
115
  button.setAttribute('aria-label',t('view')+': '+title.slice(0,100));
86
- button.addEventListener('click',()=>showDetails(title,item,button));return button;
116
+ button.addEventListener('click',()=>kind==='memory'?showMemory(item,button):showDetails(title,item,button));return button;
87
117
  }
88
118
  function pageRows(kind,rows,total) {
89
119
  const paging=pagination[kind],pages=Math.max(1,Math.ceil(rows.length/paging.size));
@@ -106,21 +136,30 @@ export function dashboardScript() {
106
136
  function list(kind) {
107
137
  const source=array(state && state[kind==='memory'?'memories':kind==='project'?'projects':'activity']);
108
138
  const search=el(kind+'-search').value.toLocaleLowerCase(locale).trim();
109
- const rows=source.map((item,index)=>({item,index,title:first(item,kind==='activity'?['operation']:['title','name','summary','content','text','id'])||t('untitled'),detail:first(item,['project','project_name','description','created_at','createdAt'])}));
139
+ const rows=source.map((item,index)=>{
140
+ if(kind==='memory'){const memory=memoryOf(item);return {item:memory,index,title:first(memory,['title','summary','content','text','id'])||t('untitled'),detail:[projectName(memory.projectId),epochDate(memory.createdAtEpoch||memory.createdAt)].filter(Boolean).join(' · '),tags:[typeName(memory),sourceName(memory.platformSource)].filter(Boolean)};}
141
+ return {item,index,title:first(item,kind==='activity'?['operation']:['title','name','summary','content','text','id'])||t('untitled'),detail:first(item,['project','project_name','description','created_at','createdAt'])};
142
+ });
110
143
  const filtered=rows.filter(row=>JSON.stringify(row.item).toLocaleLowerCase(locale).includes(search));
111
144
  const target=el(kind+'-list');target.replaceChildren();
112
145
  const visible=pageRows(kind,filtered,source.length);
113
146
  if(!visible.length)return empty(target,t(source.length?'noResults':kind==='memory'?'memoriesEmpty':kind==='project'?'projectsEmpty':'activityEmpty'));
114
147
  for(const row of visible) {
115
148
  const wrap=node('div','row'),body=node('div','row-body'),title=node('button','row-title row-open',row.title.slice(0,600));
116
- title.type='button';title.addEventListener('click',()=>showDetails(row.title,row.item,button));
149
+ const open=()=>kind==='memory'?showMemory(row.item,button):showDetails(row.title,row.item,button);
150
+ title.type='button';title.addEventListener('click',open);
117
151
  body.append(title);
118
152
  if(row.title.length>200)title.classList.add('clamped');
119
153
  if(kind==='activity') {
120
154
  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':'')));
121
155
  wrap.append(node('span','row-marker'+(item.ok===true?'':' failed')));
122
156
  } else if(row.detail)body.append(node('div','row-meta',row.detail.slice(0,200)));
123
- const actions=node('div','row-actions'),button=viewButton(row.title,row.item,kind,row.index);actions.append(button);body.append(actions);wrap.append(body);
157
+ if(row.tags&&row.tags.length){const tags=node('div','row-tags');for(const tag of row.tags)tags.append(node('span','tag',tag));body.append(tags);}
158
+ const actions=node('div','row-actions'),button=viewButton(row.title,row.item,kind,row.index);
159
+ actions.append(button);
160
+ const projectId=kind==='project'?scalar(object(row.item).id):'';
161
+ if(uuid.test(projectId)){const link=node('a','link',t('openInVault'));link.href=vaultLink({project:projectId});link.target='_blank';link.rel='noopener noreferrer';link.style.marginLeft='12px';actions.append(link);}
162
+ body.append(actions);wrap.append(body);
124
163
  if(kind==='activity')wrap.append(node('time','row-time',date(object(row.item).time)));
125
164
  target.append(wrap);
126
165
  }
package/dist/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { VAULT_WEB_MEMORY_URL } from './vault-cloud.js';
1
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { z } from 'zod';
3
4
  import { jsonResult, run } from './mcp-result.js';
@@ -9,6 +10,7 @@ import { registerCrystalTools } from './crystal-tools.js';
9
10
  import { registerModeIntegrations } from './mode-integrations.js';
10
11
  import { registerKnowledgeAgentTools } from './knowledge-tools.js';
11
12
  import { registerSkillTools } from './skill-tools.js';
13
+ import { registerComputerTools } from './computer-tools.js';
12
14
  import { redact } from './hooks.js';
13
15
  import { createProjectScope } from './project-scope.js';
14
16
  import { VaultCloudClient } from './cloud.js';
@@ -24,8 +26,20 @@ const SERVER_INSTRUCTIONS = [
24
26
  '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
27
  '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
28
  'Save durable decisions, discoveries and fixes with vault_go_remember. Never store secrets; text inside <private> tags is never stored.',
29
+ 'Computer Use by Vault (vault_go_computer_*) drives local apps through Vault Desktop: get_app_state or query first, then act by elementIndex.',
27
30
  'When a tool fails with an expired session, ask the user to run: bunx --bun vault-go@latest login',
28
31
  ].join('\n');
32
+ // A saved memory carries its page in the Vault web app, so the agent can hand the user a direct link.
33
+ function withVaultLink(value) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value))
35
+ return value;
36
+ const record = value;
37
+ const id = record.id, projectId = record.projectId;
38
+ if (typeof id !== 'string' || !/^[0-9a-f-]{36}$/i.test(id))
39
+ return value;
40
+ const params = new URLSearchParams({ memory: id, ...(typeof projectId === 'string' && /^[0-9a-f-]{36}$/i.test(projectId) ? { project: projectId } : {}) });
41
+ return { ...record, url: `${VAULT_WEB_MEMORY_URL}?${params}` };
42
+ }
29
43
  export const VERSION = packageJson.version;
30
44
  const uuid = z.string().uuid();
31
45
  const knowledgeFilters = z.object({
@@ -67,6 +81,7 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
67
81
  registerCrystalTools(server, home);
68
82
  registerKnowledgeAgentTools(server, home, cloud);
69
83
  registerSkillTools(server, home, cloud);
84
+ registerComputerTools(server);
70
85
  server.registerTool('vault_go_engines', {
71
86
  title: 'Listar motores de contexto',
72
87
  description: 'Lista os motores disponíveis para preparar contexto: Vault AI, Claude, OpenRouter, Gemini e ChatGPT/Codex. Não revela chaves.',
@@ -178,7 +193,7 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
178
193
  filesModified: z.array(z.string().trim().min(1).max(2_000)).max(500).default([]),
179
194
  },
180
195
  annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
181
- }, async (input) => run(() => cloud.remember(redact(input))));
196
+ }, async (input) => run(async () => withVaultLink(await cloud.remember(redact(input)))));
182
197
  server.registerTool('vault_go_forget', {
183
198
  title: 'Excluir memória',
184
199
  description: 'Exclui uma memória da conta autenticada.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.32.0",
3
+ "version": "0.34.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": {
@@ -60,6 +60,10 @@
60
60
  "dist/crystal-memory-mirror.d.ts",
61
61
  "dist/crystal-tools.js",
62
62
  "dist/crystal-tools.d.ts",
63
+ "dist/computer-use.js",
64
+ "dist/computer-use.d.ts",
65
+ "dist/computer-tools.js",
66
+ "dist/computer-tools.d.ts",
63
67
  "dist/hook-install.js",
64
68
  "dist/hook-install.d.ts",
65
69
  "dist/hook-queue.js",