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,374 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { PickerItem } from "#core/picker/PickerItem";
|
|
4
|
+
import { RemoteFilePickerSuggestionEngine } from "#core/picker/RemoteFilePickerSuggestionEngine";
|
|
5
|
+
import type {
|
|
6
|
+
AttachmentRef,
|
|
7
|
+
CommandDraft,
|
|
8
|
+
SearchScope,
|
|
9
|
+
SessionScope,
|
|
10
|
+
} from "#protocol/Command";
|
|
11
|
+
import {
|
|
12
|
+
isDurableEvent,
|
|
13
|
+
type ModelView,
|
|
14
|
+
type ResponseEvent,
|
|
15
|
+
type ServerEvent,
|
|
16
|
+
type SessionListing,
|
|
17
|
+
type SessionSearch,
|
|
18
|
+
} from "#protocol/ServerEvent";
|
|
19
|
+
|
|
20
|
+
export type ProbeOptions = {
|
|
21
|
+
readonly url: string;
|
|
22
|
+
/** Omit to have the server create a session; the id comes back on attach. */
|
|
23
|
+
readonly sessionId?: string;
|
|
24
|
+
readonly cwd?: string;
|
|
25
|
+
readonly fromSeq?: number;
|
|
26
|
+
/** Sent on the attach frame; false is a client that is connected but not looking. */
|
|
27
|
+
readonly attentive?: boolean;
|
|
28
|
+
/** Called for every frame, in arrival order. */
|
|
29
|
+
readonly onEvent?: (event: ServerEvent) => void;
|
|
30
|
+
/** Keystroke debounce for `files`; 0 makes tests deterministic. */
|
|
31
|
+
readonly debounceMs?: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** What `POST /upload` answers with, all of it server-side. */
|
|
35
|
+
export type UploadedFile = {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly path: string;
|
|
38
|
+
/** Where the bytes can be read back, relative to the server's origin. */
|
|
39
|
+
readonly url: string;
|
|
40
|
+
readonly mimeType: string;
|
|
41
|
+
readonly isImage: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type Pending = {
|
|
45
|
+
readonly resolve: (response: ResponseEvent) => void;
|
|
46
|
+
readonly reject: (err: Error) => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The reference client: speaks the whole protocol, renders nothing. */
|
|
50
|
+
export class ProbeClient {
|
|
51
|
+
public sessionId: string | undefined;
|
|
52
|
+
public seq: number;
|
|
53
|
+
public readonly events: ServerEvent[] = [];
|
|
54
|
+
/** The `@` picker, answered by the server one query at a time. */
|
|
55
|
+
public readonly files: RemoteFilePickerSuggestionEngine;
|
|
56
|
+
private readonly options: ProbeOptions;
|
|
57
|
+
private readonly pending = new Map<string, Pending>();
|
|
58
|
+
private readonly waiters = new Set<{
|
|
59
|
+
readonly test: (event: ServerEvent) => boolean;
|
|
60
|
+
readonly resolve: (event: ServerEvent) => void;
|
|
61
|
+
}>();
|
|
62
|
+
private socket: WebSocket | undefined;
|
|
63
|
+
private nextId = 0;
|
|
64
|
+
|
|
65
|
+
public constructor(options: ProbeOptions) {
|
|
66
|
+
this.options = options;
|
|
67
|
+
this.sessionId = options.sessionId;
|
|
68
|
+
this.seq = options.fromSeq ?? 0;
|
|
69
|
+
this.files = new RemoteFilePickerSuggestionEngine(
|
|
70
|
+
(query, limit) => this.pickFiles(query, limit),
|
|
71
|
+
options.debounceMs
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Connects and completes the resume handshake; resolves once replay ends. */
|
|
76
|
+
public async connect(): Promise<ResponseEvent> {
|
|
77
|
+
const socket = new WebSocket(this.options.url);
|
|
78
|
+
this.socket = socket;
|
|
79
|
+
socket.addEventListener("message", (event) => {
|
|
80
|
+
this.receive(String(event.data));
|
|
81
|
+
});
|
|
82
|
+
socket.addEventListener("close", (event) => {
|
|
83
|
+
for (const { reject } of this.pending.values()) {
|
|
84
|
+
reject(new Error(`socket closed: ${event.code} ${event.reason}`));
|
|
85
|
+
}
|
|
86
|
+
this.pending.clear();
|
|
87
|
+
});
|
|
88
|
+
await new Promise<void>((resolve, reject) => {
|
|
89
|
+
socket.addEventListener("open", () => {
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
socket.addEventListener("error", () => {
|
|
93
|
+
reject(new Error(`could not connect to ${this.options.url}`));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
return await this.send({
|
|
97
|
+
type: "attach",
|
|
98
|
+
...(this.sessionId === undefined ? {} : { sessionId: this.sessionId }),
|
|
99
|
+
...(this.options.cwd === undefined ? {} : { cwd: this.options.cwd }),
|
|
100
|
+
...(this.options.attentive === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { attentive: this.options.attentive }),
|
|
103
|
+
fromSeq: this.seq,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Says whether this client's reader is present; regaining it reads the session. */
|
|
108
|
+
public attention(value: boolean): Promise<ResponseEvent> {
|
|
109
|
+
return this.send({ type: "attention", value });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public send(command: CommandDraft): Promise<ResponseEvent> {
|
|
113
|
+
const socket = this.socket;
|
|
114
|
+
if (!socket) {
|
|
115
|
+
return Promise.reject(new Error("probe is not connected"));
|
|
116
|
+
}
|
|
117
|
+
const id = `probe-${++this.nextId}`;
|
|
118
|
+
const frame = { ...command, id };
|
|
119
|
+
return new Promise<ResponseEvent>((resolve, reject) => {
|
|
120
|
+
this.pending.set(id, { resolve, reject });
|
|
121
|
+
socket.send(JSON.stringify(frame));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public prompt(text: string): Promise<ResponseEvent> {
|
|
126
|
+
return this.send({
|
|
127
|
+
type: "user_message",
|
|
128
|
+
sessionId: this.sessionId ?? "",
|
|
129
|
+
text,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public async pickFiles(
|
|
134
|
+
query: string,
|
|
135
|
+
limit = 50
|
|
136
|
+
): Promise<readonly PickerItem[]> {
|
|
137
|
+
const response = await this.send({
|
|
138
|
+
type: "pick_files",
|
|
139
|
+
sessionId: this.sessionId ?? "",
|
|
140
|
+
query,
|
|
141
|
+
limit,
|
|
142
|
+
});
|
|
143
|
+
return itemsOf(response);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public async pickCommands(
|
|
147
|
+
query: string,
|
|
148
|
+
limit?: number
|
|
149
|
+
): Promise<readonly PickerItem[]> {
|
|
150
|
+
const response = await this.send({
|
|
151
|
+
type: "pick_commands",
|
|
152
|
+
sessionId: this.sessionId ?? "",
|
|
153
|
+
query,
|
|
154
|
+
...(limit === undefined ? {} : { limit }),
|
|
155
|
+
});
|
|
156
|
+
return itemsOf(response);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Reads one subagent's transcript; its events stay enveloped in `events`. */
|
|
160
|
+
public watchSubagent(callId: string, fromSeq = 0): Promise<ResponseEvent> {
|
|
161
|
+
return this.send({
|
|
162
|
+
type: "watch_subagent",
|
|
163
|
+
sessionId: this.sessionId ?? "",
|
|
164
|
+
callId,
|
|
165
|
+
fromSeq,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public unwatchSubagent(callId: string): Promise<ResponseEvent> {
|
|
170
|
+
return this.send({ type: "unwatch_subagent", callId });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Pi's session catalogue; answers whether or not this probe is attached. */
|
|
174
|
+
public async listSessions(
|
|
175
|
+
scope: SessionScope = {}
|
|
176
|
+
): Promise<SessionListing["sessions"]> {
|
|
177
|
+
return (await this.catalogue(scope)).sessions;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The listing whole: the rows the page kept and the projects they came from. */
|
|
181
|
+
public async catalogue(scope: SessionScope = {}): Promise<SessionListing> {
|
|
182
|
+
const response = await this.send({ type: "list_sessions", ...scope });
|
|
183
|
+
if (!response.success) {
|
|
184
|
+
throw new Error(response.error ?? "list_sessions failed");
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
sessions: response.sessions ?? [],
|
|
188
|
+
projects: response.projects ?? [],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Searches every session on disk; an empty query warms the index and answers with no hits. */
|
|
193
|
+
public async search(
|
|
194
|
+
query: string,
|
|
195
|
+
scope: SearchScope = {}
|
|
196
|
+
): Promise<SessionSearch> {
|
|
197
|
+
const response = await this.send({
|
|
198
|
+
type: "search_sessions",
|
|
199
|
+
query,
|
|
200
|
+
...scope,
|
|
201
|
+
});
|
|
202
|
+
if (!response.success) {
|
|
203
|
+
throw new Error(response.error ?? "search_sessions failed");
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
hits: response.hits ?? [],
|
|
207
|
+
dropped: response.dropped ?? [],
|
|
208
|
+
scanned: response.scanned ?? 0,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Names a session through pi's own name; `null` clears it back to its opening message. */
|
|
213
|
+
public rename(
|
|
214
|
+
sessionId: string,
|
|
215
|
+
value: string | null
|
|
216
|
+
): Promise<ResponseEvent> {
|
|
217
|
+
return this.send({ type: "set_session_name", sessionId, value });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Puts a session out of the live listing, or brings it back. */
|
|
221
|
+
public setArchived(
|
|
222
|
+
sessionId: string,
|
|
223
|
+
value: boolean
|
|
224
|
+
): Promise<ResponseEvent> {
|
|
225
|
+
return this.send({ type: "set_session_archived", sessionId, value });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Holds a session unread until something reads it; survives a listing and a re-attach. */
|
|
229
|
+
public markUnread(sessionId: string, value: boolean): Promise<ResponseEvent> {
|
|
230
|
+
return this.send({ type: "set_session_unread", sessionId, value });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Pins a working directory, not a session. */
|
|
234
|
+
public setPinned(cwd: string, value: boolean): Promise<ResponseEvent> {
|
|
235
|
+
return this.send({ type: "set_project_pinned", cwd, value });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Re-orders the pinned directories; the whole order, pinned ones only. */
|
|
239
|
+
public setPinOrder(order: readonly string[]): Promise<ResponseEvent> {
|
|
240
|
+
return this.send({ type: "set_pin_order", order });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Unfolds a working directory's sidebar group, or folds it. */
|
|
244
|
+
public setExpanded(cwd: string, value: boolean): Promise<ResponseEvent> {
|
|
245
|
+
return this.send({ type: "set_project_expanded", cwd, value });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Names a working directory for the listings; `null` puts it back to its base name. */
|
|
249
|
+
public setLabel(cwd: string, value: string | null): Promise<ResponseEvent> {
|
|
250
|
+
return this.send({ type: "set_project_label", cwd, value });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The model catalogue, plus this session's thinking levels; empty when unattached. */
|
|
254
|
+
public async listModels(): Promise<{
|
|
255
|
+
readonly models: readonly ModelView[];
|
|
256
|
+
readonly thinkingLevels: readonly string[];
|
|
257
|
+
}> {
|
|
258
|
+
const response = await this.send({ type: "list_models" });
|
|
259
|
+
if (!response.success) {
|
|
260
|
+
throw new Error(response.error ?? "list_models failed");
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
models: response.models ?? [],
|
|
264
|
+
thinkingLevels: response.thinkingLevels ?? [],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Transfers a client-local file into the server's world; only the bytes and bare filename are sent. */
|
|
269
|
+
public async upload(localPath: string): Promise<UploadedFile> {
|
|
270
|
+
const file = Bun.file(localPath);
|
|
271
|
+
const form = new FormData();
|
|
272
|
+
form.append("file", file, basename(localPath));
|
|
273
|
+
const response = await fetch(
|
|
274
|
+
`${this.httpUrl}/upload?session=${encodeURIComponent(this.sessionId ?? "")}`,
|
|
275
|
+
{ method: "POST", body: form }
|
|
276
|
+
);
|
|
277
|
+
const body = (await response.json()) as UploadedFile & {
|
|
278
|
+
readonly error?: string;
|
|
279
|
+
};
|
|
280
|
+
if (!response.ok) {
|
|
281
|
+
throw new Error(`upload failed: ${body.error ?? response.status}`);
|
|
282
|
+
}
|
|
283
|
+
return body;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public promptWith(
|
|
287
|
+
text: string,
|
|
288
|
+
attachments: readonly AttachmentRef[]
|
|
289
|
+
): Promise<ResponseEvent> {
|
|
290
|
+
return this.send({
|
|
291
|
+
type: "user_message",
|
|
292
|
+
sessionId: this.sessionId ?? "",
|
|
293
|
+
text,
|
|
294
|
+
attachments,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public get httpUrl(): string {
|
|
299
|
+
return this.options.url.replace(/^ws/, "http");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** `from` skips frames already received, so a repeated state can be awaited. */
|
|
303
|
+
public waitFor(
|
|
304
|
+
test: (event: ServerEvent) => boolean,
|
|
305
|
+
opts?: { readonly timeoutMs?: number; readonly from?: number }
|
|
306
|
+
): Promise<ServerEvent> {
|
|
307
|
+
const timeoutMs = opts?.timeoutMs ?? 30_000;
|
|
308
|
+
const hit = this.events.slice(opts?.from ?? 0).find(test);
|
|
309
|
+
if (hit) {
|
|
310
|
+
return Promise.resolve(hit);
|
|
311
|
+
}
|
|
312
|
+
return new Promise<ServerEvent>((resolve, reject) => {
|
|
313
|
+
const waiter = { test, resolve };
|
|
314
|
+
this.waiters.add(waiter);
|
|
315
|
+
const timer = setTimeout(() => {
|
|
316
|
+
this.waiters.delete(waiter);
|
|
317
|
+
reject(
|
|
318
|
+
new Error(`timed out waiting for an event after ${timeoutMs}ms`)
|
|
319
|
+
);
|
|
320
|
+
}, timeoutMs);
|
|
321
|
+
timer.unref?.();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Drops the socket without a close frame, the way a killed client would. */
|
|
326
|
+
public kill(): void {
|
|
327
|
+
this.socket?.close(4000, "probe killed");
|
|
328
|
+
this.socket = undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public close(): void {
|
|
332
|
+
this.socket?.close();
|
|
333
|
+
this.socket = undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private receive(raw: string): void {
|
|
337
|
+
const frame = JSON.parse(raw) as ServerEvent;
|
|
338
|
+
if (frame.type === "replay") {
|
|
339
|
+
for (const event of frame.events) {
|
|
340
|
+
this.dispatch(event);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.dispatch(frame);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private dispatch(event: ServerEvent): void {
|
|
348
|
+
this.events.push(event);
|
|
349
|
+
if (isDurableEvent(event)) {
|
|
350
|
+
this.seq = Math.max(this.seq, event.seq);
|
|
351
|
+
} else if (event.type === "attached") {
|
|
352
|
+
this.sessionId = event.sessionId;
|
|
353
|
+
} else if (event.type === "picker_invalidate") {
|
|
354
|
+
void this.files.refreshRelative();
|
|
355
|
+
} else if (event.type === "response") {
|
|
356
|
+
this.pending.get(event.id)?.resolve(event);
|
|
357
|
+
this.pending.delete(event.id);
|
|
358
|
+
}
|
|
359
|
+
this.options.onEvent?.(event);
|
|
360
|
+
for (const waiter of this.waiters) {
|
|
361
|
+
if (waiter.test(event)) {
|
|
362
|
+
this.waiters.delete(waiter);
|
|
363
|
+
waiter.resolve(event);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function itemsOf(response: ResponseEvent): readonly PickerItem[] {
|
|
370
|
+
if (!response.success) {
|
|
371
|
+
throw new Error(response.error ?? "picker query failed");
|
|
372
|
+
}
|
|
373
|
+
return response.items ?? [];
|
|
374
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { DaemonUnit } from "#core/shared/DaemonUnit";
|
|
2
|
+
import { Supervisor } from "#core/shared/Supervisor";
|
|
3
|
+
import { Updater, type UpdateOutcome } from "#core/shared/Updater";
|
|
4
|
+
import type { ServerEvent } from "#protocol/ServerEvent";
|
|
5
|
+
|
|
6
|
+
export type ReloaderDeps = {
|
|
7
|
+
/** To every connection: the restart takes all of them down together. */
|
|
8
|
+
readonly announce: (event: ServerEvent) => void;
|
|
9
|
+
/** Runs the update, reporting each step as it starts. */
|
|
10
|
+
readonly update?: (onStep: (label: string) => void) => Promise<UpdateOutcome>;
|
|
11
|
+
/** Takes this process down so the supervisor puts the new code up in its place. */
|
|
12
|
+
readonly shutdown?: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Re-raise SIGTERM rather than exiting: the handler stops the gateway and flushes the read cursors.
|
|
16
|
+
async function restartAndExit(): Promise<void> {
|
|
17
|
+
await Supervisor.restartSiblings(DaemonUnit);
|
|
18
|
+
process.kill(process.pid, "SIGTERM");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Runs the update-restart-return an operator asked for, announcing each phase. */
|
|
22
|
+
export class Reloader {
|
|
23
|
+
private readonly announce: (event: ServerEvent) => void;
|
|
24
|
+
private readonly update: (
|
|
25
|
+
onStep: (label: string) => void
|
|
26
|
+
) => Promise<UpdateOutcome>;
|
|
27
|
+
private readonly shutdown: () => Promise<void>;
|
|
28
|
+
private running: Promise<void> | undefined;
|
|
29
|
+
|
|
30
|
+
public constructor(deps: ReloaderDeps) {
|
|
31
|
+
this.announce = deps.announce;
|
|
32
|
+
this.update = deps.update ?? ((onStep) => Updater.run({ onStep }));
|
|
33
|
+
this.shutdown = deps.shutdown ?? restartAndExit;
|
|
34
|
+
this.running = undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The run already in flight, or a new one; two callers share one install. */
|
|
38
|
+
public start(): Promise<void> {
|
|
39
|
+
this.running ??= this.run().finally(() => {
|
|
40
|
+
this.running = undefined;
|
|
41
|
+
});
|
|
42
|
+
return this.running;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async run(): Promise<void> {
|
|
46
|
+
let outcome: UpdateOutcome;
|
|
47
|
+
try {
|
|
48
|
+
outcome = await this.update((label) => {
|
|
49
|
+
this.announce({ type: "update_state", phase: "step", label });
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
this.failed(messageOf(err));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!outcome.ok) {
|
|
56
|
+
this.failed(outcome.error ?? "the update failed");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const { from, to, skipped } = outcome;
|
|
60
|
+
// Unsupervised, exiting would end this process rather than replace it.
|
|
61
|
+
if (!Supervisor.isSupervised()) {
|
|
62
|
+
this.announce({
|
|
63
|
+
type: "update_state",
|
|
64
|
+
phase: "stranded",
|
|
65
|
+
from,
|
|
66
|
+
to,
|
|
67
|
+
skipped,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.announce({
|
|
72
|
+
type: "update_state",
|
|
73
|
+
phase: "restarting",
|
|
74
|
+
from,
|
|
75
|
+
to,
|
|
76
|
+
skipped,
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
await this.shutdown();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Must follow the `restarting` frame: it corrects a client already told to expect the drop.
|
|
82
|
+
this.failed(messageOf(err));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private failed(error: string): void {
|
|
87
|
+
this.announce({ type: "update_state", phase: "failed", error });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function messageOf(err: unknown): string {
|
|
92
|
+
return (err as Error).message ?? String(err);
|
|
93
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { type Static, Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
import type { AttachmentStore } from "#core/attachments/AttachmentStore";
|
|
6
|
+
import { Paths } from "#core/shared/Paths";
|
|
7
|
+
import { SendFile } from "#core/shared/SendFile";
|
|
8
|
+
import type { PimToolDefinition } from "#core/shared/Tools";
|
|
9
|
+
import type { ToolView } from "#core/view/ViewBlock";
|
|
10
|
+
import { attachmentUrl } from "./AttachmentEndpoint";
|
|
11
|
+
|
|
12
|
+
const sendFileSchema = Type.Object({
|
|
13
|
+
path: Type.String({
|
|
14
|
+
minLength: 1,
|
|
15
|
+
description: "Absolute or relative path to file (resolved against cwd).",
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type SendFileInput = Static<typeof sendFileSchema>;
|
|
20
|
+
|
|
21
|
+
type SendFileDetails = {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly isImage: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SendFileDeps = {
|
|
28
|
+
readonly store: AttachmentStore;
|
|
29
|
+
readonly cwd: string;
|
|
30
|
+
/** Pi's session uuid, read at call time. */
|
|
31
|
+
readonly sessionId: () => string | undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function build(
|
|
35
|
+
deps: SendFileDeps
|
|
36
|
+
): PimToolDefinition<typeof sendFileSchema, SendFileDetails> {
|
|
37
|
+
return {
|
|
38
|
+
...defineTool({
|
|
39
|
+
name: "send_file",
|
|
40
|
+
label: "send_file",
|
|
41
|
+
description: `Send a local file to the user's web browser. Images appear inline. Max ${SendFile.MAX_BYTES / (1024 * 1024)} MB.`,
|
|
42
|
+
parameters: sendFileSchema,
|
|
43
|
+
async execute(_id, params) {
|
|
44
|
+
const { path: rawPath } = params as SendFileInput;
|
|
45
|
+
const { path, size } = await SendFile.validate(rawPath, deps.cwd);
|
|
46
|
+
const scope = deps.sessionId();
|
|
47
|
+
if (!scope) {
|
|
48
|
+
throw new Error("This session cannot send files yet.");
|
|
49
|
+
}
|
|
50
|
+
const stored = await deps.store.storeFile(scope, path);
|
|
51
|
+
const details: SendFileDetails = {
|
|
52
|
+
name: basename(path),
|
|
53
|
+
url: attachmentUrl(stored.path),
|
|
54
|
+
isImage: stored.mimeType.startsWith("image/"),
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
// Never the URL: an address the model can repeat is a second, dead copy of this delivery.
|
|
58
|
+
content: [{ type: "text", text: `Sent ${details.name} (${size} B)` }],
|
|
59
|
+
details,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
effect: { kind: "readOnly" },
|
|
64
|
+
toViewModel: ({ args, result, cwd }): ToolView => {
|
|
65
|
+
const details = result?.details;
|
|
66
|
+
return {
|
|
67
|
+
label: "Send File",
|
|
68
|
+
icon: "upload",
|
|
69
|
+
title: [
|
|
70
|
+
{
|
|
71
|
+
kind: "file",
|
|
72
|
+
path: Paths.titleOr(
|
|
73
|
+
(args as Partial<SendFileInput> | undefined)?.path,
|
|
74
|
+
cwd
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
...(details === undefined
|
|
79
|
+
? {}
|
|
80
|
+
: {
|
|
81
|
+
summary: [{ kind: "attachment", ...details }],
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const SendFileTool = { build };
|