altalandi 0.5.0 → 0.6.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
@@ -114,6 +114,85 @@ altalandi --report --file draft.txt # also write a Markdown report to ./altal
114
114
  | `--report-dir <dir>` | | write the report to `<dir>` instead (implies `--report`) |
115
115
  | `--fast` | off | `preserveMeaning:false`, `scoreNativeness:false`, `onlyImprove:false`, `polishPass:false`, `scoring:"none"` — normally one model call (structured input can still add a structure-preserving retry) |
116
116
 
117
+ ## Folders, i18n catalogs and websites
118
+
119
+ One command for a whole folder, one web page, or a site. The input is never
120
+ written to.
121
+
122
+ ```bash
123
+ altalandi ./docs # every .md .txt .html .json under ./docs → ./docs-is
124
+ altalandi ./locales --out ./locales-is # i18n catalogs: values only, placeholders kept
125
+ altalandi ./docs --dry-run # every passage and the estimate; sends nothing
126
+ altalandi https://example.com/pricing --out pricing-is.md # one page → Markdown
127
+ altalandi https://example.com/docs/ --site --max-pages 50 # a site → ./example.com-is/
128
+ ```
129
+
130
+ Each passage (a paragraph, list item, table cell, heading or string) is its own
131
+ call. Everything outside the passages is written back byte for byte:
132
+
133
+ | format | refined | never touched |
134
+ |--------|---------|---------------|
135
+ | Markdown | paragraphs, list items, table cells, heading text, front-matter `title`/`description`-style values | code blocks, inline code, URLs, link targets, HTML tags, front-matter keys, heading markup |
136
+ | HTML | text in elements (inline tags travel as numbered markers and come back exactly) | tags, attributes, entities, `script`, `style`, `pre`, `code`, `translate="no"` |
137
+ | JSON | string values of i18n catalogs | keys, `@`/`$` metadata, `{name}` `{{name}}` `%s` `%(name)s`, ICU plural/select structure, HTML in strings |
138
+ | Text | paragraphs (re-wrapped to their width) and lines | |
139
+
140
+ Passages with nothing to refine (code, numbers, URLs or placeholders alone) are
141
+ skipped, and a passage that appears twice is sent once. `node_modules`, `.git`,
142
+ `dist`, `build`, `altalandi-reports`, hidden files and folders and symlinks are
143
+ skipped. JSON config (`package.json`, `tsconfig.json`, …) and JSON with numbers
144
+ or booleans is copied unchanged; other file types are not copied.
145
+
146
+ Pages are fetched one at a time with a User-Agent naming altalandi, only where
147
+ robots.txt allows it for `altalandi` and for `*` (`Crawl-delay` is honoured).
148
+ The readable part (`<main>` or `<article>`, else `<body>` without navigation,
149
+ header, footer, sidebars, forms, scripts and hidden elements) becomes Markdown.
150
+ `--site` reads `/sitemap.xml`, or follows links when there is none, staying on
151
+ the same origin and under the URL's folder.
152
+
153
+ **Cost and safety.** Before anything is sent, the CLI prints an estimate, priced
154
+ from the public tariff:
155
+
156
+ ```text
157
+ 5 files, 24 passages, about 851 characters (23 calls: repeated passages are sent once).
158
+ 1 more file has nothing to refine and is copied unchanged.
159
+ Billing: about 920 units (at least 40 a call), about 74 kr if paid from credit. The free allowance is 2,000 units a day.
160
+ ```
161
+
162
+ Every call costs at least the per-call minimum, so many short strings cost more
163
+ than their word count. In a terminal it asks before sending; without one (CI,
164
+ agents) it sends nothing unless you pass `--yes`.
165
+
166
+ | flag | default | meaning |
167
+ |------|---------|---------|
168
+ | `--out <path>` | `<folder>-is` | output folder, refused if it is the input, inside it or around it; for one page, the `.md` file |
169
+ | `--dry-run` | off | list every passage and the estimate; send nothing, write nothing |
170
+ | `--yes`, `-y` | off | send without asking |
171
+ | `--max-chars <n>` | | files are sent in order until the next would go over n characters; the rest are neither sent nor written |
172
+ | `--concurrency <n>` | `2` | calls at once, 1–4 |
173
+ | `--site` / `--max-pages <n>` | off / `20` | read a site from the URL, up to n pages |
174
+ | `--no-store` | off | keep none of the passages (free use is otherwise kept, scrubbed of personal data) |
175
+ | `--json` | off | a machine-readable summary of every file and passage on stdout |
176
+
177
+ A `429` waits as long as `Retry-After` asks (up to 5 minutes) and retries. A
178
+ `402` stops the run cleanly: finished files are written, the rest are listed and
179
+ not written; run the same command again after topping up.
180
+
181
+ **Results.** Only verified changes are written. A passage keeps its original
182
+ text when its meaning check did not pass (`meaningCheck` other than `passed`, or
183
+ `meaningPreserved` not `true`), or when the refined text lost or changed code, a
184
+ URL, a placeholder or a tag. One report next to the output
185
+ (`altalandi-reports/altalandi-<time>-<input>.md`) lists every file and every
186
+ changed passage with its meaning verdict, and the kept passages under their own
187
+ heading with the unused suggestion and the reason.
188
+
189
+ | exit | meaning |
190
+ |------|---------|
191
+ | `0` | done |
192
+ | `2` | done, but some passages kept their original text (see the report) |
193
+ | `4` | stopped: out of credit or allowance |
194
+ | `1` | error, or not everything was done (a failed call, `--max-chars`, no `--yes`) |
195
+
117
196
  ## Before and after
