Files
deepseek-harness/apps/cli

@deepseek-ai/dsh

English | 中文

Argv is parsed once through a Commander adapter (src/args.ts): one program whose default (no subcommand) is the TUI/headless surface (--config, -p/--prompt, --resume, --dump-config, --dump-default-config), whose meta subcommand is the same TUI over this checkout, whose upgrade subcommand is an option-less guided-session entry, and whose web subcommand is the browser UI. src/bin.ts switches on the resolved mode and dynamic-imports only that mode's module. dsh --help lists every mode and dsh web --help renders the web usage, dsh --version prints this app's version, and an unknown option or a mistyped --resume fails loud (stderr, exit 1) instead of misrouting. Every subcommand that shares no option with the default surface — upgrade, web, meta — rejects a leaked --config/-p/--resume/dump flag rather than running and dropping it. dsh web's --host/--port are unvalidated pass-through overrides: the dsh-host-webserver schema is the single source of both the default (the shipped Web overlay value when a flag is absent) and validity, and rejects a bad value at boot. --trusted-host appends named authorities for the /api browser-trust fence; an all-interfaces bind additionally derives the machine's LAN IP literals itself (src/app-cli-entry.ts), so the printed LAN URL works without flags.

The TUI surface:

  • boots base.cordis.yml plus tui.cordis.yml through dsh-app-boot; --config <path> applies a patch-list overlay instead of the personal overlay, while --config-replace <path> boots that file as the complete tree;
  • resumes a persisted session with dsh --resume <session-id> and, when the Node host exposes process.execve, supplies the TUI's in-place handoff host: after selector preflight and current-session flush, the host disposes the app and replaces the process with a normalized resume invocation; runtimes without process replacement leave the session running and say so. This CLI owns session identity and the exit line rather than the config: it mints or selects the main session id and provides it, plus the exact command that reproduces this invocation, on the boot context (MAIN_SESSION_ID_KEY and TUI_GOODBYE_MESSAGE_KEY). No cordis.yml key can drop resume, and a missing or unreadable id fails loud instead of creating a fresh session;
  • treats the invoking directory as the workspace — sessions, relative paths, and workspace instructions resolve from the cwd (dsh meta is the sole exception, below);
  • tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential cordis toolset can read and modify it;
  • applies the personal overlay from ~/.dsh (see app-boot's Personal config): config.yaml patches the booted tree, while .env there is the credential provider's own store (never hoisted into the environment, so keys stay rotatable). Environment precedence is ambient > project .env.
  • registers bare /compact: while the agent is idle, it summarizes useful older history even below automatic pressure, rejects arguments, and reports success only after the standalone replacement bracket is durable. A prompt submitted during compaction keeps its queue identity and starts after that checkpoint; injected context remains visible.

dsh meta is that same TUI with this harness checkout as the workspace, so working on dsh itself needs no cd. It chdirs to the checkout root — resolved from the launcher's real path, the same root the source-path prompt section names — after the environment is settled, so precedence is unchanged while the session cwd and HMR watch root move together. Meta always starts a fresh session and accepts no default-surface options; use ordinary dsh --resume <id> to resume a persisted session.

dsh upgrade is a guided fresh-session entry over the default TUI surface: it mints a fresh session in the invoking directory and seeds its first turn with the bundled dsh-upgrade skill, exactly as if the user typed /skill:<name>. The launcher passes the skill name on the boot context (INITIAL_SKILL_KEY), which the TUI auto-invokes once the chat is live. Both take no options — --config, -p, and --resume fail loud — and seed only on this first launch, so a later dsh --resume <id> of the session is an ordinary TUI session with no re-injection.

dsh --dump-config and dsh web --dump-config print the composed config tree — the shipped base, the surface overlay, and the --config or personal overlay, exactly the layers that surface would boot — as YAML on stdout and exit without booting; --dump-default-config stops at the surface overlay, so diffing the two shows precisely what the user layer changes. Each run of rows is preceded by a # == comment naming the file it comes from and the layers that patched it (e.g. # == base.cordis.yml, patched by tui.cordis.yml), so the output shows provenance while staying one loadable document. Composition runs through the include's own patch algorithm and YAML dialect (applyEntryPatches/entryListSchema from @cordisjs/plugin-include), so the dump cannot drift from what boots; !!js expressions print verbatim and unevaluated, and a patch whose target row is absent is reported on stderr with its layer, mirroring the Loader's boot-time warning. Launcher-owned boot-context values (session identity, CLI-flag patches) are per-invocation facts outside the config tree and do not appear. The dump flags reject boot-only flags (-p, --resume, --config-replace) rather than silently ignoring them, and --dump-default-config takes no --config.

The Web and headless surfaces boot base.cordis.yml plus web.cordis.yml, then apply $DSH_HOME/config.yaml; an explicit --config <path> replaces that personal overlay. Both surfaces otherwise share the same composition: both treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless --workspace-root <path> overrides it, load applicable AGENTS.md/CLAUDE.md instructions into each agent-loop request prefix with a 65,536-byte render budget, opt into first-message model titles, use the same bounded transient model-request retry policy as the TUI, and mount a disposable in-memory SQLite content-index service. That service is ACTIVE at boot, while its node:sqlite module and database handle open only on the first content search. This keeps Node 22 startup output free of SQLite's experimental warning before search is used; the first actual search may still emit the runtime warning. Each service instance owns its database, so parallel invocations neither share unsupported SQLite state nor leave derived index files behind, and the first search lazily reconciles live and persisted logs. Headless differs only in listening on an OS-assigned port (parallel dsh -p runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (pnpm run build && pnpm run build:web).

The shipped TUI and Web compositions register the native DeepSeek adapter plus pi-ai OpenAI and Anthropic profiles. Credentials and endpoint overrides come from the provider-standard DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL, OPENAI_API_KEY / OPENAI_BASE_URL, and ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL pairs in the boot's layered environment.

The Web/headless composition also registers only web_search. Search uses DeepSeek's Anthropic-compatible Messages endpoint, resolves the same DEEPSEEK_API_KEY reference for every call, and accepts the separate DEEPSEEK_SEARCH_BASE_URL endpoint override; each search is an auxiliary model request with its own latency and token cost. web_fetch remains disabled and the composition mounts no default fetch provider, so deployments that need arbitrary page retrieval must opt in through an overlay. The TUI composition does not mount Web tools by default. The deployment decision and its security boundary live in the default Web search Agent Note.

DSH_TOOLS_MODE selects the tool presentation mode for the whole Web/headless process: native (the schema default when unset), code (the run_code-only Code Mode wire), or both; any other value fails loud at boot through the dsh-tools config schema. It is a TEMPORARY seam — process-wide because Loader composition is static — and is removed once the web UI owns per-session tool-mode selection; the TUI surface ignores it (its config tree pins its own mode).

core-web.cordis.yml is an opt-in dsh web --config overlay that keeps the shipped Web host, browser, Workspace, persistence, and permission composition while reducing the default native model surface to owner-scoped persistent bash and str_replace_editor. The PTY backend and editor consume the existing Web sandbox and filesystem providers. An open persistent shell prevents changing that session's permission mode until the shell closes, so a shell created under wider access cannot survive a downgrade. DSH_TOOLS_MODE still controls native/Code Mode presentation for the resulting two-tool registry.

From a source checkout, start this minimal Web profile with:

pnpm run dsh web --config apps/cli/config/core-web.cordis.yml

Every dsh surface — TUI, Web, and headless — reports session telemetry by default (the row lives in the shared base.cordis.yml): every session-log event streams as OTLP/HTTP log records to https://harness-telemetry.deepseeksvc.com/v1/logs on a 10-second batch cadence. DSH_TELEMETRY_OTLP_URL points the exporter at a different collector; setting DSH_TELEMETRY_DISABLED to ANY non-empty value — including 0 or false — disables the row before it loads (a privacy switch prefers off-by-mistake over on-by-mistake). No redaction rule is mounted in this composition yet: exported records are the raw captured copy, including message text, tool arguments and results, and the session's working-directory path. The deployment rulings live in the web-telemetry-default-mount Agent Note.

Install (developer machine)

Symlink the source-running launcher onto your PATH; it resolves the checkout through its own real path, so code changes apply on the next launch with no build step:

ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh

Source launches run apps/cli/src/bin.ts through tsx's ESM-only hook (node --import tsx/esm), which transforms TypeScript and projects the root tsconfig paths map into module resolution. Node's native TypeScript modes are not used: Node 26 removed --experimental-transform-types, and strip-only mode rejects syntax the source graph relies on (vendored parameter properties, decorators, runtime enums/namespaces). The CJS hook stays off because the source graph is ESM-only and the CJS resolver adds ~0.4s of startup. bin/dsh pins TSX_TSCONFIG_PATH to the checkout's root tsconfig so resolution is cwd-independent, and the dsh-source-launch-smoke node-compat gate runs this exact launch vector on every supported Node line. tsx applies the paths map without checking dependency declarations, so declaration completeness rests on the static gates: the TUI configs resolve bare plugins through examples/package.json, the Web/headless cordis.yml through this package's dependencies, and verify-cordis-config requires every configured bare plugin to be declared, while allowing unrelated dependencies.

pnpm run dsh runs the same entry from the repo root and forwards arguments directly, for example pnpm run dsh -p "task". The built form (lib/bin.js, via pnpm run build) boots the same config under plain Node.