@noobdemon/noob-cli 1.13.2 → 1.13.4

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,25 @@
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.13.4] - 2026-06-29
6
+
7
+ ### Added
8
+ - **Ctrl+Backspace / Ctrl+W xoá nguyên từ** (`src/tui.js`): trên thanh nhập, Ctrl+Backspace (`\x08`/Ctrl+H ở phần lớn terminal) và Ctrl+W xoá cả từ trước con trỏ thay vì từng ký tự. Dùng cùng quy ước ranh giới từ với di chuyển từ (khoảng trắng + chip ảnh dán tính là ranh giới).
9
+
10
+ ### Changed
11
+ - **Chặn slash command khi model đang chạy** (`src/repl.js` + `src/i18n.js`): gõ slash (`/kc`, `/model`, …) lúc task đang chạy không còn bị xếp hàng / chèn nhầm cho AI — in cảnh báo `⏳ model đang chạy — chờ xong task mới dùng được lệnh` và bỏ qua. Tin nhắn thường vẫn xếp hàng/chèn cho AI như cũ.
12
+
13
+ ### Verified
14
+ - `npm test` 109/109 pass.
15
+
16
+ ## [1.13.3] - 2026-06-29
17
+
18
+ ### Fixed
19
+ - **Message nhắc việc nội bộ không còn gây nhiễu mô hình** (`src/agent.js`): các message nhắc tiếp tục công việc (nhắc todo còn dở, hướng dẫn khi tool lỗi, cảnh báo gọi trùng tool) trước đây được chèn vào hội thoại như thể là tin nhắn người dùng và mở đầu bằng nhãn `[SYSTEM]`. Mô hình ở lượt sau đọc lại tưởng có người chèn lệnh giả dạng hệ thống vào cuộc trò chuyện nên sinh nghi ngờ, đi sửa lung tung. Giờ các message này đi đúng kênh kết quả tool và đổi nhãn thành `[TODO]`/`[TOOL GUIDANCE]`.
20
+
21
+ ### Verified
22
+ - `npm test` 109/109 pass.
23
+
5
24
  ## [1.13.2] - 2026-06-28
6
25
 
7
26
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.13.2",
3
+ "version": "1.13.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -479,18 +479,18 @@ export function todoContinuationMessage(toolName, ok, tasks) {
479
479
  if (!tasks?.length) return null;
480
480
  const next = tasks[0];
481
481
  const status = ok ? `đã hoàn thành` : `vừa LỖI`;
482
- return `[SYSTEM] Việc "${toolName}" ${status}. Còn ${tasks.length} việc: ${tasks.map((t) => `"${t}"`).join(', ')}. Việc tiếp theo BẮT BUỘC phải làm ngay: "${next}". Gọi tool (write_file/edit_file/run_command) để làm việc này. KHÔNG dừng, KHÔNG tóm tắt.`;
482
+ return `[TODO] Việc "${toolName}" ${status}. Còn ${tasks.length} việc: ${tasks.map((t) => `"${t}"`).join(', ')}. Việc tiếp theo BẮT BUỘC phải làm ngay: "${next}". Gọi tool (write_file/edit_file/run_command) để làm việc này. KHÔNG dừng, KHÔNG tóm tắt.`;
483
483
  }
484
484
 
485
485
  export function toolErrorGuidance(name, { result } = {}) {
486
486
  if (name !== 'edit_file' || !String(result || '').includes('old_string not found')) return null;
487
487
  const m = String(result).match(/NEXT REQUIRED TOOL:\s*(read_file \{[^\n]+\})/);
488
488
  if (!m) return null;
489
- return `[SYSTEM] edit_file thất bại vì old_string sai. Bạn PHẢI gọi đúng tool này ngay: ${m[1]}. KHÔNG gọi edit_file lại trước khi đọc file. Sau đó copy đúng text từ read_file mới để retry.`;
489
+ return `[TOOL GUIDANCE] edit_file thất bại vì old_string sai. Bạn PHẢI gọi đúng tool này ngay: ${m[1]}. KHÔNG gọi edit_file lại trước khi đọc file. Sau đó copy đúng text từ read_file mới để retry.`;
490
490
  }
491
491
 