118
197
 
119
198
  `--diff` prints what the website shows inline, in the terminal:
package/altalandi.mjs CHANGED
@@ -5,10 +5,14 @@
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
+ // altalandi ./docs # a whole folder → ./docs-is (batch.mjs)
9
+ // altalandi https://example.com --site
8
10
  //
9
11
  // stdout = the refined text (so you can pipe it); stderr = nativeness score,
10
12
  // meaning verdict, API warnings, and the diff.
11
13
 
14
+ import { existsSync, statSync } from "node:fs";
15
+
12
16
  import { refine } from "./client.mjs";
13
17
  import { beforeAfterText, costLines, outcomeOf, reportEnabled, reviewMarkdown, writeReport } from "./report.mjs";
14
18
  import { readCredentials, resolveKey, runLogin, runLogout, runWhoami } from "./auth.mjs";
@@ -72,7 +76,10 @@ applied · 1 the check or the update failed.`);
72
76
  }
73
77
 
74
78
  if (!handled) {
75
- const opts = { domain: "general", register: "neutral", json: false, fast: false, diff: false, review: false, report: undefined };
79
+ const opts = {
80
+ domain: "general", register: "neutral", json: false, fast: false, diff: false, review: false, report: undefined,
81
+ site: false, dryRun: false, yes: false,
82
+ };
76
83
  const positional = [];
77
84
  for (let i = 0; i < args.length; i++) {
78
85
  const a = args[i];
@@ -90,6 +97,12 @@ for (let i = 0; i < args.length; i++) {
90
97
  else if (a === "--no-store") opts.noStore = true;
91
98
  else if (a === "--report") opts.report = true;
92
99
  else if (a === "--report-dir") { opts.report = true; opts.reportDir = args[++i]; }
100
+ else if (a === "--site") opts.site = true;
101
+ else if (a === "--max-pages") opts.maxPages = Number(args[++i]);
102
+ else if (a === "--dry-run") opts.dryRun = true;
103
+ else if (a === "--yes" || a === "-y") opts.yes = true;
104
+ else if (a === "--max-chars") opts.maxChars = Number(args[++i]);
105
+ else if (a === "--concurrency") opts.concurrency = Number(args[++i]);
93
106
  else positional.push(a);
94
107
  }
95
108
 
@@ -105,6 +118,13 @@ Usage:
105
118
  echo "text" | altalandi
106
119
  altalandi --domain marketing --register warm "text"
107
120
 
121
+ Folders and websites:
122
+ altalandi ./docs every .md .txt .html .json under ./docs → ./docs-is
123
+ altalandi ./locales --out ./locales-is
124
+ altalandi https://example.com/about --out about-is.md one page, as Markdown
125
+ altalandi https://example.com --site a site: /sitemap.xml, else its links
126
+ altalandi ./docs --dry-run list what would be sent and the estimate; send nothing
127
+
108
128
  Commands:
109
129
  altalandi login open altalandi.is, sign in or sign up, approve; stores the key
110
130
  and installs the Claude Code skill (--no-skill, --no-browser)
@@ -122,7 +142,8 @@ Options:
122
142
  --diff show the text before and after, and every change with its reason (stderr)
123
143
  --review print one Markdown block on stdout to show the user as it is: verdict,
124
144
  before, after, every change with its reason, the cost and the balance
125
- --out write the refined text to this UTF-8 file (use with --review)
145
+ --out write the refined text to this UTF-8 file (use with --review;
146
+ for a folder or a URL, see below)
126
147
  --no-store don't keep this text: free use is otherwise kept, scrubbed of personal
127
148
  data, to improve the service (paid use is kept only with consent)
128
149
  --report also write a Markdown report (input, output, changes, reasons, verdict)
@@ -131,7 +152,23 @@ Options:
131
152
  --base-url API base (default $ALTALANDI_BASE_URL or https://altalandi.is/api)
132
153
 
133
154
  stdout = refined text; stderr = nativeness / meaning / warnings / diff.
134
- Without --fast, false/null meaning verification exits 3 after printing the response.`);
155
+ Without --fast, false/null meaning verification exits 3 after printing the response.
156
+
157
+ Folder and website options:
158
+ --out output folder (default <folder>-is; never the input or inside it),
159
+ or the .md file for one page
160
+ --dry-run show every passage and the estimate; send nothing, write nothing
161
+ --yes, -y send without asking (needed when there is no terminal to ask in)
162
+ --max-chars send at most this many characters; files past the budget are left out
163
+ --concurrency calls at once, 1-4 (default 2)
164
+ --site with a URL: its pages on the same site, under the URL's folder
165
+ --max-pages pages to read with --site (default 20)
166
+ --json print a machine-readable summary at the end
167
+ Code, URLs, link targets, placeholders ({name}, %s, ICU), HTML tags and JSON keys
168
+ stay byte for byte. Only changes whose meaning check passed are written; other
169
+ passages keep the original text and are listed in the report
170
+ (./altalandi-reports/). Exit: 0 done · 2 some passages kept their original ·
171
+ 4 out of credit · 1 error.`);
135
172
  }
136
173
 
137
174
  function readStdin() {
@@ -152,6 +189,19 @@ if (opts.key) {
152
189
  console.error("⚠ --key is deprecated because command arguments can leak through shell history/process listings; load ALTALANDI_API_KEY instead.");
153
190
  }
154
191
 
192
+ // A folder or a URL as the one argument: many documents in one run (batch.mjs).
193
+ const target = positional.length === 1 && !opts.file ? positional[0] : "";
194
+ const many = /^https?:\/\//i.test(target) || (Boolean(target) && existsSync(target) && statSync(target).isDirectory());
195
+ if (many) {
196
+ const { runBatch } = await import("./batch.mjs");
197
+ const { currentVersion } = await import("./update.mjs");
198
+ process.exitCode = await runBatch({ ...opts, target, key, baseUrl }, { version: await currentVersion() });
199
+ } else {
200
+ if (opts.site || opts.dryRun || opts.maxChars !== undefined || opts.maxPages !== undefined || opts.concurrency !== undefined) {
201
+ console.error("--site, --dry-run, --max-chars, --max-pages and --concurrency need a folder or a URL: altalandi ./docs --dry-run");
202
+ process.exit(2);
203
+ }
204
+
155
205
  let text = positional.join(" ").trim();
156
206
  if (opts.file) {
157
207
  const { readFile } = await import("node:fs/promises");
@@ -271,4 +321,5 @@ try {
271
321
  if (e.status === 401) console.error("→ The key is invalid or revoked. Run: npx altalandi login");
272
322
  process.exit(e.status === 402 ? 4 : 1);
273
323
  }
324
+ } // single text
274
325
  }