From 17d4ae8c1431ab9ad6ffccd89666fa94e1172923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:20:46 +0100 Subject: [PATCH 1/9] fix: honor CLAUDE_CONFIG_DIR (non-default Claude Code config roots) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code supports multiple profiles via CLAUDE_CONFIG_DIR, but the hooks payload assumed the config root is always ~/.claude, at three levels: - hooks.json ships every command as $HOME/.claude/hooks/... and InstallHooks.ts merged them verbatim, so on a non-default profile all 60 hook entries pointed at files the installer never deployed there — every hook event errored. - hooks/lib/paths.ts getClaudeDir() only honored CLAUDE_PLUGIN_ROOT, never CLAUDE_CONFIG_DIR. - ~20 hook files bypassed lib/paths.ts with their own inline join(homedir(), '.claude', ...) spellings. Fixes, in the spirit of the agentic installer (DetectEnv.ts already detects CLAUDE_CONFIG_DIR correctly — the rest of the pipeline just ignored what it found): - InstallEngine.rewriteHooksConfigRoot(): retargets the default-root spellings ($HOME/${HOME}/~ + /.claude) in payload hook commands at the detected configRoot at merge time. No-op when configRoot IS ~/.claude, so default installs stay byte-identical. - getClaudeDir() falls back CLAUDE_PLUGIN_ROOT → CLAUDE_CONFIG_DIR → ~/.claude; getLifeosDir() default now derives from getClaudeDir(). - All hook files with inline .claude paths now route through the lib/paths.ts helpers (getClaudeDir/getSkillsDir/getSettingsPath/ paiPath). - install.sh skills-dir detection probes CLAUDE_CONFIG_DIR before the ~/.claude and ~/.config/claude defaults. - Nested skill-payload mirrors (install/skills/LifeOS/...) synced. Verified: InstallHooks.ts --apply against a sandbox config root rewrites all 60 commands (0 left on the default root), preserves pre-existing foreign hook entries, and the rewrite is a no-op for a default-root install. --- LifeOS/Tools/InstallEngine.ts | 22 +++++++++++++++++++ LifeOS/Tools/InstallHooks.ts | 11 ++++++++-- LifeOS/install/hooks/AgentInvocation.hook.ts | 5 ++--- LifeOS/install/hooks/ArtWorkflowGuard.hook.ts | 4 ++-- LifeOS/install/hooks/CheckpointPerISC.hook.ts | 3 ++- LifeOS/install/hooks/EgressClassGuard.hook.ts | 5 ++--- LifeOS/install/hooks/HookHealer.hook.ts | 4 ++-- LifeOS/install/hooks/ISARenderOnStop.hook.ts | 8 +++---- LifeOS/install/hooks/ISASync.hook.ts | 6 ++--- .../hooks/InstructionsLoadedHandler.hook.ts | 7 +++--- LifeOS/install/hooks/KittyEnvPersist.hook.ts | 4 ++-- LifeOS/install/hooks/LoadContext.hook.ts | 4 ++-- LifeOS/install/hooks/MemoryHealthGate.hook.ts | 5 ++--- LifeOS/install/hooks/ReminderRouter.hook.ts | 4 ++-- LifeOS/install/hooks/Safety.hook.ts | 10 ++------- LifeOS/install/hooks/SettingsBackport.hook.ts | 4 ++-- LifeOS/install/hooks/SystemFileGuard.hook.ts | 5 ++--- LifeOS/install/hooks/lib/identity.ts | 6 ++--- LifeOS/install/hooks/lib/notifications.ts | 4 ++-- LifeOS/install/hooks/lib/paths.ts | 19 ++++++++++++---- .../hooks/lib/system-file-guard-core.ts | 5 ++--- LifeOS/install/hooks/lib/work-config.ts | 4 ++-- LifeOS/install/install.sh | 5 ++++- .../skills/LifeOS/Tools/InstallEngine.ts | 22 +++++++++++++++++++ .../skills/LifeOS/Tools/InstallHooks.ts | 11 ++++++++-- .../install/skills/LifeOS/install/install.sh | 5 ++++- 26 files changed, 128 insertions(+), 64 deletions(-) diff --git a/LifeOS/Tools/InstallEngine.ts b/LifeOS/Tools/InstallEngine.ts index 41b71badb..835b8b629 100644 --- a/LifeOS/Tools/InstallEngine.ts +++ b/LifeOS/Tools/InstallEngine.ts @@ -548,6 +548,28 @@ function normalizeCommand(cmd: string): string { .trim(); } +/** + * Rewrite the default `~/.claude` config-root spellings inside hook commands to + * the actually-detected configRoot. The payload's hooks.json ships commands + * against the default root (`$HOME/.claude/...`); on a multi-profile setup + * (Claude Code's CLAUDE_CONFIG_DIR) those paths point at a different — usually + * nonexistent — profile, so every hook errors on startup. No-op when configRoot + * IS the default, keeping default installs byte-identical. Pure (no I/O). + */ +export function rewriteHooksConfigRoot(hooks: HooksMap, configRoot: string, homeDir: string): HooksMap { + if (resolve(configRoot) === resolve(join(homeDir, ".claude"))) return hooks; + const defaultRootPattern = /(\$\{HOME\}|\$HOME|~)\/\.claude(?=\/|\s|$)/g; + const rewritten: HooksMap = JSON.parse(JSON.stringify(hooks ?? {})); + for (const groups of Object.values(rewritten)) { + for (const group of groups) { + for (const h of group.hooks ?? []) { + if (typeof h.command === "string") h.command = h.command.replace(defaultRootPattern, configRoot); + } + } + } + return rewritten; +} + /** Identity key for a hook entry: http → url, else normalized command. */ function hookKey(h: HookEntry): string { if (h.type === "http" && h.url) return `http:${h.url}`; diff --git a/LifeOS/Tools/InstallHooks.ts b/LifeOS/Tools/InstallHooks.ts index 030fb9218..f3f60dd36 100755 --- a/LifeOS/Tools/InstallHooks.ts +++ b/LifeOS/Tools/InstallHooks.ts @@ -16,7 +16,7 @@ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { detectDevTree, mergeHooks } from "./InstallEngine"; +import { detectDevTree, mergeHooks, rewriteHooksConfigRoot } from "./InstallEngine"; interface Args { configRoot: string; skillRoot: string; apply: boolean; allowDev: boolean; } @@ -59,7 +59,14 @@ function main(): void { console.log(JSON.stringify({ ok: false, error: `payload hooks.json not found at ${hooksJsonPath}` }, null, 2)); process.exit(1); } - const incoming = JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {}; + // Payload commands are written against the default ~/.claude root; retarget + // them at the detected configRoot so non-default CLAUDE_CONFIG_DIR profiles + // get hooks that point at the files this installer actually deploys. + const incoming = rewriteHooksConfigRoot( + JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {}, + configRoot, + process.env.HOME || "", + ); // The hook SCRIPTS (*.hook.ts|sh + lib/**) live beside hooks.json in the payload. // Merging hooks.json into settings.json wires commands that point at these files, diff --git a/LifeOS/install/hooks/AgentInvocation.hook.ts b/LifeOS/install/hooks/AgentInvocation.hook.ts index b676be239..2837a49a1 100755 --- a/LifeOS/install/hooks/AgentInvocation.hook.ts +++ b/LifeOS/install/hooks/AgentInvocation.hook.ts @@ -19,8 +19,7 @@ import { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; -import { homedir } from 'os'; -import { paiPath } from './lib/paths'; +import { getClaudeDir, paiPath } from './lib/paths'; import { getISOTimestamp } from './lib/time'; import { EFFORT_MODEL, ALIAS, CROSS_VENDOR } from '../LIFEOS/TOOLS/models'; @@ -49,7 +48,7 @@ function resolveDispatch(subagentType: string, inputModel?: string): { model: st if (CROSS_VENDOR[cvKey]) return { model: CROSS_VENDOR[cvKey], level: 'cross-vendor' }; if (inputModel) return { model: inputModel, level: levelForModel(inputModel) }; try { - const fm = readFileSync(join(homedir(), '.claude', 'agents', `${subagentType}.md`), 'utf-8').slice(0, 4000); + const fm = readFileSync(join(getClaudeDir(), 'agents', `${subagentType}.md`), 'utf-8').slice(0, 4000); const m = fm.match(/^model:\s*(\S+)/m); if (m) return { model: m[1], level: `${levelForModel(m[1])}-pin` }; } catch { /* no agent file — built-in type */ } diff --git a/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts b/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts index 8172a86c5..f906335d3 100755 --- a/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts +++ b/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts @@ -16,6 +16,7 @@ */ import { readFileSync, readdirSync } from "node:fs"; +import { getSkillsDir } from "./lib/paths"; interface HookInput { session_id?: string; @@ -23,7 +24,6 @@ interface HookInput { tool_input?: { command?: string; description?: string }; } -const HOME = process.env.HOME ?? ""; const ART_TOOL_PATH_FRAGMENTS = [ "skills/Art/Tools/Generate.ts", ".claude/skills/Art/Tools/Generate.ts", @@ -39,7 +39,7 @@ function readHookInput(): HookInput { } function listWorkflows(): string[] { - const dir = `${HOME}/.claude/skills/Art/Workflows`; + const dir = `${getSkillsDir()}/Art/Workflows`; try { return readdirSync(dir) .filter((f) => f.endsWith(".md")) diff --git a/LifeOS/install/hooks/CheckpointPerISC.hook.ts b/LifeOS/install/hooks/CheckpointPerISC.hook.ts index d1f5eabd4..34479f730 100755 --- a/LifeOS/install/hooks/CheckpointPerISC.hook.ts +++ b/LifeOS/install/hooks/CheckpointPerISC.hook.ts @@ -24,12 +24,13 @@ import { execFileSync } from 'node:child_process'; import { basename, dirname, join } from 'node:path'; import { homedir } from 'node:os'; import { parseFrontmatter, parseCriteriaList, ARTIFACT_FILENAME, LEGACY_ARTIFACT_FILENAME } from './lib/isa-utils'; +import { getClaudeDir } from './lib/paths'; // Allowlist path: top of ~/.claude per spec. One absolute repo path per line; // '#' comments and blank // lines are ignored. Tilde and $HOME prefixes are expanded as a quality-of- // life feature so users can write `~/Projects/foo` instead of the long form. -const ALLOWLIST_PATH = join(homedir(), '.claude', 'checkpoint-repos.txt'); +const ALLOWLIST_PATH = join(getClaudeDir(), 'checkpoint-repos.txt'); const GIT_TIMEOUT_MS = 5000; interface CheckpointState { diff --git a/LifeOS/install/hooks/EgressClassGuard.hook.ts b/LifeOS/install/hooks/EgressClassGuard.hook.ts index 07a559351..3a9e99f4c 100755 --- a/LifeOS/install/hooks/EgressClassGuard.hook.ts +++ b/LifeOS/install/hooks/EgressClassGuard.hook.ts @@ -19,11 +19,10 @@ import { readFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; -import { homedir } from "node:os"; import { evaluateEgress } from "./lib/egress-class-core"; +import { paiPath } from "./lib/paths"; -const HOME = process.env.HOME ?? homedir(); -const LOG_PATH = join(HOME, ".claude/LIFEOS/MEMORY/OBSERVABILITY/egress-decisions.jsonl"); +const LOG_PATH = paiPath("MEMORY/OBSERVABILITY/egress-decisions.jsonl"); interface HookInput { session_id?: string; diff --git a/LifeOS/install/hooks/HookHealer.hook.ts b/LifeOS/install/hooks/HookHealer.hook.ts index 0eea7bb37..1898fb144 100755 --- a/LifeOS/install/hooks/HookHealer.hook.ts +++ b/LifeOS/install/hooks/HookHealer.hook.ts @@ -34,9 +34,9 @@ import { mkdirSync, openSync, readSync, closeSync, realpathSync, } from 'fs'; import { join } from 'path'; -import { homedir } from 'os'; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_DIR = join(homedir(), '.claude'); +const CLAUDE_DIR = getClaudeDir(); const OBS_DIR = join(CLAUDE_DIR, 'LIFEOS', 'MEMORY', 'OBSERVABILITY'); const LOG_FILE = join(OBS_DIR, 'hook-healer.jsonl'); const SETTINGS_FILES = ['settings.json', 'settings.local.json']; diff --git a/LifeOS/install/hooks/ISARenderOnStop.hook.ts b/LifeOS/install/hooks/ISARenderOnStop.hook.ts index 3758a22e6..1b30bd0bc 100755 --- a/LifeOS/install/hooks/ISARenderOnStop.hook.ts +++ b/LifeOS/install/hooks/ISARenderOnStop.hook.ts @@ -19,11 +19,11 @@ import { readFileSync, existsSync, unlinkSync } from 'fs'; import { spawn } from 'child_process'; -import { homedir } from 'os'; import { join, dirname } from 'path'; +import { paiPath } from './lib/paths'; -const STATE_DIR = join(homedir(), '.claude/LIFEOS/MEMORY/STATE/isa-render-debounce'); -const ISA_RENDER = join(homedir(), '.claude/LIFEOS/TOOLS/ISARender.ts'); +const STATE_DIR = paiPath('MEMORY/STATE/isa-render-debounce'); +const ISA_RENDER = paiPath('TOOLS/ISARender.ts'); /** * Has this ISA reached completion at least once? This is the real gate the @@ -108,7 +108,7 @@ try { unlinkSync(stateFile); } catch {} if (rendered.length || skipped.length) { try { const { appendFileSync, mkdirSync } = require('fs'); - const logDir = join(homedir(), '.claude/LIFEOS/MEMORY/OBSERVABILITY'); + const logDir = paiPath('MEMORY/OBSERVABILITY'); mkdirSync(logDir, { recursive: true }); appendFileSync(join(logDir, 'isa-render.jsonl'), JSON.stringify({ diff --git a/LifeOS/install/hooks/ISASync.hook.ts b/LifeOS/install/hooks/ISASync.hook.ts index 557d9c587..7c7468fb2 100755 --- a/LifeOS/install/hooks/ISASync.hook.ts +++ b/LifeOS/install/hooks/ISASync.hook.ts @@ -20,8 +20,8 @@ import { readFileSync, existsSync } from 'fs'; import { spawn } from 'child_process'; -import { homedir } from 'os'; import { join } from 'path'; +import { paiPath } from './lib/paths'; import { parseFrontmatter, syncToWorkJson, @@ -103,7 +103,7 @@ async function main() { // constantly remaking the HTML file." if (newPhase === 'COMPLETE' && oldPhase !== 'COMPLETE' && fm.slug) { try { - const isaRender = join(homedir(), '.claude/LIFEOS/TOOLS/ISARender.ts'); + const isaRender = paiPath('TOOLS/ISARender.ts'); const proc = spawn('bun', [isaRender, isaPath], { detached: true, stdio: 'ignore', @@ -120,7 +120,7 @@ async function main() { // pre-completion edits never trigger renders even though they show up here. if (input.session_id) { try { - const stateDir = join(homedir(), '.claude/LIFEOS/MEMORY/STATE/isa-render-debounce'); + const stateDir = paiPath('MEMORY/STATE/isa-render-debounce'); const stateFile = join(stateDir, `${input.session_id}.json`); const { mkdirSync, writeFileSync } = require('fs'); mkdirSync(stateDir, { recursive: true }); diff --git a/LifeOS/install/hooks/InstructionsLoadedHandler.hook.ts b/LifeOS/install/hooks/InstructionsLoadedHandler.hook.ts index db0276964..5d99f62fc 100755 --- a/LifeOS/install/hooks/InstructionsLoadedHandler.hook.ts +++ b/LifeOS/install/hooks/InstructionsLoadedHandler.hook.ts @@ -24,21 +24,20 @@ import { existsSync, mkdirSync } from 'fs'; import { join } from 'path'; -import { homedir } from 'os'; +import { getClaudeDir, getLifeosDir } from './lib/paths'; // ======================================== // Configuration // ======================================== -const HOME = homedir(); -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, '.claude', 'LIFEOS'); +const LIFEOS_DIR = getLifeosDir(); const STATE_DIR = join(LIFEOS_DIR, 'MEMORY', 'STATE'); const HASHES_FILE = join(STATE_DIR, 'instruction-hashes.json'); const INTEGRITY_LOG = join(STATE_DIR, 'instruction-integrity.jsonl'); /** Critical LifeOS instruction files to monitor */ const CRITICAL_FILES: Record = { - 'CLAUDE.md': join(HOME, '.claude', 'CLAUDE.md'), + 'CLAUDE.md': join(getClaudeDir(), 'CLAUDE.md'), 'SYSTEM-PROMPT': join(LIFEOS_DIR, 'LIFEOS_SYSTEM_PROMPT.md'), 'DA_IDENTITY': join(LIFEOS_DIR, 'USER', 'DA_IDENTITY.md'), 'PRINCIPAL_IDENTITY': join(LIFEOS_DIR, 'USER', 'PRINCIPAL_IDENTITY.md'), diff --git a/LifeOS/install/hooks/KittyEnvPersist.hook.ts b/LifeOS/install/hooks/KittyEnvPersist.hook.ts index 8bfcf7ea7..a49e751c4 100755 --- a/LifeOS/install/hooks/KittyEnvPersist.hook.ts +++ b/LifeOS/install/hooks/KittyEnvPersist.hook.ts @@ -12,7 +12,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { join } from 'path'; -import { getLifeosDir } from './lib/paths'; +import { getClaudeDir, getLifeosDir } from './lib/paths'; import { setTabState, readTabState, persistKittySession } from './lib/tab-setter'; import { getDAName } from './lib/identity'; @@ -20,7 +20,7 @@ const paiDir = getLifeosDir(); // Skip for subagents const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || ''; -const isSubagent = claudeProjectDir.includes('/.claude/Agents/') || +const isSubagent = claudeProjectDir.includes(join(getClaudeDir(), 'Agents') + '/') || process.env.CLAUDE_AGENT_TYPE !== undefined; if (isSubagent) process.exit(0); diff --git a/LifeOS/install/hooks/LoadContext.hook.ts b/LifeOS/install/hooks/LoadContext.hook.ts index 3bdc4767a..9f7fae1bd 100755 --- a/LifeOS/install/hooks/LoadContext.hook.ts +++ b/LifeOS/install/hooks/LoadContext.hook.ts @@ -39,7 +39,7 @@ import { readFileSync, existsSync, readdirSync } from 'fs'; import { join } from 'path'; -import { getLifeosDir, getSettingsPath } from './lib/paths'; +import { getClaudeDir, getLifeosDir, getSettingsPath } from './lib/paths'; import { recordSessionStart } from './lib/notifications'; import { loadLearningDigest, loadWisdomFrames, loadFailurePatterns, loadSignalTrends, loadSynthesisPatterns } from './lib/learning-readback'; import { findArtifactPath } from './lib/isa-utils'; @@ -386,7 +386,7 @@ async function main() { try { // Subagents don't need dynamic context injection const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || ''; - const isSubagent = claudeProjectDir.includes('/.claude/Agents/') || + const isSubagent = claudeProjectDir.includes(join(getClaudeDir(), 'Agents') + '/') || process.env.CLAUDE_AGENT_TYPE !== undefined; if (isSubagent) { diff --git a/LifeOS/install/hooks/MemoryHealthGate.hook.ts b/LifeOS/install/hooks/MemoryHealthGate.hook.ts index b2a654b56..e14db29bc 100755 --- a/LifeOS/install/hooks/MemoryHealthGate.hook.ts +++ b/LifeOS/install/hooks/MemoryHealthGate.hook.ts @@ -14,10 +14,9 @@ */ import { execFileSync } from "node:child_process"; -import { join } from "node:path"; +import { paiPath } from "./lib/paths"; -const HOME = process.env.HOME || ""; -const CHECK = join(HOME, ".claude/LIFEOS/TOOLS/MemoryHealthCheck.ts"); +const CHECK = paiPath("TOOLS/MemoryHealthCheck.ts"); try { const out = execFileSync("bun", [CHECK], { diff --git a/LifeOS/install/hooks/ReminderRouter.hook.ts b/LifeOS/install/hooks/ReminderRouter.hook.ts index 8635ab041..b90e4dc7a 100755 --- a/LifeOS/install/hooks/ReminderRouter.hook.ts +++ b/LifeOS/install/hooks/ReminderRouter.hook.ts @@ -22,9 +22,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; import { createHash } from "crypto"; import { join, dirname } from "path"; import { loadWorkConfig } from "./lib/work-config"; +import { paiPath } from "./lib/paths"; -const HOME = process.env.HOME || ""; -const STATE_PATH = join(HOME, ".claude", "LIFEOS", "MEMORY", "STATE", "reminder-router-seen.json"); +const STATE_PATH = paiPath("MEMORY", "STATE", "reminder-router-seen.json"); interface HookInput { session_id?: string; diff --git a/LifeOS/install/hooks/Safety.hook.ts b/LifeOS/install/hooks/Safety.hook.ts index 4afd1b5f6..068e28659 100755 --- a/LifeOS/install/hooks/Safety.hook.ts +++ b/LifeOS/install/hooks/Safety.hook.ts @@ -39,7 +39,6 @@ import { statSync, writeFileSync, } from "node:fs"; -import { homedir } from "node:os"; import { join } from "node:path"; import { createHash } from "node:crypto"; import { @@ -48,17 +47,12 @@ import { type Classification, type ToolCall, } from "./lib/safety-classifier"; +import { getLifeosDir } from "./lib/paths"; const STDIN_CAP_BYTES = 2 * 1024 * 1024; const CACHE_MAX_BYTES = 10 * 1024 * 1024; -const HOME = homedir(); -const LIFEOS_DIR = process.env.LIFEOS_DIR - ? process.env.LIFEOS_DIR.replace(/^~(?=\/|$)/, HOME).replace( - /^\$\{?HOME\}?(?=\/|$)/, - HOME, - ) - : join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const STATE_DIR = join(LIFEOS_DIR, "MEMORY", "STATE"); const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY"); const CACHE_PATH = join(STATE_DIR, "permission-cache.json"); diff --git a/LifeOS/install/hooks/SettingsBackport.hook.ts b/LifeOS/install/hooks/SettingsBackport.hook.ts index b7e6ae0ff..c66346600 100755 --- a/LifeOS/install/hooks/SettingsBackport.hook.ts +++ b/LifeOS/install/hooks/SettingsBackport.hook.ts @@ -14,9 +14,9 @@ import { readFileSync } from 'fs'; import { execSync } from 'child_process'; import { homedir } from 'os'; import { join } from 'path'; -import { paiPath } from './lib/paths'; +import { getSettingsPath, paiPath } from './lib/paths'; -const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json'); +const SETTINGS_PATH = getSettingsPath(); const BACKPORT = paiPath('TOOLS', 'SettingsBackport.ts'); let input: any; diff --git a/LifeOS/install/hooks/SystemFileGuard.hook.ts b/LifeOS/install/hooks/SystemFileGuard.hook.ts index e6927ba88..9f4815b9d 100755 --- a/LifeOS/install/hooks/SystemFileGuard.hook.ts +++ b/LifeOS/install/hooks/SystemFileGuard.hook.ts @@ -21,11 +21,10 @@ import { readFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; -import { homedir } from "node:os"; import { evaluateWrite, extractNewContent } from "./lib/system-file-guard-core"; +import { paiPath } from "./lib/paths"; -const HOME = process.env.HOME ?? homedir(); -const LOG_PATH = join(HOME, ".claude/LIFEOS/MEMORY/OBSERVABILITY/system-file-guard.jsonl"); +const LOG_PATH = paiPath("MEMORY/OBSERVABILITY/system-file-guard.jsonl"); interface HookInput { session_id?: string; diff --git a/LifeOS/install/hooks/lib/identity.ts b/LifeOS/install/hooks/lib/identity.ts index 0ae796f9c..78b12b9dd 100755 --- a/LifeOS/install/hooks/lib/identity.ts +++ b/LifeOS/install/hooks/lib/identity.ts @@ -14,9 +14,9 @@ import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { parse as parseYaml } from 'yaml'; import { loadLifeosConfig } from '../../LIFEOS/TOOLS/LifeosConfig'; +import { getSettingsPath, paiPath } from './paths'; -const HOME = process.env.HOME!; -const SETTINGS_PATH = join(HOME, '.claude/settings.json'); +const SETTINGS_PATH = getSettingsPath(); // Identity-file paths derive from LifeosConfig's userDir. On fresh installs where // LIFEOS_CONFIG.toml hasn't been created yet, fall back to the conventional @@ -26,7 +26,7 @@ function paiUserDir(): string { try { return loadLifeosConfig().paths.userDir; } catch { - return join(HOME, '.claude/LIFEOS/USER'); + return paiPath('USER'); } } const DA_IDENTITY_PATH = join(paiUserDir(), 'DIGITAL_ASSISTANT/DA_IDENTITY.md'); diff --git a/LifeOS/install/hooks/lib/notifications.ts b/LifeOS/install/hooks/lib/notifications.ts index 54a52f4e4..da3d246a8 100755 --- a/LifeOS/install/hooks/lib/notifications.ts +++ b/LifeOS/install/hooks/lib/notifications.ts @@ -7,9 +7,9 @@ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join } from 'path'; +import { paiPath } from './paths'; -const HOME = process.env.HOME!; -const PULSE_TOML_PATH = join(HOME, '.claude/LIFEOS/PULSE/PULSE.toml'); +const PULSE_TOML_PATH = paiPath('PULSE/PULSE.toml'); // ============================================================================ // Session Timing diff --git a/LifeOS/install/hooks/lib/paths.ts b/LifeOS/install/hooks/lib/paths.ts index 205c4ef7f..b9174d7e3 100755 --- a/LifeOS/install/hooks/lib/paths.ts +++ b/LifeOS/install/hooks/lib/paths.ts @@ -31,7 +31,7 @@ export function expandPath(path: string): string { * Priority: * 1. CLAUDE_PLUGIN_ROOT (plugin install) → /PAI * 2. LIFEOS_DIR env var (expanded) - * 3. ~/.claude/LIFEOS (live default — byte-identical to pre-plugin behavior) + * 3. /LIFEOS (getClaudeDir() — honors CLAUDE_CONFIG_DIR, defaults ~/.claude/LIFEOS) * * The CLAUDE_PLUGIN_ROOT guard MUST precede the LIFEOS_DIR check: in a packed * plugin, bin/pai exports LIFEOS_DIR equal to CLAUDE_PLUGIN_ROOT (the flattened @@ -51,7 +51,7 @@ export function getLifeosDir(): string { return expandPath(envLifeosDir); } - return join(homedir(), '.claude', 'LIFEOS'); + return join(getClaudeDir(), 'LIFEOS'); } /** @@ -59,8 +59,13 @@ export function getLifeosDir(): string { * * Plugin install: CLAUDE_PLUGIN_ROOT is the flattened plugin root that plays the * live ~/.claude role (skills/ and hooks/ sit directly under it, matching live - * .claude/skills and .claude/hooks). Live default: ~/.claude — byte-identical to - * pre-plugin behavior, since CLAUDE_PLUGIN_ROOT is unset on a normal install. + * .claude/skills and .claude/hooks). + * + * Live install: CLAUDE_CONFIG_DIR is Claude Code's own override for its config + * directory (multi-profile setups). When the harness runs from a non-default + * config dir, hooks inherit that env var, so honoring it keeps every LifeOS + * path inside the profile that actually loaded the hooks. Default: ~/.claude — + * byte-identical to prior behavior when neither env var is set. */ export function getClaudeDir(): string { const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT; @@ -69,6 +74,12 @@ export function getClaudeDir(): string { return expandPath(pluginRoot); } + const configDir = process.env.CLAUDE_CONFIG_DIR; + + if (configDir) { + return expandPath(configDir); + } + return join(homedir(), '.claude'); } diff --git a/LifeOS/install/hooks/lib/system-file-guard-core.ts b/LifeOS/install/hooks/lib/system-file-guard-core.ts index 4eb835cd6..55de3377c 100644 --- a/LifeOS/install/hooks/lib/system-file-guard-core.ts +++ b/LifeOS/install/hooks/lib/system-file-guard-core.ts @@ -12,11 +12,10 @@ import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; import { isContained, isPatternAllowlisted, relativeToClaudeRoot } from "./containment-zones"; +import { getClaudeDir } from "./paths"; -const HOME = process.env.HOME ?? homedir(); -const CLAUDE_ROOT = join(HOME, ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const DEFAULT_DENY_LIST_PATH = join(CLAUDE_ROOT, "skills/_LIFEOS/DENY_LIST.txt"); export type GuardClassification = "system" | "user" | "out-of-tree"; diff --git a/LifeOS/install/hooks/lib/work-config.ts b/LifeOS/install/hooks/lib/work-config.ts index 7e4643ac9..bba597215 100755 --- a/LifeOS/install/hooks/lib/work-config.ts +++ b/LifeOS/install/hooks/lib/work-config.ts @@ -26,11 +26,11 @@ */ import { existsSync, readFileSync, writeFileSync, chmodSync } from "fs"; import { join } from "path"; +import { getLifeosDir } from "./paths"; declare const Bun: { spawnSync: (cmd: string[], opts?: any) => any }; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const REPO_JSON_PATH = join(LIFEOS_DIR, "USER", "WORK", "work_repo.json"); const COLUMNS_YAML_PATH = join(LIFEOS_DIR, "USER", "WORK", "config.yaml"); diff --git a/LifeOS/install/install.sh b/LifeOS/install/install.sh index 19a7de744..113508a6b 100755 --- a/LifeOS/install/install.sh +++ b/LifeOS/install/install.sh @@ -100,7 +100,10 @@ fi # ─── Step 2: Detect harness (no clobber) ───────────────────────── step "2/5 Detecting your harness" if [ -z "$LIFEOS_SKILLS_DIR" ]; then - if [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" + # CLAUDE_CONFIG_DIR is Claude Code's own config-dir override (multi-profile + # setups) — when set, it IS the config root, so probe it before the defaults. + if [ -n "${CLAUDE_CONFIG_DIR:-}" ] && [ -d "$CLAUDE_CONFIG_DIR" ]; then LIFEOS_SKILLS_DIR="$CLAUDE_CONFIG_DIR/skills" + elif [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" elif [ -d "$HOME/.config/claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.config/claude/skills" else LIFEOS_SKILLS_DIR="$HOME/.claude/skills"; fi fi diff --git a/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts b/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts index c9970874b..d204863d7 100644 --- a/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts +++ b/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts @@ -548,6 +548,28 @@ function normalizeCommand(cmd: string): string { .trim(); } +/** + * Rewrite the default `~/.claude` config-root spellings inside hook commands to + * the actually-detected configRoot. The payload's hooks.json ships commands + * against the default root (`$HOME/.claude/...`); on a multi-profile setup + * (Claude Code's CLAUDE_CONFIG_DIR) those paths point at a different — usually + * nonexistent — profile, so every hook errors on startup. No-op when configRoot + * IS the default, keeping default installs byte-identical. Pure (no I/O). + */ +export function rewriteHooksConfigRoot(hooks: HooksMap, configRoot: string, homeDir: string): HooksMap { + if (resolve(configRoot) === resolve(join(homeDir, ".claude"))) return hooks; + const defaultRootPattern = /(\$\{HOME\}|\$HOME|~)\/\.claude(?=\/|\s|$)/g; + const rewritten: HooksMap = JSON.parse(JSON.stringify(hooks ?? {})); + for (const groups of Object.values(rewritten)) { + for (const group of groups) { + for (const h of group.hooks ?? []) { + if (typeof h.command === "string") h.command = h.command.replace(defaultRootPattern, configRoot); + } + } + } + return rewritten; +} + /** Identity key for a hook entry: http → url, else normalized command. */ function hookKey(h: HookEntry): string { if (h.type === "http" && h.url) return `http:${h.url}`; diff --git a/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts b/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts index 030fb9218..f3f60dd36 100755 --- a/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts +++ b/LifeOS/install/skills/LifeOS/Tools/InstallHooks.ts @@ -16,7 +16,7 @@ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { detectDevTree, mergeHooks } from "./InstallEngine"; +import { detectDevTree, mergeHooks, rewriteHooksConfigRoot } from "./InstallEngine"; interface Args { configRoot: string; skillRoot: string; apply: boolean; allowDev: boolean; } @@ -59,7 +59,14 @@ function main(): void { console.log(JSON.stringify({ ok: false, error: `payload hooks.json not found at ${hooksJsonPath}` }, null, 2)); process.exit(1); } - const incoming = JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {}; + // Payload commands are written against the default ~/.claude root; retarget + // them at the detected configRoot so non-default CLAUDE_CONFIG_DIR profiles + // get hooks that point at the files this installer actually deploys. + const incoming = rewriteHooksConfigRoot( + JSON.parse(readFileSync(hooksJsonPath, "utf-8"))?.hooks ?? {}, + configRoot, + process.env.HOME || "", + ); // The hook SCRIPTS (*.hook.ts|sh + lib/**) live beside hooks.json in the payload. // Merging hooks.json into settings.json wires commands that point at these files, diff --git a/LifeOS/install/skills/LifeOS/install/install.sh b/LifeOS/install/skills/LifeOS/install/install.sh index 19a7de744..113508a6b 100755 --- a/LifeOS/install/skills/LifeOS/install/install.sh +++ b/LifeOS/install/skills/LifeOS/install/install.sh @@ -100,7 +100,10 @@ fi # ─── Step 2: Detect harness (no clobber) ───────────────────────── step "2/5 Detecting your harness" if [ -z "$LIFEOS_SKILLS_DIR" ]; then - if [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" + # CLAUDE_CONFIG_DIR is Claude Code's own config-dir override (multi-profile + # setups) — when set, it IS the config root, so probe it before the defaults. + if [ -n "${CLAUDE_CONFIG_DIR:-}" ] && [ -d "$CLAUDE_CONFIG_DIR" ]; then LIFEOS_SKILLS_DIR="$CLAUDE_CONFIG_DIR/skills" + elif [ -d "$HOME/.claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.claude/skills" elif [ -d "$HOME/.config/claude" ]; then LIFEOS_SKILLS_DIR="$HOME/.config/claude/skills" else LIFEOS_SKILLS_DIR="$HOME/.claude/skills"; fi fi From 1f9218472c64688697348e6ea2ee268fdaf5ed4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:25:57 +0100 Subject: [PATCH 2/9] fix: route always-on LIFEOS/TOOLS through a shared config-root resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to the hooks fix: the tools that hooks invoke on every turn (MemoryHealthCheck, FreshnessCache, SettingsBackport, MemoryReviewer, ISARender) and the shared LifeosConfig loader still hardcoded ~/.claude, so on a non-default CLAUDE_CONFIG_DIR profile they read from — and wrote observability/state files into — the wrong profile. Adds LIFEOS/TOOLS/Paths.ts mirroring hooks/lib/paths.ts resolution (CLAUDE_PLUGIN_ROOT → CLAUDE_CONFIG_DIR → ~/.claude; LIFEOS_DIR override for the data dir) and routes those tools' root constants through it. Default installs resolve to the same paths as before. --- LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts | 4 +- LifeOS/install/LIFEOS/TOOLS/ISARender.ts | 12 ++-- LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts | 9 +-- .../install/LIFEOS/TOOLS/MemoryHealthCheck.ts | 4 +- LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts | 8 +-- LifeOS/install/LIFEOS/TOOLS/Paths.ts | 58 +++++++++++++++++++ .../install/LIFEOS/TOOLS/SettingsBackport.ts | 3 +- 7 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 LifeOS/install/LIFEOS/TOOLS/Paths.ts diff --git a/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts b/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts index 1e2c12339..75ed841c6 100755 --- a/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts +++ b/LifeOS/install/LIFEOS/TOOLS/FreshnessCache.ts @@ -15,9 +15,9 @@ import { writeFileSync, renameSync, mkdirSync, existsSync } from "fs"; import { join } from "path"; import { readContextFreshness } from "./TelosFreshness"; +import { getLifeosDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const CACHE_DIR = join(LIFEOS_DIR, "USER", "CACHE"); const CACHE_PATH = join(CACHE_DIR, "freshness.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/ISARender.ts b/LifeOS/install/LIFEOS/TOOLS/ISARender.ts index aa6520e5c..c8beb31eb 100644 --- a/LifeOS/install/LIFEOS/TOOLS/ISARender.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ISARender.ts @@ -18,18 +18,18 @@ import { readFileSync, writeFileSync, existsSync, statSync, renameSync, readdirSync } from "node:fs"; import { resolve, dirname, basename, join } from "node:path"; import { spawn } from "node:child_process"; -import { homedir } from "node:os"; +import { getLifeosDir } from "./Paths"; -const HOME = process.env.HOME || homedir(); -const TOOLS_DIR = resolve(HOME, ".claude/LIFEOS/TOOLS"); +const LIFEOS_DIR = getLifeosDir(); +const TOOLS_DIR = join(LIFEOS_DIR, "TOOLS"); const TEMPLATE_HTML = join(TOOLS_DIR, "ISARender/template.html"); const TEMPLATE_CSS = join(TOOLS_DIR, "ISARender/template.css"); // Brand logo: user override via LIFEOS_BRAND_LOGO_PATH env var (absolute path), // else system default under PAI/ASSETS/, else inert (empty src). const BRAND_LOGO_PATH_OVERRIDE = process.env.LIFEOS_BRAND_LOGO_PATH ?? ""; -const BRAND_LOGO_PATH_DEFAULT = resolve(HOME, ".claude/LIFEOS/ASSETS/pai-logo.png"); -const WORK_DIR = resolve(HOME, ".claude/LIFEOS/MEMORY/WORK"); -const WORK_JSON = resolve(HOME, ".claude/LIFEOS/MEMORY/STATE/work.json"); +const BRAND_LOGO_PATH_DEFAULT = join(LIFEOS_DIR, "ASSETS/pai-logo.png"); +const WORK_DIR = join(LIFEOS_DIR, "MEMORY/WORK"); +const WORK_JSON = join(LIFEOS_DIR, "MEMORY/STATE/work.json"); const PHASES = ["observe", "think", "plan", "build", "execute", "verify", "learn", "complete"]; diff --git a/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts b/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts index ca5d03d9b..6f694a764 100644 --- a/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LifeosConfig.ts @@ -20,6 +20,7 @@ import { existsSync, statSync } from "node:fs"; import { resolve } from "node:path"; import { homedir } from "node:os"; +import { getClaudeDir } from "./Paths"; // Expand leading `~` (and `~/`) to the user's home directory. node:fs APIs do // not expand tildes, so any path returned from this loader must be absolute. @@ -85,7 +86,7 @@ export interface LifeosConfig { // ─────────── Resolution ─────────── const DEFAULT_HOME = process.env.HOME || homedir(); -const DEFAULT_CONFIG_PATH = resolve(DEFAULT_HOME, ".claude/LIFEOS/USER/CONFIG/LIFEOS_CONFIG.toml"); +const DEFAULT_CONFIG_PATH = resolve(getClaudeDir(), "LIFEOS/USER/CONFIG/LIFEOS_CONFIG.toml"); let cache: { config: LifeosConfig; mtime: number; path: string } | null = null; @@ -133,7 +134,7 @@ export function paiUserDir(): string { try { return loadLifeosConfig().paths.userDir; } catch { - return resolve(DEFAULT_HOME, ".claude/LIFEOS/USER"); + return resolve(getClaudeDir(), "LIFEOS/USER"); } } @@ -188,10 +189,10 @@ function validateAndNormalize(raw: unknown, path: string): LifeosConfig { }, paths: { userDir: expandHome( - root.paths?.userDir ?? root.paths?.user_dir ?? resolve(DEFAULT_HOME, ".claude/LIFEOS/USER"), + root.paths?.userDir ?? root.paths?.user_dir ?? resolve(getClaudeDir(), "LIFEOS/USER"), ), memoryDir: expandHome( - root.paths?.memoryDir ?? root.paths?.memory_dir ?? resolve(DEFAULT_HOME, ".claude/LIFEOS/MEMORY"), + root.paths?.memoryDir ?? root.paths?.memory_dir ?? resolve(getClaudeDir(), "LIFEOS/MEMORY"), ), projectsDir: expandHome( root.paths?.projectsDir ?? root.paths?.projects_dir ?? resolve(DEFAULT_HOME, "Projects"), diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts index 68d061ecd..6183323a5 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryHealthCheck.ts @@ -23,9 +23,9 @@ import { existsSync, readFileSync, appendFileSync, mkdirSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const CLAUDE = join(HOME, ".claude"); +const CLAUDE = getClaudeDir(); const HOOKS_DIR = join(CLAUDE, "hooks"); const TOOLS_DIR = join(CLAUDE, "LIFEOS/TOOLS"); const OBS_DIR = join(CLAUDE, "LIFEOS/MEMORY/OBSERVABILITY"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts index 5a0521093..ea4b90483 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts @@ -40,7 +40,6 @@ import { writeFileSync, } from "node:fs"; import { dirname, join as pathJoin, resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; import { add as memoryAdd, type AddResult } from "./MemorySystem"; import { read as memoryWriterRead } from "./MemoryWriter"; @@ -51,11 +50,12 @@ import { markProposal, logProposalEvent, } from "../PULSE/lib/telegram-proposals"; +import { getClaudeDir } from "./Paths"; // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); -const HARNESS_PROJECTS_DIR = pathResolve(homedir(), ".claude", "projects"); +const CLAUDE_ROOT = getClaudeDir(); +const HARNESS_PROJECTS_DIR = pathJoin(CLAUDE_ROOT, "projects"); const RUNS_LOG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-runs.jsonl"); const RUNS_DEBUG_DIR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-runs"); const REVIEW_CONFIG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/CONFIG/memory-review.json"); @@ -648,7 +648,7 @@ async function smokeTest(): Promise { const mockResponse = JSON.stringify({ items: [ { type: "memory", actor: "principal", content: "PREFERENCE: smoke E2E mock" }, - { type: "proposal", target_file: pathJoin(homedir(), ".claude/LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), edit: "RULE: E2E mock", confidence: 0.5, rationale: "smoke" }, + { type: "proposal", target_file: pathJoin(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), edit: "RULE: E2E mock", confidence: 0.5, rationale: "smoke" }, ], }); diff --git a/LifeOS/install/LIFEOS/TOOLS/Paths.ts b/LifeOS/install/LIFEOS/TOOLS/Paths.ts new file mode 100644 index 000000000..3150e0633 --- /dev/null +++ b/LifeOS/install/LIFEOS/TOOLS/Paths.ts @@ -0,0 +1,58 @@ +/** + * Paths.ts — centralized config-root resolution for LIFEOS/TOOLS. + * + * Mirrors hooks/lib/paths.ts (the hooks' resolver) so runtime tools agree with + * the hooks about where the Claude config root lives: + * + * 1. CLAUDE_PLUGIN_ROOT (plugin install) — the flattened plugin root + * 2. CLAUDE_CONFIG_DIR — Claude Code's own config-dir override + * (multi-profile setups) + * 3. ~/.claude — the default, byte-identical to prior behavior + * + * LIFEOS data resolves via LIFEOS_DIR when set, else /LIFEOS. + */ + +import { homedir } from "node:os"; +import { join } from "node:path"; + +/** Expand $HOME, ${HOME}, and ~ prefixes in a path string. */ +export function expandPath(path: string): string { + const home = homedir(); + + return path + .replace(/^\$HOME(?=\/|$)/, home) + .replace(/^\$\{HOME\}(?=\/|$)/, home) + .replace(/^~(?=\/|$)/, home); +} + +/** Get the Claude Code config directory (see resolution order above). */ +export function getClaudeDir(): string { + const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT; + + if (pluginRoot) { + return expandPath(pluginRoot); + } + + const configDir = process.env.CLAUDE_CONFIG_DIR; + + if (configDir) { + return expandPath(configDir); + } + + return join(homedir(), ".claude"); +} + +/** Get the LifeOS data directory: LIFEOS_DIR when set, else /LIFEOS. */ +export function getLifeosDir(): string { + if (process.env.CLAUDE_PLUGIN_ROOT) { + return join(getClaudeDir(), "LIFEOS"); + } + + const envLifeosDir = process.env.LIFEOS_DIR; + + if (envLifeosDir) { + return expandPath(envLifeosDir); + } + + return join(getClaudeDir(), "LIFEOS"); +} diff --git a/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts b/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts index 769bd0487..c90316323 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SettingsBackport.ts @@ -27,8 +27,9 @@ import { existsSync } from "node:fs"; import path from "node:path"; import os from "node:os"; import { mergeSettings, deepEqual, parseJsonFileOrThrow } from "./MergeSettings"; +import { getClaudeDir } from "./Paths"; -const CLAUDE_DIR = path.join(os.homedir(), ".claude"); +const CLAUDE_DIR = getClaudeDir(); const SYSTEM_PATH = path.join(CLAUDE_DIR, "settings.system.json"); const USER_PATH = path.join(CLAUDE_DIR, "LIFEOS", "USER", "CONFIG", "settings.user.json"); const GENERATED_PATH = path.join(CLAUDE_DIR, "settings.json"); From 4c6cc96db162baa92f028d5cd4953ba8613ed485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:31:42 +0100 Subject: [PATCH 3/9] fix: sweep remaining inline ~/.claude fallbacks in hooks and tool imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second sweep after runtime verification against a non-default CLAUDE_CONFIG_DIR profile caught stragglers the first pass missed: - 13 hooks + handlers/UpdateCounts.ts still resolved LIFEOS_DIR or the Claude root with inline 'process.env.LIFEOS_DIR || join(HOME, ".claude", ...)' / 'resolve(homedir(), ".claude")' fallbacks — they wrote MEMORY state/observability files into ~/.claude from a ~/.claude-gc profile. All now route through lib/paths.ts. - lib/safety-classifier.ts TRUSTED_PREFIXES trusted the literal ~/.claude instead of the active config root. - Five LIFEOS/TOOLS files (lifeos.ts launcher, algorithm.ts, TranscriptParser.ts, IntegrityMaintenance.ts, SessionHarvester.ts) imported hooks/lib via '../../../.claude/hooks/lib/...' — a relative path that climbs OUT of the config root and back in through a literal '.claude' segment. Now the structure-relative '../../hooks/lib/...', which resolves in any profile. Verified: all 46 wired hooks execute from a ~/.claude-gc profile with zero module-resolution or path failures, write state/observability only under the active profile, and leave ~/.claude untouched. Display-only strings that mention ~/.claude in user-facing messages are left as-is. --- LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts | 2 +- LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts | 2 +- LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts | 2 +- LifeOS/install/LIFEOS/TOOLS/algorithm.ts | 2 +- LifeOS/install/LIFEOS/TOOLS/lifeos.ts | 2 +- LifeOS/install/hooks/DriftReminder.hook.ts | 3 ++- LifeOS/install/hooks/LastResponseCache.hook.ts | 3 ++- LifeOS/install/hooks/LoadMemory.hook.ts | 4 ++-- LifeOS/install/hooks/MemoryDeltaSurface.hook.ts | 4 ++-- LifeOS/install/hooks/MemoryReviewFire.hook.ts | 4 ++-- LifeOS/install/hooks/MemoryReviewTrigger.hook.ts | 4 ++-- LifeOS/install/hooks/OutputFormatGate.hook.ts | 3 ++- LifeOS/install/hooks/PromptProcessing.hook.ts | 3 ++- LifeOS/install/hooks/SatisfactionCapture.hook.ts | 3 ++- LifeOS/install/hooks/SessionCleanup.hook.ts | 3 ++- LifeOS/install/hooks/SkillSurface.hook.ts | 5 +++-- LifeOS/install/hooks/WorkCompletionLearning.hook.ts | 3 ++- LifeOS/install/hooks/WritingGate.hook.ts | 3 ++- LifeOS/install/hooks/handlers/UpdateCounts.ts | 3 ++- LifeOS/install/hooks/lib/safety-classifier.ts | 3 ++- 20 files changed, 36 insertions(+), 25 deletions(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts b/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts index 62f028319..1f792e920 100755 --- a/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts +++ b/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts @@ -22,7 +22,7 @@ import { spawn } from 'child_process'; import { readFileSync, existsSync } from 'fs'; import { join, basename, dirname } from 'path'; import { inference } from './Inference'; -import { getIdentity } from '../../../.claude/hooks/lib/identity'; +import { getIdentity } from '../../hooks/lib/identity'; // ============================================================================ // Types diff --git a/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts b/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts index d0501ee87..f3962c564 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts @@ -22,7 +22,7 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; -import { getLearningCategory, isLearningCapture } from "../../../.claude/hooks/lib/learning-utils"; +import { getLearningCategory, isLearningCapture } from "../../hooks/lib/learning-utils"; // ============================================================================ // Configuration diff --git a/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts b/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts index 594031c9f..a236b7f5e 100755 --- a/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TranscriptParser.ts @@ -17,7 +17,7 @@ */ import { readFileSync } from 'fs'; -import { getIdentity } from '../../../.claude/hooks/lib/identity'; +import { getIdentity } from '../../hooks/lib/identity'; const DA_IDENTITY = getIdentity(); diff --git a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts index 609a8a28c..789a6956f 100644 --- a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts +++ b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts @@ -50,7 +50,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, append import { resolve, basename, join, dirname } from "path"; import { spawnSync, spawn } from "child_process"; import { randomUUID } from "crypto"; -import { generateISATemplate } from "../../../.claude/hooks/lib/isa-template"; +import { generateISATemplate } from "../../hooks/lib/isa-template"; // ─── Paths ─────────────────────────────────────────────────────────────────── diff --git a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts index 65a1fd9b6..efeaab048 100755 --- a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts +++ b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts @@ -19,7 +19,7 @@ */ import { spawn, spawnSync } from "bun"; -import { getIdentity, getStartupCatchphrase } from "../../../.claude/hooks/lib/identity"; +import { getIdentity, getStartupCatchphrase } from "../../hooks/lib/identity"; import { existsSync, readFileSync, writeFileSync, readdirSync, symlinkSync, unlinkSync, lstatSync } from "fs"; import { homedir } from "os"; import { join, basename } from "path"; diff --git a/LifeOS/install/hooks/DriftReminder.hook.ts b/LifeOS/install/hooks/DriftReminder.hook.ts index 50c6fe679..96e773d29 100755 --- a/LifeOS/install/hooks/DriftReminder.hook.ts +++ b/LifeOS/install/hooks/DriftReminder.hook.ts @@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { firstBannedHit } from "./lib/banned-vocab"; +import { getLifeosDir } from './lib/paths'; interface HookInput { prompt?: string; @@ -27,7 +28,7 @@ interface DriftState { const STDIN_TIMEOUT_MS = 300; const MIN_TURNS_BETWEEN_FIRES = 5; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME || "", ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const LAST_RESPONSE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "last-response.txt"); const STATE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "drift-reminder.json"); const INITIAL_STATE: DriftState = { diff --git a/LifeOS/install/hooks/LastResponseCache.hook.ts b/LifeOS/install/hooks/LastResponseCache.hook.ts index 61f773cce..e03926af9 100755 --- a/LifeOS/install/hooks/LastResponseCache.hook.ts +++ b/LifeOS/install/hooks/LastResponseCache.hook.ts @@ -14,6 +14,7 @@ import { readHookInput, parseTranscriptFromInput } from './lib/hook-io'; import { writeFileSync } from 'fs'; import { join } from 'path'; +import { getLifeosDir } from './lib/paths'; async function main() { const input = await readHookInput(); @@ -28,7 +29,7 @@ async function main() { if (lastResponse) { try { - const paiDir = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); + const paiDir = getLifeosDir(); const cachePath = join(paiDir, 'MEMORY', 'STATE', 'last-response.txt'); writeFileSync(cachePath, lastResponse.slice(0, 2000), 'utf-8'); } catch (err) { diff --git a/LifeOS/install/hooks/LoadMemory.hook.ts b/LifeOS/install/hooks/LoadMemory.hook.ts index ea2dfd706..50bb56362 100755 --- a/LifeOS/install/hooks/LoadMemory.hook.ts +++ b/LifeOS/install/hooks/LoadMemory.hook.ts @@ -21,9 +21,9 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const PRINCIPAL_MEMORY = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"); const DA_MEMORY = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/DIGITAL_ASSISTANT/DA_MEMORY.md"); diff --git a/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts b/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts index 246aa0ca5..5fbce4b90 100755 --- a/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts +++ b/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts @@ -33,9 +33,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs"; import { resolve as pathResolve, dirname } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const WRITES_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-writes.jsonl"); const CURSOR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-delta-cursor.json"); const HEALTH_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-health.jsonl"); diff --git a/LifeOS/install/hooks/MemoryReviewFire.hook.ts b/LifeOS/install/hooks/MemoryReviewFire.hook.ts index ba5c1b75f..091a9667e 100755 --- a/LifeOS/install/hooks/MemoryReviewFire.hook.ts +++ b/LifeOS/install/hooks/MemoryReviewFire.hook.ts @@ -22,9 +22,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { spawn } from "node:child_process"; import { dirname, resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const STATE_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/review-state.json"); const FIRE_LOG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/reviewer-fires.jsonl"); const REVIEWER_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/TOOLS/MemoryReviewer.ts"); diff --git a/LifeOS/install/hooks/MemoryReviewTrigger.hook.ts b/LifeOS/install/hooks/MemoryReviewTrigger.hook.ts index 508ee6987..1cdc07778 100755 --- a/LifeOS/install/hooks/MemoryReviewTrigger.hook.ts +++ b/LifeOS/install/hooks/MemoryReviewTrigger.hook.ts @@ -38,9 +38,9 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { dirname, resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from './lib/paths'; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const STATE_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/review-state.json"); const CONFIG_PATH = pathResolve(CLAUDE_ROOT, "LIFEOS/USER/CONFIG/memory-review.json"); const EFFORT_ROUTER_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/effort-router.jsonl"); diff --git a/LifeOS/install/hooks/OutputFormatGate.hook.ts b/LifeOS/install/hooks/OutputFormatGate.hook.ts index 605cc1126..6d807c89f 100755 --- a/LifeOS/install/hooks/OutputFormatGate.hook.ts +++ b/LifeOS/install/hooks/OutputFormatGate.hook.ts @@ -29,9 +29,10 @@ import { collectCurrentResponseText } from "../LIFEOS/TOOLS/TranscriptParser"; import { scanAiSpeak } from "./lib/ai-speak-patterns"; import { readFileSync, appendFileSync, mkdirSync } from "fs"; import { dirname, join } from "path"; +import { getLifeosDir } from './lib/paths'; const OBS_PATH = join( - process.env.LIFEOS_DIR || join(process.env.HOME!, ".claude", "LIFEOS"), + getLifeosDir(), "MEMORY", "OBSERVABILITY", "format-gate.jsonl", diff --git a/LifeOS/install/hooks/PromptProcessing.hook.ts b/LifeOS/install/hooks/PromptProcessing.hook.ts index f30a04c0e..770ad538d 100755 --- a/LifeOS/install/hooks/PromptProcessing.hook.ts +++ b/LifeOS/install/hooks/PromptProcessing.hook.ts @@ -39,6 +39,7 @@ import type { AlgorithmTabPhase } from './lib/tab-constants'; import { paiPath } from './lib/paths'; import { updateSessionNameInWorkJson, upsertSession } from './lib/isa-utils'; import { isDesktopChannel, logSkippedVoice, getNotificationChannel } from './lib/notification-channel'; +import { getLifeosDir } from './lib/paths'; // ── Types ── @@ -66,7 +67,7 @@ function appendPromptProcessingTelemetry(entry: Record): void { // ── Constants ── -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = getLifeosDir(); const SESSION_NAMES_PATH = paiPath('MEMORY', 'STATE', 'session-names.json'); const LOCK_PATH = SESSION_NAMES_PATH + '.lock'; const MIN_PROMPT_LENGTH = 3; diff --git a/LifeOS/install/hooks/SatisfactionCapture.hook.ts b/LifeOS/install/hooks/SatisfactionCapture.hook.ts index 9385ec502..ac30ac9fc 100755 --- a/LifeOS/install/hooks/SatisfactionCapture.hook.ts +++ b/LifeOS/install/hooks/SatisfactionCapture.hook.ts @@ -31,6 +31,7 @@ import { getLearningCategory } from './lib/learning-utils'; import { getISOTimestamp, getPSTComponents } from './lib/time'; import { captureFailure } from '../LIFEOS/TOOLS/FailureCapture'; import { addRatingPulse } from './lib/isa-utils'; +import { getLifeosDir } from './lib/paths'; // ── Types ── @@ -63,7 +64,7 @@ interface SentimentResult { // ── Constants ── -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = getLifeosDir(); const SIGNALS_DIR = join(BASE_DIR, 'MEMORY', 'LEARNING', 'SIGNALS'); const RATINGS_FILE = join(SIGNALS_DIR, 'ratings.jsonl'); const LAST_RESPONSE_CACHE = join(BASE_DIR, 'MEMORY', 'STATE', 'last-response.txt'); diff --git a/LifeOS/install/hooks/SessionCleanup.hook.ts b/LifeOS/install/hooks/SessionCleanup.hook.ts index 340e58af2..0f85ffa9c 100755 --- a/LifeOS/install/hooks/SessionCleanup.hook.ts +++ b/LifeOS/install/hooks/SessionCleanup.hook.ts @@ -38,8 +38,9 @@ import { join } from 'path'; import { getISOTimestamp } from './lib/time'; import { setTabState, cleanupKittySession } from './lib/tab-setter'; import { readRegistry, writeRegistry, findArtifactPath, findActiveSessionByUUID } from './lib/isa-utils'; +import { getLifeosDir } from './lib/paths'; -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = getLifeosDir(); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const STATE_DIR = join(MEMORY_DIR, 'STATE'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); diff --git a/LifeOS/install/hooks/SkillSurface.hook.ts b/LifeOS/install/hooks/SkillSurface.hook.ts index f1daa1efb..f3bf104ae 100755 --- a/LifeOS/install/hooks/SkillSurface.hook.ts +++ b/LifeOS/install/hooks/SkillSurface.hook.ts @@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs"; import { basename, dirname, join } from "node:path"; +import { getLifeosDir, getSkillsDir } from './lib/paths'; interface HookInput { prompt?: string; @@ -44,8 +45,8 @@ interface ScoredSkill { const STDIN_TIMEOUT_MS = 300; const MAX_SKILLS_TO_EMIT = 3; const MIN_DISTINCT_TRIGGER_TOKEN_HITS = 2; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME || "", ".claude", "LIFEOS"); -const SKILLS_DIR = join(process.env.HOME || "", ".claude", "skills"); +const LIFEOS_DIR = getLifeosDir(); +const SKILLS_DIR = getSkillsDir(); const CACHE_PATH = join(LIFEOS_DIR, "MEMORY", "STATE", "skill-index.json"); async function readStdinWithTimeout(timeoutMs: number = STDIN_TIMEOUT_MS): Promise { diff --git a/LifeOS/install/hooks/WorkCompletionLearning.hook.ts b/LifeOS/install/hooks/WorkCompletionLearning.hook.ts index 376e15ecd..7a06d3263 100755 --- a/LifeOS/install/hooks/WorkCompletionLearning.hook.ts +++ b/LifeOS/install/hooks/WorkCompletionLearning.hook.ts @@ -54,8 +54,9 @@ import { join } from 'path'; import { getISOTimestamp, getPSTDate } from './lib/time'; import { getLearningCategory } from './lib/learning-utils'; import { findArtifactPath, findActiveSessionByUUID } from './lib/isa-utils'; +import { getLifeosDir } from './lib/paths'; -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude', 'LIFEOS'); +const BASE_DIR = getLifeosDir(); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); const LEARNING_DIR = join(MEMORY_DIR, 'LEARNING'); diff --git a/LifeOS/install/hooks/WritingGate.hook.ts b/LifeOS/install/hooks/WritingGate.hook.ts index a8710b37d..d8884dc4b 100755 --- a/LifeOS/install/hooks/WritingGate.hook.ts +++ b/LifeOS/install/hooks/WritingGate.hook.ts @@ -29,8 +29,9 @@ import { readHookInput, parseTranscriptFromInput } from "./lib/hook-io"; import { appendFileSync, mkdirSync, existsSync, readFileSync } from "fs"; import { createHash } from "crypto"; import { dirname, join } from "path"; +import { getLifeosDir } from './lib/paths'; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, ".claude", "LIFEOS"); +const LIFEOS_DIR = getLifeosDir(); const OBS_PATH = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "writing-gate.jsonl"); const RUNS_PATH = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "pangram-runs.jsonl"); const RUN_WINDOW_MS = 30 * 60 * 1000; // a run counts as "this turn" within 30 min diff --git a/LifeOS/install/hooks/handlers/UpdateCounts.ts b/LifeOS/install/hooks/handlers/UpdateCounts.ts index 46f678872..3334e9bb4 100755 --- a/LifeOS/install/hooks/handlers/UpdateCounts.ts +++ b/LifeOS/install/hooks/handlers/UpdateCounts.ts @@ -22,6 +22,7 @@ import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { execSync } from 'child_process'; import { getLifeosDir } from '../lib/paths'; +import { getClaudeDir } from '../lib/paths'; /** * Refresh usage cache from Anthropic OAuth API. @@ -39,7 +40,7 @@ async function refreshUsageCache(paiDir: string): Promise { { encoding: 'utf-8', timeout: 3000 } ).trim(); } else { - const credPath = join(process.env.HOME || '', '.claude', '.credentials.json'); + const credPath = join(getClaudeDir(), '.credentials.json'); credJson = readFileSync(credPath, 'utf-8').trim(); } diff --git a/LifeOS/install/hooks/lib/safety-classifier.ts b/LifeOS/install/hooks/lib/safety-classifier.ts index 9b7cbda1e..86fed1f0c 100755 --- a/LifeOS/install/hooks/lib/safety-classifier.ts +++ b/LifeOS/install/hooks/lib/safety-classifier.ts @@ -1,5 +1,6 @@ import { homedir } from "node:os"; import { resolve } from "node:path"; +import { getClaudeDir } from "./paths"; export type Decision = "allow" | "neutral"; @@ -31,7 +32,7 @@ export interface ToolCall { const HOME = homedir(); export const TRUSTED_PREFIXES: readonly string[] = [ - resolve(HOME, ".claude"), + getClaudeDir(), resolve(HOME, "Projects"), resolve(HOME, "LocalProjects"), resolve(HOME, "Downloads"), From 1c6a24279f3b346691ebebbe2fb5b74d0978e81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:33:56 +0100 Subject: [PATCH 4/9] fix: resolve launcher (lifeos.ts) config paths through Paths.ts The launcher hardcoded CLAUDE_DIR/BANNER_SCRIPT at ~/.claude, so on a non-default CLAUDE_CONFIG_DIR profile it would cd into and read MCP state from the wrong profile even though the spawned claude process inherits the right env. WALLPAPER_DIR stays home-relative by design. --- LifeOS/install/LIFEOS/TOOLS/lifeos.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts index efeaab048..0b30fd0bd 100755 --- a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts +++ b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts @@ -23,15 +23,16 @@ import { getIdentity, getStartupCatchphrase } from "../../hooks/lib/identity"; import { existsSync, readFileSync, writeFileSync, readdirSync, symlinkSync, unlinkSync, lstatSync } from "fs"; import { homedir } from "os"; import { join, basename } from "path"; +import { getClaudeDir, getLifeosDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ -const CLAUDE_DIR = join(homedir(), ".claude"); +const CLAUDE_DIR = getClaudeDir(); const MCP_DIR = join(CLAUDE_DIR, "MCPs"); const ACTIVE_MCP = join(CLAUDE_DIR, ".mcp.json"); -const BANNER_SCRIPT = join(homedir(), ".claude", "LIFEOS", "TOOLS", "Banner.ts"); +const BANNER_SCRIPT = join(getLifeosDir(), "TOOLS", "Banner.ts"); const VOICE_SERVER = "http://localhost:31337/notify/personality"; const WALLPAPER_DIR = join(homedir(), "Projects", "Wallpaper"); // Note: RAW archiving removed - Claude Code handles its own cleanup (30-day retention in projects/) From ee51ccafd7ea7e630b7cd5808951530c8416b118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Mon, 6 Jul 2026 01:32:43 +0100 Subject: [PATCH 5/9] fix: user-facing command hints show the active profile's paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The always-on hooks' guidance messages (memory-health 'Run: bun ~/.claude/...', writing-gate PangramScore instructions, Art workflow listing, system-file-guard import suggestion, work-repo setup hint) hardcoded ~/.claude, so on a non-default CLAUDE_CONFIG_DIR profile they told the user to run commands against the wrong profile — commands that either fail or, worse, act on another install. Adds displayPath() to hooks/lib/paths.ts (abbreviates the home prefix to ~ for display) and builds those messages from the already-resolved path constants. Default-root installs render byte-identical messages. --- LifeOS/install/hooks/ArtWorkflowGuard.hook.ts | 6 +++--- LifeOS/install/hooks/MemoryDeltaSurface.hook.ts | 4 ++-- LifeOS/install/hooks/MemoryHealthGate.hook.ts | 6 +++--- LifeOS/install/hooks/SystemFileGuard.hook.ts | 4 ++-- LifeOS/install/hooks/WritingGate.hook.ts | 4 ++-- LifeOS/install/hooks/lib/paths.ts | 10 ++++++++++ LifeOS/install/hooks/lib/work-config.ts | 4 ++-- 7 files changed, 24 insertions(+), 14 deletions(-) diff --git a/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts b/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts index f906335d3..bb0bd902b 100755 --- a/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts +++ b/LifeOS/install/hooks/ArtWorkflowGuard.hook.ts @@ -16,7 +16,7 @@ */ import { readFileSync, readdirSync } from "node:fs"; -import { getSkillsDir } from "./lib/paths"; +import { displayPath, getSkillsDir } from "./lib/paths"; interface HookInput { session_id?: string; @@ -83,7 +83,7 @@ function main(): never { " ArtWorkflowGuard — Generate.ts call BLOCKED before execution.", "═══════════════════════════════════════════════════════════════════════════", "", - " This Bash command invokes ~/.claude/skills/Art/Tools/Generate.ts but", + ` This Bash command invokes ${displayPath(getSkillsDir())}/Art/Tools/Generate.ts but`, " does not name a workflow. The Art skill requires every image generation", " to run through a named workflow. Freeform prompts are documented to fail.", "", @@ -93,7 +93,7 @@ function main(): never { "", " Available workflows (each is a file under skills/Art/Workflows/):", ...workflows.map( - (w) => ` --workflow=${w.padEnd(28)} → ~/.claude/skills/Art/Workflows/${w}.md` + (w) => ` --workflow=${w.padEnd(28)} → ${displayPath(getSkillsDir())}/Art/Workflows/${w}.md` ), "", " Recommended next step: Read the workflow file matching your task,", diff --git a/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts b/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts index 5fbce4b90..8461078db 100755 --- a/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts +++ b/LifeOS/install/hooks/MemoryDeltaSurface.hook.ts @@ -33,7 +33,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs"; import { resolve as pathResolve, dirname } from "node:path"; -import { getClaudeDir } from './lib/paths'; +import { displayPath, getClaudeDir } from './lib/paths'; const CLAUDE_ROOT = getClaudeDir(); const WRITES_LOG = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-writes.jsonl"); @@ -161,7 +161,7 @@ function criticalHealthLine(): string | null { .filter((f: any) => f.severity === "critical") .map((f: any) => f.message) .slice(0, 3); - return `🩺 MEMORY HEALTH: CRITICAL — ${blockers.join(" · ") || `${last.counts?.critical ?? "?"} blocker(s)`}. Fix: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`; + return `🩺 MEMORY HEALTH: CRITICAL — ${blockers.join(" · ") || `${last.counts?.critical ?? "?"} blocker(s)`}. Fix: bun ${displayPath(pathResolve(CLAUDE_ROOT, "LIFEOS/TOOLS/MemoryHealthCheck.ts"))}`; } catch { return null; } diff --git a/LifeOS/install/hooks/MemoryHealthGate.hook.ts b/LifeOS/install/hooks/MemoryHealthGate.hook.ts index e14db29bc..247694968 100755 --- a/LifeOS/install/hooks/MemoryHealthGate.hook.ts +++ b/LifeOS/install/hooks/MemoryHealthGate.hook.ts @@ -14,7 +14,7 @@ */ import { execFileSync } from "node:child_process"; -import { paiPath } from "./lib/paths"; +import { displayPath, paiPath } from "./lib/paths"; const CHECK = paiPath("TOOLS/MemoryHealthCheck.ts"); @@ -26,7 +26,7 @@ try { }); const report = JSON.parse(out); if (report.overall === "critical") { - console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`); + console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ${displayPath(CHECK)}`); } else if (report.overall === "warn") { console.error(`⚠️ Memory health: WARN — ${report.counts.warn} finding(s).`); } @@ -39,7 +39,7 @@ try { if (stdout) { const report = JSON.parse(stdout); if (report.overall === "critical") { - console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ~/.claude/LIFEOS/TOOLS/MemoryHealthCheck.ts`); + console.error(`🚨 Memory health: CRITICAL — ${report.counts.critical} blocker(s). Run: bun ${displayPath(CHECK)}`); } else if (report.overall === "warn") { console.error(`⚠️ Memory health: WARN — ${report.counts.warn} finding(s).`); } diff --git a/LifeOS/install/hooks/SystemFileGuard.hook.ts b/LifeOS/install/hooks/SystemFileGuard.hook.ts index 9f4815b9d..e1e796f03 100755 --- a/LifeOS/install/hooks/SystemFileGuard.hook.ts +++ b/LifeOS/install/hooks/SystemFileGuard.hook.ts @@ -22,7 +22,7 @@ import { readFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { evaluateWrite, extractNewContent } from "./lib/system-file-guard-core"; -import { paiPath } from "./lib/paths"; +import { displayPath, paiPath } from "./lib/paths"; const LOG_PATH = paiPath("MEMORY/OBSERVABILITY/system-file-guard.jsonl"); @@ -72,7 +72,7 @@ function denyMessage(relPath: string, pattern: string, match: string): string { " patterns. Move the user-specific content to a USER-zone location", " or read it through the LifeosConfig interface:", "", - " import { loadLifeosConfig, paiUserDir } from '~/.claude/LIFEOS/TOOLS/LifeosConfig';", + ` import { loadLifeosConfig, paiUserDir } from '${displayPath(paiPath("TOOLS/LifeosConfig"))}';`, "", " Canonical USER zones (any of these can hold the content):", " LIFEOS/USER/PRINCIPAL/ principal identity files", diff --git a/LifeOS/install/hooks/WritingGate.hook.ts b/LifeOS/install/hooks/WritingGate.hook.ts index d8884dc4b..c0fb7a3c9 100755 --- a/LifeOS/install/hooks/WritingGate.hook.ts +++ b/LifeOS/install/hooks/WritingGate.hook.ts @@ -29,7 +29,7 @@ import { readHookInput, parseTranscriptFromInput } from "./lib/hook-io"; import { appendFileSync, mkdirSync, existsSync, readFileSync } from "fs"; import { createHash } from "crypto"; import { dirname, join } from "path"; -import { getLifeosDir } from './lib/paths'; +import { displayPath, getLifeosDir } from './lib/paths'; const LIFEOS_DIR = getLifeosDir(); const OBS_PATH = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "writing-gate.jsonl"); @@ -130,7 +130,7 @@ const BLOCK_REASON = "through the _WRITING skill AND the detector before it is shown (OPERATIONAL_RULES.md § Authored content). " + "The gate checks for a real PangramScore.ts run on the draft — a typed token does NOT satisfy it. Before " + "stopping: (1) run Skill(\"_WRITING\") DETECT mode on the draft and fix every P0/P1 in the right voice; " + - "(2) run `bun ~/.claude/LIFEOS/TOOLS/PangramScore.ts --file ` on the ACTUAL draft text so the run is " + + "(2) run `bun " + displayPath(join(getLifeosDir(), "TOOLS/PangramScore.ts")) + " --file ` on the ACTUAL draft text so the run is " + "logged; (3) cite the detect result + the reported AI% in a `✍️ WRITING-AUDIT:` line. Pangram saturates on " + "model prose, so the number is REPORTED, not a pass/fail bar — the requirement is that the audit RAN."; diff --git a/LifeOS/install/hooks/lib/paths.ts b/LifeOS/install/hooks/lib/paths.ts index b9174d7e3..69d5bc65f 100755 --- a/LifeOS/install/hooks/lib/paths.ts +++ b/LifeOS/install/hooks/lib/paths.ts @@ -25,6 +25,16 @@ export function expandPath(path: string): string { .replace(/^~(?=\/|$)/, home); } +/** + * Abbreviate the home prefix to `~` for user-facing display, so messages show + * the path that actually resolves on this install (e.g. `~/.claude-gc/...` on + * a CLAUDE_CONFIG_DIR profile) instead of a hardcoded `~/.claude/...`. + */ +export function displayPath(path: string): string { + const home = homedir(); + return path.startsWith(home + '/') ? '~' + path.slice(home.length) : path; +} + /** * Get the LifeOS data directory (expanded). * diff --git a/LifeOS/install/hooks/lib/work-config.ts b/LifeOS/install/hooks/lib/work-config.ts index bba597215..0c1b17501 100755 --- a/LifeOS/install/hooks/lib/work-config.ts +++ b/LifeOS/install/hooks/lib/work-config.ts @@ -26,7 +26,7 @@ */ import { existsSync, readFileSync, writeFileSync, chmodSync } from "fs"; import { join } from "path"; -import { getLifeosDir } from "./paths"; +import { displayPath, getClaudeDir, getLifeosDir } from "./paths"; declare const Bun: { spawnSync: (cmd: string[], opts?: any) => any }; @@ -104,7 +104,7 @@ export function loadWorkConfig(): WorkConfig { if (!existsSync(REPO_JSON_PATH)) { return disabled( "missing", - "USER/WORK/work_repo.json missing — run `bun ~/.claude/skills/_ULWORK/Tools/SetWorkRepo.ts `", + `USER/WORK/work_repo.json missing — run \`bun ${displayPath(join(getClaudeDir(), "skills/_ULWORK/Tools/SetWorkRepo.ts"))} \``, ); } From 094e223a9f9bad4f4f6d59bd9c5c3bf109ba654d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Mon, 6 Jul 2026 01:36:35 +0100 Subject: [PATCH 6/9] docs: clarify displayPath() is cosmetic, not part of path resolution The message-correctness fix is building hints from resolved path constants; displayPath() only abbreviates the home prefix so default installs render byte-identical strings. --- LifeOS/install/hooks/lib/paths.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/LifeOS/install/hooks/lib/paths.ts b/LifeOS/install/hooks/lib/paths.ts index 69d5bc65f..f1f64e3fe 100755 --- a/LifeOS/install/hooks/lib/paths.ts +++ b/LifeOS/install/hooks/lib/paths.ts @@ -26,9 +26,11 @@ export function expandPath(path: string): string { } /** - * Abbreviate the home prefix to `~` for user-facing display, so messages show - * the path that actually resolves on this install (e.g. `~/.claude-gc/...` on - * a CLAUDE_CONFIG_DIR profile) instead of a hardcoded `~/.claude/...`. + * Abbreviate the home prefix to `~` in a path shown to the user. Purely + * cosmetic — the absolute path would work as-is. Exists so that messages + * built from resolved path constants render byte-identical to the previous + * hardcoded `~/.claude/...` strings on a default-root install, and follow + * the usual home-relative display convention everywhere else. */ export function displayPath(path: string): string { const home = homedir(); From 56b4eed7a6e4703003b3a87f80a1a0caaf398c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Mon, 6 Jul 2026 02:02:47 +0100 Subject: [PATCH 7/9] fix: sweep remaining ~/.claude hardcodes across LIFEOS/TOOLS, PULSE, and skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the CLAUDE_CONFIG_DIR fix: extends the same treatment from the always-on hooks surface to the on-demand tools — ~150 files across LIFEOS/TOOLS, LIFEOS/PULSE, and skills/*/Tools. - Path resolution: every inline '~/.claude' fallback (join/resolve with homedir()/HOME, 'process.env.LIFEOS_DIR ||' patterns, template strings) now routes through LIFEOS/TOOLS/Paths.ts. - Claude Code projects-dir encoding (ActivityParser, KnowledgeHarvester) derives the munged '-Users-x--claude' segment from the active config dir instead of hardcoding the default profile's encoding. - User-facing hints (usage blocks, .env key instructions, audit output) render the active profile's paths via displayPath(). - Shell scripts use ${CLAUDE_CONFIG_DIR:-$HOME/.claude}. Left as-is, deliberately: doc-notation parsers and scanner regexes that match the canonical '~/.claude' spelling inside documentation text (DocCheck, ReferenceCheck, ArchitectureSummaryGenerator, the Daemon SecurityFilter redaction pattern), the remote Arbol layout constant, and macOS-only Interceptor shell help text. Verified: bun build clean on all 299 swept files (75 pre-existing failures from missing external deps are unchanged); zero functional '.claude' literals remain outside the justified set. --- .../install/LIFEOS/PULSE/MenuBar/install.sh | 2 +- .../PULSE/Observability/observability.ts | 9 ++++---- .../PULSE/Performance/cost-aggregator.ts | 5 +++-- .../LIFEOS/PULSE/Performance/module.ts | 5 +++-- .../LIFEOS/PULSE/Tools/ReleaseAudit.ts | 3 ++- .../LIFEOS/PULSE/Tools/SnapshotFromFixture.ts | 6 ++--- .../LIFEOS/PULSE/Tools/SnapshotPages.ts | 4 ++-- .../install/LIFEOS/PULSE/VoiceServer/voice.ts | 7 +++--- .../LIFEOS/PULSE/adapters/AdapterRunner.ts | 4 ++-- .../LIFEOS/PULSE/checks/airgradient-poll.ts | 5 +++-- .../install/LIFEOS/PULSE/checks/calendar.ts | 3 ++- .../LIFEOS/PULSE/checks/github-work.ts | 3 ++- LifeOS/install/LIFEOS/PULSE/checks/github.ts | 5 +++-- .../LIFEOS/PULSE/checks/life-morning-brief.ts | 3 ++- .../PULSE/checks/notification-governor.ts | 4 ++-- .../PULSE/checks/poller-meta-monitor.ts | 4 ++-- .../install/LIFEOS/PULSE/edit/edit-handler.ts | 8 +++---- LifeOS/install/LIFEOS/PULSE/lib.ts | 7 +++--- LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts | 4 ++-- .../LIFEOS/PULSE/lib/provenance-watcher.ts | 4 ++-- .../LIFEOS/PULSE/lib/telegram-proposals.ts | 5 +++-- LifeOS/install/LIFEOS/PULSE/manage-deriver.sh | 6 ++--- LifeOS/install/LIFEOS/PULSE/manage.sh | 2 +- LifeOS/install/LIFEOS/PULSE/modules/hooks.ts | 3 ++- .../LIFEOS/PULSE/modules/hypotheses.ts | 4 ++-- .../install/LIFEOS/PULSE/modules/imessage.ts | 7 +++--- .../PULSE/modules/local-intelligence.ts | 7 +++--- LifeOS/install/LIFEOS/PULSE/modules/memory.ts | 4 ++-- LifeOS/install/LIFEOS/PULSE/modules/syslog.ts | 5 ++--- .../LIFEOS/PULSE/modules/tab-freshness.ts | 11 +++++----- .../install/LIFEOS/PULSE/modules/telegram.ts | 13 ++++++----- .../LIFEOS/PULSE/modules/user-index.ts | 3 ++- LifeOS/install/LIFEOS/PULSE/modules/wiki.ts | 9 ++++---- LifeOS/install/LIFEOS/PULSE/modules/work.ts | 10 ++++----- LifeOS/install/LIFEOS/PULSE/pulse-old.ts | 5 +++-- LifeOS/install/LIFEOS/PULSE/pulse-unified.ts | 5 +++-- LifeOS/install/LIFEOS/PULSE/pulse.ts | 5 +++-- LifeOS/install/LIFEOS/PULSE/run-job.ts | 5 +++-- LifeOS/install/LIFEOS/PULSE/setup.ts | 7 +++--- LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts | 21 +++++++++++------- LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/AlgoPhase.ts | 9 ++++---- .../LIFEOS/TOOLS/AlgorithmPhaseReport.ts | 4 ++-- .../TOOLS/ApproveCurrentStateEntries.ts | 4 ++-- .../LIFEOS/TOOLS/ArchDecisionHarvest.ts | 4 ++-- .../TOOLS/ArchitectureSummaryGenerator.ts | 11 +++++----- LifeOS/install/LIFEOS/TOOLS/Banner.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts | 9 ++++---- LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts | 6 ++--- LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts | 11 +++++----- LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts | 4 ++-- .../install/LIFEOS/TOOLS/CommitmentSweep.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts | 7 +++--- LifeOS/install/LIFEOS/TOOLS/CostTracker.ts | 13 ++++++----- .../install/LIFEOS/TOOLS/CrossVendorAudit.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/DASchedule.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/DocCheck.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts | 3 ++- .../install/LIFEOS/TOOLS/FeatureRegistry.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/GetCounts.ts | 6 ++--- LifeOS/install/LIFEOS/TOOLS/Grok.ts | 7 +++--- .../install/LIFEOS/TOOLS/HarvestExecutor.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/HealthSync.ts | 7 +++--- LifeOS/install/LIFEOS/TOOLS/Inference.ts | 5 +++-- .../LIFEOS/TOOLS/InstallBlogDiscovery.ts | 3 ++- .../LIFEOS/TOOLS/InstallBookmarkSweep.ts | 3 ++- .../LIFEOS/TOOLS/InstallCodexUpdate.ts | 3 ++- .../LIFEOS/TOOLS/InstallCommitmentSweep.ts | 5 +++-- .../LIFEOS/TOOLS/InstallDerivedSync.ts | 5 +++-- .../install/LIFEOS/TOOLS/InstallHealthSync.ts | 3 ++- .../install/LIFEOS/TOOLS/InstallWorkSweep.ts | 3 ++- .../LIFEOS/TOOLS/IntegrityMaintenance.ts | 7 +++--- .../LIFEOS/TOOLS/InterviewIdealState.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts | 6 ++--- LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts | 4 ++-- .../LIFEOS/TOOLS/KnowledgeHarvester.ts | 7 +++--- .../LIFEOS/TOOLS/LearningPatternSynthesis.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts | 5 ++--- .../install/LIFEOS/TOOLS/LoadSkillConfig.ts | 7 +++--- LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts | 4 ++-- .../install/LIFEOS/TOOLS/MemoryRetriever.ts | 9 ++++---- LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts | 5 +++-- LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts | 6 ++--- .../LIFEOS/TOOLS/MigrateContextFreshness.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts | 6 ++--- .../LIFEOS/TOOLS/MigrateTelosFreshness.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/MutationTier.ts | 6 ++--- LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/OpenRouter.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/PangramScore.ts | 7 +++--- LifeOS/install/LIFEOS/TOOLS/Paths.ts | 12 ++++++++++ LifeOS/install/LIFEOS/TOOLS/PiSync.sh | 2 +- .../LIFEOS/TOOLS/ProposeCurrentStateEntry.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/Recommend.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts | 3 ++- .../install/LIFEOS/TOOLS/SessionHarvester.ts | 3 ++- .../install/LIFEOS/TOOLS/SessionProgress.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/SessionRename.ts | 4 ++-- .../LIFEOS/TOOLS/SyncIdentityToSettings.ts | 5 ++--- LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts | 4 ++-- .../LIFEOS/TOOLS/TokenXray/TokenXray.ts | 12 +++++----- .../install/LIFEOS/TOOLS/UpdateLifeosState.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts | 3 ++- .../TOOLS/WisdomCrossFrameSynthesizer.ts | 3 ++- .../LIFEOS/TOOLS/WisdomDomainClassifier.ts | 3 ++- .../LIFEOS/TOOLS/WisdomFrameUpdater.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts | 7 +++--- LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts | 3 ++- LifeOS/install/LIFEOS/TOOLS/algorithm.ts | 5 +++-- .../LIFEOS/TOOLS/healthsync/eightsleep.ts | 3 ++- .../LIFEOS/TOOLS/healthsync/function.ts | 3 ++- .../install/LIFEOS/TOOLS/healthsync/oura.ts | 3 ++- .../install/LIFEOS/TOOLS/healthsync/store.ts | 10 ++++----- LifeOS/install/LIFEOS/TOOLS/lifeos.ts | 4 ++-- LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts | 14 ++++++------ .../skills/Agents/Tools/ComposeAgent.ts | 22 +++++++++---------- .../skills/Agents/Tools/LoadAgentContext.ts | 4 ++-- .../skills/Apify/examples/comparison-test.ts | 3 ++- .../skills/Art/Tools/ComposeThumbnail.ts | 5 +++-- LifeOS/install/skills/Art/Tools/Generate.ts | 7 +++--- .../Art/Tools/GenerateMidjourneyImage.ts | 3 ++- .../skills/Art/Tools/GeneratePrompt.ts | 5 +++-- .../skills/Art/Tools/PickExpression.ts | 4 ++-- .../install/skills/Art/Tools/ThumbnailText.ts | 3 ++- .../skills/AudioEditor/Tools/Polish.ts | 6 ++--- .../ContextSearch/Tools/ContextSearch.ts | 5 +++-- .../skills/Daemon/Tools/DaemonAggregator.ts | 6 ++--- .../skills/Evals/Tools/AlgorithmBridge.ts | 3 ++- .../skills/Interceptor/Tools/Capture.sh | 2 +- .../Interceptor/Tools/LaunchTestProfile.sh | 2 +- .../Interceptor/Tools/PreflightIsolation.sh | 2 +- .../LocalIntelligence/Tools/Hometown.ts | 5 ++--- .../skills/LocalIntelligence/Tools/Refresh.ts | 3 ++- .../install/skills/Telos/Tools/UpdateTelos.ts | 3 ++- .../install/skills/Upgrade/Tools/Anthropic.ts | 7 +++--- .../Webdesign/Tools/DriveClaudeDesign.ts | 3 ++- .../skills/Webdesign/Tools/VerifyDesign.ts | 3 ++- 152 files changed, 436 insertions(+), 343 deletions(-) diff --git a/LifeOS/install/LIFEOS/PULSE/MenuBar/install.sh b/LifeOS/install/LIFEOS/PULSE/MenuBar/install.sh index 84bd64524..de9ba2c4e 100755 --- a/LifeOS/install/LIFEOS/PULSE/MenuBar/install.sh +++ b/LifeOS/install/LIFEOS/PULSE/MenuBar/install.sh @@ -68,7 +68,7 @@ sed "s|__HOME__|$HOME_DIR|g" "$PLIST_SRC" > "$PLIST_DST" echo " Installed $PLIST_DST" # Ensure logs directory exists -mkdir -p "$HOME_DIR/.claude/LIFEOS/PULSE/logs" +mkdir -p "${CLAUDE_CONFIG_DIR:-$HOME_DIR/.claude}/LIFEOS/PULSE/logs" launchctl load "$PLIST_DST" echo " Loaded $PLIST_LABEL" diff --git a/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts b/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts index e5ea2fee4..7b3c1c77d 100644 --- a/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts +++ b/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts @@ -26,6 +26,7 @@ import { join, extname } from "path" import { readFileSync, readdirSync, existsSync, realpathSync, statSync, watch, type FSWatcher } from "fs" import YAML from "yaml" import { effortToCanonicalTierName } from "../../../hooks/lib/effort" +import { getClaudeDir } from "../../TOOLS/Paths"; // Growth is an OPTIONAL USER customization (USER/CUSTOMIZATIONS/TOOLS/Growth.ts): present on the // principal's machine, absent on fresh installs (private code, not shipped). It is loaded via a // guarded dynamic import in handleLifeGrowth so the Pulse server boots without it. A static import @@ -55,7 +56,7 @@ export interface ObservabilityConfig { // ── Path Construction ── const HOME = process.env.HOME ?? "" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") const MEMORY_DIR = join(LIFEOS_DIR, "MEMORY") const WORK_JSON_PATH = join(MEMORY_DIR, "STATE", "work.json") @@ -64,7 +65,7 @@ const SUBAGENT_EVENTS_PATH = join(MEMORY_DIR, "OBSERVABILITY", "subagent-events. const VOICE_EVENTS_PATH = join(MEMORY_DIR, "VOICE", "voice-events.jsonl") const TOOL_FAILURES_PATH = join(MEMORY_DIR, "OBSERVABILITY", "tool-failures.jsonl") const TOOL_ACTIVITY_PATH = join(MEMORY_DIR, "OBSERVABILITY", "tool-activity.jsonl") -const SETTINGS_PATH = join(HOME, ".claude", "settings.json") +const SETTINGS_PATH = join(getClaudeDir(), "settings.json") const LADDER_DIR = join(HOME, "Projects", "Ladder") const DEFAULT_DASHBOARD_DIR = join(LIFEOS_DIR, "PULSE", "Observability", "out") @@ -142,7 +143,7 @@ function getDashboardDir(): string { const dir = config.dashboard_dir ?? DEFAULT_DASHBOARD_DIR // Resolve relative paths against Pulse directory if (!dir.startsWith("/")) { - return join(HOME, ".claude", "LIFEOS", "PULSE", dir) + return join(getClaudeDir(), "LIFEOS", "PULSE", dir) } return dir } @@ -1626,7 +1627,7 @@ function readDirMdFiles(dir: string): { name: string, content: string, sections: function handleUserIndexApi(filter: string | null): Response { try { - const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME || "", ".claude", "LIFEOS") + const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS") const indexPath = join(LIFEOS_DIR, "PULSE", "state", "user-index.json") const raw = Bun.file(indexPath) if (!raw.size) { diff --git a/LifeOS/install/LIFEOS/PULSE/Performance/cost-aggregator.ts b/LifeOS/install/LIFEOS/PULSE/Performance/cost-aggregator.ts index 2eaeb97a7..b7920057c 100755 --- a/LifeOS/install/LIFEOS/PULSE/Performance/cost-aggregator.ts +++ b/LifeOS/install/LIFEOS/PULSE/Performance/cost-aggregator.ts @@ -12,10 +12,11 @@ import { join, basename, dirname } from "path" import { existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, statSync, mkdirSync } from "fs" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") -const PROJECTS_DIR = join(HOME, ".claude", "projects") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") +const PROJECTS_DIR = join(getClaudeDir(), "projects") const OUTPUT_FILE = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "session-costs.jsonl") const STATE_FILE = join(LIFEOS_DIR, "PULSE", "Performance", "aggregator-state.json") diff --git a/LifeOS/install/LIFEOS/PULSE/Performance/module.ts b/LifeOS/install/LIFEOS/PULSE/Performance/module.ts index aa44617f3..ba40a9988 100644 --- a/LifeOS/install/LIFEOS/PULSE/Performance/module.ts +++ b/LifeOS/install/LIFEOS/PULSE/Performance/module.ts @@ -12,9 +12,10 @@ import { join } from "path" import { existsSync, readFileSync } from "fs" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") const MEMORY_DIR = join(LIFEOS_DIR, "MEMORY") const SESSION_COSTS_PATH = join(MEMORY_DIR, "OBSERVABILITY", "session-costs.jsonl") const TOOL_FAILURES_PATH = join(MEMORY_DIR, "OBSERVABILITY", "tool-failures.jsonl") @@ -278,7 +279,7 @@ async function handleAnthropicCostApi(): Promise { const { readFileSync, existsSync } = await import("fs") const { join } = await import("path") const home = process.env.HOME ?? "" - const obsDir = join(home, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY") + const obsDir = join(getClaudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY") const ledgerPath = join(obsDir, "anthropic-cost.jsonl") const sitesPath = join(obsDir, "anthropic-call-sites.json") diff --git a/LifeOS/install/LIFEOS/PULSE/Tools/ReleaseAudit.ts b/LifeOS/install/LIFEOS/PULSE/Tools/ReleaseAudit.ts index 1bbae6250..657ae4350 100644 --- a/LifeOS/install/LIFEOS/PULSE/Tools/ReleaseAudit.ts +++ b/LifeOS/install/LIFEOS/PULSE/Tools/ReleaseAudit.ts @@ -3,6 +3,7 @@ import { readdirSync, statSync, readFileSync, existsSync } from "node:fs"; import { resolve, join, relative } from "node:path"; import { homedir } from "node:os"; import { parseFrontmatter } from "../lib/frontmatter"; +import { getLifeosDir } from "../../TOOLS/Paths"; const args = process.argv.slice(2); const stagingArg = args.find((a) => !a.startsWith("--")); @@ -34,7 +35,7 @@ function loadProhibitedStrings(): string[] { const candidates = [ process.env.LIFEOS_RELEASE_AUDIT_STRINGS, join(homedir(), ".config/LIFEOS/USER/CONFIG/release-audit-strings.json"), - join(homedir(), ".claude/LIFEOS/USER/CONFIG/release-audit-strings.json"), + join(getLifeosDir(), "USER/CONFIG/release-audit-strings.json"), ].filter(Boolean) as string[]; for (const p of candidates) { try { diff --git a/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotFromFixture.ts b/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotFromFixture.ts index fdfe38189..e8a7947c7 100755 --- a/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotFromFixture.ts +++ b/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotFromFixture.ts @@ -4,10 +4,10 @@ import { resolve, join, basename } from "node:path"; import { renderShell, renderPage } from "../ui/render"; import type { PageData } from "../Schema/PulseSchema"; import { PageDataSchema } from "../Schema/PulseSchema"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME!; -const FIX_DIR = resolve(HOME, ".claude", "LIFEOS", "PULSE", "Schema", "Fixtures"); -const OUT_DIR = resolve(HOME, ".claude", "LIFEOS", "PULSE", "Schema", "Snapshots"); +const FIX_DIR = resolve(getClaudeDir(), "LIFEOS", "PULSE", "Schema", "Fixtures"); +const OUT_DIR = resolve(getClaudeDir(), "LIFEOS", "PULSE", "Schema", "Snapshots"); mkdirSync(OUT_DIR, { recursive: true }); const fixtures = readdirSync(FIX_DIR).filter((f) => f.endsWith(".json") && !f.startsWith("invalid")); diff --git a/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotPages.ts b/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotPages.ts index d2b20f7eb..1b2466ce4 100755 --- a/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotPages.ts +++ b/LifeOS/install/LIFEOS/PULSE/Tools/SnapshotPages.ts @@ -5,9 +5,9 @@ import { readFileSync, existsSync } from "node:fs"; import { renderShell, renderPage, renderEmpty } from "../ui/render"; import { readIndex, readPage } from "../lib/data-plane"; import { loadAllManifests } from "../lib/manifest-loader"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME!; -const SNAP_DIR = resolve(HOME, ".claude", "LIFEOS", "PULSE", "Schema", "Snapshots"); +const SNAP_DIR = resolve(getClaudeDir(), "LIFEOS", "PULSE", "Schema", "Snapshots"); mkdirSync(SNAP_DIR, { recursive: true }); const idx = readIndex(); diff --git a/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts b/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts index d72402090..cd3f13ed4 100644 --- a/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts +++ b/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts @@ -18,6 +18,7 @@ import { join } from "path" import { existsSync, readFileSync } from "fs" import { log } from "../lib" import { disambiguateHomographs } from "../lib/homographs" +import { getClaudeDir } from "../../TOOLS/Paths"; // ── Public Config Interface ── @@ -163,7 +164,7 @@ function escapeRegex(str: string): string { } function loadPronunciations(customPath?: string): void { - const paiDir = join(process.env.HOME ?? "~", ".claude", "LIFEOS") + const paiDir = join(getClaudeDir(), "LIFEOS") const userPronPath = customPath ?? join(paiDir, "USER", "PRINCIPAL", "PRONUNCIATIONS.json") try { @@ -196,7 +197,7 @@ function applyPronunciations(text: string): string { // ── Voice Config from settings.json ── function loadVoiceConfigFromSettings(): LoadedVoiceConfig { - const settingsPath = join(process.env.HOME ?? "~", ".claude", "settings.json") + const settingsPath = join(getClaudeDir(), "settings.json") try { if (!existsSync(settingsPath)) { @@ -655,7 +656,7 @@ export async function handleVoiceRequest(req: Request): Promise // /notify/personality honest with whatever the user last selected. let voiceId: string | null = null try { - const settingsFile = join(process.env.HOME ?? "~", ".claude", "settings.json") + const settingsFile = join(getClaudeDir(), "settings.json") const settings = JSON.parse(readFileSync(settingsFile, "utf-8")) const main = settings?.daidentity?.voices?.main const vid = (main?.voiceId || main?.VOICE_ID || main?.voice_id) as string | undefined diff --git a/LifeOS/install/LIFEOS/PULSE/adapters/AdapterRunner.ts b/LifeOS/install/LIFEOS/PULSE/adapters/AdapterRunner.ts index effa27b89..30548f4a5 100644 --- a/LifeOS/install/LIFEOS/PULSE/adapters/AdapterRunner.ts +++ b/LifeOS/install/LIFEOS/PULSE/adapters/AdapterRunner.ts @@ -6,9 +6,9 @@ import { hashFile, combineSourceHashes } from "../lib/cache"; import { writePage, writeError, clearError, readMeta, type DataPlaneFile, PULSE_DATA_DIR } from "../lib/data-plane"; import { getProvenance } from "../lib/frontmatter"; import { inference, type InferenceLevel } from "../../TOOLS/Inference"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME!; -const OBSERVABILITY_DIR = resolve(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY"); +const OBSERVABILITY_DIR = resolve(getClaudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY"); const RUNS_LOG = join(OBSERVABILITY_DIR, "adapter-runs.jsonl"); const ADAPTER_TIMEOUT_MS = 120_000; diff --git a/LifeOS/install/LIFEOS/PULSE/checks/airgradient-poll.ts b/LifeOS/install/LIFEOS/PULSE/checks/airgradient-poll.ts index 591534046..7bdce2de5 100755 --- a/LifeOS/install/LIFEOS/PULSE/checks/airgradient-poll.ts +++ b/LifeOS/install/LIFEOS/PULSE/checks/airgradient-poll.ts @@ -12,9 +12,10 @@ import { join } from "node:path" import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } from "node:fs" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" -const CACHE_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "_AIRGRADIENT") +const CACHE_DIR = join(getClaudeDir(), "LIFEOS", "MEMORY", "_AIRGRADIENT") const LATEST = join(CACHE_DIR, "latest.json") const HISTORY = join(CACHE_DIR, "history.jsonl") @@ -23,7 +24,7 @@ const API_BASE = "https://api.airgradient.com/public/api/v1" // Bun auto-loads .env from CWD only; Pulse cron runs from LIFEOS/PULSE/, so the // symlink at ~/.claude/.env isn't picked up. Read it directly if env is empty. function loadTokenFromDotenv(): string | null { - const envPath = join(HOME, ".claude", ".env") + const envPath = join(getClaudeDir(), ".env") if (!existsSync(envPath)) return null try { const raw = readFileSync(envPath, "utf8") diff --git a/LifeOS/install/LIFEOS/PULSE/checks/calendar.ts b/LifeOS/install/LIFEOS/PULSE/checks/calendar.ts index bb97533f7..e7ffd2a3f 100644 --- a/LifeOS/install/LIFEOS/PULSE/checks/calendar.ts +++ b/LifeOS/install/LIFEOS/PULSE/checks/calendar.ts @@ -10,6 +10,7 @@ import { readFileSync } from "fs" import { join } from "path" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" const LOOKAHEAD_MS = 30 * 60 * 1000 @@ -17,7 +18,7 @@ const LOOKAHEAD_MS = 30 * 60 * 1000 function loadEnv(): Record { const env: Record = {} try { - const content = readFileSync(join(HOME, ".claude", ".env"), "utf-8") + const content = readFileSync(join(getClaudeDir(), ".env"), "utf-8") for (const line of content.split("\n")) { const match = line.match(/^([^#=]+)=(.*)$/) if (match) { diff --git a/LifeOS/install/LIFEOS/PULSE/checks/github-work.ts b/LifeOS/install/LIFEOS/PULSE/checks/github-work.ts index 41b1b5b7d..e696316f5 100755 --- a/LifeOS/install/LIFEOS/PULSE/checks/github-work.ts +++ b/LifeOS/install/LIFEOS/PULSE/checks/github-work.ts @@ -12,9 +12,10 @@ import { join } from "path" import { readFileSync } from "fs" import { parse } from "smol-toml" import { SignJWT, importPKCS8 } from "jose" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" -const PULSE_DIR = join(HOME, ".claude", "LIFEOS", "PULSE") +const PULSE_DIR = join(getClaudeDir(), "LIFEOS", "PULSE") const STATE_FILE = join(PULSE_DIR, "state", "work-token.json") // ── Worker Config (from PULSE.toml [worker] section) ── diff --git a/LifeOS/install/LIFEOS/PULSE/checks/github.ts b/LifeOS/install/LIFEOS/PULSE/checks/github.ts index a2f2cef6e..f48d1a026 100755 --- a/LifeOS/install/LIFEOS/PULSE/checks/github.ts +++ b/LifeOS/install/LIFEOS/PULSE/checks/github.ts @@ -10,10 +10,11 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs" import { dirname, join } from "path" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" -const LEGACY_STATE_FILE = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "github-seen.json") -const STATE_FILE = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "github-seen.jsonl") +const LEGACY_STATE_FILE = join(getClaudeDir(), "LIFEOS", "PULSE", "state", "github-seen.json") +const STATE_FILE = join(getClaudeDir(), "LIFEOS", "PULSE", "state", "github-seen.jsonl") // Repos to monitor for new issues / activity. Override via LIFEOS_PULSE_REPOS // env var (comma-separated "owner/name" pairs). Empty default keeps fresh // installs from polling repos the user hasn't opted into. diff --git a/LifeOS/install/LIFEOS/PULSE/checks/life-morning-brief.ts b/LifeOS/install/LIFEOS/PULSE/checks/life-morning-brief.ts index 6f7355b52..3f56f097d 100755 --- a/LifeOS/install/LIFEOS/PULSE/checks/life-morning-brief.ts +++ b/LifeOS/install/LIFEOS/PULSE/checks/life-morning-brief.ts @@ -11,9 +11,10 @@ import { join } from "path" import { existsSync, readFileSync } from "fs" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" -const TELOS_DIR = join(HOME, ".claude", "LIFEOS", "USER", "TELOS") +const TELOS_DIR = join(getClaudeDir(), "LIFEOS", "USER", "TELOS") function readFile(name: string): string { const p = join(TELOS_DIR, name) diff --git a/LifeOS/install/LIFEOS/PULSE/checks/notification-governor.ts b/LifeOS/install/LIFEOS/PULSE/checks/notification-governor.ts index a3b04fc8e..b6f50d8af 100755 --- a/LifeOS/install/LIFEOS/PULSE/checks/notification-governor.ts +++ b/LifeOS/install/LIFEOS/PULSE/checks/notification-governor.ts @@ -31,9 +31,9 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "fs"; import { join, dirname } from "path"; import { createHash } from "crypto"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const STATE_FILE = join(LIFEOS_DIR, "PULSE", "state", "notification-governor.json"); const LOG_FILE = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "notification-governor.jsonl"); const NOTIFY_URL = "http://localhost:31337/notify"; diff --git a/LifeOS/install/LIFEOS/PULSE/checks/poller-meta-monitor.ts b/LifeOS/install/LIFEOS/PULSE/checks/poller-meta-monitor.ts index 22f68d3ac..ba7b902fd 100755 --- a/LifeOS/install/LIFEOS/PULSE/checks/poller-meta-monitor.ts +++ b/LifeOS/install/LIFEOS/PULSE/checks/poller-meta-monitor.ts @@ -17,9 +17,9 @@ import { readFileSync, existsSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const PULSE_STATE = join(LIFEOS_DIR, "PULSE", "state", "state.json"); const PULSE_TOML = join(LIFEOS_DIR, "PULSE", "PULSE.toml"); diff --git a/LifeOS/install/LIFEOS/PULSE/edit/edit-handler.ts b/LifeOS/install/LIFEOS/PULSE/edit/edit-handler.ts index 5eefb80f2..bc3c8b3d8 100644 --- a/LifeOS/install/LIFEOS/PULSE/edit/edit-handler.ts +++ b/LifeOS/install/LIFEOS/PULSE/edit/edit-handler.ts @@ -3,10 +3,10 @@ import { resolve, join } from "node:path"; import { atomicWriteText } from "../lib/atomic-write"; import { parseFrontmatter, serializeFrontmatter } from "../lib/frontmatter"; import { sha256Hex } from "../lib/cache"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME!; -const USER_ROOT = resolve(HOME, ".claude", "LIFEOS", "USER"); -const EDITS_LOG = resolve(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY", "pulse-edits.jsonl"); +const USER_ROOT = resolve(getClaudeDir(), "LIFEOS", "USER"); +const EDITS_LOG = resolve(getClaudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY", "pulse-edits.jsonl"); const CONTAINMENT_PREFIX_DENY = ["MEMORY/PULSE_DATA", "MEMORY/OBSERVABILITY"]; export interface EditRequest { @@ -32,7 +32,7 @@ function isInUserTree(absPath: string): boolean { } function isContainmentPath(absPath: string): boolean { - const rel = absPath.replace(resolve(HOME, ".claude", "LIFEOS") + "/", ""); + const rel = absPath.replace(resolve(getClaudeDir(), "LIFEOS") + "/", ""); return CONTAINMENT_PREFIX_DENY.some((p) => rel.startsWith(p)); } diff --git a/LifeOS/install/LIFEOS/PULSE/lib.ts b/LifeOS/install/LIFEOS/PULSE/lib.ts index 5ba5cb96f..a143024d1 100644 --- a/LifeOS/install/LIFEOS/PULSE/lib.ts +++ b/LifeOS/install/LIFEOS/PULSE/lib.ts @@ -10,6 +10,7 @@ import { join } from "path" import { existsSync } from "fs" import { rename } from "fs/promises" import { modelForEffort } from "../TOOLS/models.ts" +import { getClaudeDir, getLifeosDir } from "../TOOLS/Paths"; // ── Types ── @@ -49,8 +50,8 @@ export interface DaemonConfig { // structural privacy lever — no separate scrub policy needed. export const USER_CRON_PATH = join( - process.env.HOME ?? "~", - ".claude", "LIFEOS", "USER", "CONFIG", "PULSE.user.toml", + getLifeosDir(), + "USER", "CONFIG", "PULSE.user.toml", ) export interface JobState { @@ -319,7 +320,7 @@ export async function spawnScript(command: string, timeoutMs = 60_000): Promise< const proc = Bun.spawn([BASH_PATH, "-c", command], { stdout: "pipe", stderr: "pipe", - cwd: join(process.env.HOME ?? "~", ".claude", "LIFEOS", "PULSE"), + cwd: join(getClaudeDir(), "LIFEOS", "PULSE"), env: { ...process.env }, }) diff --git a/LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts b/LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts index 096a65df1..e569ac1c4 100644 --- a/LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts +++ b/LifeOS/install/LIFEOS/PULSE/lib/data-plane.ts @@ -3,9 +3,9 @@ import { resolve, join } from "node:path"; import { paiRoot } from "./manifest-loader"; import { atomicWriteJSON } from "./atomic-write"; import type { PageData, PageMeta, Provenance } from "../Schema/PulseSchema"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME!; -export const PULSE_DATA_DIR = resolve(HOME, ".claude", "LIFEOS", "MEMORY", "PULSE_DATA"); +export const PULSE_DATA_DIR = resolve(getClaudeDir(), "LIFEOS", "MEMORY", "PULSE_DATA"); export interface DataPlaneFile { schemaVersion: string; diff --git a/LifeOS/install/LIFEOS/PULSE/lib/provenance-watcher.ts b/LifeOS/install/LIFEOS/PULSE/lib/provenance-watcher.ts index dd2e1297e..5bb6690a1 100644 --- a/LifeOS/install/LIFEOS/PULSE/lib/provenance-watcher.ts +++ b/LifeOS/install/LIFEOS/PULSE/lib/provenance-watcher.ts @@ -3,9 +3,9 @@ import { watch } from "node:fs"; import { resolve } from "node:path"; import { atomicWriteText } from "./atomic-write"; import { parseFrontmatter, serializeFrontmatter } from "./frontmatter"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME!; -const EDITS_LOG = resolve(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY", "pulse-edits.jsonl"); +const EDITS_LOG = resolve(getClaudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY", "pulse-edits.jsonl"); const PULSE_EDIT_GRACE_MS = 5_000; export interface WatcherOptions { diff --git a/LifeOS/install/LIFEOS/PULSE/lib/telegram-proposals.ts b/LifeOS/install/LIFEOS/PULSE/lib/telegram-proposals.ts index a8dd519fd..0cbfd463a 100644 --- a/LifeOS/install/LIFEOS/PULSE/lib/telegram-proposals.ts +++ b/LifeOS/install/LIFEOS/PULSE/lib/telegram-proposals.ts @@ -10,6 +10,7 @@ * F7 (ISC-82..95). ISC-85/86 superseded by the 2026-05-23 decision to * route all proposals through Telegram (no silent direct-apply yet). */ +import { getClaudeDir } from "../../TOOLS/Paths"; import { appendFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; @@ -21,7 +22,7 @@ import { } from "../../TOOLS/MemoryTypes"; const HOME = process.env.HOME ?? homedir(); -const OBS_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY"); +const OBS_DIR = join(getClaudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY"); const PROPOSAL_REPLIES_LOG_PATH = join(OBS_DIR, "proposal-replies.jsonl"); const IDENTITY_PROPOSALS_LOG_PATH = join(OBS_DIR, "identity-proposals.jsonl"); @@ -123,7 +124,7 @@ export function logProposalReply(event: Record, path: string = } export function formatProposalMessage(p: ProposalRow, home: string = HOME): string { - const fileLabel = p.target_file.replace(`${home}/.claude/`, ""); + const fileLabel = p.target_file.replace(`${getClaudeDir()}`, ""); const conf = p.confidence.toFixed(2); const obs = p.observed_across_sessions ?? 1; // P1 2026-05-25: prepend subtype badge so the principal sees at a glance diff --git a/LifeOS/install/LIFEOS/PULSE/manage-deriver.sh b/LifeOS/install/LIFEOS/PULSE/manage-deriver.sh index df012bf14..ac0508897 100755 --- a/LifeOS/install/LIFEOS/PULSE/manage-deriver.sh +++ b/LifeOS/install/LIFEOS/PULSE/manage-deriver.sh @@ -13,11 +13,11 @@ set -euo pipefail -PULSE_DIR="$HOME/.claude/LIFEOS/PULSE" +PULSE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/LIFEOS/PULSE" PLIST_NAME="com.lifeos.deriver" PLIST_SRC="$PULSE_DIR/$PLIST_NAME.plist" PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_NAME.plist" -OBSERVABILITY_DIR="$HOME/.claude/LIFEOS/MEMORY/OBSERVABILITY" +OBSERVABILITY_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/LIFEOS/MEMORY/OBSERVABILITY" if [ -x "$HOME/.bun/bin/bun" ]; then BUN_PATH="$HOME/.bun/bin/bun" @@ -81,7 +81,7 @@ case "${1:-}" in run-now) # One-shot manual invocation for testing / first-run priming. - exec "$BUN_PATH" run "$HOME/.claude/LIFEOS/TOOLS/LearningPatternSynthesis.ts" --hypothesize + exec "$BUN_PATH" run "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/LIFEOS/TOOLS/LearningPatternSynthesis.ts" --hypothesize ;; *) diff --git a/LifeOS/install/LIFEOS/PULSE/manage.sh b/LifeOS/install/LIFEOS/PULSE/manage.sh index e08ecd092..a8309e763 100755 --- a/LifeOS/install/LIFEOS/PULSE/manage.sh +++ b/LifeOS/install/LIFEOS/PULSE/manage.sh @@ -2,7 +2,7 @@ # LifeOS Pulse — Process Management # Usage: manage.sh {start|stop|restart|status|install|uninstall} -PULSE_DIR="$HOME/.claude/LIFEOS/PULSE" +PULSE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/LIFEOS/PULSE" PLIST_NAME="com.lifeos.pulse" PLIST_SRC="$PULSE_DIR/$PLIST_NAME.plist" PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_NAME.plist" diff --git a/LifeOS/install/LIFEOS/PULSE/modules/hooks.ts b/LifeOS/install/LIFEOS/PULSE/modules/hooks.ts index 95607e523..13e169389 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/hooks.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/hooks.ts @@ -1,4 +1,5 @@ /** +import { getLifeosDir } from "../../TOOLS/Paths"; * Pulse Module: Hook Validation Server * * Extracted from pulse.ts inline code. @@ -115,7 +116,7 @@ function handleAgentGuard(body: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", - additionalContext: `WATCHDOG: Background agent "${name}" launching. If not already running, start an agent watchdog Monitor:\nMonitor({ description: "Agent watchdog", persistent: true, timeout_ms: 3600000, command: "bun $HOME/.claude/LIFEOS/TOOLS/AgentWatchdog.ts" })`, + additionalContext: `WATCHDOG: Background agent "${name}" launching. If not already running, start an agent watchdog Monitor:\nMonitor({ description: "Agent watchdog", persistent: true, timeout_ms: 3600000, command: "bun ${join(getLifeosDir(), "TOOLS/AgentWatchdog.ts")}" })`, }, }) } diff --git a/LifeOS/install/LIFEOS/PULSE/modules/hypotheses.ts b/LifeOS/install/LIFEOS/PULSE/modules/hypotheses.ts index 2743310b4..5779abca6 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/hypotheses.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/hypotheses.ts @@ -26,9 +26,9 @@ import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const FRAMES_DIR = join(LIFEOS_DIR, "MEMORY", "WISDOM", "FRAMES"); const HYPOTHESES_DIR = join(FRAMES_DIR, "_hypotheses"); const ARCHIVE_DIR = join(HYPOTHESES_DIR, "_archive"); diff --git a/LifeOS/install/LIFEOS/PULSE/modules/imessage.ts b/LifeOS/install/LIFEOS/PULSE/modules/imessage.ts index 003b855f0..d2a6e5fce 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/imessage.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/imessage.ts @@ -28,6 +28,7 @@ import { sendMessage } from "../lib/imessage-send" import { join } from "path" import { appendFile, mkdir, rename } from "fs/promises" import { stripModeScaffolding, hasModeScaffolding } from "../lib/strip-mode-scaffolding" +import { getClaudeDir } from "../../TOOLS/Paths"; // BILLING: Strip ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN before any SDK // query() call. Same rationale as modules/telegram.ts — both outrank OAuth in @@ -62,9 +63,9 @@ export interface IMessageHealth { // ── Module State ── const HOME = process.env.HOME ?? "" -const CWD = join(HOME, ".claude") -const STATE_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "imessage") -const LOGS_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "logs", "imessage") +const CWD = getClaudeDir() +const STATE_DIR = join(getClaudeDir(), "LIFEOS", "PULSE", "state", "imessage") +const LOGS_DIR = join(getClaudeDir(), "LIFEOS", "PULSE", "logs", "imessage") let pollTimer: ReturnType | null = null let running = false diff --git a/LifeOS/install/LIFEOS/PULSE/modules/local-intelligence.ts b/LifeOS/install/LIFEOS/PULSE/modules/local-intelligence.ts index b2534fd66..cdfc6b59b 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/local-intelligence.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/local-intelligence.ts @@ -19,19 +19,20 @@ import { readFile, mkdir, writeFile, stat } from "node:fs/promises" import { join } from "node:path" import { homedir } from "node:os" import { randomUUID } from "node:crypto" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? homedir() const MODULE_NAME = "local-intelligence" // Primary path: user-scoped customizations directory (per {{PRINCIPAL_NAME}} directive 2026-05-03). // Fallback path: legacy MEMORY/DATA path (used when customizations file absent). -const CUSTOMIZATIONS_DIR = join(HOME, ".claude", "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "LocalIntelligence") -const LEGACY_DATA_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "DATA", "LocalIntelligence") +const CUSTOMIZATIONS_DIR = join(getClaudeDir(), "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "LocalIntelligence") +const LEGACY_DATA_DIR = join(getClaudeDir(), "LIFEOS", "MEMORY", "DATA", "LocalIntelligence") const LATEST_PATH = join(CUSTOMIZATIONS_DIR, "latest.json") const LEGACY_LATEST_PATH = join(LEGACY_DATA_DIR, "latest.json") const DATA_DIR = CUSTOMIZATIONS_DIR // alias for existing references in this file const RUNS_DIR = join(CUSTOMIZATIONS_DIR, "runs") -const REFRESH_SCRIPT = join(HOME, ".claude", "skills", "LocalIntelligence", "Tools", "Refresh.ts") +const REFRESH_SCRIPT = join(getClaudeDir(), "skills", "LocalIntelligence", "Tools", "Refresh.ts") async function readLatest(): Promise { try { return await readFile(LATEST_PATH, "utf8") } catch {} diff --git a/LifeOS/install/LIFEOS/PULSE/modules/memory.ts b/LifeOS/install/LIFEOS/PULSE/modules/memory.ts index 22db4510a..455174258 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/memory.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/memory.ts @@ -28,9 +28,9 @@ import { statSync, } from "node:fs"; import { join } from "node:path"; +import { getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME || ""; -const CLAUDE = join(HOME, ".claude"); +const CLAUDE = getClaudeDir(); const OBS_DIR = join(CLAUDE, "LIFEOS/MEMORY/OBSERVABILITY"); const REVIEW_STATE = join(OBS_DIR, "review-state.json"); diff --git a/LifeOS/install/LIFEOS/PULSE/modules/syslog.ts b/LifeOS/install/LIFEOS/PULSE/modules/syslog.ts index d2bbca59b..e884e5287 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/syslog.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/syslog.ts @@ -15,6 +15,7 @@ import { createSocket, type Socket } from "dgram" import { appendFileSync, mkdirSync, existsSync, statSync, readFileSync } from "fs" import { dirname, join } from "path" +import { getLifeosDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" const MODULE_NAME = "syslog" @@ -22,9 +23,7 @@ const DEFAULT_PORT = 5514 const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB rotation threshold const LOG_PATH = join( - HOME, - ".claude", - "LIFEOS", + getLifeosDir(), "MEMORY", "OBSERVABILITY", "unifi-syslog.jsonl", diff --git a/LifeOS/install/LIFEOS/PULSE/modules/tab-freshness.ts b/LifeOS/install/LIFEOS/PULSE/modules/tab-freshness.ts index 73e522293..046e68ec1 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/tab-freshness.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/tab-freshness.ts @@ -28,9 +28,10 @@ import { existsSync, statSync, readdirSync, readFileSync } from "fs" import { join } from "path" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "~" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") const USER_DIR = join(LIFEOS_DIR, "USER") const TELOS_DIR = join(USER_DIR, "TELOS") @@ -82,14 +83,14 @@ const REGISTRY: Record = { { name: "KNOWLEDGE/", path: join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"), expand: true }, ], hooks: [ - { name: "hooks/", path: join(HOME, ".claude", "hooks"), expand: true }, - { name: "settings.json", path: join(HOME, ".claude", "settings.json") }, + { name: "hooks/", path: join(getClaudeDir(), "hooks"), expand: true }, + { name: "settings.json", path: join(getClaudeDir(), "settings.json") }, ], skills: [ - { name: "skills/", path: join(HOME, ".claude", "skills"), expand: true }, + { name: "skills/", path: join(getClaudeDir(), "skills"), expand: true }, ], agents: [ - { name: "agents/", path: join(HOME, ".claude", "agents"), expand: true }, + { name: "agents/", path: join(getClaudeDir(), "agents"), expand: true }, ], docs: [ { name: "DOCUMENTATION/", path: join(LIFEOS_DIR, "DOCUMENTATION"), expand: true }, diff --git a/LifeOS/install/LIFEOS/PULSE/modules/telegram.ts b/LifeOS/install/LIFEOS/PULSE/modules/telegram.ts index 708730b7d..34b60bdf2 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/telegram.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/telegram.ts @@ -31,6 +31,7 @@ import { type ProposalReply, } from "../lib/telegram-proposals" import { stripModeScaffolding, hasModeScaffolding } from "../lib/strip-mode-scaffolding" +import { getClaudeDir } from "../../TOOLS/Paths"; // BILLING: Strip ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN before any SDK // query() call. Bun auto-loads ~/.claude/.env into this process; if either key @@ -63,9 +64,9 @@ export interface TelegramConfig { // ── Constants ── const HOME = process.env.HOME ?? "" -const CWD = join(HOME, ".claude") -const STATE_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "state", "telegram") -const LOGS_DIR = join(HOME, ".claude", "LIFEOS", "PULSE", "logs", "telegram") +const CWD = getClaudeDir() +const STATE_DIR = join(getClaudeDir(), "LIFEOS", "PULSE", "state", "telegram") +const LOGS_DIR = join(getClaudeDir(), "LIFEOS", "PULSE", "logs", "telegram") const STALE_ACK_CACHE_DIR = join(STATE_DIR, "ack-cache") const MAX_TELEGRAM_LENGTH = 4096 const CURSOR = " ▌" @@ -76,7 +77,7 @@ const IDLE_TIMEOUT_MS = 60 * 60 * 1000 // 1 hour — gap of silence tha const INFERENCE_HARD_BUDGET_MS = 10_000 // outer race cap on summarize; measured Sonnet subprocess cost is 4-6s, this gives slack without losing the voice trailing the text by too much const MIN_FALLBACK_WORDS = 6 // a fallback summary shorter than this is presumed too thin to be worth voicing const MEANINGFUL_REPLY_WORDS = 25 // when a reply is at least this long, a too-short fallback is a regression — skip voice rather than ship a "0:00" stub -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") // Voice ID for outbound voice summaries. Read at module import from // LifeosConfig — `[da.voices.main] voice_id` in LIFEOS/USER/CONFIG/LIFEOS_CONFIG.toml. @@ -402,7 +403,7 @@ async function handleProposalReply(chatId: number, reply: ProposalReply, ctx: { markProposal(row.id, { status: "accepted", resolved_at: new Date().toISOString(), applied_edit: row.edit }) logProposalEvent({ id: row.id, file: row.target_file, edit: row.edit, confidence: row.confidence, status: "accepted" }) logProposalReply({ kind: "yes", id: row.id, outcome: "applied", chatId }) - const fileLabel = row.target_file.replace(`${HOME}/.claude/`, "") + const fileLabel = row.target_file.replace(`${getClaudeDir()}`, "") await ctx.reply(`✅ Applied to ${fileLabel}`).catch(() => {}) } else { logProposalReply({ kind: "yes", id: row.id, outcome: "apply-failed", reason: result.reason, chatId }) @@ -429,7 +430,7 @@ async function handleProposalReply(chatId: number, reply: ProposalReply, ctx: { markProposal(row.id, { status: "edited", resolved_at: new Date().toISOString(), applied_edit: reply.editText }) logProposalEvent({ id: row.id, file: row.target_file, edit: reply.editText, confidence: row.confidence, status: "edited" }) logProposalReply({ kind: "edit", id: row.id, outcome: "applied", chatId }) - const fileLabel = row.target_file.replace(`${HOME}/.claude/`, "") + const fileLabel = row.target_file.replace(`${getClaudeDir()}`, "") await ctx.reply(`✅ Applied your edit to ${fileLabel}`).catch(() => {}) } else { logProposalReply({ kind: "edit", id: row.id, outcome: "apply-failed", reason: result.reason, chatId }) diff --git a/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts b/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts index 6446e6fb5..e1172af1f 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts @@ -21,9 +21,10 @@ import { readFileSync, writeFileSync, statSync, readdirSync, mkdirSync, existsSync, watch } from "fs" import { join, relative, basename, dirname } from "path" +import { getClaudeDir } from "../../TOOLS/Paths"; const HOME = process.env.HOME ?? "" -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS") const USER_DIR = join(LIFEOS_DIR, "USER") const STATE_DIR = join(LIFEOS_DIR, "PULSE", "state") const INDEX_PATH = join(STATE_DIR, "user-index.json") diff --git a/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts b/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts index 7dcaf4bfa..a287e0f33 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts @@ -32,19 +32,20 @@ import { writeFileSync, } from "fs" import MiniSearch from "minisearch" +import { getClaudeDir } from "../../TOOLS/Paths"; // Path Construction const HOME = process.env.HOME ?? "~" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") const DOCUMENTATION_DIR = join(LIFEOS_DIR, "DOCUMENTATION") const KNOWLEDGE_DIR = join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE") const BOOKMARKS_DIR = join(LIFEOS_DIR, "MEMORY", "BOOKMARKS") const BOOKMARKS_CSV = join(BOOKMARKS_DIR, "bookmarks.csv") const ALGORITHM_DIR = join(LIFEOS_DIR, "ALGORITHM") -const SKILLS_DIR = join(HOME, ".claude", "skills") -const HOOKS_DIR = join(HOME, ".claude", "hooks") -const SETTINGS_PATH = join(HOME, ".claude", "settings.json") +const SKILLS_DIR = join(getClaudeDir(), "skills") +const HOOKS_DIR = join(getClaudeDir(), "hooks") +const SETTINGS_PATH = join(getClaudeDir(), "settings.json") const ARBOL_WORKERS_DIR = join(LIFEOS_DIR, "USER", "CUSTOMIZATIONS", "ARBOL", "Workers") const SYSTEM_PROMPT_PATH = join(LIFEOS_DIR, "LIFEOS_SYSTEM_PROMPT.md") diff --git a/LifeOS/install/LIFEOS/PULSE/modules/work.ts b/LifeOS/install/LIFEOS/PULSE/modules/work.ts index 6191c2f42..31982ec73 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/work.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/work.ts @@ -24,9 +24,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs"; import { join } from "path"; import { loadWorkConfig, type WorkConfig } from "../../../hooks/lib/work-config"; +import { displayPath, getClaudeDir } from "../../TOOLS/Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const PULSE_STATE_DIR = join(LIFEOS_DIR, "PULSE", "state"); const CACHE_PATH = join(PULSE_STATE_DIR, "work-cache.json"); const MODULE = "work"; @@ -145,7 +145,7 @@ function extractSlug(title: string): string | undefined { // issues; the workload is bounded and the files are small. function extractPrincipalGoal(slug: string | undefined): string | undefined { if (!slug) return undefined; - const isaPath = join(HOME, ".claude", "LIFEOS", "MEMORY", "WORK", slug, "ISA.md"); + const isaPath = join(getClaudeDir(), "LIFEOS", "MEMORY", "WORK", slug, "ISA.md"); if (!existsSync(isaPath)) return undefined; try { const content = readFileSync(isaPath, "utf-8"); @@ -309,9 +309,9 @@ function setupTemplate(reason: string): Response { reason, subtype, instructions: [ - "Configure the work repo via the privacy-attested CLI: `bun ~/.claude/skills/_ULWORK/Tools/SetWorkRepo.ts `. The CLI calls `gh repo view --json visibility,isPrivate` and refuses to write the config unless the repo is currently private.", + "Configure the work repo via the privacy-attested CLI: `bun ${displayPath(getClaudeDir())}/skills/_ULWORK/Tools/SetWorkRepo.ts `. The CLI calls `gh repo view --json visibility,isPrivate` and refuses to write the config unless the repo is currently private.", "Ensure the repo has these labels: Type:feature, Type:reminder, Type:research, Type:queue, Status:queued, Status:in-progress, Status:in-review, Status:blocked, Status:done, Priority:P0..P3, Property:internal, Agent:kai, pai-sync.", - "Restart Pulse so this module re-reads work_repo.json: `bun ~/.claude/LIFEOS/PULSE/manage.sh restart`.", + "Restart Pulse so this module re-reads work_repo.json: `bun ${displayPath(getClaudeDir())}/LIFEOS/PULSE/manage.sh restart`.", "Run an Algorithm session — ULWorkSync.hook.ts will open the first issue at SessionEnd.", ], docs: "skills/_ULWORK/SKILL.md (search 'Capture flow')", diff --git a/LifeOS/install/LIFEOS/PULSE/pulse-old.ts b/LifeOS/install/LIFEOS/PULSE/pulse-old.ts index 913750a74..336780814 100755 --- a/LifeOS/install/LIFEOS/PULSE/pulse-old.ts +++ b/LifeOS/install/LIFEOS/PULSE/pulse-old.ts @@ -6,13 +6,14 @@ * Checks things on schedule, dispatches to existing services. * No channels, no queue, no AI triage — just run jobs and route output. */ +import { getClaudeDir } from "../TOOLS/Paths"; import { join } from "path" import { readFileSync } from "fs" // ── Load .env before anything else ── -const envPath = join(process.env.HOME ?? "~", ".claude", ".env") +const envPath = join(getClaudeDir(), ".env") try { const envContent = readFileSync(envPath, "utf-8") for (const line of envContent.split("\n")) { @@ -46,7 +47,7 @@ import { // ── Constants ── -const PULSE_DIR = join(process.env.HOME ?? "~", ".claude", "LIFEOS", "PULSE") +const PULSE_DIR = join(getClaudeDir(), "LIFEOS", "PULSE") const STATE_PATH = join(PULSE_DIR, "state", "state.json") const PID_PATH = join(PULSE_DIR, "state", "pulse.pid") const HOOK_PORT = parseInt(process.env.HOOK_SERVER_PORT || "8686", 10) diff --git a/LifeOS/install/LIFEOS/PULSE/pulse-unified.ts b/LifeOS/install/LIFEOS/PULSE/pulse-unified.ts index a3d384282..be1cffc08 100755 --- a/LifeOS/install/LIFEOS/PULSE/pulse-unified.ts +++ b/LifeOS/install/LIFEOS/PULSE/pulse-unified.ts @@ -21,10 +21,10 @@ import { parse } from "smol-toml" // ── Load .env before anything else ── const HOME = process.env.HOME ?? "~" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") const PULSE_DIR = join(LIFEOS_DIR, "PULSE") -const envPath = join(HOME, ".claude", ".env") +const envPath = join(getClaudeDir(), ".env") try { const envContent = readFileSync(envPath, "utf-8") for (const line of envContent.split("\n")) { @@ -58,6 +58,7 @@ import { } from "./lib" import { startHooks, handleHooksRequestAsync, hooksHealth } from "./modules/hooks" +import { getClaudeDir } from "../TOOLS/Paths"; // Conditional imports — modules may not exist yet during incremental migration let voiceModule: any = null diff --git a/LifeOS/install/LIFEOS/PULSE/pulse.ts b/LifeOS/install/LIFEOS/PULSE/pulse.ts index 1a7251df4..66d30ff9c 100755 --- a/LifeOS/install/LIFEOS/PULSE/pulse.ts +++ b/LifeOS/install/LIFEOS/PULSE/pulse.ts @@ -21,10 +21,10 @@ import { parse } from "smol-toml" // ── Load .env before anything else ── const HOME = process.env.HOME ?? "~" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") const PULSE_DIR = join(LIFEOS_DIR, "PULSE") -const envPath = join(HOME, ".claude", ".env") +const envPath = join(getClaudeDir(), ".env") try { const envContent = readFileSync(envPath, "utf-8") for (const line of envContent.split("\n")) { @@ -69,6 +69,7 @@ import { } from "./lib" import { startHooks, handleHooksRequestAsync, hooksHealth } from "./modules/hooks" +import { getClaudeDir } from "../TOOLS/Paths"; // Conditional imports — modules may not exist yet during incremental migration let voiceModule: any = null diff --git a/LifeOS/install/LIFEOS/PULSE/run-job.ts b/LifeOS/install/LIFEOS/PULSE/run-job.ts index d00ad89ae..217c01d58 100755 --- a/LifeOS/install/LIFEOS/PULSE/run-job.ts +++ b/LifeOS/install/LIFEOS/PULSE/run-job.ts @@ -7,7 +7,7 @@ import { join } from "path" import { readFileSync } from "fs" // Load .env -const envPath = join(process.env.HOME ?? "~", ".claude", ".env") +const envPath = join(getClaudeDir(), ".env") try { const envContent = readFileSync(envPath, "utf-8") for (const line of envContent.split("\n")) { @@ -24,6 +24,7 @@ try { } catch {} import { loadConfig, spawnClaude, spawnScript, dispatch, isSentinel, log } from "./lib" +import { getClaudeDir } from "../TOOLS/Paths"; const jobName = process.argv[2] if (!jobName) { @@ -31,7 +32,7 @@ if (!jobName) { process.exit(1) } -const PULSE_DIR = join(process.env.HOME ?? "~", ".claude", "LIFEOS", "PULSE") +const PULSE_DIR = join(getClaudeDir(), "LIFEOS", "PULSE") const config = await loadConfig(PULSE_DIR) const job = config.jobs.find((j) => j.name === jobName) if (!job) { diff --git a/LifeOS/install/LIFEOS/PULSE/setup.ts b/LifeOS/install/LIFEOS/PULSE/setup.ts index e3cd88c4a..089e0e113 100755 --- a/LifeOS/install/LIFEOS/PULSE/setup.ts +++ b/LifeOS/install/LIFEOS/PULSE/setup.ts @@ -12,9 +12,10 @@ import { join, resolve } from "path" import { existsSync, mkdirSync } from "fs" +import { getClaudeDir } from "../TOOLS/Paths"; const HOME = process.env.HOME ?? "~" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") const PULSE_DIR = join(LIFEOS_DIR, "PULSE") // ── Helpers ── @@ -235,7 +236,7 @@ enabled = true ``, ] - const envPath = join(HOME, ".claude", ".env") + const envPath = join(getClaudeDir(), ".env") if (existsSync(envPath)) { warn(`.env already exists — appending worker config`) const existing = await Bun.file(envPath).text() @@ -444,7 +445,7 @@ ${"═".repeat(50)} Time: ${Math.floor(elapsed / 60)}m ${elapsed % 60}s Next steps: - - Verify ANTHROPIC_API_KEY is set in ${join(HOME, ".claude", ".env")} + - Verify ANTHROPIC_API_KEY is set in ${join(getClaudeDir(), ".env")} - Create a test issue with label "status:ready" in one of your repos - Watch: tail -f ${join(PULSE_DIR, "logs", "pulse-stdout.log")} - Status: ${join(PULSE_DIR, "manage.sh")} status diff --git a/LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts b/LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts index ea6aae592..4cef1b1e3 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ActivityParser.ts @@ -16,15 +16,20 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; +import { getClaudeDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ -const CLAUDE_DIR = path.join(process.env.HOME!, ".claude"); +const CLAUDE_DIR = getClaudeDir(); const MEMORY_DIR = path.join(CLAUDE_DIR, "LIFEOS", "MEMORY"); -const USERNAME = process.env.USER || require("os").userInfo().username; -const PROJECTS_DIR = path.join(CLAUDE_DIR, "projects", `-Users-${USERNAME}--claude`); // Claude Code native storage +// Claude Code encodes a session's cwd into its projects/ subdir name by +// replacing every "/" and "." with "-"; sessions started in the config dir +// land under that encoding of the config dir itself. +const PROJECTS_DIR = path.join(CLAUDE_DIR, "projects", CLAUDE_DIR.replace(/[/.]/g, "-")); // Claude Code native storage +// Path fragment that marks a file as living inside the active config dir. +const CLAUDE_MARKER = "/" + path.basename(CLAUDE_DIR) + "/"; const SYSTEM_UPDATES_DIR = path.join(MEMORY_DIR, "PAISYSTEMUPDATES"); // Canonical system change history // ============================================================================ @@ -85,7 +90,7 @@ function shouldSkip(filePath: string): boolean { function categorizeFile(filePath: string): keyof ParsedActivity["categories"] | null { if (shouldSkip(filePath)) return null; - if (!filePath.includes("/.claude/")) return null; + if (!filePath.includes(CLAUDE_MARKER)) return null; if (PATTERNS.skills.test(filePath)) return "skills"; if (PATTERNS.workflows.test(filePath)) return "workflows"; @@ -103,9 +108,9 @@ function extractSkillName(filePath: string): string | null { } function getRelativePath(filePath: string): string { - const claudeIndex = filePath.indexOf("/.claude/"); + const claudeIndex = filePath.indexOf(CLAUDE_MARKER); if (claudeIndex === -1) return filePath; - return filePath.substring(claudeIndex + 9); // Skip "/.claude/" + return filePath.substring(claudeIndex + CLAUDE_MARKER.length); } // ============================================================================ @@ -202,7 +207,7 @@ async function parseEvents(sessionFilter?: string): Promise { // Write tool = new files if (contentItem.name === "Write" && contentItem.input?.file_path) { const filePath = contentItem.input.file_path; - if (filePath.includes("/.claude/")) { + if (filePath.includes(CLAUDE_MARKER)) { filesCreated.add(filePath); } } @@ -210,7 +215,7 @@ async function parseEvents(sessionFilter?: string): Promise { // Edit tool = modified files if (contentItem.name === "Edit" && contentItem.input?.file_path) { const filePath = contentItem.input.file_path; - if (filePath.includes("/.claude/")) { + if (filePath.includes(CLAUDE_MARKER)) { filesModified.add(filePath); } } diff --git a/LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts b/LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts index 8987e29da..ca613c6a8 100755 --- a/LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts +++ b/LifeOS/install/LIFEOS/TOOLS/AgentWatchdog.ts @@ -19,8 +19,9 @@ import { existsSync, readFileSync, statSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY"); const ACTIVITY_FILE = join(OBS_DIR, "tool-activity.jsonl"); const STARTS_FILE = join(OBS_DIR, "subagent-starts.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/AlgoPhase.ts b/LifeOS/install/LIFEOS/TOOLS/AlgoPhase.ts index 3e1e79b42..f61b8c324 100755 --- a/LifeOS/install/LIFEOS/TOOLS/AlgoPhase.ts +++ b/LifeOS/install/LIFEOS/TOOLS/AlgoPhase.ts @@ -31,6 +31,7 @@ import { readRegistry, writeRegistry } from '../../hooks/lib/isa-utils'; import { setPhaseTab } from '../../hooks/lib/tab-setter'; import { effortToCanonicalELevel } from '../../hooks/lib/effort'; import type { AlgorithmTabPhase } from '../../hooks/lib/tab-constants'; +import { displayPath, getClaudeDir } from "./Paths"; const VALID_PHASES = new Set([ 'observe', 'think', 'plan', 'build', 'execute', 'verify', 'learn', 'complete', 'starting', @@ -72,7 +73,7 @@ function printHelp(): void { console.log(`AlgoPhase — atomic Algorithm phase emitter Usage: - bun ~/.claude/LIFEOS/TOOLS/AlgoPhase.ts [--slug X] [--uuid X] [--iteration N] + bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/AlgoPhase.ts [--slug X] [--uuid X] [--iteration N] Phases: observe | think | plan | build | execute | verify | learn | complete | starting @@ -83,9 +84,9 @@ Slug resolution priority: 4. most-recent algorithm-mode row in work.json Examples: - bun ~/.claude/LIFEOS/TOOLS/AlgoPhase.ts think - bun ~/.claude/LIFEOS/TOOLS/AlgoPhase.ts build --slug 20260524-072107_pulse-agents - bun ~/.claude/LIFEOS/TOOLS/AlgoPhase.ts verify --uuid 49348c25-a71f-47f1-b038-0f26192f24bf + bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/AlgoPhase.ts think + bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/AlgoPhase.ts build --slug 20260524-072107_pulse-agents + bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/AlgoPhase.ts verify --uuid 49348c25-a71f-47f1-b038-0f26192f24bf `); } diff --git a/LifeOS/install/LIFEOS/TOOLS/AlgorithmPhaseReport.ts b/LifeOS/install/LIFEOS/TOOLS/AlgorithmPhaseReport.ts index 6eb80a5e3..6bb80d4c7 100755 --- a/LifeOS/install/LIFEOS/TOOLS/AlgorithmPhaseReport.ts +++ b/LifeOS/install/LIFEOS/TOOLS/AlgorithmPhaseReport.ts @@ -15,10 +15,10 @@ import { readFileSync, writeFileSync, mkdirSync } from "fs"; import { join } from "path"; -import { homedir } from "os"; import { parseArgs } from "util"; +import { getClaudeDir } from "./Paths"; -const STATE_DIR = join(homedir(), ".claude", "LIFEOS", "MEMORY", "STATE"); +const STATE_DIR = join(getClaudeDir(), "LIFEOS", "MEMORY", "STATE"); const STATE_FILE = join(STATE_DIR, "algorithm-phase.json"); interface AlgorithmState { diff --git a/LifeOS/install/LIFEOS/TOOLS/ApproveCurrentStateEntries.ts b/LifeOS/install/LIFEOS/TOOLS/ApproveCurrentStateEntries.ts index 79870037f..c26569a79 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ApproveCurrentStateEntries.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ApproveCurrentStateEntries.ts @@ -18,9 +18,9 @@ import { readFileSync, writeFileSync, existsSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const QUEUE_FILE = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE", "proposals.jsonl"); const CURRENT_STATE_DIR = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE"); diff --git a/LifeOS/install/LIFEOS/TOOLS/ArchDecisionHarvest.ts b/LifeOS/install/LIFEOS/TOOLS/ArchDecisionHarvest.ts index 47d9467a7..2660dd500 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ArchDecisionHarvest.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ArchDecisionHarvest.ts @@ -18,13 +18,13 @@ import { spawnSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; +import { getClaudeDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ -const HOME = process.env.HOME || os.homedir(); -const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(getClaudeDir(), "LIFEOS"); const DEFAULT_WORK_DIR = path.join(LIFEOS_DIR, "MEMORY", "WORK"); const DEFAULT_ARCH_DOC = path.join(LIFEOS_DIR, "DOCUMENTATION", "LifeosSystemArchitecture.md"); const ARCH_DECISIONS_HEADING = "## Architecture Decisions"; diff --git a/LifeOS/install/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts b/LifeOS/install/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts index e8e65f8dc..8342f5e2e 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ArchitectureSummaryGenerator.ts @@ -14,18 +14,19 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; +import { getClaudeDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ const HOME = process.env.HOME!; -const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(getClaudeDir(), "LIFEOS"); const ARCH_SOURCE = path.join(LIFEOS_DIR, "DOCUMENTATION", "LifeosSystemArchitecture.md"); const SUMMARY_OUTPUT = path.join(LIFEOS_DIR, "DOCUMENTATION", "ARCHITECTURE_SUMMARY.md"); const ALGORITHM_DIR = path.join(LIFEOS_DIR, "ALGORITHM"); const MEMORY_SYSTEM_DOC = path.join(LIFEOS_DIR, "DOCUMENTATION", "Memory", "MemorySystem.md"); -const CLAUDE_MD = path.join(HOME, ".claude", "CLAUDE.md"); +const CLAUDE_MD = path.join(getClaudeDir(), "CLAUDE.md"); // ============================================================================ // Version detection (source-of-truth lookups — no hardcoded versions) @@ -128,7 +129,7 @@ function extractSections(content: string): Array<{ heading: string; level: numbe * LIFEOS/DOCUMENTATION/. The downstream `USER/` filter then correctly drops them. */ function extractSubsystems(): Array<{ name: string; description: string; docPath: string }> { - const claudeMdPath = path.join(HOME, ".claude", "CLAUDE.md"); + const claudeMdPath = path.join(getClaudeDir(), "CLAUDE.md"); if (!fs.existsSync(claudeMdPath)) return []; const content = fs.readFileSync(claudeMdPath, "utf-8"); @@ -257,7 +258,7 @@ function generate(): string { ? sub.docPath : sub.docPath.startsWith("~") ? sub.docPath.replace(/^~/, HOME) - : path.join(HOME, ".claude", sub.docPath); + : path.join(getClaudeDir(), sub.docPath); const shortPath = sub.docPath.replace("~/.claude/", ""); if (!fs.existsSync(resolved)) missing.push(shortPath); subsystemRows.push(`- ${sub.name} — ${shortPath}`); @@ -352,7 +353,7 @@ function cmdCheck(): void { const sourceMtime = getMtime(ARCH_SOURCE); const summaryMtime = getMtime(SUMMARY_OUTPUT); - const claudeMdMtime = getMtime(path.join(HOME, ".claude", "CLAUDE.md")); + const claudeMdMtime = getMtime(path.join(getClaudeDir(), "CLAUDE.md")); if (sourceMtime > summaryMtime || claudeMdMtime > summaryMtime) { console.log("STALE: Source files are newer than summary"); diff --git a/LifeOS/install/LIFEOS/TOOLS/Banner.ts b/LifeOS/install/LIFEOS/TOOLS/Banner.ts index 1e34ede28..2c5c7f795 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Banner.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Banner.ts @@ -12,9 +12,9 @@ import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { spawnSync } from "child_process"; import { parse as parseYaml } from "yaml"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME!; -const CLAUDE_DIR = join(HOME, ".claude"); +const CLAUDE_DIR = getClaudeDir(); // ═══════════════════════════════════════════════════════════════════════════ // Terminal Width Detection diff --git a/LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts b/LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts index c8bb841f1..51fdc7001 100755 --- a/LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts +++ b/LifeOS/install/LIFEOS/TOOLS/BannerMatrix.ts @@ -21,9 +21,9 @@ import { readdirSync, existsSync, readFileSync } from "fs"; import { join } from "path"; import { spawnSync } from "child_process"; import { paiUserDir } from "./LifeosConfig"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME!; -const CLAUDE_DIR = join(HOME, ".claude"); +const CLAUDE_DIR = getClaudeDir(); // ============================================================================= // Terminal Width Detection diff --git a/LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts b/LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts index 99fade61b..be84c7a51 100755 --- a/LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts +++ b/LifeOS/install/LIFEOS/TOOLS/BannerNeofetch.ts @@ -14,9 +14,9 @@ import { readdirSync, existsSync, readFileSync } from "fs"; import { join } from "path"; import { spawnSync } from "child_process"; import { paiUserDir } from "./LifeosConfig"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME!; -const CLAUDE_DIR = join(HOME, ".claude"); +const CLAUDE_DIR = getClaudeDir(); // ═══════════════════════════════════════════════════════════════════════ // Terminal Width Detection diff --git a/LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts b/LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts index 6caa6807a..3c3ab29b8 100755 --- a/LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts +++ b/LifeOS/install/LIFEOS/TOOLS/BannerRetro.ts @@ -19,9 +19,9 @@ import { readdirSync, existsSync, readFileSync } from "fs"; import { join } from "path"; import { spawnSync } from "child_process"; import { paiUserDir } from "./LifeosConfig"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME!; -const CLAUDE_DIR = join(HOME, ".claude"); +const CLAUDE_DIR = getClaudeDir(); // ═══════════════════════════════════════════════════════════════════════════ // Terminal Width Detection diff --git a/LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts b/LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts index ef5b7dd44..6bd89007c 100755 --- a/LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts +++ b/LifeOS/install/LIFEOS/TOOLS/BlogDiscovery.ts @@ -34,11 +34,12 @@ import { homedir } from "os"; import { join } from "path"; import { spawnSync } from "child_process"; import { randomUUID } from "crypto"; +import { getClaudeDir, getLifeosDir } from "./Paths"; const HOME = homedir(); -const DB_PATH = join(HOME, ".claude/LIFEOS/MEMORY/STATE/feed-candidates.db"); -const CFENV = join(HOME, ".claude/skills/_CLOUDFLARE/Tools/CfEnv.ts"); -const INFERENCE = join(HOME, ".claude/LIFEOS/TOOLS/Inference.ts"); +const DB_PATH = join(getLifeosDir(), "MEMORY/STATE/feed-candidates.db"); +const CFENV = join(getClaudeDir(), "skills/_CLOUDFLARE/Tools/CfEnv.ts"); +const INFERENCE = join(getLifeosDir(), "TOOLS/Inference.ts"); const SURFACE_TAX = join(HOME, "Projects/Surface/src/categories.ts"); const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"; @@ -396,7 +397,7 @@ async function cmdHarvest() { const pend = (db.query("SELECT COUNT(*) n FROM candidates WHERE status='pending' AND combined IS NOT NULL").get() as any).n; const good = (db.query("SELECT COUNT(*) n FROM candidates WHERE status='pending' AND combined>=55").get() as any).n; console.log(`[harvest] done. pending scored: ${pend}, of which combined>=55: ${good}.`); - console.log(`Shortlist: bun ${join(HOME, ".claude/LIFEOS/TOOLS/BlogDiscovery.ts")} top --n 50`); + console.log(`Shortlist: bun ${join(getLifeosDir(), "TOOLS/BlogDiscovery.ts")} top --n 50`); } function rankRows(db: Database, limit: number, min: number, maxAgeDays: number, minFit = 0): any[] { diff --git a/LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts b/LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts index 2fda61d1e..d02da9d07 100755 --- a/LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/BookmarkSweep.ts @@ -21,12 +21,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); -const X_DIR = join(HOME, ".claude", "skills", "_X"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); +const X_DIR = join(getClaudeDir(), "skills", "_X"); const BOOKMARKS_TOOL = join(X_DIR, "Tools", "bookmarks.ts"); const BOOKMARK_ISSUE_TOOL = join(X_DIR, "Tools", "bookmark-issue.ts"); const INFERENCE_TOOL = join(LIFEOS_DIR, "TOOLS", "Inference.ts"); diff --git a/LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts b/LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts index e7dc366d2..05f0afd10 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Checkpoint.ts @@ -16,13 +16,14 @@ import { execFileSync } from 'node:child_process'; import { join } from 'node:path'; import { homedir } from 'node:os'; import { parseCriteriaList } from '../../hooks/lib/isa-utils'; +import { displayPath, getClaudeDir } from "./Paths"; // Allowlist path: top of ~/.claude per spec. We only READ it (never write), // so the ContainmentGuard write restriction does not apply. Parser must match // the hook's parser exactly: skip blanks and '#' lines, expand tilde / $HOME // prefixes, treat the rest as absolute repo paths. -const ALLOWLIST_PATH = join(homedir(), '.claude', 'checkpoint-repos.txt'); -const WORK_DIR = join(homedir(), '.claude', 'LIFEOS', 'MEMORY', 'WORK'); +const ALLOWLIST_PATH = join(getClaudeDir(), 'checkpoint-repos.txt'); +const WORK_DIR = join(getClaudeDir(), 'LIFEOS', 'MEMORY', 'WORK'); function expandPath(p: string): string { let s = p.trim(); @@ -188,9 +189,9 @@ function cmdRollback(slug: string, iscId: string) { function usage() { console.log(`Usage: - bun ~/.claude/LIFEOS/TOOLS/Checkpoint.ts list - bun ~/.claude/LIFEOS/TOOLS/Checkpoint.ts show - bun ~/.claude/LIFEOS/TOOLS/Checkpoint.ts rollback + bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/Checkpoint.ts list + bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/Checkpoint.ts show + bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/Checkpoint.ts rollback Allowlist: ${ALLOWLIST_PATH} Work dir: ${WORK_DIR} diff --git a/LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts b/LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts index 0e95522db..b798dc82b 100755 --- a/LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts +++ b/LifeOS/install/LIFEOS/TOOLS/CodexUpdate.ts @@ -18,10 +18,10 @@ import { spawnSync } from "child_process"; import { appendFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; const PKG = "@openai/codex"; -const LOG = join(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY", "codex-update.jsonl"); +const LOG = join(getClaudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY", "codex-update.jsonl"); function codexVersion(): string | null { const r = spawnSync("codex", ["--version"], { encoding: "utf-8" }); diff --git a/LifeOS/install/LIFEOS/TOOLS/CommitmentSweep.ts b/LifeOS/install/LIFEOS/TOOLS/CommitmentSweep.ts index 7b5c3a92a..e80c567ad 100755 --- a/LifeOS/install/LIFEOS/TOOLS/CommitmentSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/CommitmentSweep.ts @@ -23,9 +23,9 @@ import { existsSync, mkdirSync, appendFileSync } from "fs"; import { join } from "path"; import { spawnSync } from "child_process"; import { loadWorkConfig } from "../../hooks/lib/work-config"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY"); const OBS_LOG = join(OBS_DIR, "commitment-digest.jsonl"); const PULSE_NOTIFY = "http://localhost:31337/notify"; diff --git a/LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts b/LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts index 76606bbae..a8cb2d412 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ComputeGap.ts @@ -22,9 +22,9 @@ import { readFileSync, existsSync, appendFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const IDEAL_DIR = join(LIFEOS_DIR, "USER", "TELOS", "IDEAL_STATE"); const CURRENT_DIR = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE"); const HEALTH_DIR = join(LIFEOS_DIR, "USER", "HEALTH"); diff --git a/LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts b/LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts index 3357f6226..608086c7d 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ContextAudit.ts @@ -11,9 +11,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { basename, dirname, join } from "path"; import { CONTEXT_FRESHNESS_REGISTRY, parseFrontmatter, type ContextFile } from "./TelosFreshness"; import { currentModel } from "./models"; +import { getClaudeDir, getLifeosDir } from "./Paths"; const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const CLAUDE_DIR = dirname(LIFEOS_DIR); const AUDIT_PATH = join( LIFEOS_DIR, @@ -192,8 +193,8 @@ function normalizeReference(raw: string): string | null { if (/[*?[\]{}]/.test(value)) return null; if (value.startsWith("LIFEOS/")) return join(CLAUDE_DIR, value); - if (value.startsWith("~/.claude/LIFEOS/")) return join(HOME, value.slice(2)); - if (value.startsWith(`${HOME}/.claude/LIFEOS/`)) return value; + if (value.startsWith("~/.claude/LIFEOS/")) return join(getLifeosDir(), value.slice("~/.claude/LIFEOS/".length)); + if (value.startsWith(`${getLifeosDir()}/`)) return value; return null; } diff --git a/LifeOS/install/LIFEOS/TOOLS/CostTracker.ts b/LifeOS/install/LIFEOS/TOOLS/CostTracker.ts index f481f13ea..08259197b 100755 --- a/LifeOS/install/LIFEOS/TOOLS/CostTracker.ts +++ b/LifeOS/install/LIFEOS/TOOLS/CostTracker.ts @@ -31,9 +31,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "fs"; import { join } from "path"; import { execSync } from "child_process"; +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME ?? ""; -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS"); const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY"); const LEDGER_PATH = join(OBS_DIR, "anthropic-cost.jsonl"); const CALL_SITES_PATH = join(OBS_DIR, "anthropic-call-sites.json"); @@ -129,11 +130,11 @@ async function fetchApiSpend(): Promise<{ month_used_usd: number | null; source: // Paths we scan (source-of-truth for LifeOS-local billing risk) const SCAN_ROOTS = [ - join(HOME, ".claude", "LIFEOS", "PULSE"), - join(HOME, ".claude", "LIFEOS", "TOOLS"), - join(HOME, ".claude", "LIFEOS", "USER"), - join(HOME, ".claude", "skills"), - join(HOME, ".claude", "hooks"), + join(getClaudeDir(), "LIFEOS", "PULSE"), + join(getClaudeDir(), "LIFEOS", "TOOLS"), + join(getClaudeDir(), "LIFEOS", "USER"), + join(getClaudeDir(), "skills"), + join(getClaudeDir(), "hooks"), ]; // Paths to exclude from scan diff --git a/LifeOS/install/LIFEOS/TOOLS/CrossVendorAudit.ts b/LifeOS/install/LIFEOS/TOOLS/CrossVendorAudit.ts index d8c32d19f..a9c955fae 100755 --- a/LifeOS/install/LIFEOS/TOOLS/CrossVendorAudit.ts +++ b/LifeOS/install/LIFEOS/TOOLS/CrossVendorAudit.ts @@ -20,9 +20,10 @@ import { readFile, writeFile, readdir, appendFile, mkdir, stat } from "node:fs/p import { existsSync } from "node:fs"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; +import { getClaudeDir } from "./Paths"; const HOME = homedir(); -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS"); const WORK_DIR = join(LIFEOS_DIR, "MEMORY", "WORK"); const FINDINGS_LOG = join(LIFEOS_DIR, "MEMORY", "VERIFICATION", "cato-findings.jsonl"); const TOOL_ACTIVITY_LOG = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY", "tool-activity.jsonl"); diff --git a/LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts b/LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts index 4402c0144..f1b5c8ba1 100755 --- a/LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts +++ b/LifeOS/install/LIFEOS/TOOLS/DAGrowth.ts @@ -13,9 +13,10 @@ */ import { join } from "path" +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME ?? "~" -const LifeOS = join(HOME, ".claude", "LIFEOS") +const LifeOS = join(getClaudeDir(), "LIFEOS") const REGISTRY_PATH = join(LifeOS, "USER", "DA", "_registry.yaml") // ── Types ── diff --git a/LifeOS/install/LIFEOS/TOOLS/DASchedule.ts b/LifeOS/install/LIFEOS/TOOLS/DASchedule.ts index 6bd0f70c5..19ae06636 100755 --- a/LifeOS/install/LIFEOS/TOOLS/DASchedule.ts +++ b/LifeOS/install/LIFEOS/TOOLS/DASchedule.ts @@ -12,9 +12,10 @@ import { join } from "path" import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "fs" +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME ?? "~" -const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS") +const LIFEOS_DIR = join(getClaudeDir(), "LIFEOS") const TASKS_DIR = join(LIFEOS_DIR, "PULSE", "state", "da") const TASKS_PATH = join(TASKS_DIR, "scheduled-tasks.jsonl") diff --git a/LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts b/LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts index 3f4ee8289..52286389e 100755 --- a/LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts +++ b/LifeOS/install/LIFEOS/TOOLS/DerivedSync.ts @@ -17,6 +17,7 @@ import { writeFileSync, } from "node:fs"; import { join } from "node:path"; +import { getClaudeDir } from "./Paths"; type SpawnReadable = ReadableStream | null; type SpawnProcess = { @@ -75,8 +76,7 @@ type RunSummary = { ts: string; }; -const HOME = process.env.HOME || ""; -const CLAUDE_DIR = join(HOME, ".claude"); +const CLAUDE_DIR = getClaudeDir(); const LIFEOS_DIR = join(CLAUDE_DIR, "LIFEOS"); const USER_DIR = join(LIFEOS_DIR, "USER"); const TOOLS_DIR = join(LIFEOS_DIR, "TOOLS"); diff --git a/LifeOS/install/LIFEOS/TOOLS/DocCheck.ts b/LifeOS/install/LIFEOS/TOOLS/DocCheck.ts index 1524f59b2..5d780b8c4 100644 --- a/LifeOS/install/LIFEOS/TOOLS/DocCheck.ts +++ b/LifeOS/install/LIFEOS/TOOLS/DocCheck.ts @@ -18,9 +18,10 @@ import { readFileSync, statSync, existsSync, readdirSync } from 'fs'; import { join, resolve, dirname, relative } from 'path'; import { execSync } from 'child_process'; +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME || ''; -const CLAUDE_DIR = join(HOME, '.claude'); +const CLAUDE_DIR = getClaudeDir(); const LIFEOS_DIR = join(CLAUDE_DIR, 'LIFEOS'); const HOOKS_DIR = join(CLAUDE_DIR, 'hooks'); diff --git a/LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts b/LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts index 70f5bacd4..8b6920d79 100644 --- a/LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts +++ b/LifeOS/install/LIFEOS/TOOLS/FailureCapture.ts @@ -28,8 +28,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs'; import { join, basename } from 'path'; import { inference } from './Inference'; +import { getClaudeDir } from "./Paths"; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude'); +const LIFEOS_DIR = process.env.LIFEOS_DIR || getClaudeDir(); interface FailureCaptureInput { transcriptPath: string; diff --git a/LifeOS/install/LIFEOS/TOOLS/FeatureRegistry.ts b/LifeOS/install/LIFEOS/TOOLS/FeatureRegistry.ts index 982af613d..bd8fa8139 100755 --- a/LifeOS/install/LIFEOS/TOOLS/FeatureRegistry.ts +++ b/LifeOS/install/LIFEOS/TOOLS/FeatureRegistry.ts @@ -20,6 +20,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; +import { getClaudeDir } from "./Paths"; interface TestStep { step: string; @@ -55,7 +56,7 @@ interface FeatureRegistry { }; } -const REGISTRY_DIR = join(process.env.HOME || '', '.claude', 'LIFEOS', 'MEMORY', 'STATE', 'progress'); +const REGISTRY_DIR = join(getClaudeDir(), 'LIFEOS', 'MEMORY', 'STATE', 'progress'); function getRegistryPath(project: string): string { return join(REGISTRY_DIR, `${project}-features.json`); diff --git a/LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts b/LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts index 920231729..4bd8418a5 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ForgeProgress.ts @@ -5,6 +5,7 @@ import { accessSync, constants, createWriteStream, existsSync, type WriteStream import { mkdir, readFile } from "node:fs/promises"; import { join } from "node:path"; import process from "node:process"; +import { getClaudeDir } from "./Paths"; type Args = { slug: string; prompt?: string; model: string; effort: string; sandbox: string; timeoutMs: number; pulseUrl: string }; type JsonRecord = Record; @@ -67,7 +68,7 @@ function preflightCodex(home: string): string | null { catch (_error: unknown) { return null; } // Safe: caller emits the exact unavailable JSON. } async function ensureSlugDir(home: string, slug: string): Promise { - const slugDir = join(home, ".claude", "LIFEOS", "MEMORY", "WORK", slug); + const slugDir = join(getClaudeDir(), "LIFEOS", "MEMORY", "WORK", slug); await mkdir(slugDir, { recursive: true }); // Local artifact I/O is unbounded so errors can surface naturally. return { eventsFile: join(slugDir, "forge-events.jsonl"), finalFile: join(slugDir, "forge-final.txt") }; } diff --git a/LifeOS/install/LIFEOS/TOOLS/GetCounts.ts b/LifeOS/install/LIFEOS/TOOLS/GetCounts.ts index 61c0e7d3f..ce17d239c 100755 --- a/LifeOS/install/LIFEOS/TOOLS/GetCounts.ts +++ b/LifeOS/install/LIFEOS/TOOLS/GetCounts.ts @@ -35,9 +35,9 @@ import { readdirSync, existsSync, statSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME!; -const CLAUDE_DIR = join(HOME, ".claude"); +const CLAUDE_DIR = getClaudeDir(); // skills/, hooks/, settings.json live under CLAUDE_DIR. // MEMORY/, USER/ live under LIFEOS_DIR (which is CLAUDE_DIR/PAI). const LIFEOS_DIR = process.env.LIFEOS_DIR || join(CLAUDE_DIR, "LIFEOS"); @@ -129,7 +129,7 @@ function countSkills(): number { * count — only what Claude Code will actually fire. */ function countHooks(): number { - const settingsPath = join(HOME, ".claude", "settings.json"); + const settingsPath = join(getClaudeDir(), "settings.json"); try { const fs = require('fs'); const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); diff --git a/LifeOS/install/LIFEOS/TOOLS/Grok.ts b/LifeOS/install/LIFEOS/TOOLS/Grok.ts index 2607d5728..f0028c348 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Grok.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Grok.ts @@ -40,6 +40,7 @@ import { readFileSync } from 'fs' import { homedir } from 'os' import { join } from 'path' +import { displayPath, getClaudeDir } from "./Paths"; const colors = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', @@ -50,7 +51,7 @@ const colors = { function loadEnv(): Record { const envPath = process.env.LIFEOS_CONFIG_DIR ? join(process.env.LIFEOS_CONFIG_DIR, '.env') - : join(homedir(), '.claude', '.env') + : join(getClaudeDir(), '.env') const env: Record = {} try { const content = readFileSync(envPath, 'utf-8') @@ -122,12 +123,12 @@ async function main() { const { opts, query } = parseArgs(process.argv.slice(2)) if (!API_KEY) { - console.error(`${colors.red}Error: GROK_API_KEY (or XAI_API_KEY) not set in ~/.claude/.env${colors.reset}`) + console.error(`${colors.red}Error: GROK_API_KEY (or XAI_API_KEY) not set in ${displayPath(getClaudeDir())}/.env${colors.reset}`) process.exit(1) } if (!query) { console.error(`${colors.red}Error: no query provided${colors.reset}`) - console.error(`Usage: bun ~/.claude/LIFEOS/TOOLS/Grok.ts [--x-only|--web-only] [--model ] [--json] ""`) + console.error(`Usage: bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/Grok.ts [--x-only|--web-only] [--model ] [--json] ""`) process.exit(1) } diff --git a/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts b/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts index ec663cc19..8427c1a34 100755 --- a/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts +++ b/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts @@ -12,9 +12,10 @@ import { parseArgs } from "node:util"; import * as fs from "fs"; import * as path from "path"; import { inference } from "./Inference"; +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME!; -const LIFEOS_DIR = path.join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = path.join(getClaudeDir(), "LIFEOS"); const MEMORY_DIR = path.join(LIFEOS_DIR, "MEMORY"); const KNOWLEDGE_DIR = path.join(MEMORY_DIR, "KNOWLEDGE"); const LEARNING_DIR = path.join(MEMORY_DIR, "LEARNING"); diff --git a/LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts b/LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts index e3c2b79fa..f6c62e273 100644 --- a/LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts +++ b/LifeOS/install/LIFEOS/TOOLS/HealthSnapshot.ts @@ -4,6 +4,7 @@ import { existsSync } from "node:fs" import { homedir } from "node:os" import { join } from "node:path" import { loadLifeosConfig } from "./LifeosConfig" +import { getLifeosDir } from "./Paths"; const INBOX = join(homedir(), "Library/Mobile Documents/com~apple~CloudDocs/PAI/health/inbox") const PROCESSED = join(homedir(), "Library/Mobile Documents/com~apple~CloudDocs/PAI/health/processed") @@ -11,7 +12,7 @@ const PROCESSED = join(homedir(), "Library/Mobile Documents/com~apple~CloudDocs/ // a relocated LIFEOS_USER_DIR Just Works. const SNAPSHOTS = ((): string => { try { return join(loadLifeosConfig().paths.userDir, "TELOS/HEALTH/snapshots") } - catch { return join(homedir(), ".claude/LIFEOS/USER/TELOS/HEALTH/snapshots") } + catch { return join(getLifeosDir(), "USER/TELOS/HEALTH/snapshots") } })() type HealthSnapshot = { diff --git a/LifeOS/install/LIFEOS/TOOLS/HealthSync.ts b/LifeOS/install/LIFEOS/TOOLS/HealthSync.ts index bfe00876c..d7fa2dc1f 100755 --- a/LifeOS/install/LIFEOS/TOOLS/HealthSync.ts +++ b/LifeOS/install/LIFEOS/TOOLS/HealthSync.ts @@ -9,6 +9,7 @@ * bun LIFEOS/TOOLS/HealthSync.ts current * bun LIFEOS/TOOLS/HealthSync.ts auth oura */ +import { getClaudeDir, getLifeosDir } from "./Paths"; import { join } from "node:path"; import type { Ctx, @@ -42,11 +43,9 @@ type CliCommand = "pull" | "status" | "current" | "auth"; const HOME = process.env.HOME || ""; const PREFIX = "[HealthSync]"; const SOURCE_NAMES: readonly SourceName[] = ["oura", "eightsleep", "apple", "function"]; -const CURRENT_PATH = join(HOME, ".claude", "LIFEOS", "USER", "HEALTH", "current.json"); +const CURRENT_PATH = join(getClaudeDir(), "LIFEOS", "USER", "HEALTH", "current.json"); const HEALTHSYNC_LOG_PATH = join( - HOME, - ".claude", - "LIFEOS", + getLifeosDir(), "MEMORY", "OBSERVABILITY", "healthsync.jsonl", diff --git a/LifeOS/install/LIFEOS/TOOLS/Inference.ts b/LifeOS/install/LIFEOS/TOOLS/Inference.ts index f13b5cea0..821863c38 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Inference.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Inference.ts @@ -113,6 +113,7 @@ export interface InferenceResult { } import { modelForEffort, pinnedModelForEffort, EFFORT_MODEL, LEVEL_TO_HARNESS_EFFORT, type EffortLevel, type HarnessEffort } from './models'; +import { getClaudeDir } from "./Paths"; // Level configurations — models resolve via models.ts EFFORT_MODEL (the single // edit point on a lineup change). No model names appear here. `effort` is the @@ -419,8 +420,8 @@ export async function synthesizeAdvisorState(): Promise { const fs = await import("fs/promises"); const path = await import("path"); const home = process.env.HOME || process.env.USERPROFILE || ""; - const workDir = path.join(home, ".claude", "LIFEOS", "MEMORY", "WORK"); - const stateFile = path.join(home, ".claude", "LIFEOS", "MEMORY", "STATE", "work.json"); + const workDir = path.join(getClaudeDir(), "LIFEOS", "MEMORY", "WORK"); + const stateFile = path.join(getClaudeDir(), "LIFEOS", "MEMORY", "STATE", "work.json"); // Try to read active session from work.json let activeSlug: string | undefined; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallBlogDiscovery.ts b/LifeOS/install/LIFEOS/TOOLS/InstallBlogDiscovery.ts index bc0732cf9..08a5b6613 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallBlogDiscovery.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallBlogDiscovery.ts @@ -13,11 +13,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.blogdiscovery.plist.template"); +const TEMPLATE_PATH = join(getClaudeDir(), "LIFEOS", "TOOLS", "com.lifeos.blogdiscovery.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.blogdiscovery.plist"); const LABEL = "com.lifeos.blogdiscovery"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallBookmarkSweep.ts b/LifeOS/install/LIFEOS/TOOLS/InstallBookmarkSweep.ts index d6dd3f20e..cabe0fe9d 100644 --- a/LifeOS/install/LIFEOS/TOOLS/InstallBookmarkSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallBookmarkSweep.ts @@ -15,11 +15,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.bookmarksweep.plist.template"); +const TEMPLATE_PATH = join(getClaudeDir(), "LIFEOS", "TOOLS", "com.lifeos.bookmarksweep.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.bookmarksweep.plist"); const LABEL = "com.lifeos.bookmarksweep"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallCodexUpdate.ts b/LifeOS/install/LIFEOS/TOOLS/InstallCodexUpdate.ts index b5d9e4d6e..566c40dba 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallCodexUpdate.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallCodexUpdate.ts @@ -14,11 +14,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.codexupdate.plist.template"); +const TEMPLATE_PATH = join(getClaudeDir(), "LIFEOS", "TOOLS", "com.lifeos.codexupdate.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.codexupdate.plist"); const LABEL = "com.lifeos.codexupdate"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallCommitmentSweep.ts b/LifeOS/install/LIFEOS/TOOLS/InstallCommitmentSweep.ts index 66a55f3b6..f349994ce 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallCommitmentSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallCommitmentSweep.ts @@ -12,12 +12,13 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; import { spawnSync } from "child_process"; +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME || ""; -const TEMPLATE = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.commitmentsweep.plist.template"); +const TEMPLATE = join(getClaudeDir(), "LIFEOS", "TOOLS", "com.lifeos.commitmentsweep.plist.template"); const TARGET_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET = join(TARGET_DIR, "com.lifeos.commitmentsweep.plist"); -const STATE_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "STATE"); +const STATE_DIR = join(getClaudeDir(), "LIFEOS", "MEMORY", "STATE"); const LABEL = "com.lifeos.commitmentsweep"; function uid(): string { diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallDerivedSync.ts b/LifeOS/install/LIFEOS/TOOLS/InstallDerivedSync.ts index fca3b87ea..52ad106bd 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallDerivedSync.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallDerivedSync.ts @@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, realpathSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; type SpawnProcess = { stdout: ReadableStream | null; @@ -34,7 +35,7 @@ type LaunchctlResult = { }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.derivedsync.plist.template"); +const TEMPLATE_PATH = join(getClaudeDir(), "LIFEOS", "TOOLS", "com.lifeos.derivedsync.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.derivedsync.plist"); const LABEL = "com.lifeos.derivedsync"; @@ -87,7 +88,7 @@ async function install(): Promise { } const bunPath = await detectBun(); const bunDir = bunPath.replace(/\/bun$/, ""); - const userDir = realpathSync(join(HOME, ".claude", "LIFEOS", "USER")); + const userDir = realpathSync(join(getClaudeDir(), "LIFEOS", "USER")); console.log(`[InstallDerivedSync] detected bun at ${bunPath}`); const template = readFileSync(TEMPLATE_PATH, "utf-8"); const materialized = template diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallHealthSync.ts b/LifeOS/install/LIFEOS/TOOLS/InstallHealthSync.ts index 4a8b70dd9..4627b7755 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InstallHealthSync.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallHealthSync.ts @@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; type SpawnProcess = { exited: Promise; @@ -30,7 +31,7 @@ type LaunchctlResult = { }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.healthsync.plist.template"); +const TEMPLATE_PATH = join(getClaudeDir(), "LIFEOS", "TOOLS", "com.lifeos.healthsync.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.healthsync.plist"); const LABEL = "com.lifeos.healthsync"; diff --git a/LifeOS/install/LIFEOS/TOOLS/InstallWorkSweep.ts b/LifeOS/install/LIFEOS/TOOLS/InstallWorkSweep.ts index aff4ff215..955aae4c8 100644 --- a/LifeOS/install/LIFEOS/TOOLS/InstallWorkSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InstallWorkSweep.ts @@ -15,11 +15,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const TEMPLATE_PATH = join(HOME, ".claude", "LIFEOS", "TOOLS", "com.lifeos.worksweep.plist.template"); +const TEMPLATE_PATH = join(getClaudeDir(), "LIFEOS", "TOOLS", "com.lifeos.worksweep.plist.template"); const LAUNCH_AGENTS_DIR = join(HOME, "Library", "LaunchAgents"); const TARGET_PLIST = join(LAUNCH_AGENTS_DIR, "com.lifeos.worksweep.plist"); const LABEL = "com.lifeos.worksweep"; diff --git a/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts b/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts index 1f792e920..d183ab192 100755 --- a/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts +++ b/LifeOS/install/LIFEOS/TOOLS/IntegrityMaintenance.ts @@ -23,6 +23,7 @@ import { readFileSync, existsSync } from 'fs'; import { join, basename, dirname } from 'path'; import { inference } from './Inference'; import { getIdentity } from '../../hooks/lib/identity'; +import { displayPath, getClaudeDir, getLifeosDir } from "./Paths"; // ============================================================================ // Types @@ -108,7 +109,7 @@ interface UpdateData { // Constants // ============================================================================ -const LIFEOS_DIR = process.env.HOME + '/.claude/LIFEOS'; +const LIFEOS_DIR = getLifeosDir(); const CREATE_UPDATE_SCRIPT = join(LIFEOS_DIR, 'skills/_LIFEOS/Tools/CreateUpdate.ts'); // Words that indicate generic/bad titles - reject these @@ -719,7 +720,7 @@ async function generateVerboseNarrative( future_impact: aiNarrative.future_impact, future_bullets: aiNarrative.future_bullets, verification_steps: aiNarrative.verification_steps, - verification_commands: [`bun ~/.claude/skills/_LIFEOS/Tools/UpdateSearch.ts recent 5`], + verification_commands: [`bun ${displayPath(getClaudeDir())}/skills/_LIFEOS/Tools/UpdateSearch.ts recent 5`], confidence: 'high', }, aiTitle: aiNarrative.title, @@ -749,7 +750,7 @@ async function generateVerboseNarrative( future_impact: `The ${changeType.replace('_', ' ')} will use updated behavior.`, future_bullets: ['Changes are active for future sessions'], verification_steps: ['Changes applied via automatic detection'], - verification_commands: [`bun ~/.claude/skills/_LIFEOS/Tools/UpdateSearch.ts recent 5`], + verification_commands: [`bun ${displayPath(getClaudeDir())}/skills/_LIFEOS/Tools/UpdateSearch.ts recent 5`], confidence: 'medium', }, }; diff --git a/LifeOS/install/LIFEOS/TOOLS/InterviewIdealState.ts b/LifeOS/install/LIFEOS/TOOLS/InterviewIdealState.ts index bd7318559..16d2cbd36 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InterviewIdealState.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InterviewIdealState.ts @@ -17,9 +17,9 @@ import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const TELOS_DIR = join(LIFEOS_DIR, "USER", "TELOS"); const IDEAL_DIR = join(TELOS_DIR, "IDEAL_STATE"); const STATE_FILE = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE", "interview-state.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts b/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts index 956aecc4b..c49fa2637 100755 --- a/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts +++ b/LifeOS/install/LIFEOS/TOOLS/InterviewScan.ts @@ -19,9 +19,9 @@ import { readFileSync, existsSync } from "fs"; import { join } from "path"; import { readTelosFreshness, sectionSlug, type SectionFreshness } from "./TelosFreshness"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const USER_DIR = join(LIFEOS_DIR, "USER"); const TELOS_DIR = join(USER_DIR, "TELOS"); const TELOS_PATH = join(TELOS_DIR, "TELOS.md"); @@ -84,7 +84,7 @@ const REGISTRY: RegistryTarget[] = [ prompts: ["Main DA voice — pick from ElevenLabs library, or stick with default Rachel (21m00Tcm4TlvDq8ikWAM)?", "Algorithm voice (used for phase transitions) — default Adam (pNInz6obpgDQGcFmaJgB) is fine?", "Want voice notifications on by default? (default: yes)"] }, - { phase: 0, path: join(HOME, ".claude", ".env"), name: ".env/credentials", category: "setup", leverage: 10, + { phase: 0, path: join(getClaudeDir(), ".env"), name: ".env/credentials", category: "setup", leverage: 10, prompts: ["ANTHROPIC_API_KEY — required for inference. Paste here (will write to .env, won't echo back)?", "ELEVENLABS_API_KEY — required for voice notifications. Skip if you don't want voice.", "GH_TOKEN — optional, only if you want the work pipeline. Skip if not using GitHub issues.", diff --git a/LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts b/LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts index e4f317781..a36a044f7 100755 --- a/LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts +++ b/LifeOS/install/LIFEOS/TOOLS/KnowledgeGraph.ts @@ -26,13 +26,13 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; +import { getClaudeDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ -const HOME = process.env.HOME!; -const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(getClaudeDir(), "LIFEOS"); const KNOWLEDGE_DIR = path.join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"); const DOMAINS = ["People", "Companies", "Ideas", "Research"]; const SKIP_FILES = new Set(["_index.md", "_schema.md", "_log.md"]); diff --git a/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts b/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts index 9bed8d34a..55596c400 100755 --- a/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts +++ b/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts @@ -21,13 +21,14 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; +import { getClaudeDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ const HOME = process.env.HOME!; -const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(getClaudeDir(), "LIFEOS"); const MEMORY_DIR = path.join(LIFEOS_DIR, "MEMORY"); const KNOWLEDGE_DIR = path.join(MEMORY_DIR, "KNOWLEDGE"); const WORK_DIR = path.join(MEMORY_DIR, "WORK"); @@ -41,8 +42,8 @@ if (!CURRENT_USER) { console.error("KnowledgeHarvester: USER env var is required to locate auto-memory dir"); process.exit(1); } -const AUTO_MEMORY_DIR = path.join(HOME, ".claude", "projects", - `-Users-${CURRENT_USER}--claude`, "memory"); +const AUTO_MEMORY_DIR = path.join(getClaudeDir(), "projects", + getClaudeDir().replace(/[/.]/g, "-"), "memory"); const HARVEST_STATE_FILE = path.join(KNOWLEDGE_DIR, ".harvest-state.json"); const REFLECTIONS_FILE = path.join(LEARNING_DIR, "REFLECTIONS", "algorithm-reflections.jsonl"); diff --git a/LifeOS/install/LIFEOS/TOOLS/LearningPatternSynthesis.ts b/LifeOS/install/LIFEOS/TOOLS/LearningPatternSynthesis.ts index 9aa5b24e7..a433e642c 100755 --- a/LifeOS/install/LIFEOS/TOOLS/LearningPatternSynthesis.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LearningPatternSynthesis.ts @@ -26,12 +26,13 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; import * as crypto from "crypto"; +import { getClaudeDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ -const CLAUDE_DIR = path.join(process.env.HOME!, ".claude"); +const CLAUDE_DIR = getClaudeDir(); const LIFEOS_DIR = path.join(CLAUDE_DIR, "LIFEOS"); const MEMORY_DIR = path.join(LIFEOS_DIR, "MEMORY"); const LEARNING_DIR = path.join(MEMORY_DIR, "LEARNING"); diff --git a/LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts b/LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts index 5007662fa..dd3374100 100755 --- a/LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LifeosUpgrade.ts @@ -19,10 +19,9 @@ import { existsSync, readFileSync, lstatSync, readlinkSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME ?? homedir(); -const CLAUDE_ROOT = join(HOME, ".claude"); +const CLAUDE_ROOT = getClaudeDir(); interface MigrationContext { claudeRoot: string; diff --git a/LifeOS/install/LIFEOS/TOOLS/LoadSkillConfig.ts b/LifeOS/install/LIFEOS/TOOLS/LoadSkillConfig.ts index 40d6fe0af..a13b1cc41 100755 --- a/LifeOS/install/LIFEOS/TOOLS/LoadSkillConfig.ts +++ b/LifeOS/install/LIFEOS/TOOLS/LoadSkillConfig.ts @@ -16,8 +16,8 @@ import { readFileSync, existsSync, readdirSync } from 'fs'; import { join, basename } from 'path'; -import { homedir } from 'os'; import { parse as parseYaml } from 'yaml'; +import { displayPath, getClaudeDir } from "./Paths"; // Types interface CustomizationMetadata { @@ -34,8 +34,7 @@ interface ExtendManifest { } // Constants -const HOME = homedir(); -const CUSTOMIZATION_DIR = join(HOME, '.claude', 'LIFEOS', 'USER', 'SKILLCUSTOMIZATIONS'); +const CUSTOMIZATION_DIR = join(getClaudeDir(), 'LIFEOS', 'USER', 'SKILLCUSTOMIZATIONS'); /** * Deep merge two objects recursively @@ -250,7 +249,7 @@ Usage: bun LoadSkillConfig.ts --check Check if skill has customizations Examples: - bun LoadSkillConfig.ts ~/.claude/skills/Upgrade sources.json + bun LoadSkillConfig.ts ${displayPath(getClaudeDir())}/skills/Upgrade sources.json bun LoadSkillConfig.ts --list bun LoadSkillConfig.ts --check Upgrade `); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts index 06c7385f6..1b6c663d1 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryGraph.ts @@ -27,9 +27,9 @@ import louvain from "graphology-communities-louvain"; import pagerank from "graphology-metrics/centrality/pagerank"; import * as fs from "fs"; import * as path from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME!; -const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(getClaudeDir(), "LIFEOS"); const MEMORY = path.join(LIFEOS_DIR, "MEMORY"); const KNOWLEDGE_DIR = path.join(MEMORY, "KNOWLEDGE"); const WORK_DIR = path.join(MEMORY, "WORK"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts index 8f6afa16c..4507d23d9 100644 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryInsights.ts @@ -21,9 +21,9 @@ import { existsSync, readFileSync, statSync } from "node:fs"; import { resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from "./Paths"; -const ROOT = pathResolve(homedir(), ".claude"); +const ROOT = getClaudeDir(); const OBS = pathResolve(ROOT, "LIFEOS/MEMORY/OBSERVABILITY"); const PRINCIPAL_MEM = pathResolve(ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"); const DA_MEM = pathResolve(ROOT, "LIFEOS/USER/DIGITAL_ASSISTANT/DA_MEMORY.md"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts index 8d7cbdbc9..ee5114236 100644 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryRestore.ts @@ -14,9 +14,9 @@ import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; import { resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from "./Paths"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const SNAPSHOT_DIR = pathResolve(CLAUDE_ROOT, "LIFEOS/MEMORY/OBSERVABILITY/memory-snapshots"); const TARGETS: Record = { principal: pathResolve(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"), diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryRetriever.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryRetriever.ts index 7e9ba2ef3..d6138af66 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryRetriever.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryRetriever.ts @@ -35,13 +35,14 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; import { spawnSync } from "child_process"; +import { getClaudeDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ const HOME = process.env.HOME!; -const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || path.join(getClaudeDir(), "LIFEOS"); const KNOWLEDGE_DIR = path.join(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"); const DOMAINS = ["People", "Companies", "Ideas", "Research"]; @@ -392,8 +393,8 @@ function formatResults( // dual-tier prefetch, no graph traversal on hot path). const MEMORY_FILES: ReadonlyArray<{ path: string; title: string }> = [ - { path: path.join(HOME, ".claude", "LIFEOS", "USER", "PRINCIPAL", "PRINCIPAL_MEMORY.md"), title: "Principal Memory" }, - { path: path.join(HOME, ".claude", "LIFEOS", "USER", "DIGITAL_ASSISTANT", "DA_MEMORY.md"), title: "DA Memory" }, + { path: path.join(getClaudeDir(), "LIFEOS", "USER", "PRINCIPAL", "PRINCIPAL_MEMORY.md"), title: "Principal Memory" }, + { path: path.join(getClaudeDir(), "LIFEOS", "USER", "DIGITAL_ASSISTANT", "DA_MEMORY.md"), title: "DA Memory" }, ]; const RELEVANT_CACHE_TTL_MS = 60_000; @@ -567,7 +568,7 @@ function formatRelevantBlock(results: RelevantResultItem[]): string { if (results.length === 0) return ""; const lines: string[] = ["## RELEVANT MEMORY"]; for (const r of results) { - const shortPath = r.path.replace(HOME + "/.claude/", ""); + const shortPath = r.path.replace(getClaudeDir() + "/", ""); lines.push(""); lines.push(`### [${r.type} · ${r.score.toFixed(1)}] ${r.title}`); lines.push(``); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts index ea4b90483..a42b06150 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryReviewer.ts @@ -50,7 +50,7 @@ import { markProposal, logProposalEvent, } from "../PULSE/lib/telegram-proposals"; -import { getClaudeDir } from "./Paths"; +import { displayPath, getClaudeDir } from "./Paths"; // ── Constants ── @@ -578,7 +578,7 @@ export async function review(opts: ReviewOptions = {}): Promise { ...summary.failures.map((f) => ` FAIL [${f.index}] ${f.type}: ${f.error}`), "", "Per-item results:", - ...results.map((r, i) => `[${i}] ${r.ok ? "OK " + (r as any).type : "FAIL " + (r as any).code}: ${r.ok ? (r as any).path?.replace(CLAUDE_ROOT, "~/.claude") : (r as any).message}`), + ...results.map((r, i) => `[${i}] ${r.ok ? "OK " + (r as any).type : "FAIL " + (r as any).code}: ${r.ok ? (r as any).path?.replace(CLAUDE_ROOT, displayPath(CLAUDE_ROOT)) : (r as any).message}`), ].join("\n"), }); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts index f6029b5b1..c99ae1e61 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryStatus.ts @@ -24,15 +24,15 @@ import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join as pathJoin, resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; import { PRINCIPAL_MEMORY_PATH, DA_MEMORY_PATH, PENDING_PROPOSALS_PATH, } from "./MemoryTypes"; import { read as readMemory } from "./MemoryWriter"; +import { getClaudeDir } from "./Paths"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const LIFEOS_DIR = pathJoin(CLAUDE_ROOT, "LIFEOS"); const IDEAS_DIR = pathJoin(LIFEOS_DIR, "MEMORY", "IDEAS"); const KNOWLEDGE_DIR = pathJoin(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts b/LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts index 7365045f5..b5a43ddc1 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemorySystem.ts @@ -69,10 +69,11 @@ import { import { setEntries as memoryWriterSetEntries, read as memoryWriterRead } from "./MemoryWriter"; import { getTier } from "./MutationTier"; import { getRelevantContext, type RelevantResultItem } from "./MemoryRetriever"; +import { getClaudeDir, getLifeosDir } from "./Paths"; // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); // ── Result types ── @@ -679,7 +680,7 @@ async function smokeTest(): Promise { // 5. ISC-156 — proposal enqueues const r5 = add({ type: "proposal", - target_file: pathJoin(homedir(), ".claude/LIFEOS/USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), + target_file: pathJoin(getLifeosDir(), "USER/PRINCIPAL/PRINCIPAL_IDENTITY.md"), edit: "RULE: This is a smoke-test proposal — DO NOT APPLY.", confidence: 0.42, rationale: "smoke test", diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts index 1657fa899..9e251f7db 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts @@ -29,11 +29,11 @@ */ import { resolve as pathResolve, join as pathJoin } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from "./Paths"; // ── Paths ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const LIFEOS_DIR = pathJoin(CLAUDE_ROOT, "LIFEOS"); const KNOWLEDGE_DIR = pathJoin(LIFEOS_DIR, "MEMORY", "KNOWLEDGE"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts b/LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts index f33f5424f..3b4ee28da 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MemoryWriter.ts @@ -50,11 +50,11 @@ import { writeFileSync, } from "node:fs"; import { dirname, resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { getClaudeDir } from "./Paths"; // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const ALLOWED_FILES = new Set([ pathResolve(CLAUDE_ROOT, "LIFEOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md"), diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts index f49c84783..7e6daade3 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateApprove.ts @@ -24,9 +24,9 @@ import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const QUEUE_FILE = join(LIFEOS_DIR, "MEMORY", "MIGRATION", "migration-proposals.jsonl"); const COMMITTED_LOG = join(LIFEOS_DIR, "MEMORY", "MIGRATION", "committed.jsonl"); @@ -71,7 +71,7 @@ function resolveTargetPath(target: string): string { } if (target === "memory/feedback") { // Feedback memories live outside LifeOS dir in projects/${HARNESS_USER_DIR}/memory/ - return join(HOME, ".claude", "projects", "${HARNESS_USER_DIR}", "memory"); + return join(getClaudeDir(), "projects", "${HARNESS_USER_DIR}", "memory"); } return join(LIFEOS_DIR, target); } diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateContextFreshness.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateContextFreshness.ts index 705bf5ecb..3c6dbf637 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateContextFreshness.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateContextFreshness.ts @@ -7,6 +7,7 @@ * bun ~/.claude/LIFEOS/TOOLS/MigrateContextFreshness.ts * bun ~/.claude/LIFEOS/TOOLS/MigrateContextFreshness.ts --dry-run */ +import { getClaudeDir } from "./Paths"; import { createHash } from "crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; @@ -18,8 +19,7 @@ import { type ContextFile, } from "./TelosFreshness"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const CLAUDE_DIR = dirname(LIFEOS_DIR); const SEED_ISO = "2026-05-03T23:00:00-07:00"; const BACKUP_TS = "2026-05-03-23-00-00"; diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts index 8a34eb942..7178f3eb0 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateScan.ts @@ -22,9 +22,9 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync, appendFileSync } from "fs"; import { join, basename, dirname, extname } from "path"; import { randomUUID } from "crypto"; +import { displayPath, getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const QUEUE_FILE = join(LIFEOS_DIR, "MEMORY", "MIGRATION", "migration-proposals.jsonl"); type Target = @@ -301,7 +301,7 @@ function main(): void { console.log(`⚠️ ${lowConf.length} chunks classified at <40% confidence — review recommended.`); } console.log(``); - console.log(`Next: bun ~/.claude/LIFEOS/TOOLS/MigrateApprove.ts --review`); + console.log(`Next: bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/MigrateApprove.ts --review`); } main(); diff --git a/LifeOS/install/LIFEOS/TOOLS/MigrateTelosFreshness.ts b/LifeOS/install/LIFEOS/TOOLS/MigrateTelosFreshness.ts index 2e0dca3cd..735cb7cd9 100755 --- a/LifeOS/install/LIFEOS/TOOLS/MigrateTelosFreshness.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MigrateTelosFreshness.ts @@ -22,9 +22,10 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "fs"; import { createHash } from "crypto"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const TELOS_PATH = join(LIFEOS_DIR, "USER", "TELOS", "TELOS.md"); const BACKUP_DIR = join(LIFEOS_DIR, "USER", "TELOS", "Backups"); diff --git a/LifeOS/install/LIFEOS/TOOLS/MutationTier.ts b/LifeOS/install/LIFEOS/TOOLS/MutationTier.ts index 21ef51af4..134ee33ac 100644 --- a/LifeOS/install/LIFEOS/TOOLS/MutationTier.ts +++ b/LifeOS/install/LIFEOS/TOOLS/MutationTier.ts @@ -34,11 +34,11 @@ */ import { resolve as pathResolve } from "node:path"; -import { homedir } from "node:os"; +import { displayPath, getClaudeDir } from "./Paths"; // ── Constants ── -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); export type Tier = "A" | "B" | "C" | "D"; @@ -253,7 +253,7 @@ function smokeTest(): number { const ok = got === c.expected; if (ok) { pass++; - console.log(` ✓ ${c.expected} ${c.path.replace(CLAUDE_ROOT, "~/.claude")} — ${c.why}`); + console.log(` ✓ ${c.expected} ${displayPath(c.path)} — ${c.why}`); } else { fail++; console.error(` ✗ expected ${c.expected}, got ${got} ${c.path} — ${c.why}`); diff --git a/LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.ts b/LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.ts index 3b77a998a..4358aa04f 100755 --- a/LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.ts +++ b/LifeOS/install/LIFEOS/TOOLS/NeofetchBanner.ts @@ -19,9 +19,9 @@ import { readdirSync, existsSync, readFileSync } from "fs"; import { join } from "path"; import { spawnSync } from "child_process"; import { paiUserDir } from "./LifeosConfig"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME!; -const CLAUDE_DIR = join(HOME, ".claude"); +const CLAUDE_DIR = getClaudeDir(); // ═══════════════════════════════════════════════════════════════════════ // Terminal Width Detection diff --git a/LifeOS/install/LIFEOS/TOOLS/OpenRouter.ts b/LifeOS/install/LIFEOS/TOOLS/OpenRouter.ts index 72de0725a..d7e450857 100755 --- a/LifeOS/install/LIFEOS/TOOLS/OpenRouter.ts +++ b/LifeOS/install/LIFEOS/TOOLS/OpenRouter.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun +import { displayPath, getClaudeDir } from "./Paths"; /** * ============================================================================ * OPENROUTER — frontier open-model access via the OpenRouter broker @@ -104,7 +105,7 @@ export async function openrouter(options: ORandOptions): Promise { const apiKey = getApiKey(); if (!apiKey) { - return { success: false, output: "", error: "OPENROUTER_API_KEY not set in environment (~/.claude/.env)", latencyMs: 0, model, level }; + return { success: false, output: "", error: `OPENROUTER_API_KEY not set in environment (${displayPath(getClaudeDir())}/.env)`, latencyMs: 0, model, level }; } const controller = new AbortController(); diff --git a/LifeOS/install/LIFEOS/TOOLS/PangramScore.ts b/LifeOS/install/LIFEOS/TOOLS/PangramScore.ts index c29c6d200..37091b29c 100755 --- a/LifeOS/install/LIFEOS/TOOLS/PangramScore.ts +++ b/LifeOS/install/LIFEOS/TOOLS/PangramScore.ts @@ -20,14 +20,15 @@ import { readFileSync, appendFileSync, mkdirSync } from "node:fs"; import { createHash } from "node:crypto"; import { dirname, join } from "node:path"; +import { displayPath, getClaudeDir } from "./Paths"; -const ENV_PATH = `${process.env.HOME}/.claude/.env`; +const ENV_PATH = `${getClaudeDir()}/.env`; // Run-record: proof the detector actually executed on a specific text. The // WritingGate Stop hook reads this so its pass condition is "Pangram ran on // this content", not "a token string is present" (Forge audit 2026-07-01). const RUNS_PATH = join( - process.env.LIFEOS_DIR || `${process.env.HOME}/.claude/LifeOS`, + process.env.LIFEOS_DIR || `${getClaudeDir()}/LifeOS`, "MEMORY", "OBSERVABILITY", "pangram-runs.jsonl", ); export function normalizeForHash(text: string): string { @@ -55,7 +56,7 @@ function loadKey(): string { const line = env.split("\n").find((l) => l.startsWith("PANGRAM_API_KEY=")); if (line) return line.slice("PANGRAM_API_KEY=".length).replace(/^["']|["']$/g, "").trim(); } catch {} - console.error("No PANGRAM_API_KEY found. Add it to ~/.claude/.env, then re-run."); + console.error(`No PANGRAM_API_KEY found. Add it to ${displayPath(getClaudeDir())}/.env, then re-run.`); process.exit(1); } diff --git a/LifeOS/install/LIFEOS/TOOLS/Paths.ts b/LifeOS/install/LIFEOS/TOOLS/Paths.ts index 3150e0633..8b2c9bbc9 100644 --- a/LifeOS/install/LIFEOS/TOOLS/Paths.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Paths.ts @@ -56,3 +56,15 @@ export function getLifeosDir(): string { return join(getClaudeDir(), "LIFEOS"); } + +/** + * Abbreviate the home prefix to `~` in a path shown to the user. Purely + * cosmetic — the absolute path would work as-is. Exists so that messages + * built from resolved path constants render byte-identical to the previous + * hardcoded `~/.claude/...` strings on a default-root install, and follow + * the usual home-relative display convention everywhere else. + */ +export function displayPath(path: string): string { + const home = homedir(); + return path.startsWith(home + "/") ? "~" + path.slice(home.length) : path; +} diff --git a/LifeOS/install/LIFEOS/TOOLS/PiSync.sh b/LifeOS/install/LIFEOS/TOOLS/PiSync.sh index f37c8f178..9f01bf044 100755 --- a/LifeOS/install/LIFEOS/TOOLS/PiSync.sh +++ b/LifeOS/install/LIFEOS/TOOLS/PiSync.sh @@ -6,7 +6,7 @@ set -euo pipefail -LifeOS=~/.claude +LifeOS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" PI=~/.pi/agent [ -d "$PI" ] || { echo "✗ ~/.pi/agent missing"; exit 1; } diff --git a/LifeOS/install/LIFEOS/TOOLS/ProposeCurrentStateEntry.ts b/LifeOS/install/LIFEOS/TOOLS/ProposeCurrentStateEntry.ts index 9fe226ac6..91d52db70 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ProposeCurrentStateEntry.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ProposeCurrentStateEntry.ts @@ -20,9 +20,9 @@ import { appendFileSync, mkdirSync, existsSync } from "fs"; import { join, dirname } from "path"; import { randomUUID } from "crypto"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const QUEUE_FILE = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE", "proposals.jsonl"); const ALLOWED_SOURCES = ["lifelog", "calendar", "gmail", "homebridge", "manual", "amazon", "bills"]; diff --git a/LifeOS/install/LIFEOS/TOOLS/Recommend.ts b/LifeOS/install/LIFEOS/TOOLS/Recommend.ts index e3dbc2b42..2bd99577b 100755 --- a/LifeOS/install/LIFEOS/TOOLS/Recommend.ts +++ b/LifeOS/install/LIFEOS/TOOLS/Recommend.ts @@ -16,9 +16,9 @@ import { readFileSync, existsSync } from "fs"; import { join } from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const TELOS_DIR = join(LIFEOS_DIR, "USER", "TELOS"); const CURRENT_DIR = join(TELOS_DIR, "CURRENT_STATE"); diff --git a/LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts b/LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts index c1f8c262c..e7550a9a9 100755 --- a/LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ReferenceCheck.ts @@ -27,9 +27,10 @@ import { readFileSync, statSync, existsSync, readdirSync, realpathSync } from 'fs'; import { join, resolve, dirname, relative, extname, sep } from 'path'; import { execSync } from 'child_process'; +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME || ''; -const CLAUDE_DIR = join(HOME, '.claude'); +const CLAUDE_DIR = getClaudeDir(); const LIFEOS_DIR = join(CLAUDE_DIR, 'LIFEOS'); // ── Arg parsing (manual, zero deps) ── diff --git a/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts b/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts index f3962c564..d734884be 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SessionHarvester.ts @@ -23,12 +23,13 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; import { getLearningCategory, isLearningCapture } from "../../hooks/lib/learning-utils"; +import { getClaudeDir } from "./Paths"; // ============================================================================ // Configuration // ============================================================================ -const CLAUDE_DIR = path.join(process.env.HOME!, ".claude"); +const CLAUDE_DIR = getClaudeDir(); // Derive the project slug dynamically from CLAUDE_DIR (works on macOS and Linux) // macOS: ${HOME}/.claude → ${HARNESS_USER_DIR} // Linux: ${HOME}/.claude → ${HARNESS_USER_DIR} diff --git a/LifeOS/install/LIFEOS/TOOLS/SessionProgress.ts b/LifeOS/install/LIFEOS/TOOLS/SessionProgress.ts index 8986f8534..9d47e2d62 100755 --- a/LifeOS/install/LIFEOS/TOOLS/SessionProgress.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SessionProgress.ts @@ -11,6 +11,7 @@ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs'; import { join } from 'path'; +import { getClaudeDir } from "./Paths"; interface Decision { timestamp: string; @@ -44,7 +45,7 @@ interface SessionProgress { } // Progress files are now in STATE/progress/ (consolidated from MEMORY/PROGRESS/) -const PROGRESS_DIR = join(process.env.HOME || '', '.claude', 'LIFEOS', 'MEMORY', 'STATE', 'progress'); +const PROGRESS_DIR = join(getClaudeDir(), 'LIFEOS', 'MEMORY', 'STATE', 'progress'); function getProgressPath(project: string): string { return join(PROGRESS_DIR, `${project}-progress.json`); diff --git a/LifeOS/install/LIFEOS/TOOLS/SessionRename.ts b/LifeOS/install/LIFEOS/TOOLS/SessionRename.ts index 50c927b65..6fef0cbe3 100644 --- a/LifeOS/install/LIFEOS/TOOLS/SessionRename.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SessionRename.ts @@ -35,13 +35,13 @@ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { resolve as pathResolve, join as pathJoin } from "node:path"; -import { homedir } from "node:os"; // Registry access goes through the isa-utils choke point (2026-06-10) — the // event-sourced write path. This file previously carried a duplicate // tmp+rename implementation; that was the one writer outside writeRegistry. import { readRegistry, writeRegistry } from "../../hooks/lib/isa-utils"; +import { getClaudeDir } from "./Paths"; -const CLAUDE_ROOT = pathResolve(homedir(), ".claude"); +const CLAUDE_ROOT = getClaudeDir(); const SESSION_NAMES_JSON = pathJoin(CLAUDE_ROOT, "LIFEOS/MEMORY/STATE/session-names.json"); interface WorkSession { diff --git a/LifeOS/install/LIFEOS/TOOLS/SyncIdentityToSettings.ts b/LifeOS/install/LIFEOS/TOOLS/SyncIdentityToSettings.ts index 37fce1d66..395872fb7 100644 --- a/LifeOS/install/LIFEOS/TOOLS/SyncIdentityToSettings.ts +++ b/LifeOS/install/LIFEOS/TOOLS/SyncIdentityToSettings.ts @@ -17,12 +17,11 @@ */ import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; -import { homedir } from 'os'; import { paiUserDir } from './LifeosConfig'; +import { getClaudeDir } from "./Paths"; -const HOME = homedir(); const PRINCIPAL_PATH = join(paiUserDir(), 'PRINCIPAL/PRINCIPAL_IDENTITY.md'); -const SETTINGS_PATH = join(HOME, '.claude/settings.json'); +const SETTINGS_PATH = join(getClaudeDir(), 'settings.json'); const VERBOSE = process.argv.includes('--verbose'); function snakeToCamel(s: string): string { diff --git a/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts b/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts index 7dc3dca44..7a5570fe6 100755 --- a/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TelosFreshness.ts @@ -24,9 +24,10 @@ import { readFileSync, writeFileSync, existsSync } from "fs"; import { basename, join } from "path"; +import { getClaudeDir } from "./Paths"; const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const TELOS_PATH = join(LIFEOS_DIR, "USER", "TELOS", "TELOS.md"); const DA_IDENTITY_PATH = join(LIFEOS_DIR, "USER", "DIGITAL_ASSISTANT", "DA_IDENTITY.md"); const PRINCIPAL_IDENTITY_PATH = join(LIFEOS_DIR, "USER", "PRINCIPAL", "PRINCIPAL_IDENTITY.md"); diff --git a/LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts b/LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts index 1d63dc452..35f93e6d3 100644 --- a/LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TlpArchive.ts @@ -14,9 +14,9 @@ import { writeFileSync, existsSync, readFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; +import { getLifeosDir } from "./Paths"; -const HOME = process.env.HOME!; -const KNOWLEDGE_DIR = join(HOME, ".claude/LIFEOS/MEMORY/KNOWLEDGE/Blogs"); +const KNOWLEDGE_DIR = join(getLifeosDir(), "MEMORY/KNOWLEDGE/Blogs"); const URL_FILE = "/tmp/tlp-urls.txt"; const FAILED_FILE = "/tmp/tlp-failed.txt"; const SUCCESS_FILE = "/tmp/tlp-success.txt"; diff --git a/LifeOS/install/LIFEOS/TOOLS/TokenXray/TokenXray.ts b/LifeOS/install/LIFEOS/TOOLS/TokenXray/TokenXray.ts index 404021162..e6abadb14 100644 --- a/LifeOS/install/LIFEOS/TOOLS/TokenXray/TokenXray.ts +++ b/LifeOS/install/LIFEOS/TOOLS/TokenXray/TokenXray.ts @@ -20,9 +20,9 @@ import { readFileSync, statSync } from "fs"; import { globSync } from "fs"; -import { homedir } from "os"; import { join } from "path"; import { getEncoding } from "js-tiktoken"; +import { displayPath, getClaudeDir, getLifeosDir } from "../Paths"; const enc = getEncoding("cl100k_base"); @@ -105,11 +105,11 @@ function* readJsonl(path: string): IterableIterator> { } function mainLogs(): string[] { - return glob(join(homedir(), ".claude/projects/*/*.jsonl")); + return glob(join(getClaudeDir(), "projects/*/*.jsonl")); } function sideLogs(): string[] { - return glob(join(homedir(), ".claude/projects/*/*/subagents/*.jsonl")); + return glob(join(getClaudeDir(), "projects/*/*/subagents/*.jsonl")); } function fmtNum(n: number, w = 0): string { @@ -228,7 +228,7 @@ interface ApiCostSnapshot { } function loadLatestApiCostSnapshot(): ApiCostSnapshot | null { - const path = join(homedir(), ".claude/LIFEOS/MEMORY/OBSERVABILITY/anthropic-cost.jsonl"); + const path = join(getLifeosDir(), "MEMORY/OBSERVABILITY/anthropic-cost.jsonl"); let raw: string; try { raw = readFileSync(path, "utf8"); @@ -281,7 +281,7 @@ function runActual(jsonOut: boolean): void { return; } - console.log("SUBSCRIPTION (~/.claude/projects/, OAuth-billed via Claude Max)"); + console.log(`SUBSCRIPTION (${displayPath(getClaudeDir())}/projects/, OAuth-billed via Claude Max)`); console.log(` counterfactual list-rate cost: $${fmtMoney(ct.total)}`); console.log(` actual marginal: $0.00 (covered by Max subscription fee)`); if (snap?.subscription) { @@ -875,7 +875,7 @@ FLAGS --json Emit structured JSON instead of human tables --help, -h Show this help -Reads only ~/.claude/projects/*​/*.jsonl and LIFEOS/MEMORY/OBSERVABILITY/anthropic-cost.jsonl. +Reads only ${displayPath(getClaudeDir())}/projects/*​/*.jsonl and LIFEOS/MEMORY/OBSERVABILITY/anthropic-cost.jsonl. Nothing leaves the machine. `); } diff --git a/LifeOS/install/LIFEOS/TOOLS/UpdateLifeosState.ts b/LifeOS/install/LIFEOS/TOOLS/UpdateLifeosState.ts index 0ccef1976..b2ba557b7 100755 --- a/LifeOS/install/LIFEOS/TOOLS/UpdateLifeosState.ts +++ b/LifeOS/install/LIFEOS/TOOLS/UpdateLifeosState.ts @@ -28,9 +28,9 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; import { join, dirname } from "path"; +import { getClaudeDir } from "./Paths"; -const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const IDEAL_DIR = join(LIFEOS_DIR, "USER", "TELOS", "IDEAL_STATE"); const CURRENT_DIR = join(LIFEOS_DIR, "USER", "TELOS", "CURRENT_STATE"); const STATE_FILE = join(LIFEOS_DIR, "USER", "TELOS", "LIFEOS_STATE.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts b/LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts index 0d3a7de79..c569f8a86 100755 --- a/LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts +++ b/LifeOS/install/LIFEOS/TOOLS/UpdateModels.ts @@ -24,6 +24,7 @@ import { readFileSync, writeFileSync, appendFileSync, readdirSync } from "fs"; import { join } from "path"; import { CURRENT, ALIAS, EFFORT_MODEL, CLAUDE_ID_PATTERN, type ClaudeTier } from "./models"; +import { displayPath, getClaudeDir } from "./Paths"; const CLAUDE_DIR = join(import.meta.dir, "..", ".."); const REGISTRY_PATH = join(import.meta.dir, "models.ts"); @@ -112,7 +113,7 @@ export function buildActionCommands(ids: string[]): string[] { return ids.map((id) => { const t = tierOf(id); return t - ? `bun ~/.claude/LIFEOS/TOOLS/UpdateModels.ts --apply ${t} ${id} && bun ~/.claude/LIFEOS/TOOLS/UpdateModels.ts --check` + ? `bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/UpdateModels.ts --apply ${t} ${id} && bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/UpdateModels.ts --check` : `review ${id} (unparseable tier)`; }); } diff --git a/LifeOS/install/LIFEOS/TOOLS/WisdomCrossFrameSynthesizer.ts b/LifeOS/install/LIFEOS/TOOLS/WisdomCrossFrameSynthesizer.ts index e542035ea..1694f772b 100644 --- a/LifeOS/install/LIFEOS/TOOLS/WisdomCrossFrameSynthesizer.ts +++ b/LifeOS/install/LIFEOS/TOOLS/WisdomCrossFrameSynthesizer.ts @@ -17,8 +17,9 @@ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join, basename } from 'path'; import { parseArgs } from 'util'; +import { getClaudeDir } from "./Paths"; -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = process.env.LIFEOS_DIR || getClaudeDir(); const WISDOM_DIR = join(BASE_DIR, 'MEMORY', 'WISDOM'); const FRAMES_DIR = join(WISDOM_DIR, 'FRAMES'); const PRINCIPLES_DIR = join(WISDOM_DIR, 'PRINCIPLES'); diff --git a/LifeOS/install/LIFEOS/TOOLS/WisdomDomainClassifier.ts b/LifeOS/install/LIFEOS/TOOLS/WisdomDomainClassifier.ts index 84dcf3fe2..6d2b25be1 100644 --- a/LifeOS/install/LIFEOS/TOOLS/WisdomDomainClassifier.ts +++ b/LifeOS/install/LIFEOS/TOOLS/WisdomDomainClassifier.ts @@ -16,8 +16,9 @@ import { existsSync, readdirSync, readFileSync } from 'fs'; import { join, basename } from 'path'; import { parseArgs } from 'util'; +import { getClaudeDir } from "./Paths"; -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = process.env.LIFEOS_DIR || getClaudeDir(); const FRAMES_DIR = join(BASE_DIR, 'MEMORY', 'WISDOM', 'FRAMES'); // ── Domain Keyword Map ── diff --git a/LifeOS/install/LIFEOS/TOOLS/WisdomFrameUpdater.ts b/LifeOS/install/LIFEOS/TOOLS/WisdomFrameUpdater.ts index 41d0386af..e7ac3d648 100644 --- a/LifeOS/install/LIFEOS/TOOLS/WisdomFrameUpdater.ts +++ b/LifeOS/install/LIFEOS/TOOLS/WisdomFrameUpdater.ts @@ -18,8 +18,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { parseArgs } from 'util'; +import { getClaudeDir } from "./Paths"; -const BASE_DIR = process.env.LIFEOS_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = process.env.LIFEOS_DIR || getClaudeDir(); const FRAMES_DIR = join(BASE_DIR, 'MEMORY', 'WISDOM', 'FRAMES'); // ── Types ── diff --git a/LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts b/LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts index 9819379c9..d8e7ad258 100755 --- a/LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/WorkSweep.ts @@ -27,11 +27,12 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, appendFileSync } from "fs"; import { join } from "path"; import { loadWorkConfig } from "../../hooks/lib/work-config"; +import { displayPath, getClaudeDir } from "./Paths"; declare const Bun: { spawn: (cmd: string[], opts?: any) => any }; const HOME = process.env.HOME || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const WORK_DIR = join(LIFEOS_DIR, "MEMORY", "WORK"); const OBS_DIR = join(LIFEOS_DIR, "MEMORY", "OBSERVABILITY"); const OBS_LOG = join(OBS_DIR, "worksweep.jsonl"); @@ -516,7 +517,7 @@ async function sweepBpeCadence( "", `**Run:** \`Skill("BitterPillEngineering", "audit")\` over the ${BPE_AUDIT_TARGETS}.`, `**Cadence:** every ${BPE_CADENCE_DAYS} days. Last audit: ${days === null ? "never" : days + " days ago"}.`, - `**Then stamp it:** \`bun ~/.claude/LIFEOS/TOOLS/WorkSweep.ts --stamp-bpe\` to reset the clock.`, + `**Then stamp it:** \`bun ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/WorkSweep.ts --stamp-bpe\` to reset the clock.`, "", `Propose-only — never auto-cut. A cut needs judgment + ratification (the producer-lock / shadow-log call is why).`, "", @@ -599,7 +600,7 @@ async function main(): Promise { // Final step: regenerate the TASKLIST.md and push (best-effort, never blocks) if (!dryRun) { const proc = Bun.spawn( - ["bun", join(HOME, ".claude", "skills", "_ULWORK", "Tools", "RegenerateTasklist.ts"), "--commit-push"], + ["bun", join(getClaudeDir(), "skills", "_ULWORK", "Tools", "RegenerateTasklist.ts"), "--commit-push"], { stdout: "inherit", stderr: "inherit", timeout: 30000 }, ); await proc.exited; diff --git a/LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts b/LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts index c3813a789..eed2cf03f 100755 --- a/LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts +++ b/LifeOS/install/LIFEOS/TOOLS/YouTubeApi.ts @@ -22,6 +22,7 @@ import { readFileSync } from 'fs' import { homedir } from 'os' import { join } from 'path' +import { getClaudeDir } from "./Paths"; // ANSI colors const colors = { @@ -37,7 +38,7 @@ const colors = { // Load environment function loadEnv(): Record { - const envPath = process.env.LIFEOS_CONFIG_DIR ? join(process.env.LIFEOS_CONFIG_DIR, '.env') : join(homedir(), '.claude', '.env') + const envPath = process.env.LIFEOS_CONFIG_DIR ? join(process.env.LIFEOS_CONFIG_DIR, '.env') : join(getClaudeDir(), '.env') const env: Record = {} try { const content = readFileSync(envPath, 'utf-8') diff --git a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts index 789a6956f..ba020ea6a 100644 --- a/LifeOS/install/LIFEOS/TOOLS/algorithm.ts +++ b/LifeOS/install/LIFEOS/TOOLS/algorithm.ts @@ -51,11 +51,12 @@ import { resolve, basename, join, dirname } from "path"; import { spawnSync, spawn } from "child_process"; import { randomUUID } from "crypto"; import { generateISATemplate } from "../../hooks/lib/isa-template"; +import { displayPath, getClaudeDir } from "./Paths"; // ─── Paths ─────────────────────────────────────────────────────────────────── const HOME = process.env.HOME || "~"; -const BASE_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude"); +const BASE_DIR = process.env.LIFEOS_DIR || getClaudeDir(); const ALGORITHMS_DIR = join(BASE_DIR, "MEMORY", "STATE", "algorithms"); const SESSION_NAMES_PATH = join(BASE_DIR, "MEMORY", "STATE", "session-names.json"); const PROJECTS_DIR = process.env.PROJECTS_DIR || join(HOME, "Projects"); @@ -362,7 +363,7 @@ Optimize Presets: aggressive Large steps, accepts regression — for prototypes ISA Resolution: - Full path ~/.claude/LIFEOS/MEMORY/WORK/auth/ISA-20260207-auth.md + Full path ${displayPath(getClaudeDir())}/LIFEOS/MEMORY/WORK/auth/ISA-20260207-auth.md ISA ID ISA-20260207-auth (searches MEMORY/WORK/ and ~/Projects/*/.prd/) Project path /path/to/project/.prd/ISA-20260213-feature.md diff --git a/LifeOS/install/LIFEOS/TOOLS/healthsync/eightsleep.ts b/LifeOS/install/LIFEOS/TOOLS/healthsync/eightsleep.ts index 03d91e387..ea13fb5eb 100644 --- a/LifeOS/install/LIFEOS/TOOLS/healthsync/eightsleep.ts +++ b/LifeOS/install/LIFEOS/TOOLS/healthsync/eightsleep.ts @@ -9,6 +9,7 @@ * rotate them or the endpoints at any time; on breakage, re-research the HA * integration first. */ +import { getClaudeDir, displayPath } from "../Paths"; import type { Ctx, SourceResult } from "./types"; import { authCooldownUntil, @@ -113,7 +114,7 @@ async function authenticate(ctx: Ctx): Promise { const email = str(ctx.env.EIGHTSLEEP_EMAIL); const password = str(ctx.env.EIGHTSLEEP_PASSWORD); if (email === null || password === null) { - return { ok: false, error: "EIGHTSLEEP_EMAIL / EIGHTSLEEP_PASSWORD not set in ~/.claude/.env", attempted: false }; + return { ok: false, error: `EIGHTSLEEP_EMAIL / EIGHTSLEEP_PASSWORD not set in ${displayPath(getClaudeDir())}/.env`, attempted: false }; } const tokens = await loadTokens(ctx); diff --git a/LifeOS/install/LIFEOS/TOOLS/healthsync/function.ts b/LifeOS/install/LIFEOS/TOOLS/healthsync/function.ts index 9f05596d8..51c48eab9 100644 --- a/LifeOS/install/LIFEOS/TOOLS/healthsync/function.ts +++ b/LifeOS/install/LIFEOS/TOOLS/healthsync/function.ts @@ -10,6 +10,7 @@ import { join } from "node:path"; import type { Biomarker, Ctx, LabsFile, SourceResult } from "./types"; import { authCooldownUntil, dayKeyLA, isoNowLA, loadState, timedFetch, writeJson } from "./store"; +import { displayPath, getClaudeDir } from "../Paths"; const BASE = "https://production-member-app-mid-lhuqotpy2a-ue.a.run.app/api/v1"; const FETCH_TIMEOUT_MS = 15_000; @@ -102,7 +103,7 @@ export async function pull(ctx: Ctx): Promise { const email = str(ctx.env.FUNCTION_HEALTH_EMAIL); const password = str(ctx.env.FUNCTION_HEALTH_PASSWORD); if (email === null || password === null) { - return unconfigured("FUNCTION_HEALTH_EMAIL / FUNCTION_HEALTH_PASSWORD not set in ~/.claude/.env", startedAt); + return unconfigured(`FUNCTION_HEALTH_EMAIL / FUNCTION_HEALTH_PASSWORD not set in ${displayPath(getClaudeDir())}/.env`, startedAt); } const state = await loadState(ctx); diff --git a/LifeOS/install/LIFEOS/TOOLS/healthsync/oura.ts b/LifeOS/install/LIFEOS/TOOLS/healthsync/oura.ts index f67ceb83a..d116a48a6 100644 --- a/LifeOS/install/LIFEOS/TOOLS/healthsync/oura.ts +++ b/LifeOS/install/LIFEOS/TOOLS/healthsync/oura.ts @@ -2,6 +2,7 @@ * Oura source module — OFFICIAL API v2 (OAuth2; PATs were removed by Oura Dec 2025). * Provenance: https://cloud.ouraring.com/v2/docs + /docs/authentication, verified 2026-06-11. */ +import { getClaudeDir, displayPath } from "../Paths"; import type { Ctx, SourceResult, TokenStore } from "./types"; import { dayKeyLA, @@ -185,7 +186,7 @@ export async function pull(ctx: Ctx): Promise { str(ctx.env.OURA_CLIENT_ID) === null || str(ctx.env.OURA_CLIENT_SECRET) === null ) { - return unconfigured("OURA_CLIENT_ID / OURA_CLIENT_SECRET not set in ~/.claude/.env", startedAt); + return unconfigured(`OURA_CLIENT_ID / OURA_CLIENT_SECRET not set in ${displayPath(getClaudeDir())}/.env`, startedAt); } let tokens = await loadTokens(ctx); diff --git a/LifeOS/install/LIFEOS/TOOLS/healthsync/store.ts b/LifeOS/install/LIFEOS/TOOLS/healthsync/store.ts index ac2b58301..11c453d2a 100644 --- a/LifeOS/install/LIFEOS/TOOLS/healthsync/store.ts +++ b/LifeOS/install/LIFEOS/TOOLS/healthsync/store.ts @@ -8,12 +8,12 @@ import type { SyncState, TokenStore, } from "./types"; +import { getClaudeDir } from "../Paths"; -const HOME = process.env.HOME || ""; -const ENV_PATH = join(HOME, ".claude", ".env"); -const STATE_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "STATE"); -const DATA_DIR = join(HOME, ".claude", "LIFEOS", "USER", "HEALTH", "DATA"); -const OBS_DIR = join(HOME, ".claude", "LIFEOS", "MEMORY", "OBSERVABILITY"); +const ENV_PATH = join(getClaudeDir(), ".env"); +const STATE_DIR = join(getClaudeDir(), "LIFEOS", "MEMORY", "STATE"); +const DATA_DIR = join(getClaudeDir(), "LIFEOS", "USER", "HEALTH", "DATA"); +const OBS_DIR = join(getClaudeDir(), "LIFEOS", "MEMORY", "OBSERVABILITY"); const TOKENS_PATH = join(STATE_DIR, "healthsync-tokens.json"); const STATE_PATH = join(STATE_DIR, "healthsync-state.json"); diff --git a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts index 0b30fd0bd..d13597077 100755 --- a/LifeOS/install/LIFEOS/TOOLS/lifeos.ts +++ b/LifeOS/install/LIFEOS/TOOLS/lifeos.ts @@ -10,7 +10,7 @@ * pai -m bd Launch with Bright Data MCP * pai -m bd,ap Launch with multiple MCPs * pai -r / --resume Resume last session - * pai --local Stay in current directory (don't cd to ~/.claude) + * pai --local Stay in current directory (don't cd to the Claude config dir) * pai update Update Claude Code * pai version Show version info * pai profiles List available profiles @@ -597,7 +597,7 @@ USAGE: k -m bd,ap Launch with multiple MCPs k -r, --resume Resume last session k -s, --system-prompt System prompt file to append (default: LIFEOS_SYSTEM_PROMPT.md) - k -l, --local Stay in current directory (don't cd to ~/.claude) + k -l, --local Stay in current directory (don't cd to the Claude config dir) COMMANDS: k update Update Claude Code to latest version diff --git a/LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts b/LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts index 40b3a4812..b5a666ff8 100755 --- a/LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts +++ b/LifeOS/install/LIFEOS/TOOLS/llcli/llcli.ts @@ -11,7 +11,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; -import { homedir } from 'os'; +import { displayPath, getClaudeDir } from "../Paths"; // ============================================================================ // Types @@ -53,7 +53,7 @@ const DEFAULTS = { * Load configuration from environment */ function loadConfig(): Config { - const envPath = process.env.LIFEOS_CONFIG_DIR ? join(process.env.LIFEOS_CONFIG_DIR, '.env') : join(homedir(), '.claude', '.env'); + const envPath = process.env.LIFEOS_CONFIG_DIR ? join(process.env.LIFEOS_CONFIG_DIR, '.env') : join(getClaudeDir(), '.env'); try { const envContent = readFileSync(envPath, 'utf-8'); @@ -64,7 +64,7 @@ function loadConfig(): Config { ?.trim(); if (!apiKey) { - console.error('Error: LIMITLESS_API_KEY not found in ~/.claude/.env'); + console.error(`Error: LIMITLESS_API_KEY not found in ${displayPath(getClaudeDir())}/.env`); process.exit(1); } @@ -74,8 +74,8 @@ function loadConfig(): Config { baseUrl: DEFAULTS.baseUrl, }; } catch (error) { - console.error(`Error: Cannot read ~/.claude/.env file`); - console.error('Make sure LIMITLESS_API_KEY is set in ~/.claude/.env'); + console.error(`Error: Cannot read ${displayPath(getClaudeDir())}/.env file`); + console.error(`Make sure LIMITLESS_API_KEY is set in ${displayPath(getClaudeDir())}/.env`); process.exit(1); } } @@ -222,7 +222,7 @@ OUTPUT: Exit code 0 on success, 1 on error CONFIGURATION: - API Key: ~/.claude/.env (LIMITLESS_API_KEY=your_key) + API Key: ${displayPath(getClaudeDir())}/.env (LIMITLESS_API_KEY=your_key) Timezone: America/Los_Angeles (Pacific Time) Base URL: https://api.limitless.ai/v1 @@ -251,7 +251,7 @@ PHILOSOPHY: - Documented: Full help and examples - Testable: Predictable behavior -For more information, see ~/.claude/LIFEOS/TOOLS/llcli/README.md +For more information, see ${displayPath(getClaudeDir())}/LIFEOS/TOOLS/llcli/README.md Version: 1.0.0 Author: {{PRINCIPAL_FULL_NAME}} diff --git a/LifeOS/install/skills/Agents/Tools/ComposeAgent.ts b/LifeOS/install/skills/Agents/Tools/ComposeAgent.ts index 82f3f4654..9930d2054 100755 --- a/LifeOS/install/skills/Agents/Tools/ComposeAgent.ts +++ b/LifeOS/install/skills/Agents/Tools/ComposeAgent.ts @@ -31,13 +31,13 @@ import { parseArgs } from "util"; import { readFileSync, existsSync, readdirSync, unlinkSync, mkdirSync, writeFileSync } from "fs"; import { parse as parseYaml } from "yaml"; import Handlebars from "handlebars"; +import { displayPath, getClaudeDir, getLifeosDir } from "../../../LIFEOS/TOOLS/Paths"; // Paths -const HOME = process.env.HOME || "~"; -const BASE_TRAITS_PATH = `${HOME}/.claude/skills/Agents/Data/Traits.yaml`; -const USER_TRAITS_PATH = `${HOME}/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Agents/Traits.yaml`; -const TEMPLATE_PATH = `${HOME}/.claude/skills/Agents/Templates/DynamicAgent.hbs`; -const CUSTOM_AGENTS_DIR = `${HOME}/.claude/custom-agents`; +const BASE_TRAITS_PATH = `${getClaudeDir()}/skills/Agents/Data/Traits.yaml`; +const USER_TRAITS_PATH = `${getLifeosDir()}/USER/CUSTOMIZATIONS/SKILLS/Agents/Traits.yaml`; +const TEMPLATE_PATH = `${getClaudeDir()}/skills/Agents/Templates/DynamicAgent.hbs`; +const CUSTOM_AGENTS_DIR = `${getClaudeDir()}/custom-agents`; // Types interface ProsodySettings { @@ -727,13 +727,13 @@ ${identityList} To re-compose this agent with a specific task: \`\`\`bash -bun run ~/.claude/skills/Agents/Tools/ComposeAgent.ts --load "${slug}" +bun run ${displayPath(getClaudeDir())}/skills/Agents/Tools/ComposeAgent.ts --load "${slug}" \`\`\` Or reconstruct from traits: \`\`\`bash -bun run ~/.claude/skills/Agents/Tools/ComposeAgent.ts --traits "${agent.traits.join(",")}" +bun run ${displayPath(getClaudeDir())}/skills/Agents/Tools/ComposeAgent.ts --traits "${agent.traits.join(",")}" \`\`\` --- @@ -883,16 +883,16 @@ OPTIONS: -o, --output Output format: prompt (default), json, yaml, summary --timing Timing scope: fast, standard (default), deep -l, --list List all available traits - -s, --save Save composed agent to ~/.claude/custom-agents/ + -s, --save Save composed agent to ${displayPath(getClaudeDir())}/custom-agents/ --list-saved List all saved custom agents --load Load a saved custom agent's prompt --delete Delete a saved custom agent -h, --help Show this help CONFIGURATION: - Base traits: ~/.claude/skills/Agents/Data/Traits.yaml - User traits: ~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Agents/Traits.yaml - Custom agents: ~/.claude/custom-agents/ + Base traits: ${displayPath(getClaudeDir())}/skills/Agents/Data/Traits.yaml + User traits: ${displayPath(getClaudeDir())}/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Agents/Traits.yaml + Custom agents: ${displayPath(getClaudeDir())}/custom-agents/ User traits are merged over base (user takes priority). Add your custom voices, personalities, and prosody settings in the user file. diff --git a/LifeOS/install/skills/Agents/Tools/LoadAgentContext.ts b/LifeOS/install/skills/Agents/Tools/LoadAgentContext.ts index f0918489c..01841a7e4 100755 --- a/LifeOS/install/skills/Agents/Tools/LoadAgentContext.ts +++ b/LifeOS/install/skills/Agents/Tools/LoadAgentContext.ts @@ -12,7 +12,7 @@ import { readFileSync, existsSync, readdirSync } from "fs"; import { join } from "path"; -import { homedir } from "os"; +import { getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; interface AgentContext { agentType: string; @@ -25,7 +25,7 @@ export class AgentContextLoader { private agentsDir: string; constructor() { - this.claudeHome = join(homedir(), ".claude"); + this.claudeHome = getClaudeDir(); this.agentsDir = join(this.claudeHome, "Skills", "Agents"); } diff --git a/LifeOS/install/skills/Apify/examples/comparison-test.ts b/LifeOS/install/skills/Apify/examples/comparison-test.ts index dc8fa7ca4..73dbc1b98 100755 --- a/LifeOS/install/skills/Apify/examples/comparison-test.ts +++ b/LifeOS/install/skills/Apify/examples/comparison-test.ts @@ -63,7 +63,8 @@ async function demonstrateCodeFirstApproach() { console.log('\nStep 2: Model writes code to execute operations') const codeExample = ` -import { Apify } from '~/.claude/filesystem-mcps/apify' +import { Apify } from '${displayPath(getClaudeDir())}/filesystem-mcps/apify' +import { displayPath, getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; const apify = new Apify() diff --git a/LifeOS/install/skills/Art/Tools/ComposeThumbnail.ts b/LifeOS/install/skills/Art/Tools/ComposeThumbnail.ts index c1587843e..7674e3bf9 100755 --- a/LifeOS/install/skills/Art/Tools/ComposeThumbnail.ts +++ b/LifeOS/install/skills/Art/Tools/ComposeThumbnail.ts @@ -16,6 +16,7 @@ import { spawn } from "node:child_process"; import { existsSync, unlinkSync } from "node:fs"; import { resolve, dirname } from "node:path"; +import { displayPath, getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; // ============================================================================ // Types @@ -105,7 +106,7 @@ function printHelp(): void { ComposeThumbnail - YouTube Thumbnail Composition CLI USAGE: - bun ~/.claude/skills/Art/Tools/ComposeThumbnail.ts [OPTIONS] + bun ${displayPath(getClaudeDir())}/skills/Art/Tools/ComposeThumbnail.ts [OPTIONS] REQUIRED: --background Background image (dramatic tech art) @@ -123,7 +124,7 @@ OPTIONAL: --help, -h Show this help message EXAMPLE: - bun ~/.claude/skills/Art/Tools/ComposeThumbnail.ts \\ + bun ${displayPath(getClaudeDir())}/skills/Art/Tools/ComposeThumbnail.ts \\ --background ~/Downloads/tech-background.png \\ --headshot ~/Downloads/headshot-nobg.png \\ --title "AI AGENTS KILLING SOFTWARE" \\ diff --git a/LifeOS/install/skills/Art/Tools/Generate.ts b/LifeOS/install/skills/Art/Tools/Generate.ts index bedebf4e3..213de8426 100755 --- a/LifeOS/install/skills/Art/Tools/Generate.ts +++ b/LifeOS/install/skills/Art/Tools/Generate.ts @@ -31,7 +31,7 @@ import { extname, resolve } from "node:path"; * This ensures API keys are available regardless of how the CLI is invoked */ async function loadEnv(): Promise { - const paiDir = process.env.LIFEOS_DIR || resolve(process.env.HOME!, '.claude'); + const paiDir = process.env.LIFEOS_DIR || getClaudeDir(); const envPath = resolve(paiDir, '.env'); try { const envContent = await readFile(envPath, 'utf-8'); @@ -211,7 +211,7 @@ async function detectMimeType(filePath: string): Promise { // ============================================================================ // LifeOS directory for documentation paths -const LIFEOS_DIR = process.env.LIFEOS_DIR || `${process.env.HOME}/.claude`; +const LIFEOS_DIR = process.env.LIFEOS_DIR || `${getClaudeDir()}`; function showHelp(): void { console.log(` @@ -345,7 +345,7 @@ MORE INFO: * --workflow= → exit 1 listing valid workflow names. */ function enforceWorkflowDiscipline(parsed: Partial): void { - const workflowsDir = `${process.env.HOME}/.claude/skills/Art/Workflows`; + const workflowsDir = `${getClaudeDir()}/skills/Art/Workflows`; let availableWorkflows: string[] = []; try { // readdirSync via Bun.readdirSync isn't a thing; use Node fs sync via dynamic @@ -674,6 +674,7 @@ function enhancePromptForTransparency(prompt: string): string { import { exec } from "node:child_process"; import { promisify } from "node:util"; +import { getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; const execAsync = promisify(exec); diff --git a/LifeOS/install/skills/Art/Tools/GenerateMidjourneyImage.ts b/LifeOS/install/skills/Art/Tools/GenerateMidjourneyImage.ts index 41bb9c865..98d0f4214 100755 --- a/LifeOS/install/skills/Art/Tools/GenerateMidjourneyImage.ts +++ b/LifeOS/install/skills/Art/Tools/GenerateMidjourneyImage.ts @@ -16,6 +16,7 @@ import { DiscordBotClient } from '../lib/discord-bot.js'; import { MidjourneyClient, MidjourneyError } from '../lib/midjourney-client.js'; import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; +import { getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; // ============================================================================ // Environment Loading @@ -26,7 +27,7 @@ import { resolve } from 'node:path'; * This ensures API keys are available regardless of how the CLI is invoked */ async function loadEnv(): Promise { - const paiDir = process.env.LIFEOS_DIR || resolve(process.env.HOME!, '.claude'); + const paiDir = process.env.LIFEOS_DIR || getClaudeDir(); const envPath = resolve(paiDir, '.env'); try { const envContent = await readFile(envPath, 'utf-8'); diff --git a/LifeOS/install/skills/Art/Tools/GeneratePrompt.ts b/LifeOS/install/skills/Art/Tools/GeneratePrompt.ts index 320294417..597e99e3e 100755 --- a/LifeOS/install/skills/Art/Tools/GeneratePrompt.ts +++ b/LifeOS/install/skills/Art/Tools/GeneratePrompt.ts @@ -19,6 +19,7 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; +import { getLifeosDir } from "../../../LIFEOS/TOOLS/Paths"; // ============================================================================ // Types @@ -68,8 +69,8 @@ interface PromptOutput { // ============================================================================ const ART_AESTHETIC_PATH = resolve( - process.env.HOME!, - ".claude/LIFEOS/Aesthetic.md" + getLifeosDir(), + "Aesthetic.md" ); const COLOR_HEX_MAP: Record = { diff --git a/LifeOS/install/skills/Art/Tools/PickExpression.ts b/LifeOS/install/skills/Art/Tools/PickExpression.ts index c70508730..10bd3c3b6 100755 --- a/LifeOS/install/skills/Art/Tools/PickExpression.ts +++ b/LifeOS/install/skills/Art/Tools/PickExpression.ts @@ -18,10 +18,10 @@ * directly in the workflow if a more specific expression fits. */ import { existsSync, readdirSync } from "node:fs"; -import { homedir } from "node:os"; import { join } from "node:path"; +import { getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; -const DIR = join(homedir(), ".claude", "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "Art", "HeadshotExamples"); +const DIR = join(getClaudeDir(), "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "Art", "HeadshotExamples"); // sentiment -> headshot filename (without .png), with topic keywords that route to it. const MAP: Array<{ sentiment: string; file: string; keywords: string[] }> = [ diff --git a/LifeOS/install/skills/Art/Tools/ThumbnailText.ts b/LifeOS/install/skills/Art/Tools/ThumbnailText.ts index f1dc490dd..bf07ad5e4 100755 --- a/LifeOS/install/skills/Art/Tools/ThumbnailText.ts +++ b/LifeOS/install/skills/Art/Tools/ThumbnailText.ts @@ -32,6 +32,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, rmSync } from "node:fs"; import { homedir } from "node:os"; import { join, parse } from "node:path"; +import { getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; const W = 1280; const H = 720; @@ -41,7 +42,7 @@ const NAVY = "#1A2744"; const PERIWINKLE = "#6B8DD6"; const WHITE = "#FFFFFF"; const VARIANT_BORDER: Record = { core: "#316AE9", sponsored: "#306F1D" }; -const BRAND_LOGO = join(homedir(), ".claude", "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "Art", "brand", "ti-logo-white.png"); +const BRAND_LOGO = join(getClaudeDir(), "LIFEOS", "USER", "CUSTOMIZATIONS", "SKILLS", "Art", "brand", "ti-logo-white.png"); function arg(name: string, def?: string): string | undefined { const i = process.argv.indexOf(`--${name}`); diff --git a/LifeOS/install/skills/AudioEditor/Tools/Polish.ts b/LifeOS/install/skills/AudioEditor/Tools/Polish.ts index d2ac589f6..68bccd8a8 100644 --- a/LifeOS/install/skills/AudioEditor/Tools/Polish.ts +++ b/LifeOS/install/skills/AudioEditor/Tools/Polish.ts @@ -16,7 +16,7 @@ import { existsSync, readFileSync } from "fs"; import { basename, dirname, extname, join, resolve } from "path"; -import { homedir } from "os"; +import { displayPath, getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; // ============================================================================ // Environment Loading — keys from ~/.claude/.env @@ -25,7 +25,7 @@ import { homedir } from "os"; function loadEnv(): void { const envPath = process.env.LIFEOS_CONFIG_DIR ? resolve(process.env.LIFEOS_CONFIG_DIR, ".env") - : resolve(homedir(), ".claude/.env"); + : resolve(getClaudeDir(), ".env"); try { const content = readFileSync(envPath, "utf-8"); for (const line of content.split("\n")) { @@ -70,7 +70,7 @@ if (!existsSync(audioFile)) { const apiKey = process.env.CLEANVOICE_API_KEY; if (!apiKey) { - console.error("CLEANVOICE_API_KEY not found. Set it in ~/.claude/.env"); + console.error(`CLEANVOICE_API_KEY not found. Set it in ${displayPath(getClaudeDir())}/.env`); console.error("Get key at: https://cleanvoice.ai → Dashboard → Settings → API Key"); process.exit(1); } diff --git a/LifeOS/install/skills/ContextSearch/Tools/ContextSearch.ts b/LifeOS/install/skills/ContextSearch/Tools/ContextSearch.ts index 1a95fce65..f7bda6664 100755 --- a/LifeOS/install/skills/ContextSearch/Tools/ContextSearch.ts +++ b/LifeOS/install/skills/ContextSearch/Tools/ContextSearch.ts @@ -4,9 +4,10 @@ declare const Bun: any; import { readFileSync, statSync, existsSync, readdirSync } from "node:fs"; import { join, basename } from "node:path"; import { homedir } from "node:os"; +import { getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; const HOME = homedir(); -const LIFEOS_DIR = join(HOME, ".claude"); +const LIFEOS_DIR = getClaudeDir(); const STATE_DIR = join(LIFEOS_DIR, "LIFEOS", "MEMORY", "STATE"); const WORK_DIR = join(LIFEOS_DIR, "LIFEOS", "MEMORY", "WORK"); // Claude Code names each project dir by its absolute path with "/" and "." mapped to "-", @@ -352,7 +353,7 @@ async function searchIsaBodies(tokens: string[], since: Date | null, until: Date async function searchJsonl(tokens: string[], since: Date | null, until: Date | null = null): Promise { if (tokens.length === 0 || !existsSync(JSONL_DIR)) return []; const realDir = JSONL_DIR; - if (!realDir.startsWith(join(HOME, ".claude", "Projects", PROJECT_SLUG))) return []; + if (!realDir.startsWith(join(getClaudeDir(), "Projects", PROJECT_SLUG))) return []; const pattern = tokens.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); const out = await ripgrep(pattern, realDir, ["--glob", "*.jsonl", "-c"]); const byFile = new Map(); diff --git a/LifeOS/install/skills/Daemon/Tools/DaemonAggregator.ts b/LifeOS/install/skills/Daemon/Tools/DaemonAggregator.ts index f7256942e..3e22bf86a 100755 --- a/LifeOS/install/skills/Daemon/Tools/DaemonAggregator.ts +++ b/LifeOS/install/skills/Daemon/Tools/DaemonAggregator.ts @@ -18,11 +18,11 @@ import { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from "fs"; import { join, resolve } from "path"; import { filterContent, filterDaemonData, loadSecurityOverrides } from "./SecurityFilter.ts"; +import { getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; // ─── Path Resolution ─── -const HOME = process.env.HOME || process.env.USERPROFILE || ""; -const LIFEOS_DIR = process.env.LIFEOS_DIR || join(HOME, ".claude", "LIFEOS"); +const LIFEOS_DIR = process.env.LIFEOS_DIR || join(getClaudeDir(), "LIFEOS"); const USER_DIR = join(LIFEOS_DIR, "USER"); const MEMORY_DIR = join(LIFEOS_DIR, "MEMORY"); const TELOS_DIR = join(USER_DIR, "TELOS"); @@ -369,7 +369,7 @@ function readExistingDaemon(): Record { const daemonPath = join(USER_DAEMON_DIR, "daemon.md"); if (!existsSync(daemonPath)) { // Fall back to old location - const oldPath = join(HOME, ".claude", "skills", "_DAEMON", "Mcp", "daemon.md"); + const oldPath = join(getClaudeDir(), "skills", "_DAEMON", "Mcp", "daemon.md"); if (!existsSync(oldPath)) return {}; return parseDaemonMd(readFileSync(oldPath, "utf-8")); } diff --git a/LifeOS/install/skills/Evals/Tools/AlgorithmBridge.ts b/LifeOS/install/skills/Evals/Tools/AlgorithmBridge.ts index a3c96bb91..5ddd7daf9 100755 --- a/LifeOS/install/skills/Evals/Tools/AlgorithmBridge.ts +++ b/LifeOS/install/skills/Evals/Tools/AlgorithmBridge.ts @@ -13,6 +13,7 @@ import { join } from 'path'; import { parse as parseYaml } from 'yaml'; import { parseArgs } from 'util'; import { $ } from 'bun'; +import { displayPath, getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; const EVALS_DIR = join(import.meta.dir, '..'); // Run artifacts live outside the skill tree (runtime state, not skill content). @@ -160,7 +161,7 @@ export function formatForISC(result: AlgorithmEvalResult): string { export async function updateISCWithResult(result: AlgorithmEvalResult): Promise { const status = result.passed ? 'DONE' : 'BLOCKED'; - await $`bun run ~/.claude/skills/THEALGORITHM/Tools/ISCManager.ts update --row ${result.isc_row} --status ${status} --note "${formatForISC(result)}"`.quiet(); + await $`bun run ${displayPath(getClaudeDir())}/skills/THEALGORITHM/Tools/ISCManager.ts update --row ${result.isc_row} --status ${status} --note "${formatForISC(result)}"`.quiet(); } // CLI interface diff --git a/LifeOS/install/skills/Interceptor/Tools/Capture.sh b/LifeOS/install/skills/Interceptor/Tools/Capture.sh index 173ad48ba..0aa2c9079 100755 --- a/LifeOS/install/skills/Interceptor/Tools/Capture.sh +++ b/LifeOS/install/skills/Interceptor/Tools/Capture.sh @@ -25,7 +25,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -USER_PREFS="${HOME}/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" +USER_PREFS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env" usage() { cat < = { construction: fetchConstruction, diff --git a/LifeOS/install/skills/Telos/Tools/UpdateTelos.ts b/LifeOS/install/skills/Telos/Tools/UpdateTelos.ts index 150af139a..ae41b6b19 100755 --- a/LifeOS/install/skills/Telos/Tools/UpdateTelos.ts +++ b/LifeOS/install/skills/Telos/Tools/UpdateTelos.ts @@ -37,8 +37,9 @@ import { readFileSync, writeFileSync, copyFileSync, existsSync } from 'fs'; import { join } from 'path'; import { getPrincipal } from '../../../hooks/lib/identity'; +import { getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; -const TELOS_DIR = join(process.env.HOME!, '.claude', 'LIFEOS', 'USER', 'TELOS'); +const TELOS_DIR = join(getClaudeDir(), 'LIFEOS', 'USER', 'TELOS'); const BACKUPS_DIR = join(TELOS_DIR, 'Backups'); const UPDATES_FILE = join(TELOS_DIR, 'Updates.md'); diff --git a/LifeOS/install/skills/Upgrade/Tools/Anthropic.ts b/LifeOS/install/skills/Upgrade/Tools/Anthropic.ts index da9b4d308..c7b11d44c 100755 --- a/LifeOS/install/skills/Upgrade/Tools/Anthropic.ts +++ b/LifeOS/install/skills/Upgrade/Tools/Anthropic.ts @@ -27,7 +27,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { createHash } from 'crypto'; import { join } from 'path'; -import { homedir } from 'os'; +import { displayPath, getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; // Types interface Source { @@ -84,8 +84,7 @@ interface State { } // Config -const HOME = homedir(); -const SKILL_DIR = join(HOME, '.claude', 'skills', 'Upgrade'); +const SKILL_DIR = join(getClaudeDir(), 'skills', 'Upgrade'); const STATE_DIR = join(SKILL_DIR, 'State'); const STATE_FILE = join(STATE_DIR, 'last-check.json'); const SOURCES_FILE = join(SKILL_DIR, 'sources.json'); @@ -544,7 +543,7 @@ function generateRecommendation(update: Update): string { // Commands/Slash Commands if (titleLower.includes('command') || titleLower.includes('slash command')) { return `**PAI Impact:** HIGH - Command system update\n` + - `**Why:** PAI uses slash commands extensively (~/.claude/Commands/). Changes affect our command architecture and user workflows.\n` + + `**Why:** PAI uses slash commands extensively (${displayPath(getClaudeDir())}/Commands/). Changes affect our command architecture and user workflows.\n` + `**Action:** Review for new command patterns or capabilities. Update PAI's command templates if conventions change.`; } diff --git a/LifeOS/install/skills/Webdesign/Tools/DriveClaudeDesign.ts b/LifeOS/install/skills/Webdesign/Tools/DriveClaudeDesign.ts index 48fa4a401..98c27e41b 100644 --- a/LifeOS/install/skills/Webdesign/Tools/DriveClaudeDesign.ts +++ b/LifeOS/install/skills/Webdesign/Tools/DriveClaudeDesign.ts @@ -11,6 +11,7 @@ Thin Interceptor wrapper. UI targeting uses loud accessibility-tree heuristics. */ import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises"; import { basename, join, resolve } from "node:path"; +import { displayPath, getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; type TreeNode = { ref?: string; role?: string; name?: string; text?: string; contenteditable?: boolean | string; children?: TreeNode[] }; @@ -18,7 +19,7 @@ function resolveInterceptorBin(): string { const found = Bun.spawnSync(["which", "interceptor"]); const bin = found.stdout.toString().trim(); if (found.exitCode !== 0 || bin.length === 0) { - console.error("interceptor CLI not found on PATH — install the Interceptor skill (see ~/.claude/skills/Interceptor/SKILL.md)"); + console.error(`interceptor CLI not found on PATH — install the Interceptor skill (see ${displayPath(getClaudeDir())}/skills/Interceptor/SKILL.md)`); process.exit(127); } return bin; diff --git a/LifeOS/install/skills/Webdesign/Tools/VerifyDesign.ts b/LifeOS/install/skills/Webdesign/Tools/VerifyDesign.ts index bbd2917d4..ad6277fbd 100644 --- a/LifeOS/install/skills/Webdesign/Tools/VerifyDesign.ts +++ b/LifeOS/install/skills/Webdesign/Tools/VerifyDesign.ts @@ -10,6 +10,7 @@ verb. Accessibility checks are viewport-independent tree heuristics, not axe-cor import { stat } from "node:fs/promises"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; +import { displayPath, getClaudeDir } from "../../../LIFEOS/TOOLS/Paths"; type TreeNode = { ref?: string; @@ -33,7 +34,7 @@ function resolveInterceptorBin(): string { const found = Bun.spawnSync(["which", "interceptor"]); const bin = found.stdout.toString().trim(); if (found.exitCode !== 0 || bin.length === 0) { - console.error("interceptor CLI not found on PATH — install the Interceptor skill (see ~/.claude/skills/Interceptor/SKILL.md)"); + console.error(`interceptor CLI not found on PATH — install the Interceptor skill (see ${displayPath(getClaudeDir())}/skills/Interceptor/SKILL.md)`); process.exit(127); } return bin; From a4e6451c51721d3db9a1d01cdc6133b9b034b075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Mon, 6 Jul 2026 02:12:45 +0100 Subject: [PATCH 8/9] fix: statusline resolves the config root and expands literal env values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LIFEOS_StatusLine.sh was missed by the sweep (root-level LIFEOS file): CLAUDE_HOME hardcoded ~/.claude, and LIFEOS_DIR consumed the settings env value as-is — but the harness passes env values UNexpanded, so '$HOME/.claude/LIFEOS' arrived literally and every cache write failed. Now honours CLAUDE_CONFIG_DIR and expands $HOME/${HOME}/~ prefixes. --- LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh b/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh index c256405df..c8e1b7c4e 100755 --- a/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh +++ b/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh @@ -11,8 +11,18 @@ set -o pipefail # CONFIGURATION # ───────────────────────────────────────────────────────────────────────────── -LIFEOS_DIR="${LIFEOS_DIR:-$HOME/.claude/LIFEOS}" -CLAUDE_HOME="$HOME/.claude" +# CLAUDE_CONFIG_DIR is Claude Code's own config-dir override (multi-profile +# setups). Env values arriving from settings.json `env` are NOT shell-expanded +# by the harness, so a LIFEOS_DIR like `$HOME/.claude/LIFEOS` reaches us +# literally — expand the $HOME/${HOME}/~ prefix forms before use. +CLAUDE_HOME="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +CLAUDE_HOME="${CLAUDE_HOME/#\$HOME/$HOME}" +CLAUDE_HOME="${CLAUDE_HOME/#\$\{HOME\}/$HOME}" +CLAUDE_HOME="${CLAUDE_HOME/#\~/$HOME}" +LIFEOS_DIR="${LIFEOS_DIR:-$CLAUDE_HOME/LIFEOS}" +LIFEOS_DIR="${LIFEOS_DIR/#\$HOME/$HOME}" +LIFEOS_DIR="${LIFEOS_DIR/#\$\{HOME\}/$HOME}" +LIFEOS_DIR="${LIFEOS_DIR/#\~/$HOME}" SETTINGS_FILE="$CLAUDE_HOME/settings.json" RATINGS_FILE="$LIFEOS_DIR/MEMORY/LEARNING/SIGNALS/ratings.jsonl" MODEL_CACHE="$LIFEOS_DIR/MEMORY/STATE/model-cache.txt" From eae867134a7262cb1a916e9595b360c43f8f39ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Santos?= <4837+borfast@users.noreply.github.com> Date: Mon, 6 Jul 2026 02:15:28 +0100 Subject: [PATCH 9/9] refactor: extract expand_home() in the statusline instead of repeated substitutions --- LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh b/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh index c8e1b7c4e..37e826836 100755 --- a/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh +++ b/LifeOS/install/LIFEOS/LIFEOS_StatusLine.sh @@ -14,15 +14,17 @@ set -o pipefail # CLAUDE_CONFIG_DIR is Claude Code's own config-dir override (multi-profile # setups). Env values arriving from settings.json `env` are NOT shell-expanded # by the harness, so a LIFEOS_DIR like `$HOME/.claude/LIFEOS` reaches us -# literally — expand the $HOME/${HOME}/~ prefix forms before use. -CLAUDE_HOME="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" -CLAUDE_HOME="${CLAUDE_HOME/#\$HOME/$HOME}" -CLAUDE_HOME="${CLAUDE_HOME/#\$\{HOME\}/$HOME}" -CLAUDE_HOME="${CLAUDE_HOME/#\~/$HOME}" -LIFEOS_DIR="${LIFEOS_DIR:-$CLAUDE_HOME/LIFEOS}" -LIFEOS_DIR="${LIFEOS_DIR/#\$HOME/$HOME}" -LIFEOS_DIR="${LIFEOS_DIR/#\$\{HOME\}/$HOME}" -LIFEOS_DIR="${LIFEOS_DIR/#\~/$HOME}" +# literally. expand_home() resolves the $HOME/${HOME}/~ prefix spellings +# (bash port of the TS helpers' expandPath) — no eval, env values are data. +expand_home() { + local p="$1" + p="${p/#\$HOME/$HOME}" + p="${p/#\$\{HOME\}/$HOME}" + p="${p/#\~/$HOME}" + printf '%s' "$p" +} +CLAUDE_HOME="$(expand_home "${CLAUDE_CONFIG_DIR:-$HOME/.claude}")" +LIFEOS_DIR="$(expand_home "${LIFEOS_DIR:-$CLAUDE_HOME/LIFEOS}")" SETTINGS_FILE="$CLAUDE_HOME/settings.json" RATINGS_FILE="$LIFEOS_DIR/MEMORY/LEARNING/SIGNALS/ratings.jsonl" MODEL_CACHE="$LIFEOS_DIR/MEMORY/STATE/model-cache.txt"