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 +11 -7
- package/altalandi.mjs +40 -6
- package/auth.mjs +215 -0
- package/package.json +4 -3
- package/skill/SKILL.md +95 -105
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
|
-
#
|
|
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
|
-
|
|
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.
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
//
|
|
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
|
|
106
|
-
const
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Icelandic
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
and
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|