jinzd-ai-cli 0.2.27 → 0.2.28
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/dist/{chunk-ZJWCFC7I.js → chunk-PVP4QYKG.js} +91 -91
- package/dist/chunk-RMEUZON4.js +468 -0
- package/dist/{chunk-5JC5ZS3M.js → chunk-STOS2HXS.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/{run-tests-JUMTBKQI.js → run-tests-6XMNZEGY.js} +1 -1
- package/dist/run-tests-IICMDEPJ.js +8 -0
- package/dist/{server-2V5IMB4T.js → server-AVYHN2F5.js} +609 -9
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
17
17
|
VERSION,
|
|
18
18
|
runTestsTool
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-STOS2HXS.js";
|
|
20
20
|
|
|
21
21
|
// src/config/config-manager.ts
|
|
22
22
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
@@ -5687,96 +5687,6 @@ async function setupProxy(configProxy) {
|
|
|
5687
5687
|
}
|
|
5688
5688
|
}
|
|
5689
5689
|
|
|
5690
|
-
// src/repl/dev-state.ts
|
|
5691
|
-
import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, mkdirSync as mkdirSync8 } from "fs";
|
|
5692
|
-
import { join as join10 } from "path";
|
|
5693
|
-
import { homedir as homedir4 } from "os";
|
|
5694
|
-
var DEV_STATE_MAX_CHARS = 6e3;
|
|
5695
|
-
var SNAPSHOT_PROMPT = `You are about to be replaced by a different AI model. Please generate a structured development state snapshot so the next model can continue seamlessly.
|
|
5696
|
-
|
|
5697
|
-
CRITICAL: Be SPECIFIC and DETAILED. Include exact values, file paths, format requirements, and constraints \u2014 vague summaries are useless to the next model.
|
|
5698
|
-
|
|
5699
|
-
Output ONLY the snapshot in the following exact format (no preamble, no explanation):
|
|
5700
|
-
|
|
5701
|
-
## Development State Snapshot
|
|
5702
|
-
|
|
5703
|
-
### Current Task
|
|
5704
|
-
[1-2 sentence summary of the primary task/goal the user is working on]
|
|
5705
|
-
|
|
5706
|
-
### Key Parameters & Constraints
|
|
5707
|
-
- [List ALL specific parameters, values, format requirements discovered during this conversation]
|
|
5708
|
-
- [Include exact numbers, dimensions, scoring rules, naming conventions, etc.]
|
|
5709
|
-
- [Example: "Exam duration 90 minutes, total score 200, 40 single-choice x2 pts + 20 multi-choice x4 pts + 20 true/false x2 pts"]
|
|
5710
|
-
- [Example: "File naming format YYYYMMDD-NN-mock-difficulty.md, saved to exam_papers/ directory"]
|
|
5711
|
-
|
|
5712
|
-
### Completed Steps
|
|
5713
|
-
- [List each completed step with specific details (file paths, key outcomes)]
|
|
5714
|
-
|
|
5715
|
-
### In-Progress Work
|
|
5716
|
-
- [List any work that was started but not finished]
|
|
5717
|
-
|
|
5718
|
-
### Critical Reference Files
|
|
5719
|
-
- [List file paths that the next model MUST read before doing any work]
|
|
5720
|
-
- [Include brief description of each file's purpose]
|
|
5721
|
-
- [Example: "Exam2025.md \u2014 reference format for official past exams (90min/200pts/question type distribution)"]
|
|
5722
|
-
- [Example: "style-guide.md \u2014 style guidelines (character setup/scenario building/current events)"]
|
|
5723
|
-
|
|
5724
|
-
### Modified/Created Files
|
|
5725
|
-
- [List any files that were created or modified, with brief notes on content]
|
|
5726
|
-
|
|
5727
|
-
### Key Decisions & Context
|
|
5728
|
-
- [Important decisions, user preferences, or constraints established]
|
|
5729
|
-
|
|
5730
|
-
### Next Steps
|
|
5731
|
-
- [What should be done next to continue this work]
|
|
5732
|
-
- [Include specific instructions the next model should follow]
|
|
5733
|
-
|
|
5734
|
-
### Important Notes
|
|
5735
|
-
- [Any warnings, caveats, or critical context the next model needs to know]
|
|
5736
|
-
- [Things that went wrong or should be avoided]
|
|
5737
|
-
|
|
5738
|
-
If any section has no content, write "(none)" for that section. Be thorough \u2014 the next model may have access to our conversation messages, but the detailed tool call results (file contents, command outputs) are NOT preserved. This snapshot is the primary source of specific details and context.`;
|
|
5739
|
-
function sessionHasMeaningfulContent(messages) {
|
|
5740
|
-
if (messages.length < 2) return false;
|
|
5741
|
-
const hasUser = messages.some((m) => m.role === "user");
|
|
5742
|
-
const hasAssistant = messages.some((m) => m.role === "assistant");
|
|
5743
|
-
return hasUser && hasAssistant;
|
|
5744
|
-
}
|
|
5745
|
-
function getDevStatePath() {
|
|
5746
|
-
return join10(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
|
|
5747
|
-
}
|
|
5748
|
-
function saveDevState(content) {
|
|
5749
|
-
const configDir = join10(homedir4(), CONFIG_DIR_NAME);
|
|
5750
|
-
if (!existsSync14(configDir)) {
|
|
5751
|
-
mkdirSync8(configDir, { recursive: true });
|
|
5752
|
-
}
|
|
5753
|
-
let trimmed = content.trim();
|
|
5754
|
-
if (trimmed.length > DEV_STATE_MAX_CHARS) {
|
|
5755
|
-
trimmed = trimmed.slice(0, DEV_STATE_MAX_CHARS);
|
|
5756
|
-
const lastNewline = trimmed.lastIndexOf("\n");
|
|
5757
|
-
if (lastNewline > DEV_STATE_MAX_CHARS * 0.8) {
|
|
5758
|
-
trimmed = trimmed.slice(0, lastNewline);
|
|
5759
|
-
}
|
|
5760
|
-
trimmed += "\n\n[...truncated]";
|
|
5761
|
-
}
|
|
5762
|
-
writeFileSync7(getDevStatePath(), trimmed, "utf-8");
|
|
5763
|
-
}
|
|
5764
|
-
function loadDevState() {
|
|
5765
|
-
const path = getDevStatePath();
|
|
5766
|
-
if (!existsSync14(path)) return null;
|
|
5767
|
-
const content = readFileSync8(path, "utf-8").trim();
|
|
5768
|
-
return content || null;
|
|
5769
|
-
}
|
|
5770
|
-
function clearDevState() {
|
|
5771
|
-
const path = getDevStatePath();
|
|
5772
|
-
if (existsSync14(path)) {
|
|
5773
|
-
try {
|
|
5774
|
-
unlinkSync3(path);
|
|
5775
|
-
} catch {
|
|
5776
|
-
}
|
|
5777
|
-
}
|
|
5778
|
-
}
|
|
5779
|
-
|
|
5780
5690
|
// src/tools/diff-utils.ts
|
|
5781
5691
|
import chalk4 from "chalk";
|
|
5782
5692
|
function renderDiff(oldText, newText, opts = {}) {
|
|
@@ -5930,6 +5840,96 @@ function simpleDiff(oldLines, newLines) {
|
|
|
5930
5840
|
return result;
|
|
5931
5841
|
}
|
|
5932
5842
|
|
|
5843
|
+
// src/repl/dev-state.ts
|
|
5844
|
+
import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, mkdirSync as mkdirSync8 } from "fs";
|
|
5845
|
+
import { join as join10 } from "path";
|
|
5846
|
+
import { homedir as homedir4 } from "os";
|
|
5847
|
+
var DEV_STATE_MAX_CHARS = 6e3;
|
|
5848
|
+
var SNAPSHOT_PROMPT = `You are about to be replaced by a different AI model. Please generate a structured development state snapshot so the next model can continue seamlessly.
|
|
5849
|
+
|
|
5850
|
+
CRITICAL: Be SPECIFIC and DETAILED. Include exact values, file paths, format requirements, and constraints \u2014 vague summaries are useless to the next model.
|
|
5851
|
+
|
|
5852
|
+
Output ONLY the snapshot in the following exact format (no preamble, no explanation):
|
|
5853
|
+
|
|
5854
|
+
## Development State Snapshot
|
|
5855
|
+
|
|
5856
|
+
### Current Task
|
|
5857
|
+
[1-2 sentence summary of the primary task/goal the user is working on]
|
|
5858
|
+
|
|
5859
|
+
### Key Parameters & Constraints
|
|
5860
|
+
- [List ALL specific parameters, values, format requirements discovered during this conversation]
|
|
5861
|
+
- [Include exact numbers, dimensions, scoring rules, naming conventions, etc.]
|
|
5862
|
+
- [Example: "Exam duration 90 minutes, total score 200, 40 single-choice x2 pts + 20 multi-choice x4 pts + 20 true/false x2 pts"]
|
|
5863
|
+
- [Example: "File naming format YYYYMMDD-NN-mock-difficulty.md, saved to exam_papers/ directory"]
|
|
5864
|
+
|
|
5865
|
+
### Completed Steps
|
|
5866
|
+
- [List each completed step with specific details (file paths, key outcomes)]
|
|
5867
|
+
|
|
5868
|
+
### In-Progress Work
|
|
5869
|
+
- [List any work that was started but not finished]
|
|
5870
|
+
|
|
5871
|
+
### Critical Reference Files
|
|
5872
|
+
- [List file paths that the next model MUST read before doing any work]
|
|
5873
|
+
- [Include brief description of each file's purpose]
|
|
5874
|
+
- [Example: "Exam2025.md \u2014 reference format for official past exams (90min/200pts/question type distribution)"]
|
|
5875
|
+
- [Example: "style-guide.md \u2014 style guidelines (character setup/scenario building/current events)"]
|
|
5876
|
+
|
|
5877
|
+
### Modified/Created Files
|
|
5878
|
+
- [List any files that were created or modified, with brief notes on content]
|
|
5879
|
+
|
|
5880
|
+
### Key Decisions & Context
|
|
5881
|
+
- [Important decisions, user preferences, or constraints established]
|
|
5882
|
+
|
|
5883
|
+
### Next Steps
|
|
5884
|
+
- [What should be done next to continue this work]
|
|
5885
|
+
- [Include specific instructions the next model should follow]
|
|
5886
|
+
|
|
5887
|
+
### Important Notes
|
|
5888
|
+
- [Any warnings, caveats, or critical context the next model needs to know]
|
|
5889
|
+
- [Things that went wrong or should be avoided]
|
|
5890
|
+
|
|
5891
|
+
If any section has no content, write "(none)" for that section. Be thorough \u2014 the next model may have access to our conversation messages, but the detailed tool call results (file contents, command outputs) are NOT preserved. This snapshot is the primary source of specific details and context.`;
|
|
5892
|
+
function sessionHasMeaningfulContent(messages) {
|
|
5893
|
+
if (messages.length < 2) return false;
|
|
5894
|
+
const hasUser = messages.some((m) => m.role === "user");
|
|
5895
|
+
const hasAssistant = messages.some((m) => m.role === "assistant");
|
|
5896
|
+
return hasUser && hasAssistant;
|
|
5897
|
+
}
|
|
5898
|
+
function getDevStatePath() {
|
|
5899
|
+
return join10(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
|
|
5900
|
+
}
|
|
5901
|
+
function saveDevState(content) {
|
|
5902
|
+
const configDir = join10(homedir4(), CONFIG_DIR_NAME);
|
|
5903
|
+
if (!existsSync14(configDir)) {
|
|
5904
|
+
mkdirSync8(configDir, { recursive: true });
|
|
5905
|
+
}
|
|
5906
|
+
let trimmed = content.trim();
|
|
5907
|
+
if (trimmed.length > DEV_STATE_MAX_CHARS) {
|
|
5908
|
+
trimmed = trimmed.slice(0, DEV_STATE_MAX_CHARS);
|
|
5909
|
+
const lastNewline = trimmed.lastIndexOf("\n");
|
|
5910
|
+
if (lastNewline > DEV_STATE_MAX_CHARS * 0.8) {
|
|
5911
|
+
trimmed = trimmed.slice(0, lastNewline);
|
|
5912
|
+
}
|
|
5913
|
+
trimmed += "\n\n[...truncated]";
|
|
5914
|
+
}
|
|
5915
|
+
writeFileSync7(getDevStatePath(), trimmed, "utf-8");
|
|
5916
|
+
}
|
|
5917
|
+
function loadDevState() {
|
|
5918
|
+
const path = getDevStatePath();
|
|
5919
|
+
if (!existsSync14(path)) return null;
|
|
5920
|
+
const content = readFileSync8(path, "utf-8").trim();
|
|
5921
|
+
return content || null;
|
|
5922
|
+
}
|
|
5923
|
+
function clearDevState() {
|
|
5924
|
+
const path = getDevStatePath();
|
|
5925
|
+
if (existsSync14(path)) {
|
|
5926
|
+
try {
|
|
5927
|
+
unlinkSync3(path);
|
|
5928
|
+
} catch {
|
|
5929
|
+
}
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5933
5933
|
// src/tools/hooks.ts
|
|
5934
5934
|
import { execSync as execSync4 } from "child_process";
|
|
5935
5935
|
function shellEscape(value) {
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
// src/tools/builtin/run-tests.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { platform } from "os";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
// src/core/constants.ts
|
|
9
|
+
var VERSION = "0.2.28";
|
|
10
|
+
var APP_NAME = "ai-cli";
|
|
11
|
+
var CONFIG_DIR_NAME = ".aicli";
|
|
12
|
+
var CONFIG_FILE_NAME = "config.json";
|
|
13
|
+
var HISTORY_DIR_NAME = "history";
|
|
14
|
+
var PLUGINS_DIR_NAME = "plugins";
|
|
15
|
+
var SKILLS_DIR_NAME = "skills";
|
|
16
|
+
var CONTEXT_FILE_CANDIDATES = ["AICLI.md", "CLAUDE.md"];
|
|
17
|
+
var MEMORY_FILE_NAME = "memory.md";
|
|
18
|
+
var MEMORY_MAX_CHARS = 1e4;
|
|
19
|
+
var DEV_STATE_FILE_NAME = "dev-state.md";
|
|
20
|
+
var DEFAULT_MAX_TOKENS = 8192;
|
|
21
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
22
|
+
var MCP_PROJECT_CONFIG_NAME = ".mcp.json";
|
|
23
|
+
var MCP_CONNECT_TIMEOUT = 3e4;
|
|
24
|
+
var MCP_CALL_TIMEOUT = 6e4;
|
|
25
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
26
|
+
var PLAN_MODE_READONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
27
|
+
"read_file",
|
|
28
|
+
"list_dir",
|
|
29
|
+
"grep_files",
|
|
30
|
+
"glob_files",
|
|
31
|
+
"web_fetch",
|
|
32
|
+
"google_search",
|
|
33
|
+
"ask_user",
|
|
34
|
+
// 允许:可向用户澄清需求
|
|
35
|
+
"write_todos"
|
|
36
|
+
// 允许:可输出任务列表作为实施计划
|
|
37
|
+
]);
|
|
38
|
+
var PLAN_MODE_SYSTEM_ADDON = `# \u{1F50D} Plan Mode \u2014 Read-Only Planning Mode
|
|
39
|
+
|
|
40
|
+
You are currently in read-only planning (Plan) mode.
|
|
41
|
+
|
|
42
|
+
**Allowed tools**: read_file \xB7 list_dir \xB7 grep_files \xB7 glob_files \xB7 web_fetch \xB7 google_search \xB7 ask_user \xB7 write_todos
|
|
43
|
+
**Disabled tools**: bash \xB7 write_file \xB7 edit_file \xB7 run_interactive \xB7 save_last_response \xB7 save_memory and all MCP tools
|
|
44
|
+
|
|
45
|
+
**Your task**:
|
|
46
|
+
1. Use read-only tools to thoroughly analyze the codebase, file structure, and existing implementation
|
|
47
|
+
2. Use ask_user to clarify any ambiguous requirements with the user
|
|
48
|
+
3. Develop a detailed implementation plan (you may use write_todos to present the task list), including:
|
|
49
|
+
- List of files to be modified or created
|
|
50
|
+
- Specific changes for each file
|
|
51
|
+
- Execution order and dependencies
|
|
52
|
+
- Potential risks and considerations
|
|
53
|
+
|
|
54
|
+
**CRITICAL RULES**:
|
|
55
|
+
- Do NOT attempt to call bash, write_file, edit_file, or any disabled tool \u2014 they will fail silently.
|
|
56
|
+
- Do NOT write shell commands, SQL queries, or code in your text response as a substitute for tool calls \u2014 the user's system will misinterpret this as a pseudo-tool-call error.
|
|
57
|
+
- If the user asks you to run commands, test connections, or modify files, respond with: "This requires execution tools. Please type \`/plan execute\` to switch to execute mode, then I can perform these operations."
|
|
58
|
+
- Do NOT call write_todos repeatedly with the same content \u2014 call it once, then give a text response.
|
|
59
|
+
- Focus your analysis on reading files and producing actionable plans.
|
|
60
|
+
|
|
61
|
+
Once planning is complete, clearly inform the user: type \`/plan execute\` to begin executing the plan, or \`/plan exit\` to discard it.`;
|
|
62
|
+
var SUBAGENT_DEFAULT_MAX_ROUNDS = 10;
|
|
63
|
+
var SUBAGENT_MAX_ROUNDS_LIMIT = 15;
|
|
64
|
+
var SUBAGENT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
|
|
65
|
+
"bash",
|
|
66
|
+
"read_file",
|
|
67
|
+
"write_file",
|
|
68
|
+
"edit_file",
|
|
69
|
+
"list_dir",
|
|
70
|
+
"grep_files",
|
|
71
|
+
"glob_files",
|
|
72
|
+
"run_interactive",
|
|
73
|
+
"web_fetch",
|
|
74
|
+
"google_search",
|
|
75
|
+
"write_todos",
|
|
76
|
+
"run_tests"
|
|
77
|
+
]);
|
|
78
|
+
var TEST_TIMEOUT = 3e5;
|
|
79
|
+
var AGENTIC_BEHAVIOR_GUIDELINE = `# Important Behavioral Guidelines
|
|
80
|
+
|
|
81
|
+
**Respond appropriately to the user's intent \u2014 do NOT over-react**:
|
|
82
|
+
- For **greetings and casual chat** (e.g., "hello", "hi", "hey", "\u4F60\u597D", "what's up"): respond naturally with a friendly greeting. Do NOT use any tools. Do NOT explore directories, read files, or start any project work. Just chat.
|
|
83
|
+
- When the user asks you to "read", "understand", "review", "analyze", "examine", or "look at" files or a project, your task is only to **read and summarize**, then wait for the user's next instruction. Do not automatically start executing tasks described in the project.
|
|
84
|
+
- Only begin using write/execute tools when the user **explicitly requests** an action (e.g., "generate", "create", "modify", "run", "start", etc.).
|
|
85
|
+
- Project context files (CLAUDE.md, AICLI.md) provide background information about the project. They are NOT instructions to start working. Only use them as reference when the user asks a project-related question or task.
|
|
86
|
+
- If you are unsure about the user's intent, use the ask_user tool to confirm with the user, rather than assuming and executing on your own.`;
|
|
87
|
+
|
|
88
|
+
// src/tools/builtin/run-tests.ts
|
|
89
|
+
var IS_WINDOWS = platform() === "win32";
|
|
90
|
+
function detectNodeTestFramework(cwd, pkg) {
|
|
91
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
92
|
+
const deps = pkg.dependencies ?? {};
|
|
93
|
+
const allDeps = { ...deps, ...devDeps };
|
|
94
|
+
if ("vitest" in allDeps || existsSync(join(cwd, "vitest.config.ts")) || existsSync(join(cwd, "vitest.config.js")) || existsSync(join(cwd, "vitest.config.mts"))) {
|
|
95
|
+
return { type: "node", framework: "vitest", command: "npx vitest run" };
|
|
96
|
+
}
|
|
97
|
+
if ("jest" in allDeps || existsSync(join(cwd, "jest.config.js")) || existsSync(join(cwd, "jest.config.ts")) || existsSync(join(cwd, "jest.config.mjs"))) {
|
|
98
|
+
return { type: "node", framework: "jest", command: "npx jest" };
|
|
99
|
+
}
|
|
100
|
+
if ("mocha" in allDeps || existsSync(join(cwd, ".mocharc.yml")) || existsSync(join(cwd, ".mocharc.yaml")) || existsSync(join(cwd, ".mocharc.json")) || existsSync(join(cwd, ".mocharc.js"))) {
|
|
101
|
+
return { type: "node", framework: "mocha", command: "npx mocha" };
|
|
102
|
+
}
|
|
103
|
+
if ("ava" in allDeps) {
|
|
104
|
+
return { type: "node", framework: "ava", command: "npx ava" };
|
|
105
|
+
}
|
|
106
|
+
if ("@playwright/test" in allDeps) {
|
|
107
|
+
return { type: "node", framework: "playwright", command: "npx playwright test" };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function safeReadPackageJson(cwd) {
|
|
112
|
+
const filePath = join(cwd, "package.json");
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = readFileSync(filePath, "utf-8");
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const code = err.code;
|
|
118
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
119
|
+
process.stderr.write(`[Warning] Permission denied reading package.json (${code})
|
|
120
|
+
`);
|
|
121
|
+
} else {
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`[Warning] Cannot read package.json: ${err instanceof Error ? err.message : String(err)}
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (raw.charCodeAt(0) === 65279) {
|
|
130
|
+
raw = raw.slice(1);
|
|
131
|
+
}
|
|
132
|
+
if (!raw.trim()) {
|
|
133
|
+
process.stderr.write("[Warning] package.json is empty\n");
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const parsed = JSON.parse(raw);
|
|
138
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
139
|
+
process.stderr.write("[Warning] package.json root is not a JSON object\n");
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return parsed;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
145
|
+
if (msg.includes("Unexpected token")) {
|
|
146
|
+
process.stderr.write(`[Warning] package.json has syntax error: ${msg}
|
|
147
|
+
`);
|
|
148
|
+
} else if (msg.includes("Unexpected end")) {
|
|
149
|
+
process.stderr.write("[Warning] package.json is truncated or incomplete\n");
|
|
150
|
+
} else {
|
|
151
|
+
process.stderr.write(`[Warning] package.json parse failed: ${msg}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function detectProject(cwd) {
|
|
158
|
+
if (existsSync(join(cwd, "pom.xml"))) {
|
|
159
|
+
return { type: "java", framework: "Maven (JUnit)", command: IS_WINDOWS ? "mvn.cmd test" : "mvn test" };
|
|
160
|
+
}
|
|
161
|
+
if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
|
|
162
|
+
const wrapper = IS_WINDOWS ? "gradlew.bat" : "./gradlew";
|
|
163
|
+
const cmd = existsSync(join(cwd, IS_WINDOWS ? "gradlew.bat" : "gradlew")) ? `${wrapper} test` : "gradle test";
|
|
164
|
+
return { type: "java", framework: "Gradle (JUnit)", command: cmd };
|
|
165
|
+
}
|
|
166
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
167
|
+
const pkg = safeReadPackageJson(cwd);
|
|
168
|
+
if (pkg) {
|
|
169
|
+
const testScript = pkg.scripts?.test;
|
|
170
|
+
if (testScript && testScript !== 'echo "Error: no test specified" && exit 1') {
|
|
171
|
+
return { type: "node", framework: "npm", command: "npm test" };
|
|
172
|
+
}
|
|
173
|
+
const frameworkDetected = detectNodeTestFramework(cwd, pkg);
|
|
174
|
+
if (frameworkDetected) return frameworkDetected;
|
|
175
|
+
} else {
|
|
176
|
+
return { type: "node", framework: "npm (package.json error)", command: "npm test" };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py")) || existsSync(join(cwd, "pytest.ini"))) {
|
|
180
|
+
return { type: "python", framework: "pytest", command: "pytest -v" };
|
|
181
|
+
}
|
|
182
|
+
if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
183
|
+
return { type: "rust", framework: "cargo", command: "cargo test" };
|
|
184
|
+
}
|
|
185
|
+
if (existsSync(join(cwd, "go.mod"))) {
|
|
186
|
+
return { type: "go", framework: "go test", command: "go test ./..." };
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function parseJUnitXml(xmlContent) {
|
|
191
|
+
const summary = { tests: 0, passed: 0, failures: 0, errors: 0, skipped: 0, duration: 0, failedTests: [] };
|
|
192
|
+
const suiteMatch = xmlContent.match(/<testsuite[^>]*\btests="(\d+)"[^>]*/);
|
|
193
|
+
if (suiteMatch) {
|
|
194
|
+
summary.tests = parseInt(suiteMatch[1], 10);
|
|
195
|
+
const failMatch = xmlContent.match(/<testsuite[^>]*\bfailures="(\d+)"/);
|
|
196
|
+
const errMatch = xmlContent.match(/<testsuite[^>]*\berrors="(\d+)"/);
|
|
197
|
+
const skipMatch = xmlContent.match(/<testsuite[^>]*\bskipped="(\d+)"/);
|
|
198
|
+
const timeMatch = xmlContent.match(/<testsuite[^>]*\btime="([^"]*)"/);
|
|
199
|
+
summary.failures = failMatch ? parseInt(failMatch[1], 10) : 0;
|
|
200
|
+
summary.errors = errMatch ? parseInt(errMatch[1], 10) : 0;
|
|
201
|
+
summary.skipped = skipMatch ? parseInt(skipMatch[1], 10) : 0;
|
|
202
|
+
summary.duration = timeMatch ? parseFloat(timeMatch[1]) : 0;
|
|
203
|
+
summary.passed = summary.tests - summary.failures - summary.errors - summary.skipped;
|
|
204
|
+
}
|
|
205
|
+
const tcPattern = /<testcase[^>]*\bname="([^"]*)"[^>]*classname="([^"]*)"[^>]*>[\s\S]*?<(failure|error)[^>]*>([^<]*)/g;
|
|
206
|
+
let m;
|
|
207
|
+
while ((m = tcPattern.exec(xmlContent)) !== null) {
|
|
208
|
+
summary.failedTests.push({
|
|
209
|
+
name: `${m[2]}#${m[1]}`,
|
|
210
|
+
message: m[4].trim().slice(0, 200)
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return summary;
|
|
214
|
+
}
|
|
215
|
+
function findJUnitReports(cwd) {
|
|
216
|
+
const dirs = [
|
|
217
|
+
join(cwd, "target", "surefire-reports"),
|
|
218
|
+
// Maven
|
|
219
|
+
join(cwd, "build", "test-results", "test"),
|
|
220
|
+
// Gradle
|
|
221
|
+
join(cwd, "build", "test-results")
|
|
222
|
+
// Gradle (older)
|
|
223
|
+
];
|
|
224
|
+
const xmlFiles = [];
|
|
225
|
+
for (const dir of dirs) {
|
|
226
|
+
if (!existsSync(dir)) continue;
|
|
227
|
+
try {
|
|
228
|
+
const files = readdirSync(dir);
|
|
229
|
+
for (const f of files) {
|
|
230
|
+
if (f.endsWith(".xml")) xmlFiles.push(join(dir, f));
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return xmlFiles;
|
|
236
|
+
}
|
|
237
|
+
function parseGenericOutput(output) {
|
|
238
|
+
const mvn = output.match(/Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)/);
|
|
239
|
+
if (mvn) {
|
|
240
|
+
const [, tests, failures, errors, skipped] = mvn.map(Number);
|
|
241
|
+
return { tests, failures, errors, skipped, passed: tests - failures - errors - skipped };
|
|
242
|
+
}
|
|
243
|
+
const jest = output.match(/Tests:\s+(?:(\d+)\s+passed,?\s*)?(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+skipped,?\s*)?(\d+)\s+total/);
|
|
244
|
+
if (jest) {
|
|
245
|
+
const passed = parseInt(jest[1] ?? "0", 10);
|
|
246
|
+
const failures = parseInt(jest[2] ?? "0", 10);
|
|
247
|
+
const skipped = parseInt(jest[3] ?? "0", 10);
|
|
248
|
+
const tests = parseInt(jest[4], 10);
|
|
249
|
+
return { tests, passed, failures, skipped, errors: 0 };
|
|
250
|
+
}
|
|
251
|
+
const pytest = output.match(/(\d+)\s+passed(?:.*?(\d+)\s+failed)?(?:.*?(\d+)\s+error)?/);
|
|
252
|
+
if (pytest) {
|
|
253
|
+
const passed = parseInt(pytest[1], 10);
|
|
254
|
+
const failures = parseInt(pytest[2] ?? "0", 10);
|
|
255
|
+
const errors = parseInt(pytest[3] ?? "0", 10);
|
|
256
|
+
return { tests: passed + failures + errors, passed, failures, errors, skipped: 0 };
|
|
257
|
+
}
|
|
258
|
+
const cargo = output.match(/test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed;\s*(\d+)\s+ignored/);
|
|
259
|
+
if (cargo) {
|
|
260
|
+
const passed = parseInt(cargo[1], 10);
|
|
261
|
+
const failures = parseInt(cargo[2], 10);
|
|
262
|
+
const skipped = parseInt(cargo[3], 10);
|
|
263
|
+
return { tests: passed + failures + skipped, passed, failures, skipped, errors: 0 };
|
|
264
|
+
}
|
|
265
|
+
const goPass = (output.match(/^ok\s/gm) ?? []).length;
|
|
266
|
+
const goFail = (output.match(/^FAIL\s/gm) ?? []).length;
|
|
267
|
+
if (goPass + goFail > 0) {
|
|
268
|
+
return { tests: goPass + goFail, passed: goPass, failures: goFail, errors: 0, skipped: 0 };
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function formatReport(summary, framework, output, exitCode) {
|
|
273
|
+
const status = summary.failures + summary.errors > 0 ? "FAILED \u2717" : "PASSED \u2713";
|
|
274
|
+
const lines = [];
|
|
275
|
+
lines.push(`## Test Results \u2014 ${status}`);
|
|
276
|
+
lines.push(`**Total: ${summary.tests} | Passed: ${summary.passed} | Failed: ${summary.failures}${summary.errors > 0 ? ` | Errors: ${summary.errors}` : ""} | Skipped: ${summary.skipped}**`);
|
|
277
|
+
if (summary.duration > 0) {
|
|
278
|
+
lines.push(`Duration: ${summary.duration.toFixed(1)}s | Framework: ${framework}`);
|
|
279
|
+
} else {
|
|
280
|
+
lines.push(`Framework: ${framework}`);
|
|
281
|
+
}
|
|
282
|
+
if (summary.failedTests.length > 0) {
|
|
283
|
+
lines.push("");
|
|
284
|
+
lines.push("### Failed Tests");
|
|
285
|
+
for (const ft of summary.failedTests) {
|
|
286
|
+
lines.push(`- ${ft.name} \u2014 ${ft.message}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const outputLines = output.split("\n");
|
|
290
|
+
const tail = outputLines.length > 150 ? outputLines.slice(-150) : outputLines;
|
|
291
|
+
lines.push("");
|
|
292
|
+
lines.push(`### Output (last ${tail.length} lines)`);
|
|
293
|
+
lines.push("```");
|
|
294
|
+
lines.push(tail.join("\n"));
|
|
295
|
+
lines.push("```");
|
|
296
|
+
return lines.join("\n");
|
|
297
|
+
}
|
|
298
|
+
function renderColorReport(summary, framework) {
|
|
299
|
+
const isPass = summary.failures + summary.errors === 0;
|
|
300
|
+
const status = isPass ? chalk.green.bold("PASSED \u2713") : chalk.red.bold("FAILED \u2717");
|
|
301
|
+
console.log();
|
|
302
|
+
console.log(` ${chalk.bold("Test Results")} \u2014 ${status}`);
|
|
303
|
+
console.log(
|
|
304
|
+
` 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}`)}`
|
|
305
|
+
);
|
|
306
|
+
if (summary.duration > 0) {
|
|
307
|
+
console.log(` Duration: ${summary.duration.toFixed(1)}s | Framework: ${framework}`);
|
|
308
|
+
}
|
|
309
|
+
if (summary.failedTests.length > 0) {
|
|
310
|
+
console.log(chalk.red("\n Failed Tests:"));
|
|
311
|
+
for (const ft of summary.failedTests) {
|
|
312
|
+
console.log(chalk.red(` - ${ft.name}`));
|
|
313
|
+
if (ft.message) console.log(chalk.dim(` ${ft.message}`));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
console.log();
|
|
317
|
+
}
|
|
318
|
+
async function executeTests(args) {
|
|
319
|
+
const cwd = process.cwd();
|
|
320
|
+
const customCmd = args["command"] ? String(args["command"]).trim() : "";
|
|
321
|
+
const filter = args["filter"] ? String(args["filter"]).trim() : "";
|
|
322
|
+
let command;
|
|
323
|
+
let framework;
|
|
324
|
+
if (customCmd) {
|
|
325
|
+
command = customCmd;
|
|
326
|
+
framework = "custom";
|
|
327
|
+
} else {
|
|
328
|
+
const detected = detectProject(cwd);
|
|
329
|
+
if (!detected) {
|
|
330
|
+
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.";
|
|
331
|
+
}
|
|
332
|
+
command = detected.command;
|
|
333
|
+
framework = detected.framework;
|
|
334
|
+
if (filter) {
|
|
335
|
+
if (detected.type === "java" && command.includes("mvn")) {
|
|
336
|
+
command += ` -Dtest="${filter}"`;
|
|
337
|
+
} else if (detected.type === "python") {
|
|
338
|
+
command += ` -k "${filter}"`;
|
|
339
|
+
} else if (detected.type === "rust") {
|
|
340
|
+
command += ` ${filter}`;
|
|
341
|
+
} else if (detected.type === "go") {
|
|
342
|
+
command = `go test ./... -run "${filter}"`;
|
|
343
|
+
} else if (detected.type === "node") {
|
|
344
|
+
if (detected.framework === "vitest") {
|
|
345
|
+
command += ` -t "${filter}"`;
|
|
346
|
+
} else if (detected.framework === "jest") {
|
|
347
|
+
command += ` -t "${filter}"`;
|
|
348
|
+
} else if (detected.framework === "mocha") {
|
|
349
|
+
command += ` --grep "${filter}"`;
|
|
350
|
+
} else if (detected.framework === "playwright") {
|
|
351
|
+
command += ` --grep "${filter}"`;
|
|
352
|
+
} else {
|
|
353
|
+
command += ` -- --grep "${filter}"`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
let output;
|
|
359
|
+
let exitCode = 0;
|
|
360
|
+
try {
|
|
361
|
+
const buf = execSync(command, {
|
|
362
|
+
cwd,
|
|
363
|
+
timeout: TEST_TIMEOUT,
|
|
364
|
+
encoding: "buffer",
|
|
365
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
366
|
+
env: {
|
|
367
|
+
...process.env,
|
|
368
|
+
...IS_WINDOWS ? {} : { FORCE_COLOR: "0" }
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
output = buf.toString("utf-8");
|
|
372
|
+
} catch (err) {
|
|
373
|
+
const e = err;
|
|
374
|
+
exitCode = e.status ?? 1;
|
|
375
|
+
const stdout = e.stdout?.toString("utf-8") ?? "";
|
|
376
|
+
const stderr = e.stderr?.toString("utf-8") ?? "";
|
|
377
|
+
output = stdout + (stderr ? "\n" + stderr : "");
|
|
378
|
+
}
|
|
379
|
+
let summary = {
|
|
380
|
+
tests: 0,
|
|
381
|
+
passed: 0,
|
|
382
|
+
failures: 0,
|
|
383
|
+
errors: 0,
|
|
384
|
+
skipped: 0,
|
|
385
|
+
duration: 0,
|
|
386
|
+
failedTests: []
|
|
387
|
+
};
|
|
388
|
+
const xmlFiles = findJUnitReports(cwd);
|
|
389
|
+
if (xmlFiles.length > 0) {
|
|
390
|
+
for (const xmlFile of xmlFiles) {
|
|
391
|
+
try {
|
|
392
|
+
const xml = readFileSync(xmlFile, "utf-8");
|
|
393
|
+
const parsed = parseJUnitXml(xml);
|
|
394
|
+
summary.tests += parsed.tests;
|
|
395
|
+
summary.passed += parsed.passed;
|
|
396
|
+
summary.failures += parsed.failures;
|
|
397
|
+
summary.errors += parsed.errors;
|
|
398
|
+
summary.skipped += parsed.skipped;
|
|
399
|
+
summary.duration += parsed.duration;
|
|
400
|
+
summary.failedTests.push(...parsed.failedTests);
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
const parsed = parseGenericOutput(output);
|
|
406
|
+
if (parsed) {
|
|
407
|
+
summary = { ...summary, ...parsed };
|
|
408
|
+
} else {
|
|
409
|
+
summary.tests = 1;
|
|
410
|
+
if (exitCode === 0) {
|
|
411
|
+
summary.passed = 1;
|
|
412
|
+
} else {
|
|
413
|
+
summary.failures = 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
renderColorReport(summary, framework);
|
|
418
|
+
return formatReport(summary, framework, output, exitCode);
|
|
419
|
+
}
|
|
420
|
+
var runTestsTool = {
|
|
421
|
+
definition: {
|
|
422
|
+
name: "run_tests",
|
|
423
|
+
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.",
|
|
424
|
+
parameters: {
|
|
425
|
+
command: {
|
|
426
|
+
type: "string",
|
|
427
|
+
description: "Optional: custom test command to run (overrides auto-detection)",
|
|
428
|
+
required: false
|
|
429
|
+
},
|
|
430
|
+
filter: {
|
|
431
|
+
type: "string",
|
|
432
|
+
description: 'Optional: test name filter/pattern (e.g., "ExamService" to run only matching tests)',
|
|
433
|
+
required: false
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
async execute(args) {
|
|
438
|
+
return executeTests(args);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
export {
|
|
443
|
+
VERSION,
|
|
444
|
+
APP_NAME,
|
|
445
|
+
CONFIG_DIR_NAME,
|
|
446
|
+
CONFIG_FILE_NAME,
|
|
447
|
+
HISTORY_DIR_NAME,
|
|
448
|
+
PLUGINS_DIR_NAME,
|
|
449
|
+
SKILLS_DIR_NAME,
|
|
450
|
+
CONTEXT_FILE_CANDIDATES,
|
|
451
|
+
MEMORY_FILE_NAME,
|
|
452
|
+
MEMORY_MAX_CHARS,
|
|
453
|
+
DEV_STATE_FILE_NAME,
|
|
454
|
+
DEFAULT_MAX_TOKENS,
|
|
455
|
+
MCP_TOOL_PREFIX,
|
|
456
|
+
MCP_PROJECT_CONFIG_NAME,
|
|
457
|
+
MCP_CONNECT_TIMEOUT,
|
|
458
|
+
MCP_CALL_TIMEOUT,
|
|
459
|
+
MCP_PROTOCOL_VERSION,
|
|
460
|
+
PLAN_MODE_READONLY_TOOLS,
|
|
461
|
+
PLAN_MODE_SYSTEM_ADDON,
|
|
462
|
+
SUBAGENT_DEFAULT_MAX_ROUNDS,
|
|
463
|
+
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
464
|
+
SUBAGENT_ALLOWED_TOOLS,
|
|
465
|
+
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
466
|
+
executeTests,
|
|
467
|
+
runTestsTool
|
|
468
|
+
};
|