@tuskydp/cli 0.1.0 → 0.1.1

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 (87) hide show
  1. package/bin/tuskydp.ts +2 -0
  2. package/dist/src/commands/account.d.ts.map +1 -1
  3. package/dist/src/commands/account.js +5 -2
  4. package/dist/src/commands/account.js.map +1 -1
  5. package/dist/src/commands/auth.d.ts.map +1 -1
  6. package/dist/src/commands/auth.js +2 -1
  7. package/dist/src/commands/auth.js.map +1 -1
  8. package/dist/src/commands/files.d.ts.map +1 -1
  9. package/dist/src/commands/files.js +9 -4
  10. package/dist/src/commands/files.js.map +1 -1
  11. package/dist/src/commands/mcp.js +1 -1
  12. package/dist/src/commands/mcp.js.map +1 -1
  13. package/dist/src/commands/rehydrate.d.ts.map +1 -1
  14. package/dist/src/commands/rehydrate.js +5 -2
  15. package/dist/src/commands/rehydrate.js.map +1 -1
  16. package/dist/src/commands/upload.d.ts.map +1 -1
  17. package/dist/src/commands/upload.js +5 -0
  18. package/dist/src/commands/upload.js.map +1 -1
  19. package/dist/src/config.d.ts +0 -2
  20. package/dist/src/config.d.ts.map +1 -1
  21. package/dist/src/config.js +5 -4
  22. package/dist/src/config.js.map +1 -1
  23. package/dist/src/index.js +16 -2
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/lib/keyring.d.ts.map +1 -1
  26. package/dist/src/lib/keyring.js +3 -5
  27. package/dist/src/lib/keyring.js.map +1 -1
  28. package/dist/src/lib/output.js +1 -1
  29. package/dist/src/lib/output.js.map +1 -1
  30. package/dist/src/lib/resolve.js +1 -1
  31. package/dist/src/lib/resolve.js.map +1 -1
  32. package/dist/src/mcp/tools/files.d.ts.map +1 -1
  33. package/dist/src/mcp/tools/files.js +20 -0
  34. package/dist/src/mcp/tools/files.js.map +1 -1
  35. package/dist/src/mcp/tools/folders.d.ts.map +1 -1
  36. package/dist/src/mcp/tools/folders.js +15 -0
  37. package/dist/src/mcp/tools/folders.js.map +1 -1
  38. package/dist/src/mcp/tools/trash.d.ts.map +1 -1
  39. package/dist/src/mcp/tools/trash.js +14 -0
  40. package/dist/src/mcp/tools/trash.js.map +1 -1
  41. package/dist/src/sdk.d.ts +1 -1
  42. package/dist/src/sdk.d.ts.map +1 -1
  43. package/dist/src/sdk.js +3 -3
  44. package/dist/src/sdk.js.map +1 -1
  45. package/dist/src/tui/auth-screen.d.ts.map +1 -1
  46. package/dist/src/tui/auth-screen.js +7 -1
  47. package/dist/src/tui/auth-screen.js.map +1 -1
  48. package/package.json +12 -18
  49. package/src/__tests__/crypto.test.ts +315 -0
  50. package/src/commands/account.ts +82 -0
  51. package/src/commands/auth.ts +190 -0
  52. package/src/commands/decrypt.ts +276 -0
  53. package/src/commands/download.ts +82 -0
  54. package/src/commands/encryption.ts +305 -0
  55. package/src/commands/export.ts +251 -0
  56. package/src/commands/files.ts +192 -0
  57. package/src/commands/mcp.ts +220 -0
  58. package/src/commands/rehydrate.ts +37 -0
  59. package/src/commands/tui.ts +11 -0
  60. package/src/commands/upload.ts +143 -0
  61. package/src/commands/vault.ts +132 -0
  62. package/src/config.ts +38 -0
  63. package/src/crypto.ts +130 -0
  64. package/src/index.ts +79 -0
  65. package/src/lib/keyring.ts +50 -0
  66. package/src/lib/output.ts +36 -0
  67. package/src/lib/progress.ts +5 -0
  68. package/src/lib/resolve.ts +26 -0
  69. package/src/mcp/context.ts +22 -0
  70. package/src/mcp/server.ts +140 -0
  71. package/src/mcp/tools/account.ts +40 -0
  72. package/src/mcp/tools/files.ts +428 -0
  73. package/src/mcp/tools/folders.ts +109 -0
  74. package/src/mcp/tools/helpers.ts +28 -0
  75. package/src/mcp/tools/trash.ts +82 -0
  76. package/src/mcp/tools/vaults.ts +114 -0
  77. package/src/sdk.ts +115 -0
  78. package/src/tui/auth-screen.ts +176 -0
  79. package/src/tui/dialogs.ts +339 -0
  80. package/src/tui/files-panel.ts +165 -0
  81. package/src/tui/helpers.ts +206 -0
  82. package/src/tui/index.ts +420 -0
  83. package/src/tui/overview.ts +155 -0
  84. package/src/tui/status-bar.ts +61 -0
  85. package/src/tui/vaults-panel.ts +143 -0
  86. package/tsconfig.json +9 -0
  87. package/vitest.config.ts +7 -0
