kyp-mem 0.4.1 → 0.4.3

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,140 +1,289 @@
1
1
  # KYP-MEM — Know Your Project Memory
2
2
 
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.
3
+ **Persistent memory for AI coding agents.** Your AI agent forgets everything between sessions. KYP-MEM fixes that.
4
4
 
5
- ## Install
5
+ KYP-MEM is an MCP server that gives your AI agent a knowledge base and session memory. It remembers your architecture, past bugs, decisions, and what happened last session — so you never repeat yourself.
6
+
7
+ ---
8
+
9
+ ## How It Works
10
+
11
+ KYP-MEM gives your agent two types of memory:
12
+
13
+ ### Knowledge Base — long-term project memory
14
+
15
+ Structured markdown notes organized by project. Architecture docs, API references, known bugs, key decisions, setup guides. The agent reads these before doing any work and updates them as it learns.
6
16
 
7
- ```bash
8
- npm install -g kyp-mem
9
17
  ```
18
+ MyProject/
19
+ ├── Knowledge.md # Architecture, bugs, decisions, notes
20
+ ├── API.md # Endpoints and contracts
21
+ ├── Setup.md # Environment setup guide
22
+ └── Sessions/
23
+ ```
24
+
25
+ ### Session Memory — what happened each session
10
26
 
11
- Or run directly:
27
+ Every coding session is automatically captured — files changed, commands run, prompts used. These notes are embedded into a vector database for semantic search, so the agent can find past work by meaning, not just keywords.
28
+
29
+ ```
30
+ MyProject/Sessions/
31
+ ├── 2026-05-12_143022.md # "Fixed auth bug, found rate limiter issue"
32
+ ├── 2026-05-12_091544.md # "Investigated flaky tests — race condition"
33
+ └── 2026-05-11_162301.md # "Set up CI, decided on GitHub Actions"
34
+ ```
35
+
36
+ ### The Loop
37
+
38
+ ```
39
+ Session Start
40
+ → Agent loads project knowledge + recent sessions
41
+ → Agent is grounded: knows architecture, past bugs, last session's next steps
42
+
43
+ During Work
44
+ → Agent hits a bug → searches session memory → finds it was already investigated
45
+ → Agent makes a decision → updates Knowledge.md so future sessions know
46
+
47
+ Session End
48
+ → Hooks auto-capture everything into a structured session note
49
+ → Note is embedded into the vector store for future semantic search
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Install
12
55
 
13
56
  ```bash
14
- npx -y kyp-mem
57
+ pip install kyp-mem
15
58
  ```
16
59
 
17
60
  ## Setup
18
61
 
19
62
  ```bash
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
63
+ kyp-mem init # Choose where to store your vault
64
+ kyp-mem setup-claude # Register MCP server with Claude Code
65
+ kyp-mem install-hooks # Enable automatic session capture
23
66
  ```
24
67
 
25
- Restart Claude Code. Done — kyp-mem runs headlessly every session with 9 tools available.
68
+ Restart Claude Code. KYP-MEM runs automatically with 14 tools available to the agent.
26
69
 
27
- ## Auto-Learning
70
+ ### Quick Setup (one command)
71
+
72
+ ```bash
73
+ claude mcp add -s user -e KYP_VAULT="$HOME/.kyp-mem/vault" kyp-mem -- kyp-mem serve
74
+ ```
28
75
 
29
- KYP-MEM can automatically capture what happens in every Claude Code session:
76
+ ### Enable for All Projects
30
77
 
31
78
  ```bash
79
+ kyp-mem setup-claude --global
32
80
  kyp-mem install-hooks --global
33
81
  ```
34
82
 
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/`
83
+ ---
84
+
85
+ ## What the Agent Does Automatically
86
+
87
+ KYP-MEM embeds behavioral instructions directly into tool descriptions. The agent follows these rules without any user action:
88
+
89
+ 1. **Session start** — loads project knowledge + recent session history
90
+ 2. **Before investigating bugs** — searches session memory first to avoid duplicate work
91
+ 3. **Before making decisions** — checks if a prior session already decided this
92
+ 4. **After fixing or learning something** — updates Knowledge.md for future sessions
93
+ 5. **Never hallucinates** — if it's not in the knowledge base, it says so
94
+
95
+ No prompting needed. The agent reads the tool descriptions and follows the protocol.
96
+
97
+ ---
98
+
99
+ ## Automatic Session Capture
100
+
101
+ ```bash
102
+ kyp-mem install-hooks
103
+ ```
104
+
105
+ Installs three Claude Code hooks:
106
+
107
+ - **UserPromptSubmit** — captures every prompt you send to the agent
108
+ - **PostToolUse** — captures file edits, writes, and shell commands in real-time
109
+ - **Stop** — when the session ends, compiles all activity into a structured note
110
+
111
+ Each session note looks like this:
112
+
113
+ ```markdown
114
+ # Session 2026-05-12_143022
115
+
116
+ **Project:** MyProject
117
+ **Actions:** 15 total, 12 substantive
118
+
119
+ ## Summary
120
+ Fixed auth bug, refactored token refresh logic.
121
+
122
+ ## PROMPTS
123
+ ### 1. [14:25:01]
124
+ > fix the token refresh bug in auth.py
125
+
126
+ ### 2. [14:30:22]
127
+ > also add retry logic for expired tokens
128
+
129
+ ## INVESTIGATED
130
+ - grep for "token expired" across auth module
131
+ - read OAuth provider docs
132
+
133
+ ## LEARNED
134
+ - Refresh tokens expire after 30 days, not 90
135
+
136
+ ## COMPLETED
137
+ - Fixed token refresh in auth.py
138
+ - Added retry logic for expired tokens
139
+
140
+ ## NEXT STEPS
141
+ - Add integration test for token refresh flow
142
+ ```
38
143
 
39
144
  Sessions with fewer than 3 substantive actions are automatically skipped.
40
145
 
146
+ ---
147
+
148
+ ## Semantic Search
149
+
150
+ Session notes are embedded into a vector database (ChromaDB). This enables search by meaning:
151
+
152
+ - Searching **"authentication failing"** finds sessions about "login bug" and "OAuth token expiry"
153
+ - Searching **"deploy process"** finds sessions about "CI pipeline setup" and "release workflow"
154
+
155
+ The agent doesn't need exact keywords — it finds semantically related past work.
156
+
157
+ ---
158
+
41
159
  ## Web UI
42
160
 
43
161
  ```bash
