cohvu 0.6.2 → 1.0.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.
Files changed (49) hide show
  1. package/dist/api.d.ts +128 -0
  2. package/dist/api.js +221 -0
  3. package/dist/api.js.map +1 -0
  4. package/dist/auth.js +2 -2
  5. package/dist/auth.js.map +1 -1
  6. package/dist/index.js +83 -825
  7. package/dist/index.js.map +1 -1
  8. package/dist/proxy.d.ts +1 -0
  9. package/dist/proxy.js +151 -0
  10. package/dist/proxy.js.map +1 -0
  11. package/dist/setup.js +1 -1
  12. package/dist/setup.js.map +1 -1
  13. package/dist/tui/ansi.d.ts +41 -0
  14. package/dist/tui/ansi.js +120 -0
  15. package/dist/tui/ansi.js.map +1 -0
  16. package/dist/tui/dashboard.d.ts +1 -0
  17. package/dist/tui/dashboard.js +1051 -0
  18. package/dist/tui/dashboard.js.map +1 -0
  19. package/dist/tui/keys.d.ts +31 -0
  20. package/dist/tui/keys.js +53 -0
  21. package/dist/tui/keys.js.map +1 -0
  22. package/dist/tui/platform-detect.d.ts +2 -0
  23. package/dist/tui/platform-detect.js +102 -0
  24. package/dist/tui/platform-detect.js.map +1 -0
  25. package/dist/tui/render.d.ts +5 -0
  26. package/dist/tui/render.js +139 -0
  27. package/dist/tui/render.js.map +1 -0
  28. package/dist/tui/state.d.ts +215 -0
  29. package/dist/tui/state.js +206 -0
  30. package/dist/tui/state.js.map +1 -0
  31. package/dist/tui/views/billing.d.ts +3 -0
  32. package/dist/tui/views/billing.js +127 -0
  33. package/dist/tui/views/billing.js.map +1 -0
  34. package/dist/tui/views/knowledge.d.ts +3 -0
  35. package/dist/tui/views/knowledge.js +195 -0
  36. package/dist/tui/views/knowledge.js.map +1 -0
  37. package/dist/tui/views/modals.d.ts +3 -0
  38. package/dist/tui/views/modals.js +192 -0
  39. package/dist/tui/views/modals.js.map +1 -0
  40. package/dist/tui/views/project.d.ts +3 -0
  41. package/dist/tui/views/project.js +76 -0
  42. package/dist/tui/views/project.js.map +1 -0
  43. package/dist/tui/views/team.d.ts +3 -0
  44. package/dist/tui/views/team.js +100 -0
  45. package/dist/tui/views/team.js.map +1 -0
  46. package/dist/tui/views/you.d.ts +3 -0
  47. package/dist/tui/views/you.js +74 -0
  48. package/dist/tui/views/you.js.map +1 -0
  49. package/package.json +1 -1
