@oh-my-pi/pi-coding-agent 15.10.7 → 15.10.9
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/CHANGELOG.md +27 -0
- package/dist/types/config/model-registry.d.ts +4 -2
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/extensibility/custom-tools/loader.d.ts +22 -3
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +17 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/mcp/oauth-discovery.d.ts +4 -1
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +12 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/sdk.d.ts +42 -2
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tools/fetch.d.ts +2 -1
- package/dist/types/tools/index.d.ts +20 -1
- package/dist/types/tools/report-tool-issue.d.ts +5 -0
- package/dist/types/tui/hyperlink.d.ts +8 -0
- package/dist/types/web/kagi.d.ts +2 -1
- package/dist/types/web/parallel.d.ts +3 -0
- package/dist/types/web/search/providers/anthropic.d.ts +2 -1
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/brave.d.ts +2 -1
- package/dist/types/web/search/providers/codex.d.ts +2 -1
- package/dist/types/web/search/providers/exa.d.ts +2 -1
- package/dist/types/web/search/providers/gemini.d.ts +2 -1
- package/dist/types/web/search/providers/jina.d.ts +7 -2
- package/dist/types/web/search/providers/kagi.d.ts +7 -2
- package/dist/types/web/search/providers/kimi.d.ts +7 -2
- package/dist/types/web/search/providers/parallel.d.ts +2 -1
- package/dist/types/web/search/providers/perplexity.d.ts +2 -1
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +7 -3
- package/dist/types/web/search/providers/tavily.d.ts +2 -1
- package/dist/types/web/search/providers/zai.d.ts +2 -1
- package/package.json +9 -9
- package/src/config/model-registry.ts +13 -7
- package/src/config/model-resolver.ts +57 -2
- package/src/config/settings-schema.ts +6 -0
- package/src/extensibility/custom-tools/loader.ts +43 -19
- package/src/extensibility/custom-tools/types.ts +3 -1
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/loader.ts +29 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +30 -6
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/mcp/oauth-discovery.ts +8 -3
- package/src/mcp/oauth-flow.ts +12 -5
- package/src/mcp/transports/stdio.ts +139 -3
- package/src/modes/components/assistant-message.ts +28 -6
- package/src/modes/components/custom-editor.ts +69 -9
- package/src/modes/components/transcript-container.ts +77 -25
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +2 -2
- package/src/sdk.ts +138 -56
- package/src/ssh/ssh-executor.ts +60 -4
- package/src/task/executor.ts +19 -0
- package/src/task/index.ts +4 -0
- package/src/tools/fetch.ts +22 -5
- package/src/tools/image-gen.ts +33 -11
- package/src/tools/index.ts +21 -2
- package/src/tools/report-tool-issue.ts +7 -1
- package/src/tui/hyperlink.ts +27 -3
- package/src/web/kagi.ts +5 -2
- package/src/web/parallel.ts +7 -3
- package/src/web/search/providers/anthropic.ts +5 -1
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/brave.ts +5 -2
- package/src/web/search/providers/codex.ts +6 -2
- package/src/web/search/providers/exa.ts +91 -8
- package/src/web/search/providers/gemini.ts +6 -0
- package/src/web/search/providers/jina.ts +15 -5
- package/src/web/search/providers/kagi.ts +9 -2
- package/src/web/search/providers/kimi.ts +18 -4
- package/src/web/search/providers/parallel.ts +6 -2
- package/src/web/search/providers/perplexity.ts +7 -4
- package/src/web/search/providers/searxng.ts +6 -2
- package/src/web/search/providers/synthetic.ts +9 -5
- package/src/web/search/providers/tavily.ts +4 -2
- package/src/web/search/providers/zai.ts +15 -4
package/src/ssh/ssh-executor.ts
CHANGED
|
@@ -42,6 +42,42 @@ export interface SSHResult {
|
|
|
42
42
|
artifactId?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
type SSHExitEvent = { kind: "exit"; exitCode: number } | { kind: "error"; error: unknown };
|
|
46
|
+
|
|
47
|
+
function sshExitEvent(exitCode: number): SSHExitEvent {
|
|
48
|
+
return { kind: "exit", exitCode };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sshErrorEvent(error: unknown): SSHExitEvent {
|
|
52
|
+
return { kind: "error", error };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createAbortWaiter(
|
|
56
|
+
signal: AbortSignal | undefined,
|
|
57
|
+
streamAbort: AbortController,
|
|
58
|
+
): { promise: Promise<ptree.AbortError> | undefined; cleanup: () => void } {
|
|
59
|
+
if (!signal) {
|
|
60
|
+
return { promise: undefined, cleanup: () => {} };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { promise, resolve } = Promise.withResolvers<ptree.AbortError>();
|
|
64
|
+
const onAbort = () => {
|
|
65
|
+
const error = new ptree.AbortError(signal.reason, "<cancelled>");
|
|
66
|
+
if (!streamAbort.signal.aborted) {
|
|
67
|
+
streamAbort.abort(error);
|
|
68
|
+
}
|
|
69
|
+
resolve(error);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (signal.aborted) {
|
|
73
|
+
onAbort();
|
|
74
|
+
return { promise, cleanup: () => {} };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
78
|
+
return { promise, cleanup: () => signal.removeEventListener("abort", onAbort) };
|
|
79
|
+
}
|
|
80
|
+
|
|
45
81
|
function quoteForCompatShell(command: string): string {
|
|
46
82
|
if (command.length === 0) {
|
|
47
83
|
return "''";
|
|
@@ -94,19 +130,37 @@ export async function executeSSH(
|
|
|
94
130
|
maxColumns: resolveOutputMaxColumns(settings),
|
|
95
131
|
});
|
|
96
132
|
|
|
97
|
-
const
|
|
133
|
+
const streamAbort = new AbortController();
|
|
134
|
+
const abortWaiter = createAbortWaiter(options?.signal, streamAbort);
|
|
135
|
+
const streamOptions = { signal: streamAbort.signal };
|
|
136
|
+
const streams = [child.stdout.pipeTo(sink.createInput(), streamOptions)];
|
|
98
137
|
if (child.stderr) {
|
|
99
|
-
streams.push(child.stderr.pipeTo(sink.createInput()));
|
|
138
|
+
streams.push(child.stderr.pipeTo(sink.createInput(), streamOptions));
|
|
100
139
|
}
|
|
101
|
-
|
|
140
|
+
const streamsSettled = Promise.allSettled(streams).then(() => {});
|
|
102
141
|
|
|
103
142
|
try {
|
|
143
|
+
const exitEvent = child.exited.then(sshExitEvent, sshErrorEvent);
|
|
144
|
+
const abortEvent = abortWaiter.promise?.then(sshErrorEvent);
|
|
145
|
+
const event = await (abortEvent ? Promise.race([exitEvent, abortEvent]) : exitEvent);
|
|
146
|
+
if (event.kind === "error") {
|
|
147
|
+
throw event.error;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const streamEvent = await (abortEvent ? Promise.race([streamsSettled, abortEvent]) : streamsSettled);
|
|
151
|
+
if (streamEvent?.kind === "error") {
|
|
152
|
+
throw streamEvent.error;
|
|
153
|
+
}
|
|
104
154
|
return {
|
|
105
|
-
exitCode:
|
|
155
|
+
exitCode: event.exitCode,
|
|
106
156
|
cancelled: false,
|
|
107
157
|
...(await sink.dump()),
|
|
108
158
|
};
|
|
109
159
|
} catch (err) {
|
|
160
|
+
if (!streamAbort.signal.aborted) {
|
|
161
|
+
streamAbort.abort(err);
|
|
162
|
+
}
|
|
163
|
+
void streamsSettled;
|
|
110
164
|
if (err instanceof ptree.Exception) {
|
|
111
165
|
if (err instanceof ptree.TimeoutError) {
|
|
112
166
|
return {
|
|
@@ -129,5 +183,7 @@ export async function executeSSH(
|
|
|
129
183
|
};
|
|
130
184
|
}
|
|
131
185
|
throw err;
|
|
186
|
+
} finally {
|
|
187
|
+
abortWaiter.cleanup();
|
|
132
188
|
}
|
|
133
189
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -8,11 +8,13 @@ import path from "node:path";
|
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
10
|
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import type { Rule } from "../capability/rule";
|
|
11
12
|
import { ModelRegistry } from "../config/model-registry";
|
|
12
13
|
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
13
14
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
14
15
|
import { Settings } from "../config/settings";
|
|
15
16
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
17
|
+
import type { ToolPathWithSource } from "../extensibility/custom-tools";
|
|
16
18
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
17
19
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
18
20
|
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
@@ -190,6 +192,20 @@ export interface ExecutorOptions {
|
|
|
190
192
|
skills?: Skill[];
|
|
191
193
|
promptTemplates?: PromptTemplate[];
|
|
192
194
|
workspaceTree?: WorkspaceTree;
|
|
195
|
+
/** Parent-discovered rules, forwarded to skip rule discovery in the subagent. */
|
|
196
|
+
rules?: Rule[];
|
|
197
|
+
/**
|
|
198
|
+
* Parent's discovered extension source paths. Forwarded to skip the
|
|
199
|
+
* extension FS scan in the subagent; the subagent then re-binds each
|
|
200
|
+
* extension against its own `ExtensionAPI` (cwd, eventBus, runtime).
|
|
201
|
+
*/
|
|
202
|
+
preloadedExtensionPaths?: string[];
|
|
203
|
+
/**
|
|
204
|
+
* Parent's discovered custom-tool source paths. Forwarded to skip the
|
|
205
|
+
* `.omp/tools/` FS scan in the subagent; the subagent then re-binds each
|
|
206
|
+
* tool against its own `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
207
|
+
*/
|
|
208
|
+
preloadedCustomToolPaths?: ToolPathWithSource[];
|
|
193
209
|
mcpManager?: MCPManager;
|
|
194
210
|
authStorage?: AuthStorage;
|
|
195
211
|
modelRegistry?: ModelRegistry;
|
|
@@ -1284,6 +1300,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1284
1300
|
skills: options.skills,
|
|
1285
1301
|
promptTemplates: options.promptTemplates,
|
|
1286
1302
|
workspaceTree: options.workspaceTree,
|
|
1303
|
+
rules: options.rules,
|
|
1304
|
+
preloadedExtensionPaths: options.preloadedExtensionPaths,
|
|
1305
|
+
preloadedCustomToolPaths: options.preloadedCustomToolPaths,
|
|
1287
1306
|
systemPrompt: defaultPrompt => {
|
|
1288
1307
|
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
1289
1308
|
agent: agent.systemPrompt,
|
package/src/task/index.ts
CHANGED
|
@@ -990,6 +990,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
990
990
|
autoloadSkills: resolvedAutoloadSkills,
|
|
991
991
|
workspaceTree: this.session.workspaceTree,
|
|
992
992
|
promptTemplates,
|
|
993
|
+
rules: this.session.rules,
|
|
994
|
+
preloadedExtensionPaths: this.session.extensionPaths,
|
|
995
|
+
preloadedCustomToolPaths: this.session.customToolPaths,
|
|
993
996
|
localProtocolOptions,
|
|
994
997
|
parentArtifactManager,
|
|
995
998
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
@@ -1048,6 +1051,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1048
1051
|
autoloadSkills: resolvedAutoloadSkills,
|
|
1049
1052
|
workspaceTree: this.session.workspaceTree,
|
|
1050
1053
|
promptTemplates,
|
|
1054
|
+
rules: this.session.rules,
|
|
1051
1055
|
localProtocolOptions,
|
|
1052
1056
|
parentArtifactManager,
|
|
1053
1057
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
package/src/tools/fetch.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
6
|
-
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import type { FetchImpl, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
@@ -637,6 +637,7 @@ export async function renderHtmlToText(
|
|
|
637
637
|
settings: Settings,
|
|
638
638
|
userSignal: AbortSignal | undefined,
|
|
639
639
|
storage: AgentStorage | null,
|
|
640
|
+
fetchOverride?: FetchImpl,
|
|
640
641
|
): Promise<{ content: string; ok: boolean; method: string }> {
|
|
641
642
|
const overallSignal = ptree.combineSignals(userSignal, timeout * 1000);
|
|
642
643
|
const execOptions = {
|
|
@@ -650,6 +651,7 @@ export async function renderHtmlToText(
|
|
|
650
651
|
// Per-attempt budget for remote endpoints so one stall cannot consume the
|
|
651
652
|
// whole reader-mode budget and starve the local fallbacks.
|
|
652
653
|
const remoteSignal = () => ptree.combineSignals(userSignal, remoteBudgetMs);
|
|
654
|
+
const fetchImpl = fetchOverride ?? fetch;
|
|
653
655
|
|
|
654
656
|
const runners: Record<FetchProvider, () => Promise<string | null>> = {
|
|
655
657
|
// Purely local, no network/subprocess: still works on already-loaded HTML
|
|
@@ -670,14 +672,20 @@ export async function renderHtmlToText(
|
|
|
670
672
|
if (!findParallelApiKey(storage)) return null;
|
|
671
673
|
const parallelResult = await extractWithParallel(
|
|
672
674
|
[url],
|
|
673
|
-
{
|
|
675
|
+
{
|
|
676
|
+
objective: "Extract the main content",
|
|
677
|
+
excerpts: true,
|
|
678
|
+
fullContent: false,
|
|
679
|
+
signal: remoteSignal(),
|
|
680
|
+
fetch: fetchImpl,
|
|
681
|
+
},
|
|
674
682
|
storage,
|
|
675
683
|
);
|
|
676
684
|
const firstDocument = parallelResult.results[0];
|
|
677
685
|
return firstDocument ? getParallelExtractContent(firstDocument) : null;
|
|
678
686
|
},
|
|
679
687
|
jina: async () => {
|
|
680
|
-
const response = await
|
|
688
|
+
const response = await fetchImpl(`https://r.jina.ai/${url}`, {
|
|
681
689
|
headers: { Accept: "text/markdown" },
|
|
682
690
|
signal: remoteSignal(),
|
|
683
691
|
});
|
|
@@ -1052,6 +1060,7 @@ async function renderUrl(
|
|
|
1052
1060
|
settings: Settings,
|
|
1053
1061
|
signal: AbortSignal | undefined,
|
|
1054
1062
|
storage: AgentStorage | null,
|
|
1063
|
+
fetchOverride?: FetchImpl,
|
|
1055
1064
|
): Promise<FetchRenderResult> {
|
|
1056
1065
|
const notes: string[] = [];
|
|
1057
1066
|
const fetchedAt = new Date().toISOString();
|
|
@@ -1425,7 +1434,15 @@ async function renderUrl(
|
|
|
1425
1434
|
}
|
|
1426
1435
|
|
|
1427
1436
|
// 5E: Render HTML via the reader-backend chain (native/trafilatura/lynx/parallel/jina)
|
|
1428
|
-
const htmlResult = await renderHtmlToText(
|
|
1437
|
+
const htmlResult = await renderHtmlToText(
|
|
1438
|
+
finalUrl,
|
|
1439
|
+
rawContent,
|
|
1440
|
+
timeout,
|
|
1441
|
+
settings,
|
|
1442
|
+
signal,
|
|
1443
|
+
storage,
|
|
1444
|
+
fetchOverride,
|
|
1445
|
+
);
|
|
1429
1446
|
if (!htmlResult.ok) {
|
|
1430
1447
|
notes.push("html rendering failed (no reader backend produced usable output)");
|
|
1431
1448
|
|
|
@@ -1626,7 +1643,7 @@ async function buildReadUrlCacheEntry(
|
|
|
1626
1643
|
}
|
|
1627
1644
|
|
|
1628
1645
|
const storage = session.settings.getStorage();
|
|
1629
|
-
const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage);
|
|
1646
|
+
const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage, session.fetch);
|
|
1630
1647
|
const output = buildUrlReadOutput(result, result.content);
|
|
1631
1648
|
const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, output) : undefined;
|
|
1632
1649
|
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ApiKey,
|
|
5
|
+
type FetchImpl,
|
|
6
|
+
getAntigravityUserAgent,
|
|
7
|
+
getEnvApiKey,
|
|
8
|
+
type Model,
|
|
9
|
+
withAuth,
|
|
10
|
+
} from "@oh-my-pi/pi-ai";
|
|
4
11
|
import {
|
|
5
12
|
CODEX_BASE_URL,
|
|
6
13
|
getCodexAccountId,
|
|
@@ -366,7 +373,11 @@ function toDataUrl(image: InlineImageData): string {
|
|
|
366
373
|
return `data:${image.mimeType};base64,${image.data}`;
|
|
367
374
|
}
|
|
368
375
|
|
|
369
|
-
async function loadImageFromUrl(
|
|
376
|
+
async function loadImageFromUrl(
|
|
377
|
+
imageUrl: string,
|
|
378
|
+
fetchImpl: FetchImpl,
|
|
379
|
+
signal?: AbortSignal,
|
|
380
|
+
): Promise<InlineImageData> {
|
|
370
381
|
if (imageUrl.startsWith("data:")) {
|
|
371
382
|
const normalized = normalizeDataUrl(imageUrl.trim());
|
|
372
383
|
if (!normalized.mimeType) {
|
|
@@ -378,7 +389,7 @@ async function loadImageFromUrl(imageUrl: string, signal?: AbortSignal): Promise
|
|
|
378
389
|
return { data: normalized.data, mimeType: normalized.mimeType };
|
|
379
390
|
}
|
|
380
391
|
|
|
381
|
-
const response = await
|
|
392
|
+
const response = await fetchImpl(imageUrl, { signal });
|
|
382
393
|
if (!response.ok) {
|
|
383
394
|
const rawText = await response.text();
|
|
384
395
|
throw new Error(`Image download failed (${response.status}): ${rawText}`);
|
|
@@ -850,13 +861,14 @@ async function generateOpenAIHostedImage(
|
|
|
850
861
|
model: Model,
|
|
851
862
|
params: ImageGenParams,
|
|
852
863
|
inputImages: InlineImageData[],
|
|
864
|
+
fetchImpl: FetchImpl,
|
|
853
865
|
signal: AbortSignal | undefined,
|
|
854
866
|
sessionId: string | undefined,
|
|
855
867
|
): Promise<OpenAIHostedImageResult> {
|
|
856
868
|
const promptText = assemblePrompt(params);
|
|
857
869
|
const stream = model.api === "openai-codex-responses" || model.provider === "openai-codex";
|
|
858
870
|
const requestBody = buildOpenAIHostedImageRequest(model, promptText, params, inputImages, stream);
|
|
859
|
-
const response = await
|
|
871
|
+
const response = await fetchImpl(getOpenAIResponsesUrl(model), {
|
|
860
872
|
method: "POST",
|
|
861
873
|
headers: buildOpenAIImageHeaders(model, apiKey, sessionId),
|
|
862
874
|
body: JSON.stringify(requestBody),
|
|
@@ -1035,6 +1047,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1035
1047
|
}
|
|
1036
1048
|
|
|
1037
1049
|
const requestSignal = ptree.combineSignals(signal, IMAGE_TIMEOUT);
|
|
1050
|
+
const fetchImpl = ctx.fetch ?? fetch;
|
|
1038
1051
|
|
|
1039
1052
|
if (provider === "openai" || provider === "openai-codex") {
|
|
1040
1053
|
if (!apiKey.model) {
|
|
@@ -1049,7 +1062,16 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1049
1062
|
|
|
1050
1063
|
const parsed = await withAuth(
|
|
1051
1064
|
hostedKey,
|
|
1052
|
-
key =>
|
|
1065
|
+
key =>
|
|
1066
|
+
generateOpenAIHostedImage(
|
|
1067
|
+
key,
|
|
1068
|
+
hostedModel,
|
|
1069
|
+
params,
|
|
1070
|
+
resolvedImages,
|
|
1071
|
+
fetchImpl,
|
|
1072
|
+
requestSignal,
|
|
1073
|
+
sessionId,
|
|
1074
|
+
),
|
|
1053
1075
|
{ signal: requestSignal },
|
|
1054
1076
|
);
|
|
1055
1077
|
|
|
@@ -1117,7 +1139,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1117
1139
|
resolvedImages,
|
|
1118
1140
|
);
|
|
1119
1141
|
|
|
1120
|
-
const resp = await
|
|
1142
|
+
const resp = await fetchImpl(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
|
|
1121
1143
|
method: "POST",
|
|
1122
1144
|
headers: {
|
|
1123
1145
|
Authorization: `Bearer ${bearer}`,
|
|
@@ -1225,7 +1247,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1225
1247
|
const xaiRawText = await withAuth(
|
|
1226
1248
|
xaiKey,
|
|
1227
1249
|
async key => {
|
|
1228
|
-
const resp = await
|
|
1250
|
+
const resp = await fetchImpl(`${xaiCreds.baseURL}${xaiEndpoint}`, {
|
|
1229
1251
|
method: "POST",
|
|
1230
1252
|
headers: {
|
|
1231
1253
|
Authorization: `Bearer ${key}`,
|
|
@@ -1263,7 +1285,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1263
1285
|
const mimeType = parseImageMetadata(bytes)?.mimeType ?? "image/png";
|
|
1264
1286
|
xaiInlineImages.push({ data: entry.b64_json, mimeType });
|
|
1265
1287
|
} else if (entry.url) {
|
|
1266
|
-
xaiInlineImages.push(await loadImageFromUrl(entry.url, requestSignal));
|
|
1288
|
+
xaiInlineImages.push(await loadImageFromUrl(entry.url, fetchImpl, requestSignal));
|
|
1267
1289
|
}
|
|
1268
1290
|
}
|
|
1269
1291
|
|
|
@@ -1309,7 +1331,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1309
1331
|
};
|
|
1310
1332
|
|
|
1311
1333
|
const rawText = await withAuth(apiKey.apiKey, async key => {
|
|
1312
|
-
const resp = await
|
|
1334
|
+
const resp = await fetchImpl("https://openrouter.ai/api/v1/chat/completions", {
|
|
1313
1335
|
method: "POST",
|
|
1314
1336
|
headers: {
|
|
1315
1337
|
"Content-Type": "application/json",
|
|
@@ -1343,7 +1365,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1343
1365
|
const imageUrls = extractOpenRouterImageUrls(message);
|
|
1344
1366
|
const inlineImages: InlineImageData[] = [];
|
|
1345
1367
|
for (const imageUrl of imageUrls) {
|
|
1346
|
-
inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal));
|
|
1368
|
+
inlineImages.push(await loadImageFromUrl(imageUrl, fetchImpl, requestSignal));
|
|
1347
1369
|
}
|
|
1348
1370
|
|
|
1349
1371
|
if (inlineImages.length === 0) {
|
|
@@ -1404,7 +1426,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1404
1426
|
};
|
|
1405
1427
|
|
|
1406
1428
|
const rawText = await withAuth(apiKey.apiKey, async key => {
|
|
1407
|
-
const resp = await
|
|
1429
|
+
const resp = await fetchImpl(
|
|
1408
1430
|
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
|
|
1409
1431
|
{
|
|
1410
1432
|
method: "POST",
|
package/src/tools/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import type { FetchImpl, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { AsyncJobManager } from "../async/job-manager";
|
|
6
|
+
import type { Rule } from "../capability/rule";
|
|
6
7
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
7
8
|
import type { Settings } from "../config/settings";
|
|
8
9
|
import { EditTool } from "../edit";
|
|
9
10
|
import { checkPythonKernelAvailability } from "../eval/py/kernel";
|
|
11
|
+
import type { ToolPathWithSource } from "../extensibility/custom-tools";
|
|
10
12
|
import type { Skill } from "../extensibility/skills";
|
|
11
13
|
import type { GoalModeState, GoalRuntime } from "../goals";
|
|
12
14
|
import { GoalTool } from "../goals/tools/goal-tool";
|
|
@@ -142,6 +144,8 @@ export interface ToolSession {
|
|
|
142
144
|
cwd: string;
|
|
143
145
|
/** Whether UI is available */
|
|
144
146
|
hasUI: boolean;
|
|
147
|
+
/** Optional fetch implementation injected into the URL read pipeline (tests, proxies). Defaults to global fetch. */
|
|
148
|
+
fetch?: FetchImpl;
|
|
145
149
|
/** Skip Python kernel availability check and warmup */
|
|
146
150
|
skipPythonPreflight?: boolean;
|
|
147
151
|
/** Pre-loaded context files (AGENTS.md, etc) */
|
|
@@ -152,6 +156,21 @@ export interface ToolSession {
|
|
|
152
156
|
skills?: Skill[];
|
|
153
157
|
/** Pre-loaded prompt templates */
|
|
154
158
|
promptTemplates?: PromptTemplate[];
|
|
159
|
+
/** Pre-loaded rules (forwarded to subagents to skip re-discovery). */
|
|
160
|
+
rules?: Rule[];
|
|
161
|
+
/**
|
|
162
|
+
* Pre-discovered extension source paths. Forwarded to subagents so they
|
|
163
|
+
* skip the FS scan but still re-bind extensions to their own session-scoped
|
|
164
|
+
* `ExtensionAPI` (cwd, eventBus, runtime). Inline extension factories
|
|
165
|
+
* (`<inline-N>`) are NOT included — those are session-local.
|
|
166
|
+
*/
|
|
167
|
+
extensionPaths?: string[];
|
|
168
|
+
/**
|
|
169
|
+
* Pre-discovered custom-tool source paths from `.omp/tools/`, `.claude/tools/`,
|
|
170
|
+
* plugins, etc. Forwarded to subagents so they skip the FS scan but still
|
|
171
|
+
* re-bind tools to their own session-scoped `CustomToolAPI`.
|
|
172
|
+
*/
|
|
173
|
+
customToolPaths?: ToolPathWithSource[];
|
|
155
174
|
/** Whether LSP integrations are enabled */
|
|
156
175
|
enableLsp?: boolean;
|
|
157
176
|
/** Whether an edit-capable tool is available in this session (controls hashline output) */
|
|
@@ -492,7 +511,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
492
511
|
const isToolAllowed = (name: string) => {
|
|
493
512
|
if (name === "goal") return goalEnabled && goalModeActive;
|
|
494
513
|
if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
|
|
495
|
-
if (name === "bash") return
|
|
514
|
+
if (name === "bash") return session.settings.get("bash.enabled");
|
|
496
515
|
if (name === "eval") return allowEval;
|
|
497
516
|
if (name === "debug") return session.settings.get("debug.enabled");
|
|
498
517
|
if (name === "todo") return !includeYield && session.settings.get("todo.enabled");
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { Database } from "bun:sqlite";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
25
|
+
import type { FetchImpl } from "@oh-my-pi/pi-ai";
|
|
25
26
|
import { $env, $flag, getAgentDir, getInstallId, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
26
27
|
import * as z from "zod/v4";
|
|
27
28
|
import type { Settings } from "..";
|
|
@@ -260,6 +261,10 @@ export interface FlushOptions {
|
|
|
260
261
|
* future debug recipes); never set from the tool's auto-flush path.
|
|
261
262
|
*/
|
|
262
263
|
bypassConsent?: boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Fetch implementation for the push POST. Defaults to global fetch.
|
|
266
|
+
*/
|
|
267
|
+
fetch?: FetchImpl;
|
|
263
268
|
/**
|
|
264
269
|
* Fires once at the start of the loop with the snapshot count of
|
|
265
270
|
* unpushed rows. Subsequent inserts won't be reflected (the count is
|
|
@@ -345,6 +350,7 @@ async function performFlush(db: Database, config: PushConfig, options: FlushOpti
|
|
|
345
350
|
const totalRow = db.prepare("SELECT COUNT(*) AS n FROM grievances WHERE pushed = 0").get() as { n: number };
|
|
346
351
|
options.onStart(totalRow.n);
|
|
347
352
|
}
|
|
353
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
348
354
|
let totalPushed = 0;
|
|
349
355
|
for (;;) {
|
|
350
356
|
const rows = selectStmt.all(FLUSH_BATCH_SIZE) as GrievanceRow[];
|
|
@@ -366,7 +372,7 @@ async function performFlush(db: Database, config: PushConfig, options: FlushOpti
|
|
|
366
372
|
|
|
367
373
|
let response: Response;
|
|
368
374
|
try {
|
|
369
|
-
response = await
|
|
375
|
+
response = await fetchImpl(config.endpoint, {
|
|
370
376
|
method: "POST",
|
|
371
377
|
headers,
|
|
372
378
|
body,
|
package/src/tui/hyperlink.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
const OSC = "\x1b]";
|
|
20
20
|
const ST = "\x1b\\";
|
|
21
|
+
const BEL = "\x07";
|
|
21
22
|
|
|
22
23
|
/** Stable 8-char hex ID derived from a URI — hints terminals to coalesce identical adjacent links. */
|
|
23
24
|
function buildLinkId(uri: string): string {
|
|
@@ -60,14 +61,18 @@ function safeHyperlinkUri(uri: string): string | undefined {
|
|
|
60
61
|
return uri;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function
|
|
64
|
-
if (!isHyperlinkEnabled()) return displayText;
|
|
64
|
+
function wrapHyperlinkCore(uri: string, displayText: string, terminator: typeof ST | typeof BEL): string {
|
|
65
65
|
// Do not double-wrap if the text already embeds an OSC 8 sequence.
|
|
66
66
|
if (displayText.includes("\x1b]8;")) return displayText;
|
|
67
67
|
const safeUri = safeHyperlinkUri(uri);
|
|
68
68
|
if (!safeUri) return displayText;
|
|
69
69
|
const id = buildLinkId(safeUri);
|
|
70
|
-
return `${OSC}8;id=${id};${safeUri}${
|
|
70
|
+
return `${OSC}8;id=${id};${safeUri}${terminator}${displayText}${OSC}8;;${terminator}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function wrapHyperlink(uri: string, displayText: string): string {
|
|
74
|
+
if (!isHyperlinkEnabled()) return displayText;
|
|
75
|
+
return wrapHyperlinkCore(uri, displayText, ST);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/**
|
|
@@ -95,6 +100,25 @@ export function urlHyperlink(url: string, displayText: string): string {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Wrap `displayText` in an OSC 8 hyperlink pointing at an HTTP(S) URL,
|
|
105
|
+
* bypassing terminal capability auto-detection. Used for auth prompts where
|
|
106
|
+
* an inert "click" label blocks login on terminals whose capabilities are
|
|
107
|
+
* not advertised. Still returns plain text when the user has explicitly
|
|
108
|
+
* opted out via `tui.hyperlinks=off`.
|
|
109
|
+
*/
|
|
110
|
+
export function urlHyperlinkAlways(url: string, displayText: string): string {
|
|
111
|
+
if (settings.get("tui.hyperlinks") === "off") return displayText;
|
|
112
|
+
const normalized = url.match(/^www\./i) ? `https://${url}` : url;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(normalized);
|
|
115
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return displayText;
|
|
116
|
+
return wrapHyperlinkCore(parsed.href, displayText, BEL);
|
|
117
|
+
} catch {
|
|
118
|
+
return displayText;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
/**
|
|
99
123
|
* Wrap `displayText` in an OSC 8 hyperlink pointing at a filesystem path.
|
|
100
124
|
*
|
package/src/web/kagi.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* through the shared {@link AuthStorage} broker (Bearer token), and responses
|
|
7
7
|
* are categorized result buckets rather than the legacy flat object array.
|
|
8
8
|
*/
|
|
9
|
-
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { withHardTimeout } from "./search/providers/utils";
|
|
11
11
|
|
|
12
12
|
const KAGI_SEARCH_URL = "https://kagi.com/api/v1/search";
|
|
@@ -156,6 +156,7 @@ export interface KagiSearchOptions {
|
|
|
156
156
|
recency?: "day" | "week" | "month" | "year";
|
|
157
157
|
sessionId?: string;
|
|
158
158
|
signal?: AbortSignal;
|
|
159
|
+
fetch?: FetchImpl;
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
export interface KagiSearchSource {
|
|
@@ -251,7 +252,9 @@ export async function searchWithKagi(
|
|
|
251
252
|
throw new KagiApiError("Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.");
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
const
|
|
255
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
256
|
+
|
|
257
|
+
const response = await fetchImpl(KAGI_SEARCH_URL, {
|
|
255
258
|
method: "POST",
|
|
256
259
|
headers: {
|
|
257
260
|
Authorization: `Bearer ${apiKey}`,
|
package/src/web/parallel.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import { type FetchImpl, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { AgentStorage } from "../session/agent-storage";
|
|
3
3
|
import { findCredential, withHardTimeout } from "./search/providers/utils";
|
|
4
4
|
|
|
@@ -54,6 +54,7 @@ export interface ParallelSearchOptions {
|
|
|
54
54
|
mode?: "fast" | "research";
|
|
55
55
|
maxCharsPerResult?: number;
|
|
56
56
|
signal?: AbortSignal;
|
|
57
|
+
fetch?: FetchImpl;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface ParallelExtractOptions {
|
|
@@ -62,6 +63,7 @@ export interface ParallelExtractOptions {
|
|
|
62
63
|
excerpts?: boolean;
|
|
63
64
|
fullContent?: boolean;
|
|
64
65
|
signal?: AbortSignal;
|
|
66
|
+
fetch?: FetchImpl;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
export class ParallelApiError extends Error {
|
|
@@ -295,7 +297,8 @@ export async function searchWithParallel(
|
|
|
295
297
|
);
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
const
|
|
300
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
301
|
+
const response = await fetchImpl(PARALLEL_SEARCH_URL, {
|
|
299
302
|
method: "POST",
|
|
300
303
|
headers: getAuthHeaders(apiKey),
|
|
301
304
|
body: JSON.stringify({
|
|
@@ -328,7 +331,8 @@ export async function extractWithParallel(
|
|
|
328
331
|
);
|
|
329
332
|
}
|
|
330
333
|
|
|
331
|
-
const
|
|
334
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
335
|
+
const response = await fetchImpl(PARALLEL_EXTRACT_URL, {
|
|
332
336
|
method: "POST",
|
|
333
337
|
headers: getAuthHeaders(apiKey),
|
|
334
338
|
body: JSON.stringify({
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildAnthropicSearchHeaders,
|
|
14
14
|
buildAnthropicSystemBlocks,
|
|
15
15
|
buildAnthropicUrl,
|
|
16
|
+
type FetchImpl,
|
|
16
17
|
stripClaudeToolPrefix,
|
|
17
18
|
withAuth,
|
|
18
19
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -40,6 +41,7 @@ export interface AnthropicSearchParams {
|
|
|
40
41
|
max_tokens?: number;
|
|
41
42
|
temperature?: number;
|
|
42
43
|
signal?: AbortSignal;
|
|
44
|
+
fetch?: FetchImpl;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -89,6 +91,7 @@ async function callSearch(
|
|
|
89
91
|
maxTokens?: number,
|
|
90
92
|
temperature?: number,
|
|
91
93
|
signal?: AbortSignal,
|
|
94
|
+
fetchImpl: FetchImpl = fetch,
|
|
92
95
|
): Promise<AnthropicApiResponse> {
|
|
93
96
|
const url = buildAnthropicUrl(auth);
|
|
94
97
|
const headers = buildAnthropicSearchHeaders(auth);
|
|
@@ -115,7 +118,7 @@ async function callSearch(
|
|
|
115
118
|
body.system = systemBlocks;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
const response = await
|
|
121
|
+
const response = await fetchImpl(url, {
|
|
119
122
|
method: "POST",
|
|
120
123
|
headers,
|
|
121
124
|
body: JSON.stringify(body),
|
|
@@ -275,6 +278,7 @@ export async function searchAnthropic(
|
|
|
275
278
|
maxTokens,
|
|
276
279
|
params.temperature,
|
|
277
280
|
params.signal,
|
|
281
|
+
params.fetch,
|
|
278
282
|
),
|
|
279
283
|
{
|
|
280
284
|
signal: params.signal,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { SearchProviderId, SearchResponse } from "../types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -30,6 +30,7 @@ export interface SearchParams {
|
|
|
30
30
|
recency?: "day" | "week" | "month" | "year";
|
|
31
31
|
systemPrompt: string;
|
|
32
32
|
signal?: AbortSignal;
|
|
33
|
+
fetch?: FetchImpl;
|
|
33
34
|
maxOutputTokens?: number;
|
|
34
35
|
numSearchResults?: number;
|
|
35
36
|
temperature?: number;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Calls Brave's web search REST API and maps results into the unified
|
|
5
5
|
* SearchResponse shape used by the web search tool.
|
|
6
6
|
*/
|
|
7
|
-
import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { type AuthStorage, type FetchImpl, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
9
9
|
import { SearchProviderError } from "../../../web/search/types";
|
|
10
10
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
@@ -28,6 +28,7 @@ export interface BraveSearchParams {
|
|
|
28
28
|
num_results?: number;
|
|
29
29
|
recency?: "day" | "week" | "month" | "year";
|
|
30
30
|
signal?: AbortSignal;
|
|
31
|
+
fetch?: FetchImpl;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
interface BraveSearchResult {
|
|
@@ -80,7 +81,8 @@ async function callBraveSearch(
|
|
|
80
81
|
url.searchParams.set("freshness", RECENCY_MAP[params.recency]);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
const
|
|
84
|
+
const fetchImpl = params.fetch ?? fetch;
|
|
85
|
+
const response = await fetchImpl(url, {
|
|
84
86
|
headers: {
|
|
85
87
|
Accept: "application/json",
|
|
86
88
|
"X-Subscription-Token": apiKey,
|
|
@@ -144,6 +146,7 @@ export class BraveProvider extends SearchProvider {
|
|
|
144
146
|
num_results: params.numSearchResults ?? params.limit,
|
|
145
147
|
recency: params.recency,
|
|
146
148
|
signal: params.signal,
|
|
149
|
+
fetch: params.fetch,
|
|
147
150
|
});
|
|
148
151
|
}
|
|
149
152
|
}
|