diff --git a/AGENTS.md b/AGENTS.md index 24513c4..b3be946 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,23 @@ +## ⚠️ CRITICAL: Use the Question Tool for Planning and Decisions + +**STRONGLY prefer using mcp_question when:** +- Planning how to approach a task with multiple valid options +- Facing design decisions or architectural choices +- Uncertain about user intent or requirements +- About to make assumptions that could lead to rework +- Working with ambiguous requests + +**During planning phases, ASK before acting:** +- "I see 3 approaches to this. Which do you prefer: A, B, or C?" +- "Should I prioritize X or Y for this implementation?" +- "This could mean either A or B. Which did you intend?" + +**It's better to ask and get it right than to guess and need to redo work.** + +See detailed Question Tool guidance in CLAUDE.md below. + +--- + ## ⚠️ CRITICAL: File Access in Chezmoi Repository **When working in `/home/xevion/.local/share/chezmoi`, ONLY access files within this directory.** diff --git a/CLAUDE.md b/CLAUDE.md index 3e62b02..949d5c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -322,6 +322,50 @@ These are very different operations - which did you mean?" 4. **Prefer explanations** over automated actions 5. **Present options** when requests are ambiguous (direction, location, scope) 6. **Show consequences** before destructive operations +7. **USE THE QUESTION TOOL** - When planning or facing design decisions, use mcp_question to present options and get user input proactively + +## Question Tool Usage in Chezmoi Context + +**STRONGLY prefer using the Question tool when working in this repository, especially for:** + +**Planning & Ambiguity Resolution:** +- **Direction questions** - Source→Target vs Target→Source operations + - "Should I apply changes from source to target, or import from target to source?" +- **File location questions** - Which CLAUDE.md/AGENTS.md file to edit + - "Which file should I edit: project-level CLAUDE.md or user-level home/dot_claude/CLAUDE.md.tmpl?" +- **Platform targeting** - Which OS/environment to optimize for + - "Should this template prioritize Windows, Linux, or work equally on both?" +- **Template complexity** - How sophisticated the conditional logic should be + - "Should I add platform detection here, or keep it simple?" + +**Before Making Changes:** +- **Encryption decisions** - Whether files should be encrypted + - "This file contains [type of data]. Should I encrypt it with age or leave it plain?" +- **Permission choices** - Using `private_` prefix or standard permissions + - "Should this config file use private_ prefix (600 permissions)?" +- **Template vs static** - Whether to use `.tmpl` suffix + - "Should this be a template file, or a static file?" + +**Examples of good question tool usage:** +``` +User: "Add my GitHub token to the config" +AI: *Uses Question tool* "How should I handle this token? + A. Store encrypted with age (encrypted_*.age) + B. Reference from Doppler ({{ dopplerProjectJson.GITHUB_TOKEN }}) + C. Add to .chezmoiignore and manage manually" +``` + +``` +User: "Update the shell config" +AI: *Uses Question tool* "Which file should I edit? + A. home/dot_bashrc.tmpl (bash configuration) + B. home/dot_config/nushell/config.nu.tmpl (nushell configuration) + C. home/dot_config/fish/config.fish (fish configuration)" +``` + +**When NOT to use:** Trivial clarifications that don't affect functionality or security. + +**Remember:** In a templating/dotfiles context, wrong assumptions lead to broken deployments. Always confirm when uncertain. # Extended Documentation diff --git a/home/.managed/mise/config.toml b/home/.managed/mise/config.toml index 4ec8692..97cb5b2 100644 --- a/home/.managed/mise/config.toml +++ b/home/.managed/mise/config.toml @@ -26,3 +26,4 @@ delta = "latest" lazydocker = "latest" lazyssh = "latest" lazyjournal = "latest" +gradle-profiler = "latest" diff --git a/home/claude-settings.json b/home/claude-settings.json index 77f96ef..0d93f8a 100644 --- a/home/claude-settings.json +++ b/home/claude-settings.json @@ -241,7 +241,8 @@ "rust-analyzer-lsp@claude-plugins-official": true, "ralph-wiggum@claude-plugins-official": true, "superpowers@superpowers-marketplace": true, - "gopls-lsp@claude-plugins-official": true + "gopls-lsp@claude-plugins-official": true, + "code-review@claude-code-plugins": true }, "alwaysThinkingEnabled": true } diff --git a/home/dot_local/bin/executable_share.ts.tmpl b/home/dot_local/bin/executable_share.ts.tmpl index 45ef216..d88c00f 100755 --- a/home/dot_local/bin/executable_share.ts.tmpl +++ b/home/dot_local/bin/executable_share.ts.tmpl @@ -8,12 +8,6 @@ * share file.png # Upload specific file * cat file.txt | share # Upload from stdin * share -c video.mov # Convert then upload - * - * Environment Variables: - * R2_ENDPOINT S3-compatible endpoint URL - * R2_ACCESS_KEY_ID Access key ID - * R2_SECRET_ACCESS_KEY Secret access key - * R2_BUCKET Bucket name */ import { existsSync, fstatSync } from "fs"; @@ -65,12 +59,6 @@ type IntervalHandle = ReturnType; const DOMAIN = "https://i.xevion.dev"; -const REQUIRED_ENV = [ - "R2_ENDPOINT", - "R2_ACCESS_KEY_ID", - "R2_SECRET_ACCESS_KEY", - "R2_BUCKET", -]; const ENV = { endpoint: "{{ dopplerProjectJson.R2_ENDPOINT }}", accessKeyId: "{{ dopplerProjectJson.R2_ACCESS_KEY_ID }}", @@ -591,6 +579,33 @@ async function hasCommand(cmd: string): Promise { } } +async function runMediaCommand(cmd: string[]): Promise { + try { + const proc = Bun.spawn(cmd, { + stdout: "ignore", + stderr: "pipe", + }); + + const exitCode = await proc.exited; + + if (exitCode !== 0) { + const stderr = await new Response(proc.stderr).text(); + const errorLines = stderr + .split('\n') + .filter(line => line.trim()) + .slice(-10) // Last 10 non-empty lines + .join('\n'); + + throw new Error(`Command failed with exit code ${exitCode}\n${errorLines}`); + } + } catch (e) { + if (e instanceof Error) { + throw e; + } + throw new Error(`Command failed: ${String(e)}`); + } +} + async function writeTempFile(buffer: Buffer, ext = ""): Promise { const path = join(tmpdir(), `share-probe-${nanoid(8)}${ext}`); await Bun.write(path, buffer); @@ -824,8 +839,11 @@ async function remuxVideo(buffer: Buffer): Promise { const outputPath = join(tmpdir(), `share-remux-${nanoid(8)}.webm`); try { - // Remux without re-encoding, adding faststart for MP4 - await $`ffmpeg -y -i ${inputPath} -c copy -fflags +genpts ${outputPath}`.quiet(); + await runMediaCommand([ + "ffmpeg", "-y", "-i", inputPath, + "-c", "copy", "-fflags", "+genpts", + outputPath + ]); const result = Buffer.from(await Bun.file(outputPath).arrayBuffer()); spinner.stop(); @@ -833,9 +851,9 @@ async function remuxVideo(buffer: Buffer): Promise { return result; } catch (e) { spinner.stop(); - throw new Error( - `Video remux failed: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(chalk.hex(COLORS.error)("\nRemux error:")); + console.error(chalk.hex(COLORS.dim)(e instanceof Error ? e.message : String(e))); + throw new Error("Video remux failed"); } finally { await cleanupTempFiles(inputPath, outputPath); } @@ -849,7 +867,11 @@ async function addFaststart(buffer: Buffer): Promise { const outputPath = join(tmpdir(), `share-faststart-${nanoid(8)}.mp4`); try { - await $`ffmpeg -y -i ${inputPath} -c copy -movflags +faststart ${outputPath}`.quiet(); + await runMediaCommand([ + "ffmpeg", "-y", "-i", inputPath, + "-c", "copy", "-movflags", "+faststart", + outputPath + ]); const result = Buffer.from(await Bun.file(outputPath).arrayBuffer()); spinner.stop(); @@ -857,9 +879,9 @@ async function addFaststart(buffer: Buffer): Promise { return result; } catch (e) { spinner.stop(); - throw new Error( - `Faststart optimization failed: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(chalk.hex(COLORS.error)("\nFaststart optimization error:")); + console.error(chalk.hex(COLORS.dim)(e instanceof Error ? e.message : String(e))); + throw new Error("Faststart optimization failed"); } finally { await cleanupTempFiles(inputPath, outputPath); } @@ -873,7 +895,13 @@ async function reencodeVideo(buffer: Buffer): Promise { const outputPath = join(tmpdir(), `share-reencode-${nanoid(8)}.mp4`); try { - await $`ffmpeg -y -i ${inputPath} -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k -movflags +faststart ${outputPath}`.quiet(); + await runMediaCommand([ + "ffmpeg", "-y", "-i", inputPath, + "-c:v", "libx264", "-crf", "23", "-preset", "medium", + "-c:a", "aac", "-b:a", "128k", + "-movflags", "+faststart", + outputPath + ]); const result = Buffer.from(await Bun.file(outputPath).arrayBuffer()); spinner.stop(); @@ -881,9 +909,9 @@ async function reencodeVideo(buffer: Buffer): Promise { return result; } catch (e) { spinner.stop(); - throw new Error( - `Video re-encode failed: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(chalk.hex(COLORS.error)("\nRe-encode error:")); + console.error(chalk.hex(COLORS.dim)(e instanceof Error ? e.message : String(e))); + throw new Error("Video re-encode failed"); } finally { await cleanupTempFiles(inputPath, outputPath); } @@ -894,11 +922,14 @@ async function remuxAudio(buffer: Buffer): Promise { spinner.start("Fixing audio metadata..."); const inputPath = await writeTempFile(buffer); - // Detect format and use same extension const outputPath = join(tmpdir(), `share-audio-${nanoid(8)}.mka`); try { - await $`ffmpeg -y -i ${inputPath} -c copy ${outputPath}`.quiet(); + await runMediaCommand([ + "ffmpeg", "-y", "-i", inputPath, + "-c", "copy", + outputPath + ]); const result = Buffer.from(await Bun.file(outputPath).arrayBuffer()); spinner.stop(); @@ -906,9 +937,9 @@ async function remuxAudio(buffer: Buffer): Promise { return result; } catch (e) { spinner.stop(); - throw new Error( - `Audio remux failed: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(chalk.hex(COLORS.error)("\nAudio remux error:")); + console.error(chalk.hex(COLORS.dim)(e instanceof Error ? e.message : String(e))); + throw new Error("Audio remux failed"); } finally { await cleanupTempFiles(inputPath, outputPath); } @@ -922,7 +953,9 @@ async function convertImageToJpeg(buffer: Buffer): Promise { const outputPath = join(tmpdir(), `share-convert-${nanoid(8)}.jpg`); try { - await $`convert ${inputPath} -quality 90 ${outputPath}`.quiet(); + await runMediaCommand([ + "convert", inputPath, "-quality", "90", outputPath + ]); const result = Buffer.from(await Bun.file(outputPath).arrayBuffer()); spinner.stop(); @@ -930,9 +963,9 @@ async function convertImageToJpeg(buffer: Buffer): Promise { return result; } catch (e) { spinner.stop(); - throw new Error( - `JPEG conversion failed: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(chalk.hex(COLORS.error)("\nJPEG conversion error:")); + console.error(chalk.hex(COLORS.dim)(e instanceof Error ? e.message : String(e))); + throw new Error("JPEG conversion failed"); } finally { await cleanupTempFiles(inputPath, outputPath); } @@ -946,7 +979,9 @@ async function convertImageToWebp(buffer: Buffer): Promise { const outputPath = join(tmpdir(), `share-convert-${nanoid(8)}.webp`); try { - await $`convert ${inputPath} -quality 85 ${outputPath}`.quiet(); + await runMediaCommand([ + "convert", inputPath, "-quality", "85", outputPath + ]); const result = Buffer.from(await Bun.file(outputPath).arrayBuffer()); spinner.stop(); @@ -954,9 +989,9 @@ async function convertImageToWebp(buffer: Buffer): Promise { return result; } catch (e) { spinner.stop(); - throw new Error( - `WebP conversion failed: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(chalk.hex(COLORS.error)("\nWebP conversion error:")); + console.error(chalk.hex(COLORS.dim)(e instanceof Error ? e.message : String(e))); + throw new Error("WebP conversion failed"); } finally { await cleanupTempFiles(inputPath, outputPath); } @@ -986,9 +1021,9 @@ async function convertImage( await Bun.write(inputPath, buffer); if (fromMime === "image/heic" || fromMime === "image/heif") { - await $`convert ${inputPath} -quality 90 ${outputPath}`.quiet(); + await runMediaCommand(["convert", inputPath, "-quality", "90", outputPath]); } else { - await $`convert ${inputPath} ${outputPath}`.quiet(); + await runMediaCommand(["convert", inputPath, outputPath]); } const converted = Buffer.from(await Bun.file(outputPath).arrayBuffer()); @@ -999,9 +1034,9 @@ async function convertImage( return converted; } catch (e) { spinner.stop(); - throw new Error( - `Image conversion failed: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(chalk.hex(COLORS.error)("\nImage conversion error:")); + console.error(chalk.hex(COLORS.dim)(e instanceof Error ? e.message : String(e))); + throw new Error("Image conversion failed"); } finally { await cleanupTempFiles(inputPath, outputPath); } @@ -1017,7 +1052,14 @@ async function convertVideo(buffer: Buffer): Promise { try { await Bun.write(inputPath, buffer); - await $`ffmpeg -i ${inputPath} -c:v libx264 -crf 28 -preset slow -vf "scale=-2:min(720,ih)" -c:a aac -b:a 128k -movflags +faststart ${outputPath}`.quiet(); + await runMediaCommand([ + "ffmpeg", "-y", "-i", inputPath, + "-c:v", "libx264", "-crf", "28", "-preset", "slow", + "-vf", "scale=-2:'min(720,ih)'", + "-c:a", "aac", "-b:a", "128k", + "-movflags", "+faststart", + outputPath + ]); const converted = Buffer.from(await Bun.file(outputPath).arrayBuffer()); @@ -1027,9 +1069,9 @@ async function convertVideo(buffer: Buffer): Promise { return converted; } catch (e) { spinner.stop(); - throw new Error( - `Video conversion failed: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(chalk.hex(COLORS.error)("\nConversion error:")); + console.error(chalk.hex(COLORS.dim)(e instanceof Error ? e.message : String(e))); + throw new Error("Video conversion failed"); } finally { await cleanupTempFiles(inputPath, outputPath); } @@ -1193,20 +1235,18 @@ ${chalk.hex(COLORS.label)("Examples:")} share -c large-video.mov # Convert/re-encode then upload share -Fa video.webm # Fix all issues automatically -${chalk.hex(COLORS.label)("Environment Variables:")} - R2_ENDPOINT S3-compatible endpoint URL - R2_ACCESS_KEY_ID Access key ID - R2_SECRET_ACCESS_KEY Secret access key - R2_BUCKET Bucket name -`); +${chalk.hex(COLORS.label)("Note:")} + R2 credentials are embedded via chezmoi template from Doppler. + Use 'chezmoi apply' to update the deployed script with new credentials. + `); return; } VERBOSE = values.verbose || false; - const missing = REQUIRED_ENV.filter((key) => !process.env[key]); - if (missing.length > 0) { - log(`Missing environment variables: ${missing.join(", ")}`, "error"); + // Credentials are embedded via chezmoi template - check ENV object instead + if (!ENV.endpoint || !ENV.accessKeyId || !ENV.secretAccessKey || !ENV.bucket) { + log("Missing R2 credentials - chezmoi template may not have been processed correctly", "error"); process.exit(1); }