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 +21 -0
- package/README.md +59 -0
- package/bin/noiton.js +9 -0
- package/package.json +40 -0
- package/src/commands/change.js +77 -0
- package/src/commands/init.js +85 -0
- package/src/commands/refresh.js +68 -0
- package/src/commands/remove.js +38 -0
- package/src/commands/status.js +41 -0
- package/src/constants.js +26 -0
- package/src/index.js +77 -0
- package/src/lib/api.js +83 -0
- package/src/lib/config.js +160 -0
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
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
|
+
}
|
package/src/constants.js
ADDED
|
@@ -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
|
+
}
|