@sbains2/lifeos 0.1.3 → 0.2.0
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/README.md +14 -6
- package/bin/lifeos.js +46 -17
- package/package.json +1 -1
- package/src/commands/doctor.js +78 -0
- package/src/doctor.js +329 -0
- package/src/index.js +144 -15
- package/src/mcp-config.js +231 -0
- package/templates/SCHEMA.md +32 -1
- package/templates/mcps/registry.json +302 -133
package/README.md
CHANGED
|
@@ -46,14 +46,22 @@ See [../docs/TRUST.md](../docs/TRUST.md) for the full commitments.
|
|
|
46
46
|
|
|
47
47
|
After the wizard prints `Done in Xm Ys.`, here's the path from "scaffold exists" to "council convenes on real artifacts":
|
|
48
48
|
|
|
49
|
-
### 1.
|
|
49
|
+
### 1. Set env vars + restart Claude (~3-5 min the first time)
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
As of v0.1.5, the wizard does the heavy lifting for you:
|
|
52
52
|
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
|
|
53
|
+
- **`.mcp.json` is already written** in your scaffold dir — Claude Code, Cursor, and Cline auto-detect this
|
|
54
|
+
- **Claude Desktop config was offered for merge** (with backup) if you said yes
|
|
55
|
+
- **Per-MCP setup status was already printed** at the end of the wizard — read it carefully
|
|
56
|
+
|
|
57
|
+
What you still need to do, listed in the wizard's status report:
|
|
58
|
+
|
|
59
|
+
- **For `needs_env` MCPs:** set the env var in your shell (`export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_...`) — the wizard surfaced exactly which env vars + the URL to get each one. Add to your `~/.zshrc` or `~/.bashrc` for persistence.
|
|
60
|
+
- **For `needs_oauth` MCPs:** Claude will trigger the OAuth flow on first invocation — usually opens a browser, you click Authorize, done.
|
|
61
|
+
- **For `manual` MCPs:** read `.lifeos/INSTALL_MCPS.md` for install hints; these are MCPs without a single-command auto-install (e.g., Canvas, Discord, Apple Notes). One-time clone + build per repo.
|
|
62
|
+
- **For `risk` MCPs (only LinkedIn at v0.1.5):** if you said "yes, include in auto-config" the entry was wired; if "no" the MCP is in your `lifeos.config.json` but skipped from `.mcp.json` — install manually.
|
|
63
|
+
|
|
64
|
+
**Then restart Claude Desktop** (Cmd+Q, then reopen) so the new MCPs load. For Claude Code, restart your terminal or run `claude /mcp` to see the wired list.
|
|
57
65
|
|
|
58
66
|
### 2. Open the folder in Claude Code
|
|
59
67
|
|
package/bin/lifeos.js
CHANGED
|
@@ -1,41 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run } from '../src/index.js';
|
|
3
|
+
import { runDoctorCommand } from '../src/commands/doctor.js';
|
|
3
4
|
|
|
4
|
-
// Parse argv: positional target dir + flags
|
|
5
|
-
// Usage:
|
|
5
|
+
// Parse argv: optional subcommand + positional target dir + flags
|
|
6
|
+
// Usage:
|
|
7
|
+
// lifeos [target-dir] [--minimal] # wizard (default)
|
|
8
|
+
// lifeos doctor [target-dir] [--json] [--quiet] [--strict]
|
|
6
9
|
const args = process.argv.slice(2);
|
|
7
10
|
const flags = new Set(args.filter((a) => a.startsWith('--')));
|
|
8
|
-
const positional = args.filter((a) => !a.startsWith('--'));
|
|
11
|
+
const positional = args.filter((a) => !a.startsWith('--') && !a.startsWith('-'));
|
|
9
12
|
|
|
10
|
-
if (flags.has('--help') || flags.has('-h')) {
|
|
11
|
-
console.log(`lifeos — scaffold a personal LifeOS
|
|
13
|
+
if (flags.has('--help') || flags.has('-h') || positional[0] === 'help') {
|
|
14
|
+
console.log(`lifeos — scaffold and verify a personal LifeOS
|
|
12
15
|
|
|
13
16
|
Usage:
|
|
14
|
-
lifeos [target-dir] [options]
|
|
17
|
+
lifeos [target-dir] [options] # run the wizard
|
|
18
|
+
lifeos doctor [target-dir] [options] # verify scaffold health
|
|
19
|
+
lifeos --help # show this
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
Wizard options:
|
|
17
22
|
--minimal Skip optional substantive prompts (longer-arc focus,
|
|
18
23
|
quadrant/council/MCP confirmations). Uses template
|
|
19
24
|
defaults everywhere. Target install time: < 60 seconds.
|
|
20
|
-
|
|
25
|
+
|
|
26
|
+
Doctor options:
|
|
27
|
+
--json Emit machine-readable JSON (for CI / scripting)
|
|
28
|
+
--quiet Suppress ✓ lines, only show warnings and errors
|
|
29
|
+
--strict Exit code 1 even on warnings (default: warnings are 0)
|
|
21
30
|
|
|
22
31
|
Arguments:
|
|
23
|
-
target-dir Directory to scaffold into (default: current working dir)
|
|
32
|
+
target-dir Directory to scaffold into / verify (default: current working dir)
|
|
24
33
|
|
|
25
34
|
Examples:
|
|
26
|
-
lifeos
|
|
27
|
-
lifeos ~/my-lifeos
|
|
28
|
-
lifeos ~/my-lifeos --minimal
|
|
35
|
+
lifeos # scaffold into current dir, full wizard
|
|
36
|
+
lifeos ~/my-lifeos # scaffold into ~/my-lifeos
|
|
37
|
+
lifeos ~/my-lifeos --minimal # speed-optimized install
|
|
38
|
+
lifeos doctor # verify current dir
|
|
39
|
+
lifeos doctor ~/my-lifeos # verify ~/my-lifeos
|
|
40
|
+
lifeos doctor --json | jq '.summary' # programmatic check
|
|
29
41
|
`);
|
|
30
42
|
process.exit(0);
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
// Subcommand dispatch
|
|
46
|
+
const subcommand = ['doctor', 'brain', 'migrate'].includes(positional[0]) ? positional[0] : null;
|
|
47
|
+
const remainingPositional = subcommand ? positional.slice(1) : positional;
|
|
48
|
+
const targetDir = remainingPositional[0] ?? process.cwd();
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
if (subcommand === 'doctor') {
|
|
52
|
+
const exitCode = await runDoctorCommand(targetDir, {
|
|
53
|
+
json: flags.has('--json'),
|
|
54
|
+
quiet: flags.has('--quiet'),
|
|
55
|
+
strict: flags.has('--strict'),
|
|
56
|
+
});
|
|
57
|
+
process.exit(exitCode);
|
|
58
|
+
}
|
|
59
|
+
if (subcommand === 'brain' || subcommand === 'migrate') {
|
|
60
|
+
console.error(`\nNot yet implemented: \`lifeos ${subcommand}\`. See docs/COMMANDS.md for the design.`);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
// Default: run the wizard
|
|
64
|
+
await run(targetDir, { minimal: flags.has('--minimal') });
|
|
65
|
+
}
|
|
37
66
|
|
|
38
|
-
|
|
67
|
+
main().catch((err) => {
|
|
39
68
|
// Inquirer throws ExitPromptError when user hits Ctrl+C — handle gracefully
|
|
40
69
|
if (err?.name === 'ExitPromptError') {
|
|
41
70
|
console.log('\nCancelled.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sbains2/lifeos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scaffold a personal LifeOS — life-quadrant folders, a council of AI agent voices, and curated MCP wiring. Generates a working lifeos.config.json + .claude/agents/council/ in 2-4 minutes (or <60s with --minimal). BETA — early seed release.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lifeos doctor` subcommand orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Runs the doctor checks against a target directory, prints a per-check report,
|
|
5
|
+
* and exits with code 0 (all green or warnings only) or 1 (any error).
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* --json — emit machine-readable JSON instead of formatted output
|
|
9
|
+
* --quiet — suppress ✓ lines, only show warnings + errors
|
|
10
|
+
* --strict — exit code 1 even on warnings (default treats warnings as 0)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { runDoctor } from '../doctor.js';
|
|
14
|
+
import { c } from '../utils/colors.js';
|
|
15
|
+
|
|
16
|
+
export async function runDoctorCommand(targetDir, options = {}) {
|
|
17
|
+
const { json = false, quiet = false, strict = false } = options;
|
|
18
|
+
|
|
19
|
+
const result = runDoctor(targetDir);
|
|
20
|
+
|
|
21
|
+
if (json) {
|
|
22
|
+
console.log(JSON.stringify(result, null, 2));
|
|
23
|
+
} else {
|
|
24
|
+
printHumanReport(result, { quiet });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Exit code: 1 if any error; 1 if any warning AND strict mode; else 0
|
|
28
|
+
if (result.summary.error > 0) return 1;
|
|
29
|
+
if (strict && result.summary.warn > 0) return 1;
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printHumanReport(result, { quiet }) {
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(c.bold('lifeos doctor') + c.dim(' — checking your scaffold'));
|
|
36
|
+
console.log('');
|
|
37
|
+
|
|
38
|
+
for (const check of result.checks) {
|
|
39
|
+
if (quiet && check.status === 'ok') continue;
|
|
40
|
+
const icon =
|
|
41
|
+
check.status === 'ok'
|
|
42
|
+
? c.ok('✓')
|
|
43
|
+
: check.status === 'warn'
|
|
44
|
+
? c.warn('⚠')
|
|
45
|
+
: c.err('✗');
|
|
46
|
+
const label = check.status === 'ok' ? c.dim(check.name) : check.name;
|
|
47
|
+
console.log(` ${icon} ${label}`);
|
|
48
|
+
if (check.message && check.status !== 'ok') {
|
|
49
|
+
// Wrap long messages
|
|
50
|
+
const lines = check.message.split('\n');
|
|
51
|
+
for (const line of lines) console.log(c.dim(` ${line}`));
|
|
52
|
+
}
|
|
53
|
+
if (check.fix) {
|
|
54
|
+
const fixLines = check.fix.split('\n');
|
|
55
|
+
console.log(c.cyan(` → fix: ${fixLines[0]}`));
|
|
56
|
+
for (let i = 1; i < fixLines.length; i++) {
|
|
57
|
+
console.log(c.cyan(` ${fixLines[i].replace(/^\s+/, '')}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('');
|
|
63
|
+
const { ok, warn, error } = result.summary;
|
|
64
|
+
const summaryParts = [
|
|
65
|
+
`${c.ok(`${ok} ok`)}`,
|
|
66
|
+
warn > 0 ? c.warn(`${warn} warning${warn === 1 ? '' : 's'}`) : null,
|
|
67
|
+
error > 0 ? c.err(`${error} error${error === 1 ? '' : 's'}`) : null,
|
|
68
|
+
].filter(Boolean);
|
|
69
|
+
console.log(c.bold('Summary: ') + summaryParts.join(', '));
|
|
70
|
+
if (error === 0 && warn === 0) {
|
|
71
|
+
console.log(c.dim('Your scaffold looks healthy. Open it in Claude Code and convene the council.'));
|
|
72
|
+
} else if (error === 0) {
|
|
73
|
+
console.log(c.dim('Mostly good — warnings are non-blocking but worth addressing when you have a moment.'));
|
|
74
|
+
} else {
|
|
75
|
+
console.log(c.dim('Errors above will block convening. Apply the fixes shown.'));
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
}
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lifeos doctor` — verifies post-install health of a LifeOS scaffold.
|
|
3
|
+
*
|
|
4
|
+
* Loads the user's `lifeos.config.json` and runs 8 checks:
|
|
5
|
+
* 1. Config exists + parses
|
|
6
|
+
* 2. Schema version matches CLI's supported version
|
|
7
|
+
* 3. Schema cross-refs (reuses validate.js — same checks the wizard runs at write time)
|
|
8
|
+
* 4. Council files present at the paths config references
|
|
9
|
+
* 5. Quadrant folders exist for every active quadrant
|
|
10
|
+
* 6. Env vars set for every wired MCP that needs auth (read-only check —
|
|
11
|
+
* never reads the value, only checks `process.env[name]` is truthy)
|
|
12
|
+
* 7. Writing-style file present (if `writing_style_path` is set)
|
|
13
|
+
* 8. Brain artifact populated (warning if `.lifeos/brain/` is empty)
|
|
14
|
+
*
|
|
15
|
+
* Plus a bonus check (added for v0.2.0):
|
|
16
|
+
* 9. MCP runtimes available (npx / uvx / docker — checks `which <runtime>` for
|
|
17
|
+
* each unique runtime referenced by wired MCPs)
|
|
18
|
+
*
|
|
19
|
+
* Each check returns { id, name, status: 'ok'|'warn'|'error', message, fix? }.
|
|
20
|
+
* Caller decides how to render and what exit code to use.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync, accessSync, readdirSync, constants } from 'node:fs';
|
|
24
|
+
import { join, dirname, isAbsolute } from 'node:path';
|
|
25
|
+
import { execSync } from 'node:child_process';
|
|
26
|
+
import { listCouncilFiles, loadRegistry } from './templates-loader.js';
|
|
27
|
+
import { validate } from './validate.js';
|
|
28
|
+
|
|
29
|
+
const SUPPORTED_VERSION = '0.2';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run all checks against a directory. Returns { checks: [], summary: {ok, warn, error} }.
|
|
33
|
+
*/
|
|
34
|
+
export function runDoctor(targetDir) {
|
|
35
|
+
const checks = [];
|
|
36
|
+
|
|
37
|
+
// Check 1: config exists + parses
|
|
38
|
+
const configPath = join(targetDir, 'lifeos.config.json');
|
|
39
|
+
let config = null;
|
|
40
|
+
if (!existsSync(configPath)) {
|
|
41
|
+
checks.push({
|
|
42
|
+
id: 'config_exists',
|
|
43
|
+
name: 'lifeos.config.json present',
|
|
44
|
+
status: 'error',
|
|
45
|
+
message: `File not found at ${configPath}`,
|
|
46
|
+
fix: `Run \`npx @sbains2/lifeos ${targetDir}\` to scaffold a fresh config.`,
|
|
47
|
+
});
|
|
48
|
+
return finalize(checks);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
53
|
+
checks.push({
|
|
54
|
+
id: 'config_exists',
|
|
55
|
+
name: 'lifeos.config.json present + parses',
|
|
56
|
+
status: 'ok',
|
|
57
|
+
message: `Loaded config (schema v${config.version})`,
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
checks.push({
|
|
61
|
+
id: 'config_exists',
|
|
62
|
+
name: 'lifeos.config.json present + parses',
|
|
63
|
+
status: 'error',
|
|
64
|
+
message: `JSON parse failed: ${err.message}`,
|
|
65
|
+
fix: `Open ${configPath} in an editor and fix the JSON syntax.`,
|
|
66
|
+
});
|
|
67
|
+
return finalize(checks);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check 2: schema version
|
|
71
|
+
if (config.version !== SUPPORTED_VERSION) {
|
|
72
|
+
checks.push({
|
|
73
|
+
id: 'schema_version',
|
|
74
|
+
name: `Schema version matches (cli wants v${SUPPORTED_VERSION})`,
|
|
75
|
+
status: 'error',
|
|
76
|
+
message: `Config is at v${config.version}, but this CLI was built for v${SUPPORTED_VERSION}`,
|
|
77
|
+
fix: `Either upgrade your config (when \`lifeos migrate\` ships) or pin to a CLI version compatible with v${config.version}.`,
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
checks.push({
|
|
81
|
+
id: 'schema_version',
|
|
82
|
+
name: `Schema version matches (v${SUPPORTED_VERSION})`,
|
|
83
|
+
status: 'ok',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check 3: schema cross-refs (reuse validate.js)
|
|
88
|
+
const validateResult = validate(config, { targetDir });
|
|
89
|
+
if (validateResult.ok) {
|
|
90
|
+
checks.push({
|
|
91
|
+
id: 'schema_cross_refs',
|
|
92
|
+
name: 'Schema cross-refs resolve (council ↔ quadrants ↔ MCPs)',
|
|
93
|
+
status: 'ok',
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
checks.push({
|
|
97
|
+
id: 'schema_cross_refs',
|
|
98
|
+
name: 'Schema cross-refs resolve',
|
|
99
|
+
status: 'error',
|
|
100
|
+
message: validateResult.errors.join('; '),
|
|
101
|
+
fix: 'Re-run the wizard with `npx @sbains2/lifeos` to regenerate from a clean template, or fix the references manually in lifeos.config.json.',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (const w of validateResult.warnings) {
|
|
105
|
+
checks.push({
|
|
106
|
+
id: 'schema_warning',
|
|
107
|
+
name: 'Schema warning',
|
|
108
|
+
status: 'warn',
|
|
109
|
+
message: w,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check 4: council files present
|
|
114
|
+
const councilFilesMissing = [];
|
|
115
|
+
for (const member of config.council ?? []) {
|
|
116
|
+
if (!member.system_prompt_path) continue;
|
|
117
|
+
const filename = member.system_prompt_path.split('/').pop();
|
|
118
|
+
const installedPath = join(targetDir, '.claude', 'agents', 'council', filename);
|
|
119
|
+
if (!existsSync(installedPath)) {
|
|
120
|
+
councilFilesMissing.push({ id: member.id, expected: installedPath });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (councilFilesMissing.length === 0) {
|
|
124
|
+
checks.push({
|
|
125
|
+
id: 'council_files',
|
|
126
|
+
name: `Council files present (${(config.council ?? []).length} members)`,
|
|
127
|
+
status: 'ok',
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
checks.push({
|
|
131
|
+
id: 'council_files',
|
|
132
|
+
name: 'Council files present',
|
|
133
|
+
status: 'error',
|
|
134
|
+
message: `Missing files for: ${councilFilesMissing.map((c) => c.id).join(', ')}`,
|
|
135
|
+
fix: `Re-run \`npx @sbains2/lifeos ${targetDir}\` to copy the bundled council prompts.`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check 5: quadrant folders exist (active only)
|
|
140
|
+
const missingFolders = [];
|
|
141
|
+
for (const q of config.quadrants ?? []) {
|
|
142
|
+
if (!q.active) continue;
|
|
143
|
+
const fullPath = isAbsolute(q.path) ? q.path : join(targetDir, q.path);
|
|
144
|
+
if (!existsSync(fullPath)) {
|
|
145
|
+
missingFolders.push({ id: q.id, expected: fullPath });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const activeQuadrantCount = (config.quadrants ?? []).filter((q) => q.active).length;
|
|
149
|
+
if (missingFolders.length === 0) {
|
|
150
|
+
checks.push({
|
|
151
|
+
id: 'quadrant_folders',
|
|
152
|
+
name: `Active quadrant folders exist (${activeQuadrantCount} active)`,
|
|
153
|
+
status: 'ok',
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
checks.push({
|
|
157
|
+
id: 'quadrant_folders',
|
|
158
|
+
name: 'Active quadrant folders exist',
|
|
159
|
+
status: 'warn',
|
|
160
|
+
message: `Missing: ${missingFolders.map((m) => m.id).join(', ')}`,
|
|
161
|
+
fix: `mkdir -p ${missingFolders.map((m) => m.expected).join(' ')}`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check 6: env vars set for wired MCPs (read-only — never reads the value)
|
|
166
|
+
let registry = null;
|
|
167
|
+
try {
|
|
168
|
+
registry = loadRegistry();
|
|
169
|
+
} catch {
|
|
170
|
+
/* registry not loadable — skip env-var check */
|
|
171
|
+
}
|
|
172
|
+
const missingEnv = [];
|
|
173
|
+
if (registry) {
|
|
174
|
+
const byId = new Map(registry.servers.map((s) => [s.id, s]));
|
|
175
|
+
for (const m of config.mcp_servers ?? []) {
|
|
176
|
+
const entry = byId.get(m.id);
|
|
177
|
+
if (!entry?.auth?.env_var) continue;
|
|
178
|
+
const envName = entry.auth.env_var;
|
|
179
|
+
if (!process.env[envName]) {
|
|
180
|
+
missingEnv.push({ id: m.id, env_var: envName, help_url: entry.auth.help_url });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (missingEnv.length === 0) {
|
|
185
|
+
const wiredAuthCount = (config.mcp_servers ?? []).filter((m) => {
|
|
186
|
+
const entry = registry?.servers.find((s) => s.id === m.id);
|
|
187
|
+
return entry?.auth?.env_var;
|
|
188
|
+
}).length;
|
|
189
|
+
checks.push({
|
|
190
|
+
id: 'env_vars',
|
|
191
|
+
name: `Auth env vars set (${wiredAuthCount} wired MCPs need credentials)`,
|
|
192
|
+
status: 'ok',
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
checks.push({
|
|
196
|
+
id: 'env_vars',
|
|
197
|
+
name: 'Auth env vars set for wired MCPs',
|
|
198
|
+
status: 'error',
|
|
199
|
+
message: missingEnv
|
|
200
|
+
.map((m) => `${m.id} needs ${m.env_var}${m.help_url ? ` (get one: ${m.help_url})` : ''}`)
|
|
201
|
+
.join('; '),
|
|
202
|
+
fix: `Set the missing env vars in your shell rc (~/.zshrc or ~/.bashrc), e.g.:\n ${missingEnv
|
|
203
|
+
.map((m) => `export ${m.env_var}=...`)
|
|
204
|
+
.join('\n ')}\n Then reload your shell + restart Claude.`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check 7: writing_style file present (if path set)
|
|
209
|
+
if (config.writing_style_path) {
|
|
210
|
+
const stylePath = isAbsolute(config.writing_style_path)
|
|
211
|
+
? config.writing_style_path
|
|
212
|
+
: join(targetDir, config.writing_style_path);
|
|
213
|
+
if (existsSync(stylePath)) {
|
|
214
|
+
checks.push({
|
|
215
|
+
id: 'writing_style',
|
|
216
|
+
name: 'writing_style.md present',
|
|
217
|
+
status: 'ok',
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
checks.push({
|
|
221
|
+
id: 'writing_style',
|
|
222
|
+
name: 'writing_style.md present',
|
|
223
|
+
status: 'warn',
|
|
224
|
+
message: `Path set but file not found: ${stylePath}`,
|
|
225
|
+
fix: `Either create the file (with a sample of your writing) or remove writing_style_path from lifeos.config.json.`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check 8: brain artifact populated (warning only)
|
|
231
|
+
const brainPath = join(targetDir, config.brain?.graphify?.output ?? '.lifeos/brain');
|
|
232
|
+
if (existsSync(brainPath)) {
|
|
233
|
+
let brainEmpty = true;
|
|
234
|
+
try {
|
|
235
|
+
brainEmpty = readdirSync(brainPath).length === 0;
|
|
236
|
+
} catch {
|
|
237
|
+
/* permissions issue, skip */
|
|
238
|
+
}
|
|
239
|
+
if (brainEmpty) {
|
|
240
|
+
checks.push({
|
|
241
|
+
id: 'brain',
|
|
242
|
+
name: 'Brain index populated',
|
|
243
|
+
status: 'warn',
|
|
244
|
+
message: `${brainPath} exists but is empty`,
|
|
245
|
+
fix: `Run \`npx @sbains2/lifeos brain\` to populate the brain (when that command ships in v0.2.x).`,
|
|
246
|
+
});
|
|
247
|
+
} else {
|
|
248
|
+
checks.push({
|
|
249
|
+
id: 'brain',
|
|
250
|
+
name: 'Brain index populated',
|
|
251
|
+
status: 'ok',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check 9: MCP runtimes available (npx / uvx / docker)
|
|
257
|
+
const wiredRuntimes = new Set();
|
|
258
|
+
if (registry) {
|
|
259
|
+
const byId = new Map(registry.servers.map((s) => [s.id, s]));
|
|
260
|
+
for (const m of config.mcp_servers ?? []) {
|
|
261
|
+
const entry = byId.get(m.id);
|
|
262
|
+
if (entry?.runtime && entry.runtime !== 'manual' && entry.runtime !== 'remote') {
|
|
263
|
+
wiredRuntimes.add(entry.runtime);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Map our runtime enum to the actual binary to check
|
|
268
|
+
const runtimeBinaries = {
|
|
269
|
+
npx: 'npx',
|
|
270
|
+
uvx: 'uvx',
|
|
271
|
+
'pip-install': 'pip',
|
|
272
|
+
'git+pip': 'pip',
|
|
273
|
+
docker: 'docker',
|
|
274
|
+
};
|
|
275
|
+
const missingRuntimes = [];
|
|
276
|
+
for (const rt of wiredRuntimes) {
|
|
277
|
+
const binary = runtimeBinaries[rt] ?? rt;
|
|
278
|
+
try {
|
|
279
|
+
execSync(`command -v ${binary}`, { stdio: 'ignore' });
|
|
280
|
+
} catch {
|
|
281
|
+
missingRuntimes.push({ runtime: rt, binary });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (wiredRuntimes.size === 0) {
|
|
285
|
+
// No runtime needed — all MCPs are manual / remote
|
|
286
|
+
checks.push({
|
|
287
|
+
id: 'runtimes',
|
|
288
|
+
name: 'MCP runtimes available',
|
|
289
|
+
status: 'ok',
|
|
290
|
+
message: 'No automated runtimes wired (all MCPs are manual or remote)',
|
|
291
|
+
});
|
|
292
|
+
} else if (missingRuntimes.length === 0) {
|
|
293
|
+
checks.push({
|
|
294
|
+
id: 'runtimes',
|
|
295
|
+
name: `MCP runtimes available (${[...wiredRuntimes].join(', ')})`,
|
|
296
|
+
status: 'ok',
|
|
297
|
+
});
|
|
298
|
+
} else {
|
|
299
|
+
const fixes = missingRuntimes
|
|
300
|
+
.map((m) => {
|
|
301
|
+
if (m.binary === 'npx') return 'Install Node.js 20+ from https://nodejs.org';
|
|
302
|
+
if (m.binary === 'uvx') return 'Install uv from https://docs.astral.sh/uv/getting-started/installation/';
|
|
303
|
+
if (m.binary === 'docker') return 'Install Docker Desktop from https://docker.com';
|
|
304
|
+
if (m.binary === 'pip') return 'Install Python 3.10+ + pip from https://python.org';
|
|
305
|
+
return `Install ${m.binary}`;
|
|
306
|
+
})
|
|
307
|
+
.join('; ');
|
|
308
|
+
checks.push({
|
|
309
|
+
id: 'runtimes',
|
|
310
|
+
name: 'MCP runtimes available',
|
|
311
|
+
status: 'warn',
|
|
312
|
+
message: `Missing: ${missingRuntimes.map((m) => m.binary).join(', ')} (needed for: ${missingRuntimes
|
|
313
|
+
.map((m) => m.runtime)
|
|
314
|
+
.join(', ')})`,
|
|
315
|
+
fix: fixes,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return finalize(checks);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function finalize(checks) {
|
|
323
|
+
const summary = {
|
|
324
|
+
ok: checks.filter((c) => c.status === 'ok').length,
|
|
325
|
+
warn: checks.filter((c) => c.status === 'warn').length,
|
|
326
|
+
error: checks.filter((c) => c.status === 'error').length,
|
|
327
|
+
};
|
|
328
|
+
return { checks, summary };
|
|
329
|
+
}
|