deepseek-cli-mode 1.3.0 → 1.5.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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0 — `/api` configuration and coloured permission footer
4
+
5
+ - **`/api`** configures the DeepSeek endpoint from inside the CLI:
6
+ - `/api` opens a menu (Show status / Set API key / Set base URL / Remove key)
7
+ - `/api set <key>` or bare `/api <key>` stores the key through the Harness credential service, which writes
8
+ `$DSH_HOME/.credentials.yaml` immediately — no restart, the next request uses it
9
+ - `/api url <https://…>` writes `baseURL` into `$DSH_HOME/settings.yaml`
10
+ - `/api unset` removes the stored key; `/api status` prints key state, base URL, and both file paths
11
+ - The key prompt is masked (`*`) in the interactive editor
12
+ - The permission footer is colour-coded: **`⏵ danger-full-access` in red**, `read-only` in green,
13
+ `workspace-write` in yellow; the rest of the line stays dim.
14
+ - Security: keys are never printed back; the value is passed only to the credential service. `/api` is a user-typed
15
+ command — the model cannot change credentials.
16
+
17
+ ## 1.4.0 — reasoning effort levels in the model picker
18
+
19
+ - `/model` now asks for the reasoning effort after you pick a model, listing the adapter's real levels
20
+ (`low`, `medium`, `high`, `max`) plus `default`, with the current value preselected.
21
+ - `/reasoning` lists the same adapter-provided levels (falling back to `low`/`medium`/`high`/`max` when the
22
+ adapter exposes no metadata).
23
+ - The footer and banner show the active model **and** effort, e.g. `deepseek-flash · high`.
24
+ - Direct arguments still work: `/reasoning low`, `/model deepseek-v4-pro`.
25
+
3
26
  ## 1.3.0 — pickers, Shift+Tab permission cycling, permanent mode footer
4
27
 
