@noobdemon/noob-cli 1.11.0 → 1.11.1

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
4
4
 
5
+ ## [1.11.1] - 2026-06-10
6
+
7
+ ### Fixed
8
+ - **Câu trả lời không còn bị cắt cụt khi viết về web search / SEO** (`src/api.js`): regex dọn block `## Web Search Results` chỉ chạy ở `/search` mode. Trước đây nó chạy luôn ở chat → nếu bạn đang viết content/SEO có heading dạng đó, phần còn lại của câu trả lời bị nuốt mất.
9
+ - **SSE stream bền hơn với reverse proxy CRLF**: chuẩn hoá `\r\n` → `\n` trước khi parse, tránh dòng SSE bị bỏ sót khi gateway trả line-ending hỗn tạp.
10
+ - **Cờ `--insecure-tls` áp dụng đúng thời điểm** (`src/api.js`): bỏ lệnh áp dụng top-level (chạy trước parse argv vì ESM hoist). Giờ chỉ áp dụng đúng một lần từ `bin/noob.js` sau khi đọc argv.
11
+ - **Không còn `MaxListenersExceededWarning` khi chạy test / dynamic import** (`src/tools.js`): handler cleanup tiến trình nền (`exit` + `SIGTERM`) có guard chống đăng ký kép.
12
+
13
+ ### Chore
14
+ - **`.gitignore` / `.npmignore`**: bổ sung `*.bak`, `*.swp`, `Thumbs.db`, `Desktop.ini`, lockfile backup của npm/yarn/pnpm — defence-in-depth, không ảnh hưởng nội dung package đã publish.
15
+
5
16
  ## [1.11.0] - 2026-06-10
6
17
 
7
18
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/api.js CHANGED
@@ -45,8 +45,10 @@ export function applyInsecureTLS() {
45
45
  '\x1b[33m⚠ NOOB_INSECURE_TLS=1: TLS verification DISABLED for this process. MITM-vulnerable. Unset when done.\x1b[0m'
46
46
  );
47
47
  }
48
- // Vẫn áp dụng ngay nếu env sẵn từ shell (export NOOB_INSECURE_TLS=1).
49
- applyInsecureTLS();
48
+ // KHÔNG gọi applyInsecureTLS() top-level: ESM hoist, lệnh này sẽ chạy
49
+ // TRƯỚC khi bin/noob.js kịp parse `--insecure-tls` và set env. bin/noob.js đã
50
+ // gọi applyInsecureTLS() sau khi parse argv — đó là điểm duy nhất.
51
+ // Nếu env NOOB_INSECURE_TLS=1 đã có sẵn từ shell, bin/noob.js cũng sẽ áp dụng.
50
52
 
51
53
  function authHeaders() {
52
54
  const h = { 'Content-Type': 'application/json' };
@@ -98,7 +100,13 @@ async function parseError(resp) {
98
100
  // Unlimited.surf & similar proxies inject web search results as XML/markdown blocks
99
101
  // into the SSE stream. These get appended as regular text and confuse the AI model
100
102
  // when seen in subsequent turns (it thinks they are prompt injection attempts).
101
- function cleanResponseText(text) {
103
+ //
104
+ // QUAN TRỌNG: chỉ chạy stripping markdown-heading khi mode === 'search'. Với
105
+ // chat/merge, người dùng có thể đang viết về chính chủ đề "Web Search Results"
106
+ // (vd nội dung SEO/content) — regex tham sẽ nuốt mất câu trả lời thật.
107
+ // Các tag XML/bracket/plain-marker thì hiếm khi xuất hiện tự nhiên nên vẫn an
108
+ // toàn để strip ở mọi mode.
109
+ function cleanResponseText(text, mode = 'chat') {
102
110
  if (!text) return text;
103
111
  let cleaned = text;
104
112
  // XML/SGML style: <web_search_results>...</web_search_results>
@@ -110,11 +118,15 @@ function cleanResponseText(text) {
110
118
  // Plain text markers: web_search_results ... web_search_results_end
111
119
  cleaned = cleaned.replace(/web_search_results[\s\S]*?web_search_results_end/gi, '');
112
120
  cleaned = cleaned.replace(/web_search_summary[\s\S]*?web_search_summary_end/gi, '');
113
- // Markdown headings: ## Web Search Results / ## Web Search Summary (with content until next heading)
114
- cleaned = cleaned.replace(
115
- /^#{1,3}\s+Web\s+Search\s+(Results|Summary)\s*[\s\S]*?(?=^#{1,3}|\n# |$)/gim,
116
- ''
117
- );
121
+ // Markdown headings: chỉ áp dụng cho search mode (nguồn duy nhất bị inject
122
+ // dạng heading bởi proxy). Lookahead bám sát: dừng tại heading kế tiếp HOẶC
123
+ // 2 dòng trống liên tiếp (đoạn văn mới) — tránh nuốt phần còn lại của câu trả lời.
124
+ if (mode === 'search') {
125
+ cleaned = cleaned.replace(
126
+ /^#{1,3}\s+Web\s+Search\s+(Results|Summary)\b[^\n]*\n[\s\S]*?(?=^#{1,3}\s|\n\n\n|$)/gim,
127
+ ''
128
+ );
129
+ }
118
130
  return cleaned.trim();
119
131
  }
120
132
 
@@ -213,7 +225,7 @@ export async function stream({
213
225
  }
214
226
 
215
227
  return {
216
- text: cleanResponseText(fullText.trim()),
228
+ text: cleanResponseText(fullText.trim(), mode),
217
229
  reasoning: reasoning.trim(),
218
230
  finishReason: lastFinishReason,
219
231
  };
@@ -321,6 +333,11 @@ async function streamOnce({
321
333
  const { done, value } = await reader.read();
322
334
  if (done) break;
323
335
  buf += decoder.decode(value, { stream: true });
336
+ // Chuẩn hoá CRLF→LF: một số reverse proxy (Cloudflare, nginx config khác)
337
+ // gửi line endings \r\n. processLine() có trim() nên \r trailing tự rụng,
338
+ // nhưng nếu chunk biên giới rơi đúng giữa \r và \n thì vẫn ổn — đây là
339
+ // safety belt cho trường hợp \r đơn lẻ (rất hiếm nhưng có thấy thực tế).
340
+ buf = buf.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
324
341
  let nl;
325
342
  while ((nl = buf.indexOf('\n')) !== -1) {
326
343
  processLine(buf.slice(0, nl));
package/src/tools.js CHANGED
@@ -217,11 +217,19 @@ function killBgTree(child) {
217
217
  function cleanupBg() {
218
218
  for (const p of bg.values()) killBgTree(p.child);
219
219
  }
220
- process.on('exit', cleanupBg);
221
- process.on('SIGTERM', () => {
220
+ function onSigterm() {
222
221
  cleanupBg();
223
222
  process.exit(143);
224
- });
223
+ }
224
+ // Guard chống đăng ký kép: vitest hot-reload / dynamic import có thể re-evaluate
225
+ // module này → nhiều listener → MaxListenersExceededWarning + leak. Chỉ add khi
226
+ // chưa có handler chính xác này.
227
+ if (!process.listeners('exit').includes(cleanupBg)) {
228
+ process.on('exit', cleanupBg);
229
+ }
230
+ if (!process.listeners('SIGTERM').includes(onSigterm)) {
231
+ process.on('SIGTERM', onSigterm);
232
+ }
225
233
 
226
234
  // Helper: throw nếu signal đã abort. Dùng ở đầu mỗi tool + giữa các vòng walk dài
227
235
  // để tool fs (glob/grep) cũng phản ứng với Ctrl+C, không chỉ run_command.