@andespindola/brainlink 0.1.0-beta.4 → 0.1.0-beta.41

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 (59) hide show
  1. package/AGENTS.md +5 -5
  2. package/CHANGELOG.md +45 -2
  3. package/CONTRIBUTING.md +2 -2
  4. package/COPYRIGHT.md +5 -0
  5. package/README.md +216 -20
  6. package/SECURITY.md +1 -1
  7. package/dist/application/add-note.js +62 -13
  8. package/dist/application/analyze-vault.js +95 -8
  9. package/dist/application/build-context.js +56 -1
  10. package/dist/application/dedupe-notes.js +226 -0
  11. package/dist/application/frontend/client-css.js +214 -100
  12. package/dist/application/frontend/client-html.js +60 -45
  13. package/dist/application/frontend/client-js.js +818 -94
  14. package/dist/application/get-graph-layout.js +22 -7
  15. package/dist/application/get-graph-node.js +12 -0
  16. package/dist/application/get-graph-summary.js +12 -0
  17. package/dist/application/get-graph.js +3 -3
  18. package/dist/application/import-legacy-sqlite.js +296 -0
  19. package/dist/application/index-vault.js +143 -20
  20. package/dist/application/list-agents.js +3 -3
  21. package/dist/application/list-links.js +5 -5
  22. package/dist/application/migrate-vault.js +91 -0
  23. package/dist/application/search-graph-node-ids.js +12 -0
  24. package/dist/application/search-knowledge.js +75 -5
  25. package/dist/application/server/routes.js +27 -1
  26. package/dist/benchmarks/large-vault.js +1 -1
  27. package/dist/cli/commands/agent-commands.js +412 -0
  28. package/dist/cli/commands/config-commands.js +167 -0
  29. package/dist/cli/commands/read-commands.js +25 -8
  30. package/dist/cli/commands/write-commands.js +669 -9
  31. package/dist/cli/main.js +4 -0
  32. package/dist/cli/runtime.js +5 -2
  33. package/dist/domain/context.js +53 -11
  34. package/dist/domain/embeddings.js +2 -1
  35. package/dist/domain/graph-layout.js +20 -14
  36. package/dist/domain/markdown.js +36 -4
  37. package/dist/domain/middle-out.js +18 -0
  38. package/dist/infrastructure/config.js +94 -8
  39. package/dist/infrastructure/file-index.js +358 -0
  40. package/dist/infrastructure/file-system-vault.js +30 -0
  41. package/dist/infrastructure/index-state.js +50 -0
  42. package/dist/infrastructure/paths.js +9 -1
  43. package/dist/infrastructure/private-pack-codec.js +73 -0
  44. package/dist/infrastructure/search-packs.js +348 -0
  45. package/dist/infrastructure/session-state.js +172 -0
  46. package/dist/mcp/main.js +11 -3
  47. package/dist/mcp/server.js +27 -2
  48. package/dist/mcp/startup.js +35 -0
  49. package/dist/mcp/tools.js +633 -19
  50. package/docs/AGENT_USAGE.md +144 -16
  51. package/docs/ARCHITECTURE.md +37 -26
  52. package/docs/QUICKSTART.md +111 -0
  53. package/package.json +6 -4
  54. package/dist/infrastructure/sqlite/document-writer.js +0 -51
  55. package/dist/infrastructure/sqlite/graph-reader.js +0 -120
  56. package/dist/infrastructure/sqlite/schema.js +0 -111
  57. package/dist/infrastructure/sqlite/search-reader.js +0 -156
  58. package/dist/infrastructure/sqlite/types.js +0 -1
  59. package/dist/infrastructure/sqlite-index.js +0 -25
