altalandi 0.6.2 → 0.6.3

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/client.mjs CHANGED
@@ -1,258 +1,260 @@
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
- // /v1/refine/batch takes fewer options: no writer/scoring tiers, polish or diffs switch.
18
- const BATCH_REQUEST_FIELDS = [
19
- "sourceLang",
20
- "domain",
21
- "register",
22
- "preserveMeaning",
23
- "scoreNativeness",
24
- "onlyImprove",
25
- "glossary",
26
- "autoTerminology",
27
- "contribute",
28
- ];
29
-
30
- export class AltalandiError extends Error {
31
- constructor(message, { code = "ALTALANDI_ERROR", status, retryAfterMs } = {}) {
32
- super(message);
33
- this.name = "AltalandiError";
34
- this.code = code;
35
- if (Number.isInteger(status)) this.status = status;
36
- if (Number.isFinite(retryAfterMs)) this.retryAfterMs = retryAfterMs;
37
- }
38
- }
39
-
40
- /** Retry-After as milliseconds: delay-seconds or an HTTP date. */
41
- function retryAfterMs(response) {
42
- const value = response?.headers?.get?.("retry-after");
43
- if (!value) return undefined;
44
- if (/^\s*\d+\s*$/.test(value)) return Number(value) * 1000;
45
- const at = Date.parse(value);
46
- return Number.isFinite(at) ? Math.max(0, at - Date.now()) : undefined;
47
- }
48
-
49
- export class MeaningVerificationError extends AltalandiError {
50
- constructor(verdict) {
51
- const changed = verdict === false;
52
- super(
53
- changed
54
- ? "Altalandi could not preserve the input meaning."
55
- : "Altalandi did not return a verified meaning verdict.",
56
- { code: changed ? "MEANING_NOT_PRESERVED" : "MEANING_UNVERIFIED" },
57
- );
58
- this.name = "MeaningVerificationError";
59
- this.verdict = verdict === false ? false : null;
60
- }
61
- }
62
-
63
- function env(name) {
64
- if (typeof process === "undefined" || !process.env) return undefined;
65
- return process.env[name];
66
- }
67
-
68
- function cleanText(value, secret = "") {
69
- if (typeof value !== "string") return "";
70
- let cleaned = value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
71
- if (secret) cleaned = cleaned.split(secret).join("[redacted]");
72
- return cleaned.slice(0, 300);
73
- }
74
-
75
- function errorDetail(payload) {
76
- const detail = payload?.detail;
77
- if (typeof detail === "string") return detail;
78
- if (Array.isArray(detail)) {
79
- return detail
80
- .map((entry) => (typeof entry?.msg === "string" ? entry.msg : ""))
81
- .filter(Boolean)
82
- .join("; ");
83
- }
84
- return "";
85
- }
86
-
87
- function requestBody(text, options) {
88
- const body = { text };
89
- for (const field of REQUEST_FIELDS) {
90
- if (options[field] !== undefined) body[field] = options[field];
91
- }
92
- return body;
93
- }
94
-
95
- /** POST a JSON body to `${baseUrl}${route}` with the key, base URL, timeout and
96
- * error handling every call shares. Returns the payload when `validShape`
97
- * accepts it; throws an AltalandiError (with status and Retry-After) otherwise. */
98
- async function post(route, body, options, validShape) {
99
- const explicitKey = options.apiKey;
100
- const selectedKey = explicitKey === undefined ? env("ALTALANDI_API_KEY") : explicitKey;
101
- if (selectedKey !== undefined && typeof selectedKey !== "string") {
102
- throw new AltalandiError("Altalandi apiKey must be a string.", { code: "INVALID_CONFIG" });
103
- }
104
- const apiKey = selectedKey?.trim();
105
- if (!apiKey) {
106
- throw new AltalandiError(
107
- "No Altalandi API key. Set ALTALANDI_API_KEY or pass apiKey explicitly.",
108
- { code: "MISSING_API_KEY" },
109
- );
110
- }
111
-
112
- const selectedBaseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
113
- if (typeof selectedBaseUrl !== "string") {
114
- throw new AltalandiError("Altalandi baseUrl must be a string.", { code: "INVALID_CONFIG" });
115
- }
116
- const baseUrl = selectedBaseUrl.trim().replace(/\/+$/, "");
117
- if (!baseUrl) {
118
- throw new AltalandiError("Altalandi baseUrl must not be empty.", { code: "INVALID_CONFIG" });
119
- }
120
-
121
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
122
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
123
- throw new AltalandiError("Altalandi timeoutMs must be a positive number.", {
124
- code: "INVALID_CONFIG",
125
- });
126
- }
127
-
128
- const fetchImpl = options.fetch ?? globalThis.fetch;
129
- if (typeof fetchImpl !== "function") {
130
- throw new AltalandiError("This runtime does not provide fetch.", { code: "INVALID_CONFIG" });
131
- }
132
-
133
- const controller = new AbortController();
134
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
135
- try {
136
- let response;
137
- try {
138
- response = await fetchImpl(`${baseUrl}${route}`, {
139
- method: "POST",
140
- headers: {
141
- "content-type": "application/json",
142
- authorization: `Bearer ${apiKey}`,
143
- },
144
- body: JSON.stringify(body),
145
- signal: controller.signal,
146
- });
147
- } catch {
148
- if (controller.signal.aborted) {
149
- throw new AltalandiError(`Altalandi timed out after ${timeoutMs} ms.`, {
150
- code: "TIMEOUT",
151
- });
152
- }
153
- throw new AltalandiError("Altalandi could not reach the API.", { code: "NETWORK_ERROR" });
154
- }
155
-
156
- let raw = "";
157
- try {
158
- raw = await response.text();
159
- } catch {
160
- if (controller.signal.aborted) {
161
- throw new AltalandiError(`Altalandi timed out after ${timeoutMs} ms.`, {
162
- code: "TIMEOUT",
163
- });
164
- }
165
- if (response.ok) {
166
- throw new AltalandiError("Altalandi could not read the API response.", {
167
- code: "INVALID_RESPONSE",
168
- status: response.status,
169
- });
170
- }
171
- // The HTTP status remains useful when an error response has no readable body.
172
- }
173
-
174
- let payload;
175
- if (raw) {
176
- try {
177
- payload = JSON.parse(raw);
178
- } catch {
179
- if (response.ok) {
180
- throw new AltalandiError("Altalandi returned an invalid JSON response.", {
181
- code: "INVALID_RESPONSE",
182
- status: response.status,
183
- });
184
- }
185
- }
186
- }
187
-
188
- if (!response.ok) {
189
- const status = Number.isInteger(response.status) ? response.status : 0;
190
- const statusText = cleanText(response.statusText);
191
- const detail = cleanText(errorDetail(payload), apiKey);
192
- const suffix = detail || statusText;
193
- throw new AltalandiError(
194
- `Altalandi API request failed (${status || "unknown status"})${suffix ? `: ${suffix}` : "."}`,
195
- { code: "API_ERROR", status, retryAfterMs: retryAfterMs(response) },
196
- );
197
- }
198
-
199
- if (!payload || typeof payload !== "object" || Array.isArray(payload) || !validShape(payload)) {
200
- throw new AltalandiError("Altalandi returned an invalid response shape.", {
201
- code: "INVALID_RESPONSE",
202
- status: response.status,
203
- });
204
- }
205
-
206
- return payload;
207
- } finally {
208
- clearTimeout(timeout);
209
- }
210
- }
211
-
212
- /**
213
- * Refine English or Icelandic text through the hosted Altalandi API.
214
- * Client settings (`apiKey`, `baseUrl`, `timeoutMs`, `fetch`) are never sent in
215
- * the JSON body. The full API result is returned without weakening its verdict.
216
- */
217
- export async function refine(text, options = {}) {
218
- if (typeof text !== "string" || !text.trim()) {
219
- throw new AltalandiError("Altalandi requires non-empty text.", { code: "INVALID_INPUT" });
220
- }
221
- return post("/v1/refine", requestBody(text, options), options, (p) => typeof p.output === "string");
222
- }
223
-
224
- /**
225
- * Refine many short strings in one call (POST /v1/refine/batch): at most 500
226
- * items of at most 2,000 characters each, 20,000 characters together. Each item
227
- * comes back with its own output and meaning verdict; an item with `error` came
228
- * back unchanged and was not billed. Same key, limits and errors as refine().
229
- */
230
- export async function refineBatch(items, options = {}) {
231
- if (!Array.isArray(items) || !items.length
232
- || !items.every((it) => it && typeof it.text === "string" && (it.id === undefined || typeof it.id === "string"))) {
233
- throw new AltalandiError("Altalandi refineBatch requires a non-empty list of { id?, text } items.", {
234
- code: "INVALID_INPUT",
235
- });
236
- }
237
- const body = { items: items.map((it) => (it.id === undefined ? { text: it.text } : { id: it.id, text: it.text })) };
238
- for (const field of BATCH_REQUEST_FIELDS) {
239
- if (options[field] !== undefined) body[field] = options[field];
240
- }
241
- return post("/v1/refine/batch", body, options, (p) => Array.isArray(p.items)
242
- && p.items.every((it) => it && typeof it === "object" && typeof it.output === "string"));
243
- }
244
-
245
- /** Assert that an existing result carries a completed, positive meaning check. */
246
- export function assertMeaningVerified(result) {
247
- if (result?.meaningPreserved !== true) {
248
- throw new MeaningVerificationError(result?.meaningPreserved);
249
- }
250
- return result;
251
- }
252
-
253
- /** Request meaning verification and reject unless the API returns strict true. */
254
- export async function refineVerified(text, options = {}) {
255
- const result = await refine(text, { ...options, preserveMeaning: true });
256
- assertMeaningVerified(result);
257
- return result;
258
- }
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
+ "job",
17
+ ];
18
+ // /v1/refine/batch takes fewer options: no writer/scoring tiers, polish or diffs switch.
19
+ const BATCH_REQUEST_FIELDS = [
20
+ "sourceLang",
21
+ "domain",
22
+ "register",
23
+ "preserveMeaning",
24
+ "scoreNativeness",
25
+ "onlyImprove",
26
+ "glossary",
27
+ "autoTerminology",
28
+ "contribute",
29
+ "job",
30
+ ];
31
+
32
+ export class AltalandiError extends Error {
33
+ constructor(message, { code = "ALTALANDI_ERROR", status, retryAfterMs } = {}) {
34
+ super(message);
35
+ this.name = "AltalandiError";
36
+ this.code = code;
37
+ if (Number.isInteger(status)) this.status = status;
38
+ if (Number.isFinite(retryAfterMs)) this.retryAfterMs = retryAfterMs;
39
+ }
40
+ }
41
+
42
+ /** Retry-After as milliseconds: delay-seconds or an HTTP date. */
43
+ function retryAfterMs(response) {
44
+ const value = response?.headers?.get?.("retry-after");
45
+ if (!value) return undefined;
46
+ if (/^\s*\d+\s*$/.test(value)) return Number(value) * 1000;
47
+ const at = Date.parse(value);
48
+ return Number.isFinite(at) ? Math.max(0, at - Date.now()) : undefined;
49
+ }
50
+
51
+ export class MeaningVerificationError extends AltalandiError {
52
+ constructor(verdict) {
53
+ const changed = verdict === false;
54
+ super(
55
+ changed
56
+ ? "Altalandi could not preserve the input meaning."
57
+ : "Altalandi did not return a verified meaning verdict.",
58
+ { code: changed ? "MEANING_NOT_PRESERVED" : "MEANING_UNVERIFIED" },
59
+ );
60
+ this.name = "MeaningVerificationError";
61
+ this.verdict = verdict === false ? false : null;
62
+ }
63
+ }
64
+
65
+ function env(name) {
66
+ if (typeof process === "undefined" || !process.env) return undefined;
67
+ return process.env[name];
68
+ }
69
+
70
+ function cleanText(value, secret = "") {
71
+ if (typeof value !== "string") return "";
72
+ let cleaned = value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
73
+ if (secret) cleaned = cleaned.split(secret).join("[redacted]");
74
+ return cleaned.slice(0, 300);
75
+ }
76
+
77
+ function errorDetail(payload) {
78
+ const detail = payload?.detail;
79
+ if (typeof detail === "string") return detail;
80
+ if (Array.isArray(detail)) {
81
+ return detail
82
+ .map((entry) => (typeof entry?.msg === "string" ? entry.msg : ""))
83
+ .filter(Boolean)
84
+ .join("; ");
85
+ }
86
+ return "";
87
+ }
88
+
89
+ function requestBody(text, options) {
90
+ const body = { text };
91
+ for (const field of REQUEST_FIELDS) {
92
+ if (options[field] !== undefined) body[field] = options[field];
93
+ }
94
+ return body;
95
+ }
96
+
97
+ /** POST a JSON body to `${baseUrl}${route}` with the key, base URL, timeout and
98
+ * error handling every call shares. Returns the payload when `validShape`
99
+ * accepts it; throws an AltalandiError (with status and Retry-After) otherwise. */
100
+ async function post(route, body, options, validShape) {
101
+ const explicitKey = options.apiKey;
102
+ const selectedKey = explicitKey === undefined ? env("ALTALANDI_API_KEY") : explicitKey;
103
+ if (selectedKey !== undefined && typeof selectedKey !== "string") {
104
+ throw new AltalandiError("Altalandi apiKey must be a string.", { code: "INVALID_CONFIG" });
105
+ }
106
+ const apiKey = selectedKey?.trim();
107
+ if (!apiKey) {
108
+ throw new AltalandiError(
109
+ "No Altalandi API key. Set ALTALANDI_API_KEY or pass apiKey explicitly.",
110
+ { code: "MISSING_API_KEY" },
111
+ );
112
+ }
113
+
114
+ const selectedBaseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
115
+ if (typeof selectedBaseUrl !== "string") {
116
+ throw new AltalandiError("Altalandi baseUrl must be a string.", { code: "INVALID_CONFIG" });
117
+ }
118
+ const baseUrl = selectedBaseUrl.trim().replace(/\/+$/, "");
119
+ if (!baseUrl) {
120
+ throw new AltalandiError("Altalandi baseUrl must not be empty.", { code: "INVALID_CONFIG" });
121
+ }
122
+
123
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
124
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
125
+ throw new AltalandiError("Altalandi timeoutMs must be a positive number.", {
126
+ code: "INVALID_CONFIG",
127
+ });
128
+ }
129
+
130
+ const fetchImpl = options.fetch ?? globalThis.fetch;
131
+ if (typeof fetchImpl !== "function") {
132
+ throw new AltalandiError("This runtime does not provide fetch.", { code: "INVALID_CONFIG" });
133
+ }
134
+
135
+ const controller = new AbortController();
136
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
137
+ try {
138
+ let response;
139
+ try {
140
+ response = await fetchImpl(`${baseUrl}${route}`, {
141
+ method: "POST",
142
+ headers: {
143
+ "content-type": "application/json",
144
+ authorization: `Bearer ${apiKey}`,
145
+ },
146
+ body: JSON.stringify(body),
147
+ signal: controller.signal,
148
+ });
149
+ } catch {
150
+ if (controller.signal.aborted) {
151
+ throw new AltalandiError(`Altalandi timed out after ${timeoutMs} ms.`, {
152
+ code: "TIMEOUT",
153
+ });
154
+ }
155
+ throw new AltalandiError("Altalandi could not reach the API.", { code: "NETWORK_ERROR" });
156
+ }
157
+
158
+ let raw = "";
159
+ try {
160
+ raw = await response.text();
161
+ } catch {
162
+ if (controller.signal.aborted) {
163
+ throw new AltalandiError(`Altalandi timed out after ${timeoutMs} ms.`, {
164
+ code: "TIMEOUT",
165
+ });
166
+ }
167
+ if (response.ok) {
168
+ throw new AltalandiError("Altalandi could not read the API response.", {
169
+ code: "INVALID_RESPONSE",
170
+ status: response.status,
171
+ });
172
+ }
173
+ // The HTTP status remains useful when an error response has no readable body.
174
+ }
175
+
176
+ let payload;
177
+ if (raw) {
178
+ try {
179
+ payload = JSON.parse(raw);
180
+ } catch {
181
+ if (response.ok) {
182
+ throw new AltalandiError("Altalandi returned an invalid JSON response.", {
183
+ code: "INVALID_RESPONSE",
184
+ status: response.status,
185
+ });
186
+ }
187
+ }
188
+ }
189
+
190
+ if (!response.ok) {
191
+ const status = Number.isInteger(response.status) ? response.status : 0;
192
+ const statusText = cleanText(response.statusText);
193
+ const detail = cleanText(errorDetail(payload), apiKey);
194
+ const suffix = detail || statusText;
195
+ throw new AltalandiError(
196
+ `Altalandi API request failed (${status || "unknown status"})${suffix ? `: ${suffix}` : "."}`,
197
+ { code: "API_ERROR", status, retryAfterMs: retryAfterMs(response) },
198
+ );
199
+ }
200
+
201
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || !validShape(payload)) {
202
+ throw new AltalandiError("Altalandi returned an invalid response shape.", {
203
+ code: "INVALID_RESPONSE",
204
+ status: response.status,
205
+ });
206
+ }
207
+
208
+ return payload;
209
+ } finally {
210
+ clearTimeout(timeout);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Refine English or Icelandic text through the hosted Altalandi API.
216
+ * Client settings (`apiKey`, `baseUrl`, `timeoutMs`, `fetch`) are never sent in
217
+ * the JSON body. The full API result is returned without weakening its verdict.
218
+ */
219
+ export async function refine(text, options = {}) {
220
+ if (typeof text !== "string" || !text.trim()) {
221
+ throw new AltalandiError("Altalandi requires non-empty text.", { code: "INVALID_INPUT" });
222
+ }
223
+ return post("/v1/refine", requestBody(text, options), options, (p) => typeof p.output === "string");
224
+ }
225
+
226
+ /**
227
+ * Refine many short strings in one call (POST /v1/refine/batch): at most 500
228
+ * items of at most 2,000 characters each, 20,000 characters together. Each item
229
+ * comes back with its own output and meaning verdict; an item with `error` came
230
+ * back unchanged and was not billed. Same key, limits and errors as refine().
231
+ */
232
+ export async function refineBatch(items, options = {}) {
233
+ if (!Array.isArray(items) || !items.length
234
+ || !items.every((it) => it && typeof it.text === "string" && (it.id === undefined || typeof it.id === "string"))) {
235
+ throw new AltalandiError("Altalandi refineBatch requires a non-empty list of { id?, text } items.", {
236
+ code: "INVALID_INPUT",
237
+ });
238
+ }
239
+ const body = { items: items.map((it) => (it.id === undefined ? { text: it.text } : { id: it.id, text: it.text })) };
240
+ for (const field of BATCH_REQUEST_FIELDS) {
241
+ if (options[field] !== undefined) body[field] = options[field];
242
+ }
243
+ return post("/v1/refine/batch", body, options, (p) => Array.isArray(p.items)
244
+ && p.items.every((it) => it && typeof it === "object" && typeof it.output === "string"));
245
+ }
246
+
247
+ /** Assert that an existing result carries a completed, positive meaning check. */
248
+ export function assertMeaningVerified(result) {
249
+ if (result?.meaningPreserved !== true) {
250
+ throw new MeaningVerificationError(result?.meaningPreserved);
251
+ }
252
+ return result;
253
+ }
254
+
255
+ /** Request meaning verification and reject unless the API returns strict true. */
256
+ export async function refineVerified(text, options = {}) {
257
+ const result = await refine(text, { ...options, preserveMeaning: true });
258
+ assertMeaningVerified(result);
259
+ return result;
260
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altalandi",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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",
package/report.mjs CHANGED
@@ -89,6 +89,7 @@ export function reportMarkdown(input, data, options = {}) {
89
89
  `| Innfæddni | ${q.nativenessScore ?? "n/a"}/100 |`,
90
90
  `| Ritill | ${tier.writer || "standard"} / ${tier.scoring || "?"} |`,
91
91
  `| Auðkenni | ${data?.runUrl ? `[${data.id}](${data.runUrl})` : data?.id || "?"} |`,
92
+ ...(data?.jobUrl ? [`| Verkið | ${data.jobUrl} |`] : []),
92
93
  "",
93
94
  "## Fyrir", "", fence(input), "",
94
95
  "## Eftir", "", fence(data?.output), "",
@@ -335,7 +336,9 @@ export function reviewMarkdown(input, data, { before = null, after = null, maxCh
335
336
 
336
337
  out.push("**Kostnaður**", ...costLines(data, before, after));
337
338
  // The run's own page: the text sent, the result, the checks and the review.
338
- if (data?.runUrl) out.push(`- Verkið: ${data.runUrl}`);
339
+ // A call sent as part of a job (--job) links the job first, then this part.
340
+ if (data?.jobUrl) out.push(`- Verkið: ${data.jobUrl}`);
341
+ if (data?.runUrl) out.push(`- ${data?.jobUrl ? "Hlutinn" : "Verkið"}: ${data.runUrl}`);
339
342
  else if (data?.id) out.push(`- Auðkenni: \`${data.id}\``);
340
343
  return out.join("\n") + "\n";
341
344
  }
@@ -388,6 +391,8 @@ export function batchReportMarkdown(summary) {
388
391
  ? [`| Stafir rukkaðir | ${t.billedCharactersApprox ? "um " : ""}${count(t.billedCharacters)} |`] : []),
389
392
  `| Tegund texta | ${cell(summary?.domain || "general")} |`,
390
393
  `| Málsnið | ${cell(summary?.register || "neutral")} |`,
394
+ // The run's one job page (an engine that knows jobs); never one link per call.
395
+ ...(summary?.jobUrl ? [`| Verkið | ${summary.jobUrl} |`] : []),
391
396
  ];
392
397
 
393
398
  if (summary?.stopped) {
package/skill/SKILL.md CHANGED
@@ -27,7 +27,7 @@ yourself or summarise the result in your own words.
27
27
  line, below. It waits up to 5 minutes per call and handles long texts.
28
28
  - **No shell, only the Altalandi tools** (`refine_icelandic`,
29
29
  `proofread_icelandic`, `get_refinement_status`, for example in claude.ai or
30
- ChatGPT): use the tools, and follow "Long texts through the tools" below. The
30
+ ChatGPT): use the tools, and follow "Long texts in parts" below. The
31
31
  tools cannot wait as long as the command line, so long texts go in parts.
32
32
 
33
33
  How long a call takes: a few seconds for a sentence, 10–30 seconds for a
@@ -73,21 +73,30 @@ A document of several sections (more than about 3,000 characters) goes better
73
73
  through the folder run below: it refines and checks each passage on its own,
74
74
  and a passage that fails keeps its original without holding up the rest. Save
75
75
  the text as a `.md` file in an empty temporary folder and run the folder steps.
76
+ To send it in parts instead, follow "Long texts in parts".
76
77
 
77
- ## Long texts through the tools
78
+ ## Long texts in parts
78
79
 
79
80
  1. **Split the text into parts** at paragraph breaks, never inside a sentence:
80
81
  at most **1,000 characters** a part for English, **1,500** for Icelandic.
81
82
  Keep a heading with the paragraph under it. A list or a table stays in one
82
83
  part when it fits.
83
- 2. **Call the tool once per part,** at most 3 at a time, in order. Give every
84
- part its own new `operationId` (a fresh UUID). Keep a note of which ID
84
+ 2. **Command line: make one job id** for the whole text: `j_`, today's date
85
+ (yyyymmdd) and 6 random letters or digits, e.g. `j_20261013k4x9qa`. Run the
86
+ procedure once per part, in order, each with its own in and out files, and
87
+ add `--job <id> --part "Hluti N af M"` every time; on the first part add
88
+ `--job-title "<short name of the document>"` too. All parts are then one
89
+ verk on the site.
90
+ 3. **Tools: call the tool once per part,** at most 3 at a time, in order. Give
91
+ every part its own new `operationId` (a fresh UUID). Keep a note of which ID
85
92
  belongs to which part.
86
- 3. **Join the results in the original order** and give the user the whole text,
87
- then each part's verdict and changes, as the tool returns them.
88
- 4. **A part that is refused or unverified keeps its original text.** Say which
93
+ 4. **Join the results in the original order** and give the user the whole text,
94
+ then each part's verdict and changes, as the command or the tool returns
95
+ them. With a job, end with its link once: `Verkið: <link>`, as the blocks
96
+ print it.
97
+ 5. **A part that is refused or unverified keeps its original text.** Say which
89
98
  part, and why (the tool says why). Never rewrite it yourself.
90
- 5. **Terms must match across parts.** If two parts translate the same term
99
+ 6. **Terms must match across parts.** If two parts translate the same term
91
100
  differently, point it out and ask which to keep; do not quietly change one.
92
101
 
93
102
  ## If a call times out
package/update.mjs CHANGED
@@ -142,6 +142,7 @@ export const SHIPPED_SKILLS = new Set([
142
142
  "aeb88010599539fd", // 0.6.0
143
143
  "ebfc4ef17a614973", // 0.6.1
144
144
  "9def55ecc6b30785", // 0.6.2
145
+ "2168eaa25a5c561c", // after 0.6.2 (3c73702: stafir and krónur)
145
146
  ]);
146
147
 
147
148
  export function skillFingerprint(text) {