@andespindola/brainlink 1.0.7 → 1.2.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/CHANGELOG.md +16 -1
- package/README.md +63 -2
- package/dist/application/analyze-vault.js +1 -1
- package/dist/application/build-context.js +14 -7
- package/dist/application/get-graph-node.js +2 -2
- package/dist/application/get-graph-summary.js +2 -2
- package/dist/application/get-graph.js +2 -2
- package/dist/application/index-vault-phases.js +1 -0
- package/dist/application/index-vault.js +16 -2
- package/dist/application/list-agents.js +2 -2
- package/dist/application/list-links.js +3 -3
- package/dist/application/ports/knowledge-store.js +1 -0
- package/dist/application/provision-vault.js +74 -0
- package/dist/application/ranking/run-search.js +212 -0
- package/dist/application/search-graph-node-ids.js +3 -2
- package/dist/application/search-knowledge.js +9 -17
- package/dist/application/server/routes.js +6 -1
- package/dist/application/vault-encryption.js +64 -0
- package/dist/application/vault-git-core.js +168 -0
- package/dist/application/vault-git.js +75 -0
- package/dist/application/vault-snapshot.js +87 -0
- package/dist/cli/commands/read-commands.js +9 -5
- package/dist/cli/commands/vault-sync-commands.js +137 -0
- package/dist/cli/commands/write/vault-lifecycle-commands.js +1 -1
- package/dist/cli/main.js +2 -0
- package/dist/infrastructure/config.js +16 -1
- package/dist/infrastructure/context-packs.js +57 -18
- package/dist/infrastructure/file-index.js +22 -409
- package/dist/infrastructure/index-signature.js +27 -0
- package/dist/infrastructure/index-state.js +6 -0
- package/dist/infrastructure/knowledge-store/binary-store.js +333 -0
- package/dist/infrastructure/knowledge-store/index.js +37 -0
- package/dist/infrastructure/knowledge-store/stored-index.js +216 -0
- package/dist/infrastructure/vault-crypto.js +78 -0
- package/dist/mcp/server.js +46 -1
- package/dist/mcp/tools/maintenance-tools.js +2 -2
- package/dist/mcp/tools/read-tools.js +8 -4
- package/dist/mcp/tools/vault-tools.js +163 -0
- package/dist/mcp/tools.js +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { cloneVault, commitVault, initVaultGit, pullVault, pushVault, restoreVault, vaultGitStatus, vaultHistory } from '../../application/vault-git.js';
|
|
2
|
+
import { provisionVaultRepo } from '../../application/provision-vault.js';
|
|
3
|
+
import { exportVaultSnapshot, importVaultSnapshot } from '../../application/vault-snapshot.js';
|
|
4
|
+
import { print, resolveOptions } from '../runtime.js';
|
|
5
|
+
export const registerVaultSyncCommands = (program) => {
|
|
6
|
+
program
|
|
7
|
+
.command('vault-init-git')
|
|
8
|
+
.option('--vault <vault>', 'vault directory')
|
|
9
|
+
.option('--remote <url>', 'git remote URL (SSH or credential-helper backed; inline tokens are rejected)')
|
|
10
|
+
.option('--json', 'print machine-readable JSON')
|
|
11
|
+
.description('initialize git versioning for the vault (ignores the derived .brainlink/)')
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
const { vault } = await resolveOptions(options);
|
|
14
|
+
const result = await initVaultGit(vault, { remote: options.remote });
|
|
15
|
+
print(options.json, result, () => `${result.initialized ? 'Initialized' : 'Reused'} git repo at ${result.root}${options.remote ? ` (origin: ${options.remote})` : ''}.`);
|
|
16
|
+
});
|
|
17
|
+
program
|
|
18
|
+
.command('vault-provision')
|
|
19
|
+
.option('--vault <vault>', 'vault directory')
|
|
20
|
+
.option('--name <name>', 'repository name to create', 'brainlink-vault')
|
|
21
|
+
.option('--public', 'create a public repository instead of private')
|
|
22
|
+
.option('--no-auto-version', 'do not enable autoVersion after provisioning')
|
|
23
|
+
.option('--no-encrypt', 'commit plaintext Markdown instead of encrypted ciphertext')
|
|
24
|
+
.option('-m, --message <message>', 'initial commit message')
|
|
25
|
+
.option('--json', 'print machine-readable JSON')
|
|
26
|
+
.description('create a private GitHub repo for the vault via gh, push it, and enable encrypted auto-versioning')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
const { vault } = await resolveOptions(options);
|
|
29
|
+
const result = await provisionVaultRepo(vault, {
|
|
30
|
+
name: options.name,
|
|
31
|
+
private: options.public !== true,
|
|
32
|
+
enableAutoVersion: options.autoVersion !== false,
|
|
33
|
+
encrypt: options.encrypt !== false,
|
|
34
|
+
message: options.message
|
|
35
|
+
});
|
|
36
|
+
print(options.json, result, () => `${result.created ? 'Created' : 'Reused'} ${result.repo} and pushed the vault${result.encrypted ? ' (encrypted)' : ''}${result.autoVersionEnabled ? '; autoVersion enabled' : ''}.`);
|
|
37
|
+
});
|
|
38
|
+
program
|
|
39
|
+
.command('vault-commit')
|
|
40
|
+
.requiredOption('-m, --message <message>', 'commit message')
|
|
41
|
+
.option('--vault <vault>', 'vault directory')
|
|
42
|
+
.option('--json', 'print machine-readable JSON')
|
|
43
|
+
.description('commit the vault Markdown')
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
const { vault } = await resolveOptions(options);
|
|
46
|
+
const hash = await commitVault(vault, options.message ?? 'Update vault');
|
|
47
|
+
print(options.json, { committed: hash != null, hash }, () => hash ? `Committed ${hash.slice(0, 10)}.` : 'Nothing to commit.');
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command('vault-status')
|
|
51
|
+
.option('--vault <vault>', 'vault directory')
|
|
52
|
+
.option('--json', 'print machine-readable JSON')
|
|
53
|
+
.description('show git status of the vault Markdown')
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
const { vault } = await resolveOptions(options);
|
|
56
|
+
const status = await vaultGitStatus(vault);
|
|
57
|
+
print(options.json, status, () => `Branch ${status.branch}; ${status.dirty ? `${status.changedMarkdown.length} changed markdown file(s)` : 'clean'}.`);
|
|
58
|
+
});
|
|
59
|
+
program
|
|
60
|
+
.command('vault-history')
|
|
61
|
+
.option('--vault <vault>', 'vault directory')
|
|
62
|
+
.option('--limit <n>', 'number of entries', '20')
|
|
63
|
+
.option('--json', 'print machine-readable JSON')
|
|
64
|
+
.description('show vault commit history')
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
const { vault } = await resolveOptions(options);
|
|
67
|
+
const entries = await vaultHistory(vault, Number.parseInt(options.limit ?? '20', 10));
|
|
68
|
+
print(options.json, entries, () => entries.map((entry) => `${entry.hash.slice(0, 10)} ${entry.date} ${entry.subject}`).join('\n') || 'No history.');
|
|
69
|
+
});
|
|
70
|
+
program
|
|
71
|
+
.command('vault-push')
|
|
72
|
+
.option('--vault <vault>', 'vault directory')
|
|
73
|
+
.option('--json', 'print machine-readable JSON')
|
|
74
|
+
.description('push the vault to its git remote')
|
|
75
|
+
.action(async (options) => {
|
|
76
|
+
const { vault } = await resolveOptions(options);
|
|
77
|
+
await pushVault(vault);
|
|
78
|
+
print(options.json, { pushed: true }, () => 'Pushed to origin.');
|
|
79
|
+
});
|
|
80
|
+
program
|
|
81
|
+
.command('vault-pull')
|
|
82
|
+
.option('--vault <vault>', 'vault directory')
|
|
83
|
+
.option('--no-index', 'skip reindexing after pull')
|
|
84
|
+
.option('--json', 'print machine-readable JSON')
|
|
85
|
+
.description('pull the vault from git and reindex changed notes')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
const { vault } = await resolveOptions(options);
|
|
88
|
+
const result = await pullVault(vault, { skipIndex: options.index === false });
|
|
89
|
+
print(options.json, result, () => `Pulled. ${result.changedMarkdown.length} markdown file(s) changed${result.index ? `; reindexed ${result.index.documentCount} docs` : ''}.`);
|
|
90
|
+
});
|
|
91
|
+
program
|
|
92
|
+
.command('vault-restore')
|
|
93
|
+
.argument('<ref>', 'git ref (commit, tag, branch) to restore Markdown from')
|
|
94
|
+
.option('--vault <vault>', 'vault directory')
|
|
95
|
+
.option('--no-index', 'skip reindexing after restore')
|
|
96
|
+
.option('--json', 'print machine-readable JSON')
|
|
97
|
+
.description('restore vault Markdown from a git ref and reindex')
|
|
98
|
+
.action(async (ref, options) => {
|
|
99
|
+
const { vault } = await resolveOptions(options);
|
|
100
|
+
const result = await restoreVault(vault, ref, { skipIndex: options.index === false });
|
|
101
|
+
print(options.json, result, () => `Restored from ${ref}. ${result.changedMarkdown.length} markdown file(s) changed${result.index ? `; reindexed ${result.index.documentCount} docs` : ''}.`);
|
|
102
|
+
});
|
|
103
|
+
program
|
|
104
|
+
.command('vault-clone')
|
|
105
|
+
.argument('<remote>', 'git remote URL')
|
|
106
|
+
.argument('<dir>', 'target directory')
|
|
107
|
+
.option('--no-index', 'skip building the index after clone')
|
|
108
|
+
.option('--json', 'print machine-readable JSON')
|
|
109
|
+
.description('clone a remote vault and build its index')
|
|
110
|
+
.action(async (remote, dir, options) => {
|
|
111
|
+
const result = await cloneVault(remote, dir, { skipIndex: options.index === false });
|
|
112
|
+
print(options.json, result, () => `Cloned to ${result.root}${result.index ? `; indexed ${result.index.documentCount} docs` : ''}.`);
|
|
113
|
+
});
|
|
114
|
+
program
|
|
115
|
+
.command('vault-export')
|
|
116
|
+
.requiredOption('-o, --output <file>', 'output .blvault file')
|
|
117
|
+
.option('--vault <vault>', 'vault directory')
|
|
118
|
+
.option('--json', 'print machine-readable JSON')
|
|
119
|
+
.description('export the vault Markdown to a portable .blvault snapshot')
|
|
120
|
+
.action(async (options) => {
|
|
121
|
+
const { vault } = await resolveOptions(options);
|
|
122
|
+
const result = await exportVaultSnapshot(vault, options.output ?? 'vault.blvault');
|
|
123
|
+
print(options.json, result, () => `Exported ${result.fileCount} files to ${result.file} (${result.bytes} bytes).`);
|
|
124
|
+
});
|
|
125
|
+
program
|
|
126
|
+
.command('vault-import')
|
|
127
|
+
.argument('<file>', 'input .blvault file')
|
|
128
|
+
.option('--vault <vault>', 'vault directory')
|
|
129
|
+
.option('--no-index', 'skip reindexing after import')
|
|
130
|
+
.option('--json', 'print machine-readable JSON')
|
|
131
|
+
.description('import a .blvault snapshot into the vault and reindex')
|
|
132
|
+
.action(async (file, options) => {
|
|
133
|
+
const { vault } = await resolveOptions(options);
|
|
134
|
+
const result = await importVaultSnapshot(file, vault, { skipIndex: options.index === false });
|
|
135
|
+
print(options.json, result, () => `Imported ${result.imported} files (${result.conflicted} conflicts)${result.index ? `; reindexed ${result.index.documentCount} docs` : ''}.`);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
@@ -204,7 +204,7 @@ export const registerVaultLifecycleCommands = (program) => {
|
|
|
204
204
|
const policy = await getBootstrapPolicy();
|
|
205
205
|
const bootstrapStatus = await getBootstrapSessionStatus(resolved.vault, resolved.agent);
|
|
206
206
|
const context = options.query
|
|
207
|
-
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs)
|
|
207
|
+
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs, resolved.defaults.cagPackTtlMs)
|
|
208
208
|
: null;
|
|
209
209
|
const agentIntegration = options.installAgent === false
|
|
210
210
|
? null
|
package/dist/cli/main.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerConfigCommands } from './commands/config-commands.js';
|
|
|
10
10
|
import { registerPracticalCommands } from './commands/practical-commands.js';
|
|
11
11
|
import { registerReadCommands } from './commands/read-commands.js';
|
|
12
12
|
import { registerVaultCommands } from './commands/vault-commands.js';
|
|
13
|
+
import { registerVaultSyncCommands } from './commands/vault-sync-commands.js';
|
|
13
14
|
import { registerWriteCommands } from './commands/write-commands.js';
|
|
14
15
|
const readPackageMetadata = () => {
|
|
15
16
|
const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
@@ -63,6 +64,7 @@ registerReadCommands(program);
|
|
|
63
64
|
registerPracticalCommands(program);
|
|
64
65
|
registerConfigCommands(program);
|
|
65
66
|
registerVaultCommands(program);
|
|
67
|
+
registerVaultSyncCommands(program);
|
|
66
68
|
registerAgentCommands(program);
|
|
67
69
|
await maybePrintUpdateNotice(packageMetadata);
|
|
68
70
|
program.parseAsync().catch((error) => {
|
|
@@ -11,16 +11,21 @@ export const defaultBrainlinkConfig = {
|
|
|
11
11
|
defaultAgent: undefined,
|
|
12
12
|
autoIndexOnWrite: true,
|
|
13
13
|
autoCanonicalContextLinks: true,
|
|
14
|
+
autoVersion: false,
|
|
15
|
+
vaultEncryption: false,
|
|
14
16
|
autoUpdateCheck: true,
|
|
15
17
|
updateCheckIntervalMs: 86_400_000,
|
|
16
18
|
defaultSearchLimit: 8,
|
|
17
19
|
defaultContextTokens: 1500,
|
|
18
20
|
defaultContextStrategy: 'auto',
|
|
19
21
|
defaultContextCacheTtlMs: 120_000,
|
|
22
|
+
cagPackTtlMs: 0,
|
|
20
23
|
embeddingProvider: 'local',
|
|
21
24
|
embedding: {},
|
|
22
25
|
defaultSearchMode: 'hybrid',
|
|
23
26
|
chunkSize: 1200,
|
|
27
|
+
storageBackend: 'binary',
|
|
28
|
+
hybridFusion: 'rrf',
|
|
24
29
|
searchPack: {
|
|
25
30
|
rowChunkSize: 5_000,
|
|
26
31
|
compressionLevel: 5,
|
|
@@ -47,6 +52,10 @@ const isRecord = (value) => typeof value === 'object' && value !== null && !Arra
|
|
|
47
52
|
const embeddingProviders = new Set(['none', 'local', 'ollama', 'openai']);
|
|
48
53
|
const searchModes = new Set(['fts', 'semantic', 'hybrid']);
|
|
49
54
|
const contextStrategies = new Set(['rag', 'cag', 'auto']);
|
|
55
|
+
const storageBackends = new Set(['json', 'binary', 'lmdb']);
|
|
56
|
+
const hybridFusions = new Set(['rrf', 'linear']);
|
|
57
|
+
const sanitizeStorageBackend = (value) => typeof value === 'string' && storageBackends.has(value) ? value : defaultBrainlinkConfig.storageBackend;
|
|
58
|
+
const sanitizeHybridFusion = (value) => typeof value === 'string' && hybridFusions.has(value) ? value : defaultBrainlinkConfig.hybridFusion;
|
|
50
59
|
const sanitizeEmbeddingProvider = (value) => typeof value === 'string' && embeddingProviders.has(value) ? value : defaultBrainlinkConfig.embeddingProvider;
|
|
51
60
|
const sanitizeOptionalString = (value) => typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
52
61
|
const sanitizeEmbeddingConfig = (value) => {
|
|
@@ -204,6 +213,8 @@ const sanitizeConfig = (value) => ({
|
|
|
204
213
|
autoCanonicalContextLinks: typeof value.autoCanonicalContextLinks === 'boolean'
|
|
205
214
|
? value.autoCanonicalContextLinks
|
|
206
215
|
: defaultBrainlinkConfig.autoCanonicalContextLinks,
|
|
216
|
+
autoVersion: typeof value.autoVersion === 'boolean' ? value.autoVersion : defaultBrainlinkConfig.autoVersion,
|
|
217
|
+
vaultEncryption: typeof value.vaultEncryption === 'boolean' ? value.vaultEncryption : defaultBrainlinkConfig.vaultEncryption,
|
|
207
218
|
autoUpdateCheck: typeof value.autoUpdateCheck === 'boolean' ? value.autoUpdateCheck : defaultBrainlinkConfig.autoUpdateCheck,
|
|
208
219
|
updateCheckIntervalMs: typeof value.updateCheckIntervalMs === 'number' && Number.isFinite(value.updateCheckIntervalMs) && value.updateCheckIntervalMs > 0
|
|
209
220
|
? Math.floor(value.updateCheckIntervalMs)
|
|
@@ -218,8 +229,11 @@ const sanitizeConfig = (value) => ({
|
|
|
218
229
|
defaultContextCacheTtlMs: typeof value.defaultContextCacheTtlMs === 'number' && Number.isFinite(value.defaultContextCacheTtlMs) && value.defaultContextCacheTtlMs > 0
|
|
219
230
|
? Math.floor(value.defaultContextCacheTtlMs)
|
|
220
231
|
: defaultBrainlinkConfig.defaultContextCacheTtlMs,
|
|
232
|
+
cagPackTtlMs: sanitizeIntegerInRange(value.cagPackTtlMs, defaultBrainlinkConfig.cagPackTtlMs, 0, 2_147_483_647),
|
|
221
233
|
allowedVaults: [...sanitizeAllowedVaults(value.allowedVaults), ...readAllowedVaultsFromEnv()],
|
|
222
234
|
chunkSize: typeof value.chunkSize === 'number' && value.chunkSize > 0 ? value.chunkSize : defaultBrainlinkConfig.chunkSize,
|
|
235
|
+
storageBackend: sanitizeStorageBackend(value.storageBackend),
|
|
236
|
+
hybridFusion: sanitizeHybridFusion(value.hybridFusion),
|
|
223
237
|
searchPack: sanitizeSearchPackConfig(value.searchPack),
|
|
224
238
|
embeddingProvider: sanitizeEmbeddingProvider(value.embeddingProvider),
|
|
225
239
|
embedding: sanitizeEmbeddingConfig(value.embedding),
|
|
@@ -234,7 +248,8 @@ export const resolveAgentRuntimeDefaults = (config, agent) => {
|
|
|
234
248
|
defaultContextTokens: profile?.defaultContextTokens ?? config.defaultContextTokens,
|
|
235
249
|
defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode,
|
|
236
250
|
defaultContextStrategy: profile?.defaultContextStrategy ?? config.defaultContextStrategy,
|
|
237
|
-
defaultContextCacheTtlMs: profile?.defaultContextCacheTtlMs ?? config.defaultContextCacheTtlMs
|
|
251
|
+
defaultContextCacheTtlMs: profile?.defaultContextCacheTtlMs ?? config.defaultContextCacheTtlMs,
|
|
252
|
+
cagPackTtlMs: config.cagPackTtlMs
|
|
238
253
|
};
|
|
239
254
|
};
|
|
240
255
|
const mergeConfigLayers = (layers) => layers.reduce((state, config) => ({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
|
+
const storedPackVersion = 2;
|
|
4
5
|
const normalizePackKey = (key) => ({
|
|
5
6
|
query: key.query.trim().toLowerCase(),
|
|
6
7
|
limit: key.limit,
|
|
@@ -13,12 +14,35 @@ export const contextPackPath = (vaultPath, key) => {
|
|
|
13
14
|
const digest = createHash('sha256').update(JSON.stringify(normalizePackKey(key))).digest('hex');
|
|
14
15
|
return join(contextPacksDirectory(vaultPath), `${digest}.json`);
|
|
15
16
|
};
|
|
17
|
+
// Signature of a single file, matching the volatile/index signatures used
|
|
18
|
+
// elsewhere (floored mtime + size). A missing file collapses to a stable
|
|
19
|
+
// sentinel so a deleted dependency reliably differs from its recorded value.
|
|
20
|
+
const fileSignature = async (path) => {
|
|
21
|
+
try {
|
|
22
|
+
const info = await stat(path);
|
|
23
|
+
return `${Math.floor(info.mtimeMs)}:${info.size}`;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return '0:0';
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
// Durable (non-volatile) section source paths, deduplicated. Volatile sections
|
|
30
|
+
// come from turn memory rather than vault files, so they are tracked through the
|
|
31
|
+
// separate volatile signature instead of per-document dependencies.
|
|
32
|
+
const dependencyPaths = (context) => {
|
|
33
|
+
const paths = context.sections.filter((section) => !section.volatile).map((section) => section.path);
|
|
34
|
+
return Array.from(new Set(paths));
|
|
35
|
+
};
|
|
36
|
+
const computeDependencies = async (vaultPath, context) => Promise.all(dependencyPaths(context).map(async (path) => ({ path, signature: await fileSignature(join(vaultPath, path)) })));
|
|
16
37
|
const isStoredContextPack = (value) => {
|
|
17
38
|
if (!value || typeof value !== 'object') {
|
|
18
39
|
return false;
|
|
19
40
|
}
|
|
20
41
|
const candidate = value;
|
|
21
|
-
return candidate.version ===
|
|
42
|
+
return (candidate.version === storedPackVersion &&
|
|
43
|
+
Array.isArray(candidate.dependencies) &&
|
|
44
|
+
typeof candidate.volatileSignature === 'string' &&
|
|
45
|
+
Boolean(candidate.context));
|
|
22
46
|
};
|
|
23
47
|
const readStoredContextPack = async (path) => {
|
|
24
48
|
try {
|
|
@@ -29,30 +53,43 @@ const readStoredContextPack = async (path) => {
|
|
|
29
53
|
return null;
|
|
30
54
|
}
|
|
31
55
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
// A pack is fresh while it is within its optional TTL, its volatile signature
|
|
57
|
+
// matches, and every cited document is unchanged on disk. Reads no vault index,
|
|
58
|
+
// so the CAG fast path stays cheap: a few stats over the cited files.
|
|
59
|
+
const isPackFresh = async (vaultPath, stored, freshness, now) => {
|
|
60
|
+
if (freshness.ttlMs > 0) {
|
|
61
|
+
const age = now - Date.parse(stored.createdAt);
|
|
62
|
+
if (!Number.isFinite(age) || age > freshness.ttlMs) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (stored.volatileSignature !== freshness.volatileSignature) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const current = await Promise.all(stored.dependencies.map((dependency) => fileSignature(join(vaultPath, dependency.path))));
|
|
70
|
+
return stored.dependencies.every((dependency, index) => dependency.signature === current[index]);
|
|
71
|
+
};
|
|
72
|
+
const toContextPackSummary = async (path, vaultPath, freshness) => {
|
|
73
|
+
const [info, stored] = await Promise.all([stat(path), readStoredContextPack(path)]);
|
|
74
|
+
const stale = !stored || (freshness ? !(await isPackFresh(vaultPath, stored, freshness, Date.now())) : false);
|
|
38
75
|
return {
|
|
39
76
|
path,
|
|
40
77
|
filename: basename(path),
|
|
41
78
|
createdAt: stored?.createdAt ?? null,
|
|
42
|
-
|
|
79
|
+
dependencyCount: stored?.dependencies.length ?? null,
|
|
43
80
|
key: stored?.key ?? null,
|
|
44
81
|
sizeBytes: info.size,
|
|
45
82
|
stale
|
|
46
83
|
};
|
|
47
84
|
};
|
|
48
|
-
export const listContextPacks = async (vaultPath,
|
|
85
|
+
export const listContextPacks = async (vaultPath, freshness) => {
|
|
49
86
|
const directory = contextPacksDirectory(vaultPath);
|
|
50
87
|
try {
|
|
51
88
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
52
89
|
const paths = entries
|
|
53
90
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
54
91
|
.map((entry) => join(directory, entry.name));
|
|
55
|
-
const summaries = await Promise.all(paths.map((path) => toContextPackSummary(path,
|
|
92
|
+
const summaries = await Promise.all(paths.map((path) => toContextPackSummary(path, vaultPath, freshness)));
|
|
56
93
|
return summaries.sort((left, right) => (right.createdAt ?? '').localeCompare(left.createdAt ?? '') || left.filename.localeCompare(right.filename));
|
|
57
94
|
}
|
|
58
95
|
catch (error) {
|
|
@@ -63,20 +100,20 @@ export const listContextPacks = async (vaultPath, dataSignature) => {
|
|
|
63
100
|
}
|
|
64
101
|
};
|
|
65
102
|
export const clearContextPacks = async (vaultPath, options = {}) => {
|
|
66
|
-
const packs = await listContextPacks(vaultPath, options.
|
|
103
|
+
const packs = await listContextPacks(vaultPath, options.freshness);
|
|
67
104
|
const removed = options.staleOnly ? packs.filter((pack) => pack.stale) : packs;
|
|
68
105
|
const kept = options.staleOnly ? packs.filter((pack) => !pack.stale) : [];
|
|
69
106
|
await Promise.all(removed.map((pack) => rm(pack.path, { force: true })));
|
|
70
107
|
return { removed, kept };
|
|
71
108
|
};
|
|
72
|
-
export const readContextPack = async (vaultPath, key,
|
|
109
|
+
export const readContextPack = async (vaultPath, key, freshness) => {
|
|
73
110
|
const path = contextPackPath(vaultPath, key);
|
|
74
111
|
try {
|
|
75
112
|
const parsed = await readStoredContextPack(path);
|
|
76
113
|
if (!parsed) {
|
|
77
114
|
return { status: 'stale', path };
|
|
78
115
|
}
|
|
79
|
-
if (parsed
|
|
116
|
+
if (!(await isPackFresh(vaultPath, parsed, freshness, Date.now()))) {
|
|
80
117
|
return { status: 'stale', path };
|
|
81
118
|
}
|
|
82
119
|
return {
|
|
@@ -88,7 +125,7 @@ export const readContextPack = async (vaultPath, key, dataSignature) => {
|
|
|
88
125
|
cache: {
|
|
89
126
|
storage: 'context-pack',
|
|
90
127
|
status: 'hit',
|
|
91
|
-
dataSignature,
|
|
128
|
+
dataSignature: parsed.volatileSignature,
|
|
92
129
|
path
|
|
93
130
|
}
|
|
94
131
|
}
|
|
@@ -98,12 +135,14 @@ export const readContextPack = async (vaultPath, key, dataSignature) => {
|
|
|
98
135
|
return { status: 'miss', path };
|
|
99
136
|
}
|
|
100
137
|
};
|
|
101
|
-
export const writeContextPack = async (vaultPath, key,
|
|
138
|
+
export const writeContextPack = async (vaultPath, key, context, meta) => {
|
|
102
139
|
const path = contextPackPath(vaultPath, key);
|
|
140
|
+
const dependencies = await computeDependencies(vaultPath, context);
|
|
103
141
|
const stored = {
|
|
104
|
-
version:
|
|
142
|
+
version: storedPackVersion,
|
|
105
143
|
createdAt: new Date().toISOString(),
|
|
106
|
-
|
|
144
|
+
dependencies,
|
|
145
|
+
volatileSignature: meta.volatileSignature,
|
|
107
146
|
key: normalizePackKey(key),
|
|
108
147
|
context: {
|
|
109
148
|
...context,
|
|
@@ -111,7 +150,7 @@ export const writeContextPack = async (vaultPath, key, dataSignature, context) =
|
|
|
111
150
|
cache: {
|
|
112
151
|
storage: 'context-pack',
|
|
113
152
|
status: 'refresh',
|
|
114
|
-
dataSignature,
|
|
153
|
+
dataSignature: meta.volatileSignature,
|
|
115
154
|
path
|
|
116
155
|
}
|
|
117
156
|
}
|