@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/cli.ts
CHANGED
|
@@ -1,407 +1,407 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { type FSWatcher, watch as fsWatch } from "node:fs";
|
|
4
|
-
import * as fs from "node:fs/promises";
|
|
5
|
-
import * as path from "node:path";
|
|
6
|
-
import { Indexer } from "./indexer.js";
|
|
7
|
-
import { search } from "./search.js";
|
|
8
|
-
|
|
9
|
-
const HELP = `
|
|
10
|
-
teammates-recall — Semantic memory search for teammates
|
|
11
|
-
|
|
12
|
-
Usage:
|
|
13
|
-
teammates-recall index [options] Full rebuild of all indexes
|
|
14
|
-
teammates-recall sync [options] Sync new/changed files into indexes
|
|
15
|
-
teammates-recall add <file> [options] Add a single file to a teammate's index
|
|
16
|
-
teammates-recall search <query> [options] Search teammate memories (auto-syncs)
|
|
17
|
-
teammates-recall status [options] Show index status
|
|
18
|
-
teammates-recall watch [options] Watch for changes and auto-sync
|
|
19
|
-
|
|
20
|
-
Options:
|
|
21
|
-
--dir <path> Path to .teammates directory (default: ./.teammates)
|
|
22
|
-
--teammate <name> Limit to a specific teammate
|
|
23
|
-
--results <n> Max results (default: 5)
|
|
24
|
-
--max-chunks <n> Max chunks per document (default: 3)
|
|
25
|
-
--max-tokens <n> Max tokens per section (default: 500)
|
|
26
|
-
--recency-depth <n> Number of recent weekly summaries to include (default: 2)
|
|
27
|
-
--typed-memory-boost <n> Relevance boost for typed memories (default: 1.2)
|
|
28
|
-
--model <name> Embedding model (default: Xenova/all-MiniLM-L6-v2)
|
|
29
|
-
--no-sync Skip auto-sync before search
|
|
30
|
-
--json Output as JSON
|
|
31
|
-
--help Show this help
|
|
32
|
-
`.trim();
|
|
33
|
-
|
|
34
|
-
interface Args {
|
|
35
|
-
command: string;
|
|
36
|
-
query: string;
|
|
37
|
-
file: string;
|
|
38
|
-
dir: string;
|
|
39
|
-
teammate?: string;
|
|
40
|
-
results: number;
|
|
41
|
-
maxChunks?: number;
|
|
42
|
-
maxTokens?: number;
|
|
43
|
-
recencyDepth?: number;
|
|
44
|
-
typedMemoryBoost?: number;
|
|
45
|
-
model?: string;
|
|
46
|
-
json: boolean;
|
|
47
|
-
sync: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function parseArgs(argv: string[]): Args {
|
|
51
|
-
const args: Args = {
|
|
52
|
-
command: "",
|
|
53
|
-
query: "",
|
|
54
|
-
file: "",
|
|
55
|
-
dir: "./.teammates",
|
|
56
|
-
results: 5,
|
|
57
|
-
json: false,
|
|
58
|
-
sync: true,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
let i = 0;
|
|
62
|
-
// Skip node and script path
|
|
63
|
-
while (
|
|
64
|
-
i < argv.length &&
|
|
65
|
-
(argv[i].includes("node") ||
|
|
66
|
-
argv[i].includes("teammates-recall") ||
|
|
67
|
-
argv[i].endsWith(".js"))
|
|
68
|
-
) {
|
|
69
|
-
i++;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (i < argv.length && !argv[i].startsWith("-")) {
|
|
73
|
-
args.command = argv[i++];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// For search, next non-flag arg is the query; for add, it's the file path
|
|
77
|
-
if (
|
|
78
|
-
args.command === "search" &&
|
|
79
|
-
i < argv.length &&
|
|
80
|
-
!argv[i].startsWith("-")
|
|
81
|
-
) {
|
|
82
|
-
args.query = argv[i++];
|
|
83
|
-
} else if (
|
|
84
|
-
args.command === "add" &&
|
|
85
|
-
i < argv.length &&
|
|
86
|
-
!argv[i].startsWith("-")
|
|
87
|
-
) {
|
|
88
|
-
args.file = argv[i++];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
while (i < argv.length) {
|
|
92
|
-
const arg = argv[i++];
|
|
93
|
-
switch (arg) {
|
|
94
|
-
case "--dir":
|
|
95
|
-
args.dir = argv[i++];
|
|
96
|
-
break;
|
|
97
|
-
case "--teammate":
|
|
98
|
-
args.teammate = argv[i++];
|
|
99
|
-
break;
|
|
100
|
-
case "--results":
|
|
101
|
-
args.results = parseInt(argv[i++], 10);
|
|
102
|
-
break;
|
|
103
|
-
case "--model":
|
|
104
|
-
args.model = argv[i++];
|
|
105
|
-
break;
|
|
106
|
-
case "--max-chunks":
|
|
107
|
-
args.maxChunks = parseInt(argv[i++], 10);
|
|
108
|
-
break;
|
|
109
|
-
case "--max-tokens":
|
|
110
|
-
args.maxTokens = parseInt(argv[i++], 10);
|
|
111
|
-
break;
|
|
112
|
-
case "--recency-depth":
|
|
113
|
-
args.recencyDepth = parseInt(argv[i++], 10);
|
|
114
|
-
break;
|
|
115
|
-
case "--typed-memory-boost":
|
|
116
|
-
args.typedMemoryBoost = parseFloat(argv[i++]);
|
|
117
|
-
break;
|
|
118
|
-
case "--no-sync":
|
|
119
|
-
args.sync = false;
|
|
120
|
-
break;
|
|
121
|
-
case "--json":
|
|
122
|
-
args.json = true;
|
|
123
|
-
break;
|
|
124
|
-
case "--help":
|
|
125
|
-
case "-h":
|
|
126
|
-
console.log(HELP);
|
|
127
|
-
process.exit(0);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return args;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function resolveTeammatesDir(dir: string): Promise<string> {
|
|
135
|
-
const resolved = path.resolve(dir);
|
|
136
|
-
try {
|
|
137
|
-
await fs.access(resolved);
|
|
138
|
-
return resolved;
|
|
139
|
-
} catch {
|
|
140
|
-
console.error(`Error: .teammates directory not found at ${resolved}`);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function cmdIndex(args: Args): Promise<void> {
|
|
146
|
-
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
147
|
-
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
148
|
-
|
|
149
|
-
if (args.teammate) {
|
|
150
|
-
console.error(`Indexing ${args.teammate}...`);
|
|
151
|
-
const count = await indexer.indexTeammate(args.teammate);
|
|
152
|
-
if (args.json) {
|
|
153
|
-
console.log(JSON.stringify({ teammate: args.teammate, files: count }));
|
|
154
|
-
} else {
|
|
155
|
-
console.log(`Indexed ${count} files for ${args.teammate}`);
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
console.error("Indexing all teammates...");
|
|
159
|
-
const results = await indexer.indexAll();
|
|
160
|
-
if (args.json) {
|
|
161
|
-
const obj = Object.fromEntries(results);
|
|
162
|
-
console.log(JSON.stringify(obj));
|
|
163
|
-
} else {
|
|
164
|
-
for (const [teammate, count] of results) {
|
|
165
|
-
console.log(` ${teammate}: ${count} files`);
|
|
166
|
-
}
|
|
167
|
-
console.log(`Done.`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function cmdSync(args: Args): Promise<void> {
|
|
173
|
-
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
174
|
-
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
175
|
-
|
|
176
|
-
if (args.teammate) {
|
|
177
|
-
console.error(`Syncing ${args.teammate}...`);
|
|
178
|
-
const count = await indexer.syncTeammate(args.teammate);
|
|
179
|
-
if (args.json) {
|
|
180
|
-
console.log(JSON.stringify({ teammate: args.teammate, files: count }));
|
|
181
|
-
} else {
|
|
182
|
-
console.log(`Synced ${count} files for ${args.teammate}`);
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
console.error("Syncing all teammates...");
|
|
186
|
-
const results = await indexer.syncAll();
|
|
187
|
-
if (args.json) {
|
|
188
|
-
const obj = Object.fromEntries(results);
|
|
189
|
-
console.log(JSON.stringify(obj));
|
|
190
|
-
} else {
|
|
191
|
-
for (const [teammate, count] of results) {
|
|
192
|
-
console.log(` ${teammate}: ${count} files`);
|
|
193
|
-
}
|
|
194
|
-
console.log(`Done.`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function cmdAdd(args: Args): Promise<void> {
|
|
200
|
-
if (!args.file) {
|
|
201
|
-
console.error("Error: add requires a file path argument");
|
|
202
|
-
console.error("Usage: teammates-recall add <file> --teammate <name>");
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
if (!args.teammate) {
|
|
206
|
-
console.error("Error: add requires --teammate <name>");
|
|
207
|
-
process.exit(1);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
211
|
-
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
212
|
-
await indexer.upsertFile(args.teammate, args.file);
|
|
213
|
-
|
|
214
|
-
if (args.json) {
|
|
215
|
-
console.log(
|
|
216
|
-
JSON.stringify({
|
|
217
|
-
teammate: args.teammate,
|
|
218
|
-
file: args.file,
|
|
219
|
-
status: "ok",
|
|
220
|
-
}),
|
|
221
|
-
);
|
|
222
|
-
} else {
|
|
223
|
-
console.log(`Added ${args.file} to ${args.teammate}'s index`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function cmdSearch(args: Args): Promise<void> {
|
|
228
|
-
if (!args.query) {
|
|
229
|
-
console.error("Error: search requires a query argument");
|
|
230
|
-
console.error("Usage: teammates-recall search <query> [options]");
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
235
|
-
const results = await search(args.query, {
|
|
236
|
-
teammatesDir,
|
|
237
|
-
teammate: args.teammate,
|
|
238
|
-
maxResults: args.results,
|
|
239
|
-
maxChunks: args.maxChunks,
|
|
240
|
-
maxTokens: args.maxTokens,
|
|
241
|
-
recencyDepth: args.recencyDepth,
|
|
242
|
-
typedMemoryBoost: args.typedMemoryBoost,
|
|
243
|
-
model: args.model,
|
|
244
|
-
skipSync: !args.sync,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
if (args.json) {
|
|
248
|
-
console.log(JSON.stringify(results, null, 2));
|
|
249
|
-
} else {
|
|
250
|
-
if (results.length === 0) {
|
|
251
|
-
console.log("No results found.");
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
for (const result of results) {
|
|
255
|
-
console.log(
|
|
256
|
-
`--- ${result.teammate} | ${result.uri} (score: ${result.score.toFixed(3)}) ---`,
|
|
257
|
-
);
|
|
258
|
-
console.log(result.text);
|
|
259
|
-
console.log();
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function cmdStatus(args: Args): Promise<void> {
|
|
265
|
-
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
266
|
-
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
267
|
-
const teammates = await indexer.discoverTeammates();
|
|
268
|
-
|
|
269
|
-
const status: Record<string, { memoryFiles: number; indexed: boolean }> = {};
|
|
270
|
-
|
|
271
|
-
for (const teammate of teammates) {
|
|
272
|
-
const { files } = await indexer.collectFiles(teammate);
|
|
273
|
-
const indexPath = indexer.indexPath(teammate);
|
|
274
|
-
let indexed = false;
|
|
275
|
-
try {
|
|
276
|
-
await fs.access(indexPath);
|
|
277
|
-
indexed = true;
|
|
278
|
-
} catch {
|
|
279
|
-
// Not indexed
|
|
280
|
-
}
|
|
281
|
-
status[teammate] = { memoryFiles: files.length, indexed };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (args.json) {
|
|
285
|
-
console.log(JSON.stringify(status, null, 2));
|
|
286
|
-
} else {
|
|
287
|
-
for (const [teammate, info] of Object.entries(status)) {
|
|
288
|
-
const tag = info.indexed ? "indexed" : "not indexed";
|
|
289
|
-
console.log(` ${teammate}: ${info.memoryFiles} memory files (${tag})`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function cmdWatch(args: Args): Promise<void> {
|
|
295
|
-
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
296
|
-
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
297
|
-
|
|
298
|
-
// Initial sync
|
|
299
|
-
console.error("Initial sync...");
|
|
300
|
-
const results = await indexer.syncAll();
|
|
301
|
-
for (const [teammate, count] of results) {
|
|
302
|
-
console.error(` ${teammate}: ${count} files`);
|
|
303
|
-
}
|
|
304
|
-
console.error("Watching for changes...");
|
|
305
|
-
|
|
306
|
-
if (args.json) {
|
|
307
|
-
console.log(JSON.stringify({ status: "watching", dir: teammatesDir }));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Debounce: collect changes, sync after 2s of quiet
|
|
311
|
-
let syncTimer: ReturnType<typeof setTimeout> | null = null;
|
|
312
|
-
const pendingTeammates = new Set<string>();
|
|
313
|
-
|
|
314
|
-
const scheduleSync = (teammate: string) => {
|
|
315
|
-
pendingTeammates.add(teammate);
|
|
316
|
-
if (syncTimer) clearTimeout(syncTimer);
|
|
317
|
-
syncTimer = setTimeout(async () => {
|
|
318
|
-
for (const t of pendingTeammates) {
|
|
319
|
-
try {
|
|
320
|
-
const count = await indexer.syncTeammate(t);
|
|
321
|
-
if (args.json) {
|
|
322
|
-
console.log(
|
|
323
|
-
JSON.stringify({ event: "sync", teammate: t, files: count }),
|
|
324
|
-
);
|
|
325
|
-
} else {
|
|
326
|
-
console.error(` synced ${t}: ${count} files`);
|
|
327
|
-
}
|
|
328
|
-
} catch (err: unknown) {
|
|
329
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
330
|
-
console.error(` error syncing ${t}: ${msg}`);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
pendingTeammates.clear();
|
|
334
|
-
}, 2000);
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
// Watch each teammate's directory for changes
|
|
338
|
-
const watchers: FSWatcher[] = [];
|
|
339
|
-
const teammates = await indexer.discoverTeammates();
|
|
340
|
-
|
|
341
|
-
for (const teammate of teammates) {
|
|
342
|
-
const teammateDir = path.join(teammatesDir, teammate);
|
|
343
|
-
try {
|
|
344
|
-
const watcher = fsWatch(
|
|
345
|
-
teammateDir,
|
|
346
|
-
{ recursive: true },
|
|
347
|
-
(_eventType, filename) => {
|
|
348
|
-
if (!filename) return;
|
|
349
|
-
// Only care about .md files, skip .index/
|
|
350
|
-
if (!filename.endsWith(".md") || filename.includes(".index")) return;
|
|
351
|
-
scheduleSync(teammate);
|
|
352
|
-
},
|
|
353
|
-
);
|
|
354
|
-
watchers.push(watcher);
|
|
355
|
-
} catch {
|
|
356
|
-
console.error(` warning: could not watch ${teammate}/`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Keep alive until killed
|
|
361
|
-
const shutdown = () => {
|
|
362
|
-
if (syncTimer) clearTimeout(syncTimer);
|
|
363
|
-
for (const w of watchers) w.close();
|
|
364
|
-
if (args.json) {
|
|
365
|
-
console.log(JSON.stringify({ status: "stopped" }));
|
|
366
|
-
}
|
|
367
|
-
process.exit(0);
|
|
368
|
-
};
|
|
369
|
-
process.on("SIGTERM", shutdown);
|
|
370
|
-
process.on("SIGINT", shutdown);
|
|
371
|
-
|
|
372
|
-
// Block forever
|
|
373
|
-
await new Promise(() => {});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function main(): Promise<void> {
|
|
377
|
-
const args = parseArgs(process.argv);
|
|
378
|
-
|
|
379
|
-
switch (args.command) {
|
|
380
|
-
case "index":
|
|
381
|
-
await cmdIndex(args);
|
|
382
|
-
break;
|
|
383
|
-
case "sync":
|
|
384
|
-
await cmdSync(args);
|
|
385
|
-
break;
|
|
386
|
-
case "add":
|
|
387
|
-
await cmdAdd(args);
|
|
388
|
-
break;
|
|
389
|
-
case "search":
|
|
390
|
-
await cmdSearch(args);
|
|
391
|
-
break;
|
|
392
|
-
case "status":
|
|
393
|
-
await cmdStatus(args);
|
|
394
|
-
break;
|
|
395
|
-
case "watch":
|
|
396
|
-
await cmdWatch(args);
|
|
397
|
-
break;
|
|
398
|
-
default:
|
|
399
|
-
console.log(HELP);
|
|
400
|
-
process.exit(args.command ? 1 : 0);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
main().catch((err) => {
|
|
405
|
-
console.error(err.message);
|
|
406
|
-
process.exit(1);
|
|
407
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { type FSWatcher, watch as fsWatch } from "node:fs";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { Indexer } from "./indexer.js";
|
|
7
|
+
import { search } from "./search.js";
|
|
8
|
+
|
|
9
|
+
const HELP = `
|
|
10
|
+
teammates-recall — Semantic memory search for teammates
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
teammates-recall index [options] Full rebuild of all indexes
|
|
14
|
+
teammates-recall sync [options] Sync new/changed files into indexes
|
|
15
|
+
teammates-recall add <file> [options] Add a single file to a teammate's index
|
|
16
|
+
teammates-recall search <query> [options] Search teammate memories (auto-syncs)
|
|
17
|
+
teammates-recall status [options] Show index status
|
|
18
|
+
teammates-recall watch [options] Watch for changes and auto-sync
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--dir <path> Path to .teammates directory (default: ./.teammates)
|
|
22
|
+
--teammate <name> Limit to a specific teammate
|
|
23
|
+
--results <n> Max results (default: 5)
|
|
24
|
+
--max-chunks <n> Max chunks per document (default: 3)
|
|
25
|
+
--max-tokens <n> Max tokens per section (default: 500)
|
|
26
|
+
--recency-depth <n> Number of recent weekly summaries to include (default: 2)
|
|
27
|
+
--typed-memory-boost <n> Relevance boost for typed memories (default: 1.2)
|
|
28
|
+
--model <name> Embedding model (default: Xenova/all-MiniLM-L6-v2)
|
|
29
|
+
--no-sync Skip auto-sync before search
|
|
30
|
+
--json Output as JSON
|
|
31
|
+
--help Show this help
|
|
32
|
+
`.trim();
|
|
33
|
+
|
|
34
|
+
interface Args {
|
|
35
|
+
command: string;
|
|
36
|
+
query: string;
|
|
37
|
+
file: string;
|
|
38
|
+
dir: string;
|
|
39
|
+
teammate?: string;
|
|
40
|
+
results: number;
|
|
41
|
+
maxChunks?: number;
|
|
42
|
+
maxTokens?: number;
|
|
43
|
+
recencyDepth?: number;
|
|
44
|
+
typedMemoryBoost?: number;
|
|
45
|
+
model?: string;
|
|
46
|
+
json: boolean;
|
|
47
|
+
sync: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseArgs(argv: string[]): Args {
|
|
51
|
+
const args: Args = {
|
|
52
|
+
command: "",
|
|
53
|
+
query: "",
|
|
54
|
+
file: "",
|
|
55
|
+
dir: "./.teammates",
|
|
56
|
+
results: 5,
|
|
57
|
+
json: false,
|
|
58
|
+
sync: true,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let i = 0;
|
|
62
|
+
// Skip node and script path
|
|
63
|
+
while (
|
|
64
|
+
i < argv.length &&
|
|
65
|
+
(argv[i].includes("node") ||
|
|
66
|
+
argv[i].includes("teammates-recall") ||
|
|
67
|
+
argv[i].endsWith(".js"))
|
|
68
|
+
) {
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (i < argv.length && !argv[i].startsWith("-")) {
|
|
73
|
+
args.command = argv[i++];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// For search, next non-flag arg is the query; for add, it's the file path
|
|
77
|
+
if (
|
|
78
|
+
args.command === "search" &&
|
|
79
|
+
i < argv.length &&
|
|
80
|
+
!argv[i].startsWith("-")
|
|
81
|
+
) {
|
|
82
|
+
args.query = argv[i++];
|
|
83
|
+
} else if (
|
|
84
|
+
args.command === "add" &&
|
|
85
|
+
i < argv.length &&
|
|
86
|
+
!argv[i].startsWith("-")
|
|
87
|
+
) {
|
|
88
|
+
args.file = argv[i++];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
while (i < argv.length) {
|
|
92
|
+
const arg = argv[i++];
|
|
93
|
+
switch (arg) {
|
|
94
|
+
case "--dir":
|
|
95
|
+
args.dir = argv[i++];
|
|
96
|
+
break;
|
|
97
|
+
case "--teammate":
|
|
98
|
+
args.teammate = argv[i++];
|
|
99
|
+
break;
|
|
100
|
+
case "--results":
|
|
101
|
+
args.results = parseInt(argv[i++], 10);
|
|
102
|
+
break;
|
|
103
|
+
case "--model":
|
|
104
|
+
args.model = argv[i++];
|
|
105
|
+
break;
|
|
106
|
+
case "--max-chunks":
|
|
107
|
+
args.maxChunks = parseInt(argv[i++], 10);
|
|
108
|
+
break;
|
|
109
|
+
case "--max-tokens":
|
|
110
|
+
args.maxTokens = parseInt(argv[i++], 10);
|
|
111
|
+
break;
|
|
112
|
+
case "--recency-depth":
|
|
113
|
+
args.recencyDepth = parseInt(argv[i++], 10);
|
|
114
|
+
break;
|
|
115
|
+
case "--typed-memory-boost":
|
|
116
|
+
args.typedMemoryBoost = parseFloat(argv[i++]);
|
|
117
|
+
break;
|
|
118
|
+
case "--no-sync":
|
|
119
|
+
args.sync = false;
|
|
120
|
+
break;
|
|
121
|
+
case "--json":
|
|
122
|
+
args.json = true;
|
|
123
|
+
break;
|
|
124
|
+
case "--help":
|
|
125
|
+
case "-h":
|
|
126
|
+
console.log(HELP);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return args;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function resolveTeammatesDir(dir: string): Promise<string> {
|
|
135
|
+
const resolved = path.resolve(dir);
|
|
136
|
+
try {
|
|
137
|
+
await fs.access(resolved);
|
|
138
|
+
return resolved;
|
|
139
|
+
} catch {
|
|
140
|
+
console.error(`Error: .teammates directory not found at ${resolved}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function cmdIndex(args: Args): Promise<void> {
|
|
146
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
147
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
148
|
+
|
|
149
|
+
if (args.teammate) {
|
|
150
|
+
console.error(`Indexing ${args.teammate}...`);
|
|
151
|
+
const count = await indexer.indexTeammate(args.teammate);
|
|
152
|
+
if (args.json) {
|
|
153
|
+
console.log(JSON.stringify({ teammate: args.teammate, files: count }));
|
|
154
|
+
} else {
|
|
155
|
+
console.log(`Indexed ${count} files for ${args.teammate}`);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
console.error("Indexing all teammates...");
|
|
159
|
+
const results = await indexer.indexAll();
|
|
160
|
+
if (args.json) {
|
|
161
|
+
const obj = Object.fromEntries(results);
|
|
162
|
+
console.log(JSON.stringify(obj));
|
|
163
|
+
} else {
|
|
164
|
+
for (const [teammate, count] of results) {
|
|
165
|
+
console.log(` ${teammate}: ${count} files`);
|
|
166
|
+
}
|
|
167
|
+
console.log(`Done.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function cmdSync(args: Args): Promise<void> {
|
|
173
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
174
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
175
|
+
|
|
176
|
+
if (args.teammate) {
|
|
177
|
+
console.error(`Syncing ${args.teammate}...`);
|
|
178
|
+
const count = await indexer.syncTeammate(args.teammate);
|
|
179
|
+
if (args.json) {
|
|
180
|
+
console.log(JSON.stringify({ teammate: args.teammate, files: count }));
|
|
181
|
+
} else {
|
|
182
|
+
console.log(`Synced ${count} files for ${args.teammate}`);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
console.error("Syncing all teammates...");
|
|
186
|
+
const results = await indexer.syncAll();
|
|
187
|
+
if (args.json) {
|
|
188
|
+
const obj = Object.fromEntries(results);
|
|
189
|
+
console.log(JSON.stringify(obj));
|
|
190
|
+
} else {
|
|
191
|
+
for (const [teammate, count] of results) {
|
|
192
|
+
console.log(` ${teammate}: ${count} files`);
|
|
193
|
+
}
|
|
194
|
+
console.log(`Done.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function cmdAdd(args: Args): Promise<void> {
|
|
200
|
+
if (!args.file) {
|
|
201
|
+
console.error("Error: add requires a file path argument");
|
|
202
|
+
console.error("Usage: teammates-recall add <file> --teammate <name>");
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
if (!args.teammate) {
|
|
206
|
+
console.error("Error: add requires --teammate <name>");
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
211
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
212
|
+
await indexer.upsertFile(args.teammate, args.file);
|
|
213
|
+
|
|
214
|
+
if (args.json) {
|
|
215
|
+
console.log(
|
|
216
|
+
JSON.stringify({
|
|
217
|
+
teammate: args.teammate,
|
|
218
|
+
file: args.file,
|
|
219
|
+
status: "ok",
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
console.log(`Added ${args.file} to ${args.teammate}'s index`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function cmdSearch(args: Args): Promise<void> {
|
|
228
|
+
if (!args.query) {
|
|
229
|
+
console.error("Error: search requires a query argument");
|
|
230
|
+
console.error("Usage: teammates-recall search <query> [options]");
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
235
|
+
const results = await search(args.query, {
|
|
236
|
+
teammatesDir,
|
|
237
|
+
teammate: args.teammate,
|
|
238
|
+
maxResults: args.results,
|
|
239
|
+
maxChunks: args.maxChunks,
|
|
240
|
+
maxTokens: args.maxTokens,
|
|
241
|
+
recencyDepth: args.recencyDepth,
|
|
242
|
+
typedMemoryBoost: args.typedMemoryBoost,
|
|
243
|
+
model: args.model,
|
|
244
|
+
skipSync: !args.sync,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (args.json) {
|
|
248
|
+
console.log(JSON.stringify(results, null, 2));
|
|
249
|
+
} else {
|
|
250
|
+
if (results.length === 0) {
|
|
251
|
+
console.log("No results found.");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
for (const result of results) {
|
|
255
|
+
console.log(
|
|
256
|
+
`--- ${result.teammate} | ${result.uri} (score: ${result.score.toFixed(3)}) ---`,
|
|
257
|
+
);
|
|
258
|
+
console.log(result.text);
|
|
259
|
+
console.log();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function cmdStatus(args: Args): Promise<void> {
|
|
265
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
266
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
267
|
+
const teammates = await indexer.discoverTeammates();
|
|
268
|
+
|
|
269
|
+
const status: Record<string, { memoryFiles: number; indexed: boolean }> = {};
|
|
270
|
+
|
|
271
|
+
for (const teammate of teammates) {
|
|
272
|
+
const { files } = await indexer.collectFiles(teammate);
|
|
273
|
+
const indexPath = indexer.indexPath(teammate);
|
|
274
|
+
let indexed = false;
|
|
275
|
+
try {
|
|
276
|
+
await fs.access(indexPath);
|
|
277
|
+
indexed = true;
|
|
278
|
+
} catch {
|
|
279
|
+
// Not indexed
|
|
280
|
+
}
|
|
281
|
+
status[teammate] = { memoryFiles: files.length, indexed };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (args.json) {
|
|
285
|
+
console.log(JSON.stringify(status, null, 2));
|
|
286
|
+
} else {
|
|
287
|
+
for (const [teammate, info] of Object.entries(status)) {
|
|
288
|
+
const tag = info.indexed ? "indexed" : "not indexed";
|
|
289
|
+
console.log(` ${teammate}: ${info.memoryFiles} memory files (${tag})`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function cmdWatch(args: Args): Promise<void> {
|
|
295
|
+
const teammatesDir = await resolveTeammatesDir(args.dir);
|
|
296
|
+
const indexer = new Indexer({ teammatesDir, model: args.model });
|
|
297
|
+
|
|
298
|
+
// Initial sync
|
|
299
|
+
console.error("Initial sync...");
|
|
300
|
+
const results = await indexer.syncAll();
|
|
301
|
+
for (const [teammate, count] of results) {
|
|
302
|
+
console.error(` ${teammate}: ${count} files`);
|
|
303
|
+
}
|
|
304
|
+
console.error("Watching for changes...");
|
|
305
|
+
|
|
306
|
+
if (args.json) {
|
|
307
|
+
console.log(JSON.stringify({ status: "watching", dir: teammatesDir }));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Debounce: collect changes, sync after 2s of quiet
|
|
311
|
+
let syncTimer: ReturnType<typeof setTimeout> | null = null;
|
|
312
|
+
const pendingTeammates = new Set<string>();
|
|
313
|
+
|
|
314
|
+
const scheduleSync = (teammate: string) => {
|
|
315
|
+
pendingTeammates.add(teammate);
|
|
316
|
+
if (syncTimer) clearTimeout(syncTimer);
|
|
317
|
+
syncTimer = setTimeout(async () => {
|
|
318
|
+
for (const t of pendingTeammates) {
|
|
319
|
+
try {
|
|
320
|
+
const count = await indexer.syncTeammate(t);
|
|
321
|
+
if (args.json) {
|
|
322
|
+
console.log(
|
|
323
|
+
JSON.stringify({ event: "sync", teammate: t, files: count }),
|
|
324
|
+
);
|
|
325
|
+
} else {
|
|
326
|
+
console.error(` synced ${t}: ${count} files`);
|
|
327
|
+
}
|
|
328
|
+
} catch (err: unknown) {
|
|
329
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
330
|
+
console.error(` error syncing ${t}: ${msg}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
pendingTeammates.clear();
|
|
334
|
+
}, 2000);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Watch each teammate's directory for changes
|
|
338
|
+
const watchers: FSWatcher[] = [];
|
|
339
|
+
const teammates = await indexer.discoverTeammates();
|
|
340
|
+
|
|
341
|
+
for (const teammate of teammates) {
|
|
342
|
+
const teammateDir = path.join(teammatesDir, teammate);
|
|
343
|
+
try {
|
|
344
|
+
const watcher = fsWatch(
|
|
345
|
+
teammateDir,
|
|
346
|
+
{ recursive: true },
|
|
347
|
+
(_eventType, filename) => {
|
|
348
|
+
if (!filename) return;
|
|
349
|
+
// Only care about .md files, skip .index/
|
|
350
|
+
if (!filename.endsWith(".md") || filename.includes(".index")) return;
|
|
351
|
+
scheduleSync(teammate);
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
watchers.push(watcher);
|
|
355
|
+
} catch {
|
|
356
|
+
console.error(` warning: could not watch ${teammate}/`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Keep alive until killed
|
|
361
|
+
const shutdown = () => {
|
|
362
|
+
if (syncTimer) clearTimeout(syncTimer);
|
|
363
|
+
for (const w of watchers) w.close();
|
|
364
|
+
if (args.json) {
|
|
365
|
+
console.log(JSON.stringify({ status: "stopped" }));
|
|
366
|
+
}
|
|
367
|
+
process.exit(0);
|
|
368
|
+
};
|
|
369
|
+
process.on("SIGTERM", shutdown);
|
|
370
|
+
process.on("SIGINT", shutdown);
|
|
371
|
+
|
|
372
|
+
// Block forever
|
|
373
|
+
await new Promise(() => {});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function main(): Promise<void> {
|
|
377
|
+
const args = parseArgs(process.argv);
|
|
378
|
+
|
|
379
|
+
switch (args.command) {
|
|
380
|
+
case "index":
|
|
381
|
+
await cmdIndex(args);
|
|
382
|
+
break;
|
|
383
|
+
case "sync":
|
|
384
|
+
await cmdSync(args);
|
|
385
|
+
break;
|
|
386
|
+
case "add":
|
|
387
|
+
await cmdAdd(args);
|
|
388
|
+
break;
|
|
389
|
+
case "search":
|
|
390
|
+
await cmdSearch(args);
|
|
391
|
+
break;
|
|
392
|
+
case "status":
|
|
393
|
+
await cmdStatus(args);
|
|
394
|
+
break;
|
|
395
|
+
case "watch":
|
|
396
|
+
await cmdWatch(args);
|
|
397
|
+
break;
|
|
398
|
+
default:
|
|
399
|
+
console.log(HELP);
|
|
400
|
+
process.exit(args.command ? 1 : 0);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
main().catch((err) => {
|
|
405
|
+
console.error(err.message);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
});
|