@andespindola/brainlink 0.1.0-beta.1 → 0.1.0-beta.11
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 +46 -0
- package/README.md +241 -10
- package/dist/application/add-note.js +62 -13
- package/dist/application/analyze-vault.js +104 -9
- package/dist/application/frontend/client-css.js +154 -71
- package/dist/application/frontend/client-html.js +42 -33
- package/dist/application/frontend/client-js.js +316 -84
- package/dist/application/get-graph-layout.js +22 -7
- package/dist/application/get-graph-node.js +12 -0
- package/dist/application/get-graph-summary.js +12 -0
- package/dist/application/index-vault.js +7 -0
- package/dist/application/migrate-vault.js +91 -0
- package/dist/application/search-graph-node-ids.js +12 -0
- package/dist/application/search-knowledge.js +74 -4
- package/dist/application/server/routes.js +27 -1
- package/dist/cli/commands/agent-commands.js +412 -0
- package/dist/cli/commands/config-commands.js +167 -0
- package/dist/cli/commands/read-commands.js +25 -8
- package/dist/cli/commands/write-commands.js +173 -4
- package/dist/cli/main.js +4 -0
- package/dist/cli/runtime.js +5 -2
- package/dist/domain/embeddings.js +2 -1
- package/dist/domain/graph-layout.js +20 -14
- package/dist/domain/markdown.js +36 -4
- package/dist/infrastructure/config.js +94 -8
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/search-packs.js +151 -0
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/infrastructure/sqlite/graph-reader.js +252 -105
- package/dist/infrastructure/sqlite/recovery.js +83 -0
- package/dist/infrastructure/sqlite/schema.js +4 -1
- package/dist/infrastructure/sqlite/search-reader.js +104 -72
- package/dist/infrastructure/sqlite-index.js +16 -3
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +17 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +571 -19
- package/docs/AGENT_USAGE.md +87 -3
- package/docs/ARCHITECTURE.md +16 -1
- package/docs/QUICKSTART.md +104 -0
- package/docs/RELEASE.md +3 -3
- package/package.json +1 -1
|
@@ -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'
|
|
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.
|
|
22
|
-
const mode = sanitizeSearchMode(options.mode, resolved.
|
|
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'
|
|
62
|
-
.option('-t, --tokens <tokens>', 'maximum estimated context tokens'
|
|
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.
|
|
69
|
-
const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ??
|
|
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}`,
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
2
4
|
import { addNote } from '../../application/add-note.js';
|
|
5
|
+
import { buildContextPackage } from '../../application/build-context.js';
|
|
3
6
|
import { indexVault } from '../../application/index-vault.js';
|
|
7
|
+
import { migrateVaultContent, planVaultMigration, previewVaultMigration, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
|
|
4
8
|
import { startServer } from '../../application/start-server.js';
|
|
5
9
|
import { startVaultWatcher } from '../../application/watch-vault.js';
|
|
6
|
-
import { doctorVault } from '../../application/analyze-vault.js';
|
|
10
|
+
import { doctorVault, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
11
|
+
import { defaultBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
7
12
|
import { loadBrainlinkConfig } from '../../infrastructure/config.js';
|
|
8
13
|
import { assertVaultAllowed, ensureVault } from '../../infrastructure/file-system-vault.js';
|
|
14
|
+
import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
|
|
15
|
+
import { installAgentIntegration } from './agent-commands.js';
|
|
9
16
|
import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
|
|
10
17
|
const resolveAddContent = (options) => {
|
|
11
18
|
if (options.content != null && options.content.trim().length > 0) {
|
|
@@ -20,12 +27,77 @@ export const registerWriteCommands = (program) => {
|
|
|
20
27
|
program
|
|
21
28
|
.command('init')
|
|
22
29
|
.argument('[vault]', 'vault directory')
|
|
30
|
+
.option('--migrate-from <vault>', 'copy existing vault content into the initialized vault')
|
|
31
|
+
.option('--no-migrate-existing', 'skip automatic migration from the default Brainlink vault into an empty custom vault')
|
|
23
32
|
.option('--json', 'print machine-readable JSON')
|
|
24
33
|
.description('initialize a Brainlink vault')
|
|
25
34
|
.action(async (vault, options) => {
|
|
26
35
|
const config = await loadBrainlinkConfig();
|
|
27
|
-
const
|
|
28
|
-
|
|
36
|
+
const targetVault = assertVaultAllowed(vault ?? config.vault, config.allowedVaults);
|
|
37
|
+
const path = await ensureVault(targetVault);
|
|
38
|
+
const explicitSource = options.migrateFrom ? assertVaultAllowed(options.migrateFrom, config.allowedVaults) : undefined;
|
|
39
|
+
const shouldAutoMigrate = explicitSource === undefined &&
|
|
40
|
+
options.migrateExisting !== false &&
|
|
41
|
+
(await shouldMigrateDefaultVault(defaultBrainlinkConfig.vault, targetVault));
|
|
42
|
+
const migration = explicitSource || shouldAutoMigrate ? await migrateVaultContent(explicitSource ?? defaultBrainlinkConfig.vault, targetVault) : undefined;
|
|
43
|
+
const index = migration && migration.copied + migration.conflicted > 0 ? await indexVault(targetVault) : undefined;
|
|
44
|
+
print(options.json, { path, ...(migration ? { migration } : {}), ...(index ? { index } : {}) }, () => {
|
|
45
|
+
const migrated = migration
|
|
46
|
+
? ` Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`
|
|
47
|
+
: '';
|
|
48
|
+
return `Initialized Brainlink vault at ${path}.${migrated}`;
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
program
|
|
52
|
+
.command('migrate-vault')
|
|
53
|
+
.option('--from <vault>', 'source vault path')
|
|
54
|
+
.option('--to <vault>', 'target vault path')
|
|
55
|
+
.option('--dry-run', 'preview migration without writing files')
|
|
56
|
+
.option('--report <path>', 'write detailed per-file migration report to JSON file')
|
|
57
|
+
.option('--no-index', 'skip reindexing target vault after migration')
|
|
58
|
+
.option('--json', 'print machine-readable JSON')
|
|
59
|
+
.description('copy markdown memory from one vault to another with conflict preservation')
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
const config = await loadBrainlinkConfig();
|
|
62
|
+
const sourceVault = assertVaultAllowed(options.from ?? config.vault, config.allowedVaults);
|
|
63
|
+
const targetVault = assertVaultAllowed(options.to ?? defaultBrainlinkConfig.vault, config.allowedVaults);
|
|
64
|
+
const sourceRoot = await ensureVault(sourceVault);
|
|
65
|
+
const targetRoot = await ensureVault(targetVault);
|
|
66
|
+
const preview = await previewVaultMigration(sourceVault, targetVault);
|
|
67
|
+
const actions = await planVaultMigration(sourceRoot, targetRoot);
|
|
68
|
+
const reportEntries = actions.map((action) => ({
|
|
69
|
+
kind: action.kind,
|
|
70
|
+
sourcePath: action.sourcePath,
|
|
71
|
+
sourceRelativePath: relative(sourceRoot, action.sourcePath),
|
|
72
|
+
targetPath: action.targetPath,
|
|
73
|
+
targetRelativePath: relative(targetRoot, action.targetPath)
|
|
74
|
+
}));
|
|
75
|
+
const writeReport = async () => {
|
|
76
|
+
if (!options.report) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const reportPath = resolve(options.report);
|
|
80
|
+
await mkdir(dirname(reportPath), { recursive: true });
|
|
81
|
+
await writeFile(reportPath, `${JSON.stringify({ source: sourceVault, target: targetVault, summary: preview, entries: reportEntries }, null, 2)}\n`, 'utf8');
|
|
82
|
+
return reportPath;
|
|
83
|
+
};
|
|
84
|
+
if (options.dryRun) {
|
|
85
|
+
const reportPath = await writeReport();
|
|
86
|
+
print(options.json, { dryRun: true, ...preview, entries: reportEntries, ...(reportPath ? { reportPath } : {}) }, () => `Dry run migration ${preview.source} -> ${preview.target}: copy=${preview.copied}, conflicts=${preview.conflicted}, unchanged=${preview.unchanged}${reportPath ? ` report=${reportPath}` : ''}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const migration = await migrateVaultContent(sourceVault, targetVault);
|
|
90
|
+
const shouldIndex = options.index !== false && migration.copied + migration.conflicted > 0;
|
|
91
|
+
const index = shouldIndex ? await indexVault(targetVault) : undefined;
|
|
92
|
+
const reportPath = await writeReport();
|
|
93
|
+
print(options.json, { dryRun: false, ...migration, entries: reportEntries, ...(index ? { index } : {}), ...(reportPath ? { reportPath } : {}) }, () => {
|
|
94
|
+
const summary = `Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`;
|
|
95
|
+
const indexMessage = index
|
|
96
|
+
? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
|
|
97
|
+
: '';
|
|
98
|
+
const reportMessage = reportPath ? ` Report written to ${reportPath}.` : '';
|
|
99
|
+
return `${summary}${indexMessage}${reportMessage}`;
|
|
100
|
+
});
|
|
29
101
|
});
|
|
30
102
|
program
|
|
31
103
|
.command('add')
|
|
@@ -66,7 +138,13 @@ export const registerWriteCommands = (program) => {
|
|
|
66
138
|
.action(async (options) => {
|
|
67
139
|
const resolved = await resolveOptions(options);
|
|
68
140
|
const report = await doctorVault(resolved.vault);
|
|
69
|
-
print(options.json, report, () =>
|
|
141
|
+
print(options.json, report, () => {
|
|
142
|
+
const checks = report.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`).join('\n');
|
|
143
|
+
const recommendations = report.recommendations && report.recommendations.length > 0
|
|
144
|
+
? `\n\nRecommended next steps:\n${report.recommendations.map((step) => `- ${step}`).join('\n')}`
|
|
145
|
+
: '';
|
|
146
|
+
return `${checks}${recommendations}`;
|
|
147
|
+
});
|
|
70
148
|
process.exitCode = report.ok ? 0 : 1;
|
|
71
149
|
});
|
|
72
150
|
program
|
|
@@ -117,4 +195,95 @@ export const registerWriteCommands = (program) => {
|
|
|
117
195
|
});
|
|
118
196
|
print(options.json, { url: server.url, watch: Boolean(options.watch), readonly: true }, () => `Brainlink graph server running at ${server.url}`);
|
|
119
197
|
});
|
|
198
|
+
program
|
|
199
|
+
.command('quickstart')
|
|
200
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
201
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
202
|
+
.option('--query <query>', 'optional task query to return immediate grounded context')
|
|
203
|
+
.option('--mode <mode>', 'search mode for context (fts|semantic|hybrid)')
|
|
204
|
+
.option('--limit <limit>', 'maximum context sections')
|
|
205
|
+
.option('--tokens <tokens>', 'maximum context token budget')
|
|
206
|
+
.option('--no-install-agent', 'skip agent MCP/plugin installation and upgrade automation')
|
|
207
|
+
.option('--mcp-only', 'when installing agent integration, only configure MCP section')
|
|
208
|
+
.option('--plugin-path <path>', 'custom source path for Brainlink plugin files')
|
|
209
|
+
.option('--allowed-vaults <paths>', 'comma separated vault allowlist to inject in MCP env')
|
|
210
|
+
.option('--brainlink-home <path>', 'BRAINLINK_HOME value to inject in MCP env')
|
|
211
|
+
.option('--json', 'print machine-readable JSON')
|
|
212
|
+
.description('run plug-and-play setup for vault, MCP integration and bootstrap readiness')
|
|
213
|
+
.action(async (options) => {
|
|
214
|
+
const resolved = await resolveOptions(options);
|
|
215
|
+
const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
|
|
216
|
+
const tokens = parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens);
|
|
217
|
+
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
218
|
+
const index = await indexVault(resolved.vault);
|
|
219
|
+
const stats = await getStats(resolved.vault, resolved.agent);
|
|
220
|
+
const validation = await validateVault(resolved.vault, resolved.agent);
|
|
221
|
+
const doctor = await doctorVault(resolved.vault);
|
|
222
|
+
const session = await touchBootstrapSession(resolved.vault, resolved.agent);
|
|
223
|
+
const policy = await getBootstrapPolicy();
|
|
224
|
+
const bootstrapStatus = await getBootstrapSessionStatus(resolved.vault, resolved.agent);
|
|
225
|
+
const context = options.query
|
|
226
|
+
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode)
|
|
227
|
+
: null;
|
|
228
|
+
const agentIntegration = options.installAgent === false
|
|
229
|
+
? null
|
|
230
|
+
: await installAgentIntegration({
|
|
231
|
+
mcpOnly: options.mcpOnly,
|
|
232
|
+
pluginPath: options.pluginPath,
|
|
233
|
+
allowedVaults: options.allowedVaults,
|
|
234
|
+
brainlinkHome: options.brainlinkHome,
|
|
235
|
+
selfTest: true
|
|
236
|
+
});
|
|
237
|
+
const nextActions = stats.documentCount === 0
|
|
238
|
+
? [
|
|
239
|
+
{
|
|
240
|
+
priority: 'required',
|
|
241
|
+
command: `blink add "Architecture" --vault "${resolved.vault}" --content "Durable memory with [[Links]] and #tags."`,
|
|
242
|
+
reason: 'Seed your vault with at least one durable Markdown note.'
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
priority: 'required',
|
|
246
|
+
command: `blink index --vault "${resolved.vault}"`,
|
|
247
|
+
reason: 'Rebuild index after adding notes so retrieval can find new memory.'
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
: options.query
|
|
251
|
+
? [
|
|
252
|
+
{
|
|
253
|
+
priority: 'recommended',
|
|
254
|
+
command: `blink add "Task Update" --vault "${resolved.vault}" --agent "${resolved.agent ?? 'shared'}" --content "<durable memory>"`,
|
|
255
|
+
reason: 'Persist important findings as Markdown notes after using the returned context.'
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
: [
|
|
259
|
+
{
|
|
260
|
+
priority: 'recommended',
|
|
261
|
+
command: `blink context "<task>" --vault "${resolved.vault}" --agent "${resolved.agent ?? 'shared'}" --mode ${mode}`,
|
|
262
|
+
reason: 'Retrieve grounded context for each task before responding.'
|
|
263
|
+
}
|
|
264
|
+
];
|
|
265
|
+
print(options.json, {
|
|
266
|
+
vault: resolved.vault,
|
|
267
|
+
agent: resolved.agent ?? 'shared',
|
|
268
|
+
mode,
|
|
269
|
+
index,
|
|
270
|
+
stats,
|
|
271
|
+
validation,
|
|
272
|
+
doctor,
|
|
273
|
+
policy,
|
|
274
|
+
bootstrapStatus,
|
|
275
|
+
session,
|
|
276
|
+
context,
|
|
277
|
+
agentIntegration,
|
|
278
|
+
nextActions
|
|
279
|
+
}, () => [
|
|
280
|
+
`quickstart vault=${resolved.vault}`,
|
|
281
|
+
`agent=${resolved.agent ?? 'shared'}`,
|
|
282
|
+
`documents=${stats.documentCount}`,
|
|
283
|
+
`links=${stats.linkCount}`,
|
|
284
|
+
`bootstrapReady=${bootstrapStatus.ready}`,
|
|
285
|
+
...(agentIntegration?.selfTest ? [`agentIntegrationSelfTest=${agentIntegration.selfTest.ok}`] : []),
|
|
286
|
+
...(nextActions.length > 0 ? ['Next actions:', ...nextActions.map((step) => `- ${step.command}`)] : [])
|
|
287
|
+
].join('\n'));
|
|
288
|
+
});
|
|
120
289
|
};
|
package/dist/cli/main.js
CHANGED
|
@@ -3,6 +3,8 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { registerAgentCommands } from './commands/agent-commands.js';
|
|
7
|
+
import { registerConfigCommands } from './commands/config-commands.js';
|
|
6
8
|
import { registerReadCommands } from './commands/read-commands.js';
|
|
7
9
|
import { registerWriteCommands } from './commands/write-commands.js';
|
|
8
10
|
const readPackageVersion = () => {
|
|
@@ -21,6 +23,8 @@ program
|
|
|
21
23
|
.version(readPackageVersion());
|
|
22
24
|
registerWriteCommands(program);
|
|
23
25
|
registerReadCommands(program);
|
|
26
|
+
registerConfigCommands(program);
|
|
27
|
+
registerAgentCommands(program);
|
|
24
28
|
program.parseAsync().catch((error) => {
|
|
25
29
|
const message = error instanceof Error ? error.message : String(error);
|
|
26
30
|
console.error(message);
|
package/dist/cli/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
1
|
+
import { loadBrainlinkConfig, resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
|
|
2
2
|
import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
|
|
3
3
|
export const parsePositiveInteger = (value, fallback) => {
|
|
4
4
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -8,10 +8,13 @@ export const resolveOptions = async (options) => {
|
|
|
8
8
|
const config = await loadBrainlinkConfig();
|
|
9
9
|
const vault = options.vault ?? config.vault;
|
|
10
10
|
const allowedVault = assertVaultAllowed(vault, config.allowedVaults);
|
|
11
|
+
const agent = options.agent ?? config.defaultAgent;
|
|
12
|
+
const defaults = resolveAgentRuntimeDefaults(config, agent);
|
|
11
13
|
return {
|
|
12
14
|
config,
|
|
13
15
|
vault: allowedVault,
|
|
14
|
-
agent
|
|
16
|
+
agent,
|
|
17
|
+
defaults
|
|
15
18
|
};
|
|
16
19
|
};
|
|
17
20
|
export const print = (json, value, human) => {
|
|
@@ -58,7 +58,8 @@ const tokenize = (input) => input
|
|
|
58
58
|
.match(tokenPattern)
|
|
59
59
|
?.map(normalizeToken)
|
|
60
60
|
.filter((token) => token.length > 1 && !stopWords.has(token)) ?? [];
|
|
61
|
-
const
|
|
61
|
+
const getAliasesForToken = (token) => Object.hasOwn(aliases, token) ? aliases[token] ?? [] : [];
|
|
62
|
+
const expandTokens = (tokens) => tokens.flatMap((token) => [token, ...getAliasesForToken(token)]);
|
|
62
63
|
const hash = (value) => Array.from(value).reduce((state, char) => Math.imul(state ^ char.charCodeAt(0), 16777619), 2166136261) >>> 0;
|
|
63
64
|
const featureHash = (feature) => {
|
|
64
65
|
const value = hash(feature);
|
|
@@ -45,20 +45,17 @@ const countDegrees = (edges) => edges.reduce((degrees, edge) => {
|
|
|
45
45
|
? incrementDegreeBy(incrementDegreeBy(degrees, edge.source, weight), edge.target, weight)
|
|
46
46
|
: incrementDegreeBy(degrees, edge.source, weight);
|
|
47
47
|
}, new Map());
|
|
48
|
-
const uniqueIds = (ids) => Array.from(new Set(ids));
|
|
49
48
|
const createAdjacency = (nodes, edges) => {
|
|
50
49
|
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
51
|
-
const
|
|
52
|
-
|
|
50
|
+
const adjacency = new Map(nodes.map((node) => [node.id, new Set()]));
|
|
51
|
+
edges.forEach((edge) => {
|
|
53
52
|
if (!edge.target || !nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
|
|
54
|
-
return
|
|
53
|
+
return;
|
|
55
54
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
]);
|
|
61
|
-
}, emptyAdjacency);
|
|
55
|
+
adjacency.get(edge.source)?.add(edge.target);
|
|
56
|
+
adjacency.get(edge.target)?.add(edge.source);
|
|
57
|
+
});
|
|
58
|
+
return new Map(Array.from(adjacency.entries(), ([id, neighbors]) => [id, Array.from(neighbors)]));
|
|
62
59
|
};
|
|
63
60
|
const byTitle = (left, right) => left.title.localeCompare(right.title);
|
|
64
61
|
const byDegreeThenTitle = (degrees) => (left, right) => {
|
|
@@ -117,10 +114,19 @@ const assignSegments = (nodes, edges, degrees) => {
|
|
|
117
114
|
}
|
|
118
115
|
return new Map(nodes.map((node) => [node.id, assignments.get(node.id) ?? groupLabel(groupKey(node))]));
|
|
119
116
|
};
|
|
120
|
-
const groupNodesBySegment = (nodes, segments) =>
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
const groupNodesBySegment = (nodes, segments) => {
|
|
118
|
+
const groups = new Map();
|
|
119
|
+
nodes.forEach((node) => {
|
|
120
|
+
const segment = segments.get(node.id) ?? groupLabel(groupKey(node));
|
|
121
|
+
const bucket = groups.get(segment);
|
|
122
|
+
if (bucket) {
|
|
123
|
+
bucket.push(node);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
groups.set(segment, [node]);
|
|
127
|
+
});
|
|
128
|
+
return new Map(groups);
|
|
129
|
+
};
|
|
124
130
|
const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
|
|
125
131
|
const createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes], segmentIndex) => {
|
|
126
132
|
const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
|
package/dist/domain/markdown.js
CHANGED
|
@@ -77,11 +77,13 @@ export const extractWikiLinkReferences = (content) => visibleMarkdownLines(conte
|
|
|
77
77
|
}));
|
|
78
78
|
});
|
|
79
79
|
const priorityFromWeight = (weight) => weight >= 8 ? 'critical' : weight >= 4 ? 'high' : 'normal';
|
|
80
|
+
const normalizeAccumulatedWeight = (weight) => Math.max(1, Math.min(12, weight));
|
|
80
81
|
export const extractWikiLinkWeights = (content) => {
|
|
81
82
|
const weights = extractWikiLinkReferences(content).reduce((state, reference) => {
|
|
82
83
|
const titleKey = reference.title.toLowerCase();
|
|
83
84
|
const current = state.get(titleKey);
|
|
84
|
-
const
|
|
85
|
+
const rawWeight = (current?.weight ?? 0) + reference.weight;
|
|
86
|
+
const weight = normalizeAccumulatedWeight(rawWeight);
|
|
85
87
|
const explicitPriority = reference.priority
|
|
86
88
|
? maxPriority(current?.priority ?? reference.priority, reference.priority)
|
|
87
89
|
: current?.priority;
|
|
@@ -116,10 +118,38 @@ const normalizeChunkContent = (content) => content
|
|
|
116
118
|
.join('\n')
|
|
117
119
|
.replace(/\n{3,}/g, '\n\n')
|
|
118
120
|
.trim();
|
|
121
|
+
const splitLongParagraph = (paragraph, maxCharacters) => {
|
|
122
|
+
if (paragraph.length <= maxCharacters) {
|
|
123
|
+
return [paragraph];
|
|
124
|
+
}
|
|
125
|
+
const sentences = paragraph
|
|
126
|
+
.split(/(?<=[.!?])\s+/)
|
|
127
|
+
.map((sentence) => sentence.trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
if (sentences.length <= 1) {
|
|
130
|
+
const chunks = [];
|
|
131
|
+
for (let index = 0; index < paragraph.length; index += maxCharacters) {
|
|
132
|
+
chunks.push(paragraph.slice(index, index + maxCharacters).trim());
|
|
133
|
+
}
|
|
134
|
+
return chunks.filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
return sentences.reduce((state, sentence) => {
|
|
137
|
+
const last = state.at(-1);
|
|
138
|
+
if (!last) {
|
|
139
|
+
return [sentence];
|
|
140
|
+
}
|
|
141
|
+
const merged = `${last} ${sentence}`;
|
|
142
|
+
if (merged.length <= maxCharacters) {
|
|
143
|
+
return [...state.slice(0, -1), merged];
|
|
144
|
+
}
|
|
145
|
+
return [...state, sentence];
|
|
146
|
+
}, []);
|
|
147
|
+
};
|
|
119
148
|
export const splitIntoChunks = (documentId, content, maxCharacters = 1200) => {
|
|
120
149
|
const paragraphs = normalizeChunkContent(stripFrontmatter(content))
|
|
121
150
|
.split(/\n{2,}/)
|
|
122
|
-
.filter(Boolean)
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.flatMap((paragraph) => splitLongParagraph(paragraph, maxCharacters));
|
|
123
153
|
const chunks = paragraphs.reduce((state, paragraph) => {
|
|
124
154
|
const lastChunk = state.at(-1);
|
|
125
155
|
if (!lastChunk) {
|
|
@@ -162,13 +192,15 @@ export const parseMarkdownDocument = (input) => {
|
|
|
162
192
|
export const createIndexedDocument = (document, titleToDocumentId, maxChunkCharacters = 1200) => {
|
|
163
193
|
const chunks = splitIntoChunks(document.id, document.content, maxChunkCharacters);
|
|
164
194
|
const linkWeights = new Map(extractWikiLinkWeights(document.content).map((link) => [link.title.toLowerCase(), link]));
|
|
165
|
-
const links = document.links
|
|
195
|
+
const links = document.links
|
|
196
|
+
.map((toTitle) => ({
|
|
166
197
|
fromDocumentId: document.id,
|
|
167
198
|
toTitle,
|
|
168
199
|
toDocumentId: titleToDocumentId.get(toTitle.toLowerCase()) ?? null,
|
|
169
200
|
weight: linkWeights.get(toTitle.toLowerCase())?.weight ?? 1,
|
|
170
201
|
priority: linkWeights.get(toTitle.toLowerCase())?.priority ?? 'normal'
|
|
171
|
-
}))
|
|
202
|
+
}))
|
|
203
|
+
.filter((link) => link.toDocumentId !== document.id);
|
|
172
204
|
return {
|
|
173
205
|
document,
|
|
174
206
|
chunks,
|