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/README.md +366 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +667 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +55 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +18 -0
- package/dist/constants.js.map +1 -0
- package/dist/db.d.ts +60 -0
- package/dist/db.js +246 -0
- package/dist/db.js.map +1 -0
- package/dist/embed.d.ts +6 -0
- package/dist/embed.js +107 -0
- package/dist/embed.js.map +1 -0
- package/dist/extract.d.ts +9 -0
- package/dist/extract.js +101 -0
- package/dist/extract.js.map +1 -0
- package/dist/git.d.ts +9 -0
- package/dist/git.js +54 -0
- package/dist/git.js.map +1 -0
- package/dist/search.d.ts +11 -0
- package/dist/search.js +131 -0
- package/dist/search.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +153 -0
- package/dist/server.js.map +1 -0
- package/dist/staleness.d.ts +10 -0
- package/dist/staleness.js +40 -0
- package/dist/staleness.js.map +1 -0
- package/dist/sync.d.ts +22 -0
- package/dist/sync.js +92 -0
- package/dist/sync.js.map +1 -0
- package/package.json +38 -0
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
|