@rookiestar/eng-lang-tutor 1.2.5 → 1.2.7

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.
Files changed (54) hide show
  1. package/.gitignore +32 -0
  2. package/CHANGELOG.md +37 -0
  3. package/CLAUDE.md +275 -0
  4. package/README.md +53 -0
  5. package/bin/eng-lang-tutor.js +177 -0
  6. package/docs/OPENCLAW_DEPLOYMENT.md +241 -0
  7. package/examples/sample_quiz_a1.json +4 -4
  8. package/examples/sample_quiz_a2.json +4 -4
  9. package/examples/sample_quiz_b1.json +2 -0
  10. package/examples/sample_quiz_b2.json +3 -3
  11. package/examples/sample_quiz_c1.json +2 -2
  12. package/examples/sample_quiz_c2.json +3 -3
  13. package/package.json +8 -10
  14. package/templates/prompts/quiz_generation.md +192 -11
  15. package/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  16. package/scripts/__pycache__/audio_composer.cpython-313.pyc +0 -0
  17. package/scripts/__pycache__/audio_converter.cpython-313.pyc +0 -0
  18. package/scripts/__pycache__/audio_enhancer.cpython-313.pyc +0 -0
  19. package/scripts/__pycache__/audio_utils.cpython-313.pyc +0 -0
  20. package/scripts/__pycache__/command_parser.cpython-313.pyc +0 -0
  21. package/scripts/__pycache__/constants.cpython-313.pyc +0 -0
  22. package/scripts/__pycache__/cron_push.cpython-313.pyc +0 -0
  23. package/scripts/__pycache__/dedup.cpython-313.pyc +0 -0
  24. package/scripts/__pycache__/error_notebook.cpython-313.pyc +0 -0
  25. package/scripts/__pycache__/feishu_voice.cpython-313.pyc +0 -0
  26. package/scripts/__pycache__/gamification.cpython-313.pyc +0 -0
  27. package/scripts/__pycache__/scorer.cpython-313.pyc +0 -0
  28. package/scripts/__pycache__/state_manager.cpython-313.pyc +0 -0
  29. package/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  30. package/scripts/audio/__pycache__/__init__.cpython-313.pyc +0 -0
  31. package/scripts/audio/__pycache__/composer.cpython-313.pyc +0 -0
  32. package/scripts/audio/__pycache__/converter.cpython-313.pyc +0 -0
  33. package/scripts/audio/__pycache__/feishu_voice.cpython-313.pyc +0 -0
  34. package/scripts/audio/__pycache__/utils.cpython-313.pyc +0 -0
  35. package/scripts/audio/tts/__pycache__/__init__.cpython-313.pyc +0 -0
  36. package/scripts/audio/tts/__pycache__/base.cpython-313.pyc +0 -0
  37. package/scripts/audio/tts/__pycache__/manager.cpython-313.pyc +0 -0
  38. package/scripts/audio/tts/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  39. package/scripts/audio/tts/providers/__pycache__/edge.cpython-313.pyc +0 -0
  40. package/scripts/audio/tts/providers/__pycache__/xunfei.cpython-313.pyc +0 -0
  41. package/scripts/cli/__pycache__/__init__.cpython-313.pyc +0 -0
  42. package/scripts/cli/__pycache__/cli.cpython-313.pyc +0 -0
  43. package/scripts/cli/__pycache__/command_parser.cpython-313.pyc +0 -0
  44. package/scripts/core/__pycache__/__init__.cpython-313.pyc +0 -0
  45. package/scripts/core/__pycache__/constants.cpython-313.pyc +0 -0
  46. package/scripts/core/__pycache__/error_notebook.cpython-313.pyc +0 -0
  47. package/scripts/core/__pycache__/gamification.cpython-313.pyc +0 -0
  48. package/scripts/core/__pycache__/scorer.cpython-313.pyc +0 -0
  49. package/scripts/core/__pycache__/state_manager.cpython-313.pyc +0 -0
  50. package/scripts/scheduling/__pycache__/__init__.cpython-313.pyc +0 -0
  51. package/scripts/scheduling/__pycache__/cron_push.cpython-313.pyc +0 -0
  52. package/scripts/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/scripts/utils/__pycache__/dedup.cpython-313.pyc +0 -0
  54. package/scripts/utils/__pycache__/helpers.cpython-313.pyc +0 -0
