@herbertgao/pi-extensions 2026.9.9 → 2026.9.11
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/README.md +1 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +427 -120
- package/node_modules/@herbertgao/pi-subagents/docs/rpc.md +184 -0
- package/node_modules/@herbertgao/pi-subagents/docs/workflows.md +466 -0
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +6 -6
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/compose.js +52 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/fan-out-audit.js +56 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/gated-fix.js +60 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/lib/count-child.js +30 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/review-panel.js +68 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/structured-findings.js +81 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +12 -10
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +52 -12
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +837 -146
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +213 -39
- package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +73 -14
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +101 -47
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +2249 -914
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +13 -0
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +215 -0
- package/node_modules/@herbertgao/pi-subagents/src/mention.ts +147 -0
- package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +9 -1
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +40 -26
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +18 -8
- package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +46 -9
- package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +21 -16
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +137 -7
- package/node_modules/@herbertgao/pi-subagents/src/structured-output.ts +136 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +126 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-mention.ts +274 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +20 -5
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +10 -4
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +167 -22
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-card.ts +555 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-dialog.ts +1304 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-menu.ts +226 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +122 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/entry.ts +47 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/host.ts +463 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/journal.ts +164 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +142 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/meta.ts +401 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/progress.ts +622 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/runtime.ts +1399 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/saved.ts +230 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/task.ts +333 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/tool-description.ts +200 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/worker-source.ts +781 -0
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +97 -95
- package/node_modules/@herbertgao/pi-subagents/src/xml.ts +13 -0
- package/node_modules/@herbertgao/resume-from/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
- package/node_modules/@herbertgao/sol-pi/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
- package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
- package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
- package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
- package/node_modules/pi-multi-account/LICENSE +21 -0
- package/node_modules/pi-multi-account/README.md +197 -0
- package/node_modules/pi-multi-account/SECURITY.md +27 -0
- package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
- package/node_modules/pi-multi-account/child-usability.ts +233 -0
- package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
- package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
- package/node_modules/pi-multi-account/context-guard.ts +420 -0
- package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
- package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
- package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
- package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
- package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
- package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
- package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
- package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
- package/node_modules/pi-multi-account/cursor/index.ts +572 -0
- package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
- package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
- package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
- package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
- package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
- package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
- package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
- package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
- package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
- package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
- package/node_modules/pi-multi-account/index.ts +11825 -0
- package/node_modules/pi-multi-account/model-catalog.ts +354 -0
- package/node_modules/pi-multi-account/package.json +101 -0
- package/node_modules/pi-multi-account/pi-contract.ts +281 -0
- package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
- package/node_modules/pi-multi-account/provider-priority.ts +189 -0
- package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
- package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
- package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
- package/node_modules/pi-multi-account/usage.ts +1099 -0
- package/node_modules/pi-typesafe/README.md +6 -2
- package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
- package/node_modules/pi-typesafe/dist/client.js +45 -10
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/package.json +2 -2
- package/node_modules/pi-web-access/CHANGELOG.md +36 -0
- package/node_modules/pi-web-access/README.md +75 -18
- package/node_modules/pi-web-access/anysearch.ts +4 -15
- package/node_modules/pi-web-access/bocha.ts +3 -22
- package/node_modules/pi-web-access/brave.ts +3 -21
- package/node_modules/pi-web-access/brightdata.ts +5 -32
- package/node_modules/pi-web-access/content-find.ts +168 -53
- package/node_modules/pi-web-access/curator-page.ts +4 -1
- package/node_modules/pi-web-access/curator-run.ts +2 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +24620 -0
- package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
- package/node_modules/pi-web-access/duckduckgo.ts +3 -21
- package/node_modules/pi-web-access/extract.ts +3 -1
- package/node_modules/pi-web-access/firecrawl.ts +5 -29
- package/node_modules/pi-web-access/gemini-search.ts +81 -32
- package/node_modules/pi-web-access/index.ts +149 -148
- package/node_modules/pi-web-access/jina-search.ts +4 -15
- package/node_modules/pi-web-access/kagi.ts +4 -13
- package/node_modules/pi-web-access/kimi-search.ts +5 -30
- package/node_modules/pi-web-access/mistral-search.ts +1 -15
- package/node_modules/pi-web-access/ollama.ts +2 -7
- package/node_modules/pi-web-access/openai-search.ts +174 -36
- package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
- package/node_modules/pi-web-access/package.json +10 -4
- package/node_modules/pi-web-access/page-query.ts +10 -2
- package/node_modules/pi-web-access/parallel.ts +1 -15
- package/node_modules/pi-web-access/pdf-extract.ts +3 -0
- package/node_modules/pi-web-access/querit.ts +5 -29
- package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
- package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
- package/node_modules/pi-web-access/search1api.ts +5 -29
- package/node_modules/pi-web-access/searchinfinity.ts +5 -29
- package/node_modules/pi-web-access/searxng.ts +3 -21
- package/node_modules/pi-web-access/serpapi.ts +5 -28
- package/node_modules/pi-web-access/serpbase.ts +3 -22
- package/node_modules/pi-web-access/serpdive.ts +3 -21
- package/node_modules/pi-web-access/serper.ts +5 -28
- package/node_modules/pi-web-access/serply.ts +197 -0
- package/node_modules/pi-web-access/source-check.ts +11 -47
- package/node_modules/pi-web-access/summary-review.ts +7 -3
- package/node_modules/pi-web-access/tavily.ts +3 -21
- package/node_modules/pi-web-access/tinyfish.ts +5 -29
- package/node_modules/pi-web-access/utils.ts +9 -1
- package/node_modules/pi-web-access/valyu.ts +5 -28
- package/node_modules/pi-web-access/xai-search.ts +1 -15
- package/node_modules/pi-web-access/xcrawl.ts +5 -32
- package/package.json +17 -11
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { activityMonitor } from "./activity.ts";
|
|
3
|
+
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
4
|
+
import type { SearchOptions, SearchResponse } from "./perplexity.ts";
|
|
5
|
+
import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
|
|
6
|
+
import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
|
|
7
|
+
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
8
|
+
import { fetchWithCredentialRedirects, getWebSearchConfigPath } from "./utils.ts";
|
|
9
|
+
|
|
10
|
+
const SERPLY_SEARCH_URL = "https://api.serply.io/v1/search";
|
|
11
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
12
|
+
const SEARCH_TIMEOUT_MS = 60_000;
|
|
13
|
+
const RECENCY_TBS: Record<NonNullable<SearchOptions["recencyFilter"]>, string> = {
|
|
14
|
+
day: "qdr:d",
|
|
15
|
+
week: "qdr:w",
|
|
16
|
+
month: "qdr:m",
|
|
17
|
+
year: "qdr:y",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface WebSearchConfig {
|
|
21
|
+
serplyApiKey?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SerplyOrganicResult {
|
|
25
|
+
title?: unknown;
|
|
26
|
+
link?: unknown;
|
|
27
|
+
description?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let cachedConfig: WebSearchConfig | null = null;
|
|
31
|
+
|
|
32
|
+
function loadConfig(): WebSearchConfig {
|
|
33
|
+
if (cachedConfig) return cachedConfig;
|
|
34
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
35
|
+
cachedConfig = {};
|
|
36
|
+
return cachedConfig;
|
|
37
|
+
}
|
|
38
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
39
|
+
let parsed: unknown;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(raw);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
47
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
|
|
48
|
+
}
|
|
49
|
+
cachedConfig = parsed as WebSearchConfig;
|
|
50
|
+
return cachedConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getApiKey(signal?: AbortSignal): Promise<string | null> {
|
|
54
|
+
return resolveCredential({
|
|
55
|
+
provider: "Serply",
|
|
56
|
+
configuredValue: loadConfig().serplyApiKey,
|
|
57
|
+
environmentValue: process.env.SERPLY_API_KEY,
|
|
58
|
+
signal,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function requireApiKey(signal?: AbortSignal): Promise<string> {
|
|
63
|
+
const apiKey = await getApiKey(signal);
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Serply API key not found. Either:\n" +
|
|
67
|
+
` 1. Create ${CONFIG_PATH} with { "serplyApiKey": "your-key" }\n` +
|
|
68
|
+
" 2. Set SERPLY_API_KEY environment variable\n" +
|
|
69
|
+
"Get a key at https://serply.io",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return apiKey;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface DomainFilters {
|
|
76
|
+
include: string[];
|
|
77
|
+
exclude: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
|
|
81
|
+
const filters: DomainFilters = { include: [], exclude: [] };
|
|
82
|
+
for (const raw of domainFilter ?? []) {
|
|
83
|
+
const domain = normalizeDomain(raw);
|
|
84
|
+
if (!domain) continue;
|
|
85
|
+
const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
|
|
86
|
+
if (!target.includes(domain)) target.push(domain);
|
|
87
|
+
}
|
|
88
|
+
return filters;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function passesDomainFilters(url: URL, filters: DomainFilters): boolean {
|
|
92
|
+
if (filters.include.length === 0 && filters.exclude.length === 0) return true;
|
|
93
|
+
const hostname = url.hostname.toLowerCase();
|
|
94
|
+
const matches = (domain: string) => hostname === domain || hostname.endsWith(`.${domain}`);
|
|
95
|
+
if (filters.exclude.some(matches)) return false;
|
|
96
|
+
return filters.include.length === 0 || filters.include.some(matches);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildQuery(query: string, filters: DomainFilters): string {
|
|
100
|
+
const parts = [query];
|
|
101
|
+
if (filters.include.length === 1) parts.push(`site:${filters.include[0]}`);
|
|
102
|
+
if (filters.include.length > 1) parts.push(`(${filters.include.map(domain => `site:${domain}`).join(" OR ")})`);
|
|
103
|
+
for (const domain of filters.exclude) parts.push(`-site:${domain}`);
|
|
104
|
+
return parts.join(" ");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function errorMessage(err: unknown): string {
|
|
108
|
+
return err instanceof Error ? err.message : String(err);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function invalidResponse(message: string): Error {
|
|
112
|
+
return new Error(`Serply returned invalid response: ${message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseResponse(value: unknown): SerplyOrganicResult[] {
|
|
116
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidResponse("expected an object envelope");
|
|
117
|
+
const envelope = value as Record<string, unknown>;
|
|
118
|
+
if (typeof envelope.detail === "string" && envelope.detail.trim()) throw invalidResponse(envelope.detail.trim());
|
|
119
|
+
if (!Array.isArray(envelope.results)) throw invalidResponse("expected results array");
|
|
120
|
+
return envelope.results as SerplyOrganicResult[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isSerplyAvailable(): boolean {
|
|
124
|
+
return hasCredentialSource({ provider: "Serply", configuredValue: loadConfig().serplyApiKey, environmentValue: process.env.SERPLY_API_KEY });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function searchWithSerply(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
|
|
128
|
+
const apiKey = await requireApiKey(options.signal);
|
|
129
|
+
const numResults = normalizeSearchResultCount(options.numResults);
|
|
130
|
+
const filters = parseDomainFilter(options.domainFilter);
|
|
131
|
+
const requestCount = options.domainFilter?.length ? Math.min(20, numResults + 5) : numResults;
|
|
132
|
+
const url = new URL(SERPLY_SEARCH_URL);
|
|
133
|
+
url.searchParams.set("q", buildQuery(query, filters));
|
|
134
|
+
url.searchParams.set("num", String(requestCount));
|
|
135
|
+
if (options.recencyFilter) url.searchParams.set("tbs", RECENCY_TBS[options.recencyFilter]);
|
|
136
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
137
|
+
const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
|
|
138
|
+
let response: Response;
|
|
139
|
+
let entries: SerplyOrganicResult[];
|
|
140
|
+
try {
|
|
141
|
+
response = await fetchWithCredentialRedirects(String(url), {
|
|
142
|
+
headers: { Accept: "application/json", "X-Api-Key": apiKey },
|
|
143
|
+
signal: options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal,
|
|
144
|
+
}, ["X-Api-Key"]);
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const errorText = await response.text();
|
|
147
|
+
throw new Error(`Serply error ${response.status}: ${redactCredential(errorText, apiKey).slice(0, 300)}`);
|
|
148
|
+
}
|
|
149
|
+
let rawData: unknown;
|
|
150
|
+
try {
|
|
151
|
+
rawData = await response.json();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err instanceof Error && err.name === "TimeoutError") throw err;
|
|
154
|
+
throw new Error(`Serply returned invalid JSON: ${errorMessage(err)}`);
|
|
155
|
+
}
|
|
156
|
+
entries = parseResponse(rawData);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (options.signal?.aborted) {
|
|
159
|
+
activityMonitor.logComplete(activityId, 0);
|
|
160
|
+
throw new Error("Aborted");
|
|
161
|
+
}
|
|
162
|
+
const message = errorMessage(err);
|
|
163
|
+
const providerTimeout = timeoutSignal.aborted || (err instanceof Error && err.name === "TimeoutError");
|
|
164
|
+
const outgoing = providerTimeout
|
|
165
|
+
? new Error(`Serply request timed out after ${Math.round(SEARCH_TIMEOUT_MS / 1000)}s`)
|
|
166
|
+
: (() => {
|
|
167
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
168
|
+
if (redactedMessage === message && err instanceof Error) return err;
|
|
169
|
+
const redactedError = new Error(redactedMessage);
|
|
170
|
+
if (err instanceof Error) redactedError.name = err.name;
|
|
171
|
+
return redactedError;
|
|
172
|
+
})();
|
|
173
|
+
activityMonitor.logError(activityId, redactCredential(errorMessage(outgoing), apiKey));
|
|
174
|
+
throw outgoing;
|
|
175
|
+
}
|
|
176
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
177
|
+
const results: SearchResponse["results"] = [];
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (!entry || typeof entry !== "object") continue;
|
|
180
|
+
if (typeof entry.link !== "string" || !entry.link) continue;
|
|
181
|
+
let resultUrl: URL;
|
|
182
|
+
try {
|
|
183
|
+
resultUrl = new URL(entry.link);
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (resultUrl.protocol !== "http:" && resultUrl.protocol !== "https:") continue;
|
|
188
|
+
if (!passesDomainFilters(resultUrl, filters)) continue;
|
|
189
|
+
results.push({
|
|
190
|
+
title: typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : `Source ${results.length + 1}`,
|
|
191
|
+
url: resultUrl.href,
|
|
192
|
+
snippet: typeof entry.description === "string" ? entry.description : "",
|
|
193
|
+
});
|
|
194
|
+
if (results.length >= numResults) break;
|
|
195
|
+
}
|
|
196
|
+
return { answer: formatSearchResultsAsAnswer(results), results };
|
|
197
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Structured source
|
|
1
|
+
// Structured source evidence and machine-readable research artifacts.
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { generateId, getResult, storeResult } from "./storage.ts";
|
|
4
4
|
import type { SearchResult } from "./perplexity.ts";
|
|
@@ -159,54 +159,18 @@ export function buildPassages(sources: ResearchSource[], fetched: ExtractedConte
|
|
|
159
159
|
return passages;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
const CONTRADICTION_MARKERS = ["not true", "false", "incorrect", "debunked", "retracted", "no longer", "never", "denied", "contrary", "misleading"];
|
|
163
|
-
const SUPPORT_MARKERS = ["yes", "true", "correct", "confirmed", "according to", "shows that", "demonstrates", "reported", "verified", "established"];
|
|
164
|
-
|
|
165
|
-
function containsPhrase(value: string, phrase: string): boolean {
|
|
166
|
-
const escaped = phrase.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
167
|
-
return new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i").test(value);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function markerIsNegated(value: string, marker: string): boolean {
|
|
171
|
-
const escaped = marker.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
172
|
-
const markerPattern = new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i");
|
|
173
|
-
const match = markerPattern.exec(value);
|
|
174
|
-
if (!match || match.index === undefined) return false;
|
|
175
|
-
const matchedMarker = match[0].replace(/^[^a-z0-9]+/i, "");
|
|
176
|
-
const beforeMarker = value.slice(0, match.index + match[0].length - matchedMarker.length);
|
|
177
|
-
return /(?:^|[^a-z0-9])(?:not|no|never|without)\s+$/i.test(beforeMarker);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function hasPolarityMarker(value: string, markers: string[], allowNegated = false): boolean {
|
|
181
|
-
return markers.some((marker) => containsPhrase(value, marker) && (allowNegated || !markerIsNegated(value, marker)));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
162
|
export function assessClaim(claim: string, passages: ResearchPassage[]): ClaimAssessment {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return { claim, status: "missing-evidence", supporting_passages: [], contradicting_passages: [], rationale: "No passages available that discuss the claim's terms.", confidence: 0.2 };
|
|
163
|
+
if (passages.length === 0) {
|
|
164
|
+
return { claim, status: "missing-evidence", supporting_passages: [], contradicting_passages: [], rationale: "No passages were retrieved for the claim.", confidence: 0.2 };
|
|
188
165
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (contra && !support) contradicting.push(passage.passage_id);
|
|
198
|
-
else if (support && !contra) supporting.push(passage.passage_id);
|
|
199
|
-
}
|
|
200
|
-
if (contradicting.length > 0 && supporting.length === 0) {
|
|
201
|
-
return { claim, status: "contradicted", supporting_passages: [], contradicting_passages: contradicting, rationale: `${contradicting.length} passage(s) contradict the claim; none support it.`, confidence: Math.min(0.85, 0.5 + contradicting.length * 0.1) };
|
|
202
|
-
}
|
|
203
|
-
if (supporting.length > 0 && contradicting.length === 0) {
|
|
204
|
-
return { claim, status: "supported", supporting_passages: supporting, contradicting_passages: [], rationale: `${supporting.length} passage(s) support the claim; none contradict it.`, confidence: Math.min(0.85, 0.5 + supporting.length * 0.1) };
|
|
205
|
-
}
|
|
206
|
-
if (supporting.length > 0 || contradicting.length > 0) {
|
|
207
|
-
return { claim, status: "unclear", supporting_passages: supporting, contradicting_passages: contradicting, rationale: `${supporting.length} supporting and ${contradicting.length} contradicting passage(s); evidence is mixed.`, confidence: 0.4 };
|
|
208
|
-
}
|
|
209
|
-
return { claim, status: "unclear", supporting_passages: [], contradicting_passages: [], rationale: "Passages mention the claim's terms but contain no clear support or contradiction markers.", confidence: 0.3 };
|
|
166
|
+
return {
|
|
167
|
+
claim,
|
|
168
|
+
status: "unclear",
|
|
169
|
+
supporting_passages: [],
|
|
170
|
+
contradicting_passages: [],
|
|
171
|
+
rationale: "Passages were retrieved, but automated semantic support or contradiction assessment is unavailable; review the cited passages manually.",
|
|
172
|
+
confidence: 0.3,
|
|
173
|
+
};
|
|
210
174
|
}
|
|
211
175
|
|
|
212
176
|
interface RankedSearchResult extends SearchResult {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ModelThinkingLevel, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { complete, Api, Message, Model } from "@earendil-works/pi-ai/compat";
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { openCodeSessionHeaders } from "./opencode-session-headers.ts";
|
|
4
5
|
import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns, splitThinkingSuffix, type SummaryThinkingLevel } from "./summary-model-scope.ts";
|
|
5
6
|
import type { QueryResultData } from "./storage.ts";
|
|
6
7
|
|
|
@@ -29,7 +30,7 @@ export interface SummaryMeta {
|
|
|
29
30
|
edited?: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export type SummaryGenerationContext = Pick<ExtensionContext, "model" | "modelRegistry" | "cwd" | "isProjectTrusted">;
|
|
33
|
+
export type SummaryGenerationContext = Pick<ExtensionContext, "model" | "modelRegistry" | "sessionManager" | "cwd" | "isProjectTrusted">;
|
|
33
34
|
|
|
34
35
|
function estimateTokens(text: string): number {
|
|
35
36
|
const trimmed = text.trim();
|
|
@@ -368,6 +369,7 @@ export async function generateSummaryDraft(
|
|
|
368
369
|
for (const { model, apiKey, headers, thinkingLevel } of resolved.candidates) {
|
|
369
370
|
const startedAt = Date.now();
|
|
370
371
|
try {
|
|
372
|
+
const sessionHeaders = openCodeSessionHeaders(model, ctx.sessionManager);
|
|
371
373
|
const userMessage: Message = {
|
|
372
374
|
role: "user",
|
|
373
375
|
content: [{ type: "text", text: prompt }],
|
|
@@ -378,14 +380,16 @@ export async function generateSummaryDraft(
|
|
|
378
380
|
? requestedThinkingLevel as ThinkingLevel
|
|
379
381
|
: undefined;
|
|
380
382
|
const completionOptions = {
|
|
381
|
-
...(usesRegistryComplete
|
|
383
|
+
...(usesRegistryComplete
|
|
384
|
+
? (sessionHeaders ? { transformHeaders: (requestHeaders: Record<string, string>) => ({ ...requestHeaders, ...sessionHeaders }) } : {})
|
|
385
|
+
: { apiKey, headers: sessionHeaders ? { ...headers, ...sessionHeaders } : headers }),
|
|
382
386
|
signal: completionSignal,
|
|
383
387
|
...(requestedThinkingLevel ? { reasoning: requestedThinkingLevel } : {}),
|
|
384
388
|
...(enabledThinkingLevel ? { reasoningEffort: enabledThinkingLevel } : {}),
|
|
385
389
|
};
|
|
386
390
|
checkSummaryDeadline();
|
|
387
391
|
const completion = thinkingLevel !== undefined && !customCompleteFn && !usesRegistryComplete
|
|
388
|
-
? piAiCompat!.completeSimple(model, { messages: [userMessage] }, { apiKey, headers, signal: completionSignal, ...(enabledThinkingLevel ? { reasoning: enabledThinkingLevel } : {}) })
|
|
392
|
+
? piAiCompat!.completeSimple(model, { messages: [userMessage] }, { apiKey, headers: sessionHeaders ? { ...headers, ...sessionHeaders } : headers, signal: completionSignal, ...(enabledThinkingLevel ? { reasoning: enabledThinkingLevel } : {}) })
|
|
389
393
|
: completeFn(model, { messages: [userMessage] }, completionOptions);
|
|
390
394
|
|
|
391
395
|
const response = await raceSummaryOperation(Promise.resolve(completion));
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { activityMonitor } from "./activity.ts";
|
|
3
|
+
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
3
4
|
import type { ExtractedContent } from "./extract.ts";
|
|
4
5
|
import type { SearchOptions, SearchResponse } from "./perplexity.ts";
|
|
6
|
+
import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
|
|
5
7
|
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
6
8
|
import { fetchWithCredentialRedirects, getWebSearchConfigPath, resolveApiBaseUrl } from "./utils.ts";
|
|
7
9
|
|
|
@@ -81,26 +83,6 @@ async function requireApiKey(signal?: AbortSignal): Promise<string> {
|
|
|
81
83
|
return apiKey;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
function normalizeCount(value: number | undefined): number {
|
|
85
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
86
|
-
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function normalizeDomain(value: string): string | null {
|
|
90
|
-
let input = value.trim().toLowerCase();
|
|
91
|
-
if (!input) return null;
|
|
92
|
-
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
93
|
-
if (!input) return null;
|
|
94
|
-
try {
|
|
95
|
-
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
96
|
-
input = parsed.hostname;
|
|
97
|
-
} catch {
|
|
98
|
-
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
99
|
-
}
|
|
100
|
-
input = input.replace(/^\.+|\.+$/g, "");
|
|
101
|
-
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
86
|
function mapDomainFilter(domainFilter: string[] | undefined): { include_domains?: string[]; exclude_domains?: string[] } {
|
|
105
87
|
if (!domainFilter?.length) return {};
|
|
106
88
|
const include_domains: string[] = [];
|
|
@@ -165,7 +147,7 @@ export function isTavilyAvailable(): boolean {
|
|
|
165
147
|
export async function searchWithTavily(query: string, options: TavilySearchOptions = {}): Promise<SearchResponse> {
|
|
166
148
|
const apiUrl = getApiUrl();
|
|
167
149
|
const apiKey = await requireApiKey(options.signal);
|
|
168
|
-
const numResults =
|
|
150
|
+
const numResults = normalizeSearchResultCount(options.numResults);
|
|
169
151
|
const body: Record<string, unknown> = {
|
|
170
152
|
query,
|
|
171
153
|
search_depth: "basic",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { activityMonitor } from "./activity.ts";
|
|
3
|
+
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
3
4
|
import type { ExtractedContent, ExtractOptions } from "./extract.ts";
|
|
4
5
|
import type { SearchOptions, SearchResponse } from "./perplexity.ts";
|
|
6
|
+
import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
|
|
7
|
+
import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
|
|
5
8
|
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
6
9
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
7
10
|
|
|
@@ -115,21 +118,6 @@ function requestSignal(signal: AbortSignal | undefined, timeoutMs: number): Abor
|
|
|
115
118
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
function normalizeDomain(value: string): string | null {
|
|
119
|
-
let input = value.trim().toLowerCase();
|
|
120
|
-
if (!input) return null;
|
|
121
|
-
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
122
|
-
if (!input) return null;
|
|
123
|
-
try {
|
|
124
|
-
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
125
|
-
input = parsed.hostname;
|
|
126
|
-
} catch {
|
|
127
|
-
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
128
|
-
}
|
|
129
|
-
input = input.replace(/^\.+|\.+$/g, "");
|
|
130
|
-
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
121
|
function mapDomainFilter(domainFilter: string[] | undefined): { includeDomains: string[]; excludeDomains: string[] } {
|
|
134
122
|
const includeDomains: string[] = [];
|
|
135
123
|
const excludeDomains: string[] = [];
|
|
@@ -153,11 +141,6 @@ function recencyMinutes(filter: SearchOptions["recencyFilter"]): number | undefi
|
|
|
153
141
|
return minutes[filter];
|
|
154
142
|
}
|
|
155
143
|
|
|
156
|
-
function normalizeNumResults(value: number | undefined): number {
|
|
157
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
158
|
-
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
144
|
function buildSearchUrl(query: string, options: SearchOptions, page: number): string {
|
|
162
145
|
const params = new URLSearchParams({ query });
|
|
163
146
|
const { includeDomains, excludeDomains } = mapDomainFilter(options.domainFilter);
|
|
@@ -233,13 +216,6 @@ function deduplicateResults(results: SearchResponse["results"], limit: number):
|
|
|
233
216
|
return unique;
|
|
234
217
|
}
|
|
235
218
|
|
|
236
|
-
function buildAnswer(results: SearchResponse["results"]): string {
|
|
237
|
-
return results.map((result) => {
|
|
238
|
-
if (result.snippet) return `${result.snippet}\nSource: ${result.title} (${result.url})`;
|
|
239
|
-
return `Source: ${result.title} (${result.url})`;
|
|
240
|
-
}).join("\n\n");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
219
|
function fetchPerUrlTimeout(value: number | undefined): number {
|
|
244
220
|
if (typeof value !== "number" || !Number.isFinite(value)) return MAX_FETCH_PER_URL_TIMEOUT_MS;
|
|
245
221
|
return Math.max(1, Math.min(Math.floor(value), MAX_FETCH_PER_URL_TIMEOUT_MS));
|
|
@@ -320,7 +296,7 @@ async function fetchInlineContent(
|
|
|
320
296
|
|
|
321
297
|
export async function searchWithTinyFish(query: string, options: TinyFishSearchOptions = {}): Promise<SearchResponse> {
|
|
322
298
|
const apiKey = await getApiKey(options.signal);
|
|
323
|
-
const numResults =
|
|
299
|
+
const numResults = normalizeSearchResultCount(options.numResults);
|
|
324
300
|
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
325
301
|
try {
|
|
326
302
|
const combined: SearchResponse["results"] = [];
|
|
@@ -340,7 +316,7 @@ export async function searchWithTinyFish(query: string, options: TinyFishSearchO
|
|
|
340
316
|
}
|
|
341
317
|
|
|
342
318
|
const results = deduplicateResults(combined, numResults);
|
|
343
|
-
const response: SearchResponse = { answer:
|
|
319
|
+
const response: SearchResponse = { answer: formatSearchResultsAsAnswer(results), results };
|
|
344
320
|
if (options.includeContent && results.length > 0) {
|
|
345
321
|
const inlineContent = await fetchInlineContent(results.map(result => result.url), apiKey, options.signal);
|
|
346
322
|
if (inlineContent.length > 0) response.inlineContent = inlineContent;
|
|
@@ -22,7 +22,13 @@ export function getWebSearchConfigDir(): string {
|
|
|
22
22
|
if (existsSync(join(legacyDir, "web-search.json"))) return cachedWebSearchConfigDir = legacyDir;
|
|
23
23
|
return cachedWebSearchConfigDir = xdgDir;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
const agentDir = join(homedir(), ".pi", "agent");
|
|
26
|
+
if (existsSync(join(agentDir, "web-search.json"))) return cachedWebSearchConfigDir = agentDir;
|
|
27
|
+
|
|
28
|
+
const legacyDir = join(homedir(), ".pi");
|
|
29
|
+
if (existsSync(join(legacyDir, "web-search.json"))) return cachedWebSearchConfigDir = legacyDir;
|
|
30
|
+
|
|
31
|
+
return cachedWebSearchConfigDir = agentDir;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export function getWebSearchConfigPath(): string {
|
|
@@ -256,9 +262,11 @@ export function runWithProxy<T>(proxy: string | undefined, fn: () => T): T {
|
|
|
256
262
|
if (proxy === undefined) {
|
|
257
263
|
const configured = loadConfiguredProxy();
|
|
258
264
|
if (configured === null) return fn();
|
|
265
|
+
installGlobalProxyFetch();
|
|
259
266
|
return proxyStorage.run(configured, fn);
|
|
260
267
|
}
|
|
261
268
|
const normalized = normalizeProxyUrl(proxy, "proxy");
|
|
269
|
+
if (normalized !== null) installGlobalProxyFetch();
|
|
262
270
|
return proxyStorage.run(normalized, fn);
|
|
263
271
|
}
|
|
264
272
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { activityMonitor } from "./activity.ts";
|
|
3
|
+
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
3
4
|
import type { ExtractedContent } from "./extract.ts";
|
|
4
5
|
import type { SearchOptions, SearchResponse } from "./perplexity.ts";
|
|
6
|
+
import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
|
|
7
|
+
import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
|
|
5
8
|
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
6
9
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
7
10
|
|
|
@@ -67,26 +70,6 @@ async function requireApiKey(signal?: AbortSignal): Promise<string> {
|
|
|
67
70
|
return apiKey;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
function normalizeCount(value: number | undefined): number {
|
|
71
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
72
|
-
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function normalizeDomain(value: string): string | null {
|
|
76
|
-
let input = value.trim().toLowerCase();
|
|
77
|
-
if (!input) return null;
|
|
78
|
-
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
79
|
-
if (!input) return null;
|
|
80
|
-
try {
|
|
81
|
-
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
82
|
-
input = parsed.hostname;
|
|
83
|
-
} catch {
|
|
84
|
-
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
85
|
-
}
|
|
86
|
-
input = input.replace(/^\.+|\.+$/g, "");
|
|
87
|
-
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
73
|
function mapDomainFilter(domainFilter: string[] | undefined): { included_sources?: string[]; excluded_sources?: string[] } {
|
|
91
74
|
if (!domainFilter?.length) return {};
|
|
92
75
|
const included_sources: string[] = [];
|
|
@@ -129,19 +112,13 @@ function parseResponse(value: unknown): ValyuResult[] {
|
|
|
129
112
|
return envelope.results as ValyuResult[];
|
|
130
113
|
}
|
|
131
114
|
|
|
132
|
-
function buildAnswer(results: SearchResponse["results"]): string {
|
|
133
|
-
return results.map((result) => result.snippet
|
|
134
|
-
? `${result.snippet}\nSource: ${result.title} (${result.url})`
|
|
135
|
-
: `Source: ${result.title} (${result.url})`).join("\n\n");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
115
|
export function isValyuAvailable(): boolean {
|
|
139
116
|
return hasCredentialSource({ provider: "Valyu", configuredValue: loadConfig().valyuApiKey, environmentValue: process.env.VALYU_API_KEY });
|
|
140
117
|
}
|
|
141
118
|
|
|
142
119
|
export async function searchWithValyu(query: string, options: SearchOptions & { includeContent?: boolean } = {}): Promise<SearchResponse> {
|
|
143
120
|
const apiKey = await requireApiKey(options.signal);
|
|
144
|
-
const numResults =
|
|
121
|
+
const numResults = normalizeSearchResultCount(options.numResults);
|
|
145
122
|
const startDate = recencyToStartDate(options.recencyFilter);
|
|
146
123
|
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
147
124
|
let response: Response;
|
|
@@ -195,5 +172,5 @@ export async function searchWithValyu(query: string, options: SearchOptions & {
|
|
|
195
172
|
if (options.includeContent && content) inlineContent.push({ url, title, content, error: null });
|
|
196
173
|
if (results.length >= numResults) break;
|
|
197
174
|
}
|
|
198
|
-
return { answer:
|
|
175
|
+
return { answer: formatSearchResultsAsAnswer(results), results, ...(inlineContent.length > 0 ? { inlineContent } : {}) };
|
|
199
176
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { activityMonitor } from "./activity.ts";
|
|
4
|
+
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
4
5
|
import type { SearchOptions, SearchResponse, SearchResult } from "./perplexity.ts";
|
|
5
6
|
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
6
7
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
@@ -174,21 +175,6 @@ export async function isXaiSearchAvailable(ctx?: ExtensionContext): Promise<bool
|
|
|
174
175
|
});
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
function normalizeDomain(value: string): string | null {
|
|
178
|
-
let input = value.trim().toLowerCase();
|
|
179
|
-
if (!input) return null;
|
|
180
|
-
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
181
|
-
if (!input) return null;
|
|
182
|
-
try {
|
|
183
|
-
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
184
|
-
input = parsed.hostname;
|
|
185
|
-
} catch {
|
|
186
|
-
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
187
|
-
}
|
|
188
|
-
input = input.replace(/^\.+|\.+$/g, "");
|
|
189
|
-
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
178
|
/**
|
|
193
179
|
* Recency, result count and domain filters are folded into the prompt rather
|
|
194
180
|
* than sent as tool parameters. xAI's filter field names are not verified, and
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { activityMonitor } from "./activity.ts";
|
|
3
|
+
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
3
4
|
import type { SearchOptions, SearchResponse } from "./perplexity.ts";
|
|
5
|
+
import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
|
|
6
|
+
import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
|
|
4
7
|
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
5
8
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
6
9
|
|
|
@@ -70,11 +73,6 @@ function invalidResponse(message: string): Error {
|
|
|
70
73
|
return new Error(`XCrawl API returned invalid response: ${message}`);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
function normalizeCount(value: number | undefined): number {
|
|
74
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
75
|
-
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
76
|
function hostnameOf(url: string): string {
|
|
79
77
|
try {
|
|
80
78
|
return new URL(url).hostname.toLowerCase();
|
|
@@ -96,23 +94,6 @@ function absolutizeLink(link: string): string {
|
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
// Normalize a shared domainFilter entry the same way Valyu does before
|
|
100
|
-
// matching: trim, lowercase, strip URL/paths/ports, validate the shape.
|
|
101
|
-
function normalizeDomain(value: string): string | null {
|
|
102
|
-
let input = value.trim().toLowerCase();
|
|
103
|
-
if (!input) return null;
|
|
104
|
-
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
105
|
-
if (!input) return null;
|
|
106
|
-
try {
|
|
107
|
-
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
108
|
-
input = parsed.hostname;
|
|
109
|
-
} catch {
|
|
110
|
-
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
111
|
-
}
|
|
112
|
-
input = input.replace(/^\.+|\.+$/g, "");
|
|
113
|
-
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
97
|
// XCrawl's SERP API has no server-side domain filter, so apply the shared
|
|
117
98
|
// include/exclude domainFilter locally instead of returning off-domain results.
|
|
118
99
|
function applyDomainFilter(results: SearchResponse["results"], domainFilter: NonNullable<SearchOptions["domainFilter"]>): SearchResponse["results"] {
|
|
@@ -175,17 +156,9 @@ function parseResponse(value: unknown): SearchResponse["results"] {
|
|
|
175
156
|
return results;
|
|
176
157
|
}
|
|
177
158
|
|
|
178
|
-
function buildAnswer(results: SearchResponse["results"]): string {
|
|
179
|
-
return results
|
|
180
|
-
.map((result) => result.snippet
|
|
181
|
-
? `${result.snippet}\nSource: ${result.title} (${result.url})`
|
|
182
|
-
: `Source: ${result.title} (${result.url})`)
|
|
183
|
-
.join("\n\n");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
159
|
export async function searchWithXCrawl(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
|
|
187
160
|
const apiKey = await getApiKey(options.signal);
|
|
188
|
-
const numResults =
|
|
161
|
+
const numResults = normalizeSearchResultCount(options.numResults);
|
|
189
162
|
if (!apiKey) {
|
|
190
163
|
throw new Error(
|
|
191
164
|
"XCrawl search requires an API key. Set xcrawlApiKey in " + CONFIG_PATH +
|
|
@@ -258,7 +231,7 @@ export async function searchWithXCrawl(query: string, options: SearchOptions = {
|
|
|
258
231
|
const filtered = (options.domainFilter?.length ? applyDomainFilter(results, options.domainFilter) : results).slice(0, numResults);
|
|
259
232
|
|
|
260
233
|
return {
|
|
261
|
-
answer:
|
|
234
|
+
answer: formatSearchResultsAsAnswer(filtered),
|
|
262
235
|
results: filtered,
|
|
263
236
|
};
|
|
264
237
|
}
|