@andespindola/brainlink 0.1.0-beta.167 → 0.1.0-beta.168

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 CHANGED
@@ -717,6 +717,21 @@ When the configured default vault is changed manually in config files, Brainlink
717
717
  Use `--global` to write to `$BRAINLINK_HOME/brainlink.config.json`, `--no-migrate` to skip migration, and `--no-index` to skip post-migration indexing.
718
718
  `config doctor` is dry-run by default; use `--fix` to apply safe config normalization and allowlist fixes.
719
719
 
720
+ ### `vaults`
721
+
722
+ ```bash
723
+ blink vaults list
724
+ blink vaults list --json
725
+ blink vaults use /absolute/path/to/vault
726
+ blink vaults use /absolute/path/to/vault --global
727
+ blink vaults delete /absolute/path/to/vault --yes
728
+ blink vaults delete /absolute/path/to/vault --yes --prune-config
729
+ ```
730
+
731
+ Lists known vaults from the configured default, `allowedVaults`, and the built-in default at `$HOME/.brainlink/vault`.
732
+ `vaults use` chooses the default vault without migrating memory; use `migrate-vault` or `config set-vault --migrate-from` when you want to copy Markdown memory between vaults.
733
+ `vaults delete` only deletes local filesystem vaults, requires `--yes`, refuses bucket vaults, and refuses deleting the current default vault. Choose another default first with `vaults use`.
734
+
720
735
  ### `migrate-vault`
721
736
 
