noiton 1.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 NOITON
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,59 @@
1
+ # NOITON
2
+
3
+ OpenCode authenticator - configure providers with a single key.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx noiton
9
+ ```
10
+
11
+ The wizard will:
12
+ 1. Ask for your NOITON API key
13
+ 2. Validate it against the API
14
+ 3. Fetch available models
15
+ 4. Write the `noiton` provider to your `opencode.json`
16
+
17
+ ## Commands
18
+
19
+ ```bash
20
+ npx noiton # Setup wizard (default)
21
+ npx noiton status # Show current configuration
22
+ npx noiton refresh # Re-fetch models from API
23
+ npx noiton change # Change API key
24
+ npx noiton remove # Remove NOITON from config
25
+ npx noiton help # Show help
26
+ ```
27
+
28
+ ## How It Works
29
+
30
+ NOITON adds a provider to your `~/.config/opencode/opencode.json`:
31
+
32
+ ```json
33
+ {
34
+ "provider": {
35
+ "noiton": {
36
+ "npm": "@ai-sdk/openai-compatible",
37
+ "name": "NOITON",
38
+ "options": {
39
+ "baseURL": "https://blogs-litellm-app.rwezkp.easypanel.host",
40
+ "apiKey": "sk-your-key-here"
41
+ },
42
+ "models": {
43
+ "model-id": { "name": "Model Name", "limit": { ... } }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ After running `npx noiton`, restart OpenCode and the models will be available.
51
+
52
+ ## Requirements
53
+
54
+ - Node.js >= 18
55
+ - OpenCode CLI installed
56
+
57
+ ## License
58
+
59
+ MIT
package/bin/noiton.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../src/index.js";
3
+
4
+ const args = process.argv.slice(2);
5
+
6
+ run(args).catch((err) => {
7
+ console.error("\n ❌ Erro inesperado:", err.message);
8
+ process.exit(1);
9
+ });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "noiton",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode authenticator - configure providers with a single key",
5
+ "keywords": [
6
+ "opencode",
7
+ "auth",
8
+ "cli",
9
+ "noiton",
10
+ "provider",
11
+ "ai"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "NOITON",
15
+ "type": "module",
16
+ "bin": {
17
+ "noiton": "./bin/noiton.js"
18
+ },
19
+ "main": "./src/index.js",
20
+ "files": [
21
+ "bin/",
22
+ "src/",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "dependencies": {
30
+ "@inquirer/prompts": "^7.0.0"
31
+ },
32
+ "scripts": {
33
+ "test": "node --test tests/",
34
+ "prepublishOnly": "echo 'no build step - pure JS'"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org/"
39
+ }
40
+ }
@@ -0,0 +1,77 @@
1
+ import { input, confirm } from "@inquirer/prompts";
2
+ import { BANNER, color } from "../constants.js";
3
+ import { validateKey, maskKey } from "../lib/api.js";
4
+ import {
5
+ readConfig,
6
+ writeConfig,
7
+ hasProvider,
8
+ getProvider,
9
+ upsertProvider,
10
+ buildModels,
11
+ } from "../lib/config.js";
12
+
13
+ /**
14
+ * Change API key (re-runs wizard with new key).
15
+ */
16
+ export async function changeCommand() {
17
+ console.log(BANNER);
18
+
19
+ const { config, path } = readConfig();
20
+
21
+ if (!hasProvider(config)) {
22
+ console.log(color(" ❌ NOITON não está configurado.\n", "red"));
23
+ console.log(color(" Rode ", "dim") + color("npx noiton", "cyan") + color(" para configurar.\n", "dim"));
24
+ process.exit(1);
25
+ }
26
+
27
+ const provider = getProvider(config);
28
+ const oldKey = provider?.options?.apiKey || "";
29
+
30
+ console.log(color(" Chave atual: ", "dim") + maskKey(oldKey) + "\n");
31
+
32
+ const newKey = await input({
33
+ message: " Nova chave:",
34
+ validate: (v) => (v.trim().length >= 10 ? true : "Chave muito curta"),
35
+ transformer: (v) => maskKey(v),
36
+ });
37
+
38
+ const trimmedKey = newKey.trim();
39
+
40
+ console.log(color("\n ⏳ Validando nova chave...", "dim"));
41
+
42
+ const { valid, models, error } = await validateKey(trimmedKey);
43
+
44
+ if (!valid) {
45
+ console.log(color(`\n ❌ Erro: ${error}\n`, "red"));
46
+ process.exit(1);
47
+ }
48
+
49
+ console.log(color(" ✓ Chave válida!\n", "green"));
50
+
51
+ if (!models || models.length === 0) {
52
+ console.log(color(" ⚠ Nenhum modelo disponível para esta chave.\n", "yellow"));
53
+ process.exit(1);
54
+ }
55
+
56
+ console.log(color(` 📦 ${models.length} modelo(s) encontrado(s):\n`, "cyan"));
57
+ for (const m of models) {
58
+ console.log(` • ${m.id}`);
59
+ }
60
+
61
+ const proceed = await confirm({
62
+ message: "\n Atualizar configuração com a nova chave?",
63
+ default: true,
64
+ });
65
+
66
+ if (!proceed) {
67
+ console.log(color("\n ✋ Operação cancelada.\n", "dim"));
68
+ return;
69
+ }
70
+
71
+ const modelsObj = buildModels(models);
72
+ const newConfig = upsertProvider(config, trimmedKey, modelsObj);
73
+ writeConfig(newConfig, path);
74
+
75
+ console.log(color("\n ✓ Chave atualizada!\n", "green"));
76
+ console.log(color(" 🚀 Reinicie o OpenCode para usar a nova chave.\n", "cyan"));
77
+ }
@@ -0,0 +1,85 @@
1
+ import { input, confirm } from "@inquirer/prompts";
2
+ import { BANNER, color } from "../constants.js";
3
+ import { validateKey, maskKey } from "../lib/api.js";
4
+ import {
5
+ readConfig,
6
+ writeConfig,
7
+ hasProvider,
8
+ upsertProvider,
9
+ buildModels,
10
+ } from "../lib/config.js";
11
+
12
+ /**
13
+ * Main wizard command - interactive setup.
14
+ */
15
+ export async function initCommand() {
16
+ console.log(BANNER);
17
+
18
+ const { config, path } = readConfig();
19
+
20
+ if (hasProvider(config)) {
21
+ console.log(color(" ⚠ NOITON já está configurado!\n", "yellow"));
22
+ const overwrite = await confirm({
23
+ message: "Deseja sobrescrever a configuração existente?",
24
+ default: false,
25
+ });
26
+ if (!overwrite) {
27
+ console.log(color("\n ✋ Operação cancelada.\n", "dim"));
28
+ return;
29
+ }
30
+ }
31
+
32
+ console.log(color(" Cole sua chave NOITON para começar.\n", "dim"));
33
+
34
+ const apiKey = await input({
35
+ message: " Chave:",
36
+ validate: (v) => (v.trim().length >= 10 ? true : "Chave muito curta"),
37
+ transformer: (v) => maskKey(v),
38
+ });
39
+
40
+ const trimmedKey = apiKey.trim();
41
+
42
+ console.log(color("\n ⏳ Validando chave...", "dim"));
43
+
44
+ const { valid, models, error } = await validateKey(trimmedKey);
45
+
46
+ if (!valid) {
47
+ console.log(color(`\n ❌ Erro: ${error}\n`, "red"));
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log(color(" ✓ Chave válida!\n", "green"));
52
+
53
+ if (!models || models.length === 0) {
54
+ console.log(color(" ⚠ Nenhum modelo disponível para esta chave.\n", "yellow"));
55
+ process.exit(1);
56
+ }
57
+
58
+ console.log(color(` 📦 ${models.length} modelo(s) encontrado(s):\n`, "cyan"));
59
+ for (const m of models) {
60
+ const ctx = m.context_window
61
+ ? color(` (${Math.round(m.context_window / 1000)}K ctx)`, "dim")
62
+ : "";
63
+ console.log(` • ${m.id}${ctx}`);
64
+ }
65
+
66
+ const proceed = await confirm({
67
+ message: "\n Configurar OpenCode com estes modelos?",
68
+ default: true,
69
+ });
70
+
71
+ if (!proceed) {
72
+ console.log(color("\n ✋ Operação cancelada.\n", "dim"));
73
+ return;
74
+ }
75
+
76
+ console.log(color("\n ⏳ Configurando OpenCode...", "dim"));
77
+
78
+ const modelsObj = buildModels(models);
79
+ const newConfig = upsertProvider(config, trimmedKey, modelsObj);
80
+ writeConfig(newConfig, path);
81
+
82
+ console.log(color(" ✓ Provider 'noiton' adicionado!\n", "green"));
83
+ console.log(color(` 📁 Arquivo: ${path}\n`, "dim"));
84
+ console.log(color(" 🚀 Reinicie o OpenCode e use os modelos!\n", "cyan"));
85
+ }
@@ -0,0 +1,68 @@
1
+ import { BANNER, color } from "../constants.js";
2
+ import { validateKey } from "../lib/api.js";
3
+ import {
4
+ readConfig,
5
+ writeConfig,
6
+ hasProvider,
7
+ getProvider,
8
+ upsertProvider,
9
+ buildModels,
10
+ } from "../lib/config.js";
11
+
12
+ /**
13
+ * Re-fetch models from API and update config.
14
+ */
15
+ export async function refreshCommand() {
16
+ console.log(BANNER);
17
+
18
+ const { config, path } = readConfig();
19
+
20
+ if (!hasProvider(config)) {
21
+ console.log(color(" ❌ NOITON não está configurado.\n", "red"));
22
+ console.log(color(" Rode ", "dim") + color("npx noiton", "cyan") + color(" primeiro.\n", "dim"));
23
+ process.exit(1);
24
+ }
25
+
26
+ const provider = getProvider(config);
27
+ const apiKey = provider?.options?.apiKey;
28
+
29
+ if (!apiKey) {
30
+ console.log(color(" ❌ Chave API não encontrada na config.\n", "red"));
31
+ process.exit(1);
32
+ }
33
+
34
+ console.log(color(" ⏳ Buscando modelos atualizados...\n", "dim"));
35
+
36
+ const { valid, models, error } = await validateKey(apiKey);
37
+
38
+ if (!valid) {
39
+ console.log(color(`\n ❌ Erro: ${error}\n`, "red"));
40
+ console.log(color(" Dica: rode ", "dim") + color("npx noiton change", "cyan") + color(" para trocar a chave.\n", "dim"));
41
+ process.exit(1);
42
+ }
43
+
44
+ if (!models || models.length === 0) {
45
+ console.log(color(" ⚠ Nenhum modelo disponível.\n", "yellow"));
46
+ return;
47
+ }
48
+
49
+ const oldCount = Object.keys(provider.models || {}).length;
50
+ const newModels = buildModels(models);
51
+ const newCount = Object.keys(newModels).length;
52
+
53
+ console.log(color(" ✓ Modelos atualizados!\n", "green"));
54
+ console.log(color(` Antes: ${oldCount} modelo(s) → Agora: ${newCount} modelo(s)\n`, "dim"));
55
+
56
+ for (const m of models) {
57
+ const ctx = m.context_window
58
+ ? color(` (${Math.round(m.context_window / 1000)}K ctx)`, "dim")
59
+ : "";
60
+ console.log(` • ${m.id}${ctx}`);
61
+ }
62
+
63
+ const newConfig = upsertProvider(config, apiKey, newModels);
64
+ writeConfig(newConfig, path);
65
+
66
+ console.log(color("\n ✓ Config atualizada!\n", "green"));
67
+ console.log(color(" 🚀 Reinicie o OpenCode para usar os modelos atualizados.\n", "cyan"));
68
+ }
@@ -0,0 +1,38 @@
1
+ import { confirm } from "@inquirer/prompts";
2
+ import { BANNER, color } from "../constants.js";
3
+ import {
4
+ readConfig,
5
+ writeConfig,
6
+ hasProvider,
7
+ removeProvider,
8
+ } from "../lib/config.js";
9
+
10
+ /**
11
+ * Remove NOITON provider from config.
12
+ */
13
+ export async function removeCommand() {
14
+ console.log(BANNER);
15
+
16
+ const { config, path } = readConfig();
17
+
18
+ if (!hasProvider(config)) {
19
+ console.log(color(" ℹ️ NOITON não está configurado.\n", "yellow"));
20
+ return;
21
+ }
22
+
23
+ const proceed = await confirm({
24
+ message: " Remover provider NOITON do opencode.json?",
25
+ default: false,
26
+ });
27
+
28
+ if (!proceed) {
29
+ console.log(color("\n ✋ Operação cancelada.\n", "dim"));
30
+ return;
31
+ }
32
+
33
+ const newConfig = removeProvider(config);
34
+ writeConfig(newConfig, path);
35
+
36
+ console.log(color("\n ✓ Provider NOITON removido!\n", "green"));
37
+ console.log(color(` 📁 Arquivo: ${path}\n`, "dim"));
38
+ }
@@ -0,0 +1,41 @@
1
+ import { BANNER, color } from "../constants.js";
2
+ import { maskKey } from "../lib/api.js";
3
+ import { readConfig, hasProvider, getProvider } from "../lib/config.js";
4
+
5
+ /**
6
+ * Show current NOITON configuration.
7
+ */
8
+ export async function statusCommand() {
9
+ console.log(BANNER);
10
+
11
+ const { config, path } = readConfig();
12
+
13
+ if (!hasProvider(config)) {
14
+ console.log(color(" ❌ NOITON não está configurado.\n", "red"));
15
+ console.log(color(" Rode ", "dim") + color("npx noiton", "cyan") + color(" para configurar.\n", "dim"));
16
+ return;
17
+ }
18
+
19
+ const provider = getProvider(config);
20
+ const apiKey = provider?.options?.apiKey || "";
21
+ const baseURL = provider?.options?.baseURL || "";
22
+ const models = provider?.models || {};
23
+ const modelCount = Object.keys(models).length;
24
+
25
+ console.log(color(" ✅ NOITON está configurado!\n\n", "green"));
26
+ console.log(color(" 📁 Config: ", "dim") + path);
27
+ console.log(color(" 🌐 Base URL: ", "dim") + baseURL);
28
+ console.log(color(" 🔑 Chave: ", "dim") + maskKey(apiKey));
29
+ console.log(color(` 📦 Modelos (${modelCount}):\n`, "dim"));
30
+
31
+ for (const [id, model] of Object.entries(models)) {
32
+ const ctx = model?.limit?.context
33
+ ? Math.round(model.limit.context / 1000) + "K"
34
+ : "?";
35
+ const out = model?.limit?.output
36
+ ? Math.round(model.limit.output / 1000) + "K"
37
+ : "?";
38
+ console.log(` • ${id} ` + color(`[${ctx} ctx / ${out} out]`, "dim"));
39
+ }
40
+ console.log("");
41
+ }
@@ -0,0 +1,26 @@
1
+ export const VERSION = "1.0.0";
2
+ export const PROVIDER_ID = "noiton";
3
+ export const PROVIDER_NAME = "NOITON";
4
+ export const BASE_URL = "https://blogs-litellm-app.rwezkp.easypanel.host";
5
+ export const NPM_PACKAGE = "@ai-sdk/openai-compatible";
6
+
7
+ export const BANNER = `
8
+ ╔══════════════════════════════════════════╗
9
+ ║ NOITON - OpenCode Authenticator v${VERSION} ║
10
+ ╚══════════════════════════════════════════╝
11
+ `;
12
+
13
+ export const COLORS = {
14
+ reset: "\x1b[0m",
15
+ bold: "\x1b[1m",
16
+ dim: "\x1b[2m",
17
+ red: "\x1b[31m",
18
+ green: "\x1b[32m",
19
+ yellow: "\x1b[33m",
20
+ blue: "\x1b[34m",
21
+ cyan: "\x1b[36m",
22
+ };
23
+
24
+ export function color(text, c) {
25
+ return `${COLORS[c] || ""}${text}${COLORS.reset}`;
26
+ }
package/src/index.js ADDED
@@ -0,0 +1,77 @@
1
+ import { BANNER, VERSION, color } from "./constants.js";
2
+ import { initCommand } from "./commands/init.js";
3
+ import { statusCommand } from "./commands/status.js";
4
+ import { refreshCommand } from "./commands/refresh.js";
5
+ import { changeCommand } from "./commands/change.js";
6
+ import { removeCommand } from "./commands/remove.js";
7
+
8
+ const HELP = `
9
+ ${color("NOITON", "cyan")} ${color("v" + VERSION, "dim")} - OpenCode Authenticator
10
+
11
+ ${color("USAGE", "bold")}
12
+ npx noiton Wizard interativo (padrão)
13
+ npx noiton init Mesmo que acima
14
+ npx noiton status Mostra configuração atual
15
+ npx noiton refresh Re-busca modelos da API
16
+ npx noiton change Troca a chave API
17
+ npx noiton remove Remove provider NOITON
18
+ npx noiton help, --help Mostra esta ajuda
19
+ npx noiton --version Mostra versão
20
+
21
+ ${color("EXEMPLOS", "bold")}
22
+ npx noiton # configura pela primeira vez
23
+ npx noiton refresh # atualiza lista de modelos
24
+ npx noiton status # ver o que está configurado
25
+
26
+ ${color("MAIS INFO", "bold")}
27
+ GitHub: https://github.com/tokensilimitados/noiton
28
+ `;
29
+
30
+ /**
31
+ * Main entry point - parses args and routes to commands.
32
+ * @param {string[]} args
33
+ */
34
+ export async function run(args) {
35
+ const command = args[0] || "init";
36
+
37
+ // Handle help/version flags
38
+ if (["--help", "-h", "help"].includes(command)) {
39
+ console.log(HELP);
40
+ return;
41
+ }
42
+
43
+ if (["--version", "-v"].includes(command)) {
44
+ console.log(VERSION);
45
+ return;
46
+ }
47
+
48
+ // Route to command
49
+ switch (command) {
50
+ case "init":
51
+ case "login":
52
+ case "add":
53
+ await initCommand();
54
+ break;
55
+ case "status":
56
+ case "show":
57
+ await statusCommand();
58
+ break;
59
+ case "refresh":
60
+ case "update":
61
+ await refreshCommand();
62
+ break;
63
+ case "change":
64
+ case "new-key":
65
+ await changeCommand();
66
+ break;
67
+ case "remove":
68
+ case "rm":
69
+ case "delete":
70
+ await removeCommand();
71
+ break;
72
+ default:
73
+ console.error(color(`\n ❌ Comando desconhecido: ${command}\n`, "red"));
74
+ console.log(HELP);
75
+ process.exit(1);
76
+ }
77
+ }
package/src/lib/api.js ADDED
@@ -0,0 +1,83 @@
1
+ import { BASE_URL } from "../constants.js";
2
+
3
+ /**
4
+ * Validate API key by fetching /v1/models
5
+ * @param {string} apiKey
6
+ * @returns {Promise<{ valid: boolean, models?: Array, error?: string }>}
7
+ */
8
+ export async function validateKey(apiKey) {
9
+ try {
10
+ const res = await fetch(`${BASE_URL}/v1/models`, {
11
+ headers: {
12
+ Authorization: `Bearer ${apiKey}`,
13
+ },
14
+ signal: AbortSignal.timeout(10000),
15
+ });
16
+
17
+ if (res.status === 401 || res.status === 403) {
18
+ return { valid: false, error: "Chave inválida ou sem permissão" };
19
+ }
20
+
21
+ if (!res.ok) {
22
+ return { valid: false, error: `Erro ${res.status} ao validar chave` };
23
+ }
24
+
25
+ const data = await res.json();
26
+
27
+ if (!data?.data || !Array.isArray(data.data)) {
28
+ return { valid: false, error: "Resposta inválida da API" };
29
+ }
30
+
31
+ return { valid: true, models: data.data };
32
+ } catch (err) {
33
+ if (err.name === "TimeoutError") {
34
+ return { valid: false, error: "Timeout - verifique sua conexão" };
35
+ }
36
+ return { valid: false, error: err.message };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Test a single model with a quick chat completion
42
+ * @param {string} apiKey
43
+ * @param {string} modelId
44
+ * @returns {Promise<{ ok: boolean, error?: string }>}
45
+ */
46
+ export async function testModel(apiKey, modelId) {
47
+ try {
48
+ const res = await fetch(`${BASE_URL}/v1/chat/completions`, {
49
+ method: "POST",
50
+ headers: {
51
+ Authorization: `Bearer ${apiKey}`,
52
+ "Content-Type": "application/json",
53
+ },
54
+ body: JSON.stringify({
55
+ model: modelId,
56
+ messages: [{ role: "user", content: "hi" }],
57
+ max_tokens: 5,
58
+ }),
59
+ signal: AbortSignal.timeout(15000),
60
+ });
61
+
62
+ if (!res.ok) {
63
+ const body = await res.json().catch(() => ({}));
64
+ return { ok: false, error: body?.error?.message || `HTTP ${res.status}` };
65
+ }
66
+
67
+ return { ok: true };
68
+ } catch (err) {
69
+ return { ok: false, error: err.message };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Mask API key for display (sk-xxx...xxxx)
75
+ * @param {string} key
76
+ * @returns {string}
77
+ */
78
+ export function maskKey(key) {
79
+ if (!key || key.length < 10) return "***";
80
+ const start = key.slice(0, 6);
81
+ const end = key.slice(-4);
82
+ return `${start}...${end}`;
83
+ }
@@ -0,0 +1,160 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import {
5
+ PROVIDER_ID,
6
+ PROVIDER_NAME,
7
+ BASE_URL,
8
+ NPM_PACKAGE,
9
+ } from "../constants.js";
10
+
11
+ /**
12
+ * Resolve the opencode.json config path.
13
+ * Priority: env OPENCODE_CONFIG > ~/.config/opencode/opencode.json
14
+ * @returns {string}
15
+ */
16
+ export function getConfigPath() {
17
+ if (process.env.OPENCODE_CONFIG) {
18
+ return process.env.OPENCODE_CONFIG;
19
+ }
20
+ const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
21
+ return join(xdg, "opencode", "opencode.json");
22
+ }
23
+
24
+ /**
25
+ * Read opencode.json. Returns empty object if missing or invalid.
26
+ * @param {string} [configPath]
27
+ * @returns {{ config: object, path: string }}
28
+ */
29
+ export function readConfig(configPath) {
30
+ const path = configPath || getConfigPath();
31
+ if (!existsSync(path)) {
32
+ return { config: {}, path };
33
+ }
34
+ try {
35
+ const raw = readFileSync(path, "utf8");
36
+ return { config: JSON.parse(raw), path };
37
+ } catch {
38
+ return { config: {}, path };
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Write opencode.json preserving existing content.
44
+ * @param {object} config
45
+ * @param {string} [configPath]
46
+ */
47
+ export function writeConfig(config, configPath) {
48
+ const path = configPath || getConfigPath();
49
+ const dir = dirname(path);
50
+ if (!existsSync(dir)) {
51
+ mkdirSync(dir, { recursive: true });
52
+ }
53
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf8");
54
+ }
55
+
56
+ /**
57
+ * Check if noiton provider exists in config.
58
+ * @param {object} config
59
+ * @returns {boolean}
60
+ */
61
+ export function hasProvider(config) {
62
+ return Boolean(config?.provider?.[PROVIDER_ID]);
63
+ }
64
+
65
+ /**
66
+ * Get noiton provider config.
67
+ * @param {object} config
68
+ * @returns {object|null}
69
+ */
70
+ export function getProvider(config) {
71
+ return config?.provider?.[PROVIDER_ID] ?? null;
72
+ }
73
+
74
+ /**
75
+ * Build models object from API response.
76
+ * @param {Array} models - Array of { id, context_window?, ... }
77
+ * @param {object} [defaults]
78
+ * @returns {object}
79
+ */
80
+ export function buildModels(models, defaults = {}) {
81
+ const defaultContext = defaults.context || 32768;
82
+ const defaultOutput = defaults.output || 4096;
83
+
84
+ const result = {};
85
+ for (const m of models) {
86
+ const id = m.id;
87
+ if (!id) continue;
88
+ result[id] = {
89
+ name: prettifyName(id),
90
+ limit: {
91
+ context: m.context_window || defaultContext,
92
+ output: defaultOutput,
93
+ },
94
+ };
95
+ }
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Convert model id to human-readable name.
101
+ * deepseek-v4-flash-free -> DeepSeek V4 Flash Free
102
+ */
103
+ function prettifyName(id) {
104
+ return id
105
+ .replace(/[@/]/g, " ")
106
+ .split(/[-_]/)
107
+ .filter(Boolean)
108
+ .map((w) => {
109
+ const lower = w.toLowerCase();
110
+ if (["glm", "api", "v4", "v3", "v2", "v5", "gpt", "m3"].includes(lower)) {
111
+ return w.toUpperCase();
112
+ }
113
+ return w.charAt(0).toUpperCase() + w.slice(1);
114
+ })
115
+ .join(" ");
116
+ }
117
+
118
+ /**
119
+ * Build the full noiton provider object.
120
+ * @param {string} apiKey
121
+ * @param {object} models - from buildModels()
122
+ */
123
+ export function buildProvider(apiKey, models) {
124
+ return {
125
+ npm: NPM_PACKAGE,
126
+ name: PROVIDER_NAME,
127
+ options: {
128
+ baseURL: BASE_URL,
129
+ apiKey,
130
+ },
131
+ models,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Insert/replace noiton provider in config (preserves other providers).
137
+ * @param {object} config
138
+ * @param {string} apiKey
139
+ * @param {object} models
140
+ * @returns {object} new config
141
+ */
142
+ export function upsertProvider(config, apiKey, models) {
143
+ const next = { ...config };
144
+ if (!next.provider) next.provider = {};
145
+ next.provider[PROVIDER_ID] = buildProvider(apiKey, models);
146
+ return next;
147
+ }
148
+
149
+ /**
150
+ * Remove noiton provider from config.
151
+ * @param {object} config
152
+ * @returns {object} new config
153
+ */
154
+ export function removeProvider(config) {
155
+ const next = { ...config };
156
+ if (next.provider && PROVIDER_ID in next.provider) {
157
+ delete next.provider[PROVIDER_ID];
158
+ }
159
+ return next;
160
+ }