vault-go 0.10.0 → 0.11.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.
@@ -0,0 +1,91 @@
1
+ /** Local monitor: no secrets or cloud content are interpolated into this document. */
2
+ export function dashboardHtml() {
3
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>Vault · Local</title><style>
4
+ :root{font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#eaf0f1;background:#080b10;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px;background:radial-gradient(ellipse at 80% 0%,#17363c66,transparent 50%),#080b10}button,input,select{font:inherit}button,select{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid #88ebd4;outline-offset:4px}button:disabled{opacity:.5;cursor:wait}.shell{max-width:1440px;margin:auto;display:grid;grid-template-columns:228px 1fr;min-height:100vh}.rail{border-right:1px solid #ffffff0c;padding:34px 22px;background:#0b1018a6}.brand{display:flex;align-items:center;gap:12px;font-size:24px;font-weight:660;letter-spacing:-1px}.brand svg{width:34px;height:34px}.badge{font-size:10px;letter-spacing:1.4px;color:#8edbc8;border:1px solid #91dbc32b;border-radius:5px;padding:4px 6px;margin-left:2px}.rail-label{margin:52px 12px 15px;font-size:10px;color:#6f7f91;letter-spacing:1.7px}.nav{display:flex;flex-direction:column;gap:7px}.nav a{display:flex;align-items:center;gap:12px;color:#8c9caf;text-decoration:none;padding:12px;border-radius:9px;font-size:13px}.nav a:hover,.nav a.active{background:#90e4cf0c;color:#a2eedb}.nav .icon{width:20px;font-size:18px}.rail-footer{margin-top:70px;color:#637388;font-size:11px;line-height:1.8;padding:12px}.dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:#7ce0b8;margin-right:7px}.main{padding:29px 42px 50px;min-width:0}.top{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #ffffff0c;padding-bottom:24px;gap:18px}.breadcrumb{font-size:12px;color:#738398}.breadcrumb b{font-weight:400;color:#c2ccd7}.controls{display:flex;align-items:center;gap:12px}select,.button{color:#c7d4df;border:1px solid #ffffff17;background:#141d28;border-radius:8px;padding:9px 12px;font-size:12px}.button{display:inline-flex;align-items:center;gap:8px}.button:hover{border-color:#79d8c05c}.welcome{display:flex;justify-content:space-between;align-items:center;margin:36px 0 26px;gap:20px}.eyebrow{font-size:10px;letter-spacing:2px;color:#79cbb6;margin-bottom:12px}h1{font-size:30px;letter-spacing:-1.1px;font-weight:550;margin:0 0 10px}p{color:#8494a7;font-size:13px;line-height:1.7;margin:0}.pulse{width:48px;height:48px;display:grid;place-items:center;background:#9af1d30a;border:1px solid #99eace20;border-radius:15px;color:#94e6ce;font-size:23px;animation:arrive .7s ease both}.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.card,.panel{border:1px solid #ffffff11;background:linear-gradient(140deg,#1a263172,#101721aa);border-radius:14px;box-shadow:0 15px 40px #0000000a}.card{padding:22px}.card-label{font-size:11px;color:#8e9eb0;display:flex;justify-content:space-between}.card-value{font-size:23px;letter-spacing:-.6px;margin:17px 0 9px;font-weight:550;overflow-wrap:anywhere}.card-note{font-size:11px;color:#708297}.good{color:#9ce9cc}.warning{color:#efc489}.layout{display:grid;grid-template-columns:1.3fr 1fr;gap:20px;margin-top:24px}.panel{padding:23px;min-width:0;scroll-margin-top:20px}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px}h2{font-size:14px;font-weight:550;margin:0;letter-spacing:-.2px}.count{color:#76899e;font-size:11px;border:1px solid #ffffff13;padding:3px 7px;border-radius:5px}.search{width:100%;background:#070e1680;border:1px solid #ffffff12;color:#d8e3ec;border-radius:8px;padding:10px 12px;margin-bottom:14px;font-size:12px}.empty{min-height:162px;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;padding:22px 10px;gap:10px}.empty-symbol{width:34px;height:34px;border:1px solid #ffffff15;border-radius:10px;display:grid;place-items:center;color:#698a96}.empty p{font-size:12px;max-width:280px}.row{padding:13px 0;border-top:1px solid #ffffff0b;display:flex;gap:12px;align-items:flex-start}.row:first-child{border-top:0}.row-marker{width:7px;height:7px;background:#8bddbd;border-radius:50%;margin-top:6px;flex-shrink:0}.row-marker.failed{background:#dfb483}.row-body{flex:1;min-width:0}.row-title{font-size:12px;color:#cbd8e4;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.row-meta{font-size:10px;color:#6f839a;margin-top:4px;overflow-wrap:anywhere}.row-time{font-size:10px;color:#7d8ea1;white-space:nowrap;margin-top:4px}.wide{grid-column:1/-1}.footer{display:flex;justify-content:space-between;margin-top:23px;gap:15px;font-size:10px;color:#586c82;line-height:1.6}.notice{padding:17px 20px;background:#172c31;border:1px solid #93e3ce30;border-radius:10px;margin-bottom:22px}.notice p{color:#c0d5d3}.notice code{display:block;color:#a2efcf;font-size:12px;margin-top:9px;overflow-wrap:anywhere}.notice[hidden]{display:none}.error{color:#e7bd8a;font-size:12px;line-height:1.6;margin:10px 0;overflow-wrap:anywhere}.error:empty{display:none}@keyframes arrive{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.cards,.layout{animation:arrive .6s ease both}.layout{animation-delay:.08s}@media(prefers-reduced-motion:reduce){*{animation:none!important;scroll-behavior:auto!important}}@media(max-width:1000px){.shell{grid-template-columns:180px 1fr}.rail{padding:28px 15px}.main{padding:28px 25px}.cards{gap:10px}.card{padding:17px}.card-value{font-size:20px}.layout{grid-template-columns:1fr}.wide{grid-column:auto}}@media(max-width:680px){.shell{display:block}.rail{padding:18px 20px;border-right:0;border-bottom:1px solid #ffffff0c}.brand{font-size:21px}.rail-label,.rail-footer{display:none}.nav{flex-direction:row;margin-top:18px;gap:3px;overflow:auto}.nav a{padding:8px;font-size:11px;white-space:nowrap;gap:5px}.nav .icon{font-size:14px;width:15px}.main{padding:20px}.top{padding-bottom:18px}.breadcrumb{display:none}.controls{width:100%;justify-content:space-between}.welcome{margin-top:28px}h1{font-size:26px}.cards{grid-template-columns:1fr}.card{padding:18px;display:grid;grid-template-columns:1fr 1fr;align-items:center}.card-value{margin:0;text-align:right;font-size:20px}.card-note{margin-top:9px;grid-column:1/-1}.panel{padding:18px}.footer{flex-direction:column;gap:4px}}
5
+ </style><script src="/app.js" defer></script></head><body><div class="shell"><aside class="rail"><div class="brand"><svg viewBox="0 0 40 40" aria-hidden="true"><defs><linearGradient id="v" x2="1" y2="1"><stop stop-color="#ccfbea"/><stop offset="1" stop-color="#65b9a8"/></linearGradient></defs><path d="M5 8h8l7 18L27 8h8L23 34h-6z" fill="url(#v)"/><path d="m17 8 3 8 3-8" fill="#82baac" opacity=".4"/></svg>vault<span class="badge">LOCAL</span></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="#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="refresh" type="button"><span aria-hidden="true">↻</span><span data-i18n="refresh">Refresh cloud</span></button></div></header><div class="welcome"><div><div class="eyebrow" data-i18n="eyebrow">YOUR KNOWLEDGE, CONNECTED</div><h1 data-i18n="title">A little more context.</h1><p data-i18n="subtitle">A clear view of your local Vault and cloud connection.</p></div><div class="pulse" aria-hidden="true">⌁</div></div><div id="session-notice" class="notice" hidden><p data-i18n="session">Open a secure local session from your terminal:</p><code>bunx --bun vault-go@latest local open</code></div><div id="error" class="error" role="status" aria-live="polite"></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><div class="layout"><section class="panel" id="activity"><div class="panel-head"><h2 data-i18n="activity">Activity</h2><span class="count" id="activity-count">0</span></div><div id="activity-list"></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></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></section></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></body></html>`;
6
+ }
7
+ export function dashboardScript() {
8
+ return `"use strict";
9
+ (() => {
10
+ const fragment = location.hash.slice(1);
11
+ history.replaceState(null, '', location.pathname + location.search);
12
+ const token = /^[A-Za-z0-9_-]{43}$/.test(fragment) ? fragment : new URLSearchParams(fragment).get('token');
13
+ const strings = {
14
+ en: {workspace:'WORKSPACE',overview:'Overview',activity:'Activity',memories:'Memories',projects:'Projects',localWorkspace:'Your local workspace',privacy:'Activity metadata stays local.',localMonitor:'Local monitor',refresh:'Refresh cloud',eyebrow:'YOUR KNOWLEDGE, CONNECTED',title:'A little more context.',subtitle:'A clear view of your local Vault and cloud connection.',session:'Open a secure local session from your terminal:',localService:'Local service',cloudConnection:'Cloud connection',recentOperations:'Recent operations',operationsNote:'Recent MCP request metadata',searchMemories:'Search memories…',searchProjects:'Search projects…',footer:'Vault Local · Context with a place to call home.',online:'Running',offline:'Unavailable',connected:'Connected',disconnected:'Disconnected',checking:'Checking…',signedOut:'Sign in through the CLI to connect.',activityEmpty:'No operations recorded yet. Activity will appear as this local service is used.',memoriesEmpty:'No memories available. Memories returned by your cloud account will appear here.',projectsEmpty:'No projects available. Projects returned by your cloud account will appear here.',noResults:'No matching results.',updated:'Updated',waiting:'Waiting for a secure session',fetchError:'Unable to reach the local service. Check that Vault Local is running.',syncError:'Cloud refresh failed. Try again shortly.',success:'Completed',failed:'Failed',untitled:'Untitled',unknown:'Unknown',paused:'Updates paused while this tab is hidden'},
15
+ pt: {workspace:'ESPAÇO DE TRABALHO',overview:'Visão geral',activity:'Atividade',memories:'Memórias',projects:'Projetos',localWorkspace:'Seu espaço local',privacy:'Metadados de atividade ficam locais.',localMonitor:'Monitor local',refresh:'Atualizar nuvem',eyebrow:'SEU CONHECIMENTO, CONECTADO',title:'Um pouco mais de contexto.',subtitle:'Uma visão clara do seu Vault local e da conexão com a nuvem.',session:'Abra uma sessão local segura pelo terminal:',localService:'Serviço local',cloudConnection:'Conexão com a nuvem',recentOperations:'Operações recentes',operationsNote:'Metadados recentes das chamadas MCP',searchMemories:'Buscar memórias…',searchProjects:'Buscar projetos…',footer:'Vault Local · Um lugar para o seu contexto.',online:'Em execução',offline:'Indisponível',connected:'Conectada',disconnected:'Desconectada',checking:'Verificando…',signedOut:'Entre pela CLI para conectar.',activityEmpty:'Nenhuma operação registrada. A atividade aparecerá conforme este serviço local for usado.',memoriesEmpty:'Nenhuma memória disponível. As memórias retornadas pela sua conta na nuvem aparecerão aqui.',projectsEmpty:'Nenhum projeto disponível. Os projetos retornados pela sua conta na nuvem aparecerão aqui.',noResults:'Nenhum resultado encontrado.',updated:'Atualizado',waiting:'Aguardando uma sessão segura',fetchError:'Não foi possível acessar o serviço local. Verifique se o Vault Local está em execução.',syncError:'A atualização da nuvem falhou. Tente novamente em instantes.',success:'Concluída',failed:'Falhou',untitled:'Sem título',unknown:'Desconhecido',paused:'Atualizações pausadas enquanto esta aba está oculta'},
16
+ es: {workspace:'ESPACIO DE TRABAJO',overview:'Resumen',activity:'Actividad',memories:'Memorias',projects:'Proyectos',localWorkspace:'Tu espacio local',privacy:'Los metadatos de actividad quedan locales.',localMonitor:'Monitor local',refresh:'Actualizar nube',eyebrow:'TU CONOCIMIENTO, CONECTADO',title:'Un poco más de contexto.',subtitle:'Una vista clara de tu Vault local y de la conexión con la nube.',session:'Abre una sesión local segura desde tu terminal:',localService:'Servicio local',cloudConnection:'Conexión con la nube',recentOperations:'Operaciones recientes',operationsNote:'Metadatos recientes de llamadas MCP',searchMemories:'Buscar memorias…',searchProjects:'Buscar proyectos…',footer:'Vault Local · Un hogar para tu contexto.',online:'En ejecución',offline:'No disponible',connected:'Conectada',disconnected:'Desconectada',checking:'Comprobando…',signedOut:'Inicia sesión desde la CLI para conectar.',activityEmpty:'Aún no hay operaciones registradas. La actividad aparecerá al usar este servicio local.',memoriesEmpty:'No hay memorias disponibles. Las memorias de tu cuenta en la nube aparecerán aquí.',projectsEmpty:'No hay proyectos disponibles. Los proyectos de tu cuenta en la nube aparecerán aquí.',noResults:'No se encontraron resultados.',updated:'Actualizado',waiting:'Esperando una sesión segura',fetchError:'No se pudo acceder al servicio local. Comprueba que Vault Local esté en ejecución.',syncError:'La actualización de la nube falló. Inténtalo de nuevo en unos momentos.',success:'Completada',failed:'Falló',untitled:'Sin título',unknown:'Desconocido',paused:'Actualizaciones pausadas mientras esta pestaña está oculta'}
17
+ };
18
+ let locale = navigator.language.slice(0,2);
19
+ try { locale = localStorage.getItem('vault-local-locale') || locale; } catch {}
20
+ if (!strings[locale]) locale = 'en';
21
+ let state = null, busy = false, sessionRequired = false, failure = '', pendingRefresh = false;
22
+ const el = id => document.getElementById(id);
23
+ const t = key => strings[locale][key] || key;
24
+ const object = value => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
25
+ const scalar = value => typeof value === 'string' || typeof value === 'number' ? String(value) : '';
26
+ const first = (value, keys) => { const record = object(value); for (const key of keys) { const result = scalar(record[key]); if (result) return result; } return scalar(value); };
27
+ const array = value => Array.isArray(value) ? value : [];
28
+ const date = value => { if (!value) return ''; const d = new Date(value); return Number.isFinite(d.getTime()) ? d.toLocaleString(locale) : ''; };
29
+ const text = (id, value) => { el(id).textContent = value; };
30
+ const node = (tag, className, value) => { const n = document.createElement(tag); n.className = className; if (value !== undefined) n.textContent = value; return n; };
31
+ function empty(target, message) { const box = node('div','empty'); box.append(node('span','empty-symbol','◇'),node('p','',message)); target.append(box); }
32
+ function list(kind) {
33
+ const source = array(state && state[kind === 'memory' ? 'memories' : 'projects']);
34
+ const search = el(kind + '-search').value.toLocaleLowerCase(locale).trim();
35
+ const rows = source.map(item => ({title:first(item,['title','name','summary','content','text','id']) || t('untitled'),detail:first(item,['project','project_name','description','created_at','createdAt'])}));
36
+ const filtered = rows.filter(row => (row.title + ' ' + row.detail).toLocaleLowerCase(locale).includes(search));
37
+ const target = el(kind + '-list'); target.replaceChildren(); text(kind + '-count',String(source.length));
38
+ if (!filtered.length) return empty(target,t(source.length ? 'noResults' : kind === 'memory' ? 'memoriesEmpty' : 'projectsEmpty'));
39
+ for (const row of filtered.slice(0,100)) { const wrap=node('div','row'),body=node('div','row-body'); body.append(node('div','row-title',row.title.slice(0,600))); if (row.detail) body.append(node('div','row-meta',row.detail.slice(0,200))); wrap.append(body); target.append(wrap); }
40
+ }
41
+ function render() {
42
+ document.documentElement.lang = locale; el('locale').value = locale;
43
+ document.querySelectorAll('[data-i18n]').forEach(n => { n.textContent=t(n.dataset.i18n); });
44
+ document.querySelectorAll('[data-placeholder]').forEach(n => { n.placeholder=t(n.dataset.placeholder); n.setAttribute('aria-label',t(n.dataset.placeholder)); });
45
+ el('session-notice').hidden = !sessionRequired;
46
+ el('refresh').disabled = busy || sessionRequired;
47
+ text('error',failure ? t(failure) : '');
48
+ text('local-status',sessionRequired ? t('waiting') : state ? t('online') : failure ? t('offline') : t('checking'));
49
+ el('local-status').className='card-value ' + (state ? 'good' : '');
50
+ text('local-note',state ? 'localhost:' + (Number.isInteger(state.port) ? state.port : 38850) + ' · v' + scalar(state.version) : 'localhost:38850');
51
+ const cloud=object(state && state.cloud);
52
+ text('cloud-status',state ? t(cloud.connected === true ? 'connected' : 'disconnected') : '—');
53
+ el('cloud-status').className='card-value ' + (cloud.connected === true ? 'good' : '');
54
+ text('cloud-note',state ? (cloud.error ? t('syncError') : '') || scalar(state.email) || (state.authenticated ? 'Vault Cloud' : t('signedOut')) : '—');
55
+ const activity=array(state && state.activity); text('activity-total',state ? String(activity.length) : '—'); text('activity-count',String(activity.length));
56
+ const target=el('activity-list'); target.replaceChildren();
57
+ if (!activity.length) empty(target,t('activityEmpty'));
58
+ for (const item of activity.slice(0,50)) { const a=object(item),wrap=node('div','row'),body=node('div','row-body');body.append(node('div','row-title',scalar(a.operation) || t('unknown')),node('div','row-meta',(a.ok === true ? t('success') : t('failed')) + (typeof a.durationMs === 'number' && Number.isFinite(a.durationMs) ? ' · ' + Math.round(a.durationMs) + ' ms' : '')));wrap.append(node('span','row-marker'+(a.ok === true ? '' : ' failed')),body,node('time','row-time',date(a.time)));target.append(wrap); }
59
+ list('memory'); list('project'); text('updated',state && state.updatedAt ? t('updated') + ' ' + date(state.updatedAt) : t('waiting'));
60
+ }
61
+ async function request(path, options) {
62
+ const response=await fetch(path,{...options,credentials:'same-origin',cache:'no-store'});
63
+ if (response.status === 401) { sessionRequired=true; state=null; failure=''; return null; }
64
+ if (!response.ok) throw new Error('request failed');
65
+ return response.json();
66
+ }
67
+ async function refresh(sync=false) {
68
+ if (busy || document.hidden || sessionRequired) return;
69
+ busy=true; failure=''; render();
70
+ try {
71
+ if (sync) await request('/api/sync',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
72
+ if (!sessionRequired) { const next=await request('/api/status'); if (next && next.service === 'vault-go') state=next; else if (!sessionRequired) throw new Error('invalid status'); }
73
+ } catch { failure=sync ? 'syncError' : 'fetchError'; if (!sync) state=null; }
74
+ finally { busy=false; render(); if(pendingRefresh) {pendingRefresh=false; void refresh(true);} }
75
+ }
76
+ el('locale').addEventListener('change',event => {locale=event.target.value;try{localStorage.setItem('vault-local-locale',locale);}catch{}render();});
77
+ el('refresh').addEventListener('click',() => { if (busy) pendingRefresh=true; else void refresh(true); });
78
+ el('memory-search').addEventListener('input',() => list('memory')); el('project-search').addEventListener('input',() => list('project'));
79
+ document.querySelectorAll('.nav a').forEach(link => link.addEventListener('click',event => {event.preventDefault(); document.querySelectorAll('.nav a').forEach(n=>n.classList.remove('active'));link.classList.add('active');document.querySelector(link.getAttribute('href')).scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth'});}));
80
+ document.addEventListener('visibilitychange',() => {if(!document.hidden) void refresh();});
81
+ async function start() {
82
+ busy=true; render();
83
+ try { if(token && /^[A-Za-z0-9_-]{43}$/.test(token)) await request('/api/session',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token})}); }
84
+ catch { failure='fetchError'; }
85
+ finally {busy=false;}
86
+ if(!sessionRequired) await refresh(); else render();
87
+ setInterval(() => {void refresh();},5000);
88
+ }
89
+ void start();
90
+ })();`;
91
+ }
@@ -0,0 +1,17 @@
1
+ export declare const LOCAL_PORT = 38850;
2
+ export declare const localUrl = "http://localhost:38850/";
3
+ export interface LocalHealth {
4
+ service: 'vault-go';
5
+ version: string;
6
+ port: number;
7
+ }
8
+ export declare function localStatus(port?: number, home?: string): Promise<LocalHealth | null>;
9
+ export declare function stageLocalRuntime(home?: string, source?: string): string;
10
+ export declare function startLocal(home?: string): Promise<LocalHealth>;
11
+ export declare function openLocal(home?: string): Promise<void>;
12
+ export declare function stopLocal(home?: string): Promise<boolean>;
13
+ export declare function launchAgentXml(executable: string, worker: string, home: string): string;
14
+ export declare function installLocal(home?: string): Promise<{
15
+ startup: boolean;
16
+ }>;
17
+ export declare function uninstallLocal(home?: string): Promise<void>;
@@ -0,0 +1,169 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { promisify } from 'node:util';
8
+ import { vaultHome } from './config.js';
9
+ import { openVaultBrowser } from './auth.js';
10
+ const execute = promisify(execFile);
11
+ export const LOCAL_PORT = 38850;
12
+ const LABEL = 'com.resolveup.vault-go';
13
+ const OWNER = '<!-- Managed by vault-go local install -->';
14
+ export const localUrl = `http://localhost:${LOCAL_PORT}/`;
15
+ export async function localStatus(port = LOCAL_PORT, home = vaultHome()) {
16
+ const challenge = randomBytes(32).toString('base64url');
17
+ let response;
18
+ try {
19
+ response = await fetch(`http://127.0.0.1:${port}/health?challenge=${challenge}`, { signal: AbortSignal.timeout(1500), redirect: 'error' });
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ let body;
25
+ try {
26
+ body = await response.json();
27
+ }
28
+ catch {
29
+ throw new Error(`Port ${port} is occupied by another service.`);
30
+ }
31
+ if (!response.ok || body.service !== 'vault-go' || typeof body.version !== 'string' || !/^\d+\.\d+\.\d+/.test(body.version) || body.port !== port) {
32
+ throw new Error(`Port ${port} is occupied by another service.`);
33
+ }
34
+ const expected = createHmac('sha256', localToken(home)).update(challenge).digest();
35
+ const received = Buffer.from(body.proof ?? '', 'base64url');
36
+ if (expected.length !== received.length || !timingSafeEqual(expected, received))
37
+ throw new Error(`Could not verify the Vault service on port ${port}.`);
38
+ return { service: body.service, version: body.version, port: body.port };
39
+ }
40
+ export function stageLocalRuntime(home = vaultHome(), source = dirname(fileURLToPath(import.meta.url))) {
41
+ const manifest = JSON.parse(readFileSync(join(source, '..', 'package.json'), 'utf8'));
42
+ const root = join(home, 'local-runtime');
43
+ const target = join(root, 'dist');
44
+ mkdirSync(target, { recursive: true, mode: 0o700 });
45
+ chmodSync(root, 0o700);
46
+ chmodSync(target, 0o700);
47
+ const excluded = new Set(['index.js', 'server.js', 'installer.js', 'cli.js', 'auth.js']);
48
+ if (!existsSync(join(source, 'local-worker.js')))
49
+ throw new Error('Local worker is missing. Build or reinstall vault-go first.');
50
+ for (const file of readdirSync(source)) {
51
+ if (!file.endsWith('.js') || file.endsWith('.test.js') || excluded.has(file))
52
+ continue;
53
+ const destination = join(target, file);
54
+ copyFileSync(join(source, file), `${destination}.tmp`);
55
+ chmodSync(`${destination}.tmp`, 0o600);
56
+ renameSync(`${destination}.tmp`, destination);
57
+ }
58
+ writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'vault-go-local-runtime', version: manifest.version, type: 'module' }), { mode: 0o600 });
59
+ return join(target, 'local-worker.js');
60
+ }
61
+ export async function startLocal(home = vaultHome()) {
62
+ const running = await localStatus(LOCAL_PORT, home);
63
+ if (running)
64
+ return running;
65
+ const worker = stageLocalRuntime(home);
66
+ const log = openSync(join(home, 'local-service.log'), 'a', 0o600);
67
+ try {
68
+ const child = spawn(process.execPath, [worker], { detached: true, stdio: ['ignore', log, log], env: { ...process.env, VAULT_GO_HOME: home, VAULT_GO_LOCAL_PORT: String(LOCAL_PORT) } });
69
+ await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
70
+ child.unref();
71
+ }
72
+ finally {
73
+ closeSync(log);
74
+ }
75
+ for (let attempt = 0; attempt < 40; attempt++) {
76
+ await new Promise(resolve => setTimeout(resolve, 150));
77
+ const status = await localStatus(LOCAL_PORT, home);
78
+ if (status)
79
+ return status;
80
+ }
81
+ throw new Error(`Local service did not start. Check ${join(home, 'local-service.log')}.`);
82
+ }
83
+ function localToken(home) {
84
+ const value = JSON.parse(readFileSync(join(home, 'local-access.json'), 'utf8'));
85
+ if (!value.token || !/^[A-Za-z0-9_-]{32,}$/.test(value.token))
86
+ throw new Error('Local access token is missing or invalid.');
87
+ return value.token;
88
+ }
89
+ export async function openLocal(home = vaultHome()) {
90
+ await startLocal(home);
91
+ try {
92
+ await openVaultBrowser(`${localUrl}#token=${encodeURIComponent(localToken(home))}`);
93
+ }
94
+ catch {
95
+ throw new Error('Could not open the local dashboard. Retry: vault-go local open');
96
+ }
97
+ }
98
+ export async function stopLocal(home = vaultHome()) {
99
+ if (!await localStatus(LOCAL_PORT, home))
100
+ return false;
101
+ const result = await fetch(`http://127.0.0.1:${LOCAL_PORT}/api/shutdown`, { method: 'POST', headers: { Authorization: `Bearer ${localToken(home)}` }, signal: AbortSignal.timeout(3000), redirect: 'error' });
102
+ if (!result.ok)
103
+ throw new Error(`Local shutdown refused (${result.status}).`);
104
+ for (let attempt = 0; attempt < 20; attempt++) {
105
+ if (!await localStatus(LOCAL_PORT, home))
106
+ return true;
107
+ await new Promise(resolve => setTimeout(resolve, 100));
108
+ }
109
+ throw new Error('Local service has not stopped yet.');
110
+ }
111
+ export function launchAgentXml(executable, worker, home) {
112
+ const escape = (value) => value.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' }[c]));
113
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${OWNER}\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${LABEL}</string><key>ProgramArguments</key><array><string>${escape(executable)}</string><string>${escape(worker)}</string></array><key>EnvironmentVariables</key><dict><key>VAULT_GO_HOME</key><string>${escape(home)}</string><key>VAULT_GO_LOCAL_PORT</key><string>${LOCAL_PORT}</string></dict><key>RunAtLoad</key><true/><key>StandardOutPath</key><string>${escape(join(home, 'local-service.log'))}</string><key>StandardErrorPath</key><string>${escape(join(home, 'local-service.log'))}</string></dict></plist>\n`;
114
+ }
115
+ function agentPath() { return join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`); }
116
+ function assertOwned(path) { if (existsSync(path) && !readFileSync(path, 'utf8').includes(OWNER))
117
+ throw new Error(`Refusing to replace an unowned startup file: ${path}`); }
118
+ export async function installLocal(home = vaultHome()) {
119
+ const worker = stageLocalRuntime(home);
120
+ if (process.platform !== 'darwin') {
121
+ await startLocal(home);
122
+ return { startup: false };
123
+ }
124
+ const path = agentPath();
125
+ assertOwned(path);
126
+ await localStatus(LOCAL_PORT, home);
127
+ mkdirSync(dirname(path), { recursive: true });
128
+ const domain = `gui/${process.getuid()}`;
129
+ // An absent registration is expected on the first installation.
130
+ try {
131
+ await execute('launchctl', ['bootout', `${domain}/${LABEL}`]);
132
+ }
133
+ catch { }
134
+ await stopLocal(home);
135
+ writeFileSync(path, launchAgentXml(process.execPath, worker, home), { mode: 0o600 });
136
+ await execute('launchctl', ['bootstrap', domain, path]);
137
+ for (let attempt = 0; attempt < 40; attempt++) {
138
+ const health = await localStatus(LOCAL_PORT, home);
139
+ if (health)
140
+ return { startup: true };
141
+ await new Promise(resolve => setTimeout(resolve, 150));
142
+ }
143
+ throw new Error('LaunchAgent was registered, but the local service did not start. Check local-service.log.');
144
+ }
145
+ export async function uninstallLocal(home = vaultHome()) {
146
+ if (process.platform === 'darwin') {
147
+ const path = agentPath();
148
+ assertOwned(path);
149
+ if (existsSync(path)) {
150
+ try {
151
+ await execute('launchctl', ['bootout', `gui/${process.getuid()}/${LABEL}`]);
152
+ }
153
+ catch (error) {
154
+ // Only remove registration files after confirming the job is absent.
155
+ try {
156
+ await execute('launchctl', ['print', `gui/${process.getuid()}/${LABEL}`]);
157
+ }
158
+ catch {
159
+ rmSync(path);
160
+ await stopLocal(home);
161
+ return;
162
+ }
163
+ throw error;
164
+ }
165
+ rmSync(path);
166
+ }
167
+ }
168
+ await stopLocal(home);
169
+ }
@@ -0,0 +1,16 @@
1
+ import { type IncomingMessage } from 'node:http';
2
+ import { VaultCloudClient } from './cloud.js';
3
+ export declare const LOCAL_PORT = 38850;
4
+ export declare function localAccessToken(home: string): string;
5
+ type LocalCloud = Pick<VaultCloudClient, 'projects' | 'feed' | 'health'>;
6
+ export declare function startLocalService(options?: {
7
+ home?: string;
8
+ port?: number;
9
+ cloud?: LocalCloud;
10
+ pollMs?: number;
11
+ }): Promise<{
12
+ server: import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
13
+ port: number;
14
+ close: () => Promise<void>;
15
+ }>;
16
+ export {};
@@ -0,0 +1,217 @@
1
+ import { createServer } from 'node:http';
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
5
+ import { loadConfig, loadTokens, vaultHome } from './config.js';
6
+ import { VaultCloudClient } from './cloud.js';
7
+ import { readActivity } from './activity.js';
8
+ import { dashboardHtml, dashboardScript } from './local-dashboard.js';
9
+ import pkg from '../package.json' with { type: 'json' };
10
+ export const LOCAL_PORT = 38850;
11
+ const safeEqual = (a, b) => a.length === b.length && /^[A-Za-z0-9_-]{43}$/.test(a) && timingSafeEqual(Buffer.from(a), Buffer.from(b));
12
+ export function localAccessToken(home) {
13
+ mkdirSync(home, { recursive: true, mode: 0o700 });
14
+ chmodSync(home, 0o700);
15
+ const file = join(home, 'local-access.json');
16
+ if (!existsSync(file)) {
17
+ try {
18
+ writeFileSync(file, JSON.stringify({ token: randomBytes(32).toString('base64url') }), { mode: 0o600, flag: 'wx' });
19
+ }
20
+ catch (e) {
21
+ if (e.code !== 'EEXIST')
22
+ throw e;
23
+ }
24
+ }
25
+ chmodSync(file, 0o600);
26
+ const value = JSON.parse(readFileSync(file, 'utf8'));
27
+ if (typeof value.token !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(value.token))
28
+ throw new Error('Invalid local access file.');
29
+ return value.token;
30
+ }
31
+ function array(value) {
32
+ if (Array.isArray(value))
33
+ return value.slice(0, 100);
34
+ if (value && typeof value === 'object')
35
+ for (const key of ['items', 'memories', 'observations', 'projects', 'events', 'data']) {
36
+ const items = value[key];
37
+ if (Array.isArray(items))
38
+ return items.slice(0, 100);
39
+ }
40
+ return [];
41
+ }
42
+ async function readBody(req) {
43
+ const chunks = [];
44
+ let size = 0;
45
+ for await (const chunk of req) {
46
+ size += chunk.length;
47
+ if (size > 8192)
48
+ throw new Error('Body too large');
49
+ chunks.push(Buffer.from(chunk));
50
+ }
51
+ return JSON.parse(Buffer.concat(chunks).toString() || '{}');
52
+ }
53
+ export async function startLocalService(options = {}) {
54
+ const home = options.home ?? vaultHome();
55
+ const token = localAccessToken(home);
56
+ const cloud = options.cloud ?? new VaultCloudClient(home, fetch, false);
57
+ const sessions = new Map();
58
+ let port = options.port ?? LOCAL_PORT;
59
+ let connected = false, cloudError, updatedAt = null;
60
+ let projects = [], memories = [];
61
+ let owner = '';
62
+ let syncing = null;
63
+ let closed = false;
64
+ const cache = join(home, 'local-cache.json');
65
+ function identity() { const config = loadConfig(home); return createHash('sha256').update(config.apiUrl + '|' + (process.env['VAULT_GO_TOKEN'] ?? config.email ?? loadTokens(home)?.accessToken ?? '') + '|' + (loadTokens(home) || process.env['VAULT_GO_TOKEN'] ? 'authenticated' : 'anonymous')).digest('hex'); }
66
+ function resetOwner() { const next = identity(); if (next === owner)
67
+ return; owner = next; projects = []; memories = []; updatedAt = null; connected = false; cloudError = undefined; try {
68
+ const stored = JSON.parse(readFileSync(cache, 'utf8'));
69
+ if (stored.owner === owner && (loadTokens(home) || process.env['VAULT_GO_TOKEN'])) {
70
+ projects = array(stored.projects);
71
+ memories = array(stored.memories);
72
+ updatedAt = typeof stored.updatedAt === 'string' ? stored.updatedAt : null;
73
+ }
74
+ }
75
+ catch { } }
76
+ async function refresh() {
77
+ resetOwner();
78
+ if (syncing)
79
+ return syncing;
80
+ if (!loadTokens(home) && !process.env['VAULT_GO_TOKEN']) {
81
+ connected = false;
82
+ return;
83
+ }
84
+ const requestOwner = owner;
85
+ syncing = (async () => {
86
+ try {
87
+ const newProjects = await cloud.projects();
88
+ const newMemories = await cloud.feed({ limit: 50 });
89
+ if (closed || requestOwner !== identity())
90
+ return;
91
+ projects = array(newProjects);
92
+ memories = array(newMemories);
93
+ updatedAt = new Date().toISOString();
94
+ connected = true;
95
+ cloudError = undefined;
96
+ const temp = cache + '.tmp';
97
+ writeFileSync(temp, JSON.stringify({ owner, projects, memories, updatedAt }), { mode: 0o600 });
98
+ renameSync(temp, cache);
99
+ chmodSync(cache, 0o600);
100
+ }
101
+ catch {
102
+ if (requestOwner === identity()) {
103
+ connected = false;
104
+ cloudError = 'cloud_unavailable';
105
+ }
106
+ }
107
+ finally {
108
+ syncing = null;
109
+ }
110
+ })();
111
+ return syncing;
112
+ }
113
+ const server = createServer(async (req, res) => {
114
+ res.setHeader('Cache-Control', 'no-store');
115
+ res.setHeader('Referrer-Policy', 'no-referrer');
116
+ res.setHeader('X-Content-Type-Options', 'nosniff');
117
+ res.setHeader('X-Frame-Options', 'DENY');
118
+ res.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'");
119
+ const json = (status, value) => { res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(value)); };
120
+ const host = req.headers.host?.toLowerCase();
121
+ const allowed = [`localhost:${port}`, `127.0.0.1:${port}`];
122
+ if (!host || !allowed.includes(host)) {
123
+ json(403, { error: 'invalid_host' });
124
+ return;
125
+ }
126
+ const origin = req.headers.origin;
127
+ if (origin && !allowed.map(h => 'http://' + h).includes(origin)) {
128
+ json(403, { error: 'invalid_origin' });
129
+ return;
130
+ }
131
+ if (req.headers['sec-fetch-site'] === 'cross-site' && req.url?.startsWith('/api/')) {
132
+ json(403, { error: 'cross_site_request' });
133
+ return;
134
+ }
135
+ let url;
136
+ try {
137
+ url = new URL(req.url ?? '/', `http://${host}`);
138
+ }
139
+ catch {
140
+ json(400, { error: 'invalid_url' });
141
+ return;
142
+ }
143
+ if (url.pathname === '/health' && req.method === 'GET') {
144
+ const challenge = url.searchParams.get('challenge');
145
+ json(200, { service: 'vault-go', version: pkg.version, port, ...(challenge && /^[A-Za-z0-9_-]{43}$/.test(challenge) ? { proof: createHmac('sha256', token).update(challenge).digest('base64url') } : {}) });
146
+ return;
147
+ }
148
+ if (url.pathname === '/' && req.method === 'GET') {
149
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
150
+ res.end(dashboardHtml());
151
+ return;
152
+ }
153
+ if (url.pathname === '/app.js' && req.method === 'GET') {
154
+ res.setHeader('Content-Type', 'text/javascript; charset=utf-8');
155
+ res.end(dashboardScript());
156
+ return;
157
+ }
158
+ try {
159
+ if (url.pathname === '/api/session' && req.method === 'POST') {
160
+ const data = await readBody(req);
161
+ if (typeof data.token !== 'string' || !safeEqual(data.token, token)) {
162
+ json(401, { error: 'unauthorized' });
163
+ return;
164
+ }
165
+ const session = randomBytes(32).toString('base64url');
166
+ for (const [key, expiry] of sessions)
167
+ if (expiry < Date.now())
168
+ sessions.delete(key);
169
+ if (sessions.size >= 32)
170
+ sessions.delete(sessions.keys().next().value);
171
+ sessions.set(session, Date.now() + 12 * 60 * 60 * 1000);
172
+ res.setHeader('Set-Cookie', `vault_local_session=${session}; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200`);
173
+ json(200, { ok: true });
174
+ return;
175
+ }
176
+ const bearer = req.headers.authorization?.replace(/^Bearer /, '') ?? '';
177
+ const cookie = req.headers.cookie?.split(';').map(s => s.trim()).find(s => s.startsWith('vault_local_session='))?.slice('vault_local_session='.length) ?? '';
178
+ if (!safeEqual(bearer, token) && (sessions.get(cookie) ?? 0) <= Date.now()) {
179
+ json(401, { error: 'unauthorized' });
180
+ return;
181
+ }
182
+ resetOwner();
183
+ if (url.pathname === '/api/status' && req.method === 'GET') {
184
+ const config = loadConfig(home);
185
+ json(200, { service: 'vault-go', version: pkg.version, port, authenticated: Boolean(loadTokens(home) || process.env['VAULT_GO_TOKEN']), email: config.email, cloud: { connected, error: cloudError, syncing: Boolean(syncing) }, activity: readActivity(home), projects, memories, updatedAt });
186
+ return;
187
+ }
188
+ if (url.pathname === '/api/sync' && req.method === 'POST') {
189
+ await readBody(req);
190
+ await refresh();
191
+ json(200, { ok: connected });
192
+ return;
193
+ }
194
+ if (url.pathname === '/api/shutdown' && req.method === 'POST') {
195
+ json(200, { ok: true });
196
+ setTimeout(() => void close(), 30);
197
+ return;
198
+ }
199
+ json(404, { error: 'not_found' });
200
+ }
201
+ catch {
202
+ if (!res.headersSent)
203
+ json(400, { error: 'invalid_request' });
204
+ else
205
+ res.end();
206
+ }
207
+ });
208
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', resolve); });
209
+ const address = server.address();
210
+ if (address && typeof address !== 'string')
211
+ port = address.port;
212
+ const timer = setInterval(() => void refresh(), options.pollMs ?? 15_000);
213
+ timer.unref();
214
+ void refresh();
215
+ async function close() { closed = true; clearInterval(timer); sessions.clear(); server.closeAllConnections(); await new Promise(resolve => server.close(() => resolve())); }
216
+ return { server, port, close };
217
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { startLocalService } from './local-service.js';
2
+ const port = Number(process.env['VAULT_GO_LOCAL_PORT'] ?? 38850);
3
+ if (!Number.isInteger(port) || port < 1024 || port > 65535)
4
+ throw new Error('Invalid local port');
5
+ const local = await startLocalService({ port });
6
+ for (const signal of ['SIGINT', 'SIGTERM'])
7
+ process.once(signal, () => void local.close());
8
+ console.log(`AI Vault Memory local service · http://localhost:${local.port}`);