@vertana/cli 0.1.0-dev.1
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 +20 -0
- package/dist/index.cjs +667 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +639 -0
- package/package.json +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2025 Hong Minhee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//#region rolldown:runtime
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let node_process = require("node:process");
|
|
30
|
+
node_process = __toESM(node_process);
|
|
31
|
+
let _logtape_logtape = require("@logtape/logtape");
|
|
32
|
+
let _optique_core_message = require("@optique/core/message");
|
|
33
|
+
let _optique_logtape = require("@optique/logtape");
|
|
34
|
+
let _optique_run = require("@optique/run");
|
|
35
|
+
let _optique_core_constructs = require("@optique/core/constructs");
|
|
36
|
+
let _optique_core_modifiers = require("@optique/core/modifiers");
|
|
37
|
+
let _optique_core_primitives = require("@optique/core/primitives");
|
|
38
|
+
let _optique_core_valueparser = require("@optique/core/valueparser");
|
|
39
|
+
let _optique_run_valueparser = require("@optique/run/valueparser");
|
|
40
|
+
let node_fs = require("node:fs");
|
|
41
|
+
let _napi_rs_keyring = require("@napi-rs/keyring");
|
|
42
|
+
let node_os = require("node:os");
|
|
43
|
+
let node_path = require("node:path");
|
|
44
|
+
let _vertana_facade = require("@vertana/facade");
|
|
45
|
+
let node_stream = require("node:stream");
|
|
46
|
+
|
|
47
|
+
//#region src/glossary.ts
|
|
48
|
+
/**
|
|
49
|
+
* Creates a ValueParser for glossary entries in "TERM=TRANSLATION" format.
|
|
50
|
+
*
|
|
51
|
+
* @returns A ValueParser that parses glossary entry strings.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const parser = glossaryEntry();
|
|
56
|
+
* const result = parser.parse("LLM=Large Language Model");
|
|
57
|
+
* // result.value === { original: "LLM", translated: "Large Language Model" }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
function glossaryEntry() {
|
|
61
|
+
return {
|
|
62
|
+
metavar: "TERM=TRANSLATION",
|
|
63
|
+
parse(input) {
|
|
64
|
+
const index = input.indexOf("=");
|
|
65
|
+
if (index === -1) return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: _optique_core_message.message`Invalid format. Expected ${(0, _optique_core_message.metavar)("TERM")}${(0, _optique_core_message.text)("=")}${(0, _optique_core_message.metavar)("TRANSLATION")}.`
|
|
68
|
+
};
|
|
69
|
+
if (index === 0) return {
|
|
70
|
+
success: false,
|
|
71
|
+
error: _optique_core_message.message`${(0, _optique_core_message.metavar)("TERM")} cannot be empty.`
|
|
72
|
+
};
|
|
73
|
+
const original = input.slice(0, index);
|
|
74
|
+
const translated = input.slice(index + 1);
|
|
75
|
+
if (translated === "") return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: _optique_core_message.message`${(0, _optique_core_message.metavar)("TRANSLATION")} cannot be empty.`
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
value: {
|
|
82
|
+
original,
|
|
83
|
+
translated
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
format(value) {
|
|
88
|
+
return `${value.original}=${value.translated}`;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Loads a glossary from a JSON file.
|
|
94
|
+
*
|
|
95
|
+
* @param filePath The path to the glossary JSON file.
|
|
96
|
+
* @returns The parsed glossary.
|
|
97
|
+
* @throws {SyntaxError} If the file is not valid JSON.
|
|
98
|
+
* @throws {TypeError} If the JSON structure is invalid.
|
|
99
|
+
*/
|
|
100
|
+
function loadGlossaryFile(filePath) {
|
|
101
|
+
const content = (0, node_fs.readFileSync)(filePath, "utf-8");
|
|
102
|
+
const parsed = JSON.parse(content);
|
|
103
|
+
if (!Array.isArray(parsed)) throw new TypeError(`Invalid glossary file: expected an array, got ${typeof parsed}.`);
|
|
104
|
+
const entries = [];
|
|
105
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
106
|
+
const item = parsed[i];
|
|
107
|
+
if (typeof item !== "object" || item === null) throw new TypeError(`Invalid glossary entry at index ${i}: expected an object.`);
|
|
108
|
+
const entry = item;
|
|
109
|
+
if (typeof entry.original !== "string" || entry.original === "") throw new TypeError(`Invalid glossary entry at index ${i}: "original" must be a non-empty string.`);
|
|
110
|
+
if (typeof entry.translated !== "string" || entry.translated === "") throw new TypeError(`Invalid glossary entry at index ${i}: "translated" must be a non-empty string.`);
|
|
111
|
+
const glossaryEntry$1 = {
|
|
112
|
+
original: entry.original,
|
|
113
|
+
translated: entry.translated
|
|
114
|
+
};
|
|
115
|
+
if (typeof entry.context === "string" && entry.context !== "") glossaryEntry$1.context = entry.context;
|
|
116
|
+
entries.push(glossaryEntry$1);
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Merges multiple glossaries into one.
|
|
122
|
+
* Later entries take precedence over earlier ones for the same original term.
|
|
123
|
+
*
|
|
124
|
+
* @param glossaries The glossaries to merge.
|
|
125
|
+
* @returns The merged glossary.
|
|
126
|
+
*/
|
|
127
|
+
function mergeGlossaries(...glossaries) {
|
|
128
|
+
const merged = /* @__PURE__ */ new Map();
|
|
129
|
+
for (const glossary of glossaries) for (const entry of glossary) merged.set(entry.original, entry);
|
|
130
|
+
return Array.from(merged.values());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/config/types.ts
|
|
135
|
+
/**
|
|
136
|
+
* The default configuration.
|
|
137
|
+
*/
|
|
138
|
+
const defaultConfig = {};
|
|
139
|
+
/**
|
|
140
|
+
* List of supported provider names.
|
|
141
|
+
*/
|
|
142
|
+
const providerNames = [
|
|
143
|
+
"openai",
|
|
144
|
+
"anthropic",
|
|
145
|
+
"google"
|
|
146
|
+
];
|
|
147
|
+
/**
|
|
148
|
+
* Environment variable names for API keys by provider.
|
|
149
|
+
*/
|
|
150
|
+
const providerEnvVars = {
|
|
151
|
+
openai: "OPENAI_API_KEY",
|
|
152
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
153
|
+
google: "GOOGLE_GENERATIVE_AI_API_KEY"
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Checks if a string is a valid provider name.
|
|
157
|
+
*
|
|
158
|
+
* @param provider The string to check.
|
|
159
|
+
* @returns True if the string is a valid provider name.
|
|
160
|
+
*/
|
|
161
|
+
function isProviderName(provider) {
|
|
162
|
+
return provider === "openai" || provider === "anthropic" || provider === "google";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/config/keyring.ts
|
|
167
|
+
const SERVICE_NAME = "vertana";
|
|
168
|
+
/**
|
|
169
|
+
* Gets the API key for a provider.
|
|
170
|
+
* First checks the keyring, then falls back to environment variables.
|
|
171
|
+
*
|
|
172
|
+
* @param provider The provider name.
|
|
173
|
+
* @returns The API key, or undefined if not found.
|
|
174
|
+
*/
|
|
175
|
+
function getApiKey(provider) {
|
|
176
|
+
try {
|
|
177
|
+
const password = new _napi_rs_keyring.Entry(SERVICE_NAME, provider).getPassword();
|
|
178
|
+
if (password != null && password !== "") return password;
|
|
179
|
+
} catch {}
|
|
180
|
+
const envVar = providerEnvVars[provider];
|
|
181
|
+
const envValue = node_process.default.env[envVar];
|
|
182
|
+
if (envValue != null && envValue !== "") return envValue;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Sets the API key for a provider in the keyring.
|
|
186
|
+
*
|
|
187
|
+
* @param provider The provider name.
|
|
188
|
+
* @param apiKey The API key to store.
|
|
189
|
+
* @throws {Error} If the keyring is not available.
|
|
190
|
+
*/
|
|
191
|
+
function setApiKey(provider, apiKey) {
|
|
192
|
+
new _napi_rs_keyring.Entry(SERVICE_NAME, provider).setPassword(apiKey);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Masks an API key for display.
|
|
196
|
+
*
|
|
197
|
+
* @param apiKey The API key to mask.
|
|
198
|
+
* @returns The masked API key (e.g., "sk-...xxxx").
|
|
199
|
+
*/
|
|
200
|
+
function maskApiKey(apiKey) {
|
|
201
|
+
if (apiKey.length <= 8) return "****";
|
|
202
|
+
return `${apiKey.slice(0, 3)}...${apiKey.slice(-4)}`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Gets the masked API key for a provider (for display purposes).
|
|
206
|
+
*
|
|
207
|
+
* @param provider The provider name.
|
|
208
|
+
* @returns The masked API key, or undefined if not found.
|
|
209
|
+
*/
|
|
210
|
+
function getMaskedApiKey(provider) {
|
|
211
|
+
const apiKey = getApiKey(provider);
|
|
212
|
+
if (apiKey == null) return;
|
|
213
|
+
return maskApiKey(apiKey);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/config/loader.ts
|
|
218
|
+
/**
|
|
219
|
+
* Gets the configuration directory path following XDG Base Directory specification.
|
|
220
|
+
*
|
|
221
|
+
* @returns The configuration directory path.
|
|
222
|
+
*/
|
|
223
|
+
function getConfigDir() {
|
|
224
|
+
const xdgConfig = node_process.default.env.XDG_CONFIG_HOME;
|
|
225
|
+
if (xdgConfig != null && xdgConfig !== "") return (0, node_path.join)(xdgConfig, "vertana");
|
|
226
|
+
if (node_process.default.platform === "win32") {
|
|
227
|
+
const appData = node_process.default.env.APPDATA;
|
|
228
|
+
if (appData != null && appData !== "") return (0, node_path.join)(appData, "vertana");
|
|
229
|
+
}
|
|
230
|
+
return (0, node_path.join)((0, node_os.homedir)(), ".config", "vertana");
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Gets the configuration file path.
|
|
234
|
+
*
|
|
235
|
+
* @returns The configuration file path.
|
|
236
|
+
*/
|
|
237
|
+
function getConfigPath() {
|
|
238
|
+
return (0, node_path.join)(getConfigDir(), "config.json");
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Loads the configuration from the config file.
|
|
242
|
+
*
|
|
243
|
+
* @returns The loaded configuration, or the default configuration if the file
|
|
244
|
+
* does not exist.
|
|
245
|
+
*/
|
|
246
|
+
function loadConfig() {
|
|
247
|
+
const configPath = getConfigPath();
|
|
248
|
+
if (!(0, node_fs.existsSync)(configPath)) return { ...defaultConfig };
|
|
249
|
+
try {
|
|
250
|
+
const content = (0, node_fs.readFileSync)(configPath, "utf-8");
|
|
251
|
+
const parsed = JSON.parse(content);
|
|
252
|
+
if (typeof parsed !== "object" || parsed === null) return { ...defaultConfig };
|
|
253
|
+
const config = parsed;
|
|
254
|
+
return { model: typeof config.model === "string" ? config.model : void 0 };
|
|
255
|
+
} catch {
|
|
256
|
+
return { ...defaultConfig };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Saves the configuration to the config file.
|
|
261
|
+
*
|
|
262
|
+
* @param config The configuration to save.
|
|
263
|
+
*/
|
|
264
|
+
function saveConfig(config) {
|
|
265
|
+
const configDir = getConfigDir();
|
|
266
|
+
const configPath = getConfigPath();
|
|
267
|
+
if (!(0, node_fs.existsSync)(configDir)) (0, node_fs.mkdirSync)(configDir, { recursive: true });
|
|
268
|
+
(0, node_fs.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Updates a specific configuration value.
|
|
272
|
+
*
|
|
273
|
+
* @param key The configuration key to update.
|
|
274
|
+
* @param value The new value.
|
|
275
|
+
*/
|
|
276
|
+
function updateConfig(key, value) {
|
|
277
|
+
saveConfig({
|
|
278
|
+
...loadConfig(),
|
|
279
|
+
[key]: value
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/model.ts
|
|
285
|
+
/**
|
|
286
|
+
* Parses a model code into provider and model ID.
|
|
287
|
+
*
|
|
288
|
+
* @param code The model code in "provider:model" format.
|
|
289
|
+
* @returns The parsed provider and model ID.
|
|
290
|
+
* @throws {SyntaxError} If the model code format is invalid.
|
|
291
|
+
* @throws {TypeError} If the provider is not supported.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* const { provider, modelId } = parseModelCode("openai:gpt-4o");
|
|
296
|
+
* // provider === "openai", modelId === "gpt-4o"
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
function parseModelCode(code) {
|
|
300
|
+
const colonIndex = code.indexOf(":");
|
|
301
|
+
if (colonIndex === -1) throw new SyntaxError(`Invalid model code format: "${code}". Expected format: "provider:model" (e.g., "openai:gpt-4o").`);
|
|
302
|
+
const provider = code.slice(0, colonIndex);
|
|
303
|
+
const modelId = code.slice(colonIndex + 1);
|
|
304
|
+
if (modelId === "") throw new SyntaxError(`Invalid model code format: "${code}". Model ID cannot be empty.`);
|
|
305
|
+
if (!isProviderName(provider)) throw new TypeError(`Unsupported provider: "${provider}". Supported providers: ${providerNames.map((p) => `"${p}"`).join(", ")}.`);
|
|
306
|
+
return {
|
|
307
|
+
provider,
|
|
308
|
+
modelId
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Creates a ValueParser for model codes.
|
|
313
|
+
* Model codes are in the format "provider:model" (e.g., "openai:gpt-4o").
|
|
314
|
+
*
|
|
315
|
+
* @returns A ValueParser for model codes.
|
|
316
|
+
*/
|
|
317
|
+
function modelCode() {
|
|
318
|
+
return {
|
|
319
|
+
metavar: "PROVIDER:MODEL",
|
|
320
|
+
parse(input) {
|
|
321
|
+
const colonIndex = input.indexOf(":");
|
|
322
|
+
if (colonIndex === -1) return {
|
|
323
|
+
success: false,
|
|
324
|
+
error: _optique_core_message.message`Invalid model code format. Expected ${(0, _optique_core_message.metavar)("PROVIDER")}${(0, _optique_core_message.text)(":")}${(0, _optique_core_message.metavar)("MODEL")} (e.g., ${(0, _optique_core_message.commandLine)("openai:gpt-4o")}).`
|
|
325
|
+
};
|
|
326
|
+
const provider = input.slice(0, colonIndex);
|
|
327
|
+
const modelId = input.slice(colonIndex + 1);
|
|
328
|
+
if (modelId === "") return {
|
|
329
|
+
success: false,
|
|
330
|
+
error: _optique_core_message.message`${(0, _optique_core_message.metavar)("MODEL")} cannot be empty.`
|
|
331
|
+
};
|
|
332
|
+
if (!isProviderName(provider)) {
|
|
333
|
+
let providerList = [];
|
|
334
|
+
for (let i = 0; i < providerNames.length; i++) {
|
|
335
|
+
if (i > 0) providerList = [...providerList, ..._optique_core_message.message`, `];
|
|
336
|
+
providerList = [...providerList, ..._optique_core_message.message`${providerNames[i]}`];
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
error: _optique_core_message.message`Unsupported provider: ${provider}. Supported providers: ${providerList}.`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
success: true,
|
|
345
|
+
value: {
|
|
346
|
+
provider,
|
|
347
|
+
modelId
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
format(value) {
|
|
352
|
+
return `${value.provider}:${value.modelId}`;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Creates a language model from a model code.
|
|
358
|
+
* Uses the API key from the keyring or environment variables.
|
|
359
|
+
*
|
|
360
|
+
* @param code The model code (e.g., "openai:gpt-4o").
|
|
361
|
+
* @returns The language model instance.
|
|
362
|
+
* @throws {SyntaxError} If the model code format is invalid.
|
|
363
|
+
* @throws {TypeError} If the provider is not supported.
|
|
364
|
+
* @throws {Error} If the API key is not configured.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const model = await createModel("openai:gpt-4o");
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
async function createModel(code) {
|
|
372
|
+
const { provider, modelId } = parseModelCode(code);
|
|
373
|
+
const apiKey = getApiKey(provider);
|
|
374
|
+
if (apiKey == null) throw new Error(`API key not configured for provider "${provider}". Run "vertana config api-key ${provider} <key>" to set it.`);
|
|
375
|
+
switch (provider) {
|
|
376
|
+
case "openai": {
|
|
377
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
378
|
+
return createOpenAI({ apiKey })(modelId);
|
|
379
|
+
}
|
|
380
|
+
case "anthropic": {
|
|
381
|
+
const { createAnthropic } = await import("@ai-sdk/anthropic");
|
|
382
|
+
return createAnthropic({ apiKey })(modelId);
|
|
383
|
+
}
|
|
384
|
+
case "google": {
|
|
385
|
+
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
|
|
386
|
+
return createGoogleGenerativeAI({ apiKey })(modelId);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/cli.ts
|
|
393
|
+
/**
|
|
394
|
+
* Global options shared across all subcommands.
|
|
395
|
+
*/
|
|
396
|
+
const globalOptions = (0, _optique_core_constructs.object)({ logging: (0, _optique_logtape.loggingOptions)({ level: "verbosity" }) });
|
|
397
|
+
/**
|
|
398
|
+
* Parser for the "translate" subcommand.
|
|
399
|
+
*/
|
|
400
|
+
const translateCommand = (0, _optique_core_primitives.command)("translate", (0, _optique_core_constructs.object)({
|
|
401
|
+
command: (0, _optique_core_primitives.constant)("translate"),
|
|
402
|
+
target: (0, _optique_core_primitives.option)("-t", "--target", (0, _optique_core_valueparser.string)({ metavar: "LANG" })),
|
|
403
|
+
source: (0, _optique_core_modifiers.optional)((0, _optique_core_primitives.option)("-s", "--source", (0, _optique_core_valueparser.string)({ metavar: "LANG" }))),
|
|
404
|
+
mediaType: (0, _optique_core_modifiers.withDefault)((0, _optique_core_primitives.option)("-T", "--type", (0, _optique_core_valueparser.choice)([
|
|
405
|
+
"text/plain",
|
|
406
|
+
"text/markdown",
|
|
407
|
+
"text/html"
|
|
408
|
+
])), "text/plain"),
|
|
409
|
+
tone: (0, _optique_core_modifiers.optional)((0, _optique_core_primitives.option)("--tone", (0, _optique_core_valueparser.choice)([
|
|
410
|
+
"formal",
|
|
411
|
+
"informal",
|
|
412
|
+
"technical",
|
|
413
|
+
"casual",
|
|
414
|
+
"professional",
|
|
415
|
+
"literary",
|
|
416
|
+
"journalistic"
|
|
417
|
+
]))),
|
|
418
|
+
domain: (0, _optique_core_modifiers.optional)((0, _optique_core_primitives.option)("--domain", (0, _optique_core_valueparser.string)())),
|
|
419
|
+
glossary: (0, _optique_core_modifiers.multiple)((0, _optique_core_primitives.option)("-g", "--glossary", glossaryEntry())),
|
|
420
|
+
glossaryFile: (0, _optique_core_modifiers.optional)((0, _optique_core_primitives.option)("--glossary-file", (0, _optique_run_valueparser.path)({
|
|
421
|
+
mustExist: true,
|
|
422
|
+
type: "file"
|
|
423
|
+
}))),
|
|
424
|
+
output: (0, _optique_core_modifiers.optional)((0, _optique_core_primitives.option)("-o", "--output", (0, _optique_run_valueparser.path)({ metavar: "FILE" }))),
|
|
425
|
+
input: (0, _optique_core_modifiers.optional)((0, _optique_core_primitives.argument)((0, _optique_run_valueparser.path)({ metavar: "FILE" })))
|
|
426
|
+
}));
|
|
427
|
+
/**
|
|
428
|
+
* Parser for the "config model" subcommand.
|
|
429
|
+
*/
|
|
430
|
+
const configModelCommand = (0, _optique_core_primitives.command)("model", (0, _optique_core_constructs.object)({
|
|
431
|
+
subcommand: (0, _optique_core_primitives.constant)("model"),
|
|
432
|
+
value: (0, _optique_core_modifiers.optional)((0, _optique_core_primitives.argument)(modelCode()))
|
|
433
|
+
}));
|
|
434
|
+
/**
|
|
435
|
+
* Parser for the "config api-key" subcommand.
|
|
436
|
+
*/
|
|
437
|
+
const configApiKeyCommand = (0, _optique_core_primitives.command)("api-key", (0, _optique_core_constructs.object)({
|
|
438
|
+
subcommand: (0, _optique_core_primitives.constant)("api-key"),
|
|
439
|
+
provider: (0, _optique_core_primitives.argument)((0, _optique_core_valueparser.choice)([
|
|
440
|
+
"openai",
|
|
441
|
+
"anthropic",
|
|
442
|
+
"google"
|
|
443
|
+
], { metavar: "PROVIDER" })),
|
|
444
|
+
key: (0, _optique_core_modifiers.optional)((0, _optique_core_primitives.argument)((0, _optique_core_valueparser.string)({ metavar: "KEY" })))
|
|
445
|
+
}));
|
|
446
|
+
/**
|
|
447
|
+
* Parser for the "config" subcommand with nested subcommands.
|
|
448
|
+
*/
|
|
449
|
+
const configCommand = (0, _optique_core_primitives.command)("config", (0, _optique_core_constructs.object)({
|
|
450
|
+
command: (0, _optique_core_primitives.constant)("config"),
|
|
451
|
+
action: (0, _optique_core_constructs.or)(configModelCommand, configApiKeyCommand)
|
|
452
|
+
}));
|
|
453
|
+
/**
|
|
454
|
+
* The main CLI parser combining all subcommands.
|
|
455
|
+
*/
|
|
456
|
+
const parser = (0, _optique_core_constructs.merge)(globalOptions, (0, _optique_core_constructs.or)(translateCommand, configCommand));
|
|
457
|
+
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/commands/config.ts
|
|
460
|
+
/**
|
|
461
|
+
* Executes the config command.
|
|
462
|
+
*
|
|
463
|
+
* @param result The parsed config command result.
|
|
464
|
+
*/
|
|
465
|
+
function executeConfig(result) {
|
|
466
|
+
const { action } = result;
|
|
467
|
+
switch (action.subcommand) {
|
|
468
|
+
case "model":
|
|
469
|
+
handleModelConfig(action.value);
|
|
470
|
+
break;
|
|
471
|
+
case "api-key":
|
|
472
|
+
handleApiKeyConfig(action.provider, action.key);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Formats a parsed model code back to string.
|
|
478
|
+
*
|
|
479
|
+
* @param model The parsed model code.
|
|
480
|
+
* @returns The model code string in "provider:model" format.
|
|
481
|
+
*/
|
|
482
|
+
function formatModelCode(model) {
|
|
483
|
+
return `${model.provider}:${model.modelId}`;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Handles the "config model" subcommand.
|
|
487
|
+
*
|
|
488
|
+
* @param value The model value to set, or undefined to get the current value.
|
|
489
|
+
*/
|
|
490
|
+
function handleModelConfig(value) {
|
|
491
|
+
if (value == null) {
|
|
492
|
+
const config = loadConfig();
|
|
493
|
+
if (config.model == null) {
|
|
494
|
+
(0, _optique_run.print)(_optique_core_message.message`No model configured.`);
|
|
495
|
+
(0, _optique_run.print)(_optique_core_message.message`Use ${(0, _optique_core_message.commandLine)("vertana config model")} ${(0, _optique_core_message.metavar)("PROVIDER:MODEL")} to set one.`);
|
|
496
|
+
} else (0, _optique_run.print)(_optique_core_message.message`${(0, _optique_core_message.text)(config.model)}`);
|
|
497
|
+
} else {
|
|
498
|
+
const code = formatModelCode(value);
|
|
499
|
+
updateConfig("model", code);
|
|
500
|
+
(0, _optique_run.print)(_optique_core_message.message`Model set to: ${(0, _optique_core_message.text)(code)}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Handles the "config api-key" subcommand.
|
|
505
|
+
*
|
|
506
|
+
* @param provider The provider name.
|
|
507
|
+
* @param key The API key to set, or undefined to get the current value.
|
|
508
|
+
*/
|
|
509
|
+
function handleApiKeyConfig(provider, key) {
|
|
510
|
+
if (key == null) {
|
|
511
|
+
const masked = getMaskedApiKey(provider);
|
|
512
|
+
if (masked == null) {
|
|
513
|
+
(0, _optique_run.print)(_optique_core_message.message`No API key configured for ${(0, _optique_core_message.text)(provider)}.`);
|
|
514
|
+
(0, _optique_run.print)(_optique_core_message.message`Use ${(0, _optique_core_message.commandLine)(`vertana config api-key ${provider}`)} ${(0, _optique_core_message.metavar)("KEY")} to set one.`);
|
|
515
|
+
} else (0, _optique_run.print)(_optique_core_message.message`${(0, _optique_core_message.text)(masked)}`);
|
|
516
|
+
} else {
|
|
517
|
+
setApiKey(provider, key);
|
|
518
|
+
(0, _optique_run.print)(_optique_core_message.message`API key for ${(0, _optique_core_message.text)(provider)} has been saved.`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/input.ts
|
|
524
|
+
/**
|
|
525
|
+
* Reads input from a file or stdin.
|
|
526
|
+
*
|
|
527
|
+
* @param filePath The file path to read from, or undefined to read from stdin.
|
|
528
|
+
* @returns The input text.
|
|
529
|
+
*/
|
|
530
|
+
async function readInput(filePath) {
|
|
531
|
+
if (filePath != null) return (0, node_fs.readFileSync)(filePath, "utf-8");
|
|
532
|
+
return await readStdin();
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Reads all input from stdin.
|
|
536
|
+
*
|
|
537
|
+
* @returns The stdin content as a string.
|
|
538
|
+
*/
|
|
539
|
+
async function readStdin() {
|
|
540
|
+
const stream = await getStdinStream();
|
|
541
|
+
const decoder = new TextDecoder();
|
|
542
|
+
const chunks = [];
|
|
543
|
+
for await (const chunk of stream) chunks.push(decoder.decode(chunk, { stream: true }));
|
|
544
|
+
chunks.push(decoder.decode());
|
|
545
|
+
return chunks.join("");
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Gets the stdin stream as a ReadableStream.
|
|
549
|
+
* Works across Deno, Node.js, and Bun.
|
|
550
|
+
*
|
|
551
|
+
* @returns The stdin ReadableStream.
|
|
552
|
+
*/
|
|
553
|
+
async function getStdinStream() {
|
|
554
|
+
if (typeof Deno !== "undefined" && Deno?.stdin?.readable != null) return Deno.stdin.readable;
|
|
555
|
+
const { stdin } = await import("node:process");
|
|
556
|
+
return node_stream.Readable.toWeb(stdin);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/commands/translate.ts
|
|
561
|
+
/**
|
|
562
|
+
* Executes the translate command.
|
|
563
|
+
*
|
|
564
|
+
* @param result The parsed translate command result.
|
|
565
|
+
*/
|
|
566
|
+
async function executeTranslate(result) {
|
|
567
|
+
const config = loadConfig();
|
|
568
|
+
if (config.model == null) {
|
|
569
|
+
(0, _optique_run.printError)(_optique_core_message.message`No model configured.`, { exitCode: 1 });
|
|
570
|
+
(0, _optique_run.print)(_optique_core_message.message`Run ${(0, _optique_core_message.commandLine)("vertana config model")} ${(0, _optique_core_message.metavar)("PROVIDER:MODEL")} to set one.`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const model = await createModel(config.model);
|
|
574
|
+
const inputText = await readInput(result.input);
|
|
575
|
+
if (inputText.trim() === "") {
|
|
576
|
+
(0, _optique_run.printError)(_optique_core_message.message`No input text provided.`, { exitCode: 1 });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const glossary = buildGlossary(result.glossary, result.glossaryFile);
|
|
580
|
+
const translation = await (0, _vertana_facade.translate)(model, result.target, inputText, {
|
|
581
|
+
sourceLanguage: result.source,
|
|
582
|
+
mediaType: result.mediaType,
|
|
583
|
+
tone: result.tone,
|
|
584
|
+
domain: result.domain,
|
|
585
|
+
glossary: glossary.length > 0 ? glossary : void 0
|
|
586
|
+
});
|
|
587
|
+
if (result.output != null) try {
|
|
588
|
+
(0, node_fs.writeFileSync)(result.output, translation.text + "\n", "utf-8");
|
|
589
|
+
(0, _optique_run.print)(_optique_core_message.message`Translation saved to: ${(0, _optique_core_message.text)(result.output)}`);
|
|
590
|
+
} catch (e) {
|
|
591
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
592
|
+
(0, _optique_run.printError)(_optique_core_message.message`Failed to write to file: ${result.output} — ${(0, _optique_core_message.text)(errorMessage)}`, { exitCode: 1 });
|
|
593
|
+
}
|
|
594
|
+
else console.log(translation.text);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Builds a glossary from CLI options and an optional file.
|
|
598
|
+
*
|
|
599
|
+
* @param cliEntries Glossary entries from -g/--glossary options.
|
|
600
|
+
* @param filePath Path to a glossary JSON file.
|
|
601
|
+
* @returns The merged glossary.
|
|
602
|
+
*/
|
|
603
|
+
function buildGlossary(cliEntries, filePath) {
|
|
604
|
+
const glossaries = [];
|
|
605
|
+
if (filePath != null) try {
|
|
606
|
+
const fileGlossary = loadGlossaryFile(filePath);
|
|
607
|
+
glossaries.push(fileGlossary);
|
|
608
|
+
} catch (e) {
|
|
609
|
+
(0, _optique_run.printError)(_optique_core_message.message`Failed to load glossary file: ${filePath} — ${(0, _optique_core_message.text)(e instanceof Error ? e.message : String(e))}`, { exitCode: 1 });
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
if (cliEntries.length > 0) glossaries.push(cliEntries);
|
|
613
|
+
return mergeGlossaries(...glossaries);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/index.ts
|
|
618
|
+
/**
|
|
619
|
+
* The CLI entry point.
|
|
620
|
+
*/
|
|
621
|
+
async function main() {
|
|
622
|
+
const result = (0, _optique_run.run)(parser, {
|
|
623
|
+
programName: "vertana",
|
|
624
|
+
brief: _optique_core_message.message`LLM-powered translation CLI`,
|
|
625
|
+
description: _optique_core_message.message`Translate text using large language models with support for
|
|
626
|
+
multiple providers (OpenAI, Anthropic, Google) and various options
|
|
627
|
+
for customizing the translation output.`,
|
|
628
|
+
footer: _optique_core_message.message`Examples:
|
|
629
|
+
|
|
630
|
+
${(0, _optique_core_message.commandLine)("vertana translate -t ko input.txt")}
|
|
631
|
+
|
|
632
|
+
${(0, _optique_core_message.commandLine)("vertana translate -t ko -o output.txt input.txt")}
|
|
633
|
+
|
|
634
|
+
${(0, _optique_core_message.commandLine)("echo \"Hello\" | vertana translate -t ko")}
|
|
635
|
+
|
|
636
|
+
${(0, _optique_core_message.commandLine)("vertana config model openai:gpt-4o")}
|
|
637
|
+
|
|
638
|
+
${(0, _optique_core_message.commandLine)("vertana config api-key openai sk-...")}`,
|
|
639
|
+
help: "both",
|
|
640
|
+
version: "0.1.0",
|
|
641
|
+
aboveError: "usage"
|
|
642
|
+
});
|
|
643
|
+
await (0, _logtape_logtape.configure)(await (0, _optique_logtape.createLoggingConfig)(result.logging));
|
|
644
|
+
await executeCommand(result);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Executes the parsed CLI command.
|
|
648
|
+
*
|
|
649
|
+
* @param result The parsed CLI result.
|
|
650
|
+
*/
|
|
651
|
+
async function executeCommand(result) {
|
|
652
|
+
switch (result.command) {
|
|
653
|
+
case "translate":
|
|
654
|
+
await executeTranslate(result);
|
|
655
|
+
break;
|
|
656
|
+
case "config":
|
|
657
|
+
executeConfig(result);
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if ("main" in {} && {}.main || typeof node_process.default !== "undefined" && node_process.default.argv[1]?.endsWith("index.ts") || typeof node_process.default !== "undefined" && node_process.default.argv[1]?.endsWith("index.js") || typeof node_process.default !== "undefined" && node_process.default.argv[1]?.endsWith("index.mjs") || typeof node_process.default !== "undefined" && node_process.default.argv[1]?.endsWith("index.cjs")) main().catch((error) => {
|
|
662
|
+
if (error instanceof Error) (0, _optique_run.printError)(_optique_core_message.message`${(0, _optique_core_message.text)(error.message)}`, { exitCode: 1 });
|
|
663
|
+
else (0, _optique_run.printError)(_optique_core_message.message`An unexpected error occurred.`, { exitCode: 1 });
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
//#endregion
|
|
667
|
+
exports.main = main;
|