chatccc 0.2.220 → 0.2.221
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
|
@@ -158,6 +158,37 @@ describe("builtin file tools", () => {
|
|
|
158
158
|
await expect(readFile(file, "utf8")).resolves.toBe("alpha\nBETA\ngamma\n");
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
it("edits a CRLF file with LF oldText by normalizing line endings", async () => {
|
|
162
|
+
const dir = await makeTempDir();
|
|
163
|
+
const file = join(dir, "crlf.txt");
|
|
164
|
+
const crlfContent = "alpha\r\nbeta\r\ngamma\r\n";
|
|
165
|
+
await writeFile(file, crlfContent, "utf8");
|
|
166
|
+
|
|
167
|
+
const result = await editFileForTool(dir, {
|
|
168
|
+
path: "crlf.txt",
|
|
169
|
+
expectedSha256: sha256(crlfContent),
|
|
170
|
+
edits: [{ oldText: "alpha\nbeta", newText: "ALPHA\nBETA" }],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(result).toEqual(expect.objectContaining({
|
|
174
|
+
changed: true,
|
|
175
|
+
editsApplied: 1,
|
|
176
|
+
beforeSha256: sha256(crlfContent),
|
|
177
|
+
afterSha256: sha256("ALPHA\r\nBETA\r\ngamma\r\n"),
|
|
178
|
+
}));
|
|
179
|
+
await expect(readFile(file, "utf8")).resolves.toBe("ALPHA\r\nBETA\r\ngamma\r\n");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("rejects a multi-line oldText that still does not match after EOL normalization", async () => {
|
|
183
|
+
const dir = await makeTempDir();
|
|
184
|
+
await writeFile(join(dir, "crlf.txt"), "alpha\r\nbeta\r\n", "utf8");
|
|
185
|
+
|
|
186
|
+
await expect(editFileForTool(dir, {
|
|
187
|
+
path: "crlf.txt",
|
|
188
|
+
edits: [{ oldText: "alpha\ngamma", newText: "x" }],
|
|
189
|
+
})).rejects.toThrow("oldText was not found");
|
|
190
|
+
});
|
|
191
|
+
|
|
161
192
|
it("rejects edits when the SHA-256 precondition does not match", async () => {
|
|
162
193
|
const dir = await makeTempDir();
|
|
163
194
|
await writeFile(join(dir, "edit.txt"), "current\n", "utf8");
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildButtons,
|
|
14
14
|
truncateContent,
|
|
15
15
|
getToolEmoji,
|
|
16
|
+
normalizeToolName,
|
|
16
17
|
} from "../cards.ts";
|
|
17
18
|
import { ABD_HELP_LINE } from "../shared-prefix.ts";
|
|
18
19
|
|
|
@@ -83,6 +84,25 @@ describe("getToolEmoji", () => {
|
|
|
83
84
|
expect(getToolEmoji("AskUserQuestion")).toBe("\u{2753}");// ❓
|
|
84
85
|
});
|
|
85
86
|
|
|
87
|
+
it("returns correct emoji for CCC builtin tool names (snake_case)", () => {
|
|
88
|
+
expect(getToolEmoji("read_file")).toBe("\u{1F4D6}");
|
|
89
|
+
expect(getToolEmoji("list_dir")).toBe("\u{1F4C2}");
|
|
90
|
+
expect(getToolEmoji("search_code")).toBe("\u{1F50E}");
|
|
91
|
+
expect(getToolEmoji("run_command")).toBe("\u{1F5A5}\u{FE0F}");
|
|
92
|
+
expect(getToolEmoji("edit_file")).toBe("\u{270F}\u{FE0F}");
|
|
93
|
+
expect(getToolEmoji("create_file")).toBe("\u{270D}\u{FE0F}");
|
|
94
|
+
expect(getToolEmoji("delete_file")).toBe("\u{1F5D1}\u{FE0F}");
|
|
95
|
+
expect(getToolEmoji("move_file")).toBe("\u{1F4E6}");
|
|
96
|
+
expect(getToolEmoji("apply_patch")).toBe("\u{1F4CB}");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("normalizeToolName converts snake_case to PascalCase", () => {
|
|
100
|
+
expect(normalizeToolName("read_file")).toBe("ReadFile");
|
|
101
|
+
expect(normalizeToolName("run_command")).toBe("RunCommand");
|
|
102
|
+
expect(normalizeToolName("Read")).toBe("Read");
|
|
103
|
+
expect(normalizeToolName("")).toBe("");
|
|
104
|
+
});
|
|
105
|
+
|
|
86
106
|
it("returns wrench for unknown tool names", () => {
|
|
87
107
|
expect(getToolEmoji("UnknownTool")).toBe("\u{1F527}");
|
|
88
108
|
expect(getToolEmoji("cat")).toBe("\u{1F527}");
|
|
@@ -979,13 +979,19 @@ export async function editFileForTool(cwd: string, input: EditFileInput): Promis
|
|
|
979
979
|
const before = await readEditableTextFile(filePath);
|
|
980
980
|
assertExpectedSha256(filePath, before.sha, input.expectedSha256);
|
|
981
981
|
|
|
982
|
-
|
|
982
|
+
// Normalize line endings before matching so that LF-based oldText/newText
|
|
983
|
+
// (which is what models typically emit) works against CRLF files checked
|
|
984
|
+
// out on Windows. The file's dominant EOL style is restored on write.
|
|
985
|
+
const eol = detectEol(before.text);
|
|
986
|
+
let text = eol === "\r\n" ? before.text.replace(/\r\n/g, "\n") : before.text;
|
|
983
987
|
let editsApplied = 0;
|
|
984
988
|
for (const [index, edit] of input.edits.entries()) {
|
|
985
989
|
if (!edit.oldText) {
|
|
986
990
|
throw new Error(`edit ${index + 1} oldText must not be empty`);
|
|
987
991
|
}
|
|
988
|
-
const
|
|
992
|
+
const oldText = edit.oldText.replace(/\r\n/g, "\n");
|
|
993
|
+
const newText = edit.newText.replace(/\r\n/g, "\n");
|
|
994
|
+
const count = countOccurrences(text, oldText);
|
|
989
995
|
if (count === 0) {
|
|
990
996
|
throw new Error(`edit ${index + 1} oldText was not found in ${filePath}`);
|
|
991
997
|
}
|
|
@@ -993,10 +999,14 @@ export async function editFileForTool(cwd: string, input: EditFileInput): Promis
|
|
|
993
999
|
throw new Error(`edit ${index + 1} oldText matched ${count} times in ${filePath}; set replaceAll=true or provide more context`);
|
|
994
1000
|
}
|
|
995
1001
|
text = edit.replaceAll
|
|
996
|
-
? replaceAllLiteral(text,
|
|
997
|
-
: text.replace(
|
|
1002
|
+
? replaceAllLiteral(text, oldText, newText)
|
|
1003
|
+
: text.replace(oldText, newText);
|
|
998
1004
|
editsApplied += edit.replaceAll ? count : 1;
|
|
999
1005
|
}
|
|
1006
|
+
if (eol === "\r\n") {
|
|
1007
|
+
// After normalization above the buffer contains only \n, so this is safe.
|
|
1008
|
+
text = text.replace(/\n/g, "\r\n");
|
|
1009
|
+
}
|
|
1000
1010
|
|
|
1001
1011
|
assertTextSize(filePath, text, MAX_EDIT_BYTES);
|
|
1002
1012
|
const afterSha = sha256(text);
|
package/src/cards.ts
CHANGED
|
@@ -70,10 +70,29 @@ const TOOL_EMOJI_MAP: Record<string, string> = {
|
|
|
70
70
|
Agent: "\u{1F916}", // 🤖
|
|
71
71
|
NotebookEdit: "\u{1F4D3}", // 📓
|
|
72
72
|
AskUserQuestion: "\u{2753}",// ❓
|
|
73
|
+
// CCC 内置 Agent 下划线命名(getToolEmoji 会先把下划线转驼峰再查表,这里保留蛇形条目以便直接命中)
|
|
74
|
+
read_file: "\u{1F4D6}", // 📖
|
|
75
|
+
list_dir: "\u{1F4C2}", // 📂
|
|
76
|
+
search_code: "\u{1F50E}", // 🔎
|
|
77
|
+
run_command: "\u{1F5A5}\u{FE0F}", // 🖥️
|
|
78
|
+
edit_file: "\u{270F}\u{FE0F}", // ✏️
|
|
79
|
+
create_file: "\u{270D}\u{FE0F}", // ✍️
|
|
80
|
+
delete_file: "\u{1F5D1}\u{FE0F}", // 🗑️
|
|
81
|
+
move_file: "\u{1F4E6}", // 📦
|
|
82
|
+
apply_patch: "\u{1F4CB}", // 📋
|
|
73
83
|
};
|
|
74
84
|
|
|
75
85
|
export function getToolEmoji(name: string): string {
|
|
76
|
-
return TOOL_EMOJI_MAP[name] ?? "\u{1F527}"; // 🔧
|
|
86
|
+
return TOOL_EMOJI_MAP[name] ?? TOOL_EMOJI_MAP[normalizeToolName(name)] ?? "\u{1F527}"; // 🔧
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 把下划线命名转成驼峰(read_file → ReadFile),用于兼容两种命名风格 */
|
|
90
|
+
export function normalizeToolName(name: string): string {
|
|
91
|
+
return name
|
|
92
|
+
.split("_")
|
|
93
|
+
.filter((part) => part.length > 0)
|
|
94
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
95
|
+
.join("");
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
// ---------------------------------------------------------------------------
|