kyp-mem 0.6.1 → 0.6.5
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 +65 -15
- package/kyp_mem/static/index.html +16 -1
- package/kyp_mem/ui.py +2 -0
- package/kyp_mem/vault.py +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
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,20 @@ def handle_session_start():
|
|
|
107
149
|
if not note:
|
|
108
150
|
continue
|
|
109
151
|
parts.append(f"### {note.title}")
|
|
110
|
-
|
|
111
|
-
|
|
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)
|
|
160
|
+
|
|
161
|
+
parts.append("")
|
|
162
|
+
parts.append("**INSTRUCTION:** Display the session summaries and token savings above to the user as your first message at session start. Format it cleanly.")
|
|
163
|
+
|
|
119
164
|
output = "\n".join(parts)
|
|
165
|
+
|
|
120
166
|
try:
|
|
121
167
|
_record_injection(project_name, len(output))
|
|
122
168
|
except Exception:
|
|
@@ -128,7 +174,7 @@ def handle_session_start():
|
|
|
128
174
|
|
|
129
175
|
def handle_user_prompt():
|
|
130
176
|
raw = sys.stdin.read().strip()
|
|
131
|
-
if not raw:
|
|
177
|
+
if not raw or _is_subprocess():
|
|
132
178
|
return
|
|
133
179
|
try:
|
|
134
180
|
data = json.loads(raw)
|
|
@@ -153,7 +199,7 @@ def handle_user_prompt():
|
|
|
153
199
|
|
|
154
200
|
def handle_post_tool_use():
|
|
155
201
|
raw = sys.stdin.read().strip()
|
|
156
|
-
if not raw:
|
|
202
|
+
if not raw or _is_subprocess():
|
|
157
203
|
return
|
|
158
204
|
try:
|
|
159
205
|
data = json.loads(raw)
|
|
@@ -454,9 +500,11 @@ Return ONLY this format (no preamble):
|
|
|
454
500
|
Raw session data:
|
|
455
501
|
{raw_note}"""
|
|
456
502
|
|
|
503
|
+
env = os.environ.copy()
|
|
504
|
+
env["KYP_MEM_SUMMARIZING"] = "1"
|
|
457
505
|
result = subprocess.run(
|
|
458
506
|
[claude_bin, "-p", prompt, "--max-turns", "1", "--model", model],
|
|
459
|
-
capture_output=True, text=True, timeout=120,
|
|
507
|
+
capture_output=True, text=True, timeout=120, env=env,
|
|
460
508
|
)
|
|
461
509
|
if result.returncode == 0 and result.stdout.strip():
|
|
462
510
|
return result.stdout.strip()
|
|
@@ -466,7 +514,7 @@ Raw session data:
|
|
|
466
514
|
|
|
467
515
|
|
|
468
516
|
def handle_stop():
|
|
469
|
-
if not CURRENT_SESSION.exists():
|
|
517
|
+
if _is_subprocess() or not CURRENT_SESSION.exists():
|
|
470
518
|
return
|
|
471
519
|
|
|
472
520
|
text = CURRENT_SESSION.read_text().strip()
|
|
@@ -642,6 +690,10 @@ def handle_stop():
|
|
|
642
690
|
except Exception:
|
|
643
691
|
pass
|
|
644
692
|
|
|
693
|
+
# Delete session file BEFORE summarization so the spawned claude subprocess
|
|
694
|
+
# doesn't pollute it via hooks writing back into current.jsonl
|
|
695
|
+
CURRENT_SESSION.unlink(missing_ok=True)
|
|
696
|
+
|
|
645
697
|
# Try Claude summarization, fall back to raw sections
|
|
646
698
|
summarized = _summarize_with_claude(raw_note, project_name)
|
|
647
699
|
|
|
@@ -710,8 +762,6 @@ def handle_stop():
|
|
|
710
762
|
vault = Vault(get_vault_path())
|
|
711
763
|
vault.write_note(f"{project_name}/Sessions/{session_id}.md", content, tags, {})
|
|
712
764
|
|
|
713
|
-
CURRENT_SESSION.unlink(missing_ok=True)
|
|
714
|
-
|
|
715
765
|
|
|
716
766
|
def main():
|
|
717
767
|
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
|
-
<
|
|
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/kyp_mem/ui.py
CHANGED
|
@@ -27,6 +27,7 @@ def create_app(vault_path: str = None) -> FastAPI:
|
|
|
27
27
|
|
|
28
28
|
@app.get("/api/stats")
|
|
29
29
|
def stats():
|
|
30
|
+
vault.refresh_if_stale()
|
|
30
31
|
return JSONResponse(vault.get_stats())
|
|
31
32
|
|
|
32
33
|
@app.get("/api/graph")
|
|
@@ -199,6 +200,7 @@ def create_app(vault_path: str = None) -> FastAPI:
|
|
|
199
200
|
|
|
200
201
|
@app.get("/api/sessions")
|
|
201
202
|
def list_sessions(project: str = ""):
|
|
203
|
+
vault.refresh_if_stale()
|
|
202
204
|
sessions = {}
|
|
203
205
|
for path, note in vault.index.notes.items():
|
|
204
206
|
if "/Sessions/" not in path and not path.startswith("Sessions/"):
|
package/kyp_mem/vault.py
CHANGED
|
@@ -211,6 +211,14 @@ class Vault:
|
|
|
211
211
|
self._load_all()
|
|
212
212
|
self._sync_vector_db()
|
|
213
213
|
|
|
214
|
+
def _disk_note_paths(self) -> set[str]:
|
|
215
|
+
return {str(f.relative_to(self.root)) for f in self.root.rglob("*.md")}
|
|
216
|
+
|
|
217
|
+
def refresh_if_stale(self):
|
|
218
|
+
if self._disk_note_paths() != set(self.index.notes.keys()):
|
|
219
|
+
self._load_all()
|
|
220
|
+
self._sync_vector_db()
|
|
221
|
+
|
|
214
222
|
def _sync_vector_db(self):
|
|
215
223
|
mem = get_session_memory()
|
|
216
224
|
for path, note in self.index.notes.items():
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyp-mem",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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.
|
|
7
|
+
version = "0.6.5"
|
|
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"}
|