@openlife/cli 1.7.10 → 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.
@@ -229,78 +229,73 @@ class Gatekeeper {
229
229
  return direct;
230
230
  }
231
231
  let finalResponse = "";
232
- const recoveredContext = await this.conversationMemory.recoverContext(userId, userInput);
233
232
  switch (task.intent) {
234
- case IntentClassifier_1.TaskIntent.KNOWLEDGE_RETRIEVAL:
235
- finalResponse = await this.handleFastPath(userInput, recoveredContext.recentHistory, recoveredContext.memorySnippet);
233
+ case IntentClassifier_1.TaskIntent.KNOWLEDGE_RETRIEVAL: {
234
+ const useMemoryContext = this.shouldUseMemoryContext(userInput);
235
+ const recoveredContext = useMemoryContext
236
+ ? await this.conversationMemory.recoverContext(userId, userInput)
237
+ : { recentHistory: this.getRecentSessionHistory(userId), memorySnippet: '', autoRecovered: false };
238
+ finalResponse = await this.handleFastPath(userInput, recoveredContext.recentHistory, recoveredContext.memorySnippet, useMemoryContext);
236
239
  break;
240
+ }
237
241
  case IntentClassifier_1.TaskIntent.ENGINEERING_BUILD:
238
- case IntentClassifier_1.TaskIntent.RESEARCH_ANALYSIS:
242
+ case IntentClassifier_1.TaskIntent.RESEARCH_ANALYSIS: {
243
+ const recoveredContext = await this.conversationMemory.recoverContext(userId, userInput);
239
244
  finalResponse = await this.handleComplexPath(userInput, task, recoveredContext.recentHistory, userId, options?.mode);
240
245
  break;
246
+ }
241
247
  default:
242
- finalResponse = "Intenção desconhecida. Proteção Mythos ativada.";
248
+ finalResponse = await this.handleFastPath(userInput, this.getRecentSessionHistory(userId), '', false);
243
249
  }
244
250
  this.sessionManager.addMessage(userId, 'agent', finalResponse);
245
251
  return finalResponse;
246
252
  }
