@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,214 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { activityMonitor } from "./activity.js";
|
|
3
|
+
import { redactCredential } from "./credential-source.js";
|
|
4
|
+
const KIMI_SEARCH_URL = "https://api.kimi.com/coding/v1/search";
|
|
5
|
+
const KIMI_PROVIDERS = ["kimi-coding", "kimi-code"];
|
|
6
|
+
const SEARCH_TIMEOUT_MS = 30000;
|
|
7
|
+
function buildRequestHeaders(auth) {
|
|
8
|
+
const headers = new Headers;
|
|
9
|
+
for (const [name, value] of Object.entries(auth.headers)) {
|
|
10
|
+
if (value !== null)
|
|
11
|
+
headers.set(name, value);
|
|
12
|
+
}
|
|
13
|
+
headers.set("Authorization", `Bearer ${auth.apiKey}`);
|
|
14
|
+
headers.set("Content-Type", "application/json");
|
|
15
|
+
headers.set("X-Msh-Tool-Call-Id", randomUUID());
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
function bearerToken(headers) {
|
|
19
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
20
|
+
if (name.toLowerCase() !== "authorization" || typeof value !== "string")
|
|
21
|
+
continue;
|
|
22
|
+
const match = /^Bearer\s+(.+)$/i.exec(value.trim());
|
|
23
|
+
if (match?.[1])
|
|
24
|
+
return match[1];
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
async function resolveKimiAuth(ctx) {
|
|
29
|
+
if (!ctx)
|
|
30
|
+
return;
|
|
31
|
+
const models = ctx.modelRegistry.getAll();
|
|
32
|
+
let firstError;
|
|
33
|
+
for (const provider of KIMI_PROVIDERS) {
|
|
34
|
+
for (const model of models) {
|
|
35
|
+
if (model.provider !== provider)
|
|
36
|
+
continue;
|
|
37
|
+
let resolved;
|
|
38
|
+
try {
|
|
39
|
+
resolved = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
firstError ??= err;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!resolved.ok)
|
|
45
|
+
continue;
|
|
46
|
+
const headers = resolved.headers ?? {};
|
|
47
|
+
const apiKey = resolved.apiKey || bearerToken(headers);
|
|
48
|
+
if (apiKey)
|
|
49
|
+
return { apiKey, headers };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (firstError)
|
|
53
|
+
throw firstError;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
function normalizeCount(value) {
|
|
57
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
58
|
+
return 5;
|
|
59
|
+
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
60
|
+
}
|
|
61
|
+
function normalizeDomain(value) {
|
|
62
|
+
let input = value.trim().toLowerCase();
|
|
63
|
+
if (!input)
|
|
64
|
+
return null;
|
|
65
|
+
if (input.startsWith("-"))
|
|
66
|
+
input = input.slice(1).trim();
|
|
67
|
+
if (!input)
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
71
|
+
input = parsed.hostname;
|
|
72
|
+
} catch {
|
|
73
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
74
|
+
}
|
|
75
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
76
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
77
|
+
}
|
|
78
|
+
function normalizeDomainFilters(domainFilter) {
|
|
79
|
+
const filters = { allowed: [], blocked: [] };
|
|
80
|
+
if (!domainFilter?.length)
|
|
81
|
+
return filters;
|
|
82
|
+
for (const raw of domainFilter) {
|
|
83
|
+
const domain = normalizeDomain(raw);
|
|
84
|
+
if (!domain)
|
|
85
|
+
continue;
|
|
86
|
+
const target = raw.trim().startsWith("-") ? filters.blocked : filters.allowed;
|
|
87
|
+
if (!target.includes(domain))
|
|
88
|
+
target.push(domain);
|
|
89
|
+
}
|
|
90
|
+
return filters;
|
|
91
|
+
}
|
|
92
|
+
function hostMatchesDomain(hostname, domain) {
|
|
93
|
+
return hostname === domain || hostname.endsWith(`.${domain}`);
|
|
94
|
+
}
|
|
95
|
+
function matchesDomainFilters(url, filters) {
|
|
96
|
+
if (filters.allowed.length === 0 && filters.blocked.length === 0)
|
|
97
|
+
return true;
|
|
98
|
+
let hostname = "";
|
|
99
|
+
try {
|
|
100
|
+
hostname = new URL(url).hostname.toLowerCase();
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (filters.allowed.length > 0 && !filters.allowed.some((domain) => hostMatchesDomain(hostname, domain))) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return !filters.blocked.some((domain) => hostMatchesDomain(hostname, domain));
|
|
108
|
+
}
|
|
109
|
+
function normalizeResultUrl(value) {
|
|
110
|
+
if (typeof value !== "string")
|
|
111
|
+
return null;
|
|
112
|
+
const input = value.trim();
|
|
113
|
+
if (!input)
|
|
114
|
+
return null;
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(input);
|
|
117
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
118
|
+
return null;
|
|
119
|
+
return url.href;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function parseResults(value, options) {
|
|
125
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.search_results)) {
|
|
126
|
+
throw new Error("Kimi Code search API returned an invalid response: search_results must be an array");
|
|
127
|
+
}
|
|
128
|
+
const filters = normalizeDomainFilters(options.domainFilter);
|
|
129
|
+
const numResults = normalizeCount(options.numResults);
|
|
130
|
+
const results = [];
|
|
131
|
+
for (const item of value.search_results) {
|
|
132
|
+
if (!item || typeof item !== "object")
|
|
133
|
+
continue;
|
|
134
|
+
const record = item;
|
|
135
|
+
const url = normalizeResultUrl(record.url);
|
|
136
|
+
if (!url || !matchesDomainFilters(url, filters))
|
|
137
|
+
continue;
|
|
138
|
+
results.push({
|
|
139
|
+
title: typeof record.title === "string" && record.title.trim() ? record.title : url,
|
|
140
|
+
url,
|
|
141
|
+
snippet: typeof record.snippet === "string" ? record.snippet : ""
|
|
142
|
+
});
|
|
143
|
+
if (results.length >= numResults)
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
function formatAnswer(results) {
|
|
149
|
+
return results.map((result) => result.snippet ? `${result.snippet}
|
|
150
|
+
Source: ${result.title} (${result.url})` : `Source: ${result.title} (${result.url})`).join(`
|
|
151
|
+
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
export async function isKimiSearchAvailable(ctx) {
|
|
155
|
+
try {
|
|
156
|
+
return !!await resolveKimiAuth(ctx);
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export async function searchWithKimi(query, options = {}, ctx) {
|
|
162
|
+
const auth = await resolveKimiAuth(ctx);
|
|
163
|
+
if (!auth) {
|
|
164
|
+
throw new Error("Kimi Code web search unavailable. Run /login kimi-coding to authenticate a Kimi Code Plan account.");
|
|
165
|
+
}
|
|
166
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
167
|
+
const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
|
|
168
|
+
const requestSignal = options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal;
|
|
169
|
+
try {
|
|
170
|
+
const response = await fetch(KIMI_SEARCH_URL, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: buildRequestHeaders(auth),
|
|
173
|
+
body: JSON.stringify({ text_query: query }),
|
|
174
|
+
signal: requestSignal
|
|
175
|
+
});
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
activityMonitor.logError(activityId, `HTTP ${response.status}`);
|
|
178
|
+
const errorText = redactCredential(await response.text(), auth.apiKey);
|
|
179
|
+
throw new Error(`Kimi Code search API error ${response.status}: ${errorText.slice(0, 300)}`);
|
|
180
|
+
}
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = await response.json();
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
186
|
+
throw new Error(`Kimi Code search API returned invalid JSON: ${message}`);
|
|
187
|
+
}
|
|
188
|
+
const results = parseResults(parsed, options);
|
|
189
|
+
if (results.length === 0) {
|
|
190
|
+
throw new Error("Kimi Code search API returned no results");
|
|
191
|
+
}
|
|
192
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
193
|
+
return { answer: formatAnswer(results), results };
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
196
|
+
const redactedMessage = redactCredential(message, auth.apiKey);
|
|
197
|
+
if (options.signal?.aborted) {
|
|
198
|
+
activityMonitor.logComplete(activityId, 0);
|
|
199
|
+
} else if (timeoutSignal.aborted || err instanceof Error && err.name === "TimeoutError") {
|
|
200
|
+
activityMonitor.logError(activityId, "Kimi Code search API timed out");
|
|
201
|
+
throw new Error("Kimi Code search API timed out");
|
|
202
|
+
} else if (redactedMessage.toLowerCase().includes("abort")) {
|
|
203
|
+
activityMonitor.logComplete(activityId, 0);
|
|
204
|
+
} else {
|
|
205
|
+
activityMonitor.logError(activityId, redactedMessage);
|
|
206
|
+
}
|
|
207
|
+
if (redactedMessage === message)
|
|
208
|
+
throw err;
|
|
209
|
+
const redactedError = new Error(redactedMessage);
|
|
210
|
+
if (err instanceof Error)
|
|
211
|
+
redactedError.name = err.name;
|
|
212
|
+
throw redactedError;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { activityMonitor } from "./activity.js";
|
|
3
|
+
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.js";
|
|
4
|
+
import { fetchRemoteUrl, loadFetchContentDomainPolicy, loadSsrfConfig, validateRemoteUrl } from "./ssrf-protection.js";
|
|
5
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
6
|
+
const OLLAMA_SEARCH_URL = "https://ollama.com/api/web_search";
|
|
7
|
+
const OLLAMA_FETCH_URL = "https://ollama.com/api/web_fetch";
|
|
8
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
9
|
+
const SEARCH_TIMEOUT_MS = 60000;
|
|
10
|
+
let cachedConfig = null;
|
|
11
|
+
function loadConfig() {
|
|
12
|
+
if (cachedConfig)
|
|
13
|
+
return cachedConfig;
|
|
14
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
15
|
+
cachedConfig = {};
|
|
16
|
+
return cachedConfig;
|
|
17
|
+
}
|
|
18
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(raw);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
25
|
+
}
|
|
26
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
27
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
|
|
28
|
+
}
|
|
29
|
+
cachedConfig = parsed;
|
|
30
|
+
return cachedConfig;
|
|
31
|
+
}
|
|
32
|
+
async function getApiKey(signal) {
|
|
33
|
+
return resolveCredential({
|
|
34
|
+
provider: "Ollama",
|
|
35
|
+
configuredValue: loadConfig().ollamaApiKey,
|
|
36
|
+
environmentValue: process.env.OLLAMA_API_KEY,
|
|
37
|
+
signal
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function requireApiKey(signal) {
|
|
41
|
+
const apiKey = await getApiKey(signal);
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
throw new Error(`Ollama API key not found. Either:
|
|
44
|
+
` + ` 1. Create ${CONFIG_PATH} with { "ollamaApiKey": "your-key" }
|
|
45
|
+
` + ` 2. Set OLLAMA_API_KEY environment variable
|
|
46
|
+
` + "Create a key at https://ollama.com/settings/keys");
|
|
47
|
+
}
|
|
48
|
+
return apiKey;
|
|
49
|
+
}
|
|
50
|
+
function normalizeCount(value) {
|
|
51
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
52
|
+
return 5;
|
|
53
|
+
return Math.max(1, Math.min(Math.floor(value), 10));
|
|
54
|
+
}
|
|
55
|
+
function errorMessage(err) {
|
|
56
|
+
return err instanceof Error ? err.message : String(err);
|
|
57
|
+
}
|
|
58
|
+
function invalidResponse(message) {
|
|
59
|
+
return new Error(`Ollama API returned invalid response: ${message}`);
|
|
60
|
+
}
|
|
61
|
+
function parseSearchResponse(value) {
|
|
62
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
63
|
+
throw invalidResponse("expected an object envelope");
|
|
64
|
+
const envelope = value;
|
|
65
|
+
if (!Array.isArray(envelope.results))
|
|
66
|
+
throw invalidResponse("expected results array");
|
|
67
|
+
const results = [];
|
|
68
|
+
for (const [index, value] of envelope.results.entries()) {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
70
|
+
throw invalidResponse(`expected results[${index}] object`);
|
|
71
|
+
const item = value;
|
|
72
|
+
if (typeof item.title !== "string")
|
|
73
|
+
throw invalidResponse(`expected results[${index}].title string`);
|
|
74
|
+
if (typeof item.url !== "string" || !item.url)
|
|
75
|
+
throw invalidResponse(`expected results[${index}].url non-empty string`);
|
|
76
|
+
if (typeof item.content !== "string")
|
|
77
|
+
throw invalidResponse(`expected results[${index}].content string`);
|
|
78
|
+
results.push({ title: item.title, url: item.url, content: item.content });
|
|
79
|
+
}
|
|
80
|
+
return { results };
|
|
81
|
+
}
|
|
82
|
+
function parseFetchResponse(value) {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
84
|
+
throw invalidResponse("expected fetch object envelope");
|
|
85
|
+
const envelope = value;
|
|
86
|
+
if (typeof envelope.title !== "string")
|
|
87
|
+
throw invalidResponse("expected title string");
|
|
88
|
+
if (typeof envelope.content !== "string")
|
|
89
|
+
throw invalidResponse("expected content string");
|
|
90
|
+
return { title: envelope.title, content: envelope.content, links: envelope.links };
|
|
91
|
+
}
|
|
92
|
+
function buildAnswer(results) {
|
|
93
|
+
return results.map((result) => result.snippet ? `${result.snippet}
|
|
94
|
+
Source: ${result.title} (${result.url})` : `Source: ${result.title} (${result.url})`).join(`
|
|
95
|
+
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
export function isOllamaAvailable() {
|
|
99
|
+
return hasCredentialSource({ provider: "Ollama", configuredValue: loadConfig().ollamaApiKey, environmentValue: process.env.OLLAMA_API_KEY });
|
|
100
|
+
}
|
|
101
|
+
export async function searchWithOllama(query, options = {}) {
|
|
102
|
+
const apiKey = await requireApiKey(options.signal);
|
|
103
|
+
const numResults = normalizeCount(options.numResults);
|
|
104
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
105
|
+
let response;
|
|
106
|
+
try {
|
|
107
|
+
response = await fetch(OLLAMA_SEARCH_URL, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({ query, max_results: numResults }),
|
|
111
|
+
signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS)
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const message = errorMessage(err);
|
|
115
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
116
|
+
if (redactedMessage.toLowerCase().includes("abort"))
|
|
117
|
+
activityMonitor.logComplete(activityId, 0);
|
|
118
|
+
else
|
|
119
|
+
activityMonitor.logError(activityId, redactedMessage);
|
|
120
|
+
if (redactedMessage === message)
|
|
121
|
+
throw err;
|
|
122
|
+
const redactedError = new Error(redactedMessage);
|
|
123
|
+
if (err instanceof Error)
|
|
124
|
+
redactedError.name = err.name;
|
|
125
|
+
throw redactedError;
|
|
126
|
+
}
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
129
|
+
const errorText = redactCredential(await response.text(), apiKey);
|
|
130
|
+
throw new Error(`Ollama API error ${response.status}: ${errorText.slice(0, 300)}`);
|
|
131
|
+
}
|
|
132
|
+
let rawData;
|
|
133
|
+
try {
|
|
134
|
+
rawData = await response.json();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
137
|
+
throw new Error(`Ollama API returned invalid JSON: ${errorMessage(err)}`);
|
|
138
|
+
}
|
|
139
|
+
const data = parseSearchResponse(rawData);
|
|
140
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
141
|
+
const results = data.results.slice(0, numResults).map((result) => ({ title: result.title, url: result.url, snippet: result.content }));
|
|
142
|
+
const mapped = { answer: buildAnswer(results), results };
|
|
143
|
+
if (options.includeContent) {
|
|
144
|
+
const inlineContent = data.results.slice(0, numResults).filter((result) => result.content.trim().length > 0).map((result) => ({ url: result.url, title: result.title, content: result.content, error: null }));
|
|
145
|
+
if (inlineContent.length > 0)
|
|
146
|
+
mapped.inlineContent = inlineContent;
|
|
147
|
+
}
|
|
148
|
+
return mapped;
|
|
149
|
+
}
|
|
150
|
+
export function isOllamaFetchAvailable() {
|
|
151
|
+
return isOllamaAvailable();
|
|
152
|
+
}
|
|
153
|
+
export async function extractWithOllama(url, signal, options = {}) {
|
|
154
|
+
const ssrf = options.ssrf ?? loadSsrfConfig();
|
|
155
|
+
const domainPolicy = loadFetchContentDomainPolicy();
|
|
156
|
+
await validateRemoteUrl(url, {
|
|
157
|
+
allowRanges: ssrf.allowRanges,
|
|
158
|
+
trustEnvProxy: ssrf.trustEnvProxy,
|
|
159
|
+
domainPolicy,
|
|
160
|
+
...options.lookup ? { lookup: options.lookup } : {}
|
|
161
|
+
});
|
|
162
|
+
const apiKey = await requireApiKey(signal);
|
|
163
|
+
const activityId = activityMonitor.logStart({ type: "api", query: `ollama fetch: ${url}` });
|
|
164
|
+
let response;
|
|
165
|
+
try {
|
|
166
|
+
response = await fetchRemoteUrl(OLLAMA_FETCH_URL, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
169
|
+
body: JSON.stringify({ url }),
|
|
170
|
+
signal: signal ? AbortSignal.any([AbortSignal.timeout(options.timeoutMs ?? SEARCH_TIMEOUT_MS), signal]) : AbortSignal.timeout(options.timeoutMs ?? SEARCH_TIMEOUT_MS)
|
|
171
|
+
}, {
|
|
172
|
+
allowRanges: ssrf.allowRanges,
|
|
173
|
+
trustEnvProxy: ssrf.trustEnvProxy,
|
|
174
|
+
onRedirect: ({ from, to, init }) => to.origin === from.origin ? init : { ...init, headers: { "Content-Type": "application/json" } },
|
|
175
|
+
...options.lookup ? { lookup: options.lookup } : {}
|
|
176
|
+
});
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const message = errorMessage(err);
|
|
179
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
180
|
+
if (redactedMessage.toLowerCase().includes("abort"))
|
|
181
|
+
activityMonitor.logComplete(activityId, 0);
|
|
182
|
+
else
|
|
183
|
+
activityMonitor.logError(activityId, redactedMessage);
|
|
184
|
+
if (redactedMessage === message)
|
|
185
|
+
throw err;
|
|
186
|
+
const redactedError = new Error(redactedMessage);
|
|
187
|
+
if (err instanceof Error)
|
|
188
|
+
redactedError.name = err.name;
|
|
189
|
+
throw redactedError;
|
|
190
|
+
}
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
193
|
+
const errorText = redactCredential(await response.text(), apiKey);
|
|
194
|
+
throw new Error(`Ollama Web Fetch error ${response.status}: ${errorText.slice(0, 300)}`);
|
|
195
|
+
}
|
|
196
|
+
let rawData;
|
|
197
|
+
try {
|
|
198
|
+
rawData = await response.json();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
201
|
+
throw new Error(`Ollama Web Fetch returned invalid JSON: ${errorMessage(err)}`);
|
|
202
|
+
}
|
|
203
|
+
const data = parseFetchResponse(rawData);
|
|
204
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
205
|
+
const content = data.content.trim();
|
|
206
|
+
if (!content)
|
|
207
|
+
return null;
|
|
208
|
+
return { url, title: data.title, content, error: null };
|
|
209
|
+
}
|