@rennii/deepseek-cli 1.0.0 → 1.0.2
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/deepseek-cli.js +72 -14
- package/package.json +1 -1
- package/test-deepseek-cli.js +23 -0
package/deepseek-cli.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
readFileSync,
|
|
12
12
|
readdirSync,
|
|
13
13
|
renameSync,
|
|
14
|
+
unlinkSync,
|
|
14
15
|
writeFileSync,
|
|
15
16
|
} = require("node:fs");
|
|
16
17
|
const { dirname, join } = require("node:path");
|
|
@@ -46,6 +47,7 @@ const COMMAND_SUGGESTIONS = [
|
|
|
46
47
|
["/logout", "xóa token đã lưu"],
|
|
47
48
|
["/new", "phiên mới"],
|
|
48
49
|
["/resume", "mở phiên đã lưu"],
|
|
50
|
+
["/remove", "xóa phiên đã lưu"],
|
|
49
51
|
["/agent", "bật/tắt terminal"],
|
|
50
52
|
["/clear", "xóa màn hình"],
|
|
51
53
|
["/help", "trợ giúp"],
|
|
@@ -172,6 +174,12 @@ function matchingCommands(prefix) {
|
|
|
172
174
|
return COMMAND_SUGGESTIONS.filter(([command]) => command.startsWith(prefix.toLowerCase()));
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
function selectedCommand(prefix, selected = 0) {
|
|
178
|
+
const matches = matchingCommands(prefix);
|
|
179
|
+
if (!matches.length) return null;
|
|
180
|
+
return matches[((selected % matches.length) + matches.length) % matches.length][0];
|
|
181
|
+
}
|
|
182
|
+
|
|
175
183
|
function parseSseEvent(event, state, onDelta) {
|
|
176
184
|
if (event.response_message_id != null) state.parentMessageId = event.response_message_id;
|
|
177
185
|
const path = event.p;
|
|
@@ -221,6 +229,17 @@ class SessionStore {
|
|
|
221
229
|
for (const session of sessions) saveJson(this.sessionPath(session), session);
|
|
222
230
|
}
|
|
223
231
|
|
|
232
|
+
remove(session) {
|
|
233
|
+
if (!session?.id) throw new Error("Phiên không có mã định danh để xóa");
|
|
234
|
+
let removed = false;
|
|
235
|
+
for (const path of this.sessionFiles(this.sessionDir)) {
|
|
236
|
+
if (readJson(path, null)?.id !== session.id) continue;
|
|
237
|
+
unlinkSync(path);
|
|
238
|
+
removed = true;
|
|
239
|
+
}
|
|
240
|
+
return removed;
|
|
241
|
+
}
|
|
242
|
+
|
|
224
243
|
sessionPath(session) {
|
|
225
244
|
const timestamp = new Date(session.created_at || session.updated_at || nowIso());
|
|
226
245
|
const date = Number.isNaN(timestamp.getTime()) ? new Date() : timestamp;
|
|
@@ -332,6 +351,7 @@ class TerminalUI {
|
|
|
332
351
|
` ${CYAN}/logout${RESET} Xóa token DeepSeek đã lưu`,
|
|
333
352
|
` ${CYAN}/new${RESET} Tạo phiên mới`,
|
|
334
353
|
` ${CYAN}/resume${RESET} Chọn phiên đã lưu để tiếp tục`,
|
|
354
|
+
` ${CYAN}/remove${RESET} Chọn và xóa một phiên đã lưu`,
|
|
335
355
|
` ${CYAN}/agent${RESET} Bật/tắt chế độ chạy lệnh terminal`,
|
|
336
356
|
` ${CYAN}/clear${RESET} Xóa nội dung khỏi màn hình`,
|
|
337
357
|
` ${CYAN}/help${RESET} Hiện trợ giúp`,
|
|
@@ -341,20 +361,22 @@ class TerminalUI {
|
|
|
341
361
|
].join("\n"));
|
|
342
362
|
}
|
|
343
363
|
|
|
344
|
-
showCommandSuggestions(prefix) {
|
|
345
|
-
const
|
|
346
|
-
|
|
364
|
+
showCommandSuggestions(prefix, selected = 0) {
|
|
365
|
+
const matches = matchingCommands(prefix);
|
|
366
|
+
const active = matches.length ? ((selected % matches.length) + matches.length) % matches.length : 0;
|
|
367
|
+
const suggestions = matches.map(([command, description], index) =>
|
|
368
|
+
`${index === active ? `${CYAN}›${RESET}` : " "} ${CYAN}${command.slice(0, prefix.length)}${RESET}${DIM}${command.slice(prefix.length)}${RESET} ${DIM}${description}${RESET}`,
|
|
347
369
|
);
|
|
348
370
|
this.write(`\x1b[s\n\x1b[J${suggestions.join("\n")}\x1b[u`);
|
|
349
371
|
}
|
|
350
372
|
|
|
351
373
|
clearCommandSuggestions() { this.write("\x1b[J"); }
|
|
352
374
|
|
|
353
|
-
completeCommand(characters) {
|
|
375
|
+
completeCommand(characters, selected = 0) {
|
|
354
376
|
const prefix = characters.join("");
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
const suffix =
|
|
377
|
+
const command = selectedCommand(prefix, selected);
|
|
378
|
+
if (!command || command === prefix.toLowerCase()) return false;
|
|
379
|
+
const suffix = command.slice(prefix.length);
|
|
358
380
|
characters.push(...suffix);
|
|
359
381
|
this.write(`${DIM}${suffix}${RESET}`);
|
|
360
382
|
this.clearCommandSuggestions();
|
|
@@ -371,6 +393,7 @@ class TerminalUI {
|
|
|
371
393
|
const reader = new RawReader();
|
|
372
394
|
const characters = [];
|
|
373
395
|
let pending = null;
|
|
396
|
+
let selectedSuggestion = 0;
|
|
374
397
|
reader.open();
|
|
375
398
|
try {
|
|
376
399
|
while (true) {
|
|
@@ -380,7 +403,7 @@ class TerminalUI {
|
|
|
380
403
|
const character = chunk[index];
|
|
381
404
|
if (character === "\u0003") throw new UserInterrupted();
|
|
382
405
|
if (character === "\r" || character === "\n") {
|
|
383
|
-
if (characters[0] === "/" && !characters.includes("\n") && this.completeCommand(characters)) continue;
|
|
406
|
+
if (characters[0] === "/" && !characters.includes("\n") && this.completeCommand(characters, selectedSuggestion)) continue;
|
|
384
407
|
if (characters[0] === "/") this.clearCommandSuggestions();
|
|
385
408
|
if (index === chunk.length - 1) {
|
|
386
409
|
const next = await reader.next(120);
|
|
@@ -399,18 +422,31 @@ class TerminalUI {
|
|
|
399
422
|
characters.pop();
|
|
400
423
|
this.write("\b \b");
|
|
401
424
|
}
|
|
402
|
-
|
|
425
|
+
selectedSuggestion = 0;
|
|
426
|
+
if (characters[0] === "/" && !characters.includes("\n")) this.showCommandSuggestions(characters.join(""), selectedSuggestion);
|
|
403
427
|
else if (!characters.length) this.clearCommandSuggestions();
|
|
404
428
|
continue;
|
|
405
429
|
}
|
|
406
430
|
if (character === "\u001b") {
|
|
431
|
+
const sequence = chunk.slice(index, index + 3);
|
|
432
|
+
const direction = sequence === "\u001b[A" ? -1 : sequence === "\u001b[B" ? 1 : 0;
|
|
433
|
+
if (direction && characters[0] === "/" && !characters.includes("\n")) {
|
|
434
|
+
const matches = matchingCommands(characters.join(""));
|
|
435
|
+
if (matches.length) {
|
|
436
|
+
selectedSuggestion = (selectedSuggestion + direction + matches.length) % matches.length;
|
|
437
|
+
this.showCommandSuggestions(characters.join(""), selectedSuggestion);
|
|
438
|
+
}
|
|
439
|
+
index += 2;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
407
442
|
while (index + 1 < chunk.length && !/[A-Za-z~]/.test(chunk[index + 1])) index += 1;
|
|
408
443
|
continue;
|
|
409
444
|
}
|
|
410
445
|
if (character >= " ") {
|
|
411
446
|
characters.push(character);
|
|
412
447
|
this.write(character);
|
|
413
|
-
|
|
448
|
+
selectedSuggestion = 0;
|
|
449
|
+
if (characters[0] === "/" && !characters.includes("\n")) this.showCommandSuggestions(characters.join(""), selectedSuggestion);
|
|
414
450
|
}
|
|
415
451
|
}
|
|
416
452
|
}
|
|
@@ -461,7 +497,7 @@ class TerminalUI {
|
|
|
461
497
|
}
|
|
462
498
|
}
|
|
463
499
|
|
|
464
|
-
async selectSession(sessions) {
|
|
500
|
+
async selectSession(sessions, { title = "Tiếp tục phiên", action = "mở" } = {}) {
|
|
465
501
|
if (!sessions.length) {
|
|
466
502
|
this.notice("Chưa có phiên nào được lưu.");
|
|
467
503
|
return null;
|
|
@@ -478,8 +514,8 @@ class TerminalUI {
|
|
|
478
514
|
const visibleRows = Math.max(1, (process.stdout.rows || 24) - 4);
|
|
479
515
|
const first = Math.min(Math.max(0, selected - visibleRows + 1), Math.max(0, sessions.length - visibleRows));
|
|
480
516
|
this.write("\x1b[2J\x1b[H");
|
|
481
|
-
console.log(`${BOLD}${BLUE}
|
|
482
|
-
console.log(`${DIM}↑ ↓ để chọn · Enter để
|
|
517
|
+
console.log(`${BOLD}${BLUE}${title}${RESET}`);
|
|
518
|
+
console.log(`${DIM}↑ ↓ để chọn · Enter để ${action} · Esc để hủy${RESET}\n`);
|
|
483
519
|
for (let index = first; index < Math.min(first + visibleRows, sessions.length); index += 1) {
|
|
484
520
|
console.log(sessionPickerLine(sessions[index], index, selected, columns));
|
|
485
521
|
}
|
|
@@ -491,6 +527,16 @@ class TerminalUI {
|
|
|
491
527
|
if (/^[1-9]$/.test(key) && Number(key) <= sessions.length) selected = Number(key) - 1;
|
|
492
528
|
}
|
|
493
529
|
}
|
|
530
|
+
|
|
531
|
+
async confirmSessionRemoval(session) {
|
|
532
|
+
const title = sessionPreview(session, 48);
|
|
533
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
534
|
+
const answer = await readline.createInterface({ input: process.stdin, output: process.stdout }).question(`Xóa "${title}"? (y/N): `);
|
|
535
|
+
return answer.trim().toLowerCase() === "y";
|
|
536
|
+
}
|
|
537
|
+
this.write(`\x1b[2J\x1b[H${BOLD}${RED}Xóa phiên${RESET}\n${DIM}${title}${RESET}\n\n${DIM}Enter để xóa · Esc để hủy${RESET}`);
|
|
538
|
+
return (await this.readKey()) === "ENTER";
|
|
539
|
+
}
|
|
494
540
|
}
|
|
495
541
|
|
|
496
542
|
class DeepSeekCLI {
|
|
@@ -693,6 +739,17 @@ class DeepSeekCLI {
|
|
|
693
739
|
this.ui.notice("Đã tiếp tục phiên đã chọn.");
|
|
694
740
|
}
|
|
695
741
|
|
|
742
|
+
async removeSession() {
|
|
743
|
+
const session = await this.ui.selectSession(this.savedSessions, { title: "Xóa phiên", action: "chọn" });
|
|
744
|
+
if (!session) { this.ui.clear(this.activeSession); return; }
|
|
745
|
+
if (!await this.ui.confirmSessionRemoval(session)) { this.ui.clear(this.activeSession); return; }
|
|
746
|
+
if (!this.store.remove(session)) { this.ui.error("Không tìm thấy tệp phiên để xóa."); return; }
|
|
747
|
+
this.savedSessions = this.store.load();
|
|
748
|
+
if (this.activeSession.id === session.id) this.newSession();
|
|
749
|
+
this.ui.clear(this.activeSession);
|
|
750
|
+
this.ui.notice("Đã xóa phiên đã chọn.");
|
|
751
|
+
}
|
|
752
|
+
|
|
696
753
|
async handleCommand(prompt) {
|
|
697
754
|
const command = prompt.toLowerCase();
|
|
698
755
|
if (["/exit", "exit", "quit", "q"].includes(command)) return false;
|
|
@@ -702,6 +759,7 @@ class DeepSeekCLI {
|
|
|
702
759
|
else if (command === "/clear") this.ui.clear(this.activeSession);
|
|
703
760
|
else if (command === "/new") { this.newSession(); this.ui.clear(this.activeSession); this.ui.notice("Đã tạo phiên mới."); }
|
|
704
761
|
else if (command === "/resume") await this.resumeSession();
|
|
762
|
+
else if (command === "/remove") await this.removeSession();
|
|
705
763
|
else if (command === "/agent") { this.agentEnabled = !this.agentEnabled; this.ui.notice(`Chế độ terminal: ${this.agentEnabled ? "bật" : "tắt"}`); }
|
|
706
764
|
else if (command.startsWith("/")) this.ui.error("Lệnh không hợp lệ. Dùng /help để xem danh sách lệnh.");
|
|
707
765
|
else return null;
|
|
@@ -782,7 +840,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
782
840
|
await cli.run();
|
|
783
841
|
}
|
|
784
842
|
|
|
785
|
-
module.exports = { DATA_DIR, DeepSeekCLI, RUNTIME_DATA, SessionStore, bootstrapDataDirectory, extractCommands, formatCommandFeedback, main, matchingCommands, newLocalSession, parseSseEvent, sessionPickerLine };
|
|
843
|
+
module.exports = { DATA_DIR, DeepSeekCLI, RUNTIME_DATA, SessionStore, bootstrapDataDirectory, extractCommands, formatCommandFeedback, main, matchingCommands, newLocalSession, parseSseEvent, selectedCommand, sessionPickerLine };
|
|
786
844
|
|
|
787
845
|
if (require.main === module) {
|
|
788
846
|
main().catch((error) => {
|
package/package.json
CHANGED
package/test-deepseek-cli.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
matchingCommands,
|
|
15
15
|
newLocalSession,
|
|
16
16
|
parseSseEvent,
|
|
17
|
+
selectedCommand,
|
|
17
18
|
sessionPickerLine,
|
|
18
19
|
} = require("./deepseek-cli");
|
|
19
20
|
|
|
@@ -21,6 +22,12 @@ test("filters slash commands by typed prefix", () => {
|
|
|
21
22
|
assert.deepEqual(matchingCommands("/res").map(([command]) => command), ["/resume"]);
|
|
22
23
|
});
|
|
23
24
|
|
|
25
|
+
test("selects a slash-command suggestion by arrow position", () => {
|
|
26
|
+
assert.equal(selectedCommand("/", 0), "/login");
|
|
27
|
+
assert.equal(selectedCommand("/", 1), "/logout");
|
|
28
|
+
assert.equal(selectedCommand("/res", 0), "/resume");
|
|
29
|
+
});
|
|
30
|
+
|
|
24
31
|
test("login opens the official DeepSeek page", async () => {
|
|
25
32
|
const directory = mkdtempSync(join(tmpdir(), "deepseek-cli-"));
|
|
26
33
|
try {
|
|
@@ -84,6 +91,22 @@ test("persists a local session", () => {
|
|
|
84
91
|
}
|
|
85
92
|
});
|
|
86
93
|
|
|
94
|
+
test("removes every saved file for the selected session", () => {
|
|
95
|
+
const directory = mkdtempSync(join(tmpdir(), "deepseek-cli-"));
|
|
96
|
+
try {
|
|
97
|
+
const store = new SessionStore(join(directory, "sessions"), []);
|
|
98
|
+
const session = newLocalSession();
|
|
99
|
+
session.id = "remove-me";
|
|
100
|
+
session.created_at = "2026-08-30T19:45:10.000Z";
|
|
101
|
+
store.save([session]);
|
|
102
|
+
assert.equal(store.remove(session), true);
|
|
103
|
+
assert.deepEqual(store.load(), []);
|
|
104
|
+
assert.equal(store.remove(session), false);
|
|
105
|
+
} finally {
|
|
106
|
+
rmSync(directory, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
87
110
|
test("migrates flat session history into one file per dated session", () => {
|
|
88
111
|
const directory = mkdtempSync(join(tmpdir(), "deepseek-cli-"));
|
|
89
112
|
try {
|