5
28
  - `/model`, `/reasoning`, `/permissions`, and `/resume` now open an interactive picker (`↑`/`↓`, `Enter`,
package/README.md CHANGED
@@ -145,10 +145,10 @@ Typing `/` opens a live command menu, like Claude Code:
145
145
  `↑`/`↓` moves the selection, `Tab` completes it, `Enter` runs it. The menu filters as you type.
146
146
 
147
147
  `/model`, `/reasoning`, `/permissions`, and `/resume` open the same kind of picker when called without an argument.
148
- **Shift+Tab** cycles the permission mode, and a dim footer above the prompt always shows the active one:
148
+ **Shift+Tab** cycles the permission mode, and a footer above the prompt always shows the active one — `danger-full-access` in red, `read-only` in green, `workspace-write` in yellow:
149
149
 
150
150
  ```
151
- ⏵⏵ workspace-write · deepseek-official/deepseek-chat · shift+tab to cycle
151
+ ⏵ workspace-write · deepseek-official/deepseek-chat · shift+tab to cycle
152
152
  >
153
153
  ```
154
154
 
@@ -158,8 +158,9 @@ Typing `/` opens a live command menu, like Claude Code:
158
158
  | `/new` | start a fresh session (new id, keeps the process warm) |
159
159
  | `/sessions` | recent sessions in this directory (with a ▶ running marker) |
160
160
  | `/resume <id>` | switch to another session |
161
- | `/model [id]` | show or switch the model for the next message |
162
- | `/reasoning [off\|high\|max]` | show or set the reasoning effort |
161
+ | `/model [id]` | pick a model and its reasoning effort |
162
+ | `/reasoning [effort]` | pick a reasoning effort (low/medium/high/max) |
163
+ | `/api [set <key>\|url <url>\|unset\|status]` | configure the API key and endpoint; writes `$DSH_HOME/.credentials.yaml` and `settings.yaml` |
163
164
  | `/permission [preset]` | switch the sandbox + approval preset |
164
165
  | `/skills` | list the skills discovered in this workspace |
165
166
  | `/jobs` | list this session's background jobs |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-cli-mode",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",
@@ -55,6 +55,7 @@ export class LineEditor {
55
55
  this.selectOptions = [];
56
56
  this.selectIndex = 0;
57
57
  this.hint = "";
58
+ this.secret = false;
58
59
  this._onData = (chunk) => this.handleData(chunk);
59
60
  this._onEnd = () => {
60
61
  this.onEnd();
@@ -99,11 +100,13 @@ export class LineEditor {
99
100
  * Ask one line.
100
101
  * @param {string} prompt prompt string (may contain ANSI)
101
102
  * @param {boolean} menuEnabled show slash-command suggestions
103
+ * @param {boolean} [secret] mask the typed value (API keys, tokens)
102
104
  * @returns {Promise<string|null>} line, or null on Ctrl+C / EOF
103
105
  */
104
- ask(prompt, menuEnabled = false) {
106
+ ask(prompt, menuEnabled = false, secret = false) {
105
107
  this.prompt = prompt;
106
108
  this.menuEnabled = menuEnabled;
109
+ this.secret = secret === true;
107
110
  this.buffer = "";
108
111
  this.cursor = 0;
109
112
  this.menu = [];
@@ -118,6 +121,11 @@ export class LineEditor {
118
121
  });
119
122
  }
120
123
 
124
+ /** Ask for a secret value without echoing it. */
125
+ askSecret(prompt) {
126
+ return this.ask(prompt, false, true);
127
+ }
128
+
121
129
  /**
122
130
  * Ask the user to pick one option from a list.
123
131
  * @param {string} title prompt line
@@ -371,6 +379,7 @@ export class LineEditor {
371
379
  this.selectMode = false;
372
380
  this.selectOptions = [];
373
381
  this.hint = "";
382
+ this.secret = false;
374
383
  this.clearRender();
375
384
  this.menu = [];
376
385
  this.output.write("\x1b[?25h");
@@ -409,9 +418,10 @@ export class LineEditor {
409
418
  const prompt = this.prompt;
410
419
  const promptWidth = visibleWidth(prompt);
411
420
  const available = Math.max(8, columns - promptWidth - 1);
421
+ const display = this.secret ? "*".repeat(this.buffer.length) : this.buffer;
412
422
  let offset = 0;
413
- if (this.buffer.length > available) offset = Math.max(0, this.cursor - available + 1);
414
- const visible = this.buffer.slice(offset, offset + available);
423
+ if (display.length > available) offset = Math.max(0, this.cursor - available + 1);
424
+ const visible = display.slice(offset, offset + available);
415
425
  const column = promptWidth + (this.cursor - offset);
416
426
  this.output.write(prompt + visible);
417
427
 
package/runner/runner.js CHANGED
@@ -23,6 +23,7 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
23
23
  import { SessionId } from "@deepseek-ai/dsh-session";
24
24
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
25
25
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
26
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
26
27
  import { LineEditor } from "./line-editor.js";
27
28
 
28
29
  /** Stable Cordis plugin name. */
@@ -497,6 +498,7 @@ function printBanner(model) {
497
498
  // Slash-command suggestions for the live menu (native Harness commands are
498
499
  // merged in at runtime).
499
500
  const BUILTIN_COMMANDS = [
501
+ { name: "api", description: "Set the API key or base URL" },
500
502
  { name: "clear", description: "Clear the screen" },
501
503
  { name: "exit", description: "Leave the session" },
502
504
  { name: "export", description: "Write this run's transcript as markdown" },
@@ -547,8 +549,9 @@ Interactive commands:
547
549
  /new start a fresh session
548
550
  /sessions list recent sessions in this directory
549
551
  /resume <id> switch to another session
550
- /model [id] pick a model, or pass an id directly
552
+ /model [id] pick a model and its reasoning effort (low/medium/high/max)
551
553
  /reasoning [off|high|max] pick a reasoning effort
554
+ /api [set <key>|url <url>|unset|status] configure the API key and endpoint
552
555
  /permissions [p] pick a permission mode (native: /permission; Shift+Tab cycles)
553
556
  /skills list the skills discovered in this workspace
554
557
  /jobs list this session's background jobs
@@ -644,6 +647,8 @@ async function run(ctx, options, exit) {
644
647
  const attachments = ctx.get("attachments");
645
648
  const tokenMeter = ctx.get("tokenMeter");
646
649
  const llm = ctx.get("llm");
650
+ const credentials = ctx.get("credentials");
651
+ const settings = ctx.get("settings");
647
652
 
648
653
  if (options.list) {
649
654
  printSessionList(listSessions());
@@ -1029,10 +1034,23 @@ async function run(ctx, options, exit) {
1029
1034
  }
1030
1035
  }
1031
1036
 
1037
+ /** Colour the active permission mode by risk. */
1038
+ function modeColor(mode) {
1039
+ if (mode.includes("danger")) return ansi.red;
1040
+ if (mode.includes("read")) return ansi.green;
1041
+ return ansi.yellow;
1042
+ }
1043
+
1032
1044
  /** Permanent one-line footer above the prompt. */
1033
1045
  function printStatusBar() {
1034
1046
  if (!tty || !interactive) return;
1035
- out(paint(ansi.dim, ` ⏵⏵ ${sanitize(currentPermission())} · ${sanitize(modelDesc())} · shift+tab to cycle`) + "\n");
1047
+ const mode = currentPermission();
1048
+ out(
1049
+ " " +
1050
+ paint(modeColor(mode), `⏵ ${sanitize(mode)}`) +
1051
+ paint(ansi.dim, ` · ${sanitize(modelDesc())} · shift+tab to cycle`) +
1052
+ "\n"
1053
+ );
1036
1054
  }
1037
1055
 
1038
1056
  /** One list picker shared by /model, /reasoning, /permissions, /resume. */
@@ -1051,7 +1069,95 @@ async function run(ctx, options, exit) {
1051
1069
  return options[index]?.value ?? null;
1052
1070
  }
1053
1071
 
1054
- async function applyModelSelection(modelId) {
1072
+ // ---- API key / endpoint management --------------------------------------
1073
+ const API_KEY_REF = credentialRef("DEEPSEEK_API_KEY");
1074
+ const DEFAULT_BASE_URL = "https://api.deepseek.com";
1075
+
1076
+ async function apiBaseUrl() {
1077
+ try {
1078
+ const descriptor = settings?.describe().find((entry) => String(entry.ns) === "llm-deepseek");
1079
+ const value = descriptor?.value;
1080
+ if (value && typeof value === "object" && typeof value.baseURL === "string" && value.baseURL !== "") return value.baseURL;
1081
+ } catch {}
1082
+ return DEFAULT_BASE_URL;
1083
+ }
1084
+
1085
+ async function printApiStatus() {
1086
+ let info = null;
1087
+ try {
1088
+ info = credentials === undefined ? null : await credentials.describe(API_KEY_REF);
1089
+ } catch {}
1090
+ const keyState = info?.configured
1091
+ ? `configured (source: ${info.source}${info.writable === false ? ", read-only" : ""})`
1092
+ : "not configured";
1093
+ out(paint(ansi.bold, " API configuration") + "\n");
1094
+ out(` key: ${sanitize(keyState)}\n`);
1095
+ out(` base: ${sanitize(await apiBaseUrl())}\n`);
1096
+ out(paint(ansi.dim, ` credentials file: ${join(resolveDshHome(), ".credentials.yaml")}`) + "\n");
1097
+ out(paint(ansi.dim, ` settings file: ${join(resolveDshHome(), "settings.yaml")}`) + "\n");
1098
+ }
1099
+
1100
+ async function storeApiKey(raw) {
1101
+ const key = String(raw).trim();
1102
+ if (key === "") {
1103
+ out(paint(ansi.yellow, " empty key — nothing stored\n"));
1104
+ return false;
1105
+ }
1106
+ if (/\s/.test(key)) {
1107
+ out(paint(ansi.red, " API key must not contain whitespace\n"));
1108
+ return false;
1109
+ }
1110
+ if (credentials === undefined) {
1111
+ out(paint(ansi.yellow, " this profile has no credential store\n"));
1112
+ return false;
1113
+ }
1114
+ try {
1115
+ await credentials.set(API_KEY_REF, key);
1116
+ out(paint(ansi.green, ` ✓ API key saved to ${sanitize(join(resolveDshHome(), ".credentials.yaml"))}\n`));
1117
+ out(paint(ansi.dim, " new requests use it immediately\n"));
1118
+ return true;
1119
+ } catch (error) {
1120
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1121
+ return false;
1122
+ }
1123
+ }
1124
+
1125
+ async function storeBaseUrl(raw) {
1126
+ const url = String(raw).trim();
1127
+ if (!/^https?:\/\//i.test(url)) {
1128
+ out(paint(ansi.red, " base URL must start with http:// or https://\n"));
1129
+ return false;
1130
+ }
1131
+ if (settings === undefined) {
1132
+ out(paint(ansi.yellow, " this profile has no settings service\n"));
1133
+ return false;
1134
+ }
1135
+ try {
1136
+ await settings.update("llm-deepseek", { baseURL: url });
1137
+ out(paint(ansi.green, ` ✓ base URL saved: ${sanitize(url)}\n`));
1138
+ return true;
1139
+ } catch (error) {
1140
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1141
+ return false;
1142
+ }
1143
+ }
1144
+
1145
+ function askSecret(prompt) {
1146
+ return editor ? editor.askSecret(prompt) : ask(prompt);
1147
+ }
1148
+
1149
+ /** Adapter-owned reasoning levels for the selected model, when exposed. */
1150
+ async function reasoningInfo() {
1151
+ try {
1152
+ if (llm === undefined) return null;
1153
+ const resolved = await llm.resolveModel(selectionRef.current.provider, selectionRef.current.model);
1154
+ return resolved.reasoning ?? null;
1155
+ } catch {
1156
+ return null;
1157
+ }
1158
+ }
1159
+
1160
+ async function applyModelSelection(modelId, pickEffort = true) {
1055
1161
  const next = { ...selectionRef.current, model: modelId };
1056
1162
  try {
1057
1163
  await defaultModel.saveSelection(next);
@@ -1059,7 +1165,23 @@ async function run(ctx, options, exit) {
1059
1165
  out(paint(ansi.green, ` ✓ model set: ${sanitize(next.provider)}/${sanitize(next.model)} (next message)\n`));
1060
1166
  } catch (error) {
1061
1167
  out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1168
+ return;
1062
1169
  }
1170
+ if (!pickEffort) return;
1171
+ const info = await reasoningInfo();
1172
+ const effortOptions =
1173
+ info !== null && info.efforts.length > 0
1174
+ ? info.efforts.map((effort) => ({ label: effort.name, description: effort.id, value: effort.id }))
1175
+ : ["low", "medium", "high", "max"].map((effort) => ({ label: effort, description: `effort: ${effort}`, value: effort }));
1176
+ const options = [...effortOptions, { label: "default", description: "provider default", value: "default" }];
1177
+ const defaultEffort = info !== null && info.defaultEffort !== undefined ? info.defaultEffort : "default";
1178
+ // Offer the effort choice for the newly selected model, keeping the current
1179
+ // value when it is still valid.
1180
+ const currentEffort = selectionRef.current.reasoningEffort ?? "default";
1181
+ const preferred = options.some((option) => option.value === currentEffort) ? currentEffort : defaultEffort;
1182
+ const initial = Math.max(0, options.findIndex((option) => option.value === preferred));
1183
+ const chosen = await selectOption(`Reasoning effort for ${modelId}`, options, initial);
1184
+ if (chosen !== null) await applyReasoningSelection(chosen);
1063
1185
  }
1064
1186
 
1065
1187
  async function applyReasoningSelection(effort) {
@@ -1364,6 +1486,69 @@ async function run(ctx, options, exit) {
1364
1486
  } catch {}
1365
1487
  continue;
1366
1488
  }
1489
+ case "/api": {
1490
+ const trimmed = argument.trim();
1491
+ if (trimmed === "") {
1492
+ if (!editor) {
1493
+ await printApiStatus();
1494
+ continue;
1495
+ }
1496
+ const action = await selectOption(
1497
+ "API configuration",
1498
+ [
1499
+ { label: "Show status", description: "key, base URL, files", value: "status" },
1500
+ { label: "Set DeepSeek API key", description: "saved to $DSH_HOME/.credentials.yaml", value: "key" },
1501
+ { label: "Set base URL", description: "saved to $DSH_HOME/settings.yaml", value: "url" },
1502
+ { label: "Remove stored API key", description: "falls back to environment", value: "unset" },
1503
+ ],
1504
+ 0
1505
+ );
1506
+ if (action === "status") await printApiStatus();
1507
+ else if (action === "key") {
1508
+ const key = await askSecret(paint(ansi.green, " API key: "));
1509
+ if (key !== null) await storeApiKey(key);
1510
+ } else if (action === "url") {
1511
+ const url = await ask(paint(ansi.green, " Base URL: "));
1512
+ if (url !== null) await storeBaseUrl(url);
1513
+ } else if (action === "unset") {
1514
+ try {
1515
+ await credentials?.unset(API_KEY_REF);
1516
+ out(paint(ansi.green, " ✓ stored API key removed\n"));
1517
+ } catch (error) {
1518
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1519
+ }
1520
+ }
1521
+ continue;
1522
+ }
1523
+ const [subcommand, ...rest] = trimmed.split(/\s+/);
1524
+ const value = rest.join(" ").trim();
1525
+ if (subcommand === "status" || subcommand === "show") {
1526
+ await printApiStatus();
1527
+ continue;
1528
+ }
1529
+ if (subcommand === "unset" || subcommand === "remove" || subcommand === "clear") {
1530
+ try {
1531
+ await credentials?.unset(API_KEY_REF);
1532
+ out(paint(ansi.green, " ✓ stored API key removed\n"));
1533
+ } catch (error) {
1534
+ out(paint(ansi.red, ` ✘ ${sanitize(describeError(error))}\n`));
1535
+ }
1536
+ continue;
1537
+ }
1538
+ if (subcommand === "url" || subcommand === "base" || subcommand === "base-url") {
1539
+ if (value === "") out(paint(ansi.yellow, " usage: /api url <https://endpoint>\n"));
1540
+ else await storeBaseUrl(value);
1541
+ continue;
1542
+ }
1543
+ if (subcommand === "set" || subcommand === "key") {
1544
+ if (value === "") out(paint(ansi.yellow, " usage: /api set <key>\n"));
1545
+ else await storeApiKey(value);
1546
+ continue;
1547
+ }
1548
+ // Bare value: treat it as the API key.
1549
+ await storeApiKey(trimmed);
1550
+ continue;
1551
+ }
1367
1552
  case "/model": {
1368
1553
  if (argument !== "") {
1369
1554
  await applyModelSelection(argument);
@@ -1392,17 +1577,22 @@ async function run(ctx, options, exit) {
1392
1577
  await applyReasoningSelection(argument);
1393
1578
  continue;
1394
1579
  }
1395
- const efforts = ["off", "high", "max", "default"];
1580
+ const info = await reasoningInfo();
1581
+ const efforts =
1582
+ info !== null && info.efforts.length > 0
1583
+ ? info.efforts.map((effort) => ({
1584
+ label: effort.name,
1585
+ description: effort.description ?? effort.id,
1586
+ value: effort.id,
1587
+ }))
1588
+ : ["low", "medium", "high", "max"].map((effort) => ({
1589
+ label: effort,
1590
+ description: `effort: ${effort}`,
1591
+ value: effort,
1592
+ }));
1593
+ efforts.push({ label: "default", description: "provider default", value: "default" });
1396
1594
  const current = selectionRef.current.reasoningEffort ?? "default";
1397
- const chosen = await selectOption(
1398
- "Select reasoning effort",
1399
- efforts.map((effort) => ({
1400
- label: effort,
1401
- description: effort === "default" ? "provider default" : `effort: ${effort}`,
1402
- value: effort,
1403
- })),
1404
- Math.max(0, efforts.indexOf(current))
1405
- );
1595
+ const chosen = await selectOption("Select reasoning effort", efforts, Math.max(0, efforts.findIndex((option) => option.value === current)));
1406
1596
  if (chosen !== null) await applyReasoningSelection(chosen);
1407
1597
  continue;
1408
1598
  }