linksee-memory 0.3.0 → 0.4.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 +24 -9
- package/dist/bin/import-sessions.js +10 -4
- 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 +274 -17
- package/package.json +5 -3
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,7 +98,7 @@ 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
|
## Install
|
|
@@ -226,9 +236,12 @@ The conversation↔file linkage is the key. Every file edit captured by the Stop
|
|
|
226
236
|
- ✅ Core 6 MCP tools (`remember` / `recall` / `recall_file` / `forget` / `consolidate` / `read_smart`)
|
|
227
237
|
- ✅ Stop-hook auto-capture for Claude Code
|
|
228
238
|
- ✅ JP/EN trigram FTS5
|
|
239
|
+
- ✅ Five Blocks (v0.3.0): Tools + Resources + Prompts + Sampling + Roots + Elicitation
|
|
240
|
+
- ✅ Cursor + OpenAI Codex + Gemini CLI adapters (stdio MCP, same `npx -y linksee-memory`)
|
|
229
241
|
- 🚧 `PreToolUse` hook to auto-intercept `Read` (zero-config token savings)
|
|
230
|
-
-
|
|
231
|
-
- 🔮 Vector search via `sqlite-vec`
|
|
242
|
+
- 🔮 ChatGPT app (web/mobile) support via `linksee-memory-remote` (Remote MCP over HTTPS, v0.4)
|
|
243
|
+
- 🔮 Vector search via `sqlite-vec` (already in deps, embedding backend pending)
|
|
244
|
+
- 🔮 Cross-device cloud sync (Pro tier, summer 2026)
|
|
232
245
|
- 🔮 Optional anonymized telemetry → MCP-quality intelligence layer
|
|
233
246
|
|
|
234
247
|
## Comparison with Claude Code auto-memory
|
|
@@ -376,8 +389,8 @@ Three axes:
|
|
|
376
389
|
<details>
|
|
377
390
|
<summary><strong>Why not just use Claude's built-in auto-memory?</strong></summary>
|
|
378
391
|
|
|
379
|
-
Claude Code's auto-memory is Claude-only (doesn't help if you switch to Cursor or
|
|
380
|
-
- Works across Claude Code, Cursor,
|
|
392
|
+
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:
|
|
393
|
+
- Works across Claude Code, Cursor, OpenAI Codex, Gemini CLI (shared SQLite)
|
|
381
394
|
- Structured 6-layer format makes recall explainable
|
|
382
395
|
- Provides explicit forget/consolidate primitives rather than the agent guessing
|
|
383
396
|
</details>
|
|
@@ -415,8 +428,10 @@ In practice a solo developer hits ~100MB after 6 months of heavy use. A year-old
|
|
|
415
428
|
Yes — any MCP-compatible client works:
|
|
416
429
|
- **Claude Code**: `claude mcp add -s user linksee -- npx -y linksee-memory`
|
|
417
430
|
- **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
|
-
- **
|
|
431
|
+
- **Cursor**: add to MCP settings in Cursor → Settings → Features → Model Context Protocol
|
|
432
|
+
- **OpenAI Codex**: `codex mcp add linksee -- npx -y linksee-memory` (or `~/.codex/config.toml` with `[mcp_servers.linksee]` block)
|
|
433
|
+
- **Gemini CLI**: add to `~/.gemini/settings.json` mcpServers section
|
|
434
|
+
- **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
435
|
- **Custom agent**: the MCP stdio protocol is documented at modelcontextprotocol.io
|
|
421
436
|
</details>
|
|
422
437
|
|
|
@@ -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)
|
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
|
|
@@ -3,6 +3,54 @@
|
|
|
3
3
|
// to its intent context. A memory like "edited server.ts" becomes
|
|
4
4
|
// "edited server.ts BECAUSE the user wanted the FTS5 + LIKE merge fix".
|
|
5
5
|
import { isMetaOrNoise, isAutomatedSession, isPastedExternalContent } from './session-parser.js';
|
|
6
|
+
const ALTITUDE_PATTERNS = [
|
|
7
|
+
[/mission|ミッション|ビジョン|vision|product\s+direction|事業方針/i, 'mission'],
|
|
8
|
+
[/strategy|戦略|方針|positioning|GTM|go.to.market|revenue|pricing|ICP|ターゲット|マーケ/i, 'strategy'],
|
|
9
|
+
[/architect|設計|schema|database|DB設計|migration|API\s+design|system\s+design|layer\s+model|アーキテクチャ/i, 'architecture'],
|
|
10
|
+
];
|
|
11
|
+
function inferAltitude(text) {
|
|
12
|
+
for (const [pattern, altitude] of ALTITUDE_PATTERNS) {
|
|
13
|
+
if (pattern.test(text))
|
|
14
|
+
return altitude;
|
|
15
|
+
}
|
|
16
|
+
return 'implementation';
|
|
17
|
+
}
|
|
18
|
+
/** Extract a concise title from raw text (first sentence or up to maxLen chars) */
|
|
19
|
+
function makeTitle(text, maxLen = 80) {
|
|
20
|
+
const cleaned = text.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
21
|
+
const firstSentence = cleaned.split(/[。!?!?\n]/)[0].trim();
|
|
22
|
+
if (firstSentence.length <= maxLen)
|
|
23
|
+
return firstSentence;
|
|
24
|
+
return firstSentence.slice(0, maxLen - 3) + '...';
|
|
25
|
+
}
|
|
26
|
+
/** Extract file paths from surrounding context */
|
|
27
|
+
function extractAffectedPaths(ops) {
|
|
28
|
+
const unique = new Set(ops.map((o) => o.path));
|
|
29
|
+
return Array.from(unique).slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
function buildStructuredContent(opts) {
|
|
32
|
+
const obj = {
|
|
33
|
+
title: opts.title,
|
|
34
|
+
altitude: opts.altitude,
|
|
35
|
+
type: opts.type,
|
|
36
|
+
state: opts.state,
|
|
37
|
+
what: opts.what,
|
|
38
|
+
};
|
|
39
|
+
if (opts.why)
|
|
40
|
+
obj.why = opts.why;
|
|
41
|
+
if (opts.affects && opts.affects.length > 0)
|
|
42
|
+
obj.affects = opts.affects;
|
|
43
|
+
if (opts.next_action !== undefined)
|
|
44
|
+
obj.next_action = opts.next_action;
|
|
45
|
+
if (opts.evidence_refs && opts.evidence_refs.length > 0)
|
|
46
|
+
obj.evidence_refs = opts.evidence_refs;
|
|
47
|
+
// Merge any extra fields (session_id, git_branch, etc.)
|
|
48
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
49
|
+
if (!(k in obj) && v !== undefined)
|
|
50
|
+
obj[k] = v;
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(obj, null, 2);
|
|
53
|
+
}
|
|
6
54
|
// ============================================================
|
|
7
55
|
// Intent detection — first non-noise user message in the session.
|
|
8
56
|
// ============================================================
|
|
@@ -50,7 +98,9 @@ const CAVEAT_PATTERNS = [
|
|
|
50
98
|
// sentence terminators 。!!、 / particles ね・よ / whitespace / ください
|
|
51
99
|
// Anything else (e.g. も = concessive「〜ないでも」, 止まる・いる・ほしい etc.)
|
|
52
100
|
// is treated as descriptive and excluded.
|
|
53
|
-
|
|
101
|
+
// 心配し・気にし・遠慮し are reassurance ("don't worry / don't mind / don't hesitate")
|
|
102
|
+
// — semantically opposite to a caveat. Lookbehind excludes them.
|
|
103
|
+
/気をつけて|注意して|[!!]注意[!!]|避けて(?!いる|いない)|(?<!心配|気に|遠慮)[ぁ-ん一-龯]ないで(?=[。!!、\s]|ください|ね[^い]|よ[^う]|$)|やめて(?!おく|ほし)|禁止|ダメだ(?!った|ろうと|と思)|危険[だです]/,
|
|
54
104
|
// English: require a concrete action after avoid/don't/never — a bare
|
|
55
105
|
// "Avoiding rebuild of unchanged files" in a Vercel log is not a caveat.
|
|
56
106
|
/\b(?:don'?t|do\s+not)\s+(?:do|use|run|call|forget|try|send|share|commit|push|paste|edit)\b|\bnever\s+(?:do|use|call|share|commit|paste|run|push|edit)\b|\bavoid\s+(?:using|running|calling|committing|pushing|sharing|pasting|editing|creating|modifying)\b|\bwatch\s+out\b/i,
|
|
@@ -58,6 +108,27 @@ const CAVEAT_PATTERNS = [
|
|
|
58
108
|
function matchesAny(text, patterns) {
|
|
59
109
|
return patterns.some((p) => p.test(text));
|
|
60
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Check if a message is mostly chitchat with a decision keyword buried in it.
|
|
113
|
+
* The サイダー problem: "おお!そうだね。書斎で無糖のサイダーでした。…決めた"
|
|
114
|
+
* matches DECISION_PATTERNS because of "決めた" at the end, but the message
|
|
115
|
+
* is primarily casual conversation, not a project decision.
|
|
116
|
+
*
|
|
117
|
+
* Heuristic: if the decision keyword appears ONLY in the last 30% of a long
|
|
118
|
+
* message (>100 chars) AND the first 50 chars match chitchat patterns, skip it.
|
|
119
|
+
*/
|
|
120
|
+
const CHITCHAT_OPENERS = /^(?:おお|うん|そう(?:だね|だよね)|ありがと|はは|笑|www|OK|おー|へー|なるほど|ちなみに|そういえば|あー|えー|まぁ|まあ|ああ)/;
|
|
121
|
+
function isChitchatWithBuriedDecision(text, patterns) {
|
|
122
|
+
if (text.length < 100)
|
|
123
|
+
return false; // short messages are fine
|
|
124
|
+
if (!CHITCHAT_OPENERS.test(text.trim()))
|
|
125
|
+
return false; // doesn't open with chitchat
|
|
126
|
+
// Check if any pattern matches in the first 40% of the text
|
|
127
|
+
const earlyPortion = text.slice(0, Math.floor(text.length * 0.4));
|
|
128
|
+
if (patterns.some((p) => p.test(earlyPortion)))
|
|
129
|
+
return false; // decision is early = legitimate
|
|
130
|
+
return true; // chitchat opening + decision keyword only appears late = noise
|
|
131
|
+
}
|
|
61
132
|
// Dedupe successive file edits to the same path within N seconds —
|
|
62
133
|
// they're usually the same logical change.
|
|
63
134
|
// NOTE: this only dedupes for memory CREATION (1 implementation memory per file
|
|
@@ -86,31 +157,39 @@ export function extractSession(session, projectName) {
|
|
|
86
157
|
// 1) Goal layer — the first REAL intent (or synthetic marker for automated sessions)
|
|
87
158
|
const firstIntent = findFirstIntent(session);
|
|
88
159
|
if (firstIntent) {
|
|
160
|
+
const intentText = firstIntent.text.slice(0, 1000);
|
|
89
161
|
memories.push({
|
|
90
162
|
layer: 'goal',
|
|
91
|
-
content:
|
|
92
|
-
|
|
93
|
-
|
|
163
|
+
content: buildStructuredContent({
|
|
164
|
+
title: makeTitle(intentText),
|
|
165
|
+
altitude: inferAltitude(intentText),
|
|
166
|
+
type: 'work',
|
|
167
|
+
state: 'in_progress',
|
|
168
|
+
what: intentText,
|
|
169
|
+
why: 'Session intent — first user message',
|
|
170
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'source session' }],
|
|
94
171
|
session_id: session.session_id,
|
|
95
172
|
git_branch: session.git_branch,
|
|
96
|
-
}
|
|
97
|
-
importance: automated ? 0.3 : 0.8,
|
|
173
|
+
}),
|
|
174
|
+
importance: automated ? 0.3 : 0.8,
|
|
98
175
|
source: { session_id: session.session_id, turn_uuid: firstIntent.uuid, kind: 'first_intent' },
|
|
99
176
|
});
|
|
100
177
|
}
|
|
101
178
|
else if (automated) {
|
|
102
|
-
// Synthetic goal so the session is still discoverable
|
|
103
179
|
const match = firstRawUserText.match(/<scheduled-task\s+name="([^"]+)"/);
|
|
104
180
|
const taskName = match ? match[1] : 'unknown';
|
|
105
181
|
memories.push({
|
|
106
182
|
layer: 'goal',
|
|
107
|
-
content:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
183
|
+
content: buildStructuredContent({
|
|
184
|
+
title: `Automated: ${taskName}`,
|
|
185
|
+
altitude: 'implementation',
|
|
186
|
+
type: 'work',
|
|
187
|
+
state: 'in_progress',
|
|
188
|
+
what: `Automated scheduled task run: ${taskName}`,
|
|
189
|
+
why: 'Scheduled automation',
|
|
111
190
|
session_id: session.session_id,
|
|
112
191
|
git_branch: session.git_branch,
|
|
113
|
-
}
|
|
192
|
+
}),
|
|
114
193
|
importance: 0.2,
|
|
115
194
|
source: { session_id: session.session_id, kind: 'automated_task' },
|
|
116
195
|
});
|
|
@@ -131,11 +210,17 @@ export function extractSession(session, projectName) {
|
|
|
131
210
|
if (t.text.trim().length < 40)
|
|
132
211
|
continue;
|
|
133
212
|
clarifyCount++;
|
|
213
|
+
const msgText = t.text.slice(0, 600);
|
|
134
214
|
memories.push({
|
|
135
215
|
layer: 'context',
|
|
136
|
-
content:
|
|
137
|
-
|
|
138
|
-
|
|
216
|
+
content: buildStructuredContent({
|
|
217
|
+
title: makeTitle(msgText, 60),
|
|
218
|
+
altitude: inferAltitude(msgText),
|
|
219
|
+
type: 'note',
|
|
220
|
+
state: 'open',
|
|
221
|
+
what: msgText,
|
|
222
|
+
why: 'Clarification during session',
|
|
223
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'source session' }],
|
|
139
224
|
session_id: session.session_id,
|
|
140
225
|
}),
|
|
141
226
|
importance: 0.5,
|
|
@@ -153,17 +238,23 @@ export function extractSession(session, projectName) {
|
|
|
153
238
|
for (const [path, ops] of byPath) {
|
|
154
239
|
const first = ops[0];
|
|
155
240
|
const opsKinds = Array.from(new Set(ops.map((o) => o.operation))).join('+');
|
|
156
|
-
const
|
|
241
|
+
const userIntent = (first.preceding_user_text || '').slice(0, 400);
|
|
157
242
|
const contentSnippet = ops.map((o) => o.tool_input_preview).slice(0, 2).join(' | ').slice(0, 500);
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
243
|
+
const fileName = path.replace(/\\/g, '/').split('/').pop() || path;
|
|
244
|
+
const memoryContent = buildStructuredContent({
|
|
245
|
+
title: `${opsKinds} ${fileName} (${ops.length} ops)`,
|
|
246
|
+
altitude: 'implementation',
|
|
247
|
+
type: 'work',
|
|
248
|
+
state: 'done',
|
|
249
|
+
what: userIntent || `File operation: ${opsKinds} on ${path}`,
|
|
250
|
+
why: userIntent ? `User intent: ${makeTitle(userIntent, 120)}` : '(no explicit preceding intent)',
|
|
251
|
+
affects: [path],
|
|
252
|
+
next_action: null,
|
|
253
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'source session' }],
|
|
163
254
|
sample_change: contentSnippet,
|
|
164
|
-
|
|
255
|
+
op_count: ops.length,
|
|
165
256
|
session_id: session.session_id,
|
|
166
|
-
}
|
|
257
|
+
});
|
|
167
258
|
memories.push({
|
|
168
259
|
layer: 'implementation',
|
|
169
260
|
content: memoryContent,
|
|
@@ -195,12 +286,20 @@ export function extractSession(session, projectName) {
|
|
|
195
286
|
continue;
|
|
196
287
|
if (isPastedExternalContent(t.text))
|
|
197
288
|
continue;
|
|
198
|
-
if (matchesAny(t.text, CAVEAT_PATTERNS) && t.text.length > 20) {
|
|
289
|
+
if (matchesAny(t.text, CAVEAT_PATTERNS) && t.text.length > 20 && !isChitchatWithBuriedDecision(t.text, CAVEAT_PATTERNS)) {
|
|
290
|
+
const caveatText = t.text.slice(0, 500);
|
|
199
291
|
memories.push({
|
|
200
292
|
layer: 'caveat',
|
|
201
|
-
content:
|
|
202
|
-
|
|
203
|
-
|
|
293
|
+
content: buildStructuredContent({
|
|
294
|
+
title: makeTitle(caveatText, 70),
|
|
295
|
+
altitude: inferAltitude(caveatText),
|
|
296
|
+
type: 'learning',
|
|
297
|
+
state: 'done',
|
|
298
|
+
what: caveatText,
|
|
299
|
+
why: 'User-stated warning/prohibition — auto-extracted by caveat pattern match',
|
|
300
|
+
affects: extractAffectedPaths(session.file_ops),
|
|
301
|
+
next_action: null,
|
|
302
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'caveat source' }],
|
|
204
303
|
session_id: session.session_id,
|
|
205
304
|
}),
|
|
206
305
|
importance: 0.75,
|
|
@@ -210,6 +309,9 @@ export function extractSession(session, projectName) {
|
|
|
210
309
|
}
|
|
211
310
|
// 5) Learning layer — messages matching decision patterns
|
|
212
311
|
// Same strict filter applies.
|
|
312
|
+
// NOTE: Without LLM, we store the raw user text as `what` — this is the best
|
|
313
|
+
// heuristic extraction can do. Agent-initiated `remember()` calls should use
|
|
314
|
+
// the full structured format with agent_proposal + user_approval_scope.
|
|
213
315
|
for (const t of session.turns) {
|
|
214
316
|
if (t.role !== 'user' || isMetaOrNoise(t.text))
|
|
215
317
|
continue;
|
|
@@ -217,12 +319,20 @@ export function extractSession(session, projectName) {
|
|
|
217
319
|
continue;
|
|
218
320
|
if (isPastedExternalContent(t.text))
|
|
219
321
|
continue;
|
|
220
|
-
if (matchesAny(t.text, DECISION_PATTERNS) && t.text.length > 15) {
|
|
322
|
+
if (matchesAny(t.text, DECISION_PATTERNS) && t.text.length > 15 && !isChitchatWithBuriedDecision(t.text, DECISION_PATTERNS)) {
|
|
323
|
+
const decisionText = t.text.slice(0, 500);
|
|
221
324
|
memories.push({
|
|
222
325
|
layer: 'learning',
|
|
223
|
-
content:
|
|
224
|
-
|
|
225
|
-
|
|
326
|
+
content: buildStructuredContent({
|
|
327
|
+
title: makeTitle(decisionText, 70),
|
|
328
|
+
altitude: inferAltitude(decisionText),
|
|
329
|
+
type: 'decision',
|
|
330
|
+
state: 'decided',
|
|
331
|
+
what: decisionText,
|
|
332
|
+
why: 'Decision detected by pattern match — may need agent enrichment',
|
|
333
|
+
affects: extractAffectedPaths(session.file_ops),
|
|
334
|
+
next_action: null,
|
|
335
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'decision source' }],
|
|
226
336
|
session_id: session.session_id,
|
|
227
337
|
}),
|
|
228
338
|
importance: 0.7,
|
|
@@ -236,10 +346,15 @@ export function extractSession(session, projectName) {
|
|
|
236
346
|
if (session.errors_count > 3) {
|
|
237
347
|
memories.push({
|
|
238
348
|
layer: 'context',
|
|
239
|
-
content:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
349
|
+
content: buildStructuredContent({
|
|
350
|
+
title: `High error session (${session.errors_count} errors / ${session.turns.length} turns)`,
|
|
351
|
+
altitude: 'implementation',
|
|
352
|
+
type: 'outcome',
|
|
353
|
+
state: 'done',
|
|
354
|
+
what: `This session had ${session.errors_count} tool errors across ${session.turns.length} turns.`,
|
|
355
|
+
why: 'High error rate may indicate environmental or configuration issues worth investigating',
|
|
356
|
+
affects: extractAffectedPaths(session.file_ops),
|
|
357
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'error session' }],
|
|
243
358
|
session_id: session.session_id,
|
|
244
359
|
}),
|
|
245
360
|
importance: 0.4,
|
|
@@ -248,20 +363,31 @@ export function extractSession(session, projectName) {
|
|
|
248
363
|
}
|
|
249
364
|
// 7) Session summary — one meta-implementation memory per session for overview
|
|
250
365
|
if (memories.length > 0) {
|
|
366
|
+
const durationMin = Math.round((session.ended_at - session.started_at) / 60);
|
|
367
|
+
const firstGoalTitle = firstIntent ? makeTitle(firstIntent.text, 60) : 'automated task';
|
|
251
368
|
memories.push({
|
|
252
369
|
layer: 'implementation',
|
|
253
|
-
content:
|
|
370
|
+
content: buildStructuredContent({
|
|
371
|
+
title: `Session: ${firstGoalTitle} (${durationMin}min, ${byPath.size} files)`,
|
|
372
|
+
altitude: 'implementation',
|
|
373
|
+
type: 'outcome',
|
|
374
|
+
state: 'done',
|
|
375
|
+
what: `Session completed: ${durationMin} minutes, ${session.turn_count_user} user turns, ${byPath.size} files touched, ${session.errors_count} errors`,
|
|
376
|
+
why: 'Session overview for timeline and activity tracking',
|
|
377
|
+
affects: extractAffectedPaths(session.file_ops),
|
|
378
|
+
next_action: null,
|
|
379
|
+
evidence_refs: [{ type: 'session', id: session.session_id, label: 'session overview' }],
|
|
254
380
|
summary_kind: 'session_overview',
|
|
255
381
|
session_id: session.session_id,
|
|
256
382
|
started_at: new Date(session.started_at * 1000).toISOString(),
|
|
257
383
|
ended_at: new Date(session.ended_at * 1000).toISOString(),
|
|
258
|
-
duration_min:
|
|
384
|
+
duration_min: durationMin,
|
|
259
385
|
turns_user: session.turn_count_user,
|
|
260
386
|
turns_assistant: session.turn_count_assistant,
|
|
261
387
|
files_touched: byPath.size,
|
|
262
388
|
errors: session.errors_count,
|
|
263
389
|
git_branch: session.git_branch,
|
|
264
|
-
}
|
|
390
|
+
}),
|
|
265
391
|
importance: 0.4,
|
|
266
392
|
source: { session_id: session.session_id, kind: 'session_summary' },
|
|
267
393
|
});
|
package/dist/mcp/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { decideForgetting } from '../lib/forgetting.js';
|
|
|
11
11
|
import { refreshMomentumForEntity } from '../lib/momentum.js';
|
|
12
12
|
import { consolidate as runConsolidate } from '../lib/consolidate.js';
|
|
13
13
|
import { isPastedExternalContent } from '../lib/session-parser.js';
|
|
14
|
+
import { normalizeEntityName } from '../lib/normalize.js';
|
|
14
15
|
import { handleReadSmart as handleReadSmartImpl } from './read-smart.js';
|
|
15
16
|
import { STATIC_RESOURCES, RESOURCE_TEMPLATES, readResource } from './resources.js';
|
|
16
17
|
import { PROMPTS, getPrompt } from './prompts.js';
|
|
@@ -62,7 +63,7 @@ const LAYER_ENUM = ['goal', 'context', 'emotion', 'implementation', 'caveat', 'l
|
|
|
62
63
|
const TOOLS = [
|
|
63
64
|
{
|
|
64
65
|
name: 'remember',
|
|
65
|
-
description: '
|
|
66
|
+
description: 'Save important context that should persist across sessions and across AI tools. When the user says "use linksee", "remember this", "don\'t forget", or you discover a decision, preference, or lesson worth preserving — store it here. The server automatically organizes into layers (goal/context/emotion/implementation/caveat/learning) and manages memory lifecycle. Just save what matters. Supports Japanese (日本語) and English. Memories saved here are accessible from any AI agent the user connects to — Claude, GPT, Cursor, Codex, Gemini. This is the only memory that works cross-agent.',
|
|
66
67
|
inputSchema: {
|
|
67
68
|
type: 'object',
|
|
68
69
|
properties: {
|
|
@@ -79,7 +80,7 @@ const TOOLS = [
|
|
|
79
80
|
},
|
|
80
81
|
{
|
|
81
82
|
name: 'recall',
|
|
82
|
-
description: '
|
|
83
|
+
description: 'Your persistent memory across all AI tools — Claude, GPT, Cursor, Codex, Gemini. When the user says "use linksee" or asks about past decisions, context, or preferences, call this first. Returns memories ranked by relevance and recency. Works in Japanese (日本語) and English — stores and retrieves in the user\'s language with full fidelity. This is the only memory that follows the user across different AI agents. Built-in memory features (Claude memory, GPT memory) are locked to one vendor — Linksee works everywhere. Use at the start of any task that might involve prior work.',
|
|
83
84
|
inputSchema: {
|
|
84
85
|
type: 'object',
|
|
85
86
|
properties: {
|
|
@@ -177,23 +178,41 @@ const TOOLS = [
|
|
|
177
178
|
// Handlers
|
|
178
179
|
// ============================================================
|
|
179
180
|
function upsertEntity(args) {
|
|
181
|
+
// 1. Fastest path: canonical_key exact match (e.g. project path)
|
|
180
182
|
if (args.key) {
|
|
181
183
|
const byKey = db.prepare('SELECT id FROM entities WHERE canonical_key = ?').get(args.key);
|
|
182
184
|
if (byKey)
|
|
183
185
|
return byKey.id;
|
|
184
186
|
}
|
|
187
|
+
// 2. Normalized name match (prevents "CockpitMCP" / "Cockpit MCP" / "cockpit-mcp" dups)
|
|
188
|
+
const normalized = normalizeEntityName(args.name);
|
|
189
|
+
const byNorm = db
|
|
190
|
+
.prepare('SELECT id FROM entities WHERE kind = ? AND normalized_name = ?')
|
|
191
|
+
.get(args.kind, normalized);
|
|
192
|
+
if (byNorm) {
|
|
193
|
+
if (args.key) {
|
|
194
|
+
db.prepare('UPDATE entities SET canonical_key = ?, updated_at = unixepoch() WHERE id = ? AND canonical_key IS NULL').run(args.key, byNorm.id);
|
|
195
|
+
}
|
|
196
|
+
return byNorm.id;
|
|
197
|
+
}
|
|
198
|
+
// 3. Fallback: exact case-insensitive match (covers names that normalize differently
|
|
199
|
+
// but the user typed the exact same string — shouldn't happen after v5 migration
|
|
200
|
+
// but keeps backward compat if normalized_name is NULL for some row)
|
|
185
201
|
const byName = db
|
|
186
202
|
.prepare('SELECT id FROM entities WHERE kind = ? AND LOWER(name) = LOWER(?)')
|
|
187
203
|
.get(args.kind, args.name);
|
|
188
204
|
if (byName) {
|
|
205
|
+
// Backfill normalized_name while we're here
|
|
206
|
+
db.prepare('UPDATE entities SET normalized_name = ?, updated_at = unixepoch() WHERE id = ? AND normalized_name IS NULL').run(normalized, byName.id);
|
|
189
207
|
if (args.key) {
|
|
190
208
|
db.prepare('UPDATE entities SET canonical_key = ?, updated_at = unixepoch() WHERE id = ? AND canonical_key IS NULL').run(args.key, byName.id);
|
|
191
209
|
}
|
|
192
210
|
return byName.id;
|
|
193
211
|
}
|
|
212
|
+
// 4. Insert new entity with normalized_name
|
|
194
213
|
const result = db
|
|
195
|
-
.prepare('INSERT INTO entities (kind, name, canonical_key) VALUES (?, ?, ?)')
|
|
196
|
-
.run(args.kind, args.name, args.key ?? null);
|
|
214
|
+
.prepare('INSERT INTO entities (kind, name, normalized_name, canonical_key) VALUES (?, ?, ?, ?)')
|
|
215
|
+
.run(args.kind, args.name, normalized, args.key ?? null);
|
|
197
216
|
return Number(result.lastInsertRowid);
|
|
198
217
|
}
|
|
199
218
|
function handleRemember(args) {
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -74,6 +74,103 @@ remember({
|
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
77
|
+
## 🏗️ 3-Axis Classification (v2) — REQUIRED for all `remember()` calls
|
|
78
|
+
|
|
79
|
+
The 6-layer system tells you WHICH DRAWER. The 3-axis system tells you WHAT KIND of memory goes in it. Every `content` field must be a JSON string containing these 3 axes:
|
|
80
|
+
|
|
81
|
+
### Axis ① Altitude — where in the abstraction hierarchy
|
|
82
|
+
|
|
83
|
+
| Level | Description | Survives |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `mission` | Company/product-level direction ("KanseiLINK is the intelligence layer for the Agent Economy") | Permanent |
|
|
86
|
+
| `strategy` | Approach to achieving mission ("AEO-first, 引き出しカタログ model") | Permanent |
|
|
87
|
+
| `architecture` | System design decisions ("2-layer: Memory=agent-optimized, Dashboard=human-optimized") | Long-lived |
|
|
88
|
+
| `implementation` | Specific code/config changes ("Added FTS5 trigram index") | Auto-archives after 30d if untouched |
|
|
89
|
+
|
|
90
|
+
### Axis ② Type — what kind of information
|
|
91
|
+
|
|
92
|
+
| Type | Description |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `question` | User asked something, answer pending or delivered |
|
|
95
|
+
| `comparison` | Multiple options analyzed (e.g., Stripe vs Square) |
|
|
96
|
+
| `decision` | A choice was made — store agent_proposal + user_approval_scope |
|
|
97
|
+
| `work` | Code written, config changed, command run |
|
|
98
|
+
| `outcome` | Result of work (success/failure + what happened) |
|
|
99
|
+
| `learning` | Insight gained, prior belief updated |
|
|
100
|
+
| `note` | General context — **chitchat = DISCARD, do NOT save** |
|
|
101
|
+
|
|
102
|
+
### Axis ③ State — lifecycle position
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
open → decided → in_progress → done
|
|
106
|
+
→ stalled (blocked, can't proceed)
|
|
107
|
+
→ parked (intentionally paused)
|
|
108
|
+
→ superseded (replaced by newer decision)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 📝 Structured Content Format — the JSON schema for `content`
|
|
114
|
+
|
|
115
|
+
**Every `remember()` call MUST use this JSON format in the `content` field:**
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"title": "<one-line: WHAT this memory IS — future-agent skims this>",
|
|
120
|
+
"altitude": "strategy",
|
|
121
|
+
"type": "decision",
|
|
122
|
+
"state": "decided",
|
|
123
|
+
"what": "<the actual content — 5W1H extracted, NOT raw chat>",
|
|
124
|
+
"why": "<why this matters — the reasoning>",
|
|
125
|
+
"affects": ["src/mcp/server.ts", "lib/db.ts"],
|
|
126
|
+
"next_action": "Implement the schema migration next session",
|
|
127
|
+
"supersedes_id": null,
|
|
128
|
+
"evidence_refs": [
|
|
129
|
+
{"type": "session", "id": "b78dc5ba", "label": "Architecture discussion"}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Required fields (ALL memories)
|
|
135
|
+
|
|
136
|
+
| Field | Type | Description |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `title` | string | One-line summary. Future agents read ONLY this when scanning. Make it specific: "freee OAuth 24h expiry caveat" not "OAuth issue" |
|
|
139
|
+
| `altitude` | enum | mission / strategy / architecture / implementation |
|
|
140
|
+
| `type` | enum | question / comparison / decision / work / outcome / learning |
|
|
141
|
+
| `state` | enum | open / decided / in_progress / done / stalled / parked / superseded |
|
|
142
|
+
| `what` | string | The semantic content. Extract 5W1H from conversation — NEVER store raw chat like "そうだね。全部やろう。" |
|
|
143
|
+
| `why` | string | Why this matters. Without this, future agents can't judge relevance |
|
|
144
|
+
|
|
145
|
+
### Required fields (DECISION type only)
|
|
146
|
+
|
|
147
|
+
| Field | Type | Description |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `agent_proposal` | string | What the agent proposed (the full context the user was responding to) |
|
|
150
|
+
| `user_approval_scope` | string | What EXACTLY the user approved — "うん全部やって" → translate to "approved all 6 panels of Agent Brain Dashboard including data layer, API route, and view component" |
|
|
151
|
+
|
|
152
|
+
### Optional but recommended fields
|
|
153
|
+
|
|
154
|
+
| Field | Type | Description |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `affects` | string[] | File paths or areas this touches. Critical for future `recall_file` accuracy |
|
|
157
|
+
| `next_action` | string / null | What should happen next. Null if done/completed |
|
|
158
|
+
| `supersedes_id` | number / null | Memory ID this replaces (builds pivot chains) |
|
|
159
|
+
| `prior_belief` | string | What we used to think (for learnings — enables belief-update tracking) |
|
|
160
|
+
| `evidence_refs` | object[] | Links to evidence: `{"type": "session"|"file"|"url", "id": "...", "label": "..."}`. Store as REFERENCES, never inline the full content |
|
|
161
|
+
|
|
162
|
+
### Content quality rules — what NOT to save
|
|
163
|
+
|
|
164
|
+
| ❌ DO NOT | ✅ INSTEAD |
|
|
165
|
+
|---|---|
|
|
166
|
+
| Store raw chat: `"決めた。それでいこう"` | Extract: `"Decided to use 2-layer architecture splitting Memory (agent-optimized) from Dashboard (human-optimized)"` |
|
|
167
|
+
| Store ambient chat: `"書斎で無糖のサイダー飲んでる"` | Discard. Not a memory. |
|
|
168
|
+
| Store vague approval: `"うん全部やって"` | Extract the SCOPE: `"Approved: (1) agent-brain data layer, (2) API route, (3) 6-panel view component implementation"` |
|
|
169
|
+
| Paste back assistant output as memory | Summarize the KEY INSIGHT from the output in YOUR words |
|
|
170
|
+
| Store without `why` | Always include WHY — without it, memory is noise |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
77
174
|
## 🔄 Execution flow — 5 canonical moments
|
|
78
175
|
|
|
79
176
|
### ① Task Start — Always recall before starting work
|
|
@@ -113,6 +210,30 @@ From past caveat: "Watch out for MCP tool name collisions"
|
|
|
113
210
|
→ Before adding a new tool, check existing tool names first.
|
|
114
211
|
```
|
|
115
212
|
|
|
213
|
+
#### ⚡ Writing effective recall queries
|
|
214
|
+
|
|
215
|
+
The recall engine uses FTS5 full-text search + heat_score ranking. Your query determines what comes back.
|
|
216
|
+
|
|
217
|
+
| Pattern | Query style | Example |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| Entity + topic | `"<entity> <topic keyword>"` | `"KanseiLink OAuth"` |
|
|
220
|
+
| Error recall | `"<error message core> <technology>"` | `"401 freee token expired"` |
|
|
221
|
+
| Decision recall | `"<entity> decided strategy approach"` | `"Linksee Memory plugin vs MCP"` |
|
|
222
|
+
| File-related | Use `recall_file` instead | `recall_file({ path_substring: "server.ts" })` |
|
|
223
|
+
| Cross-entity | Call recall TWICE for each entity | `recall({ query: "KanseiLink" })` then `recall({ query: "Linksee Memory" })` |
|
|
224
|
+
|
|
225
|
+
**Anti-patterns:**
|
|
226
|
+
- ❌ `recall({ query: "what happened" })` — too vague, FTS5 matches everything
|
|
227
|
+
- ❌ `recall({ query: "the user said to fix the bug in the auth flow" })` — natural language sentences score poorly in FTS5
|
|
228
|
+
- ✅ `recall({ query: "auth bug fix caveat", layer: "caveat" })` — keywords + layer filter = precise
|
|
229
|
+
|
|
230
|
+
#### 🔇 When NOT to recall (save tokens)
|
|
231
|
+
|
|
232
|
+
- Same entity already recalled in this session AND no new context arrived → **skip**
|
|
233
|
+
- User is just chatting / thinking aloud / no task yet → **skip** (wait for concrete task)
|
|
234
|
+
- You just wrote a memory 2 turns ago → **skip** (it's still in your context window)
|
|
235
|
+
- The answer is already in your conversation context → **skip** (don't waste a tool call)
|
|
236
|
+
|
|
116
237
|
### ② File Edit — Use recall_file before touching a file
|
|
117
238
|
|
|
118
239
|
Before touching a specific file, check its edit history:
|
|
@@ -168,7 +289,16 @@ mcp__linksee__remember({
|
|
|
168
289
|
entity_name: "<project name or service name>",
|
|
169
290
|
entity_kind: "project",
|
|
170
291
|
layer: "caveat",
|
|
171
|
-
content:
|
|
292
|
+
content: JSON.stringify({
|
|
293
|
+
title: "<one-line: what failed + the rule>",
|
|
294
|
+
altitude: "implementation",
|
|
295
|
+
type: "outcome",
|
|
296
|
+
state: "done",
|
|
297
|
+
what: "<what failed + workaround found>",
|
|
298
|
+
why: "<root cause analysis>",
|
|
299
|
+
affects: ["<file paths where the error occurred>"],
|
|
300
|
+
next_action: null
|
|
301
|
+
}),
|
|
172
302
|
importance: 0.8 // failures are high-importance
|
|
173
303
|
})
|
|
174
304
|
```
|
|
@@ -176,9 +306,15 @@ mcp__linksee__remember({
|
|
|
176
306
|
**Example**:
|
|
177
307
|
```json
|
|
178
308
|
{
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
309
|
+
"title": "freee OAuth token expires in 24h — must refresh proactively",
|
|
310
|
+
"altitude": "implementation",
|
|
311
|
+
"type": "outcome",
|
|
312
|
+
"state": "done",
|
|
313
|
+
"what": "freee MCP OAuth token expires in 24 hours. Reusing access_token directly causes 401. Must call refresh_token endpoint proactively.",
|
|
314
|
+
"why": "freee's OAuth implementation uses short-lived tokens unlike most SaaS (usually 30-90 day expiry)",
|
|
315
|
+
"affects": ["src/integrations/freee/auth.ts"],
|
|
316
|
+
"next_action": null,
|
|
317
|
+
"evidence_refs": [{"type":"session", "id":"02759...", "label":"freee auth_expired incident"}]
|
|
182
318
|
}
|
|
183
319
|
```
|
|
184
320
|
|
|
@@ -193,7 +329,17 @@ mcp__linksee__remember({
|
|
|
193
329
|
entity_name: "<entity>",
|
|
194
330
|
entity_kind: "project | concept | ...",
|
|
195
331
|
layer: "learning",
|
|
196
|
-
content:
|
|
332
|
+
content: JSON.stringify({
|
|
333
|
+
title: "<one-line: what was learned>",
|
|
334
|
+
altitude: "<strategy|architecture|implementation>",
|
|
335
|
+
type: "learning",
|
|
336
|
+
state: "done",
|
|
337
|
+
what: "<the insight>",
|
|
338
|
+
why: "<why this changes how we work>",
|
|
339
|
+
prior_belief: "<what we used to think>",
|
|
340
|
+
affects: ["<file paths if applicable>"],
|
|
341
|
+
next_action: "<follow-up action if any>"
|
|
342
|
+
}),
|
|
197
343
|
importance: 0.7
|
|
198
344
|
})
|
|
199
345
|
```
|
|
@@ -211,6 +357,9 @@ Recording `prior_belief` leaves a **belief-update history**. Later, this becomes
|
|
|
211
357
|
3. **Prefer `read_smart` over `Read` for larger files**
|
|
212
358
|
4. **When an error occurs, record a `caveat` immediately** (on the spot — don't defer)
|
|
213
359
|
5. **When the user is surprised or says "interesting", record a `learning`**
|
|
360
|
+
6. **Before risky/irreversible actions, proactively recall caveats** (Case H)
|
|
361
|
+
7. **Before finalizing a decision, check for prior decisions on the same topic** (Case D)
|
|
362
|
+
8. **Use keywords + layer filter in recall queries**, not natural language sentences
|
|
214
363
|
|
|
215
364
|
### ❌ Don't
|
|
216
365
|
|
|
@@ -219,6 +368,8 @@ Recording `prior_belief` leaves a **belief-update history**. Later, this becomes
|
|
|
219
368
|
3. ❌ Use `Read` everywhere instead of `read_smart` (wastes tokens)
|
|
220
369
|
4. ❌ Write caveats in a flippant tone — preserve them seriously
|
|
221
370
|
5. ❌ Skip `consolidate` during long-running work — run it weekly
|
|
371
|
+
6. ❌ Recall the same entity twice in one session without new context (wastes tokens)
|
|
372
|
+
7. ❌ Write `recall({ query: "what happened last time" })` — use specific keywords
|
|
222
373
|
|
|
223
374
|
---
|
|
224
375
|
|
|
@@ -275,36 +426,104 @@ User: "Fix server.ts"
|
|
|
275
426
|
5. After editing, record success / failure via implementation layer
|
|
276
427
|
```
|
|
277
428
|
|
|
278
|
-
### Case D —
|
|
429
|
+
### Case D — Before finalizing a decision (pre-decision check)
|
|
430
|
+
|
|
431
|
+
User: "Let's switch to Stripe for payments"
|
|
432
|
+
|
|
433
|
+
**Key: BEFORE recording a decision, check if a past decision on the same topic exists. This prevents flip-flopping and builds on prior reasoning.**
|
|
434
|
+
|
|
435
|
+
```
|
|
436
|
+
1. recall({ query: "<entity> <topic> decided strategy", layer: "learning", max_tokens: 1000 })
|
|
437
|
+
→ Look for: type="decision", state="decided" memories on the same topic
|
|
438
|
+
2. If past decision found:
|
|
439
|
+
a. Tell user: "Previously we decided <X> because <reason>. Override?"
|
|
440
|
+
b. If user confirms override → use supersedes_id to link to old decision
|
|
441
|
+
c. If user says "oh right, keep it" → no new memory needed, proceed
|
|
442
|
+
3. If no past decision found → proceed to record (see Case E below)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Example**: Past memory says "Decided: Square over Stripe due to SG tax handling". When user now says "switch to Stripe", surface that context FIRST. The user may not remember the original reasoning.
|
|
446
|
+
|
|
447
|
+
### Case E — Decision made (recording)
|
|
279
448
|
|
|
280
449
|
User: "Alright, let's switch to Sonnet"
|
|
281
450
|
|
|
451
|
+
**Key: capture WHAT was decided, WHY, and what the user was responding to — not the raw chat.**
|
|
452
|
+
|
|
282
453
|
```
|
|
283
454
|
1. remember({
|
|
284
455
|
entity_name: "<project>",
|
|
285
456
|
entity_kind: "project",
|
|
286
457
|
layer: "learning",
|
|
287
|
-
content:
|
|
458
|
+
content: JSON.stringify({
|
|
459
|
+
title: "Model switch: Opus → Sonnet for this project",
|
|
460
|
+
altitude: "architecture",
|
|
461
|
+
type: "decision",
|
|
462
|
+
state: "decided",
|
|
463
|
+
what: "Switched default model from Opus to Sonnet for this project",
|
|
464
|
+
why: "Sonnet is faster and cheaper for implementation-heavy work; Opus reserved for architecture decisions",
|
|
465
|
+
agent_proposal: "Suggested Sonnet for faster iteration on implementation tasks",
|
|
466
|
+
user_approval_scope: "Approved switching default model to Sonnet for all tasks in this project",
|
|
467
|
+
prior_belief: "Was using Opus for everything",
|
|
468
|
+
affects: [".claude/settings.json"],
|
|
469
|
+
next_action: null
|
|
470
|
+
}),
|
|
288
471
|
importance: 0.8
|
|
289
472
|
})
|
|
290
473
|
2. Brief confirmation: "Recorded."
|
|
291
474
|
3. From here, proceed assuming Sonnet
|
|
292
475
|
```
|
|
293
476
|
|
|
294
|
-
### Case
|
|
477
|
+
### Case F — End of long session
|
|
295
478
|
|
|
296
479
|
User: "That's it for today"
|
|
297
480
|
|
|
481
|
+
**Key: extract the SEMANTIC decisions and outcomes, not raw chat dumps.**
|
|
482
|
+
|
|
298
483
|
```
|
|
299
|
-
1.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
484
|
+
1. For each major decision made during the session:
|
|
485
|
+
remember({
|
|
486
|
+
entity_name: "<project>",
|
|
487
|
+
entity_kind: "project",
|
|
488
|
+
layer: "learning",
|
|
489
|
+
content: JSON.stringify({
|
|
490
|
+
title: "<one-line: what was decided>",
|
|
491
|
+
altitude: "<strategy|architecture|implementation>",
|
|
492
|
+
type: "decision",
|
|
493
|
+
state: "decided",
|
|
494
|
+
what: "<5W1H extraction of the decision>",
|
|
495
|
+
why: "<reasoning behind it>",
|
|
496
|
+
agent_proposal: "<what you proposed>",
|
|
497
|
+
user_approval_scope: "<what exactly user approved>",
|
|
498
|
+
affects: ["<file paths>"],
|
|
499
|
+
next_action: "<what's next>",
|
|
500
|
+
evidence_refs: [{"type":"session", "id":"<current_session_id>", "label":"<topic>"}]
|
|
501
|
+
}),
|
|
502
|
+
importance: 0.85
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
2. For each failure/lesson:
|
|
506
|
+
remember({
|
|
507
|
+
...,
|
|
508
|
+
layer: "caveat",
|
|
509
|
+
content: JSON.stringify({
|
|
510
|
+
title: "<one-line: what went wrong and the fix>",
|
|
511
|
+
altitude: "implementation",
|
|
512
|
+
type: "outcome",
|
|
513
|
+
state: "done",
|
|
514
|
+
what: "<what failed + workaround found>",
|
|
515
|
+
why: "<root cause>",
|
|
516
|
+
affects: ["<file paths>"],
|
|
517
|
+
next_action: null
|
|
518
|
+
}),
|
|
519
|
+
importance: 0.8
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
3. Report: "Recorded N decisions, M caveats. Retrievable via recall."
|
|
523
|
+
4. Optionally suggest: consolidate({scope:"session", min_age_days: 14})
|
|
305
524
|
```
|
|
306
525
|
|
|
307
|
-
### Case
|
|
526
|
+
### Case G — User explicitly says "remember this"
|
|
308
527
|
|
|
309
528
|
User: "Remember this: DocuSign is more stable than CloudSign"
|
|
310
529
|
|
|
@@ -313,12 +532,50 @@ User: "Remember this: DocuSign is more stable than CloudSign"
|
|
|
313
532
|
entity_name: "CloudSign vs DocuSign",
|
|
314
533
|
entity_kind: "concept",
|
|
315
534
|
layer: "caveat",
|
|
316
|
-
content:
|
|
535
|
+
content: JSON.stringify({
|
|
536
|
+
title: "DocuSign-JP >> CloudSign for reliability",
|
|
537
|
+
altitude: "strategy",
|
|
538
|
+
type: "comparison",
|
|
539
|
+
state: "decided",
|
|
540
|
+
what: "CloudSign (61% success) is significantly less reliable than DocuSign-JP (100% success). Recommend DocuSign when advising customers.",
|
|
541
|
+
why: "Based on KanseiLINK agent success rate data across multiple integrations",
|
|
542
|
+
affects: [],
|
|
543
|
+
next_action: null
|
|
544
|
+
}),
|
|
317
545
|
importance: 0.9 // user-explicit instruction = high priority
|
|
318
546
|
})
|
|
319
547
|
2. Confirm: "Recorded. Since it's in the caveat layer, it won't be forgotten."
|
|
320
548
|
```
|
|
321
549
|
|
|
550
|
+
### Case H — Proactive caveat surfacing (the "間違えたらやばい" pattern)
|
|
551
|
+
|
|
552
|
+
**This is the precision memory killer feature.** When you're about to take an action that could go wrong, check for past caveats BEFORE acting — even if the user didn't ask.
|
|
553
|
+
|
|
554
|
+
**Trigger**: You are about to do something risky or irreversible:
|
|
555
|
+
- Deploy / publish / push to production
|
|
556
|
+
- Delete / overwrite / destructive operation
|
|
557
|
+
- External API call (payment, email send, etc.)
|
|
558
|
+
- Architecture change affecting multiple files
|
|
559
|
+
- Changing auth/security configuration
|
|
560
|
+
|
|
561
|
+
```
|
|
562
|
+
1. recall({ query: "<entity> <action keyword>", layer: "caveat", max_tokens: 800 })
|
|
563
|
+
2. If caveat found:
|
|
564
|
+
a. STOP before acting
|
|
565
|
+
b. Tell user: "⚠️ Past caveat: <title>. <what>. Proceed anyway?"
|
|
566
|
+
c. Wait for confirmation
|
|
567
|
+
3. If no caveat → proceed normally
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**Example**: About to run `npm publish`:
|
|
571
|
+
```
|
|
572
|
+
recall({ query: "npm publish", layer: "caveat", max_tokens: 500 })
|
|
573
|
+
→ Caveat found: "npm token rotation — always verify token validity before publish"
|
|
574
|
+
→ "⚠️ Past caveat: npm token may need rotation. Want me to check `npm whoami` first?"
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Why this matters**: This is what separates precision memory from ambient memory. Ambient memory passively injects everything. Precision memory **actively intervenes at the moment it matters most** — when you're about to make the same mistake twice.
|
|
578
|
+
|
|
322
579
|
---
|
|
323
580
|
|
|
324
581
|
## 🔐 Privacy (when the user asks)
|
|
@@ -368,7 +625,7 @@ Now you have both bodies of knowledge before starting
|
|
|
368
625
|
|
|
369
626
|
---
|
|
370
627
|
|
|
371
|
-
*This skill runs on top of linksee-memory MCP v0.
|
|
628
|
+
*This skill runs on top of linksee-memory MCP v0.4.0+.*
|
|
372
629
|
*Auto-write via Stop hook, explicit read via recall.*
|
|
373
630
|
*Listed in MCP Official Registry, PulseMCP, mcpservers.org, Glama.*
|
|
374
631
|
*MIT License — Synapse Arrows PTE. LTD.*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linksee-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"mcpName": "io.github.michielinksee/linksee-memory",
|
|
5
5
|
"description": "Local-first agent memory MCP — cross-agent brain with 6-layer structured memory + token-saving file diff cache",
|
|
6
6
|
"type": "module",
|
|
@@ -69,14 +69,16 @@
|
|
|
69
69
|
"node": ">=20"
|
|
70
70
|
},
|
|
71
71
|
"overrides": {
|
|
72
|
-
"ip-address": "^10.2.0"
|
|
72
|
+
"ip-address": "^10.2.0",
|
|
73
|
+
"hono": "^4.12.18"
|
|
73
74
|
},
|
|
74
75
|
"pnpm": {
|
|
75
76
|
"onlyBuiltDependencies": [
|
|
76
77
|
"better-sqlite3"
|
|
77
78
|
],
|
|
78
79
|
"overrides": {
|
|
79
|
-
"ip-address": "^10.2.0"
|
|
80
|
+
"ip-address": "^10.2.0",
|
|
81
|
+
"hono": "^4.12.18"
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
}
|