Metadata-Version: 2.4
Name: claude-interactive-sdk
Version: 0.0.3
Summary: Drop-in replacement for ClaudeSDKClient that drives Claude Code in interactive TUI mode via tmux
Project-URL: Homepage, https://github.com/finndersen/claude-interactive-sdk
Project-URL: Issues, https://github.com/finndersen/claude-interactive-sdk/issues
Author-email: Finn Andersen <finndersen@gmail.com>
License: MIT
License-File: LICENSE
Keywords: anthropic,claude,claude-code,sdk,tmux
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: <4.0,>=3.11
Requires-Dist: anyio>=4.0.0
Requires-Dist: claude-agent-sdk>=0.1.8
Requires-Dist: mcp>=1.27.0
Requires-Dist: starlette>=0.37.0
Requires-Dist: uvicorn>=0.30.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-timeout>=2.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# claude-interactive-sdk

A drop-in replacement for [`ClaudeSDKClient`](https://github.com/anthropics/claude-agent-sdk-python) that runs `claude` Code in its **interactive session mode** instead of the SDK's headless stream-json mode, so that usage is billed against your Claude subscription rather than the API. Supports the full `ClaudeAgentOptions` API including function tools, MCP servers, system prompts, multi-turn conversations, model selection, and more.

## Why

Anthropic announced that programmatic usage via the API and SDK will be billed separately from subscription usage. This SDK lets you drive Claude programmatically while keeping usage on your existing Claude subscription, by launching `claude` exactly the way a human would (no `-p`, no `--input-format`, no `--output-format`) and exposing the same Python API on top.

## Install

```bash
pip install claude-interactive-sdk
```

Requires `tmux` and the `claude` CLI to be installed. Install tmux via your package manager:

```bash
# macOS
brew install tmux

# Ubuntu/Debian
sudo apt install tmux
```

## Usage

```python
from claude_interactive_sdk import ClaudeInteractiveClient
from claude_agent_sdk import ClaudeAgentOptions, tool, create_sdk_mcp_server

@tool("get_weather", "Get current weather", {"location": str})
async def get_weather(args):
    return {"content": [{"type": "text", "text": f"It's sunny in {args['location']}"}]}

mcp = create_sdk_mcp_server(name="my_tools", tools=[get_weather])

async with ClaudeInteractiveClient(
    options=ClaudeAgentOptions(
        model="claude-sonnet-4-6",
        mcp_servers={"my_tools": mcp},
    )
) as client:
    await client.query("What's the weather in Sydney?")
    async for message in client.receive_response():
        print(message)
```

More in [`examples/`](https://github.com/finndersen/claude-interactive-sdk/tree/main/examples).

## Compatibility with `ClaudeAgentOptions` and `ClaudeSDKClient`

### `ClaudeSDKClient` methods

| Method | Status | Notes |
|---|---|---|
| `connect()` / `disconnect()` / `__aenter__` / `__aexit__` | ✅ | |
| `query(prompt: str)` | ✅ | |
| `query(prompt: AsyncIterable[dict])` | ⚠️ | Each dict serialised as plain text — tool-result semantics lost |
| `receive_messages()` / `receive_response()` | ✅ | |
| `interrupt()` | ⚠️ | Best-effort — sends `C-c` to the running session |
| `get_server_info()` | ⚠️ | Returns a minimal stub |
| `disconnect(detach=True)` | ❌ | `NotImplementedError` |
| `set_model`, `set_permission_mode`, `rewind_files`, `reconnect_mcp_server`, `toggle_mcp_server`, `stop_task`, `get_mcp_status`, `get_context_usage` | ❌ | All require the SDK control protocol, which doesn't exist in interactive mode |

### `ClaudeAgentOptions` fields

**Fully supported** — translated to argv, env vars, or `--settings` JSON:

`tools`, `allowed_tools`, `disallowed_tools`, `system_prompt` (str or file), `mcp_servers` (all transport types), `strict_mcp_config`, `continue_conversation`, `resume`, `session_id`, `fork_session`, `max_turns`, `max_budget_usd`, `model`, `fallback_model`, `betas`, `cwd`, `settings`, `sandbox`, `add_dirs`, `setting_sources`, `plugins`, `thinking`, `max_thinking_tokens`, `effort`, `output_format` (json_schema), `extra_args`, `cli_path`, `env`, `enable_file_checkpointing`.

**Adapted** — same semantics, different mechanism:

| Field | Mechanism |
|---|---|
| `mcp_servers={"name": {"type": "sdk", …}}` | Materialised into an in-process HTTP MCP server; config rewritten to `{"type": "http", "url": "http://127.0.0.1:<port>/mcp/<name>/"}` |
| `can_use_tool` | Intercepted in the HTTP MCP server — **MCP-hosted tools only** |
| `hooks` (command strings) | Merged into `--settings` JSON alongside the internal Stop hook |

**Restricted**:

| Field | Restriction |
|---|---|
| `permission_mode` | Only `bypassPermissions` (default) and `acceptEdits` are accepted. `default` and `plan` would surface interactive permission prompts the harness can't answer, so they raise at construction |
| `can_use_tool` | Does not fire for built-in tools (Bash, Edit, etc.). Gate those statically via `disallowed_tools` (Layer 1) or `settings.permissions.deny` patterns (Layer 2) |

**Not supported** — raise at construction:

| Field | Reason |
|---|---|
| `hooks` with Python callables | No control-protocol channel for hook callbacks |
| `transport` | This package doesn't use the SDK's Transport abstraction |
| `debug_stderr` | Deprecated upstream |

**Limited / best-effort**:

| Field | Limitation |
|---|---|
| `include_partial_messages`, `include_hook_events` | Forwarded to the CLI, but the JSONL transcript may not emit the corresponding events |
| `agents`, `skills` (inline dicts) | Inline definitions use the control protocol — lost. File-based agents/skills in `~/.claude/agents` / `~/.claude/skills` work normally |
| `session_store` | Remote transcript mirroring not implemented |

Full details: [`docs/DESIGN.md`](https://github.com/finndersen/claude-interactive-sdk/blob/main/docs/DESIGN.md) §4, §10.

## Implementation overview

`claude` runs in interactive mode inside a detached `tmux` session. Prompts go in via `tmux paste-buffer`; output comes back via the JSONL transcript that claude writes to `<CLAUDE_CONFIG_DIR>/projects/<encoded-cwd>/<session-id>.jsonl`. Turn termination is signalled by a `Stop` hook (installed via `--settings`) that writes to a FIFO the harness reads. User-defined tools are hosted in an in-process HTTP MCP server bound to an ephemeral localhost port. The harness wraps the launch in a stealth chain (`env -u … script -q /dev/null setsid claude …`) that strips harness-identifying env vars and gives the child a fresh PTY.

Sessions launched this way produce `"entrypoint":"cli"` in Claude's session logs, versus `"entrypoint":"sdk-cli"` for sessions launched via `claude -p` or the official SDK.

See [`docs/DESIGN.md`](https://github.com/finndersen/claude-interactive-sdk/blob/main/docs/DESIGN.md) for the full design including event-flow diagrams and rationale for each decision.

## Development

```bash
make test        # full test suite
make test-cov    # with coverage
make lint        # ruff
make format
make typecheck   # mypy
make run-example NAME=01_hello_world
```

## License

MIT
