deepseek-cli-mode 1.0.1 → 1.1.0

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 — Claude-Code-style terminal UI
4
+
5
+ - New welcome panel: rounded box with version, model, cwd, and first-run hints.
6
+ - Prompt is now `> `; each turn ends with a dim status line (`model · tokens · seconds`).
7
+ - Tool activity renders as `⏺ Tool(args)` followed by a `⎿ result` summary line.
8
+ - Animated `✻ Thinking… (Ns · ctrl+c to interrupt)` spinner while the model works.
9
+ - Permission prompts use the numbered `Do you want to allow …? 1. Yes / 2. No` dialog.
10
+ - Todo list uses `☒`/`☐` checkboxes and prints only changed items after the first render.
11
+ - New `/status` command (session, model, cwd, mode, token pressure); `/session` kept.
12
+ - Security: all new rendering paths pass model/repository text through the existing
13
+ control-character sanitizer; no new process, network, or filesystem surfaces were added.
14
+
15
+ ## 1.0.2 — runtime discovery fixes
16
+
17
+ - `resolveDshBin()` now also looks for `@deepseek-ai/dsh` next to this package (same `node_modules`) and in the
18
+ `node_modules` of every directory on `PATH`. This fixes "cannot find @deepseek-ai/dsh" when dsh is installed under a
19
+ custom npm prefix (including Windows drive-relative prefixes such as `E:\root\.npm-global`).
20
+ - The not-found message now explains the Windows drive-relative `prefix=/root/.npm-global` pitfall and points at
21
+ `npm root -g`.
22
+
3
23
  ## 1.0.1 — security fixes
4
24
 
5
25
  Fixes from an independent security audit (Cloudflare `security-audit` skill, quick profile).
