@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 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 suggestions = matchingCommands(prefix).map(([command, description]) =>
346
- ` ${CYAN}${command.slice(0, prefix.length)}${RESET}${DIM}${command.slice(prefix.length)}${RESET} ${DIM}${description}${RESET}`,
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 matches = matchingCommands(prefix);
356
- if (matches.length !== 1 || matches[0][0] === prefix.toLowerCase()) return false;
357
- const suffix = matches[0][0].slice(prefix.length);
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
- if (characters[0] === "/" && !characters.includes("\n")) this.showCommandSuggestions(characters.join(""));
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
- if (characters[0] === "/" && !characters.includes("\n")) this.showCommandSuggestions(characters.join(""));
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}Tiếp tục phiên${RESET}`);
482
- console.log(`${DIM}↑ ↓ để chọn · Enter để mở · Esc để hủy${RESET}\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rennii/deepseek-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "description": "DeepSeek terminal client for Termux",
6
6
  "type": "commonjs",
@@ -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 {