@@ -0,0 +1,339 @@
1
+ import blessed from 'blessed';
2
+ import { readdirSync } from 'fs';
3
+ import { resolve as pathResolve, dirname, join as pathJoin } from 'path';
4
+
5
+ export function confirmDialog(
6
+ screen: blessed.Widgets.Screen,
7
+ message: string,
8
+ ): Promise<boolean> {
9
+ return new Promise((resolve) => {
10
+ const dialog = blessed.question({
11
+ parent: screen,
12
+ top: 'center',
13
+ left: 'center',
14
+ width: '50%',
15
+ height: 'shrink',
16
+ border: { type: 'line' },
17
+ style: {
18
+ border: { fg: 'yellow' },
19
+ fg: 'white',
20
+ },
21
+ label: ' Confirm ',
22
+ tags: true,
23
+ keys: true,
24
+ vi: true,
25
+ });
26
+ dialog.ask(message, (err, value) => {
27
+ dialog.destroy();
28
+ screen.render();
29
+ resolve(!err && !!value);
30
+ });
31
+ });
32
+ }
33
+
34
+ export function textInputDialog(
35
+ screen: blessed.Widgets.Screen,
36
+ label: string,
37
+ defaultValue = '',
38
+ ): Promise<string | null> {
39
+ return new Promise((resolve) => {
40
+ const form = blessed.form({
41
+ parent: screen,
42
+ top: 'center',
43
+ left: 'center',
44
+ width: '60%',
45
+ height: 7,
46
+ border: { type: 'line' },
47
+ style: {
48
+ border: { fg: 'cyan' },
49
+ fg: 'white',
50
+ },
51
+ label: ` ${label} `,
52
+ tags: true,
53
+ keys: true,
54
+ });
55
+
56
+ const input = blessed.textbox({
57
+ parent: form,
58
+ top: 1,
59
+ left: 1,
60
+ right: 1,
61
+ height: 1,
62
+ inputOnFocus: true,
63
+ style: {
64
+ fg: 'white',
65
+ bg: 'black',
66
+ focus: {
67
+ bg: 'black',
68
+ fg: 'cyan',
69
+ },
70
+ },
71
+ value: defaultValue,
72
+ });
73
+
74
+ const hint = blessed.box({
75
+ parent: form,
76
+ top: 3,
77
+ left: 1,
78
+ right: 1,
79
+ height: 1,
80
+ content: 'Enter to confirm, Escape to cancel',
81
+ style: { fg: 'gray' },
82
+ });
83
+
84
+ input.on('submit', (value: string) => {
85
+ form.destroy();
86
+ screen.render();
87
+ resolve(value || null);
88
+ });
89
+
90
+ input.on('cancel', () => {
91
+ form.destroy();
92
+ screen.render();
93
+ resolve(null);
94
+ });
95
+
96
+ input.focus();
97
+ screen.render();
98
+ });
99
+ }
100
+
101
+ export function selectDialog(
102
+ screen: blessed.Widgets.Screen,
103
+ label: string,
104
+ items: string[],
105
+ ): Promise<number> {
106
+ return new Promise((resolve) => {
107
+ const list = blessed.list({
108
+ parent: screen,
109
+ top: 'center',
110
+ left: 'center',
111
+ width: '50%',
112
+ height: items.length + 2,
113
+ border: { type: 'line' },
114
+ style: {
115
+ border: { fg: 'cyan' },
116
+ fg: 'white',
117
+ selected: {
118
+ bg: 'cyan',
119
+ fg: 'black',
120
+ },
121
+ },
122
+ label: ` ${label} `,
123
+ tags: true,
124
+ keys: true,
125
+ vi: true,
126
+ items,
127
+ interactive: true,
128
+ });
129
+
130
+ list.on('select', (_item: any, index: number) => {
131
+ list.destroy();
132
+ screen.render();
133
+ resolve(index);
134
+ });
135
+
136
+ list.on('cancel', () => {
137
+ list.destroy();
138
+ screen.render();
139
+ resolve(-1);
140
+ });
141
+
142
+ list.focus();
143
+ screen.render();
144
+ });
145
+ }
146
+
147
+ export function fileBrowserDialog(
148
+ screen: blessed.Widgets.Screen,
149
+ startDir = process.env.HOME || '/',
150
+ ): Promise<{ path: string; isDirectory: boolean } | null> {
151
+ return new Promise((res) => {
152
+ let currentDir = pathResolve(startDir);
153
+ let resolved = false;
154
+
155
+ function finish(result: { path: string; isDirectory: boolean } | null) {
156
+ if (resolved) return;
157
+ resolved = true;
158
+ box.destroy();
159
+ screen.render();
160
+ res(result);
161
+ }
162
+
163
+ const box = blessed.box({
164
+ parent: screen,
165
+ top: 'center',
166
+ left: 'center',
167
+ width: '70%',
168
+ height: '70%',
169
+ border: { type: 'line' },
170
+ style: {
171
+ border: { fg: 'cyan' },
172
+ fg: 'white',
173
+ },
174
+ label: ` Browse `,
175
+ tags: true,
176
+ });
177
+
178
+ const pathBar = blessed.box({
179
+ parent: box,
180
+ top: 0,
181
+ left: 0,
182
+ width: '100%',
183
+ height: 1,
184
+ tags: true,
185
+ style: { fg: 'cyan' },
186
+ });
187
+
188
+ const list = blessed.list({
189
+ parent: box,
190
+ top: 2,
191
+ left: 0,
192
+ width: '100%',
193
+ height: '100%-5',
194
+ style: {
195
+ fg: 'white',
196
+ selected: { bg: 'cyan', fg: 'black' },
197
+ },
198
+ keys: true,
199
+ vi: true,
200
+ interactive: true,
201
+ scrollable: true,
202
+ alwaysScroll: true,
203
+ scrollbar: { style: { bg: 'cyan' } },
204
+ tags: true,
205
+ padding: { left: 1, right: 1 },
206
+ });
207
+
208
+ const hint = blessed.box({
209
+ parent: box,
210
+ bottom: 0,
211
+ left: 0,
212
+ width: '100%',
213
+ height: 1,
214
+ tags: true,
215
+ style: { fg: 'gray' },
216
+ content: ' {bold}Enter{/bold} open/select {bold}u{/bold} upload folder {bold}Esc{/bold} cancel',
217
+ });
218
+
219
+ // Cache dir listing to avoid re-reading on selection
220
+ let cachedDirs: string[] = [];
221
+ let cachedFiles: string[] = [];
222
+
223
+ function loadDir(dir: string) {
224
+ currentDir = dir;
225
+ pathBar.setContent(` ${currentDir}`);
226
+ try {
227
+ const entries = readdirSync(dir, { withFileTypes: true });
228
+ cachedDirs = [];
229
+ cachedFiles = [];
230
+ for (const e of entries) {
231
+ if (e.name.startsWith('.')) continue;
232
+ if (e.isDirectory()) cachedDirs.push(e.name);
233
+ else if (e.isFile()) cachedFiles.push(e.name);
234
+ }
235
+ cachedDirs.sort((a, b) => a.localeCompare(b));
236
+ cachedFiles.sort((a, b) => a.localeCompare(b));
237
+ const items: string[] = ['{yellow-fg}../{/yellow-fg}'];
238
+ for (const d of cachedDirs) items.push(`{cyan-fg}📁 ${d}/{/cyan-fg}`);
239
+ for (const f of cachedFiles) items.push(` ${f}`);
240
+ list.setItems(items as any);
241
+ list.select(0);
242
+ screen.render();
243
+ } catch {
244
+ list.setItems(['{red-fg}Permission denied{/red-fg}'] as any);
245
+ screen.render();
246
+ }
247
+ }
248
+
249
+ function getSelectedEntry(): { name: string; fullPath: string; isDir: boolean } | null {
250
+ const idx = (list as any).selected as number;
251
+ if (idx === 0) return { name: '..', fullPath: dirname(currentDir), isDir: true };
252
+ const dirOffset = 1;
253
+ if (idx < dirOffset + cachedDirs.length) {
254
+ const name = cachedDirs[idx - dirOffset];
255
+ return { name, fullPath: pathJoin(currentDir, name), isDir: true };
256
+ }
257
+ const fileOffset = dirOffset + cachedDirs.length;
258
+ if (idx < fileOffset + cachedFiles.length) {
259
+ const name = cachedFiles[idx - fileOffset];
260
+ return { name, fullPath: pathJoin(currentDir, name), isDir: false };
261
+ }
262
+ return null;
263
+ }
264
+
265
+ list.on('select', () => {
266
+ const entry = getSelectedEntry();
267
+ if (!entry) return;
268
+ if (entry.isDir) {
269
+ loadDir(entry.fullPath);
270
+ } else {
271
+ finish({ path: entry.fullPath, isDirectory: false });
272
+ }
273
+ });
274
+
275
+ list.key(['escape'], () => finish(null));
276
+ box.key(['escape'], () => finish(null));
277
+
278
+ list.key(['u'], () => {
279
+ finish({ path: currentDir, isDirectory: true });
280
+ });
281
+
282
+ loadDir(currentDir);
283
+ list.focus();
284
+ screen.render();
285
+ });
286
+ }
287
+
288
+ export function detailPopup(
289
+ screen: blessed.Widgets.Screen,
290
+ title: string,
291
+ content: string,
292
+ ) {
293
+ // Count content lines to auto-size height
294
+ const lines = content.split('\n').length;
295
+ const height = Math.min(lines + 5, Math.floor((screen.height as number) * 0.5));
296
+
297
+ const box = blessed.box({
298
+ parent: screen,
299
+ top: 'center',
300
+ left: 'center',
301
+ width: '55%',
302
+ height,
303
+ border: { type: 'line' },
304
+ style: {
305
+ border: { fg: 'cyan' },
306
+ fg: 'white',
307
+ },
308
+ label: ` ${title} `,
309
+ tags: true,
310
+ keys: true,
311
+ vi: true,
312
+ scrollable: true,
313
+ alwaysScroll: true,
314
+ scrollbar: {
315
+ style: { bg: 'cyan' },
316
+ },
317
+ padding: { top: 1, bottom: 1, left: 1, right: 1 },
318
+ content,
319
+ });
320
+
321
+ const hint = blessed.box({
322
+ parent: box,
323
+ bottom: 0,
324
+ left: 0,
325
+ width: '100%',
326
+ height: 1,
327
+ content: '{gray-fg}Esc to close{/gray-fg}',
328
+ tags: true,
329
+ style: { fg: 'gray' },
330
+ });
331
+
332
+ box.key(['escape', 'q', 'enter'], () => {
333
+ box.destroy();
334
+ screen.render();
335
+ });
336
+
337
+ box.focus();
338
+ screen.render();
339
+ }
@@ -0,0 +1,165 @@
1
+ import blessed from 'blessed';
2
+ import type { TuskyClient } from '@tuskydp/sdk';
3
+ import { formatBytes, formatDate, formatRow, statusColor, statusColorClose, getCurrentTheme } from './helpers.js';
4
+
5
+ export interface FileItem {
6
+ id: string;
7
+ name: string;
8
+ sizeBytes: number;
9
+ plaintextSizeBytes: number | null;
10
+ mimeType: string;
11
+ status: string;
12
+ createdAt: string;
13
+ walrusBlobId: string | null;
14
+ walrusBlobObjectId: string | null;
15
+ encrypted: boolean;
16
+ }
17
+
18
+ export class FilesPanel {
19
+ list: blessed.Widgets.ListElement;
20
+ private screen: blessed.Widgets.Screen;
21
+ private sdk: TuskyClient;
22
+ private files: FileItem[] = [];
23
+ private loading = false;
24
+ private currentVaultId: string | null = null;
25
+ private currentVaultName = '';
26
+
27
+ constructor(
28
+ screen: blessed.Widgets.Screen,
29
+ sdk: TuskyClient,
30
+ parent: blessed.Widgets.BoxElement,
31
+ ) {
32
+ this.screen = screen;
33
+ this.sdk = sdk;
34
+
35
+ this.list = blessed.list({
36
+ parent,
37
+ top: 0,
38
+ left: 0,
39
+ width: '100%',
40
+ height: '100%',
41
+ border: { type: 'line' },
42
+ style: {
43
+ border: { fg: 'cyan' },
44
+ fg: 'white',
45
+ selected: {
46
+ bg: 'cyan',
47
+ fg: 'black',
48
+ },
49
+ focus: {
50
+ border: { fg: 'green' },
51
+ },
52
+ },
53
+ label: ' Files ',
54
+ tags: true,
55
+ keys: true,
56
+ vi: true,
57
+ interactive: true,
58
+ scrollable: true,
59
+ alwaysScroll: true,
60
+ scrollbar: {
61
+ style: { bg: 'cyan' },
62
+ },
63
+ });
64
+ }
65
+
66
+ async loadForVault(vaultId: string, vaultName: string) {
67
+ if (this.loading) return;
68
+ this.loading = true;
69
+ this.currentVaultId = vaultId;
70
+ this.currentVaultName = vaultName;
71
+ this.list.setLabel(` Files — ${vaultName} `);
72
+ this.list.setItems(['Loading files...'] as any);
73
+ this.screen.render();
74
+
75
+ try {
76
+ const { files } = await this.sdk.files.list({ vaultId, limit: 100, sortBy: 'createdAt', order: 'desc' });
77
+ this.files = files.map((f) => ({
78
+ id: f.id,
79
+ name: f.name,
80
+ sizeBytes: f.sizeBytes || 0,
81
+ plaintextSizeBytes: f.plaintextSizeBytes || null,
82
+ mimeType: f.mimeType || '',
83
+ status: f.status || 'unknown',
84
+ createdAt: f.createdAt || '',
85
+ walrusBlobId: f.walrusBlobId || null,
86
+ walrusBlobObjectId: f.walrusBlobObjectId || null,
87
+ encrypted: f.encrypted || false,
88
+ }));
89
+
90
+ if (this.files.length === 0) {
91
+ this.list.setItems(['(no files — press u to upload)'] as any);
92
+ } else {
93
+ const totalW = (this.list.width as number) - 4;
94
+ const sizeW = 9;
95
+ const statusW = 8;
96
+ const dateW = 12;
97
+ const encW = 3;
98
+ const nameW = Math.max(10, totalW - sizeW - statusW - dateW - encW - 4);
99
+
100
+ const items = this.files.map((f) => {
101
+ const size = formatBytes(f.plaintextSizeBytes ?? f.sizeBytes);
102
+ const status = f.status;
103
+ const statusTagged = `${statusColor(f.status)}${f.status}${statusColorClose(f.status)}`;
104
+ const date = f.createdAt ? formatDate(f.createdAt) : '';
105
+ const enc = f.encrypted ? '🔒' : '';
106
+ const name = f.name.length > nameW ? f.name.slice(0, nameW - 1) + '…' : f.name;
107
+ return formatRow([
108
+ { text: name, width: nameW },
109
+ { text: size, width: sizeW, align: 'right' },
110
+ { text: status, tagged: statusTagged, width: statusW },
111
+ { text: enc, width: encW },
112
+ { text: date, width: dateW, align: 'right' },
113
+ ], totalW);
114
+ });
115
+ this.list.setItems(items as any);
116
+ }
117
+ } catch (err: any) {
118
+ this.list.setItems([`Error: ${err.message}`] as any);
119
+ }
120
+
121
+ this.loading = false;
122
+ this.screen.render();
123
+ }
124
+
125
+ clear() {
126
+ this.files = [];
127
+ this.currentVaultId = null;
128
+ this.currentVaultName = '';
129
+ this.list.setLabel(' Files ');
130
+ this.list.setItems(['Select a vault'] as any);
131
+ this.screen.render();
132
+ }
133
+
134
+ getSelected(): FileItem | null {
135
+ const idx = (this.list as any).selected as number;
136
+ return this.files[idx] || null;
137
+ }
138
+
139
+ getFiles(): FileItem[] {
140
+ return this.files;
141
+ }
142
+
143
+ getCurrentVaultId(): string | null {
144
+ return this.currentVaultId;
145
+ }
146
+
147
+ getCurrentVaultName(): string {
148
+ return this.currentVaultName;
149
+ }
150
+
151
+ focus() {
152
+ this.list.focus();
153
+ this.list.style.border = { fg: getCurrentTheme().borderFocus } as any;
154
+ this.screen.render();
155
+ }
156
+
157
+ blur() {
158
+ this.list.style.border = { fg: getCurrentTheme().border } as any;
159
+ this.screen.render();
160
+ }
161
+
162
+ destroy() {
163
+ this.list.destroy();
164
+ }
165
+ }
@@ -0,0 +1,206 @@
1
+ import blessed from 'blessed';
2
+
3
+ export function formatBytes(bytes: number): string {
4
+ if (bytes === 0) return '0 B';
5
+ const k = 1024;
6
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
7
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
8
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
9
+ }
10
+
11
+ export function formatDate(date: string | Date): string {
12
+ const d = new Date(date);
13
+ const now = new Date();
14
+ const diffMs = now.getTime() - d.getTime();
15
+ const diffMins = Math.floor(diffMs / 60000);
16
+ const diffHours = Math.floor(diffMs / 3600000);
17
+ const diffDays = Math.floor(diffMs / 86400000);
18
+
19
+ if (diffMins < 1) return 'just now';
20
+ if (diffMins < 60) return `${diffMins}m ago`;
21
+ if (diffHours < 24) return `${diffHours}h ago`;
22
+ if (diffDays < 7) return `${diffDays}d ago`;
23
+ return d.toLocaleDateString();
24
+ }
25
+
26
+ export function statusColor(status: string): string {
27
+ switch (status) {
28
+ case 'hot': return '{green-fg}';
29
+ case 'synced': return '{blue-fg}';
30
+ case 'cold': return '{yellow-fg}';
31
+ case 'error': return '{red-fg}';
32
+ case 'uploading': return '{gray-fg}';
33
+ default: return '';
34
+ }
35
+ }
36
+
37
+ export function statusColorClose(status: string): string {
38
+ switch (status) {
39
+ case 'hot': return '{/green-fg}';
40
+ case 'synced': return '{/blue-fg}';
41
+ case 'cold': return '{/yellow-fg}';
42
+ case 'error': return '{/red-fg}';
43
+ case 'uploading': return '{/gray-fg}';
44
+ default: return '';
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Format columns into a fixed-width row.
50
+ * Each col: { text, taggedText?, width, align? }
51
+ * `text` is used for width calc, `taggedText` (with color tags) for display.
52
+ */
53
+ export function formatRow(
54
+ cols: { text: string; tagged?: string; width: number; align?: 'left' | 'right' }[],
55
+ totalWidth: number,
56
+ ): string {
57
+ // Last column gets remaining space
58
+ const parts: string[] = [];
59
+ for (let i = 0; i < cols.length; i++) {
60
+ const col = cols[i];
61
+ const w = i === cols.length - 1
62
+ ? Math.max(col.width, totalWidth - cols.slice(0, i).reduce((s, c) => s + c.width + 1, 0))
63
+ : col.width;
64
+ const display = col.tagged ?? col.text;
65
+ const plain = col.text;
66
+ if (col.align === 'right') {
67
+ const pad = Math.max(0, w - plain.length);
68
+ parts.push(' '.repeat(pad) + display);
69
+ } else {
70
+ const pad = Math.max(0, w - plain.length);
71
+ parts.push(display + ' '.repeat(pad));
72
+ }
73
+ }
74
+ return parts.join(' ');
75
+ }
76
+
77
+ export interface Theme {
78
+ name: string;
79
+ border: string;
80
+ borderFocus: string;
81
+ selected: { bg: string; fg: string };
82
+ fg: string;
83
+ accent: string;
84
+ statusBar: { bg: string; fg: string };
85
+ hint: string;
86
+ }
87
+
88
+ export const themes: Theme[] = [
89
+ {
90
+ name: 'Tusky',
91
+ border: 'cyan', borderFocus: 'green',
92
+ selected: { bg: 'cyan', fg: 'black' },
93
+ fg: 'white', accent: 'cyan',
94
+ statusBar: { bg: 'cyan', fg: 'black' },
95
+ hint: 'gray',
96
+ },
97
+ {
98
+ name: 'Claude',
99
+ border: '#d97706', borderFocus: '#f59e0b',
100
+ selected: { bg: '#d97706', fg: 'black' },
101
+ fg: 'white', accent: '#f59e0b',
102
+ statusBar: { bg: '#d97706', fg: 'black' },
103
+ hint: '#a3a3a3',
104
+ },
105
+ {
106
+ name: 'OpenClaw',
107
+ border: '#6366f1', borderFocus: '#818cf8',
108
+ selected: { bg: '#6366f1', fg: 'white' },
109
+ fg: 'white', accent: '#818cf8',
110
+ statusBar: { bg: '#6366f1', fg: 'white' },
111
+ hint: '#71717a',
112
+ },
113
+ {
114
+ name: 'Matrix',
115
+ border: 'green', borderFocus: '#00ff00',
116
+ selected: { bg: 'green', fg: 'black' },
117
+ fg: 'green', accent: '#00ff00',
118
+ statusBar: { bg: 'green', fg: 'black' },
119
+ hint: '#006600',
120
+ },
121
+ {
122
+ name: 'Dracula',
123
+ border: '#bd93f9', borderFocus: '#ff79c6',
124
+ selected: { bg: '#bd93f9', fg: '#282a36' },
125
+ fg: '#f8f8f2', accent: '#ff79c6',
126
+ statusBar: { bg: '#44475a', fg: '#f8f8f2' },
127
+ hint: '#6272a4',
128
+ },
129
+ {
130
+ name: 'Monokai',
131
+ border: '#f92672', borderFocus: '#a6e22e',
132
+ selected: { bg: '#f92672', fg: 'white' },
133
+ fg: '#f8f8f2', accent: '#a6e22e',
134
+ statusBar: { bg: '#49483e', fg: '#f8f8f2' },
135
+ hint: '#75715e',
136
+ },
137
+ {
138
+ name: 'Nord',
139
+ border: '#5e81ac', borderFocus: '#88c0d0',
140
+ selected: { bg: '#5e81ac', fg: '#eceff4' },
141
+ fg: '#d8dee9', accent: '#88c0d0',
142
+ statusBar: { bg: '#3b4252', fg: '#d8dee9' },
143
+ hint: '#4c566a',
144
+ },
145
+ {
146
+ name: 'Sunset',
147
+ border: '#e11d48', borderFocus: '#fb923c',
148
+ selected: { bg: '#e11d48', fg: 'white' },
149
+ fg: '#fecdd3', accent: '#fb923c',
150
+ statusBar: { bg: '#9f1239', fg: 'white' },
151
+ hint: '#be123c',
152
+ },
153
+ ];
154
+
155
+ let currentThemeIdx = 0;
156
+
157
+ export function getCurrentTheme(): Theme {
158
+ return themes[currentThemeIdx];
159
+ }
160
+
161
+ export function cycleTheme(): Theme {
162
+ currentThemeIdx = (currentThemeIdx + 1) % themes.length;
163
+ return themes[currentThemeIdx];
164
+ }
165
+
166
+ export function showError(screen: blessed.Widgets.Screen, message: string) {
167
+ const msg = blessed.message({
168
+ parent: screen,
169
+ top: 'center',
170
+ left: 'center',
171
+ width: '50%',
172
+ height: 'shrink',
173
+ border: { type: 'line' },
174
+ style: {
175
+ border: { fg: 'red' },
176
+ fg: 'red',
177
+ },
178
+ label: ' Error ',
179
+ tags: true,
180
+ });
181
+ msg.display(message, 0, () => {
182
+ msg.destroy();
183
+ screen.render();
184
+ });
185
+ }
186
+
187
+ export function showMessage(screen: blessed.Widgets.Screen, message: string, label = ' Info ') {
188
+ const msg = blessed.message({
189
+ parent: screen,
190
+ top: 'center',
191
+ left: 'center',
192
+ width: '50%',
193
+ height: 'shrink',
194
+ border: { type: 'line' },
195
+ style: {
196
+ border: { fg: 'green' },
197
+ fg: 'white',
198
+ },
199
+ label,
200
+ tags: true,
201
+ });
202
+ msg.display(message, 0, () => {
203
+ msg.destroy();
204
+ screen.render();
205
+ });
206
+ }