@linxiraos/pi-natives 1.1.26 → 1.2.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/README.md CHANGED
@@ -10,6 +10,8 @@ Native Rust functionality via N-API.
10
10
  - **Audio**: Cross-platform low-latency microphone capture and gapless speaker playback
11
11
  - **WebRTC**: Native Opus media, SDP offer/answer negotiation, and data-channel events for live sessions
12
12
  - **File locking**: Process-owned cross-process locks with in-memory kernel names on Linux/Windows and `flock(2)` sidecars on other Unix platforms
13
+ - **PDF**: In-memory PDF-to-Markdown extraction with OCR-page classification via `pdf-inspector`
14
+ - **Binary patches**: `applyBinaryPatch(oldPath, patchPath, outPath)` applies an HDiffPatch single-stream patch (HDIFFSF20, zstd or uncompressed), streaming from files; callers verify digests
13
15
 
14
16
  General-purpose image processing (decode/resize/encode for files and buffers)
15
17
  lives in [`Bun.Image`](https://bun.com/docs/runtime/image) on the JS side; this
@@ -19,7 +21,7 @@ that terminal protocol.
19
21
  ## Usage
20
22
 
21
23
  ```typescript
22
- import { grep, find, encodeSixel } from "@linxiraos/pi-natives";
24
+ import { encodeSixelAsync, grep, pdfToMarkdown } from "@linxiraos/pi-natives";
23
25
 
24
26
  // Grep for a pattern
25
27
  const results = await grep({
@@ -36,8 +38,13 @@ const files = await find({
36
38
  fileType: "file",
37
39
  });
38
40
 
39
- // SIXEL encode for a terminal cell box (px)
40
- const sequence = encodeSixel(pngBytes, widthPx, heightPx);
41
+ // SIXEL encode for a terminal cell box (px), off the JS thread
42
+ // (`encodeSixel` is the synchronous form)
43
+ const sequence = await encodeSixelAsync(pngBytes, widthPx, heightPx);
44
+
45
+ // Extract PDF text and identify pages that still need OCR
46
+ const pdf = await pdfToMarkdown(pdfBytes);
47
+ console.log(pdf.markdown, pdf.pagesNeedingOcr);
41
48
  ```
42
49
 
43
50
  ### Lazy Windows path helpers
@@ -71,7 +78,7 @@ crates/pi-natives/ # Rust source (workspace member)
71
78
  native/ # Core loader files and local/CI native build outputs
72
79
  index.js # Public native export surface
73
80
  loader-state.js # Platform, ISA variant, and addon resolution
74
- embedded-addon.js # Standalone binary embed stub/generated metadata
81
+ embedded-addon.js # Null embed stub; binary builds replace it in memory
75
82
  pi_natives.<platform>-<arch>-modern.node # x64 modern ISA (local/CI artifact)
76
83
  pi_natives.<platform>-<arch>-baseline.node # x64 baseline ISA (local/CI artifact)
77
84
  pi_natives.<platform>-<arch>.node # non-x64 build artifact
@@ -1,28 +1,4 @@
1
- interface AdaptedDesktopCapabilities {
2
- readonly [key: string]: unknown;
3
- readonly ax: boolean;
4
- readonly backgroundWindowInput: boolean;
5
- readonly takeover: boolean;
6
- readonly axPermission: string;
7
- }
1
+ import type { DesktopSession } from "./index.js";
8
2
 
9
- interface AdaptedDesktopSession {
10
- readonly capabilities: AdaptedDesktopCapabilities;
11
- listWindows(): Promise<Array<Record<string, unknown>>>;
12
- capture(target: string, caps?: unknown): Promise<Record<string, unknown>>;
13
- click(
14
- target: string,
15
- x: number,
16
- y: number,
17
- options?: { button?: string; count?: number; modifiers?: string[]; takeover?: boolean },
18
- ): Promise<void>;
19
- typeText(target: string, text: string, options?: { takeover?: boolean }): Promise<void>;
20
- keyChord(target: string, keys: string[], options?: { takeover?: boolean }): Promise<void>;
21
- close(): Promise<void>;
22
- }
23
-
24
- interface AdaptedDesktopSessionConstructor {
25
- new (options: Record<string, unknown>): AdaptedDesktopSession;
26
- }
27
-
28
- export function adaptDesktopSession(NativeDesktopSession: unknown): AdaptedDesktopSessionConstructor;
3
+ /** Return the current native ABI, or a constructor that rejects an outdated addon on first desktop use. */
4
+ export function adaptDesktopSession(NativeDesktopSession: unknown): typeof DesktopSession;
@@ -1,404 +1,36 @@
1
- const LEGACY_ERROR_CODES = {
2
- DESKTOP_INVALID_OPTIONS: "InvalidTarget",
3
- DESKTOP_INVALID_ACTION: "InvalidTarget",
4
- DESKTOP_BACKEND_UNAVAILABLE: null,
5
- DESKTOP_PERMISSION_DENIED: "PermissionDenied",
6
- DESKTOP_CAPTURE_FAILED: "CaptureFailed",
7
- DESKTOP_INPUT_FAILED: "InputFailed",
8
- DESKTOP_DEADLINE_EXCEEDED: "Timeout",
9
- DESKTOP_LAYOUT_CHANGED: "InvalidCoordinateFrame",
10
- DESKTOP_COORDINATE_OUT_OF_BOUNDS: "InvalidCoordinateFrame",
11
- DESKTOP_SESSION_CLOSED: "Closed",
12
- DESKTOP_WORKER_FAILED: "Internal",
13
- };
14
-
15
- const ADAPTED_SESSION_CLASSES = new WeakMap();
16
-
17
- function desktopError(code, message) {
18
- return new Error(`${code}: ${message}`);
19
- }
20
-
21
- function normalizeError(error, fallbackCode) {
22
- if (!(error instanceof Error)) return desktopError(fallbackCode, String(error));
23
- if (/^[A-Z][A-Za-z]+: /.test(error.message)) return error;
24
-
25
- const match = /^(DESKTOP_[A-Z_]+):\s*(.*)$/.exec(error.message);
26
- if (match === null) return desktopError(fallbackCode, error.message);
27
- const code = LEGACY_ERROR_CODES[match[1]] ?? fallbackCode;
28
- return desktopError(code, match[2]);
29
- }
30
-
31
- function normalizeCapabilities(capabilities) {
32
- return {
33
- ...capabilities,
34
- ax: false,
35
- backgroundWindowInput: false,
36
- takeover: true,
37
- axPermission: "unavailable",
38
- };
39
- }
40
-
41
- function legacyPoint(point) {
42
- return { x: Math.round(point.x), y: Math.round(point.y) };
43
- }
44
-
45
- function captureCapsKey(caps) {
46
- return `${caps?.maxWidth ?? ""}:${caps?.maxHeight ?? ""}`;
47
- }
48
-
49
- function legacyButton(button) {
50
- return button === "middle" ? "wheel" : button;
51
- }
52
- function sourceDimensions(capture) {
53
- if (capture.sourceWidth !== undefined && capture.sourceHeight !== undefined) {
54
- return { sourceWidth: capture.sourceWidth, sourceHeight: capture.sourceHeight };
55
- }
56
- const displays = Array.isArray(capture.displays) ? capture.displays : [];
57
- if (displays.length === 0) {
58
- return { sourceWidth: capture.width, sourceHeight: capture.height };
59
- }
60
- const minX = Math.min(...displays.map(display => display.x));
61
- const minY = Math.min(...displays.map(display => display.y));
62
- const maxX = Math.max(...displays.map(display => display.x + display.width));
63
- const maxY = Math.max(...displays.map(display => display.y + display.height));
64
- const nativeScale = Math.max(1, ...displays.map(display => display.scale ?? 1));
65
- return {
66
- sourceWidth: Math.max(1, Math.round((maxX - minX) * nativeScale)),
67
- sourceHeight: Math.max(1, Math.round((maxY - minY) * nativeScale)),
68
- };
69
- }
70
-
71
- function displaysEqual(a, b) {
72
- if (a === b) return true;
73
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
74
- for (let i = 0; i < a.length; i++) {
75
- const x = a[i];
76
- const y = b[i];
77
- if (
78
- x === y ||
79
- (x?.id === y?.id &&
80
- x?.x === y?.x &&
81
- x?.y === y?.y &&
82
- x?.width === y?.width &&
83
- x?.height === y?.height &&
84
- x?.scale === y?.scale &&
85
- x?.pixelX === y?.pixelX &&
86
- x?.pixelY === y?.pixelY &&
87
- x?.pixelWidth === y?.pixelWidth &&
88
- x?.pixelHeight === y?.pixelHeight)
89
- ) {
90
- continue;
91
- }
92
- return false;
93
- }
94
- return true;
95
- }
96
-
97
- // Field-by-field frame comparison: the JSON.stringify signature allocated a
98
- // full string over the display list on every capture and every input action.
99
- // A rolling hash would risk collisions where string equality had none, so
100
- // compare the same fields directly instead.
101
- function sameFrame(a, b) {
102
- if (a === b) return true;
103
- if (!a || !b) return false;
104
- return a.target === b.target && a.width === b.width && a.height === b.height && displaysEqual(a.displays, b.displays);
105
- }
106
-
107
- // Lightweight projection of the fields sameFrame reads: the stored capture
108
- // must NOT retain the full screenshot (`data: Uint8Array`, multi-MB per
109
- // target) after callers finish with it.
110
- function frameIdentity(capture) {
111
- const displays = Array.isArray(capture.displays) ? capture.displays.map(displayOf) : capture.displays;
112
- return { target: capture.target, width: capture.width, height: capture.height, displays };
113
- }
114
-
115
- function displayOf(display) {
116
- return {
117
- id: display?.id,
118
- x: display?.x,
119
- y: display?.y,
120
- width: display?.width,
121
- height: display?.height,
122
- scale: display?.scale,
123
- pixelX: display?.pixelX,
124
- pixelY: display?.pixelY,
125
- pixelWidth: display?.pixelWidth,
126
- pixelHeight: display?.pixelHeight,
127
- };
128
- }
129
-
130
- function frameSignature(capture) {
131
- return JSON.stringify({
132
- target: capture.target,
133
- width: capture.width,
134
- height: capture.height,
135
- displays: capture.displays?.map(display => ({
136
- id: display.id,
137
- x: display.x,
138
- y: display.y,
139
- width: display.width,
140
- height: display.height,
141
- scale: display.scale,
142
- pixelX: display.pixelX,
143
- pixelY: display.pixelY,
144
- pixelWidth: display.pixelWidth,
145
- pixelHeight: display.pixelHeight,
146
- })),
147
- });
148
- }
149
-
150
- /**
151
- * Adapt the pre-parity desktop addon ABI used by pull-request CI artifacts to
152
- * the current session contract. Released addons exposed capture/execute/close;
153
- * current addons already expose the complete API and pass through unchanged.
154
- */
1
+ const REQUIRED_METHODS = [
2
+ "click",
3
+ "capture",
4
+ "captureRegion",
5
+ "cancel",
6
+ "retire",
7
+ "observe",
8
+ "listApplications",
9
+ "openApplication",
10
+ "menuItems",
11
+ "menuSelect",
12
+ "holdKeys",
13
+ "holdMouse",
14
+ "acquireControl",
15
+ "releaseControl",
16
+ "controlState",
17
+ "bringToCurrentSpace",
18
+ ];
19
+
20
+ /** Require the current desktop ABI; partial legacy emulation cannot preserve input ownership. */
155
21
  export function adaptDesktopSession(NativeDesktopSession) {
156
- if (typeof NativeDesktopSession?.prototype?.click === "function") return NativeDesktopSession;
157
- const cached = ADAPTED_SESSION_CLASSES.get(NativeDesktopSession);
158
- if (cached) return cached;
159
-
160
- class DesktopSession {
161
- #native;
162
- #nativeDesktopSession;
163
- #options;
164
- #sessions;
165
- #closed = false;
166
- #capturedTargets = new Map();
167
-
168
- constructor(options) {
169
- try {
170
- this.#nativeDesktopSession = NativeDesktopSession;
171
- this.#options = options;
172
- this.#native = new NativeDesktopSession(options);
173
- this.#sessions = new Map([[captureCapsKey(), this.#native]]);
174
- } catch (error) {
175
- throw normalizeError(error, "Internal");
176
- }
177
- }
178
-
179
- get capabilities() {
180
- return normalizeCapabilities(this.#native.capabilities);
181
- }
182
-
183
- #ensureOpen() {
184
- if (this.#closed) throw desktopError("Closed", "desktop session is closed");
185
- }
186
-
187
- #nativeForCapturedTarget(target) {
188
- const capture = this.#capturedTargets.get(target);
189
- if (capture) return capture.native;
190
- throw desktopError(
191
- "InvalidCoordinateFrame",
192
- `no capture of '${target}' yet — take a screenshot of this target first`,
193
- );
194
- }
195
-
196
- #sessionForCapture(caps) {
197
- const key = captureCapsKey(caps);
198
- const existing = this.#sessions.get(key);
199
- if (existing) return existing;
200
- try {
201
- const native = new this.#nativeDesktopSession({
202
- ...this.#options,
203
- maxWidth: caps?.maxWidth,
204
- maxHeight: caps?.maxHeight,
205
- });
206
- this.#sessions.set(key, native);
207
- return native;
208
- } catch (error) {
209
- throw normalizeError(error, "CaptureFailed");
210
- }
211
- }
212
-
213
- #ensureForeground(target, options) {
214
- if (target !== "desktop" && options?.takeover !== true) {
215
- throw desktopError(
216
- "BackgroundUnavailable",
217
- "the installed native addon supports foreground input only",
218
- );
219
- }
220
- }
221
-
222
- async #execute(actions, target, native = this.#native, fallbackCode = "InputFailed") {
223
- try {
224
- const capture = await native.execute(Array.isArray(actions) ? actions : [actions], target);
225
- const previous = this.#capturedTargets.get(target);
226
- if (capture && previous?.native === native && !sameFrame(capture, previous.capture)) {
227
- this.#capturedTargets.delete(target);
228
- }
229
- } catch (error) {
230
- throw normalizeError(error, fallbackCode);
231
- }
232
- }
233
-
234
- async listDisplays() {
235
- this.#ensureOpen();
236
- throw desktopError("CaptureFailed", "the installed native addon does not expose display enumeration");
237
- }
238
-
239
- async listWindows() {
240
- this.#ensureOpen();
241
- if (typeof this.#native.listWindows !== "function") {
242
- throw desktopError("CaptureFailed", "the installed native addon does not expose window enumeration");
243
- }
244
- try {
245
- return await this.#native.listWindows();
246
- } catch (error) {
247
- throw normalizeError(error, "CaptureFailed");
248
- }
249
- }
250
-
251
- async capture(target, caps) {
252
- this.#ensureOpen();
253
- if (target !== "desktop" && !/^\d+$/.test(target)) {
254
- throw desktopError("InvalidTarget", `invalid window target '${target}'`);
255
- }
256
- if (target !== "desktop" && typeof this.#native.listWindows !== "function") {
257
- throw desktopError("CaptureFailed", "the installed native addon does not support window capture");
258
- }
259
- try {
260
- const native = this.#sessionForCapture(caps);
261
- const capture = await native.capture(target);
262
- const adapted = { ...capture, ...sourceDimensions(capture), target };
263
- // Store only the comparison fields, not the screenshot bytes:
264
- // `adapted` carries the full PNG `data` (multi-MB per target).
265
- // Keep the string signature for any external reader of the
266
- // stored shape.
267
- this.#capturedTargets.set(target, { native, capture: frameIdentity(adapted), signature: frameSignature(adapted) });
268
- return adapted;
269
- } catch (error) {
270
- throw normalizeError(error, "CaptureFailed");
271
- }
272
- }
273
-
274
- async click(target, x, y, options) {
275
- this.#ensureOpen();
276
- this.#ensureForeground(target, options);
277
- const native = this.#nativeForCapturedTarget(target);
278
- const count = Math.max(1, options?.count ?? 1);
279
- const button = legacyButton(options?.button ?? "left");
280
- const point = { x: Math.round(x), y: Math.round(y), keys: options?.modifiers ?? [] };
281
- const actions =
282
- count === 2 && button === "left"
283
- ? [{ type: "double_click", ...point }]
284
- : Array.from({ length: count }, () => ({ type: "click", ...point, button }));
285
- await this.#execute(actions, target, native);
286
- }
287
-
288
- async moveMouse(target, x, y, options) {
289
- this.#ensureOpen();
290
- this.#ensureForeground(target, options);
291
- await this.#execute(
292
- { type: "move", x: Math.round(x), y: Math.round(y), keys: options?.modifiers ?? [] },
293
- target,
294
- this.#nativeForCapturedTarget(target),
295
- );
296
- }
297
-
298
- async drag(target, path, options) {
299
- this.#ensureOpen();
300
- this.#ensureForeground(target, options);
301
- await this.#execute(
302
- { type: "drag", path: path.map(legacyPoint), keys: options?.modifiers ?? [] },
303
- target,
304
- this.#nativeForCapturedTarget(target),
305
- );
306
- }
307
-
308
- async scroll(target, x, y, dx, dy, options) {
309
- this.#ensureOpen();
310
- this.#ensureForeground(target, options);
311
- await this.#execute(
312
- {
313
- type: "scroll",
314
- x: Math.round(x),
315
- y: Math.round(y),
316
- scroll_x: Math.round(dx),
317
- scroll_y: Math.round(dy),
318
- keys: options?.modifiers ?? [],
319
- },
320
- target,
321
- this.#nativeForCapturedTarget(target),
22
+ const prototype = NativeDesktopSession?.prototype;
23
+ const missing = REQUIRED_METHODS.filter(method => typeof prototype?.[method] !== "function");
24
+ if (missing.length === 0) {
25
+ return NativeDesktopSession;
26
+ }
27
+ // The shared native entrypoint is also imported by non-desktop tools. Keep
28
+ // their imports usable, but never emulate the new ABI with unsafe fallbacks.
29
+ return class UnsupportedDesktopSession {
30
+ constructor() {
31
+ throw new Error(
32
+ `Unsupported: desktop native addon is outdated (missing ${missing.join(", ")}). Update the native addon or run bun run build:native from the repository root.`,
322
33
  );
323
34
  }
324
-
325
- async typeText(target, text, options) {
326
- this.#ensureOpen();
327
- this.#ensureForeground(target, options);
328
- await this.#execute({ type: "type", text }, target, this.#capturedTargets.get(target)?.native ?? this.#native);
329
- }
330
-
331
- async keyChord(target, keys, options) {
332
- this.#ensureOpen();
333
- this.#ensureForeground(target, options);
334
- await this.#execute({ type: "keypress", keys }, target, this.#capturedTargets.get(target)?.native ?? this.#native);
335
- }
336
-
337
- async raiseWindow() {
338
- this.#ensureOpen();
339
- throw desktopError("BackgroundUnavailable", "the installed native addon does not support window control");
340
- }
341
-
342
- async axSnapshot() {
343
- this.#ensureOpen();
344
- throw desktopError("AxUnsupported", "accessibility is unavailable in the installed native addon");
345
- }
346
-
347
- async axQuery() {
348
- return this.axSnapshot();
349
- }
350
-
351
- async axElementAt() {
352
- return this.axSnapshot();
353
- }
354
-
355
- async axFocused() {
356
- return this.axSnapshot();
357
- }
358
-
359
- async axNode() {
360
- return this.axSnapshot();
361
- }
362
-
363
- async axAttributes() {
364
- return this.axSnapshot();
365
- }
366
-
367
- async axChildren() {
368
- return this.axSnapshot();
369
- }
370
-
371
- async axParent() {
372
- return this.axSnapshot();
373
- }
374
-
375
- async axPerform() {
376
- return this.axSnapshot();
377
- }
378
-
379
- async axSetValue() {
380
- return this.axSnapshot();
381
- }
382
-
383
- async axFocus() {
384
- return this.axSnapshot();
385
- }
386
-
387
- async axClick() {
388
- return this.axSnapshot();
389
- }
390
-
391
- async close() {
392
- if (this.#closed) return;
393
- this.#closed = true;
394
- try {
395
- await Promise.all([...this.#sessions.values()].map(native => native.close()));
396
- } catch (error) {
397
- throw normalizeError(error, "Internal");
398
- }
399
- }
400
- }
401
-
402
- ADAPTED_SESSION_CLASSES.set(NativeDesktopSession, DesktopSession);
403
- return DesktopSession;
35
+ };
404
36
  }
@@ -1,6 +1,5 @@
1
-
2
- // AUTOGENERATED FILE -- DO NOT EDIT DIRECTLY
3
- // See scripts/embed-native.ts
1
+ // Always null on disk. Standalone binary builds replace this module in memory
2
+ // with the target's manifest and one zstd frame per addon (scripts/embed-native.ts).
4
3
 
5
4
  /** @typedef {"modern" | "baseline" | "default"} EmbeddedAddonVariant */
6
5
 
@@ -8,15 +7,8 @@
8
7
  * @typedef {Object} EmbeddedAddonFile
9
8
  * @property {EmbeddedAddonVariant} variant
10
9
  * @property {string} filename
11
- * @property {number} size
12
- * @property {string=} filePath
13
- */
14
-
15
- /**
16
- * @typedef {Object} EmbeddedAddonArchive
17
- * @property {"tar.gz"} format
18
- * @property {string} filename
19
- * @property {string} filePath
10
+ * @property {number} size Decompressed `.node` size in bytes.
11
+ * @property {string} zstdPath Embedded zstd frame holding the addon bytes.
20
12
  */
21
13
 
22
14
  /**
@@ -24,7 +16,6 @@
24
16
  * @property {string} platformTag
25
17
  * @property {string} version
26
18
  * @property {EmbeddedAddonFile[]} files
27
- * @property {EmbeddedAddonArchive=} archive
28
19
  */
29
20
 
30
21
  /** @type {EmbeddedAddon|null} */