kyp-mem 0.6.1 → 0.6.4

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/kyp_mem/hooks.py CHANGED
@@ -3,6 +3,7 @@
3
3
  import os
4
4
  import sys
5
5
  import json
6
+ import subprocess
6
7
  from datetime import datetime
7
8
  from pathlib import Path
8
9
 
@@ -73,9 +74,51 @@ def _record_injection(project, chars):
73
74
  _save_token_stats(stats)
74
75
 
75
76
 
77
+ def _is_subprocess():
78
+ return os.environ.get("KYP_MEM_SUMMARIZING") == "1"
79
+
80
+
81
+ def _extract_session_summary(content, max_chars=800):
82
+ import re
83
+ parts = []
84
+ for heading in ("Summary", "LEARNED", "COMPLETED"):
85
+ m = re.search(rf"(?:^|\n)##\s+{re.escape(heading)}\s*\n(.*?)(?=\n##\s|\Z)", content, re.DOTALL)
86
+ if m:
87
+ text = m.group(1).strip()
88
+ if heading == "Summary":
89
+ parts.append(text)
90
+ else:
91
+ parts.append(f"**{heading.title()}:** {text[:250]}")
92
+ return "\n".join(parts)[:max_chars] if parts else content[:200]
93
+
94
+
95
+ def _build_stats_line(project_name, injected_chars, session_ids):
96
+ try:
97
+ stats = _load_token_stats()
98
+ project_sessions = [s for s in stats.get("sessions", []) if s.get("project") == project_name]
99
+ if not project_sessions:
100
+ return None
101
+ matched = [s for s in project_sessions if s.get("id") in session_ids]
102
+ exploration_tokens = sum(s.get("exploration_tokens", 0) for s in matched)
103
+ injected_tokens = injected_chars // CHARS_PER_TOKEN
104
+ if exploration_tokens == 0:
105
+ return None
106
+ saved = exploration_tokens - injected_tokens
107
+ if saved <= 0:
108
+ return None
109
+ return (
110
+ f"---\n"
111
+ f"*kyp-mem saved ~{saved:,} tokens this session (injected ~{injected_tokens:,} tokens instead of re-exploring ~{exploration_tokens:,})*"
112
+ )
113
+ except Exception:
114
+ return None
115
+
116
+
76
117
  def handle_session_start():
77
118
  """Inject recent session memory into the conversation at session start."""
78
119
  sys.stdin.read()
120
+ if _is_subprocess():
121
+ return
79
122
 
80
123
  cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
81
124
  project_name = Path(cwd).name
@@ -98,8 +141,7 @@ def handle_session_start():
98
141
  return
99
142
 
100
143
  parts = [f"# [kyp-mem] {project_name} — Recent Sessions"]
101
- parts.append(f"Use `kyp_search` or `kyp_project_context` for architecture/project knowledge on demand.")
102
- parts.append("")
144
+ parts.append(f"Use `kyp_search` or `kyp_project_context` for architecture/project knowledge on demand.\n")
103
145
 
104
146
  parts.append(f"## Last {len(sessions)} Sessions")
105
147
  for sp in sessions:
@@ -107,16 +149,16 @@ def handle_session_start():
107
149
  if not note:
108
150
  continue
109
151
  parts.append(f"### {note.title}")
110
- content = note.content
111
- timeline_idx = content.find("## TIMELINE")
112
- if timeline_idx < 0:
113
- timeline_idx = content.find("## Timeline")
114
- if timeline_idx > 0:
115
- content = content[:timeline_idx].strip()
116
- parts.append(content)
152
+ summary = _extract_session_summary(note.content)
153
+ parts.append(summary)
117
154
  parts.append("")
118
155
 
156
+ session_ids = {Path(sp).stem for sp in sessions}
157
+ stats_line = _build_stats_line(project_name, len("\n".join(parts)), session_ids)
158
+ if stats_line:
159
+ parts.append(stats_line)
119
160
  output = "\n".join(parts)
161
+
120
162
  try:
121
163
  _record_injection(project_name, len(output))
122
164
  except Exception:
@@ -128,7 +170,7 @@ def handle_session_start():
128
170
 
129
171
  def handle_user_prompt():
130
172
  raw = sys.stdin.read().strip()
131
- if not raw:
173
+ if not raw or _is_subprocess():
132
174
  return
