pim-agent 0.0.1 → 0.9.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 +293 -7
- package/bin/pim.ts +151 -0
- package/package.json +82 -4
- package/packages/core/src/attachments/AttachmentStore.ts +99 -0
- package/packages/core/src/attachments/Attachments.ts +69 -0
- package/packages/core/src/extensions/CoreExtensions.ts +51 -0
- package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
- package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
- package/packages/core/src/extensions/apply-patch/index.ts +69 -0
- package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
- package/packages/core/src/extensions/apply-patch/model.ts +39 -0
- package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
- package/packages/core/src/extensions/apply-patch/render.ts +216 -0
- package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
- package/packages/core/src/extensions/apply-patch/types.ts +30 -0
- package/packages/core/src/extensions/bash/capture.ts +86 -0
- package/packages/core/src/extensions/bash/format.ts +110 -0
- package/packages/core/src/extensions/bash/image.ts +31 -0
- package/packages/core/src/extensions/bash/index.ts +88 -0
- package/packages/core/src/extensions/bash/render.ts +31 -0
- package/packages/core/src/extensions/bash/run.ts +221 -0
- package/packages/core/src/extensions/bash/schema.ts +64 -0
- package/packages/core/src/extensions/edit/edit.ts +366 -0
- package/packages/core/src/extensions/edit/index.ts +36 -0
- package/packages/core/src/extensions/edit/render.ts +16 -0
- package/packages/core/src/extensions/edit/schema.ts +37 -0
- package/packages/core/src/extensions/glob/glob.ts +34 -0
- package/packages/core/src/extensions/glob/index.ts +83 -0
- package/packages/core/src/extensions/glob/render.ts +72 -0
- package/packages/core/src/extensions/glob/schema.ts +66 -0
- package/packages/core/src/extensions/grep/grep.ts +256 -0
- package/packages/core/src/extensions/grep/index.ts +114 -0
- package/packages/core/src/extensions/grep/render.ts +271 -0
- package/packages/core/src/extensions/grep/schema.ts +92 -0
- package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
- package/packages/core/src/extensions/read/image.ts +57 -0
- package/packages/core/src/extensions/read/index.ts +88 -0
- package/packages/core/src/extensions/read/read.ts +191 -0
- package/packages/core/src/extensions/read/render.ts +58 -0
- package/packages/core/src/extensions/read/schema.ts +49 -0
- package/packages/core/src/extensions/subagent/index.ts +33 -0
- package/packages/core/src/extensions/subagent/render.ts +127 -0
- package/packages/core/src/extensions/subagent/schema.ts +9 -0
- package/packages/core/src/extensions/subagent/subagent.ts +419 -0
- package/packages/core/src/extensions/system-prompt/index.ts +28 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
- package/packages/core/src/extensions/todo/index.ts +123 -0
- package/packages/core/src/extensions/todo/render.ts +156 -0
- package/packages/core/src/extensions/todo/schema.ts +24 -0
- package/packages/core/src/extensions/todo/todo.ts +185 -0
- package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
- package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
- package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
- package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
- package/packages/core/src/extensions/web-fetch/image.ts +165 -0
- package/packages/core/src/extensions/web-fetch/index.ts +83 -0
- package/packages/core/src/extensions/web-fetch/render.ts +100 -0
- package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
- package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
- package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
- package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
- package/packages/core/src/extensions/web-search/index.ts +90 -0
- package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
- package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
- package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
- package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
- package/packages/core/src/extensions/web-search/render.ts +49 -0
- package/packages/core/src/extensions/web-search/schema.ts +29 -0
- package/packages/core/src/extensions/web-search/search.ts +23 -0
- package/packages/core/src/extensions/write/index.ts +58 -0
- package/packages/core/src/extensions/write/render.ts +16 -0
- package/packages/core/src/extensions/write/schema.ts +14 -0
- package/packages/core/src/extensions/write/write.ts +87 -0
- package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
- package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
- package/packages/core/src/picker/PickerItem.ts +6 -0
- package/packages/core/src/picker/PickerService.ts +110 -0
- package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
- package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
- package/packages/core/src/picker/catalog.ts +220 -0
- package/packages/core/src/picker/commandRanker.ts +17 -0
- package/packages/core/src/picker/filePickerWorker.ts +66 -0
- package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
- package/packages/core/src/picker/ranker.ts +245 -0
- package/packages/core/src/picker/token.ts +70 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +213 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionHost.ts +876 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionRegistry.ts +169 -0
- package/packages/core/src/session/WriteMark.ts +37 -0
- package/packages/core/src/shared/Cli.ts +25 -0
- package/packages/core/src/shared/DaemonUnit.ts +13 -0
- package/packages/core/src/shared/DiffLines.ts +304 -0
- package/packages/core/src/shared/DiffRenderer.ts +283 -0
- package/packages/core/src/shared/DiffView.ts +29 -0
- package/packages/core/src/shared/Directories.ts +61 -0
- package/packages/core/src/shared/EditMatcher.ts +767 -0
- package/packages/core/src/shared/Errors.ts +9 -0
- package/packages/core/src/shared/ExtensionToggles.ts +130 -0
- package/packages/core/src/shared/FileEnumerator.ts +361 -0
- package/packages/core/src/shared/FileScanner.ts +57 -0
- package/packages/core/src/shared/FileWatch.ts +107 -0
- package/packages/core/src/shared/Format.ts +74 -0
- package/packages/core/src/shared/Fs.ts +91 -0
- package/packages/core/src/shared/FsErrors.ts +67 -0
- package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
- package/packages/core/src/shared/Git.ts +89 -0
- package/packages/core/src/shared/GlobExclusions.ts +13 -0
- package/packages/core/src/shared/Http.ts +14 -0
- package/packages/core/src/shared/ImageMime.ts +36 -0
- package/packages/core/src/shared/Images.ts +357 -0
- package/packages/core/src/shared/Json.ts +19 -0
- package/packages/core/src/shared/Languages.ts +95 -0
- package/packages/core/src/shared/Levenshtein.ts +30 -0
- package/packages/core/src/shared/Lines.ts +79 -0
- package/packages/core/src/shared/McpClient.ts +444 -0
- package/packages/core/src/shared/OutputBudget.ts +84 -0
- package/packages/core/src/shared/PatchSummary.ts +40 -0
- package/packages/core/src/shared/Paths.ts +80 -0
- package/packages/core/src/shared/PimSettings.ts +128 -0
- package/packages/core/src/shared/PimVersion.ts +93 -0
- package/packages/core/src/shared/Pool.ts +26 -0
- package/packages/core/src/shared/Proc.ts +30 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/SafePath.ts +17 -0
- package/packages/core/src/shared/SearchRender.ts +82 -0
- package/packages/core/src/shared/SendFile.ts +23 -0
- package/packages/core/src/shared/SpillCache.ts +78 -0
- package/packages/core/src/shared/SubagentLogs.ts +102 -0
- package/packages/core/src/shared/Supervisor.ts +460 -0
- package/packages/core/src/shared/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +193 -0
- package/packages/core/src/view/AnsiPainter.ts +227 -0
- package/packages/core/src/view/BodyRenderer.ts +102 -0
- package/packages/core/src/view/DiffBlocks.ts +52 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/Painting.ts +182 -0
- package/packages/core/src/view/ViewBlock.ts +124 -0
- package/packages/daemon/src/Daemon.ts +63 -0
- package/packages/daemon/src/DaemonInstall.ts +115 -0
- package/packages/daemon/src/Surfaces.ts +66 -0
- package/packages/daemon/src/TelegramSurface.ts +30 -0
- package/packages/daemon/src/WebSurface.ts +57 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +92 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/Protocol.ts +6 -0
- package/packages/protocol/src/ServerEvent.ts +251 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +265 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +312 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +202 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +601 -0
- package/packages/server/src/StaticClient.ts +95 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +563 -0
- package/packages/server/src/probe.ts +146 -0
- package/packages/telegram/src/Bot.ts +229 -0
- package/packages/telegram/src/Commands.ts +768 -0
- package/packages/telegram/src/Config.ts +148 -0
- package/packages/telegram/src/Markdown.ts +235 -0
- package/packages/telegram/src/Message.ts +152 -0
- package/packages/telegram/src/Renderer.ts +456 -0
- package/packages/telegram/src/SendFileSchema.ts +18 -0
- package/packages/telegram/src/SendFileTool.ts +65 -0
- package/packages/telegram/src/Session.ts +252 -0
- package/packages/telegram/src/SessionRegistry.ts +126 -0
- package/packages/telegram/src/TaskScheduler.ts +296 -0
- package/packages/telegram/src/TaskSchema.ts +88 -0
- package/packages/telegram/src/TaskStore.ts +67 -0
- package/packages/telegram/src/TaskTool.ts +208 -0
- package/packages/telegram/src/TelegramHtml.ts +288 -0
- package/packages/telegram/src/TypingIndicator.ts +43 -0
- package/packages/telegram/src/UpdateConfirm.ts +61 -0
- package/packages/tui/src/extensions/_init/index.ts +144 -0
- package/packages/tui/src/extensions/command-picker/index.ts +43 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +117 -0
- package/packages/tui/src/extensions/footer/powerline.ts +87 -0
- package/packages/tui/src/extensions/footer/segments.ts +202 -0
- package/packages/tui/src/extensions/pim/index.ts +89 -0
- package/packages/tui/src/extensions/session-lease/index.ts +248 -0
- package/packages/tui/src/extensions/tps/index.ts +118 -0
- package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
- package/packages/tui/src/extensions/wrapProvider.ts +18 -0
- package/packages/tui/src/themes/pim-dark.json +84 -0
- package/packages/tui/src/themes/pim-light.json +84 -0
- package/packages/tui/src/themes/themeCliArgs.ts +20 -0
- package/packages/web/dist/client/apple-touch-icon.png +0 -0
- package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
- package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
- package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
- package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
- package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
- package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
- package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
- package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
- package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
- package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
- package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
- package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
- package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
- package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
- package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
- package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
- package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
- package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
- package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
- package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
- package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
- package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
- package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
- package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
- package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
- package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
- package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
- package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
- package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
- package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
- package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
- package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
- package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
- package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
- package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
- package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
- package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
- package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
- package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
- package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
- package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
- package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
- package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
- package/packages/web/dist/client/favicon.svg +1 -0
- package/packages/web/dist/client/icon-192.png +0 -0
- package/packages/web/dist/client/icon-512.png +0 -0
- package/packages/web/dist/client/icon-maskable.png +0 -0
- package/packages/web/dist/client/index.html +24 -0
- package/packages/web/dist/client/manifest.webmanifest +21 -0
- package/packages/web/dist/client/wordmark.svg +1 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { Errors } from "../../shared/Errors";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import type { HttpFetch } from "../../shared/Http";
|
|
4
|
+
import { Lines } from "../../shared/Lines";
|
|
5
|
+
import { OutputBudget } from "../../shared/OutputBudget";
|
|
6
|
+
import { SpillCache } from "../../shared/SpillCache";
|
|
7
|
+
import { fetchImage, type WebFetchImageOutcome } from "./image";
|
|
8
|
+
import type { JinaReaderClient } from "./JinaReaderClient";
|
|
9
|
+
import {
|
|
10
|
+
WEB_FETCH_INLINE_BYTES,
|
|
11
|
+
type WebFetchFormat,
|
|
12
|
+
type WebFetchResolvedFormat,
|
|
13
|
+
} from "./schema";
|
|
14
|
+
import type { WebViewFetchClient } from "./WebViewFetchClient";
|
|
15
|
+
|
|
16
|
+
export type WebFetchPage = {
|
|
17
|
+
readonly title: string;
|
|
18
|
+
readonly url: string;
|
|
19
|
+
readonly content: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type WebFetchPageOutcome = {
|
|
23
|
+
readonly kind: "page";
|
|
24
|
+
readonly text: string;
|
|
25
|
+
readonly title: string;
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly format: WebFetchResolvedFormat;
|
|
28
|
+
readonly returnedBytes: number;
|
|
29
|
+
readonly totalBytes: number;
|
|
30
|
+
readonly truncated: boolean;
|
|
31
|
+
readonly path: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type WebFetchOutcome = WebFetchPageOutcome | WebFetchImageOutcome;
|
|
35
|
+
|
|
36
|
+
export function truncationFooter(
|
|
37
|
+
returnedBytes: number,
|
|
38
|
+
totalBytes: number,
|
|
39
|
+
spillPath: string | null,
|
|
40
|
+
nextStart: number
|
|
41
|
+
): string {
|
|
42
|
+
const base = `[web_fetch tool: showing first ${returnedBytes} bytes of ${totalBytes}`;
|
|
43
|
+
if (spillPath) {
|
|
44
|
+
return `${base}; use read with path=${spillPath} and start=${nextStart} for the rest.]`;
|
|
45
|
+
}
|
|
46
|
+
return `${base}; full content unavailable.]`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validatePublicUrl(value: string): string {
|
|
50
|
+
let url: URL;
|
|
51
|
+
try {
|
|
52
|
+
url = new URL(value);
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error(`URL must be a valid public HTTP or HTTPS URL: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
58
|
+
throw new Error(`URL must use http:// or https://: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (url.username.length > 0 || url.password.length > 0) {
|
|
62
|
+
throw new Error("URL must not contain embedded credentials.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!isPublicHostname(url.hostname)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`URL must use a public hostname or IP address (no localhost, .local, or RFC1918 ranges): ${url.hostname}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return url.href;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function formatOutcome(
|
|
75
|
+
page: WebFetchPage,
|
|
76
|
+
format: WebFetchResolvedFormat
|
|
77
|
+
): Promise<WebFetchPageOutcome> {
|
|
78
|
+
const { body, returnedBytes, totalBytes, truncated } =
|
|
79
|
+
OutputBudget.truncateUtf8(page.content, WEB_FETCH_INLINE_BYTES);
|
|
80
|
+
const path = truncated
|
|
81
|
+
? await SpillCache.write(
|
|
82
|
+
"fetch",
|
|
83
|
+
format === "html" ? "html" : "md",
|
|
84
|
+
page.content
|
|
85
|
+
)
|
|
86
|
+
: null;
|
|
87
|
+
|
|
88
|
+
const lines = [
|
|
89
|
+
`title: ${page.title}`,
|
|
90
|
+
`url: ${page.url}`,
|
|
91
|
+
`format: ${format}`,
|
|
92
|
+
"content:",
|
|
93
|
+
body,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
if (truncated) {
|
|
97
|
+
const nextStart = Lines.continuationLine(body);
|
|
98
|
+
lines.push(
|
|
99
|
+
"",
|
|
100
|
+
truncationFooter(returnedBytes, totalBytes, path, nextStart)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
kind: "page",
|
|
106
|
+
text: lines.join("\n"),
|
|
107
|
+
title: page.title,
|
|
108
|
+
url: page.url,
|
|
109
|
+
format,
|
|
110
|
+
returnedBytes,
|
|
111
|
+
totalBytes,
|
|
112
|
+
truncated,
|
|
113
|
+
path,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type ExecuteFetchInput = {
|
|
118
|
+
readonly jina: JinaReaderClient;
|
|
119
|
+
readonly webView: WebViewFetchClient;
|
|
120
|
+
readonly url: string;
|
|
121
|
+
readonly format: WebFetchFormat;
|
|
122
|
+
readonly signal?: AbortSignal;
|
|
123
|
+
/** Only the image probe takes it; the readers own their own transports. */
|
|
124
|
+
readonly fetch?: HttpFetch;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export async function executeFetch(
|
|
128
|
+
input: ExecuteFetchInput
|
|
129
|
+
): Promise<WebFetchOutcome> {
|
|
130
|
+
const { jina, webView, url, format, signal } = input;
|
|
131
|
+
const fetchInput = {
|
|
132
|
+
url,
|
|
133
|
+
...(signal === undefined ? {} : { signal }),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const image = await fetchImage({
|
|
137
|
+
...fetchInput,
|
|
138
|
+
fetch: input.fetch,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (image !== null) {
|
|
142
|
+
return { kind: "image", url, image };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (format === "html") {
|
|
146
|
+
const page = await webView.fetchHtml(fetchInput);
|
|
147
|
+
return formatOutcome(page, "html");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sources: readonly WebFetchSource[] = [
|
|
151
|
+
() => jina.fetchUrl(fetchInput),
|
|
152
|
+
() => webView.fetchMarkdown(fetchInput),
|
|
153
|
+
];
|
|
154
|
+
let lastError: unknown;
|
|
155
|
+
|
|
156
|
+
for (const source of sources) {
|
|
157
|
+
try {
|
|
158
|
+
return await formatOutcome(await source(), "markdown");
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (signal?.aborted) {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
lastError = error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new Error(`Failed to fetch: ${Errors.describe(lastError)}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type WebFetchSource = () => Promise<WebFetchPage>;
|
|
171
|
+
|
|
172
|
+
function isPublicHostname(hostname: string): boolean {
|
|
173
|
+
const normalized = normalizeHostname(hostname);
|
|
174
|
+
|
|
175
|
+
if (normalized === "localhost" || normalized.endsWith(".local")) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const version = isIP(normalized);
|
|
180
|
+
|
|
181
|
+
if (version === 4) {
|
|
182
|
+
return isPublicIpv4(normalized);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (version === 6) {
|
|
186
|
+
return isPublicIpv6(normalized);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return normalized.length > 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeHostname(hostname: string): string {
|
|
193
|
+
const lower = hostname.toLowerCase().replace(/\.+$/u, "");
|
|
194
|
+
|
|
195
|
+
if (lower.startsWith("[") && lower.endsWith("]")) {
|
|
196
|
+
return lower.slice(1, -1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return lower;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isPublicIpv4(ip: string): boolean {
|
|
203
|
+
const octets = ip.split(".").map((octet) => Number(octet));
|
|
204
|
+
const [a = 0, b = 0, c = 0] = octets;
|
|
205
|
+
|
|
206
|
+
if (a === 0 || a === 10 || a === 127) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (a === 169 && b === 254) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (a === 192 && b === 168) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (a === 100 && b >= 64 && b <= 127) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
if (a === 192 && b === 0 && c === 0) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
if (a === 198 && (b === 18 || b === 19)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return a < 224;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isPublicIpv6(ip: string): boolean {
|
|
232
|
+
const embedded = readEmbeddedIpv4(ip);
|
|
233
|
+
|
|
234
|
+
if (embedded !== undefined && !isPublicIpv4(embedded)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (ip === "::" || ip === "::1") {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const first = readFirstIpv6Segment(ip);
|
|
243
|
+
|
|
244
|
+
if ((first & 0xfe00) === 0xfc00) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if ((first & 0xffc0) === 0xfe80) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return (first & 0xff00) !== 0xff00;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function readEmbeddedIpv4(ip: string): string | undefined {
|
|
255
|
+
const match = /(?:^|:)(\d{1,3}(?:\.\d{1,3}){3})$/u.exec(ip);
|
|
256
|
+
return match?.[1];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readFirstIpv6Segment(ip: string): number {
|
|
260
|
+
const first =
|
|
261
|
+
ip
|
|
262
|
+
.split(":")
|
|
263
|
+
.find((segment) => segment.length > 0 && !segment.includes(".")) ?? "0";
|
|
264
|
+
return Number.parseInt(first, 16);
|
|
265
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Format } from "../../shared/Format";
|
|
3
|
+
import type { HttpFetch } from "../../shared/Http";
|
|
4
|
+
import { Images, type NormalisedImage } from "../../shared/Images";
|
|
5
|
+
import type { WebFetchImageDetails } from "./schema";
|
|
6
|
+
|
|
7
|
+
export type WebFetchImageOutcome = {
|
|
8
|
+
readonly kind: "image";
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly image: NormalisedImage;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ImageFetchInput = {
|
|
14
|
+
readonly url: string;
|
|
15
|
+
readonly signal?: AbortSignal;
|
|
16
|
+
readonly fetch?: HttpFetch;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const TIMEOUT_MS = 20_000;
|
|
20
|
+
|
|
21
|
+
const IMAGE_CONTENT_TYPE = /^image\//iu;
|
|
22
|
+
|
|
23
|
+
/** The picture the URL really serves, or null for anything the page path should handle. */
|
|
24
|
+
export async function fetchImage(
|
|
25
|
+
input: ImageFetchInput
|
|
26
|
+
): Promise<NormalisedImage | null> {
|
|
27
|
+
const response = await declaredImage(input);
|
|
28
|
+
|
|
29
|
+
if (response === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const bytes = await readCapped(response, input.url);
|
|
34
|
+
return bytes === null ? null : Images.normalise(bytes, input.url);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function imageContent(
|
|
38
|
+
image: NormalisedImage
|
|
39
|
+
): AgentToolResult<unknown>["content"] {
|
|
40
|
+
const footer =
|
|
41
|
+
image.cachePath === null
|
|
42
|
+
? []
|
|
43
|
+
: [
|
|
44
|
+
{
|
|
45
|
+
type: "text" as const,
|
|
46
|
+
text: `[web_fetch tool: image saved to ${image.cachePath}]`,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
return [...Images.contentOf(image, Images.noteOf(image)), ...footer];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The fetch succeeded and the intent was ambiguous, so the model is told what it got, not refused. */
|
|
53
|
+
export function noVisionNote(url: string, image: NormalisedImage): string {
|
|
54
|
+
const saved = image.cachePath === null ? "" : `, saved to ${image.cachePath}`;
|
|
55
|
+
return Images.noVisionNote(
|
|
56
|
+
"web_fetch",
|
|
57
|
+
`${url} is a ${image.width}x${image.height} ${Images.extensionOf(image.mimeType)}${saved}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function imageDetails(
|
|
62
|
+
url: string,
|
|
63
|
+
image: NormalisedImage,
|
|
64
|
+
withheld: boolean
|
|
65
|
+
): WebFetchImageDetails {
|
|
66
|
+
return {
|
|
67
|
+
kind: "image",
|
|
68
|
+
url,
|
|
69
|
+
...Images.detailsOf(image),
|
|
70
|
+
path: image.cachePath,
|
|
71
|
+
withheld,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A response whose own headers claim a picture; the bytes still have to agree. */
|
|
76
|
+
async function declaredImage(input: ImageFetchInput): Promise<Response | null> {
|
|
77
|
+
let response: Response;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
response = await (input.fetch ?? fetch)(input.url, {
|
|
81
|
+
headers: { Accept: "image/*,*/*;q=0.8" },
|
|
82
|
+
signal: deadline(input.signal),
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (input.signal?.aborted) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
92
|
+
|
|
93
|
+
if (!response.ok || !IMAGE_CONTENT_TYPE.test(contentType)) {
|
|
94
|
+
await discard(response);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
Number(response.headers.get("content-length")) > Images.MAX_SOURCE_BYTES
|
|
100
|
+
) {
|
|
101
|
+
await discard(response);
|
|
102
|
+
throw oversize(input.url);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return response;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Stops at the cap and at the first chunk that is not a picture, so neither is ever buffered whole. */
|
|
109
|
+
async function readCapped(
|
|
110
|
+
response: Response,
|
|
111
|
+
url: string
|
|
112
|
+
): Promise<Uint8Array | null> {
|
|
113
|
+
const reader = response.body?.getReader();
|
|
114
|
+
|
|
115
|
+
if (reader === undefined) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const chunks: Uint8Array[] = [];
|
|
120
|
+
let total = 0;
|
|
121
|
+
let sniffed = false;
|
|
122
|
+
|
|
123
|
+
while (true) {
|
|
124
|
+
const { done, value } = await reader.read();
|
|
125
|
+
|
|
126
|
+
if (done) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
chunks.push(value);
|
|
131
|
+
total += value.byteLength;
|
|
132
|
+
|
|
133
|
+
if (total > Images.MAX_SOURCE_BYTES) {
|
|
134
|
+
await reader.cancel();
|
|
135
|
+
throw oversize(url);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!sniffed && total >= Images.SNIFF_BYTES) {
|
|
139
|
+
if (Images.sniff(Buffer.concat(chunks)) === null) {
|
|
140
|
+
await reader.cancel();
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
sniffed = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return sniffed ? Buffer.concat(chunks) : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function oversize(url: string): Error {
|
|
151
|
+
return new Error(
|
|
152
|
+
`Image at ${url} is over the ${Format.bytes(Images.MAX_SOURCE_BYTES)} fetch cap. Download and shrink it with bash: curl -sL ${url} -o /tmp/image && magick /tmp/image -resize 2000x2000 /tmp/small.png`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function deadline(signal: AbortSignal | undefined): AbortSignal {
|
|
157
|
+
const timeout = AbortSignal.timeout(TIMEOUT_MS);
|
|
158
|
+
return signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function discard(response: Response): Promise<void> {
|
|
162
|
+
try {
|
|
163
|
+
await response.body?.cancel();
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Images } from "../../shared/Images";
|
|
3
|
+
import { PimSettings } from "../../shared/PimSettings";
|
|
4
|
+
import { SpillCache } from "../../shared/SpillCache";
|
|
5
|
+
import { Tools } from "../../shared/Tools";
|
|
6
|
+
import { executeFetch, validatePublicUrl } from "./fetch";
|
|
7
|
+
import { imageContent, imageDetails, noVisionNote } from "./image";
|
|
8
|
+
import { JinaReaderClient } from "./JinaReaderClient";
|
|
9
|
+
import { webFetchView } from "./render";
|
|
10
|
+
import {
|
|
11
|
+
type WebFetchDetails,
|
|
12
|
+
type WebFetchInput,
|
|
13
|
+
webFetchSchema,
|
|
14
|
+
} from "./schema";
|
|
15
|
+
import { WebViewFetchClient } from "./WebViewFetchClient";
|
|
16
|
+
|
|
17
|
+
async function createJina(): Promise<JinaReaderClient> {
|
|
18
|
+
const apiKey = await PimSettings.getJinaApiKey();
|
|
19
|
+
return new JinaReaderClient(apiKey ? { apiKey } : {});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function (pi: ExtensionAPI): void {
|
|
23
|
+
SpillCache.installSweeper();
|
|
24
|
+
|
|
25
|
+
let jinaPromise: Promise<JinaReaderClient> | undefined;
|
|
26
|
+
const getJina = () => (jinaPromise ??= createJina());
|
|
27
|
+
const webView = new WebViewFetchClient();
|
|
28
|
+
|
|
29
|
+
Tools.register<typeof webFetchSchema, WebFetchDetails>(pi, {
|
|
30
|
+
name: "web_fetch",
|
|
31
|
+
label: "web_fetch",
|
|
32
|
+
description:
|
|
33
|
+
"Fetch a web page as markdown or HTML. " +
|
|
34
|
+
"A URL serving an image (png, jpeg, gif, webp) returns the picture itself, saved to the cache; the format parameter does not apply to it.",
|
|
35
|
+
parameters: webFetchSchema,
|
|
36
|
+
renderShell: "self",
|
|
37
|
+
effect: { kind: "readOnly" },
|
|
38
|
+
executionMode: "parallel",
|
|
39
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
40
|
+
const { url, format } = params as WebFetchInput;
|
|
41
|
+
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
throw new Error("Web fetch aborted before execution.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const safeUrl = validatePublicUrl(url);
|
|
47
|
+
|
|
48
|
+
const jina = await getJina();
|
|
49
|
+
const outcome = await executeFetch({
|
|
50
|
+
jina,
|
|
51
|
+
webView,
|
|
52
|
+
url: safeUrl,
|
|
53
|
+
format: format ?? "markdown",
|
|
54
|
+
...(signal === undefined ? {} : { signal }),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (outcome.kind === "image") {
|
|
58
|
+
const withheld = !Images.canSee(ctx.model);
|
|
59
|
+
return {
|
|
60
|
+
content: withheld
|
|
61
|
+
? [{ type: "text", text: noVisionNote(outcome.url, outcome.image) }]
|
|
62
|
+
: imageContent(outcome.image),
|
|
63
|
+
details: imageDetails(outcome.url, outcome.image, withheld),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: outcome.text }],
|
|
69
|
+
details: {
|
|
70
|
+
kind: "page",
|
|
71
|
+
url: outcome.url,
|
|
72
|
+
title: outcome.title,
|
|
73
|
+
format: outcome.format,
|
|
74
|
+
returnedBytes: outcome.returnedBytes,
|
|
75
|
+
totalBytes: outcome.totalBytes,
|
|
76
|
+
truncated: outcome.truncated,
|
|
77
|
+
path: outcome.path,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
toViewModel: webFetchView,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Format } from "../../shared/Format";
|
|
2
|
+
import { Images } from "../../shared/Images";
|
|
3
|
+
import { Renderer } from "../../shared/Renderer";
|
|
4
|
+
import type { ToolViewInput } from "../../shared/Tools";
|
|
5
|
+
import { Painting } from "../../view/Painting";
|
|
6
|
+
import type { ToolView, ViewBlock } from "../../view/ViewBlock";
|
|
7
|
+
import type {
|
|
8
|
+
WebFetchDetails,
|
|
9
|
+
WebFetchFormat,
|
|
10
|
+
WebFetchImageDetails,
|
|
11
|
+
WebFetchInput,
|
|
12
|
+
WebFetchResolvedFormat,
|
|
13
|
+
webFetchSchema,
|
|
14
|
+
} from "./schema";
|
|
15
|
+
|
|
16
|
+
type WebFetchViewInput = ToolViewInput<typeof webFetchSchema, WebFetchDetails>;
|
|
17
|
+
|
|
18
|
+
type TitleOutcome = {
|
|
19
|
+
readonly format: WebFetchResolvedFormat;
|
|
20
|
+
readonly totalBytes: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function webFetchView({ args, result }: WebFetchViewInput): ToolView {
|
|
24
|
+
const input = (args ?? {}) as Partial<WebFetchInput>;
|
|
25
|
+
const image = imageDetailsOf(result?.details);
|
|
26
|
+
const url = image?.url ?? input.url ?? "...";
|
|
27
|
+
return {
|
|
28
|
+
label: "Web Fetch",
|
|
29
|
+
icon: "globe",
|
|
30
|
+
title: [
|
|
31
|
+
{ kind: "text", text: url },
|
|
32
|
+
{
|
|
33
|
+
kind: "text",
|
|
34
|
+
tone: "muted",
|
|
35
|
+
text: image
|
|
36
|
+
? imageDetail(image)
|
|
37
|
+
: formatDetail(input.format, titleOutcome(result)),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
body: image ? imageBody(image, url) : formatBody(result),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Legacy sessions predate the tag, so anything untagged reads as a page. */
|
|
45
|
+
function imageDetailsOf(
|
|
46
|
+
details: WebFetchDetails | undefined
|
|
47
|
+
): WebFetchImageDetails | undefined {
|
|
48
|
+
return details?.kind === "image" ? details : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function imageBody(
|
|
52
|
+
details: WebFetchImageDetails,
|
|
53
|
+
url: string
|
|
54
|
+
): readonly ViewBlock[] {
|
|
55
|
+
return Painting.imageBlocks(
|
|
56
|
+
details,
|
|
57
|
+
url,
|
|
58
|
+
details.withheld
|
|
59
|
+
? [["not sent", "the current model has no vision input"]]
|
|
60
|
+
: []
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function imageDetail(details: WebFetchImageDetails): string {
|
|
65
|
+
return `${Format.bytesCompact(details.bytes)} ${Images.extensionOf(details.mimeType).toUpperCase()}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function titleOutcome(
|
|
69
|
+
result: WebFetchViewInput["result"]
|
|
70
|
+
): TitleOutcome | undefined {
|
|
71
|
+
const details = result?.details;
|
|
72
|
+
if (
|
|
73
|
+
details === undefined ||
|
|
74
|
+
details.kind === "image" ||
|
|
75
|
+
details.format === undefined ||
|
|
76
|
+
typeof details.totalBytes !== "number"
|
|
77
|
+
) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return { format: details.format, totalBytes: details.totalBytes };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatBody(result: WebFetchViewInput["result"]): readonly ViewBlock[] {
|
|
84
|
+
const text = Renderer.firstText(result);
|
|
85
|
+
return text === "" ? [] : [{ kind: "text", text }];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatDetail(
|
|
89
|
+
format: WebFetchFormat | undefined,
|
|
90
|
+
outcome: TitleOutcome | undefined
|
|
91
|
+
): string {
|
|
92
|
+
const label = formatLabel(outcome?.format ?? format ?? "markdown");
|
|
93
|
+
return outcome === undefined
|
|
94
|
+
? label
|
|
95
|
+
: `${Format.bytesCompact(outcome.totalBytes)} ${label}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatLabel(format: WebFetchResolvedFormat): string {
|
|
99
|
+
return format === "html" ? "HTML" : "Markdown";
|
|
100
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import type { ImageDetails } from "../../shared/Images";
|
|
4
|
+
|
|
5
|
+
export const WEB_FETCH_INLINE_BYTES = 32 * 1024;
|
|
6
|
+
|
|
7
|
+
export const FETCH_FORMATS = ["markdown", "html"] as const;
|
|
8
|
+
export type WebFetchFormat = (typeof FETCH_FORMATS)[number];
|
|
9
|
+
export type WebFetchResolvedFormat = WebFetchFormat;
|
|
10
|
+
|
|
11
|
+
export const webFetchSchema = Type.Object({
|
|
12
|
+
url: Type.String({
|
|
13
|
+
minLength: 1,
|
|
14
|
+
description: "Must be a public http(s) URL.",
|
|
15
|
+
}),
|
|
16
|
+
format: Type.Optional(
|
|
17
|
+
StringEnum(FETCH_FORMATS, {
|
|
18
|
+
description:
|
|
19
|
+
"`markdown`: used by default. `html`: use only when raw source is required.",
|
|
20
|
+
})
|
|
21
|
+
),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type WebFetchInput = Static<typeof webFetchSchema>;
|
|
25
|
+
|
|
26
|
+
export type WebFetchPageDetails = {
|
|
27
|
+
readonly kind: "page";
|
|
28
|
+
readonly url: string;
|
|
29
|
+
readonly title: string;
|
|
30
|
+
readonly format: WebFetchResolvedFormat;
|
|
31
|
+
readonly returnedBytes: number;
|
|
32
|
+
readonly totalBytes: number;
|
|
33
|
+
readonly truncated: boolean;
|
|
34
|
+
readonly path: string | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type WebFetchImageDetails = ImageDetails & {
|
|
38
|
+
readonly kind: "image";
|
|
39
|
+
readonly url: string;
|
|
40
|
+
/** `~/.pim/cache/img-<sha256>.<ext>`, or null when the cache write failed. */
|
|
41
|
+
readonly path: string | null;
|
|
42
|
+
/** Cached and shown here, but never sent: the current model has no vision input. */
|
|
43
|
+
readonly withheld: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type WebFetchDetails = WebFetchPageDetails | WebFetchImageDetails;
|