@ia-ccun/code-agent-cli 0.0.54-beta.1 → 0.0.54-beta.4
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/bin/cli.js +116 -74
- package/config/agent/extensions/aicode-advisor.ts +187 -64
- package/config/agent/extensions/aicode-banner.ts +101 -0
- package/config/agent/extensions/aicode-btw.ts +619 -182
- package/config/agent/extensions/aicode-cd.ts +246 -63
- package/config/agent/extensions/aicode-pets.ts +0 -11
- package/config/agent/extensions/aicode-plan.ts +2 -0
- package/config/agent/extensions/aicode-safety-guard.ts +3 -0
- package/config/agent/extensions/aicode-tips.ts +1 -16
- package/config/agent/extensions/context-monitor.ts +30 -15
- package/config/agent/extensions/context.ts +132 -60
- package/config/agent/extensions/custom-footer.ts +42 -24
- package/config/agent/extensions/custom.ts +41 -29
- package/config/agent/extensions/error-monitor.ts +37 -33
- package/config/agent/extensions/goal.ts +8 -6
- package/config/agent/extensions/msg-debug.ts +103 -112
- package/config/agent/extensions/review.ts +365 -164
- package/config/agent/extensions/working-msg.ts +2 -4
- package/config/agent/settings.json +15 -14
- package/config/agent/skills/hello/SKILL.md +1 -1
- package/config/agent/skills/refactor/SKILL.md +40 -0
- package/dist/banner.d.ts.map +1 -1
- package/dist/banner.js +8 -1
- package/dist/banner.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +9 -2
- package/dist/config-loader.js.map +1 -1
- package/package.json +6 -7
- package/scripts/regression/README.md +224 -0
- package/scripts/regression/cases/01-version-and-deps.sh +94 -0
- package/scripts/regression/cases/02-extension-loading.sh +130 -0
- package/scripts/regression/cases/03-rpc-protocol.sh +259 -0
- package/scripts/regression/cases/04-prompt-stream.sh +130 -0
- package/scripts/regression/cases/05-slash-commands-and-skills.sh +119 -0
- package/scripts/regression/cases/06-tool-execution.sh +115 -0
- package/scripts/regression/cases/07-pi-new-features.sh +141 -0
- package/scripts/regression/cases/08-cwd-resilience.sh +84 -0
- package/scripts/regression/lib/common.sh +287 -0
- package/scripts/regression/results/.last_pid +1 -0
- package/scripts/regression/results/.last_workdir +1 -0
- package/scripts/regression/results/02-extension-loading.log +19 -0
- package/scripts/regression/results/03-prompt-stream.log +16 -0
- package/scripts/regression/results/03-rpc-protocol.log +28 -0
- package/scripts/regression/results/04-slash-commands-and-skills.log +11 -0
- package/scripts/regression/results/05-tool-execution.log +8 -0
- package/scripts/regression/results/06-pi-new-features.log +27 -0
- package/scripts/regression/results/08-cwd-resilience.log +1 -0
- package/scripts/regression/run-all.sh +187 -0
- package/config/agent/bin/fd.exe +0 -0
- package/config/agent/bin/rg.exe +0 -0
- package/config/agent/extensions/aicode-filter-outputs.ts +0 -131
- package/config/agent/extensions/aicode-snake.ts +0 -354
- /package/config/agent/{skills_bak → skills}/csv-data-summarizer/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/csv-data-summarizer/analyze.py +0 -0
- /package/config/agent/{skills_bak → skills}/csv-data-summarizer/examples/showcase_financial_pl_data.csv +0 -0
- /package/config/agent/{skills_bak → skills}/csv-data-summarizer/requirements.txt +0 -0
- /package/config/agent/{skills_bak → skills}/csv-data-summarizer/resources/sample.csv +0 -0
- /package/config/agent/{skills_bak → skills}/dispatching-parallel-agents/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/find-skills/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/finishing-a-development-branch/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/LICENSE +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/README.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/README.zh.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/agentloop-core.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/agents/openai.yaml +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/assets/samples/sample-style1-flat.png +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/assets/samples/sample-style2-dark.png +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/assets/samples/sample-style3-blueprint.png +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/assets/samples/sample-style4-notion.png +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/assets/samples/sample-style5-glass.png +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/assets/samples/sample-style6-claude.png +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/assets/samples/sample-style7-openai.png +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/fixtures/agent-memory-types-style4.json +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/fixtures/api-flow-style7.json +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/fixtures/mem0-style1.json +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/fixtures/microservices-style3.json +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/fixtures/multi-agent-style5.json +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/fixtures/system-architecture-style6.json +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/fixtures/tool-call-style2.json +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/package.json +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/icons.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/style-1-flat-icon.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/style-2-dark-terminal.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/style-3-blueprint.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/style-4-notion-clean.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/style-5-glassmorphism.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/style-6-claude-official.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/style-7-openai.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/style-diagram-matrix.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/references/svg-layout-best-practices.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/scripts/README.md +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/scripts/generate-diagram.sh +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/scripts/generate-from-template.py +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/scripts/test-all-styles.sh +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/scripts/validate-svg.sh +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/agent-architecture.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/architecture.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/comparison-matrix.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/data-flow.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/er-diagram.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/flowchart.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/sequence.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/state-machine.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/timeline.svg +0 -0
- /package/config/agent/{skills_bak → skills}/fireworks-tech-graph/templates/use-case.svg +0 -0
- /package/config/agent/{skills_bak → skills}/librarian/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/markdown-to-html/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/LICENSE +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/README.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/references/collection.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/references/database.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/references/index.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/references/partition.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/references/patterns.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/references/user-role.md +0 -0
- /package/config/agent/{skills_bak → skills}/milvus-skill/references/vector.md +0 -0
- /package/config/agent/{skills_bak → skills}/pr/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/receiving-code-review/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/requesting-code-review/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/requesting-code-review/code-reviewer.md +0 -0
- /package/config/agent/{skills_bak → skills}/subagent-driven-development/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/subagent-driven-development/code-quality-reviewer-prompt.md +0 -0
- /package/config/agent/{skills_bak → skills}/subagent-driven-development/implementer-prompt.md +0 -0
- /package/config/agent/{skills_bak → skills}/subagent-driven-development/spec-reviewer-prompt.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/CREATION-LOG.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/condition-based-waiting-example.ts +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/condition-based-waiting.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/defense-in-depth.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/find-polluter.sh +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/root-cause-tracing.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/test-academic.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/test-pressure-1.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/test-pressure-2.md +0 -0
- /package/config/agent/{skills_bak → skills}/systematic-debugging/test-pressure-3.md +0 -0
- /package/config/agent/{skills_bak → skills}/test-driven-development/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/test-driven-development/testing-anti-patterns.md +0 -0
- /package/config/agent/{skills_bak → skills}/unit-test/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/using-git-worktrees/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/zread/README.md +0 -0
- /package/config/agent/{skills_bak → skills}/zread/SKILL.md +0 -0
- /package/config/agent/{skills_bak → skills}/zread/references/stdio-protocol.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id":"ready_probe","type":"response","command":"get_state","success":true,"data":{"model":{"id":"MiniMax-M3","name":"MiniMax-M3","api":"anthropic-messages","provider":"minimax-cn","baseUrl":"https://api.minimaxi.com/anthropic","reasoning":true,"input":["text","image"],"cost":{"input":0.3,"output":1.2,"cacheRead":0.06,"cacheWrite":0,"tiers":[{"inputTokensAbove":512000,"input":0.6,"output":2.4,"cacheRead":0.12,"cacheWrite":0}]},"contextWindow":1000000,"maxTokens":512000,"inputLimits":{"images":{"resize":{"maxWidth":2000,"maxHeight":2000,"maxBytes":4718592,"jpegQuality":80}}},"type":"chat"},"thinkingLevel":"off","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"all","sessionId":"01a11f5b-ace9-709a-a3c4-97d9fb882272","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-all.sh — 串联所有回归测试用例,输出汇总报告
|
|
3
|
+
# 用法:
|
|
4
|
+
# ./run-all.sh # 跑全部 6 个 case
|
|
5
|
+
# ./run-all.sh --only 01,03,05 # 只跑指定 case
|
|
6
|
+
# ./run-all.sh --quiet # 减少输出
|
|
7
|
+
# ./run-all.sh --aicode /path/to/aicode # 指定二进制路径
|
|
8
|
+
# AICODE_BIN=/path/to/aicode ./run-all.sh
|
|
9
|
+
#
|
|
10
|
+
# 退出码:0 全部通过 / 1 有失败 / 2 环境问题(缺 jq 等)
|
|
11
|
+
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
|
|
15
|
+
# 参数解析
|
|
16
|
+
ONLY_CASES=""
|
|
17
|
+
QUIET=0
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--only) ONLY_CASES="$2"; shift 2 ;;
|
|
21
|
+
--quiet|-q) QUIET=1; shift ;;
|
|
22
|
+
--aicode) export AICODE_BIN="$2"; shift 2 ;;
|
|
23
|
+
--help|-h)
|
|
24
|
+
cat <<EOF
|
|
25
|
+
Usage: $0 [--only CASES] [--quiet] [--aicode PATH]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--only CASES 只跑指定 case(逗号分隔,如 01,03,05)
|
|
29
|
+
--quiet, -q 减少输出
|
|
30
|
+
--aicode PATH 指定 aicode 二进制路径(默认:PATH 中的 aicode)
|
|
31
|
+
--help, -h 显示帮助
|
|
32
|
+
|
|
33
|
+
Environment:
|
|
34
|
+
AICODE_BIN aicode 二进制路径(同 --aicode)
|
|
35
|
+
AICODE_CONFIG 配置目录(同 --config)
|
|
36
|
+
EOF
|
|
37
|
+
exit 0
|
|
38
|
+
;;
|
|
39
|
+
*) echo "Unknown arg: $1"; exit 2 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
[[ $QUIET -eq 1 ]] && export REGRESSION_QUIET=1
|
|
44
|
+
|
|
45
|
+
# 前置依赖检查
|
|
46
|
+
echo "═══ aicode-cli 回归测试 ═══"
|
|
47
|
+
echo ""
|
|
48
|
+
echo "AICODE_BIN: ${AICODE_BIN:-aicode}"
|
|
49
|
+
echo "AICODE_CONFIG: ${AICODE_CONFIG:-$HOME/.aicode-cli}"
|
|
50
|
+
echo "SCRIPT_DIR: $SCRIPT_DIR"
|
|
51
|
+
|
|
52
|
+
# jq 必须
|
|
53
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
54
|
+
echo ""
|
|
55
|
+
echo "ERROR: jq 未安装。请:brew install jq"
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# aicode 必须
|
|
60
|
+
if ! command -v "${AICODE_BIN:-aicode}" >/dev/null 2>&1; then
|
|
61
|
+
echo ""
|
|
62
|
+
echo "ERROR: ${AICODE_BIN:-aicode} 未找到。设 AICODE_BIN=/path/to/aicode 或 --aicode PATH"
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
echo ""
|
|
67
|
+
echo "─── 预检 ───"
|
|
68
|
+
"$AICODE_BIN" -v 2>&1 | head -1 | sed 's/^/ version: /'
|
|
69
|
+
echo ""
|
|
70
|
+
|
|
71
|
+
# 收集 case
|
|
72
|
+
ALL_CASES=($(ls "$SCRIPT_DIR/cases/"*.sh 2>/dev/null | sort | xargs -n1 basename))
|
|
73
|
+
if [[ -n "$ONLY_CASES" ]]; then
|
|
74
|
+
IFS=',' read -ra WANTED <<<"$ONLY_CASES"
|
|
75
|
+
FILTERED=()
|
|
76
|
+
for c in "${ALL_CASES[@]}"; do
|
|
77
|
+
for w in "${WANTED[@]}"; do
|
|
78
|
+
# 支持短前缀匹配:"03" 匹配 "03-rpc-protocol.sh"
|
|
79
|
+
if [[ "$c" == "${w}.sh" || "$c" == "${w}-"* ]]; then
|
|
80
|
+
FILTERED+=("$c")
|
|
81
|
+
break
|
|
82
|
+
fi
|
|
83
|
+
done
|
|
84
|
+
done
|
|
85
|
+
if [[ ${#FILTERED[@]} -gt 0 ]]; then
|
|
86
|
+
CASES_TO_RUN=("${FILTERED[@]}")
|
|
87
|
+
else
|
|
88
|
+
CASES_TO_RUN=()
|
|
89
|
+
fi
|
|
90
|
+
else
|
|
91
|
+
CASES_TO_RUN=("${ALL_CASES[@]}")
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [[ ${#CASES_TO_RUN[@]} -eq 0 ]]; then
|
|
95
|
+
echo "ERROR: 没有匹配的 case (--only=$ONLY_CASES)"
|
|
96
|
+
exit 2
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
echo "将运行 ${#CASES_TO_RUN[@]} 个 case: ${CASES_TO_RUN[*]}"
|
|
100
|
+
echo ""
|
|
101
|
+
|
|
102
|
+
# 跑每个 case
|
|
103
|
+
TOTAL_PASS=0
|
|
104
|
+
TOTAL_FAIL=0
|
|
105
|
+
TOTAL_SKIP=0
|
|
106
|
+
FAILED_CASES=()
|
|
107
|
+
|
|
108
|
+
for case_file in "${CASES_TO_RUN[@]}"; do
|
|
109
|
+
echo "━━━ $case_file ━━━"
|
|
110
|
+
chmod +x "$SCRIPT_DIR/cases/$case_file" 2>/dev/null
|
|
111
|
+
|
|
112
|
+
if "$SCRIPT_DIR/cases/$case_file"; then
|
|
113
|
+
# 从 case 输出里提取 pass/fail 数(不太可靠,但能汇总)
|
|
114
|
+
:
|
|
115
|
+
else
|
|
116
|
+
FAILED_CASES+=("$case_file")
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# 统计本 case 的 pass/fail/skip
|
|
120
|
+
last_log="$SCRIPT_DIR/results/${case_file%.sh}.log"
|
|
121
|
+
if [[ -f "$last_log" ]]; then
|
|
122
|
+
# 失败标记
|
|
123
|
+
if grep -q "✗" "$last_log" 2>/dev/null; then
|
|
124
|
+
:
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
# 解析每个 case 的日志
|
|
130
|
+
echo ""
|
|
131
|
+
echo "═══ 汇总 ═══"
|
|
132
|
+
|
|
133
|
+
for case_file in "${CASES_TO_RUN[@]}"; do
|
|
134
|
+
case_name="${case_file%.sh}"
|
|
135
|
+
log="$SCRIPT_DIR/results/${case_name}.log"
|
|
136
|
+
result="?"
|
|
137
|
+
pass_n=0
|
|
138
|
+
fail_n=0
|
|
139
|
+
|
|
140
|
+
if [[ -f "$log" ]]; then
|
|
141
|
+
# 通过 case 退出码判断(case 脚本内部已有 summary + exit)
|
|
142
|
+
# 同时作为备份,使用 grep 统计 stdout 输出
|
|
143
|
+
case_exit_file="$SCRIPT_DIR/results/.case_exit_${case_name}"
|
|
144
|
+
if [[ -f "$case_exit_file" ]]; then
|
|
145
|
+
case_exit=$(cat "$case_exit_file")
|
|
146
|
+
if [[ "$case_exit" -ne 0 ]]; then
|
|
147
|
+
# 整个 case 失败,用 FAIL 计数(不具体到哪个检查)
|
|
148
|
+
fail_n=1
|
|
149
|
+
pass_n=0
|
|
150
|
+
skip_n=0
|
|
151
|
+
else
|
|
152
|
+
pass_n=1
|
|
153
|
+
fail_n=0
|
|
154
|
+
skip_n=0
|
|
155
|
+
fi
|
|
156
|
+
else
|
|
157
|
+
pass_n=0; fail_n=0; skip_n=0
|
|
158
|
+
fi
|
|
159
|
+
else
|
|
160
|
+
pass_n=0; fail_n=0; skip_n=0
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
TOTAL_PASS=$((TOTAL_PASS + pass_n))
|
|
164
|
+
TOTAL_FAIL=$((TOTAL_FAIL + fail_n))
|
|
165
|
+
TOTAL_SKIP=$((TOTAL_SKIP + skip_n))
|
|
166
|
+
|
|
167
|
+
if [[ $fail_n -gt 0 ]]; then
|
|
168
|
+
printf " %-40s ✗ %d fails\n" "$case_name" "$fail_n"
|
|
169
|
+
else
|
|
170
|
+
printf " %-40s ✓ pass\n" "$case_name"
|
|
171
|
+
fi
|
|
172
|
+
done
|
|
173
|
+
|
|
174
|
+
echo ""
|
|
175
|
+
echo "─── 总计 ───"
|
|
176
|
+
echo " PASS: $TOTAL_PASS"
|
|
177
|
+
echo " FAIL: $TOTAL_FAIL"
|
|
178
|
+
echo " SKIP: $TOTAL_SKIP"
|
|
179
|
+
echo ""
|
|
180
|
+
|
|
181
|
+
if [[ $TOTAL_FAIL -eq 0 ]]; then
|
|
182
|
+
echo "✓ ALL CASES PASSED"
|
|
183
|
+
exit 0
|
|
184
|
+
else
|
|
185
|
+
echo "✗ $TOTAL_FAIL FAILURES — 详见 results/*.log"
|
|
186
|
+
exit 1
|
|
187
|
+
fi
|
package/config/agent/bin/fd.exe
DELETED
|
Binary file
|
package/config/agent/bin/rg.exe
DELETED
|
Binary file
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @author: xujianjiang
|
|
3
|
-
* AICode 输出脱敏扩展 — 在 LLM 看到工具结果之前,对内容进行过滤和敏感信息替换
|
|
4
|
-
*
|
|
5
|
-
* 功能说明:
|
|
6
|
-
* - 内容级脱敏:API Key、Token、密码、JWT、数据库连接串、私钥等
|
|
7
|
-
* - 文件级拦截:.env、secrets、credentials 等敏感文件的读取结果整体替换
|
|
8
|
-
* - 错误透传:工具执行失败时不拦截,保留调试线索
|
|
9
|
-
* - UI 友好:TUI 模式下弹出 AICode 风格的通知提示
|
|
10
|
-
*
|
|
11
|
-
* 触发时机:tool_result 钩子(在 Pi 注入到 LLM 上下文之前生效)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
|
|
16
|
-
type SensitivePattern = {
|
|
17
|
-
pattern: RegExp;
|
|
18
|
-
replacement: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function redactText(
|
|
22
|
-
text: string,
|
|
23
|
-
patterns: SensitivePattern[],
|
|
24
|
-
): { text: string; modified: boolean } {
|
|
25
|
-
let result = text;
|
|
26
|
-
let modified = false;
|
|
27
|
-
|
|
28
|
-
for (const { pattern, replacement } of patterns) {
|
|
29
|
-
const redacted = result.replace(pattern, replacement);
|
|
30
|
-
if (redacted !== result) {
|
|
31
|
-
modified = true;
|
|
32
|
-
result = redacted;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return { text: result, modified };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 在 LLM 看到工具结果之前,对内容进行过滤或转换。
|
|
41
|
-
* 对 API Key、Token、密码等敏感数据进行脱敏。
|
|
42
|
-
*/
|
|
43
|
-
export default function (pi: ExtensionAPI) {
|
|
44
|
-
const sensitivePatterns: SensitivePattern[] = [
|
|
45
|
-
{ pattern: /\b(sk-[a-zA-Z0-9]{20,})\b/g, replacement: "[OPENAI_KEY_REDACTED]" },
|
|
46
|
-
{ pattern: /\b(sk-ant-[a-zA-Z0-9_-]{20,})\b/g, replacement: "[ANTHROPIC_KEY_REDACTED]" },
|
|
47
|
-
{ pattern: /\b(sk-or-v1-[a-zA-Z0-9_-]{20,})\b/g, replacement: "[OPENROUTER_KEY_REDACTED]" },
|
|
48
|
-
{ pattern: /\b(AIza[a-zA-Z0-9_-]{30,})\b/g, replacement: "[GOOGLE_KEY_REDACTED]" },
|
|
49
|
-
{
|
|
50
|
-
pattern: /\b(cf(?:k|ut|at)_[a-zA-Z0-9_-]{41,})\b/g,
|
|
51
|
-
replacement: "[CLOUDFLARE_TOKEN_REDACTED]",
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
pattern: /\b(CLOUDFLARE_API_TOKEN|CF_API_TOKEN)\s*=\s*['"]?[a-zA-Z0-9_-]{40,}['"]?/gi,
|
|
55
|
-
replacement: "$1=[CLOUDFLARE_TOKEN_REDACTED]",
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
pattern: /\b(CLOUDFLARE_API_KEY|CF_API_KEY)\s*=\s*['"]?[a-f0-9]{37,45}['"]?/gi,
|
|
59
|
-
replacement: "$1=[CLOUDFLARE_KEY_REDACTED]",
|
|
60
|
-
},
|
|
61
|
-
{ pattern: /\b(npm_[a-zA-Z0-9]{20,})\b/g, replacement: "[NPM_TOKEN_REDACTED]" },
|
|
62
|
-
{ pattern: /\b(glpat-[a-zA-Z0-9_-]{20,})\b/g, replacement: "[GITLAB_TOKEN_REDACTED]" },
|
|
63
|
-
{ pattern: /\b(gh[pousr]_[a-zA-Z0-9]{36,})\b/g, replacement: "[GITHUB_TOKEN_REDACTED]" },
|
|
64
|
-
{ pattern: /\b(xox[baprs]-[a-zA-Z0-9-]{10,})\b/g, replacement: "[SLACK_TOKEN_REDACTED]" },
|
|
65
|
-
{ pattern: /\b(AKIA[A-Z0-9]{16})\b/g, replacement: "[AWS_KEY_REDACTED]" },
|
|
66
|
-
{
|
|
67
|
-
pattern: /\b(api[_-]?key|apikey)\s*[=:]\s*['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
|
|
68
|
-
replacement: "$1=[REDACTED]",
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
pattern: /\b(secret|token|password|passwd|pwd)\s*[=:]\s*['"]?([^\s'"]{8,})['"]?/gi,
|
|
72
|
-
replacement: "$1=[REDACTED]",
|
|
73
|
-
},
|
|
74
|
-
{ pattern: /\b(bearer)\s+([a-zA-Z0-9._-]{20,})\b/gi, replacement: "Bearer [REDACTED]" },
|
|
75
|
-
{
|
|
76
|
-
pattern: /\beyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\b/g,
|
|
77
|
-
replacement: "[JWT_REDACTED]",
|
|
78
|
-
},
|
|
79
|
-
{ pattern: /(mongodb(\+srv)?:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
|
|
80
|
-
{ pattern: /(postgres(ql)?:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
|
|
81
|
-
{ pattern: /(mysql:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
|
|
82
|
-
{ pattern: /(redis:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
|
|
83
|
-
{
|
|
84
|
-
pattern:
|
|
85
|
-
/-----BEGIN (RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END \1PRIVATE KEY-----/g,
|
|
86
|
-
replacement: "[PRIVATE_KEY_REDACTED]",
|
|
87
|
-
},
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
const sensitiveFiles = [
|
|
91
|
-
/(^|\/)\.env$/,
|
|
92
|
-
/(^|\/)\.env\.(?!example$)[^/]+$/,
|
|
93
|
-
/(^|\/)\.dev\.vars($|\.[^/]+$)/,
|
|
94
|
-
/(^|\/)secrets?\.(json|ya?ml|toml)$/i,
|
|
95
|
-
/(^|\/)credentials/i,
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
pi.on("tool_result", async (event, ctx) => {
|
|
99
|
-
if (event.isError) return undefined;
|
|
100
|
-
|
|
101
|
-
if (event.toolName === "read" && typeof event.input.path === "string") {
|
|
102
|
-
const filePath = event.input.path;
|
|
103
|
-
if (/(^|\/)\.env\.example$/i.test(filePath)) {
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
for (const pattern of sensitiveFiles) {
|
|
107
|
-
if (pattern.test(filePath)) {
|
|
108
|
-
if (ctx.hasUI) ctx.ui.notify(`[AICode] 已脱敏敏感文件内容:${filePath}`, "info");
|
|
109
|
-
return {
|
|
110
|
-
content: [{ type: "text", text: `[AICode] 文件 ${filePath} 内容已脱敏,出于安全考虑不予展示` }],
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let wasModified = false;
|
|
117
|
-
const content = event.content.map((item) => {
|
|
118
|
-
if (item.type !== "text") return item;
|
|
119
|
-
const redacted = redactText(item.text, sensitivePatterns);
|
|
120
|
-
if (redacted.modified) wasModified = true;
|
|
121
|
-
return redacted.modified ? { ...item, text: redacted.text } : item;
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (wasModified) {
|
|
125
|
-
if (ctx.hasUI) ctx.ui.notify("[AICode] 已对输出中的敏感信息进行脱敏", "info");
|
|
126
|
-
return { content };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return undefined;
|
|
130
|
-
});
|
|
131
|
-
}
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @author: xujianjiang
|
|
3
|
-
* AICode 贪吃蛇游戏扩展 — 通过 /snake 命令在终端内玩经典贪吃蛇
|
|
4
|
-
*
|
|
5
|
-
* 功能说明:
|
|
6
|
-
* - 经典玩法:方向键或 WASD 控制方向,吃到食物加分
|
|
7
|
-
* - 状态持久化:游戏可暂停,下次执行 /snake 自动恢复上次进度
|
|
8
|
-
* - 最高分记录:跨会话保留历史最高分(High Score)
|
|
9
|
-
* - 自适应渲染:根据终端宽度自动调整游戏区域尺寸
|
|
10
|
-
* - 简洁控制:ESC 暂停并保存,Q 退出不保存,R/空格 重开
|
|
11
|
-
*
|
|
12
|
-
* 使用方法:
|
|
13
|
-
* /snake 启动贪吃蛇游戏(无参数则新建游戏或恢复上次进度)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
18
|
-
|
|
19
|
-
const GAME_WIDTH = 40;
|
|
20
|
-
const GAME_HEIGHT = 15;
|
|
21
|
-
const TICK_MS = 100;
|
|
22
|
-
|
|
23
|
-
type Direction = "up" | "down" | "left" | "right";
|
|
24
|
-
type Point = { x: number; y: number };
|
|
25
|
-
|
|
26
|
-
interface GameState {
|
|
27
|
-
snake: Point[];
|
|
28
|
-
food: Point;
|
|
29
|
-
direction: Direction;
|
|
30
|
-
nextDirection: Direction;
|
|
31
|
-
score: number;
|
|
32
|
-
gameOver: boolean;
|
|
33
|
-
highScore: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function createInitialState(): GameState {
|
|
37
|
-
const startX = Math.floor(GAME_WIDTH / 2);
|
|
38
|
-
const startY = Math.floor(GAME_HEIGHT / 2);
|
|
39
|
-
return {
|
|
40
|
-
snake: [
|
|
41
|
-
{ x: startX, y: startY },
|
|
42
|
-
{ x: startX - 1, y: startY },
|
|
43
|
-
{ x: startX - 2, y: startY },
|
|
44
|
-
],
|
|
45
|
-
food: spawnFood([{ x: startX, y: startY }]),
|
|
46
|
-
direction: "right",
|
|
47
|
-
nextDirection: "right",
|
|
48
|
-
score: 0,
|
|
49
|
-
gameOver: false,
|
|
50
|
-
highScore: 0,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function spawnFood(snake: Point[]): Point {
|
|
55
|
-
let food: Point;
|
|
56
|
-
do {
|
|
57
|
-
food = {
|
|
58
|
-
x: Math.floor(Math.random() * GAME_WIDTH),
|
|
59
|
-
y: Math.floor(Math.random() * GAME_HEIGHT),
|
|
60
|
-
};
|
|
61
|
-
} while (snake.some((s) => s.x === food.x && s.y === food.y));
|
|
62
|
-
return food;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
class SnakeComponent {
|
|
66
|
-
private state: GameState;
|
|
67
|
-
private interval: ReturnType<typeof setInterval> | null = null;
|
|
68
|
-
private onClose: () => void;
|
|
69
|
-
private onSave: (state: GameState | null) => void;
|
|
70
|
-
private tui: { requestRender: () => void };
|
|
71
|
-
private cachedLines: string[] = [];
|
|
72
|
-
private cachedWidth = 0;
|
|
73
|
-
private version = 0;
|
|
74
|
-
private cachedVersion = -1;
|
|
75
|
-
private paused: boolean;
|
|
76
|
-
|
|
77
|
-
constructor(
|
|
78
|
-
tui: { requestRender: () => void },
|
|
79
|
-
onClose: () => void,
|
|
80
|
-
onSave: (state: GameState | null) => void,
|
|
81
|
-
savedState?: GameState,
|
|
82
|
-
) {
|
|
83
|
-
this.tui = tui;
|
|
84
|
-
if (savedState && !savedState.gameOver) {
|
|
85
|
-
// Resume from saved state, start paused
|
|
86
|
-
this.state = savedState;
|
|
87
|
-
this.paused = true;
|
|
88
|
-
} else {
|
|
89
|
-
// New game or saved game was over
|
|
90
|
-
this.state = createInitialState();
|
|
91
|
-
if (savedState) {
|
|
92
|
-
this.state.highScore = savedState.highScore;
|
|
93
|
-
}
|
|
94
|
-
this.paused = false;
|
|
95
|
-
this.startGame();
|
|
96
|
-
}
|
|
97
|
-
this.onClose = onClose;
|
|
98
|
-
this.onSave = onSave;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private startGame(): void {
|
|
102
|
-
this.interval = setInterval(() => {
|
|
103
|
-
if (!this.state.gameOver) {
|
|
104
|
-
this.tick();
|
|
105
|
-
this.version++;
|
|
106
|
-
this.tui.requestRender();
|
|
107
|
-
}
|
|
108
|
-
}, TICK_MS);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private tick(): void {
|
|
112
|
-
// Apply queued direction change
|
|
113
|
-
this.state.direction = this.state.nextDirection;
|
|
114
|
-
|
|
115
|
-
// Calculate new head position
|
|
116
|
-
const head = this.state.snake[0];
|
|
117
|
-
let newHead: Point;
|
|
118
|
-
|
|
119
|
-
switch (this.state.direction) {
|
|
120
|
-
case "up":
|
|
121
|
-
newHead = { x: head.x, y: head.y - 1 };
|
|
122
|
-
break;
|
|
123
|
-
case "down":
|
|
124
|
-
newHead = { x: head.x, y: head.y + 1 };
|
|
125
|
-
break;
|
|
126
|
-
case "left":
|
|
127
|
-
newHead = { x: head.x - 1, y: head.y };
|
|
128
|
-
break;
|
|
129
|
-
case "right":
|
|
130
|
-
newHead = { x: head.x + 1, y: head.y };
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Check wall collision
|
|
135
|
-
if (newHead.x < 0 || newHead.x >= GAME_WIDTH || newHead.y < 0 || newHead.y >= GAME_HEIGHT) {
|
|
136
|
-
this.state.gameOver = true;
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Check self collision
|
|
141
|
-
if (this.state.snake.some((s) => s.x === newHead.x && s.y === newHead.y)) {
|
|
142
|
-
this.state.gameOver = true;
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Move snake
|
|
147
|
-
this.state.snake.unshift(newHead);
|
|
148
|
-
|
|
149
|
-
// Check food collision
|
|
150
|
-
if (newHead.x === this.state.food.x && newHead.y === this.state.food.y) {
|
|
151
|
-
this.state.score += 10;
|
|
152
|
-
if (this.state.score > this.state.highScore) {
|
|
153
|
-
this.state.highScore = this.state.score;
|
|
154
|
-
}
|
|
155
|
-
this.state.food = spawnFood(this.state.snake);
|
|
156
|
-
} else {
|
|
157
|
-
this.state.snake.pop();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
handleInput(data: string): void {
|
|
162
|
-
// If paused (resuming), wait for any key
|
|
163
|
-
if (this.paused) {
|
|
164
|
-
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
165
|
-
// Quit without clearing save
|
|
166
|
-
this.dispose();
|
|
167
|
-
this.onClose();
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
// Any other key resumes
|
|
171
|
-
this.paused = false;
|
|
172
|
-
this.startGame();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ESC to pause and save
|
|
177
|
-
if (matchesKey(data, "escape")) {
|
|
178
|
-
this.dispose();
|
|
179
|
-
this.onSave(this.state);
|
|
180
|
-
this.onClose();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Q to quit without saving (clears saved state)
|
|
185
|
-
if (data === "q" || data === "Q") {
|
|
186
|
-
this.dispose();
|
|
187
|
-
this.onSave(null); // Clear saved state
|
|
188
|
-
this.onClose();
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Arrow keys or WASD
|
|
193
|
-
if (matchesKey(data, "up") || data === "w" || data === "W") {
|
|
194
|
-
if (this.state.direction !== "down") this.state.nextDirection = "up";
|
|
195
|
-
} else if (matchesKey(data, "down") || data === "s" || data === "S") {
|
|
196
|
-
if (this.state.direction !== "up") this.state.nextDirection = "down";
|
|
197
|
-
} else if (matchesKey(data, "right") || data === "d" || data === "D") {
|
|
198
|
-
if (this.state.direction !== "left") this.state.nextDirection = "right";
|
|
199
|
-
} else if (matchesKey(data, "left") || data === "a" || data === "A") {
|
|
200
|
-
if (this.state.direction !== "right") this.state.nextDirection = "left";
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Restart on game over
|
|
204
|
-
if (this.state.gameOver && (data === "r" || data === "R" || data === " ")) {
|
|
205
|
-
const highScore = this.state.highScore;
|
|
206
|
-
this.state = createInitialState();
|
|
207
|
-
this.state.highScore = highScore;
|
|
208
|
-
this.onSave(null); // Clear saved state on restart
|
|
209
|
-
this.version++;
|
|
210
|
-
this.tui.requestRender();
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
invalidate(): void {
|
|
215
|
-
this.cachedWidth = 0;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
render(width: number): string[] {
|
|
219
|
-
if (width === this.cachedWidth && this.cachedVersion === this.version) {
|
|
220
|
-
return this.cachedLines;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const lines: string[] = [];
|
|
224
|
-
|
|
225
|
-
// Each game cell is 2 chars wide to appear square (terminal cells are ~2:1 aspect)
|
|
226
|
-
const cellWidth = 2;
|
|
227
|
-
const effectiveWidth = Math.min(GAME_WIDTH, Math.floor((width - 4) / cellWidth));
|
|
228
|
-
const effectiveHeight = GAME_HEIGHT;
|
|
229
|
-
|
|
230
|
-
// Colors
|
|
231
|
-
const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
|
|
232
|
-
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
233
|
-
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
234
|
-
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
235
|
-
const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
|
|
236
|
-
|
|
237
|
-
const boxWidth = effectiveWidth * cellWidth;
|
|
238
|
-
|
|
239
|
-
// Helper to pad content inside box
|
|
240
|
-
const boxLine = (content: string) => {
|
|
241
|
-
const contentLen = visibleWidth(content);
|
|
242
|
-
const padding = Math.max(0, boxWidth - contentLen);
|
|
243
|
-
return dim(" │") + content + " ".repeat(padding) + dim("│");
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
// Top border
|
|
247
|
-
lines.push(this.padLine(dim(` ╭${"─".repeat(boxWidth)}╮`), width));
|
|
248
|
-
|
|
249
|
-
// Header with score
|
|
250
|
-
const scoreText = `Score: ${bold(yellow(String(this.state.score)))}`;
|
|
251
|
-
const highText = `High: ${bold(yellow(String(this.state.highScore)))}`;
|
|
252
|
-
const title = `${bold(green("SNAKE"))} │ ${scoreText} │ ${highText}`;
|
|
253
|
-
lines.push(this.padLine(boxLine(title), width));
|
|
254
|
-
|
|
255
|
-
// Separator
|
|
256
|
-
lines.push(this.padLine(dim(` ├${"─".repeat(boxWidth)}┤`), width));
|
|
257
|
-
|
|
258
|
-
// Game grid
|
|
259
|
-
for (let y = 0; y < effectiveHeight; y++) {
|
|
260
|
-
let row = "";
|
|
261
|
-
for (let x = 0; x < effectiveWidth; x++) {
|
|
262
|
-
const isHead = this.state.snake[0].x === x && this.state.snake[0].y === y;
|
|
263
|
-
const isBody = this.state.snake.slice(1).some((s) => s.x === x && s.y === y);
|
|
264
|
-
const isFood = this.state.food.x === x && this.state.food.y === y;
|
|
265
|
-
|
|
266
|
-
if (isHead) {
|
|
267
|
-
row += green("██"); // Snake head (2 chars)
|
|
268
|
-
} else if (isBody) {
|
|
269
|
-
row += green("▓▓"); // Snake body (2 chars)
|
|
270
|
-
} else if (isFood) {
|
|
271
|
-
row += red("◆ "); // Food (2 chars)
|
|
272
|
-
} else {
|
|
273
|
-
row += " "; // Empty cell (2 spaces)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
lines.push(this.padLine(dim(" │") + row + dim("│"), width));
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Separator
|
|
280
|
-
lines.push(this.padLine(dim(` ├${"─".repeat(boxWidth)}┤`), width));
|
|
281
|
-
|
|
282
|
-
// Footer
|
|
283
|
-
let footer: string;
|
|
284
|
-
if (this.paused) {
|
|
285
|
-
footer = `${yellow(bold("PAUSED"))} Press any key to continue, ${bold("Q")} to quit`;
|
|
286
|
-
} else if (this.state.gameOver) {
|
|
287
|
-
footer = `${red(bold("GAME OVER!"))} Press ${bold("R")} to restart, ${bold("Q")} to quit`;
|
|
288
|
-
} else {
|
|
289
|
-
footer = `↑↓←→ or WASD to move, ${bold("ESC")} pause, ${bold("Q")} quit`;
|
|
290
|
-
}
|
|
291
|
-
lines.push(this.padLine(boxLine(footer), width));
|
|
292
|
-
|
|
293
|
-
// Bottom border
|
|
294
|
-
lines.push(this.padLine(dim(` ╰${"─".repeat(boxWidth)}╯`), width));
|
|
295
|
-
|
|
296
|
-
this.cachedLines = lines;
|
|
297
|
-
this.cachedWidth = width;
|
|
298
|
-
this.cachedVersion = this.version;
|
|
299
|
-
|
|
300
|
-
return lines;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private padLine(line: string, width: number): string {
|
|
304
|
-
// Calculate visible length (strip ANSI codes)
|
|
305
|
-
const visibleLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
306
|
-
const padding = Math.max(0, width - visibleLen);
|
|
307
|
-
return line + " ".repeat(padding);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
dispose(): void {
|
|
311
|
-
if (this.interval) {
|
|
312
|
-
clearInterval(this.interval);
|
|
313
|
-
this.interval = null;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const SNAKE_SAVE_TYPE = "snake-save";
|
|
319
|
-
|
|
320
|
-
export default function (pi: ExtensionAPI) {
|
|
321
|
-
pi.registerCommand("snake", {
|
|
322
|
-
description: "AICode 贪吃蛇游戏",
|
|
323
|
-
|
|
324
|
-
handler: async (_args, ctx) => {
|
|
325
|
-
if (!ctx.hasUI) {
|
|
326
|
-
ctx.ui.notify("[AICode] 贪吃蛇需要交互模式才能运行", "error");
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Load saved state from session
|
|
331
|
-
const entries = ctx.sessionManager.getEntries();
|
|
332
|
-
let savedState: GameState | undefined;
|
|
333
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
334
|
-
const entry = entries[i];
|
|
335
|
-
if (entry.type === "custom" && entry.customType === SNAKE_SAVE_TYPE) {
|
|
336
|
-
savedState = entry.data as GameState;
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
await ctx.ui.custom((tui, _theme, _kb, done) => {
|
|
342
|
-
return new SnakeComponent(
|
|
343
|
-
tui,
|
|
344
|
-
() => done(undefined),
|
|
345
|
-
(state) => {
|
|
346
|
-
// Save or clear state
|
|
347
|
-
pi.appendEntry(SNAKE_SAVE_TYPE, state);
|
|
348
|
-
},
|
|
349
|
-
savedState,
|
|
350
|
-
);
|
|
351
|
-
});
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|