kyp-mem 0.4.3 → 0.4.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/cli.py +12 -3
- package/kyp_mem/hooks.py +75 -0
- package/kyp_mem/static/index.html +20 -2
- package/kyp_mem/ui.py +10 -0
- package/package.json +1 -1
package/kyp_mem/cli.py
CHANGED
|
@@ -45,6 +45,7 @@ def main():
|
|
|
45
45
|
|
|
46
46
|
hook_parser = subparsers.add_parser("hook", help="Handle Claude Code hook events (internal)")
|
|
47
47
|
hook_sub = hook_parser.add_subparsers(dest="hook_command")
|
|
48
|
+
hook_sub.add_parser("session-start", help="Inject project context at session start")
|
|
48
49
|
hook_sub.add_parser("post-tool-use", help="Capture tool activity to session log")
|
|
49
50
|
hook_sub.add_parser("user-prompt", help="Capture user prompt to session log")
|
|
50
51
|
hook_sub.add_parser("stop", help="Compile session into vault note")
|
|
@@ -73,8 +74,10 @@ def main():
|
|
|
73
74
|
elif args.command == "doctor":
|
|
74
75
|
_run_doctor()
|
|
75
76
|
elif args.command == "hook":
|
|
76
|
-
from .hooks import handle_post_tool_use, handle_user_prompt, handle_stop
|
|
77
|
-
if args.hook_command == "
|
|
77
|
+
from .hooks import handle_session_start, handle_post_tool_use, handle_user_prompt, handle_stop
|
|
78
|
+
if args.hook_command == "session-start":
|
|
79
|
+
handle_session_start()
|
|
80
|
+
elif args.hook_command == "post-tool-use":
|
|
78
81
|
handle_post_tool_use()
|
|
79
82
|
elif args.hook_command == "user-prompt":
|
|
80
83
|
handle_user_prompt()
|
|
@@ -286,7 +289,7 @@ def _run_install_hooks(global_config: bool = False, remove: bool = False):
|
|
|
286
289
|
|
|
287
290
|
if remove:
|
|
288
291
|
changed = False
|
|
289
|
-
for event in ("PostToolUse", "UserPromptSubmit", "Stop"):
|
|
292
|
+
for event in ("SessionStart", "PostToolUse", "UserPromptSubmit", "Stop"):
|
|
290
293
|
if event in hooks:
|
|
291
294
|
hooks[event] = [h for h in hooks[event] if not _has_kyp_hook(h)]
|
|
292
295
|
if not hooks[event]:
|
|
@@ -301,14 +304,19 @@ def _run_install_hooks(global_config: bool = False, remove: bool = False):
|
|
|
301
304
|
print()
|
|
302
305
|
return
|
|
303
306
|
|
|
307
|
+
session_start_hooks = hooks.setdefault("SessionStart", [])
|
|
304
308
|
post_tool_hooks = hooks.setdefault("PostToolUse", [])
|
|
305
309
|
prompt_hooks = hooks.setdefault("UserPromptSubmit", [])
|
|
306
310
|
stop_hooks = hooks.setdefault("Stop", [])
|
|
307
311
|
|
|
312
|
+
session_start_hooks = [h for h in session_start_hooks if not _has_kyp_hook(h)]
|
|
308
313
|
post_tool_hooks = [h for h in post_tool_hooks if not _has_kyp_hook(h)]
|
|
309
314
|
prompt_hooks = [h for h in prompt_hooks if not _has_kyp_hook(h)]
|
|
310
315
|
stop_hooks = [h for h in stop_hooks if not _has_kyp_hook(h)]
|
|
311
316
|
|
|
317
|
+
session_start_hooks.append({
|
|
318
|
+
"hooks": [{"type": "command", "command": f"{mcp_command} hook session-start"}],
|
|
319
|
+
})
|
|
312
320
|
post_tool_hooks.append({
|
|
313
321
|
"matcher": "Edit|Write|Read|Bash",
|
|
314
322
|
"hooks": [{"type": "command", "command": f"{mcp_command} hook post-tool-use"}],
|
|
@@ -320,6 +328,7 @@ def _run_install_hooks(global_config: bool = False, remove: bool = False):
|
|
|
320
328
|
"hooks": [{"type": "command", "command": f"{mcp_command} hook stop"}],
|
|
321
329
|
})
|
|
322
330
|
|
|
331
|
+
hooks["SessionStart"] = session_start_hooks
|
|
323
332
|
hooks["PostToolUse"] = post_tool_hooks
|
|
324
333
|
hooks["UserPromptSubmit"] = prompt_hooks
|
|
325
334
|
hooks["Stop"] = stop_hooks
|
package/kyp_mem/hooks.py
CHANGED
|
@@ -12,6 +12,81 @@ CURRENT_SESSION = SESSION_DIR / "current.jsonl"
|
|
|
12
12
|
MIN_ACTIONS = 3
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def handle_session_start():
|
|
16
|
+
"""Inject project context into the conversation at session start."""
|
|
17
|
+
sys.stdin.read()
|
|
18
|
+
|
|
19
|
+
cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
20
|
+
project_name = Path(cwd).name
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from .config import get_vault_path
|
|
24
|
+
from .vault import Vault
|
|
25
|
+
|
|
26
|
+
vault = Vault(get_vault_path())
|
|
27
|
+
|
|
28
|
+
project_notes = [p for p in vault.index.notes if p.startswith(f"{project_name}/")]
|
|
29
|
+
if not project_notes:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
parts = [f"# [kyp-mem] {project_name} — Project Context"]
|
|
33
|
+
parts.append(f"Vault: {get_vault_path()}")
|
|
34
|
+
parts.append("")
|
|
35
|
+
|
|
36
|
+
knowledge_path = f"{project_name}/Knowledge.md"
|
|
37
|
+
knowledge = vault.read(knowledge_path)
|
|
38
|
+
if knowledge:
|
|
39
|
+
parts.append("## Knowledge")
|
|
40
|
+
content = knowledge.content
|
|
41
|
+
timeline_idx = content.find("## Timeline")
|
|
42
|
+
if timeline_idx > 0:
|
|
43
|
+
content = content[:timeline_idx].strip()
|
|
44
|
+
if len(content) > 2000:
|
|
45
|
+
parts.append(content[:2000] + "\n...")
|
|
46
|
+
else:
|
|
47
|
+
parts.append(content)
|
|
48
|
+
parts.append("")
|
|
49
|
+
|
|
50
|
+
other_notes = sorted(
|
|
51
|
+
p for p in project_notes
|
|
52
|
+
if "/Sessions/" not in p and p != knowledge_path
|
|
53
|
+
)
|
|
54
|
+
if other_notes:
|
|
55
|
+
parts.append("## Project Notes")
|
|
56
|
+
for p in other_notes:
|
|
57
|
+
note = vault.index.notes.get(p)
|
|
58
|
+
title = note.title if note else p
|
|
59
|
+
tags = f" [{', '.join(note.tags)}]" if note and note.tags else ""
|
|
60
|
+
parts.append(f"- {title} ({p}){tags}")
|
|
61
|
+
parts.append("")
|
|
62
|
+
|
|
63
|
+
sessions = sorted(
|
|
64
|
+
(p for p in project_notes if "/Sessions/" in p),
|
|
65
|
+
reverse=True,
|
|
66
|
+
)[:3]
|
|
67
|
+
if sessions:
|
|
68
|
+
parts.append(f"## Recent Sessions (last {len(sessions)})")
|
|
69
|
+
for sp in sessions:
|
|
70
|
+
note = vault.read(sp)
|
|
71
|
+
if not note:
|
|
72
|
+
continue
|
|
73
|
+
parts.append(f"### {note.title}")
|
|
74
|
+
content = note.content
|
|
75
|
+
timeline_idx = content.find("## Timeline")
|
|
76
|
+
if timeline_idx > 0:
|
|
77
|
+
content = content[:timeline_idx].strip()
|
|
78
|
+
if len(content) > 300:
|
|
79
|
+
content = content[:300] + "..."
|
|
80
|
+
parts.append(content)
|
|
81
|
+
parts.append("")
|
|
82
|
+
|
|
83
|
+
parts.append("Use `kyp_project_context` for full details. Use `kyp_session_search` to search past sessions.")
|
|
84
|
+
|
|
85
|
+
print("\n".join(parts))
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
15
90
|
def handle_user_prompt():
|
|
16
91
|
raw = sys.stdin.read().strip()
|
|
17
92
|
if not raw:
|
|
@@ -1294,8 +1294,8 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
1294
1294
|
|
|
1295
1295
|
.session-item {
|
|
1296
1296
|
display: flex;
|
|
1297
|
-
align-items:
|
|
1298
|
-
padding:
|
|
1297
|
+
align-items: flex-start;
|
|
1298
|
+
padding: 5px 10px;
|
|
1299
1299
|
border-radius: var(--radius-sm);
|
|
1300
1300
|
cursor: pointer;
|
|
1301
1301
|
font-size: 11px;
|
|
@@ -1328,6 +1328,14 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
1328
1328
|
color: var(--neon-green);
|
|
1329
1329
|
opacity: 0.5;
|
|
1330
1330
|
flex-shrink: 0;
|
|
1331
|
+
margin-top: 3px;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.session-item .si-content {
|
|
1335
|
+
display: flex;
|
|
1336
|
+
flex-direction: column;
|
|
1337
|
+
min-width: 0;
|
|
1338
|
+
flex: 1;
|
|
1331
1339
|
}
|
|
1332
1340
|
|
|
1333
1341
|
.session-item .si-time {
|
|
@@ -1336,7 +1344,17 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
1336
1344
|
color: var(--text-secondary);
|
|
1337
1345
|
}
|
|
1338
1346
|
|
|
1347
|
+
.session-item .si-summary {
|
|
1348
|
+
font-size: 10px;
|
|
1349
|
+
color: var(--text-muted);
|
|
1350
|
+
white-space: nowrap;
|
|
1351
|
+
overflow: hidden;
|
|
1352
|
+
text-overflow: ellipsis;
|
|
1353
|
+
line-height: 1.3;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1339
1356
|
.session-item.active .si-time { color: var(--neon-green); }
|
|
1357
|
+
.session-item.active .si-summary { color: var(--text-secondary); }
|
|
1340
1358
|
|
|
1341
1359
|
.session-badge {
|
|
1342
1360
|
display: inline-flex;
|
package/kyp_mem/ui.py
CHANGED
|
@@ -177,12 +177,22 @@ def create_app(vault_path: str = None) -> FastAPI:
|
|
|
177
177
|
continue
|
|
178
178
|
if proj not in sessions:
|
|
179
179
|
sessions[proj] = []
|
|
180
|
+
summary = ""
|
|
181
|
+
lines = (note.content or "").split("\n")
|
|
182
|
+
for i, line in enumerate(lines):
|
|
183
|
+
if line.strip() == "## Summary":
|
|
184
|
+
for j in range(i + 1, len(lines)):
|
|
185
|
+
if lines[j].strip() and not lines[j].startswith("#"):
|
|
186
|
+
summary = lines[j].strip()
|
|
187
|
+
break
|
|
188
|
+
break
|
|
180
189
|
sessions[proj].append({
|
|
181
190
|
"path": path,
|
|
182
191
|
"title": note.title,
|
|
183
192
|
"tags": note.tags,
|
|
184
193
|
"created": note.created,
|
|
185
194
|
"updated": note.updated,
|
|
195
|
+
"summary": summary,
|
|
186
196
|
})
|
|
187
197
|
for proj in sessions:
|
|
188
198
|
sessions[proj].sort(key=lambda s: s["path"], reverse=True)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyp-mem",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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"
|