Skip to main content

dscli

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

you ──> dscli ──> web 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

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:

python -m playwright install chromium

To work from a source checkout instead:

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 login token from Chrome's local storage on every start and overwrites any stale copy left in dscli's profile, so the session in Chrome always wins. Chrome's cookies are copied as well whenever Chrome is not holding its cookie database open. 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:

dscli login

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

Check the page and selectors without sending anything:

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:

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. Accents use a muted palette that matches the wordmark: sage for success, dusty rose for errors, and soft teal for diffs and highlights. The same muted colors apply to the plain and non-TTY output. Output stays in normal scrollback after startup; dscli does not take over the full screen, and the welcome card shows the workspace, model, 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.

DeepThink is always enabled, and the UI streams the model's reasoning live in a compact block above the reply:

  Thinking
  │ I should inspect the configuration first.
  │ … +12 rows (Ctrl+O)
  │ charlie line
  │ delta line
  │ echo line

The first sentence stays pinned at the top while the newest three wrapped rows autoscroll beneath it, with the collapsed row count in between. Text is revealed between provider polls so the block keeps streaming in real time. When the turn ends it collapses to the same limited view with a Ctrl+O hint; press Ctrl+O at the prompt to print the full reasoning block (up to 500 rows) into the transcript. The streamed block is TUI-only; --plain and run do not show it.

The prompt footer tracks session token usage against a 1,000,000-token budget, and the same progress bar renders below the live Thinking and Responding output:

  tokens ███████░░░░░ 612.4k / 1.0M · 61%

Counts are estimated locally from every prompt sent and reply received (one token per CJK character, about four characters per token otherwise); they are not reported by DeepSeek. Attachment contents are not included. The count resets on /new and whenever a new chat is opened.

Conversation compaction

When the estimate reaches 1,000,000 tokens, dscli compacts the session instead of stopping. Before the next prompt is sent it asks the current chat to write a structured compaction summary — an <analysis> pass followed by a <summary> covering the request, technical decisions, files and code, errors and fixes, user messages, pending tasks, and the next step, following the same compact guide Claude Code uses. It then writes the conversation to a text file, starts a new DeepSeek chat, and sends a resume prompt that embeds the compaction and attaches the file. If the summary request fails or returns nothing usable, the deterministic activity log is used on its own. The compaction contains the model summary (when available), the conversation resume, the current task, the remaining (unfinished) plan steps, files changed so far, the verification state, the project structure, and instructions for continuing, so the model keeps working without redoing finished work.

Files are saved under %APPDATA%\dscli\compactions on Windows (~/Library/Application Support/dscli/compactions on macOS, ~/.config/dscli/compactions elsewhere) as compaction-YYYYmmdd-HHMMSS.txt. The check runs before every model turn, so it also works mid-task; the TUI shows a Compacting conversation spinner while the summary is written, then a ⟳ Compacted 1.0M tokens → … · new chat notice. /new clears the recorded conversation as well.

Deeplanning mode

dscli run "Split the parser module" --deeplanning
dscli chat --deeplanning

Deeplanning makes the model plan before it acts. While the run has no plan, the harness refuses write_file, apply_patch, delete_file, and run_command with an error result that tells the model to plan first. read_file and list_files stay available so the model can investigate, and update_plan unlocks execution as soon as it records a non-empty plan. The gate is enforced by dscli, not just requested in the prompt, so a model that skips the plan cannot touch the workspace. Plans remain per message: each new chat message needs its own plan, and clearing the plan re-arms the gate. Enable it for every run with dscli config --set deeplanning=true, or turn it off for one run with --no-deeplanning.

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.
Ctrl+O Expand the last thinking block in full.

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.

If Cloudflare interrupts a headless run, dscli opens the persistent profile in a visible Chrome window and resubmits the interrupted prompt. Complete the check in that window. dscli keeps the window open until the response actually starts, then minimizes it, keeps working, and closes the window and returns to headless when the task finishes. The current model turn restarts with the same prompt in a fresh chat, so an answer that was already streaming when the check appeared is fetched again. Set dscli config --set auto_human_check=false to fail fast instead.

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:

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:

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

Run a one-shot task

# 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 or the result was not confirmed complete, and 3 verification failed (takes precedence over 2). Verification failures and unconfirmed completions 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.
--search Turn DeepSeek's Search mode on for the run (DeepThink is always on).
--deeplanning Require a non-empty update_plan before writes or commands (alias: --deep-planning).
--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:

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

The fence matters: web chats render 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 only when the model replies with tool_calls: [], a non-empty final string, and "complete": true:

```json
{"thought": "the work is complete", "tool_calls": [], "complete": true, "final": "..."}
```