@@ -0,0 +1,167 @@
1
+ import { doctorVault } from '../../application/analyze-vault.js';
2
+ import { indexVault } from '../../application/index-vault.js';
3
+ import { migrateVaultContent, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
4
+ import { defaultBrainlinkConfig, detectVaultConfigSource, loadBrainlinkConfig, loadLegacyLocalRawConfig, loadRawConfig, resolveConfigPath, writeRawConfig } from '../../infrastructure/config.js';
5
+ import { assertVaultAllowed } from '../../infrastructure/file-system-vault.js';
6
+ import { print } from '../runtime.js';
7
+ const resolveScope = (globalOption) => globalOption ? 'global' : 'local';
8
+ const normalizeVaultPath = (vault) => assertVaultAllowed(vault, []);
9
+ const uniqueValues = (values) => Array.from(new Set(values));
10
+ const resolveScopeFromSource = (source) => source === 'global' || source === 'default' ? 'global' : 'local';
11
+ export const registerConfigCommands = (program) => {
12
+ const configCommand = program.command('config').description('read or update Brainlink configuration');
13
+ configCommand
14
+ .command('get [key]')
15
+ .option('--json', 'print machine-readable JSON')
16
+ .description('read effective Brainlink config values')
17
+ .action(async (key, options) => {
18
+ const config = await loadBrainlinkConfig();
19
+ if (!key) {
20
+ print(options.json, config, () => JSON.stringify(config, null, 2));
21
+ return;
22
+ }
23
+ if (!(key in config)) {
24
+ throw new Error(`Unknown config key: ${key}`);
25
+ }
26
+ const value = config[key];
27
+ print(options.json, { key, value }, () => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
28
+ });
29
+ configCommand
30
+ .command('set-vault <vault>')
31
+ .option('--global', 'write to global config in $BRAINLINK_HOME/brainlink.config.json')
32
+ .option('--no-allowlist', 'do not append the vault to allowedVaults in the target config file')
33
+ .option('--migrate-from <vault>', 'copy existing Markdown memory from another vault into the configured vault')
34
+ .option('--no-migrate', 'skip migration step')
35
+ .option('--no-index', 'skip reindex after migration')
36
+ .option('--json', 'print machine-readable JSON')
37
+ .description('set the default vault path in Brainlink config')
38
+ .action(async (vault, options) => {
39
+ const scope = resolveScope(options.global);
40
+ const before = await loadBrainlinkConfig();
41
+ const targetVault = normalizeVaultPath(vault);
42
+ const rawConfig = await loadRawConfig(scope);
43
+ const configPath = resolveConfigPath(scope);
44
+ const shouldAllowlist = options.allowlist !== false;
45
+ const nextAllowedVaults = shouldAllowlist
46
+ ? uniqueValues([...(rawConfig.allowedVaults ?? []), targetVault])
47
+ : rawConfig.allowedVaults;
48
+ const nextRawConfig = {
49
+ ...rawConfig,
50
+ vault: targetVault,
51
+ ...(nextAllowedVaults ? { allowedVaults: nextAllowedVaults } : {})
52
+ };
53
+ await writeRawConfig(scope, nextRawConfig);
54
+ const shouldMigrate = options.migrate !== false;
55
+ const explicitSource = options.migrateFrom ? normalizeVaultPath(options.migrateFrom) : undefined;
56
+ const shouldAutoMigrate = shouldMigrate &&
57
+ explicitSource === undefined &&
58
+ (await shouldMigrateDefaultVault(before.vault, targetVault));
59
+ const migrationSource = shouldMigrate ? explicitSource ?? (shouldAutoMigrate ? before.vault : undefined) : undefined;
60
+ const migration = migrationSource ? await migrateVaultContent(migrationSource, targetVault) : undefined;
61
+ const shouldIndex = options.index !== false && migration !== undefined && migration.copied + migration.conflicted > 0;
62
+ const index = shouldIndex ? await indexVault(targetVault) : undefined;
63
+ const after = await loadBrainlinkConfig();
64
+ print(options.json, {
65
+ scope,
66
+ configPath,
67
+ beforeVault: before.vault,
68
+ vault: targetVault,
69
+ migration: migration ?? null,
70
+ index: index ?? null,
71
+ config: after
72
+ }, () => {
73
+ const migrationMessage = migration
74
+ ? ` Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`
75
+ : '';
76
+ const indexMessage = index
77
+ ? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
78
+ : '';
79
+ return `Configured ${scope} vault at ${targetVault} in ${configPath}.${migrationMessage}${indexMessage}`;
80
+ });
81
+ });
82
+ configCommand
83
+ .command('where')
84
+ .option('--json', 'print machine-readable JSON')
85
+ .description('show effective vault path and config file locations')
86
+ .action(async (options) => {
87
+ const config = await loadBrainlinkConfig();
88
+ print(options.json, {
89
+ vault: config.vault,
90
+ localConfigPath: resolveConfigPath('local'),
91
+ globalConfigPath: resolveConfigPath('global'),
92
+ defaultVault: defaultBrainlinkConfig.vault
93
+ }, () => [
94
+ `vault=${config.vault}`,
95
+ `localConfigPath=${resolveConfigPath('local')}`,
96
+ `globalConfigPath=${resolveConfigPath('global')}`,
97
+ `defaultVault=${defaultBrainlinkConfig.vault}`
98
+ ].join('\n'));
99
+ });
100
+ configCommand
101
+ .command('doctor')
102
+ .option('--fix', 'apply safe config fixes (without this flag, doctor is dry-run)')
103
+ .option('--json', 'print machine-readable JSON')
104
+ .description('inspect effective config sources and run vault readiness checks')
105
+ .action(async (options) => {
106
+ const config = await loadBrainlinkConfig();
107
+ const source = await detectVaultConfigSource();
108
+ const globalConfigPath = resolveConfigPath('global');
109
+ const localConfigPath = resolveConfigPath('local');
110
+ const allowedVaultCheck = assertVaultAllowed(config.vault, config.allowedVaults);
111
+ const vaultDoctor = await doctorVault(config.vault);
112
+ const targetScope = resolveScopeFromSource(source);
113
+ const rawConfig = source === 'local-legacy'
114
+ ? await loadLegacyLocalRawConfig()
115
+ : await loadRawConfig(targetScope);
116
+ const normalizedVault = normalizeVaultPath(typeof rawConfig.vault === 'string' ? rawConfig.vault : config.vault);
117
+ const normalizedAllowedVaults = uniqueValues([
118
+ ...(Array.isArray(rawConfig.allowedVaults) ? rawConfig.allowedVaults.filter((item) => typeof item === 'string') : []),
119
+ normalizedVault
120
+ ].map((value) => normalizeVaultPath(value)));
121
+ const nextRawConfig = {
122
+ ...rawConfig,
123
+ vault: normalizedVault,
124
+ allowedVaults: normalizedAllowedVaults
125
+ };
126
+ const plannedFixes = [
127
+ `normalize vault path in ${targetScope} config`,
128
+ `ensure allowedVaults includes ${normalizedVault}`,
129
+ ...(source === 'local-legacy' ? ['migrate .brainlink.json settings into brainlink.config.json'] : []),
130
+ ...(source === 'default' ? ['create global brainlink.config.json with explicit vault'] : [])
131
+ ];
132
+ let fixApplied = false;
133
+ let fixedConfigPath = null;
134
+ if (options.fix) {
135
+ fixedConfigPath = await writeRawConfig(targetScope, nextRawConfig);
136
+ fixApplied = true;
137
+ }
138
+ const response = {
139
+ vault: config.vault,
140
+ vaultSource: source,
141
+ allowedVaultCheck,
142
+ localConfigPath,
143
+ globalConfigPath,
144
+ doctor: vaultDoctor,
145
+ fix: {
146
+ dryRun: options.fix !== true,
147
+ applied: fixApplied,
148
+ scope: targetScope,
149
+ path: fixedConfigPath,
150
+ plannedFixes
151
+ }
152
+ };
153
+ print(options.json, response, () => [
154
+ `vault=${response.vault}`,
155
+ `vaultSource=${response.vaultSource}`,
156
+ `localConfigPath=${response.localConfigPath}`,
157
+ `globalConfigPath=${response.globalConfigPath}`,
158
+ `configFixDryRun=${response.fix.dryRun}`,
159
+ ...(response.fix.applied && response.fix.path ? [`configFixAppliedAt=${response.fix.path}`] : []),
160
+ ...(response.fix.plannedFixes.length > 0 ? ['Planned config fixes:', ...response.fix.plannedFixes.map((step) => `- ${step}`)] : []),
161
+ ...response.doctor.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`),
162
+ ...(response.doctor.recommendations && response.doctor.recommendations.length > 0
163
+ ? ['Recommended next steps:', ...response.doctor.recommendations.map((recommendation) => `- ${recommendation}`)]
164
+ : [])
165
+ ].join('\n'));
166
+ });
167
+ };
@@ -1,4 +1,4 @@
1
- import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../../application/analyze-vault.js';
1
+ import { getBrokenLinksReport, getExtendedStats, getOrphansReport, getStats, validateVault } from '../../application/analyze-vault.js';
2
2
  import { buildContextPackage } from '../../application/build-context.js';
3
3
  import { getGraph } from '../../application/get-graph.js';
4
4
  import { listAgents } from '../../application/list-agents.js';
@@ -12,14 +12,14 @@ export const registerReadCommands = (program) => {
12
12
  .argument('<query>', 'search query')
13
13
  .option('-v, --vault <vault>', 'vault directory')
14
14
  .option('-a, --agent <agent>', 'filter by agent memory namespace')
15
- .option('-l, --limit <limit>', 'maximum results', '10')
15
+ .option('-l, --limit <limit>', 'maximum results')
16
16
  .option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
17
17
  .option('--json', 'print machine-readable JSON')
18
18
  .description('search indexed knowledge')
19
19
  .action(async (query, options) => {
20
20
  const resolved = await resolveOptions(options);
21
- const limit = parsePositiveInteger(options.limit ?? String(resolved.config.defaultSearchLimit), resolved.config.defaultSearchLimit);
22
- const mode = sanitizeSearchMode(options.mode, resolved.config.defaultSearchMode);
21
+ const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
22
+ const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
23
23
  const results = await searchKnowledge(resolved.vault, query, limit, resolved.agent, mode);
24
24
  print(options.json, { query, agent: resolved.agent, limit, mode, results }, () => results
25
25
  .map((result, index) => [`${index + 1}. ${result.title} (${result.path}) score=${result.score.toFixed(3)} mode=${result.searchMode}`, result.content].join('\n'))
@@ -58,15 +58,15 @@ export const registerReadCommands = (program) => {
58
58
  .argument('<query>', 'context query')
59
59
  .option('-v, --vault <vault>', 'vault directory')
60
60
  .option('-a, --agent <agent>', 'filter by agent memory namespace')
61
- .option('-l, --limit <limit>', 'maximum search results before context selection', '12')
62
- .option('-t, --tokens <tokens>', 'maximum estimated context tokens', '2000')
61
+ .option('-l, --limit <limit>', 'maximum search results before context selection')
62
+ .option('-t, --tokens <tokens>', 'maximum estimated context tokens')
63
63
  .option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
64
64
  .option('--json', 'print machine-readable JSON')
65
65
  .description('build a compact context package for an agent')
66
66
  .action(async (query, options) => {
67
67
  const resolved = await resolveOptions(options);
68
- const mode = sanitizeSearchMode(options.mode, resolved.config.defaultSearchMode);
69
- const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ?? '12', 12), parsePositiveInteger(options.tokens ?? String(resolved.config.defaultContextTokens), resolved.config.defaultContextTokens), resolved.agent, mode);
68
+ const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
69
+ const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit), parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens), resolved.agent, mode);
70
70
  print(options.json, contextPackage, () => contextPackage.content);
71
71
  });
72
72
  program
@@ -94,10 +94,27 @@ export const registerReadCommands = (program) => {
94
94
  .command('stats')
95
95
  .option('-v, --vault <vault>', 'vault directory')
96
96
  .option('-a, --agent <agent>', 'filter by agent memory namespace')
97
+ .option('--extended', 'include storage, quality and latency observability probes')
97
98
  .option('--json', 'print machine-readable JSON')
98
99
  .description('print indexed vault statistics')
99
100
  .action(async (options) => {
100
101
  const resolved = await resolveOptions(options);
102
+ if (options.extended) {
103
+ const stats = await getExtendedStats(resolved.vault, resolved.agent);
104
+ print(options.json, stats, () => [
105
+ `Documents: ${stats.stats.documentCount}`,
106
+ `Links: ${stats.stats.linkCount}`,
107
+ `Resolved links: ${stats.stats.resolvedLinkCount}`,
108
+ `Broken links: ${stats.stats.brokenLinkCount}`,
109
+ `Orphans: ${stats.stats.orphanCount}`,
110
+ `Tags: ${stats.stats.tagCount}`,
111
+ `Total files: ${stats.storage.totalFileCount}`,
112
+ `Markdown files: ${stats.storage.markdownFileCount}`,
113
+ `Vault bytes: ${stats.storage.totalBytes}`,
114
+ `Latency index/search/context (ms): ${stats.observability.latenciesMs.index}/${stats.observability.latenciesMs.search}/${stats.observability.latenciesMs.context}`
115
+ ].join('\n'));
116
+ return;
117
+ }
101
118
  const stats = await getStats(resolved.vault, resolved.agent);
102
119
  print(options.json, stats, () => [
103
120
  `Documents: ${stats.documentCount}`,