@noobdemon/noob-cli 1.0.9 → 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 +1 -1
- package/src/agent.js +25 -3
- package/src/tools.js +38 -4
package/package.json
CHANGED
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
|
|
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
|
|
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/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
|
-
|
|
43
|
-
|
|
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
|
|
46
|
-
|
|
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
|
},
|