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.
@@ -0,0 +1,126 @@
1
+ """KYP-MEM session hooks — compile captured tool activity into vault notes."""
2
+
3
+ import sys
4
+ import json
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ SESSION_DIR = Path.home() / ".kyp-mem" / "sessions"
9
+ CURRENT_SESSION = SESSION_DIR / "current.jsonl"
10
+
11
+ MIN_ACTIONS = 3
12
+
13
+
14
+ def handle_stop():
15
+ if not CURRENT_SESSION.exists():
16
+ return
17
+
18
+ text = CURRENT_SESSION.read_text().strip()
19
+ if not text:
20
+ CURRENT_SESSION.unlink(missing_ok=True)
21
+ return
22
+
23
+ entries = []
24
+ for line in text.split("\n"):
25
+ try:
26
+ entries.append(json.loads(line))
27
+ except json.JSONDecodeError:
28
+ continue
29
+
30
+ if not entries:
31
+ CURRENT_SESSION.unlink(missing_ok=True)
32
+ return
33
+
34
+ write_actions = [e for e in entries if e.get("action") in ("edit", "create", "command")]
35
+ if len(write_actions) < MIN_ACTIONS:
36
+ CURRENT_SESSION.unlink(missing_ok=True)
37
+ return
38
+
39
+ project_dir = entries[0].get("cwd", "unknown")
40
+ project_name = Path(project_dir).name
41
+ session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
42
+
43
+ files_edited = set()
44
+ files_created = set()
45
+ commands = []
46
+ timeline = []
47
+
48
+ for e in entries:
49
+ ts_raw = e.get("ts", "")
50
+ ts = ts_raw[11:19] if len(ts_raw) >= 19 else ""
51
+ action = e.get("action", "")
52
+
53
+ if action == "edit":
54
+ fp = e.get("file", "")
55
+ files_edited.add(fp)
56
+ timeline.append(f" {ts} — Edit `{Path(fp).name}`")
57
+ elif action == "create":
58
+ fp = e.get("file", "")
59
+ files_created.add(fp)
60
+ timeline.append(f" {ts} — Write `{Path(fp).name}`")
61
+ elif action == "command":
62
+ cmd = e.get("command", "")
63
+ commands.append(cmd)
64
+ short = cmd[:80] + "..." if len(cmd) > 80 else cmd
65
+ timeline.append(f" {ts} — `{short}`")
66
+
67
+ parts = [f"# Session {session_id}", ""]
68
+ parts.append(f"**Project:** `{project_dir}`")
69
+ parts.append(f"**Actions:** {len(entries)} total, {len(write_actions)} substantive")
70
+ parts.append("")
71
+
72
+ if files_edited:
73
+ parts.append("## Files Modified")
74
+ for f in sorted(files_edited):
75
+ parts.append(f"- `{f}`")
76
+ parts.append("")
77
+
78
+ if files_created:
79
+ parts.append("## Files Created")
80
+ for f in sorted(files_created):
81
+ parts.append(f"- `{f}`")
82
+ parts.append("")
83
+
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("")
90
+
91
+ if timeline:
92
+ parts.append("## Timeline")
93
+ for line in timeline[:40]:
94
+ parts.append(line)
95
+ if len(timeline) > 40:
96
+ parts.append(f" ... and {len(timeline) - 40} more actions")
97
+
98
+ content = "\n".join(parts)
99
+ tags = ["session", "auto-captured", project_name]
100
+
101
+ from .config import get_vault_path
102
+ from .vault import Vault
103
+
104
+ vault = Vault(get_vault_path())
105
+ vault.write_note(f"Sessions/{session_id}.md", content, tags, {})
106
+
107
+ CURRENT_SESSION.unlink(missing_ok=True)
108
+
109
+
110
+ def main():
111
+ if len(sys.argv) > 1 and sys.argv[1] == "stop":
112
+ handle_stop()
113
+ else:
114
+ raw = sys.stdin.read().strip()
115
+ if not raw:
116
+ return
117
+ try:
118
+ data = json.loads(raw)
119
+ except json.JSONDecodeError:
120
+ return
121
+ if "stop_reason" in data:
122
+ handle_stop()
123
+
124
+
125
+ if __name__ == "__main__":
126
+ main()
package/kyp_mem/server.py CHANGED
@@ -7,18 +7,23 @@ from .vault import Vault
7
7
 
