altalandi 0.2.0 → 0.3.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 CHANGED
@@ -6,14 +6,18 @@ native wrote it—from your terminal or application. This package provides the
6
6
  [Altalandi API](https://altalandi.is) (Node ≥ 18).
7
7
 
8
8
  ```bash
9
- # Mint once at https://altalandi.is/portal, then enter it without shell history:
10
- read -rsp "Altalandi API key: " ALTALANDI_API_KEY; echo
11
- export ALTALANDI_API_KEY
9
+ npx altalandi login # once: opens altalandi.is, you sign in or sign up and click "Tengja"
12
10
  npx altalandi "Make sure to reach out to us today."
13
11
  # → Hafðu samband við okkur í dag.
14
12
  ```
15
13
 
16
- PowerShell 7: `$env:ALTALANDI_API_KEY = Read-Host "Altalandi API key" -MaskInput`.
14
+ `login` stores the key in your config directory (`%APPDATA%ltalandi` on
15
+ Windows, `~/.config/altalandi` elsewhere) and installs the Claude Code skill, so
16
+ afterwards you can just tell Claude *"write this in proper Icelandic with
17
+ Altalandi"*. `altalandi whoami` shows the account and credit; `altalandi logout`
18
+ forgets the key. On servers and in CI, set `ALTALANDI_API_KEY` instead; it takes
19
+ precedence over the stored login. For agents, `--file draft.txt` avoids shell
20
+ quoting entirely.
17
21
 
18
22
  `stdout` is the refined text (pipe it anywhere); `stderr` carries the 0–100
19
23
  nativeness score, the meaning verdict, every API warning, and the diff.
@@ -135,8 +139,8 @@ the explicit verification opt-out and exits 0 after a successful API response.
135
139
  | `ALTALANDI_API_KEY` | — | your key (Bearer). Mint at https://altalandi.is/portal |
136
140
  | `ALTALANDI_BASE_URL` | `https://altalandi.is/api` | API base (override for self-hosting) |
137
141
 
138
- MIT licensed. Portal keys share a daily account-level billable-word budget;
139
- operator-issued keys remain per-key, and key rotation does not reset account
140
- usage. The request's text and glossary fields are charged at the effective billing-tier
142
+ MIT licensed. Calls are paid from the account's prepaid credit (a 500 kr trial,
143
+ then credit bought in the portal); a call the credit cannot cover exits with the
144
+ server's `402 insufficient_balance` message. The request's text and glossary fields are charged at the effective billing-tier
141
145
  weight. Each whitespace-delimited token is at least one unit; an unbroken token
142
146
  uses one unit per 20 characters, rounded up.
package/altalandi.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // altalandi — turn English or stiff/AI Icelandic into native Icelandic from
3
3
  // your terminal. Zero dependencies (Node >= 18 has global fetch).
4
4
  //
5
- // export ALTALANDI_API_KEY=... # get one at https://altalandi.is
5
+ // npx altalandi login # once: connects this computer to your account
6
6
  // altalandi "Make sure to reach out to us today."
7
7
  // echo "some text" | altalandi --domain marketing
8
8
  //
@@ -10,6 +10,7 @@
10
10
  // meaning verdict, API warnings, and the diff.
11
11
 
12
12
  import { refine } from "./client.mjs";
13
+ import { readCredentials, resolveKey, runLogin, runLogout, runWhoami } from "./auth.mjs";
13
14
 
14
15
  const args = process.argv.slice(2);
15
16
 
@@ -32,6 +33,22 @@ if (args[0] === "update") {
32
33
  skillOnly: rest.includes("--skill-only"),
33
34
  });
34
35
  }
