vault-go 0.30.0 → 0.31.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 +12 -0
- package/dist/agent-cost.d.ts +11 -0
- package/dist/agent-cost.js +116 -0
- package/dist/auth.js +29 -2
- package/dist/capabilities.d.ts +30 -24
- package/dist/capabilities.js +7 -6
- package/dist/capture-privacy.js +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +16 -1
- package/dist/cloud.js +98 -47
- package/dist/config.d.ts +2 -0
- package/dist/config.js +15 -1
- package/dist/crystal-memory-mirror.d.ts +14 -1
- package/dist/crystal-memory-mirror.js +24 -15
- package/dist/crystal-tools.d.ts +24 -1
- package/dist/crystal-tools.js +30 -5
- package/dist/grok-awareness.js +54 -1
- package/dist/hook-install.d.ts +4 -0
- package/dist/hook-install.js +41 -1
- package/dist/hook-queue.d.ts +18 -0
- package/dist/hook-queue.js +115 -3
- package/dist/hook-runner.js +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +17 -8
- package/dist/index.js +5 -1
- package/dist/installer.js +8 -1
- package/dist/legacy-brand.d.ts +2 -0
- package/dist/legacy-brand.js +5 -0
- package/dist/local-service.js +11 -0
- package/dist/memory-archive.d.ts +25 -0
- package/dist/memory-archive.js +65 -0
- package/dist/modes.d.ts +1 -1
- package/dist/modes.js +3 -1
- package/dist/opencode-plugin.d.ts +29 -1
- package/dist/opencode-plugin.js +36 -1
- package/dist/server.js +66 -0
- package/dist/skill-download.d.ts +6 -0
- package/dist/skill-download.js +48 -0
- package/dist/skill-library.d.ts +8 -0
- package/dist/skill-library.js +32 -0
- package/dist/skill-local.d.ts +7 -0
- package/dist/skill-local.js +44 -7
- package/dist/skill-tools.js +13 -26
- package/dist/skills-cli.d.ts +31 -0
- package/dist/skills-cli.js +90 -0
- package/dist/transcript-config.d.ts +1 -1
- package/dist/transcript-config.js +4 -3
- package/dist/transcript-parser.js +1 -1
- package/dist/transcript-tools.js +1 -1
- package/package.json +13 -1
package/dist/cloud.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
1
2
|
import { recordActivity } from './activity.js';
|
|
2
|
-
import { loadConfig, loadTokens, saveTokens, validateApiUrl, vaultHome, } from './config.js';
|
|
3
|
+
import { credentialFilesGeneration, loadConfig, loadTokens, saveTokens, validateApiUrl, vaultHome, } from './config.js';
|
|
3
4
|
const KNOWLEDGE_BASE_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
4
5
|
const SKILL_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
5
6
|
const KNOWLEDGE_BASE_RESPONSE_LIMIT_ERROR = 'A base de conhecimento excede o limite de resposta de 8 MiB.';
|
|
@@ -43,6 +44,31 @@ function withoutClientDecisions(input) {
|
|
|
43
44
|
return safeRecord;
|
|
44
45
|
}) };
|
|
45
46
|
}
|
|
47
|
+
const cloudSessions = new Map();
|
|
48
|
+
function sessionFor(home) {
|
|
49
|
+
const generation = credentialFilesGeneration(home);
|
|
50
|
+
const base = validateApiUrl(loadConfig(home).apiUrl).href;
|
|
51
|
+
const environmentToken = process.env['VAULT_GO_TOKEN'];
|
|
52
|
+
const tokens = environmentToken ? null : loadTokens(home);
|
|
53
|
+
if (credentialFilesGeneration(home) !== generation) {
|
|
54
|
+
throw new VaultCloudError(401, 'Vault session changed.');
|
|
55
|
+
}
|
|
56
|
+
const key = JSON.stringify([base, environmentToken, tokens?.accessToken, tokens?.refreshToken, generation]);
|
|
57
|
+
let session = cloudSessions.get(resolve(home));
|
|
58
|
+
if (!session || session.key !== key) {
|
|
59
|
+
session = { key, base, tokens, environmentToken };
|
|
60
|
+
cloudSessions.set(resolve(home), session);
|
|
61
|
+
}
|
|
62
|
+
return session;
|
|
63
|
+
}
|
|
64
|
+
function assertSession(home, session) {
|
|
65
|
+
if (sessionFor(home) !== session)
|
|
66
|
+
throw new VaultCloudError(401, 'Vault session changed.');
|
|
67
|
+
}
|
|
68
|
+
function sessionEndpoint(base, path) {
|
|
69
|
+
const url = new URL(base);
|
|
70
|
+
return new URL(`${url.pathname.replace(/\/$/u, '')}${path}`, url.origin);
|
|
71
|
+
}
|
|
46
72
|
export class VaultCloudClient {
|
|
47
73
|
home;
|
|
48
74
|
cloudFetch;
|
|
@@ -104,7 +130,7 @@ export class VaultCloudClient {
|
|
|
104
130
|
return { deleted: true, id };
|
|
105
131
|
}
|
|
106
132
|
async search(input) {
|
|
107
|
-
return this.request('/memory/search', { method: 'POST', body: input });
|
|
133
|
+
return this.request('/memory/search', { retryRead: true, method: 'POST', body: input });
|
|
108
134
|
}
|
|
109
135
|
async retrieve(input) {
|
|
110
136
|
return this.request('/memory/retrieve', { method: 'POST', body: input });
|
|
@@ -116,19 +142,19 @@ export class VaultCloudClient {
|
|
|
116
142
|
return this.request('/memory/decisions/evaluate', { method: 'POST', body: input });
|
|
117
143
|
}
|
|
118
144
|
async searchIndex(input) {
|
|
119
|
-
return this.request('/memory/search/index', { method: 'POST', body: input });
|
|
145
|
+
return this.request('/memory/search/index', { retryRead: true, method: 'POST', body: input });
|
|
120
146
|
}
|
|
121
147
|
async observations(ids) {
|
|
122
|
-
return this.request('/memory/memories/batch', { method: 'POST', body: { ids } });
|
|
148
|
+
return this.request('/memory/memories/batch', { retryRead: true, method: 'POST', body: { ids } });
|
|
123
149
|
}
|
|
124
150
|
async timeline(input) {
|
|
125
|
-
return this.request('/memory/timeline', { method: 'POST', body: input });
|
|
151
|
+
return this.request('/memory/timeline', { retryRead: true, method: 'POST', body: input });
|
|
126
152
|
}
|
|
127
153
|
async context(input) {
|
|
128
|
-
return this.request('/memory/context', { method: 'POST', body: input });
|
|
154
|
+
return this.request('/memory/context', { retryRead: true, method: 'POST', body: input });
|
|
129
155
|
}
|
|
130
156
|
async fileContext(input) {
|
|
131
|
-
return this.request('/memory/files/context', { method: 'POST', body: input });
|
|
157
|
+
return this.request('/memory/files/context', { retryRead: true, method: 'POST', body: input });
|
|
132
158
|
}
|
|
133
159
|
async stats() {
|
|
134
160
|
return this.request('/memory/stats');
|
|
@@ -196,6 +222,7 @@ export class VaultCloudClient {
|
|
|
196
222
|
}
|
|
197
223
|
async queryKnowledgeBase(id, input) {
|
|
198
224
|
return this.request(`/memory/knowledge-bases/${encodeURIComponent(id)}/query`, {
|
|
225
|
+
retryRead: true,
|
|
199
226
|
method: 'POST',
|
|
200
227
|
body: input,
|
|
201
228
|
});
|
|
@@ -242,7 +269,7 @@ export class VaultCloudClient {
|
|
|
242
269
|
return this.request(`/memory/memories/${encodeURIComponent(id)}`);
|
|
243
270
|
}
|
|
244
271
|
async toolUses(input) {
|
|
245
|
-
return this.request('/memory/events/batch', { method: 'POST', body: input });
|
|
272
|
+
return this.request('/memory/events/batch', { retryRead: true, method: 'POST', body: input });
|
|
246
273
|
}
|
|
247
274
|
async graph() { return this.request('/memory/graph'); }
|
|
248
275
|
async graphPresets() { return this.request('/memory/graph/presets'); }
|
|
@@ -261,50 +288,59 @@ export class VaultCloudClient {
|
|
|
261
288
|
const prefix = base.pathname.replace(/\/$/u, '');
|
|
262
289
|
return new URL(`${prefix}${path}`, base.origin);
|
|
263
290
|
}
|
|
264
|
-
async accessToken(
|
|
265
|
-
|
|
266
|
-
if (environmentToken)
|
|
267
|
-
return environmentToken;
|
|
268
|
-
const tokens =
|
|
269
|
-
if (!tokens)
|
|
291
|
+
async accessToken(session, rejectedToken) {
|
|
292
|
+
assertSession(this.home, session);
|
|
293
|
+
if (session.environmentToken)
|
|
294
|
+
return session.environmentToken;
|
|
295
|
+
const tokens = session.tokens;
|
|
296
|
+
if (!tokens)
|
|
270
297
|
throw new Error('Vault não autenticado. Execute `bunx --bun vault-go@latest login`.');
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
298
|
+
// A concurrent read may already have renewed this same session.
|
|
299
|
+
if (rejectedToken && rejectedToken !== tokens.accessToken)
|
|
300
|
+
return tokens.accessToken;
|
|
301
|
+
if (!rejectedToken && (!tokens.expiresAt || Date.now() < tokens.expiresAt - 60_000))
|
|
275
302
|
return tokens.accessToken;
|
|
276
|
-
if (
|
|
303
|
+
if (session.refreshing)
|
|
304
|
+
return session.refreshing;
|
|
305
|
+
if (!tokens.refreshToken)
|
|
277
306
|
throw new Error('Sessão expirada. Execute `bunx --bun vault-go@latest login --force`.');
|
|
307
|
+
const pending = (async () => {
|
|
308
|
+
const response = await this.cloudFetch(sessionEndpoint(session.base, '/auth/refresh'), {
|
|
309
|
+
method: 'POST', redirect: 'error',
|
|
310
|
+
headers: { 'content-type': 'application/json' },
|
|
311
|
+
body: JSON.stringify({ refreshToken: tokens.refreshToken }),
|
|
312
|
+
signal: AbortSignal.timeout(10_000),
|
|
313
|
+
});
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
await response.body?.cancel().catch(() => { });
|
|
316
|
+
assertSession(this.home, session);
|
|
317
|
+
throw new Error('Sessão expirada. Execute `bunx --bun vault-go@latest login --force`.');
|
|
318
|
+
}
|
|
319
|
+
const refreshed = (await response.json());
|
|
320
|
+
assertSession(this.home, session);
|
|
321
|
+
if (!refreshed || typeof refreshed.accessToken !== 'string' || !refreshed.accessToken) {
|
|
322
|
+
throw new Error('A API Vault retornou credenciais inválidas.');
|
|
323
|
+
}
|
|
324
|
+
const stored = { ...refreshed, expiresAt: Date.now() + (refreshed.expiresIn ?? 900) * 1000 };
|
|
325
|
+
saveTokens(stored, this.home);
|
|
326
|
+
session.tokens = stored;
|
|
327
|
+
session.key = JSON.stringify([session.base, session.environmentToken, stored.accessToken, stored.refreshToken, credentialFilesGeneration(this.home)]);
|
|
328
|
+
return stored.accessToken;
|
|
329
|
+
})();
|
|
330
|
+
session.refreshing = pending;
|
|
331
|
+
try {
|
|
332
|
+
return await pending;
|
|
278
333
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
headers: { 'content-type': 'application/json' },
|
|
283
|
-
body: JSON.stringify({ refreshToken: tokens.refreshToken }),
|
|
284
|
-
signal: AbortSignal.timeout(10_000),
|
|
285
|
-
});
|
|
286
|
-
if (!response.ok) {
|
|
287
|
-
throw new Error('Sessão expirada. Execute `bunx --bun vault-go@latest login --force`.');
|
|
334
|
+
finally {
|
|
335
|
+
if (session.refreshing === pending)
|
|
336
|
+
delete session.refreshing;
|
|
288
337
|
}
|
|
289
|
-
const refreshed = (await response.json());
|
|
290
|
-
// A refresh already in flight must not overwrite a newer login or logout.
|
|
291
|
-
const latest = loadTokens(this.home);
|
|
292
|
-
if (!latest)
|
|
293
|
-
throw new Error('Vault session was disconnected.');
|
|
294
|
-
if (latest.accessToken !== tokens.accessToken || latest.refreshToken !== tokens.refreshToken)
|
|
295
|
-
return latest.accessToken;
|
|
296
|
-
const stored = {
|
|
297
|
-
...refreshed,
|
|
298
|
-
expiresAt: Date.now() + (refreshed.expiresIn ?? 900) * 1000,
|
|
299
|
-
};
|
|
300
|
-
saveTokens(stored, this.home);
|
|
301
|
-
return stored.accessToken;
|
|
302
338
|
}
|
|
303
339
|
async request(path, options = {}) {
|
|
304
340
|
const start = Date.now();
|
|
305
341
|
let ok = false;
|
|
306
342
|
try {
|
|
307
|
-
const result = await this.performRequest(path, options);
|
|
343
|
+
const result = await this.performRequest(path, options, sessionFor(this.home));
|
|
308
344
|
ok = true;
|
|
309
345
|
return result;
|
|
310
346
|
}
|
|
@@ -313,14 +349,16 @@ export class VaultCloudClient {
|
|
|
313
349
|
recordActivity(this.home, (options.method ?? 'GET') + ' ' + path.split('?')[0], ok, Date.now() - start);
|
|
314
350
|
}
|
|
315
351
|
}
|
|
316
|
-
async performRequest(path, options
|
|
352
|
+
async performRequest(path, options, session, rejectedToken) {
|
|
353
|
+
options.signal?.throwIfAborted();
|
|
354
|
+
assertSession(this.home, session);
|
|
317
355
|
const headers = new Headers({ accept: 'application/json' });
|
|
318
356
|
if (options.body)
|
|
319
357
|
headers.set('content-type', 'application/json');
|
|
320
358
|
for (const [name, value] of Object.entries(options.headers ?? {}))
|
|
321
359
|
headers.set(name, value);
|
|
322
360
|
if (options.authenticated !== false) {
|
|
323
|
-
headers.set('authorization', `Bearer ${await this.accessToken(
|
|
361
|
+
headers.set('authorization', `Bearer ${await this.accessToken(session, rejectedToken)}`);
|
|
324
362
|
}
|
|
325
363
|
const request = {
|
|
326
364
|
method: options.method ?? 'GET',
|
|
@@ -330,21 +368,34 @@ export class VaultCloudClient {
|
|
|
330
368
|
};
|
|
331
369
|
if (options.body)
|
|
332
370
|
request.body = JSON.stringify(options.body);
|
|
333
|
-
|
|
371
|
+
options.signal?.throwIfAborted();
|
|
372
|
+
assertSession(this.home, session);
|
|
373
|
+
const endpoint = sessionEndpoint(session.base, path);
|
|
334
374
|
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
335
375
|
if (value !== undefined && value !== null && value !== '') {
|
|
336
376
|
endpoint.searchParams.set(key, String(value));
|
|
337
377
|
}
|
|
338
378
|
}
|
|
339
379
|
const response = await this.cloudFetch(endpoint, request);
|
|
340
|
-
if (response.status === 401 && options.authenticated !== false && !options.retried
|
|
380
|
+
if (response.status === 401 && options.authenticated !== false && !options.retried &&
|
|
381
|
+
(['GET', 'HEAD'].includes(options.method ?? 'GET') || options.retryRead)) {
|
|
341
382
|
try {
|
|
342
383
|
await response.body?.cancel();
|
|
343
384
|
}
|
|
344
385
|
catch { /* Release the rejected response before retrying. */ }
|
|
345
|
-
return this.performRequest(path, { ...options, retried: true });
|
|
386
|
+
return this.performRequest(path, { ...options, retried: true }, session, headers.get('authorization')?.slice(7));
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
options.signal?.throwIfAborted();
|
|
390
|
+
assertSession(this.home, session);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
await response.body?.cancel().catch(() => { });
|
|
394
|
+
throw error;
|
|
346
395
|
}
|
|
347
396
|
const text = await readResponseText(response, options.responseMaxBytes);
|
|
397
|
+
options.signal?.throwIfAborted();
|
|
398
|
+
assertSession(this.home, session);
|
|
348
399
|
const payload = text ? safeJson(text) : null;
|
|
349
400
|
if (!response.ok) {
|
|
350
401
|
const message = payload && typeof payload === 'object' && 'error' in payload
|
package/dist/config.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface VaultStatus {
|
|
|
21
21
|
export declare function vaultHome(environment?: NodeJS.ProcessEnv): string;
|
|
22
22
|
export declare function configPath(home?: string): string;
|
|
23
23
|
export declare function authPath(home?: string): string;
|
|
24
|
+
/** File identity survives value-level ABA (logout then login with the same API key). */
|
|
25
|
+
export declare function credentialFilesGeneration(home?: string): string;
|
|
24
26
|
export declare function loadConfig(home?: string): VaultConfig;
|
|
25
27
|
export declare function loadTokens(home?: string): StoredTokens | null;
|
|
26
28
|
export declare function saveConfig(config: VaultConfig, home?: string): void;
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { VAULT_CLOUD_API_URL } from './vault-cloud.js';
|
|
@@ -20,6 +20,20 @@ export function configPath(home = vaultHome()) {
|
|
|
20
20
|
export function authPath(home = vaultHome()) {
|
|
21
21
|
return join(home, 'auth.json');
|
|
22
22
|
}
|
|
23
|
+
/** File identity survives value-level ABA (logout then login with the same API key). */
|
|
24
|
+
export function credentialFilesGeneration(home = vaultHome()) {
|
|
25
|
+
return [configPath(home), authPath(home)].map(path => {
|
|
26
|
+
try {
|
|
27
|
+
const stat = statSync(path, { bigint: true });
|
|
28
|
+
return [stat.dev, stat.ino, stat.birthtimeNs, stat.mtimeNs, stat.ctimeNs, stat.size].join(':');
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === 'ENOENT')
|
|
32
|
+
return 'absent';
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}).join('|');
|
|
36
|
+
}
|
|
23
37
|
export function loadConfig(home = vaultHome()) {
|
|
24
38
|
const file = configPath(home);
|
|
25
39
|
const environmentApiUrl = process.env['VAULT_GO_API_URL'];
|
|
@@ -9,10 +9,23 @@ type MirrorJob = {
|
|
|
9
9
|
export declare function shouldMirrorCrystalMemory(job: MirrorJob): boolean;
|
|
10
10
|
export declare function crystalProjectFolder(project: unknown): string | null;
|
|
11
11
|
export declare function resolveCrystalProjectFolder(cloud: Pick<VaultMemoryApi, 'projects'>, projectId: string): Promise<string | null>;
|
|
12
|
+
export interface CrystalMirrorRecord {
|
|
13
|
+
sourceId: string;
|
|
14
|
+
project: string;
|
|
15
|
+
title: string;
|
|
16
|
+
content: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
}
|
|
19
|
+
/** written; locked (retry later); rejected (non-locked tool error); unavailable (Desktop unreachable). */
|
|
20
|
+
export type CrystalMirrorOutcome = 'written' | 'locked' | 'rejected' | 'unavailable';
|
|
21
|
+
export declare function writeCrystalMirror(home: string, record: CrystalMirrorRecord): Promise<CrystalMirrorOutcome>;
|
|
12
22
|
export declare function mirrorCrystalMemory(input: {
|
|
13
23
|
home: string;
|
|
14
24
|
cloud: Pick<VaultMemoryApi, 'projects'>;
|
|
15
25
|
job: MirrorJob;
|
|
16
26
|
result: Pick<ContextResult, 'title' | 'content' | 'concepts'>;
|
|
17
|
-
}): Promise<
|
|
27
|
+
}): Promise<{
|
|
28
|
+
outcome: CrystalMirrorOutcome | 'skipped';
|
|
29
|
+
record?: CrystalMirrorRecord;
|
|
30
|
+
}>;
|
|
18
31
|
export {};
|
|
@@ -33,23 +33,32 @@ export async function resolveCrystalProjectFolder(cloud, projectId) {
|
|
|
33
33
|
const oldest = namesakes.at(-1);
|
|
34
34
|
return oldest?.id === projectId ? folder : `${folder}-${projectId.slice(0, 8)}`;
|
|
35
35
|
}
|
|
36
|
-
export async function
|
|
37
|
-
if (!shouldMirrorCrystalMemory(input.job))
|
|
38
|
-
return;
|
|
39
|
-
const project = await resolveCrystalProjectFolder(input.cloud, input.job.projectId);
|
|
40
|
-
if (!project)
|
|
41
|
-
return;
|
|
36
|
+
export async function writeCrystalMirror(home, record) {
|
|
42
37
|
try {
|
|
43
|
-
const { callCrystalTool } = await import('./crystal-tools.js');
|
|
44
|
-
await callCrystalTool(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
tags: input.result.concepts,
|
|
49
|
-
sourceId: `vault-go:event:${input.job.id}`,
|
|
50
|
-
});
|
|
38
|
+
const { callCrystalTool, isCrystalLockedResult } = await import('./crystal-tools.js');
|
|
39
|
+
const result = await callCrystalTool(home, 'crystal_memory_write', { ...record });
|
|
40
|
+
if (isCrystalLockedResult(result))
|
|
41
|
+
return 'locked';
|
|
42
|
+
return result.isError ? 'rejected' : 'written';
|
|
51
43
|
}
|
|
52
44
|
catch {
|
|
53
|
-
|
|
45
|
+
return 'unavailable';
|
|
54
46
|
}
|
|
55
47
|
}
|
|
48
|
+
export async function mirrorCrystalMemory(input) {
|
|
49
|
+
if (!shouldMirrorCrystalMemory(input.job))
|
|
50
|
+
return { outcome: 'skipped' };
|
|
51
|
+
const project = await resolveCrystalProjectFolder(input.cloud, input.job.projectId);
|
|
52
|
+
if (!project)
|
|
53
|
+
return { outcome: 'skipped' };
|
|
54
|
+
const record = {
|
|
55
|
+
project,
|
|
56
|
+
title: input.result.title,
|
|
57
|
+
content: input.result.content,
|
|
58
|
+
tags: input.result.concepts,
|
|
59
|
+
sourceId: `vault-go:event:${input.job.id}`,
|
|
60
|
+
};
|
|
61
|
+
// Crystal MCP is optional; account memory already persisted. Only a locked
|
|
62
|
+
// Crystal is worth retrying, so the caller queues the record for that case.
|
|
63
|
+
return { outcome: await writeCrystalMirror(input.home, record), record };
|
|
64
|
+
}
|
package/dist/crystal-tools.d.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export declare const CRYSTAL_LOCKED = "CRYSTAL_LOCKED";
|
|
3
|
+
type CrystalToolResult = {
|
|
4
|
+
isError?: boolean | undefined;
|
|
5
|
+
content?: {
|
|
6
|
+
type: string;
|
|
7
|
+
text?: string;
|
|
8
|
+
}[] | undefined;
|
|
9
|
+
structuredContent?: Record<string, unknown> | undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare function isCrystalLockedResult(result: CrystalToolResult): boolean;
|
|
2
12
|
export declare function callCrystalTool(home: string, name: string, args: Record<string, unknown>): Promise<{
|
|
13
|
+
content: {
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
}[];
|
|
17
|
+
structuredContent: {
|
|
18
|
+
code: string;
|
|
19
|
+
};
|
|
20
|
+
isError: boolean;
|
|
21
|
+
} | {
|
|
3
22
|
isError?: boolean;
|
|
4
23
|
content: {
|
|
5
24
|
text: string;
|
|
6
25
|
type: "text";
|
|
7
26
|
}[];
|
|
27
|
+
structuredContent?: never;
|
|
8
28
|
}>;
|
|
9
|
-
export declare function registerCrystalTools(server: McpServer, home: string
|
|
29
|
+
export declare function registerCrystalTools(server: McpServer, home: string, options?: {
|
|
30
|
+
crystalMirrorStore?: import('./hook-queue.js').CrystalMirrorStoreFactory;
|
|
31
|
+
}): void;
|
|
32
|
+
export {};
|
package/dist/crystal-tools.js
CHANGED
|
@@ -5,6 +5,19 @@ import { z } from 'zod';
|
|
|
5
5
|
const unavailable = 'Abra o Vault Desktop e ative a integração Vault MCP no Crystal.';
|
|
6
6
|
const invalidConnection = 'Conexão local do Crystal inválida. Reative a integração no Vault Desktop.';
|
|
7
7
|
const MAX_RESPONSE = 32 * 1024 * 1024;
|
|
8
|
+
export const CRYSTAL_LOCKED = 'CRYSTAL_LOCKED';
|
|
9
|
+
const lockedMessage = `${CRYSTAL_LOCKED}: O Crystal está trancado no Vault Desktop. Peça ao usuário para destrancá-lo no Vault Desktop com Touch ID ou a senha do macOS e só então tente de novo; não repita a chamada em loop.`;
|
|
10
|
+
const lockedGuidance = ` Se o Crystal usar criptografia local e estiver trancado, a chamada falha com ${CRYSTAL_LOCKED}: peça ao usuário para destrancá-lo no Vault Desktop com Touch ID e não repita em loop.`;
|
|
11
|
+
// Why: Vault Desktop 0.2.76 reports a locked Crystal only as text starting with
|
|
12
|
+
// "Crystal trancado"; newer versions use the CRYSTAL_LOCKED prefix and code.
|
|
13
|
+
export function isCrystalLockedResult(result) {
|
|
14
|
+
if (!result.isError)
|
|
15
|
+
return false;
|
|
16
|
+
if (result.structuredContent?.code === CRYSTAL_LOCKED)
|
|
17
|
+
return true;
|
|
18
|
+
return (result.content ?? []).some(item => typeof item.text === 'string' &&
|
|
19
|
+
(/^CRYSTAL_LOCKED\b/.test(item.text.trimStart()) || /^Crystal trancado/i.test(item.text.trimStart())));
|
|
20
|
+
}
|
|
8
21
|
function connection(home) {
|
|
9
22
|
const file = join(home, 'crystal-connection.json');
|
|
10
23
|
try {
|
|
@@ -71,25 +84,37 @@ export async function callCrystalTool(home, name, args) {
|
|
|
71
84
|
}
|
|
72
85
|
const envelope = z.object({
|
|
73
86
|
jsonrpc: z.literal('2.0'), id: z.literal(id),
|
|
74
|
-
result: z.object({
|
|
87
|
+
result: z.object({
|
|
88
|
+
isError: z.boolean().optional(),
|
|
89
|
+
content: z.array(z.object({ type: z.literal('text'), text: z.string() })),
|
|
90
|
+
structuredContent: z.record(z.unknown()).optional(),
|
|
91
|
+
}),
|
|
75
92
|
}).safeParse(payload);
|
|
76
93
|
if (!envelope.success)
|
|
77
94
|
throw new Error('Resposta inválida do Crystal. Atualize o Vault Desktop.');
|
|
95
|
+
if (isCrystalLockedResult(envelope.data.result)) {
|
|
96
|
+
return { content: [{ type: 'text', text: lockedMessage }], structuredContent: { code: CRYSTAL_LOCKED }, isError: true };
|
|
97
|
+
}
|
|
78
98
|
return {
|
|
79
99
|
content: envelope.data.result.content.map(item => ({ ...item, text: item.text.replaceAll(local.token, '[redacted]') })),
|
|
80
100
|
...(envelope.data.result.isError ? { isError: true } : {}),
|
|
81
101
|
};
|
|
82
102
|
}
|
|
83
|
-
export function registerCrystalTools(server, home) {
|
|
103
|
+
export function registerCrystalTools(server, home, options = {}) {
|
|
84
104
|
const path = z.string().min(1).max(2000).refine(value => !/^(?:[a-zA-Z]:|[\\/])|(?:^|[\\/])\.\.(?:[\\/]|$)/.test(value) && !value.includes('\0'), 'Informe um caminho relativo dentro do Crystal.');
|
|
85
105
|
const notePath = path.refine(value => /\.(?:md|markdown)$/i.test(value), 'Informe uma nota Markdown do Crystal.');
|
|
86
106
|
const add = (name, description, inputSchema, readOnly = true, destructive = false) => {
|
|
87
107
|
server.registerTool(`vault_go_crystal_${name}`, {
|
|
88
|
-
description: `${description} Usa o Crystal do Vault Desktop aberto neste computador
|
|
108
|
+
description: `${description} Usa o Crystal do Vault Desktop aberto neste computador.${name === 'status' ? '' : lockedGuidance}`, inputSchema,
|
|
89
109
|
annotations: { readOnlyHint: readOnly, destructiveHint: destructive, idempotentHint: readOnly, openWorldHint: name === 'sync' },
|
|
90
110
|
}, async (input) => {
|
|
91
111
|
try {
|
|
92
|
-
|
|
112
|
+
const result = await callCrystalTool(home, `crystal_${name}`, input);
|
|
113
|
+
// A successful call proves the Crystal is unlocked; status also answers while locked.
|
|
114
|
+
if (!result.isError && name !== 'status') {
|
|
115
|
+
void import('./hook-queue.js').then(queue => queue.flushPendingCrystalMirrors(home, undefined, options.crystalMirrorStore ? { openStore: options.crystalMirrorStore } : {})).catch(() => { });
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
93
118
|
}
|
|
94
119
|
catch (error) {
|
|
95
120
|
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : unavailable }] };
|
|
@@ -102,7 +127,7 @@ export function registerCrystalTools(server, home) {
|
|
|
102
127
|
add('write', 'Cria ou atualiza uma nota Markdown no Crystal.', { path, content: z.string().max(10 * 1024 * 1024), title: z.string().max(1000).optional(), tags: z.array(z.string().max(200)).max(100).optional() }, false);
|
|
103
128
|
add('delete', 'Exclui uma nota do Crystal.', { path: notePath }, false, true);
|
|
104
129
|
add('sync', 'Sincroniza o Crystal com o Vault Cloud.', {}, false);
|
|
105
|
-
add('status', 'Mostra o estado do Crystal e da sincronização.', {});
|
|
130
|
+
add('status', 'Mostra o estado do Crystal e da sincronização. localEncryption.locked indica se o Crystal está trancado no Vault Desktop (syncState pode ser paused-locked).', {});
|
|
106
131
|
add('memory_list', 'Lista notas duráveis em {projeto}/Memory no Crystal.', { project: z.string().trim().min(1).max(200).optional() });
|
|
107
132
|
add('memory_write', 'Grava uma nota durável em {projeto}/Memory no Crystal.', {
|
|
108
133
|
project: z.string().trim().min(1).max(200),
|
package/dist/grok-awareness.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { isAbsolute, join } from 'node:path';
|
|
5
5
|
import { loadConfig, loadTokens } from './config.js';
|
|
6
6
|
import { redact } from './capture-privacy.js';
|
|
@@ -133,6 +133,55 @@ async function lock(home) {
|
|
|
133
133
|
}
|
|
134
134
|
throw new Error('Awareness writer is busy');
|
|
135
135
|
}
|
|
136
|
+
function logLines(memoryDir) {
|
|
137
|
+
const log = join(memoryDir, 'log');
|
|
138
|
+
if (!existsSync(log))
|
|
139
|
+
return [];
|
|
140
|
+
const stat = lstatSync(log);
|
|
141
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || realpathSync(log) !== log)
|
|
142
|
+
return [];
|
|
143
|
+
const lines = [];
|
|
144
|
+
for (const name of readdirSync(log)) {
|
|
145
|
+
if (!name.endsWith('.md'))
|
|
146
|
+
continue;
|
|
147
|
+
const file = join(log, name);
|
|
148
|
+
const fileStat = lstatSync(file);
|
|
149
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile() || fileStat.size > MAX_LOG_BYTES)
|
|
150
|
+
continue;
|
|
151
|
+
lines.push(...readFileSync(file, 'utf8').split('\n').map(line => line.trim()).filter(line => line.startsWith('- ') && line.length <= 500));
|
|
152
|
+
}
|
|
153
|
+
return lines.slice(-80);
|
|
154
|
+
}
|
|
155
|
+
function writeLiveIndex(root, agentId, _now) {
|
|
156
|
+
const agents = ensureDirectory(root, ['agents']);
|
|
157
|
+
const seat = ensureDirectory(root, ['agents', agentId, 'memory']);
|
|
158
|
+
const lines = logLines(seat).slice(-80);
|
|
159
|
+
if (lines.length < 80) {
|
|
160
|
+
for (const name of readdirSync(agents)) {
|
|
161
|
+
if (lines.length >= 80 || name === agentId || name.startsWith('.'))
|
|
162
|
+
continue;
|
|
163
|
+
const other = join(agents, name, 'memory');
|
|
164
|
+
try {
|
|
165
|
+
const stat = lstatSync(other);
|
|
166
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || realpathSync(other) !== other)
|
|
167
|
+
continue;
|
|
168
|
+
lines.push(...logLines(other).slice(0, 80 - lines.length));
|
|
169
|
+
}
|
|
170
|
+
catch { /* unreadable sibling stays out of the index */ }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const body = ['# Vault Memory', '', ...lines.slice(0, 80), ''].join('\n');
|
|
174
|
+
const file = join(seat, 'zz-vault-memory-inject.md');
|
|
175
|
+
const temp = join(seat, `.index-${randomUUID()}.tmp`);
|
|
176
|
+
writeFileSync(temp, body, { mode: 0o600, flag: 'wx' });
|
|
177
|
+
try {
|
|
178
|
+
renameSync(temp, file);
|
|
179
|
+
chmodSync(file, 0o600);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
rmSync(temp, { force: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
136
185
|
export async function pushAwareness(home, input, now = new Date(), io = fs) {
|
|
137
186
|
const initial = load(home), identity = owner(home);
|
|
138
187
|
let release;
|
|
@@ -222,6 +271,10 @@ export async function pushAwareness(home, input, now = new Date(), io = fs) {
|
|
|
222
271
|
}
|
|
223
272
|
chmodSync(file, 0o600);
|
|
224
273
|
}
|
|
274
|
+
try {
|
|
275
|
+
writeLiveIndex(config.agentDataRoot, input.agentId, now);
|
|
276
|
+
}
|
|
277
|
+
catch { /* the monthly log remains the source of record */ }
|
|
225
278
|
config.lastPush = Date.now();
|
|
226
279
|
delete config.lastError;
|
|
227
280
|
save(home, config);
|
package/dist/hook-install.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
export declare const HOOK_OWNER = "Vault Go: captura e contexto";
|
|
2
2
|
export declare const HOOK_EVENTS: {
|
|
3
|
+
readonly Setup: "setup";
|
|
3
4
|
readonly SessionStart: "context";
|
|
4
5
|
readonly UserPromptSubmit: "session-init";
|
|
5
6
|
readonly PreToolUse: "file-context";
|
|
6
7
|
readonly PostToolUse: "observation";
|
|
7
8
|
readonly Stop: "summarize";
|
|
9
|
+
readonly SessionEnd: "session-end";
|
|
8
10
|
};
|
|
9
11
|
export declare function quoteShell(value: string): string;
|
|
10
12
|
export declare function hookEntryPath(configHome?: string): string;
|
|
11
13
|
export declare function hookCommand(adapter: string, event: string, configHome?: string): string;
|
|
14
|
+
declare const CAPTURE_HOMES: Record<string, string>;
|
|
15
|
+
export declare function installCaptureHooks(adapter: keyof typeof CAPTURE_HOMES, userHome?: string, configHome?: string): void;
|
|
12
16
|
export type HookClient = "claude" | "codex" | "grok" | "agy" | "gemini";
|
|
13
17
|
export declare function installClientHooks(client: HookClient, userHome?: string, configHome?: string): void;
|
|
14
18
|
export declare function installOpenCodePlugin(userHome?: string, configHome?: string): void;
|
package/dist/hook-install.js
CHANGED
|
@@ -35,11 +35,13 @@ function atomicWriteText(path, text) {
|
|
|
35
35
|
}
|
|
36
36
|
export const HOOK_OWNER = "Vault Go: captura e contexto";
|
|
37
37
|
export const HOOK_EVENTS = {
|
|
38
|
+
Setup: "setup",
|
|
38
39
|
SessionStart: "context",
|
|
39
40
|
UserPromptSubmit: "session-init",
|
|
40
41
|
PreToolUse: "file-context",
|
|
41
42
|
PostToolUse: "observation",
|
|
42
43
|
Stop: "summarize",
|
|
44
|
+
SessionEnd: "session-end",
|
|
43
45
|
};
|
|
44
46
|
const GEMINI_EVENTS = {
|
|
45
47
|
SessionStart: "context",
|
|
@@ -77,7 +79,7 @@ function hookManifest(adapter, configHome = vaultHome()) {
|
|
|
77
79
|
timeout: event === "context" ? 60 : 20,
|
|
78
80
|
statusMessage: HOOK_OWNER,
|
|
79
81
|
}),
|
|
80
|
-
...(adapter !== "gemini" && (event === "observation" || event === "summarize")
|
|
82
|
+
...(adapter !== "gemini" && (event === "observation" || event === "summarize" || event === "session-end")
|
|
81
83
|
? { async: true }
|
|
82
84
|
: {}),
|
|
83
85
|
},
|
|
@@ -102,8 +104,37 @@ function agyHookManifest(configHome = vaultHome()) {
|
|
|
102
104
|
{ matcher: "*", hooks: [agyHandler("observation", configHome)] },
|
|
103
105
|
],
|
|
104
106
|
Stop: [agyHandler("summarize", configHome)],
|
|
107
|
+
SessionEnd: [agyHandler("session-end", configHome)],
|
|
105
108
|
};
|
|
106
109
|
}
|
|
110
|
+
const CAPTURE_HOMES = {
|
|
111
|
+
cursor: ".cursor/hooks.json",
|
|
112
|
+
windsurf: ".codeium/windsurf/hooks.json",
|
|
113
|
+
openclaw: ".openclaw/hooks.json",
|
|
114
|
+
hermes: ".hermes/hooks.json",
|
|
115
|
+
};
|
|
116
|
+
export function installCaptureHooks(adapter, userHome = homedir(), configHome = vaultHome()) {
|
|
117
|
+
const relative = CAPTURE_HOMES[adapter];
|
|
118
|
+
if (!relative)
|
|
119
|
+
throw new Error("Adaptador de captura desconhecido.");
|
|
120
|
+
const path = join(userHome, relative);
|
|
121
|
+
const config = existsSync(path) ? readJsonObject(path) : {};
|
|
122
|
+
const hooks = {
|
|
123
|
+
...(config.hooks && typeof config.hooks === "object" && !Array.isArray(config.hooks) ? config.hooks : {}),
|
|
124
|
+
};
|
|
125
|
+
const desired = hookManifest(adapter, configHome).hooks;
|
|
126
|
+
for (const event of Object.keys(desired)) {
|
|
127
|
+
const current = hooks[event];
|
|
128
|
+
const retained = Array.isArray(current)
|
|
129
|
+
? current.filter((group) => {
|
|
130
|
+
const text = JSON.stringify(group);
|
|
131
|
+
return !text.includes(HOOK_OWNER);
|
|
132
|
+
})
|
|
133
|
+
: [];
|
|
134
|
+
hooks[event] = [...retained, ...(desired[event] || [])];
|
|
135
|
+
}
|
|
136
|
+
atomicWriteJson(path, { ...config, hooks });
|
|
137
|
+
}
|
|
107
138
|
export function installClientHooks(client, userHome = homedir(), configHome = vaultHome()) {
|
|
108
139
|
if (client === "grok") {
|
|
109
140
|
atomicWriteJson(join(userHome, ".grok", "hooks", "vault-go.json"), hookManifest("grok", configHome));
|
|
@@ -173,6 +204,15 @@ export function repairAdditionalClientHooks(userHome = homedir(), configHome = v
|
|
|
173
204
|
if (server && server.enabled !== false)
|
|
174
205
|
installOpenCodePlugin(userHome, configHome);
|
|
175
206
|
}, report);
|
|
207
|
+
for (const adapter of Object.keys(CAPTURE_HOMES)) {
|
|
208
|
+
const relative = CAPTURE_HOMES[adapter];
|
|
209
|
+
if (!relative)
|
|
210
|
+
continue;
|
|
211
|
+
const directory = join(userHome, relative.split("/").slice(0, -1).join("/"));
|
|
212
|
+
if (existsSync(directory)) {
|
|
213
|
+
repairClient(adapter, () => installCaptureHooks(adapter, userHome, configHome), report);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
176
216
|
}
|
|
177
217
|
function reportRepairWarning(message) {
|
|
178
218
|
process.stderr.write(message + "\n");
|