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,228 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
type BuildOptions = {
|
|
4
|
+
readonly model?: ExtensionContext["model"];
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
readonly contextFiles: ReadonlyArray<{
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly content: string;
|
|
9
|
+
}>;
|
|
10
|
+
readonly skillsBlock: string;
|
|
11
|
+
readonly toolGuidelines: ReadonlyArray<string>;
|
|
12
|
+
readonly appendSystemPrompt?: string;
|
|
13
|
+
readonly customPrompt?: string;
|
|
14
|
+
readonly os?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type RunCommand = (cmd: ReadonlyArray<string>) => string | undefined;
|
|
18
|
+
|
|
19
|
+
type OsDescriptionOptions = {
|
|
20
|
+
readonly platform?: typeof process.platform;
|
|
21
|
+
readonly runCommand?: RunCommand;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function dynamicGuidelines(): ReadonlyArray<string> {
|
|
25
|
+
const guidelines: string[] = [];
|
|
26
|
+
if (Bun.which("gh")) {
|
|
27
|
+
guidelines.push(
|
|
28
|
+
"Always prefer `gh` CLI instead of raw API calls when viewing GitHub content (eg. PRs, issues, comments)."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return guidelines;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildSystemPrompt(opts: BuildOptions): string {
|
|
35
|
+
const sections: string[] = [];
|
|
36
|
+
|
|
37
|
+
if (opts.customPrompt && opts.customPrompt.trim().length > 0) {
|
|
38
|
+
sections.push(opts.customPrompt);
|
|
39
|
+
} else {
|
|
40
|
+
sections.push(
|
|
41
|
+
[
|
|
42
|
+
"<system_instructions>",
|
|
43
|
+
"You are pim (Pi IMproved), a Bun-native, opinionated extension pack for the [pi agent harness](https://pi.dev/).",
|
|
44
|
+
...opts.toolGuidelines.map((g) => `- ${g}`),
|
|
45
|
+
...dynamicGuidelines().map((g) => `- ${g}`),
|
|
46
|
+
"</system_instructions>",
|
|
47
|
+
].join("\n")
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const model = opts.model
|
|
52
|
+
? `${opts.model.id} via ${opts.model.provider}`
|
|
53
|
+
: "unknown";
|
|
54
|
+
sections.push(
|
|
55
|
+
[
|
|
56
|
+
"<environment>",
|
|
57
|
+
`- cwd: ${opts.cwd}`,
|
|
58
|
+
`- os: ${opts.os ?? describeOs()}`,
|
|
59
|
+
`- model: ${model}`,
|
|
60
|
+
`- datetime: ${formatDatetime(new Date())}`,
|
|
61
|
+
"</environment>",
|
|
62
|
+
].join("\n")
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (opts.contextFiles.length > 0) {
|
|
66
|
+
const files = opts.contextFiles
|
|
67
|
+
.map(
|
|
68
|
+
({ path, content }) =>
|
|
69
|
+
`<file path="${escapeXmlAttr(path)}">\n${content}\n</file>`
|
|
70
|
+
)
|
|
71
|
+
.join("\n");
|
|
72
|
+
sections.push(`<project_instructions>\n${files}\n</project_instructions>`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (opts.skillsBlock) {
|
|
76
|
+
sections.push(opts.skillsBlock.trimStart());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (opts.appendSystemPrompt && opts.appendSystemPrompt.trim().length > 0) {
|
|
80
|
+
sections.push(opts.appendSystemPrompt);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return sections.join("\n\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function describeOs(options: OsDescriptionOptions = {}): string {
|
|
87
|
+
if (options.platform === undefined && options.runCommand === undefined) {
|
|
88
|
+
cachedOs ??= computeOs(options);
|
|
89
|
+
return cachedOs;
|
|
90
|
+
}
|
|
91
|
+
return computeOs(options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let cachedOs: string | undefined;
|
|
95
|
+
|
|
96
|
+
function computeOs(options: OsDescriptionOptions): string {
|
|
97
|
+
const platform = options.platform ?? process.platform;
|
|
98
|
+
const runCommand =
|
|
99
|
+
options.runCommand ??
|
|
100
|
+
((cmd) => {
|
|
101
|
+
try {
|
|
102
|
+
const result = Bun.spawnSync({ cmd: [...cmd] });
|
|
103
|
+
if (result.exitCode !== 0) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const output = result.stdout.toString().trim();
|
|
108
|
+
return output || undefined;
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const unixName = (): string | undefined => runCommand(["uname", "-sr"]);
|
|
114
|
+
|
|
115
|
+
if (platform === "linux") {
|
|
116
|
+
const osRelease = runCommand(["cat", "/etc/os-release"]);
|
|
117
|
+
if (osRelease) {
|
|
118
|
+
const parsed = fields(osRelease, parseOsReleaseLine);
|
|
119
|
+
const prettyName = parsed.get("PRETTY_NAME")?.trim();
|
|
120
|
+
if (prettyName) {
|
|
121
|
+
return prettyName;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const name = parsed.get("NAME")?.trim();
|
|
125
|
+
const version =
|
|
126
|
+
parsed.get("VERSION")?.trim() ?? parsed.get("VERSION_ID")?.trim();
|
|
127
|
+
const described = [name, version].filter(Boolean).join(" ");
|
|
128
|
+
if (described) {
|
|
129
|
+
return described;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const lsbRelease = runCommand(["lsb_release", "-ds"]);
|
|
134
|
+
if (lsbRelease) {
|
|
135
|
+
return unquoteValue(lsbRelease.trim());
|
|
136
|
+
}
|
|
137
|
+
} else if (platform === "darwin") {
|
|
138
|
+
const swVers = runCommand(["sw_vers"]);
|
|
139
|
+
if (swVers) {
|
|
140
|
+
const parsed = fields(swVers, parseColonLine);
|
|
141
|
+
const name = parsed.get("ProductName") ?? "macOS";
|
|
142
|
+
const version = parsed.get("ProductVersion");
|
|
143
|
+
return [name, version].filter(Boolean).join(" ") || platform;
|
|
144
|
+
}
|
|
145
|
+
} else if (platform === "win32") {
|
|
146
|
+
const ver = runCommand(["cmd.exe", "/d", "/s", "/c", "ver"]);
|
|
147
|
+
return ver?.replace(/\s+/g, " ").trim() || platform;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return unixName() ?? platform;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type FieldParser = (line: string) => readonly [string, string] | undefined;
|
|
154
|
+
|
|
155
|
+
function fields(text: string, parse: FieldParser): Map<string, string> {
|
|
156
|
+
const parsed = new Map<string, string>();
|
|
157
|
+
for (const line of text.split(/\r?\n/)) {
|
|
158
|
+
const entry = parse(line);
|
|
159
|
+
if (entry) {
|
|
160
|
+
parsed.set(entry[0], entry[1]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseOsReleaseLine(
|
|
167
|
+
line: string
|
|
168
|
+
): readonly [string, string] | undefined {
|
|
169
|
+
const trimmed = line.trim();
|
|
170
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const equalsIndex = trimmed.indexOf("=");
|
|
175
|
+
if (equalsIndex <= 0) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const key = trimmed.slice(0, equalsIndex);
|
|
180
|
+
return /^[A-Z0-9_]+$/.test(key)
|
|
181
|
+
? [key, unquoteValue(trimmed.slice(equalsIndex + 1))]
|
|
182
|
+
: undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseColonLine(line: string): readonly [string, string] | undefined {
|
|
186
|
+
const match = line.match(/^([^:]+):\s*(.+)$/);
|
|
187
|
+
const key = match?.[1]?.trim();
|
|
188
|
+
const value = match?.[2]?.trim();
|
|
189
|
+
return key && value ? [key, value] : undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function unquoteValue(value: string): string {
|
|
193
|
+
if (value.length < 2) {
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const quote = value.charAt(0);
|
|
198
|
+
if (
|
|
199
|
+
(quote !== '"' && quote !== "'") ||
|
|
200
|
+
value.charAt(value.length - 1) !== quote
|
|
201
|
+
) {
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const unquoted = value.slice(1, -1);
|
|
206
|
+
return quote === "'" ? unquoted : unquoted.replace(/\\(["\\$`])/g, "$1");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatDatetime(d: Date): string {
|
|
210
|
+
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
211
|
+
const offsetMinutes = -d.getTimezoneOffset();
|
|
212
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
213
|
+
const absMinutes = Math.abs(offsetMinutes);
|
|
214
|
+
const offset = `${sign}${pad(Math.floor(absMinutes / 60))}:${pad(absMinutes % 60)}`;
|
|
215
|
+
const iso =
|
|
216
|
+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
|
217
|
+
`T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${offset}`;
|
|
218
|
+
const day = d.toLocaleDateString("en-US", { weekday: "long" });
|
|
219
|
+
return `${iso} (${day})`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function escapeXmlAttr(value: string): string {
|
|
223
|
+
return value
|
|
224
|
+
.replace(/&/g, "&")
|
|
225
|
+
.replace(/"/g, """)
|
|
226
|
+
.replace(/</g, "<")
|
|
227
|
+
.replace(/>/g, ">");
|
|
228
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Tools } from "../../shared/Tools";
|
|
6
|
+
import { renderWidgetLines, todoView } from "./render";
|
|
7
|
+
import { todoSchema } from "./schema";
|
|
8
|
+
import {
|
|
9
|
+
formatChecklist,
|
|
10
|
+
formatUpdateSummary,
|
|
11
|
+
getCurrentItems,
|
|
12
|
+
hasActiveItems,
|
|
13
|
+
makeDetails,
|
|
14
|
+
reconstructFromBranch,
|
|
15
|
+
replaceItems,
|
|
16
|
+
resetItems,
|
|
17
|
+
TODO_STATE_CUSTOM_TYPE,
|
|
18
|
+
} from "./todo";
|
|
19
|
+
|
|
20
|
+
const WIDGET_ID = "pim-todo";
|
|
21
|
+
|
|
22
|
+
export default function (pi: ExtensionAPI): void {
|
|
23
|
+
let pendingRefresh: ReturnType<typeof setImmediate> | undefined;
|
|
24
|
+
|
|
25
|
+
const refreshWidget = (ctx: ExtensionContext): void => {
|
|
26
|
+
if (pendingRefresh !== undefined) {
|
|
27
|
+
clearImmediate(pendingRefresh);
|
|
28
|
+
pendingRefresh = undefined;
|
|
29
|
+
}
|
|
30
|
+
if (!ctx.hasUI) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const items = getCurrentItems(ctx.sessionManager);
|
|
34
|
+
if (items.length === 0) {
|
|
35
|
+
ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Defer so the todo widget registers last, right above the editor.
|
|
39
|
+
pendingRefresh = setImmediate(() => {
|
|
40
|
+
pendingRefresh = undefined;
|
|
41
|
+
ctx.ui.setWidget(WIDGET_ID, renderWidgetLines(items, ctx.ui.theme));
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const reconstructAndRefresh = (ctx: ExtensionContext): void => {
|
|
46
|
+
reconstructFromBranch(ctx.sessionManager, ctx.sessionManager.getBranch());
|
|
47
|
+
refreshWidget(ctx);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const clearInactiveTodos = (ctx: ExtensionContext): void => {
|
|
51
|
+
const items = getCurrentItems(ctx.sessionManager);
|
|
52
|
+
if (items.length === 0 || hasActiveItems(items)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
resetItems(ctx.sessionManager);
|
|
57
|
+
refreshWidget(ctx);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
Tools.register(pi, {
|
|
61
|
+
name: "todo",
|
|
62
|
+
label: "todo",
|
|
63
|
+
description:
|
|
64
|
+
"Update the session task list. " +
|
|
65
|
+
"Each call replaces the entire list; include every item that should remain, in priority order. " +
|
|
66
|
+
"Status values are pending, in_progress, completed, and cancelled. " +
|
|
67
|
+
"At most one item may be in_progress.",
|
|
68
|
+
parameters: todoSchema,
|
|
69
|
+
renderShell: "self",
|
|
70
|
+
effect: { kind: "readOnly" },
|
|
71
|
+
executionMode: "sequential",
|
|
72
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
73
|
+
const items = replaceItems(ctx.sessionManager, params.todos);
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: formatUpdateSummary(items),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
details: makeDetails(items),
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
toViewModel: todoView,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
pi.on("session_start", (_event, ctx) => {
|
|
88
|
+
reconstructAndRefresh(ctx);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
92
|
+
reconstructAndRefresh(ctx);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
96
|
+
refreshWidget(ctx);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
pi.on("input", (_event, ctx) => {
|
|
100
|
+
clearInactiveTodos(ctx);
|
|
101
|
+
return { action: "continue" };
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
105
|
+
const items = getCurrentItems(ctx.sessionManager);
|
|
106
|
+
if (items.length === 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
pi.appendEntry(TODO_STATE_CUSTOM_TYPE, { todos: items });
|
|
110
|
+
const snapshot = formatChecklist(items, { activeOnly: true });
|
|
111
|
+
if (!snapshot) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
pi.sendMessage(
|
|
115
|
+
{
|
|
116
|
+
customType: "pim-todo-snapshot",
|
|
117
|
+
content: `Current todo list:\n${snapshot}`,
|
|
118
|
+
display: false,
|
|
119
|
+
},
|
|
120
|
+
{ triggerTurn: false }
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Format } from "../../shared/Format";
|
|
3
|
+
import type { ToolViewInput } from "../../shared/Tools";
|
|
4
|
+
import type { ToolView, ViewBlock } from "../../view/ViewBlock";
|
|
5
|
+
import type { TodoInput, TodoItem, todoSchema } from "./schema";
|
|
6
|
+
import { normalizeContent, type TodoDetails } from "./todo";
|
|
7
|
+
|
|
8
|
+
const MAX_WIDGET_LINES = 7;
|
|
9
|
+
const WIDGET_TITLE_LINES = 1;
|
|
10
|
+
const WIDGET_HINT_LINES = 1;
|
|
11
|
+
const MAX_UNTRUNCATED_WIDGET_TODOS = MAX_WIDGET_LINES - WIDGET_TITLE_LINES;
|
|
12
|
+
const MAX_TRUNCATED_WIDGET_TODOS =
|
|
13
|
+
MAX_UNTRUNCATED_WIDGET_TODOS - WIDGET_HINT_LINES;
|
|
14
|
+
const WIDGET_ANCHOR_SLOT = Math.floor(MAX_WIDGET_LINES / 2) - 1;
|
|
15
|
+
|
|
16
|
+
export type TodoViewInput = ToolViewInput<typeof todoSchema, TodoDetails>;
|
|
17
|
+
|
|
18
|
+
export function todoView({ args }: TodoViewInput): ToolView {
|
|
19
|
+
const todos = ((args ?? {}) as Partial<TodoInput>).todos;
|
|
20
|
+
const items = Array.isArray(todos) ? todos : [];
|
|
21
|
+
return {
|
|
22
|
+
label: "Todo",
|
|
23
|
+
icon: "checklist",
|
|
24
|
+
title: [{ kind: "text", text: formatStatusSummary(items) }],
|
|
25
|
+
body: inProgressSection(items),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function inProgressSection(items: readonly TodoItem[]): readonly ViewBlock[] {
|
|
30
|
+
const current = items.findLast(
|
|
31
|
+
(item) => (item as Partial<TodoItem> | null)?.status === "in_progress"
|
|
32
|
+
);
|
|
33
|
+
const content =
|
|
34
|
+
typeof current?.content === "string"
|
|
35
|
+
? normalizeContent(current.content)
|
|
36
|
+
: "";
|
|
37
|
+
if (content === "") {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
kind: "section",
|
|
43
|
+
label: "In progress",
|
|
44
|
+
icon: "checklist",
|
|
45
|
+
content: [{ kind: "spans", spans: [{ text: content, strong: true }] }],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatWidgetTitle(
|
|
51
|
+
items: readonly TodoItem[],
|
|
52
|
+
theme: Theme
|
|
53
|
+
): string {
|
|
54
|
+
const total = theme.bold(Format.count(items.length, "todo"));
|
|
55
|
+
const summary = formatStatusSummary(items);
|
|
56
|
+
return summary ? `${total} (${summary})` : total;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function renderWidgetLines(
|
|
60
|
+
items: readonly TodoItem[],
|
|
61
|
+
theme: Theme
|
|
62
|
+
): string[] {
|
|
63
|
+
if (items.length === 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const visibleItems = selectVisibleWidgetItems(items);
|
|
68
|
+
const hidden = items.length - visibleItems.length;
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
formatWidgetTitle(items, theme),
|
|
72
|
+
...visibleItems.map((item) => styleItem(item, theme)),
|
|
73
|
+
...(hidden > 0 ? [theme.fg("muted", `… +${hidden} more`)] : []),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function selectVisibleWidgetItems(
|
|
78
|
+
items: readonly TodoItem[]
|
|
79
|
+
): readonly TodoItem[] {
|
|
80
|
+
if (items.length <= MAX_UNTRUNCATED_WIDGET_TODOS) {
|
|
81
|
+
return items;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const anchorIndex = findWidgetAnchorIndex(items);
|
|
85
|
+
const start = clamp(
|
|
86
|
+
anchorIndex - WIDGET_ANCHOR_SLOT,
|
|
87
|
+
0,
|
|
88
|
+
items.length - MAX_TRUNCATED_WIDGET_TODOS
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return items.slice(start, start + MAX_TRUNCATED_WIDGET_TODOS);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findWidgetAnchorIndex(items: readonly TodoItem[]): number {
|
|
95
|
+
const inProgress = items.findIndex((item) => item.status === "in_progress");
|
|
96
|
+
if (inProgress >= 0) {
|
|
97
|
+
return inProgress;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const lastNonPending = items.findLastIndex(
|
|
101
|
+
(item) => item.status !== "pending"
|
|
102
|
+
);
|
|
103
|
+
return lastNonPending >= 0 ? lastNonPending : 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clamp(value: number, min: number, max: number): number {
|
|
107
|
+
return Math.min(Math.max(value, min), max);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatStatusSummary(items: readonly TodoItem[]): string {
|
|
111
|
+
let pending = 0;
|
|
112
|
+
let done = 0;
|
|
113
|
+
let cancelled = 0;
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
switch (item.status) {
|
|
116
|
+
case "pending":
|
|
117
|
+
case "in_progress":
|
|
118
|
+
pending += 1;
|
|
119
|
+
break;
|
|
120
|
+
case "completed":
|
|
121
|
+
done += 1;
|
|
122
|
+
break;
|
|
123
|
+
case "cancelled":
|
|
124
|
+
cancelled += 1;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const segments: string[] = [];
|
|
130
|
+
if (done > 0) {
|
|
131
|
+
segments.push(`${done} done`);
|
|
132
|
+
}
|
|
133
|
+
if (pending > 0) {
|
|
134
|
+
segments.push(`${pending} pending`);
|
|
135
|
+
}
|
|
136
|
+
if (cancelled > 0) {
|
|
137
|
+
segments.push(`${cancelled} cancelled`);
|
|
138
|
+
}
|
|
139
|
+
if (segments.length === 0) {
|
|
140
|
+
return "cleared";
|
|
141
|
+
}
|
|
142
|
+
return segments.join(", ");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function styleItem(item: TodoItem, theme: Theme): string {
|
|
146
|
+
switch (item.status) {
|
|
147
|
+
case "pending":
|
|
148
|
+
return `□ ${item.content}`;
|
|
149
|
+
case "in_progress":
|
|
150
|
+
return `${theme.fg("warning", "➤")} ${theme.bold(item.content)}`;
|
|
151
|
+
case "completed":
|
|
152
|
+
return `${theme.fg("success", "✔")} ${theme.fg("muted", item.content)}`;
|
|
153
|
+
case "cancelled":
|
|
154
|
+
return `${theme.fg("muted", "✘")} ${theme.fg("muted", theme.strikethrough(item.content))}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
export const STATUSES = [
|
|
5
|
+
"pending",
|
|
6
|
+
"in_progress",
|
|
7
|
+
"completed",
|
|
8
|
+
"cancelled",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const todoItemSchema = Type.Object({
|
|
12
|
+
content: Type.String(),
|
|
13
|
+
status: StringEnum(STATUSES),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const todoSchema = Type.Object({
|
|
17
|
+
todos: Type.Array(todoItemSchema, {
|
|
18
|
+
description: "The complete replacement task list, in priority order.",
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type TodoInput = Static<typeof todoSchema>;
|
|
23
|
+
export type TodoItem = Static<typeof todoItemSchema>;
|
|
24
|
+
export type TodoStatus = (typeof STATUSES)[number];
|