noiton 1.0.0 → 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.
@@ -0,0 +1,155 @@
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
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * OpenCode Agent Adapter
3
+ *
4
+ * Config: ~/.config/opencode/opencode.json (JSON)
5
+ * Format: provider.noiton = { npm, name, options: { baseURL, apiKey }, models }
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
9
+ import { join, dirname } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import {
12
+ PROVIDER_ID,
13
+ PROVIDER_NAME,
14
+ BASE_URL,
15
+ NPM_PACKAGE,
16
+ } from "../../constants.js";
17
+
18
+ const HOME = homedir();
19
+ const XDG = process.env.XDG_CONFIG_HOME || join(HOME, ".config");
20
+
21
+ /** @returns {string} config path */
22
+ export function getConfigPath() {
23
+ if (process.env.OPENCODE_CONFIG) return process.env.OPENCODE_CONFIG;
24
+ return join(XDG, "opencode", "opencode.json");
25
+ }
26
+
27
+ /** @returns {{config: object, path: string}} */
28
+ export function readConfig() {
29
+ const path = getConfigPath();
30
+ if (!existsSync(path)) return { config: {}, path };
31
+ try {
32
+ return { config: JSON.parse(readFileSync(path, "utf8")), path };
33
+ } catch {
34
+ return { config: {}, path };
35
+ }
36
+ }
37
+
38
+ /** @param {object} config @param {string} [path] */
39
+ export function writeConfig(config, path) {
40
+ const p = path || getConfigPath();
41
+ if (!existsSync(dirname(p))) mkdirSync(dirname(p), { recursive: true });
42
+ writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf8");
43
+ }
44
+
45
+ /** @param {object} config */
46
+ export function hasProvider(config) {
47
+ return Boolean(config?.provider?.[PROVIDER_ID]);
48
+ }
49
+
50
+ /** @param {object} config */
51
+ export function getProvider(config) {
52
+ return config?.provider?.[PROVIDER_ID] ?? null;
53
+ }
54
+
55
+ /** @param {Array} models @param {object} [defaults] */
56
+ export function buildModels(models, defaults = {}) {
57
+ const ctx = defaults.context || 32768;
58
+ const out = defaults.output || 4096;
59
+ const result = {};
60
+ for (const m of models) {
61
+ if (!m.id) continue;
62
+ result[m.id] = {
63
+ name: prettifyName(m.id),
64
+ limit: { context: m.context_window || ctx, output: out },
65
+ };
66
+ }
67
+ return result;
68
+ }
69
+
70
+ function prettifyName(id) {
71
+ return id
72
+ .replace(/[@/]/g, " ")
73
+ .split(/[-_]/)
74
+ .filter(Boolean)
75
+ .map((w) => {
76
+ const lower = w.toLowerCase();
77
+ if (["glm", "api", "v4", "v3", "v2", "v5", "gpt", "m3"].includes(lower)) {
78
+ return w.toUpperCase();
79
+ }
80
+ return w.charAt(0).toUpperCase() + w.slice(1);
81
+ })
82
+ .join(" ");
83
+ }
84
+
85
+ /** @param {string} apiKey @param {object} models */
86
+ export function buildProvider(apiKey, models) {
87
+ return {
88
+ npm: NPM_PACKAGE,
89
+ name: PROVIDER_NAME,
90
+ options: { baseURL: BASE_URL, apiKey },
91
+ models,
92
+ };
93
+ }
94
+
95
+ /** @param {object} config @param {string} apiKey @param {object} models */
96
+ export function upsertProvider(config, apiKey, models) {
97
+ const next = { ...config };
98
+ if (!next.provider) next.provider = {};
99
+ next.provider[PROVIDER_ID] = buildProvider(apiKey, models);
100
+ return next;
101
+ }
102
+
103
+ /** @param {object} config */
104
+ export function removeProvider(config) {
105
+ const next = { ...config };
106
+ if (next.provider && PROVIDER_ID in next.provider) {
107
+ delete next.provider[PROVIDER_ID];
108
+ }
109
+ return next;
110
+ }
111
+
112
+ // ============= ADAPTER INTERFACE =============
113
+
114
+ export const opencodeAgent = {
115
+ id: "opencode",
116
+ name: "OpenCode",
117
+ supportsMultipleModels: true,
118
+
119
+ detect() {
120
+ return existsSync(getConfigPath());
121
+ },
122
+
123
+ isConfigured() {
124
+ const { config } = readConfig();
125
+ return hasProvider(config);
126
+ },
127
+
128
+ getStatus() {
129
+ const { config, path } = readConfig();
130
+ const provider = getProvider(config);
131
+ if (!provider) {
132
+ return { configured: false, path, models: [], apiKey: null };
133
+ }
134
+ const models = provider.models ? Object.keys(provider.models) : [];
135
+ const apiKey = provider.options?.apiKey || null;
136
+ return { configured: true, path, models, apiKey };
137
+ },
138
+
139
+ configure({ apiKey, models, primaryModel }) {
140
+ const { config, path } = readConfig();
141
+ const modelsObj = buildModels(models);
142
+ const newConfig = upsertProvider(config, apiKey, modelsObj);
143
+ writeConfig(newConfig, path);
144
+ return { path, modelsCount: models.length };
145
+ },
146
+
147
+ changeKey(apiKey) {
148
+ const { config, path } = readConfig();
149
+ if (!hasProvider(config)) {
150
+ throw new Error("NOITON não está configurado no OpenCode");
151
+ }
152
+ const provider = config.provider[PROVIDER_ID];
153
+ provider.options = provider.options || {};
154
+ provider.options.apiKey = apiKey;
155
+ writeConfig(config, path);
156
+ return { path };
157
+ },
158
+
159
+ refresh(models) {
160
+ const { config, path } = readConfig();
161
+ if (!hasProvider(config)) {
162
+ throw new Error("NOITON não está configurado no OpenCode");
163
+ }
164
+ const provider = config.provider[PROVIDER_ID];
165
+ provider.models = buildModels(models);
166
+ writeConfig(config, path);
167
+ return { path, modelsCount: models.length };
168
+ },
169
+
170
+ remove() {
171
+ const { config, path } = readConfig();
172
+ if (!hasProvider(config)) return { path, removed: false };
173
+ const newConfig = removeProvider(config);
174
+ writeConfig(newConfig, path);
175
+ return { path, removed: true };
176
+ },
177
+ };
@@ -0,0 +1,286 @@
1
+ /**
2
+ * NOITON Agent Detection
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.
9
+ */
10
+
11
+ import { existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { execSync } from "node:child_process";
15
+ import { c } from "../constants.js";
16
+
17
+ const HOME = homedir();
18
+ const XDG = process.env.XDG_CONFIG_HOME || join(HOME, ".config");
19
+
20
+ /**
21
+ * Check if a binary is available on PATH.
22
+ * @param {string} bin
23
+ * @returns {boolean}
24
+ */
25
+ function hasBin(bin) {
26
+ try {
27
+ execSync(`which ${bin} 2>/dev/null`, { stdio: "ignore" });
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get version of a CLI binary (best-effort).
36
+ * @param {string} bin
37
+ * @returns {string|null}
38
+ */
39
+ function getBinVersion(bin) {
40
+ const flags = ["--version", "-v"];
41
+ for (const flag of flags) {
42
+ try {
43
+ const out = execSync(`${bin} ${flag} 2>/dev/null`, {
44
+ encoding: "utf8",
45
+ timeout: 5000,
46
+ }).trim();
47
+ // Extract first version-like token
48
+ const match = out.match(/(\d+\.\d+(?:\.\d+)?(?:-\w+)?)/);
49
+ if (match) return match[1];
50
+ return out.split("\n")[0];
51
+ } catch {
52
+ // try next flag
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Get latest version of an npm package (best-effort).
60
+ * @param {string} pkg
61
+ * @returns {string|null}
62
+ */
63
+ function getNpmLatest(pkg) {
64
+ try {
65
+ const out = execSync(`npm view ${pkg} version 2>/dev/null`, {
66
+ encoding: "utf8",
67
+ timeout: 8000,
68
+ }).trim();
69
+ const match = out.match(/(\d+\.\d+(?:\.\d+)?)/);
70
+ return match ? match[1] : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Compare two semver-like version strings.
78
+ * Returns: 1 if a > b, -1 if a < b, 0 if equal.
79
+ * @param {string} a
80
+ * @param {string} b
81
+ */
82
+ function compareVersions(a, b) {
83
+ const pa = a.split(".").map(Number);
84
+ const pb = b.split(".").map(Number);
85
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
86
+ const va = pa[i] || 0;
87
+ const vb = pb[i] || 0;
88
+ if (va > vb) return 1;
89
+ if (va < vb) return -1;
90
+ }
91
+ return 0;
92
+ }
93
+
94
+ // ============= AGENT DEFINITIONS =============
95
+
96
+ /**
97
+ * Registry of all supported agents.
98
+ * Each entry describes how to detect the agent and where its config lives.
99
+ */
100
+ export const AGENT_REGISTRY = [
101
+ {
102
+ id: "opencode",
103
+ name: "OpenCode",
104
+ type: "CLI",
105
+ bin: "opencode",
106
+ npmPkg: "opencode-ai",
107
+ installMethod: "script",
108
+ installCmd: "curl -fsSL https://opencode.ai/install | bash",
109
+ configPath: join(XDG, "opencode", "opencode.json"),
110
+ supportsMultipleModels: true,
111
+ description: "Programador IA que escreve código junto com você",
112
+ useCase: "programming",
113
+ },
114
+ {
115
+ id: "hermes",
116
+ name: "Hermes",
117
+ type: "CLI",
118
+ bin: "hermes",
119
+ npmPkg: null,
120
+ installMethod: "pip",
121
+ installCmd: "pip install nous-hermes",
122
+ configPath: join(HOME, ".hermes", "config.yaml"),
123
+ envPath: join(HOME, ".hermes", ".env"),
124
+ supportsMultipleModels: false,
125
+ description: "Assistente que faz tarefas e se integra com apps",
126
+ useCase: "assistant",
127
+ },
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
+ ];
194
+
195
+ /**
196
+ * Detect all installed agents.
197
+ * @returns {Array} detected agents with status info
198
+ */
199
+ export function detectAgents() {
200
+ const detected = [];
201
+ 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
+ }
216
+ }
217
+ return detected;
218
+ }
219
+
220
+ /**
221
+ * Check if a specific agent is installed.
222
+ * @param {string} id
223
+ * @returns {boolean}
224
+ */
225
+ export function isAgentInstalled(id) {
226
+ 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;
231
+ }
232
+
233
+ /**
234
+ * Get the OpenCode agent specifically (most common case).
235
+ * Checks version and whether an update is available.
236
+ * @returns {object|null}
237
+ */
238
+ export function getOpenCodeStatus() {
239
+ const agent = AGENT_REGISTRY.find((a) => a.id === "opencode");
240
+ if (!agent) return null;
241
+
242
+ const binFound = hasBin("opencode");
243
+ const configFound = existsSync(agent.configPath);
244
+ const installed = binFound || configFound;
245
+
246
+ if (!installed) return null;
247
+
248
+ const version = binFound ? getBinVersion("opencode") : null;
249
+ const latest = binFound ? getNpmLatest("opencode-ai") : null;
250
+ const updateAvailable =
251
+ version && latest ? compareVersions(latest, version) > 0 : false;
252
+
253
+ return {
254
+ ...agent,
255
+ installed: true,
256
+ version,
257
+ latest,
258
+ updateAvailable,
259
+ binFound,
260
+ configFound,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Get agents that can be auto-installed.
266
+ * @returns {Array}
267
+ */
268
+ export function getInstallableAgents() {
269
+ return AGENT_REGISTRY.filter((a) => a.installCmd !== null);
270
+ }
271
+
272
+ /**
273
+ * Get the recommended agent for a given use case.
274
+ * @param {"programming"|"assistant"} useCase
275
+ * @returns {object|null}
276
+ */
277
+ export function getRecommendedAgent(useCase) {
278
+ // Prefer OpenCode for programming, Hermes for assistant
279
+ if (useCase === "programming") {
280
+ return AGENT_REGISTRY.find((a) => a.id === "opencode");
281
+ }
282
+ if (useCase === "assistant") {
283
+ return AGENT_REGISTRY.find((a) => a.id === "hermes");
284
+ }
285
+ return AGENT_REGISTRY.find((a) => a.id === "opencode");
286
+ }