kyp-mem 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
1
  """KYP-MEM — Know Your Project Memory. Headless knowledge base for AI agents."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.1"
package/kyp_mem/hooks.py CHANGED
@@ -64,29 +64,53 @@ def handle_stop():
64
64
  short = cmd[:80] + "..." if len(cmd) > 80 else cmd
65
65
  timeline.append(f" {ts} — `{short}`")
66
66
 
67
+ summary_items = []
68
+ if files_edited:
69
+ summary_items.append(f"Modified {len(files_edited)} file{'s' if len(files_edited) != 1 else ''}")
70
+ if files_created:
71
+ summary_items.append(f"Created {len(files_created)} file{'s' if len(files_created) != 1 else ''}")
72
+ if commands:
73
+ summary_items.append(f"Ran {len(commands)} command{'s' if len(commands) != 1 else ''}")
74
+
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)
81
+
67
82
  parts = [f"# Session {session_id}", ""]
68
83
  parts.append(f"**Project:** `{project_dir}`")
69
84
  parts.append(f"**Actions:** {len(entries)} total, {len(write_actions)} substantive")
70
85
  parts.append("")
71
86
 
87
+ parts.append("## Summary")
88
+ parts.append(", ".join(summary_items) + f" in `{project_name}`." if summary_items else "")
89
+ parts.append("")
90
+
91
+ 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}`")
96
+ parts.append("")
97
+
98
+ parts.append("## LEARNED")
99
+ parts.append("")
100
+ parts.append("")
101
+
102
+ parts.append("## COMPLETED")
72
103
  if files_edited:
73
- parts.append("## Files Modified")
74
104
  for f in sorted(files_edited):
75
- parts.append(f"- `{f}`")
76
- parts.append("")
77
-
105
+ parts.append(f"- Modified `{Path(f).name}`")
78
106
  if files_created:
79
- parts.append("## Files Created")
80
107
  for f in sorted(files_created):
81
- parts.append(f"- `{f}`")
82
- parts.append("")
108
+ parts.append(f"- Created `{Path(f).name}`")
109
+ parts.append("")
83
110
 
84
- if commands:
85
- parts.append("## Commands Run")
86
- for cmd in commands[:25]:
87
- short = cmd[:120] + "..." if len(cmd) > 120 else cmd
88
- parts.append(f"- `{short}`")
89
- parts.append("")
111
+ parts.append("## NEXT STEPS")
112
+ parts.append("")
113
+ parts.append("")
90
114
 
91
115
  if timeline:
92
116
  parts.append("## Timeline")
@@ -102,7 +126,7 @@ def handle_stop():
102
126
  from .vault import Vault
103
127
 
104
128
  vault = Vault(get_vault_path())
105
- vault.write_note(f"Sessions/{session_id}.md", content, tags, {})
129
+ vault.write_note(f"{project_name}/Sessions/{session_id}.md", content, tags, {})
106
130
 
107
131
  CURRENT_SESSION.unlink(missing_ok=True)
108
132
 
package/kyp_mem/server.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """KYP-MEM MCP server — headless knowledge base for AI agents."""
2
2
 
3
3
  import json
4
+ from datetime import datetime
4
5
  from mcp.server.fastmcp import FastMCP
5
6
  from .config import get_vault_path
6
7
  from .vault import Vault
@@ -10,6 +11,61 @@ vault = Vault(get_vault_path())
10
11
  mcp = FastMCP("kyp-mem")
11
12
 
12
13
 
14
+ @mcp.tool()
15
+ def ____kyp_instructions() -> str:
16
+ """## KYP-MEM — Know Your Project Memory
17
+
18
+ YOU MUST FOLLOW THESE INSTRUCTIONS when kyp-mem tools are available.
19
+
20
+ ### AT SESSION START (MANDATORY)
21
+ 1. Identify the project you are working on from the user's working directory or question.
22
+ 2. Call `kyp_project_context(project)` to load the project's knowledge base, notes, and recent session summaries.
23
+ 3. If no project exists yet, call `kyp_project_context` anyway — if it returns empty, ask the user if you should create one.
24
+ 4. Use the returned context to ground yourself: understand architecture, known bugs, past decisions, and what was done in recent sessions. Do NOT ask the user questions that are already answered in the project context.
25
+
26
+ ### DURING WORK — WHEN TO SEARCH SESSIONS
27
+ Call `kyp_session_search(query)` when:
28
+ - You encounter a bug or error — search for it to see if it was investigated before.
29
+ - You are about to make an architectural decision — check if a prior session already made this decision.
30
+ - The user asks "did we already...", "what happened with...", "last time we..." — search sessions semantically.
31
+ - You are unsure about project-specific behavior — sessions contain investigation logs.
32
+
33
+ ### DURING WORK — WHEN TO UPDATE KNOWLEDGE
34
+ Call `kyp_write` to update the project's Knowledge.md when you:
35
+ - Fix a bug → add it under ## Bugs > ### Fixed
36
+ - Discover a new bug → add it under ## Bugs > ### Known
37
+ - Make an architectural decision → add it under ## Key Decisions
38
+ - Learn something important about the project → add it under ## Notes
39
+ - Complete an improvement → move it from ## Improvements > ### Planned to ### Completed
40
+
41
+ Also use `kyp_write` to create new project notes for substantial topics (API docs, setup guides, component deep-dives). Use [[wikilinks]] to connect notes.
42
+
43
+ ### DURING WORK — WHEN TO USE OTHER TOOLS
44
+ - `kyp_search(query)` — keyword search across ALL notes (not just sessions). Use for finding specific content.
45
+ - `kyp_read(path)` — read a specific note. Default is brief mode; use full=True for complete content.
46
+ - `kyp_related(path)` — find notes connected by backlinks, tags, or folder proximity.
47
+ - `kyp_tags(tag)` — browse by tag or list all tags.
48
+
49
+ ### SESSION CAPTURE
50
+ Sessions are captured automatically by hooks — you do not need to create session notes manually. If the user explicitly asks to log a session, use `kyp_session_create`.
51
+
52
+ ### IMPORTANT RULES
53
+ - NEVER hallucinate project details. If it's not in the knowledge base or sessions, say you don't know.
54
+ - ALWAYS check knowledge base before making assumptions about project architecture.
55
+ - Keep Knowledge.md updated as you work — it is the source of truth for future sessions.
56
+ - Use [[wikilinks]] in notes to build the knowledge graph.
57
+ - Tag notes consistently: use project name, topic tags, and type tags (bug, decision, guide, etc.).
58
+
59
+ Call this tool to acknowledge these instructions. It returns a confirmation."""
60
+ projects = []
61
+ for path in vault.index.notes:
62
+ parts = path.split("/")
63
+ if len(parts) > 1:
64
+ projects.append(parts[0])
65
+ unique = sorted(set(projects))
66
+ return f"KYP-MEM active. Available projects: {', '.join(unique) if unique else '(none)'}. Call kyp_project_context(project) to load context."
67
+
68
+
13
69
  @mcp.tool()
14
70
  def kyp_list(path: str = "") -> str:
15
71
  """List notes and folders in the vault. Shows inline tags for quick navigation. Pass a folder path or empty for root."""
@@ -95,7 +151,7 @@ def kyp_read(path: str, full: bool = False) -> str:
95
151
 
96
152
  @mcp.tool()
97
153
  def kyp_write(path: str, content: str, tags: str = "", properties: str = "") -> str:
98
- """Create or update a note. Path like 'Project/Note.md'. Tags: comma-separated. Properties: JSON string."""
154
+ """Create or update a note. Use this to persist knowledge: bug fixes, architectural decisions, setup guides, API contracts. Path like 'Project/Note.md'. Use [[wikilinks]] in content to connect notes. Tags: comma-separated. Properties: JSON string."""
99
155
  if not path.endswith(".md"):
100
156
  path += ".md"
101
157
 
@@ -191,3 +247,125 @@ def kyp_stats() -> str:
191
247
  f" Links: {s['links']}\n"
192
248
  f" Backlinks: {s['backlinks']}"
193
249
  )
250
+
251
+
252
+ @mcp.tool()
253
+ def kyp_session_search(query: str, project: str = None) -> str:
254
+ """Semantic search across past session logs using vector embeddings. Use this to recall: what was investigated, bugs encountered, decisions made, and next steps from prior sessions. Search before re-investigating known issues or making decisions that may have been made before."""
255
+ from .vector import get_session_memory
256
+ results = get_session_memory().search_sessions(query, project=project, n_results=5)
257
+
258
+ if not results or not results.get("ids") or not results["ids"][0]:
259
+ return "No relevant past sessions found."
260
+
261
+ lines = ["Semantic Session Search Results:", ""]
262
+ for i, path in enumerate(results["ids"][0]):
263
+ doc = results["documents"][0][i]
264
+ score = results["distances"][0][i]
265
+ lines.append(f"--- Session: {path} (Distance: {score:.2f}) ---")
266
+ lines.append(doc)
267
+ lines.append("")
268
+ return "\n".join(lines)
269
+
270
+
271
+ @mcp.tool()
272
+ def kyp_session_create(project: str, summary: str = "", investigated: str = "", learned: str = "", completed: str = "", next_steps: str = "") -> str:
273
+ """Create a structured session note. Project is required. Sections accept markdown text."""
274
+ session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
275
+ parts = [f"# Session {session_id}", ""]
276
+ parts.append(f"**Project:** {project}")
277
+ parts.append("")
278
+ parts.append("## Summary")
279
+ parts.append(summary or "")
280
+ parts.append("")
281
+ parts.append("## INVESTIGATED")
282
+ parts.append(investigated or "")
283
+ parts.append("")
284
+ parts.append("## LEARNED")
285
+ parts.append(learned or "")
286
+ parts.append("")
287
+ parts.append("## COMPLETED")
288
+ parts.append(completed or "")
289
+ parts.append("")
290
+ parts.append("## NEXT STEPS")
291
+ parts.append(next_steps or "")
292
+
293
+ content = "\n".join(parts)
294
+ tags = ["session", "manual", project.lower().replace(" ", "-")]
295
+ path = f"{project}/Sessions/{session_id}.md"
296
+ vault.write_note(path, content, tags, {})
297
+ return f"Created session: {path}"
298
+
299
+
300
+ @mcp.tool()
301
+ def kyp_sessions(project: str = "", limit: int = 10) -> str:
302
+ """List sessions, optionally filtered by project. Shows most recent first."""
303
+ sessions = []
304
+ for path, note in vault.index.notes.items():
305
+ if "/Sessions/" not in path and not path.startswith("Sessions/"):
306
+ continue
307
+ if project and not path.lower().startswith(project.lower() + "/"):
308
+ continue
309
+ sessions.append((path, note))
310
+ sessions.sort(key=lambda s: s[0], reverse=True)
311
+ sessions = sessions[:limit]
312
+ if not sessions:
313
+ return "No sessions found." + (f" (project filter: {project})" if project else "")
314
+ lines = ["Sessions:", ""]
315
+ for path, note in sessions:
316
+ tags = f" [{', '.join(note.tags)}]" if note.tags else ""
317
+ date = note.created or note.updated or ""
318
+ lines.append(f" {date} — {note.title} ({path}){tags}")
319
+ return "\n".join(lines)
320
+
321
+
322
+ @mcp.tool()
323
+ def kyp_project_context(project: str) -> str:
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
+ parts = []
326
+
327
+ knowledge_path = f"{project}/Knowledge.md"
328
+ knowledge = vault.read(knowledge_path)
329
+ if knowledge:
330
+ parts.append("=== PROJECT KNOWLEDGE ===")
331
+ parts.append(knowledge.content)
332
+ parts.append("")
333
+
334
+ project_notes = []
335
+ for path, note in vault.index.notes.items():
336
+ if path.startswith(f"{project}/") and "/Sessions/" not in path and path != knowledge_path:
337
+ project_notes.append((path, note))
338
+
339
+ if project_notes:
340
+ parts.append("=== PROJECT NOTES ===")
341
+ for path, note in sorted(project_notes):
342
+ parts.append(f"\n--- {note.title} ({path}) ---")
343
+ preview = note.content.strip().split("\n")
344
+ parts.append("\n".join(preview[:10]))
345
+ if len(preview) > 10:
346
+ parts.append("...")
347
+ parts.append("")
348
+
349
+ sessions = []
350
+ for path, note in vault.index.notes.items():
351
+ if path.startswith(f"{project}/Sessions/"):
352
+ sessions.append((path, note))
353
+ sessions.sort(key=lambda s: s[0], reverse=True)
354
+ recent = sessions[:5]
355
+
356
+ if recent:
357
+ parts.append(f"=== RECENT SESSIONS ({len(recent)} of {len(sessions)}) ===")
358
+ for path, note in recent:
359
+ parts.append(f"\n--- {note.title} ---")
360
+ content = note.content
361
+ timeline_idx = content.find("## Timeline")
362
+ if timeline_idx > 0:
363
+ parts.append(content[:timeline_idx].strip())
364
+ else:
365
+ parts.append(content[:500])
366
+ parts.append("")
367
+
368
+ if not parts:
369
+ return f"No context found for project '{project}'. Create a Knowledge.md to get started."
370
+
371
+ return "\n".join(parts)