@sbains2/lifeos 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LifeOS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # @sbains2/lifeos
2
+
3
+ Scaffold a personal LifeOS — life-quadrant folders, a council of AI agent voices, and curated MCP wiring.
4
+
5
+ > ⚠️ **BETA — early seed release.** v0.1.2. See [../BETA.md](../BETA.md) for what's stable vs not.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ # Full wizard (~2-4 minutes for thoughtful first install)
11
+ npx @sbains2/lifeos [target-dir]
12
+
13
+ # Speed-optimized: skips longer-arc focus + role/affiliations + all
14
+ # quadrant/council/MCP confirmations. Uses template defaults everywhere.
15
+ # Target: <60 seconds.
16
+ npx @sbains2/lifeos [target-dir] --minimal
17
+
18
+ # Show help
19
+ npx @sbains2/lifeos --help
20
+ ```
21
+
22
+ If `target-dir` is omitted, the CLI scaffolds into the current working directory.
23
+
24
+ ## What it does
25
+
26
+ 7-step wizard:
27
+
28
+ 1. **Persona** — pick grad-student / early-career-ic / solo-researcher
29
+ 2. **Profile** — name, role, affiliations
30
+ 3. **Foci** — what you're working toward (now / quarter / year / arc) with priority enum
31
+ 4. **Quadrants** — auto-detects keywords (e.g. "president" → flips Leadership Role on); confirm or customize
32
+ 5. **Council** — 10 voices with priority defaults from your persona
33
+ 6. **Apps & MCPs** — checkbox of suggested servers, trust defaults applied
34
+ 7. **Validate + generate** — refuses to write invalid configs; scaffolds folders + council files + INSTALL_MCPS.md + lifeos.config.json
35
+
36
+ ## Trust defaults
37
+
38
+ - **Local-first.** No network calls.
39
+ - **Draft-only outreach.** Gmail and similar MCPs ship with `send_permission: false`.
40
+ - **Tokens by env-var name only.** Auth tokens stay in your shell environment — never copied into config files, never logged.
41
+ - **Validation before write.** Refuses to write a config that fails any cross-reference check. No partial writes.
42
+
43
+ See [../docs/TRUST.md](../docs/TRUST.md) for the full commitments.
44
+
45
+ ## Now what? (post-install workflow)
46
+
47
+ After the wizard prints `Done in Xm Ys.`, here's the path from "scaffold exists" to "council convenes on real artifacts":
48
+
49
+ ### 1. Wire your MCP servers (10-30 min the first time)
50
+
51
+ Open `.lifeos/INSTALL_MCPS.md` in the scaffolded directory. For each MCP listed:
52
+
53
+ - Read the install hint (most point to a Smithery search or specific GitHub repo)
54
+ - Install the MCP via the recommended method (usually `npx` or `uvx` — no Docker for v0.1 picks)
55
+ - Set the auth env var in your shell (`export GITHUB_PERSONAL_ACCESS_TOKEN=...`, etc. — see each MCP's docs)
56
+ - Verify the MCP shows up in your Claude Code config (`~/.claude/settings.json` or your IDE's MCP settings)
57
+
58
+ ### 2. Open the folder in Claude Code
59
+
60
+ ```bash
61
+ cd <your-scaffolded-dir>
62
+ claude
63
+ ```
64
+
65
+ (Or open in your IDE if you use the Claude Code IDE integration.)
66
+
67
+ Claude Code will pick up `lifeos.config.json` and the `.claude/agents/council/` files automatically — they're conventional locations.
68
+
69
+ ### 3. Try a council convening on a real artifact
70
+
71
+ The fastest way to feel the difference between LifeOS and "ChatGPT, please review my X" is to convene multiple council voices on a single artifact. Examples that work today:
72
+
73
+ - **Resume / job-search:** *"Read my resume PDF in JobSearch/. Convene Career Coach, Writing Critic, Network Builder, and Devil's Advocate. Give me their reads as one synthesized session report. Write the result to JobSearch/council-sessions/[date]-resume-review.md."*
74
+ - **Class deliverable:** *"Look at my draft for [class] in Classes/. Convene Academic Advisor + Writing Critic. What's the methodological weakness most likely to undermine the argument?"*
75
+ - **Code change:** *"Read my recent commits in SideProjects/[repo]. Convene Tech Lead + Devil's Advocate. Surface the load-bearing assumption and the test that would falsify it cheaply."*
76
+ - **Week ahead:** *"Look at my Google Calendar via the calendar MCP and my open quadrant folders. Convene Time Manager + Performance Coach. What's at risk this week and what would the highest-leverage block of focused time look like?"*
77
+
78
+ The council members are in `.claude/agents/council/` — read them to see what each voice is for.
79
+
80
+ ### 4. Edit `lifeos.config.json` to tune
81
+
82
+ The wizard accepts defaults at v0.1; granular per-member adjustment in the wizard is planned for v0.2. For now, just open the file and adjust:
83
+
84
+ - **Council priorities:** change `priority: "warm"` to `"hot"` for voices you want emphasized in synthesis sections, `"cold"` to disable
85
+ - **Quadrant priorities:** elevate `"hot"` for what's in the limelight this quarter
86
+ - **Foci:** add or remove entries; tune horizons and priorities
87
+ - **MCPs:** add `config_overrides` to tune per-server behavior (e.g., `send_permission: true` to enable Gmail send if you're sure)
88
+
89
+ Re-running `npx @sbains2/lifeos` against the same dir will prompt before overwriting your config.
90
+
91
+ ### 5. Add your own quadrant or council member
92
+
93
+ Adding a quadrant: edit `quadrants[]` in your config, set `active: true`, set a `priority`, list `council_member_ids` and `mcp_server_ids` to wire, then `mkdir <path>` to create the folder.
94
+
95
+ Adding a council member: drop a markdown file in `.claude/agents/council/<name>.md` with the system prompt, then add an entry to `council[]` in your config with `id`, `name`, `system_prompt_path`, and `priority`.
96
+
97
+ The CLI's validation will refuse the config if any reference is broken — re-run `npx @sbains2/lifeos --help` to see flags, or just re-edit and re-validate manually with `node cli/test/smoke-scaffold.mjs` (during local dev).
98
+
99
+ ## Local development
100
+
101
+ ```bash
102
+ git clone <repo>
103
+ cd lifeos/cli
104
+ npm install
105
+ node --test test/*.test.js # unit tests (validate + quadrants)
106
+ node test/smoke-scaffold.mjs # end-to-end scaffold pipeline test
107
+ npm start ~/lifeos-trial # run the wizard against a fresh dir
108
+ ```
109
+
110
+ The bundled templates at `cli/templates/` are the canonical copy. The `npm run sync-templates` script (legacy from when templates were duplicated at the repo root) is a no-op in this repo.
111
+
112
+ ## Status
113
+
114
+ v0.1.2 — second runnable iteration after first cold-start trial. See [../CHANGELOG.md](../CHANGELOG.md) for what shipped when.
115
+
116
+ What's NOT yet implemented (see [../BETA.md](../BETA.md)):
117
+ - `lifeos doctor` / `brain` / `migrate` commands
118
+ - Granular per-quadrant / per-council priority adjustment in the wizard
119
+ - `~/.lifeos/wizard-history.json` reuse for second-machine installs
120
+ - Custom-persona path
121
+ - Telemetry plumbing
122
+ - `npm publish` (install via `git clone` for now)
package/bin/lifeos.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/index.js';
3
+
4
+ // Parse argv: positional target dir + flags
5
+ // Usage: lifeos [target-dir] [--minimal]
6
+ const args = process.argv.slice(2);
7
+ const flags = new Set(args.filter((a) => a.startsWith('--')));
8
+ const positional = args.filter((a) => !a.startsWith('--'));
9
+
10
+ if (flags.has('--help') || flags.has('-h')) {
11
+ console.log(`lifeos — scaffold a personal LifeOS
12
+
13
+ Usage:
14
+ lifeos [target-dir] [options]
15
+
16
+ Options:
17
+ --minimal Skip optional substantive prompts (longer-arc focus,
18
+ quadrant/council/MCP confirmations). Uses template
19
+ defaults everywhere. Target install time: < 60 seconds.
20
+ --help, -h Show this help
21
+
22
+ Arguments:
23
+ target-dir Directory to scaffold into (default: current working dir)
24
+
25
+ Examples:
26
+ lifeos # scaffold into current dir, full wizard
27
+ lifeos ~/my-lifeos # scaffold into ~/my-lifeos
28
+ lifeos ~/my-lifeos --minimal # speed-optimized install
29
+ `);
30
+ process.exit(0);
31
+ }
32
+
33
+ const targetDir = positional[0] ?? process.cwd();
34
+ const options = {
35
+ minimal: flags.has('--minimal'),
36
+ };
37
+
38
+ run(targetDir, options).catch((err) => {
39
+ // Inquirer throws ExitPromptError when user hits Ctrl+C — handle gracefully
40
+ if (err?.name === 'ExitPromptError') {
41
+ console.log('\nCancelled.');
42
+ process.exit(0);
43
+ }
44
+ console.error('\nFailed:', err.stack ?? err.message ?? err);
45
+ process.exit(1);
46
+ });
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@sbains2/lifeos",
3
+ "version": "0.1.3",
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
+ "type": "module",
6
+ "bin": {
7
+ "lifeos": "bin/lifeos.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20.0.0"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "templates",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "start": "node bin/lifeos.js",
21
+ "test": "node --test test/*.test.js",
22
+ "sync-templates": "rm -rf templates && cp -R ../templates ./templates"
23
+ },
24
+ "dependencies": {
25
+ "@inquirer/prompts": "^7.2.0",
26
+ "chalk": "^5.3.0"
27
+ },
28
+ "keywords": [
29
+ "lifeos",
30
+ "agent",
31
+ "agents",
32
+ "council",
33
+ "mcp",
34
+ "model-context-protocol",
35
+ "claude-code",
36
+ "scaffold",
37
+ "cli",
38
+ "wizard"
39
+ ],
40
+ "author": "sbains2",
41
+ "license": "MIT",
42
+ "homepage": "https://github.com/sbains2/lifeos#readme",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/sbains2/lifeos.git",
46
+ "directory": "cli"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/sbains2/lifeos/issues"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }
package/src/cleanup.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * After Step 6 (Apps & MCPs) the user may have unchecked an MCP server that
3
+ * a quadrant's `mcp_server_ids[]` still references. Validation will then fail
4
+ * at Step 7 with no path to fix without restarting the wizard.
5
+ *
6
+ * This module cleans up dangling references: filter each quadrant's
7
+ * `mcp_server_ids[]` to only include MCPs that are actually wired.
8
+ *
9
+ * Pure function — exported for testability.
10
+ *
11
+ * Returns { cleaned, removed }:
12
+ * cleaned: the quadrants array with dangling refs removed
13
+ * removed: [{ quadrantId, dropped: [mcpId, ...] }] — what was filtered, for surfacing to the user
14
+ */
15
+ export function cleanupQuadrantMcpRefs(quadrants, wiredMcps) {
16
+ const wiredIds = new Set((wiredMcps ?? []).map((m) => m.id));
17
+ const removed = [];
18
+ const cleaned = (quadrants ?? []).map((q) => {
19
+ const orig = q.mcp_server_ids ?? [];
20
+ const filtered = orig.filter((id) => wiredIds.has(id));
21
+ if (filtered.length !== orig.length) {
22
+ const dropped = orig.filter((id) => !wiredIds.has(id));
23
+ removed.push({ quadrantId: q.id, dropped });
24
+ }
25
+ return { ...q, mcp_server_ids: filtered };
26
+ });
27
+ return { cleaned, removed };
28
+ }
package/src/index.js ADDED
@@ -0,0 +1,131 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { confirm } from '@inquirer/prompts';
4
+ import { loadPersonaTemplate } from './templates-loader.js';
5
+ import { askPersona } from './wizard/persona.js';
6
+ import { askProfile } from './wizard/profile.js';
7
+ import { askFoci } from './wizard/foci.js';
8
+ import { askQuadrants, detectQuadrantSignals } from './wizard/quadrants.js';
9
+ import { askCouncil } from './wizard/council.js';
10
+ import { askMcps } from './wizard/mcps.js';
11
+ import { validateAndGenerate } from './wizard/generate.js';
12
+ import { cleanupQuadrantMcpRefs } from './cleanup.js';
13
+ import { startTimer, elapsed } from './utils/timer.js';
14
+ import { c } from './utils/colors.js';
15
+
16
+ export async function run(targetDir, options = {}) {
17
+ const { minimal = false } = options;
18
+
19
+ console.log('');
20
+ console.log(c.bold('lifeos') + c.dim(' v0.1.3 — scaffold a personal LifeOS'));
21
+ console.log(c.dim(`Target directory: ${targetDir}`));
22
+ if (minimal) {
23
+ console.log(c.dim('Mode: --minimal (speed-optimized; using template defaults for quadrants, council, MCPs)'));
24
+ }
25
+
26
+ // Refuse to overwrite an existing config without explicit confirmation
27
+ const existingConfig = join(targetDir, 'lifeos.config.json');
28
+ if (existsSync(existingConfig)) {
29
+ console.log('');
30
+ console.log(c.warn(`A lifeos.config.json already exists at ${existingConfig}.`));
31
+ const overwrite = await confirm({
32
+ message: 'Overwrite it? (you should probably back it up first)',
33
+ default: false,
34
+ });
35
+ if (!overwrite) {
36
+ console.log(c.dim('Cancelled. No files written.'));
37
+ return;
38
+ }
39
+ }
40
+
41
+ const start = startTimer();
42
+
43
+ // Step 1
44
+ const personaId = await askPersona();
45
+ const template = loadPersonaTemplate(personaId);
46
+
47
+ // Step 2
48
+ const profile = await askProfile({ minimal });
49
+
50
+ // Step 3
51
+ const foci = await askFoci({ minimal });
52
+
53
+ // Step 4 — full wizard auto-detects leadership/infra from profile.
54
+ // In --minimal mode we still apply the auto-detection (it's free) but skip
55
+ // the user confirmation on the resulting list.
56
+ let quadrants;
57
+ if (minimal) {
58
+ const matches = detectQuadrantSignals(profile);
59
+ quadrants = template.quadrants.map((q) => {
60
+ if (matches[q.id] && !q.active) {
61
+ return { ...q, active: true, priority: q.priority === 'cold' ? 'lukewarm' : q.priority };
62
+ }
63
+ return q;
64
+ });
65
+ } else {
66
+ quadrants = await askQuadrants(template.quadrants, profile);
67
+ }
68
+
69
+ // Step 5 — council
70
+ const council = minimal ? template.council : await askCouncil(template.council);
71
+
72
+ // Step 6 — MCPs
73
+ const mcps = minimal ? template.mcp_servers : await askMcps(template.mcp_servers);
74
+
75
+ // Auto-cleanup: if the user unchecked any MCP at Step 6 that a quadrant
76
+ // still references, remove the dangling reference so validation passes.
77
+ // (Reported as a bug 2026-05-01 — friend tester unchecked zotero and the
78
+ // research quadrant still referenced it, blocking the entire install.)
79
+ const { cleaned: cleanedQuadrants, removed: cleanupReport } = cleanupQuadrantMcpRefs(quadrants, mcps);
80
+ if (cleanupReport.length > 0) {
81
+ console.log('');
82
+ console.log(c.dim('Auto-cleaned quadrant MCP refs after your Step 6 selections:'));
83
+ for (const r of cleanupReport) {
84
+ console.log(c.dim(` ${c.dot} ${r.quadrantId}: removed [${r.dropped.join(', ')}] (not wired)`));
85
+ }
86
+ }
87
+
88
+ // Assemble final config
89
+ const config = {
90
+ $schema: template.$schema,
91
+ version: template.version,
92
+ user: {
93
+ name: profile.name,
94
+ persona: personaId,
95
+ context: {
96
+ role: profile.context.role,
97
+ affiliations: profile.context.affiliations,
98
+ foci,
99
+ },
100
+ },
101
+ quadrants: cleanedQuadrants,
102
+ council,
103
+ mcp_servers: mcps,
104
+ agent_runtime: template.agent_runtime,
105
+ brain: {
106
+ ...template.brain,
107
+ graphify: {
108
+ ...template.brain.graphify,
109
+ // Reduce graphify paths to active quadrants only
110
+ paths: cleanedQuadrants.filter((q) => q.active).map((q) => q.path),
111
+ },
112
+ },
113
+ writing_style_path: template.writing_style_path,
114
+ };
115
+
116
+ // Step 7 — validate + scaffold
117
+ const result = await validateAndGenerate(config, { targetDir, minimal });
118
+
119
+ console.log('');
120
+ if (result.ok) {
121
+ console.log(c.bold(c.ok('Done')) + ` in ${elapsed(start)}.`);
122
+ console.log('');
123
+ console.log(c.bold('Next:'));
124
+ console.log(` ${c.arrow} Wire your MCP servers (see .lifeos/INSTALL_MCPS.md)`);
125
+ console.log(` ${c.arrow} Open this folder in Claude Code and try convening the council on a real artifact`);
126
+ console.log(` ${c.arrow} Edit lifeos.config.json to tune council priorities, add quadrants, or change foci`);
127
+ console.log('');
128
+ } else {
129
+ console.log(c.dim(`Aborted after ${elapsed(start)}.`));
130
+ }
131
+ }
@@ -0,0 +1,147 @@
1
+ import { mkdirSync, writeFileSync, copyFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { templatesDir, loadRegistry } from './templates-loader.js';
4
+ import { c } from './utils/colors.js';
5
+
6
+ /**
7
+ * Generate the full LifeOS scaffold for a validated config.
8
+ * Returns a list of created paths so the caller can summarize.
9
+ */
10
+ export function scaffold(config, { targetDir }) {
11
+ const created = [];
12
+
13
+ // Active quadrant folders + placeholder READMEs
14
+ for (const q of config.quadrants) {
15
+ if (!q.active) continue;
16
+ const dir = join(targetDir, q.path);
17
+ mkdirSync(dir, { recursive: true });
18
+ const readme = join(dir, 'README.md');
19
+ if (!existsSync(readme)) {
20
+ writeFileSync(readme, quadrantReadme(q), 'utf8');
21
+ created.push(`${q.path}/README.md`);
22
+ } else {
23
+ created.push(`${q.path}/ (folder existed; README preserved)`);
24
+ }
25
+ }
26
+
27
+ // .claude/agents/council/ + copy referenced council prompt files
28
+ const councilDir = join(targetDir, '.claude', 'agents', 'council');
29
+ mkdirSync(councilDir, { recursive: true });
30
+ const seenCouncil = new Set();
31
+ for (const member of config.council) {
32
+ const filename = member.system_prompt_path.split('/').pop();
33
+ if (seenCouncil.has(filename)) continue;
34
+ seenCouncil.add(filename);
35
+ const src = join(templatesDir(), 'council', filename);
36
+ const dest = join(councilDir, filename);
37
+ if (existsSync(dest)) {
38
+ created.push(`.claude/agents/council/${filename} (preserved — user customization not overwritten)`);
39
+ } else {
40
+ copyFileSync(src, dest);
41
+ created.push(`.claude/agents/council/${filename}`);
42
+ }
43
+ }
44
+
45
+ // .lifeos/ scaffold
46
+ const lifeosDir = join(targetDir, '.lifeos');
47
+ mkdirSync(join(lifeosDir, 'memory'), { recursive: true });
48
+ mkdirSync(join(lifeosDir, 'brain'), { recursive: true });
49
+ created.push('.lifeos/memory/');
50
+ created.push('.lifeos/brain/');
51
+
52
+ // INSTALL_MCPS.md
53
+ const installPath = join(lifeosDir, 'INSTALL_MCPS.md');
54
+ writeFileSync(installPath, installMcpsDoc(config), 'utf8');
55
+ created.push('.lifeos/INSTALL_MCPS.md');
56
+
57
+ // Finally: lifeos.config.json (last so we don't leave a config pointing at unbuilt scaffolding)
58
+ const configPath = join(targetDir, 'lifeos.config.json');
59
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
60
+ created.push('lifeos.config.json');
61
+
62
+ return created;
63
+ }
64
+
65
+ function quadrantReadme(q) {
66
+ return `# ${q.name}
67
+
68
+ ${q.description ?? ''}
69
+
70
+ This folder is the home of your **${q.id}** quadrant. The LifeOS council members assigned to it will read files here as context when convened.
71
+
72
+ ## Council members assigned
73
+
74
+ ${(q.council_member_ids ?? []).map((id) => `- \`${id}\``).join('\n') || '_none_'}
75
+
76
+ ## MCP servers wired
77
+
78
+ ${(q.mcp_server_ids ?? []).map((id) => `- \`${id}\``).join('\n') || '_none_'}
79
+
80
+ ## Priority
81
+
82
+ \`${q.priority}\` — change in \`lifeos.config.json\` to adjust how much airtime this quadrant gets in cross-quadrant convenings.
83
+ `;
84
+ }
85
+
86
+ function installMcpsDoc(config) {
87
+ let registry;
88
+ try {
89
+ registry = loadRegistry();
90
+ } catch {
91
+ registry = { servers: [] };
92
+ }
93
+ const byId = new Map(registry.servers.map((s) => [s.id, s]));
94
+
95
+ const lines = [
96
+ '# Installing your MCP servers',
97
+ '',
98
+ 'This file was generated by `npx @sbains2/lifeos`. Each MCP server below was wired into your `lifeos.config.json` based on the apps you said you use. Follow the install hint for each, then set the auth env var in your shell so Claude Code (or your chosen agent runtime) can use it.',
99
+ '',
100
+ '## Trust defaults applied',
101
+ '',
102
+ '- Auth tokens stay in your shell environment. They are never copied into your config file or logged by LifeOS.',
103
+ '- Outreach-capable MCPs (Gmail, LinkedIn) ship draft-only — they cannot send messages on your behalf without an explicit config override.',
104
+ '- All operations are local. LifeOS itself does not phone home.',
105
+ '',
106
+ '---',
107
+ '',
108
+ ];
109
+
110
+ for (const m of config.mcp_servers) {
111
+ const meta = byId.get(m.id) ?? null;
112
+ lines.push(`## \`${m.id}\``);
113
+ lines.push('');
114
+ if (meta) {
115
+ lines.push(`**${meta.name}** — ${meta.description}`);
116
+ lines.push('');
117
+ lines.push(`- Auth: ${meta.auth?.method ?? 'unknown'}${meta.auth?.env_var ? ` (env var: \`${meta.auth.env_var}\`)` : ''}`);
118
+ lines.push(`- Install hint: ${meta.install_hint ?? '_none_'}`);
119
+ if (meta.useful_for_personas?.length) {
120
+ lines.push(`- Useful for: ${meta.useful_for_personas.join(', ')}`);
121
+ }
122
+ } else {
123
+ lines.push(`_Not in the bundled registry. Add install steps manually._`);
124
+ }
125
+ if (m.config_overrides) {
126
+ lines.push('');
127
+ lines.push('Config overrides applied in `lifeos.config.json`:');
128
+ lines.push('```json');
129
+ lines.push(JSON.stringify(m.config_overrides, null, 2));
130
+ lines.push('```');
131
+ }
132
+ lines.push('');
133
+ lines.push('---');
134
+ lines.push('');
135
+ }
136
+
137
+ return lines.join('\n');
138
+ }
139
+
140
+ /**
141
+ * Print a one-line summary per created path.
142
+ */
143
+ export function printScaffoldReport(created) {
144
+ for (const p of created) {
145
+ console.log(` ${c.check} ${p}`);
146
+ }
147
+ }
@@ -0,0 +1,34 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
7
+
8
+ export const PERSONA_IDS = ['grad-student', 'early-career-ic', 'solo-researcher'];
9
+
10
+ export function loadPersonaTemplate(personaId) {
11
+ const path = join(TEMPLATES_DIR, 'personas', `${personaId}.json`);
12
+ if (!existsSync(path)) {
13
+ throw new Error(`Unknown persona: ${personaId}. Available: ${PERSONA_IDS.join(', ')}`);
14
+ }
15
+ return JSON.parse(readFileSync(path, 'utf8'));
16
+ }
17
+
18
+ export function loadRegistry() {
19
+ const path = join(TEMPLATES_DIR, 'mcps', 'registry.json');
20
+ return JSON.parse(readFileSync(path, 'utf8'));
21
+ }
22
+
23
+ export function listCouncilFiles() {
24
+ const dir = join(TEMPLATES_DIR, 'council');
25
+ return readdirSync(dir).filter((f) => f.endsWith('.md'));
26
+ }
27
+
28
+ export function readCouncilFile(filename) {
29
+ return readFileSync(join(TEMPLATES_DIR, 'council', filename), 'utf8');
30
+ }
31
+
32
+ export function templatesDir() {
33
+ return TEMPLATES_DIR;
34
+ }
@@ -0,0 +1,14 @@
1
+ import chalk from 'chalk';
2
+
3
+ export const c = {
4
+ ok: (s) => chalk.green(s),
5
+ warn: (s) => chalk.yellow(s),
6
+ err: (s) => chalk.red(s),
7
+ dim: (s) => chalk.dim(s),
8
+ bold: (s) => chalk.bold(s),
9
+ cyan: (s) => chalk.cyan(s),
10
+ check: chalk.green('✓'),
11
+ cross: chalk.red('✗'),
12
+ dot: chalk.dim('·'),
13
+ arrow: chalk.cyan('→'),
14
+ };
@@ -0,0 +1,12 @@
1
+ export function startTimer() {
2
+ return Date.now();
3
+ }
4
+
5
+ export function elapsed(startMs) {
6
+ const ms = Date.now() - startMs;
7
+ const totalSec = Math.round(ms / 1000);
8
+ const min = Math.floor(totalSec / 60);
9
+ const sec = totalSec % 60;
10
+ if (min === 0) return `${sec}s`;
11
+ return `${min}m ${sec}s`;
12
+ }