@noobdemon/noob-cli 1.0.5 → 1.0.6
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/bin/noob.js +6 -1
- package/package.json +1 -1
- package/src/i18n.js +14 -1
- package/src/repl.js +184 -24
- package/src/sessions.js +109 -0
- package/src/tools.js +4 -1
package/bin/noob.js
CHANGED
|
@@ -7,7 +7,7 @@ import { t } from "../src/i18n.js";
|
|
|
7
7
|
import { checkLatest, runUpdate, CURRENT } from "../src/update.js";
|
|
8
8
|
|
|
9
9
|
const argv = process.argv.slice(2);
|
|
10
|
-
const opts = { yolo: false, model: undefined, prompt: undefined };
|
|
10
|
+
const opts = { yolo: false, model: undefined, prompt: undefined, continue: false, resume: false };
|
|
11
11
|
const positional = [];
|
|
12
12
|
|
|
13
13
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -15,6 +15,9 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
15
15
|
if (a === "--yolo" || a === "-y") opts.yolo = true;
|
|
16
16
|
else if (a === "--insecure-tls") process.env.NOOB_INSECURE_TLS = "1";
|
|
17
17
|
else if (a === "--model" || a === "-m") opts.model = argv[++i];
|
|
18
|
+
else if (a === "--continue" || a === "-c") opts.continue = true;
|
|
19
|
+
else if (a === "--resume" || a === "-r") opts.resume = true;
|
|
20
|
+
else if (a.startsWith("--resume=")) opts.resume = a.slice("--resume=".length);
|
|
18
21
|
else if (a === "--help" || a === "-h") {
|
|
19
22
|
printHelp();
|
|
20
23
|
process.exit(0);
|
|
@@ -90,6 +93,8 @@ Cách dùng:
|
|
|
90
93
|
Tuỳ chọn:
|
|
91
94
|
-m, --model <id> chọn mô hình (vd: gateway-claude-opus-4-7)
|
|
92
95
|
-y, --yolo tự động duyệt sửa file & chạy lệnh (cẩn thận)
|
|
96
|
+
-c, --continue tiếp tục phiên gần nhất (resume)
|
|
97
|
+
-r, --resume[=id] chọn phiên để tiếp tục (không id = hiện danh sách)
|
|
93
98
|
--insecure-tls tắt kiểm tra TLS (chỉ cho mạng có proxy chặn TLS)
|
|
94
99
|
-h, --help hiện trợ giúp
|
|
95
100
|
|
package/package.json
CHANGED
package/src/i18n.js
CHANGED
|
@@ -51,7 +51,9 @@ export const t = {
|
|
|
51
51
|
cmdSearch: "/search bật/tắt chế độ tìm web",
|
|
52
52
|
cmdChat: "/chat quay lại chế độ chat thường",
|
|
53
53
|
cmdYolo: "/yolo bật/tắt tự duyệt (hoặc nhấn Shift+Tab)",
|
|
54
|
-
cmdClear: "/clear /new xoá ngữ cảnh
|
|
54
|
+
cmdClear: "/clear /new xoá ngữ cảnh (mở phiên mới, phiên cũ vẫn resume được)",
|
|
55
|
+
cmdResume: "/resume [id] tiếp tục phiên cũ (không id = chọn từ danh sách)",
|
|
56
|
+
cmdSessions: "/sessions liệt kê các phiên đã lưu",
|
|
55
57
|
cmdLogin: "/login <key> đăng nhập bằng API key",
|
|
56
58
|
cmdLogout: "/logout đăng xuất",
|
|
57
59
|
cmdUsage: "/usage xem hạn mức key còn lại",
|
|
@@ -81,6 +83,17 @@ export const t = {
|
|
|
81
83
|
maxSteps: "_(đã dừng: chạm giới hạn số bước tool)_",
|
|
82
84
|
toolDenied: "Người dùng từ chối thao tác này. Hãy đổi cách làm hoặc hỏi lại.",
|
|
83
85
|
|
|
86
|
+
// sessions (lưu lịch sử + resume)
|
|
87
|
+
sessionResumed: (id) => `Đã khôi phục phiên ${id}`,
|
|
88
|
+
sessionNonePrev: "Chưa có phiên nào trước đó — bắt đầu phiên mới.",
|
|
89
|
+
sessionNotFound: (id) => `Không tìm thấy phiên "${id}".`,
|
|
90
|
+
sessionEmpty: "Chưa có phiên đã lưu nào.",
|
|
91
|
+
sessionPickTitle: "Chọn phiên để tiếp tục:",
|
|
92
|
+
sessionListTitle: "Các phiên đã lưu:",
|
|
93
|
+
sessionPickPrompt: (n) => `chọn phiên [1-${n}], Enter để bỏ qua › `,
|
|
94
|
+
sessionPickBad: "lựa chọn không hợp lệ.",
|
|
95
|
+
sessionResumeHint: "/resume <id> để tiếp tục một phiên · hoặc chạy: noob -c (phiên gần nhất)",
|
|
96
|
+
|
|
84
97
|
// update
|
|
85
98
|
cmdUpdate: "/update cập nhật noob lên bản mới nhất",
|
|
86
99
|
updateFound: (cur, lat) => `🆕 Có bản mới ${lat} (đang dùng ${cur}) — đang tự cập nhật nền…`,
|
package/src/repl.js
CHANGED
|
@@ -10,6 +10,7 @@ import { c, banner, modelBadge, renderMarkdown, box } from "./ui.js";
|
|
|
10
10
|
import { config } from "./config.js";
|
|
11
11
|
import { t } from "./i18n.js";
|
|
12
12
|
import { checkLatest, runUpdate, CURRENT } from "./update.js";
|
|
13
|
+
import * as sessions from "./sessions.js";
|
|
13
14
|
|
|
14
15
|
export async function startRepl(opts = {}) {
|
|
15
16
|
const state = {
|
|
@@ -28,54 +29,93 @@ export async function startRepl(opts = {}) {
|
|
|
28
29
|
return c.user(nl + t.promptYou) + yolo + c.dim("v" + CURRENT + " › ");
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
// ── Input layer — KHÔNG ĐƯỢC tự tắt ───────────────────────────────────────
|
|
33
|
+
// Trên Windows CMD, readline có thể phát sự kiện 'close' KHÔNG phải vì người
|
|
34
|
+
// dùng muốn thoát (console/keypress tranh chấp, hoặc một tiến trình con chạm
|
|
35
|
+
// vào console). Nếu để 'close' đó kết thúc vòng lặp → CLI "tự động tắt" giữa
|
|
36
|
+
// chừng (thường ngay sau khi hỏi quyền). Hơn nữa, một khi readline đã đóng,
|
|
37
|
+
// không còn handle nào giữ stdin → event loop cạn → Node thoát code 0.
|
|
38
|
+
//
|
|
39
|
+
// Khắc phục: trên TTY ta DỰNG LẠI readline khi gặp 'close' bất thường và giữ
|
|
40
|
+
// nguyên reader đang chờ (người dùng chỉ thấy prompt hiện lại). Chỉ dừng thật
|
|
41
|
+
// khi: /exit, Ctrl+C hai lần, hoặc EOF thật (stdin piped đã cạn / 'close' dồn
|
|
42
|
+
// dập nghĩa là stdin đã mất).
|
|
43
|
+
let rl;
|
|
44
|
+
let closed = false; // đã ngừng đọc vĩnh viễn
|
|
45
|
+
let exiting = false; // ta chủ động thoát (/exit, Ctrl+C ×2)
|
|
46
|
+
let lastPrompt = ""; // áp lại sau khi dựng lại interface
|
|
47
|
+
const queue = []; // lines đã gõ/pipe, dùng theo thứ tự
|
|
48
|
+
let waiter = null; // resolver đang chờ dòng kế tiếp
|
|
49
|
+
let closeBurst = 0;
|
|
50
|
+
let closeAt = 0;
|
|
51
|
+
|
|
52
|
+
function deliver(line) {
|
|
37
53
|
if (waiter) {
|
|
38
54
|
const w = waiter;
|
|
39
55
|
waiter = null;
|
|
40
56
|
w(line);
|
|
41
57
|
} else {
|
|
42
|
-
queue.push(line); // type-ahead / buffered
|
|
58
|
+
queue.push(line); // type-ahead / buffered — không bao giờ mất
|
|
43
59
|
}
|
|
44
|
-
}
|
|
45
|
-
|
|
60
|
+
}
|
|
61
|
+
function endInput() {
|
|
46
62
|
closed = true;
|
|
47
63
|
if (waiter) {
|
|
48
64
|
const w = waiter;
|
|
49
65
|
waiter = null;
|
|
50
66
|
w(null);
|
|
51
67
|
}
|
|
52
|
-
}
|
|
68
|
+
}
|
|
69
|
+
function buildRl() {
|
|
70
|
+
const r = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "" });
|
|
71
|
+
r.on("line", deliver);
|
|
72
|
+
r.on("close", () => {
|
|
73
|
+
if (exiting || !process.stdin.isTTY) return endInput(); // thoát thật
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
if (now - closeAt > 1500) {
|
|
76
|
+
closeAt = now;
|
|
77
|
+
closeBurst = 0;
|
|
78
|
+
}
|
|
79
|
+
if (++closeBurst > 8) return endInput(); // stdin mất thật → khỏi quay vòng
|
|
80
|
+
// 'close' bất thường: dựng lại interface, hiện lại prompt; reader vẫn chờ.
|
|
81
|
+
rl = buildRl();
|
|
82
|
+
if (lastPrompt) {
|
|
83
|
+
rl.setPrompt(lastPrompt);
|
|
84
|
+
rl.prompt();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return r;
|
|
88
|
+
}
|
|
89
|
+
rl = buildRl();
|
|
53
90
|
|
|
54
|
-
// Robust input: every line is captured by the 'line' event (nothing is lost
|
|
55
|
-
// while a turn is processing) and handed out one at a time. Works for an
|
|
56
|
-
// interactive TTY and for piped / non-TTY stdin (Git Bash, CI, etc.).
|
|
57
91
|
function nextLine() {
|
|
58
92
|
if (queue.length) return Promise.resolve(queue.shift());
|
|
59
93
|
if (closed) return Promise.resolve(null);
|
|
60
94
|
return new Promise((res) => (waiter = res));
|
|
61
95
|
}
|
|
62
|
-
function ask(
|
|
96
|
+
function ask(prompt) {
|
|
63
97
|
if (closed && !queue.length) return Promise.resolve(null);
|
|
64
|
-
|
|
98
|
+
lastPrompt = prompt;
|
|
99
|
+
rl.setPrompt(prompt);
|
|
65
100
|
rl.prompt();
|
|
66
101
|
return nextLine();
|
|
67
102
|
}
|
|
68
103
|
|
|
69
|
-
// Shift+Tab —
|
|
104
|
+
// Shift+Tab — bật/tắt yolo nhanh (best-effort; gắn vào stdin nên sống qua mọi
|
|
105
|
+
// lần dựng lại rl; bọc try/catch để không bao giờ làm hỏng input).
|
|
70
106
|
if (process.stdin.isTTY) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
try {
|
|
108
|
+
readline.emitKeypressEvents(process.stdin);
|
|
109
|
+
process.stdin.on("keypress", (_str, key) => {
|
|
110
|
+
if (!key || key.name !== "tab" || !key.shift) return;
|
|
111
|
+
state.yolo = !state.yolo;
|
|
112
|
+
console.log(state.yolo ? c.err(" " + t.yoloOn) : c.ok(" " + t.yoloOff));
|
|
113
|
+
rl.setPrompt(promptStr(false)); // cập nhật dòng trạng thái ngay lập tức
|
|
114
|
+
rl.prompt(true);
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
/* không có Shift+Tab cũng được — vẫn dùng /yolo */
|
|
118
|
+
}
|
|
79
119
|
}
|
|
80
120
|
|
|
81
121
|
let abort = null; // active turn controller
|
|
@@ -111,6 +151,69 @@ export async function startRepl(opts = {}) {
|
|
|
111
151
|
if (!closed) rl.prompt(true);
|
|
112
152
|
});
|
|
113
153
|
|
|
154
|
+
// ── phiên (session): lưu lịch sử + resume giống Claude Code ───────────────
|
|
155
|
+
let session = null;
|
|
156
|
+
const persist = () => {
|
|
157
|
+
if (!session || !state.history.length) return; // đừng lưu phiên rỗng
|
|
158
|
+
session.history = state.history; // giữ đồng bộ tuyệt đối với history sống
|
|
159
|
+
session.model = state.model.id;
|
|
160
|
+
sessions.save(session);
|
|
161
|
+
};
|
|
162
|
+
async function restore(s) {
|
|
163
|
+
session = s;
|
|
164
|
+
state.history = s.history || [];
|
|
165
|
+
state.mode = "chat";
|
|
166
|
+
if (s.model) {
|
|
167
|
+
const m = findModel(s.model);
|
|
168
|
+
if (m) state.model = m;
|
|
169
|
+
}
|
|
170
|
+
console.log(c.ok(" ✓ " + t.sessionResumed(s.id)));
|
|
171
|
+
const turns = state.history.filter((m) => m.role === "user");
|
|
172
|
+
const tail = turns.slice(-5);
|
|
173
|
+
const base = turns.length - tail.length;
|
|
174
|
+
tail.forEach((m, i) => console.log(c.dim(` ${base + i + 1}. `) + truncate(m.content, 70)));
|
|
175
|
+
console.log("");
|
|
176
|
+
}
|
|
177
|
+
async function pickSession() {
|
|
178
|
+
const items = sessions.list(20);
|
|
179
|
+
if (!items.length) {
|
|
180
|
+
console.log(c.dim(" " + t.sessionEmpty) + "\n");
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
console.log("\n" + chalk.bold(" " + t.sessionPickTitle));
|
|
184
|
+
items.forEach((s, i) =>
|
|
185
|
+
console.log(
|
|
186
|
+
c.accent(` ${String(i + 1).padStart(2)}. `) +
|
|
187
|
+
chalk.bold(s.title || "(trống)") +
|
|
188
|
+
c.dim(` · ${s.turns} lượt · ${relTime(s.updatedAt)} · ${shortPath(s.cwd)}`),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
const ans = ((await ask(c.tool(" " + t.sessionPickPrompt(items.length)))) ?? "").trim();
|
|
192
|
+
if (!ans) return null;
|
|
193
|
+
const idx = parseInt(ans, 10) - 1;
|
|
194
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= items.length) {
|
|
195
|
+
console.log(c.err(" " + t.sessionPickBad) + "\n");
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const full = sessions.load(items[idx].id);
|
|
199
|
+
if (full) await restore(full);
|
|
200
|
+
return full;
|
|
201
|
+
}
|
|
202
|
+
function listSessions() {
|
|
203
|
+
const items = sessions.list(20);
|
|
204
|
+
if (!items.length) return console.log(c.dim(" " + t.sessionEmpty));
|
|
205
|
+
console.log("\n" + chalk.bold(" " + t.sessionListTitle));
|
|
206
|
+
items.forEach((s) =>
|
|
207
|
+
console.log(
|
|
208
|
+
c.dim(" " + s.id.padEnd(20)) +
|
|
209
|
+
chalk.bold(s.title || "(trống)") +
|
|
210
|
+
c.dim(` · ${s.turns} lượt · ${relTime(s.updatedAt)}`),
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
console.log(c.dim("\n " + t.sessionResumeHint) + "\n");
|
|
214
|
+
}
|
|
215
|
+
const startFresh = () => (session = sessions.newSession({ cwd: process.cwd(), model: state.model.id }));
|
|
216
|
+
|
|
114
217
|
banner();
|
|
115
218
|
printStatus(state);
|
|
116
219
|
if (!config.apiKey) console.log("\n" + c.tool(" " + t.notLoggedIn) + "\n");
|
|
@@ -128,9 +231,31 @@ export async function startRepl(opts = {}) {
|
|
|
128
231
|
.catch(() => {});
|
|
129
232
|
}
|
|
130
233
|
|
|
234
|
+
// Khôi phục phiên theo cờ dòng lệnh, hoặc mở phiên mới.
|
|
235
|
+
if (opts.continue) {
|
|
236
|
+
const s = sessions.latest();
|
|
237
|
+
if (s) await restore(s);
|
|
238
|
+
else {
|
|
239
|
+
startFresh();
|
|
240
|
+
console.log(c.dim(" " + t.sessionNonePrev) + "\n");
|
|
241
|
+
}
|
|
242
|
+
} else if (opts.resume === true) {
|
|
243
|
+
if (!(await pickSession())) startFresh();
|
|
244
|
+
} else if (typeof opts.resume === "string") {
|
|
245
|
+
const s = sessions.load(opts.resume);
|
|
246
|
+
if (s) await restore(s);
|
|
247
|
+
else {
|
|
248
|
+
console.log(c.err(" " + t.sessionNotFound(opts.resume)) + "\n");
|
|
249
|
+
startFresh();
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
startFresh();
|
|
253
|
+
}
|
|
254
|
+
|
|
131
255
|
if (opts.prompt) {
|
|
132
256
|
console.log(c.user(t.promptYou) + c.dim("› ") + opts.prompt);
|
|
133
257
|
await handle(opts.prompt);
|
|
258
|
+
persist();
|
|
134
259
|
}
|
|
135
260
|
|
|
136
261
|
// Main loop — runs until /exit, double Ctrl+C, or EOF. Never exits after a task.
|
|
@@ -149,10 +274,12 @@ export async function startRepl(opts = {}) {
|
|
|
149
274
|
continue;
|
|
150
275
|
}
|
|
151
276
|
await handle(input);
|
|
277
|
+
persist(); // lưu sau mỗi lượt → resume được kể cả khi tắt đột ngột
|
|
152
278
|
} catch (err) {
|
|
153
279
|
printError(err);
|
|
154
280
|
}
|
|
155
281
|
}
|
|
282
|
+
exiting = true;
|
|
156
283
|
rl.close();
|
|
157
284
|
process.exit(0);
|
|
158
285
|
|
|
@@ -296,12 +423,32 @@ export async function startRepl(opts = {}) {
|
|
|
296
423
|
break;
|
|
297
424
|
case "clear":
|
|
298
425
|
case "new":
|
|
426
|
+
persist(); // giữ lại phiên cũ trên đĩa
|
|
299
427
|
state.history = [];
|
|
428
|
+
startFresh(); // phiên mới (phiên cũ vẫn resume được)
|
|
300
429
|
console.clear();
|
|
301
430
|
banner();
|
|
302
431
|
printStatus(state);
|
|
303
432
|
console.log(c.dim(" " + t.ctxCleared + "\n"));
|
|
304
433
|
break;
|
|
434
|
+
case "resume":
|
|
435
|
+
if (arg) {
|
|
436
|
+
const s = sessions.load(arg);
|
|
437
|
+
if (s) await restore(s);
|
|
438
|
+
else console.log(c.err(" " + t.sessionNotFound(arg)));
|
|
439
|
+
} else {
|
|
440
|
+
await pickSession();
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
case "continue": {
|
|
444
|
+
const s = sessions.latest();
|
|
445
|
+
if (s) await restore(s);
|
|
446
|
+
else console.log(c.dim(" " + t.sessionNonePrev));
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case "sessions":
|
|
450
|
+
listSessions();
|
|
451
|
+
break;
|
|
305
452
|
case "cwd":
|
|
306
453
|
console.log(c.dim(" " + process.cwd()));
|
|
307
454
|
break;
|
|
@@ -315,6 +462,8 @@ export async function startRepl(opts = {}) {
|
|
|
315
462
|
case "exit":
|
|
316
463
|
case "quit":
|
|
317
464
|
case "q":
|
|
465
|
+
persist();
|
|
466
|
+
exiting = true;
|
|
318
467
|
console.log(c.dim(" " + t.bye));
|
|
319
468
|
return true;
|
|
320
469
|
default:
|
|
@@ -436,6 +585,8 @@ function printHelp() {
|
|
|
436
585
|
" " + t.cmdUsage,
|
|
437
586
|
" " + t.cmdUpdate,
|
|
438
587
|
" " + t.cmdClear,
|
|
588
|
+
" " + t.cmdResume,
|
|
589
|
+
" " + t.cmdSessions,
|
|
439
590
|
" " + t.cmdStatus,
|
|
440
591
|
" " + t.cmdVersion,
|
|
441
592
|
" " + t.cmdExit,
|
|
@@ -466,6 +617,15 @@ const shortCwd = () => {
|
|
|
466
617
|
const p = process.cwd();
|
|
467
618
|
return p.length > 48 ? "…" + p.slice(-47) : p;
|
|
468
619
|
};
|
|
620
|
+
const shortPath = (p = "") => (p.length > 30 ? "…" + p.slice(-29) : p);
|
|
621
|
+
const relTime = (ts) => {
|
|
622
|
+
const m = Math.round((Date.now() - ts) / 60000);
|
|
623
|
+
if (m < 1) return "vừa xong";
|
|
624
|
+
if (m < 60) return m + " phút trước";
|
|
625
|
+
const h = Math.round(m / 60);
|
|
626
|
+
if (h < 24) return h + " giờ trước";
|
|
627
|
+
return Math.round(h / 24) + " ngày trước";
|
|
628
|
+
};
|
|
469
629
|
const firstLine = (s) => (s.split("\n")[0] || "").slice(0, 100);
|
|
470
630
|
const truncate = (s = "", n = 120) => (s.length > n ? s.slice(0, n) + "…" : s).replace(/\n/g, "⏎");
|
|
471
631
|
const fmtTime = (iso) => {
|
package/src/sessions.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Lưu & khôi phục lịch sử hội thoại (giống --continue / --resume của Claude Code).
|
|
2
|
+
// Mỗi phiên là một file JSON trong ~/.noob/sessions/. Ghi sau mỗi lượt nên có rớt
|
|
3
|
+
// mạng / tắt máy vẫn resume được.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
|
|
8
|
+
const DIR = path.join(os.homedir(), ".noob", "sessions");
|
|
9
|
+
|
|
10
|
+
function ensure() {
|
|
11
|
+
try {
|
|
12
|
+
fs.mkdirSync(DIR, { recursive: true });
|
|
13
|
+
} catch {
|
|
14
|
+
/* best effort */
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function genId() {
|
|
19
|
+
const d = new Date();
|
|
20
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
21
|
+
const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
22
|
+
return `${stamp}-${Math.random().toString(36).slice(2, 6)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function titleFrom(history = []) {
|
|
26
|
+
const first = history.find((m) => m.role === "user");
|
|
27
|
+
if (!first) return "";
|
|
28
|
+
return String(first.content).replace(/\s+/g, " ").trim().slice(0, 60);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function newSession({ cwd, model } = {}) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
return {
|
|
34
|
+
id: genId(),
|
|
35
|
+
createdAt: now,
|
|
36
|
+
updatedAt: now,
|
|
37
|
+
cwd: cwd || process.cwd(),
|
|
38
|
+
model: model || "",
|
|
39
|
+
title: "",
|
|
40
|
+
history: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function save(session) {
|
|
45
|
+
if (!session || !session.id) return false;
|
|
46
|
+
ensure();
|
|
47
|
+
session.updatedAt = Date.now();
|
|
48
|
+
if (!session.title) session.title = titleFrom(session.history);
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(path.join(DIR, session.id + ".json"), JSON.stringify(session), "utf8");
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function load(id) {
|
|
58
|
+
if (!id) return null;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(fs.readFileSync(path.join(DIR, id + ".json"), "utf8"));
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Tóm tắt nhẹ (không tải toàn bộ history vào view), sắp xếp mới nhất trước. */
|
|
67
|
+
export function list(limit = 30) {
|
|
68
|
+
ensure();
|
|
69
|
+
let files;
|
|
70
|
+
try {
|
|
71
|
+
files = fs.readdirSync(DIR).filter((f) => f.endsWith(".json"));
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const f of files) {
|
|
77
|
+
try {
|
|
78
|
+
const s = JSON.parse(fs.readFileSync(path.join(DIR, f), "utf8"));
|
|
79
|
+
out.push({
|
|
80
|
+
id: s.id,
|
|
81
|
+
updatedAt: s.updatedAt || s.createdAt || 0,
|
|
82
|
+
cwd: s.cwd || "",
|
|
83
|
+
model: s.model || "",
|
|
84
|
+
title: s.title || titleFrom(s.history || []),
|
|
85
|
+
turns: (s.history || []).filter((m) => m.role === "user").length,
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
/* bỏ qua file hỏng */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
92
|
+
return out.slice(0, limit);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function latest() {
|
|
96
|
+
const l = list(1);
|
|
97
|
+
return l.length ? load(l[0].id) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function remove(id) {
|
|
101
|
+
try {
|
|
102
|
+
fs.unlinkSync(path.join(DIR, id + ".json"));
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const sessionsDir = DIR;
|
package/src/tools.js
CHANGED
|
@@ -119,7 +119,10 @@ export const TOOLS = {
|
|
|
119
119
|
const isWin = process.platform === "win32";
|
|
120
120
|
const shell = isWin ? "powershell.exe" : "/bin/bash";
|
|
121
121
|
const args = isWin ? ["-NoProfile", "-NonInteractive", "-Command", command] : ["-c", command];
|
|
122
|
-
|
|
122
|
+
// stdin: "ignore" — tiến trình con KHÔNG được chạm vào console/stdin của
|
|
123
|
+
// CLI. Nếu để con kế thừa stdin, trên Windows nó có thể làm readline phát
|
|
124
|
+
// 'close' → CLI tự tắt. Đóng hẳn stdin con để tránh hoàn toàn.
|
|
125
|
+
const child = spawn(shell, args, { cwd: cwd(), stdio: ["ignore", "pipe", "pipe"] });
|
|
123
126
|
let out = "";
|
|
124
127
|
const killer = setTimeout(() => child.kill(), timeout);
|
|
125
128
|
child.stdout.on("data", (d) => (out += d));
|