247
- async handleFastPath(input, recentHistory, recoveredMemory) {
248
- console.log("[FAST PATH] Acionando Omni-Memory e recuperação automática de contexto...");
249
- const searchResult = recoveredMemory || await this.memory.search(input);
253
+ getRecentSessionHistory(userId) {
254
+ const session = this.sessionManager.getSession(userId);
255
+ return session.history
256
+ .slice(-4)
257
+ .map((h) => `${h.role}: ${h.content}`)
258
+ .join('\n');
259
+ }
260
+ shouldUseMemoryContext(input) {
261
+ const normalized = input.toLowerCase();
262
+ if (String(process.env.OPENLIFE_FASTPATH_MEMORY || 'auto').toLowerCase() === 'always')
263
+ return true;
264
+ if (String(process.env.OPENLIFE_FASTPATH_MEMORY || 'auto').toLowerCase() === 'off')
265
+ return false;
266
+ if (input.length > 240)
267
+ return true;
268
+ return /(lembra|memória|memoria|projeto|openlife|lara|aiobuilder|catalog|catálogo|railway|github|deploy|repo|arquivo|documento|histórico|historico)/i.test(normalized);
269
+ }
270
+ async handleFastPath(input, recentHistory, recoveredMemory, useMemoryContext) {
271
+ console.log(useMemoryContext
272
+ ? "[FAST PATH] GPT-5.5 com contexto de memória seletivo..."
273
+ : "[FAST PATH] GPT-5.5 modo compacto sem OmniMemory...");
274
+ const searchResult = useMemoryContext ? (recoveredMemory || await this.memory.search(input)) : '';
250
275
  const systemPrompt = `
251
- Sua Alma (SOUL):
252
- ${this.systemSoul}
253
-
254
- Sua Identidade (IDENTITY):
255
- ${this.systemIdentity}
276
+ Identidade:
277
+ ${this.systemIdentity.substring(0, 900)}
256
278
 
257
- Contexto da OmniMemory (Arquivos):
258
- ${searchResult.substring(0, 1500)}
279
+ ${searchResult ? `Contexto relevante:\n${searchResult.substring(0, 900)}\n` : ''}
280
+ Histórico recente:
281
+ ${recentHistory.slice(-1200)}
259
282
 
260
- [Histórico Recente da Sessão (Save State)]:
261
- ${recentHistory.slice(-3000)}
262
-
263
- Instruções Adicionais:
264
- - IMPORTANTE: NÃO comece sua resposta com saudações ("Olá", "Oi") se o usuário já estiver conversando com você no Histórico Recente. Continue a conversa naturalmente.
265
- - Você tem memória fluida. Baseie-se fortemente no [Histórico Recente da Sessão] para manter o contexto vivo. Responda DIRETAMENTE à última mensagem.
266
- - Use a OmniMemory para informações profundas de projetos (como LARA, AIOBUILDER).
267
- - Se for apenas um 'oi' inicial, saúde amigavelmente e pergunte como pode ajudar.
268
- - NUNCA mencione tool calls, comandos internos, ou frases como "vou executar comando". Apenas responda naturalmente com o resultado final.
283
+ Instruções:
284
+ - Responda diretamente à última mensagem, em português natural.
285
+ - Seja rápido e conciso por padrão.
286
+ - Não mencione comandos internos, tool calls ou detalhes de execução.
287
+ - Se precisar de contexto profundo, diga objetivamente o que falta.
269
288
  `;
270
- const fastTimeoutMs = Number(process.env.OPENLIFE_FASTPATH_TIMEOUT_MS || 90000);
271
- const degradedPrompt = `${this.systemIdentity}\n\nResponda de forma direta e curta, sem contexto extra.`;
272
289
  try {
273
- return await Promise.race([
274
- this.brain.think(systemPrompt, input),
275
- new Promise((_, reject) => setTimeout(() => reject(new Error(`FAST_PATH_TIMEOUT_${fastTimeoutMs}`)), fastTimeoutMs))
276
- ]);
290
+ return await this.brain.thinkFast(systemPrompt, input);
277
291
  }
278
292
  catch (err) {
279
293
  const code = (err instanceof Error ? err.message : String(err)) || 'FAST_PATH_FAILURE';
280
294
  console.error(`[FAST PATH ERROR] ${code}`);
281
- if (String(code).startsWith('FAST_PATH_TIMEOUT_')) {
282
- try {
283
- const retryTimeoutMs = Number(process.env.OPENLIFE_FASTPATH_RETRY_TIMEOUT_MS || 30000);
284
- return await Promise.race([
285
- this.brain.think(degradedPrompt, input),
286
- new Promise((_, reject) => setTimeout(() => reject(new Error(`FAST_PATH_RETRY_TIMEOUT_${retryTimeoutMs}`)), retryTimeoutMs))
287
- ]);
288
- }
289
- catch (retryErr) {
290
- const retryCode = (retryErr instanceof Error ? retryErr.message : String(retryErr)) || 'FAST_PATH_RETRY_FAILURE';
291
- return [
292
- 'OPENLIFE ONLINE ✅',
293
- 'Modo resiliente: primeira rota estourou tempo, tentei fallback enxuto.',
294
- `Motivo técnico: ${retryCode}`,
295
- 'Ação: valide cadeia de modelos com `openlife models status` e aumente timeout com OPENLIFE_FASTPATH_TIMEOUT_MS.'
296
- ].join('\n');
297
- }
298
- }
299
295
  return [
300
- 'OPENLIFE ONLINE ',
301
- 'Modo seguro ativado: não vou inventar resposta quando o executor falhar.',
296
+ 'Executor GPT-5.5 indisponível ou lento demais neste momento.',
302
297
  `Motivo técnico: ${code}`,
303
- 'Ação: tente novamente em alguns segundos. Se persistir, rode `openlife governance audit` e `openlife models status`.'
298
+ 'Ação: verifique OAuth do Codex e tente novamente.'
304
299
  ].join('\n');
305
300
  }
306
301
  }
@@ -415,16 +415,123 @@ class Gateway {
415
415
  }
416
416
  }
