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 +2 -12
- package/package.json +2 -7
- package/src/commands/init.js +82 -30
- package/src/index.js +0 -5
- package/src/lib/agents/hermes.js +20 -1
- package/src/lib/agents/index.js +42 -12
- package/src/lib/agents/opencode.js +19 -1
- package/src/lib/detect.js +47 -107
- package/src/lib/install.js +0 -2
- package/src/octopus.js +163 -181
- package/src/ui.js +34 -0
- package/src/lib/agents/aider.js +0 -141
- package/src/lib/agents/cline.js +0 -128
- package/src/lib/agents/kilo.js +0 -140
- package/src/lib/agents/kimi.js +0 -149
- package/src/lib/agents/openclaw.js +0 -155
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "🐙 Universal AI Agent Connector - one key, many agents (OpenCode, Hermes
|
|
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/commands/init.js
CHANGED
|
@@ -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
|
|
8
|
-
* 4. If
|
|
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.
|
|
12
|
-
* 8.
|
|
13
|
-
* 9.
|
|
14
|
-
* 10.
|
|
15
|
-
* 11.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/src/lib/agents/hermes.js
CHANGED
|
@@ -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
|
|
98
|
+
return hasHermesBin();
|
|
80
99
|
},
|
|
81
100
|
|
|
82
101
|
isConfigured() {
|
package/src/lib/agents/index.js
CHANGED
|
@@ -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
|
|
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
|
|
138
|
+
return hasOpencodeBin();
|
|
121
139
|
},
|
|
122
140
|
|
|
123
141
|
isConfigured() {
|