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 +48 -28
- package/dist/bin/import-sessions.js +10 -4
- package/dist/bin/setup.d.ts +2 -0
- package/dist/bin/setup.js +220 -0
- package/dist/db/migrate.js +98 -0
- package/dist/db/schema.sql +6 -4
- package/dist/lib/normalize.d.ts +25 -0
- package/dist/lib/normalize.js +43 -0
- package/dist/lib/session-extractor.js +164 -38
- package/dist/mcp/server.js +23 -4
- package/dist/skill/SKILL.md +276 -19
- package/package.json +7 -4
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
|
|
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
|
[](https://registry.modelcontextprotocol.io/)
|
|
10
10
|
[](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
|
|
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
|
-
##
|
|
104
|
+
## Quick Start — One Command
|
|
95
105
|
|
|
96
106
|
```bash
|
|
97
|
-
|
|
98
|
-
linksee-memory-import --help # bundled importer for Claude Code session history
|
|
107
|
+
npx linksee-memory-setup
|
|
99
108
|
```
|
|
100
109
|
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
119
|
+
<details>
|
|
120
|
+
<summary>Click to expand manual installation</summary>
|
|
108
121
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
+
**Configure auto-capture (Stop hook):**
|
|
130
139
|
|
|
131
|
-
Add to `~/.claude/settings.json
|
|
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
|
|
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
|
-
-
|
|
231
|
-
- 🔮 Vector search via `sqlite-vec`
|
|
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
|
|
380
|
-
- Works across Claude Code, Cursor,
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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,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
|
package/dist/db/migrate.js
CHANGED
|
@@ -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')) {
|
package/dist/db/schema.sql
CHANGED
|
@@ -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
|
|
22
|
-
CREATE INDEX IF NOT EXISTS idx_entities_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', '
|
|
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 = '
|
|
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
|