@diffdelta/client 0.1.0 → 0.1.1

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/dist/index.js CHANGED
@@ -22,7 +22,12 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  CursorStore: () => CursorStore,
24
24
  DiffDelta: () => DiffDelta,
25
- MemoryCursorStore: () => MemoryCursorStore
25
+ MemoryCursorStore: () => MemoryCursorStore,
26
+ parseFeed: () => parseFeed,
27
+ parseFeedItem: () => parseFeedItem,
28
+ parseHead: () => parseHead,
29
+ parseHealthCheck: () => parseHealthCheck,
30
+ parseSourceInfo: () => parseSourceInfo
26
31
  });
27
32
  module.exports = __toCommonJS(index_exports);
28
33
 
@@ -107,12 +112,15 @@ function parseFeedItem(data, bucket = "new") {
107
112
  } else if (typeof content === "string") {
108
113
  excerpt = content;
109
114
  }
110
- let riskScore = data.risk_score ?? null;
115
+ const signals = data.signals || {};
116
+ const suggestedAction = signals.suggested_action || null;
117
+ let riskScore = null;
118
+ const risk = data.risk;
119
+ if (typeof risk === "object" && risk !== null) {
120
+ riskScore = risk.score ?? null;
121
+ }
111
122
  if (riskScore === null) {
112
- const summary = data.summary;
113
- if (typeof summary === "object" && summary !== null) {
114
- riskScore = summary.risk_score ?? null;
115
- }
123
+ riskScore = data.risk_score ?? null;
116
124
  }
117
125
  return {
118
126
  source: data.source || "",
@@ -123,18 +131,35 @@ function parseFeedItem(data, bucket = "new") {
123
131
  publishedAt: data.published_at || null,
124
132
  updatedAt: data.updated_at || null,
125
133
  bucket,
134
+ signals,
135
+ suggestedAction,
126
136
  riskScore,
127
137
  provenance: data.provenance || {},
128
138
  raw: data
129
139
  };
130
140
  }
131
141
  function parseHead(data) {
142
+ const counts = data.counts || {};
143
+ const freshness = data.freshness;
132
144
  return {
133
145
  cursor: data.cursor || "",
134
- hash: data.hash || "",
135
146
  changed: data.changed || false,
136
147
  generatedAt: data.generated_at || "",
137
- ttlSec: data.ttl_sec || 900
148
+ ttlSec: data.ttl_sec || 60,
149
+ latestUrl: data.latest_url || "",
150
+ digestUrl: data.digest_url || null,
151
+ counts: {
152
+ new: counts.new || 0,
153
+ updated: counts.updated || 0,
154
+ removed: counts.removed || 0,
155
+ flagged: counts.flagged || 0
156
+ },
157
+ sourcesChecked: data.sources_checked || 0,
158
+ sourcesOk: data.sources_ok || 0,
159
+ allClear: data.all_clear || false,
160
+ allClearConfidence: data.all_clear_confidence ?? data.confidence ?? null,
161
+ freshness: freshness || null,
162
+ raw: data
138
163
  };
139
164
  }