44
162
  kyp-mem ui
45
163
  ```
46
164
 
47
- Opens at `localhost:3333` with:
48
- - Quick switcher (`Cmd+O`) — fuzzy jump to any note
49
- - Full-text search (`Cmd+K`)
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
165
+ Opens at `localhost:3333` with two panels:
57
166
 
58
- ## How It Works
167
+ **Session Memory** semantic search across all sessions, grouped by project with timestamps
59
168
 
60
- ```
61
- ┌─────────────┐ ┌─────────────┐ ┌──────────────┐
62
- │ Claude Code │──stdio──▶│ kyp-mem │──read/──▶│ ~/.kyp-mem/ │
63
- │ (any AI) │◀─────────│ MCP server │ write │ vault/ │
64
- └─────────────┘ └─────────────┘ │ *.md files │
65
- └──────────────┘
66
- ```
169
+ **Knowledge Base** — file tree with folders, notes, tags. Full-text search, tag filtering, quick switcher (`Cmd+O`)
67
170
 
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
171
+ **Editor** — rendered markdown with `[[wikilink]]` navigation, inline editing
73
172
 
74
- ## Commands
173
+ **Graph** — knowledge graph showing connections between notes, backlinks, and related content
75
174
 
76
- | Command | What it does |
77
- |---------|-------------|
78
- | `kyp-mem init` | First-time setup — choose vault location |
79
- | `kyp-mem setup-claude` | Register MCP server with Claude Code |
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 |
83
- | `kyp-mem serve` | Start MCP server (used by Claude, not you) |
84
- | `kyp-mem ui` | Open web UI at localhost:3333 |
85
- | `kyp-mem stats` | Print vault statistics |
86
- | `kyp-mem tree` | Print vault tree |
87
- | `kyp-mem doctor` | Check installation health |
175
+ ---
176
+
177
+ ## MCP Tools (14 total)
178
+
179
+ ### Agent Behavior
180
+
181
+ | Tool | Purpose |
182
+ |------|---------|
183
+ | `____kyp_instructions` | Embeds behavioral rules the agent follows automatically |
184
+ | `kyp_project_context` | Loads project knowledge + recent sessions at session start |
88
185
 
89
- ## MCP Tools (9 tools)
186
+ ### Knowledge Base
90
187
 
91
- | Tool | Description |
92
- |------|-------------|
93
- | `kyp_list` | Browse folders and notes with inline tags |
94
- | `kyp_read` | Brief summary by default; `full=True` for complete content |
95
- | `kyp_write` | Create or update a note with tags and properties |
188
+ | Tool | Purpose |
189
+ |------|---------|
190
+ | `kyp_list` | Browse folders and notes |
191
+ | `kyp_read` | Read a note (summary by default, `full=True` for complete) |
192
+ | `kyp_write` | Create or update a note with tags and `[[wikilinks]]` |
96
193
  | `kyp_delete` | Delete a note |
97
194
  | `kyp_search` | Full-text search with optional tag filter |
98
195
  | `kyp_tags` | List all tags or filter notes by tag |
99
- | `kyp_related` | Find related notes by links, tags, folder proximity |
196
+ | `kyp_related` | Find related notes by links, tags, proximity |
100
197
  | `kyp_recent` | Recently modified notes |
101
198
  | `kyp_stats` | Vault statistics |
102
199
 
103
- ## Note Format
200
+ ### Session Memory
201
+
202
+ | Tool | Purpose |
203
+ |------|---------|
204
+ | `kyp_session_search` | Semantic search across all session logs |
205
+ | `kyp_session_create` | Manually create a session note |
206
+ | `kyp_sessions` | List sessions by project |
104
207
 
105
- ```markdown
106
- ---
107
- tags: [project, trading, config]
108
- created: 2026-05-12
109
208
  ---
110
209
 
111
- # Configuration
210
+ ## Adding to Your Project
112
211
 
