@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.
@@ -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
- const profileIdx = await this.answers.choice('Which profile do you want to install?\n framework — local interactive CLI (default)\n autonomous long-running daemon with Telegram gateway and governance', ['framework', 'autonomous'], 0);
190
- const profile = profileIdx === 1 ? 'autonomous' : 'framework';
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. Model chain (optional)
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
- // 5. Telegram (autonomous only)
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
- // 6. Skip doctor?
255
+ // 8. Skip doctor?
223
256
  const skipDoctor = await this.answers.yesNo('Skip the system doctor run?', false);
224
- // 7. Confirm preview
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) model order (text: blank or comma list)
9
- // 4) telegram? (yesNo, autonomous only)
10
- // 5) skip doctor? (yesNo)
11
- // 6) confirm preview (yesNo)
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
- assert(!result.warnings, 'scenario1: no warnings expected for default claude-code');
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
- assert(!result.warnings, 'scenario2: no warnings when telegram already configured');
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.5",
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",