@duckmind/dm-windows-x64 0.62.3 → 0.62.9

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 (93) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +43 -183
  3. package/extensions/dm-context/src/context.js +1 -2
  4. package/extensions/dm-context/src/index.js +1 -2
  5. package/extensions/{greedysearch-dm/bin/cdp.mjs → dm-cua/bin/browser-cdp.mjs} +3 -3
  6. package/extensions/dm-cua/bin/browser-cua.mjs +1 -1
  7. package/extensions/{greedysearch-dm/bin/launch.mjs → dm-cua/bin/browser-launch.mjs} +12 -11
  8. package/extensions/dm-cua/index.js +1 -1
  9. package/extensions/dm-cua/src/browser-cua-lib.mjs +17 -12
  10. package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/constants.mjs +11 -11
  11. package/extensions/{greedysearch-dm/src/utils → dm-cua/src/browser-helpers}/node-runtime.mjs +1 -1
  12. package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/port-pid.mjs +1 -1
  13. package/extensions/dm-ponytail/extensions/ponytail.mjs +186 -0
  14. package/extensions/dm-ponytail/hooks/claude-codex-hooks.json +41 -0
  15. package/extensions/dm-ponytail/hooks/copilot-hooks.json +21 -0
  16. package/extensions/dm-ponytail/hooks/ponytail-activate.js +59 -0
  17. package/extensions/dm-ponytail/hooks/ponytail-config.js +116 -0
  18. package/extensions/dm-ponytail/hooks/ponytail-instructions.js +81 -0
  19. package/extensions/dm-ponytail/hooks/ponytail-mode-tracker.js +96 -0
  20. package/extensions/dm-ponytail/hooks/ponytail-runtime.js +79 -0
  21. package/extensions/dm-ponytail/hooks/ponytail-statusline.ps1 +24 -0
  22. package/extensions/dm-ponytail/hooks/ponytail-statusline.sh +18 -0
  23. package/extensions/dm-ponytail/hooks/ponytail-subagent.js +50 -0
  24. package/extensions/dm-ponytail/hooks/qoder-hooks.json +26 -0
  25. package/extensions/dm-ponytail/package.json +19 -0
  26. package/extensions/dm-ponytail/pi-extension/index.js +186 -0
  27. package/extensions/dm-ponytail/pi-extension/package.json +8 -0
  28. package/extensions/dm-ponytail/skills/ponytail/SKILL.md +120 -0
  29. package/extensions/dm-ponytail/skills/ponytail-audit/SKILL.md +41 -0
  30. package/extensions/dm-ponytail/skills/ponytail-debt/SKILL.md +44 -0
  31. package/extensions/dm-ponytail/skills/ponytail-gain/SKILL.md +50 -0
  32. package/extensions/dm-ponytail/skills/ponytail-help/SKILL.md +71 -0
  33. package/extensions/dm-ponytail/skills/ponytail-review/SKILL.md +57 -0
  34. package/package.json +2 -6
  35. package/extensions/dm-caveman/extensions/caveman.js +0 -300
  36. package/extensions/dm-caveman/package.json +0 -20
  37. package/extensions/dm-goal/package.json +0 -23
  38. package/extensions/dm-goal/src/goal.js +0 -914
  39. package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +0 -40
  40. package/extensions/greedysearch-dm/bin/cdp-headless.mjs +0 -5
  41. package/extensions/greedysearch-dm/bin/cdp-visible.mjs +0 -5
  42. package/extensions/greedysearch-dm/bin/gschrome.mjs +0 -30
  43. package/extensions/greedysearch-dm/bin/kill-visible.mjs +0 -7
  44. package/extensions/greedysearch-dm/bin/launch-visible.mjs +0 -13
  45. package/extensions/greedysearch-dm/bin/mcp.mjs +0 -400
  46. package/extensions/greedysearch-dm/bin/search.mjs +0 -635
  47. package/extensions/greedysearch-dm/bin/visible.mjs +0 -22
  48. package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +0 -344
  49. package/extensions/greedysearch-dm/extractors/chatgpt.mjs +0 -354
  50. package/extensions/greedysearch-dm/extractors/common.mjs +0 -794
  51. package/extensions/greedysearch-dm/extractors/consensus.mjs +0 -527
  52. package/extensions/greedysearch-dm/extractors/consent.mjs +0 -449
  53. package/extensions/greedysearch-dm/extractors/gemini.mjs +0 -291
  54. package/extensions/greedysearch-dm/extractors/google-ai.mjs +0 -86
  55. package/extensions/greedysearch-dm/extractors/logically.mjs +0 -589
  56. package/extensions/greedysearch-dm/extractors/perplexity.mjs +0 -370
  57. package/extensions/greedysearch-dm/extractors/selectors.mjs +0 -32
  58. package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +0 -199
  59. package/extensions/greedysearch-dm/index.js +0 -123
  60. package/extensions/greedysearch-dm/package.json +0 -27
  61. package/extensions/greedysearch-dm/skills/greedy-search/skill.md +0 -18
  62. package/extensions/greedysearch-dm/src/fetcher.mjs +0 -577
  63. package/extensions/greedysearch-dm/src/formatters/results.js +0 -95
  64. package/extensions/greedysearch-dm/src/formatters/sources.js +0 -57
  65. package/extensions/greedysearch-dm/src/formatters/synthesis.js +0 -49
  66. package/extensions/greedysearch-dm/src/github.mjs +0 -228
  67. package/extensions/greedysearch-dm/src/reddit.mjs +0 -154
  68. package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +0 -340
  69. package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +0 -156
  70. package/extensions/greedysearch-dm/src/search/chrome.mjs +0 -510
  71. package/extensions/greedysearch-dm/src/search/defaults.mjs +0 -10
  72. package/extensions/greedysearch-dm/src/search/engines.mjs +0 -79
  73. package/extensions/greedysearch-dm/src/search/fetch-source.mjs +0 -470
  74. package/extensions/greedysearch-dm/src/search/file-sources.mjs +0 -31
  75. package/extensions/greedysearch-dm/src/search/output.mjs +0 -51
  76. package/extensions/greedysearch-dm/src/search/paths.mjs +0 -48
  77. package/extensions/greedysearch-dm/src/search/pdf.mjs +0 -64
  78. package/extensions/greedysearch-dm/src/search/progress.mjs +0 -109
  79. package/extensions/greedysearch-dm/src/search/query.mjs +0 -21
  80. package/extensions/greedysearch-dm/src/search/recovery.mjs +0 -49
  81. package/extensions/greedysearch-dm/src/search/research.mjs +0 -2255
  82. package/extensions/greedysearch-dm/src/search/scale-aware.mjs +0 -61
  83. package/extensions/greedysearch-dm/src/search/simple-research.mjs +0 -418
  84. package/extensions/greedysearch-dm/src/search/sources.mjs +0 -412
  85. package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +0 -127
  86. package/extensions/greedysearch-dm/src/search/synthesis.mjs +0 -202
  87. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +0 -209
  88. package/extensions/greedysearch-dm/src/tools/shared.js +0 -226
  89. package/extensions/greedysearch-dm/src/types.js +0 -0
  90. package/extensions/greedysearch-dm/src/utils/content.mjs +0 -38
  91. package/extensions/greedysearch-dm/src/utils/helpers.js +0 -22
  92. /package/extensions/{greedysearch-dm/src/search → dm-cua/src/browser-helpers}/minimize.mjs +0 -0
  93. /package/extensions/{greedysearch-dm/src/utils → dm-cua/src/browser-helpers}/system-cmds.mjs +0 -0
