@songmu/mdhq 0.0.2

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/assets/localize.d.ts +19 -0
  4. package/dist/assets/localize.js +364 -0
  5. package/dist/cli.d.ts +8 -0
  6. package/dist/cli.js +119 -0
  7. package/dist/config/config.d.ts +25 -0
  8. package/dist/config/config.js +170 -0
  9. package/dist/config/match.d.ts +7 -0
  10. package/dist/config/match.js +101 -0
  11. package/dist/convert/article-date.d.ts +20 -0
  12. package/dist/convert/article-date.js +255 -0
  13. package/dist/convert/convert-html.d.ts +2 -0
  14. package/dist/convert/convert-html.js +89 -0
  15. package/dist/convert/extract-published.d.ts +12 -0
  16. package/dist/convert/extract-published.js +24 -0
  17. package/dist/convert/extract-updated.d.ts +8 -0
  18. package/dist/convert/extract-updated.js +20 -0
  19. package/dist/date.d.ts +18 -0
  20. package/dist/date.js +448 -0
  21. package/dist/errors.d.ts +8 -0
  22. package/dist/errors.js +10 -0
  23. package/dist/frontmatter/frontmatter.d.ts +40 -0
  24. package/dist/frontmatter/frontmatter.js +114 -0
  25. package/dist/get-page.d.ts +2 -0
  26. package/dist/get-page.js +308 -0
  27. package/dist/http/fetch.d.ts +46 -0
  28. package/dist/http/fetch.js +195 -0
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.js +3 -0
  31. package/dist/list-files.d.ts +8 -0
  32. package/dist/list-files.js +35 -0
  33. package/dist/markdown/transform.d.ts +6 -0
  34. package/dist/markdown/transform.js +129 -0
  35. package/dist/path/storage-path.d.ts +7 -0
  36. package/dist/path/storage-path.js +110 -0
  37. package/dist/storage/atomic.d.ts +8 -0
  38. package/dist/storage/atomic.js +84 -0
  39. package/dist/storage/path-safety.d.ts +1 -0
  40. package/dist/storage/path-safety.js +55 -0
  41. package/dist/storage/save.d.ts +23 -0
  42. package/dist/storage/save.js +118 -0
  43. package/dist/types.d.ts +62 -0
  44. package/dist/types.js +1 -0
  45. package/dist/url/identity.d.ts +12 -0
  46. package/dist/url/identity.js +54 -0
  47. package/dist/url/pathname.d.ts +4 -0
  48. package/dist/url/pathname.js +46 -0
  49. package/dist/version.d.ts +3 -0
  50. package/dist/version.js +6 -0
  51. package/docs/README.md +14 -0
  52. package/docs/configuration.md +242 -0
  53. package/docs/library-api.md +275 -0
  54. package/docs/specification.md +730 -0
  55. package/package.json +73 -0
