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
@@ -0,0 +1,118 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { redact } from './capture-privacy.js';
3
+ const object = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
4
+ const text = (v) => typeof v === 'string' ? v : '';
5
+ function messageText(v) {
6
+ if (typeof v === 'string')
7
+ return v;
8
+ return Array.isArray(v) ? v.map(object).filter(b => ['text', 'input_text', 'output_text'].includes(text(b.type))).map(b => text(b.text)).join('\n') : '';
9
+ }
10
+ export const emptyTranscriptState = () => ({ session: '', cwd: '', private: false, pending: {} });
11
+ function inputObject(v) {
12
+ if (typeof v === 'string') {
13
+ try {
14
+ return object(JSON.parse(v));
15
+ }
16
+ catch {
17
+ return { input: v };
18
+ }
19
+ }
20
+ return object(v);
21
+ }
22
+ function bounded(v) {
23
+ const value = redact(v), json = JSON.stringify(value ?? null);
24
+ return json.length <= 16_000 ? value : { truncated: true, preview: String(redact(json.slice(0, 12_000))) };
25
+ }
26
+ // Rollout response_item is canonical. event_msg mirrors are deliberately ignored.
27
+ export function parseTranscript(adapter, value, previous, offset) {
28
+ const state = structuredClone(previous), row = object(value), events = [];
29
+ const timestamp = text(row.timestamp);
30
+ const recordId = text(row.uuid) || createHash('sha256').update(JSON.stringify([state.session, offset, value])).digest('hex');
31
+ function emit(event, input, id = recordId) {
32
+ if (!state.private && state.session && state.cwd)
33
+ events.push({ event, input: { session_id: state.session, cwd: state.cwd, turn_id: id, timestamp, ...input } });
34
+ }
35
+ function prompt(content, privateFlag = false) {
36
+ state.private = privateFlag || /<(?:private|vault-private)\b/i.test(content);
37
+ state.pending = {};
38
+ state.summarized = false;
39
+ if (content.trim())
40
+ emit('session-init', { prompt: content });
41
+ }
42
+ function call(id, name, input) {
43
+ if (!id || !name || state.private || /vault[_-]go|vault[_-]mem/i.test(name))
44
+ return;
45
+ if (Object.keys(state.pending).length >= 16)
46
+ delete state.pending[Object.keys(state.pending)[0]];
47
+ state.pending[id] = { name, input: object(bounded(inputObject(input))), timestamp };
48
+ }
49
+ function result(id, output) {
50
+ const call = state.pending[id];
51
+ if (!call)
52
+ return;
53
+ emit('observation', { tool_name: call.name, tool_input: call.input, tool_response: bounded(output), tool_use_id: id, timestamp: call.timestamp }, id);
54
+ delete state.pending[id];
55
+ }
56
+ function scope(session, cwd) {
57
+ if (session && session !== state.session || cwd && cwd !== state.cwd) {
58
+ state.pending = {};
59
+ state.private = true;
60
+ }
61
+ if (session)
62
+ state.session = session;
63
+ if (cwd)
64
+ state.cwd = cwd;
65
+ }
66
+ if (adapter === 'claude') {
67
+ scope(text(row.sessionId), text(row.cwd));
68
+ if (row.private === true || row.isPrivate === true || row.is_private === true) {
69
+ state.private = true;
70
+ state.pending = {};
71
+ }
72
+ const message = object(row.message), blocks = Array.isArray(message.content) ? message.content.map(object) : [];
73
+ if (row.type === 'user') {
74
+ if (blocks.some(b => b.type === 'tool_result')) {
75
+ for (const b of blocks)
76
+ if (b.type === 'tool_result')
77
+ result(text(b.tool_use_id), b.content);
78
+ }
79
+ else
80
+ prompt(messageText(message.content), row.isPrivate === true || row.is_private === true || row.private === true || row.isMeta === true);
81
+ }
82
+ else if (row.type === 'assistant') {
83
+ for (const b of blocks)
84
+ if (b.type === 'tool_use')
85
+ call(text(b.id), text(b.name), b.input);
86
+ const content = messageText(message.content);
87
+ if (content.trim() && !blocks.some(b => b.type === 'tool_use'))
88
+ emit('summarize', { last_assistant_message: content });
89
+ }
90
+ }
91
+ else {
92
+ const payload = object(row.payload);
93
+ if (row.type === 'session_meta')
94
+ scope(text(payload.id), text(payload.cwd));
95
+ if (row.type === 'turn_context')
96
+ scope('', text(payload.cwd));
97
+ if (row.type === 'response_item') {
98
+ if (payload.type === 'message' && payload.role === 'user')
99
+ prompt(messageText(payload.content));
100
+ else if (payload.type === 'message' && payload.role === 'assistant') {
101
+ const content = messageText(payload.content);
102
+ if (content.trim() && (payload.phase === undefined || payload.phase === 'final_answer')) {
103
+ emit('summarize', { last_assistant_message: content });
104
+ state.summarized = true;
105
+ }
106
+ }
107
+ else if (payload.type === 'function_call' || payload.type === 'custom_tool_call')
108
+ call(text(payload.call_id), text(payload.name), payload.arguments ?? payload.input);
109
+ else if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output')
110
+ result(text(payload.call_id), payload.output);
111
+ }
112
+ if (row.type === 'event_msg' && payload.type === 'task_complete' && !state.summarized && text(payload.last_agent_message).trim()) {
113
+ emit('summarize', { last_assistant_message: payload.last_agent_message });
114
+ state.summarized = true;
115
+ }
116
+ }
117
+ return { state, events };
118
+ }
@@ -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 registerTranscriptTools(server: McpServer, home: string, cloud: VaultMemoryApi): void;
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import { run } from './mcp-result.js';
3
+ import { configureTranscriptWatcher, transcriptStatus } from './transcript-config.js';
4
+ import { pollTranscripts } from './transcript-watchers.js';
5
+ import { configureContextChain, contextChainStatus } from './context-chain.js';
6
+ export function registerTranscriptTools(server, home, cloud) {
7
+ const path = z.string().trim().min(1).max(2000);
8
+ server.registerTool('vault_go_transcripts_status', { description: 'Watchers opt-in de Claude/Codex neste dispositivo e conta. Requer automaticCapture para enviar eventos; geração usa o motor já escolhido.', inputSchema: {}, annotations: { readOnlyHint: true, openWorldHint: false } }, () => run(async () => transcriptStatus(home)));
9
+ server.registerTool('vault_go_transcripts_configure', {
10
+ description: 'Ativa/desativa captura persistente de um workspace com consentimento explícito. Por padrão ignora histórico existente; fromStart importa histórico. Hooks nativos preservam contexto e cedem escrita ao watcher enquanto ativo. O worker local precisa estar em execução.',
11
+ inputSchema: { adapter: z.enum(['claude', 'codex']), workspace: path, root: path.optional(), enabled: z.boolean(), consent: z.boolean().default(false), fromStart: z.boolean().default(false) },
12
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
13
+ }, input => run(async () => { const { root, ...rest } = input; return configureTranscriptWatcher(home, { ...rest, ...(root === undefined ? {} : { root }) }); }));
14
+ server.registerTool('vault_go_transcripts_poll', { description: 'Processa um lote limitado dos watchers já consentidos. Retomada e idempotência persistem entre reinícios; não ativa captura nem muda o motor.', inputSchema: {}, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true } }, () => run(() => pollTranscripts(home, cloud)));
15
+ server.registerTool('vault_go_context_chain', { description: 'Lista vínculos locais entre workspaces para contexto multi-project do SessionStart. Vínculos são específicos desta conta.', inputSchema: {}, annotations: { readOnlyHint: true, openWorldHint: false } }, () => run(async () => contextChainStatus(home)));
16
+ server.registerTool('vault_go_context_chain_configure', {
17
+ description: 'Define até quatro raízes relacionadas e/ou o checkout pai de um worktree. Exige consentimento; só consulta projetos já existentes e acessíveis na conta, dentro do orçamento total de contexto. Lista vazia remove os vínculos.',
18
+ inputSchema: { workspace: path, roots: z.array(path).max(4), consent: z.boolean().default(false), includeWorktreeParent: z.boolean().default(false) },
19
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
20
+ }, input => run(async () => configureContextChain(home, input)));
21
+ }
@@ -0,0 +1,13 @@
1
+ import { type VaultMemoryApi } from './cloud.js';
2
+ export interface TranscriptPollResult {
3
+ files: number;
4
+ events: number;
5
+ bytes: number;
6
+ busy: boolean;
7
+ lastError?: string;
8
+ }
9
+ export declare function pollTranscripts(home: string, cloud?: VaultMemoryApi): Promise<TranscriptPollResult>;
10
+ export declare function startTranscriptWatchers(home: string, cloud?: VaultMemoryApi, intervalMs?: number): {
11
+ isIdle: () => boolean;
12
+ close: () => Promise<void>;
13
+ };
@@ -0,0 +1,311 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { closeSync, constants, fstatSync, lstatSync, openSync, opendirSync, readFileSync, readSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { VaultCloudClient } from './cloud.js';
5
+ import { executeHook } from './hooks.js';
6
+ import { captureCredential, captureOwner, privateJson, readPrivateJson, transcriptTargets, workspaceRoot } from './transcript-config.js';
7
+ import { emptyTranscriptState, parseTranscript } from './transcript-parser.js';
8
+ const MAX_CHUNK = 256_000, MAX_BYTES = 2_000_000, MAX_RECORDS = 200;
9
+ function lock(home) {
10
+ const path = join(home, 'transcript-consumer.lock'), token = randomUUID();
11
+ for (let attempt = 0; attempt < 2; attempt++) {
12
+ try {
13
+ writeFileSync(path, JSON.stringify({ pid: process.pid, token }), { flag: 'wx', mode: 0o600 });
14
+ return () => { try {
15
+ if (JSON.parse(readFileSync(path, 'utf8')).token === token)
16
+ unlinkSync(path);
17
+ }
18
+ catch { /* another owner */ } };
19
+ }
20
+ catch (error) {
21
+ if (error.code !== 'EEXIST')
22
+ throw error;
23
+ try {
24
+ const value = JSON.parse(readFileSync(path, 'utf8'));
25
+ if (!Number.isInteger(value.pid) || Number(value.pid) <= 0)
26
+ throw new Error('Invalid lock');
27
+ try {
28
+ process.kill(Number(value.pid), 0);
29
+ return null;
30
+ }
31
+ catch (e) {
32
+ if (e.code !== 'ESRCH')
33
+ return null;
34
+ }
35
+ unlinkSync(path);
36
+ }
37
+ catch {
38
+ try {
39
+ if (Date.now() - lstatSync(path).mtimeMs > 300_000) {
40
+ unlinkSync(path);
41
+ continue;
42
+ }
43
+ }
44
+ catch { /* lock changed */ }
45
+ return null;
46
+ }
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ function scan(root) {
52
+ const files = [], dirs = [{ path: root, mtime: 0 }];
53
+ let entries = 0;
54
+ while (dirs.length && entries < 4096 && files.length < 1000) {
55
+ dirs.sort((a, b) => b.mtime - a.mtime || b.path.localeCompare(a.path));
56
+ const directory = dirs.shift().path;
57
+ let handle;
58
+ try {
59
+ handle = opendirSync(directory);
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ try {
65
+ for (let entry = handle.readSync(); entry; entry = handle.readSync()) {
66
+ if (++entries > 4096 || files.length >= 1000)
67
+ break;
68
+ if (entry.isSymbolicLink())
69
+ continue;
70
+ const path = join(directory, entry.name);
71
+ if (entry.isDirectory()) {
72
+ if (!entry.name.startsWith('.') && dirs.length < 512) {
73
+ try {
74
+ dirs.push({ path, mtime: lstatSync(path).mtimeMs });
75
+ }
76
+ catch { /* directory removed */ }
77
+ }
78
+ }
79
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
80
+ try {
81
+ files.push({ path, mtime: lstatSync(path).mtimeMs });
82
+ }
83
+ catch { /* rotated during scan */ }
84
+ }
85
+ }
86
+ }
87
+ finally {
88
+ handle.closeSync();
89
+ }
90
+ }
91
+ return files.sort((a, b) => b.mtime - a.mtime || a.path.localeCompare(b.path)).map(f => f.path);
92
+ }
93
+ function bytesAt(fd, offset, length) {
94
+ const bytes = Buffer.alloc(length);
95
+ return bytes.subarray(0, readSync(fd, bytes, 0, length, offset));
96
+ }
97
+ const digest = (value) => createHash('sha256').update(value).digest('hex');
98
+ function initialCursor(fd, target, existing) {
99
+ let bytes = 0;
100
+ const read = (offset, length) => { const result = bytesAt(fd, offset, length); bytes += result.length; return result; };
101
+ const stat = fstatSync(fd), prefixBytes = Math.min(128, stat.size);
102
+ const cursor = { dev: stat.dev, ino: stat.ino, offset: 0, prefix: digest(read(0, prefixBytes)), prefixBytes, discarding: false, state: emptyTranscriptState() };
103
+ cursor.state.private = true;
104
+ if (!target.fromStart && !existing && Math.floor(stat.birthtimeMs || stat.ctimeMs) <= target.since) {
105
+ // Metadata bootstrap only; never capture historical records without --from-start.
106
+ if (target.adapter === 'codex') {
107
+ const head = read(0, Math.min(16_000, stat.size)).toString('utf8');
108
+ for (const line of head.split('\n').slice(0, 16)) {
109
+ try {
110
+ const row = JSON.parse(line);
111
+ if (row.type === 'session_meta')
112
+ cursor.state = parseTranscript(target.adapter, row, cursor.state, 0).state;
113
+ }
114
+ catch { /* partial or malformed header */ }
115
+ }
116
+ }
117
+ cursor.offset = stat.size;
118
+ cursor.discarding = stat.size > 0 && read(stat.size - 1, 1)[0] !== 10;
119
+ }
120
+ return { cursor, bytes };
121
+ }
122
+ function saveCheckpoints(home, state, current = '') {
123
+ let serialized = JSON.stringify(state);
124
+ for (const key of Object.keys(state.files)) {
125
+ if (Buffer.byteLength(serialized, 'utf8') <= 4_000_000 && Object.keys(state.files).length <= 1000)
126
+ break;
127
+ if (key !== current) {
128
+ delete state.files[key];
129
+ serialized = JSON.stringify(state);
130
+ }
131
+ }
132
+ privateJson(home, 'transcript-checkpoints.json', state);
133
+ }
134
+ export async function pollTranscripts(home, cloud = new VaultCloudClient(home)) {
135
+ const result = { files: 0, events: 0, bytes: 0, busy: false };
136
+ const targets = transcriptTargets(home), owner = captureOwner(home);
137
+ if (!owner || !targets.some(t => t.enabled))
138
+ return result;
139
+ const release = lock(home);
140
+ if (!release)
141
+ return { ...result, busy: true };
142
+ const saved = readPrivateJson(home, 'transcript-checkpoints.json');
143
+ const checkpoints = saved?.owner === owner && saved.files && typeof saved.files === 'object' ? saved : { owner, files: {} };
144
+ let records = 0;
145
+ try {
146
+ const preferences = await cloud.preferences?.();
147
+ if (preferences?.automaticCapture !== true)
148
+ return result;
149
+ const credential = captureCredential(home);
150
+ for (const target of targets.filter(t => t.enabled)) {
151
+ const allowed = () => {
152
+ if (owner !== captureOwner(home) || credential !== captureCredential(home) || !transcriptTargets(home).some(t => t.id === target.id && t.enabled && t.since === target.since && t.root === target.root))
153
+ throw new Error('capture_consent_changed');
154
+ };
155
+ // Keep every API operation bound to the account/consent used at the start of this poll.
156
+ const guardedCloud = new Proxy(cloud, { get(object, property) {
157
+ const method = Reflect.get(object, property);
158
+ if (typeof method !== 'function')
159
+ return method;
160
+ return async (...args) => { allowed(); const value = property === 'preferences' ? preferences : await method.apply(object, args); allowed(); return value; };
161
+ } });
162
+ allowed();
163
+ let files;
164
+ try {
165
+ if (realpathSync(target.root) !== target.root)
166
+ throw new Error('transcript_root_changed');
167
+ files = scan(target.root);
168
+ }
169
+ catch {
170
+ result.lastError = 'transcript_capture_failed';
171
+ checkpoints.lastError = result.lastError;
172
+ continue;
173
+ }
174
+ for (const file of files) {
175
+ if (result.bytes > MAX_BYTES - 16_257 || records >= MAX_RECORDS || result.files >= 32)
176
+ break;
177
+ const key = digest(JSON.stringify([target.id, file]));
178
+ let fd;
179
+ try {
180
+ // Recheck real path and O_NOFOLLOW: a replaced file cannot escape the consented root.
181
+ try {
182
+ if (realpathSync(file) !== file)
183
+ continue;
184
+ fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
185
+ }
186
+ catch (error) {
187
+ if (['ENOENT', 'ELOOP'].includes(error.code ?? ''))
188
+ continue;
189
+ throw error;
190
+ }
191
+ const stat = fstatSync(fd);
192
+ if (!stat.isFile())
193
+ continue;
194
+ const prior = checkpoints.files[key];
195
+ let cursor = prior && Number.isSafeInteger(prior.offset) && prior.offset >= 0 && typeof prior.state?.session === 'string' && typeof prior.state.cwd === 'string' && prior.state.pending && Number.isInteger(prior.prefixBytes) && prior.prefixBytes >= 0 && prior.prefixBytes <= 128 ? prior : undefined;
196
+ const prefix = cursor ? bytesAt(fd, 0, cursor.prefixBytes) : Buffer.alloc(0);
197
+ result.bytes += prefix.length;
198
+ let initialized = false;
199
+ if (!cursor || cursor.dev !== stat.dev || cursor.ino !== stat.ino || cursor.offset > stat.size || digest(prefix) !== cursor.prefix) {
200
+ const initial = initialCursor(fd, target, Boolean(prior));
201
+ cursor = initial.cursor;
202
+ result.bytes += initial.bytes;
203
+ initialized = true;
204
+ result.files++;
205
+ }
206
+ if (cursor.offset === stat.size) {
207
+ checkpoints.files[key] = cursor;
208
+ continue;
209
+ }
210
+ if (!initialized)
211
+ result.files++;
212
+ const bytes = bytesAt(fd, cursor.offset, Math.min(MAX_CHUNK, MAX_BYTES - result.bytes, stat.size - cursor.offset));
213
+ result.bytes += bytes.length;
214
+ let position = 0;
215
+ while (position < bytes.length && records < MAX_RECORDS) {
216
+ const newline = bytes.indexOf(10, position);
217
+ if (newline < 0) {
218
+ // An oversized line is skipped in bounded chunks; partial normal lines wait for append.
219
+ if (cursor.discarding || bytes.length === MAX_CHUNK && position === 0) {
220
+ cursor.offset += bytes.length - position;
221
+ cursor.discarding = true;
222
+ cursor.state.private = true;
223
+ cursor.state.pending = {};
224
+ }
225
+ break;
226
+ }
227
+ const size = newline - position + 1;
228
+ if (!cursor.discarding) {
229
+ let value;
230
+ try {
231
+ value = JSON.parse(bytes.subarray(position, newline).toString('utf8'));
232
+ }
233
+ catch {
234
+ value = null;
235
+ }
236
+ let parsed;
237
+ if (value !== null) {
238
+ try {
239
+ parsed = parseTranscript(target.adapter, value, cursor.state, cursor.offset);
240
+ }
241
+ catch { /* invalid/deeply nested record: fail private below */ }
242
+ }
243
+ if (parsed) {
244
+ if (parsed.state.cwd && workspaceRoot(parsed.state.cwd) === target.workspace) {
245
+ for (const event of parsed.events) {
246
+ allowed();
247
+ await executeHook(target.adapter, event.event, event.input, guardedCloud, home, { transcript: true });
248
+ allowed();
249
+ result.events++;
250
+ }
251
+ }
252
+ else {
253
+ parsed.state.pending = {};
254
+ parsed.state.private = true;
255
+ }
256
+ cursor.state = parsed.state;
257
+ }
258
+ else {
259
+ cursor.state.private = true;
260
+ cursor.state.pending = {};
261
+ }
262
+ records++;
263
+ }
264
+ cursor.offset += size;
265
+ cursor.discarding = false;
266
+ position = newline + 1;
267
+ checkpoints.files[key] = cursor;
268
+ // Persist only after API + durable generation queue acknowledge each complete record.
269
+ allowed();
270
+ saveCheckpoints(home, checkpoints, key);
271
+ }
272
+ checkpoints.files[key] = cursor;
273
+ }
274
+ finally {
275
+ if (fd !== undefined)
276
+ closeSync(fd);
277
+ }
278
+ }
279
+ }
280
+ if (!result.lastError)
281
+ delete checkpoints.lastError;
282
+ }
283
+ catch {
284
+ result.lastError = 'transcript_capture_failed';
285
+ checkpoints.lastError = result.lastError;
286
+ }
287
+ finally {
288
+ checkpoints.lastScan = Date.now();
289
+ // Bound checkpoint growth while retaining recent files; replay remains API-idempotent.
290
+ try {
291
+ if (owner === captureOwner(home))
292
+ saveCheckpoints(home, checkpoints);
293
+ }
294
+ finally {
295
+ release();
296
+ }
297
+ }
298
+ return result;
299
+ }
300
+ export function startTranscriptWatchers(home, cloud, intervalMs = 5_000) {
301
+ let stopped = false, running;
302
+ const tick = () => {
303
+ if (stopped || running)
304
+ return;
305
+ running = pollTranscripts(home, cloud).catch(() => ({ files: 0, events: 0, bytes: 0, busy: false, lastError: 'transcript_capture_failed' })).finally(() => { running = undefined; });
306
+ };
307
+ const timer = setInterval(tick, Math.max(100, intervalMs));
308
+ timer.unref();
309
+ tick();
310
+ return { isIdle: () => !running, close: async () => { stopped = true; clearInterval(timer); await running; } };
311
+ }
@@ -3,6 +3,8 @@ import { type UpdateState } from './updater.js';
3
3
  export declare function activatePreparedUpdate(home: string, state: UpdateState, operations?: {
4
4
  stop: (home: string) => Promise<boolean>;
5
5
  start: (home: string) => Promise<LocalHealth>;
6
+ repair?: (home: string) => void;
7
+ warn?: (message: string) => void;
6
8
  }, options?: {
7
9
  automatic?: boolean;
8
10
  }): Promise<LocalHealth>;
@@ -2,10 +2,12 @@ import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { existsSync, openSync, closeSync, readFileSync, realpathSync, renameSync, rmSync, lstatSync, unlinkSync, writeFileSync } from 'node:fs';
4
4
  import { join, relative, isAbsolute } from 'node:path';
5
+ import { homedir } from 'node:os';
5
6
  import { stageLocalRuntime, startLocal, stopLocal } from './local-install.js';
6
7
  import { installUpdate, updateStatus } from './updater.js';
7
8
  import { vaultHome } from './config.js';
8
9
  import { generationQueueStatus } from './hook-queue.js';
10
+ import { repairAdditionalClientHooks } from './hook-install.js';
9
11
  function preparedPackage(home, state) {
10
12
  if (state.status !== 'ready' || !state.packageRoot || !state.latest)
11
13
  throw new Error('No validated update is ready.');
@@ -18,8 +20,20 @@ function preparedPackage(home, state) {
18
20
  throw new Error('Invalid prepared update.');
19
21
  return root;
20
22
  }
21
- export async function activatePreparedUpdate(home, state, operations = { stop: stopLocal, start: startLocal }, options = {}) {
23
+ export async function activatePreparedUpdate(home, state, operations = { stop: stopLocal, start: startLocal, repair: home => repairAdditionalClientHooks(homedir(), home) }, options = {}) {
22
24
  const release = await acquireActivationLock(home);
25
+ const repair = () => {
26
+ try {
27
+ operations.repair?.(home);
28
+ }
29
+ catch {
30
+ const message = 'Vault Go: runtime atualizado; reparo dos hooks dos clientes pendente.';
31
+ try {
32
+ (operations.warn ?? (warning => { process.stderr.write(warning + '\n'); }))(message);
33
+ }
34
+ catch { }
35
+ }
36
+ };
23
37
  try {
24
38
  if (options.automatic && !updateStatus(home).enabled)
25
39
  throw new Error('Automatic updates are disabled.');
@@ -29,11 +43,14 @@ export async function activatePreparedUpdate(home, state, operations = { stop: s
29
43
  const health = await operations.start(home);
30
44
  if (health.version !== state.latest)
31
45
  throw new Error('Active worker version mismatch.');
46
+ repair();
32
47
  return health;
33
48
  }
34
49
  if (active && compareVersions(active, state.latest) > 0)
35
50
  throw new Error('Refusing to activate an older runtime.');
36
- return await replaceRuntime(home, state, source, operations);
51
+ const health = await replaceRuntime(home, state, source, operations);
52
+ repair();
53
+ return health;
37
54
  }
38
55
  finally {
39
56
  release();
@@ -0,0 +1,72 @@
1
+ # AST completo e awareness local do Grok
2
+
3
+ ## Idiomas AST
4
+
5
+ `vault_go_smart_languages` publica o catálogo e a disponibilidade real na plataforma. `smart_outline`, `smart_unfold` e `smart_search` usam grammars Tree-sitter carregadas pelo ast-grep; indisponibilidade nunca vira parsing por regex. Os limites anteriores de workspace, arquivos privados, bytes, quantidade de arquivos/símbolos e tamanho da saída continuam valendo.
6
+
7
+ | Idioma | Extensões |
8
+ | --- | --- |
9
+ | JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` |
10
+ | TypeScript | `.ts`, `.mts`, `.cts` |
11
+ | TSX | `.tsx` |
12
+ | HTML | `.html`, `.htm` |
13
+ | CSS | `.css` |
14
+ | Python | `.py`, `.pyw` |
15
+ | Go | `.go` |
16
+ | Rust | `.rs` |
17
+ | Ruby | `.rb` |
18
+ | Java | `.java` |
19
+ | C | `.c`, `.h` |
20
+ | C++ | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hh` |
21
+ | Kotlin | `.kt`, `.kts` |
22
+ | Swift | `.swift` |
23
+ | PHP | `.php` |
24
+ | Lua | `.lua` |
25
+ | Scala | `.scala`, `.sc` |
26
+ | Bash | `.sh`, `.bash`, `.zsh` |
27
+ | Haskell | `.hs` |
28
+ | TOML | `.toml` |
29
+ | YAML | `.yml`, `.yaml` |
30
+ | SQL | `.sql` |
31
+ | Markdown | `.md`, `.mdx` |
32
+ | Zig | `.zig` |
33
+ | SCSS | `.scss` |
34
+
35
+ Isso cobre os 24 idiomas do `LANG_MAP` upstream, além do HTML que o Vault já suportava. `.mdx` usa a grammar Markdown e `.zsh` usa Bash, como no upstream; não promete validar extensões próprias de MDX/Zsh.
36
+
37
+ JS/TS/TSX/HTML/CSS usam as grammars de `@ast-grep/napi`. Dezoito outras usam pacotes oficiais `@ast-grep/lang-*`, fixados no `package.json`/lock. Zig e SCSS usam os símbolos C `tree_sitter_zig` e `tree_sitter_scss` exportados pelos prebuilds dos pacotes `@tree-sitter-grammars/tree-sitter-zig@1.1.2` e `tree-sitter-scss@1.0.0`. Carregar esses símbolos diretamente permite usar o mesmo AST/matcher do ast-grep, sem compilar ou executar scripts de instalação.
38
+
39
+ Os prebuilds são dependências do MCP instalado. O worker local e a fila não importam o parser nem essas dependências. `npm ci --ignore-scripts` é suportado nos sistemas com prebuild compatível. O pacote SCSS não publica prebuild Linux ARM64; ali o catálogo informa indisponibilidade e operações diretas falham explicitamente. Outros alvos também dependem da presença e compatibilidade do prebuild: não há download ou compilação automática durante o uso. Todos os 25 foram verificados nesta implementação em macOS ARM64.
40
+
41
+ Os nomes/símbolos, corpos e posições vêm de nós e campos da árvore. Markdown usa a árvore de seções para desdobrar uma seção completa. Buscas com metavariáveis foram verificadas também em Python, Go, Zig e SCSS, incluindo a distinção entre chamadas reais e textos em comentários/strings.
42
+
43
+ O registro segue a [API oficial do ast-grep](https://ast-grep.github.io/guide/api-usage/js-api): todas as grammars são registradas juntas uma vez por processo. Os [pacotes oficiais de grammars](https://github.com/ast-grep/langs) fornecem os parsers pré-compilados. Nenhuma biblioteca escolhida por caminho de workspace/configuração é carregada como código executável.
44
+
45
+ ## Awareness do Grok
46
+
47
+ Awareness é escrita local de uma linha factual em `agents/<UUID>/memory/log/YYYY-MM.md`, consumida pelo agente Grok. Não é uma integração Telegram.
48
+
49
+ 1. Use `vault_go_grok_awareness_configure` com `enabled=true`, `consent=true`, `agentDataRoot` absoluto existente, `agentIds` permitidos e ao menos um filtro em `triggerTypes` ou `triggerConcepts`.
50
+ 2. Use `vault_go_grok_awareness_push` com `agentId` permitido e `memoryIds` UUID para enviar memórias já existentes na conta autenticada.
51
+ 3. Consulte `vault_go_grok_awareness_status` ou desative com `enabled=false`.
52
+
53
+ Nada é ativado por padrão. Não há escolha automática de diretório ou agentes piloto. O consentimento fica em `grok-awareness.json`, privado e específico da conta. Troca de conta revoga o consentimento local. A leitura MCP confere também a credencial antes/depois da API, impedindo publicar um resultado recebido durante troca de conta.
54
+
55
+ Hooks que recebem `agent_id`/`agentId` preservam esse agente na fila. Após persistir a memória no Vault, a fila envia o fato apenas ao agente correspondente e permitido; não faz broadcast. O pipeline atual grava essas memórias como `discovery`, portanto a automação usa esse tipo e os conceitos gerados. O push por IDs preserva o `memoryType` armazenado e atende aos demais tipos, como `decision` ou `bugfix`. Sem identidade de agente, a fila não escreve awareness.
56
+
57
+ Cada linha usa a forma `- YYYY-MM-DD [awareness] tipo — título: subtítulo. primeiro fato`, com whitespace colapsado e até 500 caracteres. Flags/blocos privados são rejeitados; credenciais usam a mesma redação da captura. A deduplicação compara o corpo, ignorando a data, dentro do log mensal. O mês usa UTC. `profile.md` não é alterado.
58
+
59
+ O writer limita lotes a 100 observações e arquivos mensais a 1 MiB, valida a raiz e todos os diretórios descendentes, recusa symlinks/hardlinks de logs e usa lock entre processos e substituição atômica com arquivos 0600. Conteúdo anterior é preservado. Um log cheio ou destino inacessível produz `awareness_write_failed`, sem conteúdo sensível no status. Falha depois da persistência mantém o job para retry; o resultado do modelo já gerado é reutilizado e a importação mantém sua chave idempotente. Desativar awareness permite concluir esses jobs sem nova escrita local. Outros processos que escrevam o mesmo log precisam coordenar acesso para garantir ausência de sobrescrita concorrente.
60
+
61
+ ## Fontes e validação
62
+
63
+ Contrato upstream consultado no SHA `adce0fdfaf1cd46646bbd0b22ae74cbd460ed787`:
64
+
65
+ - `src/services/smart-file-read/parser.ts`: `LANG_MAP`, grammars e queries de símbolos.
66
+ - `src/services/integrations/GrokBotAwarenessPusher.ts`: filtros, linha limitada, destino mensal, allowlist e deduplicação.
67
+ - `src/services/worker/agents/ResponseProcessor.ts`: envio vinculado a `pendingAgentId`, após persistência.
68
+ - `tests/integrations/grok-bot-awareness-pusher.test.ts`: casos do writer upstream.
69
+
70
+ Fixtures do Vault verificam outlines/desdobramento de todos os idiomas, busca estrutural, consentimento/filtros, privacidade, conta, concorrência, rotação, caminho malicioso, retomada sem regeneração, MCP e carregamento do worker staged sem `node_modules`. Nenhum log de agente ou configuração da instalação real foi modificado. A revisão multimodelo foi substituída por revisão manual, pois o gateway LiteLLM não está configurado neste ambiente.
71
+
72
+ Validação em 2026-09-20 sobre a base `8f08e19341eed95164ee9a77cf178486adeddb0f`: instalação limpa `npm ci --ignore-scripts`, `bun run build`, 178 testes / 1323 assertions (`bun test src`), `git diff --check` e pacote npm passaram. `npm audit --omit=dev` retornou zero vulnerabilidades. A validação posterior à instalação limpa confirmou que as grammars funcionam sem postinstall; o subprocesso staged confirmou que fila/awareness não precisam dessas dependências no worker.