@@ -0,0 +1,1051 @@
1
+ "use strict";
2
+ // Dashboard main loop — raw mode, keypress dispatch, render, SSE, fs.watch.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.launchDashboard = launchDashboard;
5
+ const keys_1 = require("./keys");
6
+ const state_1 = require("./state");
7
+ const render_1 = require("./render");
8
+ const ansi_1 = require("./ansi");
9
+ const api_1 = require("../api");
10
+ const platform_detect_1 = require("./platform-detect");
11
+ const child_process_1 = require("child_process");
12
+ const fs_1 = require("fs");
13
+ const path_1 = require("path");
14
+ const os_1 = require("os");
15
+ const STATE_FILE = (0, path_1.join)((0, os_1.homedir)(), ".cohvu", "state.json");
16
+ const PAGE_SIZE = 20;
17
+ async function launchDashboard() {
18
+ const api = new api_1.ApiClient();
19
+ let state = (0, state_1.initialState)();
20
+ const watchers = [];
21
+ let searchTimer = null;
22
+ let liveDotTimer = null;
23
+ let feedDisconnect = null;
24
+ let copiedTimer = null;
25
+ let inlineErrorTimer = null;
26
+ // Load first-login state
27
+ const savedState = loadState();
28
+ state.firstLogin = !savedState.hasOpenedDashboard;
29
+ // Dispatch helper
30
+ function dispatch(action) {
31
+ state = (0, state_1.reduce)(state, action);
32
+ // Regenerate banners on relevant state changes
33
+ if (['SET_USER_DATA', 'SET_PLATFORMS', 'SET_BILLING', 'PLATFORM_CONFIGURED', 'SET_PENDING_APPROVALS', 'SET_NOTIFICATIONS'].includes(action.type)) {
34
+ state = { ...state, bannerLines: generateBanners(state) };
35
+ }
36
+ doRender();
37
+ }
38
+ function doRender() {
39
+ const dims = { rows: process.stdout.rows, cols: process.stdout.columns };
40
+ (0, render_1.render)(state, dims);
41
+ }
42
+ // Clean exit
43
+ function cleanup() {
44
+ if (feedDisconnect)
45
+ feedDisconnect();
46
+ watchers.forEach(w => w.close());
47
+ if (searchTimer)
48
+ clearTimeout(searchTimer);
49
+ if (liveDotTimer)
50
+ clearTimeout(liveDotTimer);
51
+ if (inlineErrorTimer)
52
+ clearTimeout(inlineErrorTimer);
53
+ process.stdin.setRawMode?.(false);
54
+ (0, ansi_1.exitScreen)();
55
+ }
56
+ function cleanExit() {
57
+ cleanup();
58
+ process.exit(0);
59
+ }
60
+ process.on('SIGTERM', cleanExit);
61
+ process.on('SIGINT', cleanExit);
62
+ process.on('uncaughtException', (err) => {
63
+ cleanup();
64
+ process.stderr.write(`Error: ${err.message}\n`);
65
+ process.exit(1);
66
+ });
67
+ // Enter TUI
68
+ (0, ansi_1.enterScreen)();
69
+ process.stdin.setRawMode?.(true);
70
+ process.stdin.resume();
71
+ // Resize handler
72
+ process.stdout.on('resize', doRender);
73
+ // Initial render (loading state)
74
+ doRender();
75
+ // Load data
76
+ try {
77
+ const me = await api.me();
78
+ dispatch({ type: 'SET_USER_DATA', me });
79
+ // Detect platforms
80
+ const platforms = (0, platform_detect_1.detectPlatformStatuses)();
81
+ dispatch({ type: 'SET_PLATFORMS', platforms });
82
+ // Load knowledge, members, billing, invite links, notifications in parallel
83
+ const projectId = state.activeProjectId;
84
+ if (projectId) {
85
+ const [memResult, members, billing, inviteLinks, notifications] = await Promise.all([
86
+ api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 }).catch(() => null),
87
+ api.listMembers(projectId).catch(() => []),
88
+ api.getBilling(projectId).catch(() => null),
89
+ api.listInviteLinks(projectId).catch(() => []),
90
+ api.listNotifications().catch(() => []),
91
+ ]);
92
+ if (memResult) {
93
+ dispatch({ type: 'SET_MEMORIES', memories: memResult.memories, total: memResult.total });
94
+ }
95
+ dispatch({ type: 'SET_MEMBERS', members });
96
+ if (billing) {
97
+ dispatch({ type: 'SET_BILLING', billing });
98
+ }
99
+ dispatch({ type: 'SET_INVITE_LINKS', links: inviteLinks });
100
+ dispatch({ type: 'SET_NOTIFICATIONS', notifications });
101
+ // Mark notifications as seen
102
+ if (notifications.length > 0) {
103
+ api.markNotificationsSeen().catch(() => { });
104
+ }
105
+ // Connect SSE feed
106
+ connectFeed(projectId);
107
+ }
108
+ // Mark first login seen
109
+ if (state.firstLogin) {
110
+ saveState({ hasOpenedDashboard: true });
111
+ }
112
+ }
113
+ catch (err) {
114
+ dispatch({ type: 'SET_OFFLINE', offline: true });
115
+ dispatch({ type: 'SET_ERROR', error: "can't reach cohvu" });
116
+ }
117
+ // Watch plugin config files
118
+ setupFileWatchers();
119
+ // Keypress loop
120
+ process.stdin.on('data', async (data) => {
121
+ const key = (0, keys_1.parseKey)(data);
122
+ await handleKey(key);
123
+ doRender();
124
+ });
125
+ // ------ Key handler ------
126
+ async function handleKey(key) {
127
+ // Global keys
128
+ if (key.name === 'ctrl-c' || (key.name === 'char' && key.char === 'q' && !state.modal && state.knowledgeMode !== 'search')) {
129
+ cleanExit();
130
+ return;
131
+ }
132
+ // Modal keys
133
+ if (state.modal) {
134
+ await handleModalKey(key);
135
+ return;
136
+ }
137
+ // Tab switching
138
+ if (key.name === 'tab') {
139
+ dispatch({ type: 'NEXT_TAB' });
140
+ await loadTabData();
141
+ return;
142
+ }
143
+ if (key.name === 'char') {
144
+ const tabIdx = parseInt(key.char, 10);
145
+ if (tabIdx >= 1 && tabIdx <= 5) {
146
+ dispatch({ type: 'SWITCH_TAB', tab: state_1.TABS[tabIdx - 1] });
147
+ await loadTabData();
148
+ return;
149
+ }
150
+ }
151
+ // Tab-specific keys
152
+ switch (state.tab) {
153
+ case 'knowledge':
154
+ await handleKnowledgeKey(key);
155
+ break;
156
+ case 'team':
157
+ await handleTeamKey(key);
158
+ break;
159
+ case 'billing':
160
+ await handleBillingKey(key);
161
+ break;
162
+ case 'project':
163
+ await handleProjectKey(key);
164
+ break;
165
+ case 'you':
166
+ await handleYouKey(key);
167
+ break;
168
+ }
169
+ }
170
+ // ------ Knowledge keys ------
171
+ async function handleKnowledgeKey(key) {
172
+ // alt+d enters forget mode from any knowledge mode
173
+ if (key.name === 'alt-d' && state.userRole !== 'viewer') {
174
+ const list = state.searchResults ?? state.memories;
175
+ if (list.length > 0) {
176
+ dispatch({ type: 'ENTER_FORGET' });
177
+ }
178
+ return;
179
+ }
180
+ if (state.knowledgeMode === 'search') {
181
+ if (key.name === 'escape') {
182
+ dispatch({ type: 'EXIT_SEARCH' });
183
+ }
184
+ else if (key.name === 'enter' && state.searchQuery.length > 0) {
185
+ // Enter triggers the search
186
+ await executeSearch();
187
+ }
188
+ else if (key.name === 'backspace') {
189
+ dispatch({ type: 'SEARCH_BACKSPACE' });
190
+ }
191
+ else if (key.name === 'char') {
192
+ dispatch({ type: 'SEARCH_INPUT', char: key.char });
193
+ }
194
+ else if (key.name === 'up') {
195
+ dispatch({ type: 'SCROLL_UP' });
196
+ }
197
+ else if (key.name === 'down') {
198
+ dispatch({ type: 'SCROLL_DOWN' });
199
+ }
200
+ return;
201
+ }
202
+ if (state.knowledgeMode === 'forget') {
203
+ if (key.name === 'escape') {
204
+ dispatch({ type: 'EXIT_FORGET' });
205
+ }
206
+ else if (key.name === 'space') {
207
+ const list = state.memories;
208
+ const mem = list[state.memorySelected];
209
+ if (mem) {
210
+ // Members can only toggle their own contributions
211
+ const isOwn = state.userRole === 'admin' || (mem.contributed_by?.user_id === state.user?.id);
212
+ if (isOwn) {
213
+ dispatch({ type: 'TOGGLE_FORGET', memoryId: mem.id });
214
+ }
215
+ }
216
+ dispatch({ type: 'SCROLL_DOWN' });
217
+ }
218
+ else if (key.name === 'up') {
219
+ dispatch({ type: 'SCROLL_UP' });
220
+ }
221
+ else if (key.name === 'down') {
222
+ dispatch({ type: 'SCROLL_DOWN' });
223
+ }
224
+ else if (key.name === 'enter' && state.forgetSelected.size > 0) {
225
+ // Batch delete
226
+ const projectId = state.activeProjectId;
227
+ if (projectId) {
228
+ await Promise.all([...state.forgetSelected].map(async (id) => {
229
+ try {
230
+ await api.deleteMemory(projectId, id);
231
+ dispatch({ type: 'REMOVE_MEMORY', id });
232
+ }
233
+ catch { }
234
+ }));
235
+ dispatch({ type: 'EXIT_FORGET' });
236
+ }
237
+ }
238
+ return;
239
+ }
240
+ // Browse mode
241
+ if (key.name === 'char' && key.char === '/') {
242
+ dispatch({ type: 'ENTER_SEARCH' });
243
+ }
244
+ else if (key.name === 'char' && key.char === 'D' && state.userRole === 'admin') {
245
+ const project = (0, state_1.getActiveProject)(state);
246
+ if (project) {
247
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-forget-all', slug: project.slug, memoryCount: state.memoryTotal, input: '' } });
248
+ }
249
+ }
250
+ else if (key.name === 'up') {
251
+ dispatch({ type: 'SCROLL_UP' });
252
+ }
253
+ else if (key.name === 'down') {
254
+ dispatch({ type: 'SCROLL_DOWN' });
255
+ }
256
+ else if (key.name === 'space') {
257
+ await loadMoreMemories();
258
+ }
259
+ }
260
+ // ------ Team keys ------
261
+ function showInlineError(message, durationMs) {
262
+ dispatch({ type: 'SET_INLINE_ERROR', message });
263
+ if (inlineErrorTimer)
264
+ clearTimeout(inlineErrorTimer);
265
+ inlineErrorTimer = setTimeout(() => {
266
+ dispatch({ type: 'SET_INLINE_ERROR', message: null });
267
+ }, durationMs);
268
+ }
269
+ async function handleTeamKey(key) {
270
+ const memberCount = state.members.length;
271
+ const linkRoles = ['admin', 'member', 'viewer'];
272
+ const linkCount = state.userRole === 'admin' ? linkRoles.length : 0;
273
+ const totalRows = memberCount + linkCount;
274
+ const sel = state.teamSelected;
275
+ const onLinkRow = state.userRole === 'admin' && sel >= memberCount;
276
+ const onMemberRow = sel < memberCount;
277
+ // Up/down navigation
278
+ if (key.name === 'up') {
279
+ const newIdx = Math.max(0, sel - 1);
280
+ dispatch({ type: 'SET_TEAM_SELECTED', index: newIdx });
281
+ return;
282
+ }
283
+ if (key.name === 'down') {
284
+ const newIdx = Math.min(totalRows - 1, sel + 1);
285
+ dispatch({ type: 'SET_TEAM_SELECTED', index: newIdx });
286
+ return;
287
+ }
288
+ // 'c' — copy invite link (only when cursor is on a link row, admin only)
289
+ if (key.name === 'char' && key.char === 'c' && state.userRole === 'admin' && onLinkRow) {
290
+ const linkIdx = sel - memberCount;
291
+ const role = linkRoles[linkIdx];
292
+ const link = state.inviteLinks.find(l => l.role === role);
293
+ if (link) {
294
+ copyToClipboard(link.url);
295
+ dispatch({ type: 'SET_COPIED_FEEDBACK', active: true });
296
+ if (copiedTimer)
297
+ clearTimeout(copiedTimer);
298
+ copiedTimer = setTimeout(() => {
299
+ dispatch({ type: 'SET_COPIED_FEEDBACK', active: false });
300
+ }, 1500);
301
+ }
302
+ return;
303
+ }
304
+ // 'r' — regen invite link (only when cursor is on a link row, admin only)
305
+ if (key.name === 'char' && key.char === 'r' && state.userRole === 'admin' && onLinkRow) {
306
+ const linkIdx = sel - memberCount;
307
+ const role = linkRoles[linkIdx];
308
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-regen-link', role } });
309
+ return;
310
+ }
311
+ // 'e' — edit role (only when cursor is on a member row, admin only)
312
+ if (key.name === 'char' && key.char === 'e' && state.userRole === 'admin' && onMemberRow) {
313
+ const target = state.members[sel];
314
+ if (target) {
315
+ // Cannot edit own role
316
+ if (target.user_id === state.user?.id) {
317
+ showInlineError('you cannot change your own role', 2000);
318
+ return;
319
+ }
320
+ const roleIdx = ['admin', 'member', 'viewer'].indexOf(target.role ?? 'member');
321
+ dispatch({ type: 'OPEN_MODAL', modal: {
322
+ kind: 'edit-role',
323
+ targetEmail: target.email ?? target.user_id,
324
+ targetUserId: target.user_id,
325
+ currentRole: target.role ?? 'member',
326
+ selected: roleIdx >= 0 ? roleIdx : 1,
327
+ } });
328
+ }
329
+ return;
330
+ }
331
+ // 'a' — approve pending approval (admin only)
332
+ if (key.name === 'char' && key.char === 'a' && state.userRole === 'admin') {
333
+ const demoteApproval = state.pendingApprovals.find(a => a.action === 'demote_admin');
334
+ if (demoteApproval) {
335
+ dispatch({ type: 'OPEN_MODAL', modal: {
336
+ kind: 'approve-action',
337
+ approvalId: demoteApproval.id,
338
+ description: demoteApproval.description,
339
+ initiator: demoteApproval.initiator_email,
340
+ expiresIn: '',
341
+ } });
342
+ }
343
+ return;
344
+ }
345
+ // 'x' — cancel pending approval (admin, when approval exists) or remove member or leave
346
+ if (key.name === 'char' && key.char === 'x' && state.userRole === 'admin' && state.pendingApprovals.length > 0) {
347
+ const demoteApproval = state.pendingApprovals.find(a => a.action === 'demote_admin');
348
+ if (demoteApproval) {
349
+ const projectId = state.activeProjectId;
350
+ if (projectId) {
351
+ try {
352
+ await api.cancelApproval(projectId, demoteApproval.id);
353
+ const approvals = await api.listApprovals(projectId);
354
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
355
+ }
356
+ catch { }
357
+ }
358
+ return;
359
+ }
360
+ }
361
+ // 'x' — remove member (admin on member row) or leave (member/viewer)
362
+ if (key.name === 'char' && key.char === 'x') {
363
+ if (state.userRole === 'admin' && onMemberRow) {
364
+ const target = state.members[sel];
365
+ if (target) {
366
+ if (target.user_id === state.user?.id) {
367
+ // Admin trying to leave — check if last admin
368
+ const adminCount = state.members.filter(m => m.role === 'admin').length;
369
+ if (adminCount <= 1) {
370
+ showInlineError('you are the only admin \u00b7 promote another member before leaving', 3000);
371
+ return;
372
+ }
373
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-leave' } });
374
+ }
375
+ else {
376
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-remove-member', email: target.email ?? target.user_id, userId: target.user_id } });
377
+ }
378
+ }
379
+ }
380
+ else if (state.userRole !== 'admin') {
381
+ // Member/viewer can leave
382
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-leave' } });
383
+ }
384
+ return;
385
+ }
386
+ }
387
+ // ------ Billing keys ------
388
+ async function handleBillingKey(key) {
389
+ if (state.userRole !== 'admin')
390
+ return;
391
+ if (key.name === 'char' && key.char === 's') {
392
+ const projectId = state.activeProjectId;
393
+ if (projectId) {
394
+ try {
395
+ const checkout = await api.createCheckout(projectId);
396
+ if (checkout.checkout_url)
397
+ openBrowser(checkout.checkout_url);
398
+ }
399
+ catch { }
400
+ }
401
+ }
402
+ else if (key.name === 'char' && key.char === 'p') {
403
+ const projectId = state.activeProjectId;
404
+ if (projectId) {
405
+ try {
406
+ const portal = await api.getPortalUrl(projectId);
407
+ if (portal.url)
408
+ openBrowser(portal.url);
409
+ }
410
+ catch { }
411
+ }
412
+ }
413
+ }
414
+ // ------ Project keys ------
415
+ async function handleProjectKey(key) {
416
+ // 'a' — approve pending approval (admin only)
417
+ if (key.name === 'char' && key.char === 'a' && state.userRole === 'admin') {
418
+ const projectApproval = state.pendingApprovals.find(a => a.action === 'delete_project' || a.action === 'clear_memories');
419
+ if (projectApproval) {
420
+ dispatch({ type: 'OPEN_MODAL', modal: {
421
+ kind: 'approve-action',
422
+ approvalId: projectApproval.id,
423
+ description: projectApproval.description,
424
+ initiator: projectApproval.initiator_email,
425
+ expiresIn: '',
426
+ } });
427
+ }
428
+ return;
429
+ }
430
+ // 'x' — cancel pending approval (admin only)
431
+ if (key.name === 'char' && key.char === 'x' && state.userRole === 'admin') {
432
+ const projectApproval = state.pendingApprovals.find(a => a.action === 'delete_project' || a.action === 'clear_memories');
433
+ if (projectApproval) {
434
+ const projectId = state.activeProjectId;
435
+ if (projectId) {
436
+ try {
437
+ await api.cancelApproval(projectId, projectApproval.id);
438
+ const approvals = await api.listApprovals(projectId);
439
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
440
+ }
441
+ catch { }
442
+ }
443
+ return;
444
+ }
445
+ }
446
+ if (key.name === 'char' && key.char === 'r' && state.userRole === 'admin') {
447
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'rename', input: '' } });
448
+ }
449
+ else if (key.name === 'char' && key.char === 'n') {
450
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '' } });
451
+ }
452
+ else if (key.name === 'char' && key.char === 'w' && state.projects.length > 1) {
453
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: 0 } });
454
+ }
455
+ else if (key.name === 'char' && key.char === 'c' && state.userRole === 'admin') {
456
+ const project = (0, state_1.getActiveProject)(state);
457
+ if (project) {
458
+ const adminCount = state.members.filter(m => m.role === 'admin').length;
459
+ if (adminCount > 1) {
460
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'initiate-consensus', action: 'clear_memories', description: 'clear all memories' } });
461
+ }
462
+ else {
463
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-clear', slug: project.slug, memoryCount: state.memoryTotal, input: '' } });
464
+ }
465
+ }
466
+ }
467
+ else if (key.name === 'char' && key.char === 'd' && state.userRole === 'admin') {
468
+ const project = (0, state_1.getActiveProject)(state);
469
+ if (project) {
470
+ const adminCount = state.members.filter(m => m.role === 'admin').length;
471
+ if (adminCount > 1) {
472
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'initiate-consensus', action: 'delete_project', description: 'delete ' + project.slug } });
473
+ }
474
+ else {
475
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-delete', slug: project.slug, memoryCount: state.memoryTotal, input: '' } });
476
+ }
477
+ }
478
+ }
479
+ }
480
+ // ------ You keys ------
481
+ async function handleYouKey(key) {
482
+ if (key.name === 'char' && key.char === 'r') {
483
+ // Re-run setup
484
+ const platforms = (0, platform_detect_1.detectPlatformStatuses)();
485
+ dispatch({ type: 'SET_PLATFORMS', platforms });
486
+ }
487
+ else if (key.name === 'char' && key.char === 'l') {
488
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-logout' } });
489
+ }
490
+ }
491
+ // ------ Modal keys ------
492
+ async function handleModalKey(key) {
493
+ if (!state.modal)
494
+ return;
495
+ if (key.name === 'escape') {
496
+ dispatch({ type: 'CLOSE_MODAL' });
497
+ return;
498
+ }
499
+ const modal = state.modal;
500
+ // y/n modals
501
+ if (modal.kind === 'confirm-forget' || modal.kind === 'confirm-remove-member' || modal.kind === 'confirm-leave' || modal.kind === 'confirm-logout') {
502
+ if (key.name === 'char' && key.char === 'y') {
503
+ await confirmModal(modal);
504
+ dispatch({ type: 'CLOSE_MODAL' });
505
+ }
506
+ else if (key.name === 'char' && key.char === 'n') {
507
+ dispatch({ type: 'CLOSE_MODAL' });
508
+ }
509
+ return;
510
+ }
511
+ // Initiate consensus modal (y/n)
512
+ if (modal.kind === 'initiate-consensus') {
513
+ if (key.name === 'char' && key.char === 'y') {
514
+ await confirmModal(modal);
515
+ dispatch({ type: 'CLOSE_MODAL' });
516
+ }
517
+ else if (key.name === 'char' && key.char === 'n') {
518
+ dispatch({ type: 'CLOSE_MODAL' });
519
+ }
520
+ return;
521
+ }
522
+ // Approve action modal (y/n)
523
+ if (modal.kind === 'approve-action') {
524
+ if (key.name === 'char' && key.char === 'y') {
525
+ await confirmModal(modal);
526
+ dispatch({ type: 'CLOSE_MODAL' });
527
+ }
528
+ else if (key.name === 'char' && key.char === 'n') {
529
+ dispatch({ type: 'CLOSE_MODAL' });
530
+ }
531
+ return;
532
+ }
533
+ // Confirm regen link modal (y/n)
534
+ if (modal.kind === 'confirm-regen-link') {
535
+ if (key.name === 'char' && key.char === 'y') {
536
+ await confirmModal(modal);
537
+ dispatch({ type: 'CLOSE_MODAL' });
538
+ }
539
+ else if (key.name === 'char' && key.char === 'n') {
540
+ dispatch({ type: 'CLOSE_MODAL' });
541
+ }
542
+ return;
543
+ }
544
+ // Text input modals
545
+ if ('input' in modal) {
546
+ if (key.name === 'enter') {
547
+ await confirmModal(modal);
548
+ dispatch({ type: 'CLOSE_MODAL' });
549
+ }
550
+ else if (key.name === 'backspace') {
551
+ dispatch({ type: 'MODAL_BACKSPACE' });
552
+ }
553
+ else if (key.name === 'char') {
554
+ dispatch({ type: 'MODAL_INPUT', char: key.char });
555
+ }
556
+ return;
557
+ }
558
+ // Switch project modal
559
+ if (modal.kind === 'switch-project') {
560
+ if (key.name === 'up') {
561
+ const newIdx = Math.max(0, modal.selected - 1);
562
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: newIdx } });
563
+ }
564
+ else if (key.name === 'down') {
565
+ const newIdx = Math.min(state.projects.length - 1, modal.selected + 1);
566
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: newIdx } });
567
+ }
568
+ else if (key.name === 'enter') {
569
+ const project = state.projects[modal.selected];
570
+ if (project) {
571
+ await api.switchProject(project.project_id);
572
+ const me = await api.me();
573
+ dispatch({ type: 'SET_USER_DATA', me });
574
+ dispatch({ type: 'CLOSE_MODAL' });
575
+ // Reconnect SSE and reload data for new project
576
+ if (state.activeProjectId)
577
+ connectFeed(state.activeProjectId);
578
+ await loadTabData();
579
+ }
580
+ }
581
+ }
582
+ // Edit role modal
583
+ if (modal.kind === 'edit-role') {
584
+ const roles = ['admin', 'member', 'viewer'];
585
+ if (key.name === 'up') {
586
+ const newIdx = Math.max(0, modal.selected - 1);
587
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: newIdx } });
588
+ }
589
+ else if (key.name === 'down') {
590
+ const newIdx = Math.min(roles.length - 1, modal.selected + 1);
591
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: newIdx } });
592
+ }
593
+ else if (key.name === 'enter') {
594
+ const newRole = roles[modal.selected];
595
+ if (newRole !== modal.currentRole) {
596
+ // Check if demoting an admin
597
+ if (modal.currentRole === 'admin' && newRole !== 'admin') {
598
+ const adminCount = state.members.filter(m => m.role === 'admin').length;
599
+ // Last admin protection — cannot demote the only admin
600
+ if (adminCount <= 1) {
601
+ showInlineError('cannot demote the last admin \u00b7 promote another member first', 2000);
602
+ dispatch({ type: 'CLOSE_MODAL' });
603
+ return;
604
+ }
605
+ if (adminCount > 1) {
606
+ // Initiate consensus for demotion
607
+ dispatch({ type: 'CLOSE_MODAL' });
608
+ dispatch({ type: 'OPEN_MODAL', modal: {
609
+ kind: 'initiate-consensus',
610
+ action: 'demote_admin',
611
+ description: 'demoting ' + modal.targetEmail,
612
+ targetUserId: modal.targetUserId,
613
+ } });
614
+ return;
615
+ }
616
+ }
617
+ // Direct role change
618
+ const projectId = state.activeProjectId;
619
+ if (projectId) {
620
+ try {
621
+ await api.changeRole(projectId, modal.targetUserId, newRole);
622
+ const members = await api.listMembers(projectId);
623
+ dispatch({ type: 'SET_MEMBERS', members });
624
+ }
625
+ catch { }
626
+ }
627
+ }
628
+ dispatch({ type: 'CLOSE_MODAL' });
629
+ }
630
+ }
631
+ }
632
+ async function confirmModal(modal) {
633
+ const projectId = state.activeProjectId;
634
+ if (!projectId)
635
+ return;
636
+ switch (modal.kind) {
637
+ case 'confirm-forget':
638
+ try {
639
+ await api.deleteMemory(projectId, modal.memoryId);
640
+ dispatch({ type: 'REMOVE_MEMORY', id: modal.memoryId });
641
+ }
642
+ catch { }
643
+ break;
644
+ case 'confirm-forget-all':
645
+ case 'confirm-clear': {
646
+ const project = (0, state_1.getActiveProject)(state);
647
+ if (project && modal.input === project.slug) {
648
+ try {
649
+ await api.clearMemories(projectId);
650
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
651
+ }
652
+ catch { }
653
+ }
654
+ break;
655
+ }
656
+ case 'confirm-delete': {
657
+ const project = (0, state_1.getActiveProject)(state);
658
+ if (project && modal.input === project.slug) {
659
+ try {
660
+ await api.deleteProject(projectId);
661
+ const me = await api.me();
662
+ dispatch({ type: 'SET_USER_DATA', me });
663
+ dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
664
+ if (state.activeProjectId) {
665
+ connectFeed(state.activeProjectId);
666
+ await loadTabData();
667
+ }
668
+ else {
669
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
670
+ dispatch({ type: 'SET_MEMBERS', members: [] });
671
+ }
672
+ }
673
+ catch { }
674
+ }
675
+ break;
676
+ }
677
+ case 'confirm-remove-member':
678
+ try {
679
+ await api.removeMember(projectId, modal.userId);
680
+ const members = await api.listMembers(projectId);
681
+ dispatch({ type: 'SET_MEMBERS', members });
682
+ }
683
+ catch { }
684
+ break;
685
+ case 'confirm-leave':
686
+ try {
687
+ // If admin who initiated a pending approval is leaving, cancel it
688
+ const leavingUserId = state.user.id;
689
+ for (const approval of state.pendingApprovals) {
690
+ // Check if this user initiated the approval (email match)
691
+ const leavingEmail = state.user.email;
692
+ if (approval.initiator_email === leavingEmail) {
693
+ try {
694
+ await api.cancelApproval(projectId, approval.id);
695
+ }
696
+ catch { }
697
+ }
698
+ }
699
+ await api.removeMember(projectId, leavingUserId);
700
+ const me = await api.me();
701
+ dispatch({ type: 'SET_USER_DATA', me });
702
+ dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
703
+ if (state.activeProjectId) {
704
+ connectFeed(state.activeProjectId);
705
+ await loadTabData();
706
+ }
707
+ else {
708
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
709
+ dispatch({ type: 'SET_MEMBERS', members: [] });
710
+ }
711
+ }
712
+ catch { }
713
+ break;
714
+ case 'confirm-logout':
715
+ try {
716
+ const tokensFile = (0, path_1.join)((0, os_1.homedir)(), ".cohvu", "tokens.json");
717
+ if ((0, fs_1.existsSync)(tokensFile)) {
718
+ const { unlinkSync } = require("fs");
719
+ unlinkSync(tokensFile);
720
+ }
721
+ }
722
+ catch { }
723
+ cleanExit();
724
+ break;
725
+ case 'rename': {
726
+ if (!modal.input)
727
+ break;
728
+ const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
729
+ try {
730
+ await api.renameProject(projectId, modal.input, slug);
731
+ const me = await api.me();
732
+ dispatch({ type: 'SET_USER_DATA', me });
733
+ }
734
+ catch { }
735
+ break;
736
+ }
737
+ case 'create-project': {
738
+ if (!modal.input)
739
+ break;
740
+ const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
741
+ try {
742
+ const project = await api.createProject(modal.input, slug);
743
+ await api.switchProject(project.id);
744
+ const me = await api.me();
745
+ dispatch({ type: 'SET_USER_DATA', me });
746
+ await loadTabData();
747
+ }
748
+ catch { }
749
+ break;
750
+ }
751
+ case 'initiate-consensus': {
752
+ try {
753
+ await api.initiateApproval(projectId, modal.action, modal.targetUserId);
754
+ const approvals = await api.listApprovals(projectId);
755
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
756
+ }
757
+ catch { }
758
+ break;
759
+ }
760
+ case 'approve-action': {
761
+ try {
762
+ await api.approveAction(projectId, modal.approvalId);
763
+ const approvals = await api.listApprovals(projectId);
764
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
765
+ }
766
+ catch { }
767
+ break;
768
+ }
769
+ case 'confirm-regen-link': {
770
+ try {
771
+ await api.regenerateInviteLink(projectId, modal.role);
772
+ const links = await api.listInviteLinks(projectId);
773
+ dispatch({ type: 'SET_INVITE_LINKS', links });
774
+ }
775
+ catch { }
776
+ break;
777
+ }
778
+ }
779
+ }
780
+ // ------ Data loading ------
781
+ async function loadTabData() {
782
+ const projectId = state.activeProjectId;
783
+ if (!projectId)
784
+ return;
785
+ try {
786
+ switch (state.tab) {
787
+ case 'knowledge': {
788
+ dispatch({ type: 'SET_LOADING', loading: true });
789
+ const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 });
790
+ dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total });
791
+ break;
792
+ }
793
+ case 'team': {
794
+ const [members, links] = await Promise.all([
795
+ api.listMembers(projectId),
796
+ api.listInviteLinks(projectId).catch(() => []),
797
+ ]);
798
+ dispatch({ type: 'SET_MEMBERS', members });
799
+ dispatch({ type: 'SET_INVITE_LINKS', links });
800
+ break;
801
+ }
802
+ case 'billing': {
803
+ const billing = await api.getBilling(projectId);
804
+ dispatch({ type: 'SET_BILLING', billing });
805
+ break;
806
+ }
807
+ case 'project': {
808
+ const approvals = await api.listApprovals(projectId).catch(() => []);
809
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
810
+ break;
811
+ }
812
+ }
813
+ }
814
+ catch { }
815
+ }
816
+ async function loadMoreMemories() {
817
+ const projectId = state.activeProjectId;
818
+ if (!projectId || !state.memoryHasMore)
819
+ return;
820
+ dispatch({ type: 'SET_LOADING', loading: true });
821
+ try {
822
+ const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: state.memories.length });
823
+ dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total, append: true });
824
+ }
825
+ catch { }
826
+ }
827
+ async function executeSearch() {
828
+ const projectId = state.activeProjectId;
829
+ if (!projectId || state.searchQuery.length === 0)
830
+ return;
831
+ dispatch({ type: 'SET_SEARCHING', searching: true });
832
+ try {
833
+ const result = await api.searchMemories(projectId, state.searchQuery);
834
+ dispatch({ type: 'SET_SEARCH_RESULTS', results: result.memories });
835
+ }
836
+ catch { }
837
+ dispatch({ type: 'SET_SEARCHING', searching: false });
838
+ }
839
+ // ------ SSE feed ------
840
+ function connectFeed(projectId) {
841
+ if (feedDisconnect)
842
+ feedDisconnect();
843
+ const conn = api.connectFeed(projectId, (eventType, data) => {
844
+ if (eventType === 'memory') {
845
+ const event = data;
846
+ if (event.operation === 'create') {
847
+ dispatch({ type: 'ADD_MEMORY', memory: { id: event.id, body: event.body, updated_at: event.updated_at } });
848
+ dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
849
+ if (liveDotTimer)
850
+ clearTimeout(liveDotTimer);
851
+ liveDotTimer = setTimeout(() => {
852
+ dispatch({ type: 'CLEAR_LIVE_DOT' });
853
+ }, 10000);
854
+ }
855
+ else if (event.operation === 'update') {
856
+ // Update the memory in place
857
+ const updated = { id: event.id, body: event.body, updated_at: event.updated_at };
858
+ state = {
859
+ ...state,
860
+ memories: state.memories.map(m => m.id === event.id ? updated : m),
861
+ searchResults: state.searchResults?.map(m => m.id === event.id ? updated : m) ?? null,
862
+ };
863
+ dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
864
+ if (liveDotTimer)
865
+ clearTimeout(liveDotTimer);
866
+ liveDotTimer = setTimeout(() => {
867
+ dispatch({ type: 'CLEAR_LIVE_DOT' });
868
+ }, 10000);
869
+ }
870
+ }
871
+ else if (eventType === 'role_change') {
872
+ // Role changed for the current user — refresh user data
873
+ api.me().then(me => {
874
+ dispatch({ type: 'SET_USER_DATA', me });
875
+ }).catch(() => { });
876
+ }
877
+ else if (eventType === 'approval') {
878
+ // Approval state changed — refresh approvals
879
+ api.listApprovals(projectId).then(approvals => {
880
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
881
+ }).catch(() => { });
882
+ }
883
+ else if (eventType === 'notification') {
884
+ // New notification — refresh notifications
885
+ api.listNotifications().then(notifications => {
886
+ dispatch({ type: 'SET_NOTIFICATIONS', notifications });
887
+ api.markNotificationsSeen().catch(() => { });
888
+ }).catch(() => { });
889
+ }
890
+ });
891
+ feedDisconnect = conn.disconnect;
892
+ dispatch({ type: 'SET_SSE_CONNECTED', connected: true });
893
+ }
894
+ // ------ File watchers for plugin detection ------
895
+ function setupFileWatchers() {
896
+ const claudeConfig = (0, path_1.join)((0, os_1.homedir)(), '.claude.json');
897
+ const cursorConfig = (0, path_1.join)((0, os_1.homedir)(), '.cursor', 'mcp.json');
898
+ for (const [path, platform] of [[claudeConfig, 'claude-code'], [cursorConfig, 'cursor']]) {
899
+ if ((0, fs_1.existsSync)(path)) {
900
+ try {
901
+ const w = (0, fs_1.watch)(path, () => {
902
+ if (checkMcpConfigured(path)) {
903
+ dispatch({ type: 'PLATFORM_CONFIGURED', platform });
904
+ }
905
+ });
906
+ watchers.push(w);
907
+ }
908
+ catch { }
909
+ }
910
+ }
911
+ }
912
+ // ------ Helpers ------
913
+ function openBrowser(url) {
914
+ const cmd = process.platform === "darwin" ? "open"
915
+ : process.platform === "win32" ? "start" : "xdg-open";
916
+ (0, child_process_1.execFile)(cmd, [url], () => { });
917
+ }
918
+ function copyToClipboard(text) {
919
+ const cmd = process.platform === "darwin" ? "pbcopy"
920
+ : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
921
+ const child = (0, child_process_1.exec)(cmd);
922
+ child.stdin?.write(text);
923
+ child.stdin?.end();
924
+ }
925
+ }
926
+ function generateBanners(state) {
927
+ const lines = [];
928
+ const project = (0, state_1.getActiveProject)(state);
929
+ // Priority: pending approvals > notifications > locked > past due > trial ending > plugin nudge > joined
930
+ // 1. Pending approvals (admin only)
931
+ if (state.userRole === 'admin') {
932
+ for (const approval of state.pendingApprovals) {
933
+ const approved = approval.approved_by.length;
934
+ const required = approval.required_count;
935
+ const action = approval.action === 'delete_project' ? 'delete ' + (project?.slug ?? 'project')
936
+ : approval.action === 'clear_memories' ? 'clear memories'
937
+ : approval.action === 'demote_admin' ? 'demote ' + (approval.initiator_email ?? 'admin')
938
+ : approval.description;
939
+ lines.push(' ' + ansi_1.C.red + '! ' + ansi_1.C.secondary + 'approval pending: ' + action + ansi_1.C.reset +
940
+ ansi_1.C.muted + ' \u00b7 ' + approved + ' of ' + required + ' admins approved \u00b7 press p to review' + ansi_1.C.reset);
941
+ }
942
+ }
943
+ // 2. Notifications (unseen)
944
+ for (const notif of state.notifications) {
945
+ if (!notif.seen) {
946
+ if (notif.type === 'role_change' && notif.message.includes('admin')) {
947
+ lines.push(' ' + ansi_1.C.green + ' ' + ansi_1.C.secondary + notif.message + ansi_1.C.reset);
948
+ }
949
+ else if (notif.type === 'role_change') {
950
+ lines.push(' ' + ansi_1.C.yellow + '! ' + ansi_1.C.secondary + notif.message + ansi_1.C.reset);
951
+ }
952
+ else if (notif.type === 'approval_completed') {
953
+ lines.push(' ' + ansi_1.C.red + '! ' + ansi_1.C.secondary + notif.message + ansi_1.C.reset);
954
+ }
955
+ else if (notif.type === 'approval_expired' || notif.type === 'approval_canceled') {
956
+ lines.push(' ' + ansi_1.C.muted + ' ' + notif.message + ansi_1.C.reset);
957
+ }
958
+ else {
959
+ lines.push(' ' + ansi_1.C.muted + ' ' + notif.message + ansi_1.C.reset);
960
+ }
961
+ }
962
+ }
963
+ // Billing banners (suppress plugin nudges when billing issues exist)
964
+ let hasBillingBanner = false;
965
+ if (project) {
966
+ const trialDays = project.trial_ends_at ? (0, ansi_1.daysUntil)(project.trial_ends_at) : null;
967
+ // 3. Locked (trial ended, no subscription)
968
+ if (trialDays !== null && trialDays <= 0 && project.billing_status !== 'active') {
969
+ lines.push(' ' + ansi_1.C.red + '! ' + ansi_1.C.secondary + 'trial ended \u00b7 your agents have stopped working' + ansi_1.C.reset +
970
+ ansi_1.C.muted + ' \u00b7 press b to subscribe' + ansi_1.C.reset);
971
+ hasBillingBanner = true;
972
+ }
973
+ // 4. Past due
974
+ else if (project.billing_status === 'past_due') {
975
+ lines.push(' ' + ansi_1.C.red + '! ' + ansi_1.C.secondary + 'payment failed' + ansi_1.C.reset +
976
+ ansi_1.C.muted + ' \u00b7 update your payment method \u00b7 press b' + ansi_1.C.reset);
977
+ hasBillingBanner = true;
978
+ }
979
+ // 5. Trial ending soon (<= 3 days)
980
+ else if (trialDays !== null && trialDays > 0 && trialDays <= 3) {
981
+ const dayWord = trialDays === 1 ? 'tomorrow' : `in ${trialDays} days`;
982
+ lines.push(' ' + ansi_1.C.yellow + '! ' + ansi_1.C.secondary + 'trial ends ' + dayWord + ansi_1.C.reset +
983
+ ansi_1.C.muted + ' \u00b7 press b to subscribe' + ansi_1.C.reset);
984
+ hasBillingBanner = true;
985
+ }
986
+ }
987
+ // Plugin nudge banners (only when no billing issues)
988
+ if (!hasBillingBanner) {
989
+ // First-login: show auto-configured platforms
990
+ if (state.firstLogin) {
991
+ const autoConfigured = state.platforms
992
+ .filter(p => p.state === 'configured' && !['claude code', 'cursor'].includes(p.name))
993
+ .map(p => p.name);
994
+ if (autoConfigured.length > 0) {
995
+ lines.push(' ' + ansi_1.C.green + ' ' + ansi_1.C.secondary + autoConfigured.join(', ') + ansi_1.C.reset +
996
+ ansi_1.C.muted + ' ready' + ansi_1.C.reset);
997
+ }
998
+ }
999
+ // Plugin needed banners
1000
+ for (const p of state.platforms) {
1001
+ if (p.state === 'needs-plugin') {
1002
+ if (p.name === 'claude code') {
1003
+ lines.push(' ' + ansi_1.C.yellow + '\u2192 ' + ansi_1.C.secondary + 'open Claude Code and run ' + ansi_1.C.reset +
1004
+ ansi_1.C.primary + '/plugin marketplace add cohvu/cohvu' + ansi_1.C.reset);
1005
+ lines.push(' ' + ansi_1.C.dim + ' then ' + ansi_1.C.reset +
1006
+ ansi_1.C.primary + '/plugin install cohvu@cohvu' + ansi_1.C.reset);
1007
+ }
1008
+ else if (p.name === 'cursor') {
1009
+ lines.push(' ' + ansi_1.C.yellow + '\u2192 ' + ansi_1.C.secondary + 'open Cursor and run ' + ansi_1.C.reset +
1010
+ ansi_1.C.primary + '/add-plugin github.com/cohvu/cohvu-cursor' + ansi_1.C.reset);
1011
+ }
1012
+ }
1013
+ }
1014
+ // Joined banner
1015
+ if (state.joinedProjectName) {
1016
+ lines.push(' ' + ansi_1.C.green + ' ' + ansi_1.C.secondary + 'you joined ' + state.joinedProjectName + ansi_1.C.reset +
1017
+ ansi_1.C.muted + ' \u00b7 your agent is ready' + ansi_1.C.reset);
1018
+ }
1019
+ }
1020
+ return lines;
1021
+ }
1022
+ function checkMcpConfigured(configPath) {
1023
+ try {
1024
+ const content = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
1025
+ return !!content?.mcpServers?.cohvu;
1026
+ }
1027
+ catch {
1028
+ return false;
1029
+ }
1030
+ }
1031
+ function loadState() {
1032
+ try {
1033
+ if ((0, fs_1.existsSync)(STATE_FILE)) {
1034
+ return JSON.parse((0, fs_1.readFileSync)(STATE_FILE, 'utf8'));
1035
+ }
1036
+ }
1037
+ catch { }
1038
+ return { hasOpenedDashboard: false };
1039
+ }
1040
+ function saveState(data) {
1041
+ try {
1042
+ const dir = (0, path_1.join)((0, os_1.homedir)(), ".cohvu");
1043
+ if (!(0, fs_1.existsSync)(dir)) {
1044
+ const { mkdirSync } = require("fs");
1045
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
1046
+ }
1047
+ (0, fs_1.writeFileSync)(STATE_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
1048
+ }
1049
+ catch { }
1050
+ }
1051
+ //# sourceMappingURL=dashboard.js.map