133
175
  try:
134
176
  data = json.loads(raw)
@@ -153,7 +195,7 @@ def handle_user_prompt():
153
195
 
154
196
  def handle_post_tool_use():
155
197
  raw = sys.stdin.read().strip()
156
- if not raw:
198
+ if not raw or _is_subprocess():
157
199
  return
158
200
  try:
159
201
  data = json.loads(raw)
@@ -454,9 +496,11 @@ Return ONLY this format (no preamble):
454
496
  Raw session data:
455
497
  {raw_note}"""
456
498
 
499
+ env = os.environ.copy()
500
+ env["KYP_MEM_SUMMARIZING"] = "1"
457
501
  result = subprocess.run(
458
502
  [claude_bin, "-p", prompt, "--max-turns", "1", "--model", model],
459
- capture_output=True, text=True, timeout=120,
503
+ capture_output=True, text=True, timeout=120, env=env,
460
504
  )
461
505
  if result.returncode == 0 and result.stdout.strip():
462
506
  return result.stdout.strip()
@@ -466,7 +510,7 @@ Raw session data:
466
510
 
467
511
 
468
512
  def handle_stop():
469
- if not CURRENT_SESSION.exists():
513
+ if _is_subprocess() or not CURRENT_SESSION.exists():
470
514
  return
471
515
 
472
516
  text = CURRENT_SESSION.read_text().strip()
@@ -642,6 +686,10 @@ def handle_stop():
642
686
  except Exception:
643
687
  pass
644
688
 
689
+ # Delete session file BEFORE summarization so the spawned claude subprocess
690
+ # doesn't pollute it via hooks writing back into current.jsonl
691
+ CURRENT_SESSION.unlink(missing_ok=True)
692
+
645
693
  # Try Claude summarization, fall back to raw sections
646
694
  summarized = _summarize_with_claude(raw_note, project_name)
647
695
 
@@ -710,8 +758,6 @@ def handle_stop():
710
758
  vault = Vault(get_vault_path())
711
759
  vault.write_note(f"{project_name}/Sessions/{session_id}.md", content, tags, {})
712
760
 
713
- CURRENT_SESSION.unlink(missing_ok=True)
714
-
715
761
 
716
762
  def main():
717
763
  if len(sys.argv) > 1 and sys.argv[1] == "stop":
@@ -1042,6 +1042,18 @@ function openSession(path) {
1042
1042
  loadNote(path);
1043
1043
  }
1044
1044
 
1045
+ async function deleteSession(path) {
1046
+ if (!confirm('Delete this session?')) return;
1047
+ const res = await fetch(`/api/note/${path}`, { method: 'DELETE' });
1048
+ const data = await res.json();
1049
+ if (data.ok) {
1050
+ activeSession = null;
1051
+ currentPath = null;
1052
+ $('content-area').innerHTML = '<div class="dim" style="padding:40px;text-align:center">Session deleted</div>';
1053
+ refreshAll();
1054
+ }
1055
+ }
1056
+
1045
1057
  // ─── Load Note ───────────────────────────────────────────────────────────────
1046
1058
  async function loadNote(path) {
1047
1059
  currentPath = path;
@@ -1370,7 +1382,10 @@ function renderSessionView(note) {
1370
1382
  ${tagsHtml}
1371
1383
  <span class="dim" style="font-size:var(--fz-xs);margin-left:8px">${note.created || sessionFile}</span>
1372
1384
  </div>
1373
- <button class="ghost-btn" onclick="openEditor('${note.path}')">⌥ edit</button>
1385
+ <div style="display:flex;gap:6px">
1386
+ <button class="ghost-btn" onclick="openEditor('${note.path}')">⌥ edit</button>
1387
+ <button class="ghost-btn" style="color:var(--muted)" onclick="deleteSession('${note.path}')">✕ delete</button>
1388
+ </div>
1374
1389
  </div>
1375
1390
  <h1 style="margin:12px 0 4px;font-size:calc(var(--fz-xl) + 6px);font-weight:500;letter-spacing:-0.01em">
1376
1391
  Session ${sessionFile}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyp-mem",
3
- "version": "0.6.1",
3
+ "version": "0.6.4",
4
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"
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.2"
7
+ version = "0.6.4"
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"}