engly 1.0.0
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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/cli.js +582 -0
- package/dist/cli.js.map +1 -0
- package/package.json +78 -0
- package/templates/engly.md +29 -0
- package/templates/skills/engly-off/SKILL.md +16 -0
- package/templates/skills/engly-on/SKILL.md +14 -0
- package/templates/skills/engly-review/SKILL.md +23 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-09-26
|
|
9
|
+
|
|
10
|
+
First version: project foundation and `engly init` for Claude Code.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `engly` CLI (Node.js 22.12+, ESM) with the commands `init`, `update`, `enable`, `disable`, `config`, `status` and `remove`. Only `init` is implemented; the rest print "not implemented yet".
|
|
15
|
+
- `engly init` for Claude Code:
|
|
16
|
+
- Asks which agents you use and the feedback style (`light` or `detailed`).
|
|
17
|
+
- Writes a marked block with `@.engly/engly.md` in `CLAUDE.md`, creating the file if needed.
|
|
18
|
+
- Installs the `engly-on`, `engly-off` and `engly-review` skills in `.claude/skills/`.
|
|
19
|
+
- Saves `.engly/config.json` and renders the coach instructions to `.engly/engly.md`.
|
|
20
|
+
- Warns when `CLAUDE.md` is tracked by git, or will be once committed.
|
|
21
|
+
- Warns when `CLAUDE.md` is over 200 lines and offers to put the block near the top.
|
|
22
|
+
- Flags: `--tools`, `--style`, `--yes` and `--no-apply`.
|
|
23
|
+
- Marked block handling between `<!-- engly:start vX.Y.Z -->` and `<!-- engly:end -->`: insert, update and remove without touching content outside the markers, keeping the file's line endings (LF or CRLF).
|
|
24
|
+
- Adapters for Claude Code, Codex, OpenCode, Cursor and GitHub Copilot, with detection, instruction file, delivery mode, skill folders and size limits.
|
|
25
|
+
- Skills are copied to as few folders as possible: `.claude/skills/` with Claude Code, plus `.agents/skills/` when Codex is selected or Claude Code is not.
|
|
26
|
+
- Coach instructions template and the three skills with valid frontmatter.
|
|
27
|
+
- CI workflow for pull requests to `main`, Biome for lint and format, and vitest with 100% coverage required on the block and size modules.
|
|
28
|
+
- Version check on pull requests to `main`: `version` in `package.json` must be greater than on `main` (semver, prereleases included), and `CHANGELOG.md` must have an entry for it. All jobs report to a single `Required checks` status for branch protection.
|
|
29
|
+
- Release workflow on pushes to `main`: publishes to npm with provenance (trusted publishing, no token) and creates the `vX.Y.Z` tag and GitHub release from the CHANGELOG entry.
|
|
30
|
+
|
|
31
|
+
[1.0.0]: https://github.com/victorcastro/engly/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Victor Castro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# π£οΈ Engly
|
|
4
|
+
|
|
5
|
+
**Your English coach for AI prompts.**
|
|
6
|
+
|
|
7
|
+
One command. Every agent. Always on. Zero cost.
|
|
8
|
+
|
|
9
|
+
[](https://github.com/victorcastro/engly/actions/workflows/release.yml)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
You write prompts to an AI all day. Why not improve your English while you do it?
|
|
18
|
+
|
|
19
|
+
Engly teaches the coding agent you already use (Claude Code, Codex, Cursorβ¦) to coach your English. Every prompt becomes a tiny lesson: the agent shows you how to say it in clear English, points out a couple of mistakes, and then does the task as if you had written the better prompt.
|
|
20
|
+
|
|
21
|
+
No extra app, no extra model, no extra cost. Just better English and clearer prompts.
|
|
22
|
+
|
|
23
|
+
## β¨ See it in action
|
|
24
|
+
|
|
25
|
+
**You write in your own language:**
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
You: arregla el bug del login que no deja entrar cuando el password tiene espacios
|
|
29
|
+
|
|
30
|
+
Agent:
|
|
31
|
+
β π£οΈ Engly
|
|
32
|
+
β In English: "Fix the login bug that blocks users when the
|
|
33
|
+
β password contains spaces."
|
|
34
|
+
β Tip: "contains" is more precise than "has" for strings.
|
|
35
|
+
β
|
|
36
|
+
I'll look at the login handler...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**You write in English, with a few mistakes:**
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
You: make that the function return the users who has more than 3 orders
|
|
43
|
+
|
|
44
|
+
Agent:
|
|
45
|
+
β π£οΈ Engly
|
|
46
|
+
β β’ "make that the function return" β "make the function return"
|
|
47
|
+
β β’ "users who has" β "users who have" (plural subject)
|
|
48
|
+
β Better: "Make the function return users with more than 3 orders."
|
|
49
|
+
β
|
|
50
|
+
Sure, here's the updated function...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Your English is already right?** Engly stays quiet and the agent just gets to work.
|
|
54
|
+
|
|
55
|
+
## π‘ Why Engly
|
|
56
|
+
|
|
57
|
+
- **Always on.** No command to remember. Coaching starts with your next prompt.
|
|
58
|
+
- **Never in your way.** A few lines of feedback, then the task goes on. It never asks for confirmation.
|
|
59
|
+
- **Zero cost.** No API keys and no calls to other models. The agent you already use does the coaching.
|
|
60
|
+
- **Safe for your files.** Engly only writes inside its own marked block and never touches anything else.
|
|
61
|
+
- **Made for learners.** Built with Spanish speakers in mind, and it works for any native language from A2 to C1.
|
|
62
|
+
|
|
63
|
+
## π Quick start
|
|
64
|
+
|
|
65
|
+
> [!NOTE]
|
|
66
|
+
> Engly is in early development. Today `engly init` works for **Claude Code**, and the package is not on npm yet. More agents and commands are coming soon.
|
|
67
|
+
|
|
68
|
+
Install it from source:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
git clone https://github.com/victorcastro/engly.git
|
|
72
|
+
cd engly
|
|
73
|
+
npm install
|
|
74
|
+
npm run build
|
|
75
|
+
npm link
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then, in your project:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
cd your-project
|
|
82
|
+
engly init
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Open your agent as usual. Your next prompt will be coached. π
|
|
86
|
+
|
|
87
|
+
Once Engly is on npm, installing will be a single command: `npm install -g engly`.
|
|
88
|
+
|
|
89
|
+
## π€ Supported agents
|
|
90
|
+
|
|
91
|
+
| Agent | Where Engly adds its instructions | Status |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| Claude Code | One import line in `CLAUDE.md` | β
Available |
|
|
94
|
+
| Codex | Marked block in `AGENTS.md` | π οΈ Coming soon |
|
|
95
|
+
| OpenCode | Marked block in `AGENTS.md` | π οΈ Coming soon |
|
|
96
|
+
| Cursor | Its own rule file, `.cursor/rules/engly.mdc` | π οΈ Coming soon |
|
|
97
|
+
| GitHub Copilot | Marked block in `.github/copilot-instructions.md` | π οΈ Coming soon |
|
|
98
|
+
|
|
99
|
+
## π¬ In-chat commands
|
|
100
|
+
|
|
101
|
+
While you chat with your agent:
|
|
102
|
+
|
|
103
|
+
| Skill | What it does |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `/engly-off` | Pause the coach for this session ("I'm in a hurry"). |
|
|
106
|
+
| `/engly-on` | Resume the coach. |
|
|
107
|
+
| `/engly-review` | Deep review of a prompt: every mistake, why, and better alternatives. |
|
|
108
|
+
|
|
109
|
+
In Codex, use `$engly-off` instead of `/engly-off`. In any agent you can also just type **"engly off"** or **"engly on"**.
|
|
110
|
+
|
|
111
|
+
## β¨οΈ CLI commands
|
|
112
|
+
|
|
113
|
+
| Command | What it does |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `engly init` | Sets up Engly in the current project. Asks two questions: which agents you use and how detailed the feedback should be. |
|
|
116
|
+
| `engly update` | Updates the skills and instructions to the latest version. *(coming soon)* |
|
|
117
|
+
| `engly enable` / `engly disable` | Turns the coach on or off permanently. *(coming soon)* |
|
|
118
|
+
| `engly config <option> <value>` | Changes a setting, e.g. `engly config level B2`. *(coming soon)* |
|
|
119
|
+
| `engly status` | Shows where Engly is installed and whether it is active. *(coming soon)* |
|
|
120
|
+
| `engly remove` | Removes everything Engly added. *(coming soon)* |
|
|
121
|
+
|
|
122
|
+
`engly init` options:
|
|
123
|
+
|
|
124
|
+
| Option | What it does |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `--tools claude,codex,β¦` | Choose agents without being asked. |
|
|
127
|
+
| `--style light\|detailed` | Choose the feedback style without being asked. |
|
|
128
|
+
| `--yes` | No questions at all: detected agents and default settings. |
|
|
129
|
+
| `--no-apply` | Only install the skills, don't touch the agent files. |
|
|
130
|
+
|
|
131
|
+
## βοΈ Settings
|
|
132
|
+
|
|
133
|
+
Engly works out of the box. Your settings live in `.engly/config.json`:
|
|
134
|
+
|
|
135
|
+
| Setting | Values | Default |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `style` | `light` (up to 3 corrections) or `detailed` (every mistake explained) | `light` |
|
|
138
|
+
| `level` | `auto`, `A2`, `B1`, `B2`, `C1` | `auto`: the agent infers it from how you write |
|
|
139
|
+
| `language` | `auto` or your native language (`es`, `pt`, `fr`β¦) | `auto`: the agent infers it from your prompts |
|
|
140
|
+
| `reviewEnglish` | Also coach prompts written in English | `true` |
|
|
141
|
+
| `enabled` | Coach on or off | `true` |
|
|
142
|
+
|
|
143
|
+
## π Your files are safe
|
|
144
|
+
|
|
145
|
+
`engly init` only adds this to your project:
|
|
146
|
+
|
|
147
|
+
- `.engly/` with the coach instructions and your settings.
|
|
148
|
+
- The in-chat skills (`engly-on`, `engly-off`, `engly-review`).
|
|
149
|
+
- One small marked block in your agent file:
|
|
150
|
+
|
|
151
|
+
```md
|
|
152
|
+
<!-- engly:start v1.0.0 -->
|
|
153
|
+
@.engly/engly.md
|
|
154
|
+
<!-- engly:end -->
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Everything outside the markers stays exactly as it was, line endings included. Engly also looks out for you:
|
|
158
|
+
|
|
159
|
+
- **Shared repo?** If your agent file is tracked by git, Engly warns you first. Learning English is personal, and your teammates would be coached too if you commit it.
|
|
160
|
+
- **Long agent file?** Agents pay less attention to the end of long files, so Engly offers to put its block near the top.
|
|
161
|
+
|
|
162
|
+
## β FAQ
|
|
163
|
+
|
|
164
|
+
<details>
|
|
165
|
+
<summary><b>Does Engly cost anything or send my prompts anywhere?</b></summary>
|
|
166
|
+
|
|
167
|
+
No. Engly is a script that writes a few files. It has no server, no API keys and no telemetry. The coaching is done by the agent you already use, in the same conversation.
|
|
168
|
+
</details>
|
|
169
|
+
|
|
170
|
+
<details>
|
|
171
|
+
<summary><b>Will it slow me down?</b></summary>
|
|
172
|
+
|
|
173
|
+
No. The note is a few lines at most, and the agent never waits for your confirmation. If you're in a hurry, type "engly off".
|
|
174
|
+
</details>
|
|
175
|
+
|
|
176
|
+
<details>
|
|
177
|
+
<summary><b>My English is already good. Is Engly useful for me?</b></summary>
|
|
178
|
+
|
|
179
|
+
Yes. Engly stays silent when your prompt is right, and at higher levels it suggests more natural and precise wording, which also helps the agent understand you better.
|
|
180
|
+
</details>
|
|
181
|
+
|
|
182
|
+
<details>
|
|
183
|
+
<summary><b>How do I uninstall it?</b></summary>
|
|
184
|
+
|
|
185
|
+
`engly remove` is coming soon. Until then, delete the `<!-- engly:start -->` β¦ `<!-- engly:end -->` block from your agent file, then delete the `.engly/` folder and the `engly-*` skill folders.
|
|
186
|
+
</details>
|
|
187
|
+
|
|
188
|
+
## π€ Contributing
|
|
189
|
+
|
|
190
|
+
Contributions are welcome! Open an issue to report a bug or suggest an idea, or send a pull request.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/not-implemented.ts
|
|
7
|
+
function notImplemented(command) {
|
|
8
|
+
console.log(`engly ${command}: not implemented yet`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/commands/config.ts
|
|
12
|
+
async function configCommand(_option, _value) {
|
|
13
|
+
notImplemented("config");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/commands/disable.ts
|
|
17
|
+
async function disableCommand() {
|
|
18
|
+
notImplemented("disable");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/commands/enable.ts
|
|
22
|
+
async function enableCommand() {
|
|
23
|
+
notImplemented("enable");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/commands/init.ts
|
|
27
|
+
import { join as join5 } from "path";
|
|
28
|
+
import * as p from "@clack/prompts";
|
|
29
|
+
|
|
30
|
+
// src/adapters/detect.ts
|
|
31
|
+
import { access } from "fs/promises";
|
|
32
|
+
import { join } from "path";
|
|
33
|
+
async function anyExists(cwd, paths) {
|
|
34
|
+
for (const path of paths) {
|
|
35
|
+
try {
|
|
36
|
+
await access(join(cwd, path));
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/adapters/claude.ts
|
|
45
|
+
var claude = {
|
|
46
|
+
id: "claude",
|
|
47
|
+
name: "Claude Code",
|
|
48
|
+
delivery: "import",
|
|
49
|
+
detect: (cwd) => anyExists(cwd, ["CLAUDE.md", ".claude"]),
|
|
50
|
+
instructionFile: () => "CLAUDE.md",
|
|
51
|
+
skillDirs: [".claude/skills"],
|
|
52
|
+
sizeLimit: {
|
|
53
|
+
maxLines: 200,
|
|
54
|
+
warning: "Claude Code follows long instruction files less closely."
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/adapters/codex.ts
|
|
59
|
+
var codex = {
|
|
60
|
+
id: "codex",
|
|
61
|
+
name: "Codex",
|
|
62
|
+
delivery: "embedded",
|
|
63
|
+
detect: (cwd) => anyExists(cwd, [".codex", "AGENTS.md"]),
|
|
64
|
+
instructionFile: () => "AGENTS.md",
|
|
65
|
+
skillDirs: [".agents/skills"],
|
|
66
|
+
sizeLimit: {
|
|
67
|
+
// Real limit is 32 KiB for all AGENTS.md files combined (spec Β§9.6).
|
|
68
|
+
maxBytes: 24 * 1024,
|
|
69
|
+
warning: "Codex may stop reading long instruction files.\nThe Engly block could be cut off."
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/adapters/copilot.ts
|
|
74
|
+
var copilot = {
|
|
75
|
+
id: "copilot",
|
|
76
|
+
name: "GitHub Copilot",
|
|
77
|
+
delivery: "embedded",
|
|
78
|
+
detect: (cwd) => anyExists(cwd, [".github/copilot-instructions.md", ".github/instructions"]),
|
|
79
|
+
instructionFile: () => ".github/copilot-instructions.md",
|
|
80
|
+
skillDirs: [".agents/skills", ".claude/skills", ".github/skills"]
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/adapters/cursor.ts
|
|
84
|
+
var cursor = {
|
|
85
|
+
id: "cursor",
|
|
86
|
+
name: "Cursor",
|
|
87
|
+
delivery: "own-file",
|
|
88
|
+
detect: (cwd) => anyExists(cwd, [".cursor", ".cursorrules"]),
|
|
89
|
+
instructionFile: () => ".cursor/rules/engly.mdc",
|
|
90
|
+
skillDirs: [".agents/skills", ".cursor/skills", ".claude/skills"]
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/adapters/opencode.ts
|
|
94
|
+
var opencode = {
|
|
95
|
+
id: "opencode",
|
|
96
|
+
name: "OpenCode",
|
|
97
|
+
delivery: "embedded",
|
|
98
|
+
detect: (cwd) => anyExists(cwd, ["opencode.json", "opencode.jsonc", ".opencode"]),
|
|
99
|
+
instructionFile: () => "AGENTS.md",
|
|
100
|
+
skillDirs: [".agents/skills", ".claude/skills", ".opencode/skills"]
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/adapters/types.ts
|
|
104
|
+
var TOOL_IDS = ["claude", "codex", "opencode", "cursor", "copilot"];
|
|
105
|
+
|
|
106
|
+
// src/adapters/index.ts
|
|
107
|
+
var adapters = { claude, codex, opencode, cursor, copilot };
|
|
108
|
+
var IMPLEMENTED_TOOLS = ["claude"];
|
|
109
|
+
function skillTargets(tools) {
|
|
110
|
+
const chosen = [];
|
|
111
|
+
const byChoices = [...tools].sort((a, b) => adapters[a].skillDirs.length - adapters[b].skillDirs.length);
|
|
112
|
+
for (const tool of byChoices) {
|
|
113
|
+
const dirs = adapters[tool].skillDirs;
|
|
114
|
+
const [preferred] = dirs;
|
|
115
|
+
if (preferred && !dirs.some((dir) => chosen.includes(dir))) chosen.push(preferred);
|
|
116
|
+
}
|
|
117
|
+
return chosen;
|
|
118
|
+
}
|
|
119
|
+
async function detectTools(cwd) {
|
|
120
|
+
const found = [];
|
|
121
|
+
for (const adapter of Object.values(adapters)) {
|
|
122
|
+
if (await adapter.detect(cwd)) found.push(adapter.id);
|
|
123
|
+
}
|
|
124
|
+
return found;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/core/block.ts
|
|
128
|
+
var START_MARKER_PREFIX = "<!-- engly:start";
|
|
129
|
+
var END_MARKER = "<!-- engly:end -->";
|
|
130
|
+
var START_MARKER_RE = /<!-- engly:start(?: v?([^\s>]+))? -->/g;
|
|
131
|
+
var BlockError = class extends Error {
|
|
132
|
+
constructor(message) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "BlockError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function detectEol(text) {
|
|
138
|
+
const index = text.indexOf("\n");
|
|
139
|
+
if (index > 0 && text[index - 1] === "\r") return "\r\n";
|
|
140
|
+
return "\n";
|
|
141
|
+
}
|
|
142
|
+
function startMarker(version) {
|
|
143
|
+
return `${START_MARKER_PREFIX} v${version.replace(/^v/, "")} -->`;
|
|
144
|
+
}
|
|
145
|
+
function findBlock(text) {
|
|
146
|
+
const starts = [...text.matchAll(START_MARKER_RE)];
|
|
147
|
+
const ends = indexesOf(text, END_MARKER);
|
|
148
|
+
if (starts.length === 0 && ends.length === 0) return null;
|
|
149
|
+
const [startMatch] = starts;
|
|
150
|
+
const [endIndex] = ends;
|
|
151
|
+
if (!startMatch || endIndex === void 0 || starts.length > 1 || ends.length > 1) {
|
|
152
|
+
throw new BlockError(
|
|
153
|
+
`Expected exactly one Engly block, found ${starts.length} start and ${ends.length} end marker(s). Fix the markers by hand before running Engly again.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const start = startMatch.index;
|
|
157
|
+
if (endIndex < start) {
|
|
158
|
+
throw new BlockError("The Engly end marker appears before the start marker.");
|
|
159
|
+
}
|
|
160
|
+
return { start, end: endIndex + END_MARKER.length, version: startMatch[1] ?? null };
|
|
161
|
+
}
|
|
162
|
+
function hasBlock(text) {
|
|
163
|
+
return findBlock(text) !== null;
|
|
164
|
+
}
|
|
165
|
+
function buildBlock(content, version, eol = "\n") {
|
|
166
|
+
const body = content.replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
167
|
+
const lines = [startMarker(version), ...body === "" ? [] : body.split("\n"), END_MARKER];
|
|
168
|
+
return lines.join(eol);
|
|
169
|
+
}
|
|
170
|
+
function upsertBlock(text, content, options) {
|
|
171
|
+
const eol = detectEol(text);
|
|
172
|
+
const block = buildBlock(content, options.version, eol);
|
|
173
|
+
const range = findBlock(text);
|
|
174
|
+
if (range) {
|
|
175
|
+
return { text: text.slice(0, range.start) + block + text.slice(range.end), action: "updated" };
|
|
176
|
+
}
|
|
177
|
+
if (text === "") {
|
|
178
|
+
return { text: block + eol, action: "created" };
|
|
179
|
+
}
|
|
180
|
+
if (options.position === "top") {
|
|
181
|
+
return { text: block + eol + eol + text, action: "prepended" };
|
|
182
|
+
}
|
|
183
|
+
const separator = endsWithEol(text) ? eol : eol + eol;
|
|
184
|
+
return { text: text + separator + block + eol, action: "appended" };
|
|
185
|
+
}
|
|
186
|
+
function endsWithEol(text) {
|
|
187
|
+
return text.endsWith("\n");
|
|
188
|
+
}
|
|
189
|
+
function indexesOf(text, search) {
|
|
190
|
+
const result = [];
|
|
191
|
+
let index = text.indexOf(search);
|
|
192
|
+
while (index !== -1) {
|
|
193
|
+
result.push(index);
|
|
194
|
+
index = text.indexOf(search, index + search.length);
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/core/config.ts
|
|
200
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
201
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
202
|
+
|
|
203
|
+
// src/core/paths.ts
|
|
204
|
+
import { existsSync, readFileSync } from "fs";
|
|
205
|
+
import { dirname, join as join2 } from "path";
|
|
206
|
+
import { fileURLToPath } from "url";
|
|
207
|
+
var cachedRoot;
|
|
208
|
+
function packageRoot() {
|
|
209
|
+
if (cachedRoot) return cachedRoot;
|
|
210
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
211
|
+
while (true) {
|
|
212
|
+
const pkgPath = join2(dir, "package.json");
|
|
213
|
+
if (existsSync(pkgPath)) {
|
|
214
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
215
|
+
if (pkg.name === "engly") {
|
|
216
|
+
cachedRoot = dir;
|
|
217
|
+
return dir;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const parent = dirname(dir);
|
|
221
|
+
if (parent === dir) throw new Error("Could not locate the engly package root.");
|
|
222
|
+
dir = parent;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function templatesDir() {
|
|
226
|
+
return join2(packageRoot(), "templates");
|
|
227
|
+
}
|
|
228
|
+
function packageVersion() {
|
|
229
|
+
const pkg = JSON.parse(readFileSync(join2(packageRoot(), "package.json"), "utf8"));
|
|
230
|
+
return pkg.version;
|
|
231
|
+
}
|
|
232
|
+
var ENGLY_DIR = ".engly";
|
|
233
|
+
var CONFIG_FILE = `${ENGLY_DIR}/config.json`;
|
|
234
|
+
var INSTRUCTIONS_FILE = `${ENGLY_DIR}/engly.md`;
|
|
235
|
+
|
|
236
|
+
// src/core/config.ts
|
|
237
|
+
var LEVELS = ["auto", "A2", "B1", "B2", "C1"];
|
|
238
|
+
var STYLES = ["light", "detailed"];
|
|
239
|
+
function defaultConfig(version) {
|
|
240
|
+
return {
|
|
241
|
+
version,
|
|
242
|
+
language: "auto",
|
|
243
|
+
level: "auto",
|
|
244
|
+
style: "light",
|
|
245
|
+
reviewEnglish: true,
|
|
246
|
+
enabled: true,
|
|
247
|
+
tools: []
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
var ConfigError = class extends Error {
|
|
251
|
+
constructor(message) {
|
|
252
|
+
super(message);
|
|
253
|
+
this.name = "ConfigError";
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function configPath(cwd) {
|
|
257
|
+
return join3(cwd, CONFIG_FILE);
|
|
258
|
+
}
|
|
259
|
+
async function readConfig(cwd, version) {
|
|
260
|
+
let raw;
|
|
261
|
+
try {
|
|
262
|
+
raw = await readFile(configPath(cwd), "utf8");
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error.code === "ENOENT") return null;
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
let parsed;
|
|
268
|
+
try {
|
|
269
|
+
parsed = JSON.parse(raw);
|
|
270
|
+
} catch {
|
|
271
|
+
throw new ConfigError(`${CONFIG_FILE} is not valid JSON.`);
|
|
272
|
+
}
|
|
273
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
274
|
+
throw new ConfigError(`${CONFIG_FILE} must contain a JSON object.`);
|
|
275
|
+
}
|
|
276
|
+
return validateConfig({ ...defaultConfig(version), ...parsed });
|
|
277
|
+
}
|
|
278
|
+
async function writeConfig(cwd, config) {
|
|
279
|
+
const path = configPath(cwd);
|
|
280
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
281
|
+
await writeFile(path, `${JSON.stringify(validateConfig(config), null, 2)}
|
|
282
|
+
`, "utf8");
|
|
283
|
+
}
|
|
284
|
+
function validateConfig(config) {
|
|
285
|
+
if (!LEVELS.includes(config.level)) {
|
|
286
|
+
throw new ConfigError(`Invalid level "${config.level}". Use one of: ${LEVELS.join(", ")}.`);
|
|
287
|
+
}
|
|
288
|
+
if (!STYLES.includes(config.style)) {
|
|
289
|
+
throw new ConfigError(`Invalid style "${config.style}". Use one of: ${STYLES.join(", ")}.`);
|
|
290
|
+
}
|
|
291
|
+
if (config.language !== "auto" && !/^[a-z]{2,3}$/.test(config.language)) {
|
|
292
|
+
throw new ConfigError(`Invalid language "${config.language}". Use "auto" or a language code like "es".`);
|
|
293
|
+
}
|
|
294
|
+
if (typeof config.enabled !== "boolean" || typeof config.reviewEnglish !== "boolean") {
|
|
295
|
+
throw new ConfigError('"enabled" and "reviewEnglish" must be true or false.');
|
|
296
|
+
}
|
|
297
|
+
if (!Array.isArray(config.tools)) {
|
|
298
|
+
throw new ConfigError('"tools" must be a list.');
|
|
299
|
+
}
|
|
300
|
+
return config;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/core/fs.ts
|
|
304
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
305
|
+
import { dirname as dirname3 } from "path";
|
|
306
|
+
async function readTextIfExists(path) {
|
|
307
|
+
try {
|
|
308
|
+
return await readFile2(path, "utf8");
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error.code === "ENOENT") return null;
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function writeText(path, content) {
|
|
315
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
316
|
+
await writeFile2(path, content, "utf8");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/core/git.ts
|
|
320
|
+
import { spawnSync } from "child_process";
|
|
321
|
+
function git(cwd, args) {
|
|
322
|
+
const result = spawnSync("git", args, { cwd, stdio: "ignore" });
|
|
323
|
+
if (result.error) return null;
|
|
324
|
+
return result.status;
|
|
325
|
+
}
|
|
326
|
+
function gitStatus(cwd, relativePath) {
|
|
327
|
+
if (git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== 0) return "no-git";
|
|
328
|
+
if (git(cwd, ["ls-files", "--error-unmatch", "--", relativePath]) === 0) return "tracked";
|
|
329
|
+
if (git(cwd, ["check-ignore", "-q", "--", relativePath]) === 0) return "ignored";
|
|
330
|
+
return "untracked";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/core/instructions.ts
|
|
334
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
335
|
+
import { join as join4 } from "path";
|
|
336
|
+
var SKILL_NAMES = ["engly-on", "engly-off", "engly-review"];
|
|
337
|
+
async function renderInstructions(config) {
|
|
338
|
+
const template = await readFile3(join4(templatesDir(), "engly.md"), "utf8");
|
|
339
|
+
const values = {
|
|
340
|
+
language: config.language === "auto" ? "auto" : config.language,
|
|
341
|
+
level: config.level,
|
|
342
|
+
styleRule: config.style === "light" ? 'show up to 3 corrections ("wrong" \u2192 "right" with a short reason) and the improved prompt.' : 'explain every mistake ("wrong" \u2192 "right" with a short reason) and show the improved prompt.',
|
|
343
|
+
englishRule: config.reviewEnglish ? "Prompt in correct English: show nothing." : "Prompt in English: show nothing, even if it has mistakes. Only coach prompts in other languages.",
|
|
344
|
+
lengthRule: config.style === "light" ? "Keep the note to 6 lines at most." : "Keep the note as short as possible."
|
|
345
|
+
};
|
|
346
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => values[key] ?? match);
|
|
347
|
+
}
|
|
348
|
+
async function readSkill(name) {
|
|
349
|
+
return readFile3(join4(templatesDir(), "skills", name, "SKILL.md"), "utf8");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/core/size.ts
|
|
353
|
+
function checkSize(text, limit) {
|
|
354
|
+
if (!limit) return null;
|
|
355
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
356
|
+
if (limit.maxBytes !== void 0 && bytes > limit.maxBytes) {
|
|
357
|
+
return { label: `${(bytes / 1024).toFixed(1)} KB`, warning: limit.warning };
|
|
358
|
+
}
|
|
359
|
+
const lines = text === "" ? 0 : text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
360
|
+
if (limit.maxLines !== void 0 && lines > limit.maxLines) {
|
|
361
|
+
return { label: `${lines} lines`, warning: limit.warning };
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/commands/init.ts
|
|
367
|
+
var CancelledError = class extends Error {
|
|
368
|
+
};
|
|
369
|
+
function guard(value) {
|
|
370
|
+
if (p.isCancel(value)) throw new CancelledError();
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
async function initCommand(options) {
|
|
374
|
+
try {
|
|
375
|
+
await runInit(options);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error instanceof CancelledError) {
|
|
378
|
+
p.cancel("Cancelled. Nothing was changed.");
|
|
379
|
+
process.exitCode = 1;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function runInit(options) {
|
|
386
|
+
const cwd = process.cwd();
|
|
387
|
+
const version = packageVersion();
|
|
388
|
+
const interactive = !options.yes;
|
|
389
|
+
if (interactive && !process.stdin.isTTY) {
|
|
390
|
+
throw new Error('No interactive terminal detected. Run "engly init --yes" to use the defaults.');
|
|
391
|
+
}
|
|
392
|
+
p.intro(`Engly v${version} \u2013 your English coach for AI prompts`);
|
|
393
|
+
const config = await readConfig(cwd, version) ?? defaultConfig(version);
|
|
394
|
+
config.version = version;
|
|
395
|
+
const tools = await chooseTools(cwd, options);
|
|
396
|
+
if (tools.length === 0) {
|
|
397
|
+
throw new Error(`No supported tool selected. Supported today: ${IMPLEMENTED_TOOLS.join(", ")}.`);
|
|
398
|
+
}
|
|
399
|
+
config.style = await chooseStyle(options, config.style);
|
|
400
|
+
const adapter = adapters.claude;
|
|
401
|
+
const agentFile = adapter.instructionFile();
|
|
402
|
+
const agentPath = join5(cwd, agentFile);
|
|
403
|
+
const current = await readTextIfExists(agentPath);
|
|
404
|
+
const blockExists = current !== null && hasBlock(current);
|
|
405
|
+
let apply = options.apply;
|
|
406
|
+
let position = "end";
|
|
407
|
+
if (apply && current !== null && !blockExists) {
|
|
408
|
+
const placement = await choosePlacement(agentFile, checkSize(current, adapter.sizeLimit), interactive);
|
|
409
|
+
if (placement === "skip") apply = false;
|
|
410
|
+
else position = placement;
|
|
411
|
+
}
|
|
412
|
+
const planned = upsertBlock(current ?? "", `@${INSTRUCTIONS_FILE}`, { version, position });
|
|
413
|
+
if (apply && interactive) {
|
|
414
|
+
const choice = guard(
|
|
415
|
+
await p.select({
|
|
416
|
+
message: `Add Engly block to:
|
|
417
|
+
${agentFile} (${adapter.name}) \u2192 ${describePlan(current, planned.action)}`,
|
|
418
|
+
options: [
|
|
419
|
+
{ value: "yes", label: "Yes" },
|
|
420
|
+
{ value: "skills", label: "No, only install skills" }
|
|
421
|
+
]
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
apply = choice === "yes";
|
|
425
|
+
}
|
|
426
|
+
if (apply) {
|
|
427
|
+
await warnIfInGit(cwd, agentFile, interactive);
|
|
428
|
+
}
|
|
429
|
+
const done = [];
|
|
430
|
+
await writeText(join5(cwd, INSTRUCTIONS_FILE), await renderInstructions(config));
|
|
431
|
+
if (apply) {
|
|
432
|
+
await writeText(agentPath, planned.text);
|
|
433
|
+
done.push(`\u2713 ${agentFile} ${current === null ? "created" : "updated"}`);
|
|
434
|
+
}
|
|
435
|
+
for (const dir of skillTargets(tools)) {
|
|
436
|
+
for (const skill of SKILL_NAMES) {
|
|
437
|
+
await writeText(join5(cwd, dir, skill, "SKILL.md"), await readSkill(skill));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
done.push(`\u2713 Skills installed: ${SKILL_NAMES.join(", ")}`);
|
|
441
|
+
config.tools = tools;
|
|
442
|
+
config.enabled = true;
|
|
443
|
+
await writeConfig(cwd, config);
|
|
444
|
+
p.log.success(done.join("\n"));
|
|
445
|
+
p.outro(
|
|
446
|
+
apply ? "Done! Your next prompt will be coached." : `Skills installed. Run "engly init" again to add the Engly block to ${agentFile}.`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
async function chooseTools(cwd, options) {
|
|
450
|
+
if (options.tools) {
|
|
451
|
+
const requested = options.tools.split(",").map((tool) => tool.trim().toLowerCase()).filter(Boolean);
|
|
452
|
+
const unknown = requested.filter((tool) => !TOOL_IDS.includes(tool));
|
|
453
|
+
if (unknown.length > 0) {
|
|
454
|
+
throw new Error(`Unknown tool(s): ${unknown.join(", ")}. Use: ${TOOL_IDS.join(", ")}.`);
|
|
455
|
+
}
|
|
456
|
+
return keepImplemented(requested);
|
|
457
|
+
}
|
|
458
|
+
const detected = await detectTools(cwd);
|
|
459
|
+
const initial = detected.filter((tool) => IMPLEMENTED_TOOLS.includes(tool));
|
|
460
|
+
const defaults = initial.length > 0 ? initial : [...IMPLEMENTED_TOOLS];
|
|
461
|
+
if (options.yes) return defaults;
|
|
462
|
+
return guard(
|
|
463
|
+
await p.multiselect({
|
|
464
|
+
message: "Which AI tools do you use?",
|
|
465
|
+
options: TOOL_IDS.map((id) => ({
|
|
466
|
+
value: id,
|
|
467
|
+
label: adapters[id].name,
|
|
468
|
+
hint: IMPLEMENTED_TOOLS.includes(id) ? void 0 : "coming soon",
|
|
469
|
+
disabled: !IMPLEMENTED_TOOLS.includes(id)
|
|
470
|
+
})),
|
|
471
|
+
initialValues: defaults,
|
|
472
|
+
required: true
|
|
473
|
+
})
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
function keepImplemented(tools) {
|
|
477
|
+
const skipped = tools.filter((tool) => !IMPLEMENTED_TOOLS.includes(tool));
|
|
478
|
+
if (skipped.length > 0) {
|
|
479
|
+
p.log.warn(`Not supported yet, skipped: ${skipped.map((tool) => adapters[tool].name).join(", ")}`);
|
|
480
|
+
}
|
|
481
|
+
return tools.filter((tool) => IMPLEMENTED_TOOLS.includes(tool));
|
|
482
|
+
}
|
|
483
|
+
async function chooseStyle(options, current) {
|
|
484
|
+
if (options.style) {
|
|
485
|
+
if (!STYLES.includes(options.style)) {
|
|
486
|
+
throw new Error(`Invalid style "${options.style}". Use: ${STYLES.join(", ")}.`);
|
|
487
|
+
}
|
|
488
|
+
return options.style;
|
|
489
|
+
}
|
|
490
|
+
if (options.yes) return current;
|
|
491
|
+
return guard(
|
|
492
|
+
await p.select({
|
|
493
|
+
message: "How detailed should the feedback be?",
|
|
494
|
+
initialValue: current,
|
|
495
|
+
options: [
|
|
496
|
+
{ value: "light", label: "Light \u2013 max 3 corrections" },
|
|
497
|
+
{ value: "detailed", label: "Detailed \u2013 explain every mistake" }
|
|
498
|
+
]
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
function describePlan(current, action) {
|
|
503
|
+
if (current === null) return "new file";
|
|
504
|
+
if (action === "updated") return "exists, block updated";
|
|
505
|
+
return action === "prepended" ? "exists, block added near the top" : "exists, block appended";
|
|
506
|
+
}
|
|
507
|
+
async function choosePlacement(file, size, interactive) {
|
|
508
|
+
if (!size) return "end";
|
|
509
|
+
const message = `${file} is ${size.label}
|
|
510
|
+
\u26A0 ${size.warning}`;
|
|
511
|
+
if (!interactive) {
|
|
512
|
+
p.log.warn(`${message}
|
|
513
|
+
The Engly block goes near the top.`);
|
|
514
|
+
return "top";
|
|
515
|
+
}
|
|
516
|
+
p.log.warn(message);
|
|
517
|
+
return guard(
|
|
518
|
+
await p.select({
|
|
519
|
+
message: "Where should the block go?",
|
|
520
|
+
options: [
|
|
521
|
+
{ value: "top", label: "Near the top \u2013 recommended" },
|
|
522
|
+
{ value: "end", label: "At the end" },
|
|
523
|
+
{ value: "skip", label: `Skip ${file}` }
|
|
524
|
+
]
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
async function warnIfInGit(cwd, file, interactive) {
|
|
529
|
+
const status = gitStatus(cwd, file);
|
|
530
|
+
if (status !== "tracked" && status !== "untracked") return;
|
|
531
|
+
const title = status === "tracked" ? `${file} is tracked by git.` : `${file} is not ignored by git.`;
|
|
532
|
+
const warning = "\u26A0 Your teammates will also be coached if you commit it.";
|
|
533
|
+
if (!interactive) {
|
|
534
|
+
p.log.warn(`${title}
|
|
535
|
+
${warning}`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const choice = guard(
|
|
539
|
+
await p.select({
|
|
540
|
+
message: `${title}
|
|
541
|
+
${warning}`,
|
|
542
|
+
options: [
|
|
543
|
+
{ value: "continue", label: "Continue anyway" },
|
|
544
|
+
{ value: "cancel", label: "Cancel" }
|
|
545
|
+
]
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
if (choice === "cancel") throw new CancelledError();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/commands/remove.ts
|
|
552
|
+
async function removeCommand() {
|
|
553
|
+
notImplemented("remove");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/commands/status.ts
|
|
557
|
+
async function statusCommand() {
|
|
558
|
+
notImplemented("status");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/commands/update.ts
|
|
562
|
+
async function updateCommand() {
|
|
563
|
+
notImplemented("update");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/cli.ts
|
|
567
|
+
var program = new Command();
|
|
568
|
+
program.name("engly").description("Your English coach for AI prompts. One command. Every agent. Always on. Zero cost.").version(packageVersion());
|
|
569
|
+
program.command("init").description("Install Engly in the current project").option("--tools <tools>", "comma-separated agents: claude,codex,opencode,cursor,copilot").option("--style <style>", "feedback style: light or detailed").option("-y, --yes", "accept all defaults without asking").option("--no-apply", "only install the skills, don't touch the agent files").action(initCommand);
|
|
570
|
+
program.command("update").description("Update the skills and regenerate the Engly block").action(updateCommand);
|
|
571
|
+
program.command("enable").description("Turn the coach on permanently").action(enableCommand);
|
|
572
|
+
program.command("disable").description("Turn the coach off permanently, without uninstalling").action(disableCommand);
|
|
573
|
+
program.command("config").description('Change an option, e.g. "engly config level B2"').argument("<option>", "language, level or style").argument("<value>", "new value").action(configCommand);
|
|
574
|
+
program.command("status").description("Show where Engly is installed and whether it is active").action(statusCommand);
|
|
575
|
+
program.command("remove").description("Remove the Engly block, skills and config").action(removeCommand);
|
|
576
|
+
try {
|
|
577
|
+
await program.parseAsync();
|
|
578
|
+
} catch (error) {
|
|
579
|
+
console.error(`engly: ${error instanceof Error ? error.message : String(error)}`);
|
|
580
|
+
process.exitCode = 1;
|
|
581
|
+
}
|
|
582
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/commands/not-implemented.ts","../src/commands/config.ts","../src/commands/disable.ts","../src/commands/enable.ts","../src/commands/init.ts","../src/adapters/detect.ts","../src/adapters/claude.ts","../src/adapters/codex.ts","../src/adapters/copilot.ts","../src/adapters/cursor.ts","../src/adapters/opencode.ts","../src/adapters/types.ts","../src/adapters/index.ts","../src/core/block.ts","../src/core/config.ts","../src/core/paths.ts","../src/core/fs.ts","../src/core/git.ts","../src/core/instructions.ts","../src/core/size.ts","../src/commands/remove.ts","../src/commands/status.ts","../src/commands/update.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander';\nimport { configCommand } from './commands/config.js';\nimport { disableCommand } from './commands/disable.js';\nimport { enableCommand } from './commands/enable.js';\nimport { initCommand } from './commands/init.js';\nimport { removeCommand } from './commands/remove.js';\nimport { statusCommand } from './commands/status.js';\nimport { updateCommand } from './commands/update.js';\nimport { packageVersion } from './core/paths.js';\n\nconst program = new Command();\n\nprogram\n .name('engly')\n .description('Your English coach for AI prompts. One command. Every agent. Always on. Zero cost.')\n .version(packageVersion());\n\nprogram\n .command('init')\n .description('Install Engly in the current project')\n .option('--tools <tools>', 'comma-separated agents: claude,codex,opencode,cursor,copilot')\n .option('--style <style>', 'feedback style: light or detailed')\n .option('-y, --yes', 'accept all defaults without asking')\n .option('--no-apply', \"only install the skills, don't touch the agent files\")\n .action(initCommand);\n\nprogram.command('update').description('Update the skills and regenerate the Engly block').action(updateCommand);\nprogram.command('enable').description('Turn the coach on permanently').action(enableCommand);\nprogram.command('disable').description('Turn the coach off permanently, without uninstalling').action(disableCommand);\nprogram\n .command('config')\n .description('Change an option, e.g. \"engly config level B2\"')\n .argument('<option>', 'language, level or style')\n .argument('<value>', 'new value')\n .action(configCommand);\nprogram.command('status').description('Show where Engly is installed and whether it is active').action(statusCommand);\nprogram.command('remove').description('Remove the Engly block, skills and config').action(removeCommand);\n\ntry {\n await program.parseAsync();\n} catch (error) {\n console.error(`engly: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n}\n","export function notImplemented(command: string): void {\n console.log(`engly ${command}: not implemented yet`);\n}\n","import { notImplemented } from './not-implemented.js';\n\nexport async function configCommand(_option: string, _value: string): Promise<void> {\n notImplemented('config');\n}\n","import { notImplemented } from './not-implemented.js';\n\nexport async function disableCommand(): Promise<void> {\n notImplemented('disable');\n}\n","import { notImplemented } from './not-implemented.js';\n\nexport async function enableCommand(): Promise<void> {\n notImplemented('enable');\n}\n","import { join } from 'node:path';\nimport * as p from '@clack/prompts';\nimport { adapters, detectTools, IMPLEMENTED_TOOLS, skillTargets, TOOL_IDS, type ToolId } from '../adapters/index.js';\nimport { type BlockPosition, hasBlock, type UpsertAction, upsertBlock } from '../core/block.js';\nimport { defaultConfig, readConfig, STYLES, type Style, writeConfig } from '../core/config.js';\nimport { readTextIfExists, writeText } from '../core/fs.js';\nimport { gitStatus } from '../core/git.js';\nimport { readSkill, renderInstructions, SKILL_NAMES } from '../core/instructions.js';\nimport { INSTRUCTIONS_FILE, packageVersion } from '../core/paths.js';\nimport { checkSize, type SizeReport } from '../core/size.js';\n\nexport interface InitOptions {\n tools?: string;\n style?: string;\n yes?: boolean;\n apply: boolean;\n}\n\nclass CancelledError extends Error {}\n\nfunction guard<T>(value: T): Exclude<T, symbol> {\n if (p.isCancel(value)) throw new CancelledError();\n return value as Exclude<T, symbol>;\n}\n\nexport async function initCommand(options: InitOptions): Promise<void> {\n try {\n await runInit(options);\n } catch (error) {\n if (error instanceof CancelledError) {\n p.cancel('Cancelled. Nothing was changed.');\n process.exitCode = 1;\n return;\n }\n throw error;\n }\n}\n\nasync function runInit(options: InitOptions): Promise<void> {\n const cwd = process.cwd();\n const version = packageVersion();\n const interactive = !options.yes;\n\n if (interactive && !process.stdin.isTTY) {\n throw new Error('No interactive terminal detected. Run \"engly init --yes\" to use the defaults.');\n }\n\n p.intro(`Engly v${version} β your English coach for AI prompts`);\n\n const config = (await readConfig(cwd, version)) ?? defaultConfig(version);\n config.version = version;\n\n const tools = await chooseTools(cwd, options);\n if (tools.length === 0) {\n throw new Error(`No supported tool selected. Supported today: ${IMPLEMENTED_TOOLS.join(', ')}.`);\n }\n\n config.style = await chooseStyle(options, config.style);\n\n const adapter = adapters.claude;\n const agentFile = adapter.instructionFile();\n const agentPath = join(cwd, agentFile);\n\n const current = await readTextIfExists(agentPath);\n // Checked before any prompt or write, so broken markers fail early.\n const blockExists = current !== null && hasBlock(current);\n\n let apply = options.apply;\n let position: BlockPosition = 'end';\n if (apply && current !== null && !blockExists) {\n const placement = await choosePlacement(agentFile, checkSize(current, adapter.sizeLimit), interactive);\n if (placement === 'skip') apply = false;\n else position = placement;\n }\n const planned = upsertBlock(current ?? '', `@${INSTRUCTIONS_FILE}`, { version, position });\n\n if (apply && interactive) {\n const choice = guard(\n await p.select({\n message: `Add Engly block to:\\n${agentFile} (${adapter.name}) β ${describePlan(current, planned.action)}`,\n options: [\n { value: 'yes', label: 'Yes' },\n { value: 'skills', label: 'No, only install skills' },\n ],\n }),\n );\n apply = choice === 'yes';\n }\n\n if (apply) {\n await warnIfInGit(cwd, agentFile, interactive);\n }\n\n const done: string[] = [];\n\n await writeText(join(cwd, INSTRUCTIONS_FILE), await renderInstructions(config));\n\n if (apply) {\n await writeText(agentPath, planned.text);\n done.push(`β ${agentFile} ${current === null ? 'created' : 'updated'}`);\n }\n\n for (const dir of skillTargets(tools)) {\n for (const skill of SKILL_NAMES) {\n await writeText(join(cwd, dir, skill, 'SKILL.md'), await readSkill(skill));\n }\n }\n done.push(`β Skills installed: ${SKILL_NAMES.join(', ')}`);\n\n config.tools = tools;\n config.enabled = true;\n await writeConfig(cwd, config);\n\n p.log.success(done.join('\\n'));\n p.outro(\n apply\n ? 'Done! Your next prompt will be coached.'\n : `Skills installed. Run \"engly init\" again to add the Engly block to ${agentFile}.`,\n );\n}\n\nasync function chooseTools(cwd: string, options: InitOptions): Promise<ToolId[]> {\n if (options.tools) {\n const requested = options.tools\n .split(',')\n .map((tool) => tool.trim().toLowerCase())\n .filter(Boolean);\n const unknown = requested.filter((tool) => !(TOOL_IDS as readonly string[]).includes(tool));\n if (unknown.length > 0) {\n throw new Error(`Unknown tool(s): ${unknown.join(', ')}. Use: ${TOOL_IDS.join(', ')}.`);\n }\n return keepImplemented(requested as ToolId[]);\n }\n\n const detected = await detectTools(cwd);\n const initial = detected.filter((tool) => IMPLEMENTED_TOOLS.includes(tool));\n const defaults = initial.length > 0 ? initial : [...IMPLEMENTED_TOOLS];\n\n if (options.yes) return defaults;\n\n return guard(\n await p.multiselect<ToolId>({\n message: 'Which AI tools do you use?',\n options: TOOL_IDS.map((id) => ({\n value: id,\n label: adapters[id].name,\n hint: IMPLEMENTED_TOOLS.includes(id) ? undefined : 'coming soon',\n disabled: !IMPLEMENTED_TOOLS.includes(id),\n })),\n initialValues: defaults,\n required: true,\n }),\n );\n}\n\nfunction keepImplemented(tools: ToolId[]): ToolId[] {\n const skipped = tools.filter((tool) => !IMPLEMENTED_TOOLS.includes(tool));\n if (skipped.length > 0) {\n p.log.warn(`Not supported yet, skipped: ${skipped.map((tool) => adapters[tool].name).join(', ')}`);\n }\n return tools.filter((tool) => IMPLEMENTED_TOOLS.includes(tool));\n}\n\nasync function chooseStyle(options: InitOptions, current: Style): Promise<Style> {\n if (options.style) {\n if (!(STYLES as readonly string[]).includes(options.style)) {\n throw new Error(`Invalid style \"${options.style}\". Use: ${STYLES.join(', ')}.`);\n }\n return options.style as Style;\n }\n if (options.yes) return current;\n\n return guard(\n await p.select<Style>({\n message: 'How detailed should the feedback be?',\n initialValue: current,\n options: [\n { value: 'light', label: 'Light β max 3 corrections' },\n { value: 'detailed', label: 'Detailed β explain every mistake' },\n ],\n }),\n );\n}\n\nfunction describePlan(current: string | null, action: UpsertAction): string {\n if (current === null) return 'new file';\n if (action === 'updated') return 'exists, block updated';\n return action === 'prepended' ? 'exists, block added near the top' : 'exists, block appended';\n}\n\nasync function choosePlacement(\n file: string,\n size: SizeReport | null,\n interactive: boolean,\n): Promise<BlockPosition | 'skip'> {\n if (!size) return 'end';\n\n const message = `${file} is ${size.label}\\nβ ${size.warning}`;\n if (!interactive) {\n p.log.warn(`${message}\\nThe Engly block goes near the top.`);\n return 'top';\n }\n\n p.log.warn(message);\n return guard(\n await p.select<BlockPosition | 'skip'>({\n message: 'Where should the block go?',\n options: [\n { value: 'top', label: 'Near the top β recommended' },\n { value: 'end', label: 'At the end' },\n { value: 'skip', label: `Skip ${file}` },\n ],\n }),\n );\n}\n\nasync function warnIfInGit(cwd: string, file: string, interactive: boolean): Promise<void> {\n const status = gitStatus(cwd, file);\n if (status !== 'tracked' && status !== 'untracked') return;\n\n const title = status === 'tracked' ? `${file} is tracked by git.` : `${file} is not ignored by git.`;\n const warning = 'β Your teammates will also be coached if you commit it.';\n\n if (!interactive) {\n p.log.warn(`${title}\\n${warning}`);\n return;\n }\n\n const choice = guard(\n await p.select({\n message: `${title}\\n${warning}`,\n options: [\n { value: 'continue', label: 'Continue anyway' },\n { value: 'cancel', label: 'Cancel' },\n ],\n }),\n );\n if (choice === 'cancel') throw new CancelledError();\n}\n","import { access } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport async function anyExists(cwd: string, paths: string[]): Promise<boolean> {\n for (const path of paths) {\n try {\n await access(join(cwd, path));\n return true;\n } catch {}\n }\n return false;\n}\n","import { anyExists } from './detect.js';\nimport type { Adapter } from './types.js';\n\nexport const claude: Adapter = {\n id: 'claude',\n name: 'Claude Code',\n delivery: 'import',\n detect: (cwd) => anyExists(cwd, ['CLAUDE.md', '.claude']),\n instructionFile: () => 'CLAUDE.md',\n skillDirs: ['.claude/skills'],\n sizeLimit: {\n maxLines: 200,\n warning: 'Claude Code follows long instruction files less closely.',\n },\n};\n","import { anyExists } from './detect.js';\nimport type { Adapter } from './types.js';\n\nexport const codex: Adapter = {\n id: 'codex',\n name: 'Codex',\n delivery: 'embedded',\n detect: (cwd) => anyExists(cwd, ['.codex', 'AGENTS.md']),\n instructionFile: () => 'AGENTS.md',\n skillDirs: ['.agents/skills'],\n sizeLimit: {\n // Real limit is 32 KiB for all AGENTS.md files combined (spec Β§9.6).\n maxBytes: 24 * 1024,\n warning: 'Codex may stop reading long instruction files.\\nThe Engly block could be cut off.',\n },\n};\n","import { anyExists } from './detect.js';\nimport type { Adapter } from './types.js';\n\nexport const copilot: Adapter = {\n id: 'copilot',\n name: 'GitHub Copilot',\n delivery: 'embedded',\n detect: (cwd) => anyExists(cwd, ['.github/copilot-instructions.md', '.github/instructions']),\n instructionFile: () => '.github/copilot-instructions.md',\n skillDirs: ['.agents/skills', '.claude/skills', '.github/skills'],\n};\n","import { anyExists } from './detect.js';\nimport type { Adapter } from './types.js';\n\nexport const CURSOR_RULE_FRONTMATTER = '---\\ndescription: Engly English coach\\nalwaysApply: true\\n---\\n';\n\n// TODO(spec Β§15): Cursor reads .claude/skills/ only as a \"legacy\" folder and\n// was not tested. If it stops doing so, Cursor + Claude Code would get no skills.\nexport const cursor: Adapter = {\n id: 'cursor',\n name: 'Cursor',\n delivery: 'own-file',\n detect: (cwd) => anyExists(cwd, ['.cursor', '.cursorrules']),\n instructionFile: () => '.cursor/rules/engly.mdc',\n skillDirs: ['.agents/skills', '.cursor/skills', '.claude/skills'],\n};\n","import { anyExists } from './detect.js';\nimport type { Adapter } from './types.js';\n\nexport const opencode: Adapter = {\n id: 'opencode',\n name: 'OpenCode',\n delivery: 'embedded',\n detect: (cwd) => anyExists(cwd, ['opencode.json', 'opencode.jsonc', '.opencode']),\n instructionFile: () => 'AGENTS.md',\n skillDirs: ['.agents/skills', '.claude/skills', '.opencode/skills'],\n};\n","export const TOOL_IDS = ['claude', 'codex', 'opencode', 'cursor', 'copilot'] as const;\nexport type ToolId = (typeof TOOL_IDS)[number];\n\nexport type Delivery = 'import' | 'embedded' | 'own-file';\n\nexport interface SizeLimit {\n maxBytes?: number;\n maxLines?: number;\n warning: string;\n}\n\nexport interface Adapter {\n id: ToolId;\n name: string;\n delivery: Delivery;\n detect(cwd: string): Promise<boolean>;\n instructionFile(): string;\n // Order matters: the first folder is the one Engly installs to (see skillTargets).\n skillDirs: readonly string[];\n sizeLimit?: SizeLimit;\n}\n","import { claude } from './claude.js';\nimport { codex } from './codex.js';\nimport { copilot } from './copilot.js';\nimport { cursor } from './cursor.js';\nimport { opencode } from './opencode.js';\nimport type { Adapter, ToolId } from './types.js';\n\nexport type { Adapter, Delivery, ToolId } from './types.js';\nexport { TOOL_IDS } from './types.js';\n\n// Adding a new agent = adding its adapter here (spec Β§5).\nexport const adapters: Record<ToolId, Adapter> = { claude, codex, opencode, cursor, copilot };\n\nexport const IMPLEMENTED_TOOLS: readonly ToolId[] = ['claude'];\n\nexport function skillTargets(tools: readonly ToolId[]): string[] {\n const chosen: string[] = [];\n const byChoices = [...tools].sort((a, b) => adapters[a].skillDirs.length - adapters[b].skillDirs.length);\n for (const tool of byChoices) {\n const dirs = adapters[tool].skillDirs;\n const [preferred] = dirs;\n if (preferred && !dirs.some((dir) => chosen.includes(dir))) chosen.push(preferred);\n }\n return chosen;\n}\n\nexport async function detectTools(cwd: string): Promise<ToolId[]> {\n const found: ToolId[] = [];\n for (const adapter of Object.values(adapters)) {\n if (await adapter.detect(cwd)) found.push(adapter.id);\n }\n return found;\n}\n","export const START_MARKER_PREFIX = '<!-- engly:start';\nexport const END_MARKER = '<!-- engly:end -->';\n\nconst START_MARKER_RE = /<!-- engly:start(?: v?([^\\s>]+))? -->/g;\n\nexport type Eol = '\\n' | '\\r\\n';\nexport type BlockPosition = 'top' | 'end';\nexport type UpsertAction = 'created' | 'appended' | 'prepended' | 'updated';\n\nexport class BlockError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BlockError';\n }\n}\n\nexport interface BlockRange {\n start: number;\n end: number;\n version: string | null;\n}\n\nexport function detectEol(text: string): Eol {\n const index = text.indexOf('\\n');\n if (index > 0 && text[index - 1] === '\\r') return '\\r\\n';\n return '\\n';\n}\n\nexport function startMarker(version: string): string {\n return `${START_MARKER_PREFIX} v${version.replace(/^v/, '')} -->`;\n}\n\n// Broken markers throw instead of being repaired: editing around them could destroy user content.\nexport function findBlock(text: string): BlockRange | null {\n const starts = [...text.matchAll(START_MARKER_RE)];\n const ends = indexesOf(text, END_MARKER);\n\n if (starts.length === 0 && ends.length === 0) return null;\n\n const [startMatch] = starts;\n const [endIndex] = ends;\n if (!startMatch || endIndex === undefined || starts.length > 1 || ends.length > 1) {\n throw new BlockError(\n `Expected exactly one Engly block, found ${starts.length} start and ${ends.length} end marker(s). ` +\n 'Fix the markers by hand before running Engly again.',\n );\n }\n\n const start = startMatch.index;\n if (endIndex < start) {\n throw new BlockError('The Engly end marker appears before the start marker.');\n }\n\n return { start, end: endIndex + END_MARKER.length, version: startMatch[1] ?? null };\n}\n\nexport function hasBlock(text: string): boolean {\n return findBlock(text) !== null;\n}\n\nexport function readBlock(text: string): string | null {\n const range = findBlock(text);\n if (!range) return null;\n const inner = text.slice(range.start, range.end);\n const afterStart = inner.indexOf('-->') + 3;\n return stripOneEol(inner.slice(afterStart, inner.length - END_MARKER.length), 'both');\n}\n\nexport function buildBlock(content: string, version: string, eol: Eol = '\\n'): string {\n const body = content.replace(/\\r\\n/g, '\\n').replace(/\\n+$/, '');\n const lines = [startMarker(version), ...(body === '' ? [] : body.split('\\n')), END_MARKER];\n return lines.join(eol);\n}\n\nexport interface UpsertOptions {\n version: string;\n position?: BlockPosition;\n}\n\nexport interface UpsertResult {\n text: string;\n action: UpsertAction;\n}\n\nexport function upsertBlock(text: string, content: string, options: UpsertOptions): UpsertResult {\n const eol = detectEol(text);\n const block = buildBlock(content, options.version, eol);\n const range = findBlock(text);\n\n if (range) {\n return { text: text.slice(0, range.start) + block + text.slice(range.end), action: 'updated' };\n }\n\n if (text === '') {\n return { text: block + eol, action: 'created' };\n }\n\n if (options.position === 'top') {\n return { text: block + eol + eol + text, action: 'prepended' };\n }\n\n const separator = endsWithEol(text) ? eol : eol + eol;\n return { text: text + separator + block + eol, action: 'appended' };\n}\n\nexport interface RemoveResult {\n text: string;\n removed: boolean;\n}\n\nexport function removeBlock(text: string): RemoveResult {\n const range = findBlock(text);\n if (!range) return { text, removed: false };\n\n let before = text.slice(0, range.start);\n let after = stripOneEol(text.slice(range.end), 'start');\n\n if (before === '') {\n after = stripOneEol(after, 'start');\n } else if (/(\\r?\\n)\\r?\\n$/.test(before)) {\n before = stripOneEol(before, 'end');\n }\n\n return { text: before + after, removed: true };\n}\n\nfunction endsWithEol(text: string): boolean {\n return text.endsWith('\\n');\n}\n\nfunction stripOneEol(text: string, side: 'start' | 'end' | 'both'): string {\n let result = text;\n if (side === 'start' || side === 'both') {\n if (result.startsWith('\\r\\n')) result = result.slice(2);\n else if (result.startsWith('\\n')) result = result.slice(1);\n }\n if (side === 'end' || side === 'both') {\n if (result.endsWith('\\r\\n')) result = result.slice(0, -2);\n else if (result.endsWith('\\n')) result = result.slice(0, -1);\n }\n return result;\n}\n\nfunction indexesOf(text: string, search: string): number[] {\n const result: number[] = [];\n let index = text.indexOf(search);\n while (index !== -1) {\n result.push(index);\n index = text.indexOf(search, index + search.length);\n }\n return result;\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport type { ToolId } from '../adapters/types.js';\nimport { CONFIG_FILE } from './paths.js';\n\nexport const LEVELS = ['auto', 'A2', 'B1', 'B2', 'C1'] as const;\nexport const STYLES = ['light', 'detailed'] as const;\n\nexport type Level = (typeof LEVELS)[number];\nexport type Style = (typeof STYLES)[number];\n\nexport interface EnglyConfig {\n version: string;\n language: string;\n level: Level;\n style: Style;\n reviewEnglish: boolean;\n enabled: boolean;\n tools: ToolId[];\n}\n\nexport function defaultConfig(version: string): EnglyConfig {\n return {\n version,\n language: 'auto',\n level: 'auto',\n style: 'light',\n reviewEnglish: true,\n enabled: true,\n tools: [],\n };\n}\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\nexport function configPath(cwd: string): string {\n return join(cwd, CONFIG_FILE);\n}\n\nexport async function readConfig(cwd: string, version: string): Promise<EnglyConfig | null> {\n let raw: string;\n try {\n raw = await readFile(configPath(cwd), 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new ConfigError(`${CONFIG_FILE} is not valid JSON.`);\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new ConfigError(`${CONFIG_FILE} must contain a JSON object.`);\n }\n\n return validateConfig({ ...defaultConfig(version), ...(parsed as Partial<EnglyConfig>) });\n}\n\nexport async function writeConfig(cwd: string, config: EnglyConfig): Promise<void> {\n const path = configPath(cwd);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(validateConfig(config), null, 2)}\\n`, 'utf8');\n}\n\nexport function validateConfig(config: EnglyConfig): EnglyConfig {\n if (!(LEVELS as readonly string[]).includes(config.level)) {\n throw new ConfigError(`Invalid level \"${config.level}\". Use one of: ${LEVELS.join(', ')}.`);\n }\n if (!(STYLES as readonly string[]).includes(config.style)) {\n throw new ConfigError(`Invalid style \"${config.style}\". Use one of: ${STYLES.join(', ')}.`);\n }\n if (config.language !== 'auto' && !/^[a-z]{2,3}$/.test(config.language)) {\n throw new ConfigError(`Invalid language \"${config.language}\". Use \"auto\" or a language code like \"es\".`);\n }\n if (typeof config.enabled !== 'boolean' || typeof config.reviewEnglish !== 'boolean') {\n throw new ConfigError('\"enabled\" and \"reviewEnglish\" must be true or false.');\n }\n if (!Array.isArray(config.tools)) {\n throw new ConfigError('\"tools\" must be a list.');\n }\n return config;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nlet cachedRoot: string | undefined;\n\nexport function packageRoot(): string {\n if (cachedRoot) return cachedRoot;\n let dir = dirname(fileURLToPath(import.meta.url));\n while (true) {\n const pkgPath = join(dir, 'package.json');\n if (existsSync(pkgPath)) {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { name?: string };\n if (pkg.name === 'engly') {\n cachedRoot = dir;\n return dir;\n }\n }\n const parent = dirname(dir);\n if (parent === dir) throw new Error('Could not locate the engly package root.');\n dir = parent;\n }\n}\n\nexport function templatesDir(): string {\n return join(packageRoot(), 'templates');\n}\n\nexport function packageVersion(): string {\n const pkg = JSON.parse(readFileSync(join(packageRoot(), 'package.json'), 'utf8')) as { version: string };\n return pkg.version;\n}\n\nexport const ENGLY_DIR = '.engly';\nexport const CONFIG_FILE = `${ENGLY_DIR}/config.json`;\nexport const INSTRUCTIONS_FILE = `${ENGLY_DIR}/engly.md`;\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\nexport async function readTextIfExists(path: string): Promise<string | null> {\n try {\n return await readFile(path, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n}\n\nexport async function writeText(path: string, content: string): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, content, 'utf8');\n}\n","import { spawnSync } from 'node:child_process';\n\nexport type GitStatus = 'tracked' | 'untracked' | 'ignored' | 'no-git';\n\nfunction git(cwd: string, args: string[]): number | null {\n const result = spawnSync('git', args, { cwd, stdio: 'ignore' });\n if (result.error) return null;\n return result.status;\n}\n\nexport function gitStatus(cwd: string, relativePath: string): GitStatus {\n if (git(cwd, ['rev-parse', '--is-inside-work-tree']) !== 0) return 'no-git';\n if (git(cwd, ['ls-files', '--error-unmatch', '--', relativePath]) === 0) return 'tracked';\n if (git(cwd, ['check-ignore', '-q', '--', relativePath]) === 0) return 'ignored';\n return 'untracked';\n}\n","import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { EnglyConfig } from './config.js';\nimport { templatesDir } from './paths.js';\n\nexport const SKILL_NAMES = ['engly-on', 'engly-off', 'engly-review'] as const;\nexport type SkillName = (typeof SKILL_NAMES)[number];\n\nexport async function renderInstructions(config: EnglyConfig): Promise<string> {\n const template = await readFile(join(templatesDir(), 'engly.md'), 'utf8');\n\n const values: Record<string, string> = {\n language: config.language === 'auto' ? 'auto' : config.language,\n level: config.level,\n styleRule:\n config.style === 'light'\n ? 'show up to 3 corrections (\"wrong\" β \"right\" with a short reason) and the improved prompt.'\n : 'explain every mistake (\"wrong\" β \"right\" with a short reason) and show the improved prompt.',\n englishRule: config.reviewEnglish\n ? 'Prompt in correct English: show nothing.'\n : 'Prompt in English: show nothing, even if it has mistakes. Only coach prompts in other languages.',\n lengthRule: config.style === 'light' ? 'Keep the note to 6 lines at most.' : 'Keep the note as short as possible.',\n };\n\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => values[key] ?? match);\n}\n\nexport async function readSkill(name: SkillName): Promise<string> {\n return readFile(join(templatesDir(), 'skills', name, 'SKILL.md'), 'utf8');\n}\n","import type { SizeLimit } from '../adapters/types.js';\n\nexport interface SizeReport {\n label: string;\n warning: string;\n}\n\nexport function checkSize(text: string, limit: SizeLimit | undefined): SizeReport | null {\n if (!limit) return null;\n\n const bytes = Buffer.byteLength(text, 'utf8');\n if (limit.maxBytes !== undefined && bytes > limit.maxBytes) {\n return { label: `${(bytes / 1024).toFixed(1)} KB`, warning: limit.warning };\n }\n\n const lines = text === '' ? 0 : text.split('\\n').length - (text.endsWith('\\n') ? 1 : 0);\n if (limit.maxLines !== undefined && lines > limit.maxLines) {\n return { label: `${lines} lines`, warning: limit.warning };\n }\n\n return null;\n}\n","import { notImplemented } from './not-implemented.js';\n\nexport async function removeCommand(): Promise<void> {\n notImplemented('remove');\n}\n","import { notImplemented } from './not-implemented.js';\n\nexport async function statusCommand(): Promise<void> {\n notImplemented('status');\n}\n","import { notImplemented } from './not-implemented.js';\n\nexport async function updateCommand(): Promise<void> {\n notImplemented('update');\n}\n"],"mappings":"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","names":["join","dirname","join","join","join","dirname","mkdir","readFile","writeFile","dirname","readFile","join","readFile","join","join"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "engly",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Your English coach for AI prompts. One command. Every agent. Always on. Zero cost.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"engly": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"CHANGELOG.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22.12"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"coverage": "vitest run --coverage",
|
|
24
|
+
"lint": "biome check .",
|
|
25
|
+
"lint:fix": "biome check --write .",
|
|
26
|
+
"format": "biome format --write .",
|
|
27
|
+
"check-version": "node scripts/check-version.mjs",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"cli",
|
|
32
|
+
"english",
|
|
33
|
+
"ai",
|
|
34
|
+
"claude-code",
|
|
35
|
+
"codex",
|
|
36
|
+
"opencode",
|
|
37
|
+
"cursor",
|
|
38
|
+
"copilot",
|
|
39
|
+
"agents",
|
|
40
|
+
"prompts",
|
|
41
|
+
"english-learning",
|
|
42
|
+
"language-learning",
|
|
43
|
+
"coach",
|
|
44
|
+
"prompt-engineering",
|
|
45
|
+
"claude",
|
|
46
|
+
"skills",
|
|
47
|
+
"agents-md",
|
|
48
|
+
"ai-agents"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@clack/prompts": "^1.8.1",
|
|
53
|
+
"commander": "^14.0.3"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@biomejs/biome": "2.5.14",
|
|
57
|
+
"@types/node": "^22.20.4",
|
|
58
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
59
|
+
"tsup": "^8.5.1",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^4.1.11"
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/victorcastro/engly.git"
|
|
66
|
+
},
|
|
67
|
+
"homepage": "https://github.com/victorcastro/engly#readme",
|
|
68
|
+
"bugs": {
|
|
69
|
+
"url": "https://github.com/victorcastro/engly/issues"
|
|
70
|
+
},
|
|
71
|
+
"author": {
|
|
72
|
+
"name": "Victor Castro",
|
|
73
|
+
"url": "https://github.com/victorcastro"
|
|
74
|
+
},
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Engly β English coach
|
|
2
|
+
|
|
3
|
+
The user is practicing English while coding. Coach every message the user writes, then do the task.
|
|
4
|
+
|
|
5
|
+
## Profile
|
|
6
|
+
|
|
7
|
+
- Native language: {{language}}
|
|
8
|
+
- English level: {{level}}
|
|
9
|
+
- Adapt corrections to this level and native language. When a value is "auto", infer it from the user's prompts. Point out mistakes typical of the native language when useful.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
1. Only coach text the user typed. Never coach tool results, file contents, system messages or anything else injected into the conversation.
|
|
14
|
+
2. Prompt in another language: show the prompt rewritten in clear, precise English.
|
|
15
|
+
3. Prompt in English with mistakes: {{styleRule}}
|
|
16
|
+
4. {{englishRule}}
|
|
17
|
+
5. {{lengthRule}}
|
|
18
|
+
6. Never ask for confirmation and never block the task.
|
|
19
|
+
7. Then do the task as if the user had sent the improved prompt.
|
|
20
|
+
8. If the user says "engly off", stop coaching until they say "engly on" or a new session starts.
|
|
21
|
+
|
|
22
|
+
## Format
|
|
23
|
+
|
|
24
|
+
Put the note before your answer:
|
|
25
|
+
|
|
26
|
+
β π£οΈ Engly
|
|
27
|
+
β β’ "wrong" β "right" (short reason)
|
|
28
|
+
β Better: "Improved prompt."
|
|
29
|
+
β
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: engly-off
|
|
3
|
+
description: Pause the Engly English coach for the rest of this session. Use when the user says "engly off", asks to stop being corrected, or is in a hurry.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Engly off
|
|
7
|
+
|
|
8
|
+
Stop adding Engly notes and stop correcting the user's English for the rest of this session.
|
|
9
|
+
|
|
10
|
+
Reply with exactly:
|
|
11
|
+
|
|
12
|
+
> Engly paused for this session. Say "engly on" to resume.
|
|
13
|
+
|
|
14
|
+
Then keep working normally. Coaching resumes only when the user says "engly on", runs `engly-on`, or starts a new session.
|
|
15
|
+
|
|
16
|
+
This only affects the current session. To disable Engly permanently, the user can run `engly disable` in the terminal.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: engly-on
|
|
3
|
+
description: Resume the Engly English coach in this session after it was paused. Use when the user says "engly on" or asks to be corrected again.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Engly on
|
|
7
|
+
|
|
8
|
+
Resume coaching the user's prompts with the Engly rules from the project instructions, starting with the next message.
|
|
9
|
+
|
|
10
|
+
Reply with exactly:
|
|
11
|
+
|
|
12
|
+
> Engly resumed. Your next prompt will be coached.
|
|
13
|
+
|
|
14
|
+
If Engly is disabled in the project (no Engly instructions are loaded), tell the user to run `engly enable` in the terminal instead.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: engly-review
|
|
3
|
+
description: Detailed English review of a prompt written by the user, listing every mistake with an explanation and alternative phrasings. Use when the user runs engly-review or asks for a detailed review of a prompt.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Engly review
|
|
7
|
+
|
|
8
|
+
Review the text the user passed to this skill (or, if none, their previous prompt). This is a study session: do not execute the prompt as a task.
|
|
9
|
+
|
|
10
|
+
1. List every mistake, numbered: "wrong" β "right", followed by a one-line explanation. When the mistake comes from the user's native language, say so (for example, Spanish "quiero que" is not "want that").
|
|
11
|
+
2. Give an improved version of the whole prompt.
|
|
12
|
+
3. Give one more formal or more natural alternative.
|
|
13
|
+
4. If the prompt has no mistakes, say so and still offer a more natural or more precise alternative if there is one.
|
|
14
|
+
|
|
15
|
+
Format:
|
|
16
|
+
|
|
17
|
+
β π£οΈ Engly β detailed review
|
|
18
|
+
β 1. "wrong" β "right"
|
|
19
|
+
β Explanation.
|
|
20
|
+
β
|
|
21
|
+
β Improved: "..."
|
|
22
|
+
β More formal: "..."
|
|
23
|
+
β
|