@teammates/recall 0.3.1 → 0.3.3
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/dist/cli.js +23 -23
- package/package.json +40 -40
- package/src/cli.test.ts +324 -324
- package/src/cli.ts +407 -407
- package/src/embeddings.ts +56 -56
- package/src/index.ts +3 -3
- package/src/indexer.test.ts +262 -262
- package/src/indexer.ts +237 -237
- package/src/search.test.ts +49 -49
- package/src/search.ts +178 -178
- package/tsconfig.json +18 -18
- package/vitest.config.ts +12 -12
package/src/indexer.ts
CHANGED
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { LocalDocumentIndex } from "vectra";
|
|
4
|
-
import { LocalEmbeddings } from "./embeddings.js";
|
|
5
|
-
|
|
6
|
-
export interface IndexerConfig {
|
|
7
|
-
/** Path to the .teammates directory */
|
|
8
|
-
teammatesDir: string;
|
|
9
|
-
/** Embedding model name (default: Xenova/all-MiniLM-L6-v2) */
|
|
10
|
-
model?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface TeammateFiles {
|
|
14
|
-
teammate: string;
|
|
15
|
-
files: { uri: string; absolutePath: string }[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Indexes teammate memory files (WISDOM.md + memory/*.md) into Vectra.
|
|
20
|
-
* One index per teammate, stored at .teammates/<name>/.index/
|
|
21
|
-
*/
|
|
22
|
-
export class Indexer {
|
|
23
|
-
private _config: IndexerConfig;
|
|
24
|
-
private _embeddings: LocalEmbeddings;
|
|
25
|
-
|
|
26
|
-
constructor(config: IndexerConfig) {
|
|
27
|
-
this._config = config;
|
|
28
|
-
this._embeddings = new LocalEmbeddings(config.model);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Get the index path for a specific teammate */
|
|
32
|
-
indexPath(teammate: string): string {
|
|
33
|
-
return path.join(this._config.teammatesDir, teammate, ".index");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Discover all teammate directories (folders containing SOUL.md).
|
|
38
|
-
*/
|
|
39
|
-
async discoverTeammates(): Promise<string[]> {
|
|
40
|
-
const entries = await fs.readdir(this._config.teammatesDir, {
|
|
41
|
-
withFileTypes: true,
|
|
42
|
-
});
|
|
43
|
-
const teammates: string[] = [];
|
|
44
|
-
for (const entry of entries) {
|
|
45
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
46
|
-
const soulPath = path.join(
|
|
47
|
-
this._config.teammatesDir,
|
|
48
|
-
entry.name,
|
|
49
|
-
"SOUL.md",
|
|
50
|
-
);
|
|
51
|
-
try {
|
|
52
|
-
await fs.access(soulPath);
|
|
53
|
-
teammates.push(entry.name);
|
|
54
|
-
} catch {
|
|
55
|
-
// Not a teammate folder
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return teammates;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Collect all indexable memory files for a teammate.
|
|
63
|
-
*/
|
|
64
|
-
async collectFiles(teammate: string): Promise<TeammateFiles> {
|
|
65
|
-
const teammateDir = path.join(this._config.teammatesDir, teammate);
|
|
66
|
-
const files: TeammateFiles["files"] = [];
|
|
67
|
-
|
|
68
|
-
// WISDOM.md
|
|
69
|
-
const wisdomPath = path.join(teammateDir, "WISDOM.md");
|
|
70
|
-
try {
|
|
71
|
-
await fs.access(wisdomPath);
|
|
72
|
-
files.push({ uri: `${teammate}/WISDOM.md`, absolutePath: wisdomPath });
|
|
73
|
-
} catch {
|
|
74
|
-
// No WISDOM.md
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// memory/*.md — typed memories only (skip raw daily logs, they're in prompt context)
|
|
78
|
-
const memoryDir = path.join(teammateDir, "memory");
|
|
79
|
-
try {
|
|
80
|
-
const memoryEntries = await fs.readdir(memoryDir);
|
|
81
|
-
for (const entry of memoryEntries) {
|
|
82
|
-
if (!entry.endsWith(".md")) continue;
|
|
83
|
-
const stem = path.basename(entry, ".md");
|
|
84
|
-
// Skip daily logs (YYYY-MM-DD) — they're already in prompt context
|
|
85
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(stem)) continue;
|
|
86
|
-
files.push({
|
|
87
|
-
uri: `${teammate}/memory/${entry}`,
|
|
88
|
-
absolutePath: path.join(memoryDir, entry),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
// No memory/ directory
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// memory/weekly/*.md — weekly summaries (primary episodic search surface)
|
|
96
|
-
const weeklyDir = path.join(memoryDir, "weekly");
|
|
97
|
-
try {
|
|
98
|
-
const weeklyEntries = await fs.readdir(weeklyDir);
|
|
99
|
-
for (const entry of weeklyEntries) {
|
|
100
|
-
if (!entry.endsWith(".md")) continue;
|
|
101
|
-
files.push({
|
|
102
|
-
uri: `${teammate}/memory/weekly/${entry}`,
|
|
103
|
-
absolutePath: path.join(weeklyDir, entry),
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
// No weekly/ directory
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// memory/monthly/*.md — monthly summaries (long-term episodic context)
|
|
111
|
-
const monthlyDir = path.join(memoryDir, "monthly");
|
|
112
|
-
try {
|
|
113
|
-
const monthlyEntries = await fs.readdir(monthlyDir);
|
|
114
|
-
for (const entry of monthlyEntries) {
|
|
115
|
-
if (!entry.endsWith(".md")) continue;
|
|
116
|
-
files.push({
|
|
117
|
-
uri: `${teammate}/memory/monthly/${entry}`,
|
|
118
|
-
absolutePath: path.join(monthlyDir, entry),
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
} catch {
|
|
122
|
-
// No monthly/ directory
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return { teammate, files };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Build or rebuild the index for a single teammate.
|
|
130
|
-
*/
|
|
131
|
-
async indexTeammate(teammate: string): Promise<number> {
|
|
132
|
-
const { files } = await this.collectFiles(teammate);
|
|
133
|
-
if (files.length === 0) return 0;
|
|
134
|
-
|
|
135
|
-
const indexPath = this.indexPath(teammate);
|
|
136
|
-
const index = new LocalDocumentIndex({
|
|
137
|
-
folderPath: indexPath,
|
|
138
|
-
embeddings: this._embeddings,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Recreate index from scratch
|
|
142
|
-
await index.createIndex({ version: 1, deleteIfExists: true });
|
|
143
|
-
|
|
144
|
-
let count = 0;
|
|
145
|
-
for (const file of files) {
|
|
146
|
-
const text = await fs.readFile(file.absolutePath, "utf-8");
|
|
147
|
-
if (text.trim().length === 0) continue;
|
|
148
|
-
await index.upsertDocument(file.uri, text, "md");
|
|
149
|
-
count++;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return count;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Build or rebuild indexes for all teammates.
|
|
157
|
-
*/
|
|
158
|
-
async indexAll(): Promise<Map<string, number>> {
|
|
159
|
-
const teammates = await this.discoverTeammates();
|
|
160
|
-
const results = new Map<string, number>();
|
|
161
|
-
for (const teammate of teammates) {
|
|
162
|
-
const count = await this.indexTeammate(teammate);
|
|
163
|
-
results.set(teammate, count);
|
|
164
|
-
}
|
|
165
|
-
return results;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Upsert a single file into an existing teammate index.
|
|
170
|
-
* Creates the index if it doesn't exist yet.
|
|
171
|
-
*/
|
|
172
|
-
async upsertFile(teammate: string, filePath: string): Promise<void> {
|
|
173
|
-
const teammateDir = path.join(this._config.teammatesDir, teammate);
|
|
174
|
-
const absolutePath = path.resolve(filePath);
|
|
175
|
-
const relativePath = path.relative(teammateDir, absolutePath);
|
|
176
|
-
const uri = `${teammate}/${relativePath.replace(/\\/g, "/")}`;
|
|
177
|
-
|
|
178
|
-
const text = await fs.readFile(absolutePath, "utf-8");
|
|
179
|
-
if (text.trim().length === 0) return;
|
|
180
|
-
|
|
181
|
-
const indexPath = this.indexPath(teammate);
|
|
182
|
-
const index = new LocalDocumentIndex({
|
|
183
|
-
folderPath: indexPath,
|
|
184
|
-
embeddings: this._embeddings,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
if (!(await index.isIndexCreated())) {
|
|
188
|
-
await index.createIndex({ version: 1 });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
await index.upsertDocument(uri, text, "md");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Sync a teammate's index with their current memory files.
|
|
196
|
-
* Upserts new/changed files without a full rebuild.
|
|
197
|
-
*/
|
|
198
|
-
async syncTeammate(teammate: string): Promise<number> {
|
|
199
|
-
const { files } = await this.collectFiles(teammate);
|
|
200
|
-
if (files.length === 0) return 0;
|
|
201
|
-
|
|
202
|
-
const indexPath = this.indexPath(teammate);
|
|
203
|
-
const index = new LocalDocumentIndex({
|
|
204
|
-
folderPath: indexPath,
|
|
205
|
-
embeddings: this._embeddings,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
if (!(await index.isIndexCreated())) {
|
|
209
|
-
// No index yet — do a full build
|
|
210
|
-
return this.indexTeammate(teammate);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Upsert all files (Vectra handles dedup internally via URI)
|
|
214
|
-
let count = 0;
|
|
215
|
-
for (const file of files) {
|
|
216
|
-
const text = await fs.readFile(file.absolutePath, "utf-8");
|
|
217
|
-
if (text.trim().length === 0) continue;
|
|
218
|
-
await index.upsertDocument(file.uri, text, "md");
|
|
219
|
-
count++;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return count;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Sync indexes for all teammates.
|
|
227
|
-
*/
|
|
228
|
-
async syncAll(): Promise<Map<string, number>> {
|
|
229
|
-
const teammates = await this.discoverTeammates();
|
|
230
|
-
const results = new Map<string, number>();
|
|
231
|
-
for (const teammate of teammates) {
|
|
232
|
-
const count = await this.syncTeammate(teammate);
|
|
233
|
-
results.set(teammate, count);
|
|
234
|
-
}
|
|
235
|
-
return results;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { LocalDocumentIndex } from "vectra";
|
|
4
|
+
import { LocalEmbeddings } from "./embeddings.js";
|
|
5
|
+
|
|
6
|
+
export interface IndexerConfig {
|
|
7
|
+
/** Path to the .teammates directory */
|
|
8
|
+
teammatesDir: string;
|
|
9
|
+
/** Embedding model name (default: Xenova/all-MiniLM-L6-v2) */
|
|
10
|
+
model?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TeammateFiles {
|
|
14
|
+
teammate: string;
|
|
15
|
+
files: { uri: string; absolutePath: string }[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Indexes teammate memory files (WISDOM.md + memory/*.md) into Vectra.
|
|
20
|
+
* One index per teammate, stored at .teammates/<name>/.index/
|
|
21
|
+
*/
|
|
22
|
+
export class Indexer {
|
|
23
|
+
private _config: IndexerConfig;
|
|
24
|
+
private _embeddings: LocalEmbeddings;
|
|
25
|
+
|
|
26
|
+
constructor(config: IndexerConfig) {
|
|
27
|
+
this._config = config;
|
|
28
|
+
this._embeddings = new LocalEmbeddings(config.model);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Get the index path for a specific teammate */
|
|
32
|
+
indexPath(teammate: string): string {
|
|
33
|
+
return path.join(this._config.teammatesDir, teammate, ".index");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Discover all teammate directories (folders containing SOUL.md).
|
|
38
|
+
*/
|
|
39
|
+
async discoverTeammates(): Promise<string[]> {
|
|
40
|
+
const entries = await fs.readdir(this._config.teammatesDir, {
|
|
41
|
+
withFileTypes: true,
|
|
42
|
+
});
|
|
43
|
+
const teammates: string[] = [];
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
46
|
+
const soulPath = path.join(
|
|
47
|
+
this._config.teammatesDir,
|
|
48
|
+
entry.name,
|
|
49
|
+
"SOUL.md",
|
|
50
|
+
);
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(soulPath);
|
|
53
|
+
teammates.push(entry.name);
|
|
54
|
+
} catch {
|
|
55
|
+
// Not a teammate folder
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return teammates;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Collect all indexable memory files for a teammate.
|
|
63
|
+
*/
|
|
64
|
+
async collectFiles(teammate: string): Promise<TeammateFiles> {
|
|
65
|
+
const teammateDir = path.join(this._config.teammatesDir, teammate);
|
|
66
|
+
const files: TeammateFiles["files"] = [];
|
|
67
|
+
|
|
68
|
+
// WISDOM.md
|
|
69
|
+
const wisdomPath = path.join(teammateDir, "WISDOM.md");
|
|
70
|
+
try {
|
|
71
|
+
await fs.access(wisdomPath);
|
|
72
|
+
files.push({ uri: `${teammate}/WISDOM.md`, absolutePath: wisdomPath });
|
|
73
|
+
} catch {
|
|
74
|
+
// No WISDOM.md
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// memory/*.md — typed memories only (skip raw daily logs, they're in prompt context)
|
|
78
|
+
const memoryDir = path.join(teammateDir, "memory");
|
|
79
|
+
try {
|
|
80
|
+
const memoryEntries = await fs.readdir(memoryDir);
|
|
81
|
+
for (const entry of memoryEntries) {
|
|
82
|
+
if (!entry.endsWith(".md")) continue;
|
|
83
|
+
const stem = path.basename(entry, ".md");
|
|
84
|
+
// Skip daily logs (YYYY-MM-DD) — they're already in prompt context
|
|
85
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(stem)) continue;
|
|
86
|
+
files.push({
|
|
87
|
+
uri: `${teammate}/memory/${entry}`,
|
|
88
|
+
absolutePath: path.join(memoryDir, entry),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// No memory/ directory
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// memory/weekly/*.md — weekly summaries (primary episodic search surface)
|
|
96
|
+
const weeklyDir = path.join(memoryDir, "weekly");
|
|
97
|
+
try {
|
|
98
|
+
const weeklyEntries = await fs.readdir(weeklyDir);
|
|
99
|
+
for (const entry of weeklyEntries) {
|
|
100
|
+
if (!entry.endsWith(".md")) continue;
|
|
101
|
+
files.push({
|
|
102
|
+
uri: `${teammate}/memory/weekly/${entry}`,
|
|
103
|
+
absolutePath: path.join(weeklyDir, entry),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// No weekly/ directory
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// memory/monthly/*.md — monthly summaries (long-term episodic context)
|
|
111
|
+
const monthlyDir = path.join(memoryDir, "monthly");
|
|
112
|
+
try {
|
|
113
|
+
const monthlyEntries = await fs.readdir(monthlyDir);
|
|
114
|
+
for (const entry of monthlyEntries) {
|
|
115
|
+
if (!entry.endsWith(".md")) continue;
|
|
116
|
+
files.push({
|
|
117
|
+
uri: `${teammate}/memory/monthly/${entry}`,
|
|
118
|
+
absolutePath: path.join(monthlyDir, entry),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// No monthly/ directory
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { teammate, files };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build or rebuild the index for a single teammate.
|
|
130
|
+
*/
|
|
131
|
+
async indexTeammate(teammate: string): Promise<number> {
|
|
132
|
+
const { files } = await this.collectFiles(teammate);
|
|
133
|
+
if (files.length === 0) return 0;
|
|
134
|
+
|
|
135
|
+
const indexPath = this.indexPath(teammate);
|
|
136
|
+
const index = new LocalDocumentIndex({
|
|
137
|
+
folderPath: indexPath,
|
|
138
|
+
embeddings: this._embeddings,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Recreate index from scratch
|
|
142
|
+
await index.createIndex({ version: 1, deleteIfExists: true });
|
|
143
|
+
|
|
144
|
+
let count = 0;
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const text = await fs.readFile(file.absolutePath, "utf-8");
|
|
147
|
+
if (text.trim().length === 0) continue;
|
|
148
|
+
await index.upsertDocument(file.uri, text, "md");
|
|
149
|
+
count++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return count;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build or rebuild indexes for all teammates.
|
|
157
|
+
*/
|
|
158
|
+
async indexAll(): Promise<Map<string, number>> {
|
|
159
|
+
const teammates = await this.discoverTeammates();
|
|
160
|
+
const results = new Map<string, number>();
|
|
161
|
+
for (const teammate of teammates) {
|
|
162
|
+
const count = await this.indexTeammate(teammate);
|
|
163
|
+
results.set(teammate, count);
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Upsert a single file into an existing teammate index.
|
|
170
|
+
* Creates the index if it doesn't exist yet.
|
|
171
|
+
*/
|
|
172
|
+
async upsertFile(teammate: string, filePath: string): Promise<void> {
|
|
173
|
+
const teammateDir = path.join(this._config.teammatesDir, teammate);
|
|
174
|
+
const absolutePath = path.resolve(filePath);
|
|
175
|
+
const relativePath = path.relative(teammateDir, absolutePath);
|
|
176
|
+
const uri = `${teammate}/${relativePath.replace(/\\/g, "/")}`;
|
|
177
|
+
|
|
178
|
+
const text = await fs.readFile(absolutePath, "utf-8");
|
|
179
|
+
if (text.trim().length === 0) return;
|
|
180
|
+
|
|
181
|
+
const indexPath = this.indexPath(teammate);
|
|
182
|
+
const index = new LocalDocumentIndex({
|
|
183
|
+
folderPath: indexPath,
|
|
184
|
+
embeddings: this._embeddings,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!(await index.isIndexCreated())) {
|
|
188
|
+
await index.createIndex({ version: 1 });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await index.upsertDocument(uri, text, "md");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Sync a teammate's index with their current memory files.
|
|
196
|
+
* Upserts new/changed files without a full rebuild.
|
|
197
|
+
*/
|
|
198
|
+
async syncTeammate(teammate: string): Promise<number> {
|
|
199
|
+
const { files } = await this.collectFiles(teammate);
|
|
200
|
+
if (files.length === 0) return 0;
|
|
201
|
+
|
|
202
|
+
const indexPath = this.indexPath(teammate);
|
|
203
|
+
const index = new LocalDocumentIndex({
|
|
204
|
+
folderPath: indexPath,
|
|
205
|
+
embeddings: this._embeddings,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!(await index.isIndexCreated())) {
|
|
209
|
+
// No index yet — do a full build
|
|
210
|
+
return this.indexTeammate(teammate);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Upsert all files (Vectra handles dedup internally via URI)
|
|
214
|
+
let count = 0;
|
|
215
|
+
for (const file of files) {
|
|
216
|
+
const text = await fs.readFile(file.absolutePath, "utf-8");
|
|
217
|
+
if (text.trim().length === 0) continue;
|
|
218
|
+
await index.upsertDocument(file.uri, text, "md");
|
|
219
|
+
count++;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return count;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Sync indexes for all teammates.
|
|
227
|
+
*/
|
|
228
|
+
async syncAll(): Promise<Map<string, number>> {
|
|
229
|
+
const teammates = await this.discoverTeammates();
|
|
230
|
+
const results = new Map<string, number>();
|
|
231
|
+
for (const teammate of teammates) {
|
|
232
|
+
const count = await this.syncTeammate(teammate);
|
|
233
|
+
results.set(teammate, count);
|
|
234
|
+
}
|
|
235
|
+
return results;
|
|
236
|
+
}
|
|
237
|
+
}
|
package/src/search.test.ts
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
// classifyUri is not exported, so we test it indirectly via a re-implementation
|
|
4
|
-
// or we can import the module and test the search function behavior.
|
|
5
|
-
// Since classifyUri is a pure function, let's extract and test its logic.
|
|
6
|
-
|
|
7
|
-
describe("classifyUri", () => {
|
|
8
|
-
// Re-implement the classification logic for unit testing
|
|
9
|
-
function classifyUri(uri: string): string {
|
|
10
|
-
if (uri.includes("/memory/weekly/")) return "weekly";
|
|
11
|
-
if (uri.includes("/memory/monthly/")) return "monthly";
|
|
12
|
-
const memoryMatch = uri.match(/\/memory\/([^/]+)\.md$/);
|
|
13
|
-
if (memoryMatch) {
|
|
14
|
-
const stem = memoryMatch[1];
|
|
15
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(stem)) return "daily";
|
|
16
|
-
return "typed_memory";
|
|
17
|
-
}
|
|
18
|
-
return "other";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
it("classifies weekly summaries", () => {
|
|
22
|
-
expect(classifyUri("beacon/memory/weekly/2026-W10.md")).toBe("weekly");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("classifies monthly summaries", () => {
|
|
26
|
-
expect(classifyUri("beacon/memory/monthly/2025-12.md")).toBe("monthly");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("classifies typed memories", () => {
|
|
30
|
-
expect(classifyUri("beacon/memory/feedback_testing.md")).toBe(
|
|
31
|
-
"typed_memory",
|
|
32
|
-
);
|
|
33
|
-
expect(classifyUri("beacon/memory/project_goals.md")).toBe("typed_memory");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("classifies daily logs", () => {
|
|
37
|
-
expect(classifyUri("beacon/memory/2026-03-14.md")).toBe("daily");
|
|
38
|
-
expect(classifyUri("beacon/memory/2026-01-01.md")).toBe("daily");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("classifies WISDOM.md as other", () => {
|
|
42
|
-
expect(classifyUri("beacon/WISDOM.md")).toBe("other");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("classifies non-memory paths as other", () => {
|
|
46
|
-
expect(classifyUri("beacon/SOUL.md")).toBe("other");
|
|
47
|
-
expect(classifyUri("beacon/notes/todo.md")).toBe("other");
|
|
48
|
-
});
|
|
49
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
// classifyUri is not exported, so we test it indirectly via a re-implementation
|
|
4
|
+
// or we can import the module and test the search function behavior.
|
|
5
|
+
// Since classifyUri is a pure function, let's extract and test its logic.
|
|
6
|
+
|
|
7
|
+
describe("classifyUri", () => {
|
|
8
|
+
// Re-implement the classification logic for unit testing
|
|
9
|
+
function classifyUri(uri: string): string {
|
|
10
|
+
if (uri.includes("/memory/weekly/")) return "weekly";
|
|
11
|
+
if (uri.includes("/memory/monthly/")) return "monthly";
|
|
12
|
+
const memoryMatch = uri.match(/\/memory\/([^/]+)\.md$/);
|
|
13
|
+
if (memoryMatch) {
|
|
14
|
+
const stem = memoryMatch[1];
|
|
15
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(stem)) return "daily";
|
|
16
|
+
return "typed_memory";
|
|
17
|
+
}
|
|
18
|
+
return "other";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
it("classifies weekly summaries", () => {
|
|
22
|
+
expect(classifyUri("beacon/memory/weekly/2026-W10.md")).toBe("weekly");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("classifies monthly summaries", () => {
|
|
26
|
+
expect(classifyUri("beacon/memory/monthly/2025-12.md")).toBe("monthly");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("classifies typed memories", () => {
|
|
30
|
+
expect(classifyUri("beacon/memory/feedback_testing.md")).toBe(
|
|
31
|
+
"typed_memory",
|
|
32
|
+
);
|
|
33
|
+
expect(classifyUri("beacon/memory/project_goals.md")).toBe("typed_memory");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("classifies daily logs", () => {
|
|
37
|
+
expect(classifyUri("beacon/memory/2026-03-14.md")).toBe("daily");
|
|
38
|
+
expect(classifyUri("beacon/memory/2026-01-01.md")).toBe("daily");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("classifies WISDOM.md as other", () => {
|
|
42
|
+
expect(classifyUri("beacon/WISDOM.md")).toBe("other");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("classifies non-memory paths as other", () => {
|
|
46
|
+
expect(classifyUri("beacon/SOUL.md")).toBe("other");
|
|
47
|
+
expect(classifyUri("beacon/notes/todo.md")).toBe("other");
|
|
48
|
+
});
|
|
49
|
+
});
|