makefx 1.6.12 → 2.0.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/CHANGELOG.md +22 -0
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +132 -50
- package/THIRD_PARTY_NOTICES.md +5 -0
- package/TRADEMARKS.md +7 -0
- package/dist/makefx.js +3176 -0
- package/package.json +43 -19
- package/makefx.mjs +0 -27779
package/dist/makefx.js
ADDED
|
@@ -0,0 +1,3176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import process4 from "node:process";
|
|
5
|
+
|
|
6
|
+
// src/lib/project.ts
|
|
7
|
+
var SERVICE_NAME = "makefx.app";
|
|
8
|
+
var CONFIG_DIR_NAME = "makefx-cli";
|
|
9
|
+
var ENVIRONMENT_ORIGINS = {
|
|
10
|
+
production: "https://makefx.app",
|
|
11
|
+
stage: "https://stage.makefx.app",
|
|
12
|
+
local: "https://localhost:3002"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/lib/version.ts
|
|
16
|
+
var CLI_VERSION = true ? "2.0.0" : "0.0.0-dev";
|
|
17
|
+
|
|
18
|
+
// src/commands/convenience.ts
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { link, lstat, open as open2, unlink } from "node:fs/promises";
|
|
21
|
+
import { resolve } from "node:path";
|
|
22
|
+
|
|
23
|
+
// src/lib/auth.ts
|
|
24
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
25
|
+
import http from "node:http";
|
|
26
|
+
import { spawn } from "node:child_process";
|
|
27
|
+
var DEFAULT_CLIENT_ID = "cli";
|
|
28
|
+
var DEFAULT_REDIRECT_PORT = 8765;
|
|
29
|
+
var AUTH_SCOPES = "openid profile email read write";
|
|
30
|
+
function cliCommand(env = process.env) {
|
|
31
|
+
return env.npm_command === "exec" ? "npx makefx" : "makefx";
|
|
32
|
+
}
|
|
33
|
+
function generateCodeVerifier() {
|
|
34
|
+
return base64UrlEncode(randomBytes(32));
|
|
35
|
+
}
|
|
36
|
+
async function generateCodeChallenge(codeVerifier) {
|
|
37
|
+
const hash = createHash("sha256").update(codeVerifier).digest();
|
|
38
|
+
return base64UrlEncode(hash);
|
|
39
|
+
}
|
|
40
|
+
function generateState() {
|
|
41
|
+
return base64UrlEncode(randomBytes(16));
|
|
42
|
+
}
|
|
43
|
+
function base64UrlEncode(buffer) {
|
|
44
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
45
|
+
}
|
|
46
|
+
async function waitForAuthorizationCode(port, expectedState) {
|
|
47
|
+
return new Promise((resolve5, reject) => {
|
|
48
|
+
const server = http.createServer((req, res) => {
|
|
49
|
+
if (!req.url) {
|
|
50
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
51
|
+
res.end(getErrorPage("invalid_request", "Invalid request"));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
55
|
+
if (url.pathname !== "/callback") {
|
|
56
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
57
|
+
res.end(getErrorPage("not_found", "Page not found"));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const code = url.searchParams.get("code");
|
|
61
|
+
const state = url.searchParams.get("state");
|
|
62
|
+
const error = url.searchParams.get("error");
|
|
63
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
64
|
+
if (state !== expectedState) {
|
|
65
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
66
|
+
res.end(getErrorPage("invalid_request", "This callback does not belong to the login in progress."));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (error) {
|
|
70
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
71
|
+
res.end(getErrorPage(error, errorDescription || void 0));
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
server.close();
|
|
74
|
+
reject(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!code) {
|
|
78
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
79
|
+
res.end(getErrorPage("invalid_request", "Missing authorization code"));
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
server.close();
|
|
82
|
+
reject(new Error("Missing authorization code"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
86
|
+
res.end(getSuccessPage());
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
server.close();
|
|
89
|
+
resolve5({ code });
|
|
90
|
+
});
|
|
91
|
+
server.listen(port, "127.0.0.1");
|
|
92
|
+
server.on("error", (error) => {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
reject(new Error(`Failed to start local callback server: ${error.message}`));
|
|
95
|
+
});
|
|
96
|
+
const timeout = setTimeout(
|
|
97
|
+
() => {
|
|
98
|
+
server.close();
|
|
99
|
+
reject(new Error("Login timed out waiting for authorization response"));
|
|
100
|
+
},
|
|
101
|
+
5 * 60 * 1e3
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function getSuccessPage() {
|
|
106
|
+
return `<!DOCTYPE html>
|
|
107
|
+
<html>
|
|
108
|
+
<head>
|
|
109
|
+
<meta charset="utf-8">
|
|
110
|
+
<title>Login Successful - ${SERVICE_NAME}</title>
|
|
111
|
+
<style>
|
|
112
|
+
body {
|
|
113
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
min-height: 100vh;
|
|
118
|
+
margin: 0;
|
|
119
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
120
|
+
color: white;
|
|
121
|
+
}
|
|
122
|
+
.container {
|
|
123
|
+
text-align: center;
|
|
124
|
+
padding: 2rem;
|
|
125
|
+
background: rgba(255, 255, 255, 0.1);
|
|
126
|
+
border-radius: 1rem;
|
|
127
|
+
backdrop-filter: blur(10px);
|
|
128
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
129
|
+
}
|
|
130
|
+
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
|
131
|
+
p { font-size: 1.1rem; opacity: 0.9; }
|
|
132
|
+
.checkmark {
|
|
133
|
+
width: 80px;
|
|
134
|
+
height: 80px;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
display: inline-block;
|
|
137
|
+
stroke-width: 3;
|
|
138
|
+
stroke: #fff;
|
|
139
|
+
stroke-miterlimit: 10;
|
|
140
|
+
box-shadow: inset 0px 0px 0px #fff;
|
|
141
|
+
animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both;
|
|
142
|
+
margin-bottom: 1rem;
|
|
143
|
+
}
|
|
144
|
+
.checkmark__circle {
|
|
145
|
+
stroke-dasharray: 166;
|
|
146
|
+
stroke-dashoffset: 166;
|
|
147
|
+
stroke-width: 3;
|
|
148
|
+
stroke-miterlimit: 10;
|
|
149
|
+
stroke: #fff;
|
|
150
|
+
fill: none;
|
|
151
|
+
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
|
152
|
+
}
|
|
153
|
+
.checkmark__check {
|
|
154
|
+
transform-origin: 50% 50%;
|
|
155
|
+
stroke-dasharray: 48;
|
|
156
|
+
stroke-dashoffset: 48;
|
|
157
|
+
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
|
|
158
|
+
}
|
|
159
|
+
@keyframes stroke {
|
|
160
|
+
100% { stroke-dashoffset: 0; }
|
|
161
|
+
}
|
|
162
|
+
@keyframes scale {
|
|
163
|
+
0%, 100% { transform: none; }
|
|
164
|
+
50% { transform: scale3d(1.1, 1.1, 1); }
|
|
165
|
+
}
|
|
166
|
+
@keyframes fill {
|
|
167
|
+
100% { box-shadow: inset 0px 0px 0px 40px #fff; }
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
170
|
+
</head>
|
|
171
|
+
<body>
|
|
172
|
+
<div class="container">
|
|
173
|
+
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
|
|
174
|
+
<circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none"/>
|
|
175
|
+
<path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
|
176
|
+
</svg>
|
|
177
|
+
<h1>Login Successful!</h1>
|
|
178
|
+
<p>You can close this window and return to the terminal.</p>
|
|
179
|
+
</div>
|
|
180
|
+
</body>
|
|
181
|
+
</html>`;
|
|
182
|
+
}
|
|
183
|
+
function escapeHtml(value) {
|
|
184
|
+
return value.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
185
|
+
}
|
|
186
|
+
function getErrorPage(error, description) {
|
|
187
|
+
return `<!DOCTYPE html>
|
|
188
|
+
<html>
|
|
189
|
+
<head>
|
|
190
|
+
<meta charset="utf-8">
|
|
191
|
+
<title>Login Failed - ${SERVICE_NAME}</title>
|
|
192
|
+
<style>
|
|
193
|
+
body {
|
|
194
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
min-height: 100vh;
|
|
199
|
+
margin: 0;
|
|
200
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
201
|
+
color: white;
|
|
202
|
+
}
|
|
203
|
+
.container {
|
|
204
|
+
text-align: center;
|
|
205
|
+
padding: 2rem;
|
|
206
|
+
background: rgba(255, 255, 255, 0.1);
|
|
207
|
+
border-radius: 1rem;
|
|
208
|
+
backdrop-filter: blur(10px);
|
|
209
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
210
|
+
max-width: 500px;
|
|
211
|
+
}
|
|
212
|
+
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
|
213
|
+
p { font-size: 1.1rem; opacity: 0.9; margin-bottom: 0.5rem; }
|
|
214
|
+
.error-code { font-family: monospace; font-size: 0.9rem; opacity: 0.7; }
|
|
215
|
+
</style>
|
|
216
|
+
</head>
|
|
217
|
+
<body>
|
|
218
|
+
<div class="container">
|
|
219
|
+
<h1>\u274C Login Failed</h1>
|
|
220
|
+
<p>${escapeHtml(description || "Authorization was denied or failed.")}</p>
|
|
221
|
+
<p class="error-code">Error: ${escapeHtml(error)}</p>
|
|
222
|
+
<p>Please return to the terminal and try again.</p>
|
|
223
|
+
</div>
|
|
224
|
+
</body>
|
|
225
|
+
</html>`;
|
|
226
|
+
}
|
|
227
|
+
async function openBrowser(url) {
|
|
228
|
+
return new Promise((resolve5, reject) => {
|
|
229
|
+
const platform = process.platform;
|
|
230
|
+
let command;
|
|
231
|
+
let args;
|
|
232
|
+
if (platform === "darwin") {
|
|
233
|
+
command = "open";
|
|
234
|
+
args = [url];
|
|
235
|
+
} else if (platform === "win32") {
|
|
236
|
+
command = "cmd";
|
|
237
|
+
args = ["/c", "start", '""', url];
|
|
238
|
+
} else {
|
|
239
|
+
command = "xdg-open";
|
|
240
|
+
args = [url];
|
|
241
|
+
}
|
|
242
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
243
|
+
child.on("error", (error) => reject(error));
|
|
244
|
+
child.on("spawn", () => {
|
|
245
|
+
child.unref();
|
|
246
|
+
resolve5();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/lib/errors.ts
|
|
252
|
+
var CliUsageError = class extends Error {
|
|
253
|
+
name = "CliUsageError";
|
|
254
|
+
};
|
|
255
|
+
var ToolCallError = class extends Error {
|
|
256
|
+
name = "ToolCallError";
|
|
257
|
+
structuredContent;
|
|
258
|
+
jsonOutput;
|
|
259
|
+
constructor(structuredContent, fallbackMessage, jsonOutput) {
|
|
260
|
+
super(typeof structuredContent.message === "string" ? structuredContent.message : fallbackMessage);
|
|
261
|
+
this.structuredContent = structuredContent;
|
|
262
|
+
this.jsonOutput = jsonOutput;
|
|
263
|
+
}
|
|
264
|
+
get code() {
|
|
265
|
+
return typeof this.structuredContent.code === "string" ? this.structuredContent.code : "tool_error";
|
|
266
|
+
}
|
|
267
|
+
get details() {
|
|
268
|
+
const details = this.structuredContent.details;
|
|
269
|
+
return details !== null && typeof details === "object" && !Array.isArray(details) ? details : void 0;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/lib/config.ts
|
|
274
|
+
import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
275
|
+
import os from "node:os";
|
|
276
|
+
import path from "node:path";
|
|
277
|
+
var DEFAULT_ENVIRONMENT = "production";
|
|
278
|
+
var CONFIG_DIR_NAME2 = CONFIG_DIR_NAME;
|
|
279
|
+
var CONFIG_FILE_NAME = "config.json";
|
|
280
|
+
async function loadMultiEnvConfig() {
|
|
281
|
+
const configPath = await getConfigPath();
|
|
282
|
+
try {
|
|
283
|
+
const raw = await readFile(configPath, "utf8");
|
|
284
|
+
const data = JSON.parse(raw);
|
|
285
|
+
if (data.environment && data.token && !data.configs) {
|
|
286
|
+
const legacyConfig = data;
|
|
287
|
+
return {
|
|
288
|
+
configs: {
|
|
289
|
+
[legacyConfig.environment]: legacyConfig
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return data;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error.code === "ENOENT") {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function saveMultiEnvConfig(multiConfig) {
|
|
302
|
+
const configPath = await getConfigPath();
|
|
303
|
+
await mkdir(path.dirname(configPath), { recursive: true, mode: 448 });
|
|
304
|
+
const temporary = `${configPath}.${process.pid}.tmp`;
|
|
305
|
+
await writeFile(temporary, JSON.stringify(multiConfig, null, 2), { encoding: "utf8", mode: 384 });
|
|
306
|
+
await chmod(temporary, 384);
|
|
307
|
+
await rename(temporary, configPath);
|
|
308
|
+
}
|
|
309
|
+
async function saveConfig(config) {
|
|
310
|
+
await withConfigLock(() => saveConfigHoldingLock(config));
|
|
311
|
+
}
|
|
312
|
+
async function saveConfigHoldingLock(config) {
|
|
313
|
+
const multiConfig = await loadMultiEnvConfig() || { configs: {} };
|
|
314
|
+
multiConfig.configs[config.environment] = config;
|
|
315
|
+
await saveMultiEnvConfig(multiConfig);
|
|
316
|
+
}
|
|
317
|
+
async function loadStoredConfig(environment) {
|
|
318
|
+
const multiConfig = await loadMultiEnvConfig();
|
|
319
|
+
if (!multiConfig) return null;
|
|
320
|
+
const env = environment || DEFAULT_ENVIRONMENT;
|
|
321
|
+
return multiConfig.configs[env] || null;
|
|
322
|
+
}
|
|
323
|
+
async function listStoredConfigs() {
|
|
324
|
+
const multiConfig = await loadMultiEnvConfig();
|
|
325
|
+
return multiConfig ? Object.values(multiConfig.configs) : [];
|
|
326
|
+
}
|
|
327
|
+
async function removeConfig(environment) {
|
|
328
|
+
await withConfigLock(() => removeConfigHoldingLock(environment));
|
|
329
|
+
}
|
|
330
|
+
async function removeConfigHoldingLock(environment) {
|
|
331
|
+
if (!environment) {
|
|
332
|
+
const configPath = await getConfigPath();
|
|
333
|
+
await rm(configPath);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const multiConfig = await loadMultiEnvConfig();
|
|
337
|
+
if (multiConfig) {
|
|
338
|
+
delete multiConfig.configs[environment];
|
|
339
|
+
if (Object.keys(multiConfig.configs).length === 0) {
|
|
340
|
+
const configPath = await getConfigPath();
|
|
341
|
+
await rm(configPath);
|
|
342
|
+
} else {
|
|
343
|
+
await saveMultiEnvConfig(multiConfig);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
var LOCK_STALE_MS = 6e4;
|
|
348
|
+
var LOCK_WAIT_MS = 1e4;
|
|
349
|
+
async function retireStaleLock(lockPath) {
|
|
350
|
+
let takeover;
|
|
351
|
+
try {
|
|
352
|
+
takeover = await open(takeoverPathFor(lockPath), "wx");
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error.code === "EEXIST") return "takeover-held";
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
await takeover.close();
|
|
359
|
+
const age = Date.now() - ((await stat(lockPath).catch(() => null))?.mtimeMs ?? Date.now());
|
|
360
|
+
if (age <= LOCK_STALE_MS) return "fresh";
|
|
361
|
+
await rm(lockPath, { force: true });
|
|
362
|
+
return "retired";
|
|
363
|
+
} finally {
|
|
364
|
+
await rm(takeoverPathFor(lockPath), { force: true });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function takeoverPathFor(lockPath) {
|
|
368
|
+
return `${lockPath}.takeover`;
|
|
369
|
+
}
|
|
370
|
+
async function withConfigLock(fn) {
|
|
371
|
+
const lockPath = `${await getConfigPath()}.lock`;
|
|
372
|
+
await mkdir(path.dirname(lockPath), { recursive: true, mode: 448 });
|
|
373
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
374
|
+
const owner = `${process.pid}:${Math.random().toString(36).slice(2)}`;
|
|
375
|
+
for (; ; ) {
|
|
376
|
+
try {
|
|
377
|
+
const handle = await open(lockPath, "wx");
|
|
378
|
+
await handle.writeFile(owner, "utf8");
|
|
379
|
+
await handle.close();
|
|
380
|
+
break;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (error.code !== "EEXIST") {
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
const age = Date.now() - ((await stat(lockPath).catch(() => null))?.mtimeMs ?? Date.now());
|
|
386
|
+
const recovery = age > LOCK_STALE_MS ? await retireStaleLock(lockPath) : "fresh";
|
|
387
|
+
if (recovery === "retired") continue;
|
|
388
|
+
if (Date.now() > deadline) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
recovery === "takeover-held" ? `A crashed process left ${takeoverPathFor(lockPath)}; remove it and ${lockPath} if no other CLI is running` : `Another process holds ${lockPath}; remove it if no other CLI is running`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
await new Promise((resolve5) => setTimeout(resolve5, 50 + Math.random() * 100));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
return await fn();
|
|
398
|
+
} finally {
|
|
399
|
+
const holder = await readFile(lockPath, "utf8").catch(() => null);
|
|
400
|
+
if (holder === owner) {
|
|
401
|
+
await rm(lockPath, { force: true });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function getConfigPath() {
|
|
406
|
+
const baseDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
407
|
+
return path.join(baseDir, CONFIG_DIR_NAME2, CONFIG_FILE_NAME);
|
|
408
|
+
}
|
|
409
|
+
function resolveBaseUrl(env) {
|
|
410
|
+
switch (env) {
|
|
411
|
+
case "production":
|
|
412
|
+
return ENVIRONMENT_ORIGINS.production;
|
|
413
|
+
case "stage":
|
|
414
|
+
case "staging":
|
|
415
|
+
return ENVIRONMENT_ORIGINS.stage;
|
|
416
|
+
case "local":
|
|
417
|
+
return ENVIRONMENT_ORIGINS.local;
|
|
418
|
+
default:
|
|
419
|
+
throw new CliUsageError(`Unknown environment "${env}". Valid options: production, stage, local`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/lib/json.ts
|
|
424
|
+
function isRecord(value) {
|
|
425
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/lib/mcp-transport.ts
|
|
429
|
+
var PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
430
|
+
function encodeHeaderValue(value) {
|
|
431
|
+
if (/^[\x21-\x7E]+$/.test(value)) {
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
return `=?base64?${Buffer.from(value, "utf8").toString("base64")}?=`;
|
|
435
|
+
}
|
|
436
|
+
function mcpRequestHeaders(line) {
|
|
437
|
+
let message;
|
|
438
|
+
try {
|
|
439
|
+
message = JSON.parse(line);
|
|
440
|
+
} catch {
|
|
441
|
+
return {};
|
|
442
|
+
}
|
|
443
|
+
if (!isRecord(message) || typeof message.method !== "string") {
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
446
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
447
|
+
const meta = isRecord(params._meta) ? params._meta : {};
|
|
448
|
+
const version = meta[PROTOCOL_VERSION_META_KEY];
|
|
449
|
+
if (typeof version !== "string") {
|
|
450
|
+
return {};
|
|
451
|
+
}
|
|
452
|
+
const headers = {
|
|
453
|
+
"MCP-Protocol-Version": version,
|
|
454
|
+
"Mcp-Method": message.method
|
|
455
|
+
};
|
|
456
|
+
if (message.method === "tools/call" && typeof params.name === "string") {
|
|
457
|
+
headers["Mcp-Name"] = encodeHeaderValue(params.name);
|
|
458
|
+
}
|
|
459
|
+
return headers;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/lib/tokens.ts
|
|
463
|
+
var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
464
|
+
var REFRESH_TIMEOUT_MS = 1e4;
|
|
465
|
+
function mcpResourceFor(baseUrl) {
|
|
466
|
+
return `${baseUrl.replace(/\/$/, "")}/mcp`;
|
|
467
|
+
}
|
|
468
|
+
function requireUrl(document, field) {
|
|
469
|
+
const value = document[field];
|
|
470
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
471
|
+
throw new Error(`Authorization server metadata is missing ${field}`);
|
|
472
|
+
}
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
async function discoverAuthorizationServer(baseUrl, fetchImpl = fetch, signal) {
|
|
476
|
+
const response = await fetchImpl(`${baseUrl}/.well-known/oauth-authorization-server`, {
|
|
477
|
+
headers: { accept: "application/json" },
|
|
478
|
+
signal
|
|
479
|
+
});
|
|
480
|
+
if (!response.ok) {
|
|
481
|
+
throw new Error(`Failed to load authorization server metadata (${response.status})`);
|
|
482
|
+
}
|
|
483
|
+
const document = await response.json();
|
|
484
|
+
const revocationEndpoint = document.revocation_endpoint;
|
|
485
|
+
return {
|
|
486
|
+
authorizationEndpoint: requireUrl(document, "authorization_endpoint"),
|
|
487
|
+
tokenEndpoint: requireUrl(document, "token_endpoint"),
|
|
488
|
+
...typeof revocationEndpoint === "string" ? { revocationEndpoint } : {}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
var TokenRequestError = class extends Error {
|
|
492
|
+
status;
|
|
493
|
+
code;
|
|
494
|
+
constructor(status, code, description) {
|
|
495
|
+
super(description ? `${code}: ${description}` : code);
|
|
496
|
+
this.name = "TokenRequestError";
|
|
497
|
+
this.status = status;
|
|
498
|
+
this.code = code;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
async function postForm(endpoint, fields, fetchImpl, signal) {
|
|
502
|
+
return fetchImpl(endpoint, {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
505
|
+
body: new URLSearchParams(fields).toString(),
|
|
506
|
+
signal
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
async function readTokenResponse(response) {
|
|
510
|
+
const body = await response.json().catch(() => ({}));
|
|
511
|
+
if (!response.ok) {
|
|
512
|
+
throw new TokenRequestError(
|
|
513
|
+
response.status,
|
|
514
|
+
typeof body.error === "string" ? body.error : `http_${response.status}`,
|
|
515
|
+
typeof body.error_description === "string" ? body.error_description : void 0
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (typeof body.access_token !== "string" || typeof body.expires_in !== "number") {
|
|
519
|
+
throw new Error("Token response is missing access_token or expires_in");
|
|
520
|
+
}
|
|
521
|
+
return body;
|
|
522
|
+
}
|
|
523
|
+
function storedTokenFrom(response, now = Date.now()) {
|
|
524
|
+
return {
|
|
525
|
+
accessToken: response.access_token,
|
|
526
|
+
expiresAt: now + response.expires_in * 1e3,
|
|
527
|
+
issuedAt: now,
|
|
528
|
+
scope: response.scope,
|
|
529
|
+
...response.refresh_token ? {
|
|
530
|
+
refreshToken: response.refresh_token,
|
|
531
|
+
refreshExpiresAt: now + (response.refresh_token_expires_in ?? 0) * 1e3
|
|
532
|
+
} : {}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async function exchangeCodeForToken(input, fetchImpl = fetch) {
|
|
536
|
+
const response = await postForm(
|
|
537
|
+
input.tokenEndpoint,
|
|
538
|
+
{
|
|
539
|
+
grant_type: "authorization_code",
|
|
540
|
+
code: input.code,
|
|
541
|
+
code_verifier: input.codeVerifier,
|
|
542
|
+
redirect_uri: input.redirectUri,
|
|
543
|
+
client_id: input.clientId,
|
|
544
|
+
resource: mcpResourceFor(input.baseUrl)
|
|
545
|
+
},
|
|
546
|
+
fetchImpl
|
|
547
|
+
);
|
|
548
|
+
return readTokenResponse(response);
|
|
549
|
+
}
|
|
550
|
+
function isAccessTokenFresh(token, now = Date.now()) {
|
|
551
|
+
return token.expiresAt - now > REFRESH_MARGIN_MS;
|
|
552
|
+
}
|
|
553
|
+
function canRefresh(token, now = Date.now()) {
|
|
554
|
+
return Boolean(token.refreshToken) && (token.refreshExpiresAt ?? 0) > now;
|
|
555
|
+
}
|
|
556
|
+
async function refreshStoredToken(config, fetchImpl = fetch) {
|
|
557
|
+
if (!canRefresh(config.token)) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
return withConfigLock(async () => {
|
|
561
|
+
const stored = await loadStoredConfig(config.environment);
|
|
562
|
+
if (stored && stored.token.accessToken !== config.token.accessToken && stored.token.expiresAt > Date.now()) {
|
|
563
|
+
return stored;
|
|
564
|
+
}
|
|
565
|
+
const current = stored ?? config;
|
|
566
|
+
if (!canRefresh(current.token)) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
return refreshUnlocked(current, fetchImpl);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async function refreshUnlocked(config, fetchImpl) {
|
|
573
|
+
const server = await discoverAuthorizationServer(
|
|
574
|
+
config.baseUrl,
|
|
575
|
+
fetchImpl,
|
|
576
|
+
AbortSignal.timeout(REFRESH_TIMEOUT_MS)
|
|
577
|
+
);
|
|
578
|
+
const response = await postForm(
|
|
579
|
+
server.tokenEndpoint,
|
|
580
|
+
{
|
|
581
|
+
grant_type: "refresh_token",
|
|
582
|
+
refresh_token: config.token.refreshToken ?? "",
|
|
583
|
+
client_id: config.clientId,
|
|
584
|
+
resource: mcpResourceFor(config.baseUrl)
|
|
585
|
+
},
|
|
586
|
+
fetchImpl,
|
|
587
|
+
AbortSignal.timeout(REFRESH_TIMEOUT_MS)
|
|
588
|
+
);
|
|
589
|
+
let tokens;
|
|
590
|
+
try {
|
|
591
|
+
tokens = await readTokenResponse(response);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
if (error instanceof TokenRequestError && error.status === 400) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
const refreshed = {
|
|
599
|
+
...config,
|
|
600
|
+
token: storedTokenFrom(tokens),
|
|
601
|
+
user: tokens.user ?? config.user,
|
|
602
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
603
|
+
};
|
|
604
|
+
await saveConfigHoldingLock(refreshed);
|
|
605
|
+
return refreshed;
|
|
606
|
+
}
|
|
607
|
+
async function ensureFreshConfig(config, fetchImpl = fetch) {
|
|
608
|
+
if (isAccessTokenFresh(config.token)) {
|
|
609
|
+
return config;
|
|
610
|
+
}
|
|
611
|
+
return refreshStoredToken(config, fetchImpl);
|
|
612
|
+
}
|
|
613
|
+
var REVOCATION_TIMEOUT_MS = 5e3;
|
|
614
|
+
async function revokeStoredToken(config, fetchImpl = fetch, timeoutMs = REVOCATION_TIMEOUT_MS) {
|
|
615
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
616
|
+
try {
|
|
617
|
+
const server = await discoverAuthorizationServer(config.baseUrl, fetchImpl, signal);
|
|
618
|
+
if (!server.revocationEndpoint) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
const token = config.token.refreshToken ?? config.token.accessToken;
|
|
622
|
+
const response = await postForm(
|
|
623
|
+
server.revocationEndpoint,
|
|
624
|
+
{
|
|
625
|
+
token,
|
|
626
|
+
token_type_hint: config.token.refreshToken ? "refresh_token" : "access_token",
|
|
627
|
+
client_id: config.clientId
|
|
628
|
+
},
|
|
629
|
+
fetchImpl,
|
|
630
|
+
signal
|
|
631
|
+
);
|
|
632
|
+
return response.ok;
|
|
633
|
+
} catch {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/lib/mcp-bridge.ts
|
|
639
|
+
function createMcpBridge(options) {
|
|
640
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
641
|
+
let config = options.config;
|
|
642
|
+
let refreshing = null;
|
|
643
|
+
const endpoint = new URL("/mcp", config.baseUrl).toString();
|
|
644
|
+
async function send(line, accessToken) {
|
|
645
|
+
return fetchImpl(endpoint, {
|
|
646
|
+
method: "POST",
|
|
647
|
+
headers: {
|
|
648
|
+
"Content-Type": "application/json",
|
|
649
|
+
Accept: "application/json, text/event-stream",
|
|
650
|
+
Authorization: `Bearer ${accessToken}`,
|
|
651
|
+
...mcpRequestHeaders(line)
|
|
652
|
+
},
|
|
653
|
+
body: line
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
async function tokenAfterRejection(rejectedToken) {
|
|
657
|
+
if (config.token.accessToken !== rejectedToken) {
|
|
658
|
+
return config.token.accessToken;
|
|
659
|
+
}
|
|
660
|
+
refreshing ??= refreshStoredToken(config, fetchImpl).finally(() => {
|
|
661
|
+
refreshing = null;
|
|
662
|
+
});
|
|
663
|
+
const refreshed = await refreshing;
|
|
664
|
+
if (!refreshed) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
config = refreshed;
|
|
668
|
+
return config.token.accessToken;
|
|
669
|
+
}
|
|
670
|
+
async function forward(line) {
|
|
671
|
+
let response;
|
|
672
|
+
try {
|
|
673
|
+
const token = config.token.accessToken;
|
|
674
|
+
response = await send(line, token);
|
|
675
|
+
if (response.status === 401) {
|
|
676
|
+
const retryToken = await tokenAfterRejection(token);
|
|
677
|
+
if (retryToken) {
|
|
678
|
+
response = await send(line, retryToken);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
} catch (error) {
|
|
682
|
+
return { kind: "unreachable", message: error instanceof Error ? error.message : String(error) };
|
|
683
|
+
}
|
|
684
|
+
if (response.status === 202) {
|
|
685
|
+
return { kind: "accepted" };
|
|
686
|
+
}
|
|
687
|
+
const text2 = (await response.text()).trim();
|
|
688
|
+
if (response.status === 401) {
|
|
689
|
+
return { kind: "unauthorized", text: text2 };
|
|
690
|
+
}
|
|
691
|
+
return { kind: "response", text: text2 };
|
|
692
|
+
}
|
|
693
|
+
return { forward, currentConfig: () => config };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/lib/tool-client.ts
|
|
697
|
+
var MCP_VERSION = "2026-07-28";
|
|
698
|
+
function responseJson(text2) {
|
|
699
|
+
if (text2.startsWith("{")) return JSON.parse(text2);
|
|
700
|
+
const data = text2.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).at(-1);
|
|
701
|
+
if (!data) throw new Error("The MCP server returned no response.");
|
|
702
|
+
return JSON.parse(data);
|
|
703
|
+
}
|
|
704
|
+
function createToolClient(config, fetchImpl = fetch) {
|
|
705
|
+
const bridge = createMcpBridge({ config, fetchImpl });
|
|
706
|
+
let nextId = 1;
|
|
707
|
+
return {
|
|
708
|
+
async call(name, args) {
|
|
709
|
+
const line = JSON.stringify({
|
|
710
|
+
jsonrpc: "2.0",
|
|
711
|
+
id: nextId++,
|
|
712
|
+
method: "tools/call",
|
|
713
|
+
params: {
|
|
714
|
+
name,
|
|
715
|
+
arguments: args,
|
|
716
|
+
_meta: {
|
|
717
|
+
"io.modelcontextprotocol/protocolVersion": MCP_VERSION,
|
|
718
|
+
"io.modelcontextprotocol/clientCapabilities": {}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
const outcome = await bridge.forward(line);
|
|
723
|
+
if (outcome.kind === "unreachable") throw new Error(`MCP request failed: ${outcome.message}`);
|
|
724
|
+
if (outcome.kind === "unauthorized") throw new Error("The stored login was rejected. Sign in again.");
|
|
725
|
+
if (outcome.kind === "accepted")
|
|
726
|
+
throw new Error("The MCP server accepted a call without returning its result.");
|
|
727
|
+
const envelope = responseJson(outcome.text);
|
|
728
|
+
if (envelope.error) throw new Error(envelope.error.message ?? "MCP protocol error.");
|
|
729
|
+
const result = envelope.result;
|
|
730
|
+
if (!result) throw new Error("The MCP server returned no tool result.");
|
|
731
|
+
if (result.isError) {
|
|
732
|
+
const fallback = result.content?.find(({ type }) => type === "text")?.text ?? "The tool call failed.";
|
|
733
|
+
throw new ToolCallError(
|
|
734
|
+
result.structuredContent ?? {
|
|
735
|
+
code: "tool_error",
|
|
736
|
+
message: fallback,
|
|
737
|
+
retryable: false
|
|
738
|
+
},
|
|
739
|
+
fallback
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
if (!result.structuredContent) throw new Error("The tool returned no structured result.");
|
|
743
|
+
return result.structuredContent;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
async function authenticatedToolClient(parsed) {
|
|
748
|
+
return createToolClient(await authenticatedConfig(parsed));
|
|
749
|
+
}
|
|
750
|
+
async function authenticatedConfig(parsed) {
|
|
751
|
+
const environment = parsed.options.local === "true" ? "local" : parsed.options.env ?? DEFAULT_ENVIRONMENT;
|
|
752
|
+
const hint = `Run: ${cliCommand()} login --env ${environment}`;
|
|
753
|
+
const stored = await loadStoredConfig(environment);
|
|
754
|
+
if (!stored) throw new Error(`Not logged in for "${environment}". ${hint}`);
|
|
755
|
+
const config = await ensureFreshConfig(stored);
|
|
756
|
+
if (!config) throw new Error(`Credentials for "${environment}" have expired. ${hint}`);
|
|
757
|
+
return config;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/commands/convenience.ts
|
|
761
|
+
var defaults = {
|
|
762
|
+
client: authenticatedToolClient,
|
|
763
|
+
write: (text2) => process.stdout.write(`${text2}
|
|
764
|
+
`),
|
|
765
|
+
openUrl: openBrowser,
|
|
766
|
+
id: randomUUID,
|
|
767
|
+
env: process.env
|
|
768
|
+
};
|
|
769
|
+
var GLOBAL_OPTIONS = ["env", "local", "json", "help"];
|
|
770
|
+
var CONVENIENCE_USAGE = {
|
|
771
|
+
export: "export --space ACCOUNT/SPACE [--starred-only] [--out PATH] [--json]",
|
|
772
|
+
open: "open SPACE | open ASSET --space ACCOUNT/SPACE [--no-open]"
|
|
773
|
+
};
|
|
774
|
+
function usage(command) {
|
|
775
|
+
return `Usage: makefx ${CONVENIENCE_USAGE[command]}`;
|
|
776
|
+
}
|
|
777
|
+
function optional(parsed, name) {
|
|
778
|
+
const value = parsed.options[name];
|
|
779
|
+
if (value === "true") throw new CliUsageError(`--${name} requires a value.`);
|
|
780
|
+
return value;
|
|
781
|
+
}
|
|
782
|
+
function required(parsed, name, command) {
|
|
783
|
+
const value = optional(parsed, name);
|
|
784
|
+
if (!value) throw new CliUsageError(`${command} requires --${name} <value>.`);
|
|
785
|
+
return value;
|
|
786
|
+
}
|
|
787
|
+
function rejectUnexpected(parsed, command, options) {
|
|
788
|
+
const allowed = /* @__PURE__ */ new Set([...GLOBAL_OPTIONS, ...options]);
|
|
789
|
+
const unexpected = Object.keys(parsed.options).find((option3) => !allowed.has(option3));
|
|
790
|
+
if (unexpected) throw new CliUsageError(`Unknown option --${unexpected}. ${usage(command)}`);
|
|
791
|
+
}
|
|
792
|
+
async function destinationAvailable(path2) {
|
|
793
|
+
try {
|
|
794
|
+
await lstat(path2);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (error.code === "ENOENT") return;
|
|
797
|
+
throw error;
|
|
798
|
+
}
|
|
799
|
+
throw new Error(`Destination already exists: ${path2}`);
|
|
800
|
+
}
|
|
801
|
+
async function publishJson(destination, value, id) {
|
|
802
|
+
await destinationAvailable(destination);
|
|
803
|
+
const temporary = `${destination}.makefx-${id()}.tmp`;
|
|
804
|
+
let published = false;
|
|
805
|
+
try {
|
|
806
|
+
const handle = await open2(temporary, "wx");
|
|
807
|
+
try {
|
|
808
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}
|
|
809
|
+
`, "utf8");
|
|
810
|
+
} finally {
|
|
811
|
+
await handle.close();
|
|
812
|
+
}
|
|
813
|
+
await link(temporary, destination);
|
|
814
|
+
published = true;
|
|
815
|
+
await unlink(temporary);
|
|
816
|
+
} finally {
|
|
817
|
+
if (!published) await unlink(temporary).catch(() => void 0);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function handleExport(parsed, dependencies = defaults) {
|
|
821
|
+
rejectUnexpected(parsed, "export", ["space", "starred-only", "out"]);
|
|
822
|
+
if (parsed.positionals.length > 0) {
|
|
823
|
+
throw new CliUsageError(`export does not accept "${parsed.positionals[0]}". ${usage("export")}`);
|
|
824
|
+
}
|
|
825
|
+
const space = required(parsed, "space", "export");
|
|
826
|
+
const out = optional(parsed, "out");
|
|
827
|
+
const destination = out ? resolve(out) : void 0;
|
|
828
|
+
if (destination) await destinationAvailable(destination);
|
|
829
|
+
const client = await dependencies.client(parsed);
|
|
830
|
+
const result = await client.call("export_space", {
|
|
831
|
+
space_id: space,
|
|
832
|
+
starred_only: parsed.options["starred-only"] === "true"
|
|
833
|
+
});
|
|
834
|
+
if (!destination) {
|
|
835
|
+
dependencies.write(JSON.stringify(result, null, 2));
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
await publishJson(destination, result, dependencies.id);
|
|
839
|
+
dependencies.write(parsed.options.json === "true" ? JSON.stringify(result, null, 2) : destination);
|
|
840
|
+
}
|
|
841
|
+
function browserDisabled(parsed, env) {
|
|
842
|
+
if (parsed.options["no-open"] === "true") return true;
|
|
843
|
+
const value = env.MAKEFX_NO_OPEN?.toLowerCase();
|
|
844
|
+
return value !== void 0 && !["", "0", "false", "no"].includes(value);
|
|
845
|
+
}
|
|
846
|
+
async function handleOpen(parsed, dependencies = defaults) {
|
|
847
|
+
rejectUnexpected(parsed, "open", ["space", "no-open"]);
|
|
848
|
+
if (parsed.positionals.length !== 1) {
|
|
849
|
+
throw new CliUsageError(`open requires one space or asset id. ${usage("open")}`);
|
|
850
|
+
}
|
|
851
|
+
const target = parsed.positionals[0];
|
|
852
|
+
const space = optional(parsed, "space");
|
|
853
|
+
const isSpace = target.includes("/");
|
|
854
|
+
if (isSpace && space) {
|
|
855
|
+
throw new CliUsageError(`A space target does not accept --space. ${usage("open")}`);
|
|
856
|
+
}
|
|
857
|
+
if (!isSpace && !space) {
|
|
858
|
+
throw new CliUsageError(`An asset target requires --space <value>. ${usage("open")}`);
|
|
859
|
+
}
|
|
860
|
+
const client = await dependencies.client(parsed);
|
|
861
|
+
const result = await client.call(
|
|
862
|
+
isSpace ? "get_space" : "get_asset",
|
|
863
|
+
isSpace ? { space_id: target, starred_only: false } : { space_id: space, asset_id: target, wait_seconds: 0 }
|
|
864
|
+
);
|
|
865
|
+
const webUrl = result.web_url;
|
|
866
|
+
if (typeof webUrl !== "string") throw new Error("The tool returned no canonical web_url.");
|
|
867
|
+
dependencies.write(webUrl);
|
|
868
|
+
if (!browserDisabled(parsed, dependencies.env)) await dependencies.openUrl(webUrl);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/commands/create.ts
|
|
872
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
873
|
+
|
|
874
|
+
// src/lib/json-schema.ts
|
|
875
|
+
import { isDeepStrictEqual } from "node:util";
|
|
876
|
+
var SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
877
|
+
"$schema",
|
|
878
|
+
"type",
|
|
879
|
+
"enum",
|
|
880
|
+
"const",
|
|
881
|
+
"minimum",
|
|
882
|
+
"maximum",
|
|
883
|
+
"minLength",
|
|
884
|
+
"maxLength",
|
|
885
|
+
"minItems",
|
|
886
|
+
"maxItems",
|
|
887
|
+
"default",
|
|
888
|
+
"title",
|
|
889
|
+
"description",
|
|
890
|
+
"preview_url",
|
|
891
|
+
"properties",
|
|
892
|
+
"required",
|
|
893
|
+
"additionalProperties",
|
|
894
|
+
"items",
|
|
895
|
+
"prefixItems",
|
|
896
|
+
"anyOf",
|
|
897
|
+
"oneOf"
|
|
898
|
+
]);
|
|
899
|
+
var TYPES = /* @__PURE__ */ new Set(["object", "array", "string", "number", "integer", "boolean", "null"]);
|
|
900
|
+
function record(value) {
|
|
901
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
902
|
+
}
|
|
903
|
+
function nonnegativeInteger(value) {
|
|
904
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
905
|
+
}
|
|
906
|
+
function schemaList(value, field) {
|
|
907
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
908
|
+
throw new Error(`${field} must be a non-empty array.`);
|
|
909
|
+
}
|
|
910
|
+
return value.map((candidate, index) => {
|
|
911
|
+
const child = record(candidate);
|
|
912
|
+
if (!child) throw new Error(`${field}[${index}] must be an object.`);
|
|
913
|
+
return child;
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
function assertJsonSchema(schema, field = "params_schema") {
|
|
917
|
+
const value = record(schema);
|
|
918
|
+
if (!value) throw new Error(`${field} must be an object.`);
|
|
919
|
+
for (const key of Object.keys(value)) {
|
|
920
|
+
if (!SCHEMA_KEYS.has(key)) throw new Error(`${field} uses unsupported keyword "${key}".`);
|
|
921
|
+
}
|
|
922
|
+
if (value.type !== void 0 && (typeof value.type !== "string" || !TYPES.has(value.type))) {
|
|
923
|
+
throw new Error(`${field}.type is invalid.`);
|
|
924
|
+
}
|
|
925
|
+
if (value.enum !== void 0 && (!Array.isArray(value.enum) || value.enum.length === 0)) {
|
|
926
|
+
throw new Error(`${field}.enum must be a non-empty array.`);
|
|
927
|
+
}
|
|
928
|
+
for (const name of ["minimum", "maximum"]) {
|
|
929
|
+
if (value[name] !== void 0 && (typeof value[name] !== "number" || !Number.isFinite(value[name]))) {
|
|
930
|
+
throw new Error(`${field}.${name} must be a finite number.`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (typeof value.minimum === "number" && typeof value.maximum === "number" && value.minimum > value.maximum) {
|
|
934
|
+
throw new Error(`${field}.minimum must not exceed maximum.`);
|
|
935
|
+
}
|
|
936
|
+
for (const name of ["minLength", "maxLength", "minItems", "maxItems"]) {
|
|
937
|
+
if (value[name] !== void 0 && !nonnegativeInteger(value[name])) {
|
|
938
|
+
throw new Error(`${field}.${name} must be a nonnegative integer.`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (typeof value.minLength === "number" && typeof value.maxLength === "number" && value.minLength > value.maxLength) {
|
|
942
|
+
throw new Error(`${field}.minLength must not exceed maxLength.`);
|
|
943
|
+
}
|
|
944
|
+
if (typeof value.minItems === "number" && typeof value.maxItems === "number" && value.minItems > value.maxItems) {
|
|
945
|
+
throw new Error(`${field}.minItems must not exceed maxItems.`);
|
|
946
|
+
}
|
|
947
|
+
for (const name of ["$schema", "title", "description", "preview_url"]) {
|
|
948
|
+
if (value[name] !== void 0 && typeof value[name] !== "string") {
|
|
949
|
+
throw new Error(`${field}.${name} must be a string.`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (value.properties !== void 0) {
|
|
953
|
+
const properties = record(value.properties);
|
|
954
|
+
if (!properties) throw new Error(`${field}.properties must be an object.`);
|
|
955
|
+
for (const [name, child] of Object.entries(properties)) {
|
|
956
|
+
assertJsonSchema(child, `${field}.properties.${name}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (value.required !== void 0 && (!Array.isArray(value.required) || value.required.some((name) => typeof name !== "string"))) {
|
|
960
|
+
throw new Error(`${field}.required must be an array of strings.`);
|
|
961
|
+
}
|
|
962
|
+
if (value.additionalProperties !== void 0 && typeof value.additionalProperties !== "boolean") {
|
|
963
|
+
throw new Error(`${field}.additionalProperties must be a boolean.`);
|
|
964
|
+
}
|
|
965
|
+
if (value.items !== void 0 && value.items !== false) {
|
|
966
|
+
assertJsonSchema(value.items, `${field}.items`);
|
|
967
|
+
}
|
|
968
|
+
if (value.prefixItems !== void 0) {
|
|
969
|
+
if (!Array.isArray(value.prefixItems)) throw new Error(`${field}.prefixItems must be an array.`);
|
|
970
|
+
value.prefixItems.forEach((child, index) => assertJsonSchema(child, `${field}.prefixItems[${index}]`));
|
|
971
|
+
}
|
|
972
|
+
for (const name of ["anyOf", "oneOf"]) {
|
|
973
|
+
if (value[name] === void 0) continue;
|
|
974
|
+
schemaList(value[name], `${field}.${name}`).forEach(
|
|
975
|
+
(child, index) => assertJsonSchema(child, `${field}.${name}[${index}]`)
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if ("default" in value) {
|
|
979
|
+
const result = validate(value, value.default, field);
|
|
980
|
+
if (!result.ok) throw new Error(`${field}.default is invalid: ${result.issue.message}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function issue(field, message) {
|
|
984
|
+
return { ok: false, issue: { field, message } };
|
|
985
|
+
}
|
|
986
|
+
function typeMatches(type, value) {
|
|
987
|
+
switch (type) {
|
|
988
|
+
case "object":
|
|
989
|
+
return record(value) !== void 0;
|
|
990
|
+
case "array":
|
|
991
|
+
return Array.isArray(value);
|
|
992
|
+
case "string":
|
|
993
|
+
return typeof value === "string";
|
|
994
|
+
case "number":
|
|
995
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
996
|
+
case "integer":
|
|
997
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
998
|
+
case "boolean":
|
|
999
|
+
return typeof value === "boolean";
|
|
1000
|
+
case "null":
|
|
1001
|
+
return value === null;
|
|
1002
|
+
default:
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function validate(schema, input, field) {
|
|
1007
|
+
if (typeof schema.type === "string" && !typeMatches(schema.type, input)) {
|
|
1008
|
+
return issue(field, `Expected ${schema.type}.`);
|
|
1009
|
+
}
|
|
1010
|
+
if (schema.const !== void 0 && !isDeepStrictEqual(input, schema.const)) {
|
|
1011
|
+
return issue(field, `Expected ${JSON.stringify(schema.const)}.`);
|
|
1012
|
+
}
|
|
1013
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => isDeepStrictEqual(input, candidate))) {
|
|
1014
|
+
return issue(field, `Expected one of ${schema.enum.map((value) => JSON.stringify(value)).join(", ")}.`);
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof input === "number") {
|
|
1017
|
+
if (typeof schema.minimum === "number" && input < schema.minimum) {
|
|
1018
|
+
return issue(field, `Must be at least ${schema.minimum}.`);
|
|
1019
|
+
}
|
|
1020
|
+
if (typeof schema.maximum === "number" && input > schema.maximum) {
|
|
1021
|
+
return issue(field, `Must be at most ${schema.maximum}.`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof input === "string") {
|
|
1025
|
+
const length = Array.from(input).length;
|
|
1026
|
+
if (typeof schema.minLength === "number" && length < schema.minLength) {
|
|
1027
|
+
return issue(field, `Must contain at least ${schema.minLength} characters.`);
|
|
1028
|
+
}
|
|
1029
|
+
if (typeof schema.maxLength === "number" && length > schema.maxLength) {
|
|
1030
|
+
return issue(field, `Must contain at most ${schema.maxLength} characters.`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
let output = input;
|
|
1034
|
+
if (Array.isArray(input)) {
|
|
1035
|
+
if (typeof schema.minItems === "number" && input.length < schema.minItems) {
|
|
1036
|
+
return issue(field, `Must contain at least ${schema.minItems} items.`);
|
|
1037
|
+
}
|
|
1038
|
+
if (typeof schema.maxItems === "number" && input.length > schema.maxItems) {
|
|
1039
|
+
return issue(field, `Must contain at most ${schema.maxItems} items.`);
|
|
1040
|
+
}
|
|
1041
|
+
const prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems : [];
|
|
1042
|
+
const values = [...input];
|
|
1043
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1044
|
+
const child = prefixItems[index] ?? record(schema.items);
|
|
1045
|
+
if (!child) {
|
|
1046
|
+
if (schema.items === false && index >= prefixItems.length) {
|
|
1047
|
+
return issue(`${field}[${index}]`, "Unexpected item.");
|
|
1048
|
+
}
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const result = validate(child, values[index], `${field}[${index}]`);
|
|
1052
|
+
if (!result.ok) return result;
|
|
1053
|
+
values[index] = result.value;
|
|
1054
|
+
}
|
|
1055
|
+
output = values;
|
|
1056
|
+
}
|
|
1057
|
+
const object2 = record(input);
|
|
1058
|
+
if (object2) {
|
|
1059
|
+
const properties = record(schema.properties) ?? {};
|
|
1060
|
+
const values = { ...object2 };
|
|
1061
|
+
for (const [name, childValue] of Object.entries(properties)) {
|
|
1062
|
+
const child = childValue;
|
|
1063
|
+
if (!(name in values) && "default" in child) values[name] = structuredClone(child.default);
|
|
1064
|
+
if (!(name in values)) continue;
|
|
1065
|
+
const result = validate(child, values[name], `${field}.${name}`);
|
|
1066
|
+
if (!result.ok) return result;
|
|
1067
|
+
values[name] = result.value;
|
|
1068
|
+
}
|
|
1069
|
+
for (const name of schema.required ?? []) {
|
|
1070
|
+
if (!(name in values)) return issue(`${field}.${name}`, "Required value is missing.");
|
|
1071
|
+
}
|
|
1072
|
+
if (schema.additionalProperties === false) {
|
|
1073
|
+
const unexpected = Object.keys(values).find((name) => !(name in properties));
|
|
1074
|
+
if (unexpected) return issue(`${field}.${unexpected}`, "Unknown parameter.");
|
|
1075
|
+
}
|
|
1076
|
+
output = values;
|
|
1077
|
+
}
|
|
1078
|
+
for (const name of ["anyOf", "oneOf"]) {
|
|
1079
|
+
if (schema[name] === void 0) continue;
|
|
1080
|
+
const matches = schemaList(schema[name], name).map((candidate) => validate(candidate, output, field)).filter((result) => result.ok);
|
|
1081
|
+
if (matches.length === 0) return issue(field, "Does not match any allowed value.");
|
|
1082
|
+
if (name === "oneOf" && matches.length !== 1) {
|
|
1083
|
+
return issue(field, "Matches more than one allowed value.");
|
|
1084
|
+
}
|
|
1085
|
+
output = matches[0].value;
|
|
1086
|
+
}
|
|
1087
|
+
return { ok: true, value: output };
|
|
1088
|
+
}
|
|
1089
|
+
function validateJsonSchema(schema, input) {
|
|
1090
|
+
return validate(schema, input, "params");
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/commands/create.ts
|
|
1094
|
+
var defaults2 = {
|
|
1095
|
+
client: authenticatedToolClient,
|
|
1096
|
+
write: (text2) => process.stdout.write(`${text2}
|
|
1097
|
+
`),
|
|
1098
|
+
id: randomUUID2
|
|
1099
|
+
};
|
|
1100
|
+
var USAGE = "Usage: makefx create --space ACCOUNT/SPACE --kind image|video|audio --model MODEL [--prompt TEXT] [--ref ASSET:SLOT]... [--param NAME=VALUE]... [--count 1..8] [--name TEXT] [--seed INTEGER] [--position JSON] [--tags JSON] [--note TEXT] [--from-asset ASSET] [--recipe-mode current|exact] [--request-id ID] [--wait] [--json]";
|
|
1101
|
+
var CREATE_OPTIONS = /* @__PURE__ */ new Set([
|
|
1102
|
+
"env",
|
|
1103
|
+
"local",
|
|
1104
|
+
"json",
|
|
1105
|
+
"help",
|
|
1106
|
+
"space",
|
|
1107
|
+
"kind",
|
|
1108
|
+
"model",
|
|
1109
|
+
"prompt",
|
|
1110
|
+
"ref",
|
|
1111
|
+
"param",
|
|
1112
|
+
"count",
|
|
1113
|
+
"name",
|
|
1114
|
+
"seed",
|
|
1115
|
+
"position",
|
|
1116
|
+
"tags",
|
|
1117
|
+
"note",
|
|
1118
|
+
"from-asset",
|
|
1119
|
+
"recipe-mode",
|
|
1120
|
+
"request-id",
|
|
1121
|
+
"wait"
|
|
1122
|
+
]);
|
|
1123
|
+
function required2(parsed, name) {
|
|
1124
|
+
const value = parsed.options[name];
|
|
1125
|
+
if (!value || value === "true") throw new CliUsageError(`create requires --${name} <value>.`);
|
|
1126
|
+
return value;
|
|
1127
|
+
}
|
|
1128
|
+
function optional2(parsed, name) {
|
|
1129
|
+
const value = parsed.options[name];
|
|
1130
|
+
if (value === "true") throw new CliUsageError(`--${name} requires a value.`);
|
|
1131
|
+
return value;
|
|
1132
|
+
}
|
|
1133
|
+
function rejectUnexpected2(parsed) {
|
|
1134
|
+
if (parsed.positionals.length > 0) {
|
|
1135
|
+
throw new CliUsageError(`create does not accept "${parsed.positionals[0]}". ${USAGE}`);
|
|
1136
|
+
}
|
|
1137
|
+
const unexpected = Object.keys(parsed.options).find((name) => !CREATE_OPTIONS.has(name));
|
|
1138
|
+
if (unexpected) throw new CliUsageError(`Unknown option --${unexpected}. ${USAGE}`);
|
|
1139
|
+
}
|
|
1140
|
+
function rejectFlagValues(parsed) {
|
|
1141
|
+
for (const name of ["local", "json", "help", "wait"]) {
|
|
1142
|
+
const value = parsed.options[name];
|
|
1143
|
+
if (value !== void 0 && value !== "true") {
|
|
1144
|
+
throw new CliUsageError(`--${name} does not accept a value. ${USAGE}`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function pair(value, option3) {
|
|
1149
|
+
const at = value.lastIndexOf(":");
|
|
1150
|
+
if (at <= 0 || at === value.length - 1) throw new CliUsageError(`${option3} must be written as value:name.`);
|
|
1151
|
+
return [value.slice(0, at), value.slice(at + 1)];
|
|
1152
|
+
}
|
|
1153
|
+
function parameter(value) {
|
|
1154
|
+
const at = value.indexOf("=");
|
|
1155
|
+
if (at <= 0) throw new CliUsageError("--param must be written as name=value.");
|
|
1156
|
+
return [value.slice(0, at), scalar(value.slice(at + 1))];
|
|
1157
|
+
}
|
|
1158
|
+
function integer(parsed, name) {
|
|
1159
|
+
const value = optional2(parsed, name);
|
|
1160
|
+
if (value === void 0) return void 0;
|
|
1161
|
+
if (value.trim() === "") throw new CliUsageError(`--${name} must be an integer.`);
|
|
1162
|
+
const number = Number(value);
|
|
1163
|
+
if (!Number.isSafeInteger(number)) throw new CliUsageError(`--${name} must be an integer.`);
|
|
1164
|
+
return number;
|
|
1165
|
+
}
|
|
1166
|
+
function json(parsed, name) {
|
|
1167
|
+
const value = optional2(parsed, name);
|
|
1168
|
+
if (value === void 0) return void 0;
|
|
1169
|
+
try {
|
|
1170
|
+
return JSON.parse(value);
|
|
1171
|
+
} catch {
|
|
1172
|
+
throw new CliUsageError(`--${name} must be valid JSON.`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function positionOption(parsed) {
|
|
1176
|
+
const value = json(parsed, "position");
|
|
1177
|
+
if (value === void 0) return void 0;
|
|
1178
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => key !== "x" && key !== "y") || typeof value.x !== "number" || !Number.isFinite(value.x) || typeof value.y !== "number" || !Number.isFinite(value.y)) {
|
|
1179
|
+
throw new CliUsageError("--position must be a JSON object with finite numeric x and y.");
|
|
1180
|
+
}
|
|
1181
|
+
return value;
|
|
1182
|
+
}
|
|
1183
|
+
function tagsOption(parsed) {
|
|
1184
|
+
const value = json(parsed, "tags");
|
|
1185
|
+
if (value === void 0) return [];
|
|
1186
|
+
if (!Array.isArray(value) || value.length > 16 || !value.every((tag) => typeof tag === "string" && tag.length <= 40)) {
|
|
1187
|
+
throw new CliUsageError("--tags must be a JSON array of at most 16 strings up to 40 characters.");
|
|
1188
|
+
}
|
|
1189
|
+
return value;
|
|
1190
|
+
}
|
|
1191
|
+
function scalar(value) {
|
|
1192
|
+
if (value === "true") return true;
|
|
1193
|
+
if (value === "false") return false;
|
|
1194
|
+
if (value === "null") return null;
|
|
1195
|
+
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(value)) return Number(value);
|
|
1196
|
+
if (value.startsWith("[") || value.startsWith("{")) {
|
|
1197
|
+
try {
|
|
1198
|
+
return JSON.parse(value);
|
|
1199
|
+
} catch {
|
|
1200
|
+
throw new CliUsageError("--param arrays and objects must be valid JSON.");
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return value;
|
|
1204
|
+
}
|
|
1205
|
+
function prepareCreate(parsed) {
|
|
1206
|
+
rejectUnexpected2(parsed);
|
|
1207
|
+
rejectFlagValues(parsed);
|
|
1208
|
+
optional2(parsed, "env");
|
|
1209
|
+
const kind = required2(parsed, "kind");
|
|
1210
|
+
if (!["image", "video", "audio"].includes(kind)) {
|
|
1211
|
+
throw new CliUsageError("--kind must be image, video, or audio.");
|
|
1212
|
+
}
|
|
1213
|
+
const model = required2(parsed, "model");
|
|
1214
|
+
const references = (parsed.values.ref ?? []).map((value, order) => {
|
|
1215
|
+
const [asset_id, slot] = pair(value, "--ref");
|
|
1216
|
+
if (asset_id.length > 64 || slot.length > 64) {
|
|
1217
|
+
throw new CliUsageError("--ref asset and slot must each be at most 64 characters.");
|
|
1218
|
+
}
|
|
1219
|
+
return { asset_id, slot, order };
|
|
1220
|
+
});
|
|
1221
|
+
if (references.length > 64) throw new CliUsageError("create accepts at most 64 --ref values.");
|
|
1222
|
+
const count = parsed.options.count === void 0 ? 1 : Number(parsed.options.count);
|
|
1223
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > 8)
|
|
1224
|
+
throw new CliUsageError("--count must be an integer from 1 to 8.");
|
|
1225
|
+
const recipeMode = optional2(parsed, "recipe-mode");
|
|
1226
|
+
if (recipeMode !== void 0 && recipeMode !== "current" && recipeMode !== "exact") {
|
|
1227
|
+
throw new CliUsageError("--recipe-mode must be current or exact.");
|
|
1228
|
+
}
|
|
1229
|
+
const fromAssetId = optional2(parsed, "from-asset");
|
|
1230
|
+
if (recipeMode === "exact" && !fromAssetId) {
|
|
1231
|
+
throw new CliUsageError("--recipe-mode exact requires --from-asset <asset>.");
|
|
1232
|
+
}
|
|
1233
|
+
const name = optional2(parsed, "name");
|
|
1234
|
+
if (name !== void 0 && (!name.trim() || name.length > 120)) {
|
|
1235
|
+
throw new CliUsageError("--name must be 1 to 120 characters and not blank.");
|
|
1236
|
+
}
|
|
1237
|
+
const seed = integer(parsed, "seed");
|
|
1238
|
+
const position = positionOption(parsed);
|
|
1239
|
+
const tags = tagsOption(parsed);
|
|
1240
|
+
const note = optional2(parsed, "note");
|
|
1241
|
+
if (note !== void 0 && note.length > 4e3) {
|
|
1242
|
+
throw new CliUsageError("--note must be at most 4000 characters.");
|
|
1243
|
+
}
|
|
1244
|
+
const requestId = optional2(parsed, "request-id");
|
|
1245
|
+
if (requestId !== void 0 && requestId.length > 64) {
|
|
1246
|
+
throw new CliUsageError("--request-id must be at most 64 characters.");
|
|
1247
|
+
}
|
|
1248
|
+
const prompt = optional2(parsed, "prompt") ?? "";
|
|
1249
|
+
if (prompt.length > 8e3) throw new CliUsageError("--prompt must be at most 8000 characters.");
|
|
1250
|
+
return {
|
|
1251
|
+
spaceId: required2(parsed, "space"),
|
|
1252
|
+
kind,
|
|
1253
|
+
model,
|
|
1254
|
+
prompt,
|
|
1255
|
+
references,
|
|
1256
|
+
params: Object.fromEntries((parsed.values.param ?? []).map(parameter)),
|
|
1257
|
+
count,
|
|
1258
|
+
...recipeMode === void 0 ? {} : { recipeMode },
|
|
1259
|
+
...fromAssetId && fromAssetId !== "true" ? { fromAssetId } : {},
|
|
1260
|
+
...name ? { name } : {},
|
|
1261
|
+
...seed !== void 0 ? { seed } : {},
|
|
1262
|
+
...position !== void 0 ? { position } : {},
|
|
1263
|
+
tags,
|
|
1264
|
+
...note !== void 0 ? { note } : {},
|
|
1265
|
+
...requestId ? { requestId } : {}
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function catalogModels(document) {
|
|
1269
|
+
if (typeof document.credit_eur !== "number" || !Number.isFinite(document.credit_eur) || document.credit_eur <= 0) {
|
|
1270
|
+
throw new Error("list_models returned a malformed catalog: credit_eur must be a positive number.");
|
|
1271
|
+
}
|
|
1272
|
+
if (!Array.isArray(document.actions)) {
|
|
1273
|
+
throw new Error("list_models returned a malformed catalog: actions must be an array.");
|
|
1274
|
+
}
|
|
1275
|
+
if (!Array.isArray(document.models))
|
|
1276
|
+
throw new Error("list_models returned a malformed catalog: models must be an array.");
|
|
1277
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1278
|
+
return document.models.map((value, index) => {
|
|
1279
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1280
|
+
throw new Error(`list_models returned a malformed catalog: models[${index}] must be an object.`);
|
|
1281
|
+
}
|
|
1282
|
+
const model = value;
|
|
1283
|
+
if (typeof model.id !== "string" || model.id.length === 0) {
|
|
1284
|
+
throw new Error(`list_models returned a malformed catalog: models[${index}].id must be a string.`);
|
|
1285
|
+
}
|
|
1286
|
+
if (ids.has(model.id))
|
|
1287
|
+
throw new Error(`list_models returned a malformed catalog: duplicate model "${model.id}".`);
|
|
1288
|
+
ids.add(model.id);
|
|
1289
|
+
if (!["image", "video", "audio"].includes(String(model.kind))) {
|
|
1290
|
+
throw new Error(`list_models returned a malformed catalog: model "${model.id}" has an invalid kind.`);
|
|
1291
|
+
}
|
|
1292
|
+
if (!["available", "preview", "unavailable"].includes(String(model.availability))) {
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
`list_models returned a malformed catalog: model "${model.id}" has invalid availability.`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof model.hidden !== "boolean") {
|
|
1298
|
+
throw new Error(`list_models returned a malformed catalog: model "${model.id}" has no hidden flag.`);
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
assertJsonSchema(model.params_schema, `model "${model.id}" params_schema`);
|
|
1302
|
+
if (model.params_schema.type !== "object") {
|
|
1303
|
+
throw new Error(`model "${model.id}" params_schema.type must be object.`);
|
|
1304
|
+
}
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
`list_models returned a malformed catalog: ${error instanceof Error ? error.message : "invalid params_schema."}`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
return {
|
|
1311
|
+
id: model.id,
|
|
1312
|
+
kind: model.kind,
|
|
1313
|
+
availability: model.availability,
|
|
1314
|
+
hidden: model.hidden,
|
|
1315
|
+
paramsSchema: model.params_schema
|
|
1316
|
+
};
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
function createToolArguments(prepared, entry, requestId) {
|
|
1320
|
+
if (entry.kind !== prepared.kind) {
|
|
1321
|
+
throw new CliUsageError(`Model "${prepared.model}" creates ${entry.kind} assets, not ${prepared.kind}.`);
|
|
1322
|
+
}
|
|
1323
|
+
if (entry.availability === "unavailable" && prepared.recipeMode !== "exact") {
|
|
1324
|
+
throw new CliUsageError(
|
|
1325
|
+
`Model "${prepared.model}" is unavailable. Run models --space ${prepared.spaceId} to list the catalog.`
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
const validation = validateJsonSchema(entry.paramsSchema, prepared.params);
|
|
1329
|
+
if (!validation.ok) {
|
|
1330
|
+
const field = validation.issue.field.replace(/^params\.?/, "") || "params";
|
|
1331
|
+
throw new CliUsageError(`Invalid --param ${field}: ${validation.issue.message}`);
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
space_id: prepared.spaceId,
|
|
1335
|
+
kind: prepared.kind,
|
|
1336
|
+
model: prepared.model,
|
|
1337
|
+
prompt: prepared.prompt,
|
|
1338
|
+
references: prepared.references,
|
|
1339
|
+
params: validation.value,
|
|
1340
|
+
count: prepared.count,
|
|
1341
|
+
...prepared.recipeMode === void 0 ? {} : { recipe_mode: prepared.recipeMode },
|
|
1342
|
+
...prepared.fromAssetId ? { from_asset_id: prepared.fromAssetId } : {},
|
|
1343
|
+
...prepared.name ? { name: prepared.name } : {},
|
|
1344
|
+
...prepared.seed !== void 0 ? { seed: prepared.seed } : {},
|
|
1345
|
+
...prepared.position !== void 0 ? { position: prepared.position } : {},
|
|
1346
|
+
tags: prepared.tags,
|
|
1347
|
+
...prepared.note !== void 0 ? { note: prepared.note } : {},
|
|
1348
|
+
request_id: prepared.requestId ?? requestId
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
async function handleCreate(parsed, dependencies = defaults2) {
|
|
1352
|
+
const prepared = prepareCreate(parsed);
|
|
1353
|
+
const client = await dependencies.client(parsed);
|
|
1354
|
+
const catalog = await client.call("list_models", { space_id: prepared.spaceId });
|
|
1355
|
+
const entry = catalogModels(catalog).find(({ id, hidden }) => id === prepared.model && !hidden);
|
|
1356
|
+
if (!entry) {
|
|
1357
|
+
throw new CliUsageError(
|
|
1358
|
+
`Unknown model "${prepared.model}". Run models --space ${prepared.spaceId} to list the catalog.`
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
const args = createToolArguments(prepared, entry, dependencies.id());
|
|
1362
|
+
const created = await client.call("create_asset", args);
|
|
1363
|
+
const assets = Array.isArray(created.assets) ? created.assets : [];
|
|
1364
|
+
if (parsed.options.wait === "true" && !assets.some(({ renders_in }) => renders_in === "browser")) {
|
|
1365
|
+
const spaceId2 = prepared.spaceId;
|
|
1366
|
+
const failures = await Promise.all(
|
|
1367
|
+
assets.map(async (asset) => {
|
|
1368
|
+
const assetId = asset.asset_id;
|
|
1369
|
+
if (typeof assetId !== "string") return null;
|
|
1370
|
+
while (true) {
|
|
1371
|
+
const result = await client.call("get_asset", {
|
|
1372
|
+
space_id: spaceId2,
|
|
1373
|
+
asset_id: assetId,
|
|
1374
|
+
wait_seconds: 60
|
|
1375
|
+
});
|
|
1376
|
+
const current = result.asset;
|
|
1377
|
+
if (current?.status === "ready") return null;
|
|
1378
|
+
if (current?.status === "failed") {
|
|
1379
|
+
const error = current.error;
|
|
1380
|
+
return error?.message ?? `Asset ${assetId} failed.`;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
})
|
|
1384
|
+
);
|
|
1385
|
+
const messages = failures.filter((message) => message !== null);
|
|
1386
|
+
if (messages.length > 0) throw new Error(messages.join("\n"));
|
|
1387
|
+
}
|
|
1388
|
+
if (parsed.options.json === "true") {
|
|
1389
|
+
dependencies.write(JSON.stringify(created, null, 2));
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
for (const asset of assets) {
|
|
1393
|
+
dependencies.write(
|
|
1394
|
+
[asset.asset_id, asset.web_url].filter((value) => typeof value === "string").join(" ")
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/commands/login.ts
|
|
1400
|
+
import process2 from "node:process";
|
|
1401
|
+
async function handleLogin(parsed) {
|
|
1402
|
+
const isLocal = parsed.options.local === "true";
|
|
1403
|
+
const env = isLocal ? "local" : parsed.options.env ?? DEFAULT_ENVIRONMENT;
|
|
1404
|
+
const baseUrl = resolveBaseUrl(env);
|
|
1405
|
+
const clientId = DEFAULT_CLIENT_ID;
|
|
1406
|
+
const redirectPort = DEFAULT_REDIRECT_PORT;
|
|
1407
|
+
const insecure = isLocal;
|
|
1408
|
+
if (insecure) {
|
|
1409
|
+
console.log("\u26A0\uFE0F SSL certificate verification disabled (local dev mode)");
|
|
1410
|
+
process2.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
1411
|
+
}
|
|
1412
|
+
console.log(`Starting login for environment "${env}" using ${baseUrl}`);
|
|
1413
|
+
const server = await discoverAuthorizationServer(baseUrl);
|
|
1414
|
+
const state = generateState();
|
|
1415
|
+
const redirectUri = `http://127.0.0.1:${redirectPort}/callback`;
|
|
1416
|
+
const codeVerifier = generateCodeVerifier();
|
|
1417
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
1418
|
+
const authUrl = new URL(server.authorizationEndpoint);
|
|
1419
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
1420
|
+
authUrl.searchParams.set("response_type", "code");
|
|
1421
|
+
authUrl.searchParams.set("scope", AUTH_SCOPES);
|
|
1422
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
1423
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
1424
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1425
|
+
authUrl.searchParams.set("state", state);
|
|
1426
|
+
authUrl.searchParams.set("resource", mcpResourceFor(baseUrl));
|
|
1427
|
+
console.log("Opening browser for Google authentication...");
|
|
1428
|
+
try {
|
|
1429
|
+
await openBrowser(authUrl.toString());
|
|
1430
|
+
} catch {
|
|
1431
|
+
console.warn("Unable to open browser automatically. Please copy the URL below into your browser:");
|
|
1432
|
+
console.log(authUrl.toString());
|
|
1433
|
+
}
|
|
1434
|
+
const { code } = await waitForAuthorizationCode(redirectPort, state);
|
|
1435
|
+
console.log("Received authorization code. Exchanging for access token...");
|
|
1436
|
+
const tokenResponse = await exchangeCodeForToken({
|
|
1437
|
+
baseUrl,
|
|
1438
|
+
tokenEndpoint: server.tokenEndpoint,
|
|
1439
|
+
code,
|
|
1440
|
+
codeVerifier,
|
|
1441
|
+
redirectUri,
|
|
1442
|
+
clientId
|
|
1443
|
+
});
|
|
1444
|
+
const storedConfig = {
|
|
1445
|
+
environment: env,
|
|
1446
|
+
baseUrl,
|
|
1447
|
+
clientId,
|
|
1448
|
+
token: storedTokenFrom(tokenResponse),
|
|
1449
|
+
user: tokenResponse.user ?? null,
|
|
1450
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1451
|
+
};
|
|
1452
|
+
await saveConfig(storedConfig);
|
|
1453
|
+
console.log(`Login successful. Credentials saved to ${await getConfigPath()}`);
|
|
1454
|
+
if (!storedConfig.token.refreshToken) {
|
|
1455
|
+
console.log("No refresh token was issued; you will need to log in again when the session expires.");
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/commands/logout.ts
|
|
1460
|
+
async function handleLogout(parsed) {
|
|
1461
|
+
const isLocal = parsed.options.local === "true";
|
|
1462
|
+
const environment = isLocal ? "local" : parsed.options.env || void 0;
|
|
1463
|
+
const configs = environment ? [await loadStoredConfig(environment)].filter((config) => config !== null) : await listStoredConfigs();
|
|
1464
|
+
for (const config of configs) {
|
|
1465
|
+
const revoked = await revokeStoredToken(config);
|
|
1466
|
+
console.log(
|
|
1467
|
+
revoked ? `Revoked access for environment "${config.environment}".` : `Could not reach ${config.baseUrl} to revoke access for "${config.environment}"; the grant expires on its own.`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
if (environment) {
|
|
1472
|
+
await removeConfig(environment);
|
|
1473
|
+
console.log(`Removed stored credentials for environment "${environment}".`);
|
|
1474
|
+
} else {
|
|
1475
|
+
await removeConfig();
|
|
1476
|
+
console.log("Removed all stored credentials.");
|
|
1477
|
+
}
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
if (error.code === "ENOENT") {
|
|
1480
|
+
console.log("No stored credentials were found.");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
throw error;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/commands/mcp.ts
|
|
1488
|
+
import process3 from "node:process";
|
|
1489
|
+
async function handleMcp(parsed) {
|
|
1490
|
+
const env = parsed.options.local === "true" ? "local" : parsed.options.env ?? DEFAULT_ENVIRONMENT;
|
|
1491
|
+
const loginHint = `Run: ${cliCommand()} login --env ${env}`;
|
|
1492
|
+
const stored = await loadStoredConfig(env);
|
|
1493
|
+
if (!stored) {
|
|
1494
|
+
console.error(`Not logged in for "${env}". ${loginHint}`);
|
|
1495
|
+
process3.exitCode = 1;
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const config = await ensureFreshConfig(stored);
|
|
1499
|
+
if (!config) {
|
|
1500
|
+
console.error(`Credentials for "${env}" have expired. ${loginHint}`);
|
|
1501
|
+
process3.exitCode = 1;
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const bridge = createMcpBridge({ config });
|
|
1505
|
+
async function forward(line) {
|
|
1506
|
+
const outcome = await bridge.forward(line);
|
|
1507
|
+
switch (outcome.kind) {
|
|
1508
|
+
case "accepted":
|
|
1509
|
+
return;
|
|
1510
|
+
case "response":
|
|
1511
|
+
if (outcome.text) process3.stdout.write(`${outcome.text}
|
|
1512
|
+
`);
|
|
1513
|
+
return;
|
|
1514
|
+
case "unauthorized":
|
|
1515
|
+
if (outcome.text) process3.stdout.write(`${outcome.text}
|
|
1516
|
+
`);
|
|
1517
|
+
console.error(`Access to "${env}" was revoked or has expired. ${loginHint}`);
|
|
1518
|
+
process3.exitCode = 1;
|
|
1519
|
+
process3.stdin.destroy();
|
|
1520
|
+
return;
|
|
1521
|
+
case "unreachable":
|
|
1522
|
+
console.error(`MCP request failed: ${outcome.message}`);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
let buffer = "";
|
|
1527
|
+
const pending = [];
|
|
1528
|
+
process3.stdin.setEncoding("utf8");
|
|
1529
|
+
for await (const chunk of process3.stdin) {
|
|
1530
|
+
buffer += chunk;
|
|
1531
|
+
let newline = buffer.indexOf("\n");
|
|
1532
|
+
while (newline !== -1) {
|
|
1533
|
+
const line = buffer.slice(0, newline).trim();
|
|
1534
|
+
buffer = buffer.slice(newline + 1);
|
|
1535
|
+
if (line) {
|
|
1536
|
+
pending.push(forward(line));
|
|
1537
|
+
}
|
|
1538
|
+
newline = buffer.indexOf("\n");
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
await Promise.all(pending);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// src/commands/mutate.ts
|
|
1545
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1546
|
+
var defaults3 = {
|
|
1547
|
+
client: authenticatedToolClient,
|
|
1548
|
+
write: (text2) => process.stdout.write(`${text2}
|
|
1549
|
+
`),
|
|
1550
|
+
id: randomUUID3
|
|
1551
|
+
};
|
|
1552
|
+
var GLOBAL_OPTIONS2 = ["env", "local", "json"];
|
|
1553
|
+
var USAGE2 = {
|
|
1554
|
+
"voices sync": "voices sync --account ACCOUNT [--json]",
|
|
1555
|
+
"profile update": "profile update --name TEXT [--json]",
|
|
1556
|
+
"space update": "space update --space ACCOUNT/SPACE --name TEXT [--json]",
|
|
1557
|
+
"asset update": "asset update --space ACCOUNT/SPACE --asset ID [--name TEXT] [--note TEXT|null] [--traits TEXT|null] [--tags JSON] [--starred true|false] [--position JSON] [--recipe JSON] [--json]",
|
|
1558
|
+
"asset delete": "asset delete --space ACCOUNT/SPACE --asset ID [--json]",
|
|
1559
|
+
describe: "describe --space ACCOUNT/SPACE --asset ID [--request-id ID] [--json]",
|
|
1560
|
+
link: "link --space ACCOUNT/SPACE --from ASSET --to ASSET [--label TEXT] [--json]",
|
|
1561
|
+
unlink: "unlink --space ACCOUNT/SPACE (--link ID | --from ASSET --to ASSET [--label TEXT]) [--json]"
|
|
1562
|
+
};
|
|
1563
|
+
function mutationCommandUsage(command) {
|
|
1564
|
+
return `Usage: makefx ${USAGE2[command]}`;
|
|
1565
|
+
}
|
|
1566
|
+
function required3(parsed, name, command) {
|
|
1567
|
+
const value = parsed.options[name];
|
|
1568
|
+
if (!value || value === "true") throw new CliUsageError(`${command} requires --${name} <value>.`);
|
|
1569
|
+
return value;
|
|
1570
|
+
}
|
|
1571
|
+
function optional3(parsed, name) {
|
|
1572
|
+
const value = parsed.options[name];
|
|
1573
|
+
if (value === "true") throw new CliUsageError(`--${name} requires a value.`);
|
|
1574
|
+
return value;
|
|
1575
|
+
}
|
|
1576
|
+
function rejectUnexpected3(parsed, command, options) {
|
|
1577
|
+
if (parsed.positionals.length > 0) {
|
|
1578
|
+
throw new CliUsageError(
|
|
1579
|
+
`${command} does not accept "${parsed.positionals[0]}". ${mutationCommandUsage(command)}`
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
const allowed = /* @__PURE__ */ new Set([...GLOBAL_OPTIONS2, ...options]);
|
|
1583
|
+
const unexpected = Object.keys(parsed.options).find((option3) => !allowed.has(option3));
|
|
1584
|
+
if (unexpected) {
|
|
1585
|
+
throw new CliUsageError(`Unknown option --${unexpected}. ${mutationCommandUsage(command)}`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function jsonOption(parsed, name) {
|
|
1589
|
+
const value = optional3(parsed, name);
|
|
1590
|
+
if (value === void 0) return void 0;
|
|
1591
|
+
try {
|
|
1592
|
+
return JSON.parse(value);
|
|
1593
|
+
} catch {
|
|
1594
|
+
throw new CliUsageError(`--${name} must be valid JSON.`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function nullableText(parsed, name) {
|
|
1598
|
+
const value = optional3(parsed, name);
|
|
1599
|
+
return value === "null" ? null : value;
|
|
1600
|
+
}
|
|
1601
|
+
function booleanOption(parsed, name) {
|
|
1602
|
+
const value = parsed.options[name];
|
|
1603
|
+
if (value === void 0) return void 0;
|
|
1604
|
+
if (value === "true") return true;
|
|
1605
|
+
if (value === "false") return false;
|
|
1606
|
+
throw new CliUsageError(`--${name} must be true or false.`);
|
|
1607
|
+
}
|
|
1608
|
+
function objectOption(parsed, name) {
|
|
1609
|
+
const value = jsonOption(parsed, name);
|
|
1610
|
+
if (value === void 0) return void 0;
|
|
1611
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1612
|
+
throw new CliUsageError(`--${name} must be a JSON object.`);
|
|
1613
|
+
}
|
|
1614
|
+
return value;
|
|
1615
|
+
}
|
|
1616
|
+
function tagsOption2(parsed) {
|
|
1617
|
+
const value = jsonOption(parsed, "tags");
|
|
1618
|
+
if (value === void 0) return void 0;
|
|
1619
|
+
if (!Array.isArray(value) || !value.every((tag) => typeof tag === "string")) {
|
|
1620
|
+
throw new CliUsageError("--tags must be a JSON array of strings.");
|
|
1621
|
+
}
|
|
1622
|
+
return value;
|
|
1623
|
+
}
|
|
1624
|
+
function mutationToolCall(command, parsed, requestId) {
|
|
1625
|
+
switch (command) {
|
|
1626
|
+
case "voices sync":
|
|
1627
|
+
rejectUnexpected3(parsed, command, ["account"]);
|
|
1628
|
+
return { name: "sync_voices", args: { account_id: required3(parsed, "account", command) } };
|
|
1629
|
+
case "profile update": {
|
|
1630
|
+
rejectUnexpected3(parsed, command, ["name"]);
|
|
1631
|
+
const name = required3(parsed, "name", command);
|
|
1632
|
+
if (!name.trim()) throw new CliUsageError("profile update requires a non-empty --name.");
|
|
1633
|
+
return { name: "update_profile", args: { name } };
|
|
1634
|
+
}
|
|
1635
|
+
case "space update": {
|
|
1636
|
+
rejectUnexpected3(parsed, command, ["space", "name"]);
|
|
1637
|
+
const name = required3(parsed, "name", command);
|
|
1638
|
+
if (!name.trim()) throw new CliUsageError("space update requires a non-empty --name.");
|
|
1639
|
+
return { name: "update_space", args: { space_id: required3(parsed, "space", command), name } };
|
|
1640
|
+
}
|
|
1641
|
+
case "asset update": {
|
|
1642
|
+
rejectUnexpected3(parsed, command, [
|
|
1643
|
+
"space",
|
|
1644
|
+
"asset",
|
|
1645
|
+
"name",
|
|
1646
|
+
"note",
|
|
1647
|
+
"traits",
|
|
1648
|
+
"tags",
|
|
1649
|
+
"starred",
|
|
1650
|
+
"position",
|
|
1651
|
+
"recipe"
|
|
1652
|
+
]);
|
|
1653
|
+
const name = optional3(parsed, "name");
|
|
1654
|
+
const note = nullableText(parsed, "note");
|
|
1655
|
+
const traits = nullableText(parsed, "traits");
|
|
1656
|
+
const tags = tagsOption2(parsed);
|
|
1657
|
+
const starred = booleanOption(parsed, "starred");
|
|
1658
|
+
const position = objectOption(parsed, "position");
|
|
1659
|
+
const recipe = objectOption(parsed, "recipe");
|
|
1660
|
+
return {
|
|
1661
|
+
name: "update_asset",
|
|
1662
|
+
args: {
|
|
1663
|
+
space_id: required3(parsed, "space", command),
|
|
1664
|
+
asset_id: required3(parsed, "asset", command),
|
|
1665
|
+
...name !== void 0 ? { name } : {},
|
|
1666
|
+
...note !== void 0 ? { note } : {},
|
|
1667
|
+
...traits !== void 0 ? { traits } : {},
|
|
1668
|
+
...tags !== void 0 ? { tags } : {},
|
|
1669
|
+
...starred !== void 0 ? { starred } : {},
|
|
1670
|
+
...position !== void 0 ? { position } : {},
|
|
1671
|
+
...recipe !== void 0 ? { recipe } : {}
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
case "asset delete":
|
|
1676
|
+
rejectUnexpected3(parsed, command, ["space", "asset"]);
|
|
1677
|
+
return {
|
|
1678
|
+
name: "delete_asset",
|
|
1679
|
+
args: {
|
|
1680
|
+
space_id: required3(parsed, "space", command),
|
|
1681
|
+
asset_id: required3(parsed, "asset", command)
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
case "describe":
|
|
1685
|
+
rejectUnexpected3(parsed, command, ["space", "asset", "request-id"]);
|
|
1686
|
+
const describeRequestId = optional3(parsed, "request-id") ?? requestId;
|
|
1687
|
+
if (!describeRequestId) throw new CliUsageError("describe requires a non-empty request id.");
|
|
1688
|
+
return {
|
|
1689
|
+
name: "describe_asset",
|
|
1690
|
+
args: {
|
|
1691
|
+
space_id: required3(parsed, "space", command),
|
|
1692
|
+
asset_id: required3(parsed, "asset", command),
|
|
1693
|
+
request_id: describeRequestId
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
case "link": {
|
|
1697
|
+
rejectUnexpected3(parsed, command, ["space", "from", "to", "label"]);
|
|
1698
|
+
const label = optional3(parsed, "label");
|
|
1699
|
+
return {
|
|
1700
|
+
name: "link_assets",
|
|
1701
|
+
args: {
|
|
1702
|
+
space_id: required3(parsed, "space", command),
|
|
1703
|
+
from_asset_id: required3(parsed, "from", command),
|
|
1704
|
+
to_asset_id: required3(parsed, "to", command),
|
|
1705
|
+
...label !== void 0 ? { label } : {}
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
case "unlink": {
|
|
1710
|
+
rejectUnexpected3(parsed, command, ["space", "link", "from", "to", "label"]);
|
|
1711
|
+
const linkId = optional3(parsed, "link");
|
|
1712
|
+
const from = optional3(parsed, "from");
|
|
1713
|
+
const to = optional3(parsed, "to");
|
|
1714
|
+
const label = optional3(parsed, "label");
|
|
1715
|
+
if (linkId && (from !== void 0 || to !== void 0 || label !== void 0)) {
|
|
1716
|
+
throw new CliUsageError(
|
|
1717
|
+
`unlink accepts --link or --from/--to, not both. ${mutationCommandUsage(command)}`
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
if (!linkId && (!from || !to)) {
|
|
1721
|
+
throw new CliUsageError(
|
|
1722
|
+
`unlink requires --link or both --from and --to. ${mutationCommandUsage(command)}`
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
return {
|
|
1726
|
+
name: "unlink_assets",
|
|
1727
|
+
args: {
|
|
1728
|
+
space_id: required3(parsed, "space", command),
|
|
1729
|
+
...linkId ? { link_id: linkId } : {
|
|
1730
|
+
from_asset_id: from,
|
|
1731
|
+
to_asset_id: to,
|
|
1732
|
+
...label !== void 0 ? { label } : {}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function record2(value) {
|
|
1740
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1741
|
+
}
|
|
1742
|
+
function humanOutput(command, result, args) {
|
|
1743
|
+
if (command === "voices sync") {
|
|
1744
|
+
return `${String(result.account_id)} \xB7 ${String(result.active_voice_count)} active voices \xB7 synced ${new Date(Number(result.synced_at)).toISOString()}`;
|
|
1745
|
+
}
|
|
1746
|
+
if (command === "profile update") {
|
|
1747
|
+
const user = record2(result.user) ?? {};
|
|
1748
|
+
return [user.id, user.email, user.name].filter((value) => typeof value === "string" && value.length > 0).join(" \xB7 ");
|
|
1749
|
+
}
|
|
1750
|
+
if (command === "space update") {
|
|
1751
|
+
const space = record2(result.space) ?? {};
|
|
1752
|
+
return [space.space_id, space.name, space.is_public === true ? "public" : "private", space.web_url].filter((value) => typeof value === "string" && value.length > 0).join(" \xB7 ");
|
|
1753
|
+
}
|
|
1754
|
+
if (command === "asset update") {
|
|
1755
|
+
const asset = record2(result.asset) ?? {};
|
|
1756
|
+
return [asset.asset_id, asset.web_url].filter((value) => typeof value === "string").join(" ");
|
|
1757
|
+
}
|
|
1758
|
+
if (command === "asset delete") return `Deleted ${String(result.asset_id)}`;
|
|
1759
|
+
if (command === "describe") {
|
|
1760
|
+
return [result.asset_id, result.web_url].filter((value) => typeof value === "string").join(" ");
|
|
1761
|
+
}
|
|
1762
|
+
if (command === "link") {
|
|
1763
|
+
const link4 = record2(result.link) ?? {};
|
|
1764
|
+
return [link4.link_id, `${String(link4.from_asset_id)} -> ${String(link4.to_asset_id)}`].filter((value) => typeof value === "string").join(" ");
|
|
1765
|
+
}
|
|
1766
|
+
return typeof args.link_id === "string" ? `Unlinked ${args.link_id}` : `Unlinked ${String(args.from_asset_id)} -> ${String(args.to_asset_id)}`;
|
|
1767
|
+
}
|
|
1768
|
+
async function handleMutationCommand(command, parsed, dependencies = defaults3) {
|
|
1769
|
+
const requestId = command === "describe" ? parsed.options["request-id"] ?? dependencies.id() : void 0;
|
|
1770
|
+
const call = mutationToolCall(command, parsed, requestId);
|
|
1771
|
+
const client = await dependencies.client(parsed);
|
|
1772
|
+
const result = await client.call(call.name, call.args);
|
|
1773
|
+
if (parsed.options.json === "true") {
|
|
1774
|
+
dependencies.write(JSON.stringify(result, null, 2));
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
dependencies.write(humanOutput(command, result, call.args));
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/commands/purchase.ts
|
|
1781
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1782
|
+
import { resolve as resolve2 } from "node:path";
|
|
1783
|
+
|
|
1784
|
+
// src/lib/billing.ts
|
|
1785
|
+
var BillingIdentityError = class extends Error {
|
|
1786
|
+
path;
|
|
1787
|
+
constructor(path2, message) {
|
|
1788
|
+
super(message);
|
|
1789
|
+
this.path = path2;
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
var EMAIL_PATTERN = /^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9-]*\.)+[A-Za-z]{2,}$/;
|
|
1793
|
+
function object(value, path2, fields) {
|
|
1794
|
+
if (!isRecord(value)) throw new BillingIdentityError(path2, "must be a JSON object.");
|
|
1795
|
+
const unknown = Object.keys(value).find((key) => !fields.includes(key));
|
|
1796
|
+
if (unknown !== void 0) {
|
|
1797
|
+
throw new BillingIdentityError(path2 ? `${path2}.${unknown}` : unknown, "is not a billing identity field.");
|
|
1798
|
+
}
|
|
1799
|
+
return value;
|
|
1800
|
+
}
|
|
1801
|
+
function text(record6, key, path2, field) {
|
|
1802
|
+
const name = path2 ? `${path2}.${key}` : key;
|
|
1803
|
+
const value = record6[key];
|
|
1804
|
+
if (value === void 0 && field.optional) return void 0;
|
|
1805
|
+
if (typeof value !== "string") throw new BillingIdentityError(name, "must be a string.");
|
|
1806
|
+
const min = field.min ?? 0;
|
|
1807
|
+
if (value.length < min) throw new BillingIdentityError(name, "must not be empty.");
|
|
1808
|
+
if (value.length > field.max) {
|
|
1809
|
+
throw new BillingIdentityError(name, `must be at most ${field.max} characters.`);
|
|
1810
|
+
}
|
|
1811
|
+
if (field.pattern && !field.pattern.test(value)) {
|
|
1812
|
+
throw new BillingIdentityError(name, `must be ${field.hint}.`);
|
|
1813
|
+
}
|
|
1814
|
+
return value;
|
|
1815
|
+
}
|
|
1816
|
+
function parseBillingIdentity(value) {
|
|
1817
|
+
const root = object(value, "", ["name", "email", "address", "tax_id"]);
|
|
1818
|
+
const address = object(root.address, "address", ["line1", "line2", "city", "postal_code", "state", "country"]);
|
|
1819
|
+
const identity = {
|
|
1820
|
+
name: text(root, "name", "", { min: 1, max: 200 }),
|
|
1821
|
+
email: text(root, "email", "", { max: 254, pattern: EMAIL_PATTERN, hint: "an email address" }),
|
|
1822
|
+
address: {
|
|
1823
|
+
line1: text(address, "line1", "address", { min: 1, max: 200 }),
|
|
1824
|
+
city: text(address, "city", "address", { min: 1, max: 100 }),
|
|
1825
|
+
postal_code: text(address, "postal_code", "address", { min: 1, max: 30 }),
|
|
1826
|
+
country: text(address, "country", "address", {
|
|
1827
|
+
max: 2,
|
|
1828
|
+
pattern: /^[A-Z]{2}$/,
|
|
1829
|
+
hint: "a two-letter uppercase country code"
|
|
1830
|
+
})
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
const line2 = text(address, "line2", "address", { max: 200, optional: true });
|
|
1834
|
+
if (line2 !== void 0) identity.address.line2 = line2;
|
|
1835
|
+
const state = text(address, "state", "address", { max: 100, optional: true });
|
|
1836
|
+
if (state !== void 0) identity.address.state = state;
|
|
1837
|
+
if (root.tax_id !== void 0) {
|
|
1838
|
+
const taxId = object(root.tax_id, "tax_id", ["type", "value"]);
|
|
1839
|
+
if (taxId.type !== "eu_vat") throw new BillingIdentityError("tax_id.type", "must be eu_vat.");
|
|
1840
|
+
identity.tax_id = { type: "eu_vat", value: text(taxId, "value", "tax_id", { min: 1, max: 40 }) };
|
|
1841
|
+
}
|
|
1842
|
+
return identity;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/commands/purchase.ts
|
|
1846
|
+
var defaults4 = {
|
|
1847
|
+
client: authenticatedToolClient,
|
|
1848
|
+
read: (path2) => readFile2(path2, "utf8"),
|
|
1849
|
+
write: (text2) => process.stdout.write(`${text2}
|
|
1850
|
+
`)
|
|
1851
|
+
};
|
|
1852
|
+
var USAGE3 = {
|
|
1853
|
+
"purchase create": "purchase create --account ACCOUNT --product eur20|eur100 --request-id ID [--billing JSON|@FILE] [--json]",
|
|
1854
|
+
"purchase get": "purchase get --purchase ID [--json]"
|
|
1855
|
+
};
|
|
1856
|
+
function purchaseCommandUsage(command) {
|
|
1857
|
+
return `Usage: makefx ${USAGE3[command]}`;
|
|
1858
|
+
}
|
|
1859
|
+
function option(parsed, name) {
|
|
1860
|
+
const value = parsed.options[name];
|
|
1861
|
+
if (value === "true") throw new CliUsageError(`--${name} requires a value.`);
|
|
1862
|
+
return value;
|
|
1863
|
+
}
|
|
1864
|
+
function required4(parsed, name, command) {
|
|
1865
|
+
const value = option(parsed, name);
|
|
1866
|
+
if (!value) throw new CliUsageError(`${command} requires --${name} <value>.`);
|
|
1867
|
+
return value;
|
|
1868
|
+
}
|
|
1869
|
+
function rejectUnexpected4(parsed, command, allowed) {
|
|
1870
|
+
if (parsed.positionals.length > 0) {
|
|
1871
|
+
throw new CliUsageError(
|
|
1872
|
+
`${command} does not accept "${parsed.positionals[0]}". ${purchaseCommandUsage(command)}`
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
const options = /* @__PURE__ */ new Set(["env", "local", "json", ...allowed]);
|
|
1876
|
+
const unexpected = Object.keys(parsed.options).find((name) => !options.has(name));
|
|
1877
|
+
if (unexpected) {
|
|
1878
|
+
throw new CliUsageError(`Unknown option --${unexpected}. ${purchaseCommandUsage(command)}`);
|
|
1879
|
+
}
|
|
1880
|
+
for (const flag of ["local", "json"]) {
|
|
1881
|
+
const value = parsed.options[flag];
|
|
1882
|
+
if (value !== void 0 && value !== "true") {
|
|
1883
|
+
throw new CliUsageError(`--${flag} does not accept a value. ${purchaseCommandUsage(command)}`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
option(parsed, "env");
|
|
1887
|
+
}
|
|
1888
|
+
function billingIdentity(value) {
|
|
1889
|
+
try {
|
|
1890
|
+
return parseBillingIdentity(value);
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
if (!(error instanceof BillingIdentityError)) throw error;
|
|
1893
|
+
throw new CliUsageError(`--billing${error.path ? `.${error.path}` : ""} ${error.message}`);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
async function parseBilling(source, read) {
|
|
1897
|
+
let text2 = source;
|
|
1898
|
+
if (source.startsWith("@")) {
|
|
1899
|
+
if (source.length === 1) throw new CliUsageError("--billing @FILE requires a file path.");
|
|
1900
|
+
text2 = await read(resolve2(source.slice(1)));
|
|
1901
|
+
}
|
|
1902
|
+
let value;
|
|
1903
|
+
try {
|
|
1904
|
+
value = JSON.parse(text2);
|
|
1905
|
+
} catch {
|
|
1906
|
+
throw new CliUsageError("--billing must be valid JSON or @FILE containing JSON.");
|
|
1907
|
+
}
|
|
1908
|
+
return billingIdentity(value);
|
|
1909
|
+
}
|
|
1910
|
+
async function purchaseToolCall(command, parsed, read = defaults4.read) {
|
|
1911
|
+
if (command === "purchase get") {
|
|
1912
|
+
rejectUnexpected4(parsed, command, ["purchase"]);
|
|
1913
|
+
return { name: "get_credit_purchase", args: { purchase_id: required4(parsed, "purchase", command) } };
|
|
1914
|
+
}
|
|
1915
|
+
rejectUnexpected4(parsed, command, ["account", "product", "request-id", "billing"]);
|
|
1916
|
+
const product = required4(parsed, "product", command);
|
|
1917
|
+
if (product !== "eur20" && product !== "eur100") {
|
|
1918
|
+
throw new CliUsageError("--product must be eur20 or eur100.");
|
|
1919
|
+
}
|
|
1920
|
+
const account = required4(parsed, "account", command);
|
|
1921
|
+
const requestId = required4(parsed, "request-id", command);
|
|
1922
|
+
if (account.length > 64) throw new CliUsageError("--account must be at most 64 characters.");
|
|
1923
|
+
if (requestId.length > 64) throw new CliUsageError("--request-id must be at most 64 characters.");
|
|
1924
|
+
const source = option(parsed, "billing");
|
|
1925
|
+
const billing = source === void 0 ? void 0 : await parseBilling(source, read);
|
|
1926
|
+
return {
|
|
1927
|
+
name: "create_credit_purchase",
|
|
1928
|
+
args: {
|
|
1929
|
+
account_id: account,
|
|
1930
|
+
product,
|
|
1931
|
+
request_id: requestId,
|
|
1932
|
+
...billing ? { billing } : {}
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
function humanOutput2(command, result) {
|
|
1937
|
+
const amounts = typeof result.total_amount === "number" && typeof result.currency === "string" ? `${(result.total_amount / 100).toFixed(2)} ${result.currency.toUpperCase()}` : void 0;
|
|
1938
|
+
return [
|
|
1939
|
+
result.purchase_id,
|
|
1940
|
+
result.status,
|
|
1941
|
+
amounts,
|
|
1942
|
+
command === "purchase create" ? result.payment_url : result.invoice_url
|
|
1943
|
+
].filter((value) => typeof value === "string" && value.length > 0).join(" \xB7 ");
|
|
1944
|
+
}
|
|
1945
|
+
async function handlePurchaseCommand(command, parsed, dependencies = defaults4) {
|
|
1946
|
+
const call = await purchaseToolCall(command, parsed, dependencies.read);
|
|
1947
|
+
const client = await dependencies.client(parsed);
|
|
1948
|
+
const result = await client.call(call.name, call.args);
|
|
1949
|
+
dependencies.write(
|
|
1950
|
+
parsed.options.json === "true" ? JSON.stringify(result, null, 2) : humanOutput2(command, result)
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// src/commands/read.ts
|
|
1955
|
+
var defaults5 = {
|
|
1956
|
+
client: authenticatedToolClient,
|
|
1957
|
+
write: (text2) => process.stdout.write(`${text2}
|
|
1958
|
+
`)
|
|
1959
|
+
};
|
|
1960
|
+
var GLOBAL_OPTIONS3 = ["env", "local", "json"];
|
|
1961
|
+
var USAGE4 = {
|
|
1962
|
+
account: "account [--account ID] [--json]",
|
|
1963
|
+
spaces: "spaces [--account ID] [--query TEXT] [--limit 1..100] [--json]",
|
|
1964
|
+
"space create": "space create --name NAME [--account ID] [--id ID] [--json]",
|
|
1965
|
+
"space get": "space get --space ACCOUNT/SPACE [--starred-only] [--json]",
|
|
1966
|
+
"space delete": "space delete --space ACCOUNT/SPACE [--json]",
|
|
1967
|
+
models: "models [--space ACCOUNT/SPACE] [--kind image|video|audio] [--family provider|internal|browser] [--json]",
|
|
1968
|
+
"profile get": "profile get [--json]",
|
|
1969
|
+
health: "health [--json]",
|
|
1970
|
+
estimate: "estimate --kind KIND --model MODEL [--space ACCOUNT/SPACE] [--prompt TEXT] [--ref ASSET:SLOT]... [--param NAME=VALUE]... [--count 1..8] [--from-asset ASSET] [--recipe-mode current|exact] [--json]",
|
|
1971
|
+
"asset get": "asset get --space ACCOUNT/SPACE --asset ID [--wait-seconds 0..60] [--json]"
|
|
1972
|
+
};
|
|
1973
|
+
function commandUsage(command) {
|
|
1974
|
+
return `Usage: makefx ${USAGE4[command]}`;
|
|
1975
|
+
}
|
|
1976
|
+
function required5(parsed, name, command) {
|
|
1977
|
+
const value = parsed.options[name];
|
|
1978
|
+
if (!value || value === "true") throw new CliUsageError(`${command} requires --${name} <value>.`);
|
|
1979
|
+
return value;
|
|
1980
|
+
}
|
|
1981
|
+
function rejectUnexpected5(parsed, command, options) {
|
|
1982
|
+
if (parsed.positionals.length > 0) {
|
|
1983
|
+
throw new CliUsageError(
|
|
1984
|
+
`${command} does not accept "${parsed.positionals[0]}". ${commandUsage(command)}`
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
const allowed = /* @__PURE__ */ new Set([...GLOBAL_OPTIONS3, ...options]);
|
|
1988
|
+
const unexpected = Object.keys(parsed.options).find((option3) => !allowed.has(option3));
|
|
1989
|
+
if (unexpected) throw new CliUsageError(`Unknown option --${unexpected}. ${commandUsage(command)}`);
|
|
1990
|
+
}
|
|
1991
|
+
function optional4(parsed, name) {
|
|
1992
|
+
const value = parsed.options[name];
|
|
1993
|
+
if (value === "true") throw new CliUsageError(`--${name} requires a value.`);
|
|
1994
|
+
return value;
|
|
1995
|
+
}
|
|
1996
|
+
function integerOption(parsed, name, min, max) {
|
|
1997
|
+
const value = optional4(parsed, name);
|
|
1998
|
+
if (value === void 0) return void 0;
|
|
1999
|
+
const number = Number(value);
|
|
2000
|
+
if (!Number.isSafeInteger(number) || number < min || number > max) {
|
|
2001
|
+
throw new CliUsageError(`--${name} must be an integer from ${min} to ${max}.`);
|
|
2002
|
+
}
|
|
2003
|
+
return number;
|
|
2004
|
+
}
|
|
2005
|
+
function pair2(value) {
|
|
2006
|
+
const at = value.lastIndexOf(":");
|
|
2007
|
+
if (at <= 0 || at === value.length - 1) throw new CliUsageError("--ref must be written as asset:slot.");
|
|
2008
|
+
return { asset_id: value.slice(0, at), slot: value.slice(at + 1) };
|
|
2009
|
+
}
|
|
2010
|
+
function scalar2(value) {
|
|
2011
|
+
if (value === "true") return true;
|
|
2012
|
+
if (value === "false") return false;
|
|
2013
|
+
if (value === "null") return null;
|
|
2014
|
+
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(value)) return Number(value);
|
|
2015
|
+
if (value.startsWith("[") || value.startsWith("{")) {
|
|
2016
|
+
try {
|
|
2017
|
+
return JSON.parse(value);
|
|
2018
|
+
} catch {
|
|
2019
|
+
throw new CliUsageError("--param arrays and objects must be valid JSON.");
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return value;
|
|
2023
|
+
}
|
|
2024
|
+
function parameter2(value) {
|
|
2025
|
+
const at = value.indexOf("=");
|
|
2026
|
+
if (at <= 0) throw new CliUsageError("--param must be written as name=value.");
|
|
2027
|
+
return [value.slice(0, at), scalar2(value.slice(at + 1))];
|
|
2028
|
+
}
|
|
2029
|
+
function mediaKind(parsed, command) {
|
|
2030
|
+
const kind = required5(parsed, "kind", command);
|
|
2031
|
+
if (!["image", "video", "audio"].includes(kind)) {
|
|
2032
|
+
throw new CliUsageError("--kind must be image, video, or audio.");
|
|
2033
|
+
}
|
|
2034
|
+
return kind;
|
|
2035
|
+
}
|
|
2036
|
+
function dataToolCall(command, parsed) {
|
|
2037
|
+
switch (command) {
|
|
2038
|
+
case "account": {
|
|
2039
|
+
rejectUnexpected5(parsed, command, ["account"]);
|
|
2040
|
+
const account = optional4(parsed, "account");
|
|
2041
|
+
return { name: "get_account", args: account ? { account_id: account } : {} };
|
|
2042
|
+
}
|
|
2043
|
+
case "spaces": {
|
|
2044
|
+
rejectUnexpected5(parsed, command, ["account", "query", "limit"]);
|
|
2045
|
+
const account = optional4(parsed, "account");
|
|
2046
|
+
const query = optional4(parsed, "query");
|
|
2047
|
+
const limit = integerOption(parsed, "limit", 1, 100);
|
|
2048
|
+
return {
|
|
2049
|
+
name: "list_spaces",
|
|
2050
|
+
args: {
|
|
2051
|
+
...account ? { account_id: account } : {},
|
|
2052
|
+
...query ? { query } : {},
|
|
2053
|
+
...limit ? { limit } : {}
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
case "space create": {
|
|
2058
|
+
rejectUnexpected5(parsed, command, ["name", "account", "id"]);
|
|
2059
|
+
const account = optional4(parsed, "account");
|
|
2060
|
+
const id = optional4(parsed, "id");
|
|
2061
|
+
return {
|
|
2062
|
+
name: "create_space",
|
|
2063
|
+
args: {
|
|
2064
|
+
name: required5(parsed, "name", command),
|
|
2065
|
+
...account ? { account_id: account } : {},
|
|
2066
|
+
...id ? { space_id: id } : {}
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
case "space get":
|
|
2071
|
+
rejectUnexpected5(parsed, command, ["space", "starred-only"]);
|
|
2072
|
+
return {
|
|
2073
|
+
name: "get_space",
|
|
2074
|
+
args: {
|
|
2075
|
+
space_id: required5(parsed, "space", command),
|
|
2076
|
+
starred_only: parsed.options["starred-only"] === "true"
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
case "space delete":
|
|
2080
|
+
rejectUnexpected5(parsed, command, ["space"]);
|
|
2081
|
+
return { name: "delete_space", args: { space_id: required5(parsed, "space", command) } };
|
|
2082
|
+
case "models": {
|
|
2083
|
+
rejectUnexpected5(parsed, command, ["space", "kind", "family"]);
|
|
2084
|
+
const space = optional4(parsed, "space");
|
|
2085
|
+
const kind = optional4(parsed, "kind");
|
|
2086
|
+
const family = optional4(parsed, "family");
|
|
2087
|
+
if (kind && !["image", "video", "audio"].includes(kind)) {
|
|
2088
|
+
throw new CliUsageError("--kind must be image, video, or audio.");
|
|
2089
|
+
}
|
|
2090
|
+
if (family && !["provider", "internal", "browser"].includes(family)) {
|
|
2091
|
+
throw new CliUsageError("--family must be provider, internal, or browser.");
|
|
2092
|
+
}
|
|
2093
|
+
return {
|
|
2094
|
+
name: "list_models",
|
|
2095
|
+
args: {
|
|
2096
|
+
...space ? { space_id: space } : {},
|
|
2097
|
+
...kind ? { kind } : {},
|
|
2098
|
+
...family ? { family } : {}
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
case "profile get":
|
|
2103
|
+
rejectUnexpected5(parsed, command, []);
|
|
2104
|
+
return { name: "get_profile", args: {} };
|
|
2105
|
+
case "health":
|
|
2106
|
+
rejectUnexpected5(parsed, command, []);
|
|
2107
|
+
return { name: "health_check", args: {} };
|
|
2108
|
+
case "estimate": {
|
|
2109
|
+
rejectUnexpected5(parsed, command, [
|
|
2110
|
+
"space",
|
|
2111
|
+
"kind",
|
|
2112
|
+
"model",
|
|
2113
|
+
"prompt",
|
|
2114
|
+
"ref",
|
|
2115
|
+
"param",
|
|
2116
|
+
"count",
|
|
2117
|
+
"from-asset",
|
|
2118
|
+
"recipe-mode"
|
|
2119
|
+
]);
|
|
2120
|
+
const space = optional4(parsed, "space");
|
|
2121
|
+
const count = integerOption(parsed, "count", 1, 8);
|
|
2122
|
+
const recipeMode = parsed.options["recipe-mode"];
|
|
2123
|
+
if (recipeMode !== void 0 && recipeMode !== "current" && recipeMode !== "exact") {
|
|
2124
|
+
throw new CliUsageError("--recipe-mode must be current or exact.");
|
|
2125
|
+
}
|
|
2126
|
+
const fromAssetId = parsed.options["from-asset"];
|
|
2127
|
+
if (recipeMode === "exact" && (!fromAssetId || fromAssetId === "true")) {
|
|
2128
|
+
throw new CliUsageError("--recipe-mode exact requires --from-asset <asset>.");
|
|
2129
|
+
}
|
|
2130
|
+
return {
|
|
2131
|
+
name: "estimate_credits",
|
|
2132
|
+
args: {
|
|
2133
|
+
...space ? { space_id: space } : {},
|
|
2134
|
+
kind: mediaKind(parsed, command),
|
|
2135
|
+
model: required5(parsed, "model", command),
|
|
2136
|
+
prompt: optional4(parsed, "prompt") ?? "",
|
|
2137
|
+
references: (parsed.values.ref ?? []).map(pair2),
|
|
2138
|
+
params: Object.fromEntries((parsed.values.param ?? []).map(parameter2)),
|
|
2139
|
+
count: count ?? 1,
|
|
2140
|
+
...recipeMode === void 0 ? {} : { recipe_mode: recipeMode },
|
|
2141
|
+
...fromAssetId && fromAssetId !== "true" ? { from_asset_id: fromAssetId } : {}
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
case "asset get":
|
|
2146
|
+
rejectUnexpected5(parsed, command, ["space", "asset", "wait-seconds"]);
|
|
2147
|
+
return {
|
|
2148
|
+
name: "get_asset",
|
|
2149
|
+
args: {
|
|
2150
|
+
space_id: required5(parsed, "space", command),
|
|
2151
|
+
asset_id: required5(parsed, "asset", command),
|
|
2152
|
+
wait_seconds: integerOption(parsed, "wait-seconds", 0, 60) ?? 0
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
function record3(value) {
|
|
2158
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2159
|
+
}
|
|
2160
|
+
function humanOutput3(command, result) {
|
|
2161
|
+
if (command === "account") {
|
|
2162
|
+
const topup = typeof result.topup_url === "string" ? [`Top up: ${result.topup_url}`] : [];
|
|
2163
|
+
return [
|
|
2164
|
+
`${String(result.account_id)} ${String(result.balance_credits)} credits (${String(result.held_credits)} held)`,
|
|
2165
|
+
...topup
|
|
2166
|
+
];
|
|
2167
|
+
}
|
|
2168
|
+
if (command === "spaces") {
|
|
2169
|
+
const spaces = Array.isArray(result.spaces) ? result.spaces : [];
|
|
2170
|
+
if (spaces.length === 0) return ["No spaces."];
|
|
2171
|
+
return spaces.map((value) => {
|
|
2172
|
+
const space = record3(value) ?? {};
|
|
2173
|
+
return [space.space_id, space.web_url].filter((item) => typeof item === "string").join(" ");
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
if (command === "models") {
|
|
2177
|
+
const models = Array.isArray(result.models) ? result.models : [];
|
|
2178
|
+
return models.map((value) => record3(value) ?? {}).filter((model) => model.hidden !== true).map(
|
|
2179
|
+
(model) => [model.label, model.provider_model, model.provider, model.provider_model_kind, model.id].filter((item) => typeof item === "string").join(" \xB7 ")
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
if (command === "profile get") {
|
|
2183
|
+
return [
|
|
2184
|
+
[result.id, result.email, result.name].filter((item) => typeof item === "string" && item.length > 0).join(" \xB7 ")
|
|
2185
|
+
];
|
|
2186
|
+
}
|
|
2187
|
+
if (command === "health") {
|
|
2188
|
+
return [
|
|
2189
|
+
[result.status, result.environment].filter((item) => typeof item === "string" && item.length > 0).join(" \xB7 ")
|
|
2190
|
+
];
|
|
2191
|
+
}
|
|
2192
|
+
if (command === "estimate") {
|
|
2193
|
+
const topup = typeof result.topup_url === "string" ? [`Top up: ${result.topup_url}`] : [];
|
|
2194
|
+
return [`${String(result.credits)} credits (${String(result.balance_after_credits)} after)`, ...topup];
|
|
2195
|
+
}
|
|
2196
|
+
if (command === "space create") {
|
|
2197
|
+
const space = record3(result.space) ?? {};
|
|
2198
|
+
return [[space.space_id, space.web_url].filter((item) => typeof item === "string").join(" ")];
|
|
2199
|
+
}
|
|
2200
|
+
if (command === "space get") {
|
|
2201
|
+
const space = record3(result.space) ?? {};
|
|
2202
|
+
const assets = Array.isArray(result.assets) ? result.assets.length : 0;
|
|
2203
|
+
const links = Array.isArray(result.links) ? result.links.length : 0;
|
|
2204
|
+
return [
|
|
2205
|
+
[space.space_id, result.web_url].filter((item) => typeof item === "string").join(" "),
|
|
2206
|
+
`${assets} assets, ${links} links`
|
|
2207
|
+
];
|
|
2208
|
+
}
|
|
2209
|
+
if (command === "space delete") return [`Deleted ${String(result.space_id)} ${String(result.web_url)}`];
|
|
2210
|
+
const asset = record3(result.asset) ?? {};
|
|
2211
|
+
return [
|
|
2212
|
+
[asset.asset_id, asset.status, result.web_url].filter((item) => typeof item === "string").join(" ")
|
|
2213
|
+
];
|
|
2214
|
+
}
|
|
2215
|
+
async function handleDataCommand(command, parsed, dependencies = defaults5) {
|
|
2216
|
+
const call = dataToolCall(command, parsed);
|
|
2217
|
+
const client = await dependencies.client(parsed);
|
|
2218
|
+
const result = await client.call(call.name, call.args);
|
|
2219
|
+
if (parsed.options.json === "true") {
|
|
2220
|
+
dependencies.write(JSON.stringify(result, null, 2));
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
for (const line of humanOutput3(command, result)) dependencies.write(line);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// src/commands/transfer.ts
|
|
2227
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
2228
|
+
import { createWriteStream } from "node:fs";
|
|
2229
|
+
import { link as link2, lstat as lstat2, open as open3, unlink as unlink2 } from "node:fs/promises";
|
|
2230
|
+
import { basename, extname, resolve as resolve3 } from "node:path";
|
|
2231
|
+
import { Readable, Transform } from "node:stream";
|
|
2232
|
+
import { finished, pipeline } from "node:stream/promises";
|
|
2233
|
+
var defaults6 = {
|
|
2234
|
+
client: authenticatedToolClient,
|
|
2235
|
+
fetch,
|
|
2236
|
+
write: (text2) => process.stdout.write(`${text2}
|
|
2237
|
+
`),
|
|
2238
|
+
id: randomUUID4
|
|
2239
|
+
};
|
|
2240
|
+
var MIME_BY_EXTENSION = {
|
|
2241
|
+
".jpeg": "image/jpeg",
|
|
2242
|
+
".jpg": "image/jpeg",
|
|
2243
|
+
".mp3": "audio/mpeg",
|
|
2244
|
+
".mp4": "video/mp4",
|
|
2245
|
+
".png": "image/png",
|
|
2246
|
+
".wav": "audio/wav"
|
|
2247
|
+
};
|
|
2248
|
+
var EXTENSION_BY_MIME = {
|
|
2249
|
+
"audio/mpeg": ".mp3",
|
|
2250
|
+
"audio/wav": ".wav",
|
|
2251
|
+
"image/jpeg": ".jpg",
|
|
2252
|
+
"image/png": ".png",
|
|
2253
|
+
"video/mp4": ".mp4"
|
|
2254
|
+
};
|
|
2255
|
+
var GLOBAL_OPTIONS4 = ["env", "local", "json"];
|
|
2256
|
+
var USAGE5 = {
|
|
2257
|
+
upload: "upload --space ACCOUNT/SPACE --kind image|video|audio --file PATH [--mime TYPE] [--name TEXT] [--provider NAME] [--model ORIGIN] [--prompt TEXT] [--param NAME=VALUE]... [--ref ASSET:SLOT]... [--external-run-id ID] [--position JSON] [--tags JSON] [--request-id ID] [--json]",
|
|
2258
|
+
download: "download ASSET --space ACCOUNT/SPACE [--out PATH] [--json]"
|
|
2259
|
+
};
|
|
2260
|
+
function transferCommandUsage(command) {
|
|
2261
|
+
return `Usage: makefx ${USAGE5[command]}`;
|
|
2262
|
+
}
|
|
2263
|
+
function required6(parsed, name, command) {
|
|
2264
|
+
const value = parsed.options[name];
|
|
2265
|
+
if (!value || value === "true") throw new CliUsageError(`${command} requires --${name} <value>.`);
|
|
2266
|
+
return value;
|
|
2267
|
+
}
|
|
2268
|
+
function optional5(parsed, name) {
|
|
2269
|
+
const value = parsed.options[name];
|
|
2270
|
+
if (value === "true") throw new CliUsageError(`--${name} requires a value.`);
|
|
2271
|
+
return value;
|
|
2272
|
+
}
|
|
2273
|
+
function rejectUnexpected6(parsed, command, options) {
|
|
2274
|
+
const allowed = /* @__PURE__ */ new Set([...GLOBAL_OPTIONS4, ...options]);
|
|
2275
|
+
const unexpected = Object.keys(parsed.options).find((option3) => !allowed.has(option3));
|
|
2276
|
+
if (unexpected) throw new CliUsageError(`Unknown option --${unexpected}. ${transferCommandUsage(command)}`);
|
|
2277
|
+
}
|
|
2278
|
+
function scalar3(value) {
|
|
2279
|
+
if (value === "true") return true;
|
|
2280
|
+
if (value === "false") return false;
|
|
2281
|
+
if (value === "null") return null;
|
|
2282
|
+
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(value)) return Number(value);
|
|
2283
|
+
if (value.startsWith("[") || value.startsWith("{")) {
|
|
2284
|
+
try {
|
|
2285
|
+
return JSON.parse(value);
|
|
2286
|
+
} catch {
|
|
2287
|
+
throw new CliUsageError("--param arrays and objects must be valid JSON.");
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return value;
|
|
2291
|
+
}
|
|
2292
|
+
function parameter3(value) {
|
|
2293
|
+
const at = value.indexOf("=");
|
|
2294
|
+
if (at <= 0) throw new CliUsageError("--param must be written as name=value.");
|
|
2295
|
+
return [value.slice(0, at), scalar3(value.slice(at + 1))];
|
|
2296
|
+
}
|
|
2297
|
+
function objectOption2(parsed, name) {
|
|
2298
|
+
const value = optional5(parsed, name);
|
|
2299
|
+
if (value === void 0) return void 0;
|
|
2300
|
+
try {
|
|
2301
|
+
const parsedValue = JSON.parse(value);
|
|
2302
|
+
if (parsedValue === null || typeof parsedValue !== "object" || Array.isArray(parsedValue))
|
|
2303
|
+
throw new Error();
|
|
2304
|
+
return parsedValue;
|
|
2305
|
+
} catch {
|
|
2306
|
+
throw new CliUsageError(`--${name} must be a JSON object.`);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
function tagsOption3(parsed) {
|
|
2310
|
+
const value = optional5(parsed, "tags");
|
|
2311
|
+
if (value === void 0) return void 0;
|
|
2312
|
+
try {
|
|
2313
|
+
const tags = JSON.parse(value);
|
|
2314
|
+
if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string")) throw new Error();
|
|
2315
|
+
return tags;
|
|
2316
|
+
} catch {
|
|
2317
|
+
throw new CliUsageError("--tags must be a JSON array of strings.");
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
function mediaKind2(parsed) {
|
|
2321
|
+
const kind = required6(parsed, "kind", "upload");
|
|
2322
|
+
if (!["image", "video", "audio"].includes(kind)) {
|
|
2323
|
+
throw new CliUsageError("--kind must be image, video, or audio.");
|
|
2324
|
+
}
|
|
2325
|
+
return kind;
|
|
2326
|
+
}
|
|
2327
|
+
function uploadMime(parsed, path2) {
|
|
2328
|
+
const explicit = optional5(parsed, "mime");
|
|
2329
|
+
if (explicit) return explicit;
|
|
2330
|
+
const inferred = MIME_BY_EXTENSION[extname(path2).toLowerCase()];
|
|
2331
|
+
if (!inferred) throw new CliUsageError("Cannot infer media type from --file; pass --mime <type>.");
|
|
2332
|
+
return inferred;
|
|
2333
|
+
}
|
|
2334
|
+
function declaredRecipe(parsed) {
|
|
2335
|
+
const recipeOptions = ["provider", "model", "prompt", "external-run-id"];
|
|
2336
|
+
const hasRecipe = recipeOptions.some((name) => parsed.options[name] !== void 0) || (parsed.values.param?.length ?? 0) > 0 || (parsed.values.ref?.length ?? 0) > 0;
|
|
2337
|
+
if (!hasRecipe) return void 0;
|
|
2338
|
+
const provider = optional5(parsed, "provider") ?? "external";
|
|
2339
|
+
if (provider !== "external") throw new CliUsageError("--provider must be external.");
|
|
2340
|
+
const model = required6(parsed, "model", "upload");
|
|
2341
|
+
const references = (parsed.values.ref ?? []).map((value, order) => {
|
|
2342
|
+
const at = value.lastIndexOf(":");
|
|
2343
|
+
if (at <= 0 || at === value.length - 1) {
|
|
2344
|
+
throw new CliUsageError("--ref must be written as asset:slot.");
|
|
2345
|
+
}
|
|
2346
|
+
return { asset_id: value.slice(0, at), slot: value.slice(at + 1), order };
|
|
2347
|
+
});
|
|
2348
|
+
const externalRunId = optional5(parsed, "external-run-id");
|
|
2349
|
+
return {
|
|
2350
|
+
provider: "external",
|
|
2351
|
+
model,
|
|
2352
|
+
prompt: optional5(parsed, "prompt") ?? "",
|
|
2353
|
+
params: Object.fromEntries((parsed.values.param ?? []).map(parameter3)),
|
|
2354
|
+
references,
|
|
2355
|
+
...externalRunId !== void 0 ? { external_run_id: externalRunId } : {}
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function uploadToolArguments(parsed, file) {
|
|
2359
|
+
rejectUnexpected6(parsed, "upload", [
|
|
2360
|
+
"space",
|
|
2361
|
+
"kind",
|
|
2362
|
+
"file",
|
|
2363
|
+
"mime",
|
|
2364
|
+
"name",
|
|
2365
|
+
"provider",
|
|
2366
|
+
"model",
|
|
2367
|
+
"prompt",
|
|
2368
|
+
"param",
|
|
2369
|
+
"ref",
|
|
2370
|
+
"external-run-id",
|
|
2371
|
+
"position",
|
|
2372
|
+
"tags",
|
|
2373
|
+
"request-id"
|
|
2374
|
+
]);
|
|
2375
|
+
if (parsed.positionals.length > 0) {
|
|
2376
|
+
throw new CliUsageError(
|
|
2377
|
+
`upload does not accept "${parsed.positionals[0]}". ${transferCommandUsage("upload")}`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const name = optional5(parsed, "name");
|
|
2381
|
+
const recipe = declaredRecipe(parsed);
|
|
2382
|
+
const position = objectOption2(parsed, "position");
|
|
2383
|
+
const tags = tagsOption3(parsed);
|
|
2384
|
+
const requestId = optional5(parsed, "request-id");
|
|
2385
|
+
return {
|
|
2386
|
+
space_id: required6(parsed, "space", "upload"),
|
|
2387
|
+
kind: mediaKind2(parsed),
|
|
2388
|
+
filename: basename(file.path),
|
|
2389
|
+
mime: uploadMime(parsed, file.path),
|
|
2390
|
+
size_bytes: file.size,
|
|
2391
|
+
...name !== void 0 ? { name } : {},
|
|
2392
|
+
...recipe !== void 0 ? { recipe } : {},
|
|
2393
|
+
...position !== void 0 ? { position } : {},
|
|
2394
|
+
...tags !== void 0 ? { tags } : {},
|
|
2395
|
+
...requestId !== void 0 ? { request_id: requestId } : {}
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
function record4(value) {
|
|
2399
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2400
|
+
}
|
|
2401
|
+
function signedUpload(result) {
|
|
2402
|
+
const headers = record4(result.headers);
|
|
2403
|
+
if (result.method !== "PUT" || typeof result.upload_url !== "string" || !headers || !Object.values(headers).every((value) => typeof value === "string")) {
|
|
2404
|
+
throw new Error("upload_asset returned an unusable signed upload.");
|
|
2405
|
+
}
|
|
2406
|
+
return { url: result.upload_url, headers };
|
|
2407
|
+
}
|
|
2408
|
+
async function uploadFile(handle, size, signed, fetchImpl) {
|
|
2409
|
+
const source = handle.createReadStream();
|
|
2410
|
+
let bytes = 0;
|
|
2411
|
+
const counted = new Transform({
|
|
2412
|
+
transform(chunk, _encoding, callback) {
|
|
2413
|
+
bytes += chunk.byteLength;
|
|
2414
|
+
callback(null, chunk);
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
source.once("error", (error) => counted.destroy(error));
|
|
2418
|
+
source.pipe(counted);
|
|
2419
|
+
try {
|
|
2420
|
+
const request = {
|
|
2421
|
+
method: "PUT",
|
|
2422
|
+
headers: signed.headers,
|
|
2423
|
+
body: counted,
|
|
2424
|
+
duplex: "half"
|
|
2425
|
+
};
|
|
2426
|
+
const response = await fetchImpl(signed.url, request);
|
|
2427
|
+
if (!response.ok) {
|
|
2428
|
+
await response.body?.cancel().catch(() => void 0);
|
|
2429
|
+
throw new Error(`Upload failed with HTTP ${response.status}.`);
|
|
2430
|
+
}
|
|
2431
|
+
await finished(source);
|
|
2432
|
+
if (bytes !== size) throw new Error(`Upload ended after ${bytes} of ${size} bytes.`);
|
|
2433
|
+
await response.body?.cancel().catch(() => void 0);
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
source.destroy();
|
|
2436
|
+
counted.destroy();
|
|
2437
|
+
throw error;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
async function destinationAvailable2(path2) {
|
|
2441
|
+
try {
|
|
2442
|
+
await lstat2(path2);
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
if (error.code === "ENOENT") return;
|
|
2445
|
+
throw error;
|
|
2446
|
+
}
|
|
2447
|
+
throw new Error(`Destination already exists: ${path2}`);
|
|
2448
|
+
}
|
|
2449
|
+
function expectedLength(response, asset) {
|
|
2450
|
+
const header = response.headers.get("content-length");
|
|
2451
|
+
if (header !== null) {
|
|
2452
|
+
const length = Number(header);
|
|
2453
|
+
if (!Number.isSafeInteger(length) || length < 0)
|
|
2454
|
+
throw new Error("Download returned an invalid Content-Length.");
|
|
2455
|
+
return length;
|
|
2456
|
+
}
|
|
2457
|
+
const media = record4(asset.media);
|
|
2458
|
+
return typeof media?.size_bytes === "number" ? media.size_bytes : void 0;
|
|
2459
|
+
}
|
|
2460
|
+
async function downloadFile(url, destination, asset, fetchImpl, id) {
|
|
2461
|
+
await destinationAvailable2(destination);
|
|
2462
|
+
const temporary = `${destination}.makefx-${id()}.tmp`;
|
|
2463
|
+
let published = false;
|
|
2464
|
+
try {
|
|
2465
|
+
const response = await fetchImpl(url);
|
|
2466
|
+
if (!response.ok) {
|
|
2467
|
+
await response.body?.cancel().catch(() => void 0);
|
|
2468
|
+
throw new Error(`Download failed with HTTP ${response.status}.`);
|
|
2469
|
+
}
|
|
2470
|
+
if (!response.body) throw new Error("Download returned no media body.");
|
|
2471
|
+
const expected = expectedLength(response, asset);
|
|
2472
|
+
let bytes = 0;
|
|
2473
|
+
const counted = new Transform({
|
|
2474
|
+
transform(chunk, _encoding, callback) {
|
|
2475
|
+
bytes += chunk.byteLength;
|
|
2476
|
+
callback(null, chunk);
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
await pipeline(Readable.from(response.body), counted, createWriteStream(temporary, { flags: "wx" }));
|
|
2480
|
+
if (expected !== void 0 && bytes !== expected) {
|
|
2481
|
+
throw new Error(`Download ended after ${bytes} of ${expected} bytes.`);
|
|
2482
|
+
}
|
|
2483
|
+
await link2(temporary, destination);
|
|
2484
|
+
published = true;
|
|
2485
|
+
await unlink2(temporary);
|
|
2486
|
+
} finally {
|
|
2487
|
+
if (!published) await unlink2(temporary).catch(() => void 0);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
function downloadDestination(parsed, asset) {
|
|
2491
|
+
const out = optional5(parsed, "out");
|
|
2492
|
+
if (out) return resolve3(out);
|
|
2493
|
+
const assetId = typeof asset.asset_id === "string" ? asset.asset_id : parsed.positionals[0];
|
|
2494
|
+
const media = record4(asset.media);
|
|
2495
|
+
const extension = typeof media?.mime === "string" ? EXTENSION_BY_MIME[media.mime] ?? "" : "";
|
|
2496
|
+
return resolve3(`${assetId}${extension}`);
|
|
2497
|
+
}
|
|
2498
|
+
async function handleTransferCommand(command, parsed, dependencies = defaults6) {
|
|
2499
|
+
if (command === "upload") {
|
|
2500
|
+
const path2 = resolve3(required6(parsed, "file", command));
|
|
2501
|
+
const handle = await open3(path2, "r");
|
|
2502
|
+
try {
|
|
2503
|
+
const stat2 = await handle.stat();
|
|
2504
|
+
if (!stat2.isFile()) throw new Error(`Not a regular file: ${path2}`);
|
|
2505
|
+
const args = uploadToolArguments(parsed, { path: path2, size: stat2.size });
|
|
2506
|
+
const client2 = await dependencies.client(parsed);
|
|
2507
|
+
const result2 = await client2.call("upload_asset", args);
|
|
2508
|
+
await uploadFile(handle, stat2.size, signedUpload(result2), dependencies.fetch);
|
|
2509
|
+
dependencies.write(
|
|
2510
|
+
parsed.options.json === "true" ? JSON.stringify(result2, null, 2) : [result2.asset_id, result2.web_url].filter((value) => typeof value === "string").join(" ")
|
|
2511
|
+
);
|
|
2512
|
+
} finally {
|
|
2513
|
+
await handle.close().catch(() => void 0);
|
|
2514
|
+
}
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
rejectUnexpected6(parsed, command, ["space", "out"]);
|
|
2518
|
+
if (parsed.positionals.length !== 1) {
|
|
2519
|
+
throw new CliUsageError(`download requires one asset id. ${transferCommandUsage(command)}`);
|
|
2520
|
+
}
|
|
2521
|
+
const client = await dependencies.client(parsed);
|
|
2522
|
+
const result = await client.call("get_asset", {
|
|
2523
|
+
space_id: required6(parsed, "space", command),
|
|
2524
|
+
asset_id: parsed.positionals[0],
|
|
2525
|
+
wait_seconds: 0
|
|
2526
|
+
});
|
|
2527
|
+
const asset = record4(result.asset);
|
|
2528
|
+
if (!asset || asset.status !== "ready" || typeof asset.media_url !== "string") {
|
|
2529
|
+
throw new Error(`Asset ${parsed.positionals[0]} is not ready for download.`);
|
|
2530
|
+
}
|
|
2531
|
+
const destination = downloadDestination(parsed, asset);
|
|
2532
|
+
await downloadFile(asset.media_url, destination, asset, dependencies.fetch, dependencies.id);
|
|
2533
|
+
dependencies.write(
|
|
2534
|
+
parsed.options.json === "true" ? JSON.stringify(result, null, 2) : [asset.asset_id, result.web_url ?? asset.web_url, destination].filter((value) => typeof value === "string").join(" ")
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// src/lib/command-error.ts
|
|
2539
|
+
function reportCommandError(error, json2, output) {
|
|
2540
|
+
if (error instanceof ToolCallError) {
|
|
2541
|
+
if (json2) {
|
|
2542
|
+
output.stdout(
|
|
2543
|
+
`${JSON.stringify(error.jsonOutput === void 0 ? error.structuredContent : error.jsonOutput)}
|
|
2544
|
+
`
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
output.stderr(`${error.code}: ${error.message}
|
|
2548
|
+
`);
|
|
2549
|
+
const topupUrl = error.details?.topup_url;
|
|
2550
|
+
if (typeof topupUrl === "string") output.stderr(`Top up: ${topupUrl}
|
|
2551
|
+
`);
|
|
2552
|
+
return 1;
|
|
2553
|
+
}
|
|
2554
|
+
if (error instanceof CliUsageError) {
|
|
2555
|
+
output.stderr(`Error: ${error.message}
|
|
2556
|
+
`);
|
|
2557
|
+
return 2;
|
|
2558
|
+
}
|
|
2559
|
+
output.stderr(`Error: ${error instanceof Error ? error.message : "Unexpected error occurred"}
|
|
2560
|
+
`);
|
|
2561
|
+
return 1;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// src/lib/utils.ts
|
|
2565
|
+
function parseArgs(argv) {
|
|
2566
|
+
const options = {};
|
|
2567
|
+
const values = {};
|
|
2568
|
+
const positionals = [];
|
|
2569
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
2570
|
+
const token = argv[i];
|
|
2571
|
+
if (token.startsWith("-") && !token.startsWith("--")) {
|
|
2572
|
+
const key2 = token.slice(1);
|
|
2573
|
+
const next2 = argv[i + 1];
|
|
2574
|
+
if (next2 && !next2.startsWith("-")) {
|
|
2575
|
+
options[key2] = next2;
|
|
2576
|
+
(values[key2] ??= []).push(next2);
|
|
2577
|
+
i += 1;
|
|
2578
|
+
} else {
|
|
2579
|
+
options[key2] = "true";
|
|
2580
|
+
(values[key2] ??= []).push("true");
|
|
2581
|
+
}
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
if (!token.startsWith("--")) {
|
|
2585
|
+
positionals.push(token);
|
|
2586
|
+
continue;
|
|
2587
|
+
}
|
|
2588
|
+
const eqIndex = token.indexOf("=");
|
|
2589
|
+
if (eqIndex !== -1) {
|
|
2590
|
+
const key2 = token.slice(2, eqIndex);
|
|
2591
|
+
const value = token.slice(eqIndex + 1);
|
|
2592
|
+
options[key2] = value;
|
|
2593
|
+
(values[key2] ??= []).push(value);
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
const key = token.slice(2);
|
|
2597
|
+
const next = argv[i + 1];
|
|
2598
|
+
if (next && !next.startsWith("--")) {
|
|
2599
|
+
options[key] = next;
|
|
2600
|
+
(values[key] ??= []).push(next);
|
|
2601
|
+
i += 1;
|
|
2602
|
+
} else {
|
|
2603
|
+
options[key] = "true";
|
|
2604
|
+
(values[key] ??= []).push("true");
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
return { options, values, positionals };
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// src/commands/audio.ts
|
|
2611
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
2612
|
+
import { link as link3, lstat as lstat3, open as open4, readFile as readFile3, unlink as unlink3 } from "node:fs/promises";
|
|
2613
|
+
import { resolve as resolve4 } from "node:path";
|
|
2614
|
+
var USAGE6 = {
|
|
2615
|
+
"audio align": "audio align ASSET --space ACCOUNT/SPACE [TEXT | --input PATH] [--request-id ID] [--wait] [--json]",
|
|
2616
|
+
"audio timings": "audio timings ASSET --space ACCOUNT/SPACE [--out PATH] [--json]"
|
|
2617
|
+
};
|
|
2618
|
+
var defaults7 = {
|
|
2619
|
+
client: authenticatedToolClient,
|
|
2620
|
+
write: (text2) => process.stdout.write(`${text2}
|
|
2621
|
+
`),
|
|
2622
|
+
id: randomUUID5
|
|
2623
|
+
};
|
|
2624
|
+
function audioCommandUsage(command) {
|
|
2625
|
+
return `Usage: makefx ${USAGE6[command]}`;
|
|
2626
|
+
}
|
|
2627
|
+
function option2(parsed, name) {
|
|
2628
|
+
const value = parsed.options[name];
|
|
2629
|
+
if (value === "true") throw new CliUsageError(`--${name} requires a value.`);
|
|
2630
|
+
return value;
|
|
2631
|
+
}
|
|
2632
|
+
function required7(parsed, name, command) {
|
|
2633
|
+
const value = option2(parsed, name);
|
|
2634
|
+
if (!value) throw new CliUsageError(`${command} requires --${name} <value>.`);
|
|
2635
|
+
return value;
|
|
2636
|
+
}
|
|
2637
|
+
function rejectOptions(parsed, command, allowed) {
|
|
2638
|
+
const options = /* @__PURE__ */ new Set(["env", "local", "json", "help", ...allowed]);
|
|
2639
|
+
const unexpected = Object.keys(parsed.options).find((name) => !options.has(name));
|
|
2640
|
+
if (unexpected) throw new CliUsageError(`Unknown option --${unexpected}. ${audioCommandUsage(command)}`);
|
|
2641
|
+
}
|
|
2642
|
+
async function destinationAvailable3(path2) {
|
|
2643
|
+
try {
|
|
2644
|
+
await lstat3(path2);
|
|
2645
|
+
} catch (error) {
|
|
2646
|
+
if (error.code === "ENOENT") return;
|
|
2647
|
+
throw error;
|
|
2648
|
+
}
|
|
2649
|
+
throw new Error(`Destination already exists: ${path2}`);
|
|
2650
|
+
}
|
|
2651
|
+
async function publishJson2(path2, value, id) {
|
|
2652
|
+
await destinationAvailable3(path2);
|
|
2653
|
+
const temporary = `${path2}.makefx-${id()}.tmp`;
|
|
2654
|
+
let published = false;
|
|
2655
|
+
try {
|
|
2656
|
+
const handle = await open4(temporary, "wx");
|
|
2657
|
+
try {
|
|
2658
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}
|
|
2659
|
+
`, "utf8");
|
|
2660
|
+
} finally {
|
|
2661
|
+
await handle.close();
|
|
2662
|
+
}
|
|
2663
|
+
await link3(temporary, path2);
|
|
2664
|
+
published = true;
|
|
2665
|
+
await unlink3(temporary);
|
|
2666
|
+
} finally {
|
|
2667
|
+
if (!published) await unlink3(temporary).catch(() => void 0);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
function record5(value) {
|
|
2671
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2672
|
+
}
|
|
2673
|
+
async function alignmentText(parsed) {
|
|
2674
|
+
const path2 = option2(parsed, "input");
|
|
2675
|
+
const inline = parsed.positionals[1];
|
|
2676
|
+
if (path2 && inline !== void 0) throw new CliUsageError("Use inline text or --input, not both.");
|
|
2677
|
+
if (parsed.positionals.length > 2) throw new CliUsageError(audioCommandUsage("audio align"));
|
|
2678
|
+
if (!path2) return inline;
|
|
2679
|
+
const value = await readFile3(resolve4(path2), "utf8");
|
|
2680
|
+
if (Array.from(value).length > 675e3) throw new Error("Alignment input exceeds 675,000 characters.");
|
|
2681
|
+
return value;
|
|
2682
|
+
}
|
|
2683
|
+
async function handleAudioCommand(command, parsed, dependencies = defaults7) {
|
|
2684
|
+
if (parsed.positionals.length < 1) throw new CliUsageError(audioCommandUsage(command));
|
|
2685
|
+
const asset = parsed.positionals[0];
|
|
2686
|
+
if (!asset) throw new CliUsageError(audioCommandUsage(command));
|
|
2687
|
+
const space = required7(parsed, "space", command);
|
|
2688
|
+
const client = await dependencies.client(parsed);
|
|
2689
|
+
if (command === "audio align") {
|
|
2690
|
+
rejectOptions(parsed, command, ["space", "input", "request-id", "wait"]);
|
|
2691
|
+
const text2 = await alignmentText(parsed);
|
|
2692
|
+
let result2 = await client.call("align_audio", {
|
|
2693
|
+
space_id: space,
|
|
2694
|
+
asset_id: asset,
|
|
2695
|
+
request_id: option2(parsed, "request-id") ?? dependencies.id(),
|
|
2696
|
+
...text2 === void 0 ? {} : { text: text2 }
|
|
2697
|
+
});
|
|
2698
|
+
const admitted = record5(result2.alignment_job);
|
|
2699
|
+
if (parsed.options.wait === "true" && typeof admitted?.alignment_job_id === "string") {
|
|
2700
|
+
while (admitted.status === "queued" || admitted.status === "running") {
|
|
2701
|
+
result2 = await client.call("get_audio_word_timings", {
|
|
2702
|
+
space_id: space,
|
|
2703
|
+
asset_id: asset,
|
|
2704
|
+
alignment_job_id: admitted.alignment_job_id,
|
|
2705
|
+
wait_seconds: 60
|
|
2706
|
+
});
|
|
2707
|
+
const current = record5(result2.alignment_job);
|
|
2708
|
+
if (!current || current.status === "completed" || current.status === "failed") break;
|
|
2709
|
+
admitted.status = current.status;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (parsed.options.json === "true") {
|
|
2713
|
+
dependencies.write(JSON.stringify(result2, null, 2));
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
const job = record5(result2.alignment_job) ?? admitted ?? {};
|
|
2717
|
+
const credits = job.charged_credits ?? job.estimated_credits;
|
|
2718
|
+
dependencies.write(
|
|
2719
|
+
[
|
|
2720
|
+
typeof job.alignment_job_id === "string" ? `Alignment ${job.alignment_job_id}` : void 0,
|
|
2721
|
+
typeof job.status === "string" ? job.status : void 0,
|
|
2722
|
+
typeof credits === "number" ? `${credits} credits` : void 0,
|
|
2723
|
+
typeof result2.web_url === "string" ? result2.web_url : void 0
|
|
2724
|
+
].filter((value) => value !== void 0).join(" \xB7 ")
|
|
2725
|
+
);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
rejectOptions(parsed, command, ["space", "out"]);
|
|
2729
|
+
if (parsed.positionals.length !== 1) throw new CliUsageError(audioCommandUsage(command));
|
|
2730
|
+
const result = await client.call("get_audio_word_timings", {
|
|
2731
|
+
space_id: space,
|
|
2732
|
+
asset_id: asset,
|
|
2733
|
+
wait_seconds: 0
|
|
2734
|
+
});
|
|
2735
|
+
if (!result.word_timings) throw new Error("No word timings are available.");
|
|
2736
|
+
const out = option2(parsed, "out");
|
|
2737
|
+
if (out) {
|
|
2738
|
+
const destination = resolve4(out);
|
|
2739
|
+
await publishJson2(destination, result.word_timings, dependencies.id);
|
|
2740
|
+
dependencies.write(parsed.options.json === "true" ? JSON.stringify(result, null, 2) : destination);
|
|
2741
|
+
} else {
|
|
2742
|
+
dependencies.write(JSON.stringify(result.word_timings, null, 2));
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// src/lib/space-id.ts
|
|
2747
|
+
var LOCAL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2748
|
+
var LOCAL_ID_MAX_LENGTH = 63;
|
|
2749
|
+
function isLocalId(value) {
|
|
2750
|
+
return value.length <= LOCAL_ID_MAX_LENGTH && LOCAL_ID_PATTERN.test(value);
|
|
2751
|
+
}
|
|
2752
|
+
function parseCanonicalSpaceId(value) {
|
|
2753
|
+
const slash = value.indexOf("/");
|
|
2754
|
+
if (slash < 1 || slash !== value.lastIndexOf("/")) return null;
|
|
2755
|
+
const accountId = value.slice(0, slash);
|
|
2756
|
+
const spaceId2 = value.slice(slash + 1);
|
|
2757
|
+
return isLocalId(accountId) && isLocalId(spaceId2) ? { accountId, spaceId: spaceId2 } : null;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
// src/commands/space-visibility.ts
|
|
2761
|
+
var SPACE_UPDATE_PATH = "/api/spaces/:accountId/:spaceId";
|
|
2762
|
+
var defaults8 = {
|
|
2763
|
+
authenticate: authenticatedConfig,
|
|
2764
|
+
fetch,
|
|
2765
|
+
write: (text2) => process.stdout.write(`${text2}
|
|
2766
|
+
`)
|
|
2767
|
+
};
|
|
2768
|
+
function spaceVisibilityUsage(command) {
|
|
2769
|
+
return `Usage: makefx ${command} --space ACCOUNT/SPACE [--json]`;
|
|
2770
|
+
}
|
|
2771
|
+
function spaceId(parsed, command) {
|
|
2772
|
+
if (parsed.positionals.length > 0) {
|
|
2773
|
+
throw new CliUsageError(
|
|
2774
|
+
`${command} does not accept "${parsed.positionals[0]}". ${spaceVisibilityUsage(command)}`
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
const allowed = /* @__PURE__ */ new Set(["env", "local", "json", "space"]);
|
|
2778
|
+
const unexpected = Object.keys(parsed.options).find((option3) => !allowed.has(option3));
|
|
2779
|
+
if (unexpected) {
|
|
2780
|
+
throw new CliUsageError(`Unknown option --${unexpected}. ${spaceVisibilityUsage(command)}`);
|
|
2781
|
+
}
|
|
2782
|
+
const value = parsed.options.space;
|
|
2783
|
+
if (!value || value === "true") throw new CliUsageError(`${command} requires --space <value>.`);
|
|
2784
|
+
if (!parseCanonicalSpaceId(value)) {
|
|
2785
|
+
throw new CliUsageError(`${command} requires --space ACCOUNT/SPACE.`);
|
|
2786
|
+
}
|
|
2787
|
+
return value;
|
|
2788
|
+
}
|
|
2789
|
+
function routePath(accountId, spaceId2) {
|
|
2790
|
+
return SPACE_UPDATE_PATH.replace(":accountId", encodeURIComponent(accountId)).replace(":spaceId", encodeURIComponent(spaceId2));
|
|
2791
|
+
}
|
|
2792
|
+
async function responseError(response, messageOverride) {
|
|
2793
|
+
const payload = await response.json().catch(() => void 0);
|
|
2794
|
+
const value = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
|
|
2795
|
+
const code = typeof value?.error === "string" ? value.error : `http_${response.status}`;
|
|
2796
|
+
const message = messageOverride ?? (typeof value?.error_description === "string" ? value.error_description : typeof value?.error === "string" ? value.error : `Space visibility update failed (${response.status}).`);
|
|
2797
|
+
return new ToolCallError(
|
|
2798
|
+
{
|
|
2799
|
+
code,
|
|
2800
|
+
message,
|
|
2801
|
+
retryable: typeof value?.retryable === "boolean" ? value.retryable : response.status === 429 || response.status >= 500,
|
|
2802
|
+
details: { http_status: response.status }
|
|
2803
|
+
},
|
|
2804
|
+
message,
|
|
2805
|
+
payload
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
async function setSpaceVisibility(config, canonicalId, isPublic, fetchImpl = fetch) {
|
|
2809
|
+
const parsed = parseCanonicalSpaceId(canonicalId);
|
|
2810
|
+
if (!parsed) throw new CliUsageError("--space must be ACCOUNT/SPACE.");
|
|
2811
|
+
const response = await fetchImpl(new URL(routePath(parsed.accountId, parsed.spaceId), config.baseUrl), {
|
|
2812
|
+
method: "PATCH",
|
|
2813
|
+
headers: {
|
|
2814
|
+
Authorization: `Bearer ${config.token.accessToken}`,
|
|
2815
|
+
"Content-Type": "application/json"
|
|
2816
|
+
},
|
|
2817
|
+
body: JSON.stringify({ is_public: isPublic })
|
|
2818
|
+
});
|
|
2819
|
+
if (!response.ok) {
|
|
2820
|
+
throw await responseError(
|
|
2821
|
+
response,
|
|
2822
|
+
response.status === 401 ? "The stored login was rejected. Sign in again." : void 0
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
return spaceUpdateResponse(await response.json());
|
|
2826
|
+
}
|
|
2827
|
+
function spaceUpdateResponse(payload) {
|
|
2828
|
+
const space = isRecord(payload) ? payload.space : void 0;
|
|
2829
|
+
if (!isRecord(space) || typeof space.space_id !== "string" || typeof space.is_public !== "boolean" || typeof space.web_url !== "string") {
|
|
2830
|
+
throw new Error("The space update response was not a space.");
|
|
2831
|
+
}
|
|
2832
|
+
return { space: { ...space, space_id: space.space_id, is_public: space.is_public, web_url: space.web_url } };
|
|
2833
|
+
}
|
|
2834
|
+
async function handleSpaceVisibilityCommand(command, parsed, dependencies = defaults8) {
|
|
2835
|
+
const canonicalId = spaceId(parsed, command);
|
|
2836
|
+
const config = await dependencies.authenticate(parsed);
|
|
2837
|
+
const result = await setSpaceVisibility(
|
|
2838
|
+
config,
|
|
2839
|
+
canonicalId,
|
|
2840
|
+
command === "space publish",
|
|
2841
|
+
dependencies.fetch
|
|
2842
|
+
);
|
|
2843
|
+
if (parsed.options.json === "true") {
|
|
2844
|
+
dependencies.write(JSON.stringify(result, null, 2));
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
dependencies.write(
|
|
2848
|
+
`${result.space.space_id} \xB7 ${result.space.is_public ? "public" : "private"} \xB7 ${result.space.web_url}`
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/index.ts
|
|
2853
|
+
async function main() {
|
|
2854
|
+
const [, , command, ...args] = process4.argv;
|
|
2855
|
+
try {
|
|
2856
|
+
if (!command || command === "--help") {
|
|
2857
|
+
printHelp();
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
if (command === "--version" || command === "version") {
|
|
2861
|
+
console.log(CLI_VERSION);
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
if (command === "help") {
|
|
2865
|
+
const [topic, nested] = args;
|
|
2866
|
+
if (topic) printTopicHelp(topic, nested);
|
|
2867
|
+
else printHelp();
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const parsed = parseArgs(args);
|
|
2871
|
+
await dispatchCommand(command, parsed);
|
|
2872
|
+
} catch (error) {
|
|
2873
|
+
const parsed = parseArgs(args);
|
|
2874
|
+
process4.exitCode = reportCommandError(error, parsed.options.json === "true", {
|
|
2875
|
+
stdout: (text2) => process4.stdout.write(text2),
|
|
2876
|
+
stderr: (text2) => process4.stderr.write(text2)
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
function printHelp() {
|
|
2881
|
+
console.log(`
|
|
2882
|
+
${SERVICE_NAME} CLI ${CLI_VERSION}
|
|
2883
|
+
|
|
2884
|
+
Usage: makefx <command> [options]
|
|
2885
|
+
|
|
2886
|
+
Commands:
|
|
2887
|
+
login [--env ENV] Sign in through the browser
|
|
2888
|
+
logout [--env ENV] Revoke and remove stored credentials
|
|
2889
|
+
mcp [--env ENV] Relay stdio JSON-RPC to /mcp
|
|
2890
|
+
account [--account ID] Show balance and active holds
|
|
2891
|
+
purchase create --account ID --product PRODUCT --request-id ID
|
|
2892
|
+
Prepare a credit purchase without charging a card
|
|
2893
|
+
purchase get --purchase ID Read credit purchase status
|
|
2894
|
+
profile get Show the signed-in profile
|
|
2895
|
+
profile update --name NAME Change the signed-in display name
|
|
2896
|
+
health Check service reachability and environment
|
|
2897
|
+
spaces [--account ID] [--query TEXT] [--limit N]
|
|
2898
|
+
List available spaces
|
|
2899
|
+
space create --name NAME [--account ID] [--id ID]
|
|
2900
|
+
Create a space in an account you can edit
|
|
2901
|
+
space get --space S [--starred-only]
|
|
2902
|
+
Read every asset, link, and recipe in a space
|
|
2903
|
+
space update --space S --name TEXT
|
|
2904
|
+
Rename a space; every address it has worn keeps working
|
|
2905
|
+
space publish --space S Publish a read-only snapshot (account owner only)
|
|
2906
|
+
space unpublish --space S Withdraw the public snapshot (account owner only)
|
|
2907
|
+
space delete --space S Soft-delete a space (account owner only)
|
|
2908
|
+
voices sync --account ACCOUNT Refresh the account's ElevenLabs voices
|
|
2909
|
+
models [--space S] [--kind K] [--family F]
|
|
2910
|
+
List the model catalog for the payer
|
|
2911
|
+
estimate --kind K --model M [--from-asset A] [--recipe-mode current|exact]
|
|
2912
|
+
Quote creation or replay before spending credits
|
|
2913
|
+
create --space S --kind K --model M
|
|
2914
|
+
Validate the payer catalog, then create assets
|
|
2915
|
+
upload --space S --kind K --file PATH
|
|
2916
|
+
Stream a local file through a signed URL
|
|
2917
|
+
download ASSET --space S Stream ready media to a new local file
|
|
2918
|
+
asset get --space S --asset A Read an asset and refresh its URLs
|
|
2919
|
+
asset update --space S --asset A
|
|
2920
|
+
Update asset metadata
|
|
2921
|
+
asset delete --space S --asset A
|
|
2922
|
+
Soft-delete an asset
|
|
2923
|
+
describe --space S --asset A Describe reusable visual traits
|
|
2924
|
+
audio align ASSET --space S Align or re-align unchanged ready audio
|
|
2925
|
+
audio timings ASSET --space S Read or save current canonical timings
|
|
2926
|
+
link --space S --from A --to B
|
|
2927
|
+
Link two assets on the canvas
|
|
2928
|
+
unlink --space S (--link L | --from A --to B)
|
|
2929
|
+
Remove a canvas link
|
|
2930
|
+
export --space S [--out PATH] Export a space document via export_space
|
|
2931
|
+
open SPACE Print and open a canonical space URL
|
|
2932
|
+
open ASSET --space S Print and open a canonical asset URL
|
|
2933
|
+
|
|
2934
|
+
Common options:
|
|
2935
|
+
--env production|stage|local Select credentials and endpoint (default: production)
|
|
2936
|
+
--local Shortcut for --env local
|
|
2937
|
+
--json Print unchanged structured tool output where supported
|
|
2938
|
+
--help Show command help without authenticating
|
|
2939
|
+
--version Print the installed CLI version
|
|
2940
|
+
|
|
2941
|
+
Open control:
|
|
2942
|
+
--no-open Print the URL without launching a browser
|
|
2943
|
+
MAKEFX_NO_OPEN=1 Environment equivalent for scripts
|
|
2944
|
+
|
|
2945
|
+
Exit codes:
|
|
2946
|
+
0 success; 1 tool, network, or file error; 2 invalid command usage
|
|
2947
|
+
|
|
2948
|
+
Examples:
|
|
2949
|
+
makefx login --env production
|
|
2950
|
+
makefx create --space alv/flight --kind image --model image/frame \\
|
|
2951
|
+
--ref as_video:source --param t=last --wait
|
|
2952
|
+
makefx upload --space alv/flight --kind image --file ./frame.png \\
|
|
2953
|
+
--param source=camera --ref as_board:reference
|
|
2954
|
+
makefx export --space alv/flight --out ./flight.json
|
|
2955
|
+
|
|
2956
|
+
Run makefx <command> --help, makefx space help, makefx asset help, makefx audio help, or makefx voices help for exact arguments.
|
|
2957
|
+
`);
|
|
2958
|
+
}
|
|
2959
|
+
function helpForCommand(command) {
|
|
2960
|
+
const staticUsage = {
|
|
2961
|
+
login: "Usage: makefx login [--env production|stage|local] [--local]",
|
|
2962
|
+
logout: "Usage: makefx logout [--env production|stage|local] [--local]",
|
|
2963
|
+
mcp: "Usage: makefx mcp [--env production|stage|local] [--local]",
|
|
2964
|
+
create: "Usage: makefx create --space ACCOUNT/SPACE --kind image|video|audio --model MODEL [--prompt TEXT] [--ref ASSET:SLOT]... [--param NAME=VALUE]... [--count 1..8] [--name TEXT] [--seed INTEGER] [--position JSON] [--tags JSON] [--note TEXT] [--from-asset ASSET] [--recipe-mode current|exact] [--request-id ID] [--wait] [--json]\nRepeated --ref values preserve left-to-right order as reference order 0, 1, and so on. Reads the live catalog for the paying Space before calling create_asset.",
|
|
2965
|
+
export: `Usage: makefx ${CONVENIENCE_USAGE.export}`,
|
|
2966
|
+
open: `Usage: makefx ${CONVENIENCE_USAGE.open}
|
|
2967
|
+
Prints web_url before opening. Set MAKEFX_NO_OPEN=1 for scripts.`
|
|
2968
|
+
};
|
|
2969
|
+
if (command in staticUsage) return staticUsage[command];
|
|
2970
|
+
if (command === "upload" || command === "download") return transferCommandUsage(command);
|
|
2971
|
+
if (command === "purchase create" || command === "purchase get") {
|
|
2972
|
+
return purchaseCommandUsage(command);
|
|
2973
|
+
}
|
|
2974
|
+
if (command === "audio align" || command === "audio timings") return audioCommandUsage(command);
|
|
2975
|
+
if (command === "space publish" || command === "space unpublish") {
|
|
2976
|
+
return spaceVisibilityUsage(command);
|
|
2977
|
+
}
|
|
2978
|
+
if (["describe", "link", "unlink", "asset update", "asset delete", "profile update"].includes(command)) {
|
|
2979
|
+
return mutationCommandUsage(command);
|
|
2980
|
+
}
|
|
2981
|
+
if ([
|
|
2982
|
+
"account",
|
|
2983
|
+
"spaces",
|
|
2984
|
+
"space create",
|
|
2985
|
+
"space get",
|
|
2986
|
+
"space update",
|
|
2987
|
+
"space delete",
|
|
2988
|
+
"models",
|
|
2989
|
+
"estimate",
|
|
2990
|
+
"asset get",
|
|
2991
|
+
"profile get",
|
|
2992
|
+
"health"
|
|
2993
|
+
].includes(command)) {
|
|
2994
|
+
return commandUsage(command);
|
|
2995
|
+
}
|
|
2996
|
+
throw new CliUsageError(`Unknown help topic: ${command}`);
|
|
2997
|
+
}
|
|
2998
|
+
function printTopicHelp(topic, nested) {
|
|
2999
|
+
if (topic === "voices") {
|
|
3000
|
+
if (nested && nested !== "sync") throw new CliUsageError(`Unknown voices command: ${nested}`);
|
|
3001
|
+
console.log(mutationCommandUsage("voices sync"));
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
if (topic === "audio") {
|
|
3005
|
+
if (nested === "align" || nested === "timings") console.log(audioCommandUsage(`audio ${nested}`));
|
|
3006
|
+
else if (nested) throw new CliUsageError(`Unknown audio command: ${nested}`);
|
|
3007
|
+
else console.log([audioCommandUsage("audio align"), audioCommandUsage("audio timings")].join("\n"));
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
if (topic === "space" || topic === "asset" || topic === "purchase" || topic === "profile") {
|
|
3011
|
+
if (nested) console.log(helpForCommand(`${topic} ${nested}`));
|
|
3012
|
+
else printNestedHelp(topic);
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
console.log(helpForCommand(topic));
|
|
3016
|
+
}
|
|
3017
|
+
async function dispatchCommand(command, parsed) {
|
|
3018
|
+
if (parsed.options.help === "true" && command !== "space" && command !== "asset" && command !== "audio" && command !== "voices" && command !== "purchase" && command !== "profile") {
|
|
3019
|
+
console.log(helpForCommand(command));
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
switch (command) {
|
|
3023
|
+
case "login":
|
|
3024
|
+
await handleLogin(parsed);
|
|
3025
|
+
break;
|
|
3026
|
+
case "logout":
|
|
3027
|
+
await handleLogout(parsed);
|
|
3028
|
+
break;
|
|
3029
|
+
case "mcp":
|
|
3030
|
+
await handleMcp(parsed);
|
|
3031
|
+
break;
|
|
3032
|
+
case "create":
|
|
3033
|
+
await handleCreate(parsed);
|
|
3034
|
+
break;
|
|
3035
|
+
case "export":
|
|
3036
|
+
await handleExport(parsed);
|
|
3037
|
+
break;
|
|
3038
|
+
case "open":
|
|
3039
|
+
await handleOpen(parsed);
|
|
3040
|
+
break;
|
|
3041
|
+
case "upload":
|
|
3042
|
+
case "download":
|
|
3043
|
+
await handleTransferCommand(command, parsed);
|
|
3044
|
+
break;
|
|
3045
|
+
case "describe":
|
|
3046
|
+
case "link":
|
|
3047
|
+
case "unlink":
|
|
3048
|
+
await handleMutationCommand(command, parsed);
|
|
3049
|
+
break;
|
|
3050
|
+
case "voices": {
|
|
3051
|
+
const [subcommand, ...positionals] = parsed.positionals;
|
|
3052
|
+
if (!subcommand || subcommand === "help") {
|
|
3053
|
+
console.log(mutationCommandUsage("voices sync"));
|
|
3054
|
+
break;
|
|
3055
|
+
}
|
|
3056
|
+
if (subcommand !== "sync") throw new CliUsageError(`Unknown voices command: ${subcommand}`);
|
|
3057
|
+
if (parsed.options.help === "true") {
|
|
3058
|
+
console.log(mutationCommandUsage("voices sync"));
|
|
3059
|
+
break;
|
|
3060
|
+
}
|
|
3061
|
+
await handleMutationCommand("voices sync", { ...parsed, positionals });
|
|
3062
|
+
break;
|
|
3063
|
+
}
|
|
3064
|
+
case "audio": {
|
|
3065
|
+
const [subcommand, ...positionals] = parsed.positionals;
|
|
3066
|
+
if (!subcommand || subcommand === "help") {
|
|
3067
|
+
console.log([audioCommandUsage("audio align"), audioCommandUsage("audio timings")].join("\n"));
|
|
3068
|
+
break;
|
|
3069
|
+
}
|
|
3070
|
+
const nested = `audio ${subcommand}`;
|
|
3071
|
+
if (nested !== "audio align" && nested !== "audio timings") {
|
|
3072
|
+
throw new CliUsageError(`Unknown audio command: ${subcommand}`);
|
|
3073
|
+
}
|
|
3074
|
+
if (parsed.options.help === "true") {
|
|
3075
|
+
console.log(audioCommandUsage(nested));
|
|
3076
|
+
break;
|
|
3077
|
+
}
|
|
3078
|
+
await handleAudioCommand(nested, { ...parsed, positionals });
|
|
3079
|
+
break;
|
|
3080
|
+
}
|
|
3081
|
+
case "account":
|
|
3082
|
+
case "spaces":
|
|
3083
|
+
case "models":
|
|
3084
|
+
case "estimate":
|
|
3085
|
+
case "health":
|
|
3086
|
+
await handleDataCommand(command, parsed);
|
|
3087
|
+
break;
|
|
3088
|
+
case "purchase":
|
|
3089
|
+
case "profile": {
|
|
3090
|
+
const [subcommand, ...positionals] = parsed.positionals;
|
|
3091
|
+
if (!subcommand || subcommand === "help") {
|
|
3092
|
+
printNestedHelp(command);
|
|
3093
|
+
break;
|
|
3094
|
+
}
|
|
3095
|
+
const nested = `${command} ${subcommand}`;
|
|
3096
|
+
const allowed = command === "purchase" ? ["purchase create", "purchase get"] : ["profile get", "profile update"];
|
|
3097
|
+
if (!allowed.includes(nested)) {
|
|
3098
|
+
throw new CliUsageError(`Unknown ${command} command: ${subcommand}`);
|
|
3099
|
+
}
|
|
3100
|
+
if (parsed.options.help === "true") {
|
|
3101
|
+
console.log(helpForCommand(nested));
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
const nestedParsed = { ...parsed, positionals };
|
|
3105
|
+
if (nested === "purchase create" || nested === "purchase get") {
|
|
3106
|
+
await handlePurchaseCommand(nested, nestedParsed);
|
|
3107
|
+
} else if (nested === "profile update") {
|
|
3108
|
+
await handleMutationCommand(nested, nestedParsed);
|
|
3109
|
+
} else {
|
|
3110
|
+
await handleDataCommand(nested, nestedParsed);
|
|
3111
|
+
}
|
|
3112
|
+
break;
|
|
3113
|
+
}
|
|
3114
|
+
case "space":
|
|
3115
|
+
case "asset": {
|
|
3116
|
+
const [subcommand, ...positionals] = parsed.positionals;
|
|
3117
|
+
if (!subcommand || subcommand === "help") {
|
|
3118
|
+
printNestedHelp(command);
|
|
3119
|
+
break;
|
|
3120
|
+
}
|
|
3121
|
+
const nested = `${command} ${subcommand}`;
|
|
3122
|
+
if (![
|
|
3123
|
+
"space create",
|
|
3124
|
+
"space get",
|
|
3125
|
+
"space update",
|
|
3126
|
+
"space publish",
|
|
3127
|
+
"space unpublish",
|
|
3128
|
+
"space delete",
|
|
3129
|
+
"asset get",
|
|
3130
|
+
"asset update",
|
|
3131
|
+
"asset delete"
|
|
3132
|
+
].includes(nested)) {
|
|
3133
|
+
throw new CliUsageError(`Unknown ${command} command: ${subcommand}`);
|
|
3134
|
+
}
|
|
3135
|
+
if (parsed.options.help === "true") {
|
|
3136
|
+
console.log(helpForCommand(nested));
|
|
3137
|
+
break;
|
|
3138
|
+
}
|
|
3139
|
+
const nestedParsed = { ...parsed, positionals };
|
|
3140
|
+
if (nested === "space publish" || nested === "space unpublish") {
|
|
3141
|
+
await handleSpaceVisibilityCommand(nested, nestedParsed);
|
|
3142
|
+
} else if (nested === "space update" || nested === "asset update" || nested === "asset delete") {
|
|
3143
|
+
await handleMutationCommand(nested, nestedParsed);
|
|
3144
|
+
} else {
|
|
3145
|
+
await handleDataCommand(nested, nestedParsed);
|
|
3146
|
+
}
|
|
3147
|
+
break;
|
|
3148
|
+
}
|
|
3149
|
+
default:
|
|
3150
|
+
throw new CliUsageError(`Unknown command: ${command}`);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
function printNestedHelp(command) {
|
|
3154
|
+
if (command === "purchase") {
|
|
3155
|
+
console.log([purchaseCommandUsage("purchase create"), purchaseCommandUsage("purchase get")].join("\n"));
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
if (command === "profile") {
|
|
3159
|
+
console.log([commandUsage("profile get"), mutationCommandUsage("profile update")].join("\n"));
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
const commands = command === "space" ? ["space create", "space get", "space delete"] : ["asset get"];
|
|
3163
|
+
const usage2 = commands.map(commandUsage);
|
|
3164
|
+
if (command === "space") {
|
|
3165
|
+
usage2.push(
|
|
3166
|
+
mutationCommandUsage("space update"),
|
|
3167
|
+
spaceVisibilityUsage("space publish"),
|
|
3168
|
+
spaceVisibilityUsage("space unpublish")
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
if (command === "asset") {
|
|
3172
|
+
usage2.push(mutationCommandUsage("asset update"), mutationCommandUsage("asset delete"));
|
|
3173
|
+
}
|
|
3174
|
+
console.log(usage2.join("\n"));
|
|
3175
|
+
}
|
|
3176
|
+
void main();
|