noiton 2.0.0 → 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.0",
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",
@@ -1,18 +1,20 @@
1
1
  /**
2
2
  * NOITON init command - the universal multi-agent wizard
3
3
  *
4
- * Flow:
4
+ * Flow (with proper pacing and step-by-step reveals):
5
5
  * 1. Print banner with octopus mascot
6
- * 2. Detect all installed agents
7
- * 3. If OpenCode installed, check version & offer update
8
- * 4. If no agents, offer to install OpenCode or Hermes (based on use case)
9
- * 5. Show checklist of detected agents
6
+ * 2. Detect all installed agents (with spinner + gradual reveal)
7
+ * 3. If no agents, offer to install OpenCode or Hermes
8
+ * 4. If OpenCode installed, check version & offer update
9
+ * 5. Show checklist of detected agents (one by one)
10
10
  * 6. Ask which agents to configure
11
- * 7. Ask for NOITON API key (once)
12
- * 8. Validate key + fetch models
13
- * 9. Ask which model to use as primary (for single-model agents)
14
- * 10. Configure each selected agent
15
- * 11. Show funny success message + easter egg
11
+ * 7. Check if already configured, ask to overwrite
12
+ * 8. Ask for NOITON API key (once)
13
+ * 9. Validate key + fetch models (with spinner)
14
+ * 10. Show models list
15
+ * 11. Ask which model to use as primary
16
+ * 12. Configure each selected agent (with per-agent spinner)
17
+ * 13. Show summary + funny success message + easter egg
16
18
  */
17
19
 
18
20
  import { input, confirm, select, checkbox } from "@inquirer/prompts";
@@ -29,7 +31,8 @@ import {
29
31
  step,
30
32
  checklist,
31
33
  divider,
32
- withSpinner,
34
+ sleep,
35
+ line,
33
36
  } from "../ui.js";
