@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.
- package/LICENSE +21 -0
- package/dist/app/main.d.ts +1 -0
- package/dist/app/main.js +963 -0
- package/dist/app/main.js.map +1 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +36 -0
package/dist/app/main.js
ADDED
|
@@ -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
|