492
492
  export function duplicateToolGuidance(name, input) {
493
- return `[SYSTEM] Tool call vừa rồi TRÙNG HỆT lần trước: ${name} ${JSON.stringify(input || {})}. KHÔNG chạy tool này lại. Dùng kết quả đã có trong history; nếu cần tiến thêm thì gọi tool khác (grep/read_file/edit_file/run_command) hoặc trả lời Markdown nếu xong.`;
493
+ return `[TOOL GUIDANCE] Tool call vừa rồi TRÙNG HỆT lần trước: ${name} ${JSON.stringify(input || {})}. KHÔNG chạy tool này lại. Dùng kết quả đã có trong history; nếu cần tiến thêm thì gọi tool khác (grep/read_file/edit_file/run_command) hoặc trả lời Markdown nếu xong.`;
494
494
  }
495
495
 
496
496
  // Detect câu trả lời bị cắt giữa chừng — KHÔNG phải câu hoàn chỉnh.
@@ -722,7 +722,7 @@ export async function runAgent({
722
722
  });
723
723
  const toolOk = allow && !String(result || '').startsWith('ERROR:');
724
724
  const errorNudge = allow ? toolErrorGuidance(call.name, { result }) : null;
725
- if (errorNudge) history.push({ role: 'user', content: errorNudge });
725
+ if (errorNudge) history.push({ role: 'tool', name: 'tool_guidance', content: errorNudge });
726
726
 
727
727
  // ── Todo continuation nudge ──────────────────────────────────────────
728
728
  // Sau mỗi tool result, inject nudge nếu còn task chưa xong.
