deepseek-cli-mode 1.4.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,19 @@
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
+
3
17
  ## 1.4.0 — reasoning effort levels in the model picker
4
18
 
5
19
  - `/model` now asks for the reasoning effort after you pick a model, listing the adapter's real levels
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.4.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" },
@@ -549,6 +551,7 @@ Interactive commands:
549
551
  /resume <id> switch to another session
550
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,6 +1069,83 @@ async function run(ctx, options, exit) {
1051
1069
  return options[index]?.value ?? null;
1052
1070
  }
1053
1071
 
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
+
1054
1149
  /** Adapter-owned reasoning levels for the selected model, when exposed. */
1055
1150
  async function reasoningInfo() {
1056
1151
  try {
@@ -1391,6 +1486,69 @@ async function run(ctx, options, exit) {
1391
1486
  } catch {}
1392
1487
  continue;
1393
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
+ }
1394
1552
  case "/model": {
1395
1553
  if (argument !== "") {
1396
1554
  await applyModelSelection(argument);