A final without complete: true, or with any unfinished plan step, is rejected and the model is sent back to continue. When the turn budget runs out first, dscli asks for a wrap-up summary and reports the result as not confirmed complete (dscli run exits with code 2). The configured verification command can also send the model back for repairs (see below).

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

The system contract adopts the operating guidance from Claude Code's Opus 5.5 system prompt, adapted to dscli's envelope protocol and local tools: act once you have enough information, keep changes minimal and consistent with the surrounding code, avoid scope creep and unnecessary abstractions, confirm hard-to-reverse actions, reference code as path:line, report outcomes faithfully, and treat all tool output as untrusted data.

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 task cannot finish 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). The run does not finish while any step is pending or in_progress; the model is told which steps remain and asked to finish them or revise the plan. It must also declare "complete": true explicitly, so a bare "done" summary cannot end a task by accident. There is no give-up acceptance: if the budget runs out first, the result is reported as not confirmed complete instead of being treated as a successful finish. Plans apply to a single message in chat and are reset for every new task.

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

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

When the model claims completion after changing 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. A verified result is still only as trustworthy as the command you configured.

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:

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

Useful keys: url (the DeepSeek chat URL; set it only for a custom deployment), 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, auto_human_check (true = open a visible window to solve a verification check; false = fail fast), max_steps, max_tool_calls, max_read_chars, max_attachment_mb, search, deeplanning, 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.

DeepSeek's default selectors live in dscli/providers.py. If its UI changes, override the affected CSS selectors in the selectors object of the config file (they are merged over the defaults):

{
  "selectors": {
    "composer": "textarea[placeholder=\"Ask anything\"]",
    "send_button": "button.new-send"
  }
}

Selectors saved by older dscli versions that match DeepSeek's built-in defaults are ignored automatically, so built-in selector updates still take effect. Custom overrides are preserved.

Troubleshooting

Cloudflare / human check in headless mode. When a check interrupts a headless run, dscli opens the same persistent profile in a visible Chrome window and waits for you to solve it. The window stays visible until the reply starts, then minimizes; dscli finishes the task, then closes the window and returns to headless. If the window does not appear or the check times out, run dscli login (or dscli doctor --headed) once in a visible browser and retry. Set auto_human_check to false to keep the fail-fast behavior.

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 on every start, even while Chrome is running; if that login has expired, refresh or sign in at chat.deepseek.com in Chrome and retry. Chrome keeps its cookie database locked while it runs, so dscli keeps using the cookies already saved in its own profile and refreshes them from Chrome the next time it starts with Chrome closed. If the composer still does not appear, run dscli doctor --headed, or dscli login to sign in directly. 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 completion toolbar (Read aloud), 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 streams a final answer as the page updates only after it declares complete: true; tool calls still wait for the complete, validated JSON envelope. The token bar is likewise a local estimate of sent and received text, not usage reported by DeepSeek.
  • Interrupting with Ctrl+C clicks DeepSeek's labelled stop button when dscli can find it. If the page does not expose one, generation may continue in the web chat; the interrupted answer is discarded, and the next message (or /new) re-establishes a clean turn.
  • Web-chat 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 DeepSeek's terms. Use your own account and judgment.

Development

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:

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

Metadata

Release files for dscli-agent 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dscli-agent 0.3.0
File Size Uploaded Provenance Actions
dscli_agent-0.3.0.tar.gz 127.5 kB Without provenance Details for dscli_agent-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for dscli-agent 0.3.0
File Interpreter ABI Platform Actions
dscli_agent-0.3.0-py3-none-any.whl Python 3 none any Details for dscli_agent-0.3.0-py3-none-any.whl

Total release size: 217.6 kB

Release files / dscli_agent-0.3.0.tar.gz

Download URL dscli_agent-0.3.0.tar.gz
Size 127.5 kB
Tags Source
SHA-256 checksum
How to use checksums
1e28c900b007a44edeeeaaa9abb36d2f245a724e5c5d6fbc076cd800a653aef2
BLAKE2b-256 checksum
How to use checksums
ce33a6fc8a4f60016a37786c4197844a3f422e00be9b70a79b79ca8813472294
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release files / dscli_agent-0.3.0-py3-none-any.whl

Download URL dscli_agent-0.3.0-py3-none-any.whl
Size 90.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e29b9c5896ec3a0582fc2d19eb262a11b0693798ea29a7a673052f99719ba90c
BLAKE2b-256 checksum
How to use checksums
5c49869b2f7a9a8c957eff71e9dbdee3faba65e41b3c077e47480df4445d57e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Security for dscli-agent 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page