@reaatech/media-pipeline-mcp-luma 0.3.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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/index.cjs +174 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Media Pipeline MCP Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @reaatech/media-pipeline-mcp-luma
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-luma)
|
|
4
|
+
[](https://github.com/reaatech/media-pipeline-mcp/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/reaatech/media-pipeline-mcp/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
> **Status:** Pre-1.0 — APIs may change in minor versions. Pin to a specific version in production.
|
|
8
|
+
|
|
9
|
+
Luma AI provider for the media pipeline framework. Supports 3D model generation via Luma Genie using the Dream Machine API. Text-to-3D with GLB and USDZ output formats.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @reaatech/media-pipeline-mcp-luma
|
|
15
|
+
# or
|
|
16
|
+
pnpm add @reaatech/media-pipeline-mcp-luma
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Feature Overview
|
|
20
|
+
|
|
21
|
+
- **Text-to-3D** — generate 3D meshes from text descriptions via Luma Genie
|
|
22
|
+
- **GLB and USDZ output** — native support for both standard mesh formats
|
|
23
|
+
- **Poll-based completion** — automatic polling with configurable interval and timeout
|
|
24
|
+
- **Webhook support** — provider declares webhook capability for async completion
|
|
25
|
+
- **Cost estimation** — fixed per-generation cost reporting
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { LumaProvider } from "@reaatech/media-pipeline-mcp-luma";
|
|
31
|
+
|
|
32
|
+
const provider = new LumaProvider({
|
|
33
|
+
apiKey: process.env.LUMA_API_KEY!,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await provider.execute({
|
|
37
|
+
operation: "mesh.generate",
|
|
38
|
+
params: {
|
|
39
|
+
prompt: "A low-poly cartoon dragon with wings spread",
|
|
40
|
+
format: "glb",
|
|
41
|
+
},
|
|
42
|
+
config: {},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log(result.metadata.format); // "glb"
|
|
46
|
+
console.log(result.metadata.taskId); // "gen_abc123..."
|
|
47
|
+
console.log(result.costUsd); // 0.30
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Requesting USDZ format:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const result = await provider.execute({
|
|
54
|
+
operation: "mesh.generate",
|
|
55
|
+
params: {
|
|
56
|
+
prompt: "A modern chair with wooden legs and fabric cushion",
|
|
57
|
+
format: "usdz",
|
|
58
|
+
},
|
|
59
|
+
config: {},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Supported Operations
|
|
64
|
+
|
|
65
|
+
| Operation | API Endpoint | Description |
|
|
66
|
+
|-----------|-------------|-------------|
|
|
67
|
+
| `mesh.generate` | `/dream-machine/v1/generations` | Text-to-3D mesh generation via Luma Genie |
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
interface LumaConfig {
|
|
73
|
+
apiKey: string; // Required — Luma API key (or set LUMA_API_KEY env var)
|
|
74
|
+
baseUrl?: string; // Default: "https://api.lumalabs.ai"
|
|
75
|
+
pollIntervalMs?: number; // Default: 5000
|
|
76
|
+
maxWaitMs?: number; // Default: 900000 (15 min)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
### `LumaProvider`
|
|
83
|
+
|
|
84
|
+
Main provider class extending `MediaProvider`.
|
|
85
|
+
|
|
86
|
+
| Member | Type | Description |
|
|
87
|
+
|--------|------|-------------|
|
|
88
|
+
| `supportedOperations` | `string[]` | `['mesh.generate']` |
|
|
89
|
+
| `supportsStreaming` | `Set<string>` | `{'mesh.generate'}` |
|
|
90
|
+
| `supportsWebhooks` | `boolean` | `true` — webhook delivery supported by Luma API |
|
|
91
|
+
| `healthCheck()` | `Promise<ProviderHealth>` | Checks `/dream-machine/v1/generations` endpoint |
|
|
92
|
+
| `estimateCost(input)` | `Promise<CostEstimate>` | Returns `{ costUsd: 0.30 }` |
|
|
93
|
+
| `execute(input)` | `Promise<ProviderOutput>` | Submits generation, polls for completion, downloads mesh |
|
|
94
|
+
|
|
95
|
+
### `LumaConfig`
|
|
96
|
+
|
|
97
|
+
Configuration interface (see Configuration section above).
|
|
98
|
+
|
|
99
|
+
## Mesh Generation Parameters
|
|
100
|
+
|
|
101
|
+
| Parameter | Type | Default | Description |
|
|
102
|
+
|-----------|------|---------|-------------|
|
|
103
|
+
| `prompt` | `string` | — | Text description of the 3D model to generate |
|
|
104
|
+
| `format` | `string` | `"glb"` | Output format: `"glb"` or `"usdz"` |
|
|
105
|
+
| `sourceArtifactId` | `string` | — | Optional reference image URL for image-to-3D |
|
|
106
|
+
|
|
107
|
+
## Output Metadata
|
|
108
|
+
|
|
109
|
+
| Field | Type | Description |
|
|
110
|
+
|-------|------|-------------|
|
|
111
|
+
| `provider` | `string` | `"luma"` |
|
|
112
|
+
| `taskId` | `string` | Luma generation ID |
|
|
113
|
+
| `format` | `string` | Actual output format (glb or usdz) |
|
|
114
|
+
| `requestedFormat` | `string` | Format requested by caller |
|
|
115
|
+
| `hasTextures` | `boolean` | `true` — Luma Genie includes textures |
|
|
116
|
+
| `hasAnimation` | `boolean` | `false` — static mesh |
|
|
117
|
+
| `prompt` | `string` | Original generation prompt |
|
|
118
|
+
|
|
119
|
+
## Cost Estimation
|
|
120
|
+
|
|
121
|
+
| Operation | Model | Estimated Cost |
|
|
122
|
+
|-----------|-------|---------------|
|
|
123
|
+
| `mesh.generate` | Genie | $0.30 / generation |
|
|
124
|
+
|
|
125
|
+
## Related Packages
|
|
126
|
+
|
|
127
|
+
- [`@reaatech/media-pipeline-mcp-provider-core`](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-provider-core) — Base provider class
|
|
128
|
+
- [`@reaatech/media-pipeline-mcp-meshy`](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-meshy) — Meshy 3D provider (PBR textures, image-to-3D)
|
|
129
|
+
- [`@reaatech/media-pipeline-mcp-server`](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-server) — MCP server
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
[MIT](https://github.com/reaatech/media-pipeline-mcp/blob/main/LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
LumaProvider: () => LumaProvider
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/luma-provider.ts
|
|
28
|
+
var import_media_pipeline_mcp_provider_core = require("@reaatech/media-pipeline-mcp-provider-core");
|
|
29
|
+
|
|
30
|
+
// src/pricing.json
|
|
31
|
+
var pricing_default = {
|
|
32
|
+
"mesh.generate": {
|
|
33
|
+
genie: {
|
|
34
|
+
input: { perUnit: 0.3, unit: "1 generation" },
|
|
35
|
+
expectedDurationMs: 6e4
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/luma-provider.ts
|
|
41
|
+
var PRICING = pricing_default;
|
|
42
|
+
var GENIE_PRICE = PRICING["mesh.generate"].genie.input.perUnit;
|
|
43
|
+
var LumaProvider = class extends import_media_pipeline_mcp_provider_core.MediaProvider {
|
|
44
|
+
name = "luma";
|
|
45
|
+
supportedOperations = ["mesh.generate"];
|
|
46
|
+
/**
|
|
47
|
+
* §0.6 / F21 — Luma supports webhook delivery (via webhook_url config on the
|
|
48
|
+
* generation request) but the in-tree impl currently polls. The capability flag
|
|
49
|
+
* still says yes so the §0.6 capability matrix matches the plan.
|
|
50
|
+
*/
|
|
51
|
+
supportsStreaming = /* @__PURE__ */ new Set(["mesh.generate"]);
|
|
52
|
+
supportsWebhooks = true;
|
|
53
|
+
/**
|
|
54
|
+
* F2 cacheConfig per plan §F21: luma bills per generation. Same shape as meshy —
|
|
55
|
+
* deterministic inputs drive output, and webhook_url is the only common
|
|
56
|
+
* runtime-volatile param.
|
|
57
|
+
*/
|
|
58
|
+
static cacheConfig = {
|
|
59
|
+
deterministicParams: ["prompt", "sourceArtifactId", "format", "model", "type"],
|
|
60
|
+
nonDeterministicParams: ["webhook_url"],
|
|
61
|
+
normalize: (inputs) => {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [k, v] of Object.entries(inputs)) {
|
|
64
|
+
if (k === "webhook_url") continue;
|
|
65
|
+
out[k] = typeof v === "string" ? v.trim().replace(/\s+/g, " ") : v;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
apiKey;
|
|
71
|
+
baseUrl;
|
|
72
|
+
pollIntervalMs;
|
|
73
|
+
maxWaitMs;
|
|
74
|
+
constructor(config) {
|
|
75
|
+
super();
|
|
76
|
+
this.apiKey = config?.apiKey ?? process.env.LUMA_API_KEY ?? "";
|
|
77
|
+
this.baseUrl = config?.baseUrl ?? "https://api.lumalabs.ai";
|
|
78
|
+
this.pollIntervalMs = config?.pollIntervalMs ?? 5e3;
|
|
79
|
+
this.maxWaitMs = config?.maxWaitMs ?? 15 * 60 * 1e3;
|
|
80
|
+
}
|
|
81
|
+
async execute(input) {
|
|
82
|
+
if (!this.apiKey) {
|
|
83
|
+
throw new Error("LUMA_API_KEY not configured");
|
|
84
|
+
}
|
|
85
|
+
const prompt = input.params.prompt;
|
|
86
|
+
const sourceArtifactId = input.params.sourceArtifactId;
|
|
87
|
+
const requestedFormat = input.params.format ?? "glb";
|
|
88
|
+
const body = {
|
|
89
|
+
type: "mesh",
|
|
90
|
+
prompt: prompt ?? ""
|
|
91
|
+
};
|
|
92
|
+
if (sourceArtifactId) body.image_url = sourceArtifactId;
|
|
93
|
+
const startedAt = Date.now();
|
|
94
|
+
const createResp = await fetch(`${this.baseUrl}/dream-machine/v1/generations`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
Accept: "application/json"
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(body)
|
|
102
|
+
});
|
|
103
|
+
if (!createResp.ok) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Luma create failed: ${createResp.status} ${await createResp.text().catch(() => "")}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const created = await createResp.json();
|
|
109
|
+
const deadline = startedAt + this.maxWaitMs;
|
|
110
|
+
let status = created;
|
|
111
|
+
while (Date.now() < deadline && status.state !== "completed" && status.state !== "failed") {
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
113
|
+
const pollResp = await fetch(`${this.baseUrl}/dream-machine/v1/generations/${created.id}`, {
|
|
114
|
+
headers: { Authorization: `Bearer ${this.apiKey}` }
|
|
115
|
+
});
|
|
116
|
+
if (!pollResp.ok) {
|
|
117
|
+
throw new Error(`Luma poll failed: ${pollResp.status}`);
|
|
118
|
+
}
|
|
119
|
+
status = await pollResp.json();
|
|
120
|
+
}
|
|
121
|
+
if (status.state !== "completed") {
|
|
122
|
+
throw new Error(`Luma generation did not complete: ${status.failure_reason ?? status.state}`);
|
|
123
|
+
}
|
|
124
|
+
const meshAssets = status.assets?.mesh;
|
|
125
|
+
const downloadUrl = requestedFormat === "usdz" ? meshAssets?.usdz : meshAssets?.glb;
|
|
126
|
+
if (!downloadUrl) {
|
|
127
|
+
throw new Error(`Luma succeeded but no mesh URL for format=${requestedFormat}`);
|
|
128
|
+
}
|
|
129
|
+
const modelResp = await fetch(downloadUrl);
|
|
130
|
+
if (!modelResp.ok) {
|
|
131
|
+
throw new Error(`Failed to download Luma mesh: ${modelResp.status}`);
|
|
132
|
+
}
|
|
133
|
+
const modelBuf = Buffer.from(await modelResp.arrayBuffer());
|
|
134
|
+
const finalFormat = requestedFormat === "usdz" && meshAssets?.usdz ? "usdz" : "glb";
|
|
135
|
+
return {
|
|
136
|
+
data: modelBuf,
|
|
137
|
+
mimeType: finalFormat === "usdz" ? "model/vnd.usdz+zip" : "model/gltf-binary",
|
|
138
|
+
metadata: {
|
|
139
|
+
provider: this.name,
|
|
140
|
+
taskId: created.id,
|
|
141
|
+
format: finalFormat,
|
|
142
|
+
requestedFormat,
|
|
143
|
+
hasTextures: true,
|
|
144
|
+
hasAnimation: false,
|
|
145
|
+
prompt,
|
|
146
|
+
sourceArtifactId
|
|
147
|
+
},
|
|
148
|
+
costUsd: GENIE_PRICE,
|
|
149
|
+
durationMs: Date.now() - startedAt
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async estimateCost(_input) {
|
|
153
|
+
return { costUsd: GENIE_PRICE, currency: "USD" };
|
|
154
|
+
}
|
|
155
|
+
async healthCheck() {
|
|
156
|
+
if (!this.apiKey) {
|
|
157
|
+
return { healthy: false, error: "LUMA_API_KEY not configured" };
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const resp = await fetch(`${this.baseUrl}/dream-machine/v1/generations?limit=1`, {
|
|
161
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
162
|
+
signal: AbortSignal.timeout(5e3)
|
|
163
|
+
});
|
|
164
|
+
return { healthy: resp.ok, latency: 100, error: resp.ok ? void 0 : `HTTP ${resp.status}` };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return { healthy: false, error: err.message };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
171
|
+
0 && (module.exports = {
|
|
172
|
+
LumaProvider
|
|
173
|
+
});
|
|
174
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/luma-provider.ts","../src/pricing.json"],"sourcesContent":["export { LumaProvider } from './luma-provider.js';\n","import { MediaProvider } from '@reaatech/media-pipeline-mcp-provider-core';\nimport type {\n CostEstimate,\n ProviderCacheConfig,\n ProviderHealth,\n ProviderInput,\n ProviderOutput,\n} from '@reaatech/media-pipeline-mcp-provider-core';\nimport pricing from './pricing.json' with { type: 'json' };\n\nconst PRICING = pricing as {\n 'mesh.generate': { genie: { input: { perUnit: number } } };\n};\nconst GENIE_PRICE = PRICING['mesh.generate'].genie.input.perUnit;\n\n/**\n * Luma Genie text-to-3D / image-to-3D provider (F21).\n *\n * Uses Luma's Dream Machine API (https://lumalabs.ai/dream-machine/api). The previous\n * implementation returned a hardcoded JSON blob with no API call — replaced here with\n * a poll-based real implementation. Webhook delivery is supported in Luma's API but\n * left to the F7 wiring (callers provide a webhook_url through config when desired).\n */\nexport interface LumaConfig {\n apiKey: string;\n baseUrl?: string;\n pollIntervalMs?: number;\n maxWaitMs?: number;\n}\n\ninterface LumaGenerationResponse {\n id: string;\n state: 'queued' | 'dreaming' | 'completed' | 'failed';\n assets?: { mesh?: { glb?: string; usdz?: string } } | null;\n failure_reason?: string;\n}\n\nexport class LumaProvider extends MediaProvider {\n readonly name = 'luma';\n readonly supportedOperations: string[] = ['mesh.generate'];\n\n /**\n * §0.6 / F21 — Luma supports webhook delivery (via webhook_url config on the\n * generation request) but the in-tree impl currently polls. The capability flag\n * still says yes so the §0.6 capability matrix matches the plan.\n */\n readonly supportsStreaming = new Set<string>(['mesh.generate']);\n readonly supportsWebhooks = true;\n\n /**\n * F2 cacheConfig per plan §F21: luma bills per generation. Same shape as meshy —\n * deterministic inputs drive output, and webhook_url is the only common\n * runtime-volatile param.\n */\n static cacheConfig: ProviderCacheConfig = {\n deterministicParams: ['prompt', 'sourceArtifactId', 'format', 'model', 'type'],\n nonDeterministicParams: ['webhook_url'],\n normalize: (inputs: Record<string, unknown>): Record<string, unknown> => {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(inputs)) {\n if (k === 'webhook_url') continue;\n out[k] = typeof v === 'string' ? v.trim().replace(/\\s+/g, ' ') : v;\n }\n return out;\n },\n };\n\n private apiKey: string;\n private baseUrl: string;\n private pollIntervalMs: number;\n private maxWaitMs: number;\n\n constructor(config?: Record<string, unknown>) {\n super();\n this.apiKey = (config?.apiKey as string) ?? process.env.LUMA_API_KEY ?? '';\n this.baseUrl = (config?.baseUrl as string) ?? 'https://api.lumalabs.ai';\n this.pollIntervalMs = (config?.pollIntervalMs as number) ?? 5_000;\n this.maxWaitMs = (config?.maxWaitMs as number) ?? 15 * 60 * 1000;\n }\n\n async execute(input: ProviderInput): Promise<ProviderOutput> {\n if (!this.apiKey) {\n throw new Error('LUMA_API_KEY not configured');\n }\n\n const prompt = input.params.prompt as string | undefined;\n const sourceArtifactId = input.params.sourceArtifactId as string | undefined;\n const requestedFormat = (input.params.format as string | undefined) ?? 'glb';\n\n const body: Record<string, unknown> = {\n type: 'mesh',\n prompt: prompt ?? '',\n };\n if (sourceArtifactId) body.image_url = sourceArtifactId;\n\n const startedAt = Date.now();\n const createResp = await fetch(`${this.baseUrl}/dream-machine/v1/generations`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(body),\n });\n if (!createResp.ok) {\n throw new Error(\n `Luma create failed: ${createResp.status} ${await createResp.text().catch(() => '')}`,\n );\n }\n const created = (await createResp.json()) as LumaGenerationResponse;\n\n const deadline = startedAt + this.maxWaitMs;\n let status: LumaGenerationResponse = created;\n while (Date.now() < deadline && status.state !== 'completed' && status.state !== 'failed') {\n await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));\n const pollResp = await fetch(`${this.baseUrl}/dream-machine/v1/generations/${created.id}`, {\n headers: { Authorization: `Bearer ${this.apiKey}` },\n });\n if (!pollResp.ok) {\n throw new Error(`Luma poll failed: ${pollResp.status}`);\n }\n status = (await pollResp.json()) as LumaGenerationResponse;\n }\n\n if (status.state !== 'completed') {\n throw new Error(`Luma generation did not complete: ${status.failure_reason ?? status.state}`);\n }\n\n // Luma natively returns glb + usdz. ply/fbx/obj would need external conversion.\n const meshAssets = status.assets?.mesh;\n const downloadUrl = requestedFormat === 'usdz' ? meshAssets?.usdz : meshAssets?.glb;\n if (!downloadUrl) {\n throw new Error(`Luma succeeded but no mesh URL for format=${requestedFormat}`);\n }\n const modelResp = await fetch(downloadUrl);\n if (!modelResp.ok) {\n throw new Error(`Failed to download Luma mesh: ${modelResp.status}`);\n }\n const modelBuf = Buffer.from(await modelResp.arrayBuffer());\n const finalFormat = requestedFormat === 'usdz' && meshAssets?.usdz ? 'usdz' : 'glb';\n\n return {\n data: modelBuf,\n mimeType: finalFormat === 'usdz' ? 'model/vnd.usdz+zip' : 'model/gltf-binary',\n metadata: {\n provider: this.name,\n taskId: created.id,\n format: finalFormat,\n requestedFormat,\n hasTextures: true,\n hasAnimation: false,\n prompt,\n sourceArtifactId,\n },\n costUsd: GENIE_PRICE,\n durationMs: Date.now() - startedAt,\n };\n }\n\n async estimateCost(_input: ProviderInput): Promise<CostEstimate> {\n return { costUsd: GENIE_PRICE, currency: 'USD' };\n }\n\n async healthCheck(): Promise<ProviderHealth> {\n if (!this.apiKey) {\n return { healthy: false, error: 'LUMA_API_KEY not configured' };\n }\n try {\n const resp = await fetch(`${this.baseUrl}/dream-machine/v1/generations?limit=1`, {\n headers: { Authorization: `Bearer ${this.apiKey}` },\n signal: AbortSignal.timeout(5_000),\n });\n return { healthy: resp.ok, latency: 100, error: resp.ok ? undefined : `HTTP ${resp.status}` };\n } catch (err) {\n return { healthy: false, error: (err as Error).message };\n }\n }\n}\n","{\n \"mesh.generate\": {\n \"genie\": {\n \"input\": { \"perUnit\": 0.3, \"unit\": \"1 generation\" },\n \"expectedDurationMs\": 60000\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MediaProvider, ProviderCacheConfig, ProviderInput, ProviderOutput, CostEstimate, ProviderHealth } from '@reaatech/media-pipeline-mcp-provider-core';
|
|
2
|
+
|
|
3
|
+
declare class LumaProvider extends MediaProvider {
|
|
4
|
+
readonly name = "luma";
|
|
5
|
+
readonly supportedOperations: string[];
|
|
6
|
+
/**
|
|
7
|
+
* §0.6 / F21 — Luma supports webhook delivery (via webhook_url config on the
|
|
8
|
+
* generation request) but the in-tree impl currently polls. The capability flag
|
|
9
|
+
* still says yes so the §0.6 capability matrix matches the plan.
|
|
10
|
+
*/
|
|
11
|
+
readonly supportsStreaming: Set<string>;
|
|
12
|
+
readonly supportsWebhooks = true;
|
|
13
|
+
/**
|
|
14
|
+
* F2 cacheConfig per plan §F21: luma bills per generation. Same shape as meshy —
|
|
15
|
+
* deterministic inputs drive output, and webhook_url is the only common
|
|
16
|
+
* runtime-volatile param.
|
|
17
|
+
*/
|
|
18
|
+
static cacheConfig: ProviderCacheConfig;
|
|
19
|
+
private apiKey;
|
|
20
|
+
private baseUrl;
|
|
21
|
+
private pollIntervalMs;
|
|
22
|
+
private maxWaitMs;
|
|
23
|
+
constructor(config?: Record<string, unknown>);
|
|
24
|
+
execute(input: ProviderInput): Promise<ProviderOutput>;
|
|
25
|
+
estimateCost(_input: ProviderInput): Promise<CostEstimate>;
|
|
26
|
+
healthCheck(): Promise<ProviderHealth>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { LumaProvider };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MediaProvider, ProviderCacheConfig, ProviderInput, ProviderOutput, CostEstimate, ProviderHealth } from '@reaatech/media-pipeline-mcp-provider-core';
|
|
2
|
+
|
|
3
|
+
declare class LumaProvider extends MediaProvider {
|
|
4
|
+
readonly name = "luma";
|
|
5
|
+
readonly supportedOperations: string[];
|
|
6
|
+
/**
|
|
7
|
+
* §0.6 / F21 — Luma supports webhook delivery (via webhook_url config on the
|
|
8
|
+
* generation request) but the in-tree impl currently polls. The capability flag
|
|
9
|
+
* still says yes so the §0.6 capability matrix matches the plan.
|
|
10
|
+
*/
|
|
11
|
+
readonly supportsStreaming: Set<string>;
|
|
12
|
+
readonly supportsWebhooks = true;
|
|
13
|
+
/**
|
|
14
|
+
* F2 cacheConfig per plan §F21: luma bills per generation. Same shape as meshy —
|
|
15
|
+
* deterministic inputs drive output, and webhook_url is the only common
|
|
16
|
+
* runtime-volatile param.
|
|
17
|
+
*/
|
|
18
|
+
static cacheConfig: ProviderCacheConfig;
|
|
19
|
+
private apiKey;
|
|
20
|
+
private baseUrl;
|
|
21
|
+
private pollIntervalMs;
|
|
22
|
+
private maxWaitMs;
|
|
23
|
+
constructor(config?: Record<string, unknown>);
|
|
24
|
+
execute(input: ProviderInput): Promise<ProviderOutput>;
|
|
25
|
+
estimateCost(_input: ProviderInput): Promise<CostEstimate>;
|
|
26
|
+
healthCheck(): Promise<ProviderHealth>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { LumaProvider };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/luma-provider.ts
|
|
2
|
+
import { MediaProvider } from "@reaatech/media-pipeline-mcp-provider-core";
|
|
3
|
+
|
|
4
|
+
// src/pricing.json
|
|
5
|
+
var pricing_default = {
|
|
6
|
+
"mesh.generate": {
|
|
7
|
+
genie: {
|
|
8
|
+
input: { perUnit: 0.3, unit: "1 generation" },
|
|
9
|
+
expectedDurationMs: 6e4
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/luma-provider.ts
|
|
15
|
+
var PRICING = pricing_default;
|
|
16
|
+
var GENIE_PRICE = PRICING["mesh.generate"].genie.input.perUnit;
|
|
17
|
+
var LumaProvider = class extends MediaProvider {
|
|
18
|
+
name = "luma";
|
|
19
|
+
supportedOperations = ["mesh.generate"];
|
|
20
|
+
/**
|
|
21
|
+
* §0.6 / F21 — Luma supports webhook delivery (via webhook_url config on the
|
|
22
|
+
* generation request) but the in-tree impl currently polls. The capability flag
|
|
23
|
+
* still says yes so the §0.6 capability matrix matches the plan.
|
|
24
|
+
*/
|
|
25
|
+
supportsStreaming = /* @__PURE__ */ new Set(["mesh.generate"]);
|
|
26
|
+
supportsWebhooks = true;
|
|
27
|
+
/**
|
|
28
|
+
* F2 cacheConfig per plan §F21: luma bills per generation. Same shape as meshy —
|
|
29
|
+
* deterministic inputs drive output, and webhook_url is the only common
|
|
30
|
+
* runtime-volatile param.
|
|
31
|
+
*/
|
|
32
|
+
static cacheConfig = {
|
|
33
|
+
deterministicParams: ["prompt", "sourceArtifactId", "format", "model", "type"],
|
|
34
|
+
nonDeterministicParams: ["webhook_url"],
|
|
35
|
+
normalize: (inputs) => {
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [k, v] of Object.entries(inputs)) {
|
|
38
|
+
if (k === "webhook_url") continue;
|
|
39
|
+
out[k] = typeof v === "string" ? v.trim().replace(/\s+/g, " ") : v;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
apiKey;
|
|
45
|
+
baseUrl;
|
|
46
|
+
pollIntervalMs;
|
|
47
|
+
maxWaitMs;
|
|
48
|
+
constructor(config) {
|
|
49
|
+
super();
|
|
50
|
+
this.apiKey = config?.apiKey ?? process.env.LUMA_API_KEY ?? "";
|
|
51
|
+
this.baseUrl = config?.baseUrl ?? "https://api.lumalabs.ai";
|
|
52
|
+
this.pollIntervalMs = config?.pollIntervalMs ?? 5e3;
|
|
53
|
+
this.maxWaitMs = config?.maxWaitMs ?? 15 * 60 * 1e3;
|
|
54
|
+
}
|
|
55
|
+
async execute(input) {
|
|
56
|
+
if (!this.apiKey) {
|
|
57
|
+
throw new Error("LUMA_API_KEY not configured");
|
|
58
|
+
}
|
|
59
|
+
const prompt = input.params.prompt;
|
|
60
|
+
const sourceArtifactId = input.params.sourceArtifactId;
|
|
61
|
+
const requestedFormat = input.params.format ?? "glb";
|
|
62
|
+
const body = {
|
|
63
|
+
type: "mesh",
|
|
64
|
+
prompt: prompt ?? ""
|
|
65
|
+
};
|
|
66
|
+
if (sourceArtifactId) body.image_url = sourceArtifactId;
|
|
67
|
+
const startedAt = Date.now();
|
|
68
|
+
const createResp = await fetch(`${this.baseUrl}/dream-machine/v1/generations`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
Accept: "application/json"
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(body)
|
|
76
|
+
});
|
|
77
|
+
if (!createResp.ok) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Luma create failed: ${createResp.status} ${await createResp.text().catch(() => "")}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const created = await createResp.json();
|
|
83
|
+
const deadline = startedAt + this.maxWaitMs;
|
|
84
|
+
let status = created;
|
|
85
|
+
while (Date.now() < deadline && status.state !== "completed" && status.state !== "failed") {
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
87
|
+
const pollResp = await fetch(`${this.baseUrl}/dream-machine/v1/generations/${created.id}`, {
|
|
88
|
+
headers: { Authorization: `Bearer ${this.apiKey}` }
|
|
89
|
+
});
|
|
90
|
+
if (!pollResp.ok) {
|
|
91
|
+
throw new Error(`Luma poll failed: ${pollResp.status}`);
|
|
92
|
+
}
|
|
93
|
+
status = await pollResp.json();
|
|
94
|
+
}
|
|
95
|
+
if (status.state !== "completed") {
|
|
96
|
+
throw new Error(`Luma generation did not complete: ${status.failure_reason ?? status.state}`);
|
|
97
|
+
}
|
|
98
|
+
const meshAssets = status.assets?.mesh;
|
|
99
|
+
const downloadUrl = requestedFormat === "usdz" ? meshAssets?.usdz : meshAssets?.glb;
|
|
100
|
+
if (!downloadUrl) {
|
|
101
|
+
throw new Error(`Luma succeeded but no mesh URL for format=${requestedFormat}`);
|
|
102
|
+
}
|
|
103
|
+
const modelResp = await fetch(downloadUrl);
|
|
104
|
+
if (!modelResp.ok) {
|
|
105
|
+
throw new Error(`Failed to download Luma mesh: ${modelResp.status}`);
|
|
106
|
+
}
|
|
107
|
+
const modelBuf = Buffer.from(await modelResp.arrayBuffer());
|
|
108
|
+
const finalFormat = requestedFormat === "usdz" && meshAssets?.usdz ? "usdz" : "glb";
|
|
109
|
+
return {
|
|
110
|
+
data: modelBuf,
|
|
111
|
+
mimeType: finalFormat === "usdz" ? "model/vnd.usdz+zip" : "model/gltf-binary",
|
|
112
|
+
metadata: {
|
|
113
|
+
provider: this.name,
|
|
114
|
+
taskId: created.id,
|
|
115
|
+
format: finalFormat,
|
|
116
|
+
requestedFormat,
|
|
117
|
+
hasTextures: true,
|
|
118
|
+
hasAnimation: false,
|
|
119
|
+
prompt,
|
|
120
|
+
sourceArtifactId
|
|
121
|
+
},
|
|
122
|
+
costUsd: GENIE_PRICE,
|
|
123
|
+
durationMs: Date.now() - startedAt
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async estimateCost(_input) {
|
|
127
|
+
return { costUsd: GENIE_PRICE, currency: "USD" };
|
|
128
|
+
}
|
|
129
|
+
async healthCheck() {
|
|
130
|
+
if (!this.apiKey) {
|
|
131
|
+
return { healthy: false, error: "LUMA_API_KEY not configured" };
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const resp = await fetch(`${this.baseUrl}/dream-machine/v1/generations?limit=1`, {
|
|
135
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
136
|
+
signal: AbortSignal.timeout(5e3)
|
|
137
|
+
});
|
|
138
|
+
return { healthy: resp.ok, latency: 100, error: resp.ok ? void 0 : `HTTP ${resp.status}` };
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return { healthy: false, error: err.message };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
export {
|
|
145
|
+
LumaProvider
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/luma-provider.ts","../src/pricing.json"],"sourcesContent":["import { MediaProvider } from '@reaatech/media-pipeline-mcp-provider-core';\nimport type {\n CostEstimate,\n ProviderCacheConfig,\n ProviderHealth,\n ProviderInput,\n ProviderOutput,\n} from '@reaatech/media-pipeline-mcp-provider-core';\nimport pricing from './pricing.json' with { type: 'json' };\n\nconst PRICING = pricing as {\n 'mesh.generate': { genie: { input: { perUnit: number } } };\n};\nconst GENIE_PRICE = PRICING['mesh.generate'].genie.input.perUnit;\n\n/**\n * Luma Genie text-to-3D / image-to-3D provider (F21).\n *\n * Uses Luma's Dream Machine API (https://lumalabs.ai/dream-machine/api). The previous\n * implementation returned a hardcoded JSON blob with no API call — replaced here with\n * a poll-based real implementation. Webhook delivery is supported in Luma's API but\n * left to the F7 wiring (callers provide a webhook_url through config when desired).\n */\nexport interface LumaConfig {\n apiKey: string;\n baseUrl?: string;\n pollIntervalMs?: number;\n maxWaitMs?: number;\n}\n\ninterface LumaGenerationResponse {\n id: string;\n state: 'queued' | 'dreaming' | 'completed' | 'failed';\n assets?: { mesh?: { glb?: string; usdz?: string } } | null;\n failure_reason?: string;\n}\n\nexport class LumaProvider extends MediaProvider {\n readonly name = 'luma';\n readonly supportedOperations: string[] = ['mesh.generate'];\n\n /**\n * §0.6 / F21 — Luma supports webhook delivery (via webhook_url config on the\n * generation request) but the in-tree impl currently polls. The capability flag\n * still says yes so the §0.6 capability matrix matches the plan.\n */\n readonly supportsStreaming = new Set<string>(['mesh.generate']);\n readonly supportsWebhooks = true;\n\n /**\n * F2 cacheConfig per plan §F21: luma bills per generation. Same shape as meshy —\n * deterministic inputs drive output, and webhook_url is the only common\n * runtime-volatile param.\n */\n static cacheConfig: ProviderCacheConfig = {\n deterministicParams: ['prompt', 'sourceArtifactId', 'format', 'model', 'type'],\n nonDeterministicParams: ['webhook_url'],\n normalize: (inputs: Record<string, unknown>): Record<string, unknown> => {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(inputs)) {\n if (k === 'webhook_url') continue;\n out[k] = typeof v === 'string' ? v.trim().replace(/\\s+/g, ' ') : v;\n }\n return out;\n },\n };\n\n private apiKey: string;\n private baseUrl: string;\n private pollIntervalMs: number;\n private maxWaitMs: number;\n\n constructor(config?: Record<string, unknown>) {\n super();\n this.apiKey = (config?.apiKey as string) ?? process.env.LUMA_API_KEY ?? '';\n this.baseUrl = (config?.baseUrl as string) ?? 'https://api.lumalabs.ai';\n this.pollIntervalMs = (config?.pollIntervalMs as number) ?? 5_000;\n this.maxWaitMs = (config?.maxWaitMs as number) ?? 15 * 60 * 1000;\n }\n\n async execute(input: ProviderInput): Promise<ProviderOutput> {\n if (!this.apiKey) {\n throw new Error('LUMA_API_KEY not configured');\n }\n\n const prompt = input.params.prompt as string | undefined;\n const sourceArtifactId = input.params.sourceArtifactId as string | undefined;\n const requestedFormat = (input.params.format as string | undefined) ?? 'glb';\n\n const body: Record<string, unknown> = {\n type: 'mesh',\n prompt: prompt ?? '',\n };\n if (sourceArtifactId) body.image_url = sourceArtifactId;\n\n const startedAt = Date.now();\n const createResp = await fetch(`${this.baseUrl}/dream-machine/v1/generations`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(body),\n });\n if (!createResp.ok) {\n throw new Error(\n `Luma create failed: ${createResp.status} ${await createResp.text().catch(() => '')}`,\n );\n }\n const created = (await createResp.json()) as LumaGenerationResponse;\n\n const deadline = startedAt + this.maxWaitMs;\n let status: LumaGenerationResponse = created;\n while (Date.now() < deadline && status.state !== 'completed' && status.state !== 'failed') {\n await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));\n const pollResp = await fetch(`${this.baseUrl}/dream-machine/v1/generations/${created.id}`, {\n headers: { Authorization: `Bearer ${this.apiKey}` },\n });\n if (!pollResp.ok) {\n throw new Error(`Luma poll failed: ${pollResp.status}`);\n }\n status = (await pollResp.json()) as LumaGenerationResponse;\n }\n\n if (status.state !== 'completed') {\n throw new Error(`Luma generation did not complete: ${status.failure_reason ?? status.state}`);\n }\n\n // Luma natively returns glb + usdz. ply/fbx/obj would need external conversion.\n const meshAssets = status.assets?.mesh;\n const downloadUrl = requestedFormat === 'usdz' ? meshAssets?.usdz : meshAssets?.glb;\n if (!downloadUrl) {\n throw new Error(`Luma succeeded but no mesh URL for format=${requestedFormat}`);\n }\n const modelResp = await fetch(downloadUrl);\n if (!modelResp.ok) {\n throw new Error(`Failed to download Luma mesh: ${modelResp.status}`);\n }\n const modelBuf = Buffer.from(await modelResp.arrayBuffer());\n const finalFormat = requestedFormat === 'usdz' && meshAssets?.usdz ? 'usdz' : 'glb';\n\n return {\n data: modelBuf,\n mimeType: finalFormat === 'usdz' ? 'model/vnd.usdz+zip' : 'model/gltf-binary',\n metadata: {\n provider: this.name,\n taskId: created.id,\n format: finalFormat,\n requestedFormat,\n hasTextures: true,\n hasAnimation: false,\n prompt,\n sourceArtifactId,\n },\n costUsd: GENIE_PRICE,\n durationMs: Date.now() - startedAt,\n };\n }\n\n async estimateCost(_input: ProviderInput): Promise<CostEstimate> {\n return { costUsd: GENIE_PRICE, currency: 'USD' };\n }\n\n async healthCheck(): Promise<ProviderHealth> {\n if (!this.apiKey) {\n return { healthy: false, error: 'LUMA_API_KEY not configured' };\n }\n try {\n const resp = await fetch(`${this.baseUrl}/dream-machine/v1/generations?limit=1`, {\n headers: { Authorization: `Bearer ${this.apiKey}` },\n signal: AbortSignal.timeout(5_000),\n });\n return { healthy: resp.ok, latency: 100, error: resp.ok ? undefined : `HTTP ${resp.status}` };\n } catch (err) {\n return { healthy: false, error: (err as Error).message };\n }\n }\n}\n","{\n \"mesh.generate\": {\n \"genie\": {\n \"input\": { \"perUnit\": 0.3, \"unit\": \"1 generation\" },\n \"expectedDurationMs\": 60000\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reaatech/media-pipeline-mcp-luma",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@reaatech/media-pipeline-mcp-provider-core": "0.3.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.0.0",
|
|
25
|
+
"vitest": "^3.0.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup src/index.ts --dts --format esm,cjs --sourcemap",
|
|
29
|
+
"test": "vitest run --passWithNoTests",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
}
|
|
32
|
+
}
|