Skip to content

Commit b2d5110

Browse files
committed
feat(cli): bare \enchanter\ prefers real Claude hook stream over showcase
When hooks are installed and their target dir exists, bare \`enchanter\` auto-tails the Claude Code hook JSONL instead of running the showcase loop. This is the v0.7-direction UX: install hooks once → bare \`enchanter\` shows REAL events from real Claude work, no synthetic emitter, no manual --tail flag. New default_command() priority: 1. stdin piped → Inspect from stdin (back-compat) 2. stdin TTY + ~/.cache/enchanter/ exists → Inspect --tail of the hook JSONL (NEW — preferred when hooks are wired up) 3. stdin TTY + scripts/live.ts reachable → Live showcase (existing fallback for monorepo dev / demo) 4. stdin TTY + nothing → synthetic demo (last-resort emitter) The detection trigger is "parent dir exists" rather than "JSONL file exists" — the file may not appear until the next Claude session fires a hook, and --tail's 30s late-creation retry handles the wait. So running \`enchanter\` immediately after installing hooks Just Works. Showcase loop still available via \`enchanter live\` explicit subcommand.
1 parent f1fc5fa commit b2d5110

1 file changed

Lines changed: 45 additions & 14 deletions

File tree

inspector/src/lib.rs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -152,25 +152,56 @@ impl InspectArgs {
152152
}
153153
}
154154

155+
/// Resolve the path the Claude Code hook emitter writes to. Mirrors the
156+
/// algorithm in `scripts/hooks/claude-code-emit.mjs::cachePath` —
157+
/// XDG_CACHE_HOME → LOCALAPPDATA → HOME/.cache, all under `enchanter/`.
158+
fn claude_code_hook_jsonl() -> PathBuf {
159+
let base = std::env::var_os("XDG_CACHE_HOME")
160+
.map(PathBuf::from)
161+
.or_else(|| std::env::var_os("LOCALAPPDATA").map(PathBuf::from))
162+
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
163+
.unwrap_or_else(std::env::temp_dir);
164+
base.join("enchanter").join("claude-code.jsonl")
165+
}
166+
155167
/// Decide what bare `enchanter` (no subcommand) should do.
156168
///
157-
/// Hierarchy:
158-
/// 1. stdin is piped → consume JSONL from stdin (`Source::Stdin`, which the
159-
/// app loop turns into demo mode iff the stdin is also a TTY but that
160-
/// shouldn't happen if it's piped).
161-
/// 2. stdin is a TTY AND `scripts/live.ts` is reachable from cwd → boot the
162-
/// real runtime via `Source::Exec`. This is the "boom, just works"
163-
/// path when the binary is run from `client/enchanter/`.
164-
/// 3. stdin is a TTY AND `scripts/live.ts` is NOT reachable → fall back to
165-
/// `Source::Stdin`, which the app loop turns into the synthetic
166-
/// `src/demo.rs` emitter so the cockpit still has something to render.
169+
/// Hierarchy, highest priority first:
170+
/// 1. stdin is piped → consume JSONL from stdin.
171+
/// 2. stdin is a TTY AND the Claude Code hook JSONL exists OR its parent
172+
/// cache dir exists (hooks installed but no events yet) → tail it.
173+
/// This is the "real Claude Code work" path — every tool call, prompt,
174+
/// session boundary lights up the cockpit from authentic hook output.
175+
/// 3. stdin is a TTY AND `scripts/live.ts` is reachable from cwd → boot
176+
/// the showcase runtime. Fallback when hooks aren't wired up.
177+
/// 4. stdin is a TTY AND nothing else → demo mode (synthetic emitter).
167178
fn default_command() -> Command {
168179
use std::io::IsTerminal;
169-
if std::io::stdin().is_terminal() && std::path::Path::new("scripts/live.ts").is_file() {
170-
Command::Live(LiveArgs::default())
171-
} else {
172-
Command::Inspect(InspectArgs::default())
180+
if !std::io::stdin().is_terminal() {
181+
return Command::Inspect(InspectArgs::default());
182+
}
183+
184+
// Real Claude Code path: prefer it whenever hooks are installed —
185+
// the JSONL file may not exist yet (no Claude session has fired a hook
186+
// since install), but the parent dir does, and `--tail` waits up to 30s
187+
// for the file to appear. That's the "boom, real data" UX.
188+
let hook_jsonl = claude_code_hook_jsonl();
189+
let hooks_wired_up = hook_jsonl.exists()
190+
|| hook_jsonl.parent().map(|p| p.is_dir()).unwrap_or(false);
191+
if hooks_wired_up {
192+
return Command::Inspect(InspectArgs {
193+
tail: Some(hook_jsonl),
194+
..InspectArgs::default()
195+
});
173196
}
197+
198+
// Showcase fallback when running from the monorepo with the demo script.
199+
if std::path::Path::new("scripts/live.ts").is_file() {
200+
return Command::Live(LiveArgs::default());
201+
}
202+
203+
// Last-resort synthetic demo (handled by app::run when stdin is TTY).
204+
Command::Inspect(InspectArgs::default())
174205
}
175206

176207
/// Library entry point invoked from `main`.

0 commit comments

Comments
 (0)