36
+ if (!handled && ["login", "setup", "logout", "whoami"].includes(args[0])) {
37
+ handled = true;
38
+ const rest = args.slice(1);
39
+ const flag = (name) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; };
40
+ if (args[0] === "login" || args[0] === "setup") {
41
+ process.exitCode = await runLogin({
42
+ noBrowser: rest.includes("--no-browser"),
43
+ noSkill: rest.includes("--no-skill"),
44
+ baseUrl: flag("--base-url"),
45
+ });
46
+ } else if (args[0] === "logout") {
47
+ process.exitCode = await runLogout();
48
+ } else {
49
+ process.exitCode = await runWhoami({ json: rest.includes("--json"), baseUrl: flag("--base-url") });
50
+ }
51
+ }
35
52
  if (!handled && (args.includes("-v") || args.includes("--version"))) {
36
53
  const { currentVersion } = await import("./update.mjs");
37
54
  console.log(await currentVersion());
@@ -65,6 +82,7 @@ for (let i = 0; i < args.length; i++) {
65
82
  else if (a === "--register") opts.register = args[++i];
66
83
  else if (a === "--base-url") opts.baseUrl = args[++i];
67
84
  else if (a === "--key") opts.key = args[++i];
85
+ else if (a === "--file") opts.file = args[++i];
68
86
  else positional.push(a);
69
87
  }
70
88
 
@@ -72,11 +90,17 @@ function help() {
72
90
  console.log(`altalandi — native Icelandic from your terminal
73
91
 
74
92
  Usage:
93
+ altalandi login connect this computer to your account (once)
75
94
  altalandi "text to refine"
95
+ altalandi --file draft.txt read the text from a file (best for agents)
76
96
  echo "text" | altalandi
77
97
  altalandi --domain marketing --register warm "text"
78
98
 
79
99
  Commands:
100
+ altalandi login open altalandi.is, sign in or sign up, approve; stores the key
101
+ and installs the Claude Code skill (--no-skill, --no-browser)
102
+ altalandi whoami show the account and balance behind the stored key
103
+ altalandi logout forget the stored key on this computer
80
104
  altalandi update check for a newer version and update if you agree
81
105
  altalandi --version print the installed version
82
106
 
@@ -86,6 +110,7 @@ Options:
86
110
  --fast no polish, no nativeness score, no meaning gate — normally one writer call
87
111
  (structured input can still add a structure-preserving retry)
88
112
  --json print the full JSON response
113
+ --file read the text from a UTF-8 file instead of an argument
89
114
  --base-url API base (default $ALTALANDI_BASE_URL or https://altalandi.is/api)
90
115
 
91
116
  stdout = refined text; stderr = nativeness / meaning / warnings / diff.
@@ -102,16 +127,23 @@ function readStdin() {
102
127
  });
103
128
  }
104
129
 
105
- const baseUrl = (opts.baseUrl || process.env.ALTALANDI_BASE_URL || "https://altalandi.is/api").replace(/\/$/, "");
106
- const key = opts.key || process.env.ALTALANDI_API_KEY || process.env.ALTALANDI_KEY;
130
+ const stored = readCredentials();
131
+ const baseUrl = (opts.baseUrl || process.env.ALTALANDI_BASE_URL || stored?.baseUrl || "https://altalandi.is/api").replace(/\/$/, "");
132
+ const { key } = resolveKey({ flag: opts.key });
107
133
 
108
134
  if (opts.key) {
109
135
  console.error("⚠ --key is deprecated because command arguments can leak through shell history/process listings; load ALTALANDI_API_KEY instead.");
110
136
  }
111
137
 
112
- const text = (positional.join(" ").trim()) || (await readStdin()).trim();
138
+ let text = positional.join(" ").trim();
139
+ if (opts.file) {
140
+ const { readFile } = await import("node:fs/promises");
141
+ try { text = (await readFile(opts.file, "utf8")).replace(/^/, "").trim(); }
142
+ catch (e) { console.error(`Could not read ${opts.file}: ${e.message}`); process.exit(2); }
143
+ }
144
+ if (!text) text = (await readStdin()).trim();
113
145
  if (!text) { console.error("No input. Pass text as an argument or pipe via stdin. (altalandi --help)"); process.exit(2); }
114
- if (!key) { console.error("No API key. Load ALTALANDI_API_KEY securely (masked setup: https://altalandi.is/portal)."); process.exit(2); }
146
+ if (!key) { console.error("Not logged in. Run once: npx altalandi login (or set ALTALANDI_API_KEY)"); process.exit(2); }
115
147
 
116
148
  const requestOptions = {
117
149
  apiKey: key,
@@ -164,6 +196,8 @@ try {
164
196
  if (verificationFailed) process.exitCode = 3;
165
197
  } catch (e) {
166
198
  console.error("Request failed:", e.message);
167
- process.exit(1);
199
+ if (e.status === 402) console.error("→ Add credit at https://altalandi.is/portal (automatic top-up is available there).");
200
+ if (e.status === 401) console.error("→ The key is invalid or revoked. Run: npx altalandi login");
201
+ process.exit(e.status === 402 ? 4 : 1);
168
202
  }
169
203
  }
package/auth.mjs ADDED
@@ -0,0 +1,215 @@
1
+ // `altalandi login` — connect this computer to an Altalandi account once.
2
+ //
3
+ // Device login, the same pattern as `gh auth login`: the CLI asks the API for a
4
+ // short code, the user opens altalandi.is/tengja, signs in or creates an
5
+ // account, and approves. The CLI then receives a key for that account exactly
6
+ // once and stores it in the user's config directory. The key never passes
7
+ // through a chat, a prompt, the clipboard or shell history.
8
+ //
9
+ // Afterwards every `altalandi` call finds the key on its own, so an agent (the
10
+ // Claude Code skill, a script) needs no environment variable at all.
11
+ //
12
+ // Zero dependencies, like the rest of the CLI.
13
+
14
+ import { spawn } from "node:child_process";
15
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
16
+ import { existsSync, readFileSync } from "node:fs";
17
+ import { homedir, hostname } from "node:os";
18
+ import path from "node:path";
19
+
20
+ import { skillStatus, skillTarget, writeSkill } from "./update.mjs";
21
+
22
+ export const DEFAULT_BASE_URL = "https://altalandi.is/api";
23
+
24
+ /** Where the key is stored. ALTALANDI_CONFIG_DIR overrides (tests, CI). */
25
+ export function configDir({ env = process.env, home = homedir(), platform = process.platform } = {}) {
26
+ if (env.ALTALANDI_CONFIG_DIR) return env.ALTALANDI_CONFIG_DIR;
27
+ if (platform === "win32") return path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "altalandi");
28
+ return path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "altalandi");
29
+ }
30
+
31
+ export function credentialsFile(opts) {
32
+ return path.join(configDir(opts), "credentials.json");
33
+ }
34
+
35
+ /** The stored credentials, or null. Never throws. */
36
+ export function readCredentials(opts) {
37
+ try {
38
+ const data = JSON.parse(readFileSync(credentialsFile(opts), "utf8"));
39
+ return data && typeof data.apiKey === "string" && data.apiKey ? data : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /** Key resolution order: explicit flag, ALTALANDI_API_KEY, stored login. */
46
+ export function resolveKey({ flag, env = process.env, ...opts } = {}) {
47
+ if (flag) return { key: flag, source: "flag" };
48
+ const fromEnv = env.ALTALANDI_API_KEY || env.ALTALANDI_KEY;
49
+ if (fromEnv) return { key: fromEnv, source: "env" };
50
+ const stored = readCredentials({ env, ...opts });
51
+ return stored ? { key: stored.apiKey, source: "login", username: stored.username } : { key: "", source: "none" };
52
+ }
53
+
54
+ async function saveCredentials(data, opts) {
55
+ const dir = configDir(opts);
56
+ await mkdir(dir, { recursive: true });
57
+ const file = credentialsFile(opts);
58
+ await writeFile(file, JSON.stringify(data, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
59
+ try { await chmod(file, 0o600); } catch { /* Windows: the per-user profile directory is the protection */ }
60
+ return file;
61
+ }
62
+
63
+ function clientName(env = process.env) {
64
+ const where = (hostname() || "").split(".")[0].slice(0, 24);
65
+ const who = env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT ? "Claude Code" : "altalandi CLI";
66
+ return where ? `${who} á ${where}` : who;
67
+ }
68
+
69
+ function openBrowser(url) {
70
+ try {
71
+ const [cmd, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]]
72
+ : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
73
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true });
74
+ child.on("error", () => {});
75
+ child.unref();
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
83
+
84
+ async function postJson(fetchImpl, url, body) {
85
+ const res = await fetchImpl(url, {
86
+ method: "POST",
87
+ headers: { "content-type": "application/json", accept: "application/json" },
88
+ body: JSON.stringify(body),
89
+ });
90
+ let data = {};
91
+ try { data = await res.json(); } catch { /* keep {} */ }
92
+ return { status: res.status, data };
93
+ }
94
+
95
+ /**
96
+ * Run the device login. Returns an exit code.
97
+ * 0 logged in · 1 failed or expired · 2 bad usage
98
+ */
99
+ export async function runLogin(flags = {}, io = {}) {
100
+ const env = io.env || process.env;
101
+ const fetchImpl = io.fetch || globalThis.fetch;
102
+ const log = io.log || ((m) => console.error(m));
103
+ const base = (flags.baseUrl || env.ALTALANDI_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
104
+ const wait = io.sleep || sleep;
105
+
106
+ let start;
107
+ try {
108
+ start = await postJson(fetchImpl, `${base}/v1/device/start`, { clientName: clientName(env) });
109
+ } catch (e) {
110
+ log(`Náði ekki sambandi við Altalandi (${e.message}).`);
111
+ return 1;
112
+ }
113
+ if (start.status !== 200 || !start.data.deviceCode) {
114
+ log(`Innskráning fór ekki af stað: ${start.data.detail || `HTTP ${start.status}`}`);
115
+ return 1;
116
+ }
117
+ const { deviceCode, userCode, verificationUriComplete, verificationUri, interval = 3, expiresIn = 600 } = start.data;
118
+
119
+ log("");
120
+ log(" Tengdu þessa tölvu við Altalandi-aðganginn þinn:");
121
+ log("");
122
+ log(` 1. Opnaðu ${verificationUriComplete}`);
123
+ log(` 2. Skráðu þig inn eða stofnaðu aðgang (500 kr. prufuinneign fylgir)`);
124
+ log(` 3. Staðfestu kóðann ${userCode}`);
125
+ log("");
126
+ log(` (Connect this computer: open the link, sign in or sign up, confirm the code.`);
127
+ log(` Code page: ${verificationUri})`);
128
+ log("");
129
+ if (!flags.noBrowser && !io.noBrowser) openBrowser(verificationUriComplete);
130
+ log(" Bíð eftir staðfestingu…");
131
+
132
+ const deadline = Date.now() + expiresIn * 1000;
133
+ let result = null;
134
+ while (Date.now() < deadline) {
135
+ await wait(Math.max(1, interval) * 1000);
136
+ let poll;
137
+ try {
138
+ poll = await postJson(fetchImpl, `${base}/v1/device/token`, { deviceCode });
139
+ } catch {
140
+ continue; // transient network error: keep waiting until the code expires
141
+ }
142
+ if (poll.status === 200 && poll.data.status === "approved") { result = poll.data; break; }
143
+ if (poll.status === 200 && poll.data.status === "pending") continue;
144
+ if (poll.status === 429) { await wait(10_000); continue; }
145
+ log(` Kóðinn rann út eða var hafnað. Keyrðu \`altalandi login\` aftur.`);
146
+ return 1;
147
+ }
148
+ if (!result) {
149
+ log(` Kóðinn rann út. Keyrðu \`altalandi login\` aftur.`);
150
+ return 1;
151
+ }
152
+
153
+ const file = await saveCredentials({
154
+ apiKey: result.apiKey,
155
+ username: result.username,
156
+ baseUrl: base === DEFAULT_BASE_URL ? undefined : base,
157
+ savedAt: new Date().toISOString(),
158
+ }, { env });
159
+ log("");
160
+ log(` ✓ Tengt sem ${result.username}. Lykillinn er vistaður í ${file}`);
161
+
162
+ // Install or refresh the Claude Code skill — the point of logging in for most
163
+ // people. Only where Claude Code exists; never create ~/.claude for someone who
164
+ // does not use it. --no-skill opts out.
165
+ if (!flags.noSkill) {
166
+ const target = skillTarget({ env });
167
+ const status = await skillStatus({ target });
168
+ if (status === "missing" || status === "outdated") {
169
+ const { file: skillFile, backup } = await writeSkill({ target });
170
+ log(` ✓ Claude Code-hæfnin „altalandi" sett upp: ${skillFile}${backup ? ` (eldri útgáfa afrituð í ${backup})` : ""}`);
171
+ } else if (status === "current") {
172
+ log(` ✓ Claude Code-hæfnin „altalandi" er þegar uppfærð.`);
173
+ } else if (status === "absent") {
174
+ log(` (Claude Code fannst ekki á þessari tölvu, svo hæfnin var ekki sett upp.)`);
175
+ }
176
+ }
177
+ log("");
178
+ log(` Prófaðu: altalandi "Make sure to reach out to us today."`);
179
+ log(` Eða segðu Claude: „Skrifaðu þetta á góðri íslensku með Altalandi."`);
180
+ return 0;
181
+ }
182
+
183
+ export async function runLogout(io = {}) {
184
+ const env = io.env || process.env;
185
+ const log = io.log || ((m) => console.error(m));
186
+ const file = credentialsFile({ env });
187
+ if (!existsSync(file)) { log("Engin vistuð innskráning."); return 0; }
188
+ await rm(file, { force: true });
189
+ log(`Innskráning fjarlægð (${file}). Lykillinn sjálfur er enn virkur þar til þú afturkallar hann á https://altalandi.is/portal`);
190
+ return 0;
191
+ }
192
+
193
+ export async function runWhoami(flags = {}, io = {}) {
194
+ const env = io.env || process.env;
195
+ const fetchImpl = io.fetch || globalThis.fetch;
196
+ const out = io.out || ((m) => console.log(m));
197
+ const log = io.log || ((m) => console.error(m));
198
+ const { key, source } = resolveKey({ flag: flags.key, env });
199
+ if (!key) { log("Ekki innskráð(ur). Keyrðu: npx altalandi login"); return 2; }
200
+ const stored = readCredentials({ env });
201
+ const base = (flags.baseUrl || env.ALTALANDI_BASE_URL || stored?.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
202
+ let res;
203
+ try {
204
+ res = await fetchImpl(`${base}/v1/me`, { headers: { authorization: `Bearer ${key}`, accept: "application/json" } });
205
+ } catch (e) {
206
+ log(`Náði ekki sambandi við Altalandi (${e.message}).`);
207
+ return 1;
208
+ }
209
+ if (res.status === 401) { log("Lykillinn er ógildur eða afturkallaður. Keyrðu: npx altalandi login"); return 1; }
210
+ if (!res.ok) { log(`Villa: HTTP ${res.status}`); return 1; }
211
+ const me = await res.json();
212
+ if (flags.json) out(JSON.stringify({ ...me, keySource: source }, null, 2));
213
+ else out(`${me.username ?? "(lykill án aðgangs)"} · inneign ${me.balanceIsk ?? "—"} kr. · lykill ${me.keyId} (${source})`);
214
+ return 0;
215
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "altalandi",
3
- "version": "0.2.0",
4
- "description": "Hosted Altalandi API client and CLI for native-sounding Icelandic with meaning verification.",
3
+ "version": "0.3.0",
4
+ "description": "Native-sounding Icelandic from your terminal or AI agent: one-time login, a Claude Code skill, and meaning verification.",
5
5
  "type": "module",
6
6
  "main": "./client.mjs",
7
7
  "types": "./client.d.ts",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "altalandi.mjs",
20
+ "auth.mjs",
20
21
  "update.mjs",
21
22
  "skill/SKILL.md",
22
23
  "client.mjs",
@@ -25,7 +26,7 @@
25
26
  "LICENSE"
26
27
  ],
27
28
  "scripts": {
28
- "test": "node --test test/cli.test.mjs test/client.test.mjs test/update.test.mjs"
29
+ "test": "node --test test/cli.test.mjs test/client.test.mjs test/update.test.mjs test/auth.test.mjs"
29
30
  },
30
31
  "keywords": [
31
32
  "icelandic",
package/skill/SKILL.md CHANGED
@@ -1,105 +1,95 @@
1
- ---
2
- name: altalandi
3
- description: >-
4
- MANUAL COMMAND — only run when the user explicitly invokes /altalandi. Do NOT
5
- trigger automatically, and do not use it proactively on Icelandic writing tasks.
6
- When invoked: refine English or stiff/AI-translated Icelandic into native
7
- Icelandic via the Altalandi API, with a meaning-preservation check.
8
- ---
9
-
10
- # Altalandi — native Icelandic, with a meaning guarantee
11
-
12
- Altalandi is a guarded pipeline, not a single model call: a frontier LLM rewrites
13
- to native register, a rule proofreader cleans up, then a validation gate checks
14
- meaning (back-translation **plus** deterministic invariants — numbers, dates,
15
- negation, legal force, names) and a **candidate veto** refuses any rewrite the
16
- pipeline itself has flagged as broken (invented words, a quoted defect, mojibake).
17
- Every change comes back as a diff with the reason.
18
-
19
- ## How to call it
20
-
21
- When the user runs `/altalandi`, refine the target text via the API.
22
-
23
- 1. **CLI, always with `--json`** — `npx altalandi --json "<text>"`. Flags:
24
- `--domain`, `--register`. Needs `ALTALANDI_API_KEY` in the env.
25
- **Use `--json` from an agent.** When a rewrite is refused, the plain-text mode
26
- prints the reason as a `⚠` warning on stderr, but it also prints "meaning
27
- preserved" and exits 0 — easy for an agent to misread as success. The JSON
28
- carries `rewriteAttempted` and `rejectionReason`, which are unambiguous.
29
- Run `npx altalandi update` now and then to get the latest CLI and this skill.
30
- 2. **HTTP** — `POST https://altalandi.is/api/v1/refine` with
31
- `Authorization: Bearer $ALTALANDI_API_KEY` and body `{ "text": "...", ... }`.
32
-
33
- (Do NOT rely on an MCP tool here — an MCP `refine_icelandic` tool is always available
34
- to the model, which defeats manual-only invocation. This command calls the CLI/HTTP
35
- directly so it only runs when you ask.)
36
-
37
- Auth: set **`ALTALANDI_API_KEY`** once in the shell env (or your project's `.env`).
38
- Existing `rtv_…` keys keep working — there is no rotation. Mint or view keys at
39
- https://altalandi.is/portal. Machine-readable docs: https://altalandi.is/llms-full.txt
40
- and https://altalandi.is/api/openapi.json.
41
-
42
- ## Parameters
43
-
44
- - `text` (required) — the text to refine (English or Icelandic; auto-detected).
45
- - `domain` — `general` | `marketing` | `data-analysis` | `childrens-fiction`.
46
- - `register` — `formal` | `neutral` | `warm`.
47
- - `sourceLang` — `auto` (default) | `en` | `is`.
48
-
49
- ## The non-negotiable contract — read the outcome, not one field
50
-
51
- An Icelandic→Icelandic call ends in one of three ways. Tell them apart before you
52
- use the output:
53
-
54
- | outcome | how to recognise it |
55
- |---|---|
56
- | **Rewritten and verified** | `output` differs from your text, `meaningPreserved: true`, `rejectionReason` empty |
57
- | **Rewrite refused — original returned** | `rewriteAttempted: true` and `rejectionReason` set. Causes: `MEANING DRIFT`, `HARD INVARIANT LOST`, `UNVERIFIED`, or `CANDIDATE VETO` (then `candidateVetoFired: true`, `candidateVetoSignal` says why) |
58
- | **Already fine — nothing to change** | `rewriteAttempted: false` |
59
-
60
- - **`meaningPreserved` alone is not enough.** A vetoed rewrite returns your
61
- original with `meaningPreserved` still `true`: the meaning check passed, but the
62
- rewrite was broken in another way. Check `rewriteAttempted` and `rejectionReason`.
63
- - `meaningPreserved: null` means **unverified**, not safe. For Icelandic input the
64
- original comes back; for English input the translation comes back marked
65
- unverified — do not present it as verified.
66
- - **When the original comes back, say so.** Don't report "Altalandi improved
67
- this" when it declined to.
68
- - **Show the diffs.** When you apply Altalandi's output, surface what changed and
69
- why, never silently replace the user's text.
70
-
71
- ## The nativeness score is a hint, not a verdict
72
-
73
- `quality.nativenessScore` and `translationeseTells` point at likely problems, but
74
- the score has been measured to be confidently wrong in both directions: it gave a
75
- rewrite containing an invented word 82/100, and a perfectly fine rewrite 18/100.
76
- Treat a low score as "look at the tells", never as proof of bad Icelandic — and
77
- never as permission to override a refusal. A tell that **quotes** a specific phrase
78
- is far more trustworthy than the number.
79
-
80
- ## When to use it
81
-
82
- - Producing or polishing user-facing Icelandic (web copy, UI strings, emails,
83
- product text, children's prose).
84
- - You translated something to Icelandic and want it to not "sound translated".
85
- - Many strings at once: `POST /refine/batch`.
86
-
87
- ## When NOT to use it
88
-
89
- - Non-Icelandic output. Code, identifiers, or data that must stay literal. Quotes
90
- you must preserve verbatim (it may rephrase — check the diff).
91
-
92
- ## `--fast` removes the safety net
93
-
94
- `--fast` (`scoreNativeness:false`, `scoring:"none"`) skips the meaning gate **and**
95
- leaves the candidate veto blind: with no BÍN morphology check and no nativeness
96
- tells, it cannot catch an invented or wrong word. Only mojibake is still caught.
97
- Use it only for throwaway text you will read yourself — never for anything shipped.
98
-
99
- ## Notes
100
-
101
- - Usage is metered by a daily **word budget** per key (default 10,000 words/day;
102
- 429 when spent, resets 00:00 UTC).
103
- - A full call makes several model passes and is slower; that is the price of the
104
- checks above.
105
- - Data is processed and discarded — not stored or trained on.
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 (new accounts get 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 (`diffs`: before, after, reason). Never silently replace
80
+ the user's text. Apply changes to files only after showing them, unless the user
81
+ asked for that directly.
82
+ - `quality.nativenessScore` is a hint, not a verdict. It has been confidently wrong
83
+ in both directions; never use it to override a refusal.
84
+
85
+ ## 4. Good to know
86
+
87
+ - Altalandi rewrites for natural Icelandic. If the user only wants spelling and
88
+ grammar fixed and every other word left alone, tell them it may rephrase, and
89
+ review the diffs with them.
90
+ - `npx -y altalandi@latest whoami` shows the connected account and its credit.
91
+ - Leave code, identifiers, legal quotations and anything that must stay verbatim
92
+ out of the text you send.
93
+ - Don't use `--fast` for text people will read: it skips the meaning check.
94
+ - Text is processed and discarded, not stored or used for training.
95
+ - Docs for integrating the API into an app: https://altalandi.is/agents.md