kyp-mem 0.6.7 → 0.6.8
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/static/index.html +89 -32
- package/kyp_mem/ui.py +40 -24
- package/kyp_mem/vault.py +14 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -268,6 +268,9 @@ body.resizing #resize-handle { pointer-events: auto !important; }
|
|
|
268
268
|
.tree-folder .tf-icon { color: var(--muted); font-size: 11px; }
|
|
269
269
|
.tree-folder .tf-label { flex: 1; color: var(--fg); }
|
|
270
270
|
.tree-folder .tf-count { font-size: var(--fz-xs); color: var(--dim); font-variant-numeric: tabular-nums; }
|
|
271
|
+
.tree-folder .tf-graph-btn { font-size: 11px; color: var(--dim); opacity: 0; transition: opacity 0.15s; padding: 0 2px; }
|
|
272
|
+
.tree-folder:hover .tf-graph-btn { opacity: 1; }
|
|
273
|
+
.tree-folder .tf-graph-btn:hover { color: var(--accent); }
|
|
271
274
|
|
|
272
275
|
/* Sidebar rows */
|
|
273
276
|
.sidebar-row {
|
|
@@ -720,7 +723,7 @@ let _graphData = null;
|
|
|
720
723
|
let searchTimeout, sessionSearchTimeout;
|
|
721
724
|
|
|
722
725
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
723
|
-
async function fetchJSON(url) { const r = await fetch(url); return r.json(); }
|
|
726
|
+
async function fetchJSON(url) { const r = await fetch(url, { cache: 'no-store' }); return r.json(); }
|
|
724
727
|
|
|
725
728
|
function $(id) { return document.getElementById(id); }
|
|
726
729
|
|
|
@@ -790,7 +793,7 @@ function setView(v) {
|
|
|
790
793
|
$('view-switch').addEventListener('click', e => {
|
|
791
794
|
if (e.target.dataset.view) {
|
|
792
795
|
setView(e.target.dataset.view);
|
|
793
|
-
if (view === 'graph') { _graphData = null; renderGraphView(); }
|
|
796
|
+
if (view === 'graph') { _graphProject = null; _graphData = null; renderGraphView(); }
|
|
794
797
|
else if (view === 'session' && activeSession) loadNote(activeSession);
|
|
795
798
|
else if (view === 'note' && currentPath) loadNote(currentPath);
|
|
796
799
|
}
|
|
@@ -886,7 +889,7 @@ function renderSidebar(sessionsData) {
|
|
|
886
889
|
const group = document.createElement('div');
|
|
887
890
|
const folder = document.createElement('div');
|
|
888
891
|
folder.className = 'tree-folder';
|
|
889
|
-
folder.innerHTML = `<span class="tf-arrow">▾</span><span class="tf-icon">≡</span><span class="tf-label">${project}</span><span class="tf-count">${sessions.length}</span>`;
|
|
892
|
+
folder.innerHTML = `<span class="tf-arrow">▾</span><span class="tf-icon">≡</span><span class="tf-label">${project}</span><span class="tf-count">${sessions.length}</span><button class="tf-graph-btn ghost-btn" title="Open graph for ${project}" data-project="${project}">▦</button>`;
|
|
890
893
|
|
|
891
894
|
const list = document.createElement('div');
|
|
892
895
|
list.style.cssText = 'display:flex;flex-direction:column;gap:1px;padding-left:16px;margin-top:2px;';
|
|
@@ -902,6 +905,14 @@ function renderSidebar(sessionsData) {
|
|
|
902
905
|
list.appendChild(row);
|
|
903
906
|
});
|
|
904
907
|
|
|
908
|
+
folder.querySelector('.tf-graph-btn').addEventListener('click', (e) => {
|
|
909
|
+
e.stopPropagation();
|
|
910
|
+
_graphProject = project;
|
|
911
|
+
_graphData = null;
|
|
912
|
+
setView('graph');
|
|
913
|
+
renderGraphView();
|
|
914
|
+
});
|
|
915
|
+
|
|
905
916
|
folder.addEventListener('click', () => {
|
|
906
917
|
const arrow = folder.querySelector('.tf-arrow');
|
|
907
918
|
const isOpen = arrow.textContent === '▾';
|
|
@@ -955,13 +966,14 @@ function renderSidebar(sessionsData) {
|
|
|
955
966
|
|
|
956
967
|
function renderProjectTree(container) {
|
|
957
968
|
if (!treeData) return;
|
|
958
|
-
function walk(node, parent) {
|
|
969
|
+
function walk(node, parent, depth) {
|
|
959
970
|
if (node.type === 'folder' && node.name !== 'vault') {
|
|
960
971
|
if (node.name === 'Sessions') return;
|
|
961
972
|
const group = document.createElement('div');
|
|
962
973
|
const folder = document.createElement('div');
|
|
963
974
|
folder.className = 'tree-folder';
|
|
964
|
-
|
|
975
|
+
const isTopLevel = depth === 0;
|
|
976
|
+
folder.innerHTML = `<span class="tf-arrow">▾</span><span class="tf-icon">≡</span><span class="tf-label">${node.name}</span>${isTopLevel ? `<button class="tf-graph-btn ghost-btn" title="Open graph for ${node.name}" data-project="${node.name}">▦</button>` : ''}`;
|
|
965
977
|
const children = document.createElement('div');
|
|
966
978
|
children.style.cssText = 'display:flex;flex-direction:column;gap:1px;padding-left:16px;margin-top:2px;';
|
|
967
979
|
|
|
@@ -972,10 +984,20 @@ function renderProjectTree(container) {
|
|
|
972
984
|
children.style.display = isOpen ? 'none' : 'flex';
|
|
973
985
|
});
|
|
974
986
|
|
|
987
|
+
if (isTopLevel) {
|
|
988
|
+
folder.querySelector('.tf-graph-btn').addEventListener('click', (e) => {
|
|
989
|
+
e.stopPropagation();
|
|
990
|
+
_graphProject = node.name;
|
|
991
|
+
_graphData = null;
|
|
992
|
+
setView('graph');
|
|
993
|
+
renderGraphView();
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
975
997
|
group.appendChild(folder);
|
|
976
998
|
group.appendChild(children);
|
|
977
999
|
parent.appendChild(group);
|
|
978
|
-
(node.children || []).forEach(c => walk(c, children));
|
|
1000
|
+
(node.children || []).forEach(c => walk(c, children, depth + 1));
|
|
979
1001
|
} else if (node.type === 'note') {
|
|
980
1002
|
const row = document.createElement('button');
|
|
981
1003
|
row.className = 'sidebar-row';
|
|
@@ -985,10 +1007,10 @@ function renderProjectTree(container) {
|
|
|
985
1007
|
row.addEventListener('click', () => { setView('note'); loadNote(node.path); });
|
|
986
1008
|
parent.appendChild(row);
|
|
987
1009
|
} else if (node.children) {
|
|
988
|
-
node.children.forEach(c => walk(c, parent));
|
|
1010
|
+
node.children.forEach(c => walk(c, parent, depth));
|
|
989
1011
|
}
|
|
990
1012
|
}
|
|
991
|
-
walk(treeData, container);
|
|
1013
|
+
walk(treeData, container, 0);
|
|
992
1014
|
}
|
|
993
1015
|
|
|
994
1016
|
function renderTagCloud(container) {
|
|
@@ -1158,6 +1180,8 @@ function renderRail(note) {
|
|
|
1158
1180
|
rail.appendChild(graphCard);
|
|
1159
1181
|
|
|
1160
1182
|
graphCard.querySelector('#rail-graph-expand')?.addEventListener('click', () => {
|
|
1183
|
+
const parts = (note.path || '').split('/');
|
|
1184
|
+
_graphProject = parts.length > 1 ? parts[0] : null;
|
|
1161
1185
|
_graphData = null;
|
|
1162
1186
|
setView('graph');
|
|
1163
1187
|
renderGraphView();
|
|
@@ -1445,6 +1469,7 @@ function renderSessionView(note) {
|
|
|
1445
1469
|
|
|
1446
1470
|
// ─── Graph View ──────────────────────────────────────────────────────────────
|
|
1447
1471
|
let _graphMode = 'projects';
|
|
1472
|
+
let _graphProject = null;
|
|
1448
1473
|
|
|
1449
1474
|
function renderGraphView() {
|
|
1450
1475
|
const area = $('content-area');
|
|
@@ -1457,6 +1482,10 @@ function renderGraphView() {
|
|
|
1457
1482
|
<button class="tool-chip graph-mode active" data-mode="projects">projects</button>
|
|
1458
1483
|
<button class="tool-chip graph-mode" data-mode="sessions">sessions</button>
|
|
1459
1484
|
<span class="dim tab-nums" id="graph-stats"></span>
|
|
1485
|
+
<span id="graph-project-filter" style="display:none;font-size:var(--fz-sm);color:var(--accent);margin-left:4px">
|
|
1486
|
+
<span id="graph-project-name"></span>
|
|
1487
|
+
<button class="ghost-btn" id="graph-clear-project" style="color:var(--muted);margin-left:4px;font-size:var(--fz-xs)" title="Show all projects">✕</button>
|
|
1488
|
+
</span>
|
|
1460
1489
|
</div>
|
|
1461
1490
|
<div class="graph-header-right">
|
|
1462
1491
|
<button class="tool-chip graph-layout active" data-layout="force">force</button>
|
|
@@ -1484,17 +1513,28 @@ function renderGraphView() {
|
|
|
1484
1513
|
`;
|
|
1485
1514
|
|
|
1486
1515
|
updateGraphLegend();
|
|
1516
|
+
updateGraphProjectFilter();
|
|
1487
1517
|
|
|
1488
1518
|
const closeBtn = $('graph-close');
|
|
1489
1519
|
if (closeBtn) {
|
|
1490
1520
|
closeBtn.addEventListener('click', (e) => {
|
|
1491
1521
|
e.stopPropagation();
|
|
1522
|
+
_graphProject = null;
|
|
1492
1523
|
setView('note');
|
|
1493
1524
|
if (currentPath) loadNote(currentPath);
|
|
1494
1525
|
else $('content-area').innerHTML = '<div style="padding:40px;color:var(--muted)">Select a note from the sidebar</div>';
|
|
1495
1526
|
});
|
|
1496
1527
|
}
|
|
1497
1528
|
|
|
1529
|
+
// Clear project filter
|
|
1530
|
+
$('graph-clear-project')?.addEventListener('click', () => {
|
|
1531
|
+
_graphProject = null;
|
|
1532
|
+
_graphData = null;
|
|
1533
|
+
updateGraphProjectFilter();
|
|
1534
|
+
const activeLayout = document.querySelector('.graph-layout.active');
|
|
1535
|
+
buildFullGraph(activeLayout ? activeLayout.dataset.layout : 'force');
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1498
1538
|
// Mode toggle (projects / sessions)
|
|
1499
1539
|
document.querySelectorAll('.graph-mode').forEach(btn => {
|
|
1500
1540
|
btn.addEventListener('click', () => {
|
|
@@ -1520,6 +1560,17 @@ function renderGraphView() {
|
|
|
1520
1560
|
buildFullGraph('force');
|
|
1521
1561
|
}
|
|
1522
1562
|
|
|
1563
|
+
function updateGraphProjectFilter() {
|
|
1564
|
+
const el = $('graph-project-filter');
|
|
1565
|
+
if (!el) return;
|
|
1566
|
+
if (_graphProject) {
|
|
1567
|
+
$('graph-project-name').textContent = `› ${_graphProject}`;
|
|
1568
|
+
el.style.display = 'inline';
|
|
1569
|
+
} else {
|
|
1570
|
+
el.style.display = 'none';
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1523
1574
|
function updateGraphLegend() {
|
|
1524
1575
|
const legend = $('graph-legend');
|
|
1525
1576
|
if (!legend) return;
|
|
@@ -1544,7 +1595,9 @@ async function buildFullGraph(layout) {
|
|
|
1544
1595
|
wrap.innerHTML = '';
|
|
1545
1596
|
|
|
1546
1597
|
if (!_graphData) {
|
|
1547
|
-
|
|
1598
|
+
let url = `/api/graph?kind=${_graphMode}`;
|
|
1599
|
+
if (_graphProject) url += `&project=${encodeURIComponent(_graphProject)}`;
|
|
1600
|
+
_graphData = await fetchJSON(url);
|
|
1548
1601
|
}
|
|
1549
1602
|
|
|
1550
1603
|
let nodes = _graphData.nodes.map(n => ({ ...n }));
|
|
@@ -1931,30 +1984,34 @@ async function init() {
|
|
|
1931
1984
|
const density = localStorage.getItem('kyp-density') || 'regular';
|
|
1932
1985
|
document.documentElement.setAttribute('data-density', density);
|
|
1933
1986
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1987
|
+
try {
|
|
1988
|
+
const [rawTree, sessionsData, stats] = await Promise.all([
|
|
1989
|
+
fetchJSON('/api/tree'), fetchJSON('/api/sessions'), fetchJSON('/api/stats'),
|
|
1990
|
+
]);
|
|
1991
|
+
treeData = rawTree;
|
|
1992
|
+
|
|
1993
|
+
function walk(node) {
|
|
1994
|
+
if (node.type === 'note') allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
|
|
1995
|
+
(node.children || []).forEach(walk);
|
|
1996
|
+
}
|
|
1997
|
+
walk(treeData);
|
|
1998
|
+
|
|
1999
|
+
renderSidebar(sessionsData);
|
|
2000
|
+
$('stat-notes').textContent = stats.notes;
|
|
2001
|
+
$('stat-folders').textContent = stats.folders;
|
|
2002
|
+
|
|
2003
|
+
// Enrich notes with tags in background (don't block sidebar render)
|
|
2004
|
+
const promises = Object.keys(allNotes).map(async path => {
|
|
2005
|
+
try {
|
|
2006
|
+
const note = await fetchJSON(`/api/note/${path}`);
|
|
2007
|
+
if (!note.error) allNotes[path] = { title: note.title, tags: note.tags || [] };
|
|
2008
|
+
} catch {}
|
|
2009
|
+
});
|
|
2010
|
+
Promise.all(promises).then(() => loadTokenEconomics());
|
|
2011
|
+
} catch (e) {
|
|
2012
|
+
console.error('KYP-MEM init failed:', e);
|
|
2013
|
+
$('sidebar-scroll').innerHTML = `<div style="padding:12px;color:#f66;font-size:12px">Init error: ${e.message}<br>Check console for details</div>`;
|
|
1942
2014
|
}
|
|
1943
|
-
walk(treeData);
|
|
1944
|
-
|
|
1945
|
-
// Enrich notes with tags
|
|
1946
|
-
const promises = Object.keys(allNotes).map(async path => {
|
|
1947
|
-
try {
|
|
1948
|
-
const note = await fetchJSON(`/api/note/${path}`);
|
|
1949
|
-
if (!note.error) allNotes[path] = { title: note.title, tags: note.tags || [] };
|
|
1950
|
-
} catch {}
|
|
1951
|
-
});
|
|
1952
|
-
await Promise.all(promises);
|
|
1953
|
-
|
|
1954
|
-
renderSidebar(sessionsData);
|
|
1955
|
-
$('stat-notes').textContent = stats.notes;
|
|
1956
|
-
$('stat-folders').textContent = stats.folders;
|
|
1957
|
-
loadTokenEconomics();
|
|
1958
2015
|
|
|
1959
2016
|
// Show empty state
|
|
1960
2017
|
$('content-area').innerHTML = `
|
package/kyp_mem/ui.py
CHANGED
|
@@ -16,6 +16,18 @@ def create_app(vault_path: str = None) -> FastAPI:
|
|
|
16
16
|
vault = Vault(vault_path)
|
|
17
17
|
app = FastAPI(title="KYP-MEM")
|
|
18
18
|
|
|
19
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
20
|
+
from starlette.responses import Response
|
|
21
|
+
|
|
22
|
+
class NoCacheMiddleware(BaseHTTPMiddleware):
|
|
23
|
+
async def dispatch(self, request, call_next):
|
|
24
|
+
response: Response = await call_next(request)
|
|
25
|
+
if request.url.path.startswith("/api/"):
|
|
26
|
+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
|
27
|
+
return response
|
|
28
|
+
|
|
29
|
+
app.add_middleware(NoCacheMiddleware)
|
|
30
|
+
|
|
19
31
|
@app.get("/")
|
|
20
32
|
def index():
|
|
21
33
|
html_path = Path(__file__).parent / "static" / "index.html"
|
|
@@ -23,6 +35,7 @@ def create_app(vault_path: str = None) -> FastAPI:
|
|
|
23
35
|
|
|
24
36
|
@app.get("/api/tree")
|
|
25
37
|
def tree():
|
|
38
|
+
vault.refresh_if_stale()
|
|
26
39
|
return JSONResponse(vault.get_full_tree())
|
|
27
40
|
|
|
28
41
|
@app.get("/api/stats")
|
|
@@ -31,35 +44,38 @@ def create_app(vault_path: str = None) -> FastAPI:
|
|
|
31
44
|
return JSONResponse(vault.get_stats())
|
|
32
45
|
|
|
33
46
|
@app.get("/api/graph")
|
|
34
|
-
def graph(kind: str = "all"):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
def graph(kind: str = "all", project: str = None):
|
|
48
|
+
vault.refresh_if_stale()
|
|
49
|
+
|
|
50
|
+
def include(path: str) -> bool:
|
|
51
|
+
if project and not path.startswith(project + "/"):
|
|
52
|
+
return False
|
|
39
53
|
node_kind = "session" if "/Sessions/" in path else "note"
|
|
40
54
|
if kind == "projects" and node_kind == "session":
|
|
41
|
-
|
|
55
|
+
return False
|
|
42
56
|
if kind == "sessions" and node_kind == "note":
|
|
57
|
+
return False
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
nodes = []
|
|
61
|
+
for path, note in vault.index.notes.items():
|
|
62
|
+
if not include(path):
|
|
43
63
|
continue
|
|
64
|
+
node_kind = "session" if "/Sessions/" in path else "note"
|
|
44
65
|
nodes.append({"id": path, "title": note.title, "kind": node_kind, "tags": note.tags})
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
continue
|
|
59
|
-
key = tuple(sorted([path, target]))
|
|
60
|
-
if key not in seen_edges:
|
|
61
|
-
seen_edges.add(key)
|
|
62
|
-
edges.append({"source": path, "target": target})
|
|
66
|
+
|
|
67
|
+
node_ids = {n["id"] for n in nodes}
|
|
68
|
+
edges = []
|
|
69
|
+
seen_edges = set()
|
|
70
|
+
for path in node_ids:
|
|
71
|
+
for target in vault.index.forward_links.get(path, set()):
|
|
72
|
+
if target not in node_ids:
|
|
73
|
+
continue
|
|
74
|
+
key = tuple(sorted([path, target]))
|
|
75
|
+
if key not in seen_edges:
|
|
76
|
+
seen_edges.add(key)
|
|
77
|
+
edges.append({"source": path, "target": target})
|
|
78
|
+
|
|
63
79
|
return JSONResponse({"nodes": nodes, "edges": edges})
|
|
64
80
|
|
|
65
81
|
@app.get("/api/note/{path:path}")
|
package/kyp_mem/vault.py
CHANGED
|
@@ -98,6 +98,7 @@ class Index:
|
|
|
98
98
|
def __init__(self):
|
|
99
99
|
self.notes: dict[str, Note] = {}
|
|
100
100
|
self.backlinks: dict[str, set] = defaultdict(set)
|
|
101
|
+
self.forward_links: dict[str, set] = defaultdict(set)
|
|
101
102
|
self.tag_index: dict[str, set] = defaultdict(set)
|
|
102
103
|
self._word_index: dict[str, set] = defaultdict(set)
|
|
103
104
|
self._name_to_path: dict[str, str] = {}
|
|
@@ -105,6 +106,7 @@ class Index:
|
|
|
105
106
|
def rebuild(self, notes: dict[str, Note]):
|
|
106
107
|
self.notes = notes
|
|
107
108
|
self.backlinks = defaultdict(set)
|
|
109
|
+
self.forward_links = defaultdict(set)
|
|
108
110
|
self.tag_index = defaultdict(set)
|
|
109
111
|
self._word_index = defaultdict(set)
|
|
110
112
|
self._name_to_path = {}
|
|
@@ -116,8 +118,9 @@ class Index:
|
|
|
116
118
|
for path, note in notes.items():
|
|
117
119
|
for link in note.links:
|
|
118
120
|
target = self._name_to_path.get(link.lower())
|
|
119
|
-
if target:
|
|
121
|
+
if target and target != path:
|
|
120
122
|
self.backlinks[target].add(path)
|
|
123
|
+
self.forward_links[path].add(target)
|
|
121
124
|
|
|
122
125
|
for tag in note.tags:
|
|
123
126
|
self.tag_index[tag.lower()].add(path)
|
|
@@ -210,14 +213,23 @@ class Vault:
|
|
|
210
213
|
init_vector_db(str(self.root))
|
|
211
214
|
self._load_all()
|
|
212
215
|
self._sync_vector_db()
|
|
216
|
+
self._last_mtime = self._max_mtime()
|
|
213
217
|
|
|
214
218
|
def _disk_note_paths(self) -> set[str]:
|
|
215
219
|
return {str(f.relative_to(self.root)) for f in self.root.rglob("*.md")}
|
|
216
220
|
|
|
221
|
+
def _max_mtime(self) -> float:
|
|
222
|
+
mtimes = [f.stat().st_mtime for f in self.root.rglob("*.md")]
|
|
223
|
+
return max(mtimes) if mtimes else 0.0
|
|
224
|
+
|
|
217
225
|
def refresh_if_stale(self):
|
|
218
|
-
|
|
226
|
+
current_mtime = self._max_mtime()
|
|
227
|
+
paths_changed = self._disk_note_paths() != set(self.index.notes.keys())
|
|
228
|
+
content_changed = current_mtime != self._last_mtime
|
|
229
|
+
if paths_changed or content_changed:
|
|
219
230
|
self._load_all()
|
|
220
231
|
self._sync_vector_db()
|
|
232
|
+
self._last_mtime = current_mtime
|
|
221
233
|
|
|
222
234
|
def _sync_vector_db(self):
|
|
223
235
|
mem = get_session_memory()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyp-mem",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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.6.
|
|
7
|
+
version = "0.6.8"
|
|
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"}
|