@@ -0,0 +1,46 @@
1
+ import { MdhqError } from "../errors.js";
2
+ export const HTML_EXTENSIONS = new Set([
3
+ ".html",
4
+ ".htm",
5
+ ".xhtml",
6
+ ".php",
7
+ ".asp",
8
+ ".aspx",
9
+ ".jsp",
10
+ ".jspx"
11
+ ]);
12
+ export function decodeUrlPathSegment(segment) {
13
+ try {
14
+ return decodeURIComponent(segment.replace(/%(?![0-9a-f]{2})/giu, "%25")).normalize("NFC");
15
+ }
16
+ catch (error) {
17
+ throw new MdhqError("INVALID_URL", `Invalid UTF-8 in path segment: ${segment}`, {
18
+ cause: error
19
+ });
20
+ }
21
+ }
22
+ export function storageBasename(segment) {
23
+ const normalized = decodeUrlPathSegment(segment);
24
+ const match = normalized.match(/(\.[^.]+)$/u);
25
+ const extension = match?.[1];
26
+ return extension && HTML_EXTENSIONS.has(extension.toLowerCase())
27
+ ? normalized.slice(0, -extension.length)
28
+ : normalized;
29
+ }
30
+ export function canonicalPathname(pathname, hasEntryValue = false) {
31
+ const segments = pathname
32
+ .split("/")
33
+ .filter(Boolean)
34
+ .map((segment) => encodeURIComponent(decodeUrlPathSegment(segment)));
35
+ if (!hasEntryValue) {
36
+ if (segments.length === 0) {
37
+ return "/index";
38
+ }
39
+ const finalIndex = segments.length - 1;
40
+ const finalSegment = pathname.split("/").filter(Boolean).at(-1);
41
+ if (finalSegment !== undefined) {
42
+ segments[finalIndex] = encodeURIComponent(storageBasename(finalSegment));
43
+ }
44
+ }
45
+ return `/${segments.join("/")}`;
46
+ }
@@ -0,0 +1,3 @@
1
+ export declare const VERSION: string;
2
+ export declare const PROJECT_URL = "https://github.com/Songmu/mdhq";
3
+ export declare const DEFAULT_USER_AGENT: string;
@@ -0,0 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const packageJson = require("../package.json");
4
+ export const VERSION = packageJson.version;
5
+ export const PROJECT_URL = "https://github.com/Songmu/mdhq";
6
+ export const DEFAULT_USER_AGENT = `mdhq/${VERSION} (+${PROJECT_URL})`;
package/docs/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # mdhq documentation
2
+
3
+ This directory documents the behavior implemented in mdhq `0.0.0`.
4
+
5
+ - [Current specification](specification.md) describes CLI behavior, URL
6
+ identity, HTTP fetching, storage paths, Markdown processing, assets,
7
+ frontmatter, and concurrent writes.
8
+ - [Configuration reference](configuration.md) describes the JSON
9
+ configuration file, XDG paths, option precedence, and host/path matching.
10
+ - [Library API reference](library-api.md) describes the public exports,
11
+ options, results, warnings, and errors.
12
+
13
+ These documents describe the current implementation. They are not a roadmap
14
+ for unimplemented features.
@@ -0,0 +1,242 @@
1
+ # Configuration reference
2
+
3
+ mdhq reads one optional JSON configuration file.
4
+
5
+ Default location:
6
+
7
+ ```text
8
+ $XDG_CONFIG_HOME/mdhq/config.json
9
+ ```
10
+
11
+ When `XDG_CONFIG_HOME` is unset:
12
+
13
+ ```text
14
+ ~/.config/mdhq/config.json
15
+ ```
16
+
17
+ The library can select another file with `GetPageOptions.configPath`.
18
+ The CLI does not currently expose a configuration-path option.
19
+
20
+ A missing configuration file is accepted. An unreadable file, malformed
21
+ JSON, a non-object top-level value, or an invalid known value is a fatal
22
+ `CONFIG_ERROR`.
23
+
24
+ Unknown keys are accepted and reported with `UNKNOWN_CONFIG_KEY` warnings.
25
+ This applies at the top level and within `defuddle`, `frontmatter`, host
26
+ configuration, and path configuration objects.
27
+
28
+ ## Complete example
29
+
30
+ ```json
31
+ {
32
+ "root": "/data/mdhq",
33
+ "userAgent": "my-clipper/1.0",
34
+ "timeoutMs": 30000,
35
+ "maxResponseBytes": 20971520,
36
+ "maxRedirects": 10,
37
+ "assets": false,
38
+ "useAsync": true,
39
+ "defuddle": {
40
+ "removeSmallImages": true,
41
+ "standardize": true,
42
+ "language": "en"
43
+ },
44
+ "frontmatter": {
45
+ "exclude": ["description"],
46
+ "values": {
47
+ "collection": "reading",
48
+ "reviewed": false
49
+ }
50
+ },
51
+ "hosts": {
52
+ "*.example.com": {
53
+ "entryQueryKey": "entry_id",
54
+ "paths": {
55
+ "/articles/*.php": {
56
+ "entryQueryKey": "id"
57
+ },
58
+ "/search/*": {
59
+ "entryQueryKey": null
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ## Top-level fields
68
+
69
+ | Field | Type | Description |
70
+ | --- | --- | --- |
71
+ | `root` | string | Default storage root. |
72
+ | `userAgent` | string | Default page and asset User-Agent. |
73
+ | `timeoutMs` | positive integer | Timeout in milliseconds for each mdhq HTTP request attempt. |
74
+ | `maxResponseBytes` | positive integer | Maximum buffered bytes for each page or asset response. |
75
+ | `maxRedirects` | non-negative integer | Maximum redirect count for each page or asset request. |
76
+ | `assets` | boolean | Download images into `_assets`; defaults to `true`. |
77
+ | `useAsync` | boolean | Legacy fallback for Defuddle asynchronous extractors. |
78
+ | `defuddle` | object | Defuddle extraction options. |
79
+ | `frontmatter` | object | Frontmatter exclusions and configured values. |
80
+ | `hosts` | object | Host and pathname-specific entry query configuration. |
81
+
82
+ ## Storage root precedence
83
+
84
+ The effective root is selected in this order:
85
+
86
+ 1. CLI `--root` or library `GetPageOptions.root`
87
+ 2. `MDHQ_ROOT`
88
+ 3. configuration `root`
89
+ 4. `$XDG_DATA_HOME/mdhq`
90
+ 5. `~/.local/share/mdhq` when `XDG_DATA_HOME` is unset
91
+
92
+ The selected root is converted to an absolute path.
93
+
94
+ ## HTTP option precedence
95
+
96
+ For each setting, a defined `GetPageOptions` value overrides configuration.
97
+ Otherwise configuration overrides the built-in default.
98
+
99
+ | Library option | Configuration field | Built-in default |
100
+ | --- | --- | --- |
101
+ | `userAgent` | `userAgent` | mdhq version User-Agent |
102
+ | `timeoutMs` | `timeoutMs` | `30000` |
103
+ | `maxResponseBytes` | `maxResponseBytes` | `20971520` |
104
+ | `maxRedirects` | `maxRedirects` | `10` |
105
+
106
+ The CLI `--user-agent` option is passed as `GetPageOptions.userAgent` and
107
+ therefore overrides configuration `userAgent`.
108
+
109
+ The CLI `--no-assets` option passes `GetPageOptions.assets: false` and
110
+ therefore overrides configuration `assets`. When asset localization is
111
+ disabled, image destinations remain absolute URLs, the result contains no
112
+ asset entries, and mdhq does not create `_assets`.
113
+
114
+ Generic CLI `--header` values and library `headers` values are appended after
115
+ mdhq creates its `Accept` and User-Agent headers. A generic `User-Agent` or
116
+ `Accept` entry is combined with the existing value; it does not replace it.
117
+ Use the dedicated User-Agent option for replacement.
118
+
119
+ `GetPageOptions.headers` has no configuration equivalent. This prevents
120
+ credentials such as cookies or authorization tokens from being stored in the
121
+ configuration file.
122
+
123
+ ## Defuddle options
124
+
125
+ Supported `defuddle` fields:
126
+
127
+ | Field | Type |
128
+ | --- | --- |
129
+ | `debug` | boolean |
130
+ | `removeExactSelectors` | boolean |
131
+ | `removePartialSelectors` | boolean |
132
+ | `removeImages` | boolean |
133
+ | `useAsync` | boolean |
134
+ | `removeHiddenElements` | boolean |
135
+ | `removeLowScoring` | boolean |
136
+ | `removeSmallImages` | boolean |
137
+ | `standardize` | boolean |
138
+ | `removeContentPatterns` | boolean |
139
+ | `contentSelector` | string |
140
+ | `language` | string |
141
+ | `includeReplies` | boolean or `"extractors"` |
142
+ | `profile` | boolean |
143
+
144
+ mdhq always enables Defuddle Markdown output and supplies its own
145
+ proxy-aware fetch implementation. Those values are not configurable through
146
+ the JSON file.
147
+
148
+ The supplied fetch implementation adds environment proxy handling only.
149
+ Defuddle-internal asynchronous requests do not inherit mdhq generic
150
+ headers, the configured mdhq User-Agent, or mdhq's timeout,
151
+ response-size, and redirect limits.
152
+
153
+ The effective asynchronous-extractor setting is:
154
+
155
+ 1. `GetPageOptions.useAsync`
156
+ 2. `defuddle.useAsync`
157
+ 3. top-level `useAsync`
158
+ 4. `true`
159
+
160
+ ## Frontmatter configuration
161
+
162
+ `frontmatter.exclude` is an array of field names to remove from extracted or
163
+ derived metadata.
164
+
165
+ `frontmatter.values` is an object of scalar fixed values. Accepted values are
166
+ strings, numbers, booleans, and `null`.
167
+
168
+ Application order:
169
+
170
+ 1. Add extracted metadata and representative-image fields.
171
+ 2. Remove fields listed in `exclude`.
172
+ 3. Apply fields from `values`.
173
+ 4. Add protected mdhq fields.
174
+
175
+ Protected fields are:
176
+
177
+ - `source`
178
+ - `requested_url`
179
+ - `created`
180
+ - `modified`
181
+ - `content_digest`
182
+ - `etag`
183
+ - `last_modified`
184
+ - `vary`
185
+
186
+ `content_digest` and `vary` are reserved legacy names and are removed from
187
+ serialized frontmatter even if configured in `frontmatter.values`.
188
+
189
+ mdhq does not add a `type` property by default. Add one explicitly when
190
+ desired:
191
+
192
+ ```json
193
+ {
194
+ "frontmatter": {
195
+ "values": {
196
+ "type": "clip"
197
+ }
198
+ }
199
+ }
200
+ ```
201
+
202
+ Protected fields cannot be removed or overridden by configuration.
203
+
204
+ ## Host and path matching
205
+
206
+ `hosts` is an object whose keys are exact host strings or minimatch glob
207
+ patterns.
208
+
209
+ Before matching, URL hosts are lowercased, converted to IDNA ASCII, and
210
+ normalized for trailing DNS dots and ports. Host patterns are also lowercased
211
+ and literal labels are converted to IDNA ASCII. Explicit ports in patterns
212
+ are retained. A pattern such as `example.com:443` therefore matches an
213
+ explicit non-standard HTTP port 443, while an HTTPS URL using its standard
214
+ port matches `example.com`.
215
+
216
+ Selection order:
217
+
218
+ 1. A normalized exact match.
219
+ 2. The matching glob with the greatest number of fixed literal characters.
220
+ 3. No host configuration.
221
+
222
+ If multiple matching patterns have equal specificity, processing fails with
223
+ `CONFIG_ERROR`. Multiple patterns that normalize to the same exact host are
224
+ also an error.
225
+
226
+ After selecting a host, `paths` is matched against the URL's encoded
227
+ `pathname` using the same exact-then-most-specific policy. Path patterns are
228
+ not IDNA- or Unicode-normalized.
229
+
230
+ ## Entry query key inheritance
231
+
232
+ `entryQueryKey` selects the only query parameter that contributes to URL
233
+ identity and the storage path.
234
+
235
+ Rules:
236
+
237
+ - A host-level value applies to all paths by default.
238
+ - A matching path-level string overrides the host value.
239
+ - A matching path-level `null` disables the host value.
240
+ - A matching path object that omits `entryQueryKey` inherits the host value.
241
+ - A missing or empty parameter value falls back to normal path-based storage.
242
+ - All non-selected query parameters are ignored.
@@ -0,0 +1,275 @@
1
+ # Library API reference
2
+
3
+ The package is an ECMAScript module:
4
+
5
+ ```ts
6
+ import {
7
+ convertHtml,
8
+ getPage,
9
+ MdhqError
10
+ } from "@songmu/mdhq";
11
+ ```
12
+
13
+ Runtime exports:
14
+
15
+ - `convertHtml`
16
+ - `getPage`
17
+ - `MdhqError`
18
+
19
+ Type-only exports:
20
+
21
+ - `MdhqConfig`
22
+ - `MdhqErrorCode`
23
+ - `AssetResult`
24
+ - `ConvertedPage`
25
+ - `ConvertHtmlOptions`
26
+ - `GetPageOptions`
27
+ - `GetPageResult`
28
+ - `HeaderValue`
29
+ - `MdhqWarning`
30
+ - `PageMetadata`
31
+
32
+ TypeScript callers should import types with `import type`:
33
+
34
+ ```ts
35
+ import { getPage, MdhqError } from "@songmu/mdhq";
36
+ import type { GetPageOptions, GetPageResult, MdhqConfig } from "@songmu/mdhq";
37
+ ```
38
+
39
+ ## `convertHtml`
40
+
41
+ ```ts
42
+ function convertHtml(options: ConvertHtmlOptions): Promise<ConvertedPage>
43
+ ```
44
+
45
+ This is the low-level conversion API. It does not fetch the page, download
46
+ assets, add frontmatter, or write files.
47
+
48
+ ### Options
49
+
50
+ ```ts
51
+ interface ConvertHtmlOptions {
52
+ html: string;
53
+ url: string | URL;
54
+ defuddle?: Omit<DefuddleOptions, "markdown" | "url">;
55
+ }
56
+ ```
57
+
58
+ - `html` is the complete input HTML string.
59
+ - `url` is an absolute base URL. Unlike `getPage`, it is not restricted to
60
+ HTTP or HTTPS; for example, a `file:` URL is accepted.
61
+ - `defuddle` passes options to Defuddle.
62
+
63
+ mdhq forces `markdown: true`. `useAsync` defaults to `true` when it is not
64
+ provided.
65
+
66
+ ### Result
67
+
68
+ ```ts
69
+ interface ConvertedPage {
70
+ markdown: string;
71
+ metadata: PageMetadata;
72
+ }
73
+ ```
74
+
75
+ ```ts
76
+ interface PageMetadata {
77
+ title?: string;
78
+ description?: string;
79
+ author?: string;
80
+ published?: string;
81
+ updated?: string;
82
+ site?: string;
83
+ domain?: string;
84
+ language?: string;
85
+ image?: string;
86
+ favicon?: string;
87
+ wordCount?: number;
88
+ }
89
+ ```
90
+
91
+ Empty string metadata is omitted. `wordCount` is omitted unless it is greater
92
+ than zero.
93
+
94
+ ## `getPage`
95
+
96
+ ```ts
97
+ function getPage(options: GetPageOptions): Promise<GetPageResult>
98
+ ```
99
+
100
+ This is the high-level fetch, convert, localize, and save API.
101
+
102
+ ### Options
103
+
104
+ ```ts
105
+ interface GetPageOptions {
106
+ url: string | URL;
107
+ root?: string;
108
+ configPath?: string;
109
+ assets?: boolean;
110
+ update?: boolean;
111
+ headers?: HeaderValue[];
112
+ userAgent?: string;
113
+ timeoutMs?: number;
114
+ maxResponseBytes?: number;
115
+ maxRedirects?: number;
116
+ useAsync?: boolean;
117
+ now?: () => Date;
118
+ onWarning?: (warning: MdhqWarning) => void;
119
+ }
120
+ ```
121
+
122
+ | Field | Behavior |
123
+ | --- | --- |
124
+ | `url` | Required HTTP or HTTPS page URL. |
125
+ | `root` | Highest-precedence library storage root. |
126
+ | `configPath` | Override the default XDG configuration path. |
127
+ | `assets` | Download and localize images when `true`; overrides configuration and defaults to `true`. When `false`, image destinations remain absolute and `_assets` is not created. |
128
+ | `update` | Replace an existing same-identity document when `true`; defaults to `false`. |
129
+ | `headers` | Additional page headers. Sent only while requests remain on the initial page origin. A cross-origin page redirect permanently disables them for later page and asset requests in that operation. |
130
+ | `userAgent` | Override the configured or built-in User-Agent. |
131
+ | `timeoutMs` | Override the configured or built-in per-request timeout. |
132
+ | `maxResponseBytes` | Override the configured or built-in response-size limit. |
133
+ | `maxRedirects` | Override the configured or built-in redirect limit. |
134
+ | `useAsync` | Highest-precedence Defuddle asynchronous-extractor setting. |
135
+ | `now` | Clock injection used for `created` and `modified`; intended for deterministic callers and tests. Both fields are written on initial acquisition. |
136
+ | `onWarning` | Called once for each warning as it is produced. |
137
+
138
+ ### Headers
139
+
140
+ ```ts
141
+ interface HeaderValue {
142
+ name: string;
143
+ value: string;
144
+ }
145
+ ```
146
+
147
+ Multiple entries with the same name are appended.
148
+
149
+ ### Result
150
+
151
+ ```ts
152
+ interface GetPageResult {
153
+ requestedUrl: string;
154
+ sourceUrl: string;
155
+ path: string;
156
+ status: "saved" | "updated" | "unchanged" | "skipped";
157
+ assets: AssetResult[];
158
+ warnings: MdhqWarning[];
159
+ }
160
+ ```
161
+
162
+ - `requestedUrl` is the input after WHATWG URL serialization. This lowercases
163
+ and IDNA-normalizes the host, removes an explicit default port, and retains
164
+ the query and fragment. This serialization is separate from URL identity,
165
+ which ignores the scheme, fragment, and unselected query parameters.
166
+ - `sourceUrl` is the WHATWG-serialized final URL maintained by the redirect
167
+ loop. Its query and fragment are whatever remain on that final URL after
168
+ URL resolution. It is exactly the value written to frontmatter `source`.
169
+ For a pre-fetch skip, it is the existing document's stored `source` value.
170
+ - `path` is the absolute Markdown path.
171
+ - `assets` is empty when page processing is skipped.
172
+ - `warnings` contains configuration and asset warnings.
173
+
174
+ Status meanings:
175
+
176
+ - `saved`: a new document was created.
177
+ - `updated`: an existing document's normalized Markdown body or user-facing
178
+ frontmatter changed.
179
+ - `unchanged`: HTTP returned 304, or a 200 response produced the same
180
+ normalized Markdown body and user-facing frontmatter. HTTP validators may
181
+ still be updated.
182
+ - `skipped`: an existing same-identity document was found without `update`.
183
+
184
+ With `update`, `getPage` sends the stored `etag` as `If-None-Match`, or falls
185
+ back to the stored `last_modified` as `If-Modified-Since`, when the request is
186
+ for the same HTTP target and does not include credentials. `etag` and
187
+ `last_modified` are stored in Markdown frontmatter only when the response is
188
+ safe to revalidate; `Vary` and body digests are not serialized.
189
+
190
+ ### Asset results
191
+
192
+ ```ts
193
+ interface AssetResult {
194
+ sourceUrl: string;
195
+ finalUrl?: string;
196
+ path?: string;
197
+ status: "saved" | "reused" | "failed";
198
+ error?: string;
199
+ }
200
+ ```
201
+
202
+ - `sourceUrl` is the absolute pre-fetch asset URL discovered in Markdown or
203
+ representative-image metadata.
204
+ - `path` is the absolute local asset path.
205
+ - `saved` means a new immutable content-addressed asset file was created.
206
+ - `reused` means the deterministic asset path already existed with identical
207
+ bytes.
208
+ - `failed` means the Markdown operation continued without localizing that
209
+ asset, including the unlikely case of differing bytes at the same digest
210
+ and extension path.
211
+ - `finalUrl` and `path` are present for successful asset operations.
212
+ - `error` is present for failed operations.
213
+
214
+ The `assets` array follows first-discovery order. Markdown images are listed
215
+ in document traversal order, followed by the representative image when it was
216
+ not already discovered. Exact duplicate source URLs produce one result.
217
+ Different source URLs that redirect to the same final URL produce separate
218
+ results but reuse the same deterministic file.
219
+
220
+ ### Warnings
221
+
222
+ ```ts
223
+ interface MdhqWarning {
224
+ code: string;
225
+ message: string;
226
+ url?: string;
227
+ }
228
+ ```
229
+
230
+ Current warning codes:
231
+
232
+ | Code | Meaning |
233
+ | --- | --- |
234
+ | `UNKNOWN_CONFIG_KEY` | A JSON configuration key is not recognized but processing continues. |
235
+ | `ASSET_FETCH_FAILED` | An individual asset could not be fetched, validated, or saved. |
236
+ | `INVALID_IMAGE_URL` | Defuddle returned an invalid or non-HTTP(S) representative-image URL; the page is saved without that metadata field. |
237
+ | `INVALID_LAST_MODIFIED` | A response contained an invalid `Last-Modified` value; the page is saved without that validator. |
238
+
239
+ Warnings are both accumulated in the result and delivered to `onWarning`.
240
+
241
+ ## Error model
242
+
243
+ Fatal operational errors are represented by:
244
+
245
+ ```ts
246
+ class MdhqError extends Error {
247
+ readonly code: MdhqErrorCode;
248
+ readonly cause?: unknown;
249
+ }
250
+ ```
251
+
252
+ Current error codes:
253
+
254
+ | Code | Meaning |
255
+ | --- | --- |
256
+ | `INVALID_URL` | A URL or URL path encoding is invalid. |
257
+ | `INVALID_HEADER` | A CLI `--header` value does not use `Name: value` syntax. |
258
+ | `UNSUPPORTED_SCHEME` | The high-level operation received a non-HTTP(S) URL or a redirect used one. |
259
+ | `UNSUPPORTED_CONTENT_TYPE` | A page response was not HTML or XHTML. |
260
+ | `RESPONSE_TOO_LARGE` | A page exceeded the configured byte limit. The same condition for an individual asset is converted to an `ASSET_FETCH_FAILED` warning and failed asset result by `getPage`. |
261
+ | `TOO_MANY_REDIRECTS` | The configured redirect limit was exceeded. |
262
+ | `FETCH_FAILED` | Network, timeout, HTTP-status, or redirect metadata failure. |
263
+ | `CONVERSION_FAILED` | Defuddle failed or produced no usable Markdown. |
264
+ | `CONFIG_ERROR` | Configuration reading, syntax, type validation, or pattern selection failed. |
265
+ | `PATH_COLLISION` | A destination belongs to a different URL identity or cannot be identified safely. |
266
+ | `PATH_TOO_LONG` | The destination cannot fit after path-segment hashing. |
267
+ | `STORAGE_ERROR` | A Markdown read, create, temporary write, or replacement failed. |
268
+
269
+ Callers should branch on `error.code` rather than parsing `error.message`.
270
+
271
+ Errors produced while processing an individual asset do not escape
272
+ `getPage`. They are converted into `ASSET_FETCH_FAILED` warnings and
273
+ `AssetResult` objects with `status: "failed"`. The error table describes
274
+ fatal page-level and Markdown-storage failures unless a row explicitly says
275
+ otherwise.