@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.
- 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 +66 -7
- package/dist/memory/MemoryProviderRegistry.js +5 -1
- package/dist/memory/MempalaceProvider.js +53 -5
- package/dist/memory/OmniMemory.js +9 -5
- package/dist/orchestrator/Brain.js +126 -13
- package/dist/orchestrator/Gatekeeper.js +46 -51
- package/dist/orchestrator/Gateway.js +149 -6
- package/dist/orchestrator/RuntimePolicy.js +28 -3
- package/dist/orchestrator/SquadCreator.js +15 -11
- package/dist/test_chat_tui.js +116 -0
- package/dist/test_config_tui.js +116 -0
- package/dist/test_gateway_fast_ack.js +66 -0
- package/dist/test_matrix_rain.js +69 -0
- package/dist/test_no_automatic_messages.js +64 -0
- package/package.json +9 -3
- 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
|
@@ -537,11 +537,10 @@ program
|
|
|
537
537
|
.action(async (mensagemArgs, options) => {
|
|
538
538
|
const mensagem = mensagemArgs.join(' ');
|
|
539
539
|
console.log(`\n> Você: "${mensagem}"\n`);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
540
|
+
// Hermes parity: `ask` must send the user's message to the agent runtime.
|
|
541
|
+
// Conversational shortcuts such as design import/generate belong to explicit
|
|
542
|
+
// subcommands, not to the main chat path, otherwise OpenLife appears to
|
|
543
|
+
// "answer" without invoking the configured GPT-5.5 executor.
|
|
545
544
|
const timeoutMs = Number(process.env.OPENLIFE_ASK_TIMEOUT_MS || 90000);
|
|
546
545
|
const classifier = new IntentClassifier_1.IntentClassifier();
|
|
547
546
|
const { Gatekeeper } = require('./orchestrator/Gatekeeper');
|
|
@@ -1397,7 +1396,28 @@ program
|
|
|
1397
1396
|
program
|
|
1398
1397
|
.command('update')
|
|
1399
1398
|
.description('Atualiza dependências, recompila o core e valida status do sistema (modo dev)')
|
|
1400
|
-
.
|
|
1399
|
+
.option('--global', 'Self-update via npm global install (npm i -g @openlife/cli@latest)')
|
|
1400
|
+
.action(async (opts) => {
|
|
1401
|
+
if (opts.global) {
|
|
1402
|
+
const { execFileSync } = require('child_process');
|
|
1403
|
+
try {
|
|
1404
|
+
const current = require('../package.json').version;
|
|
1405
|
+
const latest = String(execFileSync('npm', ['view', '@openlife/cli', 'version'], { encoding: 'utf-8' })).trim();
|
|
1406
|
+
if (latest === current) {
|
|
1407
|
+
console.log(JSON.stringify({ ok: true, status: 'up-to-date', version: current }));
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
console.log(`🔄 Self-update ${current} → ${latest}`);
|
|
1411
|
+
execFileSync('npm', ['install', '-g', '@openlife/cli@latest'], { stdio: 'inherit' });
|
|
1412
|
+
const after = String(execFileSync('openlife', ['--version'], { encoding: 'utf-8' })).trim();
|
|
1413
|
+
console.log(JSON.stringify({ ok: true, from: current, to: after }));
|
|
1414
|
+
}
|
|
1415
|
+
catch (e) {
|
|
1416
|
+
console.error(JSON.stringify({ ok: false, error: 'self_update_failed', detail: errMsg(e) }));
|
|
1417
|
+
process.exitCode = 1;
|
|
1418
|
+
}
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1401
1421
|
console.log('🔄 OPEN-LIFE update: npm install + build + system status');
|
|
1402
1422
|
try {
|
|
1403
1423
|
const { stdout, stderr } = await exec('npm install && npm run build && node bin/openlife.js system status', { cwd: process.cwd(), maxBuffer: 1024 * 1024 * 10 });
|
|
@@ -2805,4 +2825,43 @@ profileCmd.command('import <file>').description('Import a profile from a JSON fi
|
|
|
2805
2825
|
process.exitCode = 1;
|
|
2806
2826
|
}
|
|
2807
2827
|
});
|
|
2808
|
-
|
|
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
|
+
}
|
|
@@ -11,11 +11,15 @@ class MemoryProviderRegistry {
|
|
|
11
11
|
constructor() {
|
|
12
12
|
this.providers = {
|
|
13
13
|
local: new LocalMemoryProvider_1.LocalMemoryProvider(),
|
|
14
|
-
mempalace: new MempalaceProvider_1.MempalaceProvider(),
|
|
15
14
|
mem0: new Mem0Provider_1.Mem0Provider(),
|
|
16
15
|
'zep-graphiti': new ZepGraphitiProvider_1.ZepGraphitiProvider(),
|
|
17
16
|
'redis-ams': new RedisAgentMemoryProvider_1.RedisAgentMemoryProvider()
|
|
18
17
|
};
|
|
18
|
+
// MemPalace é opcional: registra apenas se o binário Python existir.
|
|
19
|
+
// Evita shell-out por turno em runtimes (Railway) que não têm mempalace instalado.
|
|
20
|
+
if (MempalaceProvider_1.MempalaceProvider.isAvailable()) {
|
|
21
|
+
this.providers.mempalace = new MempalaceProvider_1.MempalaceProvider();
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
24
|
get(name) {
|
|
21
25
|
return this.providers[name] || null;
|
|
@@ -35,21 +35,69 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MempalaceProvider = void 0;
|
|
37
37
|
const child_process = __importStar(require("child_process"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
38
41
|
const util_1 = require("util");
|
|
39
|
-
const
|
|
42
|
+
const execFile = (0, util_1.promisify)(child_process.execFile);
|
|
43
|
+
const DEFAULT_BIN = '~/.venvs/mempalace/bin/mempalace';
|
|
44
|
+
function expandHome(p) {
|
|
45
|
+
if (!p)
|
|
46
|
+
return p;
|
|
47
|
+
if (p === '~')
|
|
48
|
+
return os.homedir();
|
|
49
|
+
if (p.startsWith('~/'))
|
|
50
|
+
return path.join(os.homedir(), p.slice(2));
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* MemPalace é uma integração OPCIONAL. Em produção (Railway) o binário Python
|
|
55
|
+
* normalmente não existe; tentar shell-out adiciona latência e ruído de log a
|
|
56
|
+
* cada turno de conversa. `isAvailable()` resolve o path uma única vez e
|
|
57
|
+
* retorna false silenciosamente quando o binário não está instalado, para que
|
|
58
|
+
* o MemoryProviderRegistry possa pular o registro sem custo.
|
|
59
|
+
*/
|
|
40
60
|
class MempalaceProvider {
|
|
41
61
|
mempalacePath;
|
|
42
|
-
|
|
62
|
+
static cachedAvailability = null;
|
|
63
|
+
static cachedPath = null;
|
|
64
|
+
constructor(mempalacePath = process.env.MEMPALACE_BIN_PATH || DEFAULT_BIN) {
|
|
43
65
|
this.mempalacePath = mempalacePath;
|
|
44
66
|
}
|
|
45
67
|
name() {
|
|
46
68
|
return 'mempalace';
|
|
47
69
|
}
|
|
70
|
+
static isAvailable(rawPath) {
|
|
71
|
+
const configured = (rawPath ?? process.env.MEMPALACE_BIN_PATH ?? DEFAULT_BIN).trim();
|
|
72
|
+
if (configured === '' || configured.toLowerCase() === 'off' || configured.toLowerCase() === 'disabled') {
|
|
73
|
+
MempalaceProvider.cachedAvailability = false;
|
|
74
|
+
MempalaceProvider.cachedPath = null;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (MempalaceProvider.cachedAvailability !== null && MempalaceProvider.cachedPath === configured) {
|
|
78
|
+
return MempalaceProvider.cachedAvailability;
|
|
79
|
+
}
|
|
80
|
+
const resolved = expandHome(configured);
|
|
81
|
+
let ok = false;
|
|
82
|
+
try {
|
|
83
|
+
ok = fs.existsSync(resolved) && fs.statSync(resolved).isFile();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
ok = false;
|
|
87
|
+
}
|
|
88
|
+
MempalaceProvider.cachedAvailability = ok;
|
|
89
|
+
MempalaceProvider.cachedPath = configured;
|
|
90
|
+
return ok;
|
|
91
|
+
}
|
|
92
|
+
static resetAvailabilityCache() {
|
|
93
|
+
MempalaceProvider.cachedAvailability = null;
|
|
94
|
+
MempalaceProvider.cachedPath = null;
|
|
95
|
+
}
|
|
48
96
|
async search(input) {
|
|
49
|
-
|
|
50
|
-
|
|
97
|
+
if (!MempalaceProvider.isAvailable(this.mempalacePath))
|
|
98
|
+
return [];
|
|
51
99
|
try {
|
|
52
|
-
const { stdout } = await
|
|
100
|
+
const { stdout } = await execFile(expandHome(this.mempalacePath), ['search', input.query], { timeout: 5000 });
|
|
53
101
|
if (!stdout.trim())
|
|
54
102
|
return [];
|
|
55
103
|
return [{
|
|
@@ -39,6 +39,7 @@ const util_1 = require("util");
|
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const LocalMemoryProvider_1 = require("./LocalMemoryProvider");
|
|
42
|
+
const MempalaceProvider_1 = require("./MempalaceProvider");
|
|
42
43
|
const MemoryOrchestrator_1 = require("./MemoryOrchestrator");
|
|
43
44
|
const ToolsetGuard_1 = require("../orchestrator/toolset/ToolsetGuard");
|
|
44
45
|
const exec = (0, util_1.promisify)(child_process.exec);
|
|
@@ -57,20 +58,23 @@ class OmniMemory {
|
|
|
57
58
|
if (hits.length) {
|
|
58
59
|
return hits.map(hit => `- [${hit.provider}] ${hit.record.summary || hit.record.content}`).join('\n');
|
|
59
60
|
}
|
|
61
|
+
if (!MempalaceProvider_1.MempalaceProvider.isAvailable(this.mempalacePath)) {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
60
64
|
console.log(`[OMNI-MEMORY] Buscando no MemPalace: "${query}"...`);
|
|
61
65
|
try {
|
|
62
66
|
const safeQuery = query.replace(/"/g, '\\"');
|
|
63
67
|
const command = `bash -c "${this.mempalacePath} search \\\"${safeQuery}\\\""`;
|
|
64
|
-
const
|
|
68
|
+
const timeoutMs = Number(process.env.OPENLIFE_MEMORY_SEARCH_TIMEOUT_MS || 1500);
|
|
69
|
+
const { stdout } = await exec(command, { timeout: timeoutMs });
|
|
65
70
|
if (!stdout.trim() || stdout.includes("No results found")) {
|
|
66
|
-
return "
|
|
71
|
+
return "";
|
|
67
72
|
}
|
|
68
73
|
const citation = `\n\n[RAG de Evidências: Buscado em ${new Date().toISOString()}]`;
|
|
69
74
|
return stdout + citation;
|
|
70
75
|
}
|
|
71
|
-
catch (
|
|
72
|
-
|
|
73
|
-
return "Busca na Omni-Memory concluída com alertas ou sem resultados diretos.";
|
|
76
|
+
catch (_error) {
|
|
77
|
+
return "";
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
async saveFact(fact, metadata, namespace) {
|
|
@@ -36,18 +36,43 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.Brain = void 0;
|
|
39
|
+
exports.Brain = exports.CodexTimeoutError = void 0;
|
|
40
40
|
const child_process = __importStar(require("child_process"));
|
|
41
41
|
const util_1 = require("util");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
42
44
|
const openai_1 = __importDefault(require("openai"));
|
|
43
45
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
44
46
|
const generative_ai_1 = require("@google/generative-ai");
|
|
45
47
|
const ModelManager_1 = require("./ModelManager");
|
|
46
48
|
const ToolsetGuard_1 = require("./toolset/ToolsetGuard");
|
|
47
49
|
const execFile = (0, util_1.promisify)(child_process.execFile);
|
|
48
|
-
const DEFAULT_MODEL_TIMEOUT_MS = Number(process.env.OPENLIFE_MODEL_TIMEOUT_MS ||
|
|
50
|
+
const DEFAULT_MODEL_TIMEOUT_MS = Number(process.env.OPENLIFE_MODEL_TIMEOUT_MS || 60000);
|
|
49
51
|
const REASONING_MODE = String(process.env.OPENLIFE_REASONING_MODE || 'errors').toLowerCase(); // off | errors | always
|
|
52
|
+
// Conversational fast path must fail fast so the Gateway can surface a
|
|
53
|
+
// timeout-aware reply to the user. Deep engineering missions keep the full
|
|
54
|
+
// 60s budget. Both are env-overridable.
|
|
55
|
+
function getCodexFastTimeoutMs() {
|
|
56
|
+
return Number(process.env.OPENLIFE_CODEX_FAST_TIMEOUT_MS || 30000);
|
|
57
|
+
}
|
|
58
|
+
function getCodexDeepTimeoutMs() {
|
|
59
|
+
return Number(process.env.OPENLIFE_CODEX_DEEP_TIMEOUT_MS || DEFAULT_MODEL_TIMEOUT_MS);
|
|
60
|
+
}
|
|
61
|
+
class CodexTimeoutError extends Error {
|
|
62
|
+
depth;
|
|
63
|
+
timeoutMs;
|
|
64
|
+
model;
|
|
65
|
+
constructor(depth, timeoutMs, model) {
|
|
66
|
+
super(`CODEX_TIMEOUT_${depth.toUpperCase()}_${timeoutMs}MS_${model || 'default'}`);
|
|
67
|
+
this.name = 'CodexTimeoutError';
|
|
68
|
+
this.depth = depth;
|
|
69
|
+
this.timeoutMs = timeoutMs;
|
|
70
|
+
this.model = model || 'default';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.CodexTimeoutError = CodexTimeoutError;
|
|
50
74
|
class Brain {
|
|
75
|
+
static codexQueue = Promise.resolve();
|
|
51
76
|
openai = null;
|
|
52
77
|
anthropic = null;
|
|
53
78
|
geminiApi = null;
|
|
@@ -87,21 +112,38 @@ class Brain {
|
|
|
87
112
|
return true;
|
|
88
113
|
return false;
|
|
89
114
|
}
|
|
90
|
-
async think(systemPrompt, userMessage) {
|
|
115
|
+
async think(systemPrompt, userMessage, opts) {
|
|
91
116
|
const config = this.modelManager.getModelConfig();
|
|
92
117
|
const modelsToTry = [config.primary, ...config.fallbacks].filter((model, index, arr) => arr.findIndex(m => m.raw === model.raw) === index);
|
|
118
|
+
return this.runModelChain(systemPrompt, userMessage, modelsToTry, 'deep', opts);
|
|
119
|
+
}
|
|
120
|
+
/** Fast conversational path. Keeps the configured model chain order intact. */
|
|
121
|
+
async thinkFast(systemPrompt, userMessage, opts) {
|
|
122
|
+
const config = this.modelManager.getModelConfig();
|
|
123
|
+
const models = [config.primary, ...config.fallbacks].filter((model, index, arr) => arr.findIndex(m => m.raw === model.raw) === index);
|
|
124
|
+
return this.runModelChain(systemPrompt, userMessage, models, 'fast', opts);
|
|
125
|
+
}
|
|
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(' → ')}`);
|
|
93
134
|
const failures = [];
|
|
94
135
|
for (let i = 0; i < modelsToTry.length; i++) {
|
|
95
136
|
const modelIdent = modelsToTry[i];
|
|
96
137
|
try {
|
|
97
138
|
console.log(`[BRAIN] Tentando modelo ${i === 0 ? 'PRIMÁRIO' : 'FALLBACK ' + i}: ${modelIdent.raw}...`);
|
|
139
|
+
emitReasoning(`🔎 trying ${i === 0 ? 'primary' : `fallback ${i}`}: ${modelIdent.raw}`);
|
|
98
140
|
let out = '';
|
|
99
141
|
switch (modelIdent.provider) {
|
|
100
142
|
case 'openai-api':
|
|
101
143
|
out = await this.thinkWithOpenAIAPI(systemPrompt, userMessage, modelIdent.name);
|
|
102
144
|
break;
|
|
103
145
|
case 'openai-cli':
|
|
104
|
-
out = await this.thinkWithOpenAICLI(systemPrompt, userMessage, modelIdent.name);
|
|
146
|
+
out = await this.thinkWithOpenAICLI(systemPrompt, userMessage, modelIdent.name, depth);
|
|
105
147
|
break;
|
|
106
148
|
case 'anthropic':
|
|
107
149
|
out = await this.thinkWithAnthropic(systemPrompt, userMessage, modelIdent.name);
|
|
@@ -134,15 +176,32 @@ class Brain {
|
|
|
134
176
|
const reason = error instanceof Error ? error.message : String(error);
|
|
135
177
|
failures.push({ model: modelIdent.raw, reason });
|
|
136
178
|
console.error(`[BRAIN ERROR - ${modelIdent.raw}]`, reason);
|
|
137
|
-
|
|
179
|
+
emitReasoning(`✗ ${modelIdent.raw} failed: ${reason.slice(0, 120)}`);
|
|
180
|
+
if (i !== modelsToTry.length - 1) {
|
|
138
181
|
console.log(`[BRAIN] Rotacionando para o próximo fallback da cadeia...`);
|
|
182
|
+
emitReasoning('↻ rotating to next fallback');
|
|
183
|
+
}
|
|
139
184
|
}
|
|
140
185
|
}
|
|
141
186
|
const concise = failures.slice(-3).map(f => `- ${f.model}: ${f.reason}`).join('\n');
|
|
187
|
+
const strictGpt55Only = modelsToTry.length === 1 && modelsToTry[0]?.raw === 'openai-cli/gpt-5.5';
|
|
188
|
+
if (strictGpt55Only) {
|
|
189
|
+
return [
|
|
190
|
+
'Executor GPT-5.5 indisponível no momento.',
|
|
191
|
+
'',
|
|
192
|
+
'Falhas recentes:',
|
|
193
|
+
concise,
|
|
194
|
+
'',
|
|
195
|
+
'Ação recomendada:',
|
|
196
|
+
'1) Reautentique o Codex OAuth no mesmo runtime onde o OpenLife está rodando.',
|
|
197
|
+
'2) Valide com: codex login status',
|
|
198
|
+
'3) Valide execução real com: codex exec --model gpt-5.5 "responda apenas OK"'
|
|
199
|
+
].join('\n');
|
|
200
|
+
}
|
|
142
201
|
const guidance = [
|
|
143
|
-
'1)
|
|
144
|
-
'2)
|
|
145
|
-
'3) Rode: openlife doctor e
|
|
202
|
+
'1) Valide as credenciais/binários dos provedores configurados.',
|
|
203
|
+
'2) Ajuste models.json apenas se a política do produto permitir fallbacks.',
|
|
204
|
+
'3) Rode: openlife doctor e openlife models status.'
|
|
146
205
|
].join('\n');
|
|
147
206
|
const summary = REASONING_MODE !== 'off'
|
|
148
207
|
? `\n\nResumo operacional:\n- Objetivo: responder com robustez por fallback.\n- Estratégia: rotação sequencial de provedores.\n- Resultado: todos os provedores tentados falharam.`
|
|
@@ -193,19 +252,73 @@ class Brain {
|
|
|
193
252
|
throw this.formatProviderError('openai-api', model, err, { keyEnvVar: 'OPENAI_API_KEY', expectedKeyPrefix: 'sk-' });
|
|
194
253
|
}
|
|
195
254
|
}
|
|
196
|
-
async thinkWithOpenAICLI(systemPrompt, userMessage, model) {
|
|
255
|
+
async thinkWithOpenAICLI(systemPrompt, userMessage, model, depth = 'deep') {
|
|
197
256
|
(0, ToolsetGuard_1.assertToolsetAllowed)('delegation', 'Brain.thinkWithOpenAICLI');
|
|
198
|
-
const
|
|
257
|
+
const outputFile = path.join(process.cwd(), '.openlife', `codex-last-message-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`);
|
|
258
|
+
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
259
|
+
const args = ['exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--color', 'never', '--output-last-message', outputFile];
|
|
260
|
+
if (String(process.env.OPENLIFE_CODEX_EPHEMERAL || 'true').toLowerCase() !== 'false')
|
|
261
|
+
args.push('--ephemeral');
|
|
199
262
|
if (model && model !== 'default')
|
|
200
263
|
args.push('--model', model);
|
|
201
|
-
|
|
264
|
+
const prompt = depth === 'fast'
|
|
265
|
+
? [
|
|
266
|
+
'Você é Lara, agente conversacional do OpenLife.',
|
|
267
|
+
'Responda diretamente em português natural e conciso.',
|
|
268
|
+
'Não use ferramentas, não leia arquivos e não execute comandos para responder esta mensagem.',
|
|
269
|
+
'Se não souber algo específico, seja honesta e diga o que falta.',
|
|
270
|
+
'',
|
|
271
|
+
`Mensagem: ${userMessage}`
|
|
272
|
+
].join('\n')
|
|
273
|
+
: `${systemPrompt}\n\nMensagem: ${userMessage}`;
|
|
274
|
+
args.push(prompt);
|
|
275
|
+
const timeoutMs = depth === 'fast' ? getCodexFastTimeoutMs() : getCodexDeepTimeoutMs();
|
|
276
|
+
// Deep missions remain serialized through the static queue to avoid
|
|
277
|
+
// contending Codex CLI invocations. Fast conversational calls bypass
|
|
278
|
+
// the queue so a long-running deep mission can not block a greeting.
|
|
279
|
+
let release = () => { };
|
|
280
|
+
if (depth === 'deep') {
|
|
281
|
+
const previous = Brain.codexQueue;
|
|
282
|
+
Brain.codexQueue = new Promise((resolve) => { release = resolve; });
|
|
283
|
+
await previous;
|
|
284
|
+
}
|
|
202
285
|
try {
|
|
203
|
-
const
|
|
204
|
-
|
|
286
|
+
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
287
|
+
const command = process.platform === 'win32' ? 'codex' : 'timeout';
|
|
288
|
+
const commandArgs = process.platform === 'win32'
|
|
289
|
+
? args
|
|
290
|
+
: ['-k', '2s', `${timeoutSeconds}s`, 'codex', ...args];
|
|
291
|
+
const { stdout } = await execFile(command, commandArgs, { maxBuffer: 1024 * 1024 * 10, timeout: timeoutMs + 3000, killSignal: 'SIGKILL' });
|
|
292
|
+
const lastMessage = fs.existsSync(outputFile) ? fs.readFileSync(outputFile, 'utf-8').trim() : '';
|
|
293
|
+
try {
|
|
294
|
+
fs.unlinkSync(outputFile);
|
|
295
|
+
}
|
|
296
|
+
catch { /* ignore cleanup */ }
|
|
297
|
+
const finalText = lastMessage || stdout.trim();
|
|
298
|
+
if (!finalText)
|
|
299
|
+
throw new Error(`EMPTY_CODEX_RESPONSE_${model || 'default'}`);
|
|
300
|
+
return finalText;
|
|
205
301
|
}
|
|
206
302
|
catch (err) {
|
|
303
|
+
try {
|
|
304
|
+
if (fs.existsSync(outputFile))
|
|
305
|
+
fs.unlinkSync(outputFile);
|
|
306
|
+
}
|
|
307
|
+
catch { /* ignore cleanup */ }
|
|
308
|
+
// execFile signals a timeout via either ETIMEDOUT or by killing the
|
|
309
|
+
// child (signal !== null). Surface a typed error so callers can
|
|
310
|
+
// present a depth-aware message.
|
|
311
|
+
const e = err;
|
|
312
|
+
const errCode = e.code;
|
|
313
|
+
const isTimeout = errCode === 'ETIMEDOUT' || errCode === 124 || errCode === '124' || (e?.killed === true && !!e?.signal);
|
|
314
|
+
if (isTimeout) {
|
|
315
|
+
throw new CodexTimeoutError(depth, timeoutMs, model);
|
|
316
|
+
}
|
|
207
317
|
throw this.formatProviderError('openai-cli', model, err);
|
|
208
318
|
}
|
|
319
|
+
finally {
|
|
320
|
+
release();
|
|
321
|
+
}
|
|
209
322
|
}
|
|
210
323
|
async thinkWithAnthropic(systemPrompt, userMessage, model) {
|
|
211
324
|
if (!this.anthropic)
|