@inferencesh/app 0.1.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 +236 -0
- package/dist/download.d.ts +15 -0
- package/dist/download.js +43 -0
- package/dist/file.d.ts +76 -0
- package/dist/file.js +235 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/output-meta.d.ts +69 -0
- package/dist/output-meta.js +41 -0
- package/dist/storage.d.ts +23 -0
- package/dist/storage.js +26 -0
- package/dist/test/file.test.d.ts +1 -0
- package/dist/test/file.test.js +70 -0
- package/dist/test/output-meta.test.d.ts +1 -0
- package/dist/test/output-meta.test.js +48 -0
- package/dist/test/storage.test.d.ts +1 -0
- package/dist/test/storage.test.js +10 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# @inferencesh/app — build inference.sh apps in node.js
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@inferencesh/app)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
app framework for building [inference.sh](https://inference.sh) apps in node.js — file handling, output metadata, and storage utilities.
|
|
8
|
+
|
|
9
|
+
this is the **app-side** sdk. for the **client-side** sdk (calling apps, agents, file uploads), see [@inferencesh/sdk](https://www.npmjs.com/package/@inferencesh/sdk).
|
|
10
|
+
|
|
11
|
+
## installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @inferencesh/app
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## what's included
|
|
18
|
+
|
|
19
|
+
| export | description |
|
|
20
|
+
|--------|-------------|
|
|
21
|
+
| `File` | smart file reference — downloads urls, caches locally, resolves paths, serializes for the engine |
|
|
22
|
+
| `StorageDir` | standard storage directory constants (`DATA`, `TEMP`, `CACHE`) |
|
|
23
|
+
| `download()` | download a url to a directory with caching |
|
|
24
|
+
| `textMeta`, `imageMeta`, `videoMeta`, `audioMeta`, `rawMeta` | output metadata factories for usage-based pricing |
|
|
25
|
+
|
|
26
|
+
## file handling
|
|
27
|
+
|
|
28
|
+
the `File` class handles downloading, caching, and path resolution — the node.js equivalent of the python sdk's `File` class.
|
|
29
|
+
|
|
30
|
+
### reading input files
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { File } from "@inferencesh/app";
|
|
34
|
+
|
|
35
|
+
async run(inputData) {
|
|
36
|
+
// Input files come as URLs — File downloads and caches them
|
|
37
|
+
const file = await File.from(inputData.image);
|
|
38
|
+
console.log(file.path); // /home/.cache/inferencesh/files/abc123/image.jpg
|
|
39
|
+
console.log(file.contentType); // image/jpeg
|
|
40
|
+
console.log(file.size); // 102400
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### returning output files
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import { File } from "@inferencesh/app";
|
|
48
|
+
|
|
49
|
+
async run(inputData) {
|
|
50
|
+
const outputPath = "/tmp/result.png";
|
|
51
|
+
await generateImage(inputData.prompt, outputPath);
|
|
52
|
+
|
|
53
|
+
// File.fromPath is sync — no download needed for local files
|
|
54
|
+
return { image: File.fromPath(outputPath) };
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
the engine reads `path` from the serialized output and uploads it to cdn automatically.
|
|
59
|
+
|
|
60
|
+
### how File serializes
|
|
61
|
+
|
|
62
|
+
`File` implements `toJSON()` so it works seamlessly with `JSON.stringify`:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
const file = File.fromPath("/tmp/output.png");
|
|
66
|
+
JSON.stringify(file);
|
|
67
|
+
// {"path":"/tmp/output.png","content_type":"image/png","size":102400,"filename":"output.png"}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### construction options
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
// From URL (downloads and caches)
|
|
74
|
+
const file = await File.from("https://example.com/image.jpg");
|
|
75
|
+
|
|
76
|
+
// From local path
|
|
77
|
+
const file = await File.from("/tmp/output.png");
|
|
78
|
+
|
|
79
|
+
// From options object
|
|
80
|
+
const file = await File.from({ path: "/tmp/output.png", contentType: "image/png" });
|
|
81
|
+
|
|
82
|
+
// From engine-style data (snake_case)
|
|
83
|
+
const file = await File.from({ uri: "https://...", content_type: "image/jpeg" });
|
|
84
|
+
|
|
85
|
+
// Sync from local path (no download)
|
|
86
|
+
const file = File.fromPath("/tmp/output.png");
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### caching
|
|
90
|
+
|
|
91
|
+
downloaded files are cached at `~/.cache/inferencesh/files/{url_hash}/{filename}`. set `FILE_CACHE_DIR` to override.
|
|
92
|
+
|
|
93
|
+
## storage directories
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
import { StorageDir, ensureDir } from "@inferencesh/app";
|
|
97
|
+
|
|
98
|
+
// Standard directories available on inference.sh workers
|
|
99
|
+
StorageDir.DATA // "/app/data" — persistent storage
|
|
100
|
+
StorageDir.TEMP // "/app/tmp" — cleaned between runs
|
|
101
|
+
StorageDir.CACHE // "/app/cache" — persists, may be evicted
|
|
102
|
+
|
|
103
|
+
// Ensure directory exists
|
|
104
|
+
const dataDir = ensureDir(StorageDir.DATA);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## download utility
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
import { download, StorageDir } from "@inferencesh/app";
|
|
111
|
+
|
|
112
|
+
// Download to a specific directory with caching
|
|
113
|
+
const modelPath = await download(
|
|
114
|
+
"https://huggingface.co/org/model/resolve/main/weights.bin",
|
|
115
|
+
StorageDir.CACHE
|
|
116
|
+
);
|
|
117
|
+
// Returns: /app/cache/{hash}/weights.bin
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
skips download if the file already exists in the target directory (except for `TEMP`).
|
|
121
|
+
|
|
122
|
+
## output metadata
|
|
123
|
+
|
|
124
|
+
report what your app processes and generates for usage-based pricing:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
import { textMeta, imageMeta, videoMeta, audioMeta } from "@inferencesh/app";
|
|
128
|
+
|
|
129
|
+
// LLM app
|
|
130
|
+
async run(inputData) {
|
|
131
|
+
const result = await this.llm.generate(inputData.prompt);
|
|
132
|
+
return {
|
|
133
|
+
response: result.text,
|
|
134
|
+
output_meta: {
|
|
135
|
+
inputs: [textMeta({ tokens: result.promptTokens })],
|
|
136
|
+
outputs: [textMeta({ tokens: result.completionTokens })],
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Image generation app
|
|
142
|
+
async run(inputData) {
|
|
143
|
+
const image = await this.model.generate(inputData.prompt);
|
|
144
|
+
return {
|
|
145
|
+
image: File.fromPath(image.path),
|
|
146
|
+
output_meta: {
|
|
147
|
+
outputs: [imageMeta({ width: 1024, height: 1024, steps: 20 })],
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Video generation app
|
|
153
|
+
async run(inputData) {
|
|
154
|
+
return {
|
|
155
|
+
video: File.fromPath(videoPath),
|
|
156
|
+
output_meta: {
|
|
157
|
+
outputs: [videoMeta({ resolution: "1080p", seconds: 5.0, fps: 30 })],
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### meta types
|
|
164
|
+
|
|
165
|
+
| factory | key fields |
|
|
166
|
+
|---------|-----------|
|
|
167
|
+
| `textMeta` | `tokens` |
|
|
168
|
+
| `imageMeta` | `width`, `height`, `resolution_mp`, `steps`, `count` |
|
|
169
|
+
| `videoMeta` | `width`, `height`, `resolution`, `seconds`, `fps` |
|
|
170
|
+
| `audioMeta` | `seconds`, `sample_rate` |
|
|
171
|
+
| `rawMeta` | `cost` (dollar cents) |
|
|
172
|
+
|
|
173
|
+
all meta types support an optional `extra` field for app-specific pricing factors.
|
|
174
|
+
|
|
175
|
+
## full app example
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
import { z } from "zod";
|
|
179
|
+
import { File, textMeta } from "@inferencesh/app";
|
|
180
|
+
|
|
181
|
+
export const RunInput = z.object({
|
|
182
|
+
prompt: z.string().describe("Input prompt"),
|
|
183
|
+
image: z.string().optional().describe("Optional image URL"),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export const RunOutput = z.object({
|
|
187
|
+
result: z.string(),
|
|
188
|
+
processedImage: z.any().optional(),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export class App {
|
|
192
|
+
async setup(config) {
|
|
193
|
+
this.model = await loadModel();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async run(inputData) {
|
|
197
|
+
// Handle input files
|
|
198
|
+
let imageFile;
|
|
199
|
+
if (inputData.image) {
|
|
200
|
+
imageFile = await File.from(inputData.image);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = await this.model.process({
|
|
204
|
+
prompt: inputData.prompt,
|
|
205
|
+
imagePath: imageFile?.path,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
result: result.text,
|
|
210
|
+
processedImage: result.outputPath
|
|
211
|
+
? File.fromPath(result.outputPath)
|
|
212
|
+
: undefined,
|
|
213
|
+
output_meta: {
|
|
214
|
+
inputs: [textMeta({ tokens: result.inputTokens })],
|
|
215
|
+
outputs: [textMeta({ tokens: result.outputTokens })],
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## requirements
|
|
223
|
+
|
|
224
|
+
- node.js 18.0.0 or higher
|
|
225
|
+
- zero runtime dependencies
|
|
226
|
+
|
|
227
|
+
## resources
|
|
228
|
+
|
|
229
|
+
- [documentation](https://inference.sh/docs) — getting started guides
|
|
230
|
+
- [app development guide](https://inference.sh/docs/extend/app-code) — writing app logic
|
|
231
|
+
- [client sdk](https://www.npmjs.com/package/@inferencesh/sdk) — calling apps from your code
|
|
232
|
+
- [discord](https://discord.gg/RM77SWSbyT) — community support
|
|
233
|
+
|
|
234
|
+
## license
|
|
235
|
+
|
|
236
|
+
MIT © [inference.sh](https://inference.sh)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type StorageDirValue } from "./storage.js";
|
|
2
|
+
/**
|
|
3
|
+
* Download a file to a directory and return its local path.
|
|
4
|
+
*
|
|
5
|
+
* Uses the same cache as `File.from()`. If the file already exists in the
|
|
6
|
+
* target directory (and it's not TEMP), returns the cached copy.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```js
|
|
10
|
+
* import { download, StorageDir } from "@inferencesh/app";
|
|
11
|
+
*
|
|
12
|
+
* const path = await download("https://example.com/model.bin", StorageDir.CACHE);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function download(url: string, directory: StorageDirValue | string): Promise<string>;
|
package/dist/download.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { File } from "./file.js";
|
|
5
|
+
import { StorageDir } from "./storage.js";
|
|
6
|
+
/**
|
|
7
|
+
* Download a file to a directory and return its local path.
|
|
8
|
+
*
|
|
9
|
+
* Uses the same cache as `File.from()`. If the file already exists in the
|
|
10
|
+
* target directory (and it's not TEMP), returns the cached copy.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```js
|
|
14
|
+
* import { download, StorageDir } from "@inferencesh/app";
|
|
15
|
+
*
|
|
16
|
+
* const path = await download("https://example.com/model.bin", StorageDir.CACHE);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export async function download(url, directory) {
|
|
20
|
+
const dirPath = directory;
|
|
21
|
+
mkdirSync(dirPath, { recursive: true });
|
|
22
|
+
// Build output path with hash subdirectory (matches Python SDK)
|
|
23
|
+
const parsed = new URL(url);
|
|
24
|
+
let components = parsed.host + parsed.pathname;
|
|
25
|
+
if (parsed.search)
|
|
26
|
+
components += parsed.search;
|
|
27
|
+
const hash = createHash("sha256").update(components).digest("hex").slice(0, 12);
|
|
28
|
+
const filename = basename(parsed.pathname) || "download";
|
|
29
|
+
const hashDir = join(dirPath, hash);
|
|
30
|
+
mkdirSync(hashDir, { recursive: true });
|
|
31
|
+
const outputPath = join(hashDir, filename);
|
|
32
|
+
// Skip download if already in target directory (unless TEMP)
|
|
33
|
+
if (existsSync(outputPath) && directory !== StorageDir.TEMP) {
|
|
34
|
+
return outputPath;
|
|
35
|
+
}
|
|
36
|
+
// Download via File (uses File's own cache)
|
|
37
|
+
const file = await File.from(url);
|
|
38
|
+
if (file.path) {
|
|
39
|
+
copyFileSync(file.path, outputPath);
|
|
40
|
+
return outputPath;
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Failed to download ${url}`);
|
|
43
|
+
}
|
package/dist/file.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for constructing a File.
|
|
3
|
+
*/
|
|
4
|
+
export interface FileOptions {
|
|
5
|
+
uri?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
contentType?: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
filename?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Serialized File representation — what the engine sees in task output.
|
|
13
|
+
*/
|
|
14
|
+
export interface FileData {
|
|
15
|
+
uri?: string;
|
|
16
|
+
path?: string;
|
|
17
|
+
content_type?: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
filename?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A file in the inference.sh ecosystem.
|
|
23
|
+
*
|
|
24
|
+
* Accepts a URL, local path, or options object.
|
|
25
|
+
* URLs are downloaded and cached locally on construction (via `await File.from()`).
|
|
26
|
+
* Local paths are resolved to absolute paths.
|
|
27
|
+
*
|
|
28
|
+
* In JSON output, File serializes to `{ path, uri, content_type, size, filename }`
|
|
29
|
+
* — the engine uploads local `path` files to CDN and replaces with `uri`.
|
|
30
|
+
*/
|
|
31
|
+
export declare class File {
|
|
32
|
+
uri?: string;
|
|
33
|
+
path?: string;
|
|
34
|
+
contentType?: string;
|
|
35
|
+
size?: number;
|
|
36
|
+
filename?: string;
|
|
37
|
+
private constructor();
|
|
38
|
+
/**
|
|
39
|
+
* Create a File from a URL, local path, or options object.
|
|
40
|
+
* URLs are downloaded and cached automatically.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```js
|
|
44
|
+
* // From local path
|
|
45
|
+
* const file = await File.from("/tmp/output.png");
|
|
46
|
+
*
|
|
47
|
+
* // From URL (downloads and caches)
|
|
48
|
+
* const file = await File.from("https://example.com/image.jpg");
|
|
49
|
+
*
|
|
50
|
+
* // From options
|
|
51
|
+
* const file = await File.from({ path: "/tmp/output.png", contentType: "image/png" });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
static from(input: string | FileData | FileOptions | File): Promise<File>;
|
|
55
|
+
/**
|
|
56
|
+
* Create a File from a local path (sync, no download).
|
|
57
|
+
*/
|
|
58
|
+
static fromPath(localPath: string): File;
|
|
59
|
+
/**
|
|
60
|
+
* Check if the file exists on disk.
|
|
61
|
+
*/
|
|
62
|
+
exists(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Re-read metadata (contentType, size, filename) from disk.
|
|
65
|
+
*/
|
|
66
|
+
refreshMetadata(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Serialize to a plain object for JSON output.
|
|
69
|
+
* The engine reads `path` fields and uploads them to CDN.
|
|
70
|
+
*/
|
|
71
|
+
toJSON(): FileData;
|
|
72
|
+
static getCacheDir(): string;
|
|
73
|
+
private _getCachePath;
|
|
74
|
+
private _downloadUrl;
|
|
75
|
+
private _populateMetadata;
|
|
76
|
+
}
|
package/dist/file.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, statSync, renameSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { basename, resolve, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { get as httpsGet } from "node:https";
|
|
6
|
+
import { get as httpGet } from "node:http";
|
|
7
|
+
import { URL } from "node:url";
|
|
8
|
+
/**
|
|
9
|
+
* A file in the inference.sh ecosystem.
|
|
10
|
+
*
|
|
11
|
+
* Accepts a URL, local path, or options object.
|
|
12
|
+
* URLs are downloaded and cached locally on construction (via `await File.from()`).
|
|
13
|
+
* Local paths are resolved to absolute paths.
|
|
14
|
+
*
|
|
15
|
+
* In JSON output, File serializes to `{ path, uri, content_type, size, filename }`
|
|
16
|
+
* — the engine uploads local `path` files to CDN and replaces with `uri`.
|
|
17
|
+
*/
|
|
18
|
+
export class File {
|
|
19
|
+
uri;
|
|
20
|
+
path;
|
|
21
|
+
contentType;
|
|
22
|
+
size;
|
|
23
|
+
filename;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.uri = options.uri;
|
|
26
|
+
this.path = options.path;
|
|
27
|
+
this.contentType = options.contentType;
|
|
28
|
+
this.size = options.size;
|
|
29
|
+
this.filename = options.filename;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a File from a URL, local path, or options object.
|
|
33
|
+
* URLs are downloaded and cached automatically.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```js
|
|
37
|
+
* // From local path
|
|
38
|
+
* const file = await File.from("/tmp/output.png");
|
|
39
|
+
*
|
|
40
|
+
* // From URL (downloads and caches)
|
|
41
|
+
* const file = await File.from("https://example.com/image.jpg");
|
|
42
|
+
*
|
|
43
|
+
* // From options
|
|
44
|
+
* const file = await File.from({ path: "/tmp/output.png", contentType: "image/png" });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
static async from(input) {
|
|
48
|
+
if (input instanceof File) {
|
|
49
|
+
return new File({
|
|
50
|
+
uri: input.uri,
|
|
51
|
+
path: input.path,
|
|
52
|
+
contentType: input.contentType,
|
|
53
|
+
size: input.size,
|
|
54
|
+
filename: input.filename,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
let options;
|
|
58
|
+
if (typeof input === "string") {
|
|
59
|
+
options = { uri: input };
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const data = input;
|
|
63
|
+
options = {
|
|
64
|
+
uri: data.uri,
|
|
65
|
+
path: data.path,
|
|
66
|
+
contentType: data.content_type ?? data.contentType,
|
|
67
|
+
size: data.size,
|
|
68
|
+
filename: data.filename,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!options.uri && !options.path) {
|
|
72
|
+
throw new Error("Either 'uri' or 'path' must be provided");
|
|
73
|
+
}
|
|
74
|
+
const file = new File(options);
|
|
75
|
+
// Resolve URI
|
|
76
|
+
if (file.uri) {
|
|
77
|
+
if (isUrl(file.uri)) {
|
|
78
|
+
await file._downloadUrl(file.uri);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Treat as local path
|
|
82
|
+
file.path = resolve(file.uri);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (file.path) {
|
|
86
|
+
file.path = resolve(file.path);
|
|
87
|
+
file._populateMetadata();
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw new Error("Either 'uri' or 'path' must be provided and be valid");
|
|
91
|
+
}
|
|
92
|
+
return file;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create a File from a local path (sync, no download).
|
|
96
|
+
*/
|
|
97
|
+
static fromPath(localPath) {
|
|
98
|
+
const absPath = resolve(localPath);
|
|
99
|
+
const file = new File({ path: absPath });
|
|
100
|
+
file._populateMetadata();
|
|
101
|
+
return file;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if the file exists on disk.
|
|
105
|
+
*/
|
|
106
|
+
exists() {
|
|
107
|
+
return this.path != null && existsSync(this.path);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Re-read metadata (contentType, size, filename) from disk.
|
|
111
|
+
*/
|
|
112
|
+
refreshMetadata() {
|
|
113
|
+
this._populateMetadata();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Serialize to a plain object for JSON output.
|
|
117
|
+
* The engine reads `path` fields and uploads them to CDN.
|
|
118
|
+
*/
|
|
119
|
+
toJSON() {
|
|
120
|
+
const result = {};
|
|
121
|
+
if (this.uri != null)
|
|
122
|
+
result.uri = this.uri;
|
|
123
|
+
if (this.path != null)
|
|
124
|
+
result.path = this.path;
|
|
125
|
+
if (this.contentType != null)
|
|
126
|
+
result.content_type = this.contentType;
|
|
127
|
+
if (this.size != null)
|
|
128
|
+
result.size = this.size;
|
|
129
|
+
if (this.filename != null)
|
|
130
|
+
result.filename = this.filename;
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
// --- Cache ---
|
|
134
|
+
static getCacheDir() {
|
|
135
|
+
const envDir = process.env.FILE_CACHE_DIR;
|
|
136
|
+
const dir = envDir || join(homedir(), ".cache", "inferencesh", "files");
|
|
137
|
+
mkdirSync(dir, { recursive: true });
|
|
138
|
+
return dir;
|
|
139
|
+
}
|
|
140
|
+
_getCachePath(url) {
|
|
141
|
+
const parsed = new URL(url);
|
|
142
|
+
let components = parsed.host + parsed.pathname;
|
|
143
|
+
if (parsed.search)
|
|
144
|
+
components += parsed.search;
|
|
145
|
+
const hash = createHash("sha256").update(components).digest("hex").slice(0, 12);
|
|
146
|
+
const fname = basename(parsed.pathname) || "download";
|
|
147
|
+
const hashDir = join(File.getCacheDir(), hash);
|
|
148
|
+
mkdirSync(hashDir, { recursive: true });
|
|
149
|
+
return join(hashDir, fname);
|
|
150
|
+
}
|
|
151
|
+
// --- Download ---
|
|
152
|
+
async _downloadUrl(url) {
|
|
153
|
+
const cachePath = this._getCachePath(url);
|
|
154
|
+
if (existsSync(cachePath)) {
|
|
155
|
+
this.path = cachePath;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const tmpPath = cachePath + ".tmp";
|
|
159
|
+
try {
|
|
160
|
+
await downloadToFile(url, tmpPath);
|
|
161
|
+
renameSync(tmpPath, cachePath);
|
|
162
|
+
this.path = cachePath;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
try {
|
|
166
|
+
unlinkSync(tmpPath);
|
|
167
|
+
}
|
|
168
|
+
catch { /* ignore */ }
|
|
169
|
+
throw new Error(`Failed to download ${url}: ${err.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// --- Metadata ---
|
|
173
|
+
_populateMetadata() {
|
|
174
|
+
if (!this.path || !existsSync(this.path))
|
|
175
|
+
return;
|
|
176
|
+
if (!this.contentType) {
|
|
177
|
+
this.contentType = guessContentType(this.path);
|
|
178
|
+
}
|
|
179
|
+
if (this.size == null) {
|
|
180
|
+
try {
|
|
181
|
+
this.size = statSync(this.path).size;
|
|
182
|
+
}
|
|
183
|
+
catch { /* ignore */ }
|
|
184
|
+
}
|
|
185
|
+
if (!this.filename) {
|
|
186
|
+
this.filename = basename(this.path);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// --- Helpers ---
|
|
191
|
+
function isUrl(s) {
|
|
192
|
+
return s.startsWith("http://") || s.startsWith("https://");
|
|
193
|
+
}
|
|
194
|
+
function downloadToFile(url, destPath) {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const parsed = new URL(url);
|
|
197
|
+
const getter = parsed.protocol === "https:" ? httpsGet : httpGet;
|
|
198
|
+
const request = getter(url, (response) => {
|
|
199
|
+
// Follow redirects
|
|
200
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
201
|
+
downloadToFile(response.headers.location, destPath).then(resolve, reject);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (response.statusCode && response.statusCode >= 400) {
|
|
205
|
+
reject(new Error(`HTTP ${response.statusCode}`));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const dir = join(destPath, "..");
|
|
209
|
+
mkdirSync(dir, { recursive: true });
|
|
210
|
+
const stream = createWriteStream(destPath);
|
|
211
|
+
response.pipe(stream);
|
|
212
|
+
stream.on("finish", () => {
|
|
213
|
+
stream.close();
|
|
214
|
+
resolve();
|
|
215
|
+
});
|
|
216
|
+
stream.on("error", reject);
|
|
217
|
+
});
|
|
218
|
+
request.on("error", reject);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const MIME_TYPES = {
|
|
222
|
+
".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png",
|
|
223
|
+
".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
|
|
224
|
+
".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/quicktime",
|
|
225
|
+
".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg",
|
|
226
|
+
".flac": "audio/flac", ".aac": "audio/aac",
|
|
227
|
+
".pdf": "application/pdf", ".json": "application/json",
|
|
228
|
+
".txt": "text/plain", ".csv": "text/csv", ".html": "text/html",
|
|
229
|
+
".zip": "application/zip", ".tar": "application/x-tar",
|
|
230
|
+
".gz": "application/gzip",
|
|
231
|
+
};
|
|
232
|
+
function guessContentType(filePath) {
|
|
233
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
234
|
+
return MIME_TYPES[ext];
|
|
235
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { File } from "./file.js";
|
|
2
|
+
export type { FileOptions, FileData } from "./file.js";
|
|
3
|
+
export { StorageDir, ensureDir } from "./storage.js";
|
|
4
|
+
export type { StorageDirValue } from "./storage.js";
|
|
5
|
+
export { download } from "./download.js";
|
|
6
|
+
export { textMeta, imageMeta, videoMeta, audioMeta, rawMeta } from "./output-meta.js";
|
|
7
|
+
export type { OutputMeta, MetaItem, MetaItemBase, TextMeta, ImageMeta, VideoMeta, AudioMeta, RawMeta, } from "./output-meta.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// File handling
|
|
2
|
+
export { File } from "./file.js";
|
|
3
|
+
// Storage directories
|
|
4
|
+
export { StorageDir, ensureDir } from "./storage.js";
|
|
5
|
+
// Download utility
|
|
6
|
+
export { download } from "./download.js";
|
|
7
|
+
// Output metadata for usage-based pricing
|
|
8
|
+
export { textMeta, imageMeta, videoMeta, audioMeta, rawMeta } from "./output-meta.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output metadata types for usage-based pricing.
|
|
3
|
+
*
|
|
4
|
+
* Apps include OutputMeta in their run output to report what was consumed
|
|
5
|
+
* (inputs) and what was produced (outputs). The backend uses this for
|
|
6
|
+
* pricing calculation.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```js
|
|
10
|
+
* import { textMeta, imageMeta } from "@inferencesh/app";
|
|
11
|
+
*
|
|
12
|
+
* return {
|
|
13
|
+
* result: generatedText,
|
|
14
|
+
* output_meta: {
|
|
15
|
+
* inputs: [textMeta({ tokens: promptTokens })],
|
|
16
|
+
* outputs: [textMeta({ tokens: completionTokens })],
|
|
17
|
+
* },
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export interface MetaItemBase {
|
|
22
|
+
type: string;
|
|
23
|
+
extra?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface TextMeta extends MetaItemBase {
|
|
26
|
+
type: "text";
|
|
27
|
+
tokens: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ImageMeta extends MetaItemBase {
|
|
30
|
+
type: "image";
|
|
31
|
+
width?: number;
|
|
32
|
+
height?: number;
|
|
33
|
+
resolution_mp?: number;
|
|
34
|
+
steps?: number;
|
|
35
|
+
count?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface VideoMeta extends MetaItemBase {
|
|
38
|
+
type: "video";
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
resolution_mp?: number;
|
|
42
|
+
resolution?: "480p" | "720p" | "1080p" | "1440p" | "4k";
|
|
43
|
+
seconds?: number;
|
|
44
|
+
fps?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface AudioMeta extends MetaItemBase {
|
|
47
|
+
type: "audio";
|
|
48
|
+
seconds?: number;
|
|
49
|
+
sample_rate?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface RawMeta extends MetaItemBase {
|
|
52
|
+
type: "raw";
|
|
53
|
+
cost?: number;
|
|
54
|
+
}
|
|
55
|
+
export type MetaItem = TextMeta | ImageMeta | VideoMeta | AudioMeta | RawMeta;
|
|
56
|
+
export interface OutputMeta {
|
|
57
|
+
inputs?: MetaItem[];
|
|
58
|
+
outputs?: MetaItem[];
|
|
59
|
+
}
|
|
60
|
+
/** Create a text metadata item. */
|
|
61
|
+
export declare function textMeta(opts: Omit<TextMeta, "type">): TextMeta;
|
|
62
|
+
/** Create an image metadata item. */
|
|
63
|
+
export declare function imageMeta(opts?: Omit<ImageMeta, "type">): ImageMeta;
|
|
64
|
+
/** Create a video metadata item. */
|
|
65
|
+
export declare function videoMeta(opts?: Omit<VideoMeta, "type">): VideoMeta;
|
|
66
|
+
/** Create an audio metadata item. */
|
|
67
|
+
export declare function audioMeta(opts?: Omit<AudioMeta, "type">): AudioMeta;
|
|
68
|
+
/** Create a raw metadata item (custom pricing). */
|
|
69
|
+
export declare function rawMeta(opts?: Omit<RawMeta, "type">): RawMeta;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output metadata types for usage-based pricing.
|
|
3
|
+
*
|
|
4
|
+
* Apps include OutputMeta in their run output to report what was consumed
|
|
5
|
+
* (inputs) and what was produced (outputs). The backend uses this for
|
|
6
|
+
* pricing calculation.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```js
|
|
10
|
+
* import { textMeta, imageMeta } from "@inferencesh/app";
|
|
11
|
+
*
|
|
12
|
+
* return {
|
|
13
|
+
* result: generatedText,
|
|
14
|
+
* output_meta: {
|
|
15
|
+
* inputs: [textMeta({ tokens: promptTokens })],
|
|
16
|
+
* outputs: [textMeta({ tokens: completionTokens })],
|
|
17
|
+
* },
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
// --- Factories ---
|
|
22
|
+
/** Create a text metadata item. */
|
|
23
|
+
export function textMeta(opts) {
|
|
24
|
+
return { type: "text", ...opts };
|
|
25
|
+
}
|
|
26
|
+
/** Create an image metadata item. */
|
|
27
|
+
export function imageMeta(opts = {}) {
|
|
28
|
+
return { type: "image", ...opts };
|
|
29
|
+
}
|
|
30
|
+
/** Create a video metadata item. */
|
|
31
|
+
export function videoMeta(opts = {}) {
|
|
32
|
+
return { type: "video", ...opts };
|
|
33
|
+
}
|
|
34
|
+
/** Create an audio metadata item. */
|
|
35
|
+
export function audioMeta(opts = {}) {
|
|
36
|
+
return { type: "audio", ...opts };
|
|
37
|
+
}
|
|
38
|
+
/** Create a raw metadata item (custom pricing). */
|
|
39
|
+
export function rawMeta(opts = {}) {
|
|
40
|
+
return { type: "raw", ...opts };
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard storage directories available to inference.sh apps at runtime.
|
|
3
|
+
*/
|
|
4
|
+
export declare const StorageDir: {
|
|
5
|
+
/** Persistent storage — survives across runs */
|
|
6
|
+
readonly DATA: "/app/data";
|
|
7
|
+
/** Temporary storage — cleaned between runs */
|
|
8
|
+
readonly TEMP: "/app/tmp";
|
|
9
|
+
/** Cache storage — persists across runs, may be evicted */
|
|
10
|
+
readonly CACHE: "/app/cache";
|
|
11
|
+
};
|
|
12
|
+
export type StorageDirValue = (typeof StorageDir)[keyof typeof StorageDir];
|
|
13
|
+
/**
|
|
14
|
+
* Ensure a storage directory exists and return its path.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```js
|
|
18
|
+
* import { ensureDir, StorageDir } from "@inferencesh/app";
|
|
19
|
+
*
|
|
20
|
+
* const dataDir = ensureDir(StorageDir.DATA);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function ensureDir(dir: StorageDirValue | string): string;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Standard storage directories available to inference.sh apps at runtime.
|
|
4
|
+
*/
|
|
5
|
+
export const StorageDir = {
|
|
6
|
+
/** Persistent storage — survives across runs */
|
|
7
|
+
DATA: "/app/data",
|
|
8
|
+
/** Temporary storage — cleaned between runs */
|
|
9
|
+
TEMP: "/app/tmp",
|
|
10
|
+
/** Cache storage — persists across runs, may be evicted */
|
|
11
|
+
CACHE: "/app/cache",
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Ensure a storage directory exists and return its path.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```js
|
|
18
|
+
* import { ensureDir, StorageDir } from "@inferencesh/app";
|
|
19
|
+
*
|
|
20
|
+
* const dataDir = ensureDir(StorageDir.DATA);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function ensureDir(dir) {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, before, after } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { File } from "../file.js";
|
|
7
|
+
const TEST_DIR = join(tmpdir(), "inferencesh-app-test-" + Date.now());
|
|
8
|
+
let testFile;
|
|
9
|
+
describe("File", () => {
|
|
10
|
+
before(() => {
|
|
11
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
12
|
+
testFile = join(TEST_DIR, "hello.txt");
|
|
13
|
+
writeFileSync(testFile, "hello world");
|
|
14
|
+
});
|
|
15
|
+
after(() => {
|
|
16
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
it("creates from local path", () => {
|
|
19
|
+
const file = File.fromPath(testFile);
|
|
20
|
+
assert.ok(file.path);
|
|
21
|
+
assert.ok(file.exists());
|
|
22
|
+
assert.strictEqual(file.filename, "hello.txt");
|
|
23
|
+
assert.strictEqual(file.contentType, "text/plain");
|
|
24
|
+
assert.strictEqual(file.size, 11);
|
|
25
|
+
});
|
|
26
|
+
it("creates from path via async from()", async () => {
|
|
27
|
+
const file = await File.from(testFile);
|
|
28
|
+
assert.ok(file.path);
|
|
29
|
+
assert.ok(file.exists());
|
|
30
|
+
assert.strictEqual(file.filename, "hello.txt");
|
|
31
|
+
});
|
|
32
|
+
it("creates from options object", async () => {
|
|
33
|
+
const file = await File.from({ path: testFile, contentType: "text/plain" });
|
|
34
|
+
assert.ok(file.exists());
|
|
35
|
+
assert.strictEqual(file.contentType, "text/plain");
|
|
36
|
+
});
|
|
37
|
+
it("creates from FileData with content_type (snake_case)", async () => {
|
|
38
|
+
const file = await File.from({ path: testFile, content_type: "application/octet-stream" });
|
|
39
|
+
assert.strictEqual(file.contentType, "application/octet-stream");
|
|
40
|
+
});
|
|
41
|
+
it("creates from another File", async () => {
|
|
42
|
+
const original = File.fromPath(testFile);
|
|
43
|
+
const copy = await File.from(original);
|
|
44
|
+
assert.strictEqual(copy.path, original.path);
|
|
45
|
+
assert.strictEqual(copy.filename, original.filename);
|
|
46
|
+
});
|
|
47
|
+
it("serializes to JSON with snake_case", () => {
|
|
48
|
+
const file = File.fromPath(testFile);
|
|
49
|
+
const json = file.toJSON();
|
|
50
|
+
assert.ok(json.path);
|
|
51
|
+
assert.strictEqual(json.content_type, "text/plain");
|
|
52
|
+
assert.strictEqual(json.size, 11);
|
|
53
|
+
assert.strictEqual(json.filename, "hello.txt");
|
|
54
|
+
assert.strictEqual(json.uri, undefined);
|
|
55
|
+
});
|
|
56
|
+
it("works with JSON.stringify", () => {
|
|
57
|
+
const file = File.fromPath(testFile);
|
|
58
|
+
const str = JSON.stringify({ image: file });
|
|
59
|
+
const parsed = JSON.parse(str);
|
|
60
|
+
assert.ok(parsed.image.path);
|
|
61
|
+
assert.strictEqual(parsed.image.content_type, "text/plain");
|
|
62
|
+
});
|
|
63
|
+
it("resolves relative paths to absolute", () => {
|
|
64
|
+
const file = File.fromPath("./package.json");
|
|
65
|
+
assert.ok(file.path.startsWith("/"));
|
|
66
|
+
});
|
|
67
|
+
it("throws on missing path and uri", async () => {
|
|
68
|
+
await assert.rejects(() => File.from({}), /Either 'uri' or 'path' must be provided/);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { textMeta, imageMeta, videoMeta, audioMeta, rawMeta } from "../output-meta.js";
|
|
4
|
+
describe("OutputMeta", () => {
|
|
5
|
+
it("creates text meta", () => {
|
|
6
|
+
const meta = textMeta({ tokens: 150 });
|
|
7
|
+
assert.strictEqual(meta.type, "text");
|
|
8
|
+
assert.strictEqual(meta.tokens, 150);
|
|
9
|
+
});
|
|
10
|
+
it("creates image meta", () => {
|
|
11
|
+
const meta = imageMeta({ width: 1024, height: 1024, steps: 20, count: 1 });
|
|
12
|
+
assert.strictEqual(meta.type, "image");
|
|
13
|
+
assert.strictEqual(meta.width, 1024);
|
|
14
|
+
assert.strictEqual(meta.steps, 20);
|
|
15
|
+
});
|
|
16
|
+
it("creates video meta", () => {
|
|
17
|
+
const meta = videoMeta({ resolution: "1080p", seconds: 5.0 });
|
|
18
|
+
assert.strictEqual(meta.type, "video");
|
|
19
|
+
assert.strictEqual(meta.resolution, "1080p");
|
|
20
|
+
});
|
|
21
|
+
it("creates audio meta", () => {
|
|
22
|
+
const meta = audioMeta({ seconds: 30.0 });
|
|
23
|
+
assert.strictEqual(meta.type, "audio");
|
|
24
|
+
assert.strictEqual(meta.seconds, 30.0);
|
|
25
|
+
});
|
|
26
|
+
it("creates raw meta", () => {
|
|
27
|
+
const meta = rawMeta({ cost: 0.5 });
|
|
28
|
+
assert.strictEqual(meta.type, "raw");
|
|
29
|
+
assert.strictEqual(meta.cost, 0.5);
|
|
30
|
+
});
|
|
31
|
+
it("supports extra data", () => {
|
|
32
|
+
const meta = imageMeta({ width: 512, height: 512, extra: { model: "sdxl" } });
|
|
33
|
+
assert.strictEqual(meta.extra?.model, "sdxl");
|
|
34
|
+
});
|
|
35
|
+
it("composes into OutputMeta", () => {
|
|
36
|
+
const output = {
|
|
37
|
+
inputs: [textMeta({ tokens: 100 })],
|
|
38
|
+
outputs: [textMeta({ tokens: 500 }), imageMeta({ width: 1024, height: 1024 })],
|
|
39
|
+
};
|
|
40
|
+
assert.strictEqual(output.inputs.length, 1);
|
|
41
|
+
assert.strictEqual(output.outputs.length, 2);
|
|
42
|
+
// Serializes cleanly
|
|
43
|
+
const json = JSON.stringify(output);
|
|
44
|
+
const parsed = JSON.parse(json);
|
|
45
|
+
assert.strictEqual(parsed.inputs[0].type, "text");
|
|
46
|
+
assert.strictEqual(parsed.outputs[1].type, "image");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { StorageDir } from "../storage.js";
|
|
4
|
+
describe("StorageDir", () => {
|
|
5
|
+
it("has correct paths", () => {
|
|
6
|
+
assert.strictEqual(StorageDir.DATA, "/app/data");
|
|
7
|
+
assert.strictEqual(StorageDir.TEMP, "/app/tmp");
|
|
8
|
+
assert.strictEqual(StorageDir.CACHE, "/app/cache");
|
|
9
|
+
});
|
|
10
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inferencesh/app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "App framework for building inference.sh apps — File handling, output metadata, storage utilities",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "node --test dist/test/*.test.js",
|
|
17
|
+
"clean": "rimraf dist",
|
|
18
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"inference",
|
|
22
|
+
"ai",
|
|
23
|
+
"ml",
|
|
24
|
+
"app",
|
|
25
|
+
"sdk",
|
|
26
|
+
"file",
|
|
27
|
+
"typescript"
|
|
28
|
+
],
|
|
29
|
+
"author": "Okaris <hello@inference.sh>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/inference-sh/sdk-js-app.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://inference.sh",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/inference-sh/sdk-js-app/issues"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"rimraf": "^6.0.1",
|
|
45
|
+
"typescript": "^5.8.3"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
]
|
|
52
|
+
}
|