noiton 2.0.1 → 2.0.2

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/README.md CHANGED
@@ -11,7 +11,7 @@ npx noiton
11
11
  ```
12
12
 
13
13
  The octopus will:
14
- 1. 🔍 Detect which AI agents are installed (OpenCode, Hermes, OpenClaw, Cline, Aider, Kilo Code, Kimi Code)
14
+ 1. 🔍 Detect which AI agents are installed (OpenCode, Hermes)
15
15
  2. 📦 If none found, offer to install OpenCode or Hermes based on your use case
16
16
  3. 🔄 If OpenCode is outdated, offer to update it
17
17
  4. 🔑 Ask for your NOITON API key (one key serves all agents)
@@ -25,11 +25,6 @@ The octopus will:
25
25
  |-------|------|---------------|-----------------|
26
26
  | **OpenCode** | CLI | JSON (`opencode.json`) | ✅ |
27
27
  | **Hermes** | CLI | YAML (`config.yaml` + `.env`) | Single default |
28
- | **OpenClaw** | CLI | JSON5 (`openclaw.json`) | ✅ |
29
- | **Cline** | VS Code ext | JSON (VS Code settings) | Single |
30
- | **Aider** | CLI | YAML (`.aider.conf.yml` + `.env`) | Single |
31
- | **Kilo Code** | CLI + VS Code | JSONC (`kilo.jsonc`) | ✅ |
32
- | **Kimi Code** | CLI + VS Code | TOML (`config.toml`) | Single |
33
28
 
34
29
  ## Commands
35
30
 
@@ -45,15 +40,10 @@ npx noiton --version # Show version
45
40
 
46
41
  ## How It Works
47
42
 
48
- NOITON detects your installed agents and writes to each one's native config format:
43
+ NOITON detects your installed agents by checking if the CLI binary is on PATH, then writes to each one's native config format:
49
44
 
50
45
  - **OpenCode** → `~/.config/opencode/opencode.json` (provider block)
51
46
  - **Hermes** → `~/.hermes/config.yaml` (model block) + `~/.hermes/.env` (API key)
52
- - **OpenClaw** → `~/.openclaw/openclaw.json` (models.providers block)
53
- - **Cline** → VS Code `settings.json` (cline.* keys)
54
- - **Aider** → `~/.aider.conf.yml` (openai-api-base) + `~/.env` (OPENAI_API_KEY)
55
- - **Kilo Code** → `~/.config/kilo/kilo.jsonc` (provider.openai-compatible block)
56
- - **Kimi Code** → `~/.kimi-code/config.toml` (providers + models blocks)
57
47
 
58
48
  Existing configurations are preserved — NOITON only adds/removes its own entries.
59
49
 
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "noiton",
3
- "version": "2.0.1",
4
- "description": "🐙 Universal AI Agent Connector - one key, many agents (OpenCode, Hermes, OpenClaw, Cline, Aider, Kilo, Kimi)",
3
+ "version": "2.0.2",
4
+ "description": "🐙 Universal AI Agent Connector - one key, many agents (OpenCode, Hermes)",
5
5
  "keywords": [
6
6
  "noiton",
7
7
  "opencode",
8
8
  "hermes",
9
- "openclaw",
10
- "cline",
11
- "aider",
12
- "kilo",
13
- "kimi",
14
9
  "ai",
15
10
  "cli",
16
11
  "agent",
package/src/index.js CHANGED
@@ -23,11 +23,6 @@ ${c.bold("USAGE")}
23
23
  ${c.bold("AGENTES SUPORTADOS")}
24
24
  ${c.cyan("•")} OpenCode Programador IA (CLI)
25
25
  ${c.cyan("•")} Hermes Assistente de tarefas (CLI)
26
- ${c.cyan("•")} OpenClaw Agente open-source (CLI)
27
- ${c.cyan("•")} Cline Extensão VS Code
28
- ${c.cyan("•")} Aider Pair programmer de terminal
29
- ${c.cyan("•")} Kilo Code CLI + extensão VS Code
30
- ${c.cyan("•")} Kimi Code CLI + extensão VS Code
31
26
 
32
27
  ${c.bold("EXEMPLOS")}
33
28
  npx noiton # configura pela primeira vez
@@ -14,6 +14,7 @@
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
15
15
  import { join, dirname } from "node:path";
16
16
  import { homedir } from "node:os";
17
+ import { execSync } from "node:child_process";
17
18
  import YAML from "yaml";
18
19
  import { BASE_URL } from "../../constants.js";
19
20
 
@@ -22,6 +23,19 @@ const CONFIG_PATH = join(HOME, ".hermes", "config.yaml");
22
23
  const ENV_PATH = join(HOME, ".hermes", ".env");
23
24
  const ENV_KEY = "NOITON_API_KEY";
24
25
 
26
+ /**
27
+ * Check if hermes binary exists on PATH.
28
+ * @returns {boolean}
29
+ */
30
+ function hasHermesBin() {
31
+ try {
32
+ execSync(`command -v hermes 2>/dev/null`, { stdio: "ignore" });
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
25
39
  function readYaml() {
26
40
  if (!existsSync(CONFIG_PATH)) return { config: {}, path: CONFIG_PATH };
27
41
  try {
@@ -75,8 +89,13 @@ export const hermesAgent = {
75
89
  name: "Hermes",
76
90
  supportsMultipleModels: false,
77
91
 
92
+ /**
93
+ * Detect if Hermes is really installed.
94
+ * Requires the binary on PATH - config files alone aren't enough
95
+ * (could be leftover from uninstalled tools).
96
+ */
78
97
  detect() {
79
- return existsSync(CONFIG_PATH) || existsSync(ENV_PATH);
98
+ return hasHermesBin();
80
99
  },
81
100
 
82
101
  isConfigured() {
@@ -6,24 +6,38 @@
6
6
  * configure, changeKey, refresh, remove }
7
7
  */
8
8
 
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync } from "node:fs";
9
11
  import { opencodeAgent } from "./opencode.js";
10
12
  import { hermesAgent } from "./hermes.js";
11
- import { openclawAgent } from "./openclaw.js";
12
- import { clineAgent } from "./cline.js";
13
- import { aiderAgent } from "./aider.js";
14
- import { kiloAgent } from "./kilo.js";
15
- import { kimiAgent } from "./kimi.js";
16
13
 
17
14
  export const REGISTRY = [
18
15
  opencodeAgent,
19
16
  hermesAgent,
20
- openclawAgent,
21
- clineAgent,
22
- aiderAgent,
23
- kiloAgent,
24
- kimiAgent,
25
17
  ];
26
18
 
19
+ /**
20
+ * Get version of a CLI binary (best-effort).
21
+ * @param {string} bin
22
+ * @returns {string|null}
23
+ */
24
+ function getBinVersion(bin) {
25
+ for (const flag of ["--version", "-v"]) {
26
+ try {
27
+ const out = execSync(`${bin} ${flag} 2>/dev/null`, {
28
+ encoding: "utf8",
29
+ timeout: 5000,
30
+ }).trim();
31
+ const match = out.match(/(\d+\.\d+(?:\.\d+)?(?:-\w+)?)/);
32
+ if (match) return match[1];
33
+ return out.split("\n")[0];
34
+ } catch {
35
+ // try next flag
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
27
41
  /**
28
42
  * Get an agent adapter by ID.
29
43
  * @param {string} id
@@ -35,10 +49,26 @@ export function getAgent(id) {
35
49
 
36
50
  /**
37
51
  * Get all agents that are detected as installed.
52
+ * Enriches each adapter with version and configFound info.
38
53
  * @returns {Array}
39
54
  */
40
55
  export function getDetectedAgents() {
41
- return REGISTRY.filter((a) => a.detect());
56
+ return REGISTRY.filter((a) => a.detect()).map((a) => {
57
+ // Try to get version from binary
58
+ let version = null;
59
+ const bins = { opencode: "opencode", hermes: "hermes" };
60
+ const bin = bins[a.id];
61
+ if (bin) {
62
+ version = getBinVersion(bin);
63
+ }
64
+ // Check if config file exists
65
+ let configFound = false;
66
+ try {
67
+ const status = a.getStatus();
68
+ configFound = Boolean(status?.path && existsSync(status.path));
69
+ } catch {}
70
+ return { ...a, version, configFound };
71
+ });
42
72
  }
43
73
 
44
74
  /**
@@ -56,7 +86,7 @@ export function getConfiguredAgents() {
56
86
  }
57
87
 
58
88
  /**
59
- * Get status of all detected agents.
89
+ * Get status of all agents.
60
90
  * @returns {Array}
61
91
  */
62
92
  export function getAllStatuses() {
@@ -8,6 +8,7 @@
8
8
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
9
9
  import { join, dirname } from "node:path";
10
10
  import { homedir } from "node:os";
11
+ import { execSync } from "node:child_process";
11
12
  import {
12
13
  PROVIDER_ID,
13
14
  PROVIDER_NAME,
@@ -18,6 +19,19 @@ import {
18
19
  const HOME = homedir();
19
20
  const XDG = process.env.XDG_CONFIG_HOME || join(HOME, ".config");
20
21
 
22
+ /**
23
+ * Check if opencode binary exists on PATH.
24
+ * @returns {boolean}
25
+ */
26
+ function hasOpencodeBin() {
27
+ try {
28
+ execSync(`command -v opencode 2>/dev/null`, { stdio: "ignore" });
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
21
35
  /** @returns {string} config path */
22
36
  export function getConfigPath() {
23
37
  if (process.env.OPENCODE_CONFIG) return process.env.OPENCODE_CONFIG;
@@ -116,8 +130,12 @@ export const opencodeAgent = {
116
130
  name: "OpenCode",
117
131
  supportsMultipleModels: true,
118
132
 
133
+ /**
134
+ * Detect if OpenCode is really installed.
135
+ * Requires the binary on PATH - config files alone aren't enough.
136
+ */
119
137
  detect() {
120
- return existsSync(getConfigPath());
138
+ return hasOpencodeBin();
121
139
  },
122
140
 
123
141
  isConfigured() {
package/src/lib/detect.js CHANGED
@@ -1,18 +1,15 @@
1
1
  /**
2
2
  * NOITON Agent Detection
3
3
  *
4
- * Detects which AI coding agents are installed on the system by:
5
- * 1. Checking if the CLI binary is on PATH (which)
6
- * 2. Checking if the config file exists
7
- *
8
- * Returns a list of detected agents with metadata.
4
+ * Detects which AI coding agents are installed on the system.
5
+ * Detection requires the CLI binary to be on PATH (not just config files,
6
+ * which can be leftover from uninstalled tools).
9
7
  */
10
8
 
11
9
  import { existsSync } from "node:fs";
12
10
  import { join } from "node:path";
13
11
  import { homedir } from "node:os";
14
12
  import { execSync } from "node:child_process";
15
- import { c } from "../constants.js";
16
13
 
17
14
  const HOME = homedir();
18
15
  const XDG = process.env.XDG_CONFIG_HOME || join(HOME, ".config");
@@ -24,7 +21,7 @@ const XDG = process.env.XDG_CONFIG_HOME || join(HOME, ".config");
24
21
  */
25
22
  function hasBin(bin) {
26
23
  try {
27
- execSync(`which ${bin} 2>/dev/null`, { stdio: "ignore" });
24
+ execSync(`command -v ${bin} 2>/dev/null`, { stdio: "ignore" });
28
25
  return true;
29
26
  } catch {
30
27
  return false;
@@ -44,7 +41,6 @@ function getBinVersion(bin) {
44
41
  encoding: "utf8",
45
42
  timeout: 5000,
46
43
  }).trim();
47
- // Extract first version-like token
48
44
  const match = out.match(/(\d+\.\d+(?:\.\d+)?(?:-\w+)?)/);
49
45
  if (match) return match[1];
50
46
  return out.split("\n")[0];
@@ -76,8 +72,6 @@ function getNpmLatest(pkg) {
76
72
  /**
77
73
  * Compare two semver-like version strings.
78
74
  * Returns: 1 if a > b, -1 if a < b, 0 if equal.
79
- * @param {string} a
80
- * @param {string} b
81
75
  */
82
76
  function compareVersions(a, b) {
83
77
  const pa = a.split(".").map(Number);
@@ -93,10 +87,6 @@ function compareVersions(a, b) {
93
87
 
94
88
  // ============= AGENT DEFINITIONS =============
95
89
 
96
- /**
97
- * Registry of all supported agents.
98
- * Each entry describes how to detect the agent and where its config lives.
99
- */
100
90
  export const AGENT_REGISTRY = [
101
91
  {
102
92
  id: "opencode",
@@ -125,109 +115,42 @@ export const AGENT_REGISTRY = [
125
115
  description: "Assistente que faz tarefas e se integra com apps",
126
116
  useCase: "assistant",
127
117
  },
128
- {
129
- id: "openclaw",
130
- name: "OpenClaw",
131
- type: "CLI",
132
- bin: "openclaw",
133
- npmPkg: null,
134
- installMethod: null,
135
- installCmd: null,
136
- configPath: join(HOME, ".openclaw", "openclaw.json"),
137
- supportsMultipleModels: true,
138
- description: "Agente de IA open-source com providers customizáveis",
139
- useCase: "programming",
140
- },
141
- {
142
- id: "cline",
143
- name: "Cline",
144
- type: "VSCode Extension",
145
- bin: null,
146
- npmPkg: null,
147
- installMethod: null,
148
- installCmd: null,
149
- configPath: join(XDG, "Code", "User", "settings.json"),
150
- supportsMultipleModels: true,
151
- description: "Extensão VS Code para programação com IA",
152
- useCase: "programming",
153
- },
154
- {
155
- id: "aider",
156
- name: "Aider",
157
- type: "CLI",
158
- bin: "aider",
159
- npmPkg: null,
160
- installMethod: "pip",
161
- installCmd: "pip install aider-chat",
162
- configPath: join(HOME, ".aider.conf.yml"),
163
- supportsMultipleModels: false,
164
- description: "Pair programmer de terminal que edita código com você",
165
- useCase: "programming",
166
- },
167
- {
168
- id: "kilo",
169
- name: "Kilo Code",
170
- type: "CLI + VSCode",
171
- bin: "kilo",
172
- npmPkg: "@kilocode/cli",
173
- installMethod: "npm",
174
- installCmd: "npm install -g @kilocode/cli",
175
- configPath: join(XDG, "kilo", "kilo.jsonc"),
176
- supportsMultipleModels: true,
177
- description: "Agente de IA com CLI e extensão VS Code",
178
- useCase: "programming",
179
- },
180
- {
181
- id: "kimi",
182
- name: "Kimi Code",
183
- type: "CLI + VSCode",
184
- bin: "kimi",
185
- npmPkg: null,
186
- installMethod: null,
187
- installCmd: null,
188
- configPath: join(HOME, ".kimi-code", "config.toml"),
189
- supportsMultipleModels: false,
190
- description: "Agente de IA da Moonshot com CLI e VS Code",
191
- useCase: "programming",
192
- },
193
118
  ];
194
119
 
195
120
  /**
196
121
  * Detect all installed agents.
122
+ * An agent is "installed" only if its binary is on PATH.
123
+ * Config files alone are NOT enough (could be leftover from uninstalled tools).
197
124
  * @returns {Array} detected agents with status info
198
125
  */
199
126
  export function detectAgents() {
200
127
  const detected = [];
201
128
  for (const agent of AGENT_REGISTRY) {
202
- const binFound = agent.bin ? hasBin(agent.bin) : false;
203
- const configFound = existsSync(agent.configPath);
204
- const installed = binFound || configFound;
205
-
206
- if (installed) {
207
- const version = binFound ? getBinVersion(agent.bin) : null;
208
- detected.push({
209
- ...agent,
210
- installed: true,
211
- version,
212
- binFound,
213
- configFound,
214
- });
215
- }
129
+ if (!agent.bin) continue;
130
+ const binFound = hasBin(agent.bin);
131
+ if (!binFound) continue;
132
+
133
+ const version = getBinVersion(agent.bin);
134
+ detected.push({
135
+ ...agent,
136
+ installed: true,
137
+ version,
138
+ binFound: true,
139
+ configFound: existsSync(agent.configPath),
140
+ });
216
141
  }
217
142
  return detected;
218
143
  }
219
144
 
220
145
  /**
221
- * Check if a specific agent is installed.
146
+ * Check if a specific agent is installed (binary on PATH).
222
147
  * @param {string} id
223
148
  * @returns {boolean}
224
149
  */
225
150
  export function isAgentInstalled(id) {
226
151
  const agent = AGENT_REGISTRY.find((a) => a.id === id);
227
- if (!agent) return false;
228
- const binFound = agent.bin ? hasBin(agent.bin) : false;
229
- const configFound = existsSync(agent.configPath);
230
- return binFound || configFound;
152
+ if (!agent || !agent.bin) return false;
153
+ return hasBin(agent.bin);
231
154
  }
232
155
 
233
156
  /**
@@ -240,13 +163,10 @@ export function getOpenCodeStatus() {
240
163
  if (!agent) return null;
241
164
 
242
165
  const binFound = hasBin("opencode");
243
- const configFound = existsSync(agent.configPath);
244
- const installed = binFound || configFound;
166
+ if (!binFound) return null;
245
167
 
246
- if (!installed) return null;
247
-
248
- const version = binFound ? getBinVersion("opencode") : null;
249
- const latest = binFound ? getNpmLatest("opencode-ai") : null;
168
+ const version = getBinVersion("opencode");
169
+ const latest = getNpmLatest("opencode-ai");
250
170
  const updateAvailable =
251
171
  version && latest ? compareVersions(latest, version) > 0 : false;
252
172
 
@@ -256,8 +176,29 @@ export function getOpenCodeStatus() {
256
176
  version,
257
177
  latest,
258
178
  updateAvailable,
259
- binFound,
260
- configFound,
179
+ binFound: true,
180
+ configFound: existsSync(agent.configPath),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Get the Hermes agent status.
186
+ * @returns {object|null}
187
+ */
188
+ export function getHermesStatus() {
189
+ const agent = AGENT_REGISTRY.find((a) => a.id === "hermes");
190
+ if (!agent) return null;
191
+
192
+ const binFound = hasBin("hermes");
193
+ if (!binFound) return null;
194
+
195
+ const version = getBinVersion("hermes");
196
+ return {
197
+ ...agent,
198
+ installed: true,
199
+ version,
200
+ binFound: true,
201
+ configFound: existsSync(agent.configPath),
261
202
  };
262
203
  }
263
204
 
@@ -275,7 +216,6 @@ export function getInstallableAgents() {
275
216
  * @returns {object|null}
276
217
  */
277
218
  export function getRecommendedAgent(useCase) {
278
- // Prefer OpenCode for programming, Hermes for assistant
279
219
  if (useCase === "programming") {
280
220
  return AGENT_REGISTRY.find((a) => a.id === "opencode");
281
221
  }
@@ -80,8 +80,6 @@ export async function updateAgent(agentId) {
80
80
 
81
81
  const updateCmds = {
82
82
  opencode: "curl -fsSL https://opencode.ai/install | bash",
83
- kilo: "npm update -g @kilocode/cli",
84
- aider: "pip install --upgrade aider-chat",
85
83
  hermes: "pip install --upgrade nous-hermes",
86
84
  };
87
85
 
@@ -1,141 +0,0 @@
1
- /**
2
- * Aider Agent Adapter
3
- *
4
- * Config: ~/.aider.conf.yml (YAML) + env vars
5
- * Format:
6
- * openai-api-base: <BASE_URL>
7
- * model: <model-id>
8
- * .env: OPENAI_API_KEY=<key>
9
- */
10
-
11
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
12
- import { join, dirname } from "node:path";
13
- import { homedir } from "node:os";
14
- import YAML from "yaml";
15
- import { BASE_URL } from "../../constants.js";
16
-
17
- const HOME = homedir();
18
- const CONFIG_PATH = join(HOME, ".aider.conf.yml");
19
- const ENV_PATH = join(HOME, ".env");
20
-
21
- function readYaml() {
22
- if (!existsSync(CONFIG_PATH)) return { config: {}, path: CONFIG_PATH };
23
- try {
24
- const raw = readFileSync(CONFIG_PATH, "utf8");
25
- return { config: YAML.parse(raw) || {}, path: CONFIG_PATH };
26
- } catch {
27
- return { config: {}, path: CONFIG_PATH };
28
- }
29
- }
30
-
31
- function writeYaml(config) {
32
- if (!existsSync(dirname(CONFIG_PATH))) {
33
- mkdirSync(dirname(CONFIG_PATH), { recursive: true });
34
- }
35
- writeFileSync(CONFIG_PATH, YAML.stringify(config, { indent: 2 }), "utf8");
36
- }
37
-
38
- function readEnvFile() {
39
- if (!existsSync(ENV_PATH)) return {};
40
- const raw = readFileSync(ENV_PATH, "utf8");
41
- const env = {};
42
- for (const line of raw.split("\n")) {
43
- const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
44
- if (m) env[m[1]] = m[2];
45
- }
46
- return env;
47
- }
48
-
49
- function writeEnvFile(env) {
50
- const lines = Object.entries(env).map(([k, v]) => `${k}=${v}`);
51
- writeFileSync(ENV_PATH, lines.join("\n") + "\n", "utf8");
52
- }
53
-
54
- // ============= ADAPTER INTERFACE =============
55
-
56
- export const aiderAgent = {
57
- id: "aider",
58
- name: "Aider",
59
- supportsMultipleModels: false,
60
-
61
- detect() {
62
- return existsSync(CONFIG_PATH);
63
- },
64
-
65
- isConfigured() {
66
- const { config } = readYaml();
67
- return Boolean(config?.["openai-api-base"] === BASE_URL);
68
- },
69
-
70
- getStatus() {
71
- const { config, path } = readYaml();
72
- const env = readEnvFile();
73
- const apiKey = env.OPENAI_API_KEY;
74
- const configured = config?.["openai-api-base"] === BASE_URL;
75
- const models = config?.model ? [config.model] : [];
76
- return {
77
- configured,
78
- path,
79
- models,
80
- apiKey: apiKey ? `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}` : null,
81
- envPath: ENV_PATH,
82
- };
83
- },
84
-
85
- configure({ apiKey, models, primaryModel }) {
86
- const { config } = readYaml();
87
- config["openai-api-base"] = `${BASE_URL}/v1`;
88
- config.model = primaryModel || models[0]?.id || "gpt-4";
89
- config["weak-model"] = primaryModel || models[0]?.id || "gpt-4";
90
- writeYaml(config);
91
-
92
- // Set API key in .env
93
- const env = readEnvFile();
94
- env.OPENAI_API_KEY = apiKey;
95
- writeEnvFile(env);
96
-
97
- return { path: CONFIG_PATH, envPath: ENV_PATH, modelsCount: 1 };
98
- },
99
-
100
- changeKey(apiKey) {
101
- if (!this.isConfigured()) {
102
- throw new Error("NOITON não está configurado no Aider");
103
- }
104
- const env = readEnvFile();
105
- env.OPENAI_API_KEY = apiKey;
106
- writeEnvFile(env);
107
- return { path: ENV_PATH };
108
- },
109
-
110
- refresh(models) {
111
- const { config } = readYaml();
112
- if (!config?.["openai-api-base"]) {
113
- throw new Error("NOITON não está configurado no Aider");
114
- }
115
- if (models.length > 0) {
116
- config.model = models[0].id;
117
- config["weak-model"] = models[0].id;
118
- }
119
- writeYaml(config);
120
- return { path: CONFIG_PATH, modelsCount: 1 };
121
- },
122
-
123
- remove() {
124
- const { config } = readYaml();
125
- let removed = false;
126
- if (config?.["openai-api-base"] === BASE_URL) {
127
- delete config["openai-api-base"];
128
- delete config.model;
129
- delete config["weak-model"];
130
- removed = true;
131
- writeYaml(config);
132
- }
133
- const env = readEnvFile();
134
- if (env.OPENAI_API_KEY) {
135
- delete env.OPENAI_API_KEY;
136
- writeEnvFile(env);
137
- removed = true;
138
- }
139
- return { path: CONFIG_PATH, envPath: ENV_PATH, removed };
140
- },
141
- };
@@ -1,128 +0,0 @@
1
- /**
2
- * Cline Agent Adapter
3
- *
4
- * Config: VS Code settings.json with cline config block.
5
- * Format (under `cline` key):
6
- * "cline.provider": "openai",
7
- * "cline.openAiBaseUrl": "<BASE_URL>",
8
- * "cline.openAiApiKey": "<key>",
9
- * "cline.openAiModelId": "<model>"
10
- *
11
- * VS Code stores multiple providers via cline.provider entries.
12
- */
13
-
14
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
15
- import { join, dirname } from "node:path";
16
- import { homedir } from "node:os";
17
- import JSON5 from "json5";
18
- import { BASE_URL, PROVIDER_ID } from "../../constants.js";
19
-
20
- const HOME = homedir();
21
- const XDG = process.env.XDG_CONFIG_HOME || join(HOME, ".config");
22
- const CONFIG_PATH = join(XDG, "Code", "User", "settings.json");
23
-
24
- function readConfig() {
25
- if (!existsSync(CONFIG_PATH)) return { config: {}, path: CONFIG_PATH };
26
- try {
27
- // VS Code settings support JSON with comments (JSONC)
28
- const raw = readFileSync(CONFIG_PATH, "utf8");
29
- const cleaned = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
30
- return { config: JSON5.parse(cleaned), path: CONFIG_PATH };
31
- } catch {
32
- return { config: {}, path: CONFIG_PATH };
33
- }
34
- }
35
-
36
- function writeConfig(config) {
37
- if (!existsSync(dirname(CONFIG_PATH))) {
38
- mkdirSync(dirname(dirname(CONFIG_PATH)), { recursive: true });
39
- }
40
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
41
- }
42
-
43
- // ============= ADAPTER INTERFACE =============
44
-
45
- export const clineAgent = {
46
- id: "cline",
47
- name: "Cline",
48
- supportsMultipleModels: true,
49
-
50
- detect() {
51
- return existsSync(CONFIG_PATH);
52
- },
53
-
54
- isConfigured() {
55
- const { config } = readConfig();
56
- const result = Boolean(
57
- config?.["cline.openAiBaseUrl"] === BASE_URL ||
58
- config?.["cline.apiProvider"] === "openai" && config?.["cline.openAiApiKey"]
59
- );
60
- return result;
61
- },
62
-
63
- getStatus() {
64
- const { config, path } = readConfig();
65
- const baseUrl = config?.["cline.openAiBaseUrl"];
66
- const apiKey = config?.["cline.openAiApiKey"];
67
- const modelId = config?.["cline.openAiModelId"];
68
- const configured = Boolean(baseUrl === BASE_URL || (apiKey && baseUrl));
69
-
70
- const models = modelId ? [modelId] : [];
71
-
72
- return {
73
- configured,
74
- path,
75
- models,
76
- apiKey: apiKey ? `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}` : null,
77
- };
78
- },
79
-
80
- configure({ apiKey, models, primaryModel }) {
81
- const { config } = readConfig();
82
- config["cline.apiProvider"] = "openai";
83
- config["cline.openAiBaseUrl"] = `${BASE_URL}/v1`;
84
- config["cline.openAiApiKey"] = apiKey;
85
- config["cline.openAiModelId"] = primaryModel || models[0]?.id;
86
- // Save list of available models as a JSON array string
87
- config["cline.noitonModels"] = JSON.stringify(models.map((m) => m.id));
88
- writeConfig(config);
89
- return { path: CONFIG_PATH, modelsCount: 1 };
90
- },
91
-
92
- changeKey(apiKey) {
93
- const { config } = readConfig();
94
- if (!config?.["cline.openAiApiKey"]) {
95
- throw new Error("NOITON não está configurado no Cline");
96
- }
97
- config["cline.openAiApiKey"] = apiKey;
98
- writeConfig(config);
99
- return { path: CONFIG_PATH };
100
- },
101
-
102
- refresh(models) {
103
- const { config } = readConfig();
104
- if (!config?.["cline.openAiBaseUrl"]) {
105
- throw new Error("NOITON não está configurado no Cline");
106
- }
107
- config["cline.openAiModelId"] = models[0]?.id || config["cline.openAiModelId"];
108
- config["cline.noitonModels"] = JSON.stringify(models.map((m) => m.id));
109
- writeConfig(config);
110
- return { path: CONFIG_PATH, modelsCount: 1 };
111
- },
112
-
113
- remove() {
114
- const { config } = readConfig();
115
- let removed = false;
116
- if (config?.["cline.openAiBaseUrl"] === BASE_URL ||
117
- config?.["cline.openAiApiKey"]) {
118
- delete config["cline.apiProvider"];
119
- delete config["cline.openAiBaseUrl"];
120
- delete config["cline.openAiApiKey"];
121
- delete config["cline.openAiModelId"];
122
- delete config["cline.noitonModels"];
123
- removed = true;
124
- }
125
- if (removed) writeConfig(config);
126
- return { path: CONFIG_PATH, removed };
127
- },
128
- };
@@ -1,140 +0,0 @@
1
- /**
2
- * Kilo Code Agent Adapter
3
- *
4
- * Config: ~/.config/kilo/kilo.jsonc (JSONC)
5
- * Format (under `provider.openai-compatible`):
6
- * model: openai-compatible/<model-id>
7
- * provider.openai-compatible.options.apiKey
8
- * provider.openai-compatible.options.baseURL
9
- * provider.openai-compatible.models.<id>.limit.context/output
10
- */
11
-
12
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
13
- import { join, dirname } from "node:path";
14
- import { homedir } from "node:os";
15
- import JSON5 from "json5";
16
- import { BASE_URL } from "../../constants.js";
17
-
18
- const HOME = homedir();
19
- const XDG = process.env.XDG_CONFIG_HOME || join(HOME, ".config");
20
- const CONFIG_PATH = join(XDG, "kilo", "kilo.jsonc");
21
- const PROVIDER_ID = "openai-compatible";
22
-
23
- function readConfig() {
24
- if (!existsSync(CONFIG_PATH)) return { config: {}, path: CONFIG_PATH };
25
- try {
26
- const raw = readFileSync(CONFIG_PATH, "utf8");
27
- // Strip comments (JSONC)
28
- const cleaned = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
29
- return { config: JSON5.parse(cleaned), path: CONFIG_PATH };
30
- } catch {
31
- return { config: {}, path: CONFIG_PATH };
32
- }
33
- }
34
-
35
- function writeConfig(config) {
36
- if (!existsSync(dirname(CONFIG_PATH))) {
37
- mkdirSync(dirname(CONFIG_PATH), { recursive: true });
38
- }
39
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
40
- }
41
-
42
- function buildModels(models) {
43
- const obj = {};
44
- for (const m of models) {
45
- if (!m.id) continue;
46
- obj[m.id] = {
47
- name: prettifyName(m.id),
48
- tool_call: true,
49
- limit: { context: m.context_window || 32768, output: 4096 },
50
- };
51
- }
52
- return obj;
53
- }
54
-
55
- function prettifyName(id) {
56
- return id
57
- .replace(/[@/]/g, " ")
58
- .split(/[-_]/)
59
- .filter(Boolean)
60
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
61
- .join(" ");
62
- }
63
-
64
- // ============= ADAPTER INTERFACE =============
65
-
66
- export const kiloAgent = {
67
- id: "kilo",
68
- name: "Kilo Code",
69
- supportsMultipleModels: true,
70
-
71
- detect() {
72
- return existsSync(CONFIG_PATH);
73
- },
74
-
75
- isConfigured() {
76
- const { config } = readConfig();
77
- return Boolean(config?.provider?.[PROVIDER_ID]?.options?.baseURL === BASE_URL);
78
- },
79
-
80
- getStatus() {
81
- const { config, path } = readConfig();
82
- const provider = config?.provider?.[PROVIDER_ID];
83
- if (!provider) return { configured: false, path, models: [], apiKey: null };
84
- const apiKey = provider.options?.apiKey;
85
- const models = provider.models ? Object.keys(provider.models) : [];
86
- return {
87
- configured: provider.options?.baseURL === BASE_URL,
88
- path,
89
- models,
90
- apiKey: apiKey ? (apiKey.startsWith("{env:") ? "(env var)" : `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}`) : null,
91
- };
92
- },
93
-
94
- configure({ apiKey, models, primaryModel }) {
95
- const { config } = readConfig();
96
- config.model = `${PROVIDER_ID}/${primaryModel || models[0]?.id}`;
97
- config.provider = config.provider || {};
98
- config.provider[PROVIDER_ID] = {
99
- options: {
100
- apiKey,
101
- baseURL: `${BASE_URL}/v1`,
102
- },
103
- models: buildModels(models),
104
- };
105
- writeConfig(config);
106
- return { path: CONFIG_PATH, modelsCount: models.length };
107
- },
108
-
109
- changeKey(apiKey) {
110
- const { config } = readConfig();
111
- if (!config?.provider?.[PROVIDER_ID]) {
112
- throw new Error("NOITON não está configurado no Kilo Code");
113
- }
114
- config.provider[PROVIDER_ID].options.apiKey = apiKey;
115
- writeConfig(config);
116
- return { path: CONFIG_PATH };
117
- },
118
-
119
- refresh(models) {
120
- const { config } = readConfig();
121
- if (!config?.provider?.[PROVIDER_ID]) {
122
- throw new Error("NOITON não está configurado no Kilo Code");
123
- }
124
- config.provider[PROVIDER_ID].models = buildModels(models);
125
- writeConfig(config);
126
- return { path: CONFIG_PATH, modelsCount: models.length };
127
- },
128
-
129
- remove() {
130
- const { config } = readConfig();
131
- let removed = false;
132
- if (config?.provider?.[PROVIDER_ID]) {
133
- delete config.provider[PROVIDER_ID];
134
- delete config.model;
135
- removed = true;
136
- writeConfig(config);
137
- }
138
- return { path: CONFIG_PATH, removed };
139
- },
140
- };
@@ -1,149 +0,0 @@
1
- /**
2
- * Kimi Code Agent Adapter
3
- *
4
- * Config: ~/.kimi-code/config.toml (TOML)
5
- * Format:
6
- * [providers.noiton]
7
- * type = "openai"
8
- * base_url = "<BASE_URL>/v1"
9
- * api_key = "<key>"
10
- *
11
- * [models."<id>"]
12
- * provider = "noiton"
13
- * model = "<id>"
14
- * max_context_size = <ctx>
15
- */
16
-
17
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
18
- import { join, dirname } from "node:path";
19
- import { homedir } from "node:os";
20
- import * as TOML from "@iarna/toml";
21
- import { BASE_URL, PROVIDER_ID } from "../../constants.js";
22
-
23
- const HOME = homedir();
24
- const CONFIG_PATH = join(HOME, ".kimi-code", "config.toml");
25
-
26
- function readConfig() {
27
- if (!existsSync(CONFIG_PATH)) return { config: {}, path: CONFIG_PATH };
28
- try {
29
- const raw = readFileSync(CONFIG_PATH, "utf8");
30
- return { config: TOML.parse(raw), path: CONFIG_PATH };
31
- } catch {
32
- return { config: {}, path: CONFIG_PATH };
33
- }
34
- }
35
-
36
- function writeConfig(config) {
37
- if (!existsSync(dirname(CONFIG_PATH))) {
38
- mkdirSync(dirname(CONFIG_PATH), { recursive: true });
39
- }
40
- writeFileSync(CONFIG_PATH, TOML.stringify(config), "utf8");
41
- }
42
-
43
- // ============= ADAPTER INTERFACE =============
44
-
45
- export const kimiAgent = {
46
- id: "kimi",
47
- name: "Kimi Code",
48
- supportsMultipleModels: false,
49
-
50
- detect() {
51
- return existsSync(CONFIG_PATH);
52
- },
53
-
54
- isConfigured() {
55
- const { config } = readConfig();
56
- return Boolean(config?.providers?.[PROVIDER_ID]);
57
- },
58
-
59
- getStatus() {
60
- const { config, path } = readConfig();
61
- const provider = config?.providers?.[PROVIDER_ID];
62
- if (!provider) return { configured: false, path, models: [], apiKey: null };
63
- const apiKey = provider.api_key;
64
- const models = config.models
65
- ? Object.entries(config.models)
66
- .filter(([, v]) => v.provider === PROVIDER_ID)
67
- .map(([k]) => k)
68
- : [];
69
- return {
70
- configured: true,
71
- path,
72
- models,
73
- apiKey: apiKey ? `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}` : null,
74
- };
75
- },
76
-
77
- configure({ apiKey, models, primaryModel }) {
78
- const { config } = readConfig();
79
- config.providers = config.providers || {};
80
- config.providers[PROVIDER_ID] = {
81
- type: "openai",
82
- base_url: `${BASE_URL}/v1`,
83
- api_key: apiKey,
84
- };
85
-
86
- config.models = config.models || {};
87
- const modelId = primaryModel || models[0]?.id || "glm-5.2";
88
- config.models[modelId] = {
89
- provider: PROVIDER_ID,
90
- model: modelId,
91
- max_context_size: models[0]?.context_window || 32768,
92
- };
93
-
94
- writeConfig(config);
95
- return { path: CONFIG_PATH, modelsCount: 1 };
96
- },
97
-
98
- changeKey(apiKey) {
99
- const { config } = readConfig();
100
- if (!config?.providers?.[PROVIDER_ID]) {
101
- throw new Error("NOITON não está configurado no Kimi Code");
102
- }
103
- config.providers[PROVIDER_ID].api_key = apiKey;
104
- writeConfig(config);
105
- return { path: CONFIG_PATH };
106
- },
107
-
108
- refresh(models) {
109
- const { config } = readConfig();
110
- if (!config?.providers?.[PROVIDER_ID]) {
111
- throw new Error("NOITON não está configurado no Kimi Code");
112
- }
113
- // Update context size if model exists
114
- const modelId = Object.keys(config.models || {}).find(
115
- (k) => config.models[k]?.provider === PROVIDER_ID
116
- );
117
- if (modelId && models[0]?.context_window) {
118
- config.models[modelId].max_context_size = models[0].context_window;
119
- }
120
- writeConfig(config);
121
- return { path: CONFIG_PATH, modelsCount: 1 };
122
- },
123
-
124
- remove() {
125
- const { config } = readConfig();
126
- let removed = false;
127
- if (config?.providers?.[PROVIDER_ID]) {
128
- delete config.providers[PROVIDER_ID];
129
- removed = true;
130
- }
131
- if (config?.models) {
132
- for (const [k, v] of Object.entries(config.models)) {
133
- if (v.provider === PROVIDER_ID) {
134
- delete config.models[k];
135
- removed = true;
136
- }
137
- }
138
- }
139
- // Clean up empty top-level tables
140
- if (config.providers && Object.keys(config.providers).length === 0) {
141
- delete config.providers;
142
- }
143
- if (config.models && Object.keys(config.models).length === 0) {
144
- delete config.models;
145
- }
146
- if (removed) writeConfig(config);
147
- return { path: CONFIG_PATH, removed };
148
- },
149
- };
@@ -1,155 +0,0 @@
1
- /**
2
- * OpenClaw Agent Adapter
3
- *
4
- * Config: ~/.openclaw/openclaw.json (JSON5)
5
- * Format:
6
- * models.providers.noiton = {
7
- * baseUrl, apiKey, api: "openai-completions", models: [...]
8
- * }
9
- */
10
-
11
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
12
- import { join, dirname } from "node:path";
13
- import { homedir } from "node:os";
14
- import JSON5 from "json5";
15
- import { BASE_URL, PROVIDER_ID, PROVIDER_NAME } from "../../constants.js";
16
-
17
- const HOME = homedir();
18
- const CONFIG_PATH = join(HOME, ".openclaw", "openclaw.json");
19
-
20
- function readConfig() {
21
- if (!existsSync(CONFIG_PATH)) return { config: {}, path: CONFIG_PATH };
22
- try {
23
- const raw = readFileSync(CONFIG_PATH, "utf8");
24
- return { config: JSON5.parse(raw), path: CONFIG_PATH };
25
- } catch {
26
- return { config: {}, path: CONFIG_PATH };
27
- }
28
- }
29
-
30
- function writeConfig(config) {
31
- if (!existsSync(dirname(CONFIG_PATH))) {
32
- mkdirSync(dirname(CONFIG_PATH), { recursive: true });
33
- }
34
- // Write as standard JSON (OpenClaw accepts JSON5 which is a superset)
35
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
36
- }
37
-
38
- // ============= ADAPTER INTERFACE =============
39
-
40
- export const openclawAgent = {
41
- id: "openclaw",
42
- name: "OpenClaw",
43
- supportsMultipleModels: true,
44
-
45
- detect() {
46
- return existsSync(CONFIG_PATH);
47
- },
48
-
49
- isConfigured() {
50
- const { config } = readConfig();
51
- return Boolean(config?.models?.providers?.[PROVIDER_ID]);
52
- },
53
-
54
- getStatus() {
55
- const { config, path } = readConfig();
56
- const provider = config?.models?.providers?.[PROVIDER_ID];
57
- if (!provider) return { configured: false, path, models: [], apiKey: null };
58
- const models = (provider.models || []).map((m) => m.id);
59
- return {
60
- configured: true,
61
- path,
62
- models,
63
- apiKey: provider.apiKey,
64
- };
65
- },
66
-
67
- configure({ apiKey, models, primaryModel }) {
68
- const { config } = readConfig();
69
- config.models = config.models || {};
70
- config.models.mode = config.models.mode || "merge";
71
- config.models.providers = config.models.providers || {};
72
-
73
- config.models.providers[PROVIDER_ID] = {
74
- baseUrl: `${BASE_URL}/v1`,
75
- apiKey: `\${NOITON_API_KEY}`,
76
- api: "openai-completions",
77
- models: models.map((m) => ({
78
- id: m.id,
79
- name: prettifyName(m.id),
80
- reasoning: false,
81
- input: ["text"],
82
- contextWindow: m.context_window || 32768,
83
- maxTokens: 4096,
84
- })),
85
- };
86
-
87
- // Set env for apiKey substitution
88
- config.env = config.env || {};
89
- config.env.NOITON_API_KEY = apiKey;
90
-
91
- // Set default model if missing
92
- if (!config.agents) config.agents = {};
93
- if (!config.agents.defaults) config.agents.defaults = {};
94
- if (!config.agents.defaults.model) {
95
- config.agents.defaults.model = {
96
- primary: `${PROVIDER_ID}/${primaryModel || models[0]?.id}`,
97
- };
98
- }
99
-
100
- writeConfig(config);
101
- return { path: CONFIG_PATH, modelsCount: models.length };
102
- },
103
-
104
- changeKey(apiKey) {
105
- const { config } = readConfig();
106
- if (!config?.models?.providers?.[PROVIDER_ID]) {
107
- throw new Error("NOITON não está configurado no OpenClaw");
108
- }
109
- config.env = config.env || {};
110
- config.env.NOITON_API_KEY = apiKey;
111
- writeConfig(config);
112
- return { path: CONFIG_PATH };
113
- },
114
-
115
- refresh(models) {
116
- const { config } = readConfig();
117
- if (!config?.models?.providers?.[PROVIDER_ID]) {
118
- throw new Error("NOITON não está configurado no OpenClaw");
119
- }
120
- config.models.providers[PROVIDER_ID].models = models.map((m) => ({
121
- id: m.id,
122
- name: prettifyName(m.id),
123
- reasoning: false,
124
- input: ["text"],
125
- contextWindow: m.context_window || 32768,
126
- maxTokens: 4096,
127
- }));
128
- writeConfig(config);
129
- return { path: CONFIG_PATH, modelsCount: models.length };
130
- },
131
-
132
- remove() {
133
- const { config } = readConfig();
134
- let removed = false;
135
- if (config?.models?.providers?.[PROVIDER_ID]) {
136
- delete config.models.providers[PROVIDER_ID];
137
- removed = true;
138
- }
139
- if (config?.env?.NOITON_API_KEY) {
140
- delete config.env.NOITON_API_KEY;
141
- removed = true;
142
- }
143
- if (removed) writeConfig(config);
144
- return { path: CONFIG_PATH, removed };
145
- },
146
- };
147
-
148
- function prettifyName(id) {
149
- return id
150
- .replace(/[@/]/g, " ")
151
- .split(/[-_]/)
152
- .filter(Boolean)
153
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
154
- .join(" ");
155
- }