altalandi 0.3.0 → 0.4.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/altalandi.mjs +223 -203
- package/package.json +57 -56
- package/report.mjs +114 -0
- package/skill/SKILL.md +27 -5
package/altalandi.mjs
CHANGED
|
@@ -1,203 +1,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 {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// same
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
altalandi update
|
|
64
|
-
altalandi update --
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
else if (a === "--
|
|
81
|
-
else if (a === "--
|
|
82
|
-
else if (a === "--
|
|
83
|
-
else if (a === "--
|
|
84
|
-
else if (a === "--
|
|
85
|
-
else if (a === "--
|
|
86
|
-
else
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
altalandi
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
altalandi
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
altalandi
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
--
|
|
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
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
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 === "--report") opts.report = true;
|
|
89
|
+
else if (a === "--report-dir") { opts.report = true; opts.reportDir = args[++i]; }
|
|
90
|
+
else positional.push(a);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function help() {
|
|
94
|
+
console.log(`altalandi — native Icelandic from your terminal
|
|
95
|
+
|
|
96
|
+
Usage:
|
|
97
|
+
altalandi login connect this computer to your account (once)
|
|
98
|
+
altalandi "text to refine"
|
|
99
|
+
altalandi --file draft.txt read the text from a file (best for agents)
|
|
100
|
+
echo "text" | altalandi
|
|
101
|
+
altalandi --domain marketing --register warm "text"
|
|
102
|
+
|
|
103
|
+
Commands:
|
|
104
|
+
altalandi login open altalandi.is, sign in or sign up, approve; stores the key
|
|
105
|
+
and installs the Claude Code skill (--no-skill, --no-browser)
|
|
106
|
+
altalandi whoami show the account and balance behind the stored key
|
|
107
|
+
altalandi logout forget the stored key on this computer
|
|
108
|
+
altalandi update check for a newer version and update if you agree
|
|
109
|
+
altalandi --version print the installed version
|
|
110
|
+
|
|
111
|
+
Options:
|
|
112
|
+
--domain general | marketing | data-analysis | childrens-fiction (default general)
|
|
113
|
+
--register formal | neutral | warm (default neutral)
|
|
114
|
+
--fast no polish, no nativeness score, no meaning gate — normally one writer call
|
|
115
|
+
(structured input can still add a structure-preserving retry)
|
|
116
|
+
--json print the full JSON response
|
|
117
|
+
--diff show the text before and after, and every change with its reason (stderr)
|
|
118
|
+
--report also write a Markdown report (input, output, changes, reasons, verdict)
|
|
119
|
+
to ./altalandi-reports/ (or --report-dir <dir>; ALTALANDI_REPORT=1 turns it on)
|
|
120
|
+
--file read the text from a UTF-8 file instead of an argument
|
|
121
|
+
--base-url API base (default $ALTALANDI_BASE_URL or https://altalandi.is/api)
|
|
122
|
+
|
|
123
|
+
stdout = refined text; stderr = nativeness / meaning / warnings / diff.
|
|
124
|
+
Without --fast, false/null meaning verification exits 3 after printing the response.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readStdin() {
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
if (process.stdin.isTTY) return resolve("");
|
|
130
|
+
let d = "";
|
|
131
|
+
process.stdin.setEncoding("utf8");
|
|
132
|
+
process.stdin.on("data", (c) => (d += c));
|
|
133
|
+
process.stdin.on("end", () => resolve(d));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const stored = readCredentials();
|
|
138
|
+
const baseUrl = (opts.baseUrl || process.env.ALTALANDI_BASE_URL || stored?.baseUrl || "https://altalandi.is/api").replace(/\/$/, "");
|
|
139
|
+
const { key } = resolveKey({ flag: opts.key });
|
|
140
|
+
|
|
141
|
+
if (opts.key) {
|
|
142
|
+
console.error("⚠ --key is deprecated because command arguments can leak through shell history/process listings; load ALTALANDI_API_KEY instead.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let text = positional.join(" ").trim();
|
|
146
|
+
if (opts.file) {
|
|
147
|
+
const { readFile } = await import("node:fs/promises");
|
|
148
|
+
try { text = (await readFile(opts.file, "utf8")).replace(/^/, "").trim(); }
|
|
149
|
+
catch (e) { console.error(`Could not read ${opts.file}: ${e.message}`); process.exit(2); }
|
|
150
|
+
}
|
|
151
|
+
if (!text) text = (await readStdin()).trim();
|
|
152
|
+
if (!text) { console.error("No input. Pass text as an argument or pipe via stdin. (altalandi --help)"); process.exit(2); }
|
|
153
|
+
if (!key) { console.error("Not logged in. Run once: npx altalandi login (or set ALTALANDI_API_KEY)"); process.exit(2); }
|
|
154
|
+
|
|
155
|
+
const requestOptions = {
|
|
156
|
+
apiKey: key,
|
|
157
|
+
baseUrl,
|
|
158
|
+
domain: opts.domain,
|
|
159
|
+
register: opts.register,
|
|
160
|
+
};
|
|
161
|
+
// --fast disables every optional quality/verification pass. The writer can
|
|
162
|
+
// still retry once when it drops document structure, as documented in --help.
|
|
163
|
+
// Name every switch explicitly so a future server-default change cannot make
|
|
164
|
+
// this client silently slower.
|
|
165
|
+
if (opts.fast) {
|
|
166
|
+
requestOptions.preserveMeaning = false;
|
|
167
|
+
requestOptions.scoreNativeness = false;
|
|
168
|
+
requestOptions.onlyImprove = false;
|
|
169
|
+
requestOptions.polishPass = false;
|
|
170
|
+
requestOptions.scoring = "none";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// refine() owns a single 300-second AbortController deadline across fetch
|
|
175
|
+
// and response-body parsing, matching the hosted package API.
|
|
176
|
+
const data = await refine(text, requestOptions);
|
|
177
|
+
const verificationFailed = !opts.fast && data.meaningPreserved !== true;
|
|
178
|
+
|
|
179
|
+
if (opts.json) {
|
|
180
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
181
|
+
if (verificationFailed) {
|
|
182
|
+
console.error("Meaning verification did not pass; JSON was printed, exiting with status 3.");
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
process.stdout.write(data.output + "\n");
|
|
186
|
+
const q = data.quality || {};
|
|
187
|
+
const meta = [];
|
|
188
|
+
if (q.nativenessScore != null) meta.push(`nativeness ${q.nativenessScore}/100`);
|
|
189
|
+
if (data.meaningPreserved === false) meta.push("⚠ MEANING DRIFT — returned the original");
|
|
190
|
+
else if (data.meaningPreserved === true) meta.push("meaning preserved");
|
|
191
|
+
else if (opts.fast) meta.push("meaning check skipped (--fast)");
|
|
192
|
+
else meta.push("⚠ MEANING UNVERIFIED — review before use");
|
|
193
|
+
if (meta.length) console.error("· " + meta.join(" · "));
|
|
194
|
+
for (const warning of Array.isArray(data.warnings) ? data.warnings : []) {
|
|
195
|
+
console.error(`⚠ ${warning}`);
|
|
196
|
+
}
|
|
197
|
+
if (opts.diff || process.env.ALTALANDI_DIFF === "1") {
|
|
198
|
+
console.error("\n" + beforeAfterText(text, data, { color: Boolean(process.stderr.isTTY) && !process.env.NO_COLOR }));
|
|
199
|
+
} else {
|
|
200
|
+
for (const d of data.diffs || []) console.error(` − ${d.before}\n + ${d.after} (${d.reason})`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (reportEnabled(opts.report)) {
|
|
205
|
+
try {
|
|
206
|
+
const path = await writeReport(text, data, { domain: opts.domain, register: opts.register }, opts.reportDir);
|
|
207
|
+
console.error(`Skýrsla: ${path}`);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.error(`Could not write the report: ${e.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// stdout/JSON is intentionally written first so callers can inspect the
|
|
214
|
+
// server response, but guarded automation must not accept null/false as a
|
|
215
|
+
// verified success. --fast is the explicit opt-out from this contract.
|
|
216
|
+
if (verificationFailed) process.exitCode = 3;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.error("Request failed:", e.message);
|
|
219
|
+
if (e.status === 402) console.error("→ Add credit at https://altalandi.is/portal (automatic top-up is available there).");
|
|
220
|
+
if (e.status === 401) console.error("→ The key is invalid or revoked. Run: npx altalandi login");
|
|
221
|
+
process.exit(e.status === 402 ? 4 : 1);
|
|
222
|
+
}
|
|
223
|
+
}
|
package/package.json
CHANGED
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "altalandi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Native-sounding Icelandic from your terminal or AI agent: one-time login, a Claude Code skill, and meaning verification.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./client.mjs",
|
|
7
|
-
"types": "./client.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./client.d.ts",
|
|
11
|
-
"import": "./client.mjs",
|
|
12
|
-
"default": "./client.mjs"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"bin": {
|
|
16
|
-
"altalandi": "altalandi.mjs"
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"altalandi.mjs",
|
|
20
|
-
"auth.mjs",
|
|
21
|
-
"update.mjs",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"client.
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "altalandi",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Native-sounding Icelandic from your terminal or AI agent: one-time login, a Claude Code skill, and meaning verification.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./client.mjs",
|
|
7
|
+
"types": "./client.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./client.d.ts",
|
|
11
|
+
"import": "./client.mjs",
|
|
12
|
+
"default": "./client.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"altalandi": "altalandi.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"altalandi.mjs",
|
|
20
|
+
"auth.mjs",
|
|
21
|
+
"update.mjs",
|
|
22
|
+
"report.mjs",
|
|
23
|
+
"skill/SKILL.md",
|
|
24
|
+
"client.mjs",
|
|
25
|
+
"client.d.ts",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test test/cli.test.mjs test/client.test.mjs test/update.test.mjs test/auth.test.mjs"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"icelandic",
|
|
34
|
+
"íslenska",
|
|
35
|
+
"translation",
|
|
36
|
+
"translationese",
|
|
37
|
+
"nlp",
|
|
38
|
+
"altalandi",
|
|
39
|
+
"cli",
|
|
40
|
+
"i18n",
|
|
41
|
+
"localization",
|
|
42
|
+
"language"
|
|
43
|
+
],
|
|
44
|
+
"homepage": "https://altalandi.is",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/pbidhjorvar/ritvit.git",
|
|
48
|
+
"directory": "tools/cli"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/pbidhjorvar/ritvit/issues"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"license": "MIT"
|
|
57
|
+
}
|
package/report.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Before/after for people: a terminal view and an optional Markdown report.
|
|
2
|
+
//
|
|
3
|
+
// The website shows each change inline; in a terminal or an editor the
|
|
4
|
+
// before/after got lost. This module renders it the same way everywhere:
|
|
5
|
+
// beforeAfterText() plain (or coloured) block for a terminal or a tool result
|
|
6
|
+
// reportMarkdown() the report file: input, output, every change with its
|
|
7
|
+
// reason, and the verdict
|
|
8
|
+
// writeReport() writes it to ./altalandi-reports/ (or ALTALANDI_REPORT_DIR)
|
|
9
|
+
//
|
|
10
|
+
// Reports are opt-in (--report, or ALTALANDI_REPORT=1): they contain the text.
|
|
11
|
+
|
|
12
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
const VERDICT = {
|
|
16
|
+
rewritten: "Endurskrifað og merking staðfest",
|
|
17
|
+
refused: "Endurskrift hafnað: upprunalega textanum skilað óbreyttum",
|
|
18
|
+
unchanged: "Engin breyting þurfti",
|
|
19
|
+
unverified: "Merking ekki staðfest: farðu yfir áður en þú notar textann",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function outcomeOf(data) {
|
|
23
|
+
if (data?.rejectionReason) return "refused";
|
|
24
|
+
if (data?.meaningPreserved === false) return "refused";
|
|
25
|
+
if (data?.rewriteAttempted === false) return "unchanged";
|
|
26
|
+
if (data?.meaningPreserved !== true) return "unverified";
|
|
27
|
+
return "rewritten";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function reportEnabled(flag) {
|
|
31
|
+
if (flag !== undefined && flag !== null) return Boolean(flag);
|
|
32
|
+
return ["1", "true", "yes"].includes(String(process.env.ALTALANDI_REPORT || "").toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const paint = (on) => ({
|
|
36
|
+
red: (s) => (on ? `\x1b[31m${s}\x1b[0m` : s),
|
|
37
|
+
green: (s) => (on ? `\x1b[32m${s}\x1b[0m` : s),
|
|
38
|
+
dim: (s) => (on ? `\x1b[2m${s}\x1b[0m` : s),
|
|
39
|
+
bold: (s) => (on ? `\x1b[1m${s}\x1b[0m` : s),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** A before/after block with the list of changes. `color` adds ANSI colours. */
|
|
43
|
+
export function beforeAfterText(input, data, { color = false } = {}) {
|
|
44
|
+
const c = paint(color);
|
|
45
|
+
const diffs = Array.isArray(data?.diffs) ? data.diffs : [];
|
|
46
|
+
const rule = (label) => c.dim(`── ${label} ${"─".repeat(Math.max(4, 40 - label.length))}`);
|
|
47
|
+
const lines = [
|
|
48
|
+
rule("Fyrir"), String(input ?? ""), "",
|
|
49
|
+
rule("Eftir"), String(data?.output ?? ""), "",
|
|
50
|
+
rule(`Breytingar (${diffs.length})`),
|
|
51
|
+
];
|
|
52
|
+
if (!diffs.length) lines.push(c.dim("Engar breytingar."));
|
|
53
|
+
diffs.forEach((d, i) => {
|
|
54
|
+
lines.push(`${i + 1}. ${c.red("− " + String(d.before ?? "").trim())}`);
|
|
55
|
+
lines.push(` ${c.green("+ " + String(d.after ?? "").trim())}`);
|
|
56
|
+
if (d.reason) lines.push(` ${c.dim("↳ " + String(d.reason))}`);
|
|
57
|
+
});
|
|
58
|
+
const q = data?.quality || {};
|
|
59
|
+
lines.push("", c.bold(VERDICT[outcomeOf(data)]) + (q.nativenessScore != null ? c.dim(` · innfæddni ${q.nativenessScore}/100`) : ""));
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fence = (s) => {
|
|
64
|
+
const text = String(s ?? "");
|
|
65
|
+
const ticks = text.includes("```") ? "````" : "```";
|
|
66
|
+
return `${ticks}text\n${text}\n${ticks}`;
|
|
67
|
+
};
|
|
68
|
+
const cell = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
|
|
69
|
+
|
|
70
|
+
export function reportMarkdown(input, data, options = {}) {
|
|
71
|
+
const q = data?.quality || {};
|
|
72
|
+
const diffs = Array.isArray(data?.diffs) ? data.diffs : [];
|
|
73
|
+
const warnings = Array.isArray(data?.warnings) ? data.warnings : [];
|
|
74
|
+
const tier = data?.billedTier || data?.tier || {};
|
|
75
|
+
const when = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
76
|
+
const out = [
|
|
77
|
+
`# Altalandi: ${data?.id || "yfirlestur"}`,
|
|
78
|
+
"",
|
|
79
|
+
`| | |`, `|---|---|`,
|
|
80
|
+
`| Tími | ${when} UTC |`,
|
|
81
|
+
`| Niðurstaða | ${VERDICT[outcomeOf(data)]} |`,
|
|
82
|
+
`| Frummál | ${data?.sourceLang || "?"} |`,
|
|
83
|
+
`| Tegund texta | ${options.domain || "general"} |`,
|
|
84
|
+
`| Málsnið | ${options.register || "neutral"} |`,
|
|
85
|
+
`| Innfæddni | ${q.nativenessScore ?? "n/a"}/100 |`,
|
|
86
|
+
`| Ritill | ${tier.writer || "standard"} / ${tier.scoring || "?"} |`,
|
|
87
|
+
`| Auðkenni | ${data?.id || "?"} |`,
|
|
88
|
+
"",
|
|
89
|
+
"## Fyrir", "", fence(input), "",
|
|
90
|
+
"## Eftir", "", fence(data?.output), "",
|
|
91
|
+
`## Breytingar (${diffs.length})`, "",
|
|
92
|
+
];
|
|
93
|
+
if (diffs.length) {
|
|
94
|
+
out.push("| # | Fyrir | Eftir | Ástæða | Lag |", "|---|---|---|---|---|");
|
|
95
|
+
diffs.forEach((d, i) => out.push(`| ${i + 1} | ${cell(d.before)} | ${cell(d.after)} | ${cell(d.reason)} | ${cell(d.layer)} |`));
|
|
96
|
+
} else {
|
|
97
|
+
out.push("Engar breytingar.");
|
|
98
|
+
}
|
|
99
|
+
if (data?.rejectionReason) out.push("", "## Af hverju var endurskriftinni hafnað", "", cell(data.rejectionReason));
|
|
100
|
+
if (warnings.length) out.push("", "## Viðvaranir", "", ...warnings.map((w) => `- ${cell(w)}`));
|
|
101
|
+
if (data?.meaningNotes) out.push("", "## Athugun á merkingu", "", cell(data.meaningNotes));
|
|
102
|
+
return out.join("\n") + "\n";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Write the report; returns its path. */
|
|
106
|
+
export async function writeReport(input, data, options = {}, dir) {
|
|
107
|
+
const base = resolve(dir || process.env.ALTALANDI_REPORT_DIR || join(process.cwd(), "altalandi-reports"));
|
|
108
|
+
await mkdir(base, { recursive: true });
|
|
109
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
|
|
110
|
+
const id = String(data?.id || "yfirlestur").replace(/[^A-Za-z0-9_-]/g, "");
|
|
111
|
+
const path = join(base, `altalandi-${stamp}-${id}.md`);
|
|
112
|
+
await writeFile(path, reportMarkdown(input, data, options), "utf8");
|
|
113
|
+
return path;
|
|
114
|
+
}
|
package/skill/SKILL.md
CHANGED
|
@@ -27,7 +27,7 @@ If a call prints `Not logged in` (exit code 2), connect the computer:
|
|
|
27
27
|
2. Read its output as soon as it appears. It prints a link
|
|
28
28
|
(`https://altalandi.is/tengja?kodi=XXXX-XXXX`) and a code, and opens the
|
|
29
29
|
browser. Show the user the link and the code, and tell them to sign in or
|
|
30
|
-
create an account (new accounts
|
|
30
|
+
create an account (every account gets 2,000 free words a day, and new accounts 500 kr. of trial credit) and click
|
|
31
31
|
**Tengja**.
|
|
32
32
|
3. Wait for the command to finish. `✓ Tengt sem <username>` means done. Then
|
|
33
33
|
continue with the task.
|
|
@@ -76,13 +76,35 @@ For Icelandic input, one of three things happened. Tell them apart before using
|
|
|
76
76
|
- `meaningPreserved: null` means unverified, not safe. For English input the
|
|
77
77
|
translation still comes back; say it is unverified.
|
|
78
78
|
- When the original comes back, say so. Don't claim Altalandi improved it.
|
|
79
|
-
- Always show what changed
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
- Always show what changed. Never silently replace the user's text. Apply changes
|
|
80
|
+
to files only after showing them, unless the user asked for that directly.
|
|
81
|
+
|
|
82
|
+
## 4. Show the before and after
|
|
83
|
+
|
|
84
|
+
After every call, show the user this, in the chat, before anything else:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
Fyrir: <the text you sent, or its first lines if long>
|
|
88
|
+
Eftir: <output>
|
|
89
|
+
Breytingar:
|
|
90
|
+
1. − <before> → + <after> (<reason>)
|
|
91
|
+
...
|
|
92
|
+
<the verdict: rewritten and verified / refused, original returned / no change needed / unverified>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For a long text, show the first few changes and say how many there were in all.
|
|
96
|
+
For a quick look in a terminal, `--diff` prints the same block (coloured) on stderr.
|
|
97
|
+
|
|
98
|
+
**Reports (optional).** When the user wants a record, add `--report` (or they set
|
|
99
|
+
`ALTALANDI_REPORT=1`): the CLI writes `./altalandi-reports/altalandi-<time>-<id>.md`
|
|
100
|
+
with the input, the output, every change with its reason and layer, warnings and
|
|
101
|
+
the verdict. Tell the user the path it prints (`Skýrsla: …`). Reports contain the
|
|
102
|
+
text, so don't write them unless asked or the setting is on. The MCP tool takes
|
|
103
|
+
`report: true` for the same thing.
|
|
82
104
|
- `quality.nativenessScore` is a hint, not a verdict. It has been confidently wrong
|
|
83
105
|
in both directions; never use it to override a refusal.
|
|
84
106
|
|
|
85
|
-
##
|
|
107
|
+
## 5. Good to know
|
|
86
108
|
|
|
87
109
|
- Altalandi rewrites for natural Icelandic. If the user only wants spelling and
|
|
88
110
|
grammar fixed and every other word left alone, tell them it may rephrase, and
|