vault-go 0.18.0 → 0.19.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.
@@ -1,72 +1,146 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
2
- import { writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { VaultCloudClient } from "./cloud.js";
5
- import { vaultHome } from "./config.js";
6
- import { generateContext, selectedContextEngine, } from "./context-engines.js";
7
- function queuePath(home) {
8
- return join(home, "generation-queue.json");
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ import { VaultCloudClient } from './cloud.js';
5
+ import { loadConfig, loadTokens, vaultHome } from './config.js';
6
+ import { generateContext, selectedContextEngine } from './context-engines.js';
7
+ const queuePath = (home) => join(home, 'generation-queue.json');
8
+ function ownerKey(home) {
9
+ const config = loadConfig(home);
10
+ return createHash('sha256').update(JSON.stringify([config.apiUrl, process.env['VAULT_GO_TOKEN'] ?? config.email ?? loadTokens(home)?.accessToken ?? 'anonymous'])).digest('hex');
9
11
  }
10
12
  function readQueue(home) {
11
- try {
12
- const value = JSON.parse(readFileSync(queuePath(home), "utf8"));
13
- return Array.isArray(value) ? value : [];
14
- }
15
- catch {
13
+ if (!existsSync(queuePath(home)))
16
14
  return [];
17
- }
15
+ const value = JSON.parse(readFileSync(queuePath(home), 'utf8'));
16
+ if (!Array.isArray(value))
17
+ throw new Error('Invalid generation queue; source preserved.');
18
+ return value;
18
19
  }
19
20
  function writeQueue(home, jobs) {
20
21
  mkdirSync(home, { recursive: true, mode: 0o700 });
21
- const destination = queuePath(home);
22
- const temporary = destination + "." + process.pid + ".tmp";
23
- writeFileSync(temporary, JSON.stringify(jobs.slice(0, 50)) + "\n", { mode: 0o600 });
24
- chmodSync(temporary, 0o600);
22
+ const destination = queuePath(home), temporary = `${destination}.${randomUUID()}.tmp`;
23
+ writeFileSync(temporary, JSON.stringify(jobs) + '\n', { mode: 0o600 });
25
24
  renameSync(temporary, destination);
26
25
  chmodSync(destination, 0o600);
27
26
  }
28
- export function enqueueGeneration(home, job) {
27
+ // Separate short storage locks and a long consumer lock let hooks enqueue while a model runs.
28
+ async function lock(home, name, wait = true) {
29
+ mkdirSync(home, { recursive: true, mode: 0o700 });
30
+ const path = join(home, `${name}.lock`);
31
+ const token = randomUUID();
32
+ for (let attempt = 0; attempt < (wait ? 100 : 1); attempt++) {
33
+ try {
34
+ writeFileSync(path, JSON.stringify({ pid: process.pid, token }), { flag: 'wx', mode: 0o600 });
35
+ return () => {
36
+ try {
37
+ if (JSON.parse(readFileSync(path, 'utf8')).token === token)
38
+ rmSync(path, { force: true });
39
+ }
40
+ catch { /* already released; never remove another consumer's lock */ }
41
+ };
42
+ }
43
+ catch (error) {
44
+ if (error.code !== 'EEXIST')
45
+ throw error;
46
+ try {
47
+ const before = statSync(path);
48
+ const stored = JSON.parse(readFileSync(path, 'utf8'));
49
+ const pid = typeof stored === 'number' ? stored : Number(stored?.pid);
50
+ if (Number.isInteger(pid) && pid > 0) {
51
+ try {
52
+ process.kill(pid, 0);
53
+ }
54
+ catch (probe) {
55
+ if (probe.code === 'ESRCH') {
56
+ const after = statSync(path);
57
+ if (before.ino === after.ino && before.mtimeMs === after.mtimeMs)
58
+ rmSync(path, { force: true });
59
+ continue;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ catch { /* another process may have just released it */ }
65
+ if (wait)
66
+ await new Promise(resolve => setTimeout(resolve, 20));
67
+ }
68
+ }
69
+ if (wait)
70
+ throw new Error('Generation queue is busy; retry capture.');
71
+ }
72
+ async function mutate(home, change) {
73
+ const release = await lock(home, 'generation-storage');
74
+ try {
75
+ writeQueue(home, change(readQueue(home)));
76
+ }
77
+ finally {
78
+ release?.();
79
+ }
80
+ }
81
+ export async function enqueueGeneration(home, job) {
29
82
  if (!job.text.trim() || job.text.length > 16_000)
30
83
  return;
31
- const jobs = readQueue(home);
32
- if (jobs.some((item) => item.id === job.id))
33
- return;
34
- writeQueue(home, [...jobs, job]);
84
+ await mutate(home, jobs => {
85
+ if (jobs.some(item => item.id === job.id))
86
+ return jobs;
87
+ if (jobs.length >= 1000)
88
+ throw new Error('Generation queue is full; existing items preserved.');
89
+ return [...jobs, { ...job, owner: ownerKey(home) }];
90
+ });
35
91
  }
36
- export function takeGeneration(home) {
37
- const jobs = readQueue(home);
38
- const next = jobs[0];
39
- if (!next)
40
- return;
41
- writeQueue(home, jobs.slice(1));
42
- return next;
92
+ export function generationQueueStatus(home = vaultHome()) {
93
+ const jobs = readQueue(home), owner = ownerKey(home);
94
+ return { pending: jobs.filter(j => j.owner === owner || !j.owner).length, failed: jobs.filter(j => j.owner === owner && j.lastError).length, legacyAwaitingOwnership: jobs.filter(j => !j.owner).length, heldForOtherAccount: jobs.filter(j => j.owner && j.owner !== owner).length };
43
95
  }
44
96
  export async function processGenerationQueue(home = vaultHome(), cloud = new VaultCloudClient(home), generate = generateContext) {
45
- const job = takeGeneration(home);
46
- if (!job)
97
+ const release = await lock(home, 'generation-worker', false);
98
+ if (!release)
47
99
  return false;
48
- const engine = selectedContextEngine(home);
49
100
  try {
50
- const result = await generate(home, engine, job.text.slice(0, 16_000), {
51
- ...(cloud.observerGenerate
52
- ? {
53
- managedGenerate: (text, options) => cloud.observerGenerate(text, options.signal),
54
- }
55
- : {}),
56
- });
57
- await cloud.remember({
58
- projectId: job.projectId,
59
- ...(job.sessionId ? { sessionId: job.sessionId } : {}),
60
- kind: job.kind === "summary" ? "summary" : "observation",
61
- memoryType: "discovery",
62
- title: result.title || job.title,
63
- content: result.content,
64
- facts: result.facts,
65
- concepts: result.concepts,
66
- });
101
+ const owner = ownerKey(home);
102
+ if (readQueue(home).some(item => !item.owner)) {
103
+ // Pre-upgrade jobs have no account fingerprint. Adopt only projects proven
104
+ // to belong to the currently authenticated account; never infer from login.
105
+ const projects = await cloud.projects().catch(() => null);
106
+ if (Array.isArray(projects) && owner === ownerKey(home)) {
107
+ const owned = new Set(projects.flatMap(project => project && typeof project === 'object' && typeof project.id === 'string' ? [project.id] : []));
108
+ await mutate(home, jobs => jobs.map(item => !item.owner && owned.has(item.projectId) ? { ...item, owner } : item));
109
+ }
110
+ }
111
+ const job = readQueue(home).find(item => item.owner === owner && (item.nextAttemptAt ?? 0) <= Date.now());
112
+ if (!job)
113
+ return false;
114
+ try {
115
+ const result = job.result ?? (job.generate === false ? { title: job.title ?? 'Memória', content: job.text, facts: [], concepts: [] } : await generate(home, selectedContextEngine(home), job.text, {
116
+ ...(cloud.observerGenerate ? { managedGenerate: (text, options) => cloud.observerGenerate(text, options.signal) } : {}),
117
+ }));
118
+ if (owner !== ownerKey(home))
119
+ throw new Error('Account changed.');
120
+ // Cache successful generation before persistence, so a network retry does not pay for the model twice.
121
+ await mutate(home, jobs => jobs.map(item => item.id === job.id ? { ...item, result } : item));
122
+ await cloud.importMemories({ projectId: job.projectId, records: [{
123
+ sourceId: `vault-go:event:${job.id}`,
124
+ ...(job.sessionId ? { sessionId: job.sessionId } : {}),
125
+ kind: job.kind,
126
+ memoryType: 'discovery',
127
+ title: result.title || job.title,
128
+ content: result.content.slice(0, job.maxChars ?? 4000),
129
+ facts: result.facts, concepts: result.concepts,
130
+ filesRead: job.filesRead ?? [], filesModified: job.filesModified ?? [],
131
+ }] });
132
+ await mutate(home, jobs => jobs.filter(item => item.id !== job.id));
133
+ }
134
+ catch {
135
+ await mutate(home, jobs => jobs.map(item => item.id !== job.id ? item : {
136
+ ...item, attempts: (item.attempts ?? 0) + 1,
137
+ nextAttemptAt: Date.now() + Math.min(300_000, 5000 * 2 ** Math.min(item.attempts ?? 0, 6)),
138
+ lastError: 'generation_or_persistence_failed',
139
+ }));
140
+ }
141
+ return readQueue(home).some(item => item.owner === owner);
67
142
  }
68
- catch {
69
- // Drop poison items so the watcher keeps draining.
143
+ finally {
144
+ release();
70
145
  }
71
- return existsSync(queuePath(home)) && readQueue(home).length > 0;
72
146
  }
package/dist/hooks.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type VaultMemoryApi } from "./cloud.js";
2
2
  export declare const HOOK_EVENTS: readonly ["context", "session-init", "observation", "file-context", "summarize"];
3
3
  export type HookEvent = (typeof HOOK_EVENTS)[number];
4
4
  type RecordValue = Record<string, unknown>;
5
+ export declare function redact(value: unknown): unknown;
5
6
  export declare function normalizeHookInput(adapter: string, value: unknown): RecordValue;
6
7
  export declare function hookOutput(event: HookEvent, context?: string, adapter?: string): RecordValue;
7
8
  export declare function executeHook(adapter: string, event: HookEvent, value: unknown, cloud?: VaultMemoryApi, home?: string): Promise<RecordValue>;
package/dist/hooks.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { basename, dirname, resolve } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
4
  import { VaultCloudClient } from "./cloud.js";
@@ -26,10 +26,10 @@ function root(cwd) {
26
26
  }
27
27
  return initial;
28
28
  }
29
- function redact(value) {
29
+ export function redact(value) {
30
30
  if (typeof value === "string") {
31
31
  return value
32
- .replace(/<private>[\s\S]*?<\/private>/gi, "[redacted]")
32
+ .replace(/<(private|vault-private|claude-mem-context|system_instruction|system-instruction|persisted-output|system-reminder)\b[^>]*>[\s\S]*?(?:<\/\1\s*>|$)/gi, "[redacted]")
33
33
  .replace(/(api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[^\s'"]+/gi, "$1=[redacted]");
34
34
  }
35
35
  if (Array.isArray(value))
@@ -37,7 +37,7 @@ function redact(value) {
37
37
  if (value && typeof value === "object") {
38
38
  const output = {};
39
39
  for (const [key, nested] of Object.entries(value)) {
40
- output[key] = /api[_-]?key|secret|token|password|authorization/i.test(key)
40
+ output[key] = /api[_-]?key|secret|token|password|authorization|cookie|credential|private[_-]?key/i.test(key)
41
41
  ? "[redacted]"
42
42
  : redact(nested);
43
43
  }
@@ -98,7 +98,7 @@ async function preferences(cloud) {
98
98
  /* use defaults */
99
99
  }
100
100
  return {
101
- automaticCapture: true,
101
+ automaticCapture: false,
102
102
  captureMode: "balanced",
103
103
  includeToolResults: false,
104
104
  contextItems: 20,
@@ -110,6 +110,7 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
110
110
  const toolName = string(input.tool_name);
111
111
  if (input.private === true ||
112
112
  input.is_private === true ||
113
+ input.isPrivate === true ||
113
114
  /vault[_-]go|vault[_-]mem/.test(toolName)) {
114
115
  return hookOutput(event, "", adapter);
115
116
  }
@@ -135,7 +136,9 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
135
136
  throw new Error("Resposta de projetos inválida");
136
137
  let project = projects.find((item) => item &&
137
138
  typeof item === "object" &&
138
- resolve(String(item.rootPath || "")) === cwd);
139
+ typeof item.rootPath === "string" &&
140
+ Boolean(String(item.rootPath).trim()) &&
141
+ resolve(String(item.rootPath)) === cwd);
139
142
  if (!project && reading)
140
143
  return hookOutput(event, "", adapter);
141
144
  if (!project) {
@@ -148,8 +151,8 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
148
151
  throw new Error("Projeto inválido");
149
152
  const limits = {
150
153
  projectId: project.id,
151
- limit: 20,
152
- maxChars: 24_000,
154
+ limit: boundedPreference(prefs.contextItems, 20, 1, 100),
155
+ maxChars: boundedPreference(prefs.contextMaxChars, 24_000, 1_000, 100_000),
153
156
  };
154
157
  if (reading) {
155
158
  const result = record(event === "file-context"
@@ -158,7 +161,7 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
158
161
  paths: [...new Set([file, resolve(cwd, file)])],
159
162
  })
160
163
  : await cloud.context(limits));
161
- return hookOutput(event, string(result.context).slice(0, 24_000), adapter);
164
+ return hookOutput(event, string(result.context).slice(0, limits.maxChars), adapter);
162
165
  }
163
166
  const session = record(await cloud.startSession({
164
167
  projectId: project.id,
@@ -169,14 +172,18 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
169
172
  throw new Error("Sessão inválida");
170
173
  const prompt = string(input.prompt);
171
174
  const response = string(input.last_assistant_message);
175
+ const toolPayload = record(redact({
176
+ toolName,
177
+ toolInput: tool,
178
+ ...(prefs.includeToolResults === true ? {
179
+ toolResponse: input.tool_response ?? input.toolResponse ?? input.tool_result ?? input.toolResult ?? input.result ?? null,
180
+ } : {}),
181
+ }));
172
182
  const rawContent = event === "session-init"
173
183
  ? prompt
174
184
  : event === "summarize"
175
185
  ? response
176
- : JSON.stringify(redact({
177
- toolName,
178
- toolInput: tool,
179
- }));
186
+ : JSON.stringify(toolPayload);
180
187
  const content = String(redact(rawContent));
181
188
  if (content.trim()) {
182
189
  const eventType = event === "session-init"
@@ -193,6 +200,8 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
193
200
  input.tool_use_id,
194
201
  input.turn_id,
195
202
  input.timestamp,
203
+ string(input.tool_use_id, input.turn_id, input.timestamp) ? undefined : randomUUID(),
204
+ content,
196
205
  ]))
197
206
  .digest("hex");
198
207
  await cloud.event({
@@ -201,7 +210,11 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
201
210
  sourceAdapter: adapter,
202
211
  sourceEventId,
203
212
  eventType,
213
+ generate: false,
204
214
  payload: {
215
+ ...(event === 'observation' ? toolPayload : {}),
216
+ summaryMode: 'off',
217
+ metadata: { summaryMode: 'off' },
205
218
  title: toolName ||
206
219
  (event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
207
220
  content,
@@ -210,20 +223,27 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
210
223
  },
211
224
  occurredAtEpoch: Date.now(),
212
225
  });
213
- enqueueGeneration(home, {
226
+ await enqueueGeneration(home, {
214
227
  id: sourceEventId,
228
+ generate: prefs.summaryMode !== 'off',
215
229
  projectId: project.id,
216
230
  sessionId: session.id,
217
231
  text: content.slice(0, 16_000),
218
232
  title: toolName ||
219
233
  (event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
220
234
  kind: event === "summarize" ? "summary" : event === "session-init" ? "prompt" : "observation",
235
+ maxChars: boundedPreference(prefs.summaryMaxChars, 4_000, 256, 16_000),
236
+ filesRead: !modifies && file ? [file] : [],
237
+ filesModified: modifies && file ? [file] : [],
221
238
  });
222
239
  }
223
240
  if (event === "summarize")
224
241
  await cloud.endSession(session.id);
225
242
  return hookOutput(event, "", adapter);
226
243
  }
244
+ function boundedPreference(value, fallback, min, max) {
245
+ return typeof value === 'number' && Number.isInteger(value) ? Math.max(min, Math.min(max, value)) : fallback;
246
+ }
227
247
  export async function readHookInput(stream = process.stdin) {
228
248
  const chunks = [];
229
249
  let size = 0;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { cliArguments, helpText, resolveCliMode, runEngine, runLocal, runLogin, runSetup } from './cli.js';
3
+ import { cliArguments, helpText, resolveCliMode, runEngine, runLocal, runLogin, runSetup, runUpdate } from './cli.js';
4
4
  import { HOOK_EVENTS, runHook } from './hooks.js';
5
5
  import { createVaultGoServer, VERSION } from './server.js';
6
6
  const { argument, options } = cliArguments(process.argv.slice(2));
@@ -33,6 +33,10 @@ async function main() {
33
33
  process.exitCode = await runEngine(options);
34
34
  return;
35
35
  }
36
+ if (mode === 'update') {
37
+ process.exitCode = await runUpdate(options);
38
+ return;
39
+ }
36
40
  if (mode === 'hook') {
37
41
  const adapter = options.find((item) => !item.startsWith('--')) ?? 'auto';
38
42
  const event = options.filter((item) => !item.startsWith('--'))[1];
@@ -1,5 +1,5 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
- import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
3
3
  import { chmodSync, copyFileSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
@@ -46,42 +46,51 @@ export async function localStatus(port = LOCAL_PORT, home = vaultHome()) {
46
46
  }
47
47
  export function stageLocalRuntime(home = vaultHome(), source = dirname(fileURLToPath(import.meta.url))) {
48
48
  const manifest = JSON.parse(readFileSync(join(source, "..", "package.json"), "utf8"));
49
- const root = join(home, "local-runtime");
50
- const target = join(root, "dist");
51
- mkdirSync(target, { recursive: true, mode: 0o700 });
52
- chmodSync(root, 0o700);
53
- chmodSync(target, 0o700);
54
49
  if (!existsSync(join(source, "local-worker.js")))
55
50
  throw new Error("Local worker is missing. Build or reinstall vault-go first.");
56
- for (const file of readdirSync(source)) {
57
- if (!file.endsWith(".js") || file.endsWith(".test.js"))
58
- continue;
59
- const destination = join(target, file);
60
- copyFileSync(join(source, file), `${destination}.tmp`);
61
- chmodSync(`${destination}.tmp`, 0o600);
62
- renameSync(`${destination}.tmp`, destination);
51
+ if (!existsSync(join(source, "hook-runner.js")))
52
+ throw new Error("Hook runner is missing. Build or reinstall vault-go first.");
53
+ const root = join(home, "local-runtime"), candidate = join(home, `.runtime-${randomUUID()}`), previous = join(home, "local-runtime.previous");
54
+ const target = join(candidate, "dist");
55
+ mkdirSync(target, { recursive: true, mode: 0o700 });
56
+ chmodSync(candidate, 0o700);
57
+ try {
58
+ for (const file of readdirSync(source)) {
59
+ if (!file.endsWith(".js") || file.endsWith(".test.js"))
60
+ continue;
61
+ copyFileSync(join(source, file), join(target, file));
62
+ chmodSync(join(target, file), 0o600);
63
+ }
64
+ const logo = join(source, "..", "assets", "vault-logo-glass.png");
65
+ if (existsSync(logo)) {
66
+ mkdirSync(join(candidate, "assets"), { mode: 0o700 });
67
+ copyFileSync(logo, join(candidate, "assets", "vault-logo-glass.png"));
68
+ chmodSync(join(candidate, "assets", "vault-logo-glass.png"), 0o600);
69
+ }
70
+ writeFileSync(join(candidate, "package.json"), JSON.stringify({ name: "vault-go-local-runtime", version: manifest.version, type: "module" }), { mode: 0o600 });
71
+ rmSync(previous, { recursive: true, force: true });
72
+ if (existsSync(root))
73
+ renameSync(root, previous);
74
+ try {
75
+ renameSync(candidate, root);
76
+ }
77
+ catch (error) {
78
+ if (existsSync(previous))
79
+ renameSync(previous, root);
80
+ throw error;
81
+ }
63
82
  }
64
- const assets = join(root, "assets");
65
- mkdirSync(assets, { recursive: true, mode: 0o700 });
66
- const logo = join(source, "..", "assets", "vault-logo-glass.png");
67
- if (existsSync(logo)) {
68
- copyFileSync(logo, join(assets, "vault-logo-glass.png"));
69
- chmodSync(join(assets, "vault-logo-glass.png"), 0o600);
83
+ finally {
84
+ rmSync(candidate, { recursive: true, force: true });
70
85
  }
71
- writeFileSync(join(root, "package.json"), JSON.stringify({
72
- name: "vault-go-local-runtime",
73
- version: manifest.version,
74
- type: "module",
75
- }), { mode: 0o600 });
76
- if (!existsSync(join(target, "hook-runner.js")))
77
- throw new Error("Hook runner is missing. Build or reinstall vault-go first.");
78
- return join(target, "local-worker.js");
86
+ return join(root, "dist", "local-worker.js");
79
87
  }
80
88
  export async function startLocal(home = vaultHome()) {
81
89
  const running = await localStatus(LOCAL_PORT, home);
82
90
  if (running)
83
91
  return running;
84
- const worker = stageLocalRuntime(home);
92
+ const stagedWorker = join(home, "local-runtime", "dist", "local-worker.js");
93
+ const worker = existsSync(stagedWorker) ? stagedWorker : stageLocalRuntime(home);
85
94
  const log = openSync(join(home, "local-service.log"), "a", 0o600);
86
95
  try {
87
96
  const child = spawn(process.execPath, [worker], {
@@ -22,5 +22,6 @@ export declare function startLocalService(options?: {
22
22
  server: import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
23
23
  port: number;
24
24
  close: () => Promise<void>;
25
+ isIdle: () => boolean;
25
26
  }>;
26
27
  export {};
@@ -260,7 +260,7 @@ export async function startLocalService(options = {}) {
260
260
  cloudError = undefined;
261
261
  storage?.write(cache, { owner, projects, memories, updatedAt });
262
262
  if (!generating && !options.cloud) {
263
- generating = processGenerationQueue(home, accountCloud).finally(() => {
263
+ generating = processGenerationQueue(home, accountCloud).catch(() => { cloudError = "generation_queue_unavailable"; }).finally(() => {
264
264
  generating = null;
265
265
  });
266
266
  }
@@ -657,5 +657,5 @@ export async function startLocalService(options = {}) {
657
657
  await new Promise((resolve) => server.close(() => resolve()));
658
658
  storage?.close();
659
659
  }
660
- return { server, port, close };
660
+ return { server, port, close, isIdle: () => !generating && !syncing && login?.status !== "pending" && [...jobs.values()].every(job => job.status !== "running") };
661
661
  }
@@ -1,8 +1,10 @@
1
1
  import { startLocalService } from './local-service.js';
2
+ import { scheduleAutomaticUpdates } from './update-control.js';
2
3
  const port = Number(process.env['VAULT_GO_LOCAL_PORT'] ?? 38850);
3
4
  if (!Number.isInteger(port) || port < 1024 || port > 65535)
4
5
  throw new Error('Invalid local port');
5
6
  const local = await startLocalService({ port });
7
+ const stopUpdates = scheduleAutomaticUpdates(local.isIdle);
6
8
  for (const signal of ['SIGINT', 'SIGTERM'])
7
- process.once(signal, () => void local.close());
9
+ process.once(signal, () => { stopUpdates(); void local.close(); });
8
10
  console.log(`AI Vault Memory local service · http://localhost:${local.port}`);
@@ -0,0 +1,20 @@
1
+ export declare function jsonResult(value: unknown): {
2
+ content: {
3
+ type: "text";
4
+ text: string;
5
+ }[];
6
+ structuredContent: Record<string, unknown>;
7
+ };
8
+ export declare function run(operation: () => Promise<unknown>): Promise<{
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ structuredContent: Record<string, unknown>;
14
+ } | {
15
+ content: {
16
+ type: "text";
17
+ text: string;
18
+ }[];
19
+ isError: boolean;
20
+ }>;
@@ -0,0 +1,21 @@
1
+ export function jsonResult(value) {
2
+ const structuredContent = value !== null && typeof value === 'object' && !Array.isArray(value)
3
+ ? value
4
+ : { value };
5
+ return {
6
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
7
+ structuredContent,
8
+ };
9
+ }
10
+ function errorResult(error) {
11
+ const message = error instanceof Error ? error.message : 'Erro inesperado.';
12
+ return { content: [{ type: 'text', text: message }], isError: true };
13
+ }
14
+ export async function run(operation) {
15
+ try {
16
+ return jsonResult(await operation());
17
+ }
18
+ catch (error) {
19
+ return errorResult(error);
20
+ }
21
+ }
@@ -0,0 +1,17 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare const MODE_NAMES: readonly ["babysit", "mem-search", "how-it-works", "make-plan", "do", "smart-explore", "knowledge-agent", "standup", "timeline-report", "weekly-digests", "learn-codebase", "pathfinder", "design-is", "what-the", "version-bump", "oh-my-issues", "cloud-sync", "mode-creator", "ccs-align", "wowerpoint"];
3
+ export type VaultModeName = (typeof MODE_NAMES)[number];
4
+ export type VaultModeStatus = 'native' | 'instructional' | 'limited';
5
+ export interface VaultMode {
6
+ name: VaultModeName;
7
+ promptName: string;
8
+ title: string;
9
+ summary: string;
10
+ status: VaultModeStatus;
11
+ readOnlyDefault: boolean;
12
+ runtimeRequires: readonly string[];
13
+ limitations: readonly string[];
14
+ steps: readonly string[];
15
+ }
16
+ export declare const VAULT_MODES: readonly VaultMode[];
17
+ export declare function registerVaultModes(server: McpServer): void;