@omerrgocmen/crewctl 1.0.1 → 1.0.3

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.
Files changed (96) hide show
  1. package/README.md +2 -0
  2. package/orchestrator/README.md +429 -427
  3. package/orchestrator/config.default.json +71 -65
  4. package/orchestrator/roles/executor.md +26 -18
  5. package/orchestrator/roles/operator.md +77 -75
  6. package/orchestrator/roles/planner.md +5 -0
  7. package/orchestrator/roles/reviewer.md +63 -54
  8. package/orchestrator/skills/acceptance-criteria.md +18 -18
  9. package/orchestrator/skills/accessibility-audit.md +16 -16
  10. package/orchestrator/skills/accessible-forms.md +16 -16
  11. package/orchestrator/skills/api-design.md +27 -27
  12. package/orchestrator/skills/api-documentation.md +16 -16
  13. package/orchestrator/skills/architecture-decision-record.md +18 -18
  14. package/orchestrator/skills/authentication-design.md +16 -16
  15. package/orchestrator/skills/authorization-review.md +16 -16
  16. package/orchestrator/skills/backward-compatibility.md +17 -17
  17. package/orchestrator/skills/changelog-writing.md +16 -16
  18. package/orchestrator/skills/ci-pipeline-design.md +16 -16
  19. package/orchestrator/skills/cli-design.md +16 -16
  20. package/orchestrator/skills/code-review.md +27 -27
  21. package/orchestrator/skills/configuration-management.md +16 -16
  22. package/orchestrator/skills/container-review.md +16 -16
  23. package/orchestrator/skills/contract-testing.md +16 -16
  24. package/orchestrator/skills/dashboard-design.md +16 -16
  25. package/orchestrator/skills/database-migration.md +16 -16
  26. package/orchestrator/skills/database-schema-design.md +16 -16
  27. package/orchestrator/skills/debugging.md +26 -26
  28. package/orchestrator/skills/dependency-review.md +16 -16
  29. package/orchestrator/skills/design-review.md +29 -29
  30. package/orchestrator/skills/design-system.md +16 -16
  31. package/orchestrator/skills/docs-api-reference.md +16 -16
  32. package/orchestrator/skills/docs-troubleshooting.md +16 -16
  33. package/orchestrator/skills/docs-tutorial.md +16 -16
  34. package/orchestrator/skills/docs-writing.md +25 -25
  35. package/orchestrator/skills/empty-error-loading-states.md +16 -16
  36. package/orchestrator/skills/end-to-end-testing.md +16 -16
  37. package/orchestrator/skills/error-handling.md +16 -16
  38. package/orchestrator/skills/frontend-design.md +28 -28
  39. package/orchestrator/skills/git-commit-writing.md +16 -16
  40. package/orchestrator/skills/graphql-design.md +16 -16
  41. package/orchestrator/skills/incident-runbook.md +18 -18
  42. package/orchestrator/skills/input-validation.md +16 -16
  43. package/orchestrator/skills/integration-testing.md +16 -16
  44. package/orchestrator/skills/interaction-design.md +16 -16
  45. package/orchestrator/skills/landing-page-design.md +16 -16
  46. package/orchestrator/skills/observability-design.md +16 -16
  47. package/orchestrator/skills/openapi-contract.md +16 -16
  48. package/orchestrator/skills/performance-profiling.md +16 -16
  49. package/orchestrator/skills/privacy-review.md +16 -16
  50. package/orchestrator/skills/property-based-testing.md +16 -16
  51. package/orchestrator/skills/pull-request-writing.md +16 -16
  52. package/orchestrator/skills/refactoring.md +16 -16
  53. package/orchestrator/skills/release-readiness.md +16 -16
  54. package/orchestrator/skills/responsive-design.md +16 -16
  55. package/orchestrator/skills/secrets-management.md +16 -16
  56. package/orchestrator/skills/secure-file-upload.md +16 -16
  57. package/orchestrator/skills/security-review.md +26 -26
  58. package/orchestrator/skills/semantic-versioning.md +17 -17
  59. package/orchestrator/skills/seo-on-page.md +16 -16
  60. package/orchestrator/skills/seo-structured-data.md +16 -16
  61. package/orchestrator/skills/seo-technical-audit.md +16 -16
  62. package/orchestrator/skills/sql-query-review.md +16 -16
  63. package/orchestrator/skills/supply-chain-security.md +16 -16
  64. package/orchestrator/skills/test-strategy.md +16 -16
  65. package/orchestrator/skills/threat-modeling.md +16 -16
  66. package/orchestrator/skills/unit-testing.md +16 -16
  67. package/orchestrator/skills/write-tests.md +28 -28
  68. package/orchestrator/src/cli-registry.js +616 -613
  69. package/orchestrator/src/cli.js +131 -131
  70. package/orchestrator/src/doctor.js +64 -64
  71. package/orchestrator/src/engine.js +216 -32
  72. package/orchestrator/src/server.js +766 -733
  73. package/orchestrator/src/skill-registry.js +272 -272
  74. package/orchestrator/src/store.js +399 -379
  75. package/orchestrator/web/OrbitControls.js +1417 -1417
  76. package/orchestrator/web/android-chrome-192x192.png +0 -0
  77. package/orchestrator/web/android-chrome-512x512.png +0 -0
  78. package/orchestrator/web/apple-touch-icon.png +0 -0
  79. package/orchestrator/web/board.html +5 -0
  80. package/orchestrator/web/code.html +5 -0
  81. package/orchestrator/web/favicon-16x16.png +0 -0
  82. package/orchestrator/web/favicon-32x32.png +0 -0
  83. package/orchestrator/web/favicon.ico +0 -0
  84. package/orchestrator/web/flow.html +741 -736
  85. package/orchestrator/web/index.html +588 -539
  86. package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -231
  87. package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -104
  88. package/orchestrator/web/jsm/postprocessing/Pass.js +95 -95
  89. package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -99
  90. package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -77
  91. package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -415
  92. package/orchestrator/web/jsm/shaders/CopyShader.js +45 -45
  93. package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -66
  94. package/orchestrator/web/site.webmanifest +12 -0
  95. package/orchestrator/web/three.module.min.js +6 -6
  96. package/package.json +1 -1
