@ozzylabs/feedradar 0.1.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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/dist/agents/_boundary.d.ts +44 -0
  4. package/dist/agents/_boundary.d.ts.map +1 -0
  5. package/dist/agents/_boundary.js +59 -0
  6. package/dist/agents/_boundary.js.map +1 -0
  7. package/dist/agents/claude-code.d.ts +32 -0
  8. package/dist/agents/claude-code.d.ts.map +1 -0
  9. package/dist/agents/claude-code.js +256 -0
  10. package/dist/agents/claude-code.js.map +1 -0
  11. package/dist/agents/codex-cli.d.ts +31 -0
  12. package/dist/agents/codex-cli.d.ts.map +1 -0
  13. package/dist/agents/codex-cli.js +303 -0
  14. package/dist/agents/codex-cli.js.map +1 -0
  15. package/dist/agents/copilot.d.ts +29 -0
  16. package/dist/agents/copilot.d.ts.map +1 -0
  17. package/dist/agents/copilot.js +282 -0
  18. package/dist/agents/copilot.js.map +1 -0
  19. package/dist/agents/gemini-cli.d.ts +30 -0
  20. package/dist/agents/gemini-cli.d.ts.map +1 -0
  21. package/dist/agents/gemini-cli.js +316 -0
  22. package/dist/agents/gemini-cli.js.map +1 -0
  23. package/dist/agents/index.d.ts +12 -0
  24. package/dist/agents/index.d.ts.map +1 -0
  25. package/dist/agents/index.js +33 -0
  26. package/dist/agents/index.js.map +1 -0
  27. package/dist/agents/types.d.ts +103 -0
  28. package/dist/agents/types.d.ts.map +1 -0
  29. package/dist/agents/types.js +2 -0
  30. package/dist/agents/types.js.map +1 -0
  31. package/dist/claude-skills/dismiss/SKILL.md +41 -0
  32. package/dist/claude-skills/research/SKILL.md +45 -0
  33. package/dist/claude-skills/review/SKILL.md +45 -0
  34. package/dist/claude-skills/update/SKILL.md +49 -0
  35. package/dist/cli/dismiss.d.ts +28 -0
  36. package/dist/cli/dismiss.d.ts.map +1 -0
  37. package/dist/cli/dismiss.js +122 -0
  38. package/dist/cli/dismiss.js.map +1 -0
  39. package/dist/cli/index.d.ts +7 -0
  40. package/dist/cli/index.d.ts.map +1 -0
  41. package/dist/cli/index.js +64 -0
  42. package/dist/cli/index.js.map +1 -0
  43. package/dist/cli/init.d.ts +148 -0
  44. package/dist/cli/init.d.ts.map +1 -0
  45. package/dist/cli/init.js +578 -0
  46. package/dist/cli/init.js.map +1 -0
  47. package/dist/cli/research.d.ts +30 -0
  48. package/dist/cli/research.d.ts.map +1 -0
  49. package/dist/cli/research.js +313 -0
  50. package/dist/cli/research.js.map +1 -0
  51. package/dist/cli/review.d.ts +34 -0
  52. package/dist/cli/review.d.ts.map +1 -0
  53. package/dist/cli/review.js +418 -0
  54. package/dist/cli/review.js.map +1 -0
  55. package/dist/cli/source.d.ts +57 -0
  56. package/dist/cli/source.d.ts.map +1 -0
  57. package/dist/cli/source.js +511 -0
  58. package/dist/cli/source.js.map +1 -0
  59. package/dist/cli/update.d.ts +43 -0
  60. package/dist/cli/update.d.ts.map +1 -0
  61. package/dist/cli/update.js +429 -0
  62. package/dist/cli/update.js.map +1 -0
  63. package/dist/cli/watch.d.ts +22 -0
  64. package/dist/cli/watch.d.ts.map +1 -0
  65. package/dist/cli/watch.js +101 -0
  66. package/dist/cli/watch.js.map +1 -0
  67. package/dist/core/config.d.ts +60 -0
  68. package/dist/core/config.d.ts.map +1 -0
  69. package/dist/core/config.js +101 -0
  70. package/dist/core/config.js.map +1 -0
  71. package/dist/core/feeds/derive-id.d.ts +43 -0
  72. package/dist/core/feeds/derive-id.d.ts.map +1 -0
  73. package/dist/core/feeds/derive-id.js +66 -0
  74. package/dist/core/feeds/derive-id.js.map +1 -0
  75. package/dist/core/feeds/github-api.d.ts +69 -0
  76. package/dist/core/feeds/github-api.d.ts.map +1 -0
  77. package/dist/core/feeds/github-api.js +161 -0
  78. package/dist/core/feeds/github-api.js.map +1 -0
  79. package/dist/core/feeds/github-releases.d.ts +3 -0
  80. package/dist/core/feeds/github-releases.d.ts.map +1 -0
  81. package/dist/core/feeds/github-releases.js +85 -0
  82. package/dist/core/feeds/github-releases.js.map +1 -0
  83. package/dist/core/feeds/html.d.ts +10 -0
  84. package/dist/core/feeds/html.d.ts.map +1 -0
  85. package/dist/core/feeds/html.js +263 -0
  86. package/dist/core/feeds/html.js.map +1 -0
  87. package/dist/core/feeds/index.d.ts +5 -0
  88. package/dist/core/feeds/index.d.ts.map +1 -0
  89. package/dist/core/feeds/index.js +18 -0
  90. package/dist/core/feeds/index.js.map +1 -0
  91. package/dist/core/feeds/npm-registry.d.ts +36 -0
  92. package/dist/core/feeds/npm-registry.d.ts.map +1 -0
  93. package/dist/core/feeds/npm-registry.js +200 -0
  94. package/dist/core/feeds/npm-registry.js.map +1 -0
  95. package/dist/core/feeds/rss.d.ts +12 -0
  96. package/dist/core/feeds/rss.d.ts.map +1 -0
  97. package/dist/core/feeds/rss.js +222 -0
  98. package/dist/core/feeds/rss.js.map +1 -0
  99. package/dist/core/feeds/types.d.ts +45 -0
  100. package/dist/core/feeds/types.d.ts.map +1 -0
  101. package/dist/core/feeds/types.js +2 -0
  102. package/dist/core/feeds/types.js.map +1 -0
  103. package/dist/core/filter.d.ts +25 -0
  104. package/dist/core/filter.d.ts.map +1 -0
  105. package/dist/core/filter.js +123 -0
  106. package/dist/core/filter.js.map +1 -0
  107. package/dist/core/injection-detector.d.ts +57 -0
  108. package/dist/core/injection-detector.d.ts.map +1 -0
  109. package/dist/core/injection-detector.js +109 -0
  110. package/dist/core/injection-detector.js.map +1 -0
  111. package/dist/core/items.d.ts +20 -0
  112. package/dist/core/items.d.ts.map +1 -0
  113. package/dist/core/items.js +105 -0
  114. package/dist/core/items.js.map +1 -0
  115. package/dist/core/state.d.ts +12 -0
  116. package/dist/core/state.d.ts.map +1 -0
  117. package/dist/core/state.js +42 -0
  118. package/dist/core/state.js.map +1 -0
  119. package/dist/core/templates.d.ts +21 -0
  120. package/dist/core/templates.d.ts.map +1 -0
  121. package/dist/core/templates.js +52 -0
  122. package/dist/core/templates.js.map +1 -0
  123. package/dist/core/watcher.d.ts +72 -0
  124. package/dist/core/watcher.d.ts.map +1 -0
  125. package/dist/core/watcher.js +240 -0
  126. package/dist/core/watcher.js.map +1 -0
  127. package/dist/gemini-commands/dismiss.toml +2 -0
  128. package/dist/gemini-commands/research.toml +2 -0
  129. package/dist/gemini-commands/review.toml +2 -0
  130. package/dist/gemini-commands/update.toml +2 -0
  131. package/dist/index.d.ts +3 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +8 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/schemas/config.d.ts +39 -0
  136. package/dist/schemas/config.d.ts.map +1 -0
  137. package/dist/schemas/config.js +23 -0
  138. package/dist/schemas/config.js.map +1 -0
  139. package/dist/schemas/index.d.ts +6 -0
  140. package/dist/schemas/index.d.ts.map +1 -0
  141. package/dist/schemas/index.js +6 -0
  142. package/dist/schemas/index.js.map +1 -0
  143. package/dist/schemas/item.d.ts +38 -0
  144. package/dist/schemas/item.d.ts.map +1 -0
  145. package/dist/schemas/item.js +34 -0
  146. package/dist/schemas/item.js.map +1 -0
  147. package/dist/schemas/research.d.ts +82 -0
  148. package/dist/schemas/research.d.ts.map +1 -0
  149. package/dist/schemas/research.js +45 -0
  150. package/dist/schemas/research.js.map +1 -0
  151. package/dist/schemas/source.d.ts +139 -0
  152. package/dist/schemas/source.d.ts.map +1 -0
  153. package/dist/schemas/source.js +127 -0
  154. package/dist/schemas/source.js.map +1 -0
  155. package/dist/schemas/state.d.ts +19 -0
  156. package/dist/schemas/state.d.ts.map +1 -0
  157. package/dist/schemas/state.js +12 -0
  158. package/dist/schemas/state.js.map +1 -0
  159. package/dist/skills/research/SKILL.md +156 -0
  160. package/dist/skills/review/SKILL.md +173 -0
  161. package/dist/skills/update/SKILL.md +200 -0
  162. package/dist/templates/agents/AGENTS.md +161 -0
  163. package/dist/templates/claude/CLAUDE.md +5 -0
  164. package/dist/templates/default.md +16 -0
  165. package/dist/templates/feedradar.md +165 -0
  166. package/dist/templates/routines/watch-daily.md +42 -0
  167. package/dist/templates/workflows/watch.yaml +70 -0
  168. package/package.json +73 -0
