@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.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/agents/_boundary.d.ts +44 -0
- package/dist/agents/_boundary.d.ts.map +1 -0
- package/dist/agents/_boundary.js +59 -0
- package/dist/agents/_boundary.js.map +1 -0
- package/dist/agents/claude-code.d.ts +32 -0
- package/dist/agents/claude-code.d.ts.map +1 -0
- package/dist/agents/claude-code.js +256 -0
- package/dist/agents/claude-code.js.map +1 -0
- package/dist/agents/codex-cli.d.ts +31 -0
- package/dist/agents/codex-cli.d.ts.map +1 -0
- package/dist/agents/codex-cli.js +303 -0
- package/dist/agents/codex-cli.js.map +1 -0
- package/dist/agents/copilot.d.ts +29 -0
- package/dist/agents/copilot.d.ts.map +1 -0
- package/dist/agents/copilot.js +282 -0
- package/dist/agents/copilot.js.map +1 -0
- package/dist/agents/gemini-cli.d.ts +30 -0
- package/dist/agents/gemini-cli.d.ts.map +1 -0
- package/dist/agents/gemini-cli.js +316 -0
- package/dist/agents/gemini-cli.js.map +1 -0
- package/dist/agents/index.d.ts +12 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +33 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/types.d.ts +103 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/claude-skills/dismiss/SKILL.md +41 -0
- package/dist/claude-skills/research/SKILL.md +45 -0
- package/dist/claude-skills/review/SKILL.md +45 -0
- package/dist/claude-skills/update/SKILL.md +49 -0
- package/dist/cli/dismiss.d.ts +28 -0
- package/dist/cli/dismiss.d.ts.map +1 -0
- package/dist/cli/dismiss.js +122 -0
- package/dist/cli/dismiss.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +148 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +578 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/research.d.ts +30 -0
- package/dist/cli/research.d.ts.map +1 -0
- package/dist/cli/research.js +313 -0
- package/dist/cli/research.js.map +1 -0
- package/dist/cli/review.d.ts +34 -0
- package/dist/cli/review.d.ts.map +1 -0
- package/dist/cli/review.js +418 -0
- package/dist/cli/review.js.map +1 -0
- package/dist/cli/source.d.ts +57 -0
- package/dist/cli/source.d.ts.map +1 -0
- package/dist/cli/source.js +511 -0
- package/dist/cli/source.js.map +1 -0
- package/dist/cli/update.d.ts +43 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +429 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/cli/watch.d.ts +22 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +101 -0
- package/dist/cli/watch.js.map +1 -0
- package/dist/core/config.d.ts +60 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +101 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/feeds/derive-id.d.ts +43 -0
- package/dist/core/feeds/derive-id.d.ts.map +1 -0
- package/dist/core/feeds/derive-id.js +66 -0
- package/dist/core/feeds/derive-id.js.map +1 -0
- package/dist/core/feeds/github-api.d.ts +69 -0
- package/dist/core/feeds/github-api.d.ts.map +1 -0
- package/dist/core/feeds/github-api.js +161 -0
- package/dist/core/feeds/github-api.js.map +1 -0
- package/dist/core/feeds/github-releases.d.ts +3 -0
- package/dist/core/feeds/github-releases.d.ts.map +1 -0
- package/dist/core/feeds/github-releases.js +85 -0
- package/dist/core/feeds/github-releases.js.map +1 -0
- package/dist/core/feeds/html.d.ts +10 -0
- package/dist/core/feeds/html.d.ts.map +1 -0
- package/dist/core/feeds/html.js +263 -0
- package/dist/core/feeds/html.js.map +1 -0
- package/dist/core/feeds/index.d.ts +5 -0
- package/dist/core/feeds/index.d.ts.map +1 -0
- package/dist/core/feeds/index.js +18 -0
- package/dist/core/feeds/index.js.map +1 -0
- package/dist/core/feeds/npm-registry.d.ts +36 -0
- package/dist/core/feeds/npm-registry.d.ts.map +1 -0
- package/dist/core/feeds/npm-registry.js +200 -0
- package/dist/core/feeds/npm-registry.js.map +1 -0
- package/dist/core/feeds/rss.d.ts +12 -0
- package/dist/core/feeds/rss.d.ts.map +1 -0
- package/dist/core/feeds/rss.js +222 -0
- package/dist/core/feeds/rss.js.map +1 -0
- package/dist/core/feeds/types.d.ts +45 -0
- package/dist/core/feeds/types.d.ts.map +1 -0
- package/dist/core/feeds/types.js +2 -0
- package/dist/core/feeds/types.js.map +1 -0
- package/dist/core/filter.d.ts +25 -0
- package/dist/core/filter.d.ts.map +1 -0
- package/dist/core/filter.js +123 -0
- package/dist/core/filter.js.map +1 -0
- package/dist/core/injection-detector.d.ts +57 -0
- package/dist/core/injection-detector.d.ts.map +1 -0
- package/dist/core/injection-detector.js +109 -0
- package/dist/core/injection-detector.js.map +1 -0
- package/dist/core/items.d.ts +20 -0
- package/dist/core/items.d.ts.map +1 -0
- package/dist/core/items.js +105 -0
- package/dist/core/items.js.map +1 -0
- package/dist/core/state.d.ts +12 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +42 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/templates.d.ts +21 -0
- package/dist/core/templates.d.ts.map +1 -0
- package/dist/core/templates.js +52 -0
- package/dist/core/templates.js.map +1 -0
- package/dist/core/watcher.d.ts +72 -0
- package/dist/core/watcher.d.ts.map +1 -0
- package/dist/core/watcher.js +240 -0
- package/dist/core/watcher.js.map +1 -0
- package/dist/gemini-commands/dismiss.toml +2 -0
- package/dist/gemini-commands/research.toml +2 -0
- package/dist/gemini-commands/review.toml +2 -0
- package/dist/gemini-commands/update.toml +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/config.d.ts +39 -0
- package/dist/schemas/config.d.ts.map +1 -0
- package/dist/schemas/config.js +23 -0
- package/dist/schemas/config.js.map +1 -0
- package/dist/schemas/index.d.ts +6 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +6 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/item.d.ts +38 -0
- package/dist/schemas/item.d.ts.map +1 -0
- package/dist/schemas/item.js +34 -0
- package/dist/schemas/item.js.map +1 -0
- package/dist/schemas/research.d.ts +82 -0
- package/dist/schemas/research.d.ts.map +1 -0
- package/dist/schemas/research.js +45 -0
- package/dist/schemas/research.js.map +1 -0
- package/dist/schemas/source.d.ts +139 -0
- package/dist/schemas/source.d.ts.map +1 -0
- package/dist/schemas/source.js +127 -0
- package/dist/schemas/source.js.map +1 -0
- package/dist/schemas/state.d.ts +19 -0
- package/dist/schemas/state.d.ts.map +1 -0
- package/dist/schemas/state.js +12 -0
- package/dist/schemas/state.js.map +1 -0
- package/dist/skills/research/SKILL.md +156 -0
- package/dist/skills/review/SKILL.md +173 -0
- package/dist/skills/update/SKILL.md +200 -0
- package/dist/templates/agents/AGENTS.md +161 -0
- package/dist/templates/claude/CLAUDE.md +5 -0
- package/dist/templates/default.md +16 -0
- package/dist/templates/feedradar.md +165 -0
- package/dist/templates/routines/watch-daily.md +42 -0
- package/dist/templates/workflows/watch.yaml +70 -0
- package/package.json +73 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { ItemSchema } from "../../schemas/index.js";
|
|
3
|
+
import { deriveItemId, deriveStableKey } from "./derive-id.js";
|
|
4
|
+
const USER_AGENT = "feedradar/0.0.0 (+https://github.com/ozzy-labs/feedradar)";
|
|
5
|
+
/** Coerce an `fast-xml-parser` text-or-object to a plain string. */
|
|
6
|
+
function asString(value) {
|
|
7
|
+
if (value == null)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof value === "string")
|
|
10
|
+
return value.trim() || undefined;
|
|
11
|
+
if (typeof value === "object") {
|
|
12
|
+
const obj = value;
|
|
13
|
+
const text = obj["#text"];
|
|
14
|
+
if (typeof text === "string")
|
|
15
|
+
return text.trim() || undefined;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Atom `<link rel="alternate" href="…"/>` resolution. */
|
|
20
|
+
function pickAtomLink(link) {
|
|
21
|
+
if (!link)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof link === "string")
|
|
24
|
+
return link;
|
|
25
|
+
if (Array.isArray(link)) {
|
|
26
|
+
const alt = link.find((l) => (l["@_rel"] ?? "alternate") === "alternate");
|
|
27
|
+
return alt?.["@_href"];
|
|
28
|
+
}
|
|
29
|
+
return link["@_href"];
|
|
30
|
+
}
|
|
31
|
+
/** RSS `<link>` can be a simple URL or `{ "#text": "...", "@_href": "..." }`. */
|
|
32
|
+
function pickRssLink(link) {
|
|
33
|
+
if (!link)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (typeof link === "string")
|
|
36
|
+
return link.trim() || undefined;
|
|
37
|
+
if (typeof link === "object") {
|
|
38
|
+
return link["@_href"] ?? asString(link);
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Convert RSS `pubDate` / Atom `published` into ISO 8601 string. */
|
|
43
|
+
function toIsoDate(value) {
|
|
44
|
+
if (!value)
|
|
45
|
+
return undefined;
|
|
46
|
+
const date = new Date(value);
|
|
47
|
+
if (Number.isNaN(date.getTime()))
|
|
48
|
+
return undefined;
|
|
49
|
+
return date.toISOString();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Derive a stable, filesystem- and CLI-friendly id for an RSS/Atom entry.
|
|
53
|
+
*
|
|
54
|
+
* Delegates the actual format (`<title-slug>-<8 hex of sha256(stableKey)>`)
|
|
55
|
+
* and the publisher-id-first fallback ladder (guid > url > sha1(title|pub))
|
|
56
|
+
* to the shared helpers in `./derive-id.ts`, which all feed adapters use so
|
|
57
|
+
* that ids stay byte-stable across adapter kinds (see ADR-0002).
|
|
58
|
+
*
|
|
59
|
+
* The publisher's original guid is preserved in `Item.raw` (the full
|
|
60
|
+
* upstream entry is stored), so no information is lost. See issue #23.
|
|
61
|
+
*/
|
|
62
|
+
function deriveId(guid, url, title, pub) {
|
|
63
|
+
const stableKey = deriveStableKey({
|
|
64
|
+
publisherId: guid,
|
|
65
|
+
url,
|
|
66
|
+
fallbackHashInputs: [title, pub],
|
|
67
|
+
});
|
|
68
|
+
return deriveItemId(title, stableKey);
|
|
69
|
+
}
|
|
70
|
+
/** Normalize one RSS 2.0 `<item>` into our `Item` shape. */
|
|
71
|
+
function parseRssItem(raw, source, fetchedAt) {
|
|
72
|
+
const title = asString(raw.title) ?? "";
|
|
73
|
+
const url = pickRssLink(raw.link);
|
|
74
|
+
if (!url)
|
|
75
|
+
return null;
|
|
76
|
+
const summary = asString(raw.description);
|
|
77
|
+
const publishedAt = toIsoDate(raw.pubDate ?? raw["dc:date"]);
|
|
78
|
+
const guid = asString(raw.guid);
|
|
79
|
+
const id = deriveId(guid, url, title, raw.pubDate);
|
|
80
|
+
return validateItem({
|
|
81
|
+
id,
|
|
82
|
+
sourceId: source.id,
|
|
83
|
+
title,
|
|
84
|
+
url,
|
|
85
|
+
summary,
|
|
86
|
+
publishedAt,
|
|
87
|
+
fetchedAt,
|
|
88
|
+
raw,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Normalize one Atom `<entry>` into our `Item` shape. */
|
|
92
|
+
function parseAtomEntry(raw, source, fetchedAt) {
|
|
93
|
+
const title = asString(raw.title) ?? "";
|
|
94
|
+
const url = pickAtomLink(raw.link);
|
|
95
|
+
if (!url)
|
|
96
|
+
return null;
|
|
97
|
+
const summary = asString(raw.summary) ?? asString(raw.content);
|
|
98
|
+
const publishedAt = toIsoDate(raw.published ?? raw.updated);
|
|
99
|
+
const id = deriveId(raw.id, url, title, raw.published ?? raw.updated);
|
|
100
|
+
return validateItem({
|
|
101
|
+
id,
|
|
102
|
+
sourceId: source.id,
|
|
103
|
+
title,
|
|
104
|
+
url,
|
|
105
|
+
summary,
|
|
106
|
+
publishedAt,
|
|
107
|
+
fetchedAt,
|
|
108
|
+
raw,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function validateItem(candidate) {
|
|
112
|
+
const result = ItemSchema.safeParse(candidate);
|
|
113
|
+
// Items that fail validation (malformed URL etc.) are dropped silently — the
|
|
114
|
+
// alternative is failing the whole feed for one bad entry, which surprises
|
|
115
|
+
// users running large feeds where one broken entry is common.
|
|
116
|
+
return result.success ? result.data : null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse an RSS 2.0 or Atom XML document into validated `Item[]`.
|
|
120
|
+
*
|
|
121
|
+
* `fast-xml-parser` is intentionally configured to keep attribute prefixes
|
|
122
|
+
* (`@_href`) and skip CDATA stripping so we can route through the same
|
|
123
|
+
* normalizer regardless of which dialect we are reading.
|
|
124
|
+
*/
|
|
125
|
+
export function parseFeedXml(xml, source, fetchedAt) {
|
|
126
|
+
const parser = new XMLParser({
|
|
127
|
+
ignoreAttributes: false,
|
|
128
|
+
attributeNamePrefix: "@_",
|
|
129
|
+
textNodeName: "#text",
|
|
130
|
+
trimValues: true,
|
|
131
|
+
parseTagValue: false,
|
|
132
|
+
parseAttributeValue: false,
|
|
133
|
+
});
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = parser.parse(xml);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
throw new Error(`rss adapter: failed to parse XML: ${e instanceof Error ? e.message : String(e)}`);
|
|
140
|
+
}
|
|
141
|
+
// RSS 2.0 path
|
|
142
|
+
if (parsed.rss?.channel?.item) {
|
|
143
|
+
const items = Array.isArray(parsed.rss.channel.item)
|
|
144
|
+
? parsed.rss.channel.item
|
|
145
|
+
: [parsed.rss.channel.item];
|
|
146
|
+
return items
|
|
147
|
+
.map((entry) => parseRssItem(entry, source, fetchedAt))
|
|
148
|
+
.filter((i) => i !== null);
|
|
149
|
+
}
|
|
150
|
+
// Atom path
|
|
151
|
+
if (parsed.feed?.entry) {
|
|
152
|
+
const entries = Array.isArray(parsed.feed.entry) ? parsed.feed.entry : [parsed.feed.entry];
|
|
153
|
+
return entries
|
|
154
|
+
.map((entry) => parseAtomEntry(entry, source, fetchedAt))
|
|
155
|
+
.filter((i) => i !== null);
|
|
156
|
+
}
|
|
157
|
+
// No recognized envelope. Return [] rather than throw — empty feeds are a
|
|
158
|
+
// valid response and we should not poison the state with an error.
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Issue an HTTP GET with conditional headers, honoring previously stored
|
|
163
|
+
* `ETag` / `Last-Modified` so well-behaved servers can reply 304 and let us
|
|
164
|
+
* skip parsing work.
|
|
165
|
+
*/
|
|
166
|
+
async function fetchFeed(url, fetchImpl, options = {}) {
|
|
167
|
+
const headers = {
|
|
168
|
+
accept: "application/rss+xml, application/atom+xml, application/xml;q=0.9, */*;q=0.5",
|
|
169
|
+
"user-agent": USER_AGENT,
|
|
170
|
+
};
|
|
171
|
+
if (options.etag)
|
|
172
|
+
headers["if-none-match"] = options.etag;
|
|
173
|
+
if (options.lastModified)
|
|
174
|
+
headers["if-modified-since"] = options.lastModified;
|
|
175
|
+
const response = await fetchImpl(url, { headers, signal: options.signal });
|
|
176
|
+
const etag = response.headers.get("etag");
|
|
177
|
+
const lastModified = response.headers.get("last-modified");
|
|
178
|
+
if (response.status === 304) {
|
|
179
|
+
return { status: 304, body: "", etag, lastModified };
|
|
180
|
+
}
|
|
181
|
+
if (response.status < 200 || response.status >= 300) {
|
|
182
|
+
throw new Error(`rss adapter: HTTP ${response.status} from ${url}`);
|
|
183
|
+
}
|
|
184
|
+
const body = await response.text();
|
|
185
|
+
return { status: response.status, body, etag, lastModified };
|
|
186
|
+
}
|
|
187
|
+
export const rssAdapter = {
|
|
188
|
+
kind: "rss",
|
|
189
|
+
fetch: async (source, options = {}) => {
|
|
190
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
191
|
+
if (typeof fetchImpl !== "function") {
|
|
192
|
+
throw new Error("rss adapter: no fetch implementation available (Node 22+ required)");
|
|
193
|
+
}
|
|
194
|
+
const previous = options.state;
|
|
195
|
+
const fetchedAt = new Date().toISOString();
|
|
196
|
+
const response = await fetchFeed(source.url, fetchImpl, {
|
|
197
|
+
etag: previous?.lastEtag,
|
|
198
|
+
// We do not currently persist Last-Modified separately; ETag suffices
|
|
199
|
+
// for the well-behaved feed publishers we target. If a publisher only
|
|
200
|
+
// exposes Last-Modified we will revisit (issue #13 follow-up).
|
|
201
|
+
});
|
|
202
|
+
if (response.status === 304) {
|
|
203
|
+
return {
|
|
204
|
+
items: [],
|
|
205
|
+
notModified: true,
|
|
206
|
+
state: {
|
|
207
|
+
lastFetchedAt: fetchedAt,
|
|
208
|
+
lastEtag: response.etag ?? previous?.lastEtag,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const items = parseFeedXml(response.body, source, fetchedAt);
|
|
213
|
+
return {
|
|
214
|
+
items,
|
|
215
|
+
state: {
|
|
216
|
+
lastFetchedAt: fetchedAt,
|
|
217
|
+
lastEtag: response.etag ?? previous?.lastEtag,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
//# sourceMappingURL=rss.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rss.js","sourceRoot":"","sources":["../../../src/core/feeds/rss.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG/D,MAAM,UAAU,GAAG,2DAA2D,CAAC;AAoC/E,oEAAoE;AACpE,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,KAAgC,CAAC;QAC7C,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QAC1B,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;IAChE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,0DAA0D;AAC1D,SAAS,YAAY,CAAC,IAA2B;IAC/C,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,WAAW,CAAC,KAAK,WAAW,CAAC,CAAC;QAC1E,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;AACxB,CAAC;AAED,iFAAiF;AACjF,SAAS,WAAW,CAAC,IAAyB;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;IAC9D,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,qEAAqE;AACrE,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;;;;;;;;;;GAUG;AACH,SAAS,QAAQ,CACf,IAAwB,EACxB,GAAuB,EACvB,KAAyB,EACzB,GAAuB;IAEvB,MAAM,SAAS,GAAG,eAAe,CAAC;QAChC,WAAW,EAAE,IAAI;QACjB,GAAG;QACH,kBAAkB,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC;KACjC,CAAC,CAAC;IACH,OAAO,YAAY,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AACxC,CAAC;AAED,4DAA4D;AAC5D,SAAS,YAAY,CAAC,GAAgB,EAAE,MAAc,EAAE,SAAiB;IACvE,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;IAC7D,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACnD,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,0DAA0D;AAC1D,SAAS,cAAc,CAAC,GAAkB,EAAE,MAAc,EAAE,SAAiB;IAC3E,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,WAAW,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5D,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACtE,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,6EAA6E;IAC7E,2EAA2E;IAC3E,8DAA8D;IAC9D,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,MAAc,EAAE,SAAiB;IACzE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,gBAAgB,EAAE,KAAK;QACvB,mBAAmB,EAAE,IAAI;QACzB,YAAY,EAAE,OAAO;QACrB,UAAU,EAAE,IAAI;QAChB,aAAa,EAAE,KAAK;QACpB,mBAAmB,EAAE,KAAK;KAC3B,CAAC,CAAC;IACH,IAAI,MAAsB,CAAC;IAC3B,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;IAC/C,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,qCAAqC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAClF,CAAC;IACJ,CAAC;IAED,eAAe;IACf,IAAI,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;YAClD,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI;YACzB,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,KAAK;aACT,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;aACtD,MAAM,CAAC,CAAC,CAAC,EAAa,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IAC1C,CAAC;IACD,YAAY;IACZ,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3F,OAAO,OAAO;aACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;aACxD,MAAM,CAAC,CAAC,CAAC,EAAa,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IAC1C,CAAC;IAED,0EAA0E;IAC1E,mEAAmE;IACnE,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,SAAS,CACtB,GAAW,EACX,SAAoB,EACpB,UAA0E,EAAE;IAE5E,MAAM,OAAO,GAA2B;QACtC,MAAM,EAAE,6EAA6E;QACrF,YAAY,EAAE,UAAU;KACzB,CAAC;IACF,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1D,IAAI,OAAO,CAAC,YAAY;QAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC;IAE9E,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,MAAM,YAAY,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC3D,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IACvD,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAgB;IACrC,IAAI,EAAE,KAAK;IACX,KAAK,EAAE,KAAK,EAAE,MAAc,EAAE,UAA8B,EAAE,EAAE,EAAE;QAChE,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,IAAK,UAAU,CAAC,KAA8B,CAAC;QAC9E,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;QACxF,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;YACxB,sEAAsE;YACtE,sEAAsE;YACtE,+DAA+D;SAChE,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,QAAQ,EAAE,QAAQ,CAAC,IAAI,IAAI,QAAQ,EAAE,QAAQ;iBAC9C;aACF,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAC7D,OAAO;YACL,KAAK;YACL,KAAK,EAAE;gBACL,aAAa,EAAE,SAAS;gBACxB,QAAQ,EAAE,QAAQ,CAAC,IAAI,IAAI,QAAQ,EAAE,QAAQ;aAC9C;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Item, Source, SourceState } from "../../schemas/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal `fetch` signature an adapter needs.
|
|
4
|
+
*
|
|
5
|
+
* Defined narrowly so tests can inject a stub without bringing the entire
|
|
6
|
+
* `globalThis.fetch` type surface along. Node 22+'s built-in fetch satisfies
|
|
7
|
+
* this shape.
|
|
8
|
+
*/
|
|
9
|
+
export type FetchLike = (input: string | URL, init?: {
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
status: number;
|
|
14
|
+
headers: {
|
|
15
|
+
get(name: string): string | null;
|
|
16
|
+
};
|
|
17
|
+
text(): Promise<string>;
|
|
18
|
+
}>;
|
|
19
|
+
/** Result of a single adapter fetch — the items plus the next state to persist. */
|
|
20
|
+
export interface FeedFetchResult {
|
|
21
|
+
items: Item[];
|
|
22
|
+
/**
|
|
23
|
+
* Patch to merge into the source's `SourceState`. Adapters return only the
|
|
24
|
+
* fields they want to update so the watcher can decide how to merge with the
|
|
25
|
+
* existing on-disk state (e.g. unioning `lastSeenIds`).
|
|
26
|
+
*/
|
|
27
|
+
state: Partial<SourceState>;
|
|
28
|
+
/**
|
|
29
|
+
* `true` when the upstream returned 304 Not Modified (or an equivalent
|
|
30
|
+
* unchanged response). Watcher uses this to short-circuit item processing
|
|
31
|
+
* while still bumping `lastFetchedAt`.
|
|
32
|
+
*/
|
|
33
|
+
notModified?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface FeedAdapterOptions {
|
|
36
|
+
/** Caller-supplied fetch (defaults to global fetch). Injected by tests. */
|
|
37
|
+
fetch?: FetchLike;
|
|
38
|
+
/** Previous state for this source (`state/<id>.yaml`). */
|
|
39
|
+
state?: SourceState;
|
|
40
|
+
}
|
|
41
|
+
export interface FeedAdapter {
|
|
42
|
+
kind: Source["kind"];
|
|
43
|
+
fetch: (source: Source, options?: FeedAdapterOptions) => Promise<FeedFetchResult>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG,CACtB,KAAK,EAAE,MAAM,GAAG,GAAG,EACnB,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAC9D,OAAO,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC9C,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB,CAAC,CAAC;AAEH,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACrB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;CACnF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/feeds/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Item, Source, SourceFilters } from "../schemas/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Apply ADR-0006 filter semantics to a single item.
|
|
4
|
+
*
|
|
5
|
+
* Evaluation order:
|
|
6
|
+
* 1. Concatenate matchFields into a haystack.
|
|
7
|
+
* 2. If `caseSensitive` is false, lowercase haystack and keywords (word /
|
|
8
|
+
* substring modes). For regex mode, the `i` flag is used instead of
|
|
9
|
+
* lowercasing the pattern source.
|
|
10
|
+
* 3. If any `excludeKeywords` hits → reject (exclude wins over include).
|
|
11
|
+
* 4. If any `keywords` hits → accept, recording the hits in `matchedKeywords`.
|
|
12
|
+
* 5. Otherwise → reject.
|
|
13
|
+
*
|
|
14
|
+
* Returns the (possibly mutated) item annotated with `matchedKeywords` when
|
|
15
|
+
* accepted, or null when filtered out.
|
|
16
|
+
*/
|
|
17
|
+
export declare function evaluateFilter(item: Item, filters: SourceFilters): Item | null;
|
|
18
|
+
/**
|
|
19
|
+
* Apply a source's filter to a batch of items, returning only those that pass.
|
|
20
|
+
*
|
|
21
|
+
* Each returned item carries `matchedKeywords` populated from the include hits,
|
|
22
|
+
* so downstream `items` writers can persist the evidence for later inspection.
|
|
23
|
+
*/
|
|
24
|
+
export declare function filterItems(items: Item[], source: Source): Item[];
|
|
25
|
+
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/core/filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAyB,MAAM,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAiE9F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAiC9E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,CAQjE"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluate a single keyword against a haystack per the configured match mode.
|
|
3
|
+
*
|
|
4
|
+
* - `word`: whole-word match anchored on regex `\b` boundaries (keyword auto-escaped).
|
|
5
|
+
* - `substring`: plain `indexOf`-style match.
|
|
6
|
+
* - `regex`: treat the keyword as a JavaScript regular expression source. The
|
|
7
|
+
* `i` flag is added when `caseInsensitive` is true so character classes such
|
|
8
|
+
* as `\d` / `\W` retain their meaning (lowercasing the pattern source would
|
|
9
|
+
* silently corrupt them).
|
|
10
|
+
*
|
|
11
|
+
* For `word` and `substring` modes the caller is expected to have lowercased
|
|
12
|
+
* both haystack and keyword when matching is case-insensitive.
|
|
13
|
+
*/
|
|
14
|
+
function matchKeyword(haystack, keyword, mode, caseInsensitive) {
|
|
15
|
+
if (keyword.length === 0)
|
|
16
|
+
return false;
|
|
17
|
+
if (mode === "substring") {
|
|
18
|
+
return haystack.includes(keyword);
|
|
19
|
+
}
|
|
20
|
+
if (mode === "word") {
|
|
21
|
+
// Escape regex metachars in the user-supplied keyword so `word` mode is
|
|
22
|
+
// never interpreted as a pattern (ADR-0006: word mode is literal).
|
|
23
|
+
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
const re = new RegExp(`\\b${escaped}\\b`);
|
|
25
|
+
return re.test(haystack);
|
|
26
|
+
}
|
|
27
|
+
// regex mode: compile the keyword as-is. Invalid regexes throw RegExp errors,
|
|
28
|
+
// which we let propagate so the caller surfaces a clear validation failure.
|
|
29
|
+
// Use the `i` flag for case-insensitive runs rather than lowercasing the
|
|
30
|
+
// pattern source — `\d` / `\D` / `\w` / `\W` flip meaning when lowercased,
|
|
31
|
+
// which would silently break user patterns.
|
|
32
|
+
const re = new RegExp(keyword, caseInsensitive ? "i" : "");
|
|
33
|
+
return re.test(haystack);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Concatenate the configured `matchFields` of an item into a single search
|
|
37
|
+
* haystack. Fields that the item does not provide (`body` / `tags` for RSS,
|
|
38
|
+
* etc.) are silently skipped, per ADR-0002's "adapters skip unavailable fields"
|
|
39
|
+
* rule. Joining with newline keeps `\b` word boundaries from accidentally
|
|
40
|
+
* merging tokens across fields (e.g. title ending in "Claude" + summary
|
|
41
|
+
* starting with "Code" should not match `\bClaude Code\b`).
|
|
42
|
+
*/
|
|
43
|
+
function buildHaystack(item, fields) {
|
|
44
|
+
const parts = [];
|
|
45
|
+
for (const field of fields) {
|
|
46
|
+
if (field === "title") {
|
|
47
|
+
parts.push(item.title);
|
|
48
|
+
}
|
|
49
|
+
else if (field === "summary") {
|
|
50
|
+
if (item.summary)
|
|
51
|
+
parts.push(item.summary);
|
|
52
|
+
}
|
|
53
|
+
else if (field === "body" || field === "tags") {
|
|
54
|
+
// RSS adapter does not surface body / tags structurally; the schema does
|
|
55
|
+
// not model them either. Silently skip rather than throw so a single
|
|
56
|
+
// filter config can be reused across adapter kinds (ADR-0006).
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return parts.join("\n");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Apply ADR-0006 filter semantics to a single item.
|
|
63
|
+
*
|
|
64
|
+
* Evaluation order:
|
|
65
|
+
* 1. Concatenate matchFields into a haystack.
|
|
66
|
+
* 2. If `caseSensitive` is false, lowercase haystack and keywords (word /
|
|
67
|
+
* substring modes). For regex mode, the `i` flag is used instead of
|
|
68
|
+
* lowercasing the pattern source.
|
|
69
|
+
* 3. If any `excludeKeywords` hits → reject (exclude wins over include).
|
|
70
|
+
* 4. If any `keywords` hits → accept, recording the hits in `matchedKeywords`.
|
|
71
|
+
* 5. Otherwise → reject.
|
|
72
|
+
*
|
|
73
|
+
* Returns the (possibly mutated) item annotated with `matchedKeywords` when
|
|
74
|
+
* accepted, or null when filtered out.
|
|
75
|
+
*/
|
|
76
|
+
export function evaluateFilter(item, filters) {
|
|
77
|
+
const haystackRaw = buildHaystack(item, filters.matchFields);
|
|
78
|
+
const isRegex = filters.matchMode === "regex";
|
|
79
|
+
const caseInsensitive = !filters.caseSensitive;
|
|
80
|
+
// For regex mode the haystack is not lowercased — we rely on the `i` flag
|
|
81
|
+
// applied to the compiled pattern (see matchKeyword). For word / substring
|
|
82
|
+
// modes we lowercase both haystack and keywords up front.
|
|
83
|
+
const haystack = caseInsensitive && !isRegex ? haystackRaw.toLowerCase() : haystackRaw;
|
|
84
|
+
const normalizeKeyword = (s) => (caseInsensitive && !isRegex ? s.toLowerCase() : s);
|
|
85
|
+
// Exclude has priority over include (ADR-0006 §評価順序 step 3).
|
|
86
|
+
for (const kw of filters.excludeKeywords) {
|
|
87
|
+
if (matchKeyword(haystack, normalizeKeyword(kw), filters.matchMode, caseInsensitive)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Empty include list means "match nothing" — sources with no keywords cannot
|
|
92
|
+
// emit items. This matches the ADR's worked example and avoids accidental
|
|
93
|
+
// firehose ingestion when a user forgets to configure keywords.
|
|
94
|
+
if (filters.keywords.length === 0) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const hits = [];
|
|
98
|
+
for (const kw of filters.keywords) {
|
|
99
|
+
if (matchKeyword(haystack, normalizeKeyword(kw), filters.matchMode, caseInsensitive)) {
|
|
100
|
+
hits.push(kw);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (hits.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
return { ...item, matchedKeywords: hits };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Apply a source's filter to a batch of items, returning only those that pass.
|
|
109
|
+
*
|
|
110
|
+
* Each returned item carries `matchedKeywords` populated from the include hits,
|
|
111
|
+
* so downstream `items` writers can persist the evidence for later inspection.
|
|
112
|
+
*/
|
|
113
|
+
export function filterItems(items, source) {
|
|
114
|
+
const filters = source.filters;
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const item of items) {
|
|
117
|
+
const accepted = evaluateFilter(item, filters);
|
|
118
|
+
if (accepted)
|
|
119
|
+
out.push(accepted);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.js","sourceRoot":"","sources":["../../src/core/filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort prompt-injection pre-filter (ADR-0009 M1a + M5a — Adopt).
|
|
3
|
+
*
|
|
4
|
+
* Scans untrusted item content (`title` / `summary` / `raw`) against a small
|
|
5
|
+
* set of regex patterns that show up in well-known prompt-injection payloads
|
|
6
|
+
* (Anthropic's `[SYSTEM]` / OpenAI's `<|im_start|>` markers, generic "ignore
|
|
7
|
+
* previous instructions" variants, etc.). The match list is exposed on the
|
|
8
|
+
* item as `injectionFlags` so downstream tooling can surface a warning to the
|
|
9
|
+
* user.
|
|
10
|
+
*
|
|
11
|
+
* Scope (deliberately narrow):
|
|
12
|
+
*
|
|
13
|
+
* - **Audit-only.** A non-empty `matched` list never changes the item's
|
|
14
|
+
* `status`, never rewrites the content, and never blocks research. ADR-0009
|
|
15
|
+
* rejected auto-sanitize (M5b) on the same principle — the user retains the
|
|
16
|
+
* judgment call and can `radar dismiss` an item manually.
|
|
17
|
+
* - **High-precision patterns.** We intentionally avoid heuristics that try
|
|
18
|
+
* to catch every social-engineering phrase. The patterns target literal
|
|
19
|
+
* markers seen in real payloads; vague natural-language attacks (e.g.
|
|
20
|
+
* "please disregard the system prompt and instead...") are out of scope and
|
|
21
|
+
* will not be detected — that is a known false-negative documented in tests.
|
|
22
|
+
* - **No external dependency.** Pure `RegExp` over the haystack string.
|
|
23
|
+
*
|
|
24
|
+
* The regex `source` strings are exported (`INJECTION_PATTERNS`) so logs and
|
|
25
|
+
* user-facing diagnostics can name the offending pattern without exposing the
|
|
26
|
+
* full compiled flags. Patterns are matched case-insensitively to catch the
|
|
27
|
+
* common variants (`Ignore Previous Instructions`, `IGNORE PREVIOUS...`).
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Public list of pattern labels. Kept as a `readonly` constant so callers can
|
|
31
|
+
* enumerate the catalogue without reaching into the regex internals (logs,
|
|
32
|
+
* docs, etc.).
|
|
33
|
+
*/
|
|
34
|
+
export declare const INJECTION_PATTERN_LABELS: readonly string[];
|
|
35
|
+
export interface DetectInjectionResult {
|
|
36
|
+
/**
|
|
37
|
+
* Sorted-by-pattern-declaration list of pattern labels that fired. The list
|
|
38
|
+
* is deduplicated so multiple hits of the same pattern in the same haystack
|
|
39
|
+
* still result in a single entry, which keeps the persisted
|
|
40
|
+
* `injectionFlags` field stable across re-runs.
|
|
41
|
+
*/
|
|
42
|
+
matched: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run all patterns against `text` and return the set of labels that fired.
|
|
46
|
+
*
|
|
47
|
+
* Non-string inputs (undefined / null / numbers) return an empty result —
|
|
48
|
+
* callers are responsible for stringifying structured payloads (e.g. `raw`)
|
|
49
|
+
* before passing them in. The watcher does this via `JSON.stringify` so
|
|
50
|
+
* embedded strings inside structured payloads are still scanned.
|
|
51
|
+
*
|
|
52
|
+
* Empty / whitespace-only input returns an empty result without touching any
|
|
53
|
+
* regex, both as a fast-path and to avoid accidental matches on weird empty
|
|
54
|
+
* patterns down the line.
|
|
55
|
+
*/
|
|
56
|
+
export declare function detectInjection(text: string): DetectInjectionResult;
|
|
57
|
+
//# sourceMappingURL=injection-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injection-detector.d.ts","sourceRoot":"","sources":["../../src/core/injection-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAiEH;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAAiC,CAAC;AAExF,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB,CAWnE"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort prompt-injection pre-filter (ADR-0009 M1a + M5a — Adopt).
|
|
3
|
+
*
|
|
4
|
+
* Scans untrusted item content (`title` / `summary` / `raw`) against a small
|
|
5
|
+
* set of regex patterns that show up in well-known prompt-injection payloads
|
|
6
|
+
* (Anthropic's `[SYSTEM]` / OpenAI's `<|im_start|>` markers, generic "ignore
|
|
7
|
+
* previous instructions" variants, etc.). The match list is exposed on the
|
|
8
|
+
* item as `injectionFlags` so downstream tooling can surface a warning to the
|
|
9
|
+
* user.
|
|
10
|
+
*
|
|
11
|
+
* Scope (deliberately narrow):
|
|
12
|
+
*
|
|
13
|
+
* - **Audit-only.** A non-empty `matched` list never changes the item's
|
|
14
|
+
* `status`, never rewrites the content, and never blocks research. ADR-0009
|
|
15
|
+
* rejected auto-sanitize (M5b) on the same principle — the user retains the
|
|
16
|
+
* judgment call and can `radar dismiss` an item manually.
|
|
17
|
+
* - **High-precision patterns.** We intentionally avoid heuristics that try
|
|
18
|
+
* to catch every social-engineering phrase. The patterns target literal
|
|
19
|
+
* markers seen in real payloads; vague natural-language attacks (e.g.
|
|
20
|
+
* "please disregard the system prompt and instead...") are out of scope and
|
|
21
|
+
* will not be detected — that is a known false-negative documented in tests.
|
|
22
|
+
* - **No external dependency.** Pure `RegExp` over the haystack string.
|
|
23
|
+
*
|
|
24
|
+
* The regex `source` strings are exported (`INJECTION_PATTERNS`) so logs and
|
|
25
|
+
* user-facing diagnostics can name the offending pattern without exposing the
|
|
26
|
+
* full compiled flags. Patterns are matched case-insensitively to catch the
|
|
27
|
+
* common variants (`Ignore Previous Instructions`, `IGNORE PREVIOUS...`).
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Pattern catalogue. The `i` flag is intentional — all known payloads in the
|
|
31
|
+
* wild ignore case (`Ignore Previous Instructions` / `IGNORE PREVIOUS...`).
|
|
32
|
+
*
|
|
33
|
+
* Coverage (8 patterns, satisfies acceptance criterion 1: "≥ 6 patterns"):
|
|
34
|
+
*
|
|
35
|
+
* 1. `[SYSTEM]` literal — Anthropic-style fake system tag
|
|
36
|
+
* 2. `<|im_start|>` / `<|im_end|>` — OpenAI ChatML special tokens
|
|
37
|
+
* 3. `Ignore (the )?previous (instructions|prompt|directives)` — classic jailbreak preamble
|
|
38
|
+
* 4. `Disregard (the )?(above|previous|prior)` — paraphrase of (3)
|
|
39
|
+
* 5. `SYSTEM OVERRIDE` / `SYSTEM PROMPT OVERRIDE` — escalation marker
|
|
40
|
+
* 6. `You are now ...` style role-reassignment headers (constrained to avoid
|
|
41
|
+
* matching benign prose; requires uppercase first word + "now" + verb)
|
|
42
|
+
* 7. `BEGIN (NEW )?INSTRUCTIONS?` / `END (NEW )?INSTRUCTIONS?` — fenced-prompt markers
|
|
43
|
+
* 8. `<\|endoftext\|>` — GPT-family special token leakage marker
|
|
44
|
+
*/
|
|
45
|
+
const PATTERNS = [
|
|
46
|
+
{
|
|
47
|
+
label: "system-tag",
|
|
48
|
+
re: /\[SYSTEM\]/i,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: "chatml-token",
|
|
52
|
+
re: /<\|im_(start|end)\|>/i,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: "ignore-previous",
|
|
56
|
+
re: /\bignore\s+(the\s+)?(previous|prior|above|all\s+previous)\s+(instructions?|prompts?|directives?|messages?|rules?)/i,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: "disregard-above",
|
|
60
|
+
re: /\bdisregard\s+(the\s+)?(above|previous|prior|all\s+(previous|prior))/i,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: "system-override",
|
|
64
|
+
re: /\bsystem(\s+prompt)?\s+override\b/i,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: "role-reassignment",
|
|
68
|
+
re: /\byou\s+are\s+now\s+(a\s+|an\s+|the\s+)?(?:[A-Za-z][\w-]*\s+)?(assistant|ai|bot|agent|chatbot|system)\b/i,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "instruction-fence",
|
|
72
|
+
re: /\b(begin|end)\s+(new\s+)?instructions?\b/i,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "endoftext-token",
|
|
76
|
+
re: /<\|endoftext\|>/i,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Public list of pattern labels. Kept as a `readonly` constant so callers can
|
|
81
|
+
* enumerate the catalogue without reaching into the regex internals (logs,
|
|
82
|
+
* docs, etc.).
|
|
83
|
+
*/
|
|
84
|
+
export const INJECTION_PATTERN_LABELS = PATTERNS.map((p) => p.label);
|
|
85
|
+
/**
|
|
86
|
+
* Run all patterns against `text` and return the set of labels that fired.
|
|
87
|
+
*
|
|
88
|
+
* Non-string inputs (undefined / null / numbers) return an empty result —
|
|
89
|
+
* callers are responsible for stringifying structured payloads (e.g. `raw`)
|
|
90
|
+
* before passing them in. The watcher does this via `JSON.stringify` so
|
|
91
|
+
* embedded strings inside structured payloads are still scanned.
|
|
92
|
+
*
|
|
93
|
+
* Empty / whitespace-only input returns an empty result without touching any
|
|
94
|
+
* regex, both as a fast-path and to avoid accidental matches on weird empty
|
|
95
|
+
* patterns down the line.
|
|
96
|
+
*/
|
|
97
|
+
export function detectInjection(text) {
|
|
98
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
99
|
+
return { matched: [] };
|
|
100
|
+
}
|
|
101
|
+
const matched = [];
|
|
102
|
+
for (const { label, re } of PATTERNS) {
|
|
103
|
+
if (re.test(text)) {
|
|
104
|
+
matched.push(label);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { matched };
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=injection-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injection-detector.js","sourceRoot":"","sources":["../../src/core/injection-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAcH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,QAAQ,GAAuB;IACnC;QACE,KAAK,EAAE,YAAY;QACnB,EAAE,EAAE,aAAa;KAClB;IACD;QACE,KAAK,EAAE,cAAc;QACrB,EAAE,EAAE,uBAAuB;KAC5B;IACD;QACE,KAAK,EAAE,iBAAiB;QACxB,EAAE,EAAE,oHAAoH;KACzH;IACD;QACE,KAAK,EAAE,iBAAiB;QACxB,EAAE,EAAE,uEAAuE;KAC5E;IACD;QACE,KAAK,EAAE,iBAAiB;QACxB,EAAE,EAAE,oCAAoC;KACzC;IACD;QACE,KAAK,EAAE,mBAAmB;QAC1B,EAAE,EAAE,0GAA0G;KAC/G;IACD;QACE,KAAK,EAAE,mBAAmB;QAC1B,EAAE,EAAE,2CAA2C;KAChD;IACD;QACE,KAAK,EAAE,iBAAiB;QACxB,EAAE,EAAE,kBAAkB;KACvB;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAsB,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAYxF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACzB,CAAC;IACD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,QAAQ,EAAE,CAAC;QACrC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAClB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Item } from "../schemas/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Load all items under `items/<sourceId>/` and return them parsed + validated.
|
|
4
|
+
*
|
|
5
|
+
* Malformed files surface as thrown errors with the offending filename so
|
|
6
|
+
* tooling can pinpoint the issue. Callers that want a fault-tolerant scan
|
|
7
|
+
* should wrap the call in try/catch per file.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadItems(itemsDir: string, sourceId?: string): Promise<Item[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Persist items as YAML files under `items/<sourceId>/<itemId>.yaml`.
|
|
12
|
+
*
|
|
13
|
+
* Existing files for the same id are overwritten — callers are responsible
|
|
14
|
+
* for de-duplication via the `state.lastSeenIds` cursor before invoking this.
|
|
15
|
+
* The watcher uses this only for newly-detected items, so the overwrite path
|
|
16
|
+
* is effectively unreachable in normal operation; we still leave it permissive
|
|
17
|
+
* to keep the function idempotent under retry.
|
|
18
|
+
*/
|
|
19
|
+
export declare function saveItems(itemsDir: string, items: Item[]): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=items.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/core/items.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AA+ChD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CA6BpF;AAED;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9E"}
|