@@ -730,7 +730,7 @@ export async function runAgent({
730
730
  {
731
731
  const tasks = pendingTasks || [];
732
732
  const msg = todoContinuationMessage(call.name, toolOk, tasks);
733
- if (msg) history.push({ role: 'user', content: msg });
733
+ if (msg) history.push({ role: 'tool', name: 'todo_continuation', content: msg });
734
734
  }
735
735
 
736
736
  // ── Loop detection ──────────────────────────────────────────────────
package/src/i18n.js CHANGED
@@ -12,6 +12,7 @@ export const t = {
12
12
  running: 'đang chạy…',
13
13
  denied: 'đã từ chối',
14
14
  queued: (n, txt) => `⏎ đã xếp hàng [${n}] · gửi khi model xong: ${txt}`,
15
+ slashBusy: '⏳ model đang chạy — chờ xong task mới dùng được lệnh (Ctrl+C để dừng)',
15
16
  queueCleared: (n) => `(đã xoá ${n} tin đang xếp hàng)`,
16
17
  steerHint:
17
18
  '💬 Gõ + Enter bất cứ lúc nào để chèn ý cho AI giữa chừng (không ngắt task đang chạy).',
package/src/repl.js CHANGED
@@ -164,6 +164,12 @@ export async function startRepl(opts = {}) {
164
164
  onLine: (line) => {
165
165
  // Submit khi KHÔNG có read() đang chờ = tin xếp hàng. Đang chạy task → sẽ
166
166
  // CHÈN cho AI ở bước kế tiếp (steering); rảnh → gửi như lượt mới.
167
+ // ponytail: slash command lúc busy KHÔNG vào pending — chặn, bắt user chờ
168
+ // model xong task mới dùng lệnh (tránh lệnh bị xếp hàng / chèn nhầm cho AI).
169
+ if (abort && line.trim().startsWith('/')) {
170
+ tui.print(c.dim(' ' + t.slashBusy));
171
+ return;
172
+ }
167
173
  pending.push(line);
168
174
  tui.print(
169
175
  abort
@@ -1869,7 +1875,16 @@ NGUYÊN TẮC:
1869
1875
  return await execToolCore(name, input, { retried: true });
1870
1876
  }
1871
1877
  console.log(c.err(' ✗ ' + err.message));
1872
- return { allow: true, result: 'ERROR: ' + err.message };
1878
+ // Framing: lỗi tool = GỌI SAI CÁCH, không phải mất quyền. Model có lịch sử
1879
+ // diễn giải nhầm 'ERROR:' thành 'mình không có quyền truy cập tool' rồi bỏ
1880
+ // cuộc. Nói rõ model VẪN có quyền, chỉ cần sửa tham số rồi gọi lại.
1881
+ return {
1882
+ allow: true,
1883
+ result:
1884
+ 'ERROR: ' +
1885
+ err.message +
1886
+ '\n[Đây là lỗi do tham số gọi tool chưa đúng, KHÔNG phải mất quyền truy cập. Bạn VẪN có quyền chạy tool này — đọc kỹ thông báo lỗi, sửa tham số (path/old_string/...) rồi GỌI LẠI. Đừng kết luận là không truy cập được.]',
1887
+ };
1873
1888
  }
1874
1889
  }
1875
1890
 
package/src/tools.js CHANGED
@@ -373,7 +373,10 @@ export const TOOLS = {
373
373
  checkAbort(signal);
374
374
  const file = abs(p);
375
375
  const data = await fs.readFile(file, 'utf8');
376
- if (old_string === new_string) throw new Error('old_string and new_string are identical');
376
+ if (old_string === new_string)
377
+ throw new Error(
378
+ `old_string and new_string are identical in ${rel(file)} — no change to make. This is a CALL mistake, NOT a permissions/access problem: you DO have edit access. Set new_string to the DIFFERENT text you want, then call edit_file again.`
379
+ );
377
380
  const useCRLF = data.includes('\r\n');
378
381
  const adapt = (s) => {
379
382
  const lf = s.replace(/\r\n/g, '\n');
package/src/tui.js CHANGED
@@ -322,9 +322,7 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
322
322
  if (x.image) return c.dim(imageLabel(x));
323
323
  if (x.paste === undefined) return x.c;
324
324
  const preview = pastePreview(x.paste);
325
- const label = preview
326
- ? `[pasted ${x.lines} lines: "${preview}"]`
327
- : `[pasted ${x.lines} lines]`;
325
+ const label = preview ? `[pasted ${x.lines} lines: "${preview}"]` : `[pasted ${x.lines} lines]`;
328
326
  return c.dim(label);
329
327
  };
330
328
  const coloredInput = () => {
@@ -424,7 +422,8 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
424
422
  let body = l;
425
423
  if (i === curLine) {
426
424
  if (colInLine >= l.length) body = l + `${REV} ${UNREV}`;
427
- else body = l.slice(0, colInLine) + `${REV}${l[colInLine]}${UNREV}` + l.slice(colInLine + 1);
425
+ else
426
+ body = l.slice(0, colInLine) + `${REV}${l[colInLine]}${UNREV}` + l.slice(colInLine + 1);
428
427
  }
429
428
  return (i === 0 ? promptLabel : indent) + body;
430
429
  })
@@ -716,6 +715,19 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
716
715
  histPos = null;
717
716
  }
718
717
  }
718
+ // Ctrl+Backspace / Ctrl+W: xoá nguyên từ trước con trỏ (cùng quy ước ranh giới
719
+ // từ với moveWordLeft — khoảng trắng + chip dán tính là ranh giới).
720
+ function deleteWordLeft() {
721
+ const start = cur;
722
+ while (cur > 0 && charAt(cur - 1) === ' ') cur -= 1;
723
+ if (cur > 0 && charAt(cur - 1) === null)
724
+ cur -= 1; // qua 1 chip
725
+ else while (cur > 0 && charAt(cur - 1) !== null && charAt(cur - 1) !== ' ') cur -= 1;
726
+ if (cur < start) {
727
+ cells.splice(cur, start - cur);
728
+ histPos = null;
729
+ }
730
+ }
719
731
  // null = ô dán (coi như ranh giới từ); ngược lại trả ký tự của ô.
720
732
  const charAt = (k) =>
721
733
  cells[k] && cells[k].paste === undefined && !cells[k].image ? cells[k].c : null;
@@ -923,7 +935,15 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
923
935
  i++;
924
936
  continue;
925
937
  }
926
- if (ch === '\x7f' || ch === '\b') {
938
+ if (ch === '\x08' || ch === '\x17') {
939
+ // Ctrl+Backspace (nhiều terminal gửi \x08 = Ctrl+H) và Ctrl+W: xoá 1 từ.
940
+ deleteWordLeft();
941
+ refreshMenu();
942
+ draw();
943
+ i++;
944
+ continue;
945
+ } // Ctrl+Backspace / Ctrl+W
946
+ if (ch === '\x7f') {
927
947
  backspace();
928
948
  refreshMenu();
929
949
  draw();