@nyte-ai/host 0.0.18 → 0.0.21
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/THIRD-PARTY-NOTICES.md +66 -0
- package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
- package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-3a6tst4d.js} +1 -1
- package/dist/chunks/{canvas-preview-m02v4yy6.js → canvas-preview-55s0y68g.js} +3 -2
- package/dist/chunks/{canvas-preview-xn10d4f8.js → canvas-preview-5ekjx6sw.js} +9 -4
- package/dist/chunks/{canvas-preview-g3qj3y3k.js → canvas-preview-8hr08aeh.js} +13 -3
- package/dist/chunks/{canvas-preview-75db60xy.js → canvas-preview-8xc0vn7v.js} +5 -4
- package/dist/chunks/{canvas-preview-zn2d0k9d.js → canvas-preview-af86jz8t.js} +14 -6
- package/dist/chunks/{canvas-preview-d9wwhbtz.js → canvas-preview-aq5j4p25.js} +1 -1
- package/dist/chunks/{canvas-preview-ewgspmsd.js → canvas-preview-b3r1tna2.js} +1 -1
- package/dist/chunks/canvas-preview-befkjea2.js +60 -0
- package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
- package/dist/chunks/{canvas-preview-senpjwep.js → canvas-preview-crmzxxt8.js} +3 -3
- package/dist/chunks/{canvas-preview-ymgz0937.js → canvas-preview-drctegz4.js} +8 -4
- package/dist/chunks/{canvas-preview-yrfz2ak6.js → canvas-preview-e3vnkkc5.js} +6 -6
- package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
- package/dist/chunks/{canvas-preview-0nrnbt76.js → canvas-preview-eehzfefa.js} +199 -125
- package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-geg2gckp.js} +8 -6
- package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-gh7c9831.js} +20 -16
- package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-k2xaz0ym.js} +10 -5
- package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
- package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-mstbvwz3.js} +26 -11
- package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
- package/dist/chunks/{canvas-preview-ehf3vp31.js → canvas-preview-vm9zdp2t.js} +45 -58
- package/dist/chunks/{canvas-preview-76fwvd6e.js → canvas-preview-w54ymtb3.js} +1 -1
- package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
- package/dist/chunks/{canvas-preview-vax9ne4e.js → canvas-preview-y7kq0c8q.js} +22 -11
- package/dist/chunks/{canvas-preview-98aj5p34.js → canvas-preview-ycepb05s.js} +1 -1
- package/dist/src/canvas-preview.js +7 -3
- package/dist/src/deploy/cloudflare/worker.js +1 -1
- package/dist/src/deploy/vercel/server/routes/[...].js +2 -2
- package/dist/src/deploy/vercel/server/utils/host.js +1 -1
- package/dist/src/deploy/vercel/server/workflows/drive.js +3 -3
- package/dist/src/images/exif.d.ts +1 -0
- package/dist/src/images/exif.js +6 -0
- package/dist/src/images/node.d.ts +37 -0
- package/dist/src/images/node.js +12 -0
- package/dist/src/images/photon.d.ts +3 -0
- package/dist/src/images/photon.js +7 -0
- package/dist/src/images/worker.d.ts +1 -0
- package/dist/src/images/worker.js +23 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +23 -22
- package/dist/src/listen-fetch.js +3 -3
- package/dist/src/listen.js +4 -4
- package/dist/src/local/command.d.ts +5 -0
- package/dist/src/local/command.js +6 -2
- package/dist/src/local/files.js +2 -2
- package/dist/src/local/git-watch.d.ts +1 -1
- package/dist/src/local/git-watch.js +1 -1
- package/dist/src/local/git.js +3 -3
- package/dist/src/local/github.js +2 -2
- package/dist/src/local/jobs.d.ts +1 -1
- package/dist/src/local/jobs.js +2 -1
- package/dist/src/local/provider.js +9 -9
- package/dist/src/local/snapshots.js +2 -2
- package/dist/src/local/worktrees.js +2 -2
- package/dist/src/mcp/connect.d.ts +0 -13
- package/dist/src/mcp/connect.js +1 -1
- package/dist/src/mcp/hub.js +2 -2
- package/dist/src/open.d.ts +2 -30
- package/dist/src/open.js +20 -18
- package/dist/src/serve/binding.js +1 -1
- package/dist/src/serve/handler.js +2 -2
- package/dist/src/sqlite.d.ts +2 -1
- package/dist/src/sqlite.js +4 -3
- package/dist/src/store-usage.js +4 -1
- package/dist/src/usage-history.d.ts +54 -0
- package/dist/src/usage-history.js +14 -0
- package/dist/src/usage-service.d.ts +2 -1
- package/dist/src/usage-service.js +7 -3
- package/dist/src/usage-worker.js +8 -4
- package/dist/src/usage.d.ts +150 -27
- package/dist/src/usage.js +12 -2
- package/package.json +16 -8
- package/src/canvas-preview.ts +4 -3
- package/src/images/exif.ts +69 -0
- package/src/images/node.ts +163 -0
- package/src/images/photon.ts +141 -0
- package/src/images/worker.ts +29 -0
- package/src/index.ts +19 -1
- package/src/local/command.ts +64 -12
- package/src/local/files.ts +8 -3
- package/src/local/git-watch.ts +16 -6
- package/src/local/git.ts +7 -2
- package/src/local/github.ts +3 -4
- package/src/local/jobs.ts +55 -69
- package/src/local/snapshots.ts +4 -2
- package/src/local/worktrees.ts +4 -4
- package/src/mcp/connect.ts +1 -14
- package/src/mcp/hub.ts +18 -9
- package/src/open.ts +16 -36
- package/src/serve/binding.ts +4 -3
- package/src/sqlite.ts +46 -9
- package/src/store-usage.ts +5 -2
- package/src/usage-history.ts +219 -0
- package/src/usage-service.ts +12 -3
- package/src/usage.ts +251 -176
- package/dist/chunks/canvas-preview-a3yw0j70.js +0 -72
- package/dist/chunks/canvas-preview-azg56359.js +0 -68
- package/dist/src/store-owner.d.ts +0 -15
- package/dist/src/store-owner.js +0 -9
- package/src/store-owner.ts +0 -98
package/dist/src/usage.js
CHANGED
|
@@ -2,12 +2,22 @@ import {
|
|
|
2
2
|
readClaudeCodeUsage2,
|
|
3
3
|
readCodexUsage2,
|
|
4
4
|
createUsageScanCaches2,
|
|
5
|
+
loadUsageScanCaches2,
|
|
6
|
+
saveUsageScanCaches2,
|
|
5
7
|
readLocalUsage2
|
|
6
|
-
} from "../chunks/canvas-preview-
|
|
8
|
+
} from "../chunks/canvas-preview-eehzfefa.js";
|
|
9
|
+
import"../chunks/canvas-preview-geg2gckp.js";
|
|
10
|
+
import"../chunks/canvas-preview-kp75gb63.js";
|
|
7
11
|
import"../chunks/canvas-preview-srgfpdmf.js";
|
|
12
|
+
import"../chunks/canvas-preview-mstbvwz3.js";
|
|
13
|
+
import"../chunks/canvas-preview-zaaj99wd.js";
|
|
14
|
+
import"../chunks/canvas-preview-0cmbkp2j.js";
|
|
15
|
+
import"../chunks/canvas-preview-69fkk9yn.js";
|
|
8
16
|
export {
|
|
9
17
|
createUsageScanCaches2 as createUsageScanCaches,
|
|
18
|
+
loadUsageScanCaches2 as loadUsageScanCaches,
|
|
10
19
|
readClaudeCodeUsage2 as readClaudeCodeUsage,
|
|
11
20
|
readCodexUsage2 as readCodexUsage,
|
|
12
|
-
readLocalUsage2 as readLocalUsage
|
|
21
|
+
readLocalUsage2 as readLocalUsage,
|
|
22
|
+
saveUsageScanCaches2 as saveUsageScanCaches
|
|
13
23
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nyte-ai/host",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
"types": "./dist/src/identity.d.ts",
|
|
37
37
|
"import": "./dist/src/identity.js"
|
|
38
38
|
},
|
|
39
|
+
"./images": {
|
|
40
|
+
"types": "./dist/src/images/node.d.ts",
|
|
41
|
+
"import": "./dist/src/images/node.js"
|
|
42
|
+
},
|
|
43
|
+
"./image-worker": {
|
|
44
|
+
"types": "./dist/src/images/worker.d.ts",
|
|
45
|
+
"import": "./dist/src/images/worker.js"
|
|
46
|
+
},
|
|
39
47
|
"./listen-fetch": {
|
|
40
48
|
"types": "./dist/src/listen-fetch.d.ts",
|
|
41
49
|
"import": "./dist/src/listen-fetch.js"
|
|
@@ -88,11 +96,12 @@
|
|
|
88
96
|
"dependencies": {
|
|
89
97
|
"@earendil-works/pi-mcp": "1.3.0",
|
|
90
98
|
"@lydell/node-pty": "1.2.0-beta.15",
|
|
91
|
-
"@nyte-ai/ai": "0.0.
|
|
92
|
-
"@nyte-ai/core": "0.0.
|
|
93
|
-
"@nyte-ai/plugin": "0.0.
|
|
94
|
-
"@nyte-ai/protocol": "0.0.
|
|
95
|
-
"@nyte-ai/schema": "0.0.
|
|
99
|
+
"@nyte-ai/ai": "0.0.21",
|
|
100
|
+
"@nyte-ai/core": "0.0.21",
|
|
101
|
+
"@nyte-ai/plugin": "0.0.21",
|
|
102
|
+
"@nyte-ai/protocol": "0.0.21",
|
|
103
|
+
"@nyte-ai/schema": "0.0.21",
|
|
104
|
+
"@silvia-odwyer/photon": "0.3.3",
|
|
96
105
|
"fflate": "0.8.3",
|
|
97
106
|
"ignore": "7.0.9",
|
|
98
107
|
"pg": "8.23.0",
|
|
@@ -126,10 +135,9 @@
|
|
|
126
135
|
},
|
|
127
136
|
"scripts": {
|
|
128
137
|
"clean": "rm -rf node_modules .turbo",
|
|
129
|
-
"typecheck": "
|
|
138
|
+
"typecheck": "tsrs --noEmit && tsrs --noEmit -p src/deploy/vercel",
|
|
130
139
|
"test": "vitest --run",
|
|
131
140
|
"check:store": "node scripts/check-store.ts",
|
|
132
|
-
"compact:store": "node scripts/compact-store.ts",
|
|
133
141
|
"build:package": "bun ../../scripts/sdk-packages.mjs build host"
|
|
134
142
|
}
|
|
135
143
|
}
|
package/src/canvas-preview.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
import { Type } from "typebox";
|
|
28
28
|
import type { TSchema } from "typebox";
|
|
29
29
|
import { Value } from "typebox/value";
|
|
30
|
+
import { killTree } from "./local/command.ts";
|
|
30
31
|
|
|
31
32
|
const ORIGIN = "https://canvas.preview/";
|
|
32
33
|
|
|
@@ -312,12 +313,12 @@ function stop(child: ChildProcess): Promise<void> {
|
|
|
312
313
|
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
313
314
|
|
|
314
315
|
return new Promise((resolve) => {
|
|
315
|
-
const kill = setTimeout(() => child
|
|
316
|
+
const kill = setTimeout(() => killTree(child, "SIGKILL"), 2000);
|
|
316
317
|
child.once("exit", () => {
|
|
317
318
|
clearTimeout(kill);
|
|
318
319
|
resolve();
|
|
319
320
|
});
|
|
320
|
-
child
|
|
321
|
+
killTree(child, "SIGTERM");
|
|
321
322
|
});
|
|
322
323
|
}
|
|
323
324
|
|
|
@@ -396,7 +397,7 @@ export function chromiumCanvasPreview(
|
|
|
396
397
|
}
|
|
397
398
|
} finally {
|
|
398
399
|
await stop(child);
|
|
399
|
-
await rm(profile, { recursive: true, force: true });
|
|
400
|
+
await rm(profile, { recursive: true, force: true, maxRetries: 10 });
|
|
400
401
|
}
|
|
401
402
|
};
|
|
402
403
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const ORIENTATION_TAG = 0x0112;
|
|
2
|
+
|
|
3
|
+
export function exifOrientation(bytes: Uint8Array): number {
|
|
4
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
5
|
+
const tiff = tiffStart(bytes, view);
|
|
6
|
+
|
|
7
|
+
if (tiff < 0 || tiff + 8 > bytes.length) return 1;
|
|
8
|
+
const little = bytes[tiff] === 0x49 && bytes[tiff + 1] === 0x49;
|
|
9
|
+
const directory = tiff + view.getUint32(tiff + 4, little);
|
|
10
|
+
|
|
11
|
+
if (directory + 2 > bytes.length) return 1;
|
|
12
|
+
const entries = view.getUint16(directory, little);
|
|
13
|
+
|
|
14
|
+
for (let index = 0; index < entries; index++) {
|
|
15
|
+
const entry = directory + 2 + index * 12;
|
|
16
|
+
|
|
17
|
+
if (entry + 12 > bytes.length) return 1;
|
|
18
|
+
|
|
19
|
+
if (view.getUint16(entry, little) === ORIENTATION_TAG) {
|
|
20
|
+
const value = view.getUint16(entry + 8, little);
|
|
21
|
+
|
|
22
|
+
return value >= 1 && value <= 8 ? value : 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tiffStart(bytes: Uint8Array, view: DataView): number {
|
|
30
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
31
|
+
for (let offset = 2; offset + 4 <= bytes.length;) {
|
|
32
|
+
if (bytes[offset] !== 0xff) return -1;
|
|
33
|
+
const marker = view.getUint8(offset + 1);
|
|
34
|
+
|
|
35
|
+
if (marker === 0xff) {
|
|
36
|
+
offset++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (marker === 0xe1 && startsExif(bytes, offset + 4)) return offset + 10;
|
|
41
|
+
offset += 2 + view.getUint16(offset + 2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (ascii(bytes, 0, "RIFF") && ascii(bytes, 8, "WEBP")) {
|
|
48
|
+
for (let offset = 12; offset + 8 <= bytes.length;) {
|
|
49
|
+
const size = view.getUint32(offset + 4, true);
|
|
50
|
+
|
|
51
|
+
if (ascii(bytes, offset, "EXIF"))
|
|
52
|
+
return startsExif(bytes, offset + 8) ? offset + 14 : offset + 8;
|
|
53
|
+
offset += 8 + size + (size % 2);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return -1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function startsExif(bytes: Uint8Array, offset: number): boolean {
|
|
61
|
+
return ascii(bytes, offset, "Exif") && bytes[offset + 4] === 0 && bytes[offset + 5] === 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ascii(bytes: Uint8Array, offset: number, text: string): boolean {
|
|
65
|
+
for (let index = 0; index < text.length; index++)
|
|
66
|
+
if (bytes[offset + index] !== text.charCodeAt(index)) return false;
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { Worker } from "node:worker_threads";
|
|
4
|
+
import type { ImageLimits, ImageProcessor, PreparedImage } from "@nyte-ai/core";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { Value } from "typebox/value";
|
|
7
|
+
import { photonImages } from "./photon.ts";
|
|
8
|
+
|
|
9
|
+
export interface NodeImagesOptions {
|
|
10
|
+
readonly worker?: string | URL;
|
|
11
|
+
readonly wasm?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PrepareRequest {
|
|
15
|
+
readonly id: number;
|
|
16
|
+
readonly bytes: Uint8Array;
|
|
17
|
+
readonly mimeType: string;
|
|
18
|
+
readonly limits: ImageLimits;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const size = Type.Object({ width: Type.Number(), height: Type.Number() });
|
|
22
|
+
|
|
23
|
+
export const prepareReply = Type.Union([
|
|
24
|
+
Type.Object({ id: Type.Integer(), result: Type.Null() }),
|
|
25
|
+
Type.Object({
|
|
26
|
+
id: Type.Integer(),
|
|
27
|
+
result: Type.Object({
|
|
28
|
+
data: Type.String(),
|
|
29
|
+
mimeType: Type.String(),
|
|
30
|
+
resized: Type.Optional(Type.Object({ from: size, to: size })),
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
Type.Object({ id: Type.Integer(), error: Type.String() }),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const IDLE_MS = 30_000;
|
|
37
|
+
|
|
38
|
+
export function nodeImages(options: NodeImagesOptions = {}): ImageProcessor {
|
|
39
|
+
const worker =
|
|
40
|
+
options.worker ??
|
|
41
|
+
new URL(import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js", import.meta.url);
|
|
42
|
+
|
|
43
|
+
let wasm: Promise<Uint8Array> | undefined;
|
|
44
|
+
|
|
45
|
+
const loadWasm = (): Promise<Uint8Array> =>
|
|
46
|
+
(wasm ??= readFile(options.wasm ?? resolvePhotonWasmPath()));
|
|
47
|
+
|
|
48
|
+
let thread: PhotonThread | undefined;
|
|
49
|
+
let inProcess: ImageProcessor | undefined;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
async prepare(bytes, mimeType, limits) {
|
|
53
|
+
if (inProcess !== undefined) return inProcess.prepare(bytes, mimeType, limits);
|
|
54
|
+
const module = await loadWasm();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
thread ??= new PhotonThread(worker, module, (ended) => {
|
|
58
|
+
if (thread === ended) thread = undefined;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return await thread.prepare(bytes, mimeType, limits);
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
if (!(cause instanceof WorkerUnavailable)) throw cause;
|
|
64
|
+
inProcess ??= photonImages(module);
|
|
65
|
+
|
|
66
|
+
return inProcess.prepare(bytes, mimeType, limits);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolvePhotonWasmPath(): string {
|
|
73
|
+
return fileURLToPath(import.meta.resolve("@silvia-odwyer/photon/photon_rs_bg.wasm"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class WorkerUnavailable extends Error {}
|
|
77
|
+
|
|
78
|
+
class PhotonThread {
|
|
79
|
+
readonly #worker: Worker;
|
|
80
|
+
readonly #pending = new Map<number, PromiseWithResolvers<PreparedImage | undefined>>();
|
|
81
|
+
readonly #onExit: (ended: PhotonThread) => void;
|
|
82
|
+
#nextId = 0;
|
|
83
|
+
#idle: ReturnType<typeof setTimeout> | undefined;
|
|
84
|
+
#started = false;
|
|
85
|
+
|
|
86
|
+
constructor(entry: string | URL, wasm: Uint8Array, onExit: (ended: PhotonThread) => void) {
|
|
87
|
+
this.#onExit = onExit;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
this.#worker = new Worker(entry, { workerData: wasm });
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
throw new WorkerUnavailable("The image worker cannot start", { cause });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.#worker.on("message", (reply: unknown) => {
|
|
96
|
+
if (!Value.Check(prepareReply, reply)) {
|
|
97
|
+
this.#fail(new Error("Malformed image worker reply"));
|
|
98
|
+
void this.#worker.terminate();
|
|
99
|
+
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.#started = true;
|
|
104
|
+
const request = this.#pending.get(reply.id);
|
|
105
|
+
|
|
106
|
+
if (request === undefined) return;
|
|
107
|
+
this.#pending.delete(reply.id);
|
|
108
|
+
|
|
109
|
+
if ("error" in reply) request.reject(new Error(reply.error));
|
|
110
|
+
else request.resolve(reply.result ?? undefined);
|
|
111
|
+
this.#settled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.#worker.on("error", (cause) => this.#fail(cause));
|
|
115
|
+
|
|
116
|
+
this.#worker.on("exit", (code) =>
|
|
117
|
+
this.#fail(new Error(`The image worker exited with code ${String(code)}`)),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
prepare(
|
|
122
|
+
bytes: Uint8Array,
|
|
123
|
+
mimeType: string,
|
|
124
|
+
limits: ImageLimits,
|
|
125
|
+
): Promise<PreparedImage | undefined> {
|
|
126
|
+
const id = this.#nextId++;
|
|
127
|
+
const request = Promise.withResolvers<PreparedImage | undefined>();
|
|
128
|
+
this.#pending.set(id, request);
|
|
129
|
+
clearTimeout(this.#idle);
|
|
130
|
+
this.#idle = undefined;
|
|
131
|
+
this.#worker.ref();
|
|
132
|
+
const copy = bytes.slice();
|
|
133
|
+
|
|
134
|
+
this.#worker.postMessage({ id, bytes: copy, mimeType, limits } satisfies PrepareRequest, [
|
|
135
|
+
copy.buffer,
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
return request.promise;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#settled(): void {
|
|
142
|
+
if (this.#pending.size > 0) return;
|
|
143
|
+
this.#worker.unref();
|
|
144
|
+
|
|
145
|
+
this.#idle = setTimeout(() => {
|
|
146
|
+
this.#onExit(this);
|
|
147
|
+
void this.#worker.terminate();
|
|
148
|
+
}, IDLE_MS);
|
|
149
|
+
|
|
150
|
+
this.#idle.unref();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#fail(cause: unknown): void {
|
|
154
|
+
const failure = this.#started
|
|
155
|
+
? cause
|
|
156
|
+
: new WorkerUnavailable("The image worker cannot start", { cause });
|
|
157
|
+
|
|
158
|
+
for (const request of this.#pending.values()) request.reject(failure);
|
|
159
|
+
this.#pending.clear();
|
|
160
|
+
clearTimeout(this.#idle);
|
|
161
|
+
this.#onExit(this);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PhotonImage,
|
|
3
|
+
SamplingFilter,
|
|
4
|
+
fliph,
|
|
5
|
+
flipv,
|
|
6
|
+
initSync,
|
|
7
|
+
resize,
|
|
8
|
+
} from "@silvia-odwyer/photon/photon_rs.js";
|
|
9
|
+
import type { SyncInitInput } from "@silvia-odwyer/photon/photon_rs.js";
|
|
10
|
+
import type { ImageLimits, ImageProcessor, PreparedImage } from "@nyte-ai/core";
|
|
11
|
+
import { exifOrientation } from "./exif.ts";
|
|
12
|
+
|
|
13
|
+
const INLINE_IMAGE_TYPES: ReadonlySet<string> = new Set([
|
|
14
|
+
"image/png",
|
|
15
|
+
"image/jpeg",
|
|
16
|
+
"image/gif",
|
|
17
|
+
"image/webp",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const JPEG_QUALITIES = [80, 70, 55, 40];
|
|
21
|
+
|
|
22
|
+
export function photonImages(wasm: SyncInitInput): ImageProcessor {
|
|
23
|
+
initSync({ module: wasm });
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
prepare: (bytes, mimeType, limits) => Promise.resolve(prepare(bytes, mimeType, limits)),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function prepare(
|
|
31
|
+
bytes: Uint8Array,
|
|
32
|
+
mimeType: string,
|
|
33
|
+
limits: ImageLimits,
|
|
34
|
+
): PreparedImage | undefined {
|
|
35
|
+
const orientation = exifOrientation(bytes);
|
|
36
|
+
let image: PhotonImage | undefined;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
image = PhotonImage.new_from_byteslice(bytes);
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
INLINE_IMAGE_TYPES.has(mimeType) &&
|
|
43
|
+
orientation === 1 &&
|
|
44
|
+
image.get_width() <= limits.maxWidth &&
|
|
45
|
+
image.get_height() <= limits.maxHeight &&
|
|
46
|
+
base64Length(bytes.byteLength) <= limits.maxBase64Bytes
|
|
47
|
+
)
|
|
48
|
+
return { data: bytes.toBase64(), mimeType };
|
|
49
|
+
|
|
50
|
+
if (orientation >= 5) {
|
|
51
|
+
const upright = transposed(image, orientation);
|
|
52
|
+
image.free();
|
|
53
|
+
image = undefined;
|
|
54
|
+
image = new PhotonImage(upright.pixels, upright.width, upright.height);
|
|
55
|
+
} else if (orientation !== 1) {
|
|
56
|
+
if (orientation !== 4) fliph(image);
|
|
57
|
+
|
|
58
|
+
if (orientation !== 2) flipv(image);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const width = image.get_width();
|
|
62
|
+
const height = image.get_height();
|
|
63
|
+
const scale = Math.min(1, limits.maxWidth / width, limits.maxHeight / height);
|
|
64
|
+
|
|
65
|
+
let target = {
|
|
66
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
67
|
+
height: Math.max(1, Math.round(height * scale)),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (;;) {
|
|
71
|
+
const encoded = encodeWithin(image, target, limits.maxBase64Bytes, mimeType === "image/jpeg");
|
|
72
|
+
|
|
73
|
+
if (encoded !== undefined)
|
|
74
|
+
return target.width === width && target.height === height
|
|
75
|
+
? encoded
|
|
76
|
+
: { ...encoded, resized: { from: { width, height }, to: target } };
|
|
77
|
+
|
|
78
|
+
if (target.width === 1 && target.height === 1) return undefined;
|
|
79
|
+
target = {
|
|
80
|
+
width: Math.max(1, Math.floor(target.width * 0.75)),
|
|
81
|
+
height: Math.max(1, Math.floor(target.height * 0.75)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
} finally {
|
|
87
|
+
image?.free();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function encodeWithin(
|
|
92
|
+
image: PhotonImage,
|
|
93
|
+
size: { readonly width: number; readonly height: number },
|
|
94
|
+
maxBase64Bytes: number,
|
|
95
|
+
jpegFirst: boolean,
|
|
96
|
+
): { readonly data: string; readonly mimeType: string } | undefined {
|
|
97
|
+
const same = size.width === image.get_width() && size.height === image.get_height();
|
|
98
|
+
const scaled = same ? image : resize(image, size.width, size.height, SamplingFilter.Lanczos3);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const png = { mimeType: "image/png", encode: () => scaled.get_bytes() };
|
|
102
|
+
|
|
103
|
+
const jpegs = JPEG_QUALITIES.map((quality) => ({
|
|
104
|
+
mimeType: "image/jpeg",
|
|
105
|
+
encode: () => scaled.get_bytes_jpeg(quality),
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
for (const candidate of jpegFirst ? [...jpegs, png] : [png, ...jpegs]) {
|
|
109
|
+
const encoded = candidate.encode();
|
|
110
|
+
|
|
111
|
+
if (base64Length(encoded.byteLength) <= maxBase64Bytes)
|
|
112
|
+
return { data: encoded.toBase64(), mimeType: candidate.mimeType };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return undefined;
|
|
116
|
+
} finally {
|
|
117
|
+
if (!same) scaled.free();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function transposed(image: PhotonImage, orientation: number) {
|
|
122
|
+
const width = image.get_width();
|
|
123
|
+
const height = image.get_height();
|
|
124
|
+
const source = image.get_raw_pixels();
|
|
125
|
+
const pixels = new Uint8Array(source.length);
|
|
126
|
+
|
|
127
|
+
for (let y = 0; y < height; y++) {
|
|
128
|
+
for (let x = 0; x < width; x++) {
|
|
129
|
+
const outX = orientation === 5 || orientation === 8 ? y : height - 1 - y;
|
|
130
|
+
const outY = orientation === 5 || orientation === 6 ? x : width - 1 - x;
|
|
131
|
+
const from = (y * width + x) * 4;
|
|
132
|
+
pixels.set(source.subarray(from, from + 4), (outY * height + outX) * 4);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { pixels, width: height, height: width };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function base64Length(byteLength: number): number {
|
|
140
|
+
return Math.ceil(byteLength / 3) * 4;
|
|
141
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import type { Static } from "typebox";
|
|
3
|
+
import type { PrepareRequest, prepareReply } from "./node.ts";
|
|
4
|
+
import { photonImages } from "./photon.ts";
|
|
5
|
+
|
|
6
|
+
const port = parentPort;
|
|
7
|
+
|
|
8
|
+
if (port === null) throw new Error("The image worker needs a parent thread");
|
|
9
|
+
|
|
10
|
+
const wasm: unknown = workerData;
|
|
11
|
+
|
|
12
|
+
if (!(wasm instanceof Uint8Array)) throw new Error("The image worker needs Photon's WebAssembly");
|
|
13
|
+
|
|
14
|
+
const images = photonImages(wasm);
|
|
15
|
+
|
|
16
|
+
port.on("message", (request: PrepareRequest) => {
|
|
17
|
+
void images.prepare(request.bytes, request.mimeType, request.limits).then(
|
|
18
|
+
(result) =>
|
|
19
|
+
port.postMessage({
|
|
20
|
+
id: request.id,
|
|
21
|
+
result: result ?? null,
|
|
22
|
+
} satisfies Static<typeof prepareReply>),
|
|
23
|
+
(cause: unknown) =>
|
|
24
|
+
port.postMessage({
|
|
25
|
+
id: request.id,
|
|
26
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
27
|
+
} satisfies Static<typeof prepareReply>),
|
|
28
|
+
);
|
|
29
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
export * from "./credentials.ts";
|
|
2
|
+
|
|
2
3
|
export * from "./identity.ts";
|
|
4
|
+
|
|
3
5
|
export { identitySigner } from "./identity-key.ts";
|
|
6
|
+
|
|
4
7
|
export * from "./listen.ts";
|
|
8
|
+
|
|
5
9
|
export * from "./listen-fetch.ts";
|
|
10
|
+
|
|
6
11
|
export * from "./local/files.ts";
|
|
12
|
+
|
|
7
13
|
export * from "./local/git.ts";
|
|
14
|
+
|
|
8
15
|
export * from "./local/github.ts";
|
|
16
|
+
|
|
9
17
|
export * from "./local/identity.ts";
|
|
18
|
+
|
|
10
19
|
export * from "./local/jobs.ts";
|
|
20
|
+
|
|
11
21
|
export * from "./local/provider.ts";
|
|
22
|
+
|
|
12
23
|
export { createMcpHub } from "./mcp/hub.ts";
|
|
24
|
+
|
|
13
25
|
export type { McpConnector } from "./mcp/connect.ts";
|
|
26
|
+
|
|
14
27
|
export * from "./local/snapshots.ts";
|
|
28
|
+
|
|
15
29
|
export * from "./local/worktrees.ts";
|
|
30
|
+
|
|
16
31
|
export * from "./open.ts";
|
|
32
|
+
|
|
17
33
|
export * from "./plugins/loader.ts";
|
|
34
|
+
|
|
18
35
|
export * from "./postgres.ts";
|
|
36
|
+
|
|
19
37
|
export * from "./secrets.ts";
|
|
38
|
+
|
|
20
39
|
export * from "./sqlite.ts";
|
|
21
|
-
export { StoreOwned } from "./store-owner.ts";
|
package/src/local/command.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
|
2
3
|
import { constants } from "node:fs";
|
|
3
4
|
import { access, stat } from "node:fs/promises";
|
|
4
|
-
import { delimiter, isAbsolute, join } from "node:path";
|
|
5
|
+
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
5
6
|
import { SdkError } from "@nyte-ai/schema";
|
|
6
7
|
import { processEnvironment } from "@nyte-ai/core";
|
|
7
8
|
|
|
@@ -25,9 +26,10 @@ export async function run(
|
|
|
25
26
|
},
|
|
26
27
|
): Promise<Ran> {
|
|
27
28
|
await processEnvironment();
|
|
29
|
+
const resolved = process.platform === "win32" ? ((await findExecutable(file)) ?? file) : file;
|
|
28
30
|
|
|
29
31
|
return new Promise((resolve, reject) => {
|
|
30
|
-
const child =
|
|
32
|
+
const child = spawnCommand(resolved, args, {
|
|
31
33
|
cwd: options.cwd,
|
|
32
34
|
env: { ...process.env, ...options.env },
|
|
33
35
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -44,7 +46,7 @@ export async function run(
|
|
|
44
46
|
child.on("error", (error) =>
|
|
45
47
|
reject(new SdkError("unavailable", `Cannot run ${file}: ${error.message}`)),
|
|
46
48
|
);
|
|
47
|
-
child.stdout
|
|
49
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
48
50
|
if (truncated) return;
|
|
49
51
|
size += chunk.byteLength;
|
|
50
52
|
|
|
@@ -58,8 +60,8 @@ export async function run(
|
|
|
58
60
|
|
|
59
61
|
stdout.push(chunk);
|
|
60
62
|
});
|
|
61
|
-
child.stderr
|
|
62
|
-
child.stdin
|
|
63
|
+
child.stderr?.on("data", (chunk: Buffer) => stderr.push(chunk));
|
|
64
|
+
child.stdin?.on("error", () => undefined);
|
|
63
65
|
child.on("close", (code) =>
|
|
64
66
|
resolve({
|
|
65
67
|
code,
|
|
@@ -68,22 +70,72 @@ export async function run(
|
|
|
68
70
|
truncated,
|
|
69
71
|
}),
|
|
70
72
|
);
|
|
71
|
-
child.stdin
|
|
73
|
+
child.stdin?.end(options.input);
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/** Spawns `file` as `spawn` does, except that on Windows a `.cmd` or `.bat` file runs through `cmd.exe` with its arguments escaped, as Node no longer starts batch files without a shell. */
|
|
78
|
+
export function spawnCommand(
|
|
79
|
+
file: string,
|
|
80
|
+
args: readonly string[],
|
|
81
|
+
options: SpawnOptions,
|
|
82
|
+
): ChildProcess {
|
|
83
|
+
if (process.platform !== "win32" || !/\.(cmd|bat)$/i.test(file))
|
|
84
|
+
return spawn(file, args, options);
|
|
85
|
+
const shim = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i.test(file);
|
|
86
|
+
const line = [escapeCommand(file), ...args.map((arg) => escapeArgument(arg, shim))].join(" ");
|
|
87
|
+
|
|
88
|
+
return spawn(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `"${line}"`], {
|
|
89
|
+
...options,
|
|
90
|
+
windowsVerbatimArguments: true,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Ends `child` with `signal`; on Windows, which has no signals, it ends the child's whole process tree with `taskkill /T /F`, as `kill` alone would leave what a batch file or a shell started running. */
|
|
95
|
+
export function killTree(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
96
|
+
if (process.platform !== "win32" || child.pid === undefined) {
|
|
97
|
+
child.kill(signal);
|
|
98
|
+
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
103
|
+
stdio: "ignore",
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
}).on("error", () => child.kill(signal));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
109
|
+
|
|
110
|
+
function escapeCommand(command: string): string {
|
|
111
|
+
return command.replace(CMD_META, "^$1");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function escapeArgument(argument: string, twice: boolean): string {
|
|
115
|
+
const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
|
|
116
|
+
const escaped = quoted.replace(CMD_META, "^$1");
|
|
117
|
+
|
|
118
|
+
return twice ? escaped.replace(CMD_META, "^$1") : escaped;
|
|
119
|
+
}
|
|
120
|
+
|
|
75
121
|
export async function findExecutable(
|
|
76
122
|
name: string,
|
|
77
123
|
directories: readonly string[] = [],
|
|
78
124
|
): Promise<string | null> {
|
|
79
|
-
|
|
80
|
-
await processEnvironment();
|
|
81
|
-
const path = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry !== "");
|
|
125
|
+
const pathext = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter((ext) => ext !== "");
|
|
82
126
|
|
|
83
127
|
const extensions =
|
|
84
|
-
process.platform
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
process.platform !== "win32" ? [""] : extname(name) === "" ? pathext : ["", ...pathext];
|
|
129
|
+
|
|
130
|
+
if (isAbsolute(name)) {
|
|
131
|
+
for (const extension of extensions)
|
|
132
|
+
if (await executable(`${name}${extension}`)) return `${name}${extension}`;
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await processEnvironment();
|
|
138
|
+
const path = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry !== "");
|
|
87
139
|
|
|
88
140
|
for (const directory of [...directories, ...path])
|
|
89
141
|
for (const extension of extensions) {
|