vault-go 0.20.0 → 0.22.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.
Files changed (66) hide show
  1. package/README.md +58 -9
  2. package/dist/awareness-tools.d.ts +4 -0
  3. package/dist/awareness-tools.js +38 -0
  4. package/dist/backup.d.ts +41 -0
  5. package/dist/backup.js +117 -0
  6. package/dist/capabilities.d.ts +218 -25
  7. package/dist/capabilities.js +46 -16
  8. package/dist/capture-privacy.d.ts +1 -0
  9. package/dist/capture-privacy.js +19 -0
  10. package/dist/ccs-align.d.ts +122 -0
  11. package/dist/ccs-align.js +376 -0
  12. package/dist/cli.d.ts +3 -1
  13. package/dist/cli.js +52 -0
  14. package/dist/cloud.d.ts +15 -2
  15. package/dist/cloud.js +62 -4
  16. package/dist/context-chain.d.ts +37 -0
  17. package/dist/context-chain.js +98 -0
  18. package/dist/context-engines.d.ts +6 -0
  19. package/dist/context-engines.js +28 -8
  20. package/dist/crystal-tools.d.ts +9 -0
  21. package/dist/crystal-tools.js +106 -0
  22. package/dist/custom-modes.d.ts +59 -0
  23. package/dist/custom-modes.js +249 -0
  24. package/dist/deployment-verification.d.ts +19 -0
  25. package/dist/deployment-verification.js +171 -0
  26. package/dist/grok-awareness.d.ts +51 -0
  27. package/dist/grok-awareness.js +244 -0
  28. package/dist/hook-install.d.ts +7 -2
  29. package/dist/hook-install.js +71 -15
  30. package/dist/hook-queue.d.ts +6 -2
  31. package/dist/hook-queue.js +23 -4
  32. package/dist/hooks.d.ts +4 -3
  33. package/dist/hooks.js +83 -32
  34. package/dist/index.js +9 -1
  35. package/dist/installer.js +11 -1
  36. package/dist/knowledge-agent.d.ts +39 -0
  37. package/dist/knowledge-agent.js +143 -0
  38. package/dist/knowledge-tools.d.ts +3 -0
  39. package/dist/knowledge-tools.js +19 -0
  40. package/dist/local-service.js +6 -1
  41. package/dist/mode-integrations.d.ts +9 -0
  42. package/dist/mode-integrations.js +149 -0
  43. package/dist/modes.js +2 -2
  44. package/dist/opencode-plugin.d.ts +16 -0
  45. package/dist/opencode-plugin.js +73 -0
  46. package/dist/parity-tools.js +4 -4
  47. package/dist/server.js +56 -8
  48. package/dist/smart-code.d.ts +12 -16
  49. package/dist/smart-code.js +139 -28
  50. package/dist/telegram.d.ts +39 -0
  51. package/dist/telegram.js +138 -0
  52. package/dist/transcript-config.d.ts +41 -0
  53. package/dist/transcript-config.js +97 -0
  54. package/dist/transcript-parser.d.ts +24 -0
  55. package/dist/transcript-parser.js +118 -0
  56. package/dist/transcript-tools.d.ts +3 -0
  57. package/dist/transcript-tools.js +21 -0
  58. package/dist/transcript-watchers.d.ts +13 -0
  59. package/dist/transcript-watchers.js +311 -0
  60. package/dist/update-control.d.ts +2 -0
  61. package/dist/update-control.js +19 -2
  62. package/docs/ast-and-awareness.md +72 -0
  63. package/docs/mode-integrations.md +74 -0
  64. package/docs/parity-deployment-review.md +60 -0
  65. package/docs/transcripts-and-context.md +75 -0
  66. package/package.json +62 -2
package/dist/hooks.js CHANGED
@@ -1,9 +1,14 @@
1
+ import { redact } from './capture-privacy.js';
2
+ export { redact } from './capture-privacy.js';
1
3
  import { createHash, randomUUID } from "node:crypto";
