kyp-mem 0.6.0 → 0.6.1

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
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  SESSION_DIR = Path.home() / ".kyp-mem" / "sessions"
10
10
  CURRENT_SESSION = SESSION_DIR / "current.jsonl"
11
11
 
12
- MIN_ACTIONS = 3
12
+ MIN_ACTIONS = 5
13
13
  CHARS_PER_TOKEN = 4
14
14
 
15
15
  COMMAND_OUTPUT_ESTIMATES = {
@@ -405,17 +405,19 @@ def _build_next_steps(files_edited, files_created, commands_classified):
405
405
 
406
406
 
407
407
  def _summarize_with_claude(raw_note, project_name):
408
- """Use Claude to rewrite session sections in plain, human-readable language."""
408
+ """Use Claude CLI to rewrite session sections uses existing Claude Code auth."""
409
409
  try:
410
- from .config import get_session_model
411
- import anthropic
410
+ import shutil
411
+ claude_bin = shutil.which("claude")
412
+ if not claude_bin:
413
+ return None
412
414
 
415
+ from .config import get_session_model
413
416
  model = get_session_model()
414
- client = anthropic.Anthropic()
415
417
 
416
418
  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
419
 
418
- You have: user prompts (the objectives), a timeline of file edits/reads/commands, and raw section data. Synthesize into a dense, specific narrative.
420
+ 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
421
 
420
422
  ## Format rules
421
423
 
@@ -452,12 +454,13 @@ Return ONLY this format (no preamble):
452
454
  Raw session data:
453
455
  {raw_note}"""
454
456
 
455
- response = client.messages.create(
456
- model=model,
457
- max_tokens=1024,
458
- messages=[{"role": "user", "content": prompt}],
457
+ result = subprocess.run(
458
+ [claude_bin, "-p", prompt, "--max-turns", "1", "--model", model],
459
+ capture_output=True, text=True, timeout=120,
459
460
  )
460
- return response.content[0].text.strip()
461
+ if result.returncode == 0 and result.stdout.strip():
462
+ return result.stdout.strip()
463
+ return None
461
464
  except Exception:
462
465
  return None
463
466
 
@@ -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 {
@@ -1428,6 +1429,8 @@ function renderSessionView(note) {
1428
1429
  }
1429
1430
 
1430
1431
  // ─── Graph View ──────────────────────────────────────────────────────────────
1432
+ let _graphMode = 'projects';
1433
+
1431
1434
  function renderGraphView() {
1432
1435
  const area = $('content-area');
1433
1436
  area.innerHTML = `
@@ -1436,24 +1439,19 @@ function renderGraphView() {
1436
1439
  <div class="graph-header">
1437
1440
  <div class="graph-header-left">
1438
1441
  <span class="acc">▦</span>
1439
- <span style="font-weight:500">vault graph</span>
1442
+ <button class="tool-chip graph-mode active" data-mode="projects">projects</button>
1443
+ <button class="tool-chip graph-mode" data-mode="sessions">sessions</button>
1440
1444
  <span class="dim tab-nums" id="graph-stats"></span>
1441
1445
  </div>
1442
1446
  <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>
1447
+ <button class="tool-chip graph-layout active" data-layout="force">force</button>
1448
+ <button class="tool-chip graph-layout" data-layout="radial">radial</button>
1449
+ <button class="tool-chip graph-layout" data-layout="time">time</button>
1446
1450
  <button class="ghost-btn" id="graph-close">✕</button>
1447
1451
  </div>
1448
1452
  </div>
1449
1453
  <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>
1454
+ <div class="graph-legend" id="graph-legend"></div>
1457
1455
  </section>
1458
1456
  <aside class="graph-rail" id="graph-rail">
1459
1457
  <div class="rail-card">
@@ -1470,36 +1468,88 @@ function renderGraphView() {
1470
1468
  </div>
1471
1469
  `;
1472
1470
 
1473
- $('graph-close').addEventListener('click', () => setView('note'));
1471
+ updateGraphLegend();
1472
+
1473
+ const closeBtn = $('graph-close');
1474
+ if (closeBtn) {
1475
+ closeBtn.addEventListener('click', (e) => {
1476
+ e.stopPropagation();
1477
+ setView('note');
1478
+ if (currentPath) loadNote(currentPath);
1479
+ else $('content-area').innerHTML = '<div style="padding:40px;color:var(--muted)">Select a note from the sidebar</div>';
1480
+ });
1481
+ }
1474
1482
 
1475
- // Layout toggle buttons
1476
- const chips = document.querySelectorAll('.graph-header-right .tool-chip');
1477
- chips.forEach(chip => {
1483
+ // Mode toggle (projects / sessions)
1484
+ document.querySelectorAll('.graph-mode').forEach(btn => {
1485
+ btn.addEventListener('click', () => {
1486
+ document.querySelectorAll('.graph-mode').forEach(b => b.classList.remove('active'));
1487
+ btn.classList.add('active');
1488
+ _graphMode = btn.dataset.mode;
1489
+ _graphData = null;
1490
+ updateGraphLegend();
1491
+ const activeLayout = document.querySelector('.graph-layout.active');
1492
+ buildFullGraph(activeLayout ? activeLayout.dataset.layout : 'force');
1493
+ });
1494
+ });
1495
+
1496
+ // Layout toggle (force / radial / time)
1497
+ document.querySelectorAll('.graph-layout').forEach(chip => {
1478
1498
  chip.addEventListener('click', () => {
1479
- chips.forEach(c => c.classList.remove('active'));
1499
+ document.querySelectorAll('.graph-layout').forEach(c => c.classList.remove('active'));
1480
1500
  chip.classList.add('active');
1481
- buildFullGraph(chip.textContent.trim());
1501
+ buildFullGraph(chip.dataset.layout);
1482
1502
  });
1483
1503
  });
1484
1504
 
1485
1505
  buildFullGraph('force');
1486
1506
  }
1487
1507
 
1508
+ function updateGraphLegend() {
1509
+ const legend = $('graph-legend');
1510
+ if (!legend) return;
1511
+ if (_graphMode === 'sessions') {
1512
+ legend.innerHTML = `
1513
+ <div class="graph-legend-title">sessions</div>
1514
+ <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>
1515
+ <div class="graph-legend-item"><i style="width:12px;height:0;border-top:1px dashed var(--line-2)"></i>temporal link</div>
1516
+ `;
1517
+ } else {
1518
+ legend.innerHTML = `
1519
+ <div class="graph-legend-title">projects</div>
1520
+ <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--accent)"></i>notes</div>
1521
+ <div class="graph-legend-item"><i style="width:12px;height:1px;background:var(--line-2)"></i>wikilink</div>
1522
+ `;
1523
+ }
1524
+ }
1525
+
1488
1526
  async function buildFullGraph(layout) {
1489
1527
  const wrap = $('graph-svg-wrap');
1490
1528
  if (!wrap) return;
1491
1529
  wrap.innerHTML = '';
1492
1530
 
1493
1531
  if (!_graphData) {
1494
- _graphData = await fetchJSON('/api/graph');
1532
+ _graphData = await fetchJSON(`/api/graph?kind=${_graphMode}`);
1533
+ }
1534
+
1535
+ let nodes = _graphData.nodes.map(n => ({ ...n }));
1536
+ let links = _graphData.edges.map(e => ({ ...e }));
1537
+
1538
+ // For sessions mode, create temporal links between consecutive sessions
1539
+ if (_graphMode === 'sessions' && links.length === 0 && nodes.length > 1) {
1540
+ const sorted = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
1541
+ for (let i = 1; i < sorted.length; i++) {
1542
+ links.push({ source: sorted[i - 1].id, target: sorted[i].id });
1543
+ }
1495
1544
  }
1496
1545
 
1497
- const nodes = _graphData.nodes.map(n => ({ ...n }));
1498
- const links = _graphData.edges.map(e => ({ ...e }));
1499
1546
  const nodeMap = new Map(nodes.map(n => [n.id, n]));
1500
1547
 
1501
1548
  $('graph-stats').textContent = `${nodes.length}n · ${links.length}e`;
1502
- if (nodes.length === 0) return;
1549
+ if (nodes.length === 0) {
1550
+ 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>';
1551
+ return;
1552
+ }
1503
1553
 
1504
1554
  const rect = wrap.getBoundingClientRect();
1505
1555
  const W = rect.width || 800;
@@ -1568,36 +1618,44 @@ async function buildFullGraph(layout) {
1568
1618
  const s = typeof l.source === 'object' ? l.source : nodeMap.get(l.source);
1569
1619
  const t = typeof l.target === 'object' ? l.target : nodeMap.get(l.target);
1570
1620
  if (!s || !t) return;
1571
- const isSession = s.kind === 'session' || t.kind === 'session';
1621
+ const isTemporal = _graphMode === 'sessions';
1572
1622
  edgeGroup.append('line')
1573
1623
  .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' : '')
1624
+ .attr('stroke', 'var(--line-2)').attr('stroke-width', isTemporal ? 1 : 1.2)
1625
+ .attr('stroke-opacity', isTemporal ? 0.3 : 0.6)
1626
+ .attr('stroke-dasharray', isTemporal ? '4 3' : '')
1577
1627
  .attr('stroke-linecap', 'round');
1578
1628
  });
1579
1629
 
1580
1630
  // Draw nodes
1581
1631
  const nodeGroup = g.append('g');
1582
1632
  nodes.forEach(n => {
1583
- const r = n.kind === 'session' ? 6 : 10;
1633
+ const isPrimary = _graphMode === 'sessions' ? n.kind === 'session' : n.kind === 'note';
1634
+ const r = isPrimary ? 10 : 6;
1584
1635
  const ng = nodeGroup.append('g').style('cursor', 'pointer');
1585
1636
 
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 {
1637
+ if (isPrimary) {
1590
1638
  ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r + 4)
1591
1639
  .attr('fill', 'var(--accent)').attr('fill-opacity', 0.12);
1592
1640
  ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1593
1641
  .attr('fill', 'var(--panel-2)').attr('stroke', 'var(--accent)').attr('stroke-width', 1.5);
1642
+ } else {
1643
+ ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1644
+ .attr('fill', 'var(--bg-2)').attr('stroke', 'var(--muted)').attr('stroke-width', 1.2);
1645
+ }
1646
+
1647
+ let label = n.title;
1648
+ if (_graphMode === 'sessions' && n.kind === 'session') {
1649
+ const m = n.id.match(/(\d{4}-\d{2}-\d{2})_(\d{2})(\d{2})/);
1650
+ label = m ? `${m[1]} ${m[2]}:${m[3]}` : n.title;
1594
1651
  }
1652
+ if (label.length > 22) label = label.substring(0, 20) + '…';
1595
1653
 
1596
1654
  ng.append('text').attr('x', n.x).attr('y', n.y + r + 14)
1597
1655
  .attr('text-anchor', 'middle').attr('font-size', 11)
1598
- .attr('fill', n.kind === 'session' ? 'var(--dim)' : 'var(--muted)').attr('opacity', 0.9)
1656
+ .attr('fill', isPrimary ? 'var(--muted)' : 'var(--dim)').attr('opacity', 0.9)
1599
1657
  .style('font-family', 'JetBrains Mono, monospace')
1600
- .text(n.title.length > 20 ? n.title.substring(0, 18) + '…' : n.title);
1658
+ .text(label);
1601
1659
 
1602
1660
  ng.on('click', () => {
1603
1661
  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.1",
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"