diff --git a/Releases/v5.0.0/.claude/PAI/PULSE/MenuBar/install.sh b/Releases/v5.0.0/.claude/PAI/PULSE/MenuBar/install.sh index 9df3398be9..bc9e071882 100755 --- a/Releases/v5.0.0/.claude/PAI/PULSE/MenuBar/install.sh +++ b/Releases/v5.0.0/.claude/PAI/PULSE/MenuBar/install.sh @@ -6,6 +6,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" HOME_DIR="$HOME" +. "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/lib/paths.sh" APP_NAME="PAI Pulse" APP_DIR="$HOME_DIR/Applications" APP_DEST="$APP_DIR/$APP_NAME.app" @@ -68,7 +69,7 @@ sed "s|__HOME__|$HOME_DIR|g" "$PLIST_SRC" > "$PLIST_DST" echo " Installed $PLIST_DST" # Ensure logs directory exists -mkdir -p "$HOME_DIR/.claude/PAI/PULSE/logs" +mkdir -p "$(pai_path PULSE logs)" launchctl load "$PLIST_DST" echo " Loaded $PLIST_LABEL" diff --git a/Releases/v5.0.0/.claude/PAI/PULSE/Observability/observability.ts b/Releases/v5.0.0/.claude/PAI/PULSE/Observability/observability.ts index 984c1b4486..cb3e946e4d 100644 --- a/Releases/v5.0.0/.claude/PAI/PULSE/Observability/observability.ts +++ b/Releases/v5.0.0/.claude/PAI/PULSE/Observability/observability.ts @@ -27,6 +27,7 @@ import { join, extname } from "path" import { readFileSync, readdirSync, existsSync, realpathSync } from "fs" import YAML from "yaml" +import { getPaiDir } from "../../../hooks/lib/paths" // Bun is always the runtime here (Pulse launches this via `bun`). The Next // tsconfig's DOM+esnext lib doesn't include bun-types, so declare the minimal @@ -1646,7 +1647,7 @@ function readDirMdFiles(dir: string): { name: string, content: string, sections: function handleUserIndexApi(filter: string | null): Response { try { - const PAI_DIR = process.env.PAI_DIR || join(process.env.HOME || "", ".claude", "PAI") + const PAI_DIR = getPaiDir() const indexPath = join(PAI_DIR, "Pulse", "state", "user-index.json") const raw = Bun.file(indexPath) if (!raw.size) { diff --git a/Releases/v5.0.0/.claude/PAI/PULSE/checks/notification-governor.ts b/Releases/v5.0.0/.claude/PAI/PULSE/checks/notification-governor.ts index fb1a62ea36..88189cb1d7 100644 --- a/Releases/v5.0.0/.claude/PAI/PULSE/checks/notification-governor.ts +++ b/Releases/v5.0.0/.claude/PAI/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 { getPaiDir } from "../../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const STATE_FILE = join(PAI_DIR, "Pulse", "state", "notification-governor.json"); const LOG_FILE = join(PAI_DIR, "MEMORY", "OBSERVABILITY", "notification-governor.jsonl"); const NOTIFY_URL = "http://localhost:31337/notify"; diff --git a/Releases/v5.0.0/.claude/PAI/PULSE/checks/poller-meta-monitor.ts b/Releases/v5.0.0/.claude/PAI/PULSE/checks/poller-meta-monitor.ts index 5ca27d1c6d..56a4a62bc4 100644 --- a/Releases/v5.0.0/.claude/PAI/PULSE/checks/poller-meta-monitor.ts +++ b/Releases/v5.0.0/.claude/PAI/PULSE/checks/poller-meta-monitor.ts @@ -17,9 +17,9 @@ import { readFileSync, existsSync } from "fs"; import { join } from "path"; +import { getPaiDir } from "../../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const PULSE_STATE = join(PAI_DIR, "Pulse", "state", "state.json"); const PULSE_TOML = join(PAI_DIR, "Pulse", "PULSE.toml"); diff --git a/Releases/v5.0.0/.claude/PAI/PULSE/manage.sh b/Releases/v5.0.0/.claude/PAI/PULSE/manage.sh index 86a1bb7184..6085e18bcb 100755 --- a/Releases/v5.0.0/.claude/PAI/PULSE/manage.sh +++ b/Releases/v5.0.0/.claude/PAI/PULSE/manage.sh @@ -2,7 +2,8 @@ # PAI Pulse — Process Management # Usage: manage.sh {start|stop|restart|status|install|uninstall} -PULSE_DIR="$HOME/.claude/PAI/PULSE" +. "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/lib/paths.sh" +PULSE_DIR="$(pai_path PULSE)" PLIST_NAME="com.pai.pulse" PLIST_SRC="$PULSE_DIR/$PLIST_NAME.plist" PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_NAME.plist" diff --git a/Releases/v5.0.0/.claude/PAI/PULSE/modules/user-index.ts b/Releases/v5.0.0/.claude/PAI/PULSE/modules/user-index.ts index 7ee19c5013..2ff1ef2dd3 100644 --- a/Releases/v5.0.0/.claude/PAI/PULSE/modules/user-index.ts +++ b/Releases/v5.0.0/.claude/PAI/PULSE/modules/user-index.ts @@ -21,9 +21,9 @@ import { readFileSync, writeFileSync, statSync, readdirSync, mkdirSync, existsSync, watch } from "fs" import { join, relative, basename, dirname } from "path" +import { getPaiDir } from "../../../hooks/lib/paths" -const HOME = process.env.HOME ?? "" -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI") +const PAI_DIR = getPaiDir() const USER_DIR = join(PAI_DIR, "USER") const STATE_DIR = join(PAI_DIR, "Pulse", "state") const INDEX_PATH = join(STATE_DIR, "user-index.json") diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/AgentWatchdog.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/AgentWatchdog.ts index fc1039a7ae..252687aa55 100755 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/AgentWatchdog.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/AgentWatchdog.ts @@ -19,8 +19,9 @@ import { existsSync, readFileSync, statSync } from "fs"; import { join } from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; -const PAI_DIR = process.env.PAI_DIR || join(process.env.HOME!, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const OBS_DIR = join(PAI_DIR, "MEMORY", "OBSERVABILITY"); const ACTIVITY_FILE = join(OBS_DIR, "tool-activity.jsonl"); const STARTS_FILE = join(OBS_DIR, "subagent-starts.json"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/ApproveCurrentStateEntries.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/ApproveCurrentStateEntries.ts index 0905f245ae..c105a0924c 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/ApproveCurrentStateEntries.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/ApproveCurrentStateEntries.ts @@ -18,9 +18,9 @@ import { readFileSync, writeFileSync, existsSync } from "fs"; import { join } from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const QUEUE_FILE = join(PAI_DIR, "USER", "TELOS", "CURRENT_STATE", "proposals.jsonl"); const CURRENT_STATE_DIR = join(PAI_DIR, "USER", "TELOS", "CURRENT_STATE"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/ArchitectureSummaryGenerator.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/ArchitectureSummaryGenerator.ts index ad4d2671c3..ca3c345220 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/ArchitectureSummaryGenerator.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/ArchitectureSummaryGenerator.ts @@ -14,13 +14,14 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; // ============================================================================ // Configuration // ============================================================================ const HOME = process.env.HOME!; -const PAI_DIR = process.env.PAI_DIR || path.join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const ARCH_SOURCE = path.join(PAI_DIR, "DOCUMENTATION", "PAISystemArchitecture.md"); const SUMMARY_OUTPUT = path.join(PAI_DIR, "DOCUMENTATION", "ARCHITECTURE_SUMMARY.md"); const ALGORITHM_DIR = path.join(PAI_DIR, "ALGORITHM"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/Arthur.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/Arthur.ts index 6e9c23ad7c..a949d22b08 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/Arthur.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/Arthur.ts @@ -5,10 +5,10 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; import YAML from "yaml"; +import { getPaiDir } from "../../hooks/lib/paths"; -const PAI_DIR = process.env.PAI_DIR ?? join(homedir(), ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const POLICIES_PATH = join(PAI_DIR, "USER", "ARTHUR", "policies.yaml"); const GCP_PROJECT = process.env.PAI_GCP_PROJECT ?? ""; diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/ComputeGap.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/ComputeGap.ts index 9fc57e5eee..1f94c215c3 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/ComputeGap.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/ComputeGap.ts @@ -22,9 +22,9 @@ import { readFileSync, existsSync, appendFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const IDEAL_DIR = join(PAI_DIR, "USER", "TELOS", "IDEAL_STATE"); const CURRENT_DIR = join(PAI_DIR, "USER", "TELOS", "CURRENT_STATE"); const HEALTH_DIR = join(PAI_DIR, "USER", "HEALTH"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/FailureCapture.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/FailureCapture.ts index 9b8d9c543e..6e1af94108 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/FailureCapture.ts +++ b/Releases/v5.0.0/.claude/PAI/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 { getPaiDir } from '../../hooks/lib/paths'; -const PAI_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude'); +const PAI_DIR = getPaiDir(); interface FailureCaptureInput { transcriptPath: string; diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/GetCounts.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/GetCounts.ts index 4ae27e4593..f3cb5b1d0f 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/GetCounts.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/GetCounts.ts @@ -35,9 +35,13 @@ import { readdirSync, existsSync, statSync } from "fs"; import { join } from "path"; +import { getPaiDir, getClaudeDir } from "../../hooks/lib/paths"; const HOME = process.env.HOME!; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude"); +// PAI data (MEMORY, USER) lives under getPaiDir() (~/.claude/PAI); +// skills/ live under Claude home (getClaudeDir(), ~/.claude). +const PAI_DIR = getPaiDir(); +const CLAUDE_DIR = getClaudeDir(); interface Counts { skills: number; @@ -101,7 +105,7 @@ function countWorkflowFiles(dir: string): number { */ function countSkills(): number { let count = 0; - const skillsDir = join(PAI_DIR, "skills"); + const skillsDir = join(CLAUDE_DIR, "skills"); try { for (const entry of readdirSync(skillsDir, { withFileTypes: true })) { // Handle both real directories and symlinks to directories @@ -169,10 +173,10 @@ function countRatings(): number { function getCounts(): Counts { return { skills: countSkills(), - workflows: countWorkflowFiles(join(PAI_DIR, "skills")), + workflows: countWorkflowFiles(join(CLAUDE_DIR, "skills")), hooks: countHooks(), signals: countFilesRecursive(join(PAI_DIR, "MEMORY/LEARNING"), ".md"), - files: countFilesRecursive(join(PAI_DIR, "PAI/USER")), + files: countFilesRecursive(join(PAI_DIR, "USER")), work: (() => { let count = 0; try { diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/IntegrityMaintenance.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/IntegrityMaintenance.ts index 94c36d4ab1..cba2793f0f 100755 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/IntegrityMaintenance.ts +++ b/Releases/v5.0.0/.claude/PAI/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 '../../../.claude/hooks/lib/identity'; +import { getClaudeDir } from '../../../.claude/hooks/lib/paths'; // ============================================================================ // Types @@ -108,8 +109,11 @@ interface UpdateData { // Constants // ============================================================================ -const PAI_DIR = process.env.HOME + '/.claude/PAI'; -const CREATE_UPDATE_SCRIPT = join(PAI_DIR, 'skills/_PAI/TOOLS/CreateUpdate.ts'); +// skills/ lives under Claude home, not PAI data dir. (Pre-existing: the +// referenced skills/_PAI/TOOLS/CreateUpdate.ts does not exist in the tree — +// see NOTE D in the fix spec; flagged separately, not resolved here.) +const CLAUDE_DIR = getClaudeDir(); +const CREATE_UPDATE_SCRIPT = join(CLAUDE_DIR, 'skills/_PAI/TOOLS/CreateUpdate.ts'); // Words that indicate generic/bad titles - reject these const GENERIC_TITLE_PATTERNS = [ diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/InterviewIdealState.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/InterviewIdealState.ts index bc7a1a3d84..ac92f734fa 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/InterviewIdealState.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/InterviewIdealState.ts @@ -17,9 +17,9 @@ import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs"; import { join } from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const TELOS_DIR = join(PAI_DIR, "USER", "TELOS"); const IDEAL_DIR = join(TELOS_DIR, "IDEAL_STATE"); const STATE_FILE = join(PAI_DIR, "USER", "TELOS", "CURRENT_STATE", "interview-state.json"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/InterviewScan.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/InterviewScan.ts index 0ad03f2da9..7761930bf1 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/InterviewScan.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/InterviewScan.ts @@ -18,9 +18,9 @@ import { readFileSync, existsSync } from "fs"; import { join } from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const USER_DIR = join(PAI_DIR, "USER"); const TELOS_DIR = join(USER_DIR, "TELOS"); const IDEAL_DIR = join(TELOS_DIR, "IDEAL_STATE"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeGraph.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeGraph.ts index 6b894c512e..83bb60e2f1 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeGraph.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeGraph.ts @@ -26,13 +26,13 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; // ============================================================================ // Configuration // ============================================================================ -const HOME = process.env.HOME!; -const PAI_DIR = process.env.PAI_DIR || path.join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const KNOWLEDGE_DIR = path.join(PAI_DIR, "MEMORY", "KNOWLEDGE"); const DOMAINS = ["People", "Companies", "Ideas", "Research"]; const SKIP_FILES = new Set(["_index.md", "_schema.md", "_log.md"]); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeHarvester.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeHarvester.ts index bc5e90551a..f9799fd800 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeHarvester.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeHarvester.ts @@ -21,13 +21,14 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; // ============================================================================ // Configuration // ============================================================================ const HOME = process.env.HOME!; -const PAI_DIR = process.env.PAI_DIR || path.join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const MEMORY_DIR = path.join(PAI_DIR, "MEMORY"); const KNOWLEDGE_DIR = path.join(MEMORY_DIR, "KNOWLEDGE"); const WORK_DIR = path.join(MEMORY_DIR, "WORK"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/MemoryRetriever.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/MemoryRetriever.ts index f3559103c7..ddd65d1df0 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/MemoryRetriever.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/MemoryRetriever.ts @@ -35,13 +35,13 @@ import { parseArgs } from "util"; import * as fs from "fs"; import * as path from "path"; import { spawnSync } from "child_process"; +import { getPaiDir } from "../../hooks/lib/paths"; // ============================================================================ // Configuration // ============================================================================ -const HOME = process.env.HOME!; -const PAI_DIR = process.env.PAI_DIR || path.join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const KNOWLEDGE_DIR = path.join(PAI_DIR, "MEMORY", "KNOWLEDGE"); const DOMAINS = ["People", "Companies", "Ideas", "Research"]; diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/MigrateApprove.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/MigrateApprove.ts index 770f1ca874..29929dad48 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/MigrateApprove.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/MigrateApprove.ts @@ -24,9 +24,10 @@ import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const QUEUE_FILE = join(PAI_DIR, "MEMORY", "MIGRATION", "migration-proposals.jsonl"); const COMMITTED_LOG = join(PAI_DIR, "MEMORY", "MIGRATION", "committed.jsonl"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/MigrateScan.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/MigrateScan.ts index 87955a176a..66749052a0 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/MigrateScan.ts +++ b/Releases/v5.0.0/.claude/PAI/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 { getPaiDir } from "../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const QUEUE_FILE = join(PAI_DIR, "MEMORY", "MIGRATION", "migration-proposals.jsonl"); type Target = diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/OpinionTracker.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/OpinionTracker.ts index 37c4d678df..4d73502285 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/OpinionTracker.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/OpinionTracker.ts @@ -23,9 +23,10 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { join } from 'path'; +import { getPaiDir } from '../../hooks/lib/paths'; -const PAI_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude'); -const OPINIONS_FILE = join(PAI_DIR, 'PAI/USER/OPINIONS.md'); +const PAI_DIR = getPaiDir(); +const OPINIONS_FILE = join(PAI_DIR, 'USER/OPINIONS.md'); const RELATIONSHIP_LOG = join(PAI_DIR, 'MEMORY/RELATIONSHIP'); interface Evidence { diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/ProposeCurrentStateEntry.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/ProposeCurrentStateEntry.ts index 963024b49e..67d1792d20 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/ProposeCurrentStateEntry.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/ProposeCurrentStateEntry.ts @@ -20,9 +20,9 @@ import { appendFileSync, mkdirSync, existsSync } from "fs"; import { join, dirname } from "path"; import { randomUUID } from "crypto"; +import { getPaiDir } from "../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const QUEUE_FILE = join(PAI_DIR, "USER", "TELOS", "CURRENT_STATE", "proposals.jsonl"); const ALLOWED_SOURCES = ["lifelog", "calendar", "gmail", "homebridge", "manual", "amazon", "bills"]; diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/Recommend.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/Recommend.ts index f48f97811c..b7ac826e11 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/Recommend.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/Recommend.ts @@ -16,9 +16,9 @@ import { readFileSync, existsSync } from "fs"; import { join } from "path"; +import { getPaiDir } from "../../hooks/lib/paths"; -const HOME = process.env.HOME || ""; -const PAI_DIR = process.env.PAI_DIR || join(HOME, ".claude", "PAI"); +const PAI_DIR = getPaiDir(); const TELOS_DIR = join(PAI_DIR, "USER", "TELOS"); const CURRENT_DIR = join(TELOS_DIR, "CURRENT_STATE"); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/RelationshipReflect.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/RelationshipReflect.ts index a7fc88b16e..769461137a 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/RelationshipReflect.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/RelationshipReflect.ts @@ -28,8 +28,9 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'fs'; import { join } from 'path'; import { execSync } from 'child_process'; +import { getPaiDir } from '../../hooks/lib/paths'; -const PAI_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude'); +const PAI_DIR = getPaiDir(); interface RelationshipNote { type: 'W' | 'B' | 'O'; @@ -265,7 +266,7 @@ function aggregateEvidence(notes: RelationshipNote[], ratings: Array<{ rating: n */ function parseOpinions(): Map { const opinions = new Map(); - const opinionsPath = join(PAI_DIR, 'PAI/USER/OPINIONS.md'); + const opinionsPath = join(PAI_DIR, 'USER/OPINIONS.md'); if (!existsSync(opinionsPath)) return opinions; @@ -297,7 +298,7 @@ function updateOpinionConfidence( evidence: Map, dryRun: boolean ): { updated: number; majorShifts: string[] } { - const opinionsPath = join(PAI_DIR, 'PAI/USER/OPINIONS.md'); + const opinionsPath = join(PAI_DIR, 'USER/OPINIONS.md'); if (!existsSync(opinionsPath)) return { updated: 0, majorShifts: [] }; let content = readFileSync(opinionsPath, 'utf-8'); @@ -361,7 +362,7 @@ function escapeRegex(str: string): string { */ function checkMilestones(notes: RelationshipNote[]): string[] { const achieved: string[] = []; - const storyPath = join(PAI_DIR, 'PAI/USER/OUR_STORY.md'); + const storyPath = join(PAI_DIR, 'USER/OUR_STORY.md'); if (!existsSync(storyPath)) return achieved; @@ -384,7 +385,7 @@ function checkMilestones(notes: RelationshipNote[]): string[] { * Add milestone to OUR_STORY.md */ function addMilestone(description: string, dryRun: boolean): boolean { - const storyPath = join(PAI_DIR, 'PAI/USER/OUR_STORY.md'); + const storyPath = join(PAI_DIR, 'USER/OUR_STORY.md'); if (!existsSync(storyPath)) return false; let content = readFileSync(storyPath, 'utf-8'); diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomCrossFrameSynthesizer.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomCrossFrameSynthesizer.ts index 6b36926a9c..1fc6ff7ab6 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomCrossFrameSynthesizer.ts +++ b/Releases/v5.0.0/.claude/PAI/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 { getPaiDir } from '../../hooks/lib/paths'; -const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = getPaiDir(); 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/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomDomainClassifier.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomDomainClassifier.ts index 7c1c42bf85..68b79942ff 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomDomainClassifier.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomDomainClassifier.ts @@ -16,8 +16,9 @@ import { existsSync, readdirSync, readFileSync } from 'fs'; import { join, basename } from 'path'; import { parseArgs } from 'util'; +import { getPaiDir } from '../../hooks/lib/paths'; -const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = getPaiDir(); const FRAMES_DIR = join(BASE_DIR, 'MEMORY', 'WISDOM', 'FRAMES'); // ── Domain Keyword Map ── diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomFrameUpdater.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomFrameUpdater.ts index aa2e39e2c9..14e69d7e85 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomFrameUpdater.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/WisdomFrameUpdater.ts @@ -18,8 +18,9 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { parseArgs } from 'util'; +import { getPaiDir } from '../../hooks/lib/paths'; -const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude'); +const BASE_DIR = getPaiDir(); const FRAMES_DIR = join(BASE_DIR, 'MEMORY', 'WISDOM', 'FRAMES'); // ── Types ── diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/algorithm.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/algorithm.ts index b77bcf413e..38a96fe7cf 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/algorithm.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/algorithm.ts @@ -51,14 +51,14 @@ 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 { getPaiDir, getProjectsDir } from "../../../.claude/hooks/lib/paths"; // ─── Paths ─────────────────────────────────────────────────────────────────── -const HOME = process.env.HOME || "~"; -const BASE_DIR = process.env.PAI_DIR || join(HOME, ".claude"); +const BASE_DIR = getPaiDir(); 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"); +const PROJECTS_DIR = getProjectsDir(); const VOICE_URL = "http://localhost:31337/notify"; const VOICE_ID = "fTtv3eikoepIosk8dTZ5"; diff --git a/Releases/v5.0.0/.claude/PAI/statusline-command.sh b/Releases/v5.0.0/.claude/PAI/statusline-command.sh index 68bce90cf1..7197093180 100755 --- a/Releases/v5.0.0/.claude/PAI/statusline-command.sh +++ b/Releases/v5.0.0/.claude/PAI/statusline-command.sh @@ -11,9 +11,11 @@ set -o pipefail # CONFIGURATION # ───────────────────────────────────────────────────────────────────────────── -PAI_DIR="${PAI_DIR:-$HOME/.claude/PAI}" -CLAUDE_HOME="$HOME/.claude" -SETTINGS_FILE="$CLAUDE_HOME/settings.json" +# Path resolution via shared helper (mirrors hooks/lib/paths.ts contract) +. "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/lib/paths.sh" +CLAUDE_HOME="$(get_claude_dir)" +PAI_DIR="$(get_pai_dir)" +SETTINGS_FILE="$(get_settings_path)" RATINGS_FILE="$PAI_DIR/MEMORY/LEARNING/SIGNALS/ratings.jsonl" MODEL_CACHE="$PAI_DIR/MEMORY/STATE/model-cache.txt" QUOTE_CACHE="$PAI_DIR/.quote-cache" @@ -59,7 +61,7 @@ PAI_VERSION="${PAI_VERSION:-—}" # multiple candidate paths in order, keeping the first non-empty result. ALGO_VERSION="" for _algo_path in \ - "$PAI_DIR/ALGORITHM/LATEST" \ + "$(pai_path ALGORITHM LATEST)" \ "$HOME/.claude/PAI/ALGORITHM/LATEST" \ "/Users/$(id -un 2>/dev/null)/.claude/PAI/ALGORITHM/LATEST" \ "$(eval echo ~"$(id -un 2>/dev/null)")/.claude/PAI/ALGORITHM/LATEST"; do @@ -73,7 +75,7 @@ done printf '[%s] ALGO_VERSION=%q HOME=%q PAI_DIR=%q USER=%q paths_tried:' \ "$(date '+%H:%M:%S')" "$ALGO_VERSION" "${HOME:-UNSET}" "${PAI_DIR:-UNSET}" "${USER:-UNSET}" for _algo_path in \ - "$PAI_DIR/ALGORITHM/LATEST" \ + "$(pai_path ALGORITHM LATEST)" \ "$HOME/.claude/PAI/ALGORITHM/LATEST" \ "/Users/$(id -un 2>/dev/null)/.claude/PAI/ALGORITHM/LATEST"; do printf ' %s=%s' "$_algo_path" "$([ -f "$_algo_path" ] && echo OK || echo MISS)" @@ -273,7 +275,7 @@ if [ "$context_pct" = "0" ] && [ "$total_input" -eq 0 ] 2>/dev/null; then done < <(jq -r '.loadAtStartup.files[]? // empty' "$SETTINGS_FILE" 2>/dev/null) # Project memory files (CC native memory at ~/.claude/projects/*/memory/) - for _f in "$HOME"/.claude/projects/*/memory/MEMORY.md; do + for _f in "$(get_claude_dir)"/projects/*/memory/MEMORY.md; do [ -f "$_f" ] && _est=$((_est + $(wc -c < "$_f") * 10 / 35)) done @@ -694,7 +696,7 @@ USAGEEOF if [ "$(uname -s)" = "Darwin" ]; then cred_json=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) else - cred_json=$(cat "${HOME}/.claude/.credentials.json" 2>/dev/null) + cred_json=$(cat "$(get_claude_dir)/.credentials.json" 2>/dev/null) fi token=$(echo "$cred_json" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) diff --git a/Releases/v5.0.0/.claude/hooks/InstructionsLoadedHandler.hook.ts b/Releases/v5.0.0/.claude/hooks/InstructionsLoadedHandler.hook.ts index 17e63d4d80..a1299ae63f 100755 --- a/Releases/v5.0.0/.claude/hooks/InstructionsLoadedHandler.hook.ts +++ b/Releases/v5.0.0/.claude/hooks/InstructionsLoadedHandler.hook.ts @@ -25,13 +25,14 @@ import { existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; +import { getPaiDir } from './lib/paths'; // ======================================== // Configuration // ======================================== const HOME = homedir(); -const PAI_DIR = process.env.PAI_DIR || join(HOME, '.claude', 'PAI'); +const PAI_DIR = getPaiDir(); const STATE_DIR = join(PAI_DIR, 'MEMORY', 'STATE'); const HASHES_FILE = join(STATE_DIR, 'instruction-hashes.json'); const INTEGRITY_LOG = join(STATE_DIR, 'instruction-integrity.jsonl'); diff --git a/Releases/v5.0.0/.claude/hooks/LastResponseCache.hook.ts b/Releases/v5.0.0/.claude/hooks/LastResponseCache.hook.ts index 9d2e7bf599..ad1fe77afd 100755 --- a/Releases/v5.0.0/.claude/hooks/LastResponseCache.hook.ts +++ b/Releases/v5.0.0/.claude/hooks/LastResponseCache.hook.ts @@ -14,6 +14,7 @@ import { readHookInput, parseTranscriptFromInput } from './lib/hook-io'; import { writeFileSync } from 'fs'; import { join } from 'path'; +import { getPaiDir } from './lib/paths'; async function main() { const input = await readHookInput(); @@ -28,7 +29,7 @@ async function main() { if (lastResponse) { try { - const paiDir = process.env.PAI_DIR || join(process.env.HOME!, '.claude', 'PAI'); + const paiDir = getPaiDir(); const cachePath = join(paiDir, 'MEMORY', 'STATE', 'last-response.txt'); writeFileSync(cachePath, lastResponse.slice(0, 2000), 'utf-8'); } catch (err) { diff --git a/Releases/v5.0.0/.claude/hooks/PreCompact.hook.ts b/Releases/v5.0.0/.claude/hooks/PreCompact.hook.ts index c981d1f6b6..64d826dbeb 100755 --- a/Releases/v5.0.0/.claude/hooks/PreCompact.hook.ts +++ b/Releases/v5.0.0/.claude/hooks/PreCompact.hook.ts @@ -27,8 +27,9 @@ import { existsSync, readFileSync, readdirSync } from 'fs'; import { join, basename } from 'path'; import { findArtifactPath } from './lib/isa-utils'; +import { getPaiDir } from './lib/paths'; -const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude', 'PAI'); +const BASE_DIR = getPaiDir(); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const STATE_DIR = join(MEMORY_DIR, 'STATE'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); diff --git a/Releases/v5.0.0/.claude/hooks/PromptProcessing.hook.ts b/Releases/v5.0.0/.claude/hooks/PromptProcessing.hook.ts index 0dac71f630..5abc21db17 100755 --- a/Releases/v5.0.0/.claude/hooks/PromptProcessing.hook.ts +++ b/Releases/v5.0.0/.claude/hooks/PromptProcessing.hook.ts @@ -31,7 +31,7 @@ import { inference } from '../PAI/TOOLS/Inference'; import { getIdentity, getPrincipal } from './lib/identity'; import { isValidWorkingTitle, getWorkingFallback, trimToValidTitle } from './lib/output-validators'; import { setTabState, getSessionOneWord } from './lib/tab-setter'; -import { paiPath } from './lib/paths'; +import { paiPath, getPaiDir } from './lib/paths'; import { updateSessionNameInWorkJson, upsertSession } from './lib/isa-utils'; import { pushStateToTargets } from './lib/observability-transport'; @@ -75,7 +75,7 @@ function appendPromptProcessingTelemetry(entry: Record): void { // ── Constants ── -const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude', 'PAI'); +const BASE_DIR = getPaiDir(); 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/Releases/v5.0.0/.claude/hooks/SatisfactionCapture.hook.ts b/Releases/v5.0.0/.claude/hooks/SatisfactionCapture.hook.ts index 42c5845378..c36a8ac314 100755 --- a/Releases/v5.0.0/.claude/hooks/SatisfactionCapture.hook.ts +++ b/Releases/v5.0.0/.claude/hooks/SatisfactionCapture.hook.ts @@ -31,6 +31,7 @@ import { getLearningCategory } from './lib/learning-utils'; import { getISOTimestamp, getPSTComponents } from './lib/time'; import { captureFailure } from '../PAI/TOOLS/FailureCapture'; import { addRatingPulse } from './lib/isa-utils'; +import { getPaiDir } from './lib/paths'; // ── Types ── @@ -63,7 +64,7 @@ interface SentimentResult { // ── Constants ── -const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude', 'PAI'); +const BASE_DIR = getPaiDir(); 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/Releases/v5.0.0/.claude/hooks/SessionCleanup.hook.ts b/Releases/v5.0.0/.claude/hooks/SessionCleanup.hook.ts index 87e1adaaa0..bc10b9ab56 100755 --- a/Releases/v5.0.0/.claude/hooks/SessionCleanup.hook.ts +++ b/Releases/v5.0.0/.claude/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 } from './lib/isa-utils'; +import { getPaiDir } from './lib/paths'; -const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude', 'PAI'); +const BASE_DIR = getPaiDir(); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const STATE_DIR = join(MEMORY_DIR, 'STATE'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); diff --git a/Releases/v5.0.0/.claude/hooks/WorkCompletionLearning.hook.ts b/Releases/v5.0.0/.claude/hooks/WorkCompletionLearning.hook.ts index fe62f8923e..2e8054519c 100755 --- a/Releases/v5.0.0/.claude/hooks/WorkCompletionLearning.hook.ts +++ b/Releases/v5.0.0/.claude/hooks/WorkCompletionLearning.hook.ts @@ -54,8 +54,9 @@ import { join, dirname } from 'path'; import { getISOTimestamp, getPSTDate } from './lib/time'; import { getLearningCategory } from './lib/learning-utils'; import { findArtifactPath } from './lib/isa-utils'; +import { getPaiDir } from './lib/paths'; -const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude', 'PAI'); +const BASE_DIR = getPaiDir(); const MEMORY_DIR = join(BASE_DIR, 'MEMORY'); const STATE_DIR = join(MEMORY_DIR, 'STATE'); const WORK_DIR = join(MEMORY_DIR, 'WORK'); diff --git a/Releases/v5.0.0/.claude/hooks/handlers/UpdateCounts.ts b/Releases/v5.0.0/.claude/hooks/handlers/UpdateCounts.ts index 702c529e81..cb8e3e926a 100755 --- a/Releases/v5.0.0/.claude/hooks/handlers/UpdateCounts.ts +++ b/Releases/v5.0.0/.claude/hooks/handlers/UpdateCounts.ts @@ -171,7 +171,7 @@ function getCounts(paiDir: string): Counts { workflows: countWorkflowFiles(join(getClaudeDir(), 'skills')), hooks: countHooks(paiDir), signals: countFilesRecursive(join(paiDir, 'MEMORY/LEARNING'), '.md'), - files: countFilesRecursive(join(paiDir, 'PAI/USER')), + files: countFilesRecursive(join(paiDir, 'USER')), work: countSubdirs(join(paiDir, 'MEMORY/WORK')), sessions: countFilesRecursive(join(paiDir, 'MEMORY'), '.jsonl'), research: countFilesRecursive(join(paiDir, 'MEMORY/RESEARCH'), '.md') + diff --git a/Releases/v5.0.0/.claude/hooks/lib/paths.sh b/Releases/v5.0.0/.claude/hooks/lib/paths.sh new file mode 100644 index 0000000000..a5b0c7f61b --- /dev/null +++ b/Releases/v5.0.0/.claude/hooks/lib/paths.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Centralized Path Resolution — bash mirror of hooks/lib/paths.ts +# +# Two root directories: +# - Claude home (~/.claude or $CLAUDE_CONFIG_DIR) — Claude Code: settings, +# skills, hooks, commands, agents +# - PAI_DIR (subpath inside Claude home, default 'PAI') — PAI data: MEMORY, +# Algorithm, Tools, USER +# +# Sourcing (single form, used everywhere — including inside hooks/): +# . "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/lib/paths.sh" +# The bootstrap line IS the resolution it implements: same CLAUDE_CONFIG_DIR +# fallback contract, so multi-account configs keep working at source time. +# +# Functions return paths via stdout. assert_absolute prints to stderr and +# returns 1 on failure; callers decide whether to exit. +# ───────────────────────────────────────────────────────────────────────────── + +# Idempotent sourcing guard +[ -n "${_PAI_PATHS_SH_LOADED:-}" ] && return 0 +_PAI_PATHS_SH_LOADED=1 + +# expand_path: expand leading $HOME / ${HOME} / ~ to actual $HOME. +# settings.json env values are stored literally; bash callers must expand. +expand_path() { + local p="$1" + p="${p/#\$\{HOME\}/$HOME}" + p="${p/#\$HOME/$HOME}" + p="${p/#\~/$HOME}" + printf '%s\n' "$p" +} + +# assert_absolute: enforce that a resolved directory is absolute. +# Mirrors paths.ts assertAbsolute(). Prints to stderr and returns 1 on fail. +assert_absolute() { + local dir="$1" source="$2" + if [ "${dir#/}" = "${dir}" ]; then + printf '[paths.sh] %s resolved to a non-absolute path: "%s" (env value likely unexpanded). cwd=%s\n' \ + "$source" "$dir" "$PWD" >&2 + return 1 + fi + printf '%s\n' "$dir" +} + +# get_claude_dir: CLAUDE_CONFIG_DIR (expanded) → $HOME/.claude +get_claude_dir() { + local dir + if [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then + dir="$(expand_path "$CLAUDE_CONFIG_DIR")" + else + dir="$HOME/.claude" + fi + assert_absolute "$dir" "get_claude_dir" +} + +# get_pai_dir: PAI_DIR is a SUBPATH relative to Claude home, never absolute. +# join(get_claude_dir(), PAI_DIR ?? 'PAI'). Mirrors paths.ts getPaiDir(). +get_pai_dir() { + local sub="${PAI_DIR:-PAI}" + # Treat whitespace-only as default + [ -z "${sub// /}" ] && sub="PAI" + local claude_dir + claude_dir="$(get_claude_dir)" || return 1 + assert_absolute "${claude_dir}/${sub}" "get_pai_dir" +} + +# get_projects_dir: PROJECTS_DIR (expanded) → $HOME/Projects +get_projects_dir() { + local dir + if [ -n "${PROJECTS_DIR:-}" ]; then + dir="$(expand_path "$PROJECTS_DIR")" + else + dir="$HOME/Projects" + fi + assert_absolute "$dir" "get_projects_dir" +} + +# get_settings_path: settings.json lives in Claude home +get_settings_path() { + local claude_dir + claude_dir="$(get_claude_dir)" || return 1 + printf '%s/settings.json\n' "$claude_dir" +} + +# get_env_path: authoritative .env at Claude home root +get_env_path() { + local claude_dir + claude_dir="$(get_claude_dir)" || return 1 + printf '%s/.env\n' "$claude_dir" +} + +# pai_path: join args under PAI_DIR. pai_path "MEMORY" "STATE" → $PAI/MEMORY/STATE +pai_path() { + local base + base="$(get_pai_dir)" || return 1 + local out="$base" + local seg + for seg in "$@"; do + out="${out}/${seg}" + done + printf '%s\n' "$out" +} + +# get_hooks_dir / get_skills_dir / get_memory_dir +get_hooks_dir() { + local claude_dir + claude_dir="$(get_claude_dir)" || return 1 + printf '%s/hooks\n' "$claude_dir" +} + +get_skills_dir() { + local claude_dir + claude_dir="$(get_claude_dir)" || return 1 + printf '%s/skills\n' "$claude_dir" +} + +get_memory_dir() { + pai_path "MEMORY" +} diff --git a/Releases/v5.0.0/.claude/hooks/lib/paths.ts b/Releases/v5.0.0/.claude/hooks/lib/paths.ts index 6f7d82ee32..9e6a6713e3 100755 --- a/Releases/v5.0.0/.claude/hooks/lib/paths.ts +++ b/Releases/v5.0.0/.claude/hooks/lib/paths.ts @@ -26,24 +26,74 @@ export function expandPath(path: string): string { } /** - * Get the PAI data directory (expanded) - * Priority: PAI_DIR env var (expanded) → ~/.claude/PAI + * Enforce that a resolved directory is absolute. + * + * settings.json `env` values are stored literally — Claude Code does not + * shell-expand them. A path-like env var such as `${HOME}/.claude/PAI` that is + * never expanded is non-absolute, and `path.join` silently rebases it onto + * `process.cwd()`, scattering writes into whatever directory the session was + * launched from. Fail loudly here instead of misrouting silently. */ -export function getPaiDir(): string { - const envPaiDir = process.env.PAI_DIR; - - if (envPaiDir) { - return expandPath(envPaiDir); +function assertAbsolute(dir: string, source: string): string { + if (!dir.startsWith('/')) { + throw new Error( + `[paths] ${source} resolved to a non-absolute path: "${dir}" ` + + `(env value likely unexpanded). cwd=${process.cwd()}`, + ); } - - return join(homedir(), '.claude', 'PAI'); + return dir; } /** - * Get the Claude Code home directory (~/.claude) + * Get the Claude Code home directory. + * Priority: CLAUDE_CONFIG_DIR env var (expanded) → ~/.claude + * + * `CLAUDE_CONFIG_DIR` is the official Claude Code override for the config + * directory (settings, credentials, session history, plugins) — e.g. for + * running multiple accounts side by side. Honor it; fall back to ~/.claude. */ export function getClaudeDir(): string { - return join(homedir(), '.claude'); + const override = process.env.CLAUDE_CONFIG_DIR; + const dir = override ? expandPath(override) : join(homedir(), '.claude'); + return assertAbsolute(dir, 'getClaudeDir'); +} + +/** + * Get the PAI data directory. + * + * PAI_DIR is, by definition, a SUBPATH relative to the Claude home directory — + * never an absolute path. getPaiDir() ≡ join(getClaudeDir(), PAI_DIR ?? 'PAI'). + * + * This makes "PAI data lives inside Claude home" a structural invariant rather + * than a coincidence of an env value, so a CLAUDE_CONFIG_DIR override + * propagates to PAI data automatically. The env var remains usable for + * swapping PAI dirs (e.g. PAI_DIR='experiments/PAI-v2'), always rooted in + * Claude home. + * + * NOTE: an absolute or ${HOME}-style PAI_DIR is NOT supported by design. Such a + * value joins as a literal subpath and the absolute backstop below will catch + * a non-absolute result only if getClaudeDir() itself is broken; a stray + * absolute-looking PAI_DIR instead yields an obviously-wrong visible path + * (not a silent cwd rebase). settings.json must set PAI_DIR='PAI' (or omit it). + */ +export function getPaiDir(): string { + // Treat empty/unset PAI_DIR as "use default" — `??` alone would let + // PAI_DIR='' collapse PAI data into Claude home. + const sub = process.env.PAI_DIR?.trim() || 'PAI'; + const dir = join(getClaudeDir(), sub); + return assertAbsolute(dir, 'getPaiDir'); +} + +/** + * Get the user's projects directory (expanded) + * Priority: PROJECTS_DIR env var (expanded) → ~/Projects + * + * Companion to getPaiDir(): same literal-env hazard, same resolution contract. + */ +export function getProjectsDir(): string { + const env = process.env.PROJECTS_DIR; + const dir = env ? expandPath(env) : join(homedir(), 'Projects'); + return assertAbsolute(dir, 'getProjectsDir'); } /** diff --git a/Releases/v5.0.0/.claude/settings.json b/Releases/v5.0.0/.claude/settings.json index 90f7f54b84..8376e21747 100644 --- a/Releases/v5.0.0/.claude/settings.json +++ b/Releases/v5.0.0/.claude/settings.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-settings.json", "env": { - "PAI_DIR": "${HOME}/.claude/PAI", + "PAI_DIR": "PAI", "PROJECTS_DIR": "${HOME}/Projects", "BASH_DEFAULT_TIMEOUT_MS": "600000", "API_TIMEOUT_MS": "1800000", @@ -1299,7 +1299,7 @@ }, "_httpHooks": "Pulse HTTP routes on pai:31337 (managed by Pulse process). SecurityPipeline: command hook on Bash/Write/Edit/Read (exit(2) hard-block). SkillGuard + AgentGuard: HTTP hooks via Pulse routes (fail-open).", "_env": { - "PAI_DIR": "Root directory for PAI data (~/.claude/PAI). Memory, Algorithm, Tools, USER config. Hooks and skills stay in ~/.claude.", + "PAI_DIR": "PAI data subdirectory relative to the Claude home dir (default 'PAI' → ~/.claude/PAI, or $CLAUDE_CONFIG_DIR/PAI). A subpath, not an absolute path. Memory, Algorithm, Tools, USER config. Hooks and skills stay in Claude home.", "PROJECTS_DIR": "Base directory for projects. Use ${PROJECTS_DIR}/PAI for PAI repo, ${PROJECTS_DIR}/fabric for fabric, etc.", "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "Maximum tokens for Claude responses. Higher values allow longer outputs but cost more.", "BASH_DEFAULT_TIMEOUT_MS": "Default timeout for bash commands in milliseconds."