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 +14 -0
- package/README.md +5 -4
- package/package.json +1 -1
- package/runner/line-editor.js +13 -3
- package/runner/runner.js +159 -1
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
|
|
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
|
-
|
|
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]` |
|
|
162
|
-
| `/reasoning [
|
|
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
package/runner/line-editor.js
CHANGED
|
@@ -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 (
|
|
414
|
-
const visible =
|
|
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
|
-
|
|
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);
|