kyp-mem 0.2.1 → 0.3.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 CHANGED
@@ -1,49 +1,59 @@
1
1
  # KYP-MEM — Know Your Project Memory
2
2
 
3
- **Headless knowledge base for AI agents.** Markdown notes with wikilinks, backlinks, tags, related notes, and a neon web UI — all powered by an MCP server so Claude (or any AI) can read and write your project knowledge directly.
3
+ **Persistent knowledge base for AI agents.** Markdown vault with wikilinks, backlinks, tags, graph navigation, and auto-learning — all powered by an MCP server so Claude (or any AI) can read and write project knowledge across sessions.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npx -y kyp-mem
8
+ npm install -g kyp-mem
9
9
  ```
10
10
 
11
- Or install the command globally:
11
+ Or run directly:
12
12
 
13
13
  ```bash
14
- npm install -g kyp-mem
14
+ npx -y kyp-mem
15
15
  ```
16
16
 
17
- ## Setup (3 commands)
17
+ ## Setup
18
18
 
19
19
  ```bash
20
- # 1. Choose where your vault (knowledge base) lives
21
- npx -y kyp-mem init
20
+ kyp-mem init # Choose vault location
21
+ kyp-mem setup-claude # Auto-configure Claude Code MCP
22
+ kyp-mem install-hooks # Enable auto-learning from sessions
23
+ ```
24
+
25
+ Restart Claude Code. Done — kyp-mem runs headlessly every session with 9 tools available.
22
26
 
23
- # 2. Connect to Claude Code — auto-configures MCP
24
- npx -y kyp-mem setup-claude
27
+ ## Auto-Learning
25
28
 
26
- # 3. Restart Claude Code — done!
27
- # kyp-mem now runs headlessly every session.
28
- # Claude can read/write/search your knowledge base.
29
+ KYP-MEM can automatically capture what happens in every Claude Code session:
30
+
31
+ ```bash
32
+ kyp-mem install-hooks --global
29
33
  ```
30
34
 
31
- That's it. Claude now has `kyp_read`, `kyp_write`, `kyp_search`, and 7 other tools available in every session.
35
+ This installs two hooks:
36
+ - **PostToolUse** — captures file edits, writes, and commands (pure Node, fast)
37
+ - **Stop** — compiles session activity into a vault note under `Sessions/`
32
38
 
33
- ## Optional: Web UI
39
+ Sessions with fewer than 3 substantive actions are automatically skipped.
40
+
41
+ ## Web UI
34
42
 
35
43
  ```bash
36
- npx -y kyp-mem ui
44
+ kyp-mem ui
37
45
  ```
38
46
 
39
- Opens a rich interface at `localhost:3333` with:
40
- - Collapsible folder tree
41
- - Rendered markdown with syntax highlighting
42
- - Clickable `[[wikilinks]]`
43
- - Backlinks and related notes panel
44
- - Interactive D3 graph view (toggleable)
47
+ Opens at `localhost:3333` with:
48
+ - Quick switcher (`Cmd+O`) — fuzzy jump to any note
45
49
  - Full-text search (`Cmd+K`)
46
- - Draggable resizable panels
50
+ - Tag filtering — clickable tag cloud, AND-filter
51
+ - Outline panel — heading TOC with click-to-scroll
52
+ - Backlink context — shows the surrounding line
53
+ - Unlinked mentions — finds references without `[[wikilinks]]`
54
+ - Inline editing — edit notes directly in the browser (`Cmd+S`)
55
+ - Local graph view — D3 force-directed graph of connections
56
+ - Resizable panels, collapsible tree, rendered markdown
47
57
 
48
58
  ## How It Works
49
59
 
@@ -55,95 +65,74 @@ Opens a rich interface at `localhost:3333` with:
55
65
  └──────────────┘
56
66
  ```
57
67
 
58
- - **Headless by default** — runs as an MCP server (stdio), no GUI needed
59
- - **Markdown files on disk** — plain `.md` files with YAML frontmatter, no database
60
- - **In-memory index** — links, backlinks, tags, search, similarity scoring
61
- - **Web UI optional** — `kyp-mem ui` when you want to browse visually
68
+ - **Headless by default** — MCP server over stdio, no GUI needed
69
+ - **Markdown on disk** — plain `.md` files with YAML frontmatter, no database
70
+ - **In-memory index** — wikilinks, backlinks, tags, word-level search index
71
+ - **Lightweight reads** — brief mode by default (~100 tokens), full content opt-in
72
+ - **Graph navigation** — follow `[[links]]` instead of searching broadly
62
73
 
