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 +18 -2
- package/kyp_mem/hooks.py +14 -11
- package/kyp_mem/static/index.html +91 -33
- package/kyp_mem/ui.py +12 -3
- package/package.json +1 -1
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
|
-
|
|
137
|
-
|
|
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 =
|
|
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
|
|
408
|
+
"""Use Claude CLI to rewrite session sections — uses existing Claude Code auth."""
|
|
409
409
|
try:
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
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
|
-
|
|
456
|
-
model
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
//
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
1499
|
+
document.querySelectorAll('.graph-layout').forEach(c => c.classList.remove('active'));
|
|
1480
1500
|
chip.classList.add('active');
|
|
1481
|
-
buildFullGraph(chip.
|
|
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(
|
|
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)
|
|
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
|
|
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',
|
|
1576
|
-
.attr('stroke-dasharray',
|
|
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
|
|
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 (
|
|
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',
|
|
1656
|
+
.attr('fill', isPrimary ? 'var(--muted)' : 'var(--dim)').attr('opacity', 0.9)
|
|
1599
1657
|
.style('font-family', 'JetBrains Mono, monospace')
|
|
1600
|
-
.text(
|
|
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
|
-
|
|
39
|
-
|
|
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.
|
|
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"
|