@oh-my-pi/pi-coding-agent 12.18.3 → 12.19.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.
Files changed (231) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/package.json +7 -7
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +341 -0
  5. package/src/cli/file-processor.ts +3 -3
  6. package/src/cli/list-models.ts +3 -17
  7. package/src/cli/stats-cli.ts +3 -22
  8. package/src/cli/web-search-cli.ts +8 -16
  9. package/src/commit/agentic/agent.ts +6 -9
  10. package/src/commit/agentic/index.ts +44 -50
  11. package/src/commit/agentic/state.ts +0 -9
  12. package/src/commit/agentic/tools/propose-commit.ts +1 -30
  13. package/src/commit/agentic/tools/schemas.ts +31 -0
  14. package/src/commit/agentic/tools/split-commit.ts +1 -30
  15. package/src/commit/agentic/validation.ts +1 -18
  16. package/src/commit/analysis/conventional.ts +3 -50
  17. package/src/commit/analysis/summary.ts +2 -13
  18. package/src/commit/changelog/detect.ts +4 -1
  19. package/src/commit/changelog/generate.ts +2 -25
  20. package/src/commit/changelog/index.ts +1 -2
  21. package/src/commit/cli.ts +4 -12
  22. package/src/commit/map-reduce/reduce-phase.ts +2 -43
  23. package/src/commit/pipeline.ts +7 -15
  24. package/src/commit/utils.ts +44 -0
  25. package/src/config/prompt-templates.ts +1 -81
  26. package/src/config/settings-schema.ts +20 -1
  27. package/src/config.ts +2 -3
  28. package/src/debug/index.ts +1 -6
  29. package/src/debug/system-info.ts +2 -6
  30. package/src/discovery/builtin.ts +5 -9
  31. package/src/discovery/helpers.ts +0 -26
  32. package/src/discovery/ssh.ts +1 -8
  33. package/src/exa/company.ts +8 -39
  34. package/src/exa/factory.ts +64 -0
  35. package/src/exa/index.ts +0 -16
  36. package/src/exa/linkedin.ts +8 -39
  37. package/src/exa/mcp-client.ts +0 -64
  38. package/src/exa/researcher.ts +17 -59
  39. package/src/exa/search.ts +30 -154
  40. package/src/extensibility/custom-tools/loader.ts +3 -41
  41. package/src/extensibility/extensions/loader.ts +2 -9
  42. package/src/extensibility/hooks/loader.ts +3 -20
  43. package/src/extensibility/hooks/runner.ts +3 -19
  44. package/src/extensibility/plugins/installer.ts +2 -1
  45. package/src/extensibility/plugins/loader.ts +29 -117
  46. package/src/extensibility/skills.ts +2 -89
  47. package/src/extensibility/slash-commands.ts +1 -63
  48. package/src/extensibility/utils.ts +38 -0
  49. package/src/index.ts +9 -25
  50. package/src/internal-urls/index.ts +1 -0
  51. package/src/internal-urls/jobs-protocol.ts +118 -0
  52. package/src/ipy/kernel.ts +2 -0
  53. package/src/lsp/config.ts +1 -5
  54. package/src/lsp/lspmux.ts +0 -17
  55. package/src/lsp/utils.ts +2 -24
  56. package/src/main.ts +16 -24
  57. package/src/mcp/client.ts +1 -46
  58. package/src/mcp/render.ts +8 -1
  59. package/src/mcp/tool-cache.ts +1 -5
  60. package/src/mcp/transports/http.ts +2 -7
  61. package/src/mcp/transports/stdio.ts +2 -7
  62. package/src/modes/components/bash-execution.ts +2 -16
  63. package/src/modes/components/extensions/inspector-panel.ts +8 -18
  64. package/src/modes/components/footer.ts +10 -50
  65. package/src/modes/components/model-selector.ts +2 -21
  66. package/src/modes/components/python-execution.ts +2 -16
  67. package/src/modes/components/settings-selector.ts +1 -10
  68. package/src/modes/components/status-line/segments.ts +8 -25
  69. package/src/modes/components/status-line.ts +14 -31
  70. package/src/modes/components/tool-execution.ts +8 -2
  71. package/src/modes/controllers/command-controller.ts +71 -30
  72. package/src/modes/controllers/event-controller.ts +34 -4
  73. package/src/modes/controllers/mcp-command-controller.ts +3 -34
  74. package/src/modes/controllers/selector-controller.ts +2 -2
  75. package/src/modes/controllers/ssh-command-controller.ts +3 -34
  76. package/src/modes/interactive-mode.ts +6 -2
  77. package/src/modes/rpc/rpc-client.ts +1 -5
  78. package/src/modes/shared.ts +73 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/ui-helpers.ts +26 -2
  81. package/src/patch/index.ts +4 -4
  82. package/src/patch/normalize.ts +22 -65
  83. package/src/patch/shared.ts +16 -16
  84. package/src/prompts/system/custom-system-prompt.md +0 -10
  85. package/src/prompts/system/system-prompt.md +69 -89
  86. package/src/prompts/tools/async-result.md +5 -0
  87. package/src/prompts/tools/bash.md +5 -0
  88. package/src/prompts/tools/cancel-job.md +7 -0
  89. package/src/prompts/tools/poll-jobs.md +7 -0
  90. package/src/prompts/tools/task.md +4 -0
  91. package/src/sdk.ts +70 -6
  92. package/src/session/agent-session.ts +40 -6
  93. package/src/session/agent-storage.ts +69 -278
  94. package/src/session/auth-storage.ts +14 -1430
  95. package/src/session/session-manager.ts +69 -5
  96. package/src/session/session-storage.ts +1 -5
  97. package/src/session/streaming-output.ts +637 -76
  98. package/src/slash-commands/builtin-registry.ts +8 -0
  99. package/src/ssh/connection-manager.ts +4 -12
  100. package/src/ssh/sshfs-mount.ts +3 -7
  101. package/src/ssh/utils.ts +8 -0
  102. package/src/system-prompt.ts +24 -90
  103. package/src/task/executor.ts +11 -1
  104. package/src/task/index.ts +258 -13
  105. package/src/task/parallel.ts +32 -0
  106. package/src/task/render.ts +15 -7
  107. package/src/task/types.ts +5 -0
  108. package/src/tools/ask.ts +4 -7
  109. package/src/tools/bash-interactive.ts +4 -5
  110. package/src/tools/bash.ts +125 -41
  111. package/src/tools/cancel-job.ts +93 -0
  112. package/src/tools/fetch.ts +7 -27
  113. package/src/tools/find.ts +3 -3
  114. package/src/tools/gemini-image.ts +15 -14
  115. package/src/tools/grep.ts +3 -3
  116. package/src/tools/index.ts +13 -29
  117. package/src/tools/json-tree.ts +12 -1
  118. package/src/tools/jtd-to-json-schema.ts +10 -74
  119. package/src/tools/jtd-to-typescript.ts +10 -72
  120. package/src/tools/jtd-utils.ts +102 -0
  121. package/src/tools/notebook.ts +4 -9
  122. package/src/tools/output-meta.ts +52 -26
  123. package/src/tools/path-utils.ts +13 -7
  124. package/src/tools/poll-jobs.ts +178 -0
  125. package/src/tools/python.ts +32 -35
  126. package/src/tools/read.ts +61 -82
  127. package/src/tools/render-utils.ts +8 -159
  128. package/src/tools/ssh.ts +7 -20
  129. package/src/tools/submit-result.ts +1 -1
  130. package/src/tools/tool-errors.ts +0 -30
  131. package/src/tools/tool-result.ts +1 -2
  132. package/src/tools/write.ts +8 -10
  133. package/src/tui/code-cell.ts +8 -3
  134. package/src/tui/status-line.ts +4 -4
  135. package/src/tui/types.ts +0 -1
  136. package/src/tui/utils.ts +1 -14
  137. package/src/utils/command-args.ts +76 -0
  138. package/src/utils/file-mentions.ts +15 -19
  139. package/src/utils/frontmatter.ts +5 -10
  140. package/src/utils/shell-snapshot.ts +0 -11
  141. package/src/utils/title-generator.ts +0 -12
  142. package/src/web/scrapers/artifacthub.ts +7 -16
  143. package/src/web/scrapers/arxiv.ts +3 -8
  144. package/src/web/scrapers/aur.ts +8 -22
  145. package/src/web/scrapers/biorxiv.ts +5 -14
  146. package/src/web/scrapers/bluesky.ts +13 -36
  147. package/src/web/scrapers/brew.ts +5 -10
  148. package/src/web/scrapers/cheatsh.ts +2 -12
  149. package/src/web/scrapers/chocolatey.ts +63 -26
  150. package/src/web/scrapers/choosealicense.ts +3 -18
  151. package/src/web/scrapers/cisa-kev.ts +4 -18
  152. package/src/web/scrapers/clojars.ts +6 -33
  153. package/src/web/scrapers/coingecko.ts +25 -33
  154. package/src/web/scrapers/crates-io.ts +7 -26
  155. package/src/web/scrapers/crossref.ts +4 -18
  156. package/src/web/scrapers/devto.ts +11 -41
  157. package/src/web/scrapers/discogs.ts +7 -10
  158. package/src/web/scrapers/discourse.ts +6 -31
  159. package/src/web/scrapers/dockerhub.ts +12 -35
  160. package/src/web/scrapers/fdroid.ts +8 -33
  161. package/src/web/scrapers/firefox-addons.ts +10 -34
  162. package/src/web/scrapers/flathub.ts +7 -24
  163. package/src/web/scrapers/github-gist.ts +2 -12
  164. package/src/web/scrapers/github.ts +9 -47
  165. package/src/web/scrapers/gitlab.ts +130 -185
  166. package/src/web/scrapers/go-pkg.ts +12 -22
  167. package/src/web/scrapers/hackage.ts +88 -43
  168. package/src/web/scrapers/hackernews.ts +25 -45
  169. package/src/web/scrapers/hex.ts +19 -36
  170. package/src/web/scrapers/huggingface.ts +26 -91
  171. package/src/web/scrapers/iacr.ts +3 -8
  172. package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
  173. package/src/web/scrapers/lemmy.ts +5 -23
  174. package/src/web/scrapers/lobsters.ts +16 -28
  175. package/src/web/scrapers/mastodon.ts +24 -43
  176. package/src/web/scrapers/maven.ts +6 -21
  177. package/src/web/scrapers/mdn.ts +7 -11
  178. package/src/web/scrapers/metacpan.ts +9 -41
  179. package/src/web/scrapers/musicbrainz.ts +4 -28
  180. package/src/web/scrapers/npm.ts +8 -25
  181. package/src/web/scrapers/nuget.ts +14 -37
  182. package/src/web/scrapers/nvd.ts +6 -28
  183. package/src/web/scrapers/ollama.ts +7 -34
  184. package/src/web/scrapers/open-vsx.ts +5 -19
  185. package/src/web/scrapers/opencorporates.ts +30 -14
  186. package/src/web/scrapers/openlibrary.ts +49 -33
  187. package/src/web/scrapers/orcid.ts +4 -18
  188. package/src/web/scrapers/osv.ts +7 -24
  189. package/src/web/scrapers/packagist.ts +9 -24
  190. package/src/web/scrapers/pub-dev.ts +7 -50
  191. package/src/web/scrapers/pubmed.ts +54 -21
  192. package/src/web/scrapers/pypi.ts +8 -26
  193. package/src/web/scrapers/rawg.ts +11 -19
  194. package/src/web/scrapers/readthedocs.ts +4 -9
  195. package/src/web/scrapers/reddit.ts +5 -15
  196. package/src/web/scrapers/repology.ts +8 -20
  197. package/src/web/scrapers/rfc.ts +5 -14
  198. package/src/web/scrapers/rubygems.ts +6 -21
  199. package/src/web/scrapers/searchcode.ts +8 -36
  200. package/src/web/scrapers/sec-edgar.ts +4 -18
  201. package/src/web/scrapers/semantic-scholar.ts +15 -35
  202. package/src/web/scrapers/snapcraft.ts +5 -19
  203. package/src/web/scrapers/sourcegraph.ts +5 -43
  204. package/src/web/scrapers/spdx.ts +4 -18
  205. package/src/web/scrapers/spotify.ts +4 -23
  206. package/src/web/scrapers/stackoverflow.ts +8 -13
  207. package/src/web/scrapers/terraform.ts +9 -37
  208. package/src/web/scrapers/tldr.ts +3 -7
  209. package/src/web/scrapers/twitter.ts +3 -7
  210. package/src/web/scrapers/types.ts +105 -27
  211. package/src/web/scrapers/utils.ts +97 -103
  212. package/src/web/scrapers/vimeo.ts +7 -27
  213. package/src/web/scrapers/vscode-marketplace.ts +8 -17
  214. package/src/web/scrapers/w3c.ts +6 -14
  215. package/src/web/scrapers/wikidata.ts +5 -19
  216. package/src/web/scrapers/wikipedia.ts +2 -12
  217. package/src/web/scrapers/youtube.ts +5 -34
  218. package/src/web/search/index.ts +0 -9
  219. package/src/web/search/providers/anthropic.ts +3 -2
  220. package/src/web/search/providers/brave.ts +3 -18
  221. package/src/web/search/providers/exa.ts +1 -12
  222. package/src/web/search/providers/kimi.ts +5 -44
  223. package/src/web/search/providers/perplexity.ts +1 -12
  224. package/src/web/search/providers/synthetic.ts +3 -26
  225. package/src/web/search/providers/utils.ts +36 -0
  226. package/src/web/search/providers/zai.ts +9 -50
  227. package/src/web/search/types.ts +0 -28
  228. package/src/web/search/utils.ts +17 -0
  229. package/src/tools/output-utils.ts +0 -63
  230. package/src/tools/truncate.ts +0 -385
  231. package/src/web/search/auth.ts +0 -178
