@noobdemon/noob-cli 1.0.8 → 1.1.0

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.0.8",
3
+ "version": "1.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -16,15 +16,16 @@ To call a tool, emit EXACTLY ONE fenced code block tagged \`tool\` containing a
16
16
  Then STOP and wait — the runtime executes the tool and replies with a TOOL RESULT. Use one tool per step. When the task is complete (or you are only answering a question), reply normally in Markdown with NO tool block.
17
17
 
18
18
  Available tools:
19
- - read_file {"path": str, "offset"?: int, "limit"?: int} — read a file (1-indexed lines)
19
+ - read_file {"path": str, "offset"?: int, "limit"?: int} — read a file. The "N " line-number prefix in the output is DISPLAY ONLY — it is NOT part of the file; never copy it into edit_file.
20
20
  - write_file {"path": str, "content": str} — create/overwrite a file
21
- - edit_file {"path": str, "old_string": str, "new_string": str, "replace_all"?: bool} — exact string replace; old_string must be unique unless replace_all
21
+ - edit_file {"path": str, "old_string": str, "new_string": str, "replace_all"?: bool} — exact string replace. old_string must match the file's RAW text byte-for-byte (indentation/whitespace included, NO line-number prefix) and be unique unless replace_all. If a replace fails, re-read the file and copy the exact text.
22
22
  - list_dir {"path"?: str} — list a directory
23
23
  - glob {"pattern": str} — find files by glob (supports ** and *)
24
24
  - grep {"pattern": str, "path"?: str, "glob"?: str} — regex search file contents
25
25
  - run_command {"command": str} — run a shell command in the cwd
26
26
 
27
27
  # Rules
28
+ - GROUND TRUTH = the filesystem, NOT your memory of this chat. A file was created/changed ONLY if a write_file/edit_file TOOL RESULT confirms it (see the FILES CHANGED list). Saying "I created/updated X" in prose does NOT change any file — you must emit the tool call. If the user says a file is missing or asks its state, read_file/list_dir to check reality first; never claim a file "was reverted" or "should be there" from memory.
28
29
  - Investigate before editing: read the relevant files first; never invent file contents.
29
30
  - Make the smallest change that fully solves the task. Match the surrounding code style.
30
31
  - Prefer edit_file over write_file for existing files.
@@ -108,10 +109,31 @@ function compact(history, budget) {
108
109
  return [...head, elided, ...out.slice(tailStart)];
109
110
  }
110
111
 
