@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.
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ // src/cli/ProviderSelector.ts
3
+ // Reusable arrow-key radio menu — used by ConfigTui and any TUI that
4
+ // needs a "pick one of N options" interaction.
5
+ //
6
+ // Matches the Hermes Agent provider-selector UX:
7
+ // - ↑↓ navigate
8
+ // - ENTER / SPACE select
9
+ // - ESC cancel
10
+ // - currently-active option marked with `(•)` and `← currently active`
11
+ // - other options marked with `(o)`
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.selectOne = selectOne;
47
+ exports.numberedChoice = numberedChoice;
48
+ const readline = __importStar(require("readline"));
49
+ const MatrixTheme_1 = require("./MatrixTheme");
50
+ /**
51
+ * Show the menu, await user input, return the selected value (or null on ESC).
52
+ *
53
+ * In non-TTY contexts (CI, piped), prints the menu once and returns
54
+ * `options[initialIndex].value` without blocking — keeps scripts working.
55
+ */
56
+ async function selectOne(opts) {
57
+ const activeIdx = opts.options.findIndex(o => o.value === opts.active);
58
+ let cursor = opts.initialIndex ?? (activeIdx >= 0 ? activeIdx : 0);
59
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
60
+ process.stdout.write(renderMenu(opts.title, opts.options, cursor, opts.active));
61
+ return opts.options[cursor]?.value ?? null;
62
+ }
63
+ return new Promise((resolve) => {
64
+ const previousRaw = process.stdin.isRaw === true;
65
+ try {
66
+ readline.emitKeypressEvents(process.stdin);
67
+ process.stdin.setRawMode(true);
68
+ }
69
+ catch {
70
+ // Non-TTY edge case — fall back to first option.
71
+ resolve(opts.options[cursor]?.value ?? null);
72
+ return;
73
+ }
74
+ process.stdout.write(MatrixTheme_1.ANSI.hideCursor);
75
+ let firstDraw = true;
76
+ const draw = () => {
77
+ const block = renderMenu(opts.title, opts.options, cursor, opts.active);
78
+ if (!firstDraw) {
79
+ // Move cursor up by the number of lines previously printed so we
80
+ // overwrite the menu in place rather than scroll.
81
+ const lineCount = block.split('\n').length;
82
+ process.stdout.write(`\x1b[${lineCount}A`);
83
+ }
84
+ firstDraw = false;
85
+ process.stdout.write(block);
86
+ };
87
+ const cleanup = () => {
88
+ process.stdin.removeListener('keypress', onKey);
89
+ try {
90
+ process.stdin.setRawMode(previousRaw);
91
+ }
92
+ catch { /* ignore */ }
93
+ process.stdout.write(MatrixTheme_1.ANSI.showCursor);
94
+ process.stdout.write(MatrixTheme_1.ANSI.reset);
95
+ };
96
+ const onKey = (_str, key) => {
97
+ if (!key)
98
+ return;
99
+ if (key.ctrl && key.name === 'c') {
100
+ cleanup();
101
+ process.stdout.write('\n');
102
+ resolve(null);
103
+ return;
104
+ }
105
+ if (key.name === 'escape') {
106
+ cleanup();
107
+ process.stdout.write('\n');
108
+ resolve(null);
109
+ return;
110
+ }
111
+ if (key.name === 'up' || key.name === 'k') {
112
+ cursor = (cursor - 1 + opts.options.length) % opts.options.length;
113
+ draw();
114
+ return;
115
+ }
116
+ if (key.name === 'down' || key.name === 'j') {
117
+ cursor = (cursor + 1) % opts.options.length;
118
+ draw();
119
+ return;
120
+ }
121
+ if (key.name === 'return' || key.name === 'space') {
122
+ cleanup();
123
+ process.stdout.write('\n');
124
+ resolve(opts.options[cursor]?.value ?? null);
125
+ return;
126
+ }
127
+ };
128
+ process.stdin.on('keypress', onKey);
129
+ draw();
130
+ });
131
+ }
132
+ function renderMenu(title, options, cursor, active) {
133
+ const useColor = (0, MatrixTheme_1.supportsColor)();
134
+ const head = useColor ? MatrixTheme_1.MATRIX.head : '';
135
+ const body = useColor ? MatrixTheme_1.MATRIX.body : '';
136
+ const tail = useColor ? MatrixTheme_1.MATRIX.tail : '';
137
+ const reset = useColor ? MatrixTheme_1.ANSI.reset : '';
138
+ const lines = [];
139
+ lines.push(`${head}${title}${reset}`);
140
+ lines.push(`${tail}↑↓ navigate · ENTER select · ESC cancel${reset}`);
141
+ lines.push('');
142
+ for (let i = 0; i < options.length; i++) {
143
+ const opt = options[i];
144
+ const isCursor = i === cursor;
145
+ const isActive = opt.value === active;
146
+ const marker = isActive ? '(•)' : '(o)';
147
+ const arrow = isCursor ? '▶' : ' ';
148
+ const hint = opt.hint ? `${tail} — ${opt.hint}${reset}` : '';
149
+ const activeTag = isActive ? ` ${tail}← currently active${reset}` : '';
150
+ const color = isCursor ? head : body;
151
+ lines.push(` ${color}${arrow} ${marker} ${opt.label}${reset}${hint}${activeTag}`);
152
+ }
153
+ lines.push('');
154
+ return lines.join('\n');
155
+ }
156
+ /**
157
+ * Convenience: numbered 1/2/3 choice prompt — used after provider
158
+ * selection for the `1) Use existing 2) Reauth 3) Cancel` flow.
159
+ */
160
+ async function numberedChoice(question, choices) {
161
+ const useColor = (0, MatrixTheme_1.supportsColor)();
162
+ const head = useColor ? MatrixTheme_1.MATRIX.head : '';
163
+ const body = useColor ? MatrixTheme_1.MATRIX.body : '';
164
+ const reset = useColor ? MatrixTheme_1.ANSI.reset : '';
165
+ process.stdout.write(`\n${head}${question}${reset}\n`);
166
+ for (const c of choices) {
167
+ process.stdout.write(` ${body}${c.key}) ${c.label}${reset}\n`);
168
+ }
169
+ const keys = choices.map(c => c.key).join('/');
170
+ return new Promise((resolve) => {
171
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
172
+ rl.question(`\n ${body}choice [${keys}]:${reset} `, (answer) => {
173
+ rl.close();
174
+ const k = answer.trim();
175
+ resolve(choices.find(c => c.key === k)?.key ?? null);
176
+ });
177
+ });
178
+ }
package/dist/index.js CHANGED
@@ -2825,4 +2825,43 @@ profileCmd.command('import <file>').description('Import a profile from a JSON fi
2825
2825
  process.exitCode = 1;
2826
2826
  }
