@warlok-net/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-YTUZMXWJ.mjs +781 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +798 -0
- package/dist/cli.mjs +12 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +820 -0
- package/dist/index.mjs +22 -0
- package/package.json +45 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { stdin as input, stdout as output } from "process";
|
|
6
|
+
import { createInterface } from "readline/promises";
|
|
7
|
+
import {
|
|
8
|
+
WarlokClient,
|
|
9
|
+
WarlokError
|
|
10
|
+
} from "@warlok-net/sdk";
|
|
11
|
+
var SHORT_FLAG_MAP = {
|
|
12
|
+
p: "prompt",
|
|
13
|
+
d: "description",
|
|
14
|
+
i: "image",
|
|
15
|
+
e: "entity",
|
|
16
|
+
t: "entity",
|
|
17
|
+
n: "name",
|
|
18
|
+
o: "output",
|
|
19
|
+
k: "api-key",
|
|
20
|
+
u: "base-url"
|
|
21
|
+
};
|
|
22
|
+
var DEFAULT_API_URL = "https://api.warlok.net";
|
|
23
|
+
var DEFAULT_CONFIG_FILENAME = "config.json";
|
|
24
|
+
var GENERATION_STATUSES = /* @__PURE__ */ new Set([
|
|
25
|
+
"pending",
|
|
26
|
+
"preprocessing",
|
|
27
|
+
"generating",
|
|
28
|
+
"rigging",
|
|
29
|
+
"complete",
|
|
30
|
+
"failed"
|
|
31
|
+
]);
|
|
32
|
+
var ENTITY_ALIASES = {
|
|
33
|
+
noun: "noun",
|
|
34
|
+
person: "person",
|
|
35
|
+
place: "place",
|
|
36
|
+
thing: "thing",
|
|
37
|
+
character: "person",
|
|
38
|
+
creature: "person",
|
|
39
|
+
environment: "place",
|
|
40
|
+
prop: "thing",
|
|
41
|
+
weapon: "thing",
|
|
42
|
+
vehicle: "thing",
|
|
43
|
+
material: "thing"
|
|
44
|
+
};
|
|
45
|
+
function parseArgs(args) {
|
|
46
|
+
const positional = [];
|
|
47
|
+
const options = {};
|
|
48
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
49
|
+
const token = args[i];
|
|
50
|
+
if (token === "--") {
|
|
51
|
+
positional.push(...args.slice(i + 1));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (token.startsWith("--no-")) {
|
|
55
|
+
options[token.slice(5)] = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (token.startsWith("--")) {
|
|
59
|
+
const [rawKey, inlineValue] = token.slice(2).split("=", 2);
|
|
60
|
+
if (inlineValue !== void 0) {
|
|
61
|
+
options[rawKey] = inlineValue;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const next = args[i + 1];
|
|
65
|
+
if (next && !next.startsWith("-")) {
|
|
66
|
+
options[rawKey] = next;
|
|
67
|
+
i += 1;
|
|
68
|
+
} else {
|
|
69
|
+
options[rawKey] = true;
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
74
|
+
const raw = token.slice(1);
|
|
75
|
+
if (raw.length > 1) {
|
|
76
|
+
const alias2 = SHORT_FLAG_MAP[raw[0]];
|
|
77
|
+
if (alias2) {
|
|
78
|
+
options[alias2] = raw.slice(1);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const alias = SHORT_FLAG_MAP[raw];
|
|
83
|
+
if (!alias) {
|
|
84
|
+
positional.push(token);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const next = args[i + 1];
|
|
88
|
+
if (next && !next.startsWith("-")) {
|
|
89
|
+
options[alias] = next;
|
|
90
|
+
i += 1;
|
|
91
|
+
} else {
|
|
92
|
+
options[alias] = true;
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
positional.push(token);
|
|
97
|
+
}
|
|
98
|
+
return { positional, options };
|
|
99
|
+
}
|
|
100
|
+
function stringOption(options, key) {
|
|
101
|
+
const value = options[key];
|
|
102
|
+
if (typeof value === "string" && value.trim()) {
|
|
103
|
+
return value.trim();
|
|
104
|
+
}
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
function numberOption(options, key) {
|
|
108
|
+
const raw = stringOption(options, key);
|
|
109
|
+
if (!raw) {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
const parsed = Number(raw);
|
|
113
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
114
|
+
}
|
|
115
|
+
function booleanOption(options, key, defaultValue) {
|
|
116
|
+
const value = options[key];
|
|
117
|
+
if (typeof value === "boolean") {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
if (typeof value === "string") {
|
|
121
|
+
const normalized = value.toLowerCase();
|
|
122
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return defaultValue;
|
|
130
|
+
}
|
|
131
|
+
function isUrl(value) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = new URL(value);
|
|
134
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function normalizeEntityType(raw) {
|
|
140
|
+
if (!raw) {
|
|
141
|
+
return "thing";
|
|
142
|
+
}
|
|
143
|
+
const normalized = raw.toLowerCase();
|
|
144
|
+
const mapped = ENTITY_ALIASES[normalized];
|
|
145
|
+
if (!mapped) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Unsupported entity type "${raw}". Use noun|person|place|thing.`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return mapped;
|
|
151
|
+
}
|
|
152
|
+
function resolveConfigPath(options) {
|
|
153
|
+
return stringOption(options || {}, "config") || process.env.WARLOK_CONFIG_PATH || path.join(os.homedir(), ".warlok", DEFAULT_CONFIG_FILENAME);
|
|
154
|
+
}
|
|
155
|
+
async function readCliConfig(options) {
|
|
156
|
+
const configPath = resolveConfigPath(options);
|
|
157
|
+
try {
|
|
158
|
+
const raw = await readFile(configPath, "utf8");
|
|
159
|
+
const parsed = JSON.parse(raw);
|
|
160
|
+
if (!parsed || typeof parsed !== "object") {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
return parsed;
|
|
164
|
+
} catch {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function writeCliConfig(nextConfig, options) {
|
|
169
|
+
const configPath = resolveConfigPath(options);
|
|
170
|
+
const dir = path.dirname(configPath);
|
|
171
|
+
await mkdir(dir, { recursive: true });
|
|
172
|
+
await writeFile(configPath, `${JSON.stringify(nextConfig, null, 2)}
|
|
173
|
+
`, "utf8");
|
|
174
|
+
return configPath;
|
|
175
|
+
}
|
|
176
|
+
function resolveBaseUrl(options, config) {
|
|
177
|
+
return stringOption(options, "base-url") || process.env.WARLOK_API_URL || config?.apiUrl || DEFAULT_API_URL;
|
|
178
|
+
}
|
|
179
|
+
function isLocalBaseUrl(baseUrl) {
|
|
180
|
+
try {
|
|
181
|
+
const parsed = new URL(baseUrl);
|
|
182
|
+
return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname.endsWith(".local");
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function resolveApiKey(options, baseUrl, config) {
|
|
188
|
+
const provided = stringOption(options, "api-key") || process.env.WARLOK_API_KEY || config?.apiKey;
|
|
189
|
+
if (provided) {
|
|
190
|
+
return provided;
|
|
191
|
+
}
|
|
192
|
+
if (isLocalBaseUrl(baseUrl)) {
|
|
193
|
+
return "wk_dev_local";
|
|
194
|
+
}
|
|
195
|
+
throw new Error(
|
|
196
|
+
"Missing API key. Set WARLOK_API_KEY or pass --api-key <key>."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
function createClient(options, config) {
|
|
200
|
+
const baseUrl = resolveBaseUrl(options, config);
|
|
201
|
+
const apiKey = resolveApiKey(options, baseUrl, config);
|
|
202
|
+
return new WarlokClient({ apiKey, baseUrl });
|
|
203
|
+
}
|
|
204
|
+
function shouldPrintJson(options) {
|
|
205
|
+
return booleanOption(options, "json", false);
|
|
206
|
+
}
|
|
207
|
+
function print(value, asJson) {
|
|
208
|
+
if (asJson) {
|
|
209
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
210
|
+
`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (typeof value === "string") {
|
|
214
|
+
process.stdout.write(`${value}
|
|
215
|
+
`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
async function loadImageInput(imageInput) {
|
|
222
|
+
if (!imageInput) {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
if (isUrl(imageInput)) {
|
|
226
|
+
return { imageUrl: imageInput };
|
|
227
|
+
}
|
|
228
|
+
const absolutePath = path.resolve(process.cwd(), imageInput);
|
|
229
|
+
const data = await readFile(absolutePath);
|
|
230
|
+
return { imageBase64: data.toString("base64") };
|
|
231
|
+
}
|
|
232
|
+
function defaultOutputPath(id, format) {
|
|
233
|
+
return path.resolve(
|
|
234
|
+
process.cwd(),
|
|
235
|
+
format === "rigged" ? `${id}-rigged.glb` : `${id}.glb`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
async function writeBlobToFile(blob, outputPath) {
|
|
239
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
240
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
241
|
+
await writeFile(outputPath, buffer);
|
|
242
|
+
}
|
|
243
|
+
function formatGenerationSummary(generation) {
|
|
244
|
+
return {
|
|
245
|
+
id: generation.id,
|
|
246
|
+
name: generation.name,
|
|
247
|
+
entityType: generation.entityType,
|
|
248
|
+
status: generation.status,
|
|
249
|
+
progress: generation.progress,
|
|
250
|
+
modelUrl: generation.outputs.modelUrl,
|
|
251
|
+
riggedModelUrl: generation.outputs.riggedModelUrl,
|
|
252
|
+
error: generation.error,
|
|
253
|
+
createdAt: generation.createdAt,
|
|
254
|
+
updatedAt: generation.updatedAt
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function runGenerate(args) {
|
|
258
|
+
const parsed = parseArgs(args);
|
|
259
|
+
const options = parsed.options;
|
|
260
|
+
const positional = [...parsed.positional];
|
|
261
|
+
const isCharacterSubcommand = positional[0]?.toLowerCase() === "character";
|
|
262
|
+
const descriptionPositional = isCharacterSubcommand ? positional.slice(1).join(" ").trim() : "";
|
|
263
|
+
const prompt = stringOption(options, "prompt") || stringOption(options, "description") || (descriptionPositional || void 0);
|
|
264
|
+
const image = stringOption(options, "image");
|
|
265
|
+
if (!prompt && !image) {
|
|
266
|
+
if (isCharacterSubcommand) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"Character generation requires a description and/or --image."
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
throw new Error("Generate requires --prompt/--description and/or --image.");
|
|
272
|
+
}
|
|
273
|
+
const requestedEntity = stringOption(options, "entity") || stringOption(options, "type");
|
|
274
|
+
const entityType = isCharacterSubcommand ? "person" : normalizeEntityType(requestedEntity);
|
|
275
|
+
if (isCharacterSubcommand && requestedEntity) {
|
|
276
|
+
const normalized = normalizeEntityType(requestedEntity);
|
|
277
|
+
if (normalized !== "person") {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"Character subcommand always generates entity type person. Remove --entity/--type override."
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const config = await readCliConfig(options);
|
|
284
|
+
const client = createClient(options, config);
|
|
285
|
+
const { imageUrl, imageBase64 } = await loadImageInput(image);
|
|
286
|
+
const generation = await client.generate({
|
|
287
|
+
name: stringOption(options, "name"),
|
|
288
|
+
entityType,
|
|
289
|
+
prompt,
|
|
290
|
+
imageUrl,
|
|
291
|
+
imageBase64,
|
|
292
|
+
options: {
|
|
293
|
+
rigForMixamo: booleanOption(options, "rig", entityType === "person"),
|
|
294
|
+
includeTpose: booleanOption(options, "tpose", entityType === "person"),
|
|
295
|
+
optimize: booleanOption(options, "optimize", true)
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
const wait = booleanOption(options, "wait", false);
|
|
299
|
+
const asJson = shouldPrintJson(options);
|
|
300
|
+
if (!wait) {
|
|
301
|
+
print(
|
|
302
|
+
{
|
|
303
|
+
...formatGenerationSummary(generation),
|
|
304
|
+
message: "Generation queued. Use `warlok status <id>` to check progress, or pass --wait."
|
|
305
|
+
},
|
|
306
|
+
asJson
|
|
307
|
+
);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const completed = await client.waitForCompletion(generation.id, {
|
|
311
|
+
pollingInterval: numberOption(options, "interval") || 2e3,
|
|
312
|
+
timeout: numberOption(options, "timeout") || 3e5
|
|
313
|
+
});
|
|
314
|
+
print(formatGenerationSummary(completed), asJson);
|
|
315
|
+
}
|
|
316
|
+
async function runStatus(args) {
|
|
317
|
+
const parsed = parseArgs(args);
|
|
318
|
+
const id = parsed.positional[0];
|
|
319
|
+
if (!id) {
|
|
320
|
+
throw new Error("Status requires a generation id: warlok status <id>");
|
|
321
|
+
}
|
|
322
|
+
const config = await readCliConfig(parsed.options);
|
|
323
|
+
const client = createClient(parsed.options, config);
|
|
324
|
+
const generation = await client.getGeneration(id);
|
|
325
|
+
print(formatGenerationSummary(generation), shouldPrintJson(parsed.options));
|
|
326
|
+
}
|
|
327
|
+
async function runList(args) {
|
|
328
|
+
const parsed = parseArgs(args);
|
|
329
|
+
const statusInput = stringOption(parsed.options, "status");
|
|
330
|
+
const status = statusInput ? statusInput : void 0;
|
|
331
|
+
if (status && !GENERATION_STATUSES.has(status)) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Unsupported status "${statusInput}". Use pending|preprocessing|generating|rigging|complete|failed.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const config = await readCliConfig(parsed.options);
|
|
337
|
+
const client = createClient(parsed.options, config);
|
|
338
|
+
const generations = await client.listGenerations({
|
|
339
|
+
limit: numberOption(parsed.options, "limit"),
|
|
340
|
+
status,
|
|
341
|
+
entityType: normalizeEntityType(stringOption(parsed.options, "entity"))
|
|
342
|
+
});
|
|
343
|
+
const rows = generations.map(formatGenerationSummary);
|
|
344
|
+
print({ total: rows.length, items: rows }, true);
|
|
345
|
+
}
|
|
346
|
+
async function runDownload(args) {
|
|
347
|
+
const parsed = parseArgs(args);
|
|
348
|
+
const id = parsed.positional[0];
|
|
349
|
+
if (!id) {
|
|
350
|
+
throw new Error("Download requires a generation id: warlok download <id>");
|
|
351
|
+
}
|
|
352
|
+
const format = booleanOption(
|
|
353
|
+
parsed.options,
|
|
354
|
+
"rigged",
|
|
355
|
+
false
|
|
356
|
+
) ? "rigged" : booleanOption(parsed.options, "model", false) ? "model" : void 0;
|
|
357
|
+
const outputPath = stringOption(parsed.options, "output") || defaultOutputPath(id, format);
|
|
358
|
+
const config = await readCliConfig(parsed.options);
|
|
359
|
+
const client = createClient(parsed.options, config);
|
|
360
|
+
const blob = await client.downloadGeneration(id, format);
|
|
361
|
+
await writeBlobToFile(blob, outputPath);
|
|
362
|
+
print(
|
|
363
|
+
{
|
|
364
|
+
id,
|
|
365
|
+
format: format || "best_available",
|
|
366
|
+
output: outputPath
|
|
367
|
+
},
|
|
368
|
+
true
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
async function runTools(args) {
|
|
372
|
+
const parsed = parseArgs(args);
|
|
373
|
+
const config = await readCliConfig(parsed.options);
|
|
374
|
+
const baseUrl = resolveBaseUrl(parsed.options, config);
|
|
375
|
+
const apiKey = resolveApiKey(parsed.options, baseUrl, config);
|
|
376
|
+
const response = await fetch(`${baseUrl}/api/v1/mcp/tools`, {
|
|
377
|
+
headers: {
|
|
378
|
+
Authorization: `Bearer ${apiKey}`
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
const payload = await response.json();
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
payload?.error?.message || `Failed to fetch tools (${response.status})`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
print(payload?.data || payload, true);
|
|
388
|
+
}
|
|
389
|
+
async function runAgent(args) {
|
|
390
|
+
const [subcommand, ...rest] = args;
|
|
391
|
+
if (subcommand !== "run") {
|
|
392
|
+
throw new Error("Agent command requires `run`: warlok agent run <goal>");
|
|
393
|
+
}
|
|
394
|
+
const parsed = parseArgs(rest);
|
|
395
|
+
const goal = parsed.positional.join(" ").trim();
|
|
396
|
+
if (!goal) {
|
|
397
|
+
throw new Error("Agent goal is required: warlok agent run <goal>");
|
|
398
|
+
}
|
|
399
|
+
const config = await readCliConfig(parsed.options);
|
|
400
|
+
const baseUrl = resolveBaseUrl(parsed.options, config);
|
|
401
|
+
const apiKey = resolveApiKey(parsed.options, baseUrl, config);
|
|
402
|
+
const toolsResponse = await fetch(`${baseUrl}/api/v1/mcp/tools`, {
|
|
403
|
+
headers: {
|
|
404
|
+
Authorization: `Bearer ${apiKey}`
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
const toolsPayload = await toolsResponse.json();
|
|
408
|
+
const tools = toolsPayload?.data?.tools || [];
|
|
409
|
+
print(
|
|
410
|
+
{
|
|
411
|
+
goal,
|
|
412
|
+
message: "Use the listed MCP tools from your agent runtime to execute this goal.",
|
|
413
|
+
tools: tools.map((tool) => ({
|
|
414
|
+
name: tool.name,
|
|
415
|
+
description: tool.description
|
|
416
|
+
}))
|
|
417
|
+
},
|
|
418
|
+
true
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
function redactApiKey(key) {
|
|
422
|
+
if (!key) {
|
|
423
|
+
return void 0;
|
|
424
|
+
}
|
|
425
|
+
if (key.length <= 8) {
|
|
426
|
+
return "****";
|
|
427
|
+
}
|
|
428
|
+
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
429
|
+
}
|
|
430
|
+
async function promptForApiKey() {
|
|
431
|
+
const rl = createInterface({ input, output });
|
|
432
|
+
try {
|
|
433
|
+
const entered = (await rl.question("Enter WARLOK API key: ")).trim();
|
|
434
|
+
if (!entered) {
|
|
435
|
+
throw new Error("API key is required.");
|
|
436
|
+
}
|
|
437
|
+
return entered;
|
|
438
|
+
} finally {
|
|
439
|
+
rl.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function verifyCredentials(apiKey, baseUrl) {
|
|
443
|
+
try {
|
|
444
|
+
const response = await fetch(`${baseUrl}/api/v1/generations`, {
|
|
445
|
+
headers: {
|
|
446
|
+
Authorization: `Bearer ${apiKey}`
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
if (response.ok) {
|
|
450
|
+
return { ok: true };
|
|
451
|
+
}
|
|
452
|
+
let message = `HTTP ${response.status}`;
|
|
453
|
+
try {
|
|
454
|
+
const payload = await response.json();
|
|
455
|
+
message = payload?.error?.message || payload?.error?.code || message;
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
return { ok: false, reason: message };
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
reason: error instanceof Error ? error.message : "request failed"
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function runDoctor(args) {
|
|
467
|
+
const parsed = parseArgs(args);
|
|
468
|
+
const asJson = shouldPrintJson(parsed.options);
|
|
469
|
+
const config = await readCliConfig(parsed.options);
|
|
470
|
+
const baseUrl = resolveBaseUrl(parsed.options, config);
|
|
471
|
+
const hasApiKey = Boolean(
|
|
472
|
+
stringOption(parsed.options, "api-key") || process.env.WARLOK_API_KEY || config.apiKey
|
|
473
|
+
);
|
|
474
|
+
const checks = [];
|
|
475
|
+
checks.push({
|
|
476
|
+
name: "base_url",
|
|
477
|
+
ok: isUrl(baseUrl),
|
|
478
|
+
detail: isUrl(baseUrl) ? baseUrl : `invalid URL: ${baseUrl}`
|
|
479
|
+
});
|
|
480
|
+
let healthOk = false;
|
|
481
|
+
try {
|
|
482
|
+
const response = await fetch(`${baseUrl}/api/v1/health`);
|
|
483
|
+
healthOk = response.ok;
|
|
484
|
+
checks.push({
|
|
485
|
+
name: "health",
|
|
486
|
+
ok: response.ok,
|
|
487
|
+
detail: response.ok ? "reachable" : `HTTP ${response.status}`
|
|
488
|
+
});
|
|
489
|
+
} catch (error) {
|
|
490
|
+
checks.push({
|
|
491
|
+
name: "health",
|
|
492
|
+
ok: false,
|
|
493
|
+
detail: error instanceof Error ? error.message : "request failed"
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
checks.push({
|
|
497
|
+
name: "api_key",
|
|
498
|
+
ok: hasApiKey,
|
|
499
|
+
detail: hasApiKey ? "configured" : "missing (WARLOK_API_KEY/config/--api-key)"
|
|
500
|
+
});
|
|
501
|
+
if (hasApiKey && healthOk) {
|
|
502
|
+
try {
|
|
503
|
+
const client = createClient(parsed.options, config);
|
|
504
|
+
await client.listGenerations({ limit: 1 });
|
|
505
|
+
checks.push({
|
|
506
|
+
name: "auth",
|
|
507
|
+
ok: true,
|
|
508
|
+
detail: "API key accepted"
|
|
509
|
+
});
|
|
510
|
+
} catch (error) {
|
|
511
|
+
checks.push({
|
|
512
|
+
name: "auth",
|
|
513
|
+
ok: false,
|
|
514
|
+
detail: formatCliError(error)
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (hasApiKey && healthOk) {
|
|
519
|
+
try {
|
|
520
|
+
const apiKey = resolveApiKey(parsed.options, baseUrl, config);
|
|
521
|
+
const response = await fetch(`${baseUrl}/api/v1/mcp/tools`, {
|
|
522
|
+
headers: {
|
|
523
|
+
Authorization: `Bearer ${apiKey}`
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
checks.push({
|
|
527
|
+
name: "mcp_tools",
|
|
528
|
+
ok: response.ok,
|
|
529
|
+
detail: response.ok ? "reachable" : `HTTP ${response.status}`
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
checks.push({
|
|
533
|
+
name: "mcp_tools",
|
|
534
|
+
ok: false,
|
|
535
|
+
detail: error instanceof Error ? error.message : "request failed"
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const summary = {
|
|
540
|
+
ok: checks.every((check) => check.ok),
|
|
541
|
+
baseUrl,
|
|
542
|
+
configPath: resolveConfigPath(parsed.options),
|
|
543
|
+
checks
|
|
544
|
+
};
|
|
545
|
+
if (asJson) {
|
|
546
|
+
print(summary, true);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
process.stdout.write("Warlok doctor\n\n");
|
|
550
|
+
for (const check of checks) {
|
|
551
|
+
process.stdout.write(
|
|
552
|
+
`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}
|
|
553
|
+
`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
process.stdout.write(`
|
|
557
|
+
Overall: ${summary.ok ? "healthy" : "issues found"}
|
|
558
|
+
`);
|
|
559
|
+
}
|
|
560
|
+
async function runAuth(args) {
|
|
561
|
+
const [subcommand, ...rest] = args;
|
|
562
|
+
const parsed = parseArgs(rest);
|
|
563
|
+
const asJson = shouldPrintJson(parsed.options);
|
|
564
|
+
const config = await readCliConfig(parsed.options);
|
|
565
|
+
const baseUrl = resolveBaseUrl(parsed.options, config);
|
|
566
|
+
switch (subcommand) {
|
|
567
|
+
case "login": {
|
|
568
|
+
const apiKey = stringOption(parsed.options, "api-key") || process.env.WARLOK_API_KEY || await promptForApiKey();
|
|
569
|
+
const nextConfig = {
|
|
570
|
+
...config,
|
|
571
|
+
apiUrl: baseUrl,
|
|
572
|
+
apiKey,
|
|
573
|
+
profile: config.profile || "default"
|
|
574
|
+
};
|
|
575
|
+
const verify = await verifyCredentials(apiKey, baseUrl);
|
|
576
|
+
if (!verify.ok) {
|
|
577
|
+
throw new Error(`Auth verification failed: ${verify.reason || "unknown"}`);
|
|
578
|
+
}
|
|
579
|
+
const savedPath = await writeCliConfig(nextConfig, parsed.options);
|
|
580
|
+
print(
|
|
581
|
+
{
|
|
582
|
+
message: "Login successful",
|
|
583
|
+
apiUrl: baseUrl,
|
|
584
|
+
apiKey: redactApiKey(apiKey),
|
|
585
|
+
configPath: savedPath
|
|
586
|
+
},
|
|
587
|
+
asJson
|
|
588
|
+
);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
case "whoami": {
|
|
592
|
+
const effectiveApiKey = stringOption(parsed.options, "api-key") || process.env.WARLOK_API_KEY || config.apiKey;
|
|
593
|
+
const verify = effectiveApiKey ? await verifyCredentials(effectiveApiKey, baseUrl) : { ok: false, reason: "no API key configured" };
|
|
594
|
+
print(
|
|
595
|
+
{
|
|
596
|
+
authenticated: verify.ok,
|
|
597
|
+
reason: verify.ok ? void 0 : verify.reason,
|
|
598
|
+
apiUrl: baseUrl,
|
|
599
|
+
apiKey: redactApiKey(effectiveApiKey),
|
|
600
|
+
configPath: resolveConfigPath(parsed.options),
|
|
601
|
+
source: {
|
|
602
|
+
apiKey: stringOption(parsed.options, "api-key") ? "flag" : process.env.WARLOK_API_KEY ? "env" : config.apiKey ? "config" : "none"
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
asJson
|
|
606
|
+
);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
case "logout": {
|
|
610
|
+
const keepBaseUrl = booleanOption(parsed.options, "keep-base-url", true);
|
|
611
|
+
const nextConfig = {
|
|
612
|
+
...config,
|
|
613
|
+
apiKey: void 0,
|
|
614
|
+
apiUrl: keepBaseUrl ? config.apiUrl : void 0
|
|
615
|
+
};
|
|
616
|
+
const savedPath = await writeCliConfig(nextConfig, parsed.options);
|
|
617
|
+
print(
|
|
618
|
+
{
|
|
619
|
+
message: "Logged out",
|
|
620
|
+
configPath: savedPath,
|
|
621
|
+
keptBaseUrl: keepBaseUrl
|
|
622
|
+
},
|
|
623
|
+
asJson
|
|
624
|
+
);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
default:
|
|
628
|
+
throw new Error("Auth command requires login|whoami|logout");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function runMcp(args) {
|
|
632
|
+
const [subcommand, ...rest] = args;
|
|
633
|
+
const parsed = parseArgs(rest);
|
|
634
|
+
const config = await readCliConfig(parsed.options);
|
|
635
|
+
const baseUrl = resolveBaseUrl(parsed.options, config);
|
|
636
|
+
const apiKey = resolveApiKey(parsed.options, baseUrl, config);
|
|
637
|
+
if (subcommand !== "print-config") {
|
|
638
|
+
throw new Error("MCP command requires `print-config`.");
|
|
639
|
+
}
|
|
640
|
+
const json = {
|
|
641
|
+
mcpServers: {
|
|
642
|
+
warlok: {
|
|
643
|
+
command: "pnpm",
|
|
644
|
+
args: ["dlx", "@warlok-net/mcp-server@latest"],
|
|
645
|
+
env: {
|
|
646
|
+
WARLOK_API_URL: baseUrl,
|
|
647
|
+
WARLOK_API_KEY: apiKey
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const format = stringOption(parsed.options, "format") || "json";
|
|
653
|
+
if (format === "json") {
|
|
654
|
+
print(json, true);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (format === "env") {
|
|
658
|
+
process.stdout.write(`WARLOK_API_URL=${baseUrl}
|
|
659
|
+
`);
|
|
660
|
+
process.stdout.write(`WARLOK_API_KEY=${apiKey}
|
|
661
|
+
`);
|
|
662
|
+
process.stdout.write("pnpm dlx @warlok-net/mcp-server@latest\n");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
throw new Error("Unsupported MCP format. Use --format json|env.");
|
|
666
|
+
}
|
|
667
|
+
function mainHelpText() {
|
|
668
|
+
return `
|
|
669
|
+
Warlok CLI
|
|
670
|
+
|
|
671
|
+
Usage:
|
|
672
|
+
warlok <command> [options]
|
|
673
|
+
|
|
674
|
+
Commands:
|
|
675
|
+
generate Start a generation from prompt/image
|
|
676
|
+
status <id> Get generation status
|
|
677
|
+
list List generations
|
|
678
|
+
download <id> Download generated model
|
|
679
|
+
auth Login/logout and inspect CLI auth state
|
|
680
|
+
doctor Validate local CLI/API configuration
|
|
681
|
+
mcp MCP helper commands
|
|
682
|
+
tools List MCP-compatible tools from API
|
|
683
|
+
agent run Print agent execution context for a goal
|
|
684
|
+
|
|
685
|
+
Global options:
|
|
686
|
+
--api-key <key> API key (or set WARLOK_API_KEY)
|
|
687
|
+
--base-url <url> API base URL (or set WARLOK_API_URL)
|
|
688
|
+
--config <path> Config path (or set WARLOK_CONFIG_PATH)
|
|
689
|
+
--json JSON output where supported
|
|
690
|
+
|
|
691
|
+
Generate options:
|
|
692
|
+
generate character "<description>" [--image <path-or-url>] [--wait]
|
|
693
|
+
--prompt, -p <text>
|
|
694
|
+
--description, -d <text> Alias for --prompt
|
|
695
|
+
--image, -i <path-or-url>
|
|
696
|
+
--entity, -e <noun|person|place|thing> (aliases supported)
|
|
697
|
+
--name, -n <name>
|
|
698
|
+
--wait Poll until terminal status
|
|
699
|
+
--interval <ms> Poll interval when --wait (default 2000)
|
|
700
|
+
--timeout <ms> Timeout when --wait (default 300000)
|
|
701
|
+
--rig / --no-rig
|
|
702
|
+
--tpose / --no-tpose
|
|
703
|
+
--optimize / --no-optimize
|
|
704
|
+
|
|
705
|
+
Download options:
|
|
706
|
+
--rigged Prefer rigged model
|
|
707
|
+
--model Prefer base model
|
|
708
|
+
--output, -o <path> Save path (default: ./<id>.glb)
|
|
709
|
+
|
|
710
|
+
Auth:
|
|
711
|
+
auth login [--api-key <key>] [--base-url <url>]
|
|
712
|
+
auth whoami
|
|
713
|
+
auth logout [--keep-base-url=true]
|
|
714
|
+
|
|
715
|
+
Doctor:
|
|
716
|
+
doctor [--json]
|
|
717
|
+
|
|
718
|
+
MCP:
|
|
719
|
+
mcp print-config [--format json|env]
|
|
720
|
+
`.trimStart();
|
|
721
|
+
}
|
|
722
|
+
async function run(argv) {
|
|
723
|
+
const [command, ...rest] = argv;
|
|
724
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
725
|
+
process.stdout.write(mainHelpText());
|
|
726
|
+
process.stdout.write("\n");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
switch (command) {
|
|
730
|
+
case "generate":
|
|
731
|
+
await runGenerate(rest);
|
|
732
|
+
return;
|
|
733
|
+
case "status":
|
|
734
|
+
await runStatus(rest);
|
|
735
|
+
return;
|
|
736
|
+
case "list":
|
|
737
|
+
await runList(rest);
|
|
738
|
+
return;
|
|
739
|
+
case "download":
|
|
740
|
+
await runDownload(rest);
|
|
741
|
+
return;
|
|
742
|
+
case "auth":
|
|
743
|
+
await runAuth(rest);
|
|
744
|
+
return;
|
|
745
|
+
case "doctor":
|
|
746
|
+
await runDoctor(rest);
|
|
747
|
+
return;
|
|
748
|
+
case "mcp":
|
|
749
|
+
await runMcp(rest);
|
|
750
|
+
return;
|
|
751
|
+
case "tools":
|
|
752
|
+
await runTools(rest);
|
|
753
|
+
return;
|
|
754
|
+
case "agent":
|
|
755
|
+
await runAgent(rest);
|
|
756
|
+
return;
|
|
757
|
+
default:
|
|
758
|
+
throw new Error(`Unknown command: ${command}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function formatCliError(error) {
|
|
762
|
+
if (error instanceof WarlokError) {
|
|
763
|
+
return `${error.message} [${error.code}]`;
|
|
764
|
+
}
|
|
765
|
+
if (error instanceof Error) {
|
|
766
|
+
return error.message;
|
|
767
|
+
}
|
|
768
|
+
return "Unknown error";
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export {
|
|
772
|
+
runGenerate,
|
|
773
|
+
runStatus,
|
|
774
|
+
runList,
|
|
775
|
+
runDownload,
|
|
776
|
+
runTools,
|
|
777
|
+
runAgent,
|
|
778
|
+
mainHelpText,
|
|
779
|
+
run,
|
|
780
|
+
formatCliError
|
|
781
|
+
};
|