vault-go 0.29.2 → 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 +40 -24
- package/dist/capabilities.js +8 -6
- package/dist/capture-privacy.js +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +16 -1
- package/dist/cloud.d.ts +22 -0
- package/dist/cloud.js +131 -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 +27 -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 +68 -0
- package/dist/skill-context.d.ts +4 -0
- package/dist/skill-context.js +16 -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 +53 -0
- package/dist/skill-local.js +251 -0
- package/dist/skill-tools.d.ts +52 -0
- package/dist/skill-tools.js +356 -0
- 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 +19 -1
package/dist/cloud.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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;
|
|
5
|
+
const SKILL_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
4
6
|
const KNOWLEDGE_BASE_RESPONSE_LIMIT_ERROR = 'A base de conhecimento excede o limite de resposta de 8 MiB.';
|
|
5
7
|
const observationTypes = new Set(['decision', 'discovery', 'fix', 'feature', 'refactor', 'other']);
|
|
6
8
|
const retentionClasses = new Set(['transient', 'supporting', 'durable', 'critical']);
|
|
@@ -42,6 +44,31 @@ function withoutClientDecisions(input) {
|
|
|
42
44
|
return safeRecord;
|
|
43
45
|
}) };
|
|
44
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
|
+
}
|
|
45
72
|
export class VaultCloudClient {
|
|
46
73
|
home;
|
|
47
74
|
cloudFetch;
|
|
@@ -103,7 +130,7 @@ export class VaultCloudClient {
|
|
|
103
130
|
return { deleted: true, id };
|
|
104
131
|
}
|
|
105
132
|
async search(input) {
|
|
106
|
-
return this.request('/memory/search', { method: 'POST', body: input });
|
|
133
|
+
return this.request('/memory/search', { retryRead: true, method: 'POST', body: input });
|
|
107
134
|
}
|
|
108
135
|
async retrieve(input) {
|
|
109
136
|
return this.request('/memory/retrieve', { method: 'POST', body: input });
|
|
@@ -115,19 +142,19 @@ export class VaultCloudClient {
|
|
|
115
142
|
return this.request('/memory/decisions/evaluate', { method: 'POST', body: input });
|
|
116
143
|
}
|
|
117
144
|
async searchIndex(input) {
|
|
118
|
-
return this.request('/memory/search/index', { method: 'POST', body: input });
|
|
145
|
+
return this.request('/memory/search/index', { retryRead: true, method: 'POST', body: input });
|
|
119
146
|
}
|
|
120
147
|
async observations(ids) {
|
|
121
|
-
return this.request('/memory/memories/batch', { method: 'POST', body: { ids } });
|
|
148
|
+
return this.request('/memory/memories/batch', { retryRead: true, method: 'POST', body: { ids } });
|
|
122
149
|
}
|
|
123
150
|
async timeline(input) {
|
|
124
|
-
return this.request('/memory/timeline', { method: 'POST', body: input });
|
|
151
|
+
return this.request('/memory/timeline', { retryRead: true, method: 'POST', body: input });
|
|
125
152
|
}
|
|
126
153
|
async context(input) {
|
|
127
|
-
return this.request('/memory/context', { method: 'POST', body: input });
|
|
154
|
+
return this.request('/memory/context', { retryRead: true, method: 'POST', body: input });
|
|
128
155
|
}
|
|
129
156
|
async fileContext(input) {
|
|
130
|
-
return this.request('/memory/files/context', { method: 'POST', body: input });
|
|
157
|
+
return this.request('/memory/files/context', { retryRead: true, method: 'POST', body: input });
|
|
131
158
|
}
|
|
132
159
|
async stats() {
|
|
133
160
|
return this.request('/memory/stats');
|
|
@@ -195,6 +222,7 @@ export class VaultCloudClient {
|
|
|
195
222
|
}
|
|
196
223
|
async queryKnowledgeBase(id, input) {
|
|
197
224
|
return this.request(`/memory/knowledge-bases/${encodeURIComponent(id)}/query`, {
|
|
225
|
+
retryRead: true,
|
|
198
226
|
method: 'POST',
|
|
199
227
|
body: input,
|
|
200
228
|
});
|
|
@@ -204,6 +232,36 @@ export class VaultCloudClient {
|
|
|
204
232
|
method: 'DELETE',
|
|
205
233
|
});
|
|
206
234
|
}
|
|
235
|
+
async skills(query) { return this.request('/v1/skills', { query }); }
|
|
236
|
+
async skillFolders() { return this.request('/v1/skills/folders'); }
|
|
237
|
+
async skill(id, version) {
|
|
238
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}`, { query: { version } });
|
|
239
|
+
}
|
|
240
|
+
async skillFile(id, path, version) {
|
|
241
|
+
const encoded = path.split('/').map(encodeURIComponent).join('/');
|
|
242
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}/files/${encoded}`, { query: { version }, responseMaxBytes: SKILL_RESPONSE_MAX_BYTES });
|
|
243
|
+
}
|
|
244
|
+
async createSkill(input, idempotencyKey) {
|
|
245
|
+
return this.request('/v1/skills', { method: 'POST', body: input, timeoutMs: 60_000, ...(idempotencyKey ? { headers: { 'idempotency-key': idempotencyKey } } : {}) });
|
|
246
|
+
}
|
|
247
|
+
async createSkillVersion(id, input, idempotencyKey) {
|
|
248
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}/versions`, { method: 'POST', body: input, timeoutMs: 60_000, ...(idempotencyKey ? { headers: { 'idempotency-key': idempotencyKey } } : {}) });
|
|
249
|
+
}
|
|
250
|
+
async updateSkill(id, input) {
|
|
251
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}`, { method: 'PATCH', body: input });
|
|
252
|
+
}
|
|
253
|
+
async setSkillProjects(id, projectIds) {
|
|
254
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}/projects`, { method: 'PUT', body: { projectIds } });
|
|
255
|
+
}
|
|
256
|
+
async createSkillFolder(input) {
|
|
257
|
+
return this.request('/v1/skills/folders', { method: 'POST', body: input });
|
|
258
|
+
}
|
|
259
|
+
async updateSkillFolder(id, input) {
|
|
260
|
+
return this.request(`/v1/skills/folders/${encodeURIComponent(id)}`, { method: 'PATCH', body: input });
|
|
261
|
+
}
|
|
262
|
+
async skillDownloadGrant(id, versionId) {
|
|
263
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}/download-grants`, { method: 'POST', body: versionId ? { versionId } : {} });
|
|
264
|
+
}
|
|
207
265
|
async updatePreferences(input) {
|
|
208
266
|
return this.request('/preferences', { method: 'PATCH', body: input });
|
|
209
267
|
}
|
|
@@ -211,7 +269,7 @@ export class VaultCloudClient {
|
|
|
211
269
|
return this.request(`/memory/memories/${encodeURIComponent(id)}`);
|
|
212
270
|
}
|
|
213
271
|
async toolUses(input) {
|
|
214
|
-
return this.request('/memory/events/batch', { method: 'POST', body: input });
|
|
272
|
+
return this.request('/memory/events/batch', { retryRead: true, method: 'POST', body: input });
|
|
215
273
|
}
|
|
216
274
|
async graph() { return this.request('/memory/graph'); }
|
|
217
275
|
async graphPresets() { return this.request('/memory/graph/presets'); }
|
|
@@ -230,50 +288,59 @@ export class VaultCloudClient {
|
|
|
230
288
|
const prefix = base.pathname.replace(/\/$/u, '');
|
|
231
289
|
return new URL(`${prefix}${path}`, base.origin);
|
|
232
290
|
}
|
|
233
|
-
async accessToken(
|
|
234
|
-
|
|
235
|
-
if (environmentToken)
|
|
236
|
-
return environmentToken;
|
|
237
|
-
const tokens =
|
|
238
|
-
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)
|
|
239
297
|
throw new Error('Vault não autenticado. Execute `bunx --bun vault-go@latest login`.');
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
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))
|
|
244
302
|
return tokens.accessToken;
|
|
245
|
-
if (
|
|
303
|
+
if (session.refreshing)
|
|
304
|
+
return session.refreshing;
|
|
305
|
+
if (!tokens.refreshToken)
|
|
246
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;
|
|
247
333
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
headers: { 'content-type': 'application/json' },
|
|
252
|
-
body: JSON.stringify({ refreshToken: tokens.refreshToken }),
|
|
253
|
-
signal: AbortSignal.timeout(10_000),
|
|
254
|
-
});
|
|
255
|
-
if (!response.ok) {
|
|
256
|
-
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;
|
|
257
337
|
}
|
|
258
|
-
const refreshed = (await response.json());
|
|
259
|
-
// A refresh already in flight must not overwrite a newer login or logout.
|
|
260
|
-
const latest = loadTokens(this.home);
|
|
261
|
-
if (!latest)
|
|
262
|
-
throw new Error('Vault session was disconnected.');
|
|
263
|
-
if (latest.accessToken !== tokens.accessToken || latest.refreshToken !== tokens.refreshToken)
|
|
264
|
-
return latest.accessToken;
|
|
265
|
-
const stored = {
|
|
266
|
-
...refreshed,
|
|
267
|
-
expiresAt: Date.now() + (refreshed.expiresIn ?? 900) * 1000,
|
|
268
|
-
};
|
|
269
|
-
saveTokens(stored, this.home);
|
|
270
|
-
return stored.accessToken;
|
|
271
338
|
}
|
|
272
339
|
async request(path, options = {}) {
|
|
273
340
|
const start = Date.now();
|
|
274
341
|
let ok = false;
|
|
275
342
|
try {
|
|
276
|
-
const result = await this.performRequest(path, options);
|
|
343
|
+
const result = await this.performRequest(path, options, sessionFor(this.home));
|
|
277
344
|
ok = true;
|
|
278
345
|
return result;
|
|
279
346
|
}
|
|
@@ -282,12 +349,16 @@ export class VaultCloudClient {
|
|
|
282
349
|
recordActivity(this.home, (options.method ?? 'GET') + ' ' + path.split('?')[0], ok, Date.now() - start);
|
|
283
350
|
}
|
|
284
351
|
}
|
|
285
|
-
async performRequest(path, options
|
|
352
|
+
async performRequest(path, options, session, rejectedToken) {
|
|
353
|
+
options.signal?.throwIfAborted();
|
|
354
|
+
assertSession(this.home, session);
|
|
286
355
|
const headers = new Headers({ accept: 'application/json' });
|
|
287
356
|
if (options.body)
|
|
288
357
|
headers.set('content-type', 'application/json');
|
|
358
|
+
for (const [name, value] of Object.entries(options.headers ?? {}))
|
|
359
|
+
headers.set(name, value);
|
|
289
360
|
if (options.authenticated !== false) {
|
|
290
|
-
headers.set('authorization', `Bearer ${await this.accessToken(
|
|
361
|
+
headers.set('authorization', `Bearer ${await this.accessToken(session, rejectedToken)}`);
|
|
291
362
|
}
|
|
292
363
|
const request = {
|
|
293
364
|
method: options.method ?? 'GET',
|
|
@@ -297,21 +368,34 @@ export class VaultCloudClient {
|
|
|
297
368
|
};
|
|
298
369
|
if (options.body)
|
|
299
370
|
request.body = JSON.stringify(options.body);
|
|
300
|
-
|
|
371
|
+
options.signal?.throwIfAborted();
|
|
372
|
+
assertSession(this.home, session);
|
|
373
|
+
const endpoint = sessionEndpoint(session.base, path);
|
|
301
374
|
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
302
375
|
if (value !== undefined && value !== null && value !== '') {
|
|
303
376
|
endpoint.searchParams.set(key, String(value));
|
|
304
377
|
}
|
|
305
378
|
}
|
|
306
379
|
const response = await this.cloudFetch(endpoint, request);
|
|
307
|
-
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)) {
|
|
308
382
|
try {
|
|
309
383
|
await response.body?.cancel();
|
|
310
384
|
}
|
|
311
385
|
catch { /* Release the rejected response before retrying. */ }
|
|
312
|
-
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;
|
|
313
395
|
}
|
|
314
396
|
const text = await readResponseText(response, options.responseMaxBytes);
|
|
397
|
+
options.signal?.throwIfAborted();
|
|
398
|
+
assertSession(this.home, session);
|
|
315
399
|
const payload = text ? safeJson(text) : null;
|
|
316
400
|
if (!response.ok) {
|
|
317
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;
|