claude-launchpad 0.6.1 → 0.7.0
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/README.md +54 -20
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-6ZVXZ4EF.js +101 -0
- package/dist/chunk-6ZVXZ4EF.js.map +1 -0
- package/dist/chunk-CSLWJEGD.js +25 -0
- package/dist/chunk-CSLWJEGD.js.map +1 -0
- package/dist/chunk-EBM7RBPB.js +35 -0
- package/dist/chunk-EBM7RBPB.js.map +1 -0
- package/dist/chunk-IILH26C7.js +258 -0
- package/dist/chunk-IILH26C7.js.map +1 -0
- package/dist/chunk-JE3BZ5S4.js +311 -0
- package/dist/chunk-JE3BZ5S4.js.map +1 -0
- package/dist/chunk-NAW47BYA.js +25 -0
- package/dist/chunk-NAW47BYA.js.map +1 -0
- package/dist/chunk-TALTTAMW.js +390 -0
- package/dist/chunk-TALTTAMW.js.map +1 -0
- package/dist/cli.js +348 -212
- package/dist/cli.js.map +1 -1
- package/dist/commands/memory/server.js +685 -0
- package/dist/commands/memory/server.js.map +1 -0
- package/dist/context-LNUZ4GCF.js +316 -0
- package/dist/context-LNUZ4GCF.js.map +1 -0
- package/dist/extract-NVAXO5CK.js +217 -0
- package/dist/extract-NVAXO5CK.js.map +1 -0
- package/dist/install-65P6LMUN.js +230 -0
- package/dist/install-65P6LMUN.js.map +1 -0
- package/dist/stats-FYAK7KZW.js +73 -0
- package/dist/stats-FYAK7KZW.js.map +1 -0
- package/dist/tui-R25NTQ4K.js +1100 -0
- package/dist/tui-R25NTQ4K.js.map +1 -0
- package/package.json +19 -4
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
MemoryRepo,
|
|
4
|
+
RelationRepo,
|
|
5
|
+
SearchRepo
|
|
6
|
+
} from "./chunk-TALTTAMW.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_DECAY_PARAMS,
|
|
9
|
+
closeDatabase,
|
|
10
|
+
createDatabase,
|
|
11
|
+
loadConfig,
|
|
12
|
+
migrate,
|
|
13
|
+
resolveDataDir
|
|
14
|
+
} from "./chunk-IILH26C7.js";
|
|
15
|
+
import "./chunk-2H7UOFLK.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/memory/dashboard/tui.ts
|
|
18
|
+
import blessed9 from "blessed";
|
|
19
|
+
|
|
20
|
+
// src/commands/memory/dashboard/data/data-source.ts
|
|
21
|
+
import { statSync, watchFile, unwatchFile } from "fs";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
var DashboardDataSource = class {
|
|
24
|
+
#memoryRepo;
|
|
25
|
+
#relationRepo;
|
|
26
|
+
#searchRepo;
|
|
27
|
+
#dbPath;
|
|
28
|
+
#cachedMemories = [];
|
|
29
|
+
constructor(memoryRepo, relationRepo, searchRepo, dataDir) {
|
|
30
|
+
this.#memoryRepo = memoryRepo;
|
|
31
|
+
this.#relationRepo = relationRepo;
|
|
32
|
+
this.#searchRepo = searchRepo;
|
|
33
|
+
this.#dbPath = join(dataDir, "memory.db");
|
|
34
|
+
}
|
|
35
|
+
/** Re-query all memories from DB and cache them. Excludes soft-deleted (importance=0). */
|
|
36
|
+
refresh() {
|
|
37
|
+
this.#cachedMemories = this.#memoryRepo.getAll().filter((m) => m.importance > 0);
|
|
38
|
+
}
|
|
39
|
+
/** Return cached memories, optionally filtered by type, project, or FTS query. */
|
|
40
|
+
getMemories(filter) {
|
|
41
|
+
if (!filter) return this.#cachedMemories;
|
|
42
|
+
let results = this.#cachedMemories;
|
|
43
|
+
if (filter.type) {
|
|
44
|
+
const t = filter.type;
|
|
45
|
+
results = results.filter((m) => m.type === t);
|
|
46
|
+
}
|
|
47
|
+
if (filter.project) {
|
|
48
|
+
const p = filter.project;
|
|
49
|
+
results = results.filter((m) => m.project === p);
|
|
50
|
+
}
|
|
51
|
+
if (filter.query) {
|
|
52
|
+
const matches = this.#searchRepo.searchFts({
|
|
53
|
+
query: filter.query,
|
|
54
|
+
limit: 100
|
|
55
|
+
});
|
|
56
|
+
const matchedIds = new Set(matches.map((m) => m.memoryId));
|
|
57
|
+
results = results.filter((m) => matchedIds.has(m.id));
|
|
58
|
+
}
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
/** Get all relations for a specific memory. */
|
|
62
|
+
getRelationsForMemory(id) {
|
|
63
|
+
return this.#relationRepo.getByMemory(id);
|
|
64
|
+
}
|
|
65
|
+
/** Compute aggregate stats from cached data + DB queries. */
|
|
66
|
+
getStats() {
|
|
67
|
+
const total = this.#memoryRepo.count();
|
|
68
|
+
const byType = this.#memoryRepo.countByType();
|
|
69
|
+
const relations = this.#relationRepo.count();
|
|
70
|
+
const { oldest, newest } = this.#memoryRepo.dateRange();
|
|
71
|
+
const dbSizeBytes = this.#getDbSize();
|
|
72
|
+
const byProject = this.#computeByProject();
|
|
73
|
+
return { total, byType, byProject, relations, dbSizeBytes, oldest, newest };
|
|
74
|
+
}
|
|
75
|
+
/** Derive unique project names from cached memories. */
|
|
76
|
+
getProjects() {
|
|
77
|
+
const projects = /* @__PURE__ */ new Set();
|
|
78
|
+
for (const m of this.#cachedMemories) {
|
|
79
|
+
if (m.project) {
|
|
80
|
+
projects.add(m.project);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return [...projects].sort();
|
|
84
|
+
}
|
|
85
|
+
/** Watch the DB file for changes (2s polling interval). Only fires on mtime change. */
|
|
86
|
+
startWatching(onChange) {
|
|
87
|
+
let lastMtime = 0;
|
|
88
|
+
watchFile(this.#dbPath, { interval: 2e3 }, (curr) => {
|
|
89
|
+
const mtime = curr.mtimeMs;
|
|
90
|
+
if (mtime === lastMtime) return;
|
|
91
|
+
lastMtime = mtime;
|
|
92
|
+
this.refresh();
|
|
93
|
+
onChange();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Stop watching the DB file. */
|
|
97
|
+
stopWatching() {
|
|
98
|
+
unwatchFile(this.#dbPath);
|
|
99
|
+
}
|
|
100
|
+
// -- Private helpers --------------------------------------------------------
|
|
101
|
+
#getDbSize() {
|
|
102
|
+
try {
|
|
103
|
+
return statSync(this.#dbPath).size;
|
|
104
|
+
} catch {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
#computeByProject() {
|
|
109
|
+
const counts = {};
|
|
110
|
+
for (const m of this.#cachedMemories) {
|
|
111
|
+
const key = m.project ?? "(none)";
|
|
112
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
113
|
+
}
|
|
114
|
+
return counts;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/commands/memory/dashboard/layout.ts
|
|
119
|
+
import blessed8 from "blessed";
|
|
120
|
+
|
|
121
|
+
// src/commands/memory/dashboard/widgets/header.ts
|
|
122
|
+
import blessed from "blessed";
|
|
123
|
+
function createHeader(screen) {
|
|
124
|
+
const box = blessed.box({
|
|
125
|
+
parent: screen,
|
|
126
|
+
top: 0,
|
|
127
|
+
left: 0,
|
|
128
|
+
width: "100%",
|
|
129
|
+
height: 1,
|
|
130
|
+
tags: true,
|
|
131
|
+
style: { fg: "black", bg: "green", bold: true },
|
|
132
|
+
content: "{bold} agentic-memory cockpit {/bold} {|} [/]=search [p]=project picker [[/]]=prev/next [1-5]=type [l]=life [s]=sort [tab]=focus [?]=help [q]=quit"
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
widget: box,
|
|
136
|
+
setLabel(text) {
|
|
137
|
+
box.setContent(
|
|
138
|
+
`{bold} ${text} {/bold} {|} [/]=search [p]=project picker [[/]]=prev/next [1-5]=type [l]=life [s]=sort [tab]=focus [?]=help [q]=quit`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/commands/memory/dashboard/widgets/memory-list.ts
|
|
145
|
+
import blessed2 from "blessed";
|
|
146
|
+
|
|
147
|
+
// src/commands/memory/dashboard/data/formatters.ts
|
|
148
|
+
function escapeBlessedTags(text) {
|
|
149
|
+
return text.replace(/\{/g, "\uFF5B").replace(/\}/g, "\uFF5D");
|
|
150
|
+
}
|
|
151
|
+
var MINUTE = 6e4;
|
|
152
|
+
var HOUR = 36e5;
|
|
153
|
+
var DAY = 864e5;
|
|
154
|
+
var MONTH = 30 * DAY;
|
|
155
|
+
var YEAR = 365 * DAY;
|
|
156
|
+
function formatRelativeTime(isoString) {
|
|
157
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
158
|
+
if (diff < 0) return "just now";
|
|
159
|
+
if (diff < MINUTE) return "just now";
|
|
160
|
+
if (diff < HOUR) return `${Math.floor(diff / MINUTE)}m ago`;
|
|
161
|
+
if (diff < DAY) return `${Math.floor(diff / HOUR)}h ago`;
|
|
162
|
+
if (diff < MONTH) return `${Math.floor(diff / DAY)}d ago`;
|
|
163
|
+
if (diff < YEAR) return `${Math.floor(diff / MONTH)}mo ago`;
|
|
164
|
+
return `${Math.floor(diff / YEAR)}y ago`;
|
|
165
|
+
}
|
|
166
|
+
var FILLED = "\u2588";
|
|
167
|
+
var EMPTY = "\u2591";
|
|
168
|
+
function formatImportanceBar(value, width = 8) {
|
|
169
|
+
const clamped = Math.max(0, Math.min(1, value));
|
|
170
|
+
const filled = Math.round(clamped * width);
|
|
171
|
+
return FILLED.repeat(filled) + EMPTY.repeat(width - filled);
|
|
172
|
+
}
|
|
173
|
+
function truncate(text, maxLen) {
|
|
174
|
+
if (text.length <= maxLen) return text;
|
|
175
|
+
if (maxLen <= 1) return "\u2026";
|
|
176
|
+
return text.slice(0, maxLen - 1) + "\u2026";
|
|
177
|
+
}
|
|
178
|
+
var UNITS = ["B", "KB", "MB", "GB"];
|
|
179
|
+
function formatBytes(bytes) {
|
|
180
|
+
if (bytes < 0) return "0B";
|
|
181
|
+
let value = bytes;
|
|
182
|
+
let unitIndex = 0;
|
|
183
|
+
while (value >= 1024 && unitIndex < UNITS.length - 1) {
|
|
184
|
+
value /= 1024;
|
|
185
|
+
unitIndex++;
|
|
186
|
+
}
|
|
187
|
+
if (unitIndex === 0) return `${value}B`;
|
|
188
|
+
return `${value.toFixed(1)}${UNITS[unitIndex]}`;
|
|
189
|
+
}
|
|
190
|
+
function tauDaysForType(type) {
|
|
191
|
+
return DEFAULT_DECAY_PARAMS.tauByType[type];
|
|
192
|
+
}
|
|
193
|
+
function computeLifespan(memory) {
|
|
194
|
+
const tauDays = tauDaysForType(memory.type);
|
|
195
|
+
if (tauDays === 0) {
|
|
196
|
+
return {
|
|
197
|
+
status: "session",
|
|
198
|
+
tauDays,
|
|
199
|
+
ageDays: 0,
|
|
200
|
+
remaining: 0
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const ageDays = Math.max(
|
|
204
|
+
0,
|
|
205
|
+
(Date.now() - new Date(memory.updatedAt).getTime()) / DAY
|
|
206
|
+
);
|
|
207
|
+
const remaining = Math.max(0, Math.min(1, 1 - ageDays / (tauDays * 2)));
|
|
208
|
+
const status = remaining > 0.62 ? "healthy" : remaining > 0.32 ? "fading" : "stale";
|
|
209
|
+
return { status, tauDays, ageDays, remaining };
|
|
210
|
+
}
|
|
211
|
+
function formatLifespanLabel(status) {
|
|
212
|
+
switch (status) {
|
|
213
|
+
case "healthy":
|
|
214
|
+
return "HEALTHY";
|
|
215
|
+
case "fading":
|
|
216
|
+
return "FADING ";
|
|
217
|
+
case "stale":
|
|
218
|
+
return "STALE ";
|
|
219
|
+
case "session":
|
|
220
|
+
return "SESSION";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/commands/memory/dashboard/colors.ts
|
|
225
|
+
var TYPE_COLORS = {
|
|
226
|
+
working: "red",
|
|
227
|
+
episodic: "yellow",
|
|
228
|
+
semantic: "cyan",
|
|
229
|
+
procedural: "green",
|
|
230
|
+
pattern: "magenta"
|
|
231
|
+
};
|
|
232
|
+
var TYPE_ABBREV = {
|
|
233
|
+
working: "WORK",
|
|
234
|
+
episodic: "EPIS",
|
|
235
|
+
semantic: "SEMA",
|
|
236
|
+
procedural: "PROC",
|
|
237
|
+
pattern: "PTRN"
|
|
238
|
+
};
|
|
239
|
+
var RELATION_COLORS = {
|
|
240
|
+
relates_to: "white",
|
|
241
|
+
depends_on: "blue",
|
|
242
|
+
contradicts: "red",
|
|
243
|
+
extends: "green",
|
|
244
|
+
implements: "cyan",
|
|
245
|
+
derived_from: "yellow"
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// src/commands/memory/dashboard/widgets/memory-list.ts
|
|
249
|
+
function createMemoryList(screen) {
|
|
250
|
+
const list = blessed2.list({
|
|
251
|
+
parent: screen,
|
|
252
|
+
top: 1,
|
|
253
|
+
left: 0,
|
|
254
|
+
width: "60%",
|
|
255
|
+
height: "100%-4",
|
|
256
|
+
keys: true,
|
|
257
|
+
vi: true,
|
|
258
|
+
mouse: true,
|
|
259
|
+
tags: true,
|
|
260
|
+
interactive: true,
|
|
261
|
+
scrollable: true,
|
|
262
|
+
alwaysScroll: true,
|
|
263
|
+
border: { type: "line" },
|
|
264
|
+
label: " Memories ",
|
|
265
|
+
style: {
|
|
266
|
+
border: { fg: "cyan" },
|
|
267
|
+
item: { fg: "white" },
|
|
268
|
+
selected: { fg: "black", bg: "green" }
|
|
269
|
+
},
|
|
270
|
+
scrollbar: {
|
|
271
|
+
style: { bg: "cyan" }
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
let currentMemories = [];
|
|
275
|
+
let selectCallback = null;
|
|
276
|
+
function colorForLife(status) {
|
|
277
|
+
switch (status) {
|
|
278
|
+
case "healthy":
|
|
279
|
+
return "green";
|
|
280
|
+
case "fading":
|
|
281
|
+
return "yellow";
|
|
282
|
+
case "stale":
|
|
283
|
+
return "red";
|
|
284
|
+
case "session":
|
|
285
|
+
return "magenta";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function buildRows(memories) {
|
|
289
|
+
return memories.map((m) => {
|
|
290
|
+
const life = computeLifespan(m);
|
|
291
|
+
const title = escapeBlessedTags(
|
|
292
|
+
truncate(m.title ?? m.content.replace(/\n/g, " "), 36)
|
|
293
|
+
);
|
|
294
|
+
const project = escapeBlessedTags(truncate(m.project ?? "(none)", 14));
|
|
295
|
+
const type = TYPE_ABBREV[m.type] ?? m.type;
|
|
296
|
+
const lifeColor = colorForLife(life.status);
|
|
297
|
+
const lifeLabel = formatLifespanLabel(life.status).trim();
|
|
298
|
+
const imp = `${Math.round(m.importance * 100)}%`;
|
|
299
|
+
const updated = formatRelativeTime(m.updatedAt);
|
|
300
|
+
return [
|
|
301
|
+
`{bold}${title}{/bold}`,
|
|
302
|
+
`{gray-fg}${project}{/gray-fg}`,
|
|
303
|
+
`{cyan-fg}${type}{/cyan-fg}`,
|
|
304
|
+
`{${lifeColor}-fg}${lifeLabel}{/${lifeColor}-fg}`,
|
|
305
|
+
`{white-fg}${imp}{/white-fg}`,
|
|
306
|
+
`{gray-fg}${updated}{/gray-fg}`,
|
|
307
|
+
`{blue-fg}acc:${m.accessCount}{/blue-fg}`
|
|
308
|
+
].join(" ");
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function emitCurrentSelection() {
|
|
312
|
+
const idx = list.selected;
|
|
313
|
+
const memory = currentMemories[idx];
|
|
314
|
+
if (memory && selectCallback) selectCallback(memory);
|
|
315
|
+
}
|
|
316
|
+
list.on("select", () => emitCurrentSelection());
|
|
317
|
+
list.on("select item", () => emitCurrentSelection());
|
|
318
|
+
return {
|
|
319
|
+
widget: list,
|
|
320
|
+
setData(memories) {
|
|
321
|
+
currentMemories = memories;
|
|
322
|
+
const rows = buildRows(memories);
|
|
323
|
+
list.setItems(rows);
|
|
324
|
+
},
|
|
325
|
+
onSelect(callback) {
|
|
326
|
+
selectCallback = callback;
|
|
327
|
+
},
|
|
328
|
+
focus() {
|
|
329
|
+
list.focus();
|
|
330
|
+
if (currentMemories.length > 0) {
|
|
331
|
+
list.select(0);
|
|
332
|
+
emitCurrentSelection();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/commands/memory/dashboard/widgets/memory-detail.ts
|
|
339
|
+
import blessed3 from "blessed";
|
|
340
|
+
function createMemoryDetail(screen) {
|
|
341
|
+
const box = blessed3.box({
|
|
342
|
+
parent: screen,
|
|
343
|
+
top: 1,
|
|
344
|
+
left: "45%",
|
|
345
|
+
width: "55%",
|
|
346
|
+
height: "100%-4",
|
|
347
|
+
keys: true,
|
|
348
|
+
vi: true,
|
|
349
|
+
mouse: true,
|
|
350
|
+
scrollable: true,
|
|
351
|
+
alwaysScroll: true,
|
|
352
|
+
tags: true,
|
|
353
|
+
border: { type: "line" },
|
|
354
|
+
label: " Detail ",
|
|
355
|
+
style: {
|
|
356
|
+
border: { fg: "blue" },
|
|
357
|
+
fg: "white"
|
|
358
|
+
},
|
|
359
|
+
scrollbar: {
|
|
360
|
+
style: { bg: "blue" }
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return {
|
|
364
|
+
widget: box,
|
|
365
|
+
showMemory(memory, relations) {
|
|
366
|
+
const typeColor = TYPE_COLORS[memory.type] ?? "white";
|
|
367
|
+
const life = computeLifespan(memory);
|
|
368
|
+
const lines = [
|
|
369
|
+
`{bold}${escapeBlessedTags(memory.title ?? "(untitled)")}{/bold}`,
|
|
370
|
+
"",
|
|
371
|
+
`Type: {${typeColor}-fg}${memory.type}{/${typeColor}-fg}`,
|
|
372
|
+
`Lifespan: ${formatLifespanLabel(life.status)} | age ${Math.round(life.ageDays)}d | tau ${life.tauDays}d`,
|
|
373
|
+
`Health: ${formatImportanceBar(life.remaining)} ${(life.remaining * 100).toFixed(0)}% remaining`,
|
|
374
|
+
`Importance: ${formatImportanceBar(memory.importance)} ${memory.importance.toFixed(2)}`,
|
|
375
|
+
`Project: ${escapeBlessedTags(memory.project ?? "(none)")}`,
|
|
376
|
+
`Tags: ${memory.tags.length > 0 ? memory.tags.map((t) => `[${escapeBlessedTags(t)}]`).join(" ") : "(none)"}`,
|
|
377
|
+
`Source: ${memory.source ?? "unknown"}`,
|
|
378
|
+
"",
|
|
379
|
+
`Created: ${formatRelativeTime(memory.createdAt)}`,
|
|
380
|
+
`Updated: ${formatRelativeTime(memory.updatedAt)}`,
|
|
381
|
+
`Accessed: ${memory.accessCount}x${memory.lastAccessed ? ` (last: ${formatRelativeTime(memory.lastAccessed)})` : ""}`,
|
|
382
|
+
`Injected: ${memory.injectionCount}x`,
|
|
383
|
+
"",
|
|
384
|
+
"{bold}Content{/bold}",
|
|
385
|
+
"{gray-fg}" + "\u2500".repeat(40) + "{/gray-fg}",
|
|
386
|
+
escapeBlessedTags(memory.content)
|
|
387
|
+
];
|
|
388
|
+
if (relations.length > 0) {
|
|
389
|
+
lines.push(
|
|
390
|
+
"",
|
|
391
|
+
"{bold}Relations{/bold}",
|
|
392
|
+
"{gray-fg}" + "\u2500".repeat(40) + "{/gray-fg}"
|
|
393
|
+
);
|
|
394
|
+
for (const r of relations) {
|
|
395
|
+
const relColor = RELATION_COLORS[r.relationType] ?? "white";
|
|
396
|
+
const direction = r.sourceId === memory.id ? "\u2192" : "\u2190";
|
|
397
|
+
const otherId = r.sourceId === memory.id ? r.targetId : r.sourceId;
|
|
398
|
+
lines.push(
|
|
399
|
+
` ${direction} {${relColor}-fg}${r.relationType}{/${relColor}-fg} ${otherId}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
box.setContent(lines.join("\n"));
|
|
404
|
+
box.scrollTo(0);
|
|
405
|
+
},
|
|
406
|
+
clear() {
|
|
407
|
+
box.setContent("{gray-fg}Select a memory to view details{/gray-fg}");
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/commands/memory/dashboard/widgets/stats-bar.ts
|
|
413
|
+
import blessed4 from "blessed";
|
|
414
|
+
function createStatsBar(screen) {
|
|
415
|
+
const box = blessed4.box({
|
|
416
|
+
parent: screen,
|
|
417
|
+
bottom: 0,
|
|
418
|
+
left: 0,
|
|
419
|
+
width: "100%",
|
|
420
|
+
height: 3,
|
|
421
|
+
tags: true,
|
|
422
|
+
border: { type: "line" },
|
|
423
|
+
style: {
|
|
424
|
+
border: { fg: "blue" },
|
|
425
|
+
fg: "white"
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
widget: box,
|
|
430
|
+
setStats(stats, visibleMemories) {
|
|
431
|
+
const typeBreakdown = Object.entries(stats.byType).filter(([, count]) => count > 0).map(([type, count]) => {
|
|
432
|
+
const color = TYPE_COLORS[type] ?? "white";
|
|
433
|
+
return `{${color}-fg}${type}:${count}{/${color}-fg}`;
|
|
434
|
+
}).join(" ");
|
|
435
|
+
const projects = Object.entries(stats.byProject).map(([name, count]) => `${name}(${count})`).join(" ");
|
|
436
|
+
const lifeCounts = { healthy: 0, fading: 0, stale: 0, session: 0 };
|
|
437
|
+
for (const memory of visibleMemories) {
|
|
438
|
+
lifeCounts[computeLifespan(memory).status]++;
|
|
439
|
+
}
|
|
440
|
+
const lifeSummary = `Life H:${lifeCounts.healthy} F:${lifeCounts.fading} S:${lifeCounts.stale} Sess:${lifeCounts.session}`;
|
|
441
|
+
box.setContent(
|
|
442
|
+
`{bold}Total:{/bold} ${stats.total} {bold}Relations:{/bold} ${stats.relations} {bold}Visible:{/bold} ${visibleMemories.length} {bold}DB:{/bold} ${formatBytes(stats.dbSizeBytes)}
|
|
443
|
+
{bold}${lifeSummary}{/bold} ${typeBreakdown} ${projects}`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/commands/memory/dashboard/widgets/search-modal.ts
|
|
450
|
+
import blessed5 from "blessed";
|
|
451
|
+
function createSearchModal(screen) {
|
|
452
|
+
const box = blessed5.box({
|
|
453
|
+
top: "center",
|
|
454
|
+
left: "center",
|
|
455
|
+
width: 60,
|
|
456
|
+
height: 5,
|
|
457
|
+
border: { type: "line" },
|
|
458
|
+
label: " Search ",
|
|
459
|
+
tags: true,
|
|
460
|
+
hidden: true,
|
|
461
|
+
style: {
|
|
462
|
+
border: { fg: "yellow" },
|
|
463
|
+
bg: "black"
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
const input = blessed5.textbox({
|
|
467
|
+
parent: box,
|
|
468
|
+
top: 1,
|
|
469
|
+
left: 1,
|
|
470
|
+
right: 1,
|
|
471
|
+
height: 1,
|
|
472
|
+
inputOnFocus: true,
|
|
473
|
+
style: {
|
|
474
|
+
fg: "white",
|
|
475
|
+
bg: "black"
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
screen.append(box);
|
|
479
|
+
let submitCallback = null;
|
|
480
|
+
let cancelCallback = null;
|
|
481
|
+
let isOpenState = false;
|
|
482
|
+
let previousFocus = null;
|
|
483
|
+
function restoreFocus() {
|
|
484
|
+
if (previousFocus) {
|
|
485
|
+
previousFocus.focus();
|
|
486
|
+
previousFocus = null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
input.on("submit", (value) => {
|
|
490
|
+
isOpenState = false;
|
|
491
|
+
box.hide();
|
|
492
|
+
restoreFocus();
|
|
493
|
+
screen.render();
|
|
494
|
+
if (value.trim()) submitCallback?.(value.trim());
|
|
495
|
+
});
|
|
496
|
+
input.on("cancel", () => {
|
|
497
|
+
isOpenState = false;
|
|
498
|
+
box.hide();
|
|
499
|
+
restoreFocus();
|
|
500
|
+
screen.render();
|
|
501
|
+
cancelCallback?.();
|
|
502
|
+
});
|
|
503
|
+
return {
|
|
504
|
+
widget: box,
|
|
505
|
+
open() {
|
|
506
|
+
previousFocus = screen.focused;
|
|
507
|
+
input.clearValue();
|
|
508
|
+
isOpenState = true;
|
|
509
|
+
box.show();
|
|
510
|
+
input.focus();
|
|
511
|
+
screen.render();
|
|
512
|
+
},
|
|
513
|
+
close() {
|
|
514
|
+
isOpenState = false;
|
|
515
|
+
box.hide();
|
|
516
|
+
restoreFocus();
|
|
517
|
+
screen.render();
|
|
518
|
+
},
|
|
519
|
+
isOpen() {
|
|
520
|
+
return isOpenState;
|
|
521
|
+
},
|
|
522
|
+
onSubmit(callback) {
|
|
523
|
+
submitCallback = callback;
|
|
524
|
+
},
|
|
525
|
+
onCancel(callback) {
|
|
526
|
+
cancelCallback = callback;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/commands/memory/dashboard/widgets/project-list.ts
|
|
532
|
+
import blessed6 from "blessed";
|
|
533
|
+
function createProjectList(screen) {
|
|
534
|
+
const list = blessed6.list({
|
|
535
|
+
parent: screen,
|
|
536
|
+
top: 1,
|
|
537
|
+
left: "60%",
|
|
538
|
+
width: "40%",
|
|
539
|
+
height: "35%-1",
|
|
540
|
+
keys: true,
|
|
541
|
+
vi: true,
|
|
542
|
+
mouse: true,
|
|
543
|
+
tags: true,
|
|
544
|
+
border: { type: "line" },
|
|
545
|
+
label: " Projects ",
|
|
546
|
+
scrollable: true,
|
|
547
|
+
style: {
|
|
548
|
+
border: { fg: "magenta" },
|
|
549
|
+
item: { fg: "white" },
|
|
550
|
+
selected: { fg: "black", bg: "yellow" }
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
let rows = [];
|
|
554
|
+
let selectCallback = null;
|
|
555
|
+
let suppressSelect = false;
|
|
556
|
+
function buildRows(memories) {
|
|
557
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
558
|
+
for (const memory of memories) {
|
|
559
|
+
const project = memory.project ?? "(none)";
|
|
560
|
+
const bucket = byProject.get(project);
|
|
561
|
+
if (bucket) {
|
|
562
|
+
bucket.push(memory);
|
|
563
|
+
} else {
|
|
564
|
+
byProject.set(project, [memory]);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const items = [];
|
|
568
|
+
for (const [project, projectMemories] of byProject.entries()) {
|
|
569
|
+
const avgRemaining = projectMemories.reduce(
|
|
570
|
+
(sum, m) => sum + computeLifespan(m).remaining,
|
|
571
|
+
0
|
|
572
|
+
) / projectMemories.length;
|
|
573
|
+
items.push({
|
|
574
|
+
project,
|
|
575
|
+
total: projectMemories.length,
|
|
576
|
+
healthPct: Math.round(avgRemaining * 100)
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
items.sort((a, b) => b.total - a.total);
|
|
580
|
+
const allHealth = memories.length > 0 ? Math.round(
|
|
581
|
+
memories.reduce(
|
|
582
|
+
(sum, m) => sum + computeLifespan(m).remaining,
|
|
583
|
+
0
|
|
584
|
+
) / memories.length * 100
|
|
585
|
+
) : 0;
|
|
586
|
+
return [
|
|
587
|
+
{ project: void 0, total: memories.length, healthPct: allHealth },
|
|
588
|
+
...items
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
function render(activeProject) {
|
|
592
|
+
const body = rows.map((row) => {
|
|
593
|
+
const isActive = row.project === activeProject || row.project === void 0 && !activeProject;
|
|
594
|
+
const projectName = row.project ?? "All projects";
|
|
595
|
+
const healthColor = row.healthPct > 62 ? "green" : row.healthPct > 32 ? "yellow" : "red";
|
|
596
|
+
const nameCol = projectName.padEnd(20, " ").slice(0, 20);
|
|
597
|
+
const countCol = String(row.total).padStart(3, " ");
|
|
598
|
+
const healthCol = `${String(row.healthPct).padStart(3, " ")}%`;
|
|
599
|
+
return [
|
|
600
|
+
isActive ? "{bold}> {/bold}" : " ",
|
|
601
|
+
`{bold}${nameCol}{/bold}`,
|
|
602
|
+
` {cyan-fg}${countCol} mem{/cyan-fg}`,
|
|
603
|
+
` {${healthColor}-fg}${healthCol} health{/${healthColor}-fg}`
|
|
604
|
+
].join("");
|
|
605
|
+
});
|
|
606
|
+
list.setItems(body);
|
|
607
|
+
}
|
|
608
|
+
function emitSelection() {
|
|
609
|
+
if (suppressSelect) return;
|
|
610
|
+
const selectedIndex = list.selected;
|
|
611
|
+
const row = rows[selectedIndex];
|
|
612
|
+
if (!row || !selectCallback) return;
|
|
613
|
+
selectCallback(row.project);
|
|
614
|
+
}
|
|
615
|
+
list.on("select", () => emitSelection());
|
|
616
|
+
return {
|
|
617
|
+
widget: list,
|
|
618
|
+
setData(memories, activeProject) {
|
|
619
|
+
rows = buildRows(memories);
|
|
620
|
+
render(activeProject);
|
|
621
|
+
if (rows.length > 0) {
|
|
622
|
+
suppressSelect = true;
|
|
623
|
+
const idx = rows.findIndex(
|
|
624
|
+
(r) => r.project === activeProject || r.project === void 0 && !activeProject
|
|
625
|
+
);
|
|
626
|
+
list.select(Math.max(0, idx));
|
|
627
|
+
setImmediate(() => {
|
|
628
|
+
suppressSelect = false;
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
onSelect(callback) {
|
|
633
|
+
selectCallback = callback;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/commands/memory/dashboard/widgets/project-picker-modal.ts
|
|
639
|
+
import blessed7 from "blessed";
|
|
640
|
+
function createProjectPickerModal(screen) {
|
|
641
|
+
const box = blessed7.box({
|
|
642
|
+
parent: screen,
|
|
643
|
+
top: "center",
|
|
644
|
+
left: "center",
|
|
645
|
+
width: 52,
|
|
646
|
+
height: 14,
|
|
647
|
+
border: { type: "line" },
|
|
648
|
+
label: " Project Picker ",
|
|
649
|
+
tags: true,
|
|
650
|
+
hidden: true,
|
|
651
|
+
style: {
|
|
652
|
+
border: { fg: "yellow" },
|
|
653
|
+
bg: "black"
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
const hint = blessed7.box({
|
|
657
|
+
parent: box,
|
|
658
|
+
top: 0,
|
|
659
|
+
left: 1,
|
|
660
|
+
right: 1,
|
|
661
|
+
height: 1,
|
|
662
|
+
tags: true,
|
|
663
|
+
content: "{gray-fg}Enter=select Esc=close{/gray-fg}"
|
|
664
|
+
});
|
|
665
|
+
const list = blessed7.list({
|
|
666
|
+
parent: box,
|
|
667
|
+
top: 1,
|
|
668
|
+
left: 1,
|
|
669
|
+
right: 1,
|
|
670
|
+
bottom: 1,
|
|
671
|
+
keys: true,
|
|
672
|
+
vi: true,
|
|
673
|
+
mouse: true,
|
|
674
|
+
tags: true,
|
|
675
|
+
style: {
|
|
676
|
+
item: { fg: "white" },
|
|
677
|
+
selected: { fg: "black", bg: "yellow" }
|
|
678
|
+
},
|
|
679
|
+
scrollbar: {
|
|
680
|
+
style: { bg: "yellow" }
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
let isOpenState = false;
|
|
684
|
+
let options = [];
|
|
685
|
+
let submitCallback = null;
|
|
686
|
+
let previousFocus = null;
|
|
687
|
+
function restoreFocus() {
|
|
688
|
+
if (previousFocus) {
|
|
689
|
+
previousFocus.focus();
|
|
690
|
+
previousFocus = null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function emitSelection() {
|
|
694
|
+
const selectedIndex = list.selected;
|
|
695
|
+
const selected = options[selectedIndex];
|
|
696
|
+
if (!selected || !submitCallback) return;
|
|
697
|
+
submitCallback(selected.project);
|
|
698
|
+
box.hide();
|
|
699
|
+
isOpenState = false;
|
|
700
|
+
restoreFocus();
|
|
701
|
+
screen.render();
|
|
702
|
+
}
|
|
703
|
+
list.key(["escape"], () => {
|
|
704
|
+
box.hide();
|
|
705
|
+
isOpenState = false;
|
|
706
|
+
restoreFocus();
|
|
707
|
+
screen.render();
|
|
708
|
+
});
|
|
709
|
+
list.on("select", () => emitSelection());
|
|
710
|
+
return {
|
|
711
|
+
open(projects, activeProject) {
|
|
712
|
+
options = [
|
|
713
|
+
{ label: "All projects", project: void 0 },
|
|
714
|
+
...projects.map((project) => ({ label: project, project }))
|
|
715
|
+
];
|
|
716
|
+
const items = options.map((option) => {
|
|
717
|
+
const active = option.project === activeProject || option.project === void 0 && !activeProject;
|
|
718
|
+
return `${active ? "> " : " "}${option.label}`;
|
|
719
|
+
});
|
|
720
|
+
list.setItems(items);
|
|
721
|
+
const activeIndex = options.findIndex(
|
|
722
|
+
(o) => o.project === activeProject || o.project === void 0 && !activeProject
|
|
723
|
+
);
|
|
724
|
+
list.select(Math.max(0, activeIndex));
|
|
725
|
+
previousFocus = screen.focused;
|
|
726
|
+
box.show();
|
|
727
|
+
isOpenState = true;
|
|
728
|
+
list.focus();
|
|
729
|
+
hint.setFront();
|
|
730
|
+
screen.render();
|
|
731
|
+
},
|
|
732
|
+
close() {
|
|
733
|
+
box.hide();
|
|
734
|
+
isOpenState = false;
|
|
735
|
+
restoreFocus();
|
|
736
|
+
screen.render();
|
|
737
|
+
},
|
|
738
|
+
isOpen() {
|
|
739
|
+
return isOpenState;
|
|
740
|
+
},
|
|
741
|
+
onSubmit(callback) {
|
|
742
|
+
submitCallback = callback;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/commands/memory/dashboard/keybindings.ts
|
|
748
|
+
var TYPE_KEYS = {
|
|
749
|
+
"1": "working",
|
|
750
|
+
"2": "episodic",
|
|
751
|
+
"3": "semantic",
|
|
752
|
+
"4": "procedural",
|
|
753
|
+
"5": "pattern"
|
|
754
|
+
};
|
|
755
|
+
function registerKeybindings(screen, actions) {
|
|
756
|
+
screen.key(["q"], () => actions.quit());
|
|
757
|
+
screen.key(["C-c"], () => actions.quit());
|
|
758
|
+
screen.key(["r"], () => actions.refresh());
|
|
759
|
+
screen.key(["/"], () => actions.openSearch());
|
|
760
|
+
screen.key(["escape"], () => actions.closeSearch());
|
|
761
|
+
screen.key(["tab"], () => actions.focusNext());
|
|
762
|
+
screen.key(["p"], () => actions.openProjectPicker());
|
|
763
|
+
screen.key(["]", "right"], () => actions.cycleProjectNext());
|
|
764
|
+
screen.key(["[", "left"], () => actions.cycleProjectPrev());
|
|
765
|
+
screen.key(["s"], () => actions.cycleSort());
|
|
766
|
+
screen.key(["l"], () => actions.cycleLifespan());
|
|
767
|
+
screen.key(["?"], () => actions.showHelp());
|
|
768
|
+
screen.key(["0"], () => actions.filterByType(null));
|
|
769
|
+
for (const [key, type] of Object.entries(TYPE_KEYS)) {
|
|
770
|
+
screen.key([key], () => actions.filterByType(type));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/commands/memory/dashboard/layout.ts
|
|
775
|
+
var SORT_MODES = [
|
|
776
|
+
"importance",
|
|
777
|
+
"age",
|
|
778
|
+
"access",
|
|
779
|
+
"lifespan"
|
|
780
|
+
];
|
|
781
|
+
var LIFESPAN_FILTERS = [
|
|
782
|
+
void 0,
|
|
783
|
+
"healthy",
|
|
784
|
+
"fading",
|
|
785
|
+
"stale",
|
|
786
|
+
"session"
|
|
787
|
+
];
|
|
788
|
+
function createDashboard(screen, dataSource, options) {
|
|
789
|
+
let currentTypeFilter;
|
|
790
|
+
let currentProject;
|
|
791
|
+
let currentProjectIndex = -1;
|
|
792
|
+
let currentLifespanFilter;
|
|
793
|
+
let currentSearchQuery = "";
|
|
794
|
+
let sortMode = "importance";
|
|
795
|
+
let currentMemories = [];
|
|
796
|
+
const header = createHeader(screen);
|
|
797
|
+
const memoryList = createMemoryList(screen);
|
|
798
|
+
const projectList = createProjectList(screen);
|
|
799
|
+
const memoryDetail = createMemoryDetail(screen);
|
|
800
|
+
const statsBar = createStatsBar(screen);
|
|
801
|
+
const searchModal = createSearchModal(screen);
|
|
802
|
+
const projectPickerModal = createProjectPickerModal(screen);
|
|
803
|
+
header.widget.top = 0;
|
|
804
|
+
header.widget.left = 0;
|
|
805
|
+
header.widget.width = "100%";
|
|
806
|
+
header.widget.height = 1;
|
|
807
|
+
memoryList.widget.top = 1;
|
|
808
|
+
memoryList.widget.left = 0;
|
|
809
|
+
memoryList.widget.width = "60%";
|
|
810
|
+
memoryList.widget.height = screen.rows - 4;
|
|
811
|
+
projectList.widget.top = 1;
|
|
812
|
+
projectList.widget.left = "60%";
|
|
813
|
+
projectList.widget.width = "40%";
|
|
814
|
+
projectList.widget.height = Math.max(
|
|
815
|
+
8,
|
|
816
|
+
Math.floor((screen.rows - 4) * 0.35)
|
|
817
|
+
);
|
|
818
|
+
memoryDetail.widget.top = projectList.widget.height + 1;
|
|
819
|
+
memoryDetail.widget.left = "60%";
|
|
820
|
+
memoryDetail.widget.width = "40%";
|
|
821
|
+
memoryDetail.widget.height = screen.rows - 4 - projectList.widget.height;
|
|
822
|
+
statsBar.widget.bottom = 0;
|
|
823
|
+
statsBar.widget.left = 0;
|
|
824
|
+
statsBar.widget.width = "100%";
|
|
825
|
+
statsBar.widget.height = 3;
|
|
826
|
+
memoryList.onSelect((memory) => {
|
|
827
|
+
const relations = dataSource.getRelationsForMemory(memory.id);
|
|
828
|
+
memoryDetail.showMemory(memory, relations);
|
|
829
|
+
screen.render();
|
|
830
|
+
});
|
|
831
|
+
projectList.onSelect((project) => {
|
|
832
|
+
if (project === currentProject) return;
|
|
833
|
+
currentProject = project;
|
|
834
|
+
currentProjectIndex = dataSource.getProjects().findIndex((p) => p === project);
|
|
835
|
+
refresh();
|
|
836
|
+
});
|
|
837
|
+
projectPickerModal.onSubmit((project) => {
|
|
838
|
+
if (project === currentProject) return;
|
|
839
|
+
currentProject = project;
|
|
840
|
+
currentProjectIndex = dataSource.getProjects().findIndex((p) => p === project);
|
|
841
|
+
refresh();
|
|
842
|
+
});
|
|
843
|
+
searchModal.onSubmit((query) => {
|
|
844
|
+
currentSearchQuery = query.trim();
|
|
845
|
+
refresh();
|
|
846
|
+
});
|
|
847
|
+
searchModal.onCancel(() => {
|
|
848
|
+
screen.render();
|
|
849
|
+
});
|
|
850
|
+
screen.on("resize", () => {
|
|
851
|
+
const listHeight = screen.rows - 4;
|
|
852
|
+
memoryList.widget.height = listHeight;
|
|
853
|
+
projectList.widget.height = Math.max(
|
|
854
|
+
8,
|
|
855
|
+
Math.floor(listHeight * 0.35)
|
|
856
|
+
);
|
|
857
|
+
memoryDetail.widget.top = projectList.widget.height + 1;
|
|
858
|
+
memoryDetail.widget.height = listHeight - projectList.widget.height;
|
|
859
|
+
screen.render();
|
|
860
|
+
});
|
|
861
|
+
function sortMemories(memories) {
|
|
862
|
+
const sorted = [...memories];
|
|
863
|
+
switch (sortMode) {
|
|
864
|
+
case "importance":
|
|
865
|
+
sorted.sort((a, b) => b.importance - a.importance);
|
|
866
|
+
break;
|
|
867
|
+
case "age":
|
|
868
|
+
sorted.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
869
|
+
break;
|
|
870
|
+
case "access":
|
|
871
|
+
sorted.sort((a, b) => b.accessCount - a.accessCount);
|
|
872
|
+
break;
|
|
873
|
+
case "lifespan":
|
|
874
|
+
sorted.sort(
|
|
875
|
+
(a, b) => computeLifespan(a).remaining - computeLifespan(b).remaining
|
|
876
|
+
);
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
return sorted;
|
|
880
|
+
}
|
|
881
|
+
function getCurrentProject() {
|
|
882
|
+
return currentProject;
|
|
883
|
+
}
|
|
884
|
+
function applyTextFilter(memories, query) {
|
|
885
|
+
if (!query || query.trim().length === 0) return memories;
|
|
886
|
+
const lower = query.toLowerCase();
|
|
887
|
+
return memories.filter(
|
|
888
|
+
(m) => (m.title?.toLowerCase().includes(lower) ?? false) || m.content.toLowerCase().includes(lower) || m.tags.some((t) => t.toLowerCase().includes(lower))
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
function applyLifespanFilter(memories) {
|
|
892
|
+
if (!currentLifespanFilter) return memories;
|
|
893
|
+
return memories.filter(
|
|
894
|
+
(m) => computeLifespan(m).status === currentLifespanFilter
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
function refresh() {
|
|
898
|
+
const previousFocus = screen.focused;
|
|
899
|
+
dataSource.refresh();
|
|
900
|
+
const raw = dataSource.getMemories({
|
|
901
|
+
type: currentTypeFilter,
|
|
902
|
+
project: getCurrentProject()
|
|
903
|
+
});
|
|
904
|
+
const withLife = applyLifespanFilter(raw);
|
|
905
|
+
const withSearch = applyTextFilter(withLife, currentSearchQuery);
|
|
906
|
+
currentMemories = sortMemories(withSearch);
|
|
907
|
+
memoryList.setData(currentMemories);
|
|
908
|
+
projectList.setData(dataSource.getMemories(), currentProject);
|
|
909
|
+
if (currentMemories.length === 0) {
|
|
910
|
+
memoryDetail.clear();
|
|
911
|
+
}
|
|
912
|
+
const focusableWidgets = [memoryList.widget, projectList.widget, memoryDetail.widget];
|
|
913
|
+
if (previousFocus && focusableWidgets.includes(previousFocus)) {
|
|
914
|
+
previousFocus.focus();
|
|
915
|
+
} else {
|
|
916
|
+
memoryList.focus();
|
|
917
|
+
}
|
|
918
|
+
statsBar.setStats(dataSource.getStats(), currentMemories);
|
|
919
|
+
const project = getCurrentProject();
|
|
920
|
+
const label = [
|
|
921
|
+
project ? `project:${project}` : "all projects",
|
|
922
|
+
currentTypeFilter ? `type:${currentTypeFilter}` : "all types",
|
|
923
|
+
currentLifespanFilter ? `life:${currentLifespanFilter}` : "all life",
|
|
924
|
+
currentSearchQuery ? `search:${currentSearchQuery}` : "search:off",
|
|
925
|
+
`sort:${sortMode}`
|
|
926
|
+
].join(" | ");
|
|
927
|
+
header.setLabel(`agentic-memory cockpit [${label}]`);
|
|
928
|
+
screen.render();
|
|
929
|
+
}
|
|
930
|
+
function showHelp() {
|
|
931
|
+
const helpBox = blessed8.box({
|
|
932
|
+
parent: screen,
|
|
933
|
+
top: "center",
|
|
934
|
+
left: "center",
|
|
935
|
+
width: 50,
|
|
936
|
+
height: 18,
|
|
937
|
+
border: { type: "line" },
|
|
938
|
+
style: { border: { fg: "yellow" }, bg: "black" },
|
|
939
|
+
tags: true,
|
|
940
|
+
content: [
|
|
941
|
+
"{bold}{yellow-fg}Keybindings{/yellow-fg}{/bold}",
|
|
942
|
+
"",
|
|
943
|
+
" j / k Navigate list",
|
|
944
|
+
" Enter Select memory",
|
|
945
|
+
" / Search",
|
|
946
|
+
" Esc Close search / Clear search",
|
|
947
|
+
" r Refresh",
|
|
948
|
+
" 1-5 Filter by type (0 = all)",
|
|
949
|
+
" l Cycle lifespan filter",
|
|
950
|
+
" p Open project picker",
|
|
951
|
+
" [ / ] Previous / next project",
|
|
952
|
+
" s Cycle sort mode (incl lifespan)",
|
|
953
|
+
" Tab Focus next pane (list/projects/detail)",
|
|
954
|
+
" ? Show this help",
|
|
955
|
+
" q Quit",
|
|
956
|
+
"",
|
|
957
|
+
"{center}Press any key to close{/center}"
|
|
958
|
+
].join("\n")
|
|
959
|
+
});
|
|
960
|
+
screen.render();
|
|
961
|
+
helpBox.once("keypress", () => {
|
|
962
|
+
helpBox.destroy();
|
|
963
|
+
screen.render();
|
|
964
|
+
});
|
|
965
|
+
helpBox.focus();
|
|
966
|
+
}
|
|
967
|
+
registerKeybindings(screen, {
|
|
968
|
+
refresh,
|
|
969
|
+
openSearch: () => searchModal.open(),
|
|
970
|
+
closeSearch: () => {
|
|
971
|
+
if (projectPickerModal.isOpen()) {
|
|
972
|
+
projectPickerModal.close();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (searchModal.isOpen()) {
|
|
976
|
+
searchModal.close();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (currentSearchQuery.length > 0) {
|
|
980
|
+
currentSearchQuery = "";
|
|
981
|
+
refresh();
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
openProjectPicker: () => {
|
|
985
|
+
projectPickerModal.open(
|
|
986
|
+
dataSource.getProjects(),
|
|
987
|
+
currentProject
|
|
988
|
+
);
|
|
989
|
+
},
|
|
990
|
+
filterByType: (type) => {
|
|
991
|
+
currentTypeFilter = type === null ? void 0 : type;
|
|
992
|
+
refresh();
|
|
993
|
+
},
|
|
994
|
+
cycleLifespan: () => {
|
|
995
|
+
const idx = LIFESPAN_FILTERS.findIndex(
|
|
996
|
+
(x) => x === currentLifespanFilter
|
|
997
|
+
);
|
|
998
|
+
currentLifespanFilter = LIFESPAN_FILTERS[(idx + 1) % LIFESPAN_FILTERS.length];
|
|
999
|
+
refresh();
|
|
1000
|
+
},
|
|
1001
|
+
cycleProjectNext: () => {
|
|
1002
|
+
const projects = dataSource.getProjects();
|
|
1003
|
+
if (projects.length === 0) return;
|
|
1004
|
+
currentProjectIndex = currentProjectIndex >= projects.length - 1 ? -1 : currentProjectIndex + 1;
|
|
1005
|
+
currentProject = currentProjectIndex < 0 ? void 0 : projects[currentProjectIndex];
|
|
1006
|
+
refresh();
|
|
1007
|
+
memoryList.focus();
|
|
1008
|
+
},
|
|
1009
|
+
cycleProjectPrev: () => {
|
|
1010
|
+
const projects = dataSource.getProjects();
|
|
1011
|
+
if (projects.length === 0) return;
|
|
1012
|
+
currentProjectIndex = currentProjectIndex <= -1 ? projects.length - 1 : currentProjectIndex - 1;
|
|
1013
|
+
currentProject = currentProjectIndex < 0 ? void 0 : projects[currentProjectIndex];
|
|
1014
|
+
refresh();
|
|
1015
|
+
memoryList.focus();
|
|
1016
|
+
},
|
|
1017
|
+
cycleSort: () => {
|
|
1018
|
+
const idx = SORT_MODES.indexOf(sortMode);
|
|
1019
|
+
sortMode = SORT_MODES[(idx + 1) % SORT_MODES.length];
|
|
1020
|
+
refresh();
|
|
1021
|
+
},
|
|
1022
|
+
quit: () => options.onQuit(),
|
|
1023
|
+
focusNext: () => {
|
|
1024
|
+
if (screen.focused === memoryList.widget) {
|
|
1025
|
+
projectList.widget.focus();
|
|
1026
|
+
} else if (screen.focused === projectList.widget) {
|
|
1027
|
+
memoryDetail.widget.focus();
|
|
1028
|
+
} else {
|
|
1029
|
+
memoryList.widget.focus();
|
|
1030
|
+
}
|
|
1031
|
+
screen.render();
|
|
1032
|
+
},
|
|
1033
|
+
showHelp
|
|
1034
|
+
});
|
|
1035
|
+
function destroy() {
|
|
1036
|
+
header.widget.destroy();
|
|
1037
|
+
memoryList.widget.destroy();
|
|
1038
|
+
projectList.widget.destroy();
|
|
1039
|
+
memoryDetail.widget.destroy();
|
|
1040
|
+
statsBar.widget.destroy();
|
|
1041
|
+
projectPickerModal.close();
|
|
1042
|
+
screen.render();
|
|
1043
|
+
}
|
|
1044
|
+
return { refresh, destroy };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/commands/memory/dashboard/tui.ts
|
|
1048
|
+
async function startTui(options) {
|
|
1049
|
+
if (process.env["TERM"]?.includes("256color")) {
|
|
1050
|
+
process.env["TERM"] = "xterm";
|
|
1051
|
+
}
|
|
1052
|
+
const config = loadConfig(
|
|
1053
|
+
options?.dbPath ? { dataDir: options.dbPath } : void 0
|
|
1054
|
+
);
|
|
1055
|
+
const dataDir = resolveDataDir(config.dataDir);
|
|
1056
|
+
const db = createDatabase({ dataDir });
|
|
1057
|
+
migrate(db);
|
|
1058
|
+
const memoryRepo = new MemoryRepo(db);
|
|
1059
|
+
const relationRepo = new RelationRepo(db);
|
|
1060
|
+
const searchRepo = new SearchRepo(db);
|
|
1061
|
+
const screen = blessed9.screen({
|
|
1062
|
+
smartCSR: true,
|
|
1063
|
+
title: "agentic-memory"
|
|
1064
|
+
});
|
|
1065
|
+
const dataSource = new DashboardDataSource(
|
|
1066
|
+
memoryRepo,
|
|
1067
|
+
relationRepo,
|
|
1068
|
+
searchRepo,
|
|
1069
|
+
dataDir
|
|
1070
|
+
);
|
|
1071
|
+
let shuttingDown = false;
|
|
1072
|
+
function shutdown() {
|
|
1073
|
+
if (shuttingDown) return;
|
|
1074
|
+
shuttingDown = true;
|
|
1075
|
+
dataSource.stopWatching();
|
|
1076
|
+
screen.destroy();
|
|
1077
|
+
closeDatabase(db);
|
|
1078
|
+
}
|
|
1079
|
+
const dashboard = createDashboard(screen, dataSource, { onQuit: shutdown });
|
|
1080
|
+
dashboard.refresh();
|
|
1081
|
+
dataSource.startWatching(() => {
|
|
1082
|
+
dashboard.refresh();
|
|
1083
|
+
});
|
|
1084
|
+
process.on("SIGINT", shutdown);
|
|
1085
|
+
process.on("SIGTERM", shutdown);
|
|
1086
|
+
process.on("uncaughtException", (err) => {
|
|
1087
|
+
dataSource.stopWatching();
|
|
1088
|
+
screen.destroy();
|
|
1089
|
+
closeDatabase(db);
|
|
1090
|
+
process.stderr.write(
|
|
1091
|
+
`[agentic-memory] dashboard crashed: ${err.message}
|
|
1092
|
+
`
|
|
1093
|
+
);
|
|
1094
|
+
process.exit(1);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
export {
|
|
1098
|
+
startTui
|
|
1099
|
+
};
|
|
1100
|
+
//# sourceMappingURL=tui-R25NTQ4K.js.map
|