@noobdemon/noob-cli 1.13.3 → 1.13.5

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,23 @@
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.5] - 2026-06-30
6
+
7
+ ### Fixed
8
+ - **Backspace / Ctrl+Backspace đảo đúng chiều** (`src/tui.js`): Backspace xoá 1 ký tự, Ctrl+Backspace/Ctrl+W xoá nguyên từ (trước đây bị ngược).
9
+ - **`/usage` hiện `undefined`** (`src/repl.js` + `worker/migration-key-status-fields.sql`): `printUsage` thay null bằng `?`; SQL refresh `key_status()` deployed để trả `remaining`/`limit`/`window_count`.
10
+
11
+ ## [1.13.4] - 2026-06-29
12
+
13
+ ### Added
14
+ - **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).
15
+
16
+ ### Changed
17
+ - **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ũ.
18
+
19
+ ### Verified
20
+ - `npm test` 109/109 pass.
21
+
5
22
  ## [1.13.3] - 2026-06-29
6
23
 
7
24
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.13.3",
3
+ "version": "1.13.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
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
 
@@ -2301,10 +2316,13 @@ function printUsage(u) {
2301
2316
  if (u.plan === 'admin') lines.push(` ${t.remaining}: ${c.ok(t.unlimited)}`);
2302
2317
  else if (u.plan === 'trial')
2303
2318
  lines.push(` ${t.remaining}: ${c.accent(t.trialLeft(u.remaining ?? 0))}`);
2304
- else
2319
+ else {
2320
+ const rem = u.remaining ?? '?';
2321
+ const lim = u.limit ?? '?';
2305
2322
  lines.push(
2306
- ` ${t.remaining}: ${c.accent(String(u.remaining))} / ${u.limit} (${t.windowInfo(u.window_count ?? 0, u.limit)})`
2323
+ ` ${t.remaining}: ${c.accent(String(rem))} / ${lim} (${t.windowInfo(u.window_count ?? 0, lim)})`
2307
2324
  );
2325
+ }
2308
2326
  if (u.reset_at) lines.push(c.dim(` ${t.resetAt}: ${fmtTime(u.reset_at)}`));
2309
2327
  if (u.total_used != null) lines.push(c.dim(` ${t.used} (tổng): ${u.total_used}`));
2310
2328
  console.log(box(lines.join('\n'), t.usageTitle, '#a78bfa'));
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,16 @@ 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 === '\x17' || ch === '\x08') {
939
+ // Ctrl+W và Ctrl+Backspace (terminal này gửi \x08): xoá nguyên từ.
940
+ deleteWordLeft();
941
+ refreshMenu();
942
+ draw();
943
+ i++;
944
+ continue;
945
+ } // Ctrl+W / Ctrl+Backspace
946
+ if (ch === '\x7f') {
947
+ // Backspace thường: xoá 1 ký tự.
927
948
  backspace();
928
949
  refreshMenu();
929
950
  draw();