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 +54 -65
- package/bin/cli.mjs +56 -1
- package/kyp_mem/__init__.py +1 -1
- package/kyp_mem/cli.py +223 -37
- package/kyp_mem/hooks.py +126 -0
- package/kyp_mem/server.py +37 -10
- package/kyp_mem/static/index.html +953 -310
- package/kyp_mem/ui.py +46 -2
- package/package.json +2 -2
- package/pyproject.toml +2 -2
package/README.md
CHANGED
|
@@ -1,49 +1,59 @@
|
|
|
1
1
|
# KYP-MEM — Know Your Project Memory
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
-
|
|
8
|
+
npm install -g kyp-mem
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Or
|
|
11
|
+
Or run directly:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
npx -y kyp-mem
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
## Setup
|
|
17
|
+
## Setup
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
#
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
npx -y kyp-mem setup-claude
|
|
27
|
+
## Auto-Learning
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
Sessions with fewer than 3 substantive actions are automatically skipped.
|
|
40
|
+
|
|
41
|
+
## Web UI
|
|
34
42
|
|
|
35
43
|
```bash
|
|
36
|
-
|
|
44
|
+
kyp-mem ui
|
|
37
45
|
```
|
|
38
46
|
|
|
39
|
-
Opens
|
|
40
|
-
-
|
|
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
|
-
-
|
|
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** —
|
|
59
|
-
- **Markdown
|
|
60
|
-
- **In-memory index** —
|
|
61
|
-
- **
|
|
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` |
|
|
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 (
|
|
89
|
+
## MCP Tools (9 tools)
|
|
77
90
|
|
|
78
91
|
| Tool | Description |
|
|
79
92
|
|------|-------------|
|
|
80
|
-
| `kyp_list` | Browse
|
|
81
|
-
| `kyp_read` |
|
|
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
|
|
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
|
|
113
|
+
Settings are in `HedgeConfig`. See [[Risk Management]] for safety checks.
|
|
105
114
|
```
|
|
106
115
|
|
|
107
|
-
`[[Wikilinks]]` are
|
|
116
|
+
`[[Wikilinks]]` are parsed, indexed, and resolved into navigable backlinks automatically.
|
|
108
117
|
|
|
109
118
|
## Manual Claude Code Config
|
|
110
119
|
|
|
111
|
-
|
|
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
|
|
134
|
-
|
|
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
|
-
|
|
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 {
|
|
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) {
|
package/kyp_mem/__init__.py
CHANGED
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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} —
|
|
304
|
+
print(f" {C}KYP-MEM{R} — Auto-Learning Hooks")
|
|
156
305
|
print()
|
|
157
|
-
print(f" {G}✓{R}
|
|
158
|
-
print(f" {D} File:
|
|
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"
|
|
163
|
-
print(f"
|
|
164
|
-
print(f"
|
|
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" {
|
|
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
|
|
216
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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")
|