From d734161c970a0a8eaace15318ebb9d4faefb20b7 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Fri, 26 Jun 2026 01:17:08 -0400 Subject: [PATCH] feat: auto_target toggle, print_heard debug, more wake spellings add behavior.auto_target (default false): with no sticky target and exactly one session running, false requires an explicit set/target rather than guessing; true auto-uses it. target.resolve() takes the flag. add behavior.print_heard (default false, debug): opt-in console echo of non-wake transcripts to see how Whisper renders the wake word. add behavior.filler_words. expand the wake list with the spellings Whisper actually emits for the coined word ('claude do', 'claude due', 'ok claude', 'okay claude'). Signed-off-by: disqualifier --- config.toml | 13 ++++++++++++- src/claudedo/config.py | 4 ++++ src/claudedo/target.py | 10 +++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/config.toml b/config.toml index 1f7b9bd..456b8f2 100644 --- a/config.toml +++ b/config.toml @@ -5,7 +5,7 @@ # wake phrases for listen mode. fuzzy-matched: case/space-insensitive, lenient on # the coined word "claudedo" (whisper renders it inconsistently). number words are # normalized to digits before command matching. -phrases = ["claudedo", "hey claude"] +phrases = ["claudedo", "claude do", "claude due", "hey claude", "ok claude", "okay claude"] [input] # "listen" (default): continuous capture; only acts on utterances that start with a @@ -55,3 +55,14 @@ match_threshold = 0.8 # "select yes" / "use yes" behave like "yes". (a filler word followed by a digit is # the select command, e.g. "select 1", and is not dropped.) filler_words = ["select", "use", "choose"] +# when no sticky target is set and exactly ONE claude-* session is running: +# false (default) -> require an explicit `set ` or one-shot `target `; +# a bare command does nothing and tells you to set one. +# true -> auto-target that single session (convenience). +auto_target = false +# DEBUG ONLY — relaxes the privacy invariant. when true, the daemon console prints +# the raw transcript of EVERY utterance, including non-wake speech it would otherwise +# drop silently (shown as `heard (dropped): ""`). use it to see exactly +# how Whisper renders your wake word, then turn it OFF. default false: non-wake speech +# is discarded without ever printing the transcript. +print_heard = false diff --git a/src/claudedo/config.py b/src/claudedo/config.py index 8604ccb..45f4b23 100644 --- a/src/claudedo/config.py +++ b/src/claudedo/config.py @@ -50,6 +50,8 @@ class Config: type_autosend: bool match_threshold: float filler_words: tuple[str, ...] + auto_target: bool + print_heard: bool source_path: Path | None = field(default=None) @@ -118,6 +120,8 @@ def load_config(explicit: str | os.PathLike | None = None) -> Config: match_threshold=float(_require(raw, "behavior", "match_threshold", (int, float), 0.8)), filler_words=tuple(_require(raw, "behavior", "filler_words", (list,), ["select", "use", "choose"])), + auto_target=bool(_require(raw, "behavior", "auto_target", (bool,), False)), + print_heard=bool(_require(raw, "behavior", "print_heard", (bool,), False)), source_path=path, ) if not 0.0 < cfg.match_threshold <= 1.0: diff --git a/src/claudedo/target.py b/src/claudedo/target.py index 05ca5a1..61852ea 100644 --- a/src/claudedo/target.py +++ b/src/claudedo/target.py @@ -85,7 +85,7 @@ def list_sessions() -> list[str]: return sorted(n for n in names if n.startswith(SESSION_PREFIX)) -def resolve(one_shot: str | None = None) -> tuple[str | None, str]: +def resolve(one_shot: str | None = None, auto_target: bool = False) -> tuple[str | None, str]: """resolve the destination session and a short reason describing the choice. single source of truth for targeting, used by both the voice and CLI paths. @@ -95,7 +95,9 @@ def resolve(one_shot: str | None = None) -> tuple[str | None, str]: 1. one-shot present -> claude- for THIS command only; never falls through to a different session if it doesn't exist (explicit beats convenience). 2. sticky set + exists -> use it. - 3. nothing sticky, exactly one claude-* session -> auto-use it. + 3. nothing sticky, exactly one claude-* session: + auto_target=True -> auto-use it; + auto_target=False -> require an explicit set/target, do nothing. 4. nothing sticky, multiple sessions -> ambiguous, do nothing. 5. nothing sticky, zero sessions -> do nothing. """ @@ -113,7 +115,9 @@ def resolve(one_shot: str | None = None) -> tuple[str | None, str]: sessions = list_sessions() if len(sessions) == 1: - return sessions[0], f"auto-target {sessions[0]} (only session)" + if auto_target: + return sessions[0], f"auto-target {sessions[0]} (only session)" + return None, f"no target set ({sessions[0]} running — set one)" if len(sessions) > 1: return None, f"no target set, {len(sessions)} sessions (set one)" return None, "no claude sessions"