@openlife/cli 1.7.11 → 1.7.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +57 -12
- 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,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
|
-
|
|
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
|
-
|
|
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');
|
|
@@ -282,7 +288,46 @@ class Brain {
|
|
|
282
288
|
const commandArgs = process.platform === 'win32'
|
|
283
289
|
? args
|
|
284
290
|
: ['-k', '2s', `${timeoutSeconds}s`, 'codex', ...args];
|
|
285
|
-
|
|
291
|
+
// Codex CLI reads stdin when invoked without a TTY. The promisified
|
|
292
|
+
// execFile doesn't expose stdio, so it keeps the child's stdin
|
|
293
|
+
// attached to our (open) pipe — codex then waits forever for input
|
|
294
|
+
// and our outer timeout fires. Use spawn with `stdio: ['ignore', ...]`
|
|
295
|
+
// to close stdin explicitly. Fixes ~30s phantom timeouts when the
|
|
296
|
+
// daemon (non-TTY) calls codex.
|
|
297
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
298
|
+
const child = child_process.spawn(command, commandArgs, {
|
|
299
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
300
|
+
});
|
|
301
|
+
let stdoutBuf = '';
|
|
302
|
+
let stderrBuf = '';
|
|
303
|
+
let killed = false;
|
|
304
|
+
const watchdog = setTimeout(() => {
|
|
305
|
+
killed = true;
|
|
306
|
+
try {
|
|
307
|
+
child.kill('SIGKILL');
|
|
308
|
+
}
|
|
309
|
+
catch { /* ignore */ }
|
|
310
|
+
}, timeoutMs + 3000);
|
|
311
|
+
child.stdout.on('data', (d) => { stdoutBuf += String(d); });
|
|
312
|
+
child.stderr.on('data', (d) => { stderrBuf += String(d); });
|
|
313
|
+
child.on('error', (err) => { clearTimeout(watchdog); reject(err); });
|
|
314
|
+
child.on('close', (code, signal) => {
|
|
315
|
+
clearTimeout(watchdog);
|
|
316
|
+
if (killed || code === 124 || (signal && killed)) {
|
|
317
|
+
const e = new Error('ETIMEDOUT');
|
|
318
|
+
e.code = 'ETIMEDOUT';
|
|
319
|
+
reject(e);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (code !== 0) {
|
|
323
|
+
const e = new Error(`codex exited with code ${code}: ${stderrBuf.slice(-300)}`);
|
|
324
|
+
e.code = String(code);
|
|
325
|
+
reject(e);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
resolve(stdoutBuf);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
286
331
|
const lastMessage = fs.existsSync(outputFile) ? fs.readFileSync(outputFile, 'utf-8').trim() : '';
|
|
287
332
|
try {
|
|
288
333
|
fs.unlinkSync(outputFile);
|
|
@@ -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');
|