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.
- package/README.md +58 -9
- package/dist/awareness-tools.d.ts +4 -0
- package/dist/awareness-tools.js +38 -0
- package/dist/backup.d.ts +41 -0
- package/dist/backup.js +117 -0
- package/dist/capabilities.d.ts +218 -25
- package/dist/capabilities.js +46 -16
- package/dist/capture-privacy.d.ts +1 -0
- package/dist/capture-privacy.js +19 -0
- package/dist/ccs-align.d.ts +122 -0
- package/dist/ccs-align.js +376 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +52 -0
- package/dist/cloud.d.ts +15 -2
- package/dist/cloud.js +62 -4
- package/dist/context-chain.d.ts +37 -0
- package/dist/context-chain.js +98 -0
- package/dist/context-engines.d.ts +6 -0
- package/dist/context-engines.js +28 -8
- package/dist/crystal-tools.d.ts +9 -0
- package/dist/crystal-tools.js +106 -0
- package/dist/custom-modes.d.ts +59 -0
- package/dist/custom-modes.js +249 -0
- package/dist/deployment-verification.d.ts +19 -0
- package/dist/deployment-verification.js +171 -0
- package/dist/grok-awareness.d.ts +51 -0
- package/dist/grok-awareness.js +244 -0
- package/dist/hook-install.d.ts +7 -2
- package/dist/hook-install.js +71 -15
- package/dist/hook-queue.d.ts +6 -2
- package/dist/hook-queue.js +23 -4
- package/dist/hooks.d.ts +4 -3
- package/dist/hooks.js +83 -32
- package/dist/index.js +9 -1
- package/dist/installer.js +11 -1
- package/dist/knowledge-agent.d.ts +39 -0
- package/dist/knowledge-agent.js +143 -0
- package/dist/knowledge-tools.d.ts +3 -0
- package/dist/knowledge-tools.js +19 -0
- package/dist/local-service.js +6 -1
- package/dist/mode-integrations.d.ts +9 -0
- package/dist/mode-integrations.js +149 -0
- package/dist/modes.js +2 -2
- package/dist/opencode-plugin.d.ts +16 -0
- package/dist/opencode-plugin.js +73 -0
- package/dist/parity-tools.js +4 -4
- package/dist/server.js +56 -8
- package/dist/smart-code.d.ts +12 -16
- package/dist/smart-code.js +139 -28
- package/dist/telegram.d.ts +39 -0
- package/dist/telegram.js +138 -0
- package/dist/transcript-config.d.ts +41 -0
- package/dist/transcript-config.js +97 -0
- package/dist/transcript-parser.d.ts +24 -0
- package/dist/transcript-parser.js +118 -0
- package/dist/transcript-tools.d.ts +3 -0
- package/dist/transcript-tools.js +21 -0
- package/dist/transcript-watchers.d.ts +13 -0
- package/dist/transcript-watchers.js +311 -0
- package/dist/update-control.d.ts +2 -0
- package/dist/update-control.js +19 -2
- package/docs/ast-and-awareness.md +72 -0
- package/docs/mode-integrations.md +74 -0
- package/docs/parity-deployment-review.md +60 -0
- package/docs/transcripts-and-context.md +75 -0
- 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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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(
|
|
205
|
+
paths: [...new Set([file, resolve(workspace, file), projectFile])],
|
|
162
206
|
})
|
|
163
|
-
: await cloud.
|
|
164
|
-
|
|
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
|
|
222
|
-
filesModified: modifies
|
|
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
|
|
237
|
-
filesModified: modifies
|
|
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,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
|
+
}
|
package/dist/local-service.js
CHANGED
|
@@ -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;
|