@inferencesh/app 0.1.4 → 0.1.6

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/dist/file.d.ts CHANGED
@@ -22,22 +22,60 @@ export interface FileData {
22
22
  * A file in the inference.sh ecosystem.
23
23
  *
24
24
  * Accepts a URL, local path, or options object.
25
- * URLs are downloaded and cached locally on construction (via `await File.from()`).
25
+ * URLs are downloaded lazily when `getPath()` is called.
26
26
  * Local paths are resolved to absolute paths.
27
27
  *
28
+ * For API wrapper apps that only need to forward the URL, use `uri` directly
29
+ * without calling `getPath()` to avoid unnecessary downloads.
30
+ *
28
31
  * In JSON output, File serializes to `{ path, uri, content_type, size, filename }`
29
32
  * — the engine uploads local `path` files to CDN and replaces with `uri`.
30
33
  */
31
34
  export declare class File {
32
35
  uri?: string;
33
- path?: string;
34
36
  contentType?: string;
35
37
  size?: number;
36
38
  filename?: string;
39
+ private _path?;
40
+ private _resolved;
41
+ private _downloading?;
37
42
  private constructor();
43
+ /**
44
+ * Get the local file path. Downloads the file lazily if needed.
45
+ * For sync access after download, use the `path` getter.
46
+ */
47
+ getPath(): Promise<string>;
48
+ /**
49
+ * Sync access to path. Returns undefined if not yet downloaded.
50
+ * Use `getPath()` for lazy downloading.
51
+ */
52
+ get path(): string | undefined;
53
+ /**
54
+ * Check if the file has been downloaded/resolved.
55
+ */
56
+ get isResolved(): boolean;
57
+ private _resolve;
58
+ /**
59
+ * Create a lazy File from a URL or path string.
60
+ * Does NOT download immediately — download happens when `getPath()` is called.
61
+ *
62
+ * Note: This is safe in JS because `getPath()` is async. The Python SDK had to
63
+ * revert to eager download because `.path` is a sync property that blocks the
64
+ * event loop when called from async code (e.g. Playwright + httpx apps).
65
+ *
66
+ * @example
67
+ * ```js
68
+ * const file = File.lazy("https://example.com/image.jpg");
69
+ * console.log(file.uri); // Available immediately
70
+ * const path = await file.getPath(); // Downloads here
71
+ * ```
72
+ */
73
+ static lazy(input: string): File;
38
74
  /**
39
75
  * Create a File from a URL, local path, or options object.
40
- * URLs are downloaded and cached automatically.
76
+ * URLs are downloaded and cached automatically (eager loading).
77
+ *
78
+ * For lazy loading, use `File.lazy()` instead.
41
79
  *
42
80
  * @example
43
81
  * ```js
@@ -58,8 +96,14 @@ export declare class File {
58
96
  static fromPath(localPath: string): File;
59
97
  /**
60
98
  * Check if the file exists on disk.
99
+ * Note: This checks the current state without triggering download.
100
+ * Use `getPath()` first if you need to ensure the file is downloaded.
61
101
  */
62
102
  exists(): boolean;
103
+ /**
104
+ * Check if we have a local path (without triggering download).
105
+ */
106
+ isLocal(): boolean;
63
107
  /**
64
108
  * Re-read metadata (contentType, size, filename) from disk.
65
109
  */
@@ -67,6 +111,7 @@ export declare class File {
67
111
  /**
68
112
  * Serialize to a plain object for JSON output.
69
113
  * The engine reads `path` fields and uploads them to CDN.
114
+ * Note: Uses internal _path to avoid triggering download during serialization.
70
115
  */
71
116
  toJSON(): FileData;
72
117
  static getCacheDir(): string;
package/dist/file.js CHANGED
@@ -9,28 +9,120 @@ import { URL } from "node:url";
9
9
  * A file in the inference.sh ecosystem.
10
10
  *
11
11
  * Accepts a URL, local path, or options object.
12
- * URLs are downloaded and cached locally on construction (via `await File.from()`).
12
+ * URLs are downloaded lazily when `getPath()` is called.
13
13
  * Local paths are resolved to absolute paths.
14
14
  *
15
+ * For API wrapper apps that only need to forward the URL, use `uri` directly
16
+ * without calling `getPath()` to avoid unnecessary downloads.
17
+ *
15
18
  * In JSON output, File serializes to `{ path, uri, content_type, size, filename }`
16
19
  * — the engine uploads local `path` files to CDN and replaces with `uri`.
17
20
  */
18
21
  export class File {
19
22
  uri;
20
- path;
21
23
  contentType;
22
24
  size;
23
25
  filename;
26
+ _path;
27
+ _resolved = false;
28
+ _downloading;
24
29
  constructor(options) {
25
30
  this.uri = options.uri;
26
- this.path = options.path;
31
+ this._path = options.path;
27
32
  this.contentType = options.contentType;
28
33
  this.size = options.size;
29
34
  this.filename = options.filename;
30
35
  }
36
+ /**
37
+ * Get the local file path. Downloads the file lazily if needed.
38
+ * For sync access after download, use the `path` getter.
39
+ */
40
+ async getPath() {
41
+ if (this._resolved && this._path) {
42
+ return this._path;
43
+ }
44
+ // Avoid concurrent downloads
45
+ if (this._downloading) {
46
+ return this._downloading;
47
+ }
48
+ this._downloading = this._resolve();
49
+ try {
50
+ const path = await this._downloading;
51
+ return path;
52
+ }
53
+ finally {
54
+ this._downloading = undefined;
55
+ }
56
+ }
57
+ /**
58
+ * Sync access to path. Returns undefined if not yet downloaded.
59
+ * Use `getPath()` for lazy downloading.
60
+ */
61
+ get path() {
62
+ return this._path;
63
+ }
64
+ /**
65
+ * Check if the file has been downloaded/resolved.
66
+ */
67
+ get isResolved() {
68
+ return this._resolved;
69
+ }
70
+ async _resolve() {
71
+ if (this._resolved && this._path) {
72
+ return this._path;
73
+ }
74
+ if (this.uri) {
75
+ if (isDataUri(this.uri)) {
76
+ this._decodeDataUri(this.uri);
77
+ }
78
+ else if (isUrl(this.uri)) {
79
+ await this._downloadUrl(this.uri);
80
+ }
81
+ else {
82
+ // Treat as local path
83
+ this._path = resolve(this.uri);
84
+ }
85
+ }
86
+ if (this._path) {
87
+ this._path = resolve(this._path);
88
+ this._populateMetadata();
89
+ }
90
+ this._resolved = true;
91
+ if (!this._path) {
92
+ throw new Error("Failed to resolve file path");
93
+ }
94
+ return this._path;
95
+ }
96
+ /**
97
+ * Create a lazy File from a URL or path string.
98
+ * Does NOT download immediately — download happens when `getPath()` is called.
99
+ *
100
+ * Note: This is safe in JS because `getPath()` is async. The Python SDK had to
101
+ * revert to eager download because `.path` is a sync property that blocks the
102
+ * event loop when called from async code (e.g. Playwright + httpx apps).
103
+ *
104
+ * @example
105
+ * ```js
106
+ * const file = File.lazy("https://example.com/image.jpg");
107
+ * console.log(file.uri); // Available immediately
108
+ * const path = await file.getPath(); // Downloads here
109
+ * ```
110
+ */
111
+ static lazy(input) {
112
+ const file = new File({ uri: input });
113
+ // If it's a local path (not URL or data URI), resolve immediately
114
+ if (!isUrl(input) && !isDataUri(input)) {
115
+ file._path = resolve(input);
116
+ file._resolved = true;
117
+ file._populateMetadata();
118
+ }
119
+ return file;
120
+ }
31
121
  /**
32
122
  * Create a File from a URL, local path, or options object.
33
- * URLs are downloaded and cached automatically.
123
+ * URLs are downloaded and cached automatically (eager loading).
124
+ *
125
+ * For lazy loading, use `File.lazy()` instead.
34
126
  *
35
127
  * @example
36
128
  * ```js
@@ -48,7 +140,7 @@ export class File {
48
140
  if (input instanceof File) {
49
141
  return new File({
50
142
  uri: input.uri,
51
- path: input.path,
143
+ path: input._path,
52
144
  contentType: input.contentType,
53
145
  size: input.size,
54
146
  filename: input.filename,
@@ -72,26 +164,8 @@ export class File {
72
164
  throw new Error("Either 'uri' or 'path' must be provided");
73
165
  }
74
166
  const file = new File(options);
75
- // Resolve URI
76
- if (file.uri) {
77
- if (isDataUri(file.uri)) {
78
- file._decodeDataUri(file.uri);
79
- }
80
- else if (isUrl(file.uri)) {
81
- await file._downloadUrl(file.uri);
82
- }
83
- else {
84
- // Treat as local path
85
- file.path = resolve(file.uri);
86
- }
87
- }
88
- if (file.path) {
89
- file.path = resolve(file.path);
90
- file._populateMetadata();
91
- }
92
- else {
93
- throw new Error("Either 'uri' or 'path' must be provided and be valid");
94
- }
167
+ // Eagerly resolve
168
+ await file.getPath();
95
169
  return file;
96
170
  }
97
171
  /**
@@ -100,14 +174,23 @@ export class File {
100
174
  static fromPath(localPath) {
101
175
  const absPath = resolve(localPath);
102
176
  const file = new File({ path: absPath });
177
+ file._resolved = true;
103
178
  file._populateMetadata();
104
179
  return file;
105
180
  }
106
181
  /**
107
182
  * Check if the file exists on disk.
183
+ * Note: This checks the current state without triggering download.
184
+ * Use `getPath()` first if you need to ensure the file is downloaded.
108
185
  */
109
186
  exists() {
110
- return this.path != null && existsSync(this.path);
187
+ return this._path != null && existsSync(this._path);
188
+ }
189
+ /**
190
+ * Check if we have a local path (without triggering download).
191
+ */
192
+ isLocal() {
193
+ return this._path != null;
111
194
  }
112
195
  /**
113
196
  * Re-read metadata (contentType, size, filename) from disk.
@@ -118,13 +201,14 @@ export class File {
118
201
  /**
119
202
  * Serialize to a plain object for JSON output.
120
203
  * The engine reads `path` fields and uploads them to CDN.
204
+ * Note: Uses internal _path to avoid triggering download during serialization.
121
205
  */
122
206
  toJSON() {
123
207
  const result = {};
124
208
  if (this.uri != null)
125
209
  result.uri = this.uri;
126
- if (this.path != null)
127
- result.path = this.path;
210
+ if (this._path != null)
211
+ result.path = this._path;
128
212
  if (this.contentType != null)
129
213
  result.content_type = this.contentType;
130
214
  if (this.size != null)
@@ -161,7 +245,8 @@ export class File {
161
245
  if (existsSync(cacheDir)) {
162
246
  const files = require("node:fs").readdirSync(cacheDir);
163
247
  if (files.length > 0) {
164
- this.path = join(cacheDir, files[0]);
248
+ this._path = join(cacheDir, files[0]);
249
+ this._populateMetadata();
165
250
  return;
166
251
  }
167
252
  }
@@ -175,20 +260,23 @@ export class File {
175
260
  const filename = `file${ext}`;
176
261
  const cachePath = join(cacheDir, filename);
177
262
  writeFileSync(cachePath, parsed.data);
178
- this.path = cachePath;
263
+ this._path = cachePath;
264
+ this._populateMetadata();
179
265
  }
180
266
  // --- Download ---
181
267
  async _downloadUrl(url) {
182
268
  const cachePath = this._getCachePath(url);
183
269
  if (existsSync(cachePath)) {
184
- this.path = cachePath;
270
+ this._path = cachePath;
271
+ this._populateMetadata();
185
272
  return;
186
273
  }
187
274
  const tmpPath = cachePath + ".tmp";
188
275
  try {
189
276
  await downloadToFile(url, tmpPath);
190
277
  renameSync(tmpPath, cachePath);
191
- this.path = cachePath;
278
+ this._path = cachePath;
279
+ this._populateMetadata();
192
280
  }
193
281
  catch (err) {
194
282
  try {
@@ -200,19 +288,19 @@ export class File {
200
288
  }
201
289
  // --- Metadata ---
202
290
  _populateMetadata() {
203
- if (!this.path || !existsSync(this.path))
291
+ if (!this._path || !existsSync(this._path))
204
292
  return;
205
293
  if (!this.contentType) {
206
- this.contentType = guessContentType(this.path);
294
+ this.contentType = guessContentType(this._path);
207
295
  }
208
296
  if (this.size == null) {
209
297
  try {
210
- this.size = statSync(this.path).size;
298
+ this.size = statSync(this._path).size;
211
299
  }
212
300
  catch { /* ignore */ }
213
301
  }
214
302
  if (!this.filename) {
215
- this.filename = basename(this.path);
303
+ this.filename = basename(this._path);
216
304
  }
217
305
  }
218
306
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { File } from "./file.js";
2
2
  export type { FileOptions, FileData } from "./file.js";
3
+ export { createFileSchema, isFileSchema, FILE_SCHEMA_MARKER } from "./schema.js";
3
4
  export { StorageDir, ensureDir } from "./storage.js";
4
5
  export type { StorageDirValue } from "./storage.js";
5
6
  export { download } from "./download.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // File handling
2
2
  export { File } from "./file.js";
3
+ // Zod schema utilities
4
+ export { createFileSchema, isFileSchema, FILE_SCHEMA_MARKER } from "./schema.js";
3
5
  // Storage directories
4
6
  export { StorageDir, ensureDir } from "./storage.js";
5
7
  // Download utility
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Zod schema utilities for inference.sh apps.
3
+ *
4
+ * Provides a `fileSchema` that:
5
+ * - Accepts URL strings as input
6
+ * - Transforms to lazy File objects (no download until getPath() called)
7
+ * - Generates JSON Schema with format: "file"
8
+ */
9
+ /**
10
+ * Symbol to mark a schema as a file schema.
11
+ * The kernel's zodToJsonSchema will detect this and output format: "file".
12
+ */
13
+ export declare const FILE_SCHEMA_MARKER: unique symbol;
14
+ /**
15
+ * Create a file schema using the app's Zod instance.
16
+ *
17
+ * @example
18
+ * ```js
19
+ * import { z } from "zod";
20
+ * import { createFileSchema } from "@inferencesh/app";
21
+ *
22
+ * const fileSchema = createFileSchema(z);
23
+ *
24
+ * const InputSchema = z.object({
25
+ * image: fileSchema.describe("Input image"),
26
+ * });
27
+ * ```
28
+ */
29
+ export declare function createFileSchema(z: any): any;
30
+ /**
31
+ * Check if a Zod schema is a file schema (created by createFileSchema).
32
+ */
33
+ export declare function isFileSchema(schema: any): boolean;
package/dist/schema.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Zod schema utilities for inference.sh apps.
3
+ *
4
+ * Provides a `fileSchema` that:
5
+ * - Accepts URL strings as input
6
+ * - Transforms to lazy File objects (no download until getPath() called)
7
+ * - Generates JSON Schema with format: "file"
8
+ */
9
+ import { File } from "./file.js";
10
+ // We don't import zod directly to avoid version conflicts.
11
+ // Instead, apps provide their own zod and we work with the schema structure.
12
+ /**
13
+ * Symbol to mark a schema as a file schema.
14
+ * The kernel's zodToJsonSchema will detect this and output format: "file".
15
+ */
16
+ export const FILE_SCHEMA_MARKER = Symbol.for("inferencesh.fileSchema");
17
+ /**
18
+ * Create a file schema using the app's Zod instance.
19
+ *
20
+ * @example
21
+ * ```js
22
+ * import { z } from "zod";
23
+ * import { createFileSchema } from "@inferencesh/app";
24
+ *
25
+ * const fileSchema = createFileSchema(z);
26
+ *
27
+ * const InputSchema = z.object({
28
+ * image: fileSchema.describe("Input image"),
29
+ * });
30
+ * ```
31
+ */
32
+ export function createFileSchema(z) {
33
+ const schema = z.string().transform((uri) => File.lazy(uri));
34
+ // Mark as file schema for JSON schema generation
35
+ schema._def[FILE_SCHEMA_MARKER] = true;
36
+ return schema;
37
+ }
38
+ /**
39
+ * Check if a Zod schema is a file schema (created by createFileSchema).
40
+ */
41
+ export function isFileSchema(schema) {
42
+ if (!schema || typeof schema !== "object")
43
+ return false;
44
+ // Check for our marker
45
+ if (schema._def && schema._def[FILE_SCHEMA_MARKER])
46
+ return true;
47
+ // Check inner type for transforms/effects
48
+ if (schema._def?.typeName === "ZodEffects" && schema._def.schema) {
49
+ return isFileSchema(schema._def.schema);
50
+ }
51
+ return false;
52
+ }
package/dist/types.js CHANGED
@@ -1,4 +1,4 @@
1
- // Code generated by tygo. DO NOT EDIT.
1
+ // Code generated by gotypegen. DO NOT EDIT.
2
2
  export const MetaItemTypeText = "text";
3
3
  export const MetaItemTypeImage = "image";
4
4
  export const MetaItemTypeVideo = "video";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferencesh/app",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "App framework for building inference.sh apps — File handling, output metadata, storage utilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,7 +42,8 @@
42
42
  "devDependencies": {
43
43
  "@types/node": "^22.0.0",
44
44
  "rimraf": "^6.0.1",
45
- "typescript": "^5.8.3"
45
+ "typescript": "^5.8.3",
46
+ "zod": "^4.3.6"
46
47
  },
47
48
  "files": [
48
49
  "dist",