63
74
  ## Commands
64
75
 
65
76
  | Command | What it does |
66
77
  |---------|-------------|
67
78
  | `kyp-mem init` | First-time setup — choose vault location |
68
- | `kyp-mem setup-claude` | Auto-configure Claude Code MCP settings |
79
+ | `kyp-mem setup-claude` | Register MCP server with Claude Code |
69
80
  | `kyp-mem setup-claude --global` | Configure globally (all projects) |
81
+ | `kyp-mem install-hooks` | Enable auto-learning from sessions |
82
+ | `kyp-mem install-hooks --remove` | Remove auto-learning hooks |
70
83
  | `kyp-mem serve` | Start MCP server (used by Claude, not you) |
71
84
  | `kyp-mem ui` | Open web UI at localhost:3333 |
72
85
  | `kyp-mem stats` | Print vault statistics |
73
86
  | `kyp-mem tree` | Print vault tree |
74
87
  | `kyp-mem doctor` | Check installation health |
75
88
 
76
- ## MCP Tools (what Claude gets)
89
+ ## MCP Tools (9 tools)
77
90
 
78
91
  | Tool | Description |
79
92
  |------|-------------|
80
- | `kyp_list` | Browse vault folders and notes |
81
- | `kyp_read` | Read a note content + tags + backlinks + related |
93
+ | `kyp_list` | Browse folders and notes with inline tags |
94
+ | `kyp_read` | Brief summary by default; `full=True` for complete content |
82
95
  | `kyp_write` | Create or update a note with tags and properties |
83
96
  | `kyp_delete` | Delete a note |
84
- | `kyp_search` | Full-text search across all notes |
97
+ | `kyp_search` | Full-text search with optional tag filter |
85
98
  | `kyp_tags` | List all tags or filter notes by tag |
86
- | `kyp_related` | Find related notes by links, tags, proximity |
99
+ | `kyp_related` | Find related notes by links, tags, folder proximity |
87
100
  | `kyp_recent` | Recently modified notes |
88
101
  | `kyp_stats` | Vault statistics |
89
102
 
90
103
  ## Note Format
91
104
 
92
- Standard markdown with YAML frontmatter:
93
-
94
105
  ```markdown
95
106
  ---
96
107
  tags: [project, trading, config]
97
- source: config.py
98
108
  created: 2026-05-12
99
- updated: 2026-05-12
100
109
  ---
101
110
 
102
111
  # Configuration
103
112
 
104
- Settings are defined in `HedgeConfig`. See [[Risk Management]] for safety checks.
113
+ Settings are in `HedgeConfig`. See [[Risk Management]] for safety checks.
105
114
  ```
106
115
 
107
- `[[Wikilinks]]` are automatically parsed, indexed, and turned into navigable backlinks.
116
+ `[[Wikilinks]]` are parsed, indexed, and resolved into navigable backlinks automatically.
108
117
 
109
118
  ## Manual Claude Code Config
110
119
 
111
- If you prefer to configure manually instead of using `setup-claude`:
112
-
113
- ```json
114
- {
115
- "mcpServers": {
116
- "kyp-mem": {
117
- "command": "npx",
118
- "args": ["-y", "kyp-mem", "serve"],
119
- "env": {
120
- "KYP_VAULT": "~/.kyp-mem/vault"
121
- }
122
- }
123
- }
124
- }
120
+ ```bash
121
+ claude mcp add -s user -e KYP_VAULT="$HOME/.kyp-mem/vault" kyp-mem -- npx -y kyp-mem serve
125
122
  ```
126
123
 
127
- Add to `~/.claude/settings.json` (global) or `.claude/settings.json` (per-project).
128
-
129
124
  ## Architecture
130
125
 
131
126
  ```
132
127
  ~/.kyp-mem/
133
- ├── config.json # vault path + settings
134
- └── vault/ # your knowledge base
128
+ ├── config.json # vault path
129
+ ├── sessions/ # auto-learning session logs
130
+ └── vault/
135
131
  ├── Project A/
136
132
  │ ├── Architecture.md
137
- │ ├── Configuration.md
138
133
  │ └── Bugs.md
