@openlife/cli 1.7.5 → 1.7.6
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/InstallModules.js +37 -0
- package/dist/cli/InstallWizard.js +46 -8
- package/dist/index.js +11 -0
- package/dist/test_install_wizard.js +86 -21
- package/docs/getting-started.md +137 -0
- package/package.json +2 -1
|
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.runPrecheck = runPrecheck;
|
|
37
37
|
exports.maskToken = maskToken;
|
|
38
|
+
exports.providedApiKeyEnvNames = providedApiKeyEnvNames;
|
|
39
|
+
exports.saveApiKeysToEnv = saveApiKeysToEnv;
|
|
38
40
|
exports.saveTelegramConfig = saveTelegramConfig;
|
|
39
41
|
exports.validateTelegramToken = validateTelegramToken;
|
|
40
42
|
exports.checkTelegram409Conflict = checkTelegram409Conflict;
|
|
@@ -71,6 +73,41 @@ function maskToken(token) {
|
|
|
71
73
|
return '***';
|
|
72
74
|
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
|
73
75
|
}
|
|
76
|
+
const API_KEY_ENV_MAP = {
|
|
77
|
+
openai: 'OPENAI_API_KEY',
|
|
78
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
79
|
+
gemini: 'GEMINI_API_KEY',
|
|
80
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
81
|
+
elevenlabs: 'ELEVENLABS_API_KEY',
|
|
82
|
+
};
|
|
83
|
+
function providedApiKeyEnvNames(keys) {
|
|
84
|
+
return Object.keys(keys)
|
|
85
|
+
.filter((k) => typeof keys[k] === 'string' && keys[k].trim().length > 0)
|
|
86
|
+
.map((k) => API_KEY_ENV_MAP[k]);
|
|
87
|
+
}
|
|
88
|
+
function saveApiKeysToEnv(root, keys) {
|
|
89
|
+
const envPath = path.join(root, '.env');
|
|
90
|
+
const current = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
|
91
|
+
const provided = Object.keys(keys)
|
|
92
|
+
.filter((k) => typeof keys[k] === 'string' && keys[k].trim().length > 0);
|
|
93
|
+
if (provided.length === 0)
|
|
94
|
+
return { saved: [], path: envPath };
|
|
95
|
+
const envVars = new Set(provided.map((k) => API_KEY_ENV_MAP[k]));
|
|
96
|
+
const lines = current
|
|
97
|
+
.split('\n')
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.filter((l) => {
|
|
100
|
+
const eq = l.indexOf('=');
|
|
101
|
+
if (eq < 0)
|
|
102
|
+
return true;
|
|
103
|
+
return !envVars.has(l.slice(0, eq));
|
|
104
|
+
});
|
|
105
|
+
for (const k of provided) {
|
|
106
|
+
lines.push(`${API_KEY_ENV_MAP[k]}=${keys[k].trim()}`);
|
|
107
|
+
}
|
|
108
|
+
fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf-8');
|
|
109
|
+
return { saved: provided.map((k) => API_KEY_ENV_MAP[k]), path: envPath };
|
|
110
|
+
}
|
|
74
111
|
function saveTelegramConfig(root, token, chatId) {
|
|
75
112
|
const envPath = path.join(root, '.env');
|
|
76
113
|
const current = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
|
@@ -185,9 +185,15 @@ class InstallWizard {
|
|
|
185
185
|
}
|
|
186
186
|
preExistingAction = idx === 1 ? 'reinstall' : 'repair';
|
|
187
187
|
}
|
|
188
|
-
// 2. Profile selection
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
// 2. Profile selection — 3 user-visible options. `both` is an alias for
|
|
189
|
+
// `autonomous` since autonomous already includes the framework layer
|
|
190
|
+
// (systemInstaller.install() runs unconditionally in InstallFlow.run()).
|
|
191
|
+
const profileIdx = await this.answers.choice('Which profile do you want to install?\n framework — local interactive CLI only\n autonomous — long-running daemon with Telegram gateway and governance\n both — autonomous daemon plus framework CLI (recommended for full setup)', ['framework', 'autonomous', 'both'], 0);
|
|
192
|
+
const profile = profileIdx === 0 ? 'framework' : 'autonomous';
|
|
193
|
+
const installBoth = profileIdx === 2;
|
|
194
|
+
if (installBoth) {
|
|
195
|
+
warnings.push('INSTALLING_BOTH: autonomous profile selected (includes framework layer)');
|
|
196
|
+
}
|
|
191
197
|
// 3. Host selection — auto-detect, but always ask to confirm.
|
|
192
198
|
const detected = (0, InstallFlow_1.detectHostFromEnv)();
|
|
193
199
|
const hostOptions = [...InstallFlow_1.VALID_HOSTS];
|
|
@@ -207,25 +213,53 @@ class InstallWizard {
|
|
|
207
213
|
if (validatedHost !== 'claude-code') {
|
|
208
214
|
warnings.push(`HOST_NOT_YET_SUPPORTED: ${validatedHost} host-specific install is a no-op for now; .openlife state will still be created`);
|
|
209
215
|
}
|
|
210
|
-
// 4.
|
|
216
|
+
// 4. API keys (optional) — each prompt accepts paste or blank to skip.
|
|
217
|
+
// IMPORTANT: collect first, but only persist after final confirmation.
|
|
218
|
+
// An aborted wizard must not mutate .env or write pasted secrets.
|
|
219
|
+
let savedApiKeyNames = [];
|
|
220
|
+
const collectedApiKeys = {};
|
|
221
|
+
const wantsApiKeys = await this.answers.yesNo('Configure LLM API keys now? (You can also set them later in .env)', true);
|
|
222
|
+
if (wantsApiKeys) {
|
|
223
|
+
collectedApiKeys.openai = await this.answers.text('OPENAI_API_KEY (paste or Enter to skip)', '');
|
|
224
|
+
collectedApiKeys.anthropic = await this.answers.text('ANTHROPIC_API_KEY (paste or Enter to skip)', '');
|
|
225
|
+
collectedApiKeys.gemini = await this.answers.text('GEMINI_API_KEY (paste or Enter to skip)', '');
|
|
226
|
+
collectedApiKeys.openrouter = await this.answers.text('OPENROUTER_API_KEY (paste or Enter to skip)', '');
|
|
227
|
+
savedApiKeyNames = (0, InstallModules_1.providedApiKeyEnvNames)(collectedApiKeys);
|
|
228
|
+
if (savedApiKeyNames.length === 0) {
|
|
229
|
+
warnings.push('NO_API_KEYS_PROVIDED: model providers will only work via OAuth CLIs or Ollama until keys are added to .env');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
warnings.push('SKIPPED_API_KEYS: configure providers later via .env or `openlife auth <provider>`');
|
|
234
|
+
}
|
|
235
|
+
// 5. OAuth pointer — informational only. The actual `openlife auth gemini`
|
|
236
|
+
// / `auth openai` flows are interactive subprocesses; running them inside
|
|
237
|
+
// the wizard would break the canned-answer test seam. We just signal the
|
|
238
|
+
// operator that the flag exists so they can opt-in after init completes.
|
|
239
|
+
const wantsOAuth = await this.answers.yesNo('Plan to configure OAuth for Gemini or OpenAI CLI later?', false);
|
|
240
|
+
if (wantsOAuth) {
|
|
241
|
+
warnings.push('OAUTH_PENDING: after init completes, run `openlife auth gemini` and/or `openlife auth openai` to log in');
|
|
242
|
+
}
|
|
243
|
+
// 6. Model chain (optional)
|
|
211
244
|
const modelsRaw = await this.answers.text('LLM model order (comma-separated provider/model chain, blank = defaults)', '');
|
|
212
245
|
const modelOrder = modelsRaw
|
|
213
246
|
? modelsRaw.split(',').map((m) => m.trim()).filter(Boolean)
|
|
214
247
|
: undefined;
|
|
215
|
-
//
|
|
248
|
+
// 7. Telegram (autonomous only)
|
|
216
249
|
if (profile === 'autonomous') {
|
|
217
250
|
const hasTelegram = await this.answers.yesNo('Do you already have TELEGRAM_BOT_TOKEN and OPENLIFE_TELEGRAM_ALLOWED_USER_ID set in your .env?', false);
|
|
218
251
|
if (!hasTelegram) {
|
|
219
252
|
warnings.push('TELEGRAM_NOT_CONFIGURED: autonomous profile needs TELEGRAM_BOT_TOKEN and OPENLIFE_TELEGRAM_ALLOWED_USER_ID — set them in .env before running `openlife agent start`');
|
|
220
253
|
}
|
|
221
254
|
}
|
|
222
|
-
//
|
|
255
|
+
// 8. Skip doctor?
|
|
223
256
|
const skipDoctor = await this.answers.yesNo('Skip the system doctor run?', false);
|
|
224
|
-
//
|
|
257
|
+
// 9. Confirm preview
|
|
225
258
|
const previewLines = [
|
|
226
259
|
'Review your choices:',
|
|
227
|
-
` profile : ${profile}`,
|
|
260
|
+
` profile : ${profile}${installBoth ? ' (both — framework + autonomous)' : ''}`,
|
|
228
261
|
` host : ${validatedHost}`,
|
|
262
|
+
` apiKeys : ${savedApiKeyNames.length ? savedApiKeyNames.join(', ') : '(none saved)'}`,
|
|
229
263
|
` modelOrder : ${modelOrder ? modelOrder.join(', ') : '(defaults)'}`,
|
|
230
264
|
` skipDoctor : ${skipDoctor}`,
|
|
231
265
|
preExistingAction ? ` preExisting : ${preExistingAction}` : ''
|
|
@@ -239,6 +273,10 @@ class InstallWizard {
|
|
|
239
273
|
if (!confirmed) {
|
|
240
274
|
return { ok: false, reason: 'user_aborted', detail: 'preview_not_confirmed' };
|
|
241
275
|
}
|
|
276
|
+
if (savedApiKeyNames.length > 0) {
|
|
277
|
+
const saved = (0, InstallModules_1.saveApiKeysToEnv)(this.root, collectedApiKeys);
|
|
278
|
+
savedApiKeyNames = saved.saved;
|
|
279
|
+
}
|
|
242
280
|
const options = {
|
|
243
281
|
profile,
|
|
244
282
|
host: validatedHost,
|
package/dist/index.js
CHANGED
|
@@ -298,6 +298,8 @@ program
|
|
|
298
298
|
program.command('init')
|
|
299
299
|
.description('Interactive install wizard — guided setup for OpenLife into the chosen host CLI')
|
|
300
300
|
.action(async () => {
|
|
301
|
+
console.log((0, InstallBanner_1.installationBanner)());
|
|
302
|
+
console.log('');
|
|
301
303
|
// Lazy require preserves the lazy-import contract (`src/index.ts:11-13`).
|
|
302
304
|
const { InstallWizard, ReadlineAnswerProvider } = require('./cli/InstallWizard');
|
|
303
305
|
const { InstallFlow } = require('./cli/InstallFlow');
|
|
@@ -320,6 +322,15 @@ program.command('init')
|
|
|
320
322
|
for (const w of result.warnings)
|
|
321
323
|
console.log(` - ${w}`);
|
|
322
324
|
}
|
|
325
|
+
console.log('');
|
|
326
|
+
console.log('✅ OpenLife ready.');
|
|
327
|
+
console.log('');
|
|
328
|
+
console.log('Try:');
|
|
329
|
+
console.log(' openlife ask "hello, what can you do?"');
|
|
330
|
+
console.log(' openlife status');
|
|
331
|
+
console.log(' openlife --help');
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log('Docs: https://github.com/GOOODZ/openlife-core');
|
|
323
334
|
}
|
|
324
335
|
finally {
|
|
325
336
|
// Release the readline interface so the CLI exits cleanly.
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
// Uses CannedAnswerProvider so no real stdin/tty is required.
|
|
4
4
|
//
|
|
5
5
|
// Question order in wizard.run() (when no pre-existing install):
|
|
6
|
-
// 1) profile (choice: 0=framework, 1=autonomous)
|
|
6
|
+
// 1) profile (choice: 0=framework, 1=autonomous, 2=both)
|
|
7
7
|
// 2) host (choice: 0=claude-code, 1=gemini-cli, 2=codex)
|
|
8
|
-
// 3)
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// 3) wantsApiKeys (yesNo)
|
|
9
|
+
// 3a-d) [if Y] OPENAI / ANTHROPIC / GEMINI / OPENROUTER pastes (text)
|
|
10
|
+
// 4) wantsOAuth (yesNo)
|
|
11
|
+
// 5) model order (text: blank or comma list)
|
|
12
|
+
// 6) telegram? (yesNo, autonomous only)
|
|
13
|
+
// 7) skip doctor? (yesNo)
|
|
14
|
+
// 8) confirm preview (yesNo)
|
|
12
15
|
//
|
|
13
16
|
// When pre-existing install detected, an extra choice prompt fires FIRST:
|
|
14
17
|
// 0) abort 1) reinstall 2) repair
|
|
@@ -76,8 +79,8 @@ function writeExistingInstall(root) {
|
|
|
76
79
|
async function scenario1HappyPathFrameworkClaudeCode() {
|
|
77
80
|
const root = tempRoot();
|
|
78
81
|
try {
|
|
79
|
-
// profile=framework(0), host=claude-code(0), models='', skipDoctor=false, confirm=true
|
|
80
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, '', false, true]);
|
|
82
|
+
// profile=framework(0), host=claude-code(0), apiKeys=N, oauth=N, models='', skipDoctor=false, confirm=true
|
|
83
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, false, false, '', false, true]);
|
|
81
84
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
82
85
|
const result = await wizard.run();
|
|
83
86
|
assert(result.ok === true, 'scenario1: expected ok=true');
|
|
@@ -88,7 +91,8 @@ async function scenario1HappyPathFrameworkClaudeCode() {
|
|
|
88
91
|
assert(result.options.skipDoctor === false, 'scenario1: skipDoctor should be false');
|
|
89
92
|
assert(result.options.modelOrder === undefined, 'scenario1: modelOrder should be undefined when blank');
|
|
90
93
|
assert(result.preExistingAction === undefined, 'scenario1: no pre-existing action expected');
|
|
91
|
-
|
|
94
|
+
// SKIPPED_API_KEYS warning is expected when wantsApiKeys=false
|
|
95
|
+
assert(Array.isArray(result.warnings) && result.warnings.some((w) => w.includes('SKIPPED_API_KEYS')), 'scenario1: should warn SKIPPED_API_KEYS when api-key prompt skipped');
|
|
92
96
|
console.log('✅ scenario 1: happy path framework + claude-code');
|
|
93
97
|
}
|
|
94
98
|
finally {
|
|
@@ -98,8 +102,8 @@ async function scenario1HappyPathFrameworkClaudeCode() {
|
|
|
98
102
|
async function scenario2HappyPathAutonomousClaudeCode() {
|
|
99
103
|
const root = tempRoot();
|
|
100
104
|
try {
|
|
101
|
-
// profile=autonomous(1), host=claude-code(0), models='', telegram=true, skipDoctor=false, confirm=true
|
|
102
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([1, 0, '', true, false, true]);
|
|
105
|
+
// profile=autonomous(1), host=claude-code(0), apiKeys=N, oauth=N, models='', telegram=true, skipDoctor=false, confirm=true
|
|
106
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([1, 0, false, false, '', true, false, true]);
|
|
103
107
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
104
108
|
const result = await wizard.run();
|
|
105
109
|
assert(result.ok === true, 'scenario2: expected ok=true');
|
|
@@ -107,7 +111,8 @@ async function scenario2HappyPathAutonomousClaudeCode() {
|
|
|
107
111
|
return;
|
|
108
112
|
assert(result.options.profile === 'autonomous', 'scenario2: profile should be autonomous');
|
|
109
113
|
assert(result.options.host === 'claude-code', 'scenario2: host should be claude-code');
|
|
110
|
-
|
|
114
|
+
// SKIPPED_API_KEYS warning is expected when wantsApiKeys=false
|
|
115
|
+
assert(Array.isArray(result.warnings) && result.warnings.some((w) => w.includes('SKIPPED_API_KEYS')), 'scenario2: should warn SKIPPED_API_KEYS when api-key prompt skipped');
|
|
111
116
|
console.log('✅ scenario 2: happy path autonomous + claude-code');
|
|
112
117
|
}
|
|
113
118
|
finally {
|
|
@@ -117,8 +122,8 @@ async function scenario2HappyPathAutonomousClaudeCode() {
|
|
|
117
122
|
async function scenario3UserAbortsOnConfirm() {
|
|
118
123
|
const root = tempRoot();
|
|
119
124
|
try {
|
|
120
|
-
// framework, claude-code, blank models, skipDoctor=false, confirm=false (abort)
|
|
121
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, '', false, false]);
|
|
125
|
+
// framework, claude-code, apiKeys=N, oauth=N, blank models, skipDoctor=false, confirm=false (abort)
|
|
126
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, false, false, '', false, false]);
|
|
122
127
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
123
128
|
const result = await wizard.run();
|
|
124
129
|
assert(result.ok === false, 'scenario3: expected ok=false');
|
|
@@ -156,8 +161,8 @@ async function scenario5PreExistingRepair() {
|
|
|
156
161
|
const root = tempRoot();
|
|
157
162
|
try {
|
|
158
163
|
writeExistingInstall(root);
|
|
159
|
-
// 2=repair, then full flow: framework, claude-code, '', skipDoctor=false, confirm=true
|
|
160
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([2, 0, 0, '', false, true]);
|
|
164
|
+
// 2=repair, then full flow: framework, claude-code, apiKeys=N, oauth=N, '', skipDoctor=false, confirm=true
|
|
165
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([2, 0, 0, false, false, '', false, true]);
|
|
161
166
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
162
167
|
const result = await wizard.run();
|
|
163
168
|
assert(result.ok === true, 'scenario5: expected ok=true');
|
|
@@ -175,8 +180,8 @@ async function scenario6PreExistingReinstall() {
|
|
|
175
180
|
const root = tempRoot();
|
|
176
181
|
try {
|
|
177
182
|
writeExistingInstall(root);
|
|
178
|
-
// 1=reinstall, framework, claude-code, '', skipDoctor=false, confirm=true
|
|
179
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([1, 0, 0, '', false, true]);
|
|
183
|
+
// 1=reinstall, framework, claude-code, apiKeys=N, oauth=N, '', skipDoctor=false, confirm=true
|
|
184
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([1, 0, 0, false, false, '', false, true]);
|
|
180
185
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
181
186
|
const result = await wizard.run();
|
|
182
187
|
assert(result.ok === true, 'scenario6: expected ok=true');
|
|
@@ -192,8 +197,8 @@ async function scenario6PreExistingReinstall() {
|
|
|
192
197
|
async function scenario7UnsupportedHostGeminiCli() {
|
|
193
198
|
const root = tempRoot();
|
|
194
199
|
try {
|
|
195
|
-
// framework, host=1 (gemini-cli, not yet supported), blank models, skipDoctor=false, confirm=true
|
|
196
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([0, 1, '', false, true]);
|
|
200
|
+
// framework, host=1 (gemini-cli, not yet supported), apiKeys=N, oauth=N, blank models, skipDoctor=false, confirm=true
|
|
201
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([0, 1, false, false, '', false, true]);
|
|
197
202
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
198
203
|
const result = await wizard.run();
|
|
199
204
|
assert(result.ok === true, 'scenario7: expected ok=true even with unsupported host');
|
|
@@ -212,8 +217,8 @@ async function scenario8CustomModelChain() {
|
|
|
212
217
|
const root = tempRoot();
|
|
213
218
|
try {
|
|
214
219
|
const models = 'gemini-api/gemini-3.1-pro-preview,openai-api/gpt-5.4-mini';
|
|
215
|
-
// framework, claude-code, models=custom, skipDoctor=false, confirm=true
|
|
216
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, models, false, true]);
|
|
220
|
+
// framework, claude-code, apiKeys=N, oauth=N, models=custom, skipDoctor=false, confirm=true
|
|
221
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, false, false, models, false, true]);
|
|
217
222
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
218
223
|
const result = await wizard.run();
|
|
219
224
|
assert(result.ok === true, 'scenario8: expected ok=true');
|
|
@@ -252,6 +257,64 @@ async function scenario9OutOfAnswersThrows() {
|
|
|
252
257
|
cleanup(root);
|
|
253
258
|
}
|
|
254
259
|
}
|
|
260
|
+
async function scenario10WithApiKeysPersists() {
|
|
261
|
+
const root = tempRoot();
|
|
262
|
+
try {
|
|
263
|
+
// framework, claude-code, apiKeys=Y, paste 4 keys (openai+anthropic, skip gemini+openrouter),
|
|
264
|
+
// oauth=N, blank models, skipDoctor=false, confirm=true
|
|
265
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([
|
|
266
|
+
0, 0,
|
|
267
|
+
true, // wantsApiKeys
|
|
268
|
+
'openai-test-key', // openai paste (non-secret test fixture)
|
|
269
|
+
'anthropic-test-key', // anthropic paste (non-secret test fixture)
|
|
270
|
+
'', // gemini skip
|
|
271
|
+
'', // openrouter skip
|
|
272
|
+
false, // wantsOAuth
|
|
273
|
+
'', // models
|
|
274
|
+
false, // skipDoctor
|
|
275
|
+
true, // confirm
|
|
276
|
+
]);
|
|
277
|
+
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
278
|
+
const result = await wizard.run();
|
|
279
|
+
assert(result.ok === true, 'scenario10: expected ok=true');
|
|
280
|
+
if (!result.ok)
|
|
281
|
+
return;
|
|
282
|
+
// .env should contain the pasted keys
|
|
283
|
+
const envPath = path.join(root, '.env');
|
|
284
|
+
assert(fs.existsSync(envPath), 'scenario10: .env should exist after wizard');
|
|
285
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
286
|
+
assert(envContent.includes('OPENAI_API_KEY=openai-test-key'), 'scenario10: .env should have OPENAI_API_KEY');
|
|
287
|
+
assert(envContent.includes('ANTHROPIC_API_KEY=anthropic-test-key'), 'scenario10: .env should have ANTHROPIC_API_KEY');
|
|
288
|
+
assert(!envContent.includes('GEMINI_API_KEY='), 'scenario10: .env should NOT have GEMINI_API_KEY (skipped)');
|
|
289
|
+
// SKIPPED_API_KEYS warning should NOT fire when at least one key was provided
|
|
290
|
+
if (result.warnings) {
|
|
291
|
+
assert(!result.warnings.some((w) => w.includes('SKIPPED_API_KEYS')), 'scenario10: should NOT warn SKIPPED_API_KEYS when keys were provided');
|
|
292
|
+
}
|
|
293
|
+
console.log('✅ scenario 10: API keys collected and persisted to .env');
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
cleanup(root);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function scenario11ProfileBothMapsToAutonomous() {
|
|
300
|
+
const root = tempRoot();
|
|
301
|
+
try {
|
|
302
|
+
// profile=both(2), claude-code, apiKeys=N, oauth=N, models='', telegram=true (autonomous path),
|
|
303
|
+
// skipDoctor=false, confirm=true
|
|
304
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([2, 0, false, false, '', true, false, true]);
|
|
305
|
+
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
306
|
+
const result = await wizard.run();
|
|
307
|
+
assert(result.ok === true, 'scenario11: expected ok=true');
|
|
308
|
+
if (!result.ok)
|
|
309
|
+
return;
|
|
310
|
+
assert(result.options.profile === 'autonomous', 'scenario11: profile=both should map internally to autonomous');
|
|
311
|
+
assert(Array.isArray(result.warnings) && result.warnings.some((w) => w.includes('INSTALLING_BOTH')), 'scenario11: should emit INSTALLING_BOTH warning so caller knows both layers were intended');
|
|
312
|
+
console.log('✅ scenario 11: profile=both maps to autonomous with INSTALLING_BOTH warning');
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
cleanup(root);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
255
318
|
async function main() {
|
|
256
319
|
console.log('🧪 test_install_wizard — Story 3.5 regression suite');
|
|
257
320
|
await scenario1HappyPathFrameworkClaudeCode();
|
|
@@ -263,6 +326,8 @@ async function main() {
|
|
|
263
326
|
await scenario7UnsupportedHostGeminiCli();
|
|
264
327
|
await scenario8CustomModelChain();
|
|
265
328
|
await scenario9OutOfAnswersThrows();
|
|
329
|
+
await scenario10WithApiKeysPersists();
|
|
330
|
+
await scenario11ProfileBothMapsToAutonomous();
|
|
266
331
|
console.log('');
|
|
267
332
|
console.log('TEST_INSTALL_WIZARD_OK');
|
|
268
333
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
A 5-minute walkthrough to get OpenLife CLI running on your machine.
|
|
4
|
+
|
|
5
|
+
## 1. Install globally
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @openlife/cli
|
|
9
|
+
openlife --version
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
You should see the current version (matching the npm tag).
|
|
13
|
+
|
|
14
|
+
## 2. Run the wizard
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
openlife init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The wizard prints the OpenLife banner, then walks you through:
|
|
21
|
+
|
|
22
|
+
1. **Profile** — choose `framework` (CLI only), `autonomous` (daemon),
|
|
23
|
+
or `both`. `both` installs the autonomous profile, which includes
|
|
24
|
+
the framework layer.
|
|
25
|
+
2. **Host** — `claude-code`, `gemini-cli`, or `codex`. Auto-detected
|
|
26
|
+
when you launch `openlife init` from inside one of these CLIs.
|
|
27
|
+
3. **API keys** — paste your `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`,
|
|
28
|
+
`GEMINI_API_KEY`, and/or `OPENROUTER_API_KEY`. Press Enter to skip
|
|
29
|
+
any one. Keys are saved to `.env` in the project directory.
|
|
30
|
+
4. **OAuth pointer** — answer `y` if you plan to use `openlife auth
|
|
31
|
+
gemini` or `openlife auth openai` later. The wizard does not start
|
|
32
|
+
the OAuth flow itself; it just reminds you to run those commands
|
|
33
|
+
after init completes.
|
|
34
|
+
5. **Model chain** — comma-separated `provider/model` list, e.g.
|
|
35
|
+
`openai-api/gpt-5.4-mini,anthropic-api/claude-sonnet-4-6`. Leave
|
|
36
|
+
blank to accept the defaults from `models.json`.
|
|
37
|
+
6. **Telegram** (autonomous only) — confirm whether `TELEGRAM_BOT_TOKEN`
|
|
38
|
+
and `OPENLIFE_TELEGRAM_ALLOWED_USER_ID` are already in `.env`.
|
|
39
|
+
7. **Doctor** — answer `n` to run system diagnostics after install.
|
|
40
|
+
8. **Confirm** — review the summary and answer `Y` to proceed.
|
|
41
|
+
|
|
42
|
+
When the wizard finishes you should see:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
✅ OpenLife ready.
|
|
46
|
+
|
|
47
|
+
Try:
|
|
48
|
+
openlife ask "hello, what can you do?"
|
|
49
|
+
openlife status
|
|
50
|
+
openlife --help
|
|
51
|
+
|
|
52
|
+
Docs: https://github.com/GOOODZ/openlife-core
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 3. Try your first command
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
openlife ask "summarize this README in one sentence"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
OpenLife will route the request to the highest-priority model in your
|
|
62
|
+
chain that has a working key. If no API key is configured, it falls
|
|
63
|
+
back to local Ollama (if available) or returns an error.
|
|
64
|
+
|
|
65
|
+
## 4. Common follow-ups
|
|
66
|
+
|
|
67
|
+
- **Set up OAuth (no API key needed):**
|
|
68
|
+
```bash
|
|
69
|
+
openlife auth gemini # browser-based Google login
|
|
70
|
+
openlife auth openai # browser-based OpenAI login
|
|
71
|
+
```
|
|
72
|
+
- **Start the autonomous daemon (autonomous profile):**
|
|
73
|
+
```bash
|
|
74
|
+
openlife start --daemon
|
|
75
|
+
```
|
|
76
|
+
- **Inspect runtime state:**
|
|
77
|
+
```bash
|
|
78
|
+
ls .openlife/
|
|
79
|
+
cat .openlife/heartbeat.json
|
|
80
|
+
cat .openlife/install-manifest.json
|
|
81
|
+
```
|
|
82
|
+
- **Update to latest version:**
|
|
83
|
+
```bash
|
|
84
|
+
npm install -g @openlife/cli@latest
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Troubleshooting
|
|
88
|
+
|
|
89
|
+
### `openlife: command not found` after install
|
|
90
|
+
|
|
91
|
+
The global npm bin directory is probably not on your `PATH`. Run:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm bin -g
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Add that path to your shell profile (`~/.bashrc`, `~/.zshrc`).
|
|
98
|
+
|
|
99
|
+
### `npm install -g` says EEXIST
|
|
100
|
+
|
|
101
|
+
You have a leftover symlink from a previous `npm link`. Run:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm unlink -g @openlife/cli
|
|
105
|
+
rm -f $(which openlife)
|
|
106
|
+
npm install -g @openlife/cli
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `openlife ask` fails with `MODEL_TIMEOUT` or `no provider available`
|
|
110
|
+
|
|
111
|
+
No API key is configured and no fallback (Ollama, OAuth) is wired up.
|
|
112
|
+
Either:
|
|
113
|
+
|
|
114
|
+
- Edit `.env` to add `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or
|
|
115
|
+
`GEMINI_API_KEY`, then retry, or
|
|
116
|
+
- Run `openlife auth gemini` / `openlife auth openai` to use OAuth, or
|
|
117
|
+
- Start a local Ollama server (`ollama serve`) and set
|
|
118
|
+
`OPENLIFE_ENABLE_OLLAMA=true` in `.env`.
|
|
119
|
+
|
|
120
|
+
### Telegram autonomous mode reports `TELEGRAM_NOT_CONFIGURED`
|
|
121
|
+
|
|
122
|
+
Set both in `.env`:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
TELEGRAM_BOT_TOKEN=<from BotFather>
|
|
126
|
+
OPENLIFE_TELEGRAM_ALLOWED_USER_ID=<your Telegram user id>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then restart with `openlife start --daemon`.
|
|
130
|
+
|
|
131
|
+
## Where to go next
|
|
132
|
+
|
|
133
|
+
- [INSTALL.md](../INSTALL.md) — full install options including
|
|
134
|
+
non-interactive (`openlife system setup`) for CI use.
|
|
135
|
+
- [README.md](../README.md) — feature overview and architecture.
|
|
136
|
+
- [CHANGELOG.md](../CHANGELOG.md) — what changed across versions.
|
|
137
|
+
- [CONTRIBUTING.md](../CONTRIBUTING.md) — dev setup and conventions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlife/cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.6",
|
|
4
4
|
"description": "OPEN-LIFE Córtex Orquestrador Dual-Core",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dist-templates",
|
|
11
11
|
"docs/README.md",
|
|
12
12
|
"docs/quickstart.md",
|
|
13
|
+
"docs/getting-started.md",
|
|
13
14
|
"docs/workflow-schema.md",
|
|
14
15
|
"docs/toolset-enforcement.md",
|
|
15
16
|
"docs/release-process.md",
|