@teammates/recall 0.1.0 → 0.1.1

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/cli.ts CHANGED
@@ -1,275 +1,275 @@
1
- #!/usr/bin/env node
2
-
3
- import * as path from "node:path";
4
- import * as fs from "node:fs/promises";
5
- import { Indexer } from "./indexer.js";
6
- import { search } from "./search.js";
7
-
8
- const HELP = `
9
- teammates-recall — Semantic memory search for teammates
10
-
11
- Usage:
12
- teammates-recall index [options] Full rebuild of all indexes
13
- teammates-recall sync [options] Sync new/changed files into indexes
14
- teammates-recall add <file> [options] Add a single file to a teammate's index
15
- teammates-recall search <query> [options] Search teammate memories (auto-syncs)
16
- teammates-recall status [options] Show index status
17
-
18
- Options:
19
- --dir <path> Path to .teammates directory (default: ./.teammates)
20
- --teammate <name> Limit to a specific teammate
21
- --results <n> Max results (default: 5)
22
- --model <name> Embedding model (default: Xenova/all-MiniLM-L6-v2)
23
- --no-sync Skip auto-sync before search
24
- --json Output as JSON
25
- --help Show this help
26
- `.trim();
27
-
28
- interface Args {
29
- command: string;
30
- query: string;
31
- file: string;
32
- dir: string;
33
- teammate?: string;
34
- results: number;
35
- model?: string;
36
- json: boolean;
37
- sync: boolean;
38
- }
39
-
40
- function parseArgs(argv: string[]): Args {
41
- const args: Args = {
42
- command: "",
43
- query: "",
44
- file: "",
45
- dir: "./.teammates",
46
- results: 5,
47
- json: false,
48
- sync: true,
49
- };
50
-
51
- let i = 0;
52
- // Skip node and script path
53
- while (i < argv.length && (argv[i].includes("node") || argv[i].includes("teammates-recall") || argv[i].endsWith(".js"))) {
54
- i++;
55
- }
56
-
57
- if (i < argv.length && !argv[i].startsWith("-")) {
58
- args.command = argv[i++];
59
- }
60
-
61
- // For search, next non-flag arg is the query; for add, it's the file path
62
- if (args.command === "search" && i < argv.length && !argv[i].startsWith("-")) {
63
- args.query = argv[i++];
64
- } else if (args.command === "add" && i < argv.length && !argv[i].startsWith("-")) {
65
- args.file = argv[i++];
66
- }
67
-
68
- while (i < argv.length) {
69
- const arg = argv[i++];
70
- switch (arg) {
71
- case "--dir":
72
- args.dir = argv[i++];
73
- break;
74
- case "--teammate":
75
- args.teammate = argv[i++];
76
- break;
77
- case "--results":
78
- args.results = parseInt(argv[i++], 10);
79
- break;
80
- case "--model":
81
- args.model = argv[i++];
82
- break;
83
- case "--no-sync":
84
- args.sync = false;
85
- break;
86
- case "--json":
87
- args.json = true;
88
- break;
89
- case "--help":
90
- case "-h":
91
- console.log(HELP);
92
- process.exit(0);
93
- }
94
- }
95
-
96
- return args;
97
- }
98
-
99
- async function resolveTeammatesDir(dir: string): Promise<string> {
100
- const resolved = path.resolve(dir);
101
- try {
102
- await fs.access(resolved);
103
- return resolved;
104
- } catch {
105
- console.error(`Error: .teammates directory not found at ${resolved}`);
106
- process.exit(1);
107
- }
108
- }
109
-
110
- async function cmdIndex(args: Args): Promise<void> {
111
- const teammatesDir = await resolveTeammatesDir(args.dir);
112
- const indexer = new Indexer({ teammatesDir, model: args.model });
113
-
114
- if (args.teammate) {
115
- console.error(`Indexing ${args.teammate}...`);
116
- const count = await indexer.indexTeammate(args.teammate);
117
- if (args.json) {
118
- console.log(JSON.stringify({ teammate: args.teammate, files: count }));
119
- } else {
120
- console.log(`Indexed ${count} files for ${args.teammate}`);
121
- }
122
- } else {
123
- console.error("Indexing all teammates...");
124
- const results = await indexer.indexAll();
125
- if (args.json) {
126
- const obj = Object.fromEntries(results);
127
- console.log(JSON.stringify(obj));
128
- } else {
129
- for (const [teammate, count] of results) {
130
- console.log(` ${teammate}: ${count} files`);
131
- }
132
- console.log(`Done.`);
133
- }
134
- }
135
- }
136
-
137
- async function cmdSync(args: Args): Promise<void> {
138
- const teammatesDir = await resolveTeammatesDir(args.dir);
139
- const indexer = new Indexer({ teammatesDir, model: args.model });
140
-
141
- if (args.teammate) {
142
- console.error(`Syncing ${args.teammate}...`);
143
- const count = await indexer.syncTeammate(args.teammate);
144
- if (args.json) {
145
- console.log(JSON.stringify({ teammate: args.teammate, files: count }));
146
- } else {
147
- console.log(`Synced ${count} files for ${args.teammate}`);
148
- }
149
- } else {
150
- console.error("Syncing all teammates...");
151
- const results = await indexer.syncAll();
152
- if (args.json) {
153
- const obj = Object.fromEntries(results);
154
- console.log(JSON.stringify(obj));
155
- } else {
156
- for (const [teammate, count] of results) {
157
- console.log(` ${teammate}: ${count} files`);
158
- }
159
- console.log(`Done.`);
160
- }
161
- }
162
- }
163
-
164
- async function cmdAdd(args: Args): Promise<void> {
165
- if (!args.file) {
166
- console.error("Error: add requires a file path argument");
167
- console.error("Usage: teammates-recall add <file> --teammate <name>");
168
- process.exit(1);
169
- }
170
- if (!args.teammate) {
171
- console.error("Error: add requires --teammate <name>");
172
- process.exit(1);
173
- }
174
-
175
- const teammatesDir = await resolveTeammatesDir(args.dir);
176
- const indexer = new Indexer({ teammatesDir, model: args.model });
177
- await indexer.upsertFile(args.teammate, args.file);
178
-
179
- if (args.json) {
180
- console.log(JSON.stringify({ teammate: args.teammate, file: args.file, status: "ok" }));
181
- } else {
182
- console.log(`Added ${args.file} to ${args.teammate}'s index`);
183
- }
184
- }
185
-
186
- async function cmdSearch(args: Args): Promise<void> {
187
- if (!args.query) {
188
- console.error("Error: search requires a query argument");
189
- console.error("Usage: teammates-recall search <query> [options]");
190
- process.exit(1);
191
- }
192
-
193
- const teammatesDir = await resolveTeammatesDir(args.dir);
194
- const results = await search(args.query, {
195
- teammatesDir,
196
- teammate: args.teammate,
197
- maxResults: args.results,
198
- model: args.model,
199
- skipSync: !args.sync,
200
- });
201
-
202
- if (args.json) {
203
- console.log(JSON.stringify(results, null, 2));
204
- } else {
205
- if (results.length === 0) {
206
- console.log("No results found.");
207
- return;
208
- }
209
- for (const result of results) {
210
- console.log(`--- ${result.teammate} | ${result.uri} (score: ${result.score.toFixed(3)}) ---`);
211
- console.log(result.text);
212
- console.log();
213
- }
214
- }
215
- }
216
-
217
- async function cmdStatus(args: Args): Promise<void> {
218
- const teammatesDir = await resolveTeammatesDir(args.dir);
219
- const indexer = new Indexer({ teammatesDir, model: args.model });
220
- const teammates = await indexer.discoverTeammates();
221
-
222
- const status: Record<string, { memoryFiles: number; indexed: boolean }> = {};
223
-
224
- for (const teammate of teammates) {
225
- const { files } = await indexer.collectFiles(teammate);
226
- const indexPath = path.join(indexer.indexRoot, teammate);
227
- let indexed = false;
228
- try {
229
- await fs.access(indexPath);
230
- indexed = true;
231
- } catch {
232
- // Not indexed
233
- }
234
- status[teammate] = { memoryFiles: files.length, indexed };
235
- }
236
-
237
- if (args.json) {
238
- console.log(JSON.stringify(status, null, 2));
239
- } else {
240
- for (const [teammate, info] of Object.entries(status)) {
241
- const tag = info.indexed ? "indexed" : "not indexed";
242
- console.log(` ${teammate}: ${info.memoryFiles} memory files (${tag})`);
243
- }
244
- }
245
- }
246
-
247
- async function main(): Promise<void> {
248
- const args = parseArgs(process.argv);
249
-
250
- switch (args.command) {
251
- case "index":
252
- await cmdIndex(args);
253
- break;
254
- case "sync":
255
- await cmdSync(args);
256
- break;
257
- case "add":
258
- await cmdAdd(args);
259
- break;
260
- case "search":
261
- await cmdSearch(args);
262
- break;
263
- case "status":
264
- await cmdStatus(args);
265
- break;
266
- default:
267
- console.log(HELP);
268
- process.exit(args.command ? 1 : 0);
269
- }
270
- }
271
-
272
- main().catch((err) => {
273
- console.error(err.message);
274
- process.exit(1);
275
- });
1
+ #!/usr/bin/env node
2
+
3
+ import * as path from "node:path";
4
+ import * as fs from "node:fs/promises";
5
+ import { Indexer } from "./indexer.js";
6
+ import { search } from "./search.js";
7
+
8
+ const HELP = `
9
+ teammates-recall — Semantic memory search for teammates
10
+
11
+ Usage:
12
+ teammates-recall index [options] Full rebuild of all indexes
13
+ teammates-recall sync [options] Sync new/changed files into indexes
14
+ teammates-recall add <file> [options] Add a single file to a teammate's index
15
+ teammates-recall search <query> [options] Search teammate memories (auto-syncs)
16
+ teammates-recall status [options] Show index status
17
+
18
+ Options:
19
+ --dir <path> Path to .teammates directory (default: ./.teammates)
20
+ --teammate <name> Limit to a specific teammate
21
+ --results <n> Max results (default: 5)
22
+ --model <name> Embedding model (default: Xenova/all-MiniLM-L6-v2)
23
+ --no-sync Skip auto-sync before search
24
+ --json Output as JSON
25
+ --help Show this help
26
+ `.trim();
27
+
28
+ interface Args {
29
+ command: string;
30
+ query: string;
31
+ file: string;
32
+ dir: string;
33
+ teammate?: string;
34
+ results: number;
35
+ model?: string;
36
+ json: boolean;
37
+ sync: boolean;
38
+ }
39
+
40
+ function parseArgs(argv: string[]): Args {
41
+ const args: Args = {
42
+ command: "",
43
+ query: "",
44
+ file: "",
45
+ dir: "./.teammates",
46
+ results: 5,
47
+ json: false,
48
+ sync: true,
49
+ };
50
+
51
+ let i = 0;
52
+ // Skip node and script path
53
+ while (i < argv.length && (argv[i].includes("node") || argv[i].includes("teammates-recall") || argv[i].endsWith(".js"))) {
54
+ i++;
55
+ }
56
+
57
+ if (i < argv.length && !argv[i].startsWith("-")) {
58
+ args.command = argv[i++];
59
+ }
60
+
61
+ // For search, next non-flag arg is the query; for add, it's the file path
62
+ if (args.command === "search" && i < argv.length && !argv[i].startsWith("-")) {
63
+ args.query = argv[i++];
64
+ } else if (args.command === "add" && i < argv.length && !argv[i].startsWith("-")) {
65
+ args.file = argv[i++];
66
+ }
67
+
68
+ while (i < argv.length) {
69
+ const arg = argv[i++];
70
+ switch (arg) {
71
+ case "--dir":
72
+ args.dir = argv[i++];
73
+ break;
74
+ case "--teammate":
75
+ args.teammate = argv[i++];
76
+ break;
77
+ case "--results":
78
+ args.results = parseInt(argv[i++], 10);
79
+ break;
80
+ case "--model":
81
+ args.model = argv[i++];
82
+ break;
83
+ case "--no-sync":
84
+ args.sync = false;
85
+ break;
86
+ case "--json":
87
+ args.json = true;
88
+ break;
89
+ case "--help":
90
+ case "-h":
91
+ console.log(HELP);
92
+ process.exit(0);
93
+ }
94
+ }
95
+
96
+ return args;
97
+ }
98
+
99
+ async function resolveTeammatesDir(dir: string): Promise<string> {
100
+ const resolved = path.resolve(dir);
101
+ try {
102
+ await fs.access(resolved);
103
+ return resolved;
104
+ } catch {
105
+ console.error(`Error: .teammates directory not found at ${resolved}`);
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ async function cmdIndex(args: Args): Promise<void> {
111
+ const teammatesDir = await resolveTeammatesDir(args.dir);
112
+ const indexer = new Indexer({ teammatesDir, model: args.model });
113
+
114
+ if (args.teammate) {
115
+ console.error(`Indexing ${args.teammate}...`);
116
+ const count = await indexer.indexTeammate(args.teammate);
117
+ if (args.json) {
118
+ console.log(JSON.stringify({ teammate: args.teammate, files: count }));
119
+ } else {
120
+ console.log(`Indexed ${count} files for ${args.teammate}`);
121
+ }
122
+ } else {
123
+ console.error("Indexing all teammates...");
124
+ const results = await indexer.indexAll();
125
+ if (args.json) {
126
+ const obj = Object.fromEntries(results);
127
+ console.log(JSON.stringify(obj));
128
+ } else {
129
+ for (const [teammate, count] of results) {
130
+ console.log(` ${teammate}: ${count} files`);
131
+ }
132
+ console.log(`Done.`);
133
+ }
134
+ }
135
+ }
136
+
137
+ async function cmdSync(args: Args): Promise<void> {
138
+ const teammatesDir = await resolveTeammatesDir(args.dir);
139
+ const indexer = new Indexer({ teammatesDir, model: args.model });
140
+
141
+ if (args.teammate) {
142
+ console.error(`Syncing ${args.teammate}...`);
143
+ const count = await indexer.syncTeammate(args.teammate);
144
+ if (args.json) {
145
+ console.log(JSON.stringify({ teammate: args.teammate, files: count }));
146
+ } else {
147
+ console.log(`Synced ${count} files for ${args.teammate}`);
148
+ }
149
+ } else {
150
+ console.error("Syncing all teammates...");
151
+ const results = await indexer.syncAll();
152
+ if (args.json) {
153
+ const obj = Object.fromEntries(results);
154
+ console.log(JSON.stringify(obj));
155
+ } else {
156
+ for (const [teammate, count] of results) {
157
+ console.log(` ${teammate}: ${count} files`);
158
+ }
159
+ console.log(`Done.`);
160
+ }
161
+ }
162
+ }
163
+
164
+ async function cmdAdd(args: Args): Promise<void> {
165
+ if (!args.file) {
166
+ console.error("Error: add requires a file path argument");
167
+ console.error("Usage: teammates-recall add <file> --teammate <name>");
168
+ process.exit(1);
169
+ }
170
+ if (!args.teammate) {
171
+ console.error("Error: add requires --teammate <name>");
172
+ process.exit(1);
173
+ }
174
+
175
+ const teammatesDir = await resolveTeammatesDir(args.dir);
176
+ const indexer = new Indexer({ teammatesDir, model: args.model });
177
+ await indexer.upsertFile(args.teammate, args.file);
178
+
179
+ if (args.json) {
180
+ console.log(JSON.stringify({ teammate: args.teammate, file: args.file, status: "ok" }));
181
+ } else {
182
+ console.log(`Added ${args.file} to ${args.teammate}'s index`);
183
+ }
184
+ }
185
+
186
+ async function cmdSearch(args: Args): Promise<void> {
187
+ if (!args.query) {
188
+ console.error("Error: search requires a query argument");
189
+ console.error("Usage: teammates-recall search <query> [options]");
190
+ process.exit(1);
191
+ }
192
+
193
+ const teammatesDir = await resolveTeammatesDir(args.dir);
194
+ const results = await search(args.query, {
195
+ teammatesDir,
196
+ teammate: args.teammate,
197
+ maxResults: args.results,
198
+ model: args.model,
199
+ skipSync: !args.sync,
200
+ });
201
+
202
+ if (args.json) {
203
+ console.log(JSON.stringify(results, null, 2));
204
+ } else {
205
+ if (results.length === 0) {
206
+ console.log("No results found.");
207
+ return;
208
+ }
209
+ for (const result of results) {
210
+ console.log(`--- ${result.teammate} | ${result.uri} (score: ${result.score.toFixed(3)}) ---`);
211
+ console.log(result.text);
212
+ console.log();
213
+ }
214
+ }
215
+ }
216
+
217
+ async function cmdStatus(args: Args): Promise<void> {
218
+ const teammatesDir = await resolveTeammatesDir(args.dir);
219
+ const indexer = new Indexer({ teammatesDir, model: args.model });
220
+ const teammates = await indexer.discoverTeammates();
221
+
222
+ const status: Record<string, { memoryFiles: number; indexed: boolean }> = {};
223
+
224
+ for (const teammate of teammates) {
225
+ const { files } = await indexer.collectFiles(teammate);
226
+ const indexPath = indexer.indexPath(teammate);
227
+ let indexed = false;
228
+ try {
229
+ await fs.access(indexPath);
230
+ indexed = true;
231
+ } catch {
232
+ // Not indexed
233
+ }
234
+ status[teammate] = { memoryFiles: files.length, indexed };
235
+ }
236
+
237
+ if (args.json) {
238
+ console.log(JSON.stringify(status, null, 2));
239
+ } else {
240
+ for (const [teammate, info] of Object.entries(status)) {
241
+ const tag = info.indexed ? "indexed" : "not indexed";
242
+ console.log(` ${teammate}: ${info.memoryFiles} memory files (${tag})`);
243
+ }
244
+ }
245
+ }
246
+
247
+ async function main(): Promise<void> {
248
+ const args = parseArgs(process.argv);
249
+
250
+ switch (args.command) {
251
+ case "index":
252
+ await cmdIndex(args);
253
+ break;
254
+ case "sync":
255
+ await cmdSync(args);
256
+ break;
257
+ case "add":
258
+ await cmdAdd(args);
259
+ break;
260
+ case "search":
261
+ await cmdSearch(args);
262
+ break;
263
+ case "status":
264
+ await cmdStatus(args);
265
+ break;
266
+ default:
267
+ console.log(HELP);
268
+ process.exit(args.command ? 1 : 0);
269
+ }
270
+ }
271
+
272
+ main().catch((err) => {
273
+ console.error(err.message);
274
+ process.exit(1);
275
+ });
package/src/embeddings.ts CHANGED
@@ -1,43 +1,46 @@
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];
25
- const output = await extractor(texts, {
26
- pooling: "mean",
27
- normalize: true,
28
- });
29
- const embeddings: number[][] = output.tolist();
30
- return { status: "success", output: embeddings };
31
- } catch (err: any) {
32
- return { status: "error", message: err.message };
33
- }
34
- }
35
-
36
- private async _getExtractor(): Promise<any> {
37
- if (!this._extractor) {
38
- const { pipeline } = await import("@huggingface/transformers");
39
- this._extractor = await pipeline("feature-extraction", this._model);
40
- }
41
- return this._extractor;
42
- }
43
- }
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((t) => t.trim().length > 0);
25
+ if (texts.length === 0) {
26
+ return { status: "success", output: [] };
27
+ }
28
+ const output = await extractor(texts, {
29
+ pooling: "mean",
30
+ normalize: true,
31
+ });
32
+ const embeddings: number[][] = output.tolist();
33
+ return { status: "success", output: embeddings };
34
+ } catch (err: any) {
35
+ return { status: "error", message: err.message };
36
+ }
37
+ }
38
+
39
+ private async _getExtractor(): Promise<any> {
40
+ if (!this._extractor) {
41
+ const { pipeline } = await import("@huggingface/transformers");
42
+ this._extractor = await pipeline("feature-extraction", this._model);
43
+ }
44
+ return this._extractor;
45
+ }
46
+ }
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 { search, type SearchOptions, type SearchResult } from "./search.js";
1
+ export { LocalEmbeddings } from "./embeddings.js";
2
+ export { Indexer, type IndexerConfig } from "./indexer.js";
3
+ export { search, type SearchOptions, type SearchResult } from "./search.js";