@sbains2/lifeos 0.2.1 → 0.3.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/bin/lifeos.js CHANGED
@@ -1,14 +1,52 @@
1
1
  #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
2
3
  import { run } from '../src/index.js';
3
4
  import { runDoctorCommand } from '../src/commands/doctor.js';
5
+ import { runBrainCommand } from '../src/commands/brain.js';
6
+ import { runContext } from '../src/commands/context.js';
7
+ import { runSchedule } from '../src/commands/schedule.js';
4
8
 
5
9
  // Parse argv: optional subcommand + positional target dir + flags
6
10
  // Usage:
7
11
  // lifeos [target-dir] [--minimal] # wizard (default)
8
12
  // lifeos doctor [target-dir] [--json] [--quiet] [--strict]
9
13
  const args = process.argv.slice(2);
10
- const flags = new Set(args.filter((a) => a.startsWith('--')));
11
- const positional = args.filter((a) => !a.startsWith('--') && !a.startsWith('-'));
14
+
15
+ // `schedule` dispatches EARLY, before the global flag/positional parsing:
16
+ // its value flags (--prompt <file>, --every 1d, ...) would otherwise be
17
+ // misread as positional args and corrupt targetDir resolution. It also owns
18
+ // its own --help (so `lifeos schedule --help` prints the schedule help, not
19
+ // the global help). schedule operates on cwd — no positional target-dir arg.
20
+ if (args[0] === 'schedule') {
21
+ const exitCode = await runSchedule(resolve(process.cwd()), args.slice(1));
22
+ process.exit(exitCode);
23
+ }
24
+
25
+ // `context`'s --quadrant takes a value; extract flag + value before the
26
+ // global parser treats the value as a positional target dir.
27
+ let quadrantId = null;
28
+ let filteredArgs = args;
29
+ const qIdx = args.indexOf('--quadrant');
30
+ if (qIdx !== -1) {
31
+ const qVal = args[qIdx + 1];
32
+ if (qVal !== undefined && !qVal.startsWith('-')) {
33
+ quadrantId = qVal;
34
+ filteredArgs = args.filter((_, i) => i !== qIdx && i !== qIdx + 1);
35
+ } else {
36
+ // dangling `--quadrant` (no value, or followed by another flag): drop only
37
+ // the flag so a trailing --json/--help isn't swallowed as its value.
38
+ filteredArgs = args.filter((_, i) => i !== qIdx);
39
+ }
40
+ } else {
41
+ const qEq = args.find((a) => a.startsWith('--quadrant='));
42
+ if (qEq) {
43
+ quadrantId = qEq.split('=')[1];
44
+ filteredArgs = args.filter((a) => a !== qEq);
45
+ }
46
+ }
47
+
48
+ const flags = new Set(filteredArgs.filter((a) => a.startsWith('--')));
49
+ const positional = filteredArgs.filter((a) => !a.startsWith('--') && !a.startsWith('-'));
12
50
 
13
51
  if (flags.has('--help') || flags.has('-h') || positional[0] === 'help') {
14
52
  console.log(`lifeos — scaffold and verify a personal LifeOS
@@ -16,6 +54,8 @@ if (flags.has('--help') || flags.has('-h') || positional[0] === 'help') {
16
54
  Usage:
17
55
  lifeos [target-dir] [options] # run the wizard
18
56
  lifeos doctor [target-dir] [options] # verify scaffold health
57
+ lifeos context [target-dir] [options] # print the convening context block
58
+ lifeos schedule <add|list|remove> [options] # run agent prompts on a schedule
19
59
  lifeos --help # show this
20
60
 
21
61
  Wizard options:
@@ -32,6 +72,26 @@ Doctor options:
32
72
  --quiet Suppress ✓ lines, only show warnings and errors
33
73
  --strict Exit code 1 even on warnings (default: warnings are 0)
34
74
 
75
+ Brain options:
76
+ --dry-run Show what would run without invoking graphify
77
+ --json Emit machine-readable JSON (for CI / scripting)
78
+
79
+ Context options:
80
+ --json Emit the same data as structured JSON (for runtimes / scripting)
81
+ --quadrant <id> Scope output to one quadrant (its council subset; foci stay global)
82
+
83
+ Schedule subcommands (run \`lifeos schedule --help\` for full details):
84
+ add <name> --prompt <file> (--every <n>{d|h} [--at HH:MM] | --cron "<expr>")
85
+ [--runtime claude-code|codex] [--dry-run]
86
+ list Show installed schedules
87
+ remove <name> Uninstall a schedule (launchd unload on macOS)
88
+
89
+ Note on brain: lifeos brain shells out to graphify (https://github.com/
90
+ safishamsi/graphify), an OSS knowledge-graph tool. Install once with
91
+ "pip install graphifyy && graphify install". Code parsing is 100% local;
92
+ semantic extraction uses your existing AI-assistant session (no separate
93
+ API key, no incremental cost).
94
+
35
95
  Arguments:
36
96
  target-dir Directory to scaffold into / verify (default: current working dir)
37
97
 
@@ -42,14 +102,21 @@ Examples:
42
102
  lifeos doctor # verify current dir
43
103
  lifeos doctor ~/my-lifeos # verify ~/my-lifeos
44
104
  lifeos doctor --json | jq '.summary' # programmatic check
105
+ lifeos context ~/my-lifeos # convening block to stdout (pipe-friendly)
106
+ lifeos context --quadrant job-search # scope to one quadrant's council
107
+ lifeos schedule add digest --prompt templates/prompts/weekly_digest.md --every 1d
45
108
  `);
46
109
  process.exit(0);
47
110
  }
