deepseek-cli-mode 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +258 -2
- package/bin/deepseek.js +279 -0
- package/docs/PARITY.md +108 -0
- package/index.js +13 -0
- package/package.json +50 -4
- package/runner/runner.js +1318 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.1 — security fixes
|
|
4
|
+
|
|
5
|
+
Fixes from an independent security audit (Cloudflare `security-audit` skill, quick profile).
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Repository-local runtime execution (Medium).** `resolveDshBin()` no longer considers
|
|
10
|
+
`process.cwd()/node_modules/@deepseek-ai/dsh` or a cwd-based `require` resolution. Running `deepseek` inside an
|
|
11
|
+
untrusted repository can no longer execute that repository's own Harness copy. A project-local runtime is available
|
|
12
|
+
only with the explicit `DSH_CLI_ALLOW_LOCAL=1` opt-in, and the resolved package's manifest must declare
|
|
13
|
+
`name: "@deepseek-ai/dsh"`.
|
|
14
|
+
- **One-shot plan-review auto-approval (Medium).** Non-interactive runs no longer select the first option of a
|
|
15
|
+
question. They now answer with a refusal, which keeps plan mode active instead of approving a plan no human
|
|
16
|
+
reviewed.
|
|
17
|
+
- **Terminal escape injection (Medium).** Model- and repository-derived text (assistant output, tool-call previews,
|
|
18
|
+
todo items, reasoning, question fields, error messages, and the one-shot answer) is stripped of C0/C1 control
|
|
19
|
+
characters before it reaches the terminal.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `DSH_CLI_ALLOW_LOCAL=1` is a new opt-in for executing a runtime installed inside the current project.
|
|
24
|
+
|
|
25
|
+
## 1.0.0 — first public release
|
|
26
|
+
|
|
27
|
+
- Interactive terminal mode for DeepSeek Harness: streaming, tools, approvals, plan mode, goals, compaction, skills,
|
|
28
|
+
jobs, todos, subagents, image/file attachments, session resume, and `/export`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek Harness CLI contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,259 @@
|
|
|
1
|
-
#
|
|
1
|
+
# deepseek-cli-mode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Terminal (CLI) mode for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)** — an interactive,
|
|
4
|
+
Claude Code-style coding agent that lives in your shell instead of a browser or an Electron window.
|
|
5
|
+
|
|
6
|
+
> **Community project.** This is an independent, unofficial project. It is **not** affiliated with, endorsed by, or
|
|
7
|
+
> sponsored by DeepSeek. "DeepSeek" and "DeepSeek Harness" are used only to describe what this tool connects to.
|
|
8
|
+
> Use it at your own risk and review the code before running it against your own API keys.
|
|
9
|
+
|
|
10
|
+
DeepSeek Harness ships two surfaces: the **Web** app and the **Desktop** app. This package adds the third one — a real
|
|
11
|
+
interactive **CLI** that boots the *same* Harness runtime (`dsh-base` + `dsh-headless`: agent loop, tools, sandbox,
|
|
12
|
+
session persistence, approvals) and keeps **one live Agent for the whole session**, so multi-turn context, tool state,
|
|
13
|
+
and the model's request prefix stay warm between messages.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
❯ explain what changed in this repo and run the tests
|
|
17
|
+
|
|
18
|
+
⌕ grep diff --stat
|
|
19
|
+
$ pwsh npm test
|
|
20
|
+
◇ read package.json
|
|
21
|
+
The working tree has three modified files …
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Node.js | 20 or newer |
|
|
31
|
+
| DeepSeek Harness runtime | `@deepseek-ai/dsh` **0.2.1** or newer, installed locally |
|
|
32
|
+
| Credentials | an API key saved by the Desktop/Web app, or `DEEPSEEK_API_KEY` in the environment |
|
|
33
|
+
|
|
34
|
+
The CLI is a front-end for the Harness runtime; it does not bundle the runtime itself. Install both:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
npm install -g @deepseek-ai/dsh
|
|
38
|
+
npm install -g deepseek-cli-mode
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The CLI reuses the key already stored in `$DSH_HOME/.credentials.yaml` by the GUI, so in most setups there is nothing
|
|
42
|
+
extra to configure.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
### Global install (recommended)
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
npm install -g deepseek-cli-mode
|
|
52
|
+
deepseek --version
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This installs three equivalent command names: **`deepseek`** (primary), `deepseek-cli-mode`, and `dsh-cli`.
|
|
56
|
+
|
|
57
|
+
### Run without installing
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
npx --yes deepseek-cli-mode
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### From a local checkout
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
cd deepseek-cli-mode # the folder containing package.json
|
|
67
|
+
npm install
|
|
68
|
+
npm link # optional: exposes the `deepseek` command
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
deepseek start an interactive session
|
|
77
|
+
deepseek "task text..." answer one task and exit (scriptable)
|
|
78
|
+
deepseek -r, --resume [id] resume a session (newest in this directory by default)
|
|
79
|
+
deepseek -c, --continue continue the newest session without prompting
|
|
80
|
+
deepseek --list list recent sessions in this directory
|
|
81
|
+
deepseek -y, --yes auto-approve tool actions that would ask
|
|
82
|
+
deepseek --model <id> override the model for this run
|
|
83
|
+
deepseek --reasoning <effort> override the reasoning effort (off|high|max)
|
|
84
|
+
deepseek --help show help
|
|
85
|
+
deepseek --version show the version
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Examples
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
# interactive — the normal way to work
|
|
92
|
+
deepseek
|
|
93
|
+
|
|
94
|
+
# scriptable one-shot; stdout carries only the final answer
|
|
95
|
+
deepseek "summarize the uncommitted changes" > summary.md
|
|
96
|
+
|
|
97
|
+
# continue the previous conversation in this folder
|
|
98
|
+
deepseek -c
|
|
99
|
+
|
|
100
|
+
# let it edit files and run commands unattended (approvals auto-accepted)
|
|
101
|
+
deepseek -y "fix the failing test in tests/auth.spec.ts"
|
|
102
|
+
|
|
103
|
+
# pipe a task in
|
|
104
|
+
Get-Content .\error.log | Select-Object -First 50 | deepseek "what causes this error?"
|
|
105
|
+
|
|
106
|
+
# attach a file or an image to the next message
|
|
107
|
+
deepseek "review @src/app.ts and tell me what is risky"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Inside a session
|
|
111
|
+
|
|
112
|
+
| Command | What it does |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `/help` | command list, including the live Harness command registry |
|
|
115
|
+
| `/new` | start a fresh session (new id, keeps the process warm) |
|
|
116
|
+
| `/sessions` | recent sessions in this directory (with a ▶ running marker) |
|
|
117
|
+
| `/resume <id>` | switch to another session |
|
|
118
|
+
| `/model [id]` | show or switch the model for the next message |
|
|
119
|
+
| `/reasoning [off\|high\|max]` | show or set the reasoning effort |
|
|
120
|
+
| `/permission [preset]` | switch the sandbox + approval preset |
|
|
121
|
+
| `/skills` | list the skills discovered in this workspace |
|
|
122
|
+
| `/jobs` | list this session's background jobs |
|
|
123
|
+
| `/todos` | show the current todo list |
|
|
124
|
+
| `/image <path>` | attach an image to the next message (or mention `@photo.png`) |
|
|
125
|
+
| `/export [file]` | write this run's transcript as markdown |
|
|
126
|
+
| `/thinking` | print the last turn's reasoning (collapsed by default) |
|
|
127
|
+
| `/session` | print the current session id (for `--resume`) |
|
|
128
|
+
| `/clear` | clear the screen |
|
|
129
|
+
| `/quit`, `/exit` | leave (Ctrl+C at an empty prompt also exits) |
|
|
130
|
+
|
|
131
|
+
Harness commands are shared with the Web and Desktop apps and run through the same registry:
|
|
132
|
+
|
|
133
|
+
| Command | What it does |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `/compact` | compact older conversation history |
|
|
136
|
+
| `/goal [objective\|clear\|edit\|pause\|resume]` | long-running goal mode |
|
|
137
|
+
| `/plan [off]` | plan mode (design first, approve, then execute) |
|
|
138
|
+
| `/feedback` | record feedback about the session |
|
|
139
|
+
|
|
140
|
+
Mention a file with `@path` to attach its text (up to 200 KB); mention an image (`@shot.png`) or use `/image` to attach
|
|
141
|
+
it durably through the same attachment store the GUI uses.
|
|
142
|
+
|
|
143
|
+
Ctrl+C **during a turn** cancels the turn; Ctrl+C **again** force-quits. Approval prompts (`Allow write …? [y/N]`) appear
|
|
144
|
+
when a tool wants to leave the workspace or act outside the sandbox policy.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
| Environment variable | Default | Meaning |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `DSH_HOME` | `~/.dsh` | Harness home (profiles, sessions, credentials) |
|
|
153
|
+
| `DEEPSEEK_API_KEY` | stored key | API key override |
|
|
154
|
+
| `DSH_CLI_DSH_BIN` | auto-detected | explicit path to `@deepseek-ai/dsh/lib/bin.js` |
|
|
155
|
+
| `DSH_CLI_ALLOW_LOCAL` | unset | `1` allows executing a dsh installed inside the current project; leave unset in untrusted repositories |
|
|
156
|
+
| `DSH_PERMISSION_MODE` | `workspace-write` | `read-only` \| `workspace-write` \| `danger-full-access` |
|
|
157
|
+
| `DSH_CLI_REASONING` | unset | `1` streams reasoning text instead of collapsing it |
|
|
158
|
+
| `DSH_CLI_YES` | unset | `1` auto-approves tool actions (same as `--yes`) |
|
|
159
|
+
| `NO_COLOR` | unset | disable ANSI styling |
|
|
160
|
+
|
|
161
|
+
Model and reasoning effort come from the Harness settings (the same selection the Desktop app shows). `--model`,
|
|
162
|
+
`--reasoning`, `/model`, and `/reasoning` override them for the session.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## How it works
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
deepseek (bin/deepseek.js)
|
|
170
|
+
├─ finds the local @deepseek-ai/dsh installation
|
|
171
|
+
├─ self-heals $DSH_HOME/profiles/dsh-cli (manifest + patch + runner.js)
|
|
172
|
+
└─ spawns: dsh --profile dsh-cli <your arguments>
|
|
173
|
+
└─ profile bundles: @deepseek-ai/dsh-base + @deepseek-ai/dsh-headless
|
|
174
|
+
└─ runner/runner.js (Cordis plugin, this package)
|
|
175
|
+
├─ agents.create() / agents.resume()
|
|
176
|
+
├─ agent/assistant-stream → token-level streaming
|
|
177
|
+
├─ session/event → tool calls, todos, usage
|
|
178
|
+
├─ user-questions/request → plan review, ask_user_question
|
|
179
|
+
└─ approval/request → y/N prompts
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Why a profile instead of a standalone program? The Harness is a Cordis plugin tree; running the CLI as a profile means
|
|
183
|
+
it gets the identical system prompt, tools, sandbox, permission policy, session store, and compaction behavior as the
|
|
184
|
+
GUI. This package only adds the terminal front-end.
|
|
185
|
+
|
|
186
|
+
Files this package writes (once, under `$DSH_HOME/profiles/dsh-cli/`):
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
package.json profile manifest (bundles)
|
|
190
|
+
cordis.yml empty root
|
|
191
|
+
cordis.patch.yml disables the one-shot headless driver, re-enables the
|
|
192
|
+
credential store, mounts ./runner.js
|
|
193
|
+
pnpm-workspace.yaml
|
|
194
|
+
runner.js copy of runner/runner.js (kept in sync)
|
|
195
|
+
.cli-version version stamp for the files above
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Your Web/Desktop profiles are untouched.
|
|
199
|
+
|
|
200
|
+
### Feature parity with the Web/Desktop apps
|
|
201
|
+
|
|
202
|
+
Roughly four fifths of the agent-facing product is shared by construction (tools, policies, sessions, plans, goals,
|
|
203
|
+
compaction, skills, subagents, workflows). A feature-by-feature comparison — including what stays GUI-only — is in
|
|
204
|
+
[docs/PARITY.md](docs/PARITY.md).
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Troubleshooting
|
|
209
|
+
|
|
210
|
+
**`deepseek: cannot find @deepseek-ai/dsh`** — install the runtime once: `npm i -g @deepseek-ai/dsh`, or set
|
|
211
|
+
`DSH_CLI_DSH_BIN` to the `lib/bin.js` path.
|
|
212
|
+
|
|
213
|
+
**`@deepseek-ai/dsh 0.1.x is too old for this CLI`** — upgrade with `npm i -g @deepseek-ai/dsh@latest`. This CLI needs
|
|
214
|
+
the `agent/assistant-stream` and `agentDefaultModel.saveSelection` APIs introduced in 0.2.1.
|
|
215
|
+
|
|
216
|
+
**`MISSING_CREDENTIAL` / no API key** — save a key in the Desktop/Web app (Models page), or
|
|
217
|
+
`$env:DEEPSEEK_API_KEY = "sk-…"` before starting `deepseek`.
|
|
218
|
+
|
|
219
|
+
**Approvals keep rejecting actions** — the default policy is `workspace-write`, so writes outside the working directory
|
|
220
|
+
ask first. Use `-y` for unattended runs, or start with `$env:DSH_PERMISSION_MODE = "danger-full-access"`.
|
|
221
|
+
|
|
222
|
+
**Command not found after `npm i -g`** — the npm global prefix may not be on `PATH`. Check `npm prefix -g` and add its
|
|
223
|
+
`bin` folder, or run through `npx deepseek-cli-mode`.
|
|
224
|
+
|
|
225
|
+
**`npx` with an absolute Windows tarball path does nothing** — pass a relative path or a `file:` spec:
|
|
226
|
+
`npx --yes .\deepseek-cli-mode-1.0.0.tgz`.
|
|
227
|
+
|
|
228
|
+
**Input looks garbled in Git Bash / MSYS** — use Windows Terminal, PowerShell 7, or `cmd.exe`; the runner uses Node's
|
|
229
|
+
readline and expects a normal Windows console.
|
|
230
|
+
|
|
231
|
+
**Startup shows `[llm-deepseek-api-key] model listing …`** — the adapter is discovering models; it is written to stderr,
|
|
232
|
+
so piped stdout stays clean.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```powershell
|
|
239
|
+
npm install
|
|
240
|
+
npm test # reports the CLI version
|
|
241
|
+
npm start # same as `deepseek`
|
|
242
|
+
node bin/deepseek.js --list # boot the profile without a model call
|
|
243
|
+
npm run pack:check # inspect the publish archive
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The runner is plain ESM with no build step. It is copied to the profile on every launch when its content changed, so
|
|
247
|
+
editing `runner/runner.js` and re-running `deepseek` picks the change up.
|
|
248
|
+
|
|
249
|
+
Compatibility note: the runner targets the 0.2.1 `@deepseek-ai/dsh` API surface (Cordis plugin entry, `agents.create`,
|
|
250
|
+
`agent/assistant-stream`, `session/event`, `user-questions/request`, `approval/request`,
|
|
251
|
+
`agentDefaultModel.saveSelection`). A future dsh release that changes those internals needs a matching runner update.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
[MIT](LICENSE) © 2026 DeepSeek Harness CLI contributors.
|
|
258
|
+
|
|
259
|
+
This project is independent and community-maintained; see the disclaimer at the top.
|
package/bin/deepseek.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// deepseek — the DeepSeek Harness terminal CLI launcher.
|
|
3
|
+
//
|
|
4
|
+
// DeepSeek Harness (dsh) ships Web and Desktop surfaces. This launcher adds the
|
|
5
|
+
// missing third surface: a Claude-Code-style terminal session. It resolves the
|
|
6
|
+
// locally installed `@deepseek-ai/dsh`, self-heals a dedicated `dsh-cli` profile
|
|
7
|
+
// under $DSH_HOME/profiles, and boots that profile with this package's runner.
|
|
8
|
+
//
|
|
9
|
+
// The profile keeps ONE live Agent for the whole session, so tools, approvals,
|
|
10
|
+
// session persistence, and the model's request prefix stay warm between turns.
|
|
11
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const root = resolve(here, "..");
|
|
20
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
21
|
+
|
|
22
|
+
const PROFILE = "dsh-cli";
|
|
23
|
+
const VERSION = readVersion(join(root, "package.json"));
|
|
24
|
+
const DSH_MIN = "0.2.1";
|
|
25
|
+
|
|
26
|
+
function readVersion(file) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(file, "utf8")).version ?? "0.0.0";
|
|
29
|
+
} catch {
|
|
30
|
+
return "0.0.0";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
|
|
36
|
+
// Help/version are answered here so they never pay for a profile boot.
|
|
37
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
38
|
+
console.log(VERSION);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
if (args.length === 0 ? false : args[0] === "--help" || args[0] === "-h") {
|
|
42
|
+
printHelp();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printHelp() {
|
|
47
|
+
console.log(`deepseek ${VERSION} — DeepSeek Harness in your terminal.
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
deepseek start an interactive session
|
|
51
|
+
deepseek "task text..." answer one task and exit (scriptable)
|
|
52
|
+
deepseek -r, --resume resume the newest session in this directory
|
|
53
|
+
deepseek -r <id> resume a specific session
|
|
54
|
+
deepseek -c, --continue continue the newest session without prompting
|
|
55
|
+
deepseek --list list recent sessions in this directory
|
|
56
|
+
deepseek -y, --yes auto-approve tool actions that ask
|
|
57
|
+
deepseek --model <id> override the model for this run
|
|
58
|
+
deepseek --reasoning <effort> override the reasoning effort (off|high|max)
|
|
59
|
+
deepseek --help this help
|
|
60
|
+
deepseek --version print the version
|
|
61
|
+
|
|
62
|
+
Inside a session:
|
|
63
|
+
/help commands /new fresh session
|
|
64
|
+
/sessions recent sessions /resume <id> switch session
|
|
65
|
+
/model [id] show/switch model /reasoning [e] show/set effort
|
|
66
|
+
/thinking last reasoning /session current session id
|
|
67
|
+
/clear clear the screen /quit, /exit leave
|
|
68
|
+
|
|
69
|
+
Environment:
|
|
70
|
+
DSH_HOME Harness home (default: ~/.dsh)
|
|
71
|
+
DSH_CLI_DSH_BIN explicit path to @deepseek-ai/dsh/lib/bin.js
|
|
72
|
+
DSH_PERMISSION_MODE workspace-write (default) | read-only | danger-full-access
|
|
73
|
+
DEEPSEEK_API_KEY API key; otherwise the key saved by the Desktop/Web app is used
|
|
74
|
+
|
|
75
|
+
Requires a local install of @deepseek-ai/dsh (npm i -g @deepseek-ai/dsh).`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// 1. Locate the dsh installation.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
function resolveDshBin() {
|
|
82
|
+
const candidates = [
|
|
83
|
+
process.env.DSH_CLI_DSH_BIN,
|
|
84
|
+
// Same global prefix as this node binary (npm default layout).
|
|
85
|
+
join(dirname(process.execPath), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
|
|
86
|
+
// Common global prefixes.
|
|
87
|
+
process.env.APPDATA ? join(process.env.APPDATA, "npm", "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js") : undefined,
|
|
88
|
+
process.env.NPM_CONFIG_PREFIX ? join(process.env.NPM_CONFIG_PREFIX, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js") : undefined,
|
|
89
|
+
process.env.npm_config_prefix ? join(process.env.npm_config_prefix, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js") : undefined,
|
|
90
|
+
join(homedir(), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
|
|
91
|
+
join(homedir(), ".npm-global", "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
|
|
92
|
+
].filter(Boolean);
|
|
93
|
+
// A project-local runtime is OPT-IN: running the CLI inside an untrusted
|
|
94
|
+
// repository must never execute repository-supplied code implicitly.
|
|
95
|
+
if (process.env.DSH_CLI_ALLOW_LOCAL === "1") {
|
|
96
|
+
candidates.push(join(process.cwd(), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"));
|
|
97
|
+
}
|
|
98
|
+
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
99
|
+
// Resolve only from this package's own module graph (never from cwd).
|
|
100
|
+
try {
|
|
101
|
+
return requireFromHere.resolve("@deepseek-ai/dsh/lib/bin.js");
|
|
102
|
+
} catch {}
|
|
103
|
+
// Last resort: ask npm where its global root is (covers custom prefixes).
|
|
104
|
+
try {
|
|
105
|
+
const npmCli = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
106
|
+
if (existsSync(npmCli)) {
|
|
107
|
+
const result = spawnSync(process.execPath, [npmCli, "root", "-g"], { encoding: "utf8", timeout: 20000 });
|
|
108
|
+
const dir = result.stdout?.trim();
|
|
109
|
+
if (dir) {
|
|
110
|
+
const candidate = join(dir, "@deepseek-ai", "dsh", "lib", "bin.js");
|
|
111
|
+
if (existsSync(candidate)) return candidate;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// 2. Self-heal the dedicated profile.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
const PROFILE_MANIFEST = JSON.stringify(
|
|
122
|
+
{
|
|
123
|
+
name: "dsh-profile-dsh-cli",
|
|
124
|
+
private: true,
|
|
125
|
+
type: "module",
|
|
126
|
+
dependencies: {},
|
|
127
|
+
dsh: { profile: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"] } },
|
|
128
|
+
},
|
|
129
|
+
null,
|
|
130
|
+
2
|
|
131
|
+
) + "\n";
|
|
132
|
+
|
|
133
|
+
const PROFILE_ROOT =
|
|
134
|
+
"# dsh profile root — an empty entry list. The tree is composed as patches:\n" +
|
|
135
|
+
"# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml.\n[]\n";
|
|
136
|
+
|
|
137
|
+
const PROFILE_PATCH = `# dsh-cli — interactive terminal profile for the DeepSeek Harness.
|
|
138
|
+
# Bundles: dsh-base (core machinery) + dsh-headless (coding persona, tools,
|
|
139
|
+
# sandbox policy). This layer disables the one-shot headless driver, restores
|
|
140
|
+
# the managed credential store the headless bundle disables, and mounts the
|
|
141
|
+
# interactive runner from ./runner.js.
|
|
142
|
+
- id: headless-startup
|
|
143
|
+
disabled: true
|
|
144
|
+
|
|
145
|
+
- id: headless-runner
|
|
146
|
+
disabled: true
|
|
147
|
+
|
|
148
|
+
# The CLI is interactive: use the API key saved by the Desktop/Web app.
|
|
149
|
+
- id: credentials
|
|
150
|
+
disabled: false
|
|
151
|
+
- id: authorization
|
|
152
|
+
disabled: false
|
|
153
|
+
- id: deepseek-account
|
|
154
|
+
disabled: false
|
|
155
|
+
|
|
156
|
+
- insert:
|
|
157
|
+
- id: cli-runner
|
|
158
|
+
name: './runner.js'
|
|
159
|
+
- id: tool-ask-user
|
|
160
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
const PNPM_WORKSPACE = "packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n";
|
|
164
|
+
|
|
165
|
+
function ensureProfile() {
|
|
166
|
+
const dshHome =
|
|
167
|
+
process.env.DSH_HOME && process.env.DSH_HOME.trim() !== ""
|
|
168
|
+
? process.env.DSH_HOME
|
|
169
|
+
: join(homedir(), ".dsh");
|
|
170
|
+
const profileDir = join(dshHome, "profiles", PROFILE);
|
|
171
|
+
mkdirSync(profileDir, { recursive: true });
|
|
172
|
+
|
|
173
|
+
const stamp = join(profileDir, ".cli-version");
|
|
174
|
+
let stampVersion = "";
|
|
175
|
+
try {
|
|
176
|
+
stampVersion = readFileSync(stamp, "utf8").trim();
|
|
177
|
+
} catch {}
|
|
178
|
+
|
|
179
|
+
const staticFiles = {
|
|
180
|
+
"package.json": PROFILE_MANIFEST,
|
|
181
|
+
"cordis.yml": PROFILE_ROOT,
|
|
182
|
+
"cordis.patch.yml": PROFILE_PATCH,
|
|
183
|
+
"pnpm-workspace.yaml": PNPM_WORKSPACE,
|
|
184
|
+
};
|
|
185
|
+
for (const [name, content] of Object.entries(staticFiles)) {
|
|
186
|
+
const target = join(profileDir, name);
|
|
187
|
+
if (!existsSync(target) || stampVersion !== VERSION) writeFileSync(target, content);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Keep the runner exactly in sync with this package.
|
|
191
|
+
const runnerSrc = join(root, "runner", "runner.js");
|
|
192
|
+
const runnerDst = join(profileDir, "runner.js");
|
|
193
|
+
const src = readFileSync(runnerSrc, "utf8");
|
|
194
|
+
if (!existsSync(runnerDst) || readFileSync(runnerDst, "utf8") !== src) writeFileSync(runnerDst, src);
|
|
195
|
+
|
|
196
|
+
writeFileSync(stamp, VERSION + "\n");
|
|
197
|
+
return profileDir;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// 3. Boot.
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
const dshBin = resolveDshBin();
|
|
204
|
+
if (dshBin === undefined) {
|
|
205
|
+
console.error(
|
|
206
|
+
"deepseek: cannot find @deepseek-ai/dsh.\n" +
|
|
207
|
+
" Install it once: npm i -g @deepseek-ai/dsh\n" +
|
|
208
|
+
" Or point DSH_CLI_DSH_BIN at .../@deepseek-ai/dsh/lib/bin.js"
|
|
209
|
+
);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Verify the resolved package really is the Harness runtime before executing
|
|
214
|
+
// it. This blocks a path that merely exists at a trusted prefix but is not dsh.
|
|
215
|
+
let dshManifest;
|
|
216
|
+
try {
|
|
217
|
+
dshManifest = JSON.parse(readFileSync(join(dshBin, "..", "..", "package.json"), "utf8"));
|
|
218
|
+
} catch {}
|
|
219
|
+
if (dshManifest?.name !== "@deepseek-ai/dsh") {
|
|
220
|
+
console.error(
|
|
221
|
+
`deepseek: resolved ${dshBin} is not @deepseek-ai/dsh (found ${JSON.stringify(dshManifest?.name ?? "no manifest")}).\n` +
|
|
222
|
+
" Point DSH_CLI_DSH_BIN at a real @deepseek-ai/dsh/lib/bin.js."
|
|
223
|
+
);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const dshVersion = String(dshManifest.version ?? "0.0.0");
|
|
228
|
+
if (compareVersions(dshVersion, DSH_MIN) < 0) {
|
|
229
|
+
console.error(
|
|
230
|
+
`deepseek: @deepseek-ai/dsh ${dshVersion} is too old for this CLI (needs >= ${DSH_MIN}).\n` +
|
|
231
|
+
" Upgrade with: npm i -g @deepseek-ai/dsh@latest"
|
|
232
|
+
);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function compareVersions(a, b) {
|
|
237
|
+
const parse = (v) => String(v).split(/[.+-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
|
|
238
|
+
const [x, y, z] = parse(a);
|
|
239
|
+
const [p, q, r] = parse(b);
|
|
240
|
+
if (x !== p) return x < p ? -1 : 1;
|
|
241
|
+
if (y !== q) return y < q ? -1 : 1;
|
|
242
|
+
if (z !== r) return z < r ? -1 : 1;
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
ensureProfile();
|
|
247
|
+
|
|
248
|
+
const child = spawn(process.execPath, [dshBin, "--profile", PROFILE, ...args], {
|
|
249
|
+
stdio: "inherit",
|
|
250
|
+
env: { ...process.env, DSH_CLI_VERSION: VERSION },
|
|
251
|
+
windowsHide: false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// The child shares this console, so it receives the user's Ctrl+C directly and
|
|
255
|
+
// tears down gracefully (flush + dispose + exit). This process must not die
|
|
256
|
+
// first, or the child's session flush is lost: only hard-kill on a second
|
|
257
|
+
// Ctrl+C or after a grace period.
|
|
258
|
+
let sigints = 0;
|
|
259
|
+
process.on("SIGINT", () => {
|
|
260
|
+
sigints += 1;
|
|
261
|
+
if (sigints >= 2) {
|
|
262
|
+
if (child.exitCode === null && !child.killed) child.kill();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
if (child.exitCode === null && !child.killed) child.kill();
|
|
267
|
+
}, 15000).unref();
|
|
268
|
+
});
|
|
269
|
+
process.on("SIGTERM", () => {
|
|
270
|
+
if (child.exitCode === null && !child.killed) child.kill();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
child.on("error", (error) => {
|
|
274
|
+
console.error(`deepseek: failed to start dsh: ${error.message}`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
});
|
|
277
|
+
child.on("exit", (code, signal) => {
|
|
278
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
279
|
+
});
|