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.
Files changed (50) hide show
  1. package/README.md +12 -0
  2. package/dist/agent-cost.d.ts +11 -0
  3. package/dist/agent-cost.js +116 -0
  4. package/dist/auth.js +29 -2
  5. package/dist/capabilities.d.ts +30 -24
  6. package/dist/capabilities.js +7 -6
  7. package/dist/capture-privacy.js +1 -1
  8. package/dist/cli.d.ts +2 -1
  9. package/dist/cli.js +16 -1
  10. package/dist/cloud.js +98 -47
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.js +15 -1
  13. package/dist/crystal-memory-mirror.d.ts +14 -1
  14. package/dist/crystal-memory-mirror.js +24 -15
  15. package/dist/crystal-tools.d.ts +24 -1
  16. package/dist/crystal-tools.js +30 -5
  17. package/dist/grok-awareness.js +54 -1
  18. package/dist/hook-install.d.ts +4 -0
  19. package/dist/hook-install.js +41 -1
  20. package/dist/hook-queue.d.ts +18 -0
  21. package/dist/hook-queue.js +115 -3
  22. package/dist/hook-runner.js +1 -1
  23. package/dist/hooks.d.ts +1 -1
  24. package/dist/hooks.js +17 -8
  25. package/dist/index.js +5 -1
  26. package/dist/installer.js +8 -1
  27. package/dist/legacy-brand.d.ts +2 -0
  28. package/dist/legacy-brand.js +5 -0
  29. package/dist/local-service.js +11 -0
  30. package/dist/memory-archive.d.ts +25 -0
  31. package/dist/memory-archive.js +65 -0
  32. package/dist/modes.d.ts +1 -1
  33. package/dist/modes.js +3 -1
  34. package/dist/opencode-plugin.d.ts +29 -1
  35. package/dist/opencode-plugin.js +36 -1
  36. package/dist/server.js +66 -0
  37. package/dist/skill-download.d.ts +6 -0
  38. package/dist/skill-download.js +48 -0
  39. package/dist/skill-library.d.ts +8 -0
  40. package/dist/skill-library.js +32 -0
  41. package/dist/skill-local.d.ts +7 -0
  42. package/dist/skill-local.js +44 -7
  43. package/dist/skill-tools.js +13 -26
  44. package/dist/skills-cli.d.ts +31 -0
  45. package/dist/skills-cli.js +90 -0
  46. package/dist/transcript-config.d.ts +1 -1
  47. package/dist/transcript-config.js +4 -3
  48. package/dist/transcript-parser.js +1 -1
  49. package/dist/transcript-tools.js +1 -1
  50. package/package.json +13 -1
@@ -1,6 +1,8 @@
1
1
  import { VaultCloudClient } from './cloud.js';
2
2
  import { generateContext, type ContextResult } from './context-engines.js';
3
3
  import { type TelegramObservation } from './telegram.js';