2827
2827
  });
2828
- program.parse(process.argv);
2828
+ // `openlife config` — interactive configuration TUI (provider, keys, telegram, voice, daemon)
2829
+ program
2830
+ .command('config')
2831
+ .description('Configurações interativas (provider, API keys, Telegram, voz, daemon)')
2832
+ .option('--focus <section>', 'Open directly into a section: provider | api-keys | telegram | voice | daemon')
2833
+ .action(async (opts) => {
2834
+ const { runConfig } = require('./cli/ConfigTui');
2835
+ await runConfig({ focus: opts.focus });
2836
+ });
2837
+ // `openlife chat` — explicit alias for the interactive chat TUI (matches docs)
2838
+ program
2839
+ .command('chat')
2840
+ .description('Inicia o chat interativo no terminal (Matrix-themed REPL)')
2841
+ .action(async () => {
2842
+ const { runChat } = require('./cli/ChatTui');
2843
+ await runChat();
2844
+ });
2845
+ // Bare invocation (no subcommand + interactive TTY) → launch chat TUI.
2846
+ // Piped, CI, or `--help` flows fall through to the standard Commander parse.
2847
+ const isBareInteractive = process.argv.length === 2 &&
2848
+ process.stdout.isTTY === true &&
2849
+ process.stdin.isTTY === true &&
2850
+ !process.env.OPENLIFE_DISABLE_CHAT_TUI;
2851
+ if (isBareInteractive) {
2852
+ // Defer to ChatTui — never returns under normal use.
2853
+ (async () => {
2854
+ try {
2855
+ const { runChat } = require('./cli/ChatTui');
2856
+ await runChat();
2857
+ }
2858
+ catch (e) {
2859
+ const msg = e instanceof Error ? e.message : String(e);
2860
+ console.error(`[chat] failed to start: ${msg}`);
2861
+ process.exitCode = 1;
2862
+ }
2863
+ })();
2864
+ }
2865
+ else {
2866
+ program.parse(process.argv);
2867
+ }
@@ -58,11 +58,6 @@ function getCodexFastTimeoutMs() {
58
58
  function getCodexDeepTimeoutMs() {
59
59
  return Number(process.env.OPENLIFE_CODEX_DEEP_TIMEOUT_MS || DEFAULT_MODEL_TIMEOUT_MS);
60
60
  }
61
- /**
62
- * Typed Codex CLI timeout. Callers (Gateway, Gatekeeper) can distinguish a
63
- * real timeout from a generic provider failure and present a clearer message.
64
- * The `depth` attribute marks whether the fast or deep budget fired.
65
- */
66
61
  class CodexTimeoutError extends Error {
67
62
  depth;
68
63
  timeoutMs;
@@ -117,23 +112,31 @@ class Brain {
117
112
  return true;
118
113
  return false;
119
114
  }
120
- async think(systemPrompt, userMessage) {
115
+ async think(systemPrompt, userMessage, opts) {
121
116
  const config = this.modelManager.getModelConfig();
122
117
  const modelsToTry = [config.primary, ...config.fallbacks].filter((model, index, arr) => arr.findIndex(m => m.raw === model.raw) === index);
123
- return this.runModelChain(systemPrompt, userMessage, modelsToTry, 'deep');
118
+ return this.runModelChain(systemPrompt, userMessage, modelsToTry, 'deep', opts);
124
119
  }
125
120
  /** Fast conversational path. Keeps the configured model chain order intact. */
126
- async thinkFast(systemPrompt, userMessage) {
121
+ async thinkFast(systemPrompt, userMessage, opts) {
127
122
  const config = this.modelManager.getModelConfig();
128
123
  const models = [config.primary, ...config.fallbacks].filter((model, index, arr) => arr.findIndex(m => m.raw === model.raw) === index);
129
- return this.runModelChain(systemPrompt, userMessage, models, 'fast');
124
+ return this.runModelChain(systemPrompt, userMessage, models, 'fast', opts);
130
125
  }
131
- async runModelChain(systemPrompt, userMessage, modelsToTry, depth = 'deep') {
126
+ async runModelChain(systemPrompt, userMessage, modelsToTry, depth = 'deep', opts) {
127
+ const emitReasoning = (chunk) => {
128
+ try {
129
+ opts?.onReasoning?.(chunk);
130
+ }
131
+ catch { /* never let a callback break the chain */ }
132
+ };
133
+ emitReasoning(`🧭 chain: ${modelsToTry.map(m => m.raw).join(' → ')}`);
132
134
  const failures = [];
133
135
  for (let i = 0; i < modelsToTry.length; i++) {
134
136
  const modelIdent = modelsToTry[i];
135
137
  try {
136
138
  console.log(`[BRAIN] Tentando modelo ${i === 0 ? 'PRIMÁRIO' : 'FALLBACK ' + i}: ${modelIdent.raw}...`);
139
+ emitReasoning(`🔎 trying ${i === 0 ? 'primary' : `fallback ${i}`}: ${modelIdent.raw}`);
137
140
  let out = '';
138
141
  switch (modelIdent.provider) {
139
142
  case 'openai-api':
@@ -173,8 +176,11 @@ class Brain {
173
176
  const reason = error instanceof Error ? error.message : String(error);
174
177
  failures.push({ model: modelIdent.raw, reason });
175
178
  console.error(`[BRAIN ERROR - ${modelIdent.raw}]`, reason);
176
- if (i !== modelsToTry.length - 1)
179
+ emitReasoning(`✗ ${modelIdent.raw} failed: ${reason.slice(0, 120)}`);
180
+ if (i !== modelsToTry.length - 1) {
177
181
  console.log(`[BRAIN] Rotacionando para o próximo fallback da cadeia...`);
182
+ emitReasoning('↻ rotating to next fallback');
183
+ }
178
184
  }
179
185
  }
180
186
  const concise = failures.slice(-3).map(f => `- ${f.model}: ${f.reason}`).join('\n');
@@ -418,6 +418,70 @@ class Gateway {
418
418
  const detailed = await this.processTextForTestDetailed(userId, text);
419
419
  return detailed.finalText;
420
420
  }
421
+ /**
422
+ * Public chat entry point for non-Telegram surfaces (CLI chat TUI, HTTP
423
+ * pilots, etc.). Wraps the same processInput pipeline used by the Telegram
424
+ * flow but routes replies through a minimal in-memory ctx so no Telegraf
425
+ * dependency is required at the call site.
426
+ *
427
+ * onReasoning fires for each pipeline trace event (classify → route →
428
+ * finalize) so a TUI can stream the agent's thinking into the Matrix-rain
429
+ * region instead of showing an opaque spinner.
430
+ */
431
+ async processChat(userId, text, opts) {
432
+ let finalReply = '';
433
+ let lastEdit = null;
434
+ let mid = 0;
435
+ const ctx = {
436
+ sendChatAction: async () => { },
437
+ reply: async (m) => {
438
+ finalReply = m;
439
+ return { message_id: ++mid };
440
+ },
441
+ sendVoice: async (_src, options) => {
442
+ finalReply = options?.caption || finalReply;
443
+ return { message_id: ++mid };
444
+ },
445
+ chat: { id: userId },
446
+ telegram: {
447
+ editMessageText: async (_chat, _msg, _inline, edited) => {
448
+ lastEdit = edited;
449
+ return true;
450
+ }
451
+ }
452
+ };
453
+ // Bridge: every line containing one of the Gateway trace markers also
454
+ // becomes an onReasoning event. We patch the trace array indirectly by
455
+ // wrapping safeReply / editMessageText to detect when the final ACK
456
+ // edit lands. For per-step events we rely on processInput's existing
457
+ // trace.push() calls — to expose those we use a console.log shim only
458
+ // for THIS call (not global) when a callback is provided.
459
+ const onReasoning = opts?.onReasoning;
460
+ if (typeof onReasoning === 'function') {
461
+ const originalLog = console.log;
462
+ const restore = () => { console.log = originalLog; };
463
+ console.log = ((...args) => {
464
+ originalLog(...args);
465
+ const line = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
466
+ if (/\[BRAIN\]|\[GATEWAY\]|\[GATEKEEPER\]|classify_intent|route_task|finalize_reply/.test(line)) {
467
+ try {
468
+ onReasoning(line);
469
+ }
470
+ catch { /* ignore */ }
471
+ }
472
+ });
473
+ try {
474
+ await this.processInput(ctx, userId, text);
475
+ }
476
+ finally {
477
+ restore();
478
+ }
479
+ }
480
+ else {
481
+ await this.processInput(ctx, userId, text);
482
+ }
483
+ return lastEdit ?? finalReply;
484
+ }
421
485
  /**
422
486
  * Test seam that captures the full ACK + final timeline for the fast-ACK
423
487
  * pattern. Used by the regression test to assert the placeholder reply
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ // src/test_chat_tui.ts
3
+ // Tests for the ChatTui non-TTY surface area:
4
+ // - buildInventoryStats reads .catalog counts correctly
5
+ // - reading models.json returns the active model
6
+ // - session log appends to .openlife/chat-sessions/<id>.jsonl
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ const ChatTui_1 = require("./cli/ChatTui");
45
+ let failed = 0;
46
+ function check(label, condition, detail) {
47
+ if (condition) {
48
+ console.log(`✅ ${label}`);
49
+ }
50
+ else {
51
+ console.error(`❌ ${label}${detail ? ` — ${detail}` : ''}`);
52
+ failed++;
53
+ }
54
+ }
55
+ function makeTempRoot() {
56
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openlife-chat-tui-'));
57
+ // .catalog skeleton with a couple of fake entries
58
+ fs.mkdirSync(path.join(root, '.catalog', 'agents', 'agent-a'), { recursive: true });
59
+ fs.mkdirSync(path.join(root, '.catalog', 'agents', 'agent-b'), { recursive: true });
60
+ fs.mkdirSync(path.join(root, '.catalog', 'squads', 'squad-a'), { recursive: true });
61
+ fs.mkdirSync(path.join(root, '.catalog', 'skills', 'skill-a'), { recursive: true });
62
+ fs.mkdirSync(path.join(root, '.catalog', 'mcps'), { recursive: true });
63
+ // package.json with version
64
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'test', version: '9.9.9' }), 'utf-8');
65
+ // models.json with a primary
66
+ fs.writeFileSync(path.join(root, 'models.json'), JSON.stringify({ primary: { provider: 'p', name: 'm', raw: 'p/m' }, fallbacks: [] }), 'utf-8');
67
+ return root;
68
+ }
69
+ // 1. buildInventoryStats reads .catalog and models.json
70
+ {
71
+ const root = makeTempRoot();
72
+ try {
73
+ const stats = (0, ChatTui_1.buildInventoryStats)(root, 'sess-1');
74
+ check('version reads from package.json', stats.version === 'v9.9.9', `got=${stats.version}`);
75
+ check('model reads from models.json', stats.model === 'p/m', `got=${stats.model}`);
76
+ check('agents counted', stats.agents === 2, `got=${stats.agents}`);
77
+ check('squads counted', stats.squads === 1, `got=${stats.squads}`);
78
+ check('skills counted', stats.skills === 1, `got=${stats.skills}`);
79
+ check('mcps counted (0 ok)', stats.mcps === 0, `got=${stats.mcps}`);
80
+ check('sessionId echoed', stats.sessionId === 'sess-1');
81
+ check('cwd reflects root', stats.cwd === root);
82
+ }
83
+ finally {
84
+ fs.rmSync(root, { recursive: true, force: true });
85
+ }
86
+ }
87
+ // 2. test-* directories are ignored (matches clean-test-pollution semantics)
88
+ {
89
+ const root = makeTempRoot();
90
+ try {
91
+ fs.mkdirSync(path.join(root, '.catalog', 'agents', 'test-pollution-1'), { recursive: true });
92
+ const stats = (0, ChatTui_1.buildInventoryStats)(root, 's');
93
+ check('test-* agents excluded from counts', stats.agents === 2, `got=${stats.agents} (should ignore test-pollution-1)`);
94
+ }
95
+ finally {
96
+ fs.rmSync(root, { recursive: true, force: true });
97
+ }
98
+ }
99
+ // 3. Missing .catalog returns zeros, doesn't throw
100
+ {
101
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openlife-chat-tui-empty-'));
102
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'x', version: '1.0.0' }), 'utf-8');
103
+ try {
104
+ const stats = (0, ChatTui_1.buildInventoryStats)(root, 's');
105
+ check('empty .catalog yields zero counts', stats.agents === 0 && stats.squads === 0 && stats.skills === 0 && stats.mcps === 0);
106
+ check('missing models.json yields unknown', stats.model === 'unknown');
107
+ }
108
+ finally {
109
+ fs.rmSync(root, { recursive: true, force: true });
110
+ }
111
+ }
112
+ if (failed > 0) {
113
+ console.error(`\n${failed} check(s) failed.`);
114
+ process.exit(1);
115
+ }
116
+ console.log('\nTEST_CHAT_TUI_OK');
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ // src/test_config_tui.ts
3
+ // Tests for ConfigTui persistence helpers — focused on file-level effects
4
+ // (the interactive surface is exercised separately via the TUI).
5
+ //
6
+ // What we verify:
7
+ // - saveApiKeysToEnv writes to .env, masks correctly via existing helper
8
+ // - saveTelegramConfig persists token + chat id
9
+ // - validateTelegramToken catches format errors before going to the network
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const os = __importStar(require("os"));
47
+ const InstallModules_1 = require("./cli/InstallModules");
48
+ let failed = 0;
49
+ function check(label, condition, detail) {
50
+ if (condition) {
51
+ console.log(`✅ ${label}`);
52
+ }
53
+ else {
54
+ console.error(`❌ ${label}${detail ? ` — ${detail}` : ''}`);
55
+ failed++;
56
+ }
57
+ }
58
+ // 1. saveApiKeysToEnv idempotent (writing same key twice does not duplicate)
59
+ {
60
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openlife-cfg-keys-'));
61
+ try {
62
+ (0, InstallModules_1.saveApiKeysToEnv)(root, { openai: 'sk-aaa' });
63
+ (0, InstallModules_1.saveApiKeysToEnv)(root, { openai: 'sk-bbb' });
64
+ const env = fs.readFileSync(path.join(root, '.env'), 'utf-8');
65
+ const matches = env.match(/^OPENAI_API_KEY=/gm) || [];
66
+ check('OPENAI_API_KEY appears exactly once after two saves', matches.length === 1, `count=${matches.length}`);
67
+ check('OPENAI_API_KEY reflects latest value', env.includes('OPENAI_API_KEY=sk-bbb'));
68
+ }
69
+ finally {
70
+ fs.rmSync(root, { recursive: true, force: true });
71
+ }
72
+ }
73
+ // 2. saveApiKeysToEnv preserves unrelated lines
74
+ {
75
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openlife-cfg-keys-mix-'));
76
+ try {
77
+ fs.writeFileSync(path.join(root, '.env'), 'SOME_OTHER=preserve_me\nFOO=bar\n', 'utf-8');
78
+ (0, InstallModules_1.saveApiKeysToEnv)(root, { gemini: 'gem-xyz' });
79
+ const env = fs.readFileSync(path.join(root, '.env'), 'utf-8');
80
+ check('preserved unrelated SOME_OTHER', env.includes('SOME_OTHER=preserve_me'));
81
+ check('preserved unrelated FOO', env.includes('FOO=bar'));
82
+ check('added new GEMINI_API_KEY', env.includes('GEMINI_API_KEY=gem-xyz'));
83
+ }
84
+ finally {
85
+ fs.rmSync(root, { recursive: true, force: true });
86
+ }
87
+ }
88
+ // 3. saveTelegramConfig sets token + chat id idempotently
89
+ {
90
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openlife-cfg-tg-'));
91
+ try {
92
+ (0, InstallModules_1.saveTelegramConfig)(root, '123456789:AAEEABCDEFGHIJKLMNOPQRSTUVWXYZ0123', '999');
93
+ (0, InstallModules_1.saveTelegramConfig)(root, '999999999:ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ', '999');
94
+ const env = fs.readFileSync(path.join(root, '.env'), 'utf-8');
95
+ const tokenCount = (env.match(/^TELEGRAM_BOT_TOKEN=/gm) || []).length;
96
+ check('TELEGRAM_BOT_TOKEN appears exactly once', tokenCount === 1, `count=${tokenCount}`);
97
+ check('TELEGRAM_BOT_TOKEN updated to second value', env.includes('TELEGRAM_BOT_TOKEN=999999999:'));
98
+ }
99
+ finally {
100
+ fs.rmSync(root, { recursive: true, force: true });
101
+ }
102
+ }
103
+ // 4. validateTelegramToken rejects bad format without going to network
104
+ {
105
+ const r1 = (0, InstallModules_1.validateTelegramToken)(undefined);
106
+ check('validate rejects undefined token', r1.ok === false);
107
+ const r2 = (0, InstallModules_1.validateTelegramToken)('not-a-token');
108
+ check('validate rejects malformed token', r2.ok === false && r2.detail.includes('inválido'));
109
+ const r3 = (0, InstallModules_1.validateTelegramToken)('123:short');
110
+ check('validate rejects token with short secret', r3.ok === false);
111
+ }
112
+ if (failed > 0) {
113
+ console.error(`\n${failed} check(s) failed.`);
114
+ process.exit(1);
115
+ }
116
+ console.log('\nTEST_CONFIG_TUI_OK');
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ // src/test_matrix_rain.ts
3
+ // Smoke tests for the Matrix theme + rain renderer.
4
+ // - banner renders deterministic plain-text shape under stripAnsi
5
+ // - inventory panel borders align (top/bottom widths match)
6
+ // - rain.start() in non-TTY returns no-op handle (no interval leak)
7
+ // - sampleFrame() produces requested dimensions
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const ChatBanner_1 = require("./cli/ChatBanner");
10
+ const MatrixRain_1 = require("./cli/MatrixRain");
11
+ const MatrixTheme_1 = require("./cli/MatrixTheme");
12
+ let failed = 0;
13
+ function check(label, condition, detail) {
14
+ if (condition) {
15
+ console.log(`✅ ${label}`);
16
+ }
17
+ else {
18
+ console.error(`❌ ${label}${detail ? ` — ${detail}` : ''}`);
19
+ failed++;
20
+ }
21
+ }
22
+ // 1. Banner contains the OPENLIFE letters
23
+ {
24
+ const plain = (0, MatrixTheme_1.stripAnsi)((0, ChatBanner_1.openlifeBanner)());
25
+ check('banner contains OPENLIFE letters (5 rows of █)', plain.includes('██████ ██████ ███████ ███ ██ ██ ██ ███████ ███████'));
26
+ check('banner has horizon line', plain.includes('▁'));
27
+ }
28
+ // 2. Inventory panel borders align
29
+ {
30
+ const stats = { version: 'v1.7.12', model: 'openai-cli/gpt-5.5', sessionId: 'TEST_SESSION', cwd: '/tmp', agents: 1, squads: 2, skills: 3, mcps: 4 };
31
+ const plain = (0, MatrixTheme_1.stripAnsi)((0, ChatBanner_1.inventoryPanel)(stats));
32
+ const lines = plain.split('\n').filter(Boolean);
33
+ const top = lines[0];
34
+ const bot = lines[lines.length - 1];
35
+ check('panel top and bottom widths match', top.length === bot.length, `top=${top.length} bot=${bot.length}`);
36
+ check('panel shows model', plain.includes('openai-cli/gpt-5.5'));
37
+ check('panel shows catalog counts', plain.includes('agents 1') && plain.includes('skills 3'));
38
+ }
39
+ // 3. bootOutput composes banner + panel + welcome
40
+ {
41
+ const stats = { version: 'v1.7.12', model: 'm', sessionId: 's', cwd: '/c', agents: 0, squads: 0, skills: 0, mcps: 0 };
42
+ const plain = (0, MatrixTheme_1.stripAnsi)((0, ChatBanner_1.bootOutput)(stats));
43
+ check('bootOutput contains banner OPENLIFE marker', plain.includes('██████'));
44
+ check('bootOutput contains welcome line', plain.includes('Welcome to OpenLife'));
45
+ check('bootOutput contains divider line', plain.includes('━'));
46
+ }
47
+ // 4. Rain in non-TTY env returns no-op handle
48
+ {
49
+ // We're running under node test runner — stdout.isTTY is false.
50
+ const handle = (0, MatrixRain_1.startRain)({ row: 1, col: 1, width: 10, height: 4 });
51
+ check('rain.isRunning() returns false in non-TTY', handle.isRunning() === false);
52
+ // stop() must be safe to call without state
53
+ handle.stop();
54
+ check('rain.stop() in non-TTY is a no-op (no throw)', true);
55
+ }
56
+ // 5. sampleFrame produces requested dimensions
57
+ {
58
+ const frame = (0, MatrixRain_1.sampleFrame)(10, 5);
59
+ const rows = frame.split('\n');
60
+ check('sampleFrame produces correct number of rows', rows.length === 5);
61
+ // Each row is at most 10 visible chars (after stripAnsi)
62
+ const plainRowLengths = rows.map(r => (0, MatrixTheme_1.stripAnsi)(r).length);
63
+ check('sampleFrame rows are at most width chars', plainRowLengths.every(n => n <= 10), `lengths=${plainRowLengths.join(',')}`);
64
+ }
65
+ if (failed > 0) {
66
+ console.error(`\n${failed} check(s) failed.`);
67
+ process.exit(1);
68
+ }
69
+ console.log('\nTEST_MATRIX_RAIN_OK');