@teammates/recall 0.3.1 → 0.3.2

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/src/embeddings.ts CHANGED
@@ -1,56 +1,56 @@
1
- import type { EmbeddingsModel, EmbeddingsResponse } from "vectra";
2
-
3
- const DEFAULT_MODEL = "Xenova/all-MiniLM-L6-v2";
4
- const MAX_TOKENS = 256;
5
-
6
- /**
7
- * Local embeddings using transformers.js. No API keys, no network after first model download.
8
- */
9
- export class LocalEmbeddings implements EmbeddingsModel {
10
- readonly maxTokens = MAX_TOKENS;
11
-
12
- private _model: string;
13
- private _extractor: any | null = null;
14
-
15
- constructor(model?: string) {
16
- this._model = model ?? DEFAULT_MODEL;
17
- }
18
-
19
- async createEmbeddings(
20
- inputs: string | string[],
21
- ): Promise<EmbeddingsResponse> {
22
- try {
23
- const extractor = await this._getExtractor();
24
- const texts = (Array.isArray(inputs) ? inputs : [inputs]).filter(
25
- (t) => t.trim().length > 0,
26
- );
27
- if (texts.length === 0) {
28
- return { status: "success", output: [] };
29
- }
30
- const output = await extractor(texts, {
31
- pooling: "mean",
32
- normalize: true,
33
- });
34
- const embeddings: number[][] = output.tolist();
35
- return { status: "success", output: embeddings };
36
- } catch (err: any) {
37
- return { status: "error", message: err.message };
38
- }
39
- }
40
-
41
- private async _getExtractor(): Promise<any> {
42
- if (!this._extractor) {
43
- const { pipeline } = await import("@huggingface/transformers");
44
- const origWarn = console.warn;
45
- console.warn = () => {};
46
- try {
47
- this._extractor = await pipeline("feature-extraction", this._model, {
48
- dtype: "fp32",
49
- });
50
- } finally {
51
- console.warn = origWarn;
52
- }
53
- }
54
- return this._extractor;
55
- }
56
- }
1
+ import type { EmbeddingsModel, EmbeddingsResponse } from "vectra";
2
+
3
+ const DEFAULT_MODEL = "Xenova/all-MiniLM-L6-v2";
4
+ const MAX_TOKENS = 256;
5
+
6
+ /**
7
+ * Local embeddings using transformers.js. No API keys, no network after first model download.
8
+ */
9
+ export class LocalEmbeddings implements EmbeddingsModel {
10
+ readonly maxTokens = MAX_TOKENS;
11
+
12
+ private _model: string;
13
+ private _extractor: any | null = null;
14
+
15
+ constructor(model?: string) {
16
+ this._model = model ?? DEFAULT_MODEL;
17
+ }
18
+
19
+ async createEmbeddings(
20
+ inputs: string | string[],
21
+ ): Promise<EmbeddingsResponse> {
22
+ try {
23
+ const extractor = await this._getExtractor();
24
+ const texts = (Array.isArray(inputs) ? inputs : [inputs]).filter(
25
+ (t) => t.trim().length > 0,
26
+ );
27
+ if (texts.length === 0) {
28
+ return { status: "success", output: [] };
29
+ }
30
+ const output = await extractor(texts, {
31
+ pooling: "mean",
32
+ normalize: true,
33
+ });
34
+ const embeddings: number[][] = output.tolist();
35
+ return { status: "success", output: embeddings };
36
+ } catch (err: any) {
37
+ return { status: "error", message: err.message };
38
+ }
39
+ }
40
+
41
+ private async _getExtractor(): Promise<any> {
42
+ if (!this._extractor) {
43
+ const { pipeline } = await import("@huggingface/transformers");
44
+ const origWarn = console.warn;
45
+ console.warn = () => {};
46
+ try {
47
+ this._extractor = await pipeline("feature-extraction", this._model, {
48
+ dtype: "fp32",
49
+ });
50
+ } finally {
51
+ console.warn = origWarn;
52
+ }
53
+ }
54
+ return this._extractor;
55
+ }
56
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { LocalEmbeddings } from "./embeddings.js";
2
- export { Indexer, type IndexerConfig } from "./indexer.js";
3
- export { type SearchOptions, type SearchResult, search } from "./search.js";
1
+ export { LocalEmbeddings } from "./embeddings.js";
2
+ export { Indexer, type IndexerConfig } from "./indexer.js";
3
+ export { type SearchOptions, type SearchResult, search } from "./search.js";
@@ -1,262 +1,262 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { Indexer } from "./indexer.js";
6
-
7
- // Stub embeddings — we don't want to load the real model in tests
8
- class StubEmbeddings {
9
- readonly maxTokens = 256;
10
- async createEmbeddings(inputs: string | string[]) {
11
- const texts = Array.isArray(inputs) ? inputs : [inputs];
12
- return {
13
- status: "success" as const,
14
- output: texts.map(() => new Array(384).fill(0).map(() => Math.random())),
15
- };
16
- }
17
- }
18
-
19
- // Create an Indexer with stubbed embeddings
20
- function createIndexer(teammatesDir: string): Indexer {
21
- const indexer = new Indexer({ teammatesDir });
22
- // Swap out the real embeddings with our stub
23
- (indexer as any)._embeddings = new StubEmbeddings();
24
- return indexer;
25
- }
26
-
27
- let testDir: string;
28
-
29
- beforeEach(async () => {
30
- testDir = join(
31
- tmpdir(),
32
- `recall-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
33
- );
34
- await mkdir(testDir, { recursive: true });
35
- });
36
-
37
- afterEach(async () => {
38
- await rm(testDir, { recursive: true, force: true });
39
- });
40
-
41
- describe("Indexer", () => {
42
- describe("discoverTeammates", () => {
43
- it("finds directories containing SOUL.md", async () => {
44
- const beacon = join(testDir, "beacon");
45
- const scribe = join(testDir, "scribe");
46
- const notTeammate = join(testDir, "random");
47
-
48
- await mkdir(beacon, { recursive: true });
49
- await mkdir(scribe, { recursive: true });
50
- await mkdir(notTeammate, { recursive: true });
51
-
52
- await writeFile(join(beacon, "SOUL.md"), "# Beacon");
53
- await writeFile(join(scribe, "SOUL.md"), "# Scribe");
54
- // notTeammate has no SOUL.md
55
-
56
- const indexer = createIndexer(testDir);
57
- const teammates = await indexer.discoverTeammates();
58
-
59
- expect(teammates).toContain("beacon");
60
- expect(teammates).toContain("scribe");
61
- expect(teammates).not.toContain("random");
62
- });
63
-
64
- it("ignores dot-prefixed directories", async () => {
65
- const hidden = join(testDir, ".tmp");
66
- await mkdir(hidden, { recursive: true });
67
- await writeFile(join(hidden, "SOUL.md"), "# Hidden");
68
-
69
- const indexer = createIndexer(testDir);
70
- const teammates = await indexer.discoverTeammates();
71
-
72
- expect(teammates).not.toContain(".tmp");
73
- expect(teammates).toHaveLength(0);
74
- });
75
-
76
- it("returns empty array when no teammates exist", async () => {
77
- const indexer = createIndexer(testDir);
78
- const teammates = await indexer.discoverTeammates();
79
- expect(teammates).toEqual([]);
80
- });
81
- });
82
-
83
- describe("collectFiles", () => {
84
- it("collects WISDOM.md", async () => {
85
- const beacon = join(testDir, "beacon");
86
- await mkdir(beacon, { recursive: true });
87
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
88
-
89
- const indexer = createIndexer(testDir);
90
- const { files } = await indexer.collectFiles("beacon");
91
-
92
- expect(files).toHaveLength(1);
93
- expect(files[0].uri).toBe("beacon/WISDOM.md");
94
- });
95
-
96
- it("collects typed memory files from memory/", async () => {
97
- const memDir = join(testDir, "beacon", "memory");
98
- await mkdir(memDir, { recursive: true });
99
- await writeFile(join(memDir, "feedback_testing.md"), "# Feedback");
100
- await writeFile(join(memDir, "project_goals.md"), "# Goals");
101
-
102
- const indexer = createIndexer(testDir);
103
- const { files } = await indexer.collectFiles("beacon");
104
-
105
- const uris = files.map((f) => f.uri);
106
- expect(uris).toContain("beacon/memory/feedback_testing.md");
107
- expect(uris).toContain("beacon/memory/project_goals.md");
108
- });
109
-
110
- it("skips daily logs (YYYY-MM-DD.md pattern)", async () => {
111
- const memDir = join(testDir, "beacon", "memory");
112
- await mkdir(memDir, { recursive: true });
113
- await writeFile(join(memDir, "2026-03-14.md"), "# Day 1");
114
- await writeFile(join(memDir, "2026-03-15.md"), "# Day 2");
115
- await writeFile(join(memDir, "feedback_testing.md"), "# Feedback");
116
-
117
- const indexer = createIndexer(testDir);
118
- const { files } = await indexer.collectFiles("beacon");
119
-
120
- const uris = files.map((f) => f.uri);
121
- expect(uris).not.toContain("beacon/memory/2026-03-14.md");
122
- expect(uris).not.toContain("beacon/memory/2026-03-15.md");
123
- expect(uris).toContain("beacon/memory/feedback_testing.md");
124
- });
125
-
126
- it("collects weekly summaries from memory/weekly/", async () => {
127
- const weeklyDir = join(testDir, "beacon", "memory", "weekly");
128
- await mkdir(weeklyDir, { recursive: true });
129
- await writeFile(join(weeklyDir, "2026-W10.md"), "# Week 10");
130
- await writeFile(join(weeklyDir, "2026-W11.md"), "# Week 11");
131
-
132
- const indexer = createIndexer(testDir);
133
- const { files } = await indexer.collectFiles("beacon");
134
-
135
- const uris = files.map((f) => f.uri);
136
- expect(uris).toContain("beacon/memory/weekly/2026-W10.md");
137
- expect(uris).toContain("beacon/memory/weekly/2026-W11.md");
138
- });
139
-
140
- it("collects monthly summaries from memory/monthly/", async () => {
141
- const monthlyDir = join(testDir, "beacon", "memory", "monthly");
142
- await mkdir(monthlyDir, { recursive: true });
143
- await writeFile(join(monthlyDir, "2025-12.md"), "# Dec 2025");
144
-
145
- const indexer = createIndexer(testDir);
146
- const { files } = await indexer.collectFiles("beacon");
147
-
148
- const uris = files.map((f) => f.uri);
149
- expect(uris).toContain("beacon/memory/monthly/2025-12.md");
150
- });
151
-
152
- it("skips non-md files", async () => {
153
- const memDir = join(testDir, "beacon", "memory");
154
- await mkdir(memDir, { recursive: true });
155
- await writeFile(join(memDir, "notes.txt"), "not markdown");
156
- await writeFile(join(memDir, "feedback_test.md"), "# Feedback");
157
-
158
- const indexer = createIndexer(testDir);
159
- const { files } = await indexer.collectFiles("beacon");
160
-
161
- expect(files).toHaveLength(1);
162
- expect(files[0].uri).toBe("beacon/memory/feedback_test.md");
163
- });
164
-
165
- it("returns empty files when teammate has no content", async () => {
166
- await mkdir(join(testDir, "beacon"), { recursive: true });
167
-
168
- const indexer = createIndexer(testDir);
169
- const { files } = await indexer.collectFiles("beacon");
170
- expect(files).toEqual([]);
171
- });
172
- });
173
-
174
- describe("indexPath", () => {
175
- it("returns correct path under teammate directory", () => {
176
- const indexer = createIndexer(testDir);
177
- const p = indexer.indexPath("beacon");
178
- expect(p).toBe(join(testDir, "beacon", ".index"));
179
- });
180
- });
181
-
182
- describe("indexTeammate", () => {
183
- it("creates an index and returns file count", async () => {
184
- const beacon = join(testDir, "beacon");
185
- const memDir = join(beacon, "memory");
186
- await mkdir(memDir, { recursive: true });
187
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom content");
188
- await writeFile(join(memDir, "feedback_test.md"), "# Feedback content");
189
-
190
- const indexer = createIndexer(testDir);
191
- const count = await indexer.indexTeammate("beacon");
192
-
193
- expect(count).toBe(2);
194
- });
195
-
196
- it("returns 0 when no files to index", async () => {
197
- await mkdir(join(testDir, "beacon"), { recursive: true });
198
-
199
- const indexer = createIndexer(testDir);
200
- const count = await indexer.indexTeammate("beacon");
201
- expect(count).toBe(0);
202
- });
203
-
204
- it("skips empty files", async () => {
205
- const beacon = join(testDir, "beacon");
206
- await mkdir(beacon, { recursive: true });
207
- await writeFile(join(beacon, "WISDOM.md"), " "); // whitespace only
208
-
209
- const indexer = createIndexer(testDir);
210
- const count = await indexer.indexTeammate("beacon");
211
- expect(count).toBe(0);
212
- });
213
- });
214
-
215
- describe("indexAll", () => {
216
- it("indexes all discovered teammates", async () => {
217
- const beacon = join(testDir, "beacon");
218
- const scribe = join(testDir, "scribe");
219
- await mkdir(beacon, { recursive: true });
220
- await mkdir(scribe, { recursive: true });
221
- await writeFile(join(beacon, "SOUL.md"), "# Beacon");
222
- await writeFile(join(beacon, "WISDOM.md"), "# Beacon wisdom");
223
- await writeFile(join(scribe, "SOUL.md"), "# Scribe");
224
-
225
- const indexer = createIndexer(testDir);
226
- const results = await indexer.indexAll();
227
-
228
- expect(results.get("beacon")).toBe(1); // WISDOM.md only (SOUL.md not collected)
229
- expect(results.get("scribe")).toBe(0); // no indexable files
230
- });
231
- });
232
-
233
- describe("syncTeammate", () => {
234
- it("falls back to full index when no index exists", async () => {
235
- const beacon = join(testDir, "beacon");
236
- await mkdir(beacon, { recursive: true });
237
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
238
-
239
- const indexer = createIndexer(testDir);
240
- const count = await indexer.syncTeammate("beacon");
241
- expect(count).toBe(1);
242
- });
243
-
244
- it("upserts files into existing index", async () => {
245
- const beacon = join(testDir, "beacon");
246
- const memDir = join(beacon, "memory");
247
- await mkdir(memDir, { recursive: true });
248
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
249
-
250
- const indexer = createIndexer(testDir);
251
- // First build the index
252
- await indexer.indexTeammate("beacon");
253
-
254
- // Add a new file
255
- await writeFile(join(memDir, "project_goals.md"), "# Goals");
256
-
257
- // Sync should pick up the new file
258
- const count = await indexer.syncTeammate("beacon");
259
- expect(count).toBe(2); // WISDOM + project_goals
260
- });
261
- });
262
- });
1
+ import { mkdir, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { Indexer } from "./indexer.js";
6
+
7
+ // Stub embeddings — we don't want to load the real model in tests
8
+ class StubEmbeddings {
9
+ readonly maxTokens = 256;
10
+ async createEmbeddings(inputs: string | string[]) {
11
+ const texts = Array.isArray(inputs) ? inputs : [inputs];
12
+ return {
13
+ status: "success" as const,
14
+ output: texts.map(() => new Array(384).fill(0).map(() => Math.random())),
15
+ };
16
+ }
17
+ }
18
+
19
+ // Create an Indexer with stubbed embeddings
20
+ function createIndexer(teammatesDir: string): Indexer {
21
+ const indexer = new Indexer({ teammatesDir });
22
+ // Swap out the real embeddings with our stub
23
+ (indexer as any)._embeddings = new StubEmbeddings();
24
+ return indexer;
25
+ }
26
+
27
+ let testDir: string;
28
+
29
+ beforeEach(async () => {
30
+ testDir = join(
31
+ tmpdir(),
32
+ `recall-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
33
+ );
34
+ await mkdir(testDir, { recursive: true });
35
+ });
36
+
37
+ afterEach(async () => {
38
+ await rm(testDir, { recursive: true, force: true });
39
+ });
40
+
41
+ describe("Indexer", () => {
42
+ describe("discoverTeammates", () => {
43
+ it("finds directories containing SOUL.md", async () => {
44
+ const beacon = join(testDir, "beacon");
45
+ const scribe = join(testDir, "scribe");
46
+ const notTeammate = join(testDir, "random");
47
+
48
+ await mkdir(beacon, { recursive: true });
49
+ await mkdir(scribe, { recursive: true });
50
+ await mkdir(notTeammate, { recursive: true });
51
+
52
+ await writeFile(join(beacon, "SOUL.md"), "# Beacon");
53
+ await writeFile(join(scribe, "SOUL.md"), "# Scribe");
54
+ // notTeammate has no SOUL.md
55
+
56
+ const indexer = createIndexer(testDir);
57
+ const teammates = await indexer.discoverTeammates();
58
+
59
+ expect(teammates).toContain("beacon");
60
+ expect(teammates).toContain("scribe");
61
+ expect(teammates).not.toContain("random");
62
+ });
63
+
64
+ it("ignores dot-prefixed directories", async () => {
65
+ const hidden = join(testDir, ".tmp");
66
+ await mkdir(hidden, { recursive: true });
67
+ await writeFile(join(hidden, "SOUL.md"), "# Hidden");
68
+
69
+ const indexer = createIndexer(testDir);
70
+ const teammates = await indexer.discoverTeammates();
71
+
72
+ expect(teammates).not.toContain(".tmp");
73
+ expect(teammates).toHaveLength(0);
74
+ });
75
+
76
+ it("returns empty array when no teammates exist", async () => {
77
+ const indexer = createIndexer(testDir);
78
+ const teammates = await indexer.discoverTeammates();
79
+ expect(teammates).toEqual([]);
80
+ });
81
+ });
82
+
83
+ describe("collectFiles", () => {
84
+ it("collects WISDOM.md", async () => {
85
+ const beacon = join(testDir, "beacon");
86
+ await mkdir(beacon, { recursive: true });
87
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
88
+
89
+ const indexer = createIndexer(testDir);
90
+ const { files } = await indexer.collectFiles("beacon");
91
+
92
+ expect(files).toHaveLength(1);
93
+ expect(files[0].uri).toBe("beacon/WISDOM.md");
94
+ });
95
+
96
+ it("collects typed memory files from memory/", async () => {
97
+ const memDir = join(testDir, "beacon", "memory");
98
+ await mkdir(memDir, { recursive: true });
99
+ await writeFile(join(memDir, "feedback_testing.md"), "# Feedback");
100
+ await writeFile(join(memDir, "project_goals.md"), "# Goals");
101
+
102
+ const indexer = createIndexer(testDir);
103
+ const { files } = await indexer.collectFiles("beacon");
104
+
105
+ const uris = files.map((f) => f.uri);
106
+ expect(uris).toContain("beacon/memory/feedback_testing.md");
107
+ expect(uris).toContain("beacon/memory/project_goals.md");
108
+ });
109
+
110
+ it("skips daily logs (YYYY-MM-DD.md pattern)", async () => {
111
+ const memDir = join(testDir, "beacon", "memory");
112
+ await mkdir(memDir, { recursive: true });
113
+ await writeFile(join(memDir, "2026-03-14.md"), "# Day 1");
114
+ await writeFile(join(memDir, "2026-03-15.md"), "# Day 2");
115
+ await writeFile(join(memDir, "feedback_testing.md"), "# Feedback");
116
+
117
+ const indexer = createIndexer(testDir);
118
+ const { files } = await indexer.collectFiles("beacon");
119
+
120
+ const uris = files.map((f) => f.uri);
121
+ expect(uris).not.toContain("beacon/memory/2026-03-14.md");
122
+ expect(uris).not.toContain("beacon/memory/2026-03-15.md");
123
+ expect(uris).toContain("beacon/memory/feedback_testing.md");
124
+ });
125
+
126
+ it("collects weekly summaries from memory/weekly/", async () => {
127
+ const weeklyDir = join(testDir, "beacon", "memory", "weekly");
128
+ await mkdir(weeklyDir, { recursive: true });
129
+ await writeFile(join(weeklyDir, "2026-W10.md"), "# Week 10");
130
+ await writeFile(join(weeklyDir, "2026-W11.md"), "# Week 11");
131
+
132
+ const indexer = createIndexer(testDir);
133
+ const { files } = await indexer.collectFiles("beacon");
134
+
135
+ const uris = files.map((f) => f.uri);
136
+ expect(uris).toContain("beacon/memory/weekly/2026-W10.md");
137
+ expect(uris).toContain("beacon/memory/weekly/2026-W11.md");
138
+ });
139
+
140
+ it("collects monthly summaries from memory/monthly/", async () => {
141
+ const monthlyDir = join(testDir, "beacon", "memory", "monthly");
142
+ await mkdir(monthlyDir, { recursive: true });
143
+ await writeFile(join(monthlyDir, "2025-12.md"), "# Dec 2025");
144
+
145
+ const indexer = createIndexer(testDir);
146
+ const { files } = await indexer.collectFiles("beacon");
147
+
148
+ const uris = files.map((f) => f.uri);
149
+ expect(uris).toContain("beacon/memory/monthly/2025-12.md");
150
+ });
151
+
152
+ it("skips non-md files", async () => {
153
+ const memDir = join(testDir, "beacon", "memory");
154
+ await mkdir(memDir, { recursive: true });
155
+ await writeFile(join(memDir, "notes.txt"), "not markdown");
156
+ await writeFile(join(memDir, "feedback_test.md"), "# Feedback");
157
+
158
+ const indexer = createIndexer(testDir);
159
+ const { files } = await indexer.collectFiles("beacon");
160
+
161
+ expect(files).toHaveLength(1);
162
+ expect(files[0].uri).toBe("beacon/memory/feedback_test.md");
163
+ });
164
+
165
+ it("returns empty files when teammate has no content", async () => {
166
+ await mkdir(join(testDir, "beacon"), { recursive: true });
167
+
168
+ const indexer = createIndexer(testDir);
169
+ const { files } = await indexer.collectFiles("beacon");
170
+ expect(files).toEqual([]);
171
+ });
172
+ });
173
+
174
+ describe("indexPath", () => {
175
+ it("returns correct path under teammate directory", () => {
176
+ const indexer = createIndexer(testDir);
177
+ const p = indexer.indexPath("beacon");
178
+ expect(p).toBe(join(testDir, "beacon", ".index"));
179
+ });
180
+ });
181
+
182
+ describe("indexTeammate", () => {
183
+ it("creates an index and returns file count", async () => {
184
+ const beacon = join(testDir, "beacon");
185
+ const memDir = join(beacon, "memory");
186
+ await mkdir(memDir, { recursive: true });
187
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom content");
188
+ await writeFile(join(memDir, "feedback_test.md"), "# Feedback content");
189
+
190
+ const indexer = createIndexer(testDir);
191
+ const count = await indexer.indexTeammate("beacon");
192
+
193
+ expect(count).toBe(2);
194
+ });
195
+
196
+ it("returns 0 when no files to index", async () => {
197
+ await mkdir(join(testDir, "beacon"), { recursive: true });
198
+
199
+ const indexer = createIndexer(testDir);
200
+ const count = await indexer.indexTeammate("beacon");
201
+ expect(count).toBe(0);
202
+ });
203
+
204
+ it("skips empty files", async () => {
205
+ const beacon = join(testDir, "beacon");
206
+ await mkdir(beacon, { recursive: true });
207
+ await writeFile(join(beacon, "WISDOM.md"), " "); // whitespace only
208
+
209
+ const indexer = createIndexer(testDir);
210
+ const count = await indexer.indexTeammate("beacon");
211
+ expect(count).toBe(0);
212
+ });
213
+ });
214
+
215
+ describe("indexAll", () => {
216
+ it("indexes all discovered teammates", async () => {
217
+ const beacon = join(testDir, "beacon");
218
+ const scribe = join(testDir, "scribe");
219
+ await mkdir(beacon, { recursive: true });
220
+ await mkdir(scribe, { recursive: true });
221
+ await writeFile(join(beacon, "SOUL.md"), "# Beacon");
222
+ await writeFile(join(beacon, "WISDOM.md"), "# Beacon wisdom");
223
+ await writeFile(join(scribe, "SOUL.md"), "# Scribe");
224
+
225
+ const indexer = createIndexer(testDir);
226
+ const results = await indexer.indexAll();
227
+
228
+ expect(results.get("beacon")).toBe(1); // WISDOM.md only (SOUL.md not collected)
229
+ expect(results.get("scribe")).toBe(0); // no indexable files
230
+ });
231
+ });
232
+
233
+ describe("syncTeammate", () => {
234
+ it("falls back to full index when no index exists", async () => {
235
+ const beacon = join(testDir, "beacon");
236
+ await mkdir(beacon, { recursive: true });
237
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
238
+
239
+ const indexer = createIndexer(testDir);
240
+ const count = await indexer.syncTeammate("beacon");
241
+ expect(count).toBe(1);
242
+ });
243
+
244
+ it("upserts files into existing index", async () => {
245
+ const beacon = join(testDir, "beacon");
246
+ const memDir = join(beacon, "memory");
247
+ await mkdir(memDir, { recursive: true });
248
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
249
+
250
+ const indexer = createIndexer(testDir);
251
+ // First build the index
252
+ await indexer.indexTeammate("beacon");
253
+
254
+ // Add a new file
255
+ await writeFile(join(memDir, "project_goals.md"), "# Goals");
256
+
257
+ // Sync should pick up the new file
258
+ const count = await indexer.syncTeammate("beacon");
259
+ expect(count).toBe(2); // WISDOM + project_goals
260
+ });
261
+ });
262
+ });