34
37
  import {
35
38
  successMessage,
@@ -44,30 +47,38 @@ import { getDetectedAgents, getAgent } from "../lib/agents/index.js";
44
47
  import {
45
48
  detectAgents,
46
49
  getOpenCodeStatus,
47
- getRecommendedAgent,
48
50
  } from "../lib/detect.js";
49
51
  import { installAgent, updateAgent, offerInstall } from "../lib/install.js";
50
52
 
51
53
  export async function initCommand() {
52
54
  // ============= STEP 1: BANNER =============
53
55
  printBanner("happy");
56
+ await sleep(400);
54
57
 
55
58
  // ============= STEP 2: DETECT AGENTS =============
56
59
  step(1, "Procurando agentes de IA instalados...");
60
+ await sleep(300);
57
61
 
62
+ const detectSpinner = spinner("Escaneando seu sistema...");
63
+ await sleep(800);
58
64
  const detected = getDetectedAgents();
59
65
  const opencodeStatus = getOpenCodeStatus();
66
+ detectSpinner.succeed("Escaneamento concluído!");
67
+ await sleep(300);
60
68
 
69
+ // ============= STEP 2a: NO AGENTS FOUND =============
61
70
  if (detected.length === 0) {
62
- // ============= STEP 2a: NO AGENTS - OFFER INSTALL =============
63
71
  console.log();
72
+ await sleep(200);
64
73
  box(noAgentMessage(), "warning");
74
+ await sleep(500);
65
75
  console.log();
66
76
 
67
77
  const installChoice = await offerInstall();
68
78
  if (!installChoice) {
69
79
  console.log();
70
80
  info(cancelMessage());
81
+ await sleep(200);
71
82
  console.log();
72
83
  console.log(c.dim(" 💡 Dica: instale OpenCode ou Hermes e rode noiton novamente!"));
73
84
  console.log(c.dim(" OpenCode: curl -fsSL https://opencode.ai/install | bash"));
@@ -85,6 +96,7 @@ export async function initCommand() {
85
96
  return;
86
97
  }
87
98
 
99
+ await sleep(500);
88
100
  // Re-detect after install
89
101
  const reDetected = getDetectedAgents();
90
102
  if (reDetected.length === 0) {
@@ -96,8 +108,9 @@ export async function initCommand() {
96
108
  detected.push(...reDetected);
97
109
  }
98
110
 
99
- // ============= STEP 2b: CHECK OPENCODE VERSION =============
111
+ // ============= STEP 3: CHECK OPENCODE VERSION =============
100
112
  if (opencodeStatus?.updateAvailable) {
113
+ await sleep(400);
101
114
  console.log();
102
115
  box(
103
116
  `🔄 OpenCode v${opencodeStatus.latest} disponível!\n` +
@@ -105,6 +118,7 @@ export async function initCommand() {
105
118
  ` Atualizar agora?`,
106
119
  "warning"
107
120
  );
121
+ await sleep(300);
108
122
  const wantUpdate = await confirm({
109
123
  message: "Atualizar OpenCode?",
110
124
  default: true,
@@ -116,22 +130,36 @@ export async function initCommand() {
116
130
  } else {
117
131
  warn(`Não foi possível atualizar: ${updateResult.error}`);
118
132
  }
133
+ await sleep(300);
119
134
  }
120
135
  }
121
136
 
122
- // ============= STEP 3: SHOW CHECKLIST =============
137
+ // ============= STEP 4: SHOW DETECTED AGENTS ONE BY ONE =============
123
138
  step(2, "Agentes detectados");
139
+ await sleep(300);
124
140
 
141
+ // Reveal each agent with a small delay
125
142
  const items = detected.map((a) => ({
126
143
  label: a.name,
127
144
  ok: true,
128
145
  detail: a.version ? `v${a.version}` : "(config encontrado)",
129
146
  }));
130
- checklist(items);
147
+
148
+ // Print one by one for a "checking" feel
149
+ for (const item of items) {
150
+ const mark = c.green("✓");
151
+ const label = c.white(item.label);
152
+ const detail = c.dim(` ${item.detail}`);
153
+ process.stdout.write(` ${mark} ${label}${detail}`);
154
+ await sleep(250);
155
+ process.stdout.write("\n");
156
+ }
131
157
  console.log();
158
+ await sleep(300);
132
159
 
133
- // ============= STEP 4: CHOOSE AGENTS TO CONFIGURE =============
160
+ // ============= STEP 5: CHOOSE AGENTS TO CONFIGURE =============
134
161
  step(3, "Escolha quais agentes configurar");
162
+ await sleep(200);
135
163
 
136
164
  const agentChoices = detected.map((a) => ({
137
165
  name: `${a.name}${a.version ? ` (v${a.version})` : ""}`,
@@ -143,6 +171,7 @@ export async function initCommand() {
143
171
  if (detected.length === 1) {
144
172
  selectedIds = [detected[0].id];
145
173
  info(`Configurando: ${detected[0].name}`);
174
+ await sleep(200);
146
175
  } else {
147
176
  selectedIds = await checkbox({
148
177
  message: "Quais agentes você quer conectar ao NOITON?",
@@ -155,7 +184,7 @@ export async function initCommand() {
155
184
  .map((id) => getAgent(id))
156
185
  .filter(Boolean);
157
186
 
158
- // ============= STEP 5: CHECK EXISTING CONFIG =============
187
+ // ============= STEP 6: CHECK EXISTING CONFIG =============
159
188
  const alreadyConfigured = selectedAgents.filter((a) => {
160
189
  try {
161
190
  return a.isConfigured();
@@ -165,10 +194,12 @@ export async function initCommand() {
165
194
  });
166
195
 
167
196
  if (alreadyConfigured.length > 0) {
197
+ await sleep(300);
168
198
  console.log();
169
199
  warn(
170
200
  `NOITON já está configurado em: ${alreadyConfigured.map((a) => a.name).join(", ")}`
171
201
  );
202
+ await sleep(200);
172
203
  const overwrite = await confirm({
173
204
  message: "Sobrescrever configuração existente?",
174
205
  default: false,
@@ -180,22 +211,26 @@ export async function initCommand() {
180
211
  }
181
212
  }
182
213
 
183
- // ============= STEP 6: ASK FOR API KEY =============
214
+ // ============= STEP 7: ASK FOR API KEY =============
184
215
  step(4, "Cole sua chave NOITON");
216
+ await sleep(200);
185
217
  console.log(c.dim(" Uma chave só serve para todos os agentes! 🐙"));
218
+ await sleep(200);
186
219
  console.log();
187
220
 
188
221
  const apiKey = await input({
189
- message: " Chave NOITON:",
222
+ message: " Chave:",
190
223
  validate: (v) => (v.trim().length >= 10 ? true : "Chave muito curta"),
191
224
  transformer: (v) => maskKey(v),
192
225
  });
193
226
 
194
227
  const trimmedKey = apiKey.trim();
195
228
 
196
- // ============= STEP 7: VALIDATE KEY + FETCH MODELS =============
229
+ // ============= STEP 8: VALIDATE KEY + FETCH MODELS =============
197
230
  console.log();
231
+ await sleep(200);
198
232
  const s = spinner(`${thinkingMessage()} Validando chave...`);
233
+ await sleep(500);
199
234
  const { valid, models, error } = await validateKey(trimmedKey);
200
235
 
201
236
  if (!valid) {
@@ -206,32 +241,38 @@ export async function initCommand() {
206
241
  }
207
242
 
208
243
  s.succeed("Chave válida!");
209
- console.log();
244
+ await sleep(300);
210
245
 
211
246
  if (!models || models.length === 0) {
247
+ console.log();
212
248
  warn("Nenhum modelo disponível para esta chave.");
213
249
  return;
214
250
  }
215
251
 
216
- // ============= STEP 8: SHOW MODELS =============
252
+ // ============= STEP 9: SHOW MODELS =============
217
253
  step(5, `Modelos disponíveis (${models.length})`);
254
+ await sleep(200);
218
255
 
219
256
  for (const m of models) {
220
257
  const ctx = m.context_window
221
258
  ? c.dim(` (${Math.round(m.context_window / 1000)}K ctx)`)
222
259
  : "";
223
- console.log(` ${c.cyan("•")} ${m.id}${ctx}`);
260
+ process.stdout.write(` ${c.cyan("•")} ${m.id}${ctx}`);
261
+ await sleep(120);
262
+ process.stdout.write("\n");
224
263
  }
225
264
  console.log();
265
+ await sleep(300);
226
266
 
227
- // ============= STEP 9: CHOOSE PRIMARY MODEL =============
228
- // For agents that only support one model, ask which to use
267
+ // ============= STEP 10: CHOOSE PRIMARY MODEL =============
229
268
  const needsPrimaryModel = selectedAgents.some((a) => !a.supportsMultipleModels);
230
269
 
231
270
  let primaryModel = null;
232
271
  if (needsPrimaryModel) {
233
272
  step(6, "Escolha o modelo principal");
273
+ await sleep(200);
234
274
  console.log(c.dim(" Alguns agentes usam apenas 1 modelo por vez."));
275
+ await sleep(200);
235
276
  console.log();
236
277
 
237
278
  const modelChoices = models.map((m) => ({
@@ -245,14 +286,17 @@ export async function initCommand() {
245
286
  default: models[0]?.id,
246
287
  });
247
288
  console.log();
289
+ await sleep(200);
248
290
  }
249
291
 
250
- // ============= STEP 10: CONFIGURE EACH AGENT =============
251
- step(7, "Configurando agentes...");
292
+ // ============= STEP 11: CONFIGURE EACH AGENT =============
293
+ step(needsPrimaryModel ? 7 : 6, "Configurando agentes...");
294
+ await sleep(300);
252
295
 
253
296
  const results = [];
254
297
  for (const agent of selectedAgents) {
255
298
  const s = spinner(`Configurando ${agent.name}...`);
299
+ await sleep(400);
256
300
  try {
257
301
  const result = agent.configure({
258
302
  apiKey: trimmedKey,
@@ -265,13 +309,16 @@ export async function initCommand() {
265
309
  s.fail(`Erro em ${agent.name}: ${err.message}`);
266
310
  results.push({ agent: agent.name, ok: false, error: err.message });
267
311
  }
312
+ await sleep(200);
268
313
  }
269
314
 
270
- // ============= STEP 11: SUMMARY =============
315
+ // ============= STEP 12: SUMMARY =============
271
316
  console.log();
272
317
  divider();
318
+ await sleep(300);
273
319
  console.log();
274
320
  printBanner("dancing");
321
+ await sleep(400);
275
322
 
276
323
  const successCount = results.filter((r) => r.ok).length;
277
324
  if (successCount > 0) {
@@ -281,28 +328,33 @@ export async function initCommand() {
281
328
  "success"
282
329
  );
283
330
  console.log();
331
+ await sleep(200);
284
332
  for (const r of results) {
285
333
  if (r.ok) {
286
334
  ok(`${r.agent} ${c.dim(`→ ${r.path}`)}`);
287
335
  } else {
288
336
  fail(`${r.agent} ${c.dim(`→ ${r.error}`)}`);
289
337
  }
338
+ await sleep(150);
290
339
  }
291
340
  } else {
292
341
  box("Nenhum agente foi configurado. Tente novamente!", "error");
293
342
  return;
294
343
  }
295
344
 
296
- // ============= STEP 12: EASTER EGG + FAREWELL =============
345
+ // ============= STEP 13: EASTER EGG + FAREWELL =============
297
346
  const egg = getEasterEgg();
298
347
  if (egg) {
299
348
  console.log();
349
+ await sleep(200);
300
350
  console.log(c.yellow(` 🎁 ${egg}`));
301
351
  }
302
352
 
303
353
  console.log();
354
+ await sleep(200);
304
355
  console.log(c.dim(" 💡 Reinicie os agentes para aplicar as mudanças."));
305
356
  console.log();
357
+ await sleep(200);
306
358
  console.log(c.purple(` ${farewellMessage()}`));
307
359
  console.log();
308
360
  }
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() {