48
111
 
49
- // Subcommand dispatch
50
- const subcommand = ['doctor', 'brain', 'migrate'].includes(positional[0]) ? positional[0] : null;
112
+ // Subcommand dispatch (schedule handled earlier — see top of file)
113
+ const subcommand = ['doctor', 'brain', 'migrate', 'context'].includes(positional[0]) ? positional[0] : null;
51
114
  const remainingPositional = subcommand ? positional.slice(1) : positional;
52
- const targetDir = remainingPositional[0] ?? process.cwd();
115
+ // BUGFIX 2026-05-01: always resolve to absolute path. If user passes '.' as
116
+ // the positional arg, downstream `path.join(targetDir, q.path)` collapses to a
117
+ // relative string ('Classes' instead of '/.../lifeos-demo-clean/Classes'),
118
+ // which breaks the validator's parent-writeable check (it walks up to '/').
119
+ const targetDir = resolve(remainingPositional[0] ?? process.cwd());
53
120
 
54
121
  async function main() {
55
122
  if (subcommand === 'doctor') {
@@ -61,8 +128,22 @@ async function main() {
61
128
  });
62
129
  process.exit(exitCode);
63
130
  }
64
- if (subcommand === 'brain' || subcommand === 'migrate') {
65
- console.error(`\nNot yet implemented: \`lifeos ${subcommand}\`. See docs/COMMANDS.md for the design.`);
131
+ if (subcommand === 'brain') {
132
+ const exitCode = await runBrainCommand(targetDir, {
133
+ dryRun: flags.has('--dry-run'),
134
+ json: flags.has('--json'),
135
+ });
136
+ process.exit(exitCode);
137
+ }
138
+ if (subcommand === 'context') {
139
+ const exitCode = await runContext(targetDir, {
140
+ json: flags.has('--json'),
141
+ quadrant: quadrantId,
142
+ });
143
+ process.exit(exitCode);
144
+ }
145
+ if (subcommand === 'migrate') {
146
+ console.error(`\nNot yet implemented: \`lifeos migrate\`. See docs/COMMANDS.md for the design.`);
66
147
  process.exit(2);
67
148
  }
68
149
  // Default: run the wizard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sbains2/lifeos",
3
- "version": "0.2.1",
3
+ "version": "0.3.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": {
@@ -18,8 +18,7 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "start": "node bin/lifeos.js",
21
- "test": "node --test test/*.test.js",
22
- "sync-templates": "rm -rf templates && cp -R ../templates ./templates"
21
+ "test": "node --test test/*.test.js"
23
22
  },
24
23
  "dependencies": {
25
24
  "@inquirer/prompts": "^7.2.0",
package/src/brain.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `lifeos brain` — thin wrapper around graphify (https://github.com/safishamsi/graphify).
3
+ *
4
+ * Why graphify (decision rationale):
5
+ * - Open-source, MIT-licensed
6
+ * - Runs entirely on the user's machine: code parsing is 100% local via tree-sitter
7
+ * AST + NetworkX + Leiden clustering
8
+ * - For semantic content (docs, images, PDFs), uses the user's EXISTING AI-assistant
9
+ * session — Claude Code, Cursor, Codex, etc. — via credentials they've already
10
+ * configured. No separate API key, no incremental cost beyond their existing
11
+ * subscription.
12
+ * - Cost to LifeOS project: $0 (we don't host or proxy anything)
13
+ * - Cost to friends installing LifeOS: $0 incremental (their Claude Code subscription
14
+ * is already paying for any LLM calls graphify makes)
15
+ *
16
+ * This module is a thin shell-out wrapper. It does not re-implement graphify;
17
+ * it just invokes it with the right paths from lifeos.config.json.
18
+ */
19
+
20
+ import { execSync, spawnSync } from 'node:child_process';
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { join, isAbsolute } from 'node:path';
23
+
24
+ /**
25
+ * Check if graphify is installed and available in PATH.
26
+ * Pure side-effect-free check. Returns boolean.
27
+ */
28
+ export function isGraphifyInstalled() {
29
+ try {
30
+ execSync('command -v graphify', { stdio: 'ignore' });
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Determine what `lifeos brain` would do for a given scaffold, without executing.
39
+ * Returns { ok, paths, cwd, outputHint } on success or { ok: false, error } on failure.
40
+ */
41
+ export function planBrainRun(targetDir) {
42
+ const configPath = join(targetDir, 'lifeos.config.json');
43
+ if (!existsSync(configPath)) {
44
+ return { ok: false, error: `lifeos.config.json not found at ${configPath}` };
45
+ }
46
+ let config;
47
+ try {
48
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
49
+ } catch (err) {
50
+ return { ok: false, error: `lifeos.config.json parse failed: ${err.message}` };
51
+ }
52
+
53
+ // Prefer brain.graphify.paths (explicit config); fall back to active quadrant paths
54
+ const configuredPaths = config.brain?.graphify?.paths;
55
+ const activeQuadrantPaths = (config.quadrants ?? []).filter((q) => q.active).map((q) => q.path);
56
+ const paths = configuredPaths && configuredPaths.length > 0 ? configuredPaths : activeQuadrantPaths;
57
+
58
+ if (paths.length === 0) {
59
+ return {
60
+ ok: false,
61
+ error:
62
+ 'No paths to index. Add at least one active quadrant to lifeos.config.json or set brain.graphify.paths explicitly.',
63
+ };
64
+ }
65
+
66
+ // Resolve relative paths against targetDir for display + check existence (warn, not error)
67
+ const resolvedPaths = paths.map((p) => (isAbsolute(p) ? p : join(targetDir, p)));
68
+ const missingPaths = resolvedPaths.filter((p) => !existsSync(p));
69
+
70
+ return {
71
+ ok: true,
72
+ paths,
73
+ resolvedPaths,
74
+ missingPaths,
75
+ cwd: targetDir,
76
+ outputHint: config.brain?.graphify?.output ?? './graphify-out',
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Invoke graphify with the planned paths. Inherits stdio so the user sees graphify's
82
+ * own output stream. Returns { ok, exitCode, paths, outputDir } or { ok: false, error }.
83
+ *
84
+ * Does NOT auto-install graphify (too invasive — pip install requires user consent).
85
+ * If graphify is missing, returns ok:false with install instructions.
86
+ */
87
+ export function runBrain(targetDir, options = {}) {
88
+ const { dryRun = false } = options;
89
+
90
+ const plan = planBrainRun(targetDir);
91
+ if (!plan.ok) return plan;
92
+
93
+ if (!isGraphifyInstalled()) {
94
+ return {
95
+ ok: false,
96
+ error: 'graphify not installed',
97
+ installCommand: 'pip install graphifyy && graphify install',
98
+ installUrl: 'https://github.com/safishamsi/graphify',
99
+ };
100
+ }
101
+
102
+ if (dryRun) {
103
+ return {
104
+ ok: true,
105
+ dryRun: true,
106
+ command: 'graphify',
107
+ args: plan.paths,
108
+ cwd: plan.cwd,
109
+ outputDir: join(plan.cwd, 'graphify-out'),
110
+ };
111
+ }
112
+
113
+ // Shell out to graphify; let it write to its default `graphify-out/` in the
114
+ // working directory. We deliberately don't try to redirect output — graphify's
115
+ // CLI surface evolves and we don't want to fight it. Output location is
116
+ // surfaced to the user in our wrapper's "next steps" message.
117
+ const result = spawnSync('graphify', plan.paths, {
118
+ cwd: plan.cwd,
119
+ stdio: 'inherit',
120
+ encoding: 'utf8',
121
+ });
122
+
123
+ return {
124
+ ok: result.status === 0,
125
+ exitCode: result.status,
126
+ paths: plan.paths,
127
+ outputDir: join(plan.cwd, 'graphify-out'),
128
+ };
129
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * `lifeos brain` subcommand orchestrator.
3
+ *
4
+ * Thin wrapper around src/brain.js — handles flag parsing, output formatting,
5
+ * and exit codes. The actual graphify invocation is in brain.js.
6
+ *
7
+ * Flags:
8
+ * --dry-run Show what would run without invoking graphify
9
+ * (does NOT require graphify to be installed — useful for previewing)
10
+ * --json Emit machine-readable JSON (for CI / scripting)
11
+ */
12
+
13
+ import { runBrain, planBrainRun, isGraphifyInstalled } from '../brain.js';
14
+ import { c } from '../utils/colors.js';
15
+
16
+ export async function runBrainCommand(targetDir, options = {}) {
17
+ const { dryRun = false, json = false } = options;
18
+
19
+ // Step 1: plan first (no external deps — works without graphify)
20
+ const plan = planBrainRun(targetDir);
21
+ if (!plan.ok) {
22
+ if (json) {
23
+ console.log(JSON.stringify(plan, null, 2));
24
+ } else {
25
+ console.log('');
26
+ console.log(c.err(`✗ ${plan.error}`));
27
+ console.log('');
28
+ }
29
+ return 1;
30
+ }
31
+
32
+ // Step 2: if dry-run, print plan and exit (graphify install not required)
33
+ if (dryRun) {
34
+ if (json) {
35
+ console.log(
36
+ JSON.stringify(
37
+ {
38
+ ok: true,
39
+ dryRun: true,
40
+ command: 'graphify',
41
+ args: plan.paths,
42
+ cwd: plan.cwd,
43
+ outputDir: `${plan.cwd}/graphify-out`,
44
+ graphifyInstalled: isGraphifyInstalled(),
45
+ },
46
+ null,
47
+ 2
48
+ )
49
+ );
50
+ } else {
51
+ console.log('');
52
+ console.log(c.bold('lifeos brain --dry-run') + c.dim(' — would invoke:'));
53
+ console.log('');
54
+ console.log(` ${c.cyan('cd')} ${plan.cwd}`);
55
+ console.log(` ${c.cyan('graphify')} ${plan.paths.join(' ')}`);
56
+ console.log('');
57
+ console.log(c.dim(` Output would land in: ${plan.cwd}/graphify-out/`));
58
+ if (plan.missingPaths && plan.missingPaths.length > 0) {
59
+ console.log('');
60
+ console.log(c.warn(` ⚠ ${plan.missingPaths.length} path(s) don't exist — graphify may skip them:`));
61
+ for (const p of plan.missingPaths) {
62
+ console.log(c.dim(` ${p}`));
63
+ }
64
+ }
65
+ if (!isGraphifyInstalled()) {
66
+ console.log('');
67
+ console.log(c.warn(' ⚠ graphify not installed yet — actual run would fail.'));
68
+ console.log(c.dim(' Install: pip install graphifyy && graphify install'));
69
+ }
70
+ console.log('');
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ // Step 3: check graphify installed (only required for real execution)
76
+ if (!isGraphifyInstalled()) {
77
+ if (json) {
78
+ console.log(
79
+ JSON.stringify(
80
+ {
81
+ ok: false,
82
+ error: 'graphify not installed',
83
+ installCommand: 'pip install graphifyy && graphify install',
84
+ installUrl: 'https://github.com/safishamsi/graphify',
85
+ },
86
+ null,
87
+ 2
88
+ )
89
+ );
90
+ } else {
91
+ console.log('');
92
+ console.log(c.bold('lifeos brain') + c.dim(' — knowledge graph via graphify'));
93
+ console.log('');
94
+ console.log(` ${c.err('✗')} graphify not installed`);
95
+ console.log(c.dim(` graphify is the OSS knowledge-graph builder LifeOS uses for the brain.`));
96
+ console.log(c.dim(` It runs entirely on your machine — code parsing is 100% local`));
97
+ console.log(c.dim(` (tree-sitter AST), and semantic extraction goes through your`));
98
+ console.log(c.dim(` existing Claude Code / Cursor / Codex session (no separate API`));
99
+ console.log(c.dim(` key, no incremental cost).`));
100
+ console.log('');
101
+ console.log(` ${c.cyan('→ install: ')}pip install graphifyy && graphify install`);
102
+ console.log(` ${c.cyan('→ docs: ')}https://github.com/safishamsi/graphify`);
103
+ console.log(` ${c.cyan('→ preview: ')}lifeos brain --dry-run ${targetDir}`);
104
+ console.log('');
105
+ }
106
+ return 1;
107
+ }
108
+
109
+ // Step 4: real invocation
110
+ console.log('');
111
+ console.log(c.bold('lifeos brain') + c.dim(` — indexing ${plan.paths.length} path(s) via graphify`));
112
+ console.log(c.dim(` paths: ${plan.paths.join(', ')}`));
113
+ console.log(c.dim(` cwd: ${plan.cwd}`));
114
+ if (plan.missingPaths && plan.missingPaths.length > 0) {
115
+ console.log(c.warn(` ⚠ ${plan.missingPaths.length} path(s) don't exist — graphify may skip them: ${plan.missingPaths.join(', ')}`));
116
+ }
117
+ console.log('');
118
+ console.log(c.dim('--- graphify output below ---'));
119
+ console.log('');
120
+
121
+ const result = runBrain(targetDir, { dryRun: false });
122
+
123
+ console.log('');
124
+ if (result.ok) {
125
+ if (json) {
126
+ console.log(JSON.stringify({ ok: true, paths: result.paths, outputDir: result.outputDir }, null, 2));
127
+ } else {
128
+ console.log(c.bold(c.ok('✓ brain built')));
129
+ console.log(c.dim(` Output: ${result.outputDir}`));
130
+ console.log(c.dim(` View graph: open ${result.outputDir}/graph.html`));
131
+ console.log(c.dim(` Read summary: cat ${result.outputDir}/GRAPH_REPORT.md`));
132
+ console.log(c.dim(` Query graph: graphify query "your question"`));
133
+ console.log('');
134
+ }
135
+ return 0;
136
+ } else {
137
+ if (json) {
138
+ console.log(JSON.stringify({ ok: false, exitCode: result.exitCode }, null, 2));
139
+ } else {
140
+ console.log(c.err(`✗ graphify exited with code ${result.exitCode}`));
141
+ console.log(c.dim(' Check graphify output above for details. Common issues:'));
142
+ console.log(c.dim(' - Paths not readable / outside scope'));
143
+ console.log(c.dim(' - graphify install incomplete (run `graphify install` again)'));
144
+ console.log(c.dim(' - Underlying AI assistant session not configured'));
145
+ }
146
+ return result.exitCode || 1;
147
+ }
148
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `lifeos context` subcommand orchestrator.
3
+ *
4
+ * Thin wrapper around src/context.js — handles flags, output formatting, and
5
+ * exit codes. The planning/rendering logic (rank foci/quadrants/council by
6
+ * priority) is pure and lives in context.js.
7
+ *
8
+ * Flags:
9
+ * --json Emit the same data as structured JSON (for runtimes / scripting)
10
+ * --quadrant <id> Scope output to one quadrant (its council subset + foci)
11
+ */
12
+
13
+ import { planContext, renderContextMarkdown } from '../context.js';
14
+ import { c } from '../utils/colors.js';
15
+
16
+ export async function runContext(targetDir, flags = {}) {
17
+ const { json = false, quadrant = null } = flags;
18
+
19
+ const plan = planContext(targetDir, { quadrant });
20
+ if (!plan.ok) {
21
+ if (json) {
22
+ console.log(JSON.stringify(plan, null, 2));
23
+ } else {
24
+ console.log('');
25
+ console.log(` ${c.err('✗')} ${plan.error}`);
26
+ if (plan.fix) console.log(c.cyan(` → fix: ${plan.fix}`));
27
+ console.log('');
28
+ }
29
+ return 1;
30
+ }
31
+
32
+ if (json) {
33
+ console.log(JSON.stringify(plan, null, 2));
34
+ return 0;
35
+ }
36
+
37
+ // Plain markdown to stdout — no ANSI, so it pipes cleanly into a convening.
38
+ console.log(renderContextMarkdown(plan));
39
+ return 0;
40
+ }