8
8
  vault = Vault(get_vault_path())
9
9
 
10
- mcp = FastMCP("kyp-mem", description="Know Your Project — headless knowledge base for AI agents")
10
+ mcp = FastMCP("kyp-mem")
11
11
 
12
12
 
13
13
  @mcp.tool()
14
14
  def kyp_list(path: str = "") -> str:
15
- """List notes and folders in the vault. Pass a folder path to list its contents, or empty for root."""
15
+ """List notes and folders in the vault. Shows inline tags for quick navigation. Pass a folder path or empty for root."""
16
16
  tree = vault.list_tree(path)
17
17
  lines = []
18
18
  for f in tree["folders"]:
19
19
  lines.append(f" {f}/")
20
20
  for n in tree["notes"]:
21
- lines.append(f" {n}")
21
+ rel = f"{path}/{n}" if path else n
22
+ note = vault.index.notes.get(rel)
23
+ if note and note.tags:
24
+ lines.append(f" {n} [{', '.join(note.tags)}]")
25
+ else:
26
+ lines.append(f" {n}")
22
27
  if not lines:
23
28
  lines.append("(empty vault)")
24
29
  header = f"Vault: {path or '/'}"
@@ -26,24 +31,46 @@ def kyp_list(path: str = "") -> str:
26
31
 
27
32
 
28
33
  @mcp.tool()
29
- def kyp_read(path: str) -> str:
30
- """Read a note by path (e.g. 'Hedge Engine/Configuration.md'). Returns content + properties + backlinks + related notes."""
34
+ def kyp_read(path: str, full: bool = False) -> str:
35
+ """Read a note. Returns brief summary by default (title, tags, preview, links). Set full=True for complete content."""
31
36
  note = vault.read(path)
32
37
  if not note:
33
38
  return f"Not found: {path}"
34
39
 
40
+ if not full:
41
+ parts = [f"# {note.title}"]
42
+ if note.tags:
43
+ parts.append(f"tags: {', '.join(note.tags)}")
44
+ if note.created:
45
+ parts.append(f"created: {note.created}")
46
+
47
+ lines = [l for l in note.content.strip().split("\n") if l.strip() and not l.startswith("# ")]
48
+ preview = "\n".join(lines[:6])
49
+ if len(lines) > 6:
50
+ preview += "\n..."
51
+ parts.append("")
52
+ parts.append(preview)
53
+
54
+ backlinks = vault.get_backlinks(path)
55
+ outlinks = note.links
56
+ if outlinks:
57
+ parts.append(f"\nlinks: {', '.join(f'[[{l}]]' for l in outlinks)}")
58
+ if backlinks:
59
+ parts.append(f"backlinks: {', '.join(f'[[{b.replace('.md', '')}]]' for b in backlinks)}")
60
+
61
+ return "\n".join(parts)
62
+
35
63
  parts = [f"# {note.title}", ""]
36
64
 
37
65
  if note.tags or note.properties or note.created:
38
- parts.append("**Properties:**")
39
66
  if note.tags:
40
- parts.append(f" tags: {', '.join(note.tags)}")
67
+ parts.append(f"tags: {', '.join(note.tags)}")
41
68
  if note.created:
42
- parts.append(f" created: {note.created}")
69
+ parts.append(f"created: {note.created}")
43
70
  if note.updated:
44
- parts.append(f" updated: {note.updated}")
71
+ parts.append(f"updated: {note.updated}")
45
72
  for k, v in note.properties.items():
46
- parts.append(f" {k}: {v}")
73
+ parts.append(f"{k}: {v}")
47
74
  parts.append("")
48
75
 
49
76
  parts.append(note.content)