@@ -1,385 +0,0 @@
1
- /**
2
- * Shared truncation utilities for tool outputs.
3
- *
4
- * Truncation is based on two independent limits - whichever is hit first wins:
5
- * - Line limit (default: 4000 lines)
6
- * - Byte limit (default: 50KB)
7
- *
8
- * Never returns partial lines (except bash tail truncation edge case
9
- * and the read tool's long-line snippet fallback).
10
- */
11
-
12
- export const DEFAULT_MAX_LINES = 3000;
13
- export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
14
- export const DEFAULT_MAX_COLUMN = 1024; // Max chars per grep match line
15
-
16
- export interface TruncationResult {
17
- /** The truncated content */
18
- content: string;
19
- /** Whether truncation occurred */
20
- truncated: boolean;
21
- /** Which limit was hit: "lines", "bytes", or null if not truncated */
22
- truncatedBy: "lines" | "bytes" | null;
23
- /** Total number of lines in the original content */
24
- totalLines: number;
25
- /** Total number of bytes in the original content */
26
- totalBytes: number;
27
- /** Number of complete lines in the truncated output */
28
- outputLines: number;
29
- /** Number of bytes in the truncated output */
30
- outputBytes: number;
31
- /** Whether the last line was partially truncated (only for tail truncation edge case) */
32
- lastLinePartial: boolean;
33
- /** Whether the first line exceeded the byte limit (for head truncation) */
34
- firstLineExceedsLimit: boolean;
35
- /** The max lines limit that was applied */
36
- maxLines: number;
37
- /** The max bytes limit that was applied */
38
- maxBytes: number;
39
- }
40
-
41
- export interface TruncationOptions {
42
- /** Maximum number of lines (default: 2000) */
43
- maxLines?: number;
44
- /** Maximum number of bytes (default: 50KB) */
45
- maxBytes?: number;
46
- }
47
-
48
- /**
49
- * Format bytes as human-readable size.
50
- */
51
- export function formatSize(bytes: number): string {
52
- if (bytes < 1024) {
53
- return `${bytes}B`;
54
- } else if (bytes < 1024 * 1024) {
55
- return `${(bytes / 1024).toFixed(1)}KB`;
56
- } else if (bytes < 1024 * 1024 * 1024) {
57
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
58
- } else {
59
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
60
- }
61
- }
62
-
63
- /**
64
- * Truncate content from the head (keep first N lines/bytes).
65
- * Suitable for file reads where you want to see the beginning.
66
- *
67
- * Never returns partial lines. If first line exceeds byte limit,
68
- * returns empty content with firstLineExceedsLimit=true.
69
- */
70
- export function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {
71
- const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
72
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
73
-
74
- const totalBytes = Buffer.byteLength(content, "utf-8");
75
- const lines = content.split("\n");
76
- const totalLines = lines.length;
77
-
78
- // Check if no truncation needed
79
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
80
- return {
81
- content,
82
- truncated: false,
83
- truncatedBy: null,
84
- totalLines,
85
- totalBytes,
86
- outputLines: totalLines,
87
- outputBytes: totalBytes,
88
- lastLinePartial: false,
89
- firstLineExceedsLimit: false,
90
- maxLines,
91
- maxBytes,
92
- };
93
- }
94
-
95
- // Check if first line alone exceeds byte limit
96
- const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
97
- if (firstLineBytes > maxBytes) {
98
- return {
99
- content: "",
100
- truncated: true,
101
- truncatedBy: "bytes",
102
- totalLines,
103
- totalBytes,
104
- outputLines: 0,
105
- outputBytes: 0,
106
- lastLinePartial: false,
107
- firstLineExceedsLimit: true,
108
- maxLines,
109
- maxBytes,
110
- };
111
- }
112
-
113
- // Collect complete lines that fit
114
- const outputLinesArr: string[] = [];
115
- let outputBytesCount = 0;
116
- let truncatedBy: "lines" | "bytes" = "lines";
117
-
118
- for (let i = 0; i < lines.length && i < maxLines; i++) {
119
- const line = lines[i];
120
- const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline
121
-
122
- if (outputBytesCount + lineBytes > maxBytes) {
123
- truncatedBy = "bytes";
124
- break;
125
- }
126
-
127
- outputLinesArr.push(line);
128
- outputBytesCount += lineBytes;
129
- }
130
-
131
- // If we exited due to line limit
132
- if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
133
- truncatedBy = "lines";
134
- }
135
-
136
- const outputContent = outputLinesArr.join("\n");
137
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
138
-
139
- return {
140
- content: outputContent,
141
- truncated: true,
142
- truncatedBy,
143
- totalLines,
144
- totalBytes,
145
- outputLines: outputLinesArr.length,
146
- outputBytes: finalOutputBytes,
147
- lastLinePartial: false,
148
- firstLineExceedsLimit: false,
149
- maxLines,
150
- maxBytes,
151
- };
152
- }
153
-
154
- /**
155
- * Truncate content from the tail (keep last N lines/bytes).
156
- * Suitable for bash output where you want to see the end (errors, final results).
157
- *
158
- * May return partial first line if the last line of original content exceeds byte limit.
159
- */
160
- export function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {
161
- const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
162
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
163
-
164
- const totalBytes = Buffer.byteLength(content, "utf-8");
165
- const lines = content.split("\n");
166
- const totalLines = lines.length;
167
-
168
- // Check if no truncation needed
169
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
170
- return {
171
- content,
172
- truncated: false,
173
- truncatedBy: null,
174
- totalLines,
175
- totalBytes,
176
- outputLines: totalLines,
177
- outputBytes: totalBytes,
178
- lastLinePartial: false,
179
- firstLineExceedsLimit: false,
180
- maxLines,
181
- maxBytes,
182
- };
183
- }
184
-
185
- // Work backwards from the end
186
- const outputLinesArr: string[] = [];
187
- let outputBytesCount = 0;
188
- let truncatedBy: "lines" | "bytes" = "lines";
189
- let lastLinePartial = false;
190
-
191
- for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
192
- const line = lines[i];
193
- const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
194
-
195
- if (outputBytesCount + lineBytes > maxBytes) {
196
- truncatedBy = "bytes";
197
- // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
198
- // take the end of the line (partial)
199
- if (outputLinesArr.length === 0) {
200
- const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
201
- outputLinesArr.unshift(truncatedLine);
202
- outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
203
- lastLinePartial = true;
204
- }
205
- break;
206
- }
207
-
208
- outputLinesArr.unshift(line);
209
- outputBytesCount += lineBytes;
210
- }
211
-
212
- // If we exited due to line limit
213
- if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
214
- truncatedBy = "lines";
215
- }
216
-
217
- const outputContent = outputLinesArr.join("\n");
218
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
219
-
220
- return {
221
- content: outputContent,
222
- truncated: true,
223
- truncatedBy,
224
- totalLines,
225
- totalBytes,
226
- outputLines: outputLinesArr.length,
227
- outputBytes: finalOutputBytes,
228
- lastLinePartial,
229
- firstLineExceedsLimit: false,
230
- maxLines,
231
- maxBytes,
232
- };
233
- }
234
-
235
- /**
236
- * Truncate a string to fit within a byte limit (from the end).
237
- * Handles multi-byte UTF-8 characters correctly.
238
- */
239
- function truncateStringToBytesFromEnd(str: string, maxBytes: number): string {
240
- const buf = Buffer.from(str, "utf-8");
241
- if (buf.length <= maxBytes) {
242
- return str;
243
- }
244
-
245
- // Start from the end, skip maxBytes back
246
- let start = buf.length - maxBytes;
247
-
248
- // Find a valid UTF-8 boundary (start of a character)
249
- while (start < buf.length && (buf[start] & 0xc0) === 0x80) {
250
- start++;
251
- }
252
-
253
- return buf.slice(start).toString("utf-8");
254
- }
255
-
256
- /**
257
- * Truncate a string to fit within a byte limit (from the start).
258
- * Handles multi-byte UTF-8 characters correctly.
259
- */
260
- export function truncateStringToBytesFromStart(str: string, maxBytes: number): { text: string; bytes: number } {
261
- const buf = Buffer.from(str, "utf-8");
262
- if (buf.length <= maxBytes) {
263
- return { text: str, bytes: buf.length };
264
- }
265
-
266
- let end = maxBytes;
267
-
268
- // Find a valid UTF-8 boundary (start of a character)
269
- while (end > 0 && (buf[end] & 0xc0) === 0x80) {
270
- end--;
271
- }
272
-
273
- if (end <= 0) {
274
- return { text: "", bytes: 0 };
275
- }
276
-
277
- const text = buf.slice(0, end).toString("utf-8");
278
- return { text, bytes: Buffer.byteLength(text, "utf-8") };
279
- }
280
-
281
- /**
282
- * Truncate a single line to max characters, adding [truncated] suffix.
283
- * Used for grep match lines.
284
- */
285
- export function truncateLine(
286
- line: string,
287
- maxChars: number = DEFAULT_MAX_COLUMN,
288
- ): { text: string; wasTruncated: boolean } {
289
- if (line.length <= maxChars) {
290
- return { text: line, wasTruncated: false };
291
- }
292
- return { text: `${line.slice(0, maxChars)}…`, wasTruncated: true };
293
- }
294
-
295
- // =============================================================================
296
- // Truncation notice formatting
297
- // =============================================================================
298
-
299
- export interface TailTruncationNoticeOptions {
300
- /** Path to full output file (e.g., from bash/python executor) */
301
- fullOutputPath?: string;
302
- /** Original content for computing last line size when lastLinePartial */
303
- originalContent?: string;
304
- /** Additional suffix to append inside the brackets */
305
- suffix?: string;
306
- }
307
-
308
- /**
309
- * Format a truncation notice for tail-truncated output (bash, python, ssh).
310
- * Returns empty string if not truncated.
311
- *
312
- * Examples:
313
- * - "[Showing last 50KB of line 1000 (line is 2.1MB). Full output: /tmp/out.txt]"
314
- * - "[Showing lines 500-1000 of 1000. Full output: /tmp/out.txt]"
315
- * - "[Showing lines 500-1000 of 1000 (50KB limit). Full output: /tmp/out.txt]"
316
- */
317
- export function formatTailTruncationNotice(
318
- truncation: TruncationResult,
319
- options: TailTruncationNoticeOptions = {},
320
- ): string {
321
- if (!truncation.truncated) {
322
- return "";
323
- }
324
-
325
- const { fullOutputPath, originalContent, suffix = "" } = options;
326
- const startLine = truncation.totalLines - truncation.outputLines + 1;
327
- const endLine = truncation.totalLines;
328
- const fullOutputPart = fullOutputPath ? `. Full output: ${fullOutputPath}` : "";
329
-
330
- let notice: string;
331
-
332
- if (truncation.lastLinePartial) {
333
- let lastLineSizePart = "";
334
- if (originalContent) {
335
- const lastLine = originalContent.split("\n").pop() || "";
336
- lastLineSizePart = ` (line is ${formatSize(Buffer.byteLength(lastLine, "utf-8"))})`;
337
- }
338
- notice = `[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine}${lastLineSizePart}${fullOutputPart}${suffix}]`;
339
- } else if (truncation.truncatedBy === "lines") {
340
- notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputPart}${suffix}]`;
341
- } else {
342
- notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(truncation.maxBytes)} limit)${fullOutputPart}${suffix}]`;
343
- }
344
-
345
- return `\n\n${notice}`;
346
- }
347
-
348
- export interface HeadTruncationNoticeOptions {
349
- /** 1-indexed start line number (default: 1) */
350
- startLine?: number;
351
- /** Total lines in the original file (for "of N" display) */
352
- totalFileLines?: number;
353
- }
354
-
355
- /**
356
- * Format a truncation notice for head-truncated output (read tool).
357
- * Returns empty string if not truncated.
358
- *
359
- * Examples:
360
- * - "[Showing lines 1-2000 of 5000. Use offset=2001 to continue]"
361
- * - "[Showing lines 100-2099 of 5000 (50KB limit). Use offset=2100 to continue]"
362
- */
363
- export function formatHeadTruncationNotice(
364
- truncation: TruncationResult,
365
- options: HeadTruncationNoticeOptions = {},
366
- ): string {
367
- if (!truncation.truncated) {
368
- return "";
369
- }
370
-
371
- const startLineDisplay = options.startLine ?? 1;
372
- const totalFileLines = options.totalFileLines ?? truncation.totalLines;
373
- const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
374
- const nextOffset = endLineDisplay + 1;
375
-
376
- let notice: string;
377
-
378
- if (truncation.truncatedBy === "lines") {
379
- notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
380
- } else {
381
- notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(truncation.maxBytes)} limit). Use offset=${nextOffset} to continue]`;
382
- }
383
-
384
- return `\n\n${notice}`;
385
- }
@@ -1,178 +0,0 @@
1
- /**
2
- * Anthropic Authentication
3
- *
4
- * 4-tier auth resolution:
5
- * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL env vars
6
- * 2. Provider with api="anthropic-messages" in ~/.omp/agent/models.json
7
- * 3. OAuth credentials in ~/.omp/agent/agent.db (with expiry check)
8
- * 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
9
- */
10
- import { buildAnthropicHeaders as buildProviderAnthropicHeaders, getEnvApiKey } from "@oh-my-pi/pi-ai";
11
- import { $env, logger } from "@oh-my-pi/pi-utils";
12
- import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
13
- import { AgentStorage } from "../../session/agent-storage";
14
- import type { AuthCredential } from "../../session/auth-storage";
15
- import type { AnthropicAuthConfig, AnthropicOAuthCredential, ModelsJson } from "./types";
16
-
17
- const DEFAULT_BASE_URL = "https://api.anthropic.com";
18
-
19
- /**
20
- * Reads and parses a JSON file safely.
21
- * @param filePath - Path to the JSON file
22
- * @returns Parsed JSON content, or null if file doesn't exist or parsing fails
23
- */
24
- async function readJson<T>(filePath: string): Promise<T | null> {
25
- try {
26
- const file = Bun.file(filePath);
27
- if (!(await file.exists())) return null;
28
- const content = await file.text();
29
- return JSON.parse(content) as T;
30
- } catch (error) {
31
- logger.warn("Failed to parse JSON file", { path: filePath, error: String(error) });
32
- return null;
33
- }
34
- }
35
-
36
- /**
37
- * Checks if a token is an OAuth token by looking for sk-ant-oat prefix.
38
- * @param apiKey - The API key to check
39
- * @returns True if the token is an OAuth token
40
- */
41
- export function isOAuthToken(apiKey: string): boolean {
42
- return apiKey.includes("sk-ant-oat");
43
- }
44
-
45
- /**
46
- * Converts a generic AuthCredential to AnthropicOAuthCredential if it's a valid OAuth entry.
47
- * @param credential - The credential to convert
48
- * @returns The converted OAuth credential, or null if not a valid OAuth type
49
- */
50
- function toAnthropicOAuthCredential(credential: AuthCredential): AnthropicOAuthCredential | null {
51
- if (credential.type !== "oauth") return null;
52
- if (typeof credential.access !== "string" || typeof credential.expires !== "number") return null;
53
- return {
54
- type: "oauth",
55
- access: credential.access,
56
- refresh: credential.refresh,
57
- expires: credential.expires,
58
- };
59
- }
60
-
61
- /**
62
- * Reads Anthropic OAuth credentials from agent.db.
63
- * @returns Array of valid Anthropic OAuth credentials
64
- */
65
- async function readAnthropicOAuthCredentials(): Promise<AnthropicOAuthCredential[]> {
66
- const storage = await AgentStorage.open(getAgentDbPath());
67
- const records = storage.listAuthCredentials("anthropic");
68
- const credentials: AnthropicOAuthCredential[] = [];
69
- for (const record of records) {
70
- const mapped = toAnthropicOAuthCredential(record.credential);
71
- if (mapped) {
72
- credentials.push(mapped);
73
- }
74
- }
75
-
76
- return credentials;
77
- }
78
-
79
- /**
80
- * Finds Anthropic auth config using 4-tier priority:
81
- * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
82
- * 2. Provider with api="anthropic-messages" in models.json
83
- * 3. OAuth in agent.db (with 5-minute expiry buffer)
84
- * 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
85
- * @returns The first valid auth configuration found, or null if none available
86
- */
87
- export async function findAnthropicAuth(): Promise<AnthropicAuthConfig | null> {
88
- const configDir = getAgentDir();
89
-
90
- // 1. Explicit search-specific env vars
91
- const searchApiKey = $env.ANTHROPIC_SEARCH_API_KEY;
92
- const searchBaseUrl = $env.ANTHROPIC_SEARCH_BASE_URL;
93
- if (searchApiKey) {
94
- return {
95
- apiKey: searchApiKey,
96
- baseUrl: searchBaseUrl ?? DEFAULT_BASE_URL,
97
- isOAuth: isOAuthToken(searchApiKey),
98
- };
99
- }
100
-
101
- // 2. Provider with api="anthropic-messages" in models.json
102
- const modelsJson = await readJson<ModelsJson>(`${configDir}/models.json`);
103
- if (modelsJson?.providers) {
104
- // First pass: look for providers with actual API keys
105
- for (const [_name, provider] of Object.entries(modelsJson.providers)) {
106
- if (provider.api === "anthropic-messages" && provider.apiKey && provider.apiKey !== "none") {
107
- return {
108
- apiKey: provider.apiKey,
109
- baseUrl: provider.baseUrl ?? DEFAULT_BASE_URL,
110
- isOAuth: isOAuthToken(provider.apiKey),
111
- };
112
- }
113
- }
114
- // Second pass: check for proxy mode (baseUrl but apiKey="none")
115
- for (const [_name, provider] of Object.entries(modelsJson.providers)) {
116
- if (provider.api === "anthropic-messages" && provider.baseUrl) {
117
- return {
118
- apiKey: provider.apiKey ?? "",
119
- baseUrl: provider.baseUrl,
120
- isOAuth: false,
121
- };
122
- }
123
- }
124
- }
125
-
126
- // 3. OAuth credentials in agent.db (with 5-minute expiry buffer)
127
- const expiryBuffer = 5 * 60 * 1000; // 5 minutes
128
- const now = Date.now();
129
- const credentials = await readAnthropicOAuthCredentials();
130
- for (const credential of credentials) {
131
- if (!credential.access) continue;
132
- if (credential.expires > now + expiryBuffer) {
133
- return {
134
- apiKey: credential.access,
135
- baseUrl: DEFAULT_BASE_URL,
136
- isOAuth: true,
137
- };
138
- }
139
- }
140
-
141
- // 4. Generic ANTHROPIC_API_KEY fallback
142
- const apiKey = getEnvApiKey("anthropic");
143
- const baseUrl = $env.ANTHROPIC_BASE_URL;
144
- if (apiKey) {
145
- return {
146
- apiKey,
147
- baseUrl: baseUrl ?? DEFAULT_BASE_URL,
148
- isOAuth: isOAuthToken(apiKey),
149
- };
150
- }
151
-
152
- return null;
153
- }
154
-
155
- /**
156
- * Builds HTTP headers for Anthropic API requests.
157
- * @param auth - The authentication configuration
158
- * @returns Headers object ready for use in fetch requests
159
- */
160
- export function buildAnthropicHeaders(auth: AnthropicAuthConfig): Record<string, string> {
161
- return buildProviderAnthropicHeaders({
162
- apiKey: auth.apiKey,
163
- baseUrl: auth.baseUrl,
164
- isOAuth: auth.isOAuth,
165
- extraBetas: ["web-search-2025-03-05"],
166
- stream: false,
167
- });
168
- }
169
-
170
- /**
171
- * Builds the full API URL for Anthropic messages endpoint.
172
- * @param auth - The authentication configuration
173
- * @returns The complete API URL with beta query parameter
174
- */
175
- export function buildAnthropicUrl(auth: AnthropicAuthConfig): string {
176
- const base = `${auth.baseUrl}/v1/messages`;
177
- return `${base}?beta=true`;
178
- }