@oh-my-pi/pi-utils 18.12.5 → 18.13.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 +23 -0
- package/dist/types/acp/connection.d.ts +18 -1
- package/dist/types/acp/transport.d.ts +26 -2
- package/dist/types/fs-open.d.ts +2 -1
- package/dist/types/marked/inline-closers.d.ts +18 -5
- package/dist/types/mime.d.ts +7 -0
- package/dist/types/process-name.d.ts +2 -1
- package/dist/types/which.d.ts +13 -0
- package/package.json +2 -2
- package/src/acp/connection.ts +38 -6
- package/src/acp/transport.ts +101 -4
- package/src/fs-open.ts +2 -1
- package/src/marked/core.ts +22 -17
- package/src/marked/inline-closers.ts +37 -42
- package/src/mime.ts +35 -0
- package/src/process-name.ts +10 -5
- package/src/stream.ts +16 -9
- package/src/which.ts +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.13.0] - 2026-10-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `parseHeifImageMimeType()`, which identifies HEIC, HEIF and AVIF images from a file header ([#16612](https://github.com/can1357/oh-my-pi/pull/16612) by [@will-bogusz](https://github.com/will-bogusz))
|
|
10
|
+
- Added `$whichAll()`, which returns every PATH match for a command in PATH order ([#16824](https://github.com/can1357/oh-my-pi/issues/16824))
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Faster lenient JSONL session parsing when recovering past malformed lines ([#16995](https://github.com/can1357/oh-my-pi/pull/16995) by [@GTanger](https://github.com/GTanger))
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `setProcessName()` no longer assigns `process.title` on Windows, where it overwrites the shared console title ([#16842](https://github.com/can1357/oh-my-pi/issues/16842))
|
|
19
|
+
- `setProcessName()` no longer assigns `process.title` on Windows, where it overwrites the shared console title ([#16842](https://github.com/can1357/oh-my-pi/issues/16842))
|
|
20
|
+
- Fixed inline code spans rendering flush against the surrounding text when a backslash preceded the closing backtick ([#16972](https://github.com/can1357/oh-my-pi/pull/16972) by [@akainth015](https://github.com/akainth015))
|
|
21
|
+
|
|
22
|
+
## [18.12.6] - 2026-10-21
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Added `RpcConnection.requestCancellable()` and `Client.requestPermission(params, { signal })`; client connections abort a handler and answer `-32800` when the peer sends `$/cancel_request`, while agent connections keep running the request and return its real result ([#15885](https://github.com/can1357/oh-my-pi/pull/15885) by [@marcintustin](https://github.com/marcintustin))
|
|
27
|
+
|
|
5
28
|
## [18.12.4] - 2026-10-20
|
|
6
29
|
|
|
7
30
|
### Breaking Changes
|
|
@@ -7,7 +7,15 @@ export interface CreateTerminalResponse {
|
|
|
7
7
|
}
|
|
8
8
|
/** Client callbacks handled by a client-side connection. */
|
|
9
9
|
export interface Client {
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Serve a permission elicit. `options.signal` aborts when the agent
|
|
12
|
+
* cancels the request (`$/cancel_request`) — e.g. user steering superseded
|
|
13
|
+
* the dialog — so a held handler can end instead of stranding the
|
|
14
|
+
* connection's clean-EOF drain.
|
|
15
|
+
*/
|
|
16
|
+
requestPermission(params: RequestPermissionRequest, options?: {
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}): MaybePromise<RequestPermissionResponse>;
|
|
11
19
|
sessionUpdate(params: SessionNotification): MaybePromise<void>;
|
|
12
20
|
readTextFile?(params: ReadTextFileRequest): MaybePromise<ReadTextFileResponse>;
|
|
13
21
|
writeTextFile?(params: WriteTextFileRequest): MaybePromise<WriteTextFileResponse | void>;
|
|
@@ -45,6 +53,15 @@ export declare class AgentSideConnection {
|
|
|
45
53
|
sessionUpdate(params: SessionNotification): Promise<void>;
|
|
46
54
|
/** Requests permission from the client. */
|
|
47
55
|
requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse>;
|
|
56
|
+
/**
|
|
57
|
+
* Requests permission from the client, returning a handle that cancels the
|
|
58
|
+
* pending wire request (`$/cancel_request`) — used when user steering
|
|
59
|
+
* supersedes an open permission dialog instead of waiting behind it.
|
|
60
|
+
*/
|
|
61
|
+
requestPermissionWithCancel(params: RequestPermissionRequest): {
|
|
62
|
+
promise: Promise<RequestPermissionResponse>;
|
|
63
|
+
cancel(): void;
|
|
64
|
+
};
|
|
48
65
|
/** Reads a client-side text file. */
|
|
49
66
|
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
|
|
50
67
|
/** Writes a client-side text file. */
|
|
@@ -69,17 +69,41 @@ export declare class RequestError extends Error {
|
|
|
69
69
|
/** Converts this error into a JSON-RPC error payload. */
|
|
70
70
|
toErrorResponse(): ErrorResponse;
|
|
71
71
|
}
|
|
72
|
-
type Dispatcher = (method: string, params: unknown, notification: boolean
|
|
72
|
+
type Dispatcher = (method: string, params: unknown, notification: boolean,
|
|
73
|
+
/** Abort signal for an inbound *request*: fires when the peer cancels it via `$/cancel_request`. */
|
|
74
|
+
signal?: AbortSignal) => MaybePromise<unknown>;
|
|
75
|
+
/** Options for {@link RpcConnection}. */
|
|
76
|
+
export interface RpcConnectionOptions {
|
|
77
|
+
/**
|
|
78
|
+
* The dispatcher honors the abort signal it receives for inbound requests.
|
|
79
|
+
* Only then does a peer `$/cancel_request` abort the handler and answer the
|
|
80
|
+
* request `-32800` immediately; otherwise the handler runs to completion and
|
|
81
|
+
* its real result is the response, since answering "cancelled" while the
|
|
82
|
+
* work keeps going would hide its outcome from the peer.
|
|
83
|
+
*/
|
|
84
|
+
honorsInboundCancellation?: boolean;
|
|
85
|
+
}
|
|
73
86
|
/** Correlated bidirectional JSON-RPC connection. */
|
|
74
87
|
export declare class RpcConnection {
|
|
75
88
|
#private;
|
|
76
|
-
constructor(stream: Stream, dispatcher: Dispatcher);
|
|
89
|
+
constructor(stream: Stream, dispatcher: Dispatcher, options?: RpcConnectionOptions);
|
|
77
90
|
/** Signal aborted when the stream closes. */
|
|
78
91
|
get signal(): AbortSignal;
|
|
79
92
|
/** Promise resolved when the stream closes. */
|
|
80
93
|
get closed(): Promise<void>;
|
|
81
94
|
/** Sends a correlated JSON-RPC request. */
|
|
82
95
|
request<Response>(method: string, params?: unknown): Promise<Response>;
|
|
96
|
+
/**
|
|
97
|
+
* Sends a JSON-RPC request whose pending entry can be cancelled locally:
|
|
98
|
+
* `cancel()` rejects the promise with a request-cancelled error, notifies
|
|
99
|
+
* the peer via `$/cancel_request`, and drops the pending entry so a late
|
|
100
|
+
* response is ignored. Used for agent→client requests a mid-turn event can
|
|
101
|
+
* supersede (e.g. a permission elicit cancelled when user steering arrives).
|
|
102
|
+
*/
|
|
103
|
+
requestCancellable<Response>(method: string, params?: unknown): {
|
|
104
|
+
promise: Promise<Response>;
|
|
105
|
+
cancel(): void;
|
|
106
|
+
};
|
|
83
107
|
/** Sends a JSON-RPC notification. */
|
|
84
108
|
notify(method: string, params?: unknown): Promise<void>;
|
|
85
109
|
/** Closes the connection and rejects outstanding requests. */
|
package/dist/types/fs-open.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* `fs.openSync` with close-on-exec, matching Node's default.
|
|
3
3
|
*
|
|
4
4
|
* libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
|
|
5
|
-
* dies at `exec`. Bun's `fs.open*`
|
|
5
|
+
* dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
|
|
6
|
+
* `bun@>=1.4` pin), and descriptors it returns survive
|
|
6
7
|
* into any child spawned through a path that does not close strays itself — the
|
|
7
8
|
* bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
|
|
8
9
|
* this for every descriptor kept open past such a spawn (session transcripts,
|
|
@@ -45,15 +45,23 @@ declare class EmphasisDelimiters {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* next run of exactly `width` backticks: a run of any other length is no closer, and the one that closes is the first
|
|
49
|
+
* run of that width at or after the opener. Backslash escapes do not apply inside a code span, so a backslash just
|
|
50
|
+
* before that closing backtick is content and still closes the span.
|
|
51
51
|
*/
|
|
52
52
|
declare class CodeRuns {
|
|
53
53
|
#private;
|
|
54
54
|
constructor(src: string);
|
|
55
55
|
/** Where the run of backticks holding the backtick at `at` ends. */
|
|
56
56
|
endOf(at: number): number;
|
|
57
|
+
/**
|
|
58
|
+
* Whether a backtick string starts at `at` for a scan that walked to `at`: a string is a whole run, so it starts at
|
|
59
|
+
* the run's first backtick and nowhere else. A retry deeper inside a run is not a string, which keeps a run whose
|
|
60
|
+
* opener found no closer from opening a narrower span one backtick later. A position a tokenizer's raw resumed at,
|
|
61
|
+
* or the suffix an escape token left behind, is not a walked position — `codeOpenerWidth` admits those on its own,
|
|
62
|
+
* because it is told the scan resumed there rather than inferring it from the source text.
|
|
63
|
+
*/
|
|
64
|
+
startsStringAt(at: number): boolean;
|
|
57
65
|
/** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
|
|
58
66
|
closerAfter(from: number, width: number): number;
|
|
59
67
|
}
|
|
@@ -102,8 +110,13 @@ export declare class InlineClosers {
|
|
|
102
110
|
escapeWithin(rest: string, from: number, to: number): boolean;
|
|
103
111
|
/** Where the emphasis opened by the `width` markers at the start of `rest` closes, or -1. */
|
|
104
112
|
closeEmphasis(rest: string, marker: string, width: number): number;
|
|
105
|
-
/**
|
|
106
|
-
|
|
113
|
+
/**
|
|
114
|
+
* How many backticks start `rest`, which starts with one, or 0 where no code span opens because no backtick string
|
|
115
|
+
* starts there: the opener is a whole string, so the rest of a run whose opener found no closer is text, while the
|
|
116
|
+
* backticks left after a backslash escaped the run's first one do open a span. `resumed` says the scan resumed at
|
|
117
|
+
* `rest` after a tokenizer consumed a raw that ended here, where the backticks are a string wherever they sit.
|
|
118
|
+
*/
|
|
119
|
+
codeOpenerWidth(rest: string, resumed?: boolean): number;
|
|
107
120
|
/** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
|
|
108
121
|
closeCode(rest: string, width: number): number;
|
|
109
122
|
/**
|
package/dist/types/mime.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export type ImageMetadata = {
|
|
|
26
26
|
hasAlpha?: boolean;
|
|
27
27
|
};
|
|
28
28
|
export declare function parseImageMetadata(header: Uint8Array): ImageMetadata | null;
|
|
29
|
+
/** Still images in a HEIF (ISO BMFF) container, which the image pipeline cannot decode itself. */
|
|
30
|
+
export type HeifImageMimeType = "image/heic" | "image/heif" | "image/avif";
|
|
31
|
+
/**
|
|
32
|
+
* MIME type of a HEIC, AVIF or other HEIF image from the brands of its leading `ftyp` box:
|
|
33
|
+
* the major brand, then the compatible brands, with the generic `mif1`/`msf1` brands last.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseHeifImageMimeType(header: Uint8Array): HeifImageMimeType | null;
|
|
29
36
|
/** Why base64 image data is unusable; `undefined` when it is canonical and non-empty. */
|
|
30
37
|
export type Base64ImageDataFailure = "invalid base64 image data" | "empty image data";
|
|
31
38
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Set
|
|
2
|
+
* Set the JS `process.title` (except on Windows, see above) and — on Linux —
|
|
3
|
+
* the kernel `comm` name.
|
|
3
4
|
*
|
|
4
5
|
* Never throws: `bun:ffi` unavailability or a failed syscall degrades silently
|
|
5
6
|
* to the `process.title`-only behavior, so it is safe to call at startup.
|
package/dist/types/which.d.ts
CHANGED
|
@@ -42,4 +42,17 @@ export declare const whichFresh: typeof darwinWhich;
|
|
|
42
42
|
* @returns Filesystem path if found, else null
|
|
43
43
|
*/
|
|
44
44
|
export declare function $which(command: string, options?: WhichOptions): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Locate every match for `command` on PATH, in PATH order, uncached.
|
|
47
|
+
*
|
|
48
|
+
* Callers that probe candidates (e.g. the Python eval backend) use this so a broken
|
|
49
|
+
* first hit — a version-manager shim with no version selected, a Windows Store alias —
|
|
50
|
+
* does not hide a working binary further down PATH. On macOS the Xcode/CLT toolchain
|
|
51
|
+
* match is appended last, mirroring {@link $which}'s fallback.
|
|
52
|
+
*
|
|
53
|
+
* @param command - Binary name to resolve
|
|
54
|
+
* @param options - `PATH` (defaults to `process.env.PATH`) and `cwd` for relative entries
|
|
55
|
+
* @returns Distinct filesystem paths; empty when nothing matches
|
|
56
|
+
*/
|
|
57
|
+
export declare function $whichAll(command: string, options?: Bun.WhichOptions): string[];
|
|
45
58
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.13.0",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@oh-my-pi/pi-natives": "18.
|
|
60
|
+
"@oh-my-pi/pi-natives": "18.13.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14"
|
package/src/acp/connection.ts
CHANGED
|
@@ -47,7 +47,16 @@ export interface CreateTerminalResponse {
|
|
|
47
47
|
}
|
|
48
48
|
/** Client callbacks handled by a client-side connection. */
|
|
49
49
|
export interface Client {
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Serve a permission elicit. `options.signal` aborts when the agent
|
|
52
|
+
* cancels the request (`$/cancel_request`) — e.g. user steering superseded
|
|
53
|
+
* the dialog — so a held handler can end instead of stranding the
|
|
54
|
+
* connection's clean-EOF drain.
|
|
55
|
+
*/
|
|
56
|
+
requestPermission(
|
|
57
|
+
params: RequestPermissionRequest,
|
|
58
|
+
options?: { signal?: AbortSignal },
|
|
59
|
+
): MaybePromise<RequestPermissionResponse>;
|
|
51
60
|
sessionUpdate(params: SessionNotification): MaybePromise<void>;
|
|
52
61
|
readTextFile?(params: ReadTextFileRequest): MaybePromise<ReadTextFileResponse>;
|
|
53
62
|
writeTextFile?(params: WriteTextFileRequest): MaybePromise<WriteTextFileResponse | void>;
|
|
@@ -90,6 +99,17 @@ export class AgentSideConnection {
|
|
|
90
99
|
requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
|
91
100
|
return this.#connection.request("session/request_permission", params);
|
|
92
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Requests permission from the client, returning a handle that cancels the
|
|
104
|
+
* pending wire request (`$/cancel_request`) — used when user steering
|
|
105
|
+
* supersedes an open permission dialog instead of waiting behind it.
|
|
106
|
+
*/
|
|
107
|
+
requestPermissionWithCancel(params: RequestPermissionRequest): {
|
|
108
|
+
promise: Promise<RequestPermissionResponse>;
|
|
109
|
+
cancel(): void;
|
|
110
|
+
} {
|
|
111
|
+
return this.#connection.requestCancellable<RequestPermissionResponse>("session/request_permission", params);
|
|
112
|
+
}
|
|
93
113
|
/** Reads a client-side text file. */
|
|
94
114
|
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
|
95
115
|
return this.#connection.request("fs/read_text_file", params);
|
|
@@ -168,10 +188,18 @@ export class ClientSideConnection {
|
|
|
168
188
|
#connection: RpcConnection;
|
|
169
189
|
|
|
170
190
|
constructor(toClient: (connection: ClientSideConnection) => Client, stream: Stream) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
191
|
+
// Client handlers receive the cancel signal (`requestPermission`'s
|
|
192
|
+
// `options.signal`), so a peer `$/cancel_request` may answer `-32800`
|
|
193
|
+
// at once. The agent side keeps the default: its handlers ignore the
|
|
194
|
+
// signal, and `session/cancel` is ACP's cancellation path for prompts.
|
|
195
|
+
this.#connection = new RpcConnection(
|
|
196
|
+
stream,
|
|
197
|
+
async (method, params, notification, signal) => {
|
|
198
|
+
if (!client) throw RequestError.internalError(undefined, "Client is not initialized");
|
|
199
|
+
return dispatchClient(client, method, params, notification, signal);
|
|
200
|
+
},
|
|
201
|
+
{ honorsInboundCancellation: true },
|
|
202
|
+
);
|
|
175
203
|
const client = toClient(this);
|
|
176
204
|
}
|
|
177
205
|
/** Signal aborted when the transport closes. */
|
|
@@ -272,10 +300,11 @@ async function dispatchClient(
|
|
|
272
300
|
method: string,
|
|
273
301
|
params: unknown,
|
|
274
302
|
notification: boolean,
|
|
303
|
+
signal?: AbortSignal,
|
|
275
304
|
): Promise<unknown> {
|
|
276
305
|
switch (method) {
|
|
277
306
|
case "session/request_permission":
|
|
278
|
-
return client.requestPermission(params as RequestPermissionRequest);
|
|
307
|
+
return client.requestPermission(params as RequestPermissionRequest, { signal });
|
|
279
308
|
case "session/update":
|
|
280
309
|
return client.sessionUpdate(params as SessionNotification);
|
|
281
310
|
case "fs/read_text_file":
|
|
@@ -322,6 +351,9 @@ async function dispatchClient(
|
|
|
322
351
|
params as CompleteElicitationNotification,
|
|
323
352
|
);
|
|
324
353
|
default:
|
|
354
|
+
// The transport already aborted the matching inbound handler;
|
|
355
|
+
// like the agent side, the notification itself carries no work.
|
|
356
|
+
if (method === "$/cancel_request") return;
|
|
325
357
|
if (notification && client.extNotification)
|
|
326
358
|
return client.extNotification(method, params as Record<string, unknown>);
|
|
327
359
|
if (!notification && client.extMethod) return client.extMethod(method, params as Record<string, unknown>);
|
package/src/acp/transport.ts
CHANGED
|
@@ -105,9 +105,27 @@ function createStandardError(
|
|
|
105
105
|
return new RequestError(code, additionalMessage ? `${message}: ${additionalMessage}` : message, data);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
type Dispatcher = (
|
|
108
|
+
type Dispatcher = (
|
|
109
|
+
method: string,
|
|
110
|
+
params: unknown,
|
|
111
|
+
notification: boolean,
|
|
112
|
+
/** Abort signal for an inbound *request*: fires when the peer cancels it via `$/cancel_request`. */
|
|
113
|
+
signal?: AbortSignal,
|
|
114
|
+
) => MaybePromise<unknown>;
|
|
109
115
|
type Pending = { resolve(value: unknown): void; reject(reason: unknown): void };
|
|
110
116
|
|
|
117
|
+
/** Options for {@link RpcConnection}. */
|
|
118
|
+
export interface RpcConnectionOptions {
|
|
119
|
+
/**
|
|
120
|
+
* The dispatcher honors the abort signal it receives for inbound requests.
|
|
121
|
+
* Only then does a peer `$/cancel_request` abort the handler and answer the
|
|
122
|
+
* request `-32800` immediately; otherwise the handler runs to completion and
|
|
123
|
+
* its real result is the response, since answering "cancelled" while the
|
|
124
|
+
* work keeps going would hide its outcome from the peer.
|
|
125
|
+
*/
|
|
126
|
+
honorsInboundCancellation?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
/** Bound on clean-EOF inbound drain so `closed` cannot hang if a handler never settles. */
|
|
112
130
|
const INBOUND_DRAIN_TIMEOUT_MS = 30_000;
|
|
113
131
|
|
|
@@ -117,15 +135,18 @@ export class RpcConnection {
|
|
|
117
135
|
#pending = new Map<JsonRpcId, Pending>();
|
|
118
136
|
#inbound = new Set<Promise<void>>();
|
|
119
137
|
#openInboundIds = new Set<JsonRpcId>();
|
|
138
|
+
/** Abort controller per inbound request, fired by a peer `$/cancel_request` (opt-in). */
|
|
139
|
+
#inboundCancellers: Map<JsonRpcId, AbortController> | undefined;
|
|
120
140
|
#writable: WritableStream<AnyMessage>;
|
|
121
141
|
#writeTail: Promise<void> = Promise.resolve();
|
|
122
142
|
#abort = new AbortController();
|
|
123
143
|
#closed = Promise.withResolvers<void>();
|
|
124
144
|
#dispatcher: Dispatcher;
|
|
125
145
|
|
|
126
|
-
constructor(stream: Stream, dispatcher: Dispatcher) {
|
|
146
|
+
constructor(stream: Stream, dispatcher: Dispatcher, options?: RpcConnectionOptions) {
|
|
127
147
|
this.#writable = stream.writable;
|
|
128
148
|
this.#dispatcher = dispatcher;
|
|
149
|
+
this.#inboundCancellers = options?.honorsInboundCancellation ? new Map() : undefined;
|
|
129
150
|
void this.#read(stream.readable);
|
|
130
151
|
}
|
|
131
152
|
|
|
@@ -141,6 +162,48 @@ export class RpcConnection {
|
|
|
141
162
|
/** Sends a correlated JSON-RPC request. */
|
|
142
163
|
request<Response>(method: string, params?: unknown): Promise<Response> {
|
|
143
164
|
if (this.signal.aborted) return Promise.reject(new Error("Connection closed"));
|
|
165
|
+
return this.#beginRequest(method, params).deferred.promise as Promise<Response>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Sends a JSON-RPC request whose pending entry can be cancelled locally:
|
|
170
|
+
* `cancel()` rejects the promise with a request-cancelled error, notifies
|
|
171
|
+
* the peer via `$/cancel_request`, and drops the pending entry so a late
|
|
172
|
+
* response is ignored. Used for agent→client requests a mid-turn event can
|
|
173
|
+
* supersede (e.g. a permission elicit cancelled when user steering arrives).
|
|
174
|
+
*/
|
|
175
|
+
requestCancellable<Response>(
|
|
176
|
+
method: string,
|
|
177
|
+
params?: unknown,
|
|
178
|
+
): {
|
|
179
|
+
promise: Promise<Response>;
|
|
180
|
+
cancel(): void;
|
|
181
|
+
} {
|
|
182
|
+
if (this.signal.aborted) {
|
|
183
|
+
return {
|
|
184
|
+
promise: Promise.reject(new Error("Connection closed")),
|
|
185
|
+
cancel: () => {},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const { id, deferred } = this.#beginRequest(method, params);
|
|
189
|
+
return {
|
|
190
|
+
promise: deferred.promise as Promise<Response>,
|
|
191
|
+
cancel: () => {
|
|
192
|
+
if (!this.#pending.delete(id)) return;
|
|
193
|
+
deferred.reject(RequestError.requestCancelled({ requestId: id, method }));
|
|
194
|
+
void this.notify("$/cancel_request", { requestId: id }).catch(() => undefined);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Shared request plumbing: allocate an id, register the pending entry, write the frame. */
|
|
200
|
+
#beginRequest(
|
|
201
|
+
method: string,
|
|
202
|
+
params: unknown,
|
|
203
|
+
): {
|
|
204
|
+
id: number;
|
|
205
|
+
deferred: { promise: Promise<unknown>; resolve: (value?: unknown) => void; reject: (reason?: unknown) => void };
|
|
206
|
+
} {
|
|
144
207
|
const id = this.#nextId++;
|
|
145
208
|
const deferred = Promise.withResolvers<unknown>();
|
|
146
209
|
this.#pending.set(id, deferred);
|
|
@@ -148,7 +211,7 @@ export class RpcConnection {
|
|
|
148
211
|
this.#pending.delete(id);
|
|
149
212
|
deferred.reject(error);
|
|
150
213
|
});
|
|
151
|
-
return deferred
|
|
214
|
+
return { id, deferred };
|
|
152
215
|
}
|
|
153
216
|
|
|
154
217
|
/** Sends a JSON-RPC notification. */
|
|
@@ -235,6 +298,18 @@ export class RpcConnection {
|
|
|
235
298
|
}
|
|
236
299
|
if (!("method" in message)) return;
|
|
237
300
|
if (!("id" in message)) {
|
|
301
|
+
// ACP v1 names the cancelled request `requestId`. A cancelled
|
|
302
|
+
// inbound request must still be answered (spec: result or
|
|
303
|
+
// `-32800`): the handler is aborted and the error response is
|
|
304
|
+
// written here, so a late-settling handler lands in `#respond`'s
|
|
305
|
+
// closed-id no-op instead of answering a request the peer gave
|
|
306
|
+
// up on.
|
|
307
|
+
if (message.method === "$/cancel_request" && this.#inboundCancellers) {
|
|
308
|
+
this.#cancelInbound(
|
|
309
|
+
this.#inboundCancellers,
|
|
310
|
+
(message.params as { requestId?: JsonRpcId } | undefined)?.requestId,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
238
313
|
try {
|
|
239
314
|
await this.#dispatcher(message.method, message.params, true);
|
|
240
315
|
} catch {
|
|
@@ -242,8 +317,11 @@ export class RpcConnection {
|
|
|
242
317
|
}
|
|
243
318
|
return;
|
|
244
319
|
}
|
|
320
|
+
const cancellers = this.#inboundCancellers;
|
|
321
|
+
const canceller = cancellers ? new AbortController() : undefined;
|
|
322
|
+
if (canceller) cancellers?.set(message.id, canceller);
|
|
245
323
|
try {
|
|
246
|
-
const result = await this.#dispatcher(message.method, message.params, false);
|
|
324
|
+
const result = await this.#dispatcher(message.method, message.params, false, canceller?.signal);
|
|
247
325
|
await this.#respond(message.id, { result: result ?? {} });
|
|
248
326
|
} catch (error) {
|
|
249
327
|
const protocolError =
|
|
@@ -251,6 +329,25 @@ export class RpcConnection {
|
|
|
251
329
|
? error
|
|
252
330
|
: RequestError.internalError({ details: error instanceof Error ? error.message : String(error) });
|
|
253
331
|
await this.#respond(message.id, { error: protocolError.toErrorResponse() });
|
|
332
|
+
} finally {
|
|
333
|
+
cancellers?.delete(message.id);
|
|
254
334
|
}
|
|
255
335
|
}
|
|
336
|
+
|
|
337
|
+
#cancelInbound(cancellers: Map<JsonRpcId, AbortController>, id: JsonRpcId | undefined): void {
|
|
338
|
+
if (id === undefined) return;
|
|
339
|
+
const canceller = cancellers.get(id);
|
|
340
|
+
if (canceller) {
|
|
341
|
+
cancellers.delete(id);
|
|
342
|
+
canceller.abort(RequestError.requestCancelled({ requestId: id }));
|
|
343
|
+
}
|
|
344
|
+
// Spec: a cancelled request still gets a response. Answer `-32800`
|
|
345
|
+
// here — synchronously with the notification — so the peer is never
|
|
346
|
+
// left waiting on a handler that may never settle. `#respond` closes
|
|
347
|
+
// the id, so a late handler result (a handler that ignored its abort
|
|
348
|
+
// signal) lands in the closed-id no-op instead of answering twice.
|
|
349
|
+
void this.#respond(id, { error: RequestError.requestCancelled({ requestId: id }).toErrorResponse() }).catch(
|
|
350
|
+
() => undefined,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
256
353
|
}
|
package/src/fs-open.ts
CHANGED
|
@@ -15,7 +15,8 @@ const O_CLOEXEC = process.platform === "linux" ? 0o2000000 : process.platform ==
|
|
|
15
15
|
* `fs.openSync` with close-on-exec, matching Node's default.
|
|
16
16
|
*
|
|
17
17
|
* libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
|
|
18
|
-
* dies at `exec`. Bun's `fs.open*`
|
|
18
|
+
* dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
|
|
19
|
+
* `bun@>=1.4` pin), and descriptors it returns survive
|
|
19
20
|
* into any child spawned through a path that does not close strays itself — the
|
|
20
21
|
* bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
|
|
21
22
|
* this for every descriptor kept open past such a spawn (session transcripts,
|
package/src/marked/core.ts
CHANGED
|
@@ -655,39 +655,48 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
|
655
655
|
// The context is shared with the sources nested in this one, which move its end while they are lexed.
|
|
656
656
|
context.end = closers.end;
|
|
657
657
|
let rest = lex.rest;
|
|
658
|
+
// A token the lexer consumed ends the walk, so the backticks left where the next token starts are a string of their
|
|
659
|
+
// own even when the run they belong to began earlier: an extension, an override or an escape token may leave the
|
|
660
|
+
// scan inside a run, and a code span opens there. Only the text step's own advance leaves the scan inside a run
|
|
661
|
+
// without consuming a token, and no span opens at such a position.
|
|
662
|
+
let resumed = false;
|
|
663
|
+
/** Pushes `token` and moves past its raw, which is where the next token starts. */
|
|
664
|
+
const consumeToken = (token: Token): void => {
|
|
665
|
+
output.push(token);
|
|
666
|
+
rest = rest.slice(token.raw.length);
|
|
667
|
+
resumed = true;
|
|
668
|
+
};
|
|
658
669
|
while (rest !== "") {
|
|
670
|
+
const resumedHere = resumed;
|
|
671
|
+
resumed = false;
|
|
659
672
|
let custom: Tokens.Generic | undefined;
|
|
660
673
|
for (const extension of lexer.extensions.inline) {
|
|
661
674
|
custom = extension.tokenizer.call(context, rest, output);
|
|
662
675
|
if (custom?.raw) break;
|
|
663
676
|
}
|
|
664
677
|
if (custom?.raw) {
|
|
665
|
-
|
|
666
|
-
rest = rest.slice(custom.raw.length);
|
|
678
|
+
consumeToken(custom);
|
|
667
679
|
continue;
|
|
668
680
|
}
|
|
669
681
|
|
|
670
682
|
const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/.exec(rest);
|
|
671
683
|
if (escaped) {
|
|
672
|
-
|
|
673
|
-
rest = rest.slice(2);
|
|
684
|
+
consumeToken({ type: "escape", raw: escaped[0], text: escaped[1]! });
|
|
674
685
|
continue;
|
|
675
686
|
}
|
|
676
687
|
const br = lexer.options.breaks ? /^(?: {2,}|\\)?\n/.exec(rest) : /^(?: {2,}|\\)\n/.exec(rest);
|
|
677
688
|
if (br) {
|
|
678
|
-
|
|
679
|
-
rest = rest.slice(br[0].length);
|
|
689
|
+
consumeToken({ type: "br", raw: br[0] });
|
|
680
690
|
continue;
|
|
681
691
|
}
|
|
682
692
|
if (rest[0] === "`") {
|
|
683
|
-
const width = closers.codeOpenerWidth(rest);
|
|
693
|
+
const width = closers.codeOpenerWidth(rest, resumedHere);
|
|
684
694
|
const end = closers.closeCode(rest, width);
|
|
685
695
|
if (end !== -1) {
|
|
686
696
|
const raw = rest.slice(0, end + width);
|
|
687
697
|
let text = rest.slice(width, end).replace(/\n/g, " ");
|
|
688
698
|
if (/^ .* $/.test(text) && text.trim() !== "") text = text.slice(1, -1);
|
|
689
|
-
|
|
690
|
-
rest = rest.slice(raw.length);
|
|
699
|
+
consumeToken({ type: "codespan", raw, text });
|
|
691
700
|
continue;
|
|
692
701
|
}
|
|
693
702
|
}
|
|
@@ -695,14 +704,12 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
|
695
704
|
if (auto) {
|
|
696
705
|
const text = auto.slice(1, -1);
|
|
697
706
|
const href = text.includes("@") && !/^[a-z][a-z+.-]*:\/\//i.test(text) ? `mailto:${text}` : text;
|
|
698
|
-
|
|
699
|
-
rest = rest.slice(auto.length);
|
|
707
|
+
consumeToken({ type: "link", raw: auto, text, href, tokens: [{ type: "text", raw: text, text }] });
|
|
700
708
|
continue;
|
|
701
709
|
}
|
|
702
710
|
const html = inlineHtmlPrefix(rest, closers);
|
|
703
711
|
if (html) {
|
|
704
|
-
|
|
705
|
-
rest = rest.slice(html.length);
|
|
712
|
+
consumeToken({ type: "html", raw: html, inLink: false, inRawBlock: false, block: false, text: html });
|
|
706
713
|
continue;
|
|
707
714
|
}
|
|
708
715
|
const nested = matchLink(rest, lexer, closers) ?? matchEmphasis(rest, output, closers);
|
|
@@ -716,8 +723,7 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
|
716
723
|
if (override) del = override.call(lexer.tokenizer, rest, rest);
|
|
717
724
|
if (del === false || !override) del = lexer.tokenizer.del(rest);
|
|
718
725
|
if (del) {
|
|
719
|
-
|
|
720
|
-
rest = rest.slice(del.raw.length);
|
|
726
|
+
consumeToken(del);
|
|
721
727
|
continue;
|
|
722
728
|
}
|
|
723
729
|
}
|
|
@@ -744,8 +750,7 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
|
744
750
|
}
|
|
745
751
|
}
|
|
746
752
|
if (url) {
|
|
747
|
-
|
|
748
|
-
rest = rest.slice(url.raw.length);
|
|
753
|
+
consumeToken(url);
|
|
749
754
|
continue;
|
|
750
755
|
}
|
|
751
756
|
|
|
@@ -170,38 +170,30 @@ class EmphasisDelimiters {
|
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
173
|
+
* next run of exactly `width` backticks: a run of any other length is no closer, and the one that closes is the first
|
|
174
|
+
* run of that width at or after the opener. Backslash escapes do not apply inside a code span, so a backslash just
|
|
175
|
+
* before that closing backtick is content and still closes the span.
|
|
176
176
|
*/
|
|
177
177
|
class CodeRuns {
|
|
178
|
-
// Per run, in order: where it starts and ends
|
|
178
|
+
// Per run, in order: where it starts and ends; the starts are the binary search `endOf` and `startsStringAt` need.
|
|
179
179
|
readonly #starts: number[] = [];
|
|
180
180
|
readonly #ends: number[] = [];
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
readonly #
|
|
184
|
-
readonly #leaves: number;
|
|
181
|
+
// Run starts by width, each list ascending: a closer must have the opener's exact width, so finding one is a
|
|
182
|
+
// binary search in the list for that width and no scan of the runs between.
|
|
183
|
+
readonly #byWidth = new Map<number, number[]>();
|
|
185
184
|
|
|
186
185
|
constructor(src: string) {
|
|
187
|
-
const widths: number[] = [];
|
|
188
186
|
for (let at = src.indexOf("`"); at !== -1;) {
|
|
189
187
|
let end = at + 1;
|
|
190
188
|
while (src.charCodeAt(end) === 0x60 /* ` */) end++;
|
|
191
|
-
const escaped = escapedAt(src, at);
|
|
192
189
|
this.#starts.push(at);
|
|
193
190
|
this.#ends.push(end);
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
const width = end - at;
|
|
192
|
+
const starts = this.#byWidth.get(width);
|
|
193
|
+
if (starts === undefined) this.#byWidth.set(width, [at]);
|
|
194
|
+
else starts.push(at);
|
|
196
195
|
at = src.indexOf("`", end);
|
|
197
196
|
}
|
|
198
|
-
let leaves = 1;
|
|
199
|
-
while (leaves < widths.length) leaves <<= 1;
|
|
200
|
-
const widest = new Int32Array(2 * leaves);
|
|
201
|
-
for (let run = 0; run < widths.length; run++) widest[leaves + run] = widths[run]!;
|
|
202
|
-
for (let node = leaves - 1; node >= 1; node--) widest[node] = Math.max(widest[2 * node]!, widest[2 * node + 1]!);
|
|
203
|
-
this.#widest = widest;
|
|
204
|
-
this.#leaves = leaves;
|
|
205
197
|
}
|
|
206
198
|
|
|
207
199
|
/** Where the run of backticks holding the backtick at `at` ends. */
|
|
@@ -209,28 +201,24 @@ class CodeRuns {
|
|
|
209
201
|
return this.#ends[lowerBound(this.#starts, at + 1) - 1]!;
|
|
210
202
|
}
|
|
211
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Whether a backtick string starts at `at` for a scan that walked to `at`: a string is a whole run, so it starts at
|
|
206
|
+
* the run's first backtick and nowhere else. A retry deeper inside a run is not a string, which keeps a run whose
|
|
207
|
+
* opener found no closer from opening a narrower span one backtick later. A position a tokenizer's raw resumed at,
|
|
208
|
+
* or the suffix an escape token left behind, is not a walked position — `codeOpenerWidth` admits those on its own,
|
|
209
|
+
* because it is told the scan resumed there rather than inferring it from the source text.
|
|
210
|
+
*/
|
|
211
|
+
startsStringAt(at: number): boolean {
|
|
212
|
+
const run = lowerBound(this.#starts, at + 1) - 1;
|
|
213
|
+
return this.#starts[run] === at;
|
|
214
|
+
}
|
|
215
|
+
|
|
212
216
|
/** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
|
|
213
217
|
closerAfter(from: number, width: number): number {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
/** The first run at or after `first` that holds a closer of `width` backticks, or -1. */
|
|
219
|
-
#firstHolding(first: number, width: number): number {
|
|
220
|
-
if (first >= this.#starts.length) return -1;
|
|
221
|
-
const widest = this.#widest;
|
|
222
|
-
const leaves = this.#leaves;
|
|
223
|
-
let node = leaves + first;
|
|
224
|
-
if (widest[node]! >= width) return first;
|
|
225
|
-
// Up to the nearest subtree on the right that holds one, then down to its first run that does.
|
|
226
|
-
for (;;) {
|
|
227
|
-
if (node === 1) return -1;
|
|
228
|
-
if ((node & 1) === 0 && widest[node + 1]! >= width) break;
|
|
229
|
-
node >>= 1;
|
|
230
|
-
}
|
|
231
|
-
node++;
|
|
232
|
-
while (node < leaves) node = widest[2 * node]! >= width ? 2 * node : 2 * node + 1;
|
|
233
|
-
return node - leaves;
|
|
218
|
+
const starts = this.#byWidth.get(width);
|
|
219
|
+
if (starts === undefined) return -1;
|
|
220
|
+
const run = lowerBound(starts, from);
|
|
221
|
+
return run < starts.length ? starts[run]! : -1;
|
|
234
222
|
}
|
|
235
223
|
}
|
|
236
224
|
|
|
@@ -388,10 +376,17 @@ export class InlineClosers {
|
|
|
388
376
|
return close === -1 ? -1 : close - pos;
|
|
389
377
|
}
|
|
390
378
|
|
|
391
|
-
/**
|
|
392
|
-
|
|
379
|
+
/**
|
|
380
|
+
* How many backticks start `rest`, which starts with one, or 0 where no code span opens because no backtick string
|
|
381
|
+
* starts there: the opener is a whole string, so the rest of a run whose opener found no closer is text, while the
|
|
382
|
+
* backticks left after a backslash escaped the run's first one do open a span. `resumed` says the scan resumed at
|
|
383
|
+
* `rest` after a tokenizer consumed a raw that ended here, where the backticks are a string wherever they sit.
|
|
384
|
+
*/
|
|
385
|
+
codeOpenerWidth(rest: string, resumed = false): number {
|
|
393
386
|
const pos = this.end - rest.length;
|
|
394
|
-
|
|
387
|
+
const runs = this.#indexes.codeRuns;
|
|
388
|
+
if (!resumed && !runs.startsStringAt(pos)) return 0;
|
|
389
|
+
return Math.min(runs.endOf(pos), this.end) - pos;
|
|
395
390
|
}
|
|
396
391
|
|
|
397
392
|
/** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
|
package/src/mime.ts
CHANGED
|
@@ -11,6 +11,7 @@ const GIF89A = Buffer.from("GIF89a");
|
|
|
11
11
|
const WEBP_VP8X = Buffer.from("VP8X");
|
|
12
12
|
const WEBP_VP8L = Buffer.from("VP8L");
|
|
13
13
|
const WEBP_VP8 = Buffer.from("VP8 ");
|
|
14
|
+
const FTYP = Buffer.from("ftyp");
|
|
14
15
|
|
|
15
16
|
export const SUPPORTED_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
16
17
|
|
|
@@ -143,6 +144,40 @@ export function parseImageMetadata(header: Uint8Array): ImageMetadata | null {
|
|
|
143
144
|
);
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/** Still images in a HEIF (ISO BMFF) container, which the image pipeline cannot decode itself. */
|
|
148
|
+
export type HeifImageMimeType = "image/heic" | "image/heif" | "image/avif";
|
|
149
|
+
|
|
150
|
+
const HEIF_BRAND_MIME_TYPES: Readonly<Record<string, HeifImageMimeType>> = {
|
|
151
|
+
heic: "image/heic",
|
|
152
|
+
heix: "image/heic",
|
|
153
|
+
heim: "image/heic",
|
|
154
|
+
heis: "image/heic",
|
|
155
|
+
hevc: "image/heic",
|
|
156
|
+
hevx: "image/heic",
|
|
157
|
+
avif: "image/avif",
|
|
158
|
+
avis: "image/avif",
|
|
159
|
+
mif1: "image/heif",
|
|
160
|
+
msf1: "image/heif",
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* MIME type of a HEIC, AVIF or other HEIF image from the brands of its leading `ftyp` box:
|
|
165
|
+
* the major brand, then the compatible brands, with the generic `mif1`/`msf1` brands last.
|
|
166
|
+
*/
|
|
167
|
+
export function parseHeifImageMimeType(header: Uint8Array): HeifImageMimeType | null {
|
|
168
|
+
if (!magicEquals(header, 4, FTYP)) return null;
|
|
169
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
170
|
+
const boxEnd = Math.min(view.getUint32(0, false), header.length);
|
|
171
|
+
let generic: HeifImageMimeType | null = null;
|
|
172
|
+
// The major brand sits at 8 and the compatible brands start at 16, after the minor version.
|
|
173
|
+
for (let offset = 8; offset + 4 <= boxEnd; offset += offset === 8 ? 8 : 4) {
|
|
174
|
+
const mimeType = HEIF_BRAND_MIME_TYPES[String.fromCharCode(...header.subarray(offset, offset + 4))];
|
|
175
|
+
if (mimeType === "image/heif") generic ??= mimeType;
|
|
176
|
+
else if (mimeType) return mimeType;
|
|
177
|
+
}
|
|
178
|
+
return generic;
|
|
179
|
+
}
|
|
180
|
+
|
|
146
181
|
/** Why base64 image data is unusable; `undefined` when it is canonical and non-empty. */
|
|
147
182
|
export type Base64ImageDataFailure = "invalid base64 image data" | "empty image data";
|
|
148
183
|
|
package/src/process-name.ts
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* setter) and additionally drive `prctl` via `bun:ffi` on Linux, mirroring the
|
|
11
11
|
* libc-FFI pattern in `ttyid.ts` / `stderr-guard.ts`.
|
|
12
12
|
*
|
|
13
|
-
* macOS has no clean userspace equivalent for the shebang-run path,
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* macOS has no clean userspace equivalent for the shebang-run path, so there
|
|
14
|
+
* we only set `process.title`. Windows derives the name from the exec'd file,
|
|
15
|
+
* and there Bun's `process.title` setter is libuv's `SetConsoleTitleW`: every
|
|
16
|
+
* omp child attached to a pane's console (workers, `browser-relay`, servers)
|
|
17
|
+
* would overwrite that pane's tab title, so Windows skips it entirely.
|
|
16
18
|
*/
|
|
17
19
|
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
18
20
|
import * as os from "node:os";
|
|
@@ -21,17 +23,20 @@ import * as os from "node:os";
|
|
|
21
23
|
const PR_SET_NAME = 15;
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
* Set
|
|
26
|
+
* Set the JS `process.title` (except on Windows, see above) and — on Linux —
|
|
27
|
+
* the kernel `comm` name.
|
|
25
28
|
*
|
|
26
29
|
* Never throws: `bun:ffi` unavailability or a failed syscall degrades silently
|
|
27
30
|
* to the `process.title`-only behavior, so it is safe to call at startup.
|
|
28
31
|
*/
|
|
29
32
|
export function setProcessName(name: string): void {
|
|
33
|
+
const platform = os.platform();
|
|
34
|
+
if (platform === "win32") return;
|
|
30
35
|
try {
|
|
31
36
|
process.title = name;
|
|
32
37
|
} catch {}
|
|
33
38
|
|
|
34
|
-
if (
|
|
39
|
+
if (platform !== "linux") return;
|
|
35
40
|
|
|
36
41
|
// glibc first, then the generic soname for musl-style layouts (see stderr-guard.ts).
|
|
37
42
|
for (const soname of ["libc.so.6", "libc.so"]) {
|
package/src/stream.ts
CHANGED
|
@@ -2,6 +2,7 @@ const trailingEvents = new WeakSet<ServerSentEvent>();
|
|
|
2
2
|
|
|
3
3
|
import { abortableSource } from "./abortable";
|
|
4
4
|
import { parseStreamingJson } from "./json-parse";
|
|
5
|
+
import { isWhitespace } from "./json-lexer";
|
|
5
6
|
|
|
6
7
|
const LF = 0x0a;
|
|
7
8
|
const CR = 0x0d;
|
|
@@ -613,9 +614,11 @@ export async function* readSseEvents(
|
|
|
613
614
|
*/
|
|
614
615
|
export function parseJsonlLenient<T>(buffer: string, options: { onMalformedRecord?: () => void } = {}): T[] {
|
|
615
616
|
let entries: T[] | undefined;
|
|
617
|
+
let cursor = 0;
|
|
618
|
+
const len = buffer.length;
|
|
616
619
|
|
|
617
|
-
while (
|
|
618
|
-
const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
|
|
620
|
+
while (cursor < len) {
|
|
621
|
+
const { values, error, read, done } = Bun.JSONL.parseChunk(buffer, cursor);
|
|
619
622
|
if (values.length > 0) {
|
|
620
623
|
const ext = values as T[];
|
|
621
624
|
if (!entries) {
|
|
@@ -625,18 +628,22 @@ export function parseJsonlLenient<T>(buffer: string, options: { onMalformedRecor
|
|
|
625
628
|
}
|
|
626
629
|
}
|
|
627
630
|
if (error) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
+
let malformedStart = read;
|
|
632
|
+
while (malformedStart < len && isWhitespace(buffer.charCodeAt(malformedStart))) {
|
|
633
|
+
malformedStart++;
|
|
634
|
+
}
|
|
635
|
+
const nextNewline = buffer.indexOf("\n", malformedStart);
|
|
636
|
+
const malformedEnd = nextNewline === -1 ? len : nextNewline;
|
|
637
|
+
if (buffer.substring(malformedStart, malformedEnd).trim().length > 0) options.onMalformedRecord?.();
|
|
631
638
|
if (nextNewline === -1) break;
|
|
632
|
-
|
|
639
|
+
cursor = nextNewline + 1;
|
|
633
640
|
continue;
|
|
634
641
|
}
|
|
635
|
-
if (read ===
|
|
636
|
-
if (buffer.trim().length > 0) options.onMalformedRecord?.();
|
|
642
|
+
if (read === cursor) {
|
|
643
|
+
if (buffer.substring(cursor).trim().length > 0) options.onMalformedRecord?.();
|
|
637
644
|
break;
|
|
638
645
|
}
|
|
639
|
-
|
|
646
|
+
cursor = read;
|
|
640
647
|
if (done) break;
|
|
641
648
|
}
|
|
642
649
|
return entries ?? [];
|
package/src/which.ts
CHANGED
|
@@ -261,3 +261,30 @@ export function $which(command: string, options?: WhichOptions): string | null {
|
|
|
261
261
|
}
|
|
262
262
|
return result;
|
|
263
263
|
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Locate every match for `command` on PATH, in PATH order, uncached.
|
|
267
|
+
*
|
|
268
|
+
* Callers that probe candidates (e.g. the Python eval backend) use this so a broken
|
|
269
|
+
* first hit — a version-manager shim with no version selected, a Windows Store alias —
|
|
270
|
+
* does not hide a working binary further down PATH. On macOS the Xcode/CLT toolchain
|
|
271
|
+
* match is appended last, mirroring {@link $which}'s fallback.
|
|
272
|
+
*
|
|
273
|
+
* @param command - Binary name to resolve
|
|
274
|
+
* @param options - `PATH` (defaults to `process.env.PATH`) and `cwd` for relative entries
|
|
275
|
+
* @returns Distinct filesystem paths; empty when nothing matches
|
|
276
|
+
*/
|
|
277
|
+
export function $whichAll(command: string, options?: Bun.WhichOptions): string[] {
|
|
278
|
+
const searchPath = options?.PATH ?? process.env.PATH ?? "";
|
|
279
|
+
const matches: string[] = [];
|
|
280
|
+
for (const dir of searchPath.split(path.delimiter)) {
|
|
281
|
+
if (!dir) continue;
|
|
282
|
+
const match = Bun.which(command, { cwd: options?.cwd, PATH: dir });
|
|
283
|
+
if (match && !matches.includes(match)) matches.push(match);
|
|
284
|
+
}
|
|
285
|
+
if (os.platform() === "darwin" && isXcodeBin(command)) {
|
|
286
|
+
const toolchainMatch = getMacosToolPaths().get(command);
|
|
287
|
+
if (toolchainMatch && !matches.includes(toolchainMatch)) matches.push(toolchainMatch);
|
|
288
|
+
}
|
|
289
|
+
return matches;
|
|
290
|
+
}
|