140
165
  function parseFeed(data) {
@@ -148,15 +173,19 @@ function parseFeed(data) {
148
173
  const removedItems = (buckets.removed || []).map(
149
174
  (i) => parseFeedItem(i, "removed")
150
175
  );
176
+ const flaggedItems = (buckets.flagged || []).map(
177
+ (i) => parseFeedItem(i, "flagged")
178
+ );
151
179
  return {
152
180
  cursor: data.cursor || "",
153
181
  prevCursor: data.prev_cursor || "",
154
182
  sourceId: data.source_id || "",
155
183
  generatedAt: data.generated_at || "",
156
- items: [...newItems, ...updatedItems, ...removedItems],
184
+ items: [...newItems, ...updatedItems, ...removedItems, ...flaggedItems],
157
185
  new: newItems,
158
186
  updated: updatedItems,
159
187
  removed: removedItems,
188
+ flagged: flaggedItems,
160
189
  narrative: data.batch_narrative || "",
161
190
  raw: data
162
191
  };
@@ -174,9 +203,19 @@ function parseSourceInfo(data) {
174
203
  latestUrl: data.latest_url || ""
175
204
  };
176
205
  }
206
+ function parseHealthCheck(data) {
207
+ return {
208
+ ok: data.ok || false,
209
+ service: data.service || "",
210
+ time: data.time || "",
211
+ sourcesChecked: data.sources_checked || 0,
212
+ sourcesOk: data.sources_ok || 0,
213
+ engineVersion: data.engine_version || ""
214
+ };
215
+ }
177
216
 
178
217
  // src/client.ts
179
- var VERSION = "0.1.0";
218
+ var VERSION = "0.1.1";
180
219
  var DEFAULT_BASE_URL = "https://diffdelta.io";
181
220
  var DEFAULT_TIMEOUT = 15e3;
182
221
  var DiffDelta = class {
@@ -203,11 +242,11 @@ var DiffDelta = class {
203
242
  /**
204
243
  * Poll the global feed for new items since last poll.
205
244
  *
206
- * Checks head.json first (~400 bytes). Only fetches the full feed
245
+ * Checks head.json first (~200 bytes). Only fetches the full feed
207
246
  * if the cursor has changed. Automatically saves the new cursor.
208
247
  */
209
248
  async poll(options = {}) {
210
- const { tags, sources, buckets = ["new", "updated"] } = options;
249
+ const { tags, sources, buckets = ["new", "updated", "flagged"] } = options;
211
250
  return this.pollFeed({
212
251
  headUrl: `${this.baseUrl}/diff/head.json`,
213
252
  latestUrl: `${this.baseUrl}/diff/latest.json`,
@@ -224,10 +263,10 @@ var DiffDelta = class {
224
263
  * care about one source — fetches a smaller payload.
225
264
  */
226
265
  async pollSource(sourceId, options = {}) {
227
- const { tags, buckets = ["new", "updated"] } = options;
266
+ const { tags, buckets = ["new", "updated", "flagged"] } = options;
228
267
  return this.pollFeed({
229
- headUrl: `${this.baseUrl}/diff/source/${sourceId}/head.json`,
230
- latestUrl: `${this.baseUrl}/diff/source/${sourceId}/latest.json`,
268
+ headUrl: `${this.baseUrl}/diff/${sourceId}/head.json`,
269
+ latestUrl: `${this.baseUrl}/diff/${sourceId}/latest.json`,
231
270
  cursorKey: `source:${sourceId}`,
232
271
  tags,
233
272
  sources: void 0,
@@ -236,7 +275,7 @@ var DiffDelta = class {
236
275
  }
237
276
  // ── Low-level fetch ──
238
277
  /**
239
- * Fetch a head.json pointer.
278
+ * Fetch a head.json pointer. Cheapest call (~200 bytes).
240
279
  * @param url Full URL to head.json. Defaults to global head.
241
280
  */
242
281
  async head(url) {
@@ -259,6 +298,42 @@ var DiffDelta = class {
259
298
  const raw = data.sources || [];
260
299
  return raw.map(parseSourceInfo);
261
300
  }
301
+ // ── Discovery & Health ──
302
+ /**
303
+ * Check pipeline health. Returns when the engine last ran and whether
304
+ * all sources are healthy. A stale timestamp means the pipeline is down.
305
+ */
306
+ async checkHealth() {
307
+ const data = await this.fetchJson(`${this.baseUrl}/healthz.json`);
308
+ return parseHealthCheck(data);
309
+ }
310
+ /**
311
+ * Given a list of dependency names your bot uses, returns the source IDs
312
+ * you should monitor. Uses the static stacks.json mapping — no API call,
313
+ * pure local lookup after one fetch.
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * const sources = await dd.discoverSources(["openai", "langchain", "pinecone"]);
318
+ * // → ["openai_sdk_releases", "openai_api_changelog", "langchain_releases", "pinecone_status"]
319
+ * ```
320
+ */
321
+ async discoverSources(dependencies) {
322
+ const data = await this.fetchJson(`${this.baseUrl}/diff/stacks.json`);
323
+ const depsObj = data.dependencies || data.dependency_map || {};
324
+ const sourceIds = /* @__PURE__ */ new Set();
325
+ for (const dep of dependencies) {
326
+ const entry = depsObj[dep.toLowerCase()];
327
+ if (!entry) continue;
328
+ const sources = Array.isArray(entry) ? entry : entry.sources;
329
+ if (Array.isArray(sources)) {
330
+ for (const s of sources) {
331
+ sourceIds.add(s);
332
+ }
333
+ }
334
+ }
335
+ return [...sourceIds];
336
+ }
262
337
  // ── Continuous monitoring ──
263
338
  /**
264
339
  * Continuously poll and call a function for each new item.
@@ -270,6 +345,9 @@ var DiffDelta = class {
270
345
  * ```ts
271
346
  * dd.watch(item => {
272
347
  * console.log(`🚨 ${item.source}: ${item.headline}`);
348
+ * if (item.suggestedAction === "PATCH_IMMEDIATELY") {
349
+ * triggerAlert(item);
350
+ * }
273
351
  * }, { tags: ["security"] });
274
352
  * ```
275
353
  *
@@ -289,7 +367,7 @@ var DiffDelta = class {
289
367
  const h = await this.head();
290
368
  interval = Math.max(h.ttlSec, 60);
291
369
  } catch {
292
- interval = 900;
370
+ interval = 60;
293
371
  }
294
372
  }
295
373
  console.log(`[diffdelta] Watching for changes every ${interval}s...`);
@@ -301,8 +379,6 @@ var DiffDelta = class {
301
379
  for (const item of items) {
302
380
  await callback(item);
303
381
  }
304
- } else {
305
- console.log(`[diffdelta] No changes.`);
306
382
  }
307
383
  } catch (err) {
308
384
  if (signal?.aborted) break;
@@ -310,10 +386,14 @@ var DiffDelta = class {
310
386
  }
311
387
  await new Promise((resolve) => {
312
388
  const timer = setTimeout(resolve, interval * 1e3);
313
- signal?.addEventListener("abort", () => {
314
- clearTimeout(timer);
315
- resolve();
316
- }, { once: true });
389
+ signal?.addEventListener(
390
+ "abort",
391
+ () => {
392
+ clearTimeout(timer);
393
+ resolve();
394
+ },
395
+ { once: true }
396
+ );
317
397
  });
318
398
  }
319
399
  console.log("[diffdelta] Stopped.");
@@ -394,6 +474,11 @@ var DiffDelta = class {
394
474
  0 && (module.exports = {
395
475
  CursorStore,
396
476
  DiffDelta,
397
- MemoryCursorStore
477
+ MemoryCursorStore,
478
+ parseFeed,
479
+ parseFeedItem,
480
+ parseHead,
481
+ parseHealthCheck,
482
+ parseSourceInfo
398
483
  });
399
484
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cursor.ts","../src/models.ts","../src/client.ts"],"sourcesContent":["/**\n * DiffDelta — Agent-ready intelligence feeds.\n *\n * @example\n * ```ts\n * import { DiffDelta } from \"diffdelta\";\n *\n * const dd = new DiffDelta();\n * const items = await dd.poll({ tags: [\"security\"] });\n * items.forEach(i => console.log(`🚨 ${i.source}: ${i.headline}`));\n * ```\n *\n * @packageDocumentation\n */\n\nexport { DiffDelta } from \"./client.js\";\nexport type { DiffDeltaOptions, PollOptions, WatchOptions } from \"./client.js\";\nexport type {\n FeedItem,\n Feed,\n Head,\n SourceInfo,\n} from \"./models.js\";\nexport { CursorStore, MemoryCursorStore } from \"./cursor.js\";\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst DEFAULT_CURSOR_DIR = join(homedir(), \".diffdelta\");\nconst DEFAULT_CURSOR_FILE = \"cursors.json\";\n\n/**\n * Persists cursors to a local JSON file so bots survive restarts.\n *\n * By default, cursors are saved to ~/.diffdelta/cursors.json.\n * Each feed key gets its own cursor entry.\n */\nexport class CursorStore {\n private path: string;\n private cursors: Record<string, string> = {};\n\n constructor(path?: string) {\n this.path =\n path ||\n process.env.DD_CURSOR_PATH ||\n join(DEFAULT_CURSOR_DIR, DEFAULT_CURSOR_FILE);\n this.load();\n }\n\n /** Get the stored cursor for a feed key. */\n get(key: string): string | undefined {\n return this.cursors[key];\n }\n\n /** Save a cursor and persist to disk. */\n set(key: string, cursor: string): void {\n this.cursors[key] = cursor;\n this.save();\n }\n\n /** Clear cursor(s). If key is undefined, clears all cursors. */\n clear(key?: string): void {\n if (key) {\n delete this.cursors[key];\n } else {\n this.cursors = {};\n }\n this.save();\n }\n\n private load(): void {\n try {\n if (existsSync(this.path)) {\n const raw = readFileSync(this.path, \"utf-8\");\n const data = JSON.parse(raw);\n if (typeof data === \"object\" && data !== null) {\n this.cursors = data;\n }\n }\n } catch {\n // Corrupted file — start fresh\n this.cursors = {};\n }\n }\n\n private save(): void {\n try {\n const dir = dirname(this.path);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(this.path, JSON.stringify(this.cursors, null, 2));\n } catch {\n // Can't write — silently continue (in-memory only)\n }\n }\n}\n\n/**\n * In-memory-only cursor store (no file I/O).\n * Useful for serverless, browser, or Deno environments.\n */\nexport class MemoryCursorStore {\n private cursors: Record<string, string> = {};\n\n get(key: string): string | undefined {\n return this.cursors[key];\n }\n\n set(key: string, cursor: string): void {\n this.cursors[key] = cursor;\n }\n\n clear(key?: string): void {\n if (key) {\n delete this.cursors[key];\n } else {\n this.cursors = {};\n }\n }\n}\n","/** A single item from a DiffDelta feed. */\nexport interface FeedItem {\n /** Source identifier (e.g. \"cisa_kev\", \"nist_nvd\"). */\n source: string;\n /** Unique item ID within the source. */\n id: string;\n /** Human/agent-readable headline. */\n headline: string;\n /** Link to the original source. */\n url: string;\n /** Summary text extracted from the source. */\n excerpt: string;\n /** When the item was originally published. */\n publishedAt: string | null;\n /** When the item was last updated. */\n updatedAt: string | null;\n /** Which change bucket: \"new\", \"updated\", or \"removed\". */\n bucket: \"new\" | \"updated\" | \"removed\";\n /** Risk score 0–10, or null if not scored. */\n riskScore: number | null;\n /** Raw provenance data (fetched_at, evidence_urls, content_hash). */\n provenance: Record<string, unknown>;\n /** The full raw item object from the feed. */\n raw: Record<string, unknown>;\n}\n\n/** The lightweight head pointer for change detection. */\nexport interface Head {\n /** Opaque cursor string for change detection. */\n cursor: string;\n /** Content hash of the latest feed. */\n hash: string;\n /** Whether content has changed since last generation. */\n changed: boolean;\n /** When this head was generated. */\n generatedAt: string;\n /** Recommended polling interval in seconds. */\n ttlSec: number;\n}\n\n/** A full DiffDelta feed response. */\nexport interface Feed {\n /** The new cursor (save this for next poll). */\n cursor: string;\n /** The previous cursor. */\n prevCursor: string;\n /** Source ID (if per-source feed) or \"global\". */\n sourceId: string;\n /** When this feed was generated. */\n generatedAt: string;\n /** All items across all buckets. */\n items: FeedItem[];\n /** Items in the \"new\" bucket. */\n new: FeedItem[];\n /** Items in the \"updated\" bucket. */\n updated: FeedItem[];\n /** Items in the \"removed\" bucket. */\n removed: FeedItem[];\n /** Human-readable summary of what changed. */\n narrative: string;\n /** The full raw feed object. */\n raw: Record<string, unknown>;\n}\n\n/** Metadata about an available DiffDelta source. */\nexport interface SourceInfo {\n /** Unique source identifier (e.g. \"cisa_kev\"). */\n sourceId: string;\n /** Human-readable display name. */\n name: string;\n /** List of tags (e.g. [\"security\"]). */\n tags: string[];\n /** Brief description of the source. */\n description: string;\n /** URL of the source's homepage. */\n homepage: string;\n /** Whether the source is currently active. */\n enabled: boolean;\n /** Health status (\"ok\", \"degraded\", \"error\"). */\n status: string;\n /** Path to the source's head.json. */\n headUrl: string;\n /** Path to the source's latest.json. */\n latestUrl: string;\n}\n\n// ── Parsing helpers ──\n\n/** Parse a raw feed item into a typed FeedItem. */\nexport function parseFeedItem(\n data: Record<string, unknown>,\n bucket: \"new\" | \"updated\" | \"removed\" = \"new\"\n): FeedItem {\n const content = data.content as Record<string, unknown> | string | undefined;\n let excerpt = \"\";\n if (typeof content === \"object\" && content !== null) {\n excerpt =\n (content.excerpt_text as string) ||\n (content.summary as string) ||\n \"\";\n } else if (typeof content === \"string\") {\n excerpt = content;\n }\n\n // Extract risk_score from top-level or nested summary\n let riskScore: number | null = (data.risk_score as number) ?? null;\n if (riskScore === null) {\n const summary = data.summary as Record<string, unknown> | undefined;\n if (typeof summary === \"object\" && summary !== null) {\n riskScore = (summary.risk_score as number) ?? null;\n }\n }\n\n return {\n source: (data.source as string) || \"\",\n id: (data.id as string) || \"\",\n headline: (data.headline as string) || \"\",\n url: (data.url as string) || \"\",\n excerpt,\n publishedAt: (data.published_at as string) || null,\n updatedAt: (data.updated_at as string) || null,\n bucket,\n riskScore,\n provenance: (data.provenance as Record<string, unknown>) || {},\n raw: data,\n };\n}\n\n/** Parse a raw head.json response into a typed Head. */\nexport function parseHead(data: Record<string, unknown>): Head {\n return {\n cursor: (data.cursor as string) || \"\",\n hash: (data.hash as string) || \"\",\n changed: (data.changed as boolean) || false,\n generatedAt: (data.generated_at as string) || \"\",\n ttlSec: (data.ttl_sec as number) || 900,\n };\n}\n\n/** Parse a raw latest.json response into a typed Feed. */\nexport function parseFeed(data: Record<string, unknown>): Feed {\n const buckets = (data.buckets as Record<string, unknown[]>) || {};\n const newItems = (buckets.new || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"new\")\n );\n const updatedItems = (buckets.updated || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"updated\")\n );\n const removedItems = (buckets.removed || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"removed\")\n );\n\n return {\n cursor: (data.cursor as string) || \"\",\n prevCursor: (data.prev_cursor as string) || \"\",\n sourceId: (data.source_id as string) || \"\",\n generatedAt: (data.generated_at as string) || \"\",\n items: [...newItems, ...updatedItems, ...removedItems],\n new: newItems,\n updated: updatedItems,\n removed: removedItems,\n narrative: (data.batch_narrative as string) || \"\",\n raw: data,\n };\n}\n\n/** Parse a raw source object into a typed SourceInfo. */\nexport function parseSourceInfo(data: Record<string, unknown>): SourceInfo {\n return {\n sourceId: (data.source_id as string) || \"\",\n name: (data.name as string) || \"\",\n tags: (data.tags as string[]) || [],\n description: (data.description as string) || \"\",\n homepage: (data.homepage as string) || \"\",\n enabled: (data.enabled as boolean) ?? true,\n status: (data.status as string) || \"ok\",\n headUrl: (data.head_url as string) || \"\",\n latestUrl: (data.latest_url as string) || \"\",\n };\n}\n","/**\n * DiffDelta TypeScript client — agent-ready intelligence feeds.\n *\n * @example\n * ```ts\n * import { DiffDelta } from \"diffdelta\";\n *\n * const dd = new DiffDelta();\n *\n * // Poll for new items across all sources\n * const items = await dd.poll();\n * items.forEach(i => console.log(`${i.source}: ${i.headline}`));\n *\n * // Poll only security sources\n * const sec = await dd.poll({ tags: [\"security\"] });\n *\n * // Continuous monitoring\n * dd.watch(item => console.log(\"🚨\", item.headline), { tags: [\"security\"] });\n * ```\n */\n\nimport { CursorStore, MemoryCursorStore } from \"./cursor.js\";\nimport type { FeedItem, Feed, Head, SourceInfo } from \"./models.js\";\nimport { parseFeedItem, parseFeed, parseHead, parseSourceInfo } from \"./models.js\";\n\nconst VERSION = \"0.1.0\";\nconst DEFAULT_BASE_URL = \"https://diffdelta.io\";\nconst DEFAULT_TIMEOUT = 15_000; // ms\n\nexport interface DiffDeltaOptions {\n /** DiffDelta API base URL. Defaults to https://diffdelta.io. */\n baseUrl?: string;\n /** Pro/Enterprise API key (dd_live_...). */\n apiKey?: string;\n /**\n * Path to cursor file. Defaults to ~/.diffdelta/cursors.json.\n * Set to `null` to disable file persistence (in-memory only).\n * Set to `\"memory\"` for explicit in-memory mode (serverless, edge).\n */\n cursorPath?: string | null;\n /** HTTP timeout in milliseconds. Defaults to 15000. */\n timeout?: number;\n}\n\nexport interface PollOptions {\n /** Filter items to these tags (e.g. [\"security\"]). */\n tags?: string[];\n /** Filter items to these source IDs (e.g. [\"cisa_kev\", \"nist_nvd\"]). */\n sources?: string[];\n /**\n * Which buckets to return.\n * Defaults to [\"new\", \"updated\"].\n * Use [\"new\", \"updated\", \"removed\"] to include removals.\n */\n buckets?: Array<\"new\" | \"updated\" | \"removed\">;\n}\n\nexport interface WatchOptions extends PollOptions {\n /** Seconds between polls. Defaults to feed TTL (usually 900s). */\n interval?: number;\n /** If provided, an AbortSignal to stop watching. */\n signal?: AbortSignal;\n}\n\ninterface CursorStoreInterface {\n get(key: string): string | undefined;\n set(key: string, cursor: string): void;\n clear(key?: string): void;\n}\n\nexport class DiffDelta {\n readonly baseUrl: string;\n readonly apiKey?: string;\n readonly timeout: number;\n\n private cursors: CursorStoreInterface;\n private sourceTagsCache: Record<string, string[]> | null = null;\n\n constructor(options: DiffDeltaOptions = {}) {\n this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.apiKey = options.apiKey;\n this.timeout = options.timeout || DEFAULT_TIMEOUT;\n\n // Cursor persistence\n if (options.cursorPath === null || options.cursorPath === \"memory\") {\n this.cursors = new MemoryCursorStore();\n } else {\n try {\n this.cursors = new CursorStore(options.cursorPath || undefined);\n } catch {\n // Fallback to memory if file system not available\n this.cursors = new MemoryCursorStore();\n }\n }\n }\n\n // ── Core polling ──\n\n /**\n * Poll the global feed for new items since last poll.\n *\n * Checks head.json first (~400 bytes). Only fetches the full feed\n * if the cursor has changed. Automatically saves the new cursor.\n */\n async poll(options: PollOptions = {}): Promise<FeedItem[]> {\n const { tags, sources, buckets = [\"new\", \"updated\"] } = options;\n\n return this.pollFeed({\n headUrl: `${this.baseUrl}/diff/head.json`,\n latestUrl: `${this.baseUrl}/diff/latest.json`,\n cursorKey: \"global\",\n tags,\n sources,\n buckets,\n });\n }\n\n /**\n * Poll a specific source for new items since last poll.\n *\n * More efficient than `poll({ sources: [...] })` if you only\n * care about one source — fetches a smaller payload.\n */\n async pollSource(\n sourceId: string,\n options: Omit<PollOptions, \"sources\"> = {}\n ): Promise<FeedItem[]> {\n const { tags, buckets = [\"new\", \"updated\"] } = options;\n\n return this.pollFeed({\n headUrl: `${this.baseUrl}/diff/source/${sourceId}/head.json`,\n latestUrl: `${this.baseUrl}/diff/source/${sourceId}/latest.json`,\n cursorKey: `source:${sourceId}`,\n tags,\n sources: undefined,\n buckets,\n });\n }\n\n // ── Low-level fetch ──\n\n /**\n * Fetch a head.json pointer.\n * @param url Full URL to head.json. Defaults to global head.\n */\n async head(url?: string): Promise<Head> {\n const data = await this.fetchJson(url || `${this.baseUrl}/diff/head.json`);\n return parseHead(data);\n }\n\n /**\n * Fetch a full latest.json feed.\n * @param url Full URL to latest.json. Defaults to global latest.\n */\n async fetchFeed(url?: string): Promise<Feed> {\n const data = await this.fetchJson(\n url || `${this.baseUrl}/diff/latest.json`\n );\n return parseFeed(data);\n }\n\n /** List all available DiffDelta sources. */\n async sources(): Promise<SourceInfo[]> {\n const data = await this.fetchJson(`${this.baseUrl}/diff/sources.json`);\n const raw = (data.sources || []) as Record<string, unknown>[];\n return raw.map(parseSourceInfo);\n }\n\n // ── Continuous monitoring ──\n\n /**\n * Continuously poll and call a function for each new item.\n *\n * @param callback - Async or sync function called for each new FeedItem.\n * @param options - Watch options (tags, sources, interval, signal).\n *\n * @example\n * ```ts\n * dd.watch(item => {\n * console.log(`🚨 ${item.source}: ${item.headline}`);\n * }, { tags: [\"security\"] });\n * ```\n *\n * @example\n * ```ts\n * // Stop with AbortController\n * const ac = new AbortController();\n * dd.watch(handler, { signal: ac.signal });\n * setTimeout(() => ac.abort(), 60_000); // stop after 1 minute\n * ```\n */\n async watch(\n callback: (item: FeedItem) => void | Promise<void>,\n options: WatchOptions = {}\n ): Promise<void> {\n const { tags, sources, buckets, signal } = options;\n let interval = options.interval;\n\n // Determine interval from feed TTL if not specified\n if (!interval) {\n try {\n const h = await this.head();\n interval = Math.max(h.ttlSec, 60);\n } catch {\n interval = 900; // default 15 minutes\n }\n }\n\n console.log(`[diffdelta] Watching for changes every ${interval}s...`);\n\n while (!signal?.aborted) {\n try {\n const items = await this.poll({ tags, sources, buckets });\n if (items.length > 0) {\n console.log(`[diffdelta] ${items.length} new item(s) found.`);\n for (const item of items) {\n await callback(item);\n }\n } else {\n console.log(`[diffdelta] No changes.`);\n }\n } catch (err) {\n if (signal?.aborted) break;\n console.error(`[diffdelta] Error: ${err}. Retrying in ${interval}s...`);\n }\n\n // Sleep with abort support\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, interval! * 1000);\n signal?.addEventListener(\"abort\", () => {\n clearTimeout(timer);\n resolve();\n }, { once: true });\n });\n }\n\n console.log(\"[diffdelta] Stopped.\");\n }\n\n // ── Cursor management ──\n\n /** Reset stored cursors so the next poll returns all current items. */\n resetCursors(sourceId?: string): void {\n if (sourceId) {\n this.cursors.clear(`source:${sourceId}`);\n } else {\n this.cursors.clear();\n }\n }\n\n // ── Internal ──\n\n private async pollFeed(params: {\n headUrl: string;\n latestUrl: string;\n cursorKey: string;\n tags?: string[];\n sources?: string[];\n buckets: string[];\n }): Promise<FeedItem[]> {\n const { headUrl, latestUrl, cursorKey, tags, sources, buckets } = params;\n\n // Step 1: Fetch head.json (~400 bytes)\n const head = await this.head(headUrl);\n\n // Step 2: Compare cursor\n const storedCursor = this.cursors.get(cursorKey);\n if (storedCursor && storedCursor === head.cursor) {\n return []; // Nothing changed\n }\n\n // Step 3: Fetch full feed\n const feed = await this.fetchFeed(latestUrl);\n\n // Step 4: Save new cursor\n if (feed.cursor) {\n this.cursors.set(cursorKey, feed.cursor);\n }\n\n // Step 5: Filter and return\n let items = feed.items;\n\n // Filter by bucket\n items = items.filter((i) => buckets.includes(i.bucket));\n\n // Filter by source\n if (sources?.length) {\n items = items.filter((i) => sources.includes(i.source));\n }\n\n // Filter by tags\n if (tags?.length) {\n const tagMap = await this.getSourceTags();\n items = items.filter((i) => {\n const itemTags = tagMap[i.source] || [];\n return tags.some((t) => itemTags.includes(t));\n });\n }\n\n return items;\n }\n\n private async getSourceTags(): Promise<Record<string, string[]>> {\n if (this.sourceTagsCache) return this.sourceTagsCache;\n try {\n const allSources = await this.sources();\n this.sourceTagsCache = Object.fromEntries(\n allSources.map((s) => [s.sourceId, s.tags])\n );\n } catch {\n this.sourceTagsCache = {};\n }\n return this.sourceTagsCache;\n }\n\n private async fetchJson(url: string): Promise<Record<string, unknown>> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const headers: Record<string, string> = {\n \"User-Agent\": `diffdelta-js/${VERSION}`,\n Accept: \"application/json\",\n };\n if (this.apiKey) {\n headers[\"X-DiffDelta-Key\"] = this.apiKey;\n }\n\n const res = await fetch(url, { headers, signal: controller.signal });\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}: ${res.statusText} (${url})`);\n }\n return (await res.json()) as Record<string, unknown>;\n } finally {\n clearTimeout(timer);\n }\n }\n\n toString(): string {\n const tier = this.apiKey ? \"pro\" : \"free\";\n return `DiffDelta(baseUrl=${this.baseUrl}, tier=${tier})`;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cursor.ts","../src/models.ts","../src/client.ts"],"sourcesContent":["/**\n * DiffDelta — Agent-ready intelligence feeds.\n *\n * @example\n * ```ts\n * import { DiffDelta } from \"diffdelta\";\n *\n * const dd = new DiffDelta();\n * const items = await dd.poll({ tags: [\"security\"] });\n * items.forEach(i => console.log(`🚨 ${i.source}: ${i.headline}`));\n * ```\n *\n * @packageDocumentation\n */\n\nexport { DiffDelta } from \"./client.js\";\nexport type { DiffDeltaOptions, PollOptions, WatchOptions } from \"./client.js\";\nexport type {\n FeedItem,\n Feed,\n Head,\n SourceInfo,\n HealthCheck,\n Signals,\n SeveritySignal,\n ReleaseSignal,\n IncidentSignal,\n DeprecationSignal,\n SignalProvenance,\n SuggestedAction,\n} from \"./models.js\";\nexport {\n parseFeedItem,\n parseFeed,\n parseHead,\n parseSourceInfo,\n parseHealthCheck,\n} from \"./models.js\";\nexport { CursorStore, MemoryCursorStore } from \"./cursor.js\";\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst DEFAULT_CURSOR_DIR = join(homedir(), \".diffdelta\");\nconst DEFAULT_CURSOR_FILE = \"cursors.json\";\n\n/**\n * Persists cursors to a local JSON file so bots survive restarts.\n *\n * By default, cursors are saved to ~/.diffdelta/cursors.json.\n * Each feed key gets its own cursor entry.\n */\nexport class CursorStore {\n private path: string;\n private cursors: Record<string, string> = {};\n\n constructor(path?: string) {\n this.path =\n path ||\n process.env.DD_CURSOR_PATH ||\n join(DEFAULT_CURSOR_DIR, DEFAULT_CURSOR_FILE);\n this.load();\n }\n\n /** Get the stored cursor for a feed key. */\n get(key: string): string | undefined {\n return this.cursors[key];\n }\n\n /** Save a cursor and persist to disk. */\n set(key: string, cursor: string): void {\n this.cursors[key] = cursor;\n this.save();\n }\n\n /** Clear cursor(s). If key is undefined, clears all cursors. */\n clear(key?: string): void {\n if (key) {\n delete this.cursors[key];\n } else {\n this.cursors = {};\n }\n this.save();\n }\n\n private load(): void {\n try {\n if (existsSync(this.path)) {\n const raw = readFileSync(this.path, \"utf-8\");\n const data = JSON.parse(raw);\n if (typeof data === \"object\" && data !== null) {\n this.cursors = data;\n }\n }\n } catch {\n // Corrupted file — start fresh\n this.cursors = {};\n }\n }\n\n private save(): void {\n try {\n const dir = dirname(this.path);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(this.path, JSON.stringify(this.cursors, null, 2));\n } catch {\n // Can't write — silently continue (in-memory only)\n }\n }\n}\n\n/**\n * In-memory-only cursor store (no file I/O).\n * Useful for serverless, browser, or Deno environments.\n */\nexport class MemoryCursorStore {\n private cursors: Record<string, string> = {};\n\n get(key: string): string | undefined {\n return this.cursors[key];\n }\n\n set(key: string, cursor: string): void {\n this.cursors[key] = cursor;\n }\n\n clear(key?: string): void {\n if (key) {\n delete this.cursors[key];\n } else {\n this.cursors = {};\n }\n }\n}\n","// ── Signal types ──\n\n/** Severity signal extracted from security advisories. */\nexport interface SeveritySignal {\n level: \"critical\" | \"high\" | \"medium\" | \"low\" | string;\n source: string;\n cvss?: number;\n cwes?: string[];\n packages?: string[];\n exploited?: boolean;\n provenance?: SignalProvenance;\n}\n\n/** Release signal extracted from changelogs/GitHub releases. */\nexport interface ReleaseSignal {\n version: string;\n prerelease?: boolean;\n security_patch?: boolean;\n provenance?: SignalProvenance;\n}\n\n/** Incident signal extracted from status pages. */\nexport interface IncidentSignal {\n status: \"investigating\" | \"identified\" | \"monitoring\" | \"resolved\" | string;\n impact?: \"minor\" | \"major\" | \"critical\" | string;\n provenance?: SignalProvenance;\n}\n\n/** Deprecation signal extracted from changelogs/advisories. */\nexport interface DeprecationSignal {\n type: \"breaking_change\" | \"end_of_life\" | \"removal\" | \"deprecated\" | string;\n affects?: string[];\n confidence: \"high\" | \"medium\" | \"low\" | string;\n source: string;\n provenance?: SignalProvenance;\n}\n\n/** Provenance chain for a signal — traces it to an authoritative source. */\nexport interface SignalProvenance {\n method: string;\n authority: string;\n authority_url: string;\n evidence_url: string;\n}\n\n/** Action codes telling a bot exactly what to do. */\nexport type SuggestedAction =\n | \"PATCH_IMMEDIATELY\"\n | \"PATCH_SOON\"\n | \"VERSION_PIN\"\n | \"REVIEW_CHANGELOG\"\n | \"MONITOR_STATUS\"\n | \"ACKNOWLEDGE\"\n | \"NO_ACTION\";\n\n/** All structured signals on an item. */\nexport interface Signals {\n severity?: SeveritySignal;\n release?: ReleaseSignal;\n incident?: IncidentSignal;\n deprecation?: DeprecationSignal;\n suggested_action?: SuggestedAction;\n [key: string]: unknown;\n}\n\n// ── Core types ──\n\n/** A single item from a DiffDelta feed. */\nexport interface FeedItem {\n /** Source identifier (e.g. \"cisa_kev\", \"github_advisories\"). */\n source: string;\n /** Unique item ID within the source. */\n id: string;\n /** Human/agent-readable headline. */\n headline: string;\n /** Link to the original source. */\n url: string;\n /** Summary text extracted from the source. */\n excerpt: string;\n /** When the item was originally published. */\n publishedAt: string | null;\n /** When the item was last updated. */\n updatedAt: string | null;\n /** Which change bucket: \"new\", \"updated\", \"removed\", or \"flagged\". */\n bucket: \"new\" | \"updated\" | \"removed\" | \"flagged\";\n /** Structured signals: severity, release, incident, deprecation, suggested_action. */\n signals: Signals;\n /** Shortcut: the suggested_action code, if any. */\n suggestedAction: SuggestedAction | null;\n /** Risk score 0.0–1.0, or null if not scored. */\n riskScore: number | null;\n /** Item-level provenance (fetched_at, evidence_urls, content_hash). */\n provenance: Record<string, unknown>;\n /** The full raw item object from the feed. */\n raw: Record<string, unknown>;\n}\n\n/** The lightweight head pointer for change detection. */\nexport interface Head {\n /** Opaque cursor string for change detection. */\n cursor: string;\n /** Whether content has changed since last generation. */\n changed: boolean;\n /** When this head was generated. */\n generatedAt: string;\n /** Recommended polling interval in seconds. */\n ttlSec: number;\n /** URL to the full latest.json feed. */\n latestUrl: string;\n /** URL to the digest (global head only). */\n digestUrl: string | null;\n /** Item counts: new, updated, removed, flagged. */\n counts: { new: number; updated: number; removed: number; flagged: number };\n /** Number of sources checked this cycle (Verified Silence). */\n sourcesChecked: number;\n /** Number of sources healthy this cycle. */\n sourcesOk: number;\n /** True if nothing changed AND all sources are healthy. */\n allClear: boolean;\n /** 0.0–1.0 confidence that allClear is trustworthy. */\n allClearConfidence: number | null;\n /** Pipeline freshness: oldest data age, stale count. */\n freshness: {\n oldest_data_age_sec: number;\n mean_data_age_sec: number;\n stale_count: number;\n all_fresh: boolean;\n } | null;\n /** The full raw head.json object. */\n raw: Record<string, unknown>;\n}\n\n/** A full DiffDelta feed response. */\nexport interface Feed {\n /** The new cursor (save this for next poll). */\n cursor: string;\n /** The previous cursor. */\n prevCursor: string;\n /** Source ID (if per-source feed) or \"global\". */\n sourceId: string;\n /** When this feed was generated. */\n generatedAt: string;\n /** All items across all buckets. */\n items: FeedItem[];\n /** Items in the \"new\" bucket. */\n new: FeedItem[];\n /** Items in the \"updated\" bucket. */\n updated: FeedItem[];\n /** Items in the \"removed\" bucket. */\n removed: FeedItem[];\n /** Items in the \"flagged\" bucket. */\n flagged: FeedItem[];\n /** Human-readable summary of what changed. */\n narrative: string;\n /** The full raw feed object. */\n raw: Record<string, unknown>;\n}\n\n/** Metadata about an available DiffDelta source. */\nexport interface SourceInfo {\n /** Unique source identifier (e.g. \"cisa_kev\"). */\n sourceId: string;\n /** Human-readable display name. */\n name: string;\n /** List of tags (e.g. [\"security\"]). */\n tags: string[];\n /** Brief description of the source. */\n description: string;\n /** URL of the source's homepage. */\n homepage: string;\n /** Whether the source is currently active. */\n enabled: boolean;\n /** Health status (\"ok\", \"degraded\", \"error\"). */\n status: string;\n /** Path to the source's head.json. */\n headUrl: string;\n /** Path to the source's latest.json. */\n latestUrl: string;\n}\n\n/** Health check response from /healthz.json. */\nexport interface HealthCheck {\n ok: boolean;\n service: string;\n time: string;\n sourcesChecked: number;\n sourcesOk: number;\n engineVersion: string;\n}\n\n// ── Parsing helpers ──\n\n/** Parse a raw feed item into a typed FeedItem. */\nexport function parseFeedItem(\n data: Record<string, unknown>,\n bucket: \"new\" | \"updated\" | \"removed\" | \"flagged\" = \"new\"\n): FeedItem {\n const content = data.content as Record<string, unknown> | string | undefined;\n let excerpt = \"\";\n if (typeof content === \"object\" && content !== null) {\n excerpt =\n (content.excerpt_text as string) ||\n (content.summary as string) ||\n \"\";\n } else if (typeof content === \"string\") {\n excerpt = content;\n }\n\n // Extract signals\n const signals = (data.signals as Signals) || {};\n const suggestedAction = (signals.suggested_action as SuggestedAction) || null;\n\n // Extract risk score from risk.score or legacy risk_score\n let riskScore: number | null = null;\n const risk = data.risk as Record<string, unknown> | undefined;\n if (typeof risk === \"object\" && risk !== null) {\n riskScore = (risk.score as number) ?? null;\n }\n if (riskScore === null) {\n riskScore = (data.risk_score as number) ?? null;\n }\n\n return {\n source: (data.source as string) || \"\",\n id: (data.id as string) || \"\",\n headline: (data.headline as string) || \"\",\n url: (data.url as string) || \"\",\n excerpt,\n publishedAt: (data.published_at as string) || null,\n updatedAt: (data.updated_at as string) || null,\n bucket,\n signals,\n suggestedAction,\n riskScore,\n provenance: (data.provenance as Record<string, unknown>) || {},\n raw: data,\n };\n}\n\n/** Parse a raw head.json response into a typed Head. */\nexport function parseHead(data: Record<string, unknown>): Head {\n const counts = (data.counts as Record<string, number>) || {};\n const freshness = data.freshness as Head[\"freshness\"] | undefined;\n\n return {\n cursor: (data.cursor as string) || \"\",\n changed: (data.changed as boolean) || false,\n generatedAt: (data.generated_at as string) || \"\",\n ttlSec: (data.ttl_sec as number) || 60,\n latestUrl: (data.latest_url as string) || \"\",\n digestUrl: (data.digest_url as string) || null,\n counts: {\n new: counts.new || 0,\n updated: counts.updated || 0,\n removed: counts.removed || 0,\n flagged: counts.flagged || 0,\n },\n sourcesChecked: (data.sources_checked as number) || 0,\n sourcesOk: (data.sources_ok as number) || 0,\n allClear: (data.all_clear as boolean) || false,\n allClearConfidence:\n (data.all_clear_confidence as number) ??\n (data.confidence as number) ??\n null,\n freshness: freshness || null,\n raw: data,\n };\n}\n\n/** Parse a raw latest.json response into a typed Feed. */\nexport function parseFeed(data: Record<string, unknown>): Feed {\n const buckets = (data.buckets as Record<string, unknown[]>) || {};\n const newItems = (buckets.new || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"new\")\n );\n const updatedItems = (buckets.updated || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"updated\")\n );\n const removedItems = (buckets.removed || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"removed\")\n );\n const flaggedItems = (buckets.flagged || []).map((i) =>\n parseFeedItem(i as Record<string, unknown>, \"flagged\")\n );\n\n return {\n cursor: (data.cursor as string) || \"\",\n prevCursor: (data.prev_cursor as string) || \"\",\n sourceId: (data.source_id as string) || \"\",\n generatedAt: (data.generated_at as string) || \"\",\n items: [...newItems, ...updatedItems, ...removedItems, ...flaggedItems],\n new: newItems,\n updated: updatedItems,\n removed: removedItems,\n flagged: flaggedItems,\n narrative: (data.batch_narrative as string) || \"\",\n raw: data,\n };\n}\n\n/** Parse a raw source object into a typed SourceInfo. */\nexport function parseSourceInfo(data: Record<string, unknown>): SourceInfo {\n return {\n sourceId: (data.source_id as string) || \"\",\n name: (data.name as string) || \"\",\n tags: (data.tags as string[]) || [],\n description: (data.description as string) || \"\",\n homepage: (data.homepage as string) || \"\",\n enabled: (data.enabled as boolean) ?? true,\n status: (data.status as string) || \"ok\",\n headUrl: (data.head_url as string) || \"\",\n latestUrl: (data.latest_url as string) || \"\",\n };\n}\n\n/** Parse a raw healthz.json response into a typed HealthCheck. */\nexport function parseHealthCheck(data: Record<string, unknown>): HealthCheck {\n return {\n ok: (data.ok as boolean) || false,\n service: (data.service as string) || \"\",\n time: (data.time as string) || \"\",\n sourcesChecked: (data.sources_checked as number) || 0,\n sourcesOk: (data.sources_ok as number) || 0,\n engineVersion: (data.engine_version as string) || \"\",\n };\n}\n","/**\n * DiffDelta TypeScript client — agent-ready intelligence feeds.\n *\n * @example\n * ```ts\n * import { DiffDelta } from \"diffdelta\";\n *\n * const dd = new DiffDelta();\n *\n * // Quick health check — is the pipeline alive?\n * const health = await dd.checkHealth();\n * console.log(`Pipeline: ${health.ok ? \"healthy\" : \"degraded\"}, last run: ${health.time}`);\n *\n * // Poll for new items across all sources\n * const items = await dd.poll();\n * items.forEach(i => console.log(`${i.source}: ${i.headline}`));\n *\n * // Poll only security sources\n * const sec = await dd.poll({ tags: [\"security\"] });\n *\n * // Check what's relevant to your stack\n * const sources = await dd.discoverSources([\"openai\", \"langchain\", \"pinecone\"]);\n * console.log(\"Watch these:\", sources);\n *\n * // Continuous monitoring\n * dd.watch(item => console.log(\"🚨\", item.headline), { tags: [\"security\"] });\n * ```\n */\n\nimport { CursorStore, MemoryCursorStore } from \"./cursor.js\";\nimport type { FeedItem, Feed, Head, SourceInfo, HealthCheck } from \"./models.js\";\nimport { parseFeedItem, parseFeed, parseHead, parseSourceInfo, parseHealthCheck } from \"./models.js\";\n\nconst VERSION = \"0.1.1\";\nconst DEFAULT_BASE_URL = \"https://diffdelta.io\";\nconst DEFAULT_TIMEOUT = 15_000; // ms\n\nexport interface DiffDeltaOptions {\n /** DiffDelta API base URL. Defaults to https://diffdelta.io. */\n baseUrl?: string;\n /** Pro/Enterprise API key (dd_live_...). */\n apiKey?: string;\n /**\n * Path to cursor file. Defaults to ~/.diffdelta/cursors.json.\n * Set to `null` to disable file persistence (in-memory only).\n * Set to `\"memory\"` for explicit in-memory mode (serverless, edge).\n */\n cursorPath?: string | null;\n /** HTTP timeout in milliseconds. Defaults to 15000. */\n timeout?: number;\n}\n\nexport interface PollOptions {\n /** Filter items to these tags (e.g. [\"security\"]). */\n tags?: string[];\n /** Filter items to these source IDs (e.g. [\"cisa_kev\", \"github_advisories\"]). */\n sources?: string[];\n /**\n * Which buckets to return.\n * Defaults to [\"new\", \"updated\", \"flagged\"].\n * Use [\"new\", \"updated\", \"removed\", \"flagged\"] to include removals.\n */\n buckets?: Array<\"new\" | \"updated\" | \"removed\" | \"flagged\">;\n}\n\nexport interface WatchOptions extends PollOptions {\n /** Seconds between polls. Defaults to feed TTL (usually 60s). */\n interval?: number;\n /** If provided, an AbortSignal to stop watching. */\n signal?: AbortSignal;\n}\n\ninterface CursorStoreInterface {\n get(key: string): string | undefined;\n set(key: string, cursor: string): void;\n clear(key?: string): void;\n}\n\nexport class DiffDelta {\n readonly baseUrl: string;\n readonly apiKey?: string;\n readonly timeout: number;\n\n private cursors: CursorStoreInterface;\n private sourceTagsCache: Record<string, string[]> | null = null;\n\n constructor(options: DiffDeltaOptions = {}) {\n this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.apiKey = options.apiKey;\n this.timeout = options.timeout || DEFAULT_TIMEOUT;\n\n // Cursor persistence\n if (options.cursorPath === null || options.cursorPath === \"memory\") {\n this.cursors = new MemoryCursorStore();\n } else {\n try {\n this.cursors = new CursorStore(options.cursorPath || undefined);\n } catch {\n // Fallback to memory if file system not available\n this.cursors = new MemoryCursorStore();\n }\n }\n }\n\n // ── Core polling ──\n\n /**\n * Poll the global feed for new items since last poll.\n *\n * Checks head.json first (~200 bytes). Only fetches the full feed\n * if the cursor has changed. Automatically saves the new cursor.\n */\n async poll(options: PollOptions = {}): Promise<FeedItem[]> {\n const { tags, sources, buckets = [\"new\", \"updated\", \"flagged\"] } = options;\n\n return this.pollFeed({\n headUrl: `${this.baseUrl}/diff/head.json`,\n latestUrl: `${this.baseUrl}/diff/latest.json`,\n cursorKey: \"global\",\n tags,\n sources,\n buckets,\n });\n }\n\n /**\n * Poll a specific source for new items since last poll.\n *\n * More efficient than `poll({ sources: [...] })` if you only\n * care about one source — fetches a smaller payload.\n */\n async pollSource(\n sourceId: string,\n options: Omit<PollOptions, \"sources\"> = {}\n ): Promise<FeedItem[]> {\n const { tags, buckets = [\"new\", \"updated\", \"flagged\"] } = options;\n\n return this.pollFeed({\n headUrl: `${this.baseUrl}/diff/${sourceId}/head.json`,\n latestUrl: `${this.baseUrl}/diff/${sourceId}/latest.json`,\n cursorKey: `source:${sourceId}`,\n tags,\n sources: undefined,\n buckets,\n });\n }\n\n // ── Low-level fetch ──\n\n /**\n * Fetch a head.json pointer. Cheapest call (~200 bytes).\n * @param url Full URL to head.json. Defaults to global head.\n */\n async head(url?: string): Promise<Head> {\n const data = await this.fetchJson(url || `${this.baseUrl}/diff/head.json`);\n return parseHead(data);\n }\n\n /**\n * Fetch a full latest.json feed.\n * @param url Full URL to latest.json. Defaults to global latest.\n */\n async fetchFeed(url?: string): Promise<Feed> {\n const data = await this.fetchJson(\n url || `${this.baseUrl}/diff/latest.json`\n );\n return parseFeed(data);\n }\n\n /** List all available DiffDelta sources. */\n async sources(): Promise<SourceInfo[]> {\n const data = await this.fetchJson(`${this.baseUrl}/diff/sources.json`);\n const raw = (data.sources || []) as Record<string, unknown>[];\n return raw.map(parseSourceInfo);\n }\n\n // ── Discovery & Health ──\n\n /**\n * Check pipeline health. Returns when the engine last ran and whether\n * all sources are healthy. A stale timestamp means the pipeline is down.\n */\n async checkHealth(): Promise<HealthCheck> {\n const data = await this.fetchJson(`${this.baseUrl}/healthz.json`);\n return parseHealthCheck(data);\n }\n\n /**\n * Given a list of dependency names your bot uses, returns the source IDs\n * you should monitor. Uses the static stacks.json mapping — no API call,\n * pure local lookup after one fetch.\n *\n * @example\n * ```ts\n * const sources = await dd.discoverSources([\"openai\", \"langchain\", \"pinecone\"]);\n * // → [\"openai_sdk_releases\", \"openai_api_changelog\", \"langchain_releases\", \"pinecone_status\"]\n * ```\n */\n async discoverSources(dependencies: string[]): Promise<string[]> {\n const data = await this.fetchJson(`${this.baseUrl}/diff/stacks.json`);\n // Support both formats: { dependencies: { x: { sources: [...] } } }\n // and legacy { dependency_map: { x: [...] } }\n const depsObj = (data.dependencies || data.dependency_map || {}) as Record<\n string,\n { sources?: string[] } | string[]\n >;\n const sourceIds = new Set<string>();\n for (const dep of dependencies) {\n const entry = depsObj[dep.toLowerCase()];\n if (!entry) continue;\n const sources = Array.isArray(entry) ? entry : entry.sources;\n if (Array.isArray(sources)) {\n for (const s of sources) {\n sourceIds.add(s);\n }\n }\n }\n return [...sourceIds];\n }\n\n // ── Continuous monitoring ──\n\n /**\n * Continuously poll and call a function for each new item.\n *\n * @param callback - Async or sync function called for each new FeedItem.\n * @param options - Watch options (tags, sources, interval, signal).\n *\n * @example\n * ```ts\n * dd.watch(item => {\n * console.log(`🚨 ${item.source}: ${item.headline}`);\n * if (item.suggestedAction === \"PATCH_IMMEDIATELY\") {\n * triggerAlert(item);\n * }\n * }, { tags: [\"security\"] });\n * ```\n *\n * @example\n * ```ts\n * // Stop with AbortController\n * const ac = new AbortController();\n * dd.watch(handler, { signal: ac.signal });\n * setTimeout(() => ac.abort(), 60_000); // stop after 1 minute\n * ```\n */\n async watch(\n callback: (item: FeedItem) => void | Promise<void>,\n options: WatchOptions = {}\n ): Promise<void> {\n const { tags, sources, buckets, signal } = options;\n let interval = options.interval;\n\n // Determine interval from feed TTL if not specified\n if (!interval) {\n try {\n const h = await this.head();\n interval = Math.max(h.ttlSec, 60);\n } catch {\n interval = 60; // default 1 minute\n }\n }\n\n console.log(`[diffdelta] Watching for changes every ${interval}s...`);\n\n while (!signal?.aborted) {\n try {\n const items = await this.poll({ tags, sources, buckets });\n if (items.length > 0) {\n console.log(`[diffdelta] ${items.length} new item(s) found.`);\n for (const item of items) {\n await callback(item);\n }\n }\n } catch (err) {\n if (signal?.aborted) break;\n console.error(`[diffdelta] Error: ${err}. Retrying in ${interval}s...`);\n }\n\n // Sleep with abort support\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, interval! * 1000);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n }\n\n console.log(\"[diffdelta] Stopped.\");\n }\n\n // ── Cursor management ──\n\n /** Reset stored cursors so the next poll returns all current items. */\n resetCursors(sourceId?: string): void {\n if (sourceId) {\n this.cursors.clear(`source:${sourceId}`);\n } else {\n this.cursors.clear();\n }\n }\n\n // ── Internal ──\n\n private async pollFeed(params: {\n headUrl: string;\n latestUrl: string;\n cursorKey: string;\n tags?: string[];\n sources?: string[];\n buckets: string[];\n }): Promise<FeedItem[]> {\n const { headUrl, latestUrl, cursorKey, tags, sources, buckets } = params;\n\n // Step 1: Fetch head.json (~200 bytes)\n const head = await this.head(headUrl);\n\n // Step 2: Compare cursor — if unchanged, nothing to do\n const storedCursor = this.cursors.get(cursorKey);\n if (storedCursor && storedCursor === head.cursor) {\n return []; // Nothing changed\n }\n\n // Step 3: Fetch full feed\n const feed = await this.fetchFeed(latestUrl);\n\n // Step 4: Save new cursor\n if (feed.cursor) {\n this.cursors.set(cursorKey, feed.cursor);\n }\n\n // Step 5: Filter and return\n let items = feed.items;\n\n // Filter by bucket\n items = items.filter((i) => buckets.includes(i.bucket));\n\n // Filter by source\n if (sources?.length) {\n items = items.filter((i) => sources.includes(i.source));\n }\n\n // Filter by tags\n if (tags?.length) {\n const tagMap = await this.getSourceTags();\n items = items.filter((i) => {\n const itemTags = tagMap[i.source] || [];\n return tags.some((t) => itemTags.includes(t));\n });\n }\n\n return items;\n }\n\n private async getSourceTags(): Promise<Record<string, string[]>> {\n if (this.sourceTagsCache) return this.sourceTagsCache;\n try {\n const allSources = await this.sources();\n this.sourceTagsCache = Object.fromEntries(\n allSources.map((s) => [s.sourceId, s.tags])\n );\n } catch {\n this.sourceTagsCache = {};\n }\n return this.sourceTagsCache;\n }\n\n private async fetchJson(url: string): Promise<Record<string, unknown>> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const headers: Record<string, string> = {\n \"User-Agent\": `diffdelta-js/${VERSION}`,\n Accept: \"application/json\",\n };\n if (this.apiKey) {\n headers[\"X-DiffDelta-Key\"] = this.apiKey;\n }\n\n const res = await fetch(url, { headers, signal: controller.signal });\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}: ${res.statusText} (${url})`);\n }\n return (await res.json()) as Record<string, unknown>;\n } finally {\n clearTimeout(timer);\n }\n }\n\n toString(): string {\n const tier = this.apiKey ? \"pro\" : \"free\";\n return `DiffDelta(baseUrl=${this.baseUrl}, tier=${tier})`;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -79,12 +79,15 @@ function parseFeedItem(data, bucket = "new") {
79
79
  } else if (typeof content === "string") {
80
80
  excerpt = content;
81
81
  }
82
- let riskScore = data.risk_score ?? null;
82
+ const signals = data.signals || {};
83
+ const suggestedAction = signals.suggested_action || null;
84
+ let riskScore = null;
85
+ const risk = data.risk;
86
+ if (typeof risk === "object" && risk !== null) {
87
+ riskScore = risk.score ?? null;
88
+ }
83
89
  if (riskScore === null) {
84
- const summary = data.summary;
85
- if (typeof summary === "object" && summary !== null) {
86
- riskScore = summary.risk_score ?? null;
87
- }
90
+ riskScore = data.risk_score ?? null;
88
91
  }
89
92
  return {
90
93
  source: data.source || "",
@@ -95,18 +98,35 @@ function parseFeedItem(data, bucket = "new") {
95
98
  publishedAt: data.published_at || null,
96
99
  updatedAt: data.updated_at || null,
97
100
  bucket,
101
+ signals,
102
+ suggestedAction,
98
103
  riskScore,
99
104
  provenance: data.provenance || {},
100
105
  raw: data
101
106
  };
102
107
  }
103
108
  function parseHead(data) {
109
+ const counts = data.counts || {};
110
+ const freshness = data.freshness;
104
111
  return {
105
112
  cursor: data.cursor || "",
106
- hash: data.hash || "",
107
113
  changed: data.changed || false,
108
114
  generatedAt: data.generated_at || "",
109
- ttlSec: data.ttl_sec || 900
115
+ ttlSec: data.ttl_sec || 60,
116
+ latestUrl: data.latest_url || "",
117
+ digestUrl: data.digest_url || null,
118
+ counts: {
119
+ new: counts.new || 0,
120
+ updated: counts.updated || 0,
121
+ removed: counts.removed || 0,
122
+ flagged: counts.flagged || 0
123
+ },
124
+ sourcesChecked: data.sources_checked || 0,
125
+ sourcesOk: data.sources_ok || 0,
126
+ allClear: data.all_clear || false,
127
+ allClearConfidence: data.all_clear_confidence ?? data.confidence ?? null,
128
+ freshness: freshness || null,
129
+ raw: data
110
130
  };
111
131
  }
112
132
  function parseFeed(data) {
@@ -120,15 +140,19 @@ function parseFeed(data) {
120
140
  const removedItems = (buckets.removed || []).map(
121
141
  (i) => parseFeedItem(i, "removed")
122
142
  );
143
+ const flaggedItems = (buckets.flagged || []).map(
144
+ (i) => parseFeedItem(i, "flagged")
145
+ );
123
146
  return {
124
147
  cursor: data.cursor || "",
125
148
  prevCursor: data.prev_cursor || "",
126
149
  sourceId: data.source_id || "",
127
150
  generatedAt: data.generated_at || "",
128
- items: [...newItems, ...updatedItems, ...removedItems],
151
+ items: [...newItems, ...updatedItems, ...removedItems, ...flaggedItems],
129
152
  new: newItems,
130
153
  updated: updatedItems,
131
154
  removed: removedItems,
155
+ flagged: flaggedItems,
132
156
  narrative: data.batch_narrative || "",
133
157
  raw: data
134
158
  };
@@ -146,9 +170,19 @@ function parseSourceInfo(data) {
146
170
  latestUrl: data.latest_url || ""
147
171
  };
148
172
  }
173
+ function parseHealthCheck(data) {
174
+ return {
175
+ ok: data.ok || false,
176
+ service: data.service || "",
177
+ time: data.time || "",
178
+ sourcesChecked: data.sources_checked || 0,
179
+ sourcesOk: data.sources_ok || 0,
180
+ engineVersion: data.engine_version || ""
181
+ };
182
+ }
149
183
 
150
184
  // src/client.ts
151
- var VERSION = "0.1.0";
185
+ var VERSION = "0.1.1";
152
186
  var DEFAULT_BASE_URL = "https://diffdelta.io";
153
187
  var DEFAULT_TIMEOUT = 15e3;
154
188
  var DiffDelta = class {
@@ -175,11 +209,11 @@ var DiffDelta = class {
175
209
  /**
176
210
  * Poll the global feed for new items since last poll.
177
211
  *
178
- * Checks head.json first (~400 bytes). Only fetches the full feed
212
+ * Checks head.json first (~200 bytes). Only fetches the full feed
179
213
  * if the cursor has changed. Automatically saves the new cursor.
180
214
  */
181
215
  async poll(options = {}) {
182
- const { tags, sources, buckets = ["new", "updated"] } = options;
216
+ const { tags, sources, buckets = ["new", "updated", "flagged"] } = options;
183
217
  return this.pollFeed({
184
218
  headUrl: `${this.baseUrl}/diff/head.json`,
185
219
  latestUrl: `${this.baseUrl}/diff/latest.json`,
@@ -196,10 +230,10 @@ var DiffDelta = class {
196
230
  * care about one source — fetches a smaller payload.
197
231
  */
198
232
  async pollSource(sourceId, options = {}) {
199
- const { tags, buckets = ["new", "updated"] } = options;
233
+ const { tags, buckets = ["new", "updated", "flagged"] } = options;
200
234
  return this.pollFeed({
201
- headUrl: `${this.baseUrl}/diff/source/${sourceId}/head.json`,
202
- latestUrl: `${this.baseUrl}/diff/source/${sourceId}/latest.json`,
235
+ headUrl: `${this.baseUrl}/diff/${sourceId}/head.json`,
236
+ latestUrl: `${this.baseUrl}/diff/${sourceId}/latest.json`,
203
237
  cursorKey: `source:${sourceId}`,
204
238
  tags,
205
239
  sources: void 0,
@@ -208,7 +242,7 @@ var DiffDelta = class {
208
242
  }
209
243
  // ── Low-level fetch ──
210
244
  /**
211
- * Fetch a head.json pointer.
245
+ * Fetch a head.json pointer. Cheapest call (~200 bytes).
212
246
  * @param url Full URL to head.json. Defaults to global head.
213
247
  */
214
248
  async head(url) {
@@ -231,6 +265,42 @@ var DiffDelta = class {
231
265
  const raw = data.sources || [];
232
266
  return raw.map(parseSourceInfo);
233
267
  }
268
+ // ── Discovery & Health ──
269
+ /**
270
+ * Check pipeline health. Returns when the engine last ran and whether
271
+ * all sources are healthy. A stale timestamp means the pipeline is down.
272
+ */
273
+ async checkHealth() {
274
+ const data = await this.fetchJson(`${this.baseUrl}/healthz.json`);
275
+ return parseHealthCheck(data);
276
+ }
277
+ /**
278
+ * Given a list of dependency names your bot uses, returns the source IDs
279
+ * you should monitor. Uses the static stacks.json mapping — no API call,
280
+ * pure local lookup after one fetch.
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * const sources = await dd.discoverSources(["openai", "langchain", "pinecone"]);
285
+ * // → ["openai_sdk_releases", "openai_api_changelog", "langchain_releases", "pinecone_status"]
286
+ * ```
287
+ */
288
+ async discoverSources(dependencies) {
289
+ const data = await this.fetchJson(`${this.baseUrl}/diff/stacks.json`);
290
+ const depsObj = data.dependencies || data.dependency_map || {};
291
+ const sourceIds = /* @__PURE__ */ new Set();
292
+ for (const dep of dependencies) {
293
+ const entry = depsObj[dep.toLowerCase()];
294
+ if (!entry) continue;
295
+ const sources = Array.isArray(entry) ? entry : entry.sources;
296
+ if (Array.isArray(sources)) {
297
+ for (const s of sources) {
298
+ sourceIds.add(s);
299
+ }
300
+ }
301
+ }
302
+ return [...sourceIds];
303
+ }
234
304
  // ── Continuous monitoring ──
235
305
  /**
236
306
  * Continuously poll and call a function for each new item.
@@ -242,6 +312,9 @@ var DiffDelta = class {
242
312
  * ```ts
243
313
  * dd.watch(item => {
244
314
  * console.log(`🚨 ${item.source}: ${item.headline}`);
315
+ * if (item.suggestedAction === "PATCH_IMMEDIATELY") {
316
+ * triggerAlert(item);
317
+ * }
245
318
  * }, { tags: ["security"] });
246
319
  * ```
247
320
  *
@@ -261,7 +334,7 @@ var DiffDelta = class {
261
334
  const h = await this.head();
262
335
  interval = Math.max(h.ttlSec, 60);
263
336
  } catch {
264
- interval = 900;
337
+ interval = 60;
265
338
  }
266
339
  }
267
340
  console.log(`[diffdelta] Watching for changes every ${interval}s...`);
@@ -273,8 +346,6 @@ var DiffDelta = class {
273
346
  for (const item of items) {
274
347
  await callback(item);
275
348
  }
276
- } else {
277
- console.log(`[diffdelta] No changes.`);
278
349
  }
279
350
  } catch (err) {
280
351
  if (signal?.aborted) break;
@@ -282,10 +353,14 @@ var DiffDelta = class {
282
353
  }
283
354
  await new Promise((resolve) => {
284
355
  const timer = setTimeout(resolve, interval * 1e3);
285
- signal?.addEventListener("abort", () => {
286
- clearTimeout(timer);
287
- resolve();
288
- }, { once: true });
356
+ signal?.addEventListener(
357
+ "abort",
358
+ () => {
359
+ clearTimeout(timer);
360
+ resolve();
361
+ },
362
+ { once: true }
363
+ );
289
364
  });
290
365
  }
291
366
  console.log("[diffdelta] Stopped.");
@@ -365,6 +440,11 @@ var DiffDelta = class {
365
440
  export {
366
441
  CursorStore,
367
442
  DiffDelta,
368
- MemoryCursorStore
443
+ MemoryCursorStore,
444
+ parseFeed,
445
+ parseFeedItem,
446
+ parseHead,
447
+ parseHealthCheck,
448
+ parseSourceInfo
369
449
  };
370
450
  //# sourceMappingURL=index.mjs.map