package/.gitignore ADDED
@@ -0,0 +1,32 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ .venv/
8
+ venv/
9
+ ENV/
10
+
11
+ # IDE
12
+ .idea/
13
+ .vscode/
14
+ *.swp
15
+ *.swo
16
+
17
+ # OS
18
+ .DS_Store
19
+ Thumbs.db
20
+
21
+ # Backups
22
+ backups/
23
+ *.bak
24
+
25
+ # User data
26
+ data/state.json
27
+ data/logs/
28
+ data/daily/
29
+
30
+ # npm
31
+ node_modules/
32
+ *.tgz
package/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.0.0] - 2025-02-26
6
+
7
+ Initial release.
8
+
9
+ ### Added
10
+
11
+ **Core Features:**
12
+ - Daily knowledge points with authentic American English expressions
13
+ - Quiz system with 4 question types (multiple choice, Chinglish fix, fill blank, dialogue completion)
14
+ - Duolingo-style gamification (XP, levels 1-20, streaks, badges, gems)
15
+ - Error notebook for tracking and reviewing mistakes
16
+ - 14-day content deduplication
17
+
18
+ **CEFR Support:**
19
+ - Complete CEFR level definitions (A1-C2) with Can-Do Statements
20
+ - 12 sample JSON files covering all 6 CEFR levels
21
+ - Content difficulty adjusted by user's CEFR level
22
+
23
+ **Audio:**
24
+ - TTS audio generation (Edge-TTS default, XunFei optional)
25
+ - Async audio generation with background threading
26
+ - Context manager pattern for reliable temp file cleanup
27
+
28
+ **Configuration:**
29
+ - 7-step onboarding flow
30
+ - Initialization guard check before content generation
31
+ - Centralized configuration constants
32
+ - Prompt version control via `_meta.prompt_version` field
33
+
34
+ **Documentation:**
35
+ - SKILL.md, README.md, CLAUDE.md
36
+ - OpenClaw deployment guide
37
+ - Prompt templates (keypoint generation, quiz generation, etc.)
package/CLAUDE.md ADDED
@@ -0,0 +1,275 @@
1
+ # SKILL: eng-lang-tutor
2
+
3
+ ## 1. 项目目标
4
+
5
+ 本项目主要为Agent添加「地道美式英语导师」的Skill,配合Agent所在环境的cron job实现定期的知识推送与测验。
6
+
7
+ ### 1.1. 学习目标
8
+
9
+ - 以提升地道的美式英语常用口语表达为主,兼顾听力与书面表达(正式场合)。
10
+ - 避免传统的Chinglish的硬翻式尴尬表达。
11
+ - 融入轻量游戏化教学,覆盖影视、新闻、游戏、体育、职场、社交、生活等用户感兴趣的场景来提升用户的粘性。
12
+
13
+ ### 1.2. 语言范围
14
+
15
+ - 以美式英语为主,包含俚语、口头语、缩写(gonna, gotta, kinda 等),能帮助用户区分「正式 vs 口语」。
16
+
17
+ ### 1.3. 技能维度
18
+
19
+ - 涉及词汇/短语、固定搭配、场景句型等口语与书面语中的表达方式。
20
+
21
+ ## 2. 项目范围
22
+
23
+ ### 2.1. MVP版本功能清单
24
+
25
+ #### 2.1.1 导师模式
26
+
27
+ Agent结合难度与偏好的设置,以及用户的历史学习进度动态生成并调整个性化的教学大纲与教学内容:
28
+
29
+ | 功能 | 描述 | 频次 | 对应脚本 |
30
+ |-----|------|-----|---------|
31
+ | **知识点** | 地道美式英语日常沟通素材,包含场景、可替换说法、Chinglish陷阱+修正 | 每天 | state_manager.py |
32
+ | **Quiz** | 3道轻量测验:1多选+1Chinglish修正+1随机(填空/对话)| 每天 | scorer.py |
33
+ | **错题本** | 记录错误答案,支持手动添加,便于针对性复习 | 实时 | state_manager.py |
34
+
35
+ **Quiz 结构:**
36
+
37
+ | 题型 | 描述 | XP值 | 每日Quiz |
38
+ |-----|------|-----|---------|
39
+ | multiple_choice | 四选一,测试表达识别 | 10 | 1 (必选) |
40
+ | chinglish_fix | 识别并修正Chinglish表达 | 15 | 1 (必选) |
41
+ | fill_blank | 填空,完成对话 | 12 | 0-1 (随机) |
42
+ | dialogue_completion | 选择合适的对话回应 | 15 | 0-1 (随机) |
43
+
44
+ - **每日Quiz**: 3题,约37 XP,答对2题即过关
45
+
46
+ #### 2.1.2 状态管理
47
+
48
+ 保障整个教学过程可备份、可追溯、可回滚:
49
+
50
+ | 组件 | 文件 | 用途 |
51
+ |-----|------|-----|
52
+ | 核心状态 | state.json | streak/xp/偏好/近期主题/错题本 |
53
+ | 事件日志 | logs/events_YYYY-MM.jsonl | 追加式事件流水账 |
54
+ | 每日内容 | daily/YYYY-MM-DD/*.json | 知识点/Quiz/用户答案 |
55
+
56
+ #### 2.1.3 成就体系(Duolingo风格)
57
+
58
+ **双等级体系说明:**
59
+ - **能力等级 (CEFR)**:A1-C2,决定内容难度,与 App 无关的绝对语言能力
60
+ - **活跃等级 (Level)**:1-20,衡量 App 使用深度,通过 XP 累积升级
61
+
62
+ | 活跃等级 | 阶段 | XP 需求 |
63
+ |---------|------|---------|
64
+ | 1-5 | 启程者 (Starter) | 0-350 |
65
+ | 6-10 | 行路人 (Traveler) | 550-2000 |
66
+ | 11-15 | 探索者 (Explorer) | 2600-6000 |
67
+ | 16-20 | 开拓者 (Pioneer) | 7200-15000 |
68
+
69
+ | 组件 | 描述 |
70
+ |-----|------|
71
+ | **XP & 等级** | 1-20级,XP累积升级(启程者 → 行路人 → 探索者 → 开拓者) |
72
+ | **连胜系统** | 连续学习天数,支持连胜冻结(50宝石) |
73
+ | **徽章系统** | First Steps、Week Warrior、Month Master、Perfect 10、Vocab Hunter、Error Slayer |
74
+ | **宝石系统** | 用于购买连胜冻结、提示等 |
75
+
76
+ **徽章详情:**
77
+
78
+ | 徽章 | 触发条件 | 宝石奖励 |
79
+ |-----|---------|---------|
80
+ | First Steps | 完成首次Quiz | 10 |
81
+ | Week Warrior | 7天连胜 | 25 |
82
+ | Month Master | 30天连胜 | 100 |
83
+ | Perfect 10 | 10次完美Quiz | 50 |
84
+ | Vocab Hunter | 学习100个表达 | 75 |
85
+ | Error Slayer | 清除30个错题 | 30 |
86
+
87
+ #### 2.1.4 系统配置
88
+
89
+ | 配置项 | 描述 | 默认值 |
90
+ |-------|------|-------|
91
+ | CEFR等级 | A1-C2语言级别 | B1 |
92
+ | 口语/书面比例 | 口语表达占比 | 70% |
93
+ | 主题配比 | movies/news/gaming/sports/workplace/social/daily_life | 各主题权重 |
94
+ | 导师风格 | humorous/rigorous/casual/professional | humorous |
95
+ | 去重天数 | 避免重复内容的时间窗口 | 14天 |
96
+
97
+ **配置常量:** 集中定义于 `scripts/core/constants.py`(宝石消耗、XP值、超时阈值等)
98
+
99
+ ### 2.2. MVP版本不做 / 延后
100
+
101
+ - **周周练**:5道综合性场景练习,覆盖本周知识点,约62 XP,答对3题即过关。计划周日推送。
102
+ - **语音模式**:支持tts便于用户跟读,支持asr便于导师对口语表达进行打分。
103
+
104
+ ### 2.3. 项目约束
105
+
106
+ **核心原则:别问"怎么翻译",先问"这个场景美国人会怎么说",会让你立刻更像美国人。**
107
+
108
+ #### 2.3.1 输出格式约束
109
+
110
+ - **必须输出合法 JSON**(不要 markdown,不要代码块,不要多余文本)
111
+ - 所有输出必须符合 `templates/` 目录下的 JSON Schema
112
+
113
+ #### 2.3.2 内容质量约束
114
+
115
+ - 语言风格:**地道美式**、轻松、可用、短句为主
116
+ - 每条知识点必须包含:**场景、可替换说法、Chinglish 陷阱 + 修正**
117
+ - 包含发音提示(gonna, gotta, wanna 等自然发音)
118
+ - 提供文化背景和使用场景说明
119
+
120
+ #### 2.3.3 去重策略(14天窗口)
121
+
122
+ 使用 `scripts/utils/dedup.py` 实现三层去重:
123
+
124
+ | 层级 | 方法 | 描述 |
125
+ |-----|------|-----|
126
+ | 1 | topic_fingerprint | 主题指纹匹配 |
127
+ | 2 | 表达重叠 | 相同短语超过50%视为重复 |
128
+ | 3 | 词根匹配 | 核心词汇结构相似 |
129
+
130
+ #### 2.3.4 反直译原则
131
+
132
+ - 优先给"美国人会怎么说",而不是"中文怎么翻英文"
133
+ - 参考 `references/resources.md` 中的主题资源库
134
+ - 使用 `references/prompt_templates.md` 中的LLM提示模板
135
+
136
+ ## 3. 技术栈与SKILL结构
137
+
138
+ ### 3.1. 技术栈与输出格式
139
+
140
+ - 技术栈:Python 3.x
141
+ - 触发方式:cron 触发 Agent 调用 Skill
142
+
143
+ ### 3.2. 目录结构
144
+
145
+ ```
146
+ eng-lang-tutor/
147
+ ├── SKILL.md # 核心技能文档
148
+ ├── CLAUDE.md # 项目说明(本文件)
149
+ ├── scripts/
150
+ │ ├── __init__.py # 包入口,重导出核心类
151
+ │ ├── core/
152
+ │ │ ├── state_manager.py # 状态持久化与事件日志
153
+ │ │ ├── error_notebook.py # 错题本管理
154
+ │ │ ├── scorer.py # 答案评估与XP计算
155
+ │ │ ├── gamification.py # 连胜/等级/徽章
156
+ │ │ └── constants.py # 共享常量和工具函数
157
+ │ ├── cli/
158
+ │ │ ├── cli.py # CLI入口点
159
+ │ │ └── command_parser.py # 命令解析
160
+ │ ├── audio/
161
+ │ │ ├── tts/ # TTS引擎(Coqui/Azure/Edge)
162
+ │ │ ├── composer.py # 音频合成
163
+ │ │ ├── converter.py # 格式转换
164
+ │ │ ├── feishu_voice.py # 飞书语音发送
165
+ │ │ └── utils.py # 音频工具函数
166
+ │ ├── scheduling/
167
+ │ │ └── cron_push.py # 定时推送
168
+ │ └── utils/
169
+ │ ├── dedup.py # 14天去重逻辑
170
+ │ └── helpers.py # 通用工具函数
171
+ ├── templates/
172
+ │ ├── state_schema.json # 状态 JSON Schema
173
+ │ ├── keypoint_schema.json # 知识点 JSON Schema
174
+ │ └── quiz_schema.json # Quiz JSON Schema
175
+ ├── references/
176
+ │ ├── resources.md # 主题化英语学习资源库
177
+ │ └── prompt_templates.md # LLM Prompt 模板
178
+ ├── examples/ # 示例文件(按 CEFR 级别命名)
179
+ │ ├── sample_keypoint_*.json # 知识点示例 (a1-c2)
180
+ │ └── sample_quiz_*.json # Quiz示例 (a1-c2)
181
+ └── (数据存储在外部目录)
182
+ ~/.openclaw/state/eng-lang-tutor/ # 实际数据存储位置
183
+ ├── state.json # 运行时状态
184
+ ├── logs/
185
+ │ └── events_YYYY-MM.jsonl
186
+ └── daily/
187
+ └── YYYY-MM-DD/
188
+ ├── keypoint.json
189
+ ├── quiz.json
190
+ └── user_answers.json
191
+
192
+ 注: 可通过 OPENCLAW_STATE_DIR 环境变量自定义存储位置
193
+ ```
194
+
195
+ ## 4. 功能切片实现
196
+
197
+ ### 4.1 功能切片与脚本对应
198
+
199
+ | 切片 | 对应脚本 | 输入 | 输出 |
200
+ |-----|---------|------|-----|
201
+ | 状态加载/保存 | core/state_manager.py | - | state.json |
202
+ | 事件日志 | core/state_manager.py | event_type, data | events_YYYY-MM.jsonl |
203
+ | 每日内容保存 | core/state_manager.py | content_type, content | daily/YYYY-MM-DD/*.json |
204
+ | 答案评估 | core/scorer.py | quiz.json + user_answers | results + updated state |
205
+ | XP计算 | core/scorer.py | correct_count, streak | XP with multiplier |
206
+ | 连胜更新 | core/gamification.py | study_date | new_streak |
207
+ | 等级更新 | core/gamification.py | XP | level |
208
+ | 徽章检查 | core/gamification.py | progress | new_badges |
209
+ | 去重检查 | utils/dedup.py | new_content, recent_content | is_duplicate |
210
+ | 异步音频生成 | core/state_manager.py | keypoint.json | audio/*.mp3 |
211
+
212
+ ### 4.2 核心工作流
213
+
214
+ #### 4.2.1 每日知识点生成
215
+
216
+ ```
217
+ 1. state_manager.load_state()
218
+ 2. dedup.get_excluded_topics(state)
219
+ 3. LLM生成知识点(使用prompt_templates.md)
220
+ 4. dedup.check_duplicate(new_content, recent_content)
221
+ 5. 验证JSON Schema
222
+ 6. state_manager.save_daily_content('keypoint', content)
223
+ 7. state_manager.append_event('keypoint_generated', {...})
224
+ ```
225
+
226
+ #### 4.2.2 Quiz评估流程
227
+
228
+ ```
229
+ 1. state_manager.load_daily_content('quiz')
230
+ 2. 读取 user_answers.json
231
+ 3. scorer.evaluate_quiz(quiz, answers, state)
232
+ 4. gamification.update_streak(state, today)
233
+ 5. gamification.update_level(state)
234
+ 6. gamification.check_badges(state)
235
+ 7. state_manager.save_state(state)
236
+ 8. state_manager.append_event('quiz_completed', results)
237
+ ```
238
+
239
+ ## 5. 开发准则
240
+
241
+ ### 5.1 Skill文档规范
242
+
243
+ - SKILL.md文档要详细但不能过长,聚焦核心步骤,细节可以放在代码示例里。建议**800-1500 tokens**。
244
+ - Skills 必须包含可执行的步骤、代码示例、验证检查点。
245
+ - 设计 Skills 时考虑复用性(模块化)。一个 Skill 解决一类问题,不是一次性的硬编码。
246
+
247
+ ### 5.2 确定性验证器
248
+
249
+ 每个任务都有**确定性验证器**(deterministic verifier):这不是用 LLM 评判,而是程序化断言。
250
+
251
+ | 验证项 | 验证方法 | 对应工具 |
252
+ |-------|---------|---------|
253
+ | JSON格式 | json.loads() | Python标准库 |
254
+ | JSON Schema | schema validation | templates/*.json |
255
+ | XP计算 | 公式验证 | scorer.py |
256
+ | 连胜更新 | 日期比较 | gamification.py |
257
+ | 去重检查 | 指纹/表达匹配 | dedup.py |
258
+
259
+ ### 5.3 聚焦核心路径
260
+
261
+ 不要过度工程化:比如应该聚焦 80% 场景的核心路径,而非覆盖尽可能多的edge cases。
262
+
263
+ ### 5.4 可复用资源
264
+
265
+ | 资源 | 位置 | 用途 |
266
+ |-----|------|-----|
267
+ | skill-creator | github.com/anthropics/skills | 自动生成skill骨架 |
268
+ | awesome-language-learning | github.com/Vuizur/awesome-language-learning | 语言学习工具集合 |
269
+ | 主题资源库 | references/resources.md | 美剧/新闻/游戏/体育/职场/生活 |
270
+ | LLM提示模板 | references/prompt_templates.md | 知识点/Quiz生成模板 |
271
+
272
+ ### 5.5 行动准则
273
+
274
+ - 未经允许不得擅自执行git commit、push和publish等相关动作
275
+ - 针对耗费较长时间做debug或者反复掉坑的情况,需要及时总结复盘,并将经验精炼到本文档中
package/README.md CHANGED
@@ -195,6 +195,59 @@ export XUNFEI_API_SECRET=xxx
195
195
  | 快速 | 1.3 | 听力挑战 |
196
196
  | 非常快 | 1.7 | 进阶训练 |
197
197
 
198
+ ## 🛠️ 飞书语音气泡魔改指南
199
+
200
+ > 通过修改 OpenClaw 飞书插件,可使 `.opus` 音频文件以语音气泡形式展示,提升用户体验。
201
+
202
+ **文件位置:** `/home/linuxbrew/.linuxbrew/lib/node_modules/openclaw/extensions/feishu/src/media.ts`
203
+
204
+ ### 📍 第一处修改:扩充类型定义 (约第 276 行)
205
+
206
+ ```typescript
207
+ // 【修改前】
208
+ msgType?: "file" | "media";
209
+
210
+ // 【修改后】
211
+ msgType?: "file" | "media" | "audio";
212
+ ```
213
+
214
+ ### 📍 第二处修改:重构路由逻辑 (约第 375 行)
215
+
216
+ ```typescript
217
+ // 【修改前】
218
+ // Feishu requires msg_type "media" for audio/video, "file" for documents
219
+ const isMedia = fileType === "mp4" || fileType === "opus";
220
+ return sendFileFeishu({
221
+ cfg,
222
+ to,
223
+ fileKey,
224
+ msgType: isMedia ? "media" : "file",
225
+ replyToMessageId,
226
+ accountId,
227
+ });
228
+
229
+ // 【修改后】
230
+ // 精细化路由:mp4 走 media (视频), opus 走 audio (语音气泡), 其余走 file
231
+ let msgType: "file" | "media" | "audio" = "file";
232
+ if (fileType === "mp4") {
233
+ msgType = "media";
234
+ } else if (fileType === "opus") {
235
+ msgType = "audio";
236
+ }
237
+ return sendFileFeishu({
238
+ cfg,
239
+ to,
240
+ fileKey,
241
+ msgType,
242
+ replyToMessageId,
243
+ accountId,
244
+ });
245
+ ```
246
+
247
+ **效果:**
248
+ - `.opus` 文件 → 语音气泡形式播放
249
+ - 其他文件 → 普通文件附件形式
250
+
198
251
  ## 命令列表
199
252
 
200
253
  | 命令 | 别名 | 描述 |
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * eng-lang-tutor CLI
5
+ *
6
+ * Commands:
7
+ * install - Install the skill to ~/.openclaw/skills/eng-lang-tutor/
8
+ * uninstall - Remove the skill from ~/.openclaw/skills/eng-lang-tutor/
9
+ */
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+
15
+ const SKILL_NAME = 'eng-lang-tutor';
16
+ const SKILLS_DIR = path.join(os.homedir(), '.openclaw', 'skills');
17
+ const SKILL_TARGET = path.join(SKILLS_DIR, SKILL_NAME);
18
+
19
+ // Get the package root directory (where package.json is)
20
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
21
+
22
+ function getSkillSourceDir() {
23
+ return PACKAGE_ROOT;
24
+ }
25
+
26
+ function install() {
27
+ console.log(`Installing ${SKILL_NAME} skill...`);
28
+
29
+ const sourceDir = getSkillSourceDir();
30
+
31
+ // Check if source directory exists
32
+ if (!fs.existsSync(sourceDir)) {
33
+ console.error(`Error: Source directory not found: ${sourceDir}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ // Create skills directory if it doesn't exist
38
+ if (!fs.existsSync(SKILLS_DIR)) {
39
+ fs.mkdirSync(SKILLS_DIR, { recursive: true });
40
+ console.log(`Created skills directory: ${SKILLS_DIR}`);
41
+ }
42
+
43
+ // Remove existing installation if present
44
+ if (fs.existsSync(SKILL_TARGET)) {
45
+ console.log(`Removing existing installation at ${SKILL_TARGET}...`);
46
+ fs.rmSync(SKILL_TARGET, { recursive: true, force: true });
47
+ }
48
+
49
+ // Create target directory
50
+ fs.mkdirSync(SKILL_TARGET, { recursive: true });
51
+
52
+ // Files and directories to copy
53
+ const itemsToCopy = [
54
+ 'scripts',
55
+ 'templates',
56
+ 'references',
57
+ 'examples',
58
+ 'docs',
59
+ 'SKILL.md',
60
+ 'CLAUDE.md',
61
+ 'README.md',
62
+ 'README_EN.md',
63
+ 'requirements.txt'
64
+ ];
65
+
66
+ // Copy each item
67
+ for (const item of itemsToCopy) {
68
+ const sourcePath = path.join(sourceDir, item);
69
+ const targetPath = path.join(SKILL_TARGET, item);
70
+
71
+ if (!fs.existsSync(sourcePath)) {
72
+ console.log(` Skipping ${item} (not found)`);
73
+ continue;
74
+ }
75
+
76
+ try {
77
+ if (fs.statSync(sourcePath).isDirectory()) {
78
+ copyDir(sourcePath, targetPath);
79
+ } else {
80
+ fs.copyFileSync(sourcePath, targetPath);
81
+ }
82
+ console.log(` Copied ${item}`);
83
+ } catch (err) {
84
+ console.error(` Error copying ${item}: ${err.message}`);
85
+ }
86
+ }
87
+
88
+ console.log(`\n✓ ${SKILL_NAME} installed to ${SKILL_TARGET}`);
89
+ console.log('\nNext steps:');
90
+ console.log(' 1. Install Python dependencies:');
91
+ console.log(` pip install -r ${SKILL_TARGET}/requirements.txt`);
92
+ console.log(' 2. Restart your OpenClaw agent');
93
+ console.log(' 3. Configure the skill through the onboarding flow');
94
+ }
95
+
96
+ function uninstall() {
97
+ console.log(`Uninstalling ${SKILL_NAME} skill...`);
98
+
99
+ if (!fs.existsSync(SKILL_TARGET)) {
100
+ console.log(`${SKILL_NAME} is not installed.`);
101
+ return;
102
+ }
103
+
104
+ try {
105
+ fs.rmSync(SKILL_TARGET, { recursive: true, force: true });
106
+ console.log(`✓ ${SKILL_NAME} removed from ${SKILL_TARGET}`);
107
+ console.log('\nNote: Your learning data is preserved at:');
108
+ console.log(' ~/.openclaw/state/eng-lang-tutor/');
109
+ console.log('\nTo completely remove all data, run:');
110
+ console.log(' rm -rf ~/.openclaw/state/eng-lang-tutor');
111
+ } catch (err) {
112
+ console.error(`Error uninstalling: ${err.message}`);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ function copyDir(src, dest) {
118
+ fs.mkdirSync(dest, { recursive: true });
119
+
120
+ const entries = fs.readdirSync(src, { withFileTypes: true });
121
+
122
+ for (const entry of entries) {
123
+ const srcPath = path.join(src, entry.name);
124
+ const destPath = path.join(dest, entry.name);
125
+
126
+ if (entry.isDirectory()) {
127
+ copyDir(srcPath, destPath);
128
+ } else {
129
+ fs.copyFileSync(srcPath, destPath);
130
+ }
131
+ }
132
+ }
133
+
134
+ function showHelp() {
135
+ console.log(`
136
+ eng-lang-tutor - English Language Tutor Skill for OpenClaw
137
+
138
+ Usage:
139
+ eng-lang-tutor <command>
140
+
141
+ Commands:
142
+ install Install the skill to ~/.openclaw/skills/eng-lang-tutor/
143
+ uninstall Remove the skill (preserves learning data)
144
+ help Show this help message
145
+
146
+ Environment Variables:
147
+ OPENCLAW_STATE_DIR Custom directory for learning data
148
+ (default: ~/.openclaw/state/eng-lang-tutor)
149
+
150
+ Examples:
151
+ eng-lang-tutor install
152
+ eng-lang-tutor uninstall
153
+ `);
154
+ }
155
+
156
+ // Main
157
+ const command = process.argv[2];
158
+
159
+ switch (command) {
160
+ case 'install':
161
+ install();
162
+ break;
163
+ case 'uninstall':
164
+ uninstall();
165
+ break;
166
+ case 'help':
167
+ case '--help':
168
+ case '-h':
169
+ showHelp();
170
+ break;
171
+ default:
172
+ if (command) {
173
+ console.error(`Unknown command: ${command}`);
174
+ }
175
+ showHelp();
176
+ process.exit(command ? 1 : 0);
177
+ }