Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,25 @@ dg usage --last-week -o yaml
When running in a non-TTY environment (pipes, CI, or AI coding tools), the CLI
automatically switches to structured JSON output with plain-text status messages.

### Forcing non-interactive mode

Three explicit ways to skip every prompt and run with defaults — useful from a
real terminal where auto-detection wouldn't otherwise trigger:

```bash
# Global flag (works at any position)
dg --non-interactive listen recording.wav
dg listen --non-interactive recording.wav

# Environment variable (good for whole scripts)
CI=1 dg listen recording.wav
```

Also recognised: `--agent-friendly` (alias intended for AI coding tools — same
effect plus JSON metadata output), and the auto-detected env vars
`CLAUDECODE`, `CLAUDE_CODE_ENTRYPOINT`, `CODEX_SANDBOX`, and Aider's
`OR_APP_NAME` / `OR_SITE_URL`.

## Plugins

Extend the CLI with custom commands.
Expand Down
8 changes: 5 additions & 3 deletions packages/deepctl-core/src/deepctl_core/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ def is_agentic() -> bool:
"""
env = os.environ

# Hard signals: explicit agent mode flag or known AI tool env vars
# Hard signals: explicit non-interactive intent or known AI tool env vars
if "--non-interactive" in sys.argv:
return True
if "--agent-friendly" in sys.argv:
return True
if env.get("CI") in ("true", "1"):
return True
if env.get("CLAUDECODE"):
return True
if env.get("CLAUDE_CODE_ENTRYPOINT"):
Expand All @@ -56,8 +60,6 @@ def is_agentic() -> bool:
score += 1
if not sys.stdout.isatty():
score += 1
if env.get("CI") in ("true", "1"):
score += 1
if not env.get("TERM") or env.get("TERM") == "dumb":
score += 1
if "NO_COLOR" in env:
Expand Down
7 changes: 7 additions & 0 deletions packages/deepctl-core/src/deepctl_core/plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,13 @@ def _agent_friendly_callback(
callback=_agent_friendly_callback,
)(cmd)

cmd = click.option(
"--non-interactive",
is_flag=True,
expose_value=False,
help="Skip interactive prompts; use defaults for any optional features.",
)(cmd)

return cmd

def _build_help_text(self, instance: Any) -> str:
Expand Down
119 changes: 119 additions & 0 deletions packages/deepctl-core/tests/unit/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,128 @@
print_warning,
print_info,
get_console,
is_agentic,
)


class TestIsAgentic:
"""Cover every signal that flips the CLI into non-interactive mode."""

@pytest.fixture
def baseline(self, monkeypatch):
"""Interactive baseline: clean argv, real-looking TTY env, no AI hints."""
monkeypatch.setattr("sys.argv", ["dg", "listen", "foo.wav"])
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
for var in (
"CI",
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CODEX_SANDBOX",
"CODEX_SANDBOX_NETWORK_DISABLED",
"OR_APP_NAME",
"OR_SITE_URL",
"NO_COLOR",
):
monkeypatch.delenv(var, raising=False)
monkeypatch.setenv("TERM", "xterm-256color")

def test_baseline_is_interactive(self, baseline):
assert is_agentic() is False

@pytest.mark.parametrize(
"argv",
[
["dg", "--non-interactive", "listen", "foo.wav"],
["dg", "listen", "--non-interactive", "foo.wav"],
["dg", "listen", "foo.wav", "--non-interactive"],
],
)
def test_non_interactive_flag_anywhere_in_argv(
self, baseline, monkeypatch, argv
):
monkeypatch.setattr("sys.argv", argv)
assert is_agentic() is True

@pytest.mark.parametrize(
"argv",
[
["dg", "--agent-friendly", "listen", "foo.wav"],
["dg", "listen", "--agent-friendly", "foo.wav"],
],
)
def test_agent_friendly_flag_anywhere_in_argv(
self, baseline, monkeypatch, argv
):
monkeypatch.setattr("sys.argv", argv)
assert is_agentic() is True

@pytest.mark.parametrize("ci_value", ["1", "true"])
def test_ci_env_var_is_hard_signal(self, baseline, monkeypatch, ci_value):
monkeypatch.setenv("CI", ci_value)
assert is_agentic() is True

@pytest.mark.parametrize(
"ci_value",
["false", "0", "", "yes", "TRUE"],
)
def test_other_ci_values_do_not_count(
self, baseline, monkeypatch, ci_value
):
monkeypatch.setenv("CI", ci_value)
assert is_agentic() is False

@pytest.mark.parametrize(
"env_var",
[
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CODEX_SANDBOX",
"CODEX_SANDBOX_NETWORK_DISABLED",
],
)
def test_ai_tool_env_var_is_hard_signal(
self, baseline, monkeypatch, env_var
):
monkeypatch.setenv(env_var, "1")
assert is_agentic() is True

def test_aider_via_or_app_name(self, baseline, monkeypatch):
monkeypatch.setenv("OR_APP_NAME", "Aider")
assert is_agentic() is True

def test_aider_via_or_site_url(self, baseline, monkeypatch):
monkeypatch.setenv("OR_SITE_URL", "https://aider.example.com")
assert is_agentic() is True

def test_one_soft_signal_not_enough(self, baseline, monkeypatch):
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
assert is_agentic() is False

def test_two_soft_signals_not_enough(self, baseline, monkeypatch):
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
assert is_agentic() is False

def test_three_soft_signals_trip_threshold(self, baseline, monkeypatch):
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
monkeypatch.setenv("NO_COLOR", "1")
assert is_agentic() is True

def test_dumb_term_counts_as_soft_signal(self, baseline, monkeypatch):
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
monkeypatch.setenv("TERM", "dumb")
assert is_agentic() is True

def test_unset_term_counts_as_soft_signal(self, baseline, monkeypatch):
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setattr("sys.stdout.isatty", lambda: False)
monkeypatch.delenv("TERM", raising=False)
assert is_agentic() is True


class TestOutputFormatter:
"""Test OutputFormatter class."""

Expand Down
7 changes: 7 additions & 0 deletions src/deepctl/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ def preprocess_hyphenated_commands(args: list[str]) -> list[str]:
is_flag=True,
help="Show detailed performance timing information",
)
@click.option(
"--non-interactive",
is_flag=True,
is_eager=True,
expose_value=False,
help="Skip interactive prompts; use defaults for any optional features.",
)
@click.option(
"--record-install-method",
is_eager=True,
Expand Down
Loading