Metadata-Version: 2.4
Name: dscli-agent
Version: 0.2.0
Summary: A local coding agent that uses the DeepSeek web chat UI as its model
Author: Anish
License-Expression: MIT
Keywords: cli,coding-agent,deepseek,playwright,terminal,ai
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: playwright>=1.45
Requires-Dist: rich>=13.7
Requires-Dist: prompt-toolkit>=3.0.43
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: twine>=5.1; extra == "dev"
Dynamic: license-file

# dscli

A local coding agent that uses the **DeepSeek web chat UI** as its model. No API key:
dscli drives a real browser session, sends prompts to `https://chat.deepseek.com/`, reads
the replies, and executes file-editing tools on your machine.

```
you ──> dscli ──> DeepSeek chat (browser) ──> one JSON envelope per turn
          │                                            │
          └────── local tools: read / write / patch <──┘
```

## Requirements

- Python 3.10+ (developed on 3.13)
- Google Chrome, or Playwright's Chromium
- A DeepSeek account (the free web chat is enough)

## Install

```powershell
python -m pip install dscli-agent
```

This installs the `dscli` command and its Python dependencies. Playwright's browsers are
not downloaded by pip: dscli drives your installed Google Chrome by default. If Chrome is
not available, install Playwright's Chromium once:

```powershell
python -m playwright install chromium
```

To work from a source checkout instead:

```powershell
python -m pip install -e ".[dev]"
```

## Sign-in

If you already use DeepSeek in Chrome, dscli reuses that saved session automatically,
even while Chrome is open. It reads DeepSeek's saved login token from Chrome's local
storage and seeds its own isolated browser profile; Chrome's cookies are copied too when
they are available. Your normal Chrome profile is never controlled by the automation,
and you do not need to quit Chrome or sign in again.

The isolated profile is persistent and lives under `%APPDATA%\dscli\profile` on Windows
(`~/Library/Application Support/dscli` on macOS, `~/.config/dscli` elsewhere). Keep this
directory private because it contains logged-in browser session data.

If you are not logged in through Chrome, either log in there first or run:

```powershell
dscli login
```

A visible browser window opens for a manual DeepSeek login, including any Cloudflare
check. dscli closes it as soon as the chat composer appears.

Check the page and selectors without sending anything:

```powershell
dscli doctor
dscli doctor --send          # also do a one-word round-trip test
dscli doctor --headed        # watch it work
```

## Interactive chat

Start dscli without a task to open an ongoing conversation:

```powershell
dscli
```

dscli shows an inline terminal UI inspired by Claude Code: a cleared screen and gradient
ASCII wordmark on launch, an animated spinner with rotating status verbs, a smoothly
revealed streaming answer, compact tool cards (`⏺ Read(...)`, `⏺ Update(...)`,
`⏺ Run(...)`), colored diffs for approvals, and the final answer rendered as Markdown.
Output stays in normal scrollback after startup; dscli does not take over the full
screen, and the welcome card shows the workspace, active modes, and shortcuts. Plans
render as a compact checklist, and a configured verification command appears as a
`Verify` card after file edits. `dscli chat --verify CMD` applies the check to each
message.

On launch, the visible terminal is cleared before the wordmark appears. Pass
`--no-clear` to `chat`, or set `DSCLI_NO_CLEAR=1`, to keep the previous screen contents;
the logo and welcome card still show. This behavior is TUI-only: `--plain`, non-TTY
output, `run`, `doctor`, `login`, and `config` keep their existing output.

The agent session stays open while you work at the `❯` prompt. The complete JSON
envelope is still parsed and validated before any tool runs. File approvals,
auto-approval state, and workspace context remain active across messages.
`--max-steps` applies separately to each message.

### Prompt shortcuts

| Key | Action |
| --- | --- |
| `Enter` | Send the message (or accept a `/` completion). |
| `Esc` then `Enter`, or `Ctrl+J` | Insert a newline for multi-line input. |
| `Ctrl+C` | While the agent is working: interrupt the current turn, keep the browser session, and return to the prompt. At the prompt: clear the input. |
| `Ctrl+D` | Exit dscli. |
| `↑` / `↓` | Browse previous messages. |