package/README.md CHANGED
@@ -12,6 +12,31 @@ interactive **CLI** that boots the *same* Harness runtime (`dsh-base` + `dsh-hea
12
12
  session persistence, approvals) and keeps **one live Agent for the whole session**, so multi-turn context, tool state,
13
13
  and the model's request prefix stay warm between messages.
14
14
 
15
+ The terminal UI follows the Claude Code layout:
16
+
17
+ ```
18
+ ╭──────────────────────────────────────────────────────╮
19
+ │ ✻ DeepSeek CLI 1.1.0 │
20
+ │ │
21
+ │ /help for help · /status for your current setup │
22
+ │ model: deepseek-official/deepseek-chat │
23
+ │ cwd: C:\work\my-project │
24
+ ╰──────────────────────────────────────────────────────╯
25
+
26
+ > explain what changed and run the tests
27
+
28
+ ⏺ glob(**/*.ts)
29
+ ⎿ src/app.ts src/auth.ts tests/auth.spec.ts
30
+ ✻ Thinking… (2s · ctrl+c to interrupt)
31
+
32
+ ⏺ pwsh(npm test)
33
+ ⎿ Tests: 42 passed, 42 total
34
+
35
+ The auth change is safe; all tests pass.
36
+
37
+ ✻ deepseek-official/deepseek-chat · 8.4k tokens · 12.1s
38
+ ```
39
+
15
40
  ```
16
41
  ❯ explain what changed in this repo and run the tests
17
42
 
@@ -125,6 +150,7 @@ deepseek "review @src/app.ts and tell me what is risky"
125
150
  | `/export [file]` | write this run's transcript as markdown |
126
151
  | `/thinking` | print the last turn's reasoning (collapsed by default) |
127
152
  | `/session` | print the current session id (for `--resume`) |
153
+ | `/status` | session, model, cwd, mode, and token pressure |
128
154
  | `/clear` | clear the screen |
129
155
  | `/quit`, `/exit` | leave (Ctrl+C at an empty prompt also exits) |
130
156
 
package/bin/deepseek.js CHANGED
@@ -12,7 +12,7 @@ import { spawn, spawnSync } from "node:child_process";
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
13
  import { createRequire } from "node:module";
14
14
  import { homedir } from "node:os";
15
- import { dirname, join, resolve } from "node:path";
15
+ import { dirname, delimiter, join, resolve } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
 
18
18
  const here = dirname(fileURLToPath(import.meta.url));
@@ -81,6 +81,9 @@ Requires a local install of @deepseek-ai/dsh (npm i -g @deepseek-ai/dsh).`);
81
81
  function resolveDshBin() {
82
82
  const candidates = [
83
83
  process.env.DSH_CLI_DSH_BIN,
84
+ // Sibling install: dsh next to this package in the same node_modules.
85
+ // This is the normal layout when both were installed with one npm prefix.
86
+ join(root, "..", "@deepseek-ai", "dsh", "lib", "bin.js"),
84
87
  // Same global prefix as this node binary (npm default layout).
85
88
  join(dirname(process.execPath), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
86
89
  // Common global prefixes.
@@ -96,6 +99,14 @@ function resolveDshBin() {
96
99
  candidates.push(join(process.cwd(), "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"));
97
100
  }
98
101
  for (const candidate of candidates) if (existsSync(candidate)) return candidate;
102
+ // npm's own bin directories are on PATH; check their sibling node_modules.
103
+ // This covers custom prefixes (and Windows drive-relative prefixes) that the
104
+ // fixed candidates above cannot know about.
105
+ for (const dir of String(process.env.PATH ?? "").split(delimiter)) {
106
+ if (dir === "") continue;
107
+ const candidate = join(dir, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js");
108
+ if (existsSync(candidate)) return candidate;
109
+ }
99
110
  // Resolve only from this package's own module graph (never from cwd).
100
111
  try {
101
112
  return requireFromHere.resolve("@deepseek-ai/dsh/lib/bin.js");
@@ -204,8 +215,10 @@ const dshBin = resolveDshBin();
204
215
  if (dshBin === undefined) {
205
216
  console.error(
206
217
  "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"
218
+ " Install it once in the same npm prefix as this CLI: npm i -g @deepseek-ai/dsh\n" +
219
+ " Or point DSH_CLI_DSH_BIN at .../@deepseek-ai/dsh/lib/bin.js\n" +
220
+ " If it is installed but not found, check `npm root -g` (a POSIX-style npm prefix such as\n" +
221
+ " /root/.npm-global resolves per drive on Windows; use C:\\... instead)."
209
222
  );
210
223
  process.exit(1);
211
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-cli-mode",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Terminal (CLI) mode for DeepSeek Harness — an interactive, Claude Code-style coding agent for your shell.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/runner/runner.js CHANGED
@@ -210,6 +210,9 @@ function createRenderer({ write, capture }) {
210
210
  get lastTodos() {
211
211
  return lastTodos;
212
212
  },
213
+ get turnUsage() {
214
+ return { ...turnUsage };
215
+ },
213
216
  resetTurn() {
214
217
  streamedThisTurn = false;
215
218
  reasoning = "";
@@ -246,15 +249,24 @@ function createRenderer({ write, capture }) {
246
249
  case "tool/call": {
247
250
  if (data.name === "todo_write") break; // the checklist below is the presentation
248
251
  endLine();
249
- const icon = TOOL_ICONS[data.name] ?? "⏺";
250
252
  const preview = previewToolCall(data.name, data.arguments);
251
- write(paint(ansi.dim, paint(ansi.italic, ` ${icon} ${data.name}${preview ? " " + preview : ""}`)) + "\n");
253
+ write(
254
+ paint(ansi.cyan, "⏺ ") +
255
+ paint(ansi.bold, sanitize(data.name)) +
256
+ paint(ansi.dim, `(${preview})`) +
257
+ "\n"
258
+ );
252
259
  break;
253
260
  }
254
261
  case "tool/result": {
255
- if (data.error) {
256
- write(paint(ansi.dim, paint(ansi.italic, ` ✗ ${sanitize(data.error.name)}: ${sanitize(data.error.code)}`)) + "\n");
262
+ let summary;
263
+ if (data.error) summary = `Error: ${sanitize(data.error.name)}${data.error.code ? ` (${sanitize(data.error.code)})` : ""}`;
264
+ else {
265
+ const content = data.message?.content ?? [];
266
+ const text = content.filter((b) => b.type === "text").map((b) => b.text).join(" ").replace(/\s+/g, " ").trim();
267
+ summary = text === "" ? "Done" : truncate(text, 76);
257
268
  }
269
+ write(paint(ansi.dim, ` ⎿ ${sanitize(summary)}`) + "\n");
258
270
  break;
259
271
  }
260
272
  case "assistant/message": {
@@ -280,15 +292,15 @@ function createRenderer({ write, capture }) {
280
292
  endLine();
281
293
  flushText();
282
294
  const done = todos.filter((todo) => todo.status === "completed").length;
283
- write(paint(ansi.dim, ` ☑ todos ${done}/${todos.length}`) + "\n");
295
+ write(paint(ansi.dim, ` ☒ todos ${done}/${todos.length}`) + "\n");
284
296
  // Print the whole list once; later writes show only the items that moved.
285
297
  const shown = firstRender ? todos : todos.filter((todo) => previous.get(todo.content) !== todo.status);
286
298
  for (const todo of shown) {
287
299
  const mark =
288
300
  todo.status === "completed"
289
- ? paint(ansi.green, "✓")
301
+ ? paint(ansi.green, "☒")
290
302
  : todo.status === "in_progress"
291
- ? paint(ansi.yellow, "▸")
303
+ ? paint(ansi.yellow, "☐")
292
304
  : paint(ansi.dim, "☐");
293
305
  const content = todo.status === "completed" ? paint(ansi.dim, sanitize(todo.content)) : sanitize(todo.content);
294
306
  write(` ${mark} ${content}\n`);
@@ -442,32 +454,43 @@ function printSessionList(sessions, write = out) {
442
454
  write(paint(ansi.dim, " no sessions in this directory yet\n"));
443
455
  return;
444
456
  }
445
- write(` Recent sessions in ${process.cwd()}:\n`);
457
+ write(` Recent sessions in ${sanitize(process.cwd())}:\n`);
446
458
  sessions.slice(0, 10).forEach((session, index) => {
447
459
  const running = session.openTurn ? paint(ansi.yellow, " ▶ running") : "";
448
- write(` ${paint(ansi.cyan, String(index + 1))}) ${paint(ansi.dim, session.id)} (${paint(ansi.dim, timeAgo(session.mtime))})${running}\n`);
460
+ write(` ${paint(ansi.cyan, String(index + 1))}) ${paint(ansi.dim, sanitize(session.id))} (${paint(ansi.dim, timeAgo(session.mtime))})${running}\n`);
449
461
  });
450
462
  }
451
463
 
452
464
  // ---------------------------------------------------------------------------
453
- // Help / banner
465
+ // Help / welcome
454
466
  // ---------------------------------------------------------------------------
467
+ /** Draw a rounded box, truncating content to the terminal width. */
468
+ function box(lines, minWidth = 56) {
469
+ const cols = Math.max(40, Math.min(process.stdout.columns || 88, 100));
470
+ const width = Math.min(cols - 2, Math.max(minWidth, ...lines.map((line) => line.length + 4)));
471
+ const inner = width - 4;
472
+ const outLines = [];
473
+ outLines.push(paint(ansi.dim, "╭" + "─".repeat(width - 2) + "╮"));
474
+ for (const line of lines) {
475
+ let text = line;
476
+ if (text.length > inner) text = text.slice(0, inner - 1) + "…";
477
+ outLines.push(paint(ansi.dim, "│ ") + text.padEnd(inner) + paint(ansi.dim, " │"));
478
+ }
479
+ outLines.push(paint(ansi.dim, "╰" + "─".repeat(width - 2) + "╯"));
480
+ for (const line of outLines) out(line + "\n");
481
+ }
482
+
455
483
  function printBanner(model) {
456
- const art = [
457
- " ooo",
458
- ' " """',
459
- ' " "',
460
- ' ___:____ " "|',
461
- " ,' \\. \\//",
462
- " | O \\___/ |",
463
- "~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~",
464
- ];
484
+ const modelLine = sanitize(`${model.provider}/${model.model}${model.reasoningEffort ? ` · ${model.reasoningEffort}` : ""}`);
465
485
  out("\n");
466
- for (const line of art) out(paint(ansi.blue, line) + "\n");
467
- out(`${paint(ansi.bold, "deepseek")} ${paint(ansi.dim, VERSION)} — DeepSeek Harness in your terminal\n`);
468
- out(paint(ansi.dim, `model: ${model.provider}/${model.model}${model.reasoningEffort ? ` · ${model.reasoningEffort}` : ""}`) + "\n");
469
- out(paint(ansi.dim, `cwd: ${process.cwd()}`) + "\n");
470
- out(paint(ansi.dim, "type /help for commands, Ctrl+C to interrupt or exit") + "\n\n");
486
+ box([
487
+ `${paint(ansi.cyan, "✻")} ${paint(ansi.bold, "DeepSeek CLI")} ${paint(ansi.dim, VERSION)}`,
488
+ "",
489
+ paint(ansi.dim, " /help for help · /status for your current setup"),
490
+ paint(ansi.dim, ` model: ${modelLine}`),
491
+ paint(ansi.dim, ` cwd: ${sanitize(process.cwd())}`),
492
+ ]);
493
+ out(paint(ansi.dim, " Type a task, or /help. Ctrl+C interrupts; twice exits.") + "\n\n");
471
494
  }
472
495
 
473
496
  function printHelp(write = out) {
@@ -498,6 +521,7 @@ Interactive commands:
498
521
  /export [file] write this run's transcript as markdown
499
522
  /thinking show the last turn's reasoning
500
523
  /session print the current session id (for --resume)
524
+ /status session, model, cwd, token pressure
501
525
  /clear clear the screen
502
526
  /quit, /exit leave (also Ctrl+C at an empty prompt)
503
527
 
@@ -576,6 +600,7 @@ async function run(ctx, options, exit) {
576
600
  const planMode = ctx.get("planMode");
577
601
  const permissionPresets = ctx.get("permissionPresets");
578
602
  const attachments = ctx.get("attachments");
603
+ const tokenMeter = ctx.get("tokenMeter");
579
604
 
580
605
  if (options.list) {
581
606
  printSessionList(listSessions());
@@ -783,24 +808,63 @@ async function run(ctx, options, exit) {
783
808
  // ---- agent lifecycle ----------------------------------------------------
784
809
  const makeAgentOptions = () => ({ ...selectionRef.current });
785
810
 
811
+ // ---- Claude-style activity indicator ------------------------------------
812
+ let spinnerTimer = null;
813
+ let spinnerIndex = 0;
814
+ let spinnerActive = false;
815
+ const SPINNER = ["✻", "✳", "✶", "✳"];
816
+
817
+ function startSpinner(label = "Thinking") {
818
+ if (!tty || options.mode === "oneshot" || spinnerActive) return;
819
+ spinnerActive = true;
820
+ spinnerIndex = 0;
821
+ const started = Date.now();
822
+ out("\x1b[?25l");
823
+ const tick = () => {
824
+ const glyph = SPINNER[spinnerIndex++ % SPINNER.length];
825
+ const seconds = Math.floor((Date.now() - started) / 1000);
826
+ out(`\r\x1b[2K${paint(ansi.dim, `${glyph} ${label}… (${seconds}s · ctrl+c to interrupt)`)}`);
827
+ };
828
+ tick();
829
+ spinnerTimer = setInterval(tick, 120);
830
+ }
831
+
832
+ function stopSpinner() {
833
+ if (!spinnerActive) return;
834
+ spinnerActive = false;
835
+ if (spinnerTimer !== null) {
836
+ clearInterval(spinnerTimer);
837
+ spinnerTimer = null;
838
+ }
839
+ out("\r\x1b[2K\x1b[?25h");
840
+ }
841
+
786
842
  const wire = (agent) => {
787
843
  agent.ctx.on("session/event", (session, event) => {
788
- if (session === agent.session) renderer.event(event);
844
+ if (session === agent.session) {
845
+ stopSpinner();
846
+ renderer.event(event);
847
+ }
789
848
  });
790
849
  agent.ctx.on("agent/assistant-stream", (payload) => {
791
- if (payload.agent === agent) renderer.frame(payload.frame);
850
+ if (payload.agent === agent) {
851
+ stopSpinner();
852
+ renderer.frame(payload.frame);
853
+ }
792
854
  });
793
855
  agent.ctx.on("user-questions/request", async (request) => answerQuestions(request));
794
856
  agent.ctx.on("approval/request", async (request) => {
795
- const toolName = request.toolName ?? "tool";
857
+ const toolName = sanitize(request.toolName ?? "tool");
796
858
  if (autoYes) return "allowed-once";
797
- if (request.reason) {
798
- out(paint(ansi.dim, paint(ansi.italic, ` ⚠ ${toolName} needs approval: ${request.reason}`)) + "\n");
799
- }
859
+ stopSpinner();
800
860
  if (!interactive || !process.stdin.isTTY) return "rejected";
801
- const answer = await ask(paint(ansi.yellow, ` ⚠ Allow ${paint(ansi.cyan, toolName)}? `) + paint(ansi.dim, "[y/N] "));
802
- if (answer === null) return "rejected";
803
- return /^y(es)?$/i.test(answer.trim()) ? "allowed-once" : "rejected";
861
+ out("\n");
862
+ if (request.reason) out(paint(ansi.dim, ` ${sanitize(request.reason)}`) + "\n");
863
+ out(` ${paint(ansi.bold, "Do you want to allow")} ${paint(ansi.cyan, toolName)}${paint(ansi.bold, "?")}\n`);
864
+ out(` ${paint(ansi.cyan, "1")}. Yes\n ${paint(ansi.cyan, "2")}. No\n`);
865
+ const answer = (await ask(paint(ansi.green, " ❯ "))) ?? "";
866
+ const choice = answer.trim().toLowerCase();
867
+ return choice === "1" || choice === "y" || choice === "yes" ? "allowed-once" : "rejected";
804
868
  });
805
869
  };
806
870
 
@@ -836,17 +900,26 @@ async function run(ctx, options, exit) {
836
900
  busy = true;
837
901
  cancelSent = false;
838
902
  renderer.resetTurn();
903
+ const startedAt = Date.now();
839
904
  setPaused(true);
905
+ startSpinner("Thinking");
840
906
  try {
841
907
  agent.followup(createUserMessage({ content, source: { kind: "user" } }));
842
908
  await agent.whenIdle();
843
909
  await persistence.flush();
844
910
  } finally {
911
+ stopSpinner();
845
912
  busy = false;
846
913
  setPaused(false);
847
914
  if (renderer.lastAssistantText && renderer.lastAssistantText !== previousAnswer) {
848
915
  transcript.push({ role: "assistant", text: renderer.lastAssistantText });
849
916
  }
917
+ if (interactive && tty) {
918
+ const usage = renderer.turnUsage;
919
+ const seconds = ((Date.now() - startedAt) / 1000).toFixed(1);
920
+ const tokens = usage.input + usage.output;
921
+ out(paint(ansi.dim, ` ✻ ${sanitize(modelDesc())}${tokens > 0 ? ` · ${formatTokens(tokens)} tokens` : ""} · ${seconds}s`) + "\n\n");
922
+ }
850
923
  }
851
924
  };
852
925
 
@@ -1057,7 +1130,7 @@ async function run(ctx, options, exit) {
1057
1130
  try {
1058
1131
  agent = resumeTarget ? await startAgent(undefined, resumeTarget) : await startAgent(SessionId(`session-${randomUUID()}`), undefined);
1059
1132
  } catch (error) {
1060
- err(`deepseek: cannot start a session: ${error instanceof Error ? error.message : String(error)}\n`);
1133
+ err(`deepseek: cannot start a session: ${sanitize(describeError(error))}\n`);
1061
1134
  await shutdown(1);
1062
1135
  return;
1063
1136
  }
@@ -1070,14 +1143,14 @@ async function run(ctx, options, exit) {
1070
1143
 
1071
1144
  printBanner(selectionRef.current);
1072
1145
  if (resumeTarget) {
1073
- out(paint(ansi.green, `▶ resumed `) + paint(ansi.dim, resumeTarget) + "\n");
1146
+ out(paint(ansi.green, `▶ resumed `) + paint(ansi.dim, sanitize(resumeTarget)) + "\n");
1074
1147
  out(paint(ansi.dim, " (history is in the session log; continue the conversation below)") + "\n\n");
1075
1148
  } else {
1076
- out(paint(ansi.dim, `session: ${agent.session.id} (resume with: deepseek --resume ${agent.session.id})`) + "\n\n");
1149
+ out(paint(ansi.dim, `session: ${sanitize(agent.session.id)} (resume with: deepseek --resume ${sanitize(agent.session.id)})`) + "\n\n");
1077
1150
  }
1078
1151
 
1079
1152
  while (!quitting) {
1080
- const line = await ask(paint(ansi.green, "❯ "));
1153
+ const line = await ask(paint(ansi.bold, "> "));
1081
1154
  if (line === null) break;
1082
1155
  const text = line.trim();
1083
1156
  if (text === "") continue;
@@ -1098,9 +1171,9 @@ async function run(ctx, options, exit) {
1098
1171
  out(paint(ansi.dim, " ↻ new session\n"));
1099
1172
  try {
1100
1173
  agent = await startAgent(SessionId(`session-${randomUUID()}`), undefined);
1101
- out(paint(ansi.dim, ` session: ${agent.session.id}\n`));
1174
+ out(paint(ansi.dim, ` session: ${sanitize(agent.session.id)}\n`));
1102
1175
  } catch (error) {
1103
- out(paint(ansi.red, ` ✘ ${error instanceof Error ? error.message : String(error)}\n`));
1176
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1104
1177
  }
1105
1178
  continue;
1106
1179
  }
@@ -1116,13 +1189,27 @@ async function run(ctx, options, exit) {
1116
1189
  out(paint(ansi.dim, ` ↻ resuming ${argument}\n`));
1117
1190
  agent = await startAgent(undefined, argument);
1118
1191
  } catch (error) {
1119
- out(paint(ansi.red, ` ✘ ${error instanceof Error ? error.message : String(error)}\n`));
1192
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1120
1193
  }
1121
1194
  continue;
1122
1195
  }
1123
1196
  case "/session":
1124
- out(paint(ansi.dim, ` ${agent.session.id}\n`));
1197
+ out(paint(ansi.dim, ` ${sanitize(agent.session.id)}\n`));
1125
1198
  continue;
1199
+ case "/status": {
1200
+ out(paint(ansi.bold, " Status") + "\n");
1201
+ out(` session: ${sanitize(agent.session.id)}\n`);
1202
+ out(` model: ${sanitize(modelDesc())}\n`);
1203
+ out(` cwd: ${sanitize(process.cwd())}\n`);
1204
+ out(` mode: ${interactive ? "interactive" : "one-shot"}${autoYes ? " · auto-approve" : ""}\n`);
1205
+ try {
1206
+ if (tokenMeter !== undefined) {
1207
+ const measurement = tokenMeter.measure(agent.session);
1208
+ out(` tokens: ${measurement.totalTokens} (surface ${measurement.surfaceTokens})\n`);
1209
+ }
1210
+ } catch {}
1211
+ continue;
1212
+ }
1126
1213
  case "/model": {
1127
1214
  if (argument === "") {
1128
1215
  out(paint(ansi.dim, ` model: ${modelDesc()}\n`));
@@ -1134,7 +1221,7 @@ async function run(ctx, options, exit) {
1134
1221
  selectionRef.current = next;
1135
1222
  out(paint(ansi.green, ` ✓ model set: ${next.provider}/${next.model} (next message)\n`));
1136
1223
  } catch (error) {
1137
- out(paint(ansi.red, ` ✘ ${error instanceof Error ? error.message : String(error)}\n`));
1224
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1138
1225
  }
1139
1226
  continue;
1140
1227
  }
@@ -1149,7 +1236,7 @@ async function run(ctx, options, exit) {
1149
1236
  selectionRef.current = next;
1150
1237
  out(paint(ansi.green, ` ✓ reasoning set: ${argument} (next message)\n`));
1151
1238
  } catch (error) {
1152
- out(paint(ansi.red, ` ✘ ${error instanceof Error ? error.message : String(error)}\n`));
1239
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1153
1240
  }
1154
1241
  continue;
1155
1242
  }
@@ -1281,7 +1368,7 @@ async function run(ctx, options, exit) {
1281
1368
  }
1282
1369
  }
1283
1370
 
1284
- out("\n" + paint(ansi.green, "❯ ") + sanitize(text) + "\n\n");
1371
+ out("\n" + paint(ansi.bold, "> ") + sanitize(text) + "\n\n");
1285
1372
  await runTurn(agent, text);
1286
1373
  }
1287
1374
 
@@ -1310,7 +1397,7 @@ export function apply(ctx) {
1310
1397
  }
1311
1398
  run(ctx, options, exit).catch((error) => {
1312
1399
  const detail = process.env.DSH_CLI_DEBUG === "1" && error instanceof Error ? `\n${error.stack}` : "";
1313
- process.stderr.write(`deepseek: ${error instanceof Error ? error.message : String(error)}${detail}\n`);
1400
+ process.stderr.write(`deepseek: ${sanitize(describeError(error))}${detail}\n`);
1314
1401
  try {
1315
1402
  exit(1);
1316
1403
  } catch {}