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,550 @@
|
|
|
1
|
+
import type { Stats } from "node:fs";
|
|
2
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { DiffLines, type ToolDiff } from "../../shared/DiffLines";
|
|
5
|
+
import { Fs } from "../../shared/Fs";
|
|
6
|
+
import { FsErrors } from "../../shared/FsErrors";
|
|
7
|
+
import { Lines } from "../../shared/Lines";
|
|
8
|
+
import { Paths } from "../../shared/Paths";
|
|
9
|
+
import { seekSequenceMatches } from "./matcher";
|
|
10
|
+
import type { Hunk, Patch, UpdateChunk } from "./types";
|
|
11
|
+
|
|
12
|
+
const CONTEXT_LINES = 3;
|
|
13
|
+
const MAX_UPDATE_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const EMPTY_PATCH_MESSAGE =
|
|
15
|
+
"No files were modified. Provide at least one *** Add File, *** Delete File, or *** Update File hunk.";
|
|
16
|
+
const NOOP_PATCH_MESSAGE =
|
|
17
|
+
"No files were modified. The patch matched the current file contents but did not change them.";
|
|
18
|
+
|
|
19
|
+
export type ApplyAction = {
|
|
20
|
+
readonly kind: "add" | "update" | "delete" | "move";
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly movePath?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ApplyEntry = {
|
|
26
|
+
readonly action: ApplyAction;
|
|
27
|
+
readonly diff: ToolDiff | undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ApplyOutcome = {
|
|
31
|
+
readonly entries: readonly ApplyEntry[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type FileWrite = {
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly displayPath: string;
|
|
37
|
+
readonly cwd: string;
|
|
38
|
+
readonly content: string;
|
|
39
|
+
readonly mode?: number;
|
|
40
|
+
readonly nlink: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type PlannedAction = {
|
|
44
|
+
readonly action: ApplyAction;
|
|
45
|
+
readonly diff: ToolDiff | undefined;
|
|
46
|
+
readonly write?: FileWrite;
|
|
47
|
+
readonly deletePath?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function isNoOpUpdate(entry: PlannedAction): boolean {
|
|
51
|
+
return entry.action.kind === "update" && entry.diff === undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function applyPatch(
|
|
55
|
+
patch: Patch,
|
|
56
|
+
cwd: string
|
|
57
|
+
): Promise<ApplyOutcome> {
|
|
58
|
+
if (patch.hunks.length === 0) {
|
|
59
|
+
throw new Error(EMPTY_PATCH_MESSAGE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const planned: PlannedAction[] = [];
|
|
63
|
+
for (const hunk of patch.hunks) {
|
|
64
|
+
planned.push(await planHunk(hunk, cwd));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (planned.every(isNoOpUpdate)) {
|
|
68
|
+
throw new Error(NOOP_PATCH_MESSAGE);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const writes = planned.flatMap((entry) =>
|
|
72
|
+
entry.write === undefined ? [] : [entry.write]
|
|
73
|
+
);
|
|
74
|
+
const deletes = planned.flatMap((entry) =>
|
|
75
|
+
entry.deletePath === undefined ? [] : [entry.deletePath]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
for (const write of writes) {
|
|
79
|
+
await writeFile(write);
|
|
80
|
+
}
|
|
81
|
+
for (const path of deletes) {
|
|
82
|
+
await Bun.file(path).delete();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
entries: planned.map((entry) => ({
|
|
87
|
+
action: entry.action,
|
|
88
|
+
diff: entry.diff,
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function planHunk(hunk: Hunk, cwd: string): Promise<PlannedAction> {
|
|
94
|
+
if (hunk.kind === "add") {
|
|
95
|
+
return planAdd(hunk.path, hunk.contents, cwd);
|
|
96
|
+
}
|
|
97
|
+
if (hunk.kind === "delete") {
|
|
98
|
+
return planDelete(hunk.path, cwd);
|
|
99
|
+
}
|
|
100
|
+
return planUpdate(hunk, cwd);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function planAdd(
|
|
104
|
+
rawPath: string,
|
|
105
|
+
contents: string,
|
|
106
|
+
cwd: string
|
|
107
|
+
): Promise<PlannedAction> {
|
|
108
|
+
const absolutePath = Paths.resolve(rawPath, cwd);
|
|
109
|
+
|
|
110
|
+
if (await Bun.file(absolutePath).exists()) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Cannot add ${rawPath}: file already exists. Use *** Update File to modify an existing file.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await ensureWritableParentPath(absolutePath, rawPath, cwd);
|
|
117
|
+
|
|
118
|
+
const newSide = DiffLines.fromText(contents);
|
|
119
|
+
const diff = DiffLines.buildToolDiff(
|
|
120
|
+
rawPath,
|
|
121
|
+
DiffLines.emptySide,
|
|
122
|
+
newSide,
|
|
123
|
+
CONTEXT_LINES
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
action: { kind: "add", path: rawPath },
|
|
128
|
+
diff,
|
|
129
|
+
write: {
|
|
130
|
+
path: absolutePath,
|
|
131
|
+
displayPath: rawPath,
|
|
132
|
+
cwd,
|
|
133
|
+
content: contents,
|
|
134
|
+
nlink: 1,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function planDelete(
|
|
140
|
+
rawPath: string,
|
|
141
|
+
cwd: string
|
|
142
|
+
): Promise<PlannedAction> {
|
|
143
|
+
const absolutePath = Paths.resolve(rawPath, cwd);
|
|
144
|
+
await statPatchTarget(absolutePath, rawPath, "delete");
|
|
145
|
+
|
|
146
|
+
const original = await readTextFile(absolutePath, rawPath, "delete");
|
|
147
|
+
const diff = DiffLines.buildToolDiff(
|
|
148
|
+
rawPath,
|
|
149
|
+
DiffLines.fromText(original.content),
|
|
150
|
+
DiffLines.emptySide,
|
|
151
|
+
CONTEXT_LINES
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
action: { kind: "delete", path: rawPath },
|
|
156
|
+
diff,
|
|
157
|
+
deletePath: absolutePath,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function planUpdate(
|
|
162
|
+
hunk: Extract<Hunk, { kind: "update" }>,
|
|
163
|
+
cwd: string
|
|
164
|
+
): Promise<PlannedAction> {
|
|
165
|
+
const absoluteSource = Paths.resolve(hunk.path, cwd);
|
|
166
|
+
const metadata = await statPatchTarget(absoluteSource, hunk.path, "update");
|
|
167
|
+
const canonicalSource = await realpathPatchTarget(
|
|
168
|
+
absoluteSource,
|
|
169
|
+
hunk.path,
|
|
170
|
+
"update"
|
|
171
|
+
);
|
|
172
|
+
if (metadata.size > MAX_UPDATE_BYTES) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Cannot update ${hunk.path}: file is too large (${metadata.size} bytes, max ${MAX_UPDATE_BYTES}). Use bash or another purpose-built tool for large-file edits.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const original = await readTextFile(canonicalSource, hunk.path, "update");
|
|
179
|
+
const split = Lines.splitWithTrailingNewline(original.content);
|
|
180
|
+
const newLines = applyChunks(split.lines, hunk.chunks, hunk.path);
|
|
181
|
+
|
|
182
|
+
const destPath = hunk.movePath ?? hunk.path;
|
|
183
|
+
const absoluteDest = Paths.resolve(destPath, cwd);
|
|
184
|
+
const isMove = hunk.movePath !== undefined && absoluteDest !== absoluteSource;
|
|
185
|
+
|
|
186
|
+
if (isMove && (await Bun.file(absoluteDest).exists())) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Cannot move ${hunk.path} to ${destPath}: destination already exists. Delete or rename the destination first, or choose a different path.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const writePath = isMove ? absoluteDest : canonicalSource;
|
|
193
|
+
|
|
194
|
+
await ensureWritableParentPath(writePath, destPath, cwd);
|
|
195
|
+
|
|
196
|
+
const oldSide = {
|
|
197
|
+
lines: split.lines,
|
|
198
|
+
hasTrailingNewline: split.hasTrailingNewline,
|
|
199
|
+
};
|
|
200
|
+
const newSide = {
|
|
201
|
+
lines: newLines,
|
|
202
|
+
hasTrailingNewline: split.hasTrailingNewline,
|
|
203
|
+
};
|
|
204
|
+
const diff = DiffLines.buildToolDiff(
|
|
205
|
+
destPath,
|
|
206
|
+
oldSide,
|
|
207
|
+
newSide,
|
|
208
|
+
CONTEXT_LINES
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const content = joinLines(
|
|
212
|
+
newLines,
|
|
213
|
+
original.lineEnding,
|
|
214
|
+
split.hasTrailingNewline
|
|
215
|
+
);
|
|
216
|
+
const restored = original.hadBom ? `${Lines.utf8Bom}${content}` : content;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
action: isMove
|
|
220
|
+
? { kind: "move", path: hunk.path, movePath: destPath }
|
|
221
|
+
: { kind: "update", path: hunk.path },
|
|
222
|
+
diff,
|
|
223
|
+
write: {
|
|
224
|
+
path: writePath,
|
|
225
|
+
displayPath: destPath,
|
|
226
|
+
cwd,
|
|
227
|
+
content: restored,
|
|
228
|
+
mode: Number(metadata.mode),
|
|
229
|
+
nlink: metadata.nlink,
|
|
230
|
+
},
|
|
231
|
+
...(isMove ? { deletePath: absoluteSource } : {}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function statPatchTarget(
|
|
236
|
+
absolutePath: string,
|
|
237
|
+
displayPath: string,
|
|
238
|
+
operation: "delete" | "update"
|
|
239
|
+
): Promise<Stats> {
|
|
240
|
+
let metadata: Stats;
|
|
241
|
+
try {
|
|
242
|
+
metadata = await stat(absolutePath);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw new Error(formatStatFailure(displayPath, operation, error));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (metadata.isDirectory()) {
|
|
248
|
+
const guidance =
|
|
249
|
+
operation === "delete"
|
|
250
|
+
? "Delete file hunks can only remove files."
|
|
251
|
+
: "Target a UTF-8 text file instead.";
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Cannot ${operation} ${displayPath}: path is a directory. ${guidance}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return metadata;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function realpathPatchTarget(
|
|
261
|
+
absolutePath: string,
|
|
262
|
+
displayPath: string,
|
|
263
|
+
operation: "delete" | "update"
|
|
264
|
+
): Promise<string> {
|
|
265
|
+
try {
|
|
266
|
+
return await realpath(absolutePath);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new Error(formatStatFailure(displayPath, operation, error));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatStatFailure(
|
|
273
|
+
displayPath: string,
|
|
274
|
+
operation: "delete" | "update",
|
|
275
|
+
error: unknown
|
|
276
|
+
): string {
|
|
277
|
+
const code = FsErrors.code(error);
|
|
278
|
+
if (operation === "delete") {
|
|
279
|
+
if (code === "ENOENT") {
|
|
280
|
+
return `Failed to delete file ${displayPath}: file does not exist. Use glob to locate the file, or omit this delete hunk.`;
|
|
281
|
+
}
|
|
282
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
283
|
+
return `Failed to delete file ${displayPath}: permission denied.`;
|
|
284
|
+
}
|
|
285
|
+
return `Failed to delete file ${displayPath}: ${errorDetail(error)}.`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (code === "ENOENT") {
|
|
289
|
+
return `Failed to read file to update ${displayPath}: file does not exist. Use *** Add File to create a new file, or use glob to locate the existing file.`;
|
|
290
|
+
}
|
|
291
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
292
|
+
return `Failed to read file to update ${displayPath}: permission denied.`;
|
|
293
|
+
}
|
|
294
|
+
return `Failed to read file to update ${displayPath}: ${errorDetail(error)}.`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function errorDetail(error: unknown): string {
|
|
298
|
+
return (
|
|
299
|
+
FsErrors.code(error) ??
|
|
300
|
+
(error instanceof Error ? error.message : "unknown error")
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function ensureWritableParentPath(
|
|
305
|
+
absolutePath: string,
|
|
306
|
+
displayPath: string,
|
|
307
|
+
cwd: string
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
const nonDirectoryParent = await findNonDirectoryParent(absolutePath);
|
|
310
|
+
if (nonDirectoryParent === undefined) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Cannot create parent directory ${Paths.displayRelative(
|
|
316
|
+
nonDirectoryParent,
|
|
317
|
+
cwd
|
|
318
|
+
)} for ${displayPath}: a file already exists at that path.`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function findNonDirectoryParent(
|
|
323
|
+
absolutePath: string
|
|
324
|
+
): Promise<string | undefined> {
|
|
325
|
+
let current = dirname(absolutePath);
|
|
326
|
+
|
|
327
|
+
while (true) {
|
|
328
|
+
try {
|
|
329
|
+
const metadata = await stat(current);
|
|
330
|
+
return metadata.isDirectory() ? undefined : current;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const code = FsErrors.code(error);
|
|
333
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const next = dirname(current);
|
|
339
|
+
if (next === current) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
current = next;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function applyChunks(
|
|
347
|
+
originalLines: readonly string[],
|
|
348
|
+
chunks: readonly UpdateChunk[],
|
|
349
|
+
path: string
|
|
350
|
+
): string[] {
|
|
351
|
+
const replacements: Array<{
|
|
352
|
+
readonly start: number;
|
|
353
|
+
readonly oldLen: number;
|
|
354
|
+
readonly newLines: readonly string[];
|
|
355
|
+
}> = [];
|
|
356
|
+
let lineIndex = 0;
|
|
357
|
+
|
|
358
|
+
for (const chunk of chunks) {
|
|
359
|
+
if (chunk.changeContext !== undefined) {
|
|
360
|
+
const idx = findUniqueSequence(
|
|
361
|
+
originalLines,
|
|
362
|
+
[chunk.changeContext],
|
|
363
|
+
lineIndex,
|
|
364
|
+
false,
|
|
365
|
+
path
|
|
366
|
+
);
|
|
367
|
+
if (idx.length === 0) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Failed to find context '${chunk.changeContext}' in ${path}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
lineIndex = idx[0]! + 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (chunk.oldLines.length === 0) {
|
|
376
|
+
const insertionIdx = originalLines.length;
|
|
377
|
+
replacements.push({
|
|
378
|
+
start: insertionIdx,
|
|
379
|
+
oldLen: 0,
|
|
380
|
+
newLines: chunk.newLines,
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let pattern = chunk.oldLines;
|
|
386
|
+
let newSlice = chunk.newLines;
|
|
387
|
+
let found = findUniqueSequence(
|
|
388
|
+
originalLines,
|
|
389
|
+
pattern,
|
|
390
|
+
lineIndex,
|
|
391
|
+
chunk.isEndOfFile,
|
|
392
|
+
path
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (found.length === 0 && pattern.at(-1) === "") {
|
|
396
|
+
pattern = pattern.slice(0, -1);
|
|
397
|
+
if (newSlice.at(-1) === "") {
|
|
398
|
+
newSlice = newSlice.slice(0, -1);
|
|
399
|
+
}
|
|
400
|
+
found = findUniqueSequence(
|
|
401
|
+
originalLines,
|
|
402
|
+
pattern,
|
|
403
|
+
lineIndex,
|
|
404
|
+
chunk.isEndOfFile,
|
|
405
|
+
path
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (found.length === 0) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`Failed to find expected lines in ${path}:\n${chunk.oldLines.join("\n")}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
replacements.push({
|
|
416
|
+
start: found[0]!,
|
|
417
|
+
oldLen: pattern.length,
|
|
418
|
+
newLines: newSlice,
|
|
419
|
+
});
|
|
420
|
+
lineIndex = found[0]! + pattern.length;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
replacements.sort((a, b) => a.start - b.start);
|
|
424
|
+
|
|
425
|
+
const lines = [...originalLines];
|
|
426
|
+
for (const { start, oldLen, newLines } of replacements.reverse()) {
|
|
427
|
+
lines.splice(start, oldLen, ...newLines);
|
|
428
|
+
}
|
|
429
|
+
return lines;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function findUniqueSequence(
|
|
433
|
+
lines: readonly string[],
|
|
434
|
+
pattern: readonly string[],
|
|
435
|
+
start: number,
|
|
436
|
+
eof: boolean,
|
|
437
|
+
path: string
|
|
438
|
+
): readonly number[] {
|
|
439
|
+
const matches = seekSequenceMatches(lines, pattern, start, eof);
|
|
440
|
+
if (matches.length <= 1) {
|
|
441
|
+
return matches;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
throw new Error(renderAmbiguousMatch(path, matches));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderAmbiguousMatch(
|
|
448
|
+
path: string,
|
|
449
|
+
matches: readonly number[]
|
|
450
|
+
): string {
|
|
451
|
+
const lineStarts = matches.map((match) => match + 1).join(", ");
|
|
452
|
+
return `Patch matched multiple regions in ${path} (lines ${lineStarts}). Use enough context to make it unique.`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function joinLines(
|
|
456
|
+
lines: readonly string[],
|
|
457
|
+
lineEnding: "\n" | "\r\n",
|
|
458
|
+
hasTrailingNewline: boolean
|
|
459
|
+
): string {
|
|
460
|
+
if (lines.length === 0) {
|
|
461
|
+
return "";
|
|
462
|
+
}
|
|
463
|
+
const body = lines.join(lineEnding);
|
|
464
|
+
return hasTrailingNewline ? `${body}${lineEnding}` : body;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
type ReadFile = {
|
|
468
|
+
readonly content: string;
|
|
469
|
+
readonly hadBom: boolean;
|
|
470
|
+
readonly lineEnding: "\n" | "\r\n";
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
async function readTextFile(
|
|
474
|
+
absolutePath: string,
|
|
475
|
+
displayPath: string,
|
|
476
|
+
operation: "delete" | "update"
|
|
477
|
+
): Promise<ReadFile> {
|
|
478
|
+
const bytes = await Bun.file(absolutePath).bytes();
|
|
479
|
+
|
|
480
|
+
if (bytes.subarray(0, 8192).includes(0)) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
`Cannot ${operation} ${displayPath}: binary file. apply_patch only supports UTF-8 text files.`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const hadBom = Lines.hasUtf8Bom(bytes);
|
|
487
|
+
const decoded = Lines.stripUtf8Bom(new TextDecoder("utf-8").decode(bytes));
|
|
488
|
+
const content = decoded.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
489
|
+
const lineEnding = decoded.includes("\r\n") ? "\r\n" : "\n";
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
content,
|
|
493
|
+
hadBom,
|
|
494
|
+
lineEnding,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function writeFile(write: FileWrite): Promise<void> {
|
|
499
|
+
try {
|
|
500
|
+
await Fs.writeKeepingLinks(write.path, write.content, {
|
|
501
|
+
...(write.mode === undefined ? {} : { mode: write.mode }),
|
|
502
|
+
nlink: write.nlink,
|
|
503
|
+
});
|
|
504
|
+
} catch (error) {
|
|
505
|
+
throw new Error(formatWriteFailure(write, error));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function formatWriteFailure(write: FileWrite, error: unknown): string {
|
|
510
|
+
const code = FsErrors.code(error);
|
|
511
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
512
|
+
return `Cannot write ${write.displayPath}: permission denied.`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const failedPath = errorPath(error);
|
|
516
|
+
if (code === "EEXIST") {
|
|
517
|
+
const parentPath = failedPath
|
|
518
|
+
? Paths.displayRelative(failedPath, write.cwd)
|
|
519
|
+
: "the parent directory";
|
|
520
|
+
return `Cannot create parent directory ${parentPath} for ${write.displayPath}: a file already exists at that path.`;
|
|
521
|
+
}
|
|
522
|
+
if (code === "ENOTDIR") {
|
|
523
|
+
return `Cannot write ${write.displayPath}: a parent path is not a directory.`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return `Failed to write ${write.displayPath}: ${errorDetail(error)}.`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function errorPath(error: unknown): string | undefined {
|
|
530
|
+
return typeof error === "object" && error !== null && "path" in error
|
|
531
|
+
? String((error as { readonly path: unknown }).path)
|
|
532
|
+
: undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function formatApplySummary(outcome: ApplyOutcome): string {
|
|
536
|
+
const parts = outcome.entries.map(({ action }) => {
|
|
537
|
+
switch (action.kind) {
|
|
538
|
+
case "add":
|
|
539
|
+
return `added ${action.path}`;
|
|
540
|
+
case "delete":
|
|
541
|
+
return `deleted ${action.path}`;
|
|
542
|
+
case "move":
|
|
543
|
+
return `moved ${action.path} to ${action.movePath}`;
|
|
544
|
+
default:
|
|
545
|
+
return `updated ${action.path}`;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
const noun = parts.length === 1 ? "change" : "changes";
|
|
549
|
+
return `Applied ${parts.length} ${noun}: ${parts.join(", ")}.`;
|
|
550
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Tools } from "../../shared/Tools";
|
|
3
|
+
import { computeActiveTools } from "./coordinator";
|
|
4
|
+
import { applyPatch, formatApplySummary } from "./executor";
|
|
5
|
+
import { prefersApplyPatch } from "./model";
|
|
6
|
+
import { parsePatch } from "./parser";
|
|
7
|
+
import { applyPatchView } from "./render";
|
|
8
|
+
import {
|
|
9
|
+
type ApplyPatchInput,
|
|
10
|
+
applyPatchSchema,
|
|
11
|
+
prepareApplyPatchArguments,
|
|
12
|
+
} from "./schema";
|
|
13
|
+
|
|
14
|
+
export default function (pi: ExtensionAPI): void {
|
|
15
|
+
Tools.register(pi, {
|
|
16
|
+
name: "apply_patch",
|
|
17
|
+
label: "Edit",
|
|
18
|
+
description:
|
|
19
|
+
"Apply a V4A patch to create, edit, delete, or move UTF-8 text files. " +
|
|
20
|
+
"Edits must be unique; include enough surrounding context for uniqueness. " +
|
|
21
|
+
"Prefer apply_patch over write for changes to existing files.",
|
|
22
|
+
parameters: applyPatchSchema,
|
|
23
|
+
prepareArguments: prepareApplyPatchArguments,
|
|
24
|
+
renderShell: "self",
|
|
25
|
+
effect: {
|
|
26
|
+
kind: "writesPaths",
|
|
27
|
+
paths: ({ input }) =>
|
|
28
|
+
parsePatch(input).hunks.flatMap((hunk) =>
|
|
29
|
+
hunk.kind === "update" && hunk.movePath
|
|
30
|
+
? [hunk.path, hunk.movePath]
|
|
31
|
+
: [hunk.path]
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
executionMode: "sequential",
|
|
35
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
36
|
+
const { input } = params as ApplyPatchInput;
|
|
37
|
+
|
|
38
|
+
if (signal?.aborted) {
|
|
39
|
+
throw new Error("apply_patch aborted before execution.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const patch = parsePatch(input);
|
|
43
|
+
const outcome = await applyPatch(patch, ctx.cwd);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: formatApplySummary(outcome) }],
|
|
47
|
+
details: { entries: outcome.entries },
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
toViewModel: applyPatchView,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const reconcile = (preferPatch: boolean): void => {
|
|
54
|
+
const active = pi.getActiveTools();
|
|
55
|
+
const available = pi.getAllTools().map((tool) => tool.name);
|
|
56
|
+
const next = computeActiveTools(available, active, preferPatch);
|
|
57
|
+
if (next !== active) {
|
|
58
|
+
pi.setActiveTools([...next]);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
pi.on("session_start", (_event, ctx) => {
|
|
63
|
+
reconcile(prefersApplyPatch(ctx.model));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
pi.on("model_select", (event) => {
|
|
67
|
+
reconcile(prefersApplyPatch(event.model));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const MATCHERS: ReadonlyArray<(a: string, b: string) => boolean> = [
|
|
2
|
+
(a, b) => a === b,
|
|
3
|
+
(a, b) => a.trimEnd() === b.trimEnd(),
|
|
4
|
+
(a, b) => a.trim() === b.trim(),
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
/** Locate `pattern` in `lines` at or after `start`, relaxing whitespace over three passes; `eof` searches flush against the end. */
|
|
8
|
+
export function seekSequenceMatches(
|
|
9
|
+
lines: readonly string[],
|
|
10
|
+
pattern: readonly string[],
|
|
11
|
+
start: number,
|
|
12
|
+
eof: boolean
|
|
13
|
+
): readonly number[] {
|
|
14
|
+
if (pattern.length === 0) {
|
|
15
|
+
return [start];
|
|
16
|
+
}
|
|
17
|
+
if (pattern.length > lines.length) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const searchStart =
|
|
22
|
+
eof && lines.length >= pattern.length
|
|
23
|
+
? lines.length - pattern.length
|
|
24
|
+
: start;
|
|
25
|
+
const last = lines.length - pattern.length;
|
|
26
|
+
|
|
27
|
+
for (const eq of MATCHERS) {
|
|
28
|
+
const matches: number[] = [];
|
|
29
|
+
for (let i = searchStart; i <= last; i += 1) {
|
|
30
|
+
let ok = true;
|
|
31
|
+
for (let p = 0; p < pattern.length; p += 1) {
|
|
32
|
+
if (!eq(lines[i + p]!, pattern[p]!)) {
|
|
33
|
+
ok = false;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (ok) {
|
|
38
|
+
matches.push(i);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (matches.length > 0) {
|
|
42
|
+
return matches;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type ModelLike = {
|
|
2
|
+
readonly provider?: string;
|
|
3
|
+
readonly api?: string;
|
|
4
|
+
readonly id?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const CLAUDE_APPLY_PATCH_MAJOR = 5;
|
|
8
|
+
|
|
9
|
+
// Never match a GPT-named non-OpenAI model (`eleutherai/gpt-neo`): a false positive hands it a tool it was not trained on.
|
|
10
|
+
function isGptModel(model: ModelLike | undefined): boolean {
|
|
11
|
+
const provider = (model?.provider ?? "").toLowerCase();
|
|
12
|
+
const api = (model?.api ?? "").toLowerCase();
|
|
13
|
+
const id = (model?.id ?? "").toLowerCase();
|
|
14
|
+
|
|
15
|
+
const isOpenAi = provider.includes("openai") || id.startsWith("openai/");
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
provider.includes("codex") ||
|
|
19
|
+
api.includes("codex") ||
|
|
20
|
+
id.includes("codex") ||
|
|
21
|
+
(isOpenAi && id.includes("gpt")) ||
|
|
22
|
+
(isOpenAi && /(^|\/)o\d/.test(id)) ||
|
|
23
|
+
((provider.includes("copilot") || api.includes("copilot")) &&
|
|
24
|
+
id.includes("gpt"))
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function claudeMajorVersion(id: string): number | undefined {
|
|
29
|
+
const match = /(?:^|[/.])claude-[a-z]+-(\d+)/.exec(id);
|
|
30
|
+
return match ? Number(match[1]) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function prefersApplyPatch(model: ModelLike | undefined): boolean {
|
|
34
|
+
if (isGptModel(model)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const major = claudeMajorVersion((model?.id ?? "").toLowerCase());
|
|
38
|
+
return major !== undefined && major >= CLAUDE_APPLY_PATCH_MAJOR;
|
|
39
|
+
}
|