@teammates/recall 0.1.0
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 +156 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +249 -0
- package/dist/embeddings.d.ts +12 -0
- package/dist/embeddings.js +35 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/indexer.d.ts +54 -0
- package/dist/indexer.js +172 -0
- package/dist/search.d.ts +26 -0
- package/dist/search.js +75 -0
- package/package.json +34 -0
- package/src/cli.ts +275 -0
- package/src/embeddings.ts +43 -0
- package/src/index.ts +3 -0
- package/src/indexer.ts +203 -0
- package/src/search.ts +107 -0
- package/tsconfig.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @teammates/recall
|
|
2
|
+
|
|
3
|
+
Local semantic memory search for teammates. Indexes `MEMORIES.md` and daily logs (`memory/*.md`) using [Vectra](https://github.com/Stevenic/vectra) for vector search and [transformers.js](https://huggingface.co/docs/transformers.js) for embeddings.
|
|
4
|
+
|
|
5
|
+
**Zero cloud dependencies.** Everything runs locally — embeddings are generated on-device, indexes are stored as local files.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @teammates/recall
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or use with npx:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @teammates/recall search "token budget issues" --dir ./.teammates
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## How Agents Use It
|
|
20
|
+
|
|
21
|
+
The typical agent workflow:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 1. Agent writes a memory file (normal file write, no special tool needed)
|
|
25
|
+
echo "## Notes\n- Fixed the auth token refresh bug" >> .teammates/atlas/memory/2026-03-11.md
|
|
26
|
+
|
|
27
|
+
# 2. Agent searches memories (auto-syncs new files before searching)
|
|
28
|
+
teammates-recall search "auth token refresh" --json
|
|
29
|
+
|
|
30
|
+
# 3. That's it. No manual index/sync step needed.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Search auto-syncs by default** — any new or changed memory files are indexed before results are returned. For large indexes where sync latency matters, use `--no-sync` and manage syncing separately.
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
### search
|
|
38
|
+
|
|
39
|
+
Search across teammate memories. Auto-syncs new/changed files before querying.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
teammates-recall search "database migration pattern" --dir ./.teammates
|
|
43
|
+
teammates-recall search "rate limiting" --teammate atlas --results 3
|
|
44
|
+
teammates-recall search "auth token expiration" --json
|
|
45
|
+
teammates-recall search "deploy process" --no-sync # skip auto-sync
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
- `--teammate <name>` — Search a specific teammate (default: all)
|
|
50
|
+
- `--results <n>` — Max results (default: 5)
|
|
51
|
+
- `--no-sync` — Skip auto-sync before searching
|
|
52
|
+
- `--json` — Output as JSON (useful for piping to agents)
|
|
53
|
+
|
|
54
|
+
### add
|
|
55
|
+
|
|
56
|
+
Add a single file to a teammate's index. Use this right after writing a memory file for immediate indexing without a full sync.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
teammates-recall add .teammates/atlas/memory/2026-03-11.md --teammate atlas
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### sync
|
|
63
|
+
|
|
64
|
+
Incrementally sync new/changed memory files into existing indexes. Faster than a full rebuild.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
teammates-recall sync --dir ./.teammates
|
|
68
|
+
teammates-recall sync --teammate atlas
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### index
|
|
72
|
+
|
|
73
|
+
Full rebuild of all indexes from scratch. Use when setting up for the first time or when indexes seem stale.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
teammates-recall index --dir ./.teammates
|
|
77
|
+
teammates-recall index --teammate beacon
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### status
|
|
81
|
+
|
|
82
|
+
Check which teammates have memory files and whether they're indexed.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
teammates-recall status --dir ./.teammates
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## How It Works
|
|
89
|
+
|
|
90
|
+
1. **Discovers** teammate directories (any folder under `.teammates/` with a `SOUL.md`)
|
|
91
|
+
2. **Collects** memory files: `MEMORIES.md` + `memory/*.md`
|
|
92
|
+
3. **Chunks and embeds** text using transformers.js (`Xenova/all-MiniLM-L6-v2`, 384-dim vectors)
|
|
93
|
+
4. **Stores** the index at `.teammates/.index/<teammate>/` (gitignored)
|
|
94
|
+
5. **Searches** using Vectra's semantic similarity matching
|
|
95
|
+
|
|
96
|
+
## Use From Any Agent
|
|
97
|
+
|
|
98
|
+
Any AI coding tool that can run shell commands can use recall:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
teammates-recall search "how does auth work" --dir ./.teammates --json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `--json` flag returns structured results that agents can parse:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
"teammate": "atlas",
|
|
110
|
+
"uri": "atlas/MEMORIES.md",
|
|
111
|
+
"text": "### 2026-01-15: JWT Auth Pattern\n...",
|
|
112
|
+
"score": 0.847
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Use As a Library
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { Indexer, search } from "@teammates/recall";
|
|
121
|
+
|
|
122
|
+
// Full index rebuild
|
|
123
|
+
const indexer = new Indexer({ teammatesDir: "./.teammates" });
|
|
124
|
+
await indexer.indexAll();
|
|
125
|
+
|
|
126
|
+
// Incremental sync
|
|
127
|
+
await indexer.syncTeammate("atlas");
|
|
128
|
+
|
|
129
|
+
// Add a single file after writing it
|
|
130
|
+
await indexer.upsertFile("atlas", ".teammates/atlas/memory/2026-03-11.md");
|
|
131
|
+
|
|
132
|
+
// Search (auto-syncs by default)
|
|
133
|
+
const results = await search("database migration", {
|
|
134
|
+
teammatesDir: "./.teammates",
|
|
135
|
+
teammate: "atlas",
|
|
136
|
+
maxResults: 5,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Search without auto-sync
|
|
140
|
+
const results2 = await search("database migration", {
|
|
141
|
+
teammatesDir: "./.teammates",
|
|
142
|
+
skipSync: true,
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Embedding Model
|
|
147
|
+
|
|
148
|
+
Default: `Xenova/all-MiniLM-L6-v2` (~23 MB, 384 dimensions)
|
|
149
|
+
|
|
150
|
+
- Downloaded automatically on first run, cached locally
|
|
151
|
+
- No API keys required
|
|
152
|
+
- Override with `--model <name>` for any transformers.js-compatible model
|
|
153
|
+
|
|
154
|
+
## Storage
|
|
155
|
+
|
|
156
|
+
Indexes live at `.teammates/.index/` and are gitignored. They're derived from the markdown source files and can be rebuilt at any time with `teammates-recall index`.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import { Indexer } from "./indexer.js";
|
|
5
|
+
import { search } from "./search.js";
|
|
6
|
+
const HELP = `
|
|
7
|
+
teammates-recall — Semantic memory search for teammates
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
teammates-recall index [options] Full rebuild of all indexes
|
|
11
|
+
teammates-recall sync [options] Sync new/changed files into indexes
|
|
12
|
+
teammates-recall add <file> [options] Add a single file to a teammate's index
|
|
13
|
+
teammates-recall search <query> [options] Search teammate memories (auto-syncs)
|
|
14
|
+
teammates-recall status [options] Show index status
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--dir <path> Path to .teammates directory (default: ./.teammates)
|
|
18
|
+
--teammate <name> Limit to a specific teammate
|
|
19
|
+
--results <n> Max results (default: 5)
|
|
20
|
+
--model <name> Embedding model (default: Xenova/all-MiniLM-L6-v2)
|
|
21
|
+
--no-sync Skip auto-sync before search
|
|
22
|
+
--json Output as JSON
|
|
23
|
+
--help Show this help
|
|
24
|
+
`.trim();
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const args = {
|
|
27
|
+
command: "",
|
|
28
|
+
query: "",
|
|
29
|
+
file: "",
|
|
30
|
+
dir: "./.teammates",
|
|
31
|
+
results: 5,
|
|
32
|
+
json: false,
|
|
33
|
+
sync: true,
|
|
34
|
+
};
|
|
35
|
+
let i = 0;
|
|
36
|
+
// Skip node and script path
|
|
37
|
+
while (i < argv.length && (argv[i].includes("node") || argv[i].includes("teammates-recall") || argv[i].endsWith(".js"))) {
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
if (i < argv.length && !argv[i].startsWith("-")) {
|
|
41
|
+
args.command = argv[i++];
|
|
42
|
+
}
|
|
43
|
+
// For search, next non-flag arg is the query; for add, it's the file path
|
|
44
|
+
if (args.command === "search" && i < argv.length && !argv[i].startsWith("-")) {
|
|
45
|
+
args.query = argv[i++];
|
|
46
|
+
}
|
|
47
|
+
else if (args.command === "add" && i < argv.length && !argv[i].startsWith("-")) {
|
|
48
|
+
args.file = argv[i++];
|
|
49
|
+
}
|
|
50
|
+
while (i < argv.length) {
|
|
51
|
+
const arg = argv[i++];
|
|
52
|
+
switch (arg) {
|
|
53
|
+
case "--dir":
|
|
54
|
+
args.dir = argv[i++];
|
|
55
|
+
break;
|
|
56
|
+
case "--teammate":
|
|
57
|
+
args.teammate = argv[i++];
|
|
58
|
+
break;
|
|
59
|
+
case "--results":
|
|
60
|
+
args.results = parseInt(argv[i++], 10);
|
|
61
|
+
break;
|
|
62
|
+
case "--model":
|
|
63
|
+
args.model = argv[i++];
|
|
64
|
+
break;
|
|
65
|
+
case "--no-sync":
|
|
66
|
+
args.sync = false;
|
|
67
|
+
break;
|
|
68
|
+
case "--json":
|
|
69
|
+
args.json = true;
|
|
70
|
+
break;
|
|
71
|
+
case "--help":
|
|
72
|
+
case "-h":
|
|
73
|
+
console.log(HELP);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return args;
|
|
78
|
+
}
|
|
79
|
+
async function resolveTeammatesDir(dir) {
|
|
80
|
+
const resolved = path.resolve(dir);
|
|
81
|
+
try {
|
|
82
|
+
await fs.access(resolved);
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
console.error(`Error: .teammates directory not found at ${resolved}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function cmdIndex(args) {
|
|
91
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
92
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
93
|
+
if (args.teammate) {
|
|
94
|
+
console.error(`Indexing ${args.teammate}...`);
|
|
95
|
+
const count = await indexer.indexTeammate(args.teammate);
|
|
96
|
+
if (args.json) {
|
|
97
|
+
console.log(JSON.stringify({ teammate: args.teammate, files: count }));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(`Indexed ${count} files for ${args.teammate}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.error("Indexing all teammates...");
|
|
105
|
+
const results = await indexer.indexAll();
|
|
106
|
+
if (args.json) {
|
|
107
|
+
const obj = Object.fromEntries(results);
|
|
108
|
+
console.log(JSON.stringify(obj));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
for (const [teammate, count] of results) {
|
|
112
|
+
console.log(` ${teammate}: ${count} files`);
|
|
113
|
+
}
|
|
114
|
+
console.log(`Done.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function cmdSync(args) {
|
|
119
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
120
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
121
|
+
if (args.teammate) {
|
|
122
|
+
console.error(`Syncing ${args.teammate}...`);
|
|
123
|
+
const count = await indexer.syncTeammate(args.teammate);
|
|
124
|
+
if (args.json) {
|
|
125
|
+
console.log(JSON.stringify({ teammate: args.teammate, files: count }));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(`Synced ${count} files for ${args.teammate}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.error("Syncing all teammates...");
|
|
133
|
+
const results = await indexer.syncAll();
|
|
134
|
+
if (args.json) {
|
|
135
|
+
const obj = Object.fromEntries(results);
|
|
136
|
+
console.log(JSON.stringify(obj));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
for (const [teammate, count] of results) {
|
|
140
|
+
console.log(` ${teammate}: ${count} files`);
|
|
141
|
+
}
|
|
142
|
+
console.log(`Done.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function cmdAdd(args) {
|
|
147
|
+
if (!args.file) {
|
|
148
|
+
console.error("Error: add requires a file path argument");
|
|
149
|
+
console.error("Usage: teammates-recall add <file> --teammate <name>");
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
if (!args.teammate) {
|
|
153
|
+
console.error("Error: add requires --teammate <name>");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
157
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
158
|
+
await indexer.upsertFile(args.teammate, args.file);
|
|
159
|
+
if (args.json) {
|
|
160
|
+
console.log(JSON.stringify({ teammate: args.teammate, file: args.file, status: "ok" }));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(`Added ${args.file} to ${args.teammate}'s index`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function cmdSearch(args) {
|
|
167
|
+
if (!args.query) {
|
|
168
|
+
console.error("Error: search requires a query argument");
|
|
169
|
+
console.error("Usage: teammates-recall search <query> [options]");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
173
|
+
const results = await search(args.query, {
|
|
174
|
+
teammatesDir,
|
|
175
|
+
teammate: args.teammate,
|
|
176
|
+
maxResults: args.results,
|
|
177
|
+
model: args.model,
|
|
178
|
+
skipSync: !args.sync,
|
|
179
|
+
});
|
|
180
|
+
if (args.json) {
|
|
181
|
+
console.log(JSON.stringify(results, null, 2));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
if (results.length === 0) {
|
|
185
|
+
console.log("No results found.");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const result of results) {
|
|
189
|
+
console.log(`--- ${result.teammate} | ${result.uri} (score: ${result.score.toFixed(3)}) ---`);
|
|
190
|
+
console.log(result.text);
|
|
191
|
+
console.log();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function cmdStatus(args) {
|
|
196
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
197
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
198
|
+
const teammates = await indexer.discoverTeammates();
|
|
199
|
+
const status = {};
|
|
200
|
+
for (const teammate of teammates) {
|
|
201
|
+
const { files } = await indexer.collectFiles(teammate);
|
|
202
|
+
const indexPath = path.join(indexer.indexRoot, teammate);
|
|
203
|
+
let indexed = false;
|
|
204
|
+
try {
|
|
205
|
+
await fs.access(indexPath);
|
|
206
|
+
indexed = true;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Not indexed
|
|
210
|
+
}
|
|
211
|
+
status[teammate] = { memoryFiles: files.length, indexed };
|
|
212
|
+
}
|
|
213
|
+
if (args.json) {
|
|
214
|
+
console.log(JSON.stringify(status, null, 2));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
for (const [teammate, info] of Object.entries(status)) {
|
|
218
|
+
const tag = info.indexed ? "indexed" : "not indexed";
|
|
219
|
+
console.log(` ${teammate}: ${info.memoryFiles} memory files (${tag})`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function main() {
|
|
224
|
+
const args = parseArgs(process.argv);
|
|
225
|
+
switch (args.command) {
|
|
226
|
+
case "index":
|
|
227
|
+
await cmdIndex(args);
|
|
228
|
+
break;
|
|
229
|
+
case "sync":
|
|
230
|
+
await cmdSync(args);
|
|
231
|
+
break;
|
|
232
|
+
case "add":
|
|
233
|
+
await cmdAdd(args);
|
|
234
|
+
break;
|
|
235
|
+
case "search":
|
|
236
|
+
await cmdSearch(args);
|
|
237
|
+
break;
|
|
238
|
+
case "status":
|
|
239
|
+
await cmdStatus(args);
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
console.log(HELP);
|
|
243
|
+
process.exit(args.command ? 1 : 0);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
main().catch((err) => {
|
|
247
|
+
console.error(err.message);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EmbeddingsModel, EmbeddingsResponse } from "vectra";
|
|
2
|
+
/**
|
|
3
|
+
* Local embeddings using transformers.js. No API keys, no network after first model download.
|
|
4
|
+
*/
|
|
5
|
+
export declare class LocalEmbeddings implements EmbeddingsModel {
|
|
6
|
+
readonly maxTokens = 256;
|
|
7
|
+
private _model;
|
|
8
|
+
private _extractor;
|
|
9
|
+
constructor(model?: string);
|
|
10
|
+
createEmbeddings(inputs: string | string[]): Promise<EmbeddingsResponse>;
|
|
11
|
+
private _getExtractor;
|
|
12
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const DEFAULT_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
2
|
+
const MAX_TOKENS = 256;
|
|
3
|
+
/**
|
|
4
|
+
* Local embeddings using transformers.js. No API keys, no network after first model download.
|
|
5
|
+
*/
|
|
6
|
+
export class LocalEmbeddings {
|
|
7
|
+
maxTokens = MAX_TOKENS;
|
|
8
|
+
_model;
|
|
9
|
+
_extractor = null;
|
|
10
|
+
constructor(model) {
|
|
11
|
+
this._model = model ?? DEFAULT_MODEL;
|
|
12
|
+
}
|
|
13
|
+
async createEmbeddings(inputs) {
|
|
14
|
+
try {
|
|
15
|
+
const extractor = await this._getExtractor();
|
|
16
|
+
const texts = Array.isArray(inputs) ? inputs : [inputs];
|
|
17
|
+
const output = await extractor(texts, {
|
|
18
|
+
pooling: "mean",
|
|
19
|
+
normalize: true,
|
|
20
|
+
});
|
|
21
|
+
const embeddings = output.tolist();
|
|
22
|
+
return { status: "success", output: embeddings };
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return { status: "error", message: err.message };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async _getExtractor() {
|
|
29
|
+
if (!this._extractor) {
|
|
30
|
+
const { pipeline } = await import("@huggingface/transformers");
|
|
31
|
+
this._extractor = await pipeline("feature-extraction", this._model);
|
|
32
|
+
}
|
|
33
|
+
return this._extractor;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface IndexerConfig {
|
|
2
|
+
/** Path to the .teammates directory */
|
|
3
|
+
teammatesDir: string;
|
|
4
|
+
/** Embedding model name (default: Xenova/all-MiniLM-L6-v2) */
|
|
5
|
+
model?: string;
|
|
6
|
+
}
|
|
7
|
+
interface TeammateFiles {
|
|
8
|
+
teammate: string;
|
|
9
|
+
files: {
|
|
10
|
+
uri: string;
|
|
11
|
+
absolutePath: string;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Indexes teammate memory files (MEMORIES.md + memory/*.md) into Vectra.
|
|
16
|
+
* One index per teammate, stored at .teammates/.index/<name>/
|
|
17
|
+
*/
|
|
18
|
+
export declare class Indexer {
|
|
19
|
+
private _config;
|
|
20
|
+
private _embeddings;
|
|
21
|
+
constructor(config: IndexerConfig);
|
|
22
|
+
get indexRoot(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Discover all teammate directories (folders containing SOUL.md).
|
|
25
|
+
*/
|
|
26
|
+
discoverTeammates(): Promise<string[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Collect all indexable memory files for a teammate.
|
|
29
|
+
*/
|
|
30
|
+
collectFiles(teammate: string): Promise<TeammateFiles>;
|
|
31
|
+
/**
|
|
32
|
+
* Build or rebuild the index for a single teammate.
|
|
33
|
+
*/
|
|
34
|
+
indexTeammate(teammate: string): Promise<number>;
|
|
35
|
+
/**
|
|
36
|
+
* Build or rebuild indexes for all teammates.
|
|
37
|
+
*/
|
|
38
|
+
indexAll(): Promise<Map<string, number>>;
|
|
39
|
+
/**
|
|
40
|
+
* Upsert a single file into an existing teammate index.
|
|
41
|
+
* Creates the index if it doesn't exist yet.
|
|
42
|
+
*/
|
|
43
|
+
upsertFile(teammate: string, filePath: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Sync a teammate's index with their current memory files.
|
|
46
|
+
* Upserts new/changed files without a full rebuild.
|
|
47
|
+
*/
|
|
48
|
+
syncTeammate(teammate: string): Promise<number>;
|
|
49
|
+
/**
|
|
50
|
+
* Sync indexes for all teammates.
|
|
51
|
+
*/
|
|
52
|
+
syncAll(): Promise<Map<string, number>>;
|
|
53
|
+
}
|
|
54
|
+
export {};
|
package/dist/indexer.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { LocalDocumentIndex } from "vectra";
|
|
2
|
+
import { LocalEmbeddings } from "./embeddings.js";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Indexes teammate memory files (MEMORIES.md + memory/*.md) into Vectra.
|
|
7
|
+
* One index per teammate, stored at .teammates/.index/<name>/
|
|
8
|
+
*/
|
|
9
|
+
export class Indexer {
|
|
10
|
+
_config;
|
|
11
|
+
_embeddings;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this._config = config;
|
|
14
|
+
this._embeddings = new LocalEmbeddings(config.model);
|
|
15
|
+
}
|
|
16
|
+
get indexRoot() {
|
|
17
|
+
return path.join(this._config.teammatesDir, ".index");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Discover all teammate directories (folders containing SOUL.md).
|
|
21
|
+
*/
|
|
22
|
+
async discoverTeammates() {
|
|
23
|
+
const entries = await fs.readdir(this._config.teammatesDir, {
|
|
24
|
+
withFileTypes: true,
|
|
25
|
+
});
|
|
26
|
+
const teammates = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
29
|
+
continue;
|
|
30
|
+
const soulPath = path.join(this._config.teammatesDir, entry.name, "SOUL.md");
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(soulPath);
|
|
33
|
+
teammates.push(entry.name);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Not a teammate folder
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return teammates;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Collect all indexable memory files for a teammate.
|
|
43
|
+
*/
|
|
44
|
+
async collectFiles(teammate) {
|
|
45
|
+
const teammateDir = path.join(this._config.teammatesDir, teammate);
|
|
46
|
+
const files = [];
|
|
47
|
+
// MEMORIES.md
|
|
48
|
+
const memoriesPath = path.join(teammateDir, "MEMORIES.md");
|
|
49
|
+
try {
|
|
50
|
+
await fs.access(memoriesPath);
|
|
51
|
+
files.push({ uri: `${teammate}/MEMORIES.md`, absolutePath: memoriesPath });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// No MEMORIES.md
|
|
55
|
+
}
|
|
56
|
+
// memory/*.md (daily logs)
|
|
57
|
+
const memoryDir = path.join(teammateDir, "memory");
|
|
58
|
+
try {
|
|
59
|
+
const memoryEntries = await fs.readdir(memoryDir);
|
|
60
|
+
for (const entry of memoryEntries) {
|
|
61
|
+
if (!entry.endsWith(".md"))
|
|
62
|
+
continue;
|
|
63
|
+
files.push({
|
|
64
|
+
uri: `${teammate}/memory/${entry}`,
|
|
65
|
+
absolutePath: path.join(memoryDir, entry),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// No memory/ directory
|
|
71
|
+
}
|
|
72
|
+
return { teammate, files };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build or rebuild the index for a single teammate.
|
|
76
|
+
*/
|
|
77
|
+
async indexTeammate(teammate) {
|
|
78
|
+
const { files } = await this.collectFiles(teammate);
|
|
79
|
+
if (files.length === 0)
|
|
80
|
+
return 0;
|
|
81
|
+
const indexPath = path.join(this.indexRoot, teammate);
|
|
82
|
+
const index = new LocalDocumentIndex({
|
|
83
|
+
folderPath: indexPath,
|
|
84
|
+
embeddings: this._embeddings,
|
|
85
|
+
});
|
|
86
|
+
// Recreate index from scratch
|
|
87
|
+
await index.createIndex({ version: 1, deleteIfExists: true });
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const text = await fs.readFile(file.absolutePath, "utf-8");
|
|
91
|
+
if (text.trim().length === 0)
|
|
92
|
+
continue;
|
|
93
|
+
await index.upsertDocument(file.uri, text, "md");
|
|
94
|
+
count++;
|
|
95
|
+
}
|
|
96
|
+
return count;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build or rebuild indexes for all teammates.
|
|
100
|
+
*/
|
|
101
|
+
async indexAll() {
|
|
102
|
+
const teammates = await this.discoverTeammates();
|
|
103
|
+
const results = new Map();
|
|
104
|
+
for (const teammate of teammates) {
|
|
105
|
+
const count = await this.indexTeammate(teammate);
|
|
106
|
+
results.set(teammate, count);
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Upsert a single file into an existing teammate index.
|
|
112
|
+
* Creates the index if it doesn't exist yet.
|
|
113
|
+
*/
|
|
114
|
+
async upsertFile(teammate, filePath) {
|
|
115
|
+
const teammateDir = path.join(this._config.teammatesDir, teammate);
|
|
116
|
+
const absolutePath = path.resolve(filePath);
|
|
117
|
+
const relativePath = path.relative(teammateDir, absolutePath);
|
|
118
|
+
const uri = `${teammate}/${relativePath.replace(/\\/g, "/")}`;
|
|
119
|
+
const text = await fs.readFile(absolutePath, "utf-8");
|
|
120
|
+
if (text.trim().length === 0)
|
|
121
|
+
return;
|
|
122
|
+
const indexPath = path.join(this.indexRoot, teammate);
|
|
123
|
+
const index = new LocalDocumentIndex({
|
|
124
|
+
folderPath: indexPath,
|
|
125
|
+
embeddings: this._embeddings,
|
|
126
|
+
});
|
|
127
|
+
if (!(await index.isIndexCreated())) {
|
|
128
|
+
await index.createIndex({ version: 1 });
|
|
129
|
+
}
|
|
130
|
+
await index.upsertDocument(uri, text, "md");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Sync a teammate's index with their current memory files.
|
|
134
|
+
* Upserts new/changed files without a full rebuild.
|
|
135
|
+
*/
|
|
136
|
+
async syncTeammate(teammate) {
|
|
137
|
+
const { files } = await this.collectFiles(teammate);
|
|
138
|
+
if (files.length === 0)
|
|
139
|
+
return 0;
|
|
140
|
+
const indexPath = path.join(this.indexRoot, teammate);
|
|
141
|
+
const index = new LocalDocumentIndex({
|
|
142
|
+
folderPath: indexPath,
|
|
143
|
+
embeddings: this._embeddings,
|
|
144
|
+
});
|
|
145
|
+
if (!(await index.isIndexCreated())) {
|
|
146
|
+
// No index yet — do a full build
|
|
147
|
+
return this.indexTeammate(teammate);
|
|
148
|
+
}
|
|
149
|
+
// Upsert all files (Vectra handles dedup internally via URI)
|
|
150
|
+
let count = 0;
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const text = await fs.readFile(file.absolutePath, "utf-8");
|
|
153
|
+
if (text.trim().length === 0)
|
|
154
|
+
continue;
|
|
155
|
+
await index.upsertDocument(file.uri, text, "md");
|
|
156
|
+
count++;
|
|
157
|
+
}
|
|
158
|
+
return count;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Sync indexes for all teammates.
|
|
162
|
+
*/
|
|
163
|
+
async syncAll() {
|
|
164
|
+
const teammates = await this.discoverTeammates();
|
|
165
|
+
const results = new Map();
|
|
166
|
+
for (const teammate of teammates) {
|
|
167
|
+
const count = await this.syncTeammate(teammate);
|
|
168
|
+
results.set(teammate, count);
|
|
169
|
+
}
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
}
|