diff --git a/.claude/agents/agentica-agent.md b/.claude/agents/agentica-agent.md index b520a1da..c92fa758 100644 --- a/.claude/agents/agentica-agent.md +++ b/.claude/agents/agentica-agent.md @@ -14,7 +14,7 @@ You are a specialized agent for building Python agents using the Agentica SDK. Y Before starting, read the SDK skill for full API reference: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/agentica-sdk/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/agentica-sdk/SKILL.md ``` ## Step 2: Understand Your Task diff --git a/.claude/agents/braintrust-analyst.md b/.claude/agents/braintrust-analyst.md index 046859fe..e8ac8f6a 100644 --- a/.claude/agents/braintrust-analyst.md +++ b/.claude/agents/braintrust-analyst.md @@ -19,7 +19,7 @@ You are a specialized analysis agent. Your job is to run Braintrust analysis scr Read the braintrust-analyze skill: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/braintrust-analyze/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/braintrust-analyze/SKILL.md ``` ## Step 2: Execute Analysis @@ -27,7 +27,7 @@ cat $CLAUDE_PROJECT_DIR/.claude/skills/braintrust-analyze/SKILL.md Run analysis IMMEDIATELY using Bash tool: ```bash -cd $CLAUDE_PROJECT_DIR && uv run python -m runtime.harness scripts/braintrust_analyze.py --last-session +cd $CLAUDE_OPC_DIR && uv run python -m runtime.harness scripts/braintrust_analyze.py --last-session ``` Other analyses (run as needed): diff --git a/.claude/agents/debug-agent.md b/.claude/agents/debug-agent.md index a27a2e0c..92ffab8e 100644 --- a/.claude/agents/debug-agent.md +++ b/.claude/agents/debug-agent.md @@ -13,7 +13,7 @@ You are a specialized debugging agent. Your job is to investigate issues, trace Before starting, read the debug skill for methodology: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/debug/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/debug/SKILL.md ``` Follow the structure and guidelines from that skill. diff --git a/.claude/agents/memory-extractor.md b/.claude/agents/memory-extractor.md index c20edd25..aa59f791 100644 --- a/.claude/agents/memory-extractor.md +++ b/.claude/agents/memory-extractor.md @@ -31,7 +31,7 @@ You receive: ```bash # Use the extraction script with filtering -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/extract_thinking_blocks.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/extract_thinking_blocks.py \ --jsonl "$JSONL_PATH" \ --filter \ --format json) > /tmp/perception-blocks.json @@ -42,7 +42,7 @@ This extracts only thinking blocks containing perception signals (actually, real ### Step 2: Check Stats ```bash -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/extract_thinking_blocks.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/extract_thinking_blocks.py \ --jsonl "$JSONL_PATH" \ --stats) ``` @@ -80,7 +80,7 @@ For each extracted perception change, use the mapped type from Step 3: ```bash # Example for a CORRECTION → ERROR_FIX -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/store_learning.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/store_learning.py \ --session-id "$SESSION_ID" \ --type "ERROR_FIX" \ --context "what this relates to" \ @@ -90,7 +90,7 @@ For each extracted perception change, use the mapped type from Step 3: --json) # Example for a REALIZATION/INSIGHT → CODEBASE_PATTERN -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/store_learning.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/store_learning.py \ --session-id "$SESSION_ID" \ --type "CODEBASE_PATTERN" \ --context "what this relates to" \ @@ -100,7 +100,7 @@ For each extracted perception change, use the mapped type from Step 3: --json) # Example for a DEBUGGING_APPROACH → WORKING_SOLUTION -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/store_learning.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/store_learning.py \ --session-id "$SESSION_ID" \ --type "WORKING_SOLUTION" \ --context "debugging methodology" \ diff --git a/.claude/agents/plan-agent.md b/.claude/agents/plan-agent.md index f2446f92..3c29bf9d 100644 --- a/.claude/agents/plan-agent.md +++ b/.claude/agents/plan-agent.md @@ -13,7 +13,7 @@ You are a specialized planning agent. Your job is to create detailed implementat Before creating any plan, read the planning skill for methodology and format: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/create_plan/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/create_plan/SKILL.md ``` Follow the structure and guidelines from that skill. diff --git a/.claude/agents/scribe.md b/.claude/agents/scribe.md index 36150db2..53eef639 100644 --- a/.claude/agents/scribe.md +++ b/.claude/agents/scribe.md @@ -15,10 +15,10 @@ Before creating documentation, read the relevant skills: ```bash # For handoffs -cat $CLAUDE_PROJECT_DIR/.claude/skills/create_handoff/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/create_handoff/SKILL.md # For ledger updates -cat $CLAUDE_PROJECT_DIR/.claude/skills/continuity_ledger/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/continuity_ledger/SKILL.md ``` Follow the structure and guidelines from those skills. diff --git a/.claude/agents/session-analyst.md b/.claude/agents/session-analyst.md index 81e0677b..68d965ce 100644 --- a/.claude/agents/session-analyst.md +++ b/.claude/agents/session-analyst.md @@ -13,7 +13,7 @@ You analyze Claude Code session data from Braintrust and provide insights. Read the skill file first: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/braintrust-analyze/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/braintrust-analyze/SKILL.md ``` ## Step 2: Run Analysis diff --git a/.claude/agents/validate-agent.md b/.claude/agents/validate-agent.md index 5b59eb32..fee65d9b 100644 --- a/.claude/agents/validate-agent.md +++ b/.claude/agents/validate-agent.md @@ -13,7 +13,7 @@ You are a specialized validation agent. Your job is to validate a technical plan Before validating, read the validation skill for methodology and format: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/validate-agent/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/validate-agent/SKILL.md ``` Follow the structure and guidelines from that skill. diff --git a/.claude/hooks/dist/mcp-directory-guard.mjs b/.claude/hooks/dist/mcp-directory-guard.mjs new file mode 100644 index 00000000..e50ce092 --- /dev/null +++ b/.claude/hooks/dist/mcp-directory-guard.mjs @@ -0,0 +1,86 @@ +// src/mcp-directory-guard.ts +import { readFileSync } from "fs"; + +// src/shared/opc-path.ts +import { existsSync } from "fs"; +import { join } from "path"; +function getOpcDir() { + const envOpcDir = process.env.CLAUDE_OPC_DIR; + if (envOpcDir && existsSync(envOpcDir)) { + return envOpcDir; + } + const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + const localOpc = join(projectDir, "opc"); + if (existsSync(localOpc)) { + return localOpc; + } + const homeDir = process.env.HOME || process.env.USERPROFILE || ""; + if (homeDir) { + const globalClaude = join(homeDir, ".claude"); + const globalScripts = join(globalClaude, "scripts", "core"); + if (existsSync(globalScripts)) { + return globalClaude; + } + } + return null; +} + +// src/mcp-directory-guard.ts +var SCRIPT_PATH_PATTERN = /\bscripts\/(mcp|core)\//; +function buildCdPrefixPattern(opcDir) { + const escapedDir = opcDir ? opcDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : ""; + const variants = [ + "\\$CLAUDE_OPC_DIR", + "\\$\\{CLAUDE_OPC_DIR\\}" + ]; + if (escapedDir) { + variants.push(escapedDir); + } + return new RegExp(`^\\s*cd\\s+(${variants.join("|")})\\s*&&`); +} +function main() { + let input; + try { + const stdinContent = readFileSync(0, "utf-8"); + input = JSON.parse(stdinContent); + } catch { + console.log("{}"); + return; + } + if (input.tool_name !== "Bash") { + console.log("{}"); + return; + } + const command = input.tool_input?.command; + if (!command) { + console.log("{}"); + return; + } + if (!SCRIPT_PATH_PATTERN.test(command)) { + console.log("{}"); + return; + } + const opcDir = getOpcDir(); + const cdPrefix = buildCdPrefixPattern(opcDir); + if (cdPrefix.test(command)) { + console.log("{}"); + return; + } + const dirRef = opcDir || "$CLAUDE_OPC_DIR"; + const corrected = `cd ${dirRef} && ${command.trimStart()}`; + const output = { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: `OPC directory guard: commands referencing scripts/(mcp|core)/ must run from the OPC directory so uv can find pyproject.toml. + +Blocked command: + ${command.trim()} + +Corrected command: + ${corrected}` + } + }; + console.log(JSON.stringify(output)); +} +main(); diff --git a/.claude/hooks/dist/session-start-continuity.mjs b/.claude/hooks/dist/session-start-continuity.mjs index 671d6630..4e9ae9b6 100644 --- a/.claude/hooks/dist/session-start-continuity.mjs +++ b/.claude/hooks/dist/session-start-continuity.mjs @@ -1,7 +1,8 @@ // src/session-start-continuity.ts import * as fs from "fs"; import * as path from "path"; -import { execSync } from "child_process"; +import * as os from "os"; +import { execSync, spawn } from "child_process"; function buildHandoffDirName(sessionName, sessionId) { const uuidShort = sessionId.replace(/-/g, "").slice(0, 8); return `${sessionName}-${uuidShort}`; @@ -159,9 +160,42 @@ function getUnmarkedHandoffs() { return []; } } +function ensureMemoryDaemon() { + const pidFile = path.join(os.homedir(), ".claude", "memory-daemon.pid"); + if (fs.existsSync(pidFile)) { + try { + const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); + process.kill(pid, 0); + return null; + } catch { + fs.unlinkSync(pidFile); + } + } + const possibleLocations = [ + path.join(os.homedir(), ".claude", "opc", "scripts", "core", "memory_daemon.py"), + path.join(os.homedir(), ".claude", "scripts", "core", "memory_daemon.py") + ]; + for (const daemonScript of possibleLocations) { + if (fs.existsSync(daemonScript)) { + try { + const child = spawn("uv", ["run", "python", daemonScript, "start"], { + detached: true, + stdio: "ignore", + cwd: path.dirname(path.dirname(path.dirname(daemonScript))) + }); + child.unref(); + return "Memory daemon: Started"; + } catch (e) { + console.error(`Warning: Failed to start memory daemon: ${e}`); + } + } + } + return null; +} async function main() { const input = JSON.parse(await readStdin()); const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + ensureMemoryDaemon(); const sessionType = input.source || input.type; let message = ""; let additionalContext = ""; diff --git a/.claude/hooks/dist/tldr-read-enforcer.mjs b/.claude/hooks/dist/tldr-read-enforcer.mjs index 2aee9800..9898c839 100644 --- a/.claude/hooks/dist/tldr-read-enforcer.mjs +++ b/.claude/hooks/dist/tldr-read-enforcer.mjs @@ -1,6 +1,6 @@ // src/tldr-read-enforcer.ts import { readFileSync as readFileSync2, existsSync as existsSync2, statSync } from "fs"; -import { basename, extname } from "path"; +import { basename, extname, isAbsolute, join as join2 } from "path"; // src/daemon-client.ts import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs"; @@ -520,7 +520,8 @@ async function main() { console.log("{}"); return; } - const filePath = input.tool_input.file_path || ""; + const rawFilePath = input.tool_input.file_path || ""; + const filePath = isAbsolute(rawFilePath) ? rawFilePath : join2(input.cwd, rawFilePath); if (!isCodeFile(filePath)) { console.log("{}"); return; diff --git a/.claude/hooks/src/mcp-directory-guard.ts b/.claude/hooks/src/mcp-directory-guard.ts new file mode 100644 index 00000000..6709b191 --- /dev/null +++ b/.claude/hooks/src/mcp-directory-guard.ts @@ -0,0 +1,104 @@ +/** + * PreToolUse:Bash Hook - OPC Script Directory Guard + * + * Prevents running scripts from `scripts/(mcp|core)/` without first + * changing to $CLAUDE_OPC_DIR. When Claude runs these scripts from the + * wrong directory, `uv run` misses `opc/pyproject.toml` and its + * dependencies, causing ModuleNotFoundError. + * + * Detection: any Bash command referencing `scripts/(mcp|core)/` paths + * Allowed: commands prefixed with `cd $CLAUDE_OPC_DIR &&` (or resolved path) + * Denied: returns corrected command in the reason message + * + * Fixes: #148 + */ + +import { readFileSync } from 'fs'; +import { getOpcDir } from './shared/opc-path.js'; +import type { PreToolUseInput, PreToolUseHookOutput } from './shared/types.js'; + +/** + * Pattern matching scripts/(mcp|core)/ references in Bash commands. + * Captures the path for use in the corrected command suggestion. + */ +const SCRIPT_PATH_PATTERN = /\bscripts\/(mcp|core)\//; + +/** + * Pattern matching a proper cd prefix to OPC dir. + * Accepts: + * cd $CLAUDE_OPC_DIR && + * cd ${CLAUDE_OPC_DIR} && + * cd /resolved/opc/path && + */ +function buildCdPrefixPattern(opcDir: string | null): RegExp { + const escapedDir = opcDir ? opcDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : ''; + // Match: cd && (with flexible whitespace) + const variants = [ + '\\$CLAUDE_OPC_DIR', + '\\$\\{CLAUDE_OPC_DIR\\}', + ]; + if (escapedDir) { + variants.push(escapedDir); + } + return new RegExp(`^\\s*cd\\s+(${variants.join('|')})\\s*&&`); +} + +function main(): void { + let input: PreToolUseInput; + try { + const stdinContent = readFileSync(0, 'utf-8'); + input = JSON.parse(stdinContent) as PreToolUseInput; + } catch { + // Can't read input - allow through + console.log('{}'); + return; + } + + // Only process Bash tool + if (input.tool_name !== 'Bash') { + console.log('{}'); + return; + } + + const command = input.tool_input?.command as string; + if (!command) { + console.log('{}'); + return; + } + + // Check if command references OPC script paths + if (!SCRIPT_PATH_PATTERN.test(command)) { + // No script path reference - allow through + console.log('{}'); + return; + } + + const opcDir = getOpcDir(); + const cdPrefix = buildCdPrefixPattern(opcDir); + + // Check if command already has the correct cd prefix + if (cdPrefix.test(command)) { + console.log('{}'); + return; + } + + // Build corrected command suggestion + const dirRef = opcDir || '$CLAUDE_OPC_DIR'; + const corrected = `cd ${dirRef} && ${command.trimStart()}`; + + const output: PreToolUseHookOutput = { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + permissionDecision: 'deny', + permissionDecisionReason: + `OPC directory guard: commands referencing scripts/(mcp|core)/ must ` + + `run from the OPC directory so uv can find pyproject.toml.\n\n` + + `Blocked command:\n ${command.trim()}\n\n` + + `Corrected command:\n ${corrected}`, + }, + }; + + console.log(JSON.stringify(output)); +} + +main(); diff --git a/.claude/hooks/src/session-start-continuity.ts b/.claude/hooks/src/session-start-continuity.ts index 8becbf59..e6a60a61 100644 --- a/.claude/hooks/src/session-start-continuity.ts +++ b/.claude/hooks/src/session-start-continuity.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { execSync } from 'child_process'; +import * as os from 'os'; +import { execSync, spawn } from 'child_process'; interface SessionStartInput { type?: 'startup' | 'resume' | 'clear' | 'compact'; // Legacy field @@ -308,10 +309,53 @@ function getUnmarkedHandoffs(): UnmarkedHandoff[] { } } +// ============================================ +// MEMORY DAEMON: Auto-start on session start +// ============================================ + +function ensureMemoryDaemon(): string | null { + const pidFile = path.join(os.homedir(), '.claude', 'memory-daemon.pid'); + + if (fs.existsSync(pidFile)) { + try { + const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10); + process.kill(pid, 0); + return null; + } catch { + fs.unlinkSync(pidFile); + } + } + + const possibleLocations = [ + path.join(os.homedir(), '.claude', 'opc', 'scripts', 'core', 'memory_daemon.py'), + path.join(os.homedir(), '.claude', 'scripts', 'core', 'memory_daemon.py'), + ]; + + for (const daemonScript of possibleLocations) { + if (fs.existsSync(daemonScript)) { + try { + const child = spawn('uv', ['run', 'python', daemonScript, 'start'], { + detached: true, + stdio: 'ignore', + cwd: path.dirname(path.dirname(path.dirname(daemonScript))), + }); + child.unref(); + return 'Memory daemon: Started'; + } catch (e) { + console.error(`Warning: Failed to start memory daemon: ${e}`); + } + } + } + + return null; +} + async function main() { const input: SessionStartInput = JSON.parse(await readStdin()); const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + ensureMemoryDaemon(); + // Support both 'source' (per docs) and 'type' (legacy) fields const sessionType = input.source || input.type; diff --git a/.claude/hooks/src/tldr-read-enforcer.ts b/.claude/hooks/src/tldr-read-enforcer.ts index 3090da00..90a1cad5 100644 --- a/.claude/hooks/src/tldr-read-enforcer.ts +++ b/.claude/hooks/src/tldr-read-enforcer.ts @@ -10,7 +10,7 @@ */ import { readFileSync, existsSync, statSync } from 'fs'; -import { basename, extname } from 'path'; +import { basename, extname, isAbsolute, join } from 'path'; import { queryDaemonSync, DaemonResponse, trackHookActivitySync } from './daemon-client'; // Search context from smart-search-router @@ -371,7 +371,8 @@ async function main() { return; } - const filePath = input.tool_input.file_path || ''; + const rawFilePath = input.tool_input.file_path || ''; + const filePath = isAbsolute(rawFilePath) ? rawFilePath : join(input.cwd, rawFilePath); // Allow non-code files if (!isCodeFile(filePath)) { diff --git a/.claude/scripts/status.py b/.claude/scripts/status.py index 04ee7f61..ce794dd9 100644 --- a/.claude/scripts/status.py +++ b/.claude/scripts/status.py @@ -1,8 +1,32 @@ #!/usr/bin/env python3 """Cross-platform status line for Claude Code. -Shows: 145K 72% | main U:6 | Goal → Current focus -Critical: ⚠ 160K 80% | main U:6 | Current focus +Output Format: + CONTEXT: {tokens} {context}% | GIT: {branch} S:{staged} U:{unstaged} A:{added} | GOAL: {goal} → NOW: {now} + +Examples: + CONTEXT: 78K 39% | GIT: main A:4 | GOAL: Fix wizard → NOW: Update timeout + CONTEXT: 145K 72% | GIT: main S:2 U:1 | GOAL: Refactor OPC → NOW: Add validation + ⚠ CONTEXT: 160K 80% | GIT: dev U:6 | NOW: Critical bug fix + +Color Coding: + Green (ctx < 60%): Normal operation + Yellow (ctx 60-79%): Warning, consider cleanup + Red (ctx ≥ 80%): Critical, show only now focus + +Components: + tokens - Total tokens used (input + cache_read + cache_creation + overhead) + context% - Percentage of context window consumed + branch - Git branch name (max 12 chars) + S:N - Staged files (git add done) + U:N - Unstaged files (changed but not staged) + A:N - Untracked files (new files not git-added) + goal - Objective from handoff file (max 25 chars) + now - Current focus from handoff (max 30 chars) + +Temporary Files: + ~/.tmp/claude-context-pct-{session_id}.txt - Context % for other hooks + ~/.tmp/claude-session-stats-{session_id}.json - Full stats for /tldr-stats skill Replaces status.sh for Windows compatibility. """ @@ -89,11 +113,14 @@ def log_context_drop(session_id: str, prev_pct: int, curr_pct: int) -> None: Logs to ~/.claude/autocompact.log (local, not pushed to repo). """ from datetime import datetime + log_file = Path.home() / ".claude" / "autocompact.log" try: with open(log_file, "a") as f: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - f.write(f"{timestamp} | session:{session_id} | {prev_pct}% → {curr_pct}% (drop: {prev_pct - curr_pct}%)\n") + f.write( + f"{timestamp} | session:{session_id} | {prev_pct}% → {curr_pct}% (drop: {prev_pct - curr_pct}%)\n" + ) except OSError: pass @@ -180,15 +207,27 @@ def get_git_info(cwd: Path) -> str: # Check if git repo result = subprocess.run( ["git", "-C", str(cwd), "rev-parse", "--git-dir"], - capture_output=True, text=True, timeout=5 + capture_output=True, + text=True, + timeout=5, ) if result.returncode != 0: return "" # Get branch name result = subprocess.run( - ["git", "-C", str(cwd), "--no-optional-locks", "rev-parse", "--abbrev-ref", "HEAD"], - capture_output=True, text=True, timeout=5 + [ + "git", + "-C", + str(cwd), + "--no-optional-locks", + "rev-parse", + "--abbrev-ref", + "HEAD", + ], + capture_output=True, + text=True, + timeout=5, ) branch = result.stdout.strip() if result.returncode == 0 else "" if len(branch) > 12: @@ -196,22 +235,46 @@ def get_git_info(cwd: Path) -> str: # Get staged count result = subprocess.run( - ["git", "-C", str(cwd), "--no-optional-locks", "diff", "--cached", "--name-only"], - capture_output=True, text=True, timeout=5 + [ + "git", + "-C", + str(cwd), + "--no-optional-locks", + "diff", + "--cached", + "--name-only", + ], + capture_output=True, + text=True, + timeout=5, ) staged = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0 # Get unstaged count result = subprocess.run( ["git", "-C", str(cwd), "--no-optional-locks", "diff", "--name-only"], - capture_output=True, text=True, timeout=5 + capture_output=True, + text=True, + timeout=5, + ) + unstaged = ( + len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0 ) - unstaged = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0 # Get untracked count result = subprocess.run( - ["git", "-C", str(cwd), "--no-optional-locks", "ls-files", "--others", "--exclude-standard"], - capture_output=True, text=True, timeout=5 + [ + "git", + "-C", + str(cwd), + "--no-optional-locks", + "ls-files", + "--others", + "--exclude-standard", + ], + capture_output=True, + text=True, + timeout=5, ) added = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0 @@ -239,8 +302,8 @@ def parse_filename_timestamp(path: Path) -> str: Returns '0000-00-00_00-00' if no timestamp found (sorts oldest). """ - match = re.search(r'(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})', path.name) - return match.group(1) if match else '0000-00-00_00-00' + match = re.search(r"(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})", path.name) + return match.group(1) if match else "0000-00-00_00-00" def find_latest_handoff(project_dir: Path) -> Path | None: @@ -267,7 +330,7 @@ def extract_yaml_field(content: str, field: str) -> str: pattern = rf"^{field}:\s*(.+?)$" match = re.search(pattern, content, re.MULTILINE) if match: - return match.group(1).strip().strip('"\'') + return match.group(1).strip().strip("\"'") return "" @@ -300,14 +363,19 @@ def get_continuity_info(project_dir: Path) -> tuple[str, str]: # Fallback for now: Action Items or Next Steps section if not now_focus: - match = re.search(r"^## (?:Action Items|Next Steps)\s*\n(?:.*\n)*?^(\d+\.)\s*(.+?)$", - content, re.MULTILINE) + match = re.search( + r"^## (?:Action Items|Next Steps)\s*\n(?:.*\n)*?^(\d+\.)\s*(.+?)$", + content, + re.MULTILINE, + ) if match: now_focus = match.group(2).strip() # Try P0 section if not now_focus: - match = re.search(r"^### P0\s*\n(?:.*\n)*?^(\d+\.)\s*(.+?)$", content, re.MULTILINE) + match = re.search( + r"^### P0\s*\n(?:.*\n)*?^(\d+\.)\s*(.+?)$", content, re.MULTILINE + ) if match: now_focus = match.group(2).strip() @@ -346,42 +414,43 @@ def get_continuity_info(project_dir: Path) -> tuple[str, str]: return goal, now_focus -def build_output(context_pct: int, token_display: str, git_info: str, - goal: str, now_focus: str) -> str: +def build_output( + context_pct: int, token_display: str, git_info: str, goal: str, now_focus: str +) -> str: """Build the final colored output string.""" # Build continuity string if goal and now_focus: - continuity = f"{goal} → {now_focus}" + continuity = f"GOAL: {goal} → NOW: {now_focus}" elif now_focus: - continuity = now_focus + continuity = f"NOW: {now_focus}" elif goal: - continuity = goal + continuity = f"GOAL: {goal}" else: continuity = "" # Color based on context usage if context_pct >= 80: # CRITICAL - Red warning - ctx_display = f"\033[31m⚠ {token_display} {context_pct}%\033[0m" + ctx_display = f"\033[31m⚠ CONTEXT: {token_display} {context_pct}%\033[0m" parts = [ctx_display] if git_info: - parts.append(git_info) + parts.append(f"GIT: {git_info}") if now_focus: # Only show now_focus when critical - parts.append(now_focus) + parts.append(continuity) elif context_pct >= 60: # WARNING - Yellow - ctx_display = f"\033[33m{token_display} {context_pct}%\033[0m" + ctx_display = f"\033[33mCONTEXT: {token_display} {context_pct}%\033[0m" parts = [ctx_display] if git_info: - parts.append(git_info) + parts.append(f"GIT: {git_info}") if continuity: parts.append(continuity) else: # NORMAL - Green - ctx_display = f"\033[32m{token_display} {context_pct}%\033[0m" + ctx_display = f"\033[32mCONTEXT: {token_display} {context_pct}%\033[0m" parts = [ctx_display] if git_info: - parts.append(git_info) + parts.append(f"GIT: {git_info}") if continuity: parts.append(continuity) diff --git a/.claude/settings.json b/.claude/settings.json index c575d685..ddb3536e 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -228,6 +228,11 @@ { "matcher": "Bash", "hooks": [ + { + "type": "command", + "command": "node $HOME/.claude/hooks/dist/mcp-directory-guard.mjs", + "timeout": 5 + }, { "type": "command", "command": "node $HOME/.claude/hooks/dist/import-error-detector.mjs", diff --git a/.claude/skills/async-repl-protocol/SKILL.md b/.claude/skills/async-repl-protocol/SKILL.md deleted file mode 100644 index 943d63f4..00000000 --- a/.claude/skills/async-repl-protocol/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: async-repl-protocol -description: Async REPL Protocol -user-invocable: false ---- - -# Async REPL Protocol - -When working with Agentica's async REPL harness for testing. - -## Rules - -### 1. Use `await` for Future-returning tools - -```python -content = await view_file(path) # NOT view_file(path) -answer = await ask_memory("...") -``` - -### 2. Single code block per response - -Compute AND return in ONE block. Multiple blocks means only first executes. - -```python -# GOOD: Single block -content = await view_file(path) -return any(c.isdigit() for c in content) - -# BAD: Split blocks (second block never runs) -content = await view_file(path) diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md index 1a3c8e35..25cc0081 100644 --- a/.claude/skills/commit/SKILL.md +++ b/.claude/skills/commit/SKILL.md @@ -32,7 +32,7 @@ You are tasked with creating git commits for the changes made during this sessio - Show the result with `git log --oneline -n [number]` 5. **Generate reasoning (after each commit):** - - Run: `bash "$CLAUDE_PROJECT_DIR/.claude/scripts/generate-reasoning.sh" ""` + - Run: `bash "$CLAUDE_CC_DIR/.claude/scripts/generate-reasoning.sh" ""` - This captures what was tried during development (build failures, fixes) - The reasoning file helps future sessions understand past decisions - Stored in `.git/claude/commits//reasoning.md` diff --git a/.claude/skills/commit/SKILL.v6.md b/.claude/skills/commit/SKILL.v6.md index 870a2dd2..735dbc32 100644 --- a/.claude/skills/commit/SKILL.v6.md +++ b/.claude/skills/commit/SKILL.v6.md @@ -71,7 +71,7 @@ eta |-> generate_reasoning(hash, message) ```bash git add ... git commit -m "message" -bash "$CLAUDE_PROJECT_DIR/.claude/scripts/generate-reasoning.sh" "" +bash "$CLAUDE_CC_DIR/.claude/scripts/generate-reasoning.sh" "" git log --oneline -n N ``` diff --git a/.claude/skills/continuity_ledger/SKILL.md b/.claude/skills/continuity-ledger/SKILL.md similarity index 100% rename from .claude/skills/continuity_ledger/SKILL.md rename to .claude/skills/continuity-ledger/SKILL.md diff --git a/.claude/skills/create_handoff/SKILL.md b/.claude/skills/create-handoff/SKILL.md similarity index 100% rename from .claude/skills/create_handoff/SKILL.md rename to .claude/skills/create-handoff/SKILL.md diff --git a/.claude/skills/create_handoff/SKILL.v6.md b/.claude/skills/create-handoff/SKILL.v6.md similarity index 100% rename from .claude/skills/create_handoff/SKILL.v6.md rename to .claude/skills/create-handoff/SKILL.v6.md diff --git a/.claude/skills/describe_pr/SKILL.md b/.claude/skills/describe-pr/SKILL.md similarity index 97% rename from .claude/skills/describe_pr/SKILL.md rename to .claude/skills/describe-pr/SKILL.md index fbf8c425..20074e87 100644 --- a/.claude/skills/describe_pr/SKILL.md +++ b/.claude/skills/describe-pr/SKILL.md @@ -34,7 +34,7 @@ You are tasked with generating a comprehensive pull request description followin 4b. **Gather reasoning history (if available):** - Check if reasoning files exist: `ls .git/claude/commits/*/reasoning.md 2>/dev/null` - - If they exist, aggregate them: `bash "$CLAUDE_PROJECT_DIR/.claude/scripts/aggregate-reasoning.sh" main` + - If they exist, aggregate them: `bash "$CLAUDE_CC_DIR/.claude/scripts/aggregate-reasoning.sh" main` - This shows what approaches were tried before the final solution - Save the output for inclusion in the PR description diff --git a/.claude/skills/discovery-interview/SKILL.md b/.claude/skills/discovery-interview/SKILL.md index ce4ac8c0..517ce079 100644 --- a/.claude/skills/discovery-interview/SKILL.md +++ b/.claude/skills/discovery-interview/SKILL.md @@ -2,7 +2,6 @@ name: discovery-interview description: Deep interview process to transform vague ideas into detailed specs. Works for technical and non-technical users. user-invocable: true -model: claude-opus-4-5-20251101 --- # Discovery Interview diff --git a/.claude/skills/git-commits/SKILL.md b/.claude/skills/git-commits/SKILL.md index cd390434..836aeedf 100644 --- a/.claude/skills/git-commits/SKILL.md +++ b/.claude/skills/git-commits/SKILL.md @@ -38,7 +38,7 @@ When you see these in user prompts, use the commit skill: The skill will prompt you to run: ```bash -bash "$CLAUDE_PROJECT_DIR/.claude/scripts/generate-reasoning.sh" "" +bash "$CLAUDE_CC_DIR/.claude/scripts/generate-reasoning.sh" "" ``` Then push if requested: diff --git a/.claude/skills/implement_plan/SKILL.md b/.claude/skills/implement-plan/SKILL.md similarity index 99% rename from .claude/skills/implement_plan/SKILL.md rename to .claude/skills/implement-plan/SKILL.md index db53ac4d..bf9b7ae8 100644 --- a/.claude/skills/implement_plan/SKILL.md +++ b/.claude/skills/implement-plan/SKILL.md @@ -1,5 +1,5 @@ --- -name: implement_plan +name: implement-plan description: Implement technical plans from thoughts/shared/plans with verification user-invocable: false --- diff --git a/.claude/skills/implement_plan/SKILL.md.backup b/.claude/skills/implement-plan/SKILL.md.backup similarity index 100% rename from .claude/skills/implement_plan/SKILL.md.backup rename to .claude/skills/implement-plan/SKILL.md.backup diff --git a/.claude/skills/implement_task/SKILL.md b/.claude/skills/implement-task/SKILL.md similarity index 99% rename from .claude/skills/implement_task/SKILL.md rename to .claude/skills/implement-task/SKILL.md index 248a4947..5f0d9483 100644 --- a/.claude/skills/implement_task/SKILL.md +++ b/.claude/skills/implement-task/SKILL.md @@ -1,5 +1,5 @@ --- -name: implement_task +name: implement-task description: Implementation agent that executes a single task and creates handoff on completion user-invocable: false --- diff --git a/.claude/skills/math-unified/SKILL.md b/.claude/skills/math-unified/SKILL.md index 70d39795..7bf96c3c 100644 --- a/.claude/skills/math-unified/SKILL.md +++ b/.claude/skills/math-unified/SKILL.md @@ -31,7 +31,7 @@ For formal proofs, use `/prove` instead. ### SymPy (Symbolic Math) ```bash -uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/sympy_compute.py" +uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/sympy_compute.py" ``` | Command | Description | Example | @@ -83,7 +83,7 @@ uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/sympy_compute.py" +uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/z3_solve.py" ``` | Command | Use Case | @@ -96,7 +96,7 @@ uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/z3_solve.py" +uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/pint_compute.py" convert ``` Example: `convert 5 miles kilometers` @@ -105,7 +105,7 @@ Example: `convert 5 miles kilometers` ### Math Router (Auto-Route) ```bash -uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/math_router.py" route "" +uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/math_router.py" route "" ``` Returns the exact command to run. Use when unsure which script. @@ -154,28 +154,28 @@ I decide based on your request: ### Solve Equation ``` User: Solve x² - 5x + 6 = 0 -Claude: uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/sympy_compute.py" solve "x**2 - 5*x + 6" --var x +Claude: uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/sympy_compute.py" solve "x**2 - 5*x + 6" --var x Result: x = 2 or x = 3 ``` ### Compute Eigenvalues ``` User: Find eigenvalues of [[2, 1], [1, 2]] -Claude: uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/sympy_compute.py" eigenvalues "[[2,1],[1,2]]" +Claude: uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/sympy_compute.py" eigenvalues "[[2,1],[1,2]]" Result: {1: 1, 3: 1} (eigenvalue 1 with multiplicity 1, eigenvalue 3 with multiplicity 1) ``` ### Prove Inequality ``` User: Is x² + y² ≥ 2xy always true? -Claude: uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/z3_solve.py" prove "x**2 + y**2 >= 2*x*y" +Claude: uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/z3_solve.py" prove "x**2 + y**2 >= 2*x*y" Result: PROVED (equivalent to (x-y)² ≥ 0) ``` ### Convert Units ``` User: How many kilometers in 26.2 miles? -Claude: uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/pint_compute.py" convert 26.2 miles kilometers +Claude: uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/pint_compute.py" convert 26.2 miles kilometers Result: 42.16 km ``` diff --git a/.claude/skills/recall-reasoning/SKILL.md b/.claude/skills/recall-reasoning/SKILL.md index 6110a74a..02c3dce5 100644 --- a/.claude/skills/recall-reasoning/SKILL.md +++ b/.claude/skills/recall-reasoning/SKILL.md @@ -32,7 +32,7 @@ This searches handoffs with post-mortems (what worked, what failed, key decision ### Secondary: Reasoning Files (build attempts) ```bash -bash "$CLAUDE_PROJECT_DIR/.claude/scripts/search-reasoning.sh" "" +bash "$CLAUDE_CC_DIR/.claude/scripts/search-reasoning.sh" "" ``` This searches `.git/claude/commits/*/reasoning.md` for build failures and fixes. @@ -50,7 +50,7 @@ uv run python scripts/core/artifact_query.py "implement agent" --outcome SUCCEED uv run python scripts/core/artifact_query.py "hook implementation" --outcome FAILED # Search build/test reasoning -bash "$CLAUDE_PROJECT_DIR/.claude/scripts/search-reasoning.sh" "TypeError" +bash "$CLAUDE_CC_DIR/.claude/scripts/search-reasoning.sh" "TypeError" ``` ## What Gets Searched diff --git a/.claude/skills/research/SKILL.md b/.claude/skills/research/SKILL.md index 5ffec538..ab247ae7 100644 --- a/.claude/skills/research/SKILL.md +++ b/.claude/skills/research/SKILL.md @@ -1,7 +1,6 @@ --- name: research description: Document codebase as-is with thoughts directory for historical context -model: claude-opus-4-5-20251101 user-invocable: false --- diff --git a/.claude/skills/resume_handoff/SKILL.md b/.claude/skills/resume-handoff/SKILL.md similarity index 100% rename from .claude/skills/resume_handoff/SKILL.md rename to .claude/skills/resume-handoff/SKILL.md diff --git a/.claude/skills/router-first-architecture/SKILL.md b/.claude/skills/router-first-architecture/SKILL.md deleted file mode 100644 index f4947d7b..00000000 --- a/.claude/skills/router-first-architecture/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: router-first-architecture -description: Router-First Architecture -user-invocable: false ---- - -# Router-First Architecture - -Route through domain routers before using individual tools. Routers abstract tool selection. - -## Pattern - -Domain routers (like `math-router`) provide deterministic mapping from user intent to exact CLI commands. Always use the router first; only bypass for edge cases. - -## DO - -- Call `math-router route ""` before any math operation -- Let domain skills co-activate with their router (via `coActivate` in skill-rules.json) -- Trust the router's confidence score; only fall back if `command: null` -- Keep trigger keywords/patterns in skill-rules.json broader than routing patterns - -## DON'T - -- Call individual scripts directly when a router exists -- Duplicate routing logic in individual skills -- Let domain skills bypass their router - -## Co-Activation Pattern - -Domain skills should co-activate with their router: - -```json -{ - "math/abstract-algebra/groups": { - "coActivate": ["math-router"], - "coActivateMode": "always" - } -} -``` - -This ensures the router is always available when domain knowledge is activated. - -## Two-Layer Architecture - -1. **Skill-rules trigger layer**: Nudges Claude to use the router (keywords, intent patterns) -2. **Router routing layer**: Deterministic mapping to scripts via regex patterns - -Keep the trigger layer broader than routing - the router should handle "not found" gracefully. - -## Source Sessions - -- 2bbc8d6e: "Trigger layer was narrower than routing layer" - expanded triggers -- This session: Wired 8 domain math skills to co-activate with math-router diff --git a/.claude/skills/search-hierarchy/SKILL.md b/.claude/skills/search-hierarchy/SKILL.md deleted file mode 100644 index 9fd1ee09..00000000 --- a/.claude/skills/search-hierarchy/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: search-hierarchy -description: Search Tool Hierarchy -user-invocable: false ---- - -# Search Tool Hierarchy - -Use the most token-efficient search tool for each query type. - -## Decision Tree - -``` -Query Type? -├── STRUCTURAL (code patterns) -│ → AST-grep (~50 tokens output) -│ Examples: "def foo", "class Bar", "import X", "@decorator" -│ -├── SEMANTIC (conceptual questions) -│ → LEANN (~100 tokens if path-only) -│ Examples: "how does auth work", "find error handling patterns" -│ -├── LITERAL (exact identifiers) -│ → Grep (variable output) -│ Examples: "TemporalMemory", "check_evocation", regex patterns -│ -└── FULL CONTEXT (need complete understanding) - → Read (1500+ tokens) - Last resort after finding the right file -``` - -## Token Efficiency Comparison - -| Tool | Output Size | Best For | -|------|-------------|----------| -| AST-grep | ~50 tokens | Function/class definitions, imports, decorators | -| LEANN | ~100 tokens | Conceptual questions, architecture, patterns | -| Grep | ~200-2000 | Exact identifiers, regex, file paths | -| Read | ~1500+ | Full understanding after finding the file | - -## Hook Enforcement - -The `grep-to-leann.sh` hook automatically: -1. Detects query type (structural/semantic/literal) -2. Blocks and suggests AST-grep for structural queries -3. Blocks and suggests LEANN for semantic queries -4. Allows literal patterns through to Grep - -## DO - -- Start with AST-grep for code structure questions -- Use LEANN for "how does X work" questions -- Use Grep only for exact identifier matches -- Read files only after finding them via search - -## DON'T - -- Use Grep for conceptual questions (returns nothing) -- Read files before knowing which ones are relevant -- Use Read when AST-grep would give file:line -- Ignore hook suggestions - -## Examples - -```bash -# STRUCTURAL → AST-grep -ast-grep --pattern "async def $FUNC($$$):" --lang python - -# SEMANTIC → LEANN -leann search opc-dev "how does authentication work" --top-k 3 - -# LITERAL → Grep -Grep pattern="check_evocation" path=opc/scripts - -# FULL CONTEXT → Read (after finding file) -Read file_path=opc/scripts/z3_erotetic.py -``` - -## Optimal Flow - -``` -1. AST-grep: "Find async functions" → 3 file:line matches -2. Read: Top match only → Full understanding -3. Skip: 4 irrelevant files → 6000 tokens saved -``` diff --git a/.claude/skills/search-tools/SKILL.md b/.claude/skills/search-tools/SKILL.md deleted file mode 100644 index f9379606..00000000 --- a/.claude/skills/search-tools/SKILL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: search-tools -description: Search Tool Hierarchy -user-invocable: false ---- - -# Search Tool Hierarchy - -When searching code, use this decision tree: - -## Decision Tree - -``` -Need CONCEPTUAL/SEMANTIC search? - (how does X work, find patterns, understand architecture) - → Use LEANN (/leann-search) - embedding-based semantic search - → PreToolUse hook auto-redirects semantic Grep queries - -Need to understand code STRUCTURE? - (find function calls, class usages, refactor patterns) - → Use AST-grep (/ast-grep-find) - -Need to find TEXT in code? - → Use Morph (/morph-search) - 20x faster - → If no Morph API key: fall back to Grep tool - -Simple one-off search? - → Use built-in Grep tool directly -``` - -## Tool Comparison - -| Tool | Best For | Requires | -|------|----------|----------| -| **LEANN** | Semantic search: "how does caching work", "error handling patterns", conceptual queries | Index built | -| **AST-grep** | Structural patterns: "find all calls to `foo()`", refactoring, find usages by type | MCP server | -| **Morph** | Fast text search: "find files mentioning error", grep across codebase | API key | -| **Grep** | Literal patterns, class/function names, regex | Nothing (built-in) | - -## Examples - -**LEANN** (semantic/conceptual): -- "how does authentication work" -- "find error handling patterns" -- "where is rate limiting implemented" - -**AST-grep** (structural): -- "Find all functions that return a Promise" -- "Find all React components using useState" -- "Refactor all imports of X to Y" - -**Morph** (text search): -- "Find all files mentioning 'authentication'" -- "Search for TODO comments" - -**Grep** (literal): -- `class ProviderAdapter` -- `def __init__` -- Regex patterns - -## LEANN Commands - -```bash -# Search with semantic query -leann search opc-dev "how does blackboard communication work" --top-k 5 - -# List available indexes -leann list - -# Rebuild index (when code changes) -leann build opc-dev --docs dir1 dir2 --no-recompute --no-compact --force -``` diff --git a/.claude/skills/skill-developer/SKILL.md b/.claude/skills/skill-developer/SKILL.md index de41038f..5678ab5d 100644 --- a/.claude/skills/skill-developer/SKILL.md +++ b/.claude/skills/skill-developer/SKILL.md @@ -56,14 +56,14 @@ To create a new MCP chain script and wrap it as a skill: Copy the multi-tool-pipeline template: ```bash -cp $CLAUDE_PROJECT_DIR/scripts/multi_tool_pipeline.py $CLAUDE_PROJECT_DIR/scripts/my_pipeline.py +cp $CLAUDE_OPC_DIR/scripts/multi_tool_pipeline.py $CLAUDE_PROJECT_DIR/scripts/my_pipeline.py ``` Reference the template pattern: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/multi-tool-pipeline/SKILL.md -cat $CLAUDE_PROJECT_DIR/scripts/multi_tool_pipeline.py +cat $CLAUDE_CC_DIR/.claude/skills/multi-tool-pipeline/SKILL.md +cat $CLAUDE_OPC_DIR/scripts/multi_tool_pipeline.py ``` ### Step 2: Customize the Script @@ -140,8 +140,8 @@ Add to `.claude/skills/skill-rules.json`: For full details, read: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/rules/skill-development.md -cat $CLAUDE_PROJECT_DIR/.claude/rules/mcp-scripts.md +cat $CLAUDE_CC_DIR/.claude/rules/skill-development.md +cat $CLAUDE_CC_DIR/.claude/rules/mcp-scripts.md ``` ## Quick Checklist @@ -157,7 +157,7 @@ cat $CLAUDE_PROJECT_DIR/.claude/rules/mcp-scripts.md Look at existing skills for patterns: ```bash -ls $CLAUDE_PROJECT_DIR/.claude/skills/ -cat $CLAUDE_PROJECT_DIR/.claude/skills/commit/SKILL.md -cat $CLAUDE_PROJECT_DIR/.claude/skills/firecrawl-scrape/SKILL.md +ls $CLAUDE_CC_DIR/.claude/skills/ +cat $CLAUDE_CC_DIR/.claude/skills/commit/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/firecrawl-scrape/SKILL.md ``` diff --git a/.claude/skills/system_overview/SKILL.md b/.claude/skills/system-overview/SKILL.md similarity index 100% rename from .claude/skills/system_overview/SKILL.md rename to .claude/skills/system-overview/SKILL.md diff --git a/.claude/skills/tldr-stats/SKILL.md b/.claude/skills/tldr-stats/SKILL.md index 6c16e4fd..cb2ed757 100644 --- a/.claude/skills/tldr-stats/SKILL.md +++ b/.claude/skills/tldr-stats/SKILL.md @@ -20,7 +20,7 @@ Show a beautiful dashboard with token usage, actual API costs, TLDR savings, and 1. Run the stats script: ```bash -python3 $CLAUDE_PROJECT_DIR/.claude/scripts/tldr_stats.py +python3 $CLAUDE_CC_DIR/.claude/scripts/tldr_stats.py ``` 2. **Copy the full output into your response** so the user sees the dashboard directly in the chat. Do not just run the command silently - the user wants to see the stats. diff --git a/opc/scripts/braintrust_analyze.py b/opc/scripts/braintrust_analyze.py index 394f8c1d..f2cf0641 100644 --- a/opc/scripts/braintrust_analyze.py +++ b/opc/scripts/braintrust_analyze.py @@ -323,7 +323,7 @@ def list_sessions(project_id: str, api_key: str, limit: int = 5): MIN(created) as started, MAX(created) as ended, COUNT(*) as span_count, - COUNT(*) FILTER (WHERE span_attributes['type'] = 'tool') as tool_count + SUM(CASE WHEN span_attributes['type'] = 'tool' THEN 1 ELSE 0 END) as tool_count FROM logs GROUP BY root_span_id ORDER BY started DESC diff --git a/opc/scripts/cc_math/math_base.py b/opc/scripts/cc_math/math_base.py index 4a2f7526..0d2ca6b3 100644 --- a/opc/scripts/cc_math/math_base.py +++ b/opc/scripts/cc_math/math_base.py @@ -24,7 +24,7 @@ from collections.abc import Callable from dataclasses import dataclass, field from functools import wraps -from typing import Any, Dict, List, Optional, TypeVar, Union +from typing import Any, TypeVar # Type variables for generic decorators T = TypeVar("T") @@ -45,7 +45,7 @@ class MathCommand: category: str description: str latex_template: str | None = None - args: list[Dict[str, Any]] = field(default_factory=list) + args: list[dict[str, Any]] = field(default_factory=list) # Global registry per script - use module-level dict @@ -57,7 +57,7 @@ def math_command( category: str, description: str = "", latex_template: str | None = None, - args: List[Dict[str, Any]] | None = None, + args: list[dict[str, Any]] | None = None, ) -> Callable[[F], F]: """Decorator to register a math command. @@ -130,7 +130,7 @@ def clear_registry() -> None: # ============================================================================= -def format_output(result: dict[str, Any], latex_template: str | None = None) -> Dict[str, Any]: +def format_output(result: dict[str, Any], latex_template: str | None = None) -> dict[str, Any]: """Format computation result as standardized JSON. Output structure: @@ -719,7 +719,7 @@ def register_commands( def run_command( args: argparse.Namespace, registry: dict[str, MathCommand] | None = None -) -> Dict[str, Any]: +) -> dict[str, Any]: """Run command based on parsed arguments. Args: @@ -764,15 +764,14 @@ def safe_compute(func: Callable, *args, timeout: int = 30, **kwargs) -> dict[str Returns: Result dict or error dict """ + import multiprocessing from concurrent.futures import ProcessPoolExecutor from concurrent.futures import TimeoutError as FuturesTimeout - def _wrapper(): - return func(*args, **kwargs) - + ctx = multiprocessing.get_context("spawn") try: - with ProcessPoolExecutor(max_workers=1) as executor: - future = executor.submit(_wrapper) + with ProcessPoolExecutor(max_workers=1, mp_context=ctx) as executor: + future = executor.submit(func, *args, **kwargs) try: return future.result(timeout=timeout) except FuturesTimeout: diff --git a/opc/scripts/cc_math/sympy_compute.py b/opc/scripts/cc_math/sympy_compute.py index 8e222e66..3fc51b33 100644 --- a/opc/scripts/cc_math/sympy_compute.py +++ b/opc/scripts/cc_math/sympy_compute.py @@ -1435,8 +1435,11 @@ def safe_solve(equation: str, var: str = "x", domain: str = "complex", timeout: Returns: dict with solutions on success, or error dict on failure/timeout """ + import multiprocessing + + ctx = multiprocessing.get_context("spawn") try: - with ProcessPoolExecutor(max_workers=1) as executor: + with ProcessPoolExecutor(max_workers=1, mp_context=ctx) as executor: future = executor.submit(_solve_internal, equation, var, domain) try: return future.result(timeout=timeout) diff --git a/opc/scripts/setup/claude_integration.py b/opc/scripts/setup/claude_integration.py index 18deaf36..158d10c0 100644 --- a/opc/scripts/setup/claude_integration.py +++ b/opc/scripts/setup/claude_integration.py @@ -382,12 +382,12 @@ def generate_migration_guidance( # Root scripts used by skills/hooks - shared between copy and symlink install ROOT_SCRIPTS = [ - "ast_grep_find.py", # /ast-grep-find skill - "braintrust_analyze.py", # session-end-cleanup hook - "qlty_check.py", # /qlty-check skill + "ast_grep_find.py", # /ast-grep-find skill + "braintrust_analyze.py", # session-end-cleanup hook + "qlty_check.py", # /qlty-check skill "research_implement_pipeline.py", # /mcp-chaining skill - "test_research_pipeline.py", # /mcp-chaining skill - "multi_tool_pipeline.py", # /skill-developer example + "test_research_pipeline.py", # /mcp-chaining skill + "multi_tool_pipeline.py", # /skill-developer example "recall_temporal_facts.py", # /system_overview skill ] @@ -431,6 +431,36 @@ def _copy_scripts(opc_source: Path, target_dir: Path) -> int: return count +def _copy_dotfile_scripts(opc_source: Path, target_dir: Path) -> int: + """Copy root-level scripts from .claude/scripts/ to target. + + These are scripts like status.py and tldr_stats.py that live in + .claude/scripts/ at the repo root, not in opc/scripts/. + + Args: + opc_source: Source OPC .claude directory + target_dir: Target .claude directory + + Returns: + Count of scripts copied + """ + count = 0 + source_scripts = opc_source.parent / ".claude" / "scripts" + target_scripts = target_dir / "scripts" + target_scripts.mkdir(parents=True, exist_ok=True) + + if not source_scripts.exists(): + return 0 + + for pattern in ["*.py", "*.sh"]: + for src in source_scripts.glob(pattern): + dst = target_scripts / src.name + shutil.copy2(src, dst) + count += 1 + + return count + + def install_opc_integration( target_dir: Path, opc_source: Path, @@ -539,6 +569,9 @@ def install_opc_integration( # Copy scripts (core, math, tldr directories + root scripts) result["installed_scripts"] = _copy_scripts(opc_source, target_dir) + # Copy root-level scripts from .claude/scripts/ (e.g. status.py) + result["installed_scripts"] += _copy_dotfile_scripts(opc_source, target_dir) + # Merge user items if requested if merge_user_items and existing and conflicts: # Merge non-conflicting hooks @@ -670,6 +703,9 @@ def install_opc_integration_symlink( # Copy scripts (core, math, tldr directories + root scripts) _copy_scripts(opc_source, target_dir) + # Copy root-level scripts from .claude/scripts/ (e.g. status.py) + _copy_dotfile_scripts(opc_source, target_dir) + result["success"] = True except Exception as e: @@ -754,9 +790,7 @@ def strip_tldr_hooks_from_settings(settings_path: Path) -> bool: if "startup" in matcher or "resume" in matcher: hooks = hook_group.get("hooks", []) new_hooks = [ - h - for h in hooks - if "session-start-tldr-cache" not in h.get("command", "") + h for h in hooks if "session-start-tldr-cache" not in h.get("command", "") ] if len(new_hooks) != len(hooks): modified = True @@ -782,7 +816,6 @@ def strip_tldr_hooks_from_settings(settings_path: Path) -> bool: return False - def get_platform_info() -> dict[str, str]: """Get current platform information. @@ -812,15 +845,15 @@ def find_latest_backup(claude_dir: Path) -> Path | None: # Files to preserve during uninstall (user data accumulated since install) PRESERVE_FILES = [ - "history.jsonl", # Command history - "mcp_config.json", # MCP server configs - ".env", # API keys and settings - "projects.json", # Project configs + "history.jsonl", # Command history + "mcp_config.json", # MCP server configs + ".env", # API keys and settings + "projects.json", # Project configs ] PRESERVE_DIRS = [ - "file-history", # File edit history - "projects", # Project-specific data + "file-history", # File edit history + "projects", # Project-specific data ] @@ -931,6 +964,32 @@ def uninstall_opc_integration( if result["preserved"]: msg_parts.append(f" Preserved user data: {', '.join(result['preserved'])}") + # Clean up CLAUDE_OPC_DIR from shell configs + import os as _os + + shell = _os.environ.get("SHELL", "") + configs_to_clean = [] + if "zsh" in shell: + configs_to_clean.append(_os.path.expanduser("~/.zshrc")) + elif "bash" in shell: + configs_to_clean.append(_os.path.expanduser("~/.bashrc")) + elif "fish" in shell: + fish_config = _os.path.expanduser("~/.config/fish/config.fish") + if _os.path.exists(fish_config): + configs_to_clean.append(fish_config) + + for config_path in configs_to_clean: + try: + with open(config_path, "r") as f: + lines = f.readlines() + new_lines = [l for l in lines if "CLAUDE_OPC_DIR" not in l] + if len(new_lines) < len(lines): + with open(config_path, "w") as f: + f.writelines(new_lines) + msg_parts.append(f" Removed CLAUDE_OPC_DIR from {_os.path.basename(config_path)}") + except Exception: + pass + result["message"] = "\n".join(msg_parts) result["success"] = True return result diff --git a/opc/scripts/setup/embedded_postgres.py b/opc/scripts/setup/embedded_postgres.py index 4cc9f6b5..bedeb38b 100644 --- a/opc/scripts/setup/embedded_postgres.py +++ b/opc/scripts/setup/embedded_postgres.py @@ -17,12 +17,14 @@ from urllib.parse import quote_plus -def start_embedded_postgres(pgdata: Path) -> dict[str, Any]: +def start_embedded_postgres(pgdata: Path, venv_path: Path | None = None) -> dict[str, Any]: """Start embedded postgres server using pgserver. Args: pgdata: Directory to store postgres data files. Will be created if it doesn't exist. + venv_path: Optional path to venv with pgserver installed. + If not provided, tries to import pgserver directly. Returns: dict with keys: @@ -31,26 +33,52 @@ def start_embedded_postgres(pgdata: Path) -> dict[str, Any]: - error: str (if failed) - server: PostgresServer instance (for cleanup) """ - try: - import pgserver - except ImportError: - return { - "success": False, - "error": "pgserver not installed. Install with: pip install pgserver", - } + import sys - try: - # Ensure pgdata directory exists - pgdata.mkdir(parents=True, exist_ok=True) + # Determine Python executable to use + if venv_path is not None: + if sys.platform == "win32": + python_exe = venv_path / "Scripts" / "python.exe" + else: + python_exe = venv_path / "bin" / "python" + else: + python_exe = sys.executable + + # Build script to start pgserver and get URI + start_script = f""" +import sys +sys.path.insert(0, '{venv_path}/lib/python3.12/site-packages') +from pgserver import get_server +server = get_server('{pgdata}') +print(server.get_uri()) +""" - # Start server (pgserver handles init if needed) - server = pgserver.get_server(str(pgdata)) - uri = server.get_uri() + import subprocess + try: + proc = subprocess.Popen( + [str(python_exe), "-c", start_script], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = proc.communicate(timeout=30) + + if proc.returncode == 0: + uri = stdout.decode().strip() + return { + "success": True, + "uri": uri, + } + else: + error_msg = stderr.decode().strip() if stderr else "Unknown error" + return { + "success": False, + "error": f"Failed to start pgserver: {error_msg[:200]}", + } + except subprocess.TimeoutExpired: return { - "success": True, - "uri": uri, - "server": server, + "success": False, + "error": "pgserver start timed out", } except Exception as e: return { @@ -260,7 +288,9 @@ async def setup_embedded_environment() -> dict[str, Any]: if venv_path.exists() and python_exe.exists(): # Verify pgserver is installed proc = await asyncio.create_subprocess_exec( - str(python_exe), "-c", "import pgserver", + str(python_exe), + "-c", + "import pgserver", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -270,7 +300,11 @@ async def setup_embedded_environment() -> dict[str, Any]: # Create venv with Python 3.12 proc = await asyncio.create_subprocess_exec( - "uv", "venv", str(venv_path), "--python", "3.12", + "uv", + "venv", + str(venv_path), + "--python", + "3.12", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -280,8 +314,13 @@ async def setup_embedded_environment() -> dict[str, Any]: # Install pgserver and psycopg2 proc = await asyncio.create_subprocess_exec( - "uv", "pip", "install", "pgserver", "psycopg2-binary", - "--python", str(python_exe), + "uv", + "pip", + "install", + "pgserver", + "psycopg2-binary", + "--python", + str(python_exe), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 142ddc78..c4395ed6 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -39,6 +39,7 @@ console = Console() except ImportError: rich_escape = lambda x: x # No escaping needed without Rich + # Fallback for minimal environments class Console: def print(self, *args, **kwargs): @@ -125,6 +126,19 @@ async def check_container_runtime() -> dict[str, Any]: - version: str | None - Version string - daemon_running: bool - True if service is responding """ + import socket + + def is_port_in_use(port: int, host: str = "localhost") -> bool: + """Check if a port is already in use.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + try: + result = sock.connect_ex((host, port)) + sock.close() + return result == 0 + except (socket.timeout, OSError): + return False + # Try Docker first (most common) result = await check_runtime_installed("docker") if result["installed"]: @@ -200,7 +214,9 @@ async def check_prerequisites_with_install_offers() -> dict[str, Any]: if not runtime_info["installed"]: await offer_docker_install() elif not runtime_info.get("daemon_running", False): - console.print(f" [yellow]{runtime_name.title()} is installed but the daemon is not running.[/yellow]") + console.print( + f" [yellow]{runtime_name.title()} is installed but the daemon is not running.[/yellow]" + ) if runtime_name == "docker": console.print(" Please start Docker Desktop or the Docker service.") else: @@ -209,24 +225,33 @@ async def check_prerequisites_with_install_offers() -> dict[str, Any]: # Retry loop for daemon startup max_retries = 3 for attempt in range(max_retries): - if Confirm.ask(f"\n Retry checking {runtime_name} daemon? (attempt {attempt + 1}/{max_retries})", default=True): + if Confirm.ask( + f"\n Retry checking {runtime_name} daemon? (attempt {attempt + 1}/{max_retries})", + default=True, + ): console.print(f" Checking {runtime_name} daemon...") await asyncio.sleep(2) # Give daemon time to start runtime_info = await check_runtime_installed(runtime_name) if runtime_info.get("daemon_running", False): result["docker"] = True result["docker_daemon_running"] = True - console.print(f" [green]OK[/green] {runtime_name.title()} daemon is now running!") + console.print( + f" [green]OK[/green] {runtime_name.title()} daemon is now running!" + ) break else: - console.print(f" [yellow]{runtime_name.title()} daemon still not running.[/yellow]") + console.print( + f" [yellow]{runtime_name.title()} daemon still not running.[/yellow]" + ) else: break # Check elan/Lean4 (optional, for theorem proving with /prove skill) if not result["elan"]: console.print("\n [dim]Optional: Lean4/elan not found (needed for /prove skill)[/dim]") - console.print(" [dim]Install with: curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh[/dim]") + console.print( + " [dim]Install with: curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh[/dim]" + ) # elan is optional, so exclude from all_present check result["all_present"] = all([result["docker"], result["python"], result["uv"]]) @@ -390,7 +415,9 @@ async def prompt_embedding_config() -> dict[str, str]: console.print(" 3. openai - OpenAI API (requires API key)") console.print(" 4. voyage - Voyage AI API (requires API key)") - provider = Prompt.ask("Embedding provider", choices=["local", "ollama", "openai", "voyage"], default="local") + provider = Prompt.ask( + "Embedding provider", choices=["local", "ollama", "openai", "voyage"], default="local" + ) config = {"provider": provider} @@ -407,7 +434,7 @@ async def prompt_api_keys() -> dict[str, str]: """Prompt user for optional API keys. Returns: - dict with keys: perplexity, nia, braintrust + dict with keys: perplexity, nia, braintrust, firecrawl, morph """ console.print("\n[bold]API Keys (optional)[/bold]") console.print("Press Enter to skip any key you don't have.\n") @@ -415,11 +442,15 @@ async def prompt_api_keys() -> dict[str, str]: perplexity = Prompt.ask("Perplexity API key (web search)", default="") nia = Prompt.ask("Nia API key (documentation search)", default="") braintrust = Prompt.ask("Braintrust API key (observability)", default="") + firecrawl = Prompt.ask("Firecrawl API key (web scraping)", default="") + morph = Prompt.ask("Morph API key (data extraction)", default="") return { "perplexity": perplexity, "nia": nia, "braintrust": braintrust, + "firecrawl": firecrawl, + "morph": morph, } @@ -448,11 +479,11 @@ def generate_env_file(config: dict[str, Any], env_path: Path) -> None: lines.append(f"# Database Mode: {mode}") if mode == "docker": - host = db.get('host', 'localhost') - port = db.get('port', 5432) - database = db.get('database', 'continuous_claude') - user = db.get('user', 'claude') - password = db.get('password', '') + host = db.get("host", "localhost") + port = db.get("port", 5432) + database = db.get("database", "continuous_claude") + user = db.get("user", "claude") + password = db.get("password", "") lines.append(f"POSTGRES_HOST={host}") lines.append(f"POSTGRES_PORT={port}") lines.append(f"POSTGRES_DB={database}") @@ -461,7 +492,9 @@ def generate_env_file(config: dict[str, Any], env_path: Path) -> None: lines.append(f"POSTGRES_PASSWORD={password}") lines.append("") lines.append("# Connection string for scripts (canonical name)") - lines.append(f"CONTINUOUS_CLAUDE_DB_URL=postgresql://{user}:{password}@{host}:{port}/{database}") + lines.append( + f"CONTINUOUS_CLAUDE_DB_URL=postgresql://{user}:{password}@{host}:{port}/{database}" + ) elif mode == "embedded": pgdata = db.get("pgdata", "") venv = db.get("venv", "") @@ -500,6 +533,10 @@ def generate_env_file(config: dict[str, Any], env_path: Path) -> None: lines.append(f"NIA_API_KEY={api_keys['nia']}") if api_keys.get("braintrust"): lines.append(f"BRAINTRUST_API_KEY={api_keys['braintrust']}") + if api_keys.get("firecrawl"): + lines.append(f"FIRECRAWL_API_KEY={api_keys['firecrawl']}") + if api_keys.get("morph"): + lines.append(f"MORPH_API_KEY={api_keys['morph']}") lines.append("") # Write file @@ -570,15 +607,30 @@ async def run_setup_wizard() -> None: console.print(" [bold]docker[/bold] - PostgreSQL in Docker (recommended)") console.print(" [bold]embedded[/bold] - Embedded PostgreSQL (no Docker needed)") console.print(" [bold]sqlite[/bold] - SQLite fallback (simplest, no cross-terminal)") - db_mode = Prompt.ask("\n Database mode", choices=["docker", "embedded", "sqlite"], default="docker") + db_mode = Prompt.ask( + "\n Database mode", choices=["docker", "embedded", "sqlite"], default="docker" + ) if db_mode == "embedded": from scripts.setup.embedded_postgres import setup_embedded_environment + console.print(" Setting up embedded postgres (creates Python 3.12 environment)...") embed_result = await setup_embedded_environment() if embed_result["success"]: - console.print(f" [green]OK[/green] Embedded environment ready at {embed_result['venv']}") - db_config = {"mode": "embedded", "pgdata": str(embed_result["pgdata"]), "venv": str(embed_result["venv"])} + console.print( + f" [green]OK[/green] Embedded environment ready at {embed_result['venv']}" + ) + console.print( + " [dim]To start server: {venv}/bin/python -c \"from pgserver import get_server; get_server('{pgdata}')\"".format( + venv=embed_result["venv"], pgdata=embed_result["pgdata"] + ) + ) + console.print(" [dim]To run schema: psql -f docker/init-schema.sql ") + db_config = { + "mode": "embedded", + "pgdata": str(embed_result["pgdata"]), + "venv": str(embed_result["venv"]), + } else: console.print(f" [red]ERROR[/red] {embed_result.get('error', 'Unknown')}") console.print(" Falling back to Docker mode") @@ -586,10 +638,14 @@ async def run_setup_wizard() -> None: if db_mode == "sqlite": db_config = {"mode": "sqlite"} - console.print(" [yellow]Note:[/yellow] Cross-terminal coordination disabled in SQLite mode") + console.print( + " [yellow]Note:[/yellow] Cross-terminal coordination disabled in SQLite mode" + ) if db_mode == "docker": - console.print(" [dim]Customize host/port for containers (podman, nerdctl) or remote postgres.[/dim]") + console.print( + " [dim]Customize host/port for containers (podman, nerdctl) or remote postgres.[/dim]" + ) if Confirm.ask("Configure database connection?", default=True): db_config = await prompt_database_config() password = Prompt.ask("Database password", password=True, default="claude_dev") @@ -626,19 +682,28 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] Generated {env_path}") # Step 5: Container stack (Sandbox Infrastructure) + # Skip if using embedded PostgreSQL or SQLite runtime = prereqs.get("container_runtime", "docker") console.print(f"\n[bold]Step 6/13: Container Stack (Sandbox Infrastructure)[/bold]") - console.print(" The sandbox requires PostgreSQL and Redis for:") - console.print(" - Agent coordination and scheduling") - console.print(" - Build cache and LSP index storage") - console.print(" - Real-time agent status") - if Confirm.ask(f"Start {runtime} stack (PostgreSQL, Redis)?", default=True): - from scripts.setup.docker_setup import run_migrations, set_container_runtime, start_docker_stack, wait_for_services + + if db_mode == "embedded": + console.print(" [dim]Skipped - using embedded PostgreSQL (no Docker needed)[/dim]") + elif db_mode == "sqlite": + console.print(" [dim]Skipped - using SQLite (no Docker needed)[/dim]") + elif Confirm.ask(f"Start {runtime} stack (PostgreSQL, Redis)?", default=True): + from scripts.setup.docker_setup import ( + run_migrations, + set_container_runtime, + start_docker_stack, + wait_for_services, + ) # Set the detected runtime before starting set_container_runtime(runtime) - console.print(f" [dim]Starting containers (first run downloads ~500MB, may take a few minutes)...[/dim]") + console.print( + f" [dim]Starting containers (first run downloads ~500MB, may take a few minutes)...[/dim]" + ) result = await start_docker_stack(env_file=env_path) if result["success"]: console.print(f" [green]OK[/green] {runtime.title()} stack started") @@ -656,7 +721,11 @@ async def run_setup_wizard() -> None: # Step 6: Migrations console.print("\n[bold]Step 7/13: Database Setup[/bold]") - if Confirm.ask("Run database migrations?", default=True): + if db_mode == "embedded": + console.print(" [dim]Skipped - run manually after starting server[/dim]") + elif db_mode == "sqlite": + console.print(" [dim]Skipped - SQLite does not need migrations[/dim]") + elif Confirm.ask("Run database migrations?", default=True): from scripts.setup.docker_setup import run_migrations, set_container_runtime # Ensure runtime is set (in case step 5 was skipped) @@ -739,7 +808,12 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] Installed {result['installed_skills']} skills") console.print(f" [green]OK[/green] Installed {result['installed_rules']} rules") console.print(f" [green]OK[/green] Installed {result['installed_agents']} agents") - console.print(f" [green]OK[/green] Installed {result['installed_servers']} MCP servers") + console.print( + f" [green]OK[/green] Installed {result['installed_servers']} MCP servers" + ) + console.print( + f" [green]OK[/green] Installed {result['installed_scripts']} scripts" + ) if result["merged_items"]: console.print( f" [green]OK[/green] Merged {len(result['merged_items'])} custom items" @@ -753,7 +827,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] {build_msg}") else: console.print(f" [yellow]WARN[/yellow] {build_msg}") - console.print(" [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]") + console.print( + " [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]" + ) else: console.print(f" [red]ERROR[/red] {result.get('error', 'Unknown error')}") elif choice == "3": @@ -761,9 +837,13 @@ async def run_setup_wizard() -> None: result = install_opc_integration_symlink(claude_dir, opc_source) if result["success"]: - console.print(f" [green]OK[/green] Symlinked: {', '.join(result['symlinked_dirs'])}") + console.print( + f" [green]OK[/green] Symlinked: {', '.join(result['symlinked_dirs'])}" + ) if result["backed_up_dirs"]: - console.print(f" [green]OK[/green] Backed up: {', '.join(result['backed_up_dirs'])}") + console.print( + f" [green]OK[/green] Backed up: {', '.join(result['backed_up_dirs'])}" + ) console.print(" [dim]Changes in ~/.claude/ now sync to repo automatically[/dim]") # Build TypeScript hooks @@ -774,7 +854,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] {build_msg}") else: console.print(f" [yellow]WARN[/yellow] {build_msg}") - console.print(" [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]") + console.print( + " [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]" + ) else: console.print(f" [red]ERROR[/red] {result.get('error', 'Unknown error')}") else: @@ -801,7 +883,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] Installed {result['installed_skills']} skills") console.print(f" [green]OK[/green] Installed {result['installed_rules']} rules") console.print(f" [green]OK[/green] Installed {result['installed_agents']} agents") - console.print(f" [green]OK[/green] Installed {result['installed_servers']} MCP servers") + console.print( + f" [green]OK[/green] Installed {result['installed_servers']} MCP servers" + ) # Build TypeScript hooks console.print(" Building TypeScript hooks...") @@ -811,7 +895,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] {build_msg}") else: console.print(f" [yellow]WARN[/yellow] {build_msg}") - console.print(" [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]") + console.print( + " [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]" + ) else: console.print(f" [red]ERROR[/red] {result.get('error', 'Unknown error')}") elif choice == "2": @@ -819,7 +905,9 @@ async def run_setup_wizard() -> None: result = install_opc_integration_symlink(claude_dir, opc_source) if result["success"]: - console.print(f" [green]OK[/green] Symlinked: {', '.join(result['symlinked_dirs'])}") + console.print( + f" [green]OK[/green] Symlinked: {', '.join(result['symlinked_dirs'])}" + ) console.print(" [dim]Changes in ~/.claude/ now sync to repo automatically[/dim]") # Build TypeScript hooks @@ -830,37 +918,73 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] {build_msg}") else: console.print(f" [yellow]WARN[/yellow] {build_msg}") - console.print(" [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]") + console.print( + " [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]" + ) else: console.print(f" [red]ERROR[/red] {result.get('error', 'Unknown error')}") else: console.print(" Skipped integration installation") - # Set CLAUDE_OPC_DIR environment variable for skills to find scripts - console.print(" Setting CLAUDE_OPC_DIR environment variable...") + # Set CLAUDE_OPC_DIR and CLAUDE_CC_DIR environment variables + # CLAUDE_OPC_DIR = opc/ directory (Python scripts, MCP runtime) + # CLAUDE_CC_DIR = repository root (contains .claude/scripts/, .claude/skills/) + console.print(" Setting environment variables...") shell_config = None shell = os.environ.get("SHELL", "") if "zsh" in shell: shell_config = Path.home() / ".zshrc" elif "bash" in shell: shell_config = Path.home() / ".bashrc" + elif "fish" in shell: + fish_config_dir = Path.home() / ".config" / "fish" + fish_config_dir.mkdir(parents=True, exist_ok=True) + shell_config = fish_config_dir / "config.fish" opc_dir = _project_root # Use script location, not cwd (robust if invoked from elsewhere) - if shell_config and shell_config.exists(): - content = shell_config.read_text() - export_line = f'export CLAUDE_OPC_DIR="{opc_dir}"' + cc_dir = opc_dir.parent # Repository root (Continuous-Claude-v3/) + + if shell_config and (shell_config.exists() or "fish" in shell): + if shell_config.exists(): + content = shell_config.read_text() + else: + content = "" + changed = False + export_opc = None + export_cc = None + + if "zsh" in shell or "bash" in shell: + export_opc = f'export CLAUDE_OPC_DIR="{opc_dir}"' + export_cc = f'export CLAUDE_CC_DIR="{cc_dir}"' + elif "fish" in shell: + export_opc = f'set -gx CLAUDE_OPC_DIR "{opc_dir}"' + export_cc = f'set -gx CLAUDE_CC_DIR "{cc_dir}"' + if "CLAUDE_OPC_DIR" not in content: with open(shell_config, "a") as f: - f.write(f"\n# Continuous-Claude OPC directory (for skills to find scripts)\n{export_line}\n") - console.print(f" [green]OK[/green] Added CLAUDE_OPC_DIR to {shell_config.name}") + f.write(f"\n# Continuous-Claude directories\n{export_opc}\n{export_cc}\n") + changed = True + elif "CLAUDE_CC_DIR" not in content: + with open(shell_config, "a") as f: + f.write(f"\n# Continuous-Claude root directory\n{export_cc}\n") + changed = True + + if changed: + console.print( + f" [green]OK[/green] Added CLAUDE_OPC_DIR and CLAUDE_CC_DIR to {shell_config.name}" + ) else: - console.print(f" [dim]CLAUDE_OPC_DIR already in {shell_config.name}[/dim]") + console.print( + f" [dim]CLAUDE_OPC_DIR and CLAUDE_CC_DIR already in {shell_config.name}[/dim]" + ) elif sys.platform == "win32": console.print(" [yellow]NOTE[/yellow] Add to your environment:") console.print(f' set CLAUDE_OPC_DIR="{opc_dir}"') + console.print(f' set CLAUDE_CC_DIR="{cc_dir}"') else: console.print(" [yellow]NOTE[/yellow] Add to your shell config:") console.print(f' export CLAUDE_OPC_DIR="{opc_dir}"') + console.print(f' export CLAUDE_CC_DIR="{cc_dir}"') # Step 8: Math Features (Optional) console.print("\n[bold]Step 9/13: Math Features (Optional)[/bold]") @@ -921,8 +1045,94 @@ async def run_setup_wizard() -> None: console.print(" Skipped math features") console.print(" [dim]Install later with: uv sync --extra math[/dim]") - # Step 9: TLDR Code Analysis Tool - console.print("\n[bold]Step 10/13: TLDR Code Analysis Tool[/bold]") + # Step 11: qlty CLI (Universal Code Quality) + console.print("\n[bold]Step 11/15: qlty Code Quality Tool[/bold]") + console.print( + " qlty is a universal code quality tool supporting 70+ linters for 40+ languages." + ) + console.print(" Unlocks: qlty-check, qlty-during-development, fix (deps scope) skills.") + console.print("") + console.print(" [dim]Free and open source - no API key needed.[/dim]") + + # Check if qlty is already installed + if shutil.which("qlty"): + console.print(" [green]OK[/green] qlty is already installed") + elif Confirm.ask("\nInstall qlty code quality tool?", default=True): + console.print(" Installing qlty via curl...") + try: + result = subprocess.run( + ["curl", "-fsSL", "https://qlty.sh/install.sh"], + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode == 0: + install_result = subprocess.run( + result.stdout.strip(), + shell=True, + capture_output=True, + text=True, + timeout=120, + ) + if install_result.returncode == 0: + console.print(" [green]OK[/green] qlty installed") + else: + console.print(" [yellow]WARN[/yellow] qlty install had issues") + else: + console.print(" [yellow]WARN[/yellow] Could not install qlty") + except subprocess.TimeoutExpired: + console.print(" [yellow]WARN[/yellow] Installation timed out") + except Exception as e: + console.print(f" [yellow]WARN[/yellow] {e}") + else: + console.print(" Skipped qlty installation") + console.print(" [dim]Install later with: curl -fsSL https://qlty.sh/install.sh | sh[/dim]") + + # Step 12: ast-grep (AST-based Code Search) + console.print("\n[bold]Step 12/15: ast-grep Code Analysis Tool[/bold]") + console.print(" ast-grep performs AST-based structural code search and refactoring.") + console.print(" Unlocks: ast-grep-find, search-router, search-tools skills.") + console.print("") + console.print(" [dim]Free and open source - no API key needed.[/dim]") + + # Check if ast-grep is already installed + if shutil.which("ast-grep"): + console.print(" [green]OK[/green] ast-grep is already installed") + elif Confirm.ask("\nInstall ast-grep code analysis tool?", default=True): + console.print(" Installing ast-grep...") + try: + result = subprocess.run( + ["cargo", "install", "ast-grep"], + capture_output=True, + text=True, + timeout=600, + ) + if result.returncode == 0: + console.print(" [green]OK[/green] ast-grep installed via cargo") + else: + result = subprocess.run( + ["npm", "install", "-g", "ast-grep"], + capture_output=True, + text=True, + timeout=120, + ) + if result.returncode == 0: + console.print(" [green]OK[/green] ast-grep installed via npm") + else: + console.print(" [yellow]WARN[/yellow] Could not install ast-grep") + console.print(" Try: cargo install ast-grep") + except subprocess.TimeoutExpired: + console.print( + " [yellow]WARN[/yellow] Installation timed out (cargo builds from source)" + ) + except Exception as e: + console.print(f" [yellow]WARN[/yellow] {e}") + else: + console.print(" Skipped ast-grep installation") + console.print(" [dim]Install later with: cargo install ast-grep[/dim]") + + # Step 13: TLDR Code Analysis Tool + console.print("\n[bold]Step 13/15: TLDR Code Analysis Tool[/bold]") console.print(" TLDR provides token-efficient code analysis for LLMs:") console.print(" - 95% token savings vs reading raw files") console.print(" - 155x faster queries with daemon mode") @@ -931,7 +1141,21 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [dim]Note: First semantic search downloads ~1.3GB embedding model.[/dim]") - if Confirm.ask("\nInstall TLDR code analysis tool?", default=True): + # Check if tldr is already installed (llm-tldr, not tldr-pages) + is_llm_tldr = False + tldr_check = shutil.which("tldr") + if tldr_check: + verify_result = subprocess.run( + [tldr_check, "--help"], + capture_output=True, + text=True, + timeout=10, + ) + is_llm_tldr = any(cmd in verify_result.stdout for cmd in ["tree", "structure", "daemon"]) + + if is_llm_tldr: + console.print(" [green]OK[/green] TLDR is already installed") + elif Confirm.ask("\nInstall TLDR code analysis tool?", default=True): console.print(" Installing TLDR...") import subprocess @@ -957,13 +1181,21 @@ async def run_setup_wizard() -> None: timeout=10, ) # Check if this is llm-tldr (has 'tree', 'structure', 'daemon') not tldr-pages - is_llm_tldr = any(cmd in verify_result.stdout for cmd in ["tree", "structure", "daemon"]) + is_llm_tldr = any( + cmd in verify_result.stdout for cmd in ["tree", "structure", "daemon"] + ) if verify_result.returncode == 0 and is_llm_tldr: console.print(" [green]OK[/green] TLDR CLI available") elif verify_result.returncode == 0 and not is_llm_tldr: - console.print(" [yellow]WARN[/yellow] Wrong tldr detected (tldr-pages, not llm-tldr)") - console.print(" [yellow] [/yellow] The 'tldr' command is shadowed by tldr-pages.") - console.print(" [yellow] [/yellow] Uninstall tldr-pages: pip uninstall tldr") + console.print( + " [yellow]WARN[/yellow] Wrong tldr detected (tldr-pages, not llm-tldr)" + ) + console.print( + " [yellow] [/yellow] The 'tldr' command is shadowed by tldr-pages." + ) + console.print( + " [yellow] [/yellow] Uninstall tldr-pages: pip uninstall tldr" + ) console.print(" [yellow] [/yellow] Or use full path: ~/.local/bin/tldr") if is_llm_tldr: @@ -977,14 +1209,15 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [bold]Semantic Search Configuration[/bold]") console.print(" Natural language code search using AI embeddings.") - console.print(" [dim]First run downloads ~1.3GB model and indexes your codebase.[/dim]") + console.print( + " [dim]First run downloads ~1.3GB model and indexes your codebase.[/dim]" + ) console.print(" [dim]Auto-reindexes in background when files change.[/dim]") if Confirm.ask("\n Enable semantic search?", default=True): # Get threshold threshold_str = Prompt.ask( - " Auto-reindex after how many file changes?", - default="20" + " Auto-reindex after how many file changes?", default="20" ) try: threshold = int(threshold_str) @@ -1005,6 +1238,7 @@ async def run_setup_wizard() -> None: has_gpu = False try: import torch + has_gpu = torch.cuda.is_available() or torch.backends.mps.is_available() except ImportError: pass # No torch = assume no GPU @@ -1025,7 +1259,9 @@ async def run_setup_wizard() -> None: settings_path.parent.mkdir(parents=True, exist_ok=True) settings_path.write_text(json.dumps(settings, indent=2)) - console.print(f" [green]OK[/green] Semantic search enabled (threshold: {threshold})") + console.print( + f" [green]OK[/green] Semantic search enabled (threshold: {threshold})" + ) # Offer to pre-download embedding model # Note: We only download the model here, not index any directory. @@ -1035,7 +1271,13 @@ async def run_setup_wizard() -> None: try: # Just load the model to trigger download (no indexing) download_result = subprocess.run( - [sys.executable, "-c", f"from tldr.semantic import get_model; get_model('{model}')"], + [ + "uv", + "run", + "python", + "-c", + f"from tldr.semantic import get_model; get_model('{model}')", + ], capture_output=True, text=True, timeout=timeout, @@ -1052,7 +1294,9 @@ async def run_setup_wizard() -> None: except Exception as e: console.print(f" [yellow]WARN[/yellow] {e}") else: - console.print(" [dim]Model downloads on first use of: tldr semantic index .[/dim]") + console.print( + " [dim]Model downloads on first use of: tldr semantic index .[/dim]" + ) else: console.print(" Semantic search disabled") console.print(" [dim]Enable later in .claude/settings.json[/dim]") @@ -1073,15 +1317,19 @@ async def run_setup_wizard() -> None: console.print(" [dim]Install later with: uv tool install llm-tldr[/dim]") # Ask to disable hooks since they are pre-configured in settings.json - if Confirm.ask("\n Disable TLDR hooks in settings.json? (Avoids crashes if TLDR missing)", default=False): + if Confirm.ask( + "\n Disable TLDR hooks in settings.json? (Avoids crashes if TLDR missing)", + default=False, + ): settings_path = get_global_claude_dir() / "settings.json" if settings_path.exists(): from scripts.setup.claude_integration import strip_tldr_hooks_from_settings + if strip_tldr_hooks_from_settings(settings_path): console.print(" [green]OK[/green] TLDR hooks disabled") # Step 10: Diagnostics Tools (Shift-Left Feedback) - console.print("\n[bold]Step 11/13: Diagnostics Tools (Shift-Left Feedback)[/bold]") + console.print("\n[bold]Step 14/15: Diagnostics Tools (Shift-Left Feedback)[/bold]") console.print(" Claude gets immediate type/lint feedback after editing files.") console.print(" This catches errors before tests run (shift-left).") console.print("") @@ -1119,7 +1367,7 @@ async def run_setup_wizard() -> None: console.print(" [dim]TypeScript, Go, Rust coming soon.[/dim]") # Step 11: Loogle (Lean 4 type search for /prove skill) - console.print("\n[bold]Step 12/13: Loogle (Lean 4 Type Search)[/bold]") + console.print("\n[bold]Step 15/15: Loogle (Lean 4 Type Search)[/bold]") console.print(" Loogle enables type-aware search of Mathlib theorems:") console.print(" - Used by /prove skill for theorem proving") console.print(" - Search by type signature (e.g., 'Nontrivial _ ↔ _')") @@ -1133,7 +1381,9 @@ async def run_setup_wizard() -> None: # Check elan prerequisite if not shutil.which("elan"): console.print(" [yellow]WARN[/yellow] Lean 4 (elan) not installed") - console.print(" Install with: curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh") + console.print( + " Install with: curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh" + ) console.print(" Then re-run the wizard to install Loogle.") else: console.print(" [green]OK[/green] elan found") @@ -1161,7 +1411,9 @@ async def run_setup_wizard() -> None: if result.returncode == 0: console.print(" [green]OK[/green] Updated") else: - console.print(f" [yellow]WARN[/yellow] Update failed: {result.stderr[:100]}") + console.print( + f" [yellow]WARN[/yellow] Update failed: {result.stderr[:100]}" + ) else: console.print(f" Cloning Loogle to {loogle_home}...") loogle_home.parent.mkdir(parents=True, exist_ok=True) @@ -1170,7 +1422,7 @@ async def run_setup_wizard() -> None: ["git", "clone", "https://github.com/nomeata/loogle", str(loogle_home)], capture_output=True, text=True, - timeout=120, + timeout=600, # 10 min for 2GB repo ) if result.returncode == 0: console.print(" [green]OK[/green] Cloned") @@ -1198,10 +1450,16 @@ async def run_setup_wizard() -> None: else: console.print(f" [red]ERROR[/red] Build failed") console.print(f" {result.stderr[:200]}") - console.print(" You can build manually: cd ~/.local/share/loogle && lake build") + console.print( + " You can build manually: cd ~/.local/share/loogle && lake build" + ) except subprocess.TimeoutExpired: - console.print(" [yellow]WARN[/yellow] Build timed out (this is normal for first build)") - console.print(" Continue building manually: cd ~/.local/share/loogle && lake build") + console.print( + " [yellow]WARN[/yellow] Build timed out (this is normal for first build)" + ) + console.print( + " Continue building manually: cd ~/.local/share/loogle && lake build" + ) except Exception as e: console.print(f" [red]ERROR[/red] {e}") @@ -1254,7 +1512,7 @@ async def run_setup_wizard() -> None: console.print(f" [yellow]WARN[/yellow] loogle_search.py not found at {src_script}") console.print("") - console.print(" [dim]Usage: loogle-search \"Nontrivial _ ↔ _\"[/dim]") + console.print(' [dim]Usage: loogle-search "Nontrivial _ ↔ _"[/dim]') console.print(" [dim]Or use /prove skill which calls it automatically[/dim]") else: console.print(" Skipped Loogle installation") @@ -1344,9 +1602,7 @@ async def main(): # Show menu if no args if len(sys.argv) == 1: - console.print( - Panel.fit("[bold]CLAUDE CONTINUITY KIT v3[/bold]", border_style="blue") - ) + console.print(Panel.fit("[bold]CLAUDE CONTINUITY KIT v3[/bold]", border_style="blue")) console.print("\n[bold]Options:[/bold]") console.print(" [bold]1[/bold] - Install / Update") console.print(" [bold]2[/bold] - Uninstall (restore backup)")