139
- └── Project B/
140
- └── ...
141
- ```
142
-
143
- ## Publishing to npm
144
-
145
- ```bash
146
- npm publish
134
+ ├── Sessions/ # auto-captured session notes
135
+ └── ...
147
136
  ```
148
137
 
149
138
  ## License
package/bin/cli.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from "child_process";
4
- import { delimiter, dirname, resolve } from "path";
4
+ import { appendFileSync, mkdirSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { delimiter, dirname, join, resolve } from "path";
5
7
  import { fileURLToPath } from "url";
6
8
 
7
9
  const args = process.argv.slice(2);
@@ -47,6 +49,59 @@ function findPython() {
47
49
  return null;
48
50
  }
49
51
 
52
+ // --- Hook fast path (pure Node, no Python startup) ---
53
+ if (args[0] === "hook") {
54
+ const hookType = args[1];
55
+ const sessionDir = join(homedir(), ".kyp-mem", "sessions");
56
+ const sessionFile = join(sessionDir, "current.jsonl");
57
+
58
+ const chunks = [];
59
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
60
+ await new Promise((r) => process.stdin.on("end", r));
61
+ const raw = Buffer.concat(chunks).toString();
62
+
63
+ if (hookType === "post-tool-use") {
64
+ try {
65
+ const data = JSON.parse(raw);
66
+ const tool = data.tool_name || "";
67
+ if (tool.includes("kyp-mem") || tool.includes("kyp_mem")) process.exit(0);
68
+
69
+ const input = data.tool_input || {};
70
+ const entry = { ts: new Date().toISOString(), tool, cwd: process.cwd() };
71
+
72
+ if (tool === "Edit" || tool === "Write") {
73
+ entry.file = input.file_path || "";
74
+ entry.action = tool === "Edit" ? "edit" : "create";
75
+ } else if (tool === "Bash") {
76
+ entry.command = (input.command || "").slice(0, 300);
77
+ entry.action = "command";
78
+ } else {
79
+ entry.action = "other";
80
+ entry.detail = tool;
81
+ }
82
+
83
+ mkdirSync(sessionDir, { recursive: true });
84
+ appendFileSync(sessionFile, JSON.stringify(entry) + "\n");
85
+ } catch (_) {
86
+ // silent — hooks must never break the flow
87
+ }
88
+ process.exit(0);
89
+ }
90
+
91
+ if (hookType === "stop") {
92
+ const py = findPython();
93
+ if (py) {
94
+ const [cmd, pre] = py;
95
+ const r = run(cmd, [...pre, "-m", "kyp_mem.hooks", "stop"], "inherit");
96
+ process.exit(r.status ?? 0);
97
+ }
98
+ process.exit(0);
99
+ }
100
+
101
+ console.error("Unknown hook type:", hookType);
102
+ process.exit(1);
103
+ }
104
+
50
105
  const python = findPython();
51
106
 
52
107
  if (python) {
@@ -1,3 +1,3 @@
1
1
  """KYP-MEM — Know Your Project Memory. Headless knowledge base for AI agents."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.3.0"
package/kyp_mem/cli.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  import json
5
5
  import shutil
6
6
  import argparse
7
+ import subprocess
7
8
  from pathlib import Path
8
9
 
9
10
  C = "\033[36m" # cyan
@@ -35,6 +36,11 @@ def main():
35
36
 
36
37
  subparsers.add_parser("stats", help="Print vault statistics")
37
38
  subparsers.add_parser("tree", help="Print vault tree")
39
+ ih = subparsers.add_parser("install-hooks", help="Set up auto-learning hooks for Claude Code")
40
+ ih.add_argument("--global", dest="global_config", action="store_true",
41
+ help="Add hooks to global ~/.claude/settings.json (default: project)")
42
+ ih.add_argument("--remove", action="store_true", help="Remove KYP-MEM hooks")
43
+
38
44
  subparsers.add_parser("doctor", help="Check installation and config health")
39
45
 
40
46
  args = parser.parse_args()
@@ -56,6 +62,8 @@ def main():
56
62
  _run_stats()
57
63
  elif args.command == "tree":
58
64
  _run_tree()
65
+ elif args.command == "install-hooks":
66
+ _run_install_hooks(global_config=args.global_config, remove=args.remove)
59
67
  elif args.command == "doctor":
60
68
  _run_doctor()
61
69
  else:
@@ -107,28 +115,112 @@ def _run_setup_claude(global_config: bool = False):
107
115
  from .config import get_vault_path
108
116
 
109
117
  vault_path = get_vault_path()
118
+ mcp_command, mcp_args = _get_mcp_command()
119
+ claude_scope = "user" if global_config else "local"
120
+ scope_label = "global user" if global_config else "local project"
121
+
122
+ registered, detail = _register_with_claude_mcp(
123
+ claude_scope,
124
+ mcp_command,
125
+ mcp_args,
126
+ vault_path,
127
+ )
128
+
129
+ print()
130
+ print(f" {C}KYP-MEM{R} — Claude Code Setup")
131
+ print()
132
+ if registered:
133
+ print(f" {G}✓{R} MCP server registered with Claude Code ({scope_label})")
134
+ else:
135
+ settings_path = _write_legacy_claude_settings(global_config, mcp_command, mcp_args, vault_path)
136
+ print(f" {Y}✗{R} Could not register with Claude Code's MCP manager")
137
+ print(f" {D} Reason: {detail}{R}")
138
+ print(f" {Y}!{R} Wrote legacy settings as a fallback")
139
+ print(f" {D} File: {settings_path}{R}")
140
+ print(f" {D} Command: {mcp_command} {' '.join(mcp_args)}{R}")
141
+ print(f" {D} Vault: {vault_path}{R}")
142
+ print()
143
+ print(f" {C}Done!{R} Restart Claude Code and kyp-mem will run automatically.")
144
+ print(f" Claude gets these tools: kyp_list, kyp_read, kyp_write, kyp_delete,")
145
+ print(f" kyp_search, kyp_tags, kyp_related, kyp_recent, kyp_stats")
146
+ print()
147
+ print(f" {D}To open the web UI anytime:{R} {Y}kyp-mem ui{R}")
148
+ print()
149
+
150
+
151
+ def _get_mcp_command() -> tuple[str, list[str]]:
110
152
  kyp_mem_bin = shutil.which("kyp-mem")
111
153
  npx_bin = shutil.which("npx")
112
154
 
113
155
  if kyp_mem_bin and "_npx" not in Path(kyp_mem_bin).parts:
114
- mcp_command = kyp_mem_bin
115
- mcp_args = ["serve"]
116
- elif npx_bin:
117
- mcp_command = npx_bin
118
- mcp_args = ["-y", "kyp-mem", "serve"]
119
- else:
120
- print(f" {Y}Warning:{R} 'kyp-mem' not found in PATH.")
121
- print(f" {D}Make sure you installed with: npm install -g kyp-mem{R}")
122
- print()
123
- mcp_command = "kyp-mem"
124
- mcp_args = ["serve"]
156
+ return kyp_mem_bin, ["serve"]
157
+ if npx_bin:
158
+ return npx_bin, ["-y", "kyp-mem", "serve"]
125
159
 
160
+ print(f" {Y}Warning:{R} 'kyp-mem' not found in PATH.")
161
+ print(f" {D}Make sure you installed with: npm install -g kyp-mem{R}")
162
+ print()
163
+ return "kyp-mem", ["serve"]
164
+
165
+
166
+ def _register_with_claude_mcp(
167
+ scope: str,
168
+ mcp_command: str,
169
+ mcp_args: list[str],
170
+ vault_path: str,
171
+ ) -> tuple[bool, str]:
172
+ claude_bin = shutil.which("claude")
173
+ if not claude_bin:
174
+ return False, "'claude' CLI not found in PATH"
175
+
176
+ server_config = {
177
+ "type": "stdio",
178
+ "command": mcp_command,
179
+ "args": mcp_args,
180
+ "env": {
181
+ "KYP_VAULT": vault_path,
182
+ },
183
+ }
184
+
185
+ # Make setup idempotent when the user reruns it with a new vault or binary.
186
+ subprocess.run(
187
+ [claude_bin, "mcp", "remove", "-s", scope, "kyp-mem"],
188
+ stdout=subprocess.DEVNULL,
189
+ stderr=subprocess.DEVNULL,
190
+ text=True,
191
+ )
192
+
193
+ result = subprocess.run(
194
+ [
195
+ claude_bin,
196
+ "mcp",
197
+ "add-json",
198
+ "-s",
199
+ scope,
200
+ "kyp-mem",
201
+ json.dumps(server_config),
202
+ ],
203
+ capture_output=True,
204
+ text=True,
205
+ )
206
+
207
+ if result.returncode == 0:
208
+ return True, result.stdout.strip()
209
+
210
+ detail = (result.stderr or result.stdout or "unknown error").strip()
211
+ return False, detail
212
+
213
+
214
+ def _write_legacy_claude_settings(
215
+ global_config: bool,
216
+ mcp_command: str,
217
+ mcp_args: list[str],
218
+ vault_path: str,
219
+ ) -> Path:
126
220
  if global_config:
127
221
  settings_path = Path.home() / ".claude" / "settings.json"
128
- scope = "global"
129
222
  else:
130
223
  settings_path = Path.cwd() / ".claude" / "settings.json"
131
- scope = "project"
132
224
 
133
225
  settings_path.parent.mkdir(parents=True, exist_ok=True)
134
226
 
@@ -140,7 +232,6 @@ def _run_setup_claude(global_config: bool = False):
140
232
  settings = {}
141
233
 
142
234
  mcp_servers = settings.setdefault("mcpServers", {})
143
-
144
235
  mcp_servers["kyp-mem"] = {
145
236
  "command": mcp_command,
146
237
  "args": mcp_args,
@@ -149,21 +240,79 @@ def _run_setup_claude(global_config: bool = False):
149
240
  },
150
241
  }
151
242
 
243
+ settings_path.write_text(json.dumps(settings, indent=2) + "\n")
244
+ return settings_path
245
+
246
+
247
+ def _run_install_hooks(global_config: bool = False, remove: bool = False):
248
+ mcp_command, _ = _get_mcp_command()
249
+
250
+ if global_config:
251
+ settings_path = Path.home() / ".claude" / "settings.json"
252
+ scope_label = "global"
253
+ else:
254
+ settings_path = Path.cwd() / ".claude" / "settings.json"
255
+ scope_label = "project"
256
+
257
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
258
+ settings = {}
259
+ if settings_path.exists():
260
+ try:
261
+ settings = json.loads(settings_path.read_text())
262
+ except json.JSONDecodeError:
263
+ settings = {}
264
+
265
+ hooks = settings.setdefault("hooks", {})
266
+
267
+ if remove:
268
+ changed = False
269
+ for event in ("PostToolUse", "Stop"):
270
+ if event in hooks:
271
+ hooks[event] = [h for h in hooks[event] if "kyp-mem hook" not in h.get("command", "")]
272
+ if not hooks[event]:
273
+ del hooks[event]
274
+ changed = True
275
+ if not hooks:
276
+ del settings["hooks"]
277
+ settings_path.write_text(json.dumps(settings, indent=2) + "\n")
278
+ print()
279
+ print(f" {G}✓{R} KYP-MEM hooks removed from {scope_label} settings")
280
+ print(f" {D} File: {settings_path}{R}")
281
+ print()
282
+ return
283
+
284
+ post_tool_hooks = hooks.setdefault("PostToolUse", [])
285
+ stop_hooks = hooks.setdefault("Stop", [])
286
+
287
+ post_tool_hooks = [h for h in post_tool_hooks if "kyp-mem hook" not in h.get("command", "")]
288
+ stop_hooks = [h for h in stop_hooks if "kyp-mem hook" not in h.get("command", "")]
289
+
290
+ post_tool_hooks.append({
291
+ "matcher": "Edit|Write|Bash",
292
+ "hooks": [{"type": "command", "command": f"{mcp_command} hook post-tool-use"}],
293
+ })
294
+ stop_hooks.append({
295
+ "hooks": [{"type": "command", "command": f"{mcp_command} hook stop"}],
296
+ })
297
+
298
+ hooks["PostToolUse"] = post_tool_hooks
299
+ hooks["Stop"] = stop_hooks
300
+
152
301
  settings_path.write_text(json.dumps(settings, indent=2) + "\n")
153
302
 
154
303
  print()
155
- print(f" {C}KYP-MEM{R} — Claude Code Setup")
304
+ print(f" {C}KYP-MEM{R} — Auto-Learning Hooks")
156
305
  print()
157
- print(f" {G}✓{R} MCP server added to {scope} settings")
158
- print(f" {D} File: {settings_path}{R}")
159
- print(f" {D} Command: {mcp_command} {' '.join(mcp_args)}{R}")
160
- print(f" {D} Vault: {vault_path}{R}")
306
+ print(f" {G}✓{R} Hooks installed ({scope_label})")
307
+ print(f" {D} File: {settings_path}{R}")
161
308
  print()
162
- print(f" {C}Done!{R} Restart Claude Code and kyp-mem will run automatically.")
163
- print(f" Claude gets these tools: kyp_list, kyp_read, kyp_write, kyp_delete,")
164
- print(f" kyp_search, kyp_tags, kyp_related, kyp_recent, kyp_stats")
309
+ print(f" How it works:")
310
+ print(f" {D} • PostToolUse hook captures file edits, writes, and commands{R}")
311
+ print(f" {D} • Stop hook compiles the session into a vault note{R}")
312
+ print(f" {D} • Notes saved under Sessions/ with timestamps and tags{R}")
313
+ print(f" {D} • Sessions with < 3 substantive actions are skipped{R}")
165
314
  print()
166
- print(f" {D}To open the web UI anytime:{R} {Y}kyp-mem ui{R}")
315
+ print(f" {C}Done!{R} Restart Claude Code. Sessions will auto-save to your vault.")
167
316
  print()
168
317
 
169
318
 
@@ -212,22 +361,59 @@ def _run_doctor():
212
361
  else:
213
362
  print(f" {Y}✗{R} Vault not found: {vault_path}")
214
363
 
215
- # Claude Code config
216
- for label, path in [
364
+ # Claude Code MCP registration
365
+ claude_bin = shutil.which("claude")
366
+ if claude_bin:
367
+ result = subprocess.run(
368
+ [claude_bin, "mcp", "get", "kyp-mem"],
369
+ capture_output=True,
370
+ text=True,
371
+ )
372
+ if result.returncode == 0 and "Status: ✓ Connected" in result.stdout:
373
+ print(f" {G}✓{R} Claude Code MCP: kyp-mem connected")
374
+ elif result.returncode == 0:
375
+ print(f" {Y}✗{R} Claude Code MCP: kyp-mem registered but not connected")
376
+ else:
377
+ print(f" {Y}✗{R} Claude Code MCP: kyp-mem not active")
378
+ else:
379
+ print(f" {Y}✗{R} Claude Code CLI not found in PATH")
380
+
381
+ legacy_paths = [
217
382
  ("project", Path.cwd() / ".claude" / "settings.json"),
218
383
  ("global", Path.home() / ".claude" / "settings.json"),
219
- ]:
220
- if path.exists():
221
- try:
222
- s = json.loads(path.read_text())
223
- if "kyp-mem" in s.get("mcpServers", {}):
224
- print(f" {G}✓{R} Claude Code ({label}): kyp-mem configured")
225
- else:
226
- print(f" {D}·{R} Claude Code ({label}): exists but kyp-mem not configured")
227
- except json.JSONDecodeError:
228
- print(f" {Y}✗{R} Claude Code ({label}): invalid JSON")
229
- else:
230
- print(f" {D}·{R} Claude Code ({label}): no settings file")
384
+ ]
385
+ for label, path in legacy_paths:
386
+ if not path.exists():
387
+ continue
388
+ try:
389
+ s = json.loads(path.read_text())
390
+ except json.JSONDecodeError:
391
+ print(f" {Y}{R} Legacy Claude settings ({label}): invalid JSON")
392
+ continue
393
+ if "kyp-mem" in s.get("mcpServers", {}):
394
+ print(f" {D}·{R} Legacy Claude settings ({label}): kyp-mem entry present")
395
+
396
+ # Hooks
397
+ for label, path in legacy_paths:
398
+ if not path.exists():
399
+ continue
400
+ try:
401
+ s = json.loads(path.read_text())
402
+ except json.JSONDecodeError:
403
+ continue
404
+ hooks = s.get("hooks", {})
405
+ has_post = any("kyp-mem hook" in h.get("command", "") for h in hooks.get("PostToolUse", []))
406
+ has_stop = any("kyp-mem hook" in h.get("command", "") for h in hooks.get("Stop", []))
407
+ if has_post and has_stop:
408
+ print(f" {G}✓{R} Auto-learning hooks installed ({label})")
409
+ elif has_post or has_stop:
410
+ print(f" {Y}!{R} Partial hooks installed ({label}) — run: kyp-mem install-hooks")
411
+
412
+ # Session log
413
+ session_file = Path.home() / ".kyp-mem" / "sessions" / "current.jsonl"
414
+ if session_file.exists():
415
+ line_count = len(session_file.read_text().strip().split("\n"))
416
+ print(f" {D}·{R} Active session log: {line_count} entries")
231
417
 
232
418
  # Binary
233
419
  kyp_bin = shutil.which("kyp-mem")