tau-coding-agent 0.1.6 → 0.2.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/README.md +15 -12
- package/extensions/answer.ts +129 -79
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +17 -2
- package/extensions/caffeinate/README.md +5 -0
- package/extensions/caffeinate/index.ts +144 -0
- package/extensions/fast.ts +292 -0
- package/extensions/ghostty.ts +214 -211
- package/extensions/git-diff-stats.ts +124 -84
- package/extensions/git-pr-status.ts +274 -208
- package/extensions/insights.ts +109 -191
- package/extensions/loop.ts +149 -145
- package/extensions/memory.ts +138 -86
- package/extensions/notify.ts +14 -26
- package/extensions/openai-verbosity.ts +108 -42
- package/extensions/review/fix.ts +15 -6
- package/extensions/review/git.ts +93 -103
- package/extensions/review/index.ts +60 -62
- package/extensions/review/interrupt.ts +117 -0
- package/extensions/review/message-queue.ts +45 -12
- package/extensions/review/models.ts +79 -3
- package/extensions/review/prompts.ts +41 -38
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- package/extensions/review/triage.ts +41 -44
- package/extensions/sandbox/bash.ts +775 -0
- package/extensions/sandbox/command.ts +605 -0
- package/extensions/sandbox/config.ts +764 -0
- package/extensions/sandbox/index.ts +132 -3338
- package/extensions/sandbox/permissions/dialog.ts +118 -0
- package/extensions/sandbox/permissions/filesystem.ts +559 -0
- package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
- package/extensions/sandbox/permissions/network.ts +164 -0
- package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
- package/extensions/sandbox/runtime.ts +616 -0
- package/extensions/stash.ts +28 -15
- package/extensions/subagent/README.md +73 -0
- package/extensions/subagent/index.ts +822 -0
- package/extensions/subagent/interrupt.ts +117 -0
- package/extensions/subagent/permissions.ts +101 -0
- package/extensions/subagent/rpc.ts +177 -0
- package/extensions/tool-display-mode.ts +267 -64
- package/extensions/usage/index.ts +249 -356
- package/extensions/usage/openrouter.ts +10 -2
- package/extensions/websearch/README.md +12 -47
- package/extensions/websearch/config.ts +5 -2
- package/extensions/websearch/index.ts +94 -109
- package/extensions/websearch/output.ts +51 -0
- package/extensions/websearch/providers/anthropic.pi.ts +27 -44
- package/extensions/websearch/providers/gemini.browser.ts +68 -77
- package/extensions/websearch/providers/gemini.pi.ts +17 -17
- package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
- package/extensions/websearch/providers/pi-model.shared.ts +33 -39
- package/extensions/websearch/providers/shared.ts +87 -2
- package/extensions/websearch/types.ts +0 -3
- package/extensions/worktree.ts +132 -172
- package/package.json +10 -5
- package/skills/browser-tools/SKILL.md +29 -234
- package/skills/browser-tools/references/cookies.md +36 -0
- package/skills/browser-tools/references/interaction.md +90 -0
- package/skills/browser-tools/references/logging.md +34 -0
- package/skills/git-clean-history/SKILL.md +6 -6
- package/skills/git-commit/SKILL.md +5 -3
- package/skills/github-pull-request/SKILL.md +60 -0
- package/skills/github-pull-request/references/create.md +40 -0
- package/skills/github-pull-request/references/stewardship.md +60 -0
- package/skills/oracle/SKILL.md +3 -3
- package/skills/oracle/scripts/oracle +10 -1
- package/skills/sentry/SKILL.md +15 -185
- package/skills/sentry/references/events.md +79 -0
- package/skills/sentry/references/issues.md +62 -0
- package/skills/sentry/references/logs.md +46 -0
- package/skills/update-changelog/SKILL.md +27 -121
- package/skills/web-design/SKILL.md +16 -105
- package/themes/tau-dark.json +4 -0
- package/extensions/openai-fast.ts +0 -229
- package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
- package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
|
@@ -95,8 +95,16 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
|
|
|
95
95
|
throw keyResult.reason;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
const totalCredits = readNumber(creditsPayload
|
|
99
|
-
const totalUsage = readNumber(creditsPayload
|
|
98
|
+
const totalCredits = readNumber(creditsPayload?.data?.total_credits);
|
|
99
|
+
const totalUsage = readNumber(creditsPayload?.data?.total_usage);
|
|
100
|
+
if (
|
|
101
|
+
totalCredits === undefined ||
|
|
102
|
+
totalCredits < 0 ||
|
|
103
|
+
totalUsage === undefined ||
|
|
104
|
+
totalUsage < 0
|
|
105
|
+
) {
|
|
106
|
+
throw new Error("OpenRouter returned invalid credit totals.");
|
|
107
|
+
}
|
|
100
108
|
const balance = Math.max(0, totalCredits - totalUsage);
|
|
101
109
|
|
|
102
110
|
const keyLimit = readNumber(keyPayload?.data?.limit);
|
|
@@ -1,36 +1,14 @@
|
|
|
1
1
|
# websearch
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Search the web with Gemini, OpenAI, or Claude using Pi credentials or existing browser sessions. The `websearch` tool tries the configured routes and falls back when a search fails.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Configuration
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- `pi:anthropic`
|
|
9
|
-
- `pi:gemini`
|
|
10
|
-
- `firefox:gemini`
|
|
11
|
-
- `firefox:openai-codex`
|
|
12
|
-
- `chromium:gemini`
|
|
13
|
-
- `chromium:openai-codex`
|
|
14
|
-
|
|
15
|
-
## Config
|
|
16
|
-
|
|
17
|
-
Optional config file:
|
|
18
|
-
|
|
19
|
-
- `~/.pi/agent/websearch.json`
|
|
20
|
-
|
|
21
|
-
Example:
|
|
7
|
+
Optionally create `websearch.json` in your Pi agent directory (normally `~/.pi/agent`):
|
|
22
8
|
|
|
23
9
|
```json
|
|
24
10
|
{
|
|
25
|
-
"routes": [
|
|
26
|
-
"pi:openai-codex",
|
|
27
|
-
"pi:anthropic",
|
|
28
|
-
"pi:gemini",
|
|
29
|
-
"firefox:gemini",
|
|
30
|
-
"firefox:openai-codex",
|
|
31
|
-
"chromium:gemini",
|
|
32
|
-
"chromium:openai-codex"
|
|
33
|
-
],
|
|
11
|
+
"routes": ["pi:openai-codex", "pi:anthropic", "pi:gemini", "firefox:gemini", "chromium:gemini"],
|
|
34
12
|
"profiles": {
|
|
35
13
|
"firefox": "default-release",
|
|
36
14
|
"chromium": "Default"
|
|
@@ -38,31 +16,18 @@ Example:
|
|
|
38
16
|
}
|
|
39
17
|
```
|
|
40
18
|
|
|
41
|
-
|
|
19
|
+
The routes shown are the default order. Set `routes` to choose which routes to use and their order.
|
|
42
20
|
|
|
43
|
-
|
|
44
|
-
- if `routes` is set, only those routes are tried, in that order
|
|
45
|
-
- Pi routes try the current Pi model first within the matching provider family, then fall back to another available model from that family
|
|
21
|
+
Pi routes use Pi's authentication. Browser routes use existing Google sessions for Gemini. Browser profiles are discovered automatically unless pinned with `profiles`.
|
|
46
22
|
|
|
47
|
-
|
|
23
|
+
Browser access supports Firefox and Chromium-family browsers on macOS and Linux. macOS may prompt for Keychain access to decrypt cookies.
|
|
48
24
|
|
|
49
25
|
## Usage
|
|
50
26
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Examples:
|
|
56
|
-
|
|
57
|
-
- `websearch({ query: "latest Todoist release notes" })`
|
|
58
|
-
- `websearch({ query: "how does Pi compaction work" })`
|
|
27
|
+
```js
|
|
28
|
+
websearch({ query: "how does Pi compaction work" });
|
|
29
|
+
```
|
|
59
30
|
|
|
60
|
-
##
|
|
31
|
+
## Output
|
|
61
32
|
|
|
62
|
-
|
|
63
|
-
- Browser routes use existing local browser sessions.
|
|
64
|
-
- Browser profiles are auto-discovered unless pinned in config.
|
|
65
|
-
- Supported Chromium-family browsers are Chromium, Chrome, Brave, and Edge.
|
|
66
|
-
- Browser discovery currently supports macOS and Linux, not Windows.
|
|
67
|
-
- On macOS, Chromium-based browsers may prompt for Keychain access so cookies can be decrypted.
|
|
68
|
-
- If one route fails, the extension falls through to the next one.
|
|
33
|
+
Output is limited to 2,000 lines or 50 KB. Longer results include a path to the full text, including sources, in a temporary file.
|
|
@@ -11,9 +11,7 @@ const DEFAULT_ROUTES: WebsearchRouteId[] = [
|
|
|
11
11
|
"pi:anthropic",
|
|
12
12
|
"pi:gemini",
|
|
13
13
|
"firefox:gemini",
|
|
14
|
-
"firefox:openai-codex",
|
|
15
14
|
"chromium:gemini",
|
|
16
|
-
"chromium:openai-codex",
|
|
17
15
|
];
|
|
18
16
|
|
|
19
17
|
function sanitizeRoutes(value: unknown): WebsearchRouteId[] {
|
|
@@ -26,6 +24,11 @@ function sanitizeRoutes(value: unknown): WebsearchRouteId[] {
|
|
|
26
24
|
const result: WebsearchRouteId[] = [];
|
|
27
25
|
|
|
28
26
|
for (const item of value) {
|
|
27
|
+
if (item === "firefox:openai-codex" || item === "chromium:openai-codex") {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Invalid websearch route: ${item} (removed). Use pi:openai-codex and sign in with Pi /login.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
29
32
|
if (typeof item !== "string" || !valid.has(item as WebsearchRouteId)) {
|
|
30
33
|
throw new Error(`Invalid websearch route: ${String(item)}`);
|
|
31
34
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_MAX_BYTES,
|
|
4
|
+
DEFAULT_MAX_LINES,
|
|
3
5
|
defineTool,
|
|
6
|
+
formatSize,
|
|
4
7
|
getMarkdownTheme,
|
|
5
8
|
keyHint,
|
|
6
9
|
type ExtensionAPI,
|
|
@@ -12,15 +15,15 @@ import { Type } from "typebox";
|
|
|
12
15
|
import { createBrowserSession, discoverProfiles } from "./browser/discovery.js";
|
|
13
16
|
import { loadConfig } from "./config.js";
|
|
14
17
|
import { renderSearchResultMarkdown } from "./normalize.js";
|
|
18
|
+
import { limitOutput } from "./output.js";
|
|
15
19
|
import { isPiAnthropicModel, searchWithPiAnthropic } from "./providers/anthropic.pi.js";
|
|
16
20
|
import { browserGemini } from "./providers/gemini.browser.js";
|
|
17
21
|
import { isPiGeminiModel, searchWithPiGemini } from "./providers/gemini.pi.js";
|
|
18
|
-
import { browserOpenAICodex } from "./providers/openai-codex.browser.js";
|
|
19
22
|
import { isPiOpenAICodexModel, searchWithPiOpenAICodex } from "./providers/openai-codex.pi.js";
|
|
20
23
|
import type { PiModelSelection } from "./providers/pi-model.shared.js";
|
|
21
|
-
import {
|
|
24
|
+
import { getPiModelCandidates, selectNextPiModel } from "./providers/pi-model.shared.js";
|
|
25
|
+
import { isModelUnavailableError } from "./providers/shared.js";
|
|
22
26
|
import type {
|
|
23
|
-
BrowserProfile,
|
|
24
27
|
WebsearchAuthSource,
|
|
25
28
|
WebsearchBackendId,
|
|
26
29
|
WebsearchBrowserFamily,
|
|
@@ -29,10 +32,13 @@ import type {
|
|
|
29
32
|
WebsearchRouteId,
|
|
30
33
|
} from "./types.js";
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
type PiRouteId = `pi:${WebsearchBackendId}`;
|
|
36
|
+
|
|
37
|
+
const PI_ROUTE_HANDLERS: Record<PiRouteId, PiRouteHandler> = {
|
|
33
38
|
"pi:openai-codex": {
|
|
34
39
|
predicate: isPiOpenAICodexModel,
|
|
35
40
|
search: searchWithPiOpenAICodex,
|
|
41
|
+
fallbackModels: ["gpt-5.6-luna", "gpt-5.5"],
|
|
36
42
|
},
|
|
37
43
|
"pi:anthropic": {
|
|
38
44
|
predicate: isPiAnthropicModel,
|
|
@@ -51,12 +57,17 @@ interface SearchSummary {
|
|
|
51
57
|
authSource: WebsearchAuthSource;
|
|
52
58
|
browserName?: string;
|
|
53
59
|
profile?: string;
|
|
54
|
-
accountLabel?: string;
|
|
55
60
|
sources: number;
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
interface PiRoutePlan {
|
|
64
|
+
remaining: Model<Api>[];
|
|
65
|
+
attemptsLeft: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
interface PiRouteHandler {
|
|
59
69
|
predicate: (model: Model<Api>) => boolean;
|
|
70
|
+
fallbackModels?: readonly string[];
|
|
60
71
|
search: (
|
|
61
72
|
selection: PiModelSelection,
|
|
62
73
|
query: string,
|
|
@@ -64,6 +75,64 @@ interface PiRouteHandler {
|
|
|
64
75
|
) => Promise<WebsearchResult>;
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
async function runSearch(
|
|
79
|
+
ctx: ExtensionContext,
|
|
80
|
+
query: string,
|
|
81
|
+
signal?: AbortSignal,
|
|
82
|
+
): Promise<SearchSummary> {
|
|
83
|
+
const config = loadConfig();
|
|
84
|
+
const plans = new Map<PiRouteId, PiRoutePlan>();
|
|
85
|
+
let pending = config.routes;
|
|
86
|
+
let lastError: string | null = null;
|
|
87
|
+
|
|
88
|
+
while (pending.length > 0) {
|
|
89
|
+
const retries: PiRouteId[] = [];
|
|
90
|
+
for (const route of pending) {
|
|
91
|
+
throwIfAborted(signal);
|
|
92
|
+
let selection: PiModelSelection | null = null;
|
|
93
|
+
let plan: PiRoutePlan | undefined;
|
|
94
|
+
try {
|
|
95
|
+
if (isPiRoute(route)) {
|
|
96
|
+
const handler = PI_ROUTE_HANDLERS[route];
|
|
97
|
+
plan = plans.get(route);
|
|
98
|
+
if (!plan) {
|
|
99
|
+
const remaining = getPiModelCandidates(ctx, handler.predicate, handler.fallbackModels);
|
|
100
|
+
plan = { remaining, attemptsLeft: handler.fallbackModels ? remaining.length : 2 };
|
|
101
|
+
plans.set(route, plan);
|
|
102
|
+
}
|
|
103
|
+
selection = await selectNextPiModel(ctx, plan.remaining, signal);
|
|
104
|
+
if (!selection) continue;
|
|
105
|
+
throwIfAborted(signal);
|
|
106
|
+
plan.attemptsLeft--;
|
|
107
|
+
return toSearchSummary(route, await handler.search(selection, query, signal));
|
|
108
|
+
}
|
|
109
|
+
const result = await searchBrowserRoute(route, config, query, signal);
|
|
110
|
+
if (result) return result;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (isAbortError(error, signal)) throw error;
|
|
113
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
114
|
+
if (
|
|
115
|
+
isPiRoute(route) &&
|
|
116
|
+
selection &&
|
|
117
|
+
plan &&
|
|
118
|
+
plan.attemptsLeft > 0 &&
|
|
119
|
+
plan.remaining.length > 0 &&
|
|
120
|
+
isModelUnavailableError(error, selection.model)
|
|
121
|
+
) {
|
|
122
|
+
retries.push(route);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
pending = retries;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(lastError ?? "No configured websearch route is available.");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isPiRoute(route: WebsearchRouteId): route is PiRouteId {
|
|
133
|
+
return route.startsWith("pi:");
|
|
134
|
+
}
|
|
135
|
+
|
|
67
136
|
function toSearchSummary(route: WebsearchRouteId, result: WebsearchResult): SearchSummary {
|
|
68
137
|
return {
|
|
69
138
|
result: renderSearchResultMarkdown(result.answer, result.sources),
|
|
@@ -72,53 +141,10 @@ function toSearchSummary(route: WebsearchRouteId, result: WebsearchResult): Sear
|
|
|
72
141
|
authSource: result.authSource,
|
|
73
142
|
browserName: result.browserName,
|
|
74
143
|
profile: result.profile,
|
|
75
|
-
accountLabel: result.accountLabel,
|
|
76
144
|
sources: result.sources.length,
|
|
77
145
|
};
|
|
78
146
|
}
|
|
79
147
|
|
|
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
148
|
function isAbortError(error: unknown, signal?: AbortSignal): boolean {
|
|
123
149
|
return signal?.aborted === true || (error instanceof Error && error.name === "AbortError");
|
|
124
150
|
}
|
|
@@ -128,32 +154,17 @@ function throwIfAborted(signal?: AbortSignal): void {
|
|
|
128
154
|
throw signal.reason ?? new Error("The operation was aborted.");
|
|
129
155
|
}
|
|
130
156
|
|
|
131
|
-
async function getBrowserProfiles(
|
|
132
|
-
cache: Partial<Record<WebsearchBrowserFamily, BrowserProfile[]>>,
|
|
133
|
-
family: WebsearchBrowserFamily,
|
|
134
|
-
config: WebsearchConfig,
|
|
135
|
-
): Promise<BrowserProfile[]> {
|
|
136
|
-
const cached = cache[family];
|
|
137
|
-
if (cached) return cached;
|
|
138
|
-
|
|
139
|
-
const profiles = await discoverProfiles([family], config.profiles);
|
|
140
|
-
cache[family] = profiles;
|
|
141
|
-
return profiles;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
157
|
async function searchBrowserRoute(
|
|
145
158
|
route: WebsearchRouteId,
|
|
146
159
|
config: WebsearchConfig,
|
|
147
160
|
query: string,
|
|
148
|
-
profileCache: Partial<Record<WebsearchBrowserFamily, BrowserProfile[]>>,
|
|
149
161
|
signal?: AbortSignal,
|
|
150
162
|
): Promise<SearchSummary | null> {
|
|
151
163
|
const profileFamily: WebsearchBrowserFamily = route.startsWith("firefox:")
|
|
152
164
|
? "firefox"
|
|
153
165
|
: "chromium";
|
|
154
|
-
const browserProvider = route.endsWith(":gemini") ? browserGemini : browserOpenAICodex;
|
|
155
166
|
const configuredProfileName = config.profiles[profileFamily];
|
|
156
|
-
const profiles = await
|
|
167
|
+
const profiles = await discoverProfiles([profileFamily], config.profiles);
|
|
157
168
|
let lastError: string | null = null;
|
|
158
169
|
|
|
159
170
|
if (configuredProfileName && profiles.length === 0) {
|
|
@@ -164,7 +175,7 @@ async function searchBrowserRoute(
|
|
|
164
175
|
throwIfAborted(signal);
|
|
165
176
|
|
|
166
177
|
try {
|
|
167
|
-
const session = await createBrowserSession(profile,
|
|
178
|
+
const session = await createBrowserSession(profile, browserGemini.domains);
|
|
168
179
|
if (!session) {
|
|
169
180
|
if (configuredProfileName) {
|
|
170
181
|
throw new Error(
|
|
@@ -173,7 +184,7 @@ async function searchBrowserRoute(
|
|
|
173
184
|
}
|
|
174
185
|
continue;
|
|
175
186
|
}
|
|
176
|
-
return toSearchSummary(route, await
|
|
187
|
+
return toSearchSummary(route, await browserGemini.search(session, query, signal));
|
|
177
188
|
} catch (error) {
|
|
178
189
|
if (isAbortError(error, signal)) throw error;
|
|
179
190
|
lastError = error instanceof Error ? error.message : String(error);
|
|
@@ -184,43 +195,6 @@ async function searchBrowserRoute(
|
|
|
184
195
|
return null;
|
|
185
196
|
}
|
|
186
197
|
|
|
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
198
|
const COLLAPSED_RESULT_LINES = 10;
|
|
225
199
|
|
|
226
200
|
function trimTrailingEmptyLines(lines: string[]): string[] {
|
|
@@ -261,11 +235,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
261
235
|
defineTool({
|
|
262
236
|
name: "websearch",
|
|
263
237
|
label: "Websearch",
|
|
264
|
-
description:
|
|
265
|
-
promptSnippet:
|
|
266
|
-
"Search the web for current or external information unavailable in local files",
|
|
238
|
+
description: `Search the web for sourced answers. Output is limited to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} (whichever is hit first), including the truncation notice. Full truncated output is saved to a temporary file.`,
|
|
239
|
+
promptSnippet: "Discover web sources for current or external information",
|
|
267
240
|
promptGuidelines: [
|
|
268
241
|
"Use websearch for recent facts, live service behavior, or external documentation that is not already present in the repo.",
|
|
242
|
+
"When asked to read a supplied URL, fetch that page directly. Use websearch to discover sources or independently verify information.",
|
|
269
243
|
"Do not use websearch when repository files or supplied context already answer the question.",
|
|
270
244
|
],
|
|
271
245
|
parameters: Type.Object({
|
|
@@ -293,16 +267,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
267
|
return container;
|
|
294
268
|
},
|
|
295
269
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
296
|
-
|
|
270
|
+
let result: SearchSummary;
|
|
271
|
+
try {
|
|
272
|
+
result = await runSearch(ctx, params.query, signal);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (isAbortError(error, signal)) throw error;
|
|
275
|
+
const output = await limitOutput(
|
|
276
|
+
error instanceof Error ? error.message : String(error),
|
|
277
|
+
signal,
|
|
278
|
+
);
|
|
279
|
+
throw new Error(output.text);
|
|
280
|
+
}
|
|
281
|
+
const output = await limitOutput(result.result, signal);
|
|
297
282
|
return {
|
|
298
|
-
content: [{ type: "text", text:
|
|
283
|
+
content: [{ type: "text", text: output.text }],
|
|
299
284
|
details: {
|
|
285
|
+
...output.details,
|
|
300
286
|
route: result.route,
|
|
301
287
|
backend: result.backend,
|
|
302
288
|
authSource: result.authSource,
|
|
303
289
|
browserName: result.browserName,
|
|
304
290
|
profile: result.profile,
|
|
305
|
-
accountLabel: result.accountLabel,
|
|
306
291
|
sources: result.sources,
|
|
307
292
|
},
|
|
308
293
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_MAX_BYTES,
|
|
6
|
+
DEFAULT_MAX_LINES,
|
|
7
|
+
formatSize,
|
|
8
|
+
truncateHead,
|
|
9
|
+
withFileMutationQueue,
|
|
10
|
+
type TruncationResult,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
interface OutputDetails {
|
|
14
|
+
truncation?: Omit<TruncationResult, "content">;
|
|
15
|
+
fullOutputPath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Limit the complete model-facing text; retain full output only in a private, published file. */
|
|
19
|
+
export async function limitOutput(
|
|
20
|
+
output: string,
|
|
21
|
+
signal?: AbortSignal,
|
|
22
|
+
): Promise<{ text: string; details: OutputDetails }> {
|
|
23
|
+
signal?.throwIfAborted();
|
|
24
|
+
if (!truncateHead(output).truncated) return { text: output, details: {} };
|
|
25
|
+
|
|
26
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "pi-websearch-"));
|
|
27
|
+
try {
|
|
28
|
+
const fullOutputPath = path.join(directory, "output.txt");
|
|
29
|
+
const notice = `\n\n[Output truncated (${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} limit). Full output saved to: ${fullOutputPath}]`;
|
|
30
|
+
const maxBytes = DEFAULT_MAX_BYTES - Buffer.byteLength(notice, "utf8");
|
|
31
|
+
const maxLines = DEFAULT_MAX_LINES - (notice.split("\n").length - 1);
|
|
32
|
+
if (maxBytes <= 0 || maxLines <= 0) {
|
|
33
|
+
throw new Error("Websearch output path is too long for the truncation notice.");
|
|
34
|
+
}
|
|
35
|
+
const { content, ...truncation } = truncateHead(output, { maxBytes, maxLines });
|
|
36
|
+
await withFileMutationQueue(fullOutputPath, async () => {
|
|
37
|
+
await writeFile(fullOutputPath, output, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
mode: 0o600,
|
|
40
|
+
flag: "wx",
|
|
41
|
+
signal,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
signal?.throwIfAborted();
|
|
45
|
+
// Published files remain readable after completion/shutdown, like native tool output files.
|
|
46
|
+
return { text: content + notice, details: { truncation, fullOutputPath } };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
await rm(directory, { recursive: true, force: true });
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -4,7 +4,7 @@ import { dedupeSources, extractMarkdownSources } from "../normalize.js";
|
|
|
4
4
|
import type { WebsearchResult } from "../types.js";
|
|
5
5
|
import type { PiModelSelection } from "./pi-model.shared.js";
|
|
6
6
|
import { buildWebsearchPrompt, WEBSEARCH_SYSTEM_PROMPT } from "./search-prompt.shared.js";
|
|
7
|
-
import { fetchText, withTimeout } from "./shared.js";
|
|
7
|
+
import { applyResolvedHeaders, fetchText, getResolvedHeader, withTimeout } from "./shared.js";
|
|
8
8
|
|
|
9
9
|
export async function searchWithPiAnthropic(
|
|
10
10
|
selection: PiModelSelection,
|
|
@@ -55,35 +55,33 @@ export async function searchWithPiAnthropic(
|
|
|
55
55
|
|
|
56
56
|
function buildAnthropicHeaders(selection: PiModelSelection): Record<string, string> {
|
|
57
57
|
const credential = getAnthropicCredential(selection);
|
|
58
|
-
const headers = { ...selection.headers };
|
|
59
|
-
|
|
60
58
|
if (isAnthropicOAuthToken(credential)) {
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
:
|
|
59
|
+
return applyResolvedHeaders(
|
|
60
|
+
{
|
|
61
|
+
...(credential ? { authorization: `Bearer ${credential}` } : {}),
|
|
62
|
+
"anthropic-version": "2023-06-01",
|
|
63
|
+
"anthropic-beta":
|
|
64
|
+
"claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14,web-search-2025-03-05",
|
|
65
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
66
|
+
"content-type": "application/json",
|
|
67
|
+
accept: "application/json",
|
|
68
|
+
"x-app": "cli",
|
|
69
|
+
"user-agent": "claude-cli/2.1.62",
|
|
70
|
+
},
|
|
71
|
+
selection.headers,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return applyResolvedHeaders(
|
|
76
|
+
{
|
|
77
|
+
...(credential ? { "x-api-key": credential } : {}),
|
|
66
78
|
"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",
|
|
79
|
+
"anthropic-beta": "web-search-2025-03-05",
|
|
70
80
|
"content-type": "application/json",
|
|
71
81
|
accept: "application/json",
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
};
|
|
82
|
+
},
|
|
83
|
+
selection.headers,
|
|
84
|
+
);
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
function resolveAnthropicMessagesUrl(baseUrl?: string): string {
|
|
@@ -104,32 +102,17 @@ function getAnthropicCredential(selection: PiModelSelection): string | undefined
|
|
|
104
102
|
return (
|
|
105
103
|
selection.apiKey ??
|
|
106
104
|
getBearerToken(selection.headers) ??
|
|
107
|
-
|
|
105
|
+
getResolvedHeader(selection.headers, "x-api-key")
|
|
108
106
|
);
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
function getBearerToken(headers?: Record<string, string>): string | undefined {
|
|
112
|
-
const authorization =
|
|
109
|
+
function getBearerToken(headers?: Record<string, string | null>): string | undefined {
|
|
110
|
+
const authorization = getResolvedHeader(headers, "authorization");
|
|
113
111
|
if (!authorization) return undefined;
|
|
114
112
|
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
115
113
|
return match?.[1];
|
|
116
114
|
}
|
|
117
115
|
|
|
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
116
|
function isAnthropicOAuthToken(apiKey?: string): boolean {
|
|
134
117
|
return Boolean(apiKey?.includes("sk-ant-oat"));
|
|
135
118
|
}
|