@openlife/cli 1.7.11 → 1.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/ChatBanner.js +112 -0
- package/dist/cli/ChatTui.js +316 -0
- package/dist/cli/ConfigTui.js +418 -0
- package/dist/cli/MatrixRain.js +141 -0
- package/dist/cli/MatrixTheme.js +86 -0
- package/dist/cli/ProviderSelector.js +178 -0
- package/dist/index.js +40 -1
- package/dist/orchestrator/Brain.js +17 -11
- package/dist/orchestrator/Gateway.js +64 -0
- package/dist/test_chat_tui.js +116 -0
- package/dist/test_config_tui.js +116 -0
- package/dist/test_matrix_rain.js +69 -0
- package/package.json +5 -2
- package/scripts/reauth-providers.sh +205 -0
|
@@ -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
|
+
}
|