@andespindola/brainlink 0.1.0-alpha.10 → 0.1.0-alpha.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/README.md +37 -0
- package/SECURITY.md +12 -0
- package/dist/application/watch-vault.js +4 -1
- package/dist/infrastructure/bucket-vault.js +171 -0
- package/dist/infrastructure/file-system-vault.js +21 -3
- package/docs/AGENT_USAGE.md +2 -1
- package/docs/ARCHITECTURE.md +5 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ Markdown is the source of truth. `.brainlink/brainlink.db` is only a rebuildable
|
|
|
66
66
|
- Full-text, semantic and hybrid retrieval modes.
|
|
67
67
|
- SQLite-backed semantic candidate buckets for larger vaults.
|
|
68
68
|
- Agent namespaces under `agents/<agent-id>/`.
|
|
69
|
+
- S3-compatible bucket vaults through `s3://bucket/prefix` URIs.
|
|
69
70
|
- CLI with machine-readable `--json` output.
|
|
70
71
|
- Short CLI alias: `blink`.
|
|
71
72
|
- Built-in MCP stdio server for agent tool integration.
|
|
@@ -256,6 +257,36 @@ http://127.0.0.1:4321
|
|
|
256
257
|
|
|
257
258
|
When `--vault` is omitted, commands use the default vault at `$HOME/.brainlink/vault`. Pass `--vault` or configure `vault` in `brainlink.config.json` when you want a custom project-local vault.
|
|
258
259
|
|
|
260
|
+
## Bucket Vaults
|
|
261
|
+
|
|
262
|
+
Brainlink can use an S3-compatible bucket as the Markdown source of truth:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
export AWS_REGION="us-east-1"
|
|
266
|
+
export AWS_ACCESS_KEY_ID="..."
|
|
267
|
+
export AWS_SECRET_ACCESS_KEY="..."
|
|
268
|
+
|
|
269
|
+
blink add "Architecture" \
|
|
270
|
+
--vault "s3://my-memory-bucket/brainlink" \
|
|
271
|
+
--content "Bucket Markdown is the source of truth. #architecture"
|
|
272
|
+
|
|
273
|
+
blink index --vault "s3://my-memory-bucket/brainlink"
|
|
274
|
+
blink context "architecture" --vault "s3://my-memory-bucket/brainlink"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
For Cloudflare R2, MinIO or another S3-compatible endpoint:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
export BRAINLINK_S3_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"
|
|
281
|
+
export BRAINLINK_S3_FORCE_PATH_STYLE=1
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Bucket vaults mirror Markdown into a local cache under
|
|
285
|
+
`$BRAINLINK_HOME/bucket-cache`. The bucket remains canonical; the local
|
|
286
|
+
`.brainlink/brainlink.db` stays a disposable index. Run `index` after remote
|
|
287
|
+
bucket changes before relying on `search`, `context`, graph or validation
|
|
288
|
+
commands. Watch mode is only supported for local filesystem vaults.
|
|
289
|
+
|
|
259
290
|
## Core Model
|
|
260
291
|
|
|
261
292
|
```txt
|
|
@@ -612,6 +643,12 @@ Set `BRAINLINK_ALLOWED_VAULTS` for external wrappers, including MCP servers, so
|
|
|
612
643
|
export BRAINLINK_ALLOWED_VAULTS="/absolute/path/to/project-vault,/absolute/path/to/team-vault"
|
|
613
644
|
```
|
|
614
645
|
|
|
646
|
+
Bucket vaults can be allowlisted with the same variable:
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
export BRAINLINK_ALLOWED_VAULTS="s3://my-memory-bucket/brainlink"
|
|
650
|
+
```
|
|
651
|
+
|
|
615
652
|
## Note Format
|
|
616
653
|
|
|
617
654
|
Brainlink supports Markdown with optional frontmatter:
|
package/SECURITY.md
CHANGED
|
@@ -32,4 +32,16 @@ External tool wrappers, including MCP servers, should set `BRAINLINK_ALLOWED_VAU
|
|
|
32
32
|
export BRAINLINK_ALLOWED_VAULTS="/absolute/path/to/project-vault"
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
For bucket vaults, allowlist the S3 URI prefix:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export BRAINLINK_ALLOWED_VAULTS="s3://my-memory-bucket/brainlink"
|
|
39
|
+
```
|
|
40
|
+
|
|
35
41
|
When the allowlist is set, CLI commands fail if `--vault` points outside the allowed roots.
|
|
42
|
+
|
|
43
|
+
## Bucket Credentials
|
|
44
|
+
|
|
45
|
+
Bucket vaults use the standard AWS SDK credential chain. Prefer short-lived,
|
|
46
|
+
least-privilege credentials scoped to the specific bucket prefix used by
|
|
47
|
+
Brainlink. Do not store bucket credentials in Markdown notes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { watch } from 'node:fs';
|
|
2
2
|
import { indexVault } from './index-vault.js';
|
|
3
|
-
import { resolveVaultPath } from '../infrastructure/file-system-vault.js';
|
|
3
|
+
import { isBucketVaultPath, resolveVaultPath } from '../infrastructure/file-system-vault.js';
|
|
4
4
|
const shouldIgnore = (filename) => {
|
|
5
5
|
if (!filename) {
|
|
6
6
|
return false;
|
|
@@ -8,6 +8,9 @@ const shouldIgnore = (filename) => {
|
|
|
8
8
|
return filename.includes('.brainlink') || !filename.endsWith('.md');
|
|
9
9
|
};
|
|
10
10
|
export const startVaultWatcher = (input) => {
|
|
11
|
+
if (isBucketVaultPath(input.vaultPath)) {
|
|
12
|
+
throw new Error('Watch mode is only supported for local filesystem vaults.');
|
|
13
|
+
}
|
|
11
14
|
const absoluteVaultPath = resolveVaultPath(input.vaultPath);
|
|
12
15
|
const debounceMs = input.debounceMs ?? 350;
|
|
13
16
|
let timeout = null;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
5
|
+
import { posix } from 'node:path';
|
|
6
|
+
import { getBrainlinkHomePath } from './paths.js';
|
|
7
|
+
const directoryMode = 0o700;
|
|
8
|
+
const fileMode = 0o600;
|
|
9
|
+
const bucketScheme = 's3:';
|
|
10
|
+
const manifestPath = '.brainlink/bucket-manifest.json';
|
|
11
|
+
const excludedSegments = new Set(['.brainlink', '.git', 'node_modules', 'dist']);
|
|
12
|
+
export const isBucketVaultUri = (value) => value.trim().toLowerCase().startsWith('s3://');
|
|
13
|
+
const trimSlashes = (value) => value.replace(/^\/+|\/+$/g, '');
|
|
14
|
+
const normalizePrefix = (value) => trimSlashes(posix.normalize(trimSlashes(value))).replace(/^\.$/, '');
|
|
15
|
+
export const parseBucketVaultUri = (uri) => {
|
|
16
|
+
const parsed = new URL(uri);
|
|
17
|
+
if (parsed.protocol !== bucketScheme || !parsed.hostname) {
|
|
18
|
+
throw new Error(`Unsupported bucket vault URI: ${uri}. Use s3://bucket/prefix.`);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
uri: formatBucketVaultUri(parsed.hostname, normalizePrefix(decodeURIComponent(parsed.pathname))),
|
|
22
|
+
bucket: parsed.hostname,
|
|
23
|
+
prefix: normalizePrefix(decodeURIComponent(parsed.pathname))
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export const formatBucketVaultUri = (bucket, prefix) => prefix ? `s3://${bucket}/${prefix}` : `s3://${bucket}`;
|
|
27
|
+
export const getBucketVaultCachePath = (uri) => {
|
|
28
|
+
const hash = createHash('sha256').update(parseBucketVaultUri(uri).uri).digest('hex').slice(0, 24);
|
|
29
|
+
return join(getBrainlinkHomePath(), 'bucket-cache', hash);
|
|
30
|
+
};
|
|
31
|
+
const ensureDirectory = async (path) => {
|
|
32
|
+
await mkdir(path, { recursive: true, mode: directoryMode });
|
|
33
|
+
await chmod(path, directoryMode);
|
|
34
|
+
};
|
|
35
|
+
const isPathInside = (parent, child) => {
|
|
36
|
+
const path = relative(parent, child);
|
|
37
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
38
|
+
};
|
|
39
|
+
const toSafeRelativePath = (key) => {
|
|
40
|
+
const normalized = normalizePrefix(key);
|
|
41
|
+
if (!normalized || normalized.split('/').some((segment) => segment === '..' || excludedSegments.has(segment))) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return normalized.endsWith('.md') ? normalized : null;
|
|
45
|
+
};
|
|
46
|
+
const toObjectKey = (reference, relativePath) => reference.prefix ? `${reference.prefix}/${relativePath}` : relativePath;
|
|
47
|
+
const toRelativeObjectKey = (reference, objectKey) => {
|
|
48
|
+
const relativePath = reference.prefix
|
|
49
|
+
? objectKey.startsWith(`${reference.prefix}/`)
|
|
50
|
+
? objectKey.slice(reference.prefix.length + 1)
|
|
51
|
+
: null
|
|
52
|
+
: objectKey;
|
|
53
|
+
return relativePath ? toSafeRelativePath(relativePath) : null;
|
|
54
|
+
};
|
|
55
|
+
const createBucketClient = () => new S3Client({
|
|
56
|
+
region: process.env.AWS_REGION ?? process.env.BRAINLINK_S3_REGION ?? 'us-east-1',
|
|
57
|
+
endpoint: process.env.BRAINLINK_S3_ENDPOINT ?? process.env.AWS_ENDPOINT_URL,
|
|
58
|
+
forcePathStyle: process.env.BRAINLINK_S3_FORCE_PATH_STYLE === '1'
|
|
59
|
+
});
|
|
60
|
+
const streamToString = async (body) => {
|
|
61
|
+
if (body && typeof body === 'object' && 'transformToString' in body && typeof body.transformToString === 'function') {
|
|
62
|
+
return body.transformToString();
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Unsupported S3 object body.');
|
|
65
|
+
};
|
|
66
|
+
const readManifest = async (cachePath) => {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(await readFile(join(cachePath, manifestPath), 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
72
|
+
return {
|
|
73
|
+
uri: '',
|
|
74
|
+
keys: []
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const writeManifest = async (cachePath, manifest) => {
|
|
81
|
+
const path = join(cachePath, manifestPath);
|
|
82
|
+
await ensureDirectory(dirname(path));
|
|
83
|
+
await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: 'utf8', mode: fileMode });
|
|
84
|
+
await chmod(path, fileMode);
|
|
85
|
+
};
|
|
86
|
+
const listBucketMarkdownKeys = async (client, reference) => {
|
|
87
|
+
const keys = [];
|
|
88
|
+
let continuationToken;
|
|
89
|
+
do {
|
|
90
|
+
const result = await client.send(new ListObjectsV2Command({
|
|
91
|
+
Bucket: reference.bucket,
|
|
92
|
+
Prefix: reference.prefix ? `${reference.prefix}/` : undefined,
|
|
93
|
+
ContinuationToken: continuationToken
|
|
94
|
+
}));
|
|
95
|
+
keys.push(...(result.Contents ?? []).flatMap((object) => (object.Key ? [object.Key] : [])));
|
|
96
|
+
continuationToken = result.NextContinuationToken;
|
|
97
|
+
} while (continuationToken);
|
|
98
|
+
return keys.flatMap((key) => {
|
|
99
|
+
const relativePath = toRelativeObjectKey(reference, key);
|
|
100
|
+
return relativePath ? [relativePath] : [];
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const removeStaleCachedFiles = async (cachePath, previousKeys, currentKeys) => {
|
|
104
|
+
await Promise.all(previousKeys
|
|
105
|
+
.filter((key) => !currentKeys.has(key))
|
|
106
|
+
.map(async (key) => {
|
|
107
|
+
const absolutePath = join(cachePath, key);
|
|
108
|
+
if (isPathInside(cachePath, absolutePath)) {
|
|
109
|
+
await rm(absolutePath, { force: true });
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
};
|
|
113
|
+
const downloadMarkdownFiles = async (client, reference, cachePath, keys) => {
|
|
114
|
+
await Promise.all(keys.map(async (key) => {
|
|
115
|
+
const absolutePath = join(cachePath, key);
|
|
116
|
+
if (!isPathInside(cachePath, absolutePath)) {
|
|
117
|
+
throw new Error(`Refusing to cache bucket object outside vault cache: ${key}`);
|
|
118
|
+
}
|
|
119
|
+
const result = await client.send(new GetObjectCommand({
|
|
120
|
+
Bucket: reference.bucket,
|
|
121
|
+
Key: toObjectKey(reference, key)
|
|
122
|
+
}));
|
|
123
|
+
await ensureDirectory(dirname(absolutePath));
|
|
124
|
+
await writeFile(absolutePath, await streamToString(result.Body), { encoding: 'utf8', mode: fileMode });
|
|
125
|
+
await chmod(absolutePath, fileMode);
|
|
126
|
+
}));
|
|
127
|
+
};
|
|
128
|
+
export const syncBucketVaultToCache = async (uri) => {
|
|
129
|
+
const reference = parseBucketVaultUri(uri);
|
|
130
|
+
const cachePath = getBucketVaultCachePath(reference.uri);
|
|
131
|
+
const client = createBucketClient();
|
|
132
|
+
const previousManifest = await readManifest(cachePath);
|
|
133
|
+
const keys = await listBucketMarkdownKeys(client, reference);
|
|
134
|
+
const currentKeys = new Set(keys);
|
|
135
|
+
await ensureDirectory(join(cachePath, '.brainlink'));
|
|
136
|
+
await removeStaleCachedFiles(cachePath, previousManifest.uri === reference.uri ? previousManifest.keys : [], currentKeys);
|
|
137
|
+
await downloadMarkdownFiles(client, reference, cachePath, keys);
|
|
138
|
+
await writeManifest(cachePath, {
|
|
139
|
+
uri: reference.uri,
|
|
140
|
+
keys
|
|
141
|
+
});
|
|
142
|
+
return cachePath;
|
|
143
|
+
};
|
|
144
|
+
export const writeBucketMarkdownFile = async (uri, filename, content) => {
|
|
145
|
+
const reference = parseBucketVaultUri(uri);
|
|
146
|
+
const cachePath = getBucketVaultCachePath(reference.uri);
|
|
147
|
+
const relativePath = toSafeRelativePath(filename.endsWith('.md') ? filename : `${filename}.md`);
|
|
148
|
+
if (!relativePath) {
|
|
149
|
+
throw new Error(`Invalid bucket Markdown path: ${filename}`);
|
|
150
|
+
}
|
|
151
|
+
const absolutePath = join(cachePath, relativePath);
|
|
152
|
+
if (!isPathInside(cachePath, absolutePath)) {
|
|
153
|
+
throw new Error(`Refusing to write outside bucket cache: ${absolutePath}`);
|
|
154
|
+
}
|
|
155
|
+
await ensureDirectory(join(cachePath, '.brainlink'));
|
|
156
|
+
await ensureDirectory(dirname(absolutePath));
|
|
157
|
+
await writeFile(absolutePath, content, { encoding: 'utf8', mode: fileMode });
|
|
158
|
+
await chmod(absolutePath, fileMode);
|
|
159
|
+
await createBucketClient().send(new PutObjectCommand({
|
|
160
|
+
Bucket: reference.bucket,
|
|
161
|
+
Key: toObjectKey(reference, relativePath),
|
|
162
|
+
Body: content,
|
|
163
|
+
ContentType: 'text/markdown; charset=utf-8'
|
|
164
|
+
}));
|
|
165
|
+
const manifest = await readManifest(cachePath);
|
|
166
|
+
await writeManifest(cachePath, {
|
|
167
|
+
uri: reference.uri,
|
|
168
|
+
keys: Array.from(new Set([...manifest.keys, relativePath])).sort()
|
|
169
|
+
});
|
|
170
|
+
return `${reference.uri}/${relativePath}`;
|
|
171
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { resolvePath } from './paths.js';
|
|
4
|
+
import { getBucketVaultCachePath, isBucketVaultUri, parseBucketVaultUri, syncBucketVaultToCache, writeBucketMarkdownFile } from './bucket-vault.js';
|
|
4
5
|
const excludedDirectories = new Set(['.brainlink', '.git', 'node_modules', 'dist']);
|
|
5
6
|
const directoryMode = 0o700;
|
|
6
7
|
const fileMode = 0o600;
|
|
@@ -15,30 +16,44 @@ const walkMarkdownFiles = async (directory) => {
|
|
|
15
16
|
}));
|
|
16
17
|
return nested.flat();
|
|
17
18
|
};
|
|
18
|
-
export const resolveVaultPath = (vaultPath) => resolvePath(vaultPath);
|
|
19
|
+
export const resolveVaultPath = (vaultPath) => isBucketVaultUri(vaultPath) ? getBucketVaultCachePath(vaultPath) : resolvePath(vaultPath);
|
|
20
|
+
export const isBucketVaultPath = (vaultPath) => isBucketVaultUri(vaultPath);
|
|
19
21
|
const isPathInside = (parent, child) => {
|
|
20
22
|
const path = relative(parent, child);
|
|
21
23
|
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
22
24
|
};
|
|
25
|
+
const isBucketPrefixInside = (parent, child) => parent === '' || child === parent || child.startsWith(`${parent}/`);
|
|
23
26
|
const secureDirectory = async (path) => {
|
|
24
27
|
await mkdir(path, { recursive: true, mode: directoryMode });
|
|
25
28
|
await chmod(path, directoryMode);
|
|
26
29
|
};
|
|
27
30
|
export const assertVaultAllowed = (vaultPath, allowedVaults) => {
|
|
31
|
+
if (isBucketVaultUri(vaultPath)) {
|
|
32
|
+
const vault = parseBucketVaultUri(vaultPath);
|
|
33
|
+
const allowed = allowedVaults.filter(isBucketVaultUri).map(parseBucketVaultUri);
|
|
34
|
+
if (allowedVaults.length > 0 &&
|
|
35
|
+
!allowed.some((allowedVault) => vault.bucket === allowedVault.bucket && isBucketPrefixInside(allowedVault.prefix, vault.prefix))) {
|
|
36
|
+
throw new Error(`Vault path is not allowed: ${vault.uri}. Configure BRAINLINK_ALLOWED_VAULTS or allowedVaults.`);
|
|
37
|
+
}
|
|
38
|
+
return vault.uri;
|
|
39
|
+
}
|
|
28
40
|
const absoluteVaultPath = resolveVaultPath(vaultPath);
|
|
29
|
-
const allowed = allowedVaults.map(resolveVaultPath);
|
|
41
|
+
const allowed = allowedVaults.filter((allowedVault) => !isBucketVaultUri(allowedVault)).map(resolveVaultPath);
|
|
30
42
|
if (allowed.length > 0 && !allowed.some((allowedPath) => isPathInside(allowedPath, absoluteVaultPath))) {
|
|
31
43
|
throw new Error(`Vault path is not allowed: ${absoluteVaultPath}. Configure BRAINLINK_ALLOWED_VAULTS or allowedVaults.`);
|
|
32
44
|
}
|
|
33
45
|
return absoluteVaultPath;
|
|
34
46
|
};
|
|
35
47
|
export const ensureVault = async (vaultPath) => {
|
|
48
|
+
if (isBucketVaultUri(vaultPath)) {
|
|
49
|
+
return syncBucketVaultToCache(vaultPath);
|
|
50
|
+
}
|
|
36
51
|
const absoluteVaultPath = resolveVaultPath(vaultPath);
|
|
37
52
|
await secureDirectory(join(absoluteVaultPath, '.brainlink'));
|
|
38
53
|
return absoluteVaultPath;
|
|
39
54
|
};
|
|
40
55
|
export const readMarkdownFiles = async (vaultPath) => {
|
|
41
|
-
const absoluteVaultPath =
|
|
56
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
42
57
|
const paths = await walkMarkdownFiles(absoluteVaultPath);
|
|
43
58
|
return Promise.all(paths.map(async (absolutePath) => {
|
|
44
59
|
const [content, stats] = await Promise.all([readFile(absolutePath, 'utf8'), stat(absolutePath)]);
|
|
@@ -51,6 +66,9 @@ export const readMarkdownFiles = async (vaultPath) => {
|
|
|
51
66
|
}));
|
|
52
67
|
};
|
|
53
68
|
export const writeMarkdownFile = async (vaultPath, filename, content) => {
|
|
69
|
+
if (isBucketVaultUri(vaultPath)) {
|
|
70
|
+
return writeBucketMarkdownFile(vaultPath, filename, content);
|
|
71
|
+
}
|
|
54
72
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
55
73
|
const absolutePath = resolve(absoluteVaultPath, filename.endsWith('.md') ? filename : `${filename}.md`);
|
|
56
74
|
if (!isPathInside(absoluteVaultPath, absolutePath)) {
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -552,4 +552,5 @@ Weak retrieval usually means:
|
|
|
552
552
|
- Local embeddings are deterministic and provider-free; remote embedding providers are not implemented yet.
|
|
553
553
|
- MCP integration is available through the `brainlink-mcp` stdio server.
|
|
554
554
|
- HTTP API is local and unauthenticated.
|
|
555
|
-
-
|
|
555
|
+
- Bucket vaults support S3-compatible `s3://bucket/prefix` URIs and use a local cache for SQLite indexes.
|
|
556
|
+
- Watch mode depends on platform filesystem watcher behavior and is only supported for local filesystem vaults.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -105,13 +105,15 @@ Application code depends on domain rules and infrastructure interfaces.
|
|
|
105
105
|
The infrastructure layer handles side effects:
|
|
106
106
|
|
|
107
107
|
- reading Markdown files from disk
|
|
108
|
+
- mirroring S3-compatible bucket Markdown into a local cache
|
|
108
109
|
- writing Markdown notes
|
|
109
110
|
- creating `.brainlink`
|
|
110
111
|
- writing and querying SQLite
|
|
111
112
|
- running FTS, semantic and hybrid retrieval
|
|
112
113
|
- narrowing semantic candidates through SQLite embedding buckets before cosine scoring
|
|
113
114
|
|
|
114
|
-
SQLite is an index, not the canonical storage model.
|
|
115
|
+
SQLite is an index, not the canonical storage model. For bucket vaults, Markdown
|
|
116
|
+
objects in the bucket remain canonical and SQLite is still local derived data.
|
|
115
117
|
|
|
116
118
|
## Indexing Flow
|
|
117
119
|
|
|
@@ -240,6 +242,7 @@ Relevant content
|
|
|
240
242
|
Permanent:
|
|
241
243
|
|
|
242
244
|
- Markdown files
|
|
245
|
+
- S3-compatible Markdown objects when the vault is `s3://bucket/prefix`
|
|
243
246
|
- optional Git history around the vault
|
|
244
247
|
|
|
245
248
|
Canonical agent memory lives under:
|
|
@@ -251,6 +254,7 @@ vault/agents/<agent-id>/**/*.md
|
|
|
251
254
|
Rebuildable:
|
|
252
255
|
|
|
253
256
|
- `.brainlink/brainlink.db`
|
|
257
|
+
- `$BRAINLINK_HOME/bucket-cache`
|
|
254
258
|
- FTS records
|
|
255
259
|
- local embedding vectors
|
|
256
260
|
- local embedding bucket index
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andespindola/brainlink",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.11",
|
|
4
4
|
"description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"pack:smoke": "npm pack --dry-run"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
+
"@aws-sdk/client-s3": "^3.1038.0",
|
|
57
58
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
59
|
"better-sqlite3": "^12.9.0",
|
|
59
60
|
"commander": "^14.0.2",
|