112
+ // GROUND TRUTH: liệt kê những file ĐÃ THỰC SỰ được ghi/sửa, suy ra từ KẾT QUẢ
113
+ // tool có thật (không phải từ lời model tự kể). Chống lỗi model "tưởng đã tạo
114
+ // file" (chỉ kể trong văn xuôi, quên gọi write_file) rồi khăng khăng "file bị
115
+ // revert". Dựng từ history ĐẦY ĐỦ (kể cả khi đã nén) để luôn đúng thực tế.
116
+ function filesLedger(history) {
117
+ const touched = new Map(); // path -> "đã ghi" | "đã sửa"
118
+ for (const m of history) {
119
+ if (m.role !== "tool" || typeof m.content !== "string" || m.content.startsWith("ERROR")) continue;
120
+ let mm;
121
+ if (m.name === "write_file" && (mm = m.content.match(/ to (.+)$/m))) touched.set(mm[1].trim(), "đã ghi");
122
+ else if (m.name === "edit_file" && (mm = m.content.match(/^Edited (.+?) \(/m))) touched.set(mm[1].trim(), "đã sửa");
123
+ }
124
+ if (!touched.size)
125
+ return "# FILES CHANGED THIS SESSION: none.\nYou have NOT created or modified any file yet. Do not claim otherwise — to change a file you MUST emit write_file/edit_file. Describing a change in prose does nothing.";
126
+ return (
127
+ "# FILES ACTUALLY CHANGED THIS SESSION (runtime ground truth — trust THIS over your memory)\n" +
128
+ [...touched].map(([p, a]) => `- ${p} (${a})`).join("\n") +
129
+ "\nIf you think you changed a file that is NOT in this list, you did NOT — emit the tool call now. Never say a file 'was reverted'/'should exist' from memory; verify with read_file or list_dir first."
130
+ );
131
+ }
132
+
111
133
  // The proxy is stateless, so we serialize the whole transcript into one prompt.
112
134
  function buildPrompt(history) {
113
135
  const msgs = compact(history, MAX_PROMPT_CHARS);
114
- const parts = [SYSTEM, "", runtimeContext(), "", "=".repeat(60), "# CONVERSATION", ""];
136
+ const parts = [SYSTEM, "", runtimeContext(), "", filesLedger(history), "", "=".repeat(60), "# CONVERSATION", ""];
115
137
  for (const m of msgs) {
116
138
  if (m.role === "user") parts.push(`## USER\n${m.content}`);
117
139
  else if (m.role === "assistant") parts.push(`## ASSISTANT\n${m.content}`);
package/src/repl.js CHANGED
@@ -133,6 +133,14 @@ export async function startRepl(opts = {}) {
133
133
  }
134
134
  }
135
135
 
136
+ // NOOB_DEBUG=1: vạch trần đường thoát thật sự (xác nhận/loại bỏ giả thuyết
137
+ // "event loop cạn"). 'beforeExit' nổ = loop cạn (stdin chết). 'exit' = thoát.
138
+ if (process.env.NOOB_DEBUG === "1") {
139
+ process.stderr.write(` [debug] isTTY=${process.stdin.isTTY} platform=${process.platform} node=${process.version}\n`);
140
+ process.on("beforeExit", (code) => process.stderr.write(` [debug] beforeExit code=${code} — EVENT LOOP CẠN (stdin chết)\n`));
141
+ process.on("exit", (code) => process.stderr.write(` [debug] exit code=${code} closed=${closed} exiting=${exiting}\n`));
142
+ }
143
+
136
144
  let abort = null; // active turn controller
137
145
  let sigintArmed = false;
138
146
  process.on("SIGINT", () => {
@@ -317,7 +325,12 @@ export async function startRepl(opts = {}) {
317
325
  return;
318
326
  }
319
327
  abort = new AbortController();
320
- const spinner = ora({ color: "magenta", spinner: "dots" });
328
+ // discardStdin:false TỐI QUAN TRỌNG. Mặc định ora chiếm stdin (raw mode +
329
+ // pause) để "nuốt" input khi quay. Trên Windows nó KHÔNG khôi phục sạch khi
330
+ // stop → stdin chết → prompt "cho phép?" hiện ra rồi event loop cạn → CLI tự
331
+ // out; và Ctrl+C không thành SIGINT (phải spam mới thoát). Tắt hẳn để ora
332
+ // đừng đụng stdin — readline tự quản.
333
+ const spinner = ora({ color: "magenta", spinner: "dots", discardStdin: false });
321
334
  const t0 = Date.now();
322
335
  let timer = null;
323
336
  const tick = (label) => {
@@ -419,7 +432,7 @@ export async function startRepl(opts = {}) {
419
432
  }
420
433
  }
421
434
 
422
- const sp = ora({ text: c.dim(" " + t.running), color: "yellow" }).start();
435
+ const sp = ora({ text: c.dim(" " + t.running), color: "yellow", discardStdin: false }).start();
423
436
  try {
424
437
  const result = await runTool(name, input);
425
438
  sp.stop();
@@ -556,7 +569,7 @@ export async function startRepl(opts = {}) {
556
569
 
557
570
  async function showUsage() {
558
571
  if (!config.apiKey) return console.log(c.tool(" " + t.notLoggedIn));
559
- const sp = ora({ text: c.dim(" ..."), color: "magenta" }).start();
572
+ const sp = ora({ text: c.dim(" ..."), color: "magenta", discardStdin: false }).start();
560
573
  try {
561
574
  const u = await usage();
562
575
  sp.stop();
package/src/tools.js CHANGED
@@ -39,11 +39,45 @@ 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 count = data.split(old_string).length - 1;
43
- if (count === 0) throw new Error("old_string not found in file");
42
+
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
+ }
63
+ }
64
+
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
+ );
44
69
  if (count > 1 && !replace_all)
45
- throw new Error(`old_string is not unique (${count} matches); set replace_all or add context`);
46
- const next = replace_all ? data.split(old_string).join(new_string) : data.replace(old_string, new_string);
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);
80
+ }
47
81
  await fs.writeFile(file, next, "utf8");
48
82
  return `Edited ${rel(file)} (${replace_all ? count : 1} replacement(s))`;
49
83
  },