@duckmind/dm-windows-x64 0.63.0 → 0.63.6
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +200 -4
- package/extensions/dm-context/package.json +1 -1
- package/extensions/dm-context/skills/context-management/SKILL.md +173 -223
- package/extensions/dm-context/skills/context-management/references/development-and-troubleshooting.md +75 -0
- package/extensions/dm-context/skills/context-management/references/interleaved-async-work.md +143 -0
- package/extensions/dm-context/skills/context-management/references/planning-and-execution.md +81 -0
- package/extensions/dm-context/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
- package/extensions/dm-context/skills/context-management/references/retry-branch-and-pivot.md +80 -0
- package/extensions/dm-context/skills/context-management/references/search-research-and-reading.md +103 -0
- package/extensions/dm-context/skills/context-management/references/task-switching-and-cleanup.md +73 -0
- package/extensions/dm-context/src/context.js +3 -2
- package/extensions/dm-context/src/index.js +168 -84
- package/extensions/dm-skills-manager/THIRD_PARTY_NOTICES.md +27 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/components.js +265 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/constants.js +31 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/creation-fallback.js +20 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/creation.js +145 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/dialog.js +738 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/dm-ai-compat.js +15 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/format.js +125 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/glyphs.js +142 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/layout.js +93 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/paths.js +76 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/registry.js +95 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/settings.js +93 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/startup.js +32 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/toggle.js +54 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/types.js +14 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/ui.js +121 -0
- package/extensions/dm-skills-manager/extensions/skills-manager.js +111 -0
- package/extensions/dm-skills-manager/package.json +121 -0
- package/extensions/dm-web-access/SECURITY.md +5 -0
- package/extensions/dm-web-access/activity.js +65 -0
- package/extensions/dm-web-access/anysearch.js +158 -0
- package/extensions/dm-web-access/auth-fetch.js +131 -0
- package/extensions/dm-web-access/bocha.js +214 -0
- package/extensions/dm-web-access/brave.js +196 -0
- package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
- package/extensions/dm-web-access/brightdata.js +334 -0
- package/extensions/dm-web-access/chrome-cookies.js +627 -0
- package/extensions/dm-web-access/content-find.js +114 -0
- package/extensions/dm-web-access/credential-source.js +150 -0
- package/extensions/dm-web-access/curator-page.js +3559 -0
- package/extensions/dm-web-access/curator-server.js +691 -0
- package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
- package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
- package/extensions/dm-web-access/declared-web-links.js +167 -0
- package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
- package/extensions/dm-web-access/duckduckgo.js +118 -0
- package/extensions/dm-web-access/exa.js +401 -0
- package/extensions/dm-web-access/extract.js +1220 -0
- package/extensions/dm-web-access/feature-config.js +24 -0
- package/extensions/dm-web-access/fetch-params.js +81 -0
- package/extensions/dm-web-access/firecrawl.js +378 -0
- package/extensions/dm-web-access/gemini-adc.js +241 -0
- package/extensions/dm-web-access/gemini-api.js +258 -0
- package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
- package/extensions/dm-web-access/gemini-search.js +889 -0
- package/extensions/dm-web-access/gemini-url-context.js +97 -0
- package/extensions/dm-web-access/gemini-web-config.js +84 -0
- package/extensions/dm-web-access/gemini-web.js +351 -0
- package/extensions/dm-web-access/github-api.js +166 -0
- package/extensions/dm-web-access/github-extract.js +991 -0
- package/extensions/dm-web-access/github-issue-pr.js +750 -0
- package/extensions/dm-web-access/index.js +3117 -0
- package/extensions/dm-web-access/jina-search.js +242 -0
- package/extensions/dm-web-access/kagi.js +255 -0
- package/extensions/dm-web-access/kimi-search.js +214 -0
- package/extensions/dm-web-access/ollama.js +209 -0
- package/extensions/dm-web-access/openai-search.js +510 -0
- package/extensions/dm-web-access/package.json +34 -0
- package/extensions/dm-web-access/page-query.js +124 -0
- package/extensions/dm-web-access/parallel-mcp.js +223 -0
- package/extensions/dm-web-access/parallel.js +345 -0
- package/extensions/dm-web-access/pdf-extract.js +257 -0
- package/extensions/dm-web-access/perplexity.js +151 -0
- package/extensions/dm-web-access/querit.js +327 -0
- package/extensions/dm-web-access/query-rewrite.js +40 -0
- package/extensions/dm-web-access/render-search-error.js +80 -0
- package/extensions/dm-web-access/rsc-extract.js +347 -0
- package/extensions/dm-web-access/search1api.js +245 -0
- package/extensions/dm-web-access/searchinfinity.js +221 -0
- package/extensions/dm-web-access/searxng.js +223 -0
- package/extensions/dm-web-access/serpbase.js +205 -0
- package/extensions/dm-web-access/serpdive.js +238 -0
- package/extensions/dm-web-access/serper.js +200 -0
- package/extensions/dm-web-access/source-check.js +198 -0
- package/extensions/dm-web-access/ssrf-protection.js +436 -0
- package/extensions/dm-web-access/storage.js +451 -0
- package/extensions/dm-web-access/summary-model-scope.js +83 -0
- package/extensions/dm-web-access/summary-review.js +364 -0
- package/extensions/dm-web-access/tavily.js +199 -0
- package/extensions/dm-web-access/tinyfish.js +325 -0
- package/extensions/dm-web-access/utils.js +476 -0
- package/extensions/dm-web-access/valyu.js +189 -0
- package/extensions/dm-web-access/video-extract.js +336 -0
- package/extensions/dm-web-access/xai-search.js +285 -0
- package/extensions/dm-web-access/xcrawl.js +221 -0
- package/extensions/dm-web-access/youtube-extract.js +279 -0
- package/package.json +9 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { join, basename } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { CredentialResolutionError } from "./credential-source.js";
|
|
6
|
+
import {
|
|
7
|
+
DATALAB_MODE_VALUES,
|
|
8
|
+
DEFAULT_DATALAB_TIMEOUT_MS,
|
|
9
|
+
isDatalabApiAvailable,
|
|
10
|
+
normalizeDatalabMode,
|
|
11
|
+
extractPDFViaDatalab
|
|
12
|
+
} from "./datalab-pdf-extract.js";
|
|
13
|
+
import { isGeminiApiAvailable } from "./gemini-api.js";
|
|
14
|
+
import { extractPDFViaGemini } from "./gemini-pdf-extract.js";
|
|
15
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
16
|
+
export const PDF_PROVIDER_VALUES = new Set([
|
|
17
|
+
"auto",
|
|
18
|
+
"gemini",
|
|
19
|
+
"datalab",
|
|
20
|
+
"unpdf"
|
|
21
|
+
]);
|
|
22
|
+
export const DEFAULT_PDF_MAX_SIZE_MB = 20;
|
|
23
|
+
export const MAX_PDF_MAX_SIZE_MB = 50;
|
|
24
|
+
export const MAX_DATALAB_TIMEOUT_MS = 300000;
|
|
25
|
+
const DEFAULT_MAX_PAGES = 100;
|
|
26
|
+
const DEFAULT_OUTPUT_DIR = join(tmpdir(), "pi-web-pdf");
|
|
27
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
28
|
+
const PAGE_MARKER_PATTERN = /^<!-- Page (\d+) -->$/gm;
|
|
29
|
+
export function loadPDFConfig() {
|
|
30
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
31
|
+
return {
|
|
32
|
+
enabled: true,
|
|
33
|
+
maxSizeMB: DEFAULT_PDF_MAX_SIZE_MB,
|
|
34
|
+
maxPages: DEFAULT_MAX_PAGES,
|
|
35
|
+
provider: "auto",
|
|
36
|
+
datalabMode: normalizeDatalabMode(process.env.DATALAB_MODE),
|
|
37
|
+
datalabTimeoutMs: DEFAULT_DATALAB_TIMEOUT_MS
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const rawText = readFileSync(CONFIG_PATH, "utf-8");
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = JSON.parse(rawText);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
const root = raw && typeof raw === "object" ? raw : {};
|
|
49
|
+
const pdf = root.pdf && typeof root.pdf === "object" ? root.pdf : {};
|
|
50
|
+
const enabled = pdf.enabled !== false;
|
|
51
|
+
const configured = pdf.maxSizeMB;
|
|
52
|
+
const normalized = typeof configured === "number" && Number.isFinite(configured) && configured > 0 ? Math.min(configured, MAX_PDF_MAX_SIZE_MB) : DEFAULT_PDF_MAX_SIZE_MB;
|
|
53
|
+
const configuredMaxPages = pdf.maxPages;
|
|
54
|
+
const maxPages = typeof configuredMaxPages === "number" && Number.isFinite(configuredMaxPages) && configuredMaxPages > 0 ? Math.max(1, Math.floor(configuredMaxPages)) : DEFAULT_MAX_PAGES;
|
|
55
|
+
const provider = typeof pdf.provider === "string" && PDF_PROVIDER_VALUES.has(pdf.provider) ? pdf.provider : "auto";
|
|
56
|
+
const datalabMode = typeof pdf.datalabMode === "string" && DATALAB_MODE_VALUES.has(pdf.datalabMode) ? pdf.datalabMode : normalizeDatalabMode(process.env.DATALAB_MODE);
|
|
57
|
+
const configuredTimeout = pdf.datalabTimeoutMs;
|
|
58
|
+
const datalabTimeoutMs = typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? Math.min(configuredTimeout, MAX_DATALAB_TIMEOUT_MS) : DEFAULT_DATALAB_TIMEOUT_MS;
|
|
59
|
+
return {
|
|
60
|
+
enabled,
|
|
61
|
+
maxSizeMB: normalized,
|
|
62
|
+
maxPages,
|
|
63
|
+
provider,
|
|
64
|
+
datalabMode,
|
|
65
|
+
datalabTimeoutMs
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function getUnpdf() {
|
|
69
|
+
if (typeof Promise.try !== "function") {
|
|
70
|
+
const { default: promiseTry } = await import("promise.try");
|
|
71
|
+
promiseTry.shim();
|
|
72
|
+
}
|
|
73
|
+
const [unpdf, pdfjs] = await Promise.all([
|
|
74
|
+
import("unpdf"),
|
|
75
|
+
import("unpdf/pdfjs")
|
|
76
|
+
]);
|
|
77
|
+
const { VerbosityLevel } = pdfjs;
|
|
78
|
+
return { getDocumentProxy: unpdf.getDocumentProxy, VerbosityLevel };
|
|
79
|
+
}
|
|
80
|
+
export async function extractPDFToMarkdown(buffer, url, options = {}) {
|
|
81
|
+
const {
|
|
82
|
+
maxPages,
|
|
83
|
+
outputDir = DEFAULT_OUTPUT_DIR,
|
|
84
|
+
filename,
|
|
85
|
+
signal,
|
|
86
|
+
geminiTimeoutMs
|
|
87
|
+
} = options;
|
|
88
|
+
const pdfConfig = loadPDFConfig();
|
|
89
|
+
const safeMaxPages = maxPages === undefined ? pdfConfig.maxPages : Number.isFinite(maxPages) ? Math.max(1, Math.floor(maxPages)) : DEFAULT_MAX_PAGES;
|
|
90
|
+
const urlTitle = extractTitleFromURL(url);
|
|
91
|
+
const provider = pdfConfig.provider;
|
|
92
|
+
if (provider === "auto" || provider === "datalab") {
|
|
93
|
+
try {
|
|
94
|
+
if (isDatalabApiAvailable()) {
|
|
95
|
+
const result = await extractPDFViaDatalab(buffer, {
|
|
96
|
+
maxPages: safeMaxPages,
|
|
97
|
+
title: urlTitle,
|
|
98
|
+
mode: pdfConfig.datalabMode,
|
|
99
|
+
timeoutMs: pdfConfig.datalabTimeoutMs,
|
|
100
|
+
...signal ? { signal } : {}
|
|
101
|
+
});
|
|
102
|
+
return writeMarkdownResult({
|
|
103
|
+
markdownBody: result.markdown,
|
|
104
|
+
title: urlTitle,
|
|
105
|
+
pages: result.pages,
|
|
106
|
+
outputDir,
|
|
107
|
+
filename,
|
|
108
|
+
url
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (shouldRethrowExtractionError(err, signal))
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (provider === "auto" || provider === "gemini") {
|
|
117
|
+
try {
|
|
118
|
+
if (isGeminiApiAvailable()) {
|
|
119
|
+
const markdownBody = await extractPDFViaGemini(buffer, {
|
|
120
|
+
maxPages: safeMaxPages,
|
|
121
|
+
title: urlTitle,
|
|
122
|
+
...signal ? { signal } : {},
|
|
123
|
+
...geminiTimeoutMs !== undefined ? { timeoutMs: geminiTimeoutMs } : {}
|
|
124
|
+
});
|
|
125
|
+
return writeMarkdownResult({
|
|
126
|
+
markdownBody,
|
|
127
|
+
title: urlTitle,
|
|
128
|
+
pages: countPageMarkers(markdownBody),
|
|
129
|
+
outputDir,
|
|
130
|
+
filename,
|
|
131
|
+
url
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (shouldRethrowExtractionError(err, signal))
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const { getDocumentProxy, VerbosityLevel } = await getUnpdf();
|
|
140
|
+
const pdf = await getDocumentProxy(new Uint8Array(buffer), {
|
|
141
|
+
verbosity: VerbosityLevel.ERRORS
|
|
142
|
+
});
|
|
143
|
+
const metadata = await pdf.getMetadata();
|
|
144
|
+
const metadataInfo = metadata.info && typeof metadata.info === "object" ? metadata.info : null;
|
|
145
|
+
const metaTitle = typeof metadataInfo?.Title === "string" ? metadataInfo.Title : undefined;
|
|
146
|
+
const metaAuthor = typeof metadataInfo?.Author === "string" ? metadataInfo.Author : undefined;
|
|
147
|
+
const title = metaTitle?.trim() || urlTitle;
|
|
148
|
+
const pagesToExtract = Math.min(pdf.numPages, safeMaxPages);
|
|
149
|
+
const truncated = pdf.numPages > safeMaxPages;
|
|
150
|
+
const pages = [];
|
|
151
|
+
for (let i = 1;i <= pagesToExtract; i++) {
|
|
152
|
+
const page = await pdf.getPage(i);
|
|
153
|
+
const textContent = await page.getTextContent();
|
|
154
|
+
const pageText = textContent.items.map((item) => {
|
|
155
|
+
const textItem = item;
|
|
156
|
+
return textItem.str || "";
|
|
157
|
+
}).join(" ").replace(/\s+/g, " ").trim();
|
|
158
|
+
if (pageText) {
|
|
159
|
+
pages.push({ pageNum: i, text: pageText });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const bodyLines = [];
|
|
163
|
+
for (let i = 0;i < pages.length; i++) {
|
|
164
|
+
if (i > 0) {
|
|
165
|
+
bodyLines.push("");
|
|
166
|
+
bodyLines.push(`<!-- Page ${pages[i].pageNum} -->`);
|
|
167
|
+
bodyLines.push("");
|
|
168
|
+
}
|
|
169
|
+
bodyLines.push(pages[i].text);
|
|
170
|
+
}
|
|
171
|
+
return writeMarkdownResult({
|
|
172
|
+
markdownBody: bodyLines.join(`
|
|
173
|
+
`),
|
|
174
|
+
title,
|
|
175
|
+
pages: pdf.numPages,
|
|
176
|
+
outputDir,
|
|
177
|
+
filename,
|
|
178
|
+
url,
|
|
179
|
+
metaAuthor,
|
|
180
|
+
truncated,
|
|
181
|
+
pagesToExtract
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async function writeMarkdownResult(options) {
|
|
185
|
+
const lines = [];
|
|
186
|
+
lines.push(`# ${options.title}`);
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push(`> Source: ${options.url}`);
|
|
189
|
+
lines.push(`> Pages: ${options.pages}${options.truncated ? ` (extracted first ${options.pagesToExtract})` : ""}`);
|
|
190
|
+
if (options.metaAuthor)
|
|
191
|
+
lines.push(`> Author: ${options.metaAuthor}`);
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push("---");
|
|
194
|
+
lines.push("");
|
|
195
|
+
if (options.markdownBody)
|
|
196
|
+
lines.push(options.markdownBody);
|
|
197
|
+
if (options.truncated) {
|
|
198
|
+
lines.push("");
|
|
199
|
+
lines.push("---");
|
|
200
|
+
lines.push("");
|
|
201
|
+
lines.push(`*[Truncated: Only first ${options.pagesToExtract} of ${options.pages} pages extracted]*`);
|
|
202
|
+
}
|
|
203
|
+
const content = lines.join(`
|
|
204
|
+
`);
|
|
205
|
+
const outputFilename = options.filename || sanitizeFilename(options.title) + ".md";
|
|
206
|
+
const outputPath = join(options.outputDir, outputFilename);
|
|
207
|
+
await mkdir(options.outputDir, { recursive: true });
|
|
208
|
+
await writeFile(outputPath, content, "utf-8");
|
|
209
|
+
return {
|
|
210
|
+
title: options.title,
|
|
211
|
+
pages: options.pages,
|
|
212
|
+
chars: content.length,
|
|
213
|
+
outputPath
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function countPageMarkers(markdown) {
|
|
217
|
+
return [...markdown.matchAll(PAGE_MARKER_PATTERN)].length;
|
|
218
|
+
}
|
|
219
|
+
function shouldRethrowExtractionError(err, signal) {
|
|
220
|
+
if (signal?.aborted)
|
|
221
|
+
return true;
|
|
222
|
+
if (err instanceof CredentialResolutionError)
|
|
223
|
+
return true;
|
|
224
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
+
return message.startsWith("Failed to parse ");
|
|
226
|
+
}
|
|
227
|
+
function extractTitleFromURL(url) {
|
|
228
|
+
try {
|
|
229
|
+
const urlObj = new URL(url);
|
|
230
|
+
const pathname = urlObj.pathname;
|
|
231
|
+
let filename = basename(pathname, ".pdf");
|
|
232
|
+
if (urlObj.hostname.includes("arxiv.org")) {
|
|
233
|
+
const match = pathname.match(/\/(?:pdf|abs)\/(\d+\.\d+)/);
|
|
234
|
+
if (match) {
|
|
235
|
+
filename = `arxiv-${match[1]}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
filename = filename.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
239
|
+
return filename || "document";
|
|
240
|
+
} catch {
|
|
241
|
+
return "document";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function sanitizeFilename(name) {
|
|
245
|
+
return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 100).replace(/^-|-$/g, "") || "document";
|
|
246
|
+
}
|
|
247
|
+
export function isPDF(url, contentType) {
|
|
248
|
+
if (contentType?.includes("application/pdf")) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const urlObj = new URL(url);
|
|
253
|
+
return urlObj.pathname.toLowerCase().endsWith(".pdf");
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { activityMonitor } from "./activity.js";
|
|
3
|
+
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.js";
|
|
4
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
5
|
+
const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
|
|
6
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
7
|
+
const RATE_LIMIT = {
|
|
8
|
+
maxRequests: 10,
|
|
9
|
+
windowMs: 60 * 1000
|
|
10
|
+
};
|
|
11
|
+
const requestTimestamps = [];
|
|
12
|
+
let cachedConfig = null;
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
if (cachedConfig)
|
|
15
|
+
return cachedConfig;
|
|
16
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
17
|
+
cachedConfig = {};
|
|
18
|
+
return cachedConfig;
|
|
19
|
+
}
|
|
20
|
+
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
21
|
+
try {
|
|
22
|
+
cachedConfig = JSON.parse(content);
|
|
23
|
+
return cachedConfig;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function getApiKey(signal) {
|
|
30
|
+
const key = await resolveCredential({
|
|
31
|
+
provider: "Perplexity",
|
|
32
|
+
configuredValue: loadConfig().perplexityApiKey,
|
|
33
|
+
environmentValue: process.env.PERPLEXITY_API_KEY,
|
|
34
|
+
signal
|
|
35
|
+
});
|
|
36
|
+
if (!key) {
|
|
37
|
+
throw new Error(`Perplexity API key not found. Either:
|
|
38
|
+
` + ` 1. Create ${CONFIG_PATH} with { "perplexityApiKey": "your-key" }
|
|
39
|
+
` + ` 2. Set PERPLEXITY_API_KEY environment variable
|
|
40
|
+
` + "Get a key at https://perplexity.ai/settings/api");
|
|
41
|
+
}
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
function checkRateLimit() {
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const windowStart = now - RATE_LIMIT.windowMs;
|
|
47
|
+
while (requestTimestamps.length > 0 && requestTimestamps[0] < windowStart) {
|
|
48
|
+
requestTimestamps.shift();
|
|
49
|
+
}
|
|
50
|
+
if (requestTimestamps.length >= RATE_LIMIT.maxRequests) {
|
|
51
|
+
const waitMs = requestTimestamps[0] + RATE_LIMIT.windowMs - now;
|
|
52
|
+
throw new Error(`Rate limited. Try again in ${Math.ceil(waitMs / 1000)}s`);
|
|
53
|
+
}
|
|
54
|
+
requestTimestamps.push(now);
|
|
55
|
+
}
|
|
56
|
+
function validateDomainFilter(domains) {
|
|
57
|
+
return domains.filter((d) => {
|
|
58
|
+
const domain = d.startsWith("-") ? d.slice(1) : d;
|
|
59
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9-_.]*\.[a-zA-Z]{2,}$/.test(domain);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function isPerplexityAvailable() {
|
|
63
|
+
return hasCredentialSource({
|
|
64
|
+
provider: "Perplexity",
|
|
65
|
+
configuredValue: loadConfig().perplexityApiKey,
|
|
66
|
+
environmentValue: process.env.PERPLEXITY_API_KEY
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function searchWithPerplexity(query, options = {}) {
|
|
70
|
+
checkRateLimit();
|
|
71
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
72
|
+
activityMonitor.updateRateLimit({
|
|
73
|
+
used: requestTimestamps.length,
|
|
74
|
+
max: RATE_LIMIT.maxRequests,
|
|
75
|
+
oldestTimestamp: requestTimestamps[0] ?? null,
|
|
76
|
+
windowMs: RATE_LIMIT.windowMs
|
|
77
|
+
});
|
|
78
|
+
const apiKey = await getApiKey(options.signal);
|
|
79
|
+
const numResults = typeof options.numResults === "number" && Number.isFinite(options.numResults) ? Math.max(1, Math.min(Math.floor(options.numResults), 20)) : 5;
|
|
80
|
+
const requestBody = {
|
|
81
|
+
model: "sonar",
|
|
82
|
+
messages: [{ role: "user", content: query }],
|
|
83
|
+
max_tokens: 1024,
|
|
84
|
+
return_related_questions: false
|
|
85
|
+
};
|
|
86
|
+
if (options.recencyFilter) {
|
|
87
|
+
requestBody.search_recency_filter = options.recencyFilter;
|
|
88
|
+
}
|
|
89
|
+
if (options.domainFilter && options.domainFilter.length > 0) {
|
|
90
|
+
const validated = validateDomainFilter(options.domainFilter);
|
|
91
|
+
if (validated.length > 0) {
|
|
92
|
+
requestBody.search_domain_filter = validated;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let response;
|
|
96
|
+
try {
|
|
97
|
+
response = await fetch(PERPLEXITY_API_URL, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: `Bearer ${apiKey}`,
|
|
101
|
+
"Content-Type": "application/json"
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(requestBody),
|
|
104
|
+
...options.signal ? { signal: options.signal } : {}
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
108
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
109
|
+
if (redactedMessage.toLowerCase().includes("abort")) {
|
|
110
|
+
activityMonitor.logComplete(activityId, 0);
|
|
111
|
+
} else {
|
|
112
|
+
activityMonitor.logError(activityId, redactedMessage);
|
|
113
|
+
}
|
|
114
|
+
if (redactedMessage === message)
|
|
115
|
+
throw err;
|
|
116
|
+
const redactedError = new Error(redactedMessage);
|
|
117
|
+
if (err instanceof Error)
|
|
118
|
+
redactedError.name = err.name;
|
|
119
|
+
throw redactedError;
|
|
120
|
+
}
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
123
|
+
const errorText = redactCredential(await response.text(), apiKey);
|
|
124
|
+
throw new Error(`Perplexity API error ${response.status}: ${errorText}`);
|
|
125
|
+
}
|
|
126
|
+
let data;
|
|
127
|
+
try {
|
|
128
|
+
data = await response.json();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
131
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
+
throw new Error(`Perplexity API returned invalid JSON: ${message}`);
|
|
133
|
+
}
|
|
134
|
+
const answer = data.choices?.[0]?.message?.content || "";
|
|
135
|
+
const citations = Array.isArray(data.citations) ? data.citations : [];
|
|
136
|
+
const results = [];
|
|
137
|
+
for (let i = 0;i < Math.min(citations.length, numResults); i++) {
|
|
138
|
+
const citation = citations[i];
|
|
139
|
+
if (typeof citation === "string") {
|
|
140
|
+
results.push({ title: `Source ${i + 1}`, url: citation, snippet: "" });
|
|
141
|
+
} else if (citation && typeof citation === "object" && typeof citation.url === "string") {
|
|
142
|
+
results.push({
|
|
143
|
+
title: citation.title || `Source ${i + 1}`,
|
|
144
|
+
url: citation.url,
|
|
145
|
+
snippet: ""
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
150
|
+
return { answer, results };
|
|
151
|
+
}
|