From 58f3a67cf7814602e63a4729a3c32ff404d99f60 Mon Sep 17 00:00:00 2001
From: AlkaidSTART <2595006848@qq.com>
Date: Sun, 23 Aug 2026 16:06:14 +0800
Subject: [PATCH 1/3] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20README?=
=?UTF-8?q?=EF=BC=8C=E8=B0=83=E6=95=B4=20Node.js=20=E5=92=8C=20Bun=20?=
=?UTF-8?q?=E7=89=88=E6=9C=AC=E4=BF=A1=E6=81=AF=EF=BC=8C=E4=BF=AE=E6=AD=A3?=
=?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=9B=86=E6=8F=8F=E8=BF=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 73 ++++++++++++++++++++++++++++++++-----------------------
1 file changed, 42 insertions(+), 31 deletions(-)
diff --git a/README.md b/README.md
index 38de30d..4b1e340 100644
--- a/README.md
+++ b/README.md
@@ -4,18 +4,18 @@
-
+

-
+

-call-code 是一个本地运行的终端编程 Agent(CLI coding agent),基于 Node.js、TypeScript 和 Ink 构建。它可以在用户当前工作目录中接受自然语言任务,通过工具调用读取文件、写入文件、执行命令、查看环境信息,并结合本地短/长期记忆持续完成任务。
+call-code 是一个本地运行的终端编程 Agent(CLI coding agent),基于 Node.js、TypeScript、Bun 和 Ink 构建。它可以在用户当前工作目录中接受自然语言任务,通过工具调用读取文件、写入文件、执行命令、搜索代码、查看环境信息,并结合本地短/长期记忆持续完成任务。
## 功能特性
- 终端交互界面:基于 Ink 的命令行界面,支持首页、对话、历史选择和相关页面预览。
- 双执行模式:`PLAN` 模式只允许生成计划和读取环境,`BUILD` 模式可以写入文件、执行命令并推进任务。
-- 本地工具集:内置 `get_environment`、`read_file`、`write_file`、`bash`、`git_diff`、`ocr_image` 六个工具。
+- 本地工具集:内置 `get_environment`、`read_file`、`write_file`、`search`、`bash`、`git_diff`、`ocr_image` 七个工具。
- 结构化响应协议:模型输出统一为 `tool_call` 或 `final` 的 JSON action,循环解析并继续执行。
- 本地记忆:短期记忆按任务保存,长期记忆按主题沉淀,仅在进程内使用,不写入本地 JSON。
- 上下文预算:运行时基于 token 估算对历史消息做裁剪,减少超出模型上下文的风险。
@@ -29,15 +29,18 @@ source/app.tsx CLI 层
首页 / 对话 / 历史 / 相关页面预览
│ 用户输入、命令与活动面板操作
▼
-agent-core 核心层
-├─ core/ agent 与 runLoop 主循环:规划 -> 执行 -> 观察
+agent-core/src/harness 核心层
+├─ core/ agent、LLM 客户端与任务状态
+├─ runtime/ runLoop 主循环、会话与工具运行时
├─ context/ 构建上下文、历史摘要与 token 预算
+├─ compaction/ 上下文压缩与分支摘要
├─ protocol/ 解析 tool_call / final JSON action
-├─ policy/ PLAN / BUILD 模式下的工具权限
-├─ tools/ get_environment / read_file / write_file
-│ bash / git_diff / ocr_image
-├─ memory/ short / long 记忆(仅存内存,不落盘 JSON)
-└─ prompt/ 系统提示词、工具说明与模式提示词
+├─ prompt/ 系统提示词、工具说明与模式提示词
+├─ tools/ 七个本地工具,及 PLAN / BUILD 权限
+│ └─ policy/ 模式权限守卫
+├─ session/ 会话恢复、活动查询与任务会话
+├─ memory/ 短期 / 长期记忆(仅存内存,不落盘 JSON)
+└─ utils/ shell 与文本截断等通用工具
│ OpenAI chat.completions 请求(支持流式)
▼
OpenAI-compatible LLM 模型层
@@ -58,7 +61,7 @@ OpenAI-compatible LLM 模型层
1. CLI 接收自然语言任务,交给 agent 构建上下文并调用 LLM。
2. 模型返回 `tool_call` 或 `final`,由 protocol 解析为结构化 action。
-3. policy 按 `PLAN` / `BUILD` 模式校验权限,允许后由对应工具执行。
+3. tools/policy 按 `PLAN` / `BUILD` 模式校验权限,允许后由对应工具执行。
4. 工具执行结果作为 observation 回写,memory 记录关键信息,循环继续,直到返回 `final`。
## 项目结构
@@ -69,13 +72,20 @@ source/
packages/
agent-core/ # 核心 agent、上下文、记忆、工具与协议实现
src/
- core/ # agent、runLoop、state、LLM 调用
- context/ # 上下文构建、历史摘要与 token 管理
- memory/ # 短期/长期记忆存储与检索
- protocol/ # 模型 action/observation 协议解析
- prompt/ # 系统提示词、工具说明、模式提示词
- tools/ # 环境、文件、命令等本地工具
- policy/ # PLAN/BUILD 模式下的工具权限
+ harness/
+ core/ # agent、LLM、任务状态
+ runtime/ # runLoop、会话与工具运行时
+ context/ # 上下文构建、摘要与 token 管理
+ compaction/ # 上下文压缩与分支摘要
+ memory/ # 短期/长期记忆存储与检索
+ protocol/ # 模型 action/observation 协议解析
+ prompt/ # 系统提示词、工具说明、模式提示词
+ session/ # 会话恢复、活动查询与任务会话
+ tools/ # 七个本地工具
+ tools/policy/ # PLAN/BUILD 模式下的工具权限
+ utils/ # shell 与文本截断等工具
+ types/ # 领域类型
+ utils/ # JSON、日志工具
web/ # GitHub Pages 客户端数据导出
client/ # TypeScript + React 会话历史界面,可部署到 GitHub Pages
session-sqlite/ # 基于 node:sqlite 的会话历史与运行状态存储
@@ -94,14 +104,14 @@ tests/ # 项目统一单元测试
## 快速开始
-1. 安装依赖(建议 Node.js 20+,并使用 pnpm)。
-2. 将 `.env.example` 复制为 `.env.local`(或 `.env`),配置 `OPENAI_API_KEY` 与 `OPENAI_MODEL`。
+1. 安装依赖(需要 Node.js 22.5+,包管理器为 Bun,版本固定为 1.3.11)。
+2. 将 `.env.example` 复制为 `.env.local`,配置 `OPENAI_API_KEY` 与 `OPENAI_MODEL`;`.env` 与 `.env.local` 都会被加载。
3. 启动 CLI,入口为 `source/app.tsx`。
```bash
cp .env.example .env.local
-pnpm install
-pnpm dev
+bun install
+bun dev
```
进入 CLI 后可以直接输入自然语言任务。CLI 默认按当前模式执行:`PLAN` 模式先生成计划,`BUILD` 模式直接参与文件读写和命令执行。计划生成后可用 Enter 确认执行,也可以继续补充修改意见。
@@ -113,6 +123,7 @@ pnpm dev
| `OPENAI_API_KEY` | 必填,OpenAI 兼容 API 的 Key。 |
| `OPENAI_API_BASE_URL` | 可选,自定义 OpenAI 兼容 base URL。 |
| `OPENAI_MODEL` | 必填,模型名称,无默认值;未配置时 CLI 会提示。 |
+| `OPENAI_CONTEXT_WINDOW` | 可选,上下文窗口 token 数,默认 8000。 |
| `AGENT_DESKTOP_DIR` | 可选,覆盖桌面目录路径,便于测试或自定义工作环境。 |
| `SESSION_DB_PATH` | 可选,SQLite 会话库文件路径,默认 `.agent-sessions/sessions.db`。 |
| `CALL_CODE_WEB_DATA` | 可选,CLI 内 `/export` 的输出路径,默认 `packages/client/public/data.json`。 |
@@ -142,28 +153,28 @@ pnpm dev
```bash
# 启动 CLI
-pnpm dev
+bun dev
# 类型检查
-pnpm typecheck
+bun run typecheck
# 类型检查(session-sqlite)
-pnpm exec tsc -p packages/session-sqlite/tsconfig.json --noEmit
+bun x tsc -p packages/session-sqlite/tsconfig.json --noEmit
# 类型检查客户端
-pnpm typecheck:client
+bun run typecheck:client
# 运行全部测试(推荐)
-pnpm test
+bun run test
# 构建 agent-core
-pnpm run build:agent-core
+bun run build:agent-core
# 导出会话历史到 packages/client/public/data.json
-pnpm export:web
+bun run export:web
# 构建 GitHub Pages 客户端
-pnpm build:client
+bun run build:client
```
## 测试说明
From 71cb7f4c1ffdbf1a62358d401b449d422beaa9f5 Mon Sep 17 00:00:00 2001
From: AlkaidSTART <2595006848@qq.com>
Date: Sun, 23 Aug 2026 16:11:47 +0800
Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=90=9C?=
=?UTF-8?q?=E7=B4=A2=E5=B7=A5=E5=85=B7=E7=9A=84=E5=9B=9E=E9=80=80=E6=9C=BA?=
=?UTF-8?q?=E5=88=B6=EF=BC=8C=E6=94=AF=E6=8C=81=E5=9C=A8=E6=9C=AA=E5=AE=89?=
=?UTF-8?q?=E8=A3=85=20ripgrep=20=E6=97=B6=E4=BD=BF=E7=94=A8=20Node=20?=
=?UTF-8?q?=E5=86=85=E7=BD=AE=E6=90=9C=E7=B4=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../agent-core/src/harness/prompt/tool.ts | 2 +-
.../agent-core/src/harness/tools/search.ts | 315 ++++++++++++++----
tests/search-fallback.spec.ts | 91 +++++
3 files changed, 341 insertions(+), 67 deletions(-)
create mode 100644 tests/search-fallback.spec.ts
diff --git a/packages/agent-core/src/harness/prompt/tool.ts b/packages/agent-core/src/harness/prompt/tool.ts
index cfc0918..5fb90d3 100644
--- a/packages/agent-core/src/harness/prompt/tool.ts
+++ b/packages/agent-core/src/harness/prompt/tool.ts
@@ -14,7 +14,7 @@ export const toolPrompt = `
- path 支持:绝对路径、相对路径、~/...、Desktop/...、桌面/...、desktop:/...
4. search(query: string, path?: string, glob?: string, caseSensitive?: boolean, fixedStrings?: boolean, maxResults?: number)
- - 使用 ripgrep 搜索文件内容,返回包含文件名、行列号和匹配文本的结果
+ - 使用 ripgrep 搜索文件内容(未安装时自动回退到 Node 内置搜索),返回包含文件名、行列号和匹配文本的结果
- path 可选,支持 ~/...、Desktop/...、桌面/...、desktop:/...
- 默认智能区分大小写;fixedStrings 为 true 时按纯文本搜索
diff --git a/packages/agent-core/src/harness/tools/search.ts b/packages/agent-core/src/harness/tools/search.ts
index 0470c62..9a5bcc3 100644
--- a/packages/agent-core/src/harness/tools/search.ts
+++ b/packages/agent-core/src/harness/tools/search.ts
@@ -1,14 +1,28 @@
import { execFile } from 'node:child_process';
+import type { Dirent } from 'node:fs';
+import { readdir, readFile, stat } from 'node:fs/promises';
+import path from 'node:path';
import { promisify } from 'node:util';
import { resolveUserPath } from './pathUtils';
const execFileAsync = promisify(execFile);
const DEFAULT_MAX_RESULTS = 100;
const MAX_RESULTS = 1000;
+const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
+const IGNORED_DIRECTORIES = new Set(['node_modules']);
+
+interface NormalizedSearch {
+ query: string;
+ glob?: string;
+ caseSensitive?: boolean;
+ fixedStrings?: boolean;
+ maxResults: number;
+ resolvedPath: string;
+}
export const searchTool = {
name: 'search',
- description: 'Search file contents with ripgrep',
+ description: 'Search file contents with ripgrep or a built-in Node fallback',
parameters: {
type: 'object',
properties: {
@@ -41,80 +55,249 @@ export const searchTool = {
required: ['query'],
},
run: async (input: unknown) => {
- const value = input as {
- query?: unknown;
- path?: unknown;
- glob?: unknown;
- caseSensitive?: unknown;
- fixedStrings?: unknown;
- maxResults?: unknown;
- };
- const query = validateRequiredString(value.query, 'query');
- const searchPath = validateOptionalString(value.path, 'path') ?? '.';
- const glob = validateOptionalString(value.glob, 'glob');
- const caseSensitive = validateOptionalBoolean(
- value.caseSensitive,
- 'caseSensitive',
+ const normalized = normalizeSearchInput(input);
+ try {
+ return await runRipgrepSearch(normalized);
+ } catch (error) {
+ if (isCommandMissing(error)) {
+ return runNodeSearch(normalized);
+ }
+ throw error;
+ }
+ },
+};
+
+const normalizeSearchInput = (input: unknown): NormalizedSearch => {
+ const value = input as {
+ query?: unknown;
+ path?: unknown;
+ glob?: unknown;
+ caseSensitive?: unknown;
+ fixedStrings?: unknown;
+ maxResults?: unknown;
+ };
+ const query = validateRequiredString(value.query, 'query');
+ const searchPath = validateOptionalString(value.path, 'path') ?? '.';
+ const glob = validateOptionalString(value.glob, 'glob');
+ const caseSensitive = validateOptionalBoolean(
+ value.caseSensitive,
+ 'caseSensitive',
+ );
+ const fixedStrings = validateOptionalBoolean(
+ value.fixedStrings,
+ 'fixedStrings',
+ );
+ const maxResults = validateMaxResults(value.maxResults);
+
+ return {
+ query,
+ glob,
+ caseSensitive,
+ fixedStrings,
+ maxResults,
+ resolvedPath: resolveUserPath(searchPath),
+ };
+};
+
+const runRipgrepSearch = async (options: NormalizedSearch) => {
+ const args = ['--line-number', '--column', '--no-heading', '--color', 'never'];
+
+ if (options.caseSensitive === false) {
+ args.push('--ignore-case');
+ } else if (options.caseSensitive === true) {
+ args.push('--case-sensitive');
+ } else {
+ args.push('--smart-case');
+ }
+ if (options.fixedStrings) {
+ args.push('--fixed-strings');
+ }
+ if (options.glob) {
+ args.push('--glob', options.glob);
+ }
+ args.push(
+ '--max-count',
+ String(options.maxResults),
+ '--',
+ options.query,
+ options.resolvedPath,
+ );
+
+ try {
+ const { stdout, stderr } = await execFileAsync('rg', args, {
+ maxBuffer: MAX_BUFFER_BYTES,
+ });
+ const matches = stdout ? stdout.trimEnd().split('\n') : [];
+ return buildSearchResult(options, matches, stderr);
+ } catch (error) {
+ if (isNoMatchError(error)) {
+ return buildSearchResult(options, [], error.stderr ?? '');
+ }
+ throw error;
+ }
+};
+
+const runNodeSearch = async (options: NormalizedSearch) => {
+ const matches: string[] = [];
+ const rootStat = await stat(options.resolvedPath);
+
+ if (rootStat.isDirectory()) {
+ await collectDirectoryMatches(
+ options.resolvedPath,
+ options.resolvedPath,
+ options,
+ matches,
);
- const fixedStrings = validateOptionalBoolean(
- value.fixedStrings,
- 'fixedStrings',
+ } else if (rootStat.isFile()) {
+ await collectFileMatches(
+ options.resolvedPath,
+ options.resolvedPath,
+ options,
+ matches,
);
- const maxResults = validateMaxResults(value.maxResults);
- const resolvedPath = resolveUserPath(searchPath);
- const args = ['--line-number', '--column', '--no-heading', '--color', 'never'];
-
- if (caseSensitive === false) {
- args.push('--ignore-case');
- } else if (caseSensitive === true) {
- args.push('--case-sensitive');
- } else {
- args.push('--smart-case');
+ }
+
+ return buildSearchResult(options, matches);
+};
+
+const collectDirectoryMatches = async (
+ root: string,
+ directory: string,
+ options: NormalizedSearch,
+ matches: string[],
+): Promise => {
+ let entries: Dirent[];
+ try {
+ entries = await readdir(directory, { withFileTypes: true });
+ } catch {
+ return;
+ }
+
+ for (const entry of entries) {
+ if (entry.name.startsWith('.')) {
+ continue;
+ }
+
+ const fullPath = path.join(directory, entry.name);
+ if (entry.isDirectory()) {
+ if (!IGNORED_DIRECTORIES.has(entry.name)) {
+ await collectDirectoryMatches(root, fullPath, options, matches);
+ }
+ continue;
}
- if (fixedStrings) {
- args.push('--fixed-strings');
+
+ if (
+ entry.isFile() &&
+ matchesGlob(path.relative(root, fullPath), options.glob)
+ ) {
+ await collectFileMatches(fullPath, root, options, matches);
}
- if (glob) {
- args.push('--glob', glob);
+ }
+};
+
+const collectFileMatches = async (
+ fullPath: string,
+ root: string,
+ options: NormalizedSearch,
+ matches: string[],
+): Promise => {
+ let content: string;
+ try {
+ content = await readFile(fullPath, 'utf8');
+ } catch {
+ return;
+ }
+
+ const regex = createQueryRegex(options);
+ const displayPath =
+ fullPath === root ? fullPath : toPosixPath(path.relative(root, fullPath));
+ const lines = content.split('\n');
+ let matchedLines = 0;
+
+ for (let index = 0; index < lines.length; index++) {
+ const line = lines[index];
+ const match = regex.exec(line);
+ if (!match) {
+ continue;
}
- args.push('--max-count', String(maxResults), '--', query, resolvedPath);
- try {
- const { stdout, stderr } = await execFileAsync('rg', args, {
- maxBuffer: 10 * 1024 * 1024,
- });
- const matches = stdout ? stdout.trimEnd().split('\n') : [];
-
- return {
- query,
- path: resolvedPath,
- glob,
- caseSensitive: caseSensitive ?? 'smart',
- fixedStrings: fixedStrings ?? false,
- maxResults,
- matchCount: matches.length,
- matches,
- stderr,
- };
- } catch (error) {
- if (isNoMatchError(error)) {
- return {
- query,
- path: resolvedPath,
- glob,
- caseSensitive: caseSensitive ?? 'smart',
- fixedStrings: fixedStrings ?? false,
- maxResults,
- matchCount: 0,
- matches: [],
- stderr: error.stderr ?? '',
- };
- }
- throw error;
+ const column = (match.index ?? 0) + 1;
+ matches.push(`${displayPath}:${index + 1}:${column}:${line}`);
+ matchedLines += 1;
+ if (matchedLines >= options.maxResults) {
+ return;
}
- },
+ }
+};
+
+const createQueryRegex = (options: NormalizedSearch): RegExp => {
+ const source = options.fixedStrings
+ ? escapeRegExp(options.query)
+ : options.query;
+ const useIgnoreCase =
+ options.caseSensitive === false ||
+ (options.caseSensitive === undefined && !/[A-Z]/.test(options.query));
+ return new RegExp(source, useIgnoreCase ? 'i' : '');
};
+const matchesGlob = (
+ relativePath: string,
+ glob: string | undefined,
+): boolean => {
+ if (!glob) {
+ return true;
+ }
+
+ const normalizedPath = toPosixPath(relativePath);
+ const pattern = toPosixPath(glob);
+ const basename = normalizedPath.split('/').pop() ?? normalizedPath;
+
+ if (pattern.startsWith('!')) {
+ const excluded = globToRegExp(pattern.slice(1));
+ return !excluded.test(normalizedPath) && !excluded.test(basename);
+ }
+ if (!pattern.includes('/')) {
+ return globToRegExp(pattern).test(basename);
+ }
+ if (pattern.startsWith('**/')) {
+ return (
+ globToRegExp(pattern).test(normalizedPath) ||
+ globToRegExp(pattern.slice(3)).test(normalizedPath)
+ );
+ }
+ return globToRegExp(pattern).test(normalizedPath);
+};
+
+const globToRegExp = (pattern: string): RegExp => {
+ const source = escapeRegExp(pattern)
+ .replace(/\\\*\\\*/g, '\0')
+ .replace(/\\\*/g, '[^/]*')
+ .replace(/\\\?/g, '[^/]')
+ .replace(/\0/g, '.*');
+ return new RegExp(`^${source}$`);
+};
+
+const escapeRegExp = (value: string): string =>
+ value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+const toPosixPath = (value: string): string => value.split(path.sep).join('/');
+
+const buildSearchResult = (
+ options: NormalizedSearch,
+ matches: string[],
+ stderr = '',
+) => ({
+ query: options.query,
+ path: options.resolvedPath,
+ glob: options.glob,
+ caseSensitive: options.caseSensitive ?? 'smart',
+ fixedStrings: options.fixedStrings ?? false,
+ maxResults: options.maxResults,
+ matchCount: matches.length,
+ matches,
+ stderr,
+});
+
const validateRequiredString = (value: unknown, name: string): string => {
if (typeof value !== 'string' || !value.trim()) {
throw new Error(`Invalid ${name}`);
diff --git a/tests/search-fallback.spec.ts b/tests/search-fallback.spec.ts
new file mode 100644
index 0000000..d53117e
--- /dev/null
+++ b/tests/search-fallback.spec.ts
@@ -0,0 +1,91 @@
+import { mkdtemp, rm, writeFile } from 'node:fs/promises';
+import os from 'node:os';
+import path from 'node:path';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+const { mockExecFile } = vi.hoisted(() => ({ mockExecFile: vi.fn() }));
+
+vi.mock('node:child_process', () => ({ execFile: mockExecFile }));
+
+import { searchTool } from '@tools/search';
+
+type ExecCallback = (
+ error?: Error | null,
+ stdout?: string,
+ stderr?: string,
+) => void;
+
+const simulateMissingRipgrep = () => {
+ mockExecFile.mockImplementation(
+ (_command: string, _args: string[], _options: object, callback: ExecCallback) => {
+ callback(
+ Object.assign(new Error('spawn rg ENOENT'), {
+ code: 'ENOENT',
+ syscall: 'spawn rg',
+ path: 'rg',
+ }),
+ );
+ },
+ );
+};
+
+let tempDir: string | undefined;
+
+afterEach(async () => {
+ if (tempDir) {
+ await rm(tempDir, { recursive: true, force: true });
+ tempDir = undefined;
+ }
+ mockExecFile.mockReset();
+});
+
+describe('search tool ripgrep fallback', () => {
+ it('uses a built-in search when rg is missing', async () => {
+ tempDir = await mkdtemp(path.join(os.tmpdir(), 'agent-search-fallback-'));
+ await writeFile(
+ path.join(tempDir, 'one.ts'),
+ 'Alpha needle\nneedle again\n',
+ );
+ await writeFile(path.join(tempDir, 'two.txt'), 'needle ignored\n');
+ simulateMissingRipgrep();
+
+ const result = await searchTool.run({
+ query: 'needle',
+ path: tempDir,
+ glob: '*.ts',
+ caseSensitive: false,
+ fixedStrings: true,
+ maxResults: 1,
+ });
+
+ expect(mockExecFile).toHaveBeenCalledWith(
+ 'rg',
+ expect.any(Array),
+ expect.any(Object),
+ expect.any(Function),
+ );
+ expect(result).toMatchObject({
+ path: tempDir,
+ glob: '*.ts',
+ caseSensitive: false,
+ fixedStrings: true,
+ maxResults: 1,
+ matchCount: 1,
+ });
+ expect(result.matches[0]).toContain('one.ts:1:7:Alpha needle');
+ });
+
+ it('returns an empty result when the fallback finds nothing', async () => {
+ tempDir = await mkdtemp(path.join(os.tmpdir(), 'agent-search-fallback-'));
+ await writeFile(path.join(tempDir, 'sample.txt'), 'content\n');
+ simulateMissingRipgrep();
+
+ const result = await searchTool.run({
+ query: 'missing',
+ path: tempDir,
+ });
+
+ expect(result.matchCount).toBe(0);
+ expect(result.matches).toEqual([]);
+ });
+});
From d43b3cd75d0ddf3d8538cae4e45d997fa5e5d2e6 Mon Sep 17 00:00:00 2001
From: AlkaidSTART <2595006848@qq.com>
Date: Sun, 23 Aug 2026 16:17:14 +0800
Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?=
=?UTF-8?q?=E4=BB=A4=E7=BC=BA=E5=A4=B1=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?=
=?UTF-8?q?=E5=87=BD=E6=95=B0=EF=BC=8C=E4=BB=A5=E5=A2=9E=E5=BC=BA=E9=94=99?=
=?UTF-8?q?=E8=AF=AF=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/agent-core/src/harness/tools/search.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/packages/agent-core/src/harness/tools/search.ts b/packages/agent-core/src/harness/tools/search.ts
index 9a5bcc3..535d030 100644
--- a/packages/agent-core/src/harness/tools/search.ts
+++ b/packages/agent-core/src/harness/tools/search.ts
@@ -352,3 +352,8 @@ const isNoMatchError = (
error instanceof Error &&
'code' in error &&
(error as { code?: unknown }).code === 1;
+
+const isCommandMissing = (
+ error: unknown,
+): error is Error & { code?: string } =>
+ error instanceof Error && (error as { code?: unknown }).code === 'ENOENT';