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 +61 -15
- package/kyp_mem/static/index.html +16 -1
- 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,16 @@ 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)
|
|
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
|
-
<
|
|
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.
|
|
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
|
|
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"}
|