@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.
- package/CHANGELOG.md +47 -0
- package/package.json +7 -7
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +341 -0
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +3 -17
- package/src/cli/stats-cli.ts +3 -22
- package/src/cli/web-search-cli.ts +8 -16
- package/src/commit/agentic/agent.ts +6 -9
- package/src/commit/agentic/index.ts +44 -50
- package/src/commit/agentic/state.ts +0 -9
- package/src/commit/agentic/tools/propose-commit.ts +1 -30
- package/src/commit/agentic/tools/schemas.ts +31 -0
- package/src/commit/agentic/tools/split-commit.ts +1 -30
- package/src/commit/agentic/validation.ts +1 -18
- package/src/commit/analysis/conventional.ts +3 -50
- package/src/commit/analysis/summary.ts +2 -13
- package/src/commit/changelog/detect.ts +4 -1
- package/src/commit/changelog/generate.ts +2 -25
- package/src/commit/changelog/index.ts +1 -2
- package/src/commit/cli.ts +4 -12
- package/src/commit/map-reduce/reduce-phase.ts +2 -43
- package/src/commit/pipeline.ts +7 -15
- package/src/commit/utils.ts +44 -0
- package/src/config/prompt-templates.ts +1 -81
- package/src/config/settings-schema.ts +20 -1
- package/src/config.ts +2 -3
- package/src/debug/index.ts +1 -6
- package/src/debug/system-info.ts +2 -6
- package/src/discovery/builtin.ts +5 -9
- package/src/discovery/helpers.ts +0 -26
- package/src/discovery/ssh.ts +1 -8
- package/src/exa/company.ts +8 -39
- package/src/exa/factory.ts +64 -0
- package/src/exa/index.ts +0 -16
- package/src/exa/linkedin.ts +8 -39
- package/src/exa/mcp-client.ts +0 -64
- package/src/exa/researcher.ts +17 -59
- package/src/exa/search.ts +30 -154
- package/src/extensibility/custom-tools/loader.ts +3 -41
- package/src/extensibility/extensions/loader.ts +2 -9
- package/src/extensibility/hooks/loader.ts +3 -20
- package/src/extensibility/hooks/runner.ts +3 -19
- package/src/extensibility/plugins/installer.ts +2 -1
- package/src/extensibility/plugins/loader.ts +29 -117
- package/src/extensibility/skills.ts +2 -89
- package/src/extensibility/slash-commands.ts +1 -63
- package/src/extensibility/utils.ts +38 -0
- package/src/index.ts +9 -25
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/jobs-protocol.ts +118 -0
- package/src/ipy/kernel.ts +2 -0
- package/src/lsp/config.ts +1 -5
- package/src/lsp/lspmux.ts +0 -17
- package/src/lsp/utils.ts +2 -24
- package/src/main.ts +16 -24
- package/src/mcp/client.ts +1 -46
- package/src/mcp/render.ts +8 -1
- package/src/mcp/tool-cache.ts +1 -5
- package/src/mcp/transports/http.ts +2 -7
- package/src/mcp/transports/stdio.ts +2 -7
- package/src/modes/components/bash-execution.ts +2 -16
- package/src/modes/components/extensions/inspector-panel.ts +8 -18
- package/src/modes/components/footer.ts +10 -50
- package/src/modes/components/model-selector.ts +2 -21
- package/src/modes/components/python-execution.ts +2 -16
- package/src/modes/components/settings-selector.ts +1 -10
- package/src/modes/components/status-line/segments.ts +8 -25
- package/src/modes/components/status-line.ts +14 -31
- package/src/modes/components/tool-execution.ts +8 -2
- package/src/modes/controllers/command-controller.ts +71 -30
- package/src/modes/controllers/event-controller.ts +34 -4
- package/src/modes/controllers/mcp-command-controller.ts +3 -34
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/controllers/ssh-command-controller.ts +3 -34
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/rpc/rpc-client.ts +1 -5
- package/src/modes/shared.ts +73 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +26 -2
- package/src/patch/index.ts +4 -4
- package/src/patch/normalize.ts +22 -65
- package/src/patch/shared.ts +16 -16
- package/src/prompts/system/custom-system-prompt.md +0 -10
- package/src/prompts/system/system-prompt.md +69 -89
- package/src/prompts/tools/async-result.md +5 -0
- package/src/prompts/tools/bash.md +5 -0
- package/src/prompts/tools/cancel-job.md +7 -0
- package/src/prompts/tools/poll-jobs.md +7 -0
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +70 -6
- package/src/session/agent-session.ts +40 -6
- package/src/session/agent-storage.ts +69 -278
- package/src/session/auth-storage.ts +14 -1430
- package/src/session/session-manager.ts +69 -5
- package/src/session/session-storage.ts +1 -5
- package/src/session/streaming-output.ts +637 -76
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/ssh/connection-manager.ts +4 -12
- package/src/ssh/sshfs-mount.ts +3 -7
- package/src/ssh/utils.ts +8 -0
- package/src/system-prompt.ts +24 -90
- package/src/task/executor.ts +11 -1
- package/src/task/index.ts +258 -13
- package/src/task/parallel.ts +32 -0
- package/src/task/render.ts +15 -7
- package/src/task/types.ts +5 -0
- package/src/tools/ask.ts +4 -7
- package/src/tools/bash-interactive.ts +4 -5
- package/src/tools/bash.ts +125 -41
- package/src/tools/cancel-job.ts +93 -0
- package/src/tools/fetch.ts +7 -27
- package/src/tools/find.ts +3 -3
- package/src/tools/gemini-image.ts +15 -14
- package/src/tools/grep.ts +3 -3
- package/src/tools/index.ts +13 -29
- package/src/tools/json-tree.ts +12 -1
- package/src/tools/jtd-to-json-schema.ts +10 -74
- package/src/tools/jtd-to-typescript.ts +10 -72
- package/src/tools/jtd-utils.ts +102 -0
- package/src/tools/notebook.ts +4 -9
- package/src/tools/output-meta.ts +52 -26
- package/src/tools/path-utils.ts +13 -7
- package/src/tools/poll-jobs.ts +178 -0
- package/src/tools/python.ts +32 -35
- package/src/tools/read.ts +61 -82
- package/src/tools/render-utils.ts +8 -159
- package/src/tools/ssh.ts +7 -20
- package/src/tools/submit-result.ts +1 -1
- package/src/tools/tool-errors.ts +0 -30
- package/src/tools/tool-result.ts +1 -2
- package/src/tools/write.ts +8 -10
- package/src/tui/code-cell.ts +8 -3
- package/src/tui/status-line.ts +4 -4
- package/src/tui/types.ts +0 -1
- package/src/tui/utils.ts +1 -14
- package/src/utils/command-args.ts +76 -0
- package/src/utils/file-mentions.ts +15 -19
- package/src/utils/frontmatter.ts +5 -10
- package/src/utils/shell-snapshot.ts +0 -11
- package/src/utils/title-generator.ts +0 -12
- package/src/web/scrapers/artifacthub.ts +7 -16
- package/src/web/scrapers/arxiv.ts +3 -8
- package/src/web/scrapers/aur.ts +8 -22
- package/src/web/scrapers/biorxiv.ts +5 -14
- package/src/web/scrapers/bluesky.ts +13 -36
- package/src/web/scrapers/brew.ts +5 -10
- package/src/web/scrapers/cheatsh.ts +2 -12
- package/src/web/scrapers/chocolatey.ts +63 -26
- package/src/web/scrapers/choosealicense.ts +3 -18
- package/src/web/scrapers/cisa-kev.ts +4 -18
- package/src/web/scrapers/clojars.ts +6 -33
- package/src/web/scrapers/coingecko.ts +25 -33
- package/src/web/scrapers/crates-io.ts +7 -26
- package/src/web/scrapers/crossref.ts +4 -18
- package/src/web/scrapers/devto.ts +11 -41
- package/src/web/scrapers/discogs.ts +7 -10
- package/src/web/scrapers/discourse.ts +6 -31
- package/src/web/scrapers/dockerhub.ts +12 -35
- package/src/web/scrapers/fdroid.ts +8 -33
- package/src/web/scrapers/firefox-addons.ts +10 -34
- package/src/web/scrapers/flathub.ts +7 -24
- package/src/web/scrapers/github-gist.ts +2 -12
- package/src/web/scrapers/github.ts +9 -47
- package/src/web/scrapers/gitlab.ts +130 -185
- package/src/web/scrapers/go-pkg.ts +12 -22
- package/src/web/scrapers/hackage.ts +88 -43
- package/src/web/scrapers/hackernews.ts +25 -45
- package/src/web/scrapers/hex.ts +19 -36
- package/src/web/scrapers/huggingface.ts +26 -91
- package/src/web/scrapers/iacr.ts +3 -8
- package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
- package/src/web/scrapers/lemmy.ts +5 -23
- package/src/web/scrapers/lobsters.ts +16 -28
- package/src/web/scrapers/mastodon.ts +24 -43
- package/src/web/scrapers/maven.ts +6 -21
- package/src/web/scrapers/mdn.ts +7 -11
- package/src/web/scrapers/metacpan.ts +9 -41
- package/src/web/scrapers/musicbrainz.ts +4 -28
- package/src/web/scrapers/npm.ts +8 -25
- package/src/web/scrapers/nuget.ts +14 -37
- package/src/web/scrapers/nvd.ts +6 -28
- package/src/web/scrapers/ollama.ts +7 -34
- package/src/web/scrapers/open-vsx.ts +5 -19
- package/src/web/scrapers/opencorporates.ts +30 -14
- package/src/web/scrapers/openlibrary.ts +49 -33
- package/src/web/scrapers/orcid.ts +4 -18
- package/src/web/scrapers/osv.ts +7 -24
- package/src/web/scrapers/packagist.ts +9 -24
- package/src/web/scrapers/pub-dev.ts +7 -50
- package/src/web/scrapers/pubmed.ts +54 -21
- package/src/web/scrapers/pypi.ts +8 -26
- package/src/web/scrapers/rawg.ts +11 -19
- package/src/web/scrapers/readthedocs.ts +4 -9
- package/src/web/scrapers/reddit.ts +5 -15
- package/src/web/scrapers/repology.ts +8 -20
- package/src/web/scrapers/rfc.ts +5 -14
- package/src/web/scrapers/rubygems.ts +6 -21
- package/src/web/scrapers/searchcode.ts +8 -36
- package/src/web/scrapers/sec-edgar.ts +4 -18
- package/src/web/scrapers/semantic-scholar.ts +15 -35
- package/src/web/scrapers/snapcraft.ts +5 -19
- package/src/web/scrapers/sourcegraph.ts +5 -43
- package/src/web/scrapers/spdx.ts +4 -18
- package/src/web/scrapers/spotify.ts +4 -23
- package/src/web/scrapers/stackoverflow.ts +8 -13
- package/src/web/scrapers/terraform.ts +9 -37
- package/src/web/scrapers/tldr.ts +3 -7
- package/src/web/scrapers/twitter.ts +3 -7
- package/src/web/scrapers/types.ts +105 -27
- package/src/web/scrapers/utils.ts +97 -103
- package/src/web/scrapers/vimeo.ts +7 -27
- package/src/web/scrapers/vscode-marketplace.ts +8 -17
- package/src/web/scrapers/w3c.ts +6 -14
- package/src/web/scrapers/wikidata.ts +5 -19
- package/src/web/scrapers/wikipedia.ts +2 -12
- package/src/web/scrapers/youtube.ts +5 -34
- package/src/web/search/index.ts +0 -9
- package/src/web/search/providers/anthropic.ts +3 -2
- package/src/web/search/providers/brave.ts +3 -18
- package/src/web/search/providers/exa.ts +1 -12
- package/src/web/search/providers/kimi.ts +5 -44
- package/src/web/search/providers/perplexity.ts +1 -12
- package/src/web/search/providers/synthetic.ts +3 -26
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +9 -50
- package/src/web/search/types.ts +0 -28
- package/src/web/search/utils.ts +17 -0
- package/src/tools/output-utils.ts +0 -63
- package/src/tools/truncate.ts +0 -385
- package/src/web/search/auth.ts +0 -178
package/src/tools/truncate.ts
DELETED
|
@@ -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
|
-
}
|
package/src/web/search/auth.ts
DELETED
|
@@ -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
|
-
}
|