altalandi 0.4.0 → 0.5.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 +35 -0
- package/altalandi.mjs +274 -223
- package/auth.mjs +223 -215
- package/client.d.ts +117 -113
- package/client.mjs +209 -208
- package/package.json +2 -2
- package/report.mjs +280 -114
- package/skill/SKILL.md +93 -117
package/README.md
CHANGED
|
@@ -96,6 +96,9 @@ altalandi "Some English or Icelandic text."
|
|
|
96
96
|
echo "some text" | altalandi --domain marketing
|
|
97
97
|
altalandi --json "..." # full JSON response
|
|
98
98
|
altalandi --fast "..." # skip the meaning + nativeness gates
|
|
99
|
+
altalandi --diff --file draft.txt # show before, after and every change with its reason
|
|
100
|
+
altalandi --review --file in.txt --out out.txt # for agents: one block to show as it is
|
|
101
|
+
altalandi --report --file draft.txt # also write a Markdown report to ./altalandi-reports/
|
|
99
102
|
```
|
|
100
103
|
|
|
101
104
|
| flag | default | meaning |
|
|
@@ -103,8 +106,37 @@ altalandi --fast "..." # skip the meaning + nativeness gates
|
|
|
103
106
|
| `--domain` | `general` | `general` \| `marketing` \| `data-analysis` \| `childrens-fiction` |
|
|
104
107
|
| `--register` | `neutral` | `formal` \| `neutral` \| `warm` |
|
|
105
108
|
| `--json` | off | print the full API response |
|
|
109
|
+
| `--diff` | off | print the text before and after, then each change with its reason, on `stderr` (coloured in a terminal; `NO_COLOR` turns colour off) |
|
|
110
|
+
| `--review` | off | print one Markdown block on `stdout`, made to be shown as it is: the verdict, the text before and after, each change with its reason, what the call cost and the balance and free units left. Exits on the outcome: `0` rewritten and verified or no change needed, `3` refused or unverified, `4` out of credit |
|
|
111
|
+
| `--no-store` | off | send `contribute: false`: this text is never kept. Without it, free use (calls covered by the free daily units) is kept with personal data scrubbed, to improve the service; paid use is kept only with consent ([privacy](https://altalandi.is/personuvernd#textabanki)) |
|
|
112
|
+
| `--out <file>` | | write the refined text to `<file>` (UTF-8); with `--review` the block names it when the text is shortened |
|
|
113
|
+
| `--report` | off | also write `./altalandi-reports/altalandi-<time>-<id>.md`: input, output, a table of every change (before, after, reason, layer), warnings and the verdict |
|
|
114
|
+
| `--report-dir <dir>` | | write the report to `<dir>` instead (implies `--report`) |
|
|
106
115
|
| `--fast` | off | `preserveMeaning:false`, `scoreNativeness:false`, `onlyImprove:false`, `polishPass:false`, `scoring:"none"` — normally one model call (structured input can still add a structure-preserving retry) |
|
|
107
116
|
|
|
117
|
+
## Before and after
|
|
118
|
+
|
|
119
|
+
`--diff` prints what the website shows inline, in the terminal:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
── Fyrir ───────────────────────────────────
|
|
123
|
+
Vertu viss um að ná út til okkar ef þú hefur einhverjar spurningar.
|
|
124
|
+
|
|
125
|
+
── Eftir ───────────────────────────────────
|
|
126
|
+
Ekki hika við að hafa samband ef einhverjar spurningar vakna.
|
|
127
|
+
|
|
128
|
+
── Breytingar (1) ──────────────────────────
|
|
129
|
+
1. − Vertu viss um að ná út til okkar ef þú hefur einhverjar spurningar.
|
|
130
|
+
+ Ekki hika við að hafa samband ef einhverjar spurningar vakna.
|
|
131
|
+
↳ <the reason the engine gives>
|
|
132
|
+
|
|
133
|
+
Endurskrifað og merking staðfest · innfæddni 91/100
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`--report` (or `ALTALANDI_REPORT=1`) writes the same content as a Markdown file
|
|
137
|
+
you can keep or share. Reports are off by default because they contain your text.
|
|
138
|
+
`stdout` stays the refined text alone either way, so piping is unaffected.
|
|
139
|
+
|
|
108
140
|
## The meaning guarantee
|
|
109
141
|
|
|
110
142
|
Altalandi never silently changes meaning. For Icelandic input where the rewrite
|
|
@@ -138,6 +170,9 @@ the explicit verification opt-out and exits 0 after a successful API response.
|
|
|
138
170
|
|-----|---------|---------|
|
|
139
171
|
| `ALTALANDI_API_KEY` | — | your key (Bearer). Mint at https://altalandi.is/portal |
|
|
140
172
|
| `ALTALANDI_BASE_URL` | `https://altalandi.is/api` | API base (override for self-hosting) |
|
|
173
|
+
| `ALTALANDI_DIFF` | — | `1` = always print the before/after (as `--diff`) |
|
|
174
|
+
| `ALTALANDI_REPORT` | — | `1` = always write a report (as `--report`) |
|
|
175
|
+
| `ALTALANDI_REPORT_DIR` | `./altalandi-reports` | where reports go |
|
|
141
176
|
|
|
142
177
|
MIT licensed. Calls are paid from the account's prepaid credit (a 500 kr trial,
|
|
143
178
|
then credit bought in the portal); a call the credit cannot cover exits with the
|
package/altalandi.mjs
CHANGED
|
@@ -1,223 +1,274 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// altalandi — turn English or stiff/AI Icelandic into native Icelandic from
|
|
3
|
-
// your terminal. Zero dependencies (Node >= 18 has global fetch).
|
|
4
|
-
//
|
|
5
|
-
// npx altalandi login # once: connects this computer to your account
|
|
6
|
-
// altalandi "Make sure to reach out to us today."
|
|
7
|
-
// echo "some text" | altalandi --domain marketing
|
|
8
|
-
//
|
|
9
|
-
// stdout = the refined text (so you can pipe it); stderr = nativeness score,
|
|
10
|
-
// meaning verdict, API warnings, and the diff.
|
|
11
|
-
|
|
12
|
-
import { refine } from "./client.mjs";
|
|
13
|
-
import { beforeAfterText, reportEnabled, writeReport } from "./report.mjs";
|
|
14
|
-
import { readCredentials, resolveKey, runLogin, runLogout, runWhoami } from "./auth.mjs";
|
|
15
|
-
|
|
16
|
-
const args = process.argv.slice(2);
|
|
17
|
-
|
|
18
|
-
// Subcommands and meta flags are handled before anything that needs an API key
|
|
19
|
-
// or input text. `altalandi update` checks for a newer CLI and skill and asks
|
|
20
|
-
// before changing anything.
|
|
21
|
-
let handled = false;
|
|
22
|
-
if (args[0] === "update") {
|
|
23
|
-
handled = true;
|
|
24
|
-
const { runUpdate } = await import("./update.mjs");
|
|
25
|
-
const rest = args.slice(1);
|
|
26
|
-
if (rest.includes("-h") || rest.includes("--help")) { updateHelp(); process.exit(0); }
|
|
27
|
-
// exitCode, never process.exit(): the registry request can leave a socket
|
|
28
|
-
// mid-close, and exiting then aborts Node on Windows with a libuv assertion
|
|
29
|
-
// (UV_HANDLE_CLOSING). The refine path below exits the same way for the
|
|
30
|
-
// same reason.
|
|
31
|
-
process.exitCode = await runUpdate({
|
|
32
|
-
check: rest.includes("--check"),
|
|
33
|
-
yes: rest.includes("--yes") || rest.includes("-y"),
|
|
34
|
-
skillOnly: rest.includes("--skill-only"),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
if (!handled && ["login", "setup", "logout", "whoami"].includes(args[0])) {
|
|
38
|
-
handled = true;
|
|
39
|
-
const rest = args.slice(1);
|
|
40
|
-
const flag = (name) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; };
|
|
41
|
-
if (args[0] === "login" || args[0] === "setup") {
|
|
42
|
-
process.exitCode = await runLogin({
|
|
43
|
-
noBrowser: rest.includes("--no-browser"),
|
|
44
|
-
noSkill: rest.includes("--no-skill"),
|
|
45
|
-
baseUrl: flag("--base-url"),
|
|
46
|
-
});
|
|
47
|
-
} else if (args[0] === "logout") {
|
|
48
|
-
process.exitCode = await runLogout();
|
|
49
|
-
} else {
|
|
50
|
-
process.exitCode = await runWhoami({ json: rest.includes("--json"), baseUrl: flag("--base-url") });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (!handled && (args.includes("-v") || args.includes("--version"))) {
|
|
54
|
-
const { currentVersion } = await import("./update.mjs");
|
|
55
|
-
console.log(await currentVersion());
|
|
56
|
-
process.exit(0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function updateHelp() {
|
|
60
|
-
console.log(`altalandi update — check for a newer version and update if you agree
|
|
61
|
-
|
|
62
|
-
Usage:
|
|
63
|
-
altalandi update check, then ask before updating the CLI and the /altalandi skill
|
|
64
|
-
altalandi update --check only report; never change anything
|
|
65
|
-
altalandi update --yes update without asking (for scripts that already have consent)
|
|
66
|
-
|
|
67
|
-
Nothing changes without consent. Without a terminal to ask in (agents, CI,
|
|
68
|
-
pipes) and without --yes, it only reports what is available.
|
|
69
|
-
|
|
70
|
-
Exit codes: 0 up to date or updated · 10 an update is available but was not
|
|
71
|
-
applied · 1 the check or the update failed.`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!handled) {
|
|
75
|
-
const opts = { domain: "general", register: "neutral", json: false, fast: false, diff: false, report: undefined };
|
|
76
|
-
const positional = [];
|
|
77
|
-
for (let i = 0; i < args.length; i++) {
|
|
78
|
-
const a = args[i];
|
|
79
|
-
if (a === "-h" || a === "--help") { help(); process.exit(0); }
|
|
80
|
-
else if (a === "--json") opts.json = true;
|
|
81
|
-
else if (a === "--fast") opts.fast = true;
|
|
82
|
-
else if (a === "--domain") opts.domain = args[++i];
|
|
83
|
-
else if (a === "--register") opts.register = args[++i];
|
|
84
|
-
else if (a === "--base-url") opts.baseUrl = args[++i];
|
|
85
|
-
else if (a === "--key") opts.key = args[++i];
|
|
86
|
-
else if (a === "--file") opts.file = args[++i];
|
|
87
|
-
else if (a === "--diff") opts.diff = true;
|
|
88
|
-
else if (a === "--
|
|
89
|
-
else if (a === "--
|
|
90
|
-
else
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
altalandi
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
altalandi
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
altalandi
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
altalandi
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
--
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
--
|
|
118
|
-
--
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
--
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// altalandi — turn English or stiff/AI Icelandic into native Icelandic from
|
|
3
|
+
// your terminal. Zero dependencies (Node >= 18 has global fetch).
|
|
4
|
+
//
|
|
5
|
+
// npx altalandi login # once: connects this computer to your account
|
|
6
|
+
// altalandi "Make sure to reach out to us today."
|
|
7
|
+
// echo "some text" | altalandi --domain marketing
|
|
8
|
+
//
|
|
9
|
+
// stdout = the refined text (so you can pipe it); stderr = nativeness score,
|
|
10
|
+
// meaning verdict, API warnings, and the diff.
|
|
11
|
+
|
|
12
|
+
import { refine } from "./client.mjs";
|
|
13
|
+
import { beforeAfterText, costLines, outcomeOf, reportEnabled, reviewMarkdown, writeReport } from "./report.mjs";
|
|
14
|
+
import { readCredentials, resolveKey, runLogin, runLogout, runWhoami } from "./auth.mjs";
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
// Subcommands and meta flags are handled before anything that needs an API key
|
|
19
|
+
// or input text. `altalandi update` checks for a newer CLI and skill and asks
|
|
20
|
+
// before changing anything.
|
|
21
|
+
let handled = false;
|
|
22
|
+
if (args[0] === "update") {
|
|
23
|
+
handled = true;
|
|
24
|
+
const { runUpdate } = await import("./update.mjs");
|
|
25
|
+
const rest = args.slice(1);
|
|
26
|
+
if (rest.includes("-h") || rest.includes("--help")) { updateHelp(); process.exit(0); }
|
|
27
|
+
// exitCode, never process.exit(): the registry request can leave a socket
|
|
28
|
+
// mid-close, and exiting then aborts Node on Windows with a libuv assertion
|
|
29
|
+
// (UV_HANDLE_CLOSING). The refine path below exits the same way for the
|
|
30
|
+
// same reason.
|
|
31
|
+
process.exitCode = await runUpdate({
|
|
32
|
+
check: rest.includes("--check"),
|
|
33
|
+
yes: rest.includes("--yes") || rest.includes("-y"),
|
|
34
|
+
skillOnly: rest.includes("--skill-only"),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!handled && ["login", "setup", "logout", "whoami"].includes(args[0])) {
|
|
38
|
+
handled = true;
|
|
39
|
+
const rest = args.slice(1);
|
|
40
|
+
const flag = (name) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; };
|
|
41
|
+
if (args[0] === "login" || args[0] === "setup") {
|
|
42
|
+
process.exitCode = await runLogin({
|
|
43
|
+
noBrowser: rest.includes("--no-browser"),
|
|
44
|
+
noSkill: rest.includes("--no-skill"),
|
|
45
|
+
baseUrl: flag("--base-url"),
|
|
46
|
+
});
|
|
47
|
+
} else if (args[0] === "logout") {
|
|
48
|
+
process.exitCode = await runLogout();
|
|
49
|
+
} else {
|
|
50
|
+
process.exitCode = await runWhoami({ json: rest.includes("--json"), baseUrl: flag("--base-url") });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!handled && (args.includes("-v") || args.includes("--version"))) {
|
|
54
|
+
const { currentVersion } = await import("./update.mjs");
|
|
55
|
+
console.log(await currentVersion());
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function updateHelp() {
|
|
60
|
+
console.log(`altalandi update — check for a newer version and update if you agree
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
altalandi update check, then ask before updating the CLI and the /altalandi skill
|
|
64
|
+
altalandi update --check only report; never change anything
|
|
65
|
+
altalandi update --yes update without asking (for scripts that already have consent)
|
|
66
|
+
|
|
67
|
+
Nothing changes without consent. Without a terminal to ask in (agents, CI,
|
|
68
|
+
pipes) and without --yes, it only reports what is available.
|
|
69
|
+
|
|
70
|
+
Exit codes: 0 up to date or updated · 10 an update is available but was not
|
|
71
|
+
applied · 1 the check or the update failed.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!handled) {
|
|
75
|
+
const opts = { domain: "general", register: "neutral", json: false, fast: false, diff: false, review: false, report: undefined };
|
|
76
|
+
const positional = [];
|
|
77
|
+
for (let i = 0; i < args.length; i++) {
|
|
78
|
+
const a = args[i];
|
|
79
|
+
if (a === "-h" || a === "--help") { help(); process.exit(0); }
|
|
80
|
+
else if (a === "--json") opts.json = true;
|
|
81
|
+
else if (a === "--fast") opts.fast = true;
|
|
82
|
+
else if (a === "--domain") opts.domain = args[++i];
|
|
83
|
+
else if (a === "--register") opts.register = args[++i];
|
|
84
|
+
else if (a === "--base-url") opts.baseUrl = args[++i];
|
|
85
|
+
else if (a === "--key") opts.key = args[++i];
|
|
86
|
+
else if (a === "--file") opts.file = args[++i];
|
|
87
|
+
else if (a === "--diff") opts.diff = true;
|
|
88
|
+
else if (a === "--review") opts.review = true;
|
|
89
|
+
else if (a === "--out") opts.out = args[++i];
|
|
90
|
+
else if (a === "--no-store") opts.noStore = true;
|
|
91
|
+
else if (a === "--report") opts.report = true;
|
|
92
|
+
else if (a === "--report-dir") { opts.report = true; opts.reportDir = args[++i]; }
|
|
93
|
+
else positional.push(a);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function help() {
|
|
97
|
+
console.log(`altalandi — native Icelandic from your terminal
|
|
98
|
+
|
|
99
|
+
Usage:
|
|
100
|
+
altalandi login connect this computer to your account (once)
|
|
101
|
+
altalandi "text to refine"
|
|
102
|
+
altalandi --file draft.txt read the text from a file
|
|
103
|
+
altalandi --review --file in.txt --out out.txt
|
|
104
|
+
for agents: one block to show as it is
|
|
105
|
+
echo "text" | altalandi
|
|
106
|
+
altalandi --domain marketing --register warm "text"
|
|
107
|
+
|
|
108
|
+
Commands:
|
|
109
|
+
altalandi login open altalandi.is, sign in or sign up, approve; stores the key
|
|
110
|
+
and installs the Claude Code skill (--no-skill, --no-browser)
|
|
111
|
+
altalandi whoami show the account and balance behind the stored key
|
|
112
|
+
altalandi logout forget the stored key on this computer
|
|
113
|
+
altalandi update check for a newer version and update if you agree
|
|
114
|
+
altalandi --version print the installed version
|
|
115
|
+
|
|
116
|
+
Options:
|
|
117
|
+
--domain general | marketing | data-analysis | childrens-fiction (default general)
|
|
118
|
+
--register formal | neutral | warm (default neutral)
|
|
119
|
+
--fast no polish, no nativeness score, no meaning gate — normally one writer call
|
|
120
|
+
(structured input can still add a structure-preserving retry)
|
|
121
|
+
--json print the full JSON response
|
|
122
|
+
--diff show the text before and after, and every change with its reason (stderr)
|
|
123
|
+
--review print one Markdown block on stdout to show the user as it is: verdict,
|
|
124
|
+
before, after, every change with its reason, the cost and the balance
|
|
125
|
+
--out write the refined text to this UTF-8 file (use with --review)
|
|
126
|
+
--no-store don't keep this text: free use is otherwise kept, scrubbed of personal
|
|
127
|
+
data, to improve the service (paid use is kept only with consent)
|
|
128
|
+
--report also write a Markdown report (input, output, changes, reasons, verdict)
|
|
129
|
+
to ./altalandi-reports/ (or --report-dir <dir>; ALTALANDI_REPORT=1 turns it on)
|
|
130
|
+
--file read the text from a UTF-8 file instead of an argument
|
|
131
|
+
--base-url API base (default $ALTALANDI_BASE_URL or https://altalandi.is/api)
|
|
132
|
+
|
|
133
|
+
stdout = refined text; stderr = nativeness / meaning / warnings / diff.
|
|
134
|
+
Without --fast, false/null meaning verification exits 3 after printing the response.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readStdin() {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
if (process.stdin.isTTY) return resolve("");
|
|
140
|
+
let d = "";
|
|
141
|
+
process.stdin.setEncoding("utf8");
|
|
142
|
+
process.stdin.on("data", (c) => (d += c));
|
|
143
|
+
process.stdin.on("end", () => resolve(d));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const stored = readCredentials();
|
|
148
|
+
const baseUrl = (opts.baseUrl || process.env.ALTALANDI_BASE_URL || stored?.baseUrl || "https://altalandi.is/api").replace(/\/$/, "");
|
|
149
|
+
const { key } = resolveKey({ flag: opts.key });
|
|
150
|
+
|
|
151
|
+
if (opts.key) {
|
|
152
|
+
console.error("⚠ --key is deprecated because command arguments can leak through shell history/process listings; load ALTALANDI_API_KEY instead.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let text = positional.join(" ").trim();
|
|
156
|
+
if (opts.file) {
|
|
157
|
+
const { readFile } = await import("node:fs/promises");
|
|
158
|
+
try { text = (await readFile(opts.file, "utf8")).replace(/^/, "").trim(); }
|
|
159
|
+
catch (e) { console.error(`Could not read ${opts.file}: ${e.message}`); process.exit(2); }
|
|
160
|
+
}
|
|
161
|
+
if (!text) text = (await readStdin()).trim();
|
|
162
|
+
if (!text) { console.error("No input. Pass text as an argument or pipe via stdin. (altalandi --help)"); process.exit(2); }
|
|
163
|
+
if (!key) { console.error("Not logged in. Run once: npx altalandi login (or set ALTALANDI_API_KEY)"); process.exit(2); }
|
|
164
|
+
|
|
165
|
+
const requestOptions = {
|
|
166
|
+
apiKey: key,
|
|
167
|
+
baseUrl,
|
|
168
|
+
domain: opts.domain,
|
|
169
|
+
register: opts.register,
|
|
170
|
+
...(opts.noStore ? { contribute: false } : {}),
|
|
171
|
+
};
|
|
172
|
+
// --fast disables every optional quality/verification pass. The writer can
|
|
173
|
+
// still retry once when it drops document structure, as documented in --help.
|
|
174
|
+
// Name every switch explicitly so a future server-default change cannot make
|
|
175
|
+
// this client silently slower.
|
|
176
|
+
if (opts.fast) {
|
|
177
|
+
requestOptions.preserveMeaning = false;
|
|
178
|
+
requestOptions.scoreNativeness = false;
|
|
179
|
+
requestOptions.onlyImprove = false;
|
|
180
|
+
requestOptions.polishPass = false;
|
|
181
|
+
requestOptions.scoring = "none";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// /v1/me before and after the call gives the cost and the balance for the
|
|
185
|
+
// review block. Best-effort: a failed lookup only blanks those lines.
|
|
186
|
+
async function me() {
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch(`${baseUrl}/v1/me`, {
|
|
189
|
+
headers: { authorization: `Bearer ${key}`, accept: "application/json" },
|
|
190
|
+
signal: AbortSignal.timeout(15_000),
|
|
191
|
+
});
|
|
192
|
+
return res.ok ? await res.json() : null;
|
|
193
|
+
} catch { return null; }
|
|
194
|
+
}
|
|
195
|
+
const meBefore = opts.review ? await me() : null;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// refine() owns a single 300-second AbortController deadline across fetch
|
|
199
|
+
// and response-body parsing, matching the hosted package API.
|
|
200
|
+
const data = await refine(text, requestOptions);
|
|
201
|
+
// --review exits on the outcome the block states: a vetoed rewrite returns
|
|
202
|
+
// the original with meaningPreserved true, and must not read as a success.
|
|
203
|
+
const verificationFailed = opts.review
|
|
204
|
+
? ["refused", "unverified"].includes(outcomeOf(data)) && !(opts.fast && outcomeOf(data) === "unverified")
|
|
205
|
+
: !opts.fast && data.meaningPreserved !== true;
|
|
206
|
+
|
|
207
|
+
if (opts.out) {
|
|
208
|
+
const { writeFile } = await import("node:fs/promises");
|
|
209
|
+
try { await writeFile(opts.out, String(data.output ?? "") + "\n", "utf8"); }
|
|
210
|
+
catch (e) { console.error(`Could not write ${opts.out}: ${e.message}`); process.exitCode = 1; }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (opts.review) {
|
|
214
|
+
const meAfter = await me();
|
|
215
|
+
// Without --out the block is the only copy of a paid result: never shorten it.
|
|
216
|
+
process.stdout.write(reviewMarkdown(text, data, {
|
|
217
|
+
before: meBefore, after: meAfter, fullTextAt: opts.out || "", maxChars: opts.out ? 1500 : Infinity,
|
|
218
|
+
}));
|
|
219
|
+
} else if (opts.json) {
|
|
220
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
221
|
+
if (verificationFailed) {
|
|
222
|
+
console.error("Meaning verification did not pass; JSON was printed, exiting with status 3.");
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
process.stdout.write(data.output + "\n");
|
|
226
|
+
const q = data.quality || {};
|
|
227
|
+
const meta = [];
|
|
228
|
+
if (q.nativenessScore != null) meta.push(`nativeness ${q.nativenessScore}/100`);
|
|
229
|
+
if (data.meaningPreserved === false) meta.push("⚠ MEANING DRIFT — returned the original");
|
|
230
|
+
else if (data.meaningPreserved === true) meta.push("meaning preserved");
|
|
231
|
+
else if (opts.fast) meta.push("meaning check skipped (--fast)");
|
|
232
|
+
else meta.push("⚠ MEANING UNVERIFIED — review before use");
|
|
233
|
+
if (meta.length) console.error("· " + meta.join(" · "));
|
|
234
|
+
for (const warning of Array.isArray(data.warnings) ? data.warnings : []) {
|
|
235
|
+
console.error(`⚠ ${warning}`);
|
|
236
|
+
}
|
|
237
|
+
if (opts.diff || process.env.ALTALANDI_DIFF === "1") {
|
|
238
|
+
console.error("\n" + beforeAfterText(text, data, { color: Boolean(process.stderr.isTTY) && !process.env.NO_COLOR }));
|
|
239
|
+
} else {
|
|
240
|
+
for (const d of data.diffs || []) console.error(` − ${d.before}\n + ${d.after} (${d.reason})`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (reportEnabled(opts.report)) {
|
|
245
|
+
try {
|
|
246
|
+
const path = await writeReport(text, data, { domain: opts.domain, register: opts.register }, opts.reportDir);
|
|
247
|
+
console.error(`Skýrsla: ${path}`);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
console.error(`Could not write the report: ${e.message}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// stdout/JSON is intentionally written first so callers can inspect the
|
|
254
|
+
// server response, but guarded automation must not accept null/false as a
|
|
255
|
+
// verified success. --fast is the explicit opt-out from this contract.
|
|
256
|
+
if (verificationFailed) process.exitCode = 3;
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (opts.review) {
|
|
259
|
+
// The block still comes out, so the user sees why and what is left.
|
|
260
|
+
const head = e.status === 402 ? "Ekki næg inneign fyrir þetta kall"
|
|
261
|
+
: e.status === 429 ? "Of mörg köll í einu eða á mínútu; reyndu aftur eftir smá stund"
|
|
262
|
+
: e.status === 401 ? "Lykillinn er ógildur eða afturkallaður (`npx altalandi login`)"
|
|
263
|
+
: `Kallið mistókst: ${e.message}`;
|
|
264
|
+
const lines = [`**Altalandi:** ${head}`, "", "Textanum var ekki breytt.", "", "**Kostnaður**",
|
|
265
|
+
"- Þetta kall: 0 kr.", ...costLines({}, null, meBefore).slice(1)];
|
|
266
|
+
if (e.status === 402 && !lines.some((l) => l.includes("/portal"))) lines.push("- Bæta við inneign: https://altalandi.is/portal");
|
|
267
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
268
|
+
}
|
|
269
|
+
console.error("Request failed:", e.message);
|
|
270
|
+
if (e.status === 402) console.error("→ Add credit at https://altalandi.is/portal (automatic top-up is available there).");
|
|
271
|
+
if (e.status === 401) console.error("→ The key is invalid or revoked. Run: npx altalandi login");
|
|
272
|
+
process.exit(e.status === 402 ? 4 : 1);
|
|
273
|
+
}
|
|
274
|
+
}
|