@nextclaw/aigen 0.1.0-beta.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.
@@ -0,0 +1,963 @@
1
+ #!/usr/bin/env node
2
+ import { Command, CommanderError, InvalidArgumentError } from "commander";
3
+ import { chmod, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { basename, join, resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { createHash } from "node:crypto";
7
+ //#region src/types/cli-output.types.ts
8
+ var AigenError = class extends Error {
9
+ code;
10
+ retryable;
11
+ constructor(code, message, options) {
12
+ super(message);
13
+ this.name = "AigenError";
14
+ this.code = code;
15
+ this.retryable = options?.retryable ?? false;
16
+ }
17
+ };
18
+ //#endregion
19
+ //#region src/utils/route.utils.ts
20
+ const parseModelRoute = (route) => {
21
+ const slashIndex = route.indexOf("/");
22
+ if (slashIndex <= 0 || slashIndex === route.length - 1) throw new AigenError("INVALID_ARGUMENT", "Model route must use <provider-id>/<provider-local-model>.");
23
+ return {
24
+ providerId: route.slice(0, slashIndex),
25
+ providerLocalModel: route.slice(slashIndex + 1)
26
+ };
27
+ };
28
+ const assertResourceId = (id, label) => {
29
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(id)) throw new AigenError("INVALID_ARGUMENT", `${label} must be a stable resource id.`);
30
+ };
31
+ //#endregion
32
+ //#region src/controllers/doctor.controller.ts
33
+ var DoctorController = class {
34
+ constructor(configRepository, secretsRepository, providerRuntimeManager) {
35
+ this.configRepository = configRepository;
36
+ this.secretsRepository = secretsRepository;
37
+ this.providerRuntimeManager = providerRuntimeManager;
38
+ }
39
+ run = async (options) => {
40
+ const { provider, model } = options;
41
+ const checks = [];
42
+ const config = await this.configRepository.readConfig();
43
+ checks.push({
44
+ name: "config",
45
+ ok: true
46
+ });
47
+ if (provider) await this.checkProvider(provider, checks);
48
+ if (model) {
49
+ const route = parseModelRoute(model);
50
+ await this.configRepository.getModel(route.providerId, route.providerLocalModel);
51
+ await this.checkProvider(route.providerId, checks);
52
+ checks.push({
53
+ name: "model",
54
+ ok: true
55
+ });
56
+ }
57
+ return {
58
+ ok: true,
59
+ providerCount: Object.keys(config.providers).length,
60
+ checks
61
+ };
62
+ };
63
+ checkProvider = async (providerId, checks) => {
64
+ const providerConfig = await this.configRepository.getProvider(providerId);
65
+ this.providerRuntimeManager.getImageProvider(providerConfig.apiFormat);
66
+ checks.push({
67
+ name: "provider",
68
+ ok: true
69
+ });
70
+ await this.secretsRepository.getProviderSecret(providerId);
71
+ checks.push({
72
+ name: "secret",
73
+ ok: true
74
+ });
75
+ };
76
+ };
77
+ //#endregion
78
+ //#region src/controllers/image.controller.ts
79
+ var ImageController = class {
80
+ constructor(imageGenerationManager) {
81
+ this.imageGenerationManager = imageGenerationManager;
82
+ }
83
+ generate = async (options) => this.imageGenerationManager.generate(options);
84
+ };
85
+ //#endregion
86
+ //#region src/controllers/models.controller.ts
87
+ var ModelsController = class {
88
+ constructor(configRepository, secretsRepository, providerRuntimeManager) {
89
+ this.configRepository = configRepository;
90
+ this.secretsRepository = secretsRepository;
91
+ this.providerRuntimeManager = providerRuntimeManager;
92
+ }
93
+ list = async (options) => options.remote ? this.listRemote(options) : this.listLocal(options);
94
+ listLocal = async (options) => {
95
+ return {
96
+ ok: true,
97
+ models: (await this.configRepository.listModels(options.provider, options.kind)).map((model) => ({
98
+ route: model.route,
99
+ provider: model.providerId,
100
+ providerLocalModel: model.providerLocalModel,
101
+ kind: model.config.kind,
102
+ displayName: model.config.displayName,
103
+ capabilities: model.config.capabilities
104
+ }))
105
+ };
106
+ };
107
+ listRemote = async (options) => {
108
+ if (!options.provider) throw new AigenError("INVALID_ARGUMENT", "Missing required --provider for remote model listing.");
109
+ const providerId = options.provider;
110
+ const providerConfig = await this.configRepository.getProvider(providerId);
111
+ const provider = this.providerRuntimeManager.getRemoteModelListProvider(providerConfig.apiFormat);
112
+ const apiKey = await this.secretsRepository.getProviderApiKey(providerId);
113
+ const result = await provider.listRemoteModels({ kind: options.kind }, {
114
+ providerId,
115
+ apiFormat: providerConfig.apiFormat,
116
+ apiBase: providerConfig.apiBase,
117
+ apiKey,
118
+ headers: providerConfig.headers
119
+ });
120
+ return {
121
+ ok: true,
122
+ models: result.models.map((model) => ({
123
+ route: `${providerId}/${model.providerLocalModel}`,
124
+ provider: providerId,
125
+ apiFormat: providerConfig.apiFormat,
126
+ providerLocalModel: model.providerLocalModel,
127
+ kind: "image",
128
+ displayName: model.displayName,
129
+ inputModalities: model.inputModalities,
130
+ outputModalities: model.outputModalities,
131
+ pricing: model.pricing,
132
+ metadata: model.metadata
133
+ })),
134
+ metadata: result.metadata
135
+ };
136
+ };
137
+ get = async (modelRoute) => {
138
+ const route = parseModelRoute(modelRoute);
139
+ const modelConfig = await this.configRepository.getModel(route.providerId, route.providerLocalModel);
140
+ return {
141
+ ok: true,
142
+ model: {
143
+ route: modelRoute,
144
+ provider: route.providerId,
145
+ providerLocalModel: route.providerLocalModel,
146
+ kind: modelConfig.kind,
147
+ displayName: modelConfig.displayName,
148
+ capabilities: modelConfig.capabilities
149
+ }
150
+ };
151
+ };
152
+ add = async (modelRoute, options) => {
153
+ const route = parseModelRoute(modelRoute);
154
+ const modelConfig = this.fullModelConfigFromOptions(options);
155
+ await this.configRepository.addModel(route.providerId, route.providerLocalModel, modelConfig);
156
+ return {
157
+ ok: true,
158
+ model: {
159
+ route: modelRoute,
160
+ provider: route.providerId,
161
+ providerLocalModel: route.providerLocalModel,
162
+ ...modelConfig
163
+ }
164
+ };
165
+ };
166
+ update = async (modelRoute, options) => {
167
+ const route = parseModelRoute(modelRoute);
168
+ const modelConfig = await this.configRepository.updateModel(route.providerId, route.providerLocalModel, this.modelConfigFromOptions(options));
169
+ return {
170
+ ok: true,
171
+ model: {
172
+ route: modelRoute,
173
+ provider: route.providerId,
174
+ providerLocalModel: route.providerLocalModel,
175
+ ...modelConfig
176
+ }
177
+ };
178
+ };
179
+ remove = async (modelRoute) => {
180
+ const route = parseModelRoute(modelRoute);
181
+ await this.configRepository.removeModel(route.providerId, route.providerLocalModel);
182
+ return {
183
+ ok: true,
184
+ removed: true,
185
+ model: modelRoute
186
+ };
187
+ };
188
+ fullModelConfigFromOptions = (options) => {
189
+ return {
190
+ kind: options.kind ?? "image",
191
+ displayName: options.displayName,
192
+ capabilities: this.capabilitiesFromOptions(options)
193
+ };
194
+ };
195
+ modelConfigFromOptions = (options) => {
196
+ return {
197
+ kind: options.kind,
198
+ displayName: options.displayName,
199
+ capabilities: this.capabilitiesFromOptions(options)
200
+ };
201
+ };
202
+ capabilitiesFromOptions = (options) => {
203
+ const maxCount = options.maxCount;
204
+ const generate = options.generate ?? false;
205
+ const edit = options.edit ?? false;
206
+ if (maxCount === void 0 && !generate && !edit) return;
207
+ return {
208
+ generate: generate || void 0,
209
+ edit: edit || void 0,
210
+ maxCount
211
+ };
212
+ };
213
+ };
214
+ //#endregion
215
+ //#region src/controllers/providers.controller.ts
216
+ var ProvidersController = class {
217
+ constructor(configRepository) {
218
+ this.configRepository = configRepository;
219
+ }
220
+ list = async () => {
221
+ return {
222
+ ok: true,
223
+ providers: (await this.configRepository.listProviders()).map(({ id, config }) => this.toProviderOutput(id, config))
224
+ };
225
+ };
226
+ get = async (providerId) => ({
227
+ ok: true,
228
+ provider: this.toProviderOutput(providerId, await this.configRepository.getProvider(providerId))
229
+ });
230
+ add = async (providerId, options) => {
231
+ const { apiFormat, displayName, apiBase } = options;
232
+ const providerConfig = {
233
+ apiFormat,
234
+ displayName,
235
+ apiKeyRef: `provider:${providerId}`,
236
+ apiBase: apiBase ?? this.defaultApiBase(apiFormat),
237
+ models: {}
238
+ };
239
+ await this.configRepository.addProvider(providerId, providerConfig);
240
+ return {
241
+ ok: true,
242
+ provider: this.toProviderOutput(providerId, providerConfig)
243
+ };
244
+ };
245
+ update = async (providerId, options) => {
246
+ const { apiFormat, displayName, apiBase } = options;
247
+ const providerConfig = await this.configRepository.updateProvider(providerId, {
248
+ apiFormat,
249
+ displayName,
250
+ apiBase
251
+ });
252
+ return {
253
+ ok: true,
254
+ provider: this.toProviderOutput(providerId, providerConfig)
255
+ };
256
+ };
257
+ remove = async (providerId) => {
258
+ await this.configRepository.removeProvider(providerId);
259
+ return {
260
+ ok: true,
261
+ removed: true,
262
+ providerId
263
+ };
264
+ };
265
+ toProviderOutput = (providerId, config) => ({
266
+ id: providerId,
267
+ apiFormat: config.apiFormat,
268
+ displayName: config.displayName,
269
+ apiBase: config.apiBase,
270
+ apiKeyRef: config.apiKeyRef,
271
+ modelCount: Object.keys(config.models).length
272
+ });
273
+ defaultApiBase = (apiFormat) => {
274
+ if (apiFormat === "openrouter") return "https://openrouter.ai/api/v1";
275
+ if (apiFormat === "openai") return "https://api.openai.com/v1";
276
+ return "";
277
+ };
278
+ };
279
+ //#endregion
280
+ //#region src/controllers/secrets.controller.ts
281
+ var SecretsController = class {
282
+ constructor(secretsRepository) {
283
+ this.secretsRepository = secretsRepository;
284
+ }
285
+ list = async () => ({
286
+ ok: true,
287
+ secrets: await this.secretsRepository.listSecrets()
288
+ });
289
+ get = async (providerId) => ({
290
+ ok: true,
291
+ secret: await this.secretsRepository.getProviderSecret(providerId)
292
+ });
293
+ set = async (providerId) => {
294
+ return {
295
+ ok: true,
296
+ secret: await this.secretsRepository.setProviderApiKey(providerId, await this.readStdin())
297
+ };
298
+ };
299
+ remove = async (providerId) => {
300
+ await this.secretsRepository.removeProviderSecret(providerId);
301
+ return {
302
+ ok: true,
303
+ removed: true,
304
+ providerId
305
+ };
306
+ };
307
+ readStdin = async () => new Promise((resolve, reject) => {
308
+ let value = "";
309
+ process.stdin.setEncoding("utf8");
310
+ process.stdin.on("data", (chunk) => {
311
+ value += chunk;
312
+ });
313
+ process.stdin.on("end", () => {
314
+ resolve(value);
315
+ });
316
+ process.stdin.on("error", reject);
317
+ });
318
+ };
319
+ //#endregion
320
+ //#region src/app/register-aigen-commands.ts
321
+ const registerAigenCommands = (program, controllers, sink) => {
322
+ registerImageCommand(program, controllers, sink);
323
+ registerProvidersCommands(program, controllers, sink);
324
+ registerModelsCommands(program, controllers, sink);
325
+ registerSecretsCommands(program, controllers, sink);
326
+ registerDoctorCommand(program, controllers, sink);
327
+ };
328
+ const registerImageCommand = (program, controllers, sink) => {
329
+ withOutputOptions(program.command("image").description("Generate an image").requiredOption("--model <route>", "Model route in <provider-id>/<provider-local-model> format").requiredOption("--prompt <text>", "Generation prompt").option("--size <size>", "Image size").option("--n <count>", "Number of images to generate", parseNumberOption, 1).option("--quality <quality>", "Image quality").option("--background <background>", "Background mode").option("--output-format <format>", "Output format").option("--output-compression <value>", "Output compression", parseNumberOption).option("--moderation <mode>", "Moderation mode").requiredOption("--output-dir <dir>", "Directory for generated assets").requiredOption("--output-name <name>", "Base output file name")).action(async (options) => {
330
+ sink(await controllers.imageController.generate(options));
331
+ });
332
+ };
333
+ const registerProvidersCommands = (program, controllers, sink) => {
334
+ const providers = program.command("providers").description("Manage image generation providers");
335
+ withOutputOptions(providers.command("list").description("List configured providers")).action(async () => {
336
+ sink(await controllers.providersController.list());
337
+ });
338
+ withOutputOptions(providers.command("get <providerId>").description("Get a configured provider")).action(async (providerId) => {
339
+ sink(await controllers.providersController.get(providerId));
340
+ });
341
+ withOutputOptions(providers.command("add <providerId>").description("Add a provider").requiredOption("--api-format <format>", "Provider API format, for example openrouter or openai").option("--display-name <name>", "Provider display name").option("--api-base <url>", "Provider API base URL")).action(async (providerId, options) => {
342
+ sink(await controllers.providersController.add(providerId, options));
343
+ });
344
+ withOutputOptions(providers.command("update <providerId>").description("Update a provider").option("--api-format <format>", "Provider API format").option("--display-name <name>", "Provider display name").option("--api-base <url>", "Provider API base URL")).action(async (providerId, options) => {
345
+ sink(await controllers.providersController.update(providerId, options));
346
+ });
347
+ withOutputOptions(providers.command("remove <providerId>").description("Remove a provider")).action(async (providerId) => {
348
+ sink(await controllers.providersController.remove(providerId));
349
+ });
350
+ };
351
+ const registerModelsCommands = (program, controllers, sink) => {
352
+ const models = program.command("models").description("Manage provider models");
353
+ withOutputOptions(models.command("list").description("List configured models, or remote provider models with --remote").option("--remote", "List remote models from a provider", false).option("--provider <providerId>", "Provider id").option("--kind <kind>", "Model kind")).action(async (options) => {
354
+ sink(await controllers.modelsController.list(options));
355
+ });
356
+ withOutputOptions(models.command("get <modelRoute>").description("Get a configured model")).action(async (modelRoute) => {
357
+ sink(await controllers.modelsController.get(modelRoute));
358
+ });
359
+ withOutputOptions(models.command("add <modelRoute>").description("Add a model under a provider").option("--kind <kind>", "Model kind", "image").option("--display-name <name>", "Model display name").option("--generate", "Mark model as generation-capable", false).option("--edit", "Mark model as edit-capable", false).option("--max-count <count>", "Maximum generated asset count", parseNumberOption)).action(async (modelRoute, options) => {
360
+ sink(await controllers.modelsController.add(modelRoute, options));
361
+ });
362
+ withOutputOptions(models.command("update <modelRoute>").description("Update a model").option("--kind <kind>", "Model kind").option("--display-name <name>", "Model display name").option("--generate", "Mark model as generation-capable").option("--edit", "Mark model as edit-capable").option("--max-count <count>", "Maximum generated asset count", parseNumberOption)).action(async (modelRoute, options) => {
363
+ sink(await controllers.modelsController.update(modelRoute, options));
364
+ });
365
+ withOutputOptions(models.command("remove <modelRoute>").description("Remove a model")).action(async (modelRoute) => {
366
+ sink(await controllers.modelsController.remove(modelRoute));
367
+ });
368
+ };
369
+ const registerSecretsCommands = (program, controllers, sink) => {
370
+ const secrets = program.command("secrets").description("Manage provider API keys");
371
+ withOutputOptions(secrets.command("list").description("List masked provider secrets")).action(async () => {
372
+ sink(await controllers.secretsController.list());
373
+ });
374
+ withOutputOptions(secrets.command("get <providerId>").description("Get masked provider secret metadata")).action(async (providerId) => {
375
+ sink(await controllers.secretsController.get(providerId));
376
+ });
377
+ withOutputOptions(secrets.command("set <providerId>").description("Set provider API key from stdin").option("--stdin", "Read provider API key from stdin", false)).action(async (providerId) => {
378
+ sink(await controllers.secretsController.set(providerId));
379
+ });
380
+ withOutputOptions(secrets.command("remove <providerId>").description("Remove provider API key")).action(async (providerId) => {
381
+ sink(await controllers.secretsController.remove(providerId));
382
+ });
383
+ };
384
+ const registerDoctorCommand = (program, controllers, sink) => {
385
+ withOutputOptions(program.command("doctor").description("Run local configuration diagnostics").option("--provider <providerId>", "Provider id to check").option("--model <modelRoute>", "Model route to check")).action(async (options) => {
386
+ sink(await controllers.doctorController.run(options));
387
+ });
388
+ };
389
+ const withOutputOptions = (command) => command.option("--json", "Output JSON", false).option("--debug", "Print debug diagnostics to stderr", false);
390
+ const parseNumberOption = (value) => {
391
+ const parsed = Number(value);
392
+ if (!Number.isFinite(parsed)) throw new InvalidArgumentError("must be a number");
393
+ return parsed;
394
+ };
395
+ //#endregion
396
+ //#region src/managers/image-generation.manager.ts
397
+ var ImageGenerationManager = class {
398
+ constructor(configRepository, secretsRepository, providerRuntimeManager, outputFileManager) {
399
+ this.configRepository = configRepository;
400
+ this.secretsRepository = secretsRepository;
401
+ this.providerRuntimeManager = providerRuntimeManager;
402
+ this.outputFileManager = outputFileManager;
403
+ }
404
+ generate = async (input) => {
405
+ const route = parseModelRoute(input.model);
406
+ const providerConfig = await this.configRepository.getProvider(route.providerId);
407
+ await this.configRepository.getModel(route.providerId, route.providerLocalModel);
408
+ const provider = this.providerRuntimeManager.getImageProvider(providerConfig.apiFormat);
409
+ const apiKey = await this.secretsRepository.getProviderApiKey(route.providerId);
410
+ const result = await provider.generateImage({
411
+ providerLocalModel: route.providerLocalModel,
412
+ prompt: input.prompt,
413
+ size: input.size,
414
+ n: input.n ?? 1,
415
+ quality: input.quality,
416
+ background: input.background,
417
+ outputFormat: input.outputFormat,
418
+ outputCompression: input.outputCompression,
419
+ moderation: input.moderation
420
+ }, {
421
+ providerId: route.providerId,
422
+ apiFormat: providerConfig.apiFormat,
423
+ apiBase: providerConfig.apiBase,
424
+ apiKey,
425
+ headers: providerConfig.headers
426
+ });
427
+ const assets = await this.outputFileManager.writeImages(result.images, input.outputDir, input.outputName);
428
+ return {
429
+ ok: true,
430
+ kind: "image",
431
+ provider: route.providerId,
432
+ apiFormat: providerConfig.apiFormat,
433
+ model: input.model,
434
+ providerLocalModel: route.providerLocalModel,
435
+ assets,
436
+ usage: result.usage,
437
+ metadata: {
438
+ ...result.metadata,
439
+ upstreamRequestId: result.upstreamRequestId
440
+ }
441
+ };
442
+ };
443
+ };
444
+ //#endregion
445
+ //#region src/utils/mime.utils.ts
446
+ const mimeTypeToExtension = (mimeType) => {
447
+ switch (mimeType.toLowerCase()) {
448
+ case "image/jpeg":
449
+ case "image/jpg": return "jpg";
450
+ case "image/png": return "png";
451
+ case "image/webp": return "webp";
452
+ case "image/gif": return "gif";
453
+ default: return "bin";
454
+ }
455
+ };
456
+ const extensionToMimeType = (extension) => {
457
+ switch (extension.toLowerCase()) {
458
+ case "jpg":
459
+ case "jpeg": return "image/jpeg";
460
+ case "png": return "image/png";
461
+ case "webp": return "image/webp";
462
+ case "gif": return "image/gif";
463
+ default: return "application/octet-stream";
464
+ }
465
+ };
466
+ //#endregion
467
+ //#region src/managers/output-file.manager.ts
468
+ var OutputFileManager = class {
469
+ writeImages = async (images, outputDir, outputName) => {
470
+ this.assertOutputName(outputName);
471
+ await mkdir(outputDir, { recursive: true });
472
+ const resolvedOutputDir = resolve(outputDir);
473
+ const assets = [];
474
+ for (let index = 0; index < images.length; index += 1) {
475
+ const image = images[index];
476
+ const extension = image.format ?? mimeTypeToExtension(image.mimeType);
477
+ const filename = this.createFilename(outputName, extension, images.length, index);
478
+ const path = resolve(resolvedOutputDir, filename);
479
+ this.assertContainedPath(resolvedOutputDir, path);
480
+ await writeFile(path, image.bytes, { flag: "wx" });
481
+ const fileStat = await stat(path);
482
+ assets.push({
483
+ path,
484
+ filename,
485
+ mimeType: image.mimeType,
486
+ format: extension,
487
+ width: image.width,
488
+ height: image.height,
489
+ sizeBytes: fileStat.size
490
+ });
491
+ }
492
+ return assets;
493
+ };
494
+ assertOutputName = (outputName) => {
495
+ if (!outputName || outputName !== basename(outputName) || outputName.includes("/") || outputName.includes("\\")) throw new AigenError("INVALID_ARGUMENT", "output-name must be a basename without path separators.");
496
+ };
497
+ assertContainedPath = (outputDir, path) => {
498
+ if (path !== outputDir && !path.startsWith(`${outputDir}/`)) throw new AigenError("FILE_WRITE_FAILED", "Generated file path escaped output-dir.");
499
+ };
500
+ createFilename = (outputName, extension, count, index) => {
501
+ return `${outputName}${count === 1 ? "" : `-${index + 1}`}.${extension}`;
502
+ };
503
+ };
504
+ //#endregion
505
+ //#region src/types/provider.types.ts
506
+ const isRemoteModelListProvider = (provider) => "listRemoteModels" in provider && typeof provider.listRemoteModels === "function";
507
+ //#endregion
508
+ //#region src/managers/provider-runtime.manager.ts
509
+ var ProviderRuntimeManager = class {
510
+ providers = /* @__PURE__ */ new Map();
511
+ constructor(providers) {
512
+ providers.forEach((provider) => {
513
+ this.providers.set(provider.apiFormat, provider);
514
+ });
515
+ }
516
+ getImageProvider = (apiFormat) => {
517
+ const provider = this.providers.get(apiFormat);
518
+ if (!provider) throw new AigenError("PROVIDER_RUNTIME_NOT_FOUND", `API format '${apiFormat}' is not supported.`);
519
+ return provider;
520
+ };
521
+ getRemoteModelListProvider = (apiFormat) => {
522
+ const provider = this.getImageProvider(apiFormat);
523
+ if (!isRemoteModelListProvider(provider)) throw new AigenError("UNSUPPORTED_PARAMETER", `API format '${apiFormat}' does not support remote model list.`);
524
+ return provider;
525
+ };
526
+ };
527
+ //#endregion
528
+ //#region src/utils/data-url.utils.ts
529
+ const parseImageDataUrl = (value) => {
530
+ const match = /^data:([^;,]+);base64,(.+)$/s.exec(value);
531
+ if (!match) throw new AigenError("PROVIDER_REQUEST_FAILED", "Provider returned an invalid image data URL.");
532
+ return {
533
+ mimeType: match[1] ?? "application/octet-stream",
534
+ bytes: Uint8Array.from(Buffer.from(match[2] ?? "", "base64"))
535
+ };
536
+ };
537
+ const isDataUrl = (value) => value.startsWith("data:");
538
+ //#endregion
539
+ //#region src/providers/openrouter.provider.ts
540
+ var OpenRouterProvider = class {
541
+ apiFormat = "openrouter";
542
+ generateImage = async (request, context) => {
543
+ const response = await fetch(this.url(context.apiBase, "/chat/completions"), {
544
+ method: "POST",
545
+ headers: this.headers(context),
546
+ body: JSON.stringify(this.toChatCompletionsBody(request))
547
+ });
548
+ const payload = await this.readJson(response);
549
+ if (!response.ok) throw new AigenError("PROVIDER_REQUEST_FAILED", "OpenRouter image generation request failed.", { retryable: response.status >= 500 });
550
+ return {
551
+ images: await this.extractImages(payload),
552
+ usage: this.recordOrUndefined(payload.usage),
553
+ upstreamRequestId: this.stringOrUndefined(payload.id),
554
+ metadata: { model: this.stringOrUndefined(payload.model) }
555
+ };
556
+ };
557
+ listRemoteModels = async (_request, context) => {
558
+ const response = await fetch(this.url(context.apiBase, "/models?output_modalities=image"), {
559
+ method: "GET",
560
+ headers: this.headers(context)
561
+ });
562
+ const payload = await this.readJson(response);
563
+ if (!response.ok) throw new AigenError("PROVIDER_REQUEST_FAILED", "OpenRouter models request failed.", { retryable: response.status >= 500 });
564
+ return { models: (Array.isArray(payload.data) ? payload.data : []).flatMap((entry) => {
565
+ const model = this.recordOrUndefined(entry);
566
+ const id = this.stringOrUndefined(model?.id);
567
+ if (!model || !id) return [];
568
+ return [{
569
+ providerLocalModel: id,
570
+ displayName: this.stringOrUndefined(model.name),
571
+ inputModalities: this.stringArrayOrUndefined(this.recordOrUndefined(model.architecture)?.input_modalities),
572
+ outputModalities: this.stringArrayOrUndefined(this.recordOrUndefined(model.architecture)?.output_modalities),
573
+ pricing: this.recordOrUndefined(model.pricing),
574
+ metadata: model
575
+ }];
576
+ }) };
577
+ };
578
+ toChatCompletionsBody = (request) => {
579
+ const body = {
580
+ model: request.providerLocalModel,
581
+ messages: [{
582
+ role: "user",
583
+ content: request.prompt
584
+ }],
585
+ modalities: ["image"],
586
+ stream: false
587
+ };
588
+ if (request.size) body.image_config = { image_size: request.size };
589
+ return body;
590
+ };
591
+ extractImages = async (payload) => {
592
+ const urls = (Array.isArray(payload.choices) ? payload.choices : []).flatMap((choice) => this.extractImageUrls(choice));
593
+ if (urls.length === 0) throw new AigenError("PROVIDER_REQUEST_FAILED", "OpenRouter response did not include generated images.");
594
+ return Promise.all(urls.map((url) => this.loadImageUrl(url)));
595
+ };
596
+ extractImageUrls = (choice) => {
597
+ const message = this.recordOrUndefined(this.recordOrUndefined(choice)?.message);
598
+ return (Array.isArray(message?.images) ? message.images : []).flatMap((image) => {
599
+ const url = this.stringOrUndefined(this.recordOrUndefined(this.recordOrUndefined(image)?.image_url)?.url);
600
+ return url ? [url] : [];
601
+ });
602
+ };
603
+ loadImageUrl = async (url) => {
604
+ if (isDataUrl(url)) {
605
+ const dataUrl = parseImageDataUrl(url);
606
+ return {
607
+ bytes: dataUrl.bytes,
608
+ mimeType: dataUrl.mimeType
609
+ };
610
+ }
611
+ const response = await fetch(url);
612
+ if (!response.ok) throw new AigenError("PROVIDER_REQUEST_FAILED", "Failed to download generated image.", { retryable: response.status >= 500 });
613
+ const contentType = response.headers.get("content-type") ?? this.mimeTypeFromUrl(url);
614
+ return {
615
+ bytes: new Uint8Array(await response.arrayBuffer()),
616
+ mimeType: contentType
617
+ };
618
+ };
619
+ readJson = async (response) => {
620
+ const value = await response.json();
621
+ const record = this.recordOrUndefined(value);
622
+ if (!record) throw new AigenError("PROVIDER_REQUEST_FAILED", "Provider returned a non-object JSON response.");
623
+ return record;
624
+ };
625
+ headers = (context) => ({
626
+ authorization: `Bearer ${context.apiKey}`,
627
+ "content-type": "application/json",
628
+ ...context.headers
629
+ });
630
+ url = (apiBase, path) => `${apiBase.replace(/\/$/, "")}${path}`;
631
+ recordOrUndefined = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
632
+ stringOrUndefined = (value) => typeof value === "string" ? value : void 0;
633
+ stringArrayOrUndefined = (value) => Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
634
+ mimeTypeFromUrl = (url) => {
635
+ const extension = url.split("?")[0]?.split(".").pop();
636
+ return extension ? extensionToMimeType(extension) : "application/octet-stream";
637
+ };
638
+ };
639
+ //#endregion
640
+ //#region src/types/config.types.ts
641
+ const createEmptyAigenConfig = () => ({
642
+ version: 1,
643
+ providers: {}
644
+ });
645
+ //#endregion
646
+ //#region src/repositories/config.repository.ts
647
+ var ConfigRepository = class {
648
+ homeDir;
649
+ configPath;
650
+ constructor(homeDir = process.env.AIGEN_HOME ?? join(homedir(), ".aigen")) {
651
+ this.homeDir = homeDir;
652
+ this.configPath = join(homeDir, "config.json");
653
+ }
654
+ readConfig = async () => {
655
+ try {
656
+ const text = await readFile(this.configPath, "utf8");
657
+ return this.parseConfig(text);
658
+ } catch (error) {
659
+ if (this.isNotFoundError(error)) throw new AigenError("CONFIG_NOT_FOUND", "aigen config.json does not exist.");
660
+ throw error;
661
+ }
662
+ };
663
+ readConfigOrCreate = async () => {
664
+ try {
665
+ return await this.readConfig();
666
+ } catch (error) {
667
+ if (error instanceof AigenError && error.code === "CONFIG_NOT_FOUND") return createEmptyAigenConfig();
668
+ throw error;
669
+ }
670
+ };
671
+ writeConfig = async (config) => {
672
+ await mkdir(this.homeDir, { recursive: true });
673
+ await writeFile(this.configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
674
+ };
675
+ listProviders = async () => {
676
+ const config = await this.readConfig();
677
+ return Object.entries(config.providers).map(([id, providerConfig]) => ({
678
+ id,
679
+ config: providerConfig
680
+ }));
681
+ };
682
+ getProvider = async (providerId) => {
683
+ const providerConfig = (await this.readConfig()).providers[providerId];
684
+ if (!providerConfig) throw new AigenError("PROVIDER_NOT_FOUND", `Provider '${providerId}' does not exist.`);
685
+ return providerConfig;
686
+ };
687
+ addProvider = async (providerId, providerConfig) => {
688
+ assertResourceId(providerId, "Provider id");
689
+ const config = await this.readConfigOrCreate();
690
+ if (config.providers[providerId]) throw new AigenError("INVALID_ARGUMENT", `Provider '${providerId}' already exists.`);
691
+ config.providers[providerId] = providerConfig;
692
+ await this.writeConfig(config);
693
+ };
694
+ updateProvider = async (providerId, patch) => {
695
+ const config = await this.readConfig();
696
+ const current = config.providers[providerId];
697
+ if (!current) throw new AigenError("PROVIDER_NOT_FOUND", `Provider '${providerId}' does not exist.`);
698
+ const next = {
699
+ ...current,
700
+ apiFormat: patch.apiFormat ?? current.apiFormat,
701
+ displayName: patch.displayName ?? current.displayName,
702
+ apiBase: patch.apiBase ?? current.apiBase,
703
+ apiKeyRef: patch.apiKeyRef ?? current.apiKeyRef,
704
+ headers: patch.headers ?? current.headers
705
+ };
706
+ config.providers[providerId] = next;
707
+ await this.writeConfig(config);
708
+ return next;
709
+ };
710
+ removeProvider = async (providerId) => {
711
+ const config = await this.readConfig();
712
+ if (!config.providers[providerId]) throw new AigenError("PROVIDER_NOT_FOUND", `Provider '${providerId}' does not exist.`);
713
+ delete config.providers[providerId];
714
+ await this.writeConfig(config);
715
+ };
716
+ listModels = async (providerId, kind) => {
717
+ const config = await this.readConfig();
718
+ return (providerId ? [[providerId, await this.getProvider(providerId)]] : Object.entries(config.providers)).flatMap(([id, providerConfig]) => Object.entries(providerConfig.models).filter(([, modelConfig]) => !kind || modelConfig.kind === kind).map(([providerLocalModel, modelConfig]) => ({
719
+ route: `${id}/${providerLocalModel}`,
720
+ providerId: id,
721
+ providerLocalModel,
722
+ config: modelConfig
723
+ })));
724
+ };
725
+ getModel = async (providerId, providerLocalModel) => {
726
+ const modelConfig = (await this.getProvider(providerId)).models[providerLocalModel];
727
+ if (!modelConfig) throw new AigenError("MODEL_NOT_FOUND", `Model '${providerId}/${providerLocalModel}' does not exist.`);
728
+ return modelConfig;
729
+ };
730
+ addModel = async (providerId, providerLocalModel, modelConfig) => {
731
+ const config = await this.readConfig();
732
+ const providerConfig = config.providers[providerId];
733
+ if (!providerConfig) throw new AigenError("PROVIDER_NOT_FOUND", `Provider '${providerId}' does not exist.`);
734
+ if (providerConfig.models[providerLocalModel]) throw new AigenError("INVALID_ARGUMENT", `Model '${providerId}/${providerLocalModel}' already exists.`);
735
+ providerConfig.models[providerLocalModel] = modelConfig;
736
+ await this.writeConfig(config);
737
+ };
738
+ updateModel = async (providerId, providerLocalModel, patch) => {
739
+ const config = await this.readConfig();
740
+ const providerConfig = config.providers[providerId];
741
+ if (!providerConfig) throw new AigenError("PROVIDER_NOT_FOUND", `Provider '${providerId}' does not exist.`);
742
+ const current = providerConfig.models[providerLocalModel];
743
+ if (!current) throw new AigenError("MODEL_NOT_FOUND", `Model '${providerId}/${providerLocalModel}' does not exist.`);
744
+ const next = {
745
+ ...current,
746
+ kind: patch.kind ?? current.kind,
747
+ displayName: patch.displayName ?? current.displayName,
748
+ capabilities: patch.capabilities ?? current.capabilities
749
+ };
750
+ providerConfig.models[providerLocalModel] = next;
751
+ await this.writeConfig(config);
752
+ return next;
753
+ };
754
+ removeModel = async (providerId, providerLocalModel) => {
755
+ const config = await this.readConfig();
756
+ const providerConfig = config.providers[providerId];
757
+ if (!providerConfig) throw new AigenError("PROVIDER_NOT_FOUND", `Provider '${providerId}' does not exist.`);
758
+ if (!providerConfig.models[providerLocalModel]) throw new AigenError("MODEL_NOT_FOUND", `Model '${providerId}/${providerLocalModel}' does not exist.`);
759
+ delete providerConfig.models[providerLocalModel];
760
+ await this.writeConfig(config);
761
+ };
762
+ clear = async () => {
763
+ await rm(this.configPath, { force: true });
764
+ };
765
+ parseConfig = (text) => {
766
+ const value = JSON.parse(text);
767
+ if (value.version !== 1 || !value.providers || typeof value.providers !== "object") throw new AigenError("CONFIG_INVALID", "aigen config.json is invalid.");
768
+ return {
769
+ version: 1,
770
+ providers: value.providers
771
+ };
772
+ };
773
+ isNotFoundError = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
774
+ };
775
+ //#endregion
776
+ //#region src/repositories/secrets.repository.ts
777
+ var SecretsRepository = class {
778
+ homeDir;
779
+ secretsPath;
780
+ constructor(homeDir = process.env.AIGEN_HOME ?? join(homedir(), ".aigen")) {
781
+ this.homeDir = homeDir;
782
+ this.secretsPath = join(homeDir, "secrets.json");
783
+ }
784
+ setProviderApiKey = async (providerId, value) => {
785
+ const trimmedValue = value.trim();
786
+ if (!trimmedValue) throw new AigenError("INVALID_ARGUMENT", "API key cannot be empty.");
787
+ const secretsFile = await this.readSecretsOrCreate();
788
+ const ref = this.providerRef(providerId);
789
+ secretsFile.secrets[ref] = {
790
+ kind: "apiKey",
791
+ value: trimmedValue,
792
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
793
+ };
794
+ await this.writeSecrets(secretsFile);
795
+ return this.toMetadata(ref, secretsFile.secrets[ref]);
796
+ };
797
+ getProviderApiKey = async (providerId) => {
798
+ const ref = this.providerRef(providerId);
799
+ return (await this.getSecretEntry(ref)).value;
800
+ };
801
+ getProviderSecret = async (providerId) => {
802
+ const ref = this.providerRef(providerId);
803
+ const entry = await this.getSecretEntry(ref);
804
+ return this.toMetadata(ref, entry);
805
+ };
806
+ listSecrets = async () => {
807
+ const secretsFile = await this.readSecretsOrCreate();
808
+ return Object.entries(secretsFile.secrets).map(([ref, entry]) => this.toMetadata(ref, entry));
809
+ };
810
+ removeProviderSecret = async (providerId) => {
811
+ const secretsFile = await this.readSecretsOrCreate();
812
+ const ref = this.providerRef(providerId);
813
+ if (!secretsFile.secrets[ref]) throw new AigenError("SECRET_NOT_FOUND", `Secret '${ref}' does not exist.`);
814
+ delete secretsFile.secrets[ref];
815
+ await this.writeSecrets(secretsFile);
816
+ };
817
+ clear = async () => {
818
+ await rm(this.secretsPath, { force: true });
819
+ };
820
+ getSecretEntry = async (ref) => {
821
+ const entry = (await this.readSecretsOrCreate()).secrets[ref];
822
+ if (!entry) throw new AigenError("MISSING_API_KEY", `Secret '${ref}' does not exist.`);
823
+ return entry;
824
+ };
825
+ readSecretsOrCreate = async () => {
826
+ try {
827
+ const text = await readFile(this.secretsPath, "utf8");
828
+ const value = JSON.parse(text);
829
+ if (value.version !== 1 || !value.secrets || typeof value.secrets !== "object") throw new AigenError("CONFIG_INVALID", "aigen secrets.json is invalid.");
830
+ return {
831
+ version: 1,
832
+ secrets: value.secrets
833
+ };
834
+ } catch (error) {
835
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return {
836
+ version: 1,
837
+ secrets: {}
838
+ };
839
+ throw error;
840
+ }
841
+ };
842
+ writeSecrets = async (secretsFile) => {
843
+ await mkdir(this.homeDir, { recursive: true });
844
+ await writeFile(this.secretsPath, `${JSON.stringify(secretsFile, null, 2)}\n`, { mode: 384 });
845
+ await chmod(this.secretsPath, 384);
846
+ };
847
+ providerRef = (providerId) => `provider:${providerId}`;
848
+ toMetadata = (ref, entry) => ({
849
+ ref,
850
+ kind: entry.kind,
851
+ exists: true,
852
+ maskedValue: this.maskValue(entry.value),
853
+ fingerprint: this.fingerprint(entry.value),
854
+ updatedAt: entry.updatedAt
855
+ });
856
+ maskValue = (value) => {
857
+ if (value.length <= 8) return "********";
858
+ return `${value.slice(0, 3)}...${value.slice(-4)}`;
859
+ };
860
+ fingerprint = (value) => `sha256:${createHash("sha256").update(value).digest("hex").slice(0, 12)}`;
861
+ };
862
+ //#endregion
863
+ //#region src/utils/error.utils.ts
864
+ const toCommandFailure = (error) => {
865
+ if (error instanceof AigenError) return {
866
+ ok: false,
867
+ error: {
868
+ code: error.code,
869
+ message: error.message,
870
+ retryable: error.retryable
871
+ }
872
+ };
873
+ return {
874
+ ok: false,
875
+ error: {
876
+ code: "PROVIDER_REQUEST_FAILED",
877
+ message: error instanceof Error ? error.message : "Unknown aigen failure.",
878
+ retryable: false
879
+ }
880
+ };
881
+ };
882
+ //#endregion
883
+ //#region src/app/aigen-app.ts
884
+ var AigenApp = class {
885
+ constructor(imageController, providersController, modelsController, secretsController, doctorController) {
886
+ this.imageController = imageController;
887
+ this.providersController = providersController;
888
+ this.modelsController = modelsController;
889
+ this.secretsController = secretsController;
890
+ this.doctorController = doctorController;
891
+ }
892
+ run = async (argv) => {
893
+ let commandOutput;
894
+ let commanderOut = "";
895
+ let commanderErr = "";
896
+ const program = this.createProgram((output) => {
897
+ commandOutput = output;
898
+ });
899
+ program.configureOutput({
900
+ writeOut: (value) => {
901
+ commanderOut += value;
902
+ },
903
+ writeErr: (value) => {
904
+ commanderErr += value;
905
+ }
906
+ });
907
+ try {
908
+ if (argv.length === 0) throw new AigenError("INVALID_ARGUMENT", "Missing command.");
909
+ await program.parseAsync(argv, { from: "user" });
910
+ return commandOutput ?? {
911
+ ok: true,
912
+ output: commanderOut.trim()
913
+ };
914
+ } catch (error) {
915
+ if (error instanceof CommanderError) return this.commanderFailure(error, commanderOut, commanderErr);
916
+ return toCommandFailure(error);
917
+ }
918
+ };
919
+ createProgram = (sink) => {
920
+ const program = new Command();
921
+ program.name("aigen").description("Stateless AI media generation CLI.").version("0.0.0", "-v, --version");
922
+ program.exitOverride();
923
+ program.showHelpAfterError();
924
+ registerAigenCommands(program, {
925
+ imageController: this.imageController,
926
+ providersController: this.providersController,
927
+ modelsController: this.modelsController,
928
+ secretsController: this.secretsController,
929
+ doctorController: this.doctorController
930
+ }, sink);
931
+ return program;
932
+ };
933
+ commanderFailure = (error, capturedOutput, capturedError) => {
934
+ const output = capturedOutput.trim();
935
+ if (error.exitCode === 0) return {
936
+ ok: true,
937
+ output
938
+ };
939
+ return {
940
+ ok: false,
941
+ error: {
942
+ code: "INVALID_ARGUMENT",
943
+ message: capturedError.trim() || error.message,
944
+ retryable: false
945
+ }
946
+ };
947
+ };
948
+ };
949
+ const createAigenApp = (homeDir) => {
950
+ const configRepository = new ConfigRepository(homeDir);
951
+ const secretsRepository = new SecretsRepository(homeDir);
952
+ const providerRuntimeManager = new ProviderRuntimeManager([new OpenRouterProvider()]);
953
+ return new AigenApp(new ImageController(new ImageGenerationManager(configRepository, secretsRepository, providerRuntimeManager, new OutputFileManager())), new ProvidersController(configRepository), new ModelsController(configRepository, secretsRepository, providerRuntimeManager), new SecretsController(secretsRepository), new DoctorController(configRepository, secretsRepository, providerRuntimeManager));
954
+ };
955
+ //#endregion
956
+ //#region src/app/main.ts
957
+ const output = await createAigenApp().run(process.argv.slice(2));
958
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
959
+ process.exitCode = output.ok ? 0 : 1;
960
+ //#endregion
961
+ export {};
962
+
963
+ //# sourceMappingURL=main.js.map