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/report.mjs CHANGED
@@ -1,114 +1,280 @@
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
- }
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
+ }
115
+
116
+ // ── The review block ─────────────────────────────────────────────────────────
117
+ // One Markdown block an agent pastes into the chat as it is: verdict, the text
118
+ // before and after, every change with its reason, what the call cost and what
119
+ // is left. `--review` prints it; the MCP tool returns it. The agent does not
120
+ // rebuild it from JSON, so every run looks the same.
121
+
122
+ const REJECTION = [
123
+ ["MEANING DRIFT", "merkingin breyttist í endurskriftinni"],
124
+ ["HARD INVARIANT LOST", "tala, dagsetning, nafn eða neitun hefði glatast"],
125
+ ["UNVERIFIED", "ekki tókst að staðfesta að merkingin héldist"],
126
+ ["CANDIDATE VETO", "endurskriftin reyndist ekki betri en frumtextinn"],
127
+ ];
128
+
129
+ export function rejectionText(reason) {
130
+ const raw = String(reason ?? "").trim();
131
+ if (!raw) return "";
132
+ const hit = REJECTION.find(([code]) => raw.toUpperCase().startsWith(code));
133
+ return hit ? `${hit[1][0].toUpperCase()}${hit[1].slice(1)} (${raw})` : raw;
134
+ }
135
+
136
+ /** 1688 → "1.688" (Icelandic thousands separator). */
137
+ export function formatInt(n) {
138
+ return String(Math.round(Number(n))).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
139
+ }
140
+
141
+ /** 487.5 → "487,50 kr.", 500 → "500 kr." */
142
+ export function formatIsk(n) {
143
+ const v = Math.round(Number(n) * 100) / 100;
144
+ const sign = v < 0 ? "−" : "";
145
+ const abs = Math.abs(v);
146
+ const whole = formatInt(Math.trunc(abs));
147
+ const cents = Math.round((abs - Math.trunc(abs)) * 100);
148
+ return `${sign}${whole}${cents ? "," + String(cents).padStart(2, "0") : ""} kr.`;
149
+ }
150
+
151
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
152
+
153
+ /**
154
+ * What the call cost and what is left. `data.billing` (sent by newer engines)
155
+ * is exact; otherwise the cost is the balance difference between /v1/me
156
+ * before and after the call, which parallel calls on the same account blur.
157
+ */
158
+ export function costLines(data, before, after) {
159
+ if (data?.test) return ["- Prófunarlykill: ekkert var rukkað og ekkert líkan keyrði."];
160
+ const b = data?.billing || {};
161
+ let charged = num(b.chargedIsk);
162
+ const exact = charged != null;
163
+ if (charged == null && num(before?.balanceIsk) != null && num(after?.balanceIsk) != null) {
164
+ charged = Math.round((before.balanceIsk - after.balanceIsk) * 100) / 100;
165
+ }
166
+ const units = num(b.units);
167
+ const unitsText = units != null ? ` · ${formatInt(units)} einingar` : "";
168
+ const lines = [];
169
+ if (charged == null) lines.push(`- Þetta kall: kostnaður óþekktur${unitsText}`);
170
+ else if (charged < 0) lines.push(`- Þetta kall: ekki hægt að reikna (inneignin breyttist á meðan)${unitsText}`);
171
+ else if (charged === 0) lines.push(b.paidFrom === "credit"
172
+ ? `- Þetta kall: 0 kr. af inneign${unitsText}`
173
+ : `- Þetta kall: 0 kr., ókeypis dagskammtur${unitsText}`);
174
+ else lines.push(`- Þetta kall: ${formatIsk(charged)} af inneign${exact ? "" : " (mismunur á inneign fyrir og eftir)"}${unitsText}`);
175
+
176
+ const balance = num(b.balanceIsk) ?? num(after?.balanceIsk);
177
+ const left = num(b.freeUnitsLeftToday) ?? num(after?.freeUnitsLeftToday);
178
+ const daily = num(after?.freeDailyUnits) ?? num(before?.freeDailyUnits);
179
+ const parts = [];
180
+ if (balance != null) parts.push(`inneign ${formatIsk(balance)}`);
181
+ if (left != null && daily != null) parts.push(`ókeypis eftir í dag ${formatInt(left)} af ${formatInt(daily)} einingum`);
182
+ else if (left != null) parts.push(`ókeypis eftir í dag ${formatInt(left)} einingar`);
183
+ else if (daily != null && daily > 0) parts.push(`ókeypis ${formatInt(daily)} einingar á dag`);
184
+ if (parts.length) lines.push(`- Staða: ${parts.join(" · ")}`);
185
+ else lines.push("- Staða: náðist ekki að sækja (`npx altalandi whoami`)");
186
+ if (balance != null && balance < 50) lines.push("- Bæta við inneign: https://altalandi.is/portal");
187
+ return lines;
188
+ }
189
+
190
+ function clip(text, max) {
191
+ const s = String(text ?? "");
192
+ if (!max || s.length <= max) return { text: s, clipped: false };
193
+ const cut = s.lastIndexOf(" ", max);
194
+ return { text: s.slice(0, cut > max * 0.6 ? cut : max).trimEnd() + " …", clipped: true };
195
+ }
196
+
197
+ // A quoted line that starts with "#" or ">" would render as a heading or a
198
+ // nested quote; escape that first character.
199
+ const quote = (s) => String(s).split(/\r?\n/)
200
+ .map((l) => (l.trim() ? `> ${l.replace(/^(\s*)([#>])/, "$1\\$2")}` : ">")).join("\n");
201
+ const inline = (s) => String(s ?? "").replace(/\s+/g, " ").trim().replace(/([\\`*_~[\]<>|#])/g, "\\$1");
202
+
203
+ /**
204
+ * The block to show the user. `before`/`after` are /v1/me responses (either
205
+ * may be null). Long texts are shortened for the chat; `fullTextAt` names the
206
+ * file that holds the whole output.
207
+ */
208
+ export function reviewMarkdown(input, data, { before = null, after = null, maxChars = 1500, maxChanges = 30, fullTextAt = "" } = {}) {
209
+ const q = data?.quality || {};
210
+ const outcome = outcomeOf(data);
211
+ const diffs = Array.isArray(data?.diffs) ? data.diffs : [];
212
+ const score = q.nativenessScore != null ? ` · innfæddni ${q.nativenessScore}/100` : "";
213
+ const out = [`**Altalandi:** ${VERDICT[outcome]}${score}`, ""];
214
+
215
+ const inText = clip(input, maxChars);
216
+ out.push("**Fyrir**", quote(inText.text), "");
217
+ const same = String(data?.output ?? "").trim() === String(input ?? "").trim();
218
+ if (same) {
219
+ out.push("**Eftir**", outcome === "refused" ? "> Óbreytt: upprunalega textanum var skilað." : "> Óbreytt.", "");
220
+ } else {
221
+ const outText = clip(data?.output, maxChars);
222
+ out.push("**Eftir**", quote(outText.text), "");
223
+ if (outText.clipped || inText.clipped) {
224
+ out.push(`_Textinn er styttur hér${fullTextAt ? `; allur textinn er í ${fullTextAt}` : ""}._`, "");
225
+ }
226
+ }
227
+
228
+ if (outcome === "refused" && data?.rejectionReason) {
229
+ out.push(`**Af hverju óbreytt:** ${inline(rejectionText(data.rejectionReason))}`, "");
230
+ }
231
+
232
+ if (diffs.length) {
233
+ out.push(`**Breytingar (${diffs.length})**`);
234
+ diffs.slice(0, maxChanges).forEach((d, i) => {
235
+ out.push(`${i + 1}. „${inline(d.before)}“ → „${inline(d.after)}“`);
236
+ if (d.reason) out.push(` Ástæða: ${inline(d.reason)}`);
237
+ });
238
+ if (diffs.length > maxChanges) out.push(` … og ${diffs.length - maxChanges} í viðbót (sjá \`--report\`).`);
239
+ out.push("");
240
+ } else if (data?.diffStatus === "translation" || (data?.sourceLang === "en" && !same)) {
241
+ out.push("**Breytingar:** þýðing úr ensku, svo allur textinn er nýr.", "");
242
+ } else if (!same && data?.diffStatus === "unavailable") {
243
+ out.push("**Breytingar:** textinn breyttist en listi yfir breytingar fékkst ekki. Berðu saman fyrir og eftir.", "");
244
+ } else if (outcome !== "refused") {
245
+ out.push("**Breytingar:** engar.", "");
246
+ }
247
+
248
+ // The refusal reason is already shown above; its warning repeats it.
249
+ const code = REJECTION.find(([c]) => String(data?.rejectionReason ?? "").toUpperCase().startsWith(c))?.[0];
250
+ const warnings = (Array.isArray(data?.warnings) ? data.warnings : [])
251
+ .filter((w) => !String(w).startsWith("TEST KEY") && !(code && String(w).toUpperCase().startsWith(code)));
252
+ if (warnings.length && outcome !== "rewritten") {
253
+ out.push("**Athugasemdir**", ...warnings.slice(0, 3).map((w) => `- ${inline(w)}`), "");
254
+ }
255
+
256
+ const term = data?.terminology;
257
+ if (term?.dictionaries?.length) {
258
+ // Offered is not used: only a term verified in the output (by form or BÍN
259
+ // lemma) is "í textanum"; a stem-only match is "líklega"; an older engine
260
+ // without `usage` is listed as offered. The caller's glossary wins.
261
+ const usage = (x) => x.status === "overridden" ? "overridden"
262
+ : x.usage ?? (x.inOutput === true ? "verified" : x.inOutput === false ? "absent" : "unchecked");
263
+ const tag = { verified: "í textanum", probable: "líklega í textanum", absent: "ekki notað",
264
+ unchecked: "boðið", overridden: "þitt orðasafn gekk framar" };
265
+ const shown = (term.terms || []).filter((x) => !x.inText);
266
+ out.push(`**Íðorð:** ${inline(term.dictionaries.map((d) => d.name).join(", "))} (úr Íðorðabanka Árnastofnunar)`);
267
+ if (!shown.some((x) => x.status !== "overridden")) out.push("- Ekkert íðorð var boðið fyrir þennan texta.");
268
+ shown.slice(0, 12).forEach((x) => {
269
+ const u = usage(x);
270
+ const is = u === "verified" || u === "probable" ? (x.used ?? x.is) : x.is;
271
+ out.push(`- ${inline(x.source)} → ${inline(is)} (${tag[u]})`);
272
+ });
273
+ if (shown.length > 12) out.push(`- … og ${shown.length - 12} í viðbót`);
274
+ out.push("");
275
+ }
276
+
277
+ out.push("**Kostnaður**", ...costLines(data, before, after));
278
+ if (data?.id) out.push(`- Auðkenni: \`${data.id}\``);
279
+ return out.join("\n") + "\n";
280
+ }
package/skill/SKILL.md CHANGED
@@ -1,117 +1,93 @@
1
- ---
2
- name: altalandi
3
- description: >-
4
- Turn English or stiff, translated or AI-written Icelandic into natural native
5
- Icelandic through the Altalandi API, with a meaning check and a diff of every
6
- change. Use when the user mentions Altalandi, or asks for text in proper,
7
- natural, correct or native Icelandic ("á góðri íslensku", "á réttri íslensku",
8
- "láttu þetta hljóma eðlilega á íslensku", "translate this into proper
9
- Icelandic"), including website copy, UI strings, emails and documents meant for
10
- Icelandic readers. Not for incidental Icelandic in conversation.
11
- ---
12
-
13
- # Altalandi: native Icelandic, with a meaning check
14
-
15
- Altalandi rewrites English or stiff/AI-translated Icelandic into Icelandic that
16
- reads as if a native wrote it. It checks that the meaning survived (numbers,
17
- dates, names, negation, obligations and a back-translation) and returns every
18
- change with a reason. Each call is paid from the user's Altalandi credit.
19
-
20
- ## 1. Make sure this computer is connected (once)
21
-
22
- Everything goes through the `altalandi` CLI, which finds the user's key by itself.
23
- If a call prints `Not logged in` (exit code 2), connect the computer:
24
-
25
- 1. Run `npx -y altalandi@latest login` **in the background** (it waits up to 10
26
- minutes for the user).
27
- 2. Read its output as soon as it appears. It prints a link
28
- (`https://altalandi.is/tengja?kodi=XXXX-XXXX`) and a code, and opens the
29
- browser. Show the user the link and the code, and tell them to sign in or
30
- create an account (every account gets 2,000 free words a day, and new accounts 500 kr. of trial credit) and click
31
- **Tengja**.
32
- 3. Wait for the command to finish. `✓ Tengt sem <username>` means done. Then
33
- continue with the task.
34
-
35
- Never ask the user to paste an API key into the chat. If they have one and
36
- prefer it, they can set `ALTALANDI_API_KEY` themselves.
37
-
38
- ## 2. Refine text
39
-
40
- Write the text to a UTF-8 temporary file and pass the file. Never put the text on
41
- the command line: quotes, `$`, backticks and Icelandic characters break shell
42
- quoting.
43
-
44
- ```bash
45
- npx -y altalandi@latest --json --file /tmp/altalandi-in.txt
46
- ```
47
-
48
- Options: `--domain general|marketing|data-analysis|childrens-fiction`,
49
- `--register formal|neutral|warm`. Pick `marketing` for website and product copy,
50
- `formal` for official or legal text, `warm` for friendly customer messages.
51
-
52
- One call per coherent text (a page section, an email, one UI string). For many
53
- strings, run them one by one or at most 4 at a time, and keep keys, placeholders
54
- (`{name}`, `%s`), HTML and code exactly as they were: send only the human text.
55
-
56
- ## 3. Read the result correctly
57
-
58
- | Exit code | Meaning | What to do |
59
- |---|---|---|
60
- | 0 | done and verified | use `output`, show the diffs |
61
- | 3 | meaning not verified: JSON still printed | read the table below; don't present it as verified |
62
- | 4 | out of credit (`402 insufficient_balance`) | stop; tell the user to add credit at https://altalandi.is/portal. Don't retry |
63
- | 2 | not logged in / no input | step 1 |
64
- | 1 | other error (network, 429 usage limit) | on 429 wait for the time given and retry once; otherwise report it |
65
-
66
- For Icelandic input, one of three things happened. Tell them apart before using `output`:
67
-
68
- | Outcome | How to recognise it |
69
- |---|---|
70
- | **Rewritten and verified** | `output` differs from the input, `meaningPreserved: true`, `rejectionReason` empty |
71
- | **Rewrite refused, original returned** | `rewriteAttempted: true` and `rejectionReason` set (`MEANING DRIFT`, `HARD INVARIANT LOST`, `UNVERIFIED`, `CANDIDATE VETO`) |
72
- | **Already fine** | `rewriteAttempted: false` |
73
-
74
- - `meaningPreserved` alone is not enough: a vetoed rewrite returns the original with
75
- `meaningPreserved: true`. Check `rewriteAttempted` and `rejectionReason`.
76
- - `meaningPreserved: null` means unverified, not safe. For English input the
77
- translation still comes back; say it is unverified.
78
- - When the original comes back, say so. Don't claim Altalandi improved it.
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.
104
- - `quality.nativenessScore` is a hint, not a verdict. It has been confidently wrong
105
- in both directions; never use it to override a refusal.
106
-
107
- ## 5. Good to know
108
-
109
- - Altalandi rewrites for natural Icelandic. If the user only wants spelling and
110
- grammar fixed and every other word left alone, tell them it may rephrase, and
111
- review the diffs with them.
112
- - `npx -y altalandi@latest whoami` shows the connected account and its credit.
113
- - Leave code, identifiers, legal quotations and anything that must stay verbatim
114
- out of the text you send.
115
- - Don't use `--fast` for text people will read: it skips the meaning check.
116
- - Text is processed and discarded, not stored or used for training.
117
- - Docs for integrating the API into an app: https://altalandi.is/agents.md
1
+ ---
2
+ name: altalandi
3
+ description: >-
4
+ Turn English or stiff, translated or AI-written Icelandic into natural native
5
+ Icelandic through the Altalandi API, with a meaning check and a diff of every
6
+ change. Use when the user mentions Altalandi, or asks for text in proper,
7
+ natural, correct or native Icelandic ("á góðri íslensku", "á réttri íslensku",
8
+ "láttu þetta hljóma eðlilega á íslensku", "translate this into proper
9
+ Icelandic"), including website copy, UI strings, emails and documents meant for
10
+ Icelandic readers. Not for incidental Icelandic in conversation.
11
+ ---
12
+
13
+ # Altalandi: native Icelandic, with a meaning check
14
+
15
+ Altalandi rewrites English or stiff Icelandic so it reads as if a native wrote
16
+ it, checks that the meaning held, and lists every change with its reason. Each
17
+ call is paid from the user's free daily units or their credit.
18
+
19
+ **Follow this procedure exactly, every time.** The user must always see the same
20
+ thing: the verdict, the text before and after, each change with its reason, what
21
+ the call cost and what is left. Do not improvise scripts, parse the JSON
22
+ yourself or summarise the result in your own words.
23
+
24
+ ## The procedure
25
+
26
+ 1. **Save the text** to a UTF-8 temporary file, e.g. `altalandi-in.txt` in the
27
+ system temp folder. Never put the text on the command line.
28
+ 2. **Run one command** and wait for it to finish:
29
+
30
+ ```bash
31
+ npx -y altalandi@latest --review --file <temp>/altalandi-in.txt --out <temp>/altalandi-out.txt
32
+ ```
33
+
34
+ Add `--domain marketing` for website and product copy, `--register formal`
35
+ for official text, `--register warm` for friendly customer messages.
36
+ 3. **Paste its stdout into your reply exactly as printed**, as the first thing in
37
+ your reply. It is Markdown: a verdict line, **Fyrir**, **Eftir**,
38
+ **Breytingar** (each change and its reason), **Kostnaður** (cost of this call,
39
+ balance and free units left) and the request id. Do not shorten, reorder,
40
+ translate or reword it.
41
+ 4. **Then act on the exit code:**
42
+
43
+ | Exit | What happened | What you do |
44
+ |---|---|---|
45
+ | 0 | rewritten and verified, or no change needed | the result is in `altalandi-out.txt`. If the user asked you to update a file, apply it from there; otherwise ask whether to apply it |
46
+ | 3 | the rewrite was refused or the meaning could not be verified | keep the user's original text. Say in one sentence that Altalandi left it unchanged and why (the block says why). Do not apply anything |
47
+ | 4 | out of credit | stop. The block shows the balance and the link to add credit. Do not retry |
48
+ | 2 | not logged in | connect (below), then run step 2 again |
49
+ | 1 | other error (network, too many calls) | the block says what happened. For "too many calls", wait a minute and retry once |
50
+
51
+ 5. **Stop there.** After the block and the action, at most one or two sentences
52
+ of your own.
53
+
54
+ ## Rules
55
+
56
+ - **Never hand-edit the Icelandic and present it as Altalandi's.** If you think
57
+ something should still change, say so separately under a heading such as
58
+ "Mín tillaga (ekki yfirfarin af Altalandi)", or ask before running it again.
59
+ - **One call per coherent text** (a page section, an email, one UI string). For
60
+ many strings, run them one by one, at most 4 at a time, and paste each block.
61
+ Keep keys, placeholders (`{name}`, `%s`), HTML and code out of the text you send.
62
+ - **Product names and coined words** the meaning check cannot recognise can make
63
+ it refuse a rewrite (exit 3, "ekki tókst að staðfesta"). That is the safe
64
+ result: keep the original and tell the user.
65
+ - **A long text** is shortened in the block; the whole result is in
66
+ `altalandi-out.txt`. Use `--report` when the user wants a full record: it
67
+ writes `./altalandi-reports/altalandi-<time>-<id>.md`, and the block shows
68
+ its path. Reports contain the text, so write them only when asked.
69
+ - **Don't use `--fast`** for text people will read: it skips the meaning check.
70
+ - If the user only wants spelling fixed and every other word kept, tell them
71
+ Altalandi may rephrase, and go through the changes with them.
72
+ - Free use (the free daily units) is kept by Altalandi, with personal data
73
+ scrubbed, to improve the service; paid use is kept only with consent. Add
74
+ `--no-store` when the user asks that their text not be kept, or when it is
75
+ confidential.
76
+
77
+ ## Connect this computer (once)
78
+
79
+ Only when a call exits with 2 (`Not logged in`):
80
+
81
+ 1. Run `npx -y altalandi@latest login` **in the background**. It waits up to 10
82
+ minutes.
83
+ 2. As soon as it prints a link (`https://altalandi.is/tengja?kodi=XXXX-XXXX`) and
84
+ a code, show both to the user. Tell them to sign in or create an account
85
+ (2,000 free units a day, and new accounts get 500 kr. of trial credit) and
86
+ click **Tengja**.
87
+ 3. When it prints `✓ Tengt sem <username>`, run step 2 of the procedure again.
88
+
89
+ Never ask the user for an API key, and do not tell them to set
90
+ `ALTALANDI_API_KEY`. `npx -y altalandi@latest whoami` shows the account, the
91
+ balance and the free units at any time.
92
+
93
+ Docs for building the API into an app: https://altalandi.is/agents.md