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,74 @@
|
|
|
1
|
+
function formatTokens(tokens: number): string {
|
|
2
|
+
if (tokens < 1000) {
|
|
3
|
+
return `${tokens}`;
|
|
4
|
+
}
|
|
5
|
+
if (tokens < 10_000) {
|
|
6
|
+
return `${(tokens / 1000).toFixed(1)}K`;
|
|
7
|
+
}
|
|
8
|
+
if (tokens < 1_000_000) {
|
|
9
|
+
return `${Math.round(tokens / 1000)}K`;
|
|
10
|
+
}
|
|
11
|
+
if (tokens < 10_000_000) {
|
|
12
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
13
|
+
}
|
|
14
|
+
return `${Math.round(tokens / 1_000_000)}M`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatElapsed(ms: number): string {
|
|
18
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
19
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
20
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
21
|
+
const seconds = totalSeconds % 60;
|
|
22
|
+
return hours > 0
|
|
23
|
+
? `${hours}h ${minutes}m ${seconds}s`
|
|
24
|
+
: minutes > 0
|
|
25
|
+
? `${minutes}m ${seconds}s`
|
|
26
|
+
: `${seconds}s`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** How full the context window reads, as a verdict each frontend colours. */
|
|
30
|
+
export type ContextFill = "ok" | "warn" | "full";
|
|
31
|
+
|
|
32
|
+
function contextFill(percent: number): ContextFill {
|
|
33
|
+
if (percent >= 70) {
|
|
34
|
+
return "full";
|
|
35
|
+
}
|
|
36
|
+
return percent > 40 ? "warn" : "ok";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function count(value: number, noun: string): string {
|
|
40
|
+
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function scaleBytes(total: number): readonly [string, string] {
|
|
44
|
+
const [value, unit] =
|
|
45
|
+
total < 1024 * 1024 ? [total / 1024, "KB"] : [total / (1024 * 1024), "MB"];
|
|
46
|
+
return [value.toFixed(2).replace(/\.?0+$/u, ""), unit];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A byte count as a person reads it, unit spaced: `40 bytes`, `240 KB`, `1.5 MB`. */
|
|
50
|
+
function bytes(total: number): string {
|
|
51
|
+
if (total < 1024) {
|
|
52
|
+
return `${total} bytes`;
|
|
53
|
+
}
|
|
54
|
+
const [value, unit] = scaleBytes(total);
|
|
55
|
+
return `${value} ${unit}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The same count where a title has no room for the space: `40B`, `240KB`, `1.5MB`. */
|
|
59
|
+
function bytesCompact(total: number): string {
|
|
60
|
+
if (total < 1024) {
|
|
61
|
+
return `${total}B`;
|
|
62
|
+
}
|
|
63
|
+
const [value, unit] = scaleBytes(total);
|
|
64
|
+
return `${value}${unit}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Format = {
|
|
68
|
+
formatTokens,
|
|
69
|
+
formatElapsed,
|
|
70
|
+
contextFill,
|
|
71
|
+
count,
|
|
72
|
+
bytes,
|
|
73
|
+
bytesCompact,
|
|
74
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { chmod, mkdir, rename, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { FsErrors } from "./FsErrors";
|
|
5
|
+
|
|
6
|
+
async function readJsonOrEmpty<T>(filePath: string, fallback: T): Promise<T> {
|
|
7
|
+
try {
|
|
8
|
+
return (await Bun.file(filePath).json()) as T;
|
|
9
|
+
} catch (err) {
|
|
10
|
+
if (FsErrors.code(err) === "ENOENT") {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Failed to parse ${filePath}: ${(err as Error).message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readJsonOr<T>(source: string | URL, fallback: T): Promise<T> {
|
|
18
|
+
try {
|
|
19
|
+
return (await Bun.file(source).json()) as T;
|
|
20
|
+
} catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function writeAtomic(
|
|
26
|
+
filePath: string,
|
|
27
|
+
data: string,
|
|
28
|
+
mode?: number
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
31
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
32
|
+
await Bun.write(tmp, data);
|
|
33
|
+
const resolvedMode = mode ?? (await existingMode(filePath));
|
|
34
|
+
if (resolvedMode !== undefined) {
|
|
35
|
+
await chmod(tmp, resolvedMode);
|
|
36
|
+
}
|
|
37
|
+
await rename(tmp, filePath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function writeJson(
|
|
41
|
+
filePath: string,
|
|
42
|
+
value: unknown,
|
|
43
|
+
mode = 0o600
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
await writeAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`, mode);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function writeKeepingLinks(
|
|
49
|
+
filePath: string,
|
|
50
|
+
content: string,
|
|
51
|
+
options: { readonly mode?: number; readonly nlink: number }
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
if (options.nlink > 1) {
|
|
54
|
+
await Bun.write(filePath, content);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await writeAtomic(filePath, content, options.mode);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function serialised(): {
|
|
61
|
+
run: <T>(task: () => Promise<T>) => Promise<T>;
|
|
62
|
+
} {
|
|
63
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
64
|
+
return {
|
|
65
|
+
run: <T>(task: () => Promise<T>): Promise<T> => {
|
|
66
|
+
const next = queue.then(task, task);
|
|
67
|
+
queue = next;
|
|
68
|
+
return next;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function existingMode(filePath: string): Promise<number | undefined> {
|
|
74
|
+
try {
|
|
75
|
+
return (await stat(filePath)).mode & 0o777;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (FsErrors.code(err) === "ENOENT") {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const Fs = {
|
|
85
|
+
readJsonOrEmpty,
|
|
86
|
+
readJsonOr,
|
|
87
|
+
writeAtomic,
|
|
88
|
+
writeJson,
|
|
89
|
+
writeKeepingLinks,
|
|
90
|
+
serialised,
|
|
91
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Stats } from "node:fs";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
function code(error: unknown): string | undefined {
|
|
6
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
7
|
+
? String((error as { code: unknown }).code)
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function statOrThrow(path: string): Promise<Stats> {
|
|
12
|
+
try {
|
|
13
|
+
return await stat(path);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
const errorCode = code(error);
|
|
16
|
+
|
|
17
|
+
if (errorCode === "ENOENT") {
|
|
18
|
+
throw new Error(await renderMissing(path));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (errorCode === "EACCES" || errorCode === "EPERM") {
|
|
22
|
+
throw new Error(`Permission denied accessing ${path}.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Cannot stat ${path}: ${errorCode ?? (error instanceof Error ? error.message : "unknown error")}.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function renderMissing(path: string): Promise<string> {
|
|
32
|
+
const suggestions = await suggestSiblings(path);
|
|
33
|
+
const headline = `Path not found: ${path}. Use glob to locate the file or directory, or verify the path.`;
|
|
34
|
+
if (suggestions.length === 0) {
|
|
35
|
+
return headline;
|
|
36
|
+
}
|
|
37
|
+
return [headline, "", "Did you mean one of these?", ...suggestions].join(
|
|
38
|
+
"\n"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function suggestSiblings(path: string): Promise<readonly string[]> {
|
|
43
|
+
const dir = dirname(path);
|
|
44
|
+
const base = basename(path).toLowerCase();
|
|
45
|
+
const stem = base.slice(0, base.length - extname(base).length);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const entries = await readdir(dir);
|
|
49
|
+
return entries
|
|
50
|
+
.filter((entry) => {
|
|
51
|
+
const lower = entry.toLowerCase();
|
|
52
|
+
const lowerStem = lower.slice(0, lower.length - extname(lower).length);
|
|
53
|
+
return (
|
|
54
|
+
lower.includes(base) ||
|
|
55
|
+
base.includes(lower) ||
|
|
56
|
+
(stem.length > 0 &&
|
|
57
|
+
(lowerStem.includes(stem) || stem.includes(lowerStem)))
|
|
58
|
+
);
|
|
59
|
+
})
|
|
60
|
+
.slice(0, 3)
|
|
61
|
+
.map((entry) => join(dir, entry));
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const FsErrors = { code, statOrThrow, renderMissing };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { byLengthAsc, byStartAsc, Fzf } from "fzf";
|
|
2
|
+
|
|
3
|
+
export type FuzzyCandidate<T> = {
|
|
4
|
+
readonly item: T;
|
|
5
|
+
readonly haystacks: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type FuzzyHit<T> = {
|
|
9
|
+
readonly item: T;
|
|
10
|
+
readonly score: number;
|
|
11
|
+
readonly positions: ReadonlySet<number>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type FuzzyRankOptions = {
|
|
15
|
+
readonly limit?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FuzzyIndex<T> = {
|
|
19
|
+
readonly find: (
|
|
20
|
+
query: string,
|
|
21
|
+
options?: FuzzyRankOptions
|
|
22
|
+
) => readonly FuzzyHit<T>[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const HAYSTACK_SEPARATOR = " ";
|
|
26
|
+
|
|
27
|
+
function rank<T>(
|
|
28
|
+
query: string,
|
|
29
|
+
candidates: readonly FuzzyCandidate<T>[],
|
|
30
|
+
options: FuzzyRankOptions = {}
|
|
31
|
+
): readonly FuzzyHit<T>[] {
|
|
32
|
+
return prepare(candidates).find(query, options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function prepare<T>(candidates: readonly FuzzyCandidate<T>[]): FuzzyIndex<T> {
|
|
36
|
+
const fzf = new Fzf<readonly FuzzyCandidate<T>[]>(candidates, {
|
|
37
|
+
selector: (candidate) => candidate.haystacks.join(HAYSTACK_SEPARATOR),
|
|
38
|
+
tiebreakers: [byStartAsc, byLengthAsc],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let emptyHits: readonly FuzzyHit<T>[] | undefined;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
find: (query, options = {}) => {
|
|
45
|
+
const trimmed = query.trim();
|
|
46
|
+
const limit = options.limit ?? Infinity;
|
|
47
|
+
if (trimmed.length === 0) {
|
|
48
|
+
if (emptyHits === undefined) {
|
|
49
|
+
emptyHits = [...candidates]
|
|
50
|
+
.sort((a, b) =>
|
|
51
|
+
(a.haystacks[0] ?? "").localeCompare(b.haystacks[0] ?? "")
|
|
52
|
+
)
|
|
53
|
+
.map((candidate) => ({
|
|
54
|
+
item: candidate.item,
|
|
55
|
+
score: 0,
|
|
56
|
+
positions: new Set<number>(),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
return limit === Infinity ? emptyHits : emptyHits.slice(0, limit);
|
|
60
|
+
}
|
|
61
|
+
const hits = fzf.find(trimmed).map((result) => ({
|
|
62
|
+
item: result.item.item,
|
|
63
|
+
score: result.score,
|
|
64
|
+
positions: result.positions,
|
|
65
|
+
}));
|
|
66
|
+
return limit === Infinity ? hits : hits.slice(0, limit);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const FuzzyMatcher = { rank, prepare };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type FSWatcher, watch } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type GitState = {
|
|
5
|
+
readonly branch: string | null;
|
|
6
|
+
/** Paths `git status` lists as changed; zero is a clean tree. */
|
|
7
|
+
readonly dirtyCount: number;
|
|
8
|
+
readonly ahead: number;
|
|
9
|
+
readonly behind: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const EMPTY: GitState = {
|
|
13
|
+
branch: null,
|
|
14
|
+
dirtyCount: 0,
|
|
15
|
+
ahead: 0,
|
|
16
|
+
behind: 0,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function parseStatus(text: string): GitState {
|
|
20
|
+
let branch: string | null = null;
|
|
21
|
+
let ahead = 0;
|
|
22
|
+
let behind = 0;
|
|
23
|
+
let dirtyCount = 0;
|
|
24
|
+
for (const line of text.split("\n")) {
|
|
25
|
+
if (line.startsWith("# branch.head ")) {
|
|
26
|
+
const head = line.slice("# branch.head ".length);
|
|
27
|
+
branch = head === "(detached)" ? "detached" : head;
|
|
28
|
+
} else if (line.startsWith("# branch.ab ")) {
|
|
29
|
+
const m = /\+(\d+)\s+-(\d+)/.exec(line);
|
|
30
|
+
if (m) {
|
|
31
|
+
ahead = Number(m[1]);
|
|
32
|
+
behind = Number(m[2]);
|
|
33
|
+
}
|
|
34
|
+
} else if (line.length > 0 && !line.startsWith("#")) {
|
|
35
|
+
dirtyCount++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { branch, dirtyCount, ahead, behind };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function watchDir(cwd: string, onChange: () => void): () => void {
|
|
42
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
43
|
+
const fire = (): void => {
|
|
44
|
+
if (timer !== null) {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
timer = setTimeout(() => {
|
|
48
|
+
timer = null;
|
|
49
|
+
onChange();
|
|
50
|
+
}, 200);
|
|
51
|
+
};
|
|
52
|
+
let watcher: FSWatcher | null = null;
|
|
53
|
+
try {
|
|
54
|
+
watcher = watch(join(cwd, ".git"), { persistent: false }, fire);
|
|
55
|
+
watcher.on("error", () => {});
|
|
56
|
+
} catch {}
|
|
57
|
+
return (): void => {
|
|
58
|
+
if (timer !== null) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
timer = null;
|
|
61
|
+
}
|
|
62
|
+
if (watcher !== null) {
|
|
63
|
+
try {
|
|
64
|
+
watcher.close();
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fetchStatus(cwd: string): Promise<GitState> {
|
|
71
|
+
try {
|
|
72
|
+
const proc = Bun.spawn(["git", "status", "--porcelain=v2", "--branch"], {
|
|
73
|
+
cwd,
|
|
74
|
+
stdout: "pipe",
|
|
75
|
+
stderr: "ignore",
|
|
76
|
+
});
|
|
77
|
+
const text = await new Response(proc.stdout).text();
|
|
78
|
+
await proc.exited;
|
|
79
|
+
if (proc.exitCode !== 0) {
|
|
80
|
+
return EMPTY;
|
|
81
|
+
}
|
|
82
|
+
return parseStatus(text);
|
|
83
|
+
} catch {
|
|
84
|
+
return EMPTY;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The cwd's git state, for the TUI footer and the web's branch chip. */
|
|
89
|
+
export const Git = { EMPTY, parseStatus, watchDir, fetchStatus };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function compile(exclude: readonly string[] | undefined): readonly Bun.Glob[] {
|
|
2
|
+
return (exclude ?? []).map((pattern) => new Bun.Glob(pattern));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function ignores(globs: readonly Bun.Glob[], relativePath: string): boolean {
|
|
6
|
+
if (globs.length === 0) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return globs.some((glob) => glob.match(relativePath));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const GlobExclusions = { compile, ignores };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ky, { type KyInstance } from "ky";
|
|
2
|
+
|
|
3
|
+
export type HttpFetch = (
|
|
4
|
+
input: Parameters<typeof fetch>[0],
|
|
5
|
+
init?: Parameters<typeof fetch>[1]
|
|
6
|
+
) => ReturnType<typeof fetch>;
|
|
7
|
+
|
|
8
|
+
/** Ky with retries off: its defaults retry GETs twice and honour `Retry-After` uncapped, stalling a tool for hours. */
|
|
9
|
+
export function createKy(custom?: HttpFetch): KyInstance {
|
|
10
|
+
return ky.create({
|
|
11
|
+
retry: 0,
|
|
12
|
+
...(custom === undefined ? {} : { fetch: custom as typeof fetch }),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The formats every provider we target accepts. */
|
|
2
|
+
export type ImageMimeType =
|
|
3
|
+
| "image/png"
|
|
4
|
+
| "image/jpeg"
|
|
5
|
+
| "image/gif"
|
|
6
|
+
| "image/webp";
|
|
7
|
+
|
|
8
|
+
const EXTENSIONS = {
|
|
9
|
+
"image/png": "png",
|
|
10
|
+
"image/jpeg": "jpg",
|
|
11
|
+
"image/gif": "gif",
|
|
12
|
+
"image/webp": "webp",
|
|
13
|
+
} satisfies Record<ImageMimeType, string>;
|
|
14
|
+
|
|
15
|
+
/** Both spellings of every format, so `.jpeg` names a picture as surely as `.jpg`. */
|
|
16
|
+
const NAME_EXTENSIONS = new Set([
|
|
17
|
+
...Object.values(EXTENSIONS),
|
|
18
|
+
...Object.keys(EXTENSIONS).map((mimeType) => mimeType.slice("image/".length)),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/** The format as a word, in the one spelling the notes, the UI and the cache filename all use. */
|
|
22
|
+
function extensionOf(mimeType: ImageMimeType): string {
|
|
23
|
+
return EXTENSIONS[mimeType];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isSupported(mimeType: string): mimeType is ImageMimeType {
|
|
27
|
+
return Object.hasOwn(EXTENSIONS, mimeType);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether a bare, lowercased extension is one of the spellings a picture goes by. */
|
|
31
|
+
function namesExtension(extension: string): boolean {
|
|
32
|
+
return NAME_EXTENSIONS.has(extension);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The format table alone, free of Node so the browser can name a picture too. */
|
|
36
|
+
export const ImageMime = { extensionOf, isSupported, namesExtension };
|