linksee-memory 0.0.4 → 0.0.6
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.
|
@@ -11,7 +11,7 @@ import { readdirSync, statSync } from 'node:fs';
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import { openDb, runMigrations } from '../db/migrate.js';
|
|
14
|
-
import { parseSessionFile,
|
|
14
|
+
import { parseSessionFile, detectProjectName } from '../lib/session-parser.js';
|
|
15
15
|
import { extractSession } from '../lib/session-extractor.js';
|
|
16
16
|
const CLAUDE_PROJECTS = join(homedir(), '.claude', 'projects');
|
|
17
17
|
function usage() {
|
|
@@ -76,7 +76,7 @@ async function main() {
|
|
|
76
76
|
console.log(`[skip] empty or invalid session: ${sessionFile}`);
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
|
-
const projectName =
|
|
79
|
+
const projectName = detectProjectName(parsed) || 'unknown';
|
|
80
80
|
const result = extractSession(parsed, projectName);
|
|
81
81
|
if (dryRun) {
|
|
82
82
|
console.log(`[dry] session ${result.session_id.slice(0, 8)} (${projectName}): ${result.memories.length} memories, ${result.file_edits.length} file_edits`);
|
|
@@ -86,9 +86,10 @@ async function main() {
|
|
|
86
86
|
return;
|
|
87
87
|
// Idempotent: wipe any prior data for THIS session before re-inserting
|
|
88
88
|
const wiped = wipeSession(db, result.session_id);
|
|
89
|
-
// Resolve project entity
|
|
89
|
+
// Resolve project entity (canonical_key = project:<name>, so same project across
|
|
90
|
+
// different sessions/cwds collapses to one entity)
|
|
90
91
|
let projectEntityId;
|
|
91
|
-
const canonicalKey =
|
|
92
|
+
const canonicalKey = `project:${projectName.toLowerCase()}`;
|
|
92
93
|
const existing = db.prepare('SELECT id FROM entities WHERE canonical_key = ? OR (kind = ? AND LOWER(name) = LOWER(?))').get(canonicalKey, 'project', projectName);
|
|
93
94
|
if (existing) {
|
|
94
95
|
projectEntityId = existing.id;
|
|
@@ -169,7 +170,11 @@ async function main() {
|
|
|
169
170
|
continue;
|
|
170
171
|
}
|
|
171
172
|
agg.projects++;
|
|
172
|
-
|
|
173
|
+
// Decode Claude Code's project-dir encoding: "C--Users-HP-KanseiLINK" → "KanseiLINK"
|
|
174
|
+
// (the encoding replaces `/` with `--`, so the last segment is the actual project name)
|
|
175
|
+
const rawDirName = projectDir.replace(/\\/g, '/').split('/').filter(Boolean).pop() ?? 'unknown';
|
|
176
|
+
const decodedParts = rawDirName.split('--').filter(Boolean);
|
|
177
|
+
const dirName = decodedParts.length > 1 ? decodedParts[decodedParts.length - 1] : rawDirName;
|
|
173
178
|
console.log(`\n=== Project: ${dirName} (${files.length} session files) ===`);
|
|
174
179
|
let projectEntityId = null;
|
|
175
180
|
for (const f of files) {
|
|
@@ -186,7 +191,10 @@ async function main() {
|
|
|
186
191
|
agg.sessions_skipped++;
|
|
187
192
|
continue;
|
|
188
193
|
}
|
|
189
|
-
|
|
194
|
+
// Detect project name from actual file edits (majority vote), falling back
|
|
195
|
+
// to cwd-based detection. Previously used cwd alone, which collapsed many
|
|
196
|
+
// distinct projects under one entity (e.g. "Card_Navi" for everything).
|
|
197
|
+
const projectName = detectProjectName(parsed) || dirName;
|
|
190
198
|
const result = extractSession(parsed, projectName);
|
|
191
199
|
agg.sessions_parsed++;
|
|
192
200
|
agg.memories_planned += result.memories.length;
|
|
@@ -196,17 +204,16 @@ async function main() {
|
|
|
196
204
|
}
|
|
197
205
|
if (!db)
|
|
198
206
|
continue;
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
207
|
+
// Resolve entity per-SESSION (not per-project-dir) since each session may
|
|
208
|
+
// belong to a different project based on its file ops.
|
|
209
|
+
const canonicalKey = `project:${projectName.toLowerCase()}`;
|
|
210
|
+
const existing = db.prepare('SELECT id FROM entities WHERE canonical_key = ? OR (kind = ? AND LOWER(name) = LOWER(?))').get(canonicalKey, 'project', projectName);
|
|
211
|
+
if (existing) {
|
|
212
|
+
projectEntityId = existing.id;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const ins = db.prepare('INSERT INTO entities (kind, name, canonical_key) VALUES (?, ?, ?)').run('project', projectName, canonicalKey);
|
|
216
|
+
projectEntityId = Number(ins.lastInsertRowid);
|
|
210
217
|
}
|
|
211
218
|
// Idempotent: wipe any prior data for THIS session before re-inserting (Phase B)
|
|
212
219
|
wipeSession(db, result.session_id);
|
|
@@ -38,3 +38,6 @@ export declare function isPastedExternalContent(text: string): boolean;
|
|
|
38
38
|
export declare function isAutomatedSession(firstUserText: string): boolean;
|
|
39
39
|
export declare function parseSessionFile(jsonlPath: string): ParsedSession | null;
|
|
40
40
|
export declare function projectNameFromCwd(cwd: string): string;
|
|
41
|
+
export declare function projectNameFromFilePath(filePath: string): string | null;
|
|
42
|
+
export declare function projectNameFromFileOps(file_ops: ParsedSession['file_ops']): string | null;
|
|
43
|
+
export declare function detectProjectName(parsed: ParsedSession): string;
|
|
@@ -300,4 +300,97 @@ export function projectNameFromCwd(cwd) {
|
|
|
300
300
|
return 'unknown';
|
|
301
301
|
return cwd.replace(/\\/g, '/').replace(/\/$/, '').split('/').filter(Boolean).pop() ?? 'unknown';
|
|
302
302
|
}
|
|
303
|
+
// ─── Improved entity detection ───────────────────────────────────────────
|
|
304
|
+
// Problem: projectNameFromCwd() uses the session's starting cwd, but Claude Code
|
|
305
|
+
// sessions often edit files across many projects. Everything ends up tagged with
|
|
306
|
+
// the one cwd name (e.g. "Card_Navi"), destroying recall precision.
|
|
307
|
+
//
|
|
308
|
+
// Solution: inspect file_ops, extract the project name from each file path,
|
|
309
|
+
// and take the majority vote. Fall back to cwd if no useful file ops exist.
|
|
310
|
+
// Directories we never treat as "project" — they're just user home conventions
|
|
311
|
+
// or Claude Code internal dirs.
|
|
312
|
+
const SKIP_DIR_SEGMENTS = new Set([
|
|
313
|
+
'.claude', '.config', '.cache', '.local', '.vscode', '.git', '.ssh', '.npm',
|
|
314
|
+
'Downloads', 'Documents', 'Desktop', 'Pictures', 'Music', 'Videos',
|
|
315
|
+
'node_modules', 'tmp', 'temp', 'AppData', 'Library',
|
|
316
|
+
'OneDrive', 'Dropbox', 'Google Drive',
|
|
317
|
+
'Users', 'home', 'HP', // part of the user home path itself
|
|
318
|
+
// Claude Code internal subdirs (appear when Claude reads its own state files):
|
|
319
|
+
'projects', 'commands', 'skills', 'worktrees', 'hooks', 'agents', 'plugins',
|
|
320
|
+
'settings', 'memory', 'todos', 'shell-snapshots', 'ide',
|
|
321
|
+
]);
|
|
322
|
+
// User-home-like prefixes we strip before looking for the project segment.
|
|
323
|
+
// Add drive-letter-agnostic patterns that match Windows / macOS / Linux / WSL.
|
|
324
|
+
const HOME_PATTERNS = [
|
|
325
|
+
/^[A-Za-z]:\/Users\/[^\/]+\//i, // C:/Users/HP/...
|
|
326
|
+
/^\/Users\/[^\/]+\//, // /Users/HP/... (macOS)
|
|
327
|
+
/^\/home\/[^\/]+\//, // /home/HP/... (Linux)
|
|
328
|
+
/^\/mnt\/[a-z]\/Users\/[^\/]+\//i, // /mnt/c/Users/HP/... (WSL)
|
|
329
|
+
/^[A-Za-z]:\/Set up company\//i, // D:/Set up company/... (Michie-specific)
|
|
330
|
+
];
|
|
331
|
+
function stripHomePrefix(normalized) {
|
|
332
|
+
for (const p of HOME_PATTERNS) {
|
|
333
|
+
if (p.test(normalized))
|
|
334
|
+
return normalized.replace(p, '');
|
|
335
|
+
}
|
|
336
|
+
return normalized;
|
|
337
|
+
}
|
|
338
|
+
// Extract a project name from a single file path.
|
|
339
|
+
// Returns null if the path doesn't contain a meaningful project segment.
|
|
340
|
+
export function projectNameFromFilePath(filePath) {
|
|
341
|
+
if (!filePath)
|
|
342
|
+
return null;
|
|
343
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
344
|
+
// Paths inside Claude Code's own state dir are internal — never a real project signal.
|
|
345
|
+
// e.g. C:/Users/HP/.claude/projects/C--Users-HP-KanseiLINK/abc.jsonl
|
|
346
|
+
if (/\/\.claude\//.test(normalized))
|
|
347
|
+
return null;
|
|
348
|
+
const rest = stripHomePrefix(normalized);
|
|
349
|
+
if (rest === normalized)
|
|
350
|
+
return null; // no home prefix matched, can't safely extract
|
|
351
|
+
const segments = rest.split('/').filter(Boolean);
|
|
352
|
+
for (const seg of segments) {
|
|
353
|
+
if (SKIP_DIR_SEGMENTS.has(seg))
|
|
354
|
+
continue;
|
|
355
|
+
if (seg.startsWith('.'))
|
|
356
|
+
continue;
|
|
357
|
+
// Skip single-file-like segments (have extension) — we want a directory name
|
|
358
|
+
if (/\.[a-z0-9]{1,5}$/i.test(seg))
|
|
359
|
+
continue;
|
|
360
|
+
// Skip Claude Code's project-dir encoding (e.g. "C--Users-HP-Xxx")
|
|
361
|
+
if (/^[A-Z]--/.test(seg))
|
|
362
|
+
continue;
|
|
363
|
+
return seg;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
// Aggregate file_ops into a single project name by majority vote.
|
|
368
|
+
// Returns null if no file_op yields a usable name.
|
|
369
|
+
export function projectNameFromFileOps(file_ops) {
|
|
370
|
+
if (!file_ops || file_ops.length === 0)
|
|
371
|
+
return null;
|
|
372
|
+
const counts = new Map();
|
|
373
|
+
for (const op of file_ops) {
|
|
374
|
+
const name = projectNameFromFilePath(op.path);
|
|
375
|
+
if (name)
|
|
376
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
377
|
+
}
|
|
378
|
+
if (counts.size === 0)
|
|
379
|
+
return null;
|
|
380
|
+
let bestName = null;
|
|
381
|
+
let bestCount = 0;
|
|
382
|
+
for (const [name, count] of counts) {
|
|
383
|
+
if (count > bestCount) {
|
|
384
|
+
bestName = name;
|
|
385
|
+
bestCount = count;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return bestName;
|
|
389
|
+
}
|
|
390
|
+
// Combined detector: prefers file_ops majority, falls back to cwd-based.
|
|
391
|
+
// This is the function import-sessions.ts should call.
|
|
392
|
+
export function detectProjectName(parsed) {
|
|
393
|
+
return (projectNameFromFileOps(parsed.file_ops) ??
|
|
394
|
+
projectNameFromCwd(parsed.project_cwd));
|
|
395
|
+
}
|
|
303
396
|
//# sourceMappingURL=session-parser.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linksee-memory",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"mcpName": "io.github.michielinksee/linksee-memory",
|
|
4
5
|
"description": "Local-first agent memory MCP — cross-agent brain with 6-layer structured memory + token-saving file diff cache",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|