Typing `/` opens an autocomplete menu. History is saved to
`%APPDATA%\dscli\chat_history` (`~/Library/Application Support/dscli/chat_history` on
macOS, `~/.config/dscli/chat_history` elsewhere).

Slash commands: `/help` lists commands and shortcuts, `/new` starts a fresh
conversation so the agent forgets earlier turns, and `/exit` (or `/quit`) closes dscli.

When dscli asks to write a file or run a command, it shows the title and a compact
colored diff (additions green, deletions red, hunks cyan) even when `-y` is active.
Answer `y` to approve once, `n` to deny, `a` to approve everything for the rest of the
session, or `q` to quit.

### Plain mode

Use `--plain` (or set `DSCLI_PLAIN=1`) to keep the earlier line-based interface:

```powershell
dscli chat --plain
```

The plain UI prints progress to stderr, accepts one line per message, and uses
`you> ` as its prompt. It is also selected automatically when stdin or stdout is not
a terminal (for example when redirecting output), when `TERM=dumb`, or when the UI
dependencies cannot start. On Windows, Windows Terminal renders the UI best; legacy
`conhost` windows may show simpler output.

You can also use the explicit command with the common agent flags:

```powershell
dscli chat --headed --workspace C:\projects\app --max-steps 8
```

## Run a one-shot task

```powershell
# Simple task in the current directory
dscli run "Add input validation to app.py and a test for it"

# Attach files (they are uploaded into the chat on the first turn)
dscli run "Refactor the parser" -f src/parser.py -f tests/test_parser.py

# Globs work too; quote them so the shell does not expand them
dscli run "Fix the bug" -f "src/**/*.py" --workspace C:\projects\app

# Read a long task from a file; auto-approve edits; allow shell commands
dscli run --prompt-file task.md -y --allow-shell
```

The final answer is printed to stdout. Progress, diffs, and approval prompts go to
stderr, so `dscli run ... > result.txt` captures only the answer. `run` never uses the
interactive UI; its output contract is unchanged.

Exit codes: `0` success, `1` error, `2` model-turn budget exhausted, and `3`
verification failed (takes precedence over `2`). Verification failures are always
logged to stderr, including under `-q`.

### Common flags

| Flag | Meaning |
| --- | --- |
| `-f, --file PATH` | Attach a file (repeatable, globs supported). |
| `--workspace PATH` | Root the tools may touch. Defaults to the current directory. |
| `-y, --yes` | Approve every write/patch/command without asking. |
| `--allow-shell` | Enable `run_command` (still approved per command). |
| `--allow-outside` | Let tools read/write outside the workspace. |
| `--headed` / `--headless` | Show or hide the browser. Headless is the default. |
| `--deepthink` / `--search` | Turn DeepSeek's modes on for the run. |
| `--max-steps N` | Maximum model turns (default 12). |
| `--timeout N` | Response timeout per turn in seconds (default 300). |
| `--verify CMD` | Run CMD automatically after file edits; failures block success. |
| `--no-verify` | Disable the configured `verify_command` for this run. |
| `--prompt-file PATH` | Read the task from a UTF-8 file. |
| `--no-clear` | Keep previous terminal output when the TUI starts (chat only). |
| `--no-auto-login` | Do not open a visible browser if a headless login is needed. |
| `-v, --verbose` | Show model thoughts and raw replies. |
| `-q, --quiet` | Only print the final answer. |
| `--dry-run` | Print the exact first prompt and exit. |

Use `-` as the task to read it from stdin.

## How it works

