chatccc 0.2.217 → 0.2.219
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/README.md +2 -0
- package/package.json +2 -1
- package/src/__tests__/builtin-file-tools.test.ts +44 -24
- package/src/__tests__/feishu-avatar.test.ts +3 -3
- package/src/builtin/file-tools.ts +279 -41
- package/src/feishu-api.ts +12 -9
package/README.md
CHANGED
|
@@ -338,6 +338,8 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
338
338
|
|
|
339
339
|
**会话停滞保护:** 当 Agent 连续 3 分钟停在“正在启动 Agent”且没有任何事件,或停在“正在生成回复”且回复字符数没有变化,同时尚未报告权威终态时,ChatCCC 会结束旧 CLI,并优先补发一次“完成了吗?如果没完成继续”;恢复轮再次发生相同停滞时不再递归续跑。`/new claude` 和 `/new cursor` 在等待底层 init 事件时也使用 3 分钟超时并主动清理 SDK/CLI。思考、搜索和工具调用阶段不按回复字符数误判,由进程资源监控负责识别真正僵死。Codex 只有 `turn.completed` 才算权威终态,阶段性的 `agent_message` 不算;任一 Agent 报告权威终态后若输出流仍超过 10 秒未关闭,ChatCCC 会强制清理该 CLI 并按正常完成收尾,不会重复询问 Agent。
|
|
340
340
|
|
|
341
|
+
**CCC Agent 代码搜索:** `search_code` 使用项目自带的跨平台 ripgrep,不要求系统另行安装 `rg`。如果当前平台没有可用的 bundled/system ripgrep,会自动降级为内置 Node 搜索,并继续支持常用正则、glob、结果上限、中止和超时控制。
|
|
342
|
+
|
|
341
343
|
## 可用指令
|
|
342
344
|
|
|
343
345
|
| 指令 | 作用 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.219",
|
|
4
4
|
"description": "Feishu bot bridge for Claude Code",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
52
52
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
53
53
|
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
54
|
+
"@vscode/ripgrep": "^1.18.0",
|
|
54
55
|
"ai": "^6.0.184",
|
|
55
56
|
"nodemailer": "^8.0.7",
|
|
56
57
|
"qrcode-terminal": "^0.12.0",
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { join } from "node:path";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
5
|
|
|
8
6
|
import { afterEach, describe, expect, it } from "vitest";
|
|
9
7
|
|
|
@@ -19,7 +17,6 @@ import {
|
|
|
19
17
|
searchCodeForTool,
|
|
20
18
|
} from "../builtin/file-tools.ts";
|
|
21
19
|
|
|
22
|
-
const execFileAsync = promisify(execFile);
|
|
23
20
|
const tempDirs: string[] = [];
|
|
24
21
|
|
|
25
22
|
async function makeTempDir(): Promise<string> {
|
|
@@ -28,15 +25,6 @@ async function makeTempDir(): Promise<string> {
|
|
|
28
25
|
return dir;
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
async function hasRg(): Promise<boolean> {
|
|
32
|
-
try {
|
|
33
|
-
await execFileAsync("rg", ["--version"]);
|
|
34
|
-
return true;
|
|
35
|
-
} catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
28
|
function sha256(text: string): string {
|
|
41
29
|
return createHash("sha256").update(text).digest("hex");
|
|
42
30
|
}
|
|
@@ -75,20 +63,52 @@ describe("builtin file tools", () => {
|
|
|
75
63
|
}));
|
|
76
64
|
});
|
|
77
65
|
|
|
78
|
-
it("searches
|
|
79
|
-
if (!await hasRg()) return;
|
|
80
|
-
|
|
66
|
+
it("searches with the project-bundled ripgrep even when rg is absent from PATH", async () => {
|
|
81
67
|
const dir = await makeTempDir();
|
|
82
68
|
await writeFile(join(dir, "a.ts"), "const marker = 1;\n", "utf8");
|
|
69
|
+
const originalPath = process.env.PATH;
|
|
70
|
+
process.env.PATH = "";
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await searchCodeForTool(dir, { query: "marker", glob: "*.ts" });
|
|
74
|
+
|
|
75
|
+
expect(result.matches).toEqual([
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
line: 1,
|
|
78
|
+
column: 7,
|
|
79
|
+
text: "const marker = 1;",
|
|
80
|
+
}),
|
|
81
|
+
]);
|
|
82
|
+
} finally {
|
|
83
|
+
process.env.PATH = originalPath;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
it("falls back to Node search when no ripgrep executable can be used", async () => {
|
|
88
|
+
const dir = await makeTempDir();
|
|
89
|
+
await writeFile(join(dir, "a.ts"), "const marker = 1;\n", "utf8");
|
|
90
|
+
await mkdir(join(dir, "nested"));
|
|
91
|
+
await writeFile(join(dir, "nested", "b.md"), "xx marker = 2\n", "utf8");
|
|
92
|
+
await writeFile(join(dir, "ignored.txt"), "marker = 3\n", "utf8");
|
|
93
|
+
const originalPath = process.env.PATH;
|
|
94
|
+
process.env.PATH = "";
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const result = await searchCodeForTool(
|
|
98
|
+
dir,
|
|
99
|
+
{ query: "marker\\s*=\\s*\\d", glob: "**/*.{ts,md}", maxResults: 10 },
|
|
100
|
+
undefined,
|
|
101
|
+
{ ripgrepCommands: [join(dir, "missing-rg")] },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(result.matches).toEqual([
|
|
105
|
+
expect.objectContaining({ path: join(dir, "a.ts"), line: 1, column: 7 }),
|
|
106
|
+
expect.objectContaining({ path: join(dir, "nested", "b.md"), line: 1, column: 4 }),
|
|
107
|
+
]);
|
|
108
|
+
expect(result.truncated).toBe(false);
|
|
109
|
+
} finally {
|
|
110
|
+
process.env.PATH = originalPath;
|
|
111
|
+
}
|
|
92
112
|
});
|
|
93
113
|
|
|
94
114
|
it("runs non-interactive shell commands in the requested cwd", async () => {
|
|
@@ -251,14 +251,14 @@ describe("Codex avatar usage battery", () => {
|
|
|
251
251
|
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
252
252
|
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
253
253
|
expect(cache["codex:idle:plain"]).toBe("img_test");
|
|
254
|
-
expect(cache["codex:idle:plain:fast-champagne-frame-
|
|
254
|
+
expect(cache["codex:idle:plain:fast-champagne-frame-v2"]).toBe("img_test");
|
|
255
255
|
} finally {
|
|
256
256
|
await rm(homeDir, { recursive: true, force: true });
|
|
257
257
|
await rm(userDataDir, { recursive: true, force: true });
|
|
258
258
|
}
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
-
it("renders
|
|
261
|
+
it("renders an extra-thick champagne frame above the Codex badge in Fast mode", async () => {
|
|
262
262
|
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
263
263
|
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
264
264
|
const uploadedNames: string[] = [];
|
|
@@ -274,7 +274,7 @@ describe("Codex avatar usage battery", () => {
|
|
|
274
274
|
|
|
275
275
|
expect(uploadedNames).toEqual(["avatar_codex_idle_fast.jpg"]);
|
|
276
276
|
const { data, info } = await sharp(uploadedImages[0]).raw().toBuffer({ resolveWithObject: true });
|
|
277
|
-
const pixelOffset = (
|
|
277
|
+
const pixelOffset = (144 * info.width + 200) * info.channels;
|
|
278
278
|
const [red, green, blue] = data.subarray(pixelOffset, pixelOffset + 3);
|
|
279
279
|
expect(red).toBeGreaterThan(220);
|
|
280
280
|
expect(green).toBeGreaterThan(150);
|
|
@@ -2,7 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import { copyFile, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
-
import {
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
6
8
|
|
|
7
9
|
import { jsonSchema, tool, type ToolSet } from "ai";
|
|
8
10
|
|
|
@@ -19,6 +21,8 @@ const SEARCH_TIMEOUT_MS = 15_000;
|
|
|
19
21
|
const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
|
|
20
22
|
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
21
23
|
const MAX_COMMAND_TIMEOUT_MS = 900_000;
|
|
24
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
25
|
+
const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
|
|
22
26
|
|
|
23
27
|
export interface ReadFileInput {
|
|
24
28
|
path: string;
|
|
@@ -77,6 +81,11 @@ export interface SearchCodeOutput {
|
|
|
77
81
|
truncated: boolean;
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
/** @internal Allows tests to force the dependency-free fallback path. */
|
|
85
|
+
export interface SearchCodeRuntimeOptions {
|
|
86
|
+
ripgrepCommands?: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
export interface RunCommandInput {
|
|
81
90
|
command: string;
|
|
82
91
|
cwd?: string;
|
|
@@ -546,32 +555,44 @@ function parseRgLine(line: string): SearchCodeMatch | null {
|
|
|
546
555
|
};
|
|
547
556
|
}
|
|
548
557
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const query = input.query?.trim();
|
|
555
|
-
if (!query) throw new Error("query is required");
|
|
558
|
+
interface RipgrepOutput {
|
|
559
|
+
stdout: string;
|
|
560
|
+
stderr: string;
|
|
561
|
+
truncated: boolean;
|
|
562
|
+
}
|
|
556
563
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
"
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
];
|
|
568
|
-
if (input.glob?.trim()) {
|
|
569
|
-
args.push("--glob", input.glob.trim());
|
|
564
|
+
function resolveBundledRipgrepPath(): string | undefined {
|
|
565
|
+
try {
|
|
566
|
+
const bundled = requireFromHere("@vscode/ripgrep") as { rgPath?: unknown };
|
|
567
|
+
return typeof bundled.rgPath === "string" && bundled.rgPath.trim()
|
|
568
|
+
? bundled.rgPath
|
|
569
|
+
: undefined;
|
|
570
|
+
} catch {
|
|
571
|
+
// Unsupported platforms or damaged optional platform packages must not
|
|
572
|
+
// prevent ChatCCC itself from starting; system rg / Node fallback remain.
|
|
573
|
+
return undefined;
|
|
570
574
|
}
|
|
571
|
-
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function defaultRipgrepCommands(): string[] {
|
|
578
|
+
const bundled = resolveBundledRipgrepPath();
|
|
579
|
+
return [...new Set([bundled, "rg"].filter((value): value is string => !!value))];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function isUnavailableExecutableError(err: unknown): boolean {
|
|
583
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
584
|
+
return code === "ENOENT" || code === "EACCES" || code === "EPERM";
|
|
585
|
+
}
|
|
572
586
|
|
|
573
|
-
|
|
574
|
-
|
|
587
|
+
async function runRipgrep(
|
|
588
|
+
command: string,
|
|
589
|
+
args: string[],
|
|
590
|
+
cwd: string,
|
|
591
|
+
signal?: AbortSignal,
|
|
592
|
+
): Promise<RipgrepOutput> {
|
|
593
|
+
return new Promise<RipgrepOutput>((resolvePromise, reject) => {
|
|
594
|
+
let settled = false;
|
|
595
|
+
const child = spawn(command, args, {
|
|
575
596
|
cwd,
|
|
576
597
|
shell: false,
|
|
577
598
|
windowsHide: true,
|
|
@@ -581,14 +602,23 @@ export async function searchCodeForTool(
|
|
|
581
602
|
let stdout = "";
|
|
582
603
|
let stderr = "";
|
|
583
604
|
let truncated = false;
|
|
605
|
+
const cleanup = () => {
|
|
606
|
+
clearTimeout(timeout);
|
|
607
|
+
signal?.removeEventListener("abort", abort);
|
|
608
|
+
};
|
|
609
|
+
const rejectOnce = (err: Error) => {
|
|
610
|
+
if (settled) return;
|
|
611
|
+
settled = true;
|
|
612
|
+
cleanup();
|
|
613
|
+
reject(err);
|
|
614
|
+
};
|
|
584
615
|
const timeout = setTimeout(() => {
|
|
585
616
|
child.kill();
|
|
586
|
-
|
|
617
|
+
rejectOnce(new Error(`search_code timed out after ${SEARCH_TIMEOUT_MS}ms`));
|
|
587
618
|
}, SEARCH_TIMEOUT_MS);
|
|
588
|
-
|
|
589
619
|
const abort = () => {
|
|
590
620
|
child.kill();
|
|
591
|
-
|
|
621
|
+
rejectOnce(new Error("search_code aborted"));
|
|
592
622
|
};
|
|
593
623
|
signal?.addEventListener("abort", abort, { once: true });
|
|
594
624
|
|
|
@@ -606,14 +636,11 @@ export async function searchCodeForTool(
|
|
|
606
636
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
607
637
|
stderr += chunk.toString("utf8");
|
|
608
638
|
});
|
|
609
|
-
child.on("error", (err) =>
|
|
610
|
-
clearTimeout(timeout);
|
|
611
|
-
signal?.removeEventListener("abort", abort);
|
|
612
|
-
reject(err);
|
|
613
|
-
});
|
|
639
|
+
child.on("error", (err) => rejectOnce(err));
|
|
614
640
|
child.on("close", (code) => {
|
|
615
|
-
|
|
616
|
-
|
|
641
|
+
if (settled) return;
|
|
642
|
+
settled = true;
|
|
643
|
+
cleanup();
|
|
617
644
|
if (code !== 0 && code !== 1) {
|
|
618
645
|
reject(new Error(stderr.trim() || `rg exited with code ${code}`));
|
|
619
646
|
return;
|
|
@@ -621,20 +648,231 @@ export async function searchCodeForTool(
|
|
|
621
648
|
resolvePromise({ stdout, stderr, truncated });
|
|
622
649
|
});
|
|
623
650
|
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function expandGlobBraces(pattern: string): string[] {
|
|
654
|
+
const openIndex = pattern.indexOf("{");
|
|
655
|
+
if (openIndex < 0) return [pattern];
|
|
656
|
+
const closeIndex = pattern.indexOf("}", openIndex + 1);
|
|
657
|
+
if (closeIndex < 0) return [pattern];
|
|
658
|
+
const alternatives = pattern.slice(openIndex + 1, closeIndex).split(",");
|
|
659
|
+
if (alternatives.length < 2) return [pattern];
|
|
660
|
+
return alternatives.flatMap((alternative) => expandGlobBraces(
|
|
661
|
+
pattern.slice(0, openIndex) + alternative + pattern.slice(closeIndex + 1),
|
|
662
|
+
));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function globToRegExp(pattern: string): RegExp {
|
|
666
|
+
let source = "";
|
|
667
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
668
|
+
const char = pattern[index];
|
|
669
|
+
if (char === "*") {
|
|
670
|
+
if (pattern[index + 1] === "*") {
|
|
671
|
+
index += 1;
|
|
672
|
+
if (pattern[index + 1] === "/") {
|
|
673
|
+
index += 1;
|
|
674
|
+
source += "(?:.*/)?";
|
|
675
|
+
} else {
|
|
676
|
+
source += ".*";
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
source += "[^/]*";
|
|
680
|
+
}
|
|
681
|
+
} else if (char === "?") {
|
|
682
|
+
source += "[^/]";
|
|
683
|
+
} else {
|
|
684
|
+
source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return new RegExp(`^${source}$`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function createGlobMatchers(glob: string | undefined): Array<{ regex: RegExp; basenameOnly: boolean }> {
|
|
691
|
+
if (!glob?.trim()) return [];
|
|
692
|
+
return expandGlobBraces(glob.trim().replaceAll("\\", "/")).map((pattern) => ({
|
|
693
|
+
regex: globToRegExp(pattern),
|
|
694
|
+
basenameOnly: !pattern.includes("/"),
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function matchesFallbackGlob(
|
|
699
|
+
filePath: string,
|
|
700
|
+
searchRoot: string,
|
|
701
|
+
matchers: Array<{ regex: RegExp; basenameOnly: boolean }>,
|
|
702
|
+
): boolean {
|
|
703
|
+
if (matchers.length === 0) return true;
|
|
704
|
+
const relativePath = relative(searchRoot, filePath).split(sep).join("/");
|
|
705
|
+
return matchers.some(({ regex, basenameOnly }) => regex.test(
|
|
706
|
+
basenameOnly ? basename(filePath) : relativePath,
|
|
707
|
+
));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function searchCodeWithNode(
|
|
711
|
+
query: string,
|
|
712
|
+
searchPath: string,
|
|
713
|
+
glob: string | undefined,
|
|
714
|
+
maxResults: number,
|
|
715
|
+
signal?: AbortSignal,
|
|
716
|
+
): Promise<{ matches: SearchCodeMatch[]; truncated: boolean }> {
|
|
717
|
+
let queryRegex: RegExp;
|
|
718
|
+
try {
|
|
719
|
+
queryRegex = new RegExp(query);
|
|
720
|
+
} catch (err) {
|
|
721
|
+
throw new Error(`invalid search regex: ${(err as Error).message}`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const startedAt = Date.now();
|
|
725
|
+
const matches: SearchCodeMatch[] = [];
|
|
726
|
+
const rootInfo = await stat(searchPath);
|
|
727
|
+
const searchRoot = rootInfo.isDirectory() ? searchPath : dirname(searchPath);
|
|
728
|
+
const globMatchers = createGlobMatchers(glob);
|
|
729
|
+
let truncated = false;
|
|
730
|
+
let outputBytes = 0;
|
|
731
|
+
|
|
732
|
+
const ensureActive = () => {
|
|
733
|
+
if (signal?.aborted) throw new Error("search_code aborted");
|
|
734
|
+
if (Date.now() - startedAt >= SEARCH_TIMEOUT_MS) {
|
|
735
|
+
throw new Error(`search_code timed out after ${SEARCH_TIMEOUT_MS}ms`);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const searchFile = async (filePath: string) => {
|
|
740
|
+
if (!matchesFallbackGlob(filePath, searchRoot, globMatchers)) return;
|
|
741
|
+
ensureActive();
|
|
742
|
+
const input = createReadStream(filePath, { encoding: "utf8" });
|
|
743
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
744
|
+
let lineNumber = 0;
|
|
745
|
+
try {
|
|
746
|
+
for await (const line of lines) {
|
|
747
|
+
ensureActive();
|
|
748
|
+
lineNumber += 1;
|
|
749
|
+
if (line.includes("\0")) break;
|
|
750
|
+
const match = queryRegex.exec(line);
|
|
751
|
+
queryRegex.lastIndex = 0;
|
|
752
|
+
if (!match) continue;
|
|
753
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
754
|
+
if (outputBytes + lineBytes > MAX_SEARCH_BYTES) {
|
|
755
|
+
truncated = true;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
outputBytes += lineBytes;
|
|
759
|
+
matches.push({
|
|
760
|
+
path: filePath,
|
|
761
|
+
line: lineNumber,
|
|
762
|
+
column: match.index + 1,
|
|
763
|
+
text: line,
|
|
764
|
+
});
|
|
765
|
+
if (matches.length >= maxResults) {
|
|
766
|
+
truncated = true;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
if (signal?.aborted) throw new Error("search_code aborted");
|
|
772
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
773
|
+
if (code !== "EACCES" && code !== "EPERM" && code !== "ENOENT") throw err;
|
|
774
|
+
} finally {
|
|
775
|
+
lines.close();
|
|
776
|
+
input.destroy();
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const visit = async (currentPath: string): Promise<void> => {
|
|
781
|
+
ensureActive();
|
|
782
|
+
if (truncated) return;
|
|
783
|
+
let info;
|
|
784
|
+
try {
|
|
785
|
+
info = currentPath === searchPath ? rootInfo : await stat(currentPath);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
788
|
+
if (code === "EACCES" || code === "EPERM" || code === "ENOENT") return;
|
|
789
|
+
throw err;
|
|
790
|
+
}
|
|
791
|
+
if (info.isFile()) {
|
|
792
|
+
await searchFile(currentPath);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (!info.isDirectory()) return;
|
|
796
|
+
|
|
797
|
+
let entries;
|
|
798
|
+
try {
|
|
799
|
+
entries = await readdir(currentPath, { withFileTypes: true });
|
|
800
|
+
} catch (err) {
|
|
801
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
802
|
+
if (code === "EACCES" || code === "EPERM" || code === "ENOENT") return;
|
|
803
|
+
throw err;
|
|
804
|
+
}
|
|
805
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
806
|
+
for (const entry of entries) {
|
|
807
|
+
if (truncated) break;
|
|
808
|
+
if (entry.name.startsWith(".")) continue;
|
|
809
|
+
if (entry.isDirectory() && FALLBACK_SKIPPED_DIRECTORIES.has(entry.name)) continue;
|
|
810
|
+
if (entry.isSymbolicLink()) continue;
|
|
811
|
+
await visit(resolve(currentPath, entry.name));
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
await visit(searchPath);
|
|
816
|
+
return { matches, truncated };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export async function searchCodeForTool(
|
|
820
|
+
cwd: string,
|
|
821
|
+
input: SearchCodeInput,
|
|
822
|
+
signal?: AbortSignal,
|
|
823
|
+
runtimeOptions: SearchCodeRuntimeOptions = {},
|
|
824
|
+
): Promise<SearchCodeOutput> {
|
|
825
|
+
const query = input.query?.trim();
|
|
826
|
+
if (!query) throw new Error("query is required");
|
|
827
|
+
if (signal?.aborted) throw new Error("search_code aborted");
|
|
828
|
+
|
|
829
|
+
const searchPath = resolveToolPath(cwd, input.path);
|
|
830
|
+
const maxResults = Math.min(toPositiveInt(input.maxResults) ?? 50, MAX_SEARCH_RESULTS);
|
|
831
|
+
const args = [
|
|
832
|
+
"--line-number",
|
|
833
|
+
"--column",
|
|
834
|
+
"--no-heading",
|
|
835
|
+
"--color",
|
|
836
|
+
"never",
|
|
837
|
+
"--max-count",
|
|
838
|
+
String(maxResults),
|
|
839
|
+
];
|
|
840
|
+
if (input.glob?.trim()) {
|
|
841
|
+
args.push("--glob", input.glob.trim());
|
|
842
|
+
}
|
|
843
|
+
args.push("--", query, searchPath);
|
|
844
|
+
|
|
845
|
+
const commands = runtimeOptions.ripgrepCommands ?? defaultRipgrepCommands();
|
|
846
|
+
let output: RipgrepOutput | undefined;
|
|
847
|
+
for (const command of commands) {
|
|
848
|
+
try {
|
|
849
|
+
output = await runRipgrep(command, args, cwd, signal);
|
|
850
|
+
break;
|
|
851
|
+
} catch (err) {
|
|
852
|
+
if (!isUnavailableExecutableError(err)) throw err;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
624
855
|
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
.
|
|
628
|
-
|
|
629
|
-
.
|
|
630
|
-
|
|
856
|
+
const fallback = output
|
|
857
|
+
? undefined
|
|
858
|
+
: await searchCodeWithNode(query, searchPath, input.glob?.trim(), maxResults, signal);
|
|
859
|
+
const matches = output
|
|
860
|
+
? output.stdout
|
|
861
|
+
.split(/\r?\n/)
|
|
862
|
+
.filter(Boolean)
|
|
863
|
+
.map(parseRgLine)
|
|
864
|
+
.filter((match): match is SearchCodeMatch => !!match)
|
|
865
|
+
.slice(0, maxResults)
|
|
866
|
+
: fallback!.matches;
|
|
631
867
|
|
|
632
868
|
return {
|
|
633
869
|
query,
|
|
634
870
|
path: searchPath,
|
|
635
871
|
...(input.glob?.trim() ? { glob: input.glob.trim() } : {}),
|
|
636
872
|
matches,
|
|
637
|
-
truncated: output
|
|
873
|
+
truncated: output
|
|
874
|
+
? output.truncated || matches.length >= maxResults
|
|
875
|
+
: fallback!.truncated,
|
|
638
876
|
};
|
|
639
877
|
}
|
|
640
878
|
|
package/src/feishu-api.ts
CHANGED
|
@@ -324,7 +324,7 @@ const AVATAR_BADGE_SIZE = 92;
|
|
|
324
324
|
const AVATAR_BADGE_MARGIN = 10;
|
|
325
325
|
const PLAIN_AVATAR_TOOL = "plain";
|
|
326
326
|
const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-window-aware-v14";
|
|
327
|
-
const CODEX_FAST_FRAME_STYLE_VERSION = "fast-champagne-frame-
|
|
327
|
+
const CODEX_FAST_FRAME_STYLE_VERSION = "fast-champagne-frame-v2";
|
|
328
328
|
const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
|
|
329
329
|
|
|
330
330
|
export interface CodexUsageBalance {
|
|
@@ -760,7 +760,10 @@ function buildCodexUsageRingSvg(remainingPercent: number): Buffer {
|
|
|
760
760
|
</svg>`);
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
-
async function buildAgentBadgeOverlay(
|
|
763
|
+
async function buildAgentBadgeOverlay(
|
|
764
|
+
tool: string,
|
|
765
|
+
margin = AVATAR_BADGE_MARGIN,
|
|
766
|
+
): Promise<sharp.OverlayOptions> {
|
|
764
767
|
const badge = await sharp(AVATAR_BADGES[tool])
|
|
765
768
|
.resize(AVATAR_BADGE_SIZE, AVATAR_BADGE_SIZE, {
|
|
766
769
|
fit: "contain",
|
|
@@ -771,14 +774,14 @@ async function buildAgentBadgeOverlay(tool: string): Promise<sharp.OverlayOption
|
|
|
771
774
|
.toBuffer();
|
|
772
775
|
return {
|
|
773
776
|
input: badge,
|
|
774
|
-
left: AVATAR_SIZE - AVATAR_BADGE_SIZE -
|
|
775
|
-
top: AVATAR_SIZE - AVATAR_BADGE_SIZE -
|
|
777
|
+
left: AVATAR_SIZE - AVATAR_BADGE_SIZE - margin,
|
|
778
|
+
top: AVATAR_SIZE - AVATAR_BADGE_SIZE - margin,
|
|
776
779
|
};
|
|
777
780
|
}
|
|
778
781
|
|
|
779
782
|
function buildCodexFastFrameOverlay(): sharp.OverlayOptions {
|
|
780
|
-
const size = AVATAR_BADGE_SIZE +
|
|
781
|
-
const innerOffset =
|
|
783
|
+
const size = AVATAR_BADGE_SIZE + 20;
|
|
784
|
+
const innerOffset = 9;
|
|
782
785
|
const innerSize = size - innerOffset * 2;
|
|
783
786
|
const frame = Buffer.from(`
|
|
784
787
|
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -794,8 +797,8 @@ function buildCodexFastFrameOverlay(): sharp.OverlayOptions {
|
|
|
794
797
|
</svg>`);
|
|
795
798
|
return {
|
|
796
799
|
input: frame,
|
|
797
|
-
left: AVATAR_SIZE - size -
|
|
798
|
-
top: AVATAR_SIZE - size -
|
|
800
|
+
left: AVATAR_SIZE - size - 1,
|
|
801
|
+
top: AVATAR_SIZE - size - 1,
|
|
799
802
|
};
|
|
800
803
|
}
|
|
801
804
|
|
|
@@ -833,7 +836,7 @@ async function renderAvatar(
|
|
|
833
836
|
|
|
834
837
|
if (useDynamicBadgeAvatar) {
|
|
835
838
|
if (useFastCodexAvatar) composites.push(buildCodexFastFrameOverlay());
|
|
836
|
-
composites.push(await buildAgentBadgeOverlay(normalizedTool));
|
|
839
|
+
composites.push(await buildAgentBadgeOverlay(normalizedTool, useFastCodexAvatar ? 11 : AVATAR_BADGE_MARGIN));
|
|
837
840
|
}
|
|
838
841
|
|
|
839
842
|
let pipeline = sharp(await readFile(basePath))
|