@mauribadnights/clooks 0.3.2 → 0.4.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/dist/tui.js ADDED
@@ -0,0 +1,560 @@
1
+ "use strict";
2
+ // clooks interactive TUI dashboard for `clooks stats -i`
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.launchDashboard = launchDashboard;
8
+ const blessed_1 = __importDefault(require("blessed"));
9
+ const fs_1 = require("fs");
10
+ const constants_js_1 = require("./constants.js");
11
+ // --- Data loading ---
12
+ function loadAllMetricEntries() {
13
+ if (!(0, fs_1.existsSync)(constants_js_1.METRICS_FILE))
14
+ return [];
15
+ try {
16
+ const raw = (0, fs_1.readFileSync)(constants_js_1.METRICS_FILE, 'utf-8');
17
+ const lines = raw.trim().split('\n').filter(Boolean);
18
+ return lines.map((line) => JSON.parse(line));
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ }
24
+ function aggregateByEvent(entries) {
25
+ const byEvent = new Map();
26
+ for (const e of entries) {
27
+ const arr = byEvent.get(e.event) ?? [];
28
+ arr.push(e);
29
+ byEvent.set(e.event, arr);
30
+ }
31
+ const result = [];
32
+ for (const [event, evEntries] of byEvent) {
33
+ const durations = evEntries.map((e) => e.duration_ms);
34
+ const byHandler = new Map();
35
+ for (const e of evEntries) {
36
+ const arr = byHandler.get(e.handler) ?? [];
37
+ arr.push(e);
38
+ byHandler.set(e.handler, arr);
39
+ }
40
+ const handlers = [];
41
+ for (const [handler, hEntries] of byHandler) {
42
+ const hDurations = hEntries.map((e) => e.duration_ms);
43
+ handlers.push({
44
+ handler,
45
+ event,
46
+ fires: hEntries.length,
47
+ errors: hEntries.filter((e) => !e.ok).length,
48
+ avgMs: hDurations.reduce((a, b) => a + b, 0) / hDurations.length,
49
+ maxMs: Math.max(...hDurations),
50
+ });
51
+ }
52
+ handlers.sort((a, b) => b.avgMs - a.avgMs);
53
+ result.push({
54
+ event,
55
+ fires: evEntries.length,
56
+ errors: evEntries.filter((e) => !e.ok).length,
57
+ avgMs: durations.reduce((a, b) => a + b, 0) / durations.length,
58
+ maxMs: Math.max(...durations),
59
+ handlers,
60
+ });
61
+ }
62
+ return result.sort((a, b) => b.fires - a.fires);
63
+ }
64
+ function aggregateAllHandlers(entries) {
65
+ const byHandler = new Map();
66
+ for (const e of entries) {
67
+ const arr = byHandler.get(e.handler) ?? [];
68
+ arr.push(e);
69
+ byHandler.set(e.handler, arr);
70
+ }
71
+ const result = [];
72
+ for (const [handler, hEntries] of byHandler) {
73
+ const durations = hEntries.map((e) => e.duration_ms);
74
+ result.push({
75
+ handler,
76
+ event: hEntries[0].event,
77
+ fires: hEntries.length,
78
+ errors: hEntries.filter((e) => !e.ok).length,
79
+ avgMs: durations.reduce((a, b) => a + b, 0) / durations.length,
80
+ maxMs: Math.max(...durations),
81
+ });
82
+ }
83
+ return result.sort((a, b) => b.avgMs - a.avgMs);
84
+ }
85
+ function aggregateByAgent(entries) {
86
+ // Group by session_id first, then infer agent from handler patterns
87
+ // Since MetricEntry doesn't have agent_type, we group by session_id
88
+ // and label sessions by the set of handlers that fired
89
+ const bySid = new Map();
90
+ for (const e of entries) {
91
+ const sid = e.session_id ?? 'unknown';
92
+ const arr = bySid.get(sid) ?? [];
93
+ arr.push(e);
94
+ bySid.set(sid, arr);
95
+ }
96
+ // Derive agent name from handler prefixes (e.g., "gsd-*" => "gsd", "cq-*" => "cq")
97
+ const agentMap = new Map();
98
+ for (const [_sid, sEntries] of bySid) {
99
+ // Use the most common handler prefix as agent name
100
+ const prefixes = new Map();
101
+ for (const e of sEntries) {
102
+ const prefix = e.handler.split('-')[0] || e.handler;
103
+ prefixes.set(prefix, (prefixes.get(prefix) ?? 0) + 1);
104
+ }
105
+ let agent = 'unknown';
106
+ let maxCount = 0;
107
+ for (const [p, c] of prefixes) {
108
+ if (c > maxCount) {
109
+ agent = p;
110
+ maxCount = c;
111
+ }
112
+ }
113
+ const arr = agentMap.get(agent) ?? [];
114
+ arr.push(...sEntries);
115
+ agentMap.set(agent, arr);
116
+ }
117
+ const result = [];
118
+ for (const [agent, aEntries] of agentMap) {
119
+ const handlers = aggregateHandlersFrom(aEntries);
120
+ result.push({
121
+ agent,
122
+ fires: aEntries.length,
123
+ errors: aEntries.filter((e) => !e.ok).length,
124
+ handlers,
125
+ });
126
+ }
127
+ return result.sort((a, b) => b.fires - a.fires);
128
+ }
129
+ function aggregateByProject(entries) {
130
+ // MetricEntry doesn't have cwd, so we group by session_id and use 'unknown' for project
131
+ // If entries had cwd, we'd group by that. For now, group by session_id as proxy.
132
+ // Actually, let's check if there's a cwd-like field or group by session
133
+ const bySid = new Map();
134
+ for (const e of entries) {
135
+ const key = e.session_id ?? 'unknown';
136
+ const arr = bySid.get(key) ?? [];
137
+ arr.push(e);
138
+ bySid.set(key, arr);
139
+ }
140
+ const result = [];
141
+ for (const [sid, pEntries] of bySid) {
142
+ const durations = pEntries.map((e) => e.duration_ms);
143
+ const handlers = aggregateHandlersFrom(pEntries);
144
+ const label = sid.length > 16 ? sid.slice(0, 8) + '...' + sid.slice(-8) : sid;
145
+ result.push({
146
+ project: label,
147
+ fires: pEntries.length,
148
+ errors: pEntries.filter((e) => !e.ok).length,
149
+ avgMs: durations.reduce((a, b) => a + b, 0) / durations.length,
150
+ handlers,
151
+ });
152
+ }
153
+ return result.sort((a, b) => b.fires - a.fires);
154
+ }
155
+ function aggregateHandlersFrom(entries) {
156
+ const byHandler = new Map();
157
+ for (const e of entries) {
158
+ const arr = byHandler.get(e.handler) ?? [];
159
+ arr.push(e);
160
+ byHandler.set(e.handler, arr);
161
+ }
162
+ const result = [];
163
+ for (const [handler, hEntries] of byHandler) {
164
+ const durations = hEntries.map((e) => e.duration_ms);
165
+ result.push({
166
+ handler,
167
+ event: hEntries[0].event,
168
+ fires: hEntries.length,
169
+ errors: hEntries.filter((e) => !e.ok).length,
170
+ avgMs: durations.reduce((a, b) => a + b, 0) / durations.length,
171
+ maxMs: Math.max(...durations),
172
+ });
173
+ }
174
+ return result.sort((a, b) => b.avgMs - a.avgMs);
175
+ }
176
+ // --- Fuzzy search ---
177
+ function fuzzyMatch(query, target) {
178
+ const q = query.toLowerCase();
179
+ const t = target.toLowerCase();
180
+ let qi = 0;
181
+ for (let ti = 0; ti < t.length && qi < q.length; ti++) {
182
+ if (t[ti] === q[qi])
183
+ qi++;
184
+ }
185
+ return qi === q.length;
186
+ }
187
+ // --- Color coding ---
188
+ function colorize(avgMs, text) {
189
+ if (avgMs < 5)
190
+ return `{green-fg}${text}{/green-fg}`;
191
+ if (avgMs <= 50)
192
+ return `{yellow-fg}${text}{/yellow-fg}`;
193
+ return `{red-fg}${text}{/red-fg}`;
194
+ }
195
+ function colorizeErrors(errors, text) {
196
+ if (errors === 0)
197
+ return `{green-fg}${text}{/green-fg}`;
198
+ return `{red-fg}${text}{/red-fg}`;
199
+ }
200
+ const VIEW_ORDER = ['events', 'handlers', 'agents', 'projects'];
201
+ const VIEW_LABELS = {
202
+ events: 'Events',
203
+ handlers: 'Handlers',
204
+ agents: 'Agents',
205
+ projects: 'Projects',
206
+ search: 'Search',
207
+ };
208
+ function launchDashboard() {
209
+ const entries = loadAllMetricEntries();
210
+ const eventAggs = aggregateByEvent(entries);
211
+ const handlerAggs = aggregateAllHandlers(entries);
212
+ const agentAggs = aggregateByAgent(entries);
213
+ const projectAggs = aggregateByProject(entries);
214
+ const totalFires = entries.length;
215
+ const totalErrors = entries.filter((e) => !e.ok).length;
216
+ const totalEvents = eventAggs.length;
217
+ const totalHandlers = handlerAggs.length;
218
+ // --- Screen setup ---
219
+ const scr = blessed_1.default.screen({
220
+ smartCSR: true,
221
+ title: 'clooks stats',
222
+ fullUnicode: true,
223
+ });
224
+ let currentView = 'events';
225
+ let drillDown = null;
226
+ let searchMode = false;
227
+ let searchQuery = '';
228
+ // --- Header (tab bar) ---
229
+ const header = blessed_1.default.box({
230
+ top: 0,
231
+ left: 0,
232
+ width: '100%',
233
+ height: 3,
234
+ tags: true,
235
+ border: { type: 'line' },
236
+ style: {
237
+ border: { fg: 'cyan' },
238
+ },
239
+ });
240
+ scr.append(header);
241
+ // --- Main list ---
242
+ const mainList = blessed_1.default.list({
243
+ top: 3,
244
+ left: 0,
245
+ width: '100%',
246
+ height: '100%-6',
247
+ tags: true,
248
+ keys: true,
249
+ vi: true,
250
+ mouse: true,
251
+ scrollable: true,
252
+ border: { type: 'line' },
253
+ style: {
254
+ border: { fg: 'blue' },
255
+ selected: { bg: 'blue', fg: 'white', bold: true },
256
+ item: { fg: 'white' },
257
+ },
258
+ scrollbar: {
259
+ ch: ' ',
260
+ style: { bg: 'blue' },
261
+ },
262
+ });
263
+ scr.append(mainList);
264
+ // --- Footer (status bar) ---
265
+ const footer = blessed_1.default.box({
266
+ bottom: 0,
267
+ left: 0,
268
+ width: '100%',
269
+ height: 3,
270
+ tags: true,
271
+ border: { type: 'line' },
272
+ style: {
273
+ border: { fg: 'cyan' },
274
+ },
275
+ });
276
+ scr.append(footer);
277
+ // --- Search input (hidden by default) ---
278
+ const searchBox = blessed_1.default.textbox({
279
+ bottom: 3,
280
+ left: 0,
281
+ width: '100%',
282
+ height: 3,
283
+ tags: true,
284
+ border: { type: 'line' },
285
+ style: {
286
+ border: { fg: 'yellow' },
287
+ fg: 'white',
288
+ },
289
+ label: ' Search (Esc to cancel) ',
290
+ hidden: true,
291
+ inputOnFocus: true,
292
+ });
293
+ scr.append(searchBox);
294
+ // --- Rendering functions ---
295
+ function renderHeader() {
296
+ const tabs = VIEW_ORDER.map((v) => {
297
+ if (v === currentView)
298
+ return `{cyan-fg}{bold}[${VIEW_LABELS[v]}]{/bold}{/cyan-fg}`;
299
+ return ` ${VIEW_LABELS[v]} `;
300
+ });
301
+ if (searchMode)
302
+ tabs.push(`{yellow-fg}{bold}[Search]{/bold}{/yellow-fg}`);
303
+ header.setContent(` ${tabs.join(' ')}`);
304
+ }
305
+ function renderFooter() {
306
+ const statusLine = `Total: ${totalEvents} events | ${totalHandlers} handlers | ${totalFires} fires | ${totalErrors} errors`;
307
+ const helpLine = 'Tab=switch view /=search Enter=drill in Esc=back q=quit';
308
+ footer.setContent(` ${statusLine}\n ${helpLine}`);
309
+ }
310
+ function formatHandlerRow(h) {
311
+ const name = h.handler.padEnd(28);
312
+ const ev = h.event.padEnd(18);
313
+ const fires = String(h.fires).padStart(6);
314
+ const errs = colorizeErrors(h.errors, String(h.errors).padStart(6));
315
+ const avg = colorize(h.avgMs, h.avgMs.toFixed(1).padStart(8));
316
+ const max = colorize(h.maxMs, h.maxMs.toFixed(1).padStart(8));
317
+ return ` ${name} ${ev} ${fires} ${errs} ${avg} ${max}`;
318
+ }
319
+ function handlerTableHeader() {
320
+ const name = 'Handler'.padEnd(28);
321
+ const ev = 'Event'.padEnd(18);
322
+ const fires = 'Fires'.padStart(6);
323
+ const errs = 'Errors'.padStart(6);
324
+ const avg = 'Avg ms'.padStart(8);
325
+ const max = 'Max ms'.padStart(8);
326
+ return `{bold} ${name} ${ev} ${fires} ${errs} ${avg} ${max}{/bold}`;
327
+ }
328
+ function renderEventsView() {
329
+ if (drillDown && 'event' in drillDown && 'handlers' in drillDown) {
330
+ const ev = drillDown;
331
+ mainList.setLabel(` Event: ${ev.event} `);
332
+ const items = [handlerTableHeader()];
333
+ for (const h of ev.handlers) {
334
+ items.push(formatHandlerRow(h));
335
+ }
336
+ mainList.setItems(items);
337
+ }
338
+ else {
339
+ mainList.setLabel(' Events ');
340
+ const items = [];
341
+ for (const ev of eventAggs) {
342
+ const name = ev.event.padEnd(22);
343
+ const fires = String(ev.fires).padStart(6);
344
+ const errs = colorizeErrors(ev.errors, String(ev.errors).padStart(6));
345
+ const avg = colorize(ev.avgMs, ev.avgMs.toFixed(1).padStart(8));
346
+ const max = colorize(ev.maxMs, ev.maxMs.toFixed(1).padStart(8));
347
+ const hCount = `(${ev.handlers.length} handlers)`.padStart(14);
348
+ items.push(` ${name} ${fires} fires ${errs} errs ${avg} avg ${max} max ${hCount}`);
349
+ }
350
+ mainList.setItems(items);
351
+ }
352
+ }
353
+ function renderHandlersView() {
354
+ mainList.setLabel(' Handlers (sorted by avg latency) ');
355
+ const items = [handlerTableHeader()];
356
+ for (const h of handlerAggs) {
357
+ items.push(formatHandlerRow(h));
358
+ }
359
+ mainList.setItems(items);
360
+ }
361
+ function renderAgentsView() {
362
+ if (drillDown && 'agent' in drillDown) {
363
+ const ag = drillDown;
364
+ mainList.setLabel(` Agent: ${ag.agent} `);
365
+ const items = [handlerTableHeader()];
366
+ for (const h of ag.handlers) {
367
+ items.push(formatHandlerRow(h));
368
+ }
369
+ mainList.setItems(items);
370
+ }
371
+ else {
372
+ mainList.setLabel(' Agents ');
373
+ const items = [];
374
+ for (const ag of agentAggs) {
375
+ const name = ag.agent.padEnd(22);
376
+ const fires = String(ag.fires).padStart(6);
377
+ const errs = colorizeErrors(ag.errors, String(ag.errors).padStart(6));
378
+ const hCount = `(${ag.handlers.length} handlers)`.padStart(14);
379
+ items.push(` ${name} ${fires} fires ${errs} errs ${hCount}`);
380
+ }
381
+ mainList.setItems(items);
382
+ }
383
+ }
384
+ function renderProjectsView() {
385
+ if (drillDown && 'project' in drillDown) {
386
+ const pr = drillDown;
387
+ mainList.setLabel(` Session: ${pr.project} `);
388
+ const items = [handlerTableHeader()];
389
+ for (const h of pr.handlers) {
390
+ items.push(formatHandlerRow(h));
391
+ }
392
+ mainList.setItems(items);
393
+ }
394
+ else {
395
+ mainList.setLabel(' Projects / Sessions ');
396
+ const items = [];
397
+ for (const pr of projectAggs) {
398
+ const name = pr.project.padEnd(22);
399
+ const fires = String(pr.fires).padStart(6);
400
+ const errs = colorizeErrors(pr.errors, String(pr.errors).padStart(6));
401
+ const avg = colorize(pr.avgMs, pr.avgMs.toFixed(1).padStart(8));
402
+ items.push(` ${name} ${fires} fires ${errs} errs ${avg} avg`);
403
+ }
404
+ mainList.setItems(items);
405
+ }
406
+ }
407
+ function renderSearchView() {
408
+ mainList.setLabel(` Search: "${searchQuery}" `);
409
+ if (!searchQuery) {
410
+ mainList.setItems([' Type to search handlers, events, agents...']);
411
+ return;
412
+ }
413
+ const results = [handlerTableHeader()];
414
+ // Search handlers
415
+ for (const h of handlerAggs) {
416
+ if (fuzzyMatch(searchQuery, h.handler) || fuzzyMatch(searchQuery, h.event)) {
417
+ results.push(formatHandlerRow(h));
418
+ }
419
+ }
420
+ // Search events
421
+ for (const ev of eventAggs) {
422
+ if (fuzzyMatch(searchQuery, ev.event) && !results.some((r) => r.includes(ev.event))) {
423
+ const name = ev.event.padEnd(22);
424
+ results.push(` {cyan-fg}[event]{/cyan-fg} ${name} ${ev.fires} fires`);
425
+ }
426
+ }
427
+ // Search agents
428
+ for (const ag of agentAggs) {
429
+ if (fuzzyMatch(searchQuery, ag.agent)) {
430
+ const name = ag.agent.padEnd(22);
431
+ results.push(` {magenta-fg}[agent]{/magenta-fg} ${name} ${ag.fires} fires`);
432
+ }
433
+ }
434
+ if (results.length === 1) {
435
+ results.push(' No matches found.');
436
+ }
437
+ mainList.setItems(results);
438
+ }
439
+ function renderCurrentView() {
440
+ renderHeader();
441
+ renderFooter();
442
+ switch (currentView) {
443
+ case 'events':
444
+ renderEventsView();
445
+ break;
446
+ case 'handlers':
447
+ renderHandlersView();
448
+ break;
449
+ case 'agents':
450
+ renderAgentsView();
451
+ break;
452
+ case 'projects':
453
+ renderProjectsView();
454
+ break;
455
+ case 'search':
456
+ renderSearchView();
457
+ break;
458
+ }
459
+ mainList.select(0);
460
+ mainList.focus();
461
+ scr.render();
462
+ }
463
+ function switchView(direction) {
464
+ if (searchMode) {
465
+ searchMode = false;
466
+ searchBox.hide();
467
+ }
468
+ drillDown = null;
469
+ const idx = VIEW_ORDER.indexOf(currentView);
470
+ const next = (idx + direction + VIEW_ORDER.length) % VIEW_ORDER.length;
471
+ currentView = VIEW_ORDER[next];
472
+ renderCurrentView();
473
+ }
474
+ function drillInto() {
475
+ // @ts-ignore — blessed ListElement exposes .selected at runtime
476
+ const selected = mainList.selected ?? 0;
477
+ if (currentView === 'events' && !drillDown) {
478
+ const ev = eventAggs[selected];
479
+ if (ev) {
480
+ drillDown = ev;
481
+ renderCurrentView();
482
+ }
483
+ }
484
+ else if (currentView === 'agents' && !drillDown) {
485
+ const ag = agentAggs[selected];
486
+ if (ag) {
487
+ drillDown = ag;
488
+ renderCurrentView();
489
+ }
490
+ }
491
+ else if (currentView === 'projects' && !drillDown) {
492
+ const pr = projectAggs[selected];
493
+ if (pr) {
494
+ drillDown = pr;
495
+ renderCurrentView();
496
+ }
497
+ }
498
+ }
499
+ function goBack() {
500
+ if (searchMode) {
501
+ searchMode = false;
502
+ searchBox.hide();
503
+ currentView = 'events';
504
+ renderCurrentView();
505
+ return;
506
+ }
507
+ if (drillDown) {
508
+ drillDown = null;
509
+ renderCurrentView();
510
+ }
511
+ }
512
+ // --- Key bindings ---
513
+ scr.key(['q', 'C-c'], () => process.exit(0));
514
+ scr.key(['tab'], () => switchView(1));
515
+ scr.key(['S-tab'], () => switchView(-1));
516
+ scr.key(['enter'], () => drillInto());
517
+ scr.key(['escape', 'backspace'], () => goBack());
518
+ scr.key(['/'], () => {
519
+ searchMode = true;
520
+ currentView = 'search';
521
+ searchQuery = '';
522
+ searchBox.show();
523
+ searchBox.setValue('');
524
+ renderCurrentView();
525
+ searchBox.focus();
526
+ searchBox.readInput(() => {
527
+ // Input submitted
528
+ searchQuery = searchBox.getValue();
529
+ searchBox.hide();
530
+ mainList.focus();
531
+ renderCurrentView();
532
+ });
533
+ scr.render();
534
+ });
535
+ // Live search: update on each keypress in the search box
536
+ searchBox.on('keypress', (_ch, _key) => {
537
+ // Small delay to let blessed update the value
538
+ setTimeout(() => {
539
+ searchQuery = searchBox.getValue();
540
+ renderSearchView();
541
+ scr.render();
542
+ }, 10);
543
+ });
544
+ // --- No data state ---
545
+ if (entries.length === 0) {
546
+ mainList.setItems([
547
+ ' No metrics recorded yet.',
548
+ '',
549
+ ' Start the clooks daemon and run some Claude Code sessions',
550
+ ' to see hook execution statistics here.',
551
+ ]);
552
+ mainList.setLabel(' clooks stats ');
553
+ renderHeader();
554
+ renderFooter();
555
+ scr.render();
556
+ }
557
+ else {
558
+ renderCurrentView();
559
+ }
560
+ }
package/dist/types.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface HookInput {
11
11
  tool_name?: string;
12
12
  tool_input?: Record<string, unknown>;
13
13
  source?: string;
14
+ agent_type?: string;
14
15
  stop_hook_active?: boolean;
15
16
  [key: string]: unknown;
16
17
  }
@@ -32,6 +33,9 @@ export interface LLMHandlerConfig {
32
33
  enabled?: boolean;
33
34
  sessionIsolation?: boolean;
34
35
  depends?: string[];
36
+ async?: boolean;
37
+ agent?: string;
38
+ project?: string;
35
39
  }
36
40
  /** Script handler config */
37
41
  export interface ScriptHandlerConfig {
@@ -43,6 +47,9 @@ export interface ScriptHandlerConfig {
43
47
  enabled?: boolean;
44
48
  sessionIsolation?: boolean;
45
49
  depends?: string[];
50
+ async?: boolean;
51
+ agent?: string;
52
+ project?: string;
46
53
  }
47
54
  /** Inline handler config */
48
55
  export interface InlineHandlerConfig {
@@ -54,6 +61,9 @@ export interface InlineHandlerConfig {
54
61
  enabled?: boolean;
55
62
  sessionIsolation?: boolean;
56
63
  depends?: string[];
64
+ async?: boolean;
65
+ agent?: string;
66
+ project?: string;
57
67
  }
58
68
  /** Union of all handler configs */
59
69
  export type HandlerConfig = ScriptHandlerConfig | InlineHandlerConfig | LLMHandlerConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mauribadnights/clooks",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Persistent hook runtime for Claude Code — eliminates process spawning overhead and gives you observability",
5
5
  "bin": {
6
6
  "clooks": "./dist/cli.js"
@@ -33,10 +33,12 @@
33
33
  "files": [
34
34
  "dist",
35
35
  "hooks",
36
+ "agents",
36
37
  "README.md",
37
38
  "LICENSE"
38
39
  ],
39
40
  "dependencies": {
41
+ "blessed": "^0.1.81",
40
42
  "commander": "^14.0.3",
41
43
  "yaml": "^2.8.3"
42
44
  },
@@ -49,6 +51,7 @@
49
51
  }
50
52
  },
51
53
  "devDependencies": {
54
+ "@types/blessed": "^0.1.27",
52
55
  "@types/node": "^25.5.0",
53
56
  "tsx": "^4.21.0",
54
57
  "typescript": "^6.0.2",