jinzd-ai-cli 0.1.23 → 0.1.26
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/CLAUDE.md +212 -17
- package/dist/chunk-BE5SYJ2G.js +382 -0
- package/dist/index.js +833 -113
- package/dist/run-tests-QXZ3ZQ4S.js +9 -0
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -45,15 +45,21 @@ src/
|
|
|
45
45
|
│ ├── dev-state.ts # 开发状态交接(provider/model 切换时快照生成、save/load/clear)
|
|
46
46
|
│ ├── setup-wizard.ts # @inquirer/prompts 首次运行交互式设置
|
|
47
47
|
│ └── commands/
|
|
48
|
-
│ └── index.ts # CommandRegistry +
|
|
48
|
+
│ └── index.ts # CommandRegistry + 26个命令(/help /about /provider /model /clear /compact /plan /session /system /context /status /search /undo /export /copy /cost /init /skill /tools /plugins /mcp /config /checkpoint /review /commands /exit)
|
|
49
|
+
│ ├── custom-commands.ts # CustomCommandManager(~/.aicli/commands/*.md 用户自定义命令)
|
|
50
|
+
├── skills/
|
|
51
|
+
│ ├── types.ts # Skill/SkillMeta 接口、parseSkillFile(YAML frontmatter 解析)
|
|
52
|
+
│ └── manager.ts # SkillManager(加载/激活/停用技能,工具白名单过滤)
|
|
49
53
|
├── mcp/
|
|
50
54
|
│ ├── types.ts # MCP 协议类型定义(JSON-RPC、工具 schema、服务器配置)
|
|
51
55
|
│ ├── client.ts # McpClient(单个 MCP 服务器 STDIO 连接,JSON-RPC 通信)
|
|
52
56
|
│ └── manager.ts # McpManager(多服务器管理,工具发现与注册,MCP→Tool 转换)
|
|
53
57
|
└── tools/
|
|
54
|
-
├── types.ts # ToolDefinition / ToolCall / ToolResult / DangerLevel / getDangerLevel
|
|
58
|
+
├── types.ts # ToolDefinition / ToolCall / ToolResult / DangerLevel / getDangerLevel / isFileWriteTool
|
|
55
59
|
├── registry.ts # ToolRegistry(注册全部内置工具,共15个)
|
|
56
|
-
├──
|
|
60
|
+
├── hooks.ts # 工具执行钩子(runHook,shell 命令模板替换 + execSync)
|
|
61
|
+
├── permissions.ts # 基于规则的工具权限控制(checkPermission,首匹配规则)
|
|
62
|
+
├── executor.ts # ToolExecutor(确认逻辑 + 批量文件写入预览 + batchConfirm + hooks + permissions)
|
|
57
63
|
└── builtin/
|
|
58
64
|
├── bash.ts # bash 工具(Windows: PowerShell + Buffer→UTF-8;Unix: $SHELL)
|
|
59
65
|
├── read-file.ts # read_file 工具
|
|
@@ -63,7 +69,8 @@ src/
|
|
|
63
69
|
├── ask-user.ts # ask_user 工具(agentic 循环中向用户提问,等待文本回答)
|
|
64
70
|
├── write-todos.ts # write_todos 工具(任务拆解与进度跟踪,终端实时渲染)
|
|
65
71
|
├── google-search.ts # google_search 工具(Google Custom Search API 搜索网页)
|
|
66
|
-
|
|
72
|
+
├── spawn-agent.ts # spawn_agent 工具(独立子代理 agentic 循环 + SubAgentExecutor)
|
|
73
|
+
└── run-tests.ts # run_tests 工具(自动检测项目类型、运行测试、JUnit XML 解析、结构化报告)
|
|
67
74
|
```
|
|
68
75
|
|
|
69
76
|
## 常用命令
|
|
@@ -207,6 +214,7 @@ AICLI_NO_STREAM 设为 1 禁用流式输出
|
|
|
207
214
|
| `write_todos` | safe | AI 拆解复杂任务为子任务列表,终端实时渲染进度(pending/in_progress/completed) |
|
|
208
215
|
| `google_search` | safe | Google Custom Search API 搜索网页,需配置 API Key + Search Engine ID (cx) |
|
|
209
216
|
| `spawn_agent` | safe | 委派独立子代理执行特定任务(隔离对话 + agentic 循环,write 自动确认,destructive 阻止) |
|
|
217
|
+
| `run_tests` | safe | 运行项目测试(自动检测 Maven/Gradle/npm/pytest/cargo/go),JUnit XML 解析,结构化报告 |
|
|
210
218
|
| `mcp__*` | safe | MCP 服务器暴露的动态工具(命名格式:`mcp__<serverId>__<toolName>`) |
|
|
211
219
|
|
|
212
220
|
### 危险级别与确认机制
|
|
@@ -312,29 +320,216 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
312
320
|
- [x] **Plan Mode 规划模式**(2026-02-25):`/plan` 进入只读规划(AI 只能用只读工具白名单),提示符显示 `[PLAN]` 黄色标记,`/plan execute` 切回正常模式,`/plan exit` 放弃计划。
|
|
313
321
|
- [x] **Sub-agents 系统**(2026-02-27):`spawn_agent` 工具在同进程中运行独立 agentic 循环。子代理拥有隔离对话、过滤后的工具集(SUBAGENT_ALLOWED_TOOLS)、自动确认 write 操作、阻止 destructive 操作。SubAgentExecutor 带前缀终端输出。
|
|
314
322
|
|
|
315
|
-
### P1 —
|
|
316
|
-
- [
|
|
317
|
-
- [
|
|
318
|
-
- [
|
|
319
|
-
- [
|
|
320
|
-
- [
|
|
323
|
+
### P1 — 重要差距(已全部完成 2026-02-28)
|
|
324
|
+
- [x] **Agent Skills 系统**(2026-02-28):`~/.aicli/skills/*.md` 可复用技能包,YAML frontmatter 声明 name/description/tools 白名单。`/skill list|<name>|off|reload` 命令。激活时注入 system prompt + 过滤工具集(Plan Mode 优先)。
|
|
325
|
+
- [x] **`/init` 项目初始化**(2026-02-28):扫描项目类型(Node/Rust/Python/Go/Java 等)+ 目录结构树,调用当前 AI 生成结构化 AICLI.md。已存在时需 `--force` 覆盖。
|
|
326
|
+
- [x] **`/copy` 剪贴板支持**(2026-02-28):跨平台复制最后 AI 回答到系统剪贴板(Windows: clip / macOS: pbcopy / Linux: xclip 或 xsel),无外部 npm 依赖。
|
|
327
|
+
- [x] **多文件编辑预览**(2026-02-28):`executeAll()` 重构为三组分流(safe→fileWrite→other)。文件写入 2+ 个时走 `executeBatchFileWrites()` 批量 diff 预览 + `batchConfirm()`(`[a]pprove all / [r]eject all / [1,3,5]` 选择性 approve)。
|
|
328
|
+
- [x] **Token 用量统计 `/cost`**(2026-02-28):显示 session 累计 input/output/total tokens + provider/model/消息数。`/cost reset` 重置计数器。
|
|
321
329
|
|
|
322
330
|
### P2 — 增强功能
|
|
323
|
-
- [
|
|
324
|
-
- [
|
|
325
|
-
- [
|
|
326
|
-
- [
|
|
327
|
-
- [
|
|
331
|
+
- [x] **Hooks 系统**(2026-02-28):pre/post tool execution shell 命令钩子(`config.hooks`),模板变量 `{tool}/{dangerLevel}/{args}/{status}`
|
|
332
|
+
- [x] **Permission Rules**(2026-02-28):基于规则的工具权限控制(`config.permissionRules`),auto-approve/deny/confirm,首匹配规则
|
|
333
|
+
- [x] **Checkpointing**(2026-02-28):`/checkpoint save/restore/list/delete`,检查点元数据随 session JSON 持久化
|
|
334
|
+
- [x] **`/review` 代码审查**(2026-02-28):读取 git diff + 上下文,AI 生成结构化审查意见(`--staged`/`--detailed`)
|
|
335
|
+
- [x] **Custom Commands**(2026-02-28):`~/.aicli/commands/*.md` 用户自定义命令,YAML frontmatter + `{{input}}/{{git-diff}}/{{git-context}}/{{file:path}}` 模板变量
|
|
328
336
|
|
|
329
337
|
## 已知待改进项
|
|
330
338
|
|
|
331
339
|
### 低优先级
|
|
332
340
|
- [ ] **macOS/Linux 完整测试**:跨平台逻辑已处理(`$SHELL` fallback、UTF-8 env vars),但尚未在非 Windows 环境实际运行验证。
|
|
333
|
-
- [
|
|
341
|
+
- [x] **Token 用量显示**:已通过 `/cost` 命令实现(v0.1.23),显示 session 累计 input/output/total tokens。
|
|
334
342
|
- [ ] **GitHub 仓库迁移**:当前在 gitee,npm 包受众需要 GitHub 托管。
|
|
335
|
-
- [
|
|
343
|
+
- [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
|
|
336
344
|
- [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
|
|
337
345
|
|
|
346
|
+
## 本轮开发完成记录(2026-03-01,v0.1.25 → v0.1.26)
|
|
347
|
+
|
|
348
|
+
### 新增功能:Context 自动管理 + 测试报告 + 脚手架
|
|
349
|
+
|
|
350
|
+
**功能 1:Context 自动管理**
|
|
351
|
+
- `src/core/constants.ts`:新增 `CONTEXT_PRESSURE_THRESHOLD`(0.8) / `CONTEXT_WARNING_THRESHOLD`(0.6)
|
|
352
|
+
- `src/config/schema.ts`:新增 `autoCompact: z.boolean().default(true)` 配置项
|
|
353
|
+
- `src/repl/repl.ts`:新增 `estimateTokens()`(字符估算 token)、`estimateConversationTokens()`(system prompt + messages 总估算)、`getContextWindowSize()`、`checkContextPressure()`(超 80% 自动 compact)
|
|
354
|
+
- 在 `handleChatSimple()` 和 `handleChatWithTools()` 末尾自动调用 `checkContextPressure()`
|
|
355
|
+
- `/status` 命令新增 `Context%` 行:显示估算 token / context window(绿色 <60%,黄色 60-80%,红色 >80%)
|
|
356
|
+
|
|
357
|
+
**功能 2:`run_tests` 工具 + `/test` 命令**
|
|
358
|
+
- 新增 `src/tools/builtin/run-tests.ts`:
|
|
359
|
+
- 自动检测项目类型:Maven → `mvn test`、Gradle → `gradle test`、npm → `npm test`、pytest、cargo、go test
|
|
360
|
+
- JUnit XML 解析:扫描 `target/surefire-reports/*.xml`,提取 testsuite 属性 + 失败用例详情
|
|
361
|
+
- 通用文本解析:正则匹配 Maven/Jest/pytest/cargo/go 格式的测试结果摘要
|
|
362
|
+
- 彩色终端报告(直接 `console.log()`,绕过 executor 截断)
|
|
363
|
+
- 返回 Markdown 结构化报告给 AI
|
|
364
|
+
- 支持 `command`(自定义命令)和 `filter`(测试名过滤)参数
|
|
365
|
+
- `src/tools/registry.ts`:注册 `runTestsTool`
|
|
366
|
+
- `src/tools/types.ts`:`getDangerLevel()` 中 `run_tests` → `safe`
|
|
367
|
+
- `src/core/constants.ts`:`SUBAGENT_ALLOWED_TOOLS` 新增 `run_tests`,新增 `TEST_TIMEOUT = 300_000`
|
|
368
|
+
- `/test` REPL 命令:快捷方式调用 `executeTests()`
|
|
369
|
+
|
|
370
|
+
**功能 3:`/scaffold` 脚手架命令**
|
|
371
|
+
- `src/repl/commands/index.ts`:新增 `/scaffold <description>` 命令
|
|
372
|
+
- 构建结构化 prompt → 通过 `ctx.sendAsChat()` 注入为用户消息 → AI 使用现有工具(bash/write_file/spawn_agent)自动创建项目骨架
|
|
373
|
+
- `src/repl/repl.ts`:新增 `sendAsChat(message)` 方法——添加 user message + 触发 handleChatWithTools
|
|
374
|
+
- CommandContext 新增 `estimateConversationTokens`、`getContextWindowSize`、`sendAsChat` 三个回调
|
|
375
|
+
|
|
376
|
+
**版本与收尾**
|
|
377
|
+
- `src/core/constants.ts`:VERSION `0.1.25` → `0.1.26`
|
|
378
|
+
- `package.json`:version 同步
|
|
379
|
+
- `src/repl/renderer.ts`:/about 工具计数 15→16,命令计数 26→28,新增 3 条特性(Context 管理、run_tests、/scaffold)
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## 本轮开发完成记录(2026-02-28,v0.1.24 → v0.1.25)
|
|
384
|
+
|
|
385
|
+
### 新增功能:Tab 自动补全 + 流式 Token 计数
|
|
386
|
+
|
|
387
|
+
**功能 1:Tab 自动补全**
|
|
388
|
+
- `src/repl/repl.ts`:`readline.createInterface` 注入 `completer` 回调
|
|
389
|
+
- 新增 `completeInput(line)` 方法:根据输入上下文分发补全逻辑
|
|
390
|
+
- 命令名补全:`/pro<TAB>` → `/provider`(内置 + 自定义命令)
|
|
391
|
+
- 子命令参数:`/provider <TAB>` → provider 列表;`/model <TAB>` → 模型列表;`/checkpoint <TAB>` → save/restore/list/delete 等
|
|
392
|
+
- `@` 文件路径补全:`@src/re<TAB>` → `@src/repl/`(递进目录补全,跳过隐藏文件)
|
|
393
|
+
- 新增 `completeFilePath(partial)` 辅助方法:`readdirSync` + `statSync` 扫描目录,目录追加 `/` 后缀
|
|
394
|
+
|
|
395
|
+
**功能 2:流式 Token 计数(内联显示)**
|
|
396
|
+
- `src/repl/renderer.ts`:`renderStream()` 签名扩展 `showTokens?` + `sessionTotal?`
|
|
397
|
+
- 流结束后在 `\n\n` 之前立即内联显示 token 计数
|
|
398
|
+
- 有精确 usage(OpenAI/Gemini)→ 显示完整 `📊 in X + out Y = Z tokens │ session total: N`
|
|
399
|
+
- 无精确 usage(Claude streaming)→ 基于字符数估算 `📊 ~X output tokens (estimated)`
|
|
400
|
+
- 返回值新增 `tokensShown: boolean` 标志,防止 REPL 重复调用 `renderUsage()`
|
|
401
|
+
- `src/repl/repl.ts`:`handleChatSimple()` + tee 流式路径传入 `showTokens`/`sessionTotal`,条件跳过后续 `renderUsage()`
|
|
402
|
+
|
|
403
|
+
**Bug 修复:DeepSeek contextWindow**
|
|
404
|
+
- `src/providers/deepseek.ts`:`deepseek-chat`(V3)contextWindow 65536 → 131072(128K),与官方 API 文档一致。`deepseek-reasoner`(R1)保持 65536(64K)。
|
|
405
|
+
|
|
406
|
+
**安全加固:web_fetch DNS SSRF 二次校验**
|
|
407
|
+
- `src/tools/builtin/web-fetch.ts`:新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名
|
|
408
|
+
- 域名解析到私有 IP(RFC1918/loopback/link-local)时阻断请求
|
|
409
|
+
- 初始 URL 和 redirect 目标均校验
|
|
410
|
+
- IP 字面量跳过 DNS 解析(已由 `isPrivateHost()` 覆盖)
|
|
411
|
+
- DNS 解析失败不拦截,让后续 fetch 自然报错
|
|
412
|
+
|
|
413
|
+
**版本与收尾**
|
|
414
|
+
- `src/core/constants.ts`:VERSION `0.1.24` → `0.1.25`
|
|
415
|
+
- `package.json`:version 同步
|
|
416
|
+
- `src/repl/renderer.ts`:/about 新增 2 条特性条目
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 本轮开发完成记录(2026-02-28,v0.1.22 → v0.1.23)
|
|
421
|
+
|
|
422
|
+
### P1 全部 5 个功能实现
|
|
423
|
+
|
|
424
|
+
**背景**:P0 全部完成后,进入 P1 重要差距功能开发。一次性实现全部 5 项。
|
|
425
|
+
|
|
426
|
+
### 功能 1:`/copy` 剪贴板支持
|
|
427
|
+
|
|
428
|
+
**实现**:`src/repl/commands/index.ts`
|
|
429
|
+
- `copyToClipboard()` 辅助函数:跨平台调用原生命令(Windows: `clip` / macOS: `pbcopy` / Linux: `xclip -selection clipboard`,fallback `xsel --clipboard --input`)
|
|
430
|
+
- 通过 `execSync` 管道写入,无外部 npm 依赖
|
|
431
|
+
- `CommandContext` 新增 `getLastResponse: () => string`
|
|
432
|
+
- `repl.ts` ctx 注入:`getLastResponse: () => lastResponseStore.content`
|
|
433
|
+
|
|
434
|
+
### 功能 2:`/cost` Token 用量统计
|
|
435
|
+
|
|
436
|
+
**实现**:`src/repl/commands/index.ts`
|
|
437
|
+
- 显示 session 累计 input/output/total tokens + provider/model/消息数
|
|
438
|
+
- `/cost reset` 重置计数器
|
|
439
|
+
- 使用已有 `ctx.getSessionTokenUsage()` 和 `ctx.resetSessionTokenUsage()`
|
|
440
|
+
|
|
441
|
+
### 功能 3:`/init` 项目初始化
|
|
442
|
+
|
|
443
|
+
**实现**:`src/repl/commands/index.ts`
|
|
444
|
+
- `SCAN_SKIP_DIRS` Set(node_modules、.git、dist 等 20+ 目录)
|
|
445
|
+
- `ProjectInfo` 接口 + `scanDirTree()` 递归目录树(深度 4,每层 30 项)
|
|
446
|
+
- `scanProject()` 检测项目类型(package.json/Cargo.toml/pyproject.toml/go.mod 等)
|
|
447
|
+
- `buildInitPrompt()` 构造 AI 提示词,要求生成结构化 AICLI.md
|
|
448
|
+
- `CommandContext` 新增 `chatOnce(prompt, options?)` 方法
|
|
449
|
+
- `repl.ts` ctx 实现:调用 provider.chat() 非流式,temperature=0.3
|
|
450
|
+
- 已存在 AICLI.md 时需 `/init --force` 覆盖
|
|
451
|
+
|
|
452
|
+
### 功能 4:Agent Skills 系统
|
|
453
|
+
|
|
454
|
+
**新文件**:
|
|
455
|
+
- `src/skills/types.ts`:`SkillMeta`(name/description/tools?)、`Skill`(meta/content/filePath)接口;`parseSimpleYaml()`(regex key:value)、`parseYamlArray()`(`[a, b, c]` 格式)、`parseSkillFile()`(读取 .md → 提取 YAML frontmatter → 返回 Skill)
|
|
456
|
+
- `src/skills/manager.ts`:`SkillManager` 类——`loadSkills()`(扫描 skillsDir,自动创建目录)、`activate(name)` / `deactivate()`、`getActivePromptContent()` / `getActiveToolFilter()`(返回工具名 Set)
|
|
457
|
+
|
|
458
|
+
**集成**(`src/repl/repl.ts`):
|
|
459
|
+
- `private skillManager: SkillManager | null` 字段
|
|
460
|
+
- `start()` 中初始化 SkillManager、加载技能、显示数量
|
|
461
|
+
- `buildCurrentSystemPrompt()` 注入激活技能的 content(在项目上下文之后,plan mode 之前)
|
|
462
|
+
- `handleChatWithTools()` 工具过滤:Plan Mode > Skill 白名单 > 全部工具
|
|
463
|
+
- `refreshPrompt()` 显示 `[skill:name]` 洋红色标记
|
|
464
|
+
|
|
465
|
+
**命令**(`/skill`):
|
|
466
|
+
- `/skill` 或 `/skill list`:列出所有技能
|
|
467
|
+
- `/skill <name>`:激活指定技能
|
|
468
|
+
- `/skill off`:停用当前技能
|
|
469
|
+
- `/skill reload`:重新扫描技能目录
|
|
470
|
+
|
|
471
|
+
**常量**:`src/core/constants.ts` 新增 `SKILLS_DIR_NAME = 'skills'`
|
|
472
|
+
|
|
473
|
+
### 功能 5:多文件编辑预览(批量确认)
|
|
474
|
+
|
|
475
|
+
**修改**:
|
|
476
|
+
- `src/tools/types.ts`:新增 `isFileWriteTool(name)` 判断 write_file / edit_file
|
|
477
|
+
- `src/tools/executor.ts`:`executeAll()` 重构为三组分流:
|
|
478
|
+
1. `safeCalls`(safe 级别)→ 先执行(保证 mkdir 等前置操作完成)
|
|
479
|
+
2. `fileWriteCalls`(isFileWriteTool && write)→ 单个走原有确认,2+ 个走批量
|
|
480
|
+
3. `otherCalls`(剩余 write/destructive)→ 逐个原有确认流程
|
|
481
|
+
- 新增 `executeBatchFileWrites(calls)`:展示编号列表 + diff 预览 → `batchConfirm()`
|
|
482
|
+
- 新增 `batchConfirm(count)`:`[a]pprove all / [r]eject all / [1,3,5] approve specific`
|
|
483
|
+
- 使用 `rl.once('line')` 读整行(与 `confirm()` 模式一致)
|
|
484
|
+
- 解析逗号分隔数字编号,返回 `'all' | 'none' | Set<number>`
|
|
485
|
+
- 支持 Ctrl+C 取消(复用 `cancelConfirmFn`)
|
|
486
|
+
|
|
487
|
+
### 架构变更
|
|
488
|
+
- `src/repl/commands/index.ts`:CommandContext 新增 `getLastResponse` / `chatOnce` / `refreshPrompt` / `getSkillManager`;新增 /copy /cost /init /skill 共 4 个命令;/help 更新
|
|
489
|
+
- `src/repl/repl.ts`:SkillManager 集成 + 4 个新 ctx 方法
|
|
490
|
+
- `src/repl/renderer.ts`:`/about` REPL 命令 19 → 23,新增 3 条特性(Agent Skills / /init / 多文件编辑预览)
|
|
491
|
+
|
|
492
|
+
### 版本与发布
|
|
493
|
+
- `src/core/constants.ts`:VERSION `0.1.22` → `0.1.23`
|
|
494
|
+
- `package.json`:version `0.1.22` → `0.1.23`
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## 本轮开发完成记录(2026-02-28,v0.1.23 → v0.1.24)
|
|
499
|
+
|
|
500
|
+
### P2 全部 5 功能实现
|
|
501
|
+
|
|
502
|
+
**功能 1:Hooks 系统(pre/post tool execution)**
|
|
503
|
+
- 新增 `src/tools/hooks.ts`:`ToolHookConfig` 接口 + `runHook(template, vars)` 函数(模板变量替换 → execSync 5s 超时,失败 stderr 警告)
|
|
504
|
+
- `src/config/schema.ts`:新增 `hooks` 可选字段(`preToolExecution`/`postToolExecution`)
|
|
505
|
+
- `src/tools/executor.ts`:新增 `setConfig()` 方法;`execute()` 中 getDangerLevel 后调 pre hook,工具完成/失败后调 post hook
|
|
506
|
+
|
|
507
|
+
**功能 2:Permission Rules(基于规则的工具权限)**
|
|
508
|
+
- 新增 `src/tools/permissions.ts`:`PermissionRule` 接口 + `checkPermission()` 纯函数(首匹配规则,tool 支持 `*` 通配,when 条件含 dangerLevel/pathPattern)
|
|
509
|
+
- `src/config/schema.ts`:新增 `permissionRules` 数组 + `defaultPermission`(默认 `confirm`)
|
|
510
|
+
- `src/tools/executor.ts`:execute() 中 getDangerLevel 后调 checkPermission → deny 直接拒绝 / auto-approve 跳过 confirm / confirm 走原有流程
|
|
511
|
+
|
|
512
|
+
**功能 3:Checkpointing(会话检查点)**
|
|
513
|
+
- `src/session/session.ts`:新增 `CheckpointMeta` 接口(name/messageIndex/timestamp)、`checkpoints` 数组、4 个方法(createCheckpoint/restoreCheckpoint/listCheckpoints/deleteCheckpoint)、toJSON/fromJSON 更新
|
|
514
|
+
- `src/repl/commands/index.ts`:新增 `/checkpoint` 命令(save/restore/list/delete 子命令)
|
|
515
|
+
|
|
516
|
+
**功能 4:`/review` 代码审查**
|
|
517
|
+
- `src/repl/commands/index.ts`:新增 `buildReviewPrompt()` 辅助函数(中文结构化审查 prompt)+ `/review` 命令(`--staged`/`--detailed`,diff 超 8000 字截断保头尾)
|
|
518
|
+
|
|
519
|
+
**功能 5:Custom Commands(用户自定义命令)**
|
|
520
|
+
- 新增 `src/repl/custom-commands.ts`:`CustomCommandManager` 类(loadCommands/listCommands/getCommand)+ `expandTemplate()` 模板变量展开(`{{input}}/{{file:path}}/{{git-diff}}/{{git-context}}`)
|
|
521
|
+
- `src/core/constants.ts`:新增 `CUSTOM_COMMANDS_DIR_NAME = 'commands'`
|
|
522
|
+
- `src/repl/repl.ts`:CustomCommandManager 集成(初始化 + handleCommand fallback + ctx 方法)
|
|
523
|
+
- `src/repl/commands/index.ts`:新增 `/commands` 命令(list/reload)
|
|
524
|
+
|
|
525
|
+
**版本与收尾**
|
|
526
|
+
- `src/core/constants.ts`:VERSION `0.1.23` → `0.1.24`
|
|
527
|
+
- `package.json`:version 同步
|
|
528
|
+
- `src/repl/renderer.ts`:/about 命令计数 23 → 26,新增 5 条 P2 特性条目
|
|
529
|
+
- 所有 P2 路线图条目标记为已完成
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
338
533
|
## 本轮开发完成记录(2026-02-27,v0.1.21 → v0.1.22)
|
|
339
534
|
|
|
340
535
|
### 新增功能:Sub-Agent 子代理系统(`spawn_agent` 工具)
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/tools/builtin/run-tests.ts
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { platform } from "os";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/core/constants.ts
|
|
11
|
+
var VERSION = "0.1.26";
|
|
12
|
+
var APP_NAME = "ai-cli";
|
|
13
|
+
var CONFIG_DIR_NAME = ".aicli";
|
|
14
|
+
var CONFIG_FILE_NAME = "config.json";
|
|
15
|
+
var HISTORY_DIR_NAME = "history";
|
|
16
|
+
var PLUGINS_DIR_NAME = "plugins";
|
|
17
|
+
var SKILLS_DIR_NAME = "skills";
|
|
18
|
+
var CUSTOM_COMMANDS_DIR_NAME = "commands";
|
|
19
|
+
var CONTEXT_FILE_CANDIDATES = ["AICLI.md", "CLAUDE.md"];
|
|
20
|
+
var MEMORY_FILE_NAME = "memory.md";
|
|
21
|
+
var MEMORY_MAX_CHARS = 1e4;
|
|
22
|
+
var DEV_STATE_FILE_NAME = "dev-state.md";
|
|
23
|
+
var DEFAULT_MAX_TOKENS = 8192;
|
|
24
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
25
|
+
var MCP_CONNECT_TIMEOUT = 3e4;
|
|
26
|
+
var MCP_CALL_TIMEOUT = 6e4;
|
|
27
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
28
|
+
var PLAN_MODE_READONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
29
|
+
"read_file",
|
|
30
|
+
"list_dir",
|
|
31
|
+
"grep_files",
|
|
32
|
+
"glob_files",
|
|
33
|
+
"web_fetch",
|
|
34
|
+
"google_search",
|
|
35
|
+
"ask_user",
|
|
36
|
+
// 允许:可向用户澄清需求
|
|
37
|
+
"write_todos"
|
|
38
|
+
// 允许:可输出任务列表作为实施计划
|
|
39
|
+
]);
|
|
40
|
+
var PLAN_MODE_SYSTEM_ADDON = `# \u{1F50D} Plan Mode \u2014 \u53EA\u8BFB\u89C4\u5212\u6A21\u5F0F
|
|
41
|
+
|
|
42
|
+
\u4F60\u5F53\u524D\u5904\u4E8E\u53EA\u8BFB\u89C4\u5212\uFF08Plan\uFF09\u6A21\u5F0F\u3002
|
|
43
|
+
|
|
44
|
+
**\u5141\u8BB8\u7684\u5DE5\u5177**\uFF1Aread_file \xB7 list_dir \xB7 grep_files \xB7 glob_files \xB7 web_fetch \xB7 google_search \xB7 ask_user \xB7 write_todos
|
|
45
|
+
**\u7981\u7528\u7684\u5DE5\u5177**\uFF1Abash \xB7 write_file \xB7 edit_file \xB7 run_interactive \xB7 save_last_response \xB7 save_memory \u53CA\u6240\u6709 MCP \u5DE5\u5177
|
|
46
|
+
|
|
47
|
+
**\u4F60\u7684\u4EFB\u52A1**\uFF1A
|
|
48
|
+
1. \u4F7F\u7528\u53EA\u8BFB\u5DE5\u5177\u5168\u9762\u5206\u6790\u4EE3\u7801\u5E93\u3001\u6587\u4EF6\u7ED3\u6784\u548C\u73B0\u6709\u5B9E\u73B0
|
|
49
|
+
2. \u4F7F\u7528 ask_user \u5411\u7528\u6237\u6F84\u6E05\u4E0D\u660E\u786E\u7684\u9700\u6C42
|
|
50
|
+
3. \u5236\u5B9A\u8BE6\u7EC6\u7684\u5B9E\u65BD\u8BA1\u5212\uFF08\u53EF\u7528 write_todos \u5C55\u793A\u4EFB\u52A1\u5217\u8868\uFF09\uFF0C\u5305\u542B\uFF1A
|
|
51
|
+
- \u9700\u8981\u4FEE\u6539\u6216\u521B\u5EFA\u7684\u6587\u4EF6\u5217\u8868
|
|
52
|
+
- \u6BCF\u4E2A\u6587\u4EF6\u7684\u5177\u4F53\u6539\u52A8\u5185\u5BB9
|
|
53
|
+
- \u6267\u884C\u987A\u5E8F\u548C\u4F9D\u8D56\u5173\u7CFB
|
|
54
|
+
- \u6F5C\u5728\u98CE\u9669\u548C\u6CE8\u610F\u4E8B\u9879
|
|
55
|
+
|
|
56
|
+
\u5B8C\u6210\u89C4\u5212\u540E\uFF0C\u8BF7\u660E\u786E\u544A\u77E5\u7528\u6237\uFF1A\u8F93\u5165 \`/plan execute\` \u5F00\u59CB\u6267\u884C\u8BA1\u5212\uFF0C\u6216 \`/plan exit\` \u653E\u5F03\u6B64\u8BA1\u5212\u3002`;
|
|
57
|
+
var SUBAGENT_DEFAULT_MAX_ROUNDS = 10;
|
|
58
|
+
var SUBAGENT_MAX_ROUNDS_LIMIT = 15;
|
|
59
|
+
var SUBAGENT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
|
|
60
|
+
"bash",
|
|
61
|
+
"read_file",
|
|
62
|
+
"write_file",
|
|
63
|
+
"edit_file",
|
|
64
|
+
"list_dir",
|
|
65
|
+
"grep_files",
|
|
66
|
+
"glob_files",
|
|
67
|
+
"run_interactive",
|
|
68
|
+
"web_fetch",
|
|
69
|
+
"google_search",
|
|
70
|
+
"write_todos",
|
|
71
|
+
"run_tests"
|
|
72
|
+
]);
|
|
73
|
+
var CONTEXT_PRESSURE_THRESHOLD = 0.8;
|
|
74
|
+
var TEST_TIMEOUT = 3e5;
|
|
75
|
+
var AUTHOR = "\u664B\u6B63\u4E1C";
|
|
76
|
+
var AUTHOR_EMAIL = "zhengdong.jin@gmail.com";
|
|
77
|
+
var DESCRIPTION = "\u8DE8\u5E73\u53F0 REPL \u98CE\u683C AI \u5BF9\u8BDD\u5DE5\u5177\uFF0C\u652F\u6301\u591A Provider \u4E0E Agentic \u5DE5\u5177\u8C03\u7528";
|
|
78
|
+
|
|
79
|
+
// src/tools/builtin/run-tests.ts
|
|
80
|
+
var IS_WINDOWS = platform() === "win32";
|
|
81
|
+
function detectProject(cwd) {
|
|
82
|
+
if (existsSync(join(cwd, "pom.xml"))) {
|
|
83
|
+
return { type: "java", framework: "Maven (JUnit)", command: IS_WINDOWS ? "mvn.cmd test" : "mvn test" };
|
|
84
|
+
}
|
|
85
|
+
if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
|
|
86
|
+
const wrapper = IS_WINDOWS ? "gradlew.bat" : "./gradlew";
|
|
87
|
+
const cmd = existsSync(join(cwd, IS_WINDOWS ? "gradlew.bat" : "gradlew")) ? `${wrapper} test` : "gradle test";
|
|
88
|
+
return { type: "java", framework: "Gradle (JUnit)", command: cmd };
|
|
89
|
+
}
|
|
90
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
91
|
+
try {
|
|
92
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
|
|
93
|
+
const testScript = pkg?.scripts?.test;
|
|
94
|
+
if (testScript && testScript !== 'echo "Error: no test specified" && exit 1') {
|
|
95
|
+
return { type: "node", framework: "npm", command: "npm test" };
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py")) || existsSync(join(cwd, "pytest.ini"))) {
|
|
101
|
+
return { type: "python", framework: "pytest", command: "pytest -v" };
|
|
102
|
+
}
|
|
103
|
+
if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
104
|
+
return { type: "rust", framework: "cargo", command: "cargo test" };
|
|
105
|
+
}
|
|
106
|
+
if (existsSync(join(cwd, "go.mod"))) {
|
|
107
|
+
return { type: "go", framework: "go test", command: "go test ./..." };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function parseJUnitXml(xmlContent) {
|
|
112
|
+
const summary = { tests: 0, passed: 0, failures: 0, errors: 0, skipped: 0, duration: 0, failedTests: [] };
|
|
113
|
+
const suiteMatch = xmlContent.match(/<testsuite[^>]*\btests="(\d+)"[^>]*/);
|
|
114
|
+
if (suiteMatch) {
|
|
115
|
+
summary.tests = parseInt(suiteMatch[1], 10);
|
|
116
|
+
const failMatch = xmlContent.match(/<testsuite[^>]*\bfailures="(\d+)"/);
|
|
117
|
+
const errMatch = xmlContent.match(/<testsuite[^>]*\berrors="(\d+)"/);
|
|
118
|
+
const skipMatch = xmlContent.match(/<testsuite[^>]*\bskipped="(\d+)"/);
|
|
119
|
+
const timeMatch = xmlContent.match(/<testsuite[^>]*\btime="([^"]*)"/);
|
|
120
|
+
summary.failures = failMatch ? parseInt(failMatch[1], 10) : 0;
|
|
121
|
+
summary.errors = errMatch ? parseInt(errMatch[1], 10) : 0;
|
|
122
|
+
summary.skipped = skipMatch ? parseInt(skipMatch[1], 10) : 0;
|
|
123
|
+
summary.duration = timeMatch ? parseFloat(timeMatch[1]) : 0;
|
|
124
|
+
summary.passed = summary.tests - summary.failures - summary.errors - summary.skipped;
|
|
125
|
+
}
|
|
126
|
+
const tcPattern = /<testcase[^>]*\bname="([^"]*)"[^>]*classname="([^"]*)"[^>]*>[\s\S]*?<(failure|error)[^>]*>([^<]*)/g;
|
|
127
|
+
let m;
|
|
128
|
+
while ((m = tcPattern.exec(xmlContent)) !== null) {
|
|
129
|
+
summary.failedTests.push({
|
|
130
|
+
name: `${m[2]}#${m[1]}`,
|
|
131
|
+
message: m[4].trim().slice(0, 200)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return summary;
|
|
135
|
+
}
|
|
136
|
+
function findJUnitReports(cwd) {
|
|
137
|
+
const dirs = [
|
|
138
|
+
join(cwd, "target", "surefire-reports"),
|
|
139
|
+
// Maven
|
|
140
|
+
join(cwd, "build", "test-results", "test"),
|
|
141
|
+
// Gradle
|
|
142
|
+
join(cwd, "build", "test-results")
|
|
143
|
+
// Gradle (older)
|
|
144
|
+
];
|
|
145
|
+
const xmlFiles = [];
|
|
146
|
+
for (const dir of dirs) {
|
|
147
|
+
if (!existsSync(dir)) continue;
|
|
148
|
+
try {
|
|
149
|
+
const files = readdirSync(dir);
|
|
150
|
+
for (const f of files) {
|
|
151
|
+
if (f.endsWith(".xml")) xmlFiles.push(join(dir, f));
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return xmlFiles;
|
|
157
|
+
}
|
|
158
|
+
function parseGenericOutput(output) {
|
|
159
|
+
const mvn = output.match(/Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)/);
|
|
160
|
+
if (mvn) {
|
|
161
|
+
const [, tests, failures, errors, skipped] = mvn.map(Number);
|
|
162
|
+
return { tests, failures, errors, skipped, passed: tests - failures - errors - skipped };
|
|
163
|
+
}
|
|
164
|
+
const jest = output.match(/Tests:\s+(?:(\d+)\s+passed,?\s*)?(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+skipped,?\s*)?(\d+)\s+total/);
|
|
165
|
+
if (jest) {
|
|
166
|
+
const passed = parseInt(jest[1] ?? "0", 10);
|
|
167
|
+
const failures = parseInt(jest[2] ?? "0", 10);
|
|
168
|
+
const skipped = parseInt(jest[3] ?? "0", 10);
|
|
169
|
+
const tests = parseInt(jest[4], 10);
|
|
170
|
+
return { tests, passed, failures, skipped, errors: 0 };
|
|
171
|
+
}
|
|
172
|
+
const pytest = output.match(/(\d+)\s+passed(?:.*?(\d+)\s+failed)?(?:.*?(\d+)\s+error)?/);
|
|
173
|
+
if (pytest) {
|
|
174
|
+
const passed = parseInt(pytest[1], 10);
|
|
175
|
+
const failures = parseInt(pytest[2] ?? "0", 10);
|
|
176
|
+
const errors = parseInt(pytest[3] ?? "0", 10);
|
|
177
|
+
return { tests: passed + failures + errors, passed, failures, errors, skipped: 0 };
|
|
178
|
+
}
|
|
179
|
+
const cargo = output.match(/test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed;\s*(\d+)\s+ignored/);
|
|
180
|
+
if (cargo) {
|
|
181
|
+
const passed = parseInt(cargo[1], 10);
|
|
182
|
+
const failures = parseInt(cargo[2], 10);
|
|
183
|
+
const skipped = parseInt(cargo[3], 10);
|
|
184
|
+
return { tests: passed + failures + skipped, passed, failures, skipped, errors: 0 };
|
|
185
|
+
}
|
|
186
|
+
const goPass = (output.match(/^ok\s/gm) ?? []).length;
|
|
187
|
+
const goFail = (output.match(/^FAIL\s/gm) ?? []).length;
|
|
188
|
+
if (goPass + goFail > 0) {
|
|
189
|
+
return { tests: goPass + goFail, passed: goPass, failures: goFail, errors: 0, skipped: 0 };
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
function formatReport(summary, framework, output, exitCode) {
|
|
194
|
+
const status = summary.failures + summary.errors > 0 ? "FAILED \u2717" : "PASSED \u2713";
|
|
195
|
+
const lines = [];
|
|
196
|
+
lines.push(`## Test Results \u2014 ${status}`);
|
|
197
|
+
lines.push(`**Total: ${summary.tests} | Passed: ${summary.passed} | Failed: ${summary.failures}${summary.errors > 0 ? ` | Errors: ${summary.errors}` : ""} | Skipped: ${summary.skipped}**`);
|
|
198
|
+
if (summary.duration > 0) {
|
|
199
|
+
lines.push(`Duration: ${summary.duration.toFixed(1)}s | Framework: ${framework}`);
|
|
200
|
+
} else {
|
|
201
|
+
lines.push(`Framework: ${framework}`);
|
|
202
|
+
}
|
|
203
|
+
if (summary.failedTests.length > 0) {
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push("### Failed Tests");
|
|
206
|
+
for (const ft of summary.failedTests) {
|
|
207
|
+
lines.push(`- ${ft.name} \u2014 ${ft.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const outputLines = output.split("\n");
|
|
211
|
+
const tail = outputLines.length > 150 ? outputLines.slice(-150) : outputLines;
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push(`### Output (last ${tail.length} lines)`);
|
|
214
|
+
lines.push("```");
|
|
215
|
+
lines.push(tail.join("\n"));
|
|
216
|
+
lines.push("```");
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
function renderColorReport(summary, framework) {
|
|
220
|
+
const isPass = summary.failures + summary.errors === 0;
|
|
221
|
+
const status = isPass ? chalk.green.bold("PASSED \u2713") : chalk.red.bold("FAILED \u2717");
|
|
222
|
+
console.log();
|
|
223
|
+
console.log(` ${chalk.bold("Test Results")} \u2014 ${status}`);
|
|
224
|
+
console.log(
|
|
225
|
+
` Total: ${chalk.bold(String(summary.tests))} | ${chalk.green(`Passed: ${summary.passed}`)} | ${chalk.red(`Failed: ${summary.failures}`)}` + (summary.errors > 0 ? ` | ${chalk.red(`Errors: ${summary.errors}`)}` : "") + ` | ${chalk.yellow(`Skipped: ${summary.skipped}`)}`
|
|
226
|
+
);
|
|
227
|
+
if (summary.duration > 0) {
|
|
228
|
+
console.log(` Duration: ${summary.duration.toFixed(1)}s | Framework: ${framework}`);
|
|
229
|
+
}
|
|
230
|
+
if (summary.failedTests.length > 0) {
|
|
231
|
+
console.log(chalk.red("\n Failed Tests:"));
|
|
232
|
+
for (const ft of summary.failedTests) {
|
|
233
|
+
console.log(chalk.red(` - ${ft.name}`));
|
|
234
|
+
if (ft.message) console.log(chalk.dim(` ${ft.message}`));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
console.log();
|
|
238
|
+
}
|
|
239
|
+
async function executeTests(args) {
|
|
240
|
+
const cwd = process.cwd();
|
|
241
|
+
const customCmd = args["command"] ? String(args["command"]).trim() : "";
|
|
242
|
+
const filter = args["filter"] ? String(args["filter"]).trim() : "";
|
|
243
|
+
let command;
|
|
244
|
+
let framework;
|
|
245
|
+
if (customCmd) {
|
|
246
|
+
command = customCmd;
|
|
247
|
+
framework = "custom";
|
|
248
|
+
} else {
|
|
249
|
+
const detected = detectProject(cwd);
|
|
250
|
+
if (!detected) {
|
|
251
|
+
return "Error: Could not detect project type. No pom.xml, build.gradle, package.json, pyproject.toml, Cargo.toml, or go.mod found.\nPlease specify a test command using the `command` parameter.";
|
|
252
|
+
}
|
|
253
|
+
command = detected.command;
|
|
254
|
+
framework = detected.framework;
|
|
255
|
+
if (filter) {
|
|
256
|
+
if (detected.type === "java" && command.includes("mvn")) {
|
|
257
|
+
command += ` -Dtest="${filter}"`;
|
|
258
|
+
} else if (detected.type === "python") {
|
|
259
|
+
command += ` -k "${filter}"`;
|
|
260
|
+
} else if (detected.type === "rust") {
|
|
261
|
+
command += ` ${filter}`;
|
|
262
|
+
} else if (detected.type === "go") {
|
|
263
|
+
command = `go test ./... -run "${filter}"`;
|
|
264
|
+
} else if (detected.type === "node") {
|
|
265
|
+
command += ` -- --grep "${filter}"`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
let output;
|
|
270
|
+
let exitCode = 0;
|
|
271
|
+
try {
|
|
272
|
+
const buf = execSync(command, {
|
|
273
|
+
cwd,
|
|
274
|
+
timeout: TEST_TIMEOUT,
|
|
275
|
+
encoding: "buffer",
|
|
276
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
277
|
+
env: {
|
|
278
|
+
...process.env,
|
|
279
|
+
...IS_WINDOWS ? {} : { FORCE_COLOR: "0" }
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
output = buf.toString("utf-8");
|
|
283
|
+
} catch (err) {
|
|
284
|
+
const e = err;
|
|
285
|
+
exitCode = e.status ?? 1;
|
|
286
|
+
const stdout = e.stdout?.toString("utf-8") ?? "";
|
|
287
|
+
const stderr = e.stderr?.toString("utf-8") ?? "";
|
|
288
|
+
output = stdout + (stderr ? "\n" + stderr : "");
|
|
289
|
+
}
|
|
290
|
+
let summary = {
|
|
291
|
+
tests: 0,
|
|
292
|
+
passed: 0,
|
|
293
|
+
failures: 0,
|
|
294
|
+
errors: 0,
|
|
295
|
+
skipped: 0,
|
|
296
|
+
duration: 0,
|
|
297
|
+
failedTests: []
|
|
298
|
+
};
|
|
299
|
+
const xmlFiles = findJUnitReports(cwd);
|
|
300
|
+
if (xmlFiles.length > 0) {
|
|
301
|
+
for (const xmlFile of xmlFiles) {
|
|
302
|
+
try {
|
|
303
|
+
const xml = readFileSync(xmlFile, "utf-8");
|
|
304
|
+
const parsed = parseJUnitXml(xml);
|
|
305
|
+
summary.tests += parsed.tests;
|
|
306
|
+
summary.passed += parsed.passed;
|
|
307
|
+
summary.failures += parsed.failures;
|
|
308
|
+
summary.errors += parsed.errors;
|
|
309
|
+
summary.skipped += parsed.skipped;
|
|
310
|
+
summary.duration += parsed.duration;
|
|
311
|
+
summary.failedTests.push(...parsed.failedTests);
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
const parsed = parseGenericOutput(output);
|
|
317
|
+
if (parsed) {
|
|
318
|
+
summary = { ...summary, ...parsed };
|
|
319
|
+
} else {
|
|
320
|
+
summary.tests = 1;
|
|
321
|
+
if (exitCode === 0) {
|
|
322
|
+
summary.passed = 1;
|
|
323
|
+
} else {
|
|
324
|
+
summary.failures = 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
renderColorReport(summary, framework);
|
|
329
|
+
return formatReport(summary, framework, output, exitCode);
|
|
330
|
+
}
|
|
331
|
+
var runTestsTool = {
|
|
332
|
+
definition: {
|
|
333
|
+
name: "run_tests",
|
|
334
|
+
description: "Run project tests and return a structured report. Auto-detects project type (Maven/Gradle/npm/pytest/cargo/go). Returns test counts (passed/failed/skipped) and failed test details.",
|
|
335
|
+
parameters: {
|
|
336
|
+
command: {
|
|
337
|
+
type: "string",
|
|
338
|
+
description: "Optional: custom test command to run (overrides auto-detection)",
|
|
339
|
+
required: false
|
|
340
|
+
},
|
|
341
|
+
filter: {
|
|
342
|
+
type: "string",
|
|
343
|
+
description: 'Optional: test name filter/pattern (e.g., "ExamService" to run only matching tests)',
|
|
344
|
+
required: false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
async execute(args) {
|
|
349
|
+
return executeTests(args);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export {
|
|
354
|
+
VERSION,
|
|
355
|
+
APP_NAME,
|
|
356
|
+
CONFIG_DIR_NAME,
|
|
357
|
+
CONFIG_FILE_NAME,
|
|
358
|
+
HISTORY_DIR_NAME,
|
|
359
|
+
PLUGINS_DIR_NAME,
|
|
360
|
+
SKILLS_DIR_NAME,
|
|
361
|
+
CUSTOM_COMMANDS_DIR_NAME,
|
|
362
|
+
CONTEXT_FILE_CANDIDATES,
|
|
363
|
+
MEMORY_FILE_NAME,
|
|
364
|
+
MEMORY_MAX_CHARS,
|
|
365
|
+
DEV_STATE_FILE_NAME,
|
|
366
|
+
DEFAULT_MAX_TOKENS,
|
|
367
|
+
MCP_TOOL_PREFIX,
|
|
368
|
+
MCP_CONNECT_TIMEOUT,
|
|
369
|
+
MCP_CALL_TIMEOUT,
|
|
370
|
+
MCP_PROTOCOL_VERSION,
|
|
371
|
+
PLAN_MODE_READONLY_TOOLS,
|
|
372
|
+
PLAN_MODE_SYSTEM_ADDON,
|
|
373
|
+
SUBAGENT_DEFAULT_MAX_ROUNDS,
|
|
374
|
+
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
375
|
+
SUBAGENT_ALLOWED_TOOLS,
|
|
376
|
+
CONTEXT_PRESSURE_THRESHOLD,
|
|
377
|
+
AUTHOR,
|
|
378
|
+
AUTHOR_EMAIL,
|
|
379
|
+
DESCRIPTION,
|
|
380
|
+
executeTests,
|
|
381
|
+
runTestsTool
|
|
382
|
+
};
|