@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.
Files changed (103) hide show
  1. package/THIRD-PARTY-NOTICES.md +66 -0
  2. package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
  3. package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-3a6tst4d.js} +1 -1
  4. package/dist/chunks/{canvas-preview-m02v4yy6.js → canvas-preview-55s0y68g.js} +3 -2
  5. package/dist/chunks/{canvas-preview-xn10d4f8.js → canvas-preview-5ekjx6sw.js} +9 -4
  6. package/dist/chunks/{canvas-preview-g3qj3y3k.js → canvas-preview-8hr08aeh.js} +13 -3
  7. package/dist/chunks/{canvas-preview-75db60xy.js → canvas-preview-8xc0vn7v.js} +5 -4
  8. package/dist/chunks/{canvas-preview-zn2d0k9d.js → canvas-preview-af86jz8t.js} +14 -6
  9. package/dist/chunks/{canvas-preview-d9wwhbtz.js → canvas-preview-aq5j4p25.js} +1 -1
  10. package/dist/chunks/{canvas-preview-ewgspmsd.js → canvas-preview-b3r1tna2.js} +1 -1
  11. package/dist/chunks/canvas-preview-befkjea2.js +60 -0
  12. package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
  13. package/dist/chunks/{canvas-preview-senpjwep.js → canvas-preview-crmzxxt8.js} +3 -3
  14. package/dist/chunks/{canvas-preview-ymgz0937.js → canvas-preview-drctegz4.js} +8 -4
  15. package/dist/chunks/{canvas-preview-yrfz2ak6.js → canvas-preview-e3vnkkc5.js} +6 -6
  16. package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
  17. package/dist/chunks/{canvas-preview-0nrnbt76.js → canvas-preview-eehzfefa.js} +199 -125
  18. package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-geg2gckp.js} +8 -6
  19. package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-gh7c9831.js} +20 -16
  20. package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-k2xaz0ym.js} +10 -5
  21. package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
  22. package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-mstbvwz3.js} +26 -11
  23. package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
  24. package/dist/chunks/{canvas-preview-ehf3vp31.js → canvas-preview-vm9zdp2t.js} +45 -58
  25. package/dist/chunks/{canvas-preview-76fwvd6e.js → canvas-preview-w54ymtb3.js} +1 -1
  26. package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
  27. package/dist/chunks/{canvas-preview-vax9ne4e.js → canvas-preview-y7kq0c8q.js} +22 -11
  28. package/dist/chunks/{canvas-preview-98aj5p34.js → canvas-preview-ycepb05s.js} +1 -1
  29. package/dist/src/canvas-preview.js +7 -3
  30. package/dist/src/deploy/cloudflare/worker.js +1 -1
  31. package/dist/src/deploy/vercel/server/routes/[...].js +2 -2
  32. package/dist/src/deploy/vercel/server/utils/host.js +1 -1
  33. package/dist/src/deploy/vercel/server/workflows/drive.js +3 -3
  34. package/dist/src/images/exif.d.ts +1 -0
  35. package/dist/src/images/exif.js +6 -0
  36. package/dist/src/images/node.d.ts +37 -0
  37. package/dist/src/images/node.js +12 -0
  38. package/dist/src/images/photon.d.ts +3 -0
  39. package/dist/src/images/photon.js +7 -0
  40. package/dist/src/images/worker.d.ts +1 -0
  41. package/dist/src/images/worker.js +23 -0
  42. package/dist/src/index.d.ts +0 -1
  43. package/dist/src/index.js +23 -22
  44. package/dist/src/listen-fetch.js +3 -3
  45. package/dist/src/listen.js +4 -4
  46. package/dist/src/local/command.d.ts +5 -0
  47. package/dist/src/local/command.js +6 -2
  48. package/dist/src/local/files.js +2 -2
  49. package/dist/src/local/git-watch.d.ts +1 -1
  50. package/dist/src/local/git-watch.js +1 -1
  51. package/dist/src/local/git.js +3 -3
  52. package/dist/src/local/github.js +2 -2
  53. package/dist/src/local/jobs.d.ts +1 -1
  54. package/dist/src/local/jobs.js +2 -1
  55. package/dist/src/local/provider.js +9 -9
  56. package/dist/src/local/snapshots.js +2 -2
  57. package/dist/src/local/worktrees.js +2 -2
  58. package/dist/src/mcp/connect.d.ts +0 -13
  59. package/dist/src/mcp/connect.js +1 -1
  60. package/dist/src/mcp/hub.js +2 -2
  61. package/dist/src/open.d.ts +2 -30
  62. package/dist/src/open.js +20 -18
  63. package/dist/src/serve/binding.js +1 -1
  64. package/dist/src/serve/handler.js +2 -2
  65. package/dist/src/sqlite.d.ts +2 -1
  66. package/dist/src/sqlite.js +4 -3
  67. package/dist/src/store-usage.js +4 -1
  68. package/dist/src/usage-history.d.ts +54 -0
  69. package/dist/src/usage-history.js +14 -0
  70. package/dist/src/usage-service.d.ts +2 -1
  71. package/dist/src/usage-service.js +7 -3
  72. package/dist/src/usage-worker.js +8 -4
  73. package/dist/src/usage.d.ts +150 -27
  74. package/dist/src/usage.js +12 -2
  75. package/package.json +16 -8
  76. package/src/canvas-preview.ts +4 -3
  77. package/src/images/exif.ts +69 -0
  78. package/src/images/node.ts +163 -0
  79. package/src/images/photon.ts +141 -0
  80. package/src/images/worker.ts +29 -0
  81. package/src/index.ts +19 -1
  82. package/src/local/command.ts +64 -12
  83. package/src/local/files.ts +8 -3
  84. package/src/local/git-watch.ts +16 -6
  85. package/src/local/git.ts +7 -2
  86. package/src/local/github.ts +3 -4
  87. package/src/local/jobs.ts +55 -69
  88. package/src/local/snapshots.ts +4 -2
  89. package/src/local/worktrees.ts +4 -4
  90. package/src/mcp/connect.ts +1 -14
  91. package/src/mcp/hub.ts +18 -9
  92. package/src/open.ts +16 -36
  93. package/src/serve/binding.ts +4 -3
  94. package/src/sqlite.ts +46 -9
  95. package/src/store-usage.ts +5 -2
  96. package/src/usage-history.ts +219 -0
  97. package/src/usage-service.ts +12 -3
  98. package/src/usage.ts +251 -176
  99. package/dist/chunks/canvas-preview-a3yw0j70.js +0 -72
  100. package/dist/chunks/canvas-preview-azg56359.js +0 -68
  101. package/dist/src/store-owner.d.ts +0 -15
  102. package/dist/src/store-owner.js +0 -9
  103. 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-0nrnbt76.js";
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.18",
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.18",
92
- "@nyte-ai/core": "0.0.18",
93
- "@nyte-ai/plugin": "0.0.18",
94
- "@nyte-ai/protocol": "0.0.18",
95
- "@nyte-ai/schema": "0.0.18",
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": "tsgo --noEmit && tsgo --noEmit -p src/deploy/vercel",
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
  }
@@ -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.kill("SIGKILL"), 2000);
316
+ const kill = setTimeout(() => killTree(child, "SIGKILL"), 2000);
316
317
  child.once("exit", () => {
317
318
  clearTimeout(kill);
318
319
  resolve();
319
320
  });
320
- child.kill();
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";
@@ -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 = spawn(file, args, {
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.on("data", (chunk: Buffer) => {
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.on("data", (chunk: Buffer) => stderr.push(chunk));
62
- child.stdin.on("error", () => undefined);
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.end(options.input);
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
- if (isAbsolute(name)) return (await executable(name)) ? name : null;
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 === "win32"
85
- ? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")]
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) {