@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.
- package/dist/cli/InstallModules.js +37 -0
- package/dist/cli/InstallWizard.js +46 -8
- package/dist/cli/LogsCommand.js +181 -0
- package/dist/cli/StatusCommand.js +217 -0
- package/dist/index.js +55 -0
- package/dist/test_install_wizard.js +112 -21
- package/dist/test_logs_command.js +177 -0
- package/dist/test_status_command.js +218 -0
- package/docs/getting-started.md +137 -0
- package/package.json +5 -2
|
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.runPrecheck = runPrecheck;
|
|
37
37
|
exports.maskToken = maskToken;
|
|
38
|
+
exports.providedApiKeyEnvNames = providedApiKeyEnvNames;
|
|
39
|
+
exports.saveApiKeysToEnv = saveApiKeysToEnv;
|
|
38
40
|
exports.saveTelegramConfig = saveTelegramConfig;
|
|
39
41
|
exports.validateTelegramToken = validateTelegramToken;
|
|
40
42
|
exports.checkTelegram409Conflict = checkTelegram409Conflict;
|
|
@@ -71,6 +73,41 @@ function maskToken(token) {
|
|
|
71
73
|
return '***';
|
|
72
74
|
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
|
73
75
|
}
|
|
76
|
+
const API_KEY_ENV_MAP = {
|
|
77
|
+
openai: 'OPENAI_API_KEY',
|
|
78
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
79
|
+
gemini: 'GEMINI_API_KEY',
|
|
80
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
81
|
+
elevenlabs: 'ELEVENLABS_API_KEY',
|
|
82
|
+
};
|
|
83
|
+
function providedApiKeyEnvNames(keys) {
|
|
84
|
+
return Object.keys(keys)
|
|
85
|
+
.filter((k) => typeof keys[k] === 'string' && keys[k].trim().length > 0)
|
|
86
|
+
.map((k) => API_KEY_ENV_MAP[k]);
|
|
87
|
+
}
|
|
88
|
+
function saveApiKeysToEnv(root, keys) {
|
|
89
|
+
const envPath = path.join(root, '.env');
|
|
90
|
+
const current = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
|
91
|
+
const provided = Object.keys(keys)
|
|
92
|
+
.filter((k) => typeof keys[k] === 'string' && keys[k].trim().length > 0);
|
|
93
|
+
if (provided.length === 0)
|
|
94
|
+
return { saved: [], path: envPath };
|
|
95
|
+
const envVars = new Set(provided.map((k) => API_KEY_ENV_MAP[k]));
|
|
96
|
+
const lines = current
|
|
97
|
+
.split('\n')
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.filter((l) => {
|
|
100
|
+
const eq = l.indexOf('=');
|
|
101
|
+
if (eq < 0)
|
|
102
|
+
return true;
|
|
103
|
+
return !envVars.has(l.slice(0, eq));
|
|
104
|
+
});
|
|
105
|
+
for (const k of provided) {
|
|
106
|
+
lines.push(`${API_KEY_ENV_MAP[k]}=${keys[k].trim()}`);
|
|
107
|
+
}
|
|
108
|
+
fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf-8');
|
|
109
|
+
return { saved: provided.map((k) => API_KEY_ENV_MAP[k]), path: envPath };
|
|
110
|
+
}
|
|
74
111
|
function saveTelegramConfig(root, token, chatId) {
|
|
75
112
|
const envPath = path.join(root, '.env');
|
|
76
113
|
const current = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
|
@@ -185,9 +185,15 @@ class InstallWizard {
|
|
|
185
185
|
}
|
|
186
186
|
preExistingAction = idx === 1 ? 'reinstall' : 'repair';
|
|
187
187
|
}
|
|
188
|
-
// 2. Profile selection
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
// 2. Profile selection — 3 user-visible options. `both` is an alias for
|
|
189
|
+
// `autonomous` since autonomous already includes the framework layer
|
|
190
|
+
// (systemInstaller.install() runs unconditionally in InstallFlow.run()).
|
|
191
|
+
const profileIdx = await this.answers.choice('Which profile do you want to install?\n framework — local interactive CLI only\n autonomous — long-running daemon with Telegram gateway and governance\n both — autonomous daemon plus framework CLI (recommended for full setup)', ['framework', 'autonomous', 'both'], 0);
|
|
192
|
+
const profile = profileIdx === 0 ? 'framework' : 'autonomous';
|
|
193
|
+
const installBoth = profileIdx === 2;
|
|
194
|
+
if (installBoth) {
|
|
195
|
+
warnings.push('INSTALLING_BOTH: autonomous profile selected (includes framework layer)');
|
|
196
|
+
}
|
|
191
197
|
// 3. Host selection — auto-detect, but always ask to confirm.
|
|
192
198
|
const detected = (0, InstallFlow_1.detectHostFromEnv)();
|
|
193
199
|
const hostOptions = [...InstallFlow_1.VALID_HOSTS];
|
|
@@ -207,25 +213,53 @@ class InstallWizard {
|
|
|
207
213
|
if (validatedHost !== 'claude-code') {
|
|
208
214
|
warnings.push(`HOST_NOT_YET_SUPPORTED: ${validatedHost} host-specific install is a no-op for now; .openlife state will still be created`);
|
|
209
215
|
}
|
|
210
|
-
// 4.
|
|
216
|
+
// 4. API keys (optional) — each prompt accepts paste or blank to skip.
|
|
217
|
+
// IMPORTANT: collect first, but only persist after final confirmation.
|
|
218
|
+
// An aborted wizard must not mutate .env or write pasted secrets.
|
|
219
|
+
let savedApiKeyNames = [];
|
|
220
|
+
const collectedApiKeys = {};
|
|
221
|
+
const wantsApiKeys = await this.answers.yesNo('Configure LLM API keys now? (You can also set them later in .env)', true);
|
|
222
|
+
if (wantsApiKeys) {
|
|
223
|
+
collectedApiKeys.openai = await this.answers.text('OPENAI_API_KEY (paste or Enter to skip)', '');
|
|
224
|
+
collectedApiKeys.anthropic = await this.answers.text('ANTHROPIC_API_KEY (paste or Enter to skip)', '');
|
|
225
|
+
collectedApiKeys.gemini = await this.answers.text('GEMINI_API_KEY (paste or Enter to skip)', '');
|
|
226
|
+
collectedApiKeys.openrouter = await this.answers.text('OPENROUTER_API_KEY (paste or Enter to skip)', '');
|
|
227
|
+
savedApiKeyNames = (0, InstallModules_1.providedApiKeyEnvNames)(collectedApiKeys);
|
|
228
|
+
if (savedApiKeyNames.length === 0) {
|
|
229
|
+
warnings.push('NO_API_KEYS_PROVIDED: model providers will only work via OAuth CLIs or Ollama until keys are added to .env');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
warnings.push('SKIPPED_API_KEYS: configure providers later via .env or `openlife auth <provider>`');
|
|
234
|
+
}
|
|
235
|
+
// 5. OAuth pointer — informational only. The actual `openlife auth gemini`
|
|
236
|
+
// / `auth openai` flows are interactive subprocesses; running them inside
|
|
237
|
+
// the wizard would break the canned-answer test seam. We just signal the
|
|
238
|
+
// operator that the flag exists so they can opt-in after init completes.
|
|
239
|
+
const wantsOAuth = await this.answers.yesNo('Plan to configure OAuth for Gemini or OpenAI CLI later?', false);
|
|
240
|
+
if (wantsOAuth) {
|
|
241
|
+
warnings.push('OAUTH_PENDING: after init completes, run `openlife auth gemini` and/or `openlife auth openai` to log in');
|
|
242
|
+
}
|
|
243
|
+
// 6. Model chain (optional)
|
|
211
244
|
const modelsRaw = await this.answers.text('LLM model order (comma-separated provider/model chain, blank = defaults)', '');
|
|
212
245
|
const modelOrder = modelsRaw
|
|
213
246
|
? modelsRaw.split(',').map((m) => m.trim()).filter(Boolean)
|
|
214
247
|
: undefined;
|
|
215
|
-
//
|
|
248
|
+
// 7. Telegram (autonomous only)
|
|
216
249
|
if (profile === 'autonomous') {
|
|
217
250
|
const hasTelegram = await this.answers.yesNo('Do you already have TELEGRAM_BOT_TOKEN and OPENLIFE_TELEGRAM_ALLOWED_USER_ID set in your .env?', false);
|
|
218
251
|
if (!hasTelegram) {
|
|
219
252
|
warnings.push('TELEGRAM_NOT_CONFIGURED: autonomous profile needs TELEGRAM_BOT_TOKEN and OPENLIFE_TELEGRAM_ALLOWED_USER_ID — set them in .env before running `openlife agent start`');
|
|
220
253
|
}
|
|
221
254
|
}
|
|
222
|
-
//
|
|
255
|
+
// 8. Skip doctor?
|
|
223
256
|
const skipDoctor = await this.answers.yesNo('Skip the system doctor run?', false);
|
|
224
|
-
//
|
|
257
|
+
// 9. Confirm preview
|
|
225
258
|
const previewLines = [
|
|
226
259
|
'Review your choices:',
|
|
227
|
-
` profile : ${profile}`,
|
|
260
|
+
` profile : ${profile}${installBoth ? ' (both — framework + autonomous)' : ''}`,
|
|
228
261
|
` host : ${validatedHost}`,
|
|
262
|
+
` apiKeys : ${savedApiKeyNames.length ? savedApiKeyNames.join(', ') : '(none saved)'}`,
|
|
229
263
|
` modelOrder : ${modelOrder ? modelOrder.join(', ') : '(defaults)'}`,
|
|
230
264
|
` skipDoctor : ${skipDoctor}`,
|
|
231
265
|
preExistingAction ? ` preExisting : ${preExistingAction}` : ''
|
|
@@ -239,6 +273,10 @@ class InstallWizard {
|
|
|
239
273
|
if (!confirmed) {
|
|
240
274
|
return { ok: false, reason: 'user_aborted', detail: 'preview_not_confirmed' };
|
|
241
275
|
}
|
|
276
|
+
if (savedApiKeyNames.length > 0) {
|
|
277
|
+
const saved = (0, InstallModules_1.saveApiKeysToEnv)(this.root, collectedApiKeys);
|
|
278
|
+
savedApiKeyNames = saved.saved;
|
|
279
|
+
}
|
|
242
280
|
const options = {
|
|
243
281
|
profile,
|
|
244
282
|
host: validatedHost,
|
|
@@ -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');
|