@noobdemon/noob-cli 1.1.0 → 1.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -55,7 +55,7 @@ Có — cả 12 test đều pass.
55
55
 
56
56
  Follow this pattern exactly. Your very first response to a task that needs the filesystem MUST be a tool block — do not refuse or explain limitations.`;
57
57
 
58
- const MAX_STEPS = 30;
58
+ const MAX_STEPS = 300;
59
59
  const MAX_PROMPT_CHARS = 80000; // ngân sách ký tự cho phần hội thoại gửi lên model
60
60
 
61
61
  // Môi trường chạy thực: model cần biết OS + shell để emit lệnh ĐÚNG. Không có
package/src/i18n.js CHANGED
@@ -54,6 +54,7 @@ export const t = {
54
54
  cmdSearch: "/search bật/tắt chế độ tìm web",
55
55
  cmdChat: "/chat quay lại chế độ chat thường",
56
56
  cmdYolo: "/yolo bật/tắt tự duyệt (hoặc nhấn Shift+Tab)",
57
+ cmdKarpathy: "/karpathy [path] rà soát code theo 4 nguyên tắc Karpathy (/kc)",
57
58
  cmdClear: "/clear /new xoá ngữ cảnh (mở phiên mới, phiên cũ vẫn resume được)",
58
59
  cmdResume: "/resume [id] tiếp tục phiên cũ (không id = chọn từ danh sách)",
59
60
  cmdSessions: "/sessions liệt kê các phiên đã lưu",
package/src/repl.js CHANGED
@@ -242,6 +242,24 @@ export async function startRepl(opts = {}) {
242
242
  }
243
243
  const startFresh = () => (session = sessions.newSession({ cwd: process.cwd(), model: state.model.id }));
244
244
 
245
+ // /karpathy [path] — bắt noob tự rà soát code theo 4 nguyên tắc Karpathy.
246
+ // Không có path → soát các file đã đổi trong phiên (model thấy qua FILES CHANGED).
247
+ async function runKarpathy(arg) {
248
+ if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
249
+ const target = arg ? `file/đường dẫn: ${arg}` : "các file bạn đã tạo/sửa trong phiên này (xem mục FILES CHANGED)";
250
+ const prompt = `Đóng vai reviewer khó tính. Rà soát ${target} theo 4 nguyên tắc code của Karpathy.
251
+ ĐỌC nội dung file thật bằng read_file trước — KHÔNG dựa vào trí nhớ.
252
+ Với MỖI nguyên tắc, cho verdict (✅ đạt / ⚠️ cảnh báo / ❌ vi phạm) + phát hiện cụ thể kèm "tên_file:dòng":
253
+ 1. THINK FIRST — giả định ẩn nào chưa nêu? chỗ nào thiếu kiểm chứng?
254
+ 2. KEEP IT SIMPLE — over-engineer, abstraction thừa, lồng quá sâu, hàm quá dài?
255
+ 3. SURGICAL — thay đổi lạc đề, refactor tiện tay, đổi style/format vô cớ?
256
+ 4. VERIFIABLE GOAL — mục tiêu có kiểm chứng được? đã chạy build/test chưa?
257
+ Cuối cùng: tóm tắt + danh sách sửa theo thứ tự ưu tiên. Thẳng thắn, KHÔNG nịnh.`;
258
+ console.log(c.tool(" ⚖ Karpathy check…"));
259
+ await handle(prompt);
260
+ persist();
261
+ }
262
+
245
263
  banner();
246
264
  printStatus(state);
247
265
  if (!config.apiKey) console.log("\n" + c.tool(" " + t.notLoggedIn) + "\n");
@@ -496,6 +514,11 @@ export async function startRepl(opts = {}) {
496
514
  state.yolo = !state.yolo;
497
515
  console.log((state.yolo ? c.err : c.ok)(" " + (state.yolo ? t.yoloOn : t.yoloOff)));
498
516
  break;
517
+ case "karpathy":
518
+ case "kcheck":
519
+ case "kc":
520
+ await runKarpathy(arg);
521
+ break;
499
522
  case "login":
500
523
  doLogin(arg);
501
524
  break;
@@ -718,6 +741,7 @@ function printHelp() {
718
741
  " " + t.cmdSearch,
719
742
  " " + t.cmdChat,
720
743
  " " + t.cmdYolo,
744
+ " " + t.cmdKarpathy,
721
745
  " " + t.cmdLogin,
722
746
  " " + t.cmdLogout,
723
747
  " " + t.cmdUsage,
package/src/tools.js CHANGED
@@ -39,47 +39,44 @@ export const TOOLS = {
39
39
  const file = abs(p);
40
40
  const data = await fs.readFile(file, "utf8");
41
41
  if (old_string === new_string) throw new Error("old_string and new_string are identical");
42
+ const useCRLF = data.includes("\r\n");
43
+ const adapt = (s) => {
44
+ const lf = s.replace(/\r\n/g, "\n");
45
+ return useCRLF ? lf.replace(/\n/g, "\r\n") : lf;
46
+ };
47
+ // Thay không diễn giải $&/$1… (split/slice, không dùng String.replace).
48
+ const applyExact = (cand, newS) => {
49
+ if (replace_all) return data.split(cand).join(newS);
50
+ const i = data.indexOf(cand);
51
+ return data.slice(0, i) + newS + data.slice(i + cand.length);
52
+ };
42
53
 
43
- let oldS = old_string;
44
- let newS = new_string;
45
- let count = data.split(oldS).length - 1;
46
-
47
- // Khoan dung CRLF: file Windows thường dùng \r\n, nhưng model hay gửi \n →
48
- // khớp hụt. Điều chỉnh kiểu xuống dòng của old/new cho khớp file RỒI thay
49
- // trên data gốc (file giữ nguyên vẹn).
50
- if (count === 0) {
51
- const useCRLF = data.includes("\r\n");
52
- const adapt = (s) => {
53
- const lf = s.replace(/\r\n/g, "\n");
54
- return useCRLF ? lf.replace(/\n/g, "\r\n") : lf;
55
- };
56
- const a = adapt(oldS);
57
- const c2 = data.split(a).length - 1;
58
- if (c2 > 0) {
59
- oldS = a;
60
- newS = adapt(newS);
61
- count = c2;
62
- }
54
+ // Tier 1 + 2: khớp NGUYÊN VĂN, rồi khớp sau khi chỉnh CRLF cho hợp file.
55
+ for (const cand of old_string === adapt(old_string) ? [old_string] : [old_string, adapt(old_string)]) {
56
+ const count = data.split(cand).length - 1;
57
+ if (count === 0) continue;
58
+ if (count > 1 && !replace_all)
59
+ throw new Error(`old_string is not unique (${count} matches) in ${rel(file)}; set replace_all, or add surrounding lines to make it unique`);
60
+ await fs.writeFile(file, applyExact(cand, cand === old_string ? new_string : adapt(new_string)), "utf8");
61
+ return `Edited ${rel(file)} (${replace_all ? count : 1} replacement(s))`;
63
62
  }
64
63
 
65
- if (count === 0)
66
- throw new Error(
67
- `old_string not found in ${rel(file)}. Read the file again with read_file and copy the target text EXACTLY — keep its indentation/whitespace and DROP the line-number prefix that read_file prints.`,
68
- );
69
- if (count > 1 && !replace_all)
70
- throw new Error(`old_string is not unique (${count} matches) in ${rel(file)}; set replace_all, or add surrounding lines to make it unique`);
71
-
72
- // split/join (không dùng String.replace) để chuỗi thay thế chứa $&, $1… KHÔNG
73
- // bị diễn giải đặc biệt — bảo toàn nguyên văn code.
74
- let next;
75
- if (replace_all) {
76
- next = data.split(oldS).join(newS);
77
- } else {
78
- const i = data.indexOf(oldS);
79
- next = data.slice(0, i) + newS + data.slice(i + oldS.length);
64
+ // Tier 3: khoan dung khoảng trắng CUỐI dòng + CRLF — khớp theo khối DÒNG.
65
+ // (Thụt đầu dòng vẫn phải khớp → không bao giờ sửa nhầm chỗ.)
66
+ const m = matchByLines(data, old_string);
67
+ if (m && m.count > 1 && !replace_all)
68
+ throw new Error(`old_string khớp ${m.count} chỗ (sau khi bỏ qua khoảng trắng cuối dòng) trong ${rel(file)}; thêm dòng ngữ cảnh hoặc dùng replace_all`);
69
+ if (m && m.count === 1) {
70
+ const next = data.slice(0, m.start) + adapt(new_string) + data.slice(m.end);
71
+ await fs.writeFile(file, next, "utf8");
72
+ return `Edited ${rel(file)} (1 replacement(s))`;
80
73
  }
81
- await fs.writeFile(file, next, "utf8");
82
- return `Edited ${rel(file)} (${replace_all ? count : 1} replacement(s))`;
74
+
75
+ // Không thấy lỗi GIÀU THÔNG TIN: cho model thấy đúng byte trong file để sửa.
76
+ throw new Error(
77
+ `old_string not found in ${rel(file)}. Copy the target text EXACTLY (indentation/whitespace included, NO line-number prefix).` +
78
+ nearbyContext(data, old_string),
79
+ );
83
80
  },
84
81
 
85
82
  async list_dir({ path: p = "." }) {
@@ -174,6 +171,71 @@ export const TOOLS = {
174
171
  },
175
172
  };
176
173
 
174
+ // Khớp old_string theo KHỐI DÒNG, bỏ qua khác biệt CRLF và khoảng trắng CUỐI
175
+ // dòng. Trả {count, start, end} (offset ký tự trong data) khi khớp đúng 1 chỗ,
176
+ // {count>1} khi nhập nhằng, hoặc null khi không thấy.
177
+ function matchByLines(data, oldStr) {
178
+ const trim = (l) => l.replace(/\r$/, "").replace(/[ \t]+$/, "");
179
+ const fileLines = data.split("\n");
180
+ const oldLines = oldStr.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n");
181
+ if (!oldLines.length) return null;
182
+ const nFile = fileLines.map(trim);
183
+ const nOld = oldLines.map(trim);
184
+ const offsets = [];
185
+ let acc = 0;
186
+ for (const l of fileLines) {
187
+ offsets.push(acc);
188
+ acc += l.length + 1; // +1 cho '\n' đã tách
189
+ }
190
+ const hits = [];
191
+ for (let i = 0; i + nOld.length <= nFile.length; i++) {
192
+ let ok = true;
193
+ for (let j = 0; j < nOld.length; j++)
194
+ if (nFile[i + j] !== nOld[j]) {
195
+ ok = false;
196
+ break;
197
+ }
198
+ if (ok) hits.push(i);
199
+ }
200
+ if (hits.length === 0) return null;
201
+ if (hits.length > 1) return { count: hits.length };
202
+ const i = hits[0];
203
+ const last = i + nOld.length - 1;
204
+ return { count: 1, start: offsets[i], end: offsets[last] + fileLines[last].length };
205
+ }
206
+
207
+ // Khi không khớp: in vùng file gần dòng giống nhất, dạng JSON-escaped để model
208
+ // thấy rõ tab/space → sửa old_string cho khớp ngay lần sau.
209
+ function nearbyContext(data, oldStr) {
210
+ const want = (oldStr.replace(/\r\n/g, "\n").split("\n").find((l) => l.trim()) || "").trim();
211
+ if (!want) return "";
212
+ const lines = data.replace(/\r\n/g, "\n").split("\n");
213
+ // Dòng giống nhất = tiền tố chung dài nhất với `want` (sau khi trim).
214
+ const commonPrefix = (a, b) => {
215
+ let i = 0;
216
+ const n = Math.min(a.length, b.length);
217
+ while (i < n && a[i] === b[i]) i++;
218
+ return i;
219
+ };
220
+ let hit = -1;
221
+ let best = 0;
222
+ for (let i = 0; i < lines.length; i++) {
223
+ const s = commonPrefix(lines[i].trim(), want);
224
+ if (s > best) {
225
+ best = s;
226
+ hit = i;
227
+ }
228
+ }
229
+ if (hit < 0 || best < 6) return ` (no similar line found; the file has ${lines.length} lines — re-read it.)`;
230
+ const a = Math.max(0, hit - 2);
231
+ const b = Math.min(lines.length, hit + 3);
232
+ const snippet = lines
233
+ .slice(a, b)
234
+ .map((l, k) => ` ${a + k + 1}: ${JSON.stringify(l)}`)
235
+ .join("\n");
236
+ return `\nActual file lines near the closest match (JSON-escaped — match these bytes EXACTLY):\n${snippet}`;
237
+ }
238
+
177
239
  function globToRegExp(glob) {
178
240
  let rx = "";
179
241
  for (let i = 0; i < glob.length; i++) {