@asterxsk/kiln 0.4.0 → 0.4.2
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 +155 -155
- package/agent/AGENTS.md +67 -67
- package/agent/README.md +5 -5
- package/agent/extensions/ask-user/index.ts +418 -418
- package/agent/extensions/ask-user/install.ps1 +27 -27
- package/agent/extensions/ask-user/install.sh +24 -24
- package/agent/extensions/ask-user/package-lock.json +769 -769
- package/agent/extensions/ask-user/package.json +19 -19
- package/agent/extensions/ask-user/prompt.ts +45 -45
- package/agent/extensions/ask-user/tsconfig.json +7 -7
- package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -942
- package/agent/extensions/background-terminals/index.ts +627 -627
- package/agent/extensions/background-terminals/install.ps1 +27 -27
- package/agent/extensions/background-terminals/install.sh +24 -24
- package/agent/extensions/background-terminals/manager.test.ts +735 -735
- package/agent/extensions/background-terminals/output.test.ts +109 -109
- package/agent/extensions/background-terminals/package-lock.json +769 -769
- package/agent/extensions/background-terminals/package.json +17 -17
- package/agent/extensions/background-terminals/prompt.test.ts +125 -125
- package/agent/extensions/background-terminals/ps.test.ts +82 -82
- package/agent/extensions/background-terminals/result-delivery.test.ts +44 -44
- package/agent/extensions/background-terminals/src/domain.ts +87 -87
- package/agent/extensions/background-terminals/src/manager.ts +907 -907
- package/agent/extensions/background-terminals/src/output.ts +84 -84
- package/agent/extensions/background-terminals/src/prompt.ts +142 -142
- package/agent/extensions/background-terminals/src/result-delivery.ts +27 -27
- package/agent/extensions/background-terminals/src/runtime.ts +36 -36
- package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -79
- package/agent/extensions/background-terminals/src/ui/ps.ts +621 -621
- package/agent/extensions/background-terminals/tsconfig.json +7 -7
- package/agent/extensions/destructive/README.md +31 -31
- package/agent/extensions/destructive/index.ts +88 -88
- package/agent/extensions/destructive/install.ps1 +27 -27
- package/agent/extensions/destructive/install.sh +24 -24
- package/agent/extensions/file-search/index.spec.ts +443 -443
- package/agent/extensions/file-search/index.ts +459 -459
- package/agent/extensions/file-search/install.ps1 +27 -27
- package/agent/extensions/file-search/install.sh +24 -24
- package/agent/extensions/file-search/package-lock.json +2253 -2253
- package/agent/extensions/file-search/package.json +23 -23
- package/agent/extensions/file-search/src/args.ts +122 -122
- package/agent/extensions/file-search/src/binaries.ts +422 -422
- package/agent/extensions/file-search/src/output.ts +126 -126
- package/agent/extensions/file-search/src/process.ts +146 -146
- package/agent/extensions/file-search/src/prompt.ts +52 -52
- package/agent/extensions/file-search/tsconfig.json +7 -7
- package/agent/extensions/goal/README.md +50 -50
- package/agent/extensions/goal/index.ts +155 -155
- package/agent/extensions/goal/install.ps1 +27 -27
- package/agent/extensions/goal/install.sh +24 -24
- package/agent/extensions/kiln-update/README.md +38 -25
- package/agent/extensions/kiln-update/index.ts +177 -99
- package/agent/extensions/modelconf/PLAN.md +915 -915
- package/agent/extensions/modelconf/README.md +66 -66
- package/agent/extensions/modelconf/index.ts +296 -296
- package/agent/extensions/modelconf/install.ps1 +27 -27
- package/agent/extensions/modelconf/install.sh +24 -24
- package/agent/extensions/modelconf/package-lock.json +1809 -1809
- package/agent/extensions/modelconf/package.json +13 -13
- package/agent/extensions/modelconf/src/fuzzy.ts +26 -26
- package/agent/extensions/modelconf/src/glob.ts +13 -13
- package/agent/extensions/modelconf/src/persistence.test.ts +46 -46
- package/agent/extensions/modelconf/src/persistence.ts +164 -164
- package/agent/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -75
- package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -1101
- package/agent/extensions/modelconf/tsconfig.json +15 -15
- package/agent/extensions/pi-web-access/CHANGELOG.md +690 -690
- package/agent/extensions/pi-web-access/LICENSE +21 -21
- package/agent/extensions/pi-web-access/README.md +470 -470
- package/agent/extensions/pi-web-access/SECURITY.md +5 -5
- package/agent/extensions/pi-web-access/activity.ts +101 -101
- package/agent/extensions/pi-web-access/auth-fetch.ts +148 -148
- package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -272
- package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -669
- package/agent/extensions/pi-web-access/content-find.ts +139 -139
- package/agent/extensions/pi-web-access/credential-source.ts +191 -191
- package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -406
- package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -568
- package/agent/extensions/pi-web-access/declared-web-links.ts +173 -173
- package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -496
- package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -140
- package/agent/extensions/pi-web-access/exa.ts +526 -526
- package/agent/extensions/pi-web-access/extract.ts +1196 -1196
- package/agent/extensions/pi-web-access/feature-config.ts +29 -29
- package/agent/extensions/pi-web-access/fetch-params.ts +111 -111
- package/agent/extensions/pi-web-access/gemini-adc.ts +298 -298
- package/agent/extensions/pi-web-access/gemini-api.ts +353 -353
- package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -108
- package/agent/extensions/pi-web-access/gemini-search.ts +21 -21
- package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -128
- package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -101
- package/agent/extensions/pi-web-access/gemini-web.ts +487 -487
- package/agent/extensions/pi-web-access/github-api.ts +197 -197
- package/agent/extensions/pi-web-access/github-extract.ts +746 -746
- package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -700
- package/agent/extensions/pi-web-access/index.ts +1737 -1737
- package/agent/extensions/pi-web-access/package-lock.json +5808 -5808
- package/agent/extensions/pi-web-access/package.json +64 -64
- package/agent/extensions/pi-web-access/page-query.ts +96 -96
- package/agent/extensions/pi-web-access/pdf-extract.ts +409 -409
- package/agent/extensions/pi-web-access/promise-try.d.ts +7 -7
- package/agent/extensions/pi-web-access/query-rewrite.ts +51 -51
- package/agent/extensions/pi-web-access/render-search-error.ts +170 -170
- package/agent/extensions/pi-web-access/rsc-extract.ts +338 -338
- package/agent/extensions/pi-web-access/search-types.ts +20 -20
- package/agent/extensions/pi-web-access/source-check.ts +282 -282
- package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -526
- package/agent/extensions/pi-web-access/storage.ts +521 -521
- package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -125
- package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -208
- package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -840
- package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -441
- package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -283
- package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -25
- package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -118
- package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -210
- package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -552
- package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -212
- package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -40
- package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -334
- package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -95
- package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -53
- package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -92
- package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -86
- package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -34
- package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -173
- package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -257
- package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -170
- package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -133
- package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -178
- package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -148
- package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -223
- package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -378
- package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -565
- package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -99
- package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -118
- package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -52
- package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -50
- package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -51
- package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -140
- package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -500
- package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -286
- package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -52
- package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -102
- package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -152
- package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -274
- package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -179
- package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -205
- package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -456
- package/agent/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -106
- package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -182
- package/agent/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -66
- package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -64
- package/agent/extensions/pi-web-access/tsconfig.json +11 -11
- package/agent/extensions/pi-web-access/utils.ts +451 -451
- package/agent/extensions/pi-web-access/video-extract.ts +392 -392
- package/agent/extensions/pi-web-access/youtube-extract.ts +328 -328
- package/agent/extensions/shared/activity-status.ts +31 -31
- package/agent/extensions/shared/child-session.test.ts +270 -270
- package/agent/extensions/shared/child-session.ts +148 -148
- package/agent/extensions/shared/context-utilization.test.ts +48 -48
- package/agent/extensions/shared/context-utilization.ts +47 -47
- package/agent/extensions/shared/dashboard-state.ts +99 -99
- package/agent/extensions/shared/install.ps1 +27 -27
- package/agent/extensions/shared/install.sh +24 -24
- package/agent/extensions/shared/tool-call-timeout.test.ts +117 -117
- package/agent/extensions/shared/tool-call-timeout.ts +104 -104
- package/agent/extensions/skillsconf/README.md +74 -74
- package/agent/extensions/skillsconf/index.ts +92 -92
- package/agent/extensions/skillsconf/install.ps1 +27 -27
- package/agent/extensions/skillsconf/install.sh +24 -24
- package/agent/extensions/skillsconf/package-lock.json +1809 -1809
- package/agent/extensions/skillsconf/package.json +18 -18
- package/agent/extensions/skillsconf/src/delete-skill.test.ts +64 -64
- package/agent/extensions/skillsconf/src/delete-skill.ts +45 -45
- package/agent/extensions/skillsconf/src/filter.test.ts +80 -80
- package/agent/extensions/skillsconf/src/filter.ts +74 -74
- package/agent/extensions/skillsconf/src/fuzzy.ts +26 -26
- package/agent/extensions/skillsconf/src/persistence.test.ts +72 -72
- package/agent/extensions/skillsconf/src/persistence.ts +169 -169
- package/agent/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -337
- package/agent/extensions/skillsconf/src/ui/SkillConfView.ts +758 -758
- package/agent/extensions/skillsconf/src/ui/text-input.ts +91 -91
- package/agent/extensions/skillsconf/src/ui/tui-helpers.ts +144 -144
- package/agent/extensions/skillsconf/tsconfig.json +15 -15
- package/agent/extensions/statusline/index.ts +273 -282
- package/agent/extensions/statusline/install.ps1 +27 -27
- package/agent/extensions/statusline/install.sh +24 -24
- package/agent/extensions/subagents/by-the-way.test.ts +29 -29
- package/agent/extensions/subagents/claude.test.ts +119 -119
- package/agent/extensions/subagents/codex.test.ts +102 -102
- package/agent/extensions/subagents/context-usage.test.ts +107 -107
- package/agent/extensions/subagents/docs/design-plan.md +568 -568
- package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -354
- package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -571
- package/agent/extensions/subagents/index.ts +779 -779
- package/agent/extensions/subagents/install.ps1 +27 -27
- package/agent/extensions/subagents/install.sh +24 -24
- package/agent/extensions/subagents/manager.test.ts +276 -276
- package/agent/extensions/subagents/package-lock.json +2244 -2244
- package/agent/extensions/subagents/package.json +19 -19
- package/agent/extensions/subagents/result-delivery.test.ts +27 -27
- package/agent/extensions/subagents/src/backend.ts +73 -73
- package/agent/extensions/subagents/src/backends/claude.ts +701 -701
- package/agent/extensions/subagents/src/backends/codex.ts +1060 -1060
- package/agent/extensions/subagents/src/backends/pi.ts +575 -575
- package/agent/extensions/subagents/src/backends/stub.ts +300 -300
- package/agent/extensions/subagents/src/by-the-way.ts +21 -21
- package/agent/extensions/subagents/src/domain.ts +253 -253
- package/agent/extensions/subagents/src/format.ts +74 -74
- package/agent/extensions/subagents/src/manager.ts +736 -736
- package/agent/extensions/subagents/src/prompt.ts +92 -92
- package/agent/extensions/subagents/src/result-delivery.ts +20 -20
- package/agent/extensions/subagents/src/runtime.ts +53 -53
- package/agent/extensions/subagents/src/ui/takeover.ts +583 -583
- package/agent/extensions/subagents/src/ui/transcript.ts +201 -201
- package/agent/extensions/subagents/takeover.test.ts +29 -29
- package/agent/extensions/subagents/tsconfig.json +7 -7
- package/agent/extensions/todo/AGENTS.md +38 -38
- package/agent/extensions/todo/LICENSE +21 -21
- package/agent/extensions/todo/config.ts +55 -55
- package/agent/extensions/todo/index.ts +151 -151
- package/agent/extensions/todo/install.ps1 +27 -27
- package/agent/extensions/todo/install.sh +24 -24
- package/agent/extensions/todo/locales/de.json +17 -17
- package/agent/extensions/todo/locales/en.json +15 -15
- package/agent/extensions/todo/locales/es.json +17 -17
- package/agent/extensions/todo/locales/fr.json +17 -17
- package/agent/extensions/todo/locales/pt-BR.json +17 -17
- package/agent/extensions/todo/locales/pt.json +17 -17
- package/agent/extensions/todo/locales/ru.json +17 -17
- package/agent/extensions/todo/locales/uk.json +17 -17
- package/agent/extensions/todo/locales/zh.json +17 -17
- package/agent/extensions/todo/package-lock.json +3358 -3358
- package/agent/extensions/todo/package.json +67 -67
- package/agent/extensions/todo/state/i18n-bridge.ts +64 -64
- package/agent/extensions/todo/state/invariants.ts +20 -20
- package/agent/extensions/todo/state/replay.ts +38 -38
- package/agent/extensions/todo/state/selectors.ts +107 -107
- package/agent/extensions/todo/state/state-reducer.ts +326 -326
- package/agent/extensions/todo/state/state.ts +18 -18
- package/agent/extensions/todo/state/store.ts +82 -82
- package/agent/extensions/todo/state/task-graph.ts +57 -57
- package/agent/extensions/todo/todo-overlay.ts +200 -200
- package/agent/extensions/todo/todo.ts +155 -155
- package/agent/extensions/todo/tool/response-envelope.ts +109 -109
- package/agent/extensions/todo/tool/types.ts +206 -206
- package/agent/extensions/todo/view/format.ts +177 -177
- package/agent/extensions/trim-context/README.md +54 -54
- package/agent/extensions/trim-context/index.ts +487 -487
- package/agent/extensions/trim-context/install.ps1 +27 -27
- package/agent/extensions/trim-context/install.sh +24 -24
- package/agent/keybindings.json +7 -7
- package/agent/version.txt +1 -1
- package/bin/kiln.js +20 -13
- package/package.json +1 -1
- package/agent/extensions/taste/index.ts +0 -443
- package/agent/extensions/taste/install.ps1 +0 -27
- package/agent/extensions/taste/install.sh +0 -24
|
@@ -1,459 +1,459 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* file-search — first-class `fd` and `rg` tools for pi.
|
|
3
|
-
*
|
|
4
|
-
* On session start the extension resolves a usable binary for each tool:
|
|
5
|
-
* a normally installed system binary is preferred (silently), then an
|
|
6
|
-
* existing fallback in this repo's `bin/` directory (silently), and only
|
|
7
|
-
* when neither exists is an official release downloaded into `bin/` — the
|
|
8
|
-
* single case that shows a UI notification. Tools await that initialization
|
|
9
|
-
* before executing, and report a clear error if it failed.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// Deep import: the package root eagerly loads NodeRedis -> ioredis, whose CJS
|
|
13
|
-
// interop (`exports = module.exports = ...`) blows Bun's stack when imported
|
|
14
|
-
// from pi's embedded runtime.
|
|
15
|
-
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
16
|
-
import type {
|
|
17
|
-
AgentToolResult,
|
|
18
|
-
ExtensionAPI,
|
|
19
|
-
ExtensionContext,
|
|
20
|
-
} from "@earendil-works/pi-coding-agent";
|
|
21
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
22
|
-
import { Cause, Data, Effect, Exit } from "effect";
|
|
23
|
-
import { Type } from "typebox";
|
|
24
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
25
|
-
import {
|
|
26
|
-
buildFdArgs,
|
|
27
|
-
buildRgArgs,
|
|
28
|
-
FD_MAX_DEPTH_LIMIT,
|
|
29
|
-
FD_MAX_LIMIT,
|
|
30
|
-
RG_MAX_CONTEXT,
|
|
31
|
-
RG_MAX_COUNT_LIMIT,
|
|
32
|
-
} from "./src/args.ts";
|
|
33
|
-
import {
|
|
34
|
-
currentTarget,
|
|
35
|
-
liveBinaryEnv,
|
|
36
|
-
repositoryBinDir,
|
|
37
|
-
resolveBinary,
|
|
38
|
-
TOOL_SPECS,
|
|
39
|
-
type BinaryEnv,
|
|
40
|
-
type BinarySource,
|
|
41
|
-
type PlatformTarget,
|
|
42
|
-
type ResolvedBinary,
|
|
43
|
-
} from "./src/binaries.ts";
|
|
44
|
-
import { formatCapturedOutput, type CapturedOutput } from "./src/output.ts";
|
|
45
|
-
import {
|
|
46
|
-
FD_PARAMETER_DESCRIPTIONS,
|
|
47
|
-
FD_PROMPT_GUIDELINES,
|
|
48
|
-
FD_PROMPT_SNIPPET,
|
|
49
|
-
FD_TOOL_DESCRIPTION,
|
|
50
|
-
RG_PARAMETER_DESCRIPTIONS,
|
|
51
|
-
RG_PROMPT_GUIDELINES,
|
|
52
|
-
RG_PROMPT_SNIPPET,
|
|
53
|
-
RG_TOOL_DESCRIPTION,
|
|
54
|
-
} from "./src/prompt.ts";
|
|
55
|
-
import { discardCapturedOutput, executeSearchProcess } from "./src/process.ts";
|
|
56
|
-
|
|
57
|
-
export function makeBinaryInitializers(
|
|
58
|
-
binDir: string,
|
|
59
|
-
target: PlatformTarget,
|
|
60
|
-
env: BinaryEnv,
|
|
61
|
-
) {
|
|
62
|
-
return {
|
|
63
|
-
fd: Effect.runSync(
|
|
64
|
-
Effect.cached(resolveBinary(TOOL_SPECS.fd, binDir, target, env)),
|
|
65
|
-
),
|
|
66
|
-
rg: Effect.runSync(
|
|
67
|
-
Effect.cached(resolveBinary(TOOL_SPECS.rg, binDir, target, env)),
|
|
68
|
-
),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Human-readable install notice, shown only for fresh downloads. */
|
|
73
|
-
export function installNotifications(binaries: readonly ResolvedBinary[]) {
|
|
74
|
-
return binaries
|
|
75
|
-
.filter((binary) => binary.source === "installed")
|
|
76
|
-
.map(
|
|
77
|
-
(binary) =>
|
|
78
|
-
`file-search: no system ${binary.tool} found — downloaded ${binary.tool} ${binary.version ?? ""}`.trimEnd() +
|
|
79
|
-
` to ${repositoryBinDir()}`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
class SearchError extends Data.TaggedError("SearchError")<{
|
|
84
|
-
readonly message: string;
|
|
85
|
-
}> {}
|
|
86
|
-
|
|
87
|
-
interface SearchOutcome {
|
|
88
|
-
readonly output: CapturedOutput;
|
|
89
|
-
readonly noMatches: boolean;
|
|
90
|
-
readonly binarySource: BinarySource;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface FdToolDetails {
|
|
94
|
-
readonly binarySource: BinarySource;
|
|
95
|
-
readonly matchCount: number;
|
|
96
|
-
readonly truncated: boolean;
|
|
97
|
-
readonly fullOutputPath?: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface RgToolDetails {
|
|
101
|
-
readonly binarySource: BinarySource;
|
|
102
|
-
readonly outputLines: number;
|
|
103
|
-
readonly truncated: boolean;
|
|
104
|
-
readonly fullOutputPath?: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const EXEC_TIMEOUT_MS = 60_000;
|
|
108
|
-
|
|
109
|
-
function causeMessage<E>(cause: Cause.Cause<E>) {
|
|
110
|
-
const [first] = Cause.prettyErrors(cause);
|
|
111
|
-
return first?.message ?? Cause.pretty(cause);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function unwrapToolExit<A, E>(exit: Exit.Exit<A, E>, tool: "fd" | "rg") {
|
|
115
|
-
if (Exit.isSuccess(exit)) return exit.value;
|
|
116
|
-
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
117
|
-
throw new Error(`${tool} search was cancelled.`);
|
|
118
|
-
}
|
|
119
|
-
throw new Error(causeMessage(exit.cause));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export default function fileSearchTools(pi: ExtensionAPI) {
|
|
123
|
-
let notified = false;
|
|
124
|
-
|
|
125
|
-
const binDir = repositoryBinDir();
|
|
126
|
-
const target = currentTarget();
|
|
127
|
-
const initializers = makeBinaryInitializers(binDir, target, liveBinaryEnv);
|
|
128
|
-
|
|
129
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
130
|
-
const exit = await Effect.runPromiseExit(
|
|
131
|
-
Effect.gen(function* () {
|
|
132
|
-
const initialized = yield* Effect.all(
|
|
133
|
-
{
|
|
134
|
-
fd: Effect.exit(initializers.fd),
|
|
135
|
-
rg: Effect.exit(initializers.rg),
|
|
136
|
-
},
|
|
137
|
-
{ concurrency: "unbounded" },
|
|
138
|
-
);
|
|
139
|
-
if (!ctx.hasUI || notified) return;
|
|
140
|
-
|
|
141
|
-
notified = true;
|
|
142
|
-
for (const tool of ["fd", "rg"] as const) {
|
|
143
|
-
const toolExit = initialized[tool];
|
|
144
|
-
if (Exit.isSuccess(toolExit)) {
|
|
145
|
-
for (const message of installNotifications([toolExit.value])) {
|
|
146
|
-
ctx.ui.notify(message, "info");
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
ctx.ui.notify(
|
|
150
|
-
`file-search ${tool} setup failed: ${causeMessage(toolExit.cause)}`,
|
|
151
|
-
"error",
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
if (Exit.isFailure(exit) && ctx.hasUI && !notified) {
|
|
159
|
-
notified = true;
|
|
160
|
-
ctx.ui.notify(
|
|
161
|
-
`file-search setup failed: ${causeMessage(exit.cause)}`,
|
|
162
|
-
"error",
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
/** Await init, stream the binary output to disk, and classify its exit. */
|
|
168
|
-
function runSearch(tool: "fd" | "rg", args: string[], ctx: ExtensionContext) {
|
|
169
|
-
return Effect.gen(function* () {
|
|
170
|
-
const binary = yield* initializers[tool];
|
|
171
|
-
const result = yield* executeSearchProcess({
|
|
172
|
-
command: binary.command,
|
|
173
|
-
args,
|
|
174
|
-
cwd: ctx.cwd,
|
|
175
|
-
tempPrefix: `pi-${tool}-`,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ripgrep exits 1 for "no matches"; fd exits 0 even with no results.
|
|
179
|
-
if (tool === "rg" && result.code === 1 && result.output.lineCount === 0) {
|
|
180
|
-
return {
|
|
181
|
-
output: result.output,
|
|
182
|
-
noMatches: true,
|
|
183
|
-
binarySource: binary.source,
|
|
184
|
-
} satisfies SearchOutcome;
|
|
185
|
-
}
|
|
186
|
-
if (result.code !== 0) {
|
|
187
|
-
yield* discardCapturedOutput(result.output);
|
|
188
|
-
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
189
|
-
return yield* new SearchError({ message: `${tool} failed: ${detail}` });
|
|
190
|
-
}
|
|
191
|
-
return {
|
|
192
|
-
output: result.output,
|
|
193
|
-
noMatches: result.output.lineCount === 0,
|
|
194
|
-
binarySource: binary.source,
|
|
195
|
-
} satisfies SearchOutcome;
|
|
196
|
-
}).pipe(
|
|
197
|
-
Effect.timeout(EXEC_TIMEOUT_MS),
|
|
198
|
-
Effect.mapError((error) => {
|
|
199
|
-
if (error instanceof SearchError) return error;
|
|
200
|
-
return new SearchError({
|
|
201
|
-
message:
|
|
202
|
-
error._tag === "TimeoutError"
|
|
203
|
-
? `${tool} timed out.`
|
|
204
|
-
: error instanceof Error
|
|
205
|
-
? error.message
|
|
206
|
-
: String(error),
|
|
207
|
-
});
|
|
208
|
-
}),
|
|
209
|
-
Effect.provide(NodeServices.layer),
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
pi.registerTool<ReturnType<typeof fdParameters>, FdToolDetails>({
|
|
214
|
-
name: "fd",
|
|
215
|
-
label: "Find Files",
|
|
216
|
-
description: FD_TOOL_DESCRIPTION,
|
|
217
|
-
promptSnippet: FD_PROMPT_SNIPPET,
|
|
218
|
-
promptGuidelines: FD_PROMPT_GUIDELINES,
|
|
219
|
-
parameters: fdParameters(),
|
|
220
|
-
|
|
221
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
222
|
-
const exit = await Effect.runPromiseExit(
|
|
223
|
-
Effect.gen(function* () {
|
|
224
|
-
const outcome = yield* runSearch("fd", buildFdArgs(params), ctx);
|
|
225
|
-
if (outcome.noMatches) {
|
|
226
|
-
return {
|
|
227
|
-
content: [{ type: "text", text: "No files found" }],
|
|
228
|
-
details: {
|
|
229
|
-
binarySource: outcome.binarySource,
|
|
230
|
-
matchCount: 0,
|
|
231
|
-
truncated: false,
|
|
232
|
-
},
|
|
233
|
-
} satisfies AgentToolResult<FdToolDetails>;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const formatted = formatCapturedOutput(outcome.output);
|
|
237
|
-
return {
|
|
238
|
-
content: [{ type: "text", text: formatted.text }],
|
|
239
|
-
details: {
|
|
240
|
-
binarySource: outcome.binarySource,
|
|
241
|
-
matchCount: formatted.lineCount,
|
|
242
|
-
truncated: formatted.truncated,
|
|
243
|
-
fullOutputPath: formatted.fullOutputPath,
|
|
244
|
-
},
|
|
245
|
-
} satisfies AgentToolResult<FdToolDetails>;
|
|
246
|
-
}),
|
|
247
|
-
signal ? { signal } : undefined,
|
|
248
|
-
);
|
|
249
|
-
return unwrapToolExit(exit, "fd");
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
renderCall(args, theme) {
|
|
253
|
-
let text = theme.fg("toolTitle", theme.bold("fd "));
|
|
254
|
-
text += theme.fg("accent", args.pattern ? `"${args.pattern}"` : "(all)");
|
|
255
|
-
if (args.path) text += theme.fg("muted", ` in ${args.path}`);
|
|
256
|
-
const flags = [
|
|
257
|
-
args.type && `type=${args.type}`,
|
|
258
|
-
args.extension && `ext=${args.extension}`,
|
|
259
|
-
args.glob && "glob",
|
|
260
|
-
args.hidden && "hidden",
|
|
261
|
-
args.max_depth !== undefined && `depth≤${args.max_depth}`,
|
|
262
|
-
].filter((flag): flag is string => typeof flag === "string");
|
|
263
|
-
if (flags.length > 0) text += " " + theme.fg("dim", flags.join(" "));
|
|
264
|
-
return new Text(text, 1, 0);
|
|
265
|
-
},
|
|
266
|
-
|
|
267
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
268
|
-
if (isPartial) return new Text(theme.fg("warning", "Searching..."), 1, 0);
|
|
269
|
-
const details = result.details;
|
|
270
|
-
if (!details || !details.matchCount) {
|
|
271
|
-
return new Text(theme.fg("dim", "No files found"), 1, 0);
|
|
272
|
-
}
|
|
273
|
-
let text = theme.fg(
|
|
274
|
-
"success",
|
|
275
|
-
`${details.matchCount} ${details.matchCount === 1 ? "entry" : "entries"}`,
|
|
276
|
-
);
|
|
277
|
-
if (details.truncated) text += theme.fg("warning", " (truncated)");
|
|
278
|
-
if (expanded)
|
|
279
|
-
text += expandedPreview(result, details.fullOutputPath, theme);
|
|
280
|
-
return new Text(text, 1, 0);
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
pi.registerTool<ReturnType<typeof rgParameters>, RgToolDetails>({
|
|
285
|
-
name: "rg",
|
|
286
|
-
label: "Search Content",
|
|
287
|
-
description: RG_TOOL_DESCRIPTION,
|
|
288
|
-
promptSnippet: RG_PROMPT_SNIPPET,
|
|
289
|
-
promptGuidelines: RG_PROMPT_GUIDELINES,
|
|
290
|
-
parameters: rgParameters(),
|
|
291
|
-
|
|
292
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
293
|
-
const exit = await Effect.runPromiseExit(
|
|
294
|
-
Effect.gen(function* () {
|
|
295
|
-
const outcome = yield* runSearch("rg", buildRgArgs(params), ctx);
|
|
296
|
-
if (outcome.noMatches) {
|
|
297
|
-
return {
|
|
298
|
-
content: [{ type: "text", text: "No matches found" }],
|
|
299
|
-
details: {
|
|
300
|
-
binarySource: outcome.binarySource,
|
|
301
|
-
outputLines: 0,
|
|
302
|
-
truncated: false,
|
|
303
|
-
},
|
|
304
|
-
} satisfies AgentToolResult<RgToolDetails>;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const formatted = formatCapturedOutput(outcome.output);
|
|
308
|
-
return {
|
|
309
|
-
content: [{ type: "text", text: formatted.text }],
|
|
310
|
-
details: {
|
|
311
|
-
binarySource: outcome.binarySource,
|
|
312
|
-
outputLines: formatted.lineCount,
|
|
313
|
-
truncated: formatted.truncated,
|
|
314
|
-
fullOutputPath: formatted.fullOutputPath,
|
|
315
|
-
},
|
|
316
|
-
} satisfies AgentToolResult<RgToolDetails>;
|
|
317
|
-
}),
|
|
318
|
-
signal ? { signal } : undefined,
|
|
319
|
-
);
|
|
320
|
-
return unwrapToolExit(exit, "rg");
|
|
321
|
-
},
|
|
322
|
-
|
|
323
|
-
renderCall(args, theme) {
|
|
324
|
-
let text = theme.fg("toolTitle", theme.bold("rg "));
|
|
325
|
-
text += theme.fg("accent", `"${args.pattern}"`);
|
|
326
|
-
if (args.path) text += theme.fg("muted", ` in ${args.path}`);
|
|
327
|
-
const flags = [
|
|
328
|
-
args.glob && `glob=${args.glob}`,
|
|
329
|
-
args.file_type && `type=${args.file_type}`,
|
|
330
|
-
args.fixed_strings && "literal",
|
|
331
|
-
args.hidden && "hidden",
|
|
332
|
-
args.context !== undefined && `ctx=${args.context}`,
|
|
333
|
-
].filter((flag): flag is string => typeof flag === "string");
|
|
334
|
-
if (flags.length > 0) text += " " + theme.fg("dim", flags.join(" "));
|
|
335
|
-
return new Text(text, 1, 0);
|
|
336
|
-
},
|
|
337
|
-
|
|
338
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
339
|
-
if (isPartial) return new Text(theme.fg("warning", "Searching..."), 1, 0);
|
|
340
|
-
const details = result.details;
|
|
341
|
-
if (!details || !details.outputLines) {
|
|
342
|
-
return new Text(theme.fg("dim", "No matches found"), 1, 0);
|
|
343
|
-
}
|
|
344
|
-
let text = theme.fg(
|
|
345
|
-
"success",
|
|
346
|
-
`${details.outputLines} output ${details.outputLines === 1 ? "line" : "lines"}`,
|
|
347
|
-
);
|
|
348
|
-
if (details.truncated) text += theme.fg("warning", " (truncated)");
|
|
349
|
-
if (expanded)
|
|
350
|
-
text += expandedPreview(result, details.fullOutputPath, theme);
|
|
351
|
-
return new Text(text, 1, 0);
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const PREVIEW_LINES = 20;
|
|
357
|
-
|
|
358
|
-
interface ThemeLike {
|
|
359
|
-
fg(color: string, text: string): string;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function expandedPreview(
|
|
363
|
-
result: { content: { type: string; text?: string }[] },
|
|
364
|
-
fullOutputPath: string | undefined,
|
|
365
|
-
theme: ThemeLike,
|
|
366
|
-
) {
|
|
367
|
-
let text = "";
|
|
368
|
-
const content = result.content[0];
|
|
369
|
-
if (content?.type === "text" && content.text) {
|
|
370
|
-
const lines = content.text.split("\n");
|
|
371
|
-
for (const line of lines.slice(0, PREVIEW_LINES)) {
|
|
372
|
-
text += `\n${theme.fg("dim", line)}`;
|
|
373
|
-
}
|
|
374
|
-
if (lines.length > PREVIEW_LINES) {
|
|
375
|
-
text += `\n${theme.fg("muted", `... ${lines.length - PREVIEW_LINES} more lines`)}`;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (fullOutputPath) {
|
|
379
|
-
text += `\n${theme.fg("dim", `Full output: ${fullOutputPath}`)}`;
|
|
380
|
-
}
|
|
381
|
-
return text;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function fdParameters() {
|
|
385
|
-
return Type.Object({
|
|
386
|
-
pattern: Type.Optional(
|
|
387
|
-
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.pattern }),
|
|
388
|
-
),
|
|
389
|
-
path: Type.Optional(
|
|
390
|
-
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.path }),
|
|
391
|
-
),
|
|
392
|
-
type: Type.Optional(
|
|
393
|
-
StringEnum(["file", "directory", "symlink"] as const, {
|
|
394
|
-
description: FD_PARAMETER_DESCRIPTIONS.type,
|
|
395
|
-
}),
|
|
396
|
-
),
|
|
397
|
-
extension: Type.Optional(
|
|
398
|
-
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.extension }),
|
|
399
|
-
),
|
|
400
|
-
glob: Type.Optional(
|
|
401
|
-
Type.Boolean({ description: FD_PARAMETER_DESCRIPTIONS.glob }),
|
|
402
|
-
),
|
|
403
|
-
hidden: Type.Optional(
|
|
404
|
-
Type.Boolean({ description: FD_PARAMETER_DESCRIPTIONS.hidden }),
|
|
405
|
-
),
|
|
406
|
-
max_depth: Type.Optional(
|
|
407
|
-
Type.Integer({
|
|
408
|
-
description: FD_PARAMETER_DESCRIPTIONS.max_depth,
|
|
409
|
-
minimum: 1,
|
|
410
|
-
maximum: FD_MAX_DEPTH_LIMIT,
|
|
411
|
-
}),
|
|
412
|
-
),
|
|
413
|
-
limit: Type.Optional(
|
|
414
|
-
Type.Integer({
|
|
415
|
-
description: FD_PARAMETER_DESCRIPTIONS.limit,
|
|
416
|
-
minimum: 1,
|
|
417
|
-
maximum: FD_MAX_LIMIT,
|
|
418
|
-
}),
|
|
419
|
-
),
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function rgParameters() {
|
|
424
|
-
return Type.Object({
|
|
425
|
-
pattern: Type.String({ description: RG_PARAMETER_DESCRIPTIONS.pattern }),
|
|
426
|
-
path: Type.Optional(
|
|
427
|
-
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.path }),
|
|
428
|
-
),
|
|
429
|
-
glob: Type.Optional(
|
|
430
|
-
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.glob }),
|
|
431
|
-
),
|
|
432
|
-
file_type: Type.Optional(
|
|
433
|
-
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.file_type }),
|
|
434
|
-
),
|
|
435
|
-
case_sensitive: Type.Optional(
|
|
436
|
-
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.case_sensitive }),
|
|
437
|
-
),
|
|
438
|
-
fixed_strings: Type.Optional(
|
|
439
|
-
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.fixed_strings }),
|
|
440
|
-
),
|
|
441
|
-
hidden: Type.Optional(
|
|
442
|
-
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.hidden }),
|
|
443
|
-
),
|
|
444
|
-
context: Type.Optional(
|
|
445
|
-
Type.Integer({
|
|
446
|
-
description: RG_PARAMETER_DESCRIPTIONS.context,
|
|
447
|
-
minimum: 0,
|
|
448
|
-
maximum: RG_MAX_CONTEXT,
|
|
449
|
-
}),
|
|
450
|
-
),
|
|
451
|
-
limit: Type.Optional(
|
|
452
|
-
Type.Integer({
|
|
453
|
-
description: RG_PARAMETER_DESCRIPTIONS.limit,
|
|
454
|
-
minimum: 1,
|
|
455
|
-
maximum: RG_MAX_COUNT_LIMIT,
|
|
456
|
-
}),
|
|
457
|
-
),
|
|
458
|
-
});
|
|
459
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* file-search — first-class `fd` and `rg` tools for pi.
|
|
3
|
+
*
|
|
4
|
+
* On session start the extension resolves a usable binary for each tool:
|
|
5
|
+
* a normally installed system binary is preferred (silently), then an
|
|
6
|
+
* existing fallback in this repo's `bin/` directory (silently), and only
|
|
7
|
+
* when neither exists is an official release downloaded into `bin/` — the
|
|
8
|
+
* single case that shows a UI notification. Tools await that initialization
|
|
9
|
+
* before executing, and report a clear error if it failed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Deep import: the package root eagerly loads NodeRedis -> ioredis, whose CJS
|
|
13
|
+
// interop (`exports = module.exports = ...`) blows Bun's stack when imported
|
|
14
|
+
// from pi's embedded runtime.
|
|
15
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
16
|
+
import type {
|
|
17
|
+
AgentToolResult,
|
|
18
|
+
ExtensionAPI,
|
|
19
|
+
ExtensionContext,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
22
|
+
import { Cause, Data, Effect, Exit } from "effect";
|
|
23
|
+
import { Type } from "typebox";
|
|
24
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
25
|
+
import {
|
|
26
|
+
buildFdArgs,
|
|
27
|
+
buildRgArgs,
|
|
28
|
+
FD_MAX_DEPTH_LIMIT,
|
|
29
|
+
FD_MAX_LIMIT,
|
|
30
|
+
RG_MAX_CONTEXT,
|
|
31
|
+
RG_MAX_COUNT_LIMIT,
|
|
32
|
+
} from "./src/args.ts";
|
|
33
|
+
import {
|
|
34
|
+
currentTarget,
|
|
35
|
+
liveBinaryEnv,
|
|
36
|
+
repositoryBinDir,
|
|
37
|
+
resolveBinary,
|
|
38
|
+
TOOL_SPECS,
|
|
39
|
+
type BinaryEnv,
|
|
40
|
+
type BinarySource,
|
|
41
|
+
type PlatformTarget,
|
|
42
|
+
type ResolvedBinary,
|
|
43
|
+
} from "./src/binaries.ts";
|
|
44
|
+
import { formatCapturedOutput, type CapturedOutput } from "./src/output.ts";
|
|
45
|
+
import {
|
|
46
|
+
FD_PARAMETER_DESCRIPTIONS,
|
|
47
|
+
FD_PROMPT_GUIDELINES,
|
|
48
|
+
FD_PROMPT_SNIPPET,
|
|
49
|
+
FD_TOOL_DESCRIPTION,
|
|
50
|
+
RG_PARAMETER_DESCRIPTIONS,
|
|
51
|
+
RG_PROMPT_GUIDELINES,
|
|
52
|
+
RG_PROMPT_SNIPPET,
|
|
53
|
+
RG_TOOL_DESCRIPTION,
|
|
54
|
+
} from "./src/prompt.ts";
|
|
55
|
+
import { discardCapturedOutput, executeSearchProcess } from "./src/process.ts";
|
|
56
|
+
|
|
57
|
+
export function makeBinaryInitializers(
|
|
58
|
+
binDir: string,
|
|
59
|
+
target: PlatformTarget,
|
|
60
|
+
env: BinaryEnv,
|
|
61
|
+
) {
|
|
62
|
+
return {
|
|
63
|
+
fd: Effect.runSync(
|
|
64
|
+
Effect.cached(resolveBinary(TOOL_SPECS.fd, binDir, target, env)),
|
|
65
|
+
),
|
|
66
|
+
rg: Effect.runSync(
|
|
67
|
+
Effect.cached(resolveBinary(TOOL_SPECS.rg, binDir, target, env)),
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Human-readable install notice, shown only for fresh downloads. */
|
|
73
|
+
export function installNotifications(binaries: readonly ResolvedBinary[]) {
|
|
74
|
+
return binaries
|
|
75
|
+
.filter((binary) => binary.source === "installed")
|
|
76
|
+
.map(
|
|
77
|
+
(binary) =>
|
|
78
|
+
`file-search: no system ${binary.tool} found — downloaded ${binary.tool} ${binary.version ?? ""}`.trimEnd() +
|
|
79
|
+
` to ${repositoryBinDir()}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class SearchError extends Data.TaggedError("SearchError")<{
|
|
84
|
+
readonly message: string;
|
|
85
|
+
}> {}
|
|
86
|
+
|
|
87
|
+
interface SearchOutcome {
|
|
88
|
+
readonly output: CapturedOutput;
|
|
89
|
+
readonly noMatches: boolean;
|
|
90
|
+
readonly binarySource: BinarySource;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface FdToolDetails {
|
|
94
|
+
readonly binarySource: BinarySource;
|
|
95
|
+
readonly matchCount: number;
|
|
96
|
+
readonly truncated: boolean;
|
|
97
|
+
readonly fullOutputPath?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface RgToolDetails {
|
|
101
|
+
readonly binarySource: BinarySource;
|
|
102
|
+
readonly outputLines: number;
|
|
103
|
+
readonly truncated: boolean;
|
|
104
|
+
readonly fullOutputPath?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const EXEC_TIMEOUT_MS = 60_000;
|
|
108
|
+
|
|
109
|
+
function causeMessage<E>(cause: Cause.Cause<E>) {
|
|
110
|
+
const [first] = Cause.prettyErrors(cause);
|
|
111
|
+
return first?.message ?? Cause.pretty(cause);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function unwrapToolExit<A, E>(exit: Exit.Exit<A, E>, tool: "fd" | "rg") {
|
|
115
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
116
|
+
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
117
|
+
throw new Error(`${tool} search was cancelled.`);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(causeMessage(exit.cause));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default function fileSearchTools(pi: ExtensionAPI) {
|
|
123
|
+
let notified = false;
|
|
124
|
+
|
|
125
|
+
const binDir = repositoryBinDir();
|
|
126
|
+
const target = currentTarget();
|
|
127
|
+
const initializers = makeBinaryInitializers(binDir, target, liveBinaryEnv);
|
|
128
|
+
|
|
129
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
130
|
+
const exit = await Effect.runPromiseExit(
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
const initialized = yield* Effect.all(
|
|
133
|
+
{
|
|
134
|
+
fd: Effect.exit(initializers.fd),
|
|
135
|
+
rg: Effect.exit(initializers.rg),
|
|
136
|
+
},
|
|
137
|
+
{ concurrency: "unbounded" },
|
|
138
|
+
);
|
|
139
|
+
if (!ctx.hasUI || notified) return;
|
|
140
|
+
|
|
141
|
+
notified = true;
|
|
142
|
+
for (const tool of ["fd", "rg"] as const) {
|
|
143
|
+
const toolExit = initialized[tool];
|
|
144
|
+
if (Exit.isSuccess(toolExit)) {
|
|
145
|
+
for (const message of installNotifications([toolExit.value])) {
|
|
146
|
+
ctx.ui.notify(message, "info");
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
ctx.ui.notify(
|
|
150
|
+
`file-search ${tool} setup failed: ${causeMessage(toolExit.cause)}`,
|
|
151
|
+
"error",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (Exit.isFailure(exit) && ctx.hasUI && !notified) {
|
|
159
|
+
notified = true;
|
|
160
|
+
ctx.ui.notify(
|
|
161
|
+
`file-search setup failed: ${causeMessage(exit.cause)}`,
|
|
162
|
+
"error",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/** Await init, stream the binary output to disk, and classify its exit. */
|
|
168
|
+
function runSearch(tool: "fd" | "rg", args: string[], ctx: ExtensionContext) {
|
|
169
|
+
return Effect.gen(function* () {
|
|
170
|
+
const binary = yield* initializers[tool];
|
|
171
|
+
const result = yield* executeSearchProcess({
|
|
172
|
+
command: binary.command,
|
|
173
|
+
args,
|
|
174
|
+
cwd: ctx.cwd,
|
|
175
|
+
tempPrefix: `pi-${tool}-`,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ripgrep exits 1 for "no matches"; fd exits 0 even with no results.
|
|
179
|
+
if (tool === "rg" && result.code === 1 && result.output.lineCount === 0) {
|
|
180
|
+
return {
|
|
181
|
+
output: result.output,
|
|
182
|
+
noMatches: true,
|
|
183
|
+
binarySource: binary.source,
|
|
184
|
+
} satisfies SearchOutcome;
|
|
185
|
+
}
|
|
186
|
+
if (result.code !== 0) {
|
|
187
|
+
yield* discardCapturedOutput(result.output);
|
|
188
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
189
|
+
return yield* new SearchError({ message: `${tool} failed: ${detail}` });
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
output: result.output,
|
|
193
|
+
noMatches: result.output.lineCount === 0,
|
|
194
|
+
binarySource: binary.source,
|
|
195
|
+
} satisfies SearchOutcome;
|
|
196
|
+
}).pipe(
|
|
197
|
+
Effect.timeout(EXEC_TIMEOUT_MS),
|
|
198
|
+
Effect.mapError((error) => {
|
|
199
|
+
if (error instanceof SearchError) return error;
|
|
200
|
+
return new SearchError({
|
|
201
|
+
message:
|
|
202
|
+
error._tag === "TimeoutError"
|
|
203
|
+
? `${tool} timed out.`
|
|
204
|
+
: error instanceof Error
|
|
205
|
+
? error.message
|
|
206
|
+
: String(error),
|
|
207
|
+
});
|
|
208
|
+
}),
|
|
209
|
+
Effect.provide(NodeServices.layer),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
pi.registerTool<ReturnType<typeof fdParameters>, FdToolDetails>({
|
|
214
|
+
name: "fd",
|
|
215
|
+
label: "Find Files",
|
|
216
|
+
description: FD_TOOL_DESCRIPTION,
|
|
217
|
+
promptSnippet: FD_PROMPT_SNIPPET,
|
|
218
|
+
promptGuidelines: FD_PROMPT_GUIDELINES,
|
|
219
|
+
parameters: fdParameters(),
|
|
220
|
+
|
|
221
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
222
|
+
const exit = await Effect.runPromiseExit(
|
|
223
|
+
Effect.gen(function* () {
|
|
224
|
+
const outcome = yield* runSearch("fd", buildFdArgs(params), ctx);
|
|
225
|
+
if (outcome.noMatches) {
|
|
226
|
+
return {
|
|
227
|
+
content: [{ type: "text", text: "No files found" }],
|
|
228
|
+
details: {
|
|
229
|
+
binarySource: outcome.binarySource,
|
|
230
|
+
matchCount: 0,
|
|
231
|
+
truncated: false,
|
|
232
|
+
},
|
|
233
|
+
} satisfies AgentToolResult<FdToolDetails>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const formatted = formatCapturedOutput(outcome.output);
|
|
237
|
+
return {
|
|
238
|
+
content: [{ type: "text", text: formatted.text }],
|
|
239
|
+
details: {
|
|
240
|
+
binarySource: outcome.binarySource,
|
|
241
|
+
matchCount: formatted.lineCount,
|
|
242
|
+
truncated: formatted.truncated,
|
|
243
|
+
fullOutputPath: formatted.fullOutputPath,
|
|
244
|
+
},
|
|
245
|
+
} satisfies AgentToolResult<FdToolDetails>;
|
|
246
|
+
}),
|
|
247
|
+
signal ? { signal } : undefined,
|
|
248
|
+
);
|
|
249
|
+
return unwrapToolExit(exit, "fd");
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
renderCall(args, theme) {
|
|
253
|
+
let text = theme.fg("toolTitle", theme.bold("fd "));
|
|
254
|
+
text += theme.fg("accent", args.pattern ? `"${args.pattern}"` : "(all)");
|
|
255
|
+
if (args.path) text += theme.fg("muted", ` in ${args.path}`);
|
|
256
|
+
const flags = [
|
|
257
|
+
args.type && `type=${args.type}`,
|
|
258
|
+
args.extension && `ext=${args.extension}`,
|
|
259
|
+
args.glob && "glob",
|
|
260
|
+
args.hidden && "hidden",
|
|
261
|
+
args.max_depth !== undefined && `depth≤${args.max_depth}`,
|
|
262
|
+
].filter((flag): flag is string => typeof flag === "string");
|
|
263
|
+
if (flags.length > 0) text += " " + theme.fg("dim", flags.join(" "));
|
|
264
|
+
return new Text(text, 1, 0);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
268
|
+
if (isPartial) return new Text(theme.fg("warning", "Searching..."), 1, 0);
|
|
269
|
+
const details = result.details;
|
|
270
|
+
if (!details || !details.matchCount) {
|
|
271
|
+
return new Text(theme.fg("dim", "No files found"), 1, 0);
|
|
272
|
+
}
|
|
273
|
+
let text = theme.fg(
|
|
274
|
+
"success",
|
|
275
|
+
`${details.matchCount} ${details.matchCount === 1 ? "entry" : "entries"}`,
|
|
276
|
+
);
|
|
277
|
+
if (details.truncated) text += theme.fg("warning", " (truncated)");
|
|
278
|
+
if (expanded)
|
|
279
|
+
text += expandedPreview(result, details.fullOutputPath, theme);
|
|
280
|
+
return new Text(text, 1, 0);
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
pi.registerTool<ReturnType<typeof rgParameters>, RgToolDetails>({
|
|
285
|
+
name: "rg",
|
|
286
|
+
label: "Search Content",
|
|
287
|
+
description: RG_TOOL_DESCRIPTION,
|
|
288
|
+
promptSnippet: RG_PROMPT_SNIPPET,
|
|
289
|
+
promptGuidelines: RG_PROMPT_GUIDELINES,
|
|
290
|
+
parameters: rgParameters(),
|
|
291
|
+
|
|
292
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
293
|
+
const exit = await Effect.runPromiseExit(
|
|
294
|
+
Effect.gen(function* () {
|
|
295
|
+
const outcome = yield* runSearch("rg", buildRgArgs(params), ctx);
|
|
296
|
+
if (outcome.noMatches) {
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
299
|
+
details: {
|
|
300
|
+
binarySource: outcome.binarySource,
|
|
301
|
+
outputLines: 0,
|
|
302
|
+
truncated: false,
|
|
303
|
+
},
|
|
304
|
+
} satisfies AgentToolResult<RgToolDetails>;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const formatted = formatCapturedOutput(outcome.output);
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: formatted.text }],
|
|
310
|
+
details: {
|
|
311
|
+
binarySource: outcome.binarySource,
|
|
312
|
+
outputLines: formatted.lineCount,
|
|
313
|
+
truncated: formatted.truncated,
|
|
314
|
+
fullOutputPath: formatted.fullOutputPath,
|
|
315
|
+
},
|
|
316
|
+
} satisfies AgentToolResult<RgToolDetails>;
|
|
317
|
+
}),
|
|
318
|
+
signal ? { signal } : undefined,
|
|
319
|
+
);
|
|
320
|
+
return unwrapToolExit(exit, "rg");
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
renderCall(args, theme) {
|
|
324
|
+
let text = theme.fg("toolTitle", theme.bold("rg "));
|
|
325
|
+
text += theme.fg("accent", `"${args.pattern}"`);
|
|
326
|
+
if (args.path) text += theme.fg("muted", ` in ${args.path}`);
|
|
327
|
+
const flags = [
|
|
328
|
+
args.glob && `glob=${args.glob}`,
|
|
329
|
+
args.file_type && `type=${args.file_type}`,
|
|
330
|
+
args.fixed_strings && "literal",
|
|
331
|
+
args.hidden && "hidden",
|
|
332
|
+
args.context !== undefined && `ctx=${args.context}`,
|
|
333
|
+
].filter((flag): flag is string => typeof flag === "string");
|
|
334
|
+
if (flags.length > 0) text += " " + theme.fg("dim", flags.join(" "));
|
|
335
|
+
return new Text(text, 1, 0);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
339
|
+
if (isPartial) return new Text(theme.fg("warning", "Searching..."), 1, 0);
|
|
340
|
+
const details = result.details;
|
|
341
|
+
if (!details || !details.outputLines) {
|
|
342
|
+
return new Text(theme.fg("dim", "No matches found"), 1, 0);
|
|
343
|
+
}
|
|
344
|
+
let text = theme.fg(
|
|
345
|
+
"success",
|
|
346
|
+
`${details.outputLines} output ${details.outputLines === 1 ? "line" : "lines"}`,
|
|
347
|
+
);
|
|
348
|
+
if (details.truncated) text += theme.fg("warning", " (truncated)");
|
|
349
|
+
if (expanded)
|
|
350
|
+
text += expandedPreview(result, details.fullOutputPath, theme);
|
|
351
|
+
return new Text(text, 1, 0);
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const PREVIEW_LINES = 20;
|
|
357
|
+
|
|
358
|
+
interface ThemeLike {
|
|
359
|
+
fg(color: string, text: string): string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function expandedPreview(
|
|
363
|
+
result: { content: { type: string; text?: string }[] },
|
|
364
|
+
fullOutputPath: string | undefined,
|
|
365
|
+
theme: ThemeLike,
|
|
366
|
+
) {
|
|
367
|
+
let text = "";
|
|
368
|
+
const content = result.content[0];
|
|
369
|
+
if (content?.type === "text" && content.text) {
|
|
370
|
+
const lines = content.text.split("\n");
|
|
371
|
+
for (const line of lines.slice(0, PREVIEW_LINES)) {
|
|
372
|
+
text += `\n${theme.fg("dim", line)}`;
|
|
373
|
+
}
|
|
374
|
+
if (lines.length > PREVIEW_LINES) {
|
|
375
|
+
text += `\n${theme.fg("muted", `... ${lines.length - PREVIEW_LINES} more lines`)}`;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (fullOutputPath) {
|
|
379
|
+
text += `\n${theme.fg("dim", `Full output: ${fullOutputPath}`)}`;
|
|
380
|
+
}
|
|
381
|
+
return text;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function fdParameters() {
|
|
385
|
+
return Type.Object({
|
|
386
|
+
pattern: Type.Optional(
|
|
387
|
+
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.pattern }),
|
|
388
|
+
),
|
|
389
|
+
path: Type.Optional(
|
|
390
|
+
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.path }),
|
|
391
|
+
),
|
|
392
|
+
type: Type.Optional(
|
|
393
|
+
StringEnum(["file", "directory", "symlink"] as const, {
|
|
394
|
+
description: FD_PARAMETER_DESCRIPTIONS.type,
|
|
395
|
+
}),
|
|
396
|
+
),
|
|
397
|
+
extension: Type.Optional(
|
|
398
|
+
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.extension }),
|
|
399
|
+
),
|
|
400
|
+
glob: Type.Optional(
|
|
401
|
+
Type.Boolean({ description: FD_PARAMETER_DESCRIPTIONS.glob }),
|
|
402
|
+
),
|
|
403
|
+
hidden: Type.Optional(
|
|
404
|
+
Type.Boolean({ description: FD_PARAMETER_DESCRIPTIONS.hidden }),
|
|
405
|
+
),
|
|
406
|
+
max_depth: Type.Optional(
|
|
407
|
+
Type.Integer({
|
|
408
|
+
description: FD_PARAMETER_DESCRIPTIONS.max_depth,
|
|
409
|
+
minimum: 1,
|
|
410
|
+
maximum: FD_MAX_DEPTH_LIMIT,
|
|
411
|
+
}),
|
|
412
|
+
),
|
|
413
|
+
limit: Type.Optional(
|
|
414
|
+
Type.Integer({
|
|
415
|
+
description: FD_PARAMETER_DESCRIPTIONS.limit,
|
|
416
|
+
minimum: 1,
|
|
417
|
+
maximum: FD_MAX_LIMIT,
|
|
418
|
+
}),
|
|
419
|
+
),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function rgParameters() {
|
|
424
|
+
return Type.Object({
|
|
425
|
+
pattern: Type.String({ description: RG_PARAMETER_DESCRIPTIONS.pattern }),
|
|
426
|
+
path: Type.Optional(
|
|
427
|
+
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.path }),
|
|
428
|
+
),
|
|
429
|
+
glob: Type.Optional(
|
|
430
|
+
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.glob }),
|
|
431
|
+
),
|
|
432
|
+
file_type: Type.Optional(
|
|
433
|
+
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.file_type }),
|
|
434
|
+
),
|
|
435
|
+
case_sensitive: Type.Optional(
|
|
436
|
+
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.case_sensitive }),
|
|
437
|
+
),
|
|
438
|
+
fixed_strings: Type.Optional(
|
|
439
|
+
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.fixed_strings }),
|
|
440
|
+
),
|
|
441
|
+
hidden: Type.Optional(
|
|
442
|
+
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.hidden }),
|
|
443
|
+
),
|
|
444
|
+
context: Type.Optional(
|
|
445
|
+
Type.Integer({
|
|
446
|
+
description: RG_PARAMETER_DESCRIPTIONS.context,
|
|
447
|
+
minimum: 0,
|
|
448
|
+
maximum: RG_MAX_CONTEXT,
|
|
449
|
+
}),
|
|
450
|
+
),
|
|
451
|
+
limit: Type.Optional(
|
|
452
|
+
Type.Integer({
|
|
453
|
+
description: RG_PARAMETER_DESCRIPTIONS.limit,
|
|
454
|
+
minimum: 1,
|
|
455
|
+
maximum: RG_MAX_COUNT_LIMIT,
|
|
456
|
+
}),
|
|
457
|
+
),
|
|
458
|
+
});
|
|
459
|
+
}
|