@oh-my-pi/pi-utils 18.15.0 → 18.17.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 +27 -0
- package/dist/types/acp/schema.d.ts +1 -1
- package/dist/types/ar/codecs/xz.d.ts +4 -0
- package/dist/types/ar/lzh.d.ts +2 -0
- package/dist/types/bytes.d.ts +7 -0
- package/dist/types/dirs.d.ts +6 -0
- package/dist/types/held-file.d.ts +5 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/logger.d.ts +29 -0
- package/dist/types/path.d.ts +2 -0
- package/dist/types/pid.d.ts +8 -0
- package/dist/types/procmgr.d.ts +1 -1
- package/dist/types/sqlite.d.ts +12 -0
- package/package.json +4 -3
- package/src/acp/schema.ts +53 -107
- package/src/ar/arj.ts +13 -32
- package/src/ar/asar.ts +5 -8
- package/src/ar/codecs/xz.ts +7 -8
- package/src/ar/lzh.ts +21 -27
- package/src/ar/sevenzip/decode.ts +1 -56
- package/src/ar/tar.ts +1 -8
- package/src/bytes.ts +14 -0
- package/src/dirs.ts +9 -0
- package/src/held-file.ts +22 -0
- package/src/index.ts +2 -0
- package/src/logger.ts +143 -29
- package/src/path.ts +14 -0
- package/src/pid.ts +16 -0
- package/src/postmortem.ts +9 -6
- package/src/procmgr.ts +1 -1
- package/src/ptree.ts +82 -18
- package/src/runtime-install.ts +1 -4
- package/src/sqlite.ts +32 -2
- package/src/stderr-guard.ts +18 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.17.0] - 2026-11-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `getUpdateStateDir()`, returning the auto-update state directory (`~/.omp/update`).
|
|
10
|
+
- Added `logger.setLogForwarder()` and `logger.emitForeign()` to forward log records across threads, preserving context and timestamps.
|
|
11
|
+
- Added `expandTilde()` to expand a leading `~` to the home directory.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed `spawn`/`exec` timeouts never firing when the command's root exited before `spawn()` returned, which let a descendant holding stdout or stderr stall output reads until it exited.
|
|
16
|
+
- `ptree` commands with `subreaper: true` now reliably terminate all descendants on graceful shutdown.
|
|
17
|
+
- `fatal()` in a worker thread now reaches the parent's `error` listeners instead of only exiting with code 1.
|
|
18
|
+
- Fixed the terminal stderr guard leaking a copy of the terminal into processes spawned from native code.
|
|
19
|
+
|
|
20
|
+
## [18.16.0] - 2026-11-02
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Added `isPidAlive` (signal-0 liveness probe) and `asStrictBytes` (a `Uint8Array` narrowed to its own `ArrayBuffer` for WebCrypto)
|
|
25
|
+
- Added `ensureSqliteLockShim()`, which `openSqliteDatabase`/`openSqliteDatabaseSync` call before opening a store, so on Linux the process's SQLite locks are open-file-description locks that other closes of the file cannot release ([#15586](https://github.com/can1357/oh-my-pi/issues/15586))
|
|
26
|
+
- Added `assertNotHeldOpen(path)` (`@oh-my-pi/pi-utils/held-file`), which throws `EBUSY` for a SQLite database or `-shm` this process holds open ([#15586](https://github.com/can1357/oh-my-pi/issues/15586))
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `acp` runtime validators (`zNewSessionResponse`, `zSessionNotification`, …) now report each failing field's path and reason in `error.issues` instead of a single `Invalid <label>`; `ValidationError` issue paths are typed `PropertyKey[]` ([#17699](https://github.com/can1357/oh-my-pi/pull/17699))
|
|
31
|
+
|
|
5
32
|
## [18.15.0] - 2026-11-01
|
|
6
33
|
|
|
7
34
|
### Added
|
|
@@ -2,7 +2,7 @@ import type { ForkSessionResponse, LoadSessionResponse, NewSessionResponse, Prom
|
|
|
2
2
|
/** Validation failure compatible with the used Zod error surface. */
|
|
3
3
|
export interface ValidationError {
|
|
4
4
|
issues: Array<{
|
|
5
|
-
path:
|
|
5
|
+
path: PropertyKey[];
|
|
6
6
|
message: string;
|
|
7
7
|
}>;
|
|
8
8
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Undo the delta filter (shared by XZ and 7z) in place. */
|
|
2
|
+
export declare function deltaDecode(bytes: Uint8Array, distance: number): void;
|
|
3
|
+
/** Undo the x86 BCJ filter (shared by XZ and 7z) in place; `startOffset` is the stream position of `bytes[0]`. */
|
|
4
|
+
export declare function x86Decode(bytes: Uint8Array, startOffset: number): void;
|
|
1
5
|
/** Whether bytes begin with the XZ stream-header magic. */
|
|
2
6
|
export declare function isXz(bytes: Uint8Array): boolean;
|
|
3
7
|
/** Decompress all concatenated streams in an XZ container within `maxOutput`. */
|
package/dist/types/ar/lzh.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { FormatReader } from "./types.js";
|
|
2
2
|
/** @internal Decode the static-Huffman LZSS stream shared by LZH and ARJ methods 1-3. */
|
|
3
3
|
export declare function decompressLhStatic(packed: Uint8Array, outSize: number, dictionarySize: number, positionBits: number, positionSymbols: number, label: string): Uint8Array;
|
|
4
|
+
/** MS-DOS packed date/time as UTC milliseconds; `undefined` for 0 or an out-of-range field. */
|
|
5
|
+
export declare function dosTimeToMs(value: number): number | undefined;
|
|
4
6
|
/** Probe whether bytes begin with an LZH/LHA member header. */
|
|
5
7
|
export declare function sniffLzh(bytes: Uint8Array): boolean;
|
|
6
8
|
/** Index an LZH/LHA archive and lazily decode its members from the bounded archive buffer. */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow `bytes` to a view over its own whole `ArrayBuffer`, which is what
|
|
3
|
+
* `crypto.subtle.{digest,sign,importKey,encrypt,decrypt}` require under the
|
|
4
|
+
* strict TS DOM typings. Returns `bytes` itself when it already is one,
|
|
5
|
+
* otherwise a copy.
|
|
6
|
+
*/
|
|
7
|
+
export declare function asStrictBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer>;
|
package/dist/types/dirs.d.ts
CHANGED
|
@@ -142,6 +142,12 @@ export declare function getAgentDir(): string;
|
|
|
142
142
|
export declare function getProjectAgentDir(cwd?: string): string;
|
|
143
143
|
/** Get the reports directory (~/.omp/reports). */
|
|
144
144
|
export declare function getReportsDir(): string;
|
|
145
|
+
/**
|
|
146
|
+
* Get the self-update state directory (~/.omp/update): the background
|
|
147
|
+
* auto-update's status file and worker log, and the lock that keeps two
|
|
148
|
+
* installs (`omp update`, auto-update) from running at once.
|
|
149
|
+
*/
|
|
150
|
+
export declare function getUpdateStateDir(): string;
|
|
145
151
|
/**
|
|
146
152
|
* Get the logs directory (~/.omp/logs). `bun test` processes get
|
|
147
153
|
* `<tmpdir>/omp-test-logs-<uid>` instead, so suites never write into the
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { once, untilAborted } from "./abortable.js";
|
|
2
2
|
export * from "./async.js";
|
|
3
3
|
export * from "./binary.js";
|
|
4
|
+
export * from "./bytes.js";
|
|
4
5
|
export * from "./color.js";
|
|
5
6
|
export * from "./dirs.js";
|
|
6
7
|
export * from "./env.js";
|
|
@@ -22,6 +23,7 @@ export * from "./mime.js";
|
|
|
22
23
|
export * from "./path.js";
|
|
23
24
|
export * from "./path-tree.js";
|
|
24
25
|
export * from "./peek-file.js";
|
|
26
|
+
export * from "./pid.js";
|
|
25
27
|
export * as postmortem from "./postmortem.js";
|
|
26
28
|
export * from "./process-name.js";
|
|
27
29
|
export * as procmgr from "./procmgr.js";
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -9,6 +9,35 @@ export interface LogEvent {
|
|
|
9
9
|
}
|
|
10
10
|
/** Receives each structured log event after the local transport path runs. */
|
|
11
11
|
export type LogSink = (event: LogEvent) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Destination that takes this thread's log records in place of its own
|
|
14
|
+
* transports and sinks (a worker thread routing records to the main thread's
|
|
15
|
+
* logger); see {@link setLogForwarder}.
|
|
16
|
+
*/
|
|
17
|
+
export interface LogForwarder {
|
|
18
|
+
/**
|
|
19
|
+
* Take one record. Its context is already merged over the enclosing
|
|
20
|
+
* {@link withContext} fields and reduced to plain JSON data, so it survives
|
|
21
|
+
* a structured clone; `timestamp` is when the record was logged.
|
|
22
|
+
*/
|
|
23
|
+
emit(event: LogEvent): void;
|
|
24
|
+
/** Write every record emitted so far on the receiving side; backs {@link flush}. */
|
|
25
|
+
flush(): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Route every later record logged on this thread to `forwarder` instead of the
|
|
29
|
+
* local transports and sinks, which then stay unused (no log file is opened);
|
|
30
|
+
* {@link flush} asks the forwarder to flush. `undefined` restores local logging.
|
|
31
|
+
* For worker threads, whose main thread owns the process log file.
|
|
32
|
+
*/
|
|
33
|
+
export declare function setLogForwarder(forwarder: LogForwarder | undefined): void;
|
|
34
|
+
/**
|
|
35
|
+
* Log a record another thread forwarded (see {@link setLogForwarder}): written
|
|
36
|
+
* to the local transports with its own timestamp, then handed to the sinks. Its
|
|
37
|
+
* context is taken as is; the caller's enclosing {@link withContext} is not
|
|
38
|
+
* merged in. On a thread that forwards itself, the record is forwarded on.
|
|
39
|
+
*/
|
|
40
|
+
export declare function emitForeign(event: LogEvent): void;
|
|
12
41
|
/** Register an out-of-band log sink and return a disposer. */
|
|
13
42
|
export declare function registerLogSink(sink: LogSink): () => void;
|
|
14
43
|
/**
|
package/dist/types/path.d.ts
CHANGED
|
@@ -8,3 +8,5 @@ export declare function stripWindowsExtendedLengthPathPrefix(filePath: string, p
|
|
|
8
8
|
* On POSIX, requires an absolute path.
|
|
9
9
|
*/
|
|
10
10
|
export declare function isFullyQualifiedPath(filePath: string, platform?: NodeJS.Platform): boolean;
|
|
11
|
+
/** Expands a leading `~` (`~`, `~/…`, `~\…`, `~name`) to `home`, the user's home directory by default. */
|
|
12
|
+
export declare function expandTilde(filePath: string, home?: string): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal-0 liveness probe: true unless the OS reports the pid as gone
|
|
3
|
+
* (`ESRCH`) or invalid (`EINVAL`). Any other failure, notably `EPERM` (alive
|
|
4
|
+
* but owned by another user), reads as alive, so callers never reclaim a
|
|
5
|
+
* resource a live process may still hold. Unlike `procmgr.isPidRunning`, this
|
|
6
|
+
* does not consult the process table's run state.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isPidAlive(pid: number): boolean;
|
package/dist/types/procmgr.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export declare function resolveBasicShell(): string | undefined;
|
|
|
40
40
|
* Resolve the external shell to advertise on Windows.
|
|
41
41
|
*
|
|
42
42
|
* A host bash is OPTIONAL: bash tool commands always execute in the embedded
|
|
43
|
-
*
|
|
43
|
+
* bash engine (pi-shell). The resolved binary only serves the spawn-a-shell paths
|
|
44
44
|
* (interactive PTY sessions, ACP client terminals, SHELL env), so this
|
|
45
45
|
* prefers a real Git Bash when one exists and otherwise falls back to
|
|
46
46
|
* cmd.exe — it never fails.
|
package/dist/types/sqlite.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** Shared SQLite opening, error attribution, and result-code classification for persistent stores. */
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
|
+
/**
|
|
4
|
+
* Moves this process's SQLite locks onto open-file-description locks (Linux), once.
|
|
5
|
+
*
|
|
6
|
+
* SQLite's POSIX record locks belong to the process, so any close of another
|
|
7
|
+
* descriptor of a database or its `-shm` released them, including Bun's own file
|
|
8
|
+
* APIs and the bash tool's builtins; a peer omp then unlinked the WAL under the live
|
|
9
|
+
* connection (issue #15586). Call before the process opens its first database: a
|
|
10
|
+
* connection opened earlier keeps POSIX locks. Returns whether the shim is active; a
|
|
11
|
+
* Linux build where it cannot be installed (Bun's SQLite layout changed) logs a warning
|
|
12
|
+
* once, and callers with a UI surface it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function ensureSqliteLockShim(): boolean;
|
|
3
15
|
/**
|
|
4
16
|
* A store's identity as `dev:ino:birthtimeMs`, `null` when the path holds no
|
|
5
17
|
* file, or `undefined` when the stat itself failed and no conclusion is safe.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.17.0",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -49,14 +49,15 @@
|
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
|
|
52
|
-
"check:types": "
|
|
52
|
+
"check:types": "bun check --no-script -p tsconfig.json",
|
|
53
53
|
"lint": "oxlint .",
|
|
54
54
|
"test": "bun test --parallel",
|
|
55
55
|
"fix": "oxlint --fix --fix-suggestions . && bun run fmt",
|
|
56
56
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@oh-my-pi/
|
|
59
|
+
"@oh-my-pi/omptype": "18.17.0",
|
|
60
|
+
"@oh-my-pi/pi-natives": "18.17.0"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@types/bun": "^1.3.14"
|
package/src/acp/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Type, type } from "@oh-my-pi/omptype";
|
|
1
2
|
import type {
|
|
2
3
|
ForkSessionResponse,
|
|
3
4
|
LoadSessionResponse,
|
|
@@ -8,7 +9,7 @@ import type {
|
|
|
8
9
|
|
|
9
10
|
/** Validation failure compatible with the used Zod error surface. */
|
|
10
11
|
export interface ValidationError {
|
|
11
|
-
issues: Array<{ path:
|
|
12
|
+
issues: Array<{ path: PropertyKey[]; message: string }>;
|
|
12
13
|
}
|
|
13
14
|
/** Successful validation result. */
|
|
14
15
|
export interface ValidationSuccess<T> {
|
|
@@ -26,129 +27,74 @@ export interface Validator<T> {
|
|
|
26
27
|
parse(value: unknown): T;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Wraps a structural schema in the Zod call shape. Schemas check the fields
|
|
32
|
+
* ACP consumers read and keep every other key, so the result is the full
|
|
33
|
+
* protocol value.
|
|
34
|
+
*/
|
|
35
|
+
function validator<T>(schema: Type, label: string): Validator<T> {
|
|
30
36
|
return {
|
|
31
37
|
safeParse(value) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
const result = schema(value);
|
|
39
|
+
if (!(result instanceof type.errors)) return { success: true, data: result as T };
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
error: { issues: result.map(issue => ({ path: [...issue.path], message: issue.message })) },
|
|
43
|
+
};
|
|
35
44
|
},
|
|
36
45
|
parse(value) {
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
const result = schema(value);
|
|
47
|
+
if (result instanceof type.errors) throw new Error(`Invalid ${label}: ${result.summary}`);
|
|
48
|
+
return result as T;
|
|
39
49
|
},
|
|
40
50
|
};
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function validModes(value: unknown): boolean {
|
|
48
|
-
if (value === undefined || value === null) return true;
|
|
49
|
-
if (typeof value !== "object" || value === null) return false;
|
|
50
|
-
const modes = value as Record<string, unknown>;
|
|
51
|
-
return (
|
|
52
|
-
typeof modes.currentModeId === "string" &&
|
|
53
|
-
Array.isArray(modes.availableModes) &&
|
|
54
|
-
modes.availableModes.every(mode => objectWithString(mode, "id") && objectWithString(mode, "name"))
|
|
55
|
-
);
|
|
56
|
-
}
|
|
53
|
+
const Modes = type({
|
|
54
|
+
currentModeId: "string",
|
|
55
|
+
availableModes: type({ id: "string", name: "string" }).array(),
|
|
56
|
+
}).or("null | undefined");
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
response.configOptions === undefined || response.configOptions === null || Array.isArray(response.configOptions)
|
|
65
|
-
);
|
|
66
|
-
}
|
|
58
|
+
// Responses and notifications are also checked in-process, where absent optionals may be explicit `undefined`.
|
|
59
|
+
const sessionResponseFields = { "modes?": Modes, "configOptions?": "unknown[] | null | undefined" } as const;
|
|
60
|
+
const notArray = (value: object): boolean => !Array.isArray(value);
|
|
61
|
+
const SessionResponse = type(sessionResponseFields).narrow(notArray);
|
|
62
|
+
const IdentifiedSessionResponse = type({ sessionId: "string", ...sessionResponseFields }).narrow(notArray);
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
case "text":
|
|
73
|
-
return typeof block.text === "string";
|
|
74
|
-
case "image":
|
|
75
|
-
case "audio":
|
|
76
|
-
return typeof block.data === "string" && typeof block.mimeType === "string";
|
|
77
|
-
case "resource_link":
|
|
78
|
-
return typeof block.uri === "string" && typeof block.name === "string";
|
|
79
|
-
case "resource":
|
|
80
|
-
return typeof block.resource === "object" && block.resource !== null;
|
|
81
|
-
default:
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
64
|
+
const ContentBlock = type({ type: "'text'", text: "string" })
|
|
65
|
+
.or({ type: "'image' | 'audio'", data: "string", mimeType: "string" })
|
|
66
|
+
.or({ type: "'resource_link'", uri: "string", name: "string" })
|
|
67
|
+
.or({ type: "'resource'", resource: "object" });
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
case "agent_message_chunk":
|
|
99
|
-
case "agent_thought_chunk":
|
|
100
|
-
case "user_message_chunk":
|
|
101
|
-
return contentBlock(update.content);
|
|
102
|
-
case "tool_call":
|
|
103
|
-
return typeof update.toolCallId === "string" && typeof update.title === "string";
|
|
104
|
-
case "tool_call_update":
|
|
105
|
-
return typeof update.toolCallId === "string";
|
|
106
|
-
case "plan":
|
|
107
|
-
return Array.isArray(update.entries);
|
|
108
|
-
case "current_mode_update":
|
|
109
|
-
return typeof update.currentModeId === "string";
|
|
110
|
-
case "available_commands_update":
|
|
111
|
-
return Array.isArray(update.availableCommands);
|
|
112
|
-
case "config_option_update":
|
|
113
|
-
return Array.isArray(update.configOptions);
|
|
114
|
-
case "session_info_update":
|
|
115
|
-
return update.title === undefined || update.title === null || typeof update.title === "string";
|
|
116
|
-
case "usage_update":
|
|
117
|
-
return typeof update.size === "number" && typeof update.used === "number";
|
|
118
|
-
default:
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
69
|
+
const SessionUpdate = type({
|
|
70
|
+
sessionUpdate: "'agent_message_chunk' | 'agent_thought_chunk' | 'user_message_chunk'",
|
|
71
|
+
content: ContentBlock,
|
|
72
|
+
})
|
|
73
|
+
.or({ sessionUpdate: "'tool_call'", toolCallId: "string", title: "string" })
|
|
74
|
+
.or({ sessionUpdate: "'tool_call_update'", toolCallId: "string" })
|
|
75
|
+
.or({ sessionUpdate: "'plan'", entries: "unknown[]" })
|
|
76
|
+
.or({ sessionUpdate: "'current_mode_update'", currentModeId: "string" })
|
|
77
|
+
.or({ sessionUpdate: "'available_commands_update'", availableCommands: "unknown[]" })
|
|
78
|
+
.or({ sessionUpdate: "'config_option_update'", configOptions: "unknown[]" })
|
|
79
|
+
.or({ sessionUpdate: "'session_info_update'", "title?": "string | null | undefined" })
|
|
80
|
+
.or({ sessionUpdate: "'usage_update'", size: "number", used: "number" });
|
|
122
81
|
|
|
123
82
|
/** Validator for new-session responses. */
|
|
124
|
-
export const zNewSessionResponse = validator<NewSessionResponse>(
|
|
125
|
-
value => sessionResponse(value, true),
|
|
126
|
-
"new session response",
|
|
127
|
-
);
|
|
83
|
+
export const zNewSessionResponse = validator<NewSessionResponse>(IdentifiedSessionResponse, "new session response");
|
|
128
84
|
/** Validator for load-session responses. */
|
|
129
|
-
export const zLoadSessionResponse = validator<LoadSessionResponse>(
|
|
130
|
-
value => sessionResponse(value, false),
|
|
131
|
-
"load session response",
|
|
132
|
-
);
|
|
85
|
+
export const zLoadSessionResponse = validator<LoadSessionResponse>(SessionResponse, "load session response");
|
|
133
86
|
/** Validator for fork-session responses. */
|
|
134
|
-
export const zForkSessionResponse = validator<ForkSessionResponse>(
|
|
135
|
-
value => sessionResponse(value, true),
|
|
136
|
-
"fork session response",
|
|
137
|
-
);
|
|
87
|
+
export const zForkSessionResponse = validator<ForkSessionResponse>(IdentifiedSessionResponse, "fork session response");
|
|
138
88
|
/** Validator for prompt responses. */
|
|
139
|
-
export const zPromptResponse = validator<PromptResponse>(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
stopReason === "end_turn" ||
|
|
144
|
-
stopReason === "max_tokens" ||
|
|
145
|
-
stopReason === "max_turn_requests" ||
|
|
146
|
-
stopReason === "refusal" ||
|
|
147
|
-
stopReason === "cancelled"
|
|
148
|
-
);
|
|
149
|
-
}, "prompt response");
|
|
89
|
+
export const zPromptResponse = validator<PromptResponse>(
|
|
90
|
+
type({ stopReason: "'end_turn' | 'max_tokens' | 'max_turn_requests' | 'refusal' | 'cancelled'" }),
|
|
91
|
+
"prompt response",
|
|
92
|
+
);
|
|
150
93
|
/** Validator for session notifications. */
|
|
151
|
-
export const zSessionNotification = validator<SessionNotification>(
|
|
94
|
+
export const zSessionNotification = validator<SessionNotification>(
|
|
95
|
+
type({ sessionId: "string", update: SessionUpdate }).narrow(notArray),
|
|
96
|
+
"session notification",
|
|
97
|
+
);
|
|
152
98
|
|
|
153
99
|
/** ACP runtime validators. */
|
|
154
100
|
export const schema = {
|
package/src/ar/arj.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { readUInt16LE, readUInt32LE } from "./bytes";
|
|
1
2
|
import { crc32 } from "./checksums";
|
|
2
3
|
import { ArchiveError } from "./error";
|
|
3
4
|
import { assertArchiveMemberSize, assertEntryCount, assertIndexSize, assertInMemorySize } from "./limits";
|
|
4
|
-
import { decompressLhStatic } from "./lzh";
|
|
5
|
+
import { decompressLhStatic, dosTimeToMs } from "./lzh";
|
|
5
6
|
import { assertArchivePathBytes, assertArchivePathString, normalizeArchiveEntryPath } from "./paths";
|
|
6
7
|
import { type ByteSource, readAllBytes } from "./source";
|
|
7
8
|
import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
|
|
@@ -11,14 +12,6 @@ const ARJ_SIGNATURE_1 = 0xea;
|
|
|
11
12
|
const ARJ_MAX_BASIC_HEADER = 2600;
|
|
12
13
|
const LEGACY_DECODER = new TextDecoder("windows-1252");
|
|
13
14
|
|
|
14
|
-
function u16(bytes: Uint8Array, offset: number): number {
|
|
15
|
-
return bytes[offset]! | (bytes[offset + 1]! << 8);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function u32(bytes: Uint8Array, offset: number): number {
|
|
19
|
-
return (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
15
|
function assertRange(bytes: Uint8Array, start: number, end: number, what: string): void {
|
|
23
16
|
if (
|
|
24
17
|
!Number.isSafeInteger(start) ||
|
|
@@ -31,18 +24,6 @@ function assertRange(bytes: Uint8Array, start: number, end: number, what: string
|
|
|
31
24
|
}
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
function dosTimeToMs(value: number): number | undefined {
|
|
35
|
-
if (value === 0) return undefined;
|
|
36
|
-
const year = 1980 + ((value >>> 25) & 0x7f);
|
|
37
|
-
const month = (value >>> 21) & 0x0f;
|
|
38
|
-
const day = (value >>> 16) & 0x1f;
|
|
39
|
-
const hour = (value >>> 11) & 0x1f;
|
|
40
|
-
const minute = (value >>> 5) & 0x3f;
|
|
41
|
-
const second = (value & 0x1f) * 2;
|
|
42
|
-
if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) return undefined;
|
|
43
|
-
return Date.UTC(year, month - 1, day, hour, minute, second);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
27
|
function readCString(bytes: Uint8Array, start: number, end: number, field: string): { value: string; next: number } {
|
|
47
28
|
let terminator = start;
|
|
48
29
|
while (terminator < end && bytes[terminator] !== 0) terminator++;
|
|
@@ -63,26 +44,26 @@ function parseArjBlock(bytes: Uint8Array, offset: number, options: FormatReadOpt
|
|
|
63
44
|
if (bytes[offset] !== ARJ_SIGNATURE_0 || bytes[offset + 1] !== ARJ_SIGNATURE_1) {
|
|
64
45
|
throw new ArchiveError("Invalid ARJ header signature");
|
|
65
46
|
}
|
|
66
|
-
const bodySize =
|
|
47
|
+
const bodySize = readUInt16LE(bytes, offset + 2);
|
|
67
48
|
if (bodySize === 0)
|
|
68
49
|
return { bodyStart: offset + 4, bodySize: 0, nextOffset: offset + 4, metadataSize: 4, isEnd: true };
|
|
69
50
|
if (bodySize < 30 || bodySize > ARJ_MAX_BASIC_HEADER) throw new ArchiveError("Invalid ARJ basic header size");
|
|
70
51
|
const bodyStart = offset + 4;
|
|
71
52
|
const bodyEnd = bodyStart + bodySize;
|
|
72
53
|
assertRange(bytes, bodyStart, bodyEnd + 4, "basic header");
|
|
73
|
-
if (crc32(bytes.subarray(bodyStart, bodyEnd)) !==
|
|
54
|
+
if (crc32(bytes.subarray(bodyStart, bodyEnd)) !== readUInt32LE(bytes, bodyEnd)) {
|
|
74
55
|
throw new ArchiveError("Invalid ARJ basic header CRC32");
|
|
75
56
|
}
|
|
76
57
|
let cursor = bodyEnd + 4;
|
|
77
58
|
let extensionCount = 0;
|
|
78
59
|
for (;;) {
|
|
79
60
|
assertRange(bytes, cursor, cursor + 2, "extended header size");
|
|
80
|
-
const extensionSize =
|
|
61
|
+
const extensionSize = readUInt16LE(bytes, cursor);
|
|
81
62
|
cursor += 2;
|
|
82
63
|
if (extensionSize === 0) break;
|
|
83
64
|
if (++extensionCount > 65_535) throw new ArchiveError("Invalid ARJ archive: too many extended headers");
|
|
84
65
|
assertRange(bytes, cursor, cursor + extensionSize + 4, "extended header");
|
|
85
|
-
if (crc32(bytes.subarray(cursor, cursor + extensionSize)) !==
|
|
66
|
+
if (crc32(bytes.subarray(cursor, cursor + extensionSize)) !== readUInt32LE(bytes, cursor + extensionSize)) {
|
|
86
67
|
throw new ArchiveError("Invalid ARJ extended header CRC32");
|
|
87
68
|
}
|
|
88
69
|
cursor += extensionSize + 4;
|
|
@@ -216,10 +197,10 @@ function normalizeHostPath(rawPath: string, hostOs: number): string {
|
|
|
216
197
|
/** Probe whether bytes begin with a CRC-framed ARJ main header. */
|
|
217
198
|
export function sniffArj(bytes: Uint8Array): boolean {
|
|
218
199
|
if (bytes.byteLength < 34 || bytes[0] !== ARJ_SIGNATURE_0 || bytes[1] !== ARJ_SIGNATURE_1) return false;
|
|
219
|
-
const size =
|
|
200
|
+
const size = readUInt16LE(bytes, 2);
|
|
220
201
|
if (size < 30 || size > ARJ_MAX_BASIC_HEADER || bytes.byteLength < size + 8) return false;
|
|
221
202
|
const body = bytes.subarray(4, 4 + size);
|
|
222
|
-
return body[0]! >= 30 && body[0]! <= size && body[6] === 2 && crc32(body) ===
|
|
203
|
+
return body[0]! >= 30 && body[0]! <= size && body[6] === 2 && crc32(body) === readUInt32LE(bytes, 4 + size);
|
|
223
204
|
}
|
|
224
205
|
|
|
225
206
|
/** Index an ARJ archive and lazily decode stored, static-Huffman, and fast-LZSS members. */
|
|
@@ -266,10 +247,10 @@ export const readArj: FormatReader = async (
|
|
|
266
247
|
const fileType = bytes[block.bodyStart + 6]!;
|
|
267
248
|
if ((flags & 0x01) !== 0) throw new ArchiveError("Encrypted ARJ members are unsupported");
|
|
268
249
|
if ((flags & 0x0c) !== 0) throw new ArchiveError("Multi-volume ARJ members are unsupported");
|
|
269
|
-
const packedSize =
|
|
270
|
-
const size =
|
|
271
|
-
const fileCrc =
|
|
272
|
-
const accessMode =
|
|
250
|
+
const packedSize = readUInt32LE(bytes, block.bodyStart + 12);
|
|
251
|
+
const size = readUInt32LE(bytes, block.bodyStart + 16);
|
|
252
|
+
const fileCrc = readUInt32LE(bytes, block.bodyStart + 20);
|
|
253
|
+
const accessMode = readUInt16LE(bytes, block.bodyStart + 26);
|
|
273
254
|
const filename = readCString(
|
|
274
255
|
bytes,
|
|
275
256
|
block.bodyStart + firstHeaderSize,
|
|
@@ -295,7 +276,7 @@ export const readArj: FormatReader = async (
|
|
|
295
276
|
if ((hostOs === 2 || hostOs === 8) && accessMode !== 0) {
|
|
296
277
|
mode = (isDirectory ? 0x4000 : 0x8000) | (accessMode & 0x0fff);
|
|
297
278
|
}
|
|
298
|
-
const rawMtime =
|
|
279
|
+
const rawMtime = readUInt32LE(bytes, block.bodyStart + 8);
|
|
299
280
|
const mtimeMs =
|
|
300
281
|
hostOs === 2 || hostOs === 8 ? (rawMtime === 0 ? undefined : rawMtime * 1000) : dosTimeToMs(rawMtime);
|
|
301
282
|
entries.push({
|
package/src/ar/asar.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import { readUInt32LE, writeUInt32LE } from "./bytes";
|
|
3
3
|
import { ArchiveError } from "./error";
|
|
4
4
|
import { assertArchiveMemberSize, assertEntryCount, assertIndexSize } from "./limits";
|
|
5
|
+
import { isRecord } from "../type-guards";
|
|
5
6
|
import {
|
|
6
7
|
assertArchivePathBytes,
|
|
7
8
|
assertArchivePathString,
|
|
@@ -55,14 +56,10 @@ function alignAsarPayload(size: number): number {
|
|
|
55
56
|
return size + ((4 - (size % 4)) % 4);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function isArchiveRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
59
|
function parseIntegrity(value: unknown, entryPath: string, size: number): AsarIntegrity | undefined {
|
|
63
60
|
if (value === undefined) return undefined;
|
|
64
61
|
const label = formatArchivePathForError(entryPath);
|
|
65
|
-
if (!
|
|
62
|
+
if (!isRecord(value)) {
|
|
66
63
|
throw invalidAsar(`file '${label}' has an invalid integrity record`);
|
|
67
64
|
}
|
|
68
65
|
if (value.algorithm !== "SHA256") {
|
|
@@ -228,7 +225,7 @@ async function readAsarIndex(source: ByteSource, options: FormatReadOptions): Pr
|
|
|
228
225
|
} catch {
|
|
229
226
|
throw invalidAsar("header is not valid JSON");
|
|
230
227
|
}
|
|
231
|
-
if (!
|
|
228
|
+
if (!isRecord(header) || !isRecord(header.files)) {
|
|
232
229
|
throw invalidAsar("root must contain a files object");
|
|
233
230
|
}
|
|
234
231
|
|
|
@@ -257,7 +254,7 @@ async function readAsarIndex(source: ByteSource, options: FormatReadOptions): Pr
|
|
|
257
254
|
) {
|
|
258
255
|
throw invalidAsar(`invalid entry name '${formatArchivePathForError(current.name)}'`);
|
|
259
256
|
}
|
|
260
|
-
if (!
|
|
257
|
+
if (!isRecord(current.node)) {
|
|
261
258
|
throw invalidAsar(`entry '${formatArchivePathForError(current.name)}' must be an object`);
|
|
262
259
|
}
|
|
263
260
|
const entryPathBytes = current.parentPathBytes + (current.parentPath ? 1 : 0) + nameBytes;
|
|
@@ -295,7 +292,7 @@ async function readAsarIndex(source: ByteSource, options: FormatReadOptions): Pr
|
|
|
295
292
|
}
|
|
296
293
|
|
|
297
294
|
if ("files" in current.node) {
|
|
298
|
-
if (!
|
|
295
|
+
if (!isRecord(current.node.files)) {
|
|
299
296
|
throw invalidAsar(`directory '${entryLabel}' must contain a files object`);
|
|
300
297
|
}
|
|
301
298
|
entries.push({ path: entryPath, isDirectory: true, size: 0 });
|
package/src/ar/codecs/xz.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readUInt32BE } from "../bytes";
|
|
1
2
|
import { crc32, crc64 } from "../checksums";
|
|
2
3
|
import { ArchiveError } from "../error";
|
|
3
4
|
import { lzma2Decompress } from "./lzma";
|
|
@@ -16,10 +17,6 @@ function write32LE(bytes: Uint8Array, offset: number, value: number): void {
|
|
|
16
17
|
bytes[offset + 3] = value >>> 24;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function read32BE(bytes: Uint8Array, offset: number): number {
|
|
20
|
-
return ((bytes[offset]! << 24) | (bytes[offset + 1]! << 16) | (bytes[offset + 2]! << 8) | bytes[offset + 3]!) >>> 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
function write32BE(bytes: Uint8Array, offset: number, value: number): void {
|
|
24
21
|
bytes[offset] = value >>> 24;
|
|
25
22
|
bytes[offset + 1] = (value >>> 16) & 0xff;
|
|
@@ -155,7 +152,8 @@ interface XzFilter {
|
|
|
155
152
|
properties: Uint8Array;
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
|
|
155
|
+
/** Undo the delta filter (shared by XZ and 7z) in place. */
|
|
156
|
+
export function deltaDecode(bytes: Uint8Array, distance: number): void {
|
|
159
157
|
const history = new Uint8Array(256);
|
|
160
158
|
let position = 0;
|
|
161
159
|
for (let index = 0; index < bytes.byteLength; index++) {
|
|
@@ -166,7 +164,8 @@ function deltaDecode(bytes: Uint8Array, distance: number): void {
|
|
|
166
164
|
}
|
|
167
165
|
}
|
|
168
166
|
|
|
169
|
-
|
|
167
|
+
/** Undo the x86 BCJ filter (shared by XZ and 7z) in place; `startOffset` is the stream position of `bytes[0]`. */
|
|
168
|
+
export function x86Decode(bytes: Uint8Array, startOffset: number): void {
|
|
170
169
|
const maskToBitNumber = [0, 1, 2, 2, 3] as const;
|
|
171
170
|
let previousMask = 0;
|
|
172
171
|
let previousPosition = 0xfffffffb;
|
|
@@ -267,7 +266,7 @@ function sparcDecode(bytes: Uint8Array, startOffset: number): void {
|
|
|
267
266
|
)
|
|
268
267
|
)
|
|
269
268
|
continue;
|
|
270
|
-
const source = (
|
|
269
|
+
const source = (readUInt32BE(bytes, index) << 2) >>> 0;
|
|
271
270
|
let destination = (source - startOffset - index) >>> 2;
|
|
272
271
|
destination = (((0 - ((destination >>> 22) & 1)) << 22) & 0x3fffffff) | (destination & 0x3fffff) | 0x40000000;
|
|
273
272
|
write32BE(bytes, index, destination >>> 0);
|
|
@@ -356,7 +355,7 @@ function riscvDecode(bytes: Uint8Array, startOffset: number): void {
|
|
|
356
355
|
index += 2;
|
|
357
356
|
continue;
|
|
358
357
|
}
|
|
359
|
-
const address = (
|
|
358
|
+
const address = (readUInt32BE(bytes, index + 4) - startOffset - index) >>> 0;
|
|
360
359
|
instruction2 = ((instruction >>> 12) | (address << 20)) >>> 0;
|
|
361
360
|
instruction = (0x17 | (register << 7) | ((address + 0x800) & 0xfffff000)) >>> 0;
|
|
362
361
|
}
|