2
- import { basename, dirname, resolve } from "node:path";
3
- import { existsSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
5
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
6
+ import { applyCustomModeContext } from './custom-modes.js';
4
7
  import { VaultCloudClient } from "./cloud.js";
5
8
  import { vaultHome } from "./config.js";
6
9
  import { enqueueGeneration } from "./hook-queue.js";
10
+ import { transcriptCaptureEnabled } from './transcript-config.js';
11
+ import { loadProjectContext } from './context-chain.js';
7
12
  export const HOOK_EVENTS = [
8
13
  "context",
9
14
  "session-init",
@@ -16,6 +21,14 @@ const record = (value) => value && typeof value === "object" && !Array.isArray(v
16
21
  : {};
17
22
  const string = (...values) => values.find((value) => typeof value === "string" && value.trim()) ||
18
23
  "";
24
+ function resolvedPath(path) {
25
+ try {
26
+ return realpathSync(resolve(path));
27
+ }
28
+ catch {
29
+ return resolve(path);
30
+ }
31
+ }
19
32
  function root(cwd) {
20
33
  const initial = resolve(cwd);
21
34
  let dir = initial;
@@ -26,24 +39,42 @@ function root(cwd) {
26
39
  }
27
40
  return initial;
28
41
  }
29
- export function redact(value) {
30
- if (typeof value === "string") {
31
- return value
32
- .replace(/<(private|vault-private|claude-mem-context|system_instruction|system-instruction|persisted-output|system-reminder)\b[^>]*>[\s\S]*?(?:<\/\1\s*>|$)/gi, "[redacted]")
33
- .replace(/(api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[^\s'"]+/gi, "$1=[redacted]");
42
+ function gitMetadata(path) {
43
+ const file = statSync(path);
44
+ if (!file.isFile() || file.size > 4096)
45
+ return "";
46
+ return readFileSync(path, "utf8").trim();
47
+ }
48
+ function projectRoots(workspace) {
49
+ const fallback = [resolvedPath(workspace)];
50
+ try {
51
+ const marker = /^gitdir:\s*(.+)$/.exec(gitMetadata(resolve(workspace, ".git")));
52
+ if (!marker)
53
+ return fallback;
54
+ const gitdir = resolve(workspace, marker[1]);
55
+ const common = gitMetadata(resolve(gitdir, "commondir"));
56
+ // A submodule or separate git directory has no shared worktree metadata.
57
+ if (!common)
58
+ return fallback;
59
+ const shared = resolvedPath(resolve(gitdir, common));
60
+ if (basename(shared) !== ".git" || !statSync(shared).isDirectory())
61
+ return fallback;
62
+ return [...new Set([dirname(shared), ...fallback])];
34
63
  }
35
- if (Array.isArray(value))
36
- return value.map(redact);
37
- if (value && typeof value === "object") {
38
- const output = {};
39
- for (const [key, nested] of Object.entries(value)) {
40
- output[key] = /api[_-]?key|secret|token|password|authorization|cookie|credential|private[_-]?key/i.test(key)
41
- ? "[redacted]"
42
- : redact(nested);
64
+ catch {
65
+ // Keep existing projects usable if Git metadata is absent or unreadable.
66
+ return fallback;
67
+ }
68
+ }
69
+ function canonicalFile(file, workspace, project) {
70
+ const absolute = resolve(workspace, file);
71
+ for (const directory of [workspace, resolvedPath(workspace)]) {
72
+ const path = relative(directory, absolute);
73
+ if (path !== ".." && !path.startsWith("../") && !path.startsWith("..\\") && !isAbsolute(path)) {
74
+ return resolve(project, path);
43
75
  }
44
- return output;
45
76
  }
46
- return value;
77
+ return absolute;
47
78
  }
48
79
  export function normalizeHookInput(adapter, value) {
49
80
  const input = record(value);
@@ -58,7 +89,7 @@ export function normalizeHookInput(adapter, value) {
58
89
  cwd: string(input.cwd, input.workspace, input.workspaceRoot, workspace),
59
90
  tool_name: string(input.tool_name, input.toolName, toolCall.name),
60
91
  tool_input: record(input.tool_input ?? input.toolInput ?? toolCall.args),
61
- last_assistant_message: string(input.last_assistant_message, input.lastAssistantMessage, input.response, input.summary),
92
+ last_assistant_message: string(input.last_assistant_message, input.lastAssistantMessage, input.prompt_response, input.response, input.summary),
62
93
  prompt: string(input.prompt, input.user_prompt, input.userPrompt),
63
94
  };
64
95
  }
@@ -73,19 +104,24 @@ export function hookOutput(event, context = "", adapter = "claude") {
73
104
  return { decision: "allow" };
74
105
  return {};
75
106
  }
76
- const result = { continue: true, suppressOutput: true };
107
+ const result = adapter === "codex"
108
+ ? {}
109
+ : { continue: true, suppressOutput: true };
77
110
  const name = event === "context"
78
111
  ? "SessionStart"
79
112
  : event === "file-context"
80
113
  ? "PreToolUse"
81
114
  : event === "session-init"
82
- ? "UserPromptSubmit"
115
+ ? adapter === "gemini" ? "BeforeAgent" : "UserPromptSubmit"
83
116
  : "";
84
117
  if (name && context) {
85
118
  result.hookSpecificOutput = {
86
119
  hookEventName: name,
87
120
  additionalContext: context,
88
121
  };
122
+ if (event === "context" && ["claude", "codex", "gemini"].includes(adapter)) {
123
+ result.systemMessage = "Vault Memory: contexto do projeto carregado.";
124
+ }
89
125
  }
90
126
  return result;
91
127
  }
@@ -105,8 +141,10 @@ async function preferences(cloud) {
105
141
  contextMaxChars: 24_000,
106
142
  };
107
143
  }
108
- export async function executeHook(adapter, event, value, cloud = new VaultCloudClient(), home = vaultHome()) {
144
+ export async function executeHook(adapter, event, value, cloud = new VaultCloudClient(), home = vaultHome(), options = {}) {
109
145
  const input = normalizeHookInput(adapter, value);
146
+ if (event !== 'context' && event !== 'file-context' && !options.transcript && transcriptCaptureEnabled(home, adapter, string(input.cwd, input.workspace) || process.cwd()))
147
+ return hookOutput(event, '', adapter);
110
148
  const toolName = string(input.tool_name);
111
149
  if (input.private === true ||
112
150
  input.is_private === true ||
@@ -130,15 +168,17 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
130
168
  const sessionId = string(input.session_id);
131
169
  if (!reading && !sessionId)
132
170
  return hookOutput(event, "", adapter);
133
- const cwd = root(string(input.cwd, input.workspace) || process.cwd());
171
+ const workspace = root(string(input.cwd, input.workspace) || process.cwd());
172
+ const roots = projectRoots(workspace);
134
173
  const projects = await cloud.projects();
135
174
  if (!Array.isArray(projects))
136
175
  throw new Error("Resposta de projetos inválida");
137
- let project = projects.find((item) => item &&
176
+ let project = roots.map((cwd) => projects.find((item) => item &&
138
177
  typeof item === "object" &&
139
178
  typeof item.rootPath === "string" &&
140
179
  Boolean(String(item.rootPath).trim()) &&
141
- resolve(String(item.rootPath)) === cwd);
180
+ resolvedPath(String(item.rootPath)) === cwd)).find(Boolean);
181
+ const cwd = project ? resolvedPath(String(project.rootPath)) : roots[0];
142
182
  if (!project && reading)
143
183
  return hookOutput(event, "", adapter);
144
184
  if (!project) {
@@ -154,14 +194,23 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
154
194
  limit: boundedPreference(prefs.contextItems, 20, 1, 100),
155
195
  maxChars: boundedPreference(prefs.contextMaxChars, 24_000, 1_000, 100_000),
156
196
  };
197
+ const projectFile = file ? canonicalFile(file, workspace, cwd) : "";
198
+ const files = file
199
+ ? [...new Set([file, ...(resolvedPath(workspace) !== cwd ? [projectFile] : [])])]
200
+ : [];
157
201
  if (reading) {
158
202
  const result = record(event === "file-context"
159
203
  ? await cloud.fileContext({
160
204
  ...limits,
161
- paths: [...new Set([file, resolve(cwd, file)])],
205
+ paths: [...new Set([file, resolve(workspace, file), projectFile])],
162
206
  })
163
- : await cloud.context(limits));
164
- return hookOutput(event, string(result.context).slice(0, limits.maxChars), adapter);
207
+ : { context: await loadProjectContext(home, cloud, { workspace, primary: { id: project.id }, projects, ...limits }) });
208
+ let context = string(result.context).slice(0, limits.maxChars);
209
+ try {
210
+ context = applyCustomModeContext(home, context, limits.maxChars);
211
+ }
212
+ catch { /* invalid local mode never blocks cloud context */ }
213
+ return hookOutput(event, context, adapter);
165
214
  }
166
215
  const session = record(await cloud.startSession({
167
216
  projectId: project.id,
@@ -204,6 +253,7 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
204
253
  content,
205
254
  ]))
206
255
  .digest("hex");
256
+ const timestamp = options.transcript && typeof input.timestamp === 'string' ? Date.parse(input.timestamp) : NaN;
207
257
  await cloud.event({
208
258
  projectId: project.id,
209
259
  sessionId: session.id,
@@ -218,23 +268,24 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
218
268
  title: toolName ||
219
269
  (event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
220
270
  content,
221
- filesRead: !modifies && file ? [file] : [],
222
- filesModified: modifies && file ? [file] : [],
271
+ filesRead: !modifies ? files : [],
272
+ filesModified: modifies ? files : [],
223
273
  },
224
- occurredAtEpoch: Date.now(),
274
+ occurredAtEpoch: Number.isFinite(timestamp) && timestamp >= 0 && timestamp <= Date.now() + 300_000 ? timestamp : Date.now(),
225
275
  });
226
276
  await enqueueGeneration(home, {
227
277
  id: sourceEventId,
228
278
  generate: prefs.summaryMode !== 'off',
229
279
  projectId: project.id,
230
280
  sessionId: session.id,
281
+ ...(string(input.agent_id, input.agentId) ? { agentId: string(input.agent_id, input.agentId) } : {}),
231
282
  text: content.slice(0, 16_000),
232
283
  title: toolName ||
233
284
  (event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
234
285
  kind: event === "summarize" ? "summary" : event === "session-init" ? "prompt" : "observation",
235
286
  maxChars: boundedPreference(prefs.summaryMaxChars, 4_000, 256, 16_000),
236
- filesRead: !modifies && file ? [file] : [],
237
- filesModified: modifies && file ? [file] : [],
287
+ filesRead: !modifies ? files : [],
288
+ filesModified: modifies ? files : [],
238
289
  });
239
290
  }
240
291
  if (event === "summarize")
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, runUpdate } from './cli.js';
3
+ import { cliArguments, helpText, resolveCliMode, runEngine, runLocal, runLogin, runSetup, runUpdate, runTranscripts, runContextChain } 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));
@@ -37,6 +37,14 @@ async function main() {
37
37
  process.exitCode = await runUpdate(options);
38
38
  return;
39
39
  }
40
+ if (mode === 'transcripts') {
41
+ process.exitCode = await runTranscripts(options);
42
+ return;
43
+ }
44
+ if (mode === 'context-chain') {
45
+ process.exitCode = await runContextChain(options);
46
+ return;
47
+ }
40
48
  if (mode === 'hook') {
41
49
  const adapter = options.find((item) => !item.startsWith('--')) ?? 'auto';
42
50
  const event = options.filter((item) => !item.startsWith('--'))[1];
package/dist/installer.js CHANGED
@@ -4,7 +4,7 @@ import { dirname, join, resolve } from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { vaultHome } from './config.js';
6
6
  import { stageLocalRuntime } from './local-install.js';
7
- import { installClientHooks } from './hook-install.js';
7
+ import { installClientHooks, installOpenCodePlugin } from './hook-install.js';
8
8
  export const MCP_SERVER_NAME = 'vault-go';
9
9
  export const MCP_SERVER_COMMAND = ['bunx', '--bun', 'vault-go@latest', 'serve'];
10
10
  export const MCP_CLIENTS = [
@@ -199,6 +199,11 @@ export function installMcpClient(client, options = {}) {
199
199
  if (client === 'gemini') {
200
200
  const path = join(home, '.gemini', 'settings.json');
201
201
  mergeServer(path);
202
+ try {
203
+ stageLocalRuntime(vaultHome());
204
+ }
205
+ catch { /* incomplete installs still write hooks */ }
206
+ installClientHooks('gemini', home);
202
207
  return { client, status: 'installed', destination: path };
203
208
  }
204
209
  if (client === 'vault-desktop') {
@@ -221,6 +226,11 @@ export function installMcpClient(client, options = {}) {
221
226
  else
222
227
  path = join(home, '.config', 'opencode', 'opencode.json');
223
228
  if (client === 'opencode') {
229
+ try {
230
+ stageLocalRuntime(vaultHome());
231
+ }
232
+ catch { /* plugin becomes active when runtime is available */ }
233
+ installOpenCodePlugin(home);
224
234
  mergeServer(path, 'mcp', {
225
235
  type: 'local',
226
236
  command: [...MCP_SERVER_COMMAND],
@@ -0,0 +1,39 @@
1
+ import type { VaultMemoryApi } from './cloud.js';
2
+ import { generateContext } from './context-engines.js';
3
+ type Dependencies = {
4
+ storageKey?: Buffer;
5
+ generate?: typeof generateContext;
6
+ };
7
+ export declare function primeKnowledgeAgent(home: string, cloud: VaultMemoryApi, id: string, rebuild?: boolean, dependencies?: Dependencies): Promise<{
8
+ corpusId: string;
9
+ name: string;
10
+ primed: boolean;
11
+ memoryCount: number;
12
+ conversationReset: boolean;
13
+ engine: "vault-ai-resume" | "claude-subscription" | "openai-subscription" | "openrouter" | "gemini";
14
+ model: string;
15
+ }>;
16
+ export declare function queryKnowledgeAgent(home: string, cloud: VaultMemoryApi, id: string, question: string, dependencies?: Dependencies): Promise<{
17
+ corpusId: string;
18
+ answer: string;
19
+ citations: never[];
20
+ generated: boolean;
21
+ conversationTurns: number;
22
+ engine?: never;
23
+ model?: never;
24
+ contextTruncated?: never;
25
+ } | {
26
+ corpusId: string;
27
+ answer: string;
28
+ citations: {
29
+ index: number;
30
+ memoryId: string;
31
+ title: string;
32
+ }[];
33
+ generated: boolean;
34
+ engine: "vault-ai-resume" | "claude-subscription" | "openai-subscription" | "openrouter" | "gemini";
35
+ model: string;
36
+ conversationTurns: number;
37
+ contextTruncated: boolean;
38
+ }>;
39
+ export {};
@@ -0,0 +1,143 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { loadConfig, loadTokens } from './config.js';
5
+ import { generateContext, selectedContextEngine, selectedEngineModel } from './context-engines.js';
6
+ import { redact } from './hooks.js';
7
+ import { privateStore } from './private-store.js';
8
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
+ function accountKey(home) {
10
+ const config = loadConfig(home), token = process.env.VAULT_GO_TOKEN || loadTokens(home)?.accessToken;
11
+ if (!token)
12
+ throw new Error('Conecte sua conta Vault antes de abrir uma conversa.');
13
+ return createHash('sha256').update(JSON.stringify([config.apiUrl, process.env.VAULT_GO_TOKEN || config.email || token])).digest('hex');
14
+ }
15
+ async function withConversation(home, id, dependencies, action) {
16
+ if (!uuid.test(id))
17
+ throw new Error('Identificador de corpus inválido.');
18
+ const owner = accountKey(home), name = `corpus-${createHash('sha256').update(owner + ':' + id).digest('hex').slice(0, 48)}.json`, path = join(home, `${name}.lock`), lease = randomUUID();
19
+ mkdirSync(home, { recursive: true, mode: 0o700 });
20
+ for (let attempt = 0;; attempt++) {
21
+ try {
22
+ writeFileSync(path, JSON.stringify({ pid: process.pid, lease }), { mode: 0o600, flag: 'wx' });
23
+ break;
24
+ }
25
+ catch (error) {
26
+ if (error.code !== 'EEXIST')
27
+ throw error;
28
+ let dead = false, staleLease;
29
+ try {
30
+ const saved = JSON.parse(readFileSync(path, 'utf8'));
31
+ staleLease = saved.lease;
32
+ if (Number.isSafeInteger(saved.pid) && saved.pid > 0) {
33
+ try {
34
+ process.kill(saved.pid, 0);
35
+ }
36
+ catch (error) {
37
+ dead = error.code === 'ESRCH';
38
+ }
39
+ }
40
+ }
41
+ catch { /* An invalid lock is not proof that its owner exited. */ }
42
+ if (attempt === 0 && dead && staleLease && JSON.parse(readFileSync(path, 'utf8')).lease === staleLease) {
43
+ rmSync(path, { force: true });
44
+ continue;
45
+ }
46
+ throw new Error('Uma consulta já está em andamento neste corpus. Tente novamente após ela terminar.');
47
+ }
48
+ }
49
+ let store;
50
+ try {
51
+ store = privateStore(home, dependencies.storageKey);
52
+ return await action(store, name, () => { if (owner !== accountKey(home))
53
+ throw new Error('A conta mudou durante a consulta; resultado descartado.'); });
54
+ }
55
+ finally {
56
+ store?.close();
57
+ try {
58
+ if (JSON.parse(readFileSync(path, 'utf8')).lease === lease)
59
+ rmSync(path, { force: true });
60
+ }
61
+ catch { /* Already released. */ }
62
+ }
63
+ }
64
+ function corpus(value, id) {
65
+ const record = value;
66
+ if (record?.base?.id !== id || !Array.isArray(record.memories) || record.memories.some(m => !uuid.test(m.id) || typeof m.content !== 'string'))
67
+ throw new Error('Resposta inválida da base de conhecimento.');
68
+ return { name: typeof record.base.name === 'string' ? record.base.name : 'Corpus', memories: record.memories };
69
+ }
70
+ export async function primeKnowledgeAgent(home, cloud, id, rebuild = false, dependencies = {}) {
71
+ return withConversation(home, id, dependencies, async (store, name, assertAccount) => {
72
+ if (rebuild)
73
+ await cloud.rebuildKnowledgeBase(id);
74
+ const base = corpus(await cloud.knowledgeBase(id), id);
75
+ assertAccount();
76
+ const state = { version: 1, corpusId: id, name: base.name, primedAt: Date.now(), turns: [] };
77
+ store.write(name, state);
78
+ return { corpusId: id, name: base.name, primed: true, memoryCount: base.memories.length, conversationReset: true, engine: selectedContextEngine(home), model: selectedEngineModel(home, selectedContextEngine(home)) };
79
+ });
80
+ }
81
+ export async function queryKnowledgeAgent(home, cloud, id, question, dependencies = {}) {
82
+ const cleanQuestion = String(redact(question)).trim();
83
+ if (!cleanQuestion || cleanQuestion.length > 1000)
84
+ throw new Error('A pergunta deve conter entre 1 e 1000 caracteres.');
85
+ return withConversation(home, id, dependencies, async (store, name, assertAccount) => {
86
+ const base = corpus(await cloud.knowledgeBase(id), id);
87
+ assertAccount();
88
+ const owned = new Map(base.memories.map(memory => [memory.id, memory]));
89
+ const saved = store.read(name);
90
+ if (!saved && existsSync(join(home, name)))
91
+ throw new Error('O histórico cifrado não pôde ser aberto. Reprepare o corpus para reiniciar a conversa.');
92
+ const state = saved ?? { version: 1, corpusId: id, name: base.name, primedAt: Date.now(), turns: [] };
93
+ if (state.version !== 1 || state.corpusId !== id || !Array.isArray(state.turns))
94
+ throw new Error('Histórico inválido. Reprepare o corpus.');
95
+ const history = state.turns.filter(turn => turn.sourceIds.every(sourceId => owned.has(sourceId))).slice(-4);
96
+ const retrievalQuestion = [cleanQuestion, history.at(-1)?.question].filter(Boolean).join(' ').slice(0, 1000);
97
+ const retrieval = await cloud.queryKnowledgeBase(id, { question: retrievalQuestion, limit: 12, summaryMode: 'off' });
98
+ if (!Array.isArray(retrieval?.citations))
99
+ throw new Error('Resposta de recuperação inválida.');
100
+ assertAccount();
101
+ const ranked = [...new Set([...retrieval.citations.map(c => c.memoryId), ...base.memories.map(m => m.id)])];
102
+ const sources = [];
103
+ let budget = 11000;
104
+ for (const memoryId of ranked) {
105
+ const memory = owned.get(memoryId);
106
+ if (!memory || budget < 300 || sources.length >= 12)
107
+ continue;
108
+ const content = String(redact(memory.content)).slice(0, Math.min(3500, budget));
109
+ const title = String(redact(memory.title ?? 'Memória')).slice(0, 200);
110
+ sources.push({ index: sources.length + 1, memoryId, title, content });
111
+ budget -= content.length + title.length + 100;
112
+ }
113
+ if (sources.length === 0)
114
+ return { corpusId: id, answer: 'Esta base não contém memórias suficientes para responder.', citations: [], generated: false, conversationTurns: state.turns.length };
115
+ const conversation = history.map(turn => ({ question: turn.question.slice(0, 350), answer: turn.answer.slice(0, 500) }));
116
+ let text = JSON.stringify({ sources, conversation });
117
+ while (text.length > 16000) {
118
+ if (conversation.length)
119
+ conversation.shift();
120
+ else if (sources.length > 1)
121
+ sources.pop();
122
+ else
123
+ sources[0].content = sources[0].content.slice(0, Math.floor(sources[0].content.length / 2));
124
+ text = JSON.stringify({ sources, conversation });
125
+ }
126
+ const engine = selectedContextEngine(home);
127
+ const options = {
128
+ purpose: 'knowledge-answer', question: cleanQuestion,
129
+ ...(cloud.observerGenerate ? { managedGenerate: (source, options) => cloud.observerGenerate(source, options.signal, { purpose: 'knowledge-answer', question: cleanQuestion }) } : {}),
130
+ };
131
+ const result = await (dependencies.generate ?? generateContext)(home, engine, text, options);
132
+ assertAccount();
133
+ const answer = String(redact(result.content));
134
+ const cited = [...answer.matchAll(/\[(\d+)\]/g)].map(match => Number(match[1]));
135
+ if (!cited.length || cited.some(index => index < 1 || index > sources.length))
136
+ throw new Error('O modelo retornou uma resposta sem citações válidas. A conversa foi preservada.');
137
+ const citations = [...new Set(cited)].map(index => { const source = sources[index - 1]; return { index, memoryId: source.memoryId, title: source.title }; });
138
+ state.turns = [...state.turns.slice(-11), { question: cleanQuestion, answer, sourceIds: citations.map(c => c.memoryId) }];
139
+ state.name = base.name;
140
+ store.write(name, state);
141
+ return { corpusId: id, answer, citations, generated: true, engine, model: selectedEngineModel(home, engine), conversationTurns: state.turns.length, contextTruncated: sources.length < base.memories.length || sources.some(source => source.content.length < (owned.get(source.memoryId)?.content.length ?? 0)) };
142
+ });
143
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { VaultMemoryApi } from './cloud.js';
3
+ export declare function registerKnowledgeAgentTools(server: McpServer, home: string, cloud: VaultMemoryApi): void;
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ import { run } from './mcp-result.js';
3
+ import { primeKnowledgeAgent, queryKnowledgeAgent } from './knowledge-agent.js';
4
+ export function registerKnowledgeAgentTools(server, home, cloud) {
5
+ const id = z.string().uuid();
6
+ const annotations = { readOnlyHint: false, destructiveHint: false, openWorldHint: true };
7
+ server.registerTool('vault_go_prime_corpus', {
8
+ description: 'Prepara uma conversa persistente e cifrada com uma base de conhecimento. Reinicia o histórico desta base; consultas usam o motor/modelo selecionado.',
9
+ inputSchema: { id }, annotations,
10
+ }, async ({ id }) => run(() => primeKnowledgeAgent(home, cloud, id)));
11
+ server.registerTool('vault_go_query_corpus', {
12
+ description: 'Responde perguntas com o modelo selecionado e citações das memórias. Mantém contexto de até 12 turnos, cifrado localmente e isolado por conta e corpus. Usa a assinatura/créditos do motor escolhido.',
13
+ inputSchema: { id, question: z.string().trim().min(1).max(1000) }, annotations,
14
+ }, async ({ id, question }) => run(() => queryKnowledgeAgent(home, cloud, id, question)));
15
+ server.registerTool('vault_go_reprime_corpus', {
16
+ description: 'Reaplica os filtros da base e reinicia a conversa usando as memórias atuais. Preserva as memórias de origem.',
17
+ inputSchema: { id }, annotations,
18
+ }, async ({ id }) => run(() => primeKnowledgeAgent(home, cloud, id, true)));
19
+ }
@@ -13,6 +13,8 @@ import { callbackPage } from "./callback-page.js";
13
13
  import { privateStore } from "./private-store.js";
14
14
  import { localDevice } from "./local-device.js";
15
15
  import { processGenerationQueue } from "./hook-queue.js";
16
+ import { startTranscriptWatchers } from './transcript-watchers.js';
17
+ import { transcriptStatus } from './transcript-config.js';
16
18
  import { generateContext, isApiEngine, isContextEngine, listContextEngines, parseEngineModel, saveEngineKey, selectContextEngine, selectEngineModel, selectedContextEngine, } from "./context-engines.js";
17
19
  export const LOCAL_PORT = 38850;
18
20
  const safeEqual = (a, b) => a.length === b.length &&
@@ -585,6 +587,7 @@ export async function startLocalService(options = {}) {
585
587
  email: config.email,
586
588
  cloud: { connected, error: cloudError, syncing: Boolean(syncing) },
587
589
  device: monitor?.status() ?? { status: "unregistered" },
590
+ transcripts: transcriptStatus(home),
588
591
  security: {
589
592
  cache: {
590
593
  encrypted: Boolean(storage),
@@ -644,6 +647,7 @@ export async function startLocalService(options = {}) {
644
647
  if (address && typeof address !== "string")
645
648
  port = address.port;
646
649
  const timer = setInterval(() => void refresh(), options.pollMs ?? 15_000);
650
+ const transcripts = startTranscriptWatchers(home, accountCloud);
647
651
  timer.unref();
648
652
  void refresh();
649
653
  async function close() {
@@ -652,10 +656,11 @@ export async function startLocalService(options = {}) {
652
656
  monitor?.close();
653
657
  clearJobs();
654
658
  clearInterval(timer);
659
+ await transcripts.close();
655
660
  sessions.clear();
656
661
  server.closeAllConnections();
657
662
  await new Promise((resolve) => server.close(() => resolve()));
658
663
  storage?.close();
659
664
  }
660
- return { server, port, close, isIdle: () => !generating && !syncing && login?.status !== "pending" && [...jobs.values()].every(job => job.status !== "running") };
665
+ return { server, port, close, isIdle: () => transcripts.isIdle() && !generating && !syncing && login?.status !== "pending" && [...jobs.values()].every(job => job.status !== "running") };
661
666
  }
@@ -0,0 +1,9 @@
1
+ import type { Buffer } from 'node:buffer';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import type { VaultMemoryApi } from './cloud.js';
4
+ export interface ModeIntegrationOptions {
5
+ storageKey?: Buffer;
6
+ telegramToken?: string;
7
+ telegramFetch?: typeof fetch;
8
+ }
9
+ export declare function registerModeIntegrations(server: McpServer, home: string, cloud: VaultMemoryApi, options?: ModeIntegrationOptions): void;