722
737
  ```bash
@@ -0,0 +1,182 @@
1
+ import { readdir, readFile, rm, stat } from 'node:fs/promises';
2
+ import { extname, isAbsolute, join } from 'node:path';
3
+ import { doctorVault } from '../../application/analyze-vault.js';
4
+ import { defaultBrainlinkConfig, loadBrainlinkConfig, loadRawConfig, writeRawConfig } from '../../infrastructure/config.js';
5
+ import { assertVaultAllowed, isBucketVaultPath, resolveVaultPath } from '../../infrastructure/file-system-vault.js';
6
+ import { print } from '../runtime.js';
7
+ const excludedDirectories = new Set(['.git', 'node_modules', 'dist']);
8
+ const resolveScope = (globalOption) => globalOption ? 'global' : 'local';
9
+ const normalizeVaultPath = (vault) => assertVaultAllowed(vault, []);
10
+ const uniqueValues = (values) => Array.from(new Set(values));
11
+ const sameVault = (left, right) => normalizeVaultPath(left) === normalizeVaultPath(right);
12
+ const isDirectory = async (path) => {
13
+ try {
14
+ return (await stat(path)).isDirectory();
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ };
20
+ const countMarkdownFiles = async (directory) => {
21
+ const entries = await readdir(directory, { withFileTypes: true });
22
+ const counts = await Promise.all(entries.map(async (entry) => {
23
+ const absolutePath = join(directory, entry.name);
24
+ if (entry.isDirectory()) {
25
+ return excludedDirectories.has(entry.name) ? 0 : countMarkdownFiles(absolutePath);
26
+ }
27
+ return entry.isFile() && extname(entry.name).toLowerCase() === '.md' ? 1 : 0;
28
+ }));
29
+ return counts.reduce((total, count) => total + count, 0);
30
+ };
31
+ const readIndexedDocumentCount = async (vaultPath) => {
32
+ try {
33
+ const raw = await readFile(join(vaultPath, '.brainlink', 'index.json'), 'utf8');
34
+ const parsed = JSON.parse(raw);
35
+ return Array.isArray(parsed.documents) ? parsed.documents.length : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ };
41
+ const addCandidate = (state, path, source) => {
42
+ const normalized = normalizeVaultPath(path);
43
+ const current = state.get(normalized);
44
+ return new Map(state).set(normalized, {
45
+ path: normalized,
46
+ sources: uniqueValues([...(current?.sources ?? []), source])
47
+ });
48
+ };
49
+ const listVaultEntries = async () => {
50
+ const config = await loadBrainlinkConfig();
51
+ const candidates = [config.vault, ...config.allowedVaults, defaultBrainlinkConfig.vault].reduce((state, path, index) => addCandidate(state, path, index === 0 ? 'configured' : path === defaultBrainlinkConfig.vault ? 'default' : 'allowed'), new Map());
52
+ const entries = await Promise.all(Array.from(candidates.values()).map(async (candidate) => {
53
+ if (isBucketVaultPath(candidate.path)) {
54
+ return {
55
+ path: candidate.path,
56
+ sources: candidate.sources,
57
+ current: sameVault(candidate.path, config.vault),
58
+ kind: 'bucket',
59
+ exists: null,
60
+ markdownCount: null,
61
+ indexedDocumentCount: null
62
+ };
63
+ }
64
+ const path = resolveVaultPath(candidate.path);
65
+ const exists = await isDirectory(path);
66
+ return {
67
+ path,
68
+ sources: candidate.sources,
69
+ current: sameVault(path, config.vault),
70
+ kind: 'local',
71
+ exists,
72
+ markdownCount: exists ? await countMarkdownFiles(path) : null,
73
+ indexedDocumentCount: exists ? await readIndexedDocumentCount(path) : null
74
+ };
75
+ }));
76
+ return entries.sort((left, right) => Number(right.current) - Number(left.current) || left.path.localeCompare(right.path));
77
+ };
78
+ const removeVaultFromAllowedVaults = async (vaultPath) => {
79
+ const scopes = ['local', 'global'];
80
+ await Promise.all(scopes.map(async (scope) => {
81
+ const rawConfig = await loadRawConfig(scope);
82
+ const allowedVaults = Array.isArray(rawConfig.allowedVaults)
83
+ ? rawConfig.allowedVaults.filter((path) => typeof path === 'string')
84
+ : [];
85
+ const nextAllowedVaults = allowedVaults.filter((path) => !sameVault(path, vaultPath));
86
+ if (nextAllowedVaults.length === allowedVaults.length) {
87
+ return;
88
+ }
89
+ await writeRawConfig(scope, {
90
+ ...rawConfig,
91
+ allowedVaults: nextAllowedVaults
92
+ });
93
+ }));
94
+ };
95
+ export const registerVaultCommands = (program) => {
96
+ const vaultsCommand = program.command('vaults').description('list, choose and delete Brainlink vaults');
97
+ vaultsCommand
98
+ .command('list')
99
+ .option('--json', 'print machine-readable JSON')
100
+ .description('list known Brainlink vaults from config and defaults')
101
+ .action(async (options) => {
102
+ const config = await loadBrainlinkConfig();
103
+ const vaults = await listVaultEntries();
104
+ print(options.json, {
105
+ currentVault: config.vault,
106
+ vaults
107
+ }, () => vaults
108
+ .map((vault) => {
109
+ const status = vault.exists === null ? 'remote' : vault.exists ? 'exists' : 'missing';
110
+ const counts = vault.markdownCount === null
111
+ ? ''
112
+ : ` markdown=${vault.markdownCount} indexed=${vault.indexedDocumentCount ?? 'unknown'}`;
113
+ return `${vault.current ? '* ' : ' '}${vault.path} [${vault.sources.join(',')}; ${status}; ${vault.kind}]${counts}`;
114
+ })
115
+ .join('\n'));
116
+ });
117
+ vaultsCommand
118
+ .command('use <vault>')
119
+ .option('--global', 'write to global config in $BRAINLINK_HOME/brainlink.config.json')
120
+ .option('--no-allowlist', 'do not append the vault to allowedVaults in the target config file')
121
+ .option('--json', 'print machine-readable JSON')
122
+ .description('choose the default Brainlink vault without migrating memory')
123
+ .action(async (vault, options) => {
124
+ const scope = resolveScope(options.global);
125
+ const before = await loadBrainlinkConfig();
126
+ const targetVault = normalizeVaultPath(vault);
127
+ const rawConfig = await loadRawConfig(scope);
128
+ const shouldAllowlist = options.allowlist !== false;
129
+ const allowedVaults = Array.isArray(rawConfig.allowedVaults)
130
+ ? rawConfig.allowedVaults.filter((path) => typeof path === 'string')
131
+ : [];
132
+ const nextAllowedVaults = shouldAllowlist ? uniqueValues([...allowedVaults, targetVault]) : allowedVaults;
133
+ const configPath = await writeRawConfig(scope, {
134
+ ...rawConfig,
135
+ vault: targetVault,
136
+ allowedVaults: nextAllowedVaults
137
+ });
138
+ const doctor = await doctorVault(targetVault);
139
+ print(options.json, {
140
+ scope,
141
+ configPath,
142
+ previousVault: before.vault,
143
+ vault: targetVault,
144
+ doctor
145
+ }, () => `Default ${scope} vault set to ${targetVault} in ${configPath}.`);
146
+ });
147
+ vaultsCommand
148
+ .command('delete <vault>')
149
+ .option('--yes', 'confirm destructive vault deletion')
150
+ .option('--prune-config', 'remove the vault from allowedVaults after deletion')
151
+ .option('--json', 'print machine-readable JSON')
152
+ .description('delete a local filesystem vault after explicit confirmation')
153
+ .action(async (vault, options) => {
154
+ const config = await loadBrainlinkConfig();
155
+ const targetVault = normalizeVaultPath(vault);
156
+ if (isBucketVaultPath(targetVault)) {
157
+ throw new Error('Refusing to delete bucket vaults from the CLI. Remove bucket data with your storage provider tooling.');
158
+ }
159
+ const absoluteVault = resolveVaultPath(targetVault);
160
+ if (!isAbsolute(absoluteVault)) {
161
+ throw new Error(`Refusing to delete non-absolute vault path: ${absoluteVault}`);
162
+ }
163
+ if (sameVault(absoluteVault, config.vault)) {
164
+ throw new Error('Refusing to delete the current default vault. Choose another default with `blink vaults use <vault>` first.');
165
+ }
166
+ if (options.yes !== true) {
167
+ throw new Error('Refusing to delete vault without --yes.');
168
+ }
169
+ const existed = await isDirectory(absoluteVault);
170
+ if (existed) {
171
+ await rm(absoluteVault, { recursive: true, force: false });
172
+ }
173
+ if (options.pruneConfig) {
174
+ await removeVaultFromAllowedVaults(absoluteVault);
175
+ }
176
+ print(options.json, {
177
+ vault: absoluteVault,
178
+ deleted: existed,
179
+ prunedConfig: options.pruneConfig === true
180
+ }, () => `${existed ? 'Deleted' : 'Vault did not exist'}: ${absoluteVault}`);
181
+ });
182
+ };
package/dist/cli/main.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { registerAgentCommands } from './commands/agent-commands.js';
7
7
  import { registerConfigCommands } from './commands/config-commands.js';
8
8
  import { registerReadCommands } from './commands/read-commands.js';
9
+ import { registerVaultCommands } from './commands/vault-commands.js';
9
10
  import { registerWriteCommands } from './commands/write-commands.js';
10
11
  const readPackageVersion = () => {
11
12
  const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
@@ -24,6 +25,7 @@ program
24
25
  registerWriteCommands(program);
25
26
  registerReadCommands(program);
26
27
  registerConfigCommands(program);
28
+ registerVaultCommands(program);
27
29
  registerAgentCommands(program);
28
30
  program.parseAsync().catch((error) => {
29
31
  const message = error instanceof Error ? error.message : String(error);
@@ -385,6 +385,19 @@ blink config set-vault /absolute/path/to/vault --global
385
385
 
386
386
  `config set-vault` updates Brainlink config through CLI. By default it writes local `brainlink.config.json`, appends the vault to `allowedVaults`, and migrates markdown when the target is empty.
387
387
 
388
+ ### Manage Known Vaults
389
+
390
+ ```bash
391
+ blink vaults list
392
+ blink vaults use /absolute/path/to/vault
393
+ blink vaults use /absolute/path/to/vault --global
394
+ blink vaults delete /absolute/path/to/vault --yes
395
+ ```
396
+
397
+ `vaults list` shows the configured default vault, allowlisted vaults and the built-in default vault, including local existence and Markdown/index counts when available.
398
+ `vaults use` switches the default vault without migrating memory.
399
+ `vaults delete` deletes only local filesystem vaults, requires `--yes`, and refuses deleting the current default vault.
400
+
388
401
  ### Migrate Vaults Explicitly
389
402
 
390
403
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.167",
3
+ "version": "0.1.0-beta.168",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",