@@ -1,613 +1,616 @@
1
- const { spawn, spawnSync } = require("child_process");
2
- const fs = require("fs");
3
- const os = require("os");
4
- const path = require("path");
5
-
6
- const isWin = process.platform === "win32";
7
-
8
- const DEFINITIONS = {
9
- codex: {
10
- command: "codex",
11
- versionArgs: ["--version"],
12
- defaultArgs: ["exec", "--skip-git-repo-check"],
13
- // Codex calisirken surekli ilerleme akitir; 3 dakikalik sessizlik gercek bir takilmadir.
14
- silenceTimeoutSeconds: 180,
15
- description: "OpenAI Codex kodlama, hata ayiklama ve inceleme agenti",
16
- capabilities: ["implementation", "debugging", "testing", "review", "analysis"],
17
- roleFile: "roles/executor.md",
18
- },
19
- claude: {
20
- command: "claude",
21
- versionArgs: ["--version"],
22
- defaultArgs: ["-p", "--permission-mode", "acceptEdits", "--output-format", "text"],
23
- description: "Claude Code uygulama, analiz ve test agenti",
24
- capabilities: ["implementation", "debugging", "testing", "review", "web"],
25
- roleFile: "roles/executor.md",
26
- },
27
- gemini: {
28
- command: "gemini",
29
- versionArgs: ["--version"],
30
- // Argumansiz gemini interaktif TUI acar ve izin bekler; yolo ile otonom, stdin'den prompt alir.
31
- defaultArgs: ["--approval-mode", "yolo"],
32
- // Gemini de akis halinde cikti verir; uzun sessizlik takilma isaretidir.
33
- silenceTimeoutSeconds: 180,
34
- description: "Gemini CLI analiz, uygulama ve web arastirma agenti",
35
- capabilities: ["implementation", "planning", "analysis", "research", "web"],
36
- roleFile: "roles/executor.md",
37
- },
38
- opencode: {
39
- command: "opencode",
40
- versionArgs: ["--version"],
41
- // OpenCode saglayici/model kombinasyonuna gore uzun sure sessiz kalabilir. Varsayilan
42
- // agent timeout'u, basarili bir calismayi SIGTERM ile yarida kesmeyecek kadar genis tut.
43
- timeoutSeconds: 1800,
44
- silenceTimeoutSeconds: 180,
45
- // Bazi eski OpenCode surumleri --auto bayragini tanimaz. Otonom izinler
46
- // engine tarafinda OPENCODE_CONFIG_CONTENT ile surumden bagimsiz aktarilir.
47
- defaultArgs: ["run", "--format", "json", "Attached file contains the full task. Follow it exactly, make the changes, and report what you did.", "--file", "{PROMPT_FILE}"],
48
- description: "OpenCode coklu saglayici kodlama, uygulama ve inceleme agenti",
49
- capabilities: ["implementation", "debugging", "testing", "review", "analysis", "research"],
50
- roleFile: "roles/executor.md",
51
- install: {
52
- windows: "npm install -g opencode-ai (alternatif: choco install opencode / scoop install opencode)",
53
- macos: "brew install anomalyco/tap/opencode (alternatif: npm install -g opencode-ai)",
54
- linux: "curl -fsSL https://opencode.ai/install | bash (alternatif: npm install -g opencode-ai)",
55
- auth: "opencode auth login",
56
- },
57
- },
58
- };
59
- const RESOLVED = new Map();
60
- let OPEN_CODE_MODELS = [];
61
-
62
- // En fazla bir CLI'yi (or. codex) ayni anda hem canli saglik/model probe'u hem de motorun
63
- // operatör kosumu calistirirsa, codex gibi bazi CLI'lar ayni is dizininde ikinci exec oturumunu
64
- // SIGTERM ile keser. Probe'lar "en iyi caba"dir; motor onceliklidir. Aktif probe cocuklarini
65
- // izleyip motor bir goreve baslarken hepsini sonlandiriyoruz.
66
- const ACTIVE_PROBES = new Set();
67
- function registerProbe(child) {
68
- if (!child) return () => {};
69
- ACTIVE_PROBES.add(child);
70
- return () => ACTIVE_PROBES.delete(child);
71
- }
72
- // Uctan uca proses agacini oldur (codex kabuk altinda alt proses baslatabilir).
73
- function terminateProbeTree(child) {
74
- if (!child) return;
75
- try {
76
- if (isWin && child.pid) spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
77
- else child.kill("SIGKILL");
78
- } catch {}
79
- }
80
- // Motor bir goreve baslarken cagirir: uctaki tum canli probe'lari sonlandirir ki operatörun
81
- // CLI'si (codex vb.) temiz bir alanda calissin. Probe'lar sonuc olarak "timeout/failed" doner;
82
- // bu zararsizdir, motor bosaldiginda yeniden denenir.
83
- function abortActiveProbes() {
84
- const count = ACTIVE_PROBES.size;
85
- for (const child of ACTIVE_PROBES) terminateProbeTree(child);
86
- ACTIVE_PROBES.clear();
87
- return count;
88
- }
89
-
90
- function selectOpenCodeModel(models) {
91
- const list = Array.isArray(models) ? models.map(String).filter(Boolean) : [];
92
- const priorities = [
93
- (x) => x === "opencode/big-pickle",
94
- (x) => /^opencode\/.+free$/i.test(x),
95
- (x) => x.startsWith("opencode/"),
96
- (x) => x.startsWith("opencode-go/"),
97
- (x) => x.startsWith("minimax-coding-plan/"),
98
- (x) => !x.startsWith("ollama/"),
99
- ];
100
- for (const match of priorities) {
101
- const found = list.find(match);
102
- if (found) return found;
103
- }
104
- return "";
105
- }
106
-
107
- // `opencode models` ciktisindan saglayici/model satirlarini ayiklar. Baslik, bos satir ve
108
- // log gurultusunu eler; her "saglayici/model" bicimli satir gecerli sayilir.
109
- function parseOpenCodeModels(stdout) {
110
- const found = new Set();
111
- for (const line of String(stdout || "").split(/\r?\n/)) {
112
- const model = line.trim();
113
- if (/^[A-Za-z0-9._-]+\/.+/.test(model) && !/\s/.test(model)) found.add(model);
114
- }
115
- return [...found];
116
- }
117
-
118
- function listOpenCodeModels(command) {
119
- try {
120
- // Tum saglayicilarin listesi sorgulanir. Kullanicilarin ucretli abonelikleri
121
- // (opencode-go, minimax-coding-plan vb.) ayri saglayici olarak gorunur; yalnizca
122
- // "opencode" saglayicisini sorgulamak bu modelleri gizliyor ve kurulum yanlislikla
123
- // "hazir degil" sayiliyordu. Otomatik oneri siralamasini selectOpenCodeModel yapar.
124
- const result = spawnSync(command, ["models"], {
125
- encoding: "utf8", timeout: 20000, windowsHide: true,
126
- shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
127
- });
128
- if (result.status !== 0) return [];
129
- return parseOpenCodeModels(result.stdout);
130
- } catch { return []; }
131
- }
132
-
133
- function adapterId(command) {
134
- const base = path.basename(String(command || "")).toLowerCase().replace(/\.(cmd|bat|exe|ps1)$/, "");
135
- if (base.includes("codex")) return "codex";
136
- if (base.includes("claude")) return "claude";
137
- if (base.includes("gemini")) return "gemini";
138
- if (base.includes("opencode")) return "opencode";
139
- return "custom";
140
- }
141
-
142
- // Farkli paket yoneticileri / kurulum araclari CLI'lari farkli dizinlere koyar. Her PC'de
143
- // calismasi icin yaygin npm/pnpm/yarn/bun/volta/scoop/winget/choco/homebrew konumlarini tarariz.
144
- function commonCandidates(id) {
145
- const home = os.homedir();
146
- if (isWin) {
147
- const APPDATA = process.env.APPDATA, LOCAL = process.env.LOCALAPPDATA, PD = process.env.ProgramData;
148
- const exts = ["cmd", "exe", "ps1", "bat", ""];
149
- const bases = [
150
- APPDATA && path.join(APPDATA, "npm"),
151
- LOCAL && path.join(LOCAL, "pnpm"),
152
- LOCAL && path.join(LOCAL, "Yarn", "bin"),
153
- LOCAL && path.join(LOCAL, "Microsoft", "WinGet", "Links"),
154
- LOCAL && path.join(LOCAL, "Programs", id),
155
- path.join(home, ".bun", "bin"),
156
- path.join(home, ".volta", "bin"),
157
- path.join(home, "scoop", "shims"),
158
- PD && path.join(PD, "chocolatey", "bin"),
159
- path.join(home, ".local", "bin"),
160
- path.join(home, "AppData", "Roaming", "npm"),
161
- ].filter(Boolean);
162
- const out = [];
163
- for (const base of bases) for (const ext of exts) out.push(path.join(base, ext ? `${id}.${ext}` : id));
164
- return out;
165
- }
166
- return [
167
- path.join(home, ".local", "bin", id),
168
- path.join(home, "bin", id),
169
- path.join(home, ".bun", "bin", id),
170
- path.join(home, ".volta", "bin", id),
171
- path.join(home, ".npm-global", "bin", id),
172
- path.join(home, ".yarn", "bin", id),
173
- path.join(home, ".local", "share", "pnpm", id),
174
- `/usr/local/bin/${id}`,
175
- `/opt/homebrew/bin/${id}`,
176
- `/usr/bin/${id}`,
177
- `/snap/bin/${id}`,
178
- ];
179
- }
180
-
181
- // Bilinen bir CLI komutu adapter alanindan daha guvenilir kaynaktir. Eski veya elle
182
- // duzenlenmis config'te adapter=claude + cmd=codex gibi bir celiski varsa hedef CLI'nin
183
- // guvenli varsayilan argumanlarina donulur; boylece bir CLI'ye digerinin bayragi gitmez.
184
- function normalizeAgentAdapter(agent) {
185
- if (!agent || typeof agent !== "object") return false;
186
- const configured = String(agent.adapter || "").trim().toLowerCase();
187
- const inferred = adapterId(agent.cmd);
188
- const adapter = inferred !== "custom" ? inferred : (DEFINITIONS[configured] ? configured : "custom");
189
- let changed = false;
190
- if (inferred !== "custom" && configured && configured !== inferred) {
191
- agent.args = DEFINITIONS[inferred].defaultArgs.slice();
192
- delete agent.model;
193
- delete agent.modelOverride;
194
- changed = true;
195
- }
196
- if (agent.adapter !== adapter) {
197
- agent.adapter = adapter;
198
- changed = true;
199
- }
200
- return changed;
201
- }
202
-
203
- function normalizeAgentAdapters(cfg) {
204
- if (!cfg?.agents || typeof cfg.agents !== "object") return false;
205
- let changed = false;
206
- for (const agent of Object.values(cfg.agents)) if (normalizeAgentAdapter(agent)) changed = true;
207
- return changed;
208
- }
209
-
210
- // cfg verilirse cfg.cliSettings[adapter] o CLI'nin varsayilan model ayari olarak uygulanir.
211
- // Oncelik: agent.model (profil bazli) > cliSettings[adapter].model (global) > CLI varsayilani.
212
- function effectiveAgent(agent, cfg) {
213
- const copy = { ...agent, args: Array.isArray(agent.args) ? agent.args.map(String) : [] };
214
- normalizeAgentAdapter(copy);
215
- const adapter = copy.adapter;
216
- const commandAdapter = adapterId(copy.cmd);
217
- const settings = (cfg && cfg.cliSettings && cfg.cliSettings[adapter]) || {};
218
- if (!copy.silenceTimeoutSeconds && DEFINITIONS[adapter]?.silenceTimeoutSeconds) copy.silenceTimeoutSeconds = DEFINITIONS[adapter].silenceTimeoutSeconds;
219
- if (commandAdapter === adapter && RESOLVED.has(adapter) && !path.isAbsolute(copy.cmd)) copy.cmd = RESOLVED.get(adapter);
220
- if (adapter === "codex") {
221
- const commands = new Set(["exec", "review", "resume", "apply"]);
222
- if (!copy.args.some((arg) => commands.has(arg))) copy.args.unshift("exec", "--skip-git-repo-check");
223
- else if (copy.args.includes("exec") && !copy.args.includes("--skip-git-repo-check")) copy.args.splice(copy.args.indexOf("exec") + 1, 0, "--skip-git-repo-check");
224
- const model = String(copy.model || settings.model || "").trim();
225
- const effort = ["low", "medium", "high", "xhigh", "max", "ultra"].includes(String(settings.reasoningEffort)) ? String(settings.reasoningEffort) : "";
226
- const serviceTier = /^[A-Za-z0-9._-]{1,64}$/.test(String(settings.serviceTier || "")) ? String(settings.serviceTier) : "";
227
- if (model && !copy.args.includes("--model") && !copy.args.includes("-m")) copy.args.push("--model", model);
228
- if (effort && !copy.args.some((arg) => String(arg).startsWith("model_reasoning_effort="))) copy.args.push("-c", `model_reasoning_effort="${effort}"`);
229
- if (serviceTier && !copy.args.some((arg) => String(arg).startsWith("service_tier="))) copy.args.push("-c", `service_tier="${serviceTier}"`);
230
- }
231
- if (adapter === "claude") {
232
- if (!copy.args.includes("-p") && !copy.args.includes("--print")) copy.args.unshift("-p");
233
- if (!copy.args.includes("--output-format")) copy.args.push("--output-format", "text");
234
- }
235
- if (adapter === "gemini") {
236
- // Otonom, non-interaktif calisma icin onay modu sart; yoksa izin bekleyip takilir.
237
- if (!copy.args.includes("--approval-mode") && !copy.args.includes("-y") && !copy.args.includes("--yolo")) copy.args.push("--approval-mode", "yolo");
238
- }
239
- if (adapter === "opencode") {
240
- if (!copy.args.includes("run")) copy.args.unshift("run");
241
- // config.json'da eski otomatik profil kalmis olsa da uyumsuz bayragi temizle.
242
- copy.args = copy.args.filter((arg) => arg !== "--auto");
243
- const formatAt = copy.args.indexOf("--format");
244
- if (formatAt >= 0) copy.args.splice(formatAt, 2);
245
- copy.args.splice(copy.args.indexOf("run") + 1, 0, "--format", "json");
246
- const hasModel = copy.args.includes("--model") || copy.args.includes("-m");
247
- const model = copy.model || String(settings.model || "").trim() || selectOpenCodeModel(OPEN_CODE_MODELS);
248
- if (!hasModel && model) copy.args.splice(copy.args.indexOf("run") + 3, 0, "--model", model);
249
- if (model) copy.model = model;
250
- if (!copy.args.includes("{PROMPT}") && !copy.args.includes("{PROMPT_FILE}")) {
251
- copy.args.push("Attached file contains the full task. Follow it exactly, make the changes, and report what you did.", "--file", "{PROMPT_FILE}");
252
- }
253
- }
254
- return copy;
255
- }
256
-
257
- function probeCommand(command, definition) {
258
- try {
259
- const result = spawnSync(command, definition.versionArgs, {
260
- encoding: "utf8",
261
- timeout: 12000,
262
- windowsHide: true,
263
- shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
264
- });
265
- const output = String(result.stdout || result.stderr || "").trim().split(/\r?\n/)[0];
266
- return { installed: result.status === 0, version: result.status === 0 ? output : "", error: result.status === 0 ? "" : output, resolvedCommand: result.status === 0 ? command : "" };
267
- } catch (error) {
268
- return { installed: false, version: "", error: error.message, resolvedCommand: "" };
269
- }
270
- }
271
-
272
- function probe(id, definition) {
273
- let result = probeCommand(definition.command, definition);
274
- let installedPath = "";
275
- if (!result.installed) {
276
- for (const candidate of commonCandidates(id)) {
277
- if (!fs.existsSync(candidate)) continue;
278
- if (!installedPath) installedPath = candidate;
279
- result = probeCommand(candidate, definition);
280
- if (result.installed) break;
281
- }
282
- // Bazi CLI'lar (or. gemini) --version cagrisinda cmd.exe uzerinden yavas acildigi icin
283
- // zaman asimina ugrar. Ikili dosya diskte mevcutsa, versiyon okunamasa bile "kurulu" say;
284
- // aksi halde kurulu CLI yanlislikla devre disi birakiliyordu.
285
- if (!result.installed && installedPath) {
286
- result = { installed: true, version: "kurulu", error: "", resolvedCommand: installedPath };
287
- }
288
- }
289
- if (result.installed) RESOLVED.set(id, result.resolvedCommand);
290
- if (result.installed && id === "opencode") {
291
- OPEN_CODE_MODELS = listOpenCodeModels(result.resolvedCommand || definition.command);
292
- result.models = OPEN_CODE_MODELS.slice();
293
- result.recommendedModel = selectOpenCodeModel(OPEN_CODE_MODELS);
294
- result.ready = Boolean(result.recommendedModel);
295
- result.readinessError = result.ready ? "" : (OPEN_CODE_MODELS.length
296
- ? "Kullanilabilir otomatik OpenCode modeli bulunamadi. Ayarlardan erisilebilir bir model secin."
297
- : "OpenCode kurulu, ancak model listesi okunamadi. Once saglayici girisini tamamlayin.");
298
- }
299
- return result;
300
- }
301
-
302
- function discoverInstalled() {
303
- return Object.entries(DEFINITIONS).map(([id, definition]) => ({ id, ...definition, ...probe(id, definition) }));
304
- }
305
-
306
- // Tek kaynak: hem gercek gorev calistirici (engine.runCli) hem saglik testi ve model
307
- // listeleme ayni komut kurulumunu kullanir. Bare npm CLI adlarini where.exe ile fiziksel
308
- // yola cevirmiyoruz; cmd.exe PATHEXT/PATH cozumlemesini Unicode olarak kendisi yapar.
309
- function buildCommand(command, args) {
310
- if (!isWin) return { file: command, args, shell: false };
311
- const bare = !path.isAbsolute(command) && !/[\\/]/.test(command);
312
- if (bare) return { file: command, args, shell: true };
313
- if (!/\.(cmd|bat)$/i.test(command)) return { file: command, args, shell: false };
314
- const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
315
- return { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", `"${[command, ...args].map(quote).join(" ")}"`], shell: false, verbatim: true };
316
- }
317
-
318
- function healthPrompt(id) {
319
- return [
320
- "CLI_TEAM_HEALTH_CHECK",
321
- `Bu ${id} cli aracinin kullanilabilirlik testidir.`,
322
- "Yalnizca HEALTH_OK yaz.",
323
- "Dosya olusturma, degistirme, silme, komut calistirma veya dis sisteme istek gonderme.",
324
- ].join("\n");
325
- }
326
-
327
- // Normal gorev calistiricisi {PROMPT}/{PROMPT_FILE} yer tutucularini cagridan once
328
- // doldurur. Health check de ayni sozlesmeyi kullanmali; aksi halde OpenCode literal
329
- // `{PROMPT_FILE}` yolunu acmaya calisir ve saglikli agent katalogdan yanlislikla elenir.
330
- function preparePromptArgs(args, prompt) {
331
- let tempDir = "", promptFile = "", useStdin = true;
332
- const prepared = (Array.isArray(args) ? args : []).map((arg) => {
333
- const value = String(arg);
334
- if (value.includes("{PROMPT}")) {
335
- useStdin = false;
336
- return value.replaceAll("{PROMPT}", prompt);
337
- }
338
- if (value.includes("{PROMPT_FILE}")) {
339
- useStdin = false;
340
- if (!promptFile) {
341
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cli-team-health-"));
342
- promptFile = path.join(tempDir, "prompt.md");
343
- fs.writeFileSync(promptFile, prompt, "utf8");
344
- }
345
- return value.replaceAll("{PROMPT_FILE}", promptFile);
346
- }
347
- return value;
348
- });
349
- return {
350
- args: prepared,
351
- useStdin,
352
- promptFile,
353
- cleanup() { if (tempDir) { try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {} } },
354
- };
355
- }
356
-
357
- // Tek kaynak: OpenCode icin otonom izinler surumden bagimsiz olarak OPENCODE_CONFIG_CONTENT
358
- // ile aktarilir. Hem engine.runCli hem saglik testi bu ortami kullanir ki test ile gercek
359
- // calistirma ayni izinlerle calissin.
360
- function agentEnvironment(agent) {
361
- const env = { ...process.env };
362
- if (agent.adapter !== "opencode") return env;
363
- let inline = {};
364
- try { inline = JSON.parse(env.OPENCODE_CONFIG_CONTENT || "{}"); } catch {}
365
- env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ ...inline, permission: { "*": "allow" } });
366
- return env;
367
- }
368
-
369
- function classifyHealthFailure(text) {
370
- const raw = String(text || "");
371
- if (/requires a newer version|upgrade to the latest|version.*unsupported|unsupported.*version|model metadata.*not found|unsupported.*model/i.test(raw)) return { status: "version-incompatible", label: "Sürüm uyumsuz", detail: raw };
372
- if (/api key|unauthorized|unauthenticated|authentication|login required|not logged in|sign in|giriÅŸ gerekli/i.test(raw)) return { status: "auth-required", label: "Giriş gerekli", detail: raw };
373
- if (/rate.?limit|quota|too many requests|billing|credit/i.test(raw)) return { status: "quota", label: "Kota veya ödeme gerekli", detail: raw };
374
- if (/timeout|timed out|zaman aşım/i.test(raw)) return { status: "timeout", label: "Yanıt zaman aşımına uğradı", detail: raw };
375
- return { status: "failed", label: "Test başarısız", detail: raw };
376
- }
377
-
378
- function testInstalledCli(id, options = {}) {
379
- const definition = DEFINITIONS[id];
380
- if (!definition) return Promise.resolve({ id, installed: false, health: { status: "unknown", label: "Bilinmiyor", detail: "Tanımsız CLI" } });
381
- const found = probe(id, definition);
382
- if (!found.installed) return Promise.resolve({ id, installed: false, version: found.version, error: found.error, health: { status: "not-installed", label: "Kurulu değil", detail: found.error || "CLI bulunamadı" } });
383
- const agent = effectiveAgent({ adapter: id, cmd: definition.command, args: definition.defaultArgs.slice() }, options.cfg);
384
- const prompt = healthPrompt(id);
385
- const prepared = preparePromptArgs(agent.args, prompt);
386
- const command = buildCommand(agent.cmd, prepared.args);
387
- const timeoutMs = Math.max(10000, Number(options.timeoutMs || 45000));
388
- return new Promise((resolve) => {
389
- let stdout = "", stderr = "", settled = false;
390
- let child;
391
- try {
392
- child = spawn(command.file, command.args, { env: agentEnvironment(agent), shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim });
393
- } catch (error) {
394
- prepared.cleanup();
395
- return resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(error.message) });
396
- }
397
- const unregister = registerProbe(child);
398
- const timer = setTimeout(() => {
399
- if (settled) return;
400
- settled = true;
401
- unregister();
402
- terminateProbeTree(child);
403
- prepared.cleanup();
404
- resolve({ id, installed: true, version: found.version, health: { status: "timeout", label: "Yanıt zaman aşımına uğradı", detail: `Sağlık testi ${Math.round(timeoutMs / 1000)} saniyede tamamlanmadı.` } });
405
- }, timeoutMs);
406
- child.stdout?.setEncoding("utf8");
407
- child.stderr?.setEncoding("utf8");
408
- child.stdout?.on("data", (data) => { stdout += String(data); });
409
- child.stderr?.on("data", (data) => { stderr += String(data); });
410
- child.on("error", (error) => {
411
- if (settled) return;
412
- settled = true;
413
- clearTimeout(timer);
414
- unregister();
415
- prepared.cleanup();
416
- const failure = classifyHealthFailure(error.message);
417
- resolve({ id, installed: true, version: found.version, health: failure });
418
- });
419
- child.on("close", (code) => {
420
- if (settled) return;
421
- settled = true;
422
- clearTimeout(timer);
423
- unregister();
424
- prepared.cleanup();
425
- const raw = `${stdout}\n${stderr}`.trim();
426
- if (code === 0 && /HEALTH_OK/i.test(raw)) resolve({ id, installed: true, version: found.version, health: { status: "ready", label: "Hazır", detail: "Gerçek sağlık testi başarılı." } });
427
- else resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(raw || `Çıkış kodu ${code}`) });
428
- });
429
- child.stdin?.on("error", () => {});
430
- child.stdin?.end(prepared.useStdin ? prompt : "");
431
- });
432
- }
433
-
434
- async function healthCheckAll(discovered = discoverInstalled(), options = {}) {
435
- const results = await Promise.all(discovered.map((item) => testInstalledCli(item.id, options)));
436
- return discovered.map((item) => ({ ...item, ...(results.find((result) => result.id === item.id) || {}) }));
437
- }
438
-
439
- function listCodexModels(options = {}) {
440
- const command = buildCommand("codex", ["app-server", "--listen", "stdio://"]);
441
- const timeoutMs = Math.max(5000, Number(options.timeoutMs || 20000));
442
- return new Promise((resolve, reject) => {
443
- let buffer = "", settled = false, modelRequestSent = false;
444
- let child, unregister = () => {};
445
- const finish = (error, value) => {
446
- if (settled) return;
447
- settled = true;
448
- clearTimeout(timer);
449
- unregister();
450
- terminateProbeTree(child);
451
- if (error) reject(error); else resolve(value);
452
- };
453
- const timer = setTimeout(() => finish(new Error(`Codex model listesi ${Math.round(timeoutMs / 1000)} saniyede alınamadı.`)), timeoutMs);
454
- try { child = spawn(command.file, command.args, { shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim }); }
455
- catch (error) { finish(error); return; }
456
- unregister = registerProbe(child);
457
- child.stdout?.setEncoding("utf8");
458
- child.stderr?.setEncoding("utf8");
459
- child.stdout?.on("data", (chunk) => {
460
- buffer += String(chunk);
461
- const lines = buffer.split(/\r?\n/);
462
- buffer = lines.pop() || "";
463
- for (const line of lines) {
464
- let message;
465
- try { message = JSON.parse(line); } catch { continue; }
466
- if (message.id === 1 && !modelRequestSent) {
467
- modelRequestSent = true;
468
- child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "model/list", params: { includeHidden: false, limit: 200 } })}\n`);
469
- } else if (message.id === 2) {
470
- if (message.error) { finish(new Error(message.error.message || "Codex model listesi alınamadı.")); return; }
471
- const models = Array.isArray(message.result?.data) ? message.result.data : [];
472
- finish(null, models.filter((model) => model && model.hidden !== true).map((model) => ({
473
- id: String(model.id || model.model || ""),
474
- model: String(model.model || model.id || ""),
475
- displayName: String(model.displayName || model.id || model.model || ""),
476
- description: String(model.description || ""),
477
- defaultReasoningEffort: String(model.defaultReasoningEffort || "medium"),
478
- reasoningEfforts: (model.supportedReasoningEfforts || []).map((item) => ({
479
- id: String(item.reasoningEffort || item.id || ""),
480
- description: String(item.description || ""),
481
- })).filter((item) => item.id),
482
- serviceTiers: (model.serviceTiers || []).map((item) => ({
483
- id: String(item.id || ""),
484
- name: String(item.name || item.id || ""),
485
- description: String(item.description || ""),
486
- })).filter((item) => item.id),
487
- additionalSpeedTiers: Array.isArray(model.additionalSpeedTiers) ? model.additionalSpeedTiers.map(String) : [],
488
- defaultServiceTier: model.defaultServiceTier ? String(model.defaultServiceTier) : "",
489
- isDefault: model.isDefault === true,
490
- })).filter((model) => model.id));
491
- }
492
- }
493
- });
494
- child.stderr?.on("data", () => {});
495
- child.on("error", finish);
496
- child.stdin?.on("error", () => {});
497
- child.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { clientInfo: { name: "crewctl", version: "1.0.0", title: "CrewCtl" } } })}\n`);
498
- });
499
- }
500
-
501
- function addMissingAgents(cfg, discovered) {
502
- cfg.agents ||= {};
503
- const ignored = new Set(Array.isArray(cfg.discoveryIgnoredAdapters) ? cfg.discoveryIgnoredAdapters.map(String) : []);
504
- let changed = normalizeAgentAdapters(cfg);
505
- for (const cli of discovered) {
506
- const usable = cli.installed && (cli.id !== "opencode" || cli.ready !== false || Boolean(cfg.cliSettings?.opencode?.model));
507
- const matchingEntries = Object.entries(cfg.agents).filter(([, agent]) => (agent.adapter || adapterId(agent.cmd)) === cli.id);
508
- const existingEntry = matchingEntries.find(([, agent]) => !agent.autoDiscovered) || matchingEntries[0];
509
- const existing = existingEntry?.[1];
510
- // Kullanici otomatik bulunan bir profili sildiyse bu karar kalicidir. O adapter'a ait
511
- // elle olusturulmus profil varsa ona dokunma; yalnizca autoDiscovered kaydini temizle.
512
- if (ignored.has(cli.id)) {
513
- for (const [name, agent] of matchingEntries) {
514
- if (!agent.autoDiscovered) continue;
515
- delete cfg.agents[name];
516
- changed = true;
517
- }
518
- continue;
519
- }
520
- if (existing) {
521
- if (usable && existing.autoDiscovered && existing.unavailablePlaceholder) {
522
- existing.enabled = true;
523
- existing.args = cli.defaultArgs.slice();
524
- delete existing.unavailablePlaceholder;
525
- changed = true;
526
- }
527
- // Eski surumler otomatik oneriyi agent.model alanina yaziyordu. Bu alan global
528
- // CLI ayarini ezdigi icin, kullanici tarafindan acikca override olarak isaretlenmemis
529
- // otomatik profiller modeli daima CLI ayarindan/otomatik secimden miras alir.
530
- if (cli.id === "opencode" && existing.autoDiscovered && existing.model && existing.modelOverride !== true) {
531
- delete existing.model;
532
- changed = true;
533
- }
534
- if (cli.installed && cli.id === "opencode" && existing.autoDiscovered && Number(existing.timeoutSeconds || 0) < cli.timeoutSeconds) {
535
- existing.timeoutSeconds = cli.timeoutSeconds;
536
- changed = true;
537
- }
538
- if (!usable && existing.autoDiscovered && existing.enabled !== false) {
539
- existing.enabled = false;
540
- existing.unavailablePlaceholder = true;
541
- changed = true;
542
- }
543
- continue;
544
- }
545
- // OpenCode adapteri her cihazda katalogda gorunur. Kurulu degilse guvenli
546
- // sekilde devre disi placeholder olur; taramada bulundugunda otomatik etkinlesir.
547
- if (!cli.installed && cli.id !== "opencode") continue;
548
- let name = `${cli.id}-auto`, suffix = 2;
549
- while (cfg.agents[name]) name = `${cli.id}-auto-${suffix++}`;
550
- cfg.agents[name] = {
551
- adapter: cli.id,
552
- cmd: cli.command,
553
- args: cli.defaultArgs.slice(),
554
- enabled: usable,
555
- description: cli.description,
556
- capabilities: cli.capabilities.slice(),
557
- roleFile: cli.roleFile,
558
- costTier: "standard",
559
- timeoutSeconds: cli.timeoutSeconds || 1200,
560
- ...(cli.silenceTimeoutSeconds ? { silenceTimeoutSeconds: cli.silenceTimeoutSeconds } : {}),
561
- autoDiscovered: true,
562
- ...(!usable ? { unavailablePlaceholder: true } : {}),
563
- };
564
- changed = true;
565
- }
566
- return changed;
567
- }
568
-
569
- // Operator, uzman agent'lardan BAGIMSIZ bir CLI secimidir (claude/codex/gemini/opencode).
570
- // operator.md rolunu bu CLI giyer; uzman agent'lar operatorun ALTINDA delege edilir.
571
- // Bir uzman agent silinse bile operator etkilenmez.
572
- function operatorSpec(cli, cfg) {
573
- const op = (cfg && cfg.operator) || {};
574
- const definition = DEFINITIONS[cli];
575
- // Standart CLI'lar DEFINITIONS'tan cozulur. cfg.operator.cmd verilirse (ozel/test operatoru)
576
- // dogrudan o komut kullanilir; boylece operator, uzman agent'lardan tamamen bagimsiz kalir.
577
- const cmd = op.cmd || (definition && definition.command);
578
- if (!cmd) return null;
579
- const args = op.cmd ? (Array.isArray(op.args) ? op.args.slice() : []) : (definition ? definition.defaultArgs.slice() : []);
580
- // Model/effort/tier secimi CLI bazlidir (cfg.cliSettings) ve effectiveAgent icinde uygulanir;
581
- // ayni ayar o CLI'yi kullanan operator ve tum uzman agent'lar icin gecerlidir.
582
- const timeoutSeconds = op.timeoutSeconds || (cfg && cfg.agentTimeoutSeconds) || 900;
583
- const silenceTimeoutSeconds = op.silenceTimeoutSeconds || (definition && definition.silenceTimeoutSeconds) || (cfg && cfg.cliSilenceTimeoutSeconds) || 300;
584
- const adapter = definition ? cli : (op.adapter || adapterId(cmd));
585
- return effectiveAgent({ adapter, cmd, args, timeoutSeconds, silenceTimeoutSeconds }, cfg);
586
- }
587
-
588
- // Operator gecerli, kurulu bir CLI olmali. Eski semada operator.agent (uzman agent adi)
589
- // tutuluyordu; burada operator.cli'ye tasinir. Kurulu degilse ilk kurulu CLI'ye gecilir.
590
- function ensureValidOperator(cfg, discovered) {
591
- cfg.operator ||= {};
592
- let changed = false;
593
- if (cfg.operator.roleFile !== "roles/operator.md") { cfg.operator.roleFile = "roles/operator.md"; changed = true; }
594
- if (!cfg.operator.cli) {
595
- const legacy = cfg.operator.agent;
596
- const source = legacy && cfg.agents?.[legacy] ? cfg.agents[legacy].cmd : legacy;
597
- cfg.operator.cli = adapterId(source || "codex");
598
- changed = true;
599
- }
600
- if ("agent" in cfg.operator) { delete cfg.operator.agent; changed = true; }
601
- const installed = Array.isArray(discovered) ? discovered
602
- .filter((x) => x.installed && (x.id !== "opencode" || x.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)))
603
- .map((x) => x.id) : null;
604
- const known = !!DEFINITIONS[cfg.operator.cli];
605
- const availableOnHost = !installed || installed.length === 0 || installed.includes(cfg.operator.cli);
606
- if (!known || !availableOnHost) {
607
- const fallback = (installed && installed[0]) || Object.keys(DEFINITIONS)[0];
608
- if (fallback && fallback !== cfg.operator.cli) { cfg.operator.cli = fallback; changed = true; }
609
- }
610
- return changed;
611
- }
612
-
613
- module.exports = { DEFINITIONS, KNOWN_CLIS: Object.keys(DEFINITIONS), adapterId, normalizeAgentAdapter, normalizeAgentAdapters, effectiveAgent, preparePromptArgs, operatorSpec, buildCommand, agentEnvironment, discoverInstalled, healthCheckAll, listCodexModels, abortActiveProbes, selectOpenCodeModel, parseOpenCodeModels, addMissingAgents, ensureValidOperator };
1
+ const { spawn, spawnSync } = require("child_process");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+
6
+ const isWin = process.platform === "win32";
7
+
8
+ const DEFINITIONS = {
9
+ codex: {
10
+ command: "codex",
11
+ versionArgs: ["--version"],
12
+ defaultArgs: ["exec", "--skip-git-repo-check"],
13
+ // Codex calisirken surekli ilerleme akitir; 3 dakikalik sessizlik gercek bir takilmadir.
14
+ silenceTimeoutSeconds: 180,
15
+ description: "OpenAI Codex kodlama, hata ayiklama ve inceleme agenti",
16
+ capabilities: ["implementation", "debugging", "testing", "review", "analysis"],
17
+ roleFile: "roles/executor.md",
18
+ },
19
+ claude: {
20
+ command: "claude",
21
+ versionArgs: ["--version"],
22
+ defaultArgs: ["-p", "--permission-mode", "acceptEdits", "--output-format", "text"],
23
+ description: "Claude Code uygulama, analiz ve test agenti",
24
+ capabilities: ["implementation", "debugging", "testing", "review", "web"],
25
+ roleFile: "roles/executor.md",
26
+ },
27
+ gemini: {
28
+ command: "gemini",
29
+ versionArgs: ["--version"],
30
+ // Argumansiz gemini interaktif TUI acar ve izin bekler; yolo ile otonom, stdin'den prompt alir.
31
+ defaultArgs: ["--approval-mode", "yolo"],
32
+ // Gemini de akis halinde cikti verir; uzun sessizlik takilma isaretidir.
33
+ silenceTimeoutSeconds: 180,
34
+ description: "Gemini CLI analiz, uygulama ve web arastirma agenti",
35
+ capabilities: ["implementation", "planning", "analysis", "research", "web"],
36
+ roleFile: "roles/executor.md",
37
+ },
38
+ opencode: {
39
+ command: "opencode",
40
+ versionArgs: ["--version"],
41
+ // OpenCode saglayici/model kombinasyonuna gore uzun sure sessiz kalabilir. Varsayilan
42
+ // agent timeout'u, basarili bir calismayi SIGTERM ile yarida kesmeyecek kadar genis tut.
43
+ timeoutSeconds: 1800,
44
+ // OpenCode adim baslattiktan sonra model/provider yanitini beklerken uzun sure hic cikti
45
+ // akitmayabilir (ozellikle yavas/rate-limitli modeller). 180sn cok agresifti ve calisan bir
46
+ // kosmayi yariyordu; sessizlik payini genis tut. Gercek takilma yine timeoutSeconds ile yakalanir.
47
+ silenceTimeoutSeconds: 300,
48
+ // Bazi eski OpenCode surumleri --auto bayragini tanimaz. Otonom izinler
49
+ // engine tarafinda OPENCODE_CONFIG_CONTENT ile surumden bagimsiz aktarilir.
50
+ defaultArgs: ["run", "--format", "json", "Attached file contains the full task. Follow it exactly, make the changes, and report what you did.", "--file", "{PROMPT_FILE}"],
51
+ description: "OpenCode coklu saglayici kodlama, uygulama ve inceleme agenti",
52
+ capabilities: ["implementation", "debugging", "testing", "review", "analysis", "research"],
53
+ roleFile: "roles/executor.md",
54
+ install: {
55
+ windows: "npm install -g opencode-ai (alternatif: choco install opencode / scoop install opencode)",
56
+ macos: "brew install anomalyco/tap/opencode (alternatif: npm install -g opencode-ai)",
57
+ linux: "curl -fsSL https://opencode.ai/install | bash (alternatif: npm install -g opencode-ai)",
58
+ auth: "opencode auth login",
59
+ },
60
+ },
61
+ };
62
+ const RESOLVED = new Map();
63
+ let OPEN_CODE_MODELS = [];
64
+
65
+ // En fazla bir CLI'yi (or. codex) ayni anda hem canli saglik/model probe'u hem de motorun
66
+ // operatör kosumu calistirirsa, codex gibi bazi CLI'lar ayni is dizininde ikinci exec oturumunu
67
+ // SIGTERM ile keser. Probe'lar "en iyi caba"dir; motor onceliklidir. Aktif probe cocuklarini
68
+ // izleyip motor bir goreve baslarken hepsini sonlandiriyoruz.
69
+ const ACTIVE_PROBES = new Set();
70
+ function registerProbe(child) {
71
+ if (!child) return () => {};
72
+ ACTIVE_PROBES.add(child);
73
+ return () => ACTIVE_PROBES.delete(child);
74
+ }
75
+ // Uctan uca proses agacini oldur (codex kabuk altinda alt proses baslatabilir).
76
+ function terminateProbeTree(child) {
77
+ if (!child) return;
78
+ try {
79
+ if (isWin && child.pid) spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
80
+ else child.kill("SIGKILL");
81
+ } catch {}
82
+ }
83
+ // Motor bir goreve baslarken cagirir: uctaki tum canli probe'lari sonlandirir ki operatörun
84
+ // CLI'si (codex vb.) temiz bir alanda calissin. Probe'lar sonuc olarak "timeout/failed" doner;
85
+ // bu zararsizdir, motor bosaldiginda yeniden denenir.
86
+ function abortActiveProbes() {
87
+ const count = ACTIVE_PROBES.size;
88
+ for (const child of ACTIVE_PROBES) terminateProbeTree(child);
89
+ ACTIVE_PROBES.clear();
90
+ return count;
91
+ }
92
+
93
+ function selectOpenCodeModel(models) {
94
+ const list = Array.isArray(models) ? models.map(String).filter(Boolean) : [];
95
+ const priorities = [
96
+ (x) => x === "opencode/big-pickle",
97
+ (x) => /^opencode\/.+free$/i.test(x),
98
+ (x) => x.startsWith("opencode/"),
99
+ (x) => x.startsWith("opencode-go/"),
100
+ (x) => x.startsWith("minimax-coding-plan/"),
101
+ (x) => !x.startsWith("ollama/"),
102
+ ];
103
+ for (const match of priorities) {
104
+ const found = list.find(match);
105
+ if (found) return found;
106
+ }
107
+ return "";
108
+ }
109
+
110
+ // `opencode models` ciktisindan saglayici/model satirlarini ayiklar. Baslik, bos satir ve
111
+ // log gurultusunu eler; her "saglayici/model" bicimli satir gecerli sayilir.
112
+ function parseOpenCodeModels(stdout) {
113
+ const found = new Set();
114
+ for (const line of String(stdout || "").split(/\r?\n/)) {
115
+ const model = line.trim();
116
+ if (/^[A-Za-z0-9._-]+\/.+/.test(model) && !/\s/.test(model)) found.add(model);
117
+ }
118
+ return [...found];
119
+ }
120
+
121
+ function listOpenCodeModels(command) {
122
+ try {
123
+ // Tum saglayicilarin listesi sorgulanir. Kullanicilarin ucretli abonelikleri
124
+ // (opencode-go, minimax-coding-plan vb.) ayri saglayici olarak gorunur; yalnizca
125
+ // "opencode" saglayicisini sorgulamak bu modelleri gizliyor ve kurulum yanlislikla
126
+ // "hazir degil" sayiliyordu. Otomatik oneri siralamasini selectOpenCodeModel yapar.
127
+ const result = spawnSync(command, ["models"], {
128
+ encoding: "utf8", timeout: 20000, windowsHide: true,
129
+ shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
130
+ });
131
+ if (result.status !== 0) return [];
132
+ return parseOpenCodeModels(result.stdout);
133
+ } catch { return []; }
134
+ }
135
+
136
+ function adapterId(command) {
137
+ const base = path.basename(String(command || "")).toLowerCase().replace(/\.(cmd|bat|exe|ps1)$/, "");
138
+ if (base.includes("codex")) return "codex";
139
+ if (base.includes("claude")) return "claude";
140
+ if (base.includes("gemini")) return "gemini";
141
+ if (base.includes("opencode")) return "opencode";
142
+ return "custom";
143
+ }
144
+
145
+ // Farkli paket yoneticileri / kurulum araclari CLI'lari farkli dizinlere koyar. Her PC'de
146
+ // calismasi icin yaygin npm/pnpm/yarn/bun/volta/scoop/winget/choco/homebrew konumlarini tarariz.
147
+ function commonCandidates(id) {
148
+ const home = os.homedir();
149
+ if (isWin) {
150
+ const APPDATA = process.env.APPDATA, LOCAL = process.env.LOCALAPPDATA, PD = process.env.ProgramData;
151
+ const exts = ["cmd", "exe", "ps1", "bat", ""];
152
+ const bases = [
153
+ APPDATA && path.join(APPDATA, "npm"),
154
+ LOCAL && path.join(LOCAL, "pnpm"),
155
+ LOCAL && path.join(LOCAL, "Yarn", "bin"),
156
+ LOCAL && path.join(LOCAL, "Microsoft", "WinGet", "Links"),
157
+ LOCAL && path.join(LOCAL, "Programs", id),
158
+ path.join(home, ".bun", "bin"),
159
+ path.join(home, ".volta", "bin"),
160
+ path.join(home, "scoop", "shims"),
161
+ PD && path.join(PD, "chocolatey", "bin"),
162
+ path.join(home, ".local", "bin"),
163
+ path.join(home, "AppData", "Roaming", "npm"),
164
+ ].filter(Boolean);
165
+ const out = [];
166
+ for (const base of bases) for (const ext of exts) out.push(path.join(base, ext ? `${id}.${ext}` : id));
167
+ return out;
168
+ }
169
+ return [
170
+ path.join(home, ".local", "bin", id),
171
+ path.join(home, "bin", id),
172
+ path.join(home, ".bun", "bin", id),
173
+ path.join(home, ".volta", "bin", id),
174
+ path.join(home, ".npm-global", "bin", id),
175
+ path.join(home, ".yarn", "bin", id),
176
+ path.join(home, ".local", "share", "pnpm", id),
177
+ `/usr/local/bin/${id}`,
178
+ `/opt/homebrew/bin/${id}`,
179
+ `/usr/bin/${id}`,
180
+ `/snap/bin/${id}`,
181
+ ];
182
+ }
183
+
184
+ // Bilinen bir CLI komutu adapter alanindan daha guvenilir kaynaktir. Eski veya elle
185
+ // duzenlenmis config'te adapter=claude + cmd=codex gibi bir celiski varsa hedef CLI'nin
186
+ // guvenli varsayilan argumanlarina donulur; boylece bir CLI'ye digerinin bayragi gitmez.
187
+ function normalizeAgentAdapter(agent) {
188
+ if (!agent || typeof agent !== "object") return false;
189
+ const configured = String(agent.adapter || "").trim().toLowerCase();
190
+ const inferred = adapterId(agent.cmd);
191
+ const adapter = inferred !== "custom" ? inferred : (DEFINITIONS[configured] ? configured : "custom");
192
+ let changed = false;
193
+ if (inferred !== "custom" && configured && configured !== inferred) {
194
+ agent.args = DEFINITIONS[inferred].defaultArgs.slice();
195
+ delete agent.model;
196
+ delete agent.modelOverride;
197
+ changed = true;
198
+ }
199
+ if (agent.adapter !== adapter) {
200
+ agent.adapter = adapter;
201
+ changed = true;
202
+ }
203
+ return changed;
204
+ }
205
+
206
+ function normalizeAgentAdapters(cfg) {
207
+ if (!cfg?.agents || typeof cfg.agents !== "object") return false;
208
+ let changed = false;
209
+ for (const agent of Object.values(cfg.agents)) if (normalizeAgentAdapter(agent)) changed = true;
210
+ return changed;
211
+ }
212
+
213
+ // cfg verilirse cfg.cliSettings[adapter] o CLI'nin varsayilan model ayari olarak uygulanir.
214
+ // Oncelik: agent.model (profil bazli) > cliSettings[adapter].model (global) > CLI varsayilani.
215
+ function effectiveAgent(agent, cfg) {
216
+ const copy = { ...agent, args: Array.isArray(agent.args) ? agent.args.map(String) : [] };
217
+ normalizeAgentAdapter(copy);
218
+ const adapter = copy.adapter;
219
+ const commandAdapter = adapterId(copy.cmd);
220
+ const settings = (cfg && cfg.cliSettings && cfg.cliSettings[adapter]) || {};
221
+ if (!copy.silenceTimeoutSeconds && DEFINITIONS[adapter]?.silenceTimeoutSeconds) copy.silenceTimeoutSeconds = DEFINITIONS[adapter].silenceTimeoutSeconds;
222
+ if (commandAdapter === adapter && RESOLVED.has(adapter) && !path.isAbsolute(copy.cmd)) copy.cmd = RESOLVED.get(adapter);
223
+ if (adapter === "codex") {
224
+ const commands = new Set(["exec", "review", "resume", "apply"]);
225
+ if (!copy.args.some((arg) => commands.has(arg))) copy.args.unshift("exec", "--skip-git-repo-check");
226
+ else if (copy.args.includes("exec") && !copy.args.includes("--skip-git-repo-check")) copy.args.splice(copy.args.indexOf("exec") + 1, 0, "--skip-git-repo-check");
227
+ const model = String(copy.model || settings.model || "").trim();
228
+ const effort = ["low", "medium", "high", "xhigh", "max", "ultra"].includes(String(settings.reasoningEffort)) ? String(settings.reasoningEffort) : "";
229
+ const serviceTier = /^[A-Za-z0-9._-]{1,64}$/.test(String(settings.serviceTier || "")) ? String(settings.serviceTier) : "";
230
+ if (model && !copy.args.includes("--model") && !copy.args.includes("-m")) copy.args.push("--model", model);
231
+ if (effort && !copy.args.some((arg) => String(arg).startsWith("model_reasoning_effort="))) copy.args.push("-c", `model_reasoning_effort="${effort}"`);
232
+ if (serviceTier && !copy.args.some((arg) => String(arg).startsWith("service_tier="))) copy.args.push("-c", `service_tier="${serviceTier}"`);
233
+ }
234
+ if (adapter === "claude") {
235
+ if (!copy.args.includes("-p") && !copy.args.includes("--print")) copy.args.unshift("-p");
236
+ if (!copy.args.includes("--output-format")) copy.args.push("--output-format", "text");
237
+ }
238
+ if (adapter === "gemini") {
239
+ // Otonom, non-interaktif calisma icin onay modu sart; yoksa izin bekleyip takilir.
240
+ if (!copy.args.includes("--approval-mode") && !copy.args.includes("-y") && !copy.args.includes("--yolo")) copy.args.push("--approval-mode", "yolo");
241
+ }
242
+ if (adapter === "opencode") {
243
+ if (!copy.args.includes("run")) copy.args.unshift("run");
244
+ // config.json'da eski otomatik profil kalmis olsa da uyumsuz bayragi temizle.
245
+ copy.args = copy.args.filter((arg) => arg !== "--auto");
246
+ const formatAt = copy.args.indexOf("--format");
247
+ if (formatAt >= 0) copy.args.splice(formatAt, 2);
248
+ copy.args.splice(copy.args.indexOf("run") + 1, 0, "--format", "json");
249
+ const hasModel = copy.args.includes("--model") || copy.args.includes("-m");
250
+ const model = copy.model || String(settings.model || "").trim() || selectOpenCodeModel(OPEN_CODE_MODELS);
251
+ if (!hasModel && model) copy.args.splice(copy.args.indexOf("run") + 3, 0, "--model", model);
252
+ if (model) copy.model = model;
253
+ if (!copy.args.includes("{PROMPT}") && !copy.args.includes("{PROMPT_FILE}")) {
254
+ copy.args.push("Attached file contains the full task. Follow it exactly, make the changes, and report what you did.", "--file", "{PROMPT_FILE}");
255
+ }
256
+ }
257
+ return copy;
258
+ }
259
+
260
+ function probeCommand(command, definition) {
261
+ try {
262
+ const result = spawnSync(command, definition.versionArgs, {
263
+ encoding: "utf8",
264
+ timeout: 12000,
265
+ windowsHide: true,
266
+ shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
267
+ });
268
+ const output = String(result.stdout || result.stderr || "").trim().split(/\r?\n/)[0];
269
+ return { installed: result.status === 0, version: result.status === 0 ? output : "", error: result.status === 0 ? "" : output, resolvedCommand: result.status === 0 ? command : "" };
270
+ } catch (error) {
271
+ return { installed: false, version: "", error: error.message, resolvedCommand: "" };
272
+ }
273
+ }
274
+
275
+ function probe(id, definition) {
276
+ let result = probeCommand(definition.command, definition);
277
+ let installedPath = "";
278
+ if (!result.installed) {
279
+ for (const candidate of commonCandidates(id)) {
280
+ if (!fs.existsSync(candidate)) continue;
281
+ if (!installedPath) installedPath = candidate;
282
+ result = probeCommand(candidate, definition);
283
+ if (result.installed) break;
284
+ }
285
+ // Bazi CLI'lar (or. gemini) --version cagrisinda cmd.exe uzerinden yavas acildigi icin
286
+ // zaman asimina ugrar. Ikili dosya diskte mevcutsa, versiyon okunamasa bile "kurulu" say;
287
+ // aksi halde kurulu CLI yanlislikla devre disi birakiliyordu.
288
+ if (!result.installed && installedPath) {
289
+ result = { installed: true, version: "kurulu", error: "", resolvedCommand: installedPath };
290
+ }
291
+ }
292
+ if (result.installed) RESOLVED.set(id, result.resolvedCommand);
293
+ if (result.installed && id === "opencode") {
294
+ OPEN_CODE_MODELS = listOpenCodeModels(result.resolvedCommand || definition.command);
295
+ result.models = OPEN_CODE_MODELS.slice();
296
+ result.recommendedModel = selectOpenCodeModel(OPEN_CODE_MODELS);
297
+ result.ready = Boolean(result.recommendedModel);
298
+ result.readinessError = result.ready ? "" : (OPEN_CODE_MODELS.length
299
+ ? "Kullanilabilir otomatik OpenCode modeli bulunamadi. Ayarlardan erisilebilir bir model secin."
300
+ : "OpenCode kurulu, ancak model listesi okunamadi. Once saglayici girisini tamamlayin.");
301
+ }
302
+ return result;
303
+ }
304
+
305
+ function discoverInstalled() {
306
+ return Object.entries(DEFINITIONS).map(([id, definition]) => ({ id, ...definition, ...probe(id, definition) }));
307
+ }
308
+
309
+ // Tek kaynak: hem gercek gorev calistirici (engine.runCli) hem saglik testi ve model
310
+ // listeleme ayni komut kurulumunu kullanir. Bare npm CLI adlarini where.exe ile fiziksel
311
+ // yola cevirmiyoruz; cmd.exe PATHEXT/PATH cozumlemesini Unicode olarak kendisi yapar.
312
+ function buildCommand(command, args) {
313
+ if (!isWin) return { file: command, args, shell: false };
314
+ const bare = !path.isAbsolute(command) && !/[\\/]/.test(command);
315
+ if (bare) return { file: command, args, shell: true };
316
+ if (!/\.(cmd|bat)$/i.test(command)) return { file: command, args, shell: false };
317
+ const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
318
+ return { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", `"${[command, ...args].map(quote).join(" ")}"`], shell: false, verbatim: true };
319
+ }
320
+
321
+ function healthPrompt(id) {
322
+ return [
323
+ "CLI_TEAM_HEALTH_CHECK",
324
+ `Bu ${id} cli aracinin kullanilabilirlik testidir.`,
325
+ "Yalnizca HEALTH_OK yaz.",
326
+ "Dosya olusturma, degistirme, silme, komut calistirma veya dis sisteme istek gonderme.",
327
+ ].join("\n");
328
+ }
329
+
330
+ // Normal gorev calistiricisi {PROMPT}/{PROMPT_FILE} yer tutucularini cagridan once
331
+ // doldurur. Health check de ayni sozlesmeyi kullanmali; aksi halde OpenCode literal
332
+ // `{PROMPT_FILE}` yolunu acmaya calisir ve saglikli agent katalogdan yanlislikla elenir.
333
+ function preparePromptArgs(args, prompt) {
334
+ let tempDir = "", promptFile = "", useStdin = true;
335
+ const prepared = (Array.isArray(args) ? args : []).map((arg) => {
336
+ const value = String(arg);
337
+ if (value.includes("{PROMPT}")) {
338
+ useStdin = false;
339
+ return value.replaceAll("{PROMPT}", prompt);
340
+ }
341
+ if (value.includes("{PROMPT_FILE}")) {
342
+ useStdin = false;
343
+ if (!promptFile) {
344
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cli-team-health-"));
345
+ promptFile = path.join(tempDir, "prompt.md");
346
+ fs.writeFileSync(promptFile, prompt, "utf8");
347
+ }
348
+ return value.replaceAll("{PROMPT_FILE}", promptFile);
349
+ }
350
+ return value;
351
+ });
352
+ return {
353
+ args: prepared,
354
+ useStdin,
355
+ promptFile,
356
+ cleanup() { if (tempDir) { try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {} } },
357
+ };
358
+ }
359
+
360
+ // Tek kaynak: OpenCode icin otonom izinler surumden bagimsiz olarak OPENCODE_CONFIG_CONTENT
361
+ // ile aktarilir. Hem engine.runCli hem saglik testi bu ortami kullanir ki test ile gercek
362
+ // calistirma ayni izinlerle calissin.
363
+ function agentEnvironment(agent) {
364
+ const env = { ...process.env };
365
+ if (agent.adapter !== "opencode") return env;
366
+ let inline = {};
367
+ try { inline = JSON.parse(env.OPENCODE_CONFIG_CONTENT || "{}"); } catch {}
368
+ env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ ...inline, permission: { "*": "allow" } });
369
+ return env;
370
+ }
371
+
372
+ function classifyHealthFailure(text) {
373
+ const raw = String(text || "");
374
+ if (/requires a newer version|upgrade to the latest|version.*unsupported|unsupported.*version|model metadata.*not found|unsupported.*model/i.test(raw)) return { status: "version-incompatible", label: "Sürüm uyumsuz", detail: raw };
375
+ if (/api key|unauthorized|unauthenticated|authentication|login required|not logged in|sign in|giriÅŸ gerekli/i.test(raw)) return { status: "auth-required", label: "Giriş gerekli", detail: raw };
376
+ if (/rate.?limit|quota|too many requests|billing|credit/i.test(raw)) return { status: "quota", label: "Kota veya ödeme gerekli", detail: raw };
377
+ if (/timeout|timed out|zaman aşım/i.test(raw)) return { status: "timeout", label: "Yanıt zaman aşımına uğradı", detail: raw };
378
+ return { status: "failed", label: "Test başarısız", detail: raw };
379
+ }
380
+
381
+ function testInstalledCli(id, options = {}) {
382
+ const definition = DEFINITIONS[id];
383
+ if (!definition) return Promise.resolve({ id, installed: false, health: { status: "unknown", label: "Bilinmiyor", detail: "Tanımsız CLI" } });
384
+ const found = probe(id, definition);
385
+ if (!found.installed) return Promise.resolve({ id, installed: false, version: found.version, error: found.error, health: { status: "not-installed", label: "Kurulu değil", detail: found.error || "CLI bulunamadı" } });
386
+ const agent = effectiveAgent({ adapter: id, cmd: definition.command, args: definition.defaultArgs.slice() }, options.cfg);
387
+ const prompt = healthPrompt(id);
388
+ const prepared = preparePromptArgs(agent.args, prompt);
389
+ const command = buildCommand(agent.cmd, prepared.args);
390
+ const timeoutMs = Math.max(10000, Number(options.timeoutMs || 45000));
391
+ return new Promise((resolve) => {
392
+ let stdout = "", stderr = "", settled = false;
393
+ let child;
394
+ try {
395
+ child = spawn(command.file, command.args, { env: agentEnvironment(agent), shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim });
396
+ } catch (error) {
397
+ prepared.cleanup();
398
+ return resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(error.message) });
399
+ }
400
+ const unregister = registerProbe(child);
401
+ const timer = setTimeout(() => {
402
+ if (settled) return;
403
+ settled = true;
404
+ unregister();
405
+ terminateProbeTree(child);
406
+ prepared.cleanup();
407
+ resolve({ id, installed: true, version: found.version, health: { status: "timeout", label: "Yanıt zaman aşımına uğradı", detail: `Sağlık testi ${Math.round(timeoutMs / 1000)} saniyede tamamlanmadı.` } });
408
+ }, timeoutMs);
409
+ child.stdout?.setEncoding("utf8");
410
+ child.stderr?.setEncoding("utf8");
411
+ child.stdout?.on("data", (data) => { stdout += String(data); });
412
+ child.stderr?.on("data", (data) => { stderr += String(data); });
413
+ child.on("error", (error) => {
414
+ if (settled) return;
415
+ settled = true;
416
+ clearTimeout(timer);
417
+ unregister();
418
+ prepared.cleanup();
419
+ const failure = classifyHealthFailure(error.message);
420
+ resolve({ id, installed: true, version: found.version, health: failure });
421
+ });
422
+ child.on("close", (code) => {
423
+ if (settled) return;
424
+ settled = true;
425
+ clearTimeout(timer);
426
+ unregister();
427
+ prepared.cleanup();
428
+ const raw = `${stdout}\n${stderr}`.trim();
429
+ if (code === 0 && /HEALTH_OK/i.test(raw)) resolve({ id, installed: true, version: found.version, health: { status: "ready", label: "Hazır", detail: "Gerçek sağlık testi başarılı." } });
430
+ else resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(raw || `Çıkış kodu ${code}`) });
431
+ });
432
+ child.stdin?.on("error", () => {});
433
+ child.stdin?.end(prepared.useStdin ? prompt : "");
434
+ });
435
+ }
436
+
437
+ async function healthCheckAll(discovered = discoverInstalled(), options = {}) {
438
+ const results = await Promise.all(discovered.map((item) => testInstalledCli(item.id, options)));
439
+ return discovered.map((item) => ({ ...item, ...(results.find((result) => result.id === item.id) || {}) }));
440
+ }
441
+
442
+ function listCodexModels(options = {}) {
443
+ const command = buildCommand("codex", ["app-server", "--listen", "stdio://"]);
444
+ const timeoutMs = Math.max(5000, Number(options.timeoutMs || 20000));
445
+ return new Promise((resolve, reject) => {
446
+ let buffer = "", settled = false, modelRequestSent = false;
447
+ let child, unregister = () => {};
448
+ const finish = (error, value) => {
449
+ if (settled) return;
450
+ settled = true;
451
+ clearTimeout(timer);
452
+ unregister();
453
+ terminateProbeTree(child);
454
+ if (error) reject(error); else resolve(value);
455
+ };
456
+ const timer = setTimeout(() => finish(new Error(`Codex model listesi ${Math.round(timeoutMs / 1000)} saniyede alınamadı.`)), timeoutMs);
457
+ try { child = spawn(command.file, command.args, { shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim }); }
458
+ catch (error) { finish(error); return; }
459
+ unregister = registerProbe(child);
460
+ child.stdout?.setEncoding("utf8");
461
+ child.stderr?.setEncoding("utf8");
462
+ child.stdout?.on("data", (chunk) => {
463
+ buffer += String(chunk);
464
+ const lines = buffer.split(/\r?\n/);
465
+ buffer = lines.pop() || "";
466
+ for (const line of lines) {
467
+ let message;
468
+ try { message = JSON.parse(line); } catch { continue; }
469
+ if (message.id === 1 && !modelRequestSent) {
470
+ modelRequestSent = true;
471
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "model/list", params: { includeHidden: false, limit: 200 } })}\n`);
472
+ } else if (message.id === 2) {
473
+ if (message.error) { finish(new Error(message.error.message || "Codex model listesi alınamadı.")); return; }
474
+ const models = Array.isArray(message.result?.data) ? message.result.data : [];
475
+ finish(null, models.filter((model) => model && model.hidden !== true).map((model) => ({
476
+ id: String(model.id || model.model || ""),
477
+ model: String(model.model || model.id || ""),
478
+ displayName: String(model.displayName || model.id || model.model || ""),
479
+ description: String(model.description || ""),
480
+ defaultReasoningEffort: String(model.defaultReasoningEffort || "medium"),
481
+ reasoningEfforts: (model.supportedReasoningEfforts || []).map((item) => ({
482
+ id: String(item.reasoningEffort || item.id || ""),
483
+ description: String(item.description || ""),
484
+ })).filter((item) => item.id),
485
+ serviceTiers: (model.serviceTiers || []).map((item) => ({
486
+ id: String(item.id || ""),
487
+ name: String(item.name || item.id || ""),
488
+ description: String(item.description || ""),
489
+ })).filter((item) => item.id),
490
+ additionalSpeedTiers: Array.isArray(model.additionalSpeedTiers) ? model.additionalSpeedTiers.map(String) : [],
491
+ defaultServiceTier: model.defaultServiceTier ? String(model.defaultServiceTier) : "",
492
+ isDefault: model.isDefault === true,
493
+ })).filter((model) => model.id));
494
+ }
495
+ }
496
+ });
497
+ child.stderr?.on("data", () => {});
498
+ child.on("error", finish);
499
+ child.stdin?.on("error", () => {});
500
+ child.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { clientInfo: { name: "crewctl", version: "1.0.0", title: "CrewCtl" } } })}\n`);
501
+ });
502
+ }
503
+
504
+ function addMissingAgents(cfg, discovered) {
505
+ cfg.agents ||= {};
506
+ const ignored = new Set(Array.isArray(cfg.discoveryIgnoredAdapters) ? cfg.discoveryIgnoredAdapters.map(String) : []);
507
+ let changed = normalizeAgentAdapters(cfg);
508
+ for (const cli of discovered) {
509
+ const usable = cli.installed && (cli.id !== "opencode" || cli.ready !== false || Boolean(cfg.cliSettings?.opencode?.model));
510
+ const matchingEntries = Object.entries(cfg.agents).filter(([, agent]) => (agent.adapter || adapterId(agent.cmd)) === cli.id);
511
+ const existingEntry = matchingEntries.find(([, agent]) => !agent.autoDiscovered) || matchingEntries[0];
512
+ const existing = existingEntry?.[1];
513
+ // Kullanici otomatik bulunan bir profili sildiyse bu karar kalicidir. O adapter'a ait
514
+ // elle olusturulmus profil varsa ona dokunma; yalnizca autoDiscovered kaydini temizle.
515
+ if (ignored.has(cli.id)) {
516
+ for (const [name, agent] of matchingEntries) {
517
+ if (!agent.autoDiscovered) continue;
518
+ delete cfg.agents[name];
519
+ changed = true;
520
+ }
521
+ continue;
522
+ }
523
+ if (existing) {
524
+ if (usable && existing.autoDiscovered && existing.unavailablePlaceholder) {
525
+ existing.enabled = true;
526
+ existing.args = cli.defaultArgs.slice();
527
+ delete existing.unavailablePlaceholder;
528
+ changed = true;
529
+ }
530
+ // Eski surumler otomatik oneriyi agent.model alanina yaziyordu. Bu alan global
531
+ // CLI ayarini ezdigi icin, kullanici tarafindan acikca override olarak isaretlenmemis
532
+ // otomatik profiller modeli daima CLI ayarindan/otomatik secimden miras alir.
533
+ if (cli.id === "opencode" && existing.autoDiscovered && existing.model && existing.modelOverride !== true) {
534
+ delete existing.model;
535
+ changed = true;
536
+ }
537
+ if (cli.installed && cli.id === "opencode" && existing.autoDiscovered && Number(existing.timeoutSeconds || 0) < cli.timeoutSeconds) {
538
+ existing.timeoutSeconds = cli.timeoutSeconds;
539
+ changed = true;
540
+ }
541
+ if (!usable && existing.autoDiscovered && existing.enabled !== false) {
542
+ existing.enabled = false;
543
+ existing.unavailablePlaceholder = true;
544
+ changed = true;
545
+ }
546
+ continue;
547
+ }
548
+ // OpenCode adapteri her cihazda katalogda gorunur. Kurulu degilse guvenli
549
+ // sekilde devre disi placeholder olur; taramada bulundugunda otomatik etkinlesir.
550
+ if (!cli.installed && cli.id !== "opencode") continue;
551
+ let name = `${cli.id}-auto`, suffix = 2;
552
+ while (cfg.agents[name]) name = `${cli.id}-auto-${suffix++}`;
553
+ cfg.agents[name] = {
554
+ adapter: cli.id,
555
+ cmd: cli.command,
556
+ args: cli.defaultArgs.slice(),
557
+ enabled: usable,
558
+ description: cli.description,
559
+ capabilities: cli.capabilities.slice(),
560
+ roleFile: cli.roleFile,
561
+ costTier: "standard",
562
+ timeoutSeconds: cli.timeoutSeconds || 1200,
563
+ ...(cli.silenceTimeoutSeconds ? { silenceTimeoutSeconds: cli.silenceTimeoutSeconds } : {}),
564
+ autoDiscovered: true,
565
+ ...(!usable ? { unavailablePlaceholder: true } : {}),
566
+ };
567
+ changed = true;
568
+ }
569
+ return changed;
570
+ }
571
+
572
+ // Operator, uzman agent'lardan BAGIMSIZ bir CLI secimidir (claude/codex/gemini/opencode).
573
+ // operator.md rolunu bu CLI giyer; uzman agent'lar operatorun ALTINDA delege edilir.
574
+ // Bir uzman agent silinse bile operator etkilenmez.
575
+ function operatorSpec(cli, cfg) {
576
+ const op = (cfg && cfg.operator) || {};
577
+ const definition = DEFINITIONS[cli];
578
+ // Standart CLI'lar DEFINITIONS'tan cozulur. cfg.operator.cmd verilirse (ozel/test operatoru)
579
+ // dogrudan o komut kullanilir; boylece operator, uzman agent'lardan tamamen bagimsiz kalir.
580
+ const cmd = op.cmd || (definition && definition.command);
581
+ if (!cmd) return null;
582
+ const args = op.cmd ? (Array.isArray(op.args) ? op.args.slice() : []) : (definition ? definition.defaultArgs.slice() : []);
583
+ // Model/effort/tier secimi CLI bazlidir (cfg.cliSettings) ve effectiveAgent icinde uygulanir;
584
+ // ayni ayar o CLI'yi kullanan operator ve tum uzman agent'lar icin gecerlidir.
585
+ const timeoutSeconds = op.timeoutSeconds || (cfg && cfg.agentTimeoutSeconds) || 900;
586
+ const silenceTimeoutSeconds = op.silenceTimeoutSeconds || (definition && definition.silenceTimeoutSeconds) || (cfg && cfg.cliSilenceTimeoutSeconds) || 300;
587
+ const adapter = definition ? cli : (op.adapter || adapterId(cmd));
588
+ return effectiveAgent({ adapter, cmd, args, timeoutSeconds, silenceTimeoutSeconds }, cfg);
589
+ }
590
+
591
+ // Operator gecerli, kurulu bir CLI olmali. Eski semada operator.agent (uzman agent adi)
592
+ // tutuluyordu; burada operator.cli'ye tasinir. Kurulu degilse ilk kurulu CLI'ye gecilir.
593
+ function ensureValidOperator(cfg, discovered) {
594
+ cfg.operator ||= {};
595
+ let changed = false;
596
+ if (cfg.operator.roleFile !== "roles/operator.md") { cfg.operator.roleFile = "roles/operator.md"; changed = true; }
597
+ if (!cfg.operator.cli) {
598
+ const legacy = cfg.operator.agent;
599
+ const source = legacy && cfg.agents?.[legacy] ? cfg.agents[legacy].cmd : legacy;
600
+ cfg.operator.cli = adapterId(source || "codex");
601
+ changed = true;
602
+ }
603
+ if ("agent" in cfg.operator) { delete cfg.operator.agent; changed = true; }
604
+ const installed = Array.isArray(discovered) ? discovered
605
+ .filter((x) => x.installed && (x.id !== "opencode" || x.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)))
606
+ .map((x) => x.id) : null;
607
+ const known = !!DEFINITIONS[cfg.operator.cli];
608
+ const availableOnHost = !installed || installed.length === 0 || installed.includes(cfg.operator.cli);
609
+ if (!known || !availableOnHost) {
610
+ const fallback = (installed && installed[0]) || Object.keys(DEFINITIONS)[0];
611
+ if (fallback && fallback !== cfg.operator.cli) { cfg.operator.cli = fallback; changed = true; }
612
+ }
613
+ return changed;
614
+ }
615
+
616
+ module.exports = { DEFINITIONS, KNOWN_CLIS: Object.keys(DEFINITIONS), adapterId, normalizeAgentAdapter, normalizeAgentAdapters, effectiveAgent, preparePromptArgs, operatorSpec, buildCommand, agentEnvironment, discoverInstalled, healthCheckAll, listCodexModels, abortActiveProbes, selectOpenCodeModel, parseOpenCodeModels, addMissingAgents, ensureValidOperator };