kyp-mem 0.6.0 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/kyp_mem/cli.py CHANGED
@@ -133,8 +133,24 @@ def _run_init():
133
133
  print(f" {G}✓{R} Vault: {vault_path}")
134
134
  print(f" {G}✓{R} Config: {CONFIG_FILE}")
135
135
  print()
136
- print(f" {Y}Next step:{R} Connect to Claude Code:")
137
- print(f" {Y}kyp-mem setup-claude{R}")
136
+
137
+ claude_settings = Path.home() / ".claude" / "settings.json"
138
+ already_setup = False
139
+ if claude_settings.exists():
140
+ try:
141
+ cs = json.loads(claude_settings.read_text())
142
+ has_mcp = "kyp-mem" in cs.get("mcpServers", {})
143
+ has_hooks = any("kyp-mem" in str(h) for h in cs.get("hooks", {}).get("Stop", []))
144
+ already_setup = has_mcp and has_hooks
145
+ except (json.JSONDecodeError, KeyError):
146
+ pass
147
+
148
+ if already_setup:
149
+ print(f" {G}✓{R} Claude Code already configured (MCP server + hooks)")
150
+ print(f" {D} Restart Claude Code if you changed the vault path.{R}")
151
+ else:
152
+ print(f" {Y}Next step:{R} Connect to Claude Code:")
153
+ print(f" {Y}kyp-mem setup-claude --global && kyp-mem install-hooks --global{R}")
138
154
  print()
139
155
 
140
156
 
package/kyp_mem/hooks.py CHANGED
@@ -3,13 +3,14 @@
3
3
  import os
4
4
  import sys
5
5
  import json
6
+ import subprocess
6
7
  from datetime import datetime
7
8
  from pathlib import Path
8
9
 
9
10
  SESSION_DIR = Path.home() / ".kyp-mem" / "sessions"
10
11
  CURRENT_SESSION = SESSION_DIR / "current.jsonl"
11
12
 
12
- MIN_ACTIONS = 3
13
+ MIN_ACTIONS = 5
13
14
  CHARS_PER_TOKEN = 4
14
15
 
