@este.systems/dsc
v1.0.0
Published
CLI coding agent for DeepSeek
Maintainers
Readme
dsc
A CLI coding agent for DeepSeek.
Streams responses, calls tools (bash, read_file, write_file, edit_file,
grep, glob, web_fetch, web_search, task_*), keeps per-cwd sessions,
and runs as a ink-based TUI — prompt +
status bar pinned at the bottom, finalized turns kept in normal scrollback so
they stay selectable and copy/paste-able. One-shot mode (dsc "prompt") runs
the agent against stdout and exits without rendering ink, so it pipes cleanly
into scripts.
Install
dsc requires Node 22+ (it uses fs.promises.glob, stable since 22.0).
Step 1 — install Node 22+
| Platform | One-liner |
|---|---|
| Linux (Debian/Ubuntu) | curl -fsSL https://deb.nodesource.com/setup_22.x \| sudo -E bash - && sudo apt install -y nodejs |
| Linux (Fedora/RHEL) | sudo dnf install -y nodejs:22/common |
| Linux (Arch) | sudo pacman -S nodejs npm |
| macOS | brew install node@22 && brew link node@22 |
| Windows (winget) | winget install OpenJS.NodeJS.LTS |
| Windows (scoop) | scoop install nodejs-lts |
| Cross-platform (nvm) | nvm install 22 && nvm use 22 |
Verify: node --version should print v22.x.x or higher.
Step 2 — install dsc
Three options. (A) is the standard install once the package is on npm. Use (B) for a frozen tarball you can ship offline, or (C) if you'll be hacking on the source.
(A) From npm:
On most Linux / macOS installs, npm's global directory is /usr/local,
which is root-owned. Before installing any global npm tool — not just
dsc — point npm at a user-owned directory so npm install -g (and dsc's
/update) work without sudo:
# One-time setup. Skip if you've done this for another npm tool.
npm config set prefix ~/.local
# Add this to ~/.bashrc / ~/.zshrc and reopen the terminal:
export PATH="$HOME/.local/bin:$PATH"Then:
npm install -g @este.systems/dscWindows users already get a user-owned prefix by default (%APPDATA%\npm).
Skip the npm config set prefix step and run the install directly.
After install, run dsc. On a fresh machine the TUI greets you with a
one-time welcome card and points you at /api-key sk-... for the
initial key save. To upgrade later, run /update inside the TUI (or
npm install -g @este.systems/dsc@latest from outside) — dsc also
checks npm in the background once a day and surfaces a one-line notice
when a newer version is published.
(B) From a local tarball (works on all platforms with the same npm):
git clone https://github.com/EsteSystems/dsc.git
cd dsc
npm install
npm run package # produces pkg/<name>-<version>.tgzthen on Linux / macOS:
scripts/install.sh # auto-configures user prefix if needed
scripts/install.sh --system # use existing prefix (may need sudo)or on Windows PowerShell:
.\scripts\install.ps1The Linux / macOS wrapper checks whether npm config get prefix points
at a user-writable directory. If not — and you aren't root — it sets up
~/.local and adds a PATH hint before installing. Pass --system to
opt out.
(C) From source for development (live edits, no rebuild step):
git clone https://github.com/EsteSystems/dsc.git
cd dsc
npm install
npm link # exposes `dsc` globallyThe shim in bin/dsc.mjs runs the TypeScript sources directly through
tsx, so your next dsc launch picks up edits immediately. If tsx
isn't available it falls back to dist/ (populate with npm run build).
Step 3 — verify
dsc --helpIf you get "command not found", your shell's PATH doesn't include npm's
global-bin directory. Find it with npm config get prefix; the binary
lives in <prefix>/bin on Linux/macOS or <prefix> on Windows. Add that
to your PATH and reopen the terminal.
API key
Easiest path — launch dsc and use the slash command:
> /api-key sk-...That writes ~/.config/deepseek/deepseek.json with 0600 perms,
preserving any other fields (e.g. search-provider keys) already in the
file. You can also set $DEEPSEEK_API_KEY in your shell — it takes
priority over the file.
The rest of this section walks through the manual file setup if you'd
rather do it yourself. A ready-to-edit template lives at the repo
root: deepseek.json.example. Copy it to
the config path below and replace the placeholder values:
# Linux / macOS:
mkdir -p ~/.config/deepseek
cp deepseek.json.example ~/.config/deepseek/deepseek.json
chmod 600 ~/.config/deepseek/deepseek.json
$EDITOR ~/.config/deepseek/deepseek.json# Windows PowerShell:
$dir = "$HOME\.config\deepseek"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Copy-Item deepseek.json.example "$dir\deepseek.json"
notepad "$dir\deepseek.json"The template's full contents:
{
"api_key": "sk-REPLACE-WITH-YOUR-DEEPSEEK-API-KEY",
"search": {
"provider": "brave",
"brave": { "api_key": "BSA-REPLACE-WITH-YOUR-BRAVE-API-KEY" },
"tavily": { "api_key": "tvly-REPLACE-WITH-YOUR-TAVILY-API-KEY" }
}
}You only need api_key. The search block is optional — leave it out
and web_search falls back to the keyless DuckDuckGo HTML scraper.
dsc reads this file from a platform-dependent path:
| Platform | Default path |
|---|---|
| Linux / macOS | ~/.config/deepseek/deepseek.json |
| Windows | %USERPROFILE%\.config\deepseek\deepseek.json |
On Windows, %USERPROFILE% is C:\Users\<your account name>, so the
absolute path is e.g. C:\Users\dann\.config\deepseek\deepseek.json.
Set XDG_CONFIG_HOME to override the location on any platform.
The env var DEEPSEEK_API_KEY takes priority over the file when both
are set.
Linux / macOS
mkdir -p ~/.config/deepseek
cat > ~/.config/deepseek/deepseek.json <<'JSON'
{
"api_key": "sk-..."
}
JSON
chmod 600 ~/.config/deepseek/deepseek.jsonWindows — PowerShell
$dir = "$HOME\.config\deepseek"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
'{"api_key":"sk-..."}' | Set-Content -Path "$dir\deepseek.json" -Encoding utf8NoBOM-Encoding utf8NoBOM matters: PowerShell 5.1's Set-Content -Encoding
utf8 writes a UTF-8 BOM by default, and not every JSON parser likes
that. dsc is forgiving (it strips a leading BOM), but using the explicit
form keeps the file portable. PowerShell 7+ defaults to no-BOM.
Windows — cmd.exe
mkdir "%USERPROFILE%\.config\deepseek"
echo {"api_key":"sk-..."} > "%USERPROFILE%\.config\deepseek\deepseek.json"Or skip the file: env var
Any platform, any shell:
export DEEPSEEK_API_KEY=sk-... # bash / zsh
$Env:DEEPSEEK_API_KEY = "sk-..." # PowerShell (current session)
[Environment]::SetEnvironmentVariable(`
"DEEPSEEK_API_KEY", "sk-...", "User") # PowerShell (persisted)
set DEEPSEEK_API_KEY=sk-... # cmd.exe (current session)
setx DEEPSEEK_API_KEY "sk-..." # cmd.exe (persisted)Accepted file shapes
{ "api_key": "sk-..." } // simple
{ "DEEPSEEK_API_KEY": "sk-..." } // alt key
{ "env": { "DEEPSEEK_API_KEY": "sk-..." } } // env-style
{ "env": { "ANTHROPIC_AUTH_TOKEN": "sk-..." } } // claude-switcher compatQuick start
dsc # interactive TUI
dsc "summarize src/api.ts" # one-shot (runs and exits)
dsc --yolo "rename Foo to Bar" # skip approval prompts
dsc --no-resume # fresh session, ignore prior history
dsc -m deepseek-v4-flash # pick a model for this launch
dsc --resume <id> # resume a specific session by idTUI layout
── scrollback (selectable, copy/pasteable) ──
user ← prior turns rendered into <Static>
assistant ← so they live in normal terminal scrollback
← tool: ok ← tool results
...
── dynamic frame (pinned to bottom) ──
assistant ← currently-streaming message (rebuilds on
… every chunk; rich markdown on finalize)
○ task one ← agent task list, hidden when empty / all done
◐ task two
● task three
→ bash(npm test) ← running tool indicator
queued (2): ← prompts you typed while busy
→ run the tests again
→ and commit
> _ ← your prompt input
deepseek-v4-pro · $0.012 ▲1.4K (h:1.1K m:300) ▼820 ctx:9.2K 0:34
↑ status bar (model · cost · in/out tokens · cache hit/miss · ctx · timer)The streaming current-turn renders plain text so re-renders stay cheap;
once the turn finishes it moves into <Static> and gets the full
markdown / table / code-block rendering. Approvals appear as their own
modal-style yellow-bordered box; the prompt is greyed out underneath
until you answer.
Slash commands
Type / and TAB completes to the longest unique prefix; an inline dim
ghost-text suggestion previews the match as you type.
| Command | What it does |
|---|---|
| /api-key [key] | Show where the key is loaded from (env / config file / unset). With a key arg, write it to ~/.config/deepseek/deepseek.json with 0600 perms. When unset, prints the DeepSeek signup URL. |
| /search / /search use <p> / /search key <p> [key] | Inspect / switch the active search provider, or show / save a per-provider key. No-arg prints active provider + key status + signup URLs. use brave\|tavily\|ddg writes search.provider. key <provider> [key] shows signup URL when no key, saves under search.<provider>.api_key when given one. |
| /update | Force-check npm for a newer release and install it (npm install -g @este.systems/dsc@latest). The TUI also checks once a day in the background and surfaces a one-line "X available" notice when behind. |
| /copy | Copy the most recent assistant response to the OS clipboard (pbcopy / clip / wl-copy / xclip / xsel). |
| /export [path] | Write the current session JSON to path (default: cwd, with a <name|id>-<date>.json filename). |
| /import <path> [--keep-cwd] | Load a session from a JSON file as the active session. Rebinds cwd to the current directory by default so auto-resume picks it up here; --keep-cwd preserves the original. Mints a fresh id on collision (no overwrites). |
| /clear | Start a new session. Old session stays on disk. Also drops per-tool "always" approvals. |
| /cost | Show token usage and estimated cost so far. |
| /model [name] | Show or switch model. Available: deepseek-v4-pro, deepseek-v4-flash. |
| /yolo | Toggle approval mode (write/edit/bash/web_fetch). |
| /reasoning [on\|off] | Show/hide reasoning_content from thinking models. Default on. |
| /lang [name\|off] | Force the model to reply exclusively in a named language. |
| /auto-continue [N\|off] | When the agent hits the per-turn tool-call cap, auto-grant up to N extra 24-call budgets. |
| /list | List sessions in the current cwd. The active session is marked with *. |
| /resume <#\|id\|name\|last> | Resume a session by index (from /list), id, name, or last. |
| /save <name> | Name the current session. Show up in /list by name. |
| /rename <text> | Override the "assistant:" label for the current session. |
| /queue [clear] | List or clear the type-ahead queue. |
| /audit [path\|show N] | Print the audit log path, or show the last N entries. |
| /transcript | Print the full conversation, including any messages compaction archived. |
| /compact [N] | Summarize older turns into a synthetic block (kept in the system prompt) and move them to the archive. Keeps the last N user turns verbatim (default 4). Cumulative across re-runs. |
| /edit [text] | Open $VISUAL/$EDITOR/vi on a tmp file; the saved content runs as the next prompt. |
| /version | Print version (dsc, Node, platform/arch). |
| /help | Show the in-app help. |
| /exit | Quit. |
Multi-line input
End a line with a single \ to continue on the next line (bash-style).
\\ is treated as a literal trailing backslash. The TUI renders the
accumulated buffer above the active prompt dim with a … marker so
you can see what's queued. ESC cancels the buffer. For longer or
paste-heavy drafts, use /edit.
> please write a function\
… that takes (a, b, c)\
… and returns a + b + cHotkeys
| Key | Where | What |
|---|---|---|
| Up / Down | Prompt | Recall past prompts (persisted across sessions in ~/.local/state/dsc/history). |
| Tab | Prompt | Complete a partial /slash command. The TUI also previews the match as dim ghost text inline. |
| Ctrl+A / Ctrl+E | Prompt | Cursor to start / end of line. |
| Ctrl+U / Ctrl+K | Prompt | Delete to start / end of line. |
| Ctrl+W | Prompt | Delete the word before the cursor. |
| y | Approval | Approve this call. |
| a | Approval | Approve this call and auto-approve future calls of the same tool for the rest of this session (cleared by /clear). |
| n / ESC | Approval | Reject. |
| ESC | Turn busy | Abort the in-flight turn. |
| ESC | Multiline | Clear the accumulated backslash-continuation buffer. |
| Ctrl+C | Turn busy | Abort the in-flight turn. |
| Ctrl+C / Ctrl+D | Idle | Exit cleanly. |
Agent task list
Non-trivial multi-step requests trigger the model to reach for the
task_create / task_update / task_list tools. The TUI renders the
list above the prompt as ○ pending / ◐ in-progress / ● completed
bullets so you can watch the agent's plan execute. Hidden once every
task is completed. Lives only in memory — cleared by /clear and on
exit.
Session scoping (per-directory)
Sessions are tied to the directory you launched dsc from — that's how auto-resume figures out which conversation to bring back.
- Run
dscin~/code/foo→ it auto-resumes the most recent session whosecwdis~/code/foo. If there isn't one, you start fresh. cdinto~/code/barand rundsc→ you get bar's last session, not foo's. Project conversations stay scoped to their project; you can't accidentally bleed astrophysics notes into a CLI refactor./clearstarts a brand-new session for the current cwd. The old one stays on disk and shows up in/listnext time you're back./listshows only sessions whosecwdmatches the current directory./resume <#>resolves indices from that list./resume <id>(or a/save'd name) can cross cwds — useful if you want to revisit a session from elsewhere; the cwd it was born in stays attached to it.--no-resumeskips auto-resume entirely and gives you a fresh session this launch.
Each session is a single JSON file under
~/.local/share/dsc/sessions/. Open one in your editor if you want to
see exactly what got persisted, including any compacted summary and
the archived messages from /transcript.
Tools the agent can use
| Tool | Approval | Notes |
|---|---|---|
| read_file(path, offset?, limit?) | none | 2000 lines default; long lines truncated. |
| grep(pattern, path?, glob?, case_insensitive?) | none | ripgrep when available, grep -rn fallback. |
| glob(pattern, path?) | none | Node 22+ fs.glob, capped at 500. |
| web_search(query, count?, freshness?) | none | Pluggable backends (Brave / Tavily / DuckDuckGo). |
| task_create(subject, activeForm?) | none | Adds a pending task to the user-visible task list. |
| task_update(id, status?, subject?, activeForm?) | none | Moves a task between pending / in_progress / completed. |
| task_list() | none | Returns the current task list with statuses. |
| write_file(path, content) | yes (unless --yolo) | Side-by-side diff inside the approval dialog. |
| edit_file(path, old_string, new_string, replace_all?) | yes | Exact substring replace; old_string must be unique unless replace_all=true. |
| bash(command, description?, timeout_ms?) | yes | /bin/sh on Linux/macOS, cmd.exe on Windows. Output capped at 16 KB. |
| web_fetch(url) | yes | HTML stripped to text, capped at 50 KB. |
Read-only tools never prompt. The rest do unless --yolo is on or
you've previously said a (always) for that tool this session. The
task_* tools mutate an in-memory list that the TUI renders above the
prompt; the model is encouraged (via the system prompt) to reach for
them on non-trivial multi-step asks so the user sees real-time progress.
Per-project instructions
Drop a Markdown file at the project root (or any ancestor of your cwd)
and dsc will append it to the system prompt every turn — same idea as
CLAUDE.md in Claude Code, project-tested with the convergent
AGENTS.md convention. Use it to teach the agent project conventions
without re-typing them in every conversation.
dsc looks in three places, in this order:
| Path | Purpose |
|---|---|
| ~/.config/dsc/instructions.md | User-global — personal preferences across all projects (default response language, "prefer ripgrep over grep", etc). |
| AGENTS.md | Project-level, walked up from cwd. Shared with other agents that read the convergent AGENTS.md convention. |
| .dsc/instructions.md | Project-level, dsc-only. Useful when a rule should apply when working through dsc but not when other agents look at the repo. |
Each present file is included as its own labeled section in the prompt, so the model knows the source. The dsc-specific file appears last and effectively overrides earlier ones on conflict. Files re-read every turn — edit them and the next request picks up the change.
/instructions lists which files are currently active and shows their
content. Empty files are treated as absent. Files larger than 64 KB are
skipped silently — keep instruction files reasonably short, both for the
cache prefix and because the model parses them every turn.
MCP servers
dsc can connect to remote MCP servers
at boot and expose their tools to the agent alongside the built-ins.
Configure under mcp.servers in ~/.config/deepseek/deepseek.json:
{
"api_key": "sk-...",
"mcp": {
"servers": {
"tavily": {
"url": "https://mcp.tavily.com/mcp/",
"headers": { "Authorization": "Bearer ${TAVILY_API_KEY}" }
}
}
}
}Per-server fields:
| Field | Notes |
|---|---|
| transport | "http" or "stdio". Inferred from which of url/command is set; specify when ambiguous. |
| url | Required for HTTP transport. Remote MCP endpoint. |
| headers | HTTP only. Static map merged into every request. ${VAR} references expand against process.env at connect time; missing vars throw. |
| query | HTTP only. Appended as URL query params (e.g. ?tavilyApiKey=…). Same ${VAR} expansion. |
| command | Required for stdio transport. Executable for the local subprocess (e.g. "npx"). |
| args | stdio only. Argv list. ${VAR} expanded. |
| env | stdio only. Extra env vars for the child, merged on top of process.env so it still sees PATH etc. ${VAR} expanded. |
| requireApproval | "always" / "never" / "auto". Defaults: "always" for stdio (local subprocesses usually touch the filesystem), "auto" for HTTP (heuristic on tool names — write/delete/send/run/etc trigger the dialog; read/list/search/get pass through). The a (always) answer adds the namespaced tool name to a session-scoped allowlist so repeats skip the prompt. |
| enabled | Optional, default true. Set to false to skip without removing the entry. |
| timeoutMs | Optional connect timeout. Default 8 s. |
Each server's tools are advertised to the model as mcp_<server>_<tool>,
so two servers exposing the same tool name don't collide. /mcp shows
the connected servers + their tools.
Example with both transports:
{
"mcp": {
"servers": {
"tavily": {
"url": "https://mcp.tavily.com/mcp/",
"headers": { "Authorization": "Bearer ${TAVILY_API_KEY}" }
},
"fs": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/workspace"]
}
}
}
}Sessions and history
Each session is a JSON file under $XDG_DATA_HOME/dsc/sessions/
(default ~/.local/share/dsc/sessions/) keyed by id. It carries:
messages— the active conversation log (sent to the API).archivedMessages— older messages that/compacthas summarized away. Persisted on disk for/transcript, never sent to the API.compaction— the cumulative summary text and metadata.stats— token / cost / tool-call counters.model— last selected model.
Saves happen on every onTurn callback (after each assistant message,
after each tool result), with a single-in-flight, coalescing writer —
so a Ctrl+C / OOM / power loss mid-turn won't cost you the latest
committed state.
Compaction
/compact [N] summarizes everything before the last N user turns,
stores the summary on the session, archives the original messages, and
trims messages to the kept tail. The summary appears in the dynamic
suffix of every subsequent system prompt (Previously in this session:),
so the model retains semantic context. Cumulative — re-running /compact
folds the prior summary into the new one.
Auto-compact runs the same routine after any successful turn whose
estimated context exceeds DSC_AUTO_COMPACT_AT tokens (default 50 000;
set 0 / off / false to disable).
/transcript prints the full conversation, including archived chunks,
so nothing is lost — just absent from the prompt the model sees.
Audit log
Every tool execution (including rejected ones) writes one JSONL line to
$XDG_STATE_HOME/dsc/audit.log (default ~/.local/state/dsc/audit.log).
Each line carries ts, session, cwd, tool, approved, plus
tool-specific fields:
{"ts":"2026-05-09T15:32:01Z","session":"…","cwd":"/home/dann/code/dsc","tool":"bash","approved":true,"command":"npm test","exit":0,"stdout_bytes":4012,"stderr_bytes":0}Useful greps:
# every bash command this week
jq 'select(.tool=="bash") | "\(.ts) \(.command)"' ~/.local/state/dsc/audit.log
# things rejected at approval
jq 'select(.approved==false)' ~/.local/state/dsc/audit.log
# files written by a specific session
jq 'select(.session=="abc1234" and .tool=="write_file") | .path' \
~/.local/state/dsc/audit.logDisable with DSC_NO_AUDIT=1. There's no rotation — at gigabyte scale
you'll want to truncate it yourself.
File locations
| Path | What |
|---|---|
| ~/.config/deepseek/deepseek.json | API key (and search-provider keys). 0600. |
| ~/.local/share/dsc/sessions/<id>.json | One file per session. |
| ~/.local/state/dsc/history | Up/down arrow recall (1000-line cap). |
| ~/.local/state/dsc/audit.log | JSONL, append-only. |
| /tmp/dsc-edit-*/prompt.md | Transient; created by /edit, removed on close. |
| <repo>/dist/ | Compiled output (only used as a fallback for the global shim). |
XDG variables observed: XDG_CONFIG_HOME, XDG_STATE_HOME, XDG_DATA_HOME.
Environment variables
| Var | Default | Purpose |
|---|---|---|
| DEEPSEEK_API_KEY | (read from config file) | Overrides the api_key in deepseek.json. |
| DSC_AUTO_COMPACT_AT | 50000 | Token threshold for auto-compact. 0/off/false disables. |
| DSC_AUTO_CONTINUE | 0 | When the agent hits the per-turn tool-call cap, auto-grant up to N extra 24-call budgets before stopping. 0/off/false keeps the manual "type continue" prompt. |
| DSC_NO_AUDIT | (off) | 1 disables the JSONL audit log. |
| DSC_SEARCH_PROVIDER | (config or ddg) | brave, tavily, or ddg. |
| BRAVE_API_KEY | (config) | Brave Search key. |
| TAVILY_API_KEY | (config) | Tavily key. |
| VISUAL / EDITOR | (vi) | Used by /edit. |
| XDG_CONFIG_HOME | ~/.config | Config root. |
| XDG_STATE_HOME | ~/.local/state | State root. |
| XDG_DATA_HOME | ~/.local/share | Data root. |
Search providers
Pick at runtime with DSC_SEARCH_PROVIDER:
- brave — api-dashboard.search.brave.com, 2000 free queries/month. Recommended.
- tavily — tavily.com, 1000 free/month, agent-tuned snippets.
- ddg — DuckDuckGo HTML scrape, no key, brittle.
Per-provider keys live in the config:
{
"api_key": "sk-...",
"search": {
"provider": "brave",
"brave": { "api_key": "BSA..." }
}
}{PROVIDER}_API_KEY env var (e.g. BRAVE_API_KEY) overrides the
file value.
Packaging and distribution
For local global install (any platform with Node 22+):
npm run package # produces pkg/<name>-<version>.tgz
scripts/install.sh # linux / macOS — wraps `npm install -g pkg/*.tgz`
.\scripts\install.ps1 # Windows PowerShell — same ideaThe build is driven by the prepack lifecycle hook (scripts/build.mjs),
which wipes dist/ before recompiling. That keeps stale artifacts (e.g.
leftover from a branch switch) out of the tarball whether you ran
npm pack, npm publish, or npm run package.
What ships is controlled by the files field in package.json (currently
bin/, dist/, README.md, LICENSE). Source TypeScript and devDeps are
deliberately excluded.
Publishing to npm
The package is configured to publish as @este.systems/dsc with public
access. To release:
# 1. Roll the `## [Unreleased]` section in CHANGELOG.md into a dated
# `## [X.Y.Z] - YYYY-MM-DD` header and add the compare link at the
# bottom. Commit the changelog edit before npm version so the bump
# commit and the changelog entry land in the same release.
# 2. Bump the version (semver):
npm version patch # → 1.0.1 (creates a commit + git tag)
# use `minor` or `major` for the others
# 3. Make sure you're logged in to npm:
npm whoami # should print your username
npm login # if not
# 4. (Optional) preview the tarball:
npm pack --dry-run
# 5. Publish:
npm publish # respects publishConfig.access=public
# 6. Push the version-bump commit and tag:
git push --follow-tagsNotes:
- The package name is scoped (
@este.systems/dsc), sonpm publishdefaults to private.publishConfig.access = "public"inpackage.jsonoverrides that. Don't drop it. - npm now nudges hard for 2FA. Enable with
npm profile enable-2fa auth-and-writes. You'll be asked for an OTP on every publish. - Once published, anyone can install with
npm install -g @este.systems/dsc. The CLI binary is still justdsc. - After publish, the
pkg/*.tgzproduced locally is identical to what you uploaded — useful for offline installs (scripts/install.sh).
Development
npm run dev # tsx src/tui.tsx
npm run typecheck # tsc --noEmit
npm test # node --test, runs tests/*.test.ts via tsx
npm run build # compiles to dist/
npm run package # build + npm pack into pkg/Tests live under tests/, separate from src/ so they don't ship in
the npm tarball. The runner is Node's built-in node --test invoked
through tsx (no extra test-framework dependency). CI runs the same
command on Linux, macOS, and Windows for every push and PR.
Source layout:
src/
tui.tsx # entry: arg parsing, session + agent wiring, slash dispatcher
agent.ts # tool-call loop, AgentEvents emitter, repair logic
api.ts # DeepSeek client, retry/abort, prompt cache rates, saveApiKey
tools.ts # tool schemas + executors (read/write/edit/bash/grep/glob/web_*/task_*)
approval.ts # confirm{Write,Edit,Bash,Fetch} + ApprovalPayload + 3-state answer
audit.ts # JSONL audit logger
search.ts # Brave / Tavily / DDG dispatch
compact.ts # /compact summarization routine
history.ts # session save/load/list, /export, /import, migrate-legacy
repl_history.ts # ~/.local/state/dsc/history reader/writer (prompt recall)
ui.ts # Spinner used by one-shot mode
prompt.ts # SYSTEM_PROMPT + buildSystemPrompt
editor.ts # $EDITOR launcher for /edit
slash.ts # SLASH_COMMANDS list + TAB-completion helper
store.ts # TUI pub/sub state container (history, agentTasks, queue, etc.)
update.ts # npm registry probe + `npm install -g` runner for /update
clipboard.ts # cross-platform clipboard write (pbcopy / clip / wl-copy / xclip / xsel)
tui/
App.tsx # root layout
History.tsx # <Static> for finalized turns
CurrentTurn.tsx # in-progress streaming message
Markdown.tsx # markdown → ink Text-tree (incl. tables)
AgentTaskList.tsx # task_* bullets above the prompt
QueuedPrompts.tsx # dim list of pending submissions
TaskLine.tsx # "→ tool(...)" indicator while a tool runs
StatusBar.tsx # full-width inverse-video status line w/ busy spinner
PromptInput.tsx # backslash-continuation buffer + prompt char
Input.tsx # custom controlled input (cursor, history, TAB + ghost suggest)
ApprovalDialog.tsx # yellow-bordered modal with inline diff/command preview
useStore.ts # selector + equality hook over store.tsThe agent loop in agent.ts supports two output modes via a single
flag. With an events callback set (the interactive TUI), it emits
structured events — assistant start/content/reasoning/final, tool
start/end, notice — that the TUI pipes into the store. Without
events (one-shot mode), it writes the same information directly to
stdout as plain text. Same loop, same tool plumbing, just two thin
output adapters.