4
+ import { privateStore } from './private-store.js';
5
+ import { type CrystalMirrorRecord } from './crystal-memory-mirror.js';
4
6
  export interface GenerationJob {
5
7
  id: string;
6
8
  projectId: string;
@@ -19,6 +21,21 @@ export interface GenerationJob {
19
21
  lastError?: string;
20
22
  result?: Pick<ContextResult, 'title' | 'content' | 'facts' | 'concepts' | 'type'>;
21
23
  }
24
+ export declare function ownerKey(home: string): string;
25
+ export interface PendingCrystalMirror extends CrystalMirrorRecord {
26
+ owner: string;
27
+ queuedAt: number;
28
+ }
29
+ export type CrystalMirrorStore = Pick<ReturnType<typeof privateStore>, 'keyStorage' | 'read' | 'write' | 'remove' | 'close'>;
30
+ export type CrystalMirrorStoreFactory = (home: string) => CrystalMirrorStore;
31
+ export declare const MAX_PENDING_CRYSTAL_MIRRORS = 500;
32
+ export declare function pendingCrystalMirrors(home?: string, openStore?: CrystalMirrorStoreFactory): Promise<PendingCrystalMirror[]>;
33
+ export declare function queuePendingCrystalMirror(home: string, owner: string, record: CrystalMirrorRecord, openStore?: CrystalMirrorStoreFactory): Promise<void>;
34
+ /** Retries locked Crystal copies for this account, stopping at the first sign the Crystal is still locked or closed. */
35
+ export declare function flushPendingCrystalMirrors(home?: string, owner?: string, options?: {
36
+ limit?: number;
37
+ openStore?: CrystalMirrorStoreFactory;
38
+ }): Promise<number>;
22
39
  export declare function enqueueGeneration(home: string, job: GenerationJob): Promise<void>;
23
40
  export declare function generationQueueStatus(home?: string): {
24
41
  pending: number;
@@ -28,4 +45,5 @@ export declare function generationQueueStatus(home?: string): {
28
45
  };
29
46
  export declare function processGenerationQueue(home?: string, cloud?: VaultCloudClient, generate?: typeof generateContext, options?: {
30
47
  notifyObservation?: (observation: TelegramObservation) => Promise<unknown>;
48
+ crystalMirrorStore?: CrystalMirrorStoreFactory;
31
49
  }): Promise<boolean>;
@@ -6,9 +6,10 @@ import { loadConfig, loadTokens, vaultHome } from './config.js';
6
6
  import { generateContext, selectedContextEngine } from './context-engines.js';
7
7
  import { notifyTelegramObservation } from './telegram.js';
8
8
  import { pushAwareness } from './grok-awareness.js';
9
- import { mirrorCrystalMemory } from './crystal-memory-mirror.js';
9
+ import { privateStore } from './private-store.js';
10
+ import { mirrorCrystalMemory, writeCrystalMirror } from './crystal-memory-mirror.js';
10
11
  const queuePath = (home) => join(home, 'generation-queue.json');
11
- function ownerKey(home) {
12
+ export function ownerKey(home) {
12
13
  const config = loadConfig(home);
13
14
  return createHash('sha256').update(JSON.stringify([config.apiUrl, process.env['VAULT_GO_TOKEN'] || config.email || loadTokens(home)?.accessToken || 'anonymous'])).digest('hex');
14
15
  }
@@ -115,6 +116,113 @@ function appliedObservationType(result, sourceId, fallback) {
115
116
  ? observationType
116
117
  : fallback;
117
118
  }
119
+ export const MAX_PENDING_CRYSTAL_MIRRORS = 500;
120
+ const PENDING_MIRROR_STORE = 'crystal-pending-mirrors.json';
121
+ const MAX_PENDING_MIRROR_BYTES = 4 * 1024 * 1024 - 64 * 1024; // private-store refuses records above 4 MiB
122
+ // Why: the user chose to encrypt the Crystal, so its copies never touch disk in
123
+ // plaintext. Without a durable key they live only for this process lifetime.
124
+ const memoryMirrors = new Map();
125
+ function openMirrorStore(home, factory) {
126
+ try {
127
+ const store = factory(home);
128
+ if (store.keyStorage !== 'ephemeral')
129
+ return store;
130
+ store.close();
131
+ }
132
+ catch { /* OS key store locked or unavailable */ }
133
+ return null;
134
+ }
135
+ function readStoredMirrors(home, store) {
136
+ if (!existsSync(join(home, PENDING_MIRROR_STORE)))
137
+ return [];
138
+ const value = store.read(PENDING_MIRROR_STORE);
139
+ if (value?.v === 1 && Array.isArray(value.items))
140
+ return value.items;
141
+ // Local key changed or the file was altered: the copies cannot be recovered.
142
+ store.remove(PENDING_MIRROR_STORE);
143
+ console.error('vault-go: cópias pendentes para o Crystal ilegíveis (chave local mudou) foram descartadas.');
144
+ return [];
145
+ }
146
+ function boundPendingMirrors(items) {
147
+ let next = items.slice(-MAX_PENDING_CRYSTAL_MIRRORS);
148
+ while (next.length && Buffer.byteLength(JSON.stringify({ v: 1, items: next })) > MAX_PENDING_MIRROR_BYTES)
149
+ next = next.slice(1);
150
+ const dropped = items.length - next.length;
151
+ if (dropped > 0)
152
+ console.error(`vault-go: ${dropped} cópia(s) pendente(s) mais antiga(s) para o Crystal descartada(s) pelo limite local.`);
153
+ return next;
154
+ }
155
+ async function mutatePendingMirrors(home, factory, change) {
156
+ const release = await lock(home, 'crystal-mirror-storage');
157
+ try {
158
+ const store = openMirrorStore(home, factory);
159
+ const memory = memoryMirrors.get(home) ?? [];
160
+ if (!store) {
161
+ memoryMirrors.set(home, boundPendingMirrors(change(memory)));
162
+ return 'memory';
163
+ }
164
+ try {
165
+ // Copies held in memory while the key was unavailable move into the encrypted file.
166
+ const stored = readStoredMirrors(home, store);
167
+ const merged = [...stored.filter(item => !memory.some(held => held.sourceId === item.sourceId)), ...memory];
168
+ const next = boundPendingMirrors(change(merged));
169
+ if (next.length)
170
+ store.write(PENDING_MIRROR_STORE, { v: 1, items: next });
171
+ else
172
+ store.remove(PENDING_MIRROR_STORE);
173
+ memoryMirrors.delete(home);
174
+ return 'encrypted';
175
+ }
176
+ finally {
177
+ store.close();
178
+ }
179
+ }
180
+ finally {
181
+ release?.();
182
+ }
183
+ }
184
+ export async function pendingCrystalMirrors(home = vaultHome(), openStore = privateStore) {
185
+ let items = [];
186
+ await mutatePendingMirrors(home, openStore, current => (items = current));
187
+ return items;
188
+ }
189
+ export async function queuePendingCrystalMirror(home, owner, record, openStore = privateStore) {
190
+ let held = 0;
191
+ const storage = await mutatePendingMirrors(home, openStore, items => {
192
+ // Same sourceId replaces the older copy; Desktop also dedups writes by sourceId.
193
+ const next = [...items.filter(item => item.sourceId !== record.sourceId), { ...record, owner, queuedAt: Date.now() }];
194
+ held = next.length;
195
+ return next;
196
+ });
197
+ if (storage === 'memory')
198
+ console.error(`vault-go: chave local indisponível; ${held} cópia(s) pendente(s) para o Crystal mantida(s) só em memória.`);
199
+ }
200
+ /** Retries locked Crystal copies for this account, stopping at the first sign the Crystal is still locked or closed. */
201
+ export async function flushPendingCrystalMirrors(home = vaultHome(), owner = ownerKey(home), options = {}) {
202
+ if (!existsSync(join(home, PENDING_MIRROR_STORE)) && !memoryMirrors.get(home)?.length)
203
+ return 0;
204
+ const openStore = options.openStore ?? privateStore;
205
+ const release = await lock(home, 'crystal-mirror-flush', false);
206
+ if (!release)
207
+ return 0;
208
+ let written = 0;
209
+ try {
210
+ const items = (await pendingCrystalMirrors(home, openStore)).filter(entry => entry.owner === owner).slice(0, options.limit ?? 25);
211
+ for (const item of items) {
212
+ const outcome = await writeCrystalMirror(home, { sourceId: item.sourceId, project: item.project, title: item.title, content: item.content, tags: item.tags });
213
+ if (outcome === 'locked' || outcome === 'unavailable')
214
+ break;
215
+ // Written, or rejected for a reason other than the lock: retrying would not help.
216
+ if (outcome === 'written')
217
+ written++;
218
+ await mutatePendingMirrors(home, openStore, current => current.filter(entry => entry.sourceId !== item.sourceId));
219
+ }
220
+ }
221
+ finally {
222
+ release();
223
+ }
224
+ return written;
225
+ }
118
226
  export async function enqueueGeneration(home, job) {
119
227
  if (isUnusableGenerationText(job.text) || job.text.length > 16_000)
120
228
  return;
@@ -145,6 +253,7 @@ export async function processGenerationQueue(home = vaultHome(), cloud = new Vau
145
253
  await mutate(home, jobs => jobs.map(item => !item.owner && owned.has(item.projectId) ? { ...item, owner } : item));
146
254
  }
147
255
  }
256
+ await flushPendingCrystalMirrors(home, owner, options.crystalMirrorStore ? { openStore: options.crystalMirrorStore } : {}).catch(() => 0);
148
257
  const job = readQueue(home).find(item => item.owner === owner && (item.nextAttemptAt ?? 0) <= Date.now());
149
258
  if (!job)
150
259
  return false;
@@ -178,7 +287,10 @@ export async function processGenerationQueue(home = vaultHome(), cloud = new Vau
178
287
  const awareness = await pushAwareness(home, { ...(job.agentId ? { agentId: job.agentId } : {}), observations: [{ type: awarenessType, title: result.title || job.title || 'Memória', facts: result.facts, concepts: result.concepts }] });
179
288
  if (awareness.error)
180
289
  throw new Error('Awareness delivery failed; retry with cached generation.');
181
- await mirrorCrystalMemory({ home, cloud, job, result });
290
+ const mirror = await mirrorCrystalMemory({ home, cloud, job, result });
291
+ // Never fail the cloud job over the Crystal copy; a locked Crystal just defers it.
292
+ if (mirror.outcome === 'locked' && mirror.record)
293
+ await queuePendingCrystalMirror(home, owner, mirror.record, options.crystalMirrorStore).catch(() => { });
182
294
  await mutate(home, jobs => jobs.filter(item => item.id !== job.id));
183
295
  if (job.kind === 'observation' && owner === ownerKey(home)) {
184
296
  try {
@@ -3,7 +3,7 @@ import { HOOK_EVENTS, runHook } from "./hooks.js";
3
3
  const adapter = process.argv[2] ?? "auto";
4
4
  const event = process.argv[3];
5
5
  if (!event || !HOOK_EVENTS.includes(event)) {
6
- process.stderr.write("Uso: hook-runner <adaptador> context|session-init|observation|file-context|summarize\n");
6
+ process.stderr.write("Uso: hook-runner <adaptador> setup|context|session-init|observation|file-context|summarize|session-end\n");
7
7
  process.exit(1);
8
8
  }
9
9
  await runHook(adapter, event);
package/dist/hooks.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { redact } from './capture-privacy.js';
2
2
  import { type VaultMemoryApi } from "./cloud.js";
3
- export declare const HOOK_EVENTS: readonly ["context", "session-init", "observation", "file-context", "summarize"];
3
+ export declare const HOOK_EVENTS: readonly ["setup", "context", "session-init", "observation", "file-context", "summarize", "session-end"];
4
4
  export type HookEvent = (typeof HOOK_EVENTS)[number];
5
5
  type RecordValue = Record<string, unknown>;
6
6
  /** The existing project for a working directory (worktrees map to their main checkout); never creates one. */
package/dist/hooks.js CHANGED
@@ -12,11 +12,13 @@ import { transcriptCaptureEnabled } from './transcript-config.js';
12
12
  import { loadProjectContext } from './context-chain.js';
13
13
  import { skillContextSection } from './skill-context.js';
14
14
  export const HOOK_EVENTS = [
15
+ "setup",
15
16
  "context",
16
17
  "session-init",
17
18
  "observation",
18
19
  "file-context",
19
20
  "summarize",
21
+ "session-end",
20
22
  ];
21
23
  const record = (value) => value && typeof value === "object" && !Array.isArray(value)
22
24
  ? value
@@ -191,6 +193,11 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
191
193
  /vault[_-]go|vault[_-]mem/.test(toolName)) {
192
194
  return hookOutput(event, "", adapter);
193
195
  }
196
+ if (event === "setup") {
197
+ const output = hookOutput(event, "", adapter);
198
+ output.systemMessage = "Vault Memory: hooks ready.";
199
+ return output;
200
+ }
194
201
  const prefs = await preferences(cloud);
195
202
  const reading = event === "context" || event === "file-context";
196
203
  if (!reading && prefs.automaticCapture !== true)
@@ -198,7 +205,7 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
198
205
  const tool = record(input.tool_input);
199
206
  const file = string(tool.file_path, tool.path, tool.notebook_path, tool.AbsolutePath, tool.Path, tool.filePath);
200
207
  const modifies = /write|edit|patch|create|delete|move|rename/i.test(toolName);
201
- if (!reading && prefs.captureMode === "focused" && event !== "summarize")
208
+ if (!reading && prefs.captureMode === "focused" && event !== "summarize" && event !== "session-end")
202
209
  return hookOutput(event, "", adapter);
203
210
  if (event === "observation" && prefs.captureMode === "balanced" && !modifies)
204
211
  return hookOutput(event, "", adapter);
@@ -280,8 +287,8 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
280
287
  }));
281
288
  const rawContent = event === "session-init"
282
289
  ? prompt
283
- : event === "summarize"
284
- ? response
290
+ : event === "summarize" || event === "session-end"
291
+ ? response || (event === "session-end" ? "session ended" : "")
285
292
  : JSON.stringify(toolPayload);
286
293
  const content = String(redact(rawContent));
287
294
  if (content.trim()) {
@@ -289,7 +296,9 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
289
296
  ? "user_prompt"
290
297
  : event === "summarize"
291
298
  ? "session_summary"
292
- : "tool_use";
299
+ : event === "session-end"
300
+ ? "session_end"
301
+ : "tool_use";
293
302
  const sourceEventId = createHash("sha256")
294
303
  .update(JSON.stringify([
295
304
  adapter,
@@ -316,7 +325,7 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
316
325
  summaryMode: 'off',
317
326
  metadata: { summaryMode: 'off' },
318
327
  title: toolName ||
319
- (event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
328
+ (event === "session-end" ? "Fim da sessão" : event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
320
329
  content,
321
330
  filesRead: !modifies ? files : [],
322
331
  filesModified: modifies ? files : [],
@@ -331,14 +340,14 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
331
340
  ...(string(input.agent_id, input.agentId) ? { agentId: string(input.agent_id, input.agentId) } : {}),
332
341
  text: content.slice(0, 16_000),
333
342
  title: toolName ||
334
- (event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
335
- kind: event === "summarize" ? "summary" : event === "session-init" ? "prompt" : "observation",
343
+ (event === "session-end" ? "Fim da sessão" : event === "summarize" ? "Resumo da sessão" : "Solicitação do usuário"),
344
+ kind: event === "summarize" || event === "session-end" ? "summary" : event === "session-init" ? "prompt" : "observation",
336
345
  maxChars: boundedPreference(prefs.summaryMaxChars, 4_000, 256, 16_000),
337
346
  filesRead: !modifies ? files : [],
338
347
  filesModified: modifies ? files : [],
339
348
  });
340
349
  }
341
- if (event === "summarize")
350
+ if (event === "summarize" || event === "session-end")
342
351
  await cloud.endSession(session.id);
343
352
  return hookOutput(event, "", adapter);
344
353
  }
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { cliArguments, helpText, resolveCliMode, runEngine, runLocal, runLogin, runSetup, runUpdate, runTranscripts, runContextChain } from './cli.js';
3
+ import { cliArguments, helpText, resolveCliMode, runEngine, runLocal, runLogin, runSetup, runUpdate, runTranscripts, runContextChain, runSkills } from './cli.js';
4
4
  import { HOOK_EVENTS, runHook } from './hooks.js';
5
5
  import { createVaultGoServer, VERSION } from './server.js';
6
6
  const { argument, options } = cliArguments(process.argv.slice(2));
@@ -45,6 +45,10 @@ async function main() {
45
45
  process.exitCode = await runContextChain(options);
46
46
  return;
47
47
  }
48
+ if (mode === 'skills') {
49
+ process.exitCode = await runSkills(options);
50
+ return;
51
+ }
48
52
  if (mode === 'hook') {
49
53
  const adapter = options.find((item) => !item.startsWith('--')) ?? 'auto';
50
54
  const event = options.filter((item) => !item.startsWith('--'))[1];
package/dist/installer.js CHANGED
@@ -4,7 +4,7 @@ import { dirname, join, resolve } from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { vaultHome } from './config.js';
6
6
  import { stageLocalRuntime } from './local-install.js';
7
- import { installClientHooks, installOpenCodePlugin } from './hook-install.js';
7
+ import { installCaptureHooks, installClientHooks, installOpenCodePlugin } from './hook-install.js';
8
8
  import { installManagedSkills } from './skill-install.js';
9
9
  export const MCP_SERVER_NAME = 'vault-go';
10
10
  export const MCP_SERVER_COMMAND = ['bunx', '--bun', 'vault-go@latest', 'serve'];
@@ -261,6 +261,13 @@ export function installMcpClient(client, options = {}) {
261
261
  else {
262
262
  mergeServer(path);
263
263
  }
264
+ if (client === 'cursor' || client === 'windsurf') {
265
+ try {
266
+ stageLocalRuntime(vaultHome());
267
+ }
268
+ catch { /* hooks still point at the staged runner when it exists */ }
269
+ installCaptureHooks(client, home);
270
+ }
264
271
  return { client, status: 'installed', destination: path };
265
272
  }
266
273
  export function detectMcpClients(home = homedir()) {
@@ -0,0 +1,2 @@
1
+ /** Caminho herdado do manifesto dentro do tar de um bundle de skills. */
2
+ export declare const LEGACY_SKILL_BUNDLE_MANIFEST_PATH = "dev.orca.skill-sharing/manifest.json";
@@ -0,0 +1,5 @@
1
+ // Por quê: o Vault Desktop nasceu de um fork de outro produto e ainda grava o
2
+ // manifesto de bundle de skills no caminho herdado dele (os builds 0.2.x só
3
+ // conhecem esse). Este é o único lugar onde o literal herdado pode morar.
4
+ /** Caminho herdado do manifesto dentro do tar de um bundle de skills. */
5
+ export const LEGACY_SKILL_BUNDLE_MANIFEST_PATH = 'dev.orca.skill-sharing/manifest.json';
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
6
6
  import { authPath, loadConfig, loadTokens, vaultHome } from "./config.js";
7
7
  import { VaultCloudClient } from "./cloud.js";
8
+ import { statuslineText } from "./agent-cost.js";
8
9
  import { readActivity } from "./activity.js";
9
10
  import { dashboardHtml, dashboardScript } from "./local-dashboard.js";
10
11
  import pkg from "../package.json" with { type: "json" };
@@ -336,6 +337,16 @@ export async function startLocalService(options = {}) {
336
337
  });
337
338
  return;
338
339
  }
340
+ if (url.pathname === "/statusline" && req.method === "GET") {
341
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
342
+ res.end(statuslineText(home, pkg.version));
343
+ return;
344
+ }
345
+ if (url.pathname === "/viewer" && req.method === "GET") {
346
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
347
+ res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Vault Memory</title></head><body><h1>Vault Memory</h1><p>Search uses the Vault account index. This page does not keep a second local vector database.</p><p><a href="/">Open the dashboard</a></p><pre id="line"></pre><script>fetch("/statusline").then(function(response){return response.text()}).then(function(text){document.getElementById("line").textContent=text})</script></body></html>`);
348
+ return;
349
+ }
339
350
  if (url.pathname === "/" && req.method === "GET") {
340
351
  res.setHeader("Content-Type", "text/html; charset=utf-8");
341
352
  res.end(dashboardHtml());
@@ -0,0 +1,25 @@
1
+ export interface ArchiveRecord {
2
+ sourceId: string;
3
+ kind: string;
4
+ content: string;
5
+ title?: string;
6
+ sessionId?: string;
7
+ memoryType?: string;
8
+ facts?: string[];
9
+ concepts?: string[];
10
+ filesRead?: string[];
11
+ filesModified?: string[];
12
+ occurredAt?: string;
13
+ }
14
+ export interface FourGroupArchive {
15
+ version: 1;
16
+ format: 'vault-memory-groups';
17
+ observations: ArchiveRecord[];
18
+ sessions: ArchiveRecord[];
19
+ summaries: ArchiveRecord[];
20
+ prompts: ArchiveRecord[];
21
+ }
22
+ export declare function normalizeArchiveRecord(value: unknown, fallbackId: string): ArchiveRecord | null;
23
+ export declare function recordsFromExport(value: unknown): ArchiveRecord[];
24
+ export declare function groupArchive(records: ArchiveRecord[]): FourGroupArchive;
25
+ export declare function flattenArchive(archive: FourGroupArchive): ArchiveRecord[];
@@ -0,0 +1,65 @@
1
+ const GROUPS = ['observations', 'sessions', 'summaries', 'prompts'];
2
+ function strings(value, max) {
3
+ if (!Array.isArray(value))
4
+ return undefined;
5
+ const items = value.filter((item) => typeof item === 'string' && item.trim().length > 0).map(item => item.trim().slice(0, 2_000)).slice(0, max);
6
+ return items.length ? items : undefined;
7
+ }
8
+ export function normalizeArchiveRecord(value, fallbackId) {
9
+ if (!value || typeof value !== 'object')
10
+ return null;
11
+ const row = value;
12
+ const content = typeof row.content === 'string' ? row.content.trim() : typeof row.text === 'string' ? row.text.trim() : '';
13
+ if (!content)
14
+ return null;
15
+ const sourceId = typeof row.sourceId === 'string' && row.sourceId.trim() ? row.sourceId.trim().slice(0, 500) : fallbackId;
16
+ const kind = typeof row.kind === 'string' && row.kind.trim() ? row.kind.trim().slice(0, 100) : 'observation';
17
+ const record = { sourceId, kind, content: content.slice(0, 200_000) };
18
+ if (typeof row.title === 'string' && row.title.trim())
19
+ record.title = row.title.trim().slice(0, 500);
20
+ if (typeof row.sessionId === 'string' && row.sessionId.trim())
21
+ record.sessionId = row.sessionId.trim().slice(0, 80);
22
+ if (typeof row.memoryType === 'string' && row.memoryType.trim())
23
+ record.memoryType = row.memoryType.trim().slice(0, 100);
24
+ if (typeof row.occurredAt === 'string' && row.occurredAt.trim())
25
+ record.occurredAt = row.occurredAt.trim().slice(0, 40);
26
+ const facts = strings(row.facts, 200);
27
+ const concepts = strings(row.concepts, 200);
28
+ const filesRead = strings(row.filesRead, 500);
29
+ const filesModified = strings(row.filesModified, 500);
30
+ if (facts)
31
+ record.facts = facts;
32
+ if (concepts)
33
+ record.concepts = concepts;
34
+ if (filesRead)
35
+ record.filesRead = filesRead;
36
+ if (filesModified)
37
+ record.filesModified = filesModified;
38
+ return record;
39
+ }
40
+ function groupOf(kind) {
41
+ const value = kind.toLowerCase();
42
+ if (value === 'prompt' || value === 'user_prompt')
43
+ return 'prompts';
44
+ if (value === 'summary' || value === 'session_summary' || value === 'session_end')
45
+ return 'summaries';
46
+ if (value === 'session')
47
+ return 'sessions';
48
+ return 'observations';
49
+ }
50
+ export function recordsFromExport(value) {
51
+ const root = value && typeof value === 'object' ? value : {};
52
+ const lists = [root.records, root.memories, root.items].find(Array.isArray);
53
+ const grouped = GROUPS.flatMap(name => Array.isArray(root[name]) ? root[name] : []);
54
+ const source = lists ?? grouped;
55
+ return source.map((item, index) => normalizeArchiveRecord(item, `export-${index + 1}`)).filter((item) => Boolean(item)).slice(0, 4_000);
56
+ }
57
+ export function groupArchive(records) {
58
+ const archive = { version: 1, format: 'vault-memory-groups', observations: [], sessions: [], summaries: [], prompts: [] };
59
+ for (const record of records.slice(0, 4_000))
60
+ archive[groupOf(record.kind)].push(record);
61
+ return archive;
62
+ }
63
+ export function flattenArchive(archive) {
64
+ return GROUPS.flatMap(name => archive[name]).slice(0, 1_000);
65
+ }
package/dist/modes.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare const MODE_NAMES: readonly ["babysit", "mem-search", "how-it-works", "make-plan", "do", "smart-explore", "knowledge-agent", "standup", "timeline-report", "weekly-digests", "learn-codebase", "pathfinder", "design-is", "what-the", "version-bump", "oh-my-issues", "cloud-sync", "mode-creator", "ccs-align", "wowerpoint"];
2
+ export declare const MODE_NAMES: readonly ["babysit", "mem-search", "how-it-works", "make-plan", "do", "smart-explore", "knowledge-agent", "standup", "timeline-report", "weekly-digests", "learn-codebase", "pathfinder", "design-is", "what-the", "version-bump", "oh-my-issues", "cloud-sync", "mode-creator", "ccs-align", "wowerpoint", "agent-cost-report"];
3
3
  export type VaultModeName = (typeof MODE_NAMES)[number];
4
4
  export type VaultModeStatus = 'native' | 'instructional' | 'limited';
5
5
  export interface VaultMode {
package/dist/modes.js CHANGED
@@ -21,6 +21,7 @@ export const MODE_NAMES = [
21
21
  'mode-creator',
22
22
  'ccs-align',
23
23
  'wowerpoint',
24
+ 'agent-cost-report',
24
25
  ];
25
26
  const mode = (name, title, summary, status, readOnlyDefault, runtimeRequires, limitations, steps) => ({
26
27
  name,
@@ -50,10 +51,11 @@ export const VAULT_MODES = [
50
51
  mode('what-the', 'Explain a technical subject plainly', 'Give a grounded who, what, where, why and when explanation without assuming specialist context.', 'native', true, ['The subject or artifact to explain', 'Vault Go retrieval or smart-code tools when evidence is needed'], ['The answer is limited by the supplied material and accessible evidence.', 'This mode does not mutate code or account state.'], ['Gather only the evidence needed to understand the named subject.', 'Explain who owns it, what it does, where it lives, why it exists and when it runs.', 'Use a concrete example and distinguish verified behavior from inference.']),
51
52
  mode('version-bump', 'Plan or perform a version bump', 'Audit release scope, update consistent version surfaces and verify a release when authorized.', 'limited', true, ['Local git and package tooling', 'gh or a connected GitHub provider for GitHub releases'], ['This mode does not execute registry publication itself.', 'Tags, releases, pushes and registry publication require explicit authorization in the current task.'], ['Inspect changes since the previous release and determine the semantic version impact.', 'Find version manifests and changelog conventions, then propose or apply consistent updates as requested.', 'Run build and tests; create tags, releases or remote changes only when the request explicitly includes them.']),
52
53
  mode('oh-my-issues', 'Cluster an issue backlog', 'Group related issues by root cause and design a small set of architectural fixes.', 'limited', true, ['gh or a connected GitHub provider', 'Read access to the requested issue tracker'], ['Vault Go has no built-in issue tracker client.', 'Comments, issue edits, closures and pull requests require explicit authorization in the current task.'], ['Collect a bounded issue set and normalize symptoms, affected areas and reproduction evidence.', 'Cluster issues by verified shared cause, keeping ambiguous reports separate.', 'Draft plan-master issues and routing actions; write them to GitHub only when requested.']),
53
- mode('cloud-sync', 'Inspect cloud and sync options', 'Explain Vault account connectivity and inspect available status without pretending to implement another service.', 'limited', true, ['vault_go_status', 'vault_go_cloud_health', 'vault_go_preferences'], ['The cmem.ai cloud-sync protocol is not implemented by Vault Go; Vault cloud is a separate service.', 'This mode does not sign in, enable sync or change preferences unless the request explicitly asks for that action.'], ['Read account and preference status to establish what this Vault runtime can access.', 'Explain the boundary between local capture, Vault service storage and unsupported cmem.ai sync.', 'If the user explicitly requests a supported configuration change, use only the relevant available Vault tool and report its result.']),
54
+ mode('cloud-sync', 'Inspect cloud and sync options', 'Explain Vault account connectivity and inspect available status without pretending to implement another service.', 'limited', true, ['vault_go_status', 'vault_go_cloud_health', 'vault_go_preferences'], ['Vault cloud is the account store. External sync protocols are not part of this runtime.', 'This mode does not sign in, enable sync or change preferences unless the request explicitly asks for that action.'], ['Read account and preference status to establish what this Vault runtime can access.', 'Explain the boundary between local capture and Vault service storage.', 'If the user explicitly requests a supported configuration change, use only the relevant available Vault tool and report its result.']),
54
55
  mode('mode-creator', 'Design a custom mode', 'Create and activate a bounded observation taxonomy with inherited prompts and optional Telegram triggers.', 'native', true, ['vault_go_custom_mode_save', 'vault_go_custom_mode_select', 'vault_go_telegram_configure'], ['A custom mode applies to future local context generation; it does not rewrite existing memories.', 'Telegram credentials must be supplied through the private service environment, never prompt or tool arguments.'], ['Learn what should be recorded and skipped, then propose distinct observation types and reusable concept tags.', 'After the user approves the taxonomy, save it with vault_go_custom_mode_save and activate it with vault_go_custom_mode_select.', 'Configure Telegram triggers only when explicitly requested, then use vault_go_telegram_test for the one authorized test message.']),
55
56
  mode('ccs-align', 'Run an alignment review', 'Run a Vault-native alignment cycle with an isolated cache, reversible exclusions and cascade-rule reporting.', 'native', true, ['vault_go_ccs_align_health', 'vault_go_ccs_align_cycle', 'vault_go_ccs_align_rules_check'], ['Vault uses cloud UUIDs and its own private cache rather than the upstream numeric worker diary.', 'Grok Bot awareness push is a separate agent-log writer and is not implemented by CCS Align.'], ['Check viewer health, then run vault_go_ccs_align_cycle to retrieve index, timeline and selected observations in order.', 'Use exclude and restore tools to filter only the compiled viewer cache; never delete authoritative cloud memory.', 'Submit bounded house, project and seat rule lines to vault_go_ccs_align_rules_check and review the append-only report.']),
56
57
  mode('wowerpoint', 'Outline a narrative slide deck', 'Transform one source document into a structured, audience-aware slide narrative.', 'limited', true, ['One source document', 'A client presentation or document renderer for a final artifact'], ['PPTX or PDF rendering is not implemented by Vault Go.', 'Vault Go can return a slide outline and art direction but cannot claim a generated deck file.'], ['Identify the audience, core claim and evidence in the source document.', 'Create a concise slide sequence with titles, key points, visual direction and speaker intent.', 'Render a presentation only if the client exposes a suitable tool and the user asks for the artifact.']),
58
+ mode('agent-cost-report', 'Report agent cost', 'Estimate transcript token cost for a local directory and keep missing prices unavailable.', 'native', true, ['vault_go_agent_cost_report'], ['Dollars are a public list estimate, not a provider invoice.', 'Grok and other clients without usage fields stay unavailable instead of being shown as zero.'], ['Choose the transcript directory the user asked to measure.', 'Call vault_go_agent_cost_report and keep the ESTIMATED label on every dollar figure.', 'Report token totals, the files scanned, and any unavailable sources without inventing a zero.']),
57
59
  ];
58
60
  const TASK_MAX_CHARS = 4_000;
59
61
  const taskSchema = z.string().trim().min(1).max(TASK_MAX_CHARS).optional();
@@ -3,7 +3,7 @@ type Options = {
3
3
  fetch?: typeof fetch;
4
4
  timeoutMs?: number;
5
5
  };
6
- /** OpenCode 1.x plugin contract: context only; capture stays with the user's existing integrations. */
6
+ /** OpenCode 1.x plugin: inject context and capture prompts, tool use and session end. */
7
7
  export declare function createOpenCodePlugin(input: {
8
8
  directory: string;
9
9
  }, options?: Options): {
@@ -12,5 +12,33 @@ export declare function createOpenCodePlugin(input: {
12
12
  }, output: {
13
13
  system: string[];
14
14
  }) => Promise<void>;
15
+ 'chat.message': (_input: {
16
+ sessionID?: string;
17
+ }, output: {
18
+ parts?: Array<{
19
+ type?: string;
20
+ text?: string;
21
+ }>;
22
+ }) => Promise<void>;
23
+ 'tool.execute.before': (toolInput: {
24
+ tool?: string;
25
+ sessionID?: string;
26
+ }, output: {
27
+ args?: Record<string, unknown>;
28
+ }) => Promise<void>;
29
+ 'tool.execute.after': (toolInput: {
30
+ tool?: string;
31
+ sessionID?: string;
32
+ callID?: string;
33
+ }, output: {
34
+ args?: Record<string, unknown>;
35
+ output?: unknown;
36
+ }) => Promise<void>;
37
+ event: ({ event }: {
38
+ event?: {
39
+ type?: string;
40
+ properties?: Record<string, unknown>;
41
+ };
42
+ }) => Promise<void>;
15
43
  };
16
44
  export {};
@@ -8,7 +8,7 @@ function owner(home) {
8
8
  process.env.VAULT_GO_TOKEN || loadTokens(home)?.accessToken || '',
9
9
  ])).digest('hex');
10
10
  }
11
- /** OpenCode 1.x plugin contract: context only; capture stays with the user's existing integrations. */
11
+ /** OpenCode 1.x plugin: inject context and capture prompts, tool use and session end. */
12
12
  export function createOpenCodePlugin(input, options = {}) {
13
13
  const home = options.home ?? vaultHome();
14
14
  const pending = new Map();
@@ -54,6 +54,13 @@ export function createOpenCodePlugin(input, options = {}) {
54
54
  controller.abort();
55
55
  }
56
56
  }
57
+ async function capture(event, value) {
58
+ try {
59
+ const cloud = new VaultCloudClient(home, options.fetch ?? globalThis.fetch, false);
60
+ await executeHook('opencode', event, { cwd: input.directory, ...value }, cloud, home);
61
+ }
62
+ catch { /* capture must not stop the client */ }
63
+ }
57
64
  return {
58
65
  'experimental.chat.system.transform': async (request, output) => {
59
66
  if (!request.sessionID || !input.directory)
@@ -69,5 +76,33 @@ export function createOpenCodePlugin(input, options = {}) {
69
76
  output.system.push(result.context);
70
77
  }
71
78
  },
79
+ 'chat.message': async (_input, output) => {
80
+ const prompt = (output.parts ?? []).filter(part => part.type === 'text' || !part.type).map(part => part.text ?? '').join('\n').trim();
81
+ if (prompt)
82
+ await capture('session-init', { session_id: _input.sessionID, prompt });
83
+ },
84
+ 'tool.execute.before': async (toolInput, output) => {
85
+ if (toolInput.tool === 'read') {
86
+ await capture('file-context', { session_id: toolInput.sessionID, tool_name: 'read', tool_input: output.args ?? {} });
87
+ }
88
+ },
89
+ 'tool.execute.after': async (toolInput, output) => {
90
+ if (!toolInput.tool || /vault[_-]go|vault[_-]mem/i.test(toolInput.tool))
91
+ return;
92
+ await capture('observation', {
93
+ session_id: toolInput.sessionID,
94
+ tool_name: toolInput.tool,
95
+ tool_use_id: toolInput.callID,
96
+ tool_input: output.args ?? {},
97
+ tool_response: output.output ?? null,
98
+ });
99
+ },
100
+ event: async ({ event }) => {
101
+ if (event?.type !== 'session.idle')
102
+ return;
103
+ const info = event.properties?.info;
104
+ const sessionId = info && typeof info === 'object' ? String(info.id ?? '') : '';
105
+ await capture('session-end', { session_id: sessionId, last_assistant_message: '' });
106
+ },
72
107
  };
73
108
  }