tau-all-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/extensions/answer.ts +601 -0
- package/extensions/branch-term.ts +405 -0
- package/extensions/btw.ts +444 -0
- package/extensions/ghostty.ts +301 -0
- package/extensions/git-diff-stats.ts +277 -0
- package/extensions/git-pr-status.ts +286 -0
- package/extensions/insights.ts +2367 -0
- package/extensions/interlude.ts +144 -0
- package/extensions/loop.ts +529 -0
- package/extensions/memory.ts +1889 -0
- package/extensions/notify.ts +161 -0
- package/extensions/openai-fast.ts +227 -0
- package/extensions/openai-verbosity.ts +223 -0
- package/extensions/review.ts +4347 -0
- package/extensions/sandbox/index.ts +2578 -0
- package/extensions/telegram/README.md +81 -0
- package/extensions/telegram/daemon.mjs +1942 -0
- package/extensions/telegram/index.ts +1073 -0
- package/extensions/telegram/message-text.d.mts +1 -0
- package/extensions/telegram/message-text.mjs +16 -0
- package/extensions/usage/anthropic.ts +198 -0
- package/extensions/usage/github-copilot.ts +204 -0
- package/extensions/usage/google-gemini-cli.ts +232 -0
- package/extensions/usage/index.ts +2310 -0
- package/extensions/usage/minimax.ts +208 -0
- package/extensions/usage/openai-codex.ts +180 -0
- package/extensions/usage/openrouter.ts +168 -0
- package/extensions/usage/providers.ts +26 -0
- package/extensions/usage/shared.ts +156 -0
- package/extensions/usage/types.ts +53 -0
- package/extensions/usage/zai.ts +186 -0
- package/extensions/websearch/README.md +68 -0
- package/extensions/websearch/browser/chromium.ts +330 -0
- package/extensions/websearch/browser/discovery.ts +75 -0
- package/extensions/websearch/browser/firefox.ts +150 -0
- package/extensions/websearch/browser/sqlite.ts +38 -0
- package/extensions/websearch/config.ts +76 -0
- package/extensions/websearch/index.ts +312 -0
- package/extensions/websearch/normalize.ts +155 -0
- package/extensions/websearch/providers/anthropic.pi.ts +139 -0
- package/extensions/websearch/providers/gemini.browser.ts +185 -0
- package/extensions/websearch/providers/gemini.pi.ts +108 -0
- package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
- package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
- package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
- package/extensions/websearch/providers/pi-model.shared.ts +86 -0
- package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
- package/extensions/websearch/providers/shared.ts +76 -0
- package/extensions/websearch/types.ts +52 -0
- package/extensions/worktree.ts +2223 -0
- package/package.json +76 -0
- package/skills/browser-tools/SKILL.md +253 -0
- package/skills/browser-tools/scripts/browser-content.js +100 -0
- package/skills/browser-tools/scripts/browser-cookies.js +33 -0
- package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
- package/skills/browser-tools/scripts/browser-eval.js +75 -0
- package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
- package/skills/browser-tools/scripts/browser-nav.js +28 -0
- package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
- package/skills/browser-tools/scripts/browser-pick.js +133 -0
- package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
- package/skills/browser-tools/scripts/browser-start.js +281 -0
- package/skills/browser-tools/scripts/browser-watch.js +321 -0
- package/skills/browser-tools/scripts/utils.js +60 -0
- package/skills/git-clean-history/SKILL.md +45 -0
- package/skills/git-commit/SKILL.md +55 -0
- package/skills/homeassistant-ops/SKILL.md +184 -0
- package/skills/homeassistant-ops/references/api.md +64 -0
- package/skills/homeassistant-ops/references/id_conventions.md +71 -0
- package/skills/homeassistant-ops/references/playbook.md +150 -0
- package/skills/homeassistant-ops/scripts/ha_ops.js +2942 -0
- package/skills/openscad/SKILL.md +149 -0
- package/skills/openscad/examples/parametric_box.scad +92 -0
- package/skills/openscad/examples/phone_stand.scad +95 -0
- package/skills/openscad/scripts/common.sh +49 -0
- package/skills/openscad/scripts/export-stl.sh +56 -0
- package/skills/openscad/scripts/extract-params.sh +147 -0
- package/skills/openscad/scripts/multi-preview.sh +68 -0
- package/skills/openscad/scripts/preview.sh +74 -0
- package/skills/openscad/scripts/render-with-params.sh +91 -0
- package/skills/openscad/scripts/validate.sh +46 -0
- package/skills/oracle/SKILL.md +64 -0
- package/skills/oracle/scripts/oracle +249 -0
- package/skills/oracle/scripts/oracle-bundle +227 -0
- package/skills/sentry/SKILL.md +198 -0
- package/skills/sentry/lib/auth.js +99 -0
- package/skills/sentry/scripts/fetch-event.js +325 -0
- package/skills/sentry/scripts/fetch-issue.js +368 -0
- package/skills/sentry/scripts/list-issues.js +245 -0
- package/skills/sentry/scripts/search-events.js +291 -0
- package/skills/sentry/scripts/search-logs.js +234 -0
- package/skills/update-changelog/SKILL.md +135 -0
- package/skills/web-design/SKILL.md +117 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
defineTool,
|
|
4
|
+
getMarkdownTheme,
|
|
5
|
+
keyHint,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionContext,
|
|
8
|
+
} from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
|
|
12
|
+
import { createBrowserSession, discoverProfiles } from "./browser/discovery.js";
|
|
13
|
+
import { loadConfig } from "./config.js";
|
|
14
|
+
import { renderSearchResultMarkdown } from "./normalize.js";
|
|
15
|
+
import { isPiAnthropicModel, searchWithPiAnthropic } from "./providers/anthropic.pi.js";
|
|
16
|
+
import { browserGemini } from "./providers/gemini.browser.js";
|
|
17
|
+
import { isPiGeminiModel, searchWithPiGemini } from "./providers/gemini.pi.js";
|
|
18
|
+
import { browserOpenAICodex } from "./providers/openai-codex.browser.js";
|
|
19
|
+
import { isPiOpenAICodexModel, searchWithPiOpenAICodex } from "./providers/openai-codex.pi.js";
|
|
20
|
+
import type { PiModelSelection } from "./providers/pi-model.shared.js";
|
|
21
|
+
import { selectCurrentPiModel, selectFallbackPiModel } from "./providers/pi-model.shared.js";
|
|
22
|
+
import type {
|
|
23
|
+
BrowserProfile,
|
|
24
|
+
WebsearchAuthSource,
|
|
25
|
+
WebsearchBackendId,
|
|
26
|
+
WebsearchBrowserFamily,
|
|
27
|
+
WebsearchConfig,
|
|
28
|
+
WebsearchResult,
|
|
29
|
+
WebsearchRouteId,
|
|
30
|
+
} from "./types.js";
|
|
31
|
+
|
|
32
|
+
const PI_ROUTE_HANDLERS: Record<`pi:${WebsearchBackendId}`, PiRouteHandler> = {
|
|
33
|
+
"pi:openai-codex": {
|
|
34
|
+
predicate: isPiOpenAICodexModel,
|
|
35
|
+
search: searchWithPiOpenAICodex,
|
|
36
|
+
},
|
|
37
|
+
"pi:anthropic": {
|
|
38
|
+
predicate: isPiAnthropicModel,
|
|
39
|
+
search: searchWithPiAnthropic,
|
|
40
|
+
},
|
|
41
|
+
"pi:gemini": {
|
|
42
|
+
predicate: isPiGeminiModel,
|
|
43
|
+
search: searchWithPiGemini,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
interface SearchSummary {
|
|
48
|
+
result: string;
|
|
49
|
+
route: WebsearchRouteId;
|
|
50
|
+
backend: WebsearchBackendId;
|
|
51
|
+
authSource: WebsearchAuthSource;
|
|
52
|
+
browserName?: string;
|
|
53
|
+
profile?: string;
|
|
54
|
+
accountLabel?: string;
|
|
55
|
+
sources: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface PiRouteHandler {
|
|
59
|
+
predicate: (model: Model<Api>) => boolean;
|
|
60
|
+
search: (
|
|
61
|
+
selection: PiModelSelection,
|
|
62
|
+
query: string,
|
|
63
|
+
signal?: AbortSignal,
|
|
64
|
+
) => Promise<WebsearchResult>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toSearchSummary(route: WebsearchRouteId, result: WebsearchResult): SearchSummary {
|
|
68
|
+
return {
|
|
69
|
+
result: renderSearchResultMarkdown(result.answer, result.sources),
|
|
70
|
+
route,
|
|
71
|
+
backend: result.backend,
|
|
72
|
+
authSource: result.authSource,
|
|
73
|
+
browserName: result.browserName,
|
|
74
|
+
profile: result.profile,
|
|
75
|
+
accountLabel: result.accountLabel,
|
|
76
|
+
sources: result.sources.length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function currentPiRoute(ctx: ExtensionContext): `pi:${WebsearchBackendId}` | null {
|
|
81
|
+
if (!ctx.model) return null;
|
|
82
|
+
|
|
83
|
+
for (const [route, handler] of Object.entries(PI_ROUTE_HANDLERS) as Array<
|
|
84
|
+
[`pi:${WebsearchBackendId}`, PiRouteHandler]
|
|
85
|
+
>) {
|
|
86
|
+
if (handler.predicate(ctx.model)) return route;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function executePiRoute(
|
|
93
|
+
route: `pi:${WebsearchBackendId}`,
|
|
94
|
+
ctx: ExtensionContext,
|
|
95
|
+
query: string,
|
|
96
|
+
mode: "current" | "fallback",
|
|
97
|
+
signal?: AbortSignal,
|
|
98
|
+
): Promise<SearchSummary | null> {
|
|
99
|
+
const handler = PI_ROUTE_HANDLERS[route];
|
|
100
|
+
const selection =
|
|
101
|
+
mode === "current"
|
|
102
|
+
? await selectCurrentPiModel(ctx, handler.predicate)
|
|
103
|
+
: await selectFallbackPiModel(ctx, handler.predicate);
|
|
104
|
+
|
|
105
|
+
return selection ? toSearchSummary(route, await handler.search(selection, query, signal)) : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function searchPiRoute(
|
|
109
|
+
route: `pi:${WebsearchBackendId}`,
|
|
110
|
+
ctx: ExtensionContext,
|
|
111
|
+
query: string,
|
|
112
|
+
signal?: AbortSignal,
|
|
113
|
+
): Promise<SearchSummary | null> {
|
|
114
|
+
if (route === currentPiRoute(ctx)) {
|
|
115
|
+
const current = await executePiRoute(route, ctx, query, "current", signal);
|
|
116
|
+
if (current) return current;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return executePiRoute(route, ctx, query, "fallback", signal);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isAbortError(error: unknown, signal?: AbortSignal): boolean {
|
|
123
|
+
return signal?.aborted === true || (error instanceof Error && error.name === "AbortError");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
127
|
+
if (!signal?.aborted) return;
|
|
128
|
+
throw signal.reason ?? new Error("The operation was aborted.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getBrowserProfiles(
|
|
132
|
+
cache: Partial<Record<WebsearchBrowserFamily, BrowserProfile[]>>,
|
|
133
|
+
family: WebsearchBrowserFamily,
|
|
134
|
+
config: WebsearchConfig,
|
|
135
|
+
): BrowserProfile[] {
|
|
136
|
+
const cached = cache[family];
|
|
137
|
+
if (cached) return cached;
|
|
138
|
+
|
|
139
|
+
const profiles = discoverProfiles([family], config.profiles);
|
|
140
|
+
cache[family] = profiles;
|
|
141
|
+
return profiles;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function searchBrowserRoute(
|
|
145
|
+
route: WebsearchRouteId,
|
|
146
|
+
config: WebsearchConfig,
|
|
147
|
+
query: string,
|
|
148
|
+
profileCache: Partial<Record<WebsearchBrowserFamily, BrowserProfile[]>>,
|
|
149
|
+
signal?: AbortSignal,
|
|
150
|
+
): Promise<SearchSummary | null> {
|
|
151
|
+
const profileFamily: WebsearchBrowserFamily = route.startsWith("firefox:")
|
|
152
|
+
? "firefox"
|
|
153
|
+
: "chromium";
|
|
154
|
+
const browserProvider = route.endsWith(":gemini") ? browserGemini : browserOpenAICodex;
|
|
155
|
+
const configuredProfileName = config.profiles[profileFamily];
|
|
156
|
+
const profiles = getBrowserProfiles(profileCache, profileFamily, config);
|
|
157
|
+
let lastError: string | null = null;
|
|
158
|
+
|
|
159
|
+
if (configuredProfileName && profiles.length === 0) {
|
|
160
|
+
throw new Error(`Configured ${profileFamily} profile was not found: ${configuredProfileName}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const profile of profiles) {
|
|
164
|
+
throwIfAborted(signal);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const session = createBrowserSession(profile, browserProvider.domains);
|
|
168
|
+
if (!session) {
|
|
169
|
+
if (configuredProfileName) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Configured ${profileFamily} profile has no usable session: ${configuredProfileName}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
return toSearchSummary(route, await browserProvider.search(session, query, signal));
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (isAbortError(error, signal)) throw error;
|
|
179
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (lastError) throw new Error(lastError);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function searchRoute(
|
|
188
|
+
route: WebsearchRouteId,
|
|
189
|
+
ctx: ExtensionContext,
|
|
190
|
+
config: WebsearchConfig,
|
|
191
|
+
query: string,
|
|
192
|
+
profileCache: Partial<Record<WebsearchBrowserFamily, BrowserProfile[]>>,
|
|
193
|
+
signal?: AbortSignal,
|
|
194
|
+
): Promise<SearchSummary | null> {
|
|
195
|
+
return route.startsWith("pi:")
|
|
196
|
+
? await searchPiRoute(route as `pi:${WebsearchBackendId}`, ctx, query, signal)
|
|
197
|
+
: await searchBrowserRoute(route, config, query, profileCache, signal);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runSearch(
|
|
201
|
+
ctx: ExtensionContext,
|
|
202
|
+
query: string,
|
|
203
|
+
signal?: AbortSignal,
|
|
204
|
+
): Promise<SearchSummary> {
|
|
205
|
+
const config = loadConfig();
|
|
206
|
+
const profileCache: Partial<Record<WebsearchBrowserFamily, BrowserProfile[]>> = {};
|
|
207
|
+
let lastError: string | null = null;
|
|
208
|
+
|
|
209
|
+
for (const route of config.routes) {
|
|
210
|
+
throwIfAborted(signal);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const result = await searchRoute(route, ctx, config, query, profileCache, signal);
|
|
214
|
+
if (result) return result;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (isAbortError(error, signal)) throw error;
|
|
217
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
throw new Error(lastError ?? "No configured websearch route is available.");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const COLLAPSED_RESULT_LINES = 10;
|
|
225
|
+
|
|
226
|
+
function trimTrailingEmptyLines(lines: string[]): string[] {
|
|
227
|
+
let end = lines.length;
|
|
228
|
+
while (end > 0 && lines[end - 1] === "") {
|
|
229
|
+
end--;
|
|
230
|
+
}
|
|
231
|
+
return lines.slice(0, end);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatWebsearchCall(query: string, theme: any): string {
|
|
235
|
+
let text = theme.fg("toolTitle", theme.bold("websearch"));
|
|
236
|
+
if (query) {
|
|
237
|
+
text += ` ${theme.fg("accent", query)}`;
|
|
238
|
+
}
|
|
239
|
+
return text;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatCollapsedWebsearchResult(resultText: string, theme: any): string {
|
|
243
|
+
const lines = trimTrailingEmptyLines(resultText.split("\n"));
|
|
244
|
+
if (lines.length === 0) {
|
|
245
|
+
return `\n${theme.fg("muted", "(no output)")}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const displayLines = lines.slice(0, COLLAPSED_RESULT_LINES);
|
|
249
|
+
const remaining = lines.length - displayLines.length;
|
|
250
|
+
|
|
251
|
+
let text = `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
|
252
|
+
if (remaining > 0) {
|
|
253
|
+
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default function (pi: ExtensionAPI) {
|
|
260
|
+
pi.registerTool(
|
|
261
|
+
defineTool({
|
|
262
|
+
name: "websearch",
|
|
263
|
+
label: "Websearch",
|
|
264
|
+
description: "Web search via Gemini, OpenAI, or Claude, leveraging Pi or browser sessions.",
|
|
265
|
+
promptSnippet:
|
|
266
|
+
"Search the web for current or external information unavailable in local files",
|
|
267
|
+
promptGuidelines: [
|
|
268
|
+
"Use this for recent facts, live service behavior, or external documentation that is not already present in the repo.",
|
|
269
|
+
"Do not use websearch when repository files or supplied context already answer the question.",
|
|
270
|
+
],
|
|
271
|
+
parameters: Type.Object({
|
|
272
|
+
query: Type.String({ description: "What to search for" }),
|
|
273
|
+
}),
|
|
274
|
+
renderShell: "self",
|
|
275
|
+
renderCall(args, theme) {
|
|
276
|
+
return new Text(formatWebsearchCall(args.query, theme), 0, 0);
|
|
277
|
+
},
|
|
278
|
+
renderResult(result, { expanded }, theme) {
|
|
279
|
+
const content = result.content.find((item) => item.type === "text");
|
|
280
|
+
const text = content?.type === "text" ? content.text : "";
|
|
281
|
+
|
|
282
|
+
if (!expanded) {
|
|
283
|
+
return new Text(formatCollapsedWebsearchResult(text, theme), 0, 0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!text.trim()) {
|
|
287
|
+
return new Text(`\n${theme.fg("muted", "(no output)")}`, 0, 0);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const container = new Container();
|
|
291
|
+
container.addChild(new Spacer(1));
|
|
292
|
+
container.addChild(new Markdown(text.trim(), 0, 0, getMarkdownTheme()));
|
|
293
|
+
return container;
|
|
294
|
+
},
|
|
295
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
296
|
+
const result = await runSearch(ctx, params.query, signal);
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: "text", text: result.result }],
|
|
299
|
+
details: {
|
|
300
|
+
route: result.route,
|
|
301
|
+
backend: result.backend,
|
|
302
|
+
authSource: result.authSource,
|
|
303
|
+
browserName: result.browserName,
|
|
304
|
+
profile: result.profile,
|
|
305
|
+
accountLabel: result.accountLabel,
|
|
306
|
+
sources: result.sources,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { BrowserCookie, WebsearchSource } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BROWSER_USER_AGENT =
|
|
4
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0";
|
|
5
|
+
|
|
6
|
+
export function buildCookieHeader(url: string, cookies: BrowserCookie[]): string {
|
|
7
|
+
return cookiesForUrl(url, cookies)
|
|
8
|
+
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
9
|
+
.join("; ");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hasCookie(url: string, cookies: BrowserCookie[], name: string): boolean {
|
|
13
|
+
return cookiesForUrl(url, cookies).some((cookie) => cookie.name === name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cookiesForUrl(url: string, cookies: BrowserCookie[]): BrowserCookie[] {
|
|
17
|
+
const target = new URL(url);
|
|
18
|
+
const requestPath = target.pathname || "/";
|
|
19
|
+
|
|
20
|
+
return [...cookies]
|
|
21
|
+
.filter((cookie) => {
|
|
22
|
+
return (
|
|
23
|
+
cookie.value.length > 0 &&
|
|
24
|
+
matchesCookieDomain(target.hostname, cookie.domain) &&
|
|
25
|
+
matchesCookiePath(requestPath, cookie.path) &&
|
|
26
|
+
(!cookie.secure || target.protocol === "https:")
|
|
27
|
+
);
|
|
28
|
+
})
|
|
29
|
+
.sort((left, right) => right.path.length - left.path.length);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matchesCookieDomain(hostname: string, domain: string): boolean {
|
|
33
|
+
const normalizedHost = hostname.toLowerCase();
|
|
34
|
+
const normalizedDomain = domain.toLowerCase();
|
|
35
|
+
|
|
36
|
+
if (normalizedDomain.startsWith(".")) {
|
|
37
|
+
const suffix = normalizedDomain.slice(1);
|
|
38
|
+
return normalizedHost === suffix || normalizedHost.endsWith(`.${suffix}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return normalizedHost === normalizedDomain;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function matchesCookiePath(requestPath: string, cookiePath: string): boolean {
|
|
45
|
+
const normalizedCookiePath = cookiePath || "/";
|
|
46
|
+
if (requestPath === normalizedCookiePath) return true;
|
|
47
|
+
if (!requestPath.startsWith(normalizedCookiePath)) return false;
|
|
48
|
+
if (normalizedCookiePath.endsWith("/")) return true;
|
|
49
|
+
return requestPath.charAt(normalizedCookiePath.length) === "/";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function browserHeaders(options: {
|
|
53
|
+
cookieHeader?: string;
|
|
54
|
+
origin: string;
|
|
55
|
+
referer: string;
|
|
56
|
+
accept?: string;
|
|
57
|
+
contentType?: string;
|
|
58
|
+
authorization?: string;
|
|
59
|
+
extra?: Record<string, string | undefined>;
|
|
60
|
+
}): Record<string, string> {
|
|
61
|
+
const headers: Record<string, string> = {
|
|
62
|
+
"User-Agent": DEFAULT_BROWSER_USER_AGENT,
|
|
63
|
+
Accept: options.accept ?? "application/json,text/plain,*/*",
|
|
64
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
65
|
+
Referer: options.referer,
|
|
66
|
+
Origin: options.origin,
|
|
67
|
+
"Sec-Fetch-Dest": "empty",
|
|
68
|
+
"Sec-Fetch-Mode": "cors",
|
|
69
|
+
"Sec-Fetch-Site": "same-origin",
|
|
70
|
+
Pragma: "no-cache",
|
|
71
|
+
"Cache-Control": "no-cache",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (options.cookieHeader) headers.Cookie = options.cookieHeader;
|
|
75
|
+
if (options.contentType) headers["Content-Type"] = options.contentType;
|
|
76
|
+
if (options.authorization) headers.Authorization = options.authorization;
|
|
77
|
+
|
|
78
|
+
if (options.extra) {
|
|
79
|
+
for (const [key, value] of Object.entries(options.extra)) {
|
|
80
|
+
if (value) headers[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return headers;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function normalizeSourceTitle(url: string, title?: string | null): string {
|
|
88
|
+
const trimmed = title?.trim();
|
|
89
|
+
if (trimmed) return trimmed;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
return parsed.hostname.replace(/^www\./, "");
|
|
94
|
+
} catch {
|
|
95
|
+
return url;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function extractMarkdownSources(text: string): WebsearchSource[] {
|
|
100
|
+
const sources: WebsearchSource[] = [];
|
|
101
|
+
const seen = new Set<string>();
|
|
102
|
+
|
|
103
|
+
const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
|
|
104
|
+
for (const match of text.matchAll(markdownLinkPattern)) {
|
|
105
|
+
const url = match[2];
|
|
106
|
+
if (!url || seen.has(url)) continue;
|
|
107
|
+
seen.add(url);
|
|
108
|
+
sources.push({ title: normalizeSourceTitle(url, match[1]), url });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const urlPattern = /https?:\/\/[^\s)\]}>,]+/g;
|
|
112
|
+
for (const match of text.matchAll(urlPattern)) {
|
|
113
|
+
const url = match[0].replace(/[.,;:]+$/, "");
|
|
114
|
+
if (!url || seen.has(url)) continue;
|
|
115
|
+
seen.add(url);
|
|
116
|
+
sources.push({ title: normalizeSourceTitle(url), url });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return sources;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function dedupeSources(sources: WebsearchSource[]): WebsearchSource[] {
|
|
123
|
+
const deduped = new Map<string, WebsearchSource>();
|
|
124
|
+
|
|
125
|
+
for (const source of sources) {
|
|
126
|
+
if (!source.url) continue;
|
|
127
|
+
if (!deduped.has(source.url)) {
|
|
128
|
+
deduped.set(source.url, {
|
|
129
|
+
title: normalizeSourceTitle(source.url, source.title),
|
|
130
|
+
url: source.url,
|
|
131
|
+
snippet: source.snippet,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return [...deduped.values()];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function renderSearchResultMarkdown(answer: string, sources: WebsearchSource[]): string {
|
|
140
|
+
const trimmedAnswer = answer.trim();
|
|
141
|
+
if (sources.length === 0) return trimmedAnswer;
|
|
142
|
+
|
|
143
|
+
const embeddedSources = new Set(
|
|
144
|
+
extractMarkdownSources(trimmedAnswer).map((source) => source.url),
|
|
145
|
+
);
|
|
146
|
+
const extraSources = sources.filter((source) => !embeddedSources.has(source.url));
|
|
147
|
+
if (extraSources.length === 0) return trimmedAnswer;
|
|
148
|
+
|
|
149
|
+
const lines = [trimmedAnswer, "", "Sources:"];
|
|
150
|
+
for (const source of extraSources) {
|
|
151
|
+
lines.push(`- [${normalizeSourceTitle(source.url, source.title)}](${source.url})`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return lines.join("\n").trim();
|
|
155
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
2
|
+
|
|
3
|
+
import { dedupeSources, extractMarkdownSources } from "../normalize.js";
|
|
4
|
+
import type { WebsearchResult } from "../types.js";
|
|
5
|
+
import type { PiModelSelection } from "./pi-model.shared.js";
|
|
6
|
+
import { buildWebsearchPrompt, WEBSEARCH_SYSTEM_PROMPT } from "./search-prompt.shared.js";
|
|
7
|
+
import { fetchText, withTimeout } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
export async function searchWithPiAnthropic(
|
|
10
|
+
selection: PiModelSelection,
|
|
11
|
+
query: string,
|
|
12
|
+
signal?: AbortSignal,
|
|
13
|
+
): Promise<WebsearchResult> {
|
|
14
|
+
const payload = await fetchText(resolveAnthropicMessagesUrl(selection.model.baseUrl), {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: buildAnthropicHeaders(selection),
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
model: selection.model.id,
|
|
19
|
+
max_tokens: 1800,
|
|
20
|
+
system: buildAnthropicSystem(getAnthropicCredential(selection)),
|
|
21
|
+
tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 5 }],
|
|
22
|
+
messages: [{ role: "user", content: buildWebsearchPrompt(query) }],
|
|
23
|
+
}),
|
|
24
|
+
signal: withTimeout(signal, 120_000),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let parsed: Record<string, unknown>;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(payload) as Record<string, unknown>;
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error("Anthropic returned non-JSON response.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const answer = (Array.isArray(parsed.content) ? parsed.content : [])
|
|
35
|
+
.filter(
|
|
36
|
+
(item) =>
|
|
37
|
+
item && typeof item === "object" && (item as Record<string, unknown>).type === "text",
|
|
38
|
+
)
|
|
39
|
+
.map((item) => (item as Record<string, unknown>).text)
|
|
40
|
+
.filter((text): text is string => typeof text === "string")
|
|
41
|
+
.join("\n\n")
|
|
42
|
+
.trim();
|
|
43
|
+
|
|
44
|
+
if (!answer) {
|
|
45
|
+
throw new Error("Anthropic returned no text content.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
backend: "anthropic",
|
|
50
|
+
authSource: "pi",
|
|
51
|
+
answer,
|
|
52
|
+
sources: dedupeSources(extractMarkdownSources(answer)),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildAnthropicHeaders(selection: PiModelSelection): Record<string, string> {
|
|
57
|
+
const credential = getAnthropicCredential(selection);
|
|
58
|
+
const headers = { ...selection.headers };
|
|
59
|
+
|
|
60
|
+
if (isAnthropicOAuthToken(credential)) {
|
|
61
|
+
return {
|
|
62
|
+
...headers,
|
|
63
|
+
...(hasHeader(headers, "authorization") || !credential
|
|
64
|
+
? {}
|
|
65
|
+
: { authorization: `Bearer ${credential}` }),
|
|
66
|
+
"anthropic-version": "2023-06-01",
|
|
67
|
+
"anthropic-beta":
|
|
68
|
+
"claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14,web-search-2025-03-05",
|
|
69
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
70
|
+
"content-type": "application/json",
|
|
71
|
+
accept: "application/json",
|
|
72
|
+
"x-app": "cli",
|
|
73
|
+
"user-agent": "claude-cli/2.1.62",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...headers,
|
|
79
|
+
...(hasHeader(headers, "authorization") || hasHeader(headers, "x-api-key") || !credential
|
|
80
|
+
? {}
|
|
81
|
+
: { "x-api-key": credential }),
|
|
82
|
+
"anthropic-version": "2023-06-01",
|
|
83
|
+
"anthropic-beta": "web-search-2025-03-05",
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
accept: "application/json",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveAnthropicMessagesUrl(baseUrl?: string): string {
|
|
90
|
+
const normalized = String(baseUrl || "https://api.anthropic.com").replace(/\/+$/, "");
|
|
91
|
+
return normalized.endsWith("/v1/messages") ? normalized : `${normalized}/v1/messages`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildAnthropicSystem(apiKey?: string): string | Array<{ type: "text"; text: string }> {
|
|
95
|
+
if (!isAnthropicOAuthToken(apiKey)) return WEBSEARCH_SYSTEM_PROMPT;
|
|
96
|
+
|
|
97
|
+
return [
|
|
98
|
+
{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." },
|
|
99
|
+
{ type: "text", text: WEBSEARCH_SYSTEM_PROMPT },
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getAnthropicCredential(selection: PiModelSelection): string | undefined {
|
|
104
|
+
return (
|
|
105
|
+
selection.apiKey ??
|
|
106
|
+
getBearerToken(selection.headers) ??
|
|
107
|
+
getHeaderValue(selection.headers, "x-api-key")
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getBearerToken(headers?: Record<string, string>): string | undefined {
|
|
112
|
+
const authorization = getHeaderValue(headers, "authorization");
|
|
113
|
+
if (!authorization) return undefined;
|
|
114
|
+
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
115
|
+
return match?.[1];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
119
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getHeaderValue(
|
|
123
|
+
headers: Record<string, string> | undefined,
|
|
124
|
+
name: string,
|
|
125
|
+
): string | undefined {
|
|
126
|
+
if (!headers) return undefined;
|
|
127
|
+
const key = Object.keys(headers).find(
|
|
128
|
+
(headerName) => headerName.toLowerCase() === name.toLowerCase(),
|
|
129
|
+
);
|
|
130
|
+
return key ? headers[key] : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isAnthropicOAuthToken(apiKey?: string): boolean {
|
|
134
|
+
return Boolean(apiKey?.includes("sk-ant-oat"));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isPiAnthropicModel(model: Model<Api>): boolean {
|
|
138
|
+
return model.api === "anthropic-messages" && model.provider === "anthropic";
|
|
139
|
+
}
|