govuk-rewrite 0.1.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/README.md +252 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +207 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +10 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +124 -0
- package/dist/io.js.map +1 -0
- package/dist/prompt.d.ts +41 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +89 -0
- package/dist/prompt.js.map +1 -0
- package/dist/providers/anthropic.d.ts +3 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +77 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/openai.d.ts +3 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +62 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +3 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +64 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# govuk-rewrite
|
|
2
|
+
|
|
3
|
+
A tiny CLI that rewrites text into GOV.UK-style content.
|
|
4
|
+
|
|
5
|
+
- **Default output:** rewritten text only (stdout)
|
|
6
|
+
- **Optional:** short "why this is better" explanation (`--explain`)
|
|
7
|
+
- Works with **stdin**, pipes, files, or a single string argument
|
|
8
|
+
- Minimal, composable, script-friendly
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
### Global (recommended)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g govuk-rewrite
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Run without installing
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx govuk-rewrite "Text to rewrite"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Basic
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
govuk-rewrite "Text to rewrite"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### With explanation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
govuk-rewrite "Text to rewrite" --explain
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### With a diff of changes
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
govuk-rewrite "Text to rewrite" --diff
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Audit for style issues without rewriting
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
govuk-rewrite --check "Thank you for your kind submission"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Provide context about your service or audience
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
govuk-rewrite --context "HMRC self-assessment portal" "Please fill in the form below"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Specify a content type
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
govuk-rewrite --mode error-message "Sorry, something went wrong. Please try again."
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Valid modes: `page-body` (default) | `error-message` | `hint-text` | `notification` | `button`
|
|
63
|
+
|
|
64
|
+
### From clipboard / pipe
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pbpaste | govuk-rewrite --explain
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### From a file
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
govuk-rewrite < draft.txt
|
|
74
|
+
govuk-rewrite --explain < draft.txt
|
|
75
|
+
cat draft.txt | govuk-rewrite > rewritten.txt
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### JSON output (for automation)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
govuk-rewrite "Text" --json
|
|
82
|
+
govuk-rewrite --json --explain < draft.txt
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Output formats
|
|
86
|
+
|
|
87
|
+
### Plain (default)
|
|
88
|
+
|
|
89
|
+
Prints only the rewritten text to stdout.
|
|
90
|
+
|
|
91
|
+
### `--explain`
|
|
92
|
+
|
|
93
|
+
Prints the rewritten text, then a separator, then short bullet points explaining the changes.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
The service will be unavailable on Friday from 6pm to 8pm.
|
|
99
|
+
|
|
100
|
+
--- why this is better ---
|
|
101
|
+
- Uses active voice and clear actions.
|
|
102
|
+
- Removes unnecessary politeness and jargon.
|
|
103
|
+
- Clarifies who needs to do what by when.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `--diff`
|
|
107
|
+
|
|
108
|
+
Prints the rewritten text, then a line-by-line diff against the original input.
|
|
109
|
+
|
|
110
|
+
### `--check`
|
|
111
|
+
|
|
112
|
+
Audits the text for GOV.UK style issues without rewriting it. Lists any problems found, or reports `No issues found.`
|
|
113
|
+
|
|
114
|
+
### `--json`
|
|
115
|
+
|
|
116
|
+
Returns machine-readable output:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"rewrittenText": "…",
|
|
121
|
+
"explanation": ["…", "…"],
|
|
122
|
+
"issues": ["…"],
|
|
123
|
+
"provider": "openai",
|
|
124
|
+
"model": "gpt-4.1-mini"
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Clipboard
|
|
129
|
+
|
|
130
|
+
When running interactively (TTY), the rewritten text is automatically copied to the clipboard after a successful rewrite. Pass `--no-copy` to disable this.
|
|
131
|
+
|
|
132
|
+
## Providers, models, and configuration
|
|
133
|
+
|
|
134
|
+
This tool supports multiple LLM providers via a small provider adapter layer.
|
|
135
|
+
|
|
136
|
+
### Quick setup (environment variables)
|
|
137
|
+
|
|
138
|
+
Pick one provider:
|
|
139
|
+
|
|
140
|
+
**OpenAI** (default)
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
export GOVUK_REWRITE_PROVIDER=openai
|
|
144
|
+
export OPENAI_API_KEY="your_key"
|
|
145
|
+
export GOVUK_REWRITE_MODEL="gpt-4.1-mini"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Anthropic**
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
export GOVUK_REWRITE_PROVIDER=anthropic
|
|
152
|
+
export ANTHROPIC_API_KEY="your_key"
|
|
153
|
+
export GOVUK_REWRITE_MODEL="claude-3-5-sonnet-latest"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**OpenRouter**
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
export GOVUK_REWRITE_PROVIDER=openrouter
|
|
160
|
+
export OPENROUTER_API_KEY="your_key"
|
|
161
|
+
export GOVUK_REWRITE_MODEL="openai/gpt-4.1-mini"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Keys are read from standard env vars (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENROUTER_API_KEY`) plus `GOVUK_REWRITE_*` overrides.
|
|
165
|
+
|
|
166
|
+
### Config file
|
|
167
|
+
|
|
168
|
+
You can also use a config file:
|
|
169
|
+
|
|
170
|
+
- **macOS/Linux:** `~/.config/govuk-rewrite/config.json`
|
|
171
|
+
- **Windows:** `%APPDATA%\govuk-rewrite\config.json`
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"provider": "openai",
|
|
178
|
+
"model": "gpt-4.1-mini",
|
|
179
|
+
"timeoutMs": 30000,
|
|
180
|
+
"baseUrl": null
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Configuration precedence
|
|
185
|
+
|
|
186
|
+
CLI flags → environment variables → config file → defaults (highest to lowest).
|
|
187
|
+
|
|
188
|
+
### Default models
|
|
189
|
+
|
|
190
|
+
| Provider | Default model |
|
|
191
|
+
|-------------|------------------------------|
|
|
192
|
+
| openai | `gpt-4.1-mini` |
|
|
193
|
+
| anthropic | `claude-3-5-sonnet-latest` |
|
|
194
|
+
| openrouter | `openai/gpt-4.1-mini` |
|
|
195
|
+
|
|
196
|
+
## CLI flags
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
govuk-rewrite [text...]
|
|
200
|
+
|
|
201
|
+
Options:
|
|
202
|
+
--explain Include a short explanation of changes
|
|
203
|
+
--diff Show a line diff between original and rewritten text
|
|
204
|
+
--check Audit text for GOV.UK style issues without rewriting
|
|
205
|
+
--json Output JSON
|
|
206
|
+
--context <text> Service or audience context to inform the rewrite
|
|
207
|
+
--mode <type> Content type: page-body | error-message | hint-text | notification | button
|
|
208
|
+
--provider <name> openai | anthropic | openrouter
|
|
209
|
+
--model <name> Model name for the chosen provider
|
|
210
|
+
--config <path> Path to a config.json file
|
|
211
|
+
--timeout <ms> Request timeout in milliseconds (default: 30000)
|
|
212
|
+
--no-spinner Disable spinner (shown only on TTY by default)
|
|
213
|
+
--no-copy Do not auto-copy result to clipboard
|
|
214
|
+
-v, --version Show version
|
|
215
|
+
-h, --help Show help
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## UX details
|
|
219
|
+
|
|
220
|
+
- The spinner is shown only when stdout is a TTY — it will not pollute piped output.
|
|
221
|
+
- Errors go to stderr; rewritten content goes to stdout.
|
|
222
|
+
- If input is empty, exits with code 2 (usage error).
|
|
223
|
+
- Rewritten text is copied to the clipboard automatically when running interactively (pass `--no-copy` to disable).
|
|
224
|
+
|
|
225
|
+
## Exit codes
|
|
226
|
+
|
|
227
|
+
| Code | Meaning |
|
|
228
|
+
|------|------------------------------------|
|
|
229
|
+
| 0 | Success |
|
|
230
|
+
| 1 | Runtime or provider error |
|
|
231
|
+
| 2 | Usage error (no input, bad flags) |
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
git clone <repo>
|
|
237
|
+
cd govuk-rewrite
|
|
238
|
+
npm install
|
|
239
|
+
npm run build
|
|
240
|
+
npm link
|
|
241
|
+
govuk-rewrite "Test input"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Run tests:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
npm test
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Licence
|
|
251
|
+
|
|
252
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Config } from "./types.js";
|
|
2
|
+
export interface CliOverrides {
|
|
3
|
+
provider?: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
config?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveConfig(cliOverrides?: CliOverrides): Config;
|
|
9
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,YAAY,CAAC;AAoDnD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,YAAY,GAAE,YAAiB,GAAG,MAAM,CA0BrE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
provider: "openai",
|
|
6
|
+
model: "gpt-4.1-mini",
|
|
7
|
+
timeoutMs: 30000,
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_MODELS = {
|
|
10
|
+
openai: "gpt-4.1-mini",
|
|
11
|
+
anthropic: "claude-3-5-sonnet-latest",
|
|
12
|
+
openrouter: "openai/gpt-4.1-mini",
|
|
13
|
+
};
|
|
14
|
+
function configFilePath() {
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
return join(process.env["APPDATA"] ?? homedir(), "govuk-rewrite", "config.json");
|
|
17
|
+
}
|
|
18
|
+
return join(homedir(), ".config", "govuk-rewrite", "config.json");
|
|
19
|
+
}
|
|
20
|
+
function loadConfigFile(customPath) {
|
|
21
|
+
const filePath = customPath ?? configFilePath();
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(filePath, "utf8");
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function loadEnvVars() {
|
|
31
|
+
const result = {};
|
|
32
|
+
const provider = process.env["GOVUK_REWRITE_PROVIDER"];
|
|
33
|
+
if (provider)
|
|
34
|
+
result.provider = provider;
|
|
35
|
+
const model = process.env["GOVUK_REWRITE_MODEL"];
|
|
36
|
+
if (model)
|
|
37
|
+
result.model = model;
|
|
38
|
+
const timeoutMs = process.env["GOVUK_REWRITE_TIMEOUT_MS"];
|
|
39
|
+
if (timeoutMs) {
|
|
40
|
+
const parsed = parseInt(timeoutMs, 10);
|
|
41
|
+
if (!isNaN(parsed))
|
|
42
|
+
result.timeoutMs = parsed;
|
|
43
|
+
}
|
|
44
|
+
const baseUrl = process.env["GOVUK_REWRITE_BASE_URL"];
|
|
45
|
+
if (baseUrl)
|
|
46
|
+
result.baseUrl = baseUrl;
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
export function resolveConfig(cliOverrides = {}) {
|
|
50
|
+
const fileConfig = loadConfigFile(cliOverrides.config);
|
|
51
|
+
const envConfig = loadEnvVars();
|
|
52
|
+
const merged = {
|
|
53
|
+
...DEFAULTS,
|
|
54
|
+
...fileConfig,
|
|
55
|
+
...envConfig,
|
|
56
|
+
};
|
|
57
|
+
if (cliOverrides.provider)
|
|
58
|
+
merged.provider = cliOverrides.provider;
|
|
59
|
+
if (cliOverrides.model)
|
|
60
|
+
merged.model = cliOverrides.model;
|
|
61
|
+
if (cliOverrides.timeout)
|
|
62
|
+
merged.timeoutMs = cliOverrides.timeout;
|
|
63
|
+
// If model was not explicitly set at any level, use provider-appropriate default
|
|
64
|
+
const modelExplicitlySet = fileConfig.model ??
|
|
65
|
+
envConfig.model ??
|
|
66
|
+
cliOverrides.model;
|
|
67
|
+
if (!modelExplicitlySet) {
|
|
68
|
+
merged.model = DEFAULT_MODELS[merged.provider] ?? DEFAULTS.model;
|
|
69
|
+
}
|
|
70
|
+
merged.apiKey = resolveApiKey(merged.provider);
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
73
|
+
function resolveApiKey(provider) {
|
|
74
|
+
const keyMap = {
|
|
75
|
+
openai: "OPENAI_API_KEY",
|
|
76
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
77
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
78
|
+
};
|
|
79
|
+
return process.env[keyMap[provider]];
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { resolveConfig } from "./config.js";
|
|
8
|
+
import { readStdin, isStdinPiped, formatOutput, writeClipboard } from "./io.js";
|
|
9
|
+
import * as openai from "./providers/openai.js";
|
|
10
|
+
import * as anthropic from "./providers/anthropic.js";
|
|
11
|
+
import * as openrouter from "./providers/openrouter.js";
|
|
12
|
+
const VALID_MODES = [
|
|
13
|
+
"page-body",
|
|
14
|
+
"error-message",
|
|
15
|
+
"hint-text",
|
|
16
|
+
"notification",
|
|
17
|
+
"button",
|
|
18
|
+
];
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
function getVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
23
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
24
|
+
return pkg.version;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return "0.0.0";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
program.configureOutput({ outputError: () => { } });
|
|
31
|
+
program.exitOverride((err) => {
|
|
32
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
// Strip the leading "error: " that commander adds so we can normalise capitalisation
|
|
36
|
+
const msg = err.message.replace(/^error:\s*/i, "");
|
|
37
|
+
if (err.code === "commander.unknownOption") {
|
|
38
|
+
process.stderr.write(`Error: ${msg}\n\nRun 'govuk-rewrite --help' for a list of valid options.\n`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
process.stderr.write(`Error: ${msg}\n\nRun 'govuk-rewrite --help' for usage information.\n`);
|
|
42
|
+
}
|
|
43
|
+
process.exit(2);
|
|
44
|
+
});
|
|
45
|
+
program
|
|
46
|
+
.name("govuk-rewrite")
|
|
47
|
+
.description("Rewrite text into GOV.UK-style content")
|
|
48
|
+
.version(getVersion(), "-v, --version")
|
|
49
|
+
.argument("[text...]", "Text to rewrite (or pipe via stdin)")
|
|
50
|
+
.option("--explain", "Include a short explanation of changes")
|
|
51
|
+
.option("--diff", "Show a line diff between original and rewritten text")
|
|
52
|
+
.option("--check", "Audit text for GOV.UK style issues without rewriting")
|
|
53
|
+
.option("--json", "Output JSON")
|
|
54
|
+
.option("--context <text>", "Service or audience context to inform the rewrite")
|
|
55
|
+
.option("--mode <type>", `Content type: ${VALID_MODES.join(" | ")} (default: page-body)`)
|
|
56
|
+
.option("--provider <name>", "Provider: openai | anthropic | openrouter")
|
|
57
|
+
.option("--model <name>", "Model name for the chosen provider")
|
|
58
|
+
.option("--config <path>", "Path to a config.json file")
|
|
59
|
+
.option("--timeout <ms>", "Request timeout in milliseconds", parseInt)
|
|
60
|
+
.option("--no-spinner", "Disable spinner")
|
|
61
|
+
.option("--no-copy", "Do not auto-copy result to clipboard")
|
|
62
|
+
.addHelpText("after", `
|
|
63
|
+
Examples:
|
|
64
|
+
$ govuk-rewrite "Please be advised that the service will be unavailable"
|
|
65
|
+
$ echo "Click here to find out more" | govuk-rewrite
|
|
66
|
+
$ govuk-rewrite --explain "Please note that you must submit your form by Friday"
|
|
67
|
+
$ govuk-rewrite --mode error-message "Sorry, something went wrong. Please try again."
|
|
68
|
+
$ govuk-rewrite --check "Thank you for your kind submission"
|
|
69
|
+
$ govuk-rewrite --diff --context "HMRC self-assessment" "Please ensure you complete the form"
|
|
70
|
+
$ govuk-rewrite --provider anthropic "Your application has been received"
|
|
71
|
+
|
|
72
|
+
Environment variables:
|
|
73
|
+
OPENAI_API_KEY API key for OpenAI (default provider)
|
|
74
|
+
ANTHROPIC_API_KEY API key for Anthropic
|
|
75
|
+
OPENROUTER_API_KEY API key for OpenRouter
|
|
76
|
+
GOVUK_REWRITE_PROVIDER Default provider (openai | anthropic | openrouter)
|
|
77
|
+
GOVUK_REWRITE_MODEL Default model name
|
|
78
|
+
GOVUK_REWRITE_TIMEOUT_MS Request timeout override in milliseconds`)
|
|
79
|
+
.action(async (textArgs, opts) => {
|
|
80
|
+
let inputText = "";
|
|
81
|
+
if (isStdinPiped()) {
|
|
82
|
+
inputText = await readStdin();
|
|
83
|
+
}
|
|
84
|
+
else if (textArgs.length > 0) {
|
|
85
|
+
inputText = textArgs.join(" ");
|
|
86
|
+
}
|
|
87
|
+
if (!inputText) {
|
|
88
|
+
process.stderr.write(program.helpInformation());
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
91
|
+
if (opts.mode && !VALID_MODES.includes(opts.mode)) {
|
|
92
|
+
process.stderr.write(`Error: invalid --mode "${opts.mode}". Valid values: ${VALID_MODES.join(", ")}\n`);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
const config = resolveConfig({
|
|
96
|
+
provider: opts.provider,
|
|
97
|
+
model: opts.model,
|
|
98
|
+
timeout: opts.timeout,
|
|
99
|
+
config: opts.config,
|
|
100
|
+
});
|
|
101
|
+
if (!config.apiKey) {
|
|
102
|
+
const KEY_INFO = {
|
|
103
|
+
openai: {
|
|
104
|
+
envVar: "OPENAI_API_KEY",
|
|
105
|
+
link: "https://platform.openai.com/api-keys",
|
|
106
|
+
},
|
|
107
|
+
anthropic: {
|
|
108
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
109
|
+
link: "https://console.anthropic.com/settings/keys",
|
|
110
|
+
},
|
|
111
|
+
openrouter: {
|
|
112
|
+
envVar: "OPENROUTER_API_KEY",
|
|
113
|
+
link: "https://openrouter.ai/keys",
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const info = KEY_INFO[config.provider];
|
|
117
|
+
const envVar = info?.envVar ?? "YOUR_API_KEY";
|
|
118
|
+
const lines = [
|
|
119
|
+
`Error: no API key found for provider "${config.provider}".`,
|
|
120
|
+
"",
|
|
121
|
+
`Set the ${envVar} environment variable:`,
|
|
122
|
+
` export ${envVar}=your-key-here`,
|
|
123
|
+
"",
|
|
124
|
+
];
|
|
125
|
+
if (info?.link) {
|
|
126
|
+
lines.push(`Get a key at: ${info.link}`, "");
|
|
127
|
+
}
|
|
128
|
+
lines.push(`Using a different provider? Pass --provider openai | anthropic | openrouter`, "");
|
|
129
|
+
process.stderr.write(lines.join("\n"));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const providerOptions = {
|
|
133
|
+
apiKey: config.apiKey,
|
|
134
|
+
model: config.model,
|
|
135
|
+
baseUrl: config.baseUrl,
|
|
136
|
+
timeoutMs: config.timeoutMs,
|
|
137
|
+
};
|
|
138
|
+
const useSpinner = opts.spinner && process.stdout.isTTY === true;
|
|
139
|
+
const spinner = useSpinner ? ora("Rewriting…").start() : null;
|
|
140
|
+
try {
|
|
141
|
+
let result;
|
|
142
|
+
const request = {
|
|
143
|
+
text: inputText,
|
|
144
|
+
explain: opts.explain ?? false,
|
|
145
|
+
check: opts.check ?? false,
|
|
146
|
+
context: opts.context,
|
|
147
|
+
mode: opts.mode ?? "page-body",
|
|
148
|
+
};
|
|
149
|
+
switch (config.provider) {
|
|
150
|
+
case "openai":
|
|
151
|
+
result = await openai.rewrite(providerOptions, request);
|
|
152
|
+
break;
|
|
153
|
+
case "anthropic":
|
|
154
|
+
result = await anthropic.rewrite(providerOptions, request);
|
|
155
|
+
break;
|
|
156
|
+
case "openrouter":
|
|
157
|
+
result = await openrouter.rewrite(providerOptions, request);
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
throw new Error(`Unknown provider: ${config.provider}`);
|
|
161
|
+
}
|
|
162
|
+
spinner?.stop();
|
|
163
|
+
let format;
|
|
164
|
+
if (opts.json) {
|
|
165
|
+
format = "json";
|
|
166
|
+
}
|
|
167
|
+
else if (opts.check) {
|
|
168
|
+
format = "check";
|
|
169
|
+
}
|
|
170
|
+
else if (opts.diff) {
|
|
171
|
+
format = "diff";
|
|
172
|
+
}
|
|
173
|
+
else if (opts.explain) {
|
|
174
|
+
format = "explain";
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
format = "plain";
|
|
178
|
+
}
|
|
179
|
+
const output = formatOutput({
|
|
180
|
+
result,
|
|
181
|
+
format,
|
|
182
|
+
provider: config.provider,
|
|
183
|
+
model: config.model,
|
|
184
|
+
originalText: inputText,
|
|
185
|
+
});
|
|
186
|
+
process.stdout.write(output + "\n");
|
|
187
|
+
// Auto-copy rewritten text to clipboard when running interactively
|
|
188
|
+
if (process.stdout.isTTY && opts.copy && !opts.check) {
|
|
189
|
+
const copied = writeClipboard(result.rewrittenText);
|
|
190
|
+
if (copied) {
|
|
191
|
+
process.stderr.write("(copied to clipboard)\n");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
spinner?.fail();
|
|
197
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
198
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
204
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
207
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/io.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OutputOptions } from "./types.js";
|
|
2
|
+
export declare function readStdin(): Promise<string>;
|
|
3
|
+
export declare function isStdinPiped(): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Write text to the system clipboard. Returns true on success, false if
|
|
6
|
+
* no clipboard command is available (e.g. Linux without xclip).
|
|
7
|
+
*/
|
|
8
|
+
export declare function writeClipboard(text: string): boolean;
|
|
9
|
+
export declare function formatOutput(options: OutputOptions): string;
|
|
10
|
+
//# sourceMappingURL=io.d.ts.map
|
package/dist/io.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../src/io.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAUjD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CA8BpD;AA8CD,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAuC3D"}
|
package/dist/io.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
export async function readStdin() {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
let data = "";
|
|
5
|
+
process.stdin.setEncoding("utf8");
|
|
6
|
+
process.stdin.on("data", (chunk) => {
|
|
7
|
+
data += chunk;
|
|
8
|
+
});
|
|
9
|
+
process.stdin.on("end", () => resolve(data.trim()));
|
|
10
|
+
process.stdin.on("error", reject);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function isStdinPiped() {
|
|
14
|
+
return !process.stdin.isTTY;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Write text to the system clipboard. Returns true on success, false if
|
|
18
|
+
* no clipboard command is available (e.g. Linux without xclip).
|
|
19
|
+
*/
|
|
20
|
+
export function writeClipboard(text) {
|
|
21
|
+
try {
|
|
22
|
+
if (process.platform === "darwin") {
|
|
23
|
+
execSync("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (process.platform === "win32") {
|
|
27
|
+
execSync("powershell -command Set-Clipboard -Value $input", {
|
|
28
|
+
input: text,
|
|
29
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// Linux — try xclip, fall back to xsel
|
|
34
|
+
try {
|
|
35
|
+
execSync("xclip -selection clipboard", {
|
|
36
|
+
input: text,
|
|
37
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
execSync("xsel --clipboard --input", {
|
|
43
|
+
input: text,
|
|
44
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compute a simple LCS-based line diff between two strings.
|
|
55
|
+
* Returns a string with lines prefixed by ' ' (same), '-' (removed), '+' (added).
|
|
56
|
+
*/
|
|
57
|
+
function diffLines(original, rewritten) {
|
|
58
|
+
const a = original.split("\n");
|
|
59
|
+
const b = rewritten.split("\n");
|
|
60
|
+
const m = a.length;
|
|
61
|
+
const n = b.length;
|
|
62
|
+
// Build LCS table
|
|
63
|
+
const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
64
|
+
for (let i = 1; i <= m; i++) {
|
|
65
|
+
for (let j = 1; j <= n; j++) {
|
|
66
|
+
lcs[i][j] =
|
|
67
|
+
a[i - 1] === b[j - 1]
|
|
68
|
+
? lcs[i - 1][j - 1] + 1
|
|
69
|
+
: Math.max(lcs[i - 1][j], lcs[i][j - 1]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Backtrack
|
|
73
|
+
const lines = [];
|
|
74
|
+
let i = m;
|
|
75
|
+
let j = n;
|
|
76
|
+
while (i > 0 || j > 0) {
|
|
77
|
+
if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
|
|
78
|
+
lines.unshift(` ${a[i - 1]}`);
|
|
79
|
+
i--;
|
|
80
|
+
j--;
|
|
81
|
+
}
|
|
82
|
+
else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
83
|
+
lines.unshift(`+ ${b[j - 1]}`);
|
|
84
|
+
j--;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
lines.unshift(`- ${a[i - 1]}`);
|
|
88
|
+
i--;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
export function formatOutput(options) {
|
|
94
|
+
const { result, format, provider, model, originalText } = options;
|
|
95
|
+
if (format === "json") {
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
rewrittenText: result.rewrittenText,
|
|
98
|
+
explanation: result.explanation ?? [],
|
|
99
|
+
issues: result.issues ?? [],
|
|
100
|
+
provider,
|
|
101
|
+
model,
|
|
102
|
+
}, null, 2);
|
|
103
|
+
}
|
|
104
|
+
if (format === "check") {
|
|
105
|
+
const issues = result.issues ?? [];
|
|
106
|
+
if (issues.length === 0) {
|
|
107
|
+
return "No issues found.";
|
|
108
|
+
}
|
|
109
|
+
return `Issues found:\n${issues.map((i) => `- ${i}`).join("\n")}`;
|
|
110
|
+
}
|
|
111
|
+
if (format === "diff") {
|
|
112
|
+
const original = originalText ?? "";
|
|
113
|
+
const diff = diffLines(original, result.rewrittenText);
|
|
114
|
+
return `${result.rewrittenText}\n\n--- diff ---\n${diff}`;
|
|
115
|
+
}
|
|
116
|
+
if (format === "explain") {
|
|
117
|
+
const bullets = (result.explanation ?? [])
|
|
118
|
+
.map((point) => `- ${point}`)
|
|
119
|
+
.join("\n");
|
|
120
|
+
return `${result.rewrittenText}\n\n--- why this is better ---\n${bullets}`;
|
|
121
|
+
}
|
|
122
|
+
return result.rewrittenText;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=io.js.map
|
package/dist/io.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io.js","sourceRoot":"","sources":["../src/io.ts"],"names":[],"mappings":"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"}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ContentMode } from "./types.js";
|
|
2
|
+
export declare const SYSTEM_PROMPT = "You are an expert GOV.UK content editor. Rewrite the user's text to meet GOV.UK content design standards.\n\nRules:\n- Use active voice throughout.\n- Write in plain English. Use short sentences (no more than 25 words where possible).\n- State actions and deadlines clearly. Make it obvious who needs to do what, and by when.\n- Remove \"please\", \"kindly\", \"we would like to\", and other filler phrases unless they are genuinely required.\n- Avoid jargon. If a domain term is necessary, keep it consistent.\n- Never invent facts or change the meaning of the source text.\n- If part of the input is ambiguous, keep the wording cautious rather than guessing.\n- Do not add headings, bullet points, or structure that was not present in the original, unless clearly implied.\n- Remove unnecessary capital letters.\n- Spell out abbreviations on first use if they may be unfamiliar.\n\nRespond only with valid JSON matching the schema provided. No preamble, no markdown fences.";
|
|
3
|
+
export declare const CHECK_SYSTEM_PROMPT = "You are an expert GOV.UK content auditor. Analyse the user's text and identify specific issues that do not meet GOV.UK content design standards. Do not rewrite the text.\n\nCheck for:\n- Passive voice\n- Sentences over 25 words\n- Filler phrases (\"please\", \"kindly\", \"we would like to\", \"please be advised\", etc.)\n- Jargon or unnecessarily complex words\n- Unclear actions or missing deadlines\n- Unnecessary capital letters\n- Unexplained abbreviations\n\nReturn `rewrittenText` as an empty string. List each issue concisely in the `issues` array. If no issues are found, return an empty `issues` array.\n\nRespond only with valid JSON matching the schema provided. No preamble, no markdown fences.";
|
|
4
|
+
export interface ResponseSchema {
|
|
5
|
+
rewrittenText: string;
|
|
6
|
+
explanation?: string[];
|
|
7
|
+
issues?: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare const JSON_SCHEMA: {
|
|
10
|
+
name: string;
|
|
11
|
+
strict: boolean;
|
|
12
|
+
schema: {
|
|
13
|
+
type: string;
|
|
14
|
+
properties: {
|
|
15
|
+
rewrittenText: {
|
|
16
|
+
type: string;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
19
|
+
explanation: {
|
|
20
|
+
type: string;
|
|
21
|
+
items: {
|
|
22
|
+
type: string;
|
|
23
|
+
};
|
|
24
|
+
description: string;
|
|
25
|
+
};
|
|
26
|
+
issues: {
|
|
27
|
+
type: string;
|
|
28
|
+
items: {
|
|
29
|
+
type: string;
|
|
30
|
+
};
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
required: string[];
|
|
35
|
+
additionalProperties: boolean;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export declare function buildSystemPrompt(mode?: ContentMode): string;
|
|
39
|
+
export declare function buildUserMessage(text: string, explain: boolean, context?: string, mode?: ContentMode): string;
|
|
40
|
+
export declare function buildCheckMessage(text: string, context?: string, mode?: ContentMode): string;
|
|
41
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,aAAa,o9BAckE,CAAC;AAE7F,eAAO,MAAM,mBAAmB,ysBAa4D,CAAC;AAc7F,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BvB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAI5D;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,WAAW,GACjB,MAAM,CAmBR;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,WAAW,GACjB,MAAM,CAeR"}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const SYSTEM_PROMPT = `You are an expert GOV.UK content editor. Rewrite the user's text to meet GOV.UK content design standards.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- Use active voice throughout.
|
|
5
|
+
- Write in plain English. Use short sentences (no more than 25 words where possible).
|
|
6
|
+
- State actions and deadlines clearly. Make it obvious who needs to do what, and by when.
|
|
7
|
+
- Remove "please", "kindly", "we would like to", and other filler phrases unless they are genuinely required.
|
|
8
|
+
- Avoid jargon. If a domain term is necessary, keep it consistent.
|
|
9
|
+
- Never invent facts or change the meaning of the source text.
|
|
10
|
+
- If part of the input is ambiguous, keep the wording cautious rather than guessing.
|
|
11
|
+
- Do not add headings, bullet points, or structure that was not present in the original, unless clearly implied.
|
|
12
|
+
- Remove unnecessary capital letters.
|
|
13
|
+
- Spell out abbreviations on first use if they may be unfamiliar.
|
|
14
|
+
|
|
15
|
+
Respond only with valid JSON matching the schema provided. No preamble, no markdown fences.`;
|
|
16
|
+
export const CHECK_SYSTEM_PROMPT = `You are an expert GOV.UK content auditor. Analyse the user's text and identify specific issues that do not meet GOV.UK content design standards. Do not rewrite the text.
|
|
17
|
+
|
|
18
|
+
Check for:
|
|
19
|
+
- Passive voice
|
|
20
|
+
- Sentences over 25 words
|
|
21
|
+
- Filler phrases ("please", "kindly", "we would like to", "please be advised", etc.)
|
|
22
|
+
- Jargon or unnecessarily complex words
|
|
23
|
+
- Unclear actions or missing deadlines
|
|
24
|
+
- Unnecessary capital letters
|
|
25
|
+
- Unexplained abbreviations
|
|
26
|
+
|
|
27
|
+
Return \`rewrittenText\` as an empty string. List each issue concisely in the \`issues\` array. If no issues are found, return an empty \`issues\` array.
|
|
28
|
+
|
|
29
|
+
Respond only with valid JSON matching the schema provided. No preamble, no markdown fences.`;
|
|
30
|
+
const MODE_PROMPTS = {
|
|
31
|
+
"page-body": "",
|
|
32
|
+
"error-message": "The text is a GOV.UK error message. Additional rules: start with 'Enter a…', 'Select a…', or 'Enter your…'. Use present tense. Maximum one sentence. Never start with 'Please'. Do not use 'must' or 'should'.",
|
|
33
|
+
"hint-text": "The text is GOV.UK hint text displayed below a form label. Additional rules: keep it short (1–2 sentences). Do not repeat the label. Do not end with punctuation unless it is a full sentence.",
|
|
34
|
+
notification: "The text is a GOV.UK Notify notification (email or SMS). Additional rules: plain text only — no markdown, no HTML. Preserve any ((variable)) placeholders exactly as written. For SMS, aim to keep the total under 160 characters.",
|
|
35
|
+
button: "The text is a GOV.UK button label. Additional rules: short imperative verb phrase (2–4 words). No punctuation. Start with a capital letter. Examples: 'Continue', 'Save and continue', 'Submit application'.",
|
|
36
|
+
};
|
|
37
|
+
export const JSON_SCHEMA = {
|
|
38
|
+
name: "rewrite_result",
|
|
39
|
+
strict: true,
|
|
40
|
+
schema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
rewrittenText: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "The rewritten text in GOV.UK style. Empty string when in check mode.",
|
|
46
|
+
},
|
|
47
|
+
explanation: {
|
|
48
|
+
type: "array",
|
|
49
|
+
items: { type: "string" },
|
|
50
|
+
description: "Short bullet points explaining what was changed and why. Only include when requested.",
|
|
51
|
+
},
|
|
52
|
+
issues: {
|
|
53
|
+
type: "array",
|
|
54
|
+
items: { type: "string" },
|
|
55
|
+
description: "GOV.UK style issues found in the original text. Only populate in check mode.",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ["rewrittenText", "explanation", "issues"],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
export function buildSystemPrompt(mode) {
|
|
63
|
+
if (!mode || mode === "page-body")
|
|
64
|
+
return SYSTEM_PROMPT;
|
|
65
|
+
const modeAddendum = MODE_PROMPTS[mode];
|
|
66
|
+
return modeAddendum ? `${SYSTEM_PROMPT}\n\n${modeAddendum}` : SYSTEM_PROMPT;
|
|
67
|
+
}
|
|
68
|
+
export function buildUserMessage(text, explain, context, mode) {
|
|
69
|
+
const parts = [];
|
|
70
|
+
if (context?.trim()) {
|
|
71
|
+
parts.push(`Service context: ${context.trim()}`);
|
|
72
|
+
}
|
|
73
|
+
const explainInstruction = explain
|
|
74
|
+
? " Also provide a brief explanation (3–6 bullet points) of the key changes made and why they improve the content."
|
|
75
|
+
: " Do not include an explanation; return an empty array for the explanation field.";
|
|
76
|
+
const modeNote = mode && mode !== "page-body" ? ` Treat it as ${mode} content.` : "";
|
|
77
|
+
parts.push(`Rewrite the following text into GOV.UK style.${modeNote}${explainInstruction} Return an empty array for the issues field.\n\n${text}`);
|
|
78
|
+
return parts.join("\n\n");
|
|
79
|
+
}
|
|
80
|
+
export function buildCheckMessage(text, context, mode) {
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (context?.trim()) {
|
|
83
|
+
parts.push(`Service context: ${context.trim()}`);
|
|
84
|
+
}
|
|
85
|
+
const modeNote = mode && mode !== "page-body" ? ` Treat it as ${mode} content.` : "";
|
|
86
|
+
parts.push(`Audit the following text for GOV.UK style issues.${modeNote} Return an empty array for the explanation field.\n\n${text}`);
|
|
87
|
+
return parts.join("\n\n");
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;4FAc+D,CAAC;AAE7F,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;4FAayD,CAAC;AAE7F,MAAM,YAAY,GAAgC;IAChD,WAAW,EAAE,EAAE;IACf,eAAe,EACb,gNAAgN;IAClN,WAAW,EACT,gMAAgM;IAClM,YAAY,EACV,oOAAoO;IACtO,MAAM,EACJ,8MAA8M;CACjN,CAAC;AAQF,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,gBAAgB;IACtB,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,sEAAsE;aACzE;YACD,WAAW,EAAE;gBACX,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EACT,uFAAuF;aAC1F;YACD,MAAM,EAAE;gBACN,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EACT,8EAA8E;aACjF;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,EAAE,aAAa,EAAE,QAAQ,CAAC;QACpD,oBAAoB,EAAE,KAAK;KAC5B;CACF,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,IAAkB;IAClD,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,aAAa,CAAC;IACxD,MAAM,YAAY,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,OAAO,YAAY,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,OAAgB,EAChB,OAAgB,EAChB,IAAkB;IAElB,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,oBAAoB,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,kBAAkB,GAAG,OAAO;QAChC,CAAC,CAAC,iHAAiH;QACnH,CAAC,CAAC,kFAAkF,CAAC;IAEvF,MAAM,QAAQ,GACZ,IAAI,IAAI,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,gBAAgB,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IAEtE,KAAK,CAAC,IAAI,CACR,gDAAgD,QAAQ,GAAG,kBAAkB,mDAAmD,IAAI,EAAE,CACvI,CAAC;IAEF,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,OAAgB,EAChB,IAAkB;IAElB,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,oBAAoB,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,QAAQ,GACZ,IAAI,IAAI,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,gBAAgB,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IAEtE,KAAK,CAAC,IAAI,CACR,oDAAoD,QAAQ,wDAAwD,IAAI,EAAE,CAC3H,CAAC;IAEF,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAmDlF,wBAAsB,OAAO,CAC3B,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAyDxB"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { buildSystemPrompt, buildUserMessage, buildCheckMessage, CHECK_SYSTEM_PROMPT, } from "../prompt.js";
|
|
2
|
+
const TOOL_DEFINITION = {
|
|
3
|
+
name: "rewrite_result",
|
|
4
|
+
description: "Return the rewritten text, optional explanation, and optional issues.",
|
|
5
|
+
input_schema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
rewrittenText: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "The rewritten text in GOV.UK style. Empty string when in check mode.",
|
|
11
|
+
},
|
|
12
|
+
explanation: {
|
|
13
|
+
type: "array",
|
|
14
|
+
items: { type: "string" },
|
|
15
|
+
description: "Short bullet points explaining what was changed and why. Only include when requested.",
|
|
16
|
+
},
|
|
17
|
+
issues: {
|
|
18
|
+
type: "array",
|
|
19
|
+
items: { type: "string" },
|
|
20
|
+
description: "GOV.UK style issues found in the original text. Only populate in check mode.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["rewrittenText", "explanation", "issues"],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export async function rewrite(options, request) {
|
|
27
|
+
const baseUrl = options.baseUrl ?? "https://api.anthropic.com";
|
|
28
|
+
const url = `${baseUrl}/v1/messages`;
|
|
29
|
+
const systemPrompt = request.check
|
|
30
|
+
? CHECK_SYSTEM_PROMPT
|
|
31
|
+
: buildSystemPrompt(request.mode);
|
|
32
|
+
const userMessage = request.check
|
|
33
|
+
? buildCheckMessage(request.text, request.context, request.mode)
|
|
34
|
+
: buildUserMessage(request.text, request.explain, request.context, request.mode);
|
|
35
|
+
const body = {
|
|
36
|
+
model: options.model,
|
|
37
|
+
max_tokens: 4096,
|
|
38
|
+
system: systemPrompt,
|
|
39
|
+
messages: [{ role: "user", content: userMessage }],
|
|
40
|
+
tools: [TOOL_DEFINITION],
|
|
41
|
+
tool_choice: { type: "tool", name: "rewrite_result" },
|
|
42
|
+
};
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
45
|
+
let response;
|
|
46
|
+
try {
|
|
47
|
+
response = await fetch(url, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
"x-api-key": options.apiKey,
|
|
52
|
+
"anthropic-version": "2023-06-01",
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify(body),
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
60
|
+
throw new Error(`Request timed out after ${options.timeoutMs}ms`);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
const data = (await response.json());
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(data.error?.message ?? `Anthropic API error: ${response.status}`);
|
|
70
|
+
}
|
|
71
|
+
const toolUse = data.content.find((block) => block.type === "tool_use");
|
|
72
|
+
if (!toolUse?.input) {
|
|
73
|
+
throw new Error("Anthropic returned no tool use result");
|
|
74
|
+
}
|
|
75
|
+
return toolUse.input;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/providers/openai.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA4BlF,wBAAsB,OAAO,CAC3B,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAkExB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { buildSystemPrompt, buildUserMessage, buildCheckMessage, CHECK_SYSTEM_PROMPT, JSON_SCHEMA, } from "../prompt.js";
|
|
2
|
+
export async function rewrite(options, request) {
|
|
3
|
+
const baseUrl = options.baseUrl ?? "https://api.openai.com";
|
|
4
|
+
const url = `${baseUrl}/v1/chat/completions`;
|
|
5
|
+
const systemPrompt = request.check
|
|
6
|
+
? CHECK_SYSTEM_PROMPT
|
|
7
|
+
: buildSystemPrompt(request.mode);
|
|
8
|
+
const userMessage = request.check
|
|
9
|
+
? buildCheckMessage(request.text, request.context, request.mode)
|
|
10
|
+
: buildUserMessage(request.text, request.explain, request.context, request.mode);
|
|
11
|
+
const body = {
|
|
12
|
+
model: options.model,
|
|
13
|
+
messages: [
|
|
14
|
+
{ role: "system", content: systemPrompt },
|
|
15
|
+
{ role: "user", content: userMessage },
|
|
16
|
+
],
|
|
17
|
+
response_format: {
|
|
18
|
+
type: "json_schema",
|
|
19
|
+
json_schema: JSON_SCHEMA,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
38
|
+
throw new Error(`Request timed out after ${options.timeoutMs}ms`);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
const data = (await response.json());
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(data.error?.message ?? `OpenAI API error: ${response.status}`);
|
|
48
|
+
}
|
|
49
|
+
const content = data.choices[0]?.message?.content;
|
|
50
|
+
if (!content) {
|
|
51
|
+
throw new Error("OpenAI returned an empty response");
|
|
52
|
+
}
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(content);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error("OpenAI returned malformed JSON");
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/providers/openai.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openrouter.d.ts","sourceRoot":"","sources":["../../src/providers/openrouter.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA4BlF,wBAAsB,OAAO,CAC3B,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAoExB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { buildSystemPrompt, buildUserMessage, buildCheckMessage, CHECK_SYSTEM_PROMPT, JSON_SCHEMA, } from "../prompt.js";
|
|
2
|
+
export async function rewrite(options, request) {
|
|
3
|
+
const baseUrl = options.baseUrl ?? "https://openrouter.ai/api";
|
|
4
|
+
const url = `${baseUrl}/v1/chat/completions`;
|
|
5
|
+
const systemPrompt = request.check
|
|
6
|
+
? CHECK_SYSTEM_PROMPT
|
|
7
|
+
: buildSystemPrompt(request.mode);
|
|
8
|
+
const userMessage = request.check
|
|
9
|
+
? buildCheckMessage(request.text, request.context, request.mode)
|
|
10
|
+
: buildUserMessage(request.text, request.explain, request.context, request.mode);
|
|
11
|
+
const body = {
|
|
12
|
+
model: options.model,
|
|
13
|
+
messages: [
|
|
14
|
+
{ role: "system", content: systemPrompt },
|
|
15
|
+
{ role: "user", content: userMessage },
|
|
16
|
+
],
|
|
17
|
+
response_format: {
|
|
18
|
+
type: "json_schema",
|
|
19
|
+
json_schema: JSON_SCHEMA,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
31
|
+
"HTTP-Referer": "https://github.com/govuk-rewrite",
|
|
32
|
+
"X-Title": "govuk-rewrite",
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
40
|
+
throw new Error(`Request timed out after ${options.timeoutMs}ms`);
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
const data = (await response.json());
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(data.error?.message ?? `OpenRouter API error: ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
const content = data.choices[0]?.message?.content;
|
|
52
|
+
if (!content) {
|
|
53
|
+
throw new Error("OpenRouter returned an empty response");
|
|
54
|
+
}
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(content);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error("OpenRouter returned malformed JSON");
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=openrouter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openrouter.js","sourceRoot":"","sources":["../../src/providers/openrouter.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type Provider = "openai" | "anthropic" | "openrouter";
|
|
2
|
+
export type ContentMode = "page-body" | "error-message" | "hint-text" | "notification" | "button";
|
|
3
|
+
export interface RewriteResult {
|
|
4
|
+
rewrittenText: string;
|
|
5
|
+
explanation?: string[];
|
|
6
|
+
issues?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface Config {
|
|
9
|
+
provider: Provider;
|
|
10
|
+
model: string;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ProviderOptions {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
model: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RewriteRequest {
|
|
22
|
+
text: string;
|
|
23
|
+
explain: boolean;
|
|
24
|
+
check?: boolean;
|
|
25
|
+
context?: string;
|
|
26
|
+
mode?: ContentMode;
|
|
27
|
+
}
|
|
28
|
+
export type ProviderAdapter = (options: ProviderOptions, request: RewriteRequest) => Promise<RewriteResult>;
|
|
29
|
+
export interface OutputOptions {
|
|
30
|
+
result: RewriteResult;
|
|
31
|
+
format: "plain" | "explain" | "json" | "diff" | "check";
|
|
32
|
+
provider: Provider;
|
|
33
|
+
model: string;
|
|
34
|
+
originalText?: string;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,CAAC;AAE7D,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,eAAe,GACf,WAAW,GACX,cAAc,GACd,QAAQ,CAAC;AAEb,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,cAAc,KACpB,OAAO,CAAC,aAAa,CAAC,CAAC;AAE5B,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACxD,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "govuk-rewrite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A tiny CLI that rewrites text into GOV.UK-style content",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"govuk-rewrite": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest"
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"keywords": ["govuk", "content", "rewrite", "cli"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"commander": "^12.1.0",
|
|
20
|
+
"ora": "^8.1.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.10.0",
|
|
24
|
+
"tsx": "^4.19.2",
|
|
25
|
+
"typescript": "^5.7.2",
|
|
26
|
+
"vitest": "^2.1.8"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
}
|
|
31
|
+
}
|