417
417
  async processTextForTest(userId, text) {
418
+ const detailed = await this.processTextForTestDetailed(userId, text);
419
+ return detailed.finalText;
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) {
418
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
+ }
485
+ /**
486
+ * Test seam that captures the full ACK + final timeline for the fast-ACK
487
+ * pattern. Used by the regression test to assert the placeholder reply
488
+ * lands within the ACK budget even when downstream model processing is
489
+ * slow. Returns:
490
+ * - finalText: the user-visible final answer (edited ACK or last reply)
491
+ * - events: ordered timeline (kind = 'reply' | 'edit' | 'voice')
492
+ * - ackMs: ms until the first reply landed (null if none was sent)
493
+ * - finalMs: ms until the last event landed
494
+ */
495
+ async processTextForTestDetailed(userId, text) {
496
+ const startTs = Date.now();
497
+ const events = [];
498
+ let ackMs = null;
499
+ let lastReplyText = '';
500
+ let lastEditText = null;
501
+ let messageIdCounter = 0;
502
+ let lastReplyMessageId = 0;
419
503
  const testCtx = {
420
504
  sendChatAction: async (_action) => { },
421
- reply: async (message) => { finalReply = message; },
505
+ reply: async (message) => {
506
+ const tsMs = Date.now() - startTs;
507
+ events.push({ kind: 'reply', text: message, tsMs });
508
+ if (ackMs === null)
509
+ ackMs = tsMs;
510
+ lastReplyText = message;
511
+ lastReplyMessageId = ++messageIdCounter;
512
+ return { message_id: lastReplyMessageId };
513
+ },
422
514
  sendVoice: async (_source, options) => {
423
- finalReply = options?.caption || '[voice_sent]';
515
+ const caption = options?.caption || '[voice_sent]';
516
+ const tsMs = Date.now() - startTs;
517
+ events.push({ kind: 'voice', text: caption, tsMs });
518
+ lastReplyText = caption;
519
+ return { message_id: ++messageIdCounter };
520
+ },
521
+ chat: { id: userId },
522
+ telegram: {
523
+ editMessageText: async (_chatId, _messageId, _inlineId, edited) => {
524
+ const tsMs = Date.now() - startTs;
525
+ events.push({ kind: 'edit', text: edited, tsMs });
526
+ lastEditText = edited;
527
+ return true;
528
+ }
424
529
  }
425
530
  };
426
531
  await this.processInput(testCtx, userId, text);
427
- return finalReply;
532
+ const finalText = lastEditText ?? lastReplyText;
533
+ const finalMs = events.length ? events[events.length - 1].tsMs : 0;
534
+ return { finalText, events, ackMs, finalMs };
428
535
  }
429
536
  async processImageForTest(userId, imagePath, caption = 'Descreva esta imagem.') {
430
537
  (0, ToolsetGuard_1.assertToolsetAllowed)('vision', 'Gateway.processImageForTest');
@@ -442,6 +549,42 @@ class Gateway {
442
549
  await this.safeReply(ctx, 'Acesso negado por policy de segurança deste bot.');
443
550
  return;
444
551
  }
552
+ // Fast ACK pattern: fire a placeholder reply in parallel with the actual
553
+ // classifier+gatekeeper work. When the work completes, edit the ACK with
554
+ // the real answer (Telegraf editMessageText). Falls back to a fresh
555
+ // reply when the surface does not expose `chat`/`telegram` (webhook mock
556
+ // and similar internal callers).
557
+ const ackText = '🔎 Recebido — processando…';
558
+ let ackMessageId = null;
559
+ const ackPromise = (async () => {
560
+ try {
561
+ const result = await ctx.reply(ackText);
562
+ if (result && typeof result === 'object' && 'message_id' in result) {
563
+ const mid = result.message_id;
564
+ if (typeof mid === 'number')
565
+ ackMessageId = mid;
566
+ }
567
+ }
568
+ catch (ackErr) {
569
+ console.error('[GATEWAY] Falha ao enviar ACK rápido:', ackErr);
570
+ }
571
+ })();
572
+ const sendFinal = async (finalText) => {
573
+ await ackPromise;
574
+ if (ackMessageId !== null &&
575
+ ctx.chat &&
576
+ ctx.telegram &&
577
+ typeof ctx.telegram.editMessageText === 'function') {
578
+ try {
579
+ await ctx.telegram.editMessageText(ctx.chat.id, ackMessageId, undefined, finalText);
580
+ return;
581
+ }
582
+ catch (editErr) {
583
+ console.error('[GATEWAY] editMessageText falhou, enviando nova mensagem:', editErr);
584
+ }
585
+ }
586
+ await this.safeReply(ctx, finalText);
587
+ };
445
588
  const stopTyping = this.startTypingIndicator(ctx);
446
589
  const trace = [];
447
590
  try {
@@ -459,11 +602,11 @@ class Gateway {
459
602
  }
460
603
  catch (ttsError) {
461
604
  console.log("[GATEWAY] Fallback para texto (Erro no TTS).", ttsError);
462
- await this.safeReply(ctx, finalResponse);
605
+ await sendFinal(finalResponse);
463
606
  }
464
607
  }
465
608
  else {
466
- await this.safeReply(ctx, finalResponse);
609
+ await sendFinal(finalResponse);
467
610
  }
468
611
  }
