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 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
- parts.append("")
108
+ parts.append(f"- Created `{Path(f).name}`")
109
+ parts.append("")
83
110
 
84
- if commands:
85
- parts.append("## Commands Run")
86
- for cmd in commands[:25]:
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: #08080f;
14
- --bg-primary: #0c0c16;
15
- --bg-secondary: #0e0e1a;
16
- --bg-tertiary: #070710;
17
- --bg-hover: #121224;
18
- --bg-active: #181836;
19
- --bg-card: #101020;
20
- --bg-surface: #0a0a15;
21
-
22
- --neon-cyan: #00e8df;
23
- --neon-green: #34d399;
24
- --neon-magenta: #e879f9;
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: #fb923c;
27
- --neon-yellow: #fbbf24;
28
- --neon-blue: #60a5fa;
29
- --neon-red: #f87171;
26
+ --neon-orange: #e8935a;
27
+ --neon-yellow: #d4a853;
28
+ --neon-blue: #7da8d4;
29
+ --neon-red: #d47171;
30
30
 
31
- --glow-cyan: 0 0 8px #00e8df30;
32
- --glow-green: 0 0 8px #34d39930;
33
- --glow-magenta: 0 0 8px #e879f930;
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: #d4d4e8;
36
- --text-secondary: #7a7a9a;
37
- --text-muted: #3e3e5c;
38
- --border: #16162a;
39
- --border-subtle: #121225;
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(0,232,223,0.015) 1px, transparent 1px),
66
- linear-gradient(90deg, rgba(0,232,223,0.015) 1px, transparent 1px);
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(0,232,223,0.2);
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(0,232,223,0.3);
194
+ border-color: rgba(217,119,87,0.3);
195
195
  color: var(--neon-cyan);
196
- background: rgba(0,232,223,0.05);
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(0,232,223,0.3);
233
- box-shadow: 0 0 16px rgba(0,232,223,0.08);
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(0,232,223,0.05);
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(0,232,223,0.08);
526
+ background: rgba(217,119,87,0.08);
527
527
  color: var(--neon-cyan);
528
- border: 1px solid rgba(0,232,223,0.2);
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(0,232,223,0.15); }
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(0,232,223,0.08);
569
+ background: rgba(217,119,87,0.08);
570
570
  color: var(--neon-cyan);
571
- border-color: rgba(0,232,223,0.2);
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(0,232,223,0.08);
640
+ background: rgba(217,119,87,0.08);
641
641
  color: var(--neon-cyan);
642
- border-color: rgba(0,232,223,0.2);
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(0,232,223,0.4);
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(0,232,223,0.5));
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(0,232,223,0.5));
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(0,232,223,0.1);
1218
+ background: rgba(217,119,87,0.1);
1029
1219
  color: var(--neon-cyan);
1030
- border-color: rgba(0,232,223,0.3);
1220
+ border-color: rgba(217,119,87,0.3);
1031
1221
  }
1032
1222
 
1033
- .edit-btn.save:hover { background: rgba(0,232,223,0.2); }
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">&#9906;</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</div>
1360
+ <div class="rp-section-title">Local Graph <span class="graph-size-controls"><button class="graph-size-btn" id="graph-shrink" title="Shrink graph">&minus;</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
- item.innerHTML = `<span class="arrow open">&#9654;</span><span class="icon folder-icon">&#9776;</span><span class="name">${node.name}</span>`;
1599
+
1600
+ if (isSessionsFolder) {
1601
+ item.innerHTML = `<span class="arrow open">&#9654;</span><span class="icon session-folder-icon">&#9716;</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">&#9654;</span><span class="icon folder-icon">&#9776;</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
- item.innerHTML = `<span class="arrow" style="visibility:hidden">&#9654;</span><span class="icon note-icon">&#9671;</span><span class="name">${node.name.replace('.md', '')}</span>`;
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">&#9654;</span><span class="icon note-icon">&#9671;</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">&#9716; SESSION &middot; ${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(path => {
1642
- if (!nodes.has(path)) {
1643
- const n = allNotes[path];
1644
- nodes.set(path, { id: path, title: n ? n.title : path, active: false });
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: path, target: note.path });
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 = '<svg></svg>';
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(50))
1669
- .force('charge', d3.forceManyBody().strength(-100))
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(22));
2087
+ .force('collision', d3.forceCollide().radius(25));
1672
2088
 
1673
- const link = svg.selectAll('.graph-link')
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 = svg.selectAll('.graph-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 ? 5 : 3.5);
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
- link
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 => `translate(${d.x},${d.y})`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyp-mem",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Know Your Project — Persistent 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.3.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"}