@andespindola/brainlink 1.0.5 → 1.0.6
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 +8 -0
- package/dist/application/add-note.js +2 -2
- package/dist/application/build-context.js +16 -10
- package/dist/application/canonical-context-links.js +44 -5
- package/dist/application/check-package-update.js +105 -0
- package/dist/application/frontend/client/chunk-fetch.js +236 -0
- package/dist/application/frontend/client/controls.js +178 -0
- package/dist/application/frontend/client/elements.js +122 -0
- package/dist/application/frontend/client/input.js +202 -0
- package/dist/application/frontend/client/node-details.js +191 -0
- package/dist/application/frontend/client/rendering.js +296 -0
- package/dist/application/frontend/client/scope-theme.js +114 -0
- package/dist/application/frontend/client/spatial.js +98 -0
- package/dist/application/frontend/client/storage.js +215 -0
- package/dist/application/frontend/client/upload.js +90 -0
- package/dist/application/frontend/client/worker-bootstrap.js +147 -0
- package/dist/application/frontend/client-js.js +24 -1837
- package/dist/application/frontend/client-render-worker-js.js +1 -1
- package/dist/application/index-vault-phases.js +189 -0
- package/dist/application/index-vault.js +44 -165
- package/dist/cli/commands/write/dedupe-commands.js +59 -0
- package/dist/cli/commands/write/index-commands.js +205 -0
- package/dist/cli/commands/write/link-commands.js +68 -0
- package/dist/cli/commands/write/note-commands.js +146 -0
- package/dist/cli/commands/write/server-commands.js +553 -0
- package/dist/cli/commands/write/shared.js +35 -0
- package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
- package/dist/cli/commands/write-commands.js +12 -1303
- package/dist/cli/main.js +39 -3
- package/dist/domain/context.js +39 -3
- package/dist/domain/embeddings.js +31 -5
- package/dist/domain/graph-contexts.js +62 -57
- package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
- package/dist/domain/graph-layout/collisions.js +100 -0
- package/dist/domain/graph-layout/hierarchy.js +135 -0
- package/dist/domain/graph-layout/metrics.js +111 -0
- package/dist/domain/graph-layout/segments.js +76 -0
- package/dist/domain/graph-layout/star-layout.js +110 -0
- package/dist/domain/graph-layout.js +4 -625
- package/dist/infrastructure/config.js +6 -0
- package/dist/infrastructure/file-index.js +13 -4
- package/dist/infrastructure/semantic-prefilter.js +24 -0
- package/dist/mcp/server.js +7 -0
- package/dist/mcp/tool-guard.js +29 -0
- package/dist/mcp/tools/maintenance-tools.js +409 -0
- package/dist/mcp/tools/read-tools.js +504 -0
- package/dist/mcp/tools/shared.js +216 -0
- package/dist/mcp/tools/write-tools.js +247 -0
- package/dist/mcp/tools.js +3 -1357
- package/docs/QUICKSTART.md +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { indexVault, indexVaultWithOptions } from '../../../application/index-vault.js';
|
|
4
|
+
import { startVaultWatcher } from '../../../application/watch-vault.js';
|
|
5
|
+
import { loadBrainlinkConfig } from '../../../infrastructure/config.js';
|
|
6
|
+
import { parsePositiveInteger, print, resolveOptions } from '../../runtime.js';
|
|
7
|
+
import { formatBytes } from './shared.js';
|
|
8
|
+
const formatMs = (value) => Number.isFinite(value) && value != null ? `${value.toFixed(value >= 100 ? 0 : 1)}ms` : 'n/a';
|
|
9
|
+
const benchEventLabel = (event) => `${event.phase}:${event.status}`;
|
|
10
|
+
const printBenchRealtimeEvent = (json, event) => {
|
|
11
|
+
print(json, {
|
|
12
|
+
event: 'bench-progress',
|
|
13
|
+
...event
|
|
14
|
+
}, () => `[bench] ${benchEventLabel(event)} ${event.message} (${formatMs(event.elapsedMs)})`);
|
|
15
|
+
};
|
|
16
|
+
const printBenchSummary = (json, trigger, vault, result) => {
|
|
17
|
+
print(json, {
|
|
18
|
+
event: 'bench-result',
|
|
19
|
+
trigger,
|
|
20
|
+
vault,
|
|
21
|
+
result
|
|
22
|
+
}, () => {
|
|
23
|
+
const packs = result.packs;
|
|
24
|
+
const compression = packs?.compression;
|
|
25
|
+
const savedPercent = compression && compression.inputBytes > 0
|
|
26
|
+
? `${((1 - compression.ratio) * 100).toFixed(1)}%`
|
|
27
|
+
: 'n/a';
|
|
28
|
+
return [
|
|
29
|
+
`[bench] trigger=${trigger}`,
|
|
30
|
+
`documents=${result.documentCount} chunks=${result.chunkCount} links=${result.linkCount}`,
|
|
31
|
+
`changedDocuments=${result.changedDocumentCount ?? 0} totalElapsed=${formatMs(result.elapsedMs)}`,
|
|
32
|
+
`packsRebuilt=${packs?.rebuilt ? 'yes' : 'no'} reason=${packs?.reason ?? 'n/a'}`,
|
|
33
|
+
packs?.rebuilt
|
|
34
|
+
? `packCount=${packs.packCount ?? 0} packDuration=${formatMs(packs.durationMs)} input=${formatBytes(compression?.inputBytes)} output=${formatBytes(compression?.outputBytes)} saved=${savedPercent}`
|
|
35
|
+
: 'packCompression=n/a'
|
|
36
|
+
].join('\n');
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const benchHistoryPath = (vaultPath) => join(vaultPath, '.brainlink', 'benchmarks', 'latest.json');
|
|
40
|
+
const readBenchHistory = async (vaultPath) => {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(await readFile(benchHistoryPath(vaultPath), 'utf8'));
|
|
43
|
+
if (typeof parsed.elapsedMs !== 'number' || typeof parsed.timestamp !== 'string') {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
elapsedMs: parsed.elapsedMs,
|
|
48
|
+
timestamp: parsed.timestamp,
|
|
49
|
+
...(typeof parsed.compressionRatio === 'number' ? { compressionRatio: parsed.compressionRatio } : {})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const writeBenchHistory = async (vaultPath, result) => {
|
|
57
|
+
await mkdir(dirname(benchHistoryPath(vaultPath)), { recursive: true });
|
|
58
|
+
const payload = {
|
|
59
|
+
elapsedMs: result.elapsedMs ?? 0,
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
...(typeof result.packs?.compression?.ratio === 'number' ? { compressionRatio: result.packs.compression.ratio } : {})
|
|
62
|
+
};
|
|
63
|
+
await writeFile(benchHistoryPath(vaultPath), `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
64
|
+
};
|
|
65
|
+
const evaluateBenchGuardrails = (config, result, baseline) => {
|
|
66
|
+
const compressionRatio = result.packs?.compression?.ratio;
|
|
67
|
+
const compressionSavingsPercent = typeof compressionRatio === 'number' ? Math.max(0, (1 - compressionRatio) * 100) : undefined;
|
|
68
|
+
const compressionPass = compressionSavingsPercent != null
|
|
69
|
+
? compressionSavingsPercent >= config.searchPack.guardrailMinSavingsPercent
|
|
70
|
+
: undefined;
|
|
71
|
+
const latencyRegressionPercent = baseline && baseline.elapsedMs > 0 && typeof result.elapsedMs === 'number'
|
|
72
|
+
? ((result.elapsedMs - baseline.elapsedMs) / baseline.elapsedMs) * 100
|
|
73
|
+
: undefined;
|
|
74
|
+
const latencyPass = latencyRegressionPercent != null
|
|
75
|
+
? latencyRegressionPercent <= config.searchPack.guardrailMaxLatencyRegressionPercent
|
|
76
|
+
: undefined;
|
|
77
|
+
return {
|
|
78
|
+
...(compressionSavingsPercent != null ? { compressionSavingsPercent } : {}),
|
|
79
|
+
...(compressionPass != null ? { compressionPass } : {}),
|
|
80
|
+
...(latencyRegressionPercent != null ? { latencyRegressionPercent } : {}),
|
|
81
|
+
...(latencyPass != null ? { latencyPass } : {})
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
const printBenchGuardrails = (json, vault, config, guardrails) => {
|
|
85
|
+
print(json, {
|
|
86
|
+
event: 'bench-guardrails',
|
|
87
|
+
vault,
|
|
88
|
+
thresholds: {
|
|
89
|
+
minSavingsPercent: config.searchPack.guardrailMinSavingsPercent,
|
|
90
|
+
maxLatencyRegressionPercent: config.searchPack.guardrailMaxLatencyRegressionPercent
|
|
91
|
+
},
|
|
92
|
+
guardrails
|
|
93
|
+
}, () => {
|
|
94
|
+
const savings = guardrails.compressionSavingsPercent;
|
|
95
|
+
const latency = guardrails.latencyRegressionPercent;
|
|
96
|
+
return [
|
|
97
|
+
'[bench] guardrails',
|
|
98
|
+
`minSavings=${config.searchPack.guardrailMinSavingsPercent.toFixed(1)}% maxLatencyRegression=${config.searchPack.guardrailMaxLatencyRegressionPercent.toFixed(1)}%`,
|
|
99
|
+
`compressionSavings=${savings != null ? `${savings.toFixed(2)}%` : 'n/a'} pass=${guardrails.compressionPass != null ? (guardrails.compressionPass ? 'yes' : 'no') : 'n/a'}`,
|
|
100
|
+
`latencyRegression=${latency != null ? `${latency.toFixed(2)}%` : 'n/a'} pass=${guardrails.latencyPass != null ? (guardrails.latencyPass ? 'yes' : 'no') : 'n/a'}`
|
|
101
|
+
].join('\n');
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
export const registerIndexCommands = (program) => {
|
|
105
|
+
program
|
|
106
|
+
.command('index')
|
|
107
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
108
|
+
.option('--full', 'force a complete reindex from Markdown source without reusing unchanged index entries')
|
|
109
|
+
.option('--json', 'print machine-readable JSON')
|
|
110
|
+
.description('index markdown notes, links, tags and chunks')
|
|
111
|
+
.action(async (options) => {
|
|
112
|
+
const resolved = await resolveOptions(options);
|
|
113
|
+
const result = await indexVault(resolved.vault, {
|
|
114
|
+
full: options.full === true
|
|
115
|
+
});
|
|
116
|
+
print(options.json, result, () => options.full === true
|
|
117
|
+
? `Fully reindexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`
|
|
118
|
+
: `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
|
|
119
|
+
});
|
|
120
|
+
program
|
|
121
|
+
.command('bench')
|
|
122
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
123
|
+
.option('-w, --watch', 'watch markdown changes and re-run benchmark in realtime')
|
|
124
|
+
.option('--debounce <ms>', 'watch debounce in milliseconds', '350')
|
|
125
|
+
.option('--json', 'print machine-readable JSON events')
|
|
126
|
+
.description('benchmark indexing in realtime, including compressed pack behavior')
|
|
127
|
+
.action(async (options) => {
|
|
128
|
+
const resolved = await resolveOptions(options);
|
|
129
|
+
const config = await loadBrainlinkConfig();
|
|
130
|
+
const emitProgress = (event) => {
|
|
131
|
+
printBenchRealtimeEvent(options.json, event);
|
|
132
|
+
};
|
|
133
|
+
const printBenchError = (error) => {
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
print(options.json, { event: 'bench-error', message }, () => `[bench] error ${message}`);
|
|
136
|
+
};
|
|
137
|
+
const runAndPrint = async (trigger) => {
|
|
138
|
+
const baseline = await readBenchHistory(resolved.vault);
|
|
139
|
+
const result = await indexVaultWithOptions(resolved.vault, {
|
|
140
|
+
onProgress: emitProgress
|
|
141
|
+
});
|
|
142
|
+
printBenchSummary(options.json, trigger, resolved.vault, result);
|
|
143
|
+
const guardrails = evaluateBenchGuardrails(config, result, baseline);
|
|
144
|
+
printBenchGuardrails(options.json, resolved.vault, config, guardrails);
|
|
145
|
+
await writeBenchHistory(resolved.vault, result);
|
|
146
|
+
return result;
|
|
147
|
+
};
|
|
148
|
+
if (!options.watch) {
|
|
149
|
+
await runAndPrint('manual');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const debounceMs = parsePositiveInteger(options.debounce ?? '350', 350);
|
|
153
|
+
await runAndPrint('manual');
|
|
154
|
+
print(options.json, {
|
|
155
|
+
event: 'bench-watching',
|
|
156
|
+
vault: resolved.vault,
|
|
157
|
+
debounceMs
|
|
158
|
+
}, () => `[bench] watching ${resolved.vault} (debounce=${debounceMs}ms)`);
|
|
159
|
+
const watcher = startVaultWatcher({
|
|
160
|
+
vaultPath: resolved.vault,
|
|
161
|
+
debounceMs,
|
|
162
|
+
onProgress: emitProgress,
|
|
163
|
+
onIndex: (result) => {
|
|
164
|
+
printBenchSummary(options.json, 'watch', resolved.vault, result);
|
|
165
|
+
},
|
|
166
|
+
onError: printBenchError
|
|
167
|
+
});
|
|
168
|
+
await new Promise((resolveSignal) => {
|
|
169
|
+
const shutdown = () => {
|
|
170
|
+
watcher.close();
|
|
171
|
+
resolveSignal();
|
|
172
|
+
};
|
|
173
|
+
process.once('SIGINT', shutdown);
|
|
174
|
+
process.once('SIGTERM', shutdown);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
program
|
|
178
|
+
.command('watch')
|
|
179
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
180
|
+
.option('--json', 'print machine-readable JSON events')
|
|
181
|
+
.description('watch markdown files and reindex on changes')
|
|
182
|
+
.action(async (options) => {
|
|
183
|
+
const resolved = await resolveOptions(options);
|
|
184
|
+
const initial = await indexVault(resolved.vault);
|
|
185
|
+
const watcher = startVaultWatcher({
|
|
186
|
+
vaultPath: resolved.vault,
|
|
187
|
+
onIndex: (result) => {
|
|
188
|
+
print(options.json, { event: 'indexed', result }, () => `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
|
|
189
|
+
},
|
|
190
|
+
onError: (error) => {
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
print(options.json, { event: 'error', message }, () => message);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
print(options.json, { event: 'watching', vault: resolved.vault, initial }, () => `Watching ${resolved.vault}`);
|
|
196
|
+
process.once('SIGINT', () => {
|
|
197
|
+
watcher.close();
|
|
198
|
+
process.exit(0);
|
|
199
|
+
});
|
|
200
|
+
process.once('SIGTERM', () => {
|
|
201
|
+
watcher.close();
|
|
202
|
+
process.exit(0);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { indexVault } from '../../../application/index-vault.js';
|
|
2
|
+
import { migrateContextLinks } from '../../../application/migrate-context-links.js';
|
|
3
|
+
import { canonicalizeContextLinks } from '../../../application/canonical-context-links.js';
|
|
4
|
+
import { parsePositiveInteger, print, resolveOptions } from '../../runtime.js';
|
|
5
|
+
export const registerLinkCommands = (program) => {
|
|
6
|
+
program
|
|
7
|
+
.command('migrate-context-links')
|
|
8
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
9
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
10
|
+
.option('-l, --limit <limit>', 'maximum context links to add per note', '5')
|
|
11
|
+
.option('--dry-run', 'preview context-link migration without writing files')
|
|
12
|
+
.option('--no-index', 'skip reindexing after migration')
|
|
13
|
+
.option('--json', 'print machine-readable JSON')
|
|
14
|
+
.description('add concise Context Links sections from existing wiki-link mentions')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
const resolved = await resolveOptions(options);
|
|
17
|
+
const result = await migrateContextLinks(resolved.vault, {
|
|
18
|
+
dryRun: options.dryRun === true,
|
|
19
|
+
limit: parsePositiveInteger(options.limit ?? '5', 5),
|
|
20
|
+
agentId: resolved.agent
|
|
21
|
+
});
|
|
22
|
+
const shouldIndex = options.index !== false && !result.dryRun && result.changed > 0;
|
|
23
|
+
const index = shouldIndex ? await indexVault(resolved.vault, { full: true }) : undefined;
|
|
24
|
+
print(options.json, {
|
|
25
|
+
vault: resolved.vault,
|
|
26
|
+
agent: resolved.agent ?? 'shared',
|
|
27
|
+
...result,
|
|
28
|
+
...(index ? { index } : {})
|
|
29
|
+
}, () => {
|
|
30
|
+
const mode = result.dryRun ? 'Previewed' : 'Migrated';
|
|
31
|
+
const indexMessage = index
|
|
32
|
+
? ` Fully reindexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} context links.`
|
|
33
|
+
: '';
|
|
34
|
+
return `${mode} ${result.scanned} notes: changed=${result.changed}, skipped=${result.skipped}, limit=${result.limit}.${indexMessage}`;
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command('canonicalize-context-links')
|
|
39
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
40
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
41
|
+
.option('--dry-run', 'preview canonical context links without writing files')
|
|
42
|
+
.option('--no-create-hubs', 'do not create missing context hub notes')
|
|
43
|
+
.option('--no-index', 'skip reindexing after canonicalization')
|
|
44
|
+
.option('--json', 'print machine-readable JSON')
|
|
45
|
+
.description('ensure notes have canonical Context Links to their inferred context hubs')
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
const resolved = await resolveOptions(options);
|
|
48
|
+
const result = await canonicalizeContextLinks(resolved.vault, {
|
|
49
|
+
dryRun: options.dryRun === true,
|
|
50
|
+
agentId: resolved.agent,
|
|
51
|
+
createMissingHubs: options.createHubs !== false
|
|
52
|
+
});
|
|
53
|
+
const shouldIndex = options.index !== false && !result.dryRun && result.changed > 0;
|
|
54
|
+
const index = shouldIndex ? await indexVault(resolved.vault, { full: true }) : undefined;
|
|
55
|
+
print(options.json, {
|
|
56
|
+
vault: resolved.vault,
|
|
57
|
+
agent: resolved.agent ?? 'shared',
|
|
58
|
+
...result,
|
|
59
|
+
...(index ? { index } : {})
|
|
60
|
+
}, () => {
|
|
61
|
+
const mode = result.dryRun ? 'Previewed' : 'Canonicalized';
|
|
62
|
+
const indexMessage = index
|
|
63
|
+
? ` Fully reindexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} context links.`
|
|
64
|
+
: '';
|
|
65
|
+
return `${mode} ${result.scanned} notes: changed=${result.changed}, createdHubs=${result.createdHubs}, skipped=${result.skipped}.${indexMessage}`;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { relative, resolve } from 'node:path';
|
|
2
|
+
import { addNoteWithMetadata } from '../../../application/add-note.js';
|
|
3
|
+
import { deleteNote } from '../../../application/delete-note.js';
|
|
4
|
+
import { scanDuplicateNotes } from '../../../application/dedupe-notes.js';
|
|
5
|
+
import { importFile } from '../../../application/import-file.js';
|
|
6
|
+
import { indexVault } from '../../../application/index-vault.js';
|
|
7
|
+
import { ensureVault } from '../../../infrastructure/file-system-vault.js';
|
|
8
|
+
import { addVolatileMemory, clearVolatileMemory } from '../../../infrastructure/volatile-memory.js';
|
|
9
|
+
import { parsePositiveInteger, print, resolveOptions } from '../../runtime.js';
|
|
10
|
+
import { resolveAddContent } from './shared.js';
|
|
11
|
+
export const registerNoteCommands = (program) => {
|
|
12
|
+
program
|
|
13
|
+
.command('volatile')
|
|
14
|
+
.option('-c, --content <content>', 'temporary memory content to add')
|
|
15
|
+
.option('--ttl <minutes>', 'time-to-live in minutes', '240')
|
|
16
|
+
.option('--tag <tag...>', 'volatile memory tag')
|
|
17
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
18
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
19
|
+
.option('--clear', 'clear volatile memory for the current agent namespace')
|
|
20
|
+
.option('--json', 'print machine-readable JSON')
|
|
21
|
+
.description('add or clear temporary agent-decided memory')
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
const resolved = await resolveOptions(options);
|
|
24
|
+
if (options.clear) {
|
|
25
|
+
const cleared = await clearVolatileMemory(resolved.vault, resolved.agent);
|
|
26
|
+
print(options.json, { cleared, agent: resolved.agent ?? 'shared' }, () => `Cleared ${cleared} volatile memories.`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!options.content || options.content.trim().length === 0) {
|
|
30
|
+
throw new Error('Use --content to add volatile memory, or --clear to remove it.');
|
|
31
|
+
}
|
|
32
|
+
const entry = await addVolatileMemory(resolved.vault, options.content, resolved.agent ?? 'shared', parsePositiveInteger(options.ttl ?? '240', 240), options.tag ?? []);
|
|
33
|
+
print(options.json, { entry }, () => `Stored volatile memory until ${entry.expiresAt}.`);
|
|
34
|
+
});
|
|
35
|
+
program
|
|
36
|
+
.command('add')
|
|
37
|
+
.argument('<title>', 'note title')
|
|
38
|
+
.option('-c, --content <content>', 'markdown content')
|
|
39
|
+
.option('-f, --content-file <contentFile>', 'read markdown content from a file')
|
|
40
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
41
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
42
|
+
.option('--allow-sensitive', 'allow writing content that looks like a secret')
|
|
43
|
+
.option('--no-auto-context-links', 'skip canonical Context Links for this note')
|
|
44
|
+
.option('--no-auto-index', 'skip reindexing after add')
|
|
45
|
+
.option('--json', 'print machine-readable JSON')
|
|
46
|
+
.description('add a markdown note to the vault')
|
|
47
|
+
.action(async (title, options) => {
|
|
48
|
+
const resolved = await resolveOptions(options);
|
|
49
|
+
const content = resolveAddContent(options);
|
|
50
|
+
const added = await addNoteWithMetadata(resolved.vault, title, content, resolved.agent, {
|
|
51
|
+
allowSensitive: Boolean(options.allowSensitive),
|
|
52
|
+
autoContextLinks: options.autoContextLinks !== false && resolved.config.autoCanonicalContextLinks
|
|
53
|
+
});
|
|
54
|
+
const shouldAutoIndex = options.autoIndex !== false && resolved.config.autoIndexOnWrite;
|
|
55
|
+
const index = shouldAutoIndex ? await indexVault(resolved.vault) : undefined;
|
|
56
|
+
const absoluteVaultPath = await ensureVault(resolved.vault);
|
|
57
|
+
const focusPath = added.path.startsWith(absoluteVaultPath)
|
|
58
|
+
? relative(absoluteVaultPath, added.path).replaceAll('\\', '/')
|
|
59
|
+
: added.path.includes('agents/')
|
|
60
|
+
? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/')
|
|
61
|
+
: undefined;
|
|
62
|
+
const possibleDuplicates = await scanDuplicateNotes(resolved.vault, {
|
|
63
|
+
agentId: resolved.agent,
|
|
64
|
+
focusPath,
|
|
65
|
+
limit: 5,
|
|
66
|
+
minSemanticScore: 0.92,
|
|
67
|
+
includeSemantic: true
|
|
68
|
+
});
|
|
69
|
+
print(options.json, {
|
|
70
|
+
title,
|
|
71
|
+
agent: resolved.agent ?? 'shared',
|
|
72
|
+
path: added.path,
|
|
73
|
+
writeConnectivity: {
|
|
74
|
+
autoLinked: added.autoLinked,
|
|
75
|
+
linkTarget: added.linkTarget,
|
|
76
|
+
context: added.context,
|
|
77
|
+
hubCreated: added.hubCreated,
|
|
78
|
+
guaranteedEdge: added.autoLinked
|
|
79
|
+
},
|
|
80
|
+
possibleDuplicates,
|
|
81
|
+
...(index ? { index } : {})
|
|
82
|
+
}, () => {
|
|
83
|
+
const duplicateMessage = possibleDuplicates.length > 0
|
|
84
|
+
? `\nPotential duplicates: ${possibleDuplicates.length}. Use "blink dedupe --json" or "blink dedupe-resolve".`
|
|
85
|
+
: '';
|
|
86
|
+
const linkMessage = added.autoLinked ? ` Linked to [[${added.linkTarget}]].` : '';
|
|
87
|
+
return `Created note at ${added.path}.${linkMessage}${duplicateMessage}`;
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
program
|
|
91
|
+
.command('import-file')
|
|
92
|
+
.argument('<file>', 'file to convert and import into the vault')
|
|
93
|
+
.option('-t, --title <title>', 'note title; defaults to the source filename')
|
|
94
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
95
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
96
|
+
.option('--allow-sensitive', 'allow writing converted content that looks like a secret')
|
|
97
|
+
.option('--no-auto-context-links', 'skip canonical Context Links for this imported note')
|
|
98
|
+
.option('--no-auto-index', 'skip reindexing after import')
|
|
99
|
+
.option('--json', 'print machine-readable JSON')
|
|
100
|
+
.description('convert a document with Docling and import it as a Markdown note')
|
|
101
|
+
.action(async (file, options) => {
|
|
102
|
+
const resolved = await resolveOptions(options);
|
|
103
|
+
const result = await importFile({
|
|
104
|
+
vaultPath: resolved.vault,
|
|
105
|
+
filePath: resolve(file),
|
|
106
|
+
title: options.title,
|
|
107
|
+
agentId: resolved.agent,
|
|
108
|
+
allowSensitive: Boolean(options.allowSensitive),
|
|
109
|
+
autoContextLinks: options.autoContextLinks !== false && resolved.config.autoCanonicalContextLinks,
|
|
110
|
+
autoIndex: options.autoIndex !== false && resolved.config.autoIndexOnWrite
|
|
111
|
+
});
|
|
112
|
+
print(options.json, {
|
|
113
|
+
vault: resolved.vault,
|
|
114
|
+
agent: resolved.agent ?? 'shared',
|
|
115
|
+
...result
|
|
116
|
+
}, () => {
|
|
117
|
+
const linkMessage = result.writeConnectivity.autoLinked ? ` Linked to [[${result.writeConnectivity.linkTarget}]].` : '';
|
|
118
|
+
const indexMessage = result.index ? ` Indexed ${result.index.documentCount} documents.` : '';
|
|
119
|
+
return `Imported ${result.sourceName} as "${result.title}" at ${result.path}.${linkMessage}${indexMessage}`;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
program
|
|
123
|
+
.command('delete-note')
|
|
124
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
125
|
+
.option('-a, --agent <agent>', 'agent memory namespace when deleting by title')
|
|
126
|
+
.option('--title <title>', 'note title to delete')
|
|
127
|
+
.option('--path <path>', 'vault-relative or absolute markdown note path to delete')
|
|
128
|
+
.option('--yes', 'confirm note deletion')
|
|
129
|
+
.option('--no-auto-index', 'skip reindexing after delete')
|
|
130
|
+
.option('--json', 'print machine-readable JSON')
|
|
131
|
+
.description('delete a Markdown note from the vault after explicit confirmation')
|
|
132
|
+
.action(async (options) => {
|
|
133
|
+
const resolved = await resolveOptions(options);
|
|
134
|
+
const result = await deleteNote(resolved.vault, {
|
|
135
|
+
title: options.title,
|
|
136
|
+
path: options.path,
|
|
137
|
+
agentId: resolved.agent,
|
|
138
|
+
confirm: Boolean(options.yes),
|
|
139
|
+
autoIndex: options.autoIndex !== false
|
|
140
|
+
});
|
|
141
|
+
print(options.json, {
|
|
142
|
+
vault: resolved.vault,
|
|
143
|
+
...result
|
|
144
|
+
}, () => `Deleted note ${result.relativePath}.`);
|
|
145
|
+
});
|
|
146
|
+
};
|