469
612
  catch (err) {
@@ -471,7 +614,7 @@ class Gateway {
471
614
  const errMsg = this.reasoningMode !== 'off'
472
615
  ? `${trace.join('\n')}\n\n❌ error: "${err instanceof Error ? err.message : String(err)}"\n\nErro no córtex ao processar a solicitação.`
473
616
  : "Erro no córtex ao processar a solicitação.";
474
- await this.safeReply(ctx, errMsg);
617
+ await sendFinal(errMsg);
475
618
  }
476
619
  finally {
477
620
  stopTyping();
@@ -46,11 +46,14 @@ class RuntimePolicy {
46
46
  this.runtimeHealth = new RuntimeHealthMonitor_1.RuntimeHealthMonitor();
47
47
  }
48
48
  decide(intent, explicitExecutors = []) {
49
+ const strictModelExecutors = this.getStrictModelExecutors();
49
50
  const base = explicitExecutors.length
50
51
  ? explicitExecutors
51
- : intent === 'RESEARCH_ANALYSIS'
52
- ? ['gemini', 'claude', 'codex']
53
- : ['codex', 'claude', 'gemini'];
52
+ : strictModelExecutors.length
53
+ ? strictModelExecutors
54
+ : intent === 'RESEARCH_ANALYSIS'
55
+ ? ['gemini', 'claude', 'codex']
56
+ : ['codex', 'claude', 'gemini'];
54
57
  const allowedRaw = (process.env.OPENLIFE_ALLOWED_LLM_EXECUTORS || '').trim().toLowerCase();
55
58
  const allowed = new Set();
56
59
  if (allowedRaw) {
@@ -76,6 +79,28 @@ class RuntimePolicy {
76
79
  : 'Todos os executores da cadeia configurada estão indisponíveis no momento.'
77
80
  };
78
81
  }
82
+ getStrictModelExecutors() {
83
+ try {
84
+ const modelsPath = path.join(process.cwd(), 'models.json');
85
+ if (!fs.existsSync(modelsPath))
86
+ return [];
87
+ const cfg = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
88
+ const chain = [cfg.primary, ...(cfg.fallbacks || [])].filter(Boolean);
89
+ if (chain.length !== 1)
90
+ return [];
91
+ const provider = chain[0].provider || '';
92
+ if (provider === 'openai-cli')
93
+ return ['codex'];
94
+ if (provider === 'gemini-cli' || provider === 'gemini-api')
95
+ return ['gemini'];
96
+ if (provider === 'anthropic-api')
97
+ return ['claude'];
98
+ return [];
99
+ }
100
+ catch {
101
+ return [];
102
+ }
103
+ }
79
104
  recordResult(executor, ok, reason) {
80
105
  this.executorHealth.set(executor, ok, reason);
81
106
  if (ok) {
@@ -171,17 +171,21 @@ class SquadCreator {
171
171
  if (frontmatter.id && frontmatter.id !== squadId) {
172
172
  errors.push(`frontmatter id '${frontmatter.id}' does not match directory '${squadId}'`);
173
173
  }
174
- // Component files referenced in SQUAD.md should exist
175
- const componentRefs = this.parseComponentRefs(content);
176
- for (const refPath of componentRefs.agents) {
177
- const full = path.join(squadDir, 'agents', refPath);
178
- if (!fs.existsSync(full))
179
- warnings.push(`referenced agent not found: agents/${refPath}`);
180
- }
181
- for (const refPath of componentRefs.tasks) {
182
- const full = path.join(squadDir, 'tasks', refPath);
183
- if (!fs.existsSync(full))
184
- warnings.push(`referenced task not found: tasks/${refPath}`);
174
+ // Imported squads (lara-squads-import, obsidian-mirror, etc.) reference
175
+ // agents/tasks from the global .catalog/{agents,skills}/ pool, not squad-local.
176
+ const isImported = typeof frontmatter.source === 'string' && frontmatter.source.endsWith('-import');
177
+ if (!isImported) {
178
+ const componentRefs = this.parseComponentRefs(content);
179
+ for (const refPath of componentRefs.agents) {
180
+ const full = path.join(squadDir, 'agents', refPath);
181
+ if (!fs.existsSync(full))
182
+ warnings.push(`referenced agent not found: agents/${refPath}`);
183
+ }
184
+ for (const refPath of componentRefs.tasks) {
185
+ const full = path.join(squadDir, 'tasks', refPath);
186
+ if (!fs.existsSync(full))
187
+ warnings.push(`referenced task not found: tasks/${refPath}`);
188
+ }
185
189
  }
186
190
  return { ok: errors.length === 0, squadId, errors, warnings };
187
191
  }
@@ -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');