@oh-my-pi/pi-utils 18.1.22 → 18.2.1

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 CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.1] - 2026-09-15
6
+
7
+ ### Added
8
+
9
+ - Added the public `postmortem.exitProcess()` utility for host-owned hard exits that must bypass temporary process-exit guards ([#11789](https://github.com/can1357/oh-my-pi/issues/11789)).
10
+ - Added `readSseJsonOrText`: like `readSseJson`, but a `data:` frame that is not valid JSON is yielded as its raw text instead of raising a `SyntaxError`, so a consumer can classify a reverse proxy's plain-text throttle page (`429 Too Many Requests`) that arrives after the stream headers were already sent. `readSseJson` is unchanged and shares the framing with it.
11
+
12
+ ### Fixed
13
+
14
+ - Reading an EPUB, PPTX or XLSX whose XML has a mismatched or stray end tag no longer hangs the session forever; the parser recovers and the document converts ([#12018](https://github.com/can1357/oh-my-pi/pull/12018) by [@kaluli123123](https://github.com/kaluli123123)).
15
+ - Fixed `filterChildShellEnv` forwarding the host process's `GIT_DIR`, `GIT_WORK_TREE`, and related repo-location overrides to child shells, where `git` would ignore the command's `cwd`.
16
+ - ACP JSON-RPC now drains accepted inbound requests on clean stdin EOF before resolving `closed`, so in-flight methods such as `session/new` still receive a success or explicit error response instead of being dropped on exit 0 ([#11567](https://github.com/can1357/oh-my-pi/issues/11567)).
17
+ - Fixed provider-local usage-limit reset timestamps making `waitForUsageReset` sessions resume up to eight hours late while preserving longest-window semantics for naive UTC timestamps ([#11014](https://github.com/can1357/oh-my-pi/issues/11014)).
18
+ - `registerStdioDisconnectHandling()` now drives graceful shutdown from `process.stdout`'s own `error` event, so a closed stdout consumer exits cleanly while an unrelated write EPIPE (subprocess stdin, socket) stays fatal ([#10930](https://github.com/can1357/oh-my-pi/issues/10930)).
19
+ - Fixed Linux `ptree` timeout cleanup occasionally leaving session-escaped descendants running during subreaper adoption.
20
+
21
+ ## [18.2.0] - 2026-09-15
22
+
23
+ ### Breaking Changes
24
+
25
+ - Removed the unused `globPaths`, `loadGitignorePatterns`, and `GlobPathsOptions` exports.
26
+ - Browser helpers now manage Chrome only: removed `Browser`, `BrowserTag`, `resolveBuildId()`, `getInstalledBrowsers()`, and `browser` options/metadata; `getDownloadUrl()` now takes `(platform, buildId, baseUrl?)`.
27
+
28
+ ### Added
29
+
30
+ - Added `relativePathWithinNormalizedRoot()` for reusing canonical paths across containment checks.
31
+ - Added `sleepLong()` and `MAX_TIMER_DELAY_MS`: an abortable sleep that chunks delays past the signed 32-bit timer ceiling so day-scale provider waits elapse instead of overflowing the timer.
32
+
33
+ ### Changed
34
+
35
+ - Log-retention cleanup now runs after logger construction without keeping short-lived commands alive.
36
+ - Debugger support now loads only when SIGUSR1 requests it.
37
+
38
+ ### Fixed
39
+
40
+ - Fixed `extractRetryHint` dropping OpenCode Go's `Resets in …` quota window (`45min`, `2hr 15min`, `3 days`): the `reset in` pattern now accepts `Resets` phrasing, `hr`/`day` units, and compound `2hr 15min` remainders, so exhausted Go credentials block for the server-stated window instead of the 60s heuristic guess. ([#12091](https://github.com/can1357/oh-my-pi/pull/12091) by [@H4vC](https://github.com/H4vC))
41
+ - Async file-peek callbacks now receive stable `Uint8Array` windows with copying `slice()` semantics.
42
+ - Dotenv loading now handles multiline values and escapes consistently with Bun, preventing project values from leaking into child-shell environments.
43
+ - SSE readers now support lone-CR line endings and CRLF split across chunks without merging or delaying events.
44
+
5
45
  ## [18.1.22] - 2026-09-14
6
46
 
7
47
  ### Fixed
@@ -10843,7 +10843,7 @@ RUST RUNTIME DEPENDENCY LICENSES
10843
10843
 
10844
10844
  Generated from Cargo.lock by cargo-about 0.8.2 in locked, offline, workspace, all-feature, all-target mode, then restricted using cargo metadata to normal and build edges reachable from the workspace (development-only edges are excluded). cargo-deny independently evaluates the complete all-target graph.
10845
10845
 
10846
- Native Opus (audiopus/audiopus_sys) and PCRE2 (pcre2/pcre2-sys) are covered by the exact crate license payloads below. inferno 0.12.8 is package-scoped as CDDL-1.0: its CDDL-covered source remains available in the locked crates.io source archive at https://crates.io/crates/inferno/0.12.8 and https://github.com/jonhoo/inferno. The full locked license text follows below.
10846
+ Native Opus (opus/opusic-sys) and PCRE2 (pcre2/pcre2-sys) are covered by the exact crate license payloads below. inferno 0.12.8 is package-scoped as CDDL-1.0: its CDDL-covered source remains available in the locked crates.io source archive at https://crates.io/crates/inferno/0.12.8 and https://github.com/jonhoo/inferno. The full locked license text follows below.
10847
10847
 
10848
10848
  -------------------------------------------------------------------------------
10849
10849
  License: Apache License 2.0 (Apache-2.0)
@@ -12509,6 +12509,58 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
12509
12509
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
12510
12510
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12511
12511
 
12512
+ -------------------------------------------------------------------------------
12513
+ License: BSD 3-Clause "New" or "Revised" License (BSD-3-Clause)
12514
+ Used by:
12515
+ - opusic-sys 0.7.5 (https://github.com/DoumanAsh/opusic-sys)
12516
+
12517
+ Full license text:
12518
+
12519
+ Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
12520
+ Jean-Marc Valin, Timothy B. Terriberry,
12521
+ CSIRO, Gregory Maxwell, Mark Borgerding,
12522
+ Erik de Castro Lopo, Mozilla, Amazon
12523
+
12524
+ Redistribution and use in source and binary forms, with or without
12525
+ modification, are permitted provided that the following conditions
12526
+ are met:
12527
+
12528
+ - Redistributions of source code must retain the above copyright
12529
+ notice, this list of conditions and the following disclaimer.
12530
+
12531
+ - Redistributions in binary form must reproduce the above copyright
12532
+ notice, this list of conditions and the following disclaimer in the
12533
+ documentation and/or other materials provided with the distribution.
12534
+
12535
+ - Neither the name of Internet Society, IETF or IETF Trust, nor the
12536
+ names of specific contributors, may be used to endorse or promote
12537
+ products derived from this software without specific prior written
12538
+ permission.
12539
+
12540
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
12541
+ ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
12542
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
12543
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
12544
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
12545
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
12546
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
12547
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
12548
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
12549
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
12550
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12551
+
12552
+ Opus is subject to the royalty-free patent licenses which are
12553
+ specified at:
12554
+
12555
+ Xiph.Org Foundation:
12556
+ https://datatracker.ietf.org/ipr/1524/
12557
+
12558
+ Microsoft Corporation:
12559
+ https://datatracker.ietf.org/ipr/1914/
12560
+
12561
+ Broadcom Corporation:
12562
+ https://datatracker.ietf.org/ipr/1526/
12563
+
12512
12564
  -------------------------------------------------------------------------------
12513
12565
  License: BSD 3-Clause "New" or "Revised" License (BSD-3-Clause)
12514
12566
  Used by:
@@ -13290,29 +13342,6 @@ jurisdiction of the Federal Courts of the Northern District of
13290
13342
  California and the state courts of the State of California, with
13291
13343
  venue lying in Santa Clara County, California.
13292
13344
 
13293
- -------------------------------------------------------------------------------
13294
- License: ISC License (ISC)
13295
- Used by:
13296
- - audiopus_sys 0.2.2 (https://github.com/lakelezz/audiopus_sys.git)
13297
-
13298
- Full license text:
13299
-
13300
- ISC License
13301
-
13302
- Copyright (c) 2019, Lakelezz
13303
-
13304
- Permission to use, copy, modify, and/or distribute this software for any
13305
- purpose with or without fee is hereby granted, provided that the above
13306
- copyright notice and this permission notice appear in all copies.
13307
-
13308
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13309
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13310
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13311
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13312
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13313
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13314
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
13315
-
13316
13345
  -------------------------------------------------------------------------------
13317
13346
  License: ISC License (ISC)
13318
13347
  Used by:
@@ -18734,7 +18763,7 @@ DEALINGS IN THE SOFTWARE.
18734
18763
  -------------------------------------------------------------------------------
18735
18764
  License: MIT License (MIT)
18736
18765
  Used by:
18737
- - opus 0.3.1 (https://github.com/SpaceManiac/opus-rs)
18766
+ - opus 0.4.0 (https://github.com/SpaceManiac/opus-rs)
18738
18767
 
18739
18768
  Full license text:
18740
18769
 
@@ -1,3 +1,28 @@
1
+ /**
2
+ * Largest delay `setTimeout` (and `timers/promises` `scheduler.wait`)
3
+ * accepts without 32-bit signed overflow: larger values wrap and fire
4
+ * almost immediately instead of sleeping. Chunk day-scale provider waits
5
+ * (e.g. a monthly quota reset parsed from an error hint) so the full
6
+ * duration elapses instead of overflowing the timer.
7
+ */
8
+ export declare const MAX_TIMER_DELAY_MS = 2147483647;
9
+ /**
10
+ * Abortable sleep for arbitrarily long delays. Waits longer than
11
+ * {@link MAX_TIMER_DELAY_MS} chunk the sleep into back-to-back timer waits
12
+ * so no single timer overflows; an abort during any chunk rejects like
13
+ * `scheduler.wait`.
14
+ *
15
+ * The remainder is deliberately consumed by chunk, not recomputed from a
16
+ * monotonic deadline: deadline tracking never terminates under the repo's
17
+ * instant `scheduler.wait` mocks (retry-cap suites spy it to resolve
18
+ * immediately, so `deadline - performance.now()` never reaches zero and the
19
+ * loop spins forever). A premature native wake (Bun `uv_async_send`, see
20
+ * `sleepAtLeast` in `packages/agent/src/utils/yield.ts`) can therefore
21
+ * under-wait by the unelapsed chunk time — but that self-corrects downstream:
22
+ * credential blocks carry the true deadline independently of this sleep, so
23
+ * an early retry re-hits 429 and re-sleeps on a fresh server hint.
24
+ */
25
+ export declare function sleepLong(delayMs: number, signal?: AbortSignal): Promise<void>;
1
26
  /**
2
27
  * Wrap a promise with a timeout and optional abort signal.
3
28
  * Rejects with the given error or a new error containing the given message if
@@ -1,12 +1,3 @@
1
- /** Behavior-compatible reimplementation of @puppeteer/browsers' used surface. */
2
- /** Supported browser products. */
3
- export declare enum Browser {
4
- CHROME = "chrome",
5
- CHROMEHEADLESSSHELL = "chrome-headless-shell",
6
- CHROMIUM = "chromium",
7
- FIREFOX = "firefox",
8
- CHROMEDRIVER = "chromedriver"
9
- }
10
1
  /** Browser download platform identifiers. */
11
2
  export declare enum BrowserPlatform {
12
3
  LINUX = "linux",
@@ -16,17 +7,6 @@ export declare enum BrowserPlatform {
16
7
  WIN32 = "win32",
17
8
  WIN64 = "win64"
18
9
  }
19
- /** Chrome-for-Testing release channel tags accepted by {@link resolveBuildId}. */
20
- export declare enum BrowserTag {
21
- CANARY = "canary",
22
- NIGHTLY = "nightly",
23
- BETA = "beta",
24
- DEV = "dev",
25
- DEVEDITION = "devedition",
26
- STABLE = "stable",
27
- ESR = "esr",
28
- LATEST = "latest"
29
- }
30
10
  /** Download progress reported while a browser archive is streamed to disk. */
31
11
  export interface BrowserDownloadProgress {
32
12
  downloadedBytes: number;
@@ -34,7 +14,6 @@ export interface BrowserDownloadProgress {
34
14
  }
35
15
  /** Inputs used to locate an installed browser executable. */
36
16
  export interface ComputeExecutablePathOptions {
37
- browser: Browser;
38
17
  buildId: string;
39
18
  cacheDir: string;
40
19
  platform?: BrowserPlatform;
@@ -44,9 +23,8 @@ export interface InstallOptions extends ComputeExecutablePathOptions {
44
23
  baseUrl?: string;
45
24
  downloadProgressCallback?: (progress: BrowserDownloadProgress) => void;
46
25
  }
47
- /** Metadata for one browser installation found in a Puppeteer cache. */
26
+ /** Metadata for a managed Chrome installation in Puppeteer's cache layout. */
48
27
  export interface InstalledBrowser {
49
- browser: Browser;
50
28
  buildId: string;
51
29
  platform: BrowserPlatform;
52
30
  path: string;
@@ -54,15 +32,9 @@ export interface InstalledBrowser {
54
32
  }
55
33
  /** Detect the current host's Puppeteer browser platform. */
56
34
  export declare function detectBrowserPlatform(): BrowserPlatform | undefined;
57
- /** Resolve a Chrome-for-Testing channel, milestone, or build prefix to a full build ID. */
58
- export declare function resolveBuildId(browser: Browser, _platform: BrowserPlatform, tag: string | BrowserTag): Promise<string>;
59
35
  /** Return the Chrome-for-Testing archive URL for a browser build. */
60
- export declare function getDownloadUrl(browser: Browser, platform: BrowserPlatform, buildId: string, baseUrl?: string): URL;
36
+ export declare function getDownloadUrl(platform: BrowserPlatform, buildId: string, baseUrl?: string): URL;
61
37
  /** Compute the executable path in Puppeteer's cache layout. */
62
38
  export declare function computeExecutablePath(options: ComputeExecutablePathOptions): string;
63
- /** Scan a Puppeteer cache for browser installation directories. */
64
- export declare function getInstalledBrowsers(options: {
65
- cacheDir: string;
66
- }): Promise<InstalledBrowser[]>;
67
39
  /** Download and unpack Chrome into Puppeteer's existing cache layout. */
68
40
  export declare function install(options: InstallOptions): Promise<InstalledBrowser>;
@@ -44,6 +44,16 @@ export declare function resolveProfileEnv(omp: string | undefined, pi: string |
44
44
  export declare function getBaseConfigRoot(): string;
45
45
  export declare function resolveEquivalentPath(inputPath: string): string;
46
46
  export declare function normalizePathForComparison(inputPath: string): string;
47
+ /**
48
+ * Compare paths already normalized by {@link normalizePathForComparison}.
49
+ *
50
+ * Returns the relative path (an empty string when the paths are equal), or
51
+ * `null` when the candidate is outside the root. Callers classifying one
52
+ * candidate against several static roots can normalize each side once and
53
+ * reuse the public helpers' containment semantics without repeating realpath
54
+ * work.
55
+ */
56
+ export declare function relativePathWithinNormalizedRoot(normalizedRoot: string, normalizedCandidate: string): string | null;
47
57
  export declare function pathIsWithin(root: string, candidate: string): boolean;
48
58
  export declare function relativePathWithinRoot(root: string, candidate: string): string | null;
49
59
  /** Get the project directory. */
@@ -271,6 +281,13 @@ export declare function getAgentModulesDir(agentDir?: string): string;
271
281
  export declare function getMemoriesDir(agentDir?: string): string;
272
282
  /** Get the terminal sessions directory (~/.omp/agent/terminal-sessions). */
273
283
  export declare function getTerminalSessionsDir(agentDir?: string): string;
284
+ /**
285
+ * Get the persistent registry of custom session files
286
+ * (~/.omp/agent/custom-session-files). Each `--session-dir`/`--session`
287
+ * transcript is recorded here as one marker file so storage GC can scan its
288
+ * exact path after its terminal breadcrumb is overwritten by a later session.
289
+ */
290
+ export declare function getCustomSessionFilesDir(agentDir?: string): string;
274
291
  /** Get the crash log path (~/.omp/agent/omp-crash.log). */
275
292
  export declare function getCrashLogPath(agentDir?: string): string;
276
293
  /** Get the debug log path (~/.omp/agent/omp-debug.log). */
@@ -26,11 +26,19 @@ export declare function isMacosMallocStackLoggingEnvName(name: string): boolean;
26
26
  */
27
27
  export declare function isWsl(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
28
28
  export declare function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string>;
29
+ /**
30
+ * Removes {@link GIT_REPO_LOCATION_ENV_NAMES} from a copied child env in place.
31
+ *
32
+ * Windows environment lookups are case-insensitive, so a block that spells a
33
+ * variable `git_dir` is just as binding there; match case-insensitively on
34
+ * win32 and exactly elsewhere (POSIX env names are case-sensitive).
35
+ */
36
+ export declare function stripGitRepoLocationEnv(env: Record<string, string>, platform?: NodeJS.Platform): void;
29
37
  /** Filters process env for child shells without launch-cwd dotenv values. */
30
38
  export declare function filterChildShellEnv(env: Record<string, string | undefined>, cwd?: string): Record<string, string>;
31
39
  /**
32
- * Parses a .env file synchronously into key-value string pairs using
33
- * {@link parseEnvLine} for Bun-compatible line semantics, then mirrors valid
40
+ * Parses a complete .env file with the runtime's dotenv grammar, then retains
41
+ * only shell-identifier names and spawn-safe values before mirroring valid
34
42
  * `OMP_` variables to their `PI_` aliases.
35
43
  */
36
44
  export declare function parseEnvFile(filePath: string): Record<string, string>;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Check if a file path exists, is a regular file, and has effective execute permission.
3
+ */
4
+ export declare function isExecutable(filePath: string): boolean;
@@ -1,3 +1,8 @@
1
+ /** Provider-specific interpretation for timezone-naive retry timestamps. */
2
+ export interface RetryHintOptions {
3
+ /** UTC offset appended to an absolute reset stamp that omits its timezone. */
4
+ naiveResetTimezoneOffset?: string;
5
+ }
1
6
  /**
2
7
  * Server-suggested retry delay extraction. Merges the patterns historically used
3
8
  *
@@ -15,14 +20,14 @@
15
20
  * - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
16
21
  * - `retry-after-ms=98497000` / `retry-after-ms: 7200000` / `retry-after-ms = 7200000`
17
22
  * - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
18
- * (offset-bearing only; a timezone-naive stamp is provider wall clock in
19
- * an unknown zone and only resolves when no relative signal is present)
23
+ * (a provider offset makes a naive wall clock authoritative; otherwise it
24
+ * resolves only when no relative signal is present)
20
25
  *
21
26
  * Returns `undefined` if no signal is found, or `0` when the provider
22
27
  * explicitly asks for an immediate retry (`retry-after…=0`, or an absolute
23
28
  * reset timestamp that has already elapsed).
24
29
  */
25
- export declare function extractRetryHint(source: Response | Headers | null | undefined, body?: string): number | undefined;
30
+ export declare function extractRetryHint(source: Response | Headers | null | undefined, body?: string, options?: RetryHintOptions): number | undefined;
26
31
  export interface FetchWithRetryOptions extends RequestInit {
27
32
  /** Total fetch attempts (initial + retries). Default `5`. */
28
33
  maxAttempts?: number;
@@ -16,6 +16,8 @@ declare function tryAcquireLock(lockPath: string): NativeFileLock | null;
16
16
  export declare function acquireFileLock(filePath: string, options?: FileLockOptions): Promise<FileLockHandle>;
17
17
  /** Run `fn` while holding an OS-backed exclusive lock for `filePath`. */
18
18
  export declare function withFileLock<T>(filePath: string, fn: () => Promise<T>, options?: FileLockOptions): Promise<T>;
19
+ /** Run synchronous `fn` while holding an OS-backed exclusive lock for `filePath`. */
20
+ export declare function withFileLockSync<T>(filePath: string, fn: () => T, options?: FileLockOptions): T;
19
21
  /**
20
22
  * Test-only acquisition handle for forcing ownership handoffs. This is not
21
23
  * part of the supported package API.
@@ -4,12 +4,12 @@ export * from "./binary.js";
4
4
  export * from "./color.js";
5
5
  export * from "./dirs.js";
6
6
  export * from "./env.js";
7
+ export * from "./executable.js";
7
8
  export * from "./fetch-retry.js";
8
9
  export * from "./file-lock.js";
9
10
  export * from "./format.js";
10
11
  export * from "./frontmatter.js";
11
12
  export * from "./fs-error.js";
12
- export * from "./glob.js";
13
13
  export * from "./incoming-json.js";
14
14
  export * from "./json.js";
15
15
  export * from "./json-parse.js";
@@ -1,3 +1,4 @@
1
+ export declare const IMAGE_METADATA_HEADER_BYTES: number;
1
2
  export declare const SUPPORTED_IMAGE_MIME_TYPES: Set<string>;
2
3
  export type ImageMetadata = {
3
4
  mimeType: "image/png";
@@ -2,3 +2,9 @@
2
2
  export declare function windowsPathToWslMount(filePath: string): string | undefined;
3
3
  /** Removes Win32 extended-length prefixes before passing paths to Bun APIs. */
4
4
  export declare function stripWindowsExtendedLengthPathPrefix(filePath: string, platform?: NodeJS.Platform): string;
5
+ /**
6
+ * Test whether a path is fully qualified and drive-independent.
7
+ * On Windows, requires a drive letter with separator (e.g. `C:\`) or UNC (`\\server\share` or `//server/share`).
8
+ * On POSIX, requires an absolute path.
9
+ */
10
+ export declare function isFullyQualifiedPath(filePath: string, platform?: NodeJS.Platform): boolean;
@@ -10,9 +10,8 @@ export declare function peekFile<T>(filePath: string, maxBytes: number, op: (hea
10
10
  /**
11
11
  * Read up to the last `maxBytes` of `filePath` and pass that slice to `op`.
12
12
  *
13
- * The tail mirror of {@link peekFile}: same pooled-buffer strategy (no per-call
14
- * allocation for small reads), but the read is positioned at `size - len` so the
15
- * window ends at EOF. When the file is shorter than `maxBytes`, the whole file is
13
+ * The tail mirror of {@link peekFile}: the read is positioned at `size - len` so
14
+ * the window ends at EOF. When the file is shorter than `maxBytes`, the whole file is
16
15
  * returned. A multi-byte codepoint straddling the leading cut decodes to a
17
16
  * replacement char — callers that parse line-oriented tails drop the partial
18
17
  * leading line anyway.
@@ -27,6 +27,27 @@ export declare enum Reason {
27
27
  * instances across bundles/realms.
28
28
  */
29
29
  export declare const NATIVE_PROCESS_EXIT: unique symbol;
30
+ /**
31
+ * Hard-exit the process through the native primitive, resolved on every call.
32
+ *
33
+ * The native exit is deliberately re-resolved here rather than bound at module
34
+ * load: the extension/hook loader's `withHostGuard` transiently swaps
35
+ * `process.reallyExit`/`process.exit` for a stub that throws
36
+ * `ExtensionExitError`, and the shipped bundle defers this module's evaluation
37
+ * until first access — which can land inside that guard window, so binding at
38
+ * init could freeze the throwing stub forever and turn every later shutdown
39
+ * (SIGHUP/SIGINT/fatal) into an unhandled-rejection loop (#7393). When the
40
+ * guard is active the stub carries the native exit under
41
+ * {@link NATIVE_PROCESS_EXIT} (#6488).
42
+ *
43
+ * Both globals are reinstalled to their natives before exiting: Bun's
44
+ * `process.exit` re-reads `process.reallyExit` at call time, so exiting through
45
+ * one primitive while its sibling still holds the throwing stub re-enters the
46
+ * guard and loops the rejection storm (#11789). After restoring, `reallyExit`
47
+ * (the low-level primitive) is preferred; `process.exit` and finally `SIGKILL`
48
+ * are fallbacks so a poisoned or absent chain can never leave the process alive.
49
+ */
50
+ export declare function exitProcess(code: number): never;
30
51
  /** User-facing command printed before fatal cleanup so interrupted work can be resumed. */
31
52
  export interface FatalRecoveryHint {
32
53
  /** Stable label identifying the recoverable session or process. */
@@ -91,11 +112,11 @@ export declare function isWorkerIpcDeserializeError(err: unknown): boolean;
91
112
  */
92
113
  export declare function registerWorkerIpcFaultHandler(handler: (err: Error) => void): () => void;
93
114
  /**
94
- * Treat unhandled stdout EPIPE rejections as a graceful peer disconnect.
95
- *
96
- * Stdio protocol servers call this for their process lifetime so a closed
97
- * client pipe runs registered cleanup callbacks instead of the fatal path.
98
- * The returned callback removes the registration.
115
+ * Treat a closed stdout consumer as a graceful peer disconnect for the caller's
116
+ * active lifetime. Attaches one shared `process.stdout` `error` listener,
117
+ * ref-counted across registrants (the ACP protocol server, the one-shot CLI
118
+ * entry). The returned callback removes the registration; the listener detaches
119
+ * when the last registrant unregisters.
99
120
  */
100
121
  export declare function registerStdioDisconnectHandling(): () => void;
101
122
  /**
@@ -1,4 +1,6 @@
1
1
  import type { Subprocess } from "bun";
2
+ import { isExecutable } from "./executable.js";
3
+ export { isExecutable };
2
4
  export interface ShellConfig {
3
5
  shell: string;
4
6
  args: string[];
@@ -10,10 +12,6 @@ export interface ShellConfigOptions {
10
12
  /** File path or runtime layer that supplied the active shell setting. */
11
13
  configSource?: string;
12
14
  }
13
- /**
14
- * Check if a shell binary is executable.
15
- */
16
- export declare function isExecutable(path: string): boolean;
17
15
  /**
18
16
  * Get shell args for the resolved shell.
19
17
  * cmd.exe takes `/c`; PowerShell (powershell.exe / pwsh) takes
@@ -5,6 +5,7 @@ declare namespace Snowflake {
5
5
  const PATTERN: RegExp;
6
6
  const EPOCH_TIMESTAMP = 1420070400000;
7
7
  const MAX_SEQUENCE = 4194303;
8
+ const MAX_TIMESTAMP: number;
8
9
  function formatParts(dt: number, seq: number): Snowflake;
9
10
  class Source {
10
11
  #private;
@@ -1,5 +1,31 @@
1
1
  export declare function readLines(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Uint8Array>;
2
2
  export declare function readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<T>;
3
+ /**
4
+ * Amortized byte accumulator for chunked stream readers.
5
+ *
6
+ * Holds the unconsumed tail of a stream in a single growing `Buffer` so that
7
+ * appending N chunks costs O(total bytes) instead of re-copying the whole
8
+ * prefix per chunk. Backs {@link readLines}, {@link readJsonl} and
9
+ * {@link readSseEvents}; also usable directly when a reader needs its own
10
+ * framing loop (see `consume` and `flush`).
11
+ */
12
+ export declare class ConcatSink {
13
+ #private;
14
+ append(chunk: Uint8Array): void;
15
+ reset(chunk: Uint8Array): void;
16
+ get isEmpty(): boolean;
17
+ /**
18
+ * The buffered bytes as a live view — invalidated by the next `append`,
19
+ * `reset` or `consume`.
20
+ */
21
+ flush(): Uint8Array | undefined;
22
+ /** Drop the first `count` buffered bytes, keeping the remainder. */
23
+ consume(count: number): void;
24
+ clear(): void;
25
+ appendAndFlushLines(chunk: Uint8Array): Generator<Uint8Array<ArrayBufferLike>, void, unknown>;
26
+ appendAndFlushText(chunk: Uint8Array, decoder: TextDecoder): string | undefined;
27
+ pullJSONL<T>(chunk: Uint8Array, beg: number, end: number): Generator<T, void, unknown>;
28
+ }
3
29
  /**
4
30
  * Stream parsed JSON objects from SSE `data:` lines.
5
31
  *
@@ -18,6 +44,25 @@ export declare function readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?
18
44
  */
19
45
  export type SseEventObserver = (event: ServerSentEvent) => void;
20
46
  export declare function readSseJson<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal, onEvent?: SseEventObserver): AsyncGenerator<T>;
47
+ /**
48
+ * Like {@link readSseJson}, but a `data:` frame that is not valid JSON is yielded
49
+ * as its raw text instead of raising a `SyntaxError`. Cut-off container-shaped
50
+ * stream tails stay recoverable, exactly as they are in {@link readSseJson}.
51
+ *
52
+ * Consumers that only understand objects must treat a `string` yield as a
53
+ * transport-level failure (for example a `429 Too Many Requests` or an HTML
54
+ * throttle page from a reverse proxy that already committed to the stream). This
55
+ * exists because `readSseJson`'s baseline consumers span unrelated transports
56
+ * whose error handling a text yield would subtly change; new call sites opt in.
57
+ *
58
+ * Note that the text lane is only the frames `JSON.parse` *rejected*: a frame
59
+ * carrying a JSON-encoded string (`data: "429 Too Many Requests"`) parses, so it
60
+ * is yielded as that string and is indistinguishable from a rejected frame by
61
+ * type alone. Consumers branching on `typeof === "string"` therefore see both,
62
+ * which is the safe direction — each is classified as text rather than trusted as
63
+ * an event object.
64
+ */
65
+ export declare function readSseJsonOrText<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal, onEvent?: SseEventObserver): AsyncGenerator<T | string>;
21
66
  /**
22
67
  * A single Server-Sent Event dispatched on a blank-line boundary.
23
68
  *
@@ -25,6 +25,12 @@ export interface WhichOptions extends Bun.WhichOptions {
25
25
  * Defaults to `WhichCachePolicy.Fresh`.
26
26
  */
27
27
  cache?: WhichCachePolicy;
28
+ /**
29
+ * Only search absolute directory entries in PATH, ignoring relative entries
30
+ * (e.g. `.` or `./bin`) and empty components to prevent resolving against
31
+ * an untrusted working directory.
32
+ */
33
+ requireAbsolutePaths?: boolean;
28
34
  }
29
35
  declare function darwinWhich(command: string, options?: Bun.WhichOptions): string | null;
30
36
  export declare const whichFresh: typeof darwinWhich;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.1.22",
3
+ "version": "18.2.1",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -54,7 +54,7 @@
54
54
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
55
55
  },
56
56
  "dependencies": {
57
- "@oh-my-pi/pi-natives": "18.1.22"
57
+ "@oh-my-pi/pi-natives": "18.2.1"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/bun": "^1.3.14"
@@ -108,10 +108,15 @@ function createStandardError(
108
108
  type Dispatcher = (method: string, params: unknown, notification: boolean) => MaybePromise<unknown>;
109
109
  type Pending = { resolve(value: unknown): void; reject(reason: unknown): void };
110
110
 
111
+ /** Bound on clean-EOF inbound drain so `closed` cannot hang if a handler never settles. */
112
+ const INBOUND_DRAIN_TIMEOUT_MS = 30_000;
113
+
111
114
  /** Correlated bidirectional JSON-RPC connection. */
112
115
  export class RpcConnection {
113
116
  #nextId = 0;
114
117
  #pending = new Map<JsonRpcId, Pending>();
118
+ #inbound = new Set<Promise<void>>();
119
+ #openInboundIds = new Set<JsonRpcId>();
115
120
  #writable: WritableStream<AnyMessage>;
116
121
  #writeTail: Promise<void> = Promise.resolve();
117
122
  #abort = new AbortController();
@@ -179,8 +184,9 @@ export class RpcConnection {
179
184
  while (true) {
180
185
  const next = await reader.read();
181
186
  if (next.done) break;
182
- void this.#handle(next.value).catch(error => this.close(error));
187
+ this.#dispatch(next.value);
183
188
  }
189
+ await this.#drainInbound();
184
190
  this.close();
185
191
  } catch (error) {
186
192
  this.close(error);
@@ -189,6 +195,34 @@ export class RpcConnection {
189
195
  }
190
196
  }
191
197
 
198
+ #dispatch(message: AnyMessage): void {
199
+ if ("method" in message && "id" in message) this.#openInboundIds.add(message.id);
200
+ const task = this.#handle(message).catch(error => this.close(error));
201
+ this.#inbound.add(task);
202
+ void task.finally(() => this.#inbound.delete(task));
203
+ }
204
+
205
+ async #drainInbound(): Promise<void> {
206
+ if (this.#inbound.size === 0) return;
207
+ const drained = Promise.allSettled(this.#inbound).then(() => {});
208
+ let timer: Timer | undefined;
209
+ const timedOut = await Promise.race([
210
+ drained.then(() => false),
211
+ new Promise<boolean>(resolve => {
212
+ timer = setTimeout(() => resolve(true), INBOUND_DRAIN_TIMEOUT_MS);
213
+ }),
214
+ ]);
215
+ if (timer !== undefined) clearTimeout(timer);
216
+ if (!timedOut) return;
217
+ const error = RequestError.internalError(undefined, "Inbound request drain timed out").toErrorResponse();
218
+ await Promise.allSettled([...this.#openInboundIds].map(id => this.#respond(id, { error })));
219
+ }
220
+
221
+ #respond(id: JsonRpcId, body: { result: unknown } | { error: ErrorResponse }): Promise<void> {
222
+ if (!this.#openInboundIds.delete(id)) return Promise.resolve();
223
+ return this.#write({ jsonrpc: "2.0", id, ...body });
224
+ }
225
+
192
226
  async #handle(message: AnyMessage): Promise<void> {
193
227
  if ("id" in message && !("method" in message)) {
194
228
  const pending = this.#pending.get(message.id);
@@ -210,13 +244,13 @@ export class RpcConnection {
210
244
  }
211
245
  try {
212
246
  const result = await this.#dispatcher(message.method, message.params, false);
213
- await this.#write({ jsonrpc: "2.0", id: message.id, result: result ?? {} });
247
+ await this.#respond(message.id, { result: result ?? {} });
214
248
  } catch (error) {
215
249
  const protocolError =
216
250
  error instanceof RequestError
217
251
  ? error
218
252
  : RequestError.internalError({ details: error instanceof Error ? error.message : String(error) });
219
- await this.#write({ jsonrpc: "2.0", id: message.id, error: protocolError.toErrorResponse() });
253
+ await this.#respond(message.id, { error: protocolError.toErrorResponse() });
220
254
  }
221
255
  }
222
256
  }