@openlife/cli 1.7.5 → 1.7.7

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,
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseDurationToSeconds = parseDurationToSeconds;
37
+ exports.collectLogs = collectLogs;
38
+ exports.renderLogsHuman = renderLogsHuman;
39
+ exports.renderLogsJson = renderLogsJson;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const DEFAULT_TAIL = 20;
43
+ const KNOWN_JSONL_FILES = [
44
+ 'governance-ledger.jsonl',
45
+ 'media-routing.log.jsonl',
46
+ 'capability-lifecycle.jsonl',
47
+ 'canonization-log.jsonl',
48
+ ];
49
+ function parseDurationToSeconds(input) {
50
+ const trimmed = input.trim().toLowerCase();
51
+ const m = trimmed.match(/^(\d+)\s*(s|m|h|d)$/);
52
+ if (!m)
53
+ return null;
54
+ const n = Number.parseInt(m[1], 10);
55
+ if (!Number.isFinite(n) || n < 0)
56
+ return null;
57
+ const unit = m[2];
58
+ const mult = unit === 's' ? 1 : unit === 'm' ? 60 : unit === 'h' ? 3600 : 86400;
59
+ return n * mult;
60
+ }
61
+ function entrySubsystem(filePath) {
62
+ const base = path.basename(filePath);
63
+ return base.replace(/\.log\.jsonl$/, '').replace(/\.jsonl$/, '');
64
+ }
65
+ function extractTs(parsed, fallbackMtime) {
66
+ if (!parsed)
67
+ return fallbackMtime;
68
+ if (typeof parsed.ts === 'string') {
69
+ const t = Date.parse(parsed.ts);
70
+ if (!Number.isNaN(t))
71
+ return t;
72
+ }
73
+ if (typeof parsed.ts === 'number' && Number.isFinite(parsed.ts)) {
74
+ return parsed.ts > 1e12 ? parsed.ts : parsed.ts * 1000;
75
+ }
76
+ if (typeof parsed.timestamp === 'string') {
77
+ const t = Date.parse(parsed.timestamp);
78
+ if (!Number.isNaN(t))
79
+ return t;
80
+ }
81
+ return fallbackMtime;
82
+ }
83
+ function summarizeEntry(parsed) {
84
+ if (!parsed)
85
+ return '<unparseable line>';
86
+ const candidates = [
87
+ parsed.summary,
88
+ parsed.decision?.auditSummary,
89
+ parsed.message,
90
+ parsed.action,
91
+ parsed.event,
92
+ parsed.type,
93
+ ];
94
+ for (const c of candidates) {
95
+ if (typeof c === 'string' && c.trim().length > 0) {
96
+ return c.length > 140 ? `${c.slice(0, 137)}...` : c;
97
+ }
98
+ }
99
+ // Fall back to a one-line preview of the parsed object.
100
+ const compact = JSON.stringify(parsed);
101
+ return compact.length > 140 ? `${compact.slice(0, 137)}...` : compact;
102
+ }
103
+ function listJsonlFiles(stateDir) {
104
+ if (!fs.existsSync(stateDir))
105
+ return [];
106
+ try {
107
+ const entries = fs.readdirSync(stateDir);
108
+ const found = [];
109
+ for (const e of entries) {
110
+ if (KNOWN_JSONL_FILES.includes(e) || e.endsWith('.jsonl')) {
111
+ found.push(path.join(stateDir, e));
112
+ }
113
+ }
114
+ return found;
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ }
120
+ function collectLogs(opts = {}) {
121
+ const root = opts.root || process.cwd();
122
+ const stateDir = process.env.OPENLIFE_STATE_DIR
123
+ ? path.resolve(process.env.OPENLIFE_STATE_DIR)
124
+ : path.join(root, '.openlife');
125
+ const files = listJsonlFiles(stateDir);
126
+ const filter = opts.filter ? opts.filter.toLowerCase() : null;
127
+ const tail = opts.tail && opts.tail > 0 ? opts.tail : DEFAULT_TAIL;
128
+ const sinceSeconds = opts.since ? parseDurationToSeconds(opts.since) : null;
129
+ const minTs = sinceSeconds !== null ? Date.now() - sinceSeconds * 1000 : null;
130
+ const out = [];
131
+ for (const fp of files) {
132
+ const subsystem = entrySubsystem(fp);
133
+ if (filter && !subsystem.toLowerCase().includes(filter))
134
+ continue;
135
+ let mtime = Date.now();
136
+ try {
137
+ mtime = fs.statSync(fp).mtimeMs;
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ let raw = '';
143
+ try {
144
+ raw = fs.readFileSync(fp, 'utf-8');
145
+ }
146
+ catch {
147
+ continue;
148
+ }
149
+ const lines = raw.split('\n').filter((l) => l.trim().length > 0);
150
+ for (const line of lines) {
151
+ let parsed = null;
152
+ try {
153
+ parsed = JSON.parse(line);
154
+ }
155
+ catch {
156
+ parsed = null;
157
+ }
158
+ const ts = extractTs(parsed, mtime);
159
+ if (minTs !== null && ts < minTs)
160
+ continue;
161
+ out.push({ subsystem, rawLine: line, parsed, ts });
162
+ }
163
+ }
164
+ out.sort((a, b) => a.ts - b.ts);
165
+ return out.slice(-tail);
166
+ }
167
+ function renderLogsHuman(entries) {
168
+ if (entries.length === 0)
169
+ return '(no entries match filter)';
170
+ return entries
171
+ .map((e) => {
172
+ const tsIso = new Date(e.ts).toISOString();
173
+ return `${tsIso} [${e.subsystem}] ${summarizeEntry(e.parsed)}`;
174
+ })
175
+ .join('\n');
176
+ }
177
+ function renderLogsJson(entries) {
178
+ return entries
179
+ .map((e) => JSON.stringify({ ts: new Date(e.ts).toISOString(), subsystem: e.subsystem, entry: e.parsed ?? e.rawLine }))
180
+ .join('\n');
181
+ }
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HEARTBEAT_STALE_S = exports.HEARTBEAT_FRESH_S = void 0;
37
+ exports.buildStatusReport = buildStatusReport;
38
+ exports.renderStatusReport = renderStatusReport;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ // ============================================================================
42
+ // `openlife status` — consolidated runtime state report.
43
+ //
44
+ // Aggregates the 6 most operationally useful files under `.openlife/`:
45
+ // - install-manifest.json → installed profile + providers detected
46
+ // - heartbeat.json → daemon uptime + last beat timestamp
47
+ // - runtime-health.json → per-executor failure budget + cooldown
48
+ // - runtime-policy-status.json → per-executor availability decision
49
+ // - agent-queue.json → queued objectives + last flush
50
+ // - governance-ledger.jsonl → tail entry hash + chain length
51
+ //
52
+ // Computes an `overall` rollup:
53
+ // - "healthy" → daemon beat within HEARTBEAT_FRESH_S AND at least one
54
+ // executor available AND governance ledger parseable
55
+ // - "degraded" → heartbeat stale (FRESH..STALE) OR some executors down
56
+ // - "down" → heartbeat older than STALE OR all executors down
57
+ //
58
+ // Returns JSON. The CLI wrapper handles --watch (re-print every 5s).
59
+ // ============================================================================
60
+ exports.HEARTBEAT_FRESH_S = 60;
61
+ exports.HEARTBEAT_STALE_S = 300;
62
+ function readJsonSafe(filePath) {
63
+ try {
64
+ if (!fs.existsSync(filePath))
65
+ return null;
66
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ function readJsonlTailSafe(filePath, maxLines = 1) {
73
+ try {
74
+ if (!fs.existsSync(filePath))
75
+ return { count: 0 };
76
+ const raw = fs.readFileSync(filePath, 'utf-8');
77
+ const lines = raw.split('\n').filter((l) => l.trim().length > 0);
78
+ if (lines.length === 0)
79
+ return { count: 0 };
80
+ const tail = lines.slice(-Math.max(1, maxLines));
81
+ const lastRaw = tail[tail.length - 1];
82
+ try {
83
+ return { count: lines.length, last: JSON.parse(lastRaw) };
84
+ }
85
+ catch {
86
+ return { count: lines.length };
87
+ }
88
+ }
89
+ catch {
90
+ return { count: 0 };
91
+ }
92
+ }
93
+ function readPackageVersion(packageRoot) {
94
+ const candidate = path.join(packageRoot, 'package.json');
95
+ const pkg = readJsonSafe(candidate);
96
+ return pkg?.version || '0.0.0';
97
+ }
98
+ function normalizeExecutors(policyStatus, runtimeHealth) {
99
+ const result = {};
100
+ const policy = (policyStatus?.executors ?? {});
101
+ for (const [name, raw] of Object.entries(policy)) {
102
+ const r = raw;
103
+ result[name] = {
104
+ executor: name,
105
+ available: Boolean(r.available),
106
+ reason: typeof r.reason === 'string' ? r.reason : undefined,
107
+ category: typeof r.category === 'string' ? r.category : undefined,
108
+ updatedAt: typeof r.updatedAt === 'string' ? r.updatedAt : undefined,
109
+ };
110
+ }
111
+ if (runtimeHealth) {
112
+ for (const [name, raw] of Object.entries(runtimeHealth)) {
113
+ if (name === 'type' || name === 'status' || name === 'updatedAt')
114
+ continue;
115
+ const r = raw;
116
+ const current = result[name] || { executor: name, available: true };
117
+ if (typeof r.failures === 'number')
118
+ current.failures = r.failures;
119
+ if (typeof r.until === 'string')
120
+ current.cooldownUntil = r.until;
121
+ if (!current.reason && typeof r.reason === 'string')
122
+ current.reason = r.reason;
123
+ result[name] = current;
124
+ }
125
+ }
126
+ return Object.values(result);
127
+ }
128
+ function deriveOverall(report) {
129
+ const notes = [];
130
+ const age = report.heartbeat.ageSeconds;
131
+ let heartbeatTier = 'fresh';
132
+ if (!report.heartbeat.present || age === null) {
133
+ heartbeatTier = 'dead';
134
+ notes.push('no_heartbeat');
135
+ }
136
+ else if (age > exports.HEARTBEAT_STALE_S) {
137
+ heartbeatTier = 'dead';
138
+ notes.push(`heartbeat_stale_${age}s`);
139
+ }
140
+ else if (age > exports.HEARTBEAT_FRESH_S) {
141
+ heartbeatTier = 'stale';
142
+ notes.push(`heartbeat_warning_${age}s`);
143
+ }
144
+ const executors = report.executors;
145
+ const anyExecutor = executors.length > 0;
146
+ const anyAvailable = executors.some((e) => e.available);
147
+ if (anyExecutor && !anyAvailable) {
148
+ notes.push('all_executors_down');
149
+ }
150
+ if (anyExecutor && !executors.every((e) => e.available)) {
151
+ const down = executors.filter((e) => !e.available).map((e) => e.executor);
152
+ notes.push(`executors_down:${down.join(',')}`);
153
+ }
154
+ if (!report.governance.ledgerPresent) {
155
+ notes.push('no_governance_ledger');
156
+ }
157
+ let overall;
158
+ if (heartbeatTier === 'dead' || (anyExecutor && !anyAvailable)) {
159
+ overall = 'down';
160
+ }
161
+ else if (heartbeatTier === 'stale' || (anyExecutor && !executors.every((e) => e.available))) {
162
+ overall = 'degraded';
163
+ }
164
+ else {
165
+ overall = 'healthy';
166
+ }
167
+ return { overall, notes };
168
+ }
169
+ function buildStatusReport(root = process.cwd(), packageRoot) {
170
+ const stateDir = process.env.OPENLIFE_STATE_DIR
171
+ ? path.resolve(process.env.OPENLIFE_STATE_DIR)
172
+ : path.join(root, '.openlife');
173
+ const heartbeat = readJsonSafe(path.join(stateDir, 'heartbeat.json'));
174
+ const runtimeHealth = readJsonSafe(path.join(stateDir, 'runtime-health.json'));
175
+ const policyStatus = readJsonSafe(path.join(stateDir, 'runtime-policy-status.json'));
176
+ const queue = readJsonSafe(path.join(stateDir, 'agent-queue.json'));
177
+ const manifest = readJsonSafe(path.join(stateDir, 'install-manifest.json'));
178
+ const ledgerTail = readJsonlTailSafe(path.join(stateDir, 'governance-ledger.jsonl'), 1);
179
+ const lastLedger = ledgerTail.last;
180
+ const ageSeconds = heartbeat?.ts ? Math.floor((Date.now() - heartbeat.ts) / 1000) : null;
181
+ const uptimeSeconds = heartbeat?.startedAt ? Math.floor((Date.now() - heartbeat.startedAt) / 1000) : null;
182
+ const partial = {
183
+ ts: new Date().toISOString(),
184
+ version: readPackageVersion(packageRoot || path.join(__dirname, '..', '..')),
185
+ profile: manifest?.profile ?? null,
186
+ uptimeSeconds,
187
+ heartbeat: {
188
+ present: Boolean(heartbeat),
189
+ pid: heartbeat?.pid,
190
+ host: heartbeat?.host,
191
+ ageSeconds,
192
+ fresh: ageSeconds !== null && ageSeconds <= exports.HEARTBEAT_FRESH_S,
193
+ },
194
+ governance: {
195
+ ledgerPresent: ledgerTail.count > 0,
196
+ entryCount: ledgerTail.count,
197
+ lastEntryHash: lastLedger?.entryHash,
198
+ lastEntryTs: lastLedger?.ts,
199
+ },
200
+ queue: {
201
+ depth: Array.isArray(queue?.queuedObjectives) ? queue.queuedObjectives.length : 0,
202
+ lastFlushedAt: queue?.lastFlushedAt,
203
+ },
204
+ executors: normalizeExecutors(policyStatus, runtimeHealth),
205
+ install: {
206
+ installedAt: manifest?.installedAt,
207
+ providers: manifest?.providers,
208
+ clis: manifest?.clis,
209
+ hasTelegramToken: manifest?.envChecks?.hasTelegramToken,
210
+ },
211
+ };
212
+ const { overall, notes } = deriveOverall(partial);
213
+ return { ...partial, overall, notes };
214
+ }
215
+ function renderStatusReport(report) {
216
+ return JSON.stringify(report, null, 2);
217
+ }
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.
@@ -327,6 +338,50 @@ program.command('init')
327
338
  }
328
339
  });
329
340
  // ============================================================================
341
+ // OBSERVABILITY — `openlife status`, `openlife logs`
342
+ // ============================================================================
343
+ program.command('status')
344
+ .description('Consolidated runtime state (heartbeat + executors + governance + queue) as JSON')
345
+ .option('--watch', 're-print every 5 seconds (Ctrl-C to stop)', false)
346
+ .action(async (options) => {
347
+ const { buildStatusReport, renderStatusReport } = require('./cli/StatusCommand');
348
+ const printOnce = () => {
349
+ const report = buildStatusReport(process.cwd());
350
+ console.log(renderStatusReport(report));
351
+ if (report.overall !== 'healthy')
352
+ process.exitCode = 1;
353
+ };
354
+ if (!options.watch) {
355
+ printOnce();
356
+ return;
357
+ }
358
+ printOnce();
359
+ const interval = setInterval(() => {
360
+ console.log('---');
361
+ printOnce();
362
+ }, 5000);
363
+ process.on('SIGINT', () => {
364
+ clearInterval(interval);
365
+ process.exit(0);
366
+ });
367
+ });
368
+ program.command('logs')
369
+ .description('Readable tail of .openlife/*.jsonl event streams')
370
+ .option('--tail <N>', 'number of entries to show', '20')
371
+ .option('--since <duration>', 'only entries newer than e.g. 5m | 1h | 2d')
372
+ .option('--filter <subsystem>', 'match subsystem (governance, media-routing, capability-lifecycle, canonization-log)')
373
+ .option('--json', 'emit one JSON object per line instead of human-readable text', false)
374
+ .action((options) => {
375
+ const { collectLogs, renderLogsHuman, renderLogsJson } = require('./cli/LogsCommand');
376
+ const tail = Number.parseInt(options.tail || '20', 10);
377
+ const entries = collectLogs({
378
+ tail: Number.isFinite(tail) && tail > 0 ? tail : 20,
379
+ since: options.since,
380
+ filter: options.filter,
381
+ });
382
+ console.log(options.json ? renderLogsJson(entries) : renderLogsHuman(entries));
383
+ });
384
+ // ============================================================================
330
385
  // AUTENTICAÇÃO (AUTH)
331
386
  // ============================================================================
332
387
  const authCmd = program.command('auth').description('Gerencia a autenticação nativa sem uso de API Keys');