@@ -0,0 +1,263 @@
1
+ import { createHash } from "node:crypto";
2
+ import { parse as parseHtml } from "node-html-parser";
3
+ import { ItemSchema } from "../../schemas/index.js";
4
+ import { deriveItemId, deriveStableKey } from "./derive-id.js";
5
+ const USER_AGENT = "feedradar/0.0.0 (+https://github.com/ozzy-labs/feedradar)";
6
+ /**
7
+ * Prefix that flags an `lastEtag` slot as carrying a content hash rather than
8
+ * an actual HTTP ETag. We reuse the `lastEtag` field so this Phase does not
9
+ * have to migrate `SourceState` (see `docs/design/source-html.md`).
10
+ */
11
+ const CONTENT_HASH_PREFIX = "sha256:";
12
+ /** Attributes the adapter checks before falling back to text content. */
13
+ const DATETIME_ATTRS = ["datetime", "content", "value"];
14
+ /**
15
+ * Convert an `HTMLElement | null` to its trimmed text, or `undefined` when
16
+ * the selector did not match. We always trim because raw scrapes routinely
17
+ * carry surrounding whitespace from formatted markup.
18
+ */
19
+ function textOf(el) {
20
+ if (!el)
21
+ return undefined;
22
+ const text = el.text?.trim();
23
+ return text ? text : undefined;
24
+ }
25
+ /**
26
+ * Apply a CSS selector relative to `root` and return the first match.
27
+ * `node-html-parser` returns `null` instead of throwing for invalid input,
28
+ * which matches what callers want here (a missing field, not a hard error).
29
+ */
30
+ function queryFirst(root, selector) {
31
+ return root.querySelector(selector);
32
+ }
33
+ /**
34
+ * Resolve the `link` selector to an `href` (or text fallback).
35
+ *
36
+ * Anchor tags expose the URL via `href` so we prefer the attribute. When the
37
+ * selector points at a non-anchor (e.g. a `<div data-link>` wrapper used by
38
+ * some changelog layouts), we fall back to text content so the adapter can
39
+ * still operate, deferring URL validation to `ItemSchema`.
40
+ */
41
+ function pickLink(el) {
42
+ if (!el)
43
+ return undefined;
44
+ const href = el.getAttribute("href");
45
+ if (href && href.trim())
46
+ return href.trim();
47
+ return textOf(el);
48
+ }
49
+ /**
50
+ * Resolve `publishedAt` to a candidate string for `new Date()`.
51
+ *
52
+ * `<time datetime="2026-05-12">` and `<meta content="..."/>` markup hide the
53
+ * canonical timestamp in attributes; the visible text is often a
54
+ * localized "May 12, 2026" that is harder to parse reliably. We probe the
55
+ * known attributes first, then fall back to element text.
56
+ */
57
+ function pickDatetime(el) {
58
+ if (!el)
59
+ return undefined;
60
+ for (const attr of DATETIME_ATTRS) {
61
+ const value = el.getAttribute(attr);
62
+ if (value && value.trim())
63
+ return value.trim();
64
+ }
65
+ return textOf(el);
66
+ }
67
+ /**
68
+ * Try to parse a candidate timestamp into ISO 8601. Returns `undefined` for
69
+ * unparseable inputs so the item can still be emitted (RSS adapter parity).
70
+ */
71
+ function toIsoDate(value) {
72
+ if (!value)
73
+ return undefined;
74
+ const date = new Date(value);
75
+ if (Number.isNaN(date.getTime()))
76
+ return undefined;
77
+ return date.toISOString();
78
+ }
79
+ /** Collect the trimmed text of every match for `selector`. */
80
+ function collectTags(root, selector) {
81
+ if (!selector)
82
+ return undefined;
83
+ const tags = root
84
+ .querySelectorAll(selector)
85
+ .map((el) => el.text?.trim())
86
+ .filter((t) => !!t && t.length > 0);
87
+ return tags.length > 0 ? tags : undefined;
88
+ }
89
+ /**
90
+ * Resolve a relative `link` against the source URL.
91
+ *
92
+ * Many sites publish `<a href="/changelog/foo">` rather than absolute URLs;
93
+ * without resolution `ItemSchema`'s `z.string().url()` would drop them. We
94
+ * intentionally swallow `URL` constructor errors so a malformed `link`
95
+ * surfaces as a normal validation drop later instead of breaking the whole
96
+ * fetch.
97
+ */
98
+ function resolveUrl(raw, base) {
99
+ try {
100
+ return new URL(raw, base).toString();
101
+ }
102
+ catch {
103
+ return raw;
104
+ }
105
+ }
106
+ /** Normalize one matched element into an Item, or `null` to drop it. */
107
+ function parseItem(itemEl, selectors, source, fetchedAt) {
108
+ const title = textOf(queryFirst(itemEl, selectors.title));
109
+ const linkRaw = pickLink(queryFirst(itemEl, selectors.link));
110
+ if (!title || !linkRaw)
111
+ return null;
112
+ const url = resolveUrl(linkRaw, source.url);
113
+ const summary = selectors.summary ? textOf(queryFirst(itemEl, selectors.summary)) : undefined;
114
+ const body = selectors.body ? textOf(queryFirst(itemEl, selectors.body)) : undefined;
115
+ const publishedAt = selectors.publishedAt
116
+ ? toIsoDate(pickDatetime(queryFirst(itemEl, selectors.publishedAt)))
117
+ : undefined;
118
+ const tags = collectTags(itemEl, selectors.tags);
119
+ const stableKey = deriveStableKey({
120
+ url,
121
+ fallbackHashInputs: [title, publishedAt],
122
+ });
123
+ const id = deriveItemId(title, stableKey);
124
+ // Preserve a structured snapshot of the raw scrape rather than the
125
+ // `HTMLElement` instance itself — the watcher serializes `raw` to YAML and
126
+ // we want the on-disk payload to be diff-friendly.
127
+ const raw = { title, link: linkRaw };
128
+ if (summary !== undefined)
129
+ raw.summary = summary;
130
+ if (body !== undefined)
131
+ raw.body = body;
132
+ if (publishedAt !== undefined)
133
+ raw.publishedAt = publishedAt;
134
+ if (tags !== undefined)
135
+ raw.tags = tags;
136
+ return validateItem({
137
+ id,
138
+ sourceId: source.id,
139
+ title,
140
+ url,
141
+ summary,
142
+ publishedAt,
143
+ fetchedAt,
144
+ raw,
145
+ });
146
+ }
147
+ function validateItem(candidate) {
148
+ const result = ItemSchema.safeParse(candidate);
149
+ // Items that fail validation (e.g. unresolvable URL) are dropped silently —
150
+ // see rss.ts for the same fail-soft rationale.
151
+ return result.success ? result.data : null;
152
+ }
153
+ /**
154
+ * Parse an HTML document into validated `Item[]` using the source's
155
+ * `selectors`. Exported so tests can drive the parser directly without
156
+ * needing a fake HTTP layer.
157
+ */
158
+ export function parseHtmlDocument(html, source, fetchedAt) {
159
+ if (!source.selectors) {
160
+ throw new Error(`html adapter: source '${source.id}' has no selectors`);
161
+ }
162
+ const selectors = source.selectors;
163
+ let root;
164
+ try {
165
+ root = parseHtml(html);
166
+ }
167
+ catch (e) {
168
+ throw new Error(`html adapter: failed to parse HTML: ${e instanceof Error ? e.message : String(e)}`);
169
+ }
170
+ const itemEls = root.querySelectorAll(selectors.item);
171
+ return itemEls
172
+ .map((el) => parseItem(el, selectors, source, fetchedAt))
173
+ .filter((i) => i !== null);
174
+ }
175
+ /**
176
+ * Compute the sha256 of the raw response body, prefixed so callers can tell
177
+ * it apart from a real ETag inside `SourceState.lastEtag`.
178
+ */
179
+ function contentHash(body) {
180
+ return `${CONTENT_HASH_PREFIX}${createHash("sha256").update(body).digest("hex")}`;
181
+ }
182
+ /**
183
+ * Issue an HTTP GET with conditional headers. The previous `lastEtag` slot
184
+ * may contain either an actual ETag (mirror RSS behavior) or a `sha256:`
185
+ * content hash; we only forward real ETags as `If-None-Match`.
186
+ */
187
+ async function fetchHtml(url, fetchImpl, options = {}) {
188
+ const headers = {
189
+ accept: "text/html, application/xhtml+xml;q=0.9, */*;q=0.5",
190
+ "user-agent": USER_AGENT,
191
+ };
192
+ // Only forward the previous value to the server when it looks like a real
193
+ // ETag; a `sha256:` slot is our own dedup marker, not something the server
194
+ // sent us.
195
+ if (options.etag && !options.etag.startsWith(CONTENT_HASH_PREFIX)) {
196
+ headers["if-none-match"] = options.etag;
197
+ }
198
+ const response = await fetchImpl(url, { headers, signal: options.signal });
199
+ const etag = response.headers.get("etag");
200
+ if (response.status === 304) {
201
+ return { status: 304, body: "", etag };
202
+ }
203
+ if (response.status < 200 || response.status >= 300) {
204
+ throw new Error(`html adapter: HTTP ${response.status} from ${url}`);
205
+ }
206
+ const body = await response.text();
207
+ return { status: response.status, body, etag };
208
+ }
209
+ export const htmlAdapter = {
210
+ kind: "html",
211
+ fetch: async (source, options = {}) => {
212
+ if (!source.selectors) {
213
+ throw new Error(`html adapter: source '${source.id}' has no selectors`);
214
+ }
215
+ const fetchImpl = options.fetch ?? globalThis.fetch;
216
+ if (typeof fetchImpl !== "function") {
217
+ throw new Error("html adapter: no fetch implementation available (Node 22+ required)");
218
+ }
219
+ const previous = options.state;
220
+ const fetchedAt = new Date().toISOString();
221
+ const response = await fetchHtml(source.url, fetchImpl, {
222
+ etag: previous?.lastEtag,
223
+ });
224
+ if (response.status === 304) {
225
+ return {
226
+ items: [],
227
+ notModified: true,
228
+ state: {
229
+ lastFetchedAt: fetchedAt,
230
+ // Preserve whatever marker we had — server may not echo the ETag
231
+ // back on 304, in which case we keep the previous content hash too.
232
+ lastEtag: response.etag ?? previous?.lastEtag,
233
+ },
234
+ };
235
+ }
236
+ // Content-hash fallback: when the server does not return an ETag at all,
237
+ // compare a sha256 of the body against the previous one we recorded so
238
+ // re-fetches without a real ETag still dedup correctly.
239
+ const bodyHash = contentHash(response.body);
240
+ const previousMarker = previous?.lastEtag;
241
+ if (!response.etag && previousMarker === bodyHash) {
242
+ return {
243
+ items: [],
244
+ notModified: true,
245
+ state: {
246
+ lastFetchedAt: fetchedAt,
247
+ lastEtag: bodyHash,
248
+ },
249
+ };
250
+ }
251
+ const items = parseHtmlDocument(response.body, source, fetchedAt);
252
+ return {
253
+ items,
254
+ state: {
255
+ lastFetchedAt: fetchedAt,
256
+ // Prefer the real ETag when the server provides one; otherwise stash
257
+ // the content hash in the same slot for next-run dedup.
258
+ lastEtag: response.etag ?? bodyHash,
259
+ },
260
+ };
261
+ },
262
+ };
263
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../../../src/core/feeds/html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAoB,KAAK,IAAI,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAExE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG/D,MAAM,UAAU,GAAG,2DAA2D,CAAC;AAE/E;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAEtC,yEAAyE;AACzE,MAAM,cAAc,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,CAAU,CAAC;AAEjE;;;;GAIG;AACH,SAAS,MAAM,CAAC,EAAsB;IACpC,IAAI,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAC1B,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;IAC7B,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU,CAAC,IAAiB,EAAE,QAAgB;IACrD,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,QAAQ,CAAC,EAAsB;IACtC,IAAI,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAC1B,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACrC,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5C,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,YAAY,CAAC,EAAsB;IAC1C,IAAI,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE;YAAE,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAAC,KAAyB;IAC1C,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IACnD,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;AAC5B,CAAC;AAED,8DAA8D;AAC9D,SAAS,WAAW,CAAC,IAAiB,EAAE,QAA4B;IAClE,IAAI,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI;SACd,gBAAgB,CAAC,QAAQ,CAAC;SAC1B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACnD,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,UAAU,CAAC,GAAW,EAAE,IAAY;IAC3C,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC;AAED,wEAAwE;AACxE,SAAS,SAAS,CAChB,MAAmB,EACnB,SAA0B,EAC1B,MAAc,EACd,SAAiB;IAEjB,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7D,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9F,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACrF,MAAM,WAAW,GAAG,SAAS,CAAC,WAAW;QACvC,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC;QACpE,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAEjD,MAAM,SAAS,GAAG,eAAe,CAAC;QAChC,GAAG;QACH,kBAAkB,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC;KACzC,CAAC,CAAC;IACH,MAAM,EAAE,GAAG,YAAY,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAE1C,mEAAmE;IACnE,2EAA2E;IAC3E,mDAAmD;IACnD,MAAM,GAAG,GAA4B,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC9D,IAAI,OAAO,KAAK,SAAS;QAAE,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC;IACjD,IAAI,IAAI,KAAK,SAAS;QAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IACxC,IAAI,WAAW,KAAK,SAAS;QAAE,GAAG,CAAC,WAAW,GAAG,WAAW,CAAC;IAC7D,IAAI,IAAI,KAAK,SAAS;QAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAExC,OAAO,YAAY,CAAC;QAClB,EAAE;QACF,QAAQ,EAAE,MAAM,CAAC,EAAE;QACnB,KAAK;QACL,GAAG;QACH,OAAO;QACP,WAAW;QACX,SAAS;QACT,GAAG;KACJ,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY,CAAC,SAAkC;IACtD,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/C,4EAA4E;IAC5E,+CAA+C;IAC/C,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,MAAc,EAAE,SAAiB;IAC/E,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,yBAAyB,MAAM,CAAC,EAAE,oBAAoB,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IACnC,IAAI,IAAiB,CAAC;IACtB,IAAI,CAAC;QACH,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,uCAAuC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CACpF,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtD,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;SACxD,MAAM,CAAC,CAAC,CAAC,EAAa,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AAC1C,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,GAAG,mBAAmB,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACpF,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,SAAS,CACtB,GAAW,EACX,SAAoB,EACpB,UAAmD,EAAE;IAMrD,MAAM,OAAO,GAA2B;QACtC,MAAM,EAAE,mDAAmD;QAC3D,YAAY,EAAE,UAAU;KACzB,CAAC;IACF,0EAA0E;IAC1E,2EAA2E;IAC3E,WAAW;IACX,IAAI,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAClE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1C,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3E,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACzC,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAgB;IACtC,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,KAAK,EAAE,MAAc,EAAE,UAA8B,EAAE,EAAE,EAAE;QAChE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,yBAAyB,MAAM,CAAC,EAAE,oBAAoB,CAAC,CAAC;QAC1E,CAAC;QACD,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,IAAK,UAAU,CAAC,KAA8B,CAAC;QAC9E,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;QACzF,CAAC;QACD,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC;QAC/B,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,SAAS,EAAE;YACtD,IAAI,EAAE,QAAQ,EAAE,QAAQ;SACzB,CAAC,CAAC;QACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,OAAO;gBACL,KAAK,EAAE,EAAE;gBACT,WAAW,EAAE,IAAI;gBACjB,KAAK,EAAE;oBACL,aAAa,EAAE,SAAS;oBACxB,iEAAiE;oBACjE,oEAAoE;oBACpE,QAAQ,EAAE,QAAQ,CAAC,IAAI,IAAI,QAAQ,EAAE,QAAQ;iBAC9C;aACF,CAAC;QACJ,CAAC;QAED,yEAAyE;QACzE,uEAAuE;QACvE,wDAAwD;QACxD,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,cAAc,GAAG,QAAQ,EAAE,QAAQ,CAAC;QAC1C,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,cAAc,KAAK,QAAQ,EAAE,CAAC;YAClD,OAAO;gBACL,KAAK,EAAE,EAAE;gBACT,WAAW,EAAE,IAAI;gBACjB,KAAK,EAAE;oBACL,aAAa,EAAE,SAAS;oBACxB,QAAQ,EAAE,QAAQ;iBACnB;aACF,CAAC;QACJ,CAAC;QAED,MAAM,KAAK,GAAG,iBAAiB,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAClE,OAAO;YACL,KAAK;YACL,KAAK,EAAE;gBACL,aAAa,EAAE,SAAS;gBACxB,qEAAqE;gBACrE,wDAAwD;gBACxD,QAAQ,EAAE,QAAQ,CAAC,IAAI,IAAI,QAAQ;aACpC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Source } from "../../schemas/index.js";
2
+ import type { FeedAdapter } from "./types.js";
3
+ export declare function getFeedAdapter(kind: Source["kind"]): FeedAdapter;
4
+ export type { FeedAdapter, FeedAdapterOptions, FeedFetchResult, FetchLike } from "./types.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAKrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAS9C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,WAAW,CAMhE;AAED,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { githubReleasesAdapter } from "./github-releases.js";
2
+ import { htmlAdapter } from "./html.js";
3
+ import { npmRegistryAdapter } from "./npm-registry.js";
4
+ import { rssAdapter } from "./rss.js";
5
+ const adapters = new Map([
6
+ [rssAdapter.kind, rssAdapter],
7
+ [htmlAdapter.kind, htmlAdapter],
8
+ [githubReleasesAdapter.kind, githubReleasesAdapter],
9
+ [npmRegistryAdapter.kind, npmRegistryAdapter],
10
+ ]);
11
+ export function getFeedAdapter(kind) {
12
+ const adapter = adapters.get(kind);
13
+ if (!adapter) {
14
+ throw new Error(`No feed adapter registered for kind: ${kind}`);
15
+ }
16
+ return adapter;
17
+ }
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/feeds/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAGtC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAA8B;IACpD,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC;IAC7B,CAAC,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;IAC/B,CAAC,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACnD,CAAC,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,CAAC;CAC9C,CAAC,CAAC;AAEH,MAAM,UAAU,cAAc,CAAC,IAAoB;IACjD,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { Item, Source } from "../../schemas/index.js";
2
+ import type { FeedAdapter } from "./types.js";
3
+ /**
4
+ * Extract the canonical npm package name from a source URL.
5
+ *
6
+ * Accepts both the bare-package form (`@scope/pkg` or `pkg`) and the public
7
+ * npmjs.com URL form (`https://www.npmjs.com/package/<pkg>` /
8
+ * `https://npmjs.com/package/<pkg>`). The latter is the only shape the strict
9
+ * `z.string().url()` validator used to accept, but per #38 the user-guide
10
+ * documents both forms; the schema now relaxes the constraint for
11
+ * `kind: "npm-registry"` so the helper here is the single place that knows
12
+ * about the two shapes.
13
+ *
14
+ * Returns `undefined` for inputs that look like an HTTP(S) URL but do not
15
+ * point at an npm package, so the caller can surface a clear error.
16
+ */
17
+ export declare function extractPackageName(rawUrl: string): string | undefined;
18
+ /**
19
+ * Compose the `https://registry.npmjs.org/<pkg>` URL.
20
+ *
21
+ * Scoped packages must keep their `@` and slash *unescaped* — the npm registry
22
+ * routes `@scope%2fname` and `@scope/name` differently and only the latter
23
+ * resolves. `encodeURIComponent` would mangle the slash, so we pass the name
24
+ * through verbatim.
25
+ */
26
+ export declare function buildMetadataUrl(packageName: string): string;
27
+ /**
28
+ * Parse an npm registry packument response into validated `Item[]`.
29
+ *
30
+ * The exported function exists so tests can exercise the normalizer without
31
+ * round-tripping through the HTTP layer (matching `parseFeedXml` in
32
+ * `rss.ts`).
33
+ */
34
+ export declare function parsePackument(body: string, source: Source, fetchedAt: string, packageName: string): Item[];
35
+ export declare const npmRegistryAdapter: FeedAdapter;
36
+ //# sourceMappingURL=npm-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"npm-registry.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/npm-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAG3D,OAAO,KAAK,EAAE,WAAW,EAAiC,MAAM,YAAY,CAAC;AAsB7E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqBrE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AA8DD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,IAAI,EAAE,CAiBR;AAkCD,eAAO,MAAM,kBAAkB,EAAE,WAwChC,CAAC"}
@@ -0,0 +1,200 @@
1
+ import { ItemSchema } from "../../schemas/index.js";
2
+ import { deriveItemId, deriveStableKey } from "./derive-id.js";
3
+ const USER_AGENT = "feedradar/0.0.0 (+https://github.com/ozzy-labs/feedradar)";
4
+ const REGISTRY_BASE = "https://registry.npmjs.org";
5
+ /**
6
+ * Extract the canonical npm package name from a source URL.
7
+ *
8
+ * Accepts both the bare-package form (`@scope/pkg` or `pkg`) and the public
9
+ * npmjs.com URL form (`https://www.npmjs.com/package/<pkg>` /
10
+ * `https://npmjs.com/package/<pkg>`). The latter is the only shape the strict
11
+ * `z.string().url()` validator used to accept, but per #38 the user-guide
12
+ * documents both forms; the schema now relaxes the constraint for
13
+ * `kind: "npm-registry"` so the helper here is the single place that knows
14
+ * about the two shapes.
15
+ *
16
+ * Returns `undefined` for inputs that look like an HTTP(S) URL but do not
17
+ * point at an npm package, so the caller can surface a clear error.
18
+ */
19
+ export function extractPackageName(rawUrl) {
20
+ const trimmed = rawUrl.trim();
21
+ if (trimmed.length === 0)
22
+ return undefined;
23
+ // URL form: pull the path segment after `/package/`.
24
+ if (/^https?:\/\//i.test(trimmed)) {
25
+ let parsed;
26
+ try {
27
+ parsed = new URL(trimmed);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ const host = parsed.host.toLowerCase();
33
+ if (host !== "npmjs.com" && host !== "www.npmjs.com")
34
+ return undefined;
35
+ // `/package/<pkg>` and `/package/<pkg>/v/<version>` are both valid; strip
36
+ // anything after the package segment.
37
+ const match = parsed.pathname.match(/^\/package\/((?:@[^/]+\/)?[^/]+)/);
38
+ if (!match)
39
+ return undefined;
40
+ return decodeURIComponent(match[1] ?? "") || undefined;
41
+ }
42
+ // Bare-package form.
43
+ return trimmed;
44
+ }
45
+ /**
46
+ * Compose the `https://registry.npmjs.org/<pkg>` URL.
47
+ *
48
+ * Scoped packages must keep their `@` and slash *unescaped* — the npm registry
49
+ * routes `@scope%2fname` and `@scope/name` differently and only the latter
50
+ * resolves. `encodeURIComponent` would mangle the slash, so we pass the name
51
+ * through verbatim.
52
+ */
53
+ export function buildMetadataUrl(packageName) {
54
+ return `${REGISTRY_BASE}/${packageName}`;
55
+ }
56
+ /**
57
+ * Compose the canonical `Item.url` for a specific package version.
58
+ *
59
+ * Always uses `www.npmjs.com` (matches the redirect target of the bare
60
+ * `npmjs.com` host) so re-derivation across re-fetches stays byte-stable.
61
+ */
62
+ function buildItemUrl(packageName, version) {
63
+ return `https://www.npmjs.com/package/${packageName}/v/${version}`;
64
+ }
65
+ /** Convert ISO datetime strings from `time[<version>]` into normalized form. */
66
+ function toIsoDate(value) {
67
+ if (!value)
68
+ return undefined;
69
+ const date = new Date(value);
70
+ if (Number.isNaN(date.getTime()))
71
+ return undefined;
72
+ return date.toISOString();
73
+ }
74
+ /**
75
+ * Normalize one `<package>@<version>` entry into our `Item` shape.
76
+ *
77
+ * `Item.id` is derived via the shared helper so npm-registry items follow the
78
+ * same `<slug>-<8 hex>` contract as RSS / Atom items (ADR-0002 §Item ID 派生
79
+ * のコントラクト). The slug source is `<package>@<version>` so ids remain
80
+ * human-readable in CLI listings.
81
+ */
82
+ function buildItem(packument, packageName, version, source, fetchedAt) {
83
+ const versionMeta = packument.versions?.[version];
84
+ // Drop a version when the packument lies about claiming it but the entry is
85
+ // missing — the registry sometimes lists tombstoned versions in `time` but
86
+ // omits them from `versions`. Skipping rather than erroring keeps the run
87
+ // green for ordinary feeds.
88
+ if (!versionMeta)
89
+ return null;
90
+ const stableKey = deriveStableKey({
91
+ publisherId: `${packageName}@${version}`,
92
+ });
93
+ const title = `${packageName}@${version}`;
94
+ const id = deriveItemId(title, stableKey);
95
+ const candidate = {
96
+ id,
97
+ sourceId: source.id,
98
+ title,
99
+ url: buildItemUrl(packageName, version),
100
+ fetchedAt,
101
+ publishedAt: toIsoDate(packument.time?.[version]),
102
+ summary: versionMeta.description,
103
+ raw: { package: packageName, version, ...versionMeta },
104
+ };
105
+ const result = ItemSchema.safeParse(candidate);
106
+ // Mirror the rss adapter: drop malformed entries silently rather than
107
+ // failing the whole feed for one bad version.
108
+ return result.success ? result.data : null;
109
+ }
110
+ /**
111
+ * Parse an npm registry packument response into validated `Item[]`.
112
+ *
113
+ * The exported function exists so tests can exercise the normalizer without
114
+ * round-tripping through the HTTP layer (matching `parseFeedXml` in
115
+ * `rss.ts`).
116
+ */
117
+ export function parsePackument(body, source, fetchedAt, packageName) {
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(body);
121
+ }
122
+ catch (e) {
123
+ throw new Error(`npm-registry adapter: failed to parse JSON: ${e instanceof Error ? e.message : String(e)}`);
124
+ }
125
+ const versions = parsed.versions;
126
+ if (!versions)
127
+ return [];
128
+ const items = [];
129
+ for (const version of Object.keys(versions)) {
130
+ const item = buildItem(parsed, packageName, version, source, fetchedAt);
131
+ if (item)
132
+ items.push(item);
133
+ }
134
+ return items;
135
+ }
136
+ /**
137
+ * Fetch the npm registry packument with ETag-aware conditional GET.
138
+ *
139
+ * The registry honors `If-None-Match` and replies 304 for unchanged
140
+ * packuments, which is how we keep `watch run` cheap for stable packages.
141
+ */
142
+ async function fetchPackument(url, fetchImpl, options = {}) {
143
+ const headers = {
144
+ accept: "application/json",
145
+ "user-agent": USER_AGENT,
146
+ };
147
+ if (options.etag)
148
+ headers["if-none-match"] = options.etag;
149
+ const response = await fetchImpl(url, { headers, signal: options.signal });
150
+ const etag = response.headers.get("etag");
151
+ if (response.status === 304) {
152
+ return { status: 304, body: "", etag };
153
+ }
154
+ if (response.status === 404) {
155
+ throw new Error(`npm-registry adapter: package not found (HTTP 404) at ${url}`);
156
+ }
157
+ if (response.status < 200 || response.status >= 300) {
158
+ throw new Error(`npm-registry adapter: HTTP ${response.status} from ${url}`);
159
+ }
160
+ const body = await response.text();
161
+ return { status: response.status, body, etag };
162
+ }
163
+ export const npmRegistryAdapter = {
164
+ kind: "npm-registry",
165
+ fetch: async (source, options = {}) => {
166
+ const fetchImpl = options.fetch ?? globalThis.fetch;
167
+ if (typeof fetchImpl !== "function") {
168
+ throw new Error("npm-registry adapter: no fetch implementation available (Node 22+ required)");
169
+ }
170
+ const packageName = extractPackageName(source.url);
171
+ if (!packageName) {
172
+ throw new Error(`npm-registry adapter: cannot extract package name from url '${source.url}' ` +
173
+ "(expected '<package>' or 'https://www.npmjs.com/package/<package>')");
174
+ }
175
+ const previous = options.state;
176
+ const fetchedAt = new Date().toISOString();
177
+ const response = await fetchPackument(buildMetadataUrl(packageName), fetchImpl, {
178
+ etag: previous?.lastEtag,
179
+ });
180
+ if (response.status === 304) {
181
+ return {
182
+ items: [],
183
+ notModified: true,
184
+ state: {
185
+ lastFetchedAt: fetchedAt,
186
+ lastEtag: response.etag ?? previous?.lastEtag,
187
+ },
188
+ };
189
+ }
190
+ const items = parsePackument(response.body, source, fetchedAt, packageName);
191
+ return {
192
+ items,
193
+ state: {
194
+ lastFetchedAt: fetchedAt,
195
+ lastEtag: response.etag ?? previous?.lastEtag,
196
+ },
197
+ };
198
+ },
199
+ };
200
+ //# sourceMappingURL=npm-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"npm-registry.js","sourceRoot":"","sources":["../../../src/core/feeds/npm-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Item, Source } from "../../schemas/index.js";
2
+ import type { FeedAdapter } from "./types.js";
3
+ /**
4
+ * Parse an RSS 2.0 or Atom XML document into validated `Item[]`.
5
+ *
6
+ * `fast-xml-parser` is intentionally configured to keep attribute prefixes
7
+ * (`@_href`) and skip CDATA stripping so we can route through the same
8
+ * normalizer regardless of which dialect we are reading.
9
+ */
10
+ export declare function parseFeedXml(xml: string, source: Source, fetchedAt: string): Item[];
11
+ export declare const rssAdapter: FeedAdapter;
12
+ //# sourceMappingURL=rss.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rss.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/rss.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAG3D,OAAO,KAAK,EAAE,WAAW,EAAiC,MAAM,YAAY,CAAC;AAyJ7E;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,CAsCnF;AAgCD,eAAO,MAAM,UAAU,EAAE,WAkCxB,CAAC"}