15
16
  COMMAND_OUTPUT_ESTIMATES = {
@@ -73,9 +74,51 @@ def _record_injection(project, chars):
73
74
  _save_token_stats(stats)
74
75
 
75
76
 
77
+ def _is_subprocess():
78
+ return os.environ.get("KYP_MEM_SUMMARIZING") == "1"
79
+
80
+
81
+ def _extract_session_summary(content, max_chars=800):
82
+ import re
83
+ parts = []
84
+ for heading in ("Summary", "LEARNED", "COMPLETED"):
85
+ m = re.search(rf"(?:^|\n)##\s+{re.escape(heading)}\s*\n(.*?)(?=\n##\s|\Z)", content, re.DOTALL)
86
+ if m:
87
+ text = m.group(1).strip()
88
+ if heading == "Summary":
89
+ parts.append(text)
90
+ else:
91
+ parts.append(f"**{heading.title()}:** {text[:250]}")
92
+ return "\n".join(parts)[:max_chars] if parts else content[:200]
93
+
94
+
95
+ def _build_stats_line(project_name, injected_chars, session_ids):
96
+ try:
97
+ stats = _load_token_stats()
98
+ project_sessions = [s for s in stats.get("sessions", []) if s.get("project") == project_name]
99
+ if not project_sessions:
100
+ return None
101
+ matched = [s for s in project_sessions if s.get("id") in session_ids]
102
+ exploration_tokens = sum(s.get("exploration_tokens", 0) for s in matched)
103
+ injected_tokens = injected_chars // CHARS_PER_TOKEN
104
+ if exploration_tokens == 0:
105
+ return None
106
+ saved = exploration_tokens - injected_tokens
107
+ if saved <= 0:
108
+ return None
109
+ return (
110
+ f"---\n"
111
+ f"*kyp-mem saved ~{saved:,} tokens this session (injected ~{injected_tokens:,} tokens instead of re-exploring ~{exploration_tokens:,})*"
112
+ )
113
+ except Exception:
114
+ return None
115
+
116
+
76
117
  def handle_session_start():
77
118
  """Inject recent session memory into the conversation at session start."""
78
119
  sys.stdin.read()
120
+ if _is_subprocess():
121
+ return
79
122
 
80
123
  cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
81
124
  project_name = Path(cwd).name
@@ -98,8 +141,7 @@ def handle_session_start():
98
141
  return
99
142
 
100
143
  parts = [f"# [kyp-mem] {project_name} — Recent Sessions"]
101
- parts.append(f"Use `kyp_search` or `kyp_project_context` for architecture/project knowledge on demand.")
102
- parts.append("")
144
+ parts.append(f"Use `kyp_search` or `kyp_project_context` for architecture/project knowledge on demand.\n")
103
145
 
104
146
  parts.append(f"## Last {len(sessions)} Sessions")
105
147
  for sp in sessions:
@@ -107,16 +149,16 @@ def handle_session_start():
107
149
  if not note:
108
150
  continue
109
151
  parts.append(f"### {note.title}")
110
- content = note.content
111
- timeline_idx = content.find("## TIMELINE")
112
- if timeline_idx < 0:
113
- timeline_idx = content.find("## Timeline")
114
- if timeline_idx > 0:
115
- content = content[:timeline_idx].strip()
116
- parts.append(content)
152
+ summary = _extract_session_summary(note.content)
153
+ parts.append(summary)
117
154
  parts.append("")
118
155
 
156
+ session_ids = {Path(sp).stem for sp in sessions}
157
+ stats_line = _build_stats_line(project_name, len("\n".join(parts)), session_ids)
158
+ if stats_line:
159
+ parts.append(stats_line)
119
160
  output = "\n".join(parts)
161
+
120
162
  try:
121
163
  _record_injection(project_name, len(output))
122
164
  except Exception:
@@ -128,7 +170,7 @@ def handle_session_start():
128
170
 
129
171
  def handle_user_prompt():
130
172
  raw = sys.stdin.read().strip()
131
- if not raw:
173
+ if not raw or _is_subprocess():
132
174
  return
133
175
  try:
134
176
  data = json.loads(raw)
@@ -153,7 +195,7 @@ def handle_user_prompt():
153
195
 
154
196
  def handle_post_tool_use():
155
197
  raw = sys.stdin.read().strip()
156
- if not raw:
198
+ if not raw or _is_subprocess():
157
199
  return
158
200
  try:
159
201
  data = json.loads(raw)
@@ -405,17 +447,19 @@ def _build_next_steps(files_edited, files_created, commands_classified):
405
447
 
406
448
 
407
449
  def _summarize_with_claude(raw_note, project_name):
408
- """Use Claude to rewrite session sections in plain, human-readable language."""
450
+ """Use Claude CLI to rewrite session sections uses existing Claude Code auth."""
409
451
  try:
410
- from .config import get_session_model
411
- import anthropic
452
+ import shutil
453
+ claude_bin = shutil.which("claude")
454
+ if not claude_bin:
455
+ return None
412
456
 
457
+ from .config import get_session_model
413
458
  model = get_session_model()
414
- client = anthropic.Anthropic()
415
459
 
416
460
  prompt = f"""Rewrite this raw coding session into a structured summary. A future AI agent reads this to pick up where you left off — be precise and technical.
417
461
 
418
- You have: user prompts (the objectives), a timeline of file edits/reads/commands, and raw section data. Synthesize into a dense, specific narrative.
462
+ You have: user prompts (the objectives), a timeline of file edits/reads/commands with their actual content and output. Synthesize into a dense, specific narrative.
419
463
 
420
464
  ## Format rules
421
465
 
@@ -452,18 +496,21 @@ Return ONLY this format (no preamble):
452
496
  Raw session data:
453
497
  {raw_note}"""
454
498
 
455
- response = client.messages.create(
456
- model=model,
457
- max_tokens=1024,
458
- messages=[{"role": "user", "content": prompt}],
499
+ env = os.environ.copy()
500
+ env["KYP_MEM_SUMMARIZING"] = "1"
501
+ result = subprocess.run(
502
+ [claude_bin, "-p", prompt, "--max-turns", "1", "--model", model],
503
+ capture_output=True, text=True, timeout=120, env=env,
459
504
  )
460
- return response.content[0].text.strip()
505
+ if result.returncode == 0 and result.stdout.strip():
506
+ return result.stdout.strip()
507
+ return None
461
508
  except Exception:
462
509
  return None
463
510
 
464
511
 
465
512
  def handle_stop():
466
- if not CURRENT_SESSION.exists():
513
+ if _is_subprocess() or not CURRENT_SESSION.exists():
467
514
  return
468
515
 
469
516
  text = CURRENT_SESSION.read_text().strip()
@@ -639,6 +686,10 @@ def handle_stop():
639
686
  except Exception:
640
687
  pass
641
688
 
689
+ # Delete session file BEFORE summarization so the spawned claude subprocess
690
+ # doesn't pollute it via hooks writing back into current.jsonl
691
+ CURRENT_SESSION.unlink(missing_ok=True)
692
+
642
693
  # Try Claude summarization, fall back to raw sections
643
694
  summarized = _summarize_with_claude(raw_note, project_name)
644
695
 
@@ -707,8 +758,6 @@ def handle_stop():
707
758
  vault = Vault(get_vault_path())
708
759
  vault.write_note(f"{project_name}/Sessions/{session_id}.md", content, tags, {})
709
760
 
710
- CURRENT_SESSION.unlink(missing_ok=True)
711
-
712
761
 
713
762
  def main():
714
763
  if len(sys.argv) > 1 and sys.argv[1] == "stop":
@@ -475,8 +475,9 @@ body.resizing #resize-handle { pointer-events: auto !important; }
475
475
  .graph-header {
476
476
  display: flex; align-items: center; justify-content: space-between;
477
477
  padding: 10px 14px; border-bottom: 1px solid var(--line);
478
- background: var(--bg-2); gap: 12px;
478
+ background: var(--bg-2); gap: 12px; position: relative; z-index: 10;
479
479
  }
480
+ .graph-header .ghost-btn { cursor: pointer; }
480
481
  .graph-header-left { display: flex; align-items: center; gap: 10px; font-size: var(--fz-sm); white-space: nowrap; }
481
482
  .graph-header-right { display: flex; gap: 6px; flex-shrink: 0; }
482
483
  .tool-chip {
@@ -1041,6 +1042,18 @@ function openSession(path) {
1041
1042
  loadNote(path);
1042
1043
  }
1043
1044
 
1045
+ async function deleteSession(path) {
1046
+ if (!confirm('Delete this session?')) return;
1047
+ const res = await fetch(`/api/note/${path}`, { method: 'DELETE' });
1048
+ const data = await res.json();
1049
+ if (data.ok) {
1050
+ activeSession = null;
1051
+ currentPath = null;
1052
+ $('content-area').innerHTML = '<div class="dim" style="padding:40px;text-align:center">Session deleted</div>';
1053
+ refreshAll();
1054
+ }
1055
+ }
1056
+
1044
1057
  // ─── Load Note ───────────────────────────────────────────────────────────────
1045
1058
  async function loadNote(path) {
1046
1059
  currentPath = path;
@@ -1369,7 +1382,10 @@ function renderSessionView(note) {
1369
1382
  ${tagsHtml}
1370
1383
  <span class="dim" style="font-size:var(--fz-xs);margin-left:8px">${note.created || sessionFile}</span>
1371
1384
  </div>
1372
- <button class="ghost-btn" onclick="openEditor('${note.path}')">⌥ edit</button>
1385
+ <div style="display:flex;gap:6px">
1386
+ <button class="ghost-btn" onclick="openEditor('${note.path}')">⌥ edit</button>
1387
+ <button class="ghost-btn" style="color:var(--muted)" onclick="deleteSession('${note.path}')">✕ delete</button>
1388
+ </div>
1373
1389
  </div>
1374
1390
  <h1 style="margin:12px 0 4px;font-size:calc(var(--fz-xl) + 6px);font-weight:500;letter-spacing:-0.01em">
1375
1391
  Session ${sessionFile}
@@ -1428,6 +1444,8 @@ function renderSessionView(note) {
1428
1444
  }
1429
1445
 
1430
1446
  // ─── Graph View ──────────────────────────────────────────────────────────────
1447
+ let _graphMode = 'projects';
1448
+
1431
1449
  function renderGraphView() {
1432
1450
  const area = $('content-area');
1433
1451
  area.innerHTML = `
@@ -1436,24 +1454,19 @@ function renderGraphView() {
1436
1454
  <div class="graph-header">
1437
1455
  <div class="graph-header-left">
1438
1456
  <span class="acc">▦</span>
1439
- <span style="font-weight:500">vault graph</span>
1457
+ <button class="tool-chip graph-mode active" data-mode="projects">projects</button>
1458
+ <button class="tool-chip graph-mode" data-mode="sessions">sessions</button>
1440
1459
  <span class="dim tab-nums" id="graph-stats"></span>
1441
1460
  </div>
1442
1461
  <div class="graph-header-right">
1443
- <button class="tool-chip active">force</button>
1444
- <button class="tool-chip">radial</button>
1445
- <button class="tool-chip">time</button>
1462
+ <button class="tool-chip graph-layout active" data-layout="force">force</button>
1463
+ <button class="tool-chip graph-layout" data-layout="radial">radial</button>
1464
+ <button class="tool-chip graph-layout" data-layout="time">time</button>
1446
1465
  <button class="ghost-btn" id="graph-close">✕</button>
1447
1466
  </div>
1448
1467
  </div>
1449
1468
  <div class="graph-svg-wrap" id="graph-svg-wrap"></div>
1450
- <div class="graph-legend">
1451
- <div class="graph-legend-title">legend</div>
1452
- <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--accent)"></i>notes</div>
1453
- <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--panel-2);border:1px solid var(--muted)"></i>sessions</div>
1454
- <div class="graph-legend-item"><i style="width:12px;height:1px;background:var(--line-2)"></i>link</div>
1455
- <div class="graph-legend-item"><i style="width:12px;height:0;border-top:1px dashed var(--line-2)"></i>session ref</div>
1456
- </div>
1469
+ <div class="graph-legend" id="graph-legend"></div>
1457
1470
  </section>
1458
1471
  <aside class="graph-rail" id="graph-rail">
1459
1472
  <div class="rail-card">
@@ -1470,36 +1483,88 @@ function renderGraphView() {
1470
1483
  </div>
1471
1484
  `;
1472
1485
 
1473
- $('graph-close').addEventListener('click', () => setView('note'));
1486
+ updateGraphLegend();
1474
1487
 
1475
- // Layout toggle buttons
1476
- const chips = document.querySelectorAll('.graph-header-right .tool-chip');
1477
- chips.forEach(chip => {
1488
+ const closeBtn = $('graph-close');
1489
+ if (closeBtn) {
1490
+ closeBtn.addEventListener('click', (e) => {
1491
+ e.stopPropagation();
1492
+ setView('note');
1493
+ if (currentPath) loadNote(currentPath);
1494
+ else $('content-area').innerHTML = '<div style="padding:40px;color:var(--muted)">Select a note from the sidebar</div>';
1495
+ });
1496
+ }
1497
+
1498
+ // Mode toggle (projects / sessions)
1499
+ document.querySelectorAll('.graph-mode').forEach(btn => {
1500
+ btn.addEventListener('click', () => {
1501
+ document.querySelectorAll('.graph-mode').forEach(b => b.classList.remove('active'));
1502
+ btn.classList.add('active');
1503
+ _graphMode = btn.dataset.mode;
1504
+ _graphData = null;
1505
+ updateGraphLegend();
1506
+ const activeLayout = document.querySelector('.graph-layout.active');
1507
+ buildFullGraph(activeLayout ? activeLayout.dataset.layout : 'force');
1508
+ });
1509
+ });
1510
+
1511
+ // Layout toggle (force / radial / time)
1512
+ document.querySelectorAll('.graph-layout').forEach(chip => {
1478
1513
  chip.addEventListener('click', () => {
1479
- chips.forEach(c => c.classList.remove('active'));
1514
+ document.querySelectorAll('.graph-layout').forEach(c => c.classList.remove('active'));
1480
1515
  chip.classList.add('active');
1481
- buildFullGraph(chip.textContent.trim());
1516
+ buildFullGraph(chip.dataset.layout);
1482
1517
  });
1483
1518
  });
1484
1519
 
1485
1520
  buildFullGraph('force');
1486
1521
  }
1487
1522
 
1523
+ function updateGraphLegend() {
1524
+ const legend = $('graph-legend');
1525
+ if (!legend) return;
1526
+ if (_graphMode === 'sessions') {
1527
+ legend.innerHTML = `
1528
+ <div class="graph-legend-title">sessions</div>
1529
+ <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--panel-2);border:1px solid var(--muted)"></i>session</div>
1530
+ <div class="graph-legend-item"><i style="width:12px;height:0;border-top:1px dashed var(--line-2)"></i>temporal link</div>
1531
+ `;
1532
+ } else {
1533
+ legend.innerHTML = `
1534
+ <div class="graph-legend-title">projects</div>
1535
+ <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--accent)"></i>notes</div>
1536
+ <div class="graph-legend-item"><i style="width:12px;height:1px;background:var(--line-2)"></i>wikilink</div>
1537
+ `;
1538
+ }
1539
+ }
1540
+
1488
1541
  async function buildFullGraph(layout) {
1489
1542
  const wrap = $('graph-svg-wrap');
1490
1543
  if (!wrap) return;
1491
1544
  wrap.innerHTML = '';
1492
1545
 
1493
1546
  if (!_graphData) {
1494
- _graphData = await fetchJSON('/api/graph');
1547
+ _graphData = await fetchJSON(`/api/graph?kind=${_graphMode}`);
1548
+ }
1549
+
1550
+ let nodes = _graphData.nodes.map(n => ({ ...n }));
1551
+ let links = _graphData.edges.map(e => ({ ...e }));
1552
+
1553
+ // For sessions mode, create temporal links between consecutive sessions
1554
+ if (_graphMode === 'sessions' && links.length === 0 && nodes.length > 1) {
1555
+ const sorted = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
1556
+ for (let i = 1; i < sorted.length; i++) {
1557
+ links.push({ source: sorted[i - 1].id, target: sorted[i].id });
1558
+ }
1495
1559
  }
1496
1560
 
1497
- const nodes = _graphData.nodes.map(n => ({ ...n }));
1498
- const links = _graphData.edges.map(e => ({ ...e }));
1499
1561
  const nodeMap = new Map(nodes.map(n => [n.id, n]));
1500
1562
 
1501
1563
  $('graph-stats').textContent = `${nodes.length}n · ${links.length}e`;
1502
- if (nodes.length === 0) return;
1564
+ if (nodes.length === 0) {
1565
+ wrap.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:var(--fz-sm)">No nodes to display</div>';
1566
+ return;
1567
+ }
1503
1568
 
1504
1569
  const rect = wrap.getBoundingClientRect();
1505
1570
  const W = rect.width || 800;
@@ -1568,36 +1633,44 @@ async function buildFullGraph(layout) {
1568
1633
  const s = typeof l.source === 'object' ? l.source : nodeMap.get(l.source);
1569
1634
  const t = typeof l.target === 'object' ? l.target : nodeMap.get(l.target);
1570
1635
  if (!s || !t) return;
1571
- const isSession = s.kind === 'session' || t.kind === 'session';
1636
+ const isTemporal = _graphMode === 'sessions';
1572
1637
  edgeGroup.append('line')
1573
1638
  .attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
1574
- .attr('stroke', 'var(--line-2)').attr('stroke-width', 1.2)
1575
- .attr('stroke-opacity', isSession ? 0.35 : 0.6)
1576
- .attr('stroke-dasharray', isSession ? '4 3' : '')
1639
+ .attr('stroke', 'var(--line-2)').attr('stroke-width', isTemporal ? 1 : 1.2)
1640
+ .attr('stroke-opacity', isTemporal ? 0.3 : 0.6)
1641
+ .attr('stroke-dasharray', isTemporal ? '4 3' : '')
1577
1642
  .attr('stroke-linecap', 'round');
1578
1643
  });
1579
1644
 
1580
1645
  // Draw nodes
1581
1646
  const nodeGroup = g.append('g');
1582
1647
  nodes.forEach(n => {
1583
- const r = n.kind === 'session' ? 6 : 10;
1648
+ const isPrimary = _graphMode === 'sessions' ? n.kind === 'session' : n.kind === 'note';
1649
+ const r = isPrimary ? 10 : 6;
1584
1650
  const ng = nodeGroup.append('g').style('cursor', 'pointer');
1585
1651
 
1586
- if (n.kind === 'session') {
1587
- ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1588
- .attr('fill', 'var(--bg-2)').attr('stroke', 'var(--muted)').attr('stroke-width', 1.2);
1589
- } else {
1652
+ if (isPrimary) {
1590
1653
  ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r + 4)
1591
1654
  .attr('fill', 'var(--accent)').attr('fill-opacity', 0.12);
1592
1655
  ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1593
1656
  .attr('fill', 'var(--panel-2)').attr('stroke', 'var(--accent)').attr('stroke-width', 1.5);
1657
+ } else {
1658
+ ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1659
+ .attr('fill', 'var(--bg-2)').attr('stroke', 'var(--muted)').attr('stroke-width', 1.2);
1660
+ }
1661
+
1662
+ let label = n.title;
1663
+ if (_graphMode === 'sessions' && n.kind === 'session') {
1664
+ const m = n.id.match(/(\d{4}-\d{2}-\d{2})_(\d{2})(\d{2})/);
1665
+ label = m ? `${m[1]} ${m[2]}:${m[3]}` : n.title;
1594
1666
  }
1667
+ if (label.length > 22) label = label.substring(0, 20) + '…';
1595
1668
 
1596
1669
  ng.append('text').attr('x', n.x).attr('y', n.y + r + 14)
1597
1670
  .attr('text-anchor', 'middle').attr('font-size', 11)
1598
- .attr('fill', n.kind === 'session' ? 'var(--dim)' : 'var(--muted)').attr('opacity', 0.9)
1671
+ .attr('fill', isPrimary ? 'var(--muted)' : 'var(--dim)').attr('opacity', 0.9)
1599
1672
  .style('font-family', 'JetBrains Mono, monospace')
1600
- .text(n.title.length > 20 ? n.title.substring(0, 18) + '…' : n.title);
1673
+ .text(label);
1601
1674
 
1602
1675
  ng.on('click', () => {
1603
1676
  updateGraphRail(n, links, nodes);
package/kyp_mem/ui.py CHANGED
@@ -30,13 +30,17 @@ def create_app(vault_path: str = None) -> FastAPI:
30
30
  return JSONResponse(vault.get_stats())
31
31
 
32
32
  @app.get("/api/graph")
33
- def graph():
33
+ def graph(kind: str = "all"):
34
34
  nodes = []
35
35
  edges = []
36
36
  seen_edges = set()
37
37
  for path, note in vault.index.notes.items():
38
- kind = "session" if "/Sessions/" in path else "note"
39
- nodes.append({"id": path, "title": note.title, "kind": kind, "tags": note.tags})
38
+ node_kind = "session" if "/Sessions/" in path else "note"
39
+ if kind == "projects" and node_kind == "session":
40
+ continue
41
+ if kind == "sessions" and node_kind == "note":
42
+ continue
43
+ nodes.append({"id": path, "title": note.title, "kind": node_kind, "tags": note.tags})
40
44
  for link in (note.links or []):
41
45
  target = None
42
46
  link_lower = link.lower()
@@ -46,6 +50,11 @@ def create_app(vault_path: str = None) -> FastAPI:
46
50
  target = p
47
51
  break
48
52
  if target and target != path:
53
+ target_kind = "session" if "/Sessions/" in target else "note"
54
+ if kind == "projects" and target_kind == "session":
55
+ continue
56
+ if kind == "sessions" and target_kind == "note":
57
+ continue
49
58
  key = tuple(sorted([path, target]))
50
59
  if key not in seen_edges:
51
60
  seen_edges.add(key)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyp-mem",
3
- "version": "0.6.0",
3
+ "version": "0.6.4",
4
4
  "description": "Know Your Project — Persistent & Session level knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI.",
5
5
  "bin": {
6
6
  "kyp-mem": "bin/cli.mjs"
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kyp-mem"
7
- version = "0.4.2"
7
+ version = "0.6.4"
8
8
  description = "Know Your Project — Persistent knowledge base for AI agents. MCP-powered with wikilinks, backlinks, auto-learning, and neon web UI."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}