@askalf/deepdive 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +118 -1
  2. package/dist/agent.d.ts +2 -1
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +36 -13
  5. package/dist/agent.js.map +1 -1
  6. package/dist/browser.d.ts.map +1 -1
  7. package/dist/browser.js.map +1 -1
  8. package/dist/cache.d.ts.map +1 -1
  9. package/dist/cache.js +22 -2
  10. package/dist/cache.js.map +1 -1
  11. package/dist/citations.d.ts +1 -0
  12. package/dist/citations.d.ts.map +1 -1
  13. package/dist/citations.js +4 -1
  14. package/dist/citations.js.map +1 -1
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +307 -17
  17. package/dist/cli.js.map +1 -1
  18. package/dist/completion.d.ts +3 -0
  19. package/dist/completion.d.ts.map +1 -0
  20. package/dist/completion.js +110 -0
  21. package/dist/completion.js.map +1 -0
  22. package/dist/confidence.d.ts +17 -0
  23. package/dist/confidence.d.ts.map +1 -0
  24. package/dist/confidence.js +41 -0
  25. package/dist/confidence.js.map +1 -0
  26. package/dist/config-file.d.ts +13 -0
  27. package/dist/config-file.d.ts.map +1 -0
  28. package/dist/config-file.js +149 -0
  29. package/dist/config-file.js.map +1 -0
  30. package/dist/config.d.ts +8 -0
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +2 -0
  33. package/dist/config.js.map +1 -1
  34. package/dist/dates.d.ts +8 -0
  35. package/dist/dates.d.ts.map +1 -0
  36. package/dist/dates.js +195 -0
  37. package/dist/dates.js.map +1 -0
  38. package/dist/diff.d.ts +46 -0
  39. package/dist/diff.d.ts.map +1 -0
  40. package/dist/diff.js +227 -0
  41. package/dist/diff.js.map +1 -0
  42. package/dist/html-export.d.ts +6 -0
  43. package/dist/html-export.d.ts.map +1 -0
  44. package/dist/html-export.js +146 -0
  45. package/dist/html-export.js.map +1 -0
  46. package/dist/index.d.ts +10 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +9 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/markdown.d.ts +7 -0
  51. package/dist/markdown.d.ts.map +1 -0
  52. package/dist/markdown.js +232 -0
  53. package/dist/markdown.js.map +1 -0
  54. package/dist/profiles.d.ts +5 -0
  55. package/dist/profiles.d.ts.map +1 -0
  56. package/dist/profiles.js +38 -0
  57. package/dist/profiles.js.map +1 -0
  58. package/dist/robots.d.ts +1 -0
  59. package/dist/robots.d.ts.map +1 -1
  60. package/dist/robots.js +4 -7
  61. package/dist/robots.js.map +1 -1
  62. package/dist/search/arxiv.d.ts +7 -0
  63. package/dist/search/arxiv.d.ts.map +1 -0
  64. package/dist/search/arxiv.js +81 -0
  65. package/dist/search/arxiv.js.map +1 -0
  66. package/dist/search/brave.d.ts +1 -1
  67. package/dist/search/brave.d.ts.map +1 -1
  68. package/dist/search/brave.js +2 -1
  69. package/dist/search/brave.js.map +1 -1
  70. package/dist/search/duckduckgo.d.ts +1 -1
  71. package/dist/search/duckduckgo.d.ts.map +1 -1
  72. package/dist/search/duckduckgo.js +2 -1
  73. package/dist/search/duckduckgo.js.map +1 -1
  74. package/dist/search/github.d.ts +16 -0
  75. package/dist/search/github.d.ts.map +1 -0
  76. package/dist/search/github.js +59 -0
  77. package/dist/search/github.js.map +1 -0
  78. package/dist/search/searxng.d.ts +1 -1
  79. package/dist/search/searxng.d.ts.map +1 -1
  80. package/dist/search/searxng.js +2 -1
  81. package/dist/search/searxng.js.map +1 -1
  82. package/dist/search/wikipedia.d.ts +16 -0
  83. package/dist/search/wikipedia.d.ts.map +1 -0
  84. package/dist/search/wikipedia.js +56 -0
  85. package/dist/search/wikipedia.js.map +1 -0
  86. package/dist/search.d.ts +1 -0
  87. package/dist/search.d.ts.map +1 -1
  88. package/dist/search.js +26 -0
  89. package/dist/search.js.map +1 -1
  90. package/dist/sessions.d.ts +15 -0
  91. package/dist/sessions.d.ts.map +1 -1
  92. package/dist/sessions.js +67 -0
  93. package/dist/sessions.js.map +1 -1
  94. package/dist/synthesize.d.ts +6 -1
  95. package/dist/synthesize.d.ts.map +1 -1
  96. package/dist/synthesize.js +14 -5
  97. package/dist/synthesize.js.map +1 -1
  98. package/package.json +1 -1
