@oh-my-pi/pi-coding-agent 15.7.6 → 15.8.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 +146 -198
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/hook-selector.d.ts +3 -0
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -2
- package/dist/types/modes/types.d.ts +4 -2
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/eval/__tests__/agent-bridge.test.ts +58 -4
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/py/prelude.py +23 -15
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/hook-selector.ts +89 -31
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +16 -3
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +19 -1
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +125 -15
- package/src/modes/types.ts +5 -2
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +5 -5
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +62 -46
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +1 -1
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fully automated Claude Code /v1/messages capture helper.
|
|
3
|
+
*
|
|
4
|
+
* Starts a local CONNECT proxy, MITMs TLS using a local self-signed debug
|
|
5
|
+
* certificate, drives Claude Code through a headless PTY/xterm, and returns the
|
|
6
|
+
* first completed /v1/messages request/response exchange.
|
|
7
|
+
*/
|
|
8
|
+
import * as net from "node:net";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as tls from "node:tls";
|
|
11
|
+
import * as zlib from "node:zlib";
|
|
12
|
+
import { PtySession } from "@oh-my-pi/pi-natives";
|
|
13
|
+
import xterm from "@xterm/headless";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PROXY_HOST = "127.0.0.1";
|
|
16
|
+
const DEFAULT_PROXY_PORT = 8080;
|
|
17
|
+
const DEFAULT_COMMAND = "claude";
|
|
18
|
+
const DEFAULT_MESSAGE = "hi";
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
20
|
+
const DEFAULT_INPUT_DELAY_MS = 1_000;
|
|
21
|
+
const DEFAULT_COLS = 120;
|
|
22
|
+
const DEFAULT_ROWS = 40;
|
|
23
|
+
const DOUBLE_CRLF = Buffer.from("\r\n\r\n", "latin1");
|
|
24
|
+
const CRLF = Buffer.from("\r\n", "latin1");
|
|
25
|
+
const TEXT_DECODER = new TextDecoder();
|
|
26
|
+
|
|
27
|
+
// Debug-only local MITM certificate. Claude is launched with
|
|
28
|
+
// NODE_TLS_REJECT_UNAUTHORIZED=0, so the certificate has no trust value; it only
|
|
29
|
+
// lets Node's TLS stack complete the CONNECT tunnel handshake.
|
|
30
|
+
export const CLAUDE_TRACE_DEBUG_CERT = `-----BEGIN CERTIFICATE-----
|
|
31
|
+
MIIDFzCCAf+gAwIBAgIUAe9omAqLbydZc5ZYZGhwbbpMSF0wDQYJKoZIhvcNAQEL
|
|
32
|
+
BQAwGzEZMBcGA1UEAwwQb21wLWNsYXVkZS10cmFjZTAeFw0yNjA2MDIwODA2MjFa
|
|
33
|
+
Fw0zNjA1MzAwODA2MjFaMBsxGTAXBgNVBAMMEG9tcC1jbGF1ZGUtdHJhY2UwggEi
|
|
34
|
+
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmpGe5T8B0oA2L82Rn5JJdXOBS
|
|
35
|
+
ZX0DyBjiIK+Tqe8T3oAr41XDLnweqtrMDSBDYbVqAoKjNbaTUSYYcxSm0MAVs63w
|
|
36
|
+
08SfJmShZM9pElfANqXqMiyhksFgji7JEyt/rbbId207a7s5KvRvm3g/sxN/wGtr
|
|
37
|
+
C5LCLMlc2GWEGD8qrVIQbmLw884qvtXi70RFUPP3Wpy4wGMWSdE+9IA27R5cMJS5
|
|
38
|
+
oHsO4HGB6J8VzLY+HGY2yr4BJ9qrAyjd1UetFd9RdcjyWpsbAfX8nWP+uleTNOiT
|
|
39
|
+
ExNz7dPt/k6OPLNmI1iT/ruRS0uUzHZTimPd67TPQR/70RaW7Bh5wArawGw9AgMB
|
|
40
|
+
AAGjUzBRMB0GA1UdDgQWBBQa4Ir8P3GAolZoPiuB4V2cq3riAjAfBgNVHSMEGDAW
|
|
41
|
+
gBQa4Ir8P3GAolZoPiuB4V2cq3riAjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
|
|
42
|
+
DQEBCwUAA4IBAQClPYki235gDEUu7eDm60qsAGWxbKVv4pSh+vB+xgNgzMk4aOuU
|
|
43
|
+
mSfp8Y8covwklph8VfDoKTaEGqqX0Q5s74Ctl6Mwy7b0u8Zztk/g4GynLocI7TQD
|
|
44
|
+
ftZMgZka49+FkEsjp+XZtQbO4vOL5UsccpsLhFQQQuhVyiJ4gNo/VzgvSDkBuf3Q
|
|
45
|
+
Rz7xFiDKCqFEoMPty4+nKEw5832FJ5mDCOyMk6fGSO8Wbt/hmRQQFu2cSdoBs0OT
|
|
46
|
+
AQQJETQjPkKeTDX4jdSAlOeKwfyjfdfgeQuMkzX8xafisJa66MLPzOVbIuGbvbWD
|
|
47
|
+
QVCd76iYPcfNK+JZUhmAUvTHSuwgJMZ6+NgI
|
|
48
|
+
-----END CERTIFICATE-----`;
|
|
49
|
+
|
|
50
|
+
export const CLAUDE_TRACE_DEBUG_KEY = `-----BEGIN PRIVATE KEY-----
|
|
51
|
+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmpGe5T8B0oA2L
|
|
52
|
+
82Rn5JJdXOBSZX0DyBjiIK+Tqe8T3oAr41XDLnweqtrMDSBDYbVqAoKjNbaTUSYY
|
|
53
|
+
cxSm0MAVs63w08SfJmShZM9pElfANqXqMiyhksFgji7JEyt/rbbId207a7s5KvRv
|
|
54
|
+
m3g/sxN/wGtrC5LCLMlc2GWEGD8qrVIQbmLw884qvtXi70RFUPP3Wpy4wGMWSdE+
|
|
55
|
+
9IA27R5cMJS5oHsO4HGB6J8VzLY+HGY2yr4BJ9qrAyjd1UetFd9RdcjyWpsbAfX8
|
|
56
|
+
nWP+uleTNOiTExNz7dPt/k6OPLNmI1iT/ruRS0uUzHZTimPd67TPQR/70RaW7Bh5
|
|
57
|
+
wArawGw9AgMBAAECggEADX2mhA3H0pPuj35J36X/5Me9xWM//AwOr6febwGalazg
|
|
58
|
+
Ctg3EOZ01/VptzaiKQetAdhoLmxidooNn9HD7JQJKPid7q7w7m1+R26mN/xrLD2A
|
|
59
|
+
WyBqv+iQoo+ANs5y1BMChuIxmVY/FwFk6UWDNlekuXqgzPln4okbrYTmBbaszniO
|
|
60
|
+
Mu1SI/3fpnTA3iJ634FUSRVoUPP8r0WEEUtpW1wAhsJR701gvKRYw/+YcRglkhm7
|
|
61
|
+
T4l6TuBcgIVzUqAc3oZLHVIMKN0ZprZSeopSRozTcUANfYONakvK9Hx1qf+/rmTR
|
|
62
|
+
qZHg2uOxlqvxyABnwdk8rmyFx8YqUeN9jaAbbXxtBQKBgQDQRjc0gVg4STqcFqUu
|
|
63
|
+
FW35MZ88S7+xTuRd/EG1dpsu2lptx1yhSLTsF5GfxBQKXCQUfWrpkyuCmlV6s+wJ
|
|
64
|
+
H0LSyAJQ4ffBsFterQz7dRKTlhRJNk5PYn8jjNCAuBYVSbQZqZ3yZgG3CT3G5+PZ
|
|
65
|
+
8Ln3tJHqTRfP5B8KTMcNYiLI0wKBgQDM0/gdb9Dvdz/32GIpxwNNIb52IxnNVVrm
|
|
66
|
+
M69+4XNg6CqvctZFuaMQ03W2J5IKAdESaCGLz9pwHZRjfBORcw3BPKD2QkfN5NJg
|
|
67
|
+
hWvLlfAsblCYiCCjTCB6rf1OJOQ5fHoNFh1wqDaQCk0flsb9nlZmQQR5ZUHaJhSC
|
|
68
|
+
QqMmeKvMrwKBgQDDzp+sH0Z/dGlDwg59auw/caWRHG4WFmOg8L4eCmoO/H4z41B0
|
|
69
|
+
2VQu+mGQYNmue733/Yl8Gz62xL5EY88vLFK4tA1pWWiCknj0Y6Fm70QNuPVNd17c
|
|
70
|
+
R2/cTlDgEzG/xdEqp0q1T62hFXEdBXoztZxBA2SDcQNIEeIU3uXs8SxevQKBgFp4
|
|
71
|
+
acf+wody4aNERR900sV32RtvJ49lWxAA1kwxone0NF5oV7JWa2scK4r4cW3QHZuG
|
|
72
|
+
uQJ7HV2WAxvqCu6cpf+rGuGKpxKPNkkBxXoX0Qye8SReRCQ8lL/7J74jV1b43yP2
|
|
73
|
+
l6xR8D+w/R2tyFjvXfQuVZ6VFgAX/8kFS/DLLf7rAoGAcnFgCwyzcq6FWL8iW23J
|
|
74
|
+
GnbZ0IQk6SPch87MzMmnOFlEXrCf5l832vwI65tNzOoB0yQoWVfBv5sb4Zy9zeFj
|
|
75
|
+
FbkpRZC0Kfi9PLzDV4IawoIINYthOJxIKJg+yrmrUWCggXxwdzYIYKLRIskMXoYs
|
|
76
|
+
mNMXfUstElEcKO7+DKiPi6U=
|
|
77
|
+
-----END PRIVATE KEY-----`;
|
|
78
|
+
|
|
79
|
+
export interface HeaderEntry {
|
|
80
|
+
name: string;
|
|
81
|
+
value: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CapturedRequest {
|
|
85
|
+
method: string;
|
|
86
|
+
path: string;
|
|
87
|
+
version: string;
|
|
88
|
+
headers: HeaderEntry[];
|
|
89
|
+
body: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CapturedResponse {
|
|
93
|
+
statusCode: number | undefined;
|
|
94
|
+
statusMessage: string;
|
|
95
|
+
version: string;
|
|
96
|
+
headers: HeaderEntry[];
|
|
97
|
+
body: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CapturedMessagesExchange {
|
|
101
|
+
target: string;
|
|
102
|
+
request: CapturedRequest;
|
|
103
|
+
response: CapturedResponse;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ClaudeMessagesProxyOptions {
|
|
107
|
+
host?: string;
|
|
108
|
+
port?: number;
|
|
109
|
+
upstreamTlsRejectUnauthorized?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface ParsedHttpMessage {
|
|
113
|
+
startLine: string;
|
|
114
|
+
method?: string;
|
|
115
|
+
path?: string;
|
|
116
|
+
version: string;
|
|
117
|
+
statusCode?: number;
|
|
118
|
+
statusMessage?: string;
|
|
119
|
+
headers: HeaderEntry[];
|
|
120
|
+
body: Buffer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface ConnectTarget {
|
|
124
|
+
host: string;
|
|
125
|
+
port: number;
|
|
126
|
+
display: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type BodyMode = { kind: "none" } | { kind: "fixed"; length: number } | { kind: "chunked" } | { kind: "until-end" };
|
|
130
|
+
|
|
131
|
+
interface ParserState {
|
|
132
|
+
head: Omit<ParsedHttpMessage, "body">;
|
|
133
|
+
bodyMode: BodyMode;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface ChunkedParseResult {
|
|
137
|
+
complete: boolean;
|
|
138
|
+
consumed: number;
|
|
139
|
+
body: Buffer;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface CaptureWaiter {
|
|
143
|
+
resolve: (exchange: CapturedMessagesExchange) => void;
|
|
144
|
+
reject: (error: Error) => void;
|
|
145
|
+
timer: NodeJS.Timeout;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ClaudeTraceCommandArgs {
|
|
149
|
+
command?: string;
|
|
150
|
+
message?: string;
|
|
151
|
+
cwd?: string;
|
|
152
|
+
host?: string;
|
|
153
|
+
port?: number;
|
|
154
|
+
timeoutMs?: number;
|
|
155
|
+
inputDelayMs?: number;
|
|
156
|
+
json?: boolean;
|
|
157
|
+
upstreamTlsRejectUnauthorized?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const XtermTerminal = xterm.Terminal;
|
|
161
|
+
|
|
162
|
+
function headerValue(headers: readonly HeaderEntry[], name: string): string | undefined {
|
|
163
|
+
for (const header of headers) {
|
|
164
|
+
if (header.name.toLowerCase() === name) return header.value;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasChunkedTransfer(headers: readonly HeaderEntry[]): boolean {
|
|
170
|
+
const value = headerValue(headers, "transfer-encoding");
|
|
171
|
+
return (
|
|
172
|
+
value
|
|
173
|
+
?.toLowerCase()
|
|
174
|
+
.split(",")
|
|
175
|
+
.some(part => part.trim() === "chunked") === true
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function contentLength(headers: readonly HeaderEntry[]): number {
|
|
180
|
+
const value = headerValue(headers, "content-length");
|
|
181
|
+
if (!value) return 0;
|
|
182
|
+
const parsed = Number.parseInt(value, 10);
|
|
183
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
184
|
+
throw new Error(`Invalid Content-Length header: ${value}`);
|
|
185
|
+
}
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
interface PendingCapturedRequest {
|
|
189
|
+
target: string;
|
|
190
|
+
request: CapturedRequest;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseHeaders(headText: string): { startLine: string; headers: HeaderEntry[] } {
|
|
194
|
+
const lines = headText.split("\r\n");
|
|
195
|
+
const startLine = lines[0] ?? "";
|
|
196
|
+
const headers: HeaderEntry[] = [];
|
|
197
|
+
for (let i = 1; i < lines.length; i++) {
|
|
198
|
+
const line = lines[i]!;
|
|
199
|
+
const colon = line.indexOf(":");
|
|
200
|
+
if (colon <= 0) continue;
|
|
201
|
+
headers.push({ name: line.slice(0, colon), value: line.slice(colon + 1).trim() });
|
|
202
|
+
}
|
|
203
|
+
return { startLine, headers };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseRequestStartLine(startLine: string): Pick<ParsedHttpMessage, "method" | "path" | "version"> {
|
|
207
|
+
const parts = startLine.split(/\s+/u);
|
|
208
|
+
return {
|
|
209
|
+
method: parts[0] ?? "",
|
|
210
|
+
path: parts[1] ?? "",
|
|
211
|
+
version: parts[2] ?? "",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseResponseStartLine(
|
|
216
|
+
startLine: string,
|
|
217
|
+
): Pick<ParsedHttpMessage, "statusCode" | "statusMessage" | "version"> {
|
|
218
|
+
const match = /^(HTTP\/\d(?:\.\d)?)\s+(\d{3})(?:\s+(.*))?$/u.exec(startLine);
|
|
219
|
+
if (!match) return { version: "", statusCode: undefined, statusMessage: "" };
|
|
220
|
+
return {
|
|
221
|
+
version: match[1]!,
|
|
222
|
+
statusCode: Number.parseInt(match[2]!, 10),
|
|
223
|
+
statusMessage: match[3] ?? "",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function responseHasNoBody(statusCode: number | undefined): boolean {
|
|
228
|
+
if (statusCode === undefined) return false;
|
|
229
|
+
return (statusCode >= 100 && statusCode < 200) || statusCode === 204 || statusCode === 304;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseChunkedBody(buffer: Buffer): ChunkedParseResult {
|
|
233
|
+
let offset = 0;
|
|
234
|
+
const chunks: Buffer[] = [];
|
|
235
|
+
while (true) {
|
|
236
|
+
const lineEnd = buffer.indexOf(CRLF, offset);
|
|
237
|
+
if (lineEnd < 0) return { complete: false, consumed: 0, body: Buffer.alloc(0) };
|
|
238
|
+
const sizeLine = buffer.subarray(offset, lineEnd).toString("latin1");
|
|
239
|
+
const semicolon = sizeLine.indexOf(";");
|
|
240
|
+
const sizeText = (semicolon >= 0 ? sizeLine.slice(0, semicolon) : sizeLine).trim();
|
|
241
|
+
const size = Number.parseInt(sizeText, 16);
|
|
242
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
243
|
+
throw new Error(`Invalid chunk size: ${sizeLine}`);
|
|
244
|
+
}
|
|
245
|
+
const dataStart = lineEnd + CRLF.length;
|
|
246
|
+
if (size === 0) {
|
|
247
|
+
if (buffer.length < dataStart + CRLF.length) return { complete: false, consumed: 0, body: Buffer.alloc(0) };
|
|
248
|
+
if (buffer.subarray(dataStart, dataStart + CRLF.length).equals(CRLF)) {
|
|
249
|
+
return { complete: true, consumed: dataStart + CRLF.length, body: Buffer.concat(chunks) };
|
|
250
|
+
}
|
|
251
|
+
const trailerEnd = buffer.indexOf(DOUBLE_CRLF, dataStart);
|
|
252
|
+
if (trailerEnd < 0) return { complete: false, consumed: 0, body: Buffer.alloc(0) };
|
|
253
|
+
return { complete: true, consumed: trailerEnd + DOUBLE_CRLF.length, body: Buffer.concat(chunks) };
|
|
254
|
+
}
|
|
255
|
+
const chunkEnd = dataStart + size;
|
|
256
|
+
if (buffer.length < chunkEnd + CRLF.length) return { complete: false, consumed: 0, body: Buffer.alloc(0) };
|
|
257
|
+
chunks.push(buffer.subarray(dataStart, chunkEnd));
|
|
258
|
+
offset = chunkEnd + CRLF.length;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
class HttpMessageParser {
|
|
263
|
+
readonly #kind: "request" | "response";
|
|
264
|
+
#buffer = Buffer.alloc(0);
|
|
265
|
+
#state: ParserState | null = null;
|
|
266
|
+
|
|
267
|
+
constructor(kind: "request" | "response") {
|
|
268
|
+
this.#kind = kind;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
push(chunk: Buffer): ParsedHttpMessage[] {
|
|
272
|
+
if (chunk.length > 0) {
|
|
273
|
+
const data = Buffer.from(chunk);
|
|
274
|
+
this.#buffer = this.#buffer.length === 0 ? data : Buffer.concat([this.#buffer, data]);
|
|
275
|
+
}
|
|
276
|
+
return this.#drain(false);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
finish(): ParsedHttpMessage[] {
|
|
280
|
+
return this.#drain(true);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#bodyMode(head: Omit<ParsedHttpMessage, "body">): BodyMode {
|
|
284
|
+
if (this.#kind === "response" && responseHasNoBody(head.statusCode)) return { kind: "none" };
|
|
285
|
+
if (hasChunkedTransfer(head.headers)) return { kind: "chunked" };
|
|
286
|
+
const length = contentLength(head.headers);
|
|
287
|
+
if (length > 0) return { kind: "fixed", length };
|
|
288
|
+
if (this.#kind === "response") return { kind: "until-end" };
|
|
289
|
+
return { kind: "none" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#parseHead(): ParserState | null {
|
|
293
|
+
const headerEnd = this.#buffer.indexOf(DOUBLE_CRLF);
|
|
294
|
+
if (headerEnd < 0) return null;
|
|
295
|
+
const headText = this.#buffer.subarray(0, headerEnd).toString("latin1");
|
|
296
|
+
const { startLine, headers } = parseHeaders(headText);
|
|
297
|
+
this.#buffer = this.#buffer.subarray(headerEnd + DOUBLE_CRLF.length);
|
|
298
|
+
if (this.#kind === "request") {
|
|
299
|
+
const request = parseRequestStartLine(startLine);
|
|
300
|
+
const head = { startLine, headers, ...request };
|
|
301
|
+
return { head, bodyMode: this.#bodyMode(head) };
|
|
302
|
+
}
|
|
303
|
+
const response = parseResponseStartLine(startLine);
|
|
304
|
+
const head = { startLine, headers, ...response };
|
|
305
|
+
return { head, bodyMode: this.#bodyMode(head) };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#drain(final: boolean): ParsedHttpMessage[] {
|
|
309
|
+
const messages: ParsedHttpMessage[] = [];
|
|
310
|
+
while (true) {
|
|
311
|
+
if (!this.#state) {
|
|
312
|
+
const state = this.#parseHead();
|
|
313
|
+
if (!state) break;
|
|
314
|
+
this.#state = state;
|
|
315
|
+
}
|
|
316
|
+
const state = this.#state;
|
|
317
|
+
if (state.bodyMode.kind === "none") {
|
|
318
|
+
messages.push({ ...state.head, body: Buffer.alloc(0) });
|
|
319
|
+
this.#state = null;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (state.bodyMode.kind === "fixed") {
|
|
323
|
+
if (this.#buffer.length < state.bodyMode.length) break;
|
|
324
|
+
const body = this.#buffer.subarray(0, state.bodyMode.length);
|
|
325
|
+
this.#buffer = this.#buffer.subarray(state.bodyMode.length);
|
|
326
|
+
messages.push({ ...state.head, body });
|
|
327
|
+
this.#state = null;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (state.bodyMode.kind === "chunked") {
|
|
331
|
+
const result = parseChunkedBody(this.#buffer);
|
|
332
|
+
if (!result.complete) break;
|
|
333
|
+
this.#buffer = this.#buffer.subarray(result.consumed);
|
|
334
|
+
messages.push({ ...state.head, body: result.body });
|
|
335
|
+
this.#state = null;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!final) break;
|
|
339
|
+
const body = this.#buffer;
|
|
340
|
+
this.#buffer = Buffer.alloc(0);
|
|
341
|
+
messages.push({ ...state.head, body });
|
|
342
|
+
this.#state = null;
|
|
343
|
+
}
|
|
344
|
+
return messages;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseConnectTarget(raw: string): ConnectTarget | null {
|
|
349
|
+
if (!raw) return null;
|
|
350
|
+
if (raw.startsWith("[")) {
|
|
351
|
+
const end = raw.indexOf("]");
|
|
352
|
+
if (end < 0) return null;
|
|
353
|
+
const host = raw.slice(1, end);
|
|
354
|
+
const portText = raw.startsWith(":", end + 1) ? raw.slice(end + 2) : "443";
|
|
355
|
+
const port = Number.parseInt(portText, 10);
|
|
356
|
+
if (!Number.isSafeInteger(port) || port <= 0 || port > 65535) return null;
|
|
357
|
+
return { host, port, display: `[${host}]:${port}` };
|
|
358
|
+
}
|
|
359
|
+
const colon = raw.lastIndexOf(":");
|
|
360
|
+
const host = colon >= 0 ? raw.slice(0, colon) : raw;
|
|
361
|
+
const portText = colon >= 0 ? raw.slice(colon + 1) : "443";
|
|
362
|
+
const port = Number.parseInt(portText, 10);
|
|
363
|
+
if (!host || !Number.isSafeInteger(port) || port <= 0 || port > 65535) return null;
|
|
364
|
+
return { host, port, display: `${host}:${port}` };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function pathNameFromRequestTarget(requestTarget: string): string {
|
|
368
|
+
if (requestTarget.startsWith("http://") || requestTarget.startsWith("https://")) {
|
|
369
|
+
try {
|
|
370
|
+
return new URL(requestTarget).pathname;
|
|
371
|
+
} catch {
|
|
372
|
+
return requestTarget;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const query = requestTarget.indexOf("?");
|
|
376
|
+
return query >= 0 ? requestTarget.slice(0, query) : requestTarget;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function isMessagesRequest(message: ParsedHttpMessage): boolean {
|
|
380
|
+
return pathNameFromRequestTarget(message.path ?? "") === "/v1/messages";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function decodeBody(headers: readonly HeaderEntry[], body: Buffer): string {
|
|
384
|
+
const encoding = headerValue(headers, "content-encoding")?.toLowerCase().trim();
|
|
385
|
+
try {
|
|
386
|
+
if (encoding === "gzip") return zlib.gunzipSync(body).toString("utf8");
|
|
387
|
+
if (encoding === "br") return zlib.brotliDecompressSync(body).toString("utf8");
|
|
388
|
+
if (encoding === "deflate") return zlib.inflateSync(body).toString("utf8");
|
|
389
|
+
} catch {
|
|
390
|
+
return TEXT_DECODER.decode(body);
|
|
391
|
+
}
|
|
392
|
+
return TEXT_DECODER.decode(body);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function toCapturedRequest(message: ParsedHttpMessage): CapturedRequest {
|
|
396
|
+
return {
|
|
397
|
+
method: message.method ?? "",
|
|
398
|
+
path: message.path ?? "",
|
|
399
|
+
version: message.version,
|
|
400
|
+
headers: message.headers,
|
|
401
|
+
body: decodeBody(message.headers, message.body),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function toCapturedResponse(message: ParsedHttpMessage): CapturedResponse {
|
|
406
|
+
return {
|
|
407
|
+
statusCode: message.statusCode,
|
|
408
|
+
statusMessage: message.statusMessage ?? "",
|
|
409
|
+
version: message.version,
|
|
410
|
+
headers: message.headers,
|
|
411
|
+
body: decodeBody(message.headers, message.body),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function formatHeaders(headers: readonly HeaderEntry[]): string {
|
|
416
|
+
return headers.map(header => `${header.name}: ${header.value}`).join("\n");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function formatCapturedMessagesExchange(exchange: CapturedMessagesExchange): string {
|
|
420
|
+
const requestHeaders = formatHeaders(exchange.request.headers);
|
|
421
|
+
const responseHeaders = formatHeaders(exchange.response.headers);
|
|
422
|
+
const responseLine =
|
|
423
|
+
`${exchange.response.version || "HTTP"} ${exchange.response.statusCode ?? ""} ${exchange.response.statusMessage}`.trim();
|
|
424
|
+
return [
|
|
425
|
+
`# /v1/messages capture (${exchange.target})`,
|
|
426
|
+
"",
|
|
427
|
+
"## Request",
|
|
428
|
+
`${exchange.request.method} ${exchange.request.path} ${exchange.request.version}`.trim(),
|
|
429
|
+
"",
|
|
430
|
+
"### Headers",
|
|
431
|
+
requestHeaders,
|
|
432
|
+
"",
|
|
433
|
+
"### Body",
|
|
434
|
+
exchange.request.body,
|
|
435
|
+
"",
|
|
436
|
+
"## Response",
|
|
437
|
+
responseLine,
|
|
438
|
+
"",
|
|
439
|
+
"### Headers",
|
|
440
|
+
responseHeaders,
|
|
441
|
+
"",
|
|
442
|
+
"### Body",
|
|
443
|
+
exchange.response.body,
|
|
444
|
+
"",
|
|
445
|
+
].join("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export class ClaudeMessagesProxy {
|
|
449
|
+
readonly #host: string;
|
|
450
|
+
readonly #requestedPort: number;
|
|
451
|
+
readonly #upstreamTlsRejectUnauthorized: boolean;
|
|
452
|
+
#server: net.Server | null = null;
|
|
453
|
+
#sockets = new Set<net.Socket | tls.TLSSocket>();
|
|
454
|
+
#completed: CapturedMessagesExchange[] = [];
|
|
455
|
+
#waiters: CaptureWaiter[] = [];
|
|
456
|
+
#stopped = false;
|
|
457
|
+
#port = 0;
|
|
458
|
+
|
|
459
|
+
constructor(options: ClaudeMessagesProxyOptions = {}) {
|
|
460
|
+
this.#host = options.host ?? DEFAULT_PROXY_HOST;
|
|
461
|
+
this.#requestedPort = options.port ?? DEFAULT_PROXY_PORT;
|
|
462
|
+
this.#upstreamTlsRejectUnauthorized = options.upstreamTlsRejectUnauthorized ?? true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
get host(): string {
|
|
466
|
+
return this.#host;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
get port(): number {
|
|
470
|
+
return this.#port;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
get url(): string {
|
|
474
|
+
return `http://${this.#host}:${this.#port}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async start(): Promise<void> {
|
|
478
|
+
if (this.#server) return;
|
|
479
|
+
const server = net.createServer(socket => this.#handleConnection(socket));
|
|
480
|
+
this.#server = server;
|
|
481
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
482
|
+
const onError = (error: Error) => reject(error);
|
|
483
|
+
server.once("error", onError);
|
|
484
|
+
server.listen(this.#requestedPort, this.#host, () => {
|
|
485
|
+
server.off("error", onError);
|
|
486
|
+
const address = server.address();
|
|
487
|
+
if (!address || typeof address === "string") {
|
|
488
|
+
reject(new Error("Proxy did not bind to a TCP address"));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this.#port = address.port;
|
|
492
|
+
resolve();
|
|
493
|
+
});
|
|
494
|
+
await promise;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async stop(): Promise<void> {
|
|
498
|
+
this.#stopped = true;
|
|
499
|
+
for (const waiter of this.#waiters.splice(0)) {
|
|
500
|
+
clearTimeout(waiter.timer);
|
|
501
|
+
waiter.reject(new Error("Proxy stopped before a /v1/messages response completed"));
|
|
502
|
+
}
|
|
503
|
+
for (const socket of this.#sockets) {
|
|
504
|
+
socket.destroy();
|
|
505
|
+
}
|
|
506
|
+
this.#sockets.clear();
|
|
507
|
+
const server = this.#server;
|
|
508
|
+
this.#server = null;
|
|
509
|
+
if (!server) return;
|
|
510
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
511
|
+
server.close(error => {
|
|
512
|
+
if (error) reject(error);
|
|
513
|
+
else resolve();
|
|
514
|
+
});
|
|
515
|
+
await promise;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
waitForCapture(timeoutMs: number): Promise<CapturedMessagesExchange> {
|
|
519
|
+
const existing = this.#completed.shift();
|
|
520
|
+
if (existing) return Promise.resolve(existing);
|
|
521
|
+
if (this.#stopped) return Promise.reject(new Error("Proxy is stopped"));
|
|
522
|
+
const { promise, resolve, reject } = Promise.withResolvers<CapturedMessagesExchange>();
|
|
523
|
+
const timer = setTimeout(() => {
|
|
524
|
+
const index = this.#waiters.findIndex(waiter => waiter.resolve === resolve);
|
|
525
|
+
if (index >= 0) this.#waiters.splice(index, 1);
|
|
526
|
+
reject(new Error("Timed out waiting for a completed /v1/messages response"));
|
|
527
|
+
}, timeoutMs);
|
|
528
|
+
this.#waiters.push({ resolve, reject, timer });
|
|
529
|
+
return promise;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#complete(exchange: CapturedMessagesExchange): void {
|
|
533
|
+
const waiter = this.#waiters.shift();
|
|
534
|
+
if (waiter) {
|
|
535
|
+
clearTimeout(waiter.timer);
|
|
536
|
+
waiter.resolve(exchange);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.#completed.push(exchange);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#track<T extends net.Socket | tls.TLSSocket>(socket: T): T {
|
|
543
|
+
this.#sockets.add(socket);
|
|
544
|
+
socket.once("close", () => this.#sockets.delete(socket));
|
|
545
|
+
return socket;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#handleConnection(socket: net.Socket): void {
|
|
549
|
+
this.#track(socket);
|
|
550
|
+
let buffer = Buffer.alloc(0);
|
|
551
|
+
const onData = (chunk: Buffer) => {
|
|
552
|
+
const data = Buffer.from(chunk);
|
|
553
|
+
buffer = buffer.length === 0 ? data : Buffer.concat([buffer, data]);
|
|
554
|
+
const headerEnd = buffer.indexOf(DOUBLE_CRLF);
|
|
555
|
+
if (headerEnd < 0) return;
|
|
556
|
+
socket.off("data", onData);
|
|
557
|
+
const head = buffer.subarray(0, headerEnd).toString("latin1");
|
|
558
|
+
const rest = buffer.subarray(headerEnd + DOUBLE_CRLF.length);
|
|
559
|
+
this.#handleProxyRequest(socket, head, rest);
|
|
560
|
+
};
|
|
561
|
+
socket.on("data", onData);
|
|
562
|
+
socket.on("error", () => socket.destroy());
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#handleProxyRequest(socket: net.Socket, head: string, rest: Buffer): void {
|
|
566
|
+
const firstLine = head.split("\r\n", 1)[0] ?? "";
|
|
567
|
+
const parts = firstLine.split(/\s+/u);
|
|
568
|
+
if (parts[0] !== "CONNECT") {
|
|
569
|
+
socket.end("HTTP/1.1 501 Not Implemented\r\nConnection: close\r\n\r\n");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const target = parseConnectTarget(parts[1] ?? "");
|
|
573
|
+
if (!target) {
|
|
574
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n", () => this.#openMitmTunnel(socket, target, rest));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#openMitmTunnel(socket: net.Socket, target: ConnectTarget, rest: Buffer): void {
|
|
581
|
+
void this.#openMitmTunnelAsync(socket, target, rest).catch(() => socket.destroy());
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async #openMitmTunnelAsync(socket: net.Socket, target: ConnectTarget, rest: Buffer): Promise<void> {
|
|
585
|
+
const clientReady = Promise.withResolvers<tls.TLSSocket>();
|
|
586
|
+
const tlsServer = tls.createServer(
|
|
587
|
+
{ cert: CLAUDE_TRACE_DEBUG_CERT, key: CLAUDE_TRACE_DEBUG_KEY, ALPNProtocols: ["http/1.1"] },
|
|
588
|
+
clientTls => {
|
|
589
|
+
this.#track(clientTls);
|
|
590
|
+
clientReady.resolve(clientTls);
|
|
591
|
+
},
|
|
592
|
+
);
|
|
593
|
+
tlsServer.once("error", error => clientReady.reject(error));
|
|
594
|
+
const listening = Promise.withResolvers<void>();
|
|
595
|
+
tlsServer.listen(0, DEFAULT_PROXY_HOST, () => listening.resolve());
|
|
596
|
+
await listening.promise;
|
|
597
|
+
const address = tlsServer.address();
|
|
598
|
+
if (!address || typeof address === "string") {
|
|
599
|
+
tlsServer.close();
|
|
600
|
+
throw new Error("Internal TLS bridge did not bind to a TCP address");
|
|
601
|
+
}
|
|
602
|
+
const bridge = this.#track(net.connect({ host: DEFAULT_PROXY_HOST, port: address.port }));
|
|
603
|
+
const connected = Promise.withResolvers<void>();
|
|
604
|
+
bridge.once("connect", () => connected.resolve());
|
|
605
|
+
bridge.once("error", error => connected.reject(error));
|
|
606
|
+
await connected.promise;
|
|
607
|
+
socket.pipe(bridge);
|
|
608
|
+
bridge.pipe(socket);
|
|
609
|
+
const closeInternalServer = () => tlsServer.close();
|
|
610
|
+
socket.once("close", closeInternalServer);
|
|
611
|
+
bridge.once("close", closeInternalServer);
|
|
612
|
+
if (rest.length > 0) bridge.write(rest);
|
|
613
|
+
const clientTls = await clientReady.promise;
|
|
614
|
+
const upstreamTls = this.#track(
|
|
615
|
+
tls.connect({
|
|
616
|
+
host: target.host,
|
|
617
|
+
port: target.port,
|
|
618
|
+
servername: net.isIP(target.host) ? undefined : target.host,
|
|
619
|
+
rejectUnauthorized: this.#upstreamTlsRejectUnauthorized,
|
|
620
|
+
ALPNProtocols: ["http/1.1"],
|
|
621
|
+
}),
|
|
622
|
+
);
|
|
623
|
+
const requestParser = new HttpMessageParser("request");
|
|
624
|
+
const responseParser = new HttpMessageParser("response");
|
|
625
|
+
const responseQueue: Array<PendingCapturedRequest | null> = [];
|
|
626
|
+
const flushResponses = (messages: ParsedHttpMessage[]) => {
|
|
627
|
+
for (const message of messages) {
|
|
628
|
+
const pending = responseQueue.shift();
|
|
629
|
+
if (!pending) continue;
|
|
630
|
+
this.#complete({ ...pending, response: toCapturedResponse(message) });
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
clientTls.on("data", chunk => {
|
|
634
|
+
if (!Buffer.isBuffer(chunk)) return;
|
|
635
|
+
const data = Buffer.from(chunk);
|
|
636
|
+
upstreamTls.write(data);
|
|
637
|
+
const messages = requestParser.push(data);
|
|
638
|
+
for (const message of messages) {
|
|
639
|
+
if (!isMessagesRequest(message)) {
|
|
640
|
+
responseQueue.push(null);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
responseQueue.push({ target: target.display, request: toCapturedRequest(message) });
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
upstreamTls.on("data", chunk => {
|
|
647
|
+
if (!Buffer.isBuffer(chunk)) return;
|
|
648
|
+
const data = Buffer.from(chunk);
|
|
649
|
+
clientTls.write(data);
|
|
650
|
+
flushResponses(responseParser.push(data));
|
|
651
|
+
});
|
|
652
|
+
clientTls.on("end", () => {
|
|
653
|
+
try {
|
|
654
|
+
requestParser.finish();
|
|
655
|
+
} catch {}
|
|
656
|
+
upstreamTls.end();
|
|
657
|
+
});
|
|
658
|
+
upstreamTls.on("end", () => {
|
|
659
|
+
try {
|
|
660
|
+
flushResponses(responseParser.finish());
|
|
661
|
+
} catch {}
|
|
662
|
+
clientTls.end();
|
|
663
|
+
});
|
|
664
|
+
clientTls.on("error", () => upstreamTls.destroy());
|
|
665
|
+
upstreamTls.on("error", () => clientTls.destroy());
|
|
666
|
+
clientTls.once("close", closeInternalServer);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function errorMessage(error: unknown): string {
|
|
670
|
+
return error instanceof Error ? error.message : String(error);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function shutdownPty(session: PtySession, runPromise: Promise<unknown>): Promise<void> {
|
|
674
|
+
try {
|
|
675
|
+
session.write("\x03");
|
|
676
|
+
} catch {}
|
|
677
|
+
await Bun.sleep(100);
|
|
678
|
+
try {
|
|
679
|
+
session.kill();
|
|
680
|
+
} catch {}
|
|
681
|
+
try {
|
|
682
|
+
await runPromise;
|
|
683
|
+
} catch {}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export async function runClaudeMessagesCapture(args: ClaudeTraceCommandArgs = {}): Promise<CapturedMessagesExchange> {
|
|
687
|
+
const proxy = new ClaudeMessagesProxy({
|
|
688
|
+
host: args.host ?? DEFAULT_PROXY_HOST,
|
|
689
|
+
port: args.port ?? DEFAULT_PROXY_PORT,
|
|
690
|
+
upstreamTlsRejectUnauthorized: args.upstreamTlsRejectUnauthorized,
|
|
691
|
+
});
|
|
692
|
+
await proxy.start();
|
|
693
|
+
const session = new PtySession();
|
|
694
|
+
const terminal = new XtermTerminal({
|
|
695
|
+
cols: DEFAULT_COLS,
|
|
696
|
+
rows: DEFAULT_ROWS,
|
|
697
|
+
disableStdin: true,
|
|
698
|
+
allowProposedApi: true,
|
|
699
|
+
scrollback: 10_000,
|
|
700
|
+
});
|
|
701
|
+
terminal.onData(data => {
|
|
702
|
+
try {
|
|
703
|
+
session.write(data);
|
|
704
|
+
} catch {}
|
|
705
|
+
});
|
|
706
|
+
const command = args.command ?? DEFAULT_COMMAND;
|
|
707
|
+
const timeoutMs = args.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
708
|
+
const message = args.message ?? DEFAULT_MESSAGE;
|
|
709
|
+
const cwd = path.resolve(args.cwd ?? process.cwd());
|
|
710
|
+
const env = {
|
|
711
|
+
HTTPS_PROXY: proxy.url,
|
|
712
|
+
HTTP_PROXY: proxy.url,
|
|
713
|
+
NODE_TLS_REJECT_UNAUTHORIZED: "0",
|
|
714
|
+
TERM: "xterm-256color",
|
|
715
|
+
};
|
|
716
|
+
let ptyOutput = "";
|
|
717
|
+
const runPromise = session.start(
|
|
718
|
+
{
|
|
719
|
+
command,
|
|
720
|
+
cwd,
|
|
721
|
+
timeoutMs,
|
|
722
|
+
env,
|
|
723
|
+
cols: DEFAULT_COLS,
|
|
724
|
+
rows: DEFAULT_ROWS,
|
|
725
|
+
},
|
|
726
|
+
(error, chunk) => {
|
|
727
|
+
if (error || !chunk) return;
|
|
728
|
+
ptyOutput += chunk;
|
|
729
|
+
if (ptyOutput.length > 20_000) ptyOutput = ptyOutput.slice(-20_000);
|
|
730
|
+
terminal.write(chunk);
|
|
731
|
+
},
|
|
732
|
+
);
|
|
733
|
+
try {
|
|
734
|
+
const outputSuffix = () => (ptyOutput.trim() ? `\n\nClaude output:\n${ptyOutput}` : "");
|
|
735
|
+
void (async () => {
|
|
736
|
+
await Bun.sleep(args.inputDelayMs ?? DEFAULT_INPUT_DELAY_MS);
|
|
737
|
+
try {
|
|
738
|
+
session.write(`${message}\r`);
|
|
739
|
+
} catch (error) {
|
|
740
|
+
ptyOutput += `\n[omp input write failed: ${errorMessage(error)}]\n`;
|
|
741
|
+
}
|
|
742
|
+
})();
|
|
743
|
+
const captureRace = proxy.waitForCapture(timeoutMs).then(
|
|
744
|
+
exchange => ({ kind: "capture" as const, exchange }),
|
|
745
|
+
error => ({ kind: "capture-error" as const, error }),
|
|
746
|
+
);
|
|
747
|
+
const ptyRace = runPromise.then(
|
|
748
|
+
() => ({ kind: "pty-exit" as const }),
|
|
749
|
+
error => ({ kind: "pty-error" as const, error }),
|
|
750
|
+
);
|
|
751
|
+
const first = await Promise.race([captureRace, ptyRace]);
|
|
752
|
+
if (first.kind === "capture") {
|
|
753
|
+
await shutdownPty(session, runPromise);
|
|
754
|
+
return first.exchange;
|
|
755
|
+
}
|
|
756
|
+
if (first.kind === "capture-error") {
|
|
757
|
+
throw new Error(`${errorMessage(first.error)}${outputSuffix()}`);
|
|
758
|
+
}
|
|
759
|
+
const late = await Promise.race([captureRace, Bun.sleep(250).then(() => ({ kind: "late-timeout" as const }))]);
|
|
760
|
+
if (late.kind === "capture") {
|
|
761
|
+
await shutdownPty(session, runPromise);
|
|
762
|
+
return late.exchange;
|
|
763
|
+
}
|
|
764
|
+
if (first.kind === "pty-error") {
|
|
765
|
+
throw new Error(
|
|
766
|
+
`Claude command failed before /v1/messages completed: ${errorMessage(first.error)}${outputSuffix()}`,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
throw new Error(`Claude command exited before /v1/messages completed${outputSuffix()}`);
|
|
770
|
+
} finally {
|
|
771
|
+
terminal.dispose();
|
|
772
|
+
await proxy.stop();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export async function runClaudeTraceCommand(args: ClaudeTraceCommandArgs = {}): Promise<void> {
|
|
777
|
+
process.stderr.write(
|
|
778
|
+
`Starting Claude trace proxy on ${args.host ?? DEFAULT_PROXY_HOST}:${args.port ?? DEFAULT_PROXY_PORT}\n`,
|
|
779
|
+
);
|
|
780
|
+
const exchange = await runClaudeMessagesCapture(args);
|
|
781
|
+
const output = args.json ? `${JSON.stringify(exchange, null, 2)}\n` : formatCapturedMessagesExchange(exchange);
|
|
782
|
+
process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
|
|
783
|
+
}
|