@openlife/cli 1.7.11 → 1.7.13

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.
@@ -0,0 +1,418 @@
1
+ "use strict";
2
+ // src/cli/ConfigTui.ts
3
+ // OpenLife configuration TUI — Matrix 2026 theme.
4
+ //
5
+ // Sections:
6
+ // 1. Provider & Model (selector + inline reauth)
7
+ // 2. API Keys (masked editor for OPENAI/ANTHROPIC/GEMINI/OPENROUTER)
8
+ // 3. Telegram Bot (token + allowed user + getMe validation)
9
+ // 4. Voice / TTS (enabled flag + provider)
10
+ // 5. Daemon (start / stop / restart shortcuts)
11
+ //
12
+ // Used by `openlife config` and `/config` slash in ChatTui.
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.runConfig = runConfig;
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const readline = __importStar(require("readline"));
51
+ const child_process_1 = require("child_process");
52
+ const MatrixTheme_1 = require("./MatrixTheme");
53
+ const ProviderSelector_1 = require("./ProviderSelector");
54
+ const InstallModules_1 = require("./InstallModules");
55
+ const PROVIDERS = [
56
+ { value: 'openai-cli', label: 'OpenAI Codex (gpt-5.5 via OAuth)', defaultModel: 'gpt-5.5', authCommand: 'codex logout && codex login' },
57
+ { value: 'openai-api', label: 'OpenAI API (gpt-5.4-mini)', defaultModel: 'gpt-5.4-mini', authCommand: 'set OPENAI_API_KEY in .env' },
58
+ { value: 'anthropic', label: 'Anthropic (Claude)', defaultModel: 'claude-opus-4-7', authCommand: 'set ANTHROPIC_API_KEY in .env' },
59
+ { value: 'gemini-api', label: 'Google Gemini API', defaultModel: 'gemini-3.1-flash', authCommand: 'set GEMINI_API_KEY in .env' },
60
+ { value: 'gemini-cli', label: 'Google Gemini CLI (OAuth)', defaultModel: 'gemini-3.1-pro', authCommand: 'gemini logout && gemini login --device-auth' },
61
+ { value: 'openrouter', label: 'OpenRouter (100+ models)', defaultModel: 'openai/gpt-4.1', authCommand: 'set OPENROUTER_API_KEY in .env' },
62
+ { value: 'ollama', label: 'Ollama (local)', defaultModel: 'qwen2.5-coder:7b', authCommand: 'set OLLAMA_URL in .env' },
63
+ ];
64
+ function readModelsJson(root) {
65
+ const p = path.join(root, 'models.json');
66
+ if (!fs.existsSync(p))
67
+ return {};
68
+ try {
69
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
70
+ }
71
+ catch {
72
+ return {};
73
+ }
74
+ }
75
+ function writeModelsJson(root, content) {
76
+ fs.writeFileSync(path.join(root, 'models.json'), JSON.stringify(content, null, 2) + '\n', 'utf-8');
77
+ }
78
+ function readEnv(root) {
79
+ const out = {};
80
+ const p = path.join(root, '.env');
81
+ if (!fs.existsSync(p))
82
+ return out;
83
+ for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
84
+ const eq = line.indexOf('=');
85
+ if (eq < 1)
86
+ continue;
87
+ out[line.slice(0, eq)] = line.slice(eq + 1).replace(/^['"]|['"]$/g, '');
88
+ }
89
+ return out;
90
+ }
91
+ function mask(s) {
92
+ if (!s)
93
+ return '(unset)';
94
+ if (s.length <= 6)
95
+ return '***';
96
+ return s.slice(0, 4) + '…' + s.slice(-2);
97
+ }
98
+ function hr(width = 60) {
99
+ process.stdout.write((0, MatrixTheme_1.paint)('─'.repeat(width), MatrixTheme_1.MATRIX.tail) + '\n');
100
+ }
101
+ async function ask(question, opts = {}) {
102
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
103
+ return new Promise((resolve) => {
104
+ if (opts.hidden) {
105
+ const stdin = process.stdin;
106
+ const onData = (char) => {
107
+ const c = char.toString();
108
+ if (c === '\n' || c === '\r' || c === '')
109
+ stdin.removeListener('data', onData);
110
+ };
111
+ stdin.on('data', onData);
112
+ }
113
+ rl.question(`${question} `, (answer) => {
114
+ rl.close();
115
+ process.stdout.write('\n');
116
+ resolve(answer.trim());
117
+ });
118
+ });
119
+ }
120
+ // ─── Section: Provider & Model ───────────────────────────────────────
121
+ async function sectionProvider(root) {
122
+ const current = readModelsJson(root);
123
+ const activeProvider = current.primary?.provider || 'openai-cli';
124
+ const activeRaw = current.primary?.raw || 'openai-cli/gpt-5.5';
125
+ process.stdout.write('\n');
126
+ const chosen = await (0, ProviderSelector_1.selectOne)({
127
+ title: (0, MatrixTheme_1.paint)('Select provider', MatrixTheme_1.MATRIX.head),
128
+ active: activeProvider,
129
+ options: PROVIDERS.map((p) => ({ value: p.value, label: p.label, hint: p.value === activeProvider ? '' : '' })),
130
+ });
131
+ if (!chosen) {
132
+ process.stdout.write((0, MatrixTheme_1.paint)('cancelled\n', MatrixTheme_1.MATRIX.tail));
133
+ return;
134
+ }
135
+ const row = PROVIDERS.find((p) => p.value === chosen);
136
+ if (!row)
137
+ return;
138
+ // Build the new ModelIdentifier (provider + default name)
139
+ const newRaw = `${row.value}/${row.defaultModel}`;
140
+ const wasActive = newRaw === activeRaw;
141
+ hr();
142
+ process.stdout.write(`Active model: ${(0, MatrixTheme_1.paint)(activeRaw, MatrixTheme_1.MATRIX.head)}\n`);
143
+ process.stdout.write(`Selected: ${(0, MatrixTheme_1.paint)(newRaw, MatrixTheme_1.MATRIX.body)}${wasActive ? (0, MatrixTheme_1.paint)(' (already active)', MatrixTheme_1.MATRIX.tail) : ''}\n`);
144
+ process.stdout.write(`Auth path: ${(0, MatrixTheme_1.paint)(row.authCommand, MatrixTheme_1.MATRIX.tail)}\n`);
145
+ const action = await (0, ProviderSelector_1.numberedChoice)('What now?', [
146
+ { key: '1', label: wasActive ? 'Keep current credentials' : `Switch primary to ${newRaw}` },
147
+ { key: '2', label: `Reauthenticate (${row.authCommand})` },
148
+ { key: '3', label: 'Cancel' },
149
+ ]);
150
+ if (action === '3' || !action) {
151
+ process.stdout.write((0, MatrixTheme_1.paint)('cancelled\n', MatrixTheme_1.MATRIX.tail));
152
+ return;
153
+ }
154
+ if (action === '1' && !wasActive) {
155
+ const next = {
156
+ primary: { provider: row.value, name: row.defaultModel, raw: newRaw },
157
+ fallbacks: current.fallbacks ?? [],
158
+ };
159
+ writeModelsJson(root, next);
160
+ process.stdout.write((0, MatrixTheme_1.paint)(`✓ primary model updated to ${newRaw}\n`, MatrixTheme_1.MATRIX.head));
161
+ }
162
+ if (action === '2') {
163
+ if (row.value === 'openai-cli') {
164
+ process.stdout.write((0, MatrixTheme_1.paint)('Running codex logout && codex login (interactive)…\n', MatrixTheme_1.MATRIX.body));
165
+ try {
166
+ (0, child_process_1.execFileSync)('codex', ['logout'], { stdio: 'inherit' });
167
+ }
168
+ catch { /* logout may fail if already out */ }
169
+ try {
170
+ (0, child_process_1.execFileSync)('codex', ['login'], { stdio: 'inherit' });
171
+ process.stdout.write((0, MatrixTheme_1.paint)('✓ codex login complete\n', MatrixTheme_1.MATRIX.head));
172
+ }
173
+ catch (e) {
174
+ process.stdout.write((0, MatrixTheme_1.paint)(`✗ codex login failed: ${e instanceof Error ? e.message : String(e)}\n`, MatrixTheme_1.MATRIX.err));
175
+ }
176
+ }
177
+ else if (row.value === 'gemini-cli') {
178
+ try {
179
+ (0, child_process_1.execFileSync)('gemini', ['logout'], { stdio: 'inherit' });
180
+ }
181
+ catch { /* ignore */ }
182
+ try {
183
+ (0, child_process_1.execFileSync)('gemini', ['login', '--device-auth'], { stdio: 'inherit' });
184
+ process.stdout.write((0, MatrixTheme_1.paint)('✓ gemini login complete\n', MatrixTheme_1.MATRIX.head));
185
+ }
186
+ catch (e) {
187
+ process.stdout.write((0, MatrixTheme_1.paint)(`✗ gemini login failed: ${e instanceof Error ? e.message : String(e)}\n`, MatrixTheme_1.MATRIX.err));
188
+ }
189
+ }
190
+ else {
191
+ // API-key providers — open the keys editor scoped to this provider
192
+ const keyName = {
193
+ 'openai-api': 'OPENAI_API_KEY',
194
+ 'anthropic': 'ANTHROPIC_API_KEY',
195
+ 'gemini-api': 'GEMINI_API_KEY',
196
+ 'openrouter': 'OPENROUTER_API_KEY',
197
+ 'ollama': 'OLLAMA_URL',
198
+ }[row.value];
199
+ process.stdout.write(`Paste new ${keyName} (input visible — clear the screen after):\n`);
200
+ const value = await ask(`${keyName}:`);
201
+ if (value) {
202
+ if (keyName.endsWith('_API_KEY')) {
203
+ const slot = keyName.replace('_API_KEY', '').toLowerCase();
204
+ (0, InstallModules_1.saveApiKeysToEnv)(root, { [slot]: value });
205
+ process.stdout.write((0, MatrixTheme_1.paint)(`✓ ${keyName} saved to .env\n`, MatrixTheme_1.MATRIX.head));
206
+ }
207
+ else if (keyName === 'OLLAMA_URL') {
208
+ // Manual .env edit
209
+ const envPath = path.join(root, '.env');
210
+ const cur = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
211
+ const lines = cur.split('\n').filter(Boolean).filter((l) => !l.startsWith('OLLAMA_URL='));
212
+ lines.push(`OLLAMA_URL=${value}`);
213
+ fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf-8');
214
+ process.stdout.write((0, MatrixTheme_1.paint)('✓ OLLAMA_URL saved\n', MatrixTheme_1.MATRIX.head));
215
+ }
216
+ }
217
+ else {
218
+ process.stdout.write((0, MatrixTheme_1.paint)('(empty — kept current)\n', MatrixTheme_1.MATRIX.tail));
219
+ }
220
+ }
221
+ }
222
+ // If user chose action=1 and was already active, just confirm and exit.
223
+ if (action === '1' && wasActive) {
224
+ process.stdout.write((0, MatrixTheme_1.paint)('keeping existing credentials\n', MatrixTheme_1.MATRIX.body) + '\n');
225
+ }
226
+ }
227
+ // ─── Section: API Keys ───────────────────────────────────────────────
228
+ async function sectionApiKeys(root) {
229
+ const env = readEnv(root);
230
+ const slots = [
231
+ { envName: 'OPENAI_API_KEY', slot: 'openai' },
232
+ { envName: 'ANTHROPIC_API_KEY', slot: 'anthropic' },
233
+ { envName: 'GEMINI_API_KEY', slot: 'gemini' },
234
+ { envName: 'OPENROUTER_API_KEY', slot: 'openrouter' },
235
+ { envName: 'ELEVENLABS_API_KEY', slot: 'elevenlabs' },
236
+ ];
237
+ process.stdout.write('\n' + (0, MatrixTheme_1.paint)('API Keys', MatrixTheme_1.MATRIX.head) + '\n');
238
+ for (let i = 0; i < slots.length; i++) {
239
+ const s = slots[i];
240
+ process.stdout.write(` ${(0, MatrixTheme_1.paint)(`${i + 1})`, MatrixTheme_1.MATRIX.body)} ${s.envName.padEnd(22)} ${(0, MatrixTheme_1.paint)(mask(env[s.envName]), MatrixTheme_1.MATRIX.tail)}\n`);
241
+ }
242
+ process.stdout.write(` ${(0, MatrixTheme_1.paint)('6)', MatrixTheme_1.MATRIX.body)} return\n\n`);
243
+ const pick = await ask('Choose [1-6]:');
244
+ const idx = Number(pick) - 1;
245
+ if (Number.isNaN(idx) || idx < 0 || idx >= slots.length)
246
+ return;
247
+ const s = slots[idx];
248
+ process.stdout.write(`\nPaste new ${s.envName} (clear screen after):\n`);
249
+ const value = await ask(`${s.envName}:`);
250
+ if (!value) {
251
+ process.stdout.write((0, MatrixTheme_1.paint)('(empty — kept current)\n', MatrixTheme_1.MATRIX.tail));
252
+ return;
253
+ }
254
+ (0, InstallModules_1.saveApiKeysToEnv)(root, { [s.slot]: value });
255
+ process.stdout.write((0, MatrixTheme_1.paint)(`✓ ${s.envName} saved\n`, MatrixTheme_1.MATRIX.head));
256
+ }
257
+ // ─── Section: Telegram ───────────────────────────────────────────────
258
+ async function sectionTelegram(root) {
259
+ const env = readEnv(root);
260
+ process.stdout.write('\n' + (0, MatrixTheme_1.paint)('Telegram Bot', MatrixTheme_1.MATRIX.head) + '\n');
261
+ process.stdout.write(` current TELEGRAM_BOT_TOKEN: ${(0, MatrixTheme_1.paint)(mask(env.TELEGRAM_BOT_TOKEN), MatrixTheme_1.MATRIX.tail)}\n`);
262
+ process.stdout.write(` current allowed user id: ${(0, MatrixTheme_1.paint)(env.OPENLIFE_TELEGRAM_ALLOWED_USER_ID || '(unset)', MatrixTheme_1.MATRIX.tail)}\n\n`);
263
+ const action = await (0, ProviderSelector_1.numberedChoice)('What to do?', [
264
+ { key: '1', label: 'Set / replace token' },
265
+ { key: '2', label: 'Set / replace allowed user id' },
266
+ { key: '3', label: 'Validate current token (getMe)' },
267
+ { key: '4', label: 'Return' },
268
+ ]);
269
+ if (!action || action === '4')
270
+ return;
271
+ if (action === '1') {
272
+ const token = await ask('Paste TELEGRAM_BOT_TOKEN:');
273
+ if (!token)
274
+ return;
275
+ const val = (0, InstallModules_1.validateTelegramToken)(token);
276
+ if (!val.ok) {
277
+ process.stdout.write((0, MatrixTheme_1.paint)(`✗ ${val.detail}\n`, MatrixTheme_1.MATRIX.err));
278
+ return;
279
+ }
280
+ (0, InstallModules_1.saveTelegramConfig)(root, token);
281
+ process.stdout.write((0, MatrixTheme_1.paint)(`✓ token saved (${val.detail})\n`, MatrixTheme_1.MATRIX.head));
282
+ }
283
+ else if (action === '2') {
284
+ const uid = await ask('Allowed Telegram user id (digits):');
285
+ if (!/^\d+$/.test(uid)) {
286
+ process.stdout.write((0, MatrixTheme_1.paint)('not a numeric id\n', MatrixTheme_1.MATRIX.err));
287
+ return;
288
+ }
289
+ const envPath = path.join(root, '.env');
290
+ const cur = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
291
+ const lines = cur.split('\n').filter(Boolean).filter((l) => !l.startsWith('OPENLIFE_TELEGRAM_ALLOWED_USER_ID='));
292
+ lines.push(`OPENLIFE_TELEGRAM_ALLOWED_USER_ID=${uid}`);
293
+ fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf-8');
294
+ process.stdout.write((0, MatrixTheme_1.paint)('✓ allowed user id saved\n', MatrixTheme_1.MATRIX.head));
295
+ }
296
+ else if (action === '3') {
297
+ const val = (0, InstallModules_1.validateTelegramToken)(env.TELEGRAM_BOT_TOKEN);
298
+ const color = val.ok ? MatrixTheme_1.MATRIX.head : MatrixTheme_1.MATRIX.err;
299
+ process.stdout.write((0, MatrixTheme_1.paint)(`${val.ok ? '✓' : '✗'} ${val.detail}\n`, color));
300
+ }
301
+ }
302
+ // ─── Section: Voice / TTS ────────────────────────────────────────────
303
+ async function sectionVoice(root) {
304
+ const voicePath = path.join(root, '.openlife', 'voice.config.json');
305
+ let cfg = { enabled: false, provider: 'gemini' };
306
+ try {
307
+ if (fs.existsSync(voicePath))
308
+ cfg = { ...cfg, ...JSON.parse(fs.readFileSync(voicePath, 'utf-8')) };
309
+ }
310
+ catch { /* defaults */ }
311
+ process.stdout.write('\n' + (0, MatrixTheme_1.paint)('Voice / TTS', MatrixTheme_1.MATRIX.head) + '\n');
312
+ process.stdout.write(` enabled: ${cfg.enabled ? (0, MatrixTheme_1.paint)('yes', MatrixTheme_1.MATRIX.head) : (0, MatrixTheme_1.paint)('no', MatrixTheme_1.MATRIX.tail)}\n`);
313
+ process.stdout.write(` provider: ${(0, MatrixTheme_1.paint)(cfg.provider, MatrixTheme_1.MATRIX.body)}\n`);
314
+ process.stdout.write(` voice: ${(0, MatrixTheme_1.paint)(cfg.voice || '(default)', MatrixTheme_1.MATRIX.body)}\n\n`);
315
+ const action = await (0, ProviderSelector_1.numberedChoice)('What to change?', [
316
+ { key: '1', label: cfg.enabled ? 'Disable TTS' : 'Enable TTS' },
317
+ { key: '2', label: 'Change provider (gemini / elevenlabs / system)' },
318
+ { key: '3', label: 'Return' },
319
+ ]);
320
+ if (!action || action === '3')
321
+ return;
322
+ if (action === '1')
323
+ cfg.enabled = !cfg.enabled;
324
+ if (action === '2') {
325
+ const newProvider = await ask('Provider [gemini / elevenlabs / system]:');
326
+ if (['gemini', 'elevenlabs', 'system'].includes(newProvider))
327
+ cfg.provider = newProvider;
328
+ }
329
+ try {
330
+ fs.mkdirSync(path.dirname(voicePath), { recursive: true });
331
+ }
332
+ catch { /* exists */ }
333
+ fs.writeFileSync(voicePath, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
334
+ process.stdout.write((0, MatrixTheme_1.paint)('✓ voice config saved\n', MatrixTheme_1.MATRIX.head));
335
+ }
336
+ // ─── Section: Daemon ─────────────────────────────────────────────────
337
+ async function sectionDaemon(root) {
338
+ process.stdout.write('\n' + (0, MatrixTheme_1.paint)('Daemon', MatrixTheme_1.MATRIX.head) + '\n');
339
+ const action = await (0, ProviderSelector_1.numberedChoice)('Action?', [
340
+ { key: '1', label: 'status' },
341
+ { key: '2', label: 'start --daemon (background)' },
342
+ { key: '3', label: 'restart' },
343
+ { key: '4', label: 'stop (restart command)' },
344
+ { key: '5', label: 'return' },
345
+ ]);
346
+ if (!action || action === '5')
347
+ return;
348
+ const bin = path.join(root, 'bin', 'openlife.js');
349
+ try {
350
+ if (action === '1') {
351
+ const out = (0, child_process_1.execFileSync)(process.execPath, [bin, 'status'], { encoding: 'utf-8' });
352
+ process.stdout.write('\n' + out + '\n');
353
+ }
354
+ else if (action === '2') {
355
+ // detach
356
+ const { spawn } = require('child_process');
357
+ const child = spawn(process.execPath, [bin, 'start', '--daemon'], { detached: true, stdio: 'ignore' });
358
+ child.unref();
359
+ process.stdout.write((0, MatrixTheme_1.paint)('✓ daemon spawn requested\n', MatrixTheme_1.MATRIX.head));
360
+ }
361
+ else if (action === '3' || action === '4') {
362
+ const out = (0, child_process_1.execFileSync)(process.execPath, [bin, 'restart'], { encoding: 'utf-8' });
363
+ process.stdout.write('\n' + out + '\n');
364
+ }
365
+ }
366
+ catch (e) {
367
+ process.stdout.write((0, MatrixTheme_1.paint)(`✗ ${e instanceof Error ? e.message : String(e)}\n`, MatrixTheme_1.MATRIX.err));
368
+ }
369
+ }
370
+ // ─── Main entry ──────────────────────────────────────────────────────
371
+ async function runConfig(opts = {}) {
372
+ const root = opts.cwd ?? process.cwd();
373
+ if (opts.focus === 'provider') {
374
+ await sectionProvider(root);
375
+ return;
376
+ }
377
+ if (opts.focus === 'api-keys') {
378
+ await sectionApiKeys(root);
379
+ return;
380
+ }
381
+ if (opts.focus === 'telegram') {
382
+ await sectionTelegram(root);
383
+ return;
384
+ }
385
+ if (opts.focus === 'voice') {
386
+ await sectionVoice(root);
387
+ return;
388
+ }
389
+ if (opts.focus === 'daemon') {
390
+ await sectionDaemon(root);
391
+ return;
392
+ }
393
+ // eslint-disable-next-line no-constant-condition
394
+ while (true) {
395
+ const env = readEnv(root);
396
+ const models = readModelsJson(root);
397
+ process.stdout.write('\n' + (0, MatrixTheme_1.paint)('OpenLife Configuration', MatrixTheme_1.MATRIX.head) + '\n\n');
398
+ process.stdout.write(` 1) ${(0, MatrixTheme_1.paint)('Provider & Model', MatrixTheme_1.MATRIX.body).padEnd(25)} ${(0, MatrixTheme_1.paint)(models.primary?.raw || 'unknown', MatrixTheme_1.MATRIX.tail)}\n`);
399
+ process.stdout.write(` 2) ${(0, MatrixTheme_1.paint)('API Keys', MatrixTheme_1.MATRIX.body).padEnd(25)} openai ${env.OPENAI_API_KEY ? '✓' : '·'} anthropic ${env.ANTHROPIC_API_KEY ? '✓' : '·'} gemini ${env.GEMINI_API_KEY ? '✓' : '·'}\n`);
400
+ process.stdout.write(` 3) ${(0, MatrixTheme_1.paint)('Telegram Bot', MatrixTheme_1.MATRIX.body).padEnd(25)} ${env.TELEGRAM_BOT_TOKEN ? '✓' : '·'}\n`);
401
+ process.stdout.write(` 4) ${(0, MatrixTheme_1.paint)('Voice / TTS', MatrixTheme_1.MATRIX.body).padEnd(25)}\n`);
402
+ process.stdout.write(` 5) ${(0, MatrixTheme_1.paint)('Daemon', MatrixTheme_1.MATRIX.body).padEnd(25)}\n`);
403
+ process.stdout.write(` 6) ${(0, MatrixTheme_1.paint)('Exit', MatrixTheme_1.MATRIX.body)}\n\n`);
404
+ const pick = await ask('Choose [1-6]:');
405
+ if (pick === '6' || pick === '')
406
+ return;
407
+ if (pick === '1')
408
+ await sectionProvider(root);
409
+ else if (pick === '2')
410
+ await sectionApiKeys(root);
411
+ else if (pick === '3')
412
+ await sectionTelegram(root);
413
+ else if (pick === '4')
414
+ await sectionVoice(root);
415
+ else if (pick === '5')
416
+ await sectionDaemon(root);
417
+ }
418
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ // src/cli/MatrixRain.ts
3
+ // Animated Matrix code rain — vertical streams of glyphs falling, brightest
4
+ // at the head fading down the tail. Renders into a fixed region via
5
+ // absolute cursor positioning so the chat scroll above stays intact.
6
+ //
7
+ // Reference: Downloads/69e80715-...png
8
+ // Usage:
9
+ // const rain = startRain({ row: 12, col: 1, width: 60, height: 8 });
10
+ // // ...await long task...
11
+ // rain.stop();
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.startRain = startRain;
14
+ exports.sampleFrame = sampleFrame;
15
+ const MatrixTheme_1 = require("./MatrixTheme");
16
+ const randomGlyph = () => MatrixTheme_1.RAIN_GLYPHS[Math.floor(Math.random() * MatrixTheme_1.RAIN_GLYPHS.length)];
17
+ /**
18
+ * Start the rain animation. Caller MUST call `.stop()` before exit or after
19
+ * the awaited work completes, otherwise the interval leaks.
20
+ *
21
+ * Safe in non-TTY contexts (returns a no-op handle) so callers don't need
22
+ * to branch.
23
+ */
24
+ function startRain(opts) {
25
+ // No-op in non-TTY / no-color environments — rain would corrupt logs.
26
+ if (!(0, MatrixTheme_1.supportsColor)() || !process.stdout.isTTY) {
27
+ return { stop: () => { }, isRunning: () => false };
28
+ }
29
+ const tickMs = opts.tickMs ?? MatrixTheme_1.RAIN_CONFIG.tickMs;
30
+ const rainWidth = Math.max(1, opts.rainWidth ?? opts.width);
31
+ const numColumns = rainWidth;
32
+ const columns = Array.from({ length: numColumns }, () => ({
33
+ head: 0,
34
+ trail: 0,
35
+ glyphs: Array.from({ length: opts.height }, () => ' '),
36
+ active: false,
37
+ }));
38
+ // Hide cursor while rain runs. Restored in stop().
39
+ process.stdout.write(MatrixTheme_1.ANSI.hideCursor);
40
+ const render = () => {
41
+ // Save cursor so the user's input prompt below is preserved.
42
+ process.stdout.write(MatrixTheme_1.ANSI.save);
43
+ for (let cIdx = 0; cIdx < numColumns; cIdx++) {
44
+ const col = columns[cIdx];
45
+ // Spawn new column if dormant
46
+ if (!col.active && Math.random() < MatrixTheme_1.RAIN_CONFIG.spawnChance) {
47
+ col.active = true;
48
+ col.head = 0;
49
+ col.trail = MatrixTheme_1.RAIN_CONFIG.trailMin + Math.floor(Math.random() * (MatrixTheme_1.RAIN_CONFIG.trailMax - MatrixTheme_1.RAIN_CONFIG.trailMin));
50
+ col.glyphs = Array.from({ length: opts.height }, () => ' ');
51
+ }
52
+ if (!col.active)
53
+ continue;
54
+ // Advance the head down one row
55
+ col.head++;
56
+ // Decide glyph at the new head position
57
+ if (col.head <= opts.height) {
58
+ col.glyphs[col.head - 1] = randomGlyph();
59
+ }
60
+ // Mutate a mid-trail glyph occasionally for noise
61
+ if (Math.random() < MatrixTheme_1.RAIN_CONFIG.glyphMutate && col.head > 2) {
62
+ const mid = col.head - 1 - Math.floor(Math.random() * Math.min(col.trail, col.head - 1));
63
+ if (mid >= 0 && mid < opts.height)
64
+ col.glyphs[mid] = randomGlyph();
65
+ }
66
+ // Paint the column from top to bottom of the region
67
+ for (let r = 0; r < opts.height; r++) {
68
+ const rowRelToHead = col.head - 1 - r; // 0 = head, positive = trail above
69
+ if (rowRelToHead < 0 || rowRelToHead > col.trail || col.glyphs[r] === ' ') {
70
+ // Erase residue when head moved past
71
+ process.stdout.write((0, MatrixTheme_1.moveTo)(opts.row + r, opts.col + cIdx) + ' ');
72
+ continue;
73
+ }
74
+ let color = MatrixTheme_1.MATRIX.body;
75
+ if (rowRelToHead === 0)
76
+ color = MatrixTheme_1.MATRIX.head;
77
+ else if (rowRelToHead >= col.trail - 2)
78
+ color = MatrixTheme_1.MATRIX.tail;
79
+ process.stdout.write((0, MatrixTheme_1.moveTo)(opts.row + r, opts.col + cIdx) + color + col.glyphs[r] + MatrixTheme_1.ANSI.reset);
80
+ }
81
+ // Deactivate when entire trail has scrolled off
82
+ if (col.head > opts.height + col.trail) {
83
+ col.active = false;
84
+ }
85
+ }
86
+ // Optional side panel overlay (reasoning text, etc.)
87
+ if (opts.sidePanelLines) {
88
+ const lines = opts.sidePanelLines();
89
+ const panelCol = opts.col + rainWidth + 1;
90
+ for (let i = 0; i < Math.min(lines.length, opts.height); i++) {
91
+ // Clear the line in the region first to avoid overdraw
92
+ process.stdout.write((0, MatrixTheme_1.moveTo)(opts.row + i, panelCol) + MatrixTheme_1.ANSI.clearLine.replace(/\[2K/, '[K'));
93
+ process.stdout.write((0, MatrixTheme_1.moveTo)(opts.row + i, panelCol) + MatrixTheme_1.MATRIX.body + lines[i] + MatrixTheme_1.ANSI.reset);
94
+ }
95
+ }
96
+ process.stdout.write(MatrixTheme_1.ANSI.restore);
97
+ };
98
+ const intervalId = setInterval(render, tickMs);
99
+ let running = true;
100
+ return {
101
+ isRunning: () => running,
102
+ stop: () => {
103
+ if (!running)
104
+ return;
105
+ running = false;
106
+ clearInterval(intervalId);
107
+ // Clear the rain region so the next chat output starts clean.
108
+ process.stdout.write(MatrixTheme_1.ANSI.save);
109
+ for (let r = 0; r < opts.height; r++) {
110
+ process.stdout.write((0, MatrixTheme_1.moveTo)(opts.row + r, opts.col));
111
+ process.stdout.write(' '.repeat(opts.width));
112
+ }
113
+ process.stdout.write(MatrixTheme_1.ANSI.restore);
114
+ process.stdout.write(MatrixTheme_1.ANSI.showCursor);
115
+ process.stdout.write(MatrixTheme_1.ANSI.reset);
116
+ },
117
+ };
118
+ }
119
+ /**
120
+ * Static (non-interactive) sample frame — used in tests to confirm the
121
+ * renderer doesn't blow up and produces reasonable bytes.
122
+ */
123
+ function sampleFrame(width = 20, height = 5) {
124
+ const rows = [];
125
+ for (let r = 0; r < height; r++) {
126
+ let line = '';
127
+ for (let c = 0; c < width; c++) {
128
+ const dice = Math.random();
129
+ if (dice < 0.05)
130
+ line += MatrixTheme_1.MATRIX.head + randomGlyph() + MatrixTheme_1.ANSI.reset;
131
+ else if (dice < 0.25)
132
+ line += MatrixTheme_1.MATRIX.body + randomGlyph() + MatrixTheme_1.ANSI.reset;
133
+ else if (dice < 0.40)
134
+ line += MatrixTheme_1.MATRIX.tail + randomGlyph() + MatrixTheme_1.ANSI.reset;
135
+ else
136
+ line += ' ';
137
+ }
138
+ rows.push(line);
139
+ }
140
+ return rows.join('\n');
141
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ // src/cli/MatrixTheme.ts
3
+ // Visual theme for the OpenLife TUI suite — Matrix 2026 redesign.
4
+ // Lime green on black. Vanilla ANSI 256-color codes, no deps.
5
+ // Reference: Downloads/ChatGPT Image 15 de mai. de 2026, 13_58_20.png (OPENLIFE logo)
6
+ // Reference: Downloads/69e80715-...png (code rain)
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.RAIN_CONFIG = exports.RAIN_GLYPHS = exports.MATRIX = exports.ANSI = void 0;
9
+ exports.moveTo = moveTo;
10
+ exports.colorize = colorize;
11
+ exports.center = center;
12
+ exports.supportsColor = supportsColor;
13
+ exports.stripAnsi = stripAnsi;
14
+ exports.paint = paint;
15
+ exports.ANSI = Object.freeze({
16
+ reset: '\x1b[0m',
17
+ bold: '\x1b[1m',
18
+ dim: '\x1b[2m',
19
+ italic: '\x1b[3m',
20
+ hideCursor: '\x1b[?25l',
21
+ showCursor: '\x1b[?25h',
22
+ clearScreen: '\x1b[2J\x1b[H',
23
+ clearLine: '\x1b[2K',
24
+ save: '\x1b[s',
25
+ restore: '\x1b[u',
26
+ });
27
+ // Matrix lime green palette — three brightness stops.
28
+ exports.MATRIX = Object.freeze({
29
+ head: '\x1b[38;5;82m\x1b[1m', // bright lime — column head, banner, highlights
30
+ body: '\x1b[38;5;46m', // canonical Matrix green — base text
31
+ tail: '\x1b[38;5;22m\x1b[2m', // faded — rain tail, dividers, secondary text
32
+ warn: '\x1b[38;5;226m', // yellow — caution
33
+ err: '\x1b[38;5;196m', // red — errors
34
+ white: '\x1b[38;5;231m', // pure white — accents only
35
+ });
36
+ // Glyph set for the Matrix rain — digits + half-width katakana + a few symbols.
37
+ // Half-width kana mimics the original Matrix film aesthetic while staying
38
+ // 1 column wide in any terminal font (full-width kana would break alignment).
39
+ exports.RAIN_GLYPHS = ('0123456789' +
40
+ 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン' +
41
+ '*+-/:.<>=#$%@').split('');
42
+ // Rain timing — 80ms tick matches the reference (~12fps), low enough CPU.
43
+ exports.RAIN_CONFIG = Object.freeze({
44
+ tickMs: 80,
45
+ trailMin: 6,
46
+ trailMax: 18,
47
+ spawnChance: 0.15, // probability a new column starts each tick
48
+ glyphMutate: 0.08, // probability a mid-trail glyph swaps for noise
49
+ });
50
+ // Cursor positioning helpers (1-indexed, ANSI convention).
51
+ function moveTo(row, col) {
52
+ return `\x1b[${row};${col}H`;
53
+ }
54
+ function colorize(text, color) {
55
+ return `${color}${text}${exports.ANSI.reset}`;
56
+ }
57
+ // Centered line padded to a given width with spaces.
58
+ function center(text, width) {
59
+ if (text.length >= width)
60
+ return text;
61
+ const pad = Math.floor((width - text.length) / 2);
62
+ return ' '.repeat(pad) + text;
63
+ }
64
+ // Detect whether stdout supports the 256-color theme.
65
+ // Conservative: requires TTY + non-dumb TERM. CI/pipes get plain output.
66
+ function supportsColor() {
67
+ if (process.env.NO_COLOR)
68
+ return false;
69
+ if (process.env.FORCE_COLOR)
70
+ return true;
71
+ if (!process.stdout.isTTY)
72
+ return false;
73
+ const term = (process.env.TERM || '').toLowerCase();
74
+ if (!term || term === 'dumb')
75
+ return false;
76
+ return true;
77
+ }
78
+ // Strip ANSI from a string — for tests and non-TTY rendering paths.
79
+ function stripAnsi(s) {
80
+ // Covers SGR, cursor moves, save/restore, screen clears. Good enough for tests.
81
+ return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
82
+ }
83
+ // Conditional wrapper — applies color only when terminal supports it.
84
+ function paint(text, color) {
85
+ return supportsColor() ? `${color}${text}${exports.ANSI.reset}` : text;
86
+ }