113
- Settings are in `HedgeConfig`. See [[Risk Management]] for safety checks.
114
- ```
212
+ Add a `CLAUDE.md` to any project root:
115
213
 
116
- `[[Wikilinks]]` are parsed, indexed, and resolved into navigable backlinks automatically.
214
+ ```markdown
215
+ # Project Memory
216
+
217
+ ## Session Start (MANDATORY)
218
+ Call `kyp_project_context("PROJECT_NAME")` at the start of every session to load:
219
+ - Project knowledge base (architecture, bugs, decisions)
220
+ - Recent session history (what was done, what's next)
221
+
222
+ ## During Work
223
+ - Before investigating bugs: `kyp_session_search("error or symptom")`
224
+ - Before making decisions: `kyp_session_search("topic")`
225
+ - After fixing bugs: update Knowledge.md via `kyp_write`
226
+ - After decisions: add to Key Decisions in Knowledge.md
227
+
228
+ ## Rules
229
+ - Never hallucinate project details — check the knowledge base first.
230
+ - Use [[wikilinks]] to connect related notes.
231
+ - Sessions are captured automatically — no manual logging needed.
232
+ ```
117
233
 
118
- ## Manual Claude Code Config
234
+ A template is available at `templates/CLAUDE.md.template`.
119
235
 
120
- ```bash
121
- claude mcp add -s user -e KYP_VAULT="$HOME/.kyp-mem/vault" kyp-mem -- npx -y kyp-mem serve
122
- ```
236
+ ---
123
237
 
124
- ## Architecture
238
+ ## Vault Structure
125
239
 
126
240
  ```
127
241
  ~/.kyp-mem/
128
- ├── config.json # vault path
129
- ├── sessions/ # auto-learning session logs
242
+ ├── config.json # Vault path configuration
243
+ ├── chroma/ # Vector database for semantic search
130
244
  └── vault/
131
- ├── Project A/
132
- │ ├── Architecture.md
133
- └── Bugs.md
134
- ├── Sessions/ # auto-captured session notes
245
+ ├── ProjectA/
246
+ │ ├── Knowledge.md # Ground truth: architecture, bugs, decisions
247
+ ├── API.md
248
+ │ └── Sessions/
249
+ │ ├── 2026-05-12_143022.md
250
+ │ └── 2026-05-11_091544.md
251
+ ├── ProjectB/
252
+ │ ├── Knowledge.md
253
+ │ └── Sessions/
135
254
  └── ...
136
255
  ```
137
256
 
257
+ Notes use YAML frontmatter for tags and `[[wikilinks]]` for cross-references:
258
+
259
+ ```markdown
260
+ ---
261
+ tags: [trading, config]
262
+ created: 2026-05-12
263
+ ---
264
+ # Configuration
265
+ Settings are in `HedgeConfig`. See [[Risk Management]] for limits.
266
+ ```
267
+
268
+ ---
269
+
270
+ ## CLI Commands
271
+
272
+ | Command | What it does |
273
+ |---------|-------------|
274
+ | `kyp-mem init` | First-time setup — choose vault location |
275
+ | `kyp-mem setup-claude` | Register MCP server with Claude Code |
276
+ | `kyp-mem setup-claude --global` | Register globally (all projects) |
277
+ | `kyp-mem install-hooks` | Enable automatic session capture |
278
+ | `kyp-mem install-hooks --remove` | Remove session capture hooks |
279
+ | `kyp-mem serve` | Start MCP server (stdio, used by the agent) |
280
+ | `kyp-mem ui` | Open web UI at localhost:3333 |
281
+ | `kyp-mem stats` | Print vault statistics |
282
+ | `kyp-mem tree` | Print vault tree |
283
+ | `kyp-mem doctor` | Check installation health |
284
+
285
+ ---
286
+
138
287
  ## License
139
288
 
140
289
  MIT
package/bin/cli.mjs CHANGED
@@ -60,6 +60,23 @@ if (args[0] === "hook") {
60
60
  await new Promise((r) => process.stdin.on("end", r));
61
61
  const raw = Buffer.concat(chunks).toString();
62
62
 
63
+ if (hookType === "user-prompt") {
64
+ try {
65
+ const data = JSON.parse(raw);
66
+ const prompt = (data.prompt || "").trim();
67
+ if (!prompt) process.exit(0);
68
+ const entry = {
69
+ ts: new Date().toISOString(),
70
+ cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
71
+ action: "prompt",
72
+ prompt,
73
+ };
74
+ mkdirSync(sessionDir, { recursive: true });
75
+ appendFileSync(sessionFile, JSON.stringify(entry) + "\n");
76
+ } catch (_) {}
77
+ process.exit(0);
78
+ }
79
+
63
80
  if (hookType === "post-tool-use") {
64
81
  try {
65
82
  const data = JSON.parse(raw);
@@ -72,6 +89,9 @@ if (args[0] === "hook") {
72
89
  if (tool === "Edit" || tool === "Write") {
73
90
  entry.file = input.file_path || "";
74
91
  entry.action = tool === "Edit" ? "edit" : "create";
92
+ } else if (tool === "Read") {
93
+ entry.file = input.file_path || "";
94
+ entry.action = "read";
75
95
  } else if (tool === "Bash") {
76
96
  entry.command = (input.command || "").slice(0, 300);
77
97
  entry.action = "command";
@@ -1,3 +1,3 @@
1
1
  """KYP-MEM — Know Your Project Memory. Headless knowledge base for AI agents."""
2
2
 
3
- __version__ = "0.4.1"
3
+ __version__ = "0.4.2"
package/kyp_mem/cli.py CHANGED
@@ -43,6 +43,12 @@ def main():
43
43
 
44
44
  subparsers.add_parser("doctor", help="Check installation and config health")
45
45
 
46
+ hook_parser = subparsers.add_parser("hook", help="Handle Claude Code hook events (internal)")
47
+ hook_sub = hook_parser.add_subparsers(dest="hook_command")
48
+ hook_sub.add_parser("post-tool-use", help="Capture tool activity to session log")
49
+ hook_sub.add_parser("user-prompt", help="Capture user prompt to session log")
50
+ hook_sub.add_parser("stop", help="Compile session into vault note")
51
+
46
52
  args = parser.parse_args()
47
53
 
48
54
  if args.vault:
@@ -66,6 +72,14 @@ def main():
66
72
  _run_install_hooks(global_config=args.global_config, remove=args.remove)
67
73
  elif args.command == "doctor":
68
74
  _run_doctor()
75
+ elif args.command == "hook":
76
+ from .hooks import handle_post_tool_use, handle_user_prompt, handle_stop
77
+ if args.hook_command == "post-tool-use":
78
+ handle_post_tool_use()
79
+ elif args.hook_command == "user-prompt":
80
+ handle_user_prompt()
81
+ elif args.hook_command == "stop":
82
+ handle_stop()
69
83
  else:
70
84
  _print_banner()
71
85
  parser.print_help()
@@ -264,11 +278,17 @@ def _run_install_hooks(global_config: bool = False, remove: bool = False):
264
278
 
265
279
  hooks = settings.setdefault("hooks", {})
266
280
 
281
+ def _has_kyp_hook(entry):
282
+ for hook in entry.get("hooks", []):
283
+ if "kyp-mem hook" in hook.get("command", ""):
284
+ return True
285
+ return "kyp-mem hook" in entry.get("command", "")
286
+
267
287
  if remove:
268
288
  changed = False
269
- for event in ("PostToolUse", "Stop"):
289
+ for event in ("PostToolUse", "UserPromptSubmit", "Stop"):
270
290
  if event in hooks:
271
- hooks[event] = [h for h in hooks[event] if "kyp-mem hook" not in h.get("command", "")]
291
+ hooks[event] = [h for h in hooks[event] if not _has_kyp_hook(h)]
272
292
  if not hooks[event]:
273
293
  del hooks[event]
274
294
  changed = True
@@ -282,20 +302,26 @@ def _run_install_hooks(global_config: bool = False, remove: bool = False):
282
302
  return
283
303
 
284
304
  post_tool_hooks = hooks.setdefault("PostToolUse", [])
305
+ prompt_hooks = hooks.setdefault("UserPromptSubmit", [])
285
306
  stop_hooks = hooks.setdefault("Stop", [])
286
307
 
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", "")]
308
+ post_tool_hooks = [h for h in post_tool_hooks if not _has_kyp_hook(h)]
309
+ prompt_hooks = [h for h in prompt_hooks if not _has_kyp_hook(h)]
310
+ stop_hooks = [h for h in stop_hooks if not _has_kyp_hook(h)]
289
311
 
290
312
  post_tool_hooks.append({
291
- "matcher": "Edit|Write|Bash",
313
+ "matcher": "Edit|Write|Read|Bash",
292
314
  "hooks": [{"type": "command", "command": f"{mcp_command} hook post-tool-use"}],
293
315
  })
316
+ prompt_hooks.append({
317
+ "hooks": [{"type": "command", "command": f"{mcp_command} hook user-prompt"}],
318
+ })
294
319
  stop_hooks.append({
295
320
  "hooks": [{"type": "command", "command": f"{mcp_command} hook stop"}],
296
321
  })
297
322
 
298
323
  hooks["PostToolUse"] = post_tool_hooks
324
+ hooks["UserPromptSubmit"] = prompt_hooks
299
325
  hooks["Stop"] = stop_hooks
300
326
 
301
327
  settings_path.write_text(json.dumps(settings, indent=2) + "\n")
package/kyp_mem/hooks.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """KYP-MEM session hooks — compile captured tool activity into vault notes."""
2
2
 
3
+ import os
3
4
  import sys
4
5
  import json
5
6
  from datetime import datetime
@@ -11,6 +12,259 @@ CURRENT_SESSION = SESSION_DIR / "current.jsonl"
11
12
  MIN_ACTIONS = 3
12
13
 
13
14
 
15
+ def handle_user_prompt():
16
+ raw = sys.stdin.read().strip()
17
+ if not raw:
18
+ return
19
+ try:
20
+ data = json.loads(raw)
21
+ except json.JSONDecodeError:
22
+ return
23
+
24
+ prompt = data.get("prompt", "").strip()
25
+ if not prompt:
26
+ return
27
+
28
+ entry = {
29
+ "ts": datetime.now().isoformat(),
30
+ "cwd": os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()),
31
+ "action": "prompt",
32
+ "prompt": prompt,
33
+ }
34
+
35
+ SESSION_DIR.mkdir(parents=True, exist_ok=True)
36
+ with open(CURRENT_SESSION, "a") as f:
37
+ f.write(json.dumps(entry) + "\n")
38
+
39
+
40
+ def handle_post_tool_use():
41
+ raw = sys.stdin.read().strip()
42
+ if not raw:
43
+ return
44
+ try:
45
+ data = json.loads(raw)
46
+ except json.JSONDecodeError:
47
+ return
48
+
49
+ tool_name = data.get("tool_name", "")
50
+ tool_input = data.get("tool_input", {})
51
+
52
+ entry = {"ts": datetime.now().isoformat()}
53
+
54
+ cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
55
+ entry["cwd"] = cwd
56
+
57
+ if tool_name == "Edit":
58
+ entry["action"] = "edit"
59
+ entry["file"] = tool_input.get("file_path", "")
60
+ elif tool_name == "Write":
61
+ entry["action"] = "create"
62
+ entry["file"] = tool_input.get("file_path", "")
63
+ elif tool_name == "Read":
64
+ entry["action"] = "read"
65
+ entry["file"] = tool_input.get("file_path", "")
66
+ elif tool_name == "Bash":
67
+ entry["action"] = "command"
68
+ entry["command"] = tool_input.get("command", "")
69
+ else:
70
+ return
71
+
72
+ SESSION_DIR.mkdir(parents=True, exist_ok=True)
73
+ with open(CURRENT_SESSION, "a") as f:
74
+ f.write(json.dumps(entry) + "\n")
75
+
76
+
77
+ def _relative_path(filepath, project_dir):
78
+ try:
79
+ return str(Path(filepath).relative_to(project_dir))
80
+ except (ValueError, TypeError):
81
+ return Path(filepath).name if filepath else ""
82
+
83
+
84
+ def _group_files_by_dir(filepaths, project_dir):
85
+ groups = {}
86
+ for fp in sorted(filepaths):
87
+ rel = _relative_path(fp, project_dir)
88
+ parent = str(Path(rel).parent)
89
+ if parent == ".":
90
+ parent = "(root)"
91
+ groups.setdefault(parent, []).append(Path(rel).name)
92
+ return groups
93
+
94
+
95
+ def _classify_command(cmd):
96
+ if not cmd.strip():
97
+ return "other", ""
98
+ first = cmd.strip().split()[0]
99
+ search_cmds = {'grep', 'rg', 'ag', 'ack'}
100
+ explore_cmds = {'find', 'fd', 'ls', 'tree', 'du'}
101
+ read_cmds = {'cat', 'head', 'tail', 'less', 'more', 'wc', 'file'}
102
+ diff_cmds = {'diff', 'git diff', 'git log', 'git status', 'git show'}
103
+ test_cmds = {'pytest', 'python -m pytest', 'npm test', 'jest', 'mocha', 'cargo test', 'go test'}
104
+ build_cmds = {'npm run', 'npm install', 'pip install', 'make', 'cargo build', 'go build'}
105
+ server_cmds = {'python3 -m', 'uvicorn', 'node', 'npm start', 'flask run'}
106
+ git_cmds = {'git commit', 'git push', 'git add', 'git checkout', 'git branch', 'git merge', 'git stash'}
107
+
108
+ if first in search_cmds or cmd.strip().startswith('git grep'):
109
+ return "search", cmd
110
+ if first in explore_cmds:
111
+ return "explore", cmd
112
+ if first in read_cmds:
113
+ return "read_cmd", cmd
114
+ for prefix in diff_cmds:
115
+ if cmd.strip().startswith(prefix):
116
+ return "git_inspect", cmd
117
+ for prefix in test_cmds:
118
+ if cmd.strip().startswith(prefix):
119
+ return "test", cmd
120
+ for prefix in build_cmds:
121
+ if cmd.strip().startswith(prefix):
122
+ return "build", cmd
123
+ for prefix in git_cmds:
124
+ if cmd.strip().startswith(prefix):
125
+ return "git_write", cmd
126
+ for prefix in server_cmds:
127
+ if cmd.strip().startswith(prefix):
128
+ return "run", cmd
129
+ if first == 'curl':
130
+ return "api_test", cmd
131
+ return "other", cmd
132
+
133
+
134
+ def _build_investigated(files_read, commands_classified, project_dir):
135
+ items = []
136
+ seen_files = set()
137
+
138
+ search_cmds = [cmd for cls, cmd in commands_classified if cls == "search"]
139
+ for cmd in search_cmds[:8]:
140
+ parts = cmd.strip().split()
141
+ if len(parts) >= 2:
142
+ pattern = None
143
+ for i, p in enumerate(parts):
144
+ if not p.startswith('-') and i > 0:
145
+ pattern = p.strip("'\"")
146
+ break
147
+ if pattern:
148
+ items.append(f"- Searched for `{pattern[:60]}`")
149
+ else:
150
+ items.append(f"- `{cmd[:100]}`")
151
+
152
+ read_groups = _group_files_by_dir(files_read, project_dir)
153
+ for dir_name, filenames in read_groups.items():
154
+ unique = [f for f in filenames if f not in seen_files]
155
+ seen_files.update(unique)
156
+ if not unique:
157
+ continue
158
+ if len(unique) <= 3:
159
+ items.append(f"- Read {', '.join(f'`{f}`' for f in unique)}")
160
+ else:
161
+ items.append(f"- Read {len(unique)} files in `{dir_name}/`")
162
+
163
+ explore_cmds = [cmd for cls, cmd in commands_classified if cls == "explore"]
164
+ if explore_cmds:
165
+ items.append(f"- Explored project structure ({len(explore_cmds)} commands)")
166
+
167
+ git_cmds = [cmd for cls, cmd in commands_classified if cls == "git_inspect"]
168
+ if git_cmds:
169
+ items.append(f"- Inspected git history/diff ({len(git_cmds)} commands)")
170
+
171
+ api_cmds = [cmd for cls, cmd in commands_classified if cls == "api_test"]
172
+ if api_cmds:
173
+ items.append(f"- Tested API endpoints ({len(api_cmds)} requests)")
174
+
175
+ return items
176
+
177
+
178
+ def _build_learned(files_read, files_edited, files_created, commands_classified, project_dir):
179
+ items = []
180
+ read_set = {Path(f).name for f in files_read}
181
+ edit_set = {Path(f).name for f in files_edited}
182
+ create_set = {Path(f).name for f in files_created}
183
+
184
+ investigated_then_modified = read_set & edit_set
185
+ if investigated_then_modified:
186
+ names = sorted(investigated_then_modified)[:5]
187
+ items.append(f"- Investigated and modified: {', '.join(f'`{n}`' for n in names)}")
188
+
189
+ config_files = {f for f in (files_edited | files_created)
190
+ if any(f.endswith(ext) for ext in
191
+ ('.json', '.toml', '.yaml', '.yml', '.ini', '.cfg', '.env', '.conf'))}
192
+ if config_files:
193
+ items.append(f"- Configuration changes: {', '.join(f'`{Path(f).name}`' for f in sorted(config_files)[:4])}")
194
+
195
+ test_cmds = [cmd for cls, cmd in commands_classified if cls == "test"]
196
+ if test_cmds:
197
+ items.append(f"- Ran tests ({len(test_cmds)} run{'s' if len(test_cmds) != 1 else ''})")
198
+
199
+ new_only = create_set - read_set
200
+ if new_only:
201
+ items.append(f"- Created new files: {', '.join(f'`{n}`' for n in sorted(new_only)[:5])}")
202
+
203
+ return items
204
+
205
+
206
+ def _build_completed(files_edited, files_created, commands_classified, project_dir):
207
+ items = []
208
+
209
+ edit_groups = _group_files_by_dir(files_edited, project_dir)
210
+ for dir_name, filenames in edit_groups.items():
211
+ if len(filenames) == 1:
212
+ items.append(f"- Modified `{filenames[0]}`")
213
+ else:
214
+ items.append(f"- Modified {len(filenames)} files in `{dir_name}/`: {', '.join(f'`{f}`' for f in filenames[:5])}")
215
+
216
+ create_groups = _group_files_by_dir(files_created, project_dir)
217
+ for dir_name, filenames in create_groups.items():
218
+ if len(filenames) == 1:
219
+ items.append(f"- Created `{filenames[0]}`")
220
+ else:
221
+ items.append(f"- Created {len(filenames)} files in `{dir_name}/`")
222
+
223
+ test_cmds = [cmd for cls, cmd in commands_classified if cls == "test"]
224
+ if test_cmds:
225
+ items.append(f"- Ran test suite")
226
+
227
+ git_writes = [cmd for cls, cmd in commands_classified if cls == "git_write"]
228
+ for cmd in git_writes:
229
+ if 'commit' in cmd:
230
+ items.append("- Committed changes to git")
231
+ break
232
+ for cmd in git_writes:
233
+ if 'push' in cmd:
234
+ items.append("- Pushed to remote")
235
+ break
236
+
237
+ build_cmds = [cmd for cls, cmd in commands_classified if cls == "build"]
238
+ if build_cmds:
239
+ items.append("- Ran build/install")
240
+
241
+ run_cmds = [cmd for cls, cmd in commands_classified if cls == "run"]
242
+ if run_cmds:
243
+ items.append("- Started/tested server")
244
+
245
+ return items
246
+
247
+
248
+ def _build_next_steps(files_edited, files_created, commands_classified):
249
+ items = []
250
+
251
+ git_writes = {cmd for cls, cmd in commands_classified if cls == "git_write"}
252
+ has_commit = any('commit' in cmd for cmd in git_writes)
253
+ has_push = any('push' in cmd for cmd in git_writes)
254
+
255
+ all_changed = files_edited | files_created
256
+ if all_changed and not has_commit:
257
+ items.append("- Commit pending changes")
258
+ if has_commit and not has_push:
259
+ items.append("- Push committed changes to remote")
260
+
261
+ test_cmds = [cmd for cls, cmd in commands_classified if cls == "test"]
262
+ if not test_cmds and all_changed:
263
+ items.append("- Run tests to verify changes")
264
+
265
+ return items
266
+
267
+
14
268
  def handle_stop():
15
269
  if not CURRENT_SESSION.exists():
16
270
  return
@@ -40,9 +294,11 @@ def handle_stop():
40
294
  project_name = Path(project_dir).name
41
295
  session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
42
296
 
297
+ files_read = set()
43
298
  files_edited = set()
44
299
  files_created = set()
45
300
  commands = []
301
+ prompts = []
46
302
  timeline = []
47
303
 
48
304
  for e in entries:
@@ -50,7 +306,14 @@ def handle_stop():
50
306
  ts = ts_raw[11:19] if len(ts_raw) >= 19 else ""
51
307
  action = e.get("action", "")
52
308
 
53
- if action == "edit":
309
+ if action == "prompt":
310
+ prompts.append({"ts": ts, "text": e.get("prompt", "")})
311
+ timeline.append(f" {ts} — Prompt: {e.get('prompt', '')[:60]}...")
312
+ elif action == "read":
313
+ fp = e.get("file", "")
314
+ files_read.add(fp)
315
+ timeline.append(f" {ts} — Read `{Path(fp).name}`")
316
+ elif action == "edit":
54
317
  fp = e.get("file", "")
55
318
  files_edited.add(fp)
56
319
  timeline.append(f" {ts} — Edit `{Path(fp).name}`")
@@ -64,6 +327,8 @@ def handle_stop():
64
327
  short = cmd[:80] + "..." if len(cmd) > 80 else cmd
65
328
  timeline.append(f" {ts} — `{short}`")
66
329
 
330
+ commands_classified = [_classify_command(cmd) for cmd in commands]
331
+
67
332
  summary_items = []
68
333
  if files_edited:
69
334
  summary_items.append(f"Modified {len(files_edited)} file{'s' if len(files_edited) != 1 else ''}")
@@ -72,12 +337,10 @@ def handle_stop():
72
337
  if commands:
73
338
  summary_items.append(f"Ran {len(commands)} command{'s' if len(commands) != 1 else ''}")
74
339
 
75
- investigate_keywords = {'grep', 'find', 'cat', 'head', 'tail', 'less', 'ls', 'tree', 'rg', 'ag', 'fd', 'wc', 'diff'}
76
- investigated_cmds = []
77
- for cmd in commands:
78
- first_word = cmd.strip().split()[0] if cmd.strip() else ''
79
- if first_word in investigate_keywords:
80
- investigated_cmds.append(cmd)
340
+ investigated = _build_investigated(files_read, commands_classified, project_dir)
341
+ learned = _build_learned(files_read, files_edited, files_created, commands_classified, project_dir)
342
+ completed = _build_completed(files_edited, files_created, commands_classified, project_dir)
343
+ next_steps = _build_next_steps(files_edited, files_created, commands_classified)
81
344
 
82
345
  parts = [f"# Session {session_id}", ""]
83
346
  parts.append(f"**Project:** `{project_dir}`")
@@ -88,28 +351,32 @@ def handle_stop():
88
351
  parts.append(", ".join(summary_items) + f" in `{project_name}`." if summary_items else "")
89
352
  parts.append("")
90
353
 
354
+ parts.append("## PROMPTS")
355
+ if prompts:
356
+ for i, p in enumerate(prompts, 1):
357
+ parts.append(f"### {i}. [{p['ts']}]")
358
+ parts.append(f"> {p['text']}")
359
+ parts.append("")
360
+ parts.append("")
361
+
91
362
  parts.append("## INVESTIGATED")
92
- if investigated_cmds:
93
- for cmd in investigated_cmds[:15]:
94
- short = cmd[:120] + "..." if len(cmd) > 120 else cmd
95
- parts.append(f"- `{short}`")
363
+ if investigated:
364
+ parts.extend(investigated)
96
365
  parts.append("")
97
366
 
98
367
  parts.append("## LEARNED")
99
- parts.append("")
368
+ if learned:
369
+ parts.extend(learned)
100
370
  parts.append("")
101
371
 
102
372
  parts.append("## COMPLETED")
103
- if files_edited:
104
- for f in sorted(files_edited):
105
- parts.append(f"- Modified `{Path(f).name}`")
106
- if files_created:
107
- for f in sorted(files_created):
108
- parts.append(f"- Created `{Path(f).name}`")
373
+ if completed:
374
+ parts.extend(completed)
109
375
  parts.append("")
110
376
 
111
377
  parts.append("## NEXT STEPS")
112
- parts.append("")
378
+ if next_steps:
379
+ parts.extend(next_steps)
113
380
  parts.append("")
114
381
 
115
382
  if timeline:
package/kyp_mem/server.py CHANGED
@@ -324,11 +324,20 @@ def kyp_project_context(project: str) -> str:
324
324
  """CALL THIS AT SESSION START. Returns the project's full context: Knowledge.md (ground truth), project notes, and recent session summaries. Use this to understand architecture, known bugs, past decisions, and what was done recently. This prevents hallucination and avoids repeating past work."""
325
325
  parts = []
326
326
 
327
+ MAX_KNOWLEDGE_CHARS = 3000
328
+ MAX_NOTE_PREVIEW_LINES = 6
329
+ MAX_SESSION_CHARS = 400
330
+
327
331
  knowledge_path = f"{project}/Knowledge.md"
328
332
  knowledge = vault.read(knowledge_path)
329
333
  if knowledge:
330
334
  parts.append("=== PROJECT KNOWLEDGE ===")
331
- parts.append(knowledge.content)
335
+ content = knowledge.content
336
+ if len(content) > MAX_KNOWLEDGE_CHARS:
337
+ parts.append(content[:MAX_KNOWLEDGE_CHARS])
338
+ parts.append(f"\n... (truncated — use kyp_read(\"{knowledge_path}\", full=True) for complete content)")
339
+ else:
340
+ parts.append(content)
332
341
  parts.append("")
333
342
 
334
343
  project_notes = []
@@ -341,9 +350,9 @@ def kyp_project_context(project: str) -> str:
341
350
  for path, note in sorted(project_notes):
342
351
  parts.append(f"\n--- {note.title} ({path}) ---")
343
352
  preview = note.content.strip().split("\n")
344
- parts.append("\n".join(preview[:10]))
345
- if len(preview) > 10:
346
- parts.append("...")
353
+ parts.append("\n".join(preview[:MAX_NOTE_PREVIEW_LINES]))
354
+ if len(preview) > MAX_NOTE_PREVIEW_LINES:
355
+ parts.append(f"... (use kyp_read(\"{path}\", full=True) for complete content)")
347
356
  parts.append("")
348
357
 
349
358
  sessions = []
@@ -360,9 +369,11 @@ def kyp_project_context(project: str) -> str:
360
369
  content = note.content
361
370
  timeline_idx = content.find("## Timeline")
362
371
  if timeline_idx > 0:
363
- parts.append(content[:timeline_idx].strip())
372
+ content = content[:timeline_idx].strip()
373
+ if len(content) > MAX_SESSION_CHARS:
374
+ parts.append(content[:MAX_SESSION_CHARS] + "...")
364
375
  else:
365
- parts.append(content[:500])
376
+ parts.append(content)
366
377
  parts.append("")
367
378
 
368
379
  if not parts:
@@ -1573,29 +1573,52 @@ body.resizing .resize-handle { pointer-events: auto !important; }
1573
1573
  color: var(--text-secondary);
1574
1574
  }
1575
1575
 
1576
- /* ============ BACKLINK CONTEXT ============ */
1577
- .rp-item .rp-context {
1578
- font-size: 10px;
1579
- color: var(--text-muted);
1580
- margin-top: 2px;
1581
- line-height: 1.4;
1582
- overflow: hidden;
1583
- text-overflow: ellipsis;
1584
- white-space: nowrap;
1585
- }
1586
-
1587
- .rp-item-col {
1576
+ /* ============ BACKLINK & UNLINKED ITEMS ============ */
1577
+ .rp-link-item {
1588
1578
  display: flex;
1589
- flex-direction: column;
1590
- padding: 5px 8px;
1579
+ align-items: center;
1580
+ padding: 4px 8px;
1591
1581
  border-radius: var(--radius-sm);
1592
1582
  cursor: pointer;
1593
1583
  margin-bottom: 1px;
1594
1584
  transition: background 0.1s;
1585
+ gap: 6px;
1586
+ }
1587
+
1588
+ .rp-link-item:hover { background: var(--bg-hover); }
1589
+
1590
+ .rp-link-item .rp-link-icon {
1591
+ font-size: 9px;
1592
+ flex-shrink: 0;
1593
+ opacity: 0.5;
1594
+ }
1595
+
1596
+ .rp-link-item .rp-link-icon.backlink { color: var(--neon-cyan); }
1597
+ .rp-link-item .rp-link-icon.unlinked { color: var(--text-muted); }
1598
+
1599
+ .rp-link-item .rp-link-title {
1600
+ font-size: 12px;
1601
+ color: var(--neon-cyan);
1602
+ font-weight: 500;
1595
1603
  overflow: hidden;
1604
+ text-overflow: ellipsis;
1605
+ white-space: nowrap;
1606
+ transition: all 0.15s;
1596
1607
  }
1597
1608
 
1598
- .rp-item-col:hover { background: var(--bg-hover); }
1609
+ .rp-link-item:hover .rp-link-title {
1610
+ text-decoration: underline;
1611
+ text-underline-offset: 2px;
1612
+ }
1613
+
1614
+ .rp-link-item.unlinked .rp-link-title {
1615
+ color: var(--text-secondary);
1616
+ }
1617
+
1618
+ .rp-link-item.unlinked:hover .rp-link-title {
1619
+ color: var(--neon-cyan);
1620
+ text-decoration: underline;
1621
+ }
1599
1622
 
1600
1623
  /* ============ TRANSITIONS ============ */
1601
1624
  .fade-in { animation: fadeIn 0.15s ease; }
@@ -2188,10 +2211,8 @@ function renderRightPanel(note) {
2188
2211
  document.getElementById('bl-count').textContent = backlinks.length;
2189
2212
  backlinks.forEach(bl => {
2190
2213
  const item = document.createElement('div');
2191
- item.className = 'rp-item-col';
2192
- let html = `<span class="rp-title rp-backlink-title" style="font-size:11px">${bl.title || bl.path || bl}</span>`;
2193
- if (bl.context) html += `<span class="rp-context">${bl.context}</span>`;
2194
- item.innerHTML = html;
2214
+ item.className = 'rp-link-item';
2215
+ item.innerHTML = `<span class="rp-link-icon backlink">&#8592;</span><span class="rp-link-title">${bl.title || bl.path || bl}</span>`;
2195
2216
  item.addEventListener('click', () => loadNote(bl.path || bl));
2196
2217
  blList.appendChild(item);
2197
2218
  });
