@andespindola/brainlink 0.1.0-beta.141 → 0.1.0-beta.142

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
@@ -694,6 +694,7 @@ blink config set-vault "s3://my-memory-bucket/brainlink" --global
694
694
 
695
695
  `config set-vault` writes configuration through CLI (no manual file edits required).
696
696
  By default it writes local config (`./brainlink.config.json`), appends the vault to `allowedVaults`, and migrates Markdown memory from the current configured vault when the target is empty.
697
+ When the configured default vault is changed manually in config files, Brainlink also performs automatic migration on the next command that uses the configured vault (without explicit `--vault`).
697
698
  Use `--global` to write to `$BRAINLINK_HOME/brainlink.config.json`, `--no-migrate` to skip migration, and `--no-index` to skip post-migration indexing.
698
699
  `config doctor` is dry-run by default; use `--fix` to apply safe config normalization and allowlist fixes.
699
700
 
@@ -0,0 +1,37 @@
1
+ import { indexVault } from './index-vault.js';
2
+ import { migrateVaultContent } from './migrate-vault.js';
3
+ import { getLastConfiguredVaultForKey, setLastConfiguredVaultForKey } from '../infrastructure/vault-migration-state.js';
4
+ export const autoMigrateConfiguredVaultIfChanged = async (input) => {
5
+ const configKey = input.configKey.trim();
6
+ const configuredVault = input.configuredVault.trim();
7
+ if (configKey.length === 0 || configuredVault.length === 0) {
8
+ return {
9
+ changed: false,
10
+ migrated: false
11
+ };
12
+ }
13
+ const previousVault = await getLastConfiguredVaultForKey(configKey);
14
+ if (!previousVault) {
15
+ await setLastConfiguredVaultForKey(configKey, configuredVault);
16
+ return {
17
+ changed: false,
18
+ migrated: false
19
+ };
20
+ }
21
+ if (previousVault === configuredVault) {
22
+ return {
23
+ changed: false,
24
+ migrated: false
25
+ };
26
+ }
27
+ const migration = await migrateVaultContent(previousVault, configuredVault);
28
+ const shouldIndex = migration.copied + migration.conflicted > 0;
29
+ if (shouldIndex) {
30
+ await indexVault(configuredVault);
31
+ }
32
+ await setLastConfiguredVaultForKey(configKey, configuredVault);
33
+ return {
34
+ changed: true,
35
+ migrated: shouldIndex
36
+ };
37
+ };
@@ -1,11 +1,19 @@
1
- import { loadBrainlinkConfig, resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
1
+ import { autoMigrateConfiguredVaultIfChanged } from '../application/auto-migrate-configured-vault.js';
2
+ import { loadBrainlinkConfigWithSource, resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
2
3
  import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
3
4
  export const parsePositiveInteger = (value, fallback) => {
4
5
  const parsed = Number.parseInt(value, 10);
5
6
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
6
7
  };
7
8
  export const resolveOptions = async (options) => {
8
- const config = await loadBrainlinkConfig();
9
+ const { config, vaultSource } = await loadBrainlinkConfigWithSource();
10
+ if (options.vault === undefined) {
11
+ const sourceKey = vaultSource.sourcePath ? `${vaultSource.source}:${vaultSource.sourcePath}` : vaultSource.source;
12
+ await autoMigrateConfiguredVaultIfChanged({
13
+ configKey: sourceKey,
14
+ configuredVault: config.vault
15
+ });
16
+ }
9
17
  const vault = options.vault ?? config.vault;
10
18
  const allowedVault = assertVaultAllowed(vault, config.allowedVaults);
11
19
  const agent = options.agent ?? config.defaultAgent;
@@ -181,12 +181,87 @@ export const resolveAgentRuntimeDefaults = (config, agent) => {
181
181
  defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode
182
182
  };
183
183
  };
184
+ const mergeConfigLayers = (layers) => layers.reduce((state, config) => ({
185
+ ...state,
186
+ ...config
187
+ }), {});
188
+ export const getVaultConfigSourceDetails = async (cwd = safeCwd()) => {
189
+ const [globalConfig, localConfig, legacyLocalConfig] = await Promise.all([
190
+ loadRawConfig('global', cwd),
191
+ loadRawConfig('local', cwd),
192
+ loadLegacyLocalRawConfig(cwd)
193
+ ]);
194
+ if (typeof legacyLocalConfig.vault === 'string' && legacyLocalConfig.vault.trim().length > 0) {
195
+ return {
196
+ source: 'local-legacy',
197
+ sourcePath: resolve(cwd, '.brainlink.json')
198
+ };
199
+ }
200
+ if (typeof localConfig.vault === 'string' && localConfig.vault.trim().length > 0) {
201
+ return {
202
+ source: 'local',
203
+ sourcePath: getLocalConfigPath(cwd)
204
+ };
205
+ }
206
+ if (typeof globalConfig.vault === 'string' && globalConfig.vault.trim().length > 0) {
207
+ return {
208
+ source: 'global',
209
+ sourcePath: getGlobalConfigPath()
210
+ };
211
+ }
212
+ return {
213
+ source: 'default',
214
+ sourcePath: null
215
+ };
216
+ };
184
217
  export const loadBrainlinkConfig = async (cwd = safeCwd()) => {
185
218
  const globalConfig = await readJsonConfig(getGlobalConfigPath());
186
219
  const localConfigs = await Promise.all(configFilenames.map((filename) => readJsonConfig(resolve(cwd, filename))));
187
- const merged = [globalConfig, ...localConfigs].reduce((state, config) => ({
188
- ...state,
189
- ...config
190
- }), {});
220
+ const merged = mergeConfigLayers([globalConfig, ...localConfigs]);
191
221
  return sanitizeConfig(merged);
192
222
  };
223
+ export const loadBrainlinkConfigWithSource = async (cwd = safeCwd()) => {
224
+ const globalConfigPath = getGlobalConfigPath();
225
+ const localConfigPath = getLocalConfigPath(cwd);
226
+ const legacyLocalConfigPath = resolve(cwd, '.brainlink.json');
227
+ const [globalConfig, localConfig, legacyLocalConfig] = await Promise.all([
228
+ readJsonConfig(globalConfigPath),
229
+ readJsonConfig(localConfigPath),
230
+ readJsonConfig(legacyLocalConfigPath)
231
+ ]);
232
+ const config = sanitizeConfig(mergeConfigLayers([globalConfig, localConfig, legacyLocalConfig]));
233
+ if (typeof legacyLocalConfig.vault === 'string' && legacyLocalConfig.vault.trim().length > 0) {
234
+ return {
235
+ config,
236
+ vaultSource: {
237
+ source: 'local-legacy',
238
+ sourcePath: legacyLocalConfigPath
239
+ }
240
+ };
241
+ }
242
+ if (typeof localConfig.vault === 'string' && localConfig.vault.trim().length > 0) {
243
+ return {
244
+ config,
245
+ vaultSource: {
246
+ source: 'local',
247
+ sourcePath: localConfigPath
248
+ }
249
+ };
250
+ }
251
+ if (typeof globalConfig.vault === 'string' && globalConfig.vault.trim().length > 0) {
252
+ return {
253
+ config,
254
+ vaultSource: {
255
+ source: 'global',
256
+ sourcePath: globalConfigPath
257
+ }
258
+ };
259
+ }
260
+ return {
261
+ config,
262
+ vaultSource: {
263
+ source: 'default',
264
+ sourcePath: null
265
+ }
266
+ };
267
+ };
@@ -0,0 +1,69 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { getBrainlinkHomePath } from './paths.js';
4
+ const defaultState = {
5
+ byConfigKey: {}
6
+ };
7
+ const statePath = () => join(getBrainlinkHomePath(), 'vault-migration-state.json');
8
+ const sanitizeState = (value) => {
9
+ if (typeof value !== 'object' || value === null) {
10
+ return defaultState;
11
+ }
12
+ const record = value;
13
+ const byConfigKeyRecord = typeof record.byConfigKey === 'object' && record.byConfigKey !== null ? record.byConfigKey : {};
14
+ const byConfigKey = Object.entries(byConfigKeyRecord).reduce((state, [key, vault]) => {
15
+ if (typeof key !== 'string' || key.trim().length === 0) {
16
+ return state;
17
+ }
18
+ if (typeof vault !== 'string' || vault.trim().length === 0) {
19
+ return state;
20
+ }
21
+ return {
22
+ ...state,
23
+ [key]: vault.trim()
24
+ };
25
+ }, {});
26
+ return {
27
+ byConfigKey
28
+ };
29
+ };
30
+ const readState = async () => {
31
+ try {
32
+ const raw = await readFile(statePath(), 'utf8');
33
+ return sanitizeState(JSON.parse(raw));
34
+ }
35
+ catch (error) {
36
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
37
+ return defaultState;
38
+ }
39
+ throw error;
40
+ }
41
+ };
42
+ const writeState = async (state) => {
43
+ const path = statePath();
44
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
45
+ await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
46
+ };
47
+ export const getVaultMigrationStatePath = () => statePath();
48
+ export const getLastConfiguredVaultForKey = async (configKey) => {
49
+ const state = await readState();
50
+ const value = state.byConfigKey[configKey];
51
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
52
+ };
53
+ export const setLastConfiguredVaultForKey = async (configKey, vault) => {
54
+ const key = configKey.trim();
55
+ const value = vault.trim();
56
+ if (key.length === 0 || value.length === 0) {
57
+ return;
58
+ }
59
+ const state = await readState();
60
+ if (state.byConfigKey[key] === value) {
61
+ return;
62
+ }
63
+ await writeState({
64
+ byConfigKey: {
65
+ ...state.byConfigKey,
66
+ [key]: value
67
+ }
68
+ });
69
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.141",
3
+ "version": "0.1.0-beta.142",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",