@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,101 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { RadarConfigSchema } from "../schemas/index.js";
5
+ /** File name of the workspace config, located at the workspace root. */
6
+ export const CONFIG_FILENAME = "radar.config.yaml";
7
+ /**
8
+ * Hard-coded fallback when neither `--agent` nor `radar.config.yaml` sets a
9
+ * default. Documented in user-guide.md and architecture.md (ADR-0001).
10
+ */
11
+ export const HARDCODED_DEFAULT_AGENT = "claude-code";
12
+ async function pathExists(p) {
13
+ try {
14
+ await access(p);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Error raised when `radar.config.yaml` exists but fails to parse or violates
23
+ * `RadarConfigSchema`. Throws (rather than fall-back to defaults) because the
24
+ * user explicitly authored a config and silently ignoring it would surprise
25
+ * them and mask typos like `defaultResarchAgent`.
26
+ */
27
+ export class RadarConfigError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = "RadarConfigError";
31
+ }
32
+ }
33
+ /**
34
+ * Load `radar.config.yaml` from a workspace root.
35
+ *
36
+ * Behavior:
37
+ * - File missing -> returns an empty config (`{}`).
38
+ * - File present and valid -> returns the parsed `RadarConfig`.
39
+ * - File present but malformed (YAML parse error / schema violation)
40
+ * -> throws `RadarConfigError` with a contextual message.
41
+ *
42
+ * Returning `{}` for the missing-file case lets callers treat absence as
43
+ * "use the hard-coded default" without an extra `await pathExists` round-trip.
44
+ */
45
+ export async function loadRadarConfig(cwd) {
46
+ const file = join(cwd, CONFIG_FILENAME);
47
+ if (!(await pathExists(file))) {
48
+ return {};
49
+ }
50
+ let raw;
51
+ try {
52
+ raw = await readFile(file, "utf8");
53
+ }
54
+ catch (e) {
55
+ throw new RadarConfigError(`failed to read ${CONFIG_FILENAME}: ${e instanceof Error ? e.message : String(e)}`);
56
+ }
57
+ let parsed;
58
+ try {
59
+ parsed = parseYaml(raw);
60
+ }
61
+ catch (e) {
62
+ throw new RadarConfigError(`failed to parse ${CONFIG_FILENAME} as YAML: ${e instanceof Error ? e.message : String(e)}`);
63
+ }
64
+ // An empty file parses to `null` / `undefined`; normalize to `{}` so the
65
+ // schema validation treats it the same as an absent file.
66
+ const candidate = parsed ?? {};
67
+ const result = RadarConfigSchema.safeParse(candidate);
68
+ if (!result.success) {
69
+ const issues = result.error.issues
70
+ .map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`)
71
+ .join("\n");
72
+ throw new RadarConfigError(`${CONFIG_FILENAME} schema violation:\n${issues}`);
73
+ }
74
+ return result.data;
75
+ }
76
+ export async function getDefaultAgent(command, options = {}) {
77
+ if (options.explicit) {
78
+ return options.explicit;
79
+ }
80
+ const config = options.configOverride ?? (await loadRadarConfig(options.cwd ?? process.cwd()));
81
+ const fromConfig = pickConfigDefault(command, config);
82
+ if (fromConfig) {
83
+ return fromConfig;
84
+ }
85
+ return HARDCODED_DEFAULT_AGENT;
86
+ }
87
+ function pickConfigDefault(command, config) {
88
+ switch (command) {
89
+ case "research":
90
+ return config.defaultResearchAgent;
91
+ case "review":
92
+ return config.defaultReviewAgent;
93
+ default: {
94
+ // Exhaustiveness check — adding a new `ConfigurableCommand` without
95
+ // updating this switch becomes a compile error.
96
+ const _exhaustive = command;
97
+ return _exhaustive;
98
+ }
99
+ }
100
+ }
101
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Candidate inputs an adapter passes to `deriveStableKey()`. The adapter
3
+ * ranks its candidates in preferred order (most-stable first); the helper
4
+ * returns the first non-empty value, falling back to a `sha1:`-prefixed
5
+ * content hash so the result is **always** defined.
6
+ *
7
+ * - `publisherId`: a stable id the publisher itself declares (RSS `guid`,
8
+ * GitHub release id, npm `<pkg>@<version>`, …). Preferred when present.
9
+ * - `url`: the canonical URL of the entity. Preferred fallback because URLs
10
+ * are usually stable across re-fetches even when no explicit id is given.
11
+ * - `fallbackHashInputs`: free-form strings (title, pubDate, …) hashed only
12
+ * when neither `publisherId` nor `url` exists. The `sha1:` prefix is
13
+ * retained from the legacy implementation so existing ids stay byte-stable
14
+ * across the refactor.
15
+ */
16
+ export interface StableKeyCandidates {
17
+ publisherId?: string;
18
+ url?: string;
19
+ fallbackHashInputs?: Array<string | undefined>;
20
+ }
21
+ /**
22
+ * Pick the most stable identifier available for an entity from a
23
+ * publisher-id-first fallback ladder. See ADR-0002 "Item ID 派生のコントラクト"
24
+ * for the contract this helper enforces.
25
+ *
26
+ * The return value is opaque — callers pass it directly to `deriveItemId()`
27
+ * (or hash it themselves) without inspecting the contents.
28
+ */
29
+ export declare function deriveStableKey(candidates: StableKeyCandidates): string;
30
+ /**
31
+ * Build the canonical `Item.id` for a feed entry.
32
+ *
33
+ * Shape: `<title-slug>-<8 hex of sha256(stableKey)>` (or just the hash when
34
+ * the title contains no slug-friendly characters).
35
+ *
36
+ * The title slug keeps ids human-readable in shell args and log lines; the
37
+ * hash suffix makes the id stable across re-fetches and avoids collisions
38
+ * between entries with identical titles within the same source. We hash the
39
+ * adapter-selected `stableKey`, not the raw title, so two posts with the
40
+ * same title still get distinct ids when their publisher ids differ.
41
+ */
42
+ export declare function deriveItemId(title: string | undefined, stableKey: string): string;
43
+ //# sourceMappingURL=derive-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-id.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/derive-id.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,mBAAmB,GAAG,MAAM,CAOvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAIjF"}
@@ -0,0 +1,66 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Maximum number of characters kept from the title slug before the hash
4
+ * suffix. 40 keeps `Item.id` short enough for shell args and file names while
5
+ * still leaving room for human-readable context.
6
+ */
7
+ const SLUG_MAX_LENGTH = 40;
8
+ /**
9
+ * Length of the hash suffix appended to the slug. 8 hex chars (32 bits) is
10
+ * enough to disambiguate same-title entries within a single source without
11
+ * bloating ids — feeds have at most a few thousand active items, far below
12
+ * the birthday-collision threshold.
13
+ */
14
+ const HASH_SUFFIX_LENGTH = 8;
15
+ /**
16
+ * Pick the most stable identifier available for an entity from a
17
+ * publisher-id-first fallback ladder. See ADR-0002 "Item ID 派生のコントラクト"
18
+ * for the contract this helper enforces.
19
+ *
20
+ * The return value is opaque — callers pass it directly to `deriveItemId()`
21
+ * (or hash it themselves) without inspecting the contents.
22
+ */
23
+ export function deriveStableKey(candidates) {
24
+ const publisherId = trimToValue(candidates.publisherId);
25
+ if (publisherId)
26
+ return publisherId;
27
+ const url = trimToValue(candidates.url);
28
+ if (url)
29
+ return url;
30
+ const joined = (candidates.fallbackHashInputs ?? []).map((v) => v ?? "").join("|");
31
+ return `sha1:${createHash("sha1").update(joined).digest("hex")}`;
32
+ }
33
+ /**
34
+ * Build the canonical `Item.id` for a feed entry.
35
+ *
36
+ * Shape: `<title-slug>-<8 hex of sha256(stableKey)>` (or just the hash when
37
+ * the title contains no slug-friendly characters).
38
+ *
39
+ * The title slug keeps ids human-readable in shell args and log lines; the
40
+ * hash suffix makes the id stable across re-fetches and avoids collisions
41
+ * between entries with identical titles within the same source. We hash the
42
+ * adapter-selected `stableKey`, not the raw title, so two posts with the
43
+ * same title still get distinct ids when their publisher ids differ.
44
+ */
45
+ export function deriveItemId(title, stableKey) {
46
+ const hash = createHash("sha256").update(stableKey).digest("hex").slice(0, HASH_SUFFIX_LENGTH);
47
+ const slug = slugifyTitle(title);
48
+ return slug ? `${slug}-${hash}` : hash;
49
+ }
50
+ /** Title → kebab-case, lowercase, ASCII-only, capped at SLUG_MAX_LENGTH. */
51
+ function slugifyTitle(title) {
52
+ return (title ?? "")
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9]+/g, "-")
55
+ .replace(/^-+|-+$/g, "")
56
+ .slice(0, SLUG_MAX_LENGTH)
57
+ .replace(/-+$/g, "");
58
+ }
59
+ /** Trim a candidate and return `undefined` for empty strings. */
60
+ function trimToValue(value) {
61
+ if (value == null)
62
+ return undefined;
63
+ const trimmed = value.trim();
64
+ return trimmed.length === 0 ? undefined : trimmed;
65
+ }
66
+ //# sourceMappingURL=derive-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-id.js","sourceRoot":"","sources":["../../../src/core/feeds/derive-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;GAIG;AACH,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAuB7B;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,UAA+B;IAC7D,MAAM,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IACxD,IAAI,WAAW;QAAE,OAAO,WAAW,CAAC;IACpC,MAAM,GAAG,GAAG,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IACpB,MAAM,MAAM,GAAG,CAAC,UAAU,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnF,OAAO,QAAQ,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAAC,KAAyB,EAAE,SAAiB;IACvE,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC/F,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED,4EAA4E;AAC5E,SAAS,YAAY,CAAC,KAAyB;IAC7C,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;SACjB,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC;SACzB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACzB,CAAC;AAED,iEAAiE;AACjE,SAAS,WAAW,CAAC,KAAyB;IAC5C,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AACpD,CAAC"}
@@ -0,0 +1,69 @@
1
+ import type { FetchLike } from "./types.js";
2
+ /** A single GitHub Release as returned by the REST API. Only fields we touch are typed. */
3
+ export interface GitHubRelease {
4
+ id: number;
5
+ tag_name: string;
6
+ name: string | null;
7
+ body: string | null;
8
+ draft: boolean;
9
+ prerelease: boolean;
10
+ html_url: string;
11
+ published_at: string | null;
12
+ created_at: string;
13
+ }
14
+ /** Parsed `owner/repo` extracted from various URL/shorthand inputs. */
15
+ export interface OwnerRepo {
16
+ owner: string;
17
+ repo: string;
18
+ }
19
+ /**
20
+ * Accept both `https://github.com/<owner>/<repo>` and shorthand `<owner>/<repo>`.
21
+ *
22
+ * We intentionally allow trailing path segments (`.../tree/main`, `.git`) so
23
+ * users can paste any GitHub URL without trimming first.
24
+ */
25
+ export declare function parseOwnerRepo(input: string): OwnerRepo;
26
+ /** Build the canonical Releases API URL for an `owner/repo`. */
27
+ export declare function buildReleasesUrl(owner: string, repo: string): string;
28
+ /** Inputs to `fetchReleases()`. `fetch` and `token` are injectable for tests. */
29
+ export interface FetchReleasesOptions {
30
+ fetch?: FetchLike;
31
+ /** GitHub PAT (or any token accepted by GitHub). Defaults to `process.env.GITHUB_TOKEN`. */
32
+ token?: string;
33
+ /** `If-None-Match` value from a previous response. */
34
+ etag?: string;
35
+ /** Logger for rate-limit warnings; defaults to `console.warn`. */
36
+ warn?: (message: string) => void;
37
+ signal?: AbortSignal;
38
+ }
39
+ /** Result shape returned by `fetchReleases()`. */
40
+ export interface FetchReleasesResult {
41
+ status: number;
42
+ releases: GitHubRelease[];
43
+ etag: string | null;
44
+ /** `true` when the server responded 304 Not Modified. */
45
+ notModified: boolean;
46
+ rateLimit: RateLimitInfo;
47
+ }
48
+ /** Subset of GitHub rate-limit headers we care about. */
49
+ export interface RateLimitInfo {
50
+ /** Requests remaining in the current window (`X-RateLimit-Remaining`). */
51
+ remaining: number | null;
52
+ /** Total quota for the current window (`X-RateLimit-Limit`). */
53
+ limit: number | null;
54
+ /** Unix epoch seconds when the window resets (`X-RateLimit-Reset`). */
55
+ resetAt: number | null;
56
+ }
57
+ /**
58
+ * Fetch GitHub Releases for an `owner/repo`.
59
+ *
60
+ * Why we hit the REST API directly instead of bringing in `@octokit/rest`:
61
+ * the adapter only needs one endpoint, and dropping the dep keeps the
62
+ * published bundle small (see ADR-0002 / issue #37 commit 1 rationale).
63
+ *
64
+ * Authentication is opportunistic — without a token we still work, just at
65
+ * the much lower 60 req/h anonymous rate. The caller (CLI) is expected to
66
+ * surface `GITHUB_TOKEN` in user-facing docs.
67
+ */
68
+ export declare function fetchReleases(owner: string, repo: string, options?: FetchReleasesOptions): Promise<FetchReleasesResult>;
69
+ //# sourceMappingURL=github-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-api.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/github-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAY5C,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,uEAAuE;AACvE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CA+BvD;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,4FAA4F;IAC5F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,yDAAyD;IACzD,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,aAAa,CAAC;CAC1B;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gEAAgE;IAChE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,uEAAuE;IACvE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA+E9B"}
@@ -0,0 +1,161 @@
1
+ const USER_AGENT = "feedradar/0.0.0 (+https://github.com/ozzy-labs/feedradar)";
2
+ const GITHUB_API_BASE = "https://api.github.com";
3
+ /**
4
+ * Threshold under which we surface a rate-limit warning. GitHub resets the
5
+ * counter every hour, so 10 leftover requests is the smallest cushion that
6
+ * still lets a typical run (1–2 sources) complete before reset.
7
+ */
8
+ const RATE_LIMIT_WARNING_THRESHOLD = 10;
9
+ /**
10
+ * Accept both `https://github.com/<owner>/<repo>` and shorthand `<owner>/<repo>`.
11
+ *
12
+ * We intentionally allow trailing path segments (`.../tree/main`, `.git`) so
13
+ * users can paste any GitHub URL without trimming first.
14
+ */
15
+ export function parseOwnerRepo(input) {
16
+ const trimmed = input.trim();
17
+ if (!trimmed)
18
+ throw new Error("github-releases adapter: empty source URL");
19
+ let candidate = trimmed;
20
+ // URL form — strip protocol/host and any trailing path.
21
+ if (/^https?:\/\//i.test(candidate)) {
22
+ let url;
23
+ try {
24
+ url = new URL(candidate);
25
+ }
26
+ catch {
27
+ throw new Error(`github-releases adapter: invalid URL: ${input}`);
28
+ }
29
+ // Accept api.github.com/repos/<owner>/<repo> too, in case someone pastes an API URL.
30
+ const path = url.pathname.replace(/^\/repos\//, "/").replace(/^\/+|\/+$/g, "");
31
+ candidate = path;
32
+ }
33
+ else {
34
+ candidate = candidate.replace(/^\/+|\/+$/g, "");
35
+ }
36
+ // Strip a `.git` suffix and anything past the second segment (`tree/main`, etc.).
37
+ const segments = candidate.split("/").filter(Boolean);
38
+ const [ownerSegment, repoSegment] = segments;
39
+ if (!ownerSegment || !repoSegment) {
40
+ throw new Error(`github-releases adapter: expected <owner>/<repo>, got: ${input}`);
41
+ }
42
+ const repo = repoSegment.replace(/\.git$/i, "");
43
+ if (!repo) {
44
+ throw new Error(`github-releases adapter: expected <owner>/<repo>, got: ${input}`);
45
+ }
46
+ return { owner: ownerSegment, repo };
47
+ }
48
+ /** Build the canonical Releases API URL for an `owner/repo`. */
49
+ export function buildReleasesUrl(owner, repo) {
50
+ return `${GITHUB_API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
51
+ }
52
+ /**
53
+ * Fetch GitHub Releases for an `owner/repo`.
54
+ *
55
+ * Why we hit the REST API directly instead of bringing in `@octokit/rest`:
56
+ * the adapter only needs one endpoint, and dropping the dep keeps the
57
+ * published bundle small (see ADR-0002 / issue #37 commit 1 rationale).
58
+ *
59
+ * Authentication is opportunistic — without a token we still work, just at
60
+ * the much lower 60 req/h anonymous rate. The caller (CLI) is expected to
61
+ * surface `GITHUB_TOKEN` in user-facing docs.
62
+ */
63
+ export async function fetchReleases(owner, repo, options = {}) {
64
+ const fetchImpl = options.fetch ?? globalThis.fetch;
65
+ if (typeof fetchImpl !== "function") {
66
+ throw new Error("github-releases adapter: no fetch implementation available (Node 22+ required)");
67
+ }
68
+ const token = options.token ?? process.env.GITHUB_TOKEN;
69
+ const warn = options.warn ?? ((m) => console.warn(m));
70
+ const headers = {
71
+ accept: "application/vnd.github+json",
72
+ "user-agent": USER_AGENT,
73
+ // Pinning the API version protects us from breaking schema changes on
74
+ // the GitHub side without an explicit migration step.
75
+ "x-github-api-version": "2022-11-28",
76
+ };
77
+ if (token)
78
+ headers.authorization = `Bearer ${token}`;
79
+ if (options.etag)
80
+ headers["if-none-match"] = options.etag;
81
+ const url = buildReleasesUrl(owner, repo);
82
+ const response = await fetchImpl(url, { headers, signal: options.signal });
83
+ const rateLimit = {
84
+ remaining: parseIntHeader(response.headers.get("x-ratelimit-remaining")),
85
+ limit: parseIntHeader(response.headers.get("x-ratelimit-limit")),
86
+ resetAt: parseIntHeader(response.headers.get("x-ratelimit-reset")),
87
+ };
88
+ emitRateLimitWarning(rateLimit, owner, repo, token != null, warn);
89
+ const etag = response.headers.get("etag");
90
+ if (response.status === 304) {
91
+ return { status: 304, releases: [], etag, notModified: true, rateLimit };
92
+ }
93
+ if (response.status === 403 && rateLimit.remaining === 0) {
94
+ // Distinct error message so the CLI / docs can guide users to set
95
+ // `GITHUB_TOKEN`. The bare HTTP 403 message is ambiguous (could be auth).
96
+ const resetHint = formatResetHint(rateLimit.resetAt);
97
+ throw new Error(`github-releases adapter: rate limit exhausted for ${owner}/${repo}${resetHint}. ` +
98
+ (token
99
+ ? "Authenticated quota is 5000 req/h."
100
+ : "Set GITHUB_TOKEN to raise the quota from 60 to 5000 req/h."));
101
+ }
102
+ if (response.status === 404) {
103
+ throw new Error(`github-releases adapter: repository not found: ${owner}/${repo}`);
104
+ }
105
+ if (response.status === 401) {
106
+ throw new Error(`github-releases adapter: authentication failed for ${owner}/${repo} (check GITHUB_TOKEN)`);
107
+ }
108
+ if (response.status < 200 || response.status >= 300) {
109
+ throw new Error(`github-releases adapter: HTTP ${response.status} from ${url}`);
110
+ }
111
+ const body = await response.text();
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(body);
115
+ }
116
+ catch (e) {
117
+ throw new Error(`github-releases adapter: failed to parse JSON: ${e instanceof Error ? e.message : String(e)}`);
118
+ }
119
+ if (!Array.isArray(parsed)) {
120
+ throw new Error(`github-releases adapter: expected JSON array from ${url}, got ${typeof parsed}`);
121
+ }
122
+ const releases = parsed.filter(isGitHubRelease);
123
+ return { status: response.status, releases, etag, notModified: false, rateLimit };
124
+ }
125
+ /** Narrow `unknown` to `GitHubRelease`. Defensive — drops malformed entries silently. */
126
+ function isGitHubRelease(value) {
127
+ if (value == null || typeof value !== "object")
128
+ return false;
129
+ const v = value;
130
+ return (typeof v.id === "number" &&
131
+ typeof v.tag_name === "string" &&
132
+ typeof v.html_url === "string" &&
133
+ typeof v.draft === "boolean" &&
134
+ typeof v.prerelease === "boolean");
135
+ }
136
+ function parseIntHeader(value) {
137
+ if (value == null)
138
+ return null;
139
+ const n = Number.parseInt(value, 10);
140
+ return Number.isFinite(n) ? n : null;
141
+ }
142
+ function emitRateLimitWarning(rateLimit, owner, repo, authenticated, warn) {
143
+ const { remaining, limit } = rateLimit;
144
+ if (remaining == null || remaining > RATE_LIMIT_WARNING_THRESHOLD)
145
+ return;
146
+ const limitHint = limit != null ? `/${limit}` : "";
147
+ const authHint = authenticated
148
+ ? ""
149
+ : " Set GITHUB_TOKEN to raise the quota from 60 to 5000 req/h.";
150
+ const resetHint = formatResetHint(rateLimit.resetAt);
151
+ warn(`github-releases: rate limit low (${remaining}${limitHint} remaining) for ${owner}/${repo}${resetHint}.${authHint}`);
152
+ }
153
+ function formatResetHint(resetAt) {
154
+ if (resetAt == null)
155
+ return "";
156
+ const date = new Date(resetAt * 1000);
157
+ if (Number.isNaN(date.getTime()))
158
+ return "";
159
+ return ` (resets at ${date.toISOString()})`;
160
+ }
161
+ //# sourceMappingURL=github-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-api.js","sourceRoot":"","sources":["../../../src/core/feeds/github-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { FeedAdapter } from "./types.js";
2
+ export declare const githubReleasesAdapter: FeedAdapter;
3
+ //# sourceMappingURL=github-releases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-releases.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/github-releases.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,YAAY,CAAC;AAqDlE,eAAO,MAAM,qBAAqB,EAAE,WAmCnC,CAAC"}
@@ -0,0 +1,85 @@
1
+ import { ItemSchema } from "../../schemas/index.js";
2
+ import { deriveItemId, deriveStableKey } from "./derive-id.js";
3
+ import { fetchReleases, parseOwnerRepo } from "./github-api.js";
4
+ /**
5
+ * Normalize one GitHub Release into our canonical `Item` shape.
6
+ *
7
+ * Stable id derivation follows ADR-0002 §"Item ID 派生のコントラクト":
8
+ * - `publisherId`: `<tag_name>#<id>` — both fields can change independently
9
+ * (re-tags rewrite `tag_name` but keep `id`; deleted-and-recreated releases
10
+ * keep `tag_name` but get a new `id`). Combining them gives the strongest
11
+ * "same entity" signal available, matching the issue spec
12
+ * ("stableKey: release tag_name + id").
13
+ * - `url`: the release HTML URL — kept as a secondary fallback even though
14
+ * GitHub guarantees `id`, so the contract behaves uniformly across adapters.
15
+ * - Title slug: prefer the release `name`; fall back to `tag_name` when the
16
+ * maintainer left `name` blank (common on auto-cut releases).
17
+ */
18
+ function releaseToItem(release, source, fetchedAt) {
19
+ const title = release.name?.trim() || release.tag_name;
20
+ const url = release.html_url;
21
+ const publishedAt = toIsoDate(release.published_at ?? release.created_at);
22
+ const summary = release.body?.trim() || undefined;
23
+ const stableKey = deriveStableKey({
24
+ publisherId: `${release.tag_name}#${release.id}`,
25
+ url,
26
+ fallbackHashInputs: [title, publishedAt],
27
+ });
28
+ const id = deriveItemId(title, stableKey);
29
+ const candidate = {
30
+ id,
31
+ sourceId: source.id,
32
+ title,
33
+ url,
34
+ summary,
35
+ publishedAt,
36
+ fetchedAt,
37
+ raw: release,
38
+ };
39
+ const result = ItemSchema.safeParse(candidate);
40
+ // Drop malformed entries silently — one broken release should not poison the
41
+ // whole feed (mirrors the RSS adapter's policy).
42
+ return result.success ? result.data : null;
43
+ }
44
+ /** Convert a GitHub timestamp to ISO 8601, returning `undefined` for invalid input. */
45
+ function toIsoDate(value) {
46
+ if (!value)
47
+ return undefined;
48
+ const date = new Date(value);
49
+ if (Number.isNaN(date.getTime()))
50
+ return undefined;
51
+ return date.toISOString();
52
+ }
53
+ export const githubReleasesAdapter = {
54
+ kind: "github-releases",
55
+ fetch: async (source, options = {}) => {
56
+ const { owner, repo } = parseOwnerRepo(source.url);
57
+ const previous = options.state;
58
+ const fetchedAt = new Date().toISOString();
59
+ const response = await fetchReleases(owner, repo, {
60
+ fetch: options.fetch,
61
+ etag: previous?.lastEtag,
62
+ });
63
+ if (response.notModified) {
64
+ return {
65
+ items: [],
66
+ notModified: true,
67
+ state: {
68
+ lastFetchedAt: fetchedAt,
69
+ lastEtag: response.etag ?? previous?.lastEtag,
70
+ },
71
+ };
72
+ }
73
+ const items = response.releases
74
+ .map((release) => releaseToItem(release, source, fetchedAt))
75
+ .filter((i) => i !== null);
76
+ return {
77
+ items,
78
+ state: {
79
+ lastFetchedAt: fetchedAt,
80
+ lastEtag: response.etag ?? previous?.lastEtag,
81
+ },
82
+ };
83
+ },
84
+ };
85
+ //# sourceMappingURL=github-releases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-releases.js","sourceRoot":"","sources":["../../../src/core/feeds/github-releases.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { Item, Source } from "../../schemas/index.js";
2
+ import type { FeedAdapter } from "./types.js";
3
+ /**
4
+ * Parse an HTML document into validated `Item[]` using the source's
5
+ * `selectors`. Exported so tests can drive the parser directly without
6
+ * needing a fake HTTP layer.
7
+ */
8
+ export declare function parseHtmlDocument(html: string, source: Source, fetchedAt: string): Item[];
9
+ export declare const htmlAdapter: FeedAdapter;
10
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/html.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAmB,MAAM,wBAAwB,CAAC;AAG5E,OAAO,KAAK,EAAE,WAAW,EAAiC,MAAM,YAAY,CAAC;AA6J7E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,CAiBzF;AA+CD,eAAO,MAAM,WAAW,EAAE,WAuDzB,CAAC"}