@openlife/cli 1.7.13 → 1.8.2
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/INSTALL.md +29 -1
- package/dist/cli/ChatTui.js +32 -0
- package/dist/cli/InstallModules.js +17 -2
- package/dist/cli/InstallWizardV2.js +110 -0
- package/dist/cli/install/Multiselect.js +285 -0
- package/dist/cli/install/OAuthRunner.js +170 -0
- package/dist/cli/install/Phases.js +864 -0
- package/dist/cli/install/ProvidersCatalog.js +320 -0
- package/dist/cli/install/WizardIO.js +271 -0
- package/dist/cli/install/types.js +17 -0
- package/dist/index.js +170 -35
- package/dist/orchestrator/ConsequenceForecaster.js +24 -1
- package/dist/orchestrator/DreamGoalStore.js +130 -0
- package/dist/orchestrator/Gatekeeper.js +14 -0
- package/dist/orchestrator/Gateway.js +194 -15
- package/dist/orchestrator/ModelManager.js +7 -1
- package/dist/orchestrator/OrchestrationLoop.js +12 -0
- package/dist/orchestrator/ParallelOrchestrationLoop.js +12 -2
- package/dist/orchestrator/RuntimePolicy.js +4 -1
- package/dist/orchestrator/ServiceCompletionPolicy.js +15 -0
- package/dist/orchestrator/SynthesizerAgent.js +20 -1
- package/dist/orchestrator/TaskExecutor.js +53 -0
- package/dist/orchestrator/capability/CapabilityGenesisEngine.js +66 -11
- package/dist/test_capability_genesis_engine.js +1 -1
- package/dist/test_chat_smoke_command.js +59 -0
- package/dist/test_dream_goal_commands.js +76 -0
- package/dist/test_gateway_telegram_formatting.js +74 -0
- package/dist/test_install_wizard_v2.js +193 -0
- package/dist/test_on_demand_voice_reply.js +65 -0
- package/dist/test_remaining_sprints_contracts.js +103 -0
- package/dist/test_runtime_policy.js +25 -6
- package/dist/test_service_completion_policy.js +7 -0
- package/dist/test_subsystems_routing_governance.js +13 -3
- package/dist/test_task_executor_gemini_api.js +68 -0
- package/package.json +5 -3
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/cli/install/Phases.ts
|
|
3
|
+
// All install-wizard phases as Phase objects with stable ids.
|
|
4
|
+
//
|
|
5
|
+
// Phase taxonomy:
|
|
6
|
+
// 00-09 : welcome / detection / product selection (always)
|
|
7
|
+
// 10-19 : openlife-core block (when core is selected)
|
|
8
|
+
// 20-29 : openlife-agent block (when agent is selected)
|
|
9
|
+
//
|
|
10
|
+
// The runner walks phases in id order, skipping those whose `when` returns false.
|
|
11
|
+
// Each phase ends with `io.pause()` so the user controls progression with Enter.
|
|
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.ALL_PHASES = void 0;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const InstallBanner_1 = require("../InstallBanner");
|
|
51
|
+
const MatrixTheme_1 = require("../MatrixTheme");
|
|
52
|
+
const InstallFlow_1 = require("../InstallFlow");
|
|
53
|
+
const Multiselect_1 = require("./Multiselect");
|
|
54
|
+
const OAuthRunner_1 = require("./OAuthRunner");
|
|
55
|
+
const ProvidersCatalog_1 = require("./ProvidersCatalog");
|
|
56
|
+
const types_1 = require("./types");
|
|
57
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
58
|
+
// Section A · Welcome & Product Selection (always run)
|
|
59
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
60
|
+
const phaseBanner = {
|
|
61
|
+
id: '00-banner',
|
|
62
|
+
label: 'Welcome',
|
|
63
|
+
async run(_ctx, io) {
|
|
64
|
+
process.stdout.write((0, InstallBanner_1.installationBanner)() + '\n\n');
|
|
65
|
+
io.print('Welcome to the OpenLife installer.');
|
|
66
|
+
io.hint('This wizard walks you through setup one step at a time.');
|
|
67
|
+
io.hint('Press Enter to advance · Ctrl-C to abort any time.');
|
|
68
|
+
io.print('');
|
|
69
|
+
io.print('What you can install:');
|
|
70
|
+
io.print(' • openlife-core — local interactive CLI (framework profile)');
|
|
71
|
+
io.print(' • openlife-agent — long-running daemon with Telegram gateway');
|
|
72
|
+
io.print('');
|
|
73
|
+
io.hint('Both can be installed together (core runs first, then agent).');
|
|
74
|
+
await io.pause();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const phaseExistingInstall = {
|
|
78
|
+
id: '01-existing-install',
|
|
79
|
+
label: 'Pre-existing install check',
|
|
80
|
+
async run(ctx, io) {
|
|
81
|
+
const manifestPath = path.join(ctx.root, '.openlife', 'install-manifest.json');
|
|
82
|
+
if (!fs.existsSync(manifestPath)) {
|
|
83
|
+
io.print('No previous install detected — clean slate.');
|
|
84
|
+
await io.pause();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
io.warn(`Existing install detected at ${manifestPath}`);
|
|
88
|
+
const idx = await io.choice('What do you want to do?', [
|
|
89
|
+
'abort — leave the existing install untouched (recommended)',
|
|
90
|
+
'reinstall — overwrite everything',
|
|
91
|
+
'repair — re-run install over existing files (keeps state)',
|
|
92
|
+
], 0);
|
|
93
|
+
if (idx === 0) {
|
|
94
|
+
ctx.aborted = true;
|
|
95
|
+
ctx.abortReason = 'pre_existing_install_abort';
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
ctx.preExisting = idx === 1 ? 'reinstall' : 'repair';
|
|
99
|
+
io.print(`Selected: ${ctx.preExisting}`);
|
|
100
|
+
await io.pause();
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const phaseProductSelection = {
|
|
104
|
+
id: '02-product-select',
|
|
105
|
+
label: 'Choose what to install',
|
|
106
|
+
async run(ctx, io) {
|
|
107
|
+
io.print('Pick the OpenLife products you want to install on this machine.');
|
|
108
|
+
io.hint('Select with SPACE, confirm with ENTER. Both = full setup.');
|
|
109
|
+
io.print('');
|
|
110
|
+
const products = await (0, Multiselect_1.multiselect)({
|
|
111
|
+
title: 'Products',
|
|
112
|
+
items: [
|
|
113
|
+
{
|
|
114
|
+
value: 'openlife-core',
|
|
115
|
+
label: 'openlife-core',
|
|
116
|
+
description: 'Local CLI · multi-provider chat, agents, skills, workflows',
|
|
117
|
+
preselected: true,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
value: 'openlife-agent',
|
|
121
|
+
label: 'openlife-agent',
|
|
122
|
+
description: 'Long-running daemon · Telegram gateway, governance, scheduler',
|
|
123
|
+
preselected: false,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
minSelections: 1,
|
|
127
|
+
});
|
|
128
|
+
if (!products || products.length === 0) {
|
|
129
|
+
ctx.aborted = true;
|
|
130
|
+
ctx.abortReason = 'no_product_selected';
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Always install core BEFORE agent when both selected (sequential).
|
|
134
|
+
ctx.products = products.sort((a, b) => (a === 'openlife-core' ? -1 : b === 'openlife-core' ? 1 : 0));
|
|
135
|
+
io.print('');
|
|
136
|
+
io.print(`Plan: ${ctx.products.join(' → ')}`);
|
|
137
|
+
if (ctx.products.length > 1) {
|
|
138
|
+
io.hint('Sequential install — openlife-core runs first, then openlife-agent.');
|
|
139
|
+
}
|
|
140
|
+
await io.pause();
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
144
|
+
// Section B · openlife-core block
|
|
145
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
146
|
+
const isCore = (ctx) => ctx.products.includes('openlife-core') && !ctx.aborted;
|
|
147
|
+
const phaseCoreEnter = {
|
|
148
|
+
id: '10-core-enter',
|
|
149
|
+
label: 'openlife-core · enter',
|
|
150
|
+
when: isCore,
|
|
151
|
+
async run(ctx, io) {
|
|
152
|
+
ctx.currentProduct = 'openlife-core';
|
|
153
|
+
io.divider();
|
|
154
|
+
io.print(`${(0, MatrixTheme_1.paint)('▣ openlife-core', MatrixTheme_1.MATRIX.head)} — local interactive CLI`);
|
|
155
|
+
io.hint('Steps: hosts → providers → keys → model chain → doctor → install.');
|
|
156
|
+
await io.pause();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const phaseCoreHosts = {
|
|
160
|
+
id: '11-core-hosts',
|
|
161
|
+
label: 'openlife-core · host CLIs',
|
|
162
|
+
when: isCore,
|
|
163
|
+
async run(ctx, io) {
|
|
164
|
+
const detected = (0, InstallFlow_1.detectHostFromEnv)();
|
|
165
|
+
if (detected)
|
|
166
|
+
io.hint(`(auto-detected current host: ${detected})`);
|
|
167
|
+
io.print('Pick the host CLI(s) you want OpenLife wired into.');
|
|
168
|
+
io.hint('You can pick more than one — OpenLife installs into each.');
|
|
169
|
+
io.print('');
|
|
170
|
+
const hosts = await (0, Multiselect_1.multiselect)({
|
|
171
|
+
title: 'Host CLIs',
|
|
172
|
+
items: InstallFlow_1.VALID_HOSTS.map((h) => ({
|
|
173
|
+
value: h,
|
|
174
|
+
label: h,
|
|
175
|
+
description: hostDescription(h),
|
|
176
|
+
preselected: h === (detected || InstallFlow_1.DEFAULT_HOST),
|
|
177
|
+
})),
|
|
178
|
+
minSelections: 1,
|
|
179
|
+
});
|
|
180
|
+
if (!hosts || hosts.length === 0) {
|
|
181
|
+
ctx.warnings.push('NO_HOST_SELECTED: defaulting to claude-code');
|
|
182
|
+
ctx.coreHosts = [InstallFlow_1.DEFAULT_HOST];
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
ctx.coreHosts = hosts;
|
|
186
|
+
}
|
|
187
|
+
io.print(`Selected hosts: ${ctx.coreHosts.join(', ')}`);
|
|
188
|
+
await io.pause();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const phaseCoreProviders = {
|
|
192
|
+
id: '12-core-providers',
|
|
193
|
+
label: 'openlife-core · providers',
|
|
194
|
+
when: isCore,
|
|
195
|
+
async run(ctx, io) {
|
|
196
|
+
io.print('Pick the LLM providers you want available in the CLI.');
|
|
197
|
+
io.hint('SPACE to toggle, A = all, N = none, ENTER to confirm.');
|
|
198
|
+
io.hint('You can add more later with `openlife auth <provider>`.');
|
|
199
|
+
io.print('');
|
|
200
|
+
const items = ProvidersCatalog_1.PROVIDERS_CATALOG.map((p) => ({
|
|
201
|
+
value: p.id,
|
|
202
|
+
label: p.label,
|
|
203
|
+
description: p.description,
|
|
204
|
+
group: p.tier,
|
|
205
|
+
// Pre-select Tier 1 essentials by default.
|
|
206
|
+
preselected: p.tier === 'tier-1',
|
|
207
|
+
}));
|
|
208
|
+
const providers = await (0, Multiselect_1.multiselect)({
|
|
209
|
+
title: 'LLM Providers',
|
|
210
|
+
items,
|
|
211
|
+
groupOrder: ['tier-1', 'tier-2', 'tier-3', 'tier-4', 'tier-5'],
|
|
212
|
+
groupLabels: ProvidersCatalog_1.TIER_LABELS,
|
|
213
|
+
minSelections: 1,
|
|
214
|
+
});
|
|
215
|
+
if (!providers || providers.length === 0) {
|
|
216
|
+
ctx.warnings.push('NO_PROVIDER_SELECTED: defaulting to openrouter');
|
|
217
|
+
ctx.coreProviders = ['openrouter'];
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
ctx.coreProviders = providers;
|
|
221
|
+
}
|
|
222
|
+
io.print(`Selected ${ctx.coreProviders.length} provider(s): ${ctx.coreProviders.join(', ')}`);
|
|
223
|
+
await io.pause();
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
const phaseCoreApiKeys = {
|
|
227
|
+
id: '13-core-api-keys',
|
|
228
|
+
label: 'openlife-core · API keys',
|
|
229
|
+
when: (ctx) => isCore(ctx) && ctx.coreProviders.some((p) => !!(0, ProvidersCatalog_1.getProvider)(p).envVar),
|
|
230
|
+
async run(ctx, io) {
|
|
231
|
+
io.print('Paste API keys for each provider (Enter to skip).');
|
|
232
|
+
io.hint('Keys are written to .env only after the final confirmation.');
|
|
233
|
+
io.print('');
|
|
234
|
+
for (const id of ctx.coreProviders) {
|
|
235
|
+
const provider = (0, ProvidersCatalog_1.getProvider)(id);
|
|
236
|
+
if (!provider.envVar) {
|
|
237
|
+
// Local / OAuth / router-only — no api key field.
|
|
238
|
+
io.hint(`${provider.label}: no API key required (${provider.tags.join(', ')})`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const key = await io.secret(`${provider.envVar} (Enter to skip)`);
|
|
242
|
+
if (key) {
|
|
243
|
+
ctx.coreApiKeys[id] = key;
|
|
244
|
+
io.print(` ✓ ${provider.envVar} captured (will save on confirm)`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
io.hint(` · ${provider.envVar} skipped`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
io.print('');
|
|
251
|
+
await io.pause();
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
const phaseCoreModelChain = {
|
|
255
|
+
id: '14-core-model-chain',
|
|
256
|
+
label: 'openlife-core · model chain',
|
|
257
|
+
when: isCore,
|
|
258
|
+
async run(ctx, io) {
|
|
259
|
+
io.print('Optionally set a custom model chain (primary,fallback1,fallback2…).');
|
|
260
|
+
io.hint('Format: provider/model · ex: gemini-api/gemini-3.1-flash,openai-api/gpt-5.5');
|
|
261
|
+
io.hint('Enter blank to use the OpenLife defaults.');
|
|
262
|
+
io.print('');
|
|
263
|
+
const raw = await io.text('Model chain (blank = defaults)');
|
|
264
|
+
if (raw) {
|
|
265
|
+
const parts = raw.split(',').map((m) => m.trim()).filter(Boolean);
|
|
266
|
+
if (parts.length === 0) {
|
|
267
|
+
io.warn('No valid models parsed — using defaults');
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
ctx.coreModelChain = parts;
|
|
271
|
+
io.print(`Model chain: ${parts.join(' → ')}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
io.hint('Using defaults: gemini-api/gemini-3.1-flash-lite-preview → openai-api/gpt-5.4-mini → ollama/qwen2.5-coder:7b');
|
|
276
|
+
}
|
|
277
|
+
await io.pause();
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
const phaseCoreDoctor = {
|
|
281
|
+
id: '15-core-doctor',
|
|
282
|
+
label: 'openlife-core · post-install doctor',
|
|
283
|
+
when: isCore,
|
|
284
|
+
async run(ctx, io) {
|
|
285
|
+
io.print('After install, run `openlife system doctor` to verify the setup.');
|
|
286
|
+
io.hint('Doctor checks API keys, model chain reachability, host paths, runtime state.');
|
|
287
|
+
io.print('');
|
|
288
|
+
const skip = await io.yesNo('Skip the post-install doctor run?', false);
|
|
289
|
+
ctx.coreSkipDoctor = skip;
|
|
290
|
+
if (skip) {
|
|
291
|
+
ctx.warnings.push('DOCTOR_SKIPPED: run `openlife system doctor` manually to verify');
|
|
292
|
+
}
|
|
293
|
+
await io.pause();
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
const phaseCoreConfirm = {
|
|
297
|
+
id: '16-core-confirm',
|
|
298
|
+
label: 'openlife-core · confirm & install',
|
|
299
|
+
when: isCore,
|
|
300
|
+
async run(ctx, io) {
|
|
301
|
+
io.divider();
|
|
302
|
+
io.header('Review openlife-core configuration');
|
|
303
|
+
io.print(`Hosts : ${ctx.coreHosts.join(', ')}`);
|
|
304
|
+
io.print(`Providers (${ctx.coreProviders.length}) : ${ctx.coreProviders.join(', ')}`);
|
|
305
|
+
const keyNames = Object.keys(ctx.coreApiKeys).filter((k) => ctx.coreApiKeys[k]);
|
|
306
|
+
io.print(`API keys captured: ${keyNames.length ? keyNames.join(', ') : '(none)'}`);
|
|
307
|
+
io.print(`Model chain : ${ctx.coreModelChain ? ctx.coreModelChain.join(' → ') : '(defaults)'}`);
|
|
308
|
+
io.print(`Skip doctor : ${ctx.coreSkipDoctor ? 'yes' : 'no'}`);
|
|
309
|
+
io.print('');
|
|
310
|
+
const ok = await io.yesNo('Apply this configuration?', true);
|
|
311
|
+
if (!ok) {
|
|
312
|
+
ctx.aborted = true;
|
|
313
|
+
ctx.abortReason = 'core_confirm_rejected';
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Persist API keys to .env (only now — never before confirm).
|
|
317
|
+
if (keyNames.length > 0) {
|
|
318
|
+
const { saveApiKeysToEnv } = await Promise.resolve().then(() => __importStar(require('../InstallModules')));
|
|
319
|
+
const keyMap = mapApiKeysForLegacyHelper(ctx.coreApiKeys);
|
|
320
|
+
saveApiKeysToEnv(ctx.root, keyMap);
|
|
321
|
+
io.print(`✓ API keys persisted to ${path.join(ctx.root, '.env')}`);
|
|
322
|
+
}
|
|
323
|
+
// Run InstallFlow for the core profile across all chosen hosts.
|
|
324
|
+
const { InstallFlow } = await Promise.resolve().then(() => __importStar(require('../InstallFlow')));
|
|
325
|
+
const flow = new InstallFlow(ctx.root);
|
|
326
|
+
for (const host of ctx.coreHosts) {
|
|
327
|
+
io.print('');
|
|
328
|
+
io.print(`▸ Installing into host: ${host}`);
|
|
329
|
+
const result = flow.run({
|
|
330
|
+
profile: 'framework',
|
|
331
|
+
host,
|
|
332
|
+
skipDoctor: ctx.coreSkipDoctor,
|
|
333
|
+
modelOrder: ctx.coreModelChain,
|
|
334
|
+
});
|
|
335
|
+
for (const line of flow.renderSummary(result))
|
|
336
|
+
io.print(' ' + line);
|
|
337
|
+
}
|
|
338
|
+
await io.pause();
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
const phaseCoreComplete = {
|
|
342
|
+
id: '17-core-complete',
|
|
343
|
+
label: 'openlife-core · complete',
|
|
344
|
+
when: isCore,
|
|
345
|
+
async run(_ctx, io) {
|
|
346
|
+
io.print('');
|
|
347
|
+
renderCompletionCard(io, 'openlife-core installed', [
|
|
348
|
+
'Run `openlife ask "hello"` to test the chat',
|
|
349
|
+
'Run `openlife status` for a runtime snapshot',
|
|
350
|
+
'Run `openlife --help` for the full command surface',
|
|
351
|
+
]);
|
|
352
|
+
await io.pause();
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
356
|
+
// Section C · openlife-agent block
|
|
357
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
358
|
+
const isAgent = (ctx) => ctx.products.includes('openlife-agent') && !ctx.aborted;
|
|
359
|
+
const phaseAgentEnter = {
|
|
360
|
+
id: '20-agent-enter',
|
|
361
|
+
label: 'openlife-agent · enter',
|
|
362
|
+
when: isAgent,
|
|
363
|
+
async run(ctx, io) {
|
|
364
|
+
ctx.currentProduct = 'openlife-agent';
|
|
365
|
+
io.divider();
|
|
366
|
+
io.print(`${(0, MatrixTheme_1.paint)('▣ openlife-agent', MatrixTheme_1.MATRIX.head)} — long-running daemon`);
|
|
367
|
+
io.hint('Steps: auth-per-provider → Telegram → service mode → install.');
|
|
368
|
+
io.print('');
|
|
369
|
+
io.hint('Tip: agent profile lets you pick API key OR OAuth per provider.');
|
|
370
|
+
io.warn('Anthropic OAuth is NOT permitted — Claude requires API key.');
|
|
371
|
+
await io.pause();
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
const phaseAgentAuthStrategy = {
|
|
375
|
+
id: '21-agent-auth-strategy',
|
|
376
|
+
label: 'openlife-agent · auth strategy per provider',
|
|
377
|
+
when: isAgent,
|
|
378
|
+
async run(ctx, io) {
|
|
379
|
+
io.print('For the autonomous daemon, choose how each provider authenticates.');
|
|
380
|
+
io.hint('OAuth uses your subscription (Plus/Pro). API key uses billed credits.');
|
|
381
|
+
io.print('');
|
|
382
|
+
// Reuse the provider list from the core block when available; otherwise
|
|
383
|
+
// ask the user to pick agent-profile providers fresh.
|
|
384
|
+
let providers = ctx.coreProviders.length > 0
|
|
385
|
+
? [...ctx.coreProviders]
|
|
386
|
+
: [];
|
|
387
|
+
if (providers.length === 0) {
|
|
388
|
+
io.hint('No providers carried from openlife-core — picking fresh for agent.');
|
|
389
|
+
const items = ProvidersCatalog_1.PROVIDERS_CATALOG.map((p) => ({
|
|
390
|
+
value: p.id,
|
|
391
|
+
label: p.label,
|
|
392
|
+
description: p.description,
|
|
393
|
+
group: p.tier,
|
|
394
|
+
preselected: p.tier === 'tier-1' && (p.id === 'openai-api' || p.id === 'gemini-api' || p.id === 'anthropic'),
|
|
395
|
+
}));
|
|
396
|
+
const picked = await (0, Multiselect_1.multiselect)({
|
|
397
|
+
title: 'Providers for the agent',
|
|
398
|
+
items,
|
|
399
|
+
groupOrder: ['tier-1', 'tier-2', 'tier-3', 'tier-4', 'tier-5'],
|
|
400
|
+
groupLabels: ProvidersCatalog_1.TIER_LABELS,
|
|
401
|
+
minSelections: 1,
|
|
402
|
+
});
|
|
403
|
+
providers = picked || [];
|
|
404
|
+
}
|
|
405
|
+
if (providers.length === 0) {
|
|
406
|
+
ctx.warnings.push('AGENT_NO_PROVIDERS: agent will rely on whatever .env contains');
|
|
407
|
+
await io.pause();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
io.print(`Providers to configure: ${providers.length}`);
|
|
411
|
+
io.print('');
|
|
412
|
+
const decisions = [];
|
|
413
|
+
for (const id of providers) {
|
|
414
|
+
const provider = (0, ProvidersCatalog_1.getProvider)(id);
|
|
415
|
+
const oauthAllowed = types_1.OAUTH_ENABLED_PROVIDERS.includes(id);
|
|
416
|
+
const noApiKey = !provider.envVar;
|
|
417
|
+
// Build available options dynamically.
|
|
418
|
+
const opts = [];
|
|
419
|
+
if (provider.envVar) {
|
|
420
|
+
opts.push({ strategy: 'api-key', label: `API key — ${provider.envVar}` });
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
opts.push({
|
|
424
|
+
strategy: 'api-key',
|
|
425
|
+
label: 'API key',
|
|
426
|
+
disabled: true,
|
|
427
|
+
reason: 'this provider has no API-key option',
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (oauthAllowed) {
|
|
431
|
+
opts.push({ strategy: 'oauth', label: `OAuth via \`${provider.oauthCli} login\`` });
|
|
432
|
+
}
|
|
433
|
+
else if (id === 'anthropic') {
|
|
434
|
+
opts.push({
|
|
435
|
+
strategy: 'oauth',
|
|
436
|
+
label: 'OAuth',
|
|
437
|
+
disabled: true,
|
|
438
|
+
reason: 'Anthropic OAuth is NOT permitted (policy)',
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
opts.push({
|
|
443
|
+
strategy: 'oauth',
|
|
444
|
+
label: 'OAuth',
|
|
445
|
+
disabled: true,
|
|
446
|
+
reason: 'OAuth not available for this provider',
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
opts.push({ strategy: 'skip', label: 'Skip — configure later' });
|
|
450
|
+
io.print(`── ${provider.label} ──`);
|
|
451
|
+
const enabledOpts = opts.filter(o => !o.disabled);
|
|
452
|
+
const labels = opts.map((o) => o.disabled ? `${o.label} (disabled: ${o.reason})` : o.label);
|
|
453
|
+
const defaultIdx = opts.findIndex(o => !o.disabled);
|
|
454
|
+
const idx = await io.choice('Auth strategy', labels, defaultIdx >= 0 ? defaultIdx : 0);
|
|
455
|
+
const chosen = opts[idx];
|
|
456
|
+
if (chosen.disabled) {
|
|
457
|
+
io.warn(`Option "${chosen.label}" is disabled — defaulting to skip.`);
|
|
458
|
+
decisions.push({ provider: id, strategy: 'skip' });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
decisions.push({ provider: id, strategy: chosen.strategy });
|
|
462
|
+
io.print(` → ${chosen.strategy}`);
|
|
463
|
+
}
|
|
464
|
+
ctx.agentAuthDecisions = decisions;
|
|
465
|
+
io.print('');
|
|
466
|
+
await io.pause();
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
const phaseAgentOAuth = {
|
|
470
|
+
id: '22-agent-oauth',
|
|
471
|
+
label: 'openlife-agent · run OAuth flows',
|
|
472
|
+
when: (ctx) => isAgent(ctx) && ctx.agentAuthDecisions.some((d) => d.strategy === 'oauth'),
|
|
473
|
+
async run(ctx, io) {
|
|
474
|
+
const oauthDecisions = ctx.agentAuthDecisions.filter((d) => d.strategy === 'oauth');
|
|
475
|
+
io.print(`Running OAuth for ${oauthDecisions.length} provider(s).`);
|
|
476
|
+
io.hint('Each step launches the vendor CLI — complete the browser flow when prompted.');
|
|
477
|
+
io.print('');
|
|
478
|
+
for (const decision of oauthDecisions) {
|
|
479
|
+
const provider = (0, ProvidersCatalog_1.getProvider)(decision.provider);
|
|
480
|
+
io.print(`── OAuth: ${provider.label} ──`);
|
|
481
|
+
const proceed = await io.yesNo(`Run \`${provider.oauthCli} login\` now?`, true);
|
|
482
|
+
if (!proceed) {
|
|
483
|
+
ctx.warnings.push(`OAUTH_DEFERRED_${decision.provider}: run \`${provider.oauthCli} login\` manually later`);
|
|
484
|
+
io.hint(' · deferred — run later manually');
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const result = await (0, OAuthRunner_1.runOAuth)(decision.provider);
|
|
488
|
+
ctx.agentOAuthResults.push(result);
|
|
489
|
+
if (result.ok) {
|
|
490
|
+
io.print(` ✓ ${result.detail}`);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
io.err(` ${result.detail}`);
|
|
494
|
+
ctx.warnings.push(`OAUTH_FAILED_${decision.provider}: ${result.detail}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
io.print('');
|
|
498
|
+
await io.pause();
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
const phaseAgentApiKeys = {
|
|
502
|
+
id: '23-agent-api-keys',
|
|
503
|
+
label: 'openlife-agent · API keys',
|
|
504
|
+
when: (ctx) => isAgent(ctx) &&
|
|
505
|
+
ctx.agentAuthDecisions.some((d) => d.strategy === 'api-key'),
|
|
506
|
+
async run(ctx, io) {
|
|
507
|
+
const apiKeyDecisions = ctx.agentAuthDecisions.filter((d) => d.strategy === 'api-key');
|
|
508
|
+
io.print(`Collecting API keys for ${apiKeyDecisions.length} provider(s).`);
|
|
509
|
+
io.hint('Paste the key when prompted (input masked). Enter to skip.');
|
|
510
|
+
io.print('');
|
|
511
|
+
for (const decision of apiKeyDecisions) {
|
|
512
|
+
const provider = (0, ProvidersCatalog_1.getProvider)(decision.provider);
|
|
513
|
+
if (!provider.envVar) {
|
|
514
|
+
io.hint(`${provider.label}: no env var configured — skipping`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
// Skip if openlife-core already collected this same key.
|
|
518
|
+
if (ctx.coreApiKeys[decision.provider]) {
|
|
519
|
+
io.print(` ✓ ${provider.envVar} reused from openlife-core`);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const key = await io.secret(`${provider.envVar} (Enter to skip)`);
|
|
523
|
+
if (key) {
|
|
524
|
+
ctx.agentApiKeys[decision.provider] = key;
|
|
525
|
+
io.print(` ✓ ${provider.envVar} captured`);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
io.hint(` · ${provider.envVar} skipped`);
|
|
529
|
+
ctx.warnings.push(`AGENT_NO_KEY_${decision.provider}: agent will fall back to other providers`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
io.print('');
|
|
533
|
+
await io.pause();
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
const phaseAgentTelegramToken = {
|
|
537
|
+
id: '24-agent-telegram-token',
|
|
538
|
+
label: 'openlife-agent · Telegram bot token',
|
|
539
|
+
when: isAgent,
|
|
540
|
+
async run(ctx, io) {
|
|
541
|
+
io.print('Configure Telegram — the autonomous daemon\'s primary gateway.');
|
|
542
|
+
io.hint('Get a token from @BotFather (https://t.me/BotFather → /newbot).');
|
|
543
|
+
io.print('');
|
|
544
|
+
const token = await io.secret('TELEGRAM_BOT_TOKEN (Enter to skip)');
|
|
545
|
+
if (!token) {
|
|
546
|
+
ctx.warnings.push('TELEGRAM_NO_TOKEN: agent will run without Telegram gateway until you add it');
|
|
547
|
+
await io.pause();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// Validate format + getMe probe.
|
|
551
|
+
const { validateTelegramToken } = await Promise.resolve().then(() => __importStar(require('../InstallModules')));
|
|
552
|
+
const validation = validateTelegramToken(token);
|
|
553
|
+
if (!validation.ok) {
|
|
554
|
+
io.err(`Token validation failed: ${validation.detail}`);
|
|
555
|
+
const proceed = await io.yesNo('Save it anyway?', false);
|
|
556
|
+
if (!proceed) {
|
|
557
|
+
ctx.warnings.push(`TELEGRAM_TOKEN_INVALID: ${validation.detail} — not saved`);
|
|
558
|
+
await io.pause();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
io.print(` ✓ ${validation.detail}`);
|
|
564
|
+
}
|
|
565
|
+
ctx.agentTelegramToken = token;
|
|
566
|
+
await io.pause();
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
const phaseAgentTelegramUserId = {
|
|
570
|
+
id: '25-agent-telegram-user',
|
|
571
|
+
label: 'openlife-agent · allowed Telegram user ID',
|
|
572
|
+
when: (ctx) => isAgent(ctx) && !!ctx.agentTelegramToken,
|
|
573
|
+
async run(ctx, io) {
|
|
574
|
+
io.print('Configure who can message the bot.');
|
|
575
|
+
io.hint('Single-user by design. Get your user ID from @userinfobot.');
|
|
576
|
+
io.print('');
|
|
577
|
+
const userId = await io.text('OPENLIFE_TELEGRAM_ALLOWED_USER_ID', '1344110010');
|
|
578
|
+
ctx.agentTelegramUserId = userId;
|
|
579
|
+
io.print(` ✓ allowed user: ${userId}`);
|
|
580
|
+
await io.pause();
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
const phaseAgentTelegramMode = {
|
|
584
|
+
id: '26-agent-telegram-mode',
|
|
585
|
+
label: 'openlife-agent · Telegram delivery mode',
|
|
586
|
+
when: (ctx) => isAgent(ctx) && !!ctx.agentTelegramToken,
|
|
587
|
+
async run(ctx, io) {
|
|
588
|
+
io.print('Choose Telegram delivery mode.');
|
|
589
|
+
io.hint('auto detects from env (PORT, RAILWAY_*) — picks polling locally, webhook in prod.');
|
|
590
|
+
io.print('');
|
|
591
|
+
const idx = await io.choice('Delivery mode', [
|
|
592
|
+
'auto — detect based on environment (recommended)',
|
|
593
|
+
'polling — long-poll (good for local dev, NAT-friendly)',
|
|
594
|
+
'webhook — push (requires public URL, good for Railway/Render)',
|
|
595
|
+
], 0);
|
|
596
|
+
const mode = idx === 0 ? 'auto' : idx === 1 ? 'polling' : 'webhook';
|
|
597
|
+
ctx.agentTelegramMode = mode;
|
|
598
|
+
io.print(` ✓ mode: ${mode}`);
|
|
599
|
+
await io.pause();
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
const phaseAgentServiceMode = {
|
|
603
|
+
id: '27-agent-service-mode',
|
|
604
|
+
label: 'openlife-agent · supervisor / service mode',
|
|
605
|
+
when: isAgent,
|
|
606
|
+
async run(ctx, io) {
|
|
607
|
+
io.print('How should the daemon be supervised?');
|
|
608
|
+
io.hint('manual = no supervisor (you run `openlife agent start` yourself).');
|
|
609
|
+
io.print('');
|
|
610
|
+
const idx = await io.choice('Service mode', [
|
|
611
|
+
'manual — run by hand with `openlife agent start --daemon`',
|
|
612
|
+
'nohup — background process via nohup (Linux/macOS)',
|
|
613
|
+
'systemd — register a systemd user unit (Linux)',
|
|
614
|
+
'pm2 — managed by pm2 (multi-platform, requires `pm2` installed)',
|
|
615
|
+
], 0);
|
|
616
|
+
const mode = idx === 0 ? 'manual' : idx === 1 ? 'nohup' : idx === 2 ? 'systemd' : 'pm2';
|
|
617
|
+
ctx.agentServiceMode = mode;
|
|
618
|
+
io.print(` ✓ supervisor: ${mode}`);
|
|
619
|
+
if (mode !== 'manual') {
|
|
620
|
+
ctx.warnings.push(`SERVICE_MODE_${mode.toUpperCase()}: supervisor wiring is a hint — finalize manually as needed`);
|
|
621
|
+
}
|
|
622
|
+
await io.pause();
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
const phaseAgentConfirm = {
|
|
626
|
+
id: '28-agent-confirm',
|
|
627
|
+
label: 'openlife-agent · confirm & install',
|
|
628
|
+
when: isAgent,
|
|
629
|
+
async run(ctx, io) {
|
|
630
|
+
io.divider();
|
|
631
|
+
io.header('Review openlife-agent configuration');
|
|
632
|
+
const authSummary = ctx.agentAuthDecisions
|
|
633
|
+
.map((d) => `${d.provider}=${d.strategy}`)
|
|
634
|
+
.join(', ') || '(none)';
|
|
635
|
+
const keyNames = Object.keys(ctx.agentApiKeys).filter((k) => ctx.agentApiKeys[k]);
|
|
636
|
+
io.print(`Auth decisions : ${authSummary}`);
|
|
637
|
+
io.print(`OAuth flows ran : ${ctx.agentOAuthResults.length} (${ctx.agentOAuthResults.filter((r) => r.ok).length} ok)`);
|
|
638
|
+
io.print(`API keys captured: ${keyNames.length ? keyNames.join(', ') : '(none)'}`);
|
|
639
|
+
io.print(`Telegram token : ${ctx.agentTelegramToken ? '✓ (validated)' : '(none)'}`);
|
|
640
|
+
io.print(`Allowed user : ${ctx.agentTelegramUserId || '(default 1344110010)'}`);
|
|
641
|
+
io.print(`Telegram mode : ${ctx.agentTelegramMode || 'auto'}`);
|
|
642
|
+
io.print(`Service mode : ${ctx.agentServiceMode || 'manual'}`);
|
|
643
|
+
io.print('');
|
|
644
|
+
const ok = await io.yesNo('Apply this configuration?', true);
|
|
645
|
+
if (!ok) {
|
|
646
|
+
ctx.aborted = true;
|
|
647
|
+
ctx.abortReason = 'agent_confirm_rejected';
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// Persist API keys collected by the agent block.
|
|
651
|
+
if (keyNames.length > 0) {
|
|
652
|
+
const { saveApiKeysToEnv } = await Promise.resolve().then(() => __importStar(require('../InstallModules')));
|
|
653
|
+
const keyMap = mapApiKeysForLegacyHelper(ctx.agentApiKeys);
|
|
654
|
+
saveApiKeysToEnv(ctx.root, keyMap);
|
|
655
|
+
io.print(`✓ API keys persisted to ${path.join(ctx.root, '.env')}`);
|
|
656
|
+
}
|
|
657
|
+
// Persist Telegram config + extras that don't fit the legacy helper.
|
|
658
|
+
if (ctx.agentTelegramToken) {
|
|
659
|
+
const { saveTelegramConfig } = await Promise.resolve().then(() => __importStar(require('../InstallModules')));
|
|
660
|
+
saveTelegramConfig(ctx.root, ctx.agentTelegramToken, ctx.agentTelegramUserId);
|
|
661
|
+
writeExtraEnv(ctx.root, {
|
|
662
|
+
OPENLIFE_TELEGRAM_ALLOWED_USER_ID: ctx.agentTelegramUserId || '1344110010',
|
|
663
|
+
OPENLIFE_TELEGRAM_MODE: ctx.agentTelegramMode || 'auto',
|
|
664
|
+
});
|
|
665
|
+
io.print('✓ Telegram config saved to .env');
|
|
666
|
+
}
|
|
667
|
+
// Run InstallFlow for autonomous profile across all chosen hosts.
|
|
668
|
+
// Agent runs across the same hosts the core picked. If core wasn't run,
|
|
669
|
+
// default to the auto-detected host.
|
|
670
|
+
const hosts = ctx.coreHosts.length > 0
|
|
671
|
+
? ctx.coreHosts
|
|
672
|
+
: [((0, InstallFlow_1.detectHostFromEnv)() || InstallFlow_1.DEFAULT_HOST)];
|
|
673
|
+
const { InstallFlow } = await Promise.resolve().then(() => __importStar(require('../InstallFlow')));
|
|
674
|
+
const flow = new InstallFlow(ctx.root);
|
|
675
|
+
for (const host of hosts) {
|
|
676
|
+
io.print('');
|
|
677
|
+
io.print(`▸ Installing agent into host: ${host}`);
|
|
678
|
+
const result = flow.run({
|
|
679
|
+
profile: 'autonomous',
|
|
680
|
+
host,
|
|
681
|
+
skipDoctor: ctx.coreSkipDoctor, // honor the user's earlier choice
|
|
682
|
+
modelOrder: ctx.coreModelChain,
|
|
683
|
+
});
|
|
684
|
+
for (const line of flow.renderSummary(result))
|
|
685
|
+
io.print(' ' + line);
|
|
686
|
+
}
|
|
687
|
+
await io.pause();
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
const phaseAgentVerify = {
|
|
691
|
+
id: '29-agent-verify',
|
|
692
|
+
label: 'openlife-agent · verify',
|
|
693
|
+
when: isAgent,
|
|
694
|
+
async run(ctx, io) {
|
|
695
|
+
io.print('Verifying agent install…');
|
|
696
|
+
const checks = [];
|
|
697
|
+
const manifestPath = path.join(ctx.root, '.openlife', 'install-manifest.json');
|
|
698
|
+
checks.push({
|
|
699
|
+
name: 'install-manifest',
|
|
700
|
+
ok: fs.existsSync(manifestPath),
|
|
701
|
+
detail: manifestPath,
|
|
702
|
+
});
|
|
703
|
+
const envPath = path.join(ctx.root, '.env');
|
|
704
|
+
const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
|
705
|
+
checks.push({
|
|
706
|
+
name: 'telegram-token',
|
|
707
|
+
ok: envContent.includes('TELEGRAM_BOT_TOKEN='),
|
|
708
|
+
detail: 'TELEGRAM_BOT_TOKEN present in .env',
|
|
709
|
+
});
|
|
710
|
+
checks.push({
|
|
711
|
+
name: 'allowed-user',
|
|
712
|
+
ok: envContent.includes('OPENLIFE_TELEGRAM_ALLOWED_USER_ID='),
|
|
713
|
+
detail: 'OPENLIFE_TELEGRAM_ALLOWED_USER_ID present in .env',
|
|
714
|
+
});
|
|
715
|
+
// Probe `openlife status` if the dist build is available.
|
|
716
|
+
try {
|
|
717
|
+
const out = (0, child_process_1.execFileSync)('node', ['bin/openlife.js', 'status', '--json'], {
|
|
718
|
+
cwd: ctx.root,
|
|
719
|
+
encoding: 'utf-8',
|
|
720
|
+
timeout: 10_000,
|
|
721
|
+
}).trim();
|
|
722
|
+
checks.push({
|
|
723
|
+
name: 'openlife-status',
|
|
724
|
+
ok: out.length > 0 && out.startsWith('{'),
|
|
725
|
+
detail: `status responded (${out.length} bytes)`,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
730
|
+
checks.push({
|
|
731
|
+
name: 'openlife-status',
|
|
732
|
+
ok: false,
|
|
733
|
+
detail: `status probe failed: ${msg.slice(0, 120)}`,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
for (const c of checks) {
|
|
737
|
+
if (c.ok)
|
|
738
|
+
io.print(` ✓ ${c.name}: ${c.detail}`);
|
|
739
|
+
else
|
|
740
|
+
io.err(` ${c.name}: ${c.detail}`);
|
|
741
|
+
}
|
|
742
|
+
if (checks.some((c) => !c.ok)) {
|
|
743
|
+
ctx.warnings.push('AGENT_VERIFY_PARTIAL: some checks failed — review .env and run `openlife system doctor`');
|
|
744
|
+
}
|
|
745
|
+
io.print('');
|
|
746
|
+
await io.pause();
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
const phaseAgentComplete = {
|
|
750
|
+
id: '30-agent-complete',
|
|
751
|
+
label: 'openlife-agent · complete',
|
|
752
|
+
when: isAgent,
|
|
753
|
+
async run(ctx, io) {
|
|
754
|
+
io.print('');
|
|
755
|
+
const next = [
|
|
756
|
+
'Run `openlife agent start --daemon` to launch the daemon',
|
|
757
|
+
'Run `openlife agent status` to confirm the heartbeat',
|
|
758
|
+
'Send a message to your Telegram bot to test the gateway',
|
|
759
|
+
];
|
|
760
|
+
if (ctx.agentServiceMode === 'systemd') {
|
|
761
|
+
next.push('See docs/install/openlife-agent.md for the systemd unit template');
|
|
762
|
+
}
|
|
763
|
+
else if (ctx.agentServiceMode === 'pm2') {
|
|
764
|
+
next.push('Run `pm2 start "openlife agent start --daemon" --name openlife-agent`');
|
|
765
|
+
}
|
|
766
|
+
else if (ctx.agentServiceMode === 'nohup') {
|
|
767
|
+
next.push('Run `nohup openlife agent start --daemon > openlife.log 2>&1 &`');
|
|
768
|
+
}
|
|
769
|
+
renderCompletionCard(io, 'openlife-agent installed', next);
|
|
770
|
+
await io.pause();
|
|
771
|
+
},
|
|
772
|
+
};
|
|
773
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
774
|
+
// Phase registry — ordered list consumed by InstallWizardV2 PhaseRunner
|
|
775
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
776
|
+
exports.ALL_PHASES = Object.freeze([
|
|
777
|
+
phaseBanner,
|
|
778
|
+
phaseExistingInstall,
|
|
779
|
+
phaseProductSelection,
|
|
780
|
+
phaseCoreEnter,
|
|
781
|
+
phaseCoreHosts,
|
|
782
|
+
phaseCoreProviders,
|
|
783
|
+
phaseCoreApiKeys,
|
|
784
|
+
phaseCoreModelChain,
|
|
785
|
+
phaseCoreDoctor,
|
|
786
|
+
phaseCoreConfirm,
|
|
787
|
+
phaseCoreComplete,
|
|
788
|
+
phaseAgentEnter,
|
|
789
|
+
phaseAgentAuthStrategy,
|
|
790
|
+
phaseAgentOAuth,
|
|
791
|
+
phaseAgentApiKeys,
|
|
792
|
+
phaseAgentTelegramToken,
|
|
793
|
+
phaseAgentTelegramUserId,
|
|
794
|
+
phaseAgentTelegramMode,
|
|
795
|
+
phaseAgentServiceMode,
|
|
796
|
+
phaseAgentConfirm,
|
|
797
|
+
phaseAgentVerify,
|
|
798
|
+
phaseAgentComplete,
|
|
799
|
+
]);
|
|
800
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
801
|
+
// Helpers
|
|
802
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
803
|
+
function hostDescription(host) {
|
|
804
|
+
switch (host) {
|
|
805
|
+
case 'claude-code': return 'Anthropic Claude Code (fully supported)';
|
|
806
|
+
case 'gemini-cli': return 'Google Gemini CLI';
|
|
807
|
+
case 'codex': return 'OpenAI Codex CLI';
|
|
808
|
+
default: return host;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Maps the new ProviderId-keyed ApiKeyMap to the legacy InstallModules
|
|
813
|
+
* { openai, anthropic, gemini, openrouter, elevenlabs } shape so we can
|
|
814
|
+
* reuse `saveApiKeysToEnv` without refactoring it.
|
|
815
|
+
*
|
|
816
|
+
* Providers not in the legacy map still get their env var written by writeExtraEnv.
|
|
817
|
+
*/
|
|
818
|
+
function mapApiKeysForLegacyHelper(input) {
|
|
819
|
+
const out = {};
|
|
820
|
+
if (input['openai-api'])
|
|
821
|
+
out.openai = input['openai-api'];
|
|
822
|
+
if (input['anthropic'])
|
|
823
|
+
out.anthropic = input['anthropic'];
|
|
824
|
+
if (input['gemini-api'])
|
|
825
|
+
out.gemini = input['gemini-api'];
|
|
826
|
+
if (input['openrouter'])
|
|
827
|
+
out.openrouter = input['openrouter'];
|
|
828
|
+
return out;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Append-or-replace env vars that don't fit the legacy ApiKeyMap.
|
|
832
|
+
* Read .env, strip matching lines, write back with new values appended.
|
|
833
|
+
*/
|
|
834
|
+
function writeExtraEnv(root, kv) {
|
|
835
|
+
const envPath = path.join(root, '.env');
|
|
836
|
+
const current = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
|
837
|
+
const keys = new Set(Object.keys(kv));
|
|
838
|
+
const lines = current
|
|
839
|
+
.split('\n')
|
|
840
|
+
.filter((l) => {
|
|
841
|
+
const eq = l.indexOf('=');
|
|
842
|
+
if (eq < 0)
|
|
843
|
+
return l.length > 0;
|
|
844
|
+
return !keys.has(l.slice(0, eq));
|
|
845
|
+
});
|
|
846
|
+
for (const [k, v] of Object.entries(kv)) {
|
|
847
|
+
lines.push(`${k}=${v}`);
|
|
848
|
+
}
|
|
849
|
+
fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf-8');
|
|
850
|
+
}
|
|
851
|
+
function renderCompletionCard(io, title, nextSteps) {
|
|
852
|
+
const useColor = (0, MatrixTheme_1.supportsColor)();
|
|
853
|
+
const head = useColor ? MatrixTheme_1.MATRIX.head : '';
|
|
854
|
+
const body = useColor ? MatrixTheme_1.MATRIX.body : '';
|
|
855
|
+
const reset = useColor ? MatrixTheme_1.ANSI.reset : '';
|
|
856
|
+
io.divider();
|
|
857
|
+
io.print(`${head}✓ ${title}${reset}`);
|
|
858
|
+
io.print('');
|
|
859
|
+
io.print('Next steps:');
|
|
860
|
+
for (const step of nextSteps) {
|
|
861
|
+
io.print(` ${body}• ${step}${reset}`);
|
|
862
|
+
}
|
|
863
|
+
io.print('');
|
|
864
|
+
}
|