@kesarcloud/omega-plus-cli 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KesarCloud Technologies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Omega Plus CLI
2
+
3
+ Interactive setup wizard for connecting local coding tools to Omega Plus.
4
+
5
+ ```bash
6
+ npx @kesarcloud/omega-plus-cli
7
+ ```
8
+
9
+ The wizard shows the Omega Plus banner, validates your Omega API key, lets you choose a supported coding tool, backs up existing config files, and writes Omega Plus settings.
10
+
11
+ ## Supported Tools
12
+
13
+ - Claude Code
14
+ - OpenAI Codex CLI
15
+ - OpenCode
16
+ - Cline
17
+ - Kilo Code
18
+ - Factory Droid
19
+
20
+ ## Non-Interactive Usage
21
+
22
+ ```bash
23
+ npx @kesarcloud/omega-plus-cli configure --tool claude --api-key YOUR_KEY
24
+ npx @kesarcloud/omega-plus-cli configure --tool codex --api-key YOUR_KEY --dry-run
25
+ ```
26
+
27
+ Defaults:
28
+
29
+ - Base URL: `https://omega.kesarcloud.in/v1`
30
+ - OpenAI-compatible URL: `https://omega.kesarcloud.in/api/v1`
31
+ - Anthropic-compatible URL: `https://omega.kesarcloud.in/v1`
32
+ - API key validation URL: `https://omega.kesarcloud.in/api/omega/validate-key`
33
+ - Model: `omega-v1-pro`
34
+ - Fast model: `omega-v1`
35
+
36
+ Use `--base-url`, `--openai-base-url`, `--anthropic-base-url`, `--model`, or `--fast-model` to override them.
37
+
38
+ Codex receives the OpenAI-compatible endpoint. Claude Code, OpenCode, Cline, Kilo Code, and Factory Droid receive the Anthropic-compatible endpoint. The CLI validates the key before writing configs; use `--skip-key-validation` only for local testing or dry runs against an unavailable server.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/cli.mjs";
4
+
5
+ main(process.argv.slice(2)).catch((error) => {
6
+ const message = error instanceof Error ? error.message : String(error);
7
+ console.error(`Omega Plus setup failed: ${message}`);
8
+ process.exitCode = 1;
9
+ });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@kesarcloud/omega-plus-cli",
3
+ "version": "2.0.0",
4
+ "description": "Interactive Omega Plus setup wizard for coding CLI tools.",
5
+ "type": "module",
6
+ "bin": {
7
+ "omega": "bin/omega-plus.mjs",
8
+ "omega-plus": "bin/omega-plus.mjs"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "keywords": [
20
+ "omega-plus",
21
+ "omega-v1",
22
+ "claude-code",
23
+ "codex",
24
+ "opencode",
25
+ "cline",
26
+ "kilocode",
27
+ "factory-droid",
28
+ "ai-cli"
29
+ ],
30
+ "license": "MIT",
31
+ "author": "KesarCloud Technologies",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/karanbavari/OmegaCluster",
35
+ "directory": "packages/omega-cli-helper"
36
+ },
37
+ "scripts": {
38
+ "test": "node --test ../../tests/unit/omega-cli-helper.test.mjs"
39
+ }
40
+ }
package/src/banner.mjs ADDED
@@ -0,0 +1,20 @@
1
+ const RED = "\x1b[38;5;196m";
2
+ const ORANGE = "\x1b[38;5;208m";
3
+ const YELLOW = "\x1b[38;5;214m";
4
+ const RESET = "\x1b[0m";
5
+ const BOLD = "\x1b[1m";
6
+
7
+ export function getBanner() {
8
+ return [
9
+ `${BOLD}${RED} ____ __ __ _____ ____ _ ${ORANGE} ____ _ _ _ ____ ${RESET}`,
10
+ `${BOLD}${RED} / __ \\| \\/ | ____/ ___| / \\ ${ORANGE} | _ \\| | | | | / ___|${RESET}`,
11
+ `${BOLD}${ORANGE} | | | | |\\/| | _|| | _ / _ \\ ${YELLOW} | |_) | | | | | \\___ \\${RESET}`,
12
+ `${BOLD}${ORANGE} | |__| | | | | |__| |_| |/ ___ \\ ${YELLOW} | __/| |__| |_| |___) |${RESET}`,
13
+ `${BOLD}${YELLOW} \\____/|_| |_|_____\\____/_/ \\_\\${RED} |_| |_____\\___/|____/${RESET}`,
14
+ ].join("\n");
15
+ }
16
+
17
+ export function printBanner() {
18
+ console.log(`\n${getBanner()}`);
19
+ console.log(`${ORANGE}Connect your coding tool to Omega V1.${RESET}\n`);
20
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,105 @@
1
+ import {
2
+ DEFAULT_ANTHROPIC_BASE_URL,
3
+ DEFAULT_BASE_URL,
4
+ DEFAULT_FAST_MODEL,
5
+ DEFAULT_MODEL,
6
+ DEFAULT_OPENAI_BASE_URL,
7
+ DEFAULT_VALIDATION_URL,
8
+ VERSION,
9
+ } from "./constants.mjs";
10
+ import { printDoctor, printToolList, runNonInteractiveConfigure, runWizard } from "./wizard.mjs";
11
+
12
+ function printHelp() {
13
+ console.log(`Omega Plus CLI ${VERSION}
14
+
15
+ Usage:
16
+ omega-plus
17
+ omega-plus setup
18
+ omega-plus configure --tool <id> --api-key <key>
19
+ omega-plus list
20
+ omega-plus doctor
21
+
22
+ Options:
23
+ --api-key <key> Omega API key
24
+ --tool <id> claude, codex, opencode, cline, kilo, droid
25
+ --base-url <url> Default: ${DEFAULT_BASE_URL}
26
+ --openai-base-url Codex/OpenAI-compatible default: ${DEFAULT_OPENAI_BASE_URL}
27
+ --anthropic-base-url Anthropic-compatible default: ${DEFAULT_ANTHROPIC_BASE_URL}
28
+ --validation-url Default: ${DEFAULT_VALIDATION_URL}
29
+ --model <id> Default: ${DEFAULT_MODEL}
30
+ --fast-model <id> Default: ${DEFAULT_FAST_MODEL}
31
+ --skip-key-validation Skip server-side API key validation
32
+ --dry-run Preview files without writing
33
+ --yes Auto-confirm install prompts
34
+ --no-install Do not install missing tools
35
+ --help Show help
36
+ --version Show version
37
+ `);
38
+ }
39
+
40
+ function parseArgs(argv) {
41
+ const args = [...argv];
42
+ const options = {};
43
+ const positionals = [];
44
+
45
+ for (let i = 0; i < args.length; i += 1) {
46
+ const arg = args[i];
47
+ if (arg === "--help" || arg === "-h") options.help = true;
48
+ else if (arg === "--version" || arg === "-v") options.version = true;
49
+ else if (arg === "--dry-run") options.dryRun = true;
50
+ else if (arg === "--skip-key-validation") options.skipKeyValidation = true;
51
+ else if (arg === "--yes" || arg === "-y") options.yes = true;
52
+ else if (arg === "--no-install") options.noInstall = true;
53
+ else if (arg === "--api-key") options.apiKey = args[++i];
54
+ else if (arg.startsWith("--api-key=")) options.apiKey = arg.slice("--api-key=".length);
55
+ else if (arg === "--tool") options.tool = args[++i];
56
+ else if (arg.startsWith("--tool=")) options.tool = arg.slice("--tool=".length);
57
+ else if (arg === "--base-url") options.baseUrl = args[++i];
58
+ else if (arg.startsWith("--base-url=")) options.baseUrl = arg.slice("--base-url=".length);
59
+ else if (arg === "--openai-base-url") options.openaiBaseUrl = args[++i];
60
+ else if (arg.startsWith("--openai-base-url="))
61
+ options.openaiBaseUrl = arg.slice("--openai-base-url=".length);
62
+ else if (arg === "--anthropic-base-url") options.anthropicBaseUrl = args[++i];
63
+ else if (arg.startsWith("--anthropic-base-url="))
64
+ options.anthropicBaseUrl = arg.slice("--anthropic-base-url=".length);
65
+ else if (arg === "--validation-url") options.validationUrl = args[++i];
66
+ else if (arg.startsWith("--validation-url="))
67
+ options.validationUrl = arg.slice("--validation-url=".length);
68
+ else if (arg === "--model") options.model = args[++i];
69
+ else if (arg.startsWith("--model=")) options.model = arg.slice("--model=".length);
70
+ else if (arg === "--fast-model") options.fastModel = args[++i];
71
+ else if (arg.startsWith("--fast-model=")) options.fastModel = arg.slice("--fast-model=".length);
72
+ else positionals.push(arg);
73
+ }
74
+
75
+ return { command: positionals[0] || "setup", options };
76
+ }
77
+
78
+ export async function main(argv = process.argv.slice(2)) {
79
+ const { command, options } = parseArgs(argv);
80
+ if (options.help) {
81
+ printHelp();
82
+ return;
83
+ }
84
+ if (options.version) {
85
+ console.log(VERSION);
86
+ return;
87
+ }
88
+
89
+ switch (command) {
90
+ case "setup":
91
+ await runWizard(options);
92
+ break;
93
+ case "configure":
94
+ await runNonInteractiveConfigure(options);
95
+ break;
96
+ case "list":
97
+ await printToolList();
98
+ break;
99
+ case "doctor":
100
+ await printDoctor();
101
+ break;
102
+ default:
103
+ throw new Error(`Unknown command: ${command}. Run omega-plus --help.`);
104
+ }
105
+ }
@@ -0,0 +1,247 @@
1
+ import {
2
+ DEFAULT_BASE_URL,
3
+ TOOL_DEFINITIONS,
4
+ DEFAULT_FAST_MODEL,
5
+ DEFAULT_MODEL,
6
+ OMEGA_MODELS,
7
+ } from "./constants.mjs";
8
+ import {
9
+ deriveEndpointOptions,
10
+ normalizeCodexBaseUrl,
11
+ normalizeHttpBaseUrl,
12
+ readJson,
13
+ readText,
14
+ writeJsonFile,
15
+ writeTextFile,
16
+ } from "./fs-utils.mjs";
17
+ import { getToolPaths } from "./tool-paths.mjs";
18
+ import { parseToml, stringifyToml } from "./toml.mjs";
19
+
20
+ function resolveOptions(input = {}) {
21
+ const baseUrl = input.baseUrl || DEFAULT_BASE_URL;
22
+ const endpoints = deriveEndpointOptions({
23
+ baseUrl,
24
+ anthropicBaseUrl: input.anthropicBaseUrl,
25
+ openaiBaseUrl: input.openaiBaseUrl,
26
+ });
27
+ return {
28
+ baseUrl: normalizeHttpBaseUrl(baseUrl),
29
+ anthropicBaseUrl: endpoints.anthropicBaseUrl,
30
+ openaiBaseUrl: endpoints.openaiBaseUrl,
31
+ apiKey: String(input.apiKey || "").trim(),
32
+ model: String(input.model || DEFAULT_MODEL).trim(),
33
+ fastModel: String(input.fastModel || DEFAULT_FAST_MODEL).trim(),
34
+ dryRun: Boolean(input.dryRun),
35
+ home: input.home,
36
+ appData: input.appData,
37
+ };
38
+ }
39
+
40
+ function requireApiKey(apiKey) {
41
+ if (!apiKey) throw new Error("API key is required");
42
+ }
43
+
44
+ function modelEntries(model, fastModel) {
45
+ return [...new Set([model, fastModel, ...OMEGA_MODELS.map((entry) => entry.id)].filter(Boolean))];
46
+ }
47
+
48
+ async function configureClaude(input) {
49
+ const options = resolveOptions(input);
50
+ requireApiKey(options.apiKey);
51
+ const paths = getToolPaths("claude", options);
52
+ const settings = await readJson(paths.settings, {});
53
+ const env =
54
+ settings && typeof settings.env === "object" && !Array.isArray(settings.env)
55
+ ? settings.env
56
+ : {};
57
+
58
+ const nextSettings = {
59
+ ...settings,
60
+ env: {
61
+ ...env,
62
+ ANTHROPIC_AUTH_TOKEN: options.apiKey,
63
+ ANTHROPIC_BASE_URL: options.anthropicBaseUrl,
64
+ ANTHROPIC_MODEL: options.model,
65
+ ANTHROPIC_SMALL_FAST_MODEL: options.fastModel,
66
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
67
+ },
68
+ hasCompletedOnboarding: true,
69
+ };
70
+
71
+ const write = await writeJsonFile(paths.settings, nextSettings, options);
72
+ return { toolId: "claude", files: [write], message: "Claude Code is configured for Omega Plus." };
73
+ }
74
+
75
+ async function configureCodex(input) {
76
+ const options = resolveOptions(input);
77
+ requireApiKey(options.apiKey);
78
+ const paths = getToolPaths("codex", options);
79
+ const parsed = parseToml(await readText(paths.config, ""));
80
+
81
+ parsed._root.model = options.model;
82
+ parsed._root.model_provider = "omega";
83
+ delete parsed._root.openai_base_url;
84
+ parsed._sections["model_providers.omega"] = {
85
+ name: "Omega Plus",
86
+ base_url: normalizeCodexBaseUrl(options.openaiBaseUrl),
87
+ wire_api: "chat",
88
+ env_key: "OPENAI_API_KEY",
89
+ };
90
+ delete parsed._sections["model_providers.omniroute"];
91
+
92
+ const configWrite = await writeTextFile(paths.config, stringifyToml(parsed), options);
93
+ const auth = await readJson(paths.auth, {});
94
+ auth.OPENAI_API_KEY = options.apiKey;
95
+ const authWrite = await writeJsonFile(paths.auth, auth, options);
96
+
97
+ return {
98
+ toolId: "codex",
99
+ files: [configWrite, authWrite],
100
+ message: "OpenAI Codex CLI is configured for Omega Plus.",
101
+ };
102
+ }
103
+
104
+ function buildOpenCodeProvider(options) {
105
+ const models = {};
106
+ for (const model of modelEntries(options.model, options.fastModel)) {
107
+ const label = OMEGA_MODELS.find((entry) => entry.id === model)?.name || model;
108
+ models[model] = { name: label };
109
+ }
110
+
111
+ return {
112
+ npm: "@ai-sdk/anthropic",
113
+ name: "Omega Plus",
114
+ options: {
115
+ baseURL: options.anthropicBaseUrl,
116
+ apiKey: options.apiKey,
117
+ },
118
+ models,
119
+ };
120
+ }
121
+
122
+ async function configureOpenCode(input) {
123
+ const options = resolveOptions(input);
124
+ requireApiKey(options.apiKey);
125
+ const paths = getToolPaths("opencode", options);
126
+ const current = await readJson(paths.config, {});
127
+ const provider =
128
+ current && typeof current.provider === "object" && !Array.isArray(current.provider)
129
+ ? current.provider
130
+ : {};
131
+
132
+ const nextConfig = {
133
+ ...current,
134
+ $schema: current.$schema || "https://opencode.ai/config.json",
135
+ provider: {
136
+ ...provider,
137
+ omega: buildOpenCodeProvider(options),
138
+ },
139
+ };
140
+ if (nextConfig.provider.omniroute) delete nextConfig.provider.omniroute;
141
+
142
+ const write = await writeJsonFile(paths.config, nextConfig, options);
143
+ return { toolId: "opencode", files: [write], message: "OpenCode is configured for Omega Plus." };
144
+ }
145
+
146
+ async function configureCline(input) {
147
+ const options = resolveOptions(input);
148
+ requireApiKey(options.apiKey);
149
+ const paths = getToolPaths("cline", options);
150
+ const globalState = await readJson(paths.globalState, {});
151
+
152
+ const nextState = {
153
+ ...globalState,
154
+ actModeApiProvider: "anthropic",
155
+ planModeApiProvider: "anthropic",
156
+ anthropicBaseUrl: options.anthropicBaseUrl,
157
+ anthropicModelId: options.model,
158
+ planModeAnthropicModelId: options.model,
159
+ };
160
+
161
+ const secrets = await readJson(paths.secrets, {});
162
+ secrets.anthropicApiKey = options.apiKey;
163
+
164
+ const stateWrite = await writeJsonFile(paths.globalState, nextState, options);
165
+ const secretsWrite = await writeJsonFile(paths.secrets, secrets, options);
166
+ return {
167
+ toolId: "cline",
168
+ files: [stateWrite, secretsWrite],
169
+ message: "Cline is configured for Omega Plus.",
170
+ };
171
+ }
172
+
173
+ async function configureKilo(input) {
174
+ const options = resolveOptions(input);
175
+ requireApiKey(options.apiKey);
176
+ const paths = getToolPaths("kilo", options);
177
+ const auth = await readJson(paths.auth, {});
178
+
179
+ auth.anthropic = {
180
+ type: "api-key",
181
+ apiKey: options.apiKey,
182
+ baseUrl: options.anthropicBaseUrl,
183
+ model: options.model,
184
+ };
185
+
186
+ const writes = [await writeJsonFile(paths.auth, auth, options)];
187
+
188
+ const vscodeSettings = await readJson(paths.vscodeSettings, null);
189
+ if (vscodeSettings && typeof vscodeSettings === "object" && !Array.isArray(vscodeSettings)) {
190
+ vscodeSettings["kilocode.customProvider"] = {
191
+ name: "Omega Plus",
192
+ baseURL: options.anthropicBaseUrl,
193
+ apiKey: options.apiKey,
194
+ };
195
+ vscodeSettings["kilocode.defaultModel"] = options.model;
196
+ writes.push(await writeJsonFile(paths.vscodeSettings, vscodeSettings, options));
197
+ }
198
+
199
+ return { toolId: "kilo", files: writes, message: "Kilo Code is configured for Omega Plus." };
200
+ }
201
+
202
+ async function configureDroid(input) {
203
+ const options = resolveOptions(input);
204
+ requireApiKey(options.apiKey);
205
+ const paths = getToolPaths("droid", options);
206
+ const settings = await readJson(paths.settings, {});
207
+ const existingModels = Array.isArray(settings.customModels) ? settings.customModels : [];
208
+
209
+ settings.customModels = existingModels.filter(
210
+ (entry) => entry?.id !== "custom:OmegaPlus-0" && entry?.id !== "custom:OmniRoute-0"
211
+ );
212
+ settings.customModels.unshift({
213
+ model: options.model,
214
+ id: "custom:OmegaPlus-0",
215
+ index: 0,
216
+ baseUrl: options.anthropicBaseUrl,
217
+ apiKey: options.apiKey,
218
+ displayName: "Omega V1 Pro",
219
+ maxOutputTokens: 131072,
220
+ noImageSupport: false,
221
+ provider: "anthropic",
222
+ });
223
+
224
+ const write = await writeJsonFile(paths.settings, settings, options);
225
+ return {
226
+ toolId: "droid",
227
+ files: [write],
228
+ message: "Factory Droid is configured for Omega Plus.",
229
+ };
230
+ }
231
+
232
+ export const CONFIG_WRITERS = {
233
+ claude: configureClaude,
234
+ codex: configureCodex,
235
+ opencode: configureOpenCode,
236
+ cline: configureCline,
237
+ kilo: configureKilo,
238
+ droid: configureDroid,
239
+ };
240
+
241
+ export async function configureTool(toolId, input = {}) {
242
+ const id = String(toolId || "")
243
+ .trim()
244
+ .toLowerCase();
245
+ if (!TOOL_DEFINITIONS[id]) throw new Error(`Unsupported tool: ${toolId}`);
246
+ return CONFIG_WRITERS[id](input);
247
+ }
@@ -0,0 +1,60 @@
1
+ export const VERSION = "2.0.0";
2
+ export const DEFAULT_ORIGIN = "https://omega.kesarcloud.in";
3
+ export const DEFAULT_BASE_URL = `${DEFAULT_ORIGIN}/v1`;
4
+ export const DEFAULT_ANTHROPIC_BASE_URL = `${DEFAULT_ORIGIN}/v1`;
5
+ export const DEFAULT_OPENAI_BASE_URL = `${DEFAULT_ORIGIN}/api/v1`;
6
+ export const DEFAULT_VALIDATION_URL = `${DEFAULT_ORIGIN}/api/omega/validate-key`;
7
+ export const DEFAULT_MODEL = "omega-v1-pro";
8
+ export const DEFAULT_FAST_MODEL = "omega-v1";
9
+
10
+ export const OMEGA_MODELS = [
11
+ { id: "omega-v1-pro", name: "Omega V1 Pro" },
12
+ { id: "omega-v1", name: "Omega V1" },
13
+ ];
14
+
15
+ export const TOOL_IDS = ["claude", "codex", "opencode", "cline", "kilo", "droid"];
16
+
17
+ export const TOOL_DEFINITIONS = {
18
+ claude: {
19
+ id: "claude",
20
+ name: "Claude Code",
21
+ commands: ["claude"],
22
+ installCommand: ["npm", ["install", "-g", "@anthropic-ai/claude-code"]],
23
+ installHint: "npm install -g @anthropic-ai/claude-code",
24
+ },
25
+ codex: {
26
+ id: "codex",
27
+ name: "OpenAI Codex CLI",
28
+ commands: ["codex"],
29
+ installCommand: ["npm", ["install", "-g", "@openai/codex"]],
30
+ installHint: "npm install -g @openai/codex",
31
+ },
32
+ opencode: {
33
+ id: "opencode",
34
+ name: "OpenCode",
35
+ commands: ["opencode"],
36
+ installCommand: ["npm", ["install", "-g", "opencode-ai"]],
37
+ installHint: "npm install -g opencode-ai",
38
+ },
39
+ cline: {
40
+ id: "cline",
41
+ name: "Cline",
42
+ commands: ["cline"],
43
+ installCommand: null,
44
+ installHint: "Install Cline first, then rerun this wizard.",
45
+ },
46
+ kilo: {
47
+ id: "kilo",
48
+ name: "Kilo Code",
49
+ commands: ["kilocode", "kilo"],
50
+ installCommand: ["npm", ["install", "-g", "@kilocode/cli"]],
51
+ installHint: "npm install -g @kilocode/cli",
52
+ },
53
+ droid: {
54
+ id: "droid",
55
+ name: "Factory Droid",
56
+ commands: ["droid"],
57
+ installCommand: ["sh", ["-c", "curl -fsSL https://app.factory.ai/cli | sh"]],
58
+ installHint: "curl -fsSL https://app.factory.ai/cli | sh",
59
+ },
60
+ };
@@ -0,0 +1,132 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { TOOL_DEFINITIONS, TOOL_IDS } from "./constants.mjs";
4
+ import { fileExists, readJson, readText } from "./fs-utils.mjs";
5
+ import { getToolPaths } from "./tool-paths.mjs";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export async function commandExists(command) {
10
+ try {
11
+ if (process.platform === "win32") {
12
+ const { stdout } = await execFileAsync("where.exe", [command], { timeout: 3000 });
13
+ return Boolean(stdout.trim());
14
+ }
15
+ const { stdout } = await execFileAsync("sh", ["-c", 'command -v -- "$1"', "sh", command], {
16
+ timeout: 3000,
17
+ });
18
+ return Boolean(stdout.trim());
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ async function getVersion(command) {
25
+ try {
26
+ const { stdout, stderr } = await execFileAsync(command, ["--version"], { timeout: 5000 });
27
+ return (
28
+ String(stdout || stderr || "")
29
+ .trim()
30
+ .replace(/^v/, "") || null
31
+ );
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ async function detectCommand(commands) {
38
+ for (const command of commands || []) {
39
+ if (await commandExists(command)) {
40
+ return { installed: true, command, version: await getVersion(command) };
41
+ }
42
+ }
43
+ return { installed: false, command: commands?.[0] || null, version: null };
44
+ }
45
+
46
+ async function configStatus(toolId, options = {}) {
47
+ const paths = getToolPaths(toolId, options);
48
+ try {
49
+ if (toolId === "claude") {
50
+ const settings = await readJson(paths.settings, null);
51
+ return settings?.env?.ANTHROPIC_BASE_URL && settings?.env?.ANTHROPIC_AUTH_TOKEN
52
+ ? "configured"
53
+ : "not_configured";
54
+ }
55
+ if (toolId === "codex") {
56
+ const config = await readText(paths.config, "");
57
+ const auth = await readJson(paths.auth, {});
58
+ return config.includes("[model_providers.omega]") && auth.OPENAI_API_KEY
59
+ ? "configured"
60
+ : "not_configured";
61
+ }
62
+ if (toolId === "opencode") {
63
+ const config = await readJson(paths.config, null);
64
+ return config?.provider?.omega?.options?.apiKey ? "configured" : "not_configured";
65
+ }
66
+ if (toolId === "cline") {
67
+ const state = await readJson(paths.globalState, null);
68
+ const secrets = await readJson(paths.secrets, {});
69
+ return state?.actModeApiProvider === "anthropic" &&
70
+ state?.anthropicBaseUrl &&
71
+ secrets.anthropicApiKey
72
+ ? "configured"
73
+ : "not_configured";
74
+ }
75
+ if (toolId === "kilo") {
76
+ const auth = await readJson(paths.auth, null);
77
+ return auth?.anthropic?.apiKey ? "configured" : "not_configured";
78
+ }
79
+ if (toolId === "droid") {
80
+ const settings = await readJson(paths.settings, null);
81
+ const models = Array.isArray(settings?.customModels) ? settings.customModels : [];
82
+ return models.some(
83
+ (entry) => entry?.id === "custom:OmegaPlus-0" && entry?.provider === "anthropic"
84
+ )
85
+ ? "configured"
86
+ : "not_configured";
87
+ }
88
+ } catch {
89
+ return "not_configured";
90
+ }
91
+ return "unknown";
92
+ }
93
+
94
+ export async function detectTool(toolId, options = {}) {
95
+ const definition = TOOL_DEFINITIONS[toolId];
96
+ if (!definition) return null;
97
+ const command = await detectCommand(definition.commands);
98
+ const paths = getToolPaths(toolId, options);
99
+ const primaryPath = Object.values(paths)[0] || null;
100
+ return {
101
+ id: definition.id,
102
+ name: definition.name,
103
+ installed: command.installed,
104
+ command: command.command,
105
+ version: command.version,
106
+ configPath: primaryPath,
107
+ configExists: primaryPath ? await fileExists(primaryPath) : false,
108
+ configStatus: await configStatus(toolId, options),
109
+ installHint: definition.installHint,
110
+ canAutoInstall: Boolean(definition.installCommand),
111
+ };
112
+ }
113
+
114
+ export async function detectAllTools(options = {}) {
115
+ return Promise.all(TOOL_IDS.map((id) => detectTool(id, options)));
116
+ }
117
+
118
+ export function runInstallCommand(toolId) {
119
+ const definition = TOOL_DEFINITIONS[toolId];
120
+ if (!definition?.installCommand) {
121
+ throw new Error(`No automatic installer is configured for ${definition?.name || toolId}`);
122
+ }
123
+ const [command, args] = definition.installCommand;
124
+ return new Promise((resolve, reject) => {
125
+ const child = spawn(command, args, { stdio: "inherit", shell: false });
126
+ child.on("error", reject);
127
+ child.on("exit", (code) => {
128
+ if (code === 0) resolve();
129
+ else reject(new Error(`${definition.name} installer exited with code ${code}`));
130
+ });
131
+ });
132
+ }
@@ -0,0 +1,126 @@
1
+ import fs from "node:fs/promises";
2
+ import fsSync from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ export function getHome(options = {}) {
7
+ return options.home || process.env.HOME || process.env.USERPROFILE || os.homedir();
8
+ }
9
+
10
+ export function getAppData(options = {}) {
11
+ return (
12
+ options.appData || process.env.APPDATA || path.join(getHome(options), "AppData", "Roaming")
13
+ );
14
+ }
15
+
16
+ export function configPath(parts, options = {}) {
17
+ return path.join(getHome(options), ...parts);
18
+ }
19
+
20
+ export function roamingConfigPath(parts, options = {}) {
21
+ if (process.platform === "win32") return path.join(getAppData(options), ...parts);
22
+ return path.join(getHome(options), ".config", ...parts);
23
+ }
24
+
25
+ export function normalizeHttpBaseUrl(value, { ensureV1 = true } = {}) {
26
+ const raw = String(value || "")
27
+ .trim()
28
+ .replace(/\/+$/, "");
29
+ if (!raw) throw new Error("Base URL is required");
30
+ const url = new URL(raw);
31
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
32
+ throw new Error("Base URL must start with http:// or https://");
33
+ }
34
+ if (!ensureV1) return raw;
35
+ return raw.endsWith("/v1") ? raw : `${raw}/v1`;
36
+ }
37
+
38
+ export function normalizeOpenAiBaseUrl(value) {
39
+ const base = normalizeHttpBaseUrl(value, { ensureV1: false })
40
+ .replace(/\/v1\/?$/, "")
41
+ .replace(/\/api\/?$/, "");
42
+ return `${base}/api/v1`;
43
+ }
44
+
45
+ export const normalizeCodexBaseUrl = normalizeOpenAiBaseUrl;
46
+
47
+ export function normalizeAnthropicBaseUrl(value) {
48
+ const raw = normalizeHttpBaseUrl(value, { ensureV1: false })
49
+ .replace(/\/api\/v1\/?$/, "")
50
+ .replace(/\/api\/?$/, "");
51
+ return raw.endsWith("/v1") ? raw : `${raw}/v1`;
52
+ }
53
+
54
+ export function deriveEndpointOptions(input = {}) {
55
+ const originBase = input.baseUrl
56
+ ? normalizeHttpBaseUrl(input.baseUrl, { ensureV1: false })
57
+ .replace(/\/api\/v1\/?$/, "")
58
+ .replace(/\/v1\/?$/, "")
59
+ .replace(/\/api\/?$/, "")
60
+ : null;
61
+
62
+ return {
63
+ anthropicBaseUrl: normalizeAnthropicBaseUrl(
64
+ input.anthropicBaseUrl || (originBase ? `${originBase}/v1` : undefined) || input.baseUrl
65
+ ),
66
+ openaiBaseUrl: normalizeOpenAiBaseUrl(
67
+ input.openaiBaseUrl || (originBase ? `${originBase}/api/v1` : undefined) || input.baseUrl
68
+ ),
69
+ };
70
+ }
71
+
72
+ export function stripV1(value) {
73
+ return normalizeHttpBaseUrl(value).replace(/\/v1\/?$/, "");
74
+ }
75
+
76
+ export async function readJson(filePath, fallback = {}) {
77
+ try {
78
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
79
+ } catch (error) {
80
+ if (error?.code === "ENOENT") return fallback;
81
+ return fallback;
82
+ }
83
+ }
84
+
85
+ export async function readText(filePath, fallback = "") {
86
+ try {
87
+ return await fs.readFile(filePath, "utf8");
88
+ } catch (error) {
89
+ if (error?.code === "ENOENT") return fallback;
90
+ return fallback;
91
+ }
92
+ }
93
+
94
+ export async function fileExists(filePath) {
95
+ try {
96
+ await fs.access(filePath);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ export async function backupFile(filePath, label = "omega-plus") {
104
+ if (!fsSync.existsSync(filePath)) return null;
105
+ const dir = path.dirname(filePath);
106
+ const backupDir = path.join(dir, ".omega-plus.bak");
107
+ await fs.mkdir(backupDir, { recursive: true });
108
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
109
+ const backupPath = path.join(backupDir, `${path.basename(filePath)}.${stamp}.${label}.bak`);
110
+ await fs.copyFile(filePath, backupPath);
111
+ return backupPath;
112
+ }
113
+
114
+ export async function writeTextFile(filePath, content, options = {}) {
115
+ if (options.dryRun) {
116
+ return { path: filePath, backupPath: null, dryRun: true, content };
117
+ }
118
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
119
+ const backupPath = await backupFile(filePath);
120
+ await fs.writeFile(filePath, content, "utf8");
121
+ return { path: filePath, backupPath, dryRun: false };
122
+ }
123
+
124
+ export async function writeJsonFile(filePath, value, options = {}) {
125
+ return writeTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`, options);
126
+ }
@@ -0,0 +1,72 @@
1
+ import { DEFAULT_VALIDATION_URL } from "./constants.mjs";
2
+
3
+ export class ApiKeyValidationError extends Error {
4
+ constructor(message, status = "invalid") {
5
+ super(message);
6
+ this.name = "ApiKeyValidationError";
7
+ this.status = status;
8
+ }
9
+ }
10
+
11
+ export function resolveValidationUrl(options = {}) {
12
+ if (options.validationUrl) return String(options.validationUrl).trim();
13
+ const base = String(options.baseUrl || "")
14
+ .trim()
15
+ .replace(/\/+$/, "");
16
+ if (!base) return DEFAULT_VALIDATION_URL;
17
+ const origin = base
18
+ .replace(/\/api\/v1\/?$/, "")
19
+ .replace(/\/v1\/?$/, "")
20
+ .replace(/\/api\/?$/, "");
21
+ return `${origin}/api/omega/validate-key`;
22
+ }
23
+
24
+ export async function validateApiKey(apiKey, options = {}) {
25
+ const trimmed = String(apiKey || "").trim();
26
+ if (!trimmed) {
27
+ throw new ApiKeyValidationError("API key is required", "invalid");
28
+ }
29
+
30
+ if (options.skipKeyValidation) {
31
+ return { valid: true, status: "skipped", message: "API key validation skipped" };
32
+ }
33
+
34
+ const url = resolveValidationUrl(options);
35
+ let response;
36
+ try {
37
+ response = await fetch(url, {
38
+ method: "POST",
39
+ headers: {
40
+ "content-type": "application/json",
41
+ authorization: `Bearer ${trimmed}`,
42
+ },
43
+ body: JSON.stringify({ apiKey: trimmed }),
44
+ });
45
+ } catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ throw new ApiKeyValidationError(
48
+ `Could not reach Omega validation server: ${message}`,
49
+ "unreachable"
50
+ );
51
+ }
52
+
53
+ let body = {};
54
+ try {
55
+ body = await response.json();
56
+ } catch {
57
+ body = {};
58
+ }
59
+
60
+ if (response.ok && body?.valid === true) {
61
+ return body;
62
+ }
63
+
64
+ const status = body?.status === "expired" ? "expired" : "invalid";
65
+ const message =
66
+ typeof body?.message === "string" && body.message.trim()
67
+ ? body.message.trim()
68
+ : status === "expired"
69
+ ? "API key expired"
70
+ : "Invalid API key";
71
+ throw new ApiKeyValidationError(message, status);
72
+ }
@@ -0,0 +1,61 @@
1
+ import readline from "node:readline";
2
+
3
+ export function createPromptInterface() {
4
+ return readline.createInterface({
5
+ input: process.stdin,
6
+ output: process.stdout,
7
+ });
8
+ }
9
+
10
+ export function ask(rl, question) {
11
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
12
+ }
13
+
14
+ export async function askSecret(question) {
15
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
16
+ const rl = createPromptInterface();
17
+ try {
18
+ return await ask(rl, question);
19
+ } finally {
20
+ rl.close();
21
+ }
22
+ }
23
+
24
+ process.stdout.write(question);
25
+ readline.emitKeypressEvents(process.stdin);
26
+ const wasRaw = process.stdin.isRaw;
27
+ process.stdin.setRawMode(true);
28
+
29
+ return new Promise((resolve, reject) => {
30
+ let value = "";
31
+ const cleanup = () => {
32
+ process.stdin.off("keypress", onKeypress);
33
+ process.stdin.setRawMode(wasRaw);
34
+ process.stdout.write("\n");
35
+ };
36
+ const onKeypress = (str, key) => {
37
+ if (key?.ctrl && key?.name === "c") {
38
+ cleanup();
39
+ reject(new Error("Cancelled"));
40
+ return;
41
+ }
42
+ if (key?.name === "return" || key?.name === "enter") {
43
+ cleanup();
44
+ resolve(value.trim());
45
+ return;
46
+ }
47
+ if (key?.name === "backspace") {
48
+ if (value.length > 0) {
49
+ value = value.slice(0, -1);
50
+ process.stdout.write("\b \b");
51
+ }
52
+ return;
53
+ }
54
+ if (str && !key?.ctrl && !key?.meta) {
55
+ value += str;
56
+ process.stdout.write("*");
57
+ }
58
+ };
59
+ process.stdin.on("keypress", onKeypress);
60
+ });
61
+ }
package/src/toml.mjs ADDED
@@ -0,0 +1,69 @@
1
+ export function parseToml(content) {
2
+ const result = { _root: {}, _sections: {} };
3
+ let currentSection = "_root";
4
+
5
+ for (const line of String(content || "").split(/\r?\n/)) {
6
+ const trimmed = line.trim();
7
+ if (!trimmed || trimmed.startsWith("#")) continue;
8
+
9
+ const sectionMatch = trimmed.match(/^\[(.+)]$/);
10
+ if (sectionMatch) {
11
+ currentSection = sectionMatch[1];
12
+ if (!result._sections[currentSection]) result._sections[currentSection] = {};
13
+ continue;
14
+ }
15
+
16
+ const kvMatch = trimmed.match(/^([^=]+)\s*=\s*(.+)$/);
17
+ if (!kvMatch) continue;
18
+
19
+ let key = kvMatch[1].trim();
20
+ if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
21
+ key = key.slice(1, -1);
22
+ }
23
+
24
+ const parsedValue = parseTomlValue(kvMatch[2].trim());
25
+ if (currentSection === "_root") result._root[key] = parsedValue;
26
+ else result._sections[currentSection][key] = parsedValue;
27
+ }
28
+
29
+ return result;
30
+ }
31
+
32
+ function parseTomlValue(rawValue) {
33
+ if (rawValue === "true") return true;
34
+ if (rawValue === "false") return false;
35
+ if (
36
+ (rawValue.startsWith('"') && rawValue.endsWith('"')) ||
37
+ (rawValue.startsWith("'") && rawValue.endsWith("'"))
38
+ ) {
39
+ return rawValue.slice(1, -1);
40
+ }
41
+ if (/^-?\d+$/.test(rawValue)) return Number.parseInt(rawValue, 10);
42
+ if (/^-?\d+\.\d+$/.test(rawValue)) return Number.parseFloat(rawValue);
43
+ return rawValue;
44
+ }
45
+
46
+ function formatTomlValue(value) {
47
+ if (typeof value === "boolean") return value ? "true" : "false";
48
+ if (typeof value === "number") return String(value);
49
+ if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) return value;
50
+ return JSON.stringify(String(value ?? ""));
51
+ }
52
+
53
+ export function stringifyToml(parsed) {
54
+ const lines = [];
55
+ for (const [key, value] of Object.entries(parsed._root || {})) {
56
+ lines.push(`${key} = ${formatTomlValue(value)}`);
57
+ }
58
+
59
+ for (const [section, values] of Object.entries(parsed._sections || {})) {
60
+ lines.push("");
61
+ lines.push(`[${section}]`);
62
+ for (const [key, value] of Object.entries(values || {})) {
63
+ const formattedKey = key.includes(".") ? JSON.stringify(key) : key;
64
+ lines.push(`${formattedKey} = ${formatTomlValue(value)}`);
65
+ }
66
+ }
67
+
68
+ return `${lines.join("\n")}\n`;
69
+ }
@@ -0,0 +1,42 @@
1
+ import path from "node:path";
2
+ import { configPath, getAppData, roamingConfigPath } from "./fs-utils.mjs";
3
+
4
+ export function getToolPaths(toolId, options = {}) {
5
+ switch (toolId) {
6
+ case "claude":
7
+ return {
8
+ settings: configPath([".claude", "settings.json"], options),
9
+ };
10
+ case "codex":
11
+ return {
12
+ config: configPath([".codex", "config.toml"], options),
13
+ auth: configPath([".codex", "auth.json"], options),
14
+ };
15
+ case "opencode":
16
+ return {
17
+ config:
18
+ process.platform === "win32"
19
+ ? path.join(getAppData(options), "opencode", "opencode.json")
20
+ : roamingConfigPath(["opencode", "opencode.json"], options),
21
+ };
22
+ case "cline":
23
+ return {
24
+ globalState: configPath([".cline", "data", "globalState.json"], options),
25
+ secrets: configPath([".cline", "data", "secrets.json"], options),
26
+ };
27
+ case "kilo":
28
+ return {
29
+ auth: configPath([".local", "share", "kilo", "auth.json"], options),
30
+ vscodeSettings:
31
+ process.platform === "win32"
32
+ ? path.join(getAppData(options), "Code", "User", "settings.json")
33
+ : roamingConfigPath(["Code", "User", "settings.json"], options),
34
+ };
35
+ case "droid":
36
+ return {
37
+ settings: configPath([".factory", "settings.json"], options),
38
+ };
39
+ default:
40
+ return {};
41
+ }
42
+ }
package/src/wizard.mjs ADDED
@@ -0,0 +1,160 @@
1
+ import {
2
+ DEFAULT_BASE_URL,
3
+ DEFAULT_FAST_MODEL,
4
+ DEFAULT_MODEL,
5
+ TOOL_DEFINITIONS,
6
+ } from "./constants.mjs";
7
+ import { printBanner } from "./banner.mjs";
8
+ import { configureTool } from "./config-writers.mjs";
9
+ import { detectAllTools, detectTool, runInstallCommand } from "./detector.mjs";
10
+ import { validateApiKey } from "./key-validator.mjs";
11
+ import { ask, askSecret, createPromptInterface } from "./prompts.mjs";
12
+
13
+ function printHeader() {
14
+ printBanner();
15
+ }
16
+
17
+ function statusLabel(tool) {
18
+ if (tool.installed && tool.configStatus === "configured") return "installed, configured";
19
+ if (tool.installed) return "installed";
20
+ return "not installed";
21
+ }
22
+
23
+ function printTools(tools) {
24
+ console.log("Select a coding tool:");
25
+ tools.forEach((tool, index) => {
26
+ console.log(` ${index + 1}. ${tool.name} (${statusLabel(tool)})`);
27
+ });
28
+ }
29
+
30
+ function resolveSelection(selection, tools) {
31
+ const trimmed = String(selection || "")
32
+ .trim()
33
+ .toLowerCase();
34
+ if (!trimmed) return null;
35
+ if (TOOL_DEFINITIONS[trimmed]) return trimmed;
36
+ const number = Number.parseInt(trimmed, 10);
37
+ if (Number.isInteger(number) && number >= 1 && number <= tools.length) {
38
+ return tools[number - 1].id;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ async function maybeInstallTool(toolId, options, rl) {
44
+ const tool = await detectTool(toolId);
45
+ if (tool?.installed) return tool;
46
+
47
+ console.log(`\n${tool.name} is not installed.`);
48
+ console.log(`Install command: ${tool.installHint}`);
49
+ if (options.noInstall || !tool.canAutoInstall) return tool;
50
+
51
+ const shouldInstall =
52
+ options.yes || (await ask(rl, "Install it now? [y/N] ")).toLowerCase() === "y";
53
+ if (!shouldInstall) return tool;
54
+
55
+ console.log(`\nInstalling ${tool.name}. Please wait...\n`);
56
+ await runInstallCommand(toolId);
57
+ return detectTool(toolId);
58
+ }
59
+
60
+ export async function runWizard(options = {}) {
61
+ printHeader();
62
+ const apiKey = options.apiKey || (await askSecret("Paste your Omega API key: "));
63
+ if (!apiKey) throw new Error("API key is required");
64
+
65
+ console.log("Checking your Omega API key...");
66
+ await validateApiKey(apiKey, {
67
+ baseUrl: options.baseUrl || DEFAULT_BASE_URL,
68
+ validationUrl: options.validationUrl,
69
+ skipKeyValidation: options.skipKeyValidation,
70
+ });
71
+ console.log(options.skipKeyValidation ? "API key validation skipped." : "API key verified.");
72
+
73
+ const rl = createPromptInterface();
74
+ try {
75
+ const tools = await detectAllTools();
76
+ printTools(tools);
77
+ const answer = options.tool || (await ask(rl, "\nEnter tool number: "));
78
+ const toolId = resolveSelection(answer, tools);
79
+ if (!toolId) throw new Error("Invalid tool selection");
80
+
81
+ const installedTool = await maybeInstallTool(toolId, options, rl);
82
+ if (!installedTool?.installed && !options.dryRun) {
83
+ console.log(`\nInstall ${installedTool.name} first, then rerun: npx @kesarcloud/omega-plus-cli`);
84
+ return { toolId, skipped: true, reason: "not_installed" };
85
+ }
86
+
87
+ console.log("\nPlease wait while Omega Plus configures your tool...");
88
+ const result = await configureTool(toolId, {
89
+ apiKey,
90
+ baseUrl: options.baseUrl || DEFAULT_BASE_URL,
91
+ openaiBaseUrl: options.openaiBaseUrl,
92
+ anthropicBaseUrl: options.anthropicBaseUrl,
93
+ model: options.model || DEFAULT_MODEL,
94
+ fastModel: options.fastModel || DEFAULT_FAST_MODEL,
95
+ dryRun: options.dryRun,
96
+ });
97
+
98
+ if (options.dryRun) {
99
+ console.log("\nDry run complete. No files were changed.");
100
+ result.files.forEach((file) => console.log(` would write: ${file.path}`));
101
+ return result;
102
+ }
103
+
104
+ console.log(`\n${result.message}`);
105
+ result.files.forEach((file) => {
106
+ console.log(` configured: ${file.path}`);
107
+ if (file.backupPath) console.log(` backup: ${file.backupPath}`);
108
+ });
109
+ console.log("\nYou can start using the selected coding tool with Omega Plus.");
110
+ return result;
111
+ } finally {
112
+ rl.close();
113
+ }
114
+ }
115
+
116
+ export async function runNonInteractiveConfigure(options = {}) {
117
+ if (!options.tool) throw new Error("--tool is required");
118
+ if (!options.apiKey) throw new Error("--api-key is required");
119
+ await validateApiKey(options.apiKey, {
120
+ baseUrl: options.baseUrl || DEFAULT_BASE_URL,
121
+ validationUrl: options.validationUrl,
122
+ skipKeyValidation: options.skipKeyValidation,
123
+ });
124
+ const result = await configureTool(options.tool, {
125
+ apiKey: options.apiKey,
126
+ baseUrl: options.baseUrl || DEFAULT_BASE_URL,
127
+ openaiBaseUrl: options.openaiBaseUrl,
128
+ anthropicBaseUrl: options.anthropicBaseUrl,
129
+ model: options.model || DEFAULT_MODEL,
130
+ fastModel: options.fastModel || DEFAULT_FAST_MODEL,
131
+ dryRun: options.dryRun,
132
+ });
133
+
134
+ if (options.dryRun) {
135
+ console.log("Dry run complete. No files were changed.");
136
+ result.files.forEach((file) => console.log(`would write: ${file.path}`));
137
+ } else {
138
+ console.log(result.message);
139
+ result.files.forEach((file) => console.log(`configured: ${file.path}`));
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ export async function printToolList() {
146
+ const tools = await detectAllTools();
147
+ printTools(tools);
148
+ return tools;
149
+ }
150
+
151
+ export async function printDoctor() {
152
+ const tools = await detectAllTools();
153
+ console.log("Omega Plus CLI Doctor\n");
154
+ for (const tool of tools) {
155
+ console.log(`${tool.name}: ${statusLabel(tool)}`);
156
+ console.log(` command: ${tool.command || "not found"}`);
157
+ console.log(` config: ${tool.configPath || "n/a"}`);
158
+ }
159
+ return tools;
160
+ }