pim-agent 0.0.1 → 0.10.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 +54 -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 +177 -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 +36 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +232 -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 +132 -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 +77 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +156 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SearchIndex.ts +752 -0
- package/packages/core/src/session/SearchTokens.ts +97 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionDigest.ts +161 -0
- package/packages/core/src/session/SessionHost.ts +892 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionMeta.ts +267 -0
- package/packages/core/src/session/SessionName.ts +18 -0
- package/packages/core/src/session/SessionRegistry.ts +244 -0
- package/packages/core/src/session/WriteMark.ts +51 -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/DiffPatch.ts +70 -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 +121 -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 +530 -0
- package/packages/core/src/shared/GitMonitor.ts +221 -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 +83 -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 +50 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/RepoDiff.ts +741 -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/Surface.ts +2 -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 +209 -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/DiffExpand.ts +214 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/DiffPairs.ts +47 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/MovePath.ts +70 -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 +56 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +219 -0
- package/packages/protocol/src/Diff.ts +15 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/ServerEvent.ts +406 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +277 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +374 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +456 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +627 -0
- package/packages/server/src/StaticClient.ts +178 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +798 -0
- package/packages/server/src/probe.ts +200 -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 +253 -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 +75 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +93 -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 +253 -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-300-normal-B-iV2FbL.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +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-CA0aSzPu.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-B9be-i9d.css +1 -0
- package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -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,85 @@
|
|
|
1
|
+
import type { Api as ModelApi, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
getAgentDir,
|
|
4
|
+
ModelRegistry,
|
|
5
|
+
ModelRuntime,
|
|
6
|
+
SettingsManager,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
/** One model a session can be switched to, named the way `setModel` takes it. */
|
|
11
|
+
export type ModelChoice = {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
/** Who serves it, for a client that groups or tags the catalogue. */
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pi's installation as one process sees it: a single `ModelRuntime` over a
|
|
20
|
+
* single `auth.json`, and one `SettingsManager` per cwd. Every surface in a
|
|
21
|
+
* process shares one of these — two runtimes over one auth file race each
|
|
22
|
+
* other's token refresh.
|
|
23
|
+
*/
|
|
24
|
+
export class AgentRuntime {
|
|
25
|
+
public readonly agentDir: string;
|
|
26
|
+
private readonly settingsManagers = new Map<string, SettingsManager>();
|
|
27
|
+
private runtime: ModelRuntime | undefined;
|
|
28
|
+
private registry: ModelRegistry | undefined;
|
|
29
|
+
private booting: Promise<void> | undefined;
|
|
30
|
+
|
|
31
|
+
public constructor(agentDir?: string) {
|
|
32
|
+
this.agentDir = agentDir ?? getAgentDir();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Idempotent, and shared by every caller: the second surface to ask waits on the first. */
|
|
36
|
+
public init(): Promise<void> {
|
|
37
|
+
this.booting ??= this.bootstrap().catch((err: unknown) => {
|
|
38
|
+
this.booting = undefined;
|
|
39
|
+
throw err;
|
|
40
|
+
});
|
|
41
|
+
return this.booting;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public get modelRuntime(): ModelRuntime {
|
|
45
|
+
return this.required(this.runtime);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public get modelRegistry(): ModelRegistry {
|
|
49
|
+
return this.required(this.registry);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every model this machine has credentials for, qualified as `setModel` takes them. */
|
|
53
|
+
public models(): readonly ModelChoice[] {
|
|
54
|
+
return this.modelRegistry.getAvailable().map((model: Model<ModelApi>) => ({
|
|
55
|
+
id: `${model.provider}/${model.id}`,
|
|
56
|
+
label: model.name,
|
|
57
|
+
provider: model.provider,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public settingsManagerFor(cwd: string): SettingsManager {
|
|
62
|
+
const existing = this.settingsManagers.get(cwd);
|
|
63
|
+
if (existing) {
|
|
64
|
+
return existing;
|
|
65
|
+
}
|
|
66
|
+
const settingsManager = SettingsManager.create(cwd, this.agentDir);
|
|
67
|
+
this.settingsManagers.set(cwd, settingsManager);
|
|
68
|
+
return settingsManager;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async bootstrap(): Promise<void> {
|
|
72
|
+
this.runtime = await ModelRuntime.create({
|
|
73
|
+
authPath: join(this.agentDir, "auth.json"),
|
|
74
|
+
modelsPath: join(this.agentDir, "models.json"),
|
|
75
|
+
});
|
|
76
|
+
this.registry = new ModelRegistry(this.runtime);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private required<T>(value: T | undefined): T {
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
throw new Error("AgentRuntime.init() must complete before use");
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { BunFile } from "bun";
|
|
2
|
+
|
|
3
|
+
import type { FileEntry, SessionHeader } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { parseSessionEntries } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import { SessionDigest, type Durable } from "./SessionDigest";
|
|
7
|
+
|
|
8
|
+
export type { SessionDigest } from "./SessionDigest";
|
|
9
|
+
|
|
10
|
+
async function ifPresent<T>(read: () => Promise<T>, absent: T): Promise<T> {
|
|
11
|
+
try {
|
|
12
|
+
return await read();
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
15
|
+
return absent;
|
|
16
|
+
}
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One durable line of a session file; the header is line 1, so entries start at `seq` 2. */
|
|
22
|
+
export type LoggedEntry = {
|
|
23
|
+
readonly seq: number;
|
|
24
|
+
readonly entry: FileEntry;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type Cursor = {
|
|
28
|
+
readonly offset: number;
|
|
29
|
+
readonly seq: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const PROBE_LIMIT = 16 * 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
const PROBE_CHUNK = 8 * 1024;
|
|
35
|
+
const PROBE_CHUNK_MAX = 256 * 1024;
|
|
36
|
+
|
|
37
|
+
const EMPTY = new Uint8Array();
|
|
38
|
+
|
|
39
|
+
function isHeader(entry: FileEntry): entry is SessionHeader {
|
|
40
|
+
return entry.type === "session";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A reader over one pi session file: append-only JSONL, so a line's `seq` never changes. */
|
|
44
|
+
export class EventLog {
|
|
45
|
+
public readonly path: string;
|
|
46
|
+
private cursor: Cursor = { offset: 0, seq: 0 };
|
|
47
|
+
|
|
48
|
+
public constructor(path: string) {
|
|
49
|
+
this.path = path;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Durable entries with `seq > afterSeq`, oldest first. */
|
|
53
|
+
public async read(afterSeq = 0): Promise<readonly LoggedEntry[]> {
|
|
54
|
+
const start =
|
|
55
|
+
this.cursor.seq <= afterSeq ? this.cursor : { offset: 0, seq: 0 };
|
|
56
|
+
const file = Bun.file(this.path);
|
|
57
|
+
const text = await ifPresent(() => file.slice(start.offset).text(), "");
|
|
58
|
+
|
|
59
|
+
// A trailing fragment is a write in progress: stop short of it or reads lose entries.
|
|
60
|
+
const lastBreak = text.lastIndexOf("\n");
|
|
61
|
+
if (lastBreak === -1) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const complete = text.slice(0, lastBreak + 1);
|
|
65
|
+
const lines = complete.split("\n");
|
|
66
|
+
lines.pop();
|
|
67
|
+
|
|
68
|
+
const entries: LoggedEntry[] = [];
|
|
69
|
+
for (const [index, line] of lines.entries()) {
|
|
70
|
+
const seq = start.seq + index + 1;
|
|
71
|
+
if (seq <= afterSeq) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const parsed = parseSessionEntries(line)[0];
|
|
75
|
+
if (parsed) {
|
|
76
|
+
entries.push({ seq, entry: parsed });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.cursor = {
|
|
81
|
+
offset: start.offset + Buffer.byteLength(complete, "utf8"),
|
|
82
|
+
seq: start.seq + lines.length,
|
|
83
|
+
};
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public async header(): Promise<SessionHeader | undefined> {
|
|
88
|
+
for await (const line of headLines(Bun.file(this.path), 1)) {
|
|
89
|
+
const entry = parseSessionEntries(line)[0];
|
|
90
|
+
return entry && isHeader(entry) ? entry : undefined;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The `seq` of the last complete line, without parsing any of them. */
|
|
96
|
+
public async head(): Promise<number> {
|
|
97
|
+
await this.read(Number.MAX_SAFE_INTEGER);
|
|
98
|
+
return this.cursor.seq;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The name pi's own `/name` writes: the last `session_info` entry wins, and an
|
|
103
|
+
* empty one clears it. Appended rather than replaced, so it is found by reading
|
|
104
|
+
* the whole file — the one unbounded read the catalogue makes, and the only
|
|
105
|
+
* reason `SessionDigest` takes a whole file rather than a bounded head and tail.
|
|
106
|
+
*/
|
|
107
|
+
public async name(): Promise<string | undefined> {
|
|
108
|
+
const body = await durableBytes(Bun.file(this.path));
|
|
109
|
+
return body === undefined ? undefined : SessionDigest.nameOf(body);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Title and settle time for the catalogue, without opening a session. */
|
|
113
|
+
public async digest(): Promise<SessionDigest> {
|
|
114
|
+
const body = await durableBytes(Bun.file(this.path));
|
|
115
|
+
return body === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: SessionDigest.of(SessionDigest.partsOf(body));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function* headLines(
|
|
122
|
+
file: BunFile,
|
|
123
|
+
count: number
|
|
124
|
+
): AsyncGenerator<string> {
|
|
125
|
+
// Decode with `stream: true`: a chunk boundary can split a character.
|
|
126
|
+
const decoder = new TextDecoder();
|
|
127
|
+
let pending = "";
|
|
128
|
+
let yielded = 0;
|
|
129
|
+
let offset = 0;
|
|
130
|
+
let ask = PROBE_CHUNK;
|
|
131
|
+
while (offset < PROBE_LIMIT) {
|
|
132
|
+
const chunk = await ifPresent(
|
|
133
|
+
() => file.slice(offset, offset + ask).bytes(),
|
|
134
|
+
EMPTY
|
|
135
|
+
);
|
|
136
|
+
if (chunk.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
offset += chunk.length;
|
|
140
|
+
ask = Math.min(ask * 2, PROBE_CHUNK_MAX);
|
|
141
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
142
|
+
const lines = pending.split("\n");
|
|
143
|
+
pending = lines.pop() ?? "";
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
yield line;
|
|
146
|
+
if (++yielded >= count) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function durableBytes(file: BunFile): Promise<Durable | undefined> {
|
|
154
|
+
const read = await ifPresent(() => file.bytes(), EMPTY);
|
|
155
|
+
return SessionDigest.durable(read);
|
|
156
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AssistantMessage, UserMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
function textOf(
|
|
4
|
+
content: UserMessage["content"] | AssistantMessage["content"],
|
|
5
|
+
kind: "text" | "thinking" = "text"
|
|
6
|
+
): string {
|
|
7
|
+
if (typeof content === "string") {
|
|
8
|
+
return kind === "text" ? content : "";
|
|
9
|
+
}
|
|
10
|
+
let out = "";
|
|
11
|
+
for (const part of content) {
|
|
12
|
+
if (kind === "text" && part.type === "text") {
|
|
13
|
+
out += part.text;
|
|
14
|
+
} else if (kind === "thinking" && part.type === "thinking") {
|
|
15
|
+
out += part.thinking;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const MessageText = { textOf };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { Fs } from "../shared/Fs";
|
|
4
|
+
import { Paths } from "../shared/Paths";
|
|
5
|
+
|
|
6
|
+
type Stored = {
|
|
7
|
+
readonly baseline: number;
|
|
8
|
+
readonly sessions: Record<string, number>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** When each session was last read, shared by every client of a machine. */
|
|
12
|
+
export class ReadCursors {
|
|
13
|
+
private readonly file: string;
|
|
14
|
+
private readonly ready: Promise<void>;
|
|
15
|
+
private readonly cursors = new Map<string, number>();
|
|
16
|
+
private baseline = 0;
|
|
17
|
+
private readonly writes = Fs.serialised();
|
|
18
|
+
|
|
19
|
+
/** Defaults to `~/.pim/read.json`. */
|
|
20
|
+
public constructor(path?: string) {
|
|
21
|
+
this.file = path ?? join(Paths.pimHomeDir(), "read.json");
|
|
22
|
+
this.ready = this.load();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Whether a session has been answered since anything last read it. */
|
|
26
|
+
public async isUnread(
|
|
27
|
+
sessionId: string,
|
|
28
|
+
answeredAt: number | undefined
|
|
29
|
+
): Promise<boolean> {
|
|
30
|
+
await this.ready;
|
|
31
|
+
return (
|
|
32
|
+
answeredAt !== undefined &&
|
|
33
|
+
answeredAt > (this.cursors.get(sessionId) ?? this.baseline)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Moves a session's cursor forward; the write behind it is not waited on. */
|
|
38
|
+
public async mark(sessionId: string, at: number = Date.now()): Promise<void> {
|
|
39
|
+
await this.ready;
|
|
40
|
+
if ((this.cursors.get(sessionId) ?? this.baseline) >= at) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.cursors.set(sessionId, at);
|
|
44
|
+
this.persist();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async prune(alive: ReadonlySet<string>): Promise<void> {
|
|
48
|
+
await this.ready;
|
|
49
|
+
let dropped = false;
|
|
50
|
+
for (const sessionId of this.cursors.keys()) {
|
|
51
|
+
if (!alive.has(sessionId)) {
|
|
52
|
+
this.cursors.delete(sessionId);
|
|
53
|
+
dropped = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (dropped) {
|
|
57
|
+
this.persist();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Settles the writes behind the marks taken so far, for a clean stop. */
|
|
62
|
+
public async flush(): Promise<void> {
|
|
63
|
+
await this.ready;
|
|
64
|
+
await this.writes.run(async () => undefined);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async load(): Promise<void> {
|
|
68
|
+
const stored = (await Bun.file(this.file)
|
|
69
|
+
.json()
|
|
70
|
+
.catch(() => undefined)) as Partial<Stored> | undefined;
|
|
71
|
+
if (typeof stored?.baseline !== "number") {
|
|
72
|
+
// Persist the baseline now: recomputing it on each start clears every unread session.
|
|
73
|
+
this.baseline = Date.now();
|
|
74
|
+
this.persist();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.baseline = stored.baseline;
|
|
78
|
+
for (const [sessionId, at] of Object.entries(stored.sessions ?? {})) {
|
|
79
|
+
if (typeof at === "number") {
|
|
80
|
+
this.cursors.set(sessionId, at);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private persist(): void {
|
|
86
|
+
void this.writes.run(async () => {
|
|
87
|
+
const stored: Stored = {
|
|
88
|
+
baseline: this.baseline,
|
|
89
|
+
sessions: Object.fromEntries(this.cursors),
|
|
90
|
+
};
|
|
91
|
+
await Fs.writeJson(this.file, stored).catch(() => undefined);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|