@@ -2244,10 +2265,8 @@ function renderRightPanel(note) {
2244
2265
  document.getElementById('unlinked-count').textContent = unlinked.length;
2245
2266
  unlinked.forEach(u => {
2246
2267
  const item = document.createElement('div');
2247
- item.className = 'rp-item-col';
2248
- let html = `<span class="rp-title" style="font-size:11px;color:var(--text-secondary)">${u.title}</span>`;
2249
- if (u.context) html += `<span class="rp-context">${u.context}</span>`;
2250
- item.innerHTML = html;
2268
+ item.className = 'rp-link-item unlinked';
2269
+ item.innerHTML = `<span class="rp-link-icon unlinked">&#8776;</span><span class="rp-link-title">${u.title}</span>`;
2251
2270
  item.addEventListener('click', () => loadNote(u.path));
2252
2271
  ulList.appendChild(item);
2253
2272
  });
@@ -2905,6 +2924,23 @@ async function init() {
2905
2924
  `;
2906
2925
  }
2907
2926
 
2927
+ // --- Auto-refresh: poll for vault changes ---
2928
+ let lastKnownStats = null;
2929
+
2930
+ async function pollForChanges() {
2931
+ try {
2932
+ const stats = await fetchJSON('/api/stats');
2933
+ const key = `${stats.notes}:${stats.links}:${stats.tags}`;
2934
+ if (lastKnownStats && lastKnownStats !== key) {
2935
+ await refreshTree();
2936
+ if (currentPath) loadNote(currentPath);
2937
+ }
2938
+ lastKnownStats = key;
2939
+ } catch {}
2940
+ }
2941
+
2942
+ setInterval(pollForChanges, 3000);
2943
+
2908
2944
  initResize();
2909
2945
  initGraphResize();
2910
2946
  init();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kyp-mem",
3
- "version": "0.4.1",
4
- "description": "Know Your Project — Persistent knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI.",
3
+ "version": "0.4.3",
4
+ "description": "Know Your Project — Persistent & Session level knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI.",
5
5
  "bin": {
6
6
  "kyp-mem": "bin/cli.mjs"
7
7
  },
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kyp-mem"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Know Your Project — Persistent knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}