@@ -1,57 +0,0 @@
1
- import { formatEngineName, humanizeSourceType } from "../utils/helpers.js";
2
- export function sourceUrl(source) {
3
- return String(source.displayUrl || source.canonicalUrl || source.url || "");
4
- }
5
- export function sourceLabel(source) {
6
- return String(source.title || source.domain || sourceUrl(source) || "Untitled source");
7
- }
8
- export function sourceConsensus(source) {
9
- if (typeof source.engineCount === "number")
10
- return source.engineCount;
11
- const engines = Array.isArray(source.engines) ? source.engines : [];
12
- return engines.length;
13
- }
14
- export function getSourceMap(sources) {
15
- return new Map(sources.map((source) => [String(source.id || ""), source]).filter(([id]) => id));
16
- }
17
- export function formatSourceLine(source) {
18
- const id = String(source.id || "?");
19
- const url = sourceUrl(source);
20
- const title = sourceLabel(source);
21
- const domain = String(source.domain || "");
22
- const engines = Array.isArray(source.engines) ? source.engines : [];
23
- const consensus = sourceConsensus(source);
24
- const typeLabel = humanizeSourceType(String(source.sourceType || ""));
25
- const fetch = source.fetch;
26
- const fetchStatus = fetch?.ok ? `fetched ${fetch.status || 200}` : fetch?.attempted ? "fetch failed" : "";
27
- const pieces = [
28
- `${id} - [${title}](${url})`,
29
- domain,
30
- typeLabel,
31
- engines.length ? `cited by ${engines.map(formatEngineName).join(", ")} (${consensus}/3)` : `${consensus}/3`,
32
- fetchStatus
33
- ].filter(Boolean);
34
- return `- ${pieces.join(" - ")}`;
35
- }
36
- export function renderSourceEvidence(lines, source) {
37
- const fetch = source.fetch;
38
- if (!fetch?.attempted)
39
- return;
40
- const snippet = String(fetch.snippet || "").trim();
41
- const lastModified = String(fetch.lastModified || "").trim();
42
- if (snippet)
43
- lines.push(` Evidence: ${snippet}`);
44
- if (lastModified)
45
- lines.push(` Last-Modified: ${lastModified}`);
46
- if (fetch.error)
47
- lines.push(` Fetch error: ${String(fetch.error)}`);
48
- }
49
- export function pickSources(sources, recommendedIds = [], max = 6) {
50
- if (!sources.length)
51
- return [];
52
- const sourceMap = getSourceMap(sources);
53
- const recommended = recommendedIds.map((id) => sourceMap.get(id)).filter((source) => Boolean(source));
54
- if (recommended.length > 0)
55
- return recommended.slice(0, max);
56
- return sources.slice(0, max);
57
- }
@@ -1,49 +0,0 @@
1
- import { formatAgreementLevel } from "../utils/helpers.js";
2
- import { formatSourceLine, pickSources } from "./sources.js";
3
- export function renderSynthesis(lines, synthesis, sources, maxSources = 6) {
4
- if (synthesis.answer) {
5
- lines.push("## Answer");
6
- lines.push(String(synthesis.answer));
7
- lines.push("");
8
- }
9
- const agreement = synthesis.agreement;
10
- const agreementSummary = String(agreement?.summary || "").trim();
11
- const agreementLevel = String(agreement?.level || "").trim();
12
- if (agreementSummary || agreementLevel) {
13
- lines.push("## Consensus");
14
- lines.push(`- ${formatAgreementLevel(agreementLevel)}${agreementSummary ? ` - ${agreementSummary}` : ""}`);
15
- lines.push("");
16
- }
17
- const differences = Array.isArray(synthesis.differences) ? synthesis.differences : [];
18
- if (differences.length > 0) {
19
- lines.push("## Where Engines Differ");
20
- for (const difference of differences)
21
- lines.push(`- ${difference}`);
22
- lines.push("");
23
- }
24
- const caveats = Array.isArray(synthesis.caveats) ? synthesis.caveats : [];
25
- if (caveats.length > 0) {
26
- lines.push("## Caveats");
27
- for (const caveat of caveats)
28
- lines.push(`- ${caveat}`);
29
- lines.push("");
30
- }
31
- const claims = Array.isArray(synthesis.claims) ? synthesis.claims : [];
32
- if (claims.length > 0) {
33
- lines.push("## Key Claims");
34
- for (const claim of claims) {
35
- const sourceIds = Array.isArray(claim.sourceIds) ? claim.sourceIds : [];
36
- const support = String(claim.support || "moderate");
37
- lines.push(`- ${String(claim.claim || "")} [${support}${sourceIds.length ? `; ${sourceIds.join(", ")}` : ""}]`);
38
- }
39
- lines.push("");
40
- }
41
- const recommendedIds = Array.isArray(synthesis.recommendedSources) ? synthesis.recommendedSources : [];
42
- const topSources = pickSources(sources, recommendedIds, maxSources);
43
- if (topSources.length > 0) {
44
- lines.push("## Top Sources");
45
- for (const source of topSources)
46
- lines.push(formatSourceLine(source));
47
- lines.push("");
48
- }
49
- }
@@ -1,228 +0,0 @@
1
- const GITHUB_API = "https://api.github.com";
2
- const DEFAULT_HEADERS = {
3
- "user-agent": "GreedySearch/1.0",
4
- accept: "application/vnd.github+json",
5
- "x-github-api-version": "2022-11-28"
6
- };
7
- export function parseGitHubUrl(url) {
8
- try {
9
- const parsed = new URL(url);
10
- if (!(parsed.hostname === "github.com" || parsed.hostname.endsWith(".github.com"))) {
11
- return null;
12
- }
13
- const parts = parsed.pathname.split("/").filter(Boolean);
14
- if (parts.length < 2) {
15
- return null;
16
- }
17
- const [owner, repo] = parts;
18
- if (parts.length === 2) {
19
- return { owner, repo, type: "root" };
20
- }
21
- if (parts.length >= 4 && (parts[2] === "blob" || parts[2] === "tree")) {
22
- const type = parts[2];
23
- const ref = parts[3];
24
- const path = parts.slice(4).join("/");
25
- return { owner, repo, type, ref, path };
26
- }
27
- return null;
28
- } catch {
29
- return null;
30
- }
31
- }
32
- async function apiGet(path, timeoutMs = 1e4) {
33
- const controller = new AbortController;
34
- const tid = setTimeout(() => controller.abort(), timeoutMs);
35
- try {
36
- const res = await fetch(`${GITHUB_API}${path}`, {
37
- headers: DEFAULT_HEADERS,
38
- signal: controller.signal
39
- });
40
- clearTimeout(tid);
41
- if (!res.ok) {
42
- throw new Error(`GitHub API ${res.status}: ${path}`);
43
- }
44
- return await res.json();
45
- } catch (err) {
46
- clearTimeout(tid);
47
- throw err;
48
- }
49
- }
50
- async function fetchReadme(owner, repo) {
51
- try {
52
- const data = await apiGet(`/repos/${owner}/${repo}/readme`);
53
- if (data.content && data.encoding === "base64") {
54
- return Buffer.from(data.content, "base64").toString("utf8");
55
- }
56
- return "";
57
- } catch {
58
- return "";
59
- }
60
- }
61
- async function fetchTree(owner, repo, ref = "HEAD", subPath = "", defaultBranch) {
62
- try {
63
- let refData;
64
- if (ref === "HEAD") {
65
- if (defaultBranch) {
66
- refData = await apiGet(`/repos/${owner}/${repo}/git/ref/heads/${defaultBranch}`).catch(() => null);
67
- } else {
68
- refData = await Promise.any([
69
- apiGet(`/repos/${owner}/${repo}/git/ref/heads/main`),
70
- apiGet(`/repos/${owner}/${repo}/git/ref/heads/master`)
71
- ]).catch(() => null);
72
- }
73
- } else {
74
- refData = await apiGet(`/repos/${owner}/${repo}/git/ref/heads/${ref}`).catch(() => apiGet(`/repos/${owner}/${repo}/git/ref/heads/master`).catch(() => null));
75
- }
76
- if (!refData?.object?.sha) {
77
- return [];
78
- }
79
- const commit = await apiGet(`/repos/${owner}/${repo}/git/commits/${refData.object.sha}`);
80
- const treeSha = commit.tree.sha;
81
- const treeData = await apiGet(`/repos/${owner}/${repo}/git/trees/${treeSha}`);
82
- let items = treeData.tree || [];
83
- if (subPath) {
84
- items = items.filter((item) => item.path.startsWith(subPath));
85
- }
86
- return items.slice(0, 50).map((item) => ({
87
- path: item.path,
88
- type: item.type === "tree" ? "dir" : "file",
89
- size: item.size
90
- }));
91
- } catch {
92
- return [];
93
- }
94
- }
95
- async function fetchRawFile(owner, repo, ref, filePath, timeoutMs = 1e4, defaultBranch) {
96
- const fetchOne = async (url) => {
97
- const controller = new AbortController;
98
- const tid = setTimeout(() => controller.abort(), timeoutMs);
99
- try {
100
- const res = await fetch(url, {
101
- headers: { "user-agent": DEFAULT_HEADERS["user-agent"] },
102
- signal: controller.signal
103
- });
104
- clearTimeout(tid);
105
- if (res.ok) {
106
- return await res.text();
107
- }
108
- throw new Error("not ok");
109
- } catch {
110
- clearTimeout(tid);
111
- throw new Error("failed");
112
- }
113
- };
114
- if (!ref || ref === "HEAD") {
115
- if (defaultBranch) {
116
- try {
117
- return await fetchOne(`https://raw.githubusercontent.com/${owner}/${repo}/${defaultBranch}/${filePath}`);
118
- } catch {
119
- return null;
120
- }
121
- }
122
- try {
123
- return await Promise.any([
124
- fetchOne(`https://raw.githubusercontent.com/${owner}/${repo}/main/${filePath}`),
125
- fetchOne(`https://raw.githubusercontent.com/${owner}/${repo}/master/${filePath}`)
126
- ]);
127
- } catch {
128
- return null;
129
- }
130
- }
131
- const urls = [
132
- `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`,
133
- `https://raw.githubusercontent.com/${owner}/${repo}/master/${filePath}`
134
- ];
135
- for (const url of urls) {
136
- try {
137
- return await fetchOne(url);
138
- } catch {}
139
- }
140
- return null;
141
- }
142
- export async function fetchGitHubContent(url) {
143
- const parsed = parseGitHubUrl(url);
144
- if (!parsed) {
145
- return { ok: false, error: "Not a valid GitHub URL" };
146
- }
147
- const { owner, repo, type, ref, path } = parsed;
148
- try {
149
- if (type === "root" || type === "tree" && !path) {
150
- const info = await apiGet(`/repos/${owner}/${repo}`);
151
- const [readme, tree] = await Promise.allSettled([
152
- fetchReadme(owner, repo),
153
- fetchTree(owner, repo, ref || "HEAD", "", info.default_branch)
154
- ]);
155
- const readmeText = readme.status === "fulfilled" ? readme.value : "";
156
- const treeItems = tree.status === "fulfilled" ? tree.value : [];
157
- const description = info?.description ? `
158
-
159
- > ${info.description}` : "";
160
- const stars = info?.stargazers_count == null ? "" : ` ⭐ ${info.stargazers_count}`;
161
- const language = info?.language ? ` · ${info.language}` : "";
162
- let content = `# ${owner}/${repo}${stars}${language}${description}
163
-
164
- `;
165
- if (readmeText) {
166
- content += readmeText.slice(0, 6000);
167
- } else {
168
- content += `[No README found]
169
-
170
- Files:
171
- ${treeItems.map((t) => ` ${t.type === "dir" ? "\uD83D\uDCC1" : "\uD83D\uDCC4"} ${t.path}`).join(`
172
- `)}`;
173
- }
174
- return {
175
- ok: true,
176
- title: `${owner}/${repo}`,
177
- content,
178
- tree: treeItems.slice(0, 30)
179
- };
180
- }
181
- if (type === "blob" && path) {
182
- let defaultBranch;
183
- if (!ref || ref === "HEAD") {
184
- try {
185
- const repoInfo = await apiGet(`/repos/${owner}/${repo}`);
186
- defaultBranch = repoInfo.default_branch;
187
- } catch {
188
- defaultBranch = undefined;
189
- }
190
- }
191
- const content = await fetchRawFile(owner, repo, ref, path, 1e4, defaultBranch);
192
- if (content === null) {
193
- return { ok: false, error: `File not found: ${path}` };
194
- }
195
- return {
196
- ok: true,
197
- title: `${owner}/${repo}: ${path}`,
198
- content
199
- };
200
- }
201
- if (type === "tree" && path) {
202
- let defaultBranch;
203
- if (!ref || ref === "HEAD") {
204
- try {
205
- const repoInfo = await apiGet(`/repos/${owner}/${repo}`);
206
- defaultBranch = repoInfo.default_branch;
207
- } catch {
208
- defaultBranch = undefined;
209
- }
210
- }
211
- const treeItems = await fetchTree(owner, repo, ref || "HEAD", path, defaultBranch);
212
- const listing = treeItems.map((t) => ` ${t.type === "dir" ? "\uD83D\uDCC1" : "\uD83D\uDCC4"} ${t.path}`).join(`
213
- `);
214
- return {
215
- ok: true,
216
- title: `${owner}/${repo}/${path}`,
217
- content: `[Directory: ${path}]
218
-
219
- Files:
220
- ${listing}`,
221
- tree: treeItems
222
- };
223
- }
224
- return { ok: false, error: "Unsupported GitHub URL type" };
225
- } catch (err) {
226
- return { ok: false, error: err.message };
227
- }
228
- }
@@ -1,154 +0,0 @@
1
- const REDDIT_HEADERS = {
2
- "user-agent": "GreedySearch/1.0 (Research Bot)",
3
- accept: "application/json"
4
- };
5
- export function parseRedditUrl(url) {
6
- try {
7
- const parsed = new URL(url);
8
- const hostname = parsed.hostname.toLowerCase();
9
- if (!(hostname === "reddit.com" || hostname.endsWith(".reddit.com"))) {
10
- return null;
11
- }
12
- const pathname = parsed.pathname;
13
- if (pathname.match(/^\/(u|user)\/[^/]+\/?$/i)) {
14
- return { type: "user", cleanUrl: normalizeRedditUrl(url) };
15
- }
16
- if (pathname.match(/^\/r\/[^/]+\/comments\/[^/]+/i)) {
17
- return { type: "post", cleanUrl: normalizeRedditUrl(url) };
18
- }
19
- return null;
20
- } catch {
21
- return null;
22
- }
23
- }
24
- function normalizeRedditUrl(url) {
25
- try {
26
- const parsed = new URL(url);
27
- return `${parsed.protocol}//${parsed.hostname}${parsed.pathname}`;
28
- } catch {
29
- return url;
30
- }
31
- }
32
- export async function fetchRedditContent(url, maxChars = 8000) {
33
- const start = Date.now();
34
- try {
35
- const jsonUrl = url.replace(/\/+$/, "") + ".json";
36
- const controller = new AbortController;
37
- const timeoutId = setTimeout(() => controller.abort(), 15000);
38
- const response = await fetch(jsonUrl, {
39
- headers: REDDIT_HEADERS,
40
- signal: controller.signal
41
- });
42
- clearTimeout(timeoutId);
43
- if (!response.ok) {
44
- throw new Error(`Reddit API ${response.status}`);
45
- }
46
- const data = await response.json();
47
- if (!Array.isArray(data) || data.length < 1) {
48
- throw new Error("Invalid Reddit API response structure");
49
- }
50
- const postListing = data[0];
51
- const commentsListing = data[1];
52
- const post = postListing?.data?.children?.[0]?.data;
53
- if (!post) {
54
- throw new Error("No post data in Reddit response");
55
- }
56
- const markdown = formatRedditPost(post, commentsListing, maxChars);
57
- return {
58
- ok: true,
59
- url,
60
- finalUrl: url,
61
- status: 200,
62
- contentType: "text/markdown",
63
- lastModified: "",
64
- title: post.title || "Reddit Post",
65
- byline: `u/${post.author}`,
66
- siteName: `r/${post.subreddit}`,
67
- lang: "en",
68
- publishedTime: new Date(post.created_utc * 1000).toISOString(),
69
- excerpt: post.selftext?.slice(0, 300).replace(/\n/g, " ") || "",
70
- markdown,
71
- contentLength: markdown.length,
72
- needsBrowser: false,
73
- duration: Date.now() - start
74
- };
75
- } catch (error) {
76
- return {
77
- ok: false,
78
- url,
79
- finalUrl: url,
80
- status: 0,
81
- error: `Reddit fetch failed: ${error.message}`,
82
- needsBrowser: false,
83
- duration: Date.now() - start
84
- };
85
- }
86
- }
87
- function formatRedditPost(post, commentsListing, maxChars) {
88
- let md = "";
89
- md += `# ${post.title}
90
-
91
- `;
92
- md += `**Subreddit:** r/${post.subreddit} | **Author:** u/${post.author} | **Score:** ${post.score}
93
-
94
- `;
95
- if (post.selftext) {
96
- md += post.selftext;
97
- md += `
98
-
99
- `;
100
- } else if (post.url) {
101
- try {
102
- const postUrlHost = new URL(post.url).hostname.toLowerCase();
103
- if (postUrlHost !== "reddit.com" && !postUrlHost.endsWith(".reddit.com")) {
104
- md += `**Link:** ${post.url}
105
-
106
- `;
107
- }
108
- } catch {
109
- md += `**Link:** ${post.url}
110
-
111
- `;
112
- }
113
- }
114
- if (commentsListing?.data?.children?.length > 0) {
115
- md += `---
116
-
117
- ## Comments
118
-
119
- `;
120
- const comments = commentsListing.data.children.filter((c) => c.kind === "t1").slice(0, 10);
121
- for (const comment of comments) {
122
- md += formatComment(comment.data, 0);
123
- md += `
124
- `;
125
- }
126
- }
127
- if (md.length > maxChars) {
128
- md = md.slice(0, maxChars).trim() + `
129
-
130
- ... (truncated)`;
131
- }
132
- return md;
133
- }
134
- function formatComment(comment, depth) {
135
- if (!comment || comment.body === "[deleted]" || comment.body === "[removed]") {
136
- return "";
137
- }
138
- const indent = "> ".repeat(depth);
139
- let md = "";
140
- md += `${indent}**u/${comment.author}** (${comment.score} pts)
141
- `;
142
- md += `${indent}${comment.body.replaceAll(`
143
- `, `
144
- ` + indent)}
145
- `;
146
- if (depth < 3 && comment.replies?.data?.children) {
147
- const replies = comment.replies.data.children.filter((r) => r.kind === "t1");
148
- for (const reply of replies.slice(0, 5)) {
149
- md += `
150
- ` + formatComment(reply.data, depth + 1);
151
- }
152
- }
153
- return md;
154
- }