kyp-mem 0.3.0 → 0.4.0
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 +38 -14
- package/kyp_mem/server.py +104 -0
- package/kyp_mem/static/index.html +539 -68
- package/kyp_mem/ui.py +89 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/kyp_mem/hooks.py
CHANGED
|
@@ -64,29 +64,53 @@ def handle_stop():
|
|
|
64
64
|
short = cmd[:80] + "..." if len(cmd) > 80 else cmd
|
|
65
65
|
timeline.append(f" {ts} — `{short}`")
|
|
66
66
|
|
|
67
|
+
summary_items = []
|
|
68
|
+
if files_edited:
|
|
69
|
+
summary_items.append(f"Modified {len(files_edited)} file{'s' if len(files_edited) != 1 else ''}")
|
|
70
|
+
if files_created:
|
|
71
|
+
summary_items.append(f"Created {len(files_created)} file{'s' if len(files_created) != 1 else ''}")
|
|
72
|
+
if commands:
|
|
73
|
+
summary_items.append(f"Ran {len(commands)} command{'s' if len(commands) != 1 else ''}")
|
|
74
|
+
|
|
75
|
+
investigate_keywords = {'grep', 'find', 'cat', 'head', 'tail', 'less', 'ls', 'tree', 'rg', 'ag', 'fd', 'wc', 'diff'}
|
|
76
|
+
investigated_cmds = []
|
|
77
|
+
for cmd in commands:
|
|
78
|
+
first_word = cmd.strip().split()[0] if cmd.strip() else ''
|
|
79
|
+
if first_word in investigate_keywords:
|
|
80
|
+
investigated_cmds.append(cmd)
|
|
81
|
+
|
|
67
82
|
parts = [f"# Session {session_id}", ""]
|
|
68
83
|
parts.append(f"**Project:** `{project_dir}`")
|
|
69
84
|
parts.append(f"**Actions:** {len(entries)} total, {len(write_actions)} substantive")
|
|
70
85
|
parts.append("")
|
|
71
86
|
|
|
87
|
+
parts.append("## Summary")
|
|
88
|
+
parts.append(", ".join(summary_items) + f" in `{project_name}`." if summary_items else "")
|
|
89
|
+
parts.append("")
|
|
90
|
+
|
|
91
|
+
parts.append("## INVESTIGATED")
|
|
92
|
+
if investigated_cmds:
|
|
93
|
+
for cmd in investigated_cmds[:15]:
|
|
94
|
+
short = cmd[:120] + "..." if len(cmd) > 120 else cmd
|
|
95
|
+
parts.append(f"- `{short}`")
|
|
96
|
+
parts.append("")
|
|
97
|
+
|
|
98
|
+
parts.append("## LEARNED")
|
|
99
|
+
parts.append("")
|
|
100
|
+
parts.append("")
|
|
101
|
+
|
|
102
|
+
parts.append("## COMPLETED")
|
|
72
103
|
if files_edited:
|
|
73
|
-
parts.append("## Files Modified")
|
|
74
104
|
for f in sorted(files_edited):
|
|
75
|
-
parts.append(f"- `{f}`")
|
|
76
|
-
parts.append("")
|
|
77
|
-
|
|
105
|
+
parts.append(f"- Modified `{Path(f).name}`")
|
|
78
106
|
if files_created:
|
|
79
|
-
parts.append("## Files Created")
|
|
80
107
|
for f in sorted(files_created):
|
|
81
|
-
parts.append(f"- `{f}`")
|
|
82
|
-
|
|
108
|
+
parts.append(f"- Created `{Path(f).name}`")
|
|
109
|
+
parts.append("")
|
|
83
110
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
short = cmd[:120] + "..." if len(cmd) > 120 else cmd
|
|
88
|
-
parts.append(f"- `{short}`")
|
|
89
|
-
parts.append("")
|
|
111
|
+
parts.append("## NEXT STEPS")
|
|
112
|
+
parts.append("")
|
|
113
|
+
parts.append("")
|
|
90
114
|
|
|
91
115
|
if timeline:
|
|
92
116
|
parts.append("## Timeline")
|
|
@@ -102,7 +126,7 @@ def handle_stop():
|
|
|
102
126
|
from .vault import Vault
|
|
103
127
|
|
|
104
128
|
vault = Vault(get_vault_path())
|
|
105
|
-
vault.write_note(f"Sessions/{session_id}.md", content, tags, {})
|
|
129
|
+
vault.write_note(f"{project_name}/Sessions/{session_id}.md", content, tags, {})
|
|
106
130
|
|
|
107
131
|
CURRENT_SESSION.unlink(missing_ok=True)
|
|
108
132
|
|
package/kyp_mem/server.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""KYP-MEM MCP server — headless knowledge base for AI agents."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
from mcp.server.fastmcp import FastMCP
|
|
5
6
|
from .config import get_vault_path
|
|
6
7
|
from .vault import Vault
|
|
@@ -191,3 +192,106 @@ def kyp_stats() -> str:
|
|
|
191
192
|
f" Links: {s['links']}\n"
|
|
192
193
|
f" Backlinks: {s['backlinks']}"
|
|
193
194
|
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@mcp.tool()
|
|
198
|
+
def kyp_session_create(project: str, summary: str = "", investigated: str = "", learned: str = "", completed: str = "", next_steps: str = "") -> str:
|
|
199
|
+
"""Create a structured session note. Project is required. Sections accept markdown text."""
|
|
200
|
+
session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
201
|
+
parts = [f"# Session {session_id}", ""]
|
|
202
|
+
parts.append(f"**Project:** {project}")
|
|
203
|
+
parts.append("")
|
|
204
|
+
parts.append("## Summary")
|
|
205
|
+
parts.append(summary or "")
|
|
206
|
+
parts.append("")
|
|
207
|
+
parts.append("## INVESTIGATED")
|
|
208
|
+
parts.append(investigated or "")
|
|
209
|
+
parts.append("")
|
|
210
|
+
parts.append("## LEARNED")
|
|
211
|
+
parts.append(learned or "")
|
|
212
|
+
parts.append("")
|
|
213
|
+
parts.append("## COMPLETED")
|
|
214
|
+
parts.append(completed or "")
|
|
215
|
+
parts.append("")
|
|
216
|
+
parts.append("## NEXT STEPS")
|
|
217
|
+
parts.append(next_steps or "")
|
|
218
|
+
|
|
219
|
+
content = "\n".join(parts)
|
|
220
|
+
tags = ["session", "manual", project.lower().replace(" ", "-")]
|
|
221
|
+
path = f"{project}/Sessions/{session_id}.md"
|
|
222
|
+
vault.write_note(path, content, tags, {})
|
|
223
|
+
return f"Created session: {path}"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@mcp.tool()
|
|
227
|
+
def kyp_sessions(project: str = "", limit: int = 10) -> str:
|
|
228
|
+
"""List sessions, optionally filtered by project. Shows most recent first."""
|
|
229
|
+
sessions = []
|
|
230
|
+
for path, note in vault.index.notes.items():
|
|
231
|
+
if "/Sessions/" not in path and not path.startswith("Sessions/"):
|
|
232
|
+
continue
|
|
233
|
+
if project and not path.lower().startswith(project.lower() + "/"):
|
|
234
|
+
continue
|
|
235
|
+
sessions.append((path, note))
|
|
236
|
+
sessions.sort(key=lambda s: s[0], reverse=True)
|
|
237
|
+
sessions = sessions[:limit]
|
|
238
|
+
if not sessions:
|
|
239
|
+
return "No sessions found." + (f" (project filter: {project})" if project else "")
|
|
240
|
+
lines = ["Sessions:", ""]
|
|
241
|
+
for path, note in sessions:
|
|
242
|
+
tags = f" [{', '.join(note.tags)}]" if note.tags else ""
|
|
243
|
+
date = note.created or note.updated or ""
|
|
244
|
+
lines.append(f" {date} — {note.title} ({path}){tags}")
|
|
245
|
+
return "\n".join(lines)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@mcp.tool()
|
|
249
|
+
def kyp_project_context(project: str) -> str:
|
|
250
|
+
"""Get full project context: knowledge base + recent session summaries. Call this at session start to understand project history, avoid repeating past work, and prevent hallucination."""
|
|
251
|
+
parts = []
|
|
252
|
+
|
|
253
|
+
knowledge_path = f"{project}/Knowledge.md"
|
|
254
|
+
knowledge = vault.read(knowledge_path)
|
|
255
|
+
if knowledge:
|
|
256
|
+
parts.append("=== PROJECT KNOWLEDGE ===")
|
|
257
|
+
parts.append(knowledge.content)
|
|
258
|
+
parts.append("")
|
|
259
|
+
|
|
260
|
+
project_notes = []
|
|
261
|
+
for path, note in vault.index.notes.items():
|
|
262
|
+
if path.startswith(f"{project}/") and "/Sessions/" not in path and path != knowledge_path:
|
|
263
|
+
project_notes.append((path, note))
|
|
264
|
+
|
|
265
|
+
if project_notes:
|
|
266
|
+
parts.append("=== PROJECT NOTES ===")
|
|
267
|
+
for path, note in sorted(project_notes):
|
|
268
|
+
parts.append(f"\n--- {note.title} ({path}) ---")
|
|
269
|
+
preview = note.content.strip().split("\n")
|
|
270
|
+
parts.append("\n".join(preview[:10]))
|
|
271
|
+
if len(preview) > 10:
|
|
272
|
+
parts.append("...")
|
|
273
|
+
parts.append("")
|
|
274
|
+
|
|
275
|
+
sessions = []
|
|
276
|
+
for path, note in vault.index.notes.items():
|
|
277
|
+
if path.startswith(f"{project}/Sessions/"):
|
|
278
|
+
sessions.append((path, note))
|
|
279
|
+
sessions.sort(key=lambda s: s[0], reverse=True)
|
|
280
|
+
recent = sessions[:5]
|
|
281
|
+
|
|
282
|
+
if recent:
|
|
283
|
+
parts.append(f"=== RECENT SESSIONS ({len(recent)} of {len(sessions)}) ===")
|
|
284
|
+
for path, note in recent:
|
|
285
|
+
parts.append(f"\n--- {note.title} ---")
|
|
286
|
+
content = note.content
|
|
287
|
+
timeline_idx = content.find("## Timeline")
|
|
288
|
+
if timeline_idx > 0:
|
|
289
|
+
parts.append(content[:timeline_idx].strip())
|
|
290
|
+
else:
|
|
291
|
+
parts.append(content[:500])
|
|
292
|
+
parts.append("")
|
|
293
|
+
|
|
294
|
+
if not parts:
|
|
295
|
+
return f"No context found for project '{project}'. Create a Knowledge.md to get started."
|
|
296
|
+
|
|
297
|
+
return "\n".join(parts)
|
|
@@ -10,33 +10,33 @@
|
|
|
10
10
|
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
|
11
11
|
<style>
|
|
12
12
|
:root {
|
|
13
|
-
--bg-void: #
|
|
14
|
-
--bg-primary: #
|
|
15
|
-
--bg-secondary: #
|
|
16
|
-
--bg-tertiary: #
|
|
17
|
-
--bg-hover: #
|
|
18
|
-
--bg-active: #
|
|
19
|
-
--bg-card: #
|
|
20
|
-
--bg-surface: #
|
|
21
|
-
|
|
22
|
-
--neon-cyan: #
|
|
23
|
-
--neon-green: #
|
|
24
|
-
--neon-magenta: #
|
|
13
|
+
--bg-void: #09090b;
|
|
14
|
+
--bg-primary: #0d0d10;
|
|
15
|
+
--bg-secondary: #0f0f12;
|
|
16
|
+
--bg-tertiary: #08080a;
|
|
17
|
+
--bg-hover: #15151c;
|
|
18
|
+
--bg-active: #1c1c26;
|
|
19
|
+
--bg-card: #111116;
|
|
20
|
+
--bg-surface: #0b0b0e;
|
|
21
|
+
|
|
22
|
+
--neon-cyan: #D97757;
|
|
23
|
+
--neon-green: #5bb98c;
|
|
24
|
+
--neon-magenta: #c47ad7;
|
|
25
25
|
--neon-purple: #a78bfa;
|
|
26
|
-
--neon-orange: #
|
|
27
|
-
--neon-yellow: #
|
|
28
|
-
--neon-blue: #
|
|
29
|
-
--neon-red: #
|
|
26
|
+
--neon-orange: #e8935a;
|
|
27
|
+
--neon-yellow: #d4a853;
|
|
28
|
+
--neon-blue: #7da8d4;
|
|
29
|
+
--neon-red: #d47171;
|
|
30
30
|
|
|
31
|
-
--glow-cyan: 0 0 8px #
|
|
32
|
-
--glow-green: 0 0 8px #
|
|
33
|
-
--glow-magenta: 0 0 8px #
|
|
31
|
+
--glow-cyan: 0 0 8px #D9775730;
|
|
32
|
+
--glow-green: 0 0 8px #5bb98c30;
|
|
33
|
+
--glow-magenta: 0 0 8px #c47ad730;
|
|
34
34
|
|
|
35
|
-
--text-primary: #
|
|
36
|
-
--text-secondary: #
|
|
37
|
-
--text-muted: #
|
|
38
|
-
--border: #
|
|
39
|
-
--border-subtle: #
|
|
35
|
+
--text-primary: #d8d5cf;
|
|
36
|
+
--text-secondary: #807b73;
|
|
37
|
+
--text-muted: #44413a;
|
|
38
|
+
--border: #1c1a17;
|
|
39
|
+
--border-subtle: #16150f;
|
|
40
40
|
|
|
41
41
|
--font: 'Inter', -apple-system, sans-serif;
|
|
42
42
|
--font-mono: 'JetBrains Mono', monospace;
|
|
@@ -62,8 +62,8 @@ body::before {
|
|
|
62
62
|
position: fixed;
|
|
63
63
|
inset: 0;
|
|
64
64
|
background-image:
|
|
65
|
-
linear-gradient(rgba(
|
|
66
|
-
linear-gradient(90deg, rgba(
|
|
65
|
+
linear-gradient(rgba(217,119,87,0.015) 1px, transparent 1px),
|
|
66
|
+
linear-gradient(90deg, rgba(217,119,87,0.015) 1px, transparent 1px);
|
|
67
67
|
background-size: 48px 48px;
|
|
68
68
|
pointer-events: none;
|
|
69
69
|
z-index: 0;
|
|
@@ -103,7 +103,7 @@ body::before {
|
|
|
103
103
|
.resize-handle:hover,
|
|
104
104
|
.resize-handle.dragging {
|
|
105
105
|
background: var(--neon-cyan);
|
|
106
|
-
box-shadow: 0 0 12px rgba(
|
|
106
|
+
box-shadow: 0 0 12px rgba(217,119,87,0.2);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
body.resizing { cursor: col-resize !important; user-select: none !important; }
|
|
@@ -191,9 +191,9 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
.header-btn.active {
|
|
194
|
-
border-color: rgba(
|
|
194
|
+
border-color: rgba(217,119,87,0.3);
|
|
195
195
|
color: var(--neon-cyan);
|
|
196
|
-
background: rgba(
|
|
196
|
+
background: rgba(217,119,87,0.05);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
.header-btn .dot {
|
|
@@ -229,8 +229,8 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
229
229
|
.search-box input::placeholder { color: var(--text-muted); }
|
|
230
230
|
|
|
231
231
|
.search-box input:focus {
|
|
232
|
-
border-color: rgba(
|
|
233
|
-
box-shadow: 0 0 16px rgba(
|
|
232
|
+
border-color: rgba(217,119,87,0.3);
|
|
233
|
+
box-shadow: 0 0 16px rgba(217,119,87,0.08);
|
|
234
234
|
width: 260px;
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -434,7 +434,7 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
434
434
|
.tree-item:hover { background: var(--bg-hover); }
|
|
435
435
|
|
|
436
436
|
.tree-item.active {
|
|
437
|
-
background: rgba(
|
|
437
|
+
background: rgba(217,119,87,0.05);
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
.tree-item.active::before {
|
|
@@ -523,9 +523,9 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
523
523
|
font-size: 9px;
|
|
524
524
|
padding: 2px 7px;
|
|
525
525
|
border-radius: var(--radius-sm);
|
|
526
|
-
background: rgba(
|
|
526
|
+
background: rgba(217,119,87,0.08);
|
|
527
527
|
color: var(--neon-cyan);
|
|
528
|
-
border: 1px solid rgba(
|
|
528
|
+
border: 1px solid rgba(217,119,87,0.2);
|
|
529
529
|
cursor: pointer;
|
|
530
530
|
display: flex;
|
|
531
531
|
align-items: center;
|
|
@@ -533,7 +533,7 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
533
533
|
transition: all 0.15s;
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
-
.active-tag:hover { background: rgba(
|
|
536
|
+
.active-tag:hover { background: rgba(217,119,87,0.15); }
|
|
537
537
|
.active-tag .remove { font-size: 11px; opacity: 0.5; }
|
|
538
538
|
.active-tag .remove:hover { opacity: 1; }
|
|
539
539
|
|
|
@@ -566,9 +566,9 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
566
566
|
}
|
|
567
567
|
|
|
568
568
|
.tag-chip.selected {
|
|
569
|
-
background: rgba(
|
|
569
|
+
background: rgba(217,119,87,0.08);
|
|
570
570
|
color: var(--neon-cyan);
|
|
571
|
-
border-color: rgba(
|
|
571
|
+
border-color: rgba(217,119,87,0.2);
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
.tag-chip .tag-count {
|
|
@@ -637,9 +637,9 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
637
637
|
}
|
|
638
638
|
|
|
639
639
|
.tag.clickable-tag:hover {
|
|
640
|
-
background: rgba(
|
|
640
|
+
background: rgba(217,119,87,0.08);
|
|
641
641
|
color: var(--neon-cyan);
|
|
642
|
-
border-color: rgba(
|
|
642
|
+
border-color: rgba(217,119,87,0.2);
|
|
643
643
|
cursor: pointer;
|
|
644
644
|
}
|
|
645
645
|
|
|
@@ -783,7 +783,7 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
783
783
|
|
|
784
784
|
.wikilink:hover {
|
|
785
785
|
color: var(--neon-cyan);
|
|
786
|
-
border-bottom-color: rgba(
|
|
786
|
+
border-bottom-color: rgba(217,119,87,0.4);
|
|
787
787
|
}
|
|
788
788
|
|
|
789
789
|
/* ============ EMPTY STATE ============ */
|
|
@@ -939,13 +939,13 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
939
939
|
|
|
940
940
|
.graph-node:hover circle {
|
|
941
941
|
fill: var(--neon-cyan);
|
|
942
|
-
filter: drop-shadow(0 0 4px rgba(
|
|
942
|
+
filter: drop-shadow(0 0 4px rgba(217,119,87,0.5));
|
|
943
943
|
}
|
|
944
944
|
|
|
945
945
|
.graph-node.active circle {
|
|
946
946
|
fill: var(--neon-cyan);
|
|
947
947
|
r: 6;
|
|
948
|
-
filter: drop-shadow(0 0 6px rgba(
|
|
948
|
+
filter: drop-shadow(0 0 6px rgba(217,119,87,0.5));
|
|
949
949
|
}
|
|
950
950
|
|
|
951
951
|
.graph-node text {
|
|
@@ -959,6 +959,196 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
959
959
|
stroke-width: 1;
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
+
.graph-size-controls {
|
|
963
|
+
margin-left: auto;
|
|
964
|
+
display: flex;
|
|
965
|
+
gap: 2px;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
.graph-size-btn {
|
|
969
|
+
background: var(--bg-hover);
|
|
970
|
+
border: 1px solid var(--border);
|
|
971
|
+
color: var(--text-muted);
|
|
972
|
+
font-family: var(--font-mono);
|
|
973
|
+
font-size: 11px;
|
|
974
|
+
width: 18px;
|
|
975
|
+
height: 18px;
|
|
976
|
+
border-radius: 3px;
|
|
977
|
+
cursor: pointer;
|
|
978
|
+
display: flex;
|
|
979
|
+
align-items: center;
|
|
980
|
+
justify-content: center;
|
|
981
|
+
transition: all 0.15s;
|
|
982
|
+
padding: 0;
|
|
983
|
+
line-height: 1;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.graph-size-btn:hover {
|
|
987
|
+
border-color: var(--neon-cyan);
|
|
988
|
+
color: var(--neon-cyan);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.graph-resize-handle {
|
|
992
|
+
height: 8px;
|
|
993
|
+
cursor: ns-resize;
|
|
994
|
+
position: relative;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.graph-resize-handle::after {
|
|
998
|
+
content: '';
|
|
999
|
+
position: absolute;
|
|
1000
|
+
bottom: 2px;
|
|
1001
|
+
left: 50%;
|
|
1002
|
+
transform: translateX(-50%);
|
|
1003
|
+
width: 24px;
|
|
1004
|
+
height: 2px;
|
|
1005
|
+
background: var(--border);
|
|
1006
|
+
border-radius: 1px;
|
|
1007
|
+
transition: background 0.2s;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.graph-resize-handle:hover::after {
|
|
1011
|
+
background: var(--neon-cyan);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/* ============ SESSIONS ============ */
|
|
1015
|
+
.session-folder-icon { color: var(--neon-green) !important; }
|
|
1016
|
+
|
|
1017
|
+
.session-add-btn {
|
|
1018
|
+
background: transparent;
|
|
1019
|
+
border: 1px solid var(--border);
|
|
1020
|
+
color: var(--text-muted);
|
|
1021
|
+
font-family: var(--font-mono);
|
|
1022
|
+
font-size: 11px;
|
|
1023
|
+
width: 16px;
|
|
1024
|
+
height: 16px;
|
|
1025
|
+
border-radius: 3px;
|
|
1026
|
+
cursor: pointer;
|
|
1027
|
+
margin-left: auto;
|
|
1028
|
+
display: none;
|
|
1029
|
+
align-items: center;
|
|
1030
|
+
justify-content: center;
|
|
1031
|
+
transition: all 0.15s;
|
|
1032
|
+
padding: 0;
|
|
1033
|
+
line-height: 1;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.tree-item:hover .session-add-btn { display: flex; }
|
|
1037
|
+
|
|
1038
|
+
.session-add-btn:hover {
|
|
1039
|
+
border-color: var(--neon-green);
|
|
1040
|
+
color: var(--neon-green);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.session-badge {
|
|
1044
|
+
display: inline-flex;
|
|
1045
|
+
align-items: center;
|
|
1046
|
+
gap: 8px;
|
|
1047
|
+
font-family: var(--font-mono);
|
|
1048
|
+
font-size: 10px;
|
|
1049
|
+
color: var(--neon-green);
|
|
1050
|
+
background: rgba(91,185,140,0.08);
|
|
1051
|
+
border: 1px solid rgba(91,185,140,0.15);
|
|
1052
|
+
padding: 4px 12px;
|
|
1053
|
+
border-radius: var(--radius);
|
|
1054
|
+
margin-bottom: 16px;
|
|
1055
|
+
letter-spacing: 0.5px;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.session-create-overlay {
|
|
1059
|
+
position: fixed;
|
|
1060
|
+
inset: 0;
|
|
1061
|
+
background: rgba(6,6,12,0.75);
|
|
1062
|
+
z-index: 200;
|
|
1063
|
+
display: none;
|
|
1064
|
+
align-items: center;
|
|
1065
|
+
justify-content: center;
|
|
1066
|
+
backdrop-filter: blur(4px);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.session-create-overlay.active { display: flex; }
|
|
1070
|
+
|
|
1071
|
+
.session-create-modal {
|
|
1072
|
+
width: 420px;
|
|
1073
|
+
background: var(--bg-card);
|
|
1074
|
+
border: 1px solid var(--border);
|
|
1075
|
+
border-radius: var(--radius-lg);
|
|
1076
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
|
|
1077
|
+
overflow: hidden;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.session-create-header {
|
|
1081
|
+
padding: 16px 20px 12px;
|
|
1082
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
1083
|
+
font-family: var(--font-mono);
|
|
1084
|
+
font-size: 11px;
|
|
1085
|
+
font-weight: 600;
|
|
1086
|
+
color: var(--text-secondary);
|
|
1087
|
+
letter-spacing: 0.5px;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.session-create-body {
|
|
1091
|
+
padding: 16px 20px;
|
|
1092
|
+
display: flex;
|
|
1093
|
+
flex-direction: column;
|
|
1094
|
+
gap: 12px;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.session-field label {
|
|
1098
|
+
display: block;
|
|
1099
|
+
font-family: var(--font-mono);
|
|
1100
|
+
font-size: 9px;
|
|
1101
|
+
font-weight: 600;
|
|
1102
|
+
text-transform: uppercase;
|
|
1103
|
+
letter-spacing: 1px;
|
|
1104
|
+
color: var(--text-muted);
|
|
1105
|
+
margin-bottom: 4px;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.session-field select,
|
|
1109
|
+
.session-field input,
|
|
1110
|
+
.session-field textarea {
|
|
1111
|
+
width: 100%;
|
|
1112
|
+
background: var(--bg-surface);
|
|
1113
|
+
border: 1px solid var(--border);
|
|
1114
|
+
color: var(--text-primary);
|
|
1115
|
+
font-family: var(--font-mono);
|
|
1116
|
+
font-size: 12px;
|
|
1117
|
+
padding: 8px 12px;
|
|
1118
|
+
border-radius: var(--radius);
|
|
1119
|
+
outline: none;
|
|
1120
|
+
transition: border-color 0.2s;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.session-field select:focus,
|
|
1124
|
+
.session-field input:focus,
|
|
1125
|
+
.session-field textarea:focus {
|
|
1126
|
+
border-color: rgba(217,119,87,0.3);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.session-field textarea {
|
|
1130
|
+
min-height: 60px;
|
|
1131
|
+
resize: vertical;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
.session-create-footer {
|
|
1135
|
+
padding: 12px 20px;
|
|
1136
|
+
border-top: 1px solid var(--border-subtle);
|
|
1137
|
+
display: flex;
|
|
1138
|
+
justify-content: flex-end;
|
|
1139
|
+
gap: 8px;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
.session-create-footer .edit-btn.create {
|
|
1143
|
+
background: rgba(91,185,140,0.1);
|
|
1144
|
+
color: var(--neon-green);
|
|
1145
|
+
border-color: rgba(91,185,140,0.3);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
.session-create-footer .edit-btn.create:hover {
|
|
1149
|
+
background: rgba(91,185,140,0.2);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
962
1152
|
/* ============ SCROLLBAR ============ */
|
|
963
1153
|
::-webkit-scrollbar { width: 4px; }
|
|
964
1154
|
::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -1025,12 +1215,12 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
1025
1215
|
.edit-btn.cancel:hover { color: var(--text-secondary); border-color: var(--text-muted); }
|
|
1026
1216
|
|
|
1027
1217
|
.edit-btn.save {
|
|
1028
|
-
background: rgba(
|
|
1218
|
+
background: rgba(217,119,87,0.1);
|
|
1029
1219
|
color: var(--neon-cyan);
|
|
1030
|
-
border-color: rgba(
|
|
1220
|
+
border-color: rgba(217,119,87,0.3);
|
|
1031
1221
|
}
|
|
1032
1222
|
|
|
1033
|
-
.edit-btn.save:hover { background: rgba(
|
|
1223
|
+
.edit-btn.save:hover { background: rgba(217,119,87,0.2); }
|
|
1034
1224
|
|
|
1035
1225
|
.edit-textarea {
|
|
1036
1226
|
flex: 1;
|
|
@@ -1112,6 +1302,9 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
1112
1302
|
<span class="dot"></span>
|
|
1113
1303
|
<span>GRAPH</span>
|
|
1114
1304
|
</button>
|
|
1305
|
+
<button class="header-btn" id="new-project-btn" title="New project">
|
|
1306
|
+
<span>+ PROJECT</span>
|
|
1307
|
+
</button>
|
|
1115
1308
|
<div class="search-box">
|
|
1116
1309
|
<span class="search-icon">⚲</span>
|
|
1117
1310
|
<input type="text" id="search-input" placeholder="Search...">
|
|
@@ -1164,8 +1357,9 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
1164
1357
|
<!-- Right panel -->
|
|
1165
1358
|
<div class="right-panel" id="right-panel">
|
|
1166
1359
|
<div id="graph-section">
|
|
1167
|
-
<div class="rp-section-title">Local Graph
|
|
1360
|
+
<div class="rp-section-title">Local Graph <span class="graph-size-controls"><button class="graph-size-btn" id="graph-shrink" title="Shrink graph">−</button><button class="graph-size-btn" id="graph-grow" title="Grow graph">+</button></span></div>
|
|
1168
1361
|
<div id="graph-container"></div>
|
|
1362
|
+
<div id="graph-resize-handle" class="graph-resize-handle"></div>
|
|
1169
1363
|
</div>
|
|
1170
1364
|
<div id="rp-outline" class="rp-section" style="display:none;">
|
|
1171
1365
|
<div class="rp-section-title">Outline <span class="rp-count" id="outline-count"></span></div>
|
|
@@ -1212,11 +1406,55 @@ body.resizing .resize-handle { pointer-events: auto !important; }
|
|
|
1212
1406
|
</div>
|
|
1213
1407
|
</div>
|
|
1214
1408
|
|
|
1409
|
+
<!-- Session Create Modal -->
|
|
1410
|
+
<div class="session-create-overlay" id="session-create-overlay">
|
|
1411
|
+
<div class="session-create-modal">
|
|
1412
|
+
<div class="session-create-header">NEW SESSION</div>
|
|
1413
|
+
<div class="session-create-body">
|
|
1414
|
+
<div class="session-field">
|
|
1415
|
+
<label>Project</label>
|
|
1416
|
+
<input type="text" id="session-project" list="project-list" placeholder="Project name...">
|
|
1417
|
+
<datalist id="project-list"></datalist>
|
|
1418
|
+
</div>
|
|
1419
|
+
<div class="session-field">
|
|
1420
|
+
<label>Summary</label>
|
|
1421
|
+
<textarea id="session-summary" placeholder="What are you working on?"></textarea>
|
|
1422
|
+
</div>
|
|
1423
|
+
</div>
|
|
1424
|
+
<div class="session-create-footer">
|
|
1425
|
+
<button class="edit-btn cancel" id="session-cancel">CANCEL</button>
|
|
1426
|
+
<button class="edit-btn create" id="session-create-btn">CREATE</button>
|
|
1427
|
+
</div>
|
|
1428
|
+
</div>
|
|
1429
|
+
</div>
|
|
1430
|
+
|
|
1431
|
+
<!-- Project Create Modal -->
|
|
1432
|
+
<div class="session-create-overlay" id="project-create-overlay">
|
|
1433
|
+
<div class="session-create-modal">
|
|
1434
|
+
<div class="session-create-header">NEW PROJECT</div>
|
|
1435
|
+
<div class="session-create-body">
|
|
1436
|
+
<div class="session-field">
|
|
1437
|
+
<label>Project Name</label>
|
|
1438
|
+
<input type="text" id="project-name-input" placeholder="My Project...">
|
|
1439
|
+
</div>
|
|
1440
|
+
<div class="session-field">
|
|
1441
|
+
<label>Overview (optional)</label>
|
|
1442
|
+
<textarea id="project-overview-input" placeholder="Brief project description, goals, tech stack..."></textarea>
|
|
1443
|
+
</div>
|
|
1444
|
+
</div>
|
|
1445
|
+
<div class="session-create-footer">
|
|
1446
|
+
<button class="edit-btn cancel" id="project-cancel">CANCEL</button>
|
|
1447
|
+
<button class="edit-btn create" id="project-create-btn">CREATE</button>
|
|
1448
|
+
</div>
|
|
1449
|
+
</div>
|
|
1450
|
+
</div>
|
|
1451
|
+
|
|
1215
1452
|
<script>
|
|
1216
1453
|
let currentPath = null;
|
|
1217
1454
|
let treeData = null;
|
|
1218
1455
|
let allNotes = {};
|
|
1219
1456
|
let graphVisible = true;
|
|
1457
|
+
let currentNote = null;
|
|
1220
1458
|
let activeTagFilters = new Set();
|
|
1221
1459
|
let qsSelectedIndex = 0;
|
|
1222
1460
|
|
|
@@ -1255,6 +1493,8 @@ document.addEventListener('keydown', (e) => {
|
|
|
1255
1493
|
}
|
|
1256
1494
|
if (e.key === 'Escape') {
|
|
1257
1495
|
closeQuickSwitcher();
|
|
1496
|
+
closeSessionCreate();
|
|
1497
|
+
closeProjectCreate();
|
|
1258
1498
|
}
|
|
1259
1499
|
});
|
|
1260
1500
|
|
|
@@ -1351,16 +1591,27 @@ function updateQsSelection() {
|
|
|
1351
1591
|
}
|
|
1352
1592
|
|
|
1353
1593
|
// --- File Tree ---
|
|
1354
|
-
function renderTree(node, container) {
|
|
1594
|
+
function renderTree(node, container, parentFolder) {
|
|
1355
1595
|
if (node.type === 'folder' && node.name !== 'vault') {
|
|
1596
|
+
const isSessionsFolder = node.name === 'Sessions';
|
|
1356
1597
|
const item = document.createElement('div');
|
|
1357
1598
|
item.className = 'tree-item';
|
|
1358
|
-
|
|
1599
|
+
|
|
1600
|
+
if (isSessionsFolder) {
|
|
1601
|
+
item.innerHTML = `<span class="arrow open">▶</span><span class="icon session-folder-icon">◴</span><span class="name">Sessions</span><button class="session-add-btn" data-project="${parentFolder || ''}" title="New session">+</button>`;
|
|
1602
|
+
} else {
|
|
1603
|
+
item.innerHTML = `<span class="arrow open">▶</span><span class="icon folder-icon">☰</span><span class="name">${node.name}</span>`;
|
|
1604
|
+
}
|
|
1359
1605
|
|
|
1360
1606
|
const children = document.createElement('div');
|
|
1361
1607
|
children.className = 'tree-children';
|
|
1362
1608
|
|
|
1363
1609
|
item.addEventListener('click', (e) => {
|
|
1610
|
+
if (e.target.classList.contains('session-add-btn')) {
|
|
1611
|
+
e.stopPropagation();
|
|
1612
|
+
openSessionCreate(e.target.dataset.project);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1364
1615
|
e.stopPropagation();
|
|
1365
1616
|
item.querySelector('.arrow').classList.toggle('open');
|
|
1366
1617
|
children.classList.toggle('collapsed');
|
|
@@ -1368,13 +1619,30 @@ function renderTree(node, container) {
|
|
|
1368
1619
|
|
|
1369
1620
|
container.appendChild(item);
|
|
1370
1621
|
container.appendChild(children);
|
|
1371
|
-
(node.children || []).forEach(c => renderTree(c, children));
|
|
1622
|
+
(node.children || []).forEach(c => renderTree(c, children, node.name));
|
|
1372
1623
|
|
|
1373
1624
|
} else if (node.type === 'note') {
|
|
1374
1625
|
const item = document.createElement('div');
|
|
1375
1626
|
item.className = 'tree-item';
|
|
1376
1627
|
item.dataset.path = node.path;
|
|
1377
|
-
|
|
1628
|
+
|
|
1629
|
+
const isSessionNote = node.path.includes('/Sessions/') || node.path.startsWith('Sessions/');
|
|
1630
|
+
let displayName = node.name.replace('.md', '');
|
|
1631
|
+
|
|
1632
|
+
if (isSessionNote) {
|
|
1633
|
+
const match = displayName.match(/^(\d{4})-(\d{2})-(\d{2})_(\d{2})(\d{2})(\d{2})$/);
|
|
1634
|
+
if (match) {
|
|
1635
|
+
const [, y, mo, d, h, mi] = match;
|
|
1636
|
+
const hr = parseInt(h);
|
|
1637
|
+
const ampm = hr >= 12 ? 'PM' : 'AM';
|
|
1638
|
+
const hr12 = hr % 12 || 12;
|
|
1639
|
+
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
1640
|
+
displayName = `${months[parseInt(mo)-1]} ${parseInt(d)}, ${hr12}:${mi} ${ampm}`;
|
|
1641
|
+
item.title = node.name.replace('.md', '');
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
item.innerHTML = `<span class="arrow" style="visibility:hidden">▶</span><span class="icon note-icon">◇</span><span class="name">${displayName}</span>`;
|
|
1378
1646
|
item.addEventListener('click', () => loadNote(node.path));
|
|
1379
1647
|
container.appendChild(item);
|
|
1380
1648
|
|
|
@@ -1388,6 +1656,7 @@ async function loadNote(path) {
|
|
|
1388
1656
|
currentPath = path;
|
|
1389
1657
|
const note = await fetchJSON(`/api/note/${path}`);
|
|
1390
1658
|
if (note.error) return;
|
|
1659
|
+
currentNote = note;
|
|
1391
1660
|
|
|
1392
1661
|
document.querySelectorAll('.tree-item').forEach(el => {
|
|
1393
1662
|
el.classList.toggle('active', el.dataset.path === path);
|
|
@@ -1404,6 +1673,13 @@ async function loadNote(path) {
|
|
|
1404
1673
|
let html = '<div class="fade-in">';
|
|
1405
1674
|
|
|
1406
1675
|
html += `<button class="note-edit-btn" onclick="openEditor('${path}')">EDIT</button>`;
|
|
1676
|
+
|
|
1677
|
+
const isSession = path.includes('/Sessions/') || path.startsWith('Sessions/');
|
|
1678
|
+
if (isSession) {
|
|
1679
|
+
const sessionProject = path.includes('/Sessions/') ? path.split('/Sessions/')[0] : 'General';
|
|
1680
|
+
html += `<div class="session-badge">◴ SESSION · ${sessionProject}</div>`;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1407
1683
|
html += '<div class="note-properties">';
|
|
1408
1684
|
(note.tags || []).forEach(t => { html += `<span class="tag clickable-tag" data-tag="${t}">#${t}</span>`; });
|
|
1409
1685
|
Object.entries(note.properties || {}).forEach(([k, v]) => {
|
|
@@ -1619,6 +1895,134 @@ document.addEventListener('keydown', (e) => {
|
|
|
1619
1895
|
}
|
|
1620
1896
|
});
|
|
1621
1897
|
|
|
1898
|
+
// --- Session Management ---
|
|
1899
|
+
function openSessionCreate(defaultProject) {
|
|
1900
|
+
const overlay = document.getElementById('session-create-overlay');
|
|
1901
|
+
const projectInput = document.getElementById('session-project');
|
|
1902
|
+
const datalist = document.getElementById('project-list');
|
|
1903
|
+
const summaryInput = document.getElementById('session-summary');
|
|
1904
|
+
|
|
1905
|
+
const projects = new Set();
|
|
1906
|
+
for (const path of Object.keys(allNotes)) {
|
|
1907
|
+
const parts = path.split('/');
|
|
1908
|
+
if (parts.length > 1) projects.add(parts[0]);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
datalist.innerHTML = '';
|
|
1912
|
+
for (const p of [...projects].sort()) {
|
|
1913
|
+
const opt = document.createElement('option');
|
|
1914
|
+
opt.value = p;
|
|
1915
|
+
datalist.appendChild(opt);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
projectInput.value = defaultProject || '';
|
|
1919
|
+
summaryInput.value = '';
|
|
1920
|
+
overlay.classList.add('active');
|
|
1921
|
+
|
|
1922
|
+
if (defaultProject) summaryInput.focus();
|
|
1923
|
+
else projectInput.focus();
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function closeSessionCreate() {
|
|
1927
|
+
document.getElementById('session-create-overlay').classList.remove('active');
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
document.getElementById('session-cancel').addEventListener('click', closeSessionCreate);
|
|
1931
|
+
document.getElementById('session-create-overlay').addEventListener('click', (e) => {
|
|
1932
|
+
if (e.target === e.currentTarget) closeSessionCreate();
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
document.getElementById('session-create-btn').addEventListener('click', async () => {
|
|
1936
|
+
const project = document.getElementById('session-project').value.trim();
|
|
1937
|
+
const summary = document.getElementById('session-summary').value.trim();
|
|
1938
|
+
|
|
1939
|
+
if (!project) {
|
|
1940
|
+
document.getElementById('session-project').focus();
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const resp = await fetch('/api/sessions/create', {
|
|
1945
|
+
method: 'POST',
|
|
1946
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1947
|
+
body: JSON.stringify({ project, summary }),
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
const result = await resp.json();
|
|
1951
|
+
if (result.ok) {
|
|
1952
|
+
closeSessionCreate();
|
|
1953
|
+
await refreshTree();
|
|
1954
|
+
loadNote(result.path);
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
// --- Project Management ---
|
|
1959
|
+
function closeProjectCreate() {
|
|
1960
|
+
document.getElementById('project-create-overlay').classList.remove('active');
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
document.getElementById('new-project-btn').addEventListener('click', () => {
|
|
1964
|
+
document.getElementById('project-create-overlay').classList.add('active');
|
|
1965
|
+
document.getElementById('project-name-input').value = '';
|
|
1966
|
+
document.getElementById('project-overview-input').value = '';
|
|
1967
|
+
document.getElementById('project-name-input').focus();
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
document.getElementById('project-cancel').addEventListener('click', closeProjectCreate);
|
|
1971
|
+
document.getElementById('project-create-overlay').addEventListener('click', (e) => {
|
|
1972
|
+
if (e.target === e.currentTarget) closeProjectCreate();
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
document.getElementById('project-create-btn').addEventListener('click', async () => {
|
|
1976
|
+
const name = document.getElementById('project-name-input').value.trim();
|
|
1977
|
+
const overview = document.getElementById('project-overview-input').value.trim();
|
|
1978
|
+
if (!name) {
|
|
1979
|
+
document.getElementById('project-name-input').focus();
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
const resp = await fetch('/api/projects/create', {
|
|
1983
|
+
method: 'POST',
|
|
1984
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1985
|
+
body: JSON.stringify({ name, overview }),
|
|
1986
|
+
});
|
|
1987
|
+
const result = await resp.json();
|
|
1988
|
+
if (result.ok) {
|
|
1989
|
+
closeProjectCreate();
|
|
1990
|
+
await refreshTree();
|
|
1991
|
+
loadNote(result.path);
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
async function refreshTree() {
|
|
1996
|
+
treeData = await fetchJSON('/api/tree');
|
|
1997
|
+
allNotes = {};
|
|
1998
|
+
function walk(node) {
|
|
1999
|
+
if (node.type === 'note') {
|
|
2000
|
+
allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
|
|
2001
|
+
}
|
|
2002
|
+
(node.children || []).forEach(walk);
|
|
2003
|
+
}
|
|
2004
|
+
walk(treeData);
|
|
2005
|
+
|
|
2006
|
+
const treeEl = document.getElementById('file-tree');
|
|
2007
|
+
treeEl.innerHTML = '';
|
|
2008
|
+
renderTree(treeData, treeEl);
|
|
2009
|
+
renderTagCloud(collectAllTags());
|
|
2010
|
+
|
|
2011
|
+
const stats = await fetchJSON('/api/stats');
|
|
2012
|
+
document.getElementById('stats-bar').innerHTML = `
|
|
2013
|
+
<span><span class="stat-val">${stats.notes}</span> notes</span>
|
|
2014
|
+
<span><span class="stat-val">${stats.folders}</span> folders</span>
|
|
2015
|
+
<span><span class="stat-val">${stats.tags}</span> tags</span>
|
|
2016
|
+
<span><span class="stat-val">${stats.links}</span> links</span>
|
|
2017
|
+
`;
|
|
2018
|
+
|
|
2019
|
+
if (currentPath) {
|
|
2020
|
+
document.querySelectorAll('.tree-item').forEach(el => {
|
|
2021
|
+
el.classList.toggle('active', el.dataset.path === currentPath);
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1622
2026
|
// --- Graph ---
|
|
1623
2027
|
function renderGraph(note) {
|
|
1624
2028
|
const container = document.getElementById('graph-container');
|
|
@@ -1638,60 +2042,126 @@ function renderGraph(note) {
|
|
|
1638
2042
|
if (path) links.push({ source: note.path, target: path });
|
|
1639
2043
|
});
|
|
1640
2044
|
|
|
1641
|
-
(note.backlinks || []).forEach(
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2045
|
+
(note.backlinks || []).forEach(bl => {
|
|
2046
|
+
const blPath = typeof bl === 'string' ? bl : (bl && bl.path);
|
|
2047
|
+
if (!blPath) return;
|
|
2048
|
+
if (!nodes.has(blPath)) {
|
|
2049
|
+
const n = allNotes[blPath];
|
|
2050
|
+
const title = (bl && typeof bl === 'object' && bl.title) ? bl.title : (n ? n.title : blPath);
|
|
2051
|
+
nodes.set(blPath, { id: blPath, title, active: false });
|
|
1645
2052
|
}
|
|
1646
|
-
links.push({ source:
|
|
2053
|
+
links.push({ source: blPath, target: note.path });
|
|
1647
2054
|
});
|
|
1648
2055
|
|
|
1649
2056
|
(note.related || []).forEach(r => {
|
|
1650
2057
|
if (!nodes.has(r.path)) {
|
|
1651
2058
|
nodes.set(r.path, { id: r.path, title: r.title, active: false });
|
|
1652
2059
|
}
|
|
2060
|
+
links.push({ source: note.path, target: r.path, dashed: true });
|
|
1653
2061
|
});
|
|
1654
2062
|
|
|
1655
2063
|
const nodeArray = Array.from(nodes.values());
|
|
1656
2064
|
if (nodeArray.length < 2) {
|
|
1657
|
-
container.innerHTML = '<
|
|
2065
|
+
container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:10px;font-family:var(--font-mono)">No connections</div>';
|
|
1658
2066
|
return;
|
|
1659
2067
|
}
|
|
1660
2068
|
|
|
1661
|
-
const width = container.clientWidth;
|
|
1662
|
-
const height = container.clientHeight;
|
|
2069
|
+
const width = container.clientWidth || 248;
|
|
2070
|
+
const height = container.clientHeight || 200;
|
|
2071
|
+
const margin = 24;
|
|
1663
2072
|
|
|
1664
2073
|
const svg = d3.select(container).append('svg')
|
|
1665
2074
|
.attr('viewBox', `0 0 ${width} ${height}`);
|
|
1666
2075
|
|
|
2076
|
+
const g = svg.append('g');
|
|
2077
|
+
|
|
2078
|
+
svg.call(d3.zoom()
|
|
2079
|
+
.scaleExtent([0.3, 3])
|
|
2080
|
+
.on('zoom', (event) => g.attr('transform', event.transform))
|
|
2081
|
+
);
|
|
2082
|
+
|
|
1667
2083
|
const simulation = d3.forceSimulation(nodeArray)
|
|
1668
|
-
.force('link', d3.forceLink(links).id(d => d.id).distance(
|
|
1669
|
-
.force('charge', d3.forceManyBody().strength(-
|
|
2084
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(60))
|
|
2085
|
+
.force('charge', d3.forceManyBody().strength(-120))
|
|
1670
2086
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
1671
|
-
.force('collision', d3.forceCollide().radius(
|
|
2087
|
+
.force('collision', d3.forceCollide().radius(25));
|
|
1672
2088
|
|
|
1673
|
-
const
|
|
2089
|
+
const linkEl = g.selectAll('.graph-link')
|
|
1674
2090
|
.data(links).enter().append('line')
|
|
1675
|
-
.attr('class', 'graph-link')
|
|
2091
|
+
.attr('class', 'graph-link')
|
|
2092
|
+
.attr('stroke-dasharray', d => d.dashed ? '3,3' : null)
|
|
2093
|
+
.style('opacity', d => d.dashed ? 0.5 : 1);
|
|
1676
2094
|
|
|
1677
|
-
const node =
|
|
2095
|
+
const node = g.selectAll('.graph-node')
|
|
1678
2096
|
.data(nodeArray).enter().append('g')
|
|
1679
2097
|
.attr('class', d => 'graph-node' + (d.active ? ' active' : ''))
|
|
1680
|
-
.on('click', (e, d) => loadNote(d.id))
|
|
2098
|
+
.on('click', (e, d) => { e.stopPropagation(); loadNote(d.id); })
|
|
1681
2099
|
.call(d3.drag()
|
|
1682
2100
|
.on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
1683
2101
|
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
|
1684
2102
|
.on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
1685
2103
|
);
|
|
1686
2104
|
|
|
1687
|
-
node.append('circle').attr('r', d => d.active ?
|
|
2105
|
+
node.append('circle').attr('r', d => d.active ? 6 : 4);
|
|
1688
2106
|
node.append('text').text(d => d.title).attr('dx', 10).attr('dy', 3);
|
|
1689
2107
|
|
|
1690
2108
|
simulation.on('tick', () => {
|
|
1691
|
-
|
|
2109
|
+
linkEl
|
|
1692
2110
|
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
1693
2111
|
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
1694
|
-
node.attr('transform', d =>
|
|
2112
|
+
node.attr('transform', d => {
|
|
2113
|
+
d.x = Math.max(margin, Math.min(width - margin, d.x));
|
|
2114
|
+
d.y = Math.max(margin, Math.min(height - margin, d.y));
|
|
2115
|
+
return `translate(${d.x},${d.y})`;
|
|
2116
|
+
});
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// --- Graph Resize ---
|
|
2121
|
+
let graphHeight = parseInt(localStorage.getItem('kyp-graph-h')) || 200;
|
|
2122
|
+
|
|
2123
|
+
function initGraphResize() {
|
|
2124
|
+
const container = document.getElementById('graph-container');
|
|
2125
|
+
container.style.height = graphHeight + 'px';
|
|
2126
|
+
|
|
2127
|
+
document.getElementById('graph-shrink').addEventListener('click', () => {
|
|
2128
|
+
graphHeight = Math.max(100, graphHeight - 50);
|
|
2129
|
+
container.style.height = graphHeight + 'px';
|
|
2130
|
+
localStorage.setItem('kyp-graph-h', graphHeight);
|
|
2131
|
+
if (graphVisible && currentNote) renderGraph(currentNote);
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
document.getElementById('graph-grow').addEventListener('click', () => {
|
|
2135
|
+
graphHeight = Math.min(600, graphHeight + 50);
|
|
2136
|
+
container.style.height = graphHeight + 'px';
|
|
2137
|
+
localStorage.setItem('kyp-graph-h', graphHeight);
|
|
2138
|
+
if (graphVisible && currentNote) renderGraph(currentNote);
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
const handle = document.getElementById('graph-resize-handle');
|
|
2142
|
+
handle.addEventListener('mousedown', (e) => {
|
|
2143
|
+
e.preventDefault();
|
|
2144
|
+
const startY = e.clientY;
|
|
2145
|
+
const startH = graphHeight;
|
|
2146
|
+
document.body.style.cursor = 'ns-resize';
|
|
2147
|
+
document.body.style.userSelect = 'none';
|
|
2148
|
+
|
|
2149
|
+
function onMove(ev) {
|
|
2150
|
+
graphHeight = Math.max(100, Math.min(600, startH + (ev.clientY - startY)));
|
|
2151
|
+
container.style.height = graphHeight + 'px';
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
function onUp() {
|
|
2155
|
+
document.body.style.cursor = '';
|
|
2156
|
+
document.body.style.userSelect = '';
|
|
2157
|
+
localStorage.setItem('kyp-graph-h', graphHeight);
|
|
2158
|
+
if (graphVisible && currentNote) renderGraph(currentNote);
|
|
2159
|
+
document.removeEventListener('mousemove', onMove);
|
|
2160
|
+
document.removeEventListener('mouseup', onUp);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
document.addEventListener('mousemove', onMove);
|
|
2164
|
+
document.addEventListener('mouseup', onUp);
|
|
1695
2165
|
});
|
|
1696
2166
|
}
|
|
1697
2167
|
|
|
@@ -1930,6 +2400,7 @@ async function init() {
|
|
|
1930
2400
|
}
|
|
1931
2401
|
|
|
1932
2402
|
initResize();
|
|
2403
|
+
initGraphResize();
|
|
1933
2404
|
init();
|
|
1934
2405
|
</script>
|
|
1935
2406
|
</body>
|
package/kyp_mem/ui.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""KYP-MEM web UI — interactive interface for browsing the vault."""
|
|
2
2
|
|
|
3
3
|
import webbrowser
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from fastapi import FastAPI, Request
|
|
6
7
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
@@ -119,6 +120,94 @@ def create_app(vault_path: str = None) -> FastAPI:
|
|
|
119
120
|
vault.write_note(path, content, tags, props)
|
|
120
121
|
return JSONResponse({"ok": True, "path": path})
|
|
121
122
|
|
|
123
|
+
@app.post("/api/sessions/create")
|
|
124
|
+
async def create_session(request: Request):
|
|
125
|
+
body = await request.json()
|
|
126
|
+
project = body.get("project", "").strip()
|
|
127
|
+
summary = body.get("summary", "").strip()
|
|
128
|
+
if not project:
|
|
129
|
+
return JSONResponse({"error": "Project name required"}, 400)
|
|
130
|
+
session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
131
|
+
content = (
|
|
132
|
+
f"# Session {session_id}\n\n"
|
|
133
|
+
f"**Project:** {project}\n\n"
|
|
134
|
+
f"## Summary\n{summary}\n\n"
|
|
135
|
+
f"## INVESTIGATED\n\n\n"
|
|
136
|
+
f"## LEARNED\n\n\n"
|
|
137
|
+
f"## COMPLETED\n\n\n"
|
|
138
|
+
f"## NEXT STEPS\n\n"
|
|
139
|
+
)
|
|
140
|
+
tags = ["session", "manual", project.lower().replace(" ", "-")]
|
|
141
|
+
path = f"{project}/Sessions/{session_id}.md"
|
|
142
|
+
vault.write_note(path, content, tags, {})
|
|
143
|
+
return JSONResponse({"ok": True, "path": path})
|
|
144
|
+
|
|
145
|
+
@app.get("/api/sessions")
|
|
146
|
+
def list_sessions(project: str = ""):
|
|
147
|
+
sessions = {}
|
|
148
|
+
for path, note in vault.index.notes.items():
|
|
149
|
+
if "/Sessions/" not in path and not path.startswith("Sessions/"):
|
|
150
|
+
continue
|
|
151
|
+
parts = path.split("/")
|
|
152
|
+
idx = parts.index("Sessions") if "Sessions" in parts else -1
|
|
153
|
+
proj = "/".join(parts[:idx]) if idx > 0 else "(root)"
|
|
154
|
+
if project and proj.lower() != project.lower():
|
|
155
|
+
continue
|
|
156
|
+
if proj not in sessions:
|
|
157
|
+
sessions[proj] = []
|
|
158
|
+
sessions[proj].append({
|
|
159
|
+
"path": path,
|
|
160
|
+
"title": note.title,
|
|
161
|
+
"tags": note.tags,
|
|
162
|
+
"created": note.created,
|
|
163
|
+
"updated": note.updated,
|
|
164
|
+
})
|
|
165
|
+
for proj in sessions:
|
|
166
|
+
sessions[proj].sort(key=lambda s: s["path"], reverse=True)
|
|
167
|
+
return JSONResponse(sessions)
|
|
168
|
+
|
|
169
|
+
@app.get("/api/projects")
|
|
170
|
+
def list_projects():
|
|
171
|
+
projects = set()
|
|
172
|
+
for path in vault.index.notes:
|
|
173
|
+
parts = path.split("/")
|
|
174
|
+
if len(parts) > 1:
|
|
175
|
+
projects.add(parts[0])
|
|
176
|
+
result = []
|
|
177
|
+
for proj in sorted(projects):
|
|
178
|
+
session_count = sum(1 for p in vault.index.notes if p.startswith(f"{proj}/Sessions/"))
|
|
179
|
+
result.append({"name": proj, "session_count": session_count})
|
|
180
|
+
return JSONResponse(result)
|
|
181
|
+
|
|
182
|
+
@app.delete("/api/note/{path:path}")
|
|
183
|
+
def delete_note(path: str):
|
|
184
|
+
if vault.delete(path):
|
|
185
|
+
return JSONResponse({"ok": True})
|
|
186
|
+
return JSONResponse({"error": "Not found"}, 404)
|
|
187
|
+
|
|
188
|
+
@app.post("/api/projects/create")
|
|
189
|
+
async def create_project(request: Request):
|
|
190
|
+
body = await request.json()
|
|
191
|
+
name = body.get("name", "").strip()
|
|
192
|
+
overview = body.get("overview", "").strip()
|
|
193
|
+
if not name:
|
|
194
|
+
return JSONResponse({"error": "Project name required"}, 400)
|
|
195
|
+
path = f"{name}/Knowledge.md"
|
|
196
|
+
if vault.read(path):
|
|
197
|
+
return JSONResponse({"error": "Project already exists"}, 409)
|
|
198
|
+
content = (
|
|
199
|
+
f"# {name}\n\n"
|
|
200
|
+
f"## Overview\n{overview or '(Project description, goals, tech stack)'}\n\n"
|
|
201
|
+
f"## Architecture\n(System design, key components, data flow)\n\n"
|
|
202
|
+
f"## Bugs\n### Known\n\n### Fixed\n\n\n"
|
|
203
|
+
f"## Improvements\n### Planned\n\n### Completed\n\n\n"
|
|
204
|
+
f"## Key Decisions\n(Important architectural or design decisions)\n\n"
|
|
205
|
+
f"## Notes\n(Miscellaneous project knowledge)\n"
|
|
206
|
+
)
|
|
207
|
+
tags = ["project", "knowledge", name.lower().replace(" ", "-")]
|
|
208
|
+
vault.write_note(path, content, tags, {})
|
|
209
|
+
return JSONResponse({"ok": True, "path": path})
|
|
210
|
+
|
|
122
211
|
@app.post("/api/reload")
|
|
123
212
|
def reload():
|
|
124
213
|
vault._load_all()
|
package/package.json
CHANGED
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.4.0"
|
|
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"}
|