@omerrgocmen/crewctl 1.3.0 → 1.3.1

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 (92) hide show
  1. package/README.md +193 -193
  2. package/orchestrator/README.md +473 -473
  3. package/orchestrator/config.default.json +90 -79
  4. package/orchestrator/roles/executor.md +57 -57
  5. package/orchestrator/roles/operator.md +77 -77
  6. package/orchestrator/roles/planner.md +58 -58
  7. package/orchestrator/roles/reviewer.md +63 -63
  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/checkpoints.js +0 -4
  69. package/orchestrator/src/cli-registry.js +848 -740
  70. package/orchestrator/src/cli.js +140 -140
  71. package/orchestrator/src/doctor.js +64 -64
  72. package/orchestrator/src/engine.js +1914 -1735
  73. package/orchestrator/src/schedule.js +124 -126
  74. package/orchestrator/src/server.js +817 -818
  75. package/orchestrator/src/skill-registry.js +272 -272
  76. package/orchestrator/src/store.js +478 -470
  77. package/orchestrator/web/OrbitControls.js +1417 -1417
  78. package/orchestrator/web/board.html +146 -146
  79. package/orchestrator/web/code.html +213 -213
  80. package/orchestrator/web/flow.html +741 -741
  81. package/orchestrator/web/index.html +622 -602
  82. package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -231
  83. package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -104
  84. package/orchestrator/web/jsm/postprocessing/Pass.js +95 -95
  85. package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -99
  86. package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -77
  87. package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -415
  88. package/orchestrator/web/jsm/shaders/CopyShader.js +45 -45
  89. package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -66
  90. package/orchestrator/web/site.webmanifest +12 -12
  91. package/orchestrator/web/three.module.min.js +6 -6
  92. package/package.json +51 -51
