altalandi 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/altalandi.mjs +274 -223
- package/auth.mjs +223 -215
- package/client.d.ts +117 -113
- package/client.mjs +209 -208
- package/package.json +2 -2
- package/report.mjs +280 -114
- package/skill/SKILL.md +93 -117
package/client.mjs
CHANGED
|
@@ -1,208 +1,209 @@
|
|
|
1
|
-
const DEFAULT_BASE_URL = "https://altalandi.is/api";
|
|
2
|
-
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
3
|
-
const REQUEST_FIELDS = [
|
|
4
|
-
"sourceLang",
|
|
5
|
-
"domain",
|
|
6
|
-
"register",
|
|
7
|
-
"preserveMeaning",
|
|
8
|
-
"scoreNativeness",
|
|
9
|
-
"onlyImprove",
|
|
10
|
-
"polishPass",
|
|
11
|
-
"writer",
|
|
12
|
-
"scoring",
|
|
13
|
-
"glossary",
|
|
14
|
-
"returnDiffs",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
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
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
1
|
+
const DEFAULT_BASE_URL = "https://altalandi.is/api";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
3
|
+
const REQUEST_FIELDS = [
|
|
4
|
+
"sourceLang",
|
|
5
|
+
"domain",
|
|
6
|
+
"register",
|
|
7
|
+
"preserveMeaning",
|
|
8
|
+
"scoreNativeness",
|
|
9
|
+
"onlyImprove",
|
|
10
|
+
"polishPass",
|
|
11
|
+
"writer",
|
|
12
|
+
"scoring",
|
|
13
|
+
"glossary",
|
|
14
|
+
"returnDiffs",
|
|
15
|
+
"contribute",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export class AltalandiError extends Error {
|
|
19
|
+
constructor(message, { code = "ALTALANDI_ERROR", status } = {}) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "AltalandiError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
if (Number.isInteger(status)) this.status = status;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class MeaningVerificationError extends AltalandiError {
|
|
28
|
+
constructor(verdict) {
|
|
29
|
+
const changed = verdict === false;
|
|
30
|
+
super(
|
|
31
|
+
changed
|
|
32
|
+
? "Altalandi could not preserve the input meaning."
|
|
33
|
+
: "Altalandi did not return a verified meaning verdict.",
|
|
34
|
+
{ code: changed ? "MEANING_NOT_PRESERVED" : "MEANING_UNVERIFIED" },
|
|
35
|
+
);
|
|
36
|
+
this.name = "MeaningVerificationError";
|
|
37
|
+
this.verdict = verdict === false ? false : null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function env(name) {
|
|
42
|
+
if (typeof process === "undefined" || !process.env) return undefined;
|
|
43
|
+
return process.env[name];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cleanText(value, secret = "") {
|
|
47
|
+
if (typeof value !== "string") return "";
|
|
48
|
+
let cleaned = value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
49
|
+
if (secret) cleaned = cleaned.split(secret).join("[redacted]");
|
|
50
|
+
return cleaned.slice(0, 300);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function errorDetail(payload) {
|
|
54
|
+
const detail = payload?.detail;
|
|
55
|
+
if (typeof detail === "string") return detail;
|
|
56
|
+
if (Array.isArray(detail)) {
|
|
57
|
+
return detail
|
|
58
|
+
.map((entry) => (typeof entry?.msg === "string" ? entry.msg : ""))
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join("; ");
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requestBody(text, options) {
|
|
66
|
+
const body = { text };
|
|
67
|
+
for (const field of REQUEST_FIELDS) {
|
|
68
|
+
if (options[field] !== undefined) body[field] = options[field];
|
|
69
|
+
}
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Refine English or Icelandic text through the hosted Altalandi API.
|
|
75
|
+
* Client settings (`apiKey`, `baseUrl`, `timeoutMs`, `fetch`) are never sent in
|
|
76
|
+
* the JSON body. The full API result is returned without weakening its verdict.
|
|
77
|
+
*/
|
|
78
|
+
export async function refine(text, options = {}) {
|
|
79
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
80
|
+
throw new AltalandiError("Altalandi requires non-empty text.", { code: "INVALID_INPUT" });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const explicitKey = options.apiKey;
|
|
84
|
+
const selectedKey = explicitKey === undefined ? env("ALTALANDI_API_KEY") : explicitKey;
|
|
85
|
+
if (selectedKey !== undefined && typeof selectedKey !== "string") {
|
|
86
|
+
throw new AltalandiError("Altalandi apiKey must be a string.", { code: "INVALID_CONFIG" });
|
|
87
|
+
}
|
|
88
|
+
const apiKey = selectedKey?.trim();
|
|
89
|
+
if (!apiKey) {
|
|
90
|
+
throw new AltalandiError(
|
|
91
|
+
"No Altalandi API key. Set ALTALANDI_API_KEY or pass apiKey explicitly.",
|
|
92
|
+
{ code: "MISSING_API_KEY" },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const selectedBaseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
97
|
+
if (typeof selectedBaseUrl !== "string") {
|
|
98
|
+
throw new AltalandiError("Altalandi baseUrl must be a string.", { code: "INVALID_CONFIG" });
|
|
99
|
+
}
|
|
100
|
+
const baseUrl = selectedBaseUrl.trim().replace(/\/+$/, "");
|
|
101
|
+
if (!baseUrl) {
|
|
102
|
+
throw new AltalandiError("Altalandi baseUrl must not be empty.", { code: "INVALID_CONFIG" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
106
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
107
|
+
throw new AltalandiError("Altalandi timeoutMs must be a positive number.", {
|
|
108
|
+
code: "INVALID_CONFIG",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
113
|
+
if (typeof fetchImpl !== "function") {
|
|
114
|
+
throw new AltalandiError("This runtime does not provide fetch.", { code: "INVALID_CONFIG" });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
119
|
+
try {
|
|
120
|
+
let response;
|
|
121
|
+
try {
|
|
122
|
+
response = await fetchImpl(`${baseUrl}/v1/refine`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"content-type": "application/json",
|
|
126
|
+
authorization: `Bearer ${apiKey}`,
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify(requestBody(text, options)),
|
|
129
|
+
signal: controller.signal,
|
|
130
|
+
});
|
|
131
|
+
} catch {
|
|
132
|
+
if (controller.signal.aborted) {
|
|
133
|
+
throw new AltalandiError(`Altalandi timed out after ${timeoutMs} ms.`, {
|
|
134
|
+
code: "TIMEOUT",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
throw new AltalandiError("Altalandi could not reach the API.", { code: "NETWORK_ERROR" });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let raw = "";
|
|
141
|
+
try {
|
|
142
|
+
raw = await response.text();
|
|
143
|
+
} catch {
|
|
144
|
+
if (controller.signal.aborted) {
|
|
145
|
+
throw new AltalandiError(`Altalandi timed out after ${timeoutMs} ms.`, {
|
|
146
|
+
code: "TIMEOUT",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (response.ok) {
|
|
150
|
+
throw new AltalandiError("Altalandi could not read the API response.", {
|
|
151
|
+
code: "INVALID_RESPONSE",
|
|
152
|
+
status: response.status,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// The HTTP status remains useful when an error response has no readable body.
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let payload;
|
|
159
|
+
if (raw) {
|
|
160
|
+
try {
|
|
161
|
+
payload = JSON.parse(raw);
|
|
162
|
+
} catch {
|
|
163
|
+
if (response.ok) {
|
|
164
|
+
throw new AltalandiError("Altalandi returned an invalid JSON response.", {
|
|
165
|
+
code: "INVALID_RESPONSE",
|
|
166
|
+
status: response.status,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const status = Number.isInteger(response.status) ? response.status : 0;
|
|
174
|
+
const statusText = cleanText(response.statusText);
|
|
175
|
+
const detail = cleanText(errorDetail(payload), apiKey);
|
|
176
|
+
const suffix = detail || statusText;
|
|
177
|
+
throw new AltalandiError(
|
|
178
|
+
`Altalandi API request failed (${status || "unknown status"})${suffix ? `: ${suffix}` : "."}`,
|
|
179
|
+
{ code: "API_ERROR", status },
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload) || typeof payload.output !== "string") {
|
|
184
|
+
throw new AltalandiError("Altalandi returned an invalid response shape.", {
|
|
185
|
+
code: "INVALID_RESPONSE",
|
|
186
|
+
status: response.status,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return payload;
|
|
191
|
+
} finally {
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Assert that an existing result carries a completed, positive meaning check. */
|
|
197
|
+
export function assertMeaningVerified(result) {
|
|
198
|
+
if (result?.meaningPreserved !== true) {
|
|
199
|
+
throw new MeaningVerificationError(result?.meaningPreserved);
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Request meaning verification and reject unless the API returns strict true. */
|
|
205
|
+
export async function refineVerified(text, options = {}) {
|
|
206
|
+
const result = await refine(text, { ...options, preserveMeaning: true });
|
|
207
|
+
assertMeaningVerified(result);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "altalandi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"LICENSE"
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node --test test/cli.test.mjs test/client.test.mjs test/update.test.mjs test/auth.test.mjs"
|
|
30
|
+
"test": "node --test test/cli.test.mjs test/client.test.mjs test/update.test.mjs test/auth.test.mjs test/report.test.mjs"
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"icelandic",
|