@noobdemon/noob-cli 1.5.1 → 1.5.3
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/package.json +1 -1
- package/src/api.js +63 -2
- package/src/repl.js +25 -15
- package/src/sessions.js +16 -4
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -41,10 +41,59 @@ async function parseError(resp) {
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Stream a chat/merge/search request from the gateway.
|
|
44
|
+
*
|
|
45
|
+
* Auto-continue (chat only): the chat upstream runs on Vercel, which kills the
|
|
46
|
+
* function at ~300s. A long reply gets cut mid-stream — the gateway flags this
|
|
47
|
+
* with `{truncated:true}` (or the connection just drops with no `{done}`). When
|
|
48
|
+
* that happens we re-send the SAME transcript plus the partial reply so far and
|
|
49
|
+
* ask the model to write ONLY the rest, then append it. The caller sees one
|
|
50
|
+
* seamless stream. Capped by `maxContinues` so a genuinely broken upstream can't
|
|
51
|
+
* loop forever.
|
|
52
|
+
*
|
|
44
53
|
* @returns {Promise<{text:string, reasoning:string}>}
|
|
45
54
|
*/
|
|
46
|
-
export async function stream({ mode = "chat", message, model, signal, onDelta, onReasoning, onStatus, idleMs = 120000 }) {
|
|
55
|
+
export async function stream({ mode = "chat", message, model, signal, onDelta, onReasoning, onStatus, idleMs = 120000, maxContinues = 6 }) {
|
|
47
56
|
const endpoint = mode === "search" ? "/api/search" : mode === "merge" ? "/api/merge" : "/api/chat";
|
|
57
|
+
|
|
58
|
+
let fullText = "";
|
|
59
|
+
let reasoning = "";
|
|
60
|
+
let prompt = message; // prompt gửi đi: lần đầu = nguyên bản, các lần sau = nối tiếp
|
|
61
|
+
|
|
62
|
+
for (let attempt = 0; ; attempt++) {
|
|
63
|
+
const r = await streamOnce({ endpoint, mode, message: prompt, model, signal, idleMs, onStatus, onDelta, onReasoning });
|
|
64
|
+
fullText = mode === "chat" ? fullText + r.text : r.text; // chat: ghép các đoạn nối tiếp; mode khác: thay thế
|
|
65
|
+
if (r.reasoning) reasoning = r.reasoning;
|
|
66
|
+
|
|
67
|
+
// Còn nối tiếp được không? Chỉ với chat, khi bị cắt, còn lượt, và lần này có
|
|
68
|
+
// ra chữ thật (đoạn rỗng → coi như xong, tránh lặp vô tận).
|
|
69
|
+
if (!r.truncated || mode !== "chat" || attempt >= maxContinues || !r.text.trim()) break;
|
|
70
|
+
prompt = continuationPrompt(message, fullText);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { text: fullText.trim(), reasoning: reasoning.trim() };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Dựng prompt "nối tiếp" khi câu trả lời bị cắt giữa chừng: gửi lại nguyên ngữ
|
|
77
|
+
// cảnh gốc + phần model đã viết dở + yêu cầu viết TIẾP đúng chỗ dừng, không lặp.
|
|
78
|
+
function continuationPrompt(message, partial) {
|
|
79
|
+
const bar = "=".repeat(60);
|
|
80
|
+
return (
|
|
81
|
+
message +
|
|
82
|
+
"\n\n" + bar +
|
|
83
|
+
"\n## ASSISTANT (bị ngắt giữa chừng — phần trả lời dưới đây CHƯA hoàn tất)\n" +
|
|
84
|
+
partial +
|
|
85
|
+
"\n\n" + bar +
|
|
86
|
+
"\n# SYSTEM: Phần trả lời ngay trên bị mạng/timeout cắt ngang trước khi xong. " +
|
|
87
|
+
"Hãy VIẾT TIẾP liền mạch từ ĐÚNG ký tự cuối cùng ở trên — KHÔNG lặp lại hay diễn đạt lại bất kỳ chữ nào đã hiện, KHÔNG mở đầu lại, KHÔNG thêm lời dẫn. " +
|
|
88
|
+
"Chỉ xuất phần CÒN LẠI. Nếu đang viết dở một khối tool thì hoàn tất đúng khối đó. Nếu thật ra đã xong, chỉ xuất một dấu cách rồi dừng."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One network attempt of the stream. Returns this attempt's accumulated text +
|
|
94
|
+
* a `truncated` flag telling the caller whether the reply was cut short.
|
|
95
|
+
*/
|
|
96
|
+
async function streamOnce({ endpoint, mode, message, model, signal, idleMs, onStatus, onDelta, onReasoning }) {
|
|
48
97
|
const body = mode === "search" ? { query: message } : mode === "merge" ? { message } : { message, model };
|
|
49
98
|
|
|
50
99
|
// Idle-timeout: nếu KHÔNG nhận được byte nào trong idleMs (kết nối treo), tự
|
|
@@ -65,6 +114,8 @@ export async function stream({ mode = "chat", message, model, signal, onDelta, o
|
|
|
65
114
|
|
|
66
115
|
let text = "";
|
|
67
116
|
let reasoning = "";
|
|
117
|
+
let sawDone = false; // thấy {done} = stream kết thúc tử tế (không bị cắt)
|
|
118
|
+
let truncated = false; // gateway báo upstream bị cắt giữa chừng (Vercel 300s)
|
|
68
119
|
|
|
69
120
|
// Một dòng SSE → cập nhật text/reasoning. Tách ra để dùng lại khi flush dòng cuối.
|
|
70
121
|
const processLine = (rawLine) => {
|
|
@@ -88,6 +139,8 @@ export async function stream({ mode = "chat", message, model, signal, onDelta, o
|
|
|
88
139
|
onReasoning?.(p.reasoning);
|
|
89
140
|
if (p.answer) text = p.answer;
|
|
90
141
|
}
|
|
142
|
+
if (p.truncated) truncated = true;
|
|
143
|
+
if (p.done) sawDone = true;
|
|
91
144
|
if (p.error) throw new ApiError(p.error, {});
|
|
92
145
|
};
|
|
93
146
|
|
|
@@ -118,9 +171,17 @@ export async function stream({ mode = "chat", message, model, signal, onDelta, o
|
|
|
118
171
|
buf += decoder.decode(); // flush decoder
|
|
119
172
|
if (buf.trim()) processLine(buf); // dòng SSE cuối không có '\n' — đừng bỏ sót
|
|
120
173
|
|
|
121
|
-
|
|
174
|
+
// Chat: gateway gửi {done} khi xong sạch. Stream EOF mà chưa thấy {done} dù
|
|
175
|
+
// đã có chữ → kết nối/edge rớt giữa chừng → coi như bị cắt để nối tiếp.
|
|
176
|
+
if (mode === "chat" && !sawDone && text) truncated = true;
|
|
177
|
+
|
|
178
|
+
return { text, reasoning, truncated };
|
|
122
179
|
} catch (err) {
|
|
180
|
+
if (signal?.aborted) throw err; // người dùng bấm Ctrl+C → huỷ thật, không nối tiếp
|
|
123
181
|
if (timedOut) throw new ApiError("Kết nối tới máy chủ quá thời gian chờ (treo).", { code: "timeout" });
|
|
182
|
+
// Rớt mạng giữa chừng (không phải huỷ, không phải treo): với chat, nếu đã có
|
|
183
|
+
// chữ thì trả phần đã nhận + cờ truncated để lớp trên nối tiếp.
|
|
184
|
+
if (mode === "chat" && text) return { text, reasoning, truncated: true };
|
|
124
185
|
throw err;
|
|
125
186
|
} finally {
|
|
126
187
|
clearTimeout(idle);
|
package/src/repl.js
CHANGED
|
@@ -282,7 +282,7 @@ export async function startRepl(opts = {}) {
|
|
|
282
282
|
console.log("");
|
|
283
283
|
}
|
|
284
284
|
async function pickSession() {
|
|
285
|
-
const items = sessions.list(20);
|
|
285
|
+
const items = sessions.list(20, process.cwd()); // chỉ phiên của workspace hiện tại
|
|
286
286
|
if (!items.length) {
|
|
287
287
|
console.log(c.dim(" " + t.sessionEmpty) + "\n");
|
|
288
288
|
return null;
|
|
@@ -307,7 +307,7 @@ export async function startRepl(opts = {}) {
|
|
|
307
307
|
return full;
|
|
308
308
|
}
|
|
309
309
|
function listSessions() {
|
|
310
|
-
const items = sessions.list(20);
|
|
310
|
+
const items = sessions.list(20, process.cwd()); // chỉ phiên của workspace hiện tại
|
|
311
311
|
if (!items.length) return console.log(c.dim(" " + t.sessionEmpty));
|
|
312
312
|
console.log("\n" + chalk.bold(" " + t.sessionListTitle));
|
|
313
313
|
items.forEach((s) =>
|
|
@@ -457,7 +457,7 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
|
|
|
457
457
|
|
|
458
458
|
// Khôi phục phiên theo cờ dòng lệnh, hoặc mở phiên mới.
|
|
459
459
|
if (opts.continue) {
|
|
460
|
-
const s = sessions.latest();
|
|
460
|
+
const s = sessions.latest(process.cwd()); // phiên gần nhất CỦA workspace này
|
|
461
461
|
if (s) await restore(s);
|
|
462
462
|
else {
|
|
463
463
|
startFresh();
|
|
@@ -663,18 +663,28 @@ Tự đánh giá: còn THIẾU gì để đạt mục tiêu? Chọn 1 nhiệm v
|
|
|
663
663
|
// "tự tắt" trước đây) — thì KHÔNG coi là từ chối: xếp nó vào hàng đợi tin
|
|
664
664
|
// nhắn rồi HỎI LẠI. Nhờ vậy không thao tác nào bị quyết định bởi rác.
|
|
665
665
|
async function askPermission(name) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
666
|
+
// Tắt spinner "đang chạy" trong lúc chờ duyệt. Nếu để nguyên, thanh dưới hiện
|
|
667
|
+
// "đang chạy · Ctrl+C để dừng" → người dùng tưởng đang bận, không biết phải gõ
|
|
668
|
+
// y/n nên lượt TREO. Báo bằng 1 dòng cố định (vào scrollback, không bị vẽ đè)
|
|
669
|
+
// + bỏ spinner để prompt nổi bật. finally khôi phục trạng thái chạy.
|
|
670
|
+
tui.setBusy(false);
|
|
671
|
+
console.log(c.tool(" ⏸ Cần quyền: " + name) + c.dim(" — gõ y (đồng ý) / n (từ chối) / a (luôn cho phép)"));
|
|
672
|
+
try {
|
|
673
|
+
while (true) {
|
|
674
|
+
const raw = await ask(c.tool(" cho phép? ") + c.dim("[y] có / [n] không / [a] luôn " + name + " › "));
|
|
675
|
+
if (raw == null) return "n"; // stdin đóng thật
|
|
676
|
+
const a = raw.trim().toLowerCase();
|
|
677
|
+
if (a === "" || a === "y" || a === "yes" || a === "có") return "y";
|
|
678
|
+
if (a === "n" || a === "no" || a === "không") return "n";
|
|
679
|
+
if (a === "a" || a === "always" || a === "luôn") return "a";
|
|
680
|
+
if (raw.trim().length > 3) {
|
|
681
|
+
pending.push(raw); // tin nhắn lạc → xếp hàng, gửi sau khi xong lượt
|
|
682
|
+
console.log(c.dim(" " + t.queued(pending.length, truncate(raw, 60))));
|
|
683
|
+
}
|
|
684
|
+
console.log(c.dim(" " + t.permRetry));
|
|
676
685
|
}
|
|
677
|
-
|
|
686
|
+
} finally {
|
|
687
|
+
tui.setBusy(true, t.thinking); // khôi phục "đang chạy" cho phần còn lại của lượt
|
|
678
688
|
}
|
|
679
689
|
}
|
|
680
690
|
|
|
@@ -926,7 +936,7 @@ function printError(err) {
|
|
|
926
936
|
}
|
|
927
937
|
|
|
928
938
|
function printUsage(u) {
|
|
929
|
-
const planName = { pro: "Pro", proplus: "Pro+", admin: "Admin", trial: "Trial" }[u.plan] || u.plan;
|
|
939
|
+
const planName = { pro: "Pro", proplus: "Pro+", ultra: "Ultra", admin: "Admin", trial: "Trial" }[u.plan] || u.plan;
|
|
930
940
|
const lines = [
|
|
931
941
|
chalk.bold(t.usageTitle),
|
|
932
942
|
` ${t.plan}: ${chalk.bold(planName)} ${t.status}: ${u.status === "active" ? c.ok(u.status) : c.err(u.status)}`,
|
package/src/sessions.js
CHANGED
|
@@ -63,9 +63,20 @@ export function load(id) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
// Chuẩn hoá thư mục workspace để so khớp: resolve + lowercase trên Windows nên
|
|
67
|
+
// "D:\x", "D:\x\" và "d:\x" coi là một.
|
|
68
|
+
const normDir = (p) => {
|
|
69
|
+
const r = path.resolve(p || "");
|
|
70
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 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.
|
|
75
|
+
* cwd != null → chỉ trả phiên thuộc đúng thư mục workspace đó (resume theo dự án).
|
|
76
|
+
*/
|
|
77
|
+
export function list(limit = 30, cwd = null) {
|
|
68
78
|
ensure();
|
|
79
|
+
const want = cwd != null ? normDir(cwd) : null;
|
|
69
80
|
let files;
|
|
70
81
|
try {
|
|
71
82
|
files = fs.readdirSync(DIR).filter((f) => f.endsWith(".json"));
|
|
@@ -76,6 +87,7 @@ export function list(limit = 30) {
|
|
|
76
87
|
for (const f of files) {
|
|
77
88
|
try {
|
|
78
89
|
const s = JSON.parse(fs.readFileSync(path.join(DIR, f), "utf8"));
|
|
90
|
+
if (want != null && normDir(s.cwd) !== want) continue; // khác workspace → bỏ
|
|
79
91
|
out.push({
|
|
80
92
|
id: s.id,
|
|
81
93
|
updatedAt: s.updatedAt || s.createdAt || 0,
|
|
@@ -92,8 +104,8 @@ export function list(limit = 30) {
|
|
|
92
104
|
return out.slice(0, limit);
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
export function latest() {
|
|
96
|
-
const l = list(1);
|
|
107
|
+
export function latest(cwd = null) {
|
|
108
|
+
const l = list(1, cwd);
|
|
97
109
|
return l.length ? load(l[0].id) : null;
|
|
98
110
|
}
|
|
99
111
|
|