deja-cli 0.1.0__py3-none-any.whl
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.
- deja/__init__.py +0 -0
- deja/config.py +127 -0
- deja/core/__init__.py +0 -0
- deja/core/extractor.py +135 -0
- deja/core/reflection.py +364 -0
- deja/core/scheduler.py +65 -0
- deja/core/store.py +1413 -0
- deja/ingest/__init__.py +0 -0
- deja/ingest/watchers/__init__.py +0 -0
- deja/ingest/watchers/base.py +143 -0
- deja/ingest/watchers/claude_code.py +62 -0
- deja/ingest/watchers/codex_cli.py +95 -0
- deja/ingest/watchers/gemini_cli.py +96 -0
- deja/interfaces/__init__.py +0 -0
- deja/interfaces/cli.py +1967 -0
- deja/interfaces/mcp_server.py +96 -0
- deja/interfaces/web.py +104 -0
- deja/interfaces/web_ui/index.html +614 -0
- deja/llm/__init__.py +0 -0
- deja/llm/base.py +34 -0
- deja/llm/embedding.py +45 -0
- deja/llm/factory.py +90 -0
- deja/llm/providers/__init__.py +0 -0
- deja/llm/providers/anthropic.py +21 -0
- deja/llm/providers/ollama.py +30 -0
- deja/main.py +4 -0
- deja_cli-0.1.0.dist-info/METADATA +100 -0
- deja_cli-0.1.0.dist-info/RECORD +31 -0
- deja_cli-0.1.0.dist-info/WHEEL +4 -0
- deja_cli-0.1.0.dist-info/entry_points.txt +3 -0
- deja_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>deja vault</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f4f4f5;
|
|
10
|
+
--sidebar-bg: #1e1e2e;
|
|
11
|
+
--sidebar-text: #cdd6f4;
|
|
12
|
+
--header-bg: #181825;
|
|
13
|
+
--card-bg: #ffffff;
|
|
14
|
+
--border: #e4e4e7;
|
|
15
|
+
--text: #18181b;
|
|
16
|
+
--muted: #71717a;
|
|
17
|
+
--accent: #7c6af7;
|
|
18
|
+
|
|
19
|
+
--gotcha: #ef4444;
|
|
20
|
+
--preference: #3b82f6;
|
|
21
|
+
--decision: #8b5cf6;
|
|
22
|
+
--pattern: #22c55e;
|
|
23
|
+
--progress: #f97316;
|
|
24
|
+
--procedure: #14b8a6;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
31
|
+
background: var(--bg);
|
|
32
|
+
color: var(--text);
|
|
33
|
+
height: 100vh;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ── HEADER ── */
|
|
40
|
+
.header {
|
|
41
|
+
background: var(--header-bg);
|
|
42
|
+
color: var(--sidebar-text);
|
|
43
|
+
padding: 10px 20px;
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 14px;
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
border-bottom: 1px solid #313244;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.logo {
|
|
52
|
+
font-size: 17px;
|
|
53
|
+
font-weight: 700;
|
|
54
|
+
color: #cba6f7;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
letter-spacing: -0.3px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.search-wrap { flex: 1; max-width: 480px; }
|
|
60
|
+
|
|
61
|
+
#search-input {
|
|
62
|
+
width: 100%;
|
|
63
|
+
padding: 7px 13px;
|
|
64
|
+
border-radius: 7px;
|
|
65
|
+
border: 1px solid #45475a;
|
|
66
|
+
background: #313244;
|
|
67
|
+
color: var(--sidebar-text);
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
outline: none;
|
|
70
|
+
transition: border-color 0.15s;
|
|
71
|
+
}
|
|
72
|
+
#search-input:focus { border-color: var(--accent); }
|
|
73
|
+
#search-input::placeholder { color: #585b70; }
|
|
74
|
+
|
|
75
|
+
.header-stats {
|
|
76
|
+
font-size: 12px;
|
|
77
|
+
color: #585b70;
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
margin-left: auto;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── LAYOUT ── */
|
|
83
|
+
.layout {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex: 1;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── SIDEBAR ── */
|
|
90
|
+
.sidebar {
|
|
91
|
+
width: 210px;
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
background: var(--sidebar-bg);
|
|
94
|
+
color: var(--sidebar-text);
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
padding: 14px 10px;
|
|
97
|
+
font-size: 13px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.sidebar-section { margin-bottom: 4px; }
|
|
101
|
+
|
|
102
|
+
.sidebar h4 {
|
|
103
|
+
font-size: 10px;
|
|
104
|
+
letter-spacing: 0.1em;
|
|
105
|
+
text-transform: uppercase;
|
|
106
|
+
color: #585b70;
|
|
107
|
+
margin: 14px 0 6px 6px;
|
|
108
|
+
}
|
|
109
|
+
.sidebar h4:first-child { margin-top: 0; }
|
|
110
|
+
|
|
111
|
+
.filter-btn {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 8px;
|
|
115
|
+
padding: 5px 8px;
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
width: 100%;
|
|
119
|
+
border: none;
|
|
120
|
+
background: none;
|
|
121
|
+
color: var(--sidebar-text);
|
|
122
|
+
font-size: 13px;
|
|
123
|
+
text-align: left;
|
|
124
|
+
transition: background 0.1s;
|
|
125
|
+
}
|
|
126
|
+
.filter-btn:hover { background: #313244; }
|
|
127
|
+
.filter-btn.active { background: #313244; color: #cba6f7; font-weight: 600; }
|
|
128
|
+
|
|
129
|
+
.filter-dot {
|
|
130
|
+
width: 8px; height: 8px;
|
|
131
|
+
border-radius: 50%;
|
|
132
|
+
flex-shrink: 0;
|
|
133
|
+
background: #585b70;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.filter-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
137
|
+
|
|
138
|
+
.filter-count {
|
|
139
|
+
font-size: 11px;
|
|
140
|
+
color: #585b70;
|
|
141
|
+
flex-shrink: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ── MAIN ── */
|
|
145
|
+
.main {
|
|
146
|
+
flex: 1;
|
|
147
|
+
overflow-y: auto;
|
|
148
|
+
padding: 14px 16px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.memories-header {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
margin-bottom: 12px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.memories-count { font-size: 13px; color: var(--muted); }
|
|
159
|
+
|
|
160
|
+
.status-tabs { display: flex; gap: 4px; }
|
|
161
|
+
|
|
162
|
+
.status-tab {
|
|
163
|
+
padding: 4px 10px;
|
|
164
|
+
border-radius: 6px;
|
|
165
|
+
border: 1px solid var(--border);
|
|
166
|
+
background: white;
|
|
167
|
+
font-size: 12px;
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
color: var(--muted);
|
|
170
|
+
transition: all 0.1s;
|
|
171
|
+
}
|
|
172
|
+
.status-tab.active { background: var(--accent); border-color: var(--accent); color: white; }
|
|
173
|
+
.status-tab:hover:not(.active) { background: #f4f4f5; }
|
|
174
|
+
|
|
175
|
+
/* ── MEMORY CARD ── */
|
|
176
|
+
.memory-card {
|
|
177
|
+
background: white;
|
|
178
|
+
border-radius: 9px;
|
|
179
|
+
border: 1px solid var(--border);
|
|
180
|
+
padding: 12px 14px;
|
|
181
|
+
margin-bottom: 7px;
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
transition: box-shadow 0.1s, border-color 0.1s;
|
|
184
|
+
}
|
|
185
|
+
.memory-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.06); border-color: #d4d4d8; }
|
|
186
|
+
.memory-card.expanded { border-color: var(--accent); box-shadow: 0 2px 14px rgba(124,106,247,0.12); }
|
|
187
|
+
.memory-card.archived { opacity: 0.55; }
|
|
188
|
+
.memory-card.invalidated { opacity: 0.5; border-left: 3px solid #ef4444; }
|
|
189
|
+
|
|
190
|
+
.card-header {
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 7px;
|
|
194
|
+
margin-bottom: 7px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.type-badge {
|
|
198
|
+
font-size: 10px;
|
|
199
|
+
font-weight: 700;
|
|
200
|
+
padding: 2px 7px;
|
|
201
|
+
border-radius: 4px;
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
letter-spacing: 0.05em;
|
|
204
|
+
color: white;
|
|
205
|
+
flex-shrink: 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.project-tag {
|
|
209
|
+
font-size: 11px;
|
|
210
|
+
color: var(--muted);
|
|
211
|
+
background: var(--bg);
|
|
212
|
+
padding: 2px 7px;
|
|
213
|
+
border-radius: 4px;
|
|
214
|
+
flex-shrink: 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.card-meta {
|
|
218
|
+
margin-left: auto;
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
gap: 8px;
|
|
222
|
+
font-size: 11px;
|
|
223
|
+
color: var(--muted);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.conf-bar-wrap { display: flex; align-items: center; gap: 4px; }
|
|
227
|
+
|
|
228
|
+
.conf-bar {
|
|
229
|
+
width: 36px; height: 4px;
|
|
230
|
+
background: #e4e4e7;
|
|
231
|
+
border-radius: 2px;
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
}
|
|
234
|
+
.conf-fill { height: 100%; border-radius: 2px; background: var(--accent); }
|
|
235
|
+
|
|
236
|
+
.card-content {
|
|
237
|
+
font-size: 13.5px;
|
|
238
|
+
line-height: 1.55;
|
|
239
|
+
color: var(--text);
|
|
240
|
+
white-space: pre-wrap;
|
|
241
|
+
word-break: break-word;
|
|
242
|
+
}
|
|
243
|
+
.memory-card:not(.expanded) .card-content {
|
|
244
|
+
display: -webkit-box;
|
|
245
|
+
-webkit-line-clamp: 2;
|
|
246
|
+
-webkit-box-orient: vertical;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.card-detail {
|
|
251
|
+
display: none;
|
|
252
|
+
margin-top: 10px;
|
|
253
|
+
padding-top: 10px;
|
|
254
|
+
border-top: 1px solid var(--border);
|
|
255
|
+
}
|
|
256
|
+
.memory-card.expanded .card-detail { display: block; }
|
|
257
|
+
|
|
258
|
+
.detail-grid {
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-wrap: wrap;
|
|
261
|
+
gap: 6px 18px;
|
|
262
|
+
font-size: 12px;
|
|
263
|
+
color: var(--muted);
|
|
264
|
+
margin-bottom: 10px;
|
|
265
|
+
}
|
|
266
|
+
.detail-grid b { color: var(--text); }
|
|
267
|
+
.detail-grid code { font-family: monospace; font-size: 11px; color: #52525b; }
|
|
268
|
+
|
|
269
|
+
.trigger-tag {
|
|
270
|
+
background: #fef3c7;
|
|
271
|
+
color: #92400e;
|
|
272
|
+
padding: 2px 7px;
|
|
273
|
+
border-radius: 4px;
|
|
274
|
+
font-size: 11px;
|
|
275
|
+
display: inline-block;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.card-actions { display: flex; gap: 7px; margin-top: 8px; }
|
|
279
|
+
.card-actions button {
|
|
280
|
+
padding: 4px 10px;
|
|
281
|
+
border-radius: 6px;
|
|
282
|
+
border: 1px solid var(--border);
|
|
283
|
+
background: white;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
cursor: pointer;
|
|
286
|
+
color: var(--muted);
|
|
287
|
+
transition: background 0.1s;
|
|
288
|
+
}
|
|
289
|
+
.card-actions button:hover { background: var(--bg); }
|
|
290
|
+
.btn-danger { color: #ef4444 !important; border-color: #fecaca !important; }
|
|
291
|
+
.btn-danger:hover { background: #fef2f2 !important; }
|
|
292
|
+
|
|
293
|
+
/* ── STATES ── */
|
|
294
|
+
.loading { text-align: center; padding: 40px; color: var(--muted); font-size: 14px; }
|
|
295
|
+
.empty-state { text-align: center; padding: 60px 20px; color: var(--muted); }
|
|
296
|
+
.empty-state h3 { font-size: 17px; margin-bottom: 6px; color: var(--text); }
|
|
297
|
+
|
|
298
|
+
/* ── SCROLLBAR ── */
|
|
299
|
+
::-webkit-scrollbar { width: 5px; }
|
|
300
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
301
|
+
::-webkit-scrollbar-thumb { background: #d4d4d8; border-radius: 3px; }
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
|
|
306
|
+
<header class="header">
|
|
307
|
+
<div class="logo">⬡ deja</div>
|
|
308
|
+
<div class="search-wrap">
|
|
309
|
+
<input type="text" id="search-input" placeholder="Search memories…" autocomplete="off" />
|
|
310
|
+
</div>
|
|
311
|
+
<div class="header-stats" id="header-stats">—</div>
|
|
312
|
+
</header>
|
|
313
|
+
|
|
314
|
+
<div class="layout">
|
|
315
|
+
<aside class="sidebar">
|
|
316
|
+
<div class="sidebar-section">
|
|
317
|
+
<h4>Project</h4>
|
|
318
|
+
<div id="project-filters"></div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="sidebar-section">
|
|
321
|
+
<h4>Type</h4>
|
|
322
|
+
<div id="type-filters"></div>
|
|
323
|
+
</div>
|
|
324
|
+
</aside>
|
|
325
|
+
|
|
326
|
+
<main class="main">
|
|
327
|
+
<div class="memories-header">
|
|
328
|
+
<div class="memories-count" id="memories-count"></div>
|
|
329
|
+
<div class="status-tabs">
|
|
330
|
+
<button class="status-tab active" data-status="active">Active</button>
|
|
331
|
+
<button class="status-tab" data-status="archived">Archived</button>
|
|
332
|
+
<button class="status-tab" data-status="invalidated">Invalidated</button>
|
|
333
|
+
<button class="status-tab" data-status="all">All</button>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div id="memories-list"></div>
|
|
337
|
+
</main>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<script>
|
|
341
|
+
const TYPE_COLORS = {
|
|
342
|
+
gotcha: '#ef4444',
|
|
343
|
+
preference: '#3b82f6',
|
|
344
|
+
decision: '#8b5cf6',
|
|
345
|
+
pattern: '#22c55e',
|
|
346
|
+
progress: '#f97316',
|
|
347
|
+
procedure: '#14b8a6',
|
|
348
|
+
};
|
|
349
|
+
const ALL_TYPES = ['gotcha', 'preference', 'decision', 'pattern', 'progress', 'procedure'];
|
|
350
|
+
|
|
351
|
+
const state = {
|
|
352
|
+
project: null, // null=all, '__global__'=global only, else name
|
|
353
|
+
type: null, // null=all
|
|
354
|
+
status: 'active',
|
|
355
|
+
search: '',
|
|
356
|
+
expanded: null,
|
|
357
|
+
};
|
|
358
|
+
let searchTimer = null;
|
|
359
|
+
|
|
360
|
+
// ── INIT ─────────────────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
async function init() {
|
|
363
|
+
await Promise.all([loadStats(), loadSidebar()]);
|
|
364
|
+
await loadMemories();
|
|
365
|
+
|
|
366
|
+
document.getElementById('search-input').addEventListener('input', e => {
|
|
367
|
+
clearTimeout(searchTimer);
|
|
368
|
+
searchTimer = setTimeout(() => {
|
|
369
|
+
state.search = e.target.value.trim();
|
|
370
|
+
state.expanded = null;
|
|
371
|
+
loadMemories();
|
|
372
|
+
}, 280);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
document.querySelectorAll('.status-tab').forEach(btn => {
|
|
376
|
+
btn.addEventListener('click', () => {
|
|
377
|
+
document.querySelectorAll('.status-tab').forEach(b => b.classList.remove('active'));
|
|
378
|
+
btn.classList.add('active');
|
|
379
|
+
state.status = btn.dataset.status;
|
|
380
|
+
state.expanded = null;
|
|
381
|
+
loadMemories();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── DATA ─────────────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
async function loadStats() {
|
|
389
|
+
try {
|
|
390
|
+
const data = await fetchJSON('/api/stats');
|
|
391
|
+
document.getElementById('header-stats').textContent =
|
|
392
|
+
`${data.active} active · ${data.archived} archived · ${data.observations} observations`;
|
|
393
|
+
} catch {}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function loadSidebar() {
|
|
397
|
+
try {
|
|
398
|
+
const { projects } = await fetchJSON('/api/projects');
|
|
399
|
+
|
|
400
|
+
const pContainer = document.getElementById('project-filters');
|
|
401
|
+
pContainer.innerHTML = '';
|
|
402
|
+
pContainer.appendChild(makeFilterBtn('All projects', null, 'project'));
|
|
403
|
+
pContainer.appendChild(makeFilterBtn('Global', '__global__', 'project'));
|
|
404
|
+
for (const p of projects) {
|
|
405
|
+
pContainer.appendChild(makeFilterBtn(p.name, p.name, 'project', p.count));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const tContainer = document.getElementById('type-filters');
|
|
409
|
+
tContainer.innerHTML = '';
|
|
410
|
+
tContainer.appendChild(makeFilterBtn('All types', null, 'type'));
|
|
411
|
+
for (const t of ALL_TYPES) {
|
|
412
|
+
const btn = makeFilterBtn(t, t, 'type');
|
|
413
|
+
btn.querySelector('.filter-dot').style.background = TYPE_COLORS[t] || '#888';
|
|
414
|
+
tContainer.appendChild(btn);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
updateFilterActive();
|
|
418
|
+
} catch (e) { console.error(e); }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function makeFilterBtn(label, value, key, count) {
|
|
422
|
+
const btn = document.createElement('button');
|
|
423
|
+
btn.className = 'filter-btn';
|
|
424
|
+
btn.dataset.key = key;
|
|
425
|
+
btn.dataset.value = value === null ? '__null__' : value;
|
|
426
|
+
|
|
427
|
+
const dot = document.createElement('span');
|
|
428
|
+
dot.className = 'filter-dot';
|
|
429
|
+
|
|
430
|
+
const lbl = document.createElement('span');
|
|
431
|
+
lbl.className = 'filter-label';
|
|
432
|
+
lbl.textContent = label;
|
|
433
|
+
|
|
434
|
+
btn.append(dot, lbl);
|
|
435
|
+
|
|
436
|
+
if (count != null) {
|
|
437
|
+
const cnt = document.createElement('span');
|
|
438
|
+
cnt.className = 'filter-count';
|
|
439
|
+
cnt.textContent = count;
|
|
440
|
+
btn.appendChild(cnt);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
btn.addEventListener('click', () => {
|
|
444
|
+
const v = btn.dataset.value === '__null__' ? null : btn.dataset.value;
|
|
445
|
+
if (key === 'project') state.project = v;
|
|
446
|
+
else state.type = v;
|
|
447
|
+
state.expanded = null;
|
|
448
|
+
updateFilterActive();
|
|
449
|
+
loadMemories();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return btn;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function updateFilterActive() {
|
|
456
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
457
|
+
const v = btn.dataset.value === '__null__' ? null : btn.dataset.value;
|
|
458
|
+
const cur = btn.dataset.key === 'project' ? state.project : state.type;
|
|
459
|
+
btn.classList.toggle('active', v === cur);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function loadMemories() {
|
|
464
|
+
const listEl = document.getElementById('memories-list');
|
|
465
|
+
listEl.innerHTML = '<div class="loading">Loading…</div>';
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
let memories;
|
|
469
|
+
|
|
470
|
+
if (state.search) {
|
|
471
|
+
let url = `/api/search?q=${encodeURIComponent(state.search)}`;
|
|
472
|
+
if (state.project && state.project !== '__global__') {
|
|
473
|
+
url += `&project=${encodeURIComponent(state.project)}`;
|
|
474
|
+
}
|
|
475
|
+
if (state.type) url += `&type=${encodeURIComponent(state.type)}`;
|
|
476
|
+
const data = await fetchJSON(url);
|
|
477
|
+
memories = data.memories;
|
|
478
|
+
} else {
|
|
479
|
+
let url = `/api/memories?status=${state.status}&limit=300`;
|
|
480
|
+
if (state.project === '__global__') url += '&project=__global__';
|
|
481
|
+
else if (state.project) url += `&project=${encodeURIComponent(state.project)}`;
|
|
482
|
+
if (state.type) url += `&type=${encodeURIComponent(state.type)}`;
|
|
483
|
+
const data = await fetchJSON(url);
|
|
484
|
+
memories = data.memories;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
document.getElementById('memories-count').textContent =
|
|
488
|
+
`${memories.length} memor${memories.length === 1 ? 'y' : 'ies'}`;
|
|
489
|
+
renderMemories(memories, listEl);
|
|
490
|
+
} catch (e) {
|
|
491
|
+
listEl.innerHTML = `<div class="loading">Error: ${escHtml(e.message)}</div>`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ── RENDER ────────────────────────────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
function renderMemories(memories, container) {
|
|
498
|
+
if (!memories.length) {
|
|
499
|
+
container.innerHTML = '<div class="empty-state"><h3>No memories found</h3><p>Try a different filter or search.</p></div>';
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
container.innerHTML = '';
|
|
503
|
+
for (const mem of memories) container.appendChild(makeCard(mem));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function makeCard(mem) {
|
|
507
|
+
const card = document.createElement('div');
|
|
508
|
+
card.className = 'memory-card';
|
|
509
|
+
card.dataset.id = mem.id;
|
|
510
|
+
if (mem.archived_at) card.classList.add('archived');
|
|
511
|
+
if (mem.invalidated_at) card.classList.add('invalidated');
|
|
512
|
+
if (state.expanded === mem.id) card.classList.add('expanded');
|
|
513
|
+
|
|
514
|
+
const color = TYPE_COLORS[mem.type] || '#888';
|
|
515
|
+
const proj = mem.project || 'global';
|
|
516
|
+
const conf = Math.round((mem.confidence || 0) * 100);
|
|
517
|
+
const reuse = mem.reuse_count || 0;
|
|
518
|
+
const when = timeAgo(mem.updated_at || mem.created_at);
|
|
519
|
+
|
|
520
|
+
const isActive = !mem.archived_at && !mem.invalidated_at;
|
|
521
|
+
|
|
522
|
+
card.innerHTML = `
|
|
523
|
+
<div class="card-header">
|
|
524
|
+
<span class="type-badge" style="background:${color}">${mem.type}</span>
|
|
525
|
+
<span class="project-tag">${escHtml(proj)}</span>
|
|
526
|
+
<div class="card-meta">
|
|
527
|
+
<span class="conf-bar-wrap">
|
|
528
|
+
<span class="conf-bar"><span class="conf-fill" style="width:${conf}%"></span></span>
|
|
529
|
+
${conf}%
|
|
530
|
+
</span>
|
|
531
|
+
<span title="reuse count">↻${reuse}</span>
|
|
532
|
+
<span>${when}</span>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
<div class="card-content">${escHtml(mem.content)}</div>
|
|
536
|
+
<div class="card-detail">
|
|
537
|
+
<div class="detail-grid">
|
|
538
|
+
<span><b>ID</b> <code>${mem.id}</code></span>
|
|
539
|
+
${mem.source ? `<span><b>source</b> ${escHtml(mem.source)}</span>` : ''}
|
|
540
|
+
${mem.domain ? `<span><b>domain</b> ${escHtml(mem.domain)}</span>` : ''}
|
|
541
|
+
${mem.category ? `<span><b>category</b> ${escHtml(mem.category)}</span>` : ''}
|
|
542
|
+
${mem.created_at ? `<span><b>created</b> ${mem.created_at.slice(0,10)}</span>` : ''}
|
|
543
|
+
${mem.last_confirmed ? `<span><b>confirmed</b> ${mem.last_confirmed.slice(0,10)}</span>` : ''}
|
|
544
|
+
${mem.archived_at ? `<span style="color:#ef4444"><b>archived</b> ${mem.archived_at.slice(0,10)}</span>` : ''}
|
|
545
|
+
${mem.invalidated_at ? `<span style="color:#ef4444"><b>invalidated</b> ${mem.invalidated_at.slice(0,10)}</span>` : ''}
|
|
546
|
+
</div>
|
|
547
|
+
${mem.trigger ? `<div style="margin-bottom:8px"><span class="trigger-tag">⚡ ${escHtml(mem.trigger)}</span></div>` : ''}
|
|
548
|
+
${isActive ? `
|
|
549
|
+
<div class="card-actions">
|
|
550
|
+
<button class="btn-danger" data-action="archive" data-id="${mem.id}">Archive</button>
|
|
551
|
+
<button class="btn-danger" data-action="invalidate" data-id="${mem.id}">Invalidate</button>
|
|
552
|
+
</div>` : ''}
|
|
553
|
+
</div>
|
|
554
|
+
`;
|
|
555
|
+
|
|
556
|
+
card.addEventListener('click', e => {
|
|
557
|
+
const action = e.target.dataset.action;
|
|
558
|
+
if (action === 'archive') { e.stopPropagation(); doArchive(mem.id); return; }
|
|
559
|
+
if (action === 'invalidate') { e.stopPropagation(); doInvalidate(mem.id); return; }
|
|
560
|
+
toggleExpand(mem.id);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
return card;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function toggleExpand(id) {
|
|
567
|
+
state.expanded = state.expanded === id ? null : id;
|
|
568
|
+
document.querySelectorAll('.memory-card').forEach(c =>
|
|
569
|
+
c.classList.toggle('expanded', c.dataset.id === state.expanded)
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ── ACTIONS ───────────────────────────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
async function doArchive(id) {
|
|
576
|
+
if (!confirm('Archive this memory?')) return;
|
|
577
|
+
await fetch(`/api/memories/${id}/archive`, { method: 'POST' });
|
|
578
|
+
loadMemories(); loadStats();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function doInvalidate(id) {
|
|
582
|
+
if (!confirm('Invalidate (mark as superseded) this memory?')) return;
|
|
583
|
+
await fetch(`/api/memories/${id}/invalidate`, { method: 'POST' });
|
|
584
|
+
loadMemories(); loadStats();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── HELPERS ───────────────────────────────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
async function fetchJSON(url) {
|
|
590
|
+
const r = await fetch(url);
|
|
591
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
592
|
+
return r.json();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function timeAgo(iso) {
|
|
596
|
+
if (!iso) return '';
|
|
597
|
+
const diff = (Date.now() - new Date(iso).getTime()) / 86400000;
|
|
598
|
+
if (diff < 1) return 'today';
|
|
599
|
+
if (diff < 2) return 'yesterday';
|
|
600
|
+
if (diff < 7) return `${Math.floor(diff)}d ago`;
|
|
601
|
+
if (diff < 30) return `${Math.floor(diff / 7)}w ago`;
|
|
602
|
+
if (diff < 365) return `${Math.floor(diff / 30)}mo ago`;
|
|
603
|
+
return `${Math.floor(diff / 365)}y ago`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function escHtml(s) {
|
|
607
|
+
if (!s) return '';
|
|
608
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
init();
|
|
612
|
+
</script>
|
|
613
|
+
</body>
|
|
614
|
+
</html>
|
deja/llm/__init__.py
ADDED
|
File without changes
|
deja/llm/base.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class LLMResponse:
|
|
12
|
+
content: str
|
|
13
|
+
raw: Any = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LLMAdapter(ABC):
|
|
17
|
+
@abstractmethod
|
|
18
|
+
async def complete(self, system: str, user: str, **kwargs) -> LLMResponse: ...
|
|
19
|
+
|
|
20
|
+
async def complete_structured(self, system: str, user: str, schema: dict) -> dict:
|
|
21
|
+
"""Prompt-engineer JSON mode for providers without native JSON output."""
|
|
22
|
+
prompt = (
|
|
23
|
+
f"{user}\n\nRespond ONLY with valid JSON matching this schema:\n"
|
|
24
|
+
f"{json.dumps(schema, indent=2)}"
|
|
25
|
+
)
|
|
26
|
+
response = await self.complete(
|
|
27
|
+
system=system + "\nRespond with valid JSON only. No markdown fences, no explanation.",
|
|
28
|
+
user=prompt,
|
|
29
|
+
)
|
|
30
|
+
return self._parse_json(response.content)
|
|
31
|
+
|
|
32
|
+
def _parse_json(self, content: str) -> dict:
|
|
33
|
+
cleaned = re.sub(r"^```(?:json)?\n?|\n?```$", "", content.strip())
|
|
34
|
+
return json.loads(cleaned)
|
deja/llm/embedding.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import struct
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EmbeddingAdapter(ABC):
|
|
11
|
+
@abstractmethod
|
|
12
|
+
async def embed(self, text: str) -> list[float]: ...
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def to_bytes(embedding: list[float]) -> bytes:
|
|
16
|
+
return struct.pack(f"{len(embedding)}f", *embedding)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def from_bytes(data: bytes) -> list[float]:
|
|
20
|
+
n = len(data) // 4
|
|
21
|
+
return list(struct.unpack(f"{n}f", data))
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def cosine_similarity(a: list[float], b: list[float]) -> float:
|
|
25
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
26
|
+
mag_a = math.sqrt(sum(x * x for x in a))
|
|
27
|
+
mag_b = math.sqrt(sum(x * x for x in b))
|
|
28
|
+
if mag_a == 0.0 or mag_b == 0.0:
|
|
29
|
+
return 0.0
|
|
30
|
+
return dot / (mag_a * mag_b)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OllamaEmbeddingAdapter(EmbeddingAdapter):
|
|
34
|
+
def __init__(self, model: str, base_url: str = "http://localhost:11434") -> None:
|
|
35
|
+
self.model = model
|
|
36
|
+
self.base_url = base_url.rstrip("/")
|
|
37
|
+
|
|
38
|
+
async def embed(self, text: str) -> list[float]:
|
|
39
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
40
|
+
response = await client.post(
|
|
41
|
+
f"{self.base_url}/api/embeddings",
|
|
42
|
+
json={"model": self.model, "prompt": text},
|
|
43
|
+
)
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
return response.json()["embedding"]
|