linksee-memory 0.3.0 → 0.4.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # linksee-memory
2
2
 
3
- > Local-first agent memory MCP. A cross-agent brain for Claude Code, Cursor, and ChatGPT Desktop — with a token-saving file diff cache that nobody else does.
3
+ > Local-first agent memory MCP. A cross-agent brain for **Claude Code, Cursor, OpenAI Codex, and Gemini CLI** — with a token-saving file diff cache that nobody else does. One SQLite file, all your LLMs read the same memory.
4
4
  >
5
5
  > **v0.3.0** ships the **Five Blocks**: Tools + Resources + Prompts + Sampling + Roots, plus the newer **Elicitation** primitive. Most public MCP servers expose only Tools; v0.3.0 moves linksee-memory into the differentiated tier. Backward compatible — all 8 v0.2.x tools keep their signatures. See [CHANGELOG.md](./CHANGELOG.md).
6
6
 
@@ -9,7 +9,17 @@
9
9
  [![mcp-registry](https://img.shields.io/badge/MCP-Official_Registry-6366f1)](https://registry.modelcontextprotocol.io/)
10
10
  [![glama-score](https://glama.ai/mcp/servers/michielinksee/linksee-memory/badges/score.svg)](https://glama.ai/mcp/servers/michielinksee/linksee-memory)
11
11
 
12
- 🌐 **Landing page**: [linksee-site.vercel.app](https://linksee-site.vercel.app) (includes non-developer onboarding for Claude Desktop / Cursor / Claude Code)
12
+ 🌐 **Landing page**: [linksee-site.vercel.app](https://linksee-site.vercel.app) (includes non-developer onboarding for Claude Desktop / Cursor / Claude Code / OpenAI Codex / Gemini CLI)
13
+
14
+ ## 📣 As featured on
15
+
16
+ - **Zenn**: [あなたの Claude Code、 実は前回のセッションを完全に忘れている](https://zenn.dev/kanseilink/articles/linksee-memory-claude-code-recall-20260508) — 73 ♡ on Zenn, **165+ users on Hatena Bookmark**, picked up by tech blogs + YouTube shorts (May 2026)
17
+ - **Zenn**: [あなたの MCP server、 実は Tools しか使ってない (5 blocks 全実装 / v0.3.0)](https://zenn.dev/kanseilink/articles/linksee-memory-mcp-five-blocks-20260507) — the 1% of MCP servers that implement all 5 blocks
18
+ - **Zenn**: [あなたの Claude memory、 実は Claude にしか残らない (5 LLM 横断する方法)](https://zenn.dev/kanseilink/articles/linksee-memory-claude-cross-llm-20260511) — cross-LLM memory pattern (May 12, 2026)
19
+ - **Zenn**: [Glama listing で 3 週間止まった話 (5 つの罠と解決策)](https://zenn.dev/michielinksee/articles/linksee-memory-mcp-publish-glama-traps-20260506) — npm + Glama deployment retrospective
20
+
21
+ > 「Cordex/Cursor/Code/Gemini 全部につなげられるから、 横断的にできてる MCP ってところがこれのすごいところ」
22
+ > — [Hatena Bookmark, May 2026](https://b.hatena.ne.jp/entry/s/zenn.dev/kanseilink/articles/linksee-memory-claude-code-recall-20260508) (165+ users)
13
23
 
14
24
  ---
15
25
 
@@ -88,47 +98,46 @@ It is a Model Context Protocol (MCP) server that gives any AI agent four superpo
88
98
  ## Three pillars
89
99
 
90
100
  1. **Token savings** via `read_smart` — sha256 + AST/heading/indent chunking. Re-reads return only diffs. **Measured 86% saved on a typical TS file edit, 99% saved on unchanged re-reads.**
91
- 2. **Cross-agent portability** — single SQLite file at `~/.linksee-memory/memory.db`. Same brain for Claude Code, Cursor, ChatGPT Desktop.
101
+ 2. **Cross-agent portability** — single SQLite file at `~/.linksee-memory/memory.db`. Same brain for Claude Code, Cursor, OpenAI Codex, Gemini CLI. (ChatGPT app needs Remote MCP — on roadmap for v0.4.)
92
102
  3. **WHY-first structured memory** — six explicit layers (`goal` / `context` / `emotion` / `implementation` / `caveat` / `learning`). Solves "flat fact memory is useless without goals".
93
103
 
94
- ## Install
104
+ ## Quick Start — One Command
95
105
 
96
106
  ```bash
97
- npm install -g linksee-memory
98
- linksee-memory-import --help # bundled importer for Claude Code session history
107
+ npx linksee-memory-setup
99
108
  ```
100
109
 
101
- Or use `npx` ad hoc:
110
+ This does everything:
111
+ 1. Registers the MCP server with Claude Code
112
+ 2. Installs the agent skill (teaches the agent when to recall/remember)
113
+ 3. Configures auto-capture (every session saved to your local brain)
102
114
 
103
- ```bash
104
- npx linksee-memory # starts the MCP server on stdio
105
- ```
115
+ Restart Claude Code, then just chat normally. Add **"Use Linksee"** to any prompt to trigger memory recall.
116
+
117
+ ### Manual setup (if you prefer step-by-step)
106
118
 
107
- The default database lives at `~/.linksee-memory/memory.db`. Override with the `LINKSEE_MEMORY_DIR` environment variable.
119
+ <details>
120
+ <summary>Click to expand manual installation</summary>
108
121
 
109
- ## Register with Claude Code
122
+ **Install & register:**
110
123
 
111
124
  ```bash
112
125
  claude mcp add -s user linksee -- npx -y linksee-memory
113
126
  ```
114
127
 
115
- Restart Claude Code. Tools appear as `mcp__linksee__remember`, `mcp__linksee__recall`, `mcp__linksee__recall_file`, `mcp__linksee__read_smart`, `mcp__linksee__forget`, `mcp__linksee__consolidate`.
128
+ Tools appear as `mcp__linksee__remember`, `mcp__linksee__recall`, `mcp__linksee__recall_file`, `mcp__linksee__read_smart`, `mcp__linksee__forget`, `mcp__linksee__consolidate`.
116
129
 
117
- ### Recommended: install the skill (auto-invocation)
118
-
119
- Installing the MCP alone doesn't teach Claude Code *when* to call `recall` / `remember`. The bundled skill fixes that:
130
+ **Install the skill (auto-invocation):**
120
131
 
121
132
  ```bash
122
133
  npx -y linksee-memory-install-skill
123
134
  ```
124
135
 
125
- This copies a `SKILL.md` to `~/.claude/skills/linksee-memory/`. Claude Code auto-discovers it and fires the skill on phrases like "前に…", "また同じエラー", "覚えておいて", new task starts, file edits, and so on — no need to say "use linksee-memory".
126
-
127
- Flags: `--dry-run`, `--force`, `--help`.
136
+ Copies `SKILL.md` to `~/.claude/skills/linksee-memory/`. Agent auto-fires on phrases like "前に…", "また同じエラー", "覚えておいて", new task starts, file edits, etc.
128
137
 
129
- ### Optional: auto-capture every session (Stop hook)
138
+ **Configure auto-capture (Stop hook):**
130
139
 
131
- Add to `~/.claude/settings.json` to record every Claude Code session to your local brain automatically:
140
+ Add to `~/.claude/settings.json`:
132
141
 
133
142
  ```json
134
143
  {
@@ -145,7 +154,13 @@ Add to `~/.claude/settings.json` to record every Claude Code session to your loc
145
154
  }
146
155
  ```
147
156
 
148
- Each turn end takes ~100 ms. Failures are silent (Claude Code never blocks). Logs at `~/.linksee-memory/hook.log`.
157
+ Each turn end takes ~100 ms. Failures are silent. Logs at `~/.linksee-memory/hook.log`.
158
+
159
+ </details>
160
+
161
+ ### Database location
162
+
163
+ Default: `~/.linksee-memory/memory.db`. Override with `LINKSEE_MEMORY_DIR` env var.
149
164
 
150
165
  ## v0.3.0 — Five Blocks at a glance
151
166
 
@@ -226,9 +241,12 @@ The conversation↔file linkage is the key. Every file edit captured by the Stop
226
241
  - ✅ Core 6 MCP tools (`remember` / `recall` / `recall_file` / `forget` / `consolidate` / `read_smart`)
227
242
  - ✅ Stop-hook auto-capture for Claude Code
228
243
  - ✅ JP/EN trigram FTS5
244
+ - ✅ Five Blocks (v0.3.0): Tools + Resources + Prompts + Sampling + Roots + Elicitation
245
+ - ✅ Cursor + OpenAI Codex + Gemini CLI adapters (stdio MCP, same `npx -y linksee-memory`)
229
246
  - 🚧 `PreToolUse` hook to auto-intercept `Read` (zero-config token savings)
230
- - 🚧 Cursor + ChatGPT Desktop adapters
231
- - 🔮 Vector search via `sqlite-vec` once an embedding backend is chosen (Ollama / API / etc.)
247
+ - 🔮 ChatGPT app (web/mobile) support via `linksee-memory-remote` (Remote MCP over HTTPS, v0.4)
248
+ - 🔮 Vector search via `sqlite-vec` (already in deps, embedding backend pending)
249
+ - 🔮 Cross-device cloud sync (Pro tier, summer 2026)
232
250
  - 🔮 Optional anonymized telemetry → MCP-quality intelligence layer
233
251
 
234
252
  ## Comparison with Claude Code auto-memory
@@ -376,8 +394,8 @@ Three axes:
376
394
  <details>
377
395
  <summary><strong>Why not just use Claude's built-in auto-memory?</strong></summary>
378
396
 
379
- Claude Code's auto-memory is Claude-only (doesn't help if you switch to Cursor or ChatGPT Desktop) and stores flat markdown with no structure. linksee-memory is the same local-first principle but:
380
- - Works across Claude Code, Cursor, ChatGPT Desktop (shared SQLite)
397
+ Claude Code's auto-memory is Claude-only (doesn't help if you switch to Cursor, OpenAI Codex, or Gemini CLI) and stores flat markdown with no structure. linksee-memory is the same local-first principle but:
398
+ - Works across Claude Code, Cursor, OpenAI Codex, Gemini CLI (shared SQLite)
381
399
  - Structured 6-layer format makes recall explainable
382
400
  - Provides explicit forget/consolidate primitives rather than the agent guessing
383
401
  </details>
@@ -415,8 +433,10 @@ In practice a solo developer hits ~100MB after 6 months of heavy use. A year-old
415
433
  Yes — any MCP-compatible client works:
416
434
  - **Claude Code**: `claude mcp add -s user linksee -- npx -y linksee-memory`
417
435
  - **Claude Desktop**: add to `claude_desktop_config.json` (see [onboarding on the LP](https://linksee-site.vercel.app))
418
- - **Cursor**: add to MCP settings in Cursor
419
- - **ChatGPT Desktop**: same pattern once MCP support ships
436
+ - **Cursor**: add to MCP settings in Cursor → Settings → Features → Model Context Protocol
437
+ - **OpenAI Codex**: `codex mcp add linksee -- npx -y linksee-memory` (or `~/.codex/config.toml` with `[mcp_servers.linksee]` block)
438
+ - **Gemini CLI**: add to `~/.gemini/settings.json` mcpServers section
439
+ - **ChatGPT (web/mobile app)**: stdio MCP not supported by the consumer app — requires Remote MCP server over HTTPS. `linksee-memory-remote` planned for v0.4.
420
440
  - **Custom agent**: the MCP stdio protocol is documented at modelcontextprotocol.io
421
441
  </details>
422
442
 
@@ -13,6 +13,7 @@ import { homedir } from 'node:os';
13
13
  import { openDb, runMigrations } from '../db/migrate.js';
14
14
  import { parseSessionFile, detectProjectName } from '../lib/session-parser.js';
15
15
  import { extractSession } from '../lib/session-extractor.js';
16
+ import { normalizeEntityName } from '../lib/normalize.js';
16
17
  const CLAUDE_PROJECTS = join(homedir(), '.claude', 'projects');
17
18
  function usage() {
18
19
  console.log(`Usage:
@@ -90,12 +91,15 @@ async function main() {
90
91
  // different sessions/cwds collapses to one entity)
91
92
  let projectEntityId;
92
93
  const canonicalKey = `project:${projectName.toLowerCase()}`;
93
- const existing = db.prepare('SELECT id FROM entities WHERE canonical_key = ? OR (kind = ? AND LOWER(name) = LOWER(?))').get(canonicalKey, 'project', projectName);
94
+ const normalized = normalizeEntityName(projectName);
95
+ const existing = db.prepare('SELECT id FROM entities WHERE canonical_key = ? OR (kind = ? AND normalized_name = ?) OR (kind = ? AND LOWER(name) = LOWER(?))').get(canonicalKey, 'project', normalized, 'project', projectName);
94
96
  if (existing) {
95
97
  projectEntityId = existing.id;
98
+ // Backfill normalized_name if missing
99
+ db.prepare('UPDATE entities SET normalized_name = ? WHERE id = ? AND normalized_name IS NULL').run(normalized, projectEntityId);
96
100
  }
97
101
  else {
98
- const ins = db.prepare('INSERT INTO entities (kind, name, canonical_key) VALUES (?, ?, ?)').run('project', projectName, canonicalKey);
102
+ const ins = db.prepare('INSERT INTO entities (kind, name, normalized_name, canonical_key) VALUES (?, ?, ?, ?)').run('project', projectName, normalized, canonicalKey);
99
103
  projectEntityId = Number(ins.lastInsertRowid);
100
104
  }
101
105
  const insMem = db.prepare('INSERT INTO memories (entity_id, layer, content, importance, source) VALUES (?, ?, ?, ?, ?)');
@@ -207,12 +211,14 @@ async function main() {
207
211
  // Resolve entity per-SESSION (not per-project-dir) since each session may
208
212
  // belong to a different project based on its file ops.
209
213
  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);
214
+ const normalized = normalizeEntityName(projectName);
215
+ const existing = db.prepare('SELECT id FROM entities WHERE canonical_key = ? OR (kind = ? AND normalized_name = ?) OR (kind = ? AND LOWER(name) = LOWER(?))').get(canonicalKey, 'project', normalized, 'project', projectName);
211
216
  if (existing) {
212
217
  projectEntityId = existing.id;
218
+ db.prepare('UPDATE entities SET normalized_name = ? WHERE id = ? AND normalized_name IS NULL').run(normalized, projectEntityId);
213
219
  }
214
220
  else {
215
- const ins = db.prepare('INSERT INTO entities (kind, name, canonical_key) VALUES (?, ?, ?)').run('project', projectName, canonicalKey);
221
+ const ins = db.prepare('INSERT INTO entities (kind, name, normalized_name, canonical_key) VALUES (?, ?, ?, ?)').run('project', projectName, normalized, canonicalKey);
216
222
  projectEntityId = Number(ins.lastInsertRowid);
217
223
  }
218
224
  // Idempotent: wipe any prior data for THIS session before re-inserting (Phase B)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ // setup: One-command setup for Linksee Memory — the "Use Linksee" installer.
3
+ //
4
+ // Usage:
5
+ // npx linksee-memory-setup (interactive setup)
6
+ // npx linksee-memory-setup --yes (accept all defaults, no prompts)
7
+ // npx linksee-memory-setup --dry-run
8
+ //
9
+ // Does three things:
10
+ // 1. Registers the MCP server with Claude Code
11
+ // 2. Installs the SKILL.md (agent trigger phrases)
12
+ // 3. Configures the Stop hook (auto-capture sessions)
13
+ //
14
+ // After setup, every Claude Code session:
15
+ // - Auto-captures decisions, learnings, caveats to local memory
16
+ // - Agent auto-recalls past context at task start (via SKILL.md triggers)
17
+ // - "Use Linksee" in any prompt forces a recall
18
+ //
19
+ // Why: Competing memory tools (claude-mem, etc.) are one-install-and-done.
20
+ // Our MCP approach gives more precision, but the setup was 3 manual steps.
21
+ // This command eliminates that friction entirely.
22
+ import { spawnSync } from 'node:child_process';
23
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
24
+ import { join, dirname } from 'node:path';
25
+ import { homedir } from 'node:os';
26
+ import { fileURLToPath } from 'node:url';
27
+ const args = process.argv.slice(2);
28
+ const dryRun = args.includes('--dry-run');
29
+ const autoYes = args.includes('--yes') || args.includes('-y');
30
+ const showHelp = args.includes('--help') || args.includes('-h');
31
+ if (showHelp) {
32
+ console.log(`linksee-memory-setup — One-command setup for Linksee Memory
33
+
34
+ Usage:
35
+ npx linksee-memory-setup Interactive setup
36
+ npx linksee-memory-setup --yes Accept all defaults, no prompts
37
+ npx linksee-memory-setup --dry-run Show what would happen
38
+
39
+ What it does:
40
+ 1. Registers linksee-memory MCP server with Claude Code
41
+ 2. Installs SKILL.md (teaches the agent when to recall/remember)
42
+ 3. Configures Stop hook (auto-captures every session)
43
+
44
+ After setup, just chat with Claude Code normally.
45
+ Add "Use Linksee" to any prompt to trigger memory recall.`);
46
+ process.exit(0);
47
+ }
48
+ // ── Constants ────────────────────────────────────────────
49
+ const HOME = homedir();
50
+ const CLAUDE_DIR = join(HOME, '.claude');
51
+ const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');
52
+ const SKILL_DIR = join(CLAUDE_DIR, 'skills', 'linksee-memory');
53
+ const SKILL_TARGET = join(SKILL_DIR, 'SKILL.md');
54
+ const __filename = fileURLToPath(import.meta.url);
55
+ const SKILL_SRC = join(dirname(__filename), '..', 'skill', 'SKILL.md');
56
+ const SERVER_NAME = 'linksee';
57
+ const MCP_COMMAND = `claude mcp add -s user ${SERVER_NAME} -- npx -y linksee-memory`;
58
+ const HOOK_COMMAND = 'npx -y linksee-memory-sync';
59
+ const CHECK = '\x1b[32m✓\x1b[0m';
60
+ const SKIP = '\x1b[33m○\x1b[0m';
61
+ const FAIL = '\x1b[31m✗\x1b[0m';
62
+ const BOLD = '\x1b[1m';
63
+ const DIM = '\x1b[2m';
64
+ const RESET = '\x1b[0m';
65
+ console.log('');
66
+ console.log(`${BOLD}Linksee Memory Setup${RESET}`);
67
+ console.log(`${DIM}Local-first cross-LLM memory · precision recall${RESET}`);
68
+ console.log('');
69
+ // ── Step 1: Register MCP server ──────────────────────────
70
+ console.log(`${BOLD}[1/3]${RESET} Registering MCP server...`);
71
+ let mcpAlreadyRegistered = false;
72
+ try {
73
+ // Check if already registered by looking at settings.json or .claude.json
74
+ for (const confFile of [SETTINGS_PATH, join(HOME, '.claude.json')]) {
75
+ if (!existsSync(confFile))
76
+ continue;
77
+ try {
78
+ const conf = JSON.parse(readFileSync(confFile, 'utf8'));
79
+ if (conf?.mcpServers?.[SERVER_NAME]) {
80
+ mcpAlreadyRegistered = true;
81
+ break;
82
+ }
83
+ }
84
+ catch { /* ignore parse errors */ }
85
+ }
86
+ }
87
+ catch { /* ignore */ }
88
+ if (mcpAlreadyRegistered) {
89
+ console.log(` ${SKIP} MCP server '${SERVER_NAME}' already registered`);
90
+ }
91
+ else if (dryRun) {
92
+ console.log(` ${DIM}[dry-run] Would run: ${MCP_COMMAND}${RESET}`);
93
+ }
94
+ else {
95
+ try {
96
+ // Check if 'claude' CLI is available
97
+ const which = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['claude'], {
98
+ encoding: 'utf8',
99
+ timeout: 5000,
100
+ });
101
+ if (which.status !== 0) {
102
+ console.log(` ${FAIL} 'claude' CLI not found. Install Claude Code first:`);
103
+ console.log(` https://docs.anthropic.com/en/docs/claude-code`);
104
+ console.log(` ${DIM}Then run this setup again.${RESET}`);
105
+ }
106
+ else {
107
+ const r = spawnSync('claude', ['mcp', 'add', '-s', 'user', SERVER_NAME, '--', 'npx', '-y', 'linksee-memory'], {
108
+ encoding: 'utf8',
109
+ timeout: 15000,
110
+ stdio: ['pipe', 'pipe', 'pipe'],
111
+ });
112
+ if (r.status === 0) {
113
+ console.log(` ${CHECK} MCP server registered as '${SERVER_NAME}'`);
114
+ }
115
+ else {
116
+ // May fail if already exists with different config
117
+ const stderr = (r.stderr || '').trim();
118
+ if (stderr.includes('already exists') || stderr.includes('already registered')) {
119
+ console.log(` ${SKIP} MCP server '${SERVER_NAME}' already registered`);
120
+ }
121
+ else {
122
+ console.log(` ${FAIL} Registration failed: ${stderr || 'unknown error'}`);
123
+ console.log(` ${DIM}Manual: ${MCP_COMMAND}${RESET}`);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ catch (e) {
129
+ console.log(` ${FAIL} Error: ${e?.message ?? e}`);
130
+ console.log(` ${DIM}Manual: ${MCP_COMMAND}${RESET}`);
131
+ }
132
+ }
133
+ console.log('');
134
+ // ── Step 2: Install SKILL.md ─────────────────────────────
135
+ console.log(`${BOLD}[2/3]${RESET} Installing agent skill...`);
136
+ if (!existsSync(SKILL_SRC)) {
137
+ console.log(` ${FAIL} Bundled SKILL.md not found (packaging bug)`);
138
+ console.log(` ${DIM}Expected at: ${SKILL_SRC}${RESET}`);
139
+ }
140
+ else if (dryRun) {
141
+ console.log(` ${DIM}[dry-run] Would copy to: ${SKILL_TARGET}${RESET}`);
142
+ }
143
+ else {
144
+ mkdirSync(SKILL_DIR, { recursive: true });
145
+ let shouldWrite = true;
146
+ if (existsSync(SKILL_TARGET)) {
147
+ try {
148
+ const existing = readFileSync(SKILL_TARGET, 'utf8');
149
+ const bundled = readFileSync(SKILL_SRC, 'utf8');
150
+ if (existing === bundled) {
151
+ console.log(` ${SKIP} Skill already installed and up to date`);
152
+ shouldWrite = false;
153
+ }
154
+ else {
155
+ // Newer version — overwrite
156
+ console.log(` ${DIM}Updating to latest version...${RESET}`);
157
+ }
158
+ }
159
+ catch { /* fallthrough to write */ }
160
+ }
161
+ if (shouldWrite) {
162
+ copyFileSync(SKILL_SRC, SKILL_TARGET);
163
+ console.log(` ${CHECK} Skill installed → ${SKILL_TARGET}`);
164
+ }
165
+ }
166
+ console.log('');
167
+ // ── Step 3: Configure Stop hook ──────────────────────────
168
+ console.log(`${BOLD}[3/3]${RESET} Configuring auto-capture hook...`);
169
+ let settings = {};
170
+ if (existsSync(SETTINGS_PATH)) {
171
+ try {
172
+ settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
173
+ }
174
+ catch {
175
+ console.log(` ${FAIL} Could not parse ${SETTINGS_PATH}`);
176
+ settings = {};
177
+ }
178
+ }
179
+ // Check if hook already exists
180
+ const stopHooks = settings?.hooks?.Stop ?? [];
181
+ const alreadyHooked = stopHooks.some((entry) => entry.hooks?.some((h) => h.command?.includes('linksee-memory-sync')));
182
+ if (alreadyHooked) {
183
+ console.log(` ${SKIP} Stop hook already configured`);
184
+ }
185
+ else if (dryRun) {
186
+ console.log(` ${DIM}[dry-run] Would add Stop hook to ${SETTINGS_PATH}${RESET}`);
187
+ }
188
+ else {
189
+ // Add hook
190
+ if (!settings.hooks)
191
+ settings.hooks = {};
192
+ if (!settings.hooks.Stop)
193
+ settings.hooks.Stop = [];
194
+ settings.hooks.Stop.push({
195
+ matcher: '',
196
+ hooks: [{ type: 'command', command: HOOK_COMMAND }],
197
+ });
198
+ mkdirSync(CLAUDE_DIR, { recursive: true });
199
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8');
200
+ console.log(` ${CHECK} Stop hook added → ${SETTINGS_PATH}`);
201
+ }
202
+ console.log('');
203
+ // ── Summary ──────────────────────────────────────────────
204
+ console.log(`${BOLD}Setup complete!${RESET}`);
205
+ console.log('');
206
+ console.log('How it works:');
207
+ console.log(` ${DIM}• Every session is auto-captured (decisions, caveats, learnings)${RESET}`);
208
+ console.log(` ${DIM}• Agent auto-recalls past context when starting a task${RESET}`);
209
+ console.log(` ${DIM}• Memory is local-first (nothing leaves your machine)${RESET}`);
210
+ console.log(` ${DIM}• Works across Claude Code, Cursor, ChatGPT (cross-LLM)${RESET}`);
211
+ console.log('');
212
+ console.log('Test by asking:');
213
+ console.log(` ${BOLD}"How did we solve this before?"${RESET}`);
214
+ console.log(` ${BOLD}"Same error again"${RESET}`);
215
+ console.log(` ${BOLD}"Remember: I prefer TypeScript over JavaScript"${RESET}`);
216
+ console.log(` ${BOLD}「前にこの問題どう解決したっけ」${RESET}`);
217
+ console.log('');
218
+ console.log(`Or add ${BOLD}"Use Linksee"${RESET} to any prompt to trigger memory recall.`);
219
+ console.log('');
220
+ //# sourceMappingURL=setup.js.map
@@ -4,6 +4,7 @@ import { mkdirSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { normalizeEntityName } from '../lib/normalize.js';
7
8
  const DEFAULT_DB_DIR = process.env.LINKSEE_MEMORY_DIR ?? join(homedir(), '.linksee-memory');
8
9
  const DB_PATH = join(DEFAULT_DB_DIR, 'memory.db');
9
10
  export function getDbPath() {
@@ -39,6 +40,14 @@ export function runMigrations(db) {
39
40
  DROP TABLE IF EXISTS memories_fts;
40
41
  `);
41
42
  }
43
+ // v4 → v5: add normalized_name column BEFORE schema.sql runs,
44
+ // so the CREATE INDEX IF NOT EXISTS on (kind, normalized_name) succeeds.
45
+ if (currentVersion > 0 && currentVersion < 5) {
46
+ const cols = db.prepare("PRAGMA table_info(entities)").all();
47
+ if (!cols.some(c => c.name === 'normalized_name')) {
48
+ db.exec('ALTER TABLE entities ADD COLUMN normalized_name TEXT');
49
+ }
50
+ }
42
51
  db.exec(sql);
43
52
  if (currentVersion > 0 && currentVersion < 4) {
44
53
  db.exec(`INSERT INTO memories_fts(rowid, content) SELECT id, content FROM memories;`);
@@ -57,6 +66,95 @@ export function runMigrations(db) {
57
66
  if (hasMemories?.name) {
58
67
  db.prepare(`UPDATE memories SET protected = 1 WHERE importance >= 0.9 AND protected = 0`).run();
59
68
  }
69
+ // v4 → v5: entity name normalization for dedup prevention.
70
+ // Adds normalized_name column + backfills from existing entity names.
71
+ if (currentVersion > 0 && currentVersion < 5) {
72
+ migrateV5EntityNormalization(db);
73
+ }
74
+ }
75
+ /**
76
+ * v5 migration: add normalized_name column and backfill + merge duplicates.
77
+ */
78
+ function migrateV5EntityNormalization(db) {
79
+ // Column + index already added before db.exec(sql) above.
80
+ // Now backfill normalized_name for all entities.
81
+ const entities = db.prepare('SELECT id, name FROM entities').all();
82
+ const updateStmt = db.prepare('UPDATE entities SET normalized_name = ? WHERE id = ?');
83
+ db.transaction(() => {
84
+ for (const e of entities) {
85
+ updateStmt.run(normalizeEntityName(e.name), e.id);
86
+ }
87
+ })();
88
+ // 4. Auto-merge duplicate entities (same kind + normalized_name)
89
+ const dupes = db.prepare(`
90
+ SELECT kind, normalized_name, GROUP_CONCAT(id) as ids
91
+ FROM entities
92
+ WHERE normalized_name IS NOT NULL
93
+ GROUP BY kind, normalized_name
94
+ HAVING COUNT(*) > 1
95
+ `).all();
96
+ if (dupes.length > 0) {
97
+ console.log(`[linksee-memory] v5 migration: merging ${dupes.length} duplicate entity clusters`);
98
+ db.transaction(() => {
99
+ for (const dupe of dupes) {
100
+ const ids = dupe.ids.split(',').map(Number);
101
+ mergeEntityCluster(db, ids);
102
+ }
103
+ })();
104
+ }
105
+ }
106
+ /**
107
+ * Merge a cluster of duplicate entity IDs into one canonical entity.
108
+ * Picks the entity with the most memories (ties broken by canonical_key presence).
109
+ * Reassigns all memories, events, edges, consolidations to the kept entity.
110
+ */
111
+ function mergeEntityCluster(db, ids) {
112
+ if (ids.length < 2)
113
+ return;
114
+ // Score each entity: prefer most memories, then has canonical_key, then lowest id
115
+ const rows = db.prepare(`
116
+ SELECT e.id, e.name, e.canonical_key, COUNT(m.id) as mem_count
117
+ FROM entities e LEFT JOIN memories m ON m.entity_id = e.id
118
+ WHERE e.id IN (${ids.map(() => '?').join(',')})
119
+ GROUP BY e.id
120
+ ORDER BY mem_count DESC, (e.canonical_key IS NOT NULL) DESC, e.id ASC
121
+ `).all(...ids);
122
+ const keep = rows[0];
123
+ const mergeIds = rows.slice(1).map(r => r.id);
124
+ console.log(` merge: keeping "${keep.name}" (id=${keep.id}, ${keep.mem_count} memories), absorbing ids=[${mergeIds.join(',')}]`);
125
+ // Inherit canonical_key if the kept entity lacks one
126
+ // Clear donor's key first to avoid UNIQUE constraint violation
127
+ if (!keep.canonical_key) {
128
+ const donor = rows.find(r => r.id !== keep.id && r.canonical_key);
129
+ if (donor) {
130
+ db.prepare('UPDATE entities SET canonical_key = NULL WHERE id = ?').run(donor.id);
131
+ db.prepare('UPDATE entities SET canonical_key = ? WHERE id = ?').run(donor.canonical_key, keep.id);
132
+ }
133
+ }
134
+ for (const mid of mergeIds) {
135
+ // Reassign memories
136
+ db.prepare('UPDATE memories SET entity_id = ? WHERE entity_id = ?').run(keep.id, mid);
137
+ // Reassign events
138
+ db.prepare('UPDATE events SET entity_id = ? WHERE entity_id = ?').run(keep.id, mid);
139
+ // Reassign edges (both directions)
140
+ // Handle UNIQUE constraint: delete duplicates first
141
+ db.prepare(`
142
+ DELETE FROM edges WHERE from_id = ? AND EXISTS (
143
+ SELECT 1 FROM edges e2 WHERE e2.from_id = ? AND e2.to_id = edges.to_id AND e2.relation = edges.relation
144
+ )
145
+ `).run(mid, keep.id);
146
+ db.prepare('UPDATE edges SET from_id = ? WHERE from_id = ?').run(keep.id, mid);
147
+ db.prepare(`
148
+ DELETE FROM edges WHERE to_id = ? AND EXISTS (
149
+ SELECT 1 FROM edges e2 WHERE e2.to_id = ? AND e2.from_id = edges.from_id AND e2.relation = edges.relation
150
+ )
151
+ `).run(mid, keep.id);
152
+ db.prepare('UPDATE edges SET to_id = ? WHERE to_id = ?').run(keep.id, mid);
153
+ // Reassign consolidations
154
+ db.prepare('UPDATE consolidations SET entity_id = ? WHERE entity_id = ?').run(keep.id, mid);
155
+ // Delete the duplicate entity
156
+ db.prepare('DELETE FROM entities WHERE id = ?').run(mid);
157
+ }
60
158
  }
61
159
  // CLI entrypoint
62
160
  if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('migrate.ts') || process.argv[1]?.endsWith('migrate.js')) {
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS entities (
10
10
  id INTEGER PRIMARY KEY AUTOINCREMENT,
11
11
  kind TEXT NOT NULL CHECK (kind IN ('person', 'company', 'project', 'concept', 'file', 'other')),
12
12
  name TEXT NOT NULL,
13
+ normalized_name TEXT, -- lowercased, separator-normalized for dedup matching
13
14
  canonical_key TEXT UNIQUE,
14
15
  attributes TEXT,
15
16
  momentum_score REAL NOT NULL DEFAULT 0.0, -- 0-10 cached; refreshed on event insert
@@ -18,8 +19,9 @@ CREATE TABLE IF NOT EXISTS entities (
18
19
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
19
20
  );
20
21
 
21
- CREATE INDEX IF NOT EXISTS idx_entities_kind ON entities(kind);
22
- CREATE INDEX IF NOT EXISTS idx_entities_key ON entities(canonical_key);
22
+ CREATE INDEX IF NOT EXISTS idx_entities_kind ON entities(kind);
23
+ CREATE INDEX IF NOT EXISTS idx_entities_key ON entities(canonical_key);
24
+ CREATE INDEX IF NOT EXISTS idx_entities_normalized ON entities(kind, normalized_name);
23
25
 
24
26
  -- ============================================================
25
27
  -- Layer 3: Meanings — 6-layer structured memory per entity
@@ -194,6 +196,6 @@ CREATE TABLE IF NOT EXISTS meta (
194
196
  value TEXT NOT NULL
195
197
  );
196
198
 
197
- INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', '4');
199
+ INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', '5');
198
200
  INSERT OR IGNORE INTO meta (key, value) VALUES ('created_at', CAST(unixepoch() AS TEXT));
199
- UPDATE meta SET value = '4' WHERE key = 'schema_version' AND value IN ('1', '2', '3');
201
+ UPDATE meta SET value = '5' WHERE key = 'schema_version' AND value IN ('1', '2', '3', '4');
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Entity name normalization for deduplication.
3
+ *
4
+ * Problem: agents call the same project by different names:
5
+ * "CockpitMCP" / "Cockpit MCP" / "cockpit-mcp" / "Cockpit_MCP"
6
+ * "Linksee Memory" / "LinkseeMemory" / "linksee-memory"
7
+ * "Sake_Navi" / "Sake Navi"
8
+ *
9
+ * This module provides a single normalizeEntityName() that collapses
10
+ * all these variants to one canonical form for matching.
11
+ */
12
+ /**
13
+ * Normalize an entity name for dedup matching.
14
+ *
15
+ * Rules applied (in order):
16
+ * 1. CamelCase split: "CockpitMCP" → "Cockpit MCP"
17
+ * 2. Separator → space: "linksee-memory" → "linksee memory"
18
+ * 3. Lowercase: "Linksee Memory" → "linksee memory"
19
+ * 4. Collapse whitespace
20
+ * 5. Trim
21
+ *
22
+ * The result is used for equality matching (WHERE normalized_name = ?),
23
+ * NOT stored as the display name — the original name is preserved.
24
+ */
25
+ export declare function normalizeEntityName(name: string): string;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Entity name normalization for deduplication.
3
+ *
4
+ * Problem: agents call the same project by different names:
5
+ * "CockpitMCP" / "Cockpit MCP" / "cockpit-mcp" / "Cockpit_MCP"
6
+ * "Linksee Memory" / "LinkseeMemory" / "linksee-memory"
7
+ * "Sake_Navi" / "Sake Navi"
8
+ *
9
+ * This module provides a single normalizeEntityName() that collapses
10
+ * all these variants to one canonical form for matching.
11
+ */
12
+ /**
13
+ * Normalize an entity name for dedup matching.
14
+ *
15
+ * Rules applied (in order):
16
+ * 1. CamelCase split: "CockpitMCP" → "Cockpit MCP"
17
+ * 2. Separator → space: "linksee-memory" → "linksee memory"
18
+ * 3. Lowercase: "Linksee Memory" → "linksee memory"
19
+ * 4. Collapse whitespace
20
+ * 5. Trim
21
+ *
22
+ * The result is used for equality matching (WHERE normalized_name = ?),
23
+ * NOT stored as the display name — the original name is preserved.
24
+ */
25
+ export function normalizeEntityName(name) {
26
+ return name
27
+ // CamelCase boundaries: insert space between lowercase→uppercase
28
+ // "CockpitMCP" → "Cockpit MCP"
29
+ // "LinkseeMemory" → "Linksee Memory"
30
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
31
+ // Uppercase run followed by uppercase+lowercase:
32
+ // "MCPServer" → "MCP Server"
33
+ // "KanseiLINK" → "Kansei LINK"
34
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
35
+ // All separators → space
36
+ .replace(/[-_]/g, ' ')
37
+ // Lowercase everything
38
+ .toLowerCase()
39
+ // Collapse multiple spaces
40
+ .replace(/\s+/g, ' ')
41
+ .trim();
42
+ }
43
+ //# sourceMappingURL=normalize.js.map