Each turn is one normal chat message. The first message contains the system contract,
the environment, a workspace preview, the attachment manifest, and the task. The model
is required to answer with exactly one ` ```json ` fenced block containing one JSON
object:

````markdown
```json
{
  "thought": "I should read the file first.",
  "tool_calls": [
    {"name": "read_file", "arguments": {"path": "src/app.py"}}
  ],
  "final": null
}
```
````

The fence matters: DeepSeek renders unfenced replies as Markdown, which auto-links
tokens like `object.is` and eats `*` or `[...]`, corrupting code inside the JSON.
dscli parses the envelope (also tolerating unfenced JSON and small formatting
variations), runs the tool calls locally, and sends the results back in the next
message inside nonce-tagged markers. The loop ends when the model replies with
`tool_calls: []` and a non-empty `final` string, or when the turn budget is exhausted.
Before a final is accepted, unfinished plan steps and the configured verification
command can send the model back for more work (see below).

Malformed replies get up to two correction prompts. Every raw reply that fails to parse
is saved to the debug directory.

### Tools

| Tool | Purpose |
| --- | --- |
| `read_file` | Read a text file, optionally a line range. Required before patching. |
| `write_file` | **Create** a new file. Refuses to overwrite an existing non-empty file. |
| `apply_patch` | The only way to edit an existing file: exact `search` → `replace`. |
| `list_files` | Bounded recursive listing. |
| `delete_file` | Delete one file when explicitly requested (approval required). |
| `run_command` | Run a shell command (only with `--allow-shell`, approval required). |
| `update_plan` | Create or revise the task plan; the final is deferred while steps are unfinished. |

`apply_patch` normalises line endings for matching, preserves the file's encoding and
line endings when writing, and refuses ambiguous matches unless `replace_all` is set.

### Plan and verification loop

The agent can maintain a short task plan with `update_plan` (at most 20 steps). While
any step is `pending` or `in_progress`, dscli holds the final answer back and sends a
bounded reminder (at most two) asking the model to finish or revise the plan. If the
steps are still unfinished at the turn limit, the answer is accepted with a visible
"plan incomplete" warning rather than deadlocking the run. Plans apply to a single
message on chat and are reset for every new task.

Configure a verification command with `--verify CMD` or the `verify_command` config
key, for example:

```powershell
dscli run -y --verify "python -m pytest -q" "Add a test for the parser"
```

After a turn changes files, dscli runs the command itself, shows the result, and feeds
failures back to the model for up to two repair attempts. If the check still fails, the
run ends with the best available answer, marks verification as failed, and `dscli run`
exits with code `3`. Verification only runs when the current message changed files.

The verification command is supplied by you and is never a model-callable tool. It is
run without `--allow-shell` and without a per-command approval prompt because you
configured it explicitly. Treat it like any other command you run from the shell: the
model can modify the files it checks, so review the final diff.

## Approvals and safety

- Tools are confined to the workspace unless `--allow-outside` is passed.
- `.git` is never writable through the tools.
- Every write, patch, delete, and command shows a diff/command and asks
  `[y]es / [n]o / [a]ll / [q]uit` before changing anything. `-y` skips the prompts.
- Shell access is off by default and can be enabled only for a run.
- The verification command is the one exception to the approval flow: when you configure
  it with `--verify` or `verify_command`, dscli runs it automatically after edits without
  `--allow-shell` or a prompt. The model can neither call nor change it.
- Attachment contents, file contents, command output, and tool results are treated as
  untrusted data by the system prompt.
- No network access is performed by the tools; the only network traffic is the browser
  talking to DeepSeek.

This is still an assistant with write access to your files. Use a clean workspace or
version control, and review diffs before approving them.

## Configuration

The config is JSON at:

```powershell
dscli config --path
dscli config --show
dscli config --set headless=false
dscli config --set stable_seconds=6
```

Useful keys: `headless`, `channel` (`chrome`, `msedge`, or empty for Playwright's
Chromium), `chrome_user_data_dir` (Chrome's user-data directory; empty disables Chrome
session import), `chrome_profile` (empty = Chrome's last-used profile, or a name such as
`Default` / `Profile 1`), `user_agent` (empty = derive a normal Chrome UA from the
installed browser), `response_timeout_sec`, `first_response_timeout_sec`,
`stable_seconds`, `fallback_stable_seconds`, `max_steps`, `max_tool_calls`,
`max_read_chars`, `max_attachment_mb`, `deepthink`, `search`, `allow_shell`,
`allow_outside`, `verify_command` (empty disables verification),
`verify_timeout_sec` (clamped to 1–600, default 300).

Headless Chrome normally sends a `HeadlessChrome/...` user agent, which DeepSeek's CDN
rejects with a CloudFront 403. dscli automatically replaces it with the installed
Chrome's real version when running headless. Set `user_agent` explicitly if you need a
different string.

If DeepSeek changes its UI, override the affected CSS selectors in the `selectors`
object of the config file (they are merged over the defaults):

```json
{
  "selectors": {
    "composer": "textarea[placeholder=\"Message DeepSeek\"]",
    "assistant_message": ".ds-markdown.ds-assistant-message-main-content",
    "send_button": "div[role=\"button\"].ds-button--primary.ds-button--circle"
  }
}
```

## Troubleshooting

**Cloudflare / human check in headless mode.** dscli stops with a clear message. Run
`dscli login` (or `doctor --headed`) once in a visible browser, complete the
check, and retry.

**403 ERROR / "Request blocked" from CloudFront.** This is usually the headless browser
being fingerprinted. dscli tries to avoid it by using a normal Chrome user agent. If it
still happens, run `dscli login` headed once, then check whether
`doctor --headed` can reach the composer. You can also set `"channel": ""` or override
`"user_agent"` in the config.

**"Not logged in" or the composer never appears.** Confirm that DeepSeek works in your
normal Chrome browser. dscli imports the saved login even while Chrome is running; if
that login has expired, refresh or sign in at `chat.deepseek.com` in Chrome and retry.
If the composer still does not appear, run `dscli doctor --headed`, or
`dscli login` to sign in directly in the isolated profile. To stop importing
Chrome sessions, set `chrome_user_data_dir` to an empty string in the config.

**"The dscli browser profile is already in use."** Close any other dscli/Chrome window
that uses `%APPDATA%\dscli\profile`, then retry. Do not point two runs at the same
profile.

**The send button never enables.** The attachment may still be uploading, the message
may exceed the chat's limit, or the selector may have changed. Run `doctor` and check
the reported counts.

**A response is cut off around 12 s after the last text change.** DeepSeek did not show
its "Read aloud" toolbar, so dscli used the fallback completion detector. Increase
`fallback_stable_seconds` if replies are still streaming at that point.

**Chrome will not launch.** Set `"channel": ""` in the config to use Playwright's
Chromium, after running `python -m playwright install chromium`.

Debug screenshots and unparsed replies are written to `%APPDATA%\dscli\debug`.

## Limitations

- The DeepSeek web UI is not a stable API. Selectors, rate limits, and features can
  change at any time; `doctor` and the configurable selectors are the escape hatch.
- Chat messages can be length-limited. dscli caps the initial prompt and tool output,
  and long files should be read in ranges rather than pasted.
- Streaming is approximated by polling the DOM, not by tokens from the model. The
  interactive chat shows the final answer as the page updates, but tool calls still
  wait for the complete, validated JSON envelope.
- Interrupting with Ctrl+C clicks DeepSeek's stop button when dscli can find it. If the
  page does not expose one, generation may continue on DeepSeek's side; the interrupted
  answer is discarded, and the next message (or `/new`) re-establishes a clean turn.
- DeepSeek attachments accept only certain file types. When a file cannot be attached,
  the agent can still read it through `read_file` because the workspace path is provided
  in the prompt.
- Using browser automation against a web chat may conflict with the provider's terms.
  Use your own account and judgment.

## Development

```powershell
python -m compileall -q dscli
dscli doctor
```

The package is intentionally small: `browser.py` owns Playwright and selectors,
`scraper.py` holds the in-page JavaScript, `protocol.py` parses the envelope,
`tools.py` enforces the sandbox, and `agent.py` runs the turn loop.

### Publishing

PyPI uploads are manual and use your own API token:

```powershell
python -m pip install -e ".[dev]"
python -m build
python -m twine check dist/*
python -m twine upload dist/*
```

Twine prompts for the username `__token__` and your PyPI API token. Bump
`__version__` in `dscli/__init__.py` before every upload; PyPI never allows a version
number to be reused.

## License

MIT