@@ -1,740 +1,848 @@
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
- // Iptal sayaci: motor bir goreve baslarken probe'lari oldurdugunde artar. Oldurulen probe
87
- // "cikis kodu != 0" ile doner ve saglik siniflandiricisi bunu GERCEK bir ariza sanardi;
88
- // sonuc 6 saatlik onbellege yazilinca CLI o sure boyunca yanlislikla kullanilamaz kalirdi.
89
- // refreshCliHealth bu sayaci once/sonra karsilastirip kirlenmis sonucu ONBELLEGE YAZMAZ.
90
- let probeGeneration = 0;
91
- function currentProbeGeneration() {
92
- return probeGeneration;
93
- }
94
-
95
- function abortActiveProbes() {
96
- const count = ACTIVE_PROBES.size;
97
- if (count) probeGeneration++;
98
- for (const child of ACTIVE_PROBES) terminateProbeTree(child);
99
- ACTIVE_PROBES.clear();
100
- return count;
101
- }
102
-
103
- function selectOpenCodeModel(models) {
104
- const list = Array.isArray(models) ? models.map(String).filter(Boolean) : [];
105
- const priorities = [
106
- (x) => x === "opencode/big-pickle",
107
- (x) => /^opencode\/.+free$/i.test(x),
108
- (x) => x.startsWith("opencode/"),
109
- (x) => x.startsWith("opencode-go/"),
110
- (x) => x.startsWith("minimax-coding-plan/"),
111
- (x) => !x.startsWith("ollama/"),
112
- ];
113
- for (const match of priorities) {
114
- const found = list.find(match);
115
- if (found) return found;
116
- }
117
- return "";
118
- }
119
-
120
- // `opencode models` ciktisindan saglayici/model satirlarini ayiklar. Baslik, bos satir ve
121
- // log gurultusunu eler; her "saglayici/model" bicimli satir gecerli sayilir.
122
- function parseOpenCodeModels(stdout) {
123
- const found = new Set();
124
- for (const line of String(stdout || "").split(/\r?\n/)) {
125
- const model = line.trim();
126
- if (/^[A-Za-z0-9._-]+\/.+/.test(model) && !/\s/.test(model)) found.add(model);
127
- }
128
- return [...found];
129
- }
130
-
131
- function listOpenCodeModels(command) {
132
- try {
133
- // Tum saglayicilarin listesi sorgulanir. Kullanicilarin ucretli abonelikleri
134
- // (opencode-go, minimax-coding-plan vb.) ayri saglayici olarak gorunur; yalnizca
135
- // "opencode" saglayicisini sorgulamak bu modelleri gizliyor ve kurulum yanlislikla
136
- // "hazir degil" sayiliyordu. Otomatik oneri siralamasini selectOpenCodeModel yapar.
137
- const result = spawnSync(command, ["models"], {
138
- encoding: "utf8", timeout: 20000, windowsHide: true,
139
- shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
140
- });
141
- if (result.status !== 0) return [];
142
- return parseOpenCodeModels(result.stdout);
143
- } catch { return []; }
144
- }
145
-
146
- function adapterId(command) {
147
- const base = path.basename(String(command || "")).toLowerCase().replace(/\.(cmd|bat|exe|ps1)$/, "");
148
- if (base.includes("codex")) return "codex";
149
- if (base.includes("claude")) return "claude";
150
- if (base.includes("gemini")) return "gemini";
151
- if (base.includes("opencode")) return "opencode";
152
- return "custom";
153
- }
154
-
155
- // Farkli paket yoneticileri / kurulum araclari CLI'lari farkli dizinlere koyar. Her PC'de
156
- // calismasi icin yaygin npm/pnpm/yarn/bun/volta/scoop/winget/choco/homebrew konumlarini tarariz.
157
- function commonCandidates(id) {
158
- const home = os.homedir();
159
- if (isWin) {
160
- const APPDATA = process.env.APPDATA, LOCAL = process.env.LOCALAPPDATA, PD = process.env.ProgramData;
161
- const exts = ["cmd", "exe", "ps1", "bat", ""];
162
- const bases = [
163
- APPDATA && path.join(APPDATA, "npm"),
164
- LOCAL && path.join(LOCAL, "pnpm"),
165
- LOCAL && path.join(LOCAL, "Yarn", "bin"),
166
- LOCAL && path.join(LOCAL, "Microsoft", "WinGet", "Links"),
167
- LOCAL && path.join(LOCAL, "Programs", id),
168
- path.join(home, ".bun", "bin"),
169
- path.join(home, ".volta", "bin"),
170
- path.join(home, "scoop", "shims"),
171
- PD && path.join(PD, "chocolatey", "bin"),
172
- path.join(home, ".local", "bin"),
173
- path.join(home, "AppData", "Roaming", "npm"),
174
- ].filter(Boolean);
175
- const out = [];
176
- for (const base of bases) for (const ext of exts) out.push(path.join(base, ext ? `${id}.${ext}` : id));
177
- return out;
178
- }
179
- return [
180
- path.join(home, ".local", "bin", id),
181
- path.join(home, "bin", id),
182
- path.join(home, ".bun", "bin", id),
183
- path.join(home, ".volta", "bin", id),
184
- path.join(home, ".npm-global", "bin", id),
185
- path.join(home, ".yarn", "bin", id),
186
- path.join(home, ".local", "share", "pnpm", id),
187
- `/usr/local/bin/${id}`,
188
- `/opt/homebrew/bin/${id}`,
189
- `/usr/bin/${id}`,
190
- `/snap/bin/${id}`,
191
- ];
192
- }
193
-
194
- // Bilinen bir CLI komutu adapter alanindan daha guvenilir kaynaktir. Eski veya elle
195
- // duzenlenmis config'te adapter=claude + cmd=codex gibi bir celiski varsa hedef CLI'nin
196
- // guvenli varsayilan argumanlarina donulur; boylece bir CLI'ye digerinin bayragi gitmez.
197
- function normalizeAgentAdapter(agent) {
198
- if (!agent || typeof agent !== "object") return false;
199
- const configured = String(agent.adapter || "").trim().toLowerCase();
200
- const inferred = adapterId(agent.cmd);
201
- const adapter = inferred !== "custom" ? inferred : (DEFINITIONS[configured] ? configured : "custom");
202
- let changed = false;
203
- if (inferred !== "custom" && configured && configured !== inferred) {
204
- agent.args = DEFINITIONS[inferred].defaultArgs.slice();
205
- delete agent.model;
206
- delete agent.modelOverride;
207
- changed = true;
208
- }
209
- if (agent.adapter !== adapter) {
210
- agent.adapter = adapter;
211
- changed = true;
212
- }
213
- return changed;
214
- }
215
-
216
- function normalizeAgentAdapters(cfg) {
217
- if (!cfg?.agents || typeof cfg.agents !== "object") return false;
218
- let changed = false;
219
- for (const agent of Object.values(cfg.agents)) if (normalizeAgentAdapter(agent)) changed = true;
220
- return changed;
221
- }
222
-
223
- // Ajan hapsi acik mi? cfg.sandbox.mode === "workspace" ise CLI'lar calisma klasoru DISINA
224
- // yazamaz/aga cikamaz. cfg verilmeyen (test/edge) cagrilar hapissiz kalir; boylece mevcut
225
- // arguman-esitlik testleri aynen gecer. Gercek calistirmada normalizeConfig mode'u daima
226
- // doldurur (varsayilan "workspace").
227
- function confineWorkspace(cfg) {
228
- return cfg?.sandbox?.mode === "workspace";
229
- }
230
- function sandboxWritableRoots(cfg) {
231
- const roots = cfg?.sandbox?.extraWritableDirs;
232
- return Array.isArray(roots) ? roots.filter((x) => typeof x === "string" && x.trim()).map(String) : [];
233
- }
234
-
235
- // cfg verilirse cfg.cliSettings[adapter] o CLI'nin varsayilan model ayari olarak uygulanir.
236
- // Oncelik: agent.model (profil bazli) > cliSettings[adapter].model (global) > CLI varsayilani.
237
- function effectiveAgent(agent, cfg) {
238
- const copy = { ...agent, args: Array.isArray(agent.args) ? agent.args.map(String) : [] };
239
- normalizeAgentAdapter(copy);
240
- const adapter = copy.adapter;
241
- const commandAdapter = adapterId(copy.cmd);
242
- const settings = (cfg && cfg.cliSettings && cfg.cliSettings[adapter]) || {};
243
- if (!copy.silenceTimeoutSeconds && DEFINITIONS[adapter]?.silenceTimeoutSeconds) copy.silenceTimeoutSeconds = DEFINITIONS[adapter].silenceTimeoutSeconds;
244
- if (commandAdapter === adapter && RESOLVED.has(adapter) && !path.isAbsolute(copy.cmd)) copy.cmd = RESOLVED.get(adapter);
245
- if (adapter === "codex") {
246
- const commands = new Set(["exec", "review", "resume", "apply"]);
247
- if (!copy.args.some((arg) => commands.has(arg))) copy.args.unshift("exec", "--skip-git-repo-check");
248
- 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");
249
- // Hapis: calisma klasoru (spawn cwd = codex "workspace") disina yazma ve ag engellenir.
250
- // Native sandbox — mac Seatbelt / Linux Landlock+seccomp / Windows restricted-token+ACL —
251
- // Docker/Git GEREKTIRMEZ. Kullanici acikca kendi -s/--sandbox veya bypass bayragini
252
- // koyduysa ona dokunmayiz. Bayrak, model/effort/tier PUSH'undan ONCE ve exec/skip-git'ten
253
- // HEMEN SONRA eklenir; testlerin args.slice(0,2) ve slice(-6) beklentileri korunur.
254
- const alreadySandboxed = copy.args.some((arg) => arg === "-s" || arg === "--sandbox" || arg === "--dangerously-bypass-approvals-and-sandbox" || arg === "--full-auto");
255
- if (confineWorkspace(cfg) && !alreadySandboxed) {
256
- const anchor = copy.args.indexOf("--skip-git-repo-check");
257
- const at = anchor >= 0 ? anchor + 1 : Math.max(copy.args.indexOf("exec") + 1, 0);
258
- const flags = ["-s", "workspace-write"];
259
- const roots = sandboxWritableRoots(cfg);
260
- if (roots.length) flags.push("-c", `sandbox_workspace_write.writable_roots=${JSON.stringify(roots)}`);
261
- copy.args.splice(at, 0, ...flags);
262
- }
263
- const model = String(copy.model || settings.model || "").trim();
264
- const effort = ["low", "medium", "high", "xhigh", "max", "ultra"].includes(String(settings.reasoningEffort)) ? String(settings.reasoningEffort) : "";
265
- const serviceTier = /^[A-Za-z0-9._-]{1,64}$/.test(String(settings.serviceTier || "")) ? String(settings.serviceTier) : "";
266
- if (model && !copy.args.includes("--model") && !copy.args.includes("-m")) copy.args.push("--model", model);
267
- if (effort && !copy.args.some((arg) => String(arg).startsWith("model_reasoning_effort="))) copy.args.push("-c", `model_reasoning_effort="${effort}"`);
268
- if (serviceTier && !copy.args.some((arg) => String(arg).startsWith("service_tier="))) copy.args.push("-c", `service_tier="${serviceTier}"`);
269
- }
270
- if (adapter === "claude") {
271
- if (!copy.args.includes("-p") && !copy.args.includes("--print")) copy.args.unshift("-p");
272
- if (!copy.args.includes("--output-format")) copy.args.push("--output-format", "text");
273
- }
274
- if (adapter === "gemini") {
275
- // Otonom, non-interaktif calisma icin onay modu sart; yoksa izin bekleyip takilir.
276
- if (!copy.args.includes("--approval-mode") && !copy.args.includes("-y") && !copy.args.includes("--yolo")) copy.args.push("--approval-mode", "yolo");
277
- }
278
- if (adapter === "opencode") {
279
- if (!copy.args.includes("run")) copy.args.unshift("run");
280
- // config.json'da eski otomatik profil kalmis olsa da uyumsuz bayragi temizle.
281
- copy.args = copy.args.filter((arg) => arg !== "--auto");
282
- const formatAt = copy.args.indexOf("--format");
283
- if (formatAt >= 0) copy.args.splice(formatAt, 2);
284
- copy.args.splice(copy.args.indexOf("run") + 1, 0, "--format", "json");
285
- const hasModel = copy.args.includes("--model") || copy.args.includes("-m");
286
- const model = copy.model || String(settings.model || "").trim() || selectOpenCodeModel(OPEN_CODE_MODELS);
287
- if (!hasModel && model) copy.args.splice(copy.args.indexOf("run") + 3, 0, "--model", model);
288
- if (model) copy.model = model;
289
- if (!copy.args.includes("{PROMPT}") && !copy.args.includes("{PROMPT_FILE}")) {
290
- copy.args.push("Attached file contains the full task. Follow it exactly, make the changes, and report what you did.", "--file", "{PROMPT_FILE}");
291
- }
292
- }
293
- return copy;
294
- }
295
-
296
- function probeCommand(command, definition) {
297
- try {
298
- const result = spawnSync(command, definition.versionArgs, {
299
- encoding: "utf8",
300
- timeout: 12000,
301
- windowsHide: true,
302
- shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
303
- });
304
- const output = String(result.stdout || result.stderr || "").trim().split(/\r?\n/)[0];
305
- return { installed: result.status === 0, version: result.status === 0 ? output : "", error: result.status === 0 ? "" : output, resolvedCommand: result.status === 0 ? command : "" };
306
- } catch (error) {
307
- return { installed: false, version: "", error: error.message, resolvedCommand: "" };
308
- }
309
- }
310
-
311
- function probe(id, definition) {
312
- let result = probeCommand(definition.command, definition);
313
- let installedPath = "";
314
- if (!result.installed) {
315
- for (const candidate of commonCandidates(id)) {
316
- if (!fs.existsSync(candidate)) continue;
317
- if (!installedPath) installedPath = candidate;
318
- result = probeCommand(candidate, definition);
319
- if (result.installed) break;
320
- }
321
- // Bazi CLI'lar (or. gemini) --version cagrisinda cmd.exe uzerinden yavas acildigi icin
322
- // zaman asimina ugrar. Ikili dosya diskte mevcutsa, versiyon okunamasa bile "kurulu" say;
323
- // aksi halde kurulu CLI yanlislikla devre disi birakiliyordu.
324
- if (!result.installed && installedPath) {
325
- result = { installed: true, version: "kurulu", error: "", resolvedCommand: installedPath };
326
- }
327
- }
328
- if (result.installed) RESOLVED.set(id, result.resolvedCommand);
329
- if (result.installed && id === "opencode") {
330
- OPEN_CODE_MODELS = listOpenCodeModels(result.resolvedCommand || definition.command);
331
- result.models = OPEN_CODE_MODELS.slice();
332
- result.recommendedModel = selectOpenCodeModel(OPEN_CODE_MODELS);
333
- result.ready = Boolean(result.recommendedModel);
334
- result.readinessError = result.ready ? "" : (OPEN_CODE_MODELS.length
335
- ? "Kullanilabilir otomatik OpenCode modeli bulunamadi. Ayarlardan erisilebilir bir model secin."
336
- : "OpenCode kurulu, ancak model listesi okunamadi. Once saglayici girisini tamamlayin.");
337
- }
338
- return result;
339
- }
340
-
341
- // Her acilista tum CLI'lar --version ile yoklaniyordu; olculen maliyet 4.8 saniye ve panel
342
- // bu sure boyunca hic acilmiyordu. Onbellek YALNIZCA su kosulda kullanilir: CLI daha once
343
- // KURULU bulunmus VE cozulmus ikili dosya HALA diskte duruyor. Bu durumda tekrar yoklamak
344
- // zaten bilinen bir cevabi dogrulamaktan ibarettir.
345
- //
346
- // Bilerek onbelleklenmeyenler (dogruluk hizdan onceliklidir):
347
- // - Daha once "kurulu degil" bulunanlar -> yeni kurulan CLI ilk acilista gorunur.
348
- // - Diskte bulunamayan cozulmus yol -> kaldirilan CLI hemen fark edilir.
349
- // - opencode -> model listesi/ready durumu degisebilir;
350
- // her acilista tam yoklanir, davranisi aynen korunur.
351
- // "Yeniden Tara" (/api/cli/discover) daima force ile cagirir ve onbellegi tumden atlar.
352
- const DISCOVERY_CACHE_VERSION = 1;
353
- const ALWAYS_PROBE = new Set(["opencode"]);
354
-
355
- // probeCommand, CLI'yi PATH uzerinden buldugunda resolvedCommand'i CIPLAK AD olarak dondurur
356
- // ("codex"), mutlak yol olarak degil. Duz fs.existsSync bu durumda daima false verir ve
357
- // onbellek hic tutmaz. Bu yuzden ciplak adlar PATH (+ Windows'ta PATHEXT) uzerinde aranir:
358
- // spawn maliyeti olmadan, birkac milisaniyede ve dogru sonucla.
359
- function resolvesOnPath(command) {
360
- if (!command) return false;
361
- if (command.includes("/") || command.includes("\\")) return fs.existsSync(command);
362
- const dirs = String(process.env.PATH || "").split(path.delimiter).filter(Boolean);
363
- const exts = isWin ? String(process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
364
- for (const dir of dirs) {
365
- for (const ext of exts) {
366
- try { if (fs.existsSync(path.join(dir, command + ext))) return true; } catch {}
367
- }
368
- }
369
- return false;
370
- }
371
-
372
- function cachedProbeUsable(entry) {
373
- return Boolean(entry && entry.installed && entry.resolvedCommand && resolvesOnPath(entry.resolvedCommand));
374
- }
375
-
376
- // OpenCode model listesi persist EDILMEZ; her acilista `opencode models` ile bellekten
377
- // uretilir. O cagri acilista gecici olarak yavas/basarisiz olursa (soguk ilk cagri, auth
378
- // oturmamis, ag) liste bos kalir ve panel model dropdown'i o process boyunca bos gorunur.
379
- // codex icin codexModelCache ile cozulen bu kirilganligi opencode icin de kapatiriz: prob
380
- // bos donerse son basarili liste (config'te saklanan) fallback olarak kullanilir. Fallback
381
- // ayni zamanda OPEN_CODE_MODELS'i doldurur ki otomatik model secimi de calissin.
382
- function applyOpenCodeFallback(entry, fallback) {
383
- if (!entry || entry.id !== "opencode" || !entry.installed) return entry;
384
- if ((entry.models && entry.models.length) || !Array.isArray(fallback) || !fallback.length) return entry;
385
- const models = fallback.map(String).filter(Boolean);
386
- if (!models.length) return entry;
387
- OPEN_CODE_MODELS = models.slice();
388
- entry.models = models.slice();
389
- entry.recommendedModel = selectOpenCodeModel(models);
390
- entry.ready = Boolean(entry.recommendedModel);
391
- entry.modelsFromCache = true;
392
- if (entry.ready) entry.readinessError = "";
393
- return entry;
394
- }
395
-
396
- function discoverInstalled(options = {}) {
397
- const cache = options.cache?.version === DISCOVERY_CACHE_VERSION ? options.cache.results || {} : {};
398
- const force = options.force === true;
399
- const openCodeFallback = Array.isArray(options.openCodeModels) ? options.openCodeModels : [];
400
- return Object.entries(DEFINITIONS).map(([id, definition]) => {
401
- const cached = cache[id];
402
- if (!force && !ALWAYS_PROBE.has(id) && cachedProbeUsable(cached)) {
403
- RESOLVED.set(id, cached.resolvedCommand);
404
- return { id, ...definition, installed: true, version: cached.version || "kurulu", error: "", resolvedCommand: cached.resolvedCommand, fromCache: true };
405
- }
406
- const result = { id, ...definition, ...probe(id, definition) };
407
- return id === "opencode" ? applyOpenCodeFallback(result, openCodeFallback) : result;
408
- });
409
- }
410
-
411
- // Bir sonraki acilista kullanilacak onbellek gorununu uretir. Yalnizca kurulu ve yolu
412
- // cozulmus girdiler saklanir; digerleri zaten her acilista yeniden yoklanir.
413
- function discoveryCacheFrom(discovered) {
414
- const results = {};
415
- for (const cli of discovered || []) {
416
- if (cli.installed && cli.resolvedCommand && !ALWAYS_PROBE.has(cli.id)) {
417
- results[cli.id] = { installed: true, version: cli.version || "kurulu", resolvedCommand: cli.resolvedCommand };
418
- }
419
- }
420
- return { version: DISCOVERY_CACHE_VERSION, checkedAt: new Date().toISOString(), results };
421
- }
422
-
423
- // Tek kaynak: hem gercek gorev calistirici (engine.runCli) hem saglik testi ve model
424
- // listeleme ayni komut kurulumunu kullanir. Bare npm CLI adlarini where.exe ile fiziksel
425
- // yola cevirmiyoruz; cmd.exe PATHEXT/PATH cozumlemesini Unicode olarak kendisi yapar.
426
- function buildCommand(command, args) {
427
- if (!isWin) return { file: command, args, shell: false };
428
- const bare = !path.isAbsolute(command) && !/[\\/]/.test(command);
429
- if (bare) {
430
- // shell:true iken Node argumanlari birlestirip cmd.exe'ye verir ve OTOMATIK TIRNAK KOYMAZ.
431
- // Bosluk iceren yol argumanlari (or. "C:\Users\John Doe\...\x.settings.json" veya
432
- // {PROMPT_FILE}) ikiye bolunurdu; bu da --settings/--file yolunu bozardi. Bosluk veya
433
- // kabuk metakarakteri iceren argumanlari tirnakla; sade bayrak/model adlari dokunulmaz.
434
- const shellQuote = (value) => {
435
- const v = String(value);
436
- return /[\s"^&|<>()%!]/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
437
- };
438
- return { file: command, args: args.map(shellQuote), shell: true };
439
- }
440
- if (!/\.(cmd|bat)$/i.test(command)) return { file: command, args, shell: false };
441
- const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
442
- return { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", `"${[command, ...args].map(quote).join(" ")}"`], shell: false, verbatim: true };
443
- }
444
-
445
- function healthPrompt(id) {
446
- return [
447
- "CLI_TEAM_HEALTH_CHECK",
448
- `Bu ${id} cli aracinin kullanilabilirlik testidir.`,
449
- "Yalnizca HEALTH_OK yaz.",
450
- "Dosya olusturma, degistirme, silme, komut calistirma veya dis sisteme istek gonderme.",
451
- ].join("\n");
452
- }
453
-
454
- // Normal gorev calistiricisi {PROMPT}/{PROMPT_FILE} yer tutucularini cagridan once
455
- // doldurur. Health check de ayni sozlesmeyi kullanmali; aksi halde OpenCode literal
456
- // `{PROMPT_FILE}` yolunu acmaya calisir ve saglikli agent katalogdan yanlislikla elenir.
457
- function preparePromptArgs(args, prompt) {
458
- let tempDir = "", promptFile = "", useStdin = true;
459
- const prepared = (Array.isArray(args) ? args : []).map((arg) => {
460
- const value = String(arg);
461
- if (value.includes("{PROMPT}")) {
462
- useStdin = false;
463
- return value.replaceAll("{PROMPT}", prompt);
464
- }
465
- if (value.includes("{PROMPT_FILE}")) {
466
- useStdin = false;
467
- if (!promptFile) {
468
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cli-team-health-"));
469
- promptFile = path.join(tempDir, "prompt.md");
470
- fs.writeFileSync(promptFile, prompt, "utf8");
471
- }
472
- return value.replaceAll("{PROMPT_FILE}", promptFile);
473
- }
474
- return value;
475
- });
476
- return {
477
- args: prepared,
478
- useStdin,
479
- promptFile,
480
- cleanup() { if (tempDir) { try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {} } },
481
- };
482
- }
483
-
484
- // Tek kaynak: OpenCode icin otonom izinler surumden bagimsiz olarak OPENCODE_CONFIG_CONTENT
485
- // ile aktarilir. Hem engine.runCli hem saglik testi bu ortami kullanir ki test ile gercek
486
- // calistirma ayni izinlerle calissin.
487
- function agentEnvironment(agent) {
488
- const env = { ...process.env };
489
- if (agent.adapter !== "opencode") return env;
490
- let inline = {};
491
- try { inline = JSON.parse(env.OPENCODE_CONFIG_CONTENT || "{}"); } catch {}
492
- env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ ...inline, permission: { "*": "allow" } });
493
- return env;
494
- }
495
-
496
- function classifyHealthFailure(text) {
497
- const raw = String(text || "");
498
- 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 };
499
- 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 };
500
- if (/rate.?limit|quota|too many requests|billing|credit/i.test(raw)) return { status: "quota", label: "Kota veya ödeme gerekli", detail: raw };
501
- if (/timeout|timed out|zaman aşım/i.test(raw)) return { status: "timeout", label: "Yanıt zaman aşımına uğradı", detail: raw };
502
- return { status: "failed", label: "Test başarısız", detail: raw };
503
- }
504
-
505
- function testInstalledCli(id, options = {}) {
506
- const definition = DEFINITIONS[id];
507
- if (!definition) return Promise.resolve({ id, installed: false, health: { status: "unknown", label: "Bilinmiyor", detail: "Tanımsız CLI" } });
508
- const found = probe(id, definition);
509
- 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ı" } });
510
- const agent = effectiveAgent({ adapter: id, cmd: definition.command, args: definition.defaultArgs.slice() }, options.cfg);
511
- const prompt = healthPrompt(id);
512
- const prepared = preparePromptArgs(agent.args, prompt);
513
- const command = buildCommand(agent.cmd, prepared.args);
514
- const timeoutMs = Math.max(10000, Number(options.timeoutMs || 45000));
515
- return new Promise((resolve) => {
516
- let stdout = "", stderr = "", settled = false;
517
- let child;
518
- try {
519
- child = spawn(command.file, command.args, { env: agentEnvironment(agent), shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim });
520
- } catch (error) {
521
- prepared.cleanup();
522
- return resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(error.message) });
523
- }
524
- const unregister = registerProbe(child);
525
- const timer = setTimeout(() => {
526
- if (settled) return;
527
- settled = true;
528
- unregister();
529
- terminateProbeTree(child);
530
- prepared.cleanup();
531
- 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ı.` } });
532
- }, timeoutMs);
533
- child.stdout?.setEncoding("utf8");
534
- child.stderr?.setEncoding("utf8");
535
- child.stdout?.on("data", (data) => { stdout += String(data); });
536
- child.stderr?.on("data", (data) => { stderr += String(data); });
537
- child.on("error", (error) => {
538
- if (settled) return;
539
- settled = true;
540
- clearTimeout(timer);
541
- unregister();
542
- prepared.cleanup();
543
- const failure = classifyHealthFailure(error.message);
544
- resolve({ id, installed: true, version: found.version, health: failure });
545
- });
546
- child.on("close", (code) => {
547
- if (settled) return;
548
- settled = true;
549
- clearTimeout(timer);
550
- unregister();
551
- prepared.cleanup();
552
- const raw = `${stdout}\n${stderr}`.trim();
553
- 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ı." } });
554
- else resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(raw || `Çıkış kodu ${code}`) });
555
- });
556
- child.stdin?.on("error", () => {});
557
- child.stdin?.end(prepared.useStdin ? prompt : "");
558
- });
559
- }
560
-
561
- async function healthCheckAll(discovered = discoverInstalled(), options = {}) {
562
- const results = await Promise.all(discovered.map((item) => testInstalledCli(item.id, options)));
563
- return discovered.map((item) => ({ ...item, ...(results.find((result) => result.id === item.id) || {}) }));
564
- }
565
-
566
- function listCodexModels(options = {}) {
567
- const command = buildCommand("codex", ["app-server", "--listen", "stdio://"]);
568
- const timeoutMs = Math.max(5000, Number(options.timeoutMs || 20000));
569
- return new Promise((resolve, reject) => {
570
- let buffer = "", settled = false, modelRequestSent = false;
571
- let child, unregister = () => {};
572
- const finish = (error, value) => {
573
- if (settled) return;
574
- settled = true;
575
- clearTimeout(timer);
576
- unregister();
577
- terminateProbeTree(child);
578
- if (error) reject(error); else resolve(value);
579
- };
580
- const timer = setTimeout(() => finish(new Error(`Codex model listesi ${Math.round(timeoutMs / 1000)} saniyede alınamadı.`)), timeoutMs);
581
- try { child = spawn(command.file, command.args, { shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim }); }
582
- catch (error) { finish(error); return; }
583
- unregister = registerProbe(child);
584
- child.stdout?.setEncoding("utf8");
585
- child.stderr?.setEncoding("utf8");
586
- child.stdout?.on("data", (chunk) => {
587
- buffer += String(chunk);
588
- const lines = buffer.split(/\r?\n/);
589
- buffer = lines.pop() || "";
590
- for (const line of lines) {
591
- let message;
592
- try { message = JSON.parse(line); } catch { continue; }
593
- if (message.id === 1 && !modelRequestSent) {
594
- modelRequestSent = true;
595
- child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "model/list", params: { includeHidden: false, limit: 200 } })}\n`);
596
- } else if (message.id === 2) {
597
- if (message.error) { finish(new Error(message.error.message || "Codex model listesi alınamadı.")); return; }
598
- const models = Array.isArray(message.result?.data) ? message.result.data : [];
599
- finish(null, models.filter((model) => model && model.hidden !== true).map((model) => ({
600
- id: String(model.id || model.model || ""),
601
- model: String(model.model || model.id || ""),
602
- displayName: String(model.displayName || model.id || model.model || ""),
603
- description: String(model.description || ""),
604
- defaultReasoningEffort: String(model.defaultReasoningEffort || "medium"),
605
- reasoningEfforts: (model.supportedReasoningEfforts || []).map((item) => ({
606
- id: String(item.reasoningEffort || item.id || ""),
607
- description: String(item.description || ""),
608
- })).filter((item) => item.id),
609
- serviceTiers: (model.serviceTiers || []).map((item) => ({
610
- id: String(item.id || ""),
611
- name: String(item.name || item.id || ""),
612
- description: String(item.description || ""),
613
- })).filter((item) => item.id),
614
- additionalSpeedTiers: Array.isArray(model.additionalSpeedTiers) ? model.additionalSpeedTiers.map(String) : [],
615
- defaultServiceTier: model.defaultServiceTier ? String(model.defaultServiceTier) : "",
616
- isDefault: model.isDefault === true,
617
- })).filter((model) => model.id));
618
- }
619
- }
620
- });
621
- child.stderr?.on("data", () => {});
622
- child.on("error", finish);
623
- child.stdin?.on("error", () => {});
624
- child.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { clientInfo: { name: "crewctl", version: "1.0.0", title: "CrewCtl" } } })}\n`);
625
- });
626
- }
627
-
628
- function addMissingAgents(cfg, discovered) {
629
- cfg.agents ||= {};
630
- const ignored = new Set(Array.isArray(cfg.discoveryIgnoredAdapters) ? cfg.discoveryIgnoredAdapters.map(String) : []);
631
- let changed = normalizeAgentAdapters(cfg);
632
- for (const cli of discovered) {
633
- const usable = cli.installed && (cli.id !== "opencode" || cli.ready !== false || Boolean(cfg.cliSettings?.opencode?.model));
634
- const matchingEntries = Object.entries(cfg.agents).filter(([, agent]) => (agent.adapter || adapterId(agent.cmd)) === cli.id);
635
- const existingEntry = matchingEntries.find(([, agent]) => !agent.autoDiscovered) || matchingEntries[0];
636
- const existing = existingEntry?.[1];
637
- // Kullanici otomatik bulunan bir profili sildiyse bu karar kalicidir. O adapter'a ait
638
- // elle olusturulmus profil varsa ona dokunma; yalnizca autoDiscovered kaydini temizle.
639
- if (ignored.has(cli.id)) {
640
- for (const [name, agent] of matchingEntries) {
641
- if (!agent.autoDiscovered) continue;
642
- delete cfg.agents[name];
643
- changed = true;
644
- }
645
- continue;
646
- }
647
- if (existing) {
648
- if (usable && existing.autoDiscovered && existing.unavailablePlaceholder) {
649
- existing.enabled = true;
650
- existing.args = cli.defaultArgs.slice();
651
- delete existing.unavailablePlaceholder;
652
- changed = true;
653
- }
654
- // Eski surumler otomatik oneriyi agent.model alanina yaziyordu. Bu alan global
655
- // CLI ayarini ezdigi icin, kullanici tarafindan acikca override olarak isaretlenmemis
656
- // otomatik profiller modeli daima CLI ayarindan/otomatik secimden miras alir.
657
- if (cli.id === "opencode" && existing.autoDiscovered && existing.model && existing.modelOverride !== true) {
658
- delete existing.model;
659
- changed = true;
660
- }
661
- if (cli.installed && cli.id === "opencode" && existing.autoDiscovered && Number(existing.timeoutSeconds || 0) < cli.timeoutSeconds) {
662
- existing.timeoutSeconds = cli.timeoutSeconds;
663
- changed = true;
664
- }
665
- if (!usable && existing.autoDiscovered && existing.enabled !== false) {
666
- existing.enabled = false;
667
- existing.unavailablePlaceholder = true;
668
- changed = true;
669
- }
670
- continue;
671
- }
672
- // OpenCode adapteri her cihazda katalogda gorunur. Kurulu degilse guvenli
673
- // sekilde devre disi placeholder olur; taramada bulundugunda otomatik etkinlesir.
674
- if (!cli.installed && cli.id !== "opencode") continue;
675
- let name = `${cli.id}-auto`, suffix = 2;
676
- while (cfg.agents[name]) name = `${cli.id}-auto-${suffix++}`;
677
- cfg.agents[name] = {
678
- adapter: cli.id,
679
- cmd: cli.command,
680
- args: cli.defaultArgs.slice(),
681
- enabled: usable,
682
- description: cli.description,
683
- capabilities: cli.capabilities.slice(),
684
- roleFile: cli.roleFile,
685
- costTier: "standard",
686
- timeoutSeconds: cli.timeoutSeconds || 1200,
687
- ...(cli.silenceTimeoutSeconds ? { silenceTimeoutSeconds: cli.silenceTimeoutSeconds } : {}),
688
- autoDiscovered: true,
689
- ...(!usable ? { unavailablePlaceholder: true } : {}),
690
- };
691
- changed = true;
692
- }
693
- return changed;
694
- }
695
-
696
- // Operator, uzman agent'lardan BAGIMSIZ bir CLI secimidir (claude/codex/gemini/opencode).
697
- // operator.md rolunu bu CLI giyer; uzman agent'lar operatorun ALTINDA delege edilir.
698
- // Bir uzman agent silinse bile operator etkilenmez.
699
- function operatorSpec(cli, cfg) {
700
- const op = (cfg && cfg.operator) || {};
701
- const definition = DEFINITIONS[cli];
702
- // Standart CLI'lar DEFINITIONS'tan cozulur. cfg.operator.cmd verilirse (ozel/test operatoru)
703
- // dogrudan o komut kullanilir; boylece operator, uzman agent'lardan tamamen bagimsiz kalir.
704
- const cmd = op.cmd || (definition && definition.command);
705
- if (!cmd) return null;
706
- const args = op.cmd ? (Array.isArray(op.args) ? op.args.slice() : []) : (definition ? definition.defaultArgs.slice() : []);
707
- // Model/effort/tier secimi CLI bazlidir (cfg.cliSettings) ve effectiveAgent icinde uygulanir;
708
- // ayni ayar o CLI'yi kullanan operator ve tum uzman agent'lar icin gecerlidir.
709
- const timeoutSeconds = op.timeoutSeconds || (cfg && cfg.agentTimeoutSeconds) || 900;
710
- const silenceTimeoutSeconds = op.silenceTimeoutSeconds || (definition && definition.silenceTimeoutSeconds) || (cfg && cfg.cliSilenceTimeoutSeconds) || 300;
711
- const adapter = definition ? cli : (op.adapter || adapterId(cmd));
712
- return effectiveAgent({ adapter, cmd, args, timeoutSeconds, silenceTimeoutSeconds }, cfg);
713
- }
714
-
715
- // Operator gecerli, kurulu bir CLI olmali. Eski semada operator.agent (uzman agent adi)
716
- // tutuluyordu; burada operator.cli'ye tasinir. Kurulu degilse ilk kurulu CLI'ye gecilir.
717
- function ensureValidOperator(cfg, discovered) {
718
- cfg.operator ||= {};
719
- let changed = false;
720
- if (cfg.operator.roleFile !== "roles/operator.md") { cfg.operator.roleFile = "roles/operator.md"; changed = true; }
721
- if (!cfg.operator.cli) {
722
- const legacy = cfg.operator.agent;
723
- const source = legacy && cfg.agents?.[legacy] ? cfg.agents[legacy].cmd : legacy;
724
- cfg.operator.cli = adapterId(source || "codex");
725
- changed = true;
726
- }
727
- if ("agent" in cfg.operator) { delete cfg.operator.agent; changed = true; }
728
- const installed = Array.isArray(discovered) ? discovered
729
- .filter((x) => x.installed && (x.id !== "opencode" || x.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)))
730
- .map((x) => x.id) : null;
731
- const known = !!DEFINITIONS[cfg.operator.cli];
732
- const availableOnHost = !installed || installed.length === 0 || installed.includes(cfg.operator.cli);
733
- if (!known || !availableOnHost) {
734
- const fallback = (installed && installed[0]) || Object.keys(DEFINITIONS)[0];
735
- if (fallback && fallback !== cfg.operator.cli) { cfg.operator.cli = fallback; changed = true; }
736
- }
737
- return changed;
738
- }
739
-
740
- module.exports = { DEFINITIONS, KNOWN_CLIS: Object.keys(DEFINITIONS), adapterId, normalizeAgentAdapter, normalizeAgentAdapters, effectiveAgent, preparePromptArgs, operatorSpec, buildCommand, agentEnvironment, discoverInstalled, discoveryCacheFrom, applyOpenCodeFallback, healthCheckAll, listCodexModels, abortActiveProbes, currentProbeGeneration, 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
+ // --skip-trust her Gemini surumunde yok; kesifte yoklanip destekleniyorsa eklenir.
35
+ optionalFlags: ["--skip-trust"],
36
+ description: "Gemini CLI analiz, uygulama ve web arastirma agenti",
37
+ capabilities: ["implementation", "planning", "analysis", "research", "web"],
38
+ roleFile: "roles/executor.md",
39
+ },
40
+ opencode: {
41
+ command: "opencode",
42
+ versionArgs: ["--version"],
43
+ // OpenCode saglayici/model kombinasyonuna gore uzun sure sessiz kalabilir. Varsayilan
44
+ // agent timeout'u, basarili bir calismayi SIGTERM ile yarida kesmeyecek kadar genis tut.
45
+ timeoutSeconds: 1800,
46
+ // OpenCode adim baslattiktan sonra model/provider yanitini beklerken uzun sure hic cikti
47
+ // akitmayabilir (ozellikle yavas/rate-limitli modeller). 180sn cok agresifti ve calisan bir
48
+ // kosmayi yariyordu; sessizlik payini genis tut. Gercek takilma yine timeoutSeconds ile yakalanir.
49
+ silenceTimeoutSeconds: 300,
50
+ // Bazi eski OpenCode surumleri --auto bayragini tanimaz. Otonom izinler
51
+ // engine tarafinda OPENCODE_CONFIG_CONTENT ile surumden bagimsiz aktarilir.
52
+ defaultArgs: ["run", "--format", "json", "Attached file contains the full task. Follow it exactly, make the changes, and report what you did.", "--file", "{PROMPT_FILE}"],
53
+ description: "OpenCode coklu saglayici kodlama, uygulama ve inceleme agenti",
54
+ capabilities: ["implementation", "debugging", "testing", "review", "analysis", "research"],
55
+ roleFile: "roles/executor.md",
56
+ install: {
57
+ windows: "npm install -g opencode-ai (alternatif: choco install opencode / scoop install opencode)",
58
+ macos: "brew install anomalyco/tap/opencode (alternatif: npm install -g opencode-ai)",
59
+ linux: "curl -fsSL https://opencode.ai/install | bash (alternatif: npm install -g opencode-ai)",
60
+ auth: "opencode auth login",
61
+ },
62
+ },
63
+ };
64
+ const RESOLVED = new Map();
65
+ let OPEN_CODE_MODELS = [];
66
+
67
+ // En fazla bir CLI'yi (or. codex) ayni anda hem canli saglik/model probe'u hem de motorun
68
+ // operatör kosumu calistirirsa, codex gibi bazi CLI'lar ayni is dizininde ikinci exec oturumunu
69
+ // SIGTERM ile keser. Probe'lar "en iyi caba"dir; motor onceliklidir. Aktif probe cocuklarini
70
+ // izleyip motor bir goreve baslarken hepsini sonlandiriyoruz.
71
+ const ACTIVE_PROBES = new Set();
72
+ function registerProbe(child) {
73
+ if (!child) return () => {};
74
+ ACTIVE_PROBES.add(child);
75
+ return () => ACTIVE_PROBES.delete(child);
76
+ }
77
+ // Uctan uca proses agacini oldur (codex kabuk altinda alt proses baslatabilir).
78
+ function terminateProbeTree(child) {
79
+ if (!child) return;
80
+ try {
81
+ if (isWin && child.pid) spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
82
+ else child.kill("SIGKILL");
83
+ } catch {}
84
+ }
85
+ // Motor bir goreve baslarken cagirir: uctaki tum canli probe'lari sonlandirir ki operatörun
86
+ // CLI'si (codex vb.) temiz bir alanda calissin. Probe'lar sonuc olarak "timeout/failed" doner;
87
+ // bu zararsizdir, motor bosaldiginda yeniden denenir.
88
+ // Iptal sayaci: motor bir goreve baslarken probe'lari oldurdugunde artar. Oldurulen probe
89
+ // "cikis kodu != 0" ile doner ve saglik siniflandiricisi bunu GERCEK bir ariza sanardi;
90
+ // sonuc 6 saatlik onbellege yazilinca CLI o sure boyunca yanlislikla kullanilamaz kalirdi.
91
+ // refreshCliHealth bu sayaci once/sonra karsilastirip kirlenmis sonucu ONBELLEGE YAZMAZ.
92
+ let probeGeneration = 0;
93
+ function currentProbeGeneration() {
94
+ return probeGeneration;
95
+ }
96
+
97
+ function abortActiveProbes() {
98
+ const count = ACTIVE_PROBES.size;
99
+ if (count) probeGeneration++;
100
+ for (const child of ACTIVE_PROBES) terminateProbeTree(child);
101
+ ACTIVE_PROBES.clear();
102
+ return count;
103
+ }
104
+
105
+ // Saglayici adlari koda gomulmez: kullanicinin aboneligi farkli olabilir ve katalog degisir.
106
+ // Sirali joker desenler; ilk eslesen kazanir. cliSettings.opencode uzerinden degistirilebilir.
107
+ const DEFAULT_OPENCODE_MODEL_PREFERENCES = ["*deepseek*free*", "*free*", "*"];
108
+ // Yerel/LAN saglayicilar erisilebilir varsayilamaz: kapali bir Ollama "Unable to connect" verir.
109
+ const DEFAULT_OPENCODE_MODEL_EXCLUDE = ["ollama/*", "lmstudio/*", "llamacpp/*", "local/*", "localhost/*"];
110
+
111
+ // Kullanici deseni regex ozel karakteri icerse bile duz metin sayilmali.
112
+ function globToRegExp(pattern) {
113
+ const escaped = String(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
114
+ return new RegExp(`^${escaped}$`, "i");
115
+ }
116
+ function patternList(value, fallback) {
117
+ const list = Array.isArray(value) ? value.map(String).map((x) => x.trim()).filter(Boolean) : [];
118
+ return (list.length ? list : fallback).map(globToRegExp);
119
+ }
120
+
121
+ function selectOpenCodeModel(models, settings = {}) {
122
+ const list = Array.isArray(models) ? models.map(String).filter(Boolean) : [];
123
+ const exclude = patternList(settings.modelExclude, DEFAULT_OPENCODE_MODEL_EXCLUDE);
124
+ const preferences = patternList(settings.modelPreferences, DEFAULT_OPENCODE_MODEL_PREFERENCES);
125
+ const usable = list.filter((model) => !exclude.some((rule) => rule.test(model)));
126
+ for (const rule of preferences) {
127
+ const found = usable.find((model) => rule.test(model));
128
+ if (found) return found;
129
+ }
130
+ return "";
131
+ }
132
+
133
+ // `opencode models` ciktisindan saglayici/model satirlarini ayiklar. Baslik, bos satir ve
134
+ // log gurultusunu eler; her "saglayici/model" bicimli satir gecerli sayilir.
135
+ function parseOpenCodeModels(stdout) {
136
+ const found = new Set();
137
+ for (const line of String(stdout || "").split(/\r?\n/)) {
138
+ const model = line.trim();
139
+ if (/^[A-Za-z0-9._-]+\/.+/.test(model) && !/\s/.test(model)) found.add(model);
140
+ }
141
+ return [...found];
142
+ }
143
+
144
+ function listOpenCodeModels(command) {
145
+ try {
146
+ // Tum saglayicilarin listesi sorgulanir. Kullanicilarin ucretli abonelikleri
147
+ // (opencode-go, minimax-coding-plan vb.) ayri saglayici olarak gorunur; yalnizca
148
+ // "opencode" saglayicisini sorgulamak bu modelleri gizliyor ve kurulum yanlislikla
149
+ // "hazir degil" sayiliyordu. Otomatik oneri siralamasini selectOpenCodeModel yapar.
150
+ const result = spawnSync(command, ["models"], {
151
+ encoding: "utf8", timeout: 20000, windowsHide: true,
152
+ shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
153
+ });
154
+ if (result.status !== 0) return [];
155
+ return parseOpenCodeModels(result.stdout);
156
+ } catch { return []; }
157
+ }
158
+
159
+ // Liste bos kalirsa --model eklenmez ve opencode kendi "son kullanilan" modeline duser; o model
160
+ // erisilemezse gorev "No provider available"/"Unable to connect" ile olur. Bu yuzden tembel doldurulur.
161
+ let openCodeModelsAttempted = false;
162
+ function ensureOpenCodeModels(command) {
163
+ // Negatif sonuc da onbelleklenir: aksi halde bos donduren bir kurulumda her effectiveAgent
164
+ // cagrisi yeniden spawn eder ve istek yolunda event loop'u kilitlerdi.
165
+ if (OPEN_CODE_MODELS.length || openCodeModelsAttempted) return OPEN_CODE_MODELS;
166
+ openCodeModelsAttempted = true;
167
+ OPEN_CODE_MODELS = listOpenCodeModels(RESOLVED.get("opencode") || command || "opencode");
168
+ return OPEN_CODE_MODELS;
169
+ }
170
+
171
+ // Oturum acma talimati CLI'ya gore degisir; tek bir sabit metin opencode hatasinda kullaniciya
172
+ // gemini komutu onerdiriyordu. Adapter adi ya da komut yolu kabul eder.
173
+ const AUTH_HINTS = {
174
+ codex: "Terminalde `codex login` calistirin (veya OPENAI_API_KEY tanimlayin).",
175
+ claude: "Terminalde `claude` calistirip oturum acin (veya ANTHROPIC_API_KEY tanimlayin).",
176
+ gemini: "Terminalde `gemini` calistirip 'Login with Google' secin (veya GEMINI_API_KEY tanimlayin).",
177
+ opencode: "Terminalde `opencode auth login` calistirip saglayiciyi secin; '401 No provider available' hatasi secili modelin saglayicisinda gecerli kimlik olmadigi anlamina gelir.",
178
+ };
179
+ function authHint(adapterOrCommand) {
180
+ const id = AUTH_HINTS[adapterOrCommand] ? adapterOrCommand : adapterId(adapterOrCommand);
181
+ return AUTH_HINTS[id] || "Bu CLI'nin kendi oturum acma komutunu terminalde calistirin.";
182
+ }
183
+
184
+ function adapterId(command) {
185
+ const base = path.basename(String(command || "")).toLowerCase().replace(/\.(cmd|bat|exe|ps1)$/, "");
186
+ if (base.includes("codex")) return "codex";
187
+ if (base.includes("claude")) return "claude";
188
+ if (base.includes("gemini")) return "gemini";
189
+ if (base.includes("opencode")) return "opencode";
190
+ return "custom";
191
+ }
192
+
193
+ // Farkli paket yoneticileri / kurulum araclari CLI'lari farkli dizinlere koyar. Her PC'de
194
+ // calismasi icin yaygin npm/pnpm/yarn/bun/volta/scoop/winget/choco/homebrew konumlarini tarariz.
195
+ function commonCandidates(id) {
196
+ const home = os.homedir();
197
+ if (isWin) {
198
+ const APPDATA = process.env.APPDATA, LOCAL = process.env.LOCALAPPDATA, PD = process.env.ProgramData;
199
+ const exts = ["cmd", "exe", "ps1", "bat", ""];
200
+ const bases = [
201
+ APPDATA && path.join(APPDATA, "npm"),
202
+ LOCAL && path.join(LOCAL, "pnpm"),
203
+ LOCAL && path.join(LOCAL, "Yarn", "bin"),
204
+ LOCAL && path.join(LOCAL, "Microsoft", "WinGet", "Links"),
205
+ LOCAL && path.join(LOCAL, "Programs", id),
206
+ path.join(home, ".bun", "bin"),
207
+ path.join(home, ".volta", "bin"),
208
+ path.join(home, "scoop", "shims"),
209
+ PD && path.join(PD, "chocolatey", "bin"),
210
+ path.join(home, ".local", "bin"),
211
+ path.join(home, "AppData", "Roaming", "npm"),
212
+ ].filter(Boolean);
213
+ const out = [];
214
+ for (const base of bases) for (const ext of exts) out.push(path.join(base, ext ? `${id}.${ext}` : id));
215
+ return out;
216
+ }
217
+ return [
218
+ path.join(home, ".local", "bin", id),
219
+ path.join(home, "bin", id),
220
+ path.join(home, ".bun", "bin", id),
221
+ path.join(home, ".volta", "bin", id),
222
+ path.join(home, ".npm-global", "bin", id),
223
+ path.join(home, ".yarn", "bin", id),
224
+ path.join(home, ".local", "share", "pnpm", id),
225
+ `/usr/local/bin/${id}`,
226
+ `/opt/homebrew/bin/${id}`,
227
+ `/usr/bin/${id}`,
228
+ `/snap/bin/${id}`,
229
+ ];
230
+ }
231
+
232
+ // Bilinen bir CLI komutu adapter alanindan daha guvenilir kaynaktir. Eski veya elle
233
+ // duzenlenmis config'te adapter=claude + cmd=codex gibi bir celiski varsa hedef CLI'nin
234
+ // guvenli varsayilan argumanlarina donulur; boylece bir CLI'ye digerinin bayragi gitmez.
235
+ function normalizeAgentAdapter(agent) {
236
+ if (!agent || typeof agent !== "object") return false;
237
+ const configured = String(agent.adapter || "").trim().toLowerCase();
238
+ const inferred = adapterId(agent.cmd);
239
+ const adapter = inferred !== "custom" ? inferred : (DEFINITIONS[configured] ? configured : "custom");
240
+ let changed = false;
241
+ if (inferred !== "custom" && configured && configured !== inferred) {
242
+ agent.args = DEFINITIONS[inferred].defaultArgs.slice();
243
+ delete agent.model;
244
+ delete agent.modelOverride;
245
+ changed = true;
246
+ }
247
+ if (agent.adapter !== adapter) {
248
+ agent.adapter = adapter;
249
+ changed = true;
250
+ }
251
+ return changed;
252
+ }
253
+
254
+ function normalizeAgentAdapters(cfg) {
255
+ if (!cfg?.agents || typeof cfg.agents !== "object") return false;
256
+ let changed = false;
257
+ for (const agent of Object.values(cfg.agents)) if (normalizeAgentAdapter(agent)) changed = true;
258
+ return changed;
259
+ }
260
+
261
+ // Ajan hapsi acik mi? cfg.sandbox.mode === "workspace" ise CLI'lar calisma klasoru DISINA
262
+ // yazamaz/aga cikamaz. cfg verilmeyen (test/edge) cagrilar hapissiz kalir; boylece mevcut
263
+ // arguman-esitlik testleri aynen gecer. Gercek calistirmada normalizeConfig mode'u daima
264
+ // doldurur (varsayilan "workspace").
265
+ function confineWorkspace(cfg) {
266
+ return cfg?.sandbox?.mode === "workspace";
267
+ }
268
+ function sandboxWritableRoots(cfg) {
269
+ const roots = cfg?.sandbox?.extraWritableDirs;
270
+ return Array.isArray(roots) ? roots.filter((x) => typeof x === "string" && x.trim()).map(String) : [];
271
+ }
272
+
273
+ // cfg verilirse cfg.cliSettings[adapter] o CLI'nin varsayilan model ayari olarak uygulanir.
274
+ // Oncelik: agent.model (profil bazli) > cliSettings[adapter].model (global) > CLI varsayilani.
275
+ function effectiveAgent(agent, cfg) {
276
+ const copy = { ...agent, args: Array.isArray(agent.args) ? agent.args.map(String) : [] };
277
+ normalizeAgentAdapter(copy);
278
+ const adapter = copy.adapter;
279
+ const commandAdapter = adapterId(copy.cmd);
280
+ const settings = (cfg && cfg.cliSettings && cfg.cliSettings[adapter]) || {};
281
+ if (!copy.silenceTimeoutSeconds && DEFINITIONS[adapter]?.silenceTimeoutSeconds) copy.silenceTimeoutSeconds = DEFINITIONS[adapter].silenceTimeoutSeconds;
282
+ if (commandAdapter === adapter && RESOLVED.has(adapter) && !path.isAbsolute(copy.cmd)) copy.cmd = RESOLVED.get(adapter);
283
+ if (adapter === "codex") {
284
+ const commands = new Set(["exec", "review", "resume", "apply"]);
285
+ if (!copy.args.some((arg) => commands.has(arg))) copy.args.unshift("exec", "--skip-git-repo-check");
286
+ 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");
287
+ // Hapis: calisma klasoru (spawn cwd = codex "workspace") disina yazma ve ag engellenir.
288
+ // AYRICA: `codex exec` bayraksiz calistiginda read-only sandbox'a duser; yazma reddedilir
289
+ // ama surec 0 doner, motor delegasyonu "tamamlandi" sayar ve hicbir dosya uretmeden turlar
290
+ // yanar. Yani bu bayrak yalnizca hapis degil, otonom kosumun calismasi icin de sarttir.
291
+ // Native sandbox — mac Seatbelt / Linux Landlock+seccomp / Windows restricted-token+ACL —
292
+ // Docker/Git GEREKTIRMEZ. Kullanici acikca kendi -s/--sandbox veya bypass bayragini
293
+ // koyduysa ona dokunmayiz. Bayrak, model/effort/tier PUSH'undan ONCE ve exec/skip-git'ten
294
+ // HEMEN SONRA eklenir; testlerin args.slice(0,2) ve slice(-6) beklentileri korunur.
295
+ const alreadySandboxed = copy.args.some((arg) => arg === "-s" || arg === "--sandbox" || arg === "--dangerously-bypass-approvals-and-sandbox" || arg === "--full-auto");
296
+ if (confineWorkspace(cfg) && !alreadySandboxed) {
297
+ const anchor = copy.args.indexOf("--skip-git-repo-check");
298
+ const at = anchor >= 0 ? anchor + 1 : Math.max(copy.args.indexOf("exec") + 1, 0);
299
+ const flags = ["-s", "workspace-write"];
300
+ const roots = sandboxWritableRoots(cfg);
301
+ if (roots.length) flags.push("-c", `sandbox_workspace_write.writable_roots=${JSON.stringify(roots)}`);
302
+ copy.args.splice(at, 0, ...flags);
303
+ }
304
+ const model = String(copy.model || settings.model || "").trim();
305
+ const effort = ["low", "medium", "high", "xhigh", "max", "ultra"].includes(String(settings.reasoningEffort)) ? String(settings.reasoningEffort) : "";
306
+ const serviceTier = /^[A-Za-z0-9._-]{1,64}$/.test(String(settings.serviceTier || "")) ? String(settings.serviceTier) : "";
307
+ if (model && !copy.args.includes("--model") && !copy.args.includes("-m")) copy.args.push("--model", model);
308
+ if (effort && !copy.args.some((arg) => String(arg).startsWith("model_reasoning_effort="))) copy.args.push("-c", `model_reasoning_effort="${effort}"`);
309
+ if (serviceTier && !copy.args.some((arg) => String(arg).startsWith("service_tier="))) copy.args.push("-c", `service_tier="${serviceTier}"`);
310
+ }
311
+ if (adapter === "claude") {
312
+ if (!copy.args.includes("-p") && !copy.args.includes("--print")) copy.args.unshift("-p");
313
+ if (!copy.args.includes("--output-format")) copy.args.push("--output-format", "text");
314
+ }
315
+ if (adapter === "gemini") {
316
+ // Otonom, non-interaktif calisma icin onay modu sart; yoksa izin bekleyip takilir.
317
+ if (!copy.args.includes("--approval-mode") && !copy.args.includes("-y") && !copy.args.includes("--yolo")) copy.args.push("--approval-mode", "yolo");
318
+ // Gemini "guvenilmeyen" klasorde yolo'yu SESSIZCE default'a dusurur ve onay bekler; otonom
319
+ // kosumda onaylayan olmadigi icin sessizlik zaman asimina dusup karantinaya girer.
320
+ // --skip-trust yalnizca o oturum icindir, kalici ayarlara dokunmaz.
321
+ if (!copy.args.includes("--skip-trust") && cliSupportsFlag(copy.cmd, "--skip-trust")) copy.args.push("--skip-trust");
322
+ }
323
+ if (adapter === "opencode") {
324
+ if (!copy.args.includes("run")) copy.args.unshift("run");
325
+ // config.json'da eski otomatik profil kalmis olsa da uyumsuz bayragi temizle.
326
+ copy.args = copy.args.filter((arg) => arg !== "--auto");
327
+ const formatAt = copy.args.indexOf("--format");
328
+ if (formatAt >= 0) copy.args.splice(formatAt, 2);
329
+ copy.args.splice(copy.args.indexOf("run") + 1, 0, "--format", "json");
330
+ const hasModel = copy.args.includes("--model") || copy.args.includes("-m");
331
+ // Sira: agent modeli > global CLI ayari > otomatik secim (bkz. ensureOpenCodeModels).
332
+ const model = copy.model || String(settings.model || "").trim()
333
+ || selectOpenCodeModel(OPEN_CODE_MODELS.length ? OPEN_CODE_MODELS : ensureOpenCodeModels(copy.cmd), settings);
334
+ if (!hasModel && model) copy.args.splice(copy.args.indexOf("run") + 3, 0, "--model", model);
335
+ if (model) copy.model = model;
336
+ if (!copy.args.includes("{PROMPT}") && !copy.args.includes("{PROMPT_FILE}")) {
337
+ copy.args.push("Attached file contains the full task. Follow it exactly, make the changes, and report what you did.", "--file", "{PROMPT_FILE}");
338
+ }
339
+ }
340
+ return copy;
341
+ }
342
+
343
+ // Bayrak destegi --help'ten okunur: kosulsuz eklemek eski surumlerde "unknown argument" ile her
344
+ // calistirmayi kirar.
345
+ //
346
+ // SENKRON SPAWN YALNIZCA KESIF SIRASINDA. effectiveAgent HTTP istek yolunda cagriliyor ve
347
+ // oradaki spawnSync event loop'u kilitleyip baglantiyi ECONNRESET ile dusuruyor (sunucu-akis
348
+ // testi bunu yakaladi); effectiveAgent yalnizca hazir sonucu okur, bilinmiyorsa bayrak eklenmez.
349
+ // Anahtar komuttur: ayni adapteri farkli binary/surumle kullanan agentler karismamali.
350
+ const FLAG_SUPPORT = new Map();
351
+ function flagSupportKey(command, flag) {
352
+ return `${command}${flag}`;
353
+ }
354
+ function probeFlagSupport(command, flags) {
355
+ let help = "";
356
+ try {
357
+ const result = spawnSync(command, ["--help"], {
358
+ encoding: "utf8", timeout: 15000, windowsHide: true,
359
+ shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
360
+ });
361
+ help = `${result.stdout || ""}\n${result.stderr || ""}`;
362
+ } catch {}
363
+ const supported = [];
364
+ for (const flag of flags) {
365
+ const ok = Boolean(help) && help.includes(flag);
366
+ FLAG_SUPPORT.set(flagSupportKey(command, flag), ok);
367
+ if (ok) supported.push(flag);
368
+ }
369
+ return supported;
370
+ }
371
+ // Desteklenmeyenler DE isaretlenir; yoksa "bilinmiyor" kalir ve sonraki probe (saglik testi
372
+ // sirasinda) yeniden yoklar — kacinmak istedigimiz spawnSync tam olarak budur.
373
+ function restoreFlagSupport(command, allFlags, supportedFlags) {
374
+ const supported = new Set(supportedFlags || []);
375
+ for (const flag of allFlags || []) FLAG_SUPPORT.set(flagSupportKey(command, flag), supported.has(flag));
376
+ }
377
+ function cliSupportsFlag(command, flag) {
378
+ return FLAG_SUPPORT.get(flagSupportKey(command, flag)) === true;
379
+ }
380
+
381
+ function probeCommand(command, definition) {
382
+ try {
383
+ const result = spawnSync(command, definition.versionArgs, {
384
+ encoding: "utf8",
385
+ timeout: 12000,
386
+ windowsHide: true,
387
+ shell: isWin && (!path.isAbsolute(command) || /\.(cmd|bat)$/i.test(command)),
388
+ });
389
+ const output = String(result.stdout || result.stderr || "").trim().split(/\r?\n/)[0];
390
+ return { installed: result.status === 0, version: result.status === 0 ? output : "", error: result.status === 0 ? "" : output, resolvedCommand: result.status === 0 ? command : "" };
391
+ } catch (error) {
392
+ return { installed: false, version: "", error: error.message, resolvedCommand: "" };
393
+ }
394
+ }
395
+
396
+ function probe(id, definition, cfg) {
397
+ let result = probeCommand(definition.command, definition);
398
+ let installedPath = "";
399
+ if (!result.installed) {
400
+ for (const candidate of commonCandidates(id)) {
401
+ if (!fs.existsSync(candidate)) continue;
402
+ if (!installedPath) installedPath = candidate;
403
+ result = probeCommand(candidate, definition);
404
+ if (result.installed) break;
405
+ }
406
+ // Bazi CLI'lar (or. gemini) --version cagrisinda cmd.exe uzerinden yavas acildigi icin
407
+ // zaman asimina ugrar. Ikili dosya diskte mevcutsa, versiyon okunamasa bile "kurulu" say;
408
+ // aksi halde kurulu CLI yanlislikla devre disi birakiliyordu.
409
+ if (!result.installed && installedPath) {
410
+ result = { installed: true, version: "kurulu", error: "", resolvedCommand: installedPath };
411
+ }
412
+ }
413
+ if (result.installed) RESOLVED.set(id, result.resolvedCommand);
414
+ // Bilinen bayrak tekrar yoklanmaz: probe() saglik testinden de cagriliyor ve o sirada
415
+ // sunucu istek servis ediyor olabilir (bkz. probeFlagSupport'taki ECONNRESET notu).
416
+ if (result.installed && definition.optionalFlags?.length) {
417
+ const command = result.resolvedCommand || definition.command;
418
+ const unknown = definition.optionalFlags.filter((flag) => !FLAG_SUPPORT.has(flagSupportKey(command, flag)));
419
+ if (unknown.length) probeFlagSupport(command, unknown);
420
+ result.supportedFlags = definition.optionalFlags.filter((flag) => cliSupportsFlag(command, flag));
421
+ }
422
+ if (result.installed && id === "opencode") {
423
+ const settings = (cfg && cfg.cliSettings && cfg.cliSettings.opencode) || {};
424
+ OPEN_CODE_MODELS = listOpenCodeModels(result.resolvedCommand || definition.command);
425
+ result.models = OPEN_CODE_MODELS.slice();
426
+ result.recommendedModel = String(settings.model || "").trim() || selectOpenCodeModel(OPEN_CODE_MODELS, settings);
427
+ result.ready = Boolean(result.recommendedModel);
428
+ result.readinessError = result.ready ? "" : (OPEN_CODE_MODELS.length
429
+ ? "Otomatik secim kurallarina uyan bir OpenCode modeli bulunamadi. Ayarlardan bir model secin veya cliSettings.opencode.modelPreferences desenlerini duzenleyin."
430
+ : "OpenCode kurulu, ancak model listesi okunamadi. Once saglayici girisini tamamlayin.");
431
+ }
432
+ return result;
433
+ }
434
+
435
+ // Her acilista tum CLI'lar --version ile yoklaniyordu; olculen maliyet 4.8 saniye ve panel
436
+ // bu sure boyunca hic acilmiyordu. Onbellek YALNIZCA su kosulda kullanilir: CLI daha once
437
+ // KURULU bulunmus VE cozulmus ikili dosya HALA diskte duruyor. Bu durumda tekrar yoklamak
438
+ // zaten bilinen bir cevabi dogrulamaktan ibarettir.
439
+ //
440
+ // Bilerek onbelleklenmeyenler (dogruluk hizdan onceliklidir):
441
+ // - Daha once "kurulu degil" bulunanlar -> yeni kurulan CLI ilk acilista gorunur.
442
+ // - Diskte bulunamayan cozulmus yol -> kaldirilan CLI hemen fark edilir.
443
+ // - opencode -> model listesi/ready durumu degisebilir;
444
+ // her acilista tam yoklanir, davranisi aynen korunur.
445
+ // "Yeniden Tara" (/api/cli/discover) daima force ile cagirir ve onbellegi tumden atlar.
446
+ const DISCOVERY_CACHE_VERSION = 1;
447
+ const ALWAYS_PROBE = new Set(["opencode"]);
448
+
449
+ // probeCommand, CLI'yi PATH uzerinden buldugunda resolvedCommand'i CIPLAK AD olarak dondurur
450
+ // ("codex"), mutlak yol olarak degil. Duz fs.existsSync bu durumda daima false verir ve
451
+ // onbellek hic tutmaz. Bu yuzden ciplak adlar PATH (+ Windows'ta PATHEXT) uzerinde aranir:
452
+ // spawn maliyeti olmadan, birkac milisaniyede ve dogru sonucla.
453
+ function resolvesOnPath(command) {
454
+ if (!command) return false;
455
+ if (command.includes("/") || command.includes("\\")) return fs.existsSync(command);
456
+ const dirs = String(process.env.PATH || "").split(path.delimiter).filter(Boolean);
457
+ const exts = isWin ? String(process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
458
+ for (const dir of dirs) {
459
+ for (const ext of exts) {
460
+ try { if (fs.existsSync(path.join(dir, command + ext))) return true; } catch {}
461
+ }
462
+ }
463
+ return false;
464
+ }
465
+
466
+ function cachedProbeUsable(entry) {
467
+ return Boolean(entry && entry.installed && entry.resolvedCommand && resolvesOnPath(entry.resolvedCommand));
468
+ }
469
+
470
+ // OpenCode model listesi persist EDILMEZ; her acilista `opencode models` ile bellekten
471
+ // uretilir. O cagri acilista gecici olarak yavas/basarisiz olursa (soguk ilk cagri, auth
472
+ // oturmamis, ag) liste bos kalir ve panel model dropdown'i o process boyunca bos gorunur.
473
+ // codex icin codexModelCache ile cozulen bu kirilganligi opencode icin de kapatiriz: prob
474
+ // bos donerse son basarili liste (config'te saklanan) fallback olarak kullanilir. Fallback
475
+ // ayni zamanda OPEN_CODE_MODELS'i doldurur ki otomatik model secimi de calissin.
476
+ // settings: fallback yolunda da kullanicinin acik modeli / modelPreferences desenleri gecerli
477
+ // olmali; aksi halde bayat listeden varsayilan kurallarla secim yapilirdi.
478
+ function applyOpenCodeFallback(entry, fallback, settings = {}) {
479
+ if (!entry || entry.id !== "opencode" || !entry.installed) return entry;
480
+ if ((entry.models && entry.models.length) || !Array.isArray(fallback) || !fallback.length) return entry;
481
+ const models = fallback.map(String).filter(Boolean);
482
+ if (!models.length) return entry;
483
+ OPEN_CODE_MODELS = models.slice();
484
+ entry.models = models.slice();
485
+ entry.recommendedModel = String(settings.model || "").trim() || selectOpenCodeModel(models, settings);
486
+ entry.ready = Boolean(entry.recommendedModel);
487
+ entry.modelsFromCache = true;
488
+ if (entry.ready) entry.readinessError = "";
489
+ return entry;
490
+ }
491
+
492
+ function discoverInstalled(options = {}) {
493
+ const cache = options.cache?.version === DISCOVERY_CACHE_VERSION ? options.cache.results || {} : {};
494
+ const force = options.force === true;
495
+ const openCodeFallback = Array.isArray(options.openCodeModels) ? options.openCodeModels : [];
496
+ return Object.entries(DEFINITIONS).map(([id, definition]) => {
497
+ const cached = cache[id];
498
+ if (!force && !ALWAYS_PROBE.has(id) && cachedProbeUsable(cached)) {
499
+ RESOLVED.set(id, cached.resolvedCommand);
500
+ // Eski sema onbelleginde supportedFlags yok. Yoklama burada, HTTP dinleme baslamadan yapilir.
501
+ let supportedFlags = cached.supportedFlags;
502
+ if (!Array.isArray(supportedFlags) && definition.optionalFlags?.length) {
503
+ supportedFlags = probeFlagSupport(cached.resolvedCommand, definition.optionalFlags);
504
+ } else {
505
+ restoreFlagSupport(cached.resolvedCommand, definition.optionalFlags, supportedFlags);
506
+ }
507
+ return { id, ...definition, installed: true, version: cached.version || "kurulu", error: "", resolvedCommand: cached.resolvedCommand, fromCache: true, supportedFlags: supportedFlags || [] };
508
+ }
509
+ // cfg: model onerisi kullanicinin modelPreferences desenlerine gore uretilir.
510
+ // fallback: prob bos donerse son bilinen model listesi korunur.
511
+ const result = { id, ...definition, ...probe(id, definition, options.cfg) };
512
+ return id === "opencode"
513
+ ? applyOpenCodeFallback(result, openCodeFallback, (options.cfg && options.cfg.cliSettings && options.cfg.cliSettings.opencode) || {})
514
+ : result;
515
+ });
516
+ }
517
+
518
+ // Bir sonraki acilista kullanilacak onbellek gorununu uretir. Yalnizca kurulu ve yolu
519
+ // cozulmus girdiler saklanir; digerleri zaten her acilista yeniden yoklanir.
520
+ function discoveryCacheFrom(discovered) {
521
+ const results = {};
522
+ for (const cli of discovered || []) {
523
+ if (cli.installed && cli.resolvedCommand && !ALWAYS_PROBE.has(cli.id)) {
524
+ // supportedFlags saklanmazsa onbellekli acilista Gemini --skip-trust sessizce duserdi.
525
+ results[cli.id] = { installed: true, version: cli.version || "kurulu", resolvedCommand: cli.resolvedCommand, supportedFlags: cli.supportedFlags || [] };
526
+ }
527
+ }
528
+ return { version: DISCOVERY_CACHE_VERSION, checkedAt: new Date().toISOString(), results };
529
+ }
530
+
531
+ // Tek kaynak: hem gercek gorev calistirici (engine.runCli) hem saglik testi ve model
532
+ // listeleme ayni komut kurulumunu kullanir. Bare npm CLI adlarini where.exe ile fiziksel
533
+ // yola cevirmiyoruz; cmd.exe PATHEXT/PATH cozumlemesini Unicode olarak kendisi yapar.
534
+ function buildCommand(command, args) {
535
+ if (!isWin) return { file: command, args, shell: false };
536
+ const bare = !path.isAbsolute(command) && !/[\\/]/.test(command);
537
+ if (bare) {
538
+ // shell:true iken Node argumanlari birlestirip cmd.exe'ye verir ve OTOMATIK TIRNAK KOYMAZ.
539
+ // Bosluk iceren yol argumanlari (or. "C:\Users\John Doe\...\x.settings.json" veya
540
+ // {PROMPT_FILE}) ikiye bolunurdu; bu da --settings/--file yolunu bozardi. Bosluk veya
541
+ // kabuk metakarakteri iceren argumanlari tirnakla; sade bayrak/model adlari dokunulmaz.
542
+ const shellQuote = (value) => {
543
+ const v = String(value);
544
+ return /[\s"^&|<>()%!]/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
545
+ };
546
+ return { file: command, args: args.map(shellQuote), shell: true };
547
+ }
548
+ if (!/\.(cmd|bat)$/i.test(command)) return { file: command, args, shell: false };
549
+ const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
550
+ return { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", `"${[command, ...args].map(quote).join(" ")}"`], shell: false, verbatim: true };
551
+ }
552
+
553
+ function healthPrompt(id) {
554
+ return [
555
+ "CLI_TEAM_HEALTH_CHECK",
556
+ `Bu ${id} cli aracinin kullanilabilirlik testidir.`,
557
+ "Yalnizca HEALTH_OK yaz.",
558
+ "Dosya olusturma, degistirme, silme, komut calistirma veya dis sisteme istek gonderme.",
559
+ ].join("\n");
560
+ }
561
+
562
+ // Normal gorev calistiricisi {PROMPT}/{PROMPT_FILE} yer tutucularini cagridan once
563
+ // doldurur. Health check de ayni sozlesmeyi kullanmali; aksi halde OpenCode literal
564
+ // `{PROMPT_FILE}` yolunu acmaya calisir ve saglikli agent katalogdan yanlislikla elenir.
565
+ function preparePromptArgs(args, prompt) {
566
+ let tempDir = "", promptFile = "", useStdin = true;
567
+ const prepared = (Array.isArray(args) ? args : []).map((arg) => {
568
+ const value = String(arg);
569
+ if (value.includes("{PROMPT}")) {
570
+ useStdin = false;
571
+ return value.replaceAll("{PROMPT}", prompt);
572
+ }
573
+ if (value.includes("{PROMPT_FILE}")) {
574
+ useStdin = false;
575
+ if (!promptFile) {
576
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cli-team-health-"));
577
+ promptFile = path.join(tempDir, "prompt.md");
578
+ fs.writeFileSync(promptFile, prompt, "utf8");
579
+ }
580
+ return value.replaceAll("{PROMPT_FILE}", promptFile);
581
+ }
582
+ return value;
583
+ });
584
+ return {
585
+ args: prepared,
586
+ useStdin,
587
+ promptFile,
588
+ cleanup() { if (tempDir) { try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {} } },
589
+ };
590
+ }
591
+
592
+ // Tek kaynak: OpenCode icin otonom izinler surumden bagimsiz olarak OPENCODE_CONFIG_CONTENT
593
+ // ile aktarilir. Hem engine.runCli hem saglik testi bu ortami kullanir ki test ile gercek
594
+ // calistirma ayni izinlerle calissin.
595
+ function agentEnvironment(agent) {
596
+ const env = { ...process.env };
597
+ if (agent.adapter !== "opencode") return env;
598
+ let inline = {};
599
+ try { inline = JSON.parse(env.OPENCODE_CONFIG_CONTENT || "{}"); } catch {}
600
+ env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ ...inline, permission: { "*": "allow" } });
601
+ return env;
602
+ }
603
+
604
+ function classifyHealthFailure(text) {
605
+ const raw = String(text || "");
606
+ 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 };
607
+ 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 };
608
+ if (/rate.?limit|quota|too many requests|billing|credit/i.test(raw)) return { status: "quota", label: "Kota veya ödeme gerekli", detail: raw };
609
+ if (/timeout|timed out|zaman aşım/i.test(raw)) return { status: "timeout", label: "Yanıt zaman aşımına uğradı", detail: raw };
610
+ return { status: "failed", label: "Test başarısız", detail: raw };
611
+ }
612
+
613
+ function testInstalledCli(id, options = {}) {
614
+ const definition = DEFINITIONS[id];
615
+ if (!definition) return Promise.resolve({ id, installed: false, health: { status: "unknown", label: "Bilinmiyor", detail: "Tanımsız CLI" } });
616
+ const found = probe(id, definition, options.cfg);
617
+ 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ı" } });
618
+ const agent = effectiveAgent({ adapter: id, cmd: definition.command, args: definition.defaultArgs.slice() }, options.cfg);
619
+ const prompt = healthPrompt(id);
620
+ const prepared = preparePromptArgs(agent.args, prompt);
621
+ const command = buildCommand(agent.cmd, prepared.args);
622
+ const timeoutMs = Math.max(10000, Number(options.timeoutMs || 45000));
623
+ return new Promise((resolve) => {
624
+ let stdout = "", stderr = "", settled = false;
625
+ let child;
626
+ try {
627
+ child = spawn(command.file, command.args, { env: agentEnvironment(agent), shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim });
628
+ } catch (error) {
629
+ prepared.cleanup();
630
+ return resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(error.message) });
631
+ }
632
+ const unregister = registerProbe(child);
633
+ const timer = setTimeout(() => {
634
+ if (settled) return;
635
+ settled = true;
636
+ unregister();
637
+ terminateProbeTree(child);
638
+ prepared.cleanup();
639
+ 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ı.` } });
640
+ }, timeoutMs);
641
+ child.stdout?.setEncoding("utf8");
642
+ child.stderr?.setEncoding("utf8");
643
+ child.stdout?.on("data", (data) => { stdout += String(data); });
644
+ child.stderr?.on("data", (data) => { stderr += String(data); });
645
+ child.on("error", (error) => {
646
+ if (settled) return;
647
+ settled = true;
648
+ clearTimeout(timer);
649
+ unregister();
650
+ prepared.cleanup();
651
+ const failure = classifyHealthFailure(error.message);
652
+ resolve({ id, installed: true, version: found.version, health: failure });
653
+ });
654
+ child.on("close", (code) => {
655
+ if (settled) return;
656
+ settled = true;
657
+ clearTimeout(timer);
658
+ unregister();
659
+ prepared.cleanup();
660
+ const raw = `${stdout}\n${stderr}`.trim();
661
+ 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ı." } });
662
+ else resolve({ id, installed: true, version: found.version, health: classifyHealthFailure(raw || `Çıkış kodu ${code}`) });
663
+ });
664
+ child.stdin?.on("error", () => {});
665
+ child.stdin?.end(prepared.useStdin ? prompt : "");
666
+ });
667
+ }
668
+
669
+ async function healthCheckAll(discovered = discoverInstalled(), options = {}) {
670
+ const results = await Promise.all(discovered.map((item) => testInstalledCli(item.id, options)));
671
+ return discovered.map((item) => ({ ...item, ...(results.find((result) => result.id === item.id) || {}) }));
672
+ }
673
+
674
+ function listCodexModels(options = {}) {
675
+ const command = buildCommand("codex", ["app-server", "--listen", "stdio://"]);
676
+ const timeoutMs = Math.max(5000, Number(options.timeoutMs || 20000));
677
+ return new Promise((resolve, reject) => {
678
+ let buffer = "", settled = false, modelRequestSent = false;
679
+ let child, unregister = () => {};
680
+ const finish = (error, value) => {
681
+ if (settled) return;
682
+ settled = true;
683
+ clearTimeout(timer);
684
+ unregister();
685
+ terminateProbeTree(child);
686
+ if (error) reject(error); else resolve(value);
687
+ };
688
+ const timer = setTimeout(() => finish(new Error(`Codex model listesi ${Math.round(timeoutMs / 1000)} saniyede alınamadı.`)), timeoutMs);
689
+ try { child = spawn(command.file, command.args, { shell: command.shell, windowsHide: true, windowsVerbatimArguments: !!command.verbatim }); }
690
+ catch (error) { finish(error); return; }
691
+ unregister = registerProbe(child);
692
+ child.stdout?.setEncoding("utf8");
693
+ child.stderr?.setEncoding("utf8");
694
+ child.stdout?.on("data", (chunk) => {
695
+ buffer += String(chunk);
696
+ const lines = buffer.split(/\r?\n/);
697
+ buffer = lines.pop() || "";
698
+ for (const line of lines) {
699
+ let message;
700
+ try { message = JSON.parse(line); } catch { continue; }
701
+ if (message.id === 1 && !modelRequestSent) {
702
+ modelRequestSent = true;
703
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "model/list", params: { includeHidden: false, limit: 200 } })}\n`);
704
+ } else if (message.id === 2) {
705
+ if (message.error) { finish(new Error(message.error.message || "Codex model listesi alınamadı.")); return; }
706
+ const models = Array.isArray(message.result?.data) ? message.result.data : [];
707
+ finish(null, models.filter((model) => model && model.hidden !== true).map((model) => ({
708
+ id: String(model.id || model.model || ""),
709
+ model: String(model.model || model.id || ""),
710
+ displayName: String(model.displayName || model.id || model.model || ""),
711
+ description: String(model.description || ""),
712
+ defaultReasoningEffort: String(model.defaultReasoningEffort || "medium"),
713
+ reasoningEfforts: (model.supportedReasoningEfforts || []).map((item) => ({
714
+ id: String(item.reasoningEffort || item.id || ""),
715
+ description: String(item.description || ""),
716
+ })).filter((item) => item.id),
717
+ serviceTiers: (model.serviceTiers || []).map((item) => ({
718
+ id: String(item.id || ""),
719
+ name: String(item.name || item.id || ""),
720
+ description: String(item.description || ""),
721
+ })).filter((item) => item.id),
722
+ additionalSpeedTiers: Array.isArray(model.additionalSpeedTiers) ? model.additionalSpeedTiers.map(String) : [],
723
+ defaultServiceTier: model.defaultServiceTier ? String(model.defaultServiceTier) : "",
724
+ isDefault: model.isDefault === true,
725
+ })).filter((model) => model.id));
726
+ }
727
+ }
728
+ });
729
+ child.stderr?.on("data", () => {});
730
+ child.on("error", finish);
731
+ child.stdin?.on("error", () => {});
732
+ child.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { clientInfo: { name: "crewctl", version: "1.0.0", title: "CrewCtl" } } })}\n`);
733
+ });
734
+ }
735
+
736
+ function addMissingAgents(cfg, discovered) {
737
+ cfg.agents ||= {};
738
+ const ignored = new Set(Array.isArray(cfg.discoveryIgnoredAdapters) ? cfg.discoveryIgnoredAdapters.map(String) : []);
739
+ let changed = normalizeAgentAdapters(cfg);
740
+ for (const cli of discovered) {
741
+ const usable = cli.installed && (cli.id !== "opencode" || cli.ready !== false || Boolean(cfg.cliSettings?.opencode?.model));
742
+ const matchingEntries = Object.entries(cfg.agents).filter(([, agent]) => (agent.adapter || adapterId(agent.cmd)) === cli.id);
743
+ const existingEntry = matchingEntries.find(([, agent]) => !agent.autoDiscovered) || matchingEntries[0];
744
+ const existing = existingEntry?.[1];
745
+ // Kullanici otomatik bulunan bir profili sildiyse bu karar kalicidir. O adapter'a ait
746
+ // elle olusturulmus profil varsa ona dokunma; yalnizca autoDiscovered kaydini temizle.
747
+ if (ignored.has(cli.id)) {
748
+ for (const [name, agent] of matchingEntries) {
749
+ if (!agent.autoDiscovered) continue;
750
+ delete cfg.agents[name];
751
+ changed = true;
752
+ }
753
+ continue;
754
+ }
755
+ if (existing) {
756
+ if (usable && existing.autoDiscovered && existing.unavailablePlaceholder) {
757
+ existing.enabled = true;
758
+ existing.args = cli.defaultArgs.slice();
759
+ delete existing.unavailablePlaceholder;
760
+ changed = true;
761
+ }
762
+ // Eski surumler otomatik oneriyi agent.model alanina yaziyordu. Bu alan global
763
+ // CLI ayarini ezdigi icin, kullanici tarafindan acikca override olarak isaretlenmemis
764
+ // otomatik profiller modeli daima CLI ayarindan/otomatik secimden miras alir.
765
+ if (cli.id === "opencode" && existing.autoDiscovered && existing.model && existing.modelOverride !== true) {
766
+ delete existing.model;
767
+ changed = true;
768
+ }
769
+ if (cli.installed && cli.id === "opencode" && existing.autoDiscovered && Number(existing.timeoutSeconds || 0) < cli.timeoutSeconds) {
770
+ existing.timeoutSeconds = cli.timeoutSeconds;
771
+ changed = true;
772
+ }
773
+ if (!usable && existing.autoDiscovered && existing.enabled !== false) {
774
+ existing.enabled = false;
775
+ existing.unavailablePlaceholder = true;
776
+ changed = true;
777
+ }
778
+ continue;
779
+ }
780
+ // OpenCode adapteri her cihazda katalogda gorunur. Kurulu degilse guvenli
781
+ // sekilde devre disi placeholder olur; taramada bulundugunda otomatik etkinlesir.
782
+ if (!cli.installed && cli.id !== "opencode") continue;
783
+ let name = `${cli.id}-auto`, suffix = 2;
784
+ while (cfg.agents[name]) name = `${cli.id}-auto-${suffix++}`;
785
+ cfg.agents[name] = {
786
+ adapter: cli.id,
787
+ cmd: cli.command,
788
+ args: cli.defaultArgs.slice(),
789
+ enabled: usable,
790
+ description: cli.description,
791
+ capabilities: cli.capabilities.slice(),
792
+ roleFile: cli.roleFile,
793
+ costTier: "standard",
794
+ timeoutSeconds: cli.timeoutSeconds || 1200,
795
+ ...(cli.silenceTimeoutSeconds ? { silenceTimeoutSeconds: cli.silenceTimeoutSeconds } : {}),
796
+ autoDiscovered: true,
797
+ ...(!usable ? { unavailablePlaceholder: true } : {}),
798
+ };
799
+ changed = true;
800
+ }
801
+ return changed;
802
+ }
803
+
804
+ // Operator, uzman agent'lardan BAGIMSIZ bir CLI secimidir (claude/codex/gemini/opencode).
805
+ // operator.md rolunu bu CLI giyer; uzman agent'lar operatorun ALTINDA delege edilir.
806
+ // Bir uzman agent silinse bile operator etkilenmez.
807
+ function operatorSpec(cli, cfg) {
808
+ const op = (cfg && cfg.operator) || {};
809
+ const definition = DEFINITIONS[cli];
810
+ // Standart CLI'lar DEFINITIONS'tan cozulur. cfg.operator.cmd verilirse (ozel/test operatoru)
811
+ // dogrudan o komut kullanilir; boylece operator, uzman agent'lardan tamamen bagimsiz kalir.
812
+ const cmd = op.cmd || (definition && definition.command);
813
+ if (!cmd) return null;
814
+ const args = op.cmd ? (Array.isArray(op.args) ? op.args.slice() : []) : (definition ? definition.defaultArgs.slice() : []);
815
+ // Model/effort/tier secimi CLI bazlidir (cfg.cliSettings) ve effectiveAgent icinde uygulanir;
816
+ // ayni ayar o CLI'yi kullanan operator ve tum uzman agent'lar icin gecerlidir.
817
+ const timeoutSeconds = op.timeoutSeconds || (cfg && cfg.agentTimeoutSeconds) || 900;
818
+ const silenceTimeoutSeconds = op.silenceTimeoutSeconds || (definition && definition.silenceTimeoutSeconds) || (cfg && cfg.cliSilenceTimeoutSeconds) || 300;
819
+ const adapter = definition ? cli : (op.adapter || adapterId(cmd));
820
+ return effectiveAgent({ adapter, cmd, args, timeoutSeconds, silenceTimeoutSeconds }, cfg);
821
+ }
822
+
823
+ // Operator gecerli, kurulu bir CLI olmali. Eski semada operator.agent (uzman agent adi)
824
+ // tutuluyordu; burada operator.cli'ye tasinir. Kurulu degilse ilk kurulu CLI'ye gecilir.
825
+ function ensureValidOperator(cfg, discovered) {
826
+ cfg.operator ||= {};
827
+ let changed = false;
828
+ if (cfg.operator.roleFile !== "roles/operator.md") { cfg.operator.roleFile = "roles/operator.md"; changed = true; }
829
+ if (!cfg.operator.cli) {
830
+ const legacy = cfg.operator.agent;
831
+ const source = legacy && cfg.agents?.[legacy] ? cfg.agents[legacy].cmd : legacy;
832
+ cfg.operator.cli = adapterId(source || "codex");
833
+ changed = true;
834
+ }
835
+ if ("agent" in cfg.operator) { delete cfg.operator.agent; changed = true; }
836
+ const installed = Array.isArray(discovered) ? discovered
837
+ .filter((x) => x.installed && (x.id !== "opencode" || x.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)))
838
+ .map((x) => x.id) : null;
839
+ const known = !!DEFINITIONS[cfg.operator.cli];
840
+ const availableOnHost = !installed || installed.length === 0 || installed.includes(cfg.operator.cli);
841
+ if (!known || !availableOnHost) {
842
+ const fallback = (installed && installed[0]) || Object.keys(DEFINITIONS)[0];
843
+ if (fallback && fallback !== cfg.operator.cli) { cfg.operator.cli = fallback; changed = true; }
844
+ }
845
+ return changed;
846
+ }
847
+
848
+ module.exports = { DEFINITIONS, KNOWN_CLIS: Object.keys(DEFINITIONS), adapterId, authHint, normalizeAgentAdapter, normalizeAgentAdapters, effectiveAgent, preparePromptArgs, operatorSpec, buildCommand, agentEnvironment, discoverInstalled, discoveryCacheFrom, applyOpenCodeFallback, healthCheckAll, listCodexModels, abortActiveProbes, currentProbeGeneration, selectOpenCodeModel, parseOpenCodeModels, addMissingAgents, ensureValidOperator };