kyp-mem 0.7.3 → 0.7.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/bin/cli.mjs +15 -3
- package/kyp_mem/hooks.py +54 -15
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -26,13 +26,25 @@ function run(command, cmdArgs, stdio = "ignore") {
|
|
|
26
26
|
if (args[0] === "hook") {
|
|
27
27
|
const hookType = args[1];
|
|
28
28
|
const sessionDir = join(homedir(), ".kyp-mem", "sessions");
|
|
29
|
-
const sessionFile = join(sessionDir, "current.jsonl");
|
|
30
29
|
|
|
31
30
|
const chunks = [];
|
|
32
31
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
33
32
|
await new Promise((r) => process.stdin.on("end", r));
|
|
34
33
|
const raw = Buffer.concat(chunks).toString();
|
|
35
34
|
|
|
35
|
+
// Partition the activity log per Claude session id. Previously every session
|
|
36
|
+
// (across all projects) appended to one shared current.jsonl, so concurrent
|
|
37
|
+
// sessions interleaved and the Stop hook filed the whole batch under whichever
|
|
38
|
+
// project logged first — leaking foreign summaries into unrelated projects.
|
|
39
|
+
let sessionId = "";
|
|
40
|
+
try {
|
|
41
|
+
sessionId = ((JSON.parse(raw) || {}).session_id || "").toString();
|
|
42
|
+
} catch (_) {}
|
|
43
|
+
const safeId = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
44
|
+
const sessionFile = safeId
|
|
45
|
+
? join(sessionDir, `current-${safeId}.jsonl`)
|
|
46
|
+
: join(sessionDir, "current.jsonl");
|
|
47
|
+
|
|
36
48
|
if (hookType === "user-prompt") {
|
|
37
49
|
try {
|
|
38
50
|
const data = JSON.parse(raw);
|
|
@@ -59,7 +71,7 @@ if (args[0] === "hook") {
|
|
|
59
71
|
const input = data.tool_input || {};
|
|
60
72
|
const rawResp = data.tool_response || "";
|
|
61
73
|
const resp = (typeof rawResp === "string" ? rawResp : JSON.stringify(rawResp)).slice(0, 2000);
|
|
62
|
-
const entry = { ts: new Date().toISOString(), tool, cwd: process.cwd() };
|
|
74
|
+
const entry = { ts: new Date().toISOString(), tool, cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd() };
|
|
63
75
|
|
|
64
76
|
if (tool === "Edit" || tool === "Write") {
|
|
65
77
|
entry.file = input.file_path || "";
|
|
@@ -93,7 +105,7 @@ if (args[0] === "hook") {
|
|
|
93
105
|
const py = resolvePython();
|
|
94
106
|
if (py) {
|
|
95
107
|
const [cmd, pre] = py;
|
|
96
|
-
const r = run(cmd, [...pre, "-m", "kyp_mem.hooks", "stop"], "inherit");
|
|
108
|
+
const r = run(cmd, [...pre, "-m", "kyp_mem.hooks", "stop", safeId], "inherit");
|
|
97
109
|
process.exit(r.status ?? 0);
|
|
98
110
|
}
|
|
99
111
|
process.exit(0);
|
package/kyp_mem/hooks.py
CHANGED
|
@@ -10,6 +10,37 @@ from pathlib import Path
|
|
|
10
10
|
SESSION_DIR = Path.home() / ".kyp-mem" / "sessions"
|
|
11
11
|
CURRENT_SESSION = SESSION_DIR / "current.jsonl"
|
|
12
12
|
|
|
13
|
+
|
|
14
|
+
def _session_file(session_id):
|
|
15
|
+
"""Per-Claude-session activity log path.
|
|
16
|
+
|
|
17
|
+
Each Claude session gets its own ``current-<session_id>.jsonl`` so that
|
|
18
|
+
concurrent sessions in different projects never share one file. Falls back
|
|
19
|
+
to the legacy shared ``current.jsonl`` when no session id is available.
|
|
20
|
+
"""
|
|
21
|
+
if session_id:
|
|
22
|
+
safe = "".join(c for c in str(session_id) if c.isalnum() or c in "_-")
|
|
23
|
+
if safe:
|
|
24
|
+
return SESSION_DIR / f"current-{safe}.jsonl"
|
|
25
|
+
return CURRENT_SESSION
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _prune_stale_logs(max_age_days=3):
|
|
29
|
+
"""Remove orphaned activity logs left by sessions that never fired Stop
|
|
30
|
+
(crashes, kills) plus the legacy shared current.jsonl once it goes idle."""
|
|
31
|
+
import time
|
|
32
|
+
|
|
33
|
+
cutoff = time.time() - max_age_days * 86400
|
|
34
|
+
try:
|
|
35
|
+
for f in SESSION_DIR.glob("current*.jsonl"):
|
|
36
|
+
try:
|
|
37
|
+
if f.stat().st_mtime < cutoff:
|
|
38
|
+
f.unlink(missing_ok=True)
|
|
39
|
+
except OSError:
|
|
40
|
+
pass
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
13
44
|
MIN_ACTIONS = 5
|
|
14
45
|
CHARS_PER_TOKEN = 4
|
|
15
46
|
|
|
@@ -123,20 +154,26 @@ def handle_session_start():
|
|
|
123
154
|
cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
124
155
|
project_name = Path(cwd).name
|
|
125
156
|
|
|
157
|
+
_prune_stale_logs()
|
|
158
|
+
|
|
126
159
|
try:
|
|
127
160
|
from .config import get_vault_path
|
|
128
161
|
from .vault import Vault
|
|
129
162
|
|
|
130
163
|
vault = Vault(get_vault_path())
|
|
131
164
|
|
|
132
|
-
|
|
165
|
+
# Match case-insensitively: the project dir may be stored in the vault
|
|
166
|
+
# with different casing than the cwd basename (e.g. on case-insensitive
|
|
167
|
+
# filesystems "KYP-MEM" and "kyp-mem" are the same directory).
|
|
168
|
+
prefix = f"{project_name}/".lower()
|
|
169
|
+
project_notes = [p for p in vault.index.notes if p.lower().startswith(prefix)]
|
|
133
170
|
if not project_notes:
|
|
134
171
|
return
|
|
135
172
|
|
|
136
173
|
sessions = sorted(
|
|
137
|
-
(p for p in project_notes if "/
|
|
174
|
+
(p for p in project_notes if "/sessions/" in p.lower()),
|
|
138
175
|
reverse=True,
|
|
139
|
-
)[:
|
|
176
|
+
)[:10]
|
|
140
177
|
if not sessions:
|
|
141
178
|
return
|
|
142
179
|
|
|
@@ -193,7 +230,7 @@ def handle_user_prompt():
|
|
|
193
230
|
}
|
|
194
231
|
|
|
195
232
|
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
196
|
-
with open(
|
|
233
|
+
with open(_session_file(data.get("session_id", "")), "a") as f:
|
|
197
234
|
f.write(json.dumps(entry) + "\n")
|
|
198
235
|
|
|
199
236
|
|
|
@@ -255,7 +292,7 @@ def handle_post_tool_use():
|
|
|
255
292
|
return
|
|
256
293
|
|
|
257
294
|
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
258
|
-
with open(
|
|
295
|
+
with open(_session_file(data.get("session_id", "")), "a") as f:
|
|
259
296
|
f.write(json.dumps(entry) + "\n")
|
|
260
297
|
|
|
261
298
|
|
|
@@ -513,13 +550,14 @@ Raw session data:
|
|
|
513
550
|
return None
|
|
514
551
|
|
|
515
552
|
|
|
516
|
-
def handle_stop():
|
|
517
|
-
|
|
553
|
+
def handle_stop(session_id=""):
|
|
554
|
+
session_file = _session_file(session_id)
|
|
555
|
+
if _is_subprocess() or not session_file.exists():
|
|
518
556
|
return
|
|
519
557
|
|
|
520
|
-
text =
|
|
558
|
+
text = session_file.read_text().strip()
|
|
521
559
|
if not text:
|
|
522
|
-
|
|
560
|
+
session_file.unlink(missing_ok=True)
|
|
523
561
|
return
|
|
524
562
|
|
|
525
563
|
entries = []
|
|
@@ -530,12 +568,12 @@ def handle_stop():
|
|
|
530
568
|
continue
|
|
531
569
|
|
|
532
570
|
if not entries:
|
|
533
|
-
|
|
571
|
+
session_file.unlink(missing_ok=True)
|
|
534
572
|
return
|
|
535
573
|
|
|
536
574
|
write_actions = [e for e in entries if e.get("action") in ("edit", "create", "command")]
|
|
537
575
|
if len(write_actions) < MIN_ACTIONS:
|
|
538
|
-
|
|
576
|
+
session_file.unlink(missing_ok=True)
|
|
539
577
|
return
|
|
540
578
|
|
|
541
579
|
project_dir = entries[0].get("cwd", "unknown")
|
|
@@ -691,8 +729,8 @@ def handle_stop():
|
|
|
691
729
|
pass
|
|
692
730
|
|
|
693
731
|
# Delete session file BEFORE summarization so the spawned claude subprocess
|
|
694
|
-
# doesn't pollute it via hooks writing back into
|
|
695
|
-
|
|
732
|
+
# doesn't pollute it via hooks writing back into the session log
|
|
733
|
+
session_file.unlink(missing_ok=True)
|
|
696
734
|
|
|
697
735
|
# Try Claude summarization, fall back to raw sections
|
|
698
736
|
summarized = _summarize_with_claude(raw_note, project_name)
|
|
@@ -765,7 +803,8 @@ def handle_stop():
|
|
|
765
803
|
|
|
766
804
|
def main():
|
|
767
805
|
if len(sys.argv) > 1 and sys.argv[1] == "stop":
|
|
768
|
-
|
|
806
|
+
session_id = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
807
|
+
handle_stop(session_id)
|
|
769
808
|
else:
|
|
770
809
|
raw = sys.stdin.read().strip()
|
|
771
810
|
if not raw:
|
|
@@ -775,7 +814,7 @@ def main():
|
|
|
775
814
|
except json.JSONDecodeError:
|
|
776
815
|
return
|
|
777
816
|
if "stop_reason" in data:
|
|
778
|
-
handle_stop()
|
|
817
|
+
handle_stop(data.get("session_id", ""))
|
|
779
818
|
|
|
780
819
|
|
|
781
820
|
if __name__ == "__main__":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyp-mem",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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.7.
|
|
7
|
+
version = "0.7.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"}
|