teammind 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,667 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ // Suppress Node.js experimental warnings (node:sqlite is stable enough for production use)
8
+ process.on('warning', (w) => {
9
+ if (w.name === 'ExperimentalWarning')
10
+ return;
11
+ console.error(w.name + ': ' + w.message);
12
+ });
13
+ const commander_1 = require("commander");
14
+ const chalk_1 = __importDefault(require("chalk"));
15
+ const ora_1 = __importDefault(require("ora"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const os_1 = __importDefault(require("os"));
18
+ const fs_1 = require("fs");
19
+ const child_process_1 = require("child_process");
20
+ // Read the full JSONL transcript from the path provided by the Stop hook
21
+ function readTranscriptFile(transcriptPath) {
22
+ try {
23
+ const raw = (0, fs_1.readFileSync)(transcriptPath, 'utf8');
24
+ const lines = raw.split('\n').filter(l => l.trim());
25
+ return lines.map(line => {
26
+ try {
27
+ const entry = JSON.parse(line);
28
+ const role = (entry.role || 'unknown').toUpperCase();
29
+ const content = Array.isArray(entry.content)
30
+ ? entry.content.map((c) => (typeof c === 'string' ? c : c?.text || JSON.stringify(c))).join('\n')
31
+ : String(entry.content || '');
32
+ return `[${role}]: ${content}`;
33
+ }
34
+ catch {
35
+ return line;
36
+ }
37
+ }).join('\n\n');
38
+ }
39
+ catch {
40
+ return '';
41
+ }
42
+ }
43
+ const constants_1 = require("./constants");
44
+ const db_1 = require("./db");
45
+ const git_1 = require("./git");
46
+ const search_1 = require("./search");
47
+ const extract_1 = require("./extract");
48
+ const embed_1 = require("./embed");
49
+ const staleness_1 = require("./staleness");
50
+ const sync_1 = require("./sync");
51
+ const server_1 = require("./server");
52
+ const config_1 = require("./config");
53
+ const search_2 = require("./search");
54
+ const db_2 = require("./db");
55
+ const program = new commander_1.Command();
56
+ program
57
+ .name('teammind')
58
+ .description('Git-aware persistent memory for Claude Code teams')
59
+ .version(constants_1.VERSION);
60
+ // ─── init ────────────────────────────────────────────────────────────────────
61
+ program
62
+ .command('init')
63
+ .description('Set up TeamMind for this machine')
64
+ .option('--silent', 'No output (for postinstall)')
65
+ .action(async (opts) => {
66
+ const log = opts.silent ? () => { } : console.log;
67
+ const spinner = opts.silent ? null : (0, ora_1.default)();
68
+ log(chalk_1.default.bold('\nTeamMind setup\n'));
69
+ // 1. Create ~/.teammind directories
70
+ (0, fs_1.mkdirSync)(constants_1.TEAMMIND_DIR, { recursive: true });
71
+ (0, fs_1.mkdirSync)(constants_1.HOOKS_DIR, { recursive: true });
72
+ log(chalk_1.default.green('✓') + ' Created ~/.teammind directory');
73
+ // 2. Initialize database
74
+ (0, db_1.getDb)();
75
+ log(chalk_1.default.green('✓') + ' Database ready at ~/.teammind/db.sqlite');
76
+ // 3. Write hook scripts
77
+ const nodeExec = process.execPath;
78
+ const cliPath = process.argv[1];
79
+ const sessionStartHook = `#!/usr/bin/env node
80
+ 'use strict'
81
+ const { execFileSync } = require('child_process')
82
+ try {
83
+ const out = execFileSync(
84
+ ${JSON.stringify(nodeExec)},
85
+ ['--no-warnings', ${JSON.stringify(cliPath)}, 'inject'],
86
+ { encoding: 'utf8', timeout: 8000, env: process.env, cwd: process.cwd() }
87
+ )
88
+ process.stdout.write(out)
89
+ } catch (e) { /* silent fail — never break Claude Code */ }
90
+ `;
91
+ const sessionStopHook = `#!/usr/bin/env node
92
+ 'use strict'
93
+ const { execFileSync } = require('child_process')
94
+ const chunks = []
95
+ process.stdin.on('data', c => chunks.push(c))
96
+ process.stdin.on('end', () => {
97
+ try {
98
+ const input = Buffer.concat(chunks).toString('utf8')
99
+ if (!input.trim()) return
100
+ execFileSync(
101
+ ${JSON.stringify(nodeExec)},
102
+ ['--no-warnings', ${JSON.stringify(cliPath)}, 'capture'],
103
+ { input, timeout: 5000, env: process.env, cwd: process.cwd(), stdio: ['pipe','ignore','ignore'] }
104
+ )
105
+ } catch (e) { /* silent fail */ }
106
+ })
107
+ `;
108
+ const startHookPath = path_1.default.join(constants_1.HOOKS_DIR, 'session-start.js');
109
+ const stopHookPath = path_1.default.join(constants_1.HOOKS_DIR, 'session-stop.js');
110
+ (0, fs_1.writeFileSync)(startHookPath, sessionStartHook);
111
+ (0, fs_1.writeFileSync)(stopHookPath, sessionStopHook);
112
+ log(chalk_1.default.green('✓') + ' Hook scripts written');
113
+ // 4. Patch ~/.claude/settings.json
114
+ const settingsPath = path_1.default.join(os_1.default.homedir(), '.claude', 'settings.json');
115
+ patchClaudeSettings(settingsPath, startHookPath, stopHookPath, nodeExec, cliPath);
116
+ log(chalk_1.default.green('✓') + ' Claude Code settings updated');
117
+ // 5. Pre-warm embedding model
118
+ if (!opts.silent) {
119
+ spinner.start('Downloading embedding model (~38MB, one time only)...');
120
+ }
121
+ try {
122
+ await (0, embed_1.warmupModel)();
123
+ spinner?.succeed('Embedding model ready');
124
+ }
125
+ catch {
126
+ spinner?.warn('Embedding model download failed (will retry on first use)');
127
+ }
128
+ // 6. Check for API key
129
+ if (!process.env.ANTHROPIC_API_KEY) {
130
+ log(chalk_1.default.yellow('\n⚠ ANTHROPIC_API_KEY not set'));
131
+ log(' Auto-extraction requires an API key (~$0.001/session).');
132
+ log(' Without it, you can still add memories manually or import team memories.');
133
+ log(' Set it with: export ANTHROPIC_API_KEY=sk-...\n');
134
+ }
135
+ log(chalk_1.default.bold.green('\nTeamMind is active.') + ' Just use Claude Code normally.');
136
+ log('Run ' + chalk_1.default.cyan('teammind status') + ' to see captured memories.');
137
+ log('Run ' + chalk_1.default.cyan('teammind team') + ' to share with your team.\n');
138
+ });
139
+ // ─── status ──────────────────────────────────────────────────────────────────
140
+ program
141
+ .command('status')
142
+ .description('Show memory stats for the current project')
143
+ .action(async () => {
144
+ const gitCtx = await (0, git_1.getGitContext)(process.cwd());
145
+ const repoPath = gitCtx?.root || (0, git_1.normalizePath)(process.cwd());
146
+ const projectName = path_1.default.basename(repoPath);
147
+ const allMemories = (0, db_1.getMemories)(repoPath, { limit: 200, includeStale: true });
148
+ const fresh = allMemories.filter(m => !m.stale);
149
+ const stale = allMemories.filter(m => m.stale);
150
+ const recent = fresh.slice(0, 5);
151
+ const lastCapture = fresh[0]
152
+ ? formatAge(fresh[0].created_at)
153
+ : 'never';
154
+ console.log(chalk_1.default.bold(`\nTeamMind — ${projectName}`));
155
+ console.log('─'.repeat(40));
156
+ if (allMemories.length === 0) {
157
+ console.log(chalk_1.default.dim(' No memories yet. Use Claude Code normally and memories will be captured automatically.'));
158
+ }
159
+ else {
160
+ console.log(` ${chalk_1.default.green(fresh.length)} fresh memories • ${stale.length > 0 ? chalk_1.default.yellow(stale.length + ' stale') : '0 stale'} • last captured ${lastCapture}`);
161
+ if (recent.length > 0) {
162
+ console.log('\n' + chalk_1.default.dim('Recent captures:'));
163
+ for (const m of recent) {
164
+ const tag = chalk_1.default.cyan(`[${m.tags[0] || 'note'}]`);
165
+ const age = chalk_1.default.dim(`— ${formatAge(m.created_at)}`);
166
+ console.log(` • ${tag} ${m.summary} ${age}`);
167
+ }
168
+ }
169
+ }
170
+ console.log();
171
+ console.log(`Run ${chalk_1.default.cyan('teammind memories')} to browse all.`);
172
+ console.log(`Run ${chalk_1.default.cyan('teammind team')} to share with your team.\n`);
173
+ });
174
+ // ─── memories ────────────────────────────────────────────────────────────────
175
+ program
176
+ .command('memories [query]')
177
+ .description('Browse or search memories for the current project')
178
+ .option('-n, --limit <n>', 'Number of results', '20')
179
+ .option('--stale', 'Include stale memories')
180
+ .action(async (query, opts) => {
181
+ const gitCtx = await (0, git_1.getGitContext)(process.cwd());
182
+ const repoPath = gitCtx?.root || (0, git_1.normalizePath)(process.cwd());
183
+ const limit = parseInt(opts.limit) || 20;
184
+ let memories;
185
+ if (query) {
186
+ const spinner = (0, ora_1.default)(`Searching for "${query}"...`).start();
187
+ try {
188
+ const results = await (0, search_1.searchMemories)(query, repoPath, { limit });
189
+ spinner.stop();
190
+ memories = results;
191
+ }
192
+ catch {
193
+ spinner.stop();
194
+ memories = (0, db_1.getMemories)(repoPath, { limit, includeStale: opts.stale });
195
+ }
196
+ }
197
+ else {
198
+ memories = (0, db_1.getMemories)(repoPath, { limit, includeStale: opts.stale });
199
+ }
200
+ if (memories.length === 0) {
201
+ console.log(chalk_1.default.dim('\nNo memories found.\n'));
202
+ return;
203
+ }
204
+ console.log();
205
+ for (let i = 0; i < memories.length; i++) {
206
+ const m = memories[i];
207
+ const tags = chalk_1.default.cyan(`[${m.tags.join(', ') || 'note'}]`);
208
+ const staleNote = m.stale ? chalk_1.default.yellow(' ⚠ STALE') : '';
209
+ const age = chalk_1.default.dim(formatAge(m.created_at));
210
+ const files = m.file_paths.length > 0
211
+ ? chalk_1.default.dim(`\n ${m.file_paths.join(', ')}`)
212
+ : '';
213
+ console.log(`${chalk_1.default.bold(String(i + 1).padStart(2))}. ${tags} ${chalk_1.default.white(m.summary)}${staleNote} ${age}`);
214
+ console.log(` ${chalk_1.default.dim(m.content.slice(0, 200) + (m.content.length > 200 ? '...' : ''))}${files}`);
215
+ console.log(` ${chalk_1.default.dim('id: ' + m.id)}`);
216
+ console.log();
217
+ }
218
+ });
219
+ // ─── forget ───────────────────────────────────────────────────────────────────
220
+ program
221
+ .command('forget [id]')
222
+ .description('Delete a memory or clear all stale memories')
223
+ .option('--stale', 'Delete all stale memories for this project')
224
+ .action(async (id, opts) => {
225
+ const gitCtx = await (0, git_1.getGitContext)(process.cwd());
226
+ const repoPath = gitCtx?.root || (0, git_1.normalizePath)(process.cwd());
227
+ if (opts.stale) {
228
+ const count = (0, db_1.deleteStaleMemories)(repoPath);
229
+ console.log(chalk_1.default.green(`✓ Deleted ${count} stale memories`));
230
+ return;
231
+ }
232
+ if (!id) {
233
+ console.log(chalk_1.default.red('Provide a memory id or use --stale to clear stale memories'));
234
+ console.log('Run ' + chalk_1.default.cyan('teammind memories') + ' to see ids');
235
+ return;
236
+ }
237
+ (0, db_1.deleteMemory)(id);
238
+ console.log(chalk_1.default.green(`✓ Deleted memory ${id}`));
239
+ });
240
+ // ─── team ─────────────────────────────────────────────────────────────────────
241
+ program
242
+ .command('team')
243
+ .description('Set up team memory sharing')
244
+ .option('--export <path>', 'Export memories to a file')
245
+ .option('--import <path>', 'Import memories from a file')
246
+ .action(async (opts) => {
247
+ const gitCtx = await (0, git_1.getGitContext)(process.cwd());
248
+ const repoPath = gitCtx?.root || (0, git_1.normalizePath)(process.cwd());
249
+ if (opts.export) {
250
+ const spinner = (0, ora_1.default)('Exporting memories...').start();
251
+ const data = (0, sync_1.exportMemories)(repoPath);
252
+ (0, sync_1.writeExportFile)(data, opts.export);
253
+ spinner.succeed(`Exported ${data.memories.length} memories to ${opts.export}`);
254
+ console.log(chalk_1.default.dim('\nCommit this file and tell teammates to run:'));
255
+ console.log(chalk_1.default.cyan(` teammind team --import ${opts.export}\n`));
256
+ return;
257
+ }
258
+ if (opts.import) {
259
+ const spinner = (0, ora_1.default)('Importing memories...').start();
260
+ try {
261
+ const result = await (0, sync_1.importMemories)(opts.import, repoPath);
262
+ spinner.succeed(`Imported ${result.imported} memories (${result.skipped} skipped)`);
263
+ }
264
+ catch (err) {
265
+ spinner.fail(`Import failed: ${err?.message}`);
266
+ }
267
+ return;
268
+ }
269
+ // Interactive team setup
270
+ const exportPath = path_1.default.join(repoPath, '.claude', 'team-memories.json');
271
+ const relPath = path_1.default.relative(process.cwd(), exportPath);
272
+ console.log(chalk_1.default.bold('\nTeam Memory Setup\n'));
273
+ console.log('This exports your memories to a file you can commit to your repo.');
274
+ console.log('Teammates import it and their Claude Code sessions get your team\'s context.\n');
275
+ const spinner = (0, ora_1.default)('Exporting memories...').start();
276
+ (0, fs_1.mkdirSync)(path_1.default.dirname(exportPath), { recursive: true });
277
+ const data = (0, sync_1.exportMemories)(repoPath);
278
+ (0, sync_1.writeExportFile)(data, exportPath);
279
+ spinner.succeed(`Exported ${data.memories.length} memories to ${chalk_1.default.cyan(relPath)}`);
280
+ console.log('\n' + chalk_1.default.bold('Next steps:'));
281
+ console.log(` 1. ${chalk_1.default.cyan(`git add ${relPath}`)}`);
282
+ console.log(` 2. ${chalk_1.default.cyan('git commit -m "feat: add team memories"')}`);
283
+ console.log(` 3. ${chalk_1.default.cyan('git push')}`);
284
+ console.log();
285
+ console.log('Tell teammates to run after pulling:');
286
+ console.log(chalk_1.default.cyan(` teammind team --import ${relPath}`));
287
+ console.log();
288
+ console.log(chalk_1.default.dim('Tip: add to .git/hooks/post-merge to auto-import on git pull:'));
289
+ console.log(chalk_1.default.dim(` echo 'teammind team --import ${relPath}' >> .git/hooks/post-merge`));
290
+ console.log(chalk_1.default.dim(' chmod +x .git/hooks/post-merge\n'));
291
+ });
292
+ // ─── inject (called by SessionStart hook) ────────────────────────────────────
293
+ program
294
+ .command('inject')
295
+ .description('Print relevant memories to stdout (used by SessionStart hook)')
296
+ .action(async () => {
297
+ try {
298
+ const cwd = process.cwd();
299
+ const gitCtx = await (0, git_1.getGitContext)(cwd);
300
+ if (!gitCtx)
301
+ return; // Not a git repo, nothing to inject
302
+ const repoPath = gitCtx.root;
303
+ const projectName = path_1.default.basename(repoPath);
304
+ // Run staleness check in background
305
+ (0, staleness_1.checkAndMarkStaleness)(repoPath).catch(() => { });
306
+ const config = (0, config_1.loadConfig)();
307
+ const memories = (0, search_1.rankMemoriesForInjection)(repoPath, gitCtx.branch, config.max_inject);
308
+ if (memories.length === 0)
309
+ return;
310
+ const total = (0, db_1.countMemories)(repoPath);
311
+ const context = (0, search_1.formatMemoriesForContext)(memories, projectName, gitCtx.branch);
312
+ process.stdout.write(context + '\n');
313
+ }
314
+ catch {
315
+ // Never fail — silent exit
316
+ }
317
+ });
318
+ // ─── capture (called by Stop hook) ───────────────────────────────────────────
319
+ program
320
+ .command('capture')
321
+ .description('Save session transcript (used by Stop hook)')
322
+ .action(async () => {
323
+ try {
324
+ const chunks = [];
325
+ await new Promise((resolve) => {
326
+ process.stdin.on('data', c => chunks.push(c));
327
+ process.stdin.on('end', resolve);
328
+ process.stdin.on('error', resolve);
329
+ // Timeout: don't wait forever
330
+ setTimeout(resolve, 10000);
331
+ });
332
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
333
+ if (!raw)
334
+ return;
335
+ const hookPayload = JSON.parse(raw);
336
+ // Claude Code Stop hook provides transcript_path (a JSONL file), not inline transcript
337
+ let transcript = '';
338
+ if (hookPayload.transcript_path && (0, fs_1.existsSync)(hookPayload.transcript_path)) {
339
+ transcript = readTranscriptFile(hookPayload.transcript_path);
340
+ }
341
+ else {
342
+ // Fallback: try to format whatever is in the payload (older format or direct test)
343
+ transcript = (0, extract_1.formatTranscript)(hookPayload);
344
+ }
345
+ if (!transcript || transcript.length < 100)
346
+ return;
347
+ const cwd = (0, git_1.normalizePath)(hookPayload.cwd || process.cwd());
348
+ const gitCtx = await (0, git_1.getGitContext)(cwd);
349
+ const repoPath = gitCtx?.root || cwd;
350
+ const sessionId = (0, db_1.saveSession)({
351
+ repo_path: repoPath,
352
+ branch: gitCtx?.branch || null,
353
+ commit: gitCtx?.commit || null,
354
+ transcript,
355
+ });
356
+ // Spawn extraction in background (fully detached — user feels nothing)
357
+ const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], 'extract', sessionId], {
358
+ detached: true,
359
+ stdio: 'ignore',
360
+ env: process.env,
361
+ });
362
+ child.unref();
363
+ }
364
+ catch {
365
+ // Never fail
366
+ }
367
+ });
368
+ // ─── extract (background worker) ─────────────────────────────────────────────
369
+ program
370
+ .command('extract [sessionId]')
371
+ .description('Extract memories: provide a session ID (background worker) or use --pending for all')
372
+ .option('--pending', 'Process all pending sessions interactively')
373
+ .option('--verbose', 'Show extraction details')
374
+ .action(async (sessionId, opts) => {
375
+ // ── --pending mode: interactive extraction of all queued sessions ──────
376
+ if (opts.pending || !sessionId) {
377
+ const pending = (0, db_2.getPendingSessions)();
378
+ if (pending.length === 0) {
379
+ console.log(chalk_1.default.green('✓ No pending sessions — everything is up to date.'));
380
+ return;
381
+ }
382
+ const apiKey = (0, config_1.getApiKey)();
383
+ if (!apiKey) {
384
+ console.log(chalk_1.default.yellow('⚠ No API key found. Set it with:'));
385
+ console.log(chalk_1.default.cyan(' teammind config set ANTHROPIC_API_KEY sk-ant-...\n'));
386
+ console.log(chalk_1.default.dim(`${pending.length} sessions are pending extraction.`));
387
+ return;
388
+ }
389
+ console.log(chalk_1.default.bold(`\nProcessing ${pending.length} pending session${pending.length > 1 ? 's' : ''}...\n`));
390
+ let totalSaved = 0, totalDeduped = 0;
391
+ const config = (0, config_1.loadConfig)();
392
+ const username = os_1.default.userInfo().username || 'local';
393
+ for (const session of pending) {
394
+ const spinner = (0, ora_1.default)(`${path_1.default.basename(session.repo_path)}/${session.branch || '?'} — ${formatAge(session.created_at)}`).start();
395
+ try {
396
+ const extracted = await (0, extract_1.extractMemoriesFromTranscript)(session.transcript, apiKey);
397
+ if (extracted.length === 0) {
398
+ (0, db_1.markSessionProcessed)(session.id);
399
+ spinner.succeed(chalk_1.default.dim('No memories worth capturing'));
400
+ continue;
401
+ }
402
+ let saved = 0, deduped = 0;
403
+ for (const m of extracted) {
404
+ let embedding = null;
405
+ try {
406
+ const v = await (0, embed_1.embed)(m.content);
407
+ embedding = (0, embed_1.serializeVec)(v);
408
+ }
409
+ catch { }
410
+ if (embedding) {
411
+ const dupId = await (0, search_2.findDuplicate)(m.content, session.repo_path, config.similarity_threshold);
412
+ if (dupId) {
413
+ deduped++;
414
+ continue;
415
+ }
416
+ }
417
+ const id = (0, db_1.saveMemory)({
418
+ content: m.content, summary: m.summary, tags: m.tags,
419
+ file_paths: m.file_paths, functions: m.functions, embedding,
420
+ repo_path: session.repo_path, git_commit: session.commit,
421
+ git_branch: session.branch, created_by: username, source: 'auto', stale: 0,
422
+ });
423
+ saved++;
424
+ if (m.file_paths.length > 0) {
425
+ const absFiles = (0, git_1.resolveFilePaths)(m.file_paths, session.repo_path);
426
+ (0, db_1.saveMemoryFiles)(id, absFiles.map(fp => ({ path: path_1.default.relative(session.repo_path, fp), hash: (0, git_1.hashFile)(fp) })));
427
+ }
428
+ if (opts.verbose)
429
+ console.log(` ${chalk_1.default.cyan(`[${m.tags[0] || 'note'}]`)} ${m.summary}`);
430
+ }
431
+ (0, db_1.markSessionProcessed)(session.id);
432
+ totalSaved += saved;
433
+ totalDeduped += deduped;
434
+ spinner.succeed(`${chalk_1.default.green(saved + ' saved')}${deduped > 0 ? chalk_1.default.dim(` · ${deduped} dupes skipped`) : ''}`);
435
+ }
436
+ catch (e) {
437
+ spinner.fail(chalk_1.default.red(e?.message || 'failed'));
438
+ }
439
+ }
440
+ console.log(chalk_1.default.bold(`\nDone. ${totalSaved} new memories` + (totalDeduped > 0 ? `, ${totalDeduped} duplicates skipped.` : '.') + '\n'));
441
+ return;
442
+ }
443
+ // ── session ID mode: background worker (called by Stop hook) ──────────
444
+ try {
445
+ const session = (0, db_1.getSession)(sessionId);
446
+ if (!session || session.processed)
447
+ return;
448
+ (0, db_1.pruneOldSessions)(7);
449
+ const apiKey = (0, config_1.getApiKey)();
450
+ const memories = await (0, extract_1.extractMemoriesFromTranscript)(session.transcript, apiKey);
451
+ if (memories.length === 0) {
452
+ (0, db_1.markSessionProcessed)(sessionId);
453
+ return;
454
+ }
455
+ const username = os_1.default.userInfo().username || 'local';
456
+ const config = (0, config_1.loadConfig)();
457
+ let saved = 0, deduped = 0;
458
+ for (const m of memories) {
459
+ // Embed the memory
460
+ let embedding = null;
461
+ try {
462
+ const vec = await (0, embed_1.embed)(m.content);
463
+ embedding = (0, embed_1.serializeVec)(vec);
464
+ }
465
+ catch { /* embedding optional */ }
466
+ // Deduplication: skip if a very similar memory already exists
467
+ if (embedding) {
468
+ const dupId = await (0, search_2.findDuplicate)(m.content, session.repo_path, config.similarity_threshold);
469
+ if (dupId) {
470
+ deduped++;
471
+ if (opts?.verbose)
472
+ console.error(`[teammind] Deduped: "${m.summary}"`);
473
+ continue;
474
+ }
475
+ }
476
+ const id = (0, db_1.saveMemory)({
477
+ content: m.content,
478
+ summary: m.summary,
479
+ tags: m.tags,
480
+ file_paths: m.file_paths,
481
+ functions: m.functions,
482
+ embedding,
483
+ repo_path: session.repo_path,
484
+ git_commit: session.commit,
485
+ git_branch: session.branch,
486
+ created_by: username,
487
+ source: 'auto',
488
+ stale: 0,
489
+ });
490
+ saved++;
491
+ // Save file refs with current hashes
492
+ if (m.file_paths.length > 0) {
493
+ const absFiles = (0, git_1.resolveFilePaths)(m.file_paths, session.repo_path);
494
+ (0, db_1.saveMemoryFiles)(id, absFiles.map(fp => ({
495
+ path: path_1.default.relative(session.repo_path, fp),
496
+ hash: (0, git_1.hashFile)(fp)
497
+ })));
498
+ }
499
+ }
500
+ if (opts?.verbose) {
501
+ console.error(`[teammind] Extracted ${saved} new memories (${deduped} duplicates skipped)`);
502
+ }
503
+ (0, db_1.markSessionProcessed)(sessionId);
504
+ }
505
+ catch {
506
+ // Background worker — silent failure is fine
507
+ }
508
+ });
509
+ // ─── server (MCP server, called by Claude Code) ───────────────────────────────
510
+ program
511
+ .command('server')
512
+ .description('Start the MCP server (called by Claude Code)')
513
+ .action(async () => {
514
+ await (0, server_1.startMcpServer)();
515
+ });
516
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
517
+ function patchClaudeSettings(settingsPath, startHookPath, stopHookPath, nodeExec, cliPath) {
518
+ let settings = {};
519
+ if ((0, fs_1.existsSync)(settingsPath)) {
520
+ try {
521
+ settings = JSON.parse((0, fs_1.readFileSync)(settingsPath, 'utf8'));
522
+ }
523
+ catch {
524
+ settings = {};
525
+ }
526
+ }
527
+ settings.hooks = settings.hooks || {};
528
+ // SessionStart
529
+ settings.hooks.SessionStart = (settings.hooks.SessionStart || [])
530
+ .filter((h) => !JSON.stringify(h).includes('teammind'));
531
+ settings.hooks.SessionStart.push({
532
+ matcher: '',
533
+ hooks: [{ type: 'command', command: `${JSON.stringify(nodeExec)} ${JSON.stringify(startHookPath)}` }]
534
+ });
535
+ // Stop
536
+ settings.hooks.Stop = (settings.hooks.Stop || [])
537
+ .filter((h) => !JSON.stringify(h).includes('teammind'));
538
+ settings.hooks.Stop.push({
539
+ matcher: '',
540
+ hooks: [{ type: 'command', command: `${JSON.stringify(nodeExec)} ${JSON.stringify(stopHookPath)}` }]
541
+ });
542
+ // MCP server
543
+ settings.mcpServers = settings.mcpServers || {};
544
+ settings.mcpServers.teammind = {
545
+ type: 'stdio',
546
+ command: nodeExec,
547
+ args: ['--no-warnings', cliPath, 'server']
548
+ };
549
+ (0, fs_1.mkdirSync)(path_1.default.dirname(settingsPath), { recursive: true });
550
+ (0, fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2));
551
+ }
552
+ function formatAge(ts) {
553
+ const days = Math.floor((Date.now() - ts) / (1000 * 60 * 60 * 24));
554
+ if (days === 0)
555
+ return 'today';
556
+ if (days === 1)
557
+ return 'yesterday';
558
+ if (days < 30)
559
+ return `${days}d ago`;
560
+ if (days < 365)
561
+ return `${Math.floor(days / 30)}mo ago`;
562
+ return `${Math.floor(days / 365)}y ago`;
563
+ }
564
+ // ─── sessions ────────────────────────────────────────────────────────────────
565
+ program
566
+ .command('sessions')
567
+ .description('List captured sessions (processed and pending extraction)')
568
+ .option('-n, --limit <n>', 'Number of sessions to show', '15')
569
+ .action(async (opts) => {
570
+ const limit = parseInt(opts.limit) || 15;
571
+ const sessions = (0, db_2.getAllSessions)(limit);
572
+ if (sessions.length === 0) {
573
+ console.log(chalk_1.default.dim('\nNo sessions captured yet.\n'));
574
+ return;
575
+ }
576
+ const pending = sessions.filter(s => !s.processed);
577
+ const done = sessions.filter(s => s.processed);
578
+ console.log();
579
+ if (pending.length > 0) {
580
+ console.log(chalk_1.default.yellow.bold(`⏳ Pending extraction (${pending.length})`));
581
+ console.log(chalk_1.default.dim(' Run `teammind extract --pending` to process these now\n'));
582
+ for (const s of pending) {
583
+ const age = formatAge(s.created_at);
584
+ const kb = Math.round(s.transcript_len / 1024);
585
+ const repo = path_1.default.basename(s.repo_path);
586
+ console.log(` ${chalk_1.default.white(s.id.slice(0, 8))} ${repo}/${s.branch || '?'} ${kb}KB ${chalk_1.default.dim(age)}`);
587
+ }
588
+ console.log();
589
+ }
590
+ if (done.length > 0) {
591
+ console.log(chalk_1.default.green.bold(`✓ Processed (${done.length})`));
592
+ console.log();
593
+ for (const s of done.slice(0, 8)) {
594
+ const age = formatAge(s.created_at);
595
+ const kb = Math.round(s.transcript_len / 1024);
596
+ const repo = path_1.default.basename(s.repo_path);
597
+ console.log(` ${chalk_1.default.dim(s.id.slice(0, 8))} ${repo}/${s.branch || '?'} ${kb}KB ${chalk_1.default.dim(age)}`);
598
+ }
599
+ }
600
+ console.log();
601
+ });
602
+ // ─── config ───────────────────────────────────────────────────────────────────
603
+ program
604
+ .command('config')
605
+ .description('View or update TeamMind configuration')
606
+ .addCommand(new (require('commander').Command)('set')
607
+ .description('Set a config value')
608
+ .argument('<key>', `Config key (${config_1.VALID_KEYS.join(', ')})`)
609
+ .argument('<value>', 'Value to set')
610
+ .action((key, value) => {
611
+ const normalizedKey = key.toUpperCase() === 'ANTHROPIC_API_KEY' ? 'anthropic_api_key' : key;
612
+ const coerced = (0, config_1.coerceConfigValue)(normalizedKey, value);
613
+ (0, config_1.saveConfig)({ [normalizedKey]: coerced });
614
+ if (normalizedKey === 'anthropic_api_key') {
615
+ console.log(chalk_1.default.green('✓ API key saved to ~/.teammind/config.json'));
616
+ console.log(chalk_1.default.dim(' Auto-extraction will now run at session end.'));
617
+ }
618
+ else {
619
+ console.log(chalk_1.default.green(`✓ ${key} = ${coerced}`));
620
+ }
621
+ }))
622
+ .addCommand(new (require('commander').Command)('get')
623
+ .description('Get a config value')
624
+ .argument('<key>', 'Config key')
625
+ .action((key) => {
626
+ const normalizedKey = key.toUpperCase() === 'ANTHROPIC_API_KEY' ? 'anthropic_api_key' : key;
627
+ const config = (0, config_1.loadConfig)();
628
+ const value = config[normalizedKey];
629
+ if (value === undefined) {
630
+ console.log(chalk_1.default.red(`Unknown key: ${key}`));
631
+ }
632
+ else if (normalizedKey === 'anthropic_api_key') {
633
+ console.log(value ? `sk-...${String(value).slice(-6)}` : chalk_1.default.dim('(not set)'));
634
+ }
635
+ else {
636
+ console.log(String(value));
637
+ }
638
+ }))
639
+ .addCommand(new (require('commander').Command)('list')
640
+ .description('Show all config values')
641
+ .action(() => {
642
+ const config = (0, config_1.loadConfig)();
643
+ const apiKey = (0, config_1.getApiKey)();
644
+ console.log();
645
+ console.log(chalk_1.default.bold('TeamMind Configuration'));
646
+ console.log(chalk_1.default.dim(' ~/.teammind/config.json\n'));
647
+ console.log(` ${'ANTHROPIC_API_KEY'.padEnd(25)} ${apiKey ? chalk_1.default.green('sk-...' + apiKey.slice(-6)) : chalk_1.default.yellow('(not set — auto-extraction disabled)')}`);
648
+ console.log(` ${'max_inject'.padEnd(25)} ${config.max_inject}`);
649
+ console.log(` ${'extraction_enabled'.padEnd(25)} ${config.extraction_enabled}`);
650
+ console.log(` ${'similarity_threshold'.padEnd(25)} ${config.similarity_threshold} (dedup threshold)`);
651
+ console.log();
652
+ }))
653
+ .action(() => {
654
+ // Default: show list
655
+ const config = (0, config_1.loadConfig)();
656
+ const apiKey = (0, config_1.getApiKey)();
657
+ console.log();
658
+ console.log(chalk_1.default.bold('TeamMind Configuration'));
659
+ console.log(chalk_1.default.dim(' Use `teammind config set <key> <value>` to change\n'));
660
+ console.log(` ${'ANTHROPIC_API_KEY'.padEnd(25)} ${apiKey ? chalk_1.default.green('set') : chalk_1.default.yellow('not set')}`);
661
+ console.log(` ${'max_inject'.padEnd(25)} ${config.max_inject}`);
662
+ console.log(` ${'extraction_enabled'.padEnd(25)} ${config.extraction_enabled}`);
663
+ console.log(` ${'similarity_threshold'.padEnd(25)} ${config.similarity_threshold}`);
664
+ console.log();
665
+ });
666
+ program.parse();
667
+ //# sourceMappingURL=cli.js.map