package/dist/dates.js ADDED
@@ -0,0 +1,195 @@
1
+ // Published-date extraction. Given a page's HTML, find when it was published
2
+ // so the source table can annotate recency and the synthesizer can weigh fresh
3
+ // sources over stale ones. Pure over the HTML string; no DOM, no deps.
4
+ //
5
+ // Precedence (first valid wins): JSON-LD datePublished -> publication-oriented
6
+ // <meta> tags -> <time datetime> -> JSON-LD dateModified -> modified <meta>
7
+ // tags. "published" beats "modified" everywhere because the question deepdive
8
+ // answers is usually "how current is this claim", and the original publication
9
+ // is the honest answer to that.
10
+ // Returns the published date as epoch milliseconds, or undefined when no
11
+ // trustworthy date is present. Dates outside a sane range (before 1990 or more
12
+ // than ~2 days into the future) are rejected as parse noise.
13
+ export function extractPublishedDate(html, now = Date.now()) {
14
+ const candidates = [];
15
+ const ld = jsonLdDates(html);
16
+ if (ld.published)
17
+ candidates.push(ld.published);
18
+ const metas = metaTags(html);
19
+ for (const key of PUBLISHED_META_KEYS) {
20
+ const hit = metas.get(key);
21
+ if (hit)
22
+ candidates.push(hit);
23
+ }
24
+ // First <time> element that carries a datetime attribute. Match the open tag
25
+ // with a single linear [^>]*, then read the attr from the bounded attribute
26
+ // string, so adversarial HTML can't trigger polynomial backtracking.
27
+ const timeRe = /<time\b([^>]*)>/gi;
28
+ let tm;
29
+ while ((tm = timeRe.exec(html)) !== null) {
30
+ const dt = attr(tm[1], "datetime");
31
+ if (dt) {
32
+ candidates.push(dt);
33
+ break;
34
+ }
35
+ }
36
+ if (ld.modified)
37
+ candidates.push(ld.modified);
38
+ for (const key of MODIFIED_META_KEYS) {
39
+ const hit = metas.get(key);
40
+ if (hit)
41
+ candidates.push(hit);
42
+ }
43
+ for (const c of candidates) {
44
+ const t = toEpoch(c, now);
45
+ if (t !== undefined)
46
+ return t;
47
+ }
48
+ return undefined;
49
+ }
50
+ // Publication-oriented meta keys, highest-trust first. Compared lowercased.
51
+ const PUBLISHED_META_KEYS = [
52
+ "article:published_time",
53
+ "og:article:published_time",
54
+ "datepublished",
55
+ "parsely-pub-date",
56
+ "sailthru.date",
57
+ "pubdate",
58
+ "publishdate",
59
+ "publish-date",
60
+ "publication_date",
61
+ "dc.date.issued",
62
+ "dcterms.issued",
63
+ "dc.date",
64
+ "date",
65
+ ];
66
+ const MODIFIED_META_KEYS = [
67
+ "article:modified_time",
68
+ "og:updated_time",
69
+ "datemodified",
70
+ "lastmod",
71
+ ];
72
+ // Exported for unit tests. Parses every <meta> into a key->content map, keyed
73
+ // by the lowercased name/property/itemprop. First occurrence of a key wins.
74
+ export function metaTags(html) {
75
+ const out = new Map();
76
+ const re = /<meta\b([^>]*?)\/?>/gi;
77
+ let m;
78
+ while ((m = re.exec(html)) !== null) {
79
+ const attrs = m[1];
80
+ const key = (attr(attrs, "property") ??
81
+ attr(attrs, "name") ??
82
+ attr(attrs, "itemprop"))?.toLowerCase();
83
+ const content = attr(attrs, "content");
84
+ if (key && content && !out.has(key))
85
+ out.set(key, content);
86
+ }
87
+ return out;
88
+ }
89
+ function attr(attrs, name) {
90
+ const m = new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(attrs);
91
+ return m ? m[1] : undefined;
92
+ }
93
+ // Exported for unit tests. Pulls datePublished / dateModified out of any
94
+ // JSON-LD <script> block, walking arrays and @graph. Malformed JSON is skipped.
95
+ export function jsonLdDates(html) {
96
+ const result = {};
97
+ // Match only the OPEN tag with a regex (single, linear [^>]*), then find the
98
+ // matching close with indexOf. A single regex spanning to the close tag can't
99
+ // be both complete (handle `</script foo>`) and linear — the literal-then-
100
+ // class shape backtracks polynomially on adversarial HTML (CodeQL). indexOf
101
+ // is linear and accepts every close-tag variant a browser does.
102
+ const openRe = /<script\b([^>]*)>/gi;
103
+ const typeRe = /type\s*=\s*["']application\/ld\+json["']/i;
104
+ // Lowercase once, not per close-tag scan — recomputing it inside the loop
105
+ // turns the whole pass O(n²) on a page with many <script> tags.
106
+ const lower = html.toLowerCase();
107
+ let m;
108
+ // Real pages carry a handful of JSON-LD blocks near the top; cap the number
109
+ // of <script> tags inspected so a malformed page with thousands of unclosed
110
+ // tags can't drive the close-tag scan into O(n²).
111
+ let scanned = 0;
112
+ while ((m = openRe.exec(html)) !== null) {
113
+ if (++scanned > 256)
114
+ break;
115
+ const contentStart = m.index + m[0].length;
116
+ const closeStart = findScriptClose(html, lower, contentStart);
117
+ if (closeStart !== -1) {
118
+ const gt = html.indexOf(">", closeStart + 8);
119
+ openRe.lastIndex = gt === -1 ? html.length : gt + 1;
120
+ }
121
+ if (!typeRe.test(m[1]))
122
+ continue;
123
+ const body = closeStart === -1 ? html.slice(contentStart) : html.slice(contentStart, closeStart);
124
+ let data;
125
+ try {
126
+ data = JSON.parse(body.trim());
127
+ }
128
+ catch {
129
+ continue;
130
+ }
131
+ walk(data, result);
132
+ if (result.published)
133
+ break; // best signal found
134
+ }
135
+ return result;
136
+ }
137
+ // Linear scan for the START index of the next `</script…>` close tag at or
138
+ // after `from`. Accepts `</script>`, `</script >`, `</script foo>` — every form
139
+ // a browser treats as a script end tag — but not `</scriptx>`. Returns -1 when
140
+ // no close tag exists.
141
+ function findScriptClose(html, lower, from) {
142
+ let i = lower.indexOf("</script", from);
143
+ while (i !== -1) {
144
+ const after = html[i + 8];
145
+ if (after === ">" || after === "/" || after === undefined || /\s/.test(after)) {
146
+ return i;
147
+ }
148
+ i = lower.indexOf("</script", i + 8);
149
+ }
150
+ return -1;
151
+ }
152
+ function walk(node, out) {
153
+ if (!node || typeof node !== "object")
154
+ return;
155
+ if (Array.isArray(node)) {
156
+ for (const item of node) {
157
+ walk(item, out);
158
+ if (out.published)
159
+ return;
160
+ }
161
+ return;
162
+ }
163
+ const obj = node;
164
+ if (!out.published && typeof obj.datePublished === "string") {
165
+ out.published = obj.datePublished;
166
+ }
167
+ if (!out.modified && typeof obj.dateModified === "string") {
168
+ out.modified = obj.dateModified;
169
+ }
170
+ for (const v of Object.values(obj)) {
171
+ if (out.published)
172
+ return;
173
+ if (v && typeof v === "object")
174
+ walk(v, out);
175
+ }
176
+ }
177
+ // Exported for unit tests. Parse a date string to epoch ms, rejecting values
178
+ // outside [1990-01-01, now + 2 days]. Bare YYYY / YYYY-MM are accepted.
179
+ export function toEpoch(s, now = Date.now()) {
180
+ const trimmed = s.trim();
181
+ if (!trimmed)
182
+ return undefined;
183
+ // Reject pure-noise short strings that Date.parse would coerce oddly.
184
+ if (!/\d{4}/.test(trimmed))
185
+ return undefined;
186
+ const t = Date.parse(trimmed);
187
+ if (Number.isNaN(t))
188
+ return undefined;
189
+ const MIN = Date.UTC(1990, 0, 1);
190
+ const MAX = now + 2 * 86_400_000;
191
+ if (t < MIN || t > MAX)
192
+ return undefined;
193
+ return t;
194
+ }
195
+ //# sourceMappingURL=dates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dates.js","sourceRoot":"","sources":["../src/dates.ts"],"names":[],"mappings":"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"}
package/dist/diff.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { SessionRecord } from "./sessions.js";
2
+ import type { SourceWithContent } from "./synthesize.js";
3
+ export interface SourceRef {
4
+ url: string;
5
+ title: string;
6
+ }
7
+ export type LineKind = "same" | "add" | "del";
8
+ export interface DiffLine {
9
+ kind: LineKind;
10
+ text: string;
11
+ }
12
+ export interface SessionSide {
13
+ id: string;
14
+ question: string;
15
+ createdAt: number;
16
+ model: string;
17
+ sourceCount: number;
18
+ rounds: number;
19
+ costUsd: number;
20
+ }
21
+ export interface SessionDiff {
22
+ a: SessionSide;
23
+ b: SessionSide;
24
+ sources: {
25
+ added: SourceRef[];
26
+ removed: SourceRef[];
27
+ shared: SourceRef[];
28
+ };
29
+ answer: {
30
+ lines: DiffLine[];
31
+ added: number;
32
+ removed: number;
33
+ unchanged: number;
34
+ };
35
+ }
36
+ export declare function diffSessions(a: SessionRecord, b: SessionRecord): SessionDiff;
37
+ export declare function diffSources(a: SourceWithContent[], b: SourceWithContent[]): SessionDiff["sources"];
38
+ export declare function diffLines(a: string[], b: string[]): SessionDiff["answer"];
39
+ export interface RenderDiffOptions {
40
+ color?: boolean;
41
+ context?: number;
42
+ }
43
+ export declare function renderDiffText(diff: SessionDiff, opts?: RenderDiffOptions): string;
44
+ export declare const DIFF_NARRATE_SYSTEM = "You compare two research answers to the same (or a closely related) question, produced at different times. Summarize what substantively changed between the OLDER answer (A) and the NEWER answer (B).\n\nRules:\n- Focus on changes in facts, conclusions, numbers, and recommendations \u2014 not wording.\n- Lead with the single most important change.\n- Call out new claims in B, claims dropped from A, and any reversals.\n- If the two answers are materially the same, say so in one sentence.\n- Be concise: a short paragraph or a few bullets. No preamble.";
45
+ export declare function buildDiffNarrateUser(a: SessionRecord, b: SessionRecord): string;
46
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAE9C,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,EAAE,WAAW,CAAC;IACf,CAAC,EAAE,WAAW,CAAC;IACf,OAAO,EAAE;QACP,KAAK,EAAE,SAAS,EAAE,CAAC;QACnB,OAAO,EAAE,SAAS,EAAE,CAAC;QACrB,MAAM,EAAE,SAAS,EAAE,CAAC;KACrB,CAAC;IACF,MAAM,EAAE;QACN,KAAK,EAAE,QAAQ,EAAE,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,GAAG,WAAW,CAO5E;AAgBD,wBAAgB,WAAW,CACzB,CAAC,EAAE,iBAAiB,EAAE,EACtB,CAAC,EAAE,iBAAiB,EAAE,GACrB,WAAW,CAAC,SAAS,CAAC,CAkBxB;AAYD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CA+CzE;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;IAGhB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,CAyEtF;AA+DD,eAAO,MAAM,mBAAmB,8iBAO+B,CAAC;AAEhE,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,GAAG,MAAM,CAQ/E"}
package/dist/diff.js ADDED
@@ -0,0 +1,227 @@
1
+ // Session diff — compare two saved research sessions and show how the answer
2
+ // (and the source set behind it) changed between them. The longitudinal use
3
+ // case hosted tools structurally can't serve: "I asked this last month; what's
4
+ // different now?" — answered entirely from your own local history.
5
+ //
6
+ // Pure over two SessionRecords. The LLM narration path (`--narrate`) only
7
+ // builds the prompt here; the CLI owns the network call, keeping this module
8
+ // I/O-free and unit-testable.
9
+ import { dedupeKey } from "./url-util.js";
10
+ export function diffSessions(a, b) {
11
+ return {
12
+ a: sideOf(a),
13
+ b: sideOf(b),
14
+ sources: diffSources(a.sources, b.sources),
15
+ answer: diffLines(splitLines(a.answer), splitLines(b.answer)),
16
+ };
17
+ }
18
+ function sideOf(r) {
19
+ return {
20
+ id: r.id,
21
+ question: r.question,
22
+ createdAt: r.createdAt,
23
+ model: r.llm?.model ?? "(unknown)",
24
+ sourceCount: r.sources.length,
25
+ rounds: r.rounds.length,
26
+ costUsd: typeof r.cost?.amountUsd === "number" ? r.cost.amountUsd : 0,
27
+ };
28
+ }
29
+ // Exported for unit tests. Source-set delta keyed on the normalized URL so
30
+ // trailing slashes / fragments don't read as "different source".
31
+ export function diffSources(a, b) {
32
+ const aKeys = new Map();
33
+ for (const s of a)
34
+ aKeys.set(dedupeKey(s.url), s);
35
+ const bKeys = new Map();
36
+ for (const s of b)
37
+ bKeys.set(dedupeKey(s.url), s);
38
+ const added = [];
39
+ const removed = [];
40
+ const shared = [];
41
+ for (const [k, s] of bKeys) {
42
+ if (aKeys.has(k))
43
+ shared.push(ref(s));
44
+ else
45
+ added.push(ref(s));
46
+ }
47
+ for (const [k, s] of aKeys) {
48
+ if (!bKeys.has(k))
49
+ removed.push(ref(s));
50
+ }
51
+ return { added, removed, shared };
52
+ }
53
+ function ref(s) {
54
+ return { url: s.url, title: s.title };
55
+ }
56
+ function splitLines(s) {
57
+ return s.replace(/\r\n/g, "\n").trimEnd().split("\n");
58
+ }
59
+ // Exported for unit tests. Standard LCS line diff. Answers are short (hundreds
60
+ // of lines at most), so the O(n·m) DP table is well within budget.
61
+ export function diffLines(a, b) {
62
+ const n = a.length;
63
+ const m = b.length;
64
+ // lcs[i][j] = length of LCS of a[i:] and b[j:].
65
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
66
+ for (let i = n - 1; i >= 0; i--) {
67
+ for (let j = m - 1; j >= 0; j--) {
68
+ lcs[i][j] =
69
+ a[i] === b[j]
70
+ ? lcs[i + 1][j + 1] + 1
71
+ : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
72
+ }
73
+ }
74
+ const lines = [];
75
+ let added = 0;
76
+ let removed = 0;
77
+ let unchanged = 0;
78
+ let i = 0;
79
+ let j = 0;
80
+ while (i < n && j < m) {
81
+ if (a[i] === b[j]) {
82
+ lines.push({ kind: "same", text: a[i] });
83
+ unchanged++;
84
+ i++;
85
+ j++;
86
+ }
87
+ else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
88
+ lines.push({ kind: "del", text: a[i] });
89
+ removed++;
90
+ i++;
91
+ }
92
+ else {
93
+ lines.push({ kind: "add", text: b[j] });
94
+ added++;
95
+ j++;
96
+ }
97
+ }
98
+ while (i < n) {
99
+ lines.push({ kind: "del", text: a[i++] });
100
+ removed++;
101
+ }
102
+ while (j < m) {
103
+ lines.push({ kind: "add", text: b[j++] });
104
+ added++;
105
+ }
106
+ return { lines, added, removed, unchanged };
107
+ }
108
+ // Exported for unit tests. Terminal-friendly rendering of a SessionDiff.
109
+ export function renderDiffText(diff, opts = {}) {
110
+ const color = opts.color ?? false;
111
+ const ctx = opts.context ?? 2;
112
+ const g = (s) => (color ? `\x1b[32m${s}\x1b[0m` : s);
113
+ const r = (s) => (color ? `\x1b[31m${s}\x1b[0m` : s);
114
+ const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
115
+ const out = [];
116
+ out.push(`diff ${diff.a.id} → ${diff.b.id}`);
117
+ out.push(dim(` ${ageBetween(diff.a.createdAt, diff.b.createdAt)} apart` +
118
+ ` · ${fmtDate(diff.a.createdAt)} → ${fmtDate(diff.b.createdAt)}`));
119
+ if (diff.a.question !== diff.b.question) {
120
+ out.push("");
121
+ out.push(` question ${r("- " + diff.a.question)}`);
122
+ out.push(` ${g("+ " + diff.b.question)}`);
123
+ }
124
+ out.push("");
125
+ out.push(" metadata");
126
+ out.push(metaRow("model", diff.a.model, diff.b.model, g, r));
127
+ out.push(metaRow("sources", String(diff.a.sourceCount), String(diff.b.sourceCount), g, r));
128
+ out.push(metaRow("rounds", String(diff.a.rounds), String(diff.b.rounds), g, r));
129
+ out.push(metaRow("cost", `$${diff.a.costUsd.toFixed(4)}`, `$${diff.b.costUsd.toFixed(4)}`, g, r));
130
+ out.push("");
131
+ const { added, removed, shared } = diff.sources;
132
+ out.push(` sources ${g("+" + added.length)} / ${r("-" + removed.length)} / ${shared.length} shared`);
133
+ for (const s of added)
134
+ out.push(" " + g(`+ ${s.url}`));
135
+ for (const s of removed)
136
+ out.push(" " + r(`- ${s.url}`));
137
+ out.push("");
138
+ out.push(` answer ${g("+" + diff.answer.added)} / ${r("-" + diff.answer.removed)} lines` +
139
+ ` (${diff.answer.unchanged} unchanged)`);
140
+ if (diff.answer.added === 0 && diff.answer.removed === 0) {
141
+ out.push(dim(" (answer text identical)"));
142
+ }
143
+ else {
144
+ out.push("");
145
+ for (const block of collapseContext(diff.answer.lines, ctx)) {
146
+ if (block.kind === "gap") {
147
+ out.push(dim(` … ${block.count} unchanged line${block.count === 1 ? "" : "s"} …`));
148
+ continue;
149
+ }
150
+ const prefix = block.line.kind === "add" ? g(" + ") : block.line.kind === "del" ? r(" - ") : " ";
151
+ const text = block.line.kind === "add"
152
+ ? g(block.line.text)
153
+ : block.line.kind === "del"
154
+ ? r(block.line.text)
155
+ : block.line.text;
156
+ out.push(prefix + text);
157
+ }
158
+ }
159
+ return out.join("\n");
160
+ }
161
+ // Collapse long runs of unchanged lines, keeping `ctx` lines of context on
162
+ // either side of each change.
163
+ function collapseContext(lines, ctx) {
164
+ const keep = new Array(lines.length).fill(false);
165
+ for (let i = 0; i < lines.length; i++) {
166
+ if (lines[i].kind !== "same") {
167
+ for (let k = Math.max(0, i - ctx); k <= Math.min(lines.length - 1, i + ctx); k++) {
168
+ keep[k] = true;
169
+ }
170
+ }
171
+ }
172
+ const blocks = [];
173
+ let gap = 0;
174
+ for (let i = 0; i < lines.length; i++) {
175
+ if (keep[i]) {
176
+ if (gap > 0) {
177
+ blocks.push({ kind: "gap", count: gap });
178
+ gap = 0;
179
+ }
180
+ blocks.push({ kind: "line", line: lines[i] });
181
+ }
182
+ else {
183
+ gap++;
184
+ }
185
+ }
186
+ if (gap > 0)
187
+ blocks.push({ kind: "gap", count: gap });
188
+ return blocks;
189
+ }
190
+ function metaRow(label, a, b, g, r) {
191
+ if (a === b)
192
+ return ` ${label.padEnd(9)} ${a}`;
193
+ return ` ${label.padEnd(9)} ${r(a)} → ${g(b)}`;
194
+ }
195
+ function fmtDate(ms) {
196
+ const d = new Date(ms);
197
+ return Number.isNaN(d.getTime()) ? "?" : d.toISOString().slice(0, 10);
198
+ }
199
+ function ageBetween(a, b) {
200
+ const ms = Math.abs(b - a);
201
+ const days = Math.floor(ms / 86_400_000);
202
+ if (days >= 1)
203
+ return `${days}d`;
204
+ const hours = Math.floor(ms / 3_600_000);
205
+ if (hours >= 1)
206
+ return `${hours}h`;
207
+ const mins = Math.floor(ms / 60_000);
208
+ return `${mins}m`;
209
+ }
210
+ // ── LLM narration (--narrate) ────────────────────────────────────────────────
211
+ // diff.ts stays I/O-free: it only builds the prompt. The CLI runs the call.
212
+ export const DIFF_NARRATE_SYSTEM = `You compare two research answers to the same (or a closely related) question, produced at different times. Summarize what substantively changed between the OLDER answer (A) and the NEWER answer (B).
213
+
214
+ Rules:
215
+ - Focus on changes in facts, conclusions, numbers, and recommendations — not wording.
216
+ - Lead with the single most important change.
217
+ - Call out new claims in B, claims dropped from A, and any reversals.
218
+ - If the two answers are materially the same, say so in one sentence.
219
+ - Be concise: a short paragraph or a few bullets. No preamble.`;
220
+ export function buildDiffNarrateUser(a, b) {
221
+ return (`Question (A, older — ${fmtDate(a.createdAt)}): ${a.question}\n` +
222
+ `Question (B, newer — ${fmtDate(b.createdAt)}): ${b.question}\n\n` +
223
+ `=== ANSWER A (older) ===\n${a.answer}\n\n` +
224
+ `=== ANSWER B (newer) ===\n${b.answer}\n\n` +
225
+ `Summarize what changed from A to B.`);
226
+ }
227
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { SessionRecord } from "./sessions.js";
2
+ export interface HtmlReportOptions {
3
+ footer?: string;
4
+ }
5
+ export declare function renderHtmlReport(record: SessionRecord, opts?: HtmlReportOptions): string;
6
+ //# sourceMappingURL=html-export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-export.d.ts","sourceRoot":"","sources":["../src/html-export.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAKnD,MAAM,WAAW,iBAAiB;IAGhC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,aAAa,EACrB,IAAI,GAAE,iBAAsB,GAC3B,MAAM,CAgCR"}
@@ -0,0 +1,146 @@
1
+ // HTML export — turn a saved session into a single, self-contained,
2
+ // shareable HTML document. No external assets, no scripts, inline CSS only,
3
+ // so the file works offline, opens in any browser, and can be emailed or
4
+ // committed as an artifact. Pure over a SessionRecord.
5
+ //
6
+ // This is the "polished artifact" that hosted research tools produce and the
7
+ // terminal markdown doesn't: the thing you hand to a colleague. The answer's
8
+ // markdown is rendered via the hand-rolled src/markdown.ts so there's still
9
+ // zero runtime dependency.
10
+ import { markdownToHtml, escapeHtml } from "./markdown.js";
11
+ const CITATION_ANCHOR_PREFIX = "source-";
12
+ const DEFAULT_FOOTER = 'Generated by <a href="https://github.com/askalf/deepdive" rel="noopener noreferrer">deepdive</a>' +
13
+ ' — a local research agent from <a href="https://sprayberrylabs.com" rel="noopener noreferrer">Sprayberry Labs</a>.';
14
+ export function renderHtmlReport(record, opts = {}) {
15
+ const title = escapeHtml(oneLine(record.question)) || "deepdive report";
16
+ const body = markdownToHtml(record.answer, {
17
+ citationAnchorPrefix: CITATION_ANCHOR_PREFIX,
18
+ });
19
+ const meta = renderMetaLine(record);
20
+ const sources = renderSourcesHtml(record);
21
+ const footer = opts.footer === undefined ? DEFAULT_FOOTER : opts.footer;
22
+ const footerHtml = footer ? `\n <footer>${footer}</footer>` : "";
23
+ return `<!doctype html>
24
+ <html lang="en">
25
+ <head>
26
+ <meta charset="utf-8">
27
+ <meta name="viewport" content="width=device-width, initial-scale=1">
28
+ <meta name="generator" content="deepdive">
29
+ <title>${title}</title>
30
+ <style>${STYLE}</style>
31
+ </head>
32
+ <body>
33
+ <main>
34
+ <h1>${title}</h1>
35
+ ${meta}
36
+ <article>
37
+ ${body}
38
+ </article>
39
+ ${sources}${footerHtml}
40
+ </main>
41
+ </body>
42
+ </html>
43
+ `;
44
+ }
45
+ function renderMetaLine(record) {
46
+ const parts = [];
47
+ const created = new Date(record.createdAt);
48
+ if (!Number.isNaN(created.getTime())) {
49
+ parts.push(`<time datetime="${created.toISOString()}">${created.toISOString().slice(0, 10)}</time>`);
50
+ }
51
+ if (record.llm?.model)
52
+ parts.push(escapeHtml(record.llm.model));
53
+ const n = record.sources.length;
54
+ parts.push(`${n} source${n === 1 ? "" : "s"}`);
55
+ if (record.rounds.length > 0) {
56
+ parts.push(`${record.rounds.length} round${record.rounds.length === 1 ? "" : "s"}`);
57
+ }
58
+ const cost = record.cost?.amountUsd;
59
+ if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) {
60
+ parts.push(`~$${cost.toFixed(cost < 0.01 ? 4 : 2)}`);
61
+ }
62
+ return `<p class="meta">${parts.join(" &middot; ")}</p>`;
63
+ }
64
+ function renderSourcesHtml(record) {
65
+ if (record.sources.length === 0)
66
+ return "";
67
+ const items = record.sources
68
+ .map((s) => {
69
+ const date = new Date(s.fetchedAt);
70
+ const fetched = Number.isNaN(date.getTime())
71
+ ? ""
72
+ : `fetched ${date.toISOString().slice(0, 10)}`;
73
+ const pub = typeof s.publishedAt === "number"
74
+ ? `published ${new Date(s.publishedAt).toISOString().slice(0, 10)}`
75
+ : "";
76
+ const metaBits = [pub, fetched].filter(Boolean).join(" · ");
77
+ const dateStr = metaBits ? ` <span class="fetched">${metaBits}</span>` : "";
78
+ const safeUrl = escapeHtml(s.url);
79
+ const label = escapeHtml(oneLine(s.title)) || safeUrl;
80
+ const safeHref = isSafeHref(s.url) ? safeUrl : "#";
81
+ return ` <li id="${CITATION_ANCHOR_PREFIX}${s.id}"><a href="${safeHref}" rel="noopener noreferrer">${label}</a>${dateStr}</li>`;
82
+ })
83
+ .join("\n");
84
+ return ` <section class="sources">\n <h2>Sources</h2>\n <ol>\n${items}\n </ol>\n </section>`;
85
+ }
86
+ function isSafeHref(url) {
87
+ return /^(https?:|mailto:|file:)/i.test(url);
88
+ }
89
+ function oneLine(s) {
90
+ return s.replace(/\s+/g, " ").trim();
91
+ }
92
+ // Inline stylesheet — readable typography, light/dark aware, print-friendly.
93
+ const STYLE = `
94
+ :root { color-scheme: light dark; }
95
+ * { box-sizing: border-box; }
96
+ body {
97
+ margin: 0;
98
+ font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
99
+ color: #1a1a1a;
100
+ background: #fbfbfa;
101
+ }
102
+ main { max-width: 46rem; margin: 0 auto; padding: 3rem 1.25rem 4rem; }
103
+ h1 { font-size: 1.85rem; line-height: 1.25; margin: 0 0 0.5rem; letter-spacing: -0.01em; }
104
+ h2 { font-size: 1.3rem; margin: 2rem 0 0.75rem; }
105
+ h3 { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; }
106
+ h4, h5, h6 { margin: 1.25rem 0 0.5rem; }
107
+ p { margin: 0 0 1rem; }
108
+ a { color: #1a56db; text-decoration: none; }
109
+ a:hover { text-decoration: underline; }
110
+ .meta { color: #6b7280; font-size: 0.85rem; margin: 0 0 2rem; }
111
+ article { font-size: 1.02rem; }
112
+ ul, ol { padding-left: 1.5rem; margin: 0 0 1rem; }
113
+ li { margin: 0.3rem 0; }
114
+ blockquote { margin: 0 0 1rem; padding: 0.4rem 1rem; border-left: 3px solid #d1d5db; color: #4b5563; }
115
+ code { font: 0.88em ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; background: #f0f0ef; padding: 0.1em 0.35em; border-radius: 4px; }
116
+ pre { background: #f0f0ef; padding: 1rem; border-radius: 8px; overflow-x: auto; margin: 0 0 1rem; }
117
+ pre code { background: none; padding: 0; }
118
+ table { border-collapse: collapse; width: 100%; margin: 0 0 1.25rem; font-size: 0.95rem; }
119
+ th, td { border: 1px solid #e5e7eb; padding: 0.5rem 0.7rem; text-align: left; vertical-align: top; }
120
+ th { background: #f3f4f6; font-weight: 600; }
121
+ hr { border: none; border-top: 1px solid #e5e7eb; margin: 2rem 0; }
122
+ sup.cite { font-size: 0.7em; line-height: 0; }
123
+ sup.cite a { padding: 0 0.1em; }
124
+ .sources { margin-top: 2.5rem; border-top: 1px solid #e5e7eb; padding-top: 1rem; }
125
+ .sources li { margin: 0.45rem 0; }
126
+ .sources .fetched { color: #9ca3af; font-size: 0.82rem; }
127
+ :target { background: #fff3bf; scroll-margin-top: 1rem; }
128
+ footer { margin-top: 3rem; padding-top: 1.25rem; border-top: 1px solid #e5e7eb; color: #9ca3af; font-size: 0.82rem; }
129
+ @media (prefers-color-scheme: dark) {
130
+ body { color: #e5e7eb; background: #18181b; }
131
+ a { color: #7aa7ff; }
132
+ .meta, footer { color: #9ca3af; }
133
+ blockquote { border-left-color: #3f3f46; color: #a1a1aa; }
134
+ code, pre { background: #27272a; }
135
+ th { background: #27272a; }
136
+ th, td { border-color: #3f3f46; }
137
+ hr, .sources, footer { border-color: #3f3f46; }
138
+ :target { background: #422006; }
139
+ }
140
+ @media print {
141
+ body { background: #fff; }
142
+ main { max-width: none; padding: 0; }
143
+ a { color: inherit; }
144
+ }
